Skip to content

Commit daef1fb

Browse files
most tests working
1 parent ae207dd commit daef1fb

File tree

5 files changed

+118
-73
lines changed

5 files changed

+118
-73
lines changed

packages/dds/matrix/src/matrix.ts

Lines changed: 88 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,11 @@ type FirstWriterWinsPolicy =
213213
cellLastWriteTracker: SparseArray2D<CellLastWriteTrackerItem>;
214214
};
215215

216+
interface PendingChanges<T> {
217+
local: { localSeq: number; value: MatrixItem<T> }[];
218+
consensus?: MatrixItem<T> | undefined;
219+
}
220+
216221
/**
217222
* A SharedMatrix holds a rectangular 2D array of values. Supported operations
218223
* include setting values and inserting/removing rows and columns.
@@ -252,7 +257,7 @@ export class SharedMatrix<T = any>
252257
private readonly cols: PermutationVector; // Map logical col to storage handle (if any)
253258

254259
private cells = new SparseArray2D<MatrixItem<T>>(); // Stores cell values.
255-
private readonly pending = new SparseArray2D<number[]>(); // Tracks pending writes.
260+
private readonly pending = new SparseArray2D<PendingChanges<T>>(); // Tracks pending writes.
256261

257262
private fwwPolicy: FirstWriterWinsPolicy = {
258263
state: "off",
@@ -418,21 +423,22 @@ export class SharedMatrix<T = any>
418423
value: MatrixItem<T>,
419424
rowHandle = this.rows.getAllocatedHandle(row),
420425
colHandle = this.cols.getAllocatedHandle(col),
426+
rollback?: boolean,
421427
): void {
422428
this.protectAgainstReentrancy(() => {
423-
if (this.undo !== undefined) {
424-
let oldValue = this.cells.getCell(rowHandle, colHandle);
425-
if (oldValue === null) {
426-
oldValue = undefined;
427-
}
429+
const oldValue = this.cells.getCell(rowHandle, colHandle) ?? undefined;
428430

431+
if (this.undo !== undefined) {
429432
this.undo.cellSet(rowHandle, colHandle, oldValue);
430433
}
431434

432435
this.cells.setCell(rowHandle, colHandle, value);
433436

434-
if (this.isAttached()) {
435-
this.sendSetCellOp(row, col, value, rowHandle, colHandle);
437+
if (this.isAttached() && rollback !== true) {
438+
const pending = this.sendSetCellOp(row, col, value, rowHandle, colHandle);
439+
if (pending.local.length === 1) {
440+
pending.consensus ??= oldValue;
441+
}
436442
}
437443

438444
// Avoid reentrancy by raising change notifications after the op is queued.
@@ -467,7 +473,7 @@ export class SharedMatrix<T = any>
467473
rowHandle: Handle,
468474
colHandle: Handle,
469475
localSeq = this.nextLocalSeq(),
470-
): void {
476+
): PendingChanges<T> {
471477
assert(
472478
this.isAttached(),
473479
0x1e2 /* "Caller must ensure 'isAttached()' before calling 'sendSetCellOp'." */,
@@ -493,9 +499,12 @@ export class SharedMatrix<T = any>
493499
};
494500

495501
this.submitLocalMessage(op, metadata);
496-
const pending = this.pending.getCell(rowHandle, colHandle) ?? [];
497-
pending.push(localSeq);
502+
const pending: PendingChanges<T> = this.pending.getCell(rowHandle, colHandle) ?? {
503+
local: [],
504+
};
505+
pending.local.push({ localSeq, value });
498506
this.pending.setCell(rowHandle, colHandle, pending);
507+
return pending;
499508
}
500509

