Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
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
3 changes: 3 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -358,10 +358,13 @@ live under `Core/Interface/`; platform-specific helpers (the `SolidSyslogPosix*`
| `SolidSyslogPosixHostname.h` | String callback implementor using POSIX hostname | `SolidSyslogPosixHostname_Get` (writes into `SolidSyslogFormatter*`) |
| `SolidSyslogPosixProcessId.h` | String callback implementor using POSIX process ID | `SolidSyslogPosixProcessId_Get` (writes into `SolidSyslogFormatter*`) |
| `SolidSyslogPosixSysUpTime.h` | SysUpTime callback implementor using POSIX `CLOCK_BOOTTIME` | `SolidSyslogPosixSysUpTime_Get` (returns `uint32_t` hundredths since boot, wraps per RFC 3418 `TimeTicks`) |
| `SolidSyslogPosixSleep.h` | System setup code wiring a `SolidSyslogSleepFunction` on POSIX targets | `SolidSyslogPosixSleep` (wraps `nanosleep`) |
| `SolidSyslogWindowsClock.h` | System setup code using Windows clock | `SolidSyslogWindowsClock_GetTimestamp` |
| `SolidSyslogWindowsHostname.h` | String callback implementor using Windows hostname | `SolidSyslogWindowsHostname_Get` (writes into `SolidSyslogFormatter*`) |
| `SolidSyslogWindowsProcessId.h` | String callback implementor using Windows process ID | `SolidSyslogWindowsProcessId_Get` (writes into `SolidSyslogFormatter*`) |
| `SolidSyslogWindowsSysUpTime.h` | SysUpTime callback implementor using `GetTickCount64` | `SolidSyslogWindowsSysUpTime_Get` (returns `uint32_t` hundredths since boot), `WindowsSysUpTime_GetTickCount64` (function-pointer seam for unit tests) |
| `SolidSyslogWindowsSleep.h` | System setup code wiring a `SolidSyslogSleepFunction` on Windows targets | `SolidSyslogWindowsSleep` (wraps `Sleep`) |
| `SolidSyslogSleep.h` | Any code passing or implementing a sleep callback | `SolidSyslogSleepFunction` typedef (used by `SolidSyslogTlsStreamConfig.sleep` for the bounded handshake retry) |
| `SolidSyslogStructuredData.h` | Library internals (SD dispatch) | `SolidSyslogStructuredData_Format` (writes into `SolidSyslogFormatter*`) |
| `SolidSyslogStructuredDataDefinition.h` | SD implementors (extension point) | `SolidSyslogStructuredData` vtable struct (Format takes `SolidSyslogFormatter*`) |
| `SolidSyslogMetaSd.h` | System setup code using meta SD (sequenceId, sysUpTime, language) | `SolidSyslogMetaSdConfig` (counter, getSysUpTime, getLanguage — each independently optional via NULL), `SolidSyslogSysUpTimeFunction`, `SolidSyslogMetaSd_Create`, `_Destroy` |
Expand Down
16 changes: 16 additions & 0 deletions Core/Interface/SolidSyslogSleep.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#ifndef SOLIDSYSLOGSLEEP_H
#define SOLIDSYSLOGSLEEP_H

#include "ExternC.h"

EXTERN_C_BEGIN

/* Cross-platform millisecond sleep callback. The TLS handshake retry loop
(and any future wait-and-retry path) takes one of these via config so
the library never sees platform-specific sleep APIs and tests can
inject a no-op without conditional compilation. */
typedef void (*SolidSyslogSleepFunction)(int milliseconds);

EXTERN_C_END

#endif /* SOLIDSYSLOGSLEEP_H */
25 changes: 20 additions & 5 deletions Core/Interface/SolidSyslogStream.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,31 @@ struct SolidSyslogAddress;
EXTERN_C_BEGIN

/* Signed byte count. Models POSIX ssize_t using a standard-C type for portability
to targets that lack <sys/types.h>'s ssize_t (notably MSVC). Semantics mirror
POSIX recv(2): >=0 = bytes transferred, 0 = EOF on stream reads, <0 = error. */
to targets that lack <sys/types.h>'s ssize_t (notably MSVC). */
typedef intptr_t SolidSyslogSsize;

struct SolidSyslogStream;

bool SolidSyslogStream_Open(struct SolidSyslogStream * stream, const struct SolidSyslogAddress* addr);
bool SolidSyslogStream_Send(struct SolidSyslogStream * stream, const void* buffer, size_t size);
/* Open a connection to addr. Returns true on success. On failure the underlying
socket has already been closed internally; the caller can call Open again to retry. */
bool SolidSyslogStream_Open(struct SolidSyslogStream * stream, const struct SolidSyslogAddress* addr);

/* Non-blocking send. Returns true only when the kernel accepts the entire frame in
a single call. Anything else — short write, EAGAIN/EWOULDBLOCK, EPIPE/ECONNRESET,
any other error — closes the underlying socket internally and returns false; the
caller must call Open before the next Send. Bounded so the service-thread drain
rate stays insensitive to peer wedges or kernel send-buffer fill. */
bool SolidSyslogStream_Send(struct SolidSyslogStream * stream, const void* buffer, size_t size);

/* Non-blocking read.
> 0 bytes transferred into buffer
= 0 nothing available right now (would-block)
< 0 EOF or error — the underlying socket has been closed internally; the
caller must call Open before the next Send or Read. */
SolidSyslogSsize SolidSyslogStream_Read(struct SolidSyslogStream * stream, void* buffer, size_t size);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
void SolidSyslogStream_Close(struct SolidSyslogStream * stream);

/* Close the underlying socket. Idempotent. */
void SolidSyslogStream_Close(struct SolidSyslogStream * stream);

EXTERN_C_END

Expand Down
51 changes: 24 additions & 27 deletions Core/Source/SolidSyslog.c
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ static inline int16_t AbsoluteInt16(int16_t value);
static inline bool CaptureTimestamp(struct SolidSyslogTimestamp* ts, SolidSyslogClockFunction clock);
static inline uint8_t CombineFacilityAndSeverity(uint8_t facility, uint8_t severity);
static inline bool FacilityIsValid(uint8_t facility);
static inline bool FetchFromStore(char* buf, size_t maxSize, size_t* len);
static inline void DrainBufferIntoStore(void);
static inline void SendOneFromStore(void);
static inline void FormatCapturedTimestamp(struct SolidSyslogFormatter* f, const struct SolidSyslogTimestamp* ts);
static inline void FormatMessage(struct SolidSyslogFormatter* f, const struct SolidSyslogMessage* message);
static inline void FormatMsg(struct SolidSyslogFormatter* f, const char* msg);
Expand All @@ -47,7 +48,6 @@ static void NilClock(struct SolidSyslogTimestamp* ts);
static void NilStringFunction(struct SolidSyslogFormatter* formatter);
static inline bool PrivalComponentsAreValid(uint8_t facility, uint8_t severity);
static void ProcessMessages(void);
static inline bool ReceiveFromBufferIntoStore(char* buf, size_t maxSize, size_t* len);
static inline bool SeverityIsValid(uint8_t severity);
static inline const char* SkipLeadingBom(const char* msg);
static inline bool StringIsValid(const char* value);
Expand Down Expand Up @@ -123,45 +123,42 @@ static inline bool IsServiceEnabled(void)
}

