Skip to content

Commit 9abbae7

Browse files
Merge branch 'sm/matrix-stress-stFWW' into sm/matrix-rollback
2 parents 95ecf6b + 45c1c97 commit 9abbae7

File tree

5 files changed

+132
-83
lines changed

5 files changed

+132
-83
lines changed

packages/dds/matrix/src/matrix.ts

Lines changed: 88 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,15 @@ export interface ISharedMatrix<T = any>
205205
switchSetCellPolicy(): void;
206206
}
207207

208+
type FirstWriterWinsPolicy =
209+
| { state: "off" }
210+
| { state: "local" }
211+
| {
212+
state: "on";
213+
switchOpSeqNumber: number;
214+
cellLastWriteTracker: SparseArray2D<CellLastWriteTrackerItem>;
215+
};
216+
208217
/**
209218
* A SharedMatrix holds a rectangular 2D array of values. Supported operations
210219
* include setting values and inserting/removing rows and columns.
@@ -245,10 +254,10 @@ export class SharedMatrix<T = any>
245254

246255
private cells = new SparseArray2D<MatrixItem<T>>(); // Stores cell values.
247256
private readonly pending = new SparseArray2D<number[]>(); // Tracks pending writes.
248-
private cellLastWriteTracker = new SparseArray2D<CellLastWriteTrackerItem>(); // Tracks last writes sequence number and clientId in a cell.
249-
// Tracks the seq number of Op at which policy switch happens from Last Write Win to First Write Win.
250-
private setCellLwwToFwwPolicySwitchOpSeqNumber: number;
251-
private userSwitchedSetCellPolicy = false; // Set to true when the user calls switchPolicy.
257+
258+
private fwwPolicy: FirstWriterWinsPolicy = {
259+
state: "off",
260+
};
252261

253262
// Used to track if there is any reentrancy in setCell code.
254263
private reentrantCount: number = 0;
@@ -268,7 +277,6 @@ export class SharedMatrix<T = any>
268277
) {
269278
super(id, runtime, attributes, "fluid_matrix_");
270279

271-
this.setCellLwwToFwwPolicySwitchOpSeqNumber = -1;
272280
this.rows = new PermutationVector(
273281
SnapshotPath.rows,
274282
this.logger,
@@ -334,7 +342,7 @@ export class SharedMatrix<T = any>
334342
}
335343

336344
public isSetCellConflictResolutionPolicyFWW(): boolean {
337-
return this.setCellLwwToFwwPolicySwitchOpSeqNumber > -1 || this.userSwitchedSetCellPolicy;
345+
return this.fwwPolicy.state !== "off";
338346
}
339347

340348
public getCell(row: number, col: number): MatrixItem<T> {
@@ -472,8 +480,7 @@ export class SharedMatrix<T = any>
472480
row,
473481
col,
474482
value,
475-
fwwMode:
476-
this.userSwitchedSetCellPolicy || this.setCellLwwToFwwPolicySwitchOpSeqNumber > -1,
483+
fwwMode: this.fwwPolicy.state !== "off",
477484
};
478485

479486
const rowsRef = this.createOpMetadataLocalRef(this.rows, row, localSeq);
@@ -673,15 +680,29 @@ export class SharedMatrix<T = any>
673680
SnapshotPath.cols,
674681
this.cols.summarize(this.runtime, this.handle, serializer),
675682
);
676-
const artifactsToSummarize = [
677-
this.cells.snapshot(),
678-
this.pending.snapshot(),
679-
this.setCellLwwToFwwPolicySwitchOpSeqNumber,
680-
];
683+
const artifactsToSummarize: (
684+
| undefined
685+
| number
686+
| ReturnType<SparseArray2D<MatrixItem<T> | number>["snapshot"]>
687+
)[] = [this.cells.snapshot(), this.pending.snapshot()];
681688

682689
// Only need to store it in the snapshot if we have switched the policy already.
683-
if (this.setCellLwwToFwwPolicySwitchOpSeqNumber > -1) {
684-
artifactsToSummarize.push(this.cellLastWriteTracker.snapshot());
690+
if (this.fwwPolicy.state === "on") {
691+
artifactsToSummarize.push(
692+
this.fwwPolicy.switchOpSeqNumber,
693+
this.fwwPolicy.cellLastWriteTracker.snapshot(),
694+
);
695+
} else {
696+
// back-compat: used -1 for disabled
697+
artifactsToSummarize.push(
698+
-1,
699+
/*
700+
* we should set undefined in place of cellLastWriteTracker to ensure the number of array entries is consistent.
701+
* Doing that currently breaks snapshot tests. Its is probably fine, but if new elements are ever added, we need
702+
* ensure undefined is also set.
703+
*/
704+
// undefined
705+
);
685706
}
686707
builder.addBlob(
687708
SnapshotPath.cells,
@@ -798,10 +819,6 @@ export class SharedMatrix<T = any>
798819
this.rows.removeLocalReferencePosition(rowsRef);
799820
this.cols.removeLocalReferencePosition(colsRef);
800821
if (row !== undefined && col !== undefined && row >= 0 && col >= 0) {
801-
const lastCellModificationDetails = this.cellLastWriteTracker.getCell(
802-
rowHandle,
803-
colHandle,
804-
);
805822
const pending = this.pending.getCell(rowHandle, colHandle);
806823
assert(pending !== undefined, "local operation must have a pending array");
807824
const localSeqIndex = pending.indexOf(localSeq);
@@ -813,9 +830,9 @@ export class SharedMatrix<T = any>
813830
// otherwise raise conflict. We want to check the current mode here and not that
814831
// whether op was made in FWW or not.
815832
if (
816-
this.setCellLwwToFwwPolicySwitchOpSeqNumber === -1 ||
817-
lastCellModificationDetails === undefined ||
818-
referenceSeqNumber >= lastCellModificationDetails.seqNum
833+
this.fwwPolicy.state !== "on" ||
834+
referenceSeqNumber >=
835+
(this.fwwPolicy.cellLastWriteTracker.getCell(rowHandle, colHandle)?.seqNum ?? 0)
819836
) {
820837
this.sendSetCellOp(
821838
row,
@@ -876,11 +893,21 @@ export class SharedMatrix<T = any>
876893
];
877894

878895
this.cells = SparseArray2D.load(cellData);
879-
this.setCellLwwToFwwPolicySwitchOpSeqNumber =
880-
setCellLwwToFwwPolicySwitchOpSeqNumber ?? -1;
881-
if (cellLastWriteTracker !== undefined) {
882-
this.cellLastWriteTracker = SparseArray2D.load(cellLastWriteTracker);
883-
}
896+
// back-compat: used -1 for disabled, also may not exist
897+
const switchOpSeqNumber =
898+
setCellLwwToFwwPolicySwitchOpSeqNumber === -1
899+
? undefined
900+
: (setCellLwwToFwwPolicySwitchOpSeqNumber ?? undefined);
901+
this.fwwPolicy =
902+
switchOpSeqNumber === undefined
903+
? {
904+
state: "off",
905+
}
906+
: {
907+
state: "on",
908+
switchOpSeqNumber,
909+
cellLastWriteTracker: SparseArray2D.load(cellLastWriteTracker),
910+
};
884911
} catch (error) {
885912
this.logger.sendErrorEvent({ eventName: "MatrixLoadFailed" }, error);
886913
}
@@ -896,11 +923,11 @@ export class SharedMatrix<T = any>
896923
message: ISequencedDocumentMessage,
897924
): boolean {
898925
assert(
899-
this.setCellLwwToFwwPolicySwitchOpSeqNumber > -1,
926+
this.fwwPolicy.state === "on",
900927
0x85f /* should be in Fww mode when calling this method */,
901928
);
902929
assert(message.clientId !== null, 0x860 /* clientId should not be null */);
903-
const lastCellModificationDetails = this.cellLastWriteTracker.getCell(
930+
const lastCellModificationDetails = this.fwwPolicy.cellLastWriteTracker.getCell(
904931
rowHandle,
905932
colHandle,
906933
);
@@ -949,11 +976,13 @@ export class SharedMatrix<T = any>
949976
);
950977

951978
const { row, col, value, fwwMode } = contents;
952-
const isPreviousSetCellPolicyModeFWW =
953-
this.setCellLwwToFwwPolicySwitchOpSeqNumber > -1;
954979
// If this is the first op notifying us of the policy change, then set the policy change seq number.
955-
if (this.setCellLwwToFwwPolicySwitchOpSeqNumber === -1 && fwwMode === true) {
956-
this.setCellLwwToFwwPolicySwitchOpSeqNumber = msg.sequenceNumber;
980+
if (fwwMode === true && this.fwwPolicy.state !== "on") {
981+
this.fwwPolicy = {
982+
state: "on",
983+
switchOpSeqNumber: msg.sequenceNumber,
984+
cellLastWriteTracker: new SparseArray2D(),
985+
};
957986
}
958987

959988
assert(msg.clientId !== null, 0x861 /* clientId should not be null!! */);
@@ -971,17 +1000,15 @@ export class SharedMatrix<T = any>
9711000

9721001
// if there are no more pending entries, the current must be the latest
9731002
//
974-
const isLatestPendingOp = pending.length === 0;
9751003
this.rows.removeLocalReferencePosition(rowsRef);
9761004
this.cols.removeLocalReferencePosition(colsRef);
9771005
// If policy is switched and cell should be modified too based on policy, then update the tracker.
9781006
// If policy is not switched, then also update the tracker in case it is the latest.
9791007
if (
980-
(this.setCellLwwToFwwPolicySwitchOpSeqNumber > -1 &&
981-
this.shouldSetCellBasedOnFWW(rowHandle, colHandle, msg)) ||
982-
(this.setCellLwwToFwwPolicySwitchOpSeqNumber === -1 && isLatestPendingOp)
1008+
this.fwwPolicy.state === "on" &&
1009+
this.shouldSetCellBasedOnFWW(rowHandle, colHandle, msg)
9831010
) {
984-
this.cellLastWriteTracker.setCell(rowHandle, colHandle, {
1011+
this.fwwPolicy.cellLastWriteTracker.setCell(rowHandle, colHandle, {
9851012
seqNum: msg.sequenceNumber,
9861013
clientId: msg.clientId,
9871014
});
@@ -999,17 +1026,14 @@ export class SharedMatrix<T = any>
9991026
isHandleValid(rowHandle) && isHandleValid(colHandle),
10001027
0x022 /* "SharedMatrix row and/or col handles are invalid!" */,
10011028
);
1002-
if (this.setCellLwwToFwwPolicySwitchOpSeqNumber > -1) {
1029+
if (this.fwwPolicy.state === "on") {
10031030
// If someone tried to Overwrite the cell value or first write on this cell or
10041031
// same client tried to modify the cell or if the previous mode was LWW, then we need to still
10051032
// overwrite the cell and raise conflict if we have pending changes as our change is going to be lost.
1006-
if (
1007-
!isPreviousSetCellPolicyModeFWW ||
1008-
this.shouldSetCellBasedOnFWW(rowHandle, colHandle, msg)
1009-
) {
1033+
if (this.shouldSetCellBasedOnFWW(rowHandle, colHandle, msg)) {
10101034
const previousValue = this.cells.getCell(rowHandle, colHandle);
10111035
this.cells.setCell(rowHandle, colHandle, value);
1012-
this.cellLastWriteTracker.setCell(rowHandle, colHandle, {
1036+
this.fwwPolicy.cellLastWriteTracker.setCell(rowHandle, colHandle, {
10131037
seqNum: msg.sequenceNumber,
10141038
clientId: msg.clientId,
10151039
});
@@ -1034,10 +1058,6 @@ export class SharedMatrix<T = any>
10341058
// If there is a pending (unACKed) local write to the same cell, skip the current op
10351059
// since it "happened before" the pending write.
10361060
this.cells.setCell(rowHandle, colHandle, value);
1037-
this.cellLastWriteTracker.setCell(rowHandle, colHandle, {
1038-
seqNum: msg.sequenceNumber,
1039-
clientId: msg.clientId,
1040-
});
10411061
for (const consumer of this.consumers.values()) {
10421062
consumer.cellsChanged(adjustedRow, adjustedCol, 1, 1, this);
10431063
}
@@ -1115,25 +1135,37 @@ export class SharedMatrix<T = any>
11151135
for (const rowHandle of rowHandles) {
11161136
this.cells.clearRows(/* rowStart: */ rowHandle, /* rowCount: */ 1);
11171137
this.pending.clearRows(/* rowStart: */ rowHandle, /* rowCount: */ 1);
1118-
this.cellLastWriteTracker.clearRows(/* rowStart: */ rowHandle, /* rowCount: */ 1);
1138+
if (this.fwwPolicy.state === "on") {
1139+
this.fwwPolicy.cellLastWriteTracker?.clearRows(
1140+
/* rowStart: */ rowHandle,
1141+
/* rowCount: */ 1,
1142+
);
1143+
}
11191144
}
11201145
};
11211146

11221147
private readonly onColHandlesRecycled = (colHandles: Handle[]): void => {
11231148
for (const colHandle of colHandles) {
11241149
this.cells.clearCols(/* colStart: */ colHandle, /* colCount: */ 1);
11251150
this.pending.clearCols(/* colStart: */ colHandle, /* colCount: */ 1);
1126-
this.cellLastWriteTracker.clearCols(/* colStart: */ colHandle, /* colCount: */ 1);
1151+
if (this.fwwPolicy.state === "on") {
1152+
this.fwwPolicy.cellLastWriteTracker?.clearCols(
1153+
/* colStart: */ colHandle,
1154+
/* colCount: */ 1,
1155+
);
1156+
}
11271157
}
11281158
};
11291159

11301160
public switchSetCellPolicy(): void {
1131-
if (this.setCellLwwToFwwPolicySwitchOpSeqNumber === -1) {
1132-
if (this.isAttached()) {
1133-
this.userSwitchedSetCellPolicy = true;
1134-
} else {
1135-
this.setCellLwwToFwwPolicySwitchOpSeqNumber = 0;
1136-
}
1161+
if (this.fwwPolicy.state === "off") {
1162+
this.fwwPolicy = this.isAttached()
1163+
? { state: "local" }
1164+
: {
1165+
state: "on",
1166+
switchOpSeqNumber: 0,
1167+
cellLastWriteTracker: new SparseArray2D(),
1168+
};
11371169
}
11381170
}
11391171

packages/dds/matrix/src/test/fuzz.ts

Lines changed: 43 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,17 @@ interface SetCell {
5353
value: MatrixItem<Value>;
5454
}
5555

56-
export type Operation = InsertRows | InsertColumns | RemoveRows | RemoveColumns | SetCell;
56+
interface SwitchSetCellPolicy {
57+
type: "switchSetCellPolicy";
58+
}
59+
60+
export type Operation =
61+
| InsertRows
62+
| InsertColumns
63+
| RemoveRows
64+
| RemoveColumns
65+
| SetCell
66+
| SwitchSetCellPolicy;
5767

5868
// This type gets used a lot as the state object of the suite; shorthand it here.
5969
type State = DDSFuzzTestState<SharedMatrixFactory>;
@@ -101,28 +111,33 @@ const reducer = combineReducers<Operation, State>({
101111
set: ({ client }, { row, col, value }) => {
102112
client.channel.setCell(row, col, value);
103113
},
114+
switchSetCellPolicy: ({ client }) => {
115+
client.channel.switchSetCellPolicy();
116+
},
104117
});
105118

106-
interface GeneratorOptions {
107-
insertRowWeight: number;
108-
insertColWeight: number;
109-
removeRowWeight: number;
110-
removeColWeight: number;
111-
setWeight: number;
112-
}
119+
type GeneratorOptions = Record<`${Operation["type"]}Weight`, number>;
113120

114121
const defaultOptions: GeneratorOptions = {
115-
insertRowWeight: 1,
116-
insertColWeight: 1,
117-
removeRowWeight: 1,
118-
removeColWeight: 1,
119-
setWeight: 20,
122+
insertRowsWeight: 10,
123+
insertColsWeight: 10,
124+
removeRowsWeight: 10,
125+
removeColsWeight: 10,
126+
setWeight: 200,
127+
switchSetCellPolicyWeight: 1,
120128
};
121129

122130
function makeGenerator(
123131
optionsParam?: Partial<GeneratorOptions>,
124132
): AsyncGenerator<Operation, State> {
125-
const { setWeight, insertColWeight, insertRowWeight, removeRowWeight, removeColWeight } = {
133+
const {
134+
setWeight,
135+
insertColsWeight,
136+
insertRowsWeight,
137+
removeRowsWeight,
138+
removeColsWeight,
139+
switchSetCellPolicyWeight,
140+
} = {
126141
...defaultOptions,
127142
...optionsParam,
128143
};
@@ -178,17 +193,27 @@ function makeGenerator(
178193
])(),
179194
});
180195

196+
const switchSetCellPolicy: Generator<SwitchSetCellPolicy, State> = () => ({
197+
type: "switchSetCellPolicy",
198+
});
199+
181200
const syncGenerator = createWeightedGenerator<Operation, State>([
182201
[
183202
setKey,
184203
setWeight,
185204
(state): boolean =>
186205
state.client.channel.rowCount > 0 && state.client.channel.colCount > 0,
187206
],
188-
[insertRows, insertRowWeight],
189-
[insertCols, insertColWeight],
190-
[removeRows, removeRowWeight, (state): boolean => state.client.channel.rowCount > 0],
191-
[removeCols, removeColWeight, (state): boolean => state.client.channel.colCount > 0],
207+
[insertRows, insertRowsWeight],
208+
[insertCols, insertColsWeight],
209+
[removeRows, removeRowsWeight, (state): boolean => state.client.channel.rowCount > 0],
210+
[removeCols, removeColsWeight, (state): boolean => state.client.channel.colCount > 0],
211+
[
212+
switchSetCellPolicy,
213+
switchSetCellPolicyWeight,
214+
(state): boolean =>
215+
state.client.channel.isSetCellConflictResolutionPolicyFWW() === false,
216+
],
192217
]);
193218

194219
return async (state) => syncGenerator(state);

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

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,6 @@
7676
"@fluidframework/id-compressor": "workspace:~",
7777
"@fluidframework/local-driver": "workspace:~",
7878
"@fluidframework/map": "workspace:~",
79-
"@fluidframework/matrix": "workspace:~",
8079
"@fluidframework/runtime-definitions": "workspace:~",
8180
"@fluidframework/runtime-utils": "workspace:~",
8281
"@fluidframework/sequence": "workspace:~",
@@ -107,8 +106,7 @@
107106
"^api-extractor:commonjs",
108107
"@fluidframework/id-compressor#build:test",
109108
"@fluidframework/sequence#build:test",
110-
"@fluidframework/map#build:test",
111-
"@fluidframework/matrix#build:test"
109+
"@fluidframework/map#build:test"
112110
]
113111
}
114112
},

0 commit comments

Comments
 (0)