Skip to content

Commit 3b74caf

Browse files
committed
Remove lifecycle component healthchecks
1 parent d0ce331 commit 3b74caf

File tree

6 files changed

+17
-161
lines changed

6 files changed

+17
-161
lines changed

README.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@ class DatabasePool extends LifecycleComponent {
3434
async close(){
3535
await this.pool.end();
3636
}
37-
checkHealth: undefined; // Optionally, can implement a health check for a component.
3837
}
3938
export const db = new DatabasePool();
4039
```
@@ -57,7 +56,6 @@ const parentComponent = new (class ParentComponent extends LifecycleComponent {
5756
async close(){
5857
await this.closeChildComponents();
5958
}
60-
checkHealth: undefined;
6159
})();
6260

6361
const lifecycle = new Lifecycle();

src/LifecycleComponent.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,16 @@
11
/**
22
* Define a lifecycle component to be managed by the lifecycle manager
33
*/
4-
54
export abstract class LifecycleComponent {
65
#components: LifecycleComponent[] = [];
76
/**
8-
* Lifecycle manager will call `start` once per process lifetime. Each component's start method will be called in the sequence they were registered. Implement your component's
7+
* Lifecycle manager will call `start` once per process lifetime. Each component's start method will be called in the sequence they were registered.
98
*/
109
abstract start(): Promise<unknown>;
1110
/**
1211
* Lifecycle manager will call close once per process lifetime, in the reverse order the components are registered
1312
*/
1413
abstract close(): Promise<unknown>;
15-
/**
16-
* Called by lifecycle manager to check the health of the component. Return true for healthy. If implemented, the lifecycle manager will call `start()` again if the component's health check returns false
17-
*/
18-
abstract checkHealth?(): Promise<boolean>;
1914
/**
2015
* Register a child component of a lifecycle component to guarantee that its children are started in the order they are registered, and closed in the reverse order. The implementer of the lifecycle component is responsible for calling startChildComponents and closeChildComponents during start and close, respectively.
2116
*/
@@ -34,6 +29,9 @@ export abstract class LifecycleComponent {
3429
async closeChildComponents(): Promise<void> {
3530
for (const component of this.#components.toReversed()) await component.close();
3631
}
32+
/**
33+
* Return the registered components of the lifecycle
34+
*/
3735
getChildren(): Readonly<LifecycleComponent[]> {
3836
return [...this.#components];
3937
}

src/componentClass.test.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,12 @@ class DatabasePool extends LifecycleComponent {
1414
close() {
1515
return delay(1);
1616
}
17-
checkHealth: undefined;
1817
}
1918

2019
Deno.test('Lifecycle component as a class', async () => {
2120
const db = new DatabasePool();
2221
const [actual, actualEvent] = setupEvents();
23-
const lc = new Lifecycle({ healthCheckIntervalMs: 50 });
22+
const lc = new Lifecycle();
2423
lc.register(db);
2524

2625
lc.on(...actualEvent('componentStarted'));
@@ -50,7 +49,6 @@ const parent = new (class ParentComponent extends LifecycleComponent {
5049
await this.closeChildComponents();
5150
events.push('parent.closed');
5251
}
53-
checkHealth: undefined;
5452
})();
5553
const childOne = new (class extends LifecycleComponent {
5654
async start() {
@@ -64,7 +62,6 @@ const childOne = new (class extends LifecycleComponent {
6462
await delay(1);
6563
events.push('childOne.closed');
6664
}
67-
checkHealth: undefined;
6865
})();
6966
const childTwo = new (class extends LifecycleComponent {
7067
async start() {
@@ -78,7 +75,6 @@ const childTwo = new (class extends LifecycleComponent {
7875
await delay(1);
7976
events.push('childTwo.closed');
8077
}
81-
checkHealth: undefined;
8278
})();
8379

8480
Deno.test('lifecycle component manages child lifecycle components', async () => {

src/lifecycle.test.ts

Lines changed: 2 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,8 @@ import { delay } from '@std/async';
22
import { expect } from '@std/expect';
33
import { Lifecycle } from './lifecycle.ts';
44
import {
5-
CrashingTestComponentOne,
6-
CrashingTestComponentTwo,
75
createChecks,
86
setupEvents,
9-
TestComponentFour,
107
TestComponentOne,
118
TestComponentThree,
129
TestComponentTwo,
@@ -15,7 +12,7 @@ import {
1512
Deno.test('LifeCycle', async ({ step }) => {
1613
await step('can start & close a lifecycle', async () => {
1714
const { checks, passCheck } = createChecks();
18-
const lc = new Lifecycle({ healthCheckIntervalMs: 50 });
15+
const lc = new Lifecycle();
1916
lc.on('starting', passCheck('didStart'));
2017
lc.on('running', passCheck('didRun'));
2118
lc.on('closing', passCheck('didClosing'));
@@ -31,7 +28,7 @@ Deno.test('LifeCycle', async ({ step }) => {
3128
});
3229
await step('start and close lifecycle components', async () => {
3330
const [actual, actualEvent] = setupEvents();
34-
const lc = new Lifecycle({ healthCheckIntervalMs: 50 });
31+
const lc = new Lifecycle();
3532
lc.register(new TestComponentOne());
3633
lc.register(new TestComponentTwo());
3734
lc.register(new TestComponentThree());
@@ -55,45 +52,4 @@ Deno.test('LifeCycle', async ({ step }) => {
5552
'componentClosed TestComponentOne',
5653
]);
5754
});
58-
await step('restarts crashed life cycle component', async () => {
59-
const [actual, actualEvent] = setupEvents();
60-
const lc = new Lifecycle({ healthCheckIntervalMs: 1 });
61-
62-
lc.register(new TestComponentOne());
63-
lc.register(new TestComponentTwo());
64-
lc.register(new CrashingTestComponentOne());
65-
lc.register(new TestComponentFour());
66-
lc.register(new CrashingTestComponentTwo());
67-
lc.on(...actualEvent('componentStarted'));
68-
lc.on(...actualEvent('componentClosing'));
69-
lc.on(...actualEvent('componentClosed'));
70-
lc.on(...actualEvent('componentRestarting'));
71-
lc.on(...actualEvent('componentRestarted'));
72-
await lc.start();
73-
await delay(15);
74-
await lc.close(false);
75-
await delay(30);
76-
console.log(actual);
77-
expect(actual).toEqual([
78-
'componentStarted TestComponentOne',
79-
'componentStarted TestComponentTwo',
80-
'componentStarted CrashingTestComponentOne',
81-
'componentStarted TestComponentFour',
82-
'componentStarted CrashingTestComponentTwo',
83-
'componentRestarting CrashingTestComponentOne',
84-
'componentRestarted CrashingTestComponentOne',
85-
'componentRestarting CrashingTestComponentTwo',
86-
'componentRestarted CrashingTestComponentTwo',
87-
'componentClosing CrashingTestComponentTwo',
88-
'componentClosed CrashingTestComponentTwo',
89-
'componentClosing TestComponentFour',
90-
'componentClosed TestComponentFour',
91-
'componentClosing CrashingTestComponentOne',
92-
'componentClosed CrashingTestComponentOne',
93-
'componentClosing TestComponentTwo',
94-
'componentClosed TestComponentTwo',
95-
'componentClosing TestComponentOne',
96-
'componentClosed TestComponentOne',
97-
]);
98-
});
9955
});

src/lifecycle.ts

Lines changed: 10 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,28 @@
11
import { isDefined } from '@antman/bool';
2-
import { delay } from '@std/async/delay';
32
import { EventEmitter } from 'node:events';
43
import type { LifecycleComponent } from './LifecycleComponent.ts';
54

6-
/**
7-
* Define the options for the lifecycle manager
8-
*/
9-
export type LifecycleOptions = {
10-
/**
11-
* Define the frequency of component health check cycles. Note: This is the interval in which the lifecycle manager will begin polling each component's status -- the interval begins once each component has returned its status. Components returning their status in a promise can delay subsequent health checks.
12-
*/
13-
healthCheckIntervalMs?: number;
14-
};
155
const statuses = [
166
'pending',
177
'starting',
188
'running',
199
'closing',
2010
'closed',
2111
] as const;
22-
type Status = 'pending' | 'starting' | 'running' | 'closing' | 'closed';
12+
type Status = (typeof statuses)[number];
2313

24-
const defaultOptions = { healthCheckIntervalMs: 600 };
2514
const componentEvents = [
2615
'componentStarted',
2716
'componentClosing',
2817
'componentClosed',
29-
'componentRestarting',
30-
'componentRestarted',
3118
] as const;
3219
type ComponentEvent = (typeof componentEvents)[number];
3320
const isComponentEvent = (e: string | undefined): e is ComponentEvent =>
3421
componentEvents.includes(e as ComponentEvent);
35-
type EventMap = Record<Status | 'healthChecked', []> & {
22+
type EventMap = Record<Status, []> & {
3623
componentStarted: [string];
3724
componentClosing: [string];
3825
componentClosed: [string];
39-
componentRestarting: [string];
40-
componentRestarted: [string];
4126
};
4227
/**
4328
* Manages the clean startup and shutdown of a process and its components.
@@ -48,8 +33,6 @@ export class Lifecycle {
4833
#emitter = new EventEmitter<EventMap>();
4934
#status: Status;
5035
#components: LifecycleComponent[];
51-
#healthCheckInterval: number;
52-
#healthCheckPromise: PromiseWithResolvers<void>;
5336
#setStatus(v: Status): void {
5437
this.#status = v;
5538
this.#emit(v);
@@ -77,16 +60,9 @@ export class Lifecycle {
7760
this.#emitter.on(event, (name: string) => cb(event, name))
7861
);
7962
}
80-
constructor(opt: LifecycleOptions = defaultOptions) {
81-
const { healthCheckIntervalMs } = { ...defaultOptions, ...opt };
63+
constructor() {
8264
this.#status = 'pending';
8365
this.#components = [];
84-
this.#healthCheckInterval = healthCheckIntervalMs;
85-
this.#healthCheckPromise = Promise.withResolvers();
86-
}
87-
88-
get status(): Status {
89-
return this.#status;
9066
}
9167

9268
/**
@@ -110,16 +86,10 @@ export class Lifecycle {
11086
public start = async (): Promise<void> => {
11187
this.#setStatus('starting');
11288
for (const component of this.#components) {
113-
await this.#startComponent(component);
89+
await component.start();
90+
this.#emit('componentStarted', component.constructor.name);
11491
}
11592
Deno.addSignalListener('SIGTERM', this.close);
116-
(async () => {
117-
do {
118-
await delay(this.#healthCheckInterval);
119-
await this.#checkComponentHealth();
120-
} while (this.status === 'running');
121-
this.#healthCheckPromise.resolve();
122-
})();
12393
this.#setStatus('running');
12494
};
12595

@@ -137,32 +107,14 @@ export class Lifecycle {
137107
Deno.removeSignalListener('SIGTERM', this.close);
138108
this.#setStatus('closing');
139109

140-
await this.#healthCheckPromise.promise;
141110
const components = this.#components.toReversed();
142-
143-
for (const component of components) await this.#closeComponent(component);
111+
for (const component of components) {
112+
this.#emit('componentClosing', component.constructor.name);
113+
await component.close();
114+
this.#emit('componentClosed', component.constructor.name);
115+
}
144116

145117
this.#setStatus('closed');
146118
shouldExit && Deno.exit(0);
147119
};
148-
async #startComponent(component: LifecycleComponent): Promise<void> {
149-
await component.start();
150-
this.#emit('componentStarted', component.constructor.name);
151-
}
152-
async #closeComponent(component: LifecycleComponent): Promise<void> {
153-
this.#emit('componentClosing', component.constructor.name);
154-
await component.close();
155-
this.#emit('componentClosed', component.constructor.name);
156-
}
157-
async #restartComponent(component: LifecycleComponent): Promise<void> {
158-
this.#emit('componentRestarting', component.constructor.name);
159-
await (component.start)();
160-
this.#emit('componentRestarted', component.constructor.name);
161-
}
162-
async #checkComponentHealth(): Promise<void> {
163-
for (const component of this.#components) {
164-
if ((await component.checkHealth?.() ?? true) === false) await this.#restartComponent(component);
165-
}
166-
this.#emit('healthChecked');
167-
}
168120
}

src/testUtil.ts

Lines changed: 0 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,6 @@ export const createChecks = () => {
1414

1515
type EventName =
1616
| 'componentStarted'
17-
| 'componentRestarting'
18-
| 'componentRestarted'
1917
| 'componentClosing'
2018
| 'componentClosed';
2119
type Event = `${EventName} ${string}`;
@@ -32,7 +30,6 @@ export class TestComponentOne extends LifecycleComponent {
3230
async close() {
3331
await delay(1);
3432
}
35-
checkHealth: undefined;
3633
}
3734

3835
export class TestComponentTwo extends LifecycleComponent {
@@ -42,7 +39,6 @@ export class TestComponentTwo extends LifecycleComponent {
4239
async close() {
4340
await delay(1);
4441
}
45-
checkHealth: undefined;
4642
}
4743
export class TestComponentThree extends LifecycleComponent {
4844
async start() {
@@ -51,7 +47,6 @@ export class TestComponentThree extends LifecycleComponent {
5147
async close() {
5248
await delay(1);
5349
}
54-
checkHealth: undefined;
5550
}
5651
export class TestComponentFour extends LifecycleComponent {
5752
async start() {
@@ -60,45 +55,6 @@ export class TestComponentFour extends LifecycleComponent {
6055
async close() {
6156
await delay(1);
6257
}
63-
checkHealth: undefined;
64-
}
65-
export class CrashingTestComponentOne extends LifecycleComponent {
66-
crashAfterMs: number;
67-
hasCrashed = false;
68-
constructor(crashAfterMs = 10) {
69-
super();
70-
this.crashAfterMs = crashAfterMs;
71-
}
72-
async start() {
73-
this.hasCrashed = false;
74-
await delay(1);
75-
delay(this.crashAfterMs).then(() => (this.hasCrashed = true));
76-
}
77-
async close() {
78-
await delay(1);
79-
}
80-
checkHealth() {
81-
return Promise.resolve(!this.hasCrashed);
82-
}
83-
}
84-
export class CrashingTestComponentTwo extends LifecycleComponent {
85-
crashAfterMs: number;
86-
hasCrashed = false;
87-
constructor(crashAfterMs = 10) {
88-
super();
89-
this.crashAfterMs = crashAfterMs;
90-
}
91-
async start() {
92-
this.hasCrashed = false;
93-
await delay(1);
94-
delay(this.crashAfterMs).then(() => (this.hasCrashed = true));
95-
}
96-
async close() {
97-
await delay(1);
98-
}
99-
checkHealth() {
100-
return Promise.resolve(!this.hasCrashed);
101-
}
10258
}
10359

10460
export const setupEvents = (): [Events, EventTracker] => {

0 commit comments

Comments
 (0)