Last active
August 3, 2024 12:03
-
-
Save vpalos/8875781ea6e413328be2d3d182355546 to your computer and use it in GitHub Desktop.
Code sketch for Broker Alarms
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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