Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: add sync backoff mechanism checks #2786

Merged
merged 1 commit into from Jan 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
55 changes: 55 additions & 0 deletions packages/services/src/Domain/Sync/SyncBackoffService.spec.ts
@@ -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 changes: 37 additions & 0 deletions packages/services/src/Domain/Sync/SyncBackoffService.ts
@@ -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
}
2 changes: 2 additions & 0 deletions packages/services/src/Domain/index.ts
Expand Up @@ -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'
Expand Down
7 changes: 7 additions & 0 deletions packages/snjs/lib/Application/Dependencies/Dependencies.ts
Expand Up @@ -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'
Expand Down Expand Up @@ -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),
Expand All @@ -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),
)
})
Expand Down
1 change: 1 addition & 0 deletions packages/snjs/lib/Application/Dependencies/Types.ts
Expand Up @@ -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'),
Expand Down
8 changes: 7 additions & 1 deletion packages/snjs/lib/Services/Sync/SyncService.ts
Expand Up @@ -86,6 +86,7 @@ import {
ApplicationSyncOptions,
WebSocketsServiceEvent,
WebSocketsService,
SyncBackoffServiceInterface,
} from '@standardnotes/services'
import { OfflineSyncResponse } from './Offline/Response'
import {
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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> {
Expand Down