Fix HPPS migrations for restricted hosting environments#7899
Conversation
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR makes HPPS (High-Performance Progress Storage) migrations compatible with restricted hosting environments (like WP.com Atomic) where set_time_limit() is disabled. The changes implement time-budgeted processing, reduce batch sizes to work within normal PHP execution limits, and add retry logic for transient failures.
Changes:
- Implemented time-budgeted migration processing with 80% threshold and default 20-second budget
- Reduced migration batch sizes: Student Progress from 1000 to 250 rows/run (50×5), Quiz from 100 to 50 submissions/run
- Added retry logic with default 3 attempts before marking migrations as failed
- Removed Settings UI restrictions requiring
set_time_limit()support - Fixed typo in constant name with backward-compatible alias
Reviewed changes
Copilot reviewed 11 out of 12 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
includes/internal/migration/class-migration-abstract.php |
Added time budget tracking with set_time_budget() and is_time_exceeded() methods using 80% threshold |
includes/internal/migration/class-migration-job.php |
Added set_time_budget() method to delegate budget setting to underlying migration |
includes/internal/migration/class-migration-job-scheduler.php |
Removed set_time_limit(0), added retry logic with configurable max retries, applies time budget filter to jobs |
includes/internal/migration/migrations/class-student-progress-migration.php |
Reduced batch size/count defaults, added filterable batch configuration, checks time budget in insert_with_batches(), fixed typo with backward compatibility |
includes/internal/migration/migrations/class-quiz-migration.php |
Reduced batch size default, added filterable batch size, checks time budget in main processing loop |
includes/class-sensei-settings.php |
Removed set_time_limit() capability check that previously disabled HPPS feature on restricted hosts |
tests/unit-tests/internal/migration/test-class-migration-abstract.php |
New test file covering time budget functionality |
tests/unit-tests/internal/migration/test-class-migration-job.php |
Added test for time budget delegation |
tests/unit-tests/internal/migration/test-class-migration-job-scheduler.php |
Added tests for time budget application, retry logic, and retry count cleanup |
tests/unit-tests/internal/migration/migrations/test-class-student-progress-migration.php |
Added tests for reduced batch sizes, filterable batch configuration, and time budget behavior |
tests/unit-tests/internal/migration/migrations/test-class-quiz-migration.php |
Added tests for reduced batch size, filterable batch size, and time budget behavior |
.gitignore |
Added .worktrees for git worktree support |
Comments suppressed due to low confidence (1)
includes/internal/migration/migrations/class-quiz-migration.php:101
- The return value documentation states "The number of quiz submissions migrated" but line 101 returns
count( $comments )which is the number of comments processed, not necessarily the number of quiz submissions successfully migrated. When a comment has no associated quiz (line 81-84), it's counted in the returned value even though no submission was migrated. Line 94 returns$processedwhich has the same issue.
Consider either updating the documentation to say "The number of comments processed" or tracking and returning the actual count of successfully migrated submissions.
* @return int The number of quiz submissions migrated.
*/
public function run( bool $dry_run = true ) {
$this->dry_run = $dry_run;
$comments = $this->get_comments();
if ( ! $comments ) {
return 0;
}
$quiz_data = $this->get_quiz_data( $comments );
$processed = 0;
foreach ( $comments as $comment ) {
$submission_id = $this->insert_quiz_submission( $comment, $quiz_data );
if ( ! $submission_id ) {
++$processed;
continue;
}
$answer_ids = $this->insert_quiz_answers( $comment, $quiz_data, $submission_id );
$this->insert_quiz_grades( $comment, $quiz_data, $answer_ids );
++$processed;
// Update cursor and stop if time budget exceeded.
if ( $this->is_time_exceeded() && $processed < count( $comments ) ) {
update_option( self::LAST_COMMENT_ID_OPTION_NAME, $comment->comment_ID );
return $processed;
}
}
$last_comment_id = end( $comments )->comment_ID;
update_option( self::LAST_COMMENT_ID_OPTION_NAME, $last_comment_id );
return count( $comments );
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When insert_with_batches() exits early due to time budget, the cursor was still set to the last fetched comment ID, skipping rows that were never inserted. Now the cursor only advances if the time budget has not been exceeded. INSERT IGNORE handles duplicates on re-run safely. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 12 out of 13 changed files in this pull request and generated no new comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Remove tests for batch size defaults, batch size filters, and backward compat constant alias. Add descriptive messages to time budget assertions. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Instead of not advancing the cursor when time is exceeded, track the last fully processed comment ID and always advance to it. This prevents infinite rescheduling when a batch consistently exceeds the time budget. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
6fb7fac to
0263ca8
Compare
Replace the bulk prepare-then-insert approach with per-comment processing, matching the pattern already used by Quiz_Migration. Each comment is fully processed before the time budget is checked, guaranteeing forward progress by construction without needing special-case logic for stalled runs. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Accumulate rows from multiple comments into a buffer and flush with a single multi-row INSERT IGNORE when the buffer reaches insert_batch_size (default 50). This keeps per-comment cursor tracking and time-budget support while restoring the performance of batched inserts (~2.3x faster than per-comment inserts at 250 users / 1200 rows). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
fdc0f29 to
6b60f90
Compare
Return the number of comments processed from Student_Progress_Migration::run() instead of the number of rows inserted by INSERT IGNORE. This prevents the migration scheduler from marking the migration as complete when a crash occurs between the INSERT and the cursor update, causing all re-processed rows to be duplicates (INSERT IGNORE returns 0). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The last_prepared_id and last_processed_id distinction was unnecessary since the cursor is only ever updated after a flush. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The variable was called $rows_inserted but the migration's run() method returns the number of comments processed, not rows inserted. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Remove unused $trashed_post_ids variable and fix docstring that referenced a non-existent method. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Move $last_processed_id assignment inside the foreach loop body so it is set on every iteration, eliminating the post-loop reference to the foreach variable $progress_comment that Psalm flagged as possibly undefined. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
@merkushin As we chatted, here's the first PR. 🙂 |
| */ | ||
| $this->batch_size = (int) apply_filters( 'sensei_migration_student_progress_batch_size', $batch_size ); | ||
|
|
||
| $this->insert_batch_size = $insert_batch_size; |
There was a problem hiding this comment.
Adding another filter here might be useful too.
Or maybe they (read batch size and insert batch size) could be combined into a single filter?
There was a problem hiding this comment.
The insert batch size is an internal implementation detail — it just controls how many rows are buffered before flushing a multi-row INSERT. The default of 50 is reasonable and there's no realistic scenario where a site admin would need to change it independently.
If someone needs to reduce memory pressure, they'd lower the read batch size instead. That doesn't change the insert buffer threshold, but it reduces the total number of rows flowing through the pipeline — e.g. reading 50 comments instead of 250 means fewer rows are produced overall, so each flush is naturally smaller even though the buffer size stays at 50.
I don't think adding a filter or combining the two adds value here — it would just expose plumbing that nobody should need to touch.
There was a problem hiding this comment.
A very Claude-esque answer, but one that makes sense to me. WDYT?
There was a problem hiding this comment.
Haha, both values come from the open interface of the class, so it's a bit slick to say one is part of the internal logic :)
If someone wants to have full control over the process, having the batch size filter would be really good. It might help to address this restriction on shared hostings: https://dev.mysql.com/doc/refman/8.4/en/packet-too-large.html
At the same time, we had none of them before 😅
- Rename $batch_size to $read_batch_size in Student_Progress_Migration - Use past tense in test method names for duplicate-row conditions - Replace @SInCE 4.26.0 with $$next-version$$ placeholder Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
mdawaffe
left a comment
There was a problem hiding this comment.
The approach looks nice to me. I don't know how to test everything :) but I'll look into that more tomorrow.
Add an apply_filters call for insert_batch_size so site admins can control the multi-row INSERT size (e.g. on hosts with a small max_allowed_packet). Also rename the read filter from sensei_migration_student_progress_batch_size to sensei_migration_student_progress_read_batch_size for consistency. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Summary
set_time_limit(0)from migration scheduler and replace with time-budgeted processing (default 20s, filterable)set_time_limitis disabledLARST_COMMENT_ID_OPTION_NAME→LAST_COMMENT_ID_OPTION_NAMENote: I'm fixing up the PHP unit test jobs in a separate PR, so you can ignore any jobs that didn't complete.
Context
HPPS (High-Performance Progress Storage) Stage 4 — Phase 1. Migrations previously required
set_time_limit(0)which fails on WP.com Atomic and restricted hosting environments. This PR makes migrations work within normal PHP execution limits by introducing a time budget that allows migrations to stop mid-batch and safely resume on the next run.New filters
sensei_migration_time_budget(float, default 20.0) — seconds per migration runsensei_migration_max_retries(int, default 3) — retry attempts before failingsensei_migration_student_progress_read_batch_size(int, default 250) — comments fetched per runsensei_migration_student_progress_insert_batch_size(int, default 50) — comments inserted per runsensei_migration_quiz_batch_size(int, default 50) — quiz submissions fetched per runTest plan