chore: add sync backoff mechanism checks (#2786)
This commit is contained in:
55
packages/services/src/Domain/Sync/SyncBackoffService.spec.ts
Normal file
55
packages/services/src/Domain/Sync/SyncBackoffService.spec.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { AnyItemInterface } from '@standardnotes/models'
|
||||
import { SyncBackoffService } from './SyncBackoffService'
|
||||
|
||||
describe('SyncBackoffService', () => {
|
||||
const createService = () => new SyncBackoffService()
|
||||
|
||||
it('should not be in backoff if no backoff was set', () => {
|
||||
const service = createService()
|
||||
|
||||
expect(service.isItemInBackoff({ uuid: '123' } as jest.Mocked<AnyItemInterface>)).toBe(false)
|
||||
})
|
||||
|
||||
it('should be in backoff if backoff was set', () => {
|
||||
const service = createService()
|
||||
|
||||
service.backoffItem({ uuid: '123' } as jest.Mocked<AnyItemInterface>)
|
||||
|
||||
expect(service.isItemInBackoff({ uuid: '123' } as jest.Mocked<AnyItemInterface>)).toBe(true)
|
||||
})
|
||||
|
||||
it('should not be in backoff if backoff expired', () => {
|
||||
const service = createService()
|
||||
|
||||
jest.spyOn(Date, 'now').mockReturnValueOnce(1_000_000)
|
||||
|
||||
service.backoffItem({ uuid: '123' } as jest.Mocked<AnyItemInterface>)
|
||||
|
||||
jest.spyOn(Date, 'now').mockReturnValueOnce(2_000_000)
|
||||
|
||||
expect(service.isItemInBackoff({ uuid: '123' } as jest.Mocked<AnyItemInterface>)).toBe(false)
|
||||
})
|
||||
|
||||
it('should double backoff penalty on each backoff', () => {
|
||||
const service = createService()
|
||||
|
||||
jest.spyOn(Date, 'now').mockReturnValueOnce(1_000_000)
|
||||
|
||||
service.backoffItem({ uuid: '123' } as jest.Mocked<AnyItemInterface>)
|
||||
|
||||
jest.spyOn(Date, 'now').mockReturnValueOnce(1_000_000)
|
||||
expect(service.isItemInBackoff({ uuid: '123' } as jest.Mocked<AnyItemInterface>)).toBe(true)
|
||||
|
||||
jest.spyOn(Date, 'now').mockReturnValueOnce(1_000_000)
|
||||
service.backoffItem({ uuid: '123' } as jest.Mocked<AnyItemInterface>)
|
||||
|
||||
jest.spyOn(Date, 'now').mockReturnValueOnce(1_001_000)
|
||||
expect(service.isItemInBackoff({ uuid: '123' } as jest.Mocked<AnyItemInterface>)).toBe(true)
|
||||
|
||||
jest.spyOn(Date, 'now').mockReturnValueOnce(1_000_000)
|
||||
service.backoffItem({ uuid: '123' } as jest.Mocked<AnyItemInterface>)
|
||||
|
||||
jest.spyOn(Date, 'now').mockReturnValueOnce(1_003_000)
|
||||
expect(service.isItemInBackoff({ uuid: '123' } as jest.Mocked<AnyItemInterface>)).toBe(true)
|
||||
})
|
||||
})
|
||||
37
packages/services/src/Domain/Sync/SyncBackoffService.ts
Normal file
37
packages/services/src/Domain/Sync/SyncBackoffService.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { AnyItemInterface } from '@standardnotes/models'
|
||||
import { SyncBackoffServiceInterface } from './SyncBackoffServiceInterface'
|
||||
|
||||
export class SyncBackoffService implements SyncBackoffServiceInterface {
|
||||
private backoffPenalties: Map<string, number>
|
||||
private backoffStartTimestamps: Map<string, number>
|
||||
|
||||
constructor() {
|
||||
this.backoffPenalties = new Map<string, number>()
|
||||
this.backoffStartTimestamps = new Map<string, number>()
|
||||
}
|
||||
|
||||
isItemInBackoff(item: AnyItemInterface): boolean {
|
||||
const backoffStartingTimestamp = this.backoffStartTimestamps.get(item.uuid)
|
||||
if (!backoffStartingTimestamp) {
|
||||
return false
|
||||
}
|
||||
|
||||
const backoffPenalty = this.backoffPenalties.get(item.uuid)
|
||||
if (!backoffPenalty) {
|
||||
return false
|
||||
}
|
||||
|
||||
const backoffEndTimestamp = backoffStartingTimestamp + backoffPenalty
|
||||
|
||||
return backoffEndTimestamp > Date.now()
|
||||
}
|
||||
|
||||
backoffItem(item: AnyItemInterface): void {
|
||||
const backoffPenalty = this.backoffPenalties.get(item.uuid) || 0
|
||||
|
||||
const newBackoffPenalty = backoffPenalty === 0 ? 1_000 : backoffPenalty * 2
|
||||
|
||||
this.backoffPenalties.set(item.uuid, newBackoffPenalty)
|
||||
this.backoffStartTimestamps.set(item.uuid, Date.now())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { AnyItemInterface } from '@standardnotes/models'
|
||||
|
||||
export interface SyncBackoffServiceInterface {
|
||||
isItemInBackoff(item: AnyItemInterface): boolean
|
||||
backoffItem(item: AnyItemInterface): void
|
||||
}
|
||||
@@ -181,6 +181,8 @@ export * from './Subscription/AppleIAPReceipt'
|
||||
export * from './Subscription/SubscriptionManager'
|
||||
export * from './Subscription/SubscriptionManagerEvent'
|
||||
export * from './Subscription/SubscriptionManagerInterface'
|
||||
export * from './Sync/SyncBackoffService'
|
||||
export * from './Sync/SyncBackoffServiceInterface'
|
||||
export * from './Sync/SyncMode'
|
||||
export * from './Sync/SyncOpStatus'
|
||||
export * from './Sync/SyncOptions'
|
||||
|
||||
@@ -145,6 +145,8 @@ import {
|
||||
IsVaultAdmin,
|
||||
IsReadonlyVaultMember,
|
||||
DesignateSurvivor,
|
||||
SyncBackoffService,
|
||||
SyncBackoffServiceInterface,
|
||||
} from '@standardnotes/services'
|
||||
import { ItemManager } from '../../Services/Items/ItemManager'
|
||||
import { PayloadManager } from '../../Services/Payloads/PayloadManager'
|
||||
@@ -1351,6 +1353,10 @@ export class Dependencies {
|
||||
)
|
||||
})
|
||||
|
||||
this.factory.set(TYPES.SyncBackoffService, () => {
|
||||
return new SyncBackoffService()
|
||||
})
|
||||
|
||||
this.factory.set(TYPES.SyncService, () => {
|
||||
return new SyncService(
|
||||
this.get<ItemManager>(TYPES.ItemManager),
|
||||
@@ -1369,6 +1375,7 @@ export class Dependencies {
|
||||
this.get<Logger>(TYPES.Logger),
|
||||
this.get<WebSocketsService>(TYPES.WebSocketsService),
|
||||
this.get<SyncFrequencyGuardInterface>(TYPES.SyncFrequencyGuard),
|
||||
this.get<SyncBackoffServiceInterface>(TYPES.SyncBackoffService),
|
||||
this.get<InternalEventBus>(TYPES.InternalEventBus),
|
||||
)
|
||||
})
|
||||
|
||||
@@ -30,6 +30,7 @@ export const TYPES = {
|
||||
SubscriptionManager: Symbol.for('SubscriptionManager'),
|
||||
HistoryManager: Symbol.for('HistoryManager'),
|
||||
SyncFrequencyGuard: Symbol.for('SyncFrequencyGuard'),
|
||||
SyncBackoffService: Symbol.for('SyncBackoffService'),
|
||||
SyncService: Symbol.for('SyncService'),
|
||||
ProtectionService: Symbol.for('ProtectionService'),
|
||||
UserService: Symbol.for('UserService'),
|
||||
|
||||
@@ -86,6 +86,7 @@ import {
|
||||
ApplicationSyncOptions,
|
||||
WebSocketsServiceEvent,
|
||||
WebSocketsService,
|
||||
SyncBackoffServiceInterface,
|
||||
} from '@standardnotes/services'
|
||||
import { OfflineSyncResponse } from './Offline/Response'
|
||||
import {
|
||||
@@ -171,6 +172,7 @@ export class SyncService
|
||||
private logger: LoggerInterface,
|
||||
private sockets: WebSocketsService,
|
||||
private syncFrequencyGuard: SyncFrequencyGuardInterface,
|
||||
private syncBackoffService: SyncBackoffServiceInterface,
|
||||
protected override internalEventBus: InternalEventBusInterface,
|
||||
) {
|
||||
super(internalEventBus)
|
||||
@@ -452,7 +454,11 @@ export class SyncService
|
||||
}
|
||||
|
||||
private itemsNeedingSync() {
|
||||
return this.itemManager.getDirtyItems()
|
||||
const dirtyItems = this.itemManager.getDirtyItems()
|
||||
|
||||
const itemsWithoutBackoffPenalty = dirtyItems.filter((item) => !this.syncBackoffService.isItemInBackoff(item))
|
||||
|
||||
return itemsWithoutBackoffPenalty
|
||||
}
|
||||
|
||||
public async markAllItemsAsNeedingSyncAndPersist(): Promise<void> {
|
||||
|
||||
Reference in New Issue
Block a user