-
-
Notifications
You must be signed in to change notification settings - Fork 10
Convert MongoDB from standalone to replica set #4184
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
Closed
Closed
Changes from all commits
Commits
Show all changes
25 commits
Select commit
Hold shift + click to select a range
5b2ea66
Initial plan
Copilot ca558a9
Convert MongoDB from standalone to replica set
Copilot dbfb84c
Add startDatabase.js to .gitignore exceptions
Copilot 34a4308
Remove setupMongo.js and its .gitignore reference
Copilot 46d3f10
Fix prettier formatting in startDatabase.js and tasks.json
Copilot d0942b0
Update run-mongo VSCode task to use npm run database
Copilot 2c9afee
Make startDatabase.js failure modes fatal with proper exit codes
Copilot 36d0f2f
Fix exit code logic: use ?? instead of || to preserve clean exit (0)
Copilot 49ca484
Merge branch 'master' into copilot/convert-standalone-to-replica-set
imnasnainaec 33d6a56
Fix replica-set loading (by Copilot locally)
imnasnainaec 369677c
Update postStart hook to reconfigure replica set member host after po…
Copilot a75f87a
Merge branch 'master' into copilot/convert-standalone-to-replica-set
imnasnainaec d909c9d
Allow mongo to stop
imnasnainaec e5a2234
Re-architecture initial run
imnasnainaec 3997ae8
Bump mongo from 7.0.28 to 7.0.30
imnasnainaec 4f5b3bc
Simplify
imnasnainaec fc59ce2
Fix first db init
imnasnainaec 4ee89ff
[npm run database] Enable Ctrl+C kill
imnasnainaec 70aac1a
[npm run database] Tidy
imnasnainaec 7281a51
[npm run database] Tidy
imnasnainaec 6aadfbe
Add exit handling and comments
imnasnainaec 8b20487
Fix return 0 on error
imnasnainaec 76f7e35
Fix ping loop
imnasnainaec 68b4048
Fix conf reconfig
imnasnainaec c4a58a6
Clarify init waiting
imnasnainaec 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
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 |
|---|---|---|
| @@ -0,0 +1,81 @@ | ||
| // Ensure a single-node replica set is initialized and advertising the | ||
| // expected host for this environment. | ||
| // | ||
| // MONGO_INITDB_REPLICA_HOST can be set to the resolvable hostname:port | ||
| // used to advertise this member (e.g. "database:27017" in Kubernetes). | ||
| const host = process.env.MONGO_INITDB_REPLICA_HOST || "localhost:27017"; | ||
| const maxWaitMs = 60 * 1000; | ||
| const intervalMs = 1000; | ||
| const start = Date.now(); | ||
|
|
||
| /** Ensure the primary host is correctly configured */ | ||
| function ensurePrimaryHost(forceReconfig) { | ||
| let conf; | ||
| try { | ||
| conf = rs.conf(); | ||
| } catch (error) { | ||
| conf = db.getSiblingDB("local").system.replset.findOne(); | ||
| if (!forceReconfig || !conf) { | ||
| throw error; | ||
| } | ||
| } | ||
|
|
||
| if (!conf.members?.length) { | ||
| throw new Error("Replica set config has no members"); | ||
| } | ||
|
|
||
| if (conf.members[0].host !== host) { | ||
| print(`Updating replica set member host to ${host}`); | ||
| conf.members[0].host = host; | ||
| conf.version = (conf.version || 1) + 1; | ||
| rs.reconfig(conf, { force: forceReconfig }); | ||
| return false; | ||
| } | ||
|
|
||
| return true; | ||
| } | ||
|
|
||
| // Wait for replica set to be initialized. | ||
| let replicaSetInitiated = false; | ||
| while (Date.now() - start < maxWaitMs) { | ||
| try { | ||
| rs.initiate({ _id: "rs0", members: [{ _id: 0, host: host }] }); | ||
| print(`Initialized replica set with host ${host}`); | ||
| replicaSetInitiated = true; | ||
| break; | ||
| } catch (err) { | ||
| if (String(err).includes("already initialized")) { | ||
| print("Replica set already initialized"); | ||
| replicaSetInitiated = true; | ||
| break; | ||
| } | ||
|
|
||
| print(`Replica set init deferred: ${err}`); | ||
| } | ||
|
|
||
| sleep(intervalMs); | ||
| } | ||
| if (!replicaSetInitiated) { | ||
| throw new Error(`Replica set not initialized after ${maxWaitMs}ms`); | ||
| } | ||
|
|
||
| // Wait for this member to be PRIMARY with the correct host. | ||
| while (Date.now() - start < maxWaitMs) { | ||
| try { | ||
| if (db.hello().isWritablePrimary) { | ||
| if (ensurePrimaryHost(false)) { | ||
| print(`Replica set is PRIMARY with correct host: ${host}`); | ||
| quit(0); | ||
| } | ||
| } else { | ||
| ensurePrimaryHost(true); | ||
| } | ||
| } catch (err) { | ||
| print(`Host alignment deferred: ${err}`); | ||
| } | ||
|
|
||
| sleep(intervalMs); | ||
| } | ||
| throw new Error( | ||
| `Replica set did not reach PRIMARY state with host ${host} after ${maxWaitMs}ms` | ||
| ); |
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
This file was deleted.
Oops, something went wrong.
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 |
|---|---|---|
| @@ -0,0 +1,173 @@ | ||
| "use strict"; | ||
|
|
||
| const { spawn, spawnSync } = require("child_process"); | ||
| const { emitKeypressEvents } = require("readline"); | ||
|
|
||
| const { ensureDir } = require("fs-extra"); | ||
|
|
||
| const dbPath = "./mongo_database"; | ||
| const replSetName = "rs0"; | ||
| const maxAttempts = 30; | ||
| const retryIntervalSeconds = 1; | ||
| const mongoshTimeoutSeconds = 10; | ||
| let mongodProcess; | ||
| let exiting = false; | ||
|
|
||
| /** Check if TTY and raw mode are available. */ | ||
| function canUseRawMode() { | ||
| return process.stdin.isTTY && typeof process.stdin.setRawMode === "function"; | ||
| } | ||
|
|
||
| /** Disable raw mode. */ | ||
| function stopRawMode() { | ||
| if (canUseRawMode()) { | ||
| process.stdin.setRawMode(false); | ||
| process.stdin.pause(); | ||
| } | ||
| } | ||
|
|
||
| /** Enable raw mode to capture Ctrl+C when it doesn't otherwise work. */ | ||
| function startRawMode() { | ||
| if (canUseRawMode()) { | ||
| emitKeypressEvents(process.stdin); | ||
| process.stdin.setRawMode(true); | ||
| process.stdin.resume(); | ||
| process.stdin.on("keypress", (_text, key) => { | ||
| if (key?.ctrl && key.name === "c") { | ||
| forceExit(130); | ||
| } | ||
| }); | ||
| } | ||
| } | ||
|
|
||
| /** Unless already exiting, forcibly kill mongod and exit process. */ | ||
| function forceExit(code = 0) { | ||
| if (exiting) { | ||
| return; | ||
| } | ||
|
|
||
| exiting = true; | ||
|
|
||
| stopRawMode(); | ||
|
|
||
| if (mongodProcess && !mongodProcess.killed) { | ||
| mongodProcess.kill("SIGKILL"); | ||
| } | ||
| process.exit(code); | ||
| } | ||
|
|
||
| /** Set up handlers for various interrupts. */ | ||
| function setUpInterruptHandling() { | ||
| process.on("SIGINT", () => forceExit(130)); | ||
| process.on("SIGBREAK", () => forceExit(131)); | ||
| process.on("SIGTERM", () => forceExit(143)); | ||
| startRawMode(); | ||
| } | ||
|
|
||
| /** Convert error to string. */ | ||
| function getErrorMessage(error) { | ||
| return error instanceof Error | ||
| ? `${error.code}: ${error.message}` | ||
| : String(error); | ||
| } | ||
|
|
||
| /** Run a mongosh command with timeout and interrupt handling. */ | ||
| function runMongosh(args, options = {}) { | ||
| const result = spawnSync("mongosh", [...args, "--quiet"], { | ||
| timeout: mongoshTimeoutSeconds * 1000, | ||
| killSignal: "SIGTERM", | ||
| ...options, | ||
| }); | ||
|
|
||
| if (result.error) { | ||
| throw result.error; | ||
| } | ||
| if (result.signal) { | ||
| throw new Error(`mongosh exited due to signal ${result.signal}`); | ||
| } | ||
|
|
||
| return result; | ||
| } | ||
|
|
||
| /** Ping with mongosh until available, up to a preset number of attempts. */ | ||
| async function waitForMongo() { | ||
| for (let i = 0; i < maxAttempts; i++) { | ||
| let result; | ||
| try { | ||
| result = runMongosh(["--eval", "db.adminCommand('ping')"]); | ||
| } catch (err) { | ||
| console.warn(`ping attempt ${i + 1} failed: ${getErrorMessage(err)}`); | ||
| } | ||
| if (result?.status === 0) { | ||
| return true; | ||
| } | ||
| if (i < maxAttempts - 1) { | ||
| await new Promise((res) => setTimeout(res, retryIntervalSeconds * 1000)); | ||
| } | ||
| } | ||
|
|
||
| console.error(`MongoDB pings failed after ${maxAttempts} attempts.`); | ||
| return false; | ||
| } | ||
|
|
||
| /** Start a replica set if not already initialized. */ | ||
| async function initReplicaSet() { | ||
| try { | ||
| const result = runMongosh( | ||
| [ | ||
| "--eval", | ||
| `try { rs.status() } catch { rs.initiate({ _id: '${replSetName}', members: [{ _id: 0, host: 'localhost:27017' }] }) }`, | ||
| ], | ||
| { stdio: "inherit" } | ||
| ); | ||
| return result.status === 0; | ||
| } catch (error) { | ||
| console.error(`Replica set init failed: ${getErrorMessage(error)}`); | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| async function main() { | ||
| setUpInterruptHandling(); | ||
|
|
||
| await ensureDir(dbPath); | ||
|
|
||
| // Start the mongod process | ||
| mongodProcess = spawn( | ||
| "mongod", | ||
| ["--dbpath", dbPath, "--replSet", replSetName, "--quiet"], | ||
| { stdio: "inherit" } | ||
| ); | ||
|
|
||
| // Exit when the mongod process errors | ||
| mongodProcess.on("error", (err) => { | ||
| console.error(`mongod error: ${err.message}`); | ||
| forceExit(1); | ||
| }); | ||
|
|
||
| // Exit when the mongod process exits | ||
| mongodProcess.on("exit", (code, signal) => { | ||
| if (exiting) { | ||
| return; | ||
| } | ||
|
|
||
| if (code || signal) { | ||
| console.error(`mongod exited with code ${code}, signal ${signal}`); | ||
| } | ||
| forceExit(signal ? 1 : (code ?? 1)); | ||
| }); | ||
|
|
||
| if (!(await waitForMongo())) { | ||
| console.error("MongoDB did not start in time"); | ||
| forceExit(1); | ||
| } | ||
| if (!(await initReplicaSet())) { | ||
| console.error("Replica set initialization failed"); | ||
| forceExit(1); | ||
| } | ||
| } | ||
|
|
||
| main().catch((err) => { | ||
| console.error(err); | ||
| forceExit(1); | ||
| }); | ||
Oops, something went wrong.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: sillsdev/TheCombine
Length of output: 2181
🏁 Script executed:
Repository: sillsdev/TheCombine
Length of output: 2202
🏁 Script executed:
Repository: sillsdev/TheCombine
Length of output: 1708
🏁 Script executed:
Repository: sillsdev/TheCombine
Length of output: 2118
Add PRIMARY readiness check after replica set initialization.
initReplicaSet()returns success as soon asrs.initiate()is accepted, but the replica set member is not usable until it elects itself PRIMARY. Sincepackage.jsonstartsbackendanddatabasein parallel (line 30:run-p), the startup can race this window. The Kubernetes bootstrap indatabase/init/00-replica-set.jsalready implements the correct pattern withdb.hello().isWritablePrimary; apply the same here.Add a
waitForPrimary()function that polls fordb.hello().isWritablePrimarywith the same retry pattern aswaitForMongo(), then call it inmain()afterinitReplicaSet()succeeds. Required variables (maxAttempts,retryIntervalSeconds) are already defined.Suggested fix
async function initReplicaSet() { try { const result = runMongosh( [ "--eval", `try { rs.status() } catch { rs.initiate({ _id: '${replSetName}', members: [{ _id: 0, host: 'localhost:27017' }] }) }`, ], { stdio: "inherit" } ); return result.status === 0; } catch (error) { console.error(`Replica set init failed: ${getErrorMessage(error)}`); return false; } } + +async function waitForPrimary() { + for (let i = 0; i < maxAttempts; i++) { + try { + const result = runMongosh([ + "--eval", + "quit(db.hello().isWritablePrimary ? 0 : 1)", + ]); + if (result.status === 0) { + return true; + } + } catch (error) { + console.warn(`MongoDB primary check failed: ${getErrorMessage(error)}`); + } + + await new Promise((res) => setTimeout(res, retryIntervalSeconds * 1000)); + } + + return false; +} async function main() { setUpInterruptHandling(); @@ -161,6 +181,10 @@ async function main() { if (!(await initReplicaSet())) { console.error("Replica set initialization failed"); forceExit(1); } + if (!(await waitForPrimary())) { + console.error("Replica set did not become PRIMARY in time"); + forceExit(1); + } }🤖 Prompt for AI Agents
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this fixed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Length of output: 4305
@imnasnainaec, not yet fixed. ThewaitForPrimary()function is absent fromscripts/startDatabase.js, andmain()still returns immediately afterinitReplicaSet()without polling for PRIMARY election. The race condition withrun-p(parallel backend + database startup) remains.Here's the change needed — it slots cleanly between
initReplicaSetandmain:And in
main(), after the existinginitReplicaSetblock: