Tenant Resolution Caching
Overview
Tenant resolution uses two cache layers that work together:
- A distributed cache shared by all replicas (main source)
- A short-lived local memory cache (temporary fallback)
For framework users, this mechanism is transparent: tenant lookup APIs keep working the same way whether data comes from the distributed cache or the fallback cache.
The main objective is to keep tenant resolution fast and available during startup, rolling deployments, and cache rebuild phases.
Functional Behavior
Normal Behavior
In steady state, tenant data is read from distributed caches.
These caches cover three functional views of tenant data:
| Cache | Functional role |
|---|---|
Tenants_ResolutionByTenant |
Resolve tenant technical information (connection, environment, metadata) |
Tenants_ClusterVersionsByTenant |
Resolve cluster/version assignment for a tenant |
Tenants_ClustersByUser |
Resolve which tenants a user can access by cluster |
During Rebuild Windows
When distributed caches are not ready yet, the system temporarily falls back to local memory cache to keep requests responsive.
Read priority is:
- Distributed cache (when ready)
- Local fallback cache (while distributed cache is rebuilding)
- Remote tenant-management call (when needed)
This means users may continue to resolve tenants while rebuild is ongoing, without changing calling code.
Startup and Self-Healing
At startup, caches are rebuilt in the background with distributed coordination so only one replica rebuilds the same store at a time.
If a read path detects an incomplete distributed cache state, the system can request a global rebuild automatically (self-heal behavior).
Rebuild Triggers
The following operations can refresh tenant-resolution caches:
| Trigger | Tenants_ResolutionByTenant |
Tenants_ClusterVersionsByTenant |
Tenants_ClustersByUser |
|---|---|---|---|
| Platform startup | Full | Full | Full |
| Read-path self-heal | Full | Full | Full |
| Manual full rebuild | Full | Full | Full |
POST /api/v1/methods/resetalltenantcache |
Full | Full | Full |
| Manual rebuild by tenant IDs | Partial | Partial | — |
POST /api/v1/methods/resettenantcache (tenantId) |
Partial | Partial | — |
| Manual rebuild by user logins | — | — | Partial |
| Tenant changed | Partial | Partial | Partial |
| Data persistence changed | Partial | Partial | Partial |
| Database server changed | Partial | Partial | — |
| User-tenant assignment changed | Partial | Partial | Partial |
| Cluster version changed | Full | Full | Full |
POST /api/v1/methods/changedatabaseclusterversion |
Full | Full | Full |
POST /api/v1/methods/setdatabaseclusterversion |
Full | Full | Full |
| Additional tenant metadata changed | Full | — | — |
| Additional tenant property changed | Partial | — | — |
Definition reminder:
- Full rebuild: rebuilds the full store content.
- Partial rebuild: updates only impacted tenants or users.
Execution Modes for Full Rebuild
Full rebuild can run in two execution modes:
- Background mode: rebuild is enqueued and completed asynchronously.
- In-process mode: rebuild executes synchronously in the current flow.
From a functional standpoint:
- Background mode favors availability and shorter request latency.
- In-process mode is used when the caller needs immediate confirmation of rebuild outcome.
When lock contention occurs during rebuild, one store can succeed while another is postponed. This is handled as a partial completion scenario. Critical operations can enforce stricter behavior by failing the operation if rebuild completeness is required before continuing.
Monitoring
Use method GET /api/v1/methods/tenantscacherebuildingstatus to monitor rebuild progress.
Note
This method requires the TenantAdministration permission.
It returns three booleans:
| Property | Meaning |
|---|---|
IsTenantResolutionCacheRebuilding |
Resolution cache is rebuilding |
IsTenantClustersCacheRebuilding |
Cluster/version cache is rebuilding |
IsTenantUsersCacheRebuilding |
User access cache is rebuilding |
Typical operational use cases:
- Validate startup readiness
- Diagnose slow rebuilds
- Explain temporary fallback-cache usage
Configuration
{
"TenantsResolutionCacheTtlInMinutes": 5,
"TenantsResolutionCacheFullRebuildUseTaskRunnerByDefault": true,
"TenantsResolutionCachePartialRebuildUseTaskRunnerByDefault": false,
"DistributedStores": {
"PartialRebuildLockWaitTimeout": 30
}
}
| Setting | Default | Functional impact |
|---|---|---|
TenantsResolutionCacheTtlInMinutes |
5 | Local fallback cache lifetime |
TenantsResolutionCacheFullRebuildUseTaskRunnerByDefault |
true | Default mode for full rebuilds |
TenantsResolutionCachePartialRebuildUseTaskRunnerByDefault |
false | Default mode for partial rebuilds |
DistributedStores:PartialRebuildLockWaitTimeout |
30 | How many seconds a partial rebuild waits for a store lock held by a concurrent full rebuild before degrading (0 = no wait, single attempt) |
Tip
Higher TTL values reduce remote calls during long rebuild windows. Lower TTL values favor fresher local entries but may increase remote calls during rebuild.
What Framework Users Should Expect
- Tenant resolution remains available during cache rebuild phases.
- Most operations do not require manual intervention.
- Cache rebuilds are normally transparent, but can be monitored when needed.
- Some admin or migration operations may enforce stronger consistency and can fail fast if rebuild cannot be completed safely.
Troubleshooting
Rebuild seems stuck
- Query GET
/api/v1/methods/tenantscacherebuildingstatus. - Identify which cache remains in rebuilding state.
- Check distributed store lock settings and logs.
- Confirm the rebuild eventually completes after lock release.
Troubleshooting technique (state store and framework internals)
Administrators can investigate and recover directly from the distributed state store.
Checks to perform quickly:
- Verify completion markers exist for each store after rebuild:
Tenants_ResolutionByTenant_completedTenants_ClusterVersionsByTenant_completedTenants_ClustersByUser_completed
- Verify no stale rebuilding marker remains set unexpectedly:
Tenants_ResolutionByTenant_rebuildingTenants_ClusterVersionsByTenant_rebuildingTenants_ClustersByUser_rebuilding
- Verify the global rebuild marker is coherent with current state:
Tenants_GlobalRebuilding
- Check lock duration configuration for each distributed store (
DistributedStores:{StoreName}MaxLockDuration, default 120s). - Inspect Tenant Management logs for lock-acquire failures and partial rebuild completion warnings.
Fast recovery actions available to administrators:
- If a stale rebuilding marker remains after a crash/interruption, remove the stale key in state store.
- If completion markers are missing while stores are expected to be ready, trigger a manual full rebuild.
- If repeated lock contention occurs during version operations, re-run the operation during a quieter window.
Warning
Altering state store keys is an administrative operation. Apply only after checking logs and current rebuild status, and prefer forcing a clean full rebuild when in doubt.
API methods tied to database version operations
Two exposed APIs trigger full tenant cache rebuilds and are relevant for diagnostics:
POST /api/v1/methods/changedatabaseclusterversionPOST /api/v1/methods/setdatabaseclusterversion
Operational note:
setdatabaseclusterversionruns the full rebuild synchronously and waits for each store lock instead of failing immediately when another rebuild (for example a replica startup rebuild) already holds it. The wait is bounded by thebuildStoreTimeoutInSecondsrequest parameter (see below); if a lock is still unavailable after that wait, the call fails with a business error so the caller can retry.changedatabaseclusterversionalso triggers full rebuild but runs it asynchronously (task runner) and does not wait on store locks.
Synchronous rebuild lock wait (setdatabaseclusterversion)
setdatabaseclusterversion forces a synchronous full rebuild so tenant caches reflect the new cluster version before the call returns. To avoid failing immediately when a concurrent rebuild already holds a store lock (typically a replica that just restarted and rebuilds caches at startup, right after a deployment), the rebuild now waits for each store lock up to a bounded budget instead of attempting it once and giving up.
Request parameter:
buildStoreTimeoutInSeconds: maximum time to wait for each full store lock.> 0: wait up to this duration per store (WaitForLockAsync), then rebuild while holding the lock.0: no wait — a single non-blocking attempt (TryLockAsync), preserving the historical behavior.< 0: rejected with a business error before anything is persisted.
If a lock is still unavailable once the wait elapses, the call fails with a business error (HTTP 400) and the caller can retry. The three full stores are rebuilt sequentially, so the overall request duration is bounded by (buildStoreTimeoutInSeconds × number of full stores) + execution margin.
| Setting | Default | Functional impact |
|---|---|---|
DistributedStores:SynchronousRebuildExecutionMargin |
360 | Extra time, added on top of the per-store lock waits, allowed for the rebuild execution itself before the overall request is cancelled. Falls back to DistributedStores:GlobalRebuildMaxLockDuration when unset. |
Note
For full rebuilds, only the forced synchronous path (setdatabaseclusterversion) waits on locks. Startup, read-path self-heal, asynchronous, and changedatabaseclusterversion full rebuilds keep the non-blocking behavior: a store is simply skipped when its lock is held by another replica. Partial rebuilds have their own bounded wait (see below). While a store is rebuilding, its *_rebuilding / *_completed markers are updated only by the replica that holds the lock.
Partial rebuild lock wait (save flows)
Saving a tenant, a database server, a data persistence, or a user-tenant assignment triggers a partial rebuild of the impacted stores — synchronously, inside the save request, when TenantsResolutionCachePartialRebuildUseTaskRunnerByDefault is false (the default).
A partial rebuild never takes the store lock itself: it only refuses to write while a full rebuild holds it. A full rebuild dispatched just before (for example by a cluster or database creation, or a replica startup rebuild) is therefore a normal transient state. Instead of failing the save immediately in that case, the partial rebuild retries once per second until the lock is released, up to DistributedStores:PartialRebuildLockWaitTimeout seconds (default 30; 0 disables the wait and restores the historical single-attempt behavior).
- If the lock is released within the budget, the save completes normally — the only visible effect is the added wait.
- If the budget elapses while the lock is still held, the behavior is unchanged from before: the store is reported as
FailedToAcquireLockand the synchronous save fails with a business error (HTTP400) so the caller can retry, since committing without updating the cache would leave the just-saved data unresolvable. - Each impacted store has its own budget, and cancelling the request interrupts the wait immediately.
Manual cache reset endpoints
Two API methods are available for direct cache reset operations:
POST /api/v1/methods/resetalltenantcachePOST /api/v1/methods/resettenantcache(withtenantId)
resetalltenantcache accepts forceUsingBackend (bool, default false):
- In UI flows, the default behavior goes through task runner (
forceUsingBackend = false). - For urgent troubleshooting, administrators can force in-process backend execution with
forceUsingBackend = trueto trigger immediate full rebuild in the current request flow.
Practical recommendation:
- Start with task runner mode for normal operations.
- Use backend-forced mode only when fast, synchronous rebuild feedback is required for incident resolution.
- In Tenant Management UI, the ResetAllCache action currently also triggers an explicit
RepublishUnavailableTenantsSnapshotscall after reset completion.
Unavailable-tenants republication after reset
Tenant resolution cache rebuild and unavailable-tenants synchronization are operationally linked.
When these endpoints are used:
POST /api/v1/methods/resetalltenantcachePOST /api/v1/methods/resettenantcache
Tenant Management republishes unavailable-tenants snapshots after rebuild execution.
This keeps gateway blocking state aligned with rebuilt tenant-resolution data.
Unavailable snapshots include tenants whose data persistence is not Running, and also tenants whose TenantStatus is Inactive even if their persistence is still Running.
Manual republish endpoint
For explicit operator control, Tenant Management exposes:
POST /api/v1/methods/republishunavailabletenantssnapshots
Use this endpoint when:
- You need immediate re-synchronization without running a full cache reset.
- You suspect pub/sub message loss or transient Redis disruption.
- You want to validate end-to-end snapshot propagation during incident handling.
Operational note:
- Gateway unavailable-tenants synchronization uses Redis pub/sub in normal mode and automatically falls back to periodic pull refresh from Tenant Management when pub/sub synchronization is unavailable.
If gateway returns a 503 page for blocked tenants, note that this page can be customized in production. See Custom status code pages in production.
Operational runbook: cache rebuild and snapshot re-sync
- Trigger
resetalltenantcache(orresettenantcachefor targeted impact). - Wait until
tenantscacherebuildingstatusno longer reports rebuilding for impacted stores. - Check gateway metric
neos_unavailable_tenantfor expected tenant/cluster labels, includingdata_persistence_stateandtenant_status. - If state mismatch persists, call
republishunavailabletenantssnapshots. - Re-test request behavior (
503for blocked tenants, success for available tenants).
Request Flow (Functional View)
flowchart TD
A["Tenant Resolution Request"] --> B{"Distributed Cache Ready?"}
B -->|Yes| C["Read Distributed Cache"]
B -->|No| D{"Local Fallback Has Valid Entry?"}
D -->|Yes| E["Return Local Cached Value"]
D -->|No| F["Call Tenant Management"]
F --> G["Populate Local Fallback Cache"]
G --> H["Return Value"]
C --> H
E --> H
style A fill:#0f766e,stroke:#0d9488,color:#fff
style B fill:#1d4ed8,stroke:#2563eb,color:#fff
style D fill:#1d4ed8,stroke:#2563eb,color:#fff
style C fill:#15803d,stroke:#16a34a,color:#fff
style E fill:#15803d,stroke:#16a34a,color:#fff
style F fill:#b45309,stroke:#d97706,color:#fff
style G fill:#b45309,stroke:#d97706,color:#fff
style H fill:#15803d,stroke:#16a34a,color:#fff