Skip to content

Ref counting reads committed state, so removing two packs that share a resource orphans it #385

Description

@bguidolim

Summary

ResourceRefCounter answers "is this still claimed?" by reading global-state.json from disk.
During a global sync, the state being mutated is that file, and Configurator.configure()'s
removal loop does not save between packs — the first write is the intermediate save that follows
it. Every pack in the loop therefore queries a snapshot that still lists its siblings.

Deselect two globally-configured packs that share a resource in one run: the first sees the second
still claiming it and reports .stillNeeded, the second sees the first and does the same. Both
release their claim, nothing is removed, and the resource is left installed with no owner. The same
reciprocal-keep happens when several retained global packs drop a shared claim in one update.

This affects brew packages and plugins, and now gitignore entries too — it lives in the shared
primitive rather than in any one resource kind. It fails in the safe direction (a resource is kept
rather than deleted), which is why it has gone unnoticed, but the leak never resolves on its own:
no later sync removes an artifact that no record claims.

Changes

Reference counting needs to see the run's uncommitted intent, not just committed state — either by
consulting the in-memory state the removal loop is mutating, or by passing it the set of claims
already released in this pass. Whichever shape is chosen must apply to all three resource kinds,
since they share the query path.

Worth deciding at the same time: whether an artifact left unclaimed should be reported by mcs doctor, so existing orphans become visible rather than silently persisting.

Test plan

  • swift test passes locally
  • swiftformat --lint . and swiftlint pass without violations
  • Affected commands verified with a real pack (e.g. mcs sync, mcs doctor)
  1. Install two packs globally that declare the same brew package (or the same gitignore entry).
  2. mcs sync --global and deselect both in the same run.
  3. Expect the shared resource to be removed once. Today it is kept, and neither pack's record
    claims it afterwards.

Found by review on #384, which added gitignore entries to the ref-counted set and inherited this.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions