Skip to content

Instantly share code, notes, and snippets.

@YBogomolov
Created March 24, 2019 09:14
Show Gist options
  • Save YBogomolov/1a0ebbd1303f4eab21ff87ec90fa331a to your computer and use it in GitHub Desktop.
Save YBogomolov/1a0ebbd1303f4eab21ff87ec90fa331a to your computer and use it in GitHub Desktop.
FSM using Circuit Breaker
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