static void ProcessMessages(void)
{
DrainBufferIntoStore();
SendOneFromStore();
}

/* Eagerly drain the buffer so the producer-side shock absorber stays small while
* the sender is slow or down — overflow then engages the store's discard policy
* rather than silently dropping at the buffer. When the store does not retain
* the message (NullStore configuration, or a store-write rejection — e.g. a
* full BlockStore under the HALT discard policy), the drain falls through to
* a best-effort direct send. The Store_Write contract is therefore: true =
* retained for later replay via ReadNextUnsent; false = not held by this
* store, the caller is on its own. */
static inline void DrainBufferIntoStore(void)
{
char buf[SOLIDSYSLOG_MAX_MESSAGE_SIZE];
size_t len = 0;

bool haveMessage = ReceiveFromBufferIntoStore(buf, sizeof(buf), &len);
bool fromStore = FetchFromStore(buf, sizeof(buf), &len);

if (fromStore || haveMessage)
while (SolidSyslogBuffer_Read(instance.buffer, buf, sizeof(buf), &len))
{
if (SolidSyslogSender_Send(instance.sender, buf, len))
if (!SolidSyslogStore_Write(instance.store, buf, len))
{
if (fromStore)
{
SolidSyslogStore_MarkSent(instance.store);
}
SolidSyslogSender_Send(instance.sender, buf, len);
}
}
}

static inline bool ReceiveFromBufferIntoStore(char* buf, size_t maxSize, size_t* len)
static inline void SendOneFromStore(void)
{
bool received = SolidSyslogBuffer_Read(instance.buffer, buf, maxSize, len);

if (received)
{
SolidSyslogStore_Write(instance.store, buf, *len);
}

return received;
}
char buf[SOLIDSYSLOG_MAX_MESSAGE_SIZE];
size_t len = 0;

static inline bool FetchFromStore(char* buf, size_t maxSize, size_t* len)
{
if (SolidSyslogStore_HasUnsent(instance.store))
if (SolidSyslogStore_ReadNextUnsent(instance.store, buf, sizeof(buf), &len) && SolidSyslogSender_Send(instance.sender, buf, len))
{
return SolidSyslogStore_ReadNextUnsent(instance.store, buf, maxSize, len);
SolidSyslogStore_MarkSent(instance.store);
}

return false;
}

