Siyao Meng created HDDS-16856:
---------------------------------

             Summary: Tenant delete racing a user assign succeeds on a 
non-empty tenant and leaves an orphan accessId that stops OM restart
                 Key: HDDS-16856
                 URL: https://issues.apache.org/jira/browse/HDDS-16856
             Project: Apache Ozone
          Issue Type: Bug
            Reporter: Siyao Meng
         Attachments: 
MC-2-check-tenant-empty-under-authorizer-lock-on-delete.patch, 
TestBugMC2TenantDeleteRacesUserAssign.java, 
TestBugMC2TenantDeleteRacesUserAssignOverRpc.java

h3. Mechanism
{{OMTenantDeleteRequest.preExecute}} checks 
{{OMMultiTenantManager.isTenantEmpty}} (the in memory tenant cache) before it 
takes the authorizer write lock, and {{validateAndUpdateCache}} does not check 
it again since HDDS-13361 moved the check out of the apply step. 
{{OMTenantAssignUserAccessIdRequest}} holds the same lock from its 
{{preExecute}} until its apply, and the tenant cache shows the new user only 
after that apply. A delete that arrives while an assignment holds the lock 
therefore passes the check, waits for the lock (up to 1 second), deletes the 
tenant in Ranger, and is applied right after the assignment. The apply removes 
the {{tenantStateTable}} row and keeps the new {{tenantAccessIdTable}} row, on 
every OM.

{{OMMultiTenantManagerImpl.loadTenantCacheFromDB}} treats an accessId whose 
tenant is missing as a metadata inconsistency and throws 
{{NullPointerException: OmDBTenantState should have existed for <tenant>}} from 
the constructor. {{OzoneManager.instantiateServices}} builds the manager at 
startup, on restart and on snapshot install, so each of these fails while 
multi-tenancy is enabled.

h3. Trigger
# An Ozone admin or a tenant admin runs {{ozone tenant user assign <user> 
--tenant <tenant>}} on a tenant with no users.
# While that request is between its {{preExecute}} and its apply, an Ozone 
admin runs {{ozone tenant delete <tenant>}}.

Both RPCs must hit the same tenant within about a second. No fault is needed.

h3. Impact
* The delete reports success for a non-empty tenant, and the leftover accessId 
row is replicated to every OM.
* Any OM that restarts, or installs a Ratis snapshot, fails to start with 
multi-tenancy enabled. OMs that are already running keep working until then.
* There is no repair command for the tenant tables. Disabling 
{{ozone.om.multitenancy.enabled}} lets the OM start (code reading, not run), 
without tenant functionality.
* Affects releases that contain HDDS-13361 (2.1.0 onwards). Before it the check 
ran in the apply step and rejected the delete.

h3. Reproduction
PASS on unmodified source. [^TestBugMC2TenantDeleteRacesUserAssignOverRpc.java] 
({{hadoop-ozone/ozone-manager}}) runs one real OM ({{OmTestManagers}}: single 
node Ratis, real apply and double buffer) through the public {{ObjectStore}} 
API, with the in memory access controller used by 
{{ozone.om.tenant.dev.skip.ranger}} in place of Ranger. The only controlled 
step is that the test holds the tenant volume's lock, which keeps the 
assignment's apply waiting and stands in for its commit and apply taking a few 
hundred ms. The delete is sent once the assignment is in the Raft log, and the 
volume lock is released once the delete waits for the authorizer lock (both by 
polling). Then the test calls {{OzoneManager.stop()}} and {{restart()}}. Output 
on unmodified source:
{noformat}
assign -> OK, delete -> OK
tenantStateTable[tenant1] exists = false, tenantAccessIdTable[tenant1$user1] 
exists = true
restart -> java.lang.NullPointerException: OmDBTenantState should have existed 
for tenant1
{noformat}
A control test assigns first, then deletes, and gets "Tenant 'tenant1' is not 
empty", after which the restart succeeds. A passing test means the defect is 
present.

[^TestBugMC2TenantDeleteRacesUserAssign.java] is a lower level variant that 
runs {{preExecute}} on two threads and the apply in log order by hand, and 
builds {{OMMultiTenantManagerImpl}} over the reopened DB.

h3. Suggested fix
The patch prevents new orphan rows. A cluster that already has one still needs 
a way out, for example {{loadTenantCacheFromDB}} logging and skipping such a 
row instead of refusing to start, or an {{ozone repair om}} command. That 
changes deliberate fail fast behaviour, so it is left for a separate change.

h3. Patch
[^MC-2-check-tenant-empty-under-authorizer-lock-on-delete.patch], against 
a6b7bdb937109ba2893688b41a89470bc95c88cf. It also applies to master at 
b0aa6475b78.

The emptiness check moves into the locked section of 
{{OMTenantDeleteRequest.preExecute}}, ahead of the Ranger delete, so a failed 
check releases the lock through the existing {{catch}}. A delete that waited 
for an assignment now sees its user and fails with {{TENANT_NOT_EMPTY}} before 
touching Ranger. Message, result code and the HDDS-13361 property that the 
check runs before any Ranger change are kept. No apply side check is added: a 
leader accepts tenant requests only when it is ready (all earlier entries 
applied), and each tenant request holds the lock until its own apply, so the 
check under the lock already sees every earlier tenant entry. An apply side 
check would also change replicated behaviour without a layout feature. For a 
delete that fails anyway, the order of checks changes: a caller without 
WRITE_ACL on the tenant volume now gets {{PERMISSION_DENIED}} before 
{{TENANT_NOT_EMPTY}}, and a delete of a non-empty tenant can get {{TIMEOUT}} 
while another tenant request holds the lock.

Covered by {{preExecuteRejectsTenantAssignedWhileWaitingForLock}} and the 
control {{preExecuteDeletesEmptyTenantAndKeepsLock}} in the existing 
{{TestOMTenantDeleteRequest}}. Without the change the first fails because no 
exception is thrown. With it the tenant request, S3 secret, multi tenant 
manager, authorizer lock and execution flow suites (50 tests) pass, as do 
{{TestMultiTenantVolume}} and {{TestOzoneTenantShell}} on a mini cluster (11 
tests). Checkstyle is clean. With the patch the reproduction's race test fails 
at its delete assertion (the delete is rejected because the tenant is not 
empty, both rows remain and the restart succeeds), and its control still passes.

Suggested priority: Major. No OM can restart or install a snapshot with 
multi-tenancy enabled once an orphan row exists, and there is no repair 
command. The trigger needs an Ozone admin to delete a tenant while a user is 
being assigned to that same tenant, within about a second, and running OMs keep 
serving until they restart.

Found by TLA+ model checking and code review of the OM multi-tenancy and Ranger 
sync paths under HDDS-15926, on commit 
a6b7bdb937109ba2893688b41a89470bc95c88cf. Checked against HDDS issues and 
apache/ozone pull requests for duplicates before filing. The attached patch is 
a proposal for review. Generated with Specula (Claude Opus 5.5).



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to