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/SubscriptionManager'
|
||||||
export * from './Subscription/SubscriptionManagerEvent'
|
export * from './Subscription/SubscriptionManagerEvent'
|
||||||
export * from './Subscription/SubscriptionManagerInterface'
|
export * from './Subscription/SubscriptionManagerInterface'
|
||||||
|
export * from './Sync/SyncBackoffService'
|
||||||
|
export * from './Sync/SyncBackoffServiceInterface'
|
||||||
export * from './Sync/SyncMode'
|
export * from './Sync/SyncMode'
|
||||||
export * from './Sync/SyncOpStatus'
|
export * from './Sync/SyncOpStatus'
|
||||||
export * from './Sync/SyncOptions'
|
export * from './Sync/SyncOptions'
|
||||||
|
|||||||
@@ -145,6 +145,8 @@ import {
|
|||||||
IsVaultAdmin,
|
IsVaultAdmin,
|
||||||
IsReadonlyVaultMember,
|
IsReadonlyVaultMember,
|
||||||
DesignateSurvivor,
|
DesignateSurvivor,
|
||||||
|
SyncBackoffService,
|
||||||
|
SyncBackoffServiceInterface,
|
||||||
} from '@standardnotes/services'
|
} from '@standardnotes/services'
|
||||||
import { ItemManager } from '../../Services/Items/ItemManager'
|
import { ItemManager } from '../../Services/Items/ItemManager'
|
||||||
import { PayloadManager } from '../../Services/Payloads/PayloadManager'
|
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, () => {
|
this.factory.set(TYPES.SyncService, () => {
|
||||||
return new SyncService(
|
return new SyncService(
|
||||||
this.get<ItemManager>(TYPES.ItemManager),
|
this.get<ItemManager>(TYPES.ItemManager),
|
||||||
@@ -1369,6 +1375,7 @@ export class Dependencies {
|
|||||||
this.get<Logger>(TYPES.Logger),
|
this.get<Logger>(TYPES.Logger),
|
||||||
this.get<WebSocketsService>(TYPES.WebSocketsService),
|
this.get<WebSocketsService>(TYPES.WebSocketsService),
|
||||||
this.get<SyncFrequencyGuardInterface>(TYPES.SyncFrequencyGuard),
|
this.get<SyncFrequencyGuardInterface>(TYPES.SyncFrequencyGuard),
|
||||||
|
this.get<SyncBackoffServiceInterface>(TYPES.SyncBackoffService),
|
||||||
this.get<InternalEventBus>(TYPES.InternalEventBus),
|
this.get<InternalEventBus>(TYPES.InternalEventBus),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ export const TYPES = {
|
|||||||
SubscriptionManager: Symbol.for('SubscriptionManager'),
|
SubscriptionManager: Symbol.for('SubscriptionManager'),
|
||||||
HistoryManager: Symbol.for('HistoryManager'),
|
HistoryManager: Symbol.for('HistoryManager'),
|
||||||
SyncFrequencyGuard: Symbol.for('SyncFrequencyGuard'),
|
SyncFrequencyGuard: Symbol.for('SyncFrequencyGuard'),
|
||||||
|
SyncBackoffService: Symbol.for('SyncBackoffService'),
|
||||||
SyncService: Symbol.for('SyncService'),
|
SyncService: Symbol.for('SyncService'),
|
||||||
ProtectionService: Symbol.for('ProtectionService'),
|
ProtectionService: Symbol.for('ProtectionService'),
|
||||||
UserService: Symbol.for('UserService'),
|
UserService: Symbol.for('UserService'),
|
||||||
|
|||||||
@@ -86,6 +86,7 @@ import {
|
|||||||
ApplicationSyncOptions,
|
ApplicationSyncOptions,
|
||||||
WebSocketsServiceEvent,
|
WebSocketsServiceEvent,
|
||||||
WebSocketsService,
|
WebSocketsService,
|
||||||
|
SyncBackoffServiceInterface,
|
||||||
} from '@standardnotes/services'
|
} from '@standardnotes/services'
|
||||||
import { OfflineSyncResponse } from './Offline/Response'
|
import { OfflineSyncResponse } from './Offline/Response'
|
||||||
import {
|
import {
|
||||||
@@ -171,6 +172,7 @@ export class SyncService
|
|||||||
private logger: LoggerInterface,
|
private logger: LoggerInterface,
|
||||||
private sockets: WebSocketsService,
|
private sockets: WebSocketsService,
|
||||||
private syncFrequencyGuard: SyncFrequencyGuardInterface,
|
private syncFrequencyGuard: SyncFrequencyGuardInterface,
|
||||||
|
private syncBackoffService: SyncBackoffServiceInterface,
|
||||||
protected override internalEventBus: InternalEventBusInterface,
|
protected override internalEventBus: InternalEventBusInterface,
|
||||||
) {
|
) {
|
||||||
super(internalEventBus)
|
super(internalEventBus)
|
||||||
@@ -452,7 +454,11 @@ export class SyncService
|
|||||||
}
|
}
|
||||||
|
|
||||||
private itemsNeedingSync() {
|
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> {
|
public async markAllItemsAsNeedingSyncAndPersist(): Promise<void> {
|
||||||
|
|||||||
Reference in New Issue
Block a user