feat: simplified simulation

This commit is contained in:
Leyla Becker 2026-02-14 18:14:08 -06:00
parent 463706e6a5
commit 87cf9f2c6c
2 changed files with 564 additions and 254 deletions

View file

@ -85,9 +85,8 @@ interface DataRecord<Data> {
interface Tombstone {
readonly id: string;
readonly frozenRecordHLL: HLL;
readonly recordHLL: HLL;
readonly tombstoneHLL: HLL;
readonly isKeeper: boolean;
}
interface NodeState<Data> {
@ -114,9 +113,8 @@ const createRecord = <Data>(id: string, data: Data, nodeId: string): DataRecord<
const createTombstone = <Data>(record: DataRecord<Data>, nodeId: string): Tombstone => ({
id: record.id,
frozenRecordHLL: hllClone(record.recordHLL),
recordHLL: hllClone(record.recordHLL),
tombstoneHLL: hllAdd(createHLL(), nodeId),
isKeeper: false,
});
const createNode = <Data>(id: string): NodeState<Data> => ({
@ -138,34 +136,32 @@ const checkGCStatus = (
myTombstoneEstimateBeforeMerge: number,
myNodeId: string,
senderNodeId: string | null
): { shouldGC: boolean; becomeKeeper: boolean; stepDownAsKeeper: boolean } => {
const targetCount = hllEstimate(tombstone.frozenRecordHLL);
const tombstoneCount = hllEstimate(tombstone.tombstoneHLL);
): { shouldGC: boolean; stepDownAsKeeper: boolean } => {
const targetCount = hllEstimate(tombstone.recordHLL);
if (tombstone.isKeeper) {
const isKeeper = myTombstoneEstimateBeforeMerge >= targetCount;
if (isKeeper) {
// Keeper step-down logic:
// If incoming tombstone has reached the target count, compare estimates.
// If incoming estimate >= my estimate before merge, step down.
// Use node ID as tie-breaker: higher node ID steps down when estimates are equal.
if (incomingTombstoneEstimate !== null && incomingTombstoneEstimate >= targetCount) {
if (myTombstoneEstimateBeforeMerge < incomingTombstoneEstimate) {
return { shouldGC: true, becomeKeeper: false, stepDownAsKeeper: true };
return { shouldGC: true, stepDownAsKeeper: true };
}
// Tie-breaker: if estimates are equal, the lexicographically higher node ID steps down
if (myTombstoneEstimateBeforeMerge === incomingTombstoneEstimate &&
senderNodeId !== null && myNodeId > senderNodeId) {
return { shouldGC: true, becomeKeeper: false, stepDownAsKeeper: true };
return { shouldGC: true, stepDownAsKeeper: true };
}
}
return { shouldGC: false, becomeKeeper: false, stepDownAsKeeper: false };
return { shouldGC: false, stepDownAsKeeper: false };
}
// Become keeper when tombstone count reaches target (all record holders have acknowledged)
if (tombstoneCount >= targetCount) {
return { shouldGC: false, becomeKeeper: true, stepDownAsKeeper: false };
}
return { shouldGC: false, becomeKeeper: false, stepDownAsKeeper: false };
// Not yet a keeper - will become one if tombstone count reaches target after merge
// (No explicit action needed here, keeper status is inferred from HLL comparison)
return { shouldGC: false, stepDownAsKeeper: false };
};
const receiveRecord = <Data>(
@ -206,21 +202,20 @@ const receiveTombstone = <Data>(
? hllAdd(hllMerge(existing.tombstoneHLL, incoming.tombstoneHLL), node.id)
: hllAdd(hllClone(incoming.tombstoneHLL), node.id);
let bestFrozenHLL = incoming.frozenRecordHLL;
if (existing?.frozenRecordHLL) {
bestFrozenHLL = hllEstimate(existing.frozenRecordHLL) > hllEstimate(bestFrozenHLL)
? existing.frozenRecordHLL
let bestFrozenHLL = incoming.recordHLL;
if (existing?.recordHLL) {
bestFrozenHLL = hllEstimate(existing.recordHLL) > hllEstimate(bestFrozenHLL)
? existing.recordHLL
: bestFrozenHLL;
}
if (hllEstimate(record.recordHLL) > hllEstimate(bestFrozenHLL)) {
bestFrozenHLL = hllClone(record.recordHLL);
}
let updatedTombstone: Tombstone = {
const updatedTombstone: Tombstone = {
id: incoming.id,
tombstoneHLL: mergedTombstoneHLL,
frozenRecordHLL: bestFrozenHLL,
isKeeper: existing?.isKeeper ?? false,
recordHLL: bestFrozenHLL,
};
const myEstimateBeforeMerge = existing ? hllEstimate(existing.tombstoneHLL) : 0;
@ -245,10 +240,6 @@ const receiveTombstone = <Data>(
return { ...node, records: newRecords, tombstones: newTombstones, stats: newStats };
}
if (gcStatus.becomeKeeper) {
updatedTombstone = { ...updatedTombstone, isKeeper: true };
}
const newTombstones = new Map(node.tombstones);
newTombstones.set(incoming.id, updatedTombstone);
return { ...node, records: newRecords, tombstones: newTombstones, stats: newStats };
@ -397,14 +388,14 @@ const gossipOnce = <Data>(network: NetworkState<Data>, senderNodeId: string, rec
// Merge HLLs
const mergedTombstoneHLL = hllMerge(tombstone.tombstoneHLL, peerTombstone.tombstoneHLL);
const bestFrozenHLL = hllEstimate(peerTombstone.frozenRecordHLL) > hllEstimate(tombstone.frozenRecordHLL)
? peerTombstone.frozenRecordHLL
: tombstone.frozenRecordHLL;
const bestFrozenHLL = hllEstimate(peerTombstone.recordHLL) > hllEstimate(tombstone.recordHLL)
? peerTombstone.recordHLL
: tombstone.recordHLL;
let updatedSenderTombstone: Tombstone = {
const updatedSenderTombstone: Tombstone = {
...tombstone,
tombstoneHLL: mergedTombstoneHLL,
frozenRecordHLL: bestFrozenHLL,
recordHLL: bestFrozenHLL,
};
// Check if sender should step down (peer has higher estimate or wins tie-breaker)
@ -429,9 +420,6 @@ const gossipOnce = <Data>(network: NetworkState<Data>, senderNodeId: string, rec
newNodes = new Map(result.nodes);
} else {
// Keep tombstone with merged data
if (gcStatus.becomeKeeper) {
updatedSenderTombstone = { ...updatedSenderTombstone, isKeeper: true };
}
const currentSender = newNodes.get(senderNodeId)!;
const newSenderTombstones = new Map(currentSender.tombstones);
newSenderTombstones.set(recordId, updatedSenderTombstone);