Skip to content

Instantly share code, notes, and snippets.

@gichamba
Last active September 23, 2023 06:34
Show Gist options
  • Save gichamba/7b93b74b1cde88b59b2eca489f660c50 to your computer and use it in GitHub Desktop.
Save gichamba/7b93b74b1cde88b59b2eca489f660c50 to your computer and use it in GitHub Desktop.
Charging Engine
/* eslint-disable @typescript-eslint/no-floating-promises */
/* eslint-disable @typescript-eslint/strict-boolean-expressions */
/* eslint-disable @typescript-eslint/restrict-template-expressions */
/* eslint-disable @typescript-eslint/no-misused-promises */
import express from "express";
import { json } from "body-parser";
import { createClient } from "redis";
const DEFAULT_BALANCE = 100;
interface ChargeResult {
isAuthorized: boolean;
remainingBalance: number;
charges: number;
}
interface EvalOptions {
keys?: string[];
arguments?: string[];
}
interface LockResult {
success: number;
balance: number;
}
async function connect(): Promise<ReturnType<typeof createClient>> {
const url = `redis://${process.env.REDIS_HOST ?? "localhost"}:${process.env.REDIS_PORT ?? "6379"}`;
const client = createClient({ url });
await client.connect();
return client;
}
async function reset(account: string): Promise<void> {
const balanceKey = `${account}/balance`;
const client = await connect();
try {
await client.set(balanceKey, DEFAULT_BALANCE);
} finally {
await client.disconnect();
}
}
// Added function to set a lock and retrieve balance in a single transaction
async function setLock(client: any, account: string): Promise<LockResult> {
const balanceVersionKey = `${account}/balance/version`;
const balanceVersionValue = generateRandomString(10);
const balanceKey = `${account}/balance`;
// We need the key to self destruct after 30 milliseconds
// to remove the need of manually having to delete it
const ttlMilliseconds = 30;
// Use a Lua script to set the key if it doesn't exist and set TTL
// And get the account balance
const luaScript = `
local success
local balance = redis.call("GET", KEYS[1])
if redis.call("EXISTS", KEYS[2]) == 0 then
redis.call("SET", KEYS[2], ARGV[1])
redis.call("PEXPIRE", KEYS[2], ARGV[2])
success = 1
else
success = 0
end
return {success, balance}
`;
// Define EvalOptions
const evalOptions: EvalOptions = {
keys: [balanceKey, balanceVersionKey],
arguments: [balanceVersionValue, ttlMilliseconds.toString()],
};
const result = await client.eval(luaScript, evalOptions);
const lockResult: LockResult = {
success: result[0],
balance: parseInt(result[1]),
};
return lockResult;
}
// updated charge function to use setLock function
async function charge(account: string, charges: number): Promise<ChargeResult> {
const client = await connect();
const balanceKey = `${account}/balance`;
try {
const lockResult = await setLock(client, account);
const balance = lockResult.balance;
if (lockResult.success === 0 || charges > balance) {
return { isAuthorized: false, remainingBalance: balance, charges: 0 };
}
const newBalance = balance - charges;
await client.set(balanceKey, newBalance);
return { isAuthorized: true, remainingBalance: newBalance, charges };
} finally {
await client.disconnect();
}
}
// Added function to generate random string
const generateRandomString = (length: number): string => {
let result = "";
const characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
const charactersLength = characters.length;
for (let i = 0; i < length; i++) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));
}
return result;
};
export function buildApp(): express.Application {
const app = express();
app.use(json());
app.post("/reset", async (req, res) => {
try {
const account = req.body.account ?? "account";
await reset(account);
console.log(`Successfully reset account ${account}`);
res.sendStatus(204);
} catch (e) {
console.error("Error while resetting account", e);
res.status(500).json({ error: String(e) });
}
});
app.post("/charge", async (req, res) => {
try {
const account = req.body.account ?? "account";
const result = await charge(account, req.body.charges ?? 10);
// Changed what is logged to make it easier to observe operation success and account balance
console.log(result);
res.status(200).json(result);
} catch (e) {
console.error("Error while charging account", e);
res.status(500).json({ error: String(e) });
}
});
return app;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment