Skip to content

fix(mq): keep one tenant's failed queue join from ending ingest for all - #680

Merged
taitelee merged 2 commits into
mainfrom
fix/mq-tenant-join-isolation
Sep 29, 2026
Merged

taitelee merged 2 commits into
mainfrom
fix/mq-tenant-join-isolation

Conversation

@taitelee

Copy link
Copy Markdown
Member

Summary

When a tenant's queue opened at runtime and the ingest worker's consumer could not join it, the broker reported that as the consumer's delivery ending. The worker failed and the process restarted, so one tenant's failure took ingest down for every tenant. The stream hub's consumer failing the same way was only logged, and that tenant's streams got no live rows until a restart.

A queue a consumer cannot join is now left unrecorded as open, whichever consumer it is:

  • SetMaxBytes returns the error and MaxBytes does not report the budget, so the next reload retries the join.
  • The tenant's publishes answer 503 and retry the join too, through the existing pacing for a queue that cannot open (one shared attempt, then refusals for five seconds).
  • No other tenant is affected and nothing restarts.

Unchanged: a consumer that had already joined and whose delivery then ends on its own still fails the worker, and a flat directory whose queue cannot open still refuses boot.

Test plan

  • One tenant's failed join refuses that tenant's publishes, reports nothing on failed, and leaves a second tenant publishing and consuming
  • A later publish or reload joins the queue and the row reach
  • Both cases run for the worker's consumer and for the hub's
  • Existing tests still cover a terminal failure of a joined consumer and the flat boot refusal

Related Issues

Closes #675

Part of #583

@coderabbitai

coderabbitai Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Advanced

Run ID: 9e966532-93e1-4828-9ca3-9ffb9c0fdc77

📥 Commits

Reviewing files that changed from the base of the PR and between 36a8eff and 1ade99c.

📒 Files selected for processing (3)
  • CHANGELOG.md
  • internal/mq/embedded.go
  • internal/mq/embedded_test.go

Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review.

📜 Recent review details
🧰 Additional context used
📓 Path-based instructions (2)
Source excerpt: **`mq/`** — the message-queue boundary: the **only** package that imports NATS/JetStream (Key Design Decision `#20`).

📄 CodeRabbit inference engine (AGENTS.md)

Files:

  • internal/mq/embedded.go
  • internal/mq/embedded_test.go
Source excerpt: **Every code change updates its docs + `CHANGELOG.md` in the same PR** — a code change without its doc update is incomplete ([§Documentation Sync](`#documentation-sync`)).

📄 CodeRabbit inference engine (AGENTS.md)

Files:

  • CHANGELOG.md
🪛 LanguageTool
CHANGELOG.md

[typographical] ~93-~93: Consider using an em dash in dialogues and enumerations.
Context: - **One tenant's failed queue join no lon...

(DASH_RULE)


[style] ~93-~93: Consider using “who” when you are referring to people instead of objects.
Context: ... so the next reload tries the consumers that had not joined again, and the tenant's ...

(THAT_WHO)


[style] ~93-~93: Consider using “who” when you are referring to a person instead of an object.
Context: ...one used to stand in for it. A consumer that had joined and whose delivery then ends...

(THAT_WHO)

🔇 Additional comments (3)
internal/mq/embedded.go (1)

451-452: LGTM!

Also applies to: 454-454, 854-863, 865-871, 925-925

internal/mq/embedded_test.go (1)

654-675: LGTM!

CHANGELOG.md (1)

93-93: LGTM!


📝 Summary

Summary by CodeRabbit

  • Bug Fixes

    • Tenant queues that consumers cannot join are no longer treated as open. Publishes to the affected tenant return 503 until a retry succeeds; other tenants continue operating, and the server does not restart.
    • Failed queue joins are retried during reloads or eligible publish attempts, without triggering the worker failure path.
  • Documentation

    • Updated queue and ingest guidance to reflect tenant-specific failures, retry behavior, and continued service for other tenants.

Walkthrough

Tenant queues are recorded as open only after registered consumers join. Failed joins leave the queue closed to publishes and can be retried by a later publish or reload. Tests and documentation cover tenant-specific failures and continued delivery for other tenants.

Changes

Tenant queue join handling

