Created
March 24, 2019 09:14
-
-
Save YBogomolov/1a0ebbd1303f4eab21ff87ec90fa331a to your computer and use it in GitHub Desktop.
FSM using Circuit Breaker
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import AbortController from 'abort-controller'; | |
import { circuitBreaker, defaultBreakerOptions } from 'circuit-breaker-monad/lib'; | |
import { BreakerClosed, BreakerOpen, BreakerState } from 'circuit-breaker-monad/lib/types'; | |
import { Either, left } from 'fp-ts/lib/Either'; | |
import { Lazy } from 'fp-ts/lib/function'; | |
import { IORef } from 'fp-ts/lib/IORef'; | |
import fetch from 'node-fetch'; | |
const fetcher = circuitBreaker<User[]>().run(defaultBreakerOptions); | |
class Initial { public readonly tag = 'Initial'; } | |
class Pending { public readonly tag = 'Pending'; } | |
class HalfOpen { public readonly tag = 'HalfOpen'; } | |
class Failure { public readonly tag = 'Failure'; } | |
class Success { public readonly tag = 'Success'; } | |
type State = Initial | Pending | BreakerState | HalfOpen | Failure | Success; | |
interface User { | |
name: string; | |
email: string; | |
imageUrl: string; | |
} | |
interface FetcherState<S extends State, Payload> { | |
state: S; | |
breakerState: IORef<BreakerState>; | |
payload: Payload; | |
} | |
class Transition<InputState extends State, OutputState extends State, Payload> { | |
readonly _I!: InputState; // <- these fields are required, so TypeScript won't complain about unused type parameters. | |
readonly _O!: OutputState; // <- NB: they will *not* be present in compiled JS, so no runtime overhead here! | |
constructor(readonly execute: () => Promise<FetcherState<OutputState, Payload>>) { } | |
} | |
class InitialToPending extends Transition<Initial, Pending, RequestInfo> { } | |
class PendingToClosed extends Transition<Pending, BreakerClosed, Lazy<Promise<User[]>>> { } | |
class ClosedToOpen extends Transition<BreakerClosed, BreakerOpen, Lazy<Promise<User[]>>> { } | |
class ClosedToSuccess extends Transition<BreakerClosed, Success, Either<Error, User[]>> { } | |
class OpenToFailure extends Transition<BreakerOpen, Failure, Either<Error, User[]>> { } | |
class OpenToHalfOpen extends Transition<BreakerOpen, HalfOpen, Lazy<Promise<User[]>>> { } | |
class HalfOpenToClosed extends Transition<HalfOpen, BreakerClosed, Lazy<Promise<User[]>>> { } | |
class HalfOpenToOpen extends Transition<HalfOpen, BreakerOpen, Lazy<Promise<User[]>>> { } | |
class Noop<S extends State, P> extends Transition<S, S, P> { } | |
const requestPromise = (req: string): Lazy<Promise<User[]>> => () => { | |
const controller = new AbortController(); | |
const signal = controller.signal; | |
const timeoutId = setTimeout(() => controller.abort(), 5000); | |
return fetch(req, { signal }) | |
.then((res) => { | |
clearTimeout(timeoutId); | |
if (res.status !== 200) { throw res.statusText; } | |
return res; | |
}) | |
.then<User[]>((res) => res.json()); | |
}; | |
const step = ({ state, payload, breakerState }: FetcherState<State, unknown>): Transition<State, State, unknown> => { | |
switch (state.tag) { | |
case 'Initial': | |
return new InitialToPending(async () => ({ | |
payload: payload as string, | |
state: new Pending(), | |
breakerState, | |
})); | |
case 'Pending': | |
return new PendingToClosed(async () => ({ | |
payload: requestPromise(payload as string), | |
state: new BreakerClosed(0), | |
breakerState, | |
})); | |
case 'Closed': { | |
const request = payload as Lazy<Promise<User[]>>; | |
switch (breakerState.read.run().tag) { | |
case 'Closed': | |
const [ref, result] = fetcher(request, breakerState); | |
return new ClosedToSuccess(async () => ({ | |
payload: await result.run(), | |
state: new Success(), | |
breakerState: ref, | |
})); | |
case 'Open': | |
return new ClosedToOpen(async () => ({ | |
payload: request, | |
state: breakerState.read.run() as BreakerOpen, | |
breakerState, | |
})); | |
} | |
} | |
case 'Open': { | |
const timeout = (breakerState.read.run() as BreakerOpen).openEndTime; | |
if (timeout <= Date.now()) { | |
return new OpenToHalfOpen(async () => ({ | |
payload: payload as Lazy<Promise<User[]>>, | |
state: new HalfOpen(), | |
breakerState: new IORef(new BreakerClosed(0)), | |
})); | |
} | |
return new OpenToFailure(async () => ({ | |
payload: left(new Error('breaker is open')), | |
state: new Failure(), | |
breakerState, | |
})); | |
} | |
case 'HalfOpen': { | |
const canaryRequest = payload as Lazy<Promise<User[]>>; | |
const [ref] = fetcher(canaryRequest, breakerState); | |
switch (ref.read.run().tag) { | |
case 'Closed': | |
return new HalfOpenToClosed(async () => ({ | |
payload: canaryRequest, | |
state: new BreakerClosed(0), | |
breakerState: ref, | |
})); | |
case 'Open': | |
return new HalfOpenToOpen(async () => ({ | |
payload: canaryRequest, | |
state: breakerState.read.run() as BreakerOpen, | |
breakerState, | |
})); | |
} | |
} | |
default: | |
return new Noop(async () => ({ | |
state, | |
payload, | |
breakerState, | |
})); | |
} | |
}; | |
// Let's fetch! | |
const executeFSM = async <A>(prevState: FetcherState<State, unknown>): Promise<FetcherState<State, A>> => { | |
const { state, payload, breakerState } = await step(prevState).execute(); | |
console.info( | |
`Previous state: ${prevState.state.tag}, ` + | |
`next state: ${state.tag}, ` + | |
`breaker state: ${breakerState.read.run().tag}, ` + | |
`payload: ${JSON.stringify(payload, null, 2)}`, | |
); | |
if (state.tag === 'Success' || state.tag === 'Failure') { | |
return { state, payload, breakerState } as FetcherState<State, A>; | |
} | |
return executeFSM({ state, payload, breakerState }); | |
}; | |
(async () => { | |
const initialState: FetcherState<Initial, string> = { | |
payload: 'http://5c95e87d939ad600149a9404.mockapi.io/users', | |
state: new Initial(), | |
breakerState: new IORef(new BreakerOpen(Date.now() - 10000)), | |
}; | |
const { payload } = await executeFSM<Either<Error, User[]>>(initialState); | |
payload.fold( | |
(err) => console.error('Error: ', err), | |
(users) => console.log(users[0].name), | |
); | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment