-
Notifications
You must be signed in to change notification settings - Fork 5
Expand file tree
/
Copy pathDropOutEnrollments.php
More file actions
171 lines (148 loc) · 6.45 KB
/
DropOutEnrollments.php
File metadata and controls
171 lines (148 loc) · 6.45 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
<?php
namespace App\Console\Commands;
use App\Models\Activity;
use App\Models\Enrollment;
use App\Models\Exam;
use App\Models\Submission;
use Carbon\Carbon;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Symfony\Component\Stopwatch\Stopwatch;
class DropOutEnrollments extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'enrollments:dropout';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Dropout enrollments on specified date.';
public function __construct(
private readonly Stopwatch $stopwatch,
) {
parent::__construct();
}
/**
* Execute the console command.
*/
public function handle()
{
try {
DB::beginTransaction();
// Retrieve the latest deadline (adjust as needed)
$deadline = Carbon::parse(Enrollment::latest('id')->value('deadline_at'));
$this->stopwatch->start(__CLASS__);
// Call the optimized dropout process.
$this->dropOutEnrollmentsBefore($deadline);
$event = $this->stopwatch->stop(__CLASS__);
$this->info($this->stopwatch->getEvent(__CLASS__));
// Note: Rollback is used here for testing/dry-run purposes.
DB::rollBack();
} catch (\Exception $e) {
DB::rollBack();
throw $e;
}
}
/**
*Optimized dropout process.
*
* Optimization Documentation:
*
* - **Step 1: Select Only Required Columns**
* - *What:* Instead of retrieving full model data, we select only 'id', 'course_id', and 'student_id'.
* - *Why:* Reduces memory usage.
* - *How:* Use ->select('id', 'course_id', 'student_id').
*
* - **Step 2: Bulk Fetch Related Records with Composite Keys**
* - *What:* Use selectRaw to retrieve composite keys (course_id-student_id) for Exams and Submissions.
* - *Why:* Minimizes the data loaded from the database.
* - *How:* Use ->selectRaw("CONCAT(course_id, '-', student_id) as composite_key")->distinct()->pluck('composite_key').
*
* - **Step 3: Cache Timestamp per Chunk**
* - *What:* Call now() only once per chunk.
* - *Why:* Reduces function call overhead.
* - *How:* Store the result in a $now variable.
*
* - **Step 4: Use Chunking, and Bulk Update & Insert**
* - *What:* Process enrollments in chunks, then update and insert in bulk.
* - *Why:* Prevents high memory usage and reduces database roundtrips.
* - *How:* Use chunkById(1000) with DB::table()->whereIn()->update() and DB::table()->insert().
*
* @param Carbon $deadline
*/
private function dropOutEnrollmentsBefore(Carbon $deadline)
{
$this->info('Starting dropout process...');
$totalDropped = 0;
$totalChecked = 0;
// Process enrollments in chunks (only select needed columns)
Enrollment::select('id', 'course_id', 'student_id')
->where('deadline_at', '<=', $deadline)
->chunkById(1000, function ($enrollments) use (&$totalDropped, &$totalChecked) {
// Extract unique course_ids and student_ids from the current chunk.
$courseIds = $enrollments->pluck('course_id')->unique()->toArray();
$studentIds = $enrollments->pluck('student_id')->unique()->toArray();
// Build lookup for active exams using composite keys.
$activeExamKeys = Exam::selectRaw("CONCAT(course_id, '-', student_id) as composite_key")
->whereIn('course_id', $courseIds)
->whereIn('student_id', $studentIds)
->where('status', 'IN_PROGRESS')
->distinct()
->pluck('composite_key')
->toArray();
$activeExamLookup = array_flip($activeExamKeys);
// Build lookup for waiting submissions using composite keys.
$waitingSubmissionKeys = Submission::selectRaw("CONCAT(course_id, '-', student_id) as composite_key")
->whereIn('course_id', $courseIds)
->whereIn('student_id', $studentIds)
->where('status', 'WAITING_REVIEW')
->distinct()
->pluck('composite_key')
->toArray();
$waitingSubmissionLookup = array_flip($waitingSubmissionKeys);
// Prepare arrays for bulk update and bulk insert.
$enrollmentIdsToDrop = [];
$activityLogs = [];
$now = now(); // Cache current timestamp for the entire chunk
foreach ($enrollments as $enrollment) {
$totalChecked++;
$key = $enrollment->course_id . '-' . $enrollment->student_id;
// Skip enrollment if it has an active exam or waiting submission.
if (isset($activeExamLookup[$key]) || isset($waitingSubmissionLookup[$key])) {
continue;
}
$enrollmentIdsToDrop[] = $enrollment->id;
$activityLogs[] = [
'resource_id' => $enrollment->id,
'user_id' => $enrollment->student_id,
'description' => 'COURSE_DROPOUT',
'created_at' => $now,
'updated_at' => $now,
];
$totalDropped++;
}
// Bulk update enrollments that qualify for dropout.
if (!empty($enrollmentIdsToDrop)) {
DB::table('enrollments')
->whereIn('id', $enrollmentIdsToDrop)
->update([
'status' => 'DROPOUT',
'updated_at' => $now,
]);
}
// Bulk insert all the activity log records.
if (!empty($activityLogs)) {
DB::table('activities')->insert($activityLogs);
}
});
// Output process statistics.
$this->info("Enrollments to be dropped out: $totalChecked");
$this->info("Excluded from drop out: " . ($totalChecked - $totalDropped));
$this->info("Final dropped out enrollments: $totalDropped");
}
}