Skip to content

feat: S12.14 decouple buffer drain from sender state — fail-fast streams + eager Service drain#283

Merged
DavidCozens merged 16 commits into
mainfrom
feat/s12-14-decouple-buffer-drain-from-sender-state
May 7, 2026
Merged

feat: S12.14 decouple buffer drain from sender state — fail-fast streams + eager Service drain#283
DavidCozens merged 16 commits into
mainfrom
feat/s12-14-decouple-buffer-drain-from-sender-state

Conversation

@DavidCozens

@DavidCozens DavidCozens commented May 7, 2026

Copy link
Copy Markdown
Owner

Summary

  • Eager Service drainProcessMessages now drains Buffer→Store in a loop, then attempts one Send from the Store. Bursts no longer drop at the in-memory buffer; the Store's discard policy can engage.
  • Posix TCP, Winsock TCP, TLS streams: non-blocking + fail-fast — sockets stay non-blocking through the lifecycle. Send returns true only when the kernel accepts the entire frame in one call; any failure (short write, would-block, error) closes the socket internally and returns false. Read returns 0 on would-block, -1 with internal close on EOF/error. TLS handshake is driven in a bounded retry loop (5 s budget, 1 ms poll).
  • Stream contract documented in Core/Interface/SolidSyslogStream.h. Caller responsibility on a false Send / -1 Read: call Open before the next Send.

Closes #276

Why this matters

ProcessMessages previously did one buffer→store write + one send per service tick. When a TCP/TLS sender stalled (slow SIEM, refused loopback retry, full kernel send buffer), the buffer→store pump stalled too. Producers' SolidSyslog_Log calls then filled the in-memory buffer, which silently drops on full — the BlockStore's discard policy never saw the records, so an outage couldn't engage discard-oldest / discard-newest / halt.

Discovered via the four @windows_wip store_capacity.feature scenarios on Windows in PR #275.

Slice-by-slice

Slice Scope Linux gates MSVC
1 Eager ProcessMessages drain + StoreFake FIFO + new burst-drain tests green n/a (Linux-only fakes)
2 PosixTcpStream non-blocking + fail-fast — fcntl O_NONBLOCK, bounded select() connect, getsockopt(SO_ERROR), fail-fast Send/Read; SocketFake gains fcntl/select/SO_ERROR seams green not applicable
3 WinsockTcpStream Send/Read non-blocking + fail-fast — stop restoring blocking after connect, drop SO_SNDTIMEO; WinsockFake gains FailNextRecvWithLastError green 956 tests
4 TlsStream non-blocking + fail-fast — BIO read translates would-block to BIO_set_retry_read, bounded SSL_connect retry loop, fail-fast SSL_write/read, idempotent Close; OpenSslFake gains return-sequence injection, SSL_get_error, BIO retry-flag spies green 973 tests

Coverage 100% (2024/2024 lines, 429/429 functions). Format / IWYU / sanitize / tidy / cppcheck / clang-debug all green.

Disagreements held during design review

  • Connect timeout: 200 ms vs 5–10 s. Pushed back on widening — 200 ms is by design to keep the BlockStore service-thread drain rate fast during outages so the discard policy actually fires. Filed as a follow-up: tunable connect/handshake timeouts for WAN deployments.
  • TLS handshake budget: 5 s. Different concern (multi-RTT handshake), accepted the wider budget.

Deferred (follow-up stories being filed in parallel)

  • TCP keepalive (SO_KEEPALIVE + TCP_KEEPIDLE/INTVL/CNT + SIO_KEEPALIVE_VALS + TCP_USER_TIMEOUT) for dead-peer idle detection.
  • TLS session resumption to cut handshake cost on fail-fast reconnects.
  • Tunable connect/handshake timeouts for WAN deployments.

Out of scope intentionally

The four @windows_wip tags in Bdd/features/store_capacity.feature are not removed in this PR. Per #276, the tag removal and BDD re-tuning are the finale of #275. This PR delivers the architectural fix that unblocks them.

Test plan

  • CI: build-linux-gcc, build-linux-clang, sanitize-linux-gcc, coverage-linux-gcc, analyze-tidy, analyze-cppcheck, analyze-format, analyze-iwyu, build-windows-msvc, integration-linux-openssl, integration-windows-openssl, bdd-linux-syslog-ng, bdd-windows-otel
  • All existing BDD scenarios remain green (no behaviour regression)
  • Local: scenario 1 of store_capacity.feature reaches "the syslog oracle receives 1 message" against the Windows MSVC build + otelcol-contrib (architectural decoupling confirmed)

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • New Features

    • Non-blocking TCP socket support with improved failure handling and automatic cleanup
    • TLS handshake retry mechanism with bounded timeout protection
    • Cross-platform sleep support for handshake polling
  • Bug Fixes

    • Improved socket closure behavior and idempotent stream close operations
    • Enhanced error handling in connection establishment and I/O operations
    • Better message buffer processing with fail-fast semantics
  • Documentation

    • Updated API documentation for non-blocking stream behavior and failure cases

… state

ProcessMessages now drains the buffer eagerly into the store and then attempts
one send from the store. When a write does not land in the store (NullStore, or
store-write rejection), it falls back to a best-effort direct send to preserve
the existing single-task / no-store-and-forward configurations.

StoreFake promoted to FIFO (8 slots) with a write counter for the burst-drain
assertion. New Service tests use the production CircularBuffer + NullMutex.
Stream becomes fully non-blocking from socket creation. connect() returns
EINPROGRESS and the wait is bounded via select() to 200 ms (mirrors the
Winsock pattern); SO_ERROR distinguishes completed-success from deferred-
failure. Send is single-call, fail-fast, with internal close on any failure
(short write, EAGAIN, errno). Read returns 0 on would-block and -1 with
internal close on EOF/error. SolidSyslogStream.h documents the new contract.

Drops SO_SNDTIMEO (no-op on non-blocking sockets) and the EINTR retry loop.
SocketFake gains fcntl, select, connect-with-errno, recv-with-errno, and
getsockopt(SO_ERROR) seams to drive the new code paths under test.
The socket is now non-blocking from creation through the full lifecycle —
no longer flips back to blocking after a successful connect. Send returns
true only when the kernel accepts the entire frame in one call; any failure
(short write, WSAEWOULDBLOCK, error) closes the socket internally and
returns false. Read returns 0 on WSAEWOULDBLOCK and -1 with internal close
on EOF (recv == 0) or other errors.

Drops SO_SNDTIMEO (no-op on non-blocking sockets). WinsockFake gains
FailNextRecvWithLastError so tests can drive the would-block / error /
EOF Read paths.

Validated locally on MSVC: 956 tests pass, including the 50 Winsock TCP
stream tests that exercise the new non-blocking Send / Read contract.
Five coordinated changes so TLS works on top of the now-non-blocking
PosixTcp/WinsockTcp streams:

- BIO read translates would-block (0) into BIO_set_retry_read + return -1,
  and clears retry on EOF/error. Without this, OpenSSL would treat the
  transport's would-block as EOF and abort the handshake on first poll.
- BIO write clears retry flags on transport-Send failure so SSL_write
  surfaces SSL_ERROR_SYSCALL rather than spinning on a closed transport.
- PerformHandshake drives SSL_connect in a bounded retry loop with a 5 s
  budget and a 1 ms poll interval — sleeps on WANT_READ / WANT_WRITE,
  fails fast on any hard SSL error.
- Send is single-call, fail-fast: SSL_write != size closes the SSL session
  and the underlying transport so the StreamSender's reconnect path picks
  up on the next service tick.
- Read returns 0 on WANT_READ (mirrors transport contract); anything else
  (errors, peer close, mid-stream renegotiation needing WANT_WRITE) takes
  the close + -1 path. Comment in TlsStream_Read records the handshake-
  vs-steady-state distinction explicitly.
- TlsStream_Close is now idempotent so internal-close-from-Send/Read
  followed by an explicit Close from the StreamSender doesn't double-free.

