feat: simplified simulation
This commit is contained in:
parent
463706e6a5
commit
87cf9f2c6c
2 changed files with 564 additions and 254 deletions
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue