Skip to content

Commit 4aef132

Browse files
committed
test(batch): add tests for BatchContext
1 parent 453f62a commit 4aef132

2 files changed

Lines changed: 323 additions & 4 deletions

File tree

Lines changed: 320 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,320 @@
1+
package io.weaviate.client6.v1.api.collections.batch;
2+
3+
import static java.util.Objects.requireNonNull;
4+
5+
import java.io.IOException;
6+
import java.util.ArrayList;
7+
import java.util.Collections;
8+
import java.util.List;
9+
import java.util.Map;
10+
import java.util.Optional;
11+
import java.util.concurrent.ArrayBlockingQueue;
12+
import java.util.concurrent.BlockingQueue;
13+
import java.util.concurrent.CompletableFuture;
14+
import java.util.concurrent.ExecutionException;
15+
import java.util.concurrent.ExecutorService;
16+
import java.util.concurrent.Executors;
17+
import java.util.concurrent.Future;
18+
import java.util.stream.Stream;
19+
20+
import org.assertj.core.api.Assertions;
21+
import org.junit.After;
22+
import org.junit.Before;
23+
import org.junit.Test;
24+
25+
import io.grpc.stub.StreamObserver;
26+
import io.weaviate.client6.v1.api.collections.CollectionHandleDefaults;
27+
import io.weaviate.client6.v1.api.collections.WeaviateObject;
28+
import io.weaviate.client6.v1.api.collections.query.ConsistencyLevel;
29+
import io.weaviate.client6.v1.internal.grpc.protocol.WeaviateProtoBatch;
30+
import io.weaviate.client6.v1.internal.orm.CollectionDescriptor;
31+
32+
public class BatchContextTest {
33+
private static final CollectionDescriptor<Map<String, Object>> DESCRIPTOR = CollectionDescriptor
34+
.ofMap("BatchContextTest");
35+
private static final CollectionHandleDefaults DEFAULTS = new CollectionHandleDefaults(
36+
Optional.of(ConsistencyLevel.ONE), Optional.of("john_doe"));
37+
38+
/**
39+
* Maximum gRPC message size of 2KB .
40+
* 1KB is {@link MessageSizeUtil#SAFETY_MARGIN}.
41+
*/
42+
private static final int MAX_SIZE_BYTES = 2 * 1024;
43+
private static final int BATCH_SIZE = 10;
44+
private static final int QUEUE_SIZE = 1;
45+
46+
private CompletableStreamFactory factory;
47+
private MockServerStream stream;
48+
private BatchContext<Map<String, Object>> context;
49+
50+
/**
51+
* Create new unstarted context with default maxSizeBytes, collection
52+
* descriptor, and collection handle defaults.
53+
*/
54+
@Before
55+
public void setupContext() throws InterruptedException, ExecutionException {
56+
factory = new CompletableStreamFactory();
57+
context = new BatchContext.Builder<>(factory, MAX_SIZE_BYTES, DESCRIPTOR, DEFAULTS)
58+
.batchSize(BATCH_SIZE)
59+
.queueSize(QUEUE_SIZE)
60+
.build();
61+
context.start();
62+
stream = factory.serverStream.get();
63+
}
64+
65+
@After
66+
public void closeContext() throws IOException {
67+
if (context != null) {
68+
// Some of the tests may close the context, so this
69+
// implicitly tests that closing it multiple times is OK.
70+
context.close();
71+
context = null;
72+
}
73+
stream = null;
74+
factory = null;
75+
}
76+
77+
@Test
78+
public void test_sendOneBatch() throws Exception {
79+
expectMessage(WeaviateProtoBatch.BatchStreamRequest.MessageCase.START);
80+
stream.emitEvent(Event.STARTED);
81+
82+
List<TaskHandle> tasks = new ArrayList<>();
83+
for (int i = 0; i < BATCH_SIZE; i++) {
84+
tasks.add(context.add(WeaviateObject.of()));
85+
}
86+
87+
// BatchContext should flush the current batch once it hits its limit.
88+
// We will ack all items in the batch and send successful result for each one.
89+
List<String> received = ack();
90+
Assertions.assertThat(tasks)
91+
.extracting(TaskHandle::id).containsExactlyInAnyOrderElementsOf(received);
92+
Assertions.assertThat(tasks)
93+
.extracting(TaskHandle::isAcked).allMatch(CompletableFuture::isDone);
94+
95+
stream.emitEvent(new Event.Results(received, Collections.emptyMap()));
96+
97+
// Since MockServerStream runs in the same thread as this test,
98+
// the context will be updated before the last emitEvent returns.
99+
context.close();
100+
101+
Assertions.assertThat(tasks).extracting(TaskHandle::result)
102+
.allMatch(CompletableFuture::isDone)
103+
.extracting(CompletableFuture::get).extracting(TaskHandle.Result::error)
104+
.allMatch(Optional::isEmpty);
105+
}
106+
107+
@Test
108+
public void test_drainOnClose() throws Exception {
109+
expectMessage(WeaviateProtoBatch.BatchStreamRequest.MessageCase.START);
110+
stream.emitEvent(Event.STARTED);
111+
112+
List<TaskHandle> tasks = new ArrayList<>();
113+
for (int i = 0; i < BATCH_SIZE - 2; i++) {
114+
tasks.add(context.add(WeaviateObject.of()));
115+
}
116+
117+
// Contrary the test above, we expect the objects to be sent
118+
// only after context.close(), as the half-empty batch will
119+
// be drained. Similarly, we want to ack everything as it arrives.
120+
ExecutorService exec = Executors.newSingleThreadExecutor();
121+
Future<?> mockServer = exec.submit(() -> {
122+
try {
123+
List<String> received = ack();
124+
Assertions.assertThat(tasks)
125+
.extracting(TaskHandle::id).containsExactlyInAnyOrderElementsOf(received);
126+
Assertions.assertThat(tasks)
127+
.extracting(TaskHandle::isAcked).allMatch(CompletableFuture::isDone);
128+
129+
stream.emitEvent(new Event.Results(received, Collections.emptyMap()));
130+
} catch (InterruptedException e) {
131+
throw new RuntimeException("mock server interrupted", e);
132+
}
133+
});
134+
135+
context.close();
136+
mockServer.get(); // Wait for the "mock server" to process the data message.
137+
138+
Assertions.assertThat(tasks).extracting(TaskHandle::result)
139+
.allMatch(CompletableFuture::isDone)
140+
.extracting(CompletableFuture::get).extracting(TaskHandle.Result::error)
141+
.allMatch(Optional::isEmpty);
142+
}
143+
144+
@Test
145+
public void test_backoff() throws Exception {
146+
expectMessage(WeaviateProtoBatch.BatchStreamRequest.MessageCase.START);
147+
stream.emitEvent(Event.STARTED);
148+
149+
stream.emitEvent(new Event.Backoff(BATCH_SIZE / 2));
150+
151+
List<TaskHandle> tasks = new ArrayList<>();
152+
ExecutorService exec = Executors.newSingleThreadExecutor();
153+
Future<?> testUser = exec.submit(() -> {
154+
try {
155+
for (int i = 0; i < BATCH_SIZE; i++) {
156+
tasks.add(context.add(WeaviateObject.of()));
157+
}
158+
} catch (InterruptedException e) {
159+
throw new RuntimeException("test user interrupted", e);
160+
}
161+
});
162+
163+
// BatchContext should flush the current batch once it hits the limit
164+
// set by the Backoff message, i.e. BATCH_SIZE / 2.
165+
List<String> received = ack();
166+
Assertions.assertThat(received).hasSize(BATCH_SIZE / 2);
167+
stream.emitEvent(new Event.Results(received, Collections.emptyMap()));
168+
169+
testUser.get(); // Finish populating batch context.
170+
171+
// Since testUser will try and add BATCH_SIZE no. objects,
172+
// we should expect there to be exactly 2 batches.
173+
received = ack();
174+
Assertions.assertThat(received).hasSize(BATCH_SIZE / 2);
175+
stream.emitEvent(new Event.Results(received, Collections.emptyMap()));
176+
177+
context.close();
178+
179+
Assertions.assertThat(tasks).extracting(TaskHandle::result)
180+
.allMatch(CompletableFuture::isDone)
181+
.extracting(CompletableFuture::get).extracting(TaskHandle.Result::error)
182+
.allMatch(Optional::isEmpty);
183+
}
184+
185+
@Test
186+
public void test_backoffBacklog() throws Exception {
187+
expectMessage(WeaviateProtoBatch.BatchStreamRequest.MessageCase.START);
188+
stream.emitEvent(Event.STARTED);
189+
190+
// Pre-fill the batch without triggering a flush (n-1).
191+
List<TaskHandle> tasks = new ArrayList<>();
192+
for (int i = 0; i < BATCH_SIZE - 1; i++) {
193+
tasks.add(context.add(WeaviateObject.of()));
194+
}
195+
196+
int batchSizeNew = BATCH_SIZE / 2;
197+
198+
// Force the last BATCH_SIZE / 2 - 1 items to be transferred to the backlog.
199+
stream.emitEvent(new Event.Backoff(batchSizeNew));
200+
201+
// The next item will go on the backlog and the trigger a flush,
202+
// which will continue to send batches and re-populate the from
203+
// the backlog as long as the batch is full, so we should expect
204+
// to see 2 batches of size BATCH_SIZE / 2 each.
205+
List<String> received;
206+
tasks.add(context.add(WeaviateObject.of()));
207+
208+
Assertions.assertThat(received = ack()).hasSize(batchSizeNew);
209+
stream.emitEvent(new Event.Results(received, Collections.emptyMap()));
210+
211+
Assertions.assertThat(received = ack()).hasSize(batchSizeNew);
212+
stream.emitEvent(new Event.Results(received, Collections.emptyMap()));
213+
214+
context.close();
215+
216+
Assertions.assertThat(tasks).extracting(TaskHandle::result)
217+
.allMatch(CompletableFuture::isDone)
218+
.extracting(CompletableFuture::get).extracting(TaskHandle.Result::error)
219+
.allMatch(Optional::isEmpty);
220+
}
221+
222+
@Test(expected = IllegalStateException.class)
223+
public void test_add_closed() throws Exception {
224+
expectMessage(WeaviateProtoBatch.BatchStreamRequest.MessageCase.START);
225+
context.close();
226+
context.add(WeaviateObject.of(o -> o.properties(Map.of())));
227+
}
228+
229+
/** Consume the next message and assert its type. */
230+
private WeaviateProtoBatch.BatchStreamRequest expectMessage(
231+
WeaviateProtoBatch.BatchStreamRequest.MessageCase messageCase) throws InterruptedException {
232+
WeaviateProtoBatch.BatchStreamRequest actual = stream.recv();
233+
Assertions.assertThat(actual)
234+
.extracting(WeaviateProtoBatch.BatchStreamRequest::getMessageCase)
235+
.isEqualTo(messageCase);
236+
return actual;
237+
}
238+
239+
private List<String> ack() throws InterruptedException {
240+
WeaviateProtoBatch.BatchStreamRequest req = expectMessage(WeaviateProtoBatch.BatchStreamRequest.MessageCase.DATA);
241+
WeaviateProtoBatch.BatchStreamRequest.Data data = req.getData();
242+
List<String> ids = Stream.concat(
243+
data.getObjects().getValuesList().stream().map(WeaviateProtoBatch.BatchObject::getUuid),
244+
data.getReferences().getValuesList().stream().map(MockServerStream::getBeacon))
245+
.toList();
246+
stream.emitEvent(new Event.Acks(ids));
247+
return ids;
248+
}
249+
250+
private class CompletableStreamFactory implements StreamFactory<Message, Event> {
251+
final CompletableFuture<MockServerStream> serverStream = new CompletableFuture<>();
252+
253+
@Override
254+
public StreamObserver<Message> createStream(StreamObserver<Event> recv) {
255+
MockServerStream mock = new MockServerStream(recv);
256+
serverStream.complete(mock);
257+
return mock;
258+
}
259+
}
260+
261+
private class MockServerStream implements StreamObserver<Message> {
262+
private final BlockingQueue<WeaviateProtoBatch.BatchStreamRequest> requestQueue;
263+
private final CompletableFuture<?> done;
264+
private final StreamObserver<Event> eventStream;
265+
266+
public MockServerStream(StreamObserver<Event> eventStream) {
267+
this.eventStream = requireNonNull(eventStream, "eventStream is null");
268+
this.requestQueue = new ArrayBlockingQueue<WeaviateProtoBatch.BatchStreamRequest>(1);
269+
this.done = new CompletableFuture<>();
270+
}
271+
272+
WeaviateProtoBatch.BatchStreamRequest recv() throws InterruptedException {
273+
return requestQueue.take();
274+
}
275+
276+
void emitEvent(Event event) {
277+
assert event != Event.EOF : "server-side stream must be closed automatically";
278+
if (event instanceof Event.StreamHangup hangup) {
279+
eventStream.onError(hangup.exception());
280+
} else {
281+
eventStream.onNext(event);
282+
}
283+
}
284+
285+
void hangupStream() {
286+
emitEvent(new Event.StreamHangup(new RuntimeException("whaam!")));
287+
}
288+
289+
private void closeStream(Object result, Throwable ex) {
290+
eventStream.onCompleted();
291+
}
292+
293+
@Override
294+
public void onCompleted() {
295+
done.complete(null);
296+
eventStream.onCompleted();
297+
}
298+
299+
@Override
300+
public void onError(Throwable t) {
301+
done.completeExceptionally(t);
302+
eventStream.onCompleted();
303+
}
304+
305+
@Override
306+
public void onNext(Message message) {
307+
requestQueue.offer(asRequest(message));
308+
}
309+
310+
private static WeaviateProtoBatch.BatchStreamRequest asRequest(Message message) {
311+
var builder = WeaviateProtoBatch.BatchStreamRequest.newBuilder();
312+
message.appendTo(builder);
313+
return builder.build();
314+
}
315+
316+
static String getBeacon(WeaviateProtoBatch.BatchReference reference) {
317+
return "weaviate://localhost/" + reference.getToCollection() + "/" + reference.getToUuid();
318+
}
319+
}
320+
}

