Skip to content

feat: add orphan stream cleanup to remove DB streams not in registry#4850

Open
bas-vk wants to merge 1 commit into
mainfrom
basvk/compare_streamd_db_stream_registry
Open

feat: add orphan stream cleanup to remove DB streams not in registry#4850
bas-vk wants to merge 1 commit into
mainfrom
basvk/compare_streamd_db_stream_registry

Conversation

@bas-vk

@bas-vk bas-vk commented Jan 9, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Adds CleanupOrphanedStreams function in storage package that identifies and deletes streams from the database that are no longer registered on this node
  • Integrates cleanup into StreamCache.Start after loading registry streams
  • Includes comprehensive test coverage

Test plan

  • Unit tests pass (go test ./node/storage/... -run TestCleanupOrphanedStreams)
  • Manual testing with local multi-node setup

🤖 Generated with Claude Code

@vercel

vercel Bot commented Jan 9, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Review Updated (UTC)
river-sample-app Ready Ready Preview, Comment Jan 9, 2026 2:08pm

Adds CleanupOrphanedStreams function that identifies and deletes streams
from the database that are no longer registered on this node in the
stream registry. Called during StreamCache.Start after loading registry
streams.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@bas-vk bas-vk force-pushed the basvk/compare_streamd_db_stream_registry branch from 0be7e63 to 5a0ef52 Compare January 9, 2026 14:08
@bas-vk bas-vk marked this pull request as ready for review January 9, 2026 14:49
@bas-vk bas-vk requested review from sergekh2 and texuf as code owners January 9, 2026 14:49
@coderabbitai

coderabbitai Bot commented Jan 9, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

This PR introduces orphaned stream cleanup functionality. A new CleanupOrphanedStreams function removes database streams not present in the registry, invoked after block updates in the cache module. Documentation clarifies the deletion strategy to avoid long-running operations in the event loop.

Changes

Cohort / File(s) Summary
Storage layer implementation
core/node/storage/orphan_stream_cleaner.go
New function CleanupOrphanedStreams accepts context, PostgresStreamStore, and registry streams; retrieves DB streams, deletes those absent from registry, handles errors with early exit on context cancellation, logs results.
Storage layer tests
core/node/storage/orphan_stream_cleaner_test.go
Test suite covering deletion of orphaned streams, preservation of tracked streams, empty states, and boundary conditions using helper makeRegistryResults.
Event loop integration
core/node/events/stream_cache.go
Calls CleanupOrphanedStreams post block-update when Storage is PostgresStreamStore.
Event loop documentation
core/node/events/stream_ephemeral.go
Comment explaining deletion of untracked streams occurs at boot to avoid main-loop DB latency.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested reviewers

  • sergekh2
  • texuf
🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 14.29% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title directly and accurately summarizes the main change: adding orphan stream cleanup functionality to remove database streams not in the registry.
Description check ✅ Passed The description is related to the changeset, covering the new function, integration point, and test coverage mentioned in the code summary.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch basvk/compare_streamd_db_stream_registry

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 and usage tips.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In @core/node/storage/orphan_stream_cleaner.go:
- Around line 17-67: CleanupOrphanedStreams performs multiple store.DeleteStream
calls without a DB transaction, risking partial deletes on context cancel;
change CleanupOrphanedStreams to start a PostgreSQL transaction (use the store's
Begin/WithTx/RunInTx pattern), perform all DeleteStream calls within that
transaction, and Commit only if all deletes succeed (Rollback on error or ctx
cancellation), return an error from CleanupOrphanedStreams instead of void, and
update callers (e.g., stream_cache.go) to check the returned error and log it
(e.g., "Orphan stream cleanup failed", "error", err).
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 42bc668 and 5a0ef52.

📒 Files selected for processing (4)
  • core/node/events/stream_cache.go
  • core/node/events/stream_ephemeral.go
  • core/node/storage/orphan_stream_cleaner.go
  • core/node/storage/orphan_stream_cleaner_test.go
🧰 Additional context used
📓 Path-based instructions (7)
core/**/*.go

📄 CodeRabbit inference engine (core/AGENTS.md)

