Skip to content

fix: serialize concurrent JJ repository initialization - #143

Merged
timeleft-- merged 1 commit into
mainfrom
automation/fava-trails-142
Oct 9, 2026
Merged

timeleft-- merged 1 commit into
mainfrom
automation/fava-trails-142

Conversation

@yia-mw-agent

Copy link
Copy Markdown
Contributor

Summary

Serialize JjBackend.init_monorepo() across processes so two MCP server
processes sharing one data repository can start together, fixing issue #142.

Root cause

Merged source b9a462d failed postmerge Tests (0.45.1):
test_two_ordinary_server_processes_isolate_authoring. Both servers log
Monorepo already initialized, then race on jj config set --repo; the second
process dies during startup (server.py:155 → init_monorepo) with:

JjError: jj config failed (rc=1): Config error: Configuration cannot be parsed as TOML document
Caused by: TOML parse error at line 3, column 32
3 | s.dev/latest/config-schema.json
key with no value, expected `=`
Hint: Check the config file: .../xdg-config/jj/repos/cf8be3f68278ea4ce3a9/config.toml

init_monorepo was unsynchronized between processes:

  • On a fresh repository both processes run jj git init --colocate; jj refuses
    the second creator (Error: The target repo already exists). Reproduced
    locally at 60/60 iterations.
  • On every startup both processes rewrite the shared repository config through
    four jj config set --repo calls. jj 0.45.1 stores that file outside the repo
    ($XDG_CONFIG_HOME/jj/repos/<config-id>/config.toml), so two server
    processes 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_monorepo now holds a blocking cross-process exclusive lock
(_repo_init_lock) for the whole init — git-init case detection plus the four
config writes. The lock is an flock on the repository directory itself,
because jj git init refuses a repository whose .jj was pre-created (verified:
Error: The target repo already exists), 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 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 — real
    two-process regression: two OS processes, released together from a shared
    start barrier, call init_monorepo on one fresh repository. Fails before the
    fix (100% locally: jj git failed (rc=1): Error: The target repo already exists), passes after. The same test confirms both processes finish with a
    parseable jj config list --repo.
  • Full suite, jj 0.45.1: 1120 passed.
  • Affected files, jj 0.28.0 (installed via fava-trails install-jj):
    tests/test_jj_backend.py tests/test_mcp_protocol.py → 43 passed, including
    test_two_ordinary_server_processes_isolate_authoring.
  • uv sync --frozen, ruff check src/ tests/, and
    scripts/mw-version.py check --base origin/main all pass.
  • Reproduced the concurrent jj git init --colocate race 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.

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-- timeleft-- left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@timeleft--
timeleft-- merged commit d18fd8b into main Oct 9, 2026
12 checks passed
@timeleft--
timeleft-- deleted the automation/fava-trails-142 branch October 9, 2026 16:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix: serialize concurrent JJ repository initialization

2 participants