Alert and record a lost recurring-ticket occurrence on creation failure (#88)
RecurringTicketModel::claimForRun() deliberately advances next_run_at before TicketModel::createTicket() runs, to prevent duplicate-ticket floods if creation fails partway and the cron retries. The tradeoff: if createTicket() then fails, that specific occurrence is gone forever with no record anywhere an admin would normally look — the catch block only wrote a line to stdout/the cron log. Added recordMissedOccurrence(), called from both the "createTicket() returned success:false" branch and the exception catch, which writes an audit_log entry (entity_type='recurring_ticket', action_type='error') and fires a new NotificationHelper::sendSystemAlert() — a generic operational alert (unlike the ticket-specific notification methods, it has no associated ticket) sent to the shared MATRIX_NOTIFY_USERS list regardless of any per-event toggle, so a silently-skipped recurring ticket surfaces immediately instead of requiring someone to grep cron logs. Verified against real MariaDB and a real webhook-capturing server: calling the recorder writes the audit_log row with the failure reason and schedule details, and fires the Matrix alert with the same information. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Lhz7pGMaoTfL5sdYS5XiKv
This commit is contained in:
@@ -29,6 +29,38 @@ function logMessage($message)
|
|||||||
echo "[" . date('Y-m-d H:i:s') . "] " . $message . "\n";
|
echo "[" . date('Y-m-d H:i:s') . "] " . $message . "\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Record a recurring-ticket occurrence that was claimed (next_run_at already
|
||||||
|
* advanced to the next future run) but then failed to actually produce a
|
||||||
|
* ticket. That claim-then-fail ordering is deliberate — it stops a failing
|
||||||
|
* creation from re-firing and flooding duplicates on every subsequent cron
|
||||||
|
* tick — but means this specific occurrence has no other record anywhere an
|
||||||
|
* admin would normally look: no audit_log entry (nothing was created), no
|
||||||
|
* Matrix "ticket created" alert, no failure table. Without this, it's simply
|
||||||
|
* gone, silently, forever.
|
||||||
|
*/
|
||||||
|
function recordMissedOccurrence($auditLog, $recurring, $reason)
|
||||||
|
{
|
||||||
|
$auditLog->log(
|
||||||
|
$recurring['created_by'],
|
||||||
|
'error',
|
||||||
|
'recurring_ticket',
|
||||||
|
(string)$recurring['recurring_id'],
|
||||||
|
[
|
||||||
|
'reason' => $reason,
|
||||||
|
'title_template' => $recurring['title_template'],
|
||||||
|
'schedule_type' => $recurring['schedule_type'],
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
NotificationHelper::sendSystemAlert(
|
||||||
|
"Recurring ticket occurrence lost: schedule #{$recurring['recurring_id']} "
|
||||||
|
. "(\"{$recurring['title_template']}\") was claimed for this run but ticket "
|
||||||
|
. "creation failed, so this occurrence will not be created or retried.",
|
||||||
|
['reason' => $reason, 'recurring_id' => $recurring['recurring_id']]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
logMessage("Starting recurring tickets cron job");
|
logMessage("Starting recurring tickets cron job");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -100,11 +132,17 @@ try {
|
|||||||
|
|
||||||
$created++;
|
$created++;
|
||||||
} else {
|
} else {
|
||||||
logMessage("ERROR: Failed to create ticket - " . ($result['error'] ?? 'Unknown error'));
|
$reason = $result['error'] ?? 'Unknown error';
|
||||||
|
logMessage("ERROR: Failed to create ticket - " . $reason);
|
||||||
|
recordMissedOccurrence($auditLog, $recurring, $reason);
|
||||||
$errors++;
|
$errors++;
|
||||||
}
|
}
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
logMessage("ERROR: Exception processing recurring ticket - " . $e->getMessage());
|
logMessage("ERROR: Exception processing recurring ticket - " . $e->getMessage());
|
||||||
|
// claimForRun() already advanced next_run_at before this point, so
|
||||||
|
// this occurrence is permanently gone unless recorded somewhere an
|
||||||
|
// admin would actually look — a cron log line alone doesn't count.
|
||||||
|
recordMissedOccurrence($auditLog, $recurring, $e->getMessage());
|
||||||
$errors++;
|
$errors++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,6 +59,23 @@ class NotificationHelper
|
|||||||
|
|
||||||
// ─── Public event methods ─────────────────────────────────────────────────
|
// ─── Public event methods ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generic operational alert with no associated ticket (e.g. a recurring
|
||||||
|
* schedule whose ticket creation failed after its next_run_at was
|
||||||
|
* already advanced, so the missed occurrence has no other record an
|
||||||
|
* admin would normally see). Always sent to the shared
|
||||||
|
* MATRIX_NOTIFY_USERS list, regardless of any per-event notify toggle.
|
||||||
|
*/
|
||||||
|
public static function sendSystemAlert(string $message, array $context = []): void
|
||||||
|
{
|
||||||
|
self::fire(array_merge([
|
||||||
|
'event' => 'system_alert',
|
||||||
|
'message' => $message,
|
||||||
|
], $context, [
|
||||||
|
'notify_users' => self::notifyUsers(),
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* New ticket created (manual or automated/API).
|
* New ticket created (manual or automated/API).
|
||||||
*
|
*
|
||||||
|
|||||||
Reference in New Issue
Block a user