fix(tenants): split tenant updates at the server's limit of 100 - #616
Open
dudanogueira wants to merge 1 commit into
Open
fix(tenants): split tenant updates at the server's limit of 100#616dudanogueira wants to merge 1 commit into
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
dudanogueira
force-pushed
the
fix/tenant-update-batching
branch
from
August 28, 2026 21:15
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.
Comment on lines
55
to
+59
| 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. |
Comment on lines
+61
to
+66
| * 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. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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