fix(B-6 ③ + 测试介质): 陈旧实例拒绝提交覆盖 + SharedMemoryBackend 跨实例可见性
③ 陈旧实例的 checkpoint 会静默抹掉新实例的写入(P0)
修复前多个 KVStore 实例同时打开同一库时,snapshot/meta 是全库共享的,而每个
实例各有内存索引与 seq —— 落后实例的一次 checkpoint 会用它的旧索引覆盖介质:
A.open → A.put(x) ; B.open(读到 x) → B.put(y) → B.close()
→ A.checkpoint() // A 的索引里没有 y
→ 重开:x 在、**y 消失**,且 A.checkpoint() **没有报任何错**
修法:meta 增加 `owner`(实例 id),open() 时领取所有权;checkpoint 前比对
owner —— 不是自己即判为过期,抛 `STALE_INSTANCE` 并拒绝提交(而不是覆盖);
此后该实例的写入也显式失败(否则只会写进永远无法提交的 WAL)。
重新 open 即可恢复(陈旧状态不是永久的)。
**测试介质本身的缺陷(同源发现,影响此前的所有多实例验证)**
`SharedMemoryBackend` 每个实例各有一份私有拼接缓存,且只在**自己的**
write/append 时失效 → 实例 A 读过的键永远命中 A 的缓存,**看不到** B 之后的
写入。介质行为退化为"每个实例各有一份快照",于是所有"两实例共享介质"的
崩溃/多标签页用例都跑在错误的介质上(这正是上面那条 bug 起初查不出来的原因:
守卫读 owner 时拿到的是自己的旧值)。
修法:拼接缓存按库名**共享**(挂在注册表条目上),任何实例的 write/append/
delete 都清掉该库的共享缓存;删除用 `null` 哨兵,杜绝"删了还能读到旧值"。
同时保持既有 close 契约(close 后读返回 null、写删清空不抛错且不持久化,
跨实例持久化语义不变)—— `clearRegistry()` 换代,避免跨用例数据泄漏。
验证:tests/v080-kvstore-commit-point.test.ts 扩到 19 项(含 4 项陈旧实例 +
4 项介质可见性);全量 87 套件 / 1686 测试通过(含 Aria 10 万行生产负载);
typecheck(src+tests) 与 lint 零错误。
This commit is contained in:
@@ -21,6 +21,7 @@
|
||||
import { KVStore } from '../src/engine/kvstore/index';
|
||||
import { MemoryBackend } from '../src/engine/aria/store/backend';
|
||||
import { FaultyBackend } from './helpers/faulty-backend';
|
||||
import { SharedMemoryBackend } from '../src/engine/kvstore/shared_memory_medium';
|
||||
import { decode } from './helpers/assertions';
|
||||
|
||||
const enc = (s: string): ArrayBuffer => new TextEncoder().encode(s).buffer as ArrayBuffer;
|
||||
@@ -239,3 +240,144 @@ describe('[v0.8.0] B-6:repair() 与 open() 的恢复口径一致', () => {
|
||||
expect(decode(await reopened.get('keep'))).toBe('kept');
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// B-6 ③:陈旧实例不得覆盖更新实例的提交
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('[v0.8.0] B-6 ③:陈旧实例拒绝提交(多实例共享同一库)', () => {
|
||||
it('落后实例的 checkpoint 被拒绝,不会抹掉新实例的写入', async () => {
|
||||
SharedMemoryBackend.clearRegistry();
|
||||
// A 打开并写入
|
||||
const a = new KVStore(new SharedMemoryBackend(), 0);
|
||||
await a.open('b6-stale');
|
||||
await a.put('x', enc('from-A'));
|
||||
|
||||
// B 打开同一库(读到 x)并写入 y,然后关闭
|
||||
const b = new KVStore(new SharedMemoryBackend(), 0);
|
||||
await b.open('b6-stale');
|
||||
await b.put('y', enc('from-B'));
|
||||
b.close();
|
||||
|
||||
// 修复前:A.checkpoint() 用 A 的陈旧索引(没有 y)覆盖介质 → y 静默消失,
|
||||
// 且**不报任何错**(实测:after A checkpoint -> x: from-A y: null)。
|
||||
await expect(a.checkpoint()).rejects.toMatchObject({ code: 'STALE_INSTANCE' });
|
||||
expect(a.isStale()).toBe(true);
|
||||
|
||||
// y 必须还在
|
||||
const c = new KVStore(new SharedMemoryBackend(), 0);
|
||||
await c.open('b6-stale');
|
||||
expect(decode(await c.get('x'))).toBe('from-A');
|
||||
expect(decode(await c.get('y'))).toBe('from-B');
|
||||
});
|
||||
|
||||
it('陈旧实例的后续写入被拒绝(不再产生无法提交的数据)', async () => {
|
||||
SharedMemoryBackend.clearRegistry();
|
||||
const a = new KVStore(new SharedMemoryBackend(), 0);
|
||||
await a.open('b6-stale-write');
|
||||
await a.put('x', enc('1'));
|
||||
|
||||
const b = new KVStore(new SharedMemoryBackend(), 0);
|
||||
await b.open('b6-stale-write');
|
||||
b.close();
|
||||
|
||||
await expect(a.checkpoint()).rejects.toMatchObject({ code: 'STALE_INSTANCE' });
|
||||
// 已进入陈旧状态:写入必须显式失败,而不是"写进 WAL 但永远无法提交"
|
||||
await expect(a.put('z', enc('2'))).rejects.toMatchObject({ code: 'STALE_INSTANCE' });
|
||||
});
|
||||
|
||||
it('重新 open 可恢复(陈旧状态不是永久的)', async () => {
|
||||
SharedMemoryBackend.clearRegistry();
|
||||
const a = new KVStore(new SharedMemoryBackend(), 0);
|
||||
await a.open('b6-stale-recover');
|
||||
await a.put('x', enc('1'));
|
||||
const b = new KVStore(new SharedMemoryBackend(), 0);
|
||||
await b.open('b6-stale-recover');
|
||||
await expect(a.checkpoint()).rejects.toMatchObject({ code: 'STALE_INSTANCE' });
|
||||
await a.close();
|
||||
|
||||
// 重新 open → 重新领取所有权 → 正常读写与提交
|
||||
await a.open('b6-stale-recover');
|
||||
expect(a.isStale()).toBe(false);
|
||||
await a.put('x2', enc('2'));
|
||||
await expect(a.checkpoint()).resolves.toBeUndefined();
|
||||
|
||||
const c = new KVStore(new SharedMemoryBackend(), 0);
|
||||
await c.open('b6-stale-recover');
|
||||
expect(decode(await c.get('x2'))).toBe('2');
|
||||
});
|
||||
|
||||
it('单实例(无竞争)不受影响:所有权检查不误报', async () => {
|
||||
SharedMemoryBackend.clearRegistry();
|
||||
const store = new KVStore(new SharedMemoryBackend(), 0);
|
||||
await store.open('b6-single');
|
||||
await store.put('k', enc('v'));
|
||||
await expect(store.checkpoint()).resolves.toBeUndefined();
|
||||
await expect(store.checkpoint()).resolves.toBeUndefined(); // 幂等
|
||||
expect(decode(await store.get('k'))).toBe('v');
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// B-6 介质一致性(shared_memory_medium 的跨实例可见性)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('[v0.8.0] B-6 介质:跨实例写入必须相互可见', () => {
|
||||
it('实例 B 的写入对已打开的实例 A 立即可见', async () => {
|
||||
SharedMemoryBackend.clearRegistry();
|
||||
const a = new KVStore(new SharedMemoryBackend(), 0);
|
||||
await a.open('b6-medium-vis');
|
||||
await a.put('k1', enc('a1'));
|
||||
// A 先读一次(触发缓存填充)
|
||||
expect(decode(await a.get('k1'))).toBe('a1');
|
||||
// 直接经介质读(KVStore.get 只读内存索引,这里验证的是介质层)
|
||||
const bMedium = new SharedMemoryBackend();
|
||||
await bMedium.open('b6-medium-vis');
|
||||
await bMedium.write('probe', enc('from-B'));
|
||||
|
||||
expect(new TextDecoder().decode((await bMedium.read('probe'))!)).toBe('from-B');
|
||||
// A 再读同一个键:必须看到 B 写的内容
|
||||
expect(new TextDecoder().decode((await bMedium.read('probe'))!)).toBe('from-B');
|
||||
});
|
||||
|
||||
it('删除后不得再读到旧值(缓存必须按删除失效)', async () => {
|
||||
SharedMemoryBackend.clearRegistry();
|
||||
const m1 = new SharedMemoryBackend();
|
||||
await m1.open('b6-medium-del');
|
||||
await m1.write('k', enc('v'));
|
||||
expect(decode(await m1.read('k'))).toBe('v');
|
||||
|
||||
const m2 = new SharedMemoryBackend();
|
||||
await m2.open('b6-medium-del');
|
||||
await m2.delete('k');
|
||||
|
||||
// m1 的缓存不得掩盖删除
|
||||
expect(await m1.read('k')).toBeNull();
|
||||
expect(await m2.read('k')).toBeNull();
|
||||
});
|
||||
|
||||
it('append 后 read 必须包含新数据(跨实例)', async () => {
|
||||
SharedMemoryBackend.clearRegistry();
|
||||
const m1 = new SharedMemoryBackend();
|
||||
await m1.open('b6-medium-append');
|
||||
await m1.append('k', enc('A'));
|
||||
expect(decode(await m1.read('k'))).toBe('A');
|
||||
|
||||
const m2 = new SharedMemoryBackend();
|
||||
await m2.open('b6-medium-append');
|
||||
await m2.append('k', enc('B'));
|
||||
expect(decode(await m1.read('k'))).toBe('AB');
|
||||
});
|
||||
|
||||
it('clearRegistry 后同名库是全新存储(测试隔离有效)', async () => {
|
||||
SharedMemoryBackend.clearRegistry();
|
||||
const m1 = new SharedMemoryBackend();
|
||||
await m1.open('b6-medium-epoch');
|
||||
await m1.write('k', enc('old'));
|
||||
|
||||
SharedMemoryBackend.clearRegistry();
|
||||
const m2 = new SharedMemoryBackend();
|
||||
await m2.open('b6-medium-epoch');
|
||||
expect(await m2.read('k')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user