Skip to content

S11.05: Sweep — Core storage-cast classes onto PoolAllocator #401

Description

@DavidCozens

Parent epic: #29

Context

S11.04 (#400) completed the Core sweep for the six file-scope-singleton classes
that had no public storage type. This story finishes the Core sweep by migrating
the three remaining storage-cast classes plus the two TU-internal
sub-components
that BlockStore composes:

Public, storage-cast today — pool migration + public Storage-type deletion:

Class Lines Vtable abstract base State at Create Notes
BlockStore 306 Store BlockDevice, full-callbacks, threshold cb, security policy Composes RecordStore + BlockSequence (today embedded in storage blob)
FileBlockDevice 407 BlockDevice Two File*s, path prefix, cached open block index Cached file-handle state
StreamSender 237 Sender Resolver, Stream, Endpoint, EndpointVersion Reuses SolidSyslogNullSender shipped in S11.04

TU-internal sub-components of BlockStore — pool migration only (already
private in Core/Source/; no public Storage type to delete):

Class Lines Init signature today State
RecordStore small _Init(self, securityPolicy) Security policy ref + per-record bookkeeping
BlockSequence medium _Init(self, config) BlockDevice ref, read/write block indices, capacity bookkeeping

CircularBuffer (also storage-cast) was migrated in S11.02 (#396) as the pilot
and is the reference for the public-class shape.

Goal

Apply the proven 3-TU split + SolidSyslogPoolAllocator pattern to all five
classes above. After this story:

  • Public Storage types and STORAGE_SIZE enums for the three public classes —
    gone. Every caller drops its <*>Storage local/static declaration.
  • _Create loses the storage parameter on the three public classes.
  • _Destroy(handle) shape everywhere (S11.04 pattern).
  • RecordStore and BlockSequence get the canonical 3-TU split too —
    _Init(self, ...)_Create(...) + _Destroy(handle), vending pool slots.
    Their headers and TUs remain in Core/Source/ (TU-internal). BlockStore holds
    pointers to them, not embedded structs.
  • Pool sizes for RecordStore and BlockSequence are hardcoded to
    SOLIDSYSLOG_BLOCK_STORE_POOL_SIZE
    — they are 1:1 with BlockStore by
    construction and don't merit their own integrator-facing tunable.

Per-class delta target unchanged from S11.02: *Static.c ends at ~3 file-scope
functions (_Create, _Destroy, IndexFromHandle) + the CleanupAtIndex
bridge + per-class config validation where one exists. No new MISRA findings.

Decisions locked in pre-raise

1. Fallback objects reuse S11.04's shared GoF nulls where they fit.

Class Fallback
BlockStore SolidSyslogNullStore (already public, _Get-only after S11.04)
FileBlockDevice Class-private — BlockDevice has no shared GoF null and FileBlockDevice is the only BlockDevice implementor today
StreamSender SolidSyslogNullSender (shipped in S11.04)
RecordStore Class-private — TU-internal, no GoF null possible
BlockSequence Class-private — TU-internal, no GoF null possible

The TU-internal class-private fallbacks for RecordStore and BlockSequence can
be minimal: their _Create returning the fallback is observable only by
BlockStoreStatic.c, which already handles pool-exhausted Create-time failure
by returning SolidSyslogNullStore_Get() from its own outer Create.

2. Pool size defaults.

Class Tunable Default
BlockStore SOLIDSYSLOG_BLOCK_STORE_POOL_SIZE 1U
FileBlockDevice SOLIDSYSLOG_FILE_BLOCK_DEVICE_POOL_SIZE 1U
StreamSender SOLIDSYSLOG_STREAM_SENDER_POOL_SIZE 1U
RecordStore none — sized to SOLIDSYSLOG_BLOCK_STORE_POOL_SIZE directly tracks BlockStore
BlockSequence none — sized to SOLIDSYSLOG_BLOCK_STORE_POOL_SIZE directly tracks BlockStore

By construction, every successful BlockStore Create consumes one RecordStore
slot and one BlockSequence slot from their respective pools. Bumping
SOLIDSYSLOG_BLOCK_STORE_POOL_SIZE automatically grows all three together.
RecordStore and BlockSequence emit a class-private POOL_EXHAUSTED message if
ever called more times than expected — they shouldn't be, but the guard
catches a future refactor that breaks the 1:1 invariant.

3. Three-TU split per class — unchanged from project_e11_three_tu_split:

  • SolidSyslog<Class>.c (or <Class>.c for TU-internal) — vtable + private
    _Initialise/_Cleanup.
  • SolidSyslog<Class>Private.h (or <Class>Private.h) — struct definition +
    _Initialise/_Cleanup decls. TU-internal; tests must not include.
  • SolidSyslog<Class>Static.c (or <Class>Static.c) — pool,
    _Create/_Destroy, IndexFromHandle, CleanupAtIndex bridge, fallback
    assignment.

For the two TU-internal sub-components, the <Class>Static.c exports
<Class>_Create and <Class>_Destroy with external linkage so
BlockStoreStatic.c can link to them. There is no SolidSyslog<Class>-style
public header.

4. BlockStore's slot struct holds pointers, not embedded structs, after
this story:

/* Before */
struct SolidSyslogBlockStore
{
    struct SolidSyslogStore Base;
    struct RecordStore      RecordStore;
    struct BlockSequence    BlockSequence;
    /* ... */
};

/* After */
struct SolidSyslogBlockStore
{
    struct SolidSyslogStore Base;
    struct RecordStore*     RecordStore;
    struct BlockSequence*   BlockSequence;
    /* ... */
};

BlockStore_Create calls RecordStore_Create and BlockSequence_Create to obtain
the handles, in that order. BlockStore_Cleanup calls their _Destroys in
reverse order. The 1:1 invariant means under normal operation neither inner
Create ever returns its fallback; if either does, BlockStore_Create routes to
SolidSyslogNullStore_Get() and emits its own POOL_EXHAUSTED error.

5. No new MISRA suppressions. E11 invariant. Storage-cast MISRA deviations
get removed by this story (the whole point) — track which deviations
evaporate.

6. File-scope statics follow the NAMING.md sweep landed in S11.04
<Class>_InUse, <Class>_Pool, <Class>_Allocator from the start. No bare
InUse/Pool/Allocator in any new *Static.c.

Sequencing on the work branch

Recommended commit shape on refactor/s11-05-storage-cast-sweep:

  1. refactor: S11.05 migrate StreamSender onto PoolAllocator — smallest of
    the three public classes, exercises the shared NullSender fallback path
    established by S11.04.
  2. refactor: S11.05 migrate FileBlockDevice onto PoolAllocator
    class-private fallback.
  3. refactor: S11.05 migrate RecordStore onto PoolAllocator — TU-internal,
    prep step for BlockStore. The 3-TU split applies to a non-public type for
    the first time.
  4. refactor: S11.05 migrate BlockSequence onto PoolAllocator — TU-internal,
    prep step for BlockStore.
  5. refactor: S11.05 migrate BlockStore onto PoolAllocator — consumes 3+4's
    results; switches the BlockStore slot from embedded structs to pointers
    into RecordStore and BlockSequence pools.
  6. docs: update DEVLOG for S11.05.

Squash-merge title: feat: S11.05 Core storage-cast classes onto PoolAllocator.

Per-class scope

For each migrating class, the per-class commit lands:

Public, storage-cast classes (StreamSender, FileBlockDevice, BlockStore):

  1. SolidSyslog<Class>.c — extracted vtable methods + _Initialise(base, ...)
    • _Cleanup(base). Cast uses existing SelfFromBase.
  2. SolidSyslog<Class>Private.h — TU-internal. Struct definition +
    _Initialise/_Cleanup decls.
  3. SolidSyslog<Class>Static.c — pool (<Class>_InUse[N], <Class>_Pool[N],
    <Class>_Allocator), fallback, _Create/_Destroy/IndexFromHandle/
    CleanupAtIndex. NULL-config validation preserved where it existed
    pre-migration.
  4. Public header — delete SolidSyslog<Class>Storage typedef and
    SOLIDSYSLOG_<*>_STORAGE_SIZE enum. Drop the storage parameter from
    _Create. Adopt _Destroy(base) shape.
  5. Tunable SOLIDSYSLOG_<CLASS>_POOL_SIZE (default 1U) in
    Core/Interface/SolidSyslogTunablesDefaults.h with #ifndef override
    • #error floor.
  6. Error messages _POOL_EXHAUSTED (ERROR) + _UNKNOWN_DESTROY
    (WARNING) in Core/Source/SolidSyslogErrorMessages.h.
  7. Core/Source/CMakeLists.txt — add the three new TU entries per class
    (drop the old single-TU entry).
  8. Existing test file — drop <*>Storage locals; update _Destroy(void)
    _Destroy(handle) call sites.
  9. New Pool TEST_GROUPFillingPoolThenOverflowReturnsDistinctFallback
    at minimum.
  10. CLAUDE.md audience-table row updated — drop Storage/SIZE mentions,
    document the new _Destroy(base) shape.

TU-internal sub-components (RecordStore, BlockSequence):

  1. <Class>.c — vtable-less; existing logic, _Init_Initialise
    rename, gain a _Cleanup.
  2. <Class>Private.h — rename of current <Class>.h. Struct + _Initialise/
    _Cleanup decls. Still TU-internal.
  3. <Class>Static.c — pool sized directly to
    SOLIDSYSLOG_BLOCK_STORE_POOL_SIZE. External-linkage _Create/_Destroy
    so BlockStoreStatic.c can link. Class-private fallback (TU-internal nature
    means there's no shared GoF null candidate).
  4. Tests — <Class>Test.cpp updated to use the new _Create/_Destroy
    shape. Existing tests carry forward as regression net. No new pool tests —
    the 1:1 invariant with BlockStore is the contract; if either pool exhausts
    before BlockStore's does, BlockStore's outer test for pool-exhaustion
    would have failed first.
  5. No public-header / tunable / audience-table changes — these classes
    stay TU-internal.

Public API changes

/* Before */
struct SolidSyslogSender* SolidSyslogStreamSender_Create(
    SolidSyslogStreamSenderStorage* storage,
    const struct SolidSyslogStreamSenderConfig* config
);
void SolidSyslogStreamSender_Destroy(struct SolidSyslogSender* sender);

struct SolidSyslogStore* SolidSyslogBlockStore_Create(
    SolidSyslogBlockStoreStorage* storage,
    const struct SolidSyslogBlockStoreConfig* config
);
void SolidSyslogBlockStore_Destroy(struct SolidSyslogStore* store);

struct SolidSyslogBlockDevice* SolidSyslogFileBlockDevice_Create(
    SolidSyslogFileBlockDeviceStorage* storage,
    struct SolidSyslogFile* readFile,
    struct SolidSyslogFile* writeFile,
    const char* pathPrefix
);
void SolidSyslogFileBlockDevice_Destroy(struct SolidSyslogBlockDevice* device);

/* After */
struct SolidSyslogSender* SolidSyslogStreamSender_Create(
    const struct SolidSyslogStreamSenderConfig* config
);
void SolidSyslogStreamSender_Destroy(struct SolidSyslogSender* base);

struct SolidSyslogStore* SolidSyslogBlockStore_Create(
    const struct SolidSyslogBlockStoreConfig* config
);
void SolidSyslogBlockStore_Destroy(struct SolidSyslogStore* base);

struct SolidSyslogBlockDevice* SolidSyslogFileBlockDevice_Create(
    struct SolidSyslogFile* readFile,
    struct SolidSyslogFile* writeFile,
    const char* pathPrefix
);
void SolidSyslogFileBlockDevice_Destroy(struct SolidSyslogBlockDevice* base);

RecordStore and BlockSequence internal API:

/* Before */
void RecordStore_Init(struct RecordStore* recordStore, struct SolidSyslogSecurityPolicy* securityPolicy);
void BlockSequence_Init(struct BlockSequence* blockSequence, const struct BlockSequenceConfig* config);

/* After */
struct RecordStore*   RecordStore_Create(struct SolidSyslogSecurityPolicy* securityPolicy);
void                  RecordStore_Destroy(struct RecordStore* self);
struct BlockSequence* BlockSequence_Create(const struct BlockSequenceConfig* config);
void                  BlockSequence_Destroy(struct BlockSequence* self);

Caller updates land in the per-class migration commit:

  • Tests/SolidSyslog<Class>Test.cpp, BlockStoreDrainOrderingTest.cpp,
    BlockStorePosixTest.cpp, BlockSequenceTest.cpp — drop storage locals,
    update Init→Create signatures.
  • Bdd/Targets/Linux/main.c, Bdd/Targets/Windows/BddTargetWindows.c,
    Bdd/Targets/FreeRtos/main.c — drop static <*>Storage lines; update
    _Create call sites.
  • Bdd/Targets/Common/BddTargetTlsSender_OpenSsl_PosixTcp.c — drop the
    static SolidSyslogStreamSenderStorage senderStorage; line.

Acceptance criteria

  1. All five classes follow the canonical 3-TU pattern. Per-class
    *Static.c at ≤ ~3 file-scope functions + CleanupAtIndex (+ NULL-config
    validation where it existed pre-migration).
  2. Public SolidSyslog<*>Storage types and SOLIDSYSLOG_<*>_STORAGE_SIZE
    enums gone from Core/Interface/. Search proves zero remaining
    references project-wide.
  3. RecordStore and BlockSequence are TU-internal pool-allocated classes
    sized to SOLIDSYSLOG_BLOCK_STORE_POOL_SIZE. BlockStore's slot holds
    pointers to them, not embedded structs.
  4. Per-class Pool TEST_GROUP — at minimum
    FillingPoolThenOverflowReturnsDistinctFallback for the three public
    classes. The TU-internal pair piggybacks on BlockStore's pool test for
    exhaustion coverage (since their pools are sized 1:1).
  5. All previously existing tests continue to pass.
  6. All gates green: debug, clang-debug, sanitize, coverage, tidy, cppcheck,
    clang-format, iwyu.
  7. cppcheck-misra produces no new findings vs running the same command on
    main.
    Track which storage-cast suppressions evaporate (record in
    DEVLOG) — they are the MISRA bonus the epic promised.
  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 via SOLIDSYSLOG_USER_TUNABLES_FILE
    override. For BlockStore, bumping its tunable to 3 must also grow
    RecordStore and BlockSequence pools to 3 automatically — confirms the
    shared sizing.
  10. Verify in cpputest-freertos container per
    feedback_verify_in_freertos_host_image.
  11. DEVLOG entry on the work branch BEFORE the PR per feedback_devlog_in_pr.
  12. File-scope statics use the class-prefix shape established in S11.04.

Out of scope (this story will not touch)

  • POSIX / Windows / FreeRTOS / FatFs / TLS / Atomics adapters
    S11.06–S11.09.
  • SolidSyslog core retrofit — S11.10.
  • MISRA storage-cast deviation removal beyond the three migrated classes
    the wholesale sweep is S11.11; this story removes only the deviations that
    apply to its three classes.
  • BlockStore algorithmic changes — the migration is mechanical, not
    architectural. The split into BlockStore + RecordStore + BlockSequence is
    preserved as-is; only their lifecycle and ownership change.
  • No widening cppcheck-misra to additional scopes — 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
    • project_e11_three_tu_split
    • project_pool_allocator_helper
    • project_configlock_injection
    • feedback_storage_pattern — storage convention; deletion is the point
    • feedback_bad_setup_contract — severity vocabulary for preserved
      NULL-checks
    • feedback_discuss_production_changes — production-code changes need
      explicit approval; behaviour-preserving refactor here, but any new
      validation requires discussion
    • feedback_tdd_and_review_style
    • feedback_cppcheck_misra_invariant
    • feedback_devlog_in_pr
    • feedback_pr_template
    • feedback_no_git_commit_dash_s
    • feedback_no_merge
    • feedback_verify_in_freertos_host_image
  • E11 epic body (E11: Static-Allocation Variant #29).
  • S11.04's DEVLOG entry — captures the bad-setup contract, file-scope static
    naming, and NullSender drop-on-the-floor semantic inherited here.

Open questions for the next session to surface

  • Whether to bundle all five migrations into one PR or split. Default:
    bundled
    — S11.04 set the precedent for sweep stories with multiple
    per-class commits in one PR. Storage-type deletion is the cross-cutting
    concern that benefits from one review surface.
  • MetaSd/OriginSd validation symmetry — TimeQualitySd grew NULL-callback
    validation in S11.04 from CodeRabbit's nudge. MetaSd already validates;
    OriginSd doesn't. Should a separate hardening story cover this, or roll it
    into E12? Default: out of scope here — S11.05 is the storage-cast
    sweep; validation symmetry belongs in a focused commit (or E12). Worth
    explicit acknowledgement so it's not forgotten.
  • Naming for the TU-internal sub-components' file-scope statics. They
    don't have a SolidSyslog prefix on their basename today
    (RecordStore.c, not SolidSyslogRecordStore.c) — consistent with the
    established convention for TU-internal classes. Their statics should
    follow RecordStore_InUse etc., matching the basename — confirm in
    session before naming the symbols.

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