OpenSslFake gains SSL_connect return-sequence injection, SSL_get_error,
SSL_write/read return overrides, and BIO_set_flags / BIO_clear_flags
spies so the new code paths are covered without touching the real OpenSSL
during unit tests. TlsStream_sleep is a function-pointer seam (Windows
Sleep / POSIX nanosleep by default; tests UT_PTR_SET to a no-op).

Coverage 100%. Validated locally on MSVC: 973 tests pass.
@coderabbitai

coderabbitai Bot commented May 7, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Rate limit exceeded

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

To continue reviewing without waiting, purchase usage credits in the billing tab.

⌛ 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: 1895e8d6-810c-463a-8424-6399cb11bbe3

📥 Commits

Reviewing files that changed from the base of the PR and between 8ee9da3 and d8dbe07.

📒 Files selected for processing (1)
  • Tests/SolidSyslogTlsStreamTest.cpp
📝 Walkthrough

Walkthrough

This PR implements S12.14, which decouples buffer drain from sender state via two coordinated changes: streams become non-blocking/fail-fast (closing on any I/O failure), and ProcessMessages switches to eager buffer drain (pump all buffer→store, then attempt one store send). This fixes a critical bug where a slow sender would stall the buffer→store pump, causing producer buffer drops before the store (with its overflow policy) could engage. The change introduces a sleep abstraction for bounded TLS handshake retries and significantly expands test infrastructure (SocketFake, OpenSslFake, StoreFake, WinsockFake) to validate non-blocking contracts across all three stream implementations.

Changes

Non-blocking Stream Refactor & Eager Buffer Drain

Layer / File(s) Summary
API Contract Clarification
Core/Interface/SolidSyslogStream.h
Stream function signatures unchanged; documentation expanded to specify non-blocking fail-fast semantics: Send returns true only on full frame acceptance (else closes socket internally); Read returns positive/zero/−1 for success/would-block/error; Close is idempotent.
Service Message Pump
Core/Source/SolidSyslog.c
ProcessMessages refactored from one-at-a-time to two-phase eager drain: drain Buffer→Store until empty (with direct-send bypass for non-retained messages), then attempt one Send from store, marking sent only on success.
NullStore Semantics
Core/Source/SolidSyslogNullStore.c
Write changed to always return false, signaling non-retention and triggering direct-send in ProcessMessages loop.
Sleep Callback Interface
Core/Interface/SolidSyslogSleep.h
New cross-platform function pointer typedef SolidSyslogSleepFunction(int milliseconds) for handshake retry pacing abstraction.
POSIX TCP Non-blocking
Platform/Posix/Source/SolidSyslogPosixTcpStream.c
Socket creation sets O_NONBLOCK via fcntl and TCP_NODELAY; Connect uses non-blocking socket with bounded select()-based completion wait and deferred SO_ERROR verification; Send performs single non-blocking call, closes on any failure; Read returns 0 on EAGAIN/EWOULDBLOCK, −1 on error/EOF with close.
POSIX Sleep Implementation
Platform/Posix/Interface/SolidSyslogPosixSleep.h, Platform/Posix/Source/SolidSyslogPosixSleep.c
Implements SolidSyslogPosixSleep(int ms) converting milliseconds to nanosleep timespec.
Windows Winsock Non-blocking
Platform/Windows/Source/SolidSyslogWinsockTcpStream.c
Socket configuration sets FIONBIO non-blocking and TCP_NODELAY; Connect uses bounded select() for WSAEWOULDBLOCK waits with SO_ERROR deferred-completion validation; Send closes on partial write or error; Read returns 0 on WSAEWOULDBLOCK, −1 on EOF/error with close.
Windows Sleep Implementation
Platform/Windows/Interface/SolidSyslogWindowsSleep.h, Platform/Windows/Source/SolidSyslogWindowsSleep.c
Implements SolidSyslogWindowsSleep(int ms) wrapping WinAPI Sleep(DWORD).
TLS Stream Non-blocking & Retry
Platform/OpenSsl/Source/SolidSyslogTlsStream.c
BIO transport callbacks now handle non-blocking contract (would-block→BIO_set_retry, error→clear flags); SSL_connect retry loop bounded by sleep-based timeout budget and retries on WANT_READ/WANT_WRITE; Send verifies full write, closes on error; Read returns 0 on WANT_READ, −1 on error with close; Close idempotent.
TLS Stream Config & Size
Platform/OpenSsl/Interface/SolidSyslogTlsStream.h
SolidSyslogTlsStreamConfig gains required sleep callback field; storage size constant increased from ×13 to ×14 intptr_t.
Example Integration
Example/Common/ExampleTlsSender_OpenSsl_PosixTcp.c, Example/Common/ExampleTlsSender_OpenSsl_WinsockTcp.c
TLS stream configuration now wires platform-specific sleep callback (SolidSyslogPosixSleep / SolidSyslogWindowsSleep).
StreamFake Extensibility
Tests/StreamFake.c, Tests/StreamFake.h
New sendFails flag and StreamFake_SetSendFails() API to simulate stream send failures.
SocketFake Non-blocking Modeling
Tests/Support/SocketFake.c, Tests/Support/SocketFake.h
Expanded to simulate non-blocking TCP: SO_ERROR lookup; F_SETFL/F_GETFL with non-blocking flag tracking; select() with timeout recording and readiness control; connect/recv with configurable errno failures.
WinsockFake Error Injection
Tests/Support/WinsockFake.c, Tests/Support/WinsockFake.h
New WinsockFake_FailNextRecvWithLastError(wsaError) to inject recv failure with WSA error code.
OpenSslFake Return Sequencing
Tests/Support/OpenSslFake.c, Tests/Support/OpenSslFake.h
Extended with SSL_connect return sequences, SSL_write/read/get_error overrides, and BIO flag operation tracking for deterministic test scenarios.
StoreFake Queue Refactor
Tests/StoreFake.c, Tests/StoreFake.h
Converted from single-buffer to bounded FIFO queue; Write enqueues and increments counter; ReadNextUnsent/MarkSent/HasUnsent operate on queue head; new StoreFake_WriteCount() accessor.
POSIX TCP Tests
Tests/SolidSyslogPosixTcpStreamTest.cpp
Tests for non-blocking Open (fcntl verification), Send fail-fast semantics (EINTR/EAGAIN close socket), bounded non-blocking connect (select timeout, SO_ERROR validation, deferred failure modes), and non-blocking Read (EAGAIN→0, error→−1).
Windows Winsock Tests
Tests/SolidSyslogWinsockTcpStreamTest.cpp
Tests for non-blocking Open (FIONBIO), Send failure closing socket, and Read non-blocking semantics (WSAEWOULDBLOCK→0, EOF/error→−1).
TLS Stream Tests
Tests/SolidSyslogTlsStreamTest.cpp
Extensive new tests for sleep injection/pacing, BIO would-block/error handling, bounded handshake retry (call counting, sleep verification), send/read failure semantics, and close idempotency.
Storage & Service Tests
Tests/StoreFakeTest.cpp, Tests/SolidSyslogTest.cpp, Tests/SolidSyslogNullStoreTest.cpp, Tests/SolidSyslogStreamSenderTest.cpp
StoreFake queue tests, service eager-drain tests validating all-buffered-to-store and FIFO order, NullStore non-retention contract, and updated socket option counts.
Sleep Tests & Integration
Tests/SolidSyslogPosixSleepTest.cpp, Tests/SolidSyslogWindowsSleepTest.cpp, Tests/OpenSslIntegration/SolidSyslogTlsStreamIntegrationTest.cpp
New platform-specific sleep tests (zero-sleep no-crash), integration test with NoOpSleep.
Build Configuration
Tests/CMakeLists.txt, Platform/Posix/CMakeLists.txt, Platform/Windows/CMakeLists.txt
Sleep tests and implementations added to platform-specific build rules.
Documentation
CLAUDE.md, DEVLOG.md
Interface segregation table updated for sleep functions; DEVLOG entry documents S12.14 design decisions, timeout constraints, deferred issues, and validation outcomes.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

  • DavidCozens/solid-syslog#278: S13.21 BDD store_capacity scenarios depend on this PR's buffer-drain refactor and non-blocking streams to exercise store overflow instead of silent buffer drops.

Possibly related PRs

Poem

🐰 Streams now fail gracefully and fast,
No more blocking on the sender's task,
Buffer drains eager to the store's deep keep,
While TLS handshakes retry with naps so sweet!
Non-blocking contracts, crystal clear—
The syslog flows, without a fear. 🌙

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 23.30% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The PR title 'feat: S12.14 decouple buffer drain from sender state — fail-fast streams + eager Service drain' follows Conventional Commits, is concise, and accurately summarizes the main architectural changes: eager buffer drain, fail-fast streams, and service-level improvements.
Description check ✅ Passed The PR description is comprehensive and well-structured. It includes Purpose (linked to #276), detailed Change Description explaining the three main improvements, Test Evidence through documented slice testing and coverage metrics, and Areas Affected across multiple stream implementations.
Linked Issues check ✅ Passed The PR fully addresses #276 requirements: eager buffer drain decoupling prevents the in-memory buffer from silently dropping messages when the sender stalls, non-blocking fail-fast streams prevent send stalls from blocking the pump, and the implementation preserves API signatures while documenting the stream contract.
Out of Scope Changes check ✅ Passed All changes are scoped to the stated objectives: buffer/store/stream implementations, test infrastructure for non-blocking semantics (StoreFake FIFO, SocketFake select/fcntl/getsockopt, OpenSslFake return sequences), platform-specific sleep callbacks, and supporting documentation (DEVLOG, CLAUDE). No unrelated refactoring or feature creep detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/s12-14-decouple-buffer-drain-from-sender-state

@github-actions

github-actions Bot commented May 7, 2026

Copy link
Copy Markdown
Contributor

☀️   Quality Summary

   🚦   build-linux-gcc: 100% successful (✔️ 1072 passed, 🙈 3 skipped)
   🚦   build-linux-clang: 100% successful (✔️ 1019 passed, 🙈 3 skipped)
   🚦   sanitize-linux-gcc: 100% successful (✔️ 1019 passed, 🙈 3 skipped)
   🚦   integration-linux-openssl: 100% successful (✔️ 9 passed)
   🚦   integration-windows-openssl: 100% successful (✔️ 9 passed)
   🚦   bdd-linux-syslog-ng: 100% successful (✔️ 46 passed)
   🚦   bdd-windows-otel: 87% successful (✔️ 40 passed, 🙈 6 skipped)
   🚦   build-windows-msvc: 100% successful (✔️ 930 passed, 🙈 1 skipped)
   ⚠️   Clang-Tidy: 1 warning (high: 1)
   ⚠️   CPPCheck: No warnings


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

POSIX fcntl(int fd, int cmd, ...) has two adjacent int parameters by
contract; we cannot change the signature in the link-substituted fake.
NOLINTNEXTLINE on the existing readability-inconsistent suppression.

@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: 3

🧹 Nitpick comments (3)
Tests/StoreFake.c (1)

76-86: 💤 Low value

LGTM — minor note on silent-drop semantics when queue is full.

When count >= STOREFAKE_MAX_MESSAGES, Write() drops the payload silently but still increments writeCount and returns true. This means future callers of HasUnsent() will not see the overflow message, and StoreDidNotRetainLastWrite() (in the current production code — see separate comment) would incorrectly conclude the write was retained. Since this is a test fake with a queue depth of 8, it will not affect current tests, but the semantics diverge from a real store.

No immediate action required; just worth noting if the fake is ever stress-tested with > 8 in-flight messages.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Tests/StoreFake.c` around lines 76 - 86, The fake store's Write() currently
drops payloads when fake->count >= STOREFAKE_MAX_MESSAGES but still increments
fake->writeCount and returns true, diverging from real-store semantics; change
Write() (the function handling fake->count, fake->writeCount and memcpy into
fake->entries[]) so that when the queue is full it does not increment
fake->writeCount and returns false (or otherwise signals failure), ensuring
HasUnsent() and tests like StoreDidNotRetainLastWrite() observe the dropped
write consistently; update any callers/tests expecting the previous
silent-accept behavior if needed.
Tests/SolidSyslogTest.cpp (1)

1476-1531: ⚡ Quick win

Extract shared CircularBuffer + StoreFake + NullMutex fixture into a TEST_GROUP_BASE.

Both new tests duplicate the same setup/teardown sequence (create circular buffer, create store, configure service, restore original config, destroy all). Per coding guidelines, TEST_BASE / TEST_GROUP_BASE should be used when multiple TEST_GROUPs (or multiple tests within a group) declare the same storage/device variables and setup/teardown.

As per coding guidelines: "Use TEST_BASE / TEST_GROUP_BASE for shared fixture when multiple TEST_GROUPs declare same storage/file/device variables and setup/teardown."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Tests/SolidSyslogTest.cpp` around lines 1476 - 1531, Both tests duplicate
circular buffer + store + null mutex setup/teardown; extract that shared fixture
into a TEST_GROUP_BASE and have the two tests use it: move the static
bufferStorage, circularBuffer (created via SolidSyslogCircularBuffer_Create),
fakeStore (StoreFake_Create), and null mutex (SolidSyslogNullMutex_Create) plus
serviceConfig/SolidSyslog_Create call into the TEST_GROUP_BASE setup, and move
SolidSyslog_Destroy(), SolidSyslog_Create(&config), StoreFake_Destroy(),
SolidSyslogCircularBuffer_Destroy() and SolidSyslogNullMutex_Destroy() into the
TEST_GROUP_BASE teardown; update the tests
ServiceEagerlyDrainsBufferIntoStoreInOneTick and
ServiceSendsStoredMessagesInFifoOrderAcrossTicks to remove their duplicated
setup/teardown and rely on the base fixture while keeping their specific
SenderFake calls and assertions.
Tests/Support/SocketFake.h (1)

94-100: 💤 Low value

exceptfds wording may mislead: non-blocking connect errors appear in writefds, not only exceptfds.

The comment says hasError=true sets fd in exceptfds, but on Linux (and most POSIX systems) a failed non-blocking connect sets the fd as writable (in writefds) and the actual error is retrieved via getsockopt(SO_ERROR). exceptfds is for out-of-band data. If the production connect-completion path only checks writefds (which is the correct approach), a test using SetSelectError(true) without SetSelectWritable(true) would not exercise the right code path. Consider clarifying whether this simulates:

  • a select() syscall failure (returns -1), or
  • a writable-but-errored fd (fd set in writefds, SO_ERROR ≠ 0).
📝 Suggested comment clarification
-    /* select configuration. ready=true makes select report fd writable in writefds;
-       ready=false leaves writefds empty (timeout). hasError=true sets fd in
-       exceptfds. returnValue overrides the int return (1 ready, 0 timeout, -1 error).
-       Default: returns 1 (one fd ready: writable, no error). */
+    /* select configuration. ready=true makes select report fd writable in writefds;
+       ready=false leaves writefds empty (timeout). hasError=true sets the fd in
+       exceptfds (out-of-band / error indicator); to model a failed non-blocking
+       connect, combine SetSelectWritable(true) + SetSoError(ECONNREFUSED) instead.
+       returnValue overrides the int return (1 ready, 0 timeout, -1 error).
+       Default: returns 1 (one fd ready: writable, no error). */
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Tests/Support/SocketFake.h` around lines 94 - 100, The comment describing
select simulation is misleading: SocketFake_SetSelectError(bool hasError) should
not imply errors are signalled via exceptfds only; on POSIX a non-blocking
connect failure is indicated by the fd being writable (writefds) with SO_ERROR
set. Update the comment to state the two distinct simulations supported (a
select() failure via SocketFake_SetSelectReturn(-1) vs. a
connect-completion-with-error simulated by setting writefds and a non-zero
SO_ERROR when SocketFake_SetSelectError(true)), and document that
SocketFake_SetSelectWritable(true) models the fd appearing in writefds and that
the test harness must call both SetSelectWritable(true) and SetSelectError(true)
to simulate a writable-but-errored socket (getsockopt(SO_ERROR) non-zero).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Core/Interface/SolidSyslogStream.h`:
- Around line 31-36: The doc for SolidSyslogStream_Read is incomplete: when it
returns < 0 it closes the underlying socket internally and the caller must call
Open before attempting either the next Send or the next Read; update the comment
above SolidSyslogStream_Read to mention that the caller must call Open before
the next Read as well as before the next Send (referencing
SolidSyslogStream_Read, Open, and Send to locate the text to change).

In `@Core/Source/SolidSyslog.c`:
- Around line 137-156: DrainBufferIntoStore currently decides to fallback to
direct send using StoreDidNotRetainLastWrite(), which checks
SolidSyslogStore_HasUnsent() and can hide a failed write when the store already
had unsent messages; instead, capture and use the return value of
SolidSyslogStore_Write(...) directly (e.g. bool wrote =
SolidSyslogStore_Write(...)) and call SolidSyslogSender_Send(...) when wrote is
false to implement the intended "write rejected -> best-effort direct send"
behavior; also remove the now-unused StoreDidNotRetainLastWrite helper (and its
forward declaration) from the translation unit.

In `@Tests/SolidSyslogTest.cpp`:
- Around line 1478-1480: The test allocates SolidSyslogCircularBufferStorage
bufferStorage[...] on the stack which can be freed if a test assertion fails and
teardown runs (leading to use-after-stack when SolidSyslog_Destroy accesses
circularBuffer); change the declaration to static so the storage has program
lifetime. Update both occurrences where SolidSyslogCircularBufferStorage
bufferStorage[...] and the corresponding SolidSyslogCircularBuffer_Create call
(symbols: SolidSyslogCircularBufferStorage, bufferStorage,
SOLIDSYSLOG_CIRCULARBUFFER_STORAGE_SIZE_BYTES, SolidSyslogCircularBuffer_Create,
circularBuffer) are used so teardown/SolidSyslog_Destroy sees valid storage even
after stack unwinding. Ensure no other initialization changes are needed because
SolidSyslogCircularBuffer_Create re-initializes the storage.

---

Nitpick comments:
In `@Tests/SolidSyslogTest.cpp`:
- Around line 1476-1531: Both tests duplicate circular buffer + store + null
mutex setup/teardown; extract that shared fixture into a TEST_GROUP_BASE and
have the two tests use it: move the static bufferStorage, circularBuffer
(created via SolidSyslogCircularBuffer_Create), fakeStore (StoreFake_Create),
and null mutex (SolidSyslogNullMutex_Create) plus
serviceConfig/SolidSyslog_Create call into the TEST_GROUP_BASE setup, and move
SolidSyslog_Destroy(), SolidSyslog_Create(&config), StoreFake_Destroy(),
SolidSyslogCircularBuffer_Destroy() and SolidSyslogNullMutex_Destroy() into the
TEST_GROUP_BASE teardown; update the tests
ServiceEagerlyDrainsBufferIntoStoreInOneTick and
ServiceSendsStoredMessagesInFifoOrderAcrossTicks to remove their duplicated
setup/teardown and rely on the base fixture while keeping their specific
SenderFake calls and assertions.

In `@Tests/StoreFake.c`:
- Around line 76-86: The fake store's Write() currently drops payloads when
fake->count >= STOREFAKE_MAX_MESSAGES but still increments fake->writeCount and
returns true, diverging from real-store semantics; change Write() (the function
handling fake->count, fake->writeCount and memcpy into fake->entries[]) so that
when the queue is full it does not increment fake->writeCount and returns false
(or otherwise signals failure), ensuring HasUnsent() and tests like
StoreDidNotRetainLastWrite() observe the dropped write consistently; update any
callers/tests expecting the previous silent-accept behavior if needed.

In `@Tests/Support/SocketFake.h`:
- Around line 94-100: The comment describing select simulation is misleading:
SocketFake_SetSelectError(bool hasError) should not imply errors are signalled
via exceptfds only; on POSIX a non-blocking connect failure is indicated by the
fd being writable (writefds) with SO_ERROR set. Update the comment to state the
two distinct simulations supported (a select() failure via
SocketFake_SetSelectReturn(-1) vs. a connect-completion-with-error simulated by
setting writefds and a non-zero SO_ERROR when SocketFake_SetSelectError(true)),
and document that SocketFake_SetSelectWritable(true) models the fd appearing in
writefds and that the test harness must call both SetSelectWritable(true) and
SetSelectError(true) to simulate a writable-but-errored socket
(getsockopt(SO_ERROR) non-zero).
🪄 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: 013a7935-83b6-460f-a7d8-6e45c8b2c18c

📥 Commits

Reviewing files that changed from the base of the PR and between 86ab301 and 13b11ce.

📒 Files selected for processing (24)
  • Core/Interface/SolidSyslogStream.h
  • Core/Source/SolidSyslog.c
  • DEVLOG.md
  • Platform/OpenSsl/Source/SolidSyslogTlsStream.c
  • Platform/OpenSsl/Source/SolidSyslogTlsStreamInternal.h
  • Platform/Posix/Source/SolidSyslogPosixTcpStream.c
  • Platform/Windows/Source/SolidSyslogWinsockTcpStream.c
  • Tests/CMakeLists.txt
  • Tests/SolidSyslogPosixTcpStreamTest.cpp
  • Tests/SolidSyslogStreamSenderTest.cpp
  • Tests/SolidSyslogTest.cpp
  • Tests/SolidSyslogTlsStreamTest.cpp
  • Tests/SolidSyslogWinsockTcpStreamTest.cpp
  • Tests/StoreFake.c
  • Tests/StoreFake.h
  • Tests/StoreFakeTest.cpp
  • Tests/StreamFake.c
  • Tests/StreamFake.h
  • Tests/Support/OpenSslFake.c
  • Tests/Support/OpenSslFake.h
  • Tests/Support/SocketFake.c
  • Tests/Support/SocketFake.h
  • Tests/Support/WinsockFake.c
  • Tests/Support/WinsockFake.h
👮 Files not reviewed due to content moderation or server errors (7)
  • Platform/Posix/Source/SolidSyslogPosixTcpStream.c
  • Platform/Windows/Source/SolidSyslogWinsockTcpStream.c
  • DEVLOG.md
  • Tests/SolidSyslogPosixTcpStreamTest.cpp
  • Platform/OpenSsl/Source/SolidSyslogTlsStream.c
  • Tests/SolidSyslogTlsStreamTest.cpp
  • Tests/Support/SocketFake.c

Comment thread Core/Interface/SolidSyslogStream.h
Comment thread Core/Source/SolidSyslog.c Outdated
Comment thread Tests/SolidSyslogTest.cpp Outdated
DavidCozens and others added 7 commits May 7, 2026 07:53
…pFunction

SKILL.md bans #ifdef in production code. The TlsStream's bounded handshake
retry sleep is now an integrator-injected callback (SolidSyslogSleepFunction
typedef in Core/Interface/SolidSyslogSleep.h). Two platform implementations
ship alongside the rest of the platform-specific helpers:

- SolidSyslogPosixSleep wraps nanosleep.
- SolidSyslogWindowsSleep wraps Sleep.

The example TLS-sender wirings inject the platform impl (PosixSleep on the
Linux Threaded example, WindowsSleep on the Windows example). Unit tests
inject a local NoOpSleep through the config field instead of UT_PTR_SET on
a function-pointer seam, so SolidSyslogTlsStreamInternal.h is no longer
needed and is deleted. SOLIDSYSLOG_TLS_STREAM_SIZE bumped from 13 to 14
intptr_t slots to account for the new config field.
- SolidSyslogStream.h: Read doc now mentions "call Open before next Send
  or Read" (the < 0 path also closes the socket internally so a follow-up
  Read needs Open too).

- SolidSyslog.c DrainBufferIntoStore: switch to using Store_Write's return
  value directly. The previous StoreDidNotRetainLastWrite() helper proxied
  via !HasUnsent(), which silently dropped a rejected Write into a
  non-empty store (e.g. BlockStore full + HALT discard policy). The bool
  contract is now explicit: true = retained for later replay; false = not
  held by this store.

- SolidSyslogNullStore.c Write now returns false to honour that contract —
  NullStore never retains, so the eager-drain loop's !Write() branch
  takes the direct-send path. Preserves the constrained-system real-buffer
  + NullStore + UDP "one attempt per message, no buffering" wiring used
  by the Threaded example's --store=null mode.

- StoreFake.c Write: queue-full now returns false and skips the writeCount
  bump so HasUnsent() and the test counters stay consistent with what was
  actually retained — mirrors the new real-store semantics.

- SolidSyslogTest.cpp: lift the duplicated CircularBuffer + StoreFake +
  NullMutex setup out of the two new eager-drain tests into a
  SolidSyslogServiceEagerDrain TEST_GROUP. bufferStorage moves inside
  setup() with static storage duration so a CHECK failure that skips the
  test body's cleanup cannot leave a dangling stack reference behind for
  SolidSyslog_Destroy in teardown.

- SocketFake.h: clarify the select() seam comment to spell out the three
  distinct simulations the fake supports — successful connect, deferred
  connect failure (writefds + SO_ERROR), and exceptfds rejection.
