Add TMessageBase.Sync for crash-safe per-message acknowledgement

NR caught a real durability gap during migration prep: the
sequence

  base.WriteMessage(msg);
  source.MarkSent(srcMsg);

looks atomic, but the OS write buffer hasn't necessarily reached
the platter when MarkSent runs.  Crash in that window = silent
message drop.

Add `Sync` virtual on TMessageBase (default no-op for read-only
and in-memory backends).  Six writable backends override and
flush every open stream they own via SysUtils.FileFlush
(fpfsync on Unix, FlushFileBuffers on Windows):

  JAM       .JHR / .JDT / .JDX / .JLR
  Squish    .SQD / .SQI / .SQL
  Hudson    msginfo / msgidx / msghdr / msgtxt / msgtoidx / LASTREAD.BBS
  PCBoard   MSGS file + index
  EzyCom    header + text
  GoldBase  msginfo / msgidx / msghdr / msgtxt / msgtoidx / LASTREAD.DAT

MSG inherits no-op (per-write open/close — buffer flushes on
close, but dir entry isn't fsynced; future enhancement).
Wildcat inherits no-op (legacy `file` IO, not TFileStream).

Helper for backend authors: TMessageBase.FlushStream(S: TStream)
class method that handles the TFileStream cast + nil-safety.

`Sync` raises after Close (data is finalized; nothing to flush).

Test: TestSyncWriteable in test_consumer_round1 -- writes a
message via JAM and Squish, calls Sync (no raise), Close, calls
Sync again (raise expected).

docs/API.md: new "Sync (durability)" section explaining the
commit-after-fsync pattern with the canonical example.

Symmetric to fpc-ftn-transport TPktWriter.Sync (commit ee8c6ad)
that NR's review prompted on the transport side.

Suite: 48/48 (added TestSyncWriteable to test_consumer_round1).
This commit is contained in:
2026-04-18 19:22:07 -07:00
parent 225a3f9090
commit 84e2efdd7e
15 changed files with 222 additions and 0 deletions

View File

@@ -214,6 +214,35 @@ Common keys — see [`docs/attributes-registry.md`](attributes-registry.md):
| `intl` / `fmpt` / `topt` | string | FSC-4008 cross-zone routing |
| `kludge.<name>` | string | unknown FTSC kludge passthrough |
### Sync (durability)
`TMessageBase.Sync` forces every open writer stream to durable
storage (`fpfsync` on Unix, `FlushFileBuffers` on Windows).
Override default no-op for read-only / in-memory backends; six of
the nine backends (JAM, Squish, Hudson, PCBoard, EzyCom, GoldBase)
flush every open stream they own. MSG inherits no-op (each
WriteMessage opens / closes its own .msg file — the OS write
buffer is flushed but not fsynced; Sync would have to fsync the
directory entry which is a future enhancement). Wildcat inherits
no-op (legacy `file` IO, not TFileStream).
Tossers needing crash-safe per-message acknowledgement use the
commit-after-fsync pattern:
```pascal
base.WriteMessage(msg);
base.Sync; { msg on platter }
source.MarkSent(srcMsg); { commit the source pointer }
```
Without Sync, a crash after MarkSent but before the OS flushes
the write buffer = silent message drop. Same durability problem
fpc-ftn-transport's `TPktWriter.Sync` solves on the transport
side; the symmetric surface is here for tossers writing to
message bases.
`Sync` raises after `Close` (data is finalized; nothing to flush).
### Capabilities API
Each backend declares the canonical list of attribute keys it