Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 1 addition & 5 deletions inc/Storage/SqliteBusyRetry.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,14 @@ final class SqliteBusyRetry {
private const DEFAULT_MAX_DELAY_MS = 250;

/**
* Retry only a SQLite write which reports a transient busy/locked failure.
* Retry only a database write which reports a transient SQLite busy/locked failure.
*
* @param callable():mixed $operation DB-only mutation callback.
* @return mixed|\WP_Error
*/
public static function run( string $operation_name, callable $operation ): mixed {
global $wpdb;

if ( ! self::is_sqlite($wpdb) ) {
return $operation();
}

$max_wait_ms = self::filtered_positive_int('datamachine_code_sqlite_busy_retry_max_wait_ms', self::DEFAULT_MAX_WAIT_MS);
$initial_wait_ms = self::filtered_positive_int('datamachine_code_sqlite_busy_retry_initial_wait_ms', self::DEFAULT_INITIAL_WAIT_MS);
$max_delay_ms = self::filtered_positive_int('datamachine_code_sqlite_busy_retry_max_delay_ms', self::DEFAULT_MAX_DELAY_MS);
Expand Down
15 changes: 11 additions & 4 deletions tests/workspace-lock-sqlite-contention.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ final class Lock_Contention_Wpdb {
public int $insert_id = 0;
private bool $errors_suppressed = false;

public function __construct(private PDO $pdo) {}
public function db_server_info(): string { return 'SQLite'; }
public function __construct(private PDO $pdo, private bool $advertise_sqlite = true) {}
public function db_server_info(): string { return $this->advertise_sqlite ? 'SQLite' : 'MySQL'; }
public function suppress_errors(bool $suppress = true): bool { $previous = $this->errors_suppressed; $this->errors_suppressed = $suppress; return $previous; }
public function prepare(string $query, mixed ...$args): string { foreach ($args as $arg) { $query = preg_replace('/%[sd]/', is_int($arg) ? (string) $arg : $this->pdo->quote((string) $arg), $query, 1); } return $query; }
public function get_var(string $query): mixed {
Expand Down Expand Up @@ -72,7 +72,7 @@ function lock_sqlite_worker(array $args): void {
$GLOBALS['filters'] = array('datamachine_code_sqlite_busy_retry_max_wait_ms' => (int) $max_wait_ms);
$pdo = new PDO('sqlite:' . $database);
$pdo->exec('PRAGMA busy_timeout = 0');
$GLOBALS['wpdb'] = new Lock_Contention_Wpdb($pdo);
$GLOBALS['wpdb'] = new Lock_Contention_Wpdb($pdo, 'decorated-acquire' !== $mode);

if ('allocation' === $mode) {
$result = WorkspaceMutationLock::with_repo($workspace, $repo, static function (WorkspaceMutationLock $lock) use ($workspace, $repo): mixed {
Expand All @@ -92,7 +92,7 @@ function lock_sqlite_worker(array $args): void {
return;
}

if ('acquire' === $mode) {
if ('acquire' === $mode || 'decorated-acquire' === $mode) {
fwrite(STDOUT, json_encode(lock_sqlite_result(WorkspaceMutationLock::acquire($workspace, $repo, 1))));
return;
}
Expand Down Expand Up @@ -202,6 +202,13 @@ function lock_sqlite_remove_tree(string $path): void {
flock($raw, LOCK_UN); fclose($raw);
$setup->exec('COMMIT');

// A decorator can hide the SQLite backend from wpdb capability probes. The
// failed operation and last_error remain authoritative for bounded retry.
$setup->exec('BEGIN EXCLUSIVE');
$decorated = lock_sqlite_finish(lock_sqlite_start(array('decorated-acquire', $database, $workspace, 'decorated-acquire-exhausted', '100')));
lock_sqlite_assert('workspace_sqlite_lock_contention' === ($decorated['error'] ?? null) && 'workspace_lock_register' === ($decorated['data']['operation'] ?? null), 'Decorated SQLite acquisition bypassed canonical contention retry.');
$setup->exec('COMMIT');

// The real worktree lifecycle must stop before Git mutation when ownership
// registration cannot be made durable.
$primary = $workspace . '/lifecycle-repo';
Expand Down
Loading