Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
7bfc10a
feat(blockdevice): introduce BlockDevice consumer API and vtable
DavidCozens May 2, 2026
1c7f750
feat(blockdevice): file-backed driver wraps SolidSyslogFile with sequ…
DavidCozens May 3, 2026
64de70c
refactor(filestore): drive BlockSequence + RecordStore via BlockDevic…
DavidCozens May 3, 2026
ae29955
test(bdd): assert no Dispose when capacity is not exhausted (S18.02)
DavidCozens May 3, 2026
3436aaf
docs: update DEVLOG for S18.02 BlockDevice introduction
DavidCozens May 3, 2026
4e8842c
fix(blockdevice): reject out-of-range block indices and handle wrappe…
DavidCozens May 3, 2026
8b31996
chore: blockDevice ownership doc, FileFake null guards, sent-flag int…
DavidCozens May 3, 2026
c305678
refactor(blockdevice): rename internal File symbols to Block
DavidCozens May 3, 2026
03b1c48
refactor(blockdevice): name intent in helpers; add CHECK_BLOCK_CONTAI…
DavidCozens May 3, 2026
66fe566
refactor(blockdevice): MISRA mechanics — bracket compound condition, …
DavidCozens May 3, 2026
f3c7640
refactor(tests): extract BlockDeviceTestBase via TEST_GROUP_BASE
DavidCozens May 3, 2026
31aea6c
refactor(api): rename maxFileSize/maxFiles to maxBlockSize/maxBlocks
DavidCozens May 3, 2026
6cfbcff
refactor(blocksequence): rename IsReadingOlderBlock to ReadIsBehindWrite
DavidCozens May 3, 2026
6f60e1b
docs(claude.md): pin Design Patterns affirmed in the E18 hygiene pass
DavidCozens May 3, 2026
d17453e
refactor(blockdevice): split ScanForExistingBlocks; extract OpenHandl…
DavidCozens May 3, 2026
15c545b
refactor(api): rename SolidSyslogFileStore to SolidSyslogBlockStore
DavidCozens May 3, 2026
556b896
test: finish maxFiles->maxBlocks rename and harden BlockDevice/Sequen…
DavidCozens May 3, 2026
8b13d2f
fix(blockstore): bound resume scan by read block size, not WritePosition
DavidCozens May 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,4 @@ __pycache__/

# Claude Code local config
.claude/
pr*-comments.json
15 changes: 15 additions & 0 deletions Bdd/features/block_lifecycle.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
@tcp @buffered
Feature: Block lifecycle
The block store rotates blocks across files of fixed size. Block files are
disposed (deleted) only when capacity pressure forces rotation past the
oldest block; idle drains do not delete blocks. This baseline pins today's
semantics — S18.03 will introduce dispose-on-empty for non-active blocks.

Scenario: Block files are not disposed when capacity is not exhausted
Given syslog-ng is running
And the block store is enabled with max-blocks 4 and max-block-size 5000 and discard-policy oldest
And the threaded example is running with transport tcp and no structured data
And the set of existing block files is recorded
When the client sends 2 messages
Then syslog-ng receives 2 messages
And no recorded block file has been disposed
6 changes: 3 additions & 3 deletions Bdd/features/capacity_threshold.feature
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
@tcp @buffered
Feature: Capacity threshold alert
An early-warning callback fires when the file store crosses a configured
an early-warning callback fires when the block store crosses a configured
capacity threshold, before the terminal full-store callback engages.

Scenario: Threshold callback fires when usage crosses configured threshold
Given syslog-ng is running
And the file store is enabled with max-files 2 and max-file-size 520 and discard-policy oldest
And the block store is enabled with max-blocks 2 and max-block-size 520 and discard-policy oldest
And the capacity threshold callback is enabled at 200 bytes
And the threaded example is running with transport tcp and no structured data
When the syslog server stops accepting TCP connections
Expand All @@ -14,7 +14,7 @@ Feature: Capacity threshold alert

Scenario: Threshold callback does not fire while usage stays below threshold
Given syslog-ng is running
And the file store is enabled with max-files 2 and max-file-size 520 and discard-policy oldest
And the block store is enabled with max-blocks 2 and max-block-size 520 and discard-policy oldest
And the capacity threshold callback is enabled at 5000 bytes
And the threaded example is running with transport tcp and no structured data
When the client sends a message
Expand Down
6 changes: 3 additions & 3 deletions Bdd/features/power_cycle_replay.feature
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
@tcp @buffered
Feature: Power cycle replay from file store
After a power cycle, unsent messages persisted in the file store
Feature: Power cycle replay from block store
After a power cycle, unsent messages persisted in the block store
are replayed before new messages. The collector sees old-session
sequenceIds followed by a jump to 1 (restart signal).

