Add useSlowFileEvents

This commit is contained in:
Andras Schmelczer 2025-03-15 18:01:33 +00:00
commit 78e1372483
No known key found for this signature in database
GPG key ID: FC8F2C3D3D1A718C
4 changed files with 87 additions and 41 deletions

View file

@ -158,7 +158,7 @@ export class UnrestrictedSyncer {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (document.metadata === undefined) { if (document.metadata === undefined) {
throw new Error( throw new Error(
`Document ${document.relativePath} no longer has metadata after updating it` `Document ${document.relativePath} no longer has metadata after updating it, this cannot happen`
); );
} }

View file

@ -18,9 +18,10 @@ export class MockAgent extends MockClient {
initialSettings: Partial<SyncSettings>, initialSettings: Partial<SyncSettings>,
public readonly name: string, public readonly name: string,
private readonly doDeletes: boolean, private readonly doDeletes: boolean,
useSlowFileEvents: boolean,
private readonly jitterScaleInSeconds: number private readonly jitterScaleInSeconds: number
) { ) {
super(initialSettings); super(initialSettings, useSlowFileEvents);
} }
public async init(): Promise<void> { public async init(): Promise<void> {
@ -62,9 +63,11 @@ export class MockAgent extends MockClient {
case LogLevel.ERROR: case LogLevel.ERROR:
console.error(formatted); console.error(formatted);
if (!this.useSlowFileEvents) {
// Let's not ignore errors // Let's not ignore errors
// eslint-disable-next-line @typescript-eslint/no-floating-promises // eslint-disable-next-line @typescript-eslint/no-floating-promises
sleep(100).then(() => process.exit(1)); sleep(100).then(() => process.exit(1));
}
break; break;
case LogLevel.WARNING: case LogLevel.WARNING:
@ -189,6 +192,14 @@ export class MockAgent extends MockClient {
} }
public assertAllContentIsPresentOnce(): void { public assertAllContentIsPresentOnce(): void {
if (this.useSlowFileEvents) {
this.client.logger.info(
// We can't ensure that we have seen every single update
`Skipping content check for ${this.name} because slow file events are enabled`
);
return;
}
for (const content of this.writtenContents) { for (const content of this.writtenContents) {
const found = Array.from(this.localFiles.keys()).filter((key) => { const found = Array.from(this.localFiles.keys()).filter((key) => {
return new TextDecoder() return new TextDecoder()

View file

@ -12,7 +12,8 @@ export class MockClient implements FileSystemOperations {
protected data: object | undefined = undefined; protected data: object | undefined = undefined;
public constructor( public constructor(
private readonly initialSettings: Partial<SyncSettings> private readonly initialSettings: Partial<SyncSettings>,
protected readonly useSlowFileEvents: boolean
) {} ) {}
public async init(): Promise<void> { public async init(): Promise<void> {
@ -64,8 +65,7 @@ export class MockClient implements FileSystemOperations {
); );
this.localFiles.set(path, newContent); this.localFiles.set(path, newContent);
// we aren't the best client and it takes some time to notice changes this.runCallback(() => {
setImmediate(() => {
void this.client.syncer.syncLocallyCreatedFile(path); void this.client.syncer.syncLocallyCreatedFile(path);
}); });
} }
@ -87,6 +87,7 @@ export class MockClient implements FileSystemOperations {
const newContentUint8Array = new TextEncoder().encode(newContent); const newContentUint8Array = new TextEncoder().encode(newContent);
this.localFiles.set(path, newContentUint8Array); this.localFiles.set(path, newContentUint8Array);
if (!this.useSlowFileEvents) {
const existingParts = currentContent const existingParts = currentContent
.split(" ") .split(" ")
.map((part) => part.trim()); .map((part) => part.trim());
@ -100,13 +101,13 @@ export class MockClient implements FileSystemOperations {
); );
} }
); );
}
this.client.logger.info( this.client.logger.info(
`Updated file ${path} with:\n current content: ${currentContent}\n new content: ${newContent}` `Updated file ${path} with:\n current content: ${currentContent}\n new content: ${newContent}`
); );
// we aren't the best client and it takes some time to notice changes this.runCallback(() => {
setImmediate(() => {
void this.client.syncer.syncLocallyUpdatedFile({ void this.client.syncer.syncLocallyUpdatedFile({
relativePath: path relativePath: path
}); });
@ -123,8 +124,7 @@ export class MockClient implements FileSystemOperations {
`Updated file ${path} with:\n new content: ${new TextDecoder().decode(content)}` `Updated file ${path} with:\n new content: ${new TextDecoder().decode(content)}`
); );
// we aren't the best client and it takes some time to notice changes this.runCallback(() => {
setImmediate(() => {
if (hasExisted) { if (hasExisted) {
void this.client.syncer.syncLocallyUpdatedFile({ void this.client.syncer.syncLocallyUpdatedFile({
relativePath: path relativePath: path
@ -140,8 +140,8 @@ export class MockClient implements FileSystemOperations {
`Deleting file: ${path} with:\n content ${new TextDecoder().decode(this.localFiles.get(path))}` `Deleting file: ${path} with:\n content ${new TextDecoder().decode(this.localFiles.get(path))}`
); );
this.localFiles.delete(path); this.localFiles.delete(path);
// we aren't the best client and it takes some time to notice changes
setImmediate(() => { this.runCallback(() => {
void this.client.syncer.syncLocallyDeletedFile(path); void this.client.syncer.syncLocallyDeletedFile(path);
}); });
} }
@ -163,12 +163,20 @@ export class MockClient implements FileSystemOperations {
`Renamed file: ${oldPath} -> ${newPath} with:\n content ${new TextDecoder().decode(file)}` `Renamed file: ${oldPath} -> ${newPath} with:\n content ${new TextDecoder().decode(file)}`
); );
// we aren't the best client and it takes some time to notice changes this.runCallback(() => {
setImmediate(() => {
void this.client.syncer.syncLocallyUpdatedFile({ void this.client.syncer.syncLocallyUpdatedFile({
oldPath, oldPath,
relativePath: newPath relativePath: newPath
}); });
}); });
} }
private runCallback(callback: () => void): void {
if (this.useSlowFileEvents) {
// we aren't the best client and it takes some time to notice changes
setTimeout(callback, 100);
} else {
callback();
}
}
} }

View file

@ -3,20 +3,26 @@ import { MockAgent } from "./agent/mock-agent";
import { sleep } from "./utils/sleep"; import { sleep } from "./utils/sleep";
import { v4 as uuidv4 } from "uuid"; import { v4 as uuidv4 } from "uuid";
let slowFileEvents = false;
async function runTest({ async function runTest({
agentCount, agentCount,
concurrency, concurrency,
iterations, iterations,
doDeletes, doDeletes,
useSlowFileEvents,
jitterScaleInSeconds jitterScaleInSeconds
}: { }: {
agentCount: number; agentCount: number;
concurrency: number; concurrency: number;
iterations: number; iterations: number;
doDeletes: boolean; doDeletes: boolean;
useSlowFileEvents: boolean;
jitterScaleInSeconds: number; jitterScaleInSeconds: number;
}): Promise<void> { }): Promise<void> {
const settings = `with ${agentCount} agents, concurrency ${concurrency}, iterations ${iterations}, doDeletes ${doDeletes}, jitterScaleInSeconds ${jitterScaleInSeconds}`; slowFileEvents = useSlowFileEvents;
const settings = `with ${agentCount} agents, concurrency ${concurrency}, iterations ${iterations}, doDeletes ${doDeletes}, jitterScaleInSeconds ${jitterScaleInSeconds}, useSlowFileEvents ${useSlowFileEvents}`;
console.info(`Running test ${settings}`); console.info(`Running test ${settings}`);
const initialSettings: Partial<SyncSettings> = { const initialSettings: Partial<SyncSettings> = {
@ -34,6 +40,7 @@ async function runTest({
initialSettings, initialSettings,
`agent-${i}`, `agent-${i}`,
doDeletes, doDeletes,
useSlowFileEvents,
jitterScaleInSeconds jitterScaleInSeconds
) )
); );
@ -56,12 +63,24 @@ async function runTest({
// Each agent can have unpushed changes which might conflict with eachother so each has to resolve the conflicts & push, and // Each agent can have unpushed changes which might conflict with eachother so each has to resolve the conflicts & push, and
for (const client of clients) { for (const client of clients) {
try {
await client.finish(); await client.finish();
} catch (err) {
if (!slowFileEvents) {
throw err;
}
}
} }
// then we need a second pass to ensure that all agents pull the same state. // then we need a second pass to ensure that all agents pull the same state.
for (const client of clients) { for (const client of clients) {
try {
await client.finish(); await client.finish();
} catch (err) {
if (!slowFileEvents) {
throw err;
}
}
} }
console.info("Agents finished successfully"); console.info("Agents finished successfully");
@ -96,31 +115,39 @@ async function runTests(): Promise<void> {
16, 16,
1 // test with concurrency 1 to check for deadlocks 1 // test with concurrency 1 to check for deadlocks
]; ];
const doDeletes = [true, false];
for (const agentCount of agentCounts) { for (const agentCount of agentCounts) {
for (const concurrency of concurrencies) { for (const concurrency of concurrencies) {
for (const jitter of networkJitterScaleInSeconds) { for (const jitter of networkJitterScaleInSeconds) {
for (const deleteFiles of doDeletes) { for (const doDeletes of [true, false]) {
for (const useSlowFileEvents of [true, false]) {
await runTest({ await runTest({
agentCount, agentCount,
concurrency, concurrency,
iterations: 200, iterations: 200,
doDeletes: deleteFiles, doDeletes,
useSlowFileEvents,
jitterScaleInSeconds: jitter jitterScaleInSeconds: jitter
}); });
} }
} }
} }
} }
}
} }
process.on("uncaughtException", (error) => { process.on("uncaughtException", (error) => {
if (slowFileEvents) {
return;
}
console.error("Uncaught Exception:", error); console.error("Uncaught Exception:", error);
process.exit(1); process.exit(1);
}); });
process.on("unhandledRejection", (reason, _promise) => { process.on("unhandledRejection", (reason, _promise) => {
if (slowFileEvents) {
return;
}
console.error("Unhandled Rejection:", reason); console.error("Unhandled Rejection:", reason);
process.exit(1); process.exit(1);
}); });