{"record":{"id":"4f26a560d73a38bb","repo":"overleaf/overleaf","slug":"base-version-mismatch","errorCode":null,"errorMessage":"base version mismatch","messagePattern":"base version mismatch","errorType":"exception","errorClass":"BaseVersionConflictError","httpStatus":409,"severity":"error","filePath":"services/history-v1/storage/lib/chunk_store/redis.js","lineNumber":210,"sourceCode":"    const args = [\n      baseVersion.toString(),\n      JSON.stringify(headSnapshot.toRaw()),\n      persistTime.toString(),\n      expireTime.toString(),\n      onlyIfExists.toString(), // Only queue changes if the snapshot already exists\n      ...changes.map(change => JSON.stringify(change.toRaw())), // Serialize changes\n    ]\n\n    const status = await rclient.queue_changes(keys, args)\n    metrics.inc('chunk_store.redis.queue_changes', 1, { status })\n    if (status === 'ok') {\n      return status\n    }\n    if (status === 'ignore') {\n      return status // skip changes when project does not exist and onlyIfExists is true\n    }\n    if (status === 'conflict') {\n      throw new BaseVersionConflictError('base version mismatch', {\n        projectId,\n        baseVersion,\n      })\n    } else {\n      throw new OError('unexpected result queuing changes', { status })\n    }\n  } catch (err) {\n    if (err instanceof BaseVersionConflictError) {\n      // Re-throw conflict errors directly\n      throw err\n    }\n    metrics.inc('chunk_store.redis.queue_changes', 1, { status: 'error' })\n    throw err\n  }\n}\n\nrclient.defineCommand('get_state', {\n  numberOfKeys: 6, // Number of keys defined in keySchema","sourceCodeStart":192,"sourceCodeEnd":228,"githubUrl":"https://github.com/overleaf/overleaf/blob/28ad3b03b71cb4311decdcb55c36b33ec10d72db/services/history-v1/storage/lib/chunk_store/redis.js#L192-L228","documentation":"queueChanges pushes changes onto the Redis pending-changes list guarded by the project's head version. When the Lua script reports status 'conflict', the caller's baseVersion does not match the current head version, and a BaseVersionConflictError is thrown with both values attached. This is the store's optimistic-concurrency mechanism protecting the change sequence.","triggerScenarios":"Calling queueChanges with a baseVersion computed from a stale head snapshot while another client has already appended changes; two editors publishing concurrently from the same base version.","commonSituations":"Long-lived editor sessions whose head version fell behind after reconnects; a second tab/device editing the same project; a retry of an old queued batch after the version advanced.","solutions":["Catch BaseVersionConflictError, reload the latest head snapshot, rebase the changes onto it, and retry queueChanges with the new baseVersion.","Before queuing, fetch the current head version and refuse/refresh when it differs from your baseVersion.","Ensure each client session refreshes its base version after any conflict or reconnect instead of retrying blindly."],"exampleFix":"// before\nawait queueChanges(projectId, snapshot, baseVersion, changes)\n\n// after\ntry {\n  await queueChanges(projectId, snapshot, baseVersion, changes)\n} catch (err) {\n  if (err instanceof BaseVersionConflictError) {\n    const fresh = await chunkStore.loadHead(projectId)\n    const rebased = rebaseChanges(changes, snapshot, fresh.getSnapshot())\n    await queueChanges(projectId, fresh.getSnapshot(), fresh.getVersion(), rebased)\n  } else {\n    throw err\n  }\n}","handlingStrategy":"retry","validationCode":"const head = await chunkStore.getProjectHeadVersion(projectId); if (head !== baseVersion) { await refreshBaseSnapshot(); }","typeGuard":"function isBaseVersionConflict(err) { return err instanceof BaseVersionConflictError }","tryCatchPattern":"try { await queueChanges(projectId, snapshot, baseVersion, changes) } catch (err) { if (isBaseVersionConflict(err)) { await rebaseAndRetry(projectId, changes, err.info) } else { throw err } }","preventionTips":["Re-read the head version right before queuing after any idle period or reconnect.","Rebase changes onto the current snapshot instead of retrying with a stale base.","Cap retry attempts and surface a merge conflict to the user after repeated conflicts."],"tags":["redis","chunk-store","concurrency","version-conflict","history-v1"],"backgroundTag":"optimistic-lock-conflict","analyzedSha":"28ad3b03b71cb4311decdcb55c36b33ec10d72db","analyzedAt":"2026-09-03T02:10:22.807Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-10T07:17:11.731Z"}