Repository navigation
v2.5.0
A bug-fix release from a third bug hunt (#111 to #114, #118). It is a minor version
because it adds a database migration (020; run it before upgrading workers or archivers,
and off-peak on MySQL, where it rebuilds the archive table) and new public items
(job::MAX_JOB_DEPENDENCIES, job::MAX_JOB_DEPENDENTS, Job::validate_dependencies,
and the defaulted DatabaseQueue::list_queue_names and
DatabaseQueue::purge_archived_jobs_in_queue), and because several fixes change
observable behaviour (see below). No public signatures change.
Behaviour changes to check when upgrading
cargo hammerwork batch cancelcancels onlyPendingjobs by default. It used
to deleteRunningjobs too, out from under their workers; pass--status running
to cancel those.cargo hammerwork maintenance dead-jobsmarks aRunningjob dead only when it
was abandoned, by the rule of the library's stale-job recovery: its lease expired, or
it holds no lease and started more than--stale-hoursago. A long-running job that
keeps heartbeating is no longer marked dead.- "Failed" includes
TimedOuteverywhere in the CLI (one shared definition: status
Failed,DeadorTimedOut, aged byCOALESCE(failed_at, timed_out_at)).queue healthcounts timed-out jobs as recent failures,job purge --faileddeletes them
(--deadstill meansDeadonly), andbatch retryretries them by default and
accepts--status timed_out. webhook add/update --max-retries Nnow means N retries after the first delivery
(stored as N + 1 attempts); it used to give N - 1 retries. Webhooks added before keep
their stored attempts andwebhook listshowsattempts - 1as their retries.- Dashboard queue routes (
/api/queues/{name},.../actions,.../jobs)
percent-decode the queue name, soemail%20jobsis the queueemail jobs; a name that
is not valid UTF-8 once decoded is a 400. POST /api/jobs/searchreads jobs in every status (failed, timed out, completed,
running, retrying and jobs scheduled for later, up to 200 per status and queue), not just
the ready, dead and recurring ones.statusesaccepts the same names as the jobs
listing (failedcoversFailed,DeadandTimedOut). The route needsT: JobHistory.GET /api/system/metrics: the dashboard serves no/metricsroute. The
metrics_endpointfield is kept for compatibility but documented as not served;
custom_metrics_countandlast_scrapeare always null.WebhookManager::shutdownandStreamManager::shutdownunsubscribe their listeners
from the event manager, soEventManager::subscription_countandpayload_wantedstop
counting them. Dropping anEventSubscriptiondoes not unregister it: call
EventManager::unsubscribe(id)when done (now documented).- Webhook statistics:
successful_deliveries,failed_deliveriesandsuccess_rate
count events by final outcome (an event delivered on its third attempt is one successful
delivery, not two failures and a success);total_attemptsstill counts every HTTP
attempt, retries included. EventFilterprocessing-time range rejects events that have no processing time when
a minimum or maximum is set (it used to let them through).HammerworkConfig::save_to_filewrites the file with mode 0600 on Unix (through a
temporary file renamed over the target);from_filevalidates the[encryption]section
likeJobQueue::from_config.- Streaming
Hashpartitioner uses a stable FNV-1a hash, so partition keys change
(they were a character sum, which collided for anagrams). RetryStrategy::Linearstarts atbase. The first retry (calculate_delay(1))
now waitsbase, thenbase + increment,base + 2 × incrementand so on
(base + (attempt - 1) × increment), likeFixed,ExponentialandFibonacci
since 2.3.0 and as the variant's documentation said. It used to wait
base + incrementfirst, so every linear delay is now oneincrementshorter.- A job can depend on at most 1000 jobs on every backend.
enqueue,
enqueue_batch,enqueue_workflow,enqueue_cron_joband spawned children
(complete_job_run_with_children) refuse a job with more (or with more than 10000
cacheddependents) withHammerworkError::Workflow, before writing anything.
PostgreSQL already refused them through aCHECKconstraint (with a database error);
MySQL andTestQueueaccepted them.JobBatch::validateandJobGroup::validate
run the same check; it is public asJob::validate_dependencies, with the limits in
job::MAX_JOB_DEPENDENCIESandjob::MAX_JOB_DEPENDENTS. get_workflow_statuscomputescompleted_jobs,failed_jobsandstatusfrom the
workflow's jobs (archived ones included) when it is read, asget_batch_status
does, instead of returning the stored counters. A workflow leftrunningby the
archive bug below reads as finished.get_workflow_jobs(andJobGroup::jobs)
also lists archived members, with statusArchived.- Spawned children count towards their workflow. A workflow whose job spawns
children now has a largertotal_jobsand completes after them (see Fixed). JobArchiver::run_scheduled_passapplies per-queue policies (set_policy): each
queue is archived and purged with its own policy, every other queue with the default
one, and a disabled policy leaves its queues alone. A pass with per-queue policies
works through the queues one at a time, using the new (defaulted)DatabaseQueue
methodslist_queue_namesandpurge_archived_jobs_in_queue.
WorkerPool::with_archival(a single policy) is unchanged.TestQueuematches the database backends in four places (see Fixed), so tests
written against it may need their expectations updated:get_batch_statusreports
Pendingfor a batch none of whose jobs has finished (it saidProcessing) and
Failedwhen every job failed (it saidPartiallyFailed), witherror_summaryworded
"N of M jobs failed";get_ready_jobsreturns jobs by priority, thenscheduled_at;
and a restored job starts over with no attempts. (#112)
Database
- Migration 020 (
020_archive_retry_strategy_and_workflow_index) indexes
hammerwork_jobs_archive(workflow_id)on PostgreSQL and MySQL. On MySQL it also
widenshammerwork_jobs_archive.retry_strategyfromVARCHAR(50)toTEXTand turns
the archive's timestamps intoTIMESTAMP(6), likehammerwork_jobs. It is idempotent.
Run it before upgrading: on MySQL, archiving a job with a retry strategy needs it.
On MySQL the column changes rebuildhammerwork_jobs_archive, which takes a while on a
large archive; run it off-peak (purge old archived jobs first to make it quicker). - Migration 012 (PostgreSQL) no longer runs its own
BEGIN;/COMMIT;: the runner
already wraps each migration in a transaction, and the script'sCOMMITended it
early. MySQL migrations 010, 018 and 019 no longer use double-quoted string literals,
whichANSI_QUOTESreads as identifiers. Re-running them is unaffected.
Fixed
- Archiving measures job age on the database clock.
archive_jobscompared the
database-writtencompleted_at/failed_at/timed_out_atagainst a cutoff taken from
the application clock, so clock skew could archive jobs early or late. The cutoff is now
read from the database inside the archive transaction (#118). config setandconfig resetno longer write environment variables into
config.toml. They used to save the merged configuration, copyingDATABASE_URL
(with its password) and theHAMMERWORK_*variables into the file. They now change
the file alone, and write it through a temp file with mode 0600 renamed into place.config setrefuses to overwrite a config file that cannot be parsed (it used to
replace it with the defaults plus the environment). Fix or remove the file, or run
config reset --confirm.config show's Source column tellsEnvironment,Config FileandDefault
apart for every setting (it said "Config File" for environment values and defaults).archive run --dry-runreports how many jobs match the policy, using the same
predicate as the real run, instead of only echoing the flags.webhook: the webhook file is created with mode 0600 from the start, not
chmod-ed after it was written.backup restore --skip-existingis hidden (it is a no-op: existing jobs are always
skipped), andarchiveresolves the database URL like the other commands.- A deserialized
CronScheduleis rebuilt from its expression and timezone, so it fires
(it used to have no schedule and never matched); an invalid expression or timezone is a
deserialization error. - Concurrent checks of the same alert reserve the cooldown under the write lock, so the
alert is sent once instead of once per concurrent caller. - The streaming
Hashpartitioner no longer panics on overflow in debug builds. - MySQL archival stopped for good on jobs with a retry strategy. The archive's
retry_strategy VARCHAR(50)column was too short for nearly every serialized
strategy, so strict MySQL rejected the insert (error 1406), the batch rolled back, and
since candidates are taken oldest first every later pass, including
WorkerPool::with_archival, failed on the same jobs. Migration 020 widens it. (#111) - MySQL archive timestamps were rounded to the second. (#111)
- A workflow never completed once one of its finished jobs was archived: its
progress counted onlyhammerwork_jobs. Workflow counters and status (every terminal
transition,get_workflow_status) now count archived member jobs with the status they
were archived with, on PostgreSQL, MySQL andTestQueue. (#111) - Children spawned by a workflow job joined the workflow without raising its
total_jobs, so the workflow could complete before they ran, and a child's later
failure never failed it.complete_job_run_with_childrennow raises the total of each
workflow its children join, in the same transaction, on both backends and
TestQueue. (#111) - The dependency limit differed between backends (see the behaviour change
above). (#111) JobArchiver::run_scheduled_passignored per-queue policies although
set_default_policydocuments that the default applies only to queues without a
policy of their own. (#111)RetryStrategy::Lineardocumentation and behaviour disagreed (see the behaviour
change above). (#111)- Cleanups:
JobStatus::Retryingis documented as never written by Hammerwork, the
batch-counter comment in both backends is accurate again,get_priority_statsdecodes
priorities withJobPriority::from_i32, and a worker no longer reads a batch's status
twice after a job's final failure. (#111) TestQueuediverged from PostgreSQL and MySQL (#112):restore_archived_jobonly set the job back toPending; it now resets attempts,
times, error and result like the backends (scheduled at the mock clock's now), so a
restored dead job no longer dies after one run.get_batch_statustallies with the backends' rule and counts archived members with
the status they were archived with (it ignored them).get_ready_jobsreturned jobs still waiting on their dependencies (outside
workflows) and in no particular order; it now filters ondependency_status
(noneorsatisfied) and orders by priority, thenscheduled_at.cancel_workflowskippedRetryingjobs, left the dependency status of waiting
jobs atwaiting, kept their leases and overwrote the workflow'sfailed_at; it
now fails every unfinished job as the backends do (waitingbecomesfailed) and
keeps an earlierfailed_at.