Skip to content

Commit

Permalink
Add retry data store sync on armory boot (#354)
Browse files Browse the repository at this point in the history
  • Loading branch information
Samuel authored Jun 21, 2024
1 parent 985bfec commit f7947f7
Show file tree
Hide file tree
Showing 11 changed files with 138 additions and 22 deletions.
4 changes: 2 additions & 2 deletions apps/devtool/src/app/_hooks/useEngineApi.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -112,8 +112,8 @@ const useEngineApi = () => {
try {
setErrors(undefined)
setIsProcessing(true)
const { latestSync } = await syncPolicyEngine(sdkEngineClientConfig)
setIsSynced(latestSync.success)
const { success } = await syncPolicyEngine(sdkEngineClientConfig)
setIsSynced(success)
setTimeout(() => setIsSynced(false), 5000)
} catch (error) {
setErrors(extractErrorMessage(error))
Expand Down
4 changes: 2 additions & 2 deletions apps/policy-engine/src/engine/engine.module.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ConfigService } from '@narval/config-module'
import { EncryptionModule } from '@narval/encryption-module'
import { HttpModule } from '@nestjs/axios'
import { AxiosRetryModule } from '@narval/nestjs-shared'
import { Module, ValidationPipe } from '@nestjs/common'
import { APP_INTERCEPTOR, APP_PIPE } from '@nestjs/core'
import { ZodSerializerInterceptor, ZodValidationPipe } from 'nestjs-zod'
Expand Down Expand Up @@ -28,7 +28,7 @@ import { HttpDataStoreRepository } from './persistence/repository/http-data-stor

@Module({
imports: [
HttpModule,
AxiosRetryModule.forRoot(),
KeyValueModule,
EncryptionModule.registerAsync({
imports: [EngineModule],
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { AxiosRetryModule } from '@narval/nestjs-shared'
import { EntityData, FIXTURE, HttpSource, SourceType } from '@narval/policy-engine-shared'
import { HttpModule } from '@nestjs/axios'
import { HttpStatus } from '@nestjs/common'
import { Test } from '@nestjs/testing'
import nock from 'nock'
Expand All @@ -24,7 +24,7 @@ describe(HttpDataStoreRepository.name, () => {

beforeEach(async () => {
const module = await Test.createTestingModule({
imports: [HttpModule],
imports: [AxiosRetryModule.forRoot()],
providers: [HttpDataStoreRepository]
}).compile()

Expand All @@ -45,5 +45,28 @@ describe(HttpDataStoreRepository.name, () => {

await expect(() => repository.fetch(source)).rejects.toThrow(DataStoreException)
})

it('retries 3 times and fail on the 4th attempt', async () => {
nock(dataStoreHost)
.get(dataStoreEndpoint)
.times(3)
.reply(HttpStatus.INTERNAL_SERVER_ERROR, {})
.get(dataStoreEndpoint)
.reply(HttpStatus.INTERNAL_SERVER_ERROR, {})

await expect(() => repository.fetch(source)).rejects.toThrow(DataStoreException)
})

it('succeeds on the 2nd retry', async () => {
nock(dataStoreHost)
.get(dataStoreEndpoint)
.reply(HttpStatus.INTERNAL_SERVER_ERROR, {})
.get(dataStoreEndpoint)
.reply(HttpStatus.OK, entityData)

const data = await repository.fetch(source)

expect(data).toEqual(entityData)
})
})
})
Original file line number Diff line number Diff line change
@@ -1,27 +1,47 @@
import { HttpSource } from '@narval/policy-engine-shared'
import { HttpService } from '@nestjs/axios'
import { HttpStatus, Injectable } from '@nestjs/common'
import { HttpStatus, Injectable, Logger } from '@nestjs/common'
import axiosRetry from 'axios-retry'
import { catchError, lastValueFrom, map } from 'rxjs'
import { DataStoreException } from '../../core/exception/data-store.exception'
import { DataStoreRepository } from '../../core/repository/data-store.repository'

const MAX_RETRIES = 3

@Injectable()
export class HttpDataStoreRepository implements DataStoreRepository {
private logger = new Logger(HttpDataStoreRepository.name)

constructor(private httpService: HttpService) {}

fetch<Data>(source: HttpSource): Promise<Data> {
return lastValueFrom(
this.httpService.get<Data>(source.url, { headers: source.headers }).pipe(
map((response) => response.data),
catchError((error) => {
throw new DataStoreException({
message: 'Unable to fetch remote data source via HTTP',
suggestedHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY,
context: { source },
origin: error
})
this.httpService
.get<Data>(source.url, {
headers: source.headers,
'axios-retry': {
retries: MAX_RETRIES,
retryDelay: axiosRetry.exponentialDelay,
onRetry: (retryCount) => {
this.logger.log('Retry request to fetch HTTP data source', {
retryCount,
maxRetries: MAX_RETRIES,
url: source.url.split('?')[0]
})
}
}
})
)
.pipe(
map((response) => response.data),
catchError((error) => {
throw new DataStoreException({
message: 'Unable to fetch remote data source via HTTP',
suggestedHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY,
context: { source },
origin: error
})
})
)
)
}
}
28 changes: 26 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@
"@scure/bip39": "^1.3.0",
"@tanstack/react-query": "^5.24.1",
"axios": "1.7.2",
"axios-retry": "^4.4.0",
"bull": "^4.12.1",
"caip": "^1.1.0",
"class-transformer": "^0.5.1",
Expand Down
4 changes: 2 additions & 2 deletions packages/armory-sdk/src/lib/http/policy-engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,15 +55,15 @@ export const sendEvaluationRequest = async (
}
}

export const syncPolicyEngine = async (config: EngineClientConfig): Promise<{ latestSync: { success: boolean } }> => {
export const syncPolicyEngine = async (config: EngineClientConfig): Promise<{ success: boolean }> => {
try {
const { engineHost, engineClientId: clientId, engineClientSecret: clientSecret } = config

if (!clientSecret) {
throw new ArmorySdkException('Client secret is required to sync engine', { config })
}

const { data } = await axios.post<{ latestSync: { success: boolean } }>(`${engineHost}/clients/sync`, null, {
const { data } = await axios.post<{ success: boolean }>(`${engineHost}/clients/sync`, null, {
headers: builBasicHeaders({ clientId, clientSecret })
})

Expand Down
1 change: 1 addition & 0 deletions packages/nestjs-shared/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export * from './lib/decorator'
export * from './lib/dto'
export * from './lib/middleware'
export * from './lib/module'
export * from './lib/service'
export * from './lib/type'
export * from './lib/util'
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export class HttpLoggerMiddleware implements NestMiddleware {
response.on('close', () => {
const { statusCode } = response

this.logger.log(`${method} ${path} ${statusCode}`)
this.logger.log(`${method} ${path.split('?')[0]} ${statusCode}`)
})

next()
Expand Down
46 changes: 46 additions & 0 deletions packages/nestjs-shared/src/lib/module/axios-retry.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { HttpModule, HttpService } from '@nestjs/axios'
import { DynamicModule, Global, Module } from '@nestjs/common'
import axios, { AxiosRequestConfig } from 'axios'
import axiosRetry, { DEFAULT_OPTIONS, IAxiosRetryConfig } from 'axios-retry'

interface AxiosRetryOptions {
axiosConfig?: AxiosRequestConfig
axiosRetryConfig?: IAxiosRetryConfig
}

/**
* A module that provides retry functionality for Axios HTTP requests.
* This module can be imported in a NestJS application to enable automatic retry of failed requests.
*/
@Global()
@Module({})
export class AxiosRetryModule {
/**
* Creates a dynamic module for the AxiosRetryModule.
* @param options - Optional configuration options for the retry behavior.
* @returns A dynamic module that can be imported in a NestJS application.
*/
static forRoot(
options: AxiosRetryOptions = {
axiosRetryConfig: {
...DEFAULT_OPTIONS,
retries: 0 // Default never retries
}
}
): DynamicModule {
const axiosInstance = axios.create(options.axiosConfig)
axiosRetry(axiosInstance, options.axiosRetryConfig)

const axiosProvider = {
provide: HttpService,
useValue: new HttpService(axiosInstance)
}

return {
module: AxiosRetryModule,
imports: [HttpModule],
providers: [axiosProvider],
exports: [axiosProvider]
}
}
}
1 change: 1 addition & 0 deletions packages/nestjs-shared/src/lib/module/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './axios-retry.module'

0 comments on commit f7947f7

Please sign in to comment.