Skip to content

Commit b44ff5e

Browse files
authored
Merge pull request #487 from weaviate/v6-cluster-nodes
v6: Cluster/Nodes API
2 parents 69d74a6 + 8196d52 commit b44ff5e

23 files changed

Lines changed: 590 additions & 22 deletions

src/it/java/io/weaviate/containers/Weaviate.java

Lines changed: 126 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,19 @@
11
package io.weaviate.containers;
22

33
import java.io.IOException;
4+
import java.time.Duration;
5+
import java.util.ArrayList;
46
import java.util.Arrays;
57
import java.util.HashMap;
68
import java.util.HashSet;
9+
import java.util.List;
710
import java.util.Map;
811
import java.util.Set;
912
import java.util.function.Function;
1013

14+
import org.testcontainers.containers.Network;
15+
import org.testcontainers.containers.wait.strategy.Wait;
16+
import org.testcontainers.lifecycle.Startable;
1117
import org.testcontainers.weaviate.WeaviateContainer;
1218

1319
import io.weaviate.client6.v1.api.Config;
@@ -20,6 +26,22 @@ public class Weaviate extends WeaviateContainer {
2026
public static String OIDC_ISSUER = "https://auth.wcs.api.weaviate.io/auth/realms/SeMI";
2127

2228
private volatile SharedClient clientInstance;
29+
private final String containerName;
30+
31+
/**
32+
* By default, testcontainer's name is only available after calling
33+
* {@link #start}.
34+
* We need to know each container's name in advance to run a cluster
35+
* of several nodes, in which case we alse set the name manually.
36+
*
37+
* @see Builder#build()
38+
*/
39+
@Override
40+
public String getContainerName() {
41+
return containerName != null
42+
? containerName
43+
: super.getContainerName();
44+
}
2345

2446
/**
2547
* Create a new instance of WeaviateClient connected to this container if none
@@ -85,17 +107,22 @@ public static Weaviate.Builder custom() {
85107
}
86108

87109
public static class Builder {
88-
private String versionTag;
110+
private String versionTag = VERSION;
111+
private String containerName = "weaviate";
89112
private Set<String> enableModules = new HashSet<>();
90113
private Set<String> adminUsers = new HashSet<>();
91114
private Set<String> viewerUsers = new HashSet<>();
92115
private Map<String, String> environment = new HashMap<>();
93116

94117
public Builder() {
95-
this.versionTag = VERSION;
96118
enableAutoSchema(false);
97119
}
98120

121+
public Builder withContainerName(String containerName) {
122+
this.containerName = containerName;
123+
return this;
124+
}
125+
99126
public Builder withVersion(String version) {
100127
this.versionTag = version;
101128
return this;
@@ -138,6 +165,7 @@ public Builder withFilesystemBackup(String fsPath) {
138165
environment.put("BACKUP_FILESYSTEM_PATH", fsPath);
139166
return this;
140167
}
168+
141169
public Builder withAdminUsers(String... admins) {
142170
adminUsers.addAll(Arrays.asList(admins));
143171
return this;
@@ -195,7 +223,7 @@ public Builder withOIDC(String clientId, String issuer, String usernameClaim, St
195223
}
196224

197225
public Weaviate build() {
198-
var c = new Weaviate(DOCKER_IMAGE + ":" + versionTag);
226+
var c = new Weaviate(containerName, DOCKER_IMAGE + ":" + versionTag);
199227

200228
if (!enableModules.isEmpty()) {
201229
c.withEnv("ENABLE_API_BASED_MODULES", Boolean.TRUE.toString());
@@ -217,13 +245,18 @@ public Weaviate build() {
217245
}
218246

219247
environment.forEach((name, value) -> c.withEnv(name, value));
220-
c.withCreateContainerCmdModifier(cmd -> cmd.withHostName("weaviate"));
248+
c.withCreateContainerCmdModifier(cmd -> cmd.withHostName(containerName));
221249
return c;
222250
}
223251
}
224252

225-
private Weaviate(String dockerImageName) {
253+
private Weaviate() {
254+
this("weaviate", DOCKER_IMAGE + ":" + VERSION);
255+
}
256+
257+
private Weaviate(String containerName, String dockerImageName) {
226258
super(dockerImageName);
259+
this.containerName = containerName;
227260
}
228261

229262
@Override
@@ -264,4 +297,92 @@ private void close(Weaviate caller) throws Exception {
264297
public void close() throws IOException {
265298
}
266299
}
300+
301+
public static Weaviate cluster(int replicas) {
302+
List<Weaviate> nodes = new ArrayList<>();
303+
for (var i = 0; i < replicas; i++) {
304+
nodes.add(Weaviate.custom()
305+
.withContainerName("weaviate-" + i)
306+
.build());
307+
}
308+
return new Cluster(nodes);
309+
}
310+
311+
public static class Cluster extends Weaviate {
312+
/** Leader and followers combined. */
313+
private final List<Weaviate> nodes;
314+
315+
private final Weaviate leader;
316+
private final List<Weaviate> followers;
317+
318+
private Cluster(List<Weaviate> nodes) {
319+
assert nodes.size() > 1 : "cluster must have 1+ nodes";
320+
321+
this.nodes = List.copyOf(nodes);
322+
this.leader = nodes.remove(0);
323+
this.followers = List.copyOf(nodes);
324+
325+
for (var follower : followers) {
326+
follower.dependsOn(leader);
327+
}
328+
setNetwork(Network.SHARED);
329+
bindNodes(7110, 7111, 8300);
330+
}
331+
332+
@Override
333+
public WeaviateContainer dependsOn(List<? extends Startable> startables) {
334+
leader.dependsOn(startables);
335+
return this;
336+
}
337+
338+
@Override
339+
public void setNetwork(Network network) {
340+
nodes.forEach(node -> node.setNetwork(network));
341+
}
342+
343+
@Override
344+
public WeaviateClient getClient() {
345+
if (!isRunning()) {
346+
start();
347+
}
348+
return leader.getClient();
349+
}
350+
351+
@Override
352+
public void start() {
353+
followers.forEach(Startable::start); // testcontainers will resolve dependencies
354+
}
355+
356+
@Override
357+
public void stop() {
358+
followers.forEach(Startable::stop);
359+
leader.stop();
360+
}
361+
362+
/**
363+
* Set environment variables for inter-cluster communication.
364+
*
365+
* @param gossip Gossip bind port.
366+
* @param data Data bind port.
367+
* @param raft RAFT port.
368+
*/
369+
private void bindNodes(int gossip, int data, int raft) {
370+
var publicPort = leader.getExposedPorts().get(0); // see WeaviateContainer Testcontainer.
371+
372+
nodes.forEach(node -> node
373+
.withEnv("CLUSTER_GOSSIP_BIND_PORT", String.valueOf(gossip))
374+
.withEnv("CLUSTER_DATA_BIND_PORT", String.valueOf(data))
375+
.withEnv("RAFT_PORT", String.valueOf(raft))
376+
.withEnv("RAFT_BOOTSTRAP_EXPECT", "1"));
377+
378+
followers.forEach(node -> node
379+
.withEnv("CLUSTER_JOIN", leader.containerName + ":" + gossip)
380+
.withEnv("RAFT_JOIN", leader.containerName)
381+
.waitingFor(
382+
Wait.forHttp("/v1/.well-known/ready")
383+
.forPort(publicPort)
384+
.forStatusCode(200)
385+
.withStartupTimeout(Duration.ofSeconds(10))));
386+
}
387+
}
267388
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package io.weaviate.integration;
2+
3+
import java.io.IOException;
4+
5+
import org.assertj.core.api.Assertions;
6+
import org.junit.Test;
7+
8+
import io.weaviate.ConcurrentTest;
9+
import io.weaviate.client6.v1.api.WeaviateClient;
10+
import io.weaviate.client6.v1.api.cluster.ShardingState;
11+
import io.weaviate.containers.Weaviate;
12+
13+
public class ClusterITest extends ConcurrentTest {
14+
private static final WeaviateClient client = Weaviate.cluster(3).getClient();
15+
16+
@Test
17+
public void test_shardingState() throws IOException {
18+
// Arrange
19+
var nsA = ns("A");
20+
var nsB = ns("B");
21+
22+
client.collections.create(nsA,
23+
a -> a.replication(r -> r.replicationFactor(2)));
24+
client.collections.create(nsB,
25+
b -> b.replication(r -> r.replicationFactor(3)));
26+
27+
// Act
28+
var optShardsA = client.cluster.shardingState(nsA);
29+
var optShardsB = client.cluster.shardingState(nsB);
30+
31+
// Assert
32+
var shardsA = Assertions.assertThat(optShardsA).get()
33+
.returns(nsA, ShardingState::collection)
34+
.extracting(ShardingState::shards)
35+
.actual();
36+
37+
var shardsB = Assertions.assertThat(optShardsB).get()
38+
.returns(nsB, ShardingState::collection)
39+
.extracting(ShardingState::shards)
40+
.actual();
41+
42+
Assertions.assertThat(shardsA).doesNotContainAnyElementsOf(shardsB);
43+
}
44+
45+
@Test
46+
public void test_listNodes() throws IOException {
47+
// Act
48+
var allNodes = client.cluster.listNodes();
49+
50+
// Assert
51+
Assertions.assertThat(allNodes).as("total no. nodes").hasSize(3);
52+
}
53+
}

