refactor: split class-executor.php into 3 focused classes (300-line rule)

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>
This commit is contained in:
peptiderepo
2026-04-18 14:08:46 +07:00
parent 4c49e8593e
commit 41f1b13497
5 changed files with 339 additions and 224 deletions

View File

@@ -0,0 +1,95 @@
<?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
)
);
}
}