Scenario: Stored messages replayed after power cycle
Given syslog-ng is running
And the file store is enabled
And the block store is enabled
And the threaded example is running with transport tcp
When the client sends a message
Then syslog-ng receives 1 message
Expand Down
43 changes: 27 additions & 16 deletions Bdd/features/steps/syslog_steps.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@

# Mirrors SOLIDSYSLOG_MAX_MESSAGE_SIZE from Core/Interface/SolidSyslog.h. Bump
# the two together. The store_capacity scenarios depend on it because production
# clamps max-file-size up to MAX + RECORD_OVERHEAD + integritySize at runtime,
# so the file size used by the file store is MAX-coupled even when the feature
# clamps max-block-size up to MAX + RECORD_OVERHEAD + integritySize at runtime,
# so the block size used by the block store is MAX-coupled even when the feature
# file specifies a smaller value.
SOLIDSYSLOG_MAX_MESSAGE_SIZE = 2048

Expand Down Expand Up @@ -450,10 +450,10 @@ def build_threaded_command(context, transport, no_sd=False):
cmd = [os.path.join(".", binary), "--transport", transport]
if getattr(context, "store_type", None):
cmd.extend(["--store", context.store_type])
if getattr(context, "store_max_files", None):
cmd.extend(["--max-files", str(context.store_max_files)])
if getattr(context, "store_max_file_size", None):
cmd.extend(["--max-file-size", str(context.store_max_file_size)])
if getattr(context, "store_max_blocks", None):
cmd.extend(["--max-blocks", str(context.store_max_blocks)])
if getattr(context, "store_max_block_size", None):
cmd.extend(["--max-block-size", str(context.store_max_block_size)])
if getattr(context, "store_discard_policy", None):
cmd.extend(["--discard-policy", context.store_discard_policy])
if getattr(context, "capacity_threshold", None):
Expand Down Expand Up @@ -492,24 +492,24 @@ def step_threaded_running_with_transport_no_sd(context, transport):
start_threaded_example(context, cmd)


@given("the file store is enabled")
def step_file_store_enabled(context):
@given("the block store is enabled")
def step_block_store_enabled(context):
context.store_type = "file"
if os.path.exists(STORE_FILE_PATH):
os.remove(STORE_FILE_PATH)


@given("the file store is enabled with max-files {max_files:d} and max-file-size {max_file_size:d} and discard-policy {policy}")
def step_file_store_enabled_with_config(context, max_files, max_file_size, policy):
@given("the block store is enabled with max-blocks {max_blocks:d} and max-block-size {max_block_size:d} and discard-policy {policy}")
def step_block_store_enabled_with_config(context, max_blocks, max_block_size, policy):
context.store_type = "file"
context.store_max_files = max_files
context.store_max_file_size = max_file_size
context.store_max_blocks = max_blocks
context.store_max_block_size = max_block_size
context.store_discard_policy = policy
# Size each MSG so ~4 records pack per (clamped) store file. The store
# Size each MSG so ~4 records pack per (clamped) block. The store
# capacity scenarios were designed around this packing — multi-record
# files give OLDEST and NEWEST symmetric retention (both keep 7 of 10
# sent), which the seqId assertions depend on. Production clamps file
# size up to MAX + 7 (MIN_MAX_FILE_SIZE), so per-record budget is
# blocks give OLDEST and NEWEST symmetric retention (both keep 7 of 10
# sent), which the seqId assertions depend on. Production clamps block
# size up to MAX + 7 (MIN_MAX_BLOCK_SIZE), so per-record budget is
# ~MAX/4. With ~95-byte RFC 5424 header + 7-byte record overhead, a
# body of MAX/5 - 50 lands a comfortable mid-band: 4 records fit, 5
# don't. Update if SOLIDSYSLOG_MAX_MESSAGE_SIZE moves.
Expand Down Expand Up @@ -548,6 +548,17 @@ def step_threshold_callback_not_invoked(context):
)


@given("the set of existing block files is recorded")
def step_record_block_files(context):
context.recorded_block_files = set(glob.glob(STORE_PATH_PREFIX + "*.log"))


@then("no recorded block file has been disposed")
def step_no_block_disposed(context):
missing = sorted(p for p in context.recorded_block_files if not os.path.exists(p))
assert not missing, f"Block files disposed unexpectedly: {missing}"


