Loading…
Loading…
Provides explicit tenant binding for background tasks like schedulers and startup hooks to prevent unbound tenant errors in a multi-tenant environment.
The DeploymentTenantBinding class is a utility for managing tenant context in background processes within the admin-api service. In a multi-tenant deployment, background tasks such as startup hooks, schedulers, and asynchronous jobs initiated by Executor threads run without the tenant context that is normally associated with an API request.
This poses a problem when these tasks need to access tenant-specific data or configuration from the database. A security component, the UnboundTenantGate, is installed on the system's DataSource. This gate prevents database connections from reading tenant-scoped data unless a specific tenant is bound to the current thread, failing with an S2R-TENANCY-UNBOUND error.
DeploymentTenantBinding solves this by providing static methods to explicitly bind a tenant for the duration of a task, ensuring that background work can operate correctly and securely.
The DeploymentTenantBinding utility provides two primary strategies for managing tenant context in background work, depending on the task's requirements. These strategies are necessary because the standard TenantContextFilter, which binds tenants for API requests, does not apply to threads that do not handle incoming requests.
For tasks that read deployment-wide configuration (e.g., public_base_url, installation_id, scheduler settings), the call() and run() methods are used. These tasks are not specific to any single tenant.
The logic is as follows:
TenantContext.DEFAULT_TENANT_ID.This ensures that reads of deployment-wide settings succeed. Binding to the default tenant preserves the behavior that existed before the UnboundTenantGate was introduced, where such settings were read from a fallback context.
For tasks that are initiated by a tenant-specific request but run on a separate thread (e.g., via CompletableFuture.supplyAsync), the withCallerTenant() method is used. This is necessary to carry the original request's tenant identity over to the execution thread.
The logic is as follows:
withCallerTenant() method is called on the original, tenant-bound thread (e.g., a request handler thread).Supplier that wraps the original task.Supplier is executed on a different thread (e.g., from a thread pool), it first binds the captured tenant ID to that thread before running the original task's logic.The following diagram illustrates the control flow for both binding strategies.
flowchart TD
subgraph Deployment-Wide Task
direction TD
A[Background Task e.g., Scheduler] -- "invokes" --> B(DeploymentTenantBinding.run)
B --> C{Tenant already bound?}
C -- No --> D[Bind to DEFAULT_TENANT_ID]
D --> E[Execute Task Logic]
C -- Yes --> E
end
subgraph Async Task from Tenant Context
direction TD
F[Request Thread with Tenant] -- "creates" --> G(DeploymentTenantBinding.withCallerTenant)
G --> H[Capture Tenant ID from Request Thread]
H --> I[Return Wrapped Task]
J[Unbound Thread Pool] -- "later executes" --> I
I --> K[Bind to Captured Tenant ID]
K --> L[Execute Task Logic]
endDeploymentTenantBinding is a final class with only static methods and cannot be instantiated.
| Name | Type | Description |
|---|---|---|
UNBOUND_ERROR_TOKEN | String | A stable string token (S2R-TENANCY-UNBOUND) present in all database errors raised by the UnboundTenantGate. |
call(Supplier<T> body)Runs a task that returns a value, ensuring a tenant is bound. If the current thread is already bound to a tenant, it uses that tenant. If the thread is unbound, it binds the TenantContext.DEFAULT_TENANT_ID for the duration of the call.
| Name | Type | Description |
|---|
| body | Supplier<T> | The task to execute. |
T - The result from the executed Supplier.run(Runnable body)A convenience method for tasks that do not return a value. It operates with the same tenant-binding logic as call().
| Name | Type | Description |
|---|
| body | Runnable | The task to execute. |
voidwithCallerTenant(Supplier<T> body)Wraps a Supplier to propagate the calling thread's tenant context to the thread where the Supplier is eventually executed. If the calling thread has no tenant bound, the returned Supplier remains unbound.
| Name | Type | Description |
|---|
| body | Supplier<T> | The task to wrap. |
Supplier<T> - A new Supplier that will run under the caller's tenant, or the original Supplier if the caller was unbound.isUnboundTenantFailure(Throwable ex)Checks if a given Throwable (or any of its causes) represents a failure from the UnboundTenantGate. This is the preferred way to detect a missing tenant binding error.
| Name | Type | Description |
|---|---|---|
ex | Throwable | The exception to inspect. |
boolean - true if the exception is an unbound tenant failure, false otherwise.Use DeploymentTenantBinding.run() to wrap logic in schedulers or startup hooks that need to read deployment-wide settings.
// In a scheduled task or startup hook
DeploymentTenantBinding.run(() -> {
// This code now runs with the default tenant context,
// allowing it to read deployment-wide settings from the database.
String installationId = readDeploymentConfiguration();
System.out.println("Installation ID: " + installationId);
});
Use withCallerTenant() when submitting a task to an executor from a tenant-aware context, such as an API request handler.
// In a request handler that has a tenant context
Supplier<String> task = () -> {
// This code will run on a separate thread
// but with the tenant context from the original request.
return processDataForTenant();
};
// The returned supplier can be passed to an async executor
CompletableFuture<String> future = CompletableFuture.supplyAsync(
DeploymentTenantBinding.withCallerTenant(task),
threadPoolExecutor
);
If a background task attempts to access tenant-scoped data without a proper tenant binding, the operation will fail with an exception.
UnboundTenantGate on the DataSource has intercepted a database read from a thread that has no tenant context.S2R-TENANCY-UNBOUND, which is available as the constant DeploymentTenantBinding.UNBOUND_ERROR_TOKEN.DeploymentTenantBinding.isUnboundTenantFailure(Throwable ex) method to reliably detect this condition in your error handling logic.WARNING
An unbound tenant failure indicates a logic error in the application—specifically, a missing tenant binding. It is not a transient database error. Callers must not retry, cache, or mask this failure. The underlying task must be fixed to use one of the DeploymentTenantBinding methods to establish the correct tenant context.