Skip to content

Commit

Permalink
Polyfill support for MongoDriver updates
Browse files Browse the repository at this point in the history
  • Loading branch information
prdoyle committed Jan 19, 2024
1 parent 33023d9 commit a4c49cd
Show file tree
Hide file tree
Showing 5 changed files with 103 additions and 8 deletions.
44 changes: 44 additions & 0 deletions bosk-core/src/main/java/io/vena/bosk/SerializationPlugin.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
import lombok.EqualsAndHashCode;
import lombok.Value;
import org.slf4j.Logger;
Expand Down Expand Up @@ -243,6 +244,47 @@ public static boolean hasDeserializationPath(Class<?> nodeClass, Parameter param
return infoFor(nodeClass).annotatedParameters_DeserializationPath().containsKey(parameter.getName());
}

public <R extends StateTreeNode> void initializeEnclosingPolyfills(Reference<?> target, BoskDriver<R> formatDriver) {
if (!ANY_POLYFILLS.get()) {
return;
}
/*
Evolution note: we should be able to make this more efficient.
For the bosk state tree, we recursively analyze all the node types upfront.
When that process is finished, a dataflow analysis over the ParameterInfo graph
could determine, for any given type, whether that type ever occurs inside a polyfill.
The common case is likely "no", and we could quickly dispense with all polyfill concerns
for all references to that type. In particular, when there are no polyfills at all,
we could quickly determine that there's nothing to do. In the absence of recursive
datatypes, a reverse postorder walk over the ParameterInfo objects should converge in a single pass.
*/
if (!target.path().isEmpty()) {
try {
initializePolyfills(target.enclosingReference(Object.class), formatDriver);
} catch (InvalidTypeException e) {
throw new AssertionError("Every non-root reference has an enclosing reference: " + target);
}
}
}

private <R extends StateTreeNode, T> void initializePolyfills(Reference<T> ref, BoskDriver<R> formatDriver) {
initializeEnclosingPolyfills(ref, formatDriver);
if (!ref.path().isEmpty()) {
Class<?> enclosing;
try {
enclosing = ref.enclosingReference(Object.class).targetClass();
} catch (InvalidTypeException e) {
throw new AssertionError("Every non-root reference must have an enclosing reference: " + ref);
}
if (StateTreeNode.class.isAssignableFrom(enclosing)) {
Object result = infoFor(enclosing).polyfills().get(ref.path().lastSegment());
if (result != null) {
formatDriver.submitInitialization(ref, ref.targetClass().cast(result));
}
}
}
}

