core: Credential Vending Refactor#3699
Conversation
| Supplier<StorageAccessConfig> loader) { | ||
| long maxCacheDurationMs = maxCacheDurationMs(realmConfig); | ||
| return cache | ||
| .get( |
There was a problem hiding this comment.
The Supplier in this .get method in Caffeine is supposed to be fast, but I believe in our case it may hit network (e.g. STS), which is not desirable... It might be best to go back to LoadingCache, which allows loaders to be blocking.
There was a problem hiding this comment.
I can revert, but I have to say my limited (ai-assisted) research convinced me that there's no point in using a LoadingCache with a dummy loader if you're always calling a get with an explicit loader. The behavior should be the same with either. might be wrong, though. happy to trust you on this.
There was a problem hiding this comment.
I agree, that the old code was using LoadingCache in a strange way... yet, I believe LoadingCache in itself is the right tool here 😅
There was a problem hiding this comment.
This is related to using the "key" as input for Storage Config generation. If we could achieve that, LoadingCache would fit naturally.
There was a problem hiding this comment.
I believe this comment thread still applies... WDYT?
There was a problem hiding this comment.
Don't you have a "chicken and egg" problem there? you need StorageIntegration for cache key first... Or do you mean we can create them twice effectively?
There was a problem hiding this comment.
Sorry, I do not see the "chicken", only the "egg" 😉 which is the key. It is created first, then we go through the cache, then the loader function takes the key, creates a light-weight AwsCredentialsStorageIntegration and feeds the data from the key into it.
There was a problem hiding this comment.
I took a shot at it. basically added a load method to the cache key interface. each key carries it's own loader method and that's what loader method of the LoadingCache is calling. let me know what you think.
There was a problem hiding this comment.
Sorry, not quite there yet from my POV 😅
So we derive storage config like this: entity -> PolarisStorageConfigurationInfo -> (1) PolarisStorageIntegration -> include CredentialVendingContext -> (cache) -> StorageAccessConfig
I believe we need to first combine PolarisStorageConfigurationInfo and CredentialVendingContext into a cache key. This requires (2) a concrete PolarisStorageIntegration (same as step 1 above). However, the key should only contain the data that acts as direct input into the config generation. So, it should not hold PolarisStorageIntegration. The cache loader function will re-derive (3) PolarisStorageIntegration from the cache key at load time (if it happens). This is a fast operation as far as I understand.
This is a fine point, I admit, but I think it is critical for caching correctness and indirectly for future improvement in this code.
There was a problem hiding this comment.
you're right, that was a poor implementation, I shouldn't have included integration itself as an aux. still I don't like the idea of re-deriving entire PolarisStorageIntegration object. the reason is that the first time it's instantiated, the point it to build a key, the second time around you build a credential. inputs for each are not necessarily identical, I changed session name derivation in AWS to highlight it. I switched to deriving session name during cache key generation and then simply using resolved string in the cache key directly. in other words, cache key generation needs entire CredentialVendingContext (which includes principal name), while credential compute needs only the session name string that's already resolved. Reinstantiating PolarisStorageIntegration with entire CredentialVendingContext is therefore wasteful.
I switched to providing fields like stsClientProvider and credentialResolver to cache key as aux fields instead of integration itself. the key then calls a static compute method on the integration which takes in only what it absolutely needs and params are coming from the fields in the cache key. let me know what you think.
| key, | ||
| k -> { | ||
| LOGGER.atDebug().log("StorageCredentialCache::load"); | ||
| StorageAccessConfig accessConfig = loader.get(); |
There was a problem hiding this comment.
Ideally, I think this should be something like loader.get(key). That is to ensure we produce StorageAccessConfig from exactly the same data that is used as the cache key, i.e. any change in key results in regeneration of StorageAccessConfig and any two StorageAccessConfig objects are logically equivalent if their keys match. WDYT?
There was a problem hiding this comment.
while I agree that it would be ideal, we probably still wouldn't be able to make a loader dependent 100% on the key, for example the key only contains catalogId, rather than a resolved polaris entity. In the current codebase, while key is available in the loader, it's only partially used. My thinking was that unless we make loader fully dependent on the key it makes little sense to make the code more confusing when some of the params depend on the key and others don't.
There was a problem hiding this comment.
Re: confusing code - TBH, IMHO the current (on main) code is very confusing because the reader has to think very carefully about which parameters go through the cache and which go outside and whether they are consistent 😅
I think we're pretty close to "ideal" here. The "entity" this code deals with, has already been "found" and loaded, we only need to extract the storage configuration JSON from it in front of the cache.
PolarisStorageIntegration is loaded based on the storage config, which can be done past the cache.
In each call, the key will be used in full as required for a particular storage backend + feature flags. Different calls will have different data in their keys, of course, but that is the idea :) It will allow one cache per JVM to handle all use cases. We could even have different expiry strategies per storage type/config, but that's looking to far into the future 😅
Would renaming StorageCredentialCacheKey to StorageAccessConfigParameters make the code clearer?
|
@tokoko : it looks like this PR has a lot of conflicts now... CI will not run because of that 😅 |
|
@dimas-b that's fine, this one was more for you to take a look 😆 I'll take care of the conflicts and bunch of other stuff that's still unfinished |
|
This PR is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 5 days. |
|
@tokoko : The general approach in this PR is fine from my POV 👍 If you have time I think we can iterate. |
|
yeah, I'm gonna rebase and make it ready for a review this weekend. |
f7d7c81 to
9e185c0
Compare
dimas-b
left a comment
There was a problem hiding this comment.
I like the idea of using application-scoped beans for producing StorageAccessConfig in runtime.
Posting some preliminary comments below. I did not review every single line, but the general approach LGTM 👍
|
Thanks @tokoko for working on it. Is there a dev mailing thread for it? If not, I'd recommend to have one for better visibility. It is generally a good practice to engage with the community earlier to avoid last minute surprises. |
|
@flyrain : This PR was mentioned on the S3 Tables thread: https://lists.apache.org/thread/c5nc7144zlsb5vdjmpsgbc0h0g5no9c0 Do you think we need a separate discussion for this? |
|
@flyrain sure, I'll make a bit more changes, change PR description to be more accurate with latest changes and send out an email on the devlist. For me, the main goal of this PR is to demo the overall direction and build consensus on it. P.S. I'd be okay to split these changes up over a series of smaller PRs if people think that's more appropriate. |
ab1423f to
799f23b
Compare
|
@dennishuo @dimas-b I changed the PR and reinstated PolarisCredentialVendor SPI, but the thing that bothers me now is that PolarisCredentialVendor.getStorageAccessConfig and PolarisStorageIntegration.getSubscopedCreds signatures are converging to essentially the same thing — both take locations, actions, refresh endpoint, context and return StorageAccessConfig. I think the current design is coupling PolarisStorageIntegrationProvider (which effectively includes PolarisStorageIntegration) and PolarisCredentialVendor too much. Having two SPIs that mirror each other adds complexity without buying us anything. The root cause is that PolarisCredentialVendor is trying to be a unified SPI that wraps both "get the right integration" and "vend credentials" into one call. So what if instead of putting PolarisStorageIntegrationProvider behind PolarisCredentialVendor, we make PolarisStorageIntegrationProvider a first-class citizen? Planned flow will look like this:
CreateCatalog:
This change would give us one SPI (PolarisStorageIntegrationProvider) instead of two that mirror each other and also keep IntegrationPersistence storage methods around for custom impls that need them. Curious what you guys think... |
|
Sorry for the delay responding to this! I've been digesting this more and I think there's a bigger convergence we can align with. I agree with what you said that if we only reintroduce PolarisCredentialVendor in the proposed way, it's a bit confusing whether a The TL;DR: We should incrementally try to converge with how the analogous ConnectionConfig flow works for federation, especially since the sigv4-to-Glue federation credentials are pretty much exactly the same as sigv4-to-s3 storage credentials. Your proposal does seem to align somewhat, so I'd say go ahead and try to have StorageIntegration be the first-class entrypoint. We should consider accordingly moving the items related to request scope ( Extended analysis: Very abstractly, there are these four things:
The fact that StorageIntegrationProvider at What I remembered is that we actually already did try to redesign the flow of such credential-handling to accommodate this same concept in a more explicit way. See:
Reposting @XJDKC 's diagram here:
In this world:
I guess the rough equivalences are:
One key difference in behavior is in the ConnectionConfig path, I guess we ended up putting the following all the way in This is in contrast to putting the "allocate" hook into the inner transaction-aware block in PolarisMetaStoreManager. So it doesn't support as good of transactional semantics as how we handle StorageConfig, but on the plus side, it forces the referential service-identity info to be first-class in the Overall, fully merging the Storage credential flow into the Connection credential flow is probably too big of an undertaking to do here, and maybe we also want to adjust the Connection credential code at the same time, but I think any incremental refactoring to make it structurally compatible is probably the right direction. All this to say, yes, I think your proposal is somewhat structurally compatible, with a couple key points:
|
|
@dennishuo thanks for the explanation. some sort of convergence between normal credential vending and federation makes sense. I'm not too familiar with federation codebase so I'll leave that out of scope for now, but I'll keep that in mind. that would probably make something like #4052 cleaner. I went ahead and made quite a few changes. technically StorageIntegration instances returned by I'm thinking of trimming PolarisStorageIntegration itself next:
|
I did not touch From my POV that method can be deleted, but I'd appreciate it if @dennishuo could share his view on that. |
|
The current state of this PR looks reasonable to me. If @dennishuo is ok with the general direction, I'll take a deeper look 😉 |
|
Sorry for the delays, took a some deep diving archaeology to assess the impact of all the suggested changes downstream, though I only just realized earlier this evening that you were just saying you'd remove For the current PR: Removing For the suggested changes: Removing getStorageIdentiferOrId - workable for the customization cases I know of, but since this is also used by customizations of MetaStoreManager, if we're going to have a dev mailing list vote for Removing The main deciding factor would be starting from the core SPIs called out in https://lists.apache.org/thread/0nj24zro7kyctqfnlml08ppo7zs9xcqs and going one level deeper into their arguments and if the interaction between those core classes and the argument (including a PolarisStorageIntegration) is part of the core SPI then we should have it in the vote. Regarding removal of read vs write and just having one locations list - I'm not sure I understand this proposal. Subscoped policy strings give different sets of allowed locations for read vs write locations. @dimas-b what was the suggestion about no longer differentiating between read vs write? I must have missed this |
|
@dennishuo thanks for the detailed response. If we're going in the ML vote direction, we might as well bundle all SPI changes in a single vote. we can then either remove everything that's no longer needed or I can simply deprecate them as part of this PR (storage id for example) and push removal for a follow up if you think that'll make adjusting to the changes easier for you. let me know. to summarize:
The only thing left is to agree on
let me try to make the case for changing current params from: to the motivation for the change stems from two observations:
I get that this will introduce even more changes and headache for downstream implementations. It's just that if we're breaking things anyway and trying to build consensus around new SPI design, it's unlikely we will have better opportunity in the future for these sorts of changes. |
|
@tokoko +1 to bundling up SPI changes for a vote. For the locations, I'm okay with refactoring a more advanced way to set different storage actions for different sets of locations. Keeping the muscle under the hood for policy-scoping logic to handle different sets of actions for different locations will be important to avoid that door inadvertently closing with other architectural decisions that start assuming a uniform action-set, even if we failed to properly capitalize on it so far. The original use case in mind was in the case of cloned tables or other tables that originate from some legacy dataset in an "old" location that is no longer the new write-location, it's important to keep the legacy location "read-only" and only provide write access to the new location. It's a layer of protection against misbehaving client-side engines that may unexpectedly delete things from the legacy dataset location, for example. In general with the future of better intent-conveyance in the APIs (e.g. loadTable for a subsequent write vs just for a SELECT), we'd also want to better distinguish whether to include write permissions at all. Some finer-grained operations, such as things that only want to compact a snapshot might only need write access to the metadata/ directory but not the data/ directory. With better specification of intent in the API spec, there's some discussion of providing finer-grained privileges under TABLE_WRITE_PROPERTIES that distinguish between data-writes vs metadata-writes, and even within metadata writes, distinguishing between some sensitive ones like In some cases you might even want to allow DELETE but not READ on a subdirectory. For example, you might run a shared snapshot-expiry maintenance engine on a highly sensitive dataset. It would need read + write on |
|
Re: voting for SPI changes - I think it would be fine, however, I'd suggest to have concrete changes in a PR first, then review the PR, then vote. This is because voting is supposed to be on specific changes. If some adjustments are called for during a vote, the whole process has to restart, so we'd better sort out all concerns before the vote even starts 😅 That said, from my personal POV posting a notice about changes on the |
# Conflicts: # persistence/nosql/persistence/metastore/src/main/java/org/apache/polaris/persistence/nosql/metastore/NoSqlMetaStore.java # persistence/nosql/persistence/metastore/src/main/java/org/apache/polaris/persistence/nosql/metastore/NoSqlMetaStoreManager.java # persistence/nosql/persistence/metastore/src/main/java/org/apache/polaris/persistence/nosql/metastore/NonFunctionalBasePersistence.java # polaris-core/src/main/java/org/apache/polaris/core/storage/gcp/GcpCredentialsStorageIntegration.java # polaris-core/src/test/java/org/apache/polaris/core/storage/aws/AwsCredentialsStorageIntegrationTest.java # polaris-core/src/test/java/org/apache/polaris/core/storage/gcp/GcpCredentialsStorageIntegrationTest.java
Two tests imported from main called the removed getSubscopedCreds(...) via a 2-arg constructor; rewrite them on the existing 3-arg test constructor and the generateStorageAccessConfig API.
dimas-b
left a comment
There was a problem hiding this comment.
LGTM overall 👍 Please fix conflicts 🙂
| key, | ||
| k -> { | ||
| LOGGER.atDebug().log("StorageCredentialCache::load"); | ||
| StorageAccessConfig accessConfig = loader.get(); |
# Conflicts: # persistence/relational-jdbc/src/main/java/org/apache/polaris/persistence/relational/jdbc/JdbcBasePersistenceImpl.java # polaris-core/src/main/java/org/apache/polaris/core/persistence/transactional/TreeMapTransactionalPersistenceImpl.java

StorageCredentialCacheby moving orchestration logic intoStorageAccessConfigProvider