Skip to content

Commit 33c75eb

Browse files
committed
datastore: Move EmbeddedMongoDBManager to datastore. #TASK-8038
1 parent a8cc8f8 commit 33c75eb

4 files changed

Lines changed: 408 additions & 1 deletion

File tree

commons-datastore/commons-datastore-mongodb/pom.xml

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,10 +55,32 @@
5555
<groupId>org.hamcrest</groupId>
5656
<artifactId>hamcrest-core</artifactId>
5757
</dependency>
58+
<!--
59+
JUnit and flapdoodle are compile-scope + optional because EmbeddedMongoDBManager
60+
and EmbeddedMongoDBRule (under .mongodb.test) ship in the main jar so downstream
61+
projects can reuse them. They don't transitively propagate: consumers must add
62+
their own test-scope flapdoodle dependency to actually use them.
63+
-->
5864
<dependency>
5965
<groupId>junit</groupId>
6066
<artifactId>junit</artifactId>
61-
<scope>test</scope>
67+
<scope>compile</scope>
68+
<optional>true</optional>
69+
</dependency>
70+
<dependency>
71+
<groupId>de.flapdoodle.embed</groupId>
72+
<artifactId>de.flapdoodle.embed.mongo</artifactId>
73+
<optional>true</optional>
74+
</dependency>
75+
<dependency>
76+
<groupId>de.flapdoodle.embed</groupId>
77+
<artifactId>de.flapdoodle.embed.process</artifactId>
78+
<optional>true</optional>
79+
</dependency>
80+
<dependency>
81+
<groupId>de.flapdoodle.reverse</groupId>
82+
<artifactId>de.flapdoodle.reverse</artifactId>
83+
<optional>true</optional>
6284
</dependency>
6385
<dependency>
6486
<groupId>org.mockito</groupId>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,301 @@
1+
/*
2+
* Copyright 2015-2020 OpenCB
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.opencb.commons.datastore.mongodb.test;
18+
19+
import com.mongodb.client.MongoClient;
20+
import com.mongodb.client.MongoClients;
21+
import com.mongodb.client.MongoDatabase;
22+
import de.flapdoodle.embed.mongo.commands.MongodArguments;
23+
import de.flapdoodle.embed.mongo.config.Net;
24+
import de.flapdoodle.embed.mongo.config.Storage;
25+
import de.flapdoodle.embed.mongo.distribution.Version;
26+
import de.flapdoodle.embed.mongo.transitions.Mongod;
27+
import de.flapdoodle.embed.mongo.transitions.RunningMongodProcess;
28+
import de.flapdoodle.reverse.TransitionWalker;
29+
import de.flapdoodle.reverse.transitions.Start;
30+
import org.bson.Document;
31+
import org.slf4j.Logger;
32+
import org.slf4j.LoggerFactory;
33+
34+
import java.io.IOException;
35+
import java.io.PrintStream;
36+
import java.net.ServerSocket;
37+
import java.util.Collections;
38+
39+
/**
40+
* Manages a process-singleton embedded MongoDB instance for testing purposes, backed by flapdoodle.
41+
* Provides faster test execution and better isolation than relying on an external MongoDB service.
42+
*
43+
* <p>Configuration via system properties:</p>
44+
* <ul>
45+
* <li>opencb.test.embeddedMongo - Enable/disable embedded MongoDB (default: true)</li>
46+
* <li>opencb.test.mongodb.version - MongoDB version to use (default: 7.0)</li>
47+
* <li>opencb.test.mongo.verbose - Enable verbose mongod stdout/stderr passthrough (default: false)</li>
48+
* </ul>
49+
*
50+
* <p>The embedded mongod is started lazily on the first {@link #start()} call, reused across the
51+
* entire JVM, and stopped via a shutdown hook. A single-member replica set {@code rs0} is
52+
* initialised so transactions are available.</p>
53+
*/
54+
public final class EmbeddedMongoDBManager {
55+
private static final Logger LOGGER = LoggerFactory.getLogger(EmbeddedMongoDBManager.class);
56+
private static final String DEFAULT_MONGODB_VERSION = "7.0";
57+
58+
private static EmbeddedMongoDBManager instance;
59+
private TransitionWalker.ReachedState<RunningMongodProcess> runningMongod;
60+
private int port;
61+
private final boolean enabled;
62+
private final String mongoVersion;
63+
private final boolean verbose;
64+
65+
private EmbeddedMongoDBManager() {
66+
this.enabled = Boolean.parseBoolean(System.getProperty("opencb.test.embeddedMongo", "true"));
67+
this.mongoVersion = System.getProperty("opencb.test.mongodb.version", DEFAULT_MONGODB_VERSION);
68+
this.verbose = Boolean.parseBoolean(System.getProperty("opencb.test.mongo.verbose", "false"));
69+
}
70+
71+
public static synchronized EmbeddedMongoDBManager getInstance() {
72+
if (instance == null) {
73+
instance = new EmbeddedMongoDBManager();
74+
}
75+
return instance;
76+
}
77+
78+
private Version.Main getMongoVersion() {
79+
switch (mongoVersion) {
80+
case "3.6":
81+
return Version.Main.V3_6;
82+
case "4.0":
83+
return Version.Main.V4_0;
84+
case "4.2":
85+
return Version.Main.V4_2;
86+
case "4.4":
87+
return Version.Main.V4_4;
88+
case "5.0":
89+
return Version.Main.V5_0;
90+
case "6.0":
91+
return Version.Main.V6_0;
92+
case "7.0":
93+
return Version.Main.V7_0;
94+
case "8.0":
95+
return Version.Main.V8_0;
96+
case "8.1":
97+
return Version.Main.V8_1;
98+
default:
99+
throw new IllegalArgumentException("Unsupported MongoDB version: " + mongoVersion);
100+
}
101+
}
102+
103+
public synchronized void start() throws IOException {
104+
if (!enabled) {
105+
LOGGER.info("Embedded MongoDB is disabled. Using external MongoDB instance.");
106+
return;
107+
}
108+
109+
if (runningMongod != null) {
110+
LOGGER.debug("Embedded MongoDB is already running on port {}", port);
111+
return;
112+
}
113+
114+
try {
115+
LOGGER.info("Starting embedded MongoDB {} with replica set support", mongoVersion);
116+
117+
port = findAvailablePort();
118+
LOGGER.info("Found available port: {}", port);
119+
120+
if (!verbose) {
121+
System.setOut(new FilteringPrintStream(System.out));
122+
System.setErr(new FilteringPrintStream(System.err));
123+
LOGGER.info("MongoDB output filtering enabled (use -Dopencb.test.mongo.verbose=true to see all logs)");
124+
}
125+
126+
runningMongod = Mongod.instance()
127+
.withNet(Start.to(Net.class).initializedWith(Net.defaults().withPort(port)))
128+
.withMongodArguments(Start.to(MongodArguments.class)
129+
.initializedWith(MongodArguments.defaults()
130+
.withReplication(Storage.of("rs0", 10))))
131+
.start(getMongoVersion());
132+
133+
LOGGER.info("Embedded MongoDB {} started on port {}, initializing replica set...", mongoVersion, port);
134+
135+
Thread.sleep(500);
136+
137+
initializeReplicaSet();
138+
139+
LOGGER.info("Embedded MongoDB {} with replica set 'rs0' ready on port {}", mongoVersion, port);
140+
141+
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
142+
if (verbose) {
143+
LOGGER.info("Shutting down embedded MongoDB via shutdown hook");
144+
}
145+
stop();
146+
}));
147+
148+
} catch (Exception e) {
149+
LOGGER.error("Failed to start embedded MongoDB version {}", mongoVersion, e);
150+
throw new IOException("Failed to start embedded MongoDB", e);
151+
}
152+
}
153+
154+
private void initializeReplicaSet() {
155+
MongoClient mongoClient = null;
156+
try {
157+
String connectionString = String.format("mongodb://localhost:%d", port);
158+
mongoClient = MongoClients.create(connectionString);
159+
160+
MongoDatabase adminDb = mongoClient.getDatabase("admin");
161+
162+
Document config = new Document("_id", "rs0")
163+
.append("members", Collections.singletonList(
164+
new Document("_id", 0)
165+
.append("host", "localhost:" + port)
166+
));
167+
168+
adminDb.runCommand(new Document("replSetInitiate", config));
169+
170+
LOGGER.debug("Replica set initiation command sent");
171+
172+
long timeout = System.currentTimeMillis() + 30000;
173+
while (System.currentTimeMillis() < timeout) {
174+
try {
175+
Document result = adminDb.runCommand(new Document("replSetGetStatus", 1));
176+
Integer myState = result.getInteger("myState");
177+
if (myState != null && myState == 1) {
178+
LOGGER.info("Replica set initialized successfully and is PRIMARY");
179+
return;
180+
}
181+
LOGGER.debug("Replica set state: {}, waiting for PRIMARY...", myState);
182+
} catch (Exception e) {
183+
LOGGER.trace("Waiting for replica set to be ready: {}", e.getMessage());
184+
}
185+
Thread.sleep(100);
186+
}
187+
188+
throw new RuntimeException("Replica set initialization timed out after 30 seconds");
189+
190+
} catch (InterruptedException e) {
191+
Thread.currentThread().interrupt();
192+
throw new RuntimeException("Replica set initialization was interrupted", e);
193+
} catch (Exception e) {
194+
LOGGER.error("Failed to initialize replica set", e);
195+
throw new RuntimeException("Could not initialize replica set", e);
196+
} finally {
197+
if (mongoClient != null) {
198+
try {
199+
mongoClient.close();
200+
} catch (Exception e) {
201+
LOGGER.warn("Error closing MongoDB client", e);
202+
}
203+
}
204+
}
205+
}
206+
207+
public synchronized void stop() {
208+
if (!enabled) {
209+
return;
210+
}
211+
212+
if (runningMongod != null) {
213+
LOGGER.info("Stopping embedded MongoDB on port {}", port);
214+
try {
215+
runningMongod.close();
216+
} catch (Exception e) {
217+
LOGGER.warn("Error stopping embedded MongoDB", e);
218+
} finally {
219+
runningMongod = null;
220+
}
221+
}
222+
}
223+
224+
public int getPort() {
225+
return port;
226+
}
227+
228+
public String getConnectionString() {
229+
if (!enabled) {
230+
return "localhost:27017";
231+
}
232+
return "localhost:" + port;
233+
}
234+
235+
public boolean isEnabled() {
236+
return enabled;
237+
}
238+
239+
public boolean isRunning() {
240+
return enabled && runningMongod != null;
241+
}
242+
243+
private int findAvailablePort() throws IOException {
244+
try (ServerSocket socket = new ServerSocket(0)) {
245+
socket.setReuseAddress(true);
246+
return socket.getLocalPort();
247+
} catch (IOException e) {
248+
throw new IOException("Failed to find an available port", e);
249+
}
250+
}
251+
252+
private static class FilteringPrintStream extends PrintStream {
253+
private final PrintStream original;
254+
private final StringBuilder lineBuffer = new StringBuilder();
255+
256+
FilteringPrintStream(PrintStream original) {
257+
super(original);
258+
this.original = original;
259+
}
260+
261+
@Override
262+
public void write(int b) {
263+
if (b == '\n') {
264+
String line = lineBuffer.toString();
265+
boolean isMongodLog = line.contains("{\"t\":{\"$date\":")
266+
|| line.startsWith("[mongod output]")
267+
|| line.startsWith("[mongod error]");
268+
if (!isMongodLog) {
269+
original.print(lineBuffer.toString());
270+
original.write(b);
271+
original.flush();
272+
}
273+
lineBuffer.setLength(0);
274+
} else if (b != '\r') {
275+
lineBuffer.append((char) b);
276+
}
277+
}
278+
279+
@Override
280+
public void write(byte[] buf, int off, int len) {
281+
for (int i = 0; i < len; i++) {
282+
write(buf[off + i]);
283+
}
284+
}
285+
286+
@Override
287+
public void flush() {
288+
if (lineBuffer.length() > 0) {
289+
String line = lineBuffer.toString();
290+
boolean isMongodLog = line.contains("{\"t\":{\"$date\":")
291+
|| line.startsWith("[mongod output]")
292+
|| line.startsWith("[mongod error]");
293+
if (!isMongodLog) {
294+
original.print(lineBuffer.toString());
295+
}
296+
lineBuffer.setLength(0);
297+
}
298+
original.flush();
299+
}
300+
}
301+
}

0 commit comments

Comments
 (0)