chore: global sync per minute safety limit (#2765)

This commit is contained in:
Karol Sójko
2024-01-11 15:26:09 +01:00
committed by GitHub
parent 3d895ca499
commit cf4b5ccf0a
11 changed files with 140 additions and 10 deletions

View File

@@ -0,0 +1,38 @@
import { SyncFrequencyGuard } from './SyncFrequencyGuard'
describe('SyncFrequencyGuard', () => {
const createUseCase = () => new SyncFrequencyGuard(3)
it('should return false when sync calls threshold is not reached', () => {
const useCase = createUseCase()
expect(useCase.isSyncCallsThresholdReachedThisMinute()).toBe(false)
})
it('should return true when sync calls threshold is reached', () => {
const useCase = createUseCase()
useCase.incrementCallsPerMinute()
useCase.incrementCallsPerMinute()
useCase.incrementCallsPerMinute()
expect(useCase.isSyncCallsThresholdReachedThisMinute()).toBe(true)
})
it('should return false when sync calls threshold is reached but a new minute has started', () => {
const spyOnGetCallsPerMinuteKey = jest.spyOn(SyncFrequencyGuard.prototype as any, 'getCallsPerMinuteKey')
spyOnGetCallsPerMinuteKey.mockReturnValueOnce('2020-1-1T1:1')
const useCase = createUseCase()
useCase.incrementCallsPerMinute()
useCase.incrementCallsPerMinute()
useCase.incrementCallsPerMinute()
spyOnGetCallsPerMinuteKey.mockReturnValueOnce('2020-1-1T1:2')
expect(useCase.isSyncCallsThresholdReachedThisMinute()).toBe(false)
spyOnGetCallsPerMinuteKey.mockRestore()
})
})

View File

@@ -0,0 +1,40 @@
import { SyncFrequencyGuardInterface } from './SyncFrequencyGuardInterface'
export class SyncFrequencyGuard implements SyncFrequencyGuardInterface {
private callsPerMinuteMap: Map<string, number>
constructor(private syncCallsThresholdPerMinute: number) {
this.callsPerMinuteMap = new Map<string, number>()
}
isSyncCallsThresholdReachedThisMinute(): boolean {
const stringDateToTheMinute = this.getCallsPerMinuteKey()
const persistedCallsCount = this.callsPerMinuteMap.get(stringDateToTheMinute) || 0
return persistedCallsCount >= this.syncCallsThresholdPerMinute
}
incrementCallsPerMinute(): void {
const stringDateToTheMinute = this.getCallsPerMinuteKey()
const persistedCallsCount = this.callsPerMinuteMap.get(stringDateToTheMinute)
const newMinuteStarted = persistedCallsCount === undefined
if (newMinuteStarted) {
this.clear()
this.callsPerMinuteMap.set(stringDateToTheMinute, 1)
} else {
this.callsPerMinuteMap.set(stringDateToTheMinute, persistedCallsCount + 1)
}
}
clear(): void {
this.callsPerMinuteMap.clear()
}
private getCallsPerMinuteKey(): string {
const now = new Date()
return `${now.getFullYear()}-${now.getMonth()}-${now.getDate()}T${now.getHours()}:${now.getMinutes()}`
}
}

View File

@@ -0,0 +1,5 @@
export interface SyncFrequencyGuardInterface {
incrementCallsPerMinute(): void
isSyncCallsThresholdReachedThisMinute(): boolean
clear(): void
}

View File

@@ -97,6 +97,7 @@ import {
import { CreatePayloadFromRawServerItem } from './Account/Utilities'
import { DecryptedServerConflictMap, TrustedServerConflictMap } from './Account/ServerConflictMap'
import { ContentType } from '@standardnotes/domain-core'
import { SyncFrequencyGuardInterface } from './SyncFrequencyGuardInterface'
const DEFAULT_MAJOR_CHANGE_THRESHOLD = 15
const INVALID_SESSION_RESPONSE_STATUS = 401
@@ -169,6 +170,7 @@ export class SyncService
private readonly options: ApplicationSyncOptions,
private logger: LoggerInterface,
private sockets: WebSocketsService,
private syncFrequencyGuard: SyncFrequencyGuardInterface,
protected override internalEventBus: InternalEventBusInterface,
) {
super(internalEventBus)
@@ -643,7 +645,8 @@ export class SyncService
const syncInProgress = this.opStatus.syncInProgress
const databaseLoaded = this.databaseLoaded
const canExecuteSync = !this.syncLock
const shouldExecuteSync = canExecuteSync && databaseLoaded && !syncInProgress
const syncLimitReached = this.syncFrequencyGuard.isSyncCallsThresholdReachedThisMinute()
const shouldExecuteSync = canExecuteSync && databaseLoaded && !syncInProgress && !syncLimitReached
if (shouldExecuteSync) {
this.syncLock = true
@@ -1296,6 +1299,8 @@ export class SyncService
this.lastSyncDate = new Date()
this.syncFrequencyGuard.incrementCallsPerMinute()
if (operation instanceof AccountSyncOperation && operation.numberOfItemsInvolved >= this.majorChangeThreshold) {
void this.notifyEvent(SyncEvent.MajorDataChange)
}