Skip to content

Commit

Permalink
Merge pull request #3 from venasolutions/refurbish-metadata
Browse files Browse the repository at this point in the history
MongoDriver "path" metadata field
  • Loading branch information
prdoyle authored Nov 13, 2022
2 parents 14a5e85 + 8091685 commit ea0a995
Show file tree
Hide file tree
Showing 3 changed files with 77 additions and 6 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ final class Formatter {
* No field name should be a prefix of any other.
*/
enum DocumentFields {
path,
state,
echo,
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@

import static com.mongodb.ErrorCategory.DUPLICATE_KEY;
import static io.vena.bosk.drivers.mongo.Formatter.DocumentFields.echo;
import static io.vena.bosk.drivers.mongo.Formatter.DocumentFields.path;
import static io.vena.bosk.drivers.mongo.Formatter.DocumentFields.state;
import static io.vena.bosk.drivers.mongo.Formatter.dottedFieldNameOf;
import static io.vena.bosk.drivers.mongo.Formatter.enclosingReference;
Expand Down Expand Up @@ -115,7 +116,7 @@ public R initialRoot(Type rootType) throws InvalidTypeException, IOException, In
}

R root = receiver.initialRoot(rootType);
ensureTenantDocumentExists(formatter.object2bsonValue(root, rootType));
ensureDocumentExists(formatter.object2bsonValue(root, rootType), "$setOnInsert");
return root;
}

Expand Down Expand Up @@ -219,8 +220,17 @@ public void refurbish() {
return;
}

// Round trip via state tree nodes
R root = formatter.document2object(newState, rootRef);
doUpdate(replacementDoc(rootRef, root), documentFilter());
BsonValue initialState = formatter.object2bsonValue(root, rootRef.targetType());

// Start with a blank document so subsequent changes become update events instead of inserts
// TODO: should we do this for initialization too? We want those to be updates as well, right?
collection.replaceOne(documentFilter(), new Document());

// Set all the same fields we set on initialization
ensureDocumentExists(initialState, "$set");

session.commitTransaction();
} finally {
if (session.hasActiveTransaction()) {
Expand Down Expand Up @@ -269,9 +279,9 @@ private <T> BsonDocument deletionDoc(Reference<T> target) {
return new BsonDocument("$unset", new BsonDocument(key, new BsonNull())); // Value is ignored
}

private void ensureTenantDocumentExists(BsonValue initialState) {
private void ensureDocumentExists(BsonValue initialState, String updateCommand) {
BsonDocument update = new BsonDocument(updateCommand, initialDocument(initialState));
BsonDocument filter = documentFilter();
BsonDocument update = initialTenantUpsert(initialState);
UpdateOptions options = new UpdateOptions();
options.upsert(true);
LOGGER.debug("** Initial tenant upsert for {}", documentID);
Expand All @@ -285,6 +295,9 @@ private void ensureTenantDocumentExists(BsonValue initialState) {
if (DUPLICATE_KEY == ErrorCategory.fromErrorCode(e.getCode())) {
// This can happen in MongoDB 4.0 if two upserts occur in parallel.
// https://docs.mongodb.com/v4.0/reference/method/db.collection.update/
// As of MongoDB 4.2, this is no longer required. Since 4.0 is not longer
// supported, we could presumably delete this code.
// https://www.mongodb.com/docs/manual/core/retryable-writes/#std-label-retryable-update-upsert
LOGGER.debug("| Retrying: {}", e.getMessage());
result = collection.updateOne(filter, update, options);
} else {
Expand All @@ -294,11 +307,12 @@ private void ensureTenantDocumentExists(BsonValue initialState) {
LOGGER.debug("| Result: {}", result);
}

BsonDocument initialTenantUpsert(BsonValue initialState) {
private BsonDocument initialDocument(BsonValue initialState) {
BsonDocument fieldValues = new BsonDocument("_id", documentID);
fieldValues.put(path.name(), new BsonString("/"));
fieldValues.put(state.name(), initialState);
fieldValues.put(echo.name(), new BsonString(uniqueEchoToken()));
return new BsonDocument("$setOnInsert", fieldValues);
return fieldValues;
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package io.vena.bosk.drivers.mongo;

import com.mongodb.MongoException;
import com.mongodb.client.MongoCollection;
import com.mongodb.client.MongoCursor;
import io.vena.bosk.Bosk;
import io.vena.bosk.BoskDriver;
import io.vena.bosk.Catalog;
Expand All @@ -26,16 +28,22 @@
import java.util.concurrent.LinkedBlockingDeque;
import lombok.Value;
import lombok.experimental.Accessors;
import org.bson.BsonDocument;
import org.bson.BsonNull;
import org.bson.BsonString;
import org.bson.Document;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import static io.vena.bosk.ListingEntry.LISTING_ENTRY;
import static io.vena.bosk.drivers.mongo.Formatter.DocumentFields.path;
import static java.lang.Long.max;
import static java.lang.System.currentTimeMillis;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;

/**
Expand Down Expand Up @@ -380,6 +388,54 @@ void refurbish_createsField() throws IOException, InterruptedException {
assertEquals(Optional.of(TestValues.blank()), after); // Now it's there
}

@Test
@UsesMongoService
void refurbish_fixesMetadata() throws IOException, InterruptedException {
// Set up the database so it looks basically right
Bosk<TestEntity> initialBosk = new Bosk<TestEntity>(
"Initial",
TestEntity.class,
this::initialRoot,
createDriverFactory()
);

// (Close this so it doesn't crash when we delete the "path" field)
((MongoDriver<TestEntity>)initialBosk.driver()).close();

// Remove the `path` metadata field
MongoCollection<Document> collection = mongoService.client()
.getDatabase(driverSettings.database())
.getCollection(driverSettings.collection());
BsonDocument filterDoc = new BsonDocument("_id", new BsonString("boskDocument"));
BsonDocument deletionDoc = new BsonDocument("$unset", new BsonDocument(path.name(), new BsonNull())); // Value is ignored
collection.updateOne(filterDoc, deletionDoc);

// Make the bosk we want to test
Bosk<TestEntity> bosk = new Bosk<TestEntity>(
"bosk",
TestEntity.class,
this::initialRoot,
createDriverFactory()
);

// Verify that the path field is indeed missing
bosk.driver().flush();
try (MongoCursor<Document> cursor = collection.find(filterDoc).cursor()) {
Document doc = cursor.next();
assertNull(doc.get(path.name()));
}

// Refurbish
((MongoDriver<?>)bosk.driver()).refurbish();

// Verify that it's there now
try (MongoCursor<Document> cursor = collection.find(filterDoc).cursor()) {
Document doc = cursor.next();
assertEquals("/", doc.get(path.name()));
}

}

private <E extends Entity> DriverFactory<E> createDriverFactory() {
return (bosk, downstream) -> {
MongoDriver<E> driver = new MongoDriver<E>(
Expand Down

0 comments on commit ea0a995

Please sign in to comment.