core/**/*.go: Format all modified Go files using ./fmt.sh from the /core directory before committing
Run the linter using ./lint.sh from the /core directory to check code quality, which runs both golangci-lint and staticcheck
Use RiverError type from node/base/error.go for structured errors
Use RiverErrorWithBase to construct a river error derived from an error of arbitrary or unknown type
Wrap blockchain errors with context using crypto/ utilities
Log errors with structured fields using zap logger
Use node/crypto/blockchain.go for Ethereum client operations
Use node/crypto/chain_monitor.go for transaction monitoring
Use xchain/entitlement/ for cross-chain entitlement checks
Do not run ./fmt.sh and ./lint.sh at the same time; run them as separate commands
Use go.uber.org/zap for structured logging with custom extensions from node/logging/
Use go.opentelemetry.io for distributed tracing across the application
Use prometheus for OpenMetrics in application metrics collection

Files:

  • core/node/events/stream_cache.go
  • core/node/storage/orphan_stream_cleaner_test.go
  • core/node/storage/orphan_stream_cleaner.go
  • core/node/events/stream_ephemeral.go
**/core/**/*.go

📄 CodeRabbit inference engine (AGENTS.md)

**/core/**/*.go: Format all Go files using the fmt.sh script from the /core directory
Run the linter using the lint.sh script from the /core directory for all Go code changes
Use structured logging with zap for Go development
Implement proper error handling with RiverError types in Go code
Implement proper context cancellation for graceful shutdowns in Go code

Files:

  • core/node/events/stream_cache.go
  • core/node/storage/orphan_stream_cleaner_test.go
  • core/node/storage/orphan_stream_cleaner.go
  • core/node/events/stream_ephemeral.go
**/core/node/**/*.go

📄 CodeRabbit inference engine (AGENTS.md)

Use structured logging with zap in Go backend River nodes for observability

Files:

  • core/node/events/stream_cache.go
  • core/node/storage/orphan_stream_cleaner_test.go
  • core/node/storage/orphan_stream_cleaner.go
  • core/node/events/stream_ephemeral.go
core/**/*_test.go

📄 CodeRabbit inference engine (core/AGENTS.md)

core/**/*_test.go: Use testify/require and testify/assert for test assertions in Go test files
Database tests should use testutils/dbtestutils for test DB lifecycle management
Do not set GOCACHE or GOMODCACHE when running Go test commands; run go commands directly without setting extra environment variables
Set RIVER_TEST_LOG=info and RIVER_TEST_PRINT=1 environment variables for debug output in tests
Mock implementations should be used from testutils/mocks/ in tests

Files:

  • core/node/storage/orphan_stream_cleaner_test.go
core/node/storage/**/*.go

📄 CodeRabbit inference engine (core/AGENTS.md)

core/node/storage/**/*.go: Use PostgreSQL transactions in node/storage/ for all database operations to ensure consistency
Use jackc/pgx/v5 for PostgreSQL connection pooling and database driver operations

Files:

  • core/node/storage/orphan_stream_cleaner_test.go
  • core/node/storage/orphan_stream_cleaner.go
**/core/**/*_test.go

📄 CodeRabbit inference engine (AGENTS.md)

**/core/**/*_test.go: Use testify for test assertions in Go tests
Make tests re-runnable by using unique identifiers (e.g., UUIDs) instead of hardcoded values

Files:

  • core/node/storage/orphan_stream_cleaner_test.go
**/core/node/storage/**/*_test.go

📄 CodeRabbit inference engine (AGENTS.md)

Storage tests should verify data persistence and constraints for database schema changes

Files:

  • core/node/storage/orphan_stream_cleaner_test.go
🧠 Learnings (18)
📚 Learning: 2025-11-25T08:48:08.084Z
Learnt from: CR
Repo: towns-protocol/towns PR: 0
File: core/node/storage/AGENTS.md:0-0
Timestamp: 2025-11-25T08:48:08.084Z
Learning: Applies to core/node/storage/storage/**/*.go : Enforce miniblock requirement constraint: if a stream exists in the `es` (event streams) table, it MUST have at least one miniblock; empty streams are not allowed

Applied to files:

  • core/node/events/stream_cache.go
  • core/node/storage/orphan_stream_cleaner.go
  • core/node/events/stream_ephemeral.go
📚 Learning: 2025-11-25T08:48:08.084Z
Learnt from: CR
Repo: towns-protocol/towns PR: 0
File: core/node/storage/AGENTS.md:0-0
Timestamp: 2025-11-25T08:48:08.084Z
Learning: Applies to core/node/storage/storage/**/*.go : Always include stream ID in queries to leverage table partitioning (256 partitions by default)

Applied to files:

  • core/node/events/stream_cache.go
  • core/node/storage/orphan_stream_cleaner_test.go
  • core/node/storage/orphan_stream_cleaner.go
  • core/node/events/stream_ephemeral.go
📚 Learning: 2025-11-25T08:48:08.084Z
Learnt from: CR
Repo: towns-protocol/towns PR: 0
File: core/node/storage/AGENTS.md:0-0
Timestamp: 2025-11-25T08:48:08.084Z
Learning: Applies to core/node/storage/storage/**/*.go : Use `lockStream(ctx, tx, streamId, true)` with `FOR UPDATE` for write operations and `lockStream(ctx, tx, streamId, false)` with `FOR SHARE` for read operations to ensure proper row-level locking

Applied to files:

  • core/node/events/stream_cache.go
  • core/node/storage/orphan_stream_cleaner.go
  • core/node/events/stream_ephemeral.go
📚 Learning: 2025-12-17T02:40:57.825Z
Learnt from: CR
Repo: towns-protocol/towns PR: 0
File: core/AGENTS.md:0-0
Timestamp: 2025-12-17T02:40:57.825Z
Learning: Applies to core/node/storage/**/*.go : Use PostgreSQL transactions in node/storage/ for all database operations to ensure consistency

Applied to files:

  • core/node/events/stream_cache.go
  • core/node/storage/orphan_stream_cleaner.go
📚 Learning: 2025-12-17T02:40:57.825Z
Learnt from: CR
Repo: towns-protocol/towns PR: 0
File: core/AGENTS.md:0-0
Timestamp: 2025-12-17T02:40:57.825Z
Learning: Applies to core/node/storage/pg_*.go : All storage operations should be in node/storage/pg_*.go files

Applied to files:

  • core/node/events/stream_cache.go
  • core/node/storage/orphan_stream_cleaner.go
📚 Learning: 2025-11-25T08:48:08.084Z
Learnt from: CR
Repo: towns-protocol/towns PR: 0
File: core/node/storage/AGENTS.md:0-0
Timestamp: 2025-11-25T08:48:08.084Z
Learning: Applies to core/node/storage/storage/**/*_test.go : All storage tests should follow the consistent setup pattern using helper functions: `setupStreamStorageTest(t)`, `setupAppRegistryStorageTest(t)`, or `prepareNotificationsDB(ctx)` depending on storage type, with automatic cleanup via `t.Cleanup(params.closer)`

Applied to files:

  • core/node/events/stream_cache.go
  • core/node/storage/orphan_stream_cleaner_test.go
  • core/node/storage/orphan_stream_cleaner.go
📚 Learning: 2025-12-17T02:40:57.825Z
Learnt from: CR
Repo: towns-protocol/towns PR: 0
File: core/AGENTS.md:0-0
Timestamp: 2025-12-17T02:40:57.825Z
Learning: Applies to core/node/storage/pg_*.go : Use PostgreSQL transactions for consistency in all storage operations

Applied to files:

  • core/node/events/stream_cache.go
  • core/node/storage/orphan_stream_cleaner.go
📚 Learning: 2025-11-25T08:48:08.084Z
Learnt from: CR
Repo: towns-protocol/towns PR: 0
File: core/node/storage/AGENTS.md:0-0
Timestamp: 2025-11-25T08:48:08.084Z
Learning: Applies to core/node/storage/storage/**/*.go : Use `COPY FROM` for atomic batch miniblock writes instead of individual insert statements in loops

Applied to files:

  • core/node/events/stream_cache.go
  • core/node/storage/orphan_stream_cleaner.go