void SolidSyslog_Log(const struct SolidSyslogMessage* message)
Expand Down
6 changes: 5 additions & 1 deletion Core/Source/SolidSyslogNullStore.c
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,16 @@ void SolidSyslogNullStore_Destroy(void)
instance.base.GetUsedBytes = NULL;
}

/* NullStore never retains. Returns false to signal "not held by this store"
* so the eager-drain loop in ProcessMessages takes the direct-send path —
* NullStore + real-buffer + UDP is the constrained-system "one attempt per
* message, no buffering" configuration. */
static bool Write(struct SolidSyslogStore* self, const void* data, size_t size)
{
(void) self;
(void) data;
(void) size;
return true;
return false;
}

static bool ReadNextUnsent(struct SolidSyslogStore* self, void* data, size_t maxSize, size_t* bytesRead)
Expand Down
95 changes: 95 additions & 0 deletions DEVLOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3314,3 +3314,98 @@ in commit `ebddeb4`:
whether to re-enable those rules and accept the resulting cleanup sweep.
Tracked in
[project_clang_tidy_magic_numbers.md](memory).

## 2026-05-07 — S12.14: decouple buffer drain from sender state

### Decisions

- **Eager-drain ProcessMessages** (slice 1). Replaced one-at-a-time pump
with a loop that drains buffer → store until empty, then attempts one
send from the store. Preserved the existing "send directly when store
doesn't retain" bypass via a unified rule (`!HasUnsent` after Write
→ best-effort direct send) so the Threaded example's `--store=null`
path keeps working. Refactored `StoreFake` into a small FIFO with a
write counter; left `BufferFake` single-slot and used the production
`SolidSyslogCircularBuffer` + `NullMutex` in the new burst-drain test.

- **PosixTcpStream non-blocking + fail-fast** (slice 2). `O_NONBLOCK`
via `fcntl` from socket creation; bounded `select()` connect wait
(200 ms, mirrors the Winsock value); `getsockopt(SO_ERROR)` reads the
deferred connect failure. `Send` is single-call, fail-fast — short
write / `EAGAIN` / any error closes the fd internally. `Read` returns
0 on `EAGAIN`/`EWOULDBLOCK`, -1 + close on EOF/error. Dropped
`SO_SNDTIMEO` (no-op on non-blocking) and the `EINTR` retry loop.
`SocketFake` gained `fcntl`, `select`, connect/recv `errno`-injection,
and `getsockopt(SO_ERROR)` seams.

- **WinsockTcpStream Send/Read non-blocking + fail-fast** (slice 3).
Stopped restoring blocking mode after a successful connect; dropped
`SO_SNDTIMEO`. `Send` and `Read` mirror the Posix contract.
`WinsockFake_FailNextRecvWithLastError` added to drive the
would-block / error / EOF Read paths. Validated on MSVC.

- **TlsStream non-blocking + fail-fast** (slice 4). BIO read translates
the transport's would-block (0) into `BIO_set_retry_read` + return -1
so OpenSSL retries instead of treating it as EOF; BIO write clears
retry on transport-Send failure. `PerformHandshake` drives
`SSL_connect` in a bounded retry loop with a 5 s budget and 1 ms poll
interval — sleeps on `WANT_READ`/`WANT_WRITE`, fails fast on hard
SSL errors. `Send` and `Read` follow the same rule as TCP, with
`Read`'s comment explicitly distinguishing handshake-vs-steady-state
WANT semantics. `TlsStream_Close` is idempotent. `TlsStream_sleep`
is a function-pointer seam (Windows `Sleep` / POSIX `nanosleep` by
default, UT_PTR_SET to no-op in tests). `OpenSslFake` gained
`SSL_connect` return-sequence injection, `SSL_get_error`,
`SSL_write`/`read` return overrides, and `BIO_set_flags`/`clear_flags`
spies.

### Disagreements held

- **Pushed back on the 5–10 s connect timeout** that came up during
socket-options review (Claude.ai). 200 ms is by design — Windows'
default `connect()` to a refused loopback retries internally for
~2 s, throttling the BlockStore service-thread drain rate enough
that the discard policy never fires in the @windows_wip scenarios.
Kept 200 ms; flagged "tunable connect/handshake timeouts for WAN
deployments" as a follow-up story so far-cloud SIEMs aren't stuck
with the loopback-tuned default.

- **Accepted 5 s for the TLS handshake budget**. Different concern
from the bare TCP connect — handshake takes 2–3 RTTs naturally;
my initial 200 ms plan was too tight for WAN. 5 s is the right
bound.

### Deferred — three follow-up issues drafted in a parallel session

- TCP keepalive + `SIO_KEEPALIVE_VALS` + `TCP_USER_TIMEOUT` for
dead-peer idle detection. Test-coverage decision (BDD with a
~120 s scenario vs unit-only setsockopt assertions) explicitly
flagged in the issue body.
- TLS session resumption (`SSL_CTX_set_session_cache_mode` +
tickets) so frequent fail-fast reconnects don't pay the full
handshake cost.
- Tunable connect/handshake timeouts via CMake or
`SolidSyslogStreamSenderConfig`/`SolidSyslogTlsStreamConfig`,
for WAN deployments.

### Local validation

- All Linux gates green: gcc + clang builds, sanitize, tidy, cppcheck,
format, IWYU, coverage 100% (2024/2024 lines, 429/429 functions).
- MSVC: 973 unit tests pass, including the 50 Winsock TCP stream tests
that exercise the new non-blocking Send/Read contract.
- The four `@windows_wip` scenarios in `store_capacity.feature` were
spot-checked locally on the Windows MSVC build with otelcol-contrib
as the oracle. Scenario 1 reaches step 5 ("the syslog oracle
receives 1 message") successfully — confirming the architectural
decoupling unblocks the path. Step 6 ("stops accepting TCP
connections") fails locally because Docker Desktop's `wslrelay` /
`com.docker.backend` keep `5514` bound on `::` and `taskkill` only
affects `otelcol-contrib.exe`. Not a regression of S12.14; the
`@windows_wip` tag removal stays in PR #275's finale on the clean
windows-2025 CI runner.

### Open questions

- None for this PR; the three deferred stories cover the residue
surfaced during socket-options review.
2 changes: 2 additions & 0 deletions Example/Common/ExampleTlsSender_OpenSsl_PosixTcp.c
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#include "ExampleMtlsConfig.h"
#include "ExampleTlsConfig.h"
#include "ExampleTlsSender.h"
#include "SolidSyslogPosixSleep.h"
#include "SolidSyslogPosixTcpStream.h"
#include "SolidSyslogStreamSender.h"
#include "SolidSyslogTlsStream.h"
Expand All @@ -23,6 +24,7 @@ struct SolidSyslogSender* ExampleTlsSender_Create(struct SolidSyslogResolver* re
static struct SolidSyslogTlsStreamConfig tlsStreamConfig;
tlsStreamConfig = (struct SolidSyslogTlsStreamConfig) {0};
tlsStreamConfig.transport = underlyingStream;
tlsStreamConfig.sleep = SolidSyslogPosixSleep;
if (mtls)
{
tlsStreamConfig.caBundlePath = ExampleMtlsConfig_GetCaBundlePath();
Expand Down
2 changes: 2 additions & 0 deletions Example/Common/ExampleTlsSender_OpenSsl_WinsockTcp.c
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#include "ExampleTlsSender.h"
#include "SolidSyslogStreamSender.h"
#include "SolidSyslogTlsStream.h"
#include "SolidSyslogWindowsSleep.h"
#include "SolidSyslogWinsockTcpStream.h"

struct SolidSyslogResolver;
Expand All @@ -23,6 +24,7 @@ struct SolidSyslogSender* ExampleTlsSender_Create(struct SolidSyslogResolver* re
static struct SolidSyslogTlsStreamConfig tlsStreamConfig;
tlsStreamConfig = (struct SolidSyslogTlsStreamConfig) {0};
tlsStreamConfig.transport = underlyingStream;
tlsStreamConfig.sleep = SolidSyslogWindowsSleep;
if (mtls)
{
tlsStreamConfig.caBundlePath = ExampleMtlsConfig_GetCaBundlePath();
Expand Down
4 changes: 3 additions & 1 deletion Platform/OpenSsl/Interface/SolidSyslogTlsStream.h
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@
#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.


struct SolidSyslogStream;

EXTERN_C_BEGIN

enum
{
SOLIDSYSLOG_TLS_STREAM_SIZE = sizeof(intptr_t) * 13
SOLIDSYSLOG_TLS_STREAM_SIZE = sizeof(intptr_t) * 14
};

typedef struct
Expand All @@ -22,6 +23,7 @@ EXTERN_C_BEGIN
struct SolidSyslogTlsStreamConfig
{
struct SolidSyslogStream* transport; /* underlying byte stream — caller owns */
SolidSyslogSleepFunction sleep; /* drives bounded handshake retry between WANT_READ/WANT_WRITE polls — required */
const char* caBundlePath; /* PEM file of trust anchors */
const char* serverName; /* SNI + cert hostname check; NULL to skip */
const char* cipherList; /* TLS 1.2 cipher list; NULL = OpenSSL default */
Expand Down
Loading
Loading