v0.3.1 — self-healing cron scheduling (#8)
Adds PRV_Cron_Guard hooked on init to re-schedule managed cron hooks when missing after rsync deploys. Delegates to PRV_Cron::schedule() and PRV_Prune_Cron::schedule(). Idempotent. Two wp_next_scheduled() reads per request; no DB writes unless event missing.
This commit is contained in:
22
CHANGELOG.md
22
CHANGELOG.md
@@ -7,6 +7,28 @@ Versioning: [Semantic Versioning](https://semver.org/)
|
||||
|
||||
---
|
||||
|
||||
## [0.3.1] — 2026-06-15
|
||||
|
||||
Self-healing cron scheduling: `PRV_Cron_Guard` ensures managed WP-Cron events
|
||||
are re-scheduled on `init` when missing (e.g. after an rsync deploy that
|
||||
bypasses the activation hook).
|
||||
|
||||
### Fixed
|
||||
- Managed cron hooks (`prv_weekly_probe`, `prv_daily_prune`) are now
|
||||
re-scheduled automatically on any page load where they are missing.
|
||||
Delegates to `PRV_Cron::schedule()` and `PRV_Prune_Cron::schedule()` so
|
||||
recurrence and first-run offset are identical to what the activator sets.
|
||||
Hot-path cost: two `wp_next_scheduled()` checks; no DB writes unless an
|
||||
event is missing.
|
||||
|
||||
### Added
|
||||
- **`PRV_Cron_Guard`** (`includes/core/class-prv-cron-guard.php`) — idempotent
|
||||
guard registered on `init`; calls canonical scheduler methods when missing.
|
||||
- `PRV_Plugin::init()` — wires `PRV_Cron_Guard::register_hooks()`.
|
||||
- Tests: `test-cron-guard.php`.
|
||||
|
||||
---
|
||||
|
||||
## [0.3.0] — 2026-06-15
|
||||
|
||||
Per-call cost accountability + LLM call audit trail. Every probe call now
|
||||
|
||||
69
includes/core/class-prv-cron-guard.php
Normal file
69
includes/core/class-prv-cron-guard.php
Normal file
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
/**
|
||||
* Self-healing cron guard: re-schedules managed cron events when missing.
|
||||
*
|
||||
* @package PrVision
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Idempotent guard that ensures all managed WP-Cron events are scheduled.
|
||||
*
|
||||
* WordPress fires the plugin activator only on activation, not on rsync
|
||||
* file-sync updates (our deploy method). This guard runs on `init` and
|
||||
* re-schedules any missing cron event by delegating to the canonical
|
||||
* scheduler method for that hook -- so recurrence and first-run offset are
|
||||
* always consistent with the activator.
|
||||
*
|
||||
* Managed hooks:
|
||||
* - `prv_weekly_probe` -- scheduled via PRV_Cron::schedule()
|
||||
* - `prv_daily_prune` -- scheduled via PRV_Prune_Cron::schedule()
|
||||
*
|
||||
* Hot-path cost: two `wp_next_scheduled()` checks per request. No DB
|
||||
* writes unless an event is actually missing.
|
||||
*
|
||||
* Who triggers: init action (registered in PRV_Plugin::init()).
|
||||
* Dependencies: PRV_Cron, PRV_Prune_Cron.
|
||||
*
|
||||
* @see class-prv-cron.php -- Canonical weekly probe scheduler.
|
||||
* @see class-prv-prune-cron.php -- Canonical daily prune scheduler.
|
||||
* @see class-prv-activator.php -- Original schedule call on activation.
|
||||
* @package PrVision
|
||||
*/
|
||||
class PRV_Cron_Guard {
|
||||
|
||||
/**
|
||||
* Register the guard on the `init` hook.
|
||||
*
|
||||
* Side effects: Adds action for `init`.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register_hooks(): void {
|
||||
add_action( 'init', array( $this, 'ensure_scheduled' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure every managed cron hook is scheduled.
|
||||
*
|
||||
* For each managed hook, checks `wp_next_scheduled()`. If the event is
|
||||
* missing, delegates to the canonical scheduler (same method the
|
||||
* activator calls) so recurrence and offset are never duplicated here.
|
||||
* Calling this when all events are already scheduled is a pure no-op.
|
||||
*
|
||||
* Side effects: May call PRV_Cron::schedule() or
|
||||
* PRV_Prune_Cron::schedule() when an event is missing.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function ensure_scheduled(): void {
|
||||
if ( ! wp_next_scheduled( PRV_CRON_HOOK ) ) {
|
||||
PRV_Cron::schedule();
|
||||
}
|
||||
|
||||
if ( ! wp_next_scheduled( PRV_Prune_Cron::PRUNE_HOOK ) ) {
|
||||
PRV_Prune_Cron::schedule();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,13 +17,15 @@ declare(strict_types=1);
|
||||
* Who triggers: plugins_loaded action (pr-vision.php).
|
||||
* Dependencies: PRV_Upgrader, PRV_Cron, PRV_Admin_Page, PRV_Settings_Page,
|
||||
* PRV_Settings_Controller, PRV_Costs_Page, PRV_Call_Log_Page,
|
||||
* PRV_Call_Detail_Ajax, PRV_Prune_Cron, PRV_Collector_Registry.
|
||||
* PRV_Call_Detail_Ajax, PRV_Prune_Cron, PRV_Cron_Guard,
|
||||
* PRV_Collector_Registry.
|
||||
*
|
||||
* @see ARCHITECTURE.md -- Boot sequence diagram.
|
||||
* @see class-prv-upgrader.php -- Runs migrations on every boot.
|
||||
* @see class-prv-costs-page.php -- [v0.3.0] Costs admin sub-page.
|
||||
* @see class-prv-call-log-page.php -- [v0.3.0] Call Log admin sub-page.
|
||||
* @see class-prv-prune-cron.php -- [v0.3.0] Daily prune cron.
|
||||
* @see class-prv-cron-guard.php -- [v0.3.1] Self-healing cron guard.
|
||||
* @package PrVision
|
||||
*/
|
||||
class PRV_Plugin {
|
||||
@@ -50,6 +52,9 @@ class PRV_Plugin {
|
||||
$prune = new PRV_Prune_Cron();
|
||||
$prune->register_hooks();
|
||||
|
||||
$guard = new PRV_Cron_Guard();
|
||||
$guard->register_hooks();
|
||||
|
||||
if ( is_admin() ) {
|
||||
$page = new PRV_Admin_Page();
|
||||
$page->register_hooks();
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Plugin Name: PR Vision
|
||||
* Plugin URI: https://peptiderepo.com
|
||||
* Description: Weekly server-side LLM probes for AI-visibility (GEO) tracking. Records whether peptiderepo.com is cited by LLMs across core peptides, stores time-series, and renders an admin dashboard + settings UI.
|
||||
* Version: 0.3.0
|
||||
* Version: 0.3.1
|
||||
* Author: peptiderepo
|
||||
* Author URI: https://peptiderepo.com
|
||||
* License: GPL-2.0-or-later
|
||||
@@ -24,7 +24,7 @@ if ( ! defined( 'ABSPATH' ) ) {
|
||||
|
||||
/* ── Constants ────────────────────────────────────────────────────────── */
|
||||
|
||||
define( 'PRV_VERSION', '0.3.0' );
|
||||
define( 'PRV_VERSION', '0.3.1' );
|
||||
define( 'PRV_PLUGIN_FILE', __FILE__ );
|
||||
define( 'PRV_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
|
||||
define( 'PRV_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
|
||||
|
||||
@@ -27,7 +27,7 @@ if ( ! defined( 'ARRAY_N' ) ) {
|
||||
define( 'ARRAY_N', 'ARRAY_N' );
|
||||
}
|
||||
|
||||
define( 'PRV_VERSION', '0.3.0' );
|
||||
define( 'PRV_VERSION', '0.3.1' );
|
||||
define( 'PRV_PLUGIN_FILE', __DIR__ . '/../pr-vision.php' );
|
||||
define( 'PRV_PLUGIN_DIR', realpath( __DIR__ . '/..' ) . '/' );
|
||||
define( 'PRV_PLUGIN_URL', 'http://example.test/wp-content/plugins/pr-vision/' );
|
||||
|
||||
96
tests/unit/test-cron-guard.php
Normal file
96
tests/unit/test-cron-guard.php
Normal file
@@ -0,0 +1,96 @@
|
||||
<?php
|
||||
/**
|
||||
* Tests for PRV_Cron_Guard: self-healing cron scheduling.
|
||||
*
|
||||
* @package PrVision
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/../bootstrap.php';
|
||||
|
||||
echo "=== PRV Cron Guard Tests ===\n";
|
||||
|
||||
// ── Test: ensure_scheduled schedules prv_weekly_probe when missing ──────
|
||||
|
||||
prv_test_reset();
|
||||
prv_assert( false === wp_next_scheduled( PRV_CRON_HOOK ), 'guard/weekly: not scheduled before call' );
|
||||
prv_assert( false === wp_next_scheduled( PRV_Prune_Cron::PRUNE_HOOK ), 'guard/prune: not scheduled before call' );
|
||||
|
||||
$guard = new PRV_Cron_Guard();
|
||||
$guard->ensure_scheduled();
|
||||
|
||||
prv_assert( false !== wp_next_scheduled( PRV_CRON_HOOK ), 'guard/weekly: prv_weekly_probe scheduled after ensure_scheduled()' );
|
||||
|
||||
// ── Test: ensure_scheduled schedules prv_daily_prune when missing ────────
|
||||
|
||||
prv_assert( false !== wp_next_scheduled( PRV_Prune_Cron::PRUNE_HOOK ), 'guard/prune: prv_daily_prune scheduled after ensure_scheduled()' );
|
||||
|
||||
// ── Test: idempotent -- called twice, still only one event each ──────────
|
||||
|
||||
$ts_weekly_1 = wp_next_scheduled( PRV_CRON_HOOK );
|
||||
$ts_prune_1 = wp_next_scheduled( PRV_Prune_Cron::PRUNE_HOOK );
|
||||
|
||||
$guard->ensure_scheduled(); // Second call.
|
||||
|
||||
$ts_weekly_2 = wp_next_scheduled( PRV_CRON_HOOK );
|
||||
$ts_prune_2 = wp_next_scheduled( PRV_Prune_Cron::PRUNE_HOOK );
|
||||
|
||||
prv_assert( $ts_weekly_1 === $ts_weekly_2, 'guard/idempotent: weekly event timestamp unchanged after second call' );
|
||||
prv_assert( $ts_prune_1 === $ts_prune_2, 'guard/idempotent: prune event timestamp unchanged after second call' );
|
||||
|
||||
// Verify the test state has exactly one entry per hook (no duplicates).
|
||||
$cron_events = $GLOBALS['prv_test_state']['cron_events'];
|
||||
prv_assert( array_key_exists( PRV_CRON_HOOK, $cron_events ), 'guard/idempotent: exactly one weekly event entry' );
|
||||
prv_assert( array_key_exists( PRV_Prune_Cron::PRUNE_HOOK, $cron_events ), 'guard/idempotent: exactly one prune event entry' );
|
||||
prv_assert( 2 === count( $cron_events ), 'guard/idempotent: only two cron event entries total' );
|
||||
|
||||
// ── Test: guard uses same recurrence as canonical schedulers ─────────────
|
||||
|
||||
prv_test_reset();
|
||||
$GLOBALS['prv_test_state']['options'][ PRV_Config::CADENCE_KEY ] = 'weekly';
|
||||
PRV_Cron::schedule();
|
||||
$canonical_weekly = $GLOBALS['prv_test_state']['cron_events'][ PRV_CRON_HOOK ]['schedule'] ?? null;
|
||||
|
||||
prv_test_reset();
|
||||
PRV_Prune_Cron::schedule();
|
||||
$canonical_prune = $GLOBALS['prv_test_state']['cron_events'][ PRV_Prune_Cron::PRUNE_HOOK ]['schedule'] ?? null;
|
||||
|
||||
// Now verify guard-scheduled events share the same recurrences.
|
||||
prv_test_reset();
|
||||
$GLOBALS['prv_test_state']['options'][ PRV_Config::CADENCE_KEY ] = 'weekly';
|
||||
$guard2 = new PRV_Cron_Guard();
|
||||
$guard2->ensure_scheduled();
|
||||
|
||||
$guard_weekly = $GLOBALS['prv_test_state']['cron_events'][ PRV_CRON_HOOK ]['schedule'] ?? null;
|
||||
$guard_prune = $GLOBALS['prv_test_state']['cron_events'][ PRV_Prune_Cron::PRUNE_HOOK ]['schedule'] ?? null;
|
||||
|
||||
prv_assert_equals( $canonical_weekly, $guard_weekly, 'guard/recurrence: weekly recurrence matches canonical PRV_Cron::schedule()' );
|
||||
prv_assert_equals( $canonical_prune, $guard_prune, 'guard/recurrence: prune recurrence matches canonical PRV_Prune_Cron::schedule()' );
|
||||
|
||||
// ── Test: no-op when both events already scheduled ───────────────────────
|
||||
|
||||
prv_test_reset();
|
||||
$GLOBALS['prv_test_state']['options'][ PRV_Config::CADENCE_KEY ] = 'weekly';
|
||||
PRV_Cron::schedule();
|
||||
PRV_Prune_Cron::schedule();
|
||||
|
||||
$before = $GLOBALS['prv_test_state']['cron_events'];
|
||||
|
||||
$guard3 = new PRV_Cron_Guard();
|
||||
$guard3->ensure_scheduled();
|
||||
|
||||
$after = $GLOBALS['prv_test_state']['cron_events'];
|
||||
|
||||
prv_assert( $before === $after, 'guard/noop: cron_events unchanged when all events already scheduled' );
|
||||
|
||||
// ── Test: register_hooks adds the init action ────────────────────────────
|
||||
|
||||
prv_test_reset();
|
||||
$guard4 = new PRV_Cron_Guard();
|
||||
$guard4->register_hooks();
|
||||
|
||||
$hooks = array_column( $GLOBALS['prv_test_state']['actions'], 'hook' );
|
||||
prv_assert( in_array( 'init', $hooks, true ), 'guard/register_hooks: init action registered' );
|
||||
|
||||
exit( prv_test_summary() );
|
||||
Reference in New Issue
Block a user