Repository navigation
fix: serialize concurrent JJ repository initialization - #143
Conversation
Two MCP server processes that share one data repository run
JjBackend.init_monorepo() at startup. The init was unsynchronized across
processes: both ran `jj git init --colocate` (one aborted with 'The
target repo already exists'), and both rewrote the shared repository
config through four `jj config set --repo` calls. Concurrent config
writers were observed leaving the config file unparseable
('Configuration cannot be parsed as TOML document'), which aborted the
second server before it could serve any request.
Take a blocking cross-process exclusive lock on the repository directory
for the whole init. The lock lives on the directory itself because
`jj git init` refuses a pre-created .jj, so a lock file inside .jj
cannot guard the fresh-init case; platforms without directory locking
fall back to a lock file beside jj's metadata.
Add real two-process regression coverage: two OS processes released from
a shared barrier initialize the same fresh repository and both must
succeed with a parseable repo config.
Addresses issue #142.
timeleft--
left a comment
There was a problem hiding this comment.
Reviewed exact head a28fa7a against b9a462d. The directory lock covers fresh JJ initialization and all shared repository configuration writes without precreating .jj. Independent affected tests: 43 passed, including concurrent-process startup and real native MCP client registration. Version policy and isolated-cache uv lock check passed. Both supported JJ CI matrices and security checks pass. Local external review completed without source mutation and found no blockers. Its same-event-loop concurrent initialization concern is nonblocking for the current once-per-process startup contract; documented Windows setup uses WSL. No blocking findings or unresolved threads. Approved for normal protected merge; this is source approval, not publication or deployment.
Summary
Serialize
JjBackend.init_monorepo()across processes so two MCP serverprocesses sharing one data repository can start together, fixing issue #142.
Root cause
Merged source
b9a462dfailed postmergeTests (0.45.1):test_two_ordinary_server_processes_isolate_authoring. Both servers logMonorepo already initialized, then race onjj config set --repo; the secondprocess dies during startup (
server.py:155 → init_monorepo) with:init_monorepowas unsynchronized between processes:jj git init --colocate; jj refusesthe second creator (
Error: The target repo already exists). Reproducedlocally at 60/60 iterations.
four
jj config set --repocalls. jj 0.45.1 stores that file outside the repo(
$XDG_CONFIG_HOME/jj/repos/<config-id>/config.toml), so two serverprocesses on one repo race the same file even though the data repo is
pre-initialized.
This is a shared-initialization race, not the transitive dependency bump.
Fix
init_monoreponow holds a blocking cross-process exclusive lock(
_repo_init_lock) for the whole init — git-init case detection plus the fourconfig writes. The lock is an
flockon the repository directory itself,because
jj git initrefuses a repository whose.jjwas pre-created (verified:Error: The target repo already exists), so a lock file inside.jjcannotguard the fresh-init case. Platforms without directory locking fall back to a
lock file beside jj's metadata once it exists.
No agent isolation, auth, server-managed data, or test matrix behavior is
changed. No test is weakened or skipped.
Verification
tests/test_jj_backend.py::test_concurrent_init_monorepo_is_serialized— realtwo-process regression: two OS processes, released together from a shared
start barrier, call
init_monorepoon one fresh repository. Fails before thefix (100% locally:
jj git failed (rc=1): Error: The target repo already exists), passes after. The same test confirms both processes finish with aparseable
jj config list --repo.1120 passed.fava-trails install-jj):tests/test_jj_backend.py tests/test_mcp_protocol.py→43 passed, includingtest_two_ordinary_server_processes_isolate_authoring.uv sync --frozen,ruff check src/ tests/, andscripts/mw-version.py check --base origin/mainall pass.jj git init --colocaterace with real jj 0.45.1(60/60 iterations) and confirmed jj config writes are individually atomic in
isolation, so the fix serializes the whole init rather than attributing the
failure to the dependency bump.
Version
0.8.2 → 0.8.3(one patch,scripts/mw-version.py update).Closes #142.