Skip to content

fix: prevent concurrent mine corruption, OOM crashes, and Ctrl+C corruption - #130

Closed
minimexat wants to merge 2 commits into
MemPalace:mainfrom
minimexat:fix/crash-lock-oom-sigint
Closed

fix: prevent concurrent mine corruption, OOM crashes, and Ctrl+C corruption#130
minimexat wants to merge 2 commits into
MemPalace:mainfrom
minimexat:fix/crash-lock-oom-sigint

Conversation

@minimexat

Copy link
Copy Markdown

What

Three crash fixes for stability issues discovered during real-world mining.

Changes

Concurrent mine lock

Running multiple mempalace mine processes simultaneously causes a ChromaDB file locking violation and silent palace corruption. Added palace-level lockfile using fcntl (Unix) / msvcrt (Windows) with a clear error message instead of silent corruption.

OOM batch cap

Large files (JS bundles, SQL dumps) can generate hundreds of chunks submitted in a single ChromaDB collection.add() call, causing bad allocation crashes on machines with limited RAM. Added BATCH_SIZE = 50 cap and add_drawers_batch() helper.

Graceful Ctrl+C

Cancelling mid-run with Ctrl+C corrupts the palace. Added SIGINT handler that sets an interrupted flag, finishes the current file cleanly, then stops. Already-mined files are skipped on re-run, so progress is preserved.

…uption

- Add palace-level lockfile (_acquire_palace_lock / _release_palace_lock)
  using fcntl (Unix) / msvcrt (Windows) to prevent simultaneous mines
  from corrupting ChromaDB. Clear error message instead of silent corruption.
- Add BATCH_SIZE=50 cap and add_drawers_batch() to prevent bad allocation
  crashes when mining large files that generate hundreds of chunks.
- Add SIGINT handler to mine() for graceful Ctrl+C: finishes current file
  cleanly before stopping. Already-mined files are skipped on re-run.

@travisbreaks travisbreaks 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.

Thanks for tackling these three real problems (concurrent mine corruption, OOM on large files, Ctrl+C corruption). The approach is sound overall. A few items to consider:

1. fcntl import at module top level breaks Windows

import fcntl  # line 18 — unconditional import

fcntl doesn't exist on Windows. The conditional msvcrt usage inside _acquire_palace_lock() is correct, but the module-level import fcntl will crash on import before it ever gets there. This should be a conditional import:

# At module level:
if sys.platform != "win32":
    import fcntl

Or move the import inside the function body.

2. Stale lock after crash/SIGKILL

If the process is killed (SIGKILL, power loss, OOM-killer), .mine.lock will be orphaned and all future mine invocations will fail with "Another mine is already running." The error message says "delete .mine.lock if it crashed" which is good guidance, but a stale-lock detection (e.g. write PID to the lock file, check if that PID is alive on next acquire) would prevent users from hitting this in production.

3. Hardcoded BATCH_SIZE = 50

50 is a reasonable default, but the sweet spot depends on document size and available RAM. Consider making this configurable via --batch-size CLI flag or mempalace.yaml config, with 50 as the default.

4. add_drawers_batch silently swallows duplicates

if "already exists" not in str(e).lower():
    raise

This silently skips duplicate entries. That's probably fine for idempotent re-mining, but a logger.debug() here would help users understand why drawer counts don't match expectations.

5. Signal handler scope

signal.signal(signal.SIGINT, _handle_interrupt) is set inside mine(). If mine() is called as a library function (not from CLI), this overwrites the caller's signal handler without restoring it. Consider saving/restoring the original handler in the finally block:

old_handler = signal.signal(signal.SIGINT, _handle_interrupt)
try:
    ...
finally:
    signal.signal(signal.SIGINT, old_handler)
    if lock is not None:
        _release_palace_lock(lock)

6. No tests

CONTRIBUTING.md says "Write code with tests" and "All tests must pass via pytest tests/ -v". The existing test_miner.py has 15 tests, but none cover concurrency or signal handling. Even basic tests would strengthen this:

  • Lock acquisition/release lifecycle
  • Double-lock returns error
  • Batch splitting with >50 items

None of these are blockers for the concept, but items 1 (Windows import crash) and 5 (signal handler leak) are bugs that should be fixed before merge. The rest are improvements worth considering.

Overall solid work addressing real pain points.

…ter mine()

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@bgauryy

bgauryy commented Apr 8, 2026

Copy link
Copy Markdown

PR Review: fix: prevent concurrent mine corruption, OOM crashes, and Ctrl+C corruption

Executive Summary

Aspect Value
PR Goal Prevent palace corruption from concurrent mine processes, OOM on large files, and Ctrl+C mid-mine
Files Changed 1 (mempalace/miner.py)
Risk Level 🟡 MEDIUM - Stability improvements, but one fix is incomplete
Review Effort 2/5 - Single file, focused changes
Recommendation 🔄 REQUEST_CHANGES

Affected Areas: mempalace/miner.py — lock mechanism, batch insertion, signal handling, mine() flow

Business Impact: Prevents silent data corruption and hard crashes during mining — critical for user trust and data integrity.

Flow Changes: mine() now wraps its file-processing loop in try/finally with palace lock acquisition and SIGINT trapping. The main processing loop gains an interrupted check to break cleanly on Ctrl+C.

Ratings

Aspect Score
Correctness 3/5
Security 4/5
Performance 4/5
Maintainability 3/5

PR Health

  • Has clear description
  • References specific real-world scenarios
  • Appropriate size (94 additions, 33 deletions — single file)
  • Has relevant tests

High Priority Issues

🐛 #1: add_drawers_batch is dead code — OOM fix is not wired up

Location: mempalace/miner.py:463-477 (new function) | Confidence: ✅ HIGH

The PR adds add_drawers_batch() as a new function with BATCH_SIZE capping, but process_file() still calls add_drawer() one chunk at a time in a loop (line ~204). The batch function is never invoked anywhere — neither in process_file(), mine(), nor any other module. The stated OOM prevention goal is not achieved.

process_file current flow (unchanged by PR):

drawers_added = 0
for chunk in chunks:
    added = add_drawer(
        collection=collection,
        wing=wing,
        room=room,
        content=chunk["content"],
        source_file=source_file,
        chunk_index=chunk["chunk_index"],
        agent=agent,
    )
    if added:
        drawers_added += 1

Fix: Either refactor process_file() to collect drawers and call add_drawers_batch(), or remove the dead function. Note: the current one-at-a-time insertion is actually already safe against OOM — the real risk would be if there were a bulk collection.add() path elsewhere.


Medium Priority Issues

🚨 #2: File handle leak on lock acquisition failure

Location: mempalace/miner.py:+25-+36 (_acquire_palace_lock) | Confidence: ✅ HIGH

If flock / msvcrt.locking raises IOError/OSError, the lock_file handle opened on line +27 is never closed before sys.exit(1). While process exit will clean up, this is a resource leak in the general case (e.g., if sys.exit is later replaced with returning an error).

  except (IOError, OSError):
+     lock_file.close()
      print("\nERROR: Another mine is already running on this palace.")
      print("Wait for it to finish, or delete .mempalace/palace/.mine.lock if it crashed.\n")
      sys.exit(1)

🏗️ #3: BATCH_SIZE defined in miner.py instead of constants.py

Location: mempalace/miner.py:+106 | Confidence: ⚠️ MED

Per project convention (AGENTS.md: "Constants live in constants.py"), BATCH_SIZE should be defined in mempalace/constants.py alongside CHUNK_SIZE, CHUNK_OVERLAP, and MIN_CHUNK_SIZE, then imported. The existing chunk constants in miner.py are already imported from constants.py (line 17).

# In constants.py:
+ BATCH_SIZE = 50  # max chunks per ChromaDB call

# In miner.py:
- BATCH_SIZE = 50
+ from .constants import ..., BATCH_SIZE

Low Priority Issues

🎨 #4: Asymmetric platform import pattern

Location: mempalace/miner.py:+18-+19 | Confidence: ⚠️ MED

fcntl is imported at module level (guarded by sys.platform), but msvcrt is imported inside _acquire_palace_lock(). Consider making both conditional imports consistent — either both at module level or both inside the function.


Flow Impact Analysis

mine()
├── [NEW] signal.signal(SIGINT, _handle_interrupt)   ← traps Ctrl+C
├── [NEW] _acquire_palace_lock(palace_path)           ← prevents concurrent mine
├── for filepath in files:
│   ├── [NEW] if interrupted: break                   ← clean Ctrl+C exit
│   └── process_file()
│       └── add_drawer() × N chunks                   ← unchanged (add_drawers_batch is dead)
└── finally:
    ├── [NEW] signal.signal(SIGINT, old_handler)      ← restore original handler
    └── [NEW] _release_palace_lock(lock)              ← release lock + delete file

The lock + SIGINT handling correctly wraps the critical section. The finally block ensures cleanup regardless of how mine() exits.


Created by Octocode MCP https://octocode.ai 🔍🐙

@bensig

bensig commented Apr 11, 2026

Copy link
Copy Markdown
Contributor

This conflicts with main and several of the fixes here (OOM, corruption guards) have been addressed in #387 and #399. If the concurrent mining lock is still needed, a focused PR for just that piece would be welcome.

@bensig bensig closed this Apr 11, 2026
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.

4 participants