@when("the client sends a message")
def step_client_sends_message(context):
send_command(context.interactive_process, "send")
Expand Down
2 changes: 1 addition & 1 deletion Bdd/features/store_and_forward.feature
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Feature: Store and forward during sender outage

Scenario: Messages delivered after sender outage
Given syslog-ng is running
And the file store is enabled
And the block store is enabled
And the threaded example is running with transport tcp
When the client sends a message
Then syslog-ng receives 1 message
Expand Down
10 changes: 5 additions & 5 deletions Bdd/features/store_capacity.feature
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
@tcp @buffered
Feature: Store capacity limit and discard policy
The file store uses rotating files with a configurable capacity.
The block store uses rotating files with a configurable capacity.
When the store is full, the discard policy determines whether
the oldest or newest messages are dropped.

Scenario: Discard-oldest drops oldest messages when store overflows
Given syslog-ng is running
And the file store is enabled with max-files 2 and max-file-size 520 and discard-policy oldest
And the block store is enabled with max-blocks 2 and max-block-size 520 and discard-policy oldest
And the threaded example is running with transport tcp and no structured data
When the client sends a message
Then syslog-ng receives 1 message
Expand All @@ -18,7 +18,7 @@ Feature: Store capacity limit and discard policy

Scenario: Discard-newest preserves oldest messages when store overflows
Given syslog-ng is running
And the file store is enabled with max-files 2 and max-file-size 520 and discard-policy newest
And the block store is enabled with max-blocks 2 and max-block-size 520 and discard-policy newest
And the threaded example is running with transport tcp and no structured data
When the client sends a message
Then syslog-ng receives 1 message
Expand All @@ -30,7 +30,7 @@ Feature: Store capacity limit and discard policy

Scenario: Halt stops the application when store overflows
Given syslog-ng is running
And the file store is enabled with max-files 2 and max-file-size 520 and discard-policy halt
And the block store is enabled with max-blocks 2 and max-block-size 520 and discard-policy halt
And the halt callback exits the process
And the threaded example is running with transport tcp and no structured data
When the client sends a message
Expand All @@ -41,7 +41,7 @@ Feature: Store capacity limit and discard policy

Scenario: Halt prevents further service after store overflows
Given syslog-ng is running
And the file store is enabled with max-files 2 and max-file-size 520 and discard-policy halt
And the block store is enabled with max-blocks 2 and max-block-size 520 and discard-policy halt
And the threaded example is running with transport tcp and no structured data
When the client sends a message
Then syslog-ng receives 1 message
Expand Down
57 changes: 55 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -338,7 +338,10 @@ live under `Core/Interface/`; platform-specific helpers (the `SolidSyslogPosix*`
| `SolidSyslogNullStore.h` | System setup code (no store-and-forward) | `SolidSyslogNullStore_Create`, `_Destroy` |
| `SolidSyslogFileDefinition.h` | File implementors (extension point) | `SolidSyslogFile` vtable struct |
| `SolidSyslogFile.h` | Any code using the file abstraction | `SolidSyslogFile_Open`, `_Close`, `_IsOpen`, `_Read`, `_Write`, `_SeekTo`, `_Size`, `_Truncate` |
| `SolidSyslogFileStore.h` | System setup code using file-based store | `SolidSyslogFileStoreStorage`, `SOLIDSYSLOG_FILESTORE_STORAGE_SIZE`, `SolidSyslogFileStoreConfig` (with `storeFullContext`, `getCapacityThreshold`, `onThresholdCrossed`, `thresholdContext`), `SolidSyslogFileStore_Create(storage, config)`, `_Destroy(store)`, `SolidSyslogDiscardPolicy` (`SOLIDSYSLOG_DISCARD_OLDEST` / `_NEWEST` / `_HALT`), `SolidSyslogStoreFullCallback(void* context)`, `SolidSyslogStoreThresholdFunction(void* context)` (returns threshold in bytes; 0 disables; queried each Write), `SolidSyslogStoreThresholdCallback(void* context)` (edge-triggered; NullBuffer recursion gotcha — see header) |
| `SolidSyslogBlockDevice.h` | Library internals consuming a block device (BlockSequence inside BlockStore) and integrator code addressing blocks directly | `SolidSyslogBlockDevice_Acquire`, `_Dispose`, `_Exists`, `_Read`, `_Append`, `_WriteAt`, `_Size` (block-indexed I/O; Acquire makes a block ready for fresh writes, Dispose releases it) |
| `SolidSyslogBlockDeviceDefinition.h` | BlockDevice implementors (extension point) | `SolidSyslogBlockDevice` vtable struct (`Acquire`, `Dispose`, `Exists`, `Read`, `Append`, `WriteAt`, `Size`) |
| `SolidSyslogFileBlockDevice.h` | System setup code wiring a file-backed block device | `SolidSyslogFileBlockDeviceStorage`, `SOLIDSYSLOG_FILEBLOCKDEVICE_STORAGE_SIZE`, `SolidSyslogFileBlockDevice_Create(storage, readFile, writeFile, pathPrefix)` (sequence-numbered filenames `<prefix><NN>.log`, two cached file handles), `_Destroy(device)` |
| `SolidSyslogBlockStore.h` | System setup code using a BlockDevice-backed store | `SolidSyslogBlockStoreStorage`, `SOLIDSYSLOG_BLOCKSTORE_STORAGE_SIZE`, `SolidSyslogBlockStoreConfig` (with `blockDevice`, `storeFullContext`, `getCapacityThreshold`, `onThresholdCrossed`, `thresholdContext`), `SolidSyslogBlockStore_Create(storage, config)`, `_Destroy(store)`, `SolidSyslogDiscardPolicy` (`SOLIDSYSLOG_DISCARD_OLDEST` / `_NEWEST` / `_HALT`), `SolidSyslogStoreFullCallback(void* context)`, `SolidSyslogStoreThresholdFunction(void* context)` (returns threshold in bytes; 0 disables; queried each Write), `SolidSyslogStoreThresholdCallback(void* context)` (edge-triggered; NullBuffer recursion gotcha — see header) |
| `SolidSyslogSecurityPolicyDefinition.h` | SecurityPolicy implementors (extension point) | `SolidSyslogSecurityPolicy` vtable struct, `SOLIDSYSLOG_MAX_INTEGRITY_SIZE` |
| `SolidSyslogNullSecurityPolicy.h` | System setup code (no integrity checking) | `SolidSyslogNullSecurityPolicy_Create`, `_Destroy` |
| `SolidSyslogCrc16Policy.h` | System setup code using CRC-16 integrity | `SolidSyslogCrc16Policy_Create`, `_Destroy` |
Expand Down Expand Up @@ -396,6 +399,56 @@ Names should be self-documenting — prefer clarity over brevity.

---

## Design Patterns

These patterns are re-affirmed each time we do a code-hygiene pass. New
code should follow them; reviewers should call out drift.

### Production code (Tier 1, `Core/Source/`)

- **Single return per function.** MISRA-leaning. If the natural shape has an
early return, restructure with a result local and an `if` wrapper. See
`BlockSequence.c::ScanForExistingBlocks` for the pattern.
- **Intent-naming static-inline predicates.** When a composite condition is
inlined into an `if` or a `return`, extract a `static inline bool IsXxx(...)`
helper. The helper's *name* is the documentation. Examples:
`IsAboveThreshold`, `IsHandleAlreadyOpenOnBlock`, `IsValidBlockIndex`,
`BlockIsFull`, `StoreIsFull`. The cost (one extra named function) is the
benefit (the reader doesn't have to decode the boolean).
- **One thing at one level of abstraction.** Functions read top-down.
`_Create` first, `_Destroy` second, public functions in API order, helpers
defined immediately beneath their first caller. See **Function Ordering**
below.
- **Bracket compound boolean conditions when mixing `||` with arithmetic /
comparison operators.** Plain `&&` between bool-typed operands needs no extra
parens — readability wins over MISRA pedantry there.
- **No null-pointer checks where the type's null object exists.** Use
`SolidSyslogNullSecurityPolicy`, `SolidSyslogNullStore`, `SolidSyslogNullBuffer`
rather than `if (policy != NULL) policy->Compute(...)`.

### Test code

- **TEST_BASE / TEST_GROUP_BASE for shared fixture.** When multiple TEST_GROUPs
in one file declare the same storage / file / device variables and the same
setup/teardown, lift the boilerplate into a `TEST_BASE` and derive each group
via `TEST_GROUP_BASE`. Test bodies still reference fixture members by their
bare names — they're inherited. See `BlockDeviceTestBase` in
`SolidSyslogBlockStoreTest.cpp`.
- **`CHECK_*` macros for repeated assertion shapes.** When the same buf+memcmp
or several-line assertion repeats across tests, wrap it in a macro that
*names* the intent. The macro must be a macro (not a function) so test
failures report the caller's `__FILE__`/`__LINE__`. Wrap with
`// NOLINTBEGIN(cppcoreguidelines-macro-usage,cppcoreguidelines-avoid-do-while)`
and use `do { ... } while (0)` for safe single-statement use. Examples:
`CHECK_PRIVAL` in `SolidSyslogTest.cpp`, `CHECK_BLOCK_CONTAINS` in
`SolidSyslogFileBlockDeviceTest.cpp`.
- **DRY the setup, DRY the asserts, keep the test body small.** Each `TEST(...)`
body should read as a sentence: arrange → act → assert. If three lines of
setup repeat in five tests, the setup belongs in `setup()` or a TEST_GROUP
helper; if the same assertion shape repeats, make a `CHECK_*` macro.

---

## Callback Conventions

The library is migrating away from singleton instances toward integrator-injected storage and
Expand Down Expand Up @@ -423,7 +476,7 @@ next touched.
- Mirrors the `SolidSyslogFormatterStorage` pattern. The public header exposes an opaque storage
type and a `SOLIDSYSLOG_<TYPE>_STORAGE_SIZE` macro; `_Create` takes a pointer to caller-allocated
storage of that size. No `malloc` anywhere in the library.
- Internal sub-components (e.g. RecordStore and BlockSequence inside FileStore) nest inside the
- Internal sub-components (e.g. RecordStore and BlockSequence inside BlockStore) nest inside the
parent's storage blob. Their types stay in `Core/Source/` and never appear in public headers,
so integrator and example code physically cannot reach them.

Expand Down
27 changes: 27 additions & 0 deletions Core/Interface/SolidSyslogBlockDevice.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#ifndef SOLIDSYSLOGBLOCKDEVICE_H
#define SOLIDSYSLOGBLOCKDEVICE_H

#include "ExternC.h"
#include <stdbool.h>
#include <stddef.h>

EXTERN_C_BEGIN

struct SolidSyslogBlockDevice;

/* Acquire makes a block ready for fresh writes. The contract requires the block to be empty
* after the call (filesystem-backed drivers create-or-truncate; flash drivers erase). Must
* not be called on a block whose existing content the integrator wants to keep — use Exists
* + Size to detect and resume from a populated block instead.
*/
bool SolidSyslogBlockDevice_Acquire(struct SolidSyslogBlockDevice * device, size_t blockIndex);
bool SolidSyslogBlockDevice_Dispose(struct SolidSyslogBlockDevice * device, size_t blockIndex);
bool SolidSyslogBlockDevice_Exists(struct SolidSyslogBlockDevice * device, size_t blockIndex);
bool SolidSyslogBlockDevice_Read(struct SolidSyslogBlockDevice * device, size_t blockIndex, size_t offset, void* buf, size_t count);
bool SolidSyslogBlockDevice_Append(struct SolidSyslogBlockDevice * device, size_t blockIndex, const void* buf, size_t count);
bool SolidSyslogBlockDevice_WriteAt(struct SolidSyslogBlockDevice * device, size_t blockIndex, size_t offset, const void* buf, size_t count);
size_t SolidSyslogBlockDevice_Size(struct SolidSyslogBlockDevice * device, size_t blockIndex);

EXTERN_C_END

#endif /* SOLIDSYSLOGBLOCKDEVICE_H */
21 changes: 21 additions & 0 deletions Core/Interface/SolidSyslogBlockDeviceDefinition.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#ifndef SOLIDSYSLOGBLOCKDEVICEDEFINITION_H
#define SOLIDSYSLOGBLOCKDEVICEDEFINITION_H

#include "SolidSyslogBlockDevice.h"

EXTERN_C_BEGIN

struct SolidSyslogBlockDevice
{
bool (*Acquire)(struct SolidSyslogBlockDevice* self, size_t blockIndex);
bool (*Dispose)(struct SolidSyslogBlockDevice* self, size_t blockIndex);
bool (*Exists)(struct SolidSyslogBlockDevice* self, size_t blockIndex);
bool (*Read)(struct SolidSyslogBlockDevice* self, size_t blockIndex, size_t offset, void* buf, size_t count);
bool (*Append)(struct SolidSyslogBlockDevice* self, size_t blockIndex, const void* buf, size_t count);
bool (*WriteAt)(struct SolidSyslogBlockDevice* self, size_t blockIndex, size_t offset, const void* buf, size_t count);
size_t (*Size)(struct SolidSyslogBlockDevice* self, size_t blockIndex);
};

EXTERN_C_END

#endif /* SOLIDSYSLOGBLOCKDEVICEDEFINITION_H */
Loading
Loading