Skip to content

Scale startup for 10k+ streams via lazy stream indexes, startup manifest bootstrap, and bounded index FDs#337

Draft
albe with Copilot wants to merge 2 commits into
mainfrom
copilot/improve-startup-performance
Draft

Scale startup for 10k+ streams via lazy stream indexes, startup manifest bootstrap, and bounded index FDs#337
albe with Copilot wants to merge 2 commits into
mainfrom
copilot/improve-startup-performance

Conversation

Copilot AI commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

Startup was dominated by eager stream-index opening and full filesystem scans, which does not scale with large stream counts and can exhaust file descriptors. This change decouples stream discovery from index opening, adds a persisted startup manifest fast-path, and enforces LRU limits for open secondary index handles.

  • Lazy stream registration and open-on-first-use

    • EventStore.registerStream() now records stream descriptors instead of opening each stream index during startup.
    • Stream indexes are opened only when actually needed (e.g., version/read/query/commit paths), while _all remains eagerly available.
    • Closed-stream transitions keep read behavior intact while aligning with lazy descriptor flow.
  • Startup manifest fast-path (startupState)

    • Added StartupState with versioned/checksummed payloads and atomic writes.
    • ReadableStorage.open() now attempts bootstrap from a clean manifest and falls back to full scan when state is missing/dirty/inconsistent (including primary-length mismatch).
    • Added dirty/clean lifecycle hooks around layout mutations and state persistence after reconciliation.
  • Generic file-handle pooling + index FD limits

    • Introduced reusable FileHandlePool.
    • Refactored PartitionPool onto the shared abstraction.
    • Added IndexPool and maxOpenIndexes to cap concurrently open secondary index files with LRU eviction.
  • State-sync integration for correctness-sensitive flows

    • Wired state invalidation/persistence through index/partition lifecycle operations, including stream create/delete/close and storage repair/truncate/reindex paths.
    • Preserves optimistic concurrency/query behavior under lazy index opening.
  • Targeted behavior coverage

    • Added specs for:
      • no eager stream-index opening on startup,
      • startup manifest fast-path + dirty fallback,
      • bounded secondary index handles via maxOpenIndexes.
// Example: startup-state + bounded index handles
const store = new EventStore({
  storageDirectory,
  storageConfig: {
    startupState: { enabled: true },
    maxOpenIndexes: 256
  }
});

Comment thread src/Storage/ReadOnlyStorage.js Outdated
Comment thread src/EventStore.js Outdated
Comment thread src/Storage/ReadableStorage.js Outdated
Comment thread src/Storage/ReadableStorage.js
Comment thread src/Storage/ReadableStorage.js Outdated
Comment thread src/Storage/ReadableStorage.js Outdated
Comment thread src/Storage/StartupState.js Outdated
Comment thread src/Storage/WritableStorage.js Outdated
}

assert((typeof matcher === 'object' || typeof matcher === 'function') && matcher !== null, 'Need to specify a matcher.');
this.markStartupStateDirty();

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

@copilot I dislike the heavy spread of those startupState calls, for two reasons:

  • they always come in two and hence need to be both placed correctly to not break the semantics. They do not directly belong to the logic currently executed, but rather are orthogonal side effects that should be executed at some specific points
  • every invocation writes a potentially pretty large JSON file to disk, twice, which very likely outweighs the other step currently taken (e.g. create new partition), i.e. it will trash performance of common paths

Under what circumstance is the dirty mark strictly necessary? Can the state persistence only be invoked when new indexes/partitions are created? Can we avoid the primaryIndexLength check, hence needing to update the state on every document write?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed in 3093427: removed broad startup-state dirty/save pairs from hot paths, introduced withStartupStateMutation(...) for structural changes, and removed the primaryLength check dependency from startup-state bootstrapping.

Comment thread src/EventStore.js
Copilot finished work on behalf of albe July 18, 2026 18:45
Copilot AI requested a review from albe July 18, 2026 18:45
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