-
Notifications
You must be signed in to change notification settings - Fork 2
feat(api): MongoDB reconnect, bounded timeouts and graceful shutdown #652
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
Kuchizu
wants to merge
5
commits into
master
Choose a base branch
from
feat/mongo-reconnect
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 2 commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
1ef27ac
feat(api): MongoDB reconnect, bounded timeouts and graceful shutdown
Kuchizu eda7b38
Bump version up to 1.5.1
github-actions[bot] 1b127c5
Merge branch 'master' into feat/mongo-reconnect
Kuchizu b7ac922
Bump version up to 1.5.2
github-actions[bot] 1d1af89
fix(api): prevent Mongo socket leak on partial connect, add connectio…
Kuchizu File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or 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
This file contains hidden or 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
This file contains hidden or 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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,6 +5,20 @@ 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; | ||
|
|
||
|
Kuchizu marked this conversation as resolved.
|
||
| /** | ||
| * 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 | ||
| */ | ||
|
|
@@ -52,42 +66,117 @@ export const mongoClients: MongoClients = { | |
| }; | ||
|
|
||
| /** | ||
| * Common params for all connections | ||
| * 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> { | ||
| 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); | ||
|
|
||
| const message = (err as Error)?.message ?? String(err); | ||
|
|
||
| if (attempt === reconnectTries) { | ||
| throw new Error(`[Mongo:${name}] failed after ${reconnectTries} attempts: ${message}`); | ||
| } | ||
| console.warn(`[Mongo:${name}] attempt ${attempt}/${reconnectTries} failed: ${message}`); | ||
| await new Promise((resolve) => setTimeout(resolve, reconnectInterval)); | ||
| } | ||
|
Kuchizu marked this conversation as resolved.
|
||
| } | ||
|
|
||
| throw new Error(`[Mongo:${name}] unreachable`); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. seems that two errors and would always come together, could we leave only one?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done |
||
| } | ||
|
|
||
| /** | ||
| * Common params for all connections | ||
| * Note: useNewUrlParser and useUnifiedTopology are deprecated in mongodb 6.x and removed | ||
| * Logs and reports heartbeat failures / recoveries once per transition. | ||
| * | ||
| * @param name - logical name for logging | ||
| * @param client - connected client to observe | ||
| */ | ||
| const connectionConfig: MongoClientOptions = withMongoMetrics({}); | ||
| 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`); | ||
| }); | ||
| } | ||
|
|
||
| /** | ||
| * Setups connections to the databases (hawk api and events databases) | ||
| * 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> { | ||
| try { | ||
| const [hawkMongoClient, eventsMongoClient] = await Promise.all([ | ||
| MongoClient.connect(hawkDBUrl, connectionConfig), | ||
| MongoClient.connect(eventsDBUrl, connectionConfig), | ||
| const [hawkClient, eventsClient] = await Promise.all([ | ||
| connectWithRetry('hawk', hawkDBUrl), | ||
| connectWithRetry('events', eventsDBUrl), | ||
| ]); | ||
|
Kuchizu marked this conversation as resolved.
Outdated
|
||
|
|
||
| mongoClients.hawk = hawkMongoClient; | ||
| mongoClients.events = eventsMongoClient; | ||
|
|
||
| databases.hawk = hawkMongoClient.db(); | ||
| databases.events = eventsMongoClient.db(); | ||
| mongoClients.hawk = hawkClient; | ||
| mongoClients.events = eventsClient; | ||
| databases.hawk = hawkClient.db(); | ||
| databases.events = eventsClient.db(); | ||
|
|
||
| /** | ||
| * Log and and measure MongoDB metrics | ||
| * Log and measure MongoDB metrics, then observe heartbeats for outage logs | ||
| */ | ||
| setupMongoMetrics(hawkMongoClient); | ||
| setupMongoMetrics(eventsMongoClient); | ||
| setupMongoMetrics(hawkClient); | ||
| setupMongoMetrics(eventsClient); | ||
| watchConnection('hawk', hawkClient); | ||
| watchConnection('events', eventsClient); | ||
| } catch (e) { | ||
| /** Catch start Mongo errors */ | ||
| HawkCatcher.send(e as Error); | ||
| throw e; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * 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 | ||
| */ | ||
|
|
||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.