Siyao Meng created HDDS-17216:
---------------------------------
Summary: 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
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]