Skip to content

fix(mine): identify lock holder + exit non-zero on contention (#1264) - #1413

Merged
igorls merged 3 commits into
developfrom
fix/1264-mine-lock-holder-diagnostics
May 8, 2026
Merged

fix(mine): identify lock holder + exit non-zero on contention (#1264)#1413
igorls merged 3 commits into
developfrom
fix/1264-mine-lock-holder-diagnostics

Conversation

@igorls

@igorls igorls commented May 8, 2026

Copy link
Copy Markdown
Member

Summary

When mempalace mine collided with another writer (live mcp_server, another mine, anything taking mine_palace_lock), the operator saw a generic "another mempalace mine is already running" message and the CLI exited 0 — making the contention invisible to nohup or shell scripts checking \$?.

The original reporter ran nohup mempalace mine ... & disown against a palace where mcp_server held the lock and got a 200-byte log with only the auto-defaults warning — no clue that the MCP server was the cause.

PR #1162 (already merged) closed the structural race by routing MCP/direct writers through mine_palace_lock. This PR fixes the observability half: when contention is detected, name the holder and exit non-zero so the operator (or their automation) can react.

Changes

  • mempalace/palace.py:

    • Lock file now records the holder's PID + first three argv tokens on acquire (e.g. 12345 mempalace mcp_server).
    • On failed acquire, the file is read and the holder identity is surfaced through MineAlreadyRunning: "palace /path is held by PID N (mempalace mcp_server); wait for it to finish or stop the holder before retrying".
    • Open mode changes from "w" to "a+" so the prior holder's identity survives long enough to be read by a failed contender.
  • mempalace/miner.py: mine() no longer swallows MineAlreadyRunning. The exception propagates so the CLI can render a clear message and exit non-zero.

  • mempalace/cli.py cmd_mine: catches MineAlreadyRunning, prints to stderr, calls sys.exit(1).

  • Tests:

    • tests/test_palace_locks.py: test_lock_failure_message_names_holder (cross-process repro that asserts PID N appears in the message), test_lock_holder_identity_persists_across_release (lock body does not grow across re-acquires).
    • tests/test_cli.py: test_cmd_mine_exits_nonzero_on_lock_holder (simulates MineAlreadyRunning, verifies SystemExit(1) and that the holder identity reaches stderr).

Behavior change

In-process callers that called miner.mine() and depended on it silently swallowing MineAlreadyRunning will now see the exception. This is intentional — the silent swallow was the bug. Library users that want to coexist with another writer should handle the exception themselves.

Test plan

  • uv run pytest tests/test_palace_locks.py tests/test_cli.py tests/test_miner.py tests/test_chroma_collection_lock.py -v — 39+ passed
  • uvx --from 'ruff>=0.4.0,<0.5' ruff check + format check — clean
  • Cross-process repro test verifies the new message format works under real fcntl contention

Closes #1264

When a `mempalace mine` collided with another writer (live mcp_server,
another mine, anything taking mine_palace_lock), the operator saw a
generic "another `mempalace mine` is already running" message and the
CLI exited 0 — making the contention invisible to nohup or scripts
checking $?. The reporter ran a `nohup mempalace mine ... & disown`
and got a 200-byte log with only the auto-defaults warning, no clue
that an MCP server was holding the store.

palace.py: the lock file now records the holder's PID + first three
argv tokens on acquire. A failed acquire reads the file and surfaces
"palace <path> is held by PID N (mempalace mcp_server); wait for it
to finish or stop the holder before retrying" in the
MineAlreadyRunning message. Open mode changes from "w" to "a+" so the
prior holder's identity survives long enough to be read.

miner.mine() now lets MineAlreadyRunning propagate. cmd_mine catches
it, prints the holder-aware message to stderr, and exits non-zero so
shell wrappers detect the contention.

Note: this is a behavior change for in-process callers that depended
on miner.mine() silently swallowing MineAlreadyRunning. The silent
swallow was the bug.

Closes #1264
Copilot AI review requested due to automatic review settings May 8, 2026 04:00
@igorls igorls added this to the v3.3.5 milestone May 8, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Improves mempalace mine lock-contention observability by recording the lock holder identity in the palace lock file, surfacing that identity in MineAlreadyRunning, and ensuring the CLI exits non-zero on contention so automation can detect it.

Changes:

  • Record PID + abbreviated argv in the per-palace lock file and include it in MineAlreadyRunning messages.
  • Stop swallowing MineAlreadyRunning in miner.mine(); handle it in cli.cmd_mine and sys.exit(1).
  • Add regression tests covering holder-identification messaging and non-zero CLI exit on contention.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
mempalace/palace.py Persist lock holder identity in the lock file and surface it in contention errors.
mempalace/miner.py Let MineAlreadyRunning propagate so callers can handle contention explicitly.
mempalace/cli.py Catch MineAlreadyRunning, print to stderr, and exit with code 1.
tests/test_palace_locks.py Add cross-process tests asserting PID appears in contention messages and lock file doesn’t grow.
tests/test_cli.py Add CLI regression test asserting non-zero exit and holder identity in stderr.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread mempalace/palace.py
Comment on lines +391 to +399
def _write_lock_holder(lock_file) -> None:
"""Record this process's identity in the lock-file body. Best-effort."""
try:
ident = f"{os.getpid()} {' '.join(sys.argv[:3])}".strip()
lock_file.seek(0)
lock_file.truncate()
lock_file.write(ident)
lock_file.flush()
except OSError:
Comment thread mempalace/palace.py
Comment on lines 453 to 457
import msvcrt

try:
msvcrt.locking(lf.fileno(), msvcrt.LK_NBLCK, 1)
acquired = True
igorls added 2 commits May 8, 2026 01:28
Windows CI surfaced two bugs introduced by the holder-identity write:

1. msvcrt.locking(LK_NBLCK, 1) locks 1 byte at the *current* file
   position. Switching to "a+" mode put the position at end-of-file,
   so two contenders locked different bytes and silently both
   acquired (the test asserts saw [(ok, 1), (ok, 2)] instead of
   ok+busy).

2. With the byte-range lock active on Windows, the locked byte is
   read-blocked for other processes. A contender trying to read the
   holder identity from byte 0 would hit PermissionError.

Switch to "r+" mode (after touch-create) and explicitly seek(0) before
both lock and unlock. Then reserve byte 0 as a pure lock sentinel and
write the holder identity from byte 1 onward. _read_lock_holder reads
from byte 1+, so it never touches the locked byte.

Also bound file growth across re-acquires: truncate to
sentinel + len(ident) before writing so the file body stays the size
of the current holder, never accumulating across runs.

Linux fcntl.flock locks the whole file independent of byte position,
so the seek(0) is harmless on POSIX. The shape works on both.
os.path.expanduser("~") reads HOME on POSIX but USERPROFILE on Windows;
the lock-body bound test was monkeypatching HOME only, so on
test-windows the lock file landed in the runner's real ~/.mempalace
and the tmp_path glob found nothing.

Patch USERPROFILE in addition to HOME, and read the body as bytes so
the byte-0 sentinel doesn't trip a UTF-8 decode warning. Assertion
shifts from line-count to size-bound (still detects unbounded growth
across re-acquires).
@igorls
igorls merged commit c70d518 into develop May 8, 2026
6 checks passed
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.

mine: silent exit when concurrent writer holds chroma lock; should detect live mcp_server and back off with clear error

2 participants