Skip to content

Commit

Permalink
feature #8 - add circuit breaker to update exchange rate lambda
Browse files Browse the repository at this point in the history
  • Loading branch information
Eugeniosales committed Oct 17, 2022
1 parent be45ee2 commit 0abf4b7
Show file tree
Hide file tree
Showing 10 changed files with 216 additions and 8 deletions.
3 changes: 3 additions & 0 deletions src/2-business/enums/circuitBreaker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export enum CircuitBreakerEnum {
EXCHANGE_RATE_EXTERNAL_API = 'EXCHANGE_RATE_EXTERNAL_API'
}
7 changes: 7 additions & 0 deletions src/2-business/repositories/iCircuitBreakerRepository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { CircuitBreakerEnum } from '../enums/circuitBreaker'
import { CircuitBreakerState, CircuitBreakerData } from '../utils/circuitBreaker'

export interface ICircuitBreakerRepository {
get (key: CircuitBreakerEnum): Promise<CircuitBreakerData>
update (key: CircuitBreakerEnum, state: CircuitBreakerState, nextAvailabilityCheckTimestamp: number): Promise<CircuitBreakerData>
}
14 changes: 12 additions & 2 deletions src/2-business/useCases/exchangeRate/updateExchangeRateUseCase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,37 @@ import { IExchangeRateService } from '../../services/iExchangeRateService'
import { ExchangeRate } from '../../../1-domain/entities/exchangeRate'
import { CurrencyEnum } from '../../enums/currencyEnum'
import { ILatestRatesResponse } from '../../../1-domain/models/iExchangeRateResponse'
import { ICircuitBreaker } from '../../utils/circuitBreaker'
import { CircuitBreakerEnum } from '../../enums/circuitBreaker'

export class UpdateExchangeRateUseCase {

constructor (
private readonly exchangeRateRepository: IExchangeRateRepository,
private readonly exchangeRateService: IExchangeRateService
private readonly exchangeRateService: IExchangeRateService,
private readonly circuitBreaker: ICircuitBreaker
) {}

private logPrefix: string = 'UpdateExchangeRateUseCase'

async execute (): Promise<void> {
async execute (): Promise<boolean> {
console.log(`${this.logPrefix} :: start`)

try {
const state = await this.circuitBreaker.checkState(CircuitBreakerEnum.EXCHANGE_RATE_EXTERNAL_API)

if (!state) { return false }

const exchangeRatesResponse: ILatestRatesResponse = await this.exchangeRateService.getLatestRates(CurrencyEnum.BRL)
const exchangeRates: ExchangeRate = {
baseCurrency: exchangeRatesResponse.base,
rates: exchangeRatesResponse.rates
}
await this.exchangeRateRepository.upsert(exchangeRates)
console.log(`${this.logPrefix} :: end`)
return true
} catch (error) {
await this.circuitBreaker.fire(CircuitBreakerEnum.EXCHANGE_RATE_EXTERNAL_API)
throw error
}
}
Expand Down
81 changes: 81 additions & 0 deletions src/2-business/utils/circuitBreaker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { CircuitBreakerEnum } from '../enums/circuitBreaker'
import { ICircuitBreakerRepository } from '../repositories/iCircuitBreakerRepository'

export interface CircuitBreakerData {
key: string
state: CircuitBreakerState
options: CircuitBreakerOptions
nextAvailabilityCheckTimestamp: number
}

export type CircuitBreakerOptions = {
openBreakerTimeoutInMs: number
closedBreakerTimeoutInMs: number
minFailedRequestThreshold: number
}

export enum CircuitBreakerState {
OPENED = 'OPENED',
CLOSED = 'CLOSED',
HALF = 'HALF'
}

export interface ICircuitBreaker {
get (key: CircuitBreakerEnum): Promise<CircuitBreakerData>
fire (key: CircuitBreakerEnum): Promise<void>
checkState (key: CircuitBreakerEnum): Promise<boolean>
sendFailObservabilityEvent (key: CircuitBreakerEnum, circuitBreaker: CircuitBreakerData): void
sendSuccessObservabilityEvent (key: CircuitBreakerEnum, circuitBreaker: CircuitBreakerData): void
}

export class CircuitBreaker implements ICircuitBreaker {
private readonly logPrefix = 'CircuitBreaker'

constructor (
private readonly circuitBreakerRepository: ICircuitBreakerRepository
) {}

async get (key: CircuitBreakerEnum): Promise<CircuitBreakerData> {
console.log(`${this.logPrefix} :: get state`)
return this.circuitBreakerRepository.get(key)
}

async fire (key: CircuitBreakerEnum) {
console.log(`${this.logPrefix} :: close cicuit for key :: ${key}`)

const circuitBreakerOptions = await this.get(key)

const currentDate = +new Date()
const nextAvailabilityCheckTimestamp = currentDate + circuitBreakerOptions.options.closedBreakerTimeoutInMs
await this.circuitBreakerRepository.update(key, CircuitBreakerState.CLOSED, nextAvailabilityCheckTimestamp)
this.sendFailObservabilityEvent(key, circuitBreakerOptions)
}

async checkState (key: CircuitBreakerEnum): Promise<boolean> {
const circuitBreakerOptions = await this.get(CircuitBreakerEnum.EXCHANGE_RATE_EXTERNAL_API)

if (circuitBreakerOptions.state === CircuitBreakerState.OPENED) {
this.sendSuccessObservabilityEvent(key, circuitBreakerOptions)
return true
}

const currentDate = +new Date()
if (circuitBreakerOptions.state === CircuitBreakerState.CLOSED && circuitBreakerOptions.nextAvailabilityCheckTimestamp < currentDate) {
await this.circuitBreakerRepository.update(key, CircuitBreakerState.OPENED, 0)
return true
}

this.sendFailObservabilityEvent(key, circuitBreakerOptions)
return false
}

sendFailObservabilityEvent (key: CircuitBreakerEnum, circuitBreaker: CircuitBreakerData): void {
console.log(`${this.logPrefix} :: CLOSED event sent to New Relic :: ${JSON.stringify(circuitBreaker)}`)
}

sendSuccessObservabilityEvent (key: CircuitBreakerEnum, circuitBreaker: CircuitBreakerData): void {
console.log(`${this.logPrefix} :: OPENED event sent to New Relic :: ${JSON.stringify(circuitBreaker)}`)
}
}

export default CircuitBreaker
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ import { IExchangeRateService } from '../../../2-business/services/iExchangeRate
import { UpdateExchangeRateUseCase } from '../../../2-business/useCases/exchangeRate/updateExchangeRateUseCase'
import { ControllerBase } from '../../controllerBase'
import { Output } from '../../../2-business/dto/output'
import { ICircuitBreaker } from '../../../2-business/utils/circuitBreaker'

export class UpdateExchangeRateController extends ControllerBase<void, void> {
constructor (
private readonly exchangeRateRepository: IExchangeRateRepository,
private readonly exchangeRateService: IExchangeRateService
private readonly exchangeRateService: IExchangeRateService,
private readonly circuitBreaker: ICircuitBreaker
) { super() }

async run (): Promise<Output<void>> {
Expand All @@ -17,7 +19,8 @@ export class UpdateExchangeRateController extends ControllerBase<void, void> {
try {
const updateExchangeRateUseCase = new UpdateExchangeRateUseCase(
this.exchangeRateRepository,
this.exchangeRateService
this.exchangeRateService,
this.circuitBreaker
)

await updateExchangeRateUseCase.execute()
Expand Down
5 changes: 4 additions & 1 deletion src/4-framework/functions/exchangeRate/updateExchangeRate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,14 @@
import { UpdateExchangeRateController } from '../../../3-adapters/controller/exchangeRate/updateExchangeRateController'
import { ExchangeRateRepository } from '../../repositories/exchangeRateRepository'
import { ExchangeRateService } from '../../services/exchangeRateService'
import { CircuitBreakerRepository } from '../../repositories/circuitBreakerRepository'
import { CircuitBreaker } from '.././../../2-business/utils/circuitBreaker'

exports.handler = async () => {
const updateExchangeRateController = new UpdateExchangeRateController(
new ExchangeRateRepository(),
new ExchangeRateService()
new ExchangeRateService(),
new CircuitBreaker(new CircuitBreakerRepository())
)

const response = await updateExchangeRateController.run()
Expand Down
40 changes: 40 additions & 0 deletions src/4-framework/models/dynamo/circuitBreaker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import dynamoose, { Schema } from 'dynamoose'
import { Document } from 'dynamoose/dist/Document'
import { SchemaDefinition } from 'dynamoose/dist/Schema'
import { Model, ModelOptionsOptional } from 'dynamoose/dist/Model'
import { CircuitBreakerData } from '../../../2-business/utils/circuitBreaker'

export interface CircuitBreakerEntity extends Document, CircuitBreakerData { }

const schemaDefinition: SchemaDefinition = {
key: {
type: String,
hashKey: true
},
state: {
type: String,
required: true
},
options: {
type: Object,
required: true
},
nextAvailabilityCheckTimestamp: {
type: Number,
required: true
}
}

const schema = new Schema(schemaDefinition, {
timestamps: true,
saveUnknown: true
})

const modelOptions: ModelOptionsOptional = {
throughput: 'ON_DEMAND',
create: false,
waitForActive: false
}

export const CircuitBreakerModel: Model<CircuitBreakerEntity> =
dynamoose.model('CircuitBreaker', schema, modelOptions)
18 changes: 18 additions & 0 deletions src/4-framework/repositories/circuitBreakerRepository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { CircuitBreakerState, CircuitBreakerData } from '../../2-business/utils/circuitBreaker'
import { CircuitBreakerModel } from '../models/dynamo/circuitBreaker'
import { ICircuitBreakerRepository } from '../../2-business/repositories/iCircuitBreakerRepository'
import { CurrencyEnum } from '../../2-business/enums/currencyEnum'
import { CircuitBreakerEnum } from '../../2-business/enums/circuitBreaker'

export class CircuitBreakerRepository implements ICircuitBreakerRepository {
get (key: CircuitBreakerEnum): Promise<CircuitBreakerData> {
return CircuitBreakerModel.get({ key })
}

update (key: CircuitBreakerEnum, state: CircuitBreakerState, nextAvailabilityCheckTimestamp: number): Promise<CircuitBreakerData> {
return CircuitBreakerModel.update(key, {
state,
nextAvailabilityCheckTimestamp
})
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { UpdateExchangeRateUseCase } from '../../../../src/2-business/useCases/e
import { IExchangeRateRepository } from '../../../../src/2-business/repositories/iExchangeRateRepository'
import { IExchangeRateService } from '../../../../src/2-business/services/iExchangeRateService'
import { CurrencyEnum } from '../../../../src/2-business/enums/currencyEnum'
import { CircuitBreakerData, ICircuitBreaker } from '../../../../src/2-business/utils/circuitBreaker'

describe('UpdateExchangeRateUseCase', () => {
const exchangeRateMock: ExchangeRate = {
Expand Down Expand Up @@ -35,8 +36,20 @@ describe('UpdateExchangeRateUseCase', () => {
}
}

const circuitBreakerMock: CircuitBreakerData = {
key: 'EXCHANGE_RATE_EXTERNAL_API',
options: {
closedBreakerTimeoutInMs: 300000,
minFailedRequestThreshold: 1,
openBreakerTimeoutInMs: 120000
},
nextAvailabilityCheckTimestamp: 1666024304,
state: 'OPENED'
} as CircuitBreakerData

let exchangeRateRepository: IExchangeRateRepository
let exchangeRateService: IExchangeRateService
let circuitBreaker: ICircuitBreaker

const setMocks = () => {
exchangeRateRepository = {
Expand All @@ -46,6 +59,13 @@ describe('UpdateExchangeRateUseCase', () => {
exchangeRateService = {
getLatestRates: jest.fn().mockResolvedValue(exchangeRateResponseMock)
}
circuitBreaker = {
get: jest.fn(),
fire: jest.fn(),
sendFailObservabilityEvent: jest.fn(),
sendSuccessObservabilityEvent: jest.fn(),
checkState: jest.fn().mockResolvedValue(true)
}
}

beforeEach(() => {
Expand All @@ -55,7 +75,8 @@ describe('UpdateExchangeRateUseCase', () => {
test('Success::should update the exchange rates successfully', async () => {
const useCase = new UpdateExchangeRateUseCase(
exchangeRateRepository,
exchangeRateService
exchangeRateService,
circuitBreaker
)

await expect(useCase.execute()).resolves.not.toThrow()
Expand All @@ -70,7 +91,8 @@ describe('UpdateExchangeRateUseCase', () => {

const useCase = new UpdateExchangeRateUseCase(
exchangeRateRepository,
exchangeRateService
exchangeRateService,
circuitBreaker
)

await expect(useCase.execute()).rejects.toThrow()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { UpdateExchangeRateController } from '../../../../src/3-adapters/control
import { IExchangeRateRepository } from '../../../../src/2-business/repositories/iExchangeRateRepository'
import { IExchangeRateService } from '../../../../src/2-business/services/iExchangeRateService'
import { ILatestRatesResponse } from '../../../../src/1-domain/models/iExchangeRateResponse'
import { ICircuitBreaker, CircuitBreakerState, CircuitBreakerData } from '../../../../src/2-business/utils/circuitBreaker'

jest.mock('../../../../src/2-business/useCases/exchangeRate/updateExchangeRateUseCase')

Expand All @@ -22,8 +23,20 @@ describe('UpdateExchangeRateController', () => {
}
}

const circuitBreakerMock: CircuitBreakerData = {
key: 'EXCHANGE_RATE_EXTERNAL_API',
options: {
closedBreakerTimeoutInMs: 300000,
minFailedRequestThreshold: 1,
openBreakerTimeoutInMs: 120000
},
nextAvailabilityCheckTimestamp: 1666024304,
state: CircuitBreakerState.OPENED
}

let exchangeRateRepository: IExchangeRateRepository
let exchangeRateService: IExchangeRateService
let circuitBreaker: ICircuitBreaker
let UpdateExchangeRateUseCase = jest.fn()

const setMocks = () => {
Expand All @@ -34,6 +47,13 @@ describe('UpdateExchangeRateController', () => {
exchangeRateService = {
getLatestRates: jest.fn().mockResolvedValue(exchangeRateResponseMock)
}
circuitBreaker = {
get: jest.fn(),
fire: jest.fn(),
sendFailObservabilityEvent: jest.fn(),
sendSuccessObservabilityEvent: jest.fn(),
checkState: jest.fn().mockResolvedValue(true)
}
UpdateExchangeRateUseCase.mockClear()
}

Expand All @@ -49,7 +69,8 @@ describe('UpdateExchangeRateController', () => {
})
const controller = new UpdateExchangeRateController(
exchangeRateRepository,
exchangeRateService
exchangeRateService,
circuitBreaker
)

await expect(controller.run()).resolves.not.toThrow()
Expand Down

0 comments on commit 0abf4b7

Please sign in to comment.