Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,8 @@ long count(

/**
* @param documents to be upserted in bulk
* @return true if the operation succeeded
* @return true if every requested document was upserted successfully; false if the write failed
* or only a subset of documents were written
*/
boolean bulkUpsert(Map<Key, Document> documents);

Expand Down
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
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand All @@ -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 {
Expand All @@ -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.");
}

return convertToDocumentIterator(mongoCursor);
} catch (JsonProcessingException e) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
import org.hypertrace.core.documentstore.Filter;
import org.hypertrace.core.documentstore.Key;
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.UpdateValidator;
import org.hypertrace.core.documentstore.model.config.postgres.CollectionConfig;
Expand Down Expand Up @@ -430,6 +431,14 @@ public boolean bulkUpsert(Map<Key, Document> documents) {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Bulk upsert results: {}", Arrays.toString(results));
}
if (!BatchWriteUtils.isBatchFullySuccessful(results, parsedDocuments.size())) {
LOGGER.error(
"Incomplete bulkUpsert. requested={}, submitted={}, updateCounts={}",
documents.size(),
parsedDocuments.size(),
Arrays.toString(results));
return false;
}
return true;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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.");
}

return new PostgresResultIterator(resultSet);
} catch (IOException e) {
Expand Down
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));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,18 @@
import com.mongodb.BasicDBObject;
import com.mongodb.MongoNamespace;
import com.mongodb.ReadPreference;
import com.mongodb.bulk.BulkWriteResult;
import com.mongodb.bulk.BulkWriteUpsert;
import com.mongodb.client.AggregateIterable;
import com.mongodb.client.FindIterable;
import com.mongodb.client.MongoCursor;
import com.mongodb.client.model.BulkWriteOptions;
import com.mongodb.client.model.FindOneAndUpdateOptions;
import com.mongodb.client.result.UpdateResult;
import java.io.IOException;
import java.time.Clock;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import org.bson.BsonString;
Expand All @@ -49,6 +53,7 @@
import org.hypertrace.core.documentstore.JSONDocument;
import org.hypertrace.core.documentstore.Key;
import org.hypertrace.core.documentstore.Query;
import org.hypertrace.core.documentstore.SingleValueKey;
import org.hypertrace.core.documentstore.expression.impl.ConstantExpression;
import org.hypertrace.core.documentstore.expression.impl.IdentifierExpression;
import org.hypertrace.core.documentstore.expression.impl.LogicalExpression;
Expand Down Expand Up @@ -452,4 +457,83 @@ void testBulkUpdateWithoutUpdates() {
UpdateOptions.DEFAULT_UPDATE_OPTIONS));
}
}

@Nested
class BulkUpsert {

@Test
void returnsTrueWhenAllDocumentsMatched() throws Exception {
Document document = new JSONDocument("{\"planet\": \"Mars\"}");
Map<Key, Document> documents =
Map.of(
new SingleValueKey("default", "k1"), document,
new SingleValueKey("default", "k2"), document);

when(collection.bulkWrite(anyList(), any(BulkWriteOptions.class)))
.thenReturn(BulkWriteResult.acknowledged(0, 2, 0, 2, emptyList(), emptyList()));

assertTrue(mongoCollection.bulkUpsert(documents));
}

@Test
void returnsTrueWhenDocumentsAreUpserted() throws Exception {
Document document = new JSONDocument("{\"planet\": \"Mars\"}");
Map<Key, Document> documents =
Map.of(
new SingleValueKey("default", "k1"), document,
new SingleValueKey("default", "k2"), document);

List<BulkWriteUpsert> upserts =
List.of(
new BulkWriteUpsert(0, new BsonString("default:k1")),
new BulkWriteUpsert(1, new BsonString("default:k2")));
when(collection.bulkWrite(anyList(), any(BulkWriteOptions.class)))
.thenReturn(BulkWriteResult.acknowledged(0, 0, 0, 0, upserts, emptyList()));

assertTrue(mongoCollection.bulkUpsert(documents));
}

@Test
void returnsFalseWhenResultAccountsForFewerDocumentsThanRequested() throws Exception {
Document document = new JSONDocument("{\"planet\": \"Mars\"}");
Map<Key, Document> documents =
Map.of(
new SingleValueKey("default", "k1"), document,
new SingleValueKey("default", "k2"), document);

// Only one of two requested docs accounted for (matched=1, upserts=0)
when(collection.bulkWrite(anyList(), any(BulkWriteOptions.class)))
.thenReturn(BulkWriteResult.acknowledged(0, 1, 0, 1, emptyList(), emptyList()));

assertFalse(mongoCollection.bulkUpsert(documents));
}

@Test
void returnsFalseForUnacknowledgedResult() throws Exception {
Document document = new JSONDocument("{\"planet\": \"Mars\"}");
Map<Key, Document> documents = Map.of(new SingleValueKey("default", "k1"), document);

when(collection.bulkWrite(anyList(), any(BulkWriteOptions.class)))
.thenReturn(BulkWriteResult.unacknowledged());

assertFalse(mongoCollection.bulkUpsert(documents));
}

@Test
void isBulkUpsertComplete_matchedPlusUpsertsEqualsRequested() {
assertTrue(
MongoCollection.isBulkUpsertComplete(
BulkWriteResult.acknowledged(0, 1, 0, 1, emptyList(), emptyList()), 1));
assertTrue(
MongoCollection.isBulkUpsertComplete(
BulkWriteResult.acknowledged(
0, 1, 0, 1, List.of(new BulkWriteUpsert(1, new BsonString("id"))), emptyList()),
2));
assertFalse(
MongoCollection.isBulkUpsertComplete(
BulkWriteResult.acknowledged(0, 1, 0, 1, emptyList(), emptyList()), 2));
assertFalse(MongoCollection.isBulkUpsertComplete(BulkWriteResult.unacknowledged(), 1));
assertTrue(MongoCollection.isBulkUpsertComplete(BulkWriteResult.unacknowledged(), 0));
}
}
}
Loading