Skip to content

(Optional) improvements for handling a subset of deserialization errors#1345

Closed
barryhughes wants to merge 8 commits into
fix/schedule-serializationfrom
fix/deserialization-problem-handling
Closed

(Optional) improvements for handling a subset of deserialization errors#1345
barryhughes wants to merge 8 commits into
fix/schedule-serializationfrom
fix/deserialization-problem-handling

Conversation

@barryhughes

@barryhughes barryhughes commented Jul 11, 2026

Copy link
Copy Markdown
Member

barryhughes and others added 8 commits July 10, 2026 18:16
A structurally-valid schedule that merely nests a class Action Scheduler does not
recognize (an un-allow-listed, non-schedule class) is not evidently dangerous --
just unknown. Returning false for it, as the hardening does, makes the stores
treat it as corrupt and the queue runner silently cancel the action, losing work
a site operator might still need.

Introduce ActionScheduler_UnrecognizedSchedule (a NullSchedule placeholder that
records the unrecognized class names) and return it from the deserializer for the
recoverable ($shadow_eligible) case, so the action stays a valid, displayable
schedule. Structural rejections -- a top-level non-schedule, an unwalkable graph,
or a throwing __wakeup -- have no legitimate form and remain corrupt (false).

This is the foundation for routing such actions to a failed (reviewable, forced-
re-runnable) state instead of cancelling them.

Refs #1318

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…e-run

The queue runner now marks an action failed -- for operator review -- when its
schedule resolves to an ActionScheduler_UnrecognizedSchedule, instead of
silently cancelling it as corrupt. We do not auto-run it, because its intended
timing is unknown.

Site operators who cannot change code still need to keep work flowing, so add
force_run_action() (backed by a new $force flag on process_action()) which
executes such an action's callback despite the unreadable schedule and completes
it. A failed action would otherwise fetch as a non-executable FinishedAction, so
for the duration of a forced run it is mapped to an executable action class. A
new action_scheduler_unrecognized_schedule_action hook fires on the failure.

Refs #1318

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…d schedules

The admin "Run" row action now force-runs a failed action (previously a no-op,
since a failed action is not pending), so an operator can retry a failed action or
flush one whose schedule could not be recognized -- without needing to change
code. Pending actions keep the normal run path.

The Scheduled Date column renders an unrecognized schedule as "Unrecognized
schedule" instead of falling through to the NullSchedule "async" label.

Refs #1318

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
When the queue runner fails an action because its schedule references an
unrecognized class, record a (non-autoloaded) flag. On admin pages, show a notice
to users who can manage options, linking to the failed actions screen and
explaining they can force-run an action that is safe. The notice renders until
the operator dismisses it (a nonce-guarded dismissal clears the flag); a later
failure sets it again.

Refs #1318

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Review follow-up. The list table routes every failed action to force_run_action(),
including one that retains a valid recurring schedule. process_action() would then
reach the tail reschedule and schedule another instance -- but the series already
advanced when the action first ran, so this created a duplicate. Suppress the
reschedule on any forced run (which also makes the code match force_run_action()'s
documented contract).

Refs #1318

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Translators should not need to understand HTML. Restructure the notice so every
translatable string is plain text: the sentence carries only numbered
placeholders, and the anchor text ("failed", "Dismiss") and label are separate
plain strings. Markup is assembled in PHP and interpolated via sprintf, with each
dynamic part escaped (esc_url/esc_html__).

esc_html__() on the sentence also prevents a rogue translation from injecting
markup, so wp_kses() -- which would instead allow whitelisted tags through -- is
no longer needed.

Refs #1318

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Splitting the anchor text into separate __() calls made the sentence a set of
fragments, which translates poorly (word order, agreement and declension vary by
language, and a lone "failed" has no context). Keep the entire sentence as a
single translatable string and pull only the HTML tags out as opening/closing
placeholder pairs, so the linked phrases stay in-sentence and can move as a unit.

Escaping is unchanged: esc_html__() on the sentence still neutralizes rogue-
translation markup, and each opening tag carries an esc_url()'d URL, so no wp_kses.

Refs #1318

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Document the new $force parameter on process_action(); add the missing short
description to ActionScheduler_UnrecognizedSchedule::__construct(); expand the
single-line test closures and the associative array to satisfy the WordPress
coding standard; and annotate the deliberate $_GET cleanup in a test.

Refs #1318

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@barryhughes

barryhughes commented Jul 13, 2026

Copy link
Copy Markdown
Member Author

Closing. This is currently incomplete, and it's not really clear if we are going in the right direction: what we really need to do is have a little more discussion about when and how we notify site operators of problems.

The specific issue we were concerned with here (cancellation due to unsafe scheduling data) is expected to occur only very rarely, and many sites will never encounter it at all. This begs the question, why go to special measures for this scenario when other action failures receive no such additional support (yet, something like a series of failing payment actions could be a serious matter for a merchant)?

A partial answer is that by merging #1344 we would be introducing a new reason for automatic action cancellation that did not previous exist, but the question still stands.

Last note: some (at least partly related) prior art exists in the form of past-due notices and the past-due action view.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant