-
Notifications
You must be signed in to change notification settings - Fork 15
Return false from bulkUpsert when write result is incomplete #322
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 1 commit
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 |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| package org.hypertrace.core.documentstore.commons; | ||
|
|
||
| import java.sql.Statement; | ||
|
|
||
| /** Shared helpers for validating JDBC batch write outcomes. */ | ||
| public final class BatchWriteUtils { | ||
|
|
||
| private BatchWriteUtils() {} | ||
|
|
||
| /** | ||
| * Returns true when every batch entry completed without {@link Statement#EXECUTE_FAILED} and the | ||
| * result length matches the number of operations submitted. | ||
| * | ||
| * <p>{@link Statement#SUCCESS_NO_INFO} (-2) and positive update counts are treated as success. | ||
| */ | ||
| public static boolean isBatchFullySuccessful(final int[] updateCounts, final int expectedSize) { | ||
| if (updateCounts == null || updateCounts.length != expectedSize) { | ||
| return false; | ||
| } | ||
| for (final int count : updateCounts) { | ||
| if (count == Statement.EXECUTE_FAILED) { | ||
| return false; | ||
| } | ||
| } | ||
| return true; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -12,6 +12,7 @@ | |
| import static org.hypertrace.core.documentstore.mongo.update.parser.MongoSetOperationParser.SET_CLAUSE; | ||
|
|
||
| import com.fasterxml.jackson.core.JsonProcessingException; | ||
| import com.google.common.annotations.VisibleForTesting; | ||
| import com.mongodb.BasicDBObject; | ||
| import com.mongodb.MongoBulkWriteException; | ||
| import com.mongodb.MongoCommandException; | ||
|
|
@@ -678,6 +679,17 @@ public boolean bulkUpsert(Map<Key, Document> documents) { | |
| try { | ||
| BulkWriteResult result = bulkUpsertImpl(documents); | ||
| LOGGER.debug(result.toString()); | ||
| if (!isBulkUpsertComplete(result, documents.size())) { | ||
| LOGGER.error( | ||
| "Incomplete bulk upsert for documents. requested={}, matched={}, upserted={}," | ||
| + " acknowledged={}, result={}", | ||
| documents.size(), | ||
| result.wasAcknowledged() ? result.getMatchedCount() : -1, | ||
| result.wasAcknowledged() ? result.getUpserts().size() : -1, | ||
| result.wasAcknowledged(), | ||
| result); | ||
| return false; | ||
| } | ||
| return true; | ||
| } catch (IOException | MongoServerException e) { | ||
| LOGGER.error("Error during bulk upsert for documents:{}", documents, e); | ||
|
|
@@ -702,6 +714,21 @@ private BulkWriteResult bulkUpsertImpl(Map<Key, Document> documents) | |
| .get(() -> collection.bulkWrite(bulkCollection, new BulkWriteOptions().ordered(false))); | ||
| } | ||
|
|
||
| /** | ||
| * Each UpdateOne upsert accounts for exactly one matched existing document or one upserted | ||
| * document. Incomplete results (or unacknowledged writes) must not be reported as success. | ||
| */ | ||
| @VisibleForTesting | ||
| static boolean isBulkUpsertComplete(final BulkWriteResult result, final int requestedCount) { | ||
| if (requestedCount == 0) { | ||
| return true; | ||
| } | ||
| if (!result.wasAcknowledged()) { | ||
| return false; | ||
| } | ||
| return result.getMatchedCount() + result.getUpserts().size() == requestedCount; | ||
| } | ||
|
|
||
| @Override | ||
| public CloseableIterator<Document> bulkUpsertAndReturnOlderDocuments(Map<Key, Document> documents) | ||
| throws IOException { | ||
|
|
@@ -714,6 +741,17 @@ public CloseableIterator<Document> bulkUpsertAndReturnOlderDocuments(Map<Key, Do | |
| // Now go ahead and do the bulk upsert. | ||
| BulkWriteResult result = bulkUpsertImpl(documents); | ||
| LOGGER.debug(result.toString()); | ||
| if (!isBulkUpsertComplete(result, documents.size())) { | ||
| LOGGER.error( | ||
| "Incomplete bulk upsert for documents. requested={}, matched={}, upserted={}," | ||
| + " acknowledged={}, result={}", | ||
| documents.size(), | ||
| result.wasAcknowledged() ? result.getMatchedCount() : -1, | ||
| result.wasAcknowledged() ? result.getUpserts().size() : -1, | ||
| result.wasAcknowledged(), | ||
| result); | ||
| throw new IOException("Incomplete bulk upsert."); | ||
|
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. This might now break existing clients.
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. Agreed it tightens the contract: callers that previously treated incomplete Mongo writes as success will now get
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. We should close the cursor here or it'll be a resource leak.
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. Fixed — close the Mongo cursor before throwing on incomplete upsert (and on other failure paths before the iterator takes ownership). |
||
| } | ||
|
|
||
| return convertToDocumentIterator(mongoCursor); | ||
| } catch (JsonProcessingException e) { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -67,6 +67,7 @@ | |
| import org.hypertrace.core.documentstore.Key; | ||
| import org.hypertrace.core.documentstore.Query; | ||
| import org.hypertrace.core.documentstore.UpdateResult; | ||
| import org.hypertrace.core.documentstore.commons.BatchWriteUtils; | ||
| import org.hypertrace.core.documentstore.commons.CommonUpdateValidator; | ||
| import org.hypertrace.core.documentstore.commons.DocStoreConstants; | ||
| import org.hypertrace.core.documentstore.commons.UpdateValidator; | ||
|
|
@@ -754,6 +755,14 @@ public boolean bulkUpsert(Map<Key, Document> documents) { | |
| LOGGER.debug("Write result: {}", Arrays.toString(updateCounts)); | ||
| } | ||
|
|
||
| if (!BatchWriteUtils.isBatchFullySuccessful(updateCounts, documents.size())) { | ||
| LOGGER.error( | ||
| "Incomplete bulk upsert for documents. requested={}, updateCounts={}", | ||
| documents.size(), | ||
| Arrays.toString(updateCounts)); | ||
| return false; | ||
| } | ||
|
|
||
| return true; | ||
| } catch (BatchUpdateException e) { | ||
| LOGGER.error("BatchUpdateException bulk inserting documents.", e); | ||
|
|
@@ -801,6 +810,13 @@ public CloseableIterator<Document> bulkUpsertAndReturnOlderDocuments(Map<Key, Do | |
| if (LOGGER.isDebugEnabled()) { | ||
| LOGGER.debug("Write result: {}", Arrays.toString(updateCounts)); | ||
| } | ||
| if (!BatchWriteUtils.isBatchFullySuccessful(updateCounts, documents.size())) { | ||
| LOGGER.error( | ||
| "Incomplete bulk upsert for documents. requested={}, updateCounts={}", | ||
| documents.size(), | ||
| Arrays.toString(updateCounts)); | ||
| throw new IOException("Incomplete bulk upsert."); | ||
|
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. Close the RS here to prevent leak?
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. Fixed — close the ResultSet in a finally when we do not hand it to |
||
| } | ||
|
|
||
| return new PostgresResultIterator(resultSet); | ||
| } catch (IOException e) { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| package org.hypertrace.core.documentstore.commons; | ||
|
|
||
| import static org.junit.jupiter.api.Assertions.assertFalse; | ||
| import static org.junit.jupiter.api.Assertions.assertTrue; | ||
|
|
||
| import java.sql.Statement; | ||
| import org.junit.jupiter.api.Test; | ||
|
|
||
| class BatchWriteUtilsTest { | ||
|
|
||
| @Test | ||
| void isBatchFullySuccessful_allPositive_returnsTrue() { | ||
| assertTrue(BatchWriteUtils.isBatchFullySuccessful(new int[] {1, 1, 2}, 3)); | ||
| } | ||
|
|
||
| @Test | ||
| void isBatchFullySuccessful_successNoInfo_returnsTrue() { | ||
| assertTrue( | ||
| BatchWriteUtils.isBatchFullySuccessful( | ||
| new int[] {Statement.SUCCESS_NO_INFO, Statement.SUCCESS_NO_INFO}, 2)); | ||
| } | ||
|
|
||
| @Test | ||
| void isBatchFullySuccessful_executeFailed_returnsFalse() { | ||
| assertFalse(BatchWriteUtils.isBatchFullySuccessful(new int[] {1, Statement.EXECUTE_FAILED}, 2)); | ||
| } | ||
|
|
||
| @Test | ||
| void isBatchFullySuccessful_lengthMismatch_returnsFalse() { | ||
| assertFalse(BatchWriteUtils.isBatchFullySuccessful(new int[] {1}, 2)); | ||
| } | ||
|
|
||
| @Test | ||
| void isBatchFullySuccessful_null_returnsFalse() { | ||
| assertFalse(BatchWriteUtils.isBatchFullySuccessful(null, 0)); | ||
| } | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.