Skip to content

S11.04: Sweep — Core singleton stateful classes onto PoolAllocator #399

Description

@DavidCozens

Parent epic: #29

Context

S11.02 (#396) landed SolidSyslogPoolAllocator and proved the canonical sweep pattern on CircularBuffer. S11.03 (#398) cleared the NullBuffer/PassthroughBuffer naming so the sweep can proceed without a cross-tree rename mid-flight. This story applies the canonical pattern to the six Core stateful classes that have no storage cast today:

Class Lines Vtable abstract base State at Create
PassthroughBuffer 55 Buffer Sender pointer
UdpSender 286 Sender Resolver, Datagram, Endpoint, EndpointVersion
SwitchingSender 123 Sender Senders array, Selector
MetaSd 114 StructuredData Counter, GetSysUpTime, GetLanguage
TimeQualitySd 89 StructuredData GetTimeQuality
OriginSd 161 StructuredData Software, swVersion, enterpriseId, GetIpCount, GetIpAt

The three storage-cast classes (BlockStore, FileBlockDevice, StreamSender) come in S11.05 because they additionally remove public Storage types — wider blast radius into BDD targets and examples.

Goal

Apply the proven 3-TU split + SolidSyslogPoolAllocator pattern to the six classes above. Public _Create names unchanged. _Destroy gains a handle parameter — see "Public API changes" below. Per-class delta target (from S11.02 epic): *Static.c ends at ~3 file-scope functions (_Create, _Destroy, IndexFromHandle) plus the tiny CleanupAtIndex bridge. No new MISRA findings. Existing tests carry through as the regression net; new Pool TEST_GROUPs pin the lock counts and the fallback dispatch.

Decisions locked in pre-raise

1. Shared GoF nulls for the abstract bases that have multiple sweep consumers:

  • New SolidSyslogNullSd — fallback for MetaSd, TimeQualitySd, OriginSd. Single no-op Format. Same shape as SolidSyslogNullStore. Public — integrators can also wire it directly into their Config.Sd[].
  • New SolidSyslogNullSender — fallback for UdpSender, SwitchingSender. Send returns false (delivery semantics: "didn't send"), Disconnect is no-op. Public. S11.05's StreamSender migration reuses this.

The class-private fallback (used by CircularBuffer in S11.02 because no NullBuffer GoF sibling exists post-S11.03) is still the right choice for PassthroughBuffer — it's the only class sharing the Buffer vtable inside this sweep, so sharing buys nothing.

Fallback assignment by class:

Class Fallback
PassthroughBuffer Class-private (Buffer no-op vtable)
UdpSender SolidSyslogNullSender (new GoF)
SwitchingSender SolidSyslogNullSender (new GoF)
MetaSd SolidSyslogNullSd (new GoF)
TimeQualitySd SolidSyslogNullSd (new GoF)
OriginSd SolidSyslogNullSd (new GoF)

2. Pool size default 1U for every class. Verified by surveying BDD targets and examples: every binary instantiates each of these classes at most once. Integrators bump per-class via SOLIDSYSLOG_USER_TUNABLES_FILE when a multi-instance need actually emerges. Singleton today → singleton-by-default after migration — no integrator-visible behaviour change.

3. Three-TU split per class — unchanged from the pilot, per project_e11_three_tu_split.md:

  • SolidSyslog<Class>.c — vtable methods + private _Initialise / _Cleanup.
  • SolidSyslog<Class>Private.h — concrete struct definition + _Initialise / _Cleanup declarations.
  • SolidSyslog<Class>Static.c — pool, public _Create / _Destroy, IndexFromHandle, CleanupAtIndex bridge, fallback assignment.

4. No new MISRA suppressions. E11 invariant per the epic. If a migrated class adds back any of AcquireFirstFree, MarkInUse, MarkFree, PoolItemIsFree, etc., stop and reshape the helper rather than duplicate.

Sequencing on the work branch

Recommended commit shape on refactor/s11-04-singleton-sweep:

  1. feat: S11.04 add SolidSyslogNullSd — new public type + tests + audience-table row.
  2. feat: S11.04 add SolidSyslogNullSender — new public type + tests + audience-table row.
  3. refactor: S11.04 migrate PassthroughBuffer onto PoolAllocator — smallest class first, sets the per-class shape.
  4. refactor: S11.04 migrate UdpSender onto PoolAllocator.
  5. refactor: S11.04 migrate SwitchingSender onto PoolAllocator.
  6. refactor: S11.04 migrate MetaSd onto PoolAllocator.
  7. refactor: S11.04 migrate TimeQualitySd onto PoolAllocator.
  8. refactor: S11.04 migrate OriginSd onto PoolAllocator.
  9. docs: update DEVLOG for S11.04.

Squash-merge title: feat: S11.04 Core singleton stateful classes onto PoolAllocator.

Per-class scope

For each of the six migrating classes, the per-class commit lands:

  1. SolidSyslog<Class>.c — extracted vtable methods + _Initialise(base, ...) + _Cleanup(base) helpers. The cast from base to concrete uses the existing SelfFromBase pattern.
  2. SolidSyslog<Class>Private.h — TU-internal header (lives in Core/Source/, never Core/Interface/). Concrete struct definition + _Initialise / _Cleanup declarations.
  3. SolidSyslog<Class>Static.c — pool (bool InUse[N], struct <Class> Pool[N], struct SolidSyslogPoolAllocator Allocator = {InUse, N}), Fallback assignment, _Create/_Destroy/IndexFromHandle/CleanupAtIndex.
  4. Tunable SOLIDSYSLOG_<CLASS>_POOL_SIZE (default 1U) added to Core/Interface/SolidSyslogTunablesDefaults.h with #ifndef override + #error floor.
  5. Error messages SOLIDSYSLOG_ERROR_MSG_<CLASS>_POOL_EXHAUSTED + SOLIDSYSLOG_ERROR_MSG_<CLASS>_UNKNOWN_DESTROY added to Core/Source/SolidSyslogErrorMessages.h.
  6. Core/Source/CMakeLists.txt — add the three new TU entries per class.
  7. Existing test file — preserved as regression net. Refactored setup()/teardown() to use the new _Destroy(handle) shape.
  8. New Pool TEST_GROUP in the existing test file — mirror SolidSyslogCircularBufferPool (S11.02's test suite is the reference):
    • FillingPoolThenOverflowReturnsDistinctFallback
    • ExhaustedCreateReportsError
    • FallbackVtableEntriesAreNoOps
    • CreateAcquiresAndReleasesConfigLockOnFirstFreeSlot
    • CreateLocksOncePerSlotProbedWhenPoolIsFull
    • DestroyOfPooledHandleLocksOnce
    • DestroyOfUnknownHandleDoesNotLock
    • DestroyOfUnknownHandleReportsWarning
    • DestroyOfStaleHandleReportsWarning
  9. CLAUDE.md audience-table row for the class — bullet updated for the new _Destroy(handle) shape.

Public API changes

The current singleton classes have _Destroy(void) (no parameter — the instance is implicit). With pool migration each _Destroy must take the handle returned by _Create to know which slot to release. Public API change:

/* Before (S11.04 pre-state) */
void SolidSyslogUdpSender_Destroy(void);
void SolidSyslogPassthroughBuffer_Destroy(void);
void SolidSyslogMetaSd_Destroy(void);
/* ... and the others */

/* After */
void SolidSyslogUdpSender_Destroy(struct SolidSyslogSender* base);
void SolidSyslogPassthroughBuffer_Destroy(struct SolidSyslogBuffer* base);
void SolidSyslogMetaSd_Destroy(struct SolidSyslogStructuredData* base);
/* ... and the others */

Caller updates land in the per-class migration commit:

  • Tests/SolidSyslog*Test.cpp — every fixture's teardown().
  • Tests/SolidSyslogTest.cpp — fixture teardown for the integration tests.
  • Bdd/Targets/Linux/main.c, Bdd/Targets/Windows/BddTargetWindows.c, Bdd/Targets/FreeRtos/main.c — destructor pairs.
  • Bdd/Targets/Common/BddTargetTlsSender_OpenSsl_*.c — n/a for S11.04 (StreamSender lands in S11.05).

The _Destroy(handle) shape matches SolidSyslogCircularBuffer_Destroy(base) post-S11.02. Consistent across every E11-migrated class.

Acceptance criteria

  1. New SolidSyslogNullSd and SolidSyslogNullSender ship as public types with audience-table entries, full coverage, no new MISRA findings.
  2. All six listed classes migrated onto SolidSyslogPoolAllocator following the canonical 3-TU pattern.
  3. Per-class *Static.c lands at ≤ ~3 file-scope functions + CleanupAtIndex (per S11.02's per-class delta target). If a migrated *Static.c adds back any of the helpers SolidSyslogPoolAllocator was meant to replace, stop and reshape the helper — that's the E11 invariant.
  4. Per-class Pool TEST_GROUP exercises the lock-count and fallback contract (9 tests minimum, mirroring the SolidSyslogCircularBufferPool reference).
  5. All previously existing tests for each migrated class continue to pass.
  6. All gates green: debug, clang-debug, sanitize, coverage, tidy, cppcheck, clang-format.
  7. cppcheck-misra produces no new findings vs running the same command on main. Do not bake in an absolute count — the count varies between runs (see feedback_cppcheck_misra_invariant.md). Confirm by stashing, running cppcheck-misra on main, comparing.
  8. Coverage at 100% line for every new and migrated file.
  9. Validation step: per migrated class, re-run the full suite at SOLIDSYSLOG_<CLASS>_POOL_SIZE=3 to confirm the helper integrates correctly under non-default sizing.
  10. Verify in cpputest-freertos container too per feedback_verify_in_freertos_host_image.md — the default debug preset's LayerGuard silently skips Tests/FreeRtos/Platform/FreeRtos when FREERTOS_KERNEL_PATH is unset.
  11. DEVLOG entry on the work branch BEFORE the PR.

Out of scope (this story will not touch)

  • BlockStore, FileBlockDevice, StreamSender — storage-cast classes; S11.05.
  • SolidSyslog core retrofit — singleton stays singleton in this sweep; the canonical-file-layout retrofit is S11.10.
  • POSIX / Windows / FreeRTOS adapters — S11.06 / S11.07 / S11.08.
  • AtomicCounter family / TLS / FatFs — S11.09.
  • MISRA storage-cast deviation removal — S11.11 (some deviations apply to S11.05's classes too; bulk removal once everything is migrated).
  • No widening cppcheck-misra to additional scopes (e.g. Tests/) — that's E10's territory.

Memory pointers for the next session

Read these before touching anything:

  • CLAUDE.md (project root) + SKILL.md
  • MEMORY.md (project memory index) — particularly:
    • project_e11_static_pool_design.md — pool design agreements
    • project_e11_three_tu_split.md — three-TU split shape
    • project_pool_allocator_helper.md — helper API + per-class delta target
    • project_configlock_injection.mdSolidSyslog_SetConfigLock shape
    • feedback_storage_pattern.md — storage convention (now obsoleted per class as each migrates)
    • feedback_tdd_and_review_style.md — pacing
    • feedback_drive_arg_values_in_same_test.md — TDD assertion bundling
    • feedback_no_premature_generalisation.md — strict TDD minimum-to-pass
    • feedback_cppcheck_misra_invariant.md — MISRA AC phrasing
    • feedback_devlog_in_pr.md — DEVLOG goes on the work branch before the PR
    • feedback_pr_template.md — PR body shape
    • feedback_no_git_commit_dash_s.md — never -S
    • feedback_no_merge.md — never merge; David merges PRs himself
    • feedback_verify_in_freertos_host_image.md — verify in the freertos-host container too
  • E11 epic body (E11: Static-Allocation Variant #29) — sequencing puts this between S11.03 (rename, done) and S11.05 (storage-cast sweep).
  • S11.02's DEVLOG entry — captures the per-class delta target.

Open questions for the next session to surface

  • Whether _Destroy(void)_Destroy(handle) warrants a separate atomic rename commit per class, like the NullBufferPassthroughBuffer split. Argument for: cleanly separates signature change from pool plumbing. Argument against: the signature change is forced by the pool plumbing — they're inseparable. Default: bundled per migration commit (lands the signature change + pool wiring + caller updates in one atomic commit per class).
  • Whether SolidSyslogNullSd and SolidSyslogNullSender belong in their own preceding story instead of the first two commits of S11.04. Default: bundled into S11.04 (they're prerequisites; landing them separately just adds bookkeeping).
  • Whether SwitchingSender's senders[] array members need anything special since they may now themselves be PoolAllocator-issued handles or Fallback handles. Default: nothing special — SwitchingSender stores them as opaque Sender* pointers; the pool dimension is per-class, and a pooled UdpSender stored inside a pooled SwitchingSender's array is fine. Worth a sanity test (SwitchingSender dispatching to a pooled UdpSender integration test).

Metadata

Metadata

Assignees

No one assigned

    Labels

    storyStory issue

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions