Skip to content

Commit efe02f3

Browse files
authored
Merge pull request #507 from Extra-Chill/fix/issue-506-workspace-mutation-locks
Fix workspace mutation lock recovery evidence
2 parents a1ee7c5 + 1b69860 commit efe02f3

3 files changed

Lines changed: 256 additions & 41 deletions

File tree

inc/Cli/Commands/WorkspaceCommand.php

Lines changed: 107 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
use DataMachineCode\Cleanup\CleanupRunEvidenceStoreInterface;
2121
use DataMachineCode\Cleanup\DataMachineJobCleanupRunEvidenceStore;
2222
use DataMachineCode\Workspace\Workspace;
23+
use DataMachineCode\Workspace\WorkspaceMutationLock;
2324

2425
defined('ABSPATH') || exit;
2526

@@ -133,7 +134,7 @@ public function list_repos( array $args, array $assoc_args ): void {
133134
$result = $ability->execute($input);
134135

135136
if ( is_wp_error($result) ) {
136-
$this->render_workspace_error($result);
137+
WP_CLI::error($result->get_error_message());
137138
return;
138139
}
139140

@@ -1975,7 +1976,7 @@ private function renderGitOperationResult( string $operation, array $result, arr
19751976
* ## OPTIONS
19761977
*
19771978
* <operation>
1978-
* : Worktree operation: add, list, remove, prune, cleanup, cleanup-artifacts,
1979+
* : Worktree operation: add, list, remove, prune, locks, cleanup, cleanup-artifacts,
19791980
* bounded-cleanup-eligible-apply, emergency-cleanup, reconcile-metadata,
19801981
* active-no-signal-report, active-no-signal-finalized-apply,
19811982
* active-no-signal-equivalent-clean-apply,
@@ -2065,7 +2066,11 @@ private function renderGitOperationResult( string $operation, array $result, arr
20652066
* : Lifecycle state to record when finalizing a worktree.
20662067
*
20672068
* [--dry-run]
2068-
* : Preview cleanup candidates without removing anything (cleanup only).
2069+
* : Preview cleanup candidates without removing anything (cleanup and locks only).
2070+
*
2071+
* [--prune-stale]
2072+
* : For `locks`, prune expired DB lock rows and old unlocked filesystem lock
2073+
* files. Active filesystem flocks are reported but never removed.
20692074
*
20702075
* [--apply-plan=<file>]
20712076
* : Low-level escape hatch for applying a previously reviewed JSON report.
@@ -2211,6 +2216,11 @@ private function renderGitOperationResult( string $operation, array $result, arr
22112216
* # Prune stale worktree registry entries across all primaries
22122217
* wp datamachine-code workspace worktree prune
22132218
*
2219+
* # Inspect and safely prune stale workspace mutation locks
2220+
* wp datamachine-code workspace worktree locks --format=json
2221+
* wp datamachine-code workspace worktree locks --prune-stale --dry-run --format=json
2222+
* wp datamachine-code workspace worktree locks --prune-stale --format=json
2223+
*
22142224
* # Preview worktrees that would be removed (upstream gone or PR merged)
22152225
* wp datamachine-code workspace worktree cleanup --dry-run
22162226
*
@@ -2287,7 +2297,18 @@ public function worktree( array $args, array $assoc_args ): void {
22872297
$operation = $args[0] ?? '';
22882298

22892299
if ( '' === $operation ) {
2290-
WP_CLI::error('Usage: wp datamachine-code workspace worktree <add|list|remove|prune|cleanup|cleanup-artifacts|bounded-cleanup-eligible-apply|emergency-cleanup|reconcile-metadata|active-no-signal-report|active-no-signal-finalized-apply|active-no-signal-equivalent-clean-apply|refresh-context|finalize|mark-cleanup-eligible> [<repo>] [<branch>] [--flags]');
2300+
WP_CLI::error('Usage: wp datamachine-code workspace worktree <add|list|remove|prune|locks|cleanup|cleanup-artifacts|bounded-cleanup-eligible-apply|emergency-cleanup|reconcile-metadata|active-no-signal-report|active-no-signal-finalized-apply|active-no-signal-equivalent-clean-apply|refresh-context|finalize|mark-cleanup-eligible> [<repo>] [<branch>] [--flags]');
2301+
return;
2302+
}
2303+
2304+
if ( 'locks' === $operation ) {
2305+
$workspace = new Workspace();
2306+
$workspace_path = $workspace->get_path();
2307+
$dry_run = ! empty($assoc_args['dry-run']) || empty($assoc_args['prune-stale']);
2308+
$result = ! empty($assoc_args['prune-stale'])
2309+
? WorkspaceMutationLock::prune_stale($workspace_path, $dry_run)
2310+
: WorkspaceMutationLock::status($workspace_path);
2311+
$this->render_workspace_lock_result($result, $assoc_args, ! empty($assoc_args['prune-stale']));
22912312
return;
22922313
}
22932314

@@ -2543,7 +2564,7 @@ public function worktree( array $args, array $assoc_args ): void {
25432564
$result = $ability->execute($input);
25442565

25452566
if ( is_wp_error($result) ) {
2546-
WP_CLI::error($result->get_error_message());
2567+
$this->render_workspace_error($result);
25472568
return;
25482569
}
25492570

@@ -2766,6 +2787,63 @@ function ( $wt ) {
27662787
}
27672788
}
27682789

2790+
/**
2791+
* Render workspace mutation lock status or prune results.
2792+
*
2793+
* @param array<string,mixed> $result Lock status or prune result.
2794+
*/
2795+
private function render_workspace_lock_result( array $result, array $assoc_args, bool $prune ): void {
2796+
if ( 'json' === (string) ( $assoc_args['format'] ?? '' ) ) {
2797+
$json = wp_json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
2798+
WP_CLI::log(false === $json ? '{}' : $json);
2799+
return;
2800+
}
2801+
2802+
$status = $prune ? (array) ( $result['after'] ?? array() ) : $result;
2803+
$fs = (array) ( $status['filesystem'] ?? array() );
2804+
$db = (array) ( $status['database'] ?? array() );
2805+
2806+
WP_CLI::log($prune ? 'Workspace mutation locks: stale prune complete' : 'Workspace mutation locks:');
2807+
WP_CLI::log(sprintf('Active: %d Stale: %d', (int) ( $status['active'] ?? 0 ), (int) ( $status['stale'] ?? 0 )));
2808+
WP_CLI::log(sprintf('Database: %s active, %s stale, available=%s', (string) ( $db['active'] ?? 0 ), (string) ( $db['stale'] ?? 0 ), ! empty($db['available']) ? 'yes' : 'no'));
2809+
WP_CLI::log(sprintf('Filesystem: %s active, %s stale, %s recent', (string) ( $fs['active'] ?? 0 ), (string) ( $fs['stale'] ?? 0 ), (string) ( $fs['recent'] ?? 0 )));
2810+
2811+
if ( $prune ) {
2812+
$filesystem = (array) ( $result['filesystem'] ?? array() );
2813+
WP_CLI::log(sprintf('Filesystem removed: %d; skipped: %d', (int) ( $filesystem['removed_count'] ?? 0 ), (int) ( $filesystem['skipped_count'] ?? 0 )));
2814+
if ( ! empty($result['dry_run']) ) {
2815+
WP_CLI::log('Dry-run only. Re-run without --dry-run to remove stale unlocked lock files.');
2816+
}
2817+
}
2818+
2819+
$locks = (array) ( $fs['locks'] ?? array() );
2820+
if ( ! empty($locks) ) {
2821+
$items = array_map(
2822+
static function ( array $lock ): array {
2823+
$owner = (array) ( $lock['owner_evidence'] ?? array() );
2824+
return array(
2825+
'lock_key' => (string) ( $lock['lock_key'] ?? '' ),
2826+
'scope' => (string) ( $lock['scope'] ?? '' ),
2827+
'state' => (string) ( $lock['state'] ?? '' ),
2828+
'age_seconds' => $lock['age_seconds'] ?? null,
2829+
'safe_to_prune' => ! empty($lock['safe_to_prune']) ? 'yes' : 'no',
2830+
'owner_source' => (string) ( $owner['source'] ?? '' ),
2831+
'path' => (string) ( $lock['path'] ?? '' ),
2832+
);
2833+
},
2834+
$locks
2835+
);
2836+
$this->format_items($items, array( 'lock_key', 'scope', 'state', 'age_seconds', 'safe_to_prune', 'owner_source', 'path' ), $assoc_args, 'lock_key');
2837+
}
2838+
2839+
$guidance = (array) ( $fs['guidance'] ?? $status['recovery_guidance'] ?? array() );
2840+
if ( ! empty($guidance) ) {
2841+
WP_CLI::log(sprintf('Status: %s', (string) ( $guidance['status_command'] ?? 'wp datamachine-code workspace worktree locks --format=json' )));
2842+
WP_CLI::log(sprintf('Prune: %s', (string) ( $guidance['dry_run_command'] ?? 'wp datamachine-code workspace worktree locks --prune-stale --dry-run --format=json' )));
2843+
WP_CLI::log( (string) ( $guidance['safety'] ?? 'Active filesystem flocks are not pruned.' ) );
2844+
}
2845+
}
2846+
27692847
private function render_workspace_error( \WP_Error $error ): void {
27702848
$data = (array) $error->get_error_data();
27712849
if ( 'workspace_repo_busy' !== $error->get_error_code() ) {
@@ -2795,6 +2873,30 @@ private function render_workspace_error( \WP_Error $error ): void {
27952873
}
27962874
}
27972875

2876+
$filesystem_lock = is_array($data['filesystem_lock'] ?? null) ? (array) $data['filesystem_lock'] : array();
2877+
if ( ! empty($filesystem_lock) ) {
2878+
WP_CLI::warning(sprintf('Filesystem lock: %s (%s)', (string) ( $filesystem_lock['lock_key'] ?? $data['lock_key'] ?? '-' ), (string) ( $filesystem_lock['state'] ?? 'unknown' )));
2879+
WP_CLI::log(sprintf('Path: %s', (string) ( $filesystem_lock['path'] ?? $data['lock_path'] ?? '-' )));
2880+
WP_CLI::log(sprintf('Age: %ss', (string) ( $filesystem_lock['age_seconds'] ?? '-' )));
2881+
$owner_evidence = (array) ( $filesystem_lock['owner_evidence'] ?? array() );
2882+
if ( ! empty($owner_evidence['source']) ) {
2883+
WP_CLI::log(sprintf('Owner src: %s', (string) $owner_evidence['source']));
2884+
}
2885+
if ( ! empty($owner_evidence['message']) ) {
2886+
WP_CLI::log(sprintf('Owner note: %s', (string) $owner_evidence['message']));
2887+
}
2888+
if ( ! empty($filesystem_lock['operator_guidance']) ) {
2889+
WP_CLI::log(sprintf('Guidance: %s', (string) $filesystem_lock['operator_guidance']));
2890+
}
2891+
}
2892+
2893+
if ( ! empty($data['status_command']) ) {
2894+
WP_CLI::log(sprintf('Inspect: %s', (string) $data['status_command']));
2895+
}
2896+
if ( ! empty($data['stale_prune_command']) ) {
2897+
WP_CLI::log(sprintf('Recover: %s', (string) $data['stale_prune_command']));
2898+
}
2899+
27982900
WP_CLI::error($error->get_error_message());
27992901
}
28002902

0 commit comments

Comments
 (0)