Skip to content

Instantly share code, notes, and snippets.

@vpalos
Last active August 3, 2024 12:03
Show Gist options
  • Save vpalos/8875781ea6e413328be2d3d182355546 to your computer and use it in GitHub Desktop.
Save vpalos/8875781ea6e413328be2d3d182355546 to your computer and use it in GitHub Desktop.
Code sketch for Broker Alarms
/**
* A Condition represents the logical test which eventually is used to trigger
* an alarm (e.g. "out-of-comm", "temperature-below-freezing" etc.). This is a
* class, because a Condition can have parameters and it has to manage them.
*/
abstract class AlarmCondition {
/**
* It can be in either "Up" state (good scenario) or "Down" state (bad scenario).
* By default (unless the constructor does something else), it starts as "Up".
*/
private breached: boolean = true;
public get isBreached() {
return this.breached;
}
public get isUnbreached() {
return !this.breached;
}
/**
* Timestamp of the last change in the condition state (Up<->Down).
*/
private at: number = 0;
public get updatedAt() {
return this.at;
}
/**
* The Condition state (this.up field) should be updated asynchronously
* based on Signals and any specific logic the Condition might implement.
*/
}
class Alarm {
private fired: boolean = false;
private firedAt: number = 0;
private unfiredAt: number = 0;
constructor(
// Triggerring logic.
public readonly condition: AlarmCondition,
// Thresholds (parameters).
public readonly fireMs: number,
public readonly unfireMs: number,
public readonly fireCooldownMs: number,
public readonly unfireCooldownMs: number,
) {}
// State.
public get isFired() {
return this.fired;
}
// Tracking alarm state over time.
public get lastFiredAt() {
return this.firedAt;
}
public get lastUnfiredAt() {
return this.unfiredAt;
}
fire() {
this.fired = true;
this.firedAt = Date.now();
}
unfire() {
this.fired = false;
this.unfiredAt = Date.now();
}
compute() {
if (this.isFired) {
const isRecovered =
this.condition.isBreached && this.since(this.condition.updatedAt, this.unfireMs);
const isUnfireCool = this.since(this.lastUnfiredAt, this.unfireCooldownMs);
if (isRecovered && isUnfireCool) {
this.unfire();
}
} else {
const isFailed =
this.condition.isUnbreached && this.since(this.condition.updatedAt, this.fireMs);
const isFireCool = this.since(this.lastFiredAt, this.fireCooldownMs);
if (isFailed && isFireCool) {
this.fire();
}
}
}
since(timestamp: number, ageMs: number): boolean {
return Date.now() - timestamp > ageMs;
}
}
/**
* This should be one instance per whole process.
* If this has to be scaled, perhaps the alarms must be partitioned somehow (future task).
*/
class AlarmManager {
private period: number = 1000;
private alarms: Readonly<Alarm[]> = [];
constructor() {
this.loadAlarms();
this.startLoop();
}
async loadAlarms() {
/**
* Loads alarms from DB.
*
* Keep `this.alarms` IMMUTABLE and assign it ATOMICALLY so that the
* event loop function will work on the previous instance reference when
* the alarms array is re-assigned.
*/
}
startLoop() {
setInterval(() => this.loopCycle(), this.period);
}
loopCycle() {
this.alarms.forEach(alarm => alarm.compute());
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment