Skip to content

Commit 1ed12ab

Browse files
authored
feat: support health check (#368)
1 parent 6e1674a commit 1ed12ab

2 files changed

Lines changed: 87 additions & 0 deletions

File tree

connection.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@ export interface RedisConnectionOptions {
3737
*/
3838
maxRetryCount?: number;
3939
backoff?: Backoff;
40+
/**
41+
* When this option is set, a `PING` command is sent every specified number of seconds.
42+
*/
43+
healthCheckInterval?: number;
4044
}
4145

4246
const kEmptyRedisArgs: Array<RedisValue> = [];
@@ -164,6 +168,8 @@ export class RedisConnection implements Connection {
164168
this.close();
165169
throw error;
166170
}
171+
172+
this.#enableHealthCheckIfNeeded();
167173
} catch (error) {
168174
if (error instanceof AuthenticationError) {
169175
throw (error.cause ?? error);
@@ -253,6 +259,31 @@ export class RedisConnection implements Connection {
253259
private isManuallyClosedByUser(): boolean {
254260
return this._isClosed && !this._isConnected;
255261
}
262+
263+
#enableHealthCheckIfNeeded() {
264+
const { healthCheckInterval } = this.options;
265+
if (healthCheckInterval == null) {
266+
return;
267+
}
268+
269+
const ping = async () => {
270+
if (this.isManuallyClosedByUser()) {
271+
return;
272+
}
273+
274+
try {
275+
await this.sendCommand("PING");
276+
this._isConnected = true;
277+
} catch {
278+
// TODO: notify the user of an error
279+
this._isConnected = false;
280+
} finally {
281+
setTimeout(ping, healthCheckInterval);
282+
}
283+
};
284+
285+
setTimeout(ping, healthCheckInterval);
286+
}
256287
}
257288

258289
class AuthenticationError extends Error {}

tests/commands/connection.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { connect, createLazyClient } from "../../mod.ts";
22
import {
33
assert,
44
assertEquals,
5+
assertExists,
56
assertNotEquals,
67
} from "../../vendor/https/deno.land/std/testing/asserts.ts";
78
import {
@@ -10,6 +11,7 @@ import {
1011
describe,
1112
it,
1213
} from "../../vendor/https/deno.land/std/testing/bdd.ts";
14+
import { delay } from "../../vendor/https/deno.land/std/async/delay.ts";
1315
import { newClient } from "../test_util.ts";
1416
import type { TestServer } from "../test_util.ts";
1517
import type { Redis } from "../../mod.ts";
@@ -68,6 +70,34 @@ export function connectionTests(
6870
});
6971
});
7072

73+
describe("health check", () => {
74+
it("should send a ping every `healthCheckInterval`", async () => {
75+
const opts = {
76+
...getOpts(),
77+
healthCheckInterval: 10,
78+
};
79+
const client = await connect(opts);
80+
const rawPreviousCommandStats = await client.info("commandstats");
81+
await delay(25);
82+
const rawCurrentCommandStats = await client.info("commandstats");
83+
client.close();
84+
85+
await delay(10); // NOTE: After closing the connection, no errors should occur
86+
87+
const previousPingStats =
88+
parseCommandStats(rawPreviousCommandStats)["ping"];
89+
const currentPingStats =
90+
parseCommandStats(rawCurrentCommandStats)["ping"];
91+
assertExists(previousPingStats);
92+
assertExists(currentPingStats);
93+
94+
const previousCallCount = previousPingStats["calls"];
95+
const currentCallCount = currentPingStats["calls"];
96+
const d = currentCallCount - previousCallCount;
97+
assert(d >= 2, `${d} should be greater than or equal to 2`);
98+
});
99+
});
100+
71101
describe("createLazyClient", () => {
72102
it("returns the lazily connected client", async () => {
73103
const opts = getOpts();
@@ -114,3 +144,29 @@ export function connectionTests(
114144
});
115145
});
116146
}
147+
148+
function parseCommandStats(
149+
stats: string,
150+
): Record<string, Record<string, number>> {
151+
return stats.split("\r\n").reduce((statsByCommand, line) => {
152+
if (line.startsWith("#") || line.length === 0) {
153+
return statsByCommand;
154+
}
155+
156+
const [section, details] = line.split(":");
157+
assertExists(section);
158+
assertExists(details);
159+
const sectionPrefix = "cmdstat_";
160+
assert(section.startsWith(sectionPrefix));
161+
const command = section.slice(sectionPrefix.length);
162+
statsByCommand[command] = details.split(",").reduce((stats, attr) => {
163+
const [key, value] = attr.split("=");
164+
assertExists(key);
165+
assertExists(value);
166+
stats[key] = parseInt(value);
167+
return stats;
168+
}, {} as Record<string, number>);
169+
170+
return statsByCommand;
171+
}, {} as Record<string, Record<string, number>>);
172+
}

0 commit comments

Comments
 (0)