Repository navigation
v1.27.0: APICronScheduler — scheduled calls of API routes - #189
Merged
Merged
Conversation
- APICronJob: a route called on a cron expression (CronExpression, 5 fields + IANA time zone, daylight saving aware), declared by @APICron on reflected routes, APIRoot.loadCronJobs() or the cron.jobs config. - Called through APIRoot.call with APIAuthentication.system, requester source internal and X-Cron-* headers; follows the X-Continuation header (APIResponse.continuation) up to maxCalls; retries with exponential backoff (APICronRetry), honoring Retry-After. - Each occurrence runs once (claimed in the APICronStore) and a job never runs in parallel (a lock with a lease, renewed while running); an interrupted run is resumed from its continuation after the lease expires; missed occurrences: the latest runs once within maxLateness, older ones are recorded as skipped. - APICronStoreMemory and APICronStoreDB (api_cron_job_run and api_cron_job_lock tables), history pruning, APICron logger. - Started by APIRoot.initialize when cron.enabled (not in auxiliary isolates), stopped by APIRoot.close. - New dependency: timezone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ntities Coverage of the cron files: expression 99%, job/config 100%, store 97%, scheduler 99%. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## master #189 +/- ##
==========================================
+ Coverage 95.44% 95.53% +0.09%
==========================================
Files 69 73 +4
Lines 24215 25174 +959
==========================================
+ Hits 23111 24049 +938
- Misses 1104 1125 +21
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
…o overlap - The scheduler starts once APIRoot and its modules are fully initialized (outside the init hook; cronSchedulerReady), not inside initialize(). - A run is only written by its owner while running (updateRun checks the owner/status; previousOwner on take-over); an instance that lost the lock (or is stopping) writes nothing more, checked after every call. - The lock is considered lost when its lease runs out unrenewed. - A timed-out call is waited for (logged), never overlapped by a retry. - A continuation token over 4096 chars, or an error while running, fails the run instead of leaving it running (no endless resume). - Lock renew/release verify the row after writing (no auto-inserted garbage lock); pruning never deletes a running run. - runNow never takes a scheduled occurrence slot; Retry-After capped by maxDelay; graceful stop (waitRunning); warning for the in-memory store; lastRun/selectRuns read only the latest rows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ration, retry options - @APICron.at(minute:, hour:, dayOfMonth:, month:, dayOfWeek:, ...): the schedule by its cron fields (default '*'); the default constructor keeps the cron expression. - Both constructors take timeout, maxLateness, leaseDuration and retry (attempts overrides the attempts of retry). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… the lateness window - Fix: history pruning deleted runs still in their job's maxLateness window, so a long-lateness occurrence could run again after pruning. - CronExpression property tests (random expressions and instants against a minute by minute scan: UTC, Europe/Paris, America/Sao_Paulo). - DB integration (memory, PostgreSQL, MySQL) with the real APIRoot lifecycle: two instances with cron.enabled on one DB run an occurrence once (DB store auto-resolved); a root closed mid-run is resumed by another. - APIServer integration: the jobs run as the system at start; HTTP clients with X-Cron-* headers are refused; stopping the server stops the scheduler. - Scheduler: several jobs in one check, a failing job doesn't stop the others, POST jobs, config override of an @APICron.at job. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…lock, bad config entries - A non-map `cron` config no longer breaks APIRoot.initialize: a bool (or bool string) is `enabled`, any other value is disabled; scheduling the start never fails the initialization. - After stop(), tick() and runNow() do nothing (no junk running runs). - A lock renewal in progress is awaited before releasing the lock (no ghost lock re-inserted by a late renewal). - A bad or stale `cron.jobs` entry is skipped (logged), an override of an unknown job ignored; a non-map `retry` ignored; top-level `attempts`. - APICronJob validates positive timeout/maxLateness/leaseDuration and retry.attempts >= 1; the time zone is resolved lazily (skipped if invalid). - Duplicated cron job names in a module are an error; an @APICron job is only added with its route. - The scheduler starts (and checks) only after a successful initialization; cronSchedulerReady works before initialize and is reset by close; startCronScheduler restarts a stopped scheduler; a rejected final save is reported as interrupted. - Docs: timeout/retry, config overrides, resume timing, skipped cap. - Tests for each, `_waitUntil` fails on timeout, no fixed real-time waits. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds a built-in scheduler that calls API routes on cron schedules, so apps don't need an external cron hitting a secret-protected route.
Jobs (
APICronJob)CronExpression: lists, ranges, steps, names,@daily/@hourly/...) evaluated in an IANA time zone, DST aware (a nonexistent time runs at the next valid instant; a repeated time runs once).@APICronon reflected route methods,APIRoot.loadCronJobs(), or thecron.jobsconfig (which can also override or disable code jobs by name).APIRoot.callwithAPIAuthentication.system(passesAPIRouteAuthenticatedRule), requester sourceinternal, andX-Cron-Job/X-Cron-Scheduled-At/X-Cron-Run/X-Cron-Attemptheaders.X-Continuation(APIResponse.continuation), the route is called again withcontinuation=<token>, up tomaxCalls.APICronRetry): exponential backoff (default 3 attempts: 30s, 2m), honorsRetry-After;UNAUTHORIZED/BAD_REQUEST/NOT_FOUNDaren't retried; per-calltimeout.Scheduler (
APICronScheduler)APIRoot.initializewhencron.enabled: true(not in auxiliary worker isolates: one per process), stopped byAPIRoot.close.maxLateness(default 1 day); older ones are recorded asskipped.runNow(job),selectRuns(job), history pruning (history_days), loggerAPICron.Store (
APICronStore)APICronStoreMemory(single instance, tests) andAPICronStoreDB(api_cron_job_run,api_cron_job_lock). It uses only inserts with unique constraints and deletes by ID or by expired lease, so it works on PostgreSQL, MySQL and the memory adapters with no adapter-specific locking.APIRoot.loadCronStore()picks the DB store whenAPICronStoreDB.buildRepositories(adapter)is registered.Notes
timezone.APIModulegainscronJobs/addCronJob. Apps with generated module reflection need to regenerate it (build_runner) for the new field.Review fixes (31a1aa8)
APIRootand its modules are fully initialized, outside the init hook (cronSchedulerReady). Before, the first check could call a route before a module'sinitialize()finished.updateRunonly writes a run that is stillrunningand owned by the writer (previousOwneron take-over). An instance that lost its lock, or is stopping, writes nothing more; this is checked after every call. A finished run is never put back torunning.runningto be resumed forever.lastRun/selectRunsread only the latest rows.runNownever takes a scheduled slot;Retry-Afteris capped atmaxDelay; gracefulstop()+waitRunning(); a warning when the in-memory store is used.Review fixes (2) (890cbb3)
cronconfig: acronvalue that isn't a map no longer breaksAPIRoot.initialize. A bool (or bool string) meansenabled; any other value disables cron. Scheduling the start never fails initialization.stop(),tick()/runNow()do nothing untilstart()again.cron.jobsentry is skipped and logged; an override of an unknown job is ignored. A top-levelattemptskey is supported.timeout/maxLateness/leaseDuration,retry.attempts >= 1; the time zone is resolved lazily (a job with an invalid zone is skipped).@APICronjob is only added with its route.cronSchedulerReadyworks beforeinitializeand is reset on close;startCronSchedulerrestarts a stopped scheduler; a rejected final save is reported as interrupted.Also added since the first description
@APICron.at(minute:, hour:, dayOfMonth:, month:, dayOfWeek:, ...); both constructors taketimeout,maxLateness,leaseDuration,retry.maxLatenesswindow (otherwise the occurrence could run again).CronExpressionproperty tests against a minute-by-minute scan; DB integration through the realAPIRootlifecycle (two instances on one DB run an occurrence once; a closed instance is resumed by another);APIServerintegration (forgedX-Cron-*headers are refused).Tests
test/bones_api_cron_test.dart: cron parsing and next/previous times (UTC, Europe/Paris DST gap/overlap, America/Sao_Paulo), config/merging, retry/backoff/Retry-After, timeout, continuation andmaxCalls, system auth and headers, locks, two schedulers on one store, no-parallel, resume, missed/skipped, history pruning,@APICronon reflected routes,APIRootintegration.test/bones_api_cron_db_*_test.dart:APICronStoreDBon memory, PostgreSQL and MySQL (docker): unique claims, locks (renew/take-over/release), parallel claims and locks with one winner, and two schedulers on the same DB including resume.890cbb3: unit (-x docker -x concurrency) 2150 pass,-t docker329 pass, concurrency 86 pass;dart analyze --fatal-infosclean;pub publish --dry-run0 warnings.890cbb3: all checks green (unit, docker, postgres, mysql, concurrency, chrome ×3, codecov).🤖 Generated with Claude Code