mirror of
https://github.com/Start9Labs/start-sdk.git
synced 2026-03-31 04:33:40 +00:00
chore: Adding mainFn helpers
This commit is contained in:
4
lib/health/HealthReceipt.ts
Normal file
4
lib/health/HealthReceipt.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
declare const HealthProof: unique symbol;
|
||||
export type HealthReceipt = {
|
||||
[HealthProof]: never;
|
||||
};
|
||||
4
lib/health/ReadyProof.ts
Normal file
4
lib/health/ReadyProof.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export declare const ReadyProof: unique symbol;
|
||||
export type ReadyReceipt = {
|
||||
[ReadyProof]: never;
|
||||
};
|
||||
6
lib/health/checkFns/CheckResult.ts
Normal file
6
lib/health/checkFns/CheckResult.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { HealthStatus } from "../../types";
|
||||
|
||||
export type CheckResult = {
|
||||
status: HealthStatus;
|
||||
message?: string;
|
||||
};
|
||||
26
lib/health/checkFns/checkPortListening.ts
Normal file
26
lib/health/checkFns/checkPortListening.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { Effects } from "../../types";
|
||||
export function containsAddress(x: string, port: number) {
|
||||
const readPorts = x
|
||||
.split("\n")
|
||||
.filter(Boolean)
|
||||
.splice(1)
|
||||
.map((x) => x.split(" ").filter(Boolean)[1]?.split(":")?.[1])
|
||||
.filter(Boolean)
|
||||
.map((x) => Number.parseInt(x, 16))
|
||||
.filter(Number.isFinite);
|
||||
return readPorts.indexOf(port) >= 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is used to check if a port is listening on the system.
|
||||
* Used during the health check fn or the check main fn.
|
||||
*/
|
||||
export async function checkPortListening(effects: Effects, port: number) {
|
||||
const hasAddress =
|
||||
containsAddress(await effects.shell("cat /proc/net/tcp"), port) ||
|
||||
containsAddress(await effects.shell("cat /proc/net/udp"), port);
|
||||
if (hasAddress) {
|
||||
return;
|
||||
}
|
||||
throw new Error(`Port ${port} is not listening`);
|
||||
}
|
||||
31
lib/health/checkFns/checkWebUrl.ts
Normal file
31
lib/health/checkFns/checkWebUrl.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { Effects } from "../../types";
|
||||
import { CheckResult } from "./CheckResult";
|
||||
import { timeoutPromise } from "./index";
|
||||
|
||||
/**
|
||||
* This is a helper function to check if a web url is reachable.
|
||||
* @param url
|
||||
* @param createSuccess
|
||||
* @returns
|
||||
*/
|
||||
export const checkWebUrl = async (
|
||||
url: string,
|
||||
effects: Effects,
|
||||
{
|
||||
timeout = 1000,
|
||||
successMessage = `Reached ${url}`,
|
||||
errorMessage = `Error while fetching URL: ${url}`,
|
||||
} = {}
|
||||
): Promise<CheckResult> => {
|
||||
return Promise.race([effects.fetch(url), timeoutPromise(timeout)])
|
||||
.then((x) => ({
|
||||
status: "passing" as const,
|
||||
message: successMessage,
|
||||
}))
|
||||
.catch((e) => {
|
||||
effects.warn(`Error while fetching URL: ${url}`);
|
||||
effects.error(JSON.stringify(e));
|
||||
effects.error(e.toString());
|
||||
return { status: "failing" as const, message: errorMessage };
|
||||
});
|
||||
};
|
||||
11
lib/health/checkFns/index.ts
Normal file
11
lib/health/checkFns/index.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { runHealthScript } from "./runHealthScript";
|
||||
export { checkPortListening } from "./checkPortListening";
|
||||
export { CheckResult } from "./CheckResult";
|
||||
export { checkWebUrl } from "./checkWebUrl";
|
||||
|
||||
export function timeoutPromise(ms: number, { message = "Timed out" } = {}) {
|
||||
return new Promise<never>((resolve, reject) =>
|
||||
setTimeout(() => reject(new Error(message)), ms)
|
||||
);
|
||||
}
|
||||
export { runHealthScript };
|
||||
35
lib/health/checkFns/runHealthScript.ts
Normal file
35
lib/health/checkFns/runHealthScript.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { Effects } from "../../types";
|
||||
import { CheckResult } from "./CheckResult";
|
||||
import { timeoutPromise } from "./index";
|
||||
|
||||
/**
|
||||
* Running a health script, is used when we want to have a simple
|
||||
* script in bash or something like that. It should return something that is useful
|
||||
* in {result: string} else it is considered an error
|
||||
* @param param0
|
||||
* @returns
|
||||
*/
|
||||
export const runHealthScript = async (
|
||||
effects: Effects,
|
||||
runCommand: Parameters<Effects["runCommand"]>[0],
|
||||
{
|
||||
timeout = 30000,
|
||||
errorMessage = `Error while running command: ${runCommand}`,
|
||||
message = (res: string) =>
|
||||
`Have ran script ${runCommand} and the result: ${res}`,
|
||||
}
|
||||
): Promise<CheckResult> => {
|
||||
const res = await Promise.race([
|
||||
effects.runCommand(runCommand),
|
||||
timeoutPromise(timeout),
|
||||
]).catch((e) => {
|
||||
effects.warn(errorMessage);
|
||||
effects.warn(JSON.stringify(e));
|
||||
effects.warn(e.toString());
|
||||
throw { status: "failing", message: errorMessage } as CheckResult;
|
||||
});
|
||||
return {
|
||||
status: "passing",
|
||||
message: message(res),
|
||||
} as CheckResult;
|
||||
};
|
||||
@@ -1,87 +0,0 @@
|
||||
import { Types } from "..";
|
||||
import { object, string } from "ts-matches";
|
||||
|
||||
export type HealthCheck = (
|
||||
effects: Types.Effects,
|
||||
dateMs: number
|
||||
) => Promise<HealthResult>;
|
||||
export type HealthResult =
|
||||
| { success: string }
|
||||
| { failure: string }
|
||||
| { disabled: null }
|
||||
| { starting: null }
|
||||
| { loading: string };
|
||||
const hasMessage = object({ message: string }).test;
|
||||
function safelyStringify(e: unknown) {
|
||||
if (hasMessage(e)) return e.message;
|
||||
if (string.test(e)) return e;
|
||||
try {
|
||||
return JSON.stringify(e);
|
||||
} catch (e) {
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
async function timeoutHealth(
|
||||
effects: Types.Effects,
|
||||
timeMs: number
|
||||
): Promise<HealthResult> {
|
||||
await effects.sleep(timeMs);
|
||||
return { failure: "Timed out " };
|
||||
}
|
||||
|
||||
/**
|
||||
* Health runner is usually used during the main function, and will be running in a loop.
|
||||
* This health check then will be run every intervalS seconds.
|
||||
* The return needs a create()
|
||||
* then from there we need a start().
|
||||
* The stop function is used to stop the health check.
|
||||
*/
|
||||
|
||||
export default function healthRunner(name: string, fn: HealthCheck) {
|
||||
return {
|
||||
/**
|
||||
* All values in seconds. Defaults):
|
||||
*
|
||||
* interval: 60s
|
||||
*
|
||||
* timeout: 10s
|
||||
*
|
||||
* delay: 10s
|
||||
*/
|
||||
create(
|
||||
effects: Types.Effects,
|
||||
options = { interval: 60, timeout: 10, delay: 10 }
|
||||
) {
|
||||
let running: any;
|
||||
function startFn() {
|
||||
clearInterval(running);
|
||||
setTimeout(() => {
|
||||
running = setInterval(async () => {
|
||||
const result = await Promise.race([
|
||||
timeoutHealth(effects, options.timeout * 1000),
|
||||
fn(effects, 123),
|
||||
]).catch((e) => {
|
||||
return { failure: safelyStringify(e) };
|
||||
});
|
||||
(effects as any).setHealthStatus({
|
||||
name,
|
||||
result,
|
||||
});
|
||||
}, options.interval * 1000);
|
||||
}, options.delay * 1000);
|
||||
}
|
||||
|
||||
const self = {
|
||||
stop() {
|
||||
clearInterval(running);
|
||||
return self;
|
||||
},
|
||||
start() {
|
||||
startFn();
|
||||
return self;
|
||||
},
|
||||
};
|
||||
return self;
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -1,2 +1,7 @@
|
||||
export * from "./util";
|
||||
export { default as healthRunner } from "./healthRunner";
|
||||
export * as checkFns from "./checkFns";
|
||||
export * as trigger from "./trigger";
|
||||
|
||||
export { TriggerInput } from "./trigger/TriggerInput";
|
||||
export { HealthReceipt } from "./HealthReceipt";
|
||||
export { readyCheck } from "./readyCheck";
|
||||
export { ReadyProof } from "./ReadyProof";
|
||||
|
||||
55
lib/health/readyCheck.ts
Normal file
55
lib/health/readyCheck.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { InterfaceReceipt } from "../mainFn/interfaceReceipt";
|
||||
import { Daemon, Effects } from "../types";
|
||||
import { CheckResult } from "./checkFns/CheckResult";
|
||||
import { ReadyReceipt } from "./ReadyProof";
|
||||
import { HealthReceipt } from "./HealthReceipt";
|
||||
import { Trigger } from "./trigger";
|
||||
import { TriggerInput } from "./trigger/TriggerInput";
|
||||
import { defaultTrigger } from "./trigger/defaultTrigger";
|
||||
|
||||
function readReciptOf<A extends { daemon: Daemon }>(a: A) {
|
||||
return a as A & ReadyReceipt;
|
||||
}
|
||||
export function readyCheck(o: {
|
||||
effects: Effects;
|
||||
started(onTerm: () => void): null;
|
||||
interfaceReceipt: InterfaceReceipt;
|
||||
healthReceipts: Iterable<HealthReceipt>;
|
||||
daemonReceipt: Daemon;
|
||||
name: string;
|
||||
trigger?: Trigger;
|
||||
fn(): Promise<CheckResult> | CheckResult;
|
||||
onFirstSuccess?: () => () => Promise<unknown> | unknown;
|
||||
}) {
|
||||
new Promise(async () => {
|
||||
const trigger = (o.trigger ?? defaultTrigger)();
|
||||
let currentValue: TriggerInput = {
|
||||
lastResult: null,
|
||||
hadSuccess: false,
|
||||
};
|
||||
for (
|
||||
let res = await trigger.next(currentValue);
|
||||
!res.done;
|
||||
res = await trigger.next(currentValue)
|
||||
) {
|
||||
try {
|
||||
const { status, message } = await o.fn();
|
||||
if (!currentValue.hadSuccess) {
|
||||
await o.started(o?.onFirstSuccess ?? (() => o.daemonReceipt.term()));
|
||||
}
|
||||
await o.effects.setHealth({
|
||||
name: o.name,
|
||||
status,
|
||||
message,
|
||||
});
|
||||
currentValue.hadSuccess = true;
|
||||
currentValue.lastResult = "success";
|
||||
} catch (_) {
|
||||
currentValue.lastResult = "failure";
|
||||
}
|
||||
}
|
||||
});
|
||||
return readReciptOf({
|
||||
daemon: o.daemonReceipt,
|
||||
});
|
||||
}
|
||||
4
lib/health/trigger/TriggerInput.ts
Normal file
4
lib/health/trigger/TriggerInput.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export type TriggerInput = {
|
||||
lastResult: "success" | "failure" | null;
|
||||
hadSuccess: boolean;
|
||||
};
|
||||
28
lib/health/trigger/changeOnFirstSuccess.ts
Normal file
28
lib/health/trigger/changeOnFirstSuccess.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { TriggerInput } from "./TriggerInput";
|
||||
import { Trigger } from "./index";
|
||||
|
||||
export function changeOnFirstSuccess(o: {
|
||||
beforeFirstSuccess: Trigger;
|
||||
afterFirstSuccess: Trigger;
|
||||
}) {
|
||||
return async function* () {
|
||||
const beforeFirstSuccess = o.beforeFirstSuccess();
|
||||
let currentValue: TriggerInput = yield;
|
||||
beforeFirstSuccess.next(currentValue);
|
||||
for (
|
||||
let res = await beforeFirstSuccess.next(currentValue);
|
||||
currentValue?.lastResult !== "success" && !res.done;
|
||||
res = await beforeFirstSuccess.next(currentValue)
|
||||
) {
|
||||
currentValue = yield;
|
||||
}
|
||||
const afterFirstSuccess = o.afterFirstSuccess();
|
||||
for (
|
||||
let res = await afterFirstSuccess.next(currentValue);
|
||||
!res.done;
|
||||
res = await afterFirstSuccess.next(currentValue)
|
||||
) {
|
||||
currentValue = yield;
|
||||
}
|
||||
};
|
||||
}
|
||||
8
lib/health/trigger/cooldownTrigger.ts
Normal file
8
lib/health/trigger/cooldownTrigger.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export function cooldownTrigger(timeMs: number) {
|
||||
return async function* () {
|
||||
while (true) {
|
||||
await new Promise((resolve) => setTimeout(resolve, timeMs));
|
||||
yield;
|
||||
}
|
||||
};
|
||||
}
|
||||
7
lib/health/trigger/defaultTrigger.ts
Normal file
7
lib/health/trigger/defaultTrigger.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { cooldownTrigger } from "./cooldownTrigger";
|
||||
import { changeOnFirstSuccess } from "./changeOnFirstSuccess";
|
||||
|
||||
export const defaultTrigger = changeOnFirstSuccess({
|
||||
beforeFirstSuccess: cooldownTrigger(0),
|
||||
afterFirstSuccess: cooldownTrigger(30000),
|
||||
});
|
||||
5
lib/health/trigger/index.ts
Normal file
5
lib/health/trigger/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { TriggerInput } from "./TriggerInput";
|
||||
export { changeOnFirstSuccess } from "./changeOnFirstSuccess";
|
||||
export { cooldownTrigger } from "./cooldownTrigger";
|
||||
|
||||
export type Trigger = () => AsyncIterator<unknown, unknown, TriggerInput>;
|
||||
@@ -1,77 +0,0 @@
|
||||
import { Effects } from "../types";
|
||||
import { isKnownError } from "../util";
|
||||
import { HealthResult } from "./healthRunner";
|
||||
|
||||
/**
|
||||
* This is a helper function to check if a web url is reachable.
|
||||
* @param url
|
||||
* @param createSuccess
|
||||
* @returns
|
||||
*/
|
||||
export const checkWebUrl: (
|
||||
url: string,
|
||||
createSuccess?: null | ((response?: string | null) => string)
|
||||
) => (effects: Effects, duration: number) => Promise<HealthResult> = (
|
||||
url,
|
||||
createSuccess = null
|
||||
) => {
|
||||
return async (effects, duration) => {
|
||||
const errorValue = guardDurationAboveMinimum({
|
||||
duration,
|
||||
minimumTime: 5000,
|
||||
});
|
||||
if (!!errorValue) {
|
||||
return errorValue;
|
||||
}
|
||||
|
||||
return await effects
|
||||
.fetch(url)
|
||||
.then((x) => ({
|
||||
success: createSuccess?.(x.body) ?? `Successfully fetched URL: ${url}`,
|
||||
}))
|
||||
.catch((e) => {
|
||||
effects.warn(`Error while fetching URL: ${url}`);
|
||||
effects.error(JSON.stringify(e));
|
||||
effects.error(e.toString());
|
||||
return { failure: `Error while fetching URL: ${url}` };
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Running a health script, is used when we want to have a simple
|
||||
* script in bash or something like that. It should return something that is useful
|
||||
* in {result: string} else it is considered an error
|
||||
* @param param0
|
||||
* @returns
|
||||
*/
|
||||
export const runHealthScript =
|
||||
({
|
||||
command,
|
||||
args,
|
||||
message,
|
||||
}: {
|
||||
command: string;
|
||||
args: string[];
|
||||
message: ((result: unknown) => string) | null;
|
||||
}) =>
|
||||
async (effects: Effects, _duration: number): Promise<HealthResult> => {
|
||||
const res = await effects.runCommand({ command, args });
|
||||
return {
|
||||
success:
|
||||
message?.(res) ?? `Have ran script ${command} and the result: ${res}`,
|
||||
};
|
||||
};
|
||||
|
||||
// Ensure the starting duration is pass a minimum
|
||||
export const guardDurationAboveMinimum = (input: {
|
||||
duration: number;
|
||||
minimumTime: number;
|
||||
}): null | HealthResult =>
|
||||
input.duration <= input.minimumTime ? { starting: null } : null;
|
||||
|
||||
export const catchError = (effects: Effects) => (e: unknown) => {
|
||||
if (isKnownError(e)) return e;
|
||||
effects.error(`Health check failed: ${e}`);
|
||||
return { failure: "Error while running health check" };
|
||||
};
|
||||
Reference in New Issue
Block a user