src/main/java/io/weaviate/client6/v1/api/WeaviateClient.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import io.weaviate.client6.v1.api.alias.WeaviateAliasClient;
77
import io.weaviate.client6.v1.api.backup.WeaviateBackupClient;
8+
import io.weaviate.client6.v1.api.cluster.WeaviateClusterClient;
89
import io.weaviate.client6.v1.api.collections.WeaviateCollectionsClient;
910
import io.weaviate.client6.v1.api.rbac.groups.WeaviateGroupsClient;
1011
import io.weaviate.client6.v1.api.rbac.roles.WeaviateRolesClient;
@@ -37,7 +38,7 @@ public class WeaviateClient implements AutoCloseable {
3738

3839
/** Client for {@code /backups} endpoints for managing backups. */
3940
public final WeaviateBackupClient backup;
40-
41+
4142
/**
4243
* Client for {@code /authz/roles} endpoints for managing RBAC roles.
4344
*/
@@ -53,6 +54,12 @@ public class WeaviateClient implements AutoCloseable {
5354
*/
5455
public final WeaviateUsersClient users;
5556

57+
/**
58+
* Client for {@code /nodes} and {@code /replication} endpoints
59+
* for managing replication and sharding.
60+
*/
61+
public final WeaviateClusterClient cluster;
62+
5663
public WeaviateClient(Config config) {
5764
RestTransportOptions restOpt;
5865
GrpcChannelOptions grpcOpt;
@@ -108,6 +115,7 @@ public WeaviateClient(Config config) {
108115
this.roles = new WeaviateRolesClient(restTransport);
109116
this.groups = new WeaviateGroupsClient(restTransport);
110117
this.users = new WeaviateUsersClient(restTransport);
118+
this.cluster = new WeaviateClusterClient(restTransport);
111119
this.config = config;
112120
}
113121

src/main/java/io/weaviate/client6/v1/api/WeaviateClientAsync.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
import io.weaviate.client6.v1.api.alias.WeaviateAliasClientAsync;
88
import io.weaviate.client6.v1.api.backup.WeaviateBackupClientAsync;
9+
import io.weaviate.client6.v1.api.cluster.WeaviateClusterClientAsync;
910
import io.weaviate.client6.v1.api.collections.WeaviateCollectionsClient;
1011
import io.weaviate.client6.v1.api.collections.WeaviateCollectionsClientAsync;
1112
import io.weaviate.client6.v1.api.rbac.groups.WeaviateGroupsClientAsync;
@@ -52,6 +53,12 @@ public class WeaviateClientAsync implements AutoCloseable {
5253
*/
5354
public final WeaviateUsersClientAsync users;
5455

56+
/**
57+
* Client for {@code /nodes} and {@code /replication} endpoints
58+
* for managing replication and sharding.
59+
*/
60+
public final WeaviateClusterClientAsync cluster;
61+
5562
/**
5663
* This constructor is blocking if {@link Authentication} configured,
5764
* as the client will need to do the initial token exchange.
@@ -110,6 +117,7 @@ public WeaviateClientAsync(Config config) {
110117
this.roles = new WeaviateRolesClientAsync(restTransport);
111118
this.groups = new WeaviateGroupsClientAsync(restTransport);
112119
this.users = new WeaviateUsersClientAsync(restTransport);
120+
this.cluster = new WeaviateClusterClientAsync(restTransport);
113121
this.collections = new WeaviateCollectionsClientAsync(restTransport, grpcTransport);
114122
}
115123

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package io.weaviate.client6.v1.api.cluster;
2+
3+
import com.google.gson.annotations.SerializedName;
4+
5+
public record AsyncReplicationStatus(
6+
@SerializedName("objectsPropagated") long objectsPropagated,
7+
@SerializedName("startDiffTimeUnixMillis") long startDiffTimeUnixMillis,
8+
@SerializedName("targetNode") String targetNode) {
9+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package io.weaviate.client6.v1.api.cluster;
2+
3+
import com.google.gson.annotations.SerializedName;
4+
5+
public record CollectionStats(
6+
@SerializedName("shardCount") int shardCount,
7+
@SerializedName("objectCount") long objectCount) {
8+
}

0 commit comments

Comments
 (0)