Skip to content

FileSystemRepository.close() never reaches its event broker — a parked watch() iterator stays parked after shutdown #11127

Description

@os-zhuang

Found while implementing #11021 (SysMetadataRepository.close() cannot drain a filtered or numeric-since watcher). Same defect class, different implementation and different package, so it is filed rather than fixed in that PR.

The mechanism

FileSystemRepository.close() (packages/metadata-fs/src/repository.ts:198) does exactly two things:

async close(): Promise<void> {
  this.stopResync();
  if (this.watcher) {
    await this.watcher.close();   // chokidar
    this.watcher = null;
  }
  this.started = false;
}

It never touches this.broker (packages/metadata-fs/src/repository.ts:117, createBroker in packages/metadata-fs/src/sync.ts:49). The broker has no teardown of its own — subscribe/unsubscribe add to and delete from a plain Set, and nothing else empties it.

Each watch() iterator (packages/metadata-fs/src/watch-iterable.ts) parks its pending next() on a waiter callback that only two things can settle: a broker push, or the iterator's own local close() — which runs from iterator.return() / iterator.throw() and from nowhere else. So after repo.close():

  • the chokidar source is gone, so no further push can arrive;
  • the subscriber is still registered, and nothing calls its terminator.

A consumer holding a for await over fsRepo.watch(...) at shutdown therefore never sees the loop end. Unlike the #11021 shape this is not filter-dependent — there is no drain attempt at all, so it applies to every subscription shape including watch({}).

Confidence

Code read, not a runtime probe. #11021's scope is packages/metadata-protocol, and building/booting metadata-fs to confirm was out of that card's surface. The read is unambiguous (there is no path from close() to the broker or to any iterator terminator), but the matrix that #11021 carries for its own repository has not been produced here. Whoever picks this up should measure it first.

Related contract statement

#11021 adds invariant 8 to the MetadataRepository invariant table in packages/metadata-core/src/repository.ts — "shutdown terminates; it does not emit" — stating what a repository-level close() owes a pending iterator. That row names this gap as the one measured non-conformance among today's implementations. Fixing it is the same one-line shape the #11021 repair uses: hold each subscription's terminator alongside its event sink, and run every terminator from close().

InMemoryRepository is unaffected: it exposes no repository-level shutdown at all, so its iterators end only through return().


Generated by Claude Code

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions