Repository navigation
v2.3.0
A bug-fix release (#99, 47 fixes) with a small docs and dashboard cleanup (#98). It is a
minor version because several fixes change observable behaviour (see below) and
cargo-hammerwork adds Config::load_or_env and Config::load_or_env_with. No public
signatures change.
Behaviour changes to check when upgrading
These are all bug fixes (details under Fixed), but each changes something you can
observe:
- Retry delays are one step shorter: the first retry waits the strategy's base
delay (calculate_delay(1)), asRetryStrategydocuments. - Cron presets fire on the right days.
every_weekday_at_9amnow fires Monday to
Friday andevery_monday_at_noonon Mondays. Recurring jobs already stored with the
old expressions (0 0 9 * * 1-5,0 0 12 * * 1) keep their stored expression: re-create
them, or updatecron_scheduleto0 0 9 * * Mon-Fri/0 0 12 * * Mon. CronSchedule::matchesis true if the schedule fires at any second of the minute.HammerworkConfig::from_envreturns an error for an invalid
HAMMERWORK_DATABASE_POOL_SIZE,HAMMERWORK_WORKER_POOL_SIZE,
HAMMERWORK_JOB_TIMEOUT_SECONDSorHAMMERWORK_EVENT_BUFFER_SIZEinstead of silently
using the default.[archive]durations are saved as12h,90m, ... instead of whole days; versions
before this one cannot read a sub-day value written this way.- Spawned jobs keep their own retry strategy, timeout and trace fields when the parent
has none, and the parent is added to a child'sdepends_oninstead of replacing it. - Webhooks with
max_attempts = 0are delivered once (they were never delivered); a
rate limit with a burst limit of 0 behaves as a burst of 1;
AutoscaleConfig::with_min_workersraisesmax_workerswhen needed. - Statistics:
InMemoryStatsCollector'srunningis the number of jobs running now,
andBatchProcessingStats.batches_successfulrequires 95% of a batch to complete. TestQueuebehaves like the databases: dependent jobs wait for their dependencies,
statistics.failedexcludesDeadjobs, the dead-job APIs excludeTimedOutjobs and
retry_dead_jobon a live job returnsInvalidJobTransition. Tests written against
the old behaviour may need updating.- Dashboard: the "failed" counts (REST overview, queue endpoints, WebSocket) include
Failed,DeadandTimedOutjobs and agree with each other, so they can go up.
/api/jobsappliescreated_after,created_before,search, andsort_order
withoutsort_by. - CLI:
job list --failedincludesTimedOutjobs;monitor metrics/monitor healthcountDeadandTimedOutjobs as failures; the configuredlog_levelis
honoured; a config file that fails to parse still gets the environment variables.
Changed
- Docs:
docs/integration-testing.mdnow describes the real CI workflow and queue module paths.
Fixed
CronSchedule::every_weekday_at_9amandevery_monday_at_noon(and the matching
cron::presets) fire on the right days. They used numeric weekdays (1-5,1), but
thecroncrate numbers days of the week from Sunday = 1, so the "weekday" preset ran
Sunday to Thursday and the "Monday" preset ran on Sundays. They now useMon-Friand
Mon.CronSchedule::matchesis true whenever the schedule fires during the given
minute. It only looked at the first fire after the minute before, so schedules that
fire several times a minute (* * * * * *,0,30 * * * * *) or later in the minute
(30 * * * * *) did not match.- The
cron_exampleexample runs. It used five-field cron expressions, which
CronSchedule::newrejects (Hammerwork's expressions have a seconds field), so it
stopped at its first schedule. list_archived_jobswith an offset and no limit works on MySQL. MySQL has no
OFFSETwithoutLIMIT, so the query was a syntax error.- A job with a huge result TTL or result size limit stays readable.
insert_jobs
wrapped values abovei64::MAX(e.g.with_result_ttl(Duration::MAX)) to negative
numbers, and every later read or claim of the job failed to decode it. They are now
stored asi64::MAX, as job timeouts already saturate. - One job row that cannot be decoded no longer stops archival.
archive_jobs
failed and rolled back the whole batch on such a row, and since candidates are taken
oldest first, every later pass failed on it too, for every queue. The row is now
skipped (with a warning, and left inhammerwork_jobs) and the batch is filled with
the jobs after it, so the pass archives everything else. - An archival or purge age too large to subtract from now no longer panics.
archive_*_afterorpurge_archived_afterset to something like
chrono::Duration::MAX(an easy way to say "never") panicked inarchive_jobsand
JobArchiver::run_scheduled_pass. Such an age now matches nothing, and a zero or
negative age matches everything finished so far, as before. ArchivalStats::compression_ratiocounts payloads stored uncompressed as 1.
They counted as 0, so a batch in which a few payloads compressed reported a ratio
below 1, as if archiving had grown the data.- A job cancelled by a fail-fast workflow or batch on MySQL keeps its satisfied
dependencies. MySQL marked themfailed, unlike PostgreSQL, so retrying such a job
withretry_jobleft itPendingforever. Jobs failed because a dependency failed
still have their dependencies marked failed on both backends. KeyManager::get_keyrefuses a key that has expired while cached. Keys are cached
for up to an hour, and the cache did not check the key's expiry, so an expired key
could still be used for up to an hour after it expired.- The first retry waits the retry strategy's first delay. A claim already counts the
run inattempts, so after the first failed runattemptsis 1, but the worker asked
the strategy forcalculate_delay(attempts + 1). That skipped a step:
RetryStrategy::exponential(1s, 2.0, ..)waited 2s, 4s, ... instead of 1s, 2s, ...,
andmax_delaywas reached one retry early. Retries now usecalculate_delay(attempts),
so the first retry is attempt 1, asRetryStrategydocuments. This shortens every
retry delay computed from a strategy by one step. - Batch-mode statistics no longer count nearly every batch as successful.
BatchResult::success_rateis a percentage, but the worker compared it with0.95,
so a batch counted inBatchProcessingStats::batches_successfulonce 0.95% of its jobs
completed (and the log printed rates like "8000.0%"). The threshold is now 95%. JobBatch::into_chunksno longer panics with a batch size of 0
(with_batch_size(0)); 0 is treated as unset.JobStatistics::runningcounts the jobs that are running, not everyStarted
event in the window, which included runs that had finished.- A timed-out run that is retried is counted once in the statistics. It records
TimedOutthenRetried, and both counted as finished runs: one timeout followed by
a successful retry reported 3 runs and a 67% error rate instead of 2 and 50%. JobGroup::update_statisticsfollows the backends' rules
(WorkflowProgress::status). AContinueOnFailureorManualworkflow whose jobs
had all finished, some failed, stayedRunningforever; it could also overwrite
Cancelled, and made an empty workflowCompleted.WorkerPool::with_stats_collectorreaches the workers already in the pool. After
from_config(..)orfrom_hammerwork_config(..), only workers added later and
autoscaled clones got the collector.- Workers whose jobs fail and are retried no longer raise starvation alerts. The
worker's last job time moved only on completed, failed, dead and timed-out runs, so a
worker busy retrying jobs looked idle. - A rate limit with a burst limit of 0 no longer stalls the worker forever. The token
bucket had no capacity, soRateLimiter::acquirenever returned; it now holds at least
one token. PriorityWeights::total_weightsaturates instead of overflowing (a panic in debug
builds) when weights sum pastu32::MAX.AutoscaleConfig::with_min_workerskeepsmax_workers >= min_workers.
with_min_workers(15)left the maximum at 10, so the pool could neither scale up past
10 nor scale down.- Spawned jobs keep their own settings when the parent has none.
SpawnManager::prepare_spawncopied the parent's retry strategy, timeout and trace
fields onto each child even when the parent had none, wiping the child's own (retry
strategy and trace context are inherited by default). They are now inherited only
when the parent has them. It also replaced the child's own dependencies with the
parent; the parent is now added to them. TestQueuebehaves like the database backends in several places where tests
written against it could pass while the code fails in production:- It holds a job enqueued with
depends_onuntil its dependencies complete (it handed
it out at once), inenqueue,enqueue_batchandenqueue_workflow. A job enqueued
after a dependency failed is enqueued asFailed. finish_job_runreports the jobs a completion released in
RecordedOutcome::unblocked(always empty for workflow jobs), and records a retried
run'serror_messageandtimed_out_at.requeue_stale_jobsreschedules a recurring job that is out of attempts for its next
occurrence instead of leaving itDead.get_dead_jobs,get_dead_jobs_by_queue,get_dead_job_summaryand
purge_dead_jobscoverDeadjobs only (they includedTimedOutjobs), and the
purge keeps jobs that died exactly at the cut-off (failed_at < older_than).- A weighted dequeue whose candidate weights are all zero takes the highest priority
instead of returning nothing, and a huge weight (e.g.u32::MAX) no longer tries to
allocate gigabytes. retry_dead_jobon a job that is not dead fails withInvalidJobTransition, not
HammerworkError::Queue.get_queue_statsreportsstatistics.failedas theFailedjobs only (it added the
Deadones, which are instatistics.dead).
- It holds a job enqueued with
[archive]durations survive saving a configuration, and invalid ones are
rejected.archive_afteranddelete_afterwere written as whole days, so
HammerworkConfig::save_to_fileturnedarchive_after = 12hinto"0d", and
reloading it archived every finished job at once. They are now written in the largest
exact unit ("30d","12h","90m","45s"), andh,mandsare read as well
asdand bare numbers (days). A negative duration (which made every finished job
eligible) or one too large forchrono::Duration(which panicked) is rejected when the
file loads. Files written by this version with sub-day durations cannot be read by
older versions.HammerworkConfig::from_envrejects invalid numbers in
HAMMERWORK_DATABASE_POOL_SIZE,HAMMERWORK_WORKER_POOL_SIZE,
HAMMERWORK_JOB_TIMEOUT_SECONDSandHAMMERWORK_EVENT_BUFFER_SIZE, naming the
variable, as the encryption variables already did. They fell back to the defaults
silently, soHAMMERWORK_JOB_TIMEOUT_SECONDS=30smeant 5 minutes.webhooks::verify_hmac_signatureaccepts the signature webhooks send, and compares
in constant time. Deliveries sendX-Hammerwork-Signature: sha256=<hex>, but the
verifier only accepted bare hex, so verifying a received header always failed; and the
comparison stopped at the first differing byte despite claiming to be constant-time.
It now accepts both forms and compares withMac::verify_slice.- A webhook with
retry_policy.max_attempts = 0is delivered once. The delivery
loop made no attempt at all, so every event was dropped without a log or statistic.
max_attemptsis documented as counting every attempt, including the first. - An alert cooldown too long for
chronosuppresses repeats instead of failing.
Withwith_cooldown(Duration::MAX)("alert once"), every later check of the same alert
returnedErr(Alerting("Invalid cooldown duration")). - Huge durations no longer time jobs out at once or panic.
Job::should_timeout
treated a timeout too large forchronoas zero, so such a job timed out as soon as it
started;Job::with_delaypanicked on a delay past the representable range (it now
saturates); andRetentionPolicy::calculate_deletion_timepanicked when the deletion
time fell past it (it now keeps the data, as for durationschronocannot hold). cargo hammerwork monitor metricsandmonitor healthcount dead and timed-out
jobs as failures. They counted onlyFailedjobs, but a job out of retries ends
Deadand a timed-out oneTimedOut, so the failure rate and the recent-failures
health check missed the usual ways a job fails.cargo hammerwork job list --failedincludes timed-out jobs, asjob retrydoes,
andqueue statsshows an icon forTimedOutjobs.- A
cargo hammerworkconfig file that cannot be parsed no longer drops the
environment variables. The CLI fell back to the built-in defaults, losing
DATABASE_URLand theHAMMERWORK_*variables; it now warns and applies the
environment to the defaults. cargo hammerworkuses the configuredlog_level(config file or
HAMMERWORK_LOG_LEVEL) when neither--verbosenor--quietis given. It was
documented but ignored, because logging was set up before the configuration loaded.- The dashboard's job lists apply the
created_after,created_beforeandsearch
filters (GET /api/jobs,/api/queues/{name}/jobs), which were parsed but ignored,
and honoursort_orderwithoutsort_by(or with an unknown one), which always
listed newest first. - The dashboard counts jobs in
Failedas failed. REST (failed_countin
/api/queues,failed_total, and the overview'sfailed_jobs,total_jobsand
overall_error_rate) counted onlyDeadandTimedOutjobs, while the WebSocket
failed_countcounted onlyFailed. Both now countFailed,DeadandTimedOut
jobs (dead_countis stillDeadalone), and a change inFailedjobs alone pushes a
queue update. - Dashboard pagination metadata describes the page served when
offsetis given.
/api/queuesand/api/archive/jobsreportedpage=1,has_prev=falseand
has_next=truefor?offset=100&limit=50of 120 items. - The
hammerwork-webdocumentation lists the real endpoints: job actions are
POST /api/jobs/{id}/actions(notPOST /api/jobs/{id}/retryor
DELETE /api/jobs/{id}), trends take?hours=N, archive purge is
DELETE /api/archive/purge, and there are no/api/archive/policiesendpoints.