Extract PRAutoBlogger_Generation_Lock (DB mutex) and PRAutoBlogger_Ajax_Handlers (image/model/test AJAX) from the 482-line executor. Executor retains generation cron/AJAX + model registry at 299 lines. Document background generation flow in ARCHITECTURE.md. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
96 lines
2.9 KiB
PHP
96 lines
2.9 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
/**
|
|
* Database-level atomic mutex for content generation.
|
|
*
|
|
* What: Provides acquire/release/check semantics for a single-writer generation lock.
|
|
* Who calls it: PRAutoBlogger_Executor (daily and manual generation), status polling fallback.
|
|
* Dependencies: WordPress $wpdb for direct SQL.
|
|
*
|
|
* Uses INSERT IGNORE against the wp_options UNIQUE index on option_name to
|
|
* guarantee only one PHP process can hold the lock at a time. Expired locks
|
|
* (>1 hour) are cleaned up automatically on acquire to prevent permanent deadlock.
|
|
*
|
|
* @see class-executor.php — Acquires/releases the lock around pipeline runs.
|
|
* @see class-ajax-handlers.php — Releases stale locks in the status fallback path.
|
|
*/
|
|
class PRAutoBlogger_Generation_Lock {
|
|
|
|
/** @var string Option name used as the lock key in wp_options. */
|
|
private const LOCK_NAME = 'prautoblogger_generation_lock';
|
|
|
|
/**
|
|
* Acquire the generation mutex.
|
|
*
|
|
* Cleans up expired locks (>1 hour) first, then attempts an atomic INSERT
|
|
* IGNORE. The UNIQUE constraint on option_name ensures only one caller wins.
|
|
*
|
|
* @return bool True if lock acquired, false if already held.
|
|
*/
|
|
public static function acquire(): bool {
|
|
global $wpdb;
|
|
|
|
// Clean up expired locks to prevent permanent deadlock.
|
|
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
|
|
$wpdb->query(
|
|
$wpdb->prepare(
|
|
"DELETE FROM {$wpdb->options} WHERE option_name = %s AND CAST(option_value AS UNSIGNED) < %d",
|
|
self::LOCK_NAME,
|
|
time() - HOUR_IN_SECONDS
|
|
)
|
|
);
|
|
|
|
// Atomic insert — UNIQUE constraint guarantees only one process succeeds.
|
|
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
|
|
$result = $wpdb->query(
|
|
$wpdb->prepare(
|
|
"INSERT IGNORE INTO {$wpdb->options} (option_name, option_value, autoload) VALUES (%s, %s, %s)",
|
|
self::LOCK_NAME,
|
|
(string) time(),
|
|
'no'
|
|
)
|
|
);
|
|
|
|
return $result > 0;
|
|
}
|
|
|
|
/**
|
|
* Check if the generation lock is currently held.
|
|
*
|
|
* @return bool True if the lock exists (generation in progress or stuck).
|
|
*/
|
|
public static function is_locked(): bool {
|
|
global $wpdb;
|
|
|
|
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
|
|
$row = $wpdb->get_var(
|
|
$wpdb->prepare(
|
|
"SELECT option_value FROM {$wpdb->options} WHERE option_name = %s LIMIT 1",
|
|
self::LOCK_NAME
|
|
)
|
|
);
|
|
|
|
return null !== $row;
|
|
}
|
|
|
|
/**
|
|
* Release the generation mutex.
|
|
*
|
|
* Safe to call even if the lock is not held (no-ops gracefully).
|
|
*
|
|
* Side effects: deletes the lock row from wp_options.
|
|
*/
|
|
public static function release(): void {
|
|
global $wpdb;
|
|
|
|
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
|
|
$wpdb->query(
|
|
$wpdb->prepare(
|
|
"DELETE FROM {$wpdb->options} WHERE option_name = %s",
|
|
self::LOCK_NAME
|
|
)
|
|
);
|
|
}
|
|
}
|