fix(tenants): split tenant updates at the server's limit of 100 - #616
dudanogueira wants to merge 1 commit into
Conversation
PUT /schema/{class}/tenants rejects more than 100 tenants with HTTP 422,
so updating more than that failed outright -- and with it activate,
deactivate and offload, which all delegate to update. The Python and
TypeScript clients split the request internally, so the same code
written against either of them worked and the Java one did not.
update() now sends the tenants in batches of 100, on the sync and async
clients alike; the async one chains them so they reach the server one
after another rather than all at once. Adding tenants stays a single
request: the server validates it with allowOverHundred=true, so only
updates are capped.
More than one request means a partial update is now possible: if a batch
fails, the tenants of the preceding batches stay updated and the error
propagates. Python and TypeScript behave the same way; it is documented
on both update() methods.
MockRestTransport.performRequestAsync returned null, which no chained
caller could compose on; it returns a completed future now.
Closes #615
Claude-Session: https://claude.ai/code/session_01WmY5dAGWCccWDoqkKNC2JU
b2ff369 to
f8d3db4
Compare
There was a problem hiding this comment.
Orca Security Scan Summary
| Status | Check | Issues by priority | |
|---|---|---|---|
| Infrastructure as Code | View in Orca | ||
| SAST | View in Orca | ||
| Secrets | View in Orca | ||
| Vulnerabilities | View in Orca |
There was a problem hiding this comment.
Pull request overview
This PR fixes tenant updates failing with HTTP 422 when more than 100 tenants are updated at once by batching PUT /schema/{class}/tenants requests at the server-enforced limit (100). The batching is centralized so that update, activate, deactivate, and offload all benefit, with both sync and async clients sending batches sequentially.
Changes:
- Add batching logic (
MAX_TENANTS_PER_REQUEST = 100andbatches(...)) toUpdateTenantsRequest. - Update sync and async tenants clients to send update batches sequentially, documenting partial-update behavior.
- Add unit and integration tests covering batching behavior and a mock transport adjustment to enable async chaining tests.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| src/test/java/io/weaviate/testutil/transport/MockRestTransport.java | Returns a completed future for async requests to enable thenCompose chaining in tests. |
| src/test/java/io/weaviate/client6/v1/api/collections/tenants/UpdateTenantsBatchingTest.java | New unit tests validating batch splitting and request counts/bodies for sync + async clients. |
| src/main/java/io/weaviate/client6/v1/api/collections/tenants/WeaviateTenantsClientAsync.java | Chains update batches sequentially via thenCompose; adds Javadoc about batching/partial updates. |
| src/main/java/io/weaviate/client6/v1/api/collections/tenants/WeaviateTenantsClient.java | Sends update batches sequentially in the sync client; adds Javadoc about batching/partial updates. |
| src/main/java/io/weaviate/client6/v1/api/collections/tenants/UpdateTenantsRequest.java | Encodes the server limit (100) and provides batching helper used by both clients. |
| src/it/java/io/weaviate/integration/TenantsITest.java | Adds an integration test validating 250-tenant deactivate/activate works via batching. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| public <RequestT, ResponseT, ExceptionT> CompletableFuture<ResponseT> performRequestAsync(RequestT request, | ||
| Endpoint<RequestT, ResponseT> endpoint) { | ||
| requests.add(new Request<>(request, endpoint)); | ||
| return null; | ||
| // A completed future rather than null, so callers which chain requests | ||
| // (thenCompose) can be tested against this transport. |
| * The server accepts at most | ||
| * {@value UpdateTenantsRequest#MAX_TENANTS_PER_REQUEST} tenants per update, so | ||
| * longer lists are sent as several requests, chained so that they reach the | ||
| * server one after another. That makes a partial update possible: if one | ||
| * request fails, the tenants of the preceding ones stay updated and the | ||
| * returned future completes exceptionally. |
There was a problem hiding this comment.
🟡 Changes recommended
The async MockRestTransport change currently drops BooleanEndpoint behavior (can return null where a boolean is expected) and there is a misleading @param Javadoc in the updated public method.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Suppressed comments (1)
src/test/java/io/weaviate/testutil/transport/MockRestTransport.java:61
performRequestAsyncalways returnscompletedFuture(null)and ignoresBooleanEndpoint, unlikeperformRequestandDefaultRestTransportwhich return a boolean result for boolean endpoints. This can cause async client tests usingMockRestTransport(e.g.,exists(...)) to seenullinstead of a boolean.
@Override
public <RequestT, ResponseT, ExceptionT> CompletableFuture<ResponseT> performRequestAsync(RequestT request,
Endpoint<RequestT, ResponseT> endpoint) {
requests.add(new Request<>(request, endpoint));
// A completed future rather than null, so callers which chain requests
// (thenCompose) can be tested against this transport.
return CompletableFuture.completedFuture(null);
}
- Files reviewed: 6/6 changed files
- Comments generated: 1
- Review effort level: Lite
| * The server accepts at most | ||
| * {@value UpdateTenantsRequest#MAX_TENANTS_PER_REQUEST} tenants per update, so | ||
| * longer lists are sent as several sequential requests. That makes a partial | ||
| * update possible: if one request fails, the tenants of the preceding ones stay | ||
| * updated and the error is propagated. | ||
| * | ||
| * @param tenants Tenant names. | ||
| * @throws WeaviateApiException in case the server returned with an |
Motivation
PUT /schema/{class}/tenantsrejects more than 100 tenants with HTTP 422, and the client sent whatever list it was given in one request. Updating more than 100 tenants therefore failed outright — and with itactivate,deactivateandoffload, which all delegate toupdate:Python (
UPDATE_TENANT_BATCH_SIZE = 100) and TypeScript (serialize/index.ts) both split this internally, so the same code written against either of them worked and the Java one did not.Approach
update(List<Tenant>)is the single choke point —activate,deactivateandoffloadall route through it — so batching there covers every affected method with one change. The batching itself lives onUpdateTenantsRequest, next to the endpoint whose limit it encodes, and both the sync and async clients use it.The async client chains its batches with
thenComposerather than firing them in parallel, so the requests reach the server one after another and the observable behaviour matches the sync client.createis deliberately left alone. The asymmetry is in the server:AddTenantsvalidates withallowOverHundred=trueandUpdateTenantswithfalse(usecases/schema/tenant.go), so adding tenants is uncapped and chunking it would only cost round-trips.Partial updates
This is the one behaviour change worth a decision rather than a default. More than one request means a batch can now fail after earlier batches have already been applied, leaving tenants in mixed states. This PR does what Python and TypeScript do — leave the earlier batches applied and propagate the error — and documents it on both
updatemethods.The alternative, rolling back applied batches, is not something the client can do safely: it has no record of the previous statuses, and the rollback could fail in the same way. Worth flagging if you would rather it did something else.
Key areas for review
UpdateTenantsRequest.batchesreturnssubListviews, so the caller must not mutate the list while requests are in flight. Documented; happy to copy defensively if you prefer.MockRestTransport.performRequestAsyncreturnednull, which nothing chaining futures could compose on. It returns a completed future now, which is what let the async path be unit-tested.Testing
UpdateTenantsBatchingTest— 8 cases: the batch split at exactly 100, 101 and 250; batches covering every tenant in order; an empty list; and, throughMockRestTransport, thatupdatesends 2 requests for 101 tenants with the right tenants in each, that 100 stays a single request, thatdeactivatesplits into 3 for 201, thatcreateis not split, and that the async client splits the same way.TenantsITest.test_updateMoreThanOneHundredTenants— creates 250 tenants against a real server, deactivates them all, then activates them all again.Verified the integration test reproduces the reported bug without the fix:
Locally green: 390 unit tests, and
TenantsITestagainst a container (2 run, 0 failures).Breaking changes
None. Public signatures are unchanged, and lists of 100 or fewer still go out as exactly one request.
Closes #615
🤖 Generated with Claude Code
https://claude.ai/code/session_01WmY5dAGWCccWDoqkKNC2JU