Skip to content

journal: OutboxRelay throws NullSinkError instead of crashing on a null sink (closes morph#95) - #111

Merged
Yaraslaut merged 1 commit into
masterfrom
fix/95-outbox-null-sink-seam
Aug 16, 2026
Merged

journal: OutboxRelay throws NullSinkError instead of crashing on a null sink (closes morph#95)#111
Yaraslaut merged 1 commit into
masterfrom
fix/95-outbox-null-sink-seam

Conversation

@Yaraslaut

Copy link
Copy Markdown
Member

Closes #95: OutboxRelay::relay()'s null-sink path
used to be real UB (a null-pointer virtual dispatch through
sink->append(), confirmed as a process crash) with no seam to test it
portably. Fixes it directly per the issue's first proposed option
(a null sink throws a catchable exception), rather than adding an
observability-only hook that leaves the crash in place.

The fix

relay() now checks sink for null immediately after
logIfAnyDepNull() (which already logs the warning) and before any row
ever reaches sink->append(), throwing a new NullSinkError (a
std::runtime_error, matching this codebase's established
<Domain>Error convention -- SerializationError, FileOfflineQueueError,
ParseError, etc.):

struct NullSinkError : std::runtime_error {
    using std::runtime_error::runtime_error;
};

A null drainOutbox/markRelayed already throws a catchable
std::bad_function_call (invoking a null std::function) -- this makes
sink consistent with that instead of the one member that used to
crash instead of throw. An empty drainOutbox() with a null sink is
still a no-op, unchanged: nothing would touch sink either way.

Deliberate, documented behavior change

Per the issue's own explicit caution ("this changes documented
behavior... needs a deliberate decision, not a silent patch"):
docs/spec/journal/journal.md's OutboxRelay contract section is
updated to state the new throw behavior, replacing "invoking a null
member still throws (std::bad_function_call) or crashes as usual".

Tests

Replaces the block comment in test_outbox.cpp explaining why the
null-sink-reaching-append() case couldn't be tested with a real test:
OutboxRelay::relay(): a null sink reaching a non-empty drain throws NullSinkError, not UB drains one real row with sink still null,
asserts NullSinkError is thrown, and asserts the null-sink warning
still fired.

Verification

Full morph_tests suite: 1047 test cases, 10040 assertions, all
passing (up from 1046/10038 on master). Doxygen doc build
(WARN_AS_ERROR=FAIL_ON_WARNINGS, per CLAUDE.md's CI notes) passes
clean for the new NullSinkError type.

🤖 Generated with Claude Code

…g on a null sink (morph#95)

A null sink reaching relay()'s sink-using path (drainOutbox() reporting
at least one row) used to be a null-pointer virtual dispatch through
sink->append() -- real UB, confirmed as a process crash, not a
catchable exception. A null drainOutbox/markRelayed already throws a
catchable std::bad_function_call; this makes sink consistent with
that instead of the odd one out.

relay() now checks sink for null right after logIfAnyDepNull() (which
already logged the null-sink warning) and before any row ever reaches
sink->append(), throwing the new NullSinkError (a std::runtime_error,
matching this codebase's established <Domain>Error convention --
SerializationError, FileOfflineQueueError, ParseError, etc.). An empty
drainOutbox() with a null sink is still a no-op, unchanged: nothing
would touch sink either way.

This is a deliberate, documented behavior change (per the issue's own
explicit caution against silently patching this) -- docs/spec/journal/
journal.md's OutboxRelay contract is updated to match. The one
previously-untestable branch (a null sink actually reaching
sink->append) now has a real test instead of a comment explaining why
it couldn't be tested.

Full suite: 1047 test cases, 10040 assertions, all passing. Doxygen
doc build (WARN_AS_ERROR=FAIL_ON_WARNINGS) passes clean for the new
NullSinkError type.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@codecov

codecov Bot commented Aug 16, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@Yaraslaut
Yaraslaut merged commit 08c7f22 into master Aug 16, 2026
26 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

journal::OutboxRelay: no seam to test the null-sink branch without a process-crashing UB call

1 participant