ReadNextUnsent already returns false on an empty store (NullStore and the
real BlockStore both honour the contract), so the leading HasUnsent check
in the && chain is dead weight. The remaining ReadNextUnsent && Send →
MarkSent reads more directly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The literal 1 in WinsockTcpStream::WaitForConnectCompletion's select()
call relied on a nearby comment for meaning. Lifting it to a named
WINSOCK_NFDS_IGNORED enum constant moves the explanation to the
definition site and makes the call read at one level of abstraction.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ments

The eager-drain TEST_GROUP used a 9-element positional brace initializer
for SolidSyslogConfig — silently shifts if a new field is added to the
config struct. Switch to value-init plus explicit named assignments so
new fields default to zero/nullptr and the intent of each populated
slot is visible at the call site.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The non-blocking BIO, handshake retry, Send fail-fast, and Read non-blocking
test groups added in S12.14 each repeated a 4-6 line arrangement:
open + drive a fake's return + invoke the SUT + grab a callback pointer.
Lift the patterns into TEST_GROUP member helpers (InvokeBioRead*,
ArrangeHandshake*, OpenThenCauseSslWriteFailure, SendShortMessage,
OpenThenReadWithSslReturnAndError) so each test reads as one or two
sentences. Same treatment for repeated assertion shapes — five CHECK_*
macros (CHECK_BIO_READ_RETRY_SIGNALLED, CHECK_BIO_RETRY_FLAGS_CLEARED,
CHECK_SSL_SESSION_CLOSED, CHECK_TRANSPORT_CLOSED_ONCE etc.) name the
intent of each multi-line check so the failing-line pointer still maps
to the caller, mirroring the CHECK_PRIVAL convention from
SolidSyslogTest.cpp.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The non-blocking-Read tests added in S12.14 each declared a 16-byte
buffer and called SolidSyslogStream_Read inline; the close-fd assertions
captured an openFd local before invoking the path that closes it and
then asserted (count == 1, lastClosed == openFd). Lift each into the
TEST_GROUP / file-local form: a Read16ByteBuffer member returning the
ssize directly, and a CHECK_SOCKET_CLOSED_ONCE macro that pairs the
count check with the matching-fd check. Same pattern applied to both
the POSIX and Winsock test files for symmetry.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented May 7, 2026

Copy link
Copy Markdown
Contributor