private Reference<?> findImplicitReferenceIfAny(Class<?> nodeClass, Parameter parameter, Bosk<?> bosk) {
if (isSelfReference(nodeClass, parameter)) {
Class<?> targetClass = rawClass(parameterType(parameter.getParameterizedType(), Reference.class, 0));
Expand Down Expand Up @@ -333,6 +375,7 @@ private static void scanForInfo(AnnotatedElement thing, String name, Set<String>
if (value == null) {
throw new NullPointerException("Polyfill value cannot be null: " + f);
}
ANY_POLYFILLS.set(true);
for (String fieldName: polyfill.value()) {
Object previous = polyfills.put(fieldName, value);
if (previous != null) {
Expand All @@ -356,6 +399,7 @@ private record ParameterInfo(
) { }

private static final Map<Class<?>, ParameterInfo> PARAMETER_INFO_MAP = new ConcurrentHashMap<>();
private static final AtomicBoolean ANY_POLYFILLS = new AtomicBoolean(false);

private static final Logger LOGGER = LoggerFactory.getLogger(SerializationPlugin.class);
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@
* Rather, this is meant to be used <em>temporarily</em> with newly added fields
* to support systems that are not yet aware of those fields.
*
* <p>
* The existence of this annotation anywhere in a bosk state tree could add overhead
* to all bosk updates, even for unrelated parts of the state tree, so it's best to
* remove this once it's no longer needed.
*
* @author Patrick Doyle
*/
@Retention(RUNTIME)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -228,34 +228,39 @@ private void refurbishTransaction() throws IOException {
@Override
public <T> void submitReplacement(Reference<T> target, T newValue) {
doRetryableDriverOperation(()->{
bsonPlugin.initializeEnclosingPolyfills(target, formatDriver);
formatDriver.submitReplacement(target, newValue);
}, "submitReplacement({})", target);
}

@Override
public <T> void submitConditionalReplacement(Reference<T> target, T newValue, Reference<Identifier> precondition, Identifier requiredValue) {
doRetryableDriverOperation(()->{
bsonPlugin.initializeEnclosingPolyfills(target, formatDriver);
formatDriver.submitConditionalReplacement(target, newValue, precondition, requiredValue);
}, "submitConditionalReplacement({}, {}={})", target, precondition, requiredValue);
}

@Override
public <T> void submitInitialization(Reference<T> target, T newValue) {
doRetryableDriverOperation(()->{
bsonPlugin.initializeEnclosingPolyfills(target, formatDriver);
formatDriver.submitInitialization(target, newValue);
}, "submitInitialization({})", target);
}

@Override
public <T> void submitDeletion(Reference<T> target) {
doRetryableDriverOperation(()->{
bsonPlugin.initializeEnclosingPolyfills(target, formatDriver);
formatDriver.submitDeletion(target);
}, "submitDeletion({})", target);
}

@Override
public <T> void submitConditionalDeletion(Reference<T> target, Reference<Identifier> precondition, Identifier requiredValue) {
doRetryableDriverOperation(() -> {
bsonPlugin.initializeEnclosingPolyfills(target, formatDriver);
formatDriver.submitConditionalDeletion(target, precondition, requiredValue);
}, "submitConditionalDeletion({}, {}={})", target, precondition, requiredValue);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -155,9 +155,10 @@ protected <E extends Entity> DriverFactory<E> createDriverFactory() {

public interface Refs {
@ReferencePath("/catalog") CatalogReference<TestEntity> catalog();
@ReferencePath("/catalog/-child-/catalog") CatalogReference<TestEntity> childCatalog(Identifier child);
@ReferencePath("/listing") ListingReference<TestEntity> listing();
@ReferencePath("/listing/-entity-") Reference<ListingEntry> listingEntry(Identifier entity);
@ReferencePath("/catalog/-child-/catalog") CatalogReference<TestEntity> childCatalog(Identifier child);
@ReferencePath("/values/string") Reference<String> valuesString();
}

private static final AtomicBoolean ALREADY_WARNED = new AtomicBoolean(false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,53 @@ void updateNonexistentField_ignored() throws InvalidTypeException, IOException,
assertEquals(expected, actual);
}

@ParametersByName
@UsesMongoService
void updateInsidePolyfill_works() throws IOException, InterruptedException, InvalidTypeException {
// We'll use this as an honest observer of the actual state
LOGGER.debug("Create Original bosk");
Bosk<TestEntity> originalBosk = new Bosk<TestEntity>(
"Original",
TestEntity.class,
this::initialRoot,
createDriverFactory()
);

LOGGER.debug("Create Upgradeable bosk");
Bosk<UpgradeableEntity> upgradeableBosk = new Bosk<UpgradeableEntity>(
"Upgradeable",
UpgradeableEntity.class,
(b) -> { throw new AssertionError("upgradeableBosk should use the state from MongoDB"); },
createDriverFactory()
);

LOGGER.debug("Ensure polyfill returns the right value on read");
TestValues polyfill;
try (var __ = upgradeableBosk.readContext()) {
polyfill = upgradeableBosk.rootReference().value().values();
}
assertEquals(TestValues.blank(), polyfill);

LOGGER.debug("Check state before");
Optional<TestValues> before;
try (var __ = originalBosk.readContext()) {
before = originalBosk.rootReference().value().values();
}
assertEquals(Optional.empty(), before); // Not there yet

LOGGER.debug("Perform update inside polyfill");
Refs refs = upgradeableBosk.buildReferences(Refs.class);
upgradeableBosk.driver().submitReplacement(refs.valuesString(), "new value");
originalBosk.driver().flush(); // Not the bosk that did the update!

LOGGER.debug("Check state after");
String after;
try (var __ = originalBosk.readContext()) {
after = originalBosk.rootReference().value().values().get().string();
}
assertEquals("new value", after); // Now it's there
}

@ParametersByName
@UsesMongoService
void deleteNonexistentField_ignored() throws InvalidTypeException, IOException, InterruptedException {
Expand Down Expand Up @@ -456,13 +503,6 @@ void refurbish_createsField() throws IOException, InterruptedException {
createDriverFactory()
);

LOGGER.debug("Ensure polyfill returns the right value on read");
TestValues polyfill;
try (var __ = upgradeableBosk.readContext()) {
polyfill = upgradeableBosk.rootReference().value().values();
}
assertEquals(TestValues.blank(), polyfill);

LOGGER.debug("Check state before");
Optional<TestValues> before;
try (var __ = originalBosk.readContext()) {
Expand Down

0 comments on commit a4c49cd

Please sign in to comment.