Skip to content

Commit af0f74d

Browse files
authored
Merge pull request #829 from Extra-Chill/issue-823-safe-drain
Fix active no-signal drain budget continuations
2 parents e54e784 + cd7b88b commit af0f74d

2 files changed

Lines changed: 82 additions & 2 deletions

File tree

inc/Workspace/WorkspaceAbandonedCleanupOrchestrator.php

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,8 @@ public function run( array $input ): array|\WP_Error {
142142

143143
if ( $this->budget_expired($deadline) ) {
144144
$result['evidence']['budget_exhausted'] = true;
145+
$result['continuation'] = $this->build_budget_continuation($step_stage, 0, $limit, $passes, $force, $until_budget, $active_no_signal_drain, 'budget_exhausted_before_stage');
146+
$result['next_commands'][] = (string) $result['continuation']['next_command'];
145147
break 2;
146148
}
147149

@@ -182,13 +184,23 @@ public function run( array $input ): array|\WP_Error {
182184
if ( is_wp_error($bounded) ) {
183185
return $bounded;
184186
}
187+
if ( $this->budget_expired($deadline) ) {
188+
$result['evidence']['budget_exhausted'] = true;
189+
$result['continuation'] = $this->build_budget_continuation($default_stage, 0, $limit, $passes, $force, $until_budget, $active_no_signal_drain, 'budget_exhausted_after_bounded_apply');
190+
$result['next_commands'][] = (string) $result['continuation']['next_command'];
191+
break;
192+
}
185193

186194
$removed_or_would = (int) ( $bounded['summary']['removed'] ?? 0 ) + (int) ( $bounded['summary']['would_remove'] ?? 0 );
187195
if ( 0 === $pass_marked && 0 === $removed_or_would ) {
188196
break;
189197
}
190198
}
191199

200+
if ( ! empty($result['continuation']) ) {
201+
return $this->finalize_result($result, $apply, $force, $limit, $passes, $until_budget, $started_at, $active_no_signal_drain);
202+
}
203+
192204
if ( $apply ) {
193205
$prune = $this->execute_ability($abilities['prune'], array());
194206
if ( is_wp_error($prune) ) {
@@ -507,8 +519,7 @@ private function run_bounded_apply( object $ability, array &$result, bool $apply
507519
private function build_continuation( string $stage, array $step, int $limit, int $passes, bool $force, string $until_budget, bool $active_no_signal_drain = false ): array {
508520
$pagination = (array) ( $step['pagination'] ?? $step['continuation'] ?? array() );
509521
$next_offset = isset($pagination['next_offset']) ? max(0, (int) $pagination['next_offset']) : 0;
510-
$operation = $active_no_signal_drain ? 'active-no-signal-drain' : 'abandoned';
511-
$command = 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, $next_offset, $limit, $passes, '' !== $until_budget ? ' --until-budget=' . $until_budget : '');
522+
$command = $this->build_continuation_command($stage, $next_offset, $limit, $passes, $force, $until_budget, $active_no_signal_drain);
512523
$current = (int) ( $pagination['offset'] ?? 0 );
513524
$mutated = (int) ( $step['summary']['written'] ?? 0 ) + (int) ( $step['summary']['removed'] ?? 0 );
514525
$restart = ! empty($pagination['partial']) && $next_offset <= $current && $mutated > 0;
@@ -540,6 +551,22 @@ private function build_continuation( string $stage, array $step, int $limit, int
540551
return $continuation;
541552
}
542553

554+
private function build_budget_continuation( string $stage, int $offset, int $limit, int $passes, bool $force, string $until_budget, bool $active_no_signal_drain, string $reason ): array {
555+
return array(
556+
'stage' => $stage,
557+
'offset' => max(0, $offset),
558+
'next_command' => $this->build_continuation_command($stage, max(0, $offset), $limit, $passes, $force, $until_budget, $active_no_signal_drain),
559+
'reason' => $reason,
560+
'budget_exhausted' => true,
561+
'hint' => 'Budget expired after safe progress. Re-run next_command to continue the drain from the next safe boundary.',
562+
);
563+
}
564+
565+
private function build_continuation_command( string $stage, int $offset, int $limit, int $passes, bool $force, string $until_budget, bool $active_no_signal_drain ): string {
566+
$operation = $active_no_signal_drain ? 'active-no-signal-drain' : 'abandoned';
567+
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 : '');
568+
}
569+
543570
private function drain_pages( object $ability, array $base_input, bool $apply, ?float $deadline = null ): array|\WP_Error {
544571
$pages = array();
545572
$summary = array();

tests/smoke-abandoned-cleanup-orchestrator.php

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,4 +269,57 @@ static function () use ( &$clock_index, $clock_values ): float {
269269
abandoned_cleanup_assert(str_contains((string) $restart_result['continuation']['next_command_label'], 'candidate set changed'), 'restart continuation labels the restart command');
270270
abandoned_cleanup_assert('active-no-signal-drain' === explode(' ', (string) $restart_result['continuation']['next_command'])[5], 'restart next command uses active/no-signal drain');
271271

272+
$stage_budget_abilities = array(
273+
'datamachine-code/workspace-worktree-reconcile-metadata' => new AbandonedCleanupFakeAbility('reconcile_metadata', array(), array( 'complete' => true )),
274+
'datamachine-code/workspace-worktree-active-no-signal-finalized-apply' => new AbandonedCleanupFakeAbility('finalized', array( 'inspected' => 1, 'written' => 1 ), array( 'complete' => true )),
275+
'datamachine-code/workspace-worktree-active-no-signal-equivalent-clean-apply' => new AbandonedCleanupFakeAbility('equivalent_clean', array(), array( 'complete' => true )),
276+
'datamachine-code/workspace-worktree-active-no-signal-merged-apply' => new AbandonedCleanupFakeAbility('merged', array(), array( 'complete' => true )),
277+
'datamachine-code/workspace-worktree-active-no-signal-remote-clean-apply' => new AbandonedCleanupFakeAbility('remote_clean', array(), array( 'complete' => true )),
278+
'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 )),
279+
'datamachine-code/workspace-worktree-bounded-cleanup-eligible-apply' => new AbandonedCleanupFakeAbility('bounded', array( 'processed' => 0, 'removed' => 0 ), array( 'complete' => true )),
280+
'datamachine-code/workspace-worktree-prune' => new AbandonedCleanupFakeAbility('prune'),
281+
);
282+
$clock_index = 0;
283+
$clock_values = array( 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1002.0, 1002.0 );
284+
$orchestrator = new DataMachineCode\Workspace\WorkspaceAbandonedCleanupOrchestrator(
285+
static fn( string $name ) => $stage_budget_abilities[ $name ] ?? null,
286+
static function () use ( &$clock_index, $clock_values ): float {
287+
$value = $clock_values[ min($clock_index, count($clock_values) - 1) ];
288+
++$clock_index;
289+
return $value;
290+
}
291+
);
292+
$stage_budget_result = $orchestrator->run(array( 'active_no_signal_drain' => true, 'apply' => true, 'limit' => 10, 'passes' => 1, 'until_budget' => '1s' ));
293+
abandoned_cleanup_assert(! is_wp_error($stage_budget_result), 'active/no-signal stage budget result succeeds');
294+
abandoned_cleanup_assert('budget_exhausted_before_stage' === $stage_budget_result['continuation']['reason'], 'stage budget continuation explains boundary');
295+
abandoned_cleanup_assert('equivalent-clean' === $stage_budget_result['continuation']['stage'], 'stage budget continuation resumes at next safe stage');
296+
abandoned_cleanup_assert(0 === count($stage_budget_abilities['datamachine-code/workspace-worktree-prune']->calls), 'stage budget continuation skips prune after budget exhaustion');
297+
abandoned_cleanup_assert(str_contains((string) $stage_budget_result['continuation']['next_command'], '--stage=equivalent-clean --offset=0'), 'stage budget continuation command resumes exact safe boundary');
298+
299+
$bounded_budget_abilities = array(
300+
'datamachine-code/workspace-worktree-reconcile-metadata' => new AbandonedCleanupFakeAbility('reconcile_metadata', array(), array( 'complete' => true )),
301+
'datamachine-code/workspace-worktree-active-no-signal-finalized-apply' => new AbandonedCleanupFakeAbility('finalized', array(), array( 'complete' => true )),
302+
'datamachine-code/workspace-worktree-active-no-signal-equivalent-clean-apply' => new AbandonedCleanupFakeAbility('equivalent_clean', array(), array( 'complete' => true )),
303+
'datamachine-code/workspace-worktree-active-no-signal-merged-apply' => new AbandonedCleanupFakeAbility('merged', array(), array( 'complete' => true )),
304+
'datamachine-code/workspace-worktree-active-no-signal-remote-clean-apply' => new AbandonedCleanupFakeAbility('remote_clean', array(), array( 'complete' => true )),
305+
'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 )),
306+
'datamachine-code/workspace-worktree-bounded-cleanup-eligible-apply' => new AbandonedCleanupFakeAbility('bounded', array( 'processed' => 1, 'removed' => 1 ), array( 'complete' => true )),
307+
'datamachine-code/workspace-worktree-prune' => new AbandonedCleanupFakeAbility('prune'),
308+
);
309+
$clock_index = 0;
310+
$orchestrator = new DataMachineCode\Workspace\WorkspaceAbandonedCleanupOrchestrator(
311+
static fn( string $name ) => $bounded_budget_abilities[ $name ] ?? null,
312+
static function () use ( &$clock_index ): float {
313+
$value = $clock_index < 14 ? 1000.0 : 1002.0;
314+
++$clock_index;
315+
return $value;
316+
}
317+
);
318+
$bounded_budget_result = $orchestrator->run(array( 'active_no_signal_drain' => true, 'apply' => true, 'limit' => 10, 'passes' => 2, 'until_budget' => '1s' ));
319+
abandoned_cleanup_assert(! is_wp_error($bounded_budget_result), 'active/no-signal bounded budget result succeeds');
320+
abandoned_cleanup_assert('budget_exhausted_after_bounded_apply' === $bounded_budget_result['continuation']['reason'], 'bounded budget continuation explains boundary');
321+
abandoned_cleanup_assert('finalized' === $bounded_budget_result['continuation']['stage'], 'bounded budget continuation resumes as full safe drain');
322+
abandoned_cleanup_assert(0 === count($bounded_budget_abilities['datamachine-code/workspace-worktree-prune']->calls), 'bounded budget continuation skips prune after budget exhaustion');
323+
abandoned_cleanup_assert(str_contains((string) $bounded_budget_result['continuation']['hint'], 'Re-run next_command'), 'bounded budget continuation has operator hint');
324+
272325
fwrite(STDOUT, 'abandoned cleanup orchestrator smoke passed' . PHP_EOL);

0 commit comments

Comments
 (0)