Skip to content

Commit 98cd93e

Browse files
Stop active no-signal drain on zero-yield pages (#851)
* Stop active no-signal drain on zero-yield pages * Fix active no-signal drain lint --------- Co-authored-by: homeboy-ci[bot] <266378653+homeboy-ci[bot]@users.noreply.github.com>
1 parent 05c3022 commit 98cd93e

2 files changed

Lines changed: 70 additions & 4 deletions

File tree

inc/Workspace/WorkspaceAbandonedCleanupOrchestrator.php

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ public function run( array $input ): array|\WP_Error {
154154
'offset' => $step_stage === $stage ? $offset : 0,
155155
)
156156
);
157-
$step = $this->drain_pages($step_config['ability'], $step_input, $apply, $deadline);
157+
$step = $this->drain_pages($step_config['ability'], $step_input, $apply, $deadline, $active_no_signal_drain);
158158
if ( is_wp_error($step) ) {
159159
return $step;
160160
}
@@ -174,8 +174,12 @@ public function run( array $input ): array|\WP_Error {
174174
return $bounded;
175175
}
176176
$result['evidence']['budget_exhausted'] = $this->budget_expired($deadline);
177-
$result['continuation'] = $this->build_continuation($step_stage, $step, $limit, $passes, $force, $until_budget, $active_no_signal_drain);
178-
$result['next_commands'][] = (string) $result['continuation']['next_command'];
177+
if ( ! empty($step['adaptive_stop']) && is_array($step['adaptive_stop']) ) {
178+
$result['evidence']['adaptive_stop'] = $step['adaptive_stop'];
179+
$result['summary']['stop_reason'] = (string) ( $step['adaptive_stop']['reason'] ?? 'no_progress_in_stage' );
180+
}
181+
$result['continuation'] = $this->build_continuation($step_stage, $step, $limit, $passes, $force, $until_budget, $active_no_signal_drain);
182+
$result['next_commands'][] = (string) $result['continuation']['next_command'];
179183
break 2;
180184
}
181185
}
@@ -524,6 +528,7 @@ private function build_continuation( string $stage, array $step, int $limit, int
524528
$restart = ! empty($pagination['partial']) && $next_offset <= $current && $mutated > 0;
525529
$command_offset = $restart ? 0 : $next_offset;
526530
$command = $this->build_continuation_command($stage, $command_offset, $limit, $passes, $force, $until_budget, $active_no_signal_drain);
531+
$adaptive = (array) ( $step['adaptive_stop'] ?? array() );
527532

528533
$continuation = array(
529534
'stage' => $stage,
@@ -549,6 +554,13 @@ private function build_continuation( string $stage, array $step, int $limit, int
549554
);
550555
$continuation['next_command_label'] = 'Restart this stage from offset 0 because the cleanup candidate set changed.';
551556
}
557+
if ( ! empty($adaptive) ) {
558+
$continuation['reason'] = (string) ( $adaptive['reason'] ?? 'no_progress_in_stage' );
559+
$continuation['reason_description'] = (string) ( $adaptive['reason_description'] ?? 'The previous stage page scanned rows but produced no cleanup mutations, so the drain stopped before spending more budget on low-yield pages.' );
560+
$continuation['recommendation'] = (string) ( $adaptive['recommendation'] ?? 'Stop this drain for now, or run next_command to continue this stage from the next page if you want a deeper scan.' );
561+
$continuation['progress_delta'] = (array) ( $adaptive['progress_delta'] ?? array() );
562+
$continuation['next_command_label'] = 'Continue this stage despite the no-progress adaptive stop.';
563+
}
552564

553565
return $continuation;
554566
}
@@ -569,7 +581,7 @@ private function build_continuation_command( string $stage, int $offset, int $li
569581
return sprintf('studio wp datamachine-code workspace worktree %s --apply%s --stage=%s --offset=%d --limit=%d --passes=%d%s --format=json', $operation, $force ? ' --force' : '', $stage, $offset, $limit, $passes, '' !== $until_budget ? ' --until-budget=' . $until_budget : '');
570582
}
571583

572-
private function drain_pages( object $ability, array $base_input, bool $apply, ?float $deadline = null ): array|\WP_Error {
584+
private function drain_pages( object $ability, array $base_input, bool $apply, ?float $deadline = null, bool $stop_on_no_progress = false ): array|\WP_Error {
573585
$pages = array();
574586
$summary = array();
575587
$pagination = array();
@@ -609,6 +621,7 @@ private function drain_pages( object $ability, array $base_input, bool $apply, ?
609621

610622
$next_offset = isset($pagination['next_offset']) ? (int) $pagination['next_offset'] : null;
611623
$mutation_count = (int) ( $result['summary']['written'] ?? 0 ) + (int) ( $result['summary']['removed'] ?? 0 );
624+
$inspected = (int) ( $result['summary']['inspected'] ?? $result['summary']['processed'] ?? 0 );
612625
if ( null === $next_offset || ( $next_offset <= $offset && ( $mutation_count <= 0 || empty($pagination['partial']) ) ) ) {
613626
break;
614627
}
@@ -618,6 +631,23 @@ private function drain_pages( object $ability, array $base_input, bool $apply, ?
618631
break;
619632
}
620633

634+
if ( $stop_on_no_progress && $mutation_count <= 0 && $inspected > 0 ) {
635+
$last_result['adaptive_stop'] = array(
636+
'reason' => 'no_progress_in_stage',
637+
'reason_description' => 'This stage scanned a page and produced no cleanup metadata writes or removals, so the drain stopped before spending more budget on low-yield pages.',
638+
'recommendation' => 'Stop this drain for now, or run next_command to continue this stage from the next page if you want a deeper scan.',
639+
'progress_delta' => array(
640+
'inspected' => $inspected,
641+
'written' => (int) ( $result['summary']['written'] ?? 0 ),
642+
'removed' => (int) ( $result['summary']['removed'] ?? 0 ),
643+
'total_mutations' => $mutation_count,
644+
'previous_offset' => $offset,
645+
'next_offset' => $next_offset,
646+
),
647+
);
648+
break;
649+
}
650+
621651
$offset = $next_offset;
622652
}
623653

tests/smoke-abandoned-cleanup-orchestrator.php

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -326,4 +326,40 @@ static function () use ( &$clock_index ): float {
326326
abandoned_cleanup_assert(0 === count($bounded_budget_abilities['datamachine-code/workspace-worktree-prune']->calls), 'bounded budget continuation skips prune after budget exhaustion');
327327
abandoned_cleanup_assert(str_contains((string) $bounded_budget_result['continuation']['hint'], 'Re-run next_command'), 'bounded budget continuation has operator hint');
328328

329+
$zero_yield_finalized = new AbandonedCleanupQueuedAbility(
330+
array(
331+
array(
332+
'mode' => 'finalized',
333+
'summary' => array( 'inspected' => 10, 'written' => 0 ),
334+
'pagination' => array( 'offset' => 0, 'limit' => 10, 'scanned' => 10, 'partial' => true, 'complete' => false, 'next_offset' => 10, 'total' => 30 ),
335+
),
336+
)
337+
);
338+
$zero_yield_equivalent = new AbandonedCleanupFakeAbility('equivalent_clean', array( 'inspected' => 10, 'written' => 1 ), array( 'complete' => true ));
339+
$zero_yield_abilities = array(
340+
'datamachine-code/workspace-worktree-reconcile-metadata' => new AbandonedCleanupFakeAbility('reconcile_metadata', array(), array( 'complete' => true )),
341+
'datamachine-code/workspace-worktree-active-no-signal-finalized-apply' => $zero_yield_finalized,
342+
'datamachine-code/workspace-worktree-active-no-signal-equivalent-clean-apply' => $zero_yield_equivalent,
343+
'datamachine-code/workspace-worktree-active-no-signal-merged-apply' => new AbandonedCleanupFakeAbility('merged', array(), array( 'complete' => true )),
344+
'datamachine-code/workspace-worktree-active-no-signal-remote-clean-apply' => new AbandonedCleanupFakeAbility('remote_clean', array(), array( 'complete' => true )),
345+
'datamachine-code/workspace-worktree-active-no-signal-report' => new AbandonedCleanupFakeAbility('active_no_signal_report', array( 'total_active_no_signal' => 0, 'inspected' => 0, 'by_suggested_action' => array() ), array( 'complete' => true, 'total' => 0 )),
346+
'datamachine-code/workspace-worktree-bounded-cleanup-eligible-apply' => new AbandonedCleanupFakeAbility('bounded', array( 'processed' => 0, 'removed' => 0 ), array( 'complete' => true )),
347+
'datamachine-code/workspace-worktree-prune' => new AbandonedCleanupFakeAbility('prune'),
348+
);
349+
$orchestrator = new DataMachineCode\Workspace\WorkspaceAbandonedCleanupOrchestrator(
350+
static fn( string $name ) => $zero_yield_abilities[ $name ] ?? null,
351+
static fn(): float => 1000.0
352+
);
353+
$zero_yield_result = $orchestrator->run(array( 'active_no_signal_drain' => true, 'apply' => true, 'limit' => 10, 'passes' => 3, 'until_budget' => '300s' ));
354+
abandoned_cleanup_assert(! is_wp_error($zero_yield_result), 'active/no-signal zero-yield result succeeds');
355+
abandoned_cleanup_assert('no_progress_in_stage' === $zero_yield_result['continuation']['reason'], 'zero-yield continuation uses no-progress reason');
356+
abandoned_cleanup_assert(false === (bool) ( $zero_yield_result['evidence']['budget_exhausted'] ?? true ), 'zero-yield stop is distinct from budget exhaustion');
357+
abandoned_cleanup_assert('no_progress_in_stage' === ( $zero_yield_result['summary']['stop_reason'] ?? null ), 'zero-yield summary exposes stop reason');
358+
abandoned_cleanup_assert(10 === (int) ( $zero_yield_result['continuation']['progress_delta']['inspected'] ?? 0 ), 'zero-yield continuation exposes inspected count');
359+
abandoned_cleanup_assert(0 === (int) ( $zero_yield_result['continuation']['progress_delta']['total_mutations'] ?? -1 ), 'zero-yield continuation exposes zero mutations');
360+
abandoned_cleanup_assert(str_contains((string) $zero_yield_result['continuation']['recommendation'], 'Stop this drain'), 'zero-yield continuation recommends stopping');
361+
abandoned_cleanup_assert(str_contains((string) $zero_yield_result['continuation']['next_command'], '--stage=finalized --offset=10'), 'zero-yield continuation resumes same stage next page');
362+
abandoned_cleanup_assert(0 === count($zero_yield_equivalent->calls), 'zero-yield stop avoids advancing into later active/no-signal stages');
363+
abandoned_cleanup_assert(0 === count($zero_yield_abilities['datamachine-code/workspace-worktree-prune']->calls), 'zero-yield stop skips prune while continuation remains');
364+
329365
fwrite(STDOUT, 'abandoned cleanup orchestrator smoke passed' . PHP_EOL);

0 commit comments

Comments
 (0)