Skip to content
This repository has been archived by the owner on Apr 26, 2024. It is now read-only.

Commit

Permalink
Implement retries strategy (#19)
Browse files Browse the repository at this point in the history
Co-authored-by: Michał Podsiadły <[email protected]>
  • Loading branch information
michalsidzej and sdlyy authored Aug 25, 2023
1 parent ad510ca commit f6fe36d
Show file tree
Hide file tree
Showing 9 changed files with 359 additions and 50 deletions.
5 changes: 5 additions & 0 deletions .changeset/cuddly-trainers-sparkle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@l2beat/uif': minor
---

Add retries
9 changes: 8 additions & 1 deletion packages/example/src/Application.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { Logger } from '@l2beat/backend-tools'

import { BaseIndexer, Retries } from '@l2beat/uif'
import { Config } from './Config'
import { BalanceIndexer } from './indexers/BalanceIndexer'
import { BlockNumberIndexer } from './indexers/BlockNumberIndexer'
import { FakeClockIndexer } from './indexers/FakeClockIndexer'
import { TvlIndexer } from './indexers/TvlIndexer'
import { BalanceRepository } from './repositories/BalanceRepository'
import { BlockNumberRepository } from './repositories/BlockNumberRepository'
import { TvlRepository } from './repositories/TvlRepository'
import { TvlIndexer } from './indexers/TvlIndexer'

export class Application {
start: () => Promise<void>
Expand All @@ -24,6 +25,12 @@ export class Application {
const balanceRepository = new BalanceRepository()
const tvlRepository = new TvlRepository()

BaseIndexer.DEFAULT_RETRY_STRATEGY = Retries.exponentialBackOff({
initialTimeoutMs: 100,
maxAttempts: 10,
maxTimeoutMs: 60 * 1000,
})

const fakeClockIndexer = new FakeClockIndexer(logger)
const blockNumberIndexer = new BlockNumberIndexer(
logger,
Expand Down
11 changes: 8 additions & 3 deletions packages/example/src/indexers/BlockNumberIndexer.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
import { ChildIndexer } from '@l2beat/uif'
import { Logger } from '@l2beat/backend-tools'
import { ChildIndexer, Retries } from '@l2beat/uif'

import { setTimeout } from 'timers/promises'
import { BlockNumberRepository } from '../repositories/BlockNumberRepository'
import { FakeClockIndexer } from './FakeClockIndexer'
import { setTimeout } from 'timers/promises'

export class BlockNumberIndexer extends ChildIndexer {
constructor(
logger: Logger,
fakeClockIndexer: FakeClockIndexer,
private readonly blockNumberRepository: BlockNumberRepository,
) {
super(logger, [fakeClockIndexer])
super(logger, [fakeClockIndexer], {
updateRetryStrategy: Retries.exponentialBackOff({
initialTimeoutMs: 100,
maxAttempts: 10,
}),
})
}

override async update(from: number, to: number): Promise<number> {
Expand Down
2 changes: 1 addition & 1 deletion packages/uif/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
"@l2beat/backend-tools": "*"
},
"devDependencies": {
"@sinonjs/fake-timers": "^10.2.0",
"@sinonjs/fake-timers": "^11.1.0",
"@types/sinonjs__fake-timers": "^8.1.2",
"wait-for-expect": "^3.0.2"
}
Expand Down
197 changes: 191 additions & 6 deletions packages/uif/src/BaseIndexer.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { Logger } from '@l2beat/backend-tools'
import { expect } from 'earl'
import { install } from '@sinonjs/fake-timers'
import { expect, mockFn } from 'earl'

import { BaseIndexer, ChildIndexer, RootIndexer } from './BaseIndexer'
import { IndexerAction } from './reducer/types/IndexerAction'
import { RetryStrategy } from './Retries'

describe(BaseIndexer.name, () => {
describe('correctly informs about updates', () => {
Expand All @@ -15,6 +17,7 @@ describe(BaseIndexer.name, () => {

await child.finishInvalidate()
await parent.doTick(1)
await parent.finishTick(1)

await child.finishUpdate(1)

Expand All @@ -29,6 +32,7 @@ describe(BaseIndexer.name, () => {
await child.start()

await parent.doTick(1)
await parent.finishTick(1)
await child.finishInvalidate()

await child.finishUpdate(1)
Expand All @@ -42,7 +46,7 @@ describe(BaseIndexer.name, () => {
// Pi - parent indexer
// Mi - middle indexer
// Ci - child indexer
it('When Mi updating, passes correcty safeHeight', async () => {
it('When Mi updating, passes correctly safeHeight', async () => {
const parent = new TestRootIndexer(0)
const middle = new TestChildIndexer([parent], 0, 'Middle')
const child = new TestChildIndexer([middle], 0, 'Child')
Expand All @@ -55,15 +59,159 @@ describe(BaseIndexer.name, () => {
await child.finishInvalidate()

await parent.doTick(10)
await parent.finishTick(10)
await middle.finishUpdate(10)

expect(child.getState().status).toEqual('updating')

await parent.doTick(5)
await parent.finishTick(5)

expect(middle.getState().waiting).toEqual(true)
})
})

describe('retries on error', () => {
it('invalidates and retries update', async () => {
const clock = install({ shouldAdvanceTime: true, advanceTimeDelta: 1 })

const parent = new TestRootIndexer(0)

const shouldRetry = mockFn(() => true)
const markAttempt = mockFn(() => {})
const clear = mockFn(() => {})

const child = new TestChildIndexer([parent], 0, '', {
updateRetryStrategy: {
shouldRetry,
markAttempt,
timeoutMs: () => 1000,
clear,
},
})

await parent.start()
await child.start()

await child.finishInvalidate()

await parent.doTick(1)
await parent.finishTick(1)

await child.finishUpdate(new Error('test error'))
expect(child.updating).toBeFalsy()
expect(child.invalidating).toBeTruthy()
expect(shouldRetry).toHaveBeenCalledTimes(1)
expect(markAttempt).toHaveBeenCalledTimes(1)

await child.finishInvalidate()
expect(child.getState().status).toEqual('idle')

await clock.tickAsync(1000)

expect(child.getState().status).toEqual('updating')
await child.finishUpdate(1)

expect(clear).toHaveBeenCalledTimes(1)
expect(child.getState().status).toEqual('idle')

clock.uninstall()
})

it('retries invalidate', async () => {
const clock = install({ shouldAdvanceTime: true, advanceTimeDelta: 1 })
const parent = new TestRootIndexer(0)
const invalidateShouldRetry = mockFn(() => true)
const invalidateMarkAttempt = mockFn(() => {})
const invalidateClear = mockFn(() => {})

const updateShouldRetry = mockFn(() => true)
const updateMarkAttempt = mockFn(() => {})
const updateClear = mockFn(() => {})

const child = new TestChildIndexer([parent], 0, '', {
invalidateRetryStrategy: {
shouldRetry: invalidateShouldRetry,
markAttempt: invalidateMarkAttempt,
timeoutMs: () => 1000,
clear: invalidateClear,
},
updateRetryStrategy: {
shouldRetry: updateShouldRetry,
markAttempt: updateMarkAttempt,
timeoutMs: () => 1000,
clear: updateClear,
},
})

await parent.start()
await child.start()

await child.finishInvalidate()
expect(invalidateClear).toHaveBeenCalledTimes(1)

await parent.doTick(1)
await parent.finishTick(1)

await child.finishUpdate(new Error('test error'))
expect(updateShouldRetry).toHaveBeenCalledTimes(1)
expect(updateMarkAttempt).toHaveBeenCalledTimes(1)

await child.finishInvalidate(new Error('test error'))
expect(invalidateMarkAttempt).toHaveBeenCalledTimes(1)
expect(invalidateShouldRetry).toHaveBeenCalledTimes(1)
expect(child.getState().status).toEqual('idle')

await clock.tickAsync(1000)

expect(child.getState().status).toEqual('invalidating')
expect(child.invalidating).toBeTruthy()

await child.finishInvalidate()
expect(invalidateClear).toHaveBeenCalledTimes(2)
expect(child.getState().status).toEqual('updating')
expect(child.updating).toBeTruthy()

await child.finishUpdate(1)
expect(updateClear).toHaveBeenCalledTimes(1)
expect(child.getState().status).toEqual('idle')
clock.uninstall()
})

it('invalidates and retries tick', async () => {
const clock = install({ shouldAdvanceTime: true, advanceTimeDelta: 1 })
const shouldRetry = mockFn(() => true)
const markAttempt = mockFn(() => {})
const clear = mockFn(() => {})

const root = new TestRootIndexer(0, '', {
tickRetryStrategy: {
shouldRetry,
markAttempt,
timeoutMs: () => 1000,
clear,
},
})

await root.start()

await root.doTick(1)
await root.finishTick(new Error('test error'))
expect(markAttempt).toHaveBeenCalledTimes(1)
expect(shouldRetry).toHaveBeenCalledTimes(1)
expect(root.getState().status).toEqual('idle')

await clock.tickAsync(1000)

expect(root.getState().status).toEqual('ticking')

await root.finishTick(1)
expect(clear).toHaveBeenCalledTimes(1)
expect(root.getState().status).toEqual('idle')

clock.uninstall()
})
})
})

async function waitUntil(predicate: () => boolean): Promise<void> {
Expand All @@ -78,10 +226,18 @@ async function waitUntil(predicate: () => boolean): Promise<void> {
}

class TestRootIndexer extends RootIndexer {
public resolveTick: (height: number) => void = () => {}
public rejectTick: (error: unknown) => void = () => {}

dispatchCounter = 0
ticking = false

constructor(private safeHeight: number, name?: string) {
super(Logger.SILENT.tag(name))
constructor(
private safeHeight: number,
name?: string,
retryStrategy?: { tickRetryStrategy?: RetryStrategy },
) {
super(Logger.SILENT.tag(name), retryStrategy ?? {})

const oldDispatch = Reflect.get(this, 'dispatch')
Reflect.set(this, 'dispatch', (action: IndexerAction) => {
Expand All @@ -98,8 +254,33 @@ class TestRootIndexer extends RootIndexer {
await waitUntil(() => this.dispatchCounter > counter)
}

async finishTick(result: number | Error): Promise<void> {
await waitUntil(() => this.ticking)
const counter = this.dispatchCounter
if (typeof result === 'number') {
this.resolveTick(result)
} else {
this.rejectTick(result)
}
await waitUntil(() => this.dispatchCounter > counter)
}

override tick(): Promise<number> {
return Promise.resolve(this.safeHeight)
this.ticking = true

return new Promise<number>((resolve, reject) => {
this.resolveTick = resolve
this.rejectTick = reject
}).finally(() => {
this.ticking = false
})
}

override async getSafeHeight(): Promise<number> {
const promise = this.tick()
this.resolveTick(this.safeHeight)
await promise
return this.safeHeight
}
}

Expand Down Expand Up @@ -144,8 +325,12 @@ class TestChildIndexer extends ChildIndexer {
parents: BaseIndexer[],
private safeHeight: number,
name?: string,
retryStrategy?: {
invalidateRetryStrategy?: RetryStrategy
updateRetryStrategy?: RetryStrategy
},
) {
super(Logger.SILENT.tag(name), parents)
super(Logger.SILENT.tag(name), parents, retryStrategy ?? {})

const oldDispatch = Reflect.get(this, 'dispatch')
Reflect.set(this, 'dispatch', (action: IndexerAction) => {
Expand Down
Loading

0 comments on commit f6fe36d

Please sign in to comment.