☀️   Quality Summary

   🚦   build-linux-gcc: 100% successful (✔️ 1090 passed, 🙈 3 skipped)
   🚦   build-linux-clang: 100% successful (✔️ 1037 passed, 🙈 3 skipped)
   🚦   sanitize-linux-gcc: 100% successful (✔️ 1037 passed, 🙈 3 skipped)
   🚦   integration-linux-openssl: 100% successful (✔️ 9 passed)
   🚦   integration-windows-openssl: 100% successful (✔️ 9 passed)
   🚦   bdd-linux-syslog-ng: 100% successful (✔️ 46 passed)
   🚦   bdd-windows-otel: 85% successful (❌ 1 failed, ✔️ 39 passed, 🙈 6 skipped)
   🚦   build-windows-msvc: 100% successful (✔️ 948 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.

IWYU reported missing forward declarations for the two test bodies that
reference the new CHECK_TRANSPORT_CLOSED_ONCE macro
(ReadReturnsNegativeOneOnHardErrorAndClosesSsl,
SendClosesTransportOnWriteFailure). Same forward-decl pattern as other
test files in the repo.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented May 7, 2026

Copy link
Copy Markdown
Contributor

☀️   Quality Summary

   🚦   build-linux-gcc: 100% successful (✔️ 1090 passed, 🙈 3 skipped)
   🚦   build-linux-clang: 100% successful (✔️ 1037 passed, 🙈 3 skipped)
   🚦   sanitize-linux-gcc: 100% successful (✔️ 1037 passed, 🙈 3 skipped)
   🚦   integration-linux-openssl: 100% successful (✔️ 9 passed)
   🚦   integration-windows-openssl: 100% successful (✔️ 9 passed)
   🚦   bdd-linux-syslog-ng: 100% successful (✔️ 46 passed)
   🚦   bdd-windows-otel: 87% successful (✔️ 40 passed, 🙈 6 skipped)
   🚦   build-windows-msvc: 100% successful (✔️ 948 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: 5

🧹 Nitpick comments (1)
Tests/SolidSyslogTlsStreamTest.cpp (1)

15-22: 💤 Low value

g_lastSleepMs is captured but never asserted.

NoOpSleep records g_lastSleepMs on every call, but no test reads it. Either drop it (it's dead test plumbing) or use it to lock down the documented HANDSHAKE_POLL_INTERVAL_MILLISECONDS=1 pacing — a one-line LONGS_EQUAL(1, g_lastSleepMs); in OpenSleepsBetweenHandshakeRetries prevents an accidental constant change from going unnoticed.

🪶 Suggested addition (preferred)
 TEST(SolidSyslogTlsStream, OpenSleepsBetweenHandshakeRetries)
 {
     ArrangeHandshakeRetryThenSucceed(SSL_ERROR_WANT_READ);
     SolidSyslogStream_Open(stream, addr);
     LONGS_EQUAL(1, g_sleepCallCount);
+    LONGS_EQUAL(1, g_lastSleepMs);  /* HANDSHAKE_POLL_INTERVAL_MILLISECONDS */
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Tests/SolidSyslogTlsStreamTest.cpp` around lines 15 - 22, g_lastSleepMs is
recorded by NoOpSleep but never asserted; update the test
OpenSleepsBetweenHandshakeRetries to assert the expected pacing by adding a
check that g_lastSleepMs equals HANDSHAKE_POLL_INTERVAL_MILLISECONDS (e.g.,
LONGS_EQUAL(HANDSHAKE_POLL_INTERVAL_MILLISECONDS, g_lastSleepMs)) after the Open
call completes, or remove g_lastSleepMs and its assignment from NoOpSleep if you
prefer to drop the dead plumbing; modify either NoOpSleep or
OpenSleepsBetweenHandshakeRetries accordingly to keep test plumbing and
expectations consistent.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Platform/OpenSsl/Interface/SolidSyslogTlsStream.h`:
- Line 7: Update the callback API to include a context pointer and expose it in
the TLS stream config: change the typedef SolidSyslogSleepFunction in
Core/Interface/SolidSyslogSleep.h from void (*)(int milliseconds) to void
(*)(void* context, int milliseconds), add a corresponding void* sleepContext
field to SolidSyslogTlsStreamConfig in SolidSyslogTlsStream.h, and then update
all implementations and call sites (e.g., SolidSyslogPosixSleep,
SolidSyslogWindowsSleep and any callers) to accept the new (void* context, int
milliseconds) signature and pass the config->sleepContext when invoking the
sleep callback. Ensure the config field name exactly matches sleepContext so
tests and instrumentation can supply the context.

In `@Platform/OpenSsl/Source/SolidSyslogTlsStream.c`:
- Around line 340-369: Update the sleep callback to accept a context pointer:
change the SolidSyslogSleepFunction typedef to take (int milliseconds, void*
context), add a void* sleepContext field to SolidSyslogTlsStreamConfig, and
update all call sites (e.g., TlsStream_PerformHandshake) to call
stream->config.sleep(HANDSHAKE_POLL_INTERVAL_MILLISECONDS,
stream->config.sleepContext); ensure any assignments to config.sleep also match
the new signature.
- Line 363: SolidSyslogTlsStream_Create must validate that the provided config
and its required callback sleep are non-NULL to avoid dereferencing
stream->config.sleep during handshake retries; modify
SolidSyslogTlsStream_Create to check that config != NULL and config->sleep !=
NULL (or stream->config.sleep after copying) and if missing return NULL (and
optionally log an error) so the function fails fast instead of allowing the
service thread to crash when HANDSHAKE_POLL_INTERVAL_MILLISECONDS is used during
WANT_READ/WANT_WRITE retries.

In `@Tests/SolidSyslogTlsStreamTest.cpp`:
- Around line 124-136: The four single-statement CHECK_* macros
(CHECK_BIO_READ_RETRY_SIGNALLED, CHECK_BIO_READ_RETRY_NOT_SIGNALLED,
CHECK_BIO_RETRY_FLAGS_CLEARED, CHECK_TRANSPORT_CLOSED_ONCE) violate the required
do { ... } while (0) pattern; update each macro to wrap its assertion in a do {
... } while (0) block (matching the style used by CHECK_SSL_SESSION_CLOSED) so
they remain safe if later expanded, keeping the existing // NOLINTBEGIN/ //
NOLINTEND guards intact.
- Around line 1052-1057: The test
SolidSyslogTlsStream::ReadReturnsNegativeOneOnZeroReturnAndClosesSsl currently
verifies only that the SSL session is closed after an SSL_ERROR_ZERO_RETURN but
not that the underlying transport was closed; update the test (the call to
OpenThenReadWithSslReturnAndError(0, SSL_ERROR_ZERO_RETURN)) to also assert
transport closure by adding CHECK_TRANSPORT_CLOSED_ONCE() after
CHECK_SSL_SESSION_CLOSED(), ensuring TlsStream_Close’s behavior of
unconditionally closing the transport is validated for the clean-shutdown path.

---

Nitpick comments:
In `@Tests/SolidSyslogTlsStreamTest.cpp`:
- Around line 15-22: g_lastSleepMs is recorded by NoOpSleep but never asserted;
update the test OpenSleepsBetweenHandshakeRetries to assert the expected pacing
by adding a check that g_lastSleepMs equals HANDSHAKE_POLL_INTERVAL_MILLISECONDS
(e.g., LONGS_EQUAL(HANDSHAKE_POLL_INTERVAL_MILLISECONDS, g_lastSleepMs)) after
the Open call completes, or remove g_lastSleepMs and its assignment from
NoOpSleep if you prefer to drop the dead plumbing; modify either NoOpSleep or
OpenSleepsBetweenHandshakeRetries accordingly to keep test plumbing and
expectations consistent.
🪄 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: 1d4d34ec-79e9-40a2-9214-328f1544f984

📥 Commits

Reviewing files that changed from the base of the PR and between 13b11ce and 8ee9da3.

📒 Files selected for processing (28)
  • CLAUDE.md
  • Core/Interface/SolidSyslogSleep.h
  • Core/Interface/SolidSyslogStream.h
  • Core/Source/SolidSyslog.c
  • Core/Source/SolidSyslogNullStore.c
  • Example/Common/ExampleTlsSender_OpenSsl_PosixTcp.c
  • Example/Common/ExampleTlsSender_OpenSsl_WinsockTcp.c
  • Platform/OpenSsl/Interface/SolidSyslogTlsStream.h
  • Platform/OpenSsl/Source/SolidSyslogTlsStream.c
  • Platform/Posix/CMakeLists.txt
  • Platform/Posix/Interface/SolidSyslogPosixSleep.h
  • Platform/Posix/Source/SolidSyslogPosixSleep.c
  • Platform/Windows/CMakeLists.txt
  • Platform/Windows/Interface/SolidSyslogWindowsSleep.h
  • Platform/Windows/Source/SolidSyslogWindowsSleep.c
  • Platform/Windows/Source/SolidSyslogWinsockTcpStream.c
  • Tests/CMakeLists.txt
  • Tests/OpenSslIntegration/SolidSyslogTlsStreamIntegrationTest.cpp
  • Tests/SolidSyslogNullStoreTest.cpp
  • Tests/SolidSyslogPosixSleepTest.cpp
  • Tests/SolidSyslogPosixTcpStreamTest.cpp
  • Tests/SolidSyslogTest.cpp
  • Tests/SolidSyslogTlsStreamTest.cpp
  • Tests/SolidSyslogWindowsSleepTest.cpp
  • Tests/SolidSyslogWinsockTcpStreamTest.cpp
  • Tests/StoreFake.c
  • Tests/Support/SocketFake.c
  • Tests/Support/SocketFake.h
✅ Files skipped from review due to trivial changes (7)
  • Platform/Windows/Source/SolidSyslogWindowsSleep.c
  • Platform/Posix/Interface/SolidSyslogPosixSleep.h
  • Core/Interface/SolidSyslogSleep.h
  • Platform/Posix/Source/SolidSyslogPosixSleep.c
  • Tests/SolidSyslogPosixSleepTest.cpp
  • Platform/Windows/Interface/SolidSyslogWindowsSleep.h
  • CLAUDE.md
🚧 Files skipped from review as they are similar to previous changes (8)
  • Core/Interface/SolidSyslogStream.h
  • Core/Source/SolidSyslog.c
  • Tests/SolidSyslogWinsockTcpStreamTest.cpp
  • Tests/StoreFake.c
  • Tests/SolidSyslogPosixTcpStreamTest.cpp
  • Tests/SolidSyslogTest.cpp
  • Tests/Support/SocketFake.c
  • Tests/Support/SocketFake.h

#include <stdint.h>

#include "ExternC.h"
#include "SolidSyslogSleep.h"

@coderabbitai coderabbitai Bot May 7, 2026

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.

⚠️ Potential issue | 🔴 Critical | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm the current SolidSyslogSleepFunction typedef and all call sites
# to verify the context parameter is absent everywhere.
rg -n "SolidSyslogSleepFunction" --type=c --type=h --type=cpp -A2

Repository: DavidCozens/solid-syslog

Length of output: 1482


SolidSyslogSleepFunction violates the callback guideline — missing void* context parameter and paired config field.

The project rule for new callbacks requires:

For new callbacks, the function pointer must take a void* context parameter. Config struct must expose a paired context field.

SolidSyslogSleepFunction is defined in Core/Interface/SolidSyslogSleep.h as void (*)(int milliseconds) with no context parameter. Both platform implementations (SolidSyslogPosixSleep and SolidSyslogWindowsSleep) follow this signature. SolidSyslogTlsStreamConfig in this file exposes no matching sleepContext field. This diverges from the callback API standard and makes test-observable instrumentation needlessly awkward.

Fix requires changes in:

  • Core/Interface/SolidSyslogSleep.h: add void* context to typedef
  • SolidSyslogTlsStream.h: add sleepContext field to config struct
  • All implementations and call sites: update signatures and pass context through
♻️ Proposed change for SolidSyslogTlsStream.h
     struct SolidSyslogTlsStreamConfig
     {
         struct SolidSyslogStream* transport;
         SolidSyslogSleepFunction  sleep;               /* drives bounded handshake retry ... — required */
+        void*                     sleepContext;        /* passed verbatim to sleep() — may be NULL if callback ignores it */
         const char*               caBundlePath;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Platform/OpenSsl/Interface/SolidSyslogTlsStream.h` at line 7, Update the
callback API to include a context pointer and expose it in the TLS stream
config: change the typedef SolidSyslogSleepFunction in
Core/Interface/SolidSyslogSleep.h from void (*)(int milliseconds) to void
(*)(void* context, int milliseconds), add a corresponding void* sleepContext
field to SolidSyslogTlsStreamConfig in SolidSyslogTlsStream.h, and then update
all implementations and call sites (e.g., SolidSyslogPosixSleep,
SolidSyslogWindowsSleep and any callers) to accept the new (void* context, int
milliseconds) signature and pass the config->sleepContext when invoking the
sleep callback. Ensure the config field name exactly matches sleepContext so
tests and instrumentation can supply the context.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Declined — sleep is stateless. Both platform implementations (nanosleep / Sleep) are pure functions over int milliseconds with no instance state to carry, so a void* context parameter would always be passed NULL at the call site. The CLAUDE.md rule for new callbacks covers cases where context-passing is a real need; a thin platform-API wrapper is not one.

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.

@DavidCozens, understood — that's a reasonable distinction. A thin wrapper over a stateless OS primitive (nanosleep / Sleep) doesn't need a context slot, and mandating one would only add noise at every call site. I'll note this nuance for future reviews.


✏️ Learnings added
Learnt from: DavidCozens
Repo: DavidCozens/solid-syslog PR: 283
File: Platform/OpenSsl/Interface/SolidSyslogTlsStream.h:7-7
Timestamp: 2026-05-07T08:22:53.110Z
Learning: In the `solid-syslog` project, the CLAUDE.md rule requiring a `void* context` parameter (and paired config field) for new callbacks applies only to callbacks that carry real instance state. Thin wrappers over stateless OS primitives — such as `SolidSyslogSleepFunction` (wrapping `nanosleep`/`Sleep`) — are exempt: both platform implementations are pure functions over `int milliseconds` with no state to carry, so a context pointer would always be NULL and should not be added.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

Comment on lines 340 to 369
static inline bool TlsStream_PerformHandshake(struct SolidSyslogTlsStream* stream)
{
return SSL_connect(stream->ssl) > 0;
int totalSleptMs = 0;
bool result = false;
bool done = false;

while (!done)
{
int rc = SSL_connect(stream->ssl);
if (rc > 0)
{
result = true;
done = true;
}
else
{
int err = SSL_get_error(stream->ssl, rc);
if (!IsRetryableSslError(err) || IsHandshakeBudgetExhausted(totalSleptMs))
{
done = true;
}
else
{
stream->config.sleep(HANDSHAKE_POLL_INTERVAL_MILLISECONDS);
totalSleptMs += HANDSHAKE_POLL_INTERVAL_MILLISECONDS;
}
}
}
return result;
}

@coderabbitai coderabbitai Bot May 7, 2026

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.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Locate the SolidSyslogSleepFunction typedef and the config struct's sleep field.
rg -nP -C2 'SolidSyslogSleepFunction|typedef.*sleep|\bsleep\s*[;,]\s*(/\*|$|//)' --type=c --type=cpp -g '!**/Tests/**'
fd -e h -e c 'SolidSyslogTlsStream' --exec rg -nC2 'sleep'

Repository: DavidCozens/solid-syslog

Length of output: 3142


Add void* context parameter to SolidSyslogSleepFunction callback per coding guidelines.

The sleep callback must follow the mandatory pattern: "For new callbacks, the function pointer must take a void* context parameter. Config struct must expose a paired context field."

Currently, SolidSyslogSleepFunction is defined as typedef void (*SolidSyslogSleepFunction)(int milliseconds); and called as stream->config.sleep(HANDSHAKE_POLL_INTERVAL_MILLISECONDS); without a context parameter. This prevents passing state into custom sleep implementations (e.g., test fakes, platform objects) and deviates from the library's callback pattern.

Update the typedef, config struct, and call site:

  • Add void* sleepContext; field to SolidSyslogTlsStreamConfig
  • Change typedef to typedef void (*SolidSyslogSleepFunction)(int milliseconds, void* context);
  • Update call to stream->config.sleep(HANDSHAKE_POLL_INTERVAL_MILLISECONDS, stream->config.sleepContext);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Platform/OpenSsl/Source/SolidSyslogTlsStream.c` around lines 340 - 369,
Update the sleep callback to accept a context pointer: change the
SolidSyslogSleepFunction typedef to take (int milliseconds, void* context), add
a void* sleepContext field to SolidSyslogTlsStreamConfig, and update all call
sites (e.g., TlsStream_PerformHandshake) to call
stream->config.sleep(HANDSHAKE_POLL_INTERVAL_MILLISECONDS,
stream->config.sleepContext); ensure any assignments to config.sleep also match
the new signature.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Declined for the same reason as the typedef thread — sleep is stateless and a void* context parameter would always be NULL.

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.

@DavidCozens, understood — acknowledged and noted. Since SolidSyslogSleepFunction is intentionally stateless (platform sleep implementations like SolidSyslogPosixSleep and SolidSyslogWindowsSleep carry no per-call state), a void* context parameter would be unnecessary noise. I'll skip this pattern for stateless callbacks going forward.


✏️ Learnings added
Learnt from: DavidCozens
Repo: DavidCozens/solid-syslog PR: 283
File: Platform/OpenSsl/Source/SolidSyslogTlsStream.c:340-369
Timestamp: 2026-05-07T08:22:56.635Z
Learning: In the `DavidCozens/solid-syslog` repository, `SolidSyslogSleepFunction` (defined in `Core/Interface/SolidSyslogSleep.h`) is intentionally stateless — `typedef void (*SolidSyslogSleepFunction)(int milliseconds);` — and must NOT be flagged for missing a `void* context` parameter. Platform implementations (`SolidSyslogPosixSleep`, `SolidSyslogWindowsSleep`) carry no per-call state, so a context field on `SolidSyslogTlsStreamConfig` would always be NULL. The callback-with-context guideline does not apply to stateless platform utility callbacks in this codebase.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

}
else
{
stream->config.sleep(HANDSHAKE_POLL_INTERVAL_MILLISECONDS);

@coderabbitai coderabbitai Bot May 7, 2026

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.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm whether any caller (Example, Platform, Bdd) creates a TlsStreamConfig without setting `sleep`.
rg -nP -C5 'SolidSyslogTlsStream_Create\b' -g '!**/Tests/**'
rg -nP -C3 'SolidSyslogTlsStreamConfig\b' -g '!**/Tests/**'

Repository: DavidCozens/solid-syslog

Length of output: 17873


🏁 Script executed:

# Examine the context around line 363 in SolidSyslogTlsStream.c
sed -n '350,380p' Platform/OpenSsl/Source/SolidSyslogTlsStream.c

Repository: DavidCozens/solid-syslog

Length of output: 992


🏁 Script executed:

# Find all usages of stream->config.sleep
rg -n 'stream->config\.sleep' Platform/OpenSsl/Source/SolidSyslogTlsStream.c -B2 -A2

Repository: DavidCozens/solid-syslog

Length of output: 279


🏁 Script executed:

# Check if there are any NULL checks before sleep is called
rg -n 'sleep\s*!=' Platform/OpenSsl/Source/SolidSyslogTlsStream.c -B3 -A3

Repository: DavidCozens/solid-syslog

Length of output: 50


🏁 Script executed:

# Get complete Create function to verify no validation exists
sed -n '71,80p' Platform/OpenSsl/Source/SolidSyslogTlsStream.c

Repository: DavidCozens/solid-syslog

Length of output: 503


🏁 Script executed:

# Search for all instantiation patterns of SolidSyslogTlsStreamConfig
rg -n 'SolidSyslogTlsStreamConfig\s*=' --type c -A10 | head -80

Repository: DavidCozens/solid-syslog

Length of output: 50


🏁 Script executed:

# Check if there are any test files that create TlsStreamConfig
fd -e c -path '*/Test*' -o -path '*/Bdd*' | xargs grep -l 'SolidSyslogTlsStreamConfig' 2>/dev/null || echo "No test files found with TlsStreamConfig"

Repository: DavidCozens/solid-syslog

Length of output: 296


🏁 Script executed:

# Examine the header documentation to confirm the "required" designation
sed -n '20,32p' Platform/OpenSsl/Interface/SolidSyslogTlsStream.h

Repository: DavidCozens/solid-syslog

Length of output: 994


Add validation in SolidSyslogTlsStream_Create to ensure config->sleep is not NULL.

stream->config.sleep is unconditionally dereferenced during handshake retry (line 363) when WANT_READ/WANT_WRITE occurs. The interface documents sleep as "required" (line 26 of .h), and all current callers (Example code) properly set it. However, _Create performs no validation—if a caller misses this, the service thread crashes. Validate at _Create and return NULL to fail fast.

Suggested fix
 struct SolidSyslogStream* SolidSyslogTlsStream_Create(SolidSyslogTlsStreamStorage* storage, const struct SolidSyslogTlsStreamConfig* config)
 {
+    if (config->sleep == NULL)
+    {
+        return NULL;
+    }
     struct SolidSyslogTlsStream* stream = (struct SolidSyslogTlsStream*) storage;
     *stream                             = DEFAULT_INSTANCE;
     stream->config                      = *config;
     return &stream->base;
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Platform/OpenSsl/Source/SolidSyslogTlsStream.c` at line 363,
SolidSyslogTlsStream_Create must validate that the provided config and its
required callback sleep are non-NULL to avoid dereferencing stream->config.sleep
during handshake retries; modify SolidSyslogTlsStream_Create to check that
config != NULL and config->sleep != NULL (or stream->config.sleep after copying)
and if missing return NULL (and optionally log an error) so the function fails
fast instead of allowing the service thread to crash when
HANDSHAKE_POLL_INTERVAL_MILLISECONDS is used during WANT_READ/WANT_WRITE
retries.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Declined — the header documents sleep as required, and the two integrator call sites (Example/Common/ExampleTlsSender_OpenSsl_PosixTcp.c and _WinsockTcp.c) wire it. CLAUDE.md says "don't add validation for scenarios that can't happen — trust internal code and framework guarantees. Only validate at system boundaries (user input, external APIs)." This is a setup-time wiring contract between integrator and library, not an external input.

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.

@DavidCozens, understood — the project's explicit policy of trusting internal wiring contracts and reserving validation for true system/external boundaries is a sound principle, and the header documentation plus the two concrete call sites make the requirement clear. I'll keep this in mind for future reviews on this repo.


✏️ Learnings added
Learnt from: DavidCozens
Repo: DavidCozens/solid-syslog PR: 283
File: Platform/OpenSsl/Source/SolidSyslogTlsStream.c:363-363
Timestamp: 2026-05-07T08:22:59.856Z
Learning: In DavidCozens/solid-syslog, CLAUDE.md explicitly states: do not add NULL/defensive validation for scenarios that can't happen under normal internal usage — trust internal code and framework guarantees. Only validate at real system boundaries (user input, external APIs). Setup-time wiring contracts between integrator and library (e.g. required function-pointer fields like `SolidSyslogTlsStreamConfig.sleep`) are intentionally left without NULL guards in `_Create` functions.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

Comment thread Tests/SolidSyslogTlsStreamTest.cpp
Comment thread Tests/SolidSyslogTlsStreamTest.cpp
- Wrap the four single-statement CHECK_* macros in `do { ... } while (0)`
  for consistency with CHECK_SSL_SESSION_CLOSED and the codebase
  convention. Guards against the if/else dangling-statement pitfall if
  the macro is ever extended.
- Add CHECK_TRANSPORT_CLOSED_ONCE() to the SSL_ERROR_ZERO_RETURN test so
  it locks down the full Read fail-fast contract on the clean-shutdown
  path, matching the hard-error variant.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented May 7, 2026

Copy link
Copy Markdown
Contributor

☀️   Quality Summary

   🚦   build-linux-gcc: 100% successful (✔️ 1090 passed, 🙈 3 skipped)
   🚦   build-linux-clang: 100% successful (✔️ 1037 passed, 🙈 3 skipped)
   🚦   sanitize-linux-gcc: 100% successful (✔️ 1037 passed, 🙈 3 skipped)
   🚦   integration-linux-openssl: 100% successful (✔️ 9 passed)
   🚦   integration-windows-openssl: 100% successful (✔️ 9 passed)
   🚦   bdd-linux-syslog-ng: 100% successful (✔️ 46 passed)
   🚦   bdd-windows-otel: 87% successful (✔️ 40 passed, 🙈 6 skipped)
   🚦   build-windows-msvc: 100% successful (✔️ 948 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.

The previous commit added CHECK_TRANSPORT_CLOSED_ONCE() to the
ReadReturnsNegativeOneOnZeroReturnAndClosesSsl test. Touching the
TEST_GROUP `transport` member via that macro triggers the same IWYU
forward-decl pattern as the other two transport-touching tests in this
file.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented May 7, 2026

Copy link
Copy Markdown
Contributor

☀️   Quality Summary

   🚦   build-linux-gcc: 100% successful (✔️ 1090 passed, 🙈 3 skipped)
   🚦   build-linux-clang: 100% successful (✔️ 1037 passed, 🙈 3 skipped)
   🚦   sanitize-linux-gcc: 100% successful (✔️ 1037 passed, 🙈 3 skipped)
   🚦   integration-linux-openssl: 100% successful (✔️ 9 passed)
   🚦   integration-windows-openssl: 100% successful (✔️ 9 passed)
   🚦   bdd-linux-syslog-ng: 100% successful (✔️ 46 passed)
   🚦   bdd-windows-otel: 87% successful (✔️ 40 passed, 🙈 6 skipped)
   🚦   build-windows-msvc: 100% successful (✔️ 948 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 DavidCozens merged commit 89fd6ce into main May 7, 2026
18 checks passed
@DavidCozens DavidCozens deleted the feat/s12-14-decouple-buffer-drain-from-sender-state branch May 7, 2026 08:32
DavidCozens added a commit that referenced this pull request May 7, 2026
* chore: bump BDD wait_for_messages deadline 5s → 10s

The 5s budget for the buffered example to complete launch + cert load +
TLS handshake + Send + oracle write was flake-prone on the Windows OTel
runner under load (mTLS scenario in PR #283 timed out once and passed
on retry). 10s gives comfortable margin without changing test intent.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore: make BDD scenarios runnable on Windows-hosted devcontainer

Two chronic local-only issues blocked TCP-outage scenarios from running
under the Windows-hosted Docker Desktop devcontainer; CI (which runs in
its own Linux VM with matching UID/GID) was unaffected. Both fixes are
no-ops for CI.

1. shutil.copy → shutil.copyfile in syslog_ng_swap_config (steps) and
   after_scenario (environment). The destination syslog-ng.conf is
   bind-mounted from the host and not owned by the container's
   `developer` user; copy()'s chmod step then fails with EPERM and
   leaves the config truncated. copyfile copies bytes only, so the
   file's existing mode (set by the host) is left intact.

2. syslog-ng compose entrypoint now keeps a watchdog re-chmodding
   /var/lib/syslog-ng/syslog-ng.ctl to 0777. syslog-ng RELOAD recreates
   the socket with default 0755 perms; the behave container connects as
   `developer` and needs world-writable to call connect(2). Single
   100ms-tick background loop, exits with the syslog-ng pid.

Closes the open follow-up tracked in MEMORY:project_syslog_ng_conf_corruption.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat: S13.21 re-enable @windows_wip store_capacity scenarios

S12.14's eager-drain refactor decoupled buffer drain from sender state
on both runners; the four store_capacity discard scenarios that were
tagged @windows_wip pending that fix now run reliably end-to-end.

Drop the four @windows_wip tags and the long explanatory note; the
existing 10/8-message form gives sufficient margin for the discard
policy to engage with both runners. Locally verified all 4 scenarios
pass on Linux (and the full BDD suite — 21 features, 46 scenarios,
242 steps — remains green).

Closes #278

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: address CodeRabbit review on PR #284

- environment.py after_scenario: move shutil.copyfile inside the existing
  try/except so a copyfile failure is logged via the warning path rather
  than bypassing the rest of teardown.
- syslog_steps.py step_oracle_stops_tcp: set
  context.syslog_ng_config_changed = True *before* syslog_ng_swap_config()
  rather than after. Any failure inside swap (or the subsequent wait /
  probe teardown / sleep) still triggers the after_scenario restore path —
  otherwise a half-applied swap would leave the on-disk config in
  UDP-only mode and contaminate the next scenario.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.

S12.14: Decouple buffer drain from sender state — fail-fast streams + eager Service drain

1 participant