Skip to content

Commit 97cb7b3

Browse files
authored
Clarify cleanup chunk drain progress (#667)
1 parent ef1bb7a commit 97cb7b3

3 files changed

Lines changed: 264 additions & 3 deletions

File tree

inc/Cleanup/DataMachineJobCleanupRunEvidenceStore.php

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ public function read( string $run_id, bool $include_evidence = false, bool $incl
5151
'artifact_cleanup' => $aggregate['artifact_cleanup'],
5252
'cleanup_items' => $aggregate['cleanup_items'],
5353
'remaining_work_summary' => CleanupRemainingWorkSummary::from_job_aggregate($aggregate),
54+
'drain' => $this->cleanup_run_drain_summary($job_id, $state, $children, $aggregate),
5455
'children' => $children_for_output,
5556
);
5657

@@ -231,6 +232,48 @@ private function summarize_cleanup_children( array $children ): array {
231232
);
232233
}
233234

235+
/**
236+
* Build exact Data Machine drain and verification commands for a cleanup run.
237+
*
238+
* @param int $job_id Parent cleanup job ID.
239+
* @param string $state Computed cleanup state.
240+
* @param array $children Full child job aggregate.
241+
* @param array $aggregate Full cleanup aggregate.
242+
* @return array<string,mixed>
243+
*/
244+
private function cleanup_run_drain_summary( int $job_id, string $state, array $children, array $aggregate ): array {
245+
$active_child_ids = array_values(
246+
array_unique(
247+
array_filter(
248+
array_map(
249+
'intval',
250+
array_merge(
251+
(array) ( $children['pending_job_ids'] ?? array() ),
252+
(array) ( $children['processing_job_ids'] ?? array() )
253+
)
254+
)
255+
)
256+
)
257+
);
258+
$run_id = $this->cleanup_run_id($job_id);
259+
$cleanup_items = (array) ( $aggregate['cleanup_items'] ?? array() );
260+
$commands = array(
261+
'parent' => sprintf('studio wp datamachine drain --job-id=%d', $job_id),
262+
'verify' => sprintf('studio wp datamachine-code workspace cleanup status %s --format=json', $run_id),
263+
);
264+
if ( array() !== $active_child_ids ) {
265+
$commands['active_children'] = sprintf('studio wp datamachine drain --job-id=%s', implode(',', $active_child_ids));
266+
}
267+
268+
return array(
269+
'needed' => in_array($state, array( 'running', 'children_processing' ), true),
270+
'commands' => $commands,
271+
'active_child_job_ids' => $active_child_ids,
272+
'bytes_reclaimed' => (int) ( $cleanup_items['bytes_reclaimed'] ?? 0 ),
273+
'freed_human' => (string) ( $cleanup_items['freed_human'] ?? $this->format_bytes(0) ),
274+
);
275+
}
276+
234277
/**
235278
* Merge one chunk task result into aggregate cleanup item counters.
236279
*

inc/Cli/Commands/WorkspaceCommand.php

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -626,6 +626,10 @@ public function adopt_repo( array $args, array $assoc_args ): void {
626626
* [--verbose]
627627
* : Include full diagnostic child job ID lists in task-backed cleanup status output.
628628
*
629+
* [--drain]
630+
* : For `cleanup run`, drain the queued parent job, drain active child cleanup
631+
* jobs discovered from cleanup status, then print verified bytes reclaimed.
632+
*
629633
* [--format=<format>]
630634
* : Output format.
631635
* ---
@@ -750,9 +754,144 @@ private function run_cleanup_task( array $assoc_args ): void {
750754
return;
751755
}
752756

757+
$result = $this->attach_cleanup_run_commands($result, $mode);
758+
if ( ! empty($assoc_args['drain']) ) {
759+
$result = $this->drain_cleanup_run_to_status($result, $assoc_args);
760+
}
761+
753762
$this->render_cleanup_control_result($result, $assoc_args);
754763
}
755764

765+
/**
766+
* Attach operator commands to a queued cleanup run response.
767+
*
768+
* @param array<string,mixed> $result Cleanup run result.
769+
* @param string $mode Cleanup mode.
770+
* @return array<string,mixed>
771+
*/
772+
private function attach_cleanup_run_commands( array $result, string $mode ): array {
773+
$job_id = (int) ( $result['job_id'] ?? 0 );
774+
$run_id = (string) ( $result['run_id'] ?? ( $job_id > 0 ? $this->cleanup_run_id($job_id) : '' ) );
775+
if ( $job_id <= 0 || '' === $run_id ) {
776+
return $result;
777+
}
778+
779+
$result['commands'] = array(
780+
'drain_parent' => sprintf('studio wp datamachine drain --job-id=%d', $job_id),
781+
'status' => sprintf('studio wp datamachine-code workspace cleanup status %s --format=json', $run_id),
782+
'status_verbose' => sprintf('studio wp datamachine-code workspace cleanup status %s --verbose --format=json', $run_id),
783+
'one_command_drain' => sprintf('studio wp datamachine-code workspace cleanup run --mode=%s --drain --format=json', $mode),
784+
'bytes_verification' => sprintf('studio wp datamachine-code workspace cleanup status %s --format=json', $run_id),
785+
);
786+
787+
return $result;
788+
}
789+
790+
/**
791+
* Drain a queued job-backed cleanup run through Data Machine, then return status evidence.
792+
*
793+
* @param array<string,mixed> $result Initial cleanup run result.
794+
* @param array<string,mixed> $assoc_args CLI associative args.
795+
* @return array<string,mixed>
796+
*/
797+
private function drain_cleanup_run_to_status( array $result, array $assoc_args ): array {
798+
$job_id = (int) ( $result['job_id'] ?? 0 );
799+
$run_id = (string) ( $result['run_id'] ?? ( $job_id > 0 ? $this->cleanup_run_id($job_id) : '' ) );
800+
if ( $job_id <= 0 || '' === $run_id ) {
801+
$result['drain'] = array(
802+
'success' => false,
803+
'error' => 'Cleanup run did not return a job id to drain.',
804+
);
805+
return $result;
806+
}
807+
808+
$commands = array();
809+
$errors = array();
810+
$max_passes = 10;
811+
812+
$parent_command = sprintf('datamachine drain --job-id=%d', $job_id);
813+
$commands[] = 'studio wp ' . $parent_command;
814+
$error = $this->run_wp_cli_command($parent_command);
815+
if ( '' !== $error ) {
816+
$errors[] = $error;
817+
}
818+
819+
for ( $pass = 0; $pass < $max_passes; ++$pass ) {
820+
$status = $this->cleanup_run_evidence_store()->read($run_id, true, true);
821+
if ( $status instanceof \WP_Error ) {
822+
$errors[] = $status->get_error_message();
823+
break;
824+
}
825+
826+
$children = (array) ( $status['evidence']['children'] ?? array() );
827+
$active_child_ids = array_values(
828+
array_unique(
829+
array_filter(
830+
array_map(
831+
'intval',
832+
array_merge(
833+
(array) ( $children['pending_job_ids'] ?? array() ),
834+
(array) ( $children['processing_job_ids'] ?? array() )
835+
)
836+
)
837+
)
838+
)
839+
);
840+
if ( array() === $active_child_ids ) {
841+
break;
842+
}
843+
844+
$child_command = sprintf('datamachine drain --job-id=%s', implode(',', $active_child_ids));
845+
$commands[] = 'studio wp ' . $child_command;
846+
$error = $this->run_wp_cli_command($child_command);
847+
if ( '' !== $error ) {
848+
$errors[] = $error;
849+
break;
850+
}
851+
}
852+
853+
$final = $this->cleanup_run_evidence_store()->read($run_id, false, ! empty($assoc_args['verbose']));
854+
$output = $final instanceof \WP_Error ? $result : $final;
855+
$output['initial_run'] = $result;
856+
$output['drain'] = array(
857+
'success' => array() === $errors,
858+
'commands' => $commands,
859+
'errors' => $errors,
860+
'verify_command' => sprintf('studio wp datamachine-code workspace cleanup status %s --format=json', $run_id),
861+
'bytes_reclaimed' => (int) ( $output['cleanup_items']['bytes_reclaimed'] ?? 0 ),
862+
'freed_human' => (string) ( $output['cleanup_items']['freed_human'] ?? $this->format_bytes(0) ),
863+
'completion_state' => (string) ( $output['state'] ?? 'unknown' ),
864+
);
865+
866+
return $output;
867+
}
868+
869+
/**
870+
* Run a WP-CLI command and return an error message on failure.
871+
*
872+
* @param string $command Command without the leading `wp` binary.
873+
* @return string Empty string on success.
874+
*/
875+
private function run_wp_cli_command( string $command ): string {
876+
if ( ! method_exists('WP_CLI', 'runcommand') ) {
877+
return 'WP_CLI::runcommand is unavailable; run the reported drain commands manually.';
878+
}
879+
880+
try {
881+
WP_CLI::runcommand(
882+
$command,
883+
array(
884+
'return' => true,
885+
'exit_error' => false,
886+
)
887+
);
888+
} catch ( \Throwable $e ) {
889+
return $e->getMessage();
890+
}
891+
892+
return '';
893+
}
894+
756895
private function cleanup_run_input( string $mode, array $assoc_args ): array {
757896
$input = array(
758897
'mode' => $mode,
@@ -1055,6 +1194,12 @@ private function render_cleanup_control_result( array $result, array $assoc_args
10551194
if ( ! empty($result['progress']) && is_array($result['progress']) ) {
10561195
$this->render_cleanup_progress_summary( (array) $result['progress']);
10571196
}
1197+
if ( ! empty($result['commands']) && is_array($result['commands']) ) {
1198+
$this->render_cleanup_command_hints( (array) $result['commands']);
1199+
}
1200+
if ( ! empty($result['drain']) && is_array($result['drain']) ) {
1201+
$this->render_cleanup_drain_summary( (array) $result['drain']);
1202+
}
10581203
if ( ! empty($result['remaining_work_summary']) && is_array($result['remaining_work_summary']) ) {
10591204
$this->render_cleanup_remaining_work_summary( (array) $result['remaining_work_summary']);
10601205
}
@@ -1135,6 +1280,47 @@ private function render_cleanup_remaining_work_summary( array $summary ): void {
11351280
}
11361281
}
11371282

1283+
/**
1284+
* Render cleanup command hints.
1285+
*
1286+
* @param array<string,string> $commands Commands keyed by purpose.
1287+
* @return void
1288+
*/
1289+
private function render_cleanup_command_hints( array $commands ): void {
1290+
WP_CLI::log('');
1291+
WP_CLI::log('Cleanup commands:');
1292+
$rows = array();
1293+
foreach ( $commands as $purpose => $command ) {
1294+
$rows[] = array(
1295+
'purpose' => (string) $purpose,
1296+
'command' => (string) $command,
1297+
);
1298+
}
1299+
$this->format_items($rows, array( 'purpose', 'command' ), array( 'format' => 'table' ), 'purpose');
1300+
}
1301+
1302+
/**
1303+
* Render cleanup drain summary.
1304+
*
1305+
* @param array<string,mixed> $drain Drain summary.
1306+
* @return void
1307+
*/
1308+
private function render_cleanup_drain_summary( array $drain ): void {
1309+
WP_CLI::log('');
1310+
WP_CLI::log('Drain summary:');
1311+
$this->format_items(
1312+
array(
1313+
array( 'metric' => 'success', 'value' => ! empty($drain['success']) ? 'yes' : 'no' ),
1314+
array( 'metric' => 'completion_state', 'value' => (string) ( $drain['completion_state'] ?? 'unknown' ) ),
1315+
array( 'metric' => 'bytes_reclaimed', 'value' => $this->format_bytes($drain['bytes_reclaimed'] ?? 0) ),
1316+
array( 'metric' => 'verify_command', 'value' => (string) ( $drain['verify_command'] ?? '' ) ),
1317+
),
1318+
array( 'metric', 'value' ),
1319+
array( 'format' => 'table' ),
1320+
'metric'
1321+
);
1322+
}
1323+
11381324
private function render_cleanup_progress_summary( array $progress ): void {
11391325
WP_CLI::log('');
11401326
WP_CLI::log('Progress:');

tests/smoke-worktree-cleanup-cli.php

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ class WP_CLI
1818
{
1919
public static array $logs = array();
2020
public static array $successes = array();
21+
public static array $runcommands = array();
2122

2223
public static function error( string $message ): void
2324
{
@@ -38,6 +39,18 @@ public static function warning( string $message ): void
3839
{
3940
self::$logs[] = 'warning: ' . $message;
4041
}
42+
43+
public static function runcommand( string $command, array $options = array() ): string
44+
{
45+
self::$runcommands[] = $command;
46+
if ('datamachine drain --job-id=123' === $command ) {
47+
$GLOBALS['datamachine_code_cleanup_parent_drained'] = true;
48+
}
49+
if ('datamachine drain --job-id=125' === $command ) {
50+
$GLOBALS['datamachine_code_cleanup_child_drained'] = true;
51+
}
52+
return '';
53+
}
4154
}
4255

4356
function is_wp_error( $value ): bool
@@ -907,10 +920,10 @@ public function execute( array $input ): array
907920
'job_id' => 125,
908921
'parent_job_id' => 124,
909922
'source' => 'batch',
910-
'status' => 'processing',
923+
'status' => ! empty($GLOBALS['datamachine_code_cleanup_child_drained']) ? 'completed' : 'processing',
911924
'engine_data' => array(
912-
'batch_id' => 'dm_batch_123',
913-
'task_type' => 'worktree_cleanup_chunk',
925+
'batch_id' => 'dm_batch_123',
926+
'task_type' => 'worktree_cleanup_chunk',
914927
),
915928
),
916929
array(
@@ -1100,6 +1113,7 @@ public function execute( array $input ): array
11001113
datamachine_code_cleanup_assert(str_contains($cleanup_doc_comment, 'Control task-backed workspace cleanup runs.'), 'workspace cleanup command documents task-backed controller surface');
11011114
datamachine_code_cleanup_assert(str_contains($cleanup_doc_comment, '<plan|apply|run|status|resume|cancel|evidence>'), 'workspace cleanup synopsis exposes DB-backed and task-backed cleanup operations');
11021115
datamachine_code_cleanup_assert(str_contains($cleanup_doc_comment, '[--dry-run]'), 'task-backed cleanup synopsis keeps synchronous dry-run review');
1116+
datamachine_code_cleanup_assert(str_contains($cleanup_doc_comment, '[--drain]'), 'task-backed cleanup synopsis exposes drain option');
11031117
datamachine_code_cleanup_assert(str_contains($cleanup_doc_comment, 'apply runs freeze eligible candidates'), 'workspace cleanup limit help clarifies artifact apply scoping');
11041118
datamachine_code_cleanup_assert(str_contains($doc_comment, 'positive maximum worktrees to scan'), 'worktree limit help requires positive page sizes');
11051119
datamachine_code_cleanup_assert(str_contains($doc_comment, 'Use `--exhaustive` instead of `--limit=0`'), 'worktree limit help points unbounded artifact scans to exhaustive mode');
@@ -1156,6 +1170,8 @@ public function execute( array $input ): array
11561170
$run_json = json_decode(WP_CLI::$logs[0] ?? '', true);
11571171
datamachine_code_cleanup_assert('jobs_queued' === ( $run_json['state'] ?? '' ), 'cleanup run queues a system task');
11581172
datamachine_code_cleanup_assert('cleanup-run-123' === ( $run_json['run_id'] ?? '' ), 'cleanup run returns stable run id');
1173+
datamachine_code_cleanup_assert('studio wp datamachine drain --job-id=123' === ( $run_json['commands']['drain_parent'] ?? '' ), 'cleanup run JSON includes exact parent drain command');
1174+
datamachine_code_cleanup_assert(str_contains((string) ( $run_json['commands']['one_command_drain'] ?? '' ), '--drain'), 'cleanup run JSON exposes one-command drain path');
11591175
datamachine_code_cleanup_assert('retention' === ( $cleanup_run_ability->last_input['mode'] ?? '' ), 'cleanup run ability receives mode');
11601176
datamachine_code_cleanup_assert('workspace_cleanup_cli' === ( $cleanup_run_ability->last_input['source'] ?? '' ), 'cleanup run ability identifies explicit CLI source');
11611177

@@ -1240,6 +1256,8 @@ public function execute( array $input ): array
12401256
datamachine_code_cleanup_assert(1 === (int) ( $status_json['remaining_work_summary']['skipped_by_reason']['stale_worktree_marker']['count'] ?? 0 ), 'cleanup status groups stale marker blockers separately');
12411257
datamachine_code_cleanup_assert(1 === (int) ( $status_json['remaining_work_summary']['skipped_by_reason']['primary_missing']['count'] ?? 0 ), 'cleanup status groups missing primary blockers separately');
12421258
datamachine_code_cleanup_assert(10240 === (int) ( $status_json['remaining_work_summary']['remaining_reclaimable_artifact_bytes'] ?? 0 ), 'cleanup status reports remaining reclaimable artifact bytes');
1259+
datamachine_code_cleanup_assert('studio wp datamachine drain --job-id=125' === ( $status_json['drain']['commands']['active_children'] ?? '' ), 'cleanup status includes exact active child drain command');
1260+
datamachine_code_cleanup_assert(4096 === (int) ( $status_json['drain']['bytes_reclaimed'] ?? 0 ), 'cleanup status drain summary verifies bytes reclaimed');
12431261
datamachine_code_cleanup_assert(str_contains((string) wp_json_encode($status_json['remaining_work_summary']['recommended_commands'] ?? array()), 'workspace cleanup run --mode=artifacts'), 'cleanup status recommends next DMC commands per bucket');
12441262
datamachine_code_cleanup_assert(str_contains((string) wp_json_encode($status_json['remaining_work_summary']['recommended_commands'] ?? array()), 'git -C <worktree-path> status --short --branch'), 'cleanup status recommends dirty git status evidence');
12451263
datamachine_code_cleanup_assert(str_contains((string) wp_json_encode($status_json['remaining_work_summary']['recommended_commands'] ?? array()), 'git -C <worktree-path> log --oneline --decorate @{u}..HEAD'), 'cleanup status recommends unpushed git log evidence');
@@ -1248,6 +1266,20 @@ public function execute( array $input ): array
12481266
datamachine_code_cleanup_assert('4.0 KiB' === ( $status_json['system_task_result']['report']['freed_human'] ?? '' ), 'cleanup status replaces pending child job freed placeholder');
12491267
datamachine_code_cleanup_assert(! isset($status_json['system_task_result']['children']['job_ids']), 'cleanup status system task result omits full child job ids by default');
12501268

1269+
WP_CLI::$logs = array();
1270+
WP_CLI::$successes = array();
1271+
WP_CLI::$runcommands = array();
1272+
$GLOBALS['datamachine_code_cleanup_parent_drained'] = false;
1273+
$GLOBALS['datamachine_code_cleanup_child_drained'] = false;
1274+
$command->cleanup(array( 'run' ), array( 'mode' => 'artifacts', 'drain' => true, 'format' => 'json' ));
1275+
$drained_json = json_decode(WP_CLI::$logs[0] ?? '', true);
1276+
datamachine_code_cleanup_assert(array( 'datamachine drain --job-id=123', 'datamachine drain --job-id=125' ) === WP_CLI::$runcommands, 'cleanup run --drain drains parent then active child jobs');
1277+
datamachine_code_cleanup_assert(4096 === (int) ( $drained_json['drain']['bytes_reclaimed'] ?? 0 ), 'cleanup run --drain reports verified reclaimed bytes');
1278+
datamachine_code_cleanup_assert('partial_failed' === (string) ( $drained_json['drain']['completion_state'] ?? '' ), 'cleanup run --drain reports final cleanup state');
1279+
datamachine_code_cleanup_assert('studio wp datamachine-code workspace cleanup status cleanup-run-123 --format=json' === (string) ( $drained_json['drain']['verify_command'] ?? '' ), 'cleanup run --drain emits one verification command');
1280+
$GLOBALS['datamachine_code_cleanup_parent_drained'] = false;
1281+
$GLOBALS['datamachine_code_cleanup_child_drained'] = false;
1282+
12511283
WP_CLI::$logs = array();
12521284
WP_CLI::$successes = array();
12531285
$command->cleanup(array( 'status', 'cleanup-run-123' ), array());

0 commit comments

Comments
 (0)