Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MergeTree: Add stress for LocalReferences #9340

Merged
merged 10 commits into from
Mar 8, 2022
117 changes: 117 additions & 0 deletions packages/dds/merge-tree/src/test/client.localReferenceFarm.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
/*!
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
* Licensed under the MIT License.
*/

import { strict as assert } from "assert";
import random from "random-js";
import { doOverRange } from ".";
import { LocalReference, ReferenceType } from "..";
import {
IMergeTreeOperationRunnerConfig,
removeRange,
runMergeTreeOperationRunner,
generateClientNames,
IConfigRange,
} from "./mergeTreeOperationRunner";
import { TestClient } from "./testClient";
import { TestClientLogger } from "./testClientLogger";

const defaultOptions: Record<"initLen" | "modLen", IConfigRange> & IMergeTreeOperationRunnerConfig = {
initLen: {min: 2, max: 4},
modLen: {min: 1, max: 8},
opsPerRoundRange: { min: 10, max: 10 },
anthony-murphy marked this conversation as resolved.
Show resolved Hide resolved
rounds: 10,
operations: [removeRange],
growthFunc: (input: number) => input * 2,
};


describe("MergeTree.Client", () => {
// Generate a list of single character client names, support up to 69 clients
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment specifying the number made me curious enough to look. It seems to always happen to be enough but there's nothing directly protecting that in other tests. There should at least be a max check in the other tests that use it. Here it doesn't matter since it's 3, in which case do we really need to generate 69 strings? Not a big deal, especially since it's test code.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code will fail with a null refer, which is probably good enough for test code localized to a single package. there are much more dangerous things in the test area, but none it will do anything besides break a test, which is easily caught and fixed

const clientNames = generateClientNames();

doOverRange(defaultOptions.initLen, defaultOptions.growthFunc, (initLen)=>{
doOverRange(defaultOptions.modLen, defaultOptions.growthFunc, (modLen)=>{

it(`LocalReferenceFarm_${initLen}_${modLen}`, async () => {
const mt = random.engines.mt19937();
mt.seedWithArray([0xDEADBEEF, 0xFEEDBED, initLen, modLen]);

const clients: TestClient[] = new Array(3).fill(0).map(()=> new TestClient());
clients.forEach(
(c, i) => c.startOrUpdateCollaboration(clientNames[i]));

let seq = 0;
// init with random values
seq = runMergeTreeOperationRunner(
mt,
seq,
clients,
initLen,
defaultOptions
);
// add local references
const refs: LocalReference[][]=[];

const validateRefs = (reason: string, workload:()=>void)=>{
const preWorkload = TestClientLogger.toString(clients);
workload();
for(let c=1;c<clients.length;c++){
for(let r =0; r<refs[c].length;r++){
const pos0 = refs[0][r].toPosition();
const posC = refs[c][r].toPosition();
if(pos0 !== posC){
assert.equal(
pos0, posC,
`${reason}:\n${preWorkload}\n${TestClientLogger.toString(clients)}`);
}
}
}
// console.log(`${reason}:\n${preWorkload}\n${TestClientLogger.toString(clients)}`)
};

validateRefs("Initialize", ()=>{
clients.forEach((c,i)=>{
refs.push([]);
for(let t = 0;t<c.getLength();t++){
const seg = c.getContainingSegment(t);
const lref = new LocalReference(c, seg.segment, seg.offset, ReferenceType.SlideOnRemove);
c.addLocalReference(lref);
lref.addProperties({t});
refs[i].push(lref);
}
});
});


validateRefs("After Init Zamboni",()=>{
//trigger zamboni multiple times as it is incremental
for(let i = clients[0].getCollabWindow().minSeq;i<=seq;i++){
clients.forEach((c)=>c.updateMinSeq(i));
}
});

validateRefs("After More Ops", ()=>{
// init with random values
seq = runMergeTreeOperationRunner(
mt,
seq,
clients,
modLen,
defaultOptions,
);
});


validateRefs("After Final Zamboni",()=>{
//trigger zamboni multiple times as it is incremental
for(let i = clients[0].getCollabWindow().minSeq;i<=seq;i++){
clients.forEach((c)=>c.updateMinSeq(i));
}
});

})
});
});
});
52 changes: 35 additions & 17 deletions packages/dds/merge-tree/src/test/testClientLogger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,16 @@ export function createClientsAtInitialState<TClients extends ClientMap>(
return {...clients, all};
}
export class TestClientLogger {

public static toString(clients: readonly TestClient[]){

return clients.map((c)=>this.getSegString(c)).reduce<[string,string]>((pv,cv)=>{
pv[0]+=`|${cv.acked.padEnd(cv.local.length,"")}`;
pv[1]+=`|${cv.local.padEnd(cv.acked.length,"")}`;
return pv;
},["",""]).join("\n");
}

private readonly incrementalLog = false;

private readonly paddings: number[] = [];
Expand All @@ -80,7 +90,7 @@ export class TestClientLogger {
const clientLogIndex = i*2

this.ackedLine[clientLogIndex]=getOpString(op.sequencedMessage ?? c.makeOpMessage(op.op))
const segStrings = this.getSegString(c);
const segStrings = TestClientLogger.getSegString(c);
this.ackedLine[clientLogIndex + 1] = segStrings.acked;
this.localLine[clientLogIndex +1] = segStrings.local;

Expand Down Expand Up @@ -109,14 +119,18 @@ export class TestClientLogger {
}

private addNewLogLine() {
if (this.incrementalLog) {
console.log(this.ackedLine.map((v, i) => v.padEnd(this.paddings[i])).join(" | "));
console.log(this.ackedLine.map((v, i) => v.padEnd(this.paddings[i])).join(" | "));
if(this.incrementalLog){
while(this.roundLogLines.length > 0){
const logLine = this.roundLogLines.shift();
if(logLine.some((c)=>c.trim().length >0)){
console.log(logLine.map((v, i) => v.padEnd(this.paddings[i])).join(" | "));
}
}
}
this.ackedLine = [];
this.localLine = [];
this.clients.forEach((cc, clientLogIndex)=>{
const segStrings = this.getSegString(cc);
const segStrings = TestClientLogger.getSegString(cc);
this.ackedLine.push("", segStrings.acked);
this.localLine.push("", segStrings.local);

Expand Down Expand Up @@ -153,17 +167,21 @@ export class TestClientLogger {
return baseText;
}

public toString() {
let str =
`_: Local State\n`
+ `-: Deleted\n`
+ `*: Unacked Insert and Delete\n`
+ `${this.clients[0].getCollabWindow().minSeq}: msn/offset\n`
+ `Op format <seq>:<ref>:<client><type>@<pos1>,<pos2>\n`
+ `sequence number represented as offset from msn. L means local.\n`
+ `op types: 0) insert 1) remove 2) annotate\n`;
if (this.title) {
str += `${this.title}\n`;
public toString(excludeHeader: boolean = false) {
let str = "";
if(!excludeHeader){
str +=
`_: Local State\n`
+ `-: Deleted\n`
+ `*: Unacked Insert and Delete\n`
+ `${this.clients[0].getCollabWindow().minSeq}: msn/offset\n`
+ `Op format <seq>:<ref>:<client><type>@<pos1>,<pos2>\n`
+ `sequence number represented as offset from msn. L means local.\n`
+ `op types: 0) insert 1) remove 2) annotate\n`;

if (this.title) {
str += `${this.title}\n`;
}
}
str += this.roundLogLines
.filter((line)=>line.some((c)=>c.trim().length >0))
Expand All @@ -172,7 +190,7 @@ export class TestClientLogger {
return str;
}

private getSegString(client: TestClient): { acked: string, local: string } {
private static getSegString(client: TestClient): { acked: string, local: string } {
let acked: string = "";
let local: string = "";
const nodes = [...client.mergeTree.root.children];
Expand Down