[ 
https://issues.apache.org/jira/browse/HDDS-17216?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
 ]

Siyao Meng updated HDDS-17216:
------------------------------
    Attachment: MC-2-keep-s3-secret-cache-entries-until-own-index-flushed.patch

> S3 secret cache cleanup can remove the entry of an unflushed transaction, so 
> OMs can commit different S3SecretTable contents
> ----------------------------------------------------------------------------------------------------------------------------
>
>                 Key: HDDS-17216
>                 URL: https://issues.apache.org/jira/browse/HDDS-17216
>             Project: Apache Ozone
>          Issue Type: Bug
>            Reporter: Siyao Meng
>            Priority: Major
>         Attachments: 
> MC-2-keep-s3-secret-cache-entries-until-own-index-flushed.patch
>
>
> h3. Mechanism
> Each write to an S3 secret puts an entry carrying its transaction index into 
> {{S3InMemoryCache}}. After the double buffer has committed a batch, the flush 
> thread calls {{S3InMemoryCache.clearCache}} with the batch's indexes, which 
> removes the entries carrying those indexes, while the apply thread keeps 
> applying later transactions. A read that misses the cache loads the committed 
> row and caches it with index 0. The cache is consistent as long as every 
> applied but unflushed write keeps its entry until its own index is flushed. 
> Three places do not ensure that:
> # {{S3RevokeSecretRequest}} replaces the entry with 
> {{S3SecretValue.deleted()}}, which keeps the index of the entry it replaces 
> instead of the revoke's index. When the revoke is applied after the batch 
> holding the previous write to that access ID was committed and before that 
> batch's cleanup, the cleanup removes the deleted entry although the revoke is 
> not flushed.
> # {{OMTenantRevokeUserAccessIdRequest}} calls {{invalidateCacheEntry}}, which 
> only replaces an entry that is already cached ({{computeIfPresent}}), and 
> calls it without the access ID's {{S3_SECRET_LOCK}}. If the access ID has no 
> cache entry, the tenant revoke leaves none.
> # {{clearCache}} maps each cached index to its access ID in a first pass and 
> removes each mapped access ID in a second pass, without checking the entry's 
> index again. {{S3SecretLockedManager.clearS3Cache}} holds the 
> {{S3_SECRET_LOCK}} stripe of the key "cache", not of the access IDs, so a 
> write to the access ID can be applied between the two passes, and its entry 
> is removed.
> In each case a read that misses the cache before the newer transaction is 
> flushed caches the older row with index 0, which the cleanup of the newer 
> transaction does not match. After that flush, the OM's cache entry for the 
> access ID no longer matches its committed row.
> Which OMs are affected depends on when each OM's double buffer flush runs 
> relative to its apply thread. That timing is local to each OM and is not part 
> of the Ratis log.
> h3. Impact
> * *OMs can commit different S3SecretTable contents.* Two OMs apply the same 
> log create@1, revoke@2, create@3 for one access ID. On an OM that flushes all 
> three in one batch, create@3 ({{GetS3Secret}}) returns OK and commits the new 
> secret. On an OM that applies revoke@2 between the commit and the cleanup of 
> batch [1], create@3 reads the older row, returns {{S3_SECRET_ALREADY_EXISTS}} 
> and writes nothing, so after both OMs have flushed index 3 one holds the new 
> secret and the other holds no row for the access ID. Nothing compares or 
> repairs the tables of OMs at the same applied index, so the difference 
> survives restarts. A later {{GetS3Secret}} or {{SetS3Secret}} for that access 
> ID can likewise get a different result on an OM whose cache entry no longer 
> matches its committed row than on the others.
> * *Cache entry and committed row disagree.* After path 3 the DB holds the 
> newer write, but the OM's cache entry for the access ID does not match its 
> committed row until the OM restarts or the access ID is written again.
> Affected versions: the deleted entries and the index based cleanup (paths 1 
> and 3) exist since HDDS-9444, path 2 since the tenant revoke request. The 
> missing cleanup registration described under Patch is on master only, since 
> HDDS-16643.
> h3. Reproduction
> There is no standalone reproduction attached. The regression tests are in the 
> patch, in the existing {{TestS3GetSecretRequest}}: real 
> {{OmMetadataManagerImpl}} (RocksDB), {{S3SecretLockedManager}} over 
> {{S3SecretManagerImpl}} with the in-memory cache, requests applied through 
> {{preExecute}} and {{validateAndUpdateCache}}, and each flush done in two 
> steps by the test (commit the responses with {{checkAndUpdateDB}}, then 
> {{clearS3Cache}} for the responses that registered the S3 secret table), so 
> the interleavings above become a plain sequence of calls. All four fail 
> without the change:
> * {{testRevokeKeptInCacheWhenEarlierIndexIsCleanedUp}} (path 1): commit [1], 
> apply revoke@2, clean up [1]; expects the deleted entry with index 2 to stay 
> until the revoke is flushed.
> * {{testCreateAfterRevokeWhenEarlierIndexIsCleanedUp}} (path 1, the diverging 
> OM): the same, then {{GetS3Secret}}@3; fails with "expected: OK but was: 
> S3_SECRET_ALREADY_EXISTS".
> * {{testTenantRevokeOfUncachedSecretIsKeptInCache}} (path 2): the secret is 
> committed and not cached, tenant revoke@1 is applied; expects a deleted entry 
> until the revoke is flushed.
> * {{testCacheCleanupKeepsEntryReplacedDuringCleanup}} (path 3): 
> {{SetS3Secret}}@2 is applied at the moment {{clearCache([1])}} reads the 
> index of the create's entry (a Mockito spy on that cache value); expects the 
> set's entry to stay.
> The two OM case and paths 1 and 3 were also run through the flush method of a 
> real {{OzoneManagerDoubleBuffer}} (the one its daemon calls) on two OM stacks 
> in one JVM, with the results described under Impact. These runs show that the 
> interleavings exist, not how often they occur.
> h3. Patch
> [^MC-2-keep-s3-secret-cache-entries-until-own-index-flushed.patch], against 
> df04bc0e309f625a7dcdcd60790a42f34e2743d9. It also applies to master at 
> 2cb1b6dd4e9.
> The patch keeps every cache entry until its own transaction is flushed:
> * {{S3SecretCache.invalidate(id, transactionLogIndex)}} (new, with a 
> {{S3SecretManager.invalidateCacheEntry}} counterpart) always puts a deleted 
> entry carrying the revoking transaction's index, whether or not the access ID 
> is cached. {{S3RevokeSecretRequest}} passes {{context.getIndex()}}. 
> {{OMTenantRevokeUserAccessIdRequest}} passes its {{transactionLogIndex}} and 
> now calls it under the access ID's lock through {{doUnderLock}}, as 
> {{OMTenantAssignUserAccessIdRequest}} already does. The index is the Ratis 
> index every OM applies the entry with, so no request or proto change is 
> needed.
> * {{S3InMemoryCache.clearCache}} removes an entry only if it still carries 
> the flushed index ({{computeIfPresent}}).
> * The new {{invalidateCacheEntry}} records the S3 secret table with 
> {{TableCacheUpdateTracker}}, as {{updateCache}} does, so the double buffer 
> runs the cleanup for the revoke's index. Since HDDS-16643 a revoke whose 
> secret was already cached did not register that table.
> For stores with batch support (the default RocksDB store), every unflushed 
> write is then represented by an entry that no earlier cleanup removes, and 
> reads and writes of an access ID are serialized by its lock, so a cache miss 
> means the committed row is current and an entry loaded with index 0 holds it. 
> On stores without batch support the store is written at flush time; that is 
> addressed by HDDS-17214, which also edits {{S3RevokeSecretRequest}}, 
> {{OMTenantRevokeUserAccessIdRequest}} and {{TestS3GetSecretRequest}}, so the 
> two patches need a rebase whichever lands second. The existing one argument 
> {{invalidate}} is kept unchanged for its remaining caller, the flush time 
> {{S3SecretManagerImpl.revokeSecret}} of those stores.
> The lock in {{OMTenantRevokeUserAccessIdRequest}} is argued from the code (a 
> read that missed the cache could otherwise overwrite the new deleted entry 
> with the committed row) and is not covered by a failing test.
> The patch includes the {{S3SecretManagerImpl.getSecretString}} change 
> proposed in HDDS-17200 so that it is complete on its own; drop that hunk if 
> HDDS-17200 is committed first.
> Without the races the behaviour is unchanged, except that a deleted entry is 
> now removed when the revoke itself is flushed instead of when the replaced 
> entry's index is flushed.
> With the patch the four tests pass, the ozone-manager unit suites for S3 
> requests and responses, the double buffer, the delegation token secret 
> manager and multi tenancy pass (315 tests), {{TestMultiTenantVolume}}, 
> {{TestSecureOzoneCluster.testGetSetRevokeS3Secret}} and 
> {{TestSecureOzoneRpcClient.testS3Auth}} pass on a mini cluster (7 tests), and 
> checkstyle is clean.
> Found by TLA+ model checking and code review of the OM S3 secret cache and 
> external S3 secret store paths under HDDS-15926, on commit 
> df04bc0e309f625a7dcdcd60790a42f34e2743d9. Checked against HDDS issues and 
> apache/ozone pull requests for duplicates before filing. Related but not 
> duplicates: HDDS-17200 (deleted cache entries in {{getSecretString}}), 
> HDDS-9444 (introduced the index based cleanup and the deleted entries), 
> HDDS-16643 (removed the static cleanup table declarations) and HDDS-5358 (an 
> earlier invalidation fix). 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