501510
/**
@@ -820,10 +829,12 @@ export class SharedMatrix<T = any>
820829

821830
const pending = this.pending.getCell(rowHandle, colHandle);
822831
assert(pending !== undefined, "local operation must have a pending array");
823-
const localSeqIndex = pending.indexOf(localSeq);
832+
const { local } = pending;
833+
assert(local !== undefined, "local operation must have a pending array");
834+
const localSeqIndex = local.findIndex((p) => p.localSeq === localSeq);
824835
assert(localSeqIndex >= 0, "local operation must have a pending entry");
825-
const [pendingSeq] = pending.splice(localSeqIndex, 1);
826-
assert(pendingSeq === localSeq, "must match");
836+
const [change] = local.splice(localSeqIndex, 1);
837+
assert(change.localSeq === localSeq, "must match");
827838

828839
if (
829840
row !== undefined &&
@@ -857,6 +868,47 @@ export class SharedMatrix<T = any>
857868
}
858869
}
859870

871+
protected rollback(content: unknown, localOpMetadata: unknown): void {
872+
const contents = content as MatrixSetOrVectorOp<T>;
873+
const target = contents.target;
874+
875+
switch (target) {
876+
case SnapshotPath.cols: {
877+
this.cols.rollback(content, localOpMetadata);
878+
break;
879+
}
880+
case SnapshotPath.rows: {
881+
this.rows.rollback(content, localOpMetadata);
882+
break;
883+
}
884+
case undefined: {
885+
assert(contents.type === MatrixOp.set, "only sets supported");
886+
const setMetadata = localOpMetadata as ISetOpMetadata;
887+
888+
const pending = this.pending.getCell(setMetadata.rowHandle, setMetadata.colHandle);
889+
assert(pending !== undefined, "must have pending");
890+
891+
const change = pending.local.pop();
892+
assert(change?.localSeq === setMetadata.localSeq, "must have change");
893+
894+
const previous =
895+
pending.local.length > 0
896+
? pending.local[pending.local.length - 1].value
897+
: pending.consensus;
898+
899+
this.setCellCore(
900+
contents.row,
901+
contents.col,
902+
previous,
903+
setMetadata.rowHandle,
904+
setMetadata.colHandle,
905+
true,
906+
);
907+
}
908+
default:
909+
}
910+
}
911+
860912
protected onDisconnect(): void {}
861913

862914
/**
@@ -989,14 +1041,20 @@ export class SharedMatrix<T = any>
9891041
this.cols.removeLocalReferencePosition(colsRef);
9901042

9911043
const pending = this.pending.getCell(rowHandle, colHandle);
992-
assert(pending?.shift() === localSeq, "must match");
1044+
const ackedChange = pending?.local.shift();
1045+
assert(ackedChange?.localSeq === localSeq, "must match");
1046+
if (pending?.local.length === 0) {
1047+
this.pending.setCell(rowHandle, colHandle, undefined);
1048+
}
9931049

9941050
// If policy is switched and cell should be modified too based on policy, then update the tracker.
9951051
// If policy is not switched, then also update the tracker in case it is the latest.
9961052
if (
9971053
this.fwwPolicy.state === "on" &&
9981054
this.shouldSetCellBasedOnFWW(rowHandle, colHandle, msg)
9991055
) {
1056+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1057+
pending!.consensus = ackedChange.value;
10001058
this.fwwPolicy.cellLastWriteTracker.setCell(rowHandle, colHandle, {
10011059
seqNum: msg.sequenceNumber,
10021060
clientId: msg.clientId,
@@ -1015,6 +1073,7 @@ export class SharedMatrix<T = any>
10151073
isHandleValid(rowHandle) && isHandleValid(colHandle),
10161074
0x022 /* "SharedMatrix row and/or col handles are invalid!" */,
10171075
);
1076+
const pending = this.pending.getCell(rowHandle, colHandle);
10181077
if (this.fwwPolicy.state === "on") {
10191078
// If someone tried to Overwrite the cell value or first write on this cell or
10201079
// same client tried to modify the cell or if the previous mode was LWW, then we need to still
@@ -1026,11 +1085,14 @@ export class SharedMatrix<T = any>
10261085
seqNum: msg.sequenceNumber,
10271086
clientId: msg.clientId,
10281087
});
1088+
if (pending !== undefined) {
1089+
pending.consensus = value;
1090+
}
10291091
for (const consumer of this.consumers.values()) {
10301092
consumer.cellsChanged(adjustedRow, adjustedCol, 1, 1, this);
10311093
}
10321094
// Check is there are any pending changes, which will be rejected. If so raise conflict.
1033-
if ((this.pending.getCell(rowHandle, colHandle)?.length ?? 0) > 0) {
1095+
if (pending !== undefined && pending.local.length > 0) {
10341096
// Don't reset the pending value yet, as there maybe more fww op from same client, so we want
10351097
// to raise conflict event for that op also.
10361098
this.emit(
@@ -1043,12 +1105,16 @@ export class SharedMatrix<T = any>
10431105
);
10441106
}
10451107
}
1046-
} else if ((this.pending.getCell(rowHandle, colHandle)?.length ?? 0) === 0) {
1047-
// If there is a pending (unACKed) local write to the same cell, skip the current op
1048-
// since it "happened before" the pending write.
1049-
this.cells.setCell(rowHandle, colHandle, value);
1050-
for (const consumer of this.consumers.values()) {
1051-
consumer.cellsChanged(adjustedRow, adjustedCol, 1, 1, this);
1108+
} else {
1109+
if (pending === undefined || pending.local.length === 0) {
1110+
// If there is a pending (unACKed) local write to the same cell, skip the current op
1111+
// since it "happened before" the pending write.
1112+
this.cells.setCell(rowHandle, colHandle, value);
1113+
for (const consumer of this.consumers.values()) {
1114+
consumer.cellsChanged(adjustedRow, adjustedCol, 1, 1, this);
1115+
}
1116+
} else {
1117+
pending.consensus = value;
10521118
}
10531119
}
10541120
}

