Skip to content

feat: SolidSyslogBlockDevice abstraction with file-backed driver (S18.02)#243

Merged
DavidCozens merged 18 commits into
mainfrom
feat/blockdevice-abstraction
May 3, 2026
Merged

feat: SolidSyslogBlockDevice abstraction with file-backed driver (S18.02)#243
DavidCozens merged 18 commits into
mainfrom
feat/blockdevice-abstraction

Conversation

@DavidCozens

@DavidCozens DavidCozens commented May 3, 2026

Copy link
Copy Markdown
Owner

Summary

  • Introduce SolidSyslogBlockDevice — block-indexed I/O with Acquire/Dispose lifecycle (Acquire/Dispose/Exists/Read/Append/WriteAt/Size). New public headers: SolidSyslogBlockDevice.h, SolidSyslogBlockDeviceDefinition.h.
  • Implement SolidSyslogFileBlockDevice — file-backed driver wrapping two SolidSyslogFile* instances with sequence-numbered filenames <prefix><NN>.log. Storage-injected per CLAUDE.md callback-conventions.
  • Repoint internal BlockSequence and RecordStore onto BlockDevice. SolidSyslogFileStoreConfig swaps readFile/writeFile/pathPrefix for one blockDevice field — integrators now construct the device separately and pass it in.
  • File-handle ownership boundary moves outside FileStore: SolidSyslogFileStore_Destroy no longer closes files; the integrator destroys the block device explicitly. The behavioural cost is small and the contract is cleaner.
  • Lazy reopen in EnsureHandleOpenOnBlock heals transient open failures on next op (the old fast-fail-on-Open semantics are gone — this is strictly better behaviour).
  • New BDD baseline: "Block files are not disposed when capacity is not exhausted" (Bdd/features/block_lifecycle.feature). S18.03 will overturn this; pinned now per the issue spec.

Slicing notes

Per the original plan, S3 (internal repoint) and S4 (public flip) were intended as separate slices. Combined into one commit because the dual-handle test setup change forced by S3 is the same change S4 needs — separating them would have meant a transitional FileBlockDevice-built-inside-FileStore hack and a temporary storage bloat. See DEVLOG.

Test plan

  • gcc debug — 994 tests, 990 ran, 0 failures
  • clang debug — 994 tests, 990 ran, 0 failures
  • sanitize (ASan + UBSan) — 994 tests, 0 failures
  • coverage — 100% lines, 100% functions
  • tidy — clean
  • cppcheck — clean
  • clang-format — clean
  • BDD block_lifecycle.feature — passes (the existing capacity / tcp_reconnect features error pre-existing on main due to a syslog-ng config-file permission issue in this dev environment, unrelated to this PR)
  • Windows (MSVC), full BDD (Linux + Windows OTel), and OpenSSL integration are CI's responsibility per CLAUDE.md.

Closes #235.

Summary by CodeRabbit

  • New Features

    • Block-based storage system for store-and-forward capability with configurable block size, count limits, and discard policies (oldest/newest/halt)
    • Command-line options for block storage configuration (--max-blocks, --max-block-size)
  • Documentation

    • Updated architecture documentation and IEC 62443 compliance references to reflect block store implementation

Adds the SolidSyslogBlockDevice abstraction (consumer header, vtable header,
thin dispatch). Block-indexed I/O with Acquire/Dispose lifecycle, Exists for
startup scan, Read/Append/WriteAt for record I/O, Size for resume. No driver
yet; that lands in the next slice.

Part of S18.02 (#235).
…ence-numbered names

Adds SolidSyslogFileBlockDevice — the file-backed implementation of the
BlockDevice abstraction. Acquire creates an empty file at <prefix><NN>.log;
Dispose deletes the file; Read/Append/WriteAt/Size operate on whichever
block the caller addresses. Internally the driver caches one read handle
and one write handle, lazily reopening when the requested block changes.

Storage is integrator-allocated (SolidSyslogFileBlockDeviceStorage opaque
blob, mirrors SolidSyslogFormatterStorage).

No callers yet — BlockSequence still talks to SolidSyslogFile directly.
That repointing lands in the next slice.

Part of S18.02 (#235).
…e; FileStoreConfig takes blockDevice

Repoints the internal BlockSequence and RecordStore layers onto SolidSyslogBlockDevice
and flips the public SolidSyslogFileStoreConfig surface to match.

Public API changes:
- SolidSyslogFileStoreConfig swaps readFile / writeFile / pathPrefix for one
  blockDevice field. Integrators construct a SolidSyslogFileBlockDevice
  separately and pass it in. The file handle ownership boundary moves outside
  FileStore — Destroy no longer closes files; the integrator destroys the
  block device when finished.
- FileFake_FailNext{Open,Write,Read} now take the target file as an argument.
  The dual-handle test setup needs to specify which fake to fail.

Internal-only changes:
- BlockSequenceConfig and BlockSequence drop readFile/writeFile/pathPrefix and
  the FormatFilename helper (now in FileBlockDevice). They hold one
  blockDevice pointer instead.
- RecordStore_{Append,Read,MarkLastReadAsSent,FindFirstUnsent} signatures
  swap (file) for (blockDevice, blockIndex). RecordStore remembers the block
  index of the last-read record for MarkLastReadAsSent.
- BlockSequence_Open: scan via Exists; Acquire(0) on cold start; Size on
  resume. The implicit O_CREAT-via-Open behaviour is now an explicit Acquire.

Behaviour notes:
- Lazy open/close of the underlying file is now FileBlockDevice's
  responsibility. EnsureHandleOpenOnBlock consults SolidSyslogFile_IsOpen as
  the source of truth so it tolerates external close (test corruption helpers
  exploit this).
- The fast-fail-on-Open semantics that the old FileStoreErrors group asserted
  are gone — a transient open failure during Create heals on the next op.
  This is strictly better behaviour. Tests updated to assert the recovery.

S3 and S4 from the original plan are combined into one slice. The S3-only
shape would have required changing every FileStore TEST_GROUP setup anyway
(dual handles), so doing the public-surface flip at the same time avoided a
transitional FileBlockDevice-built-inside-FileStore hack and the storage
bloat that would have come with it.

Part of S18.02 (#235).
Adds a baseline regression scenario for S18.02: under a workload that does
not hit capacity, no block file is disposed. Pins today's dispose-on-
capacity-pressure-only semantics before S18.03 introduces dispose-on-empty.

Two new steps:
- 'Given the set of existing block files is recorded' — snapshots the
  current <prefix>NN.log set.
- 'Then no recorded block file has been disposed' — asserts every recorded
  file still exists.

Part of S18.02 (#235).
@coderabbitai

coderabbitai Bot commented May 3, 2026

Copy link
Copy Markdown
Contributor

Warning

Rate limit exceeded

@DavidCozens has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 15 minutes and 40 seconds before requesting another review.

To keep reviews running without waiting, you can enable usage-based add-on for your organization. This allows additional reviews beyond the hourly cap. Account admins can enable it under billing.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2c3d85b8-8082-4537-9728-515be36c3d16

📥 Commits

Reviewing files that changed from the base of the PR and between 15c545b and 8b13d2f.

📒 Files selected for processing (7)
  • Core/Source/BlockSequence.c
  • Core/Source/BlockSequence.h
  • Core/Source/SolidSyslogBlockStore.c
  • Tests/BlockSequenceTest.cpp
  • Tests/Example/ExampleCommandLineTest.cpp
  • Tests/SolidSyslogBlockStoreTest.cpp
  • Tests/SolidSyslogFileBlockDeviceTest.cpp
📝 Walkthrough

Walkthrough

The PR implements S18.02 by introducing a SolidSyslogBlockDevice abstraction layer with a file-backed driver, then migrating BlockSequence, RecordStore, and the store implementation from direct file I/O to block-device I/O. The SolidSyslogFileStore is renamed to SolidSyslogBlockStore and reconfigured to accept a block device. All BDD scenarios, examples, and tests are updated to use the new block-store wiring.

Changes

BlockDevice Abstraction & File-Backed Store Migration

Layer / File(s) Summary
Public Interface (BlockDevice)
Core/Interface/SolidSyslogBlockDevice.h, Core/Interface/SolidSyslogBlockDeviceDefinition.h
Introduces block-device consumer API (Acquire, Dispose, Exists, Read, Append, WriteAt, Size) and vtable struct for integrator-supplied drivers.
File-Backed BlockDevice Implementation
Core/Source/SolidSyslogBlockDevice.c, Core/Interface/SolidSyslogFileBlockDevice.h, Core/Source/SolidSyslogFileBlockDevice.c
Implements wrapper functions and file-backed block device with per-block filename storage, handle persistence, and seek-based I/O.
BlockSequence Migration
Core/Source/BlockSequence.h, Core/Source/BlockSequence.c
Refactors from file-handle–based rotation to block-device–indexed storage with circular-wrap logic; reworks rotation, read advancement, and capacity accounting to use block indices instead of filenames/handles.
RecordStore Migration
Core/Source/RecordStore.h, Core/Source/RecordStore.c
Migrates from file-based (position, offset) to block-based (blockIndex, blockIndex+offset) addressing; updates record read/write/sent-flag persistence to use block-device I/O.
Store Layer Rename & Wiring
Core/Interface/SolidSyslogBlockStore.h, Core/Source/SolidSyslogBlockStore.c
Renames SolidSyslogFileStore to SolidSyslogBlockStore and rewires config from (readFile, writeFile, pathPrefix, maxFileSize, maxFiles) to (blockDevice, maxBlockSize, maxBlocks). Implements store vtable using updated RecordStore/BlockSequence.
Old Store Implementation Removal
Core/Source/SolidSyslogFileStore.c
File-store persistence logic now routes through BlockSequence block-device APIs; file-handle management removed.
Build Integration
Core/Source/CMakeLists.txt
Adds SolidSyslogBlockDevice.c and SolidSyslogFileBlockDevice.c to compile list.
Example & CLI Updates
Example/Common/ExampleCommandLine.h, Example/Common/ExampleCommandLine.c, Example/Threaded/main.c
Command-line options change from --max-files/--max-file-size to --max-blocks/--max-block-size; example code creates and wires SolidSyslogFileBlockDevice before SolidSyslogBlockStore.
BDD Step Definitions
Bdd/features/steps/syslog_steps.py
Replaces file-store steps (step_file_store_enabled*) with block-store steps; adds block file snapshot/disposal-check helpers; updates command builder to pass --max-blocks/--max-block-size.
BDD Feature Updates
Bdd/features/block_lifecycle.feature, Bdd/features/capacity_threshold.feature, Bdd/features/power_cycle_replay.feature, Bdd/features/store_and_forward.feature, Bdd/features/store_capacity.feature
New block-lifecycle feature; existing scenarios updated from file-store to block-store configuration (max-files → max-blocks).
Unit Test Coverage
Tests/CMakeLists.txt, Tests/SolidSyslogBlockDeviceTest.cpp, Tests/SolidSyslogFileBlockDeviceTest.cpp, Tests/BlockSequenceTest.cpp, Tests/SolidSyslogBlockStoreTest.cpp, Tests/SolidSyslogBlockStorePosixTest.cpp
Replaces file-store tests with block-store tests; adds BlockDevice/FileBlockDevice unit coverage; updates test fixtures and assertions for block-indexed storage.
Test Support Updates
Tests/FileFake.h, Tests/FileFake.c, Tests/FileFakeTest.cpp, Tests/Example/ExampleCommandLineTest.cpp
Changes fail-injection to target specific file instances; updates command-line tests for block options.
Documentation
CLAUDE.md, README.md, docs/iec62443.md, DEVLOG.md
Adds BlockDevice/BlockStore header rows to public API table; updates feature descriptions and architecture references; documents decisions and deferred items; updates Windows file comment.
Git Config
.gitignore
Adds ignore pattern for pr*-comments.json.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes


Possibly related issues


Possibly related PRs


Poem

🐰 With blocks in place of files, the store takes flight,
Device abstractions grant the devices right,
To hide their nature—flash or magnetic spin—
While round-robin rotation lets the blocks rotate in!
Five-seven lines is asking much, but here's a humble tune,
For S18.02, complete—a BlockDevice buffoon! 🎉

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/blockdevice-abstraction

@github-actions

github-actions Bot commented May 3, 2026

Copy link
Copy Markdown
Contributor

☀️   Quality Summary

   🚦   build-linux-gcc: 100% successful (✔️ 999 passed, 🙈 3 skipped)
   🚦   build-linux-clang: 100% successful (✔️ 948 passed, 🙈 3 skipped)
   🚦   sanitize-linux-gcc: 100% successful (✔️ 948 passed, 🙈 3 skipped)
   🚦   integration-linux-openssl: 100% successful (✔️ 9 passed)
   🚦   bdd-linux-syslog-ng: 100% successful (✔️ 45 passed)
   🚦   bdd-windows-otel: 58% successful (✔️ 26 passed, 🙈 19 skipped)
   🚦   build-windows-msvc: 100% successful (✔️ 861 passed, 🙈 1 skipped)
   ⚠️   Clang-Tidy: No warnings
   ⚠️   CPPCheck: No warnings


Created by Quality Monitor v1.14.0 (#f3859fd). More details are shown in the GitHub Checks Result.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
Core/Interface/SolidSyslogFileStore.h (1)

33-45: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Document blockDevice ownership/lifetime in the public API contract.

Line 35 introduces an externally supplied dependency, but this header doesn’t state whether SolidSyslogFileStore_Destroy owns it. Please add explicit contract text that blockDevice must outlive the store and is not destroyed by SolidSyslogFileStore_Destroy.

Suggested doc update
 struct SolidSyslogFileStoreConfig
 {
+    /* Required. Caller-owned dependency; must remain valid for the entire
+     * lifetime of the FileStore. Not destroyed by SolidSyslogFileStore_Destroy. */
     struct SolidSyslogBlockDevice*    blockDevice;
@@
-struct SolidSyslogStore* SolidSyslogFileStore_Create(SolidSyslogFileStoreStorage * storage, const struct SolidSyslogFileStoreConfig* config);
-void                     SolidSyslogFileStore_Destroy(struct SolidSyslogStore * store);
+/* Create does not take ownership of config->blockDevice. */
+struct SolidSyslogStore* SolidSyslogFileStore_Create(SolidSyslogFileStoreStorage * storage, const struct SolidSyslogFileStoreConfig* config);
+/* Destroy releases store state only; caller destroys the block device separately. */
+void                     SolidSyslogFileStore_Destroy(struct SolidSyslogStore * store);

Based on learnings: "Tier 1 (Full support, highest review bar): Core/Interface/, Core/Source/ — stable API required".

Also applies to: 57-58

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Core/Interface/SolidSyslogFileStore.h` around lines 33 - 45, The public
header lacks ownership/lifetime contract for the externally supplied blockDevice
in SolidSyslogFileStoreConfig; update the API docs in
Core/Interface/SolidSyslogFileStore.h around the SolidSyslogFileStoreConfig
struct (and any other mentions at the later occurrences) to state explicitly
that the blockDevice pointer is owned by the caller, must outlive the
SolidSyslogFileStore instance, and will NOT be freed or destroyed by
SolidSyslogFileStore_Destroy; keep the wording concise and place it next to the
blockDevice field and in the top-level description of SolidSyslogFileStoreConfig
so callers clearly understand the lifetime guarantee.
Core/Source/BlockSequence.c (2)

166-195: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Only advance sequence state after block-device mutation succeeds.

Line 171 ignores the result of SolidSyslogBlockDevice_Acquire(), and Line 187 ignores the result of SolidSyslogBlockDevice_Dispose(). A transient create/delete failure will still advance writeSequence or oldestSequence, leaving the in-memory ring out of sync with the actual blocks on disk.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Core/Source/BlockSequence.c` around lines 166 - 195, RotateToNextFile and
DiscardOldestFile currently advance in-memory sequence state unconditionally;
change them to check the return value of SolidSyslogBlockDevice_Acquire and
SolidSyslogBlockDevice_Dispose and only update
writeSequence/writePosition/writeFileCorrupt or oldestSequence (and call
ResetReadToOldest) when the corresponding device call succeeds; if the device
call fails, leave sequences unchanged and propagate or handle the error (e.g.,
return false or an error status) so the in-memory ring stays consistent with
on-disk blocks. Ensure you reference RotateToNextFile, DiscardOldestFile,
SolidSyslogBlockDevice_Acquire, SolidSyslogBlockDevice_Dispose, writeSequence,
oldestSequence, NextSequence, and ResetReadToOldest when making the changes.

88-113: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Handle wrapped block ranges when resuming.

ScanForExistingBlocks() currently treats the lowest existing sequence as the oldest block and the highest as the write block. That breaks as soon as retained files straddle 99 -> 00: a persisted set like 98, 99, 00, 01 resumes as oldest 00 / write 99, so FileCount(), read order, and future discards all become wrong after restart.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Core/Source/BlockSequence.c` around lines 88 - 113, ScanForExistingBlocks
currently assumes non-wrapping sequences; fix it by building a presence map of
size MAX_SEQUENCE using SolidSyslogBlockDevice_Exists for each seq, return false
if none set, then find the longest contiguous run of true entries allowing
wrap-around (treat array as circular) — pick its first index as
blockSequence->oldestSequence, set blockSequence->writeSequence = (oldest +
run_length - 1) % MAX_SEQUENCE, and blockSequence->readSequence = oldest; ensure
you handle uint8_t casts consistently and keep MAX_SEQUENCE used for loop bounds
when computing run lengths.
Core/Source/RecordStore.c (1)

271-299: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Treat sent-flag read failures as corruption instead of “sent”.

IsRecordSent() seeds flag with SENT_FLAG_SENT and ignores the SolidSyslogBlockDevice_Read() result. If that read fails, AdvancePastSentRecord() skips the record as already sent, so resume can walk past an unsent/truncated record and lose data.

💡 Suggested fix
-static inline bool IsRecordSent(const struct RecordStore* recordStore, struct SolidSyslogBlockDevice* blockDevice, size_t blockIndex, size_t recordStart,
-                                uint16_t length)
+static inline bool TryReadSentFlag(const struct RecordStore* recordStore, struct SolidSyslogBlockDevice* blockDevice, size_t blockIndex,
+                                   size_t recordStart, uint16_t length, bool* isSent)
 {
-    uint8_t flag = SENT_FLAG_SENT;
-    SolidSyslogBlockDevice_Read(blockDevice, blockIndex, SentFlagFileOffset(recordStore, recordStart, length), &flag, SENT_FLAG_SIZE);
-    return flag == SENT_FLAG_SENT;
+    uint8_t flag = SENT_FLAG_UNSENT;
+    if (!SolidSyslogBlockDevice_Read(blockDevice, blockIndex, SentFlagFileOffset(recordStore, recordStart, length), &flag, SENT_FLAG_SIZE))
+    {
+        return false;
+    }
+
+    *isSent = flag == SENT_FLAG_SENT;
+    return true;
 }
bool isSent = false;
if (!TryReadSentFlag(recordStore, blockDevice, blockIndex, *cursor, length, &isSent))
{
    *cursor  = fileSize;
    *corrupt = true;
}
else if (isSent)
{
    SkipRecord(recordStore, cursor, length);
    advanced = true;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Core/Source/RecordStore.c` around lines 271 - 299, AdvancePastSentRecord
currently treats read errors as "sent" because IsRecordSent seeds flag with
SENT_FLAG_SENT and ignores SolidSyslogBlockDevice_Read's return; update the
logic so a failed sent-flag read is treated as corruption: change IsRecordSent
into a TryReadSentFlag (or make it return a success boolean and an out-parameter
isSent) that calls SolidSyslogBlockDevice_Read and returns false on failure,
then in AdvancePastSentRecord (after ReadAndValidateRecord succeeds) check the
read result—on read failure set *cursor = fileSize and *corrupt = true,
otherwise if isSent call SkipRecord and set advanced = true; reference symbols:
AdvancePastSentRecord, IsRecordSent (or new TryReadSentFlag),
SolidSyslogBlockDevice_Read, SentFlagFileOffset, SENT_FLAG_SENT, SENT_FLAG_SIZE,
SkipRecord, ReadAndValidateRecord.
🧹 Nitpick comments (1)
Tests/SolidSyslogFileBlockDeviceTest.cpp (1)

144-154: ⚡ Quick win

Add a boundary test for unsupported block indices.

You already validate two-digit naming for 7 and 42; please add one test for an out-of-range index (e.g., 100 or 256) to lock expected behavior and prevent filename-collision regressions.

🧪 Example test shape
+TEST(SolidSyslogFileBlockDevice, AcquireRejectsOutOfRangeBlockIndex)
+{
+    CHECK_FALSE(SolidSyslogBlockDevice_Acquire(device, 256));
+}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Tests/SolidSyslogFileBlockDeviceTest.cpp` around lines 144 - 154, Add a
boundary test that verifies out-of-range block indices are rejected: call
SolidSyslogBlockDevice_Acquire(device, 100) (or 256) and assert it returns a
failure status and that no file was created at "/tmp/blockdev_100.log" by using
SolidSyslogFile_Exists(writeFile, "/tmp/blockdev_100.log") == false; place this
alongside the existing tests (referencing SolidSyslogBlockDevice_Acquire,
SolidSyslogFile_Exists, and writeFile) to lock the expected unsupported-index
behavior and prevent filename-collision regressions.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Core/Source/SolidSyslogFileBlockDevice.c`:
- Around line 157-163: Validate blockIndex before narrowing to uint8_t in
FormatBlockFilename and in Dispose, Exists, Read, Append, WriteAt, and Size:
check that blockIndex is within the device's valid range (e.g., compare against
device->blockCount or the appropriate max-blocks field) and return an
error/false for operations (and return 0 for Size) if out of range, only casting
to (uint8_t) after this guard so large indices cannot wrap and collide on-disk
filenames; update all references to FormatBlockFilename usage accordingly so
callers never pass an unchecked index.

In `@Tests/FileFake.c`:
- Around line 138-151: Add null-handle guards to the fail-injection helpers so
they don't dereference a null pointer; in FileFake_FailNextOpen,
FileFake_FailNextWrite, and FileFake_FailNextRead check that the incoming struct
SolidSyslogFile* file is non-NULL before calling AsFake(file) and either
assert/return with a clear error (e.g., use a test assertion or early return)
when file is NULL to avoid segfaults and provide a useful failure message.

---

Outside diff comments:
In `@Core/Interface/SolidSyslogFileStore.h`:
- Around line 33-45: The public header lacks ownership/lifetime contract for the
externally supplied blockDevice in SolidSyslogFileStoreConfig; update the API
docs in Core/Interface/SolidSyslogFileStore.h around the
SolidSyslogFileStoreConfig struct (and any other mentions at the later
occurrences) to state explicitly that the blockDevice pointer is owned by the
caller, must outlive the SolidSyslogFileStore instance, and will NOT be freed or
destroyed by SolidSyslogFileStore_Destroy; keep the wording concise and place it
next to the blockDevice field and in the top-level description of
SolidSyslogFileStoreConfig so callers clearly understand the lifetime guarantee.

In `@Core/Source/BlockSequence.c`:
- Around line 166-195: RotateToNextFile and DiscardOldestFile currently advance
in-memory sequence state unconditionally; change them to check the return value
of SolidSyslogBlockDevice_Acquire and SolidSyslogBlockDevice_Dispose and only
update writeSequence/writePosition/writeFileCorrupt or oldestSequence (and call
ResetReadToOldest) when the corresponding device call succeeds; if the device
call fails, leave sequences unchanged and propagate or handle the error (e.g.,
return false or an error status) so the in-memory ring stays consistent with
on-disk blocks. Ensure you reference RotateToNextFile, DiscardOldestFile,
SolidSyslogBlockDevice_Acquire, SolidSyslogBlockDevice_Dispose, writeSequence,
oldestSequence, NextSequence, and ResetReadToOldest when making the changes.
- Around line 88-113: ScanForExistingBlocks currently assumes non-wrapping
sequences; fix it by building a presence map of size MAX_SEQUENCE using
SolidSyslogBlockDevice_Exists for each seq, return false if none set, then find
the longest contiguous run of true entries allowing wrap-around (treat array as
circular) — pick its first index as blockSequence->oldestSequence, set
blockSequence->writeSequence = (oldest + run_length - 1) % MAX_SEQUENCE, and
blockSequence->readSequence = oldest; ensure you handle uint8_t casts
consistently and keep MAX_SEQUENCE used for loop bounds when computing run
lengths.

In `@Core/Source/RecordStore.c`:
- Around line 271-299: AdvancePastSentRecord currently treats read errors as
"sent" because IsRecordSent seeds flag with SENT_FLAG_SENT and ignores
SolidSyslogBlockDevice_Read's return; update the logic so a failed sent-flag
read is treated as corruption: change IsRecordSent into a TryReadSentFlag (or
make it return a success boolean and an out-parameter isSent) that calls
SolidSyslogBlockDevice_Read and returns false on failure, then in
AdvancePastSentRecord (after ReadAndValidateRecord succeeds) check the read
result—on read failure set *cursor = fileSize and *corrupt = true, otherwise if
isSent call SkipRecord and set advanced = true; reference symbols:
AdvancePastSentRecord, IsRecordSent (or new TryReadSentFlag),
SolidSyslogBlockDevice_Read, SentFlagFileOffset, SENT_FLAG_SENT, SENT_FLAG_SIZE,
SkipRecord, ReadAndValidateRecord.

---

Nitpick comments:
In `@Tests/SolidSyslogFileBlockDeviceTest.cpp`:
- Around line 144-154: Add a boundary test that verifies out-of-range block
indices are rejected: call SolidSyslogBlockDevice_Acquire(device, 100) (or 256)
and assert it returns a failure status and that no file was created at
"/tmp/blockdev_100.log" by using SolidSyslogFile_Exists(writeFile,
"/tmp/blockdev_100.log") == false; place this alongside the existing tests
(referencing SolidSyslogBlockDevice_Acquire, SolidSyslogFile_Exists, and
writeFile) to lock the expected unsupported-index behavior and prevent
filename-collision regressions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9665a7cb-357a-43e5-b284-c1df12126963

📥 Commits

Reviewing files that changed from the base of the PR and between e97da40 and 3436aaf.

📒 Files selected for processing (25)
  • Bdd/features/block_lifecycle.feature
  • Bdd/features/steps/syslog_steps.py
  • CLAUDE.md
  • Core/Interface/SolidSyslogBlockDevice.h
  • Core/Interface/SolidSyslogBlockDeviceDefinition.h
  • Core/Interface/SolidSyslogFileBlockDevice.h
  • Core/Interface/SolidSyslogFileStore.h
  • Core/Source/BlockSequence.c
  • Core/Source/BlockSequence.h
  • Core/Source/CMakeLists.txt
  • Core/Source/RecordStore.c
  • Core/Source/RecordStore.h
  • Core/Source/SolidSyslogBlockDevice.c
  • Core/Source/SolidSyslogFileBlockDevice.c
  • Core/Source/SolidSyslogFileStore.c
  • DEVLOG.md
  • Example/Threaded/main.c
  • Tests/CMakeLists.txt
  • Tests/FileFake.c
  • Tests/FileFake.h
  • Tests/FileFakeTest.cpp
  • Tests/SolidSyslogBlockDeviceTest.cpp
  • Tests/SolidSyslogFileBlockDeviceTest.cpp
  • Tests/SolidSyslogFileStorePosixTest.cpp
  • Tests/SolidSyslogFileStoreTest.cpp

Comment thread Core/Source/SolidSyslogFileBlockDevice.c
Comment thread Tests/FileFake.c
…d sequence on resume

Two correctness fixes raised in PR review:

- SolidSyslogFileBlockDevice: blockIndex was cast to uint8_t before
  filename formatting. Indices > 99 silently aliased to existing blocks
  (256 -> 00) and could overwrite or delete unrelated content. Now
  validated against MAX_BLOCK_INDEX (= 99, the two-digit on-disk
  capacity) at every entry point and rejected if out of range.
- BlockSequence: ScanForExistingBlocks treated the lowest-numbered file
  as oldest and the highest as the write block. After enough rotations
  the on-disk set straddles the 99 -> 00 boundary (e.g. {98, 99, 0, 1})
  and the naive scan misclassifies it as oldest=00 / write=99 — the
  next rotation would then truncate block 00 and we'd read in the
  wrong order. Now finds the gap and reads the run as its complement,
  treating the sequence space as circular.

Added BlockSequenceTest.cpp with focused tests for the scan algorithm
(cold start, linear range, wrapped range, single-block-at-boundary).
Pre-existing in the old code; surfaced by the BlockDevice abstraction.

Part of S18.02 PR review (#243).
…ent comment

Three review-feedback follow-ups, none behaviour-changing:

- SolidSyslogFileStoreConfig.blockDevice now carries an explicit
  ownership/lifetime contract: caller-owned, must outlive the FileStore,
  not destroyed by SolidSyslogFileStore_Destroy.
- FileFake_FailNext{Open,Write,Read} reject a null file with TestAssert_Fail
  instead of segfaulting on dereference. Defensive guard in the test helper.
- IsRecordSent: the deliberate intent of seeding flag = SENT_FLAG_SENT and
  ignoring the BlockDevice_Read return value is now documented in code.
  Treating an unreadable sent-flag as already-sent keeps the scan moving;
  the alternative (stop on the first bad byte) would jam the logger and
  skip every record after it. The single skipped record surfaces as a
  sequenceId gap downstream, which the receiver detects per RFC 5424
  §6.3.1, and a future integrator-supplied error reporter will cover the
  persistent-media-error case directly.

Also adds pr*-comments.json to .gitignore so the gh-api scratch file
doesn't get accidentally staged.

Part of S18.02 PR review (#243).
@DavidCozens

Copy link
Copy Markdown
Owner Author

Responses to the four outside-diff findings in the CodeRabbit review (couldn't be replied to inline):

Core/Interface/SolidSyslogFileStore.h 33-45 — blockDevice ownership. Adopted in 8b31996. The blockDevice field now carries a comment stating it is caller-owned, must outlive the FileStore, and is not destroyed by SolidSyslogFileStore_Destroy.

Core/Source/RecordStore.c 271-299 — sent-flag read failures. Not adopting the suggested change — the current behaviour is deliberate. Treating an unreadable sent-flag as already-sent keeps the scan moving past one bad byte; the alternative (jump cursor to fileSize, mark file corrupt) would skip every record after the first read failure and force an unnecessary rotation. The single skipped record surfaces downstream as an RFC 5424 §6.3.1 sequenceId gap, which the receiver detects. A future integrator-supplied error reporter will cover persistent media errors directly. Intent is now documented in code (8b31996) so this stays clear to future maintainers.

Core/Source/BlockSequence.c 88-113 — wrapped sequence on resume. Adopted (different shape than the suggested algorithm) in 4e8842c. ScanForExistingBlocks now treats the sequence space as circular, finds the absent-block region, and reads the run as its complement. New focused unit tests in Tests/BlockSequenceTest.cpp cover cold start, linear, wrapped, and single-block-at-boundary cases.

Core/Source/BlockSequence.c 166-195 — Acquire/Dispose return ignored on rotation/discard. Deferred to S18.04 (#237) and captured in that issue's "Design considerations (preliminary)" section. The pre-existing pattern (also present in the pre-S18.02 code, which ignored SolidSyslogFile_Open/_Delete returns) is benign for the file-backed driver because POSIX O_CREAT heals the missing-file case lazily on the next Append. For flash, where Acquire = erase, the right semantics need a real flash driver to inform them — co-designing with S18.04 is the right time to settle propagate-vs-retry-vs-error-report.

The "boundary test for unsupported block indices" nitpick is also addressed in 4e8842c.

@github-actions

github-actions Bot commented May 3, 2026

Copy link
Copy Markdown
Contributor

☀️   Quality Summary

   🚦   build-linux-gcc: 100% successful (✔️ 1012 passed, 🙈 3 skipped)
   🚦   build-linux-clang: 100% successful (✔️ 961 passed, 🙈 3 skipped)
   🚦   sanitize-linux-gcc: 100% successful (✔️ 961 passed, 🙈 3 skipped)
   🚦   integration-linux-openssl: 100% successful (✔️ 9 passed)
   🚦   bdd-linux-syslog-ng: 100% successful (✔️ 45 passed)
   🚦   bdd-windows-otel: 58% successful (✔️ 26 passed, 🙈 19 skipped)
   🚦   build-windows-msvc: 100% successful (✔️ 874 passed, 🙈 1 skipped)
   ⚠️   Clang-Tidy: No warnings
   ⚠️   CPPCheck: No warnings


Created by Quality Monitor v1.14.0 (#f3859fd). More details are shown in the GitHub Checks Result.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
Core/Source/BlockSequence.c (1)

218-245: ⚠️ Potential issue | 🔴 Critical | 🏗️ Heavy lift

Don’t commit rotation state before the block-device mutation succeeds.

RotateToNextFile() advances writeSequence/writePosition before it knows SolidSyslogBlockDevice_Acquire() actually prepared an empty block, and DiscardOldestFile() advances oldestSequence even if Dispose() fails. That breaks the new block-device contract in Core/Interface/SolidSyslogBlockDevice.h:9-14: on an Acquire failure the next Append can land on stale data, and on a Dispose failure a later restart can rediscover records that were supposed to be evicted. This path needs a real success/failure result from rotation, not just readFileChanged.

Possible shape
-static bool RotateToNextFile(struct BlockSequence* blockSequence);
+static bool RotateToNextFile(struct BlockSequence* blockSequence, bool* readFileChanged);

 ...
-        *readFileChanged = RotateToNextFile(blockSequence);
+        spaceAvailable = RotateToNextFile(blockSequence, readFileChanged);

Then:

  • acquire the next block into a local nextSequence first,
  • only publish writeSequence / writePosition after Acquire succeeds,
  • only advance oldestSequence after Dispose succeeds.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Core/Source/BlockSequence.c` around lines 218 - 245, RotateToNextFile
currently advances writeSequence/writePosition before calling
SolidSyslogBlockDevice_Acquire and DiscardOldestFile advances oldestSequence
before calling SolidSyslogBlockDevice_Dispose; change the flow so you first call
Acquire into a local nextSequence (do not modify
blockSequence->writeSequence/writePosition until Acquire returns success), only
publish writeSequence/writePosition after Acquire succeeds, and change
DiscardOldestFile to call Dispose and only update blockSequence->oldestSequence
(and call ResetReadToOldest if needed) when Dispose returns success; preserve
the existing return bool (readFileChanged) but make it reflect successful
rotation/discard operations and use NextSequence,
SolidSyslogBlockDevice_Acquire, SolidSyslogBlockDevice_Dispose,
ResetReadToOldest, writeSequence, writePosition and oldestSequence to locate the
relevant code paths.
🧹 Nitpick comments (1)
Tests/SolidSyslogFileBlockDeviceTest.cpp (1)

48-58: ⚡ Quick win

Add a re-acquire truncation test.

These cases prove a brand-new block starts empty, but not that reacquiring an existing block clears prior contents. Core/Interface/SolidSyslogBlockDevice.h:9-14 makes that guarantee, and the new BlockSequence rotation path now depends on it.

Suggested test shape
 TEST(SolidSyslogFileBlockDevice, AcquireFreshBlockHasZeroSize)
 {
     SolidSyslogBlockDevice_Acquire(device, 0);
     LONGS_EQUAL(0, SolidSyslogBlockDevice_Size(device, 0));
 }
+
+TEST(SolidSyslogFileBlockDevice, ReacquireTruncatesExistingBlock)
+{
+    CHECK_TRUE(SolidSyslogBlockDevice_Acquire(device, 0));
+    CHECK_TRUE(SolidSyslogBlockDevice_Append(device, 0, "abc", 3));
+
+    CHECK_TRUE(SolidSyslogBlockDevice_Acquire(device, 0));
+    LONGS_EQUAL(0, SolidSyslogBlockDevice_Size(device, 0));
+}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Tests/SolidSyslogFileBlockDeviceTest.cpp` around lines 48 - 58, Add a test
that verifies re-acquiring an already-existing block truncates its contents:
create a new TEST (e.g., AcquireReacquireTruncates) that calls
SolidSyslogBlockDevice_Acquire(device, 0), writes some data with
SolidSyslogBlockDevice_Write(device, 0, buf, len), checks the size is >0 via
SolidSyslogBlockDevice_Size(device, 0), then calls
SolidSyslogBlockDevice_Acquire(device, 0) again and asserts
SolidSyslogBlockDevice_Size(device, 0) == 0 (and optionally
SolidSyslogBlockDevice_Exists(device, 0) is true) to confirm the block was
cleared on re-acquire.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Tests/BlockSequenceTest.cpp`:
- Around line 114-156: Each test must assert that BlockSequence_Open(&sequence)
actually succeeds before checking read/write values: insert
CHECK_TRUE(BlockSequence_Open(&sequence)); at the start of each test
(ColdStartAcquiresBlockZero, ResumesContiguousLinearRange,
ResumesAtZeroWhenOnlyBlockZeroExists, ResumesWrappedSequenceRangeCorrectly,
ResumesWrappedSingleBlockAtBoundary) immediately before calling
BlockSequence_ReadSequence and BlockSequence_WriteSequence to prevent the
zero-initialized fixture from masking open/acquire failures.

---

Outside diff comments:
In `@Core/Source/BlockSequence.c`:
- Around line 218-245: RotateToNextFile currently advances
writeSequence/writePosition before calling SolidSyslogBlockDevice_Acquire and
DiscardOldestFile advances oldestSequence before calling
SolidSyslogBlockDevice_Dispose; change the flow so you first call Acquire into a
local nextSequence (do not modify blockSequence->writeSequence/writePosition
until Acquire returns success), only publish writeSequence/writePosition after
Acquire succeeds, and change DiscardOldestFile to call Dispose and only update
blockSequence->oldestSequence (and call ResetReadToOldest if needed) when
Dispose returns success; preserve the existing return bool (readFileChanged) but
make it reflect successful rotation/discard operations and use NextSequence,
SolidSyslogBlockDevice_Acquire, SolidSyslogBlockDevice_Dispose,
ResetReadToOldest, writeSequence, writePosition and oldestSequence to locate the
relevant code paths.

---

Nitpick comments:
In `@Tests/SolidSyslogFileBlockDeviceTest.cpp`:
- Around line 48-58: Add a test that verifies re-acquiring an already-existing
block truncates its contents: create a new TEST (e.g.,
AcquireReacquireTruncates) that calls SolidSyslogBlockDevice_Acquire(device, 0),
writes some data with SolidSyslogBlockDevice_Write(device, 0, buf, len), checks
the size is >0 via SolidSyslogBlockDevice_Size(device, 0), then calls
SolidSyslogBlockDevice_Acquire(device, 0) again and asserts
SolidSyslogBlockDevice_Size(device, 0) == 0 (and optionally
SolidSyslogBlockDevice_Exists(device, 0) is true) to confirm the block was
cleared on re-acquire.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 01e03c27-cefa-415d-bbae-2261711eb578

📥 Commits

Reviewing files that changed from the base of the PR and between 3436aaf and 8b31996.

📒 Files selected for processing (9)
  • .gitignore
  • Core/Interface/SolidSyslogFileStore.h
  • Core/Source/BlockSequence.c
  • Core/Source/RecordStore.c
  • Core/Source/SolidSyslogFileBlockDevice.c
  • Tests/BlockSequenceTest.cpp
  • Tests/CMakeLists.txt
  • Tests/FileFake.c
  • Tests/SolidSyslogFileBlockDeviceTest.cpp
✅ Files skipped from review due to trivial changes (2)
  • .gitignore
  • Core/Interface/SolidSyslogFileStore.h
🚧 Files skipped from review as they are similar to previous changes (2)
  • Tests/FileFake.c
  • Core/Source/SolidSyslogFileBlockDevice.c

Comment thread Tests/BlockSequenceTest.cpp
After S18.02 BlockSequence and RecordStore work in block-space (over a
SolidSyslogBlockDevice). Their lingering 'File' terminology now misreads
— blocks may not be files at all once flash drivers land. Renamed
internal-only symbols across BlockSequence, RecordStore, and FileStore:

  BlockSequence_AdvanceToNextReadFile  -> _AdvanceToNextReadBlock
  BlockSequence_IsReadingOlderFile     -> _IsReadingOlderBlock
  BlockSequence_MarkWriteFileCorrupt   -> _MarkWriteBlockCorrupt
  field writeFileCorrupt               -> writeBlockCorrupt
  static FileIsFull / FileCount        -> BlockIsFull / BlockCount
  static RotateToNextFile              -> RotateToNextBlock
  static DiscardOldestFile             -> DiscardOldestBlock
  parameter readFileChanged            -> readBlockChanged
  local readingOldestFile              -> readingOldestBlock

  RecordStore_FindFirstUnsent fileSize -> blockSize
  static IntegrityChecksumFileOffset   -> IntegrityChecksumOffset
  static SentFlagFileOffset            -> SentFlagOffset
  field lastSentFlagFileOffset         -> lastSentFlagOffset
  comments referring to 'file' updated to 'block'

  static ResumeFromExistingFile        -> ResumeFromExistingBlock

Public surface unchanged. SolidSyslogFileStoreConfig.maxFileSize /
.maxFiles deliberately retained — they describe the file-backed store's
configuration and match the user-facing CLI vocabulary
(--max-file-size, --max-files).
…NS macro

Three small clarity wins surfaced by the hygiene pass:

- BlockSequence_PrepareForWrite evaluated BlockIsFull twice. Cache the
  result in a local. Same logic, one call site.
- NotifyThresholdCrossed mixed the 'is the threshold even configured'
  check with the 'is usage above the threshold' check inline. Extract
  IsAboveThreshold so the body reads as the state machine it is. Single
  return preserved per the project's MISRA leanings.
- FileBlockDevice's EnsureHandleOpenOnBlock had a three-condition
  composite (cached open + underlying actually open + matching block) on
  the early-out line. Extract IsHandleAlreadyOpenOnBlock — the early-out
  now reads as one named predicate.

On the test side, repeated 'Acquire + Append + read-and-MEMCMP' shapes
become CHECK_BLOCK_CONTAINS(blockIndex, offset, expected, length).
Macro (not function) so failures report the caller's __FILE__/__LINE__,
following the CHECK_PRIVAL pattern in SolidSyslogTest.cpp.

Coverage holds at 100% lines + functions; 1398 checks (was 1393).
…single-return

Three small MISRA-leaning fixes from the hygiene pass:

- BlockIsFull mixed || with the > comparison without explicit bracketing.
  Operator precedence makes the result well-defined but the read is
  ambiguous; bracket each operand of || explicitly.
- ScanForExistingBlocks had two return statements (an early 'no files'
  exit, and a final 'found and configured' return). Restructure to single
  return via the foundAny flag, wrapping the configure step in if(foundAny)
  and returning that flag.
- BlockSequence_UsedBytes had two return statements (atCapacity early
  exit and the normal compute path). Restructure to compute into a single
  'used' local and return it once.

No behaviour change. All other compound conditions in the E18 code are
plain '&& between bool-typed operands' with no precedence ambiguity, and
match the project's existing convention (see SolidSyslog.c, StreamSender.c).
Mixed-type arithmetic was audited and found clean — every integer
narrowing is via an explicit cast, every uint8_t-to-int promotion is on
expressions that subsequently get an explicit (uint8_t) or (size_t) cast.
Eleven FileStore TEST_GROUPs each declared the same six fixture members
(readStorage, storage, readFile, file, deviceStorage, device) and ran the
same three-line setup + four-line teardown. Lift the boilerplate into a
shared TEST_BASE; each TEST_GROUP becomes TEST_GROUP_BASE + a
setupBlockDeviceFakes() / teardownBlockDeviceFakes() call.

Test bodies still reference 'file', 'readFile', 'device' directly —
they're inherited members. Two pre-existing groups that called the
write-side fake 'writeFile' (Rotation, CorruptionRecovery) renamed to
'file' for consistency with the base. SolidSyslogFile_* call sites
updated accordingly.

168 lines deleted, 94 added. 1007 tests, 100% coverage preserved.
Public API rename for vocabulary consistency. After S18.02 the FileStore
operates on a SolidSyslogBlockDevice — what the integrator configures is
the BlockDevice's behavior (block count + per-block size), and the
'file' framing was a leftover from the pre-BlockDevice API. Renamed for
clarity before the library hits beta.

Touched:
- SolidSyslogFileStoreConfig.maxFileSize/.maxFiles → .maxBlockSize/.maxBlocks
- BlockSequence internal struct + config + clamp constants
  (MIN_MAX_FILES → MIN_MAX_BLOCKS, MAX_MAX_FILES → MAX_MAX_BLOCKS)
- BuildBlockSequenceConfig locals (minFileSize → minBlockSize, etc.)
- ExampleOptions.maxFiles/.maxFileSize → .maxBlocks/.maxBlockSize, plus
  the example's CLI args (--max-files → --max-blocks,
  --max-file-size → --max-block-size) and their default constants
- ExampleCommandLineTest: variable references, CLI arg strings, test names
- BDD step definitions: regex pattern + context attribute names + the
  comment that referenced MIN_MAX_FILE_SIZE
- BDD scenarios: 4 .feature files updated to the new CLI vocabulary
- FileStoreTest: TEST_RECORDS_PER_FILE → TEST_RECORDS_PER_BLOCK,
  TEST_MAX_FILE_SIZE → TEST_MAX_BLOCK_SIZE, TEST_MAX_FILES → TEST_MAX_BLOCKS,
  CreateWithMaxFiles → CreateWithMaxBlocks, CreateWithMaxFileSize →
  CreateWithMaxBlockSize, all helper parameters

No behaviour change. 1007 tests, 100% Core coverage.
The post-S18.02 name 'IsReadingOlderBlock' read awkwardly because
'older' is implicit context (older than what?). The semantics is
'the read sequence has not caught up to the write sequence' — i.e. the
reader is on a block that the writer has already moved past. Rename
makes the intent direct: ReadIsBehindWrite. Internal-only, BlockSequence
isn't a public API.
Adds a Design Patterns section after Code Style. Covers production code
(single return, intent-naming static-inline predicates, function ordering,
selective bracketing, null-object preference) and test code (TEST_BASE
for shared fixture, CHECK_* macros for assertion shapes, DRY setup +
asserts). Each pattern points at a concrete example in the codebase so
future drift is easy to flag at review time.
…eOnBlock

Two cyclomatic-complexity reductions surfaced in the Phase 2 scan.

BlockSequence::ScanForExistingBlocks (was CC=8):
- Extract ScanForBlockPresence: walks 0..MAX_SEQUENCE-1, fills the
  presence map and records foundAny / foundAbsent / firstAbsent into a
  new file-private BlockPresence struct.
- Extract LocateRunBoundaries: given a BlockPresence with foundAbsent,
  walk outward from the gap to find the run endpoints.
- ScanForExistingBlocks now reads as: scan presence, if anything found
  locate the run, store the result. Top-level CC drops to 2.

FileBlockDevice::EnsureHandleOpenOnBlock (was CC=6):
- Extract OpenHandleOnBlock: closes if open, formats the block filename,
  opens, records the new state. Returns whether the open succeeded.
- EnsureHandleOpenOnBlock becomes: 'if already open on this block, done;
  else reopen'. CC drops to 2.

Coverage holds at 100% (391 functions, was 388 — three new helpers all
exercised by the existing tests). Single return preserved throughout.
After S18.02 the store is BlockDevice-backed and storage-medium-agnostic;
the File in the name was a pre-S18.02 leftover. Renames the public type,
header, and storage size macro, the internal struct/helper, the test files
and TEST_GROUP_BASE names, the BDD step phrase, and the going-forward
references in CLAUDE.md, README.md, docs/iec62443.md, and inline comments.
DEVLOG entries left as historical context.
@github-actions

github-actions Bot commented May 3, 2026

Copy link
Copy Markdown
Contributor

☀️   Quality Summary

   🚦   build-linux-gcc: 100% successful (✔️ 1012 passed, 🙈 3 skipped)
   🚦   build-linux-clang: 100% successful (✔️ 961 passed, 🙈 3 skipped)
   🚦   sanitize-linux-gcc: 100% successful (✔️ 961 passed, 🙈 3 skipped)
   🚦   integration-linux-openssl: 100% successful (✔️ 9 passed)
   🚦   bdd-linux-syslog-ng: 100% successful (✔️ 45 passed)
   🚦   bdd-windows-otel: 58% successful (✔️ 26 passed, 🙈 19 skipped)
   🚦   build-windows-msvc: 100% successful (✔️ 874 passed, 🙈 1 skipped)
   ⚠️   Clang-Tidy: No warnings
   ⚠️   CPPCheck: No warnings


Created by Quality Monitor v1.14.0 (#f3859fd). More details are shown in the GitHub Checks Result.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
Tests/Example/ExampleCommandLineTest.cpp (1)

24-30: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Stale test name: DefaultMaxFiles should be DefaultMaxBlocks.

The body was updated to assert options.maxBlocks but the test name was left as DefaultMaxFiles, which will mislead future readers and tooling (test reports, search).

🔧 Proposed fix
-TEST(ExampleCommandLine, DefaultMaxFiles)
+TEST(ExampleCommandLine, DefaultMaxBlocks)
 {
     char  arg0[] = "test";
     char* argv[] = {arg0, nullptr};
     Parse(1, argv);
     LONGS_EQUAL(10, options.maxBlocks);
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Tests/Example/ExampleCommandLineTest.cpp` around lines 24 - 30, Rename the
test from DefaultMaxFiles to DefaultMaxBlocks to match the assertion and avoid
confusion: update the TEST macro invocation (TEST(ExampleCommandLine,
DefaultMaxFiles)) to TEST(ExampleCommandLine, DefaultMaxBlocks) so the test name
aligns with the body that calls Parse(1, argv) and asserts LONGS_EQUAL(10,
options.maxBlocks).
🧹 Nitpick comments (1)
Tests/SolidSyslogBlockStoreTest.cpp (1)

505-533: 💤 Low value

Test names still use "MaxFiles" terminology instead of "MaxBlocks".

The test names reference the old file-store vocabulary (MaxFilesZeroClampedToMinimum, MaxFileSizeZeroClampedToMinimum) while the implementation uses block-store terminology (maxBlocks, maxBlockSize). This creates a terminology mismatch.

♻️ Suggested rename for consistency
-TEST(SolidSyslogBlockStoreConfig, MaxFilesZeroClampedToMinimum)
+TEST(SolidSyslogBlockStoreConfig, MaxBlocksZeroClampedToMinimum)
...
-TEST(SolidSyslogBlockStoreConfig, MaxFilesOneClampedToMinimum)
+TEST(SolidSyslogBlockStoreConfig, MaxBlocksOneClampedToMinimum)
...
-TEST(SolidSyslogBlockStoreConfig, MaxFilesHundredClampedToMaximum)
+TEST(SolidSyslogBlockStoreConfig, MaxBlocksHundredClampedToMaximum)
...
-TEST(SolidSyslogBlockStoreConfig, MaxFileSizeZeroClampedToMinimum)
+TEST(SolidSyslogBlockStoreConfig, MaxBlockSizeZeroClampedToMinimum)
...
-TEST(SolidSyslogBlockStoreConfig, MaxFileSizeOneClampedToMinimum)
+TEST(SolidSyslogBlockStoreConfig, MaxBlockSizeOneClampedToMinimum)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Tests/SolidSyslogBlockStoreTest.cpp` around lines 505 - 533, Rename the test
identifiers to use "MaxBlocks" and "MaxBlockSize" to match the implementation:
update TEST names MaxFilesZeroClampedToMinimum -> MaxBlocksZeroClampedToMinimum,
MaxFilesOneClampedToMinimum -> MaxBlocksOneClampedToMinimum,
MaxFilesHundredClampedToMaximum -> MaxBlocksHundredClampedToMaximum,
MaxFileSizeZeroClampedToMinimum -> MaxBlockSizeZeroClampedToMinimum, and
MaxFileSizeOneClampedToMinimum -> MaxBlockSizeOneClampedToMinimum; leave the
bodies calling CreateWithMaxBlocks/CreateWithMaxBlockSize and
VerifyWriteAndReadBack unchanged so the test semantics remain the same.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Core/Source/SolidSyslogBlockStore.c`:
- Around line 114-127: ResumeFromExistingBlock passes the write-block size to
RecordStore_FindFirstUnsent but must use the actual read-block size when
scanning the ReadSequence; change the third argument so it calls
SolidSyslogBlockDevice_Size for the read block (i.e. use
SolidSyslogBlockDevice_Size(BlockSequence_BlockDevice(&blockStore->blockSequence),
BlockSequence_ReadSequence(&blockStore->blockSequence)) instead of
BlockSequence_WritePosition(&blockStore->blockSequence)) so the cursor is
computed using the read block's real size before calling
BlockSequence_SetReadCursor and handling corrupt.

---

Outside diff comments:
In `@Tests/Example/ExampleCommandLineTest.cpp`:
- Around line 24-30: Rename the test from DefaultMaxFiles to DefaultMaxBlocks to
match the assertion and avoid confusion: update the TEST macro invocation
(TEST(ExampleCommandLine, DefaultMaxFiles)) to TEST(ExampleCommandLine,
DefaultMaxBlocks) so the test name aligns with the body that calls Parse(1,
argv) and asserts LONGS_EQUAL(10, options.maxBlocks).

---

Nitpick comments:
In `@Tests/SolidSyslogBlockStoreTest.cpp`:
- Around line 505-533: Rename the test identifiers to use "MaxBlocks" and
"MaxBlockSize" to match the implementation: update TEST names
MaxFilesZeroClampedToMinimum -> MaxBlocksZeroClampedToMinimum,
MaxFilesOneClampedToMinimum -> MaxBlocksOneClampedToMinimum,
MaxFilesHundredClampedToMaximum -> MaxBlocksHundredClampedToMaximum,
MaxFileSizeZeroClampedToMinimum -> MaxBlockSizeZeroClampedToMinimum, and
MaxFileSizeOneClampedToMinimum -> MaxBlockSizeOneClampedToMinimum; leave the
bodies calling CreateWithMaxBlocks/CreateWithMaxBlockSize and
VerifyWriteAndReadBack unchanged so the test semantics remain the same.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 83b3e53f-93db-4938-a8a8-72eb88c5ddbd

📥 Commits

Reviewing files that changed from the base of the PR and between 8b31996 and 15c545b.

📒 Files selected for processing (28)
  • Bdd/features/block_lifecycle.feature
  • Bdd/features/capacity_threshold.feature
  • Bdd/features/power_cycle_replay.feature
  • Bdd/features/steps/syslog_steps.py
  • Bdd/features/store_and_forward.feature
  • Bdd/features/store_capacity.feature
  • CLAUDE.md
  • Core/Interface/SolidSyslogBlockStore.h
  • Core/Source/BlockSequence.c
  • Core/Source/BlockSequence.h
  • Core/Source/CMakeLists.txt
  • Core/Source/RecordStore.c
  • Core/Source/RecordStore.h
  • Core/Source/SolidSyslogBlockStore.c
  • Core/Source/SolidSyslogFileBlockDevice.c
  • Core/Source/SolidSyslogFileStore.c
  • Example/Common/ExampleCommandLine.c
  • Example/Common/ExampleCommandLine.h
  • Example/Threaded/main.c
  • Platform/Windows/Source/SolidSyslogWindowsFile.c
  • README.md
  • Tests/BlockSequenceTest.cpp
  • Tests/CMakeLists.txt
  • Tests/Example/ExampleCommandLineTest.cpp
  • Tests/SolidSyslogBlockStorePosixTest.cpp
  • Tests/SolidSyslogBlockStoreTest.cpp
  • Tests/SolidSyslogFileBlockDeviceTest.cpp
  • docs/iec62443.md
💤 Files with no reviewable changes (1)
  • Core/Source/SolidSyslogFileStore.c
✅ Files skipped from review due to trivial changes (6)
  • Bdd/features/store_and_forward.feature
  • Core/Source/CMakeLists.txt
  • Platform/Windows/Source/SolidSyslogWindowsFile.c
  • Bdd/features/block_lifecycle.feature
  • README.md
  • Tests/BlockSequenceTest.cpp
🚧 Files skipped from review as they are similar to previous changes (2)
  • Core/Source/RecordStore.h
  • Tests/SolidSyslogFileBlockDeviceTest.cpp

Comment thread Core/Source/SolidSyslogBlockStore.c
…ce tests

- Rename DefaultMaxFiles -> DefaultMaxBlocks (ExampleCommandLineTest) to
  match the maxBlocks field the body asserts.
- Rename MaxFiles*/MaxFileSize* -> MaxBlocks*/MaxBlockSize* in the
  BlockStoreConfig group (still referenced the old field names after the
  31aea6c rename).
- Add ReacquireTruncatesExistingBlock to the FileBlockDevice tests; covers
  the BlockDevice contract that BlockSequence_RotateToNextBlock relies on.
- CHECK_TRUE(BlockSequence_Open(&sequence)) in the resume tests so a future
  Open/Acquire regression cannot pass under a zero-init fixture.
ResumeFromExistingBlock passed WritePosition to FindFirstUnsent as the
scan cap, but WritePosition is the size of the WRITE block, not the read
block. On a multi-block resume the read block is a closed earlier block
whose size is independent of the write block's fill level. With the
buggy cap the scan stopped early, causing already-sent records past the
cap to be re-emitted on resume.

Query SolidSyslogBlockDevice_Size for the read block instead. The
BlockSequence_WritePosition accessor was the only caller — removed,
along with its declaration.

Adds a regression test (ResumeFindsUnsentInClosedReadBlockWhenWriteBlockPartial)
that fails without the fix and passes with it. Three records per block,
two drained, one record in the partial write block — the configuration
needed to expose the cap-stopping-too-early scan.
@github-actions

github-actions Bot commented May 3, 2026

Copy link
Copy Markdown
Contributor

☀️   Quality Summary

   🚦   build-linux-gcc: 100% successful (✔️ 1014 passed, 🙈 3 skipped)
   🚦   build-linux-clang: 100% successful (✔️ 963 passed, 🙈 3 skipped)
   🚦   sanitize-linux-gcc: 100% successful (✔️ 963 passed, 🙈 3 skipped)
   🚦   integration-linux-openssl: 100% successful (✔️ 9 passed)
   🚦   bdd-linux-syslog-ng: 100% successful (✔️ 45 passed)
   🚦   bdd-windows-otel: 58% successful (✔️ 26 passed, 🙈 19 skipped)
   🚦   build-windows-msvc: 100% successful (✔️ 876 passed, 🙈 1 skipped)
   ⚠️   Clang-Tidy: No warnings
   ⚠️   CPPCheck: No warnings


Created by Quality Monitor v1.14.0 (#f3859fd). More details are shown in the GitHub Checks Result.

@DavidCozens

Copy link
Copy Markdown
Owner Author

Sweep through the outstanding CodeRabbit comments — pushed two follow-up commits and deferred one item to S18.03.

Addressed in 556b896 (test hygiene):

  • Tests/Example/ExampleCommandLineTest.cpp — renamed DefaultMaxFilesDefaultMaxBlocks to match the renamed options.maxBlocks field.
  • Tests/SolidSyslogBlockStoreTest.cpp:505-533 — renamed MaxFiles* / MaxFileSize* test names → MaxBlocks* / MaxBlockSize*.
  • Tests/SolidSyslogFileBlockDeviceTest.cpp — added ReacquireTruncatesExistingBlock covering the truncate-on-reacquire contract that BlockSequence_RotateToNextBlock relies on.
  • Tests/BlockSequenceTest.cppCHECK_TRUE(BlockSequence_Open(&sequence)) added to all five resume tests.

Fixed in 8b13d2f (production bug + regression test):

  • Core/Source/SolidSyslogBlockStore.cResumeFromExistingBlock now bounds the scan by SolidSyslogBlockDevice_Size(device, readSequence) rather than WritePosition. New ResumeFindsUnsentInClosedReadBlockWhenWriteBlockPartial test fails without the fix and passes with it. The unused BlockSequence_WritePosition accessor was removed.

Deferred to S18.03 #236:

  • Core/Source/BlockSequence.cRotateToNextBlock and DiscardOldestBlock not propagating Acquire / Dispose failure: real concern, but it changes the shape of PrepareForWrite's contract and overlaps S18.03's empty-check-on-Acquire work. S18.03: Dispose-on-empty block lifecycle #236 has been updated with the explicit success-only-state-advancement scope item plus matching BDD acceptance criteria.

Already addressed in earlier commits — leaving as historical context:

  • Core/Source/BlockSequence.c:88-113 (wrap-around in ScanForExistingBlocks) — handled by LocateRunBoundaries in 4e8842c; the doc comment at lines 107-112 describes the gap-detection approach, and BlockSequenceTest.cpp has ResumesWrappedSequenceRangeCorrectly / ResumesWrappedSingleBlockAtBoundary covering it.
  • Core/Interface/SolidSyslogFileStore.h:33-45 (blockDevice ownership/lifetime doc) — the rename to SolidSyslogBlockStore.h carried the doc forward; current header lines 33-37 say Caller-owned: must outlive the BlockStore. SolidSyslogBlockStore_Destroy does NOT destroy the block device — that is the integrator's responsibility.

Declining — see in-tree design rationale:

  • Core/Source/RecordStore.c:271-299 (treat sent-flag read failures as corruption instead of "sent") — the comment at lines 294-301 explicitly defends the current behaviour and rejects the proposed alternative: "a sent-flag we cannot read is treated as already-sent so the scan keeps walking the block. The alternative — stop scanning, refuse to advance — would jam the logger on one bad byte and skip every record that follows in the same block." Single skipped records surface to the SIEM via the MetaSd sequenceId gap (RFC 5424 §7.3.1). I'd rather keep that design and surface persistent media errors via the integrator-supplied error reporter when that path lands.

@DavidCozens DavidCozens merged commit 33e90cc into main May 3, 2026
14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

S18.02: Define SolidSyslogBlockDevice interface and file-backed driver

1 participant