js-rigor: fix batch operation ordering (last-write-wins), batch-order campaign
updateRelationsBatch previously pre-sorted ops remove->modify->add, which changed the final state whenever one tuple was touched by mixed kinds: [add, remove] left the tuple present, [modify, add, modify] ended with the middle value. Now ops apply strictly in the given order via the dedup-aware _addRelationInternal (in-place last-write-wins) with upfront validation, post-batch PLTC edge updates, and per-relation arbiter-level cache invalidation. batch-order-parity.test.js pins the contract with an in-order mirror (modify-of-missing is a silent no-op).
This commit is contained in:
@@ -382,33 +382,63 @@ export class RelationUpdates {
|
||||
if (this.manager.arbiter._snapshotReadOnly) {
|
||||
throw new Error('Cannot update relations in batch while in snapshot read-only mode. Disable snapshot before writing.');
|
||||
}
|
||||
const addOps = [];
|
||||
const removeOps = [];
|
||||
const modifyOps = [];
|
||||
|
||||
// Pre-sort operations for better cache locality
|
||||
var len = updates.length;
|
||||
for (var i = 0; i < len; i++) {
|
||||
const update = updates[i];
|
||||
switch (update.operation) {
|
||||
case 'add':
|
||||
addOps.push(update);
|
||||
break;
|
||||
case 'remove':
|
||||
removeOps.push(update);
|
||||
break;
|
||||
case 'modify':
|
||||
modifyOps.push(update);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unknown operation: ${update.operation}`);
|
||||
|
||||
// Validate every operation up front so a bad op cannot partially apply.
|
||||
for (const op of updates) {
|
||||
if (!op || (op.operation !== 'add' && op.operation !== 'remove' && op.operation !== 'modify')) {
|
||||
throw new Error(`Unknown operation: ${op.operation}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Process in optimal order: removes first, then modifications, then adds
|
||||
this._processBatchRemovals(removeOps, chunkSize);
|
||||
this._processBatchModifications(modifyOps, chunkSize);
|
||||
this._processBatchAdditions(addOps, chunkSize);
|
||||
|
||||
// Apply operations strictly in the given order. The previous
|
||||
// remove->modify->add pre-sort changed the final state whenever one
|
||||
// tuple was touched by mixed kinds: [add, remove] left the tuple
|
||||
// present, and [modify, add, modify] ended with the middle value
|
||||
// instead of the last one (last-write-wins).
|
||||
const arbiter = this.manager.arbiter;
|
||||
arbiter.batchUpdateInProgress = true;
|
||||
try {
|
||||
for (let i = 0; i < updates.length; i += chunkSize) {
|
||||
const chunk = updates.slice(i, i + chunkSize);
|
||||
for (const op of chunk) {
|
||||
switch (op.operation) {
|
||||
case 'add':
|
||||
this._addRelationInternal(op.srcKey, op.relation, op.dstKey, op.options);
|
||||
break;
|
||||
case 'remove':
|
||||
this.manager.removeRelation(op.srcKey, op.relation, op.dstKey);
|
||||
break;
|
||||
case 'modify':
|
||||
this._modifyRelation(op.srcKey, op.relation, op.dstKey, op.options);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
arbiter.batchUpdateInProgress = false;
|
||||
}
|
||||
|
||||
// After the batch completes, update PLTC indices for all touched edges
|
||||
// (adds and removes both affect reachability).
|
||||
const reachabilityChecker = arbiter.reachabilityChecker;
|
||||
if (reachabilityChecker &&
|
||||
(reachabilityChecker.pltcIndex?.initialized ||
|
||||
reachabilityChecker.pltcBackwardIndex?.initialized)) {
|
||||
const edgesToAdd = new Set();
|
||||
for (const op of updates) {
|
||||
const srcId = arbiter.nodeIdByKey.get(op.srcKey);
|
||||
const dstId = arbiter.nodeIdByKey.get(op.dstKey);
|
||||
if (srcId !== undefined && dstId !== undefined) {
|
||||
edgesToAdd.add(`${srcId}:${dstId}`);
|
||||
}
|
||||
}
|
||||
reachabilityChecker.beginUpdateBatch();
|
||||
for (const edgeKey of edgesToAdd) {
|
||||
const [srcId, dstId] = edgeKey.split(':').map(Number);
|
||||
this.manager._updatePLTCIndicesOnAdd(srcId, dstId, null);
|
||||
}
|
||||
reachabilityChecker.commitUpdateBatch();
|
||||
}
|
||||
|
||||
// The batch path bypasses Arbiter.addRelation/removeRelation, so the
|
||||
// arbiter-level rule caches (rule result cache, ChainRule caches,
|
||||
|
||||
Reference in New Issue
Block a user