Skip to content

[BUG] Lock contention between upgrade CLI and plugin causes writes to fail #632

@jlin53882

Description

@jlin53882

Issue #632 - Final Analysis and Solution

Problem Confirmed

memory-upgrader.ts currently processes each entry with LLM enrichment AND store.update() inside the same lock cycle:

// Current flow (problematic)
for (const entry of batch) {
  await this.upgradeEntry(entry, noLlm);  // LLM + lock per entry
}

This causes:

  1. N lock acquisitions for N entries = high contention
  2. Plugin waits seconds while LLM runs between lock acquisitions
  3. Cross-session lock contention between upgrade CLI and plugin

Solution: Two-Phase Approach

Separate LLM enrichment from DB writes:

// Phase 1: LLM enrichment - NO LOCK
const enriched = await Promise.all(
  batch.map(entry => this.prepareEntry(entry, noLlm))
);

// Phase 2: Single lock for ALL DB writes
await this.store.runWithFileLock(async () => {
  for (const entry of enriched) {
    await this.store.update(entry.id, {
      text: entry.l0_abstract,
      metadata: entry.metadata,
    });
  }
});

Why This Works

Phase Lock Plugin Impact
Phase 1 (LLM) No None
Phase 2 (DB writes) Yes Plugin waits only for DB write time (ms)

Comparison

Approach Lock Count (batch=10) Plugin Wait Time
Current 10 Seconds (LLM time)
Fixed 1 Milliseconds (DB write time)

Key Changes Required

  1. Extract prepareEntry() - Separate LLM logic from upgradeEntry()
  2. Modify upgrade() loop - Two-phase processing
  3. No new API needed - Use existing store.update() in lock

Unit Tests Needed

// Test 1: Lock acquired once per batch
test("upgrade acquires lock once per batch", async () => {
  let lockCount = 0;
  store.runWithFileLock = async (fn) => {
    lockCount++;
    return fn();
  };
  await upgrader.upgrade({ batchSize: 5 });
  assert.equal(lockCount, 1);  // Not 5
});

// Test 2: LLM runs before lock
test("LLM enrichment happens before lock acquisition", async () => {
  const order = [];
  store.runWithFileLock = async (fn) => {
    order.push("lock-start");
    await fn();
    order.push("lock-end");
  };
  upgrader.prepareEntry = async () => {
    order.push("llm");
    return mockEnriched;
  };
  await upgrader.upgradeBatch([entry]);
  const llmIdx = order.indexOf("llm");
  const lockIdx = order.indexOf("lock-start");
  assert.ok(llmIdx < lockIdx, "LLM should run before lock");
});

Integration Test (Future)

Cross-session test requires real processes, not mocks:

test("cross-session: upgrade and plugin writes both succeed", async () => {
  // Requires spawning real processes
  // Verifies proper-lockfile behavior across processes
});

Implementation Plan

  1. Extract prepareEntry() from upgradeEntry()
  2. Modify upgrade() to use two-phase approach
  3. Add unit tests
  4. Test with real upgrade + plugin running concurrently

Related Issues

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions