meta_package_manager.dispatch module¶
Cross-manager dispatch: scheduling many package managers at once.
Where meta_package_manager.execution runs one manager’s CLI in one
subprocess, this module schedules many managers concurrently: the job-count
policy that decides sequential-vs-concurrent (effective_jobs()), the
up-front availability probe used during selection (warm_availability()),
the two progress-wrapped fan-out primitives the CLI subcommands drive
(collect_from_managers(), collect_per_package()) with their shared
dispatch() engine, the backend-lock catalog that serializes conflicting
managers (SHARED_LOCK_FAMILIES and merge_into_lock_lanes()), and
the manager-bound ✓/✗ ledger (OperationTrail) that the
concurrent and sequential paths both report through.
The generic layers live upstream in click-extra: the concurrency primitives in
click_extra.execution (run_lanes driven by
mpm --jobs) and the batch-reporting trail in click_extra.spinner
(OperationTrail with its
trail_glyph/trail_line atoms). This module keeps what is package-manager
policy: which managers must never overlap, how the trail binds to the pool’s
--progress state, and when a batch collapses to a sequential pass.
- class meta_package_manager.dispatch.LockFamily(backend, members, contention)[source]¶
Bases:
objectA set of managers contending for one backend lock.
- backend: str¶
Short name of the contended resource, labelling the family in the docs.
Rendered as the middle level of
meta_package_manager._docs.lock_families_sankey()and as the first column ofmeta_package_manager._docs.lock_families_table(), so it reads as a thing managers queue on rather than as a tool: pacman database, not pacman.That phrasing is also load-bearing for the diagram. Mermaid identifies a sankey node by its label alone, so a family named after a member (
pkg,conda,pacmanandscoopall name both) would fold the two levels into one self-linked node.test_lock_family_backends_are_distinctholds every name clear of the pool.
- contention: str¶
Why they collide, written to complete the sentence “``mpm`` never runs X at the same time as Y: …”.
Rendered on every member’s own documentation page by
meta_package_manager._docs.manager_concurrency(), so it addresses a user of those managers rather than a reader of this module: keep it to the fact and its consequence, and leave the maintenance rationale to the notes below.
- meta_package_manager.dispatch.SHARED_LOCK_FAMILIES: Final[tuple[LockFamily, ...]] = (LockFamily(backend='dpkg lock', members=frozenset({'pacstall', 'deb-get', 'nala', 'apt-mint', 'apt'}), contention='they all install through `dpkg` and serialize on its `/var/lib/dpkg/lock`'), LockFamily(backend='Homebrew update lock', members=frozenset({'cask', 'brew'}), contention="they are the same `brew` binary, and two concurrent `brew update` collide on Homebrew's own update lock"), LockFamily(backend='conda environment prefix', members=frozenset({'micromamba', 'mamba', 'conda'}), contention='they act on one environment prefix and one package cache, and `conda` honors none of the locks `mamba` takes on them'), LockFamily(backend='RPM database', members=frozenset({'urpmi', 'yum', 'zypper', 'dnf5', 'dnf'}), contention='they all reach the RPM database'), LockFamily(backend='pacman database', members=frozenset({'trizen', 'yay', 'pikaur', 'pacman', 'paru', 'pacaur', 'pamac'}), contention='they all reach the pacman database (`/var/lib/pacman/db.lck`), and two of them mutating at once fail to init their transaction'), LockFamily(backend='pkg install database', members=frozenset({'pkg', 'ports'}), contention='`ports` keeps no registry of its own and registers what it builds through `pkg`, whose advisory lock on that shared install database refuses a second writer'), LockFamily(backend='Scoop tree', members=frozenset({'sfsu', 'scoop'}), contention='they work on the same `~/scoop` tree, `sfsu` delegating its mutating operations to the `scoop` binary itself'))¶
Managers that contend for one shared backend lock, grouped by backend.
Different managers are otherwise independent processes over disjoint state, so running them in parallel is safe. The exception is a handful that drive a shared backend and serialize on its lock:
apt,apt-mint,deb-get,nalaandpacstallall reach dpkg (/var/lib/dpkg/lock).pacstallbelongs here despite itspacprefix and its AUR-inspired design: it builds its pacscripts into.debarchives and installs those, so it contends with the Debian family and never touches pacman’s database.brewandcaskare the same brew binary and serialize on Homebrew’s own update lock: two concurrentbrew update(which mpm sync issues identically for both, as the formula/cask split does not apply to it) collide, one failing with “Another active Homebrew update process is already running”.conda,mambaandmicromambaact on one environment prefix and one package cache. This is the family that does not get the guarantee below: mamba takes a real lock on the prefix and on every cache directory for the length of a transaction, and conda honors none of them, its own locking covering the repodata cache alone. Concurrent runs corrupt rather than block, which upstream closed as not planned (conda/conda#13037). Serializing them here is what keeps that out of reach.dnf,dnf5,yum,zypperandurpmiall reach the RPM database.urpmifrontslibrpmdirectly, having no listing of its own, and the Mandriva lineage it serves shipsdnfalongside it, so the two genuinely coexist on one host.pacmanand the AUR helperspacaur,pamac,paru,pikaur,trizenandyayall reach the pacman database (/var/lib/pacman/db.lck). The helpers are front-ends rather than reimplementations: each shells out tosudo pacmanfor the privileged steps,pamacreaching the samelibalpmthrough Manjaro’slibpamac. Two of them mutating at once fail to init their transaction.pkgandportsshare the install databasepkgmaintains, which every mutating operation butsyncreaches:portshas no registry of its own, builds from/usr/portsand registers the result throughpkg, whose advisory lock on that database refuses a second writer. Theirsyncis the one pair that would not collide, refreshing a git tree and a package catalog respectively, and is serialized along with the rest rather than splitting the family per operation.scoopandsfsuwork on the same~/scooptree. sfsu reimplements Scoop’s read paths only, delegatinginstall,removeand both upgrades to thescoopbinary itself, so those are literally the same command twice; its ownupdateandcleanupthen reach the same buckets and cache Scoop’s do. Concurrent bucket refreshes are twogit pullin one repository, which fails on the index lock.
dkp-pacmanis deliberately not in the pacman family, and it is the one exclusion worth stating: it isPacmanby inheritance and would look like an oversight. But devkitPro ships it precisely so it can sit beside a distribution’s ownpacmanwithout colliding, pointed at its own repositories and its own database, so it contends with nothing.pkconis the case this model cannot express, and it is left out knowingly rather than filed under a guess. PackageKit is a client for whatever backend the host provides (apt, dnf, zypp, alpm), so the family it belongs to is a property of the machine rather than of the manager, and afrozensethere is fixed at import. It needs no protection from itself,packagekitdqueuing its own transactions, but apkconmutation still contends with a native manager mpm drives in the same run. Expressing that would mean resolving the backend at dispatch time.Concurrency is safe across families and unsafe within one, just as it is unsafe within a single manager (which is why a manager’s own packages stay serial). For every family above except the conda one, two members running at once block or fail rather than corrupt: each backend holds a real lock and the loser is told so. That is a property of those backends rather than of this mechanism, so a family added later earns the guarantee only by inspection, and conda is the standing proof that some do not.
Enforced for the mutating fan-outs only:
merge_into_lock_lanes()collapses each family’s members into a singledispatch()lane, so they run serially while distinct families still run in parallel. The read-only queries (installed/outdated/search) take no backend lock, so they keep one lane per manager and stay fully concurrent. Members of a lane also share a command cache (seerun_cache), so two that resolve to a byte-identical invocation (brewandcaskforsyncandcleanup) run the subprocess once.Adding a newly-conflicting set of managers is one entry here: a
LockFamilynaming the backend and its members, after which the serialization, the command cache, the Concurrency section of every member’s documentation page and both renderings ofdocs/concurrency.mdall pick it up.
- meta_package_manager.dispatch.FAN_OUT_CONCURRENT: Final[str] = 'concurrent'¶
Every selected manager runs at once, one
dispatch()lane each.
- meta_package_manager.dispatch.FAN_OUT_GROUPED: Final[str] = 'grouped'¶
Same, but
SHARED_LOCK_FAMILIESmembers are merged into one lane.
- meta_package_manager.dispatch.FAN_OUT_SEQUENTIAL: Final[str] = 'sequential'¶
One manager at a time, whatever
mpm --jobssays.
- meta_package_manager.dispatch.FAN_OUT_NONE: Final[str] = 'none'¶
Runs no package operation, so there is nothing to spread.
- class meta_package_manager.dispatch.FanOut(invocation, mode)[source]¶
Bases:
objectHow one way of invoking a subcommand spreads over the selected managers.
- meta_package_manager.dispatch.COMMAND_FAN_OUT: Final[tuple[FanOut, ...]] = (FanOut(invocation='cleanup', mode='grouped'), FanOut(invocation='config-template', mode='none'), FanOut(invocation='doctor', mode='grouped'), FanOut(invocation='dump', mode='concurrent'), FanOut(invocation='help', mode='none'), FanOut(invocation='install', mode='grouped'), FanOut(invocation='install <untied package>', mode='sequential'), FanOut(invocation='installed', mode='concurrent'), FanOut(invocation='managers', mode='none'), FanOut(invocation='orphans', mode='concurrent'), FanOut(invocation='outdated', mode='concurrent'), FanOut(invocation='remove', mode='grouped'), FanOut(invocation='restore', mode='grouped'), FanOut(invocation='sbom', mode='concurrent'), FanOut(invocation='search', mode='concurrent'), FanOut(invocation='sync', mode='grouped'), FanOut(invocation='upgrade', mode='grouped'), FanOut(invocation='which', mode='none'))¶
Fan-out mode of every
mpmsubcommand, rendered ondocs/concurrency.md.The three fan-out shapes above are visible from inside this module; which subcommand takes which is not, being an argument at each call site. This is where the two meet, so a reader can answer “does this command parallelize?” without following
report_state=Truethrough four CLI modules.Kept complete rather than restricted to the commands that fan out:
test_fan_out_covers_every_subcommandholds it equal to the CLI’s own command list, so a new subcommand fails the suite until someone decides its mode. TheFAN_OUT_NONEentries are that decision recorded, andmeta_package_manager._docs.concurrency_table()leaves them out of the rendered table.Caution
Hand-maintained, and the one thing here that can drift from the code silently. A subcommand switching between
collect_from_managers()andcollect_per_package(), or gainingreport_state=True, has to be reflected in the same commit: no test can read the mode back off a call site.
- meta_package_manager.dispatch.effective_jobs(ctx, count)[source]¶
Resolve how many worker threads to use for a batch of
countitems.Thin wrapper over
click_extra.execution.resolve_jobs()pinning mpm’s policy: always collapse to a single (sequential) worker atDEBUGverbosity, where coherent per-manager log narration matters more than the speed-up (interleaved threads would scramble it). The base helper also collapses to sequential with no active CLI context, for a single item, or atmpm --jobs1; otherwise thempm --jobsvalue wins, capped atcount(no point spinning up more workers than there are items).- Return type:
- meta_package_manager.dispatch.probe_signature(manager)[source]¶
Static signature of the command
manager’s version probe would spawn.Built from class attributes alone: no filesystem lookup, no subprocess. Two managers sharing a signature search the same directories for the same binary names and pass it the same arguments, so their probes may resolve to a byte-identical command line.
brewandcaskdo, and so douv/uvxandyarn/yarn-berry.Deliberately conservative in the safe direction. It never splits two managers that would spawn the same command, which is what
merge_into_probe_lanes()needs to put them on one lane; it may however merge two that turn out to differ (the Zsh plugin managers all probezsh, some with--versionand some withversion). A wrong merge costs the two a shared lane, where the second simply misses the cache and spawns as it would have anyway.Not the resolved command line, on purpose: resolving it means walking
PATHfor every candidate up front, which measured slower than the redundant subprocesses it would save.- Return type:
- meta_package_manager.dispatch.merge_into_probe_lanes(managers)[source]¶
Group
managersintowarm_availability()lanes byprobe_signature().The probe counterpart of
merge_into_lock_lanes(): managers whose version probe may resolve to the same command line land on one lane and run serially, while unrelated managers keep a lane each and run concurrently. Lanes come out in first-seen order, so a run is reproducible.- Return type:
- meta_package_manager.dispatch.warm_availability(managers)[source]¶
Probe several managers’
availableconcurrently.Reading
availableforces a manager’s--versiondetection, whose result (and thecli_path/executable/versionit depends on) is cached on the instance. Warming the candidate set up front turns the sequential string of probes into a single round bounded by the slowest one, shaving startup latency off any command that touches many managers.Managers on distinct lanes are distinct instances with their own cached attributes and subprocess, so their probes are independent and thread-safe; the GIL is released while each waits. The executor barrier publishes every cached value before the caller reads it back.
Probes run in
merge_into_probe_lanes()lanes rather than one flat batch, so the managers that would spawn a byte-identical--versioncall take turns on one worker and share arun_cache: the first spawns, the rest replay its result. That is the same mechanismdispatch()gives a lock family, and it needs the lane for the same reason — a cache handed to managers running concurrently would just race them both into spawning.Sized by
effective_jobs()over the lane count: a no-op (leaving the probes to lazy, sequential evaluation) without an active context, atDEBUGverbosity, for a single lane, or atmpm --jobs1.- Return type:
- class meta_package_manager.dispatch.OperationTrail(managers, *, label='', unit='', total=0, jobs=1, coverage=False)[source]¶
Bases:
OperationTrailclick_extra.spinner.OperationTrailbound to the manager pool.The upstream class owns the two renderings (sequential echoed lines, or one aggregate indicator with buffered-then-streamed lines) and the interactive gating; this subclass supplies mpm’s policy around it:
Enablement follows ``–progress``, folded into each manager’s
progressflag by the CLI (a TTY, no serialized output, not atDEBUGverbosity): any enabled manager turns the trail on, auto-gated on an interactive stderr.A concurrent batch mutes the managers’ own per-call spinners (which would collide on stderr) for the duration of the aggregate one.
A concurrent batch’s aggregate indicator is a determinate progress bar, not an indeterminate spinner: every
dispatch()batch counts its work up front (one task per manager, or per package-manager pair), so the bar always has a length to render against.``coverage`` keeps the read-command semantics: their result table is the real output and each manager keeps its per-call spinner, so the sequential rendering stays silent (upstream’s
echo_sequential=False).
The ordering-bound sequential state changers (
install’s priority search) construct it bare; everydispatch()batch drives it as a context manager.- Parameters:
managers (
Iterable[PackageManager]) – the batch’s managers, read for the--progressgate and (when concurrent) to mute their per-call spinners.label (
str) – present-tense verb for the running indicator (“Searching”).unit (
str) – the noun counted in the indicator tally (“managers”, “packages”).total (
int) – how many outcomes are expected, for thedone/totalcount and the progress bar’s length.jobs (
int) – the worker count fromeffective_jobs();> 1selects the concurrent rendering.coverage (
bool) – when set, a sequential run stays silent (the caller has another output, its result table). Unused when concurrent.
Configure (but do not start) the trail.
- Parameters:
label (
str) – present-tense verb for the running aggregate indicator ("Fetching"), composed into its{label} {done}/{total} {unit}tally.unit (
str) – the noun counted in the tally ("files","feeds").total (
int) – how many outcomes are expected, for thedone/totalcount.jobs (
int) – the batch’s worker count;> 1selects the concurrent rendering (one aggregate spinner),<= 1the sequential one (plain echoed lines).spinner – a
SpinnerPresetfrom theSPINNERScatalog (spinner=SPINNERS["moon"]) for the concurrent aggregate spinner. Ignored by the sequential and progress-bar renderings, and mutually exclusive withprogress_bar.progress_bar – render the aggregate indicator as a determinate
click.progressbar()instead of a spinner, for a sequential or concurrent batch alike. Requires a positivetotal(a bar needs a length) and is mutually exclusive withspinner.timer – append each operation’s and the batch’s elapsed time to the trail lines and the finisher.
None(the default) follows the CLI’s--time/--no-timeflag;Trueforces timing on withformat_duration()’s compact clock, a callable(seconds: float) -> strforces it on with a custom format, andFalseforces it off. Per-operation times come from asecondsargument tomark(), filled in automatically by anoperation()handle.clock – whether a running aggregate indicator shows elapsed time (
"elapsed", the default: a stopwatch counting up, visible from the start) or remaining time ("eta": an estimate from the batch’s rate, appearing only once an outcome lets it be computed). Both the progress bar and the concurrent spinner honor"eta"(the spinner reuses Click’s progress-bar estimate, since the trail knows itstotal). Per-operation and finisher times are always elapsed.enabled – force the trail on or off.
None(the default) auto-detects: the sequential echo renders only on an interactive stream, and the aggregate indicator applies its own TTY gate.echo_sequential – whether a sequential batch echoes its outcome lines and finisher at all. Turn it off when the batch has another output that is the real product (a result table) and the trail would be noise; an aggregate indicator is unaffected.
delay – seconds before the aggregate indicator first draws: a fast batch then completes without ever flashing one.
stream – where to render; defaults to
sys.stderrso the trail never mixes intostdoutdata.
- Raises:
ValueError – if
progress_baris set without a positivetotal, or together withspinner, or ifclockis neither"elapsed"nor"eta".
- meta_package_manager.dispatch.dispatch(label, done_label, unit, lanes, *, coverage=False, ctx=None)[source]¶
Fan a set of work lanes out across managers, narrating a
✓/✗trail.The single scheduling primitive behind both
collect_from_managers()andcollect_per_package(). A lane is one or more managers paired with a list of callables; lanes run concurrently (one worker each) while a lane’s own callables run serially, because a package manager cannot safely run two of its own invocations at once, nor can two managers sharing a backend lock (seeSHARED_LOCK_FAMILIES). A lane usually wraps a single manager;merge_into_lock_lanes()is what bundles a whole lock family into one, and such a lane also gets a shared command cache (seerun_cache) so its members collapse identical invocations.Each callable does its work, records its own outcome (output to
INFO, failures into a caller-owned list) and returns(ok, message)for the trail. The whole batch reports through oneOperationTrail: a per-outcome✓/✗line plus a finisher, behind a single aggregate progress bar when concurrent (a slow batch on a terminal) and silent otherwise.Concurrency is sized by
effective_jobs()(driven bympm --jobs): it collapses to a sequential pass — preserving each manager’s own per-call spinner — for a single lane, at--jobs 1, or atDEBUGverbosity.- Parameters:
coverage (
bool) – forwarded toOperationTrail. Read commands set it (their result table is the output, so the sequential pass stays silent and the finisher reports coverage,{done_label} N {unit}, always✓). Maintenance and state-changing commands leave itFalse(the trail is their output, so the finisher reports the success count,{done_label} N/M {unit},✗on any failure).ctx (
Context|None) – the active click context, read only to size concurrency (effective_jobs()). Defaults to the current context, so a command need not thread it; tests pass an explicit stand-in.
- Return type:
- meta_package_manager.dispatch.merge_into_lock_lanes(pairs)[source]¶
Group
(manager, task)pairs intodispatch()lanes, one per lock family.Managers sharing a
SHARED_LOCK_FAMILIESentry collapse into a single lane so their tasks run serially (the lane isdispatch()’s unit of mutual exclusion), while unrelated managers each keep their own lane and run concurrently. A manager not in any family keys on its own id, so its tasks still group together (a manager’s own invocations cannot overlap either). First-seen order is preserved, both across lanes and within a lane’s task list.Used by the mutating fan-outs only: the state changers through
collect_per_package(), andsync/cleanup/upgrade --allthroughcollect_from_managers(). The read commands take no backend lock and skip this, keeping one lane per manager.
- meta_package_manager.dispatch.collect_from_managers(label, done_label, managers, work, *, report_state=False, ctx=None)[source]¶
Run
work(manager)for every manager concurrently, results in input order.The fan-out primitive for the read-only commands (
installed/outdated/search) and the independent maintenance commands (sync/cleanup/upgrade --all). It adapts each manager into adispatch()unit that runsworkand stashes the(id, data)result in input position, so the returned list mirrorsmanagersregardless of completion order. The maintenance commands (report_state) then merge lock-family members into shared serial lanes (merge_into_lock_lanes()); the read commands keep one lane per manager.workreturns this manager’s(id, data); it must handle its ownmeta_package_manager.execution.CLIError(each manager owns its subprocess and error list, so the call is thread-safe per manager). A truthydata["errors"](ordata["failed"]) marks that manager’s trail line✗; an optionaldata["label"]overrides its text (upgrade --alluses it for cooldown skips).- Parameters:
report_state (
bool) – maintenance commands set it (their only output is the trail). It flips the finisher to a success count, keeps the trail in the sequential fallback, and turns on lock-family serialization. Read commands leave itFalse: their table is the output, so the sequential fallback is silent and the finisher reports coverage. Passed todispatch()as the inverse ofcoverage.- Return type:
- meta_package_manager.dispatch.collect_per_package(label, done_label, tasks, *, ctx=None)[source]¶
Run per-package operations across managers concurrently, serial within each.
The fan-out primitive for the ordering-free state changers that act on many (package, manager) pairs:
remove,upgrade <packages>,restoreand the manager-tied specs ofinstall. Takes a flat list of(manager, task)pairs and groups them into lanes by lock family (merge_into_lock_lanes()) — so a manager’s own packages, and any lock-family peers, stay serial while unrelated managers run in parallel — then drivesdispatch(). Each task returns(ok, message)after doing its CLI call and recording its own outcome. The unmatched-package priority search ofinstallis not routed here: it has genuine cross-manager ordering (stop at the first manager that has the package) and stays sequential on its own.- Return type:
- meta_package_manager.dispatch.warn_jobs_ignored(ctx)[source]¶
Note that
--jobsdoes not parallelize this run.Only
installwith at least one untied package reaches this: those packages need a priority search (install with the first manager that has the package, skip the rest), which is cross-manager-sequential, so the whole command runs serially. The other state changers (remove,upgrade <packages>,restore, andinstallof fully manager-tied specs) now fan out throughcollect_per_package(). When the user explicitly raisedmpm --jobsabove1, say so once atINFO: the request simply has no effect on this run, which is narration, not a problem.- Return type: