Skip to content

Commit 4cb5061

Browse files
committed
test: added test for issue #3887
1 parent b871b34 commit 4cb5061

3 files changed

Lines changed: 183 additions & 0 deletions

File tree

ha-raft/src/test/java/com/arcadedb/server/ha/raft/RaftImportDatabase3NodesIT.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@
4444
* via the INSTALL_DATABASE_ENTRY Raft log entry, and the importer's subsequent
4545
* transactions replicate to every peer as normal TX_ENTRY stream. At the end, every
4646
* peer should have the same type set and matching record counts.
47+
*
48+
* @author Luca Garulli ([email protected])
4749
*/
4850
class RaftImportDatabase3NodesIT extends BaseRaftHATest {
4951

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
/*
2+
* Copyright 2021-present Arcade Data Ltd ([email protected])
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+
* SPDX-FileCopyrightText: 2021-present Arcade Data Ltd ([email protected])
17+
* SPDX-License-Identifier: Apache-2.0
18+
*/
19+
package com.arcadedb.server.ha.raft;
20+
21+
import com.arcadedb.ContextConfiguration;
22+
import com.arcadedb.GlobalConfiguration;
23+
import com.arcadedb.database.Database;
24+
import com.arcadedb.schema.DocumentType;
25+
import com.arcadedb.serializer.json.JSONObject;
26+
import com.arcadedb.server.BaseGraphServerTest;
27+
import com.arcadedb.utility.FileUtils;
28+
import org.awaitility.Awaitility;
29+
import org.junit.jupiter.api.AfterEach;
30+
import org.junit.jupiter.api.Test;
31+
32+
import java.io.File;
33+
import java.net.HttpURLConnection;
34+
import java.net.URI;
35+
import java.util.Base64;
36+
import java.util.HashMap;
37+
import java.util.Map;
38+
import java.util.concurrent.TimeUnit;
39+
40+
import static org.assertj.core.api.Assertions.assertThat;
41+
42+
/**
43+
* Regression test for issue #3887 "Import Database issues with HA".
44+
* <p>
45+
* Imports an OrientDB-format export (Person + Friend, ~10k edges) into an empty database
46+
* on the Raft leader. The user report was that a 10k-record Whisky import against a 3-node
47+
* HA cluster timed out at the Raft quorum after minutes, while the same import on a
48+
* single node completed in seconds.
49+
* <p>
50+
* This test exercises the full OrientDB importer path through Raft: schema creation,
51+
* batch record inserts, edge creation, and index creation. Expected completion well under
52+
* one minute on a 3-node in-process cluster.
53+
*
54+
* @author Luca Garulli ([email protected])
55+
*/
56+
class RaftImportOrientDB3NodesIT extends BaseRaftHATest {
57+
58+
private static final String DB_NAME = "RaftImportOrientDBTest";
59+
// Issue #3887 reported a 10k-record import taking 10+ minutes on HA while a single-node
60+
// import completed in 2 seconds. A 60-second budget on a 3-node in-process cluster is
61+
// well above the ~2 second steady-state baseline and still catches any severe regression.
62+
private static final long IMPORT_TIMEOUT_SECS = 60;
63+
64+
RaftImportOrientDB3NodesIT() {
65+
FileUtils.deleteRecursively(new File("./target/config"));
66+
FileUtils.deleteRecursively(new File("./target/databases"));
67+
for (int i = 0; i < 3; i++)
68+
FileUtils.deleteRecursively(new File("./target/databases" + i + "/" + DB_NAME));
69+
GlobalConfiguration.SERVER_DATABASE_DIRECTORY.setValue("./target/databases");
70+
GlobalConfiguration.SERVER_ROOT_PATH.setValue("./target");
71+
}
72+
73+
@AfterEach
74+
@Override
75+
public void endTest() {
76+
super.endTest();
77+
FileUtils.deleteRecursively(new File("./target/config"));
78+
FileUtils.deleteRecursively(new File("./target/databases"));
79+
for (int i = 0; i < 3; i++)
80+
FileUtils.deleteRecursively(new File("./target/databases" + i + "/" + DB_NAME));
81+
}
82+
83+
@Override
84+
protected int getServerCount() {
85+
return 3;
86+
}
87+
88+
@Override
89+
protected boolean isCreateDatabases() {
90+
return false;
91+
}
92+
93+
@Override
94+
protected void onServerConfiguration(final ContextConfiguration config) {
95+
super.onServerConfiguration(config);
96+
config.setValue(GlobalConfiguration.HA_QUORUM, "majority");
97+
}
98+
99+
@Override
100+
protected void checkDatabasesAreIdentical() {
101+
// Page-level comparison across nodes is not meaningful for the imported database because
102+
// bucket file IDs are assigned per node. The test asserts logical equality (type set + counts).
103+
}
104+
105+
@Test
106+
void importOrientDBPropagatesAcrossCluster() throws Exception {
107+
final File fixture = new File("src/test/resources/orientdb-export-small.gz");
108+
assertThat(fixture.exists()).as("orientdb fixture should be present at %s", fixture.getAbsolutePath()).isTrue();
109+
final String fixtureUrl = "file://" + fixture.getAbsolutePath();
110+
111+
final int leader = findLeaderIndex();
112+
assertThat(leader).isGreaterThanOrEqualTo(0);
113+
114+
final long start = System.currentTimeMillis();
115+
postServerCommand(leader, "import database " + DB_NAME + " " + fixtureUrl);
116+
final long elapsedMs = System.currentTimeMillis() - start;
117+
118+
assertThat(elapsedMs)
119+
.as("IMPORT DATABASE should complete in under %d seconds on a 3-node Raft cluster (actual: %d ms)",
120+
IMPORT_TIMEOUT_SECS, elapsedMs)
121+
.isLessThan(IMPORT_TIMEOUT_SECS * 1000);
122+
123+
Awaitility.await().atMost(60, TimeUnit.SECONDS).pollInterval(500, TimeUnit.MILLISECONDS)
124+
.until(() -> {
125+
for (int i = 0; i < getServerCount(); i++)
126+
if (!getServer(i).existsDatabase(DB_NAME))
127+
return false;
128+
return true;
129+
});
130+
131+
for (int i = 0; i < getServerCount(); i++)
132+
waitForReplicationIsCompleted(i);
133+
134+
final Database leaderDb = getServer(leader).getDatabase(DB_NAME);
135+
final Map<String, Long> leaderCounts = new HashMap<>();
136+
for (final DocumentType type : leaderDb.getSchema().getTypes())
137+
leaderCounts.put(type.getName(), leaderDb.countType(type.getName(), false));
138+
139+
assertThat(leaderCounts.get("Person"))
140+
.as("Person vertex count on the leader")
141+
.isEqualTo(500L);
142+
assertThat(leaderCounts.get("Friend"))
143+
.as("Friend edge count on the leader")
144+
.isEqualTo(10_000L);
145+
146+
for (int i = 0; i < getServerCount(); i++) {
147+
if (i == leader)
148+
continue;
149+
final Database peerDb = getServer(i).getDatabase(DB_NAME);
150+
for (final Map.Entry<String, Long> entry : leaderCounts.entrySet()) {
151+
final String typeName = entry.getKey();
152+
final long expected = entry.getValue();
153+
assertThat(peerDb.getSchema().existsType(typeName))
154+
.as("server %d should have type '%s'", i, typeName)
155+
.isTrue();
156+
final long actual = peerDb.countType(typeName, false);
157+
assertThat(actual)
158+
.as("server %d count of type '%s'", i, typeName)
159+
.isEqualTo(expected);
160+
}
161+
}
162+
}
163+
164+
private void postServerCommand(final int serverIndex, final String command) throws Exception {
165+
final HttpURLConnection connection = (HttpURLConnection) new URI(
166+
"http://127.0.0.1:248" + serverIndex + "/api/v1/server").toURL().openConnection();
167+
connection.setRequestMethod("POST");
168+
connection.setRequestProperty("Authorization",
169+
"Basic " + Base64.getEncoder().encodeToString(
170+
("root:" + BaseGraphServerTest.DEFAULT_PASSWORD_FOR_TESTS).getBytes()));
171+
try {
172+
formatPayload(connection, new JSONObject().put("command", command));
173+
connection.connect();
174+
assertThat(connection.getResponseCode())
175+
.as("server command '%s' on server %d", command, serverIndex)
176+
.isEqualTo(200);
177+
} finally {
178+
connection.disconnect();
179+
}
180+
}
181+
}
149 KB
Binary file not shown.

0 commit comments

Comments
 (0)