Files
fpc-cron/docs/architecture.md

219 lines
9.9 KiB
Markdown

# fpc-cron — architecture
## Layers
```
┌──────────────────────────────────────────────────┐
│ Caller (BBS, daemon, web-admin, plugin host, …) │
└──────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────┐
│ cron.runner (TCron class, schema specs) │
├──────────────────────────────────────────────────┤
│ cron.events (typed observer callbacks) │
│ cron.types (record + callback types) │
│ cron.version (semver constants) │
├──────────────────────────────────────────────────┤
│ Sibling fpc-* libraries: │
│ fpc-log log.types.TLogProc │
│ fpc-db database.pool / database.dialect / database.schema │
├──────────────────────────────────────────────────┤
│ RTL: Classes (TThread, TBits, TCriticalSection),│
│ SysUtils, syncobjs, fpjson, DateUtils, │
│ sqldb (via fpc-db) │
└──────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────┐
│ Persistent state: SQL tables │
│ system_scheduler - row per task │
│ scheduler_log - append-only audit │
│ (names + columns byte-identical to canonical │
│ Fastway fw_schema.pas — drop-in compatible) │
└──────────────────────────────────────────────────┘
```
Four source units, no platform-specific code, no transports —
fpc-cron is pure scheduling logic + DB persistence.
## Origin
Verbatim port of `TFWScheduler` from Fastway-Server's
`fw_scheduler.pas` (1093 lines). Method bodies are copied
line-for-line from canonical, with these mechanical
translations only:
- Type renames (`TFWScheduler``TCron`, etc.) per family
convention.
- `uses` clause: drop `fw_*` and `dbapi_*` units; add fpc-log's
`log.types`, fpc-db's `database.dialect` / `database.schema` / `database.pool`,
and the local `cron.types` / `cron.events`.
- Global `DB``TDBPool` parameter on `Create`.
- `fw_log.Log.X(...)``DoLog(llX, ...)` → optional
`log.types.TLogProc` callback.
- `EventBus.Fire('_system', 'scheduler.task_*', ...)` → typed
observer callbacks (`OnTaskStart` / `OnTaskComplete` etc.) per
the fpc-* per-library typed-callback convention.
- `DoWalCheckpointTruncate` (SQLite + Fastway specific) lifted
out — consumers register their own checkpoint task via
`RegisterSystemTask`.
- Hardcoded `case ATaskName of` system-task table replaced with
consumer-registered `FSysTasks` lookup; dispatcher shape
preserved.
- `var Scheduler: TFWScheduler;` global removed; consumers
create their own instance.
- `ParseCronField` / `MatchesCron` promoted from private
instance methods to `class function` for testability. Bodies
byte-verbatim from canonical.
ParseCronField + MatchesCron + cron grammar are byte-identical
to canonical (verified by diff after type-rename
normalisation).
## Where it fits in the fpc-* family
```
┌──────────┐ log.types.TLogProc ┌─────────────┐
│ fpc-log │ ◄────────────────────────│ fpc-cron │
└──────────┘ │ │
│ TCron │
┌──────────┐ database.pool/dialect/ │ │
│ fpc-db │ ◄──── schema/types ──────│ │
└──────────┘ └─────────────┘
cron.events
typed observer
callbacks
consumer code
(BBS, web-admin,
daemon, ...)
```
fpc-cron does NOT depend on fpc-events. Events are emitted as
typed observer callbacks (per the fpc-binkp / fpc-comet /
fpc-emsi pattern), assignable as properties on `TCron`.
Consumers wanting ecosystem-wide pub/sub fan-out write a small
bridge class that forwards selected callbacks to a
fpc-events `TEventBus`. See `DEVELOPER_GUIDE.md` for the
bridging pattern.
## DB schema
Two tables, both auto-declared in `Create` via
`Pool.DeclareTable`. Names are kept verbatim from canonical
Fastway `fw_schema.pas`:
### `system_scheduler`
Holds every task definition. PK `id`, UNIQUE on
(`plugin_name`, `task_name`). Columns described in
[`API.md`](API.md) and the [Developer Guide](DEVELOPER_GUIDE.md).
### `scheduler_log`
Append-only audit log. One row per `ExecuteTask` invocation.
Columns: `task_name`, `plugin_name`, `started_at` (UTC),
`finished_at` (UTC), `duration_ms`, `result`, `error_message`,
`output`. Indexed on `started_at` and `(plugin_name,
task_name)`.
A Fastway-Server database can be reused as-is; the spec
functions `BuildSystemSchedulerSpec` / `BuildSchedulerLogSpec`
mirror canonical `fw_schema.pas`'s `BuildSystemScheduler` /
`BuildSchedulerLog` exactly.
## Threading model
`TCron.Execute` (the thread body) wakes once per second.
```
loop until Terminated:
NowTime := UTCNow
acquire FLock
try
for each task T:
if not T.Enabled: continue
if T.NextRun = 0: continue
if NowTime < T.NextRun: continue
T.IsRunning := True
FLock.Leave # release while task runs
try
ExecuteTask(T.Index) # may take seconds; locks reacquired internally
except
DoLog(llError, ...)
FLock.Enter # re-acquire to continue iteration
finally
release FLock
RTLEventWaitFor(FStopEvent, 1000) # 1 s sleep, wakes on stop signal
```
Lock-release-during-execute means a long-running task in slot N
doesn't block other tasks from being considered for firing on a
*later* wake — but within a single wake, iteration is sequential.
`UpdateTask` / `RefreshTasks` / `RegisterSystemTask` /
`GetTasksJSON` / `GetTaskJSON` are safe from any thread. All
acquire `FLock` for their mutations or reads.
`OnTaskStart` and `OnTaskComplete` are invoked synchronously on
the runner thread. Heavy work in those handlers delays
subsequent dispatches.
## Time handling
```
Server clock (TZ-dependent)
┌────────────────────────────────────────┐
│ Now → used for cron expression │
│ interpretation (LOCAL) │
│ │
│ LocalTimeToUniversal(Now) │
│ (= UTCNow) │
│ → every persisted timestamp │
│ (last_run / next_run / started_at) │
└────────────────────────────────────────┘
```
This is canonical Fastway behaviour, kept verbatim. Cron
expressions are interpreted in LOCAL time so users typing
`"0 3 * * *"` get 3 AM in the server's TZ; the matching minute
is converted to UTC for storage in `next_run` so the web-admin
UI parses every DB timestamp as UTC and converts to browser-
local on display.
Schedule miss on long-running tasks: if a 5-minute interval
task takes 6 minutes, the next firing is 5 minutes after it
*returned*, not after it *started*. Canonical, kept.
## Logger plumbing
`TCron`'s logger is `log.types.TLogProc` — the same shape every
other fpc-* library (fpc-binkp, fpc-comet, fpc-emsi, fpc-events)
uses. The runner passes `Category='cron'` for every message;
levels are `llDebug` / `llInfo` / `llWarn` / `llError`.
`nil` logger means no log output (matches canonical Fastway
behaviour for callers without an `fw_log.Log` configured).
## Known issues inherited verbatim
- `MatchesCron` allocates the five `TBits` field-bit-arrays
before its `try` / `finally`. If `ParseCronField` raises on
field 2..5, earlier `TBits` instances leak.
- `MatchesCron` calls `DecodeDateFully(ATime, Mn, Hr, Dom, Dow)`
whose results are immediately overwritten by the next four
`MonthOf`/`DayOf`/`HourOf`/`MinuteOf` calls. Cosmetic; the
call has no effect.
Both are inherited verbatim from canonical
`fw_scheduler.pas` per `feedback_copy_dont_reinterpret.md`.
Real-world impact is negligible; flagged for future cleanup
sessions.