| 1 |
<?php |
| 2 |
/** |
| 3 |
* Import Task health badge decider (GitHub issue #200). |
| 4 |
* |
| 5 |
* Pure function (no WordPress dependency) so it can be unit tested in |
| 6 |
* isolation. The execution engine already records every run's state, progress, |
| 7 |
* heartbeat, and error in the task's mlsimport_import_run_status meta, but the |
| 8 |
* Import Tasks admin list never showed any of it — a stuck or failed task |
| 9 |
* looked identical to a healthy one ("Importing 10 out of 100" forever, no |
| 10 |
* warning). This file decides, from that recorded status plus the sync |
| 11 |
* watermark, what the Status column badge says for one task. |
| 12 |
*/ |
| 13 |
|
| 14 |
if ( ! defined( 'ABSPATH' ) ) { |
| 15 |
exit; // Exit if accessed directly |
| 16 |
} |
| 17 |
|
| 18 |
// A live worker records activity after every listing; the execution engine |
| 19 |
// treats a run lock silent for 1800 seconds as dead (STALE_AFTER_SECONDS). |
| 20 |
// The badge uses the same threshold so "Stuck" and lock takeover agree. |
| 21 |
if ( ! defined( 'MLSIMPORT_TASK_HEALTH_STUCK_AFTER' ) ) { |
| 22 |
define( 'MLSIMPORT_TASK_HEALTH_STUCK_AFTER', 1800 ); |
| 23 |
} |
| 24 |
|
| 25 |
// Auto-sync runs hourly and advances the watermark on every successful run, so |
| 26 |
// a watermark 6+ hours old means several consecutive failed or skipped syncs — |
| 27 |
// old enough to be a real problem, tolerant of a temporary hiccup. |
| 28 |
if ( ! defined( 'MLSIMPORT_TASK_HEALTH_OVERDUE_AFTER' ) ) { |
| 29 |
define( 'MLSIMPORT_TASK_HEALTH_OVERDUE_AFTER', 21600 ); |
| 30 |
} |
| 31 |
|
| 32 |
/** |
| 33 |
* Decide the Status column badge for one Import Task. |
| 34 |
* |
| 35 |
* Step by step: |
| 36 |
* 0. No run status and no watermark: a task waiting for its first manual |
| 37 |
* import — neutral, not a health problem. |
| 38 |
* 1. A run the engine marked failed is an error badge carrying the run's own |
| 39 |
* human-readable error, so the administrator reads WHY in the task list. |
| 40 |
* 2. A running import whose heartbeat went silent longer than the engine's |
| 41 |
* own staleness rule (30 minutes) is a dead worker: flag it stuck. This is |
| 42 |
* the issue's reported scenario — "Importing 10 out of 100" forever. |
| 43 |
* 3. A running import with a fresh heartbeat is healthy: show its progress. |
| 44 |
* 4. A cron-enabled task without a watermark cannot hourly-sync at all (the |
| 45 |
* sync refuses to start without one): warn to run one manual import. A |
| 46 |
* watermark that fell behind the overdue cutoff means the hourly sync has |
| 47 |
* been failing or skipped for hours: warn with the last sync time. |
| 48 |
* 5. Auto-sync disabled: neutral "last import" badge — sync wording would be |
| 49 |
* false framing and an overdue warning would be permanent noise. |
| 50 |
* 6. Otherwise: syncing normally. |
| 51 |
* |
| 52 |
* @param array<string, mixed> $status The task's recorded run status meta |
| 53 |
* (state, handled, expected, error, activity_at). |
| 54 |
* @param string $watermark The task's mlsimport_last_date sync watermark ('Y-m-d\TH:i'). |
| 55 |
* @param bool $cron_enabled Whether hourly auto-sync is on for the task. |
| 56 |
* @param int $now Current Unix timestamp. |
| 57 |
* @param string $watermark_stale_before Watermarks older than this ('Y-m-d\TH:i') are overdue. |
| 58 |
* @return array<string, string> Badge as level ('ok'|'warning'|'error'|'neutral'), label, message. |
| 59 |
*/ |
| 60 |
function mlsimport_task_health( array $status, string $watermark, bool $cron_enabled, int $now, string $watermark_stale_before ): array { |
| 61 |
$state = (string) ( $status['state'] ?? '' ); |
| 62 |
|
| 63 |
// 0. Never ran at all: no run status and no watermark is a task waiting |
| 64 |
// for its first manual import, not a health problem. |
| 65 |
if ( '' === $state && '' === $watermark ) { |
| 66 |
return array( |
| 67 |
'level' => 'neutral', |
| 68 |
'label' => 'Never imported', |
| 69 |
'message' => 'Run a manual import to activate this task.', |
| 70 |
); |
| 71 |
} |
| 72 |
|
| 73 |
// 1. A failed run: show the engine's own stored error message. |
| 74 |
if ( 'failed' === $state ) { |
| 75 |
return array( |
| 76 |
'level' => 'error', |
| 77 |
'label' => 'Import failed', |
| 78 |
'message' => (string) ( $status['error'] ?? '' ), |
| 79 |
); |
| 80 |
} |
| 81 |
|
| 82 |
// 2. A silent 'running' status: the worker heartbeats after every listing, |
| 83 |
// so a heartbeat older than the engine's 30-minute staleness rule means |
| 84 |
// the worker died and nothing will update this task again — call it stuck. |
| 85 |
if ( 'running' === $state && $now - (int) ( $status['activity_at'] ?? 0 ) > MLSIMPORT_TASK_HEALTH_STUCK_AFTER ) { |
| 86 |
return array( |
| 87 |
'level' => 'error', |
| 88 |
'label' => 'Stuck', |
| 89 |
'message' => sprintf( |
| 90 |
'Stopped at listing %1$d of %2$d — no worker activity for over 30 minutes.', |
| 91 |
(int) ( $status['handled'] ?? 0 ), |
| 92 |
(int) ( $status['expected'] ?? 0 ) |
| 93 |
), |
| 94 |
); |
| 95 |
} |
| 96 |
|
| 97 |
// 3. A live import: recent activity means it is genuinely progressing — |
| 98 |
// show where it is. |
| 99 |
if ( 'running' === $state ) { |
| 100 |
return array( |
| 101 |
'level' => 'ok', |
| 102 |
'label' => 'Importing', |
| 103 |
'message' => sprintf( |
| 104 |
'%1$d of %2$d listings imported.', |
| 105 |
(int) ( $status['handled'] ?? 0 ), |
| 106 |
(int) ( $status['expected'] ?? 0 ) |
| 107 |
), |
| 108 |
); |
| 109 |
} |
| 110 |
|
| 111 |
// 4a. Cron on but no watermark at all: the hourly sync refuses to start |
| 112 |
// without one (tasks imported before manual runs seeded it — GitHub issue |
| 113 |
// #202 follow-up). Say so plainly instead of printing a blank sync date. |
| 114 |
if ( $cron_enabled && '' === $watermark ) { |
| 115 |
return array( |
| 116 |
'level' => 'warning', |
| 117 |
'label' => 'Sync not started', |
| 118 |
'message' => 'The hourly sync has no start point yet — run one manual import.', |
| 119 |
); |
| 120 |
} |
| 121 |
|
| 122 |
// 4b. Auto-sync advances the watermark on every successful hourly run, so a |
| 123 |
// cron-enabled task whose watermark is older than the overdue cutoff has |
| 124 |
// not synced successfully for hours. The fixed-width 'Y-m-d\TH:i' format |
| 125 |
// compares correctly as a plain string, no date parsing needed. |
| 126 |
if ( $cron_enabled && strcmp( $watermark, $watermark_stale_before ) < 0 ) { |
| 127 |
return array( |
| 128 |
'level' => 'warning', |
| 129 |
'label' => 'Sync overdue', |
| 130 |
'message' => sprintf( 'Last successful sync: %s.', $watermark ), |
| 131 |
); |
| 132 |
} |
| 133 |
|
| 134 |
// 5. Auto-sync disabled: "Synced"/"Sync overdue" would be false framing for |
| 135 |
// a task that never syncs — state the last import date neutrally instead. |
| 136 |
if ( ! $cron_enabled ) { |
| 137 |
return array( |
| 138 |
'level' => 'neutral', |
| 139 |
'label' => 'Auto-sync off', |
| 140 |
'message' => sprintf( 'Last import: %s.', $watermark ), |
| 141 |
); |
| 142 |
} |
| 143 |
|
| 144 |
// 6. Healthy steady state: syncing normally inside the overdue cutoff. |
| 145 |
return array( |
| 146 |
'level' => 'ok', |
| 147 |
'label' => 'Synced', |
| 148 |
'message' => sprintf( 'Last successful sync: %s.', $watermark ), |
| 149 |
); |
| 150 |
} |
| 151 |
|