src/test/java/io/weaviate/client6/v1/api/collections/batch/BatchTest.java

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,7 @@ public void test_setMaxSize_inFlight() {
210210
* The size of the serialized object is always {@link OBJECT_SIZE_BYTES}.
211211
*/
212212
private static void addObject(Batch batch) {
213-
WeaviateObject<Map<String, Object>> object = WeaviateObject.of(o -> o.uuid(UUID.randomUUID().toString()));
213+
WeaviateObject<Map<String, Object>> object = WeaviateObject.of();
214214
batch.add(new Data(object, object.uuid(), newBatchObject(object), Data.Type.OBJECT));
215215
}
216216

@@ -221,9 +221,8 @@ private static WeaviateProtoBatch.BatchObject newBatchObject(WeaviateObject<Map<
221221

222222
private static WeaviateProtoBatch.BatchObject bigBatchObject(int sizeBytes) {
223223
Random random = new Random();
224-
Function<Vectors, WeaviateProtoBatch.BatchObject> object = vectors -> newBatchObject(WeaviateObject.of(
225-
o -> o.uuid(UUID.randomUUID().toString())
226-
.vectors(vectors)));
224+
Function<Vectors, WeaviateProtoBatch.BatchObject> object = vectors -> newBatchObject(
225+
WeaviateObject.of(o -> o.vectors(vectors)));
227226

228227
// Keep adding vectors to the object until we've hit the right size.
229228
WeaviateProtoBatch.BatchObject out = null;

0 commit comments

Comments
 (0)