219 lines
9.9 KiB
Markdown
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.
|