-
Notifications
You must be signed in to change notification settings - Fork 366
Relational JDBC: Reuse connection with execute select when within transaction #3334
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?
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 |
|---|---|---|
|
|
@@ -119,13 +119,10 @@ public void writeEntity( | |
| boolean nameOrParentChanged, | ||
| PolarisBaseEntity originalEntity) { | ||
| try { | ||
| persistEntity( | ||
| callCtx, | ||
| entity, | ||
| originalEntity, | ||
| null, | ||
| (connection, preparedQuery) -> { | ||
| return datasourceOperations.executeUpdate(preparedQuery); | ||
| datasourceOperations.runWithinTransaction( | ||
| connection -> { | ||
| persistEntity(callCtx, entity, originalEntity, connection); | ||
| return true; | ||
| }); | ||
| } catch (SQLException e) { | ||
| throw new RuntimeException("Error persisting entity", e); | ||
|
|
@@ -148,15 +145,18 @@ public void writeEntities( | |
| // return it. | ||
| PolarisBaseEntity entityFound = | ||
| lookupEntity( | ||
| callCtx, entity.getCatalogId(), entity.getId(), entity.getTypeCode()); | ||
| callCtx, | ||
| entity.getCatalogId(), | ||
| entity.getId(), | ||
| entity.getTypeCode(), | ||
| connection); | ||
| if (entityFound != null && originalEntity == null) { | ||
| // probably the client retried, simply return it | ||
| // TODO: Check correctness of returning entityFound vs entity here. It may have | ||
| // already been updated after the creation. | ||
| continue; | ||
| } | ||
| persistEntity( | ||
| callCtx, entity, originalEntity, connection, datasourceOperations::execute); | ||
| persistEntity(callCtx, entity, originalEntity, connection); | ||
| } | ||
| return true; | ||
| }); | ||
|
|
@@ -172,15 +172,29 @@ private void persistEntity( | |
| @Nonnull PolarisCallContext callCtx, | ||
| @Nonnull PolarisBaseEntity entity, | ||
| PolarisBaseEntity originalEntity, | ||
| Connection connection, | ||
| QueryAction queryAction) | ||
| @Nonnull Connection connection) | ||
| throws SQLException { | ||
| ModelEntity modelEntity = ModelEntity.fromEntity(entity, schemaVersion); | ||
| if (originalEntity == null) { | ||
| // Check if entity already exists before attempting INSERT to avoid constraint violations | ||
| // that would abort the transaction in PostgreSQL | ||
| PolarisBaseEntity existingEntity = | ||
| lookupEntityByName( | ||
| callCtx, | ||
| entity.getCatalogId(), | ||
| entity.getParentId(), | ||
| entity.getTypeCode(), | ||
| entity.getName(), | ||
| connection); | ||
|
|
||
| if (existingEntity != null) { | ||
| throw new EntityAlreadyExistsException(existingEntity); | ||
| } | ||
|
|
||
| try { | ||
| List<Object> values = | ||
| modelEntity.toMap(datasourceOperations.getDatabaseType()).values().stream().toList(); | ||
| queryAction.apply( | ||
| datasourceOperations.execute( | ||
| connection, | ||
| QueryGenerator.generateInsertQuery( | ||
| ModelEntity.getAllColumnNames(schemaVersion), | ||
|
|
@@ -189,21 +203,10 @@ private void persistEntity( | |
| realmId)); | ||
| } catch (SQLException e) { | ||
| if (datasourceOperations.isConstraintViolation(e)) { | ||
| PolarisBaseEntity existingEntity = | ||
| lookupEntityByName( | ||
| callCtx, | ||
| entity.getCatalogId(), | ||
| entity.getParentId(), | ||
| entity.getTypeCode(), | ||
| entity.getName()); | ||
| // This happens in two scenarios: | ||
| // 1. PRIMARY KEY violated | ||
| // 2. UNIQUE CONSTRAINT on (realm_id, catalog_id, parent_id, type_code, name) violated | ||
| // With SERIALIZABLE isolation, the conflicting entity may _not_ be visible and | ||
| // existingEntity can be null, which would cause an NPE in | ||
| // EntityAlreadyExistsException.message(). | ||
| throw new EntityAlreadyExistsException( | ||
| existingEntity != null ? existingEntity : entity, e); | ||
|
Comment on lines
-192
to
-206
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. PG aborts the transaction when it encounters the error : hence a lookup after write failure is not possible, the tests failed because of this. Now lookup before before write is gonna cause some perf issue (but only for certain cases) and wrapping writeEntity would elevate the scenario more, will need to run some benchmark before we check it in,
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. Thx for the research! That sounds like a good plan 👍
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. Alternatively, we could explicitly start a new Tx for
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. precisely presently thats what we are doing in the current code, the connection is a separate connection than the one used in the transaction in cases of |
||
| // Constraint violation despite our check above - this can happen due to race conditions | ||
| // where another transaction inserted the same entity between our SELECT and INSERT. | ||
| // We cannot lookup again here because PostgreSQL aborts the transaction after any error. | ||
| throw new EntityAlreadyExistsException(entity, e); | ||
| } | ||
| throw new RuntimeException( | ||
| String.format("Failed to write entity due to %s", e.getMessage()), e); | ||
|
|
@@ -223,7 +226,7 @@ private void persistEntity( | |
| List<Object> values = | ||
| modelEntity.toMap(datasourceOperations.getDatabaseType()).values().stream().toList(); | ||
| int rowsUpdated = | ||
| queryAction.apply( | ||
| datasourceOperations.execute( | ||
| connection, | ||
| QueryGenerator.generateUpdateQuery( | ||
| ModelEntity.getAllColumnNames(schemaVersion), | ||
|
|
@@ -405,11 +408,30 @@ public void deleteAll(@Nonnull PolarisCallContext callCtx) { | |
| @Override | ||
| public PolarisBaseEntity lookupEntity( | ||
| @Nonnull PolarisCallContext callCtx, long catalogId, long entityId, int typeCode) { | ||
| PreparedQuery query = buildLookupEntityQuery(catalogId, entityId, typeCode); | ||
| return getPolarisBaseEntity(query); | ||
| } | ||
|
|
||
| /** | ||
| * Lookup entity by ID using an existing connection for transaction consistency. | ||
| * | ||
| * @param connection connection to use for the query (non-null) | ||
| */ | ||
| private PolarisBaseEntity lookupEntity( | ||
| @Nonnull PolarisCallContext callCtx, | ||
| long catalogId, | ||
| long entityId, | ||
| int typeCode, | ||
| @Nonnull Connection connection) { | ||
| PreparedQuery query = buildLookupEntityQuery(catalogId, entityId, typeCode); | ||
| return getPolarisBaseEntity(query, connection); | ||
| } | ||
|
|
||
| private PreparedQuery buildLookupEntityQuery(long catalogId, long entityId, int typeCode) { | ||
| Map<String, Object> params = | ||
| Map.of("catalog_id", catalogId, "id", entityId, "type_code", typeCode, "realm_id", realmId); | ||
| return getPolarisBaseEntity( | ||
| QueryGenerator.generateSelectQuery( | ||
| ModelEntity.getAllColumnNames(schemaVersion), ModelEntity.TABLE_NAME, params)); | ||
| return QueryGenerator.generateSelectQuery( | ||
| ModelEntity.getAllColumnNames(schemaVersion), ModelEntity.TABLE_NAME, params); | ||
| } | ||
|
|
||
| @Override | ||
|
|
@@ -419,6 +441,28 @@ public PolarisBaseEntity lookupEntityByName( | |
| long parentId, | ||
| int typeCode, | ||
| @Nonnull String name) { | ||
| PreparedQuery query = buildLookupEntityByNameQuery(catalogId, parentId, typeCode, name); | ||
| return getPolarisBaseEntity(query); | ||
| } | ||
|
|
||
| /** | ||
| * Lookup entity by name using an existing connection for transaction consistency. | ||
| * | ||
| * @param connection connection to use for the query (non-null) | ||
| */ | ||
| private PolarisBaseEntity lookupEntityByName( | ||
| @Nonnull PolarisCallContext callCtx, | ||
| long catalogId, | ||
| long parentId, | ||
| int typeCode, | ||
| @Nonnull String name, | ||
| @Nonnull Connection connection) { | ||
| PreparedQuery query = buildLookupEntityByNameQuery(catalogId, parentId, typeCode, name); | ||
| return getPolarisBaseEntity(query, connection); | ||
| } | ||
|
|
||
| private PreparedQuery buildLookupEntityByNameQuery( | ||
| long catalogId, long parentId, int typeCode, String name) { | ||
| Map<String, Object> params = | ||
| Map.of( | ||
| "catalog_id", | ||
|
|
@@ -431,31 +475,63 @@ public PolarisBaseEntity lookupEntityByName( | |
| name, | ||
| "realm_id", | ||
| realmId); | ||
| return getPolarisBaseEntity( | ||
| QueryGenerator.generateSelectQuery( | ||
| ModelEntity.getAllColumnNames(schemaVersion), ModelEntity.TABLE_NAME, params)); | ||
| return QueryGenerator.generateSelectQuery( | ||
| ModelEntity.getAllColumnNames(schemaVersion), ModelEntity.TABLE_NAME, params); | ||
| } | ||
|
|
||
| @Nullable | ||
| private PolarisBaseEntity getPolarisBaseEntity(QueryGenerator.PreparedQuery query) { | ||
| try { | ||
| var results = datasourceOperations.executeSelect(query, new ModelEntity(schemaVersion)); | ||
| if (results.isEmpty()) { | ||
| return null; | ||
| } else if (results.size() > 1) { | ||
| throw new IllegalStateException( | ||
| String.format( | ||
| "More than one(%s) entities were found for a given type code : %s", | ||
| results.size(), results.getFirst().getTypeCode())); | ||
| } else { | ||
| return results.getFirst(); | ||
| } | ||
| return extractSingleEntity(results); | ||
| } catch (SQLException e) { | ||
| throw new RuntimeException( | ||
| String.format("Failed to retrieve polaris entity due to %s", e.getMessage()), e); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Execute entity lookup using an existing connection for transaction consistency. | ||
| * | ||
| * @param query the prepared query to execute | ||
| * @param connection connection to use for the query (non-null) | ||
| * @return the entity if found, null otherwise | ||
| */ | ||
| @Nullable | ||
| private PolarisBaseEntity getPolarisBaseEntity( | ||
| QueryGenerator.PreparedQuery query, @Nonnull Connection connection) { | ||
| try { | ||
| var results = | ||
| datasourceOperations.executeSelect(query, new ModelEntity(schemaVersion), connection); | ||
| return extractSingleEntity(results); | ||
| } catch (SQLException e) { | ||
| throw new RuntimeException( | ||
| String.format("Failed to retrieve polaris entity due to %s", e.getMessage()), e); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Extract a single entity from query results, validating that exactly zero or one entity is | ||
| * returned. | ||
| * | ||
| * @param results the list of entities returned from the query | ||
| * @return the single entity if found, null if no results | ||
| * @throws IllegalStateException if more than one entity is found | ||
| */ | ||
| @Nullable | ||
| private PolarisBaseEntity extractSingleEntity(List<PolarisBaseEntity> results) { | ||
| if (results.isEmpty()) { | ||
| return null; | ||
| } else if (results.size() > 1) { | ||
| throw new IllegalStateException( | ||
| String.format( | ||
| "More than one(%s) entities were found for a given type code : %s", | ||
| results.size(), results.getFirst().getTypeCode())); | ||
| } else { | ||
| return results.getFirst(); | ||
| } | ||
| } | ||
|
|
||
| @Nonnull | ||
| @Override | ||
| public List<PolarisBaseEntity> lookupEntities( | ||
|
|
@@ -1050,7 +1126,7 @@ private boolean handleInheritablePolicy( | |
| @Nonnull PolarisCallContext callCtx, | ||
| @Nonnull PolarisPolicyMappingRecord record, | ||
| @Nonnull PreparedQuery insertQuery, | ||
| Connection connection) | ||
| @Nonnull Connection connection) | ||
| throws SQLException { | ||
| List<PolarisPolicyMappingRecord> existingRecords = | ||
| loadPoliciesOnTargetByType( | ||
|
|
@@ -1272,9 +1348,4 @@ PolarisStorageIntegration<T> loadPolarisStorageIntegration( | |
| BaseMetaStoreManager.extractStorageConfiguration(diagnostics, entity); | ||
| return storageIntegrationProvider.getStorageIntegrationForConfig(storageConfig); | ||
| } | ||
|
|
||
| @FunctionalInterface | ||
| private interface QueryAction { | ||
| Integer apply(Connection connection, QueryGenerator.PreparedQuery query) throws SQLException; | ||
| } | ||
| } | ||
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.
nit: this method is called from only one place... why not inline?