Files
fpc-cron/docs/architecture.md

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 (TFWSchedulerTCron, 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 DBTDBPool 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 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

  • 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.