You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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:
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>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:
feat: S11.04 add SolidSyslogNullSd — new public type + tests + audience-table row.
feat: S11.04 add SolidSyslogNullSender — new public type + tests + audience-table row.
refactor: S11.04 migrate PassthroughBuffer onto PoolAllocator — smallest class first, sets the per-class shape.
For each of the six migrating classes, the per-class commit lands:
SolidSyslog<Class>.c — extracted vtable methods + _Initialise(base, ...) + _Cleanup(base) helpers. The cast from base to concrete uses the existing SelfFromBase pattern.
SolidSyslog<Class>Private.h — TU-internal header (lives in Core/Source/, never Core/Interface/). Concrete struct definition + _Initialise / _Cleanup declarations.
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) */voidSolidSyslogUdpSender_Destroy(void);
voidSolidSyslogPassthroughBuffer_Destroy(void);
voidSolidSyslogMetaSd_Destroy(void);
/* ... and the others *//* After */voidSolidSyslogUdpSender_Destroy(structSolidSyslogSender*base);
voidSolidSyslogPassthroughBuffer_Destroy(structSolidSyslogBuffer*base);
voidSolidSyslogMetaSd_Destroy(structSolidSyslogStructuredData*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/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
New SolidSyslogNullSd and SolidSyslogNullSender ship as public types with audience-table entries, full coverage, no new MISRA findings.
All six listed classes migrated onto SolidSyslogPoolAllocator following the canonical 3-TU pattern.
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.
Per-class Pool TEST_GROUP exercises the lock-count and fallback contract (9 tests minimum, mirroring the SolidSyslogCircularBufferPool reference).
All previously existing tests for each migrated class continue to pass.
All gates green: debug, clang-debug, sanitize, coverage, tidy, cppcheck, clang-format.
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.
Coverage at 100% line for every new and migrated file.
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.
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.
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 NullBuffer→PassthroughBuffer 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).
Parent epic: #29
Context
S11.02 (#396) landed
SolidSyslogPoolAllocatorand proved the canonical sweep pattern onCircularBuffer. S11.03 (#398) cleared theNullBuffer/PassthroughBuffernaming 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:PassthroughBufferBufferUdpSenderSenderSwitchingSenderSenderMetaSdStructuredDataTimeQualitySdStructuredDataOriginSdStructuredDataThe three storage-cast classes (
BlockStore,FileBlockDevice,StreamSender) come in S11.05 because they additionally remove publicStoragetypes — wider blast radius into BDD targets and examples.Goal
Apply the proven 3-TU split +
SolidSyslogPoolAllocatorpattern to the six classes above. Public_Createnames unchanged._Destroygains a handle parameter — see "Public API changes" below. Per-class delta target (from S11.02 epic):*Static.cends at ~3 file-scope functions (_Create,_Destroy,IndexFromHandle) plus the tinyCleanupAtIndexbridge. No new MISRA findings. Existing tests carry through as the regression net; new PoolTEST_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:
SolidSyslogNullSd— fallback forMetaSd,TimeQualitySd,OriginSd. Single no-opFormat. Same shape asSolidSyslogNullStore. Public — integrators can also wire it directly into theirConfig.Sd[].SolidSyslogNullSender— fallback forUdpSender,SwitchingSender. Send returnsfalse(delivery semantics: "didn't send"), Disconnect is no-op. Public. S11.05'sStreamSendermigration reuses this.The class-private fallback (used by
CircularBufferin S11.02 because noNullBufferGoF sibling exists post-S11.03) is still the right choice forPassthroughBuffer— it's the only class sharing theBuffervtable inside this sweep, so sharing buys nothing.Fallback assignment by class:
PassthroughBufferUdpSenderSolidSyslogNullSender(new GoF)SwitchingSenderSolidSyslogNullSender(new GoF)MetaSdSolidSyslogNullSd(new GoF)TimeQualitySdSolidSyslogNullSd(new GoF)OriginSdSolidSyslogNullSd(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_FILEwhen 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/_Cleanupdeclarations.SolidSyslog<Class>Static.c— pool, public_Create/_Destroy,IndexFromHandle,CleanupAtIndexbridge, 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:feat: S11.04 add SolidSyslogNullSd— new public type + tests + audience-table row.feat: S11.04 add SolidSyslogNullSender— new public type + tests + audience-table row.refactor: S11.04 migrate PassthroughBuffer onto PoolAllocator— smallest class first, sets the per-class shape.refactor: S11.04 migrate UdpSender onto PoolAllocator.refactor: S11.04 migrate SwitchingSender onto PoolAllocator.refactor: S11.04 migrate MetaSd onto PoolAllocator.refactor: S11.04 migrate TimeQualitySd onto PoolAllocator.refactor: S11.04 migrate OriginSd onto PoolAllocator.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:
SolidSyslog<Class>.c— extracted vtable methods +_Initialise(base, ...)+_Cleanup(base)helpers. The cast from base to concrete uses the existingSelfFromBasepattern.SolidSyslog<Class>Private.h— TU-internal header (lives inCore/Source/, neverCore/Interface/). Concrete struct definition +_Initialise/_Cleanupdeclarations.SolidSyslog<Class>Static.c— pool (bool InUse[N],struct <Class> Pool[N],struct SolidSyslogPoolAllocator Allocator = {InUse, N}), Fallback assignment,_Create/_Destroy/IndexFromHandle/CleanupAtIndex.SOLIDSYSLOG_<CLASS>_POOL_SIZE(default 1U) added toCore/Interface/SolidSyslogTunablesDefaults.hwith#ifndefoverride +#errorfloor.SOLIDSYSLOG_ERROR_MSG_<CLASS>_POOL_EXHAUSTED+SOLIDSYSLOG_ERROR_MSG_<CLASS>_UNKNOWN_DESTROYadded toCore/Source/SolidSyslogErrorMessages.h.Core/Source/CMakeLists.txt— add the three new TU entries per class.setup()/teardown()to use the new_Destroy(handle)shape.TEST_GROUPin the existing test file — mirrorSolidSyslogCircularBufferPool(S11.02's test suite is the reference):FillingPoolThenOverflowReturnsDistinctFallbackExhaustedCreateReportsErrorFallbackVtableEntriesAreNoOpsCreateAcquiresAndReleasesConfigLockOnFirstFreeSlotCreateLocksOncePerSlotProbedWhenPoolIsFullDestroyOfPooledHandleLocksOnceDestroyOfUnknownHandleDoesNotLockDestroyOfUnknownHandleReportsWarningDestroyOfStaleHandleReportsWarningCLAUDE.mdaudience-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_Destroymust take the handle returned by_Createto know which slot to release. Public API change:Caller updates land in the per-class migration commit:
Tests/SolidSyslog*Test.cpp— every fixture'steardown().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 matchesSolidSyslogCircularBuffer_Destroy(base)post-S11.02. Consistent across every E11-migrated class.Acceptance criteria
SolidSyslogNullSdandSolidSyslogNullSendership as public types with audience-table entries, full coverage, no new MISRA findings.SolidSyslogPoolAllocatorfollowing the canonical 3-TU pattern.*Static.clands at ≤ ~3 file-scope functions +CleanupAtIndex(per S11.02's per-class delta target). If a migrated*Static.cadds back any of the helpersSolidSyslogPoolAllocatorwas meant to replace, stop and reshape the helper — that's the E11 invariant.TEST_GROUPexercises the lock-count and fallback contract (9 tests minimum, mirroring theSolidSyslogCircularBufferPoolreference).feedback_cppcheck_misra_invariant.md). Confirm by stashing, running cppcheck-misra on main, comparing.SOLIDSYSLOG_<CLASS>_POOL_SIZE=3to confirm the helper integrates correctly under non-default sizing.cpputest-freertoscontainer too perfeedback_verify_in_freertos_host_image.md— the defaultdebugpreset'sLayerGuardsilently skipsTests/FreeRtos/Platform/FreeRtoswhenFREERTOS_KERNEL_PATHis unset.Out of scope (this story will not touch)
BlockStore,FileBlockDevice,StreamSender— storage-cast classes; S11.05.SolidSyslogcore retrofit — singleton stays singleton in this sweep; the canonical-file-layout retrofit is S11.10.AtomicCounterfamily /TLS/FatFs— S11.09.Tests/) — that's E10's territory.Memory pointers for the next session
Read these before touching anything:
CLAUDE.md(project root) +SKILL.mdMEMORY.md(project memory index) — particularly:project_e11_static_pool_design.md— pool design agreementsproject_e11_three_tu_split.md— three-TU split shapeproject_pool_allocator_helper.md— helper API + per-class delta targetproject_configlock_injection.md—SolidSyslog_SetConfigLockshapefeedback_storage_pattern.md— storage convention (now obsoleted per class as each migrates)feedback_tdd_and_review_style.md— pacingfeedback_drive_arg_values_in_same_test.md— TDD assertion bundlingfeedback_no_premature_generalisation.md— strict TDD minimum-to-passfeedback_cppcheck_misra_invariant.md— MISRA AC phrasingfeedback_devlog_in_pr.md— DEVLOG goes on the work branch before the PRfeedback_pr_template.md— PR body shapefeedback_no_git_commit_dash_s.md— never-Sfeedback_no_merge.md— never merge; David merges PRs himselffeedback_verify_in_freertos_host_image.md— verify in the freertos-host container tooOpen questions for the next session to surface
_Destroy(void)→_Destroy(handle)warrants a separate atomic rename commit per class, like theNullBuffer→PassthroughBuffersplit. 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).SolidSyslogNullSdandSolidSyslogNullSenderbelong 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).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 opaqueSender*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 UdpSenderintegration test).