Files
prautoblogger/includes/class-generation-lock.php
peptiderepo 0e1551be42 fix(phpcs): move ignore pragmas into file-level docblocks
File-level sniffs like WordPress.Files.FileName are evaluated at the file level,
not at code level. They require phpcs:ignore directives to be placed within the
file-level docblock (/** ... */), not in code comments.

Moved all 81 phpcs:ignore pragmas from code comments to the file-level docblocks.
This applies to:
- 79 class files with class-naming convention ignores
- prautoblogger.php with strict-types docblock ignore
- uninstall.php with strict-types docblock ignore
2026-04-23 03:26:50 +00:00

98 lines
3.0 KiB
PHP

<?php
declare(strict_types=1);
/**
* phpcs:ignore WordPress.Files.FileName.InvalidClassFileName -- class naming convention differs from WordPress standard
*
* 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
)
);
}
}