9.9 KiB
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. usesclause: dropfw_*anddbapi_*units; add fpc-log'slog.types, fpc-db'sdatabase.dialect/database.schema/database.pool, and the localcron.types/cron.events.- Global
DB→TDBPoolparameter onCreate. fw_log.Log.X(...)→DoLog(llX, ...)→ optionallog.types.TLogProccallback.EventBus.Fire('_system', 'scheduler.task_*', ...)→ typed observer callbacks (OnTaskStart/OnTaskCompleteetc.) per the fpc-* per-library typed-callback convention.DoWalCheckpointTruncate(SQLite + Fastway specific) lifted out — consumers register their own checkpoint task viaRegisterSystemTask.- Hardcoded
case ATaskName ofsystem-task table replaced with consumer-registeredFSysTaskslookup; dispatcher shape preserved. var Scheduler: TFWScheduler;global removed; consumers create their own instance.ParseCronField/MatchesCronpromoted from private instance methods toclass functionfor 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 and the Developer Guide.
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
MatchesCronallocates the fiveTBitsfield-bit-arrays before itstry/finally. IfParseCronFieldraises on field 2..5, earlierTBitsinstances leak.MatchesCroncallsDecodeDateFully(ATime, Mn, Hr, Dom, Dow)whose results are immediately overwritten by the next fourMonthOf/DayOf/HourOf/MinuteOfcalls. 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.