Skip to content

Commit 9d35d74

Browse files
committed
feat: add tenants API
1 parent 452ffc0 commit 9d35d74

18 files changed

Lines changed: 488 additions & 12 deletions

pom.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,7 @@
244244
<artifactId>maven-surefire-plugin</artifactId>
245245
<version>2.22.2</version>
246246
<configuration>
247+
<trimStackTrace>false</trimStackTrace>
247248
<argLine>
248249
<!--
249250
Gson (used for JSON serialization) utilizes reflection and needs to be able to access private fields of

src/it/java/io/weaviate/ConcurrentTest.java

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,14 @@
22

33
import java.util.Random;
44
import java.util.UUID;
5+
import java.util.concurrent.Callable;
6+
import java.util.concurrent.CompletableFuture;
7+
import java.util.concurrent.ExecutionException;
8+
import java.util.concurrent.TimeUnit;
9+
import java.util.concurrent.TimeoutException;
510

611
import org.apache.commons.lang3.RandomStringUtils;
12+
import org.assertj.core.api.Assertions;
713
import org.junit.Rule;
814
import org.junit.rules.TestName;
915

@@ -62,4 +68,46 @@ protected static float[] randomVector(int length, float origin, float bound) {
6268
}
6369
return vector;
6470
}
71+
72+
/**
73+
* Check that a condition is eventually met.
74+
*
75+
* @param cond Arbitrary code that evaluates the test condition..
76+
* @param intervalMillis Polling interval.
77+
* @param timeoutSeconds Maximum waiting time.
78+
* @param message Optional failure message.
79+
*
80+
* @throws AssertionError if the condition does not evaluate to true
81+
* within {@code timeoutSeconds} or a thread
82+
* was interrupted in the meantime.
83+
* @throws RuntimeException if an exception occurred when envalating condition.
84+
*/
85+
public static void eventually(Callable<Boolean> cond, int intervalMillis, int timeoutSeconds, String... message) {
86+
var check = CompletableFuture.runAsync(() -> {
87+
try {
88+
while (!Thread.currentThread().isInterrupted() && !cond.call()) {
89+
try {
90+
Thread.sleep(intervalMillis);
91+
} catch (InterruptedException ex) {
92+
Thread.currentThread().interrupt();
93+
}
94+
}
95+
} catch (Exception e) {
96+
// Propagate to callee
97+
throw new RuntimeException(e);
98+
}
99+
});
100+
101+
try {
102+
check.get(timeoutSeconds, TimeUnit.SECONDS);
103+
} catch (TimeoutException ex) {
104+
check.cancel(true);
105+
Assertions.fail(message.length >= 0 ? message[0] : null, ex);
106+
} catch (InterruptedException ex) {
107+
Thread.currentThread().interrupt();
108+
Assertions.fail(ex);
109+
} catch (ExecutionException ex) {
110+
throw new RuntimeException(ex);
111+
}
112+
}
65113
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ public class Container {
1616
public static final Weaviate WEAVIATE = Weaviate.createDefault();
1717
public static final Contextionary CONTEXTIONARY = Contextionary.createDefault();
1818
public static final Img2VecNeural IMG2VEC_NEURAL = Img2VecNeural.createDefault();
19+
public static final MinIo MINIO = MinIo.createDefault();
1920

2021
static {
2122
startAll();
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package io.weaviate.containers;
2+
3+
import org.testcontainers.containers.MinIOContainer;
4+
5+
public class MinIo extends MinIOContainer {
6+
private static final String DOCKER_IMAGE = "minio/minio";
7+
public static final String ACCESS_KEY = "minioadmin";
8+
public static final String SECRET_KEY = "minioadmin";
9+
10+
static MinIo createDefault() {
11+
return new MinIo();
12+
}
13+
14+
private MinIo() {
15+
super(DOCKER_IMAGE);
16+
withUserName(ACCESS_KEY);
17+
withPassword(SECRET_KEY);
18+
withCreateContainerCmdModifier(cmd -> cmd.withHostName("minio"));
19+
}
20+
}

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,15 @@ public Builder withImageInference(String url, String module) {
8989
return this;
9090
}
9191

92+
public Builder withOffloadS3(String accessKey, String secretKey) {
93+
addModules("offload-s3");
94+
environment.put("OFFLOAD_S3_ENDPOINT", "http://minio:9000");
95+
environment.put("OFFLOAD_S3_BUCKET_AUTO_CREATE", "true");
96+
environment.put("AWS_ACCESS_KEY_ID", accessKey);
97+
environment.put("AWS_SECRET_KEY", secretKey);
98+
return this;
99+
}
100+
92101
public Builder enableTelemetry(boolean enable) {
93102
telemetry = enable;
94103
return this;
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package io.weaviate.integration;
2+
3+
import org.assertj.core.api.Assertions;
4+
import org.junit.Test;
5+
6+
import io.weaviate.ConcurrentTest;
7+
import io.weaviate.client6.v1.api.WeaviateClient;
8+
import io.weaviate.client6.v1.api.collections.tenants.Tenant;
9+
import io.weaviate.containers.Container;
10+
import io.weaviate.containers.Container.ContainerGroup;
11+
import io.weaviate.containers.MinIo;
12+
import io.weaviate.containers.Weaviate;
13+
14+
public class TenantsITest extends ConcurrentTest {
15+
private static final ContainerGroup compose = Container.compose(
16+
Weaviate.custom()
17+
.withOffloadS3(MinIo.ACCESS_KEY, MinIo.SECRET_KEY)
18+
.build(),
19+
Container.MINIO);
20+
21+
private static WeaviateClient client = compose.getClient();
22+
23+
@Test
24+
public void test_tenantLifecycle() throws Exception {
25+
var nsThings = ns("Things");
26+
27+
client.collections.create(
28+
nsThings, c -> c
29+
.multiTenancy(mt -> mt
30+
.autoTenantCreation(false)
31+
.autoTenantActivation(false)));
32+
33+
var things = client.collections.use(nsThings);
34+
35+
// No tenants at first
36+
Assertions.assertThat(things.tenants.list()).as("no tenants initially").isEmpty();
37+
38+
var allison = Tenant.active("active-allison");
39+
var isaac = Tenant.inactive("inactive-isaac");
40+
var owen = Tenant.inactive("offloaded-owen");
41+
42+
things.tenants.create(allison, isaac, owen);
43+
44+
// Collection has 2 tenants creted just now.
45+
Assertions.assertThat(things.tenants.list()).as("list created tenants").hasSize(3);
46+
Assertions.assertThat(things.tenants.exists(allison.name()))
47+
.describedAs("%s exists", allison.name()).isTrue();
48+
Assertions.assertThat(things.tenants.exists(isaac.name()))
49+
.describedAs("%s exists", isaac.name()).isTrue();
50+
Assertions.assertThat(things.tenants.exists(owen.name()))
51+
.describedAs("%s exists", owen.name()).isTrue();
52+
53+
things.tenants.activate(isaac.name());
54+
eventually(() -> things.tenants.get(isaac.name()).get().isActive(),
55+
200, 2, isaac.name() + " not activated");
56+
57+
things.tenants.deactivate(allison.name());
58+
eventually(() -> things.tenants.get(allison.name()).get().isInactive(),
59+
200, 2, allison.name() + " not deactivated");
60+
61+
things.tenants.offload(owen.name());
62+
eventually(() -> things.tenants.get(owen.name()).get().isOffloaded(),
63+
200, 2, owen.name() + " not offloaded");
64+
65+
things.tenants.delete(allison.name(), isaac.name(), owen.name());
66+
Assertions.assertThat(things.tenants.list()).as("no tenants after deletion").isEmpty();
67+
Assertions.assertThat(things.tenants.exists(allison.name()))
68+
.describedAs("%s not exists", allison.name()).isFalse();
69+
Assertions.assertThat(things.tenants.exists(isaac.name()))
70+
.describedAs("%s not exists", isaac.name()).isFalse();
71+
Assertions.assertThat(things.tenants.exists(owen.name()))
72+
.describedAs("%s not exists", owen.name()).isFalse();
73+
}
74+
}

src/main/java/io/weaviate/client6/v1/api/collections/CollectionHandle.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import io.weaviate.client6.v1.api.collections.pagination.Paginator;
1010
import io.weaviate.client6.v1.api.collections.query.ConsistencyLevel;
1111
import io.weaviate.client6.v1.api.collections.query.WeaviateQueryClient;
12+
import io.weaviate.client6.v1.api.collections.tenants.WeaviateTenantsClient;
1213
import io.weaviate.client6.v1.internal.ObjectBuilder;
1314
import io.weaviate.client6.v1.internal.grpc.GrpcTransport;
1415
import io.weaviate.client6.v1.internal.orm.CollectionDescriptor;
@@ -19,6 +20,7 @@ public class CollectionHandle<PropertiesT> {
1920
public final WeaviateDataClient<PropertiesT> data;
2021
public final WeaviateQueryClient<PropertiesT> query;
2122
public final WeaviateAggregateClient aggregate;
23+
public final WeaviateTenantsClient tenants;
2224

2325
private final CollectionHandleDefaults defaults;
2426

@@ -31,6 +33,7 @@ public CollectionHandle(
3133
this.aggregate = new WeaviateAggregateClient(collection, grpcTransport, defaults);
3234
this.query = new WeaviateQueryClient<>(collection, grpcTransport, defaults);
3335
this.data = new WeaviateDataClient<>(collection, restTransport, grpcTransport, defaults);
36+
this.tenants = new WeaviateTenantsClient(collection, restTransport, grpcTransport);
3437

3538
this.defaults = defaults;
3639
}
@@ -41,6 +44,7 @@ private CollectionHandle(CollectionHandle<PropertiesT> c, CollectionHandleDefaul
4144
this.aggregate = c.aggregate;
4245
this.query = new WeaviateQueryClient<>(c.query, defaults);
4346
this.data = new WeaviateDataClient<>(c.data, defaults);
47+
this.tenants = c.tenants;
4448

4549
this.defaults = defaults;
4650
}

src/main/java/io/weaviate/client6/v1/api/collections/MultiTenancy.java

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,30 +7,38 @@
77
import io.weaviate.client6.v1.internal.ObjectBuilder;
88

99
public record MultiTenancy(
10+
@SerializedName("enabled") Boolean enabled,
1011
@SerializedName("autoTenantCreation") Boolean createAutomatically,
11-
@SerializedName("autoTenantActivate") Boolean activateAutomatically) {
12+
@SerializedName("autoTenantActivation") Boolean activateAutomatically) {
1213

1314
public static MultiTenancy of(Function<Builder, ObjectBuilder<MultiTenancy>> fn) {
1415
return fn.apply(new Builder()).build();
1516
}
1617

1718
public MultiTenancy(Builder builder) {
1819
this(
20+
builder.enabled,
1921
builder.createAutomatically,
2022
builder.activateAutomatically);
2123
}
2224

2325
public static class Builder implements ObjectBuilder<MultiTenancy> {
26+
private Boolean enabled = true;
2427
private Boolean createAutomatically;
2528
private Boolean activateAutomatically;
2629

27-
public Builder createAutomatically(boolean createAutomatically) {
28-
this.createAutomatically = createAutomatically;
30+
public Builder enabled(boolean enabled) {
31+
this.enabled = enabled;
2932
return this;
3033
}
3134

32-
public Builder activateAutomatically(boolean activateAutomatically) {
33-
this.activateAutomatically = activateAutomatically;
35+
public Builder autoTenantCreation(boolean enabled) {
36+
this.createAutomatically = enabled;
37+
return this;
38+
}
39+
40+
public Builder autoTenantActivation(boolean enabled) {
41+
this.activateAutomatically = enabled;
3442
return this;
3543
}
3644

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package io.weaviate.client6.v1.api.collections.tenants;
2+
3+
import java.util.Collections;
4+
import java.util.List;
5+
6+
import io.weaviate.client6.v1.internal.json.JSON;
7+
import io.weaviate.client6.v1.internal.orm.CollectionDescriptor;
8+
import io.weaviate.client6.v1.internal.rest.Endpoint;
9+
import io.weaviate.client6.v1.internal.rest.SimpleEndpoint;
10+
11+
public record CreateTenantsRequest(List<Tenant> tenants) {
12+
static Endpoint<CreateTenantsRequest, Void> endpoint(CollectionDescriptor<?> collection) {
13+
return SimpleEndpoint.sideEffect(
14+
__ -> "POST",
15+
__ -> "/schema/" + collection.name() + "/tenants",
16+
__ -> Collections.emptyMap(),
17+
request -> JSON.serialize(request.tenants));
18+
}
19+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package io.weaviate.client6.v1.api.collections.tenants;
2+
3+
import java.util.Collections;
4+
import java.util.List;
5+
6+
import io.weaviate.client6.v1.internal.json.JSON;
7+
import io.weaviate.client6.v1.internal.orm.CollectionDescriptor;
8+
import io.weaviate.client6.v1.internal.rest.Endpoint;
9+
import io.weaviate.client6.v1.internal.rest.SimpleEndpoint;
10+
11+
public record DeleteTenantsRequest(List<String> tenants) {
12+
static Endpoint<DeleteTenantsRequest, Void> endpoint(CollectionDescriptor<?> collection) {
13+
return SimpleEndpoint.sideEffect(
14+
__ -> "DELETE",
15+
__ -> "/schema/" + collection.name() + "/tenants",
16+
__ -> Collections.emptyMap(),
17+
request -> JSON.serialize(request.tenants));
18+
}
19+
}

0 commit comments

Comments
 (0)