-
Notifications
You must be signed in to change notification settings - Fork 338
NoSQL: Metastore implementation #3237
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
dennishuo
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Still want to take a closer look at the rename/move logic, but the basic updateIfNotChanged flow looks good to me.
Left some initial comments. In general, it would be nice to expand javadoc comments whenever there are implicit format/parsing conventions defined in code, such as in the GrantTriplet (or class javadoc can link to wherever it's explained elsewhere if I just didn't see the file where it's documented).
| return identifier(namespace.levels()); | ||
| } | ||
|
|
||
| static ContentIdentifier identifierFromLocationString(String locationString) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All the other factory methods are mostly self-explanatory, but this one could use some javadocs explaining what locationString is, expected format, etc.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yup, added
| var off = -1; | ||
| for (var i = 0; i < len; i++) { | ||
| var c = locationString.charAt(i); | ||
| checkArgument(c >= ' ', "Control characters are forbidden in locations"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this really intended to support all chars with ascii codepoint > ' ' or is there a more intuitive regex? In particular, though I don't know all ascii codes offhand, a cursory search seems to indicate for example that 127 is the "Delete control character" even though it comes after all the basic symbols and letters.
Maybe Character.isJavaIdentifierPart will help provide a better standard constrained set while also making it easier to reason about when reading the code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This identifierFromLocationString is used (only) for location-overlap handling, assuming that the storage locations are already validated. Adding assumptions here about the actual format of object-store specific conventions or valid file-path chars feels a bit too risky?
| for (var i = 0; i < len; i++) { | ||
| var c = locationString.charAt(i); | ||
| checkArgument(c >= ' ', "Control characters are forbidden in locations"); | ||
| if (c == '/' || c == '\\') { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this for linux vs windows path separators? Can we just use java.nio.Path.of(...).iterator() instead?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Similar to the above comment: we'd assume that it's a file-system path (NIO Path delegates to file-system implementations), but object-store conventions do not match the the restrictions/validations.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That raises an interesting point - we'd ideally be able to incorporate storage-type-specific canonicalization logic.
In this code for example, presumably the \\ is specifically "windows local filesystem" use cases (which really should only be very limited test/dev scenarios where overlap-checks probably aren't relevant anyways; any "sensitive" production use cases would be really weird to run on a single-node Windows environment with engine and Polaris catalog in the same place, or else some NFS mount).
But for a nio Filesystem, it's actually correct to canonicalize with real filesystem syntax, such as collapsing "foo/./././././bar" into "foo/bar" or "foo/bar/.." becoming "foo" when it comes to "overlap detection" because overlap detection is precisely supposed to prevent security issues by sharing directories. So it becomes important to avoid creative syntax circumventing our check.
And yet, as mentioned before, the "local filesystem" use cases where paths have fancy syntactic interpretations also happen to be the cases where overlap-check probably doesn't actually matter.
I guess it's fine to leave as-is for now, but custom parsing of paths is probably not something we want to keep as ad-hoc inline logic here in the long-term. It might be worth adding a comment or filing a github issue to call out that this logic isn't necessarily authoritative for any future developers who might read it as gospel.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is probably the wrong place to introduce a canonization. This piece of the code just deals with what's been configured. The configured values should have already been canonicalized.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Overall, we should probably revisit the location-overlap-checks.
| new CreateCatalogResult(catalog, catalogAdminRole)); | ||
| } | ||
| // retry | ||
| return new ChangeResult.NoChange<>(new CreateCatalogResult(ENTITY_ALREADY_EXISTS, null)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure this fallthrough should exist; if returning ENTITY_ALREADY_EXISTS it's interpreted by the upper stack to mean the same name is already occupied but that this isn't actually an idempotent retry attempt (e.g. it had a different id).
Looks like line 232 already captures that case.
If we reach this line does it mean we're accruing a bunch of updates into the updatable indexes that we'll just discard when it comes time to "commit"?
If we want consistent behavior with the other metastore impls we would basically need a way to return NoChange but with a success CreateCatalogResult maybe in an else branch on line 230 (else if (catalogObj.stableId() == catalog.getId())).
It's possible the idempotent-retry scenario that code is trying to catch isn't actually possible anymore though, not sure.
I think the checkState(!byId.contains(idKey on line 246 means that this line wouldn't actually represent stableId == id though, in which case reaching this line should maybe be an IllegalStateException indicating some kind of corrupted state? Unless I'm misunderstanding the state of the index.
Basically we'd reach here if the byName already exists, and catalogObj.stableId() != catalog.getId(), but somehow catalogObj == null so that line 230 doesn't already exit early.
Is it expected for the byName index to not be atomic with what's visible in persistence.fetch?
I think all these subtleties could use some code comments to clarify if this is working as intended.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Both (index and persistence state) are always consistent. This particular one is literally an impossible case, so I removed it.
This (and other) functions have to expect cases when call sites (management API handlers) pass already existing names and/or already existing entity IDs. For example, a call site can pass a non-existing catalog name with an already used entity ID. Would be easier if the IDs were generated exclusively by the implementations, not the call sites.
I think the checkState(!byId.contains(idKey on line 246 means that this line wouldn't actually represent stableId == id though, in which case reaching this line should maybe be an IllegalStateException indicating some kind of corrupted state?
Actually not a corrupted state, but the API call sites passing an already used catalog ID into the "createCatalog" call.
When I wrote this function, I thought about how a retry could actually happen, given that the existing implementations assume entity-ID-equality (the reason for the TODO at the beginning of the function).
| anyChange = true; | ||
| } | ||
|
|
||
| void entityResultNoChange(PolarisBaseEntity entity) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could use some javadoc comments explaining the difference between entityResultNoChange and unchangedEntityResult
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, two functions for the same thing. Removed one.
|
|
||
| /** | ||
| * Represents the triplet of catalog-ID, entity-ID and type-code plus a reverse-or-key marker. | ||
| * String representations of this type are used as ACL names and "role" names. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like there's an implicit encoding convention here regarding the r/ and d/ convention. Could use javadoc examples here or else a javadoc link to wherever else the convention is defined other than just in the code's parsing logic in here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
docs added
| mutationResults.entityResult(ENTITY_CANNOT_BE_RENAMED); | ||
| return; | ||
| } | ||
| if (!byName.remove(originalNameKey)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does byName represent a fixed overall version for the duration of this method or is it a read-through to the "head" version of persistence?
My understanding is that it's a fixed/snapshot that then collects updates to apply at the end, is that correct? As in, the "immutable object index" from your design doc?
If so, would that mean that since we already validated byName.get(originalNameKey) on line 326 that the remove here must succeed, or else we have a code bug? In that case this should probably be an assert/IllegalStateException rather than returning ENTITY_NOT_FOUND because some assumptions must have gone wrong.
Or if byName is indeed subject to concurrent removal from somewhere else please add some comments around here indicating that it's possible and maybe additional comments around any other accesses in this method to the index to help explain how we safely reconcile the changing state.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yea - checkState is better here.
| mutationResults.entityResult(ENTITY_NOT_FOUND); | ||
| return; | ||
| } | ||
| var originalRef = byName.get(originalNameKey); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What are the scenarios that can lead to this byName not having this originalNameKey that we just got from byId? Would it indicate byId and byName getting out of sync somehow?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's no thinkable scenario actually.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ideally I'd prefer to not have the byId index at all and just refer to entities by name.
But that requires a bigger change.
| Optional<PolarisPrincipalSecrets> currentSecrets, | ||
| ObjBase originalObj) { | ||
| return mapToObj(entity, currentSecrets) | ||
| .updateTimestamp(originalObj.createTimestamp()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this intentional to use originalObj.createTimestamp as the updateTimestamp for the change-comparison object? If so, definitely needs some extensive comments explaining why.
If the intent is to canonicalize updateTimestamp to not be relevant for semantic comparison, it might be better to set it to 0L or something to indicate intentional/obvious missing values rather than having comparison objects floating around that have the createTimestamp written in.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oops, should have been updateTimestamp.
Sure, this could be optimized, but the call sites would become more complicated.
|
|
||
| private static final Logger LOGGER = LoggerFactory.getLogger(MutationAttempt.class); | ||
|
|
||
| public static ObjBase objForChangeComparison( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please add javadoc comment for this one explaining intent and how it intends to canonicalize whichever fields for comparison.
Is this to detect cases where a real update actually happens to result in an entity whose full contents are unchanged, or is there a common concurrency situation that is expected to cause "no change" mutations (e.g. on some kind of retry?)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's to detect whether a update is actually changing something.
| for (var i = 0; i < len; i++) { | ||
| var c = locationString.charAt(i); | ||
| checkArgument(c >= ' ', "Control characters are forbidden in locations"); | ||
| if (c == '/' || c == '\\') { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That raises an interesting point - we'd ideally be able to incorporate storage-type-specific canonicalization logic.
In this code for example, presumably the \\ is specifically "windows local filesystem" use cases (which really should only be very limited test/dev scenarios where overlap-checks probably aren't relevant anyways; any "sensitive" production use cases would be really weird to run on a single-node Windows environment with engine and Polaris catalog in the same place, or else some NFS mount).
But for a nio Filesystem, it's actually correct to canonicalize with real filesystem syntax, such as collapsing "foo/./././././bar" into "foo/bar" or "foo/bar/.." becoming "foo" when it comes to "overlap detection" because overlap detection is precisely supposed to prevent security issues by sharing directories. So it becomes important to avoid creative syntax circumventing our check.
And yet, as mentioned before, the "local filesystem" use cases where paths have fancy syntactic interpretations also happen to be the cases where overlap-check probably doesn't actually matter.
I guess it's fine to leave as-is for now, but custom parsing of paths is probably not something we want to keep as ad-hoc inline logic here in the long-term. It might be worth adding a comment or filing a github issue to call out that this logic isn't necessarily authoritative for any future developers who might read it as gospel.
No description provided.