Skip to content

feat: send over TCP instead of UDP - #8

Merged
DavidCozens merged 1 commit into
mainfrom
stage-08-tcp
Aug 16, 2026
Merged

feat: send over TCP instead of UDP#8
DavidCozens merged 1 commit into
mainfrom
stage-08-tcp

Conversation

@DavidCozens

Copy link
Copy Markdown
Contributor

UDP to TCP, by putting a SolidSyslogStreamSender over an lwIP TCP stream. The network
retransmits rather than dropping, and a send fails when the collector is gone instead of succeeding
into a void. Records are framed by octet count per RFC 6587, which is what a receiver expects on a
stream transport.

struct SolidSyslogLwipRawTcpStreamConfig tcpConfig = {.Sleep = SyslogSleep};

struct SolidSyslogStreamSenderConfig senderConfig = {
    .Resolver = SolidSyslogLwipRawResolver_Create(),
    .Stream   = SolidSyslogLwipRawTcpStream_Create(&tcpConfig),
    .Address  = SolidSyslogLwipRawAddress_Create(),
    .Endpoint = CollectorEndpoint,
};
struct SolidSyslogSender* sender = SolidSyslogStreamSender_Create(&senderConfig);

Taken with the sequence number, this completes the loss story: the transport detects loss where it
happens, and the sequence reveals afterwards anything the transport could not. It is also what
makes the delivery-failed and delivery-restored events from the error-handler stage meaningful.

The store is still the Null object, so a record whose send fails is reported but not kept. The
device learns that delivery is failing without yet being able to do anything about it; retaining
the record is the store stage's job.

TCP before TLS is deliberate. It is the smaller step — a stream, a connect and a framing rule, with
no certificates in the picture — and it is what a later store-and-forward stage will spool onto.

The stream takes a Sleep callback because a connect is not instantaneous and the library will not
pick a blocking primitive on your behalf; one vTaskDelay is the whole of it.

When you need it. If the device must know that delivery is failing — to raise an alarm, to fall
back, to start storing. Over UDP it never finds out.

RFC 6587 is Historic, and the IESG recommends TLS over plain TCP for new deployments. Plain TCP
is here for collectors you do not control, and as the step a later storage stage will spool onto,
before cryptography arrives.

@DavidCozens

Copy link
Copy Markdown
Contributor Author

@coderabbitai pause

@coderabbitai

coderabbitai Bot commented Aug 16, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 87b85a10-6904-4c01-a303-f4e316eeea9d

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Note

Reviews paused

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Comment @coderabbitai help to get the list of available commands.

@coderabbitai

coderabbitai Bot commented Aug 16, 2026

Copy link
Copy Markdown
✅ Action performed

Reviews paused.

A StreamSender over an lwIP TCP stream replaces the UDP sender, and the collector
moves to 5601. Records are framed by octet count (RFC 6587), which is what a
receiver expects on a stream transport.

  Flash       +7,348 B  (+544 on the previous stage)
  RAM         +7,664 B    (+180)
  Log stack     +672 B  (unchanged)
  Service       +896 B     (-56)

TCP before TLS is deliberate. It is the smaller step — a stream, a connect and a
framing rule, with no certificates in the picture — and it is what a later
store-and-forward stage will spool onto. It also completes what the sequenceId
started: the transport now detects loss where it happens, while the sequence only
reveals it afterwards.

The store is still the Null object, so a record whose send fails is reported but
not kept. NullStore_Write rejects the record and NullStore_IsTransient says so,
which is Service's cue to fall through to a direct send whose result it discards;
the delivery-failed event still reaches the handler. The device therefore learns
that delivery is failing without yet being able to do anything about it.

The flash is small because the baseline already holds a real mTLS session, so
lwIP's TCP code was in the image before this stage asked for it. What is charged
here is the stream and the sender in front of it, and the 180 bytes of RAM are
those two objects in SolidSyslog's static pool.

The log stack does not move at all: a task that calls Log is unaffected by the
transport underneath it. The service seam's high-water actually fell 56 bytes —
this stream's send path measures shallower than the datagram's — but the seam
keeps its allocation, so nothing is given back here. The stream takes a Sleep
callback because a connect is not instantaneous and the library will not pick a
blocking primitive on your behalf; one vTaskDelay is the whole of it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@DavidCozens
DavidCozens changed the base branch from stage-07-buffered to main August 16, 2026 21:49
@DavidCozens
DavidCozens merged commit a773f1c into main Aug 16, 2026
1 check passed
@DavidCozens
DavidCozens deleted the stage-08-tcp branch August 16, 2026 21:54
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.

1 participant