📚 Learning: 2026-01-08T18:27:30.866Z
Learnt from: CR
Repo: towns-protocol/towns PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-08T18:27:30.866Z
Learning: Applies to **/core/node/storage/**/*_test.go : Storage tests should verify data persistence and constraints for database schema changes

Applied to files:

  • core/node/storage/orphan_stream_cleaner_test.go
📚 Learning: 2025-11-25T08:48:08.084Z
Learnt from: CR
Repo: towns-protocol/towns PR: 0
File: core/node/storage/AGENTS.md:0-0
Timestamp: 2025-11-25T08:48:08.084Z
Learning: Applies to core/node/storage/storage/**/*_test.go : Use standard imports in storage tests: context, testing, github.com/ethereum/go-ethereum/common, github.com/stretchr/testify/require, and relevant River protocol packages

Applied to files:

  • core/node/storage/orphan_stream_cleaner_test.go
  • core/node/storage/orphan_stream_cleaner.go
📚 Learning: 2025-11-25T08:48:08.084Z
Learnt from: CR
Repo: towns-protocol/towns PR: 0
File: core/node/storage/AGENTS.md:0-0
Timestamp: 2025-11-25T08:48:08.084Z
Learning: Applies to core/node/storage/storage/**/*_test.go : Use helper functions to generate test data consistently: `safeAddress(t)` for Ethereum addresses, `testAppMetadataWithName(name string)` for app metadata, `testutils.FakeStreamId(streamType)` for stream IDs

Applied to files:

  • core/node/storage/orphan_stream_cleaner_test.go
📚 Learning: 2025-11-25T08:48:08.084Z
Learnt from: CR
Repo: towns-protocol/towns PR: 0
File: core/node/storage/AGENTS.md:0-0
Timestamp: 2025-11-25T08:48:08.084Z
Learning: Applies to core/node/storage/storage/**/*_test.go : Implement table-driven tests when testing multiple scenarios to improve maintainability and coverage

Applied to files:

  • core/node/storage/orphan_stream_cleaner_test.go
📚 Learning: 2025-11-25T08:48:08.084Z
Learnt from: CR
Repo: towns-protocol/towns PR: 0
File: core/node/storage/AGENTS.md:0-0
Timestamp: 2025-11-25T08:48:08.084Z
Learning: Applies to core/node/storage/storage/**/*_test.go : Always test edge cases including: empty/nil values, non-existent entities (NOT_FOUND errors), duplicate entries (ALREADY_EXISTS errors), concurrent operations, and transaction rollbacks

Applied to files:

  • core/node/storage/orphan_stream_cleaner_test.go
📚 Learning: 2025-11-25T08:48:08.084Z
Learnt from: CR
Repo: towns-protocol/towns PR: 0
File: core/node/storage/AGENTS.md:0-0
Timestamp: 2025-11-25T08:48:08.084Z
Learning: Applies to core/node/storage/storage/**/*_test.go : Use `require.New(t)` at the beginning of test functions and check error codes with `base.IsRiverErrorCode(err, code)` for specific error type verification

Applied to files:

  • core/node/storage/orphan_stream_cleaner_test.go
📚 Learning: 2025-11-25T08:47:37.754Z
Learnt from: CR
Repo: towns-protocol/towns PR: 0
File: core/node/rpc/AGENTS.md:0-0
Timestamp: 2025-11-25T08:47:37.754Z
Learning: Applies to core/node/rpc/**/*_test.go : Use `t.Cleanup()` or defer with tester cleanup functions to handle automatic cleanup and resource management

Applied to files:

  • core/node/storage/orphan_stream_cleaner_test.go
📚 Learning: 2025-11-25T08:47:37.754Z
Learnt from: CR
Repo: towns-protocol/towns PR: 0
File: core/node/rpc/AGENTS.md:0-0
Timestamp: 2025-11-25T08:47:37.754Z
Learning: Applies to core/node/rpc/**/*_test.go : Enable River logs with `enableRiverLogs: true` in tester options when debugging test failures

Applied to files:

  • core/node/storage/orphan_stream_cleaner_test.go
📚 Learning: 2025-11-25T08:48:08.084Z
Learnt from: CR
Repo: towns-protocol/towns PR: 0
File: core/node/storage/AGENTS.md:0-0
Timestamp: 2025-11-25T08:48:08.084Z
Learning: Applies to core/node/storage/storage/**/*_test.go : Use `ConfigureDB(ctx)` with default prefix 'tst', or `ConfigureDbWithPrefix(ctx, 'b_')` for app registry tests in the dbtestutils package

Applied to files:

  • core/node/storage/orphan_stream_cleaner_test.go
📚 Learning: 2025-11-25T08:48:08.084Z
Learnt from: CR
Repo: towns-protocol/towns PR: 0
File: core/node/storage/AGENTS.md:0-0
Timestamp: 2025-11-25T08:48:08.084Z
Learning: Applies to core/node/storage/storage/**/*.go : Add transaction tags for debugging in txRunner calls to improve transaction tracking and monitoring

Applied to files:

  • core/node/storage/orphan_stream_cleaner.go
🧬 Code graph analysis (3)
core/node/events/stream_cache.go (2)
core/node/storage/pg_stream_store.go (1)
  • PostgresStreamStore (41-67)
core/node/storage/orphan_stream_cleaner.go (1)
  • CleanupOrphanedStreams (17-67)
core/node/storage/orphan_stream_cleaner_test.go (2)
core/node/shared/stream_id.go (2)
  • StreamId (41-41)
  • STREAM_CHANNEL_BIN (16-16)
core/node/storage/orphan_stream_cleaner.go (1)
  • CleanupOrphanedStreams (17-67)
core/node/storage/orphan_stream_cleaner.go (2)
core/node/storage/pg_stream_store.go (1)
  • PostgresStreamStore (41-67)
core/node/shared/stream_id.go (1)
  • StreamId (41-41)
🔇 Additional comments (5)
core/node/events/stream_ephemeral.go (1)

64-67: LGTM! Clear documentation of deletion strategy.

The comment accurately explains the design decision to defer DB cleanup to boot time rather than the event loop.

core/node/events/stream_cache.go (1)

191-194: Handle cleanup errors once function signature is updated.

After CleanupOrphanedStreams returns an error (per the critical issue flagged in orphan_stream_cleaner.go), add error handling here to log failures.

core/node/storage/orphan_stream_cleaner_test.go (2)

13-19: LGTM! Clean helper for registry test data.

The helper simplifies test setup and is reused across all test cases.


21-140: LGTM! Comprehensive test coverage.

The test suite covers all critical scenarios: partial orphans, no orphans, empty database, all orphaned, and both empty. Tests follow the established pattern with setupStreamStorageTest and proper assertions.

core/node/storage/orphan_stream_cleaner.go (1)

42-50: DeleteStream properly uses row-level locking for safe concurrent operations.

The implementation acquires an exclusive lock with FOR UPDATE on the es table via lockStream(ctx, tx, streamId, true) before deletion, ensuring concurrent modifications are safely serialized. The function's behavior with concurrent operations (including newly added or concurrently updated streams) is protected by this transaction-level locking mechanism.

Note: The TOCTOU window between collecting DB streams and deleting orphaned streams is acknowledged in the function's documentation and is acceptable per design—placement updates emitted after cleanup returns can recreate streams if needed.

Comment thread core/node/storage/orphan_stream_cleaner.go
// is collected and this function returns.
func CleanupOrphanedStreams(
ctx context.Context,
store *PostgresStreamStore,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Idiomatically it seems it should be a method on PostgresStreamStore and interface - then there is no need to cast in steam_cache.go

)
failed++
} else {
log.Debugw("Deleted orphaned stream", "streamId", dbStreamId)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

let log all deletions at Info

s.appliedBlockNum.Store(uint64(s.params.AppliedBlockNum))

// delete streams from the DB that are no longer placed on this node
if store, ok := s.params.Storage.(*storage.PostgresStreamStore); ok {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

lets add a config setting to disable it - if node takes too long to start, operator can disable this functionality and start the node without it.

) {
start := time.Now()
log := logging.FromCtx(ctx)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

let log here and mention env var for disable setting.

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.

2 participants