packages/test/local-server-stress-tests/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@
7676
"@fluidframework/id-compressor": "workspace:~",
7777
"@fluidframework/local-driver": "workspace:~",
7878
"@fluidframework/map": "workspace:~",
79+
"@fluidframework/matrix": "workspace:~",
7980
"@fluidframework/runtime-definitions": "workspace:~",
8081
"@fluidframework/runtime-utils": "workspace:~",
8182
"@fluidframework/sequence": "workspace:~",
@@ -106,7 +107,8 @@
106107
"^api-extractor:commonjs",
107108
"@fluidframework/id-compressor#build:test",
108109
"@fluidframework/sequence#build:test",
109-
"@fluidframework/map#build:test"
110+
"@fluidframework/map#build:test",
111+
"@fluidframework/matrix#build:test"
110112
]
111113
}
112114
},

packages/test/local-server-stress-tests/src/ddsModels.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import { DDSFuzzModel, DDSFuzzTestState } from "@fluid-private/test-dds-utils";
88
import type { IChannelFactory } from "@fluidframework/datastore-definitions/internal";
99
// eslint-disable-next-line import/no-internal-modules
1010
import { baseMapModel, baseDirModel } from "@fluidframework/map/internal/test";
11+
// eslint-disable-next-line import/no-internal-modules
12+
import { baseSharedMatrixModel } from "@fluidframework/matrix/internal/test";
1113
import {
1214
baseSharedStringModel,
1315
baseIntervalModel,
@@ -65,4 +67,5 @@ export const ddsModelMap = generateSubModelMap(
6567
baseDirModel,
6668
baseSharedStringModel,
6769
baseIntervalModel,
70+
baseSharedMatrixModel,
6871
);

packages/test/local-server-stress-tests/src/test/localServerStressOrderSequentially.spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ describe("Local Server Stress with rollback", () => {
9191
saveFailures,
9292
// saveSuccesses,
9393
configurations: { "Fluid.ContainerRuntime.EnableRollback": true },
94+
only: [91],
9495
skip: [
9596
...[12, 28, 30], // Key not found or value not matching key
9697
...[15, 38, 51, 63], // Number of keys not same (directory)

0 commit comments

Comments
 (0)