-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathmongo.ts
More file actions
193 lines (164 loc) · 5.2 KB
/
Copy pathmongo.ts
File metadata and controls
193 lines (164 loc) · 5.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
import { Db, MongoClient, MongoClientOptions } from 'mongodb';
import HawkCatcher from '@hawk.so/nodejs';
import { setupMongoMetrics, withMongoMetrics } from './metrics';
const hawkDBUrl = process.env.MONGO_HAWK_DB_URL || 'mongodb://localhost:27017/hawk';
const eventsDBUrl = process.env.MONGO_EVENTS_DB_URL || 'mongodb://localhost:27017/events';
const reconnectTries = Number(process.env.MONGO_RECONNECT_TRIES) || 60;
const reconnectInterval = Number(process.env.MONGO_RECONNECT_INTERVAL) || 1000;
/**
* serverSelectionTimeoutMS bounds how long an op waits for an available
* server — without it queries hang forever during an outage.
*/
const connectionConfig: MongoClientOptions = withMongoMetrics({
serverSelectionTimeoutMS: 10000,
socketTimeoutMS: 45000,
retryWrites: true,
retryReads: true,
});
/**
* Connections to Hawk databases
*/
interface Databases {
/**
* Hawk database for users, workspaces, project, etc
*/
hawk: Db | null;
/**
* Events database
*/
events: Db | null;
}
/**
* Exported database connections
*/
export const databases: Databases = {
hawk: null,
events: null,
};
/**
* Mongo clients for Hawk database
*/
interface MongoClients {
/**
* Mongo client for Hawk database for users, workspaces, project, etc
*/
hawk: MongoClient | null;
/**
* Mongo client for events database
*/
events: MongoClient | null;
}
/**
* Export mongo clients
*/
export const mongoClients: MongoClients = {
hawk: null,
events: null,
};
/**
* Connects to the given URL, retrying with a fixed interval up to
* MONGO_RECONNECT_TRIES times before giving up.
*
* @param name - logical name for logging
* @param url - MongoDB connection string
* @returns connected client
*/
async function connectWithRetry(name: string, url: string): Promise<MongoClient> {
let lastError = 'unknown error';
for (let attempt = 1; attempt <= reconnectTries; attempt++) {
const client = new MongoClient(url, connectionConfig);
try {
await client.connect();
console.log(`[Mongo:${name}] connected`);
return client;
} catch (err) {
await client.close().catch(() => undefined);
lastError = (err as Error)?.message ?? String(err);
console.warn(`[Mongo:${name}] attempt ${attempt}/${reconnectTries} failed: ${lastError}`);
if (attempt < reconnectTries) {
await new Promise((resolve) => setTimeout(resolve, reconnectInterval));
}
}
}
throw new Error(`[Mongo:${name}] failed after ${reconnectTries} attempts: ${lastError}`);
}
/**
* Logs and reports heartbeat failures / recoveries once per transition.
*
* @param name - logical name for logging
* @param client - connected client to observe
*/
function watchConnection(name: string, client: MongoClient): void {
let healthy = true;
client.on('serverHeartbeatFailed', (event) => {
if (!healthy) {
return;
}
healthy = false;
const message = (event.failure as Error)?.message ?? 'heartbeat failed';
console.error(`[Mongo:${name}] connection lost: ${message}`);
HawkCatcher.send(new Error(`MongoDB ${name} connection lost: ${message}`));
});
client.on('serverHeartbeatSucceeded', () => {
if (healthy) {
return;
}
healthy = true;
console.log(`[Mongo:${name}] connection recovered`);
});
}
/**
* Connects to both databases with bounded retry. The driver auto-recovers
* from transient failures on already-open clients, so retries here cover
* the initial handshake only.
*
* @returns promise resolved when both clients are connected
*/
export async function setupConnections(): Promise<void> {
const results = await Promise.allSettled([
connectWithRetry('hawk', hawkDBUrl),
connectWithRetry('events', eventsDBUrl),
]);
const failure = results.find((r): r is PromiseRejectedResult => r.status === 'rejected');
if (failure) {
/** Close any clients that did connect so we don't leak sockets */
await Promise.allSettled(
results.map((r) => (r.status === 'fulfilled' ? r.value.close() : Promise.resolve()))
);
HawkCatcher.send(failure.reason as Error);
throw failure.reason;
}
const hawkClient = (results[0] as PromiseFulfilledResult<MongoClient>).value;
const eventsClient = (results[1] as PromiseFulfilledResult<MongoClient>).value;
mongoClients.hawk = hawkClient;
mongoClients.events = eventsClient;
databases.hawk = hawkClient.db();
databases.events = eventsClient.db();
/**
* Log and measure MongoDB metrics, then observe heartbeats for outage logs
*/
setupMongoMetrics(hawkClient);
setupMongoMetrics(eventsClient);
watchConnection('hawk', hawkClient);
watchConnection('events', eventsClient);
}
/**
* Closes both clients. Call from SIGTERM/SIGINT for graceful shutdown.
*
* @returns promise resolved once both clients are closed
*/
export async function closeConnections(): Promise<void> {
await Promise.allSettled([
mongoClients.hawk?.close(),
mongoClients.events?.close(),
]);
mongoClients.hawk = null;
mongoClients.events = null;
databases.hawk = null;
databases.events = null;
}
/**
* Makes '_id' field optional on type
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type OptionalId<TSchema> = Omit<TSchema, '_id'> & { _id?: any };