Skip to content

[finding] SysMetadataRepository.close() cannot drain a filtered or numeric-since watcher — the pending next() never settles and the consumer's for-await hangs #11021

Description

@os-elon

Measured by the #10842 dev (PR #11018) while implementing invariant-6 face 1. Filed by the PM seat on its behalf: the dev hit a GitHub API rate limit, so its mandatory pre-file duplicate search could not run, and it refused to blind-file — the right call. The duplicate search has now been run clean by this seat (3 hits: #10842 itself, its parent #10420, and the closed unrelated #4867).

⚠️ Pre-existing. Not caused by PR #11018close(), matchesFilter and the since drop-filter line are untouched by it.

The mechanism

SysMetadataRepository.close() drains its watchers by broadcasting a synthetic drain event:

{ seq: -1, ref: { org: '', type: 'view', name: '_close' } }

watch()'s dispatch closure then runs that event through the same two filters every real event passes:

  1. matchesFilter(evt, filter) — rejects it whenever the subscription named any filter.org (the drain event's org is the empty string), any filter.type other than 'view', or any filter.name.
  2. the since drop-filter — evt.seq <= since is true for -1 against every real seq, so any numeric-since watcher drops it too.

watchers.clear() then removes the listener, so nothing can ever settle the promise. The consumer's pending next() hangs forever.

Measured on the branch

With a throwaway probe (deleted afterwards; porcelain clean):

subscription drained by close()?
watch({org:'system'}, a.seq) no — still unsettled 100ms after close()
watch({org:'system'})no since at all no — still unsettled
watch({}) ✅ yes

⭐ Note the middle row: the org-filter half bites on its own, without any since. This is not only a numeric-since problem.

Who can reach it

  • MetadataCache.start() (packages/metadata-core/src/cache.ts) subscribes with this.watchFilter. Whenever that filter is non-empty, this is the production shape that hangs on close(). Reachable today.
  • MetadataManager.startRepositoryWatch() uses repo.watch({}) and is drained.

PR #11018 does not create this, but it does make numeric-since watchers functional for the first time, widening who can reach it.

Why it was not fixed in place

The bounded-exemption condition fails: the correct shape is not pinned by existing evidence. Two defensible answers, and choosing between them is a contract question about what close() owes:

  • the drain event bypasses the filters — keep the synthetic-event mechanism, exempt it from matchesFilter and the since drop; or
  • close() resolves pending promises with done: true directly — stop modelling shutdown as an event at all.

The second is arguably cleaner (shutdown is not a metadata event and giving it a seq of -1 is what makes it collide with the since comparison in the first place), but that is a design opinion, not a measurement, and the invariant table says nothing today about what close() owes a pending iterator.

Provenance

Recorded in issuecomment-5379256754. Filed unassigned for triage to rank.

Metadata

Metadata

Assignees

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions