Skip to content

Commit 6563346

Browse files
committed
2 parents 3997a8a + b7e103b commit 6563346

10 files changed

Lines changed: 332 additions & 62 deletions

File tree

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,9 @@
1-
name: E2E HA Tests
1+
name: HA Resilence Tests
22

33
on:
44
workflow_dispatch:
55
schedule:
66
- cron: "0 0 * * *" # Runs daily at midnight
7-
pull_request:
8-
branches:
9-
- main
10-
117

128
jobs:
139
setup:

.github/workflows/load-tests.yml

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
name: Load Tests
2+
3+
on:
4+
workflow_dispatch:
5+
schedule:
6+
- cron: "0 0 * * *" # Runs daily at midnight
7+
8+
jobs:
9+
setup:
10+
runs-on: ubuntu-latest
11+
permissions:
12+
contents: write
13+
packages: write
14+
attestations: write
15+
id-token: write
16+
17+
steps:
18+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
19+
- name: Ensure SHA pinned actions
20+
uses: zgosalvez/github-actions-ensure-sha-pinned-actions@471d5ace1f08e3c4df1c4c2f7e6341aa75da434a # v5.0.3
21+
- name: Run pre-commit
22+
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
23+
with:
24+
python-version: "3.13.0"
25+
cache: "pip"
26+
- uses: pre-commit/action@2c7b3805fd2a0fd8c1884dcaebf91fc102a13ecd # v3.0.1
27+
28+
- name: Set up JDK 21
29+
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
30+
with:
31+
distribution: "temurin"
32+
java-version: 21
33+
34+
- name: Cache local Maven repository
35+
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
36+
with:
37+
path: ~/.m2/repository
38+
key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
39+
restore-keys: |
40+
${{ runner.os }}-maven-
41+
42+
- name: Set up QEMU
43+
uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
44+
45+
- name: Set up Docker Buildx
46+
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
47+
48+
- name: Build and package with Maven Docker profile
49+
run: ./mvnw clean install -Pdocker -DskipTests --batch-mode --errors --show-version
50+
env:
51+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
52+
53+
- name: Load Tests
54+
run: ./mvnw verify -Pintegration --batch-mode --errors --fail-never --show-version -pl load-tests
55+
env:
56+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
57+
58+
- name: Tests Reporter
59+
uses: dorny/test-reporter@a43b3a5f7366b97d083190328d2c652e1a8b6aa2 # v3.0.0
60+
if: success() || failure()
61+
with:
62+
name: IT Tests Report
63+
path: "**/failsafe-reports/TEST*.xml"
64+
list-tests: "failed"
65+
list-suites: "failed"
66+
reporter: java-junit

.github/workflows/mvn-test.yml

Lines changed: 0 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -325,50 +325,6 @@ jobs:
325325
list-tests: "failed"
326326
reporter: java-junit
327327

328-
java-load-tests:
329-
runs-on: ubuntu-latest
330-
needs: build-and-package
331-
steps:
332-
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
333-
334-
- name: Set up JDK 21
335-
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
336-
with:
337-
distribution: "temurin"
338-
java-version: 21
339-
cache: "maven"
340-
341-
- name: Restore Maven artifacts
342-
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
343-
with:
344-
path: ~/.m2/repository
345-
key: maven-repo-${{ github.run_id }}-${{ github.run_attempt }}
346-
347-
- name: Restore Docker image
348-
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
349-
with:
350-
path: /tmp/arcadedb-image.tar
351-
key: docker-image-${{ github.run_id }}-${{ github.run_attempt }}
352-
353-
- name: Load Docker image
354-
run: docker load < /tmp/arcadedb-image.tar
355-
356-
- name: E2E Perf Tests
357-
run: ./mvnw verify -Pintegration -pl load-tests
358-
env:
359-
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
360-
ARCADEDB_DOCKER_IMAGE: ${{ needs.build-and-package.outputs.image-tag }}
361-
362-
- name: E2E Perf Tests Reporter
363-
uses: dorny/test-reporter@a43b3a5f7366b97d083190328d2c652e1a8b6aa2 # v3.0.0
364-
if: success() || failure()
365-
with:
366-
name: Java Load Tests Report
367-
path: "load-tests/target/failsafe-reports/TEST*.xml"
368-
list-suites: "failed"
369-
list-tests: "failed"
370-
reporter: java-junit
371-
372328
js-e2e-tests:
373329
runs-on: ubuntu-latest
374330
needs: build-and-package
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
/*
2+
* Copyright 2021-present Arcade Data Ltd (info@arcadedata.com)
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 (info@arcadedata.com)
17+
* SPDX-License-Identifier: Apache-2.0
18+
*/
19+
package com.arcadedb.test.load;
20+
21+
import com.arcadedb.test.support.ContainersTestTemplate;
22+
import com.arcadedb.test.support.DatabaseWrapper;
23+
import com.arcadedb.test.support.ServerWrapper;
24+
import io.micrometer.core.instrument.Metrics;
25+
import org.junit.jupiter.api.AfterEach;
26+
import org.junit.jupiter.api.DisplayName;
27+
import org.junit.jupiter.params.ParameterizedTest;
28+
import org.junit.jupiter.params.provider.EnumSource;
29+
30+
import java.time.Duration;
31+
import java.time.LocalDateTime;
32+
import java.time.format.DateTimeFormatter;
33+
import java.util.List;
34+
import java.util.concurrent.ExecutorService;
35+
import java.util.concurrent.Executors;
36+
import java.util.concurrent.TimeUnit;
37+
38+
class ThreeINodesLoadtTestIT extends ContainersTestTemplate {
39+
40+
private static final String SERVER_LIST = "arcadedb-0:2434:2480,arcadedb-1:2434:2480,arcadedb-2:2434:2480";
41+
42+
@AfterEach
43+
@Override
44+
public void tearDown() {
45+
// Skip compareAllDatabases(): with non-persistent containers, database files are not
46+
// on the host after stop. The test body already verifies convergence via Awaitility.
47+
super.tearDown();
48+
}
49+
50+
@ParameterizedTest(name = "Three-node Raft HA Load test with {0} protocol")
51+
@EnumSource(DatabaseWrapper.Protocol.class)
52+
@DisplayName("Three-node Raft HA: replication across all nodes with consistency check")
53+
void threeNodeReplication(DatabaseWrapper.Protocol protocol) {
54+
createArcadeContainer("arcadedb-0", SERVER_LIST, "majority", network);
55+
createArcadeContainer("arcadedb-1", SERVER_LIST, "majority", network);
56+
createArcadeContainer("arcadedb-2", SERVER_LIST, "majority", network);
57+
58+
logger.info("Starting all containers");
59+
final List<ServerWrapper> servers = startCluster();
60+
61+
final DatabaseWrapper db1 = new DatabaseWrapper(servers.get(0), idSupplier, wordSupplier);
62+
final DatabaseWrapper db2 = new DatabaseWrapper(servers.get(1), idSupplier, wordSupplier);
63+
final DatabaseWrapper db3 = new DatabaseWrapper(servers.get(2), idSupplier, wordSupplier);
64+
65+
logger.info("Creating database and schema");
66+
db1.createDatabase();
67+
db1.createSchema();
68+
69+
logger.info("Checking schema replicated to all nodes");
70+
db1.checkSchema();
71+
db2.checkSchema();
72+
db3.checkSchema();
73+
74+
final int numOfThreads = 3; //number of threads to use to insert users and photos
75+
final int numOfUsers = 1000; // Each thread will create 200000 users
76+
final int numOfPhotos = 10; // Each user will have 5 photos
77+
final int numOfFriendship = 0; // Each thread will create 100000 friendships
78+
final int numOfLike = 0; // Each thread will create 100000 likes
79+
80+
int expectedUsersCount = numOfUsers * numOfThreads;
81+
int expectedPhotoCount = expectedUsersCount * numOfPhotos;
82+
int expectedFriendshipCount = numOfFriendship;
83+
int expectedLikeCount = numOfLike;
84+
LocalDateTime startedAt = LocalDateTime.now();
85+
logger.info("Starting load test on protocol {}", protocol.name());
86+
logger.info("Creating {} users using {} threads", expectedUsersCount, numOfThreads);
87+
logger.info("Expected users: {} - photos: {} - friendships: {} - likes: {}", expectedUsersCount, expectedPhotoCount,
88+
expectedFriendshipCount, expectedLikeCount);
89+
logger.info("Starting at {}", DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(startedAt));
90+
91+
ExecutorService executor = Executors.newFixedThreadPool(10);
92+
for (int i = 0; i < numOfThreads; i++) {
93+
// Each thread will create users and photos
94+
executor.submit(() -> {
95+
DatabaseWrapper db = new DatabaseWrapper(servers.getFirst(), idSupplier, wordSupplier, protocol);
96+
db.addUserAndPhotos(numOfUsers, numOfPhotos);
97+
db.close();
98+
});
99+
}
100+
// Each thread will create friendships
101+
executor.submit(() -> {
102+
DatabaseWrapper db = new DatabaseWrapper(servers.getFirst(), idSupplier, wordSupplier, protocol);
103+
db.createFriendships(numOfFriendship);
104+
db.close();
105+
});
106+
// Each thread will create friendships
107+
executor.submit(() -> {
108+
DatabaseWrapper db = new DatabaseWrapper(servers.getFirst(), idSupplier, wordSupplier, protocol);
109+
db.createLike(numOfLike);
110+
db.close();
111+
});
112+
113+
executor.shutdown();
114+
115+
while (!executor.isTerminated()) {
116+
try {
117+
final long users1 = db1.countUsers();
118+
final long photos1 = db1.countPhotos();
119+
final long users2 = db2.countUsers();
120+
final long photos2 = db2.countPhotos();
121+
final long users3 = db3.countUsers();
122+
final long photos3 = db3.countPhotos();
123+
logger.info("Users: {} / {} / {} | Photos: {} / {} / {}", users1, users2, users3, photos1, photos2, photos3);
124+
125+
} catch (Exception e) {
126+
logger.error(e.getMessage(), e);
127+
}
128+
try {
129+
// Wait for 2 seconds before checking again
130+
TimeUnit.SECONDS.sleep(5);
131+
} catch (InterruptedException e) {
132+
Thread.currentThread().interrupt();
133+
}
134+
}
135+
LocalDateTime finishedAt = LocalDateTime.now();
136+
logger.info("Finishing at {}", DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(finishedAt));
137+
logger.info("Total time: {} minutes", Duration.between(startedAt, finishedAt).toMinutes());
138+
139+
Metrics.globalRegistry.getMeters().forEach(meter -> {
140+
logger.info("Meter: {} - {}", meter.getId().getName(), meter.measure());
141+
});
142+
143+
db1.assertThatUserCountIs(expectedUsersCount);
144+
db1.assertThatPhotoCountIs(expectedPhotoCount);
145+
db1.assertThatFriendshipCountIs(expectedFriendshipCount);
146+
db1.assertThatLikesCountIs(expectedLikeCount);
147+
148+
db2.assertThatUserCountIs(expectedUsersCount);
149+
db2.assertThatPhotoCountIs(expectedPhotoCount);
150+
db2.assertThatFriendshipCountIs(expectedFriendshipCount);
151+
db2.assertThatLikesCountIs(expectedLikeCount);
152+
153+
db3.assertThatUserCountIs(expectedUsersCount);
154+
db3.assertThatPhotoCountIs(expectedPhotoCount);
155+
db3.assertThatFriendshipCountIs(expectedFriendshipCount);
156+
db3.assertThatLikesCountIs(expectedLikeCount);
157+
158+
db1.close();
159+
db2.close();
160+
db3.close();
161+
}
162+
}

server/src/main/java/com/arcadedb/server/mcp/MCPHttpHandler.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,14 @@ private ExecutionResponse handleInitialize(final Object id) {
125125
capabilities.put("tools", new JSONObject().put("listChanged", false));
126126
result.put("capabilities", capabilities);
127127

128+
result.put("instructions",
129+
"You are connected to an ArcadeDB multi-model database server. Follow these rules:\n"
130+
+ "1. ALWAYS call list_databases first when you do not know the target database name. Never guess it.\n"
131+
+ "2. Prefer Cypher (language: 'cypher') for graph queries unless SQL is explicitly requested.\n"
132+
+ "3. Use the 'query' tool for read-only operations (SELECT, MATCH, RETURN) and 'execute_command' for writes (CREATE, INSERT, UPDATE, DELETE, MERGE).\n"
133+
+ "4. Call get_schema before writing queries against an unfamiliar database to understand its types and properties.\n"
134+
+ "5. If a query returns no results, verify the type/property names with get_schema before concluding the data does not exist.");
135+
128136
return jsonRpcResult(id, result);
129137
}
130138

server/src/main/java/com/arcadedb/server/mcp/tools/ExecuteCommandTool.java

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -72,10 +72,7 @@ public static JSONObject execute(final ArcadeDBServer server, final ServerSecuri
7272
final String command = args.getString("command");
7373
final int limit = args.getInt("limit", DEFAULT_LIMIT);
7474

75-
if (!user.canAccessToDatabase(databaseName))
76-
throw new SecurityException("User '" + user.getName() + "' is not authorized to access database '" + databaseName + "'");
77-
78-
final Database database = server.getDatabase(databaseName);
75+
final Database database = MCPToolUtils.resolveDatabase(server, user, databaseName);
7976

8077
// Analyze once for both permission checking and execution (avoids double parsing)
8178
final QueryEngine engine = database.getQueryEngine(language);

server/src/main/java/com/arcadedb/server/mcp/tools/GetSchemaTool.java

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -57,10 +57,7 @@ public static JSONObject execute(final ArcadeDBServer server, final ServerSecuri
5757

5858
final String databaseName = args.getString("database");
5959

60-
if (!user.canAccessToDatabase(databaseName))
61-
throw new SecurityException("User '" + user.getName() + "' is not authorized to access database '" + databaseName + "'");
62-
63-
final Database database = server.getDatabase(databaseName);
60+
final Database database = MCPToolUtils.resolveDatabase(server, user, databaseName);
6461

6562
final Schema schema = database.getSchema();
6663
final JSONArray types = new JSONArray();
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/*
2+
* Copyright © 2021-present Arcade Data Ltd (info@arcadedata.com)
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 (info@arcadedata.com)
17+
* SPDX-License-Identifier: Apache-2.0
18+
*/
19+
package com.arcadedb.server.mcp.tools;
20+
21+
import java.util.Set;
22+
import java.util.TreeSet;
23+
24+
import com.arcadedb.server.ArcadeDBServer;
25+
import com.arcadedb.server.ServerDatabase;
26+
import com.arcadedb.server.security.ServerSecurityUser;
27+
28+
public class MCPToolUtils {
29+
30+
private MCPToolUtils() {
31+
}
32+
33+
/**
34+
* Resolves a database by name, throwing an {@link IllegalArgumentException} with the list of databases
35+
* accessible to the user when the requested database does not exist — so the LLM can self-correct
36+
* without a separate list_databases round-trip.
37+
*/
38+
public static ServerDatabase resolveDatabase(final ArcadeDBServer server, final ServerSecurityUser user,
39+
final String databaseName) {
40+
if (!server.existsDatabase(databaseName)) {
41+
final Set<String> installed = new TreeSet<>(server.getDatabaseNames());
42+
installed.removeIf(db -> !user.canAccessToDatabase(db));
43+
throw new IllegalArgumentException(
44+
"Database '" + databaseName + "' does not exist. Available databases: " + installed
45+
+ ". Use one of these names or call list_databases to refresh the list.");
46+
}
47+
if (!user.canAccessToDatabase(databaseName))
48+
throw new SecurityException("User '" + user.getName() + "' is not authorized to access database '" + databaseName + "'");
49+
return server.getDatabase(databaseName);
50+
}
51+
}

0 commit comments

Comments
 (0)