Skip to content

Instantly share code, notes, and snippets.

@ivorpad
Created April 1, 2021 14:41
Show Gist options
  • Save ivorpad/c263929228ffb12d367900d47f677122 to your computer and use it in GitHub Desktop.
Save ivorpad/c263929228ffb12d367900d47f677122 to your computer and use it in GitHub Desktop.
Generated by XState Viz: https://xstate.js.org/viz
let count = 0;
const functionThatCanFail = async () => {
const urls = [
`https://api.github.com/users/mojombo`,
`https://api.github.com/users/ivorpad`,
`https://api.github.com/users/failtofetch`,
`https://api.github.com/users/davidkpiano`
];
const user = await fetch(urls[count]);
count++;
if (count > 3) {
count = 0;
}
return user;
};
// Health check will make a request with a small payload to see if the system is operational
// If the server responds with 200 then continue with the requests.
const healthCheck = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({ message: "system recovered" });
}, 2000);
});
};
// Conditional to check if we should trip the circuit
const shouldTripCircuit = (ctx, _evt) => {
console.log((ctx.failure / ctx.fired) * 100 > ctx.errorThresholdPercentage);
return (ctx.failure / ctx.fired) * 100 > ctx.errorThresholdPercentage;
};
const fetchMachine = Machine({
id: "circuit-breaker",
initial: "closed",
context: {
message: null,
user: null,
failure: 0,
success: 0,
fired: 0,
timeout: 2000,
errorThresholdPercentage: 50, // Conditionally trip circuit if error threshold is more than 50%
openTimeout: 5000
},
states: {
closed: {
on: {
FETCH: [
{ target: "open", cond: shouldTripCircuit },
{ target: "loading" }
]
},
after: {
3000: {
actions: "resetMessage",
cond: (ctx) => !!ctx.message
}
},
exit: "increaseFired"
},
loading: {
invoke: {
src: "serviceThatCanFail",
onDone: {
target: "success",
actions: "assignUser"
},
onError: {
target: "open"
}
}
},
open: {
entry: ["increaseFailure", "resetUser", "notifyFailure"],
after: {
HALF_OPEN_INTERVAL: "half-open"
},
on: {
FETCH: "fail-safe"
}
},
"half-open": {
entry: ["resetSuccess", "resetMessage"],
on: {
FETCH: "health-check"
}
},
"health-check": {
entry: assign({
message: "Checking if systems are operational"
}),
invoke: {
src: healthCheck,
onDone: {
target: "closed",
actions: ["resetFailure", "notifySuccess"] // Maybe increase the success counter here?
},
onError: "open"
}
},
"fail-safe": {
entry: ["notifyFailure", "increaseFired"],
always: "open"
},
success: {
entry: "increaseSuccess",
always: "closed"
}
}
},
{
actions: {
increaseSuccess: assign({
success: ctx => ctx.success + 1
}),
increaseFailure: assign({
failure: ctx => ctx.failure + 1
}),
increaseFired: assign({
fired: ctx => ctx.fired + 1
}),
resetFailure: assign({
failure: 0,
errorMessage: null
}),
resetSuccess: assign({
success: 0
}),
resetUser: assign({
user: null
}),
resetMessage: assign({
message: null
}),
notifyFailure: assign({
message: ctx =>
`System failed. Please try again later. Failure: ${ctx.failure}`,
user: null
}),
notifySuccess: assign({
message: "All systems operational"
}),
assignUser: assign({ user: (ctx, event) => event.data })
},
services: {
serviceThatCanFail: async () => {
const resp = await functionThatCanFail();
if (!resp.ok) {
throw new Error("User not found");
} else {
return resp.json();
}
}
},
delays: {
HALF_OPEN_INTERVAL: (ctx, event) => {
return ctx.openTimeout;
}
}
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment