-
Notifications
You must be signed in to change notification settings - Fork 332
Fix a race condition in sendNotification where concurrent parent-namespace creation causes failures #2693
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
Fix a race condition in sendNotification where concurrent parent-namespace creation causes failures #2693
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -71,6 +71,7 @@ | |
| import org.apache.iceberg.exceptions.NoSuchTableException; | ||
| import org.apache.iceberg.exceptions.NoSuchViewException; | ||
| import org.apache.iceberg.exceptions.NotFoundException; | ||
| import org.apache.iceberg.exceptions.ServiceFailureException; | ||
| import org.apache.iceberg.exceptions.UnprocessableEntityException; | ||
| import org.apache.iceberg.io.CloseableGroup; | ||
| import org.apache.iceberg.io.FileIO; | ||
|
|
@@ -508,16 +509,21 @@ private void createNamespaceInternal( | |
| BehaviorChangeConfiguration.ALLOW_NAMESPACE_CUSTOM_LOCATION, catalogEntity)) { | ||
| validateNamespaceLocation(entity, resolvedParent); | ||
| } | ||
| PolarisEntity returnedEntity = | ||
| PolarisEntity.of( | ||
| getMetaStoreManager() | ||
| .createEntityIfNotExists( | ||
| getCurrentPolarisContext(), | ||
| PolarisEntity.toCoreList(resolvedParent.getRawFullPath()), | ||
| entity)); | ||
| if (returnedEntity == null) { | ||
| throw new AlreadyExistsException( | ||
| "Cannot create namespace %s. Namespace already exists", namespace); | ||
| EntityResult result = | ||
| getMetaStoreManager() | ||
| .createEntityIfNotExists( | ||
| getCurrentPolarisContext(), | ||
| PolarisEntity.toCoreList(resolvedParent.getRawFullPath()), | ||
| entity); | ||
| if (!result.isSuccess()) { | ||
| if (result.alreadyExists()) { | ||
| throw new AlreadyExistsException( | ||
| "Cannot create namespace %s. Namespace already exists", namespace); | ||
| } else { | ||
| throw new ServiceFailureException( | ||
| "Unexpected error trying to create namespace %s. Status: %s ExtraInfo: %s", | ||
| namespace, result.getReturnStatus(), result.getExtraInformation()); | ||
| } | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -2579,7 +2585,23 @@ private void createNonExistingNamespaces(Namespace namespace) { | |
| Namespace parentNamespace = PolarisCatalogHelpers.getParentNamespace(nsLevel); | ||
| PolarisResolvedPathWrapper resolvedParent = | ||
| resolvedEntityView.getPassthroughResolvedPath(parentNamespace); | ||
| createNamespaceInternal(nsLevel, Collections.emptyMap(), resolvedParent); | ||
| try { | ||
| createNamespaceInternal(nsLevel, Collections.emptyMap(), resolvedParent); | ||
| } catch (AlreadyExistsException aee) { | ||
| // Since we only attempted to create the namespace after checking that | ||
| // getPassthroughResolvedPath for this level is null, this should be a relatively | ||
| // infrequent case during high concurrency where another notification already | ||
| // conveniently created the namespace between the time we checked and the time | ||
| // we attempted to fill it in. It's working as intended in this case to simply | ||
| // continue with the existing namespace, but the fact that this collision occurred | ||
| // may be relevant to someone running the service in case of unexpected interactions, | ||
| // so we'll still log the fact that this happened. | ||
| LOGGER | ||
| .atInfo() | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why INFO? If we expect it to be a normal and expected call path, "informing" log readers every time it happens seems to be an overkill from the operations perspective. I propose debug (non-blocking)
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah I was on the fence. It's one of those cases where it's "normal" as something that will happen time-to-time, but usually isn't deterministically triggered since here the attempt to create the namespace is only made if the immediate preceding call thinks the namespace is missing. So it would be fairly infrequent, only correlating with periods of high parent-creation contention (not just normal periods of high concurrency on an already-existing namespace). I had even started My thought is that if there are unforeseen bugs relating to the state of these "create if needed" namespaces they're more likely to be related to these collisions than to normal operation, so even if someone sets their log-viewer to exclude I also don't feel too strongly though so I'm happy to downgrade it if there's a preference for DEBUG despite this rationale .
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fair enough. No need to change :) |
||
| .setCause(aee) | ||
| .addKeyValue("namespace", namespace) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is it supposed to be referenced in the message?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah I actually normally default to the traditional format-string with curly-braces, but I think others have been pushing for the more modern structured logging convention so I'm been forcing myself to use it :) Structured logging is indeed more beneficial for cases where logs sinks are good at having structured-log elements pulled out into separate semi-structured fields for efficient aggregations, etc. The convention as I understand it is to make the main log message avoid any direct mention of the structured params: https://www.slf4j.org/manual.html As opposed to
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also added a comment above the log statement better elucidating the considerations |
||
| .log("Namespace already exists in createNonExistingNamespace"); | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
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.
Currently we throw
AlreadyExistsExceptionif the result is not success without checking whether the return status isENTITY_ALREADY_EXIST. DespiteENTITY_ALREADY_EXISTis the only error status thrown in existing metastore manager impl, furture development and other implementation may include other error status likeUNEXPECTED_ERROR, where we do not want to ignore and log here.I think we could add an additional check
polaris/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalog.java
Lines 518 to 521 in 19742cc
to only throw AlreadyExistsException when return status is that, otherwise throw a RuntimeException/IllegalState/...
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.
Good point!
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.
Good catch, updated to throw
ServiceFailureExceptionif returnStatus isn't ENTITY_ALREADY_EXISTS.