From ca1243c39633cfb39d3f33d8fc97dccd84e54d13 Mon Sep 17 00:00:00 2001 From: Ken Johnson Date: Wed, 6 May 2026 11:32:25 -0700 Subject: [PATCH] v0.1.0: consolidate intermediate tags into single fresh release Wipes v0.1.0/v0.1.1 history. fpc-cron hadn't shipped to any external consumer yet; the rebuild against fpc-db v0.4.0 was inline development noise. This commit + tag is the clean public starting point. CRON_VERSION reset to 0.1.0. CHANGELOG rewritten as a single 0.1.0 entry covering the full library surface as it stands now. --- CHANGELOG.md | 293 ++++++++++++++++--------------------------- src/cron.version.pas | 4 +- 2 files changed, 107 insertions(+), 190 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 270ca67..ea1a1da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,218 +10,135 @@ Semver intent: - **minor** — additive features - **patch** — bug fixes, internal cleanups -## 0.1.1 — 2026-05-06 +## 0.1.0 — 2026-05-06 -Rebuild against fpc-db **v0.4.0** (namespace rename `db.*` → -`database.*`). No behaviour change. 73/73 assertions still pass. - -### Changed - -- `uses` clauses across `cron.runner` / tests / examples / - docs swept from `db.{types,pool,dialect,schema}` to - `database.{types,pool,dialect,schema}`. See fpc-db v0.4.0 - changelog for the full mapping and rationale. - -### Action for consumers - -Pin against fpc-cron v0.1.1 + fpc-db v0.4.0 together, OR stay -on fpc-cron v0.1.0 + fpc-db v0.3.0. Mixing across the boundary -won't compile. - -## 0.1.0 — 2026-05-05 - -Initial release. Verbatim port of Fastway-Server's `TFWScheduler` -class from `fw_scheduler.pas` per `feedback_copy_dont_reinterpret.md`. -Method bodies copied line-by-line from canonical with only the -documented mechanical translations applied; the cron parser pair -(`ParseCronField` / `MatchesCron`) is byte-identical to canonical -after type-rename normalisation. +Initial public release. Cron + interval task runner extracted +from canonical Fastway `TFWScheduler` (`fw_scheduler.pas`, +1093 lines) per `feedback_copy_dont_reinterpret.md`. Cron +parser bodies are byte-identical to canonical after the +type-rename normalisation. ### Added -- `cron.runner.TCron` — cron+interval task runner thread (port of - `TFWScheduler`, all 1093 lines of canonical `fw_scheduler.pas`). -- `cron.types` unit with `TCronTaskKind`, `TCronTask`, - `TRunTaskProc`, `TGetExtraTasksFunc`, `TSystemTaskProc`. - (Logger type is `log.types.TLogProc` from fpc-log — the - canonical ecosystem-wide logger shape.) -- `cron.events` unit with `TCronOnTaskStart` / - `TCronOnTaskComplete` / `TCronOnTaskRegistered` / - `TCronOnPluginOrphaned` / `TCronOnThreadStart` / - `TCronOnThreadStop` typed observer callbacks. Same - per-library typed-callback shape as fpc-binkp's `bp.events` - and fpc-comet's `cm.events`. -- `cron.runner.BuildSystemSchedulerSpec(ANowExpr)` and - `BuildSchedulerLogSpec` — public fpc-db `TDBTable` specs for - the two runner-owned tables. Mirrors canonical Fastway's - `fw_schema.pas` `BuildSystemScheduler` / `BuildSchedulerLog`. -- `cron.version` unit with `CRON_VERSION_MAJOR/MINOR/PATCH/STRING`. -- `fpc.cfg` — multi-target FPC config so consumers can build - with `-Fucfg=fpc.cfg`. -- `docs/API.md` — full callable reference. -- `docs/architecture.md` — layers + dependency graph + DB - schema + threading model + integration with sibling fpc-* - libraries. -- `docs/DEVELOPER_GUIDE.md` — consumer-oriented walkthrough - covering lifecycle, observer callbacks, logger plumbing, - cron grammar, and the bridging pattern to fpc-events. +- `cron.runner.TCron` — task runner thread, public surface: + - `Create(APool, ARunTask=nil, AGetExtraTasks=nil, ALogger=nil)` + — constructs in suspended state; declares schema, loads + tasks, runs `SyncPluginTasks`. + - `RegisterSystemTask(AName, AProc)` — register a callback + for a `system/` task (replaces canonical's + hardcoded case-table). + - `RefreshTasks` / `RunTaskNow(ATaskID)`. + - `GetTasksJSON: TJSONArray` / `GetTaskJSON(ATaskID): TJSONObject`. + - `UpdateTask(ATaskID, AUpdates): Boolean` — apply enabled / + schedule_type / interval_seconds / cron_expr changes, + persists to DB. + - `Running: Boolean`. + - `class function ParseCronField(AField, AMin, AMax): TBits` + - `class function MatchesCron(ACronExpr, ATime): Boolean` + - Typed observer properties: `OnTaskStart` / `OnTaskComplete` + / `OnTaskRegistered` / `OnPluginOrphaned` / `OnThreadStart` + / `OnThreadStop`. +- `cron.types` — `TCronTaskKind`, `TCronTask`, `TRunTaskProc`, + `TGetExtraTasksFunc`, `TSystemTaskProc`. +- `cron.events` — typed observer callback types + (`TCronOnTaskStart`, ...) following the bp.events / cm.events + pattern. +- `cron.runner.BuildSystemSchedulerSpec(ANowExpr): TDBTable` + and `BuildSchedulerLogSpec: TDBTable` — `database.TDBTable` + specs mirroring canonical Fastway `fw_schema.pas`'s + `BuildSystemScheduler` / `BuildSchedulerLog`. +- `cron.version` — `CRON_VERSION` semver constants. ### Adjustments from canonical (the only behaviour-preserving changes) -| Canonical | fpc-cron | -|----------------------------|--------------------------| -| `TFWScheduler` | `TCron` | -| `TFWSchedulerTask` | `TCronTask` | -| `TFWSchedulerTaskKind` | `TCronTaskKind` | -| `TRunPluginTaskProc` | `TRunTaskProc` | -| `TGetPluginTasksFunc` | `TGetExtraTasksFunc` | +| Canonical | fpc-cron | +|------------------------|------------------------| +| `TFWScheduler` | `TCron` | +| `TFWSchedulerTask` | `TCronTask` | +| `TFWSchedulerTaskKind` | `TCronTaskKind` | +| `TRunPluginTaskProc` | `TRunTaskProc` | +| `TGetPluginTasksFunc` | `TGetExtraTasksFunc` | -- **`uses` clause:** dropped `fw_consts`, `fw_log`, `fw_config`, - `fw_database`, `fw_plugin_api`, `fw_plugin_updates`, - `fw_plugin_host`, `dbapi_consts`, `dbapi_dialect`. Added - `log.types` (fpc-log), `database.dialect`, `database.schema`, `database.pool` - (fpc-db v0.3.0), and the local `cron.types` / `cron.events`. - fpc-cron does NOT depend on fpc-events — events are emitted - as typed observer callbacks per the per-library convention. -- **TDBPool dependency injection:** the canonical reaches a - global `DB` (Fastway's `TFWDatabase`). fpc-cron takes a - `TDBPool` parameter on `Create` instead. This is the ENTIRE - adjustment to LoadTasksFromDB / SyncPluginTasks / - UpdateTaskInDB / LogTaskRun — they still hit the database - directly, just through `FPool` instead of `DB`. Specifically: - - `Q := DB.NewQuery; Q.SQL.Text := X; Q.Open; ...; DB.FreeQuery(Q)` - becomes `PQ := FPool.QuerySQL(X); try ...; finally PQ.Free; end`. - - `Q.ExecSQL; DB.Transaction.Commit` becomes - `FPool.ExecSQL(SQL, Params)`. - - `DB.Dialect.InsertOrIgnoreStmt(...)` becomes - `FPool.InsertIgnore(table, cols, vals)`. - - `DB.Dialect.NowExpr` becomes `FPool.Dialect.NowExpr`. -- **Events: typed observer callbacks instead of EventBus.** - Canonical reaches the global `EventBus` and fires - `'_system' / 'scheduler.task_start'` (line 532) + - `'_system' / FW_EVENT_SCHEDULER_RESULT` (line 609). - fpc-cron exposes typed observer callbacks (`OnTaskStart` / - `OnTaskComplete` / `OnTaskRegistered` / `OnPluginOrphaned` / - `OnThreadStart` / `OnThreadStop`, assignable as properties on - `TCron`) — same per-library typed-callback pattern as - fpc-binkp's `bp.events` and fpc-comet's `cm.events`. - Consumers wanting ecosystem-wide pub/sub fan-out write a - small bridge class that forwards selected callbacks to a - fpc-events `TEventBus`. -- **Logging:** `fw_log.Log.Info/Warning/Error/Debug(...)` calls - routed through an optional `log.types.TLogProc` callback - via the internal `DoLog(level, msg)` shim, with - `Category='cron'`. Default `nil` = silent. Same logger shape - every other fpc-* library uses. -- **System-task dispatch:** canonical's hardcoded `case - ATaskName of` over Fastway-specific names (`log_cleanup`, - `db_vacuum`, `wal_checkpoint`, `config_watch`, - `plugin_update_check`, `old_news_cleanup`, etc.) becomes a - consumer-registered table. `RegisterSystemTask(name, proc)` - adds an entry; the dispatch shape (linear lookup, fallback to - `'unknown system task'` warning) is identical to canonical. -- **`DoWalCheckpointTruncate` lifted out.** Top-level procedure - in canonical (line 616) is SQLite + Fastway specific. Stays - in Fastway-Server. fpc-cron consumers register their own - checkpoint task via `RegisterSystemTask` if needed. -- **No global `Scheduler` singleton.** Canonical exports `var - Scheduler: TFWScheduler;` for app-wide access; fpc-cron - consumers create their own instance. -- **`ParseCronField` / `MatchesCron` widened to - `class function`.** Canonical has them as private instance - methods; their bodies never reference `Self`, so promoting - them to class functions lets pure-cron tests run without - spinning up a full `TCron` instance. Bodies are byte-identical - to canonical (verified by normalised diff). +- `uses` clause swaps: drops `fw_consts` / `fw_log` / `fw_config` + / `fw_database` / `fw_plugin_api` / `fw_plugin_updates` / + `fw_plugin_host` / `dbapi_consts` / `dbapi_dialect`; adds + `log.types` (fpc-log), `database.dialect` / `database.schema` + / `database.pool` (fpc-db v0.1.0), `cron.types` / + `cron.events`. +- Global `DB` replaced with `TDBPool` parameter on `Create`. +- `EventBus.Fire(...)` calls replaced with typed observer + callbacks (assignable as properties on `TCron`). Same + per-library typed-callback pattern as fpc-binkp's + `bp.events` and fpc-comet's `cm.events`. +- `fw_log.Log.X(...)` calls routed through optional + `log.types.TLogProc` callback with `Category='cron'`. +- `DoWalCheckpointTruncate` (SQLite + Fastway specific) and + the hardcoded `case ATaskName` system-task table lifted out + — consumers register their own system tasks via + `RegisterSystemTask`. +- No global `Scheduler` singleton — consumer creates own. +- `ParseCronField` / `MatchesCron` promoted from private + instance methods to `class function` for testability; + bodies byte-verbatim from canonical. ### Behaviours preserved verbatim (per the COPY rule) -Verified by normalised diff (type renames + log-call swaps -applied to canonical, then `diff -u` vs port — zero behavioural -diff after normalisation): - - `LoadTasksFromDB` / `SyncPluginTasks` / `UpdateTaskInDB` / - `LogTaskRun` hit the database directly. Looks like it should - be inverted into consumer callbacks; canonical doesn't, so we - don't. + `LogTaskRun` hit the database directly. - 5-position cron parser (`*`, `*/N`, `a,b,c`, `a-b`, `a-b/N`, - single literals). Body byte-verbatim to canonical including - the `{ Check for step }` / `{ Every value with step }` / - `{ Check for range }` / `{ Single value }` inline comments. -- `MatchesCron` body byte-verbatim including the - `DecodeDateFully(ATime, Mn, Hr, Dom, Dow)` call (whose result - is overwritten by the next four calls — cosmetic, but kept - verbatim per COPY rule). -- Cron expression interpretation in *local* time, conversion to - UTC at storage time. -- `UTCNow = LocalTimeToUniversal(Now)` for all stored timestamps. + literal). +- Cron expressions in *local* time; UTC at storage time. +- `UTCNow = LocalTimeToUniversal(Now)` for every persisted + timestamp. +- Schedule-miss-on-long-task: next firing is delay AFTER + previous run *returned*. - TThread + PRTLEvent + 1-second wake loop. -- Schedule-miss-on-long-task semantics: next firing is delay - AFTER previous run returned, not after it started. -- Pool-bound schema reconciliation: `system_scheduler` and - `scheduler_log` are declared in `Create`. -- Event names fired through the optional bus: - `scheduler.task_start` (literal, canonical line 532) and - `scheduler.task_result` (= `FW_EVENT_SCHEDULER_RESULT` = - `'scheduler.task_result'`, canonical line 609). Source plugin - name `'_system'`. All literal-inlined to match canonical. +- Suspended-Create + `.Start`. +- SyncPluginTasks orphan cleanup. +- Event names through bus bridge: `'scheduler.task_start'` and + `'scheduler.task_result'` (canonical literals). - DB table names `system_scheduler` and `scheduler_log` kept - verbatim from canonical so existing `fw_scheduler.pas` - databases can be reused. -- `TCronTask` in-memory record shape (every field, every type). -- `GetTasksJSON` / `GetTaskJSON` / `UpdateTask` JSON shapes - (every field, every name). + verbatim (Fastway database is reusable as-is). ### Known issues inherited verbatim -- `MatchesCron` allocates five `TBits` instances before its - `try`/`finally`; if a later `ParseCronField` raises, earlier - `TBits` instances leak. Per `feedback_copy_dont_reinterpret.md` - this is preserved as-is. Real-world impact is negligible - (cron expressions reaching `MatchesCron` come from the DB and - are validated upstream); flagged here for future cleanup. +- `MatchesCron` allocates the five `TBits` field-bit-arrays + before its `try`/`finally`; if `ParseCronField` raises on + field 2..5, earlier `TBits` instances leak. Negligible + real-world impact. - `MatchesCron` calls `DecodeDateFully(ATime, Mn, Hr, Dom, Dow)` - whose results are overwritten by the immediately-following - `MonthOf`/`DayOf`/`HourOf`/`MinuteOf`/`DayOfTheWeek` calls. - Pure cosmetic; canonical has it, so we have it. - -### Notes for downstream consumers - -- `Create` is constructed *suspended* (canonical - `inherited Create(True)`). Caller must call `C.Start` to - begin the wake loop. -- `Create` runs `Pool.DeclareTable` for both runner tables. - Two runners sharing a pool both declare; the second is a - no-op (DeclareTable is idempotent). -- `SyncPluginTasks` *frees* the `TJSONArray` returned by the - supplier callback. Consumers must not retain it. -- Typed observer callbacks fire synchronously on the runner - thread; heavy work in `OnTaskStart` / `OnTaskComplete` delays - subsequent dispatches. + whose results are immediately overwritten by the next four + calls. Cosmetic. ### Tests -- `tests/test_cron.pas` — 37 assertions covering ParseCronField - for `*` / `*/N` / `a,b,c` / `a-b` / `a-b/N` / single literals - / out-of-range, and MatchesCron for every-minute / top-of-hour - / daily-3am / hourly-:30 / weekday-9-17 / every-15m / Jan-1- - midnight / Sundays-at-midnight / malformed-expr. -- `tests/test_runner.pas` — 36 assertions covering Init, - NextRun-recalculation, RunTaskNow, SyncPluginTasks-registers, - SyncPluginTasks-removes-orphans, IntervalFires, SystemTask- - Fires, SystemTaskFailureRecorded, UpdateTask-disables, - RefreshTasks, typed observer callbacks (OnTaskStart + - OnTaskComplete) fire, cron task executes through the runner - (run_count + last_run + NextRun-recalc), and - two-runners-on-one-pool (idempotent DeclareTable). +73 assertions across 23 scenarios: -Total: **73 assertions across 23 scenarios.** All passing on -x86_64-linux against fpc-log + fpc-db v0.3.0. +- `tests/test_cron.pas` — 37 pure-cron assertions. +- `tests/test_runner.pas` — 36 SQLite-backed end-to-end: + Init, NextRun-recalc, RunTaskNow, SyncPluginTasks add + + orphan, IntervalFires, SystemTaskFires, + SystemTaskFailureRecorded, UpdateTask-disables, + RefreshTasks, typed observer callbacks fire, cron task + executes through runner, two runners on one pool + (idempotent DeclareTable). ### Examples -- `examples/interval_task.pas` — schedule a 5-second interval - task, run for ~12 seconds, log every firing. -- `examples/cron_task.pas` — exercise `MatchesCron` for a daily - 3 AM schedule and persist a cron task in `system_scheduler`. +- `examples/interval_task.pas` — 5-second interval task. +- `examples/cron_task.pas` — daily 3 AM cron task. + +### Build and test + +``` +bash build.sh # compile every src/*.pas +bash run_tests.sh # build + tests + examples +``` + +`fpc.cfg` provides multi-target FPC config. + +### Dependencies + +- fpc-log v0.1.0 — `log.types.TLogProc` +- fpc-db v0.1.0 — `database.{dialect,schema,pool}` diff --git a/src/cron.version.pas b/src/cron.version.pas index 1fb5b02..91b6e23 100644 --- a/src/cron.version.pas +++ b/src/cron.version.pas @@ -11,8 +11,8 @@ interface const CRON_VERSION_MAJOR = 0; CRON_VERSION_MINOR = 1; - CRON_VERSION_PATCH = 1; - CRON_VERSION_STRING = '0.1.1'; + CRON_VERSION_PATCH = 0; + CRON_VERSION_STRING = '0.1.0'; implementation