Skip to content

fix(tenants): split tenant updates at the server's limit of 100 - #616

Open
dudanogueira wants to merge 1 commit into
mainfrom
fix/tenant-update-batching
Open

fix(tenants): split tenant updates at the server's limit of 100#616
dudanogueira wants to merge 1 commit into
mainfrom
fix/tenant-update-batching

Conversation

@dudanogueira

Copy link
Copy Markdown

Motivation

PUT /schema/{class}/tenants rejects 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 it activate, deactivate and offload, which all delegate to update:

HTTP 422: PUT /v1/schema/MyCollection/tenants: maximum number of tenants allowed to be
updated simultaneously is 100. Please reduce the number of tenants in your request and try again

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, deactivate and offload all route through it — so batching there covers every affected method with one change. The batching itself lives on UpdateTenantsRequest, next to the endpoint whose limit it encodes, and both the sync and async clients use it.

The async client chains its batches with thenCompose rather than firing them in parallel, so the requests reach the server one after another and the observable behaviour matches the sync client.

create is deliberately left alone. The asymmetry is in the server: AddTenants validates with allowOverHundred=true and UpdateTenants with false (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 update methods.

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.batches returns subList views, so the caller must not mutate the list while requests are in flight. Documented; happy to copy defensively if you prefer.
  • The async chaining — worth confirming sequential is the behaviour you want, rather than parallel batches for speed.
  • MockRestTransport.performRequestAsync returned null, 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, through MockRestTransport, that update sends 2 requests for 101 tenants with the right tenants in each, that 100 stays a single request, that deactivate splits into 3 for 201, that create is 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:

HTTP 422: PUT /v1/schema/.../tenants: maximum number of tenants allowed to be
updated simultaneously is 100.

Locally green: 390 unit tests, and TenantsITest against 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

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
dudanogueira force-pushed the fix/tenant-update-batching branch from b2ff369 to f8d3db4 Compare August 28, 2026 21:15

@orca-security-eu orca-security-eu Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Orca Security Scan Summary

Status Check Issues by priority
Passed Passed Infrastructure as Code high 0   medium 0   low 0   info 0 View in Orca
Passed Passed SAST high 0   medium 0   low 0   info 0 View in Orca
Passed Passed Secrets high 0   medium 0   low 0   info 0 View in Orca
Passed Passed Vulnerabilities high 0   medium 0   low 0   info 0 View in Orca

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 = 100 and batches(...)) to UpdateTenantsRequest.
  • 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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

v6: tenant updates are not split at the server's 100-tenant limit, so activate/deactivate fails above 100

2 participants