diff --git a/bosk-core/src/main/java/works/bosk/drivers/ForwardingDriver.java b/bosk-core/src/main/java/works/bosk/drivers/ForwardingDriver.java index a87f590b..da268248 100644 --- a/bosk-core/src/main/java/works/bosk/drivers/ForwardingDriver.java +++ b/bosk-core/src/main/java/works/bosk/drivers/ForwardingDriver.java @@ -2,8 +2,6 @@ import java.io.IOException; import java.lang.reflect.Type; -import java.util.ArrayList; -import java.util.List; import lombok.RequiredArgsConstructor; import works.bosk.BoskDriver; import works.bosk.Identifier; @@ -11,63 +9,51 @@ import works.bosk.StateTreeNode; import works.bosk.exceptions.InvalidTypeException; +/** + * Implements all {@link BoskDriver} methods by simply calling the corresponding + * methods on another driver. Useful for overriding one or two methods while leaving + * the rest unchanged. + *

+ * Unlike {@link ReplicaSet}, this does not automatically fix up the references to + * point to the right bosk. The references must already be from the downstream bosk. + */ @RequiredArgsConstructor public class ForwardingDriver implements BoskDriver { - private final Iterable> downstream; + protected final BoskDriver downstream; - /** - * @return The result of calling initialRoot on the first downstream driver - * that doesn't throw {@link UnsupportedOperationException}. Other exceptions are propagated as-is, - * and abort the initialization immediately. - */ @Override public R initialRoot(Type rootType) throws InvalidTypeException, IOException, InterruptedException { - List exceptions = new ArrayList<>(); - for (BoskDriver d: downstream) { - try { - return d.initialRoot(rootType); - } catch (UnsupportedOperationException e) { - exceptions.add(e); - } - } - - // Oh dear. - UnsupportedOperationException exception = new UnsupportedOperationException("Unable to forward initialRoot request"); - exceptions.forEach(exception::addSuppressed); - throw exception; + return downstream.initialRoot(rootType); } @Override public void submitReplacement(Reference target, T newValue) { - downstream.forEach(d -> d.submitReplacement(target, newValue)); + downstream.submitReplacement(target, newValue); } @Override public void submitConditionalReplacement(Reference target, T newValue, Reference precondition, Identifier requiredValue) { - downstream.forEach(d -> d.submitConditionalReplacement(target, newValue, precondition, requiredValue)); + downstream.submitConditionalReplacement(target, newValue, precondition, requiredValue); } @Override public void submitInitialization(Reference target, T newValue) { - downstream.forEach(d -> d.submitInitialization(target, newValue)); + downstream.submitInitialization(target, newValue); } @Override public void submitDeletion(Reference target) { - downstream.forEach(d -> d.submitDeletion(target)); + downstream.submitDeletion(target); } @Override public void submitConditionalDeletion(Reference target, Reference precondition, Identifier requiredValue) { - downstream.forEach(d -> d.submitConditionalDeletion(target, precondition, requiredValue)); + downstream.submitConditionalDeletion(target, precondition, requiredValue); } @Override public void flush() throws InterruptedException, IOException { - for (BoskDriver d: downstream) { - // Note that exceptions from a downstream flush() will abort this loop - d.flush(); - } + downstream.flush(); } @Override diff --git a/bosk-core/src/main/java/works/bosk/drivers/MirroringDriver.java b/bosk-core/src/main/java/works/bosk/drivers/MirroringDriver.java deleted file mode 100644 index 705db69a..00000000 --- a/bosk-core/src/main/java/works/bosk/drivers/MirroringDriver.java +++ /dev/null @@ -1,89 +0,0 @@ -package works.bosk.drivers; - -import java.io.IOException; -import java.lang.reflect.Type; -import lombok.RequiredArgsConstructor; -import works.bosk.Bosk; -import works.bosk.BoskDriver; -import works.bosk.DriverFactory; -import works.bosk.Identifier; -import works.bosk.Reference; -import works.bosk.StateTreeNode; -import works.bosk.exceptions.InvalidTypeException; - -import static java.util.Arrays.asList; -import static lombok.AccessLevel.PRIVATE; - -/** - * Sends events to another {@link Bosk} of the same type. - */ -@RequiredArgsConstructor(access=PRIVATE) -public class MirroringDriver implements BoskDriver { - private final Bosk mirror; - - /** - * Causes updates to be applied both to mirror and to the downstream driver. - */ - public static DriverFactory targeting(Bosk mirror) { - return (boskInfo, downstream) -> new ForwardingDriver<>(asList( - new MirroringDriver<>(mirror), - downstream - )); - } - - /** - * Causes updates to be applied only to other. - */ - public static MirroringDriver redirectingTo(Bosk other) { - return new MirroringDriver<>(other); - } - - @Override - public R initialRoot(Type rootType) { - throw new UnsupportedOperationException(MirroringDriver.class.getSimpleName() + " cannot supply an initial root"); - } - - @Override - public void submitReplacement(Reference target, T newValue) { - mirror.driver().submitReplacement(correspondingReference(target), newValue); - } - - @Override - public void submitConditionalReplacement(Reference target, T newValue, Reference precondition, Identifier requiredValue) { - mirror.driver().submitConditionalReplacement(correspondingReference(target), newValue, correspondingReference(precondition), requiredValue); - } - - @Override - public void submitInitialization(Reference target, T newValue) { - mirror.driver().submitInitialization(correspondingReference(target), newValue); - } - - @Override - public void submitDeletion(Reference target) { - mirror.driver().submitDeletion(correspondingReference(target)); - } - - @Override - public void submitConditionalDeletion(Reference target, Reference precondition, Identifier requiredValue) { - mirror.driver().submitConditionalDeletion(correspondingReference(target), correspondingReference(precondition), requiredValue); - } - - @Override - public void flush() throws InterruptedException, IOException { - mirror.driver().flush(); - } - - @SuppressWarnings("unchecked") - private Reference correspondingReference(Reference original) { - try { - return (Reference) mirror.rootReference().then(Object.class, original.path()); - } catch (InvalidTypeException e) { - throw new AssertionError("References are expected to be compatible: " + original, e); - } - } - - @Override - public String toString() { - return "Mirroring to " + mirror; - } -} diff --git a/bosk-core/src/main/java/works/bosk/drivers/NoOpDriver.java b/bosk-core/src/main/java/works/bosk/drivers/NoOpDriver.java new file mode 100644 index 00000000..5d413d84 --- /dev/null +++ b/bosk-core/src/main/java/works/bosk/drivers/NoOpDriver.java @@ -0,0 +1,28 @@ +package works.bosk.drivers; + +import java.io.IOException; +import java.lang.reflect.Type; +import works.bosk.BoskDriver; +import works.bosk.DriverFactory; +import works.bosk.Identifier; +import works.bosk.Reference; +import works.bosk.StateTreeNode; +import works.bosk.exceptions.InvalidTypeException; + +public class NoOpDriver implements BoskDriver { + public static DriverFactory factory() { + return (b,d) -> new NoOpDriver<>(); + } + + @Override + public R initialRoot(Type rootType) throws InvalidTypeException, IOException, InterruptedException { + throw new UnsupportedOperationException(); + } + + @Override public void submitReplacement(Reference target, T newValue) { } + @Override public void submitConditionalReplacement(Reference target, T newValue, Reference precondition, Identifier requiredValue) { } + @Override public void submitInitialization(Reference target, T newValue) { } + @Override public void submitDeletion(Reference target) { } + @Override public void submitConditionalDeletion(Reference target, Reference precondition, Identifier requiredValue) { } + @Override public void flush() throws IOException, InterruptedException { } +} diff --git a/bosk-core/src/main/java/works/bosk/drivers/ReplicaSet.java b/bosk-core/src/main/java/works/bosk/drivers/ReplicaSet.java index 99c1557b..42b1f0b4 100644 --- a/bosk-core/src/main/java/works/bosk/drivers/ReplicaSet.java +++ b/bosk-core/src/main/java/works/bosk/drivers/ReplicaSet.java @@ -6,6 +6,7 @@ import java.util.List; import java.util.Queue; import java.util.concurrent.ConcurrentLinkedQueue; +import works.bosk.Bosk; import works.bosk.BoskDriver; import works.bosk.DriverFactory; import works.bosk.Identifier; @@ -21,11 +22,21 @@ * Note that this isn't used for true distributed replica sets with one bosk in each JVM process, * but rather for constructing a local replica set within a single JVM process. *

- * Evolution note: This kind of subsumes the functionality of both {@link ForwardingDriver} - * and {@link MirroringDriver}. Seems like this class could either use or replace those. - * Perhaps {@link ForwardingDriver} should do the {@link Replica#correspondingReference} logic that - * {@link MirroringDriver} does, making the latter unnecessary; and then this class could - * simply be a {@link ForwardingDriver} whose list of downstream drivers is mutable. + * The primary way to use this class is to instantiate a {@code new ReplicaSet()}, + * then construct any number of bosks using {@link #driverFactory()}. + * All the resulting bosks will be in the replica set, and more can be added dynamically. + *

+ * There are also some factory methods that simplify some special use cases. + * + *

    + *
  1. + * Use {@link #mirroringTo} to mirror changes from a primary bosk to some number + * of secondary ones. + *
  2. + *
  3. + * Use {@link #redirectingTo} just to get a driver that can accept references to the wrong bosk. + *
  4. + *
*/ public class ReplicaSet { final Queue> replicas = new ConcurrentLinkedQueue<>(); @@ -43,8 +54,52 @@ public DriverFactory driverFactory() { }; } + /** + * Causes updates to be applied to {@code mirrors} and to the downstream driver. + *

+ * Assuming the returned factory is only used once, this has the effect of creating + * a fixed replica set to which new replicas can't be added dynamically; + * but the returned factory can be used multiple times. + */ + @SafeVarargs + public static DriverFactory mirroringTo(Bosk... mirrors) { + var replicaSet = new ReplicaSet(); + for (var m: mirrors) { + BoskDriver downstream = m.driver(); + replicaSet.replicas.add(new Replica<>( + m.rootReference(), + new ForwardingDriver<>(downstream) { + @Override + public RR initialRoot(Type rootType) { + throw new UnsupportedOperationException("Don't use initialRoot from " + m); + } + + @Override + public String toString() { + return downstream.toString() + " (minus initial state)"; + } + } + )); + } + return replicaSet.driverFactory(); + } + + /** + * Causes updates to be applied only to other. + */ + public static BoskDriver redirectingTo(Bosk other) { + // A ReplicaSet with only the one replica + return new ReplicaSet() + .driverFactory().build( + other, + other.driver() + ); + } + final class BroadcastShim implements BoskDriver { /** + * TODO: should return the current state somehow. For now, I guess it's best to attach all the bosks before submitting any updates. + * * @return The result of calling initialRoot on the first downstream driver * that doesn't throw {@link UnsupportedOperationException}. Other exceptions are propagated as-is, * and abort the initialization immediately. diff --git a/bosk-core/src/test/java/works/bosk/BoskConstructorTest.java b/bosk-core/src/test/java/works/bosk/BoskConstructorTest.java index c3d1dc7b..cb310020 100644 --- a/bosk-core/src/test/java/works/bosk/BoskConstructorTest.java +++ b/bosk-core/src/test/java/works/bosk/BoskConstructorTest.java @@ -10,10 +10,9 @@ import works.bosk.TypeValidationTest.MutableField; import works.bosk.TypeValidationTest.SimpleTypes; import works.bosk.drivers.ForwardingDriver; +import works.bosk.drivers.NoOpDriver; import works.bosk.exceptions.InvalidTypeException; -import static java.util.Collections.emptyList; -import static java.util.Collections.singleton; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -37,8 +36,8 @@ void basicProperties_correctValues() { name, rootType, _ -> root, - (b,d)-> { - driver.set(new ForwardingDriver<>(singleton(d))); + (_, d)-> { + driver.set(new ForwardingDriver<>(d)); return driver.get(); }); @@ -153,7 +152,7 @@ private static void assertDefaultRootThrows(Class expectedT @NotNull private static DriverFactory initialRootDriver(InitialRootFunction initialRootFunction) { - return (b,d) -> new ForwardingDriver(emptyList()) { + return (_, _) -> new NoOpDriver<>() { @Override public StateTreeNode initialRoot(Type rootType) throws InvalidTypeException, IOException, InterruptedException { return initialRootFunction.get(); diff --git a/bosk-core/src/test/java/works/bosk/DriverStackTest.java b/bosk-core/src/test/java/works/bosk/DriverStackTest.java index c7ab51aa..22749237 100644 --- a/bosk-core/src/test/java/works/bosk/DriverStackTest.java +++ b/bosk-core/src/test/java/works/bosk/DriverStackTest.java @@ -2,14 +2,13 @@ import org.junit.jupiter.api.Test; import works.bosk.drivers.ForwardingDriver; +import works.bosk.drivers.NoOpDriver; -import static java.util.Collections.emptySet; -import static java.util.Collections.singletonList; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertSame; class DriverStackTest { - final BoskDriver baseDriver = new ForwardingDriver<>(emptySet()); + final BoskDriver baseDriver = new NoOpDriver<>(); @Test void emptyStack_returnsDownstream() { @@ -25,8 +24,8 @@ void stackedDrivers_correctOrder() { ); TestDriver firstDriver = (TestDriver) stack.build(null, baseDriver); - TestDriver secondDriver = (TestDriver) firstDriver.downstream; - BoskDriver thirdDriver = secondDriver.downstream; + TestDriver secondDriver = (TestDriver) firstDriver.downstream(); + BoskDriver thirdDriver = secondDriver.downstream(); assertEquals("first", firstDriver.name); assertEquals("second", secondDriver.name); @@ -35,12 +34,14 @@ void stackedDrivers_correctOrder() { static class TestDriver extends ForwardingDriver { final String name; - final BoskDriver downstream; public TestDriver(String name, BoskDriver downstream) { - super(singletonList(downstream)); + super(downstream); this.name = name; - this.downstream = downstream; + } + + BoskDriver downstream() { + return downstream; } @Override diff --git a/bosk-core/src/test/java/works/bosk/drivers/ForwardingDriverConformanceTest.java b/bosk-core/src/test/java/works/bosk/drivers/ForwardingDriverConformanceTest.java index c025c049..1e9ada2f 100644 --- a/bosk-core/src/test/java/works/bosk/drivers/ForwardingDriverConformanceTest.java +++ b/bosk-core/src/test/java/works/bosk/drivers/ForwardingDriverConformanceTest.java @@ -2,13 +2,11 @@ import org.junit.jupiter.api.BeforeEach; -import static java.util.Collections.singletonList; - public class ForwardingDriverConformanceTest extends DriverConformanceTest { @BeforeEach void setupDriverFactory() { - driverFactory = (b,d)-> new ForwardingDriver<>(singletonList(d)); + driverFactory = (_, d)-> new ForwardingDriver<>(d); } } diff --git a/bosk-testing/src/main/java/works/bosk/drivers/AbstractDriverTest.java b/bosk-testing/src/main/java/works/bosk/drivers/AbstractDriverTest.java index 2debec3e..a87ecc33 100644 --- a/bosk-testing/src/main/java/works/bosk/drivers/AbstractDriverTest.java +++ b/bosk-testing/src/main/java/works/bosk/drivers/AbstractDriverTest.java @@ -53,7 +53,7 @@ protected void setupBosksAndReferences(DriverFactory driverFactory) // This is the bosk we're testing bosk = new Bosk(boskName("Test", 1), TestEntity.class, AbstractDriverTest::initialRoot, DriverStack.of( - MirroringDriver.targeting(canonicalBosk), + ReplicaSet.mirroringTo(canonicalBosk), DriverStateVerifier.wrap(driverFactory, TestEntity.class, AbstractDriverTest::initialRoot) )); driver = bosk.driver(); diff --git a/bosk-testing/src/main/java/works/bosk/drivers/DriverStateVerifier.java b/bosk-testing/src/main/java/works/bosk/drivers/DriverStateVerifier.java index dca36e82..a53b7b56 100644 --- a/bosk-testing/src/main/java/works/bosk/drivers/DriverStateVerifier.java +++ b/bosk-testing/src/main/java/works/bosk/drivers/DriverStateVerifier.java @@ -58,7 +58,7 @@ public static DriverFactory wrap(DriverFactory verifier = new DriverStateVerifier<>( stateTrackingBosk, - MirroringDriver.redirectingTo(stateTrackingBosk) + ReplicaSet.redirectingTo(stateTrackingBosk) ); return DriverStack.of( DiagnosticScopeDriver.factory(dc -> dc.withAttribute(THREAD_NAME, currentThread().getName())),