Layer / File(s) Summary
Queue state and consumer contract
internal/mq/embedded.go, internal/mq/mq.go
Queue state now reflects whether every registered consumer holds the queue. The consumer contract distinguishes join failures from delivery termination.
Join handling and publish retries
internal/mq/embedded.go
Opening and resizing join missing consumers before applying the budget. Reopen and publish paths retry joins; failed joins keep publishes for that tenant closed.
Regression coverage and documented behavior
internal/mq/embedded_test.go, docs/src/content/docs/*, CHANGELOG.md
Tests cover failures and recovery for worker and hub consumers. Documentation describes tenant-scoped failures and retry behavior.

Priority: ➖ Normal

Estimated code review effort: 3 (Moderate) | ~25 minutes

Change: Bug fix · Severity of issue fixed: Medium

Suggested reviewers: ericandrechek

Merge Risk: ⚪ Minimal · up to 1ade9

No established issue remains that should delay merging after normal checks.

Security Architecture Review

Security architecture risk: 🔵 Low · up to 1ade9

The change is designed to contain a failed queue join to one tenant rather than interrupt shared ingestion. No new security exposure was established, but concurrent recovery and production behavior were not fully verified.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • inferred — The intended availability boundary is one tenant’s queue: failed joins leave its publishes refused while other opened queues can continue delivering. The changed tests exercise both worker and hub join failures and a second tenant, but do not establish all production scheduling outcomes.

Trust Boundaries and Controls

  • observed — Publish derives its queue from the topic’s tenant and uses tenant-keyed pacing for failed reopen attempts. The supplied source does not establish who may choose that tenant upstream or who can cause a broker-side join obstruction.

Resilience and Maintainability Implications

  • observed — Recovery tests cover a later publish and a reload joining both consumers after an obstruction is cleared. The reopened-stream test checks resumed delivery but does not assert the failure channel during the watcher-versus-replacement race.
🚥 Pre-merge checks | ✅ 7
✅ Passed checks (7 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The changes directly address issue #675 by isolating runtime queue-join failures and preserving service for other tenants.
Out of Scope Changes check ✅ Passed The code, tests, changelog, and documentation changes are within the stated message-queue reliability objective.
Linked Issues check ✅ Passed The PR satisfies the coding requirements in issue #675. internal/mq/embedded.go records a tenant queue as open only after all registered consumers join. A failed join leaves the queue unrecorded, re…
Out of Scope Changes check ✅ Passed The changes stay within issue #675. Production changes isolate runtime queue-join failures. Tests verify tenant isolation, retry behavior, recovery, and preserved failure paths. Documentation and chan…
Docstring Coverage ✅ Passed Docstring coverage is 87.50% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 8 functions across 3 files. (1 skipped: 1 u…
Title check ✅ Passed The title clearly and concisely describes the main fix: isolating a failed tenant queue join so it does not end ingest for all tenants.
Description check ✅ Passed The description directly explains the queue-join failure, tenant-scoped behavior, retry handling, unchanged failure cases, and test coverage. It is fully related to the changeset.
✨ Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
✨ Simplify code
  • Commit to this branch
  • Create a new PR

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@github-actions github-actions Bot added documentation Improvements or additions to documentation go Pull requests that update go code area/docs Documentation, site/, README labels Sep 29, 2026
@github-actions

github-actions Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

📚 Docs preview is live → https://e31d96d3-wavehouse-docs.wave-rf.workers.dev

  • Commit — 1ade99c: fix(mq): restart delivery on a recreated ingest stream
  • Author — @taitelee
  • Committed — 2026-09-29 10:35 (UTC-04:00)
  • Deployed — 2026-09-29 11:42 EDT

@github-code-quality

github-code-quality Bot commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

Code Coverage Overview

Languages: Go

Go

The overall line coverage in commit 1ade99c in the fix/mq-tenant-join-i... branch remains at 94%, unchanged from commit 4ff5074 in the main branch.

Show a line coverage summary of the most impacted files.
File main 4ff5074 fix/mq-tenant-join-i... 1ade99c +/-
internal/mq/embedded.go 89% 90% +1%

Updated September 29, 2026 15:42 UTC

@taitelee

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Sep 29, 2026 •

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

coderabbitai[bot]
coderabbitai Bot previously requested changes Sep 29, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Advanced

Run ID: bb83d68e-c7f8-4a50-8b0b-c99d6555c788

📥 Commits

Reviewing files that changed from the base of the PR and between 4ff5074 and 36a8eff.

📒 Files selected for processing (8)
  • CHANGELOG.md
  • docs/src/content/docs/architecture.md
  • docs/src/content/docs/durability.md
  • docs/src/content/docs/ingest-pipeline.md
  • docs/src/content/docs/settings-directory.mdx
  • internal/mq/embedded.go
  • internal/mq/embedded_test.go
  • internal/mq/mq.go

Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review.

📜 Review details
🧰 Additional context used
📓 Path-based instructions (4)
Source excerpt: **WH001 applies to every tracked Markdown file, with no carve-out** — `AGENTS.md`, `CHANGELOG.md`, `.github/` CI docs and `.claude/` agent prompts included.

📄 CodeRabbit inference engine (AGENTS.md)

Files:

  • docs/src/content/docs/durability.md
  • docs/src/content/docs/ingest-pipeline.md
  • docs/src/content/docs/architecture.md
Source excerpt: **`mq/`** — the message-queue boundary: the **only** package that imports NATS/JetStream (Key Design Decision `#20`).

📄 CodeRabbit inference engine (AGENTS.md)

Files:

  • internal/mq/mq.go
  • internal/mq/embedded_test.go
  • internal/mq/embedded.go
Source excerpt: **Every code change updates its docs + `CHANGELOG.md` in the same PR** — a code change without its doc update is incomplete ([§Documentation Sync](`#documentation-sync`)).

📄 CodeRabbit inference engine (AGENTS.md)

Files:

  • CHANGELOG.md
Source excerpt: **In MDX, leave a blank line between a JSX tag and a code fence.**

📄 CodeRabbit inference engine (AGENTS.md)

Files:

  • docs/src/content/docs/settings-directory.mdx
🪛 LanguageTool
docs/src/content/docs/durability.md

[style] ~105-~105: Consider using the typographical ellipsis character here instead.
Context: ...settings; the next reload retrieswithjoin the queue: ... context deadline exceeded`, and that te...

(ELLIPSIS)

CHANGELOG.md

[typographical] ~93-~93: Consider using an em dash in dialogues and enumerations.
Context: - **One tenant's failed queue join no lon...

(DASH_RULE)


[style] ~93-~93: Consider using “who” when you are referring to people instead of objects.
Context: ... so the next reload tries the consumers that had not joined again, and the tenant's ...

(THAT_WHO)


[style] ~93-~93: Consider using “who” when you are referring to a person instead of an object.
Context: ...uched, and nothing restarts. A consumer that had joined and whose delivery then ends...

(THAT_WHO)

docs/src/content/docs/settings-directory.mdx

[typographical] ~227-~227: Consider using an em dash in dialogues and enumerations.
Context: - mq.max_bytes_gb (seed default 50) —...

(DASH_RULE)

docs/src/content/docs/ingest-pipeline.md

[style] ~206-~206: Consider using “who” when you are referring to people instead of objects.
Context: ...rker's own failed channel. A consumer that cannot start at all takes the same path...

(THAT_WHO)


[style] ~208-~208: To elevate your writing, try using a synonym here.
Context: ...at could delete a durable) this path is hard to reach. A tenant's queue, opened at r...

(HARD_TO)

docs/src/content/docs/architecture.md

[typographical] ~173-~173: Consider using an em dash in dialogues and enumerations.
Context: - mq.go — The owned surface, stated a...

(DASH_RULE)


[style] ~173-~173: Since ownership is already implied, this phrasing may be redundant.
Context: ...o a blocking handler is backpressure on its own tenant alone, spreads the prefetch acro...

(PRP_OWN)


[style] ~173-~173: A comma is missing here.
Context: ...ending on its own — ErrDeliveryEnded, e.g. a deleted consumer or a closed connecti...

(EG_NO_COMMA)


[style] ~173-~173: Since ownership is already implied, this phrasing may be redundant.
Context: ...terer.DeadLetter` (park a message under its own topic, in its tenant's dead-letter queu...

(PRP_OWN)


[style] ~177-~177: ‘out of reach’ might be wordy. Consider a shorter alternative.
Context: ...75% of the free disk by default) is set out of reach, so a budget is a cap and never a reser...

(EN_WORDINESS_PREMIUM_OUT_OF_REACH)


[style] ~177-~177: Consider using “who” when you are referring to people instead of objects.
Context: ... so the next reload tries the consumers that had not joined again. A publish or park...

(THAT_WHO)


[style] ~177-~177: For a more expressive style, consider rephrasing the sentence in the active voice.
Context: ...rts the budget last applied in full, so a failed resize is retried by the next reload. The consumers CreateConsumer and `Su...

(PASSIVE_VOICE_SIMPLE)

Comment thread internal/mq/embedded.go Outdated
@taitelee

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Sep 29, 2026 •

Copy link
Copy Markdown
⚠️ Action not completed

Review rate limited.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@taitelee

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Sep 29, 2026 •

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@taitelee
taitelee marked this pull request as ready for review September 29, 2026 15:38
@taitelee
taitelee requested review from a team and EricAndrechek September 29, 2026 15:38
@taitelee
taitelee merged commit 0de28ad into main Sep 29, 2026
32 checks passed
@taitelee
taitelee deleted the fix/mq-tenant-join-isolation branch September 29, 2026 16:05
EricAndrechek added a commit that referenced this pull request Sep 29, 2026
Brings in #614 (shared redis cache), #625 (dedupe reserve/commit,
dynamodb, dedupe.lease, WithIdempotencyKey), #680 (a failed queue join
is the tenant's alone) and #623's squash, whose content the stack
already had from its PR head.

Resolutions:
- config: Warnings keeps the nats warnings for every role and main's
  api-only cache/redis/dedupe ones; the unknown-backend test lists
  nats beside redis and dynamodb.
- mq: main's WithIdempotencyKey, ErrUnavailable and Consume contracts;
  the conformance suite is main's, idempotency case included.
- testutil: main's storedir replaces this stack's StoreDir.
- ingest: main's publishFailed already answers ErrUnavailable with 503.
- integration setup keeps startNATS beside startDynamoDBLocal; the
  Makefile runs internal/cache with the tagged suites as main does.
- docs and CHANGELOG keep both sides; the backend lists name nats,
  redis and dynamodb together.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FyrXjhR7iDg33paioLQHFq
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/docs Documentation, site/, README documentation Improvements or additions to documentation go Pull requests that update go code

Projects

Archived in project

Development

Successfully merging this pull request may close these issues.

fix(mq): one tenant's failed queue join ends ingest delivery for every tenant and restarts the process

1 participant