Repository navigation
GH-4482 follow-up: assert the external table drains, make the store guard reachable, fix the docs - #4578
Merged
Conversation
…uard reachable, fix the docs The last four findings from the #4558 review. ### The compliance suite never checked that the source rows were deleted TestMessageSend asserted only the received envelope's Destination. A deleteManyAsync that quietly matched no rows would still commit, still call ReceivedAsync, and still go green -- while the row sat in the table and was redelivered on every subsequent poll, forever. That is the exact failure mode worth guarding on a transport whose whole job is to drain a table, and it was invisible. The poller deletes in the same transaction that stores to the inbox and only calls ReceivedAsync after that commits, so the assertion needs no polling: once the tracked session is done, the table must be empty. Counting rows needs one provider-specific detail, so qualifiedTableNameFor is virtual. SQLite overrides it because SQLite has no schemas -- Wolverine folds the schema name into a table-name prefix there (TablePrefixing, GH-3943). ### The guard for a non-relational message store was unreachable SendMessageThroughExternalTable null checked the result of JasperFx's As<T>(), which is documented as a hard cast and throws InvalidCastException. The branch could never run, so a RavenDb/Cosmos/multi-tenanted store got a bare InvalidCastException instead of the explanation that was written for it. Now an `is not` pattern, throwing InvalidOperationException to match the equivalent guards in ExternalMessageTable.BuildListenerAsync and ExternalMessageTableListener. ### The docs advertised an API that does not exist The sample told readers to call UseOraclePersistenceAndTransport. There is no such method -- Oracle has no combined helper, and opts into its database queue transport fluently off PersistMessagesWithOracle instead. ### external-tables.md was hand edited rather than generated The snippet footer still pointed at #L233-L295 for a region that had moved to #L44-L112, and hand-added blank lines sat around the code fence. Regenerated with mdsnippets. Only this one file is included: the tool rewrites 89 docs files repo-wide, and committing the other 88 here would collide with every concurrent branch for no benefit. Verified: pinned `wolverine.slnx -c Release -f net9.0` clean (0 warnings), and external_message_tables green on all five providers (6 tests each). Both new assertions were negative-controlled. Making the PostgreSQL delete match zero rows fails all three end-to-end tests with "should have been drained" while the three migration tests still pass -- which is precisely the hole. Restoring the As<T>() form makes the guard test fail with InvalidCastException, confirming that is what callers actually got. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This was referenced Sep 24, 2026
Merged
This was referenced Sep 24, 2026
This was referenced Oct 1, 2026
This was referenced Oct 8, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The last four findings from the #4558 review. Independent of #4574 and #4577 — I verified all three branches merge cleanly against
mainand against each other, so they can land in any order.1. The compliance suite never checked that the source rows were deleted
TestMessageSendasserted only the received envelope'sDestination. AdeleteManyAsyncthat quietly matched no rows would still commit, still callReceivedAsync, and still go green — while the row sat in the table and was redelivered on every subsequent poll, forever.That is the exact failure mode worth guarding on a transport whose whole job is to drain a table, and nothing was watching it. It matters most for the code #4558 introduced: Oracle's hand-rolled
RAW(16)IN-list delete and SQLite'slower()literal comparison.No polling is needed for the assertion — the poller deletes in the same transaction that stores to the inbox and only calls
ReceivedAsyncafter that commits, so once the tracked session is done the table must be empty.Counting rows needs one provider-specific detail, so
qualifiedTableNameForis virtual. SQLite overrides it, because SQLite has no schemas — Wolverine folds the schema name into a table-name prefix there (TablePrefixing, GH-3943).2. The guard for a non-relational message store was unreachable
SendMessageThroughExternalTablenull-checked the result of JasperFx'sAs<T>(), which is documented as "Does a hard cast... Will throw InvalidCastException". The branch could never run, so a RavenDb / Cosmos / multi-tenanted store got a bareInvalidCastExceptioninstead of the explanation written for it.Now an
is notpattern, throwingInvalidOperationExceptionto match the equivalent guards inExternalMessageTable.BuildListenerAsyncandExternalMessageTableListener. Changing the exception type is safe precisely because the old one was unreachable — nothing can be catching it.3. The docs advertised an API that does not exist
The sample told readers to call
UseOraclePersistenceAndTransport. There is no such method: Oracle has no combined helper and opts into its database queue transport fluently offPersistMessagesWithOracleinstead. Corrected, with a note explaining the difference.4.
external-tables.mdwas hand-edited rather than generatedThe snippet footer still pointed at
#L233-L295for a region that had moved to#L44-L112, and hand-added blank lines sat around the code fence. Regenerated withmdsnippets.Only this one file is included. The tool rewrites 89 docs files repo-wide — the docs are broadly stale against source — and sweeping the other 88 in here would collide with every concurrent branch for no benefit. That backlog is worth a separate, deliberate pass.
Verification
dotnet build wolverine.slnx -c Release -f net9.0— clean, 0 warnings, 0 errors.external_message_tablesgreen on all five providers, 6 tests each.external_table_store_guardtest green.Both new assertions were negative-controlled:
and 1=0) fails all three end-to-end tests with"should have been drained"while the three migration tests still pass — precisely the hole, and confirmation the assertion is not decorative.As<T>()form makes the guard test fail withInvalidCastException, confirming that is what callers actually got.With this, every finding from the #4558 review is addressed across #4574, #4577 and this PR.
🤖 Generated with Claude Code