diff --git a/apps/backend/src/app/modules/session/run/run-processor.class.spec.ts b/apps/backend/src/app/modules/session/run/run-processor.class.spec.ts index ec86de6cf..80f3a33df 100644 --- a/apps/backend/src/app/modules/session/run/run-processor.class.spec.ts +++ b/apps/backend/src/app/modules/session/run/run-processor.class.spec.ts @@ -52,6 +52,8 @@ describe('RunProcessor', () => { const DefaultReplayHeader = ReplayFile.Stubs.ReplayHeaderStub; const DefaultSplits = ReplayFile.Stubs.RunSplitsStub; + const Constants = RunProcessor.Constants; + interface ProcessorOverrides { header?: Partial; splits?: Partial; @@ -654,11 +656,13 @@ describe('RunProcessor', () => { expectFail(ErrorType.OUT_OF_SYNC); }); - it('should throw if run time is out of sync - startDelay > AllowedTimestampDelay', () => { + it('should throw if run time is out of sync - startDelay > AllowedTimestampDelay + AllowedClockDrift', () => { processor = createProcessor({ session: { timestamps, - createdAt: cat(RunProcessor.Constants.AllowedTimestampDelay + 100) + createdAt: cat( + Constants.AllowedTimestampDelay + Constants.AllowedClockDrift + 100 + ) } }); @@ -670,7 +674,9 @@ describe('RunProcessor', () => { processor = createProcessor({ session: { timestamps, - createdAt: cat(RunProcessor.Constants.AllowedTimestampDelay - 100) + createdAt: cat( + Constants.AllowedTimestampDelay + Constants.AllowedClockDrift - 100 + ) } }); @@ -691,12 +697,13 @@ describe('RunProcessor', () => { it('should throw if run time is out of sync - submitDelay > acceptableSubmitDelay', () => { const { AllowedSubmitDelayBase: base, + AllowedClockDrift: drift, AllowedSubmitDelayIncrement: incr - } = RunProcessor.Constants; + } = Constants; processor = createProcessor({ session: { timestamps } }); - const acceptableDelay = base + (runTimeMS / 60000) * incr; + const acceptableDelay = base + drift + (runTimeMS / 60000) * incr; jest.advanceTimersByTime(runTimeMS + acceptableDelay - 100); expectPass(); @@ -871,8 +878,16 @@ describe('RunProcessor', () => { processor = createProcessor({ session: { timestamps: [ + // Note that `time` isn't really that significant in these tests, + // just making the data more realistic. The actual check is + // performed against the { createdAt: cat(0), time: 0, majorNum: 1, minorNum: 1 }, - { createdAt: cat(9999), time: 9.999, majorNum: 1, minorNum: 2 }, + { + createdAt: cat(9999 - Constants.AllowedClockDrift), + time: 9.999 - Constants.AllowedClockDrift / 1000, + majorNum: 1, + minorNum: 2 + }, { createdAt: cat(20000), time: 20, majorNum: 2, minorNum: 1 }, { createdAt: cat(30000), time: 30, majorNum: 2, minorNum: 2 } ] @@ -889,7 +904,10 @@ describe('RunProcessor', () => { { createdAt: cat(0), time: 0, majorNum: 1, minorNum: 1 }, { createdAt: cat( - 10000 + RunProcessor.Constants.AllowedTimestampDelay - 100 + 10000 + + Constants.AllowedTimestampDelay + + Constants.AllowedClockDrift - + 100 ), time: 10, majorNum: 1, @@ -911,7 +929,10 @@ describe('RunProcessor', () => { { createdAt: cat(0), time: 0, majorNum: 1, minorNum: 1 }, { createdAt: cat( - 10000 + RunProcessor.Constants.AllowedTimestampDelay + 100 + 10000 + + Constants.AllowedTimestampDelay + + Constants.AllowedClockDrift + + 100 ), time: 10, majorNum: 1, diff --git a/apps/backend/src/app/modules/session/run/run-processor.class.ts b/apps/backend/src/app/modules/session/run/run-processor.class.ts index 6766cd4ee..aa471d10f 100644 --- a/apps/backend/src/app/modules/session/run/run-processor.class.ts +++ b/apps/backend/src/app/modules/session/run/run-processor.class.ts @@ -35,7 +35,10 @@ export class RunProcessor { AllowedSubmitDelayMax: 30_000, // Allowed time diff between split time and backend timestamp created - AllowedTimestampDelay: 5000 + AllowedTimestampDelay: 5000, + + // Allowed system clock inaccuracy + AllowedClockDrift: 1000 }; static readonly Logger = new Logger('RunProcessor'); @@ -285,8 +288,8 @@ export class RunProcessor { if (header.tickInterval !== TickIntervals.get(session.gamemode)) { this.reject( - ErrorType.BAD_META, - 'header.tickInterval != session.gamemode' + ErrorType.OUT_OF_SYNC, + 'header.tickInterval != gamemode tick interval' ); } @@ -294,7 +297,8 @@ export class RunProcessor { AllowedSubmitDelayBase, AllowedSubmitDelayIncrement, AllowedSubmitDelayMax, - AllowedTimestampDelay + AllowedTimestampDelay, + AllowedClockDrift } = RunProcessor.Constants; // Check timestamps match up with replay start and end times @@ -319,6 +323,7 @@ export class RunProcessor { const submitDelay = now - header.timestamp; const allowedSubmitDelay = AllowedSubmitDelayBase + + AllowedClockDrift + Math.min( (AllowedSubmitDelayIncrement * headerRunTime) / 60_000, AllowedSubmitDelayMax @@ -326,11 +331,12 @@ export class RunProcessor { const startDelay = sessionStart - runStart; - if (submitDelay < 0) { - this.reject(ErrorType.OUT_OF_SYNC, 'submitDelay < 0', { + if (submitDelay < -AllowedClockDrift) { + this.reject(ErrorType.OUT_OF_SYNC, 'submitDelay < -AllowedClockDrift', { now, headerTimestamp: header.timestamp, - submitDelay + submitDelay, + AllowedClockDrift }); } @@ -342,31 +348,38 @@ export class RunProcessor { allowedSubmitDelay, AllowedSubmitDelayBase, AllowedSubmitDelayIncrement, - AllowedSubmitDelayMax + AllowedSubmitDelayMax, + AllowedClockDrift }); } - if (startDelay < 0) { - this.reject(ErrorType.OUT_OF_SYNC, 'startDelay < 0', { + if (startDelay < -AllowedClockDrift) { + this.reject(ErrorType.OUT_OF_SYNC, 'startDelay < -AllowedClockDrift', { headerRunTime, headerTimestamp: header.timestamp, runStart, sessionStart, now, - startDelay + startDelay, + AllowedClockDrift }); } - if (startDelay > AllowedTimestampDelay) { - this.reject(ErrorType.OUT_OF_SYNC, 'startDelay > AllowedTimestampDelay', { - headerRunTime, - headerTimestamp: header.timestamp, - runStart, - sessionStart, - now, - startDelay, - AllowedTimestampDelay - }); + if (startDelay > AllowedTimestampDelay + AllowedClockDrift) { + this.reject( + ErrorType.OUT_OF_SYNC, + 'startDelay > AllowedTimestampDelay + AllowedClockDrift', + { + headerRunTime, + headerTimestamp: header.timestamp, + runStart, + sessionStart, + now, + startDelay, + AllowedTimestampDelay, + AllowedClockDrift + } + ); } } @@ -401,31 +414,38 @@ export class RunProcessor { const unixTimeReached = replayStartTime + subseg.timeReached * 1000; const timestampDelay = timestamp.createdAt.getTime() - unixTimeReached; - const { AllowedTimestampDelay } = RunProcessor.Constants; + const { AllowedTimestampDelay, AllowedClockDrift } = + RunProcessor.Constants; - if (timestampDelay > AllowedTimestampDelay) { + if (timestampDelay > AllowedTimestampDelay + AllowedClockDrift) { this.reject( ErrorType.OUT_OF_SYNC, - 'timestampDelay > AllowedTimestampDelay', + 'timestampDelay > AllowedTimestampDelay + AllowedClockDrift', { subseg, timestamp, replayStartTime, unixTimeReached, timestampDelay, - AllowedTimestampDelay + AllowedTimestampDelay, + AllowedClockDrift } ); } - if (timestampDelay < 0) { - this.reject(ErrorType.OUT_OF_SYNC, 'timestampDelay < 0', { - subseg, - timestamp, - replayStartTime, - unixTimeReached, - timestampDelay - }); + if (timestampDelay < -AllowedClockDrift) { + this.reject( + ErrorType.OUT_OF_SYNC, + 'timestampDelay < -AllowedClockDrift', + { + subseg, + timestamp, + replayStartTime, + unixTimeReached, + timestampDelay, + AllowedClockDrift + } + ); } }); }