From 6d3b2c1b029a59d099375961703a449c7f84afef Mon Sep 17 00:00:00 2001 From: Guy Youansi Date: Sat, 22 Feb 2020 20:17:17 +0100 Subject: [PATCH 01/93] Issue-197: add npm task for preparing node-support to be able to build the js-shopping-cart docker image --- samples/js-shopping-cart/package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/samples/js-shopping-cart/package.json b/samples/js-shopping-cart/package.json index fe86ec887..4111f35b7 100644 --- a/samples/js-shopping-cart/package.json +++ b/samples/js-shopping-cart/package.json @@ -38,9 +38,10 @@ "prestart": "compile-descriptor ../../protocols/example/shoppingcart/shoppingcart.proto", "pretest": "compile-descriptor ../../protocols/example/shoppingcart/shoppingcart.proto", "postinstall": "compile-descriptor ../../protocols/example/shoppingcart/shoppingcart.proto", + "prepare-node-support": "cd ../../node-support && npm install && cd ../samples/js-shopping-cart", "start": "node index.js", "start-no-prestart": "node index.js", - "dockerbuild": "docker build -f ../../Dockerfile.js-shopping-cart -t ${DOCKER_PUBLISH_TO:-cloudstateio}/js-shopping-cart:latest ../..", + "dockerbuild": "npm run prepare-node-support && docker build -f ../../Dockerfile.js-shopping-cart -t ${DOCKER_PUBLISH_TO:-cloudstateio}/js-shopping-cart:latest ../..", "dockerpush": "docker push ${DOCKER_PUBLISH_TO:-cloudstateio}/js-shopping-cart:latest", "dockerbuildpush": "npm run dockerbuild && npm run dockerpush" } From 38327d3d745ea6e720a382def6d878b6e48fee64 Mon Sep 17 00:00:00 2001 From: Guy Youansi Date: Mon, 30 Mar 2020 22:30:13 +0200 Subject: [PATCH 02/93] Proposal for CRUD on top of Event Sourcing --- .../impl/eventsourced/EventSourcedImpl.scala | 6 +- .../eventsourced/EventSourcedEntity.scala | 6 +- .../shoppingcart/ShoppingCartCrudEntity.java | 107 ++++++++++++++++++ 3 files changed, 116 insertions(+), 3 deletions(-) create mode 100644 samples/java-shopping-cart/src/main/java/io/cloudstate/samples/shoppingcart/ShoppingCartCrudEntity.java diff --git a/java-support/src/main/scala/io/cloudstate/javasupport/impl/eventsourced/EventSourcedImpl.scala b/java-support/src/main/scala/io/cloudstate/javasupport/impl/eventsourced/EventSourcedImpl.scala index 923fc83f7..ea7f0fb82 100644 --- a/java-support/src/main/scala/io/cloudstate/javasupport/impl/eventsourced/EventSourcedImpl.scala +++ b/java-support/src/main/scala/io/cloudstate/javasupport/impl/eventsourced/EventSourcedImpl.scala @@ -131,6 +131,7 @@ final class EventSourcedImpl(_system: ActorSystem, .map(_.message) .scan[(Long, Option[EventSourcedStreamOut.Message])]((startingSequenceNumber, None)) { case (_, InEvent(event)) => + // No needed here because the UserFunction does not deal with events val context = new EventContextImpl(entityId, event.sequence) val ev = ScalaPbAny.toJavaProto(event.payload.get) // FIXME empty? handler.handleEvent(ev, context) @@ -226,9 +227,12 @@ final class EventSourcedImpl(_system: ActorSystem, override def emit(event: AnyRef): Unit = { checkActive() + //TODO: we need some kind of abstraction here between Event Sourcing and CRUD + //TODO: investigate to do compression here because it is the whole object graph which can be big val encoded = anySupport.encodeScala(event) val nextSequenceNumber = sequenceNumber + events.size + 1 - handler.handleEvent(ScalaPbAny.toJavaProto(encoded), new EventContextImpl(entityId, nextSequenceNumber)) + // No needed here because the UserFunction does not deal with events + //handler.handleEvent(ScalaPbAny.toJavaProto(encoded), new EventContextImpl(entityId, nextSequenceNumber)) events :+= encoded performSnapshot = (snapshotEvery > 0) && (performSnapshot || (nextSequenceNumber % snapshotEvery == 0)) } diff --git a/proxy/core/src/main/scala/io/cloudstate/proxy/eventsourced/EventSourcedEntity.scala b/proxy/core/src/main/scala/io/cloudstate/proxy/eventsourced/EventSourcedEntity.scala index 0688949de..9622c46bc 100644 --- a/proxy/core/src/main/scala/io/cloudstate/proxy/eventsourced/EventSourcedEntity.scala +++ b/proxy/core/src/main/scala/io/cloudstate/proxy/eventsourced/EventSourcedEntity.scala @@ -306,7 +306,10 @@ final class EventSourcedEntity(configuration: EventSourcedEntity.Configuration, throw error case SaveSnapshotSuccess(metadata) => - // Nothing to do + // TODO: is it he right place delete the oldest snapshots and events? + // TODO ideally we want a one to one mapping between event and snapshot. + deleteSnapshots(SnapshotSelectionCriteria().copy(maxSequenceNr = metadata.sequenceNr - 1)) + deleteMessages(metadata.sequenceNr - 1) case SaveSnapshotFailure(metadata, cause) => log.error("Error saving snapshot", cause) @@ -349,7 +352,6 @@ final class EventSourcedEntity(configuration: EventSourcedEntity.Configuration, case event: pbAny => maybeInit(None) - relay ! EventSourcedStreamIn(EventSourcedStreamIn.Message.Event(EventSourcedEvent(lastSequenceNr, Some(event)))) } private def reportDatabaseOperationStarted(): Unit = diff --git a/samples/java-shopping-cart/src/main/java/io/cloudstate/samples/shoppingcart/ShoppingCartCrudEntity.java b/samples/java-shopping-cart/src/main/java/io/cloudstate/samples/shoppingcart/ShoppingCartCrudEntity.java new file mode 100644 index 000000000..67a9d4dbf --- /dev/null +++ b/samples/java-shopping-cart/src/main/java/io/cloudstate/samples/shoppingcart/ShoppingCartCrudEntity.java @@ -0,0 +1,107 @@ +package io.cloudstate.samples.shoppingcart; + +import com.example.shoppingcart.Shoppingcart; +import com.example.shoppingcart.persistence.Domain; +import com.google.protobuf.Empty; +import io.cloudstate.javasupport.EntityId; +import io.cloudstate.javasupport.eventsourced.CommandContext; +import io.cloudstate.javasupport.eventsourced.CommandHandler; +import io.cloudstate.javasupport.eventsourced.EventSourcedEntity; +import io.cloudstate.javasupport.eventsourced.Snapshot; +import io.cloudstate.javasupport.eventsourced.SnapshotHandler; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** An event sourced entity. */ +@EventSourcedEntity(snapshotEvery = 1) +public class ShoppingCartCrudEntity { + private final String entityId; + private final Map cart = new LinkedHashMap<>(); + + public ShoppingCartCrudEntity(@EntityId String entityId) { + this.entityId = entityId; + } + + @Snapshot + public Domain.Cart snapshot() { + return Domain.Cart.newBuilder() + .addAllItems(cart.values().stream().map(this::convert).collect(Collectors.toList())) + .build(); + } + + @SnapshotHandler + public void handleSnapshot(Domain.Cart cart) { + this.cart.clear(); + for (Domain.LineItem item : cart.getItemsList()) { + this.cart.put(item.getProductId(), convert(item)); + } + } + + @CommandHandler + public Shoppingcart.Cart getCart() { + return Shoppingcart.Cart.newBuilder().addAllItems(cart.values()).build(); + } + + @CommandHandler + public Empty addItem(Shoppingcart.AddLineItem item, CommandContext ctx) { + if (item.getQuantity() <= 0) { + ctx.fail("Cannot add negative quantity of to lineItem" + item.getProductId()); + } + + Domain.LineItem lineItem = + Domain.LineItem.newBuilder() + .setProductId(item.getProductId()) + .setName(item.getName()) + .setQuantity(quantity(item)) + .build(); + + List lineItems = + cart.values().stream() + .map(this::convert) + .filter(someItem -> !someItem.getProductId().equals(item.getProductId())) + .collect(Collectors.toList()); + + ctx.emit(Domain.Cart.newBuilder().addAllItems(lineItems).addItems(lineItem).build()); + return Empty.getDefaultInstance(); + } + + @CommandHandler + public Empty removeItem(Shoppingcart.RemoveLineItem item, CommandContext ctx) { + if (!cart.containsKey(item.getProductId())) { + ctx.fail("Cannot remove item " + item.getProductId() + " because it is not in the cart."); + } + + List lineItems = + cart.values().stream() + .map(this::convert) + .filter(someItem -> someItem.getProductId().equals(item.getProductId())) + .collect(Collectors.toList()); + + ctx.emit(Domain.Cart.newBuilder().addAllItems(lineItems).build()); + return Empty.getDefaultInstance(); + } + + private Shoppingcart.LineItem convert(Domain.LineItem item) { + return Shoppingcart.LineItem.newBuilder() + .setProductId(item.getProductId()) + .setName(item.getName()) + .setQuantity(item.getQuantity()) + .build(); + } + + private Domain.LineItem convert(Shoppingcart.LineItem item) { + return Domain.LineItem.newBuilder() + .setProductId(item.getProductId()) + .setName(item.getName()) + .setQuantity(item.getQuantity()) + .build(); + } + + private int quantity(Shoppingcart.AddLineItem item) { + Shoppingcart.LineItem lineItem = cart.get(item.getProductId()); + return lineItem == null ? item.getQuantity() : lineItem.getQuantity() + item.getQuantity(); + } +} From 6cc25fedd266dd059bfb65c1cb96c2c770278993 Mon Sep 17 00:00:00 2001 From: Guy Youansi Date: Mon, 30 Mar 2020 22:37:56 +0200 Subject: [PATCH 03/93] Proposal for CRUD on top of Event Sourcing; Comment code handling events --- .../javasupport/impl/eventsourced/EventSourcedImpl.scala | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/java-support/src/main/scala/io/cloudstate/javasupport/impl/eventsourced/EventSourcedImpl.scala b/java-support/src/main/scala/io/cloudstate/javasupport/impl/eventsourced/EventSourcedImpl.scala index ea7f0fb82..cc4800f7f 100644 --- a/java-support/src/main/scala/io/cloudstate/javasupport/impl/eventsourced/EventSourcedImpl.scala +++ b/java-support/src/main/scala/io/cloudstate/javasupport/impl/eventsourced/EventSourcedImpl.scala @@ -132,9 +132,9 @@ final class EventSourcedImpl(_system: ActorSystem, .scan[(Long, Option[EventSourcedStreamOut.Message])]((startingSequenceNumber, None)) { case (_, InEvent(event)) => // No needed here because the UserFunction does not deal with events - val context = new EventContextImpl(entityId, event.sequence) - val ev = ScalaPbAny.toJavaProto(event.payload.get) // FIXME empty? - handler.handleEvent(ev, context) + //val context = new EventContextImpl(entityId, event.sequence) + //val ev = ScalaPbAny.toJavaProto(event.payload.get) // FIXME empty? + //handler.handleEvent(ev, context) (event.sequence, None) case ((sequence, _), InCommand(command)) => if (entityId != command.entityId) From 9a7a8bfd19d80070e75c1f00401daa7fcbca5cfe Mon Sep 17 00:00:00 2001 From: Guy Youansi Date: Tue, 31 Mar 2020 01:20:17 +0200 Subject: [PATCH 04/93] rollback changes in the proxy and add proto file for the CRUD domain --- .../impl/eventsourced/EventSourcedImpl.scala | 12 ++--- .../crud.persistence/domain.proto | 23 +++++++++ .../eventsourced/EventSourcedEntity.scala | 6 +-- .../shoppingcart/ShoppingCartCrudEntity.java | 47 +++++++++++-------- 4 files changed, 56 insertions(+), 32 deletions(-) create mode 100644 protocols/example/shoppingcart/crud.persistence/domain.proto diff --git a/java-support/src/main/scala/io/cloudstate/javasupport/impl/eventsourced/EventSourcedImpl.scala b/java-support/src/main/scala/io/cloudstate/javasupport/impl/eventsourced/EventSourcedImpl.scala index cc4800f7f..923fc83f7 100644 --- a/java-support/src/main/scala/io/cloudstate/javasupport/impl/eventsourced/EventSourcedImpl.scala +++ b/java-support/src/main/scala/io/cloudstate/javasupport/impl/eventsourced/EventSourcedImpl.scala @@ -131,10 +131,9 @@ final class EventSourcedImpl(_system: ActorSystem, .map(_.message) .scan[(Long, Option[EventSourcedStreamOut.Message])]((startingSequenceNumber, None)) { case (_, InEvent(event)) => - // No needed here because the UserFunction does not deal with events - //val context = new EventContextImpl(entityId, event.sequence) - //val ev = ScalaPbAny.toJavaProto(event.payload.get) // FIXME empty? - //handler.handleEvent(ev, context) + val context = new EventContextImpl(entityId, event.sequence) + val ev = ScalaPbAny.toJavaProto(event.payload.get) // FIXME empty? + handler.handleEvent(ev, context) (event.sequence, None) case ((sequence, _), InCommand(command)) => if (entityId != command.entityId) @@ -227,12 +226,9 @@ final class EventSourcedImpl(_system: ActorSystem, override def emit(event: AnyRef): Unit = { checkActive() - //TODO: we need some kind of abstraction here between Event Sourcing and CRUD - //TODO: investigate to do compression here because it is the whole object graph which can be big val encoded = anySupport.encodeScala(event) val nextSequenceNumber = sequenceNumber + events.size + 1 - // No needed here because the UserFunction does not deal with events - //handler.handleEvent(ScalaPbAny.toJavaProto(encoded), new EventContextImpl(entityId, nextSequenceNumber)) + handler.handleEvent(ScalaPbAny.toJavaProto(encoded), new EventContextImpl(entityId, nextSequenceNumber)) events :+= encoded performSnapshot = (snapshotEvery > 0) && (performSnapshot || (nextSequenceNumber % snapshotEvery == 0)) } diff --git a/protocols/example/shoppingcart/crud.persistence/domain.proto b/protocols/example/shoppingcart/crud.persistence/domain.proto new file mode 100644 index 000000000..27e937927 --- /dev/null +++ b/protocols/example/shoppingcart/crud.persistence/domain.proto @@ -0,0 +1,23 @@ +// These are the messages that get persisted - the events, plus the current state (Cart) for snapshots. +syntax = "proto3"; + +package com.example.shoppingcart.crud.persistence; + +option go_package = "crud.persistence"; + +message LineItem { + string productId = 1; + string name = 2; + int32 quantity = 3; +} + +// The shopping cart state. +message Cart { + repeated LineItem items = 1; +} + +// The car modification event. +message CartModification { + repeated LineItem items = 1; +} + diff --git a/proxy/core/src/main/scala/io/cloudstate/proxy/eventsourced/EventSourcedEntity.scala b/proxy/core/src/main/scala/io/cloudstate/proxy/eventsourced/EventSourcedEntity.scala index 9622c46bc..0688949de 100644 --- a/proxy/core/src/main/scala/io/cloudstate/proxy/eventsourced/EventSourcedEntity.scala +++ b/proxy/core/src/main/scala/io/cloudstate/proxy/eventsourced/EventSourcedEntity.scala @@ -306,10 +306,7 @@ final class EventSourcedEntity(configuration: EventSourcedEntity.Configuration, throw error case SaveSnapshotSuccess(metadata) => - // TODO: is it he right place delete the oldest snapshots and events? - // TODO ideally we want a one to one mapping between event and snapshot. - deleteSnapshots(SnapshotSelectionCriteria().copy(maxSequenceNr = metadata.sequenceNr - 1)) - deleteMessages(metadata.sequenceNr - 1) + // Nothing to do case SaveSnapshotFailure(metadata, cause) => log.error("Error saving snapshot", cause) @@ -352,6 +349,7 @@ final class EventSourcedEntity(configuration: EventSourcedEntity.Configuration, case event: pbAny => maybeInit(None) + relay ! EventSourcedStreamIn(EventSourcedStreamIn.Message.Event(EventSourcedEvent(lastSequenceNr, Some(event)))) } private def reportDatabaseOperationStarted(): Unit = diff --git a/samples/java-shopping-cart/src/main/java/io/cloudstate/samples/shoppingcart/ShoppingCartCrudEntity.java b/samples/java-shopping-cart/src/main/java/io/cloudstate/samples/shoppingcart/ShoppingCartCrudEntity.java index 67a9d4dbf..57744ae3a 100644 --- a/samples/java-shopping-cart/src/main/java/io/cloudstate/samples/shoppingcart/ShoppingCartCrudEntity.java +++ b/samples/java-shopping-cart/src/main/java/io/cloudstate/samples/shoppingcart/ShoppingCartCrudEntity.java @@ -1,25 +1,27 @@ package io.cloudstate.samples.shoppingcart; import com.example.shoppingcart.Shoppingcart; -import com.example.shoppingcart.persistence.Domain; +import com.example.shoppingcart.crud.persistence.Domain; import com.google.protobuf.Empty; import io.cloudstate.javasupport.EntityId; import io.cloudstate.javasupport.eventsourced.CommandContext; import io.cloudstate.javasupport.eventsourced.CommandHandler; +import io.cloudstate.javasupport.eventsourced.EventHandler; import io.cloudstate.javasupport.eventsourced.EventSourcedEntity; import io.cloudstate.javasupport.eventsourced.Snapshot; import io.cloudstate.javasupport.eventsourced.SnapshotHandler; +import java.util.Collection; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.stream.Collectors; -/** An event sourced entity. */ -@EventSourcedEntity(snapshotEvery = 1) +/** A CRUD entity. */ +@EventSourcedEntity public class ShoppingCartCrudEntity { private final String entityId; - private final Map cart = new LinkedHashMap<>(); + private final Map cart = new LinkedHashMap<>(); public ShoppingCartCrudEntity(@EntityId String entityId) { this.entityId = entityId; @@ -28,7 +30,7 @@ public ShoppingCartCrudEntity(@EntityId String entityId) { @Snapshot public Domain.Cart snapshot() { return Domain.Cart.newBuilder() - .addAllItems(cart.values().stream().map(this::convert).collect(Collectors.toList())) + .addAllItems(cart.values().stream().collect(Collectors.toList())) .build(); } @@ -36,13 +38,20 @@ public Domain.Cart snapshot() { public void handleSnapshot(Domain.Cart cart) { this.cart.clear(); for (Domain.LineItem item : cart.getItemsList()) { - this.cart.put(item.getProductId(), convert(item)); + this.cart.put(item.getProductId(), item); } } + @EventHandler + public void cartModification(Domain.CartModification modification) { + applyModification(modification); + } + @CommandHandler public Shoppingcart.Cart getCart() { - return Shoppingcart.Cart.newBuilder().addAllItems(cart.values()).build(); + Collection lineItems = + cart.values().stream().map(this::convert).collect(Collectors.toList()); + return Shoppingcart.Cart.newBuilder().addAllItems(lineItems).build(); } @CommandHandler @@ -60,11 +69,11 @@ public Empty addItem(Shoppingcart.AddLineItem item, CommandContext ctx) { List lineItems = cart.values().stream() - .map(this::convert) .filter(someItem -> !someItem.getProductId().equals(item.getProductId())) .collect(Collectors.toList()); - ctx.emit(Domain.Cart.newBuilder().addAllItems(lineItems).addItems(lineItem).build()); + ctx.emit( + Domain.CartModification.newBuilder().addAllItems(lineItems).addItems(lineItem).build()); return Empty.getDefaultInstance(); } @@ -76,11 +85,10 @@ public Empty removeItem(Shoppingcart.RemoveLineItem item, CommandContext ctx) { List lineItems = cart.values().stream() - .map(this::convert) .filter(someItem -> someItem.getProductId().equals(item.getProductId())) .collect(Collectors.toList()); - ctx.emit(Domain.Cart.newBuilder().addAllItems(lineItems).build()); + ctx.emit(Domain.CartModification.newBuilder().addAllItems(lineItems).build()); return Empty.getDefaultInstance(); } @@ -92,16 +100,15 @@ private Shoppingcart.LineItem convert(Domain.LineItem item) { .build(); } - private Domain.LineItem convert(Shoppingcart.LineItem item) { - return Domain.LineItem.newBuilder() - .setProductId(item.getProductId()) - .setName(item.getName()) - .setQuantity(item.getQuantity()) - .build(); - } - private int quantity(Shoppingcart.AddLineItem item) { - Shoppingcart.LineItem lineItem = cart.get(item.getProductId()); + Domain.LineItem lineItem = cart.get(item.getProductId()); return lineItem == null ? item.getQuantity() : lineItem.getQuantity() + item.getQuantity(); } + + private void applyModification(Domain.CartModification car) { + this.cart.clear(); + for (Domain.LineItem item : car.getItemsList()) { + this.cart.put(item.getProductId(), item); + } + } } From db2e4a84496c581d3afdff2787fd4898769d35e1 Mon Sep 17 00:00:00 2001 From: Guy Youansi Date: Tue, 31 Mar 2020 18:57:31 +0200 Subject: [PATCH 05/93] Add CrudEntity annotation. Add registration for CrudEntity in CloudState java language support. Add and example --- .../io/cloudstate/javasupport/CloudState.java | 45 +++++++ .../javasupport/crud/CrudEntity.java | 22 ++++ .../crud.persistence/domain.proto | 2 +- .../cloudstate/samples/shoppingcart/Main.java | 6 +- .../shoppingcart/ShoppingCartCrudEntity.java | 119 +++++++++++------- 5 files changed, 147 insertions(+), 47 deletions(-) create mode 100644 java-support/src/main/java/io/cloudstate/javasupport/crud/CrudEntity.java diff --git a/java-support/src/main/java/io/cloudstate/javasupport/CloudState.java b/java-support/src/main/java/io/cloudstate/javasupport/CloudState.java index 53f57c884..aafd7f7e1 100644 --- a/java-support/src/main/java/io/cloudstate/javasupport/CloudState.java +++ b/java-support/src/main/java/io/cloudstate/javasupport/CloudState.java @@ -4,6 +4,7 @@ import com.google.protobuf.Descriptors; import io.cloudstate.javasupport.crdt.CrdtEntity; import io.cloudstate.javasupport.crdt.CrdtEntityFactory; +import io.cloudstate.javasupport.crud.CrudEntity; import io.cloudstate.javasupport.eventsourced.EventSourcedEntity; import io.cloudstate.javasupport.eventsourced.EventSourcedEntityFactory; import io.cloudstate.javasupport.impl.AnySupport; @@ -154,6 +155,50 @@ public CloudState registerEventSourcedEntity( return this; } + /** + * Register an annotated crud entity. + * + *

The entity class must be annotated with {@link io.cloudstate.javasupport.crud.CrudEntity}. + * + * @param entityClass The entity class. + * @param descriptor The descriptor for the service that this entity implements. + * @param additionalDescriptors Any additional descriptors that should be used to look up protobuf + * types when needed. + * @return This stateful service builder. + */ + public CloudState registerCrudEntity( + Class entityClass, + Descriptors.ServiceDescriptor descriptor, + Descriptors.FileDescriptor... additionalDescriptors) { + + CrudEntity entity = entityClass.getAnnotation(CrudEntity.class); + if (entity == null) { + throw new IllegalArgumentException( + entityClass + " does not declare an " + CrudEntity.class + " annotation!"); + } + + final String persistenceId; + final int snapshotEvery = 1; + if (entity.persistenceId().isEmpty()) { + persistenceId = entityClass.getSimpleName(); + } else { + persistenceId = entity.persistenceId(); + } + + final AnySupport anySupport = newAnySupport(additionalDescriptors); + + services.put( + descriptor.getFullName(), + new EventSourcedStatefulService( + new AnnotationBasedEventSourcedSupport(entityClass, anySupport, descriptor), + descriptor, + anySupport, + persistenceId, + snapshotEvery)); + + return this; + } + /** * Register an annotated CRDT entity. * diff --git a/java-support/src/main/java/io/cloudstate/javasupport/crud/CrudEntity.java b/java-support/src/main/java/io/cloudstate/javasupport/crud/CrudEntity.java new file mode 100644 index 000000000..d5303bf20 --- /dev/null +++ b/java-support/src/main/java/io/cloudstate/javasupport/crud/CrudEntity.java @@ -0,0 +1,22 @@ +package io.cloudstate.javasupport.crud; + +import io.cloudstate.javasupport.impl.CloudStateAnnotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** An crud entity. */ +@CloudStateAnnotation +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface CrudEntity { + /** + * The name of the persistence id. + * + *

If not specifed, defaults to the entities unqualified classname. It's strongly recommended + * that you specify it explicitly. + */ + String persistenceId() default ""; +} diff --git a/protocols/example/shoppingcart/crud.persistence/domain.proto b/protocols/example/shoppingcart/crud.persistence/domain.proto index 27e937927..99b3d7fac 100644 --- a/protocols/example/shoppingcart/crud.persistence/domain.proto +++ b/protocols/example/shoppingcart/crud.persistence/domain.proto @@ -16,7 +16,7 @@ message Cart { repeated LineItem items = 1; } -// The car modification event. +// The shopping cart modification event. message CartModification { repeated LineItem items = 1; } diff --git a/samples/java-shopping-cart/src/main/java/io/cloudstate/samples/shoppingcart/Main.java b/samples/java-shopping-cart/src/main/java/io/cloudstate/samples/shoppingcart/Main.java index 6521315e2..979426e7d 100644 --- a/samples/java-shopping-cart/src/main/java/io/cloudstate/samples/shoppingcart/Main.java +++ b/samples/java-shopping-cart/src/main/java/io/cloudstate/samples/shoppingcart/Main.java @@ -6,10 +6,10 @@ public final class Main { public static final void main(String[] args) throws Exception { new CloudState() - .registerEventSourcedEntity( - ShoppingCartEntity.class, + .registerCrudEntity( + ShoppingCartCrudEntity.class, Shoppingcart.getDescriptor().findServiceByName("ShoppingCart"), - com.example.shoppingcart.persistence.Domain.getDescriptor()) + com.example.shoppingcart.crud.persistence.Domain.getDescriptor()) .start() .toCompletableFuture() .get(); diff --git a/samples/java-shopping-cart/src/main/java/io/cloudstate/samples/shoppingcart/ShoppingCartCrudEntity.java b/samples/java-shopping-cart/src/main/java/io/cloudstate/samples/shoppingcart/ShoppingCartCrudEntity.java index 57744ae3a..3ce8f1961 100644 --- a/samples/java-shopping-cart/src/main/java/io/cloudstate/samples/shoppingcart/ShoppingCartCrudEntity.java +++ b/samples/java-shopping-cart/src/main/java/io/cloudstate/samples/shoppingcart/ShoppingCartCrudEntity.java @@ -4,24 +4,24 @@ import com.example.shoppingcart.crud.persistence.Domain; import com.google.protobuf.Empty; import io.cloudstate.javasupport.EntityId; +import io.cloudstate.javasupport.crud.CrudEntity; import io.cloudstate.javasupport.eventsourced.CommandContext; import io.cloudstate.javasupport.eventsourced.CommandHandler; import io.cloudstate.javasupport.eventsourced.EventHandler; -import io.cloudstate.javasupport.eventsourced.EventSourcedEntity; import io.cloudstate.javasupport.eventsourced.Snapshot; import io.cloudstate.javasupport.eventsourced.SnapshotHandler; import java.util.Collection; import java.util.LinkedHashMap; -import java.util.List; import java.util.Map; import java.util.stream.Collectors; + /** A CRUD entity. */ -@EventSourcedEntity +@CrudEntity public class ShoppingCartCrudEntity { private final String entityId; - private final Map cart = new LinkedHashMap<>(); + private final CrudState state = new CrudState(); public ShoppingCartCrudEntity(@EntityId String entityId) { this.entityId = entityId; @@ -30,27 +30,25 @@ public ShoppingCartCrudEntity(@EntityId String entityId) { @Snapshot public Domain.Cart snapshot() { return Domain.Cart.newBuilder() - .addAllItems(cart.values().stream().collect(Collectors.toList())) - .build(); + .addAllItems(state.cart.values().stream().collect(Collectors.toList())) + .build(); } @SnapshotHandler public void handleSnapshot(Domain.Cart cart) { - this.cart.clear(); - for (Domain.LineItem item : cart.getItemsList()) { - this.cart.put(item.getProductId(), item); - } + state.resetTo(cart); } @EventHandler public void cartModification(Domain.CartModification modification) { - applyModification(modification); + state.applyModification(modification); } @CommandHandler public Shoppingcart.Cart getCart() { Collection lineItems = - cart.values().stream().map(this::convert).collect(Collectors.toList()); + state.cart.values().stream().map(this::convert).collect(Collectors.toList()); + return Shoppingcart.Cart.newBuilder().addAllItems(lineItems).build(); } @@ -60,55 +58,90 @@ public Empty addItem(Shoppingcart.AddLineItem item, CommandContext ctx) { ctx.fail("Cannot add negative quantity of to lineItem" + item.getProductId()); } - Domain.LineItem lineItem = - Domain.LineItem.newBuilder() - .setProductId(item.getProductId()) - .setName(item.getName()) - .setQuantity(quantity(item)) - .build(); - - List lineItems = - cart.values().stream() - .filter(someItem -> !someItem.getProductId().equals(item.getProductId())) - .collect(Collectors.toList()); + Domain.CartModification modification = state.applyCommand(item).toModification(); + ctx.emit(modification); - ctx.emit( - Domain.CartModification.newBuilder().addAllItems(lineItems).addItems(lineItem).build()); return Empty.getDefaultInstance(); } @CommandHandler public Empty removeItem(Shoppingcart.RemoveLineItem item, CommandContext ctx) { - if (!cart.containsKey(item.getProductId())) { + if (!state.cart.containsKey(item.getProductId())) { ctx.fail("Cannot remove item " + item.getProductId() + " because it is not in the cart."); } - List lineItems = - cart.values().stream() - .filter(someItem -> someItem.getProductId().equals(item.getProductId())) - .collect(Collectors.toList()); + Domain.CartModification modification = state.applyCommand(item).toModification(); + ctx.emit(modification); - ctx.emit(Domain.CartModification.newBuilder().addAllItems(lineItems).build()); return Empty.getDefaultInstance(); } private Shoppingcart.LineItem convert(Domain.LineItem item) { return Shoppingcart.LineItem.newBuilder() - .setProductId(item.getProductId()) - .setName(item.getName()) - .setQuantity(item.getQuantity()) - .build(); + .setProductId(item.getProductId()) + .setName(item.getName()) + .setQuantity(item.getQuantity()) + .build(); } - private int quantity(Shoppingcart.AddLineItem item) { - Domain.LineItem lineItem = cart.get(item.getProductId()); - return lineItem == null ? item.getQuantity() : lineItem.getQuantity() + item.getQuantity(); - } + // It would be good to externalize the methods (applyModification, resetTo and toModification) as interface in the language support + // We would like to deal with the compression of the event because it could very big + // We would like to deal with the number of events because i think only the last one is important for projection + private static final class CrudState { + + private final Map cart; + + private CrudState() { + this(new LinkedHashMap<>()); + } + + private CrudState(Map cart) { + this.cart = new LinkedHashMap<>(cart); + } + + private CrudState applyCommand(Shoppingcart.AddLineItem item) { + Domain.LineItem lineItem = + Domain.LineItem.newBuilder() + .setProductId(item.getProductId()) + .setName(item.getName()) + .setQuantity(quantity(item)) + .build(); + + LinkedHashMap map = new LinkedHashMap<>(cart); + map.put(lineItem.getProductId(), lineItem); + return new CrudState(map); + } + + private CrudState applyCommand(Shoppingcart.RemoveLineItem item) { + LinkedHashMap map = new LinkedHashMap<>(cart); + map.remove(item.getProductId()); + return new CrudState(map); + } - private void applyModification(Domain.CartModification car) { - this.cart.clear(); - for (Domain.LineItem item : car.getItemsList()) { - this.cart.put(item.getProductId(), item); + private void applyModification(Domain.CartModification modification) { + this.cart.clear(); + for (Domain.LineItem item : modification.getItemsList()) { + this.cart.put(item.getProductId(), item); + } + } + + private void resetTo(Domain.Cart cart) { + this.cart.clear(); + for (Domain.LineItem item : cart.getItemsList()) { + this.cart.put(item.getProductId(), item); + } + } + + private Domain.CartModification toModification() { + return Domain.CartModification.newBuilder() + .addAllItems(cart.values().stream().collect(Collectors.toList())) + .build(); + } + + private int quantity(Shoppingcart.AddLineItem item) { + Domain.LineItem lineItem = cart.get(item.getProductId()); + return lineItem == null ? item.getQuantity() : lineItem.getQuantity() + item.getQuantity(); } } } + From 2374b7b9a54cfcae2dc68fe204dd424f7f3bab8d Mon Sep 17 00:00:00 2001 From: Guy Youansi Date: Tue, 14 Apr 2020 15:16:23 +0200 Subject: [PATCH 06/93] change CrudEntity annotation. Change registration for CrudEntity in CloudState java language support. Add KeyValue entity that the CRUD entity use. The KeyValue entity has support for last updates and removes. --- .../io/cloudstate/javasupport/CloudState.java | 29 +- .../javasupport/crud/CrudEntity.java | 9 +- .../cloudstate/javasupport/crud/KeyValue.java | 248 ++++++++++++++++++ .../example/shoppingcart/shoppingcart.proto | 8 + protocols/frontend/cloudstate/key_value.proto | 30 +++ .../cloudstate/samples/shoppingcart/Main.java | 6 +- .../shoppingcart/ShoppingCartCrudEntity.java | 211 +++++++-------- 7 files changed, 422 insertions(+), 119 deletions(-) create mode 100644 java-support/src/main/java/io/cloudstate/javasupport/crud/KeyValue.java create mode 100644 protocols/frontend/cloudstate/key_value.proto diff --git a/java-support/src/main/java/io/cloudstate/javasupport/CloudState.java b/java-support/src/main/java/io/cloudstate/javasupport/CloudState.java index aafd7f7e1..558d0f37a 100644 --- a/java-support/src/main/java/io/cloudstate/javasupport/CloudState.java +++ b/java-support/src/main/java/io/cloudstate/javasupport/CloudState.java @@ -14,6 +14,9 @@ import io.cloudstate.javasupport.impl.eventsourced.EventSourcedStatefulService; import akka.Done; + +import java.util.Arrays; +import java.util.List; import java.util.concurrent.CompletionStage; import java.util.HashMap; import java.util.Map; @@ -167,34 +170,36 @@ public CloudState registerEventSourcedEntity( * @return This stateful service builder. */ public CloudState registerCrudEntity( - Class entityClass, - Descriptors.ServiceDescriptor descriptor, - Descriptors.FileDescriptor... additionalDescriptors) { + Class entityClass, + Descriptors.ServiceDescriptor descriptor, + Descriptors.FileDescriptor... additionalDescriptors) { CrudEntity entity = entityClass.getAnnotation(CrudEntity.class); if (entity == null) { throw new IllegalArgumentException( - entityClass + " does not declare an " + CrudEntity.class + " annotation!"); + entityClass + " does not declare an " + CrudEntity.class + " annotation!"); } final String persistenceId; - final int snapshotEvery = 1; + final int snapshotEvery; if (entity.persistenceId().isEmpty()) { persistenceId = entityClass.getSimpleName(); + snapshotEvery = 0; // Default } else { persistenceId = entity.persistenceId(); + snapshotEvery = entity.snapshotEvery(); } final AnySupport anySupport = newAnySupport(additionalDescriptors); services.put( - descriptor.getFullName(), - new EventSourcedStatefulService( - new AnnotationBasedEventSourcedSupport(entityClass, anySupport, descriptor), - descriptor, - anySupport, - persistenceId, - snapshotEvery)); + descriptor.getFullName(), + new EventSourcedStatefulService( + new AnnotationBasedEventSourcedSupport(entityClass, anySupport, descriptor), + descriptor, + anySupport, + persistenceId, + snapshotEvery)); return this; } diff --git a/java-support/src/main/java/io/cloudstate/javasupport/crud/CrudEntity.java b/java-support/src/main/java/io/cloudstate/javasupport/crud/CrudEntity.java index d5303bf20..1c6205f69 100644 --- a/java-support/src/main/java/io/cloudstate/javasupport/crud/CrudEntity.java +++ b/java-support/src/main/java/io/cloudstate/javasupport/crud/CrudEntity.java @@ -15,8 +15,15 @@ /** * The name of the persistence id. * - *

If not specifed, defaults to the entities unqualified classname. It's strongly recommended + *

If not specified, defaults to the entities unqualified classname. It's strongly recommended * that you specify it explicitly. */ String persistenceId() default ""; + + /** + * Specifies how snapshots of the entity state should be made: Zero means use default from + * configuration file. (Default) Any negative value means never snapshot. Any positive value means + * snapshot at-or-after that number of events. + */ + int snapshotEvery() default 0; } diff --git a/java-support/src/main/java/io/cloudstate/javasupport/crud/KeyValue.java b/java-support/src/main/java/io/cloudstate/javasupport/crud/KeyValue.java new file mode 100644 index 000000000..31c782949 --- /dev/null +++ b/java-support/src/main/java/io/cloudstate/javasupport/crud/KeyValue.java @@ -0,0 +1,248 @@ +package io.cloudstate.javasupport.crud; + +import akka.util.ByteString; +import io.cloudstate.javasupport.eventsourced.EventHandler; +import io.cloudstate.javasupport.eventsourced.Snapshot; +import io.cloudstate.javasupport.eventsourced.SnapshotHandler; +import io.cloudstate.keyvalue.KeyValue.KVEntity; +import io.cloudstate.keyvalue.KeyValue.KVModification; +import io.cloudstate.keyvalue.KeyValue.KVModificationOrBuilder; + +import java.util.Optional; +import java.util.function.Function; + +import static java.util.Objects.requireNonNull; + +public final class KeyValue { + public static final class Key implements Comparable> { + private final String _name; + private final Function _reader; + private final Function _writer; + + Key( + final String name, + final Function reader, + final Function writer) { + this._name = name; + this._reader = reader; + this._writer = writer; + } + + public final String name() { + return this._name; + } + + public final Function reader() { + return this._reader; + } + + public final Function writer() { + return this._writer; + } + + @Override + public final boolean equals(final Object that) { + if (this == that) return true; + else if (that instanceof Key) return ((Key) that).name().equals(this.name()); + else return false; + } + + @Override + public final int hashCode() { + return 403 + name().hashCode(); + } + + @Override + public final int compareTo(final Key other) { + return name().compareTo(other.name()); + } + } + + public static final Key keyOf( + final String name, + final Function reader, + final Function writer) { + return new Key( + requireNonNull(name, "Key name cannot be null"), + requireNonNull(reader, "Key reader cannot be null"), + requireNonNull(writer, "Key writer cannot be null")); + } + + // represents the persistent state for last changed for the key value entity + private static final class ChangeMap { + // updated and removed collections are mutual exclusive meaning a key should be only in one + // collection. + + // used for persisting all last updated by keys + private final java.util.Map updated = new java.util.TreeMap<>(); + // used for persisting all last removed by keys + private final java.util.Set removed = new java.util.TreeSet<>(); + + private void setUpdatedKey(String key, ByteString value) { + updated.put(key, value); + removed.remove(key); + } + + private void setRemovedKey(String key) { + removed.add(key); + updated.remove(key); + } + + private KVModification kvModification() { + final KVModification.Builder builder = KVModification.newBuilder(); + updated.forEach( + (k, v) -> { + builder.putUpdatedEntries(k, com.google.protobuf.ByteString.copyFrom(v.asByteBuffer())); + }); + removed.forEach(builder::addRemovedKeys); + return builder.build(); + } + } + + public static final class Map { + /* + TODO document invariants + */ + private final java.util.Map unparsed; + private final java.util.Map, Object> updated = new java.util.TreeMap<>(); + private final java.util.Set removed = new java.util.TreeSet<>(); + private final ChangeMap lastChanged = new ChangeMap(); + + public Map() { + this.unparsed = new java.util.TreeMap<>(); + } + + public final Optional get(final Key key) { + final T value = (T) updated.get(key); + if (value != null) { + return Optional.of(value); + } else { + final ByteString bytes = unparsed.get(key.name()); + if (bytes != null) { + final T parsed = key.reader().apply(bytes); + requireNonNull(parsed, "Key reader not allowed to read `null`"); + updated.put((Key) key, parsed); + return Optional.of(parsed); + } else { + return Optional.empty(); + } + } + } + + public final void set(final Key key, final T value) { + requireNonNull(key, "Map key must not be null"); + requireNonNull(value, "Map value must not be null"); + updated.put(key, value); + unparsed.remove(key.name()); + removed.remove(key.name()); + } + + public final boolean remove(final Key key) { + requireNonNull(key, "Map key must not be null"); + if (!removed.contains(key.name()) + && (updated.remove(key) != null | unparsed.remove(key.name()) != null)) { + removed.add(key.name()); + return true; + } else { + return false; + } + } + + final KVEntity toProto() { + final KVEntity.Builder builder = KVEntity.newBuilder(); + unparsed.forEach( + (k, v) -> { + builder.putEntries(k, com.google.protobuf.ByteString.copyFrom(v.asByteBuffer())); + }); + updated.forEach( + (k, v) -> { + // Skip as this item is only a parsed un-changed item, and we already added those in the + // previous step + if (!unparsed.containsKey(k.name())) { + builder.putEntries( + k.name(), + com.google.protobuf.ByteString.copyFrom( + ((Key) k).writer().apply(v).asByteBuffer())); + } + }); + return builder.build(); + } + + public final KVModification toProtoModification() { + updated.forEach( + (k, v) -> { + // Skip those which remain as unparsed, as they have not been changed + if (!unparsed.containsKey(k.name())) { + lastChanged.setUpdatedKey(k.name(), ((Key) k).writer().apply(v)); + } + }); + removed.forEach(lastChanged::setRemovedKey); + return lastChanged.kvModification(); + } + + final void resetTo(KVEntity entityState) { + unparsed.clear(); + updated.clear(); + removed.clear(); + entityState + .getEntriesMap() + .forEach( + (k, v) -> + unparsed.put( + k, + v.isEmpty() + ? ByteString.empty() + : ByteString.fromArrayUnsafe(v.toByteArray()))); + } + + final void applyModification(KVModificationOrBuilder modification) { + // Apply new modifications to the base unparsed values + modification + .getUpdatedEntriesMap() + .forEach( + (k, v) -> { + unparsed.put( + k, + v.isEmpty() ? ByteString.empty() : ByteString.fromArrayUnsafe(v.toByteArray())); + + lastChanged.setUpdatedKey( + k, + v.isEmpty() + ? ByteString.empty() + : ByteString.fromArrayUnsafe( + v.toByteArray())); // restore the persisted last updated keys + }); + + modification + .getRemovedKeysList() + .forEach( + k -> { + unparsed.remove(k); + lastChanged.setRemovedKey(k); // restored the persisted last removed keys + }); + } + } + + public abstract static class KeyValueEntity { + private final Map state = new Map(); + + protected Map state() { + return state; + } + + @Snapshot + public KVEntity snapshot() { + return state.toProto(); + } + + @SnapshotHandler + public void handleSnapshot(final KVEntity entityState) { + state.resetTo(entityState); + } + + @EventHandler + public void kVModification(final KVModification modification) { + state.applyModification(modification); + } + } +} diff --git a/protocols/example/shoppingcart/shoppingcart.proto b/protocols/example/shoppingcart/shoppingcart.proto index 27e1baf54..e8a14d6ed 100644 --- a/protocols/example/shoppingcart/shoppingcart.proto +++ b/protocols/example/shoppingcart/shoppingcart.proto @@ -24,6 +24,10 @@ message RemoveLineItem { string product_id = 2; } +message RemoveShoppingCart { + string user_id = 1 [(.cloudstate.entity_key) = true]; +} + message GetShoppingCart { string user_id = 1 [(.cloudstate.entity_key) = true]; } @@ -51,6 +55,10 @@ service ShoppingCart { option (google.api.http).post = "/cart/{user_id}/items/{product_id}/remove"; } + rpc RemoveCart(RemoveShoppingCart) returns (google.protobuf.Empty) { + option (google.api.http).post = "/cart/{user_id}/remove"; + } + rpc GetCart(GetShoppingCart) returns (Cart) { option (google.api.http) = { get: "/carts/{user_id}", diff --git a/protocols/frontend/cloudstate/key_value.proto b/protocols/frontend/cloudstate/key_value.proto new file mode 100644 index 000000000..18bd46f10 --- /dev/null +++ b/protocols/frontend/cloudstate/key_value.proto @@ -0,0 +1,30 @@ +// Copyright 2019 Lightbend Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +import "google/protobuf/any.proto"; + +package cloudstate; + +option java_package = "io.cloudstate.keyvalue"; + +message KVEntity { + map entries = 1; +} + +message KVModification { + map updated_entries = 1; + repeated string removed_keys = 2; +} diff --git a/samples/java-shopping-cart/src/main/java/io/cloudstate/samples/shoppingcart/Main.java b/samples/java-shopping-cart/src/main/java/io/cloudstate/samples/shoppingcart/Main.java index 979426e7d..6b2d8aab4 100644 --- a/samples/java-shopping-cart/src/main/java/io/cloudstate/samples/shoppingcart/Main.java +++ b/samples/java-shopping-cart/src/main/java/io/cloudstate/samples/shoppingcart/Main.java @@ -5,11 +5,15 @@ public final class Main { public static final void main(String[] args) throws Exception { + // it would be better to register this descriptor + // io.cloudstate.keyvalue.KeyValue.getDescriptor() in the registerCrudEntity + // so it is implicit to the user new CloudState() .registerCrudEntity( ShoppingCartCrudEntity.class, Shoppingcart.getDescriptor().findServiceByName("ShoppingCart"), - com.example.shoppingcart.crud.persistence.Domain.getDescriptor()) + com.example.shoppingcart.crud.persistence.Domain.getDescriptor(), + io.cloudstate.keyvalue.KeyValue.getDescriptor()) .start() .toCompletableFuture() .get(); diff --git a/samples/java-shopping-cart/src/main/java/io/cloudstate/samples/shoppingcart/ShoppingCartCrudEntity.java b/samples/java-shopping-cart/src/main/java/io/cloudstate/samples/shoppingcart/ShoppingCartCrudEntity.java index 3ce8f1961..466ae0027 100644 --- a/samples/java-shopping-cart/src/main/java/io/cloudstate/samples/shoppingcart/ShoppingCartCrudEntity.java +++ b/samples/java-shopping-cart/src/main/java/io/cloudstate/samples/shoppingcart/ShoppingCartCrudEntity.java @@ -1,147 +1,148 @@ package io.cloudstate.samples.shoppingcart; +import akka.util.ByteString; import com.example.shoppingcart.Shoppingcart; import com.example.shoppingcart.crud.persistence.Domain; -import com.google.protobuf.Empty; -import io.cloudstate.javasupport.EntityId; +import com.google.protobuf.InvalidProtocolBufferException; import io.cloudstate.javasupport.crud.CrudEntity; +import io.cloudstate.javasupport.crud.KeyValue; +import io.cloudstate.javasupport.crud.KeyValue.Key; import io.cloudstate.javasupport.eventsourced.CommandContext; import io.cloudstate.javasupport.eventsourced.CommandHandler; -import io.cloudstate.javasupport.eventsourced.EventHandler; -import io.cloudstate.javasupport.eventsourced.Snapshot; -import io.cloudstate.javasupport.eventsourced.SnapshotHandler; import java.util.Collection; -import java.util.LinkedHashMap; -import java.util.Map; +import java.util.List; import java.util.stream.Collectors; +import static io.cloudstate.javasupport.crud.KeyValue.keyOf; -/** A CRUD entity. */ @CrudEntity -public class ShoppingCartCrudEntity { - private final String entityId; - private final CrudState state = new CrudState(); - - public ShoppingCartCrudEntity(@EntityId String entityId) { - this.entityId = entityId; - } - - @Snapshot - public Domain.Cart snapshot() { - return Domain.Cart.newBuilder() - .addAllItems(state.cart.values().stream().collect(Collectors.toList())) - .build(); - } - - @SnapshotHandler - public void handleSnapshot(Domain.Cart cart) { - state.resetTo(cart); - } - - @EventHandler - public void cartModification(Domain.CartModification modification) { - state.applyModification(modification); - } +public class ShoppingCartCrudEntity extends KeyValue.KeyValueEntity { @CommandHandler - public Shoppingcart.Cart getCart() { - Collection lineItems = - state.cart.values().stream().map(this::convert).collect(Collectors.toList()); + public com.google.protobuf.Empty addItem(Shoppingcart.AddLineItem item, CommandContext ctx) { + if (item.getQuantity() <= 0) { + ctx.fail("Cannot add negative quantity of to item" + item.getProductId()); + } - return Shoppingcart.Cart.newBuilder().addAllItems(lineItems).build(); - } + Key userId = keyOf(item.getUserId(), ByteString::utf8String, ByteString::fromString); + if (!state().get(userId).isPresent()) { + Domain.Cart cart = + Domain.Cart.newBuilder() + .addItems( + Domain.LineItem.newBuilder() + .setProductId(item.getProductId()) + .setName(item.getName()) + .setQuantity(item.getQuantity()) + .build()) + .build(); - @CommandHandler - public Empty addItem(Shoppingcart.AddLineItem item, CommandContext ctx) { - if (item.getQuantity() <= 0) { - ctx.fail("Cannot add negative quantity of to lineItem" + item.getProductId()); + state().set(userId, cart.toByteString().toStringUtf8()); + } else { + Domain.Cart cart = deserialize(userId); + Domain.LineItem lineItem = + Domain.LineItem.newBuilder() + .setProductId(item.getProductId()) + .setName(item.getName()) + .setQuantity(quantity(cart, item)) + .build(); + List items = + cart.getItemsList().stream() + .filter(i -> !i.getProductId().equals(item.getProductId())) + .collect(Collectors.toList()); + Domain.Cart modifiedCart = + Domain.Cart.newBuilder().addAllItems(items).addItems(lineItem).build(); + state().set(userId, modifiedCart.toByteString().toStringUtf8()); } - Domain.CartModification modification = state.applyCommand(item).toModification(); - ctx.emit(modification); + ctx.emit(state().toProtoModification()); - return Empty.getDefaultInstance(); + return com.google.protobuf.Empty.getDefaultInstance(); } @CommandHandler - public Empty removeItem(Shoppingcart.RemoveLineItem item, CommandContext ctx) { - if (!state.cart.containsKey(item.getProductId())) { + public com.google.protobuf.Empty removeItem( + Shoppingcart.RemoveLineItem item, CommandContext ctx) { + Key userId = keyOf(item.getUserId(), ByteString::utf8String, ByteString::fromString); + if (!state().get(userId).isPresent()) { + ctx.fail( + "Cannot remove item " + item.getProductId() + " for unknown user " + item.getUserId()); + } + + Domain.Cart cart = deserialize(userId); + if (!containsItem(cart, item)) { ctx.fail("Cannot remove item " + item.getProductId() + " because it is not in the cart."); } - Domain.CartModification modification = state.applyCommand(item).toModification(); - ctx.emit(modification); + List items = + cart.getItemsList().stream() + .filter(lineItem -> !lineItem.getProductId().equals(item.getProductId())) + .collect(Collectors.toList()); + Domain.Cart modifiedCart = Domain.Cart.newBuilder().addAllItems(items).build(); - return Empty.getDefaultInstance(); - } + state().set(userId, modifiedCart.toByteString().toStringUtf8()); + ctx.emit(state().toProtoModification()); - private Shoppingcart.LineItem convert(Domain.LineItem item) { - return Shoppingcart.LineItem.newBuilder() - .setProductId(item.getProductId()) - .setName(item.getName()) - .setQuantity(item.getQuantity()) - .build(); + return com.google.protobuf.Empty.getDefaultInstance(); } - // It would be good to externalize the methods (applyModification, resetTo and toModification) as interface in the language support - // We would like to deal with the compression of the event because it could very big - // We would like to deal with the number of events because i think only the last one is important for projection - private static final class CrudState { - - private final Map cart; - - private CrudState() { - this(new LinkedHashMap<>()); + @CommandHandler + public com.google.protobuf.Empty removeCart( + Shoppingcart.RemoveShoppingCart cart, CommandContext ctx) { + Key userId = keyOf(cart.getUserId(), ByteString::utf8String, ByteString::fromString); + if (!state().get(userId).isPresent()) { + ctx.fail("Cannot remove cart " + cart.getUserId() + " because it is unknown"); } - private CrudState(Map cart) { - this.cart = new LinkedHashMap<>(cart); - } + state().remove(userId); + ctx.emit(state().toProtoModification()); - private CrudState applyCommand(Shoppingcart.AddLineItem item) { - Domain.LineItem lineItem = - Domain.LineItem.newBuilder() - .setProductId(item.getProductId()) - .setName(item.getName()) - .setQuantity(quantity(item)) - .build(); - - LinkedHashMap map = new LinkedHashMap<>(cart); - map.put(lineItem.getProductId(), lineItem); - return new CrudState(map); - } + return com.google.protobuf.Empty.getDefaultInstance(); + } - private CrudState applyCommand(Shoppingcart.RemoveLineItem item) { - LinkedHashMap map = new LinkedHashMap<>(cart); - map.remove(item.getProductId()); - return new CrudState(map); + @CommandHandler + public Shoppingcart.Cart getCart(Shoppingcart.GetShoppingCart cartId, CommandContext ctx) { + Key userId = keyOf(cartId.getUserId(), ByteString::utf8String, ByteString::fromString); + if (!state().get(userId).isPresent()) { + return Shoppingcart.Cart.newBuilder().build(); + } else { + Domain.Cart cart = deserialize(userId); + Collection lineItems = + cart.getItemsList().stream().map(this::convert).collect(Collectors.toList()); + return Shoppingcart.Cart.newBuilder().addAllItems(lineItems).build(); } + } - private void applyModification(Domain.CartModification modification) { - this.cart.clear(); - for (Domain.LineItem item : modification.getItemsList()) { - this.cart.put(item.getProductId(), item); - } + // it should be externalize + private Domain.Cart deserialize(Key userId) { + try { + return Domain.Cart.parseFrom( + com.google.protobuf.ByteString.copyFromUtf8(state().get(userId).get())); + } catch (InvalidProtocolBufferException e) { + return null; } + } - private void resetTo(Domain.Cart cart) { - this.cart.clear(); - for (Domain.LineItem item : cart.getItemsList()) { - this.cart.put(item.getProductId(), item); - } - } + private int quantity(Domain.Cart cart, Shoppingcart.AddLineItem item) { + return cart.getItemsList().stream() + .filter(lineItem -> lineItem.getProductId().equals(item.getProductId())) + .findFirst() + .map(lineItem -> lineItem.getQuantity() + item.getQuantity()) + .orElse(item.getQuantity()); + } - private Domain.CartModification toModification() { - return Domain.CartModification.newBuilder() - .addAllItems(cart.values().stream().collect(Collectors.toList())) - .build(); - } + private boolean containsItem(Domain.Cart cart, Shoppingcart.RemoveLineItem item) { + return cart.getItemsList().stream() + .filter(lineItem -> lineItem.getProductId().equals(item.getProductId())) + .findFirst() + .isPresent(); + } - private int quantity(Shoppingcart.AddLineItem item) { - Domain.LineItem lineItem = cart.get(item.getProductId()); - return lineItem == null ? item.getQuantity() : lineItem.getQuantity() + item.getQuantity(); - } + private Shoppingcart.LineItem convert(Domain.LineItem item) { + return Shoppingcart.LineItem.newBuilder() + .setProductId(item.getProductId()) + .setName(item.getName()) + .setQuantity(item.getQuantity()) + .build(); } } - From 78b7840d97783a96c656f0b12bbbda9a4bee9dfb Mon Sep 17 00:00:00 2001 From: Guy Youansi Date: Thu, 14 May 2020 22:51:11 +0200 Subject: [PATCH 07/93] Initial version for the CRUD protocol and the implementation --- .../javasupport/crud/CommandContext.java | 42 ++ .../javasupport/crud/CommandHandler.java | 38 ++ .../javasupport/crud/CrudContext.java | 6 + .../crud/CrudEntityCreationContext.java | 8 + .../crud/CrudEntityEventHandler.java | 29 ++ .../javasupport/crud/CrudEntityFactory.java | 20 + .../javasupport/crud/CrudEntityHandler.java | 39 ++ .../javasupport/crud/CrudEventContext.java | 11 + .../javasupport/crud/EventHandler.java | 32 ++ .../cloudstate/javasupport/crud/Snapshot.java | 24 ++ .../javasupport/crud/SnapshotContext.java | 11 + .../javasupport/crud/SnapshotHandler.java | 26 ++ .../javasupport/crud/package-info.java | 14 + .../javasupport/crudtwo/CommandContext.java | 41 ++ .../javasupport/crudtwo/CommandHandler.java | 38 ++ .../javasupport/crudtwo/CrudContext.java | 6 + .../javasupport/crudtwo/CrudEntity.java | 29 ++ .../crudtwo/CrudEntityCreationContext.java | 8 + .../crudtwo/CrudEntityEventHandler.java | 30 ++ .../crudtwo/CrudEntityFactory.java | 20 + .../crudtwo/CrudEntityHandler.java | 23 ++ .../javasupport/crudtwo/CrudEventContext.java | 13 + .../javasupport/crudtwo/EventHandler.java | 32 ++ .../javasupport/crudtwo/Snapshot.java | 25 ++ .../javasupport/crudtwo/SnapshotContext.java | 11 + .../javasupport/crudtwo/SnapshotHandler.java | 26 ++ .../javasupport/crudtwo/package-info.java | 14 + .../crudone/AnnotationBasedCrudSupport.scala | 348 ++++++++++++++++ .../javasupport/impl/crudone/CrudImpl.scala | 378 ++++++++++++++++++ .../crudtwo/AnnotationBasedCrudSupport.scala | 312 +++++++++++++++ .../javasupport/impl/crudtwo/CrudImpl.scala | 139 +++++++ .../AnnotationBasedCrudSupportSpec.scala | 84 ++++ .../frontend/cloudstate/sub_entity_key.proto | 30 ++ protocols/protocol/cloudstate/crud_one.proto | 159 ++++++++ protocols/protocol/cloudstate/crud_two.proto | 224 +++++++++++ .../proxy/UserFunctionTypeSupport.scala | 21 +- .../cloudstate/proxy/crudone/CrudEntity.scala | 368 +++++++++++++++++ .../proxy/crudone/CrudSupportFactory.scala | 95 +++++ .../cloudstate/proxy/crudtwo/CrudEntity.scala | 358 +++++++++++++++++ .../proxy/crudtwo/CrudSupportFactory.scala | 107 +++++ 40 files changed, 3237 insertions(+), 2 deletions(-) create mode 100644 java-support/src/main/java/io/cloudstate/javasupport/crud/CommandContext.java create mode 100644 java-support/src/main/java/io/cloudstate/javasupport/crud/CommandHandler.java create mode 100644 java-support/src/main/java/io/cloudstate/javasupport/crud/CrudContext.java create mode 100644 java-support/src/main/java/io/cloudstate/javasupport/crud/CrudEntityCreationContext.java create mode 100644 java-support/src/main/java/io/cloudstate/javasupport/crud/CrudEntityEventHandler.java create mode 100644 java-support/src/main/java/io/cloudstate/javasupport/crud/CrudEntityFactory.java create mode 100644 java-support/src/main/java/io/cloudstate/javasupport/crud/CrudEntityHandler.java create mode 100644 java-support/src/main/java/io/cloudstate/javasupport/crud/CrudEventContext.java create mode 100644 java-support/src/main/java/io/cloudstate/javasupport/crud/EventHandler.java create mode 100644 java-support/src/main/java/io/cloudstate/javasupport/crud/Snapshot.java create mode 100644 java-support/src/main/java/io/cloudstate/javasupport/crud/SnapshotContext.java create mode 100644 java-support/src/main/java/io/cloudstate/javasupport/crud/SnapshotHandler.java create mode 100644 java-support/src/main/java/io/cloudstate/javasupport/crud/package-info.java create mode 100644 java-support/src/main/java/io/cloudstate/javasupport/crudtwo/CommandContext.java create mode 100644 java-support/src/main/java/io/cloudstate/javasupport/crudtwo/CommandHandler.java create mode 100644 java-support/src/main/java/io/cloudstate/javasupport/crudtwo/CrudContext.java create mode 100644 java-support/src/main/java/io/cloudstate/javasupport/crudtwo/CrudEntity.java create mode 100644 java-support/src/main/java/io/cloudstate/javasupport/crudtwo/CrudEntityCreationContext.java create mode 100644 java-support/src/main/java/io/cloudstate/javasupport/crudtwo/CrudEntityEventHandler.java create mode 100644 java-support/src/main/java/io/cloudstate/javasupport/crudtwo/CrudEntityFactory.java create mode 100644 java-support/src/main/java/io/cloudstate/javasupport/crudtwo/CrudEntityHandler.java create mode 100644 java-support/src/main/java/io/cloudstate/javasupport/crudtwo/CrudEventContext.java create mode 100644 java-support/src/main/java/io/cloudstate/javasupport/crudtwo/EventHandler.java create mode 100644 java-support/src/main/java/io/cloudstate/javasupport/crudtwo/Snapshot.java create mode 100644 java-support/src/main/java/io/cloudstate/javasupport/crudtwo/SnapshotContext.java create mode 100644 java-support/src/main/java/io/cloudstate/javasupport/crudtwo/SnapshotHandler.java create mode 100644 java-support/src/main/java/io/cloudstate/javasupport/crudtwo/package-info.java create mode 100644 java-support/src/main/scala/io/cloudstate/javasupport/impl/crudone/AnnotationBasedCrudSupport.scala create mode 100644 java-support/src/main/scala/io/cloudstate/javasupport/impl/crudone/CrudImpl.scala create mode 100644 java-support/src/main/scala/io/cloudstate/javasupport/impl/crudtwo/AnnotationBasedCrudSupport.scala create mode 100644 java-support/src/main/scala/io/cloudstate/javasupport/impl/crudtwo/CrudImpl.scala create mode 100644 java-support/src/test/scala/io/cloudstate/javasupport/impl/crudone/AnnotationBasedCrudSupportSpec.scala create mode 100644 protocols/frontend/cloudstate/sub_entity_key.proto create mode 100644 protocols/protocol/cloudstate/crud_one.proto create mode 100644 protocols/protocol/cloudstate/crud_two.proto create mode 100644 proxy/core/src/main/scala/io/cloudstate/proxy/crudone/CrudEntity.scala create mode 100644 proxy/core/src/main/scala/io/cloudstate/proxy/crudone/CrudSupportFactory.scala create mode 100644 proxy/core/src/main/scala/io/cloudstate/proxy/crudtwo/CrudEntity.scala create mode 100644 proxy/core/src/main/scala/io/cloudstate/proxy/crudtwo/CrudSupportFactory.scala diff --git a/java-support/src/main/java/io/cloudstate/javasupport/crud/CommandContext.java b/java-support/src/main/java/io/cloudstate/javasupport/crud/CommandContext.java new file mode 100644 index 000000000..38056ac7d --- /dev/null +++ b/java-support/src/main/java/io/cloudstate/javasupport/crud/CommandContext.java @@ -0,0 +1,42 @@ +package io.cloudstate.javasupport.crud; + +import io.cloudstate.javasupport.ClientActionContext; +import io.cloudstate.javasupport.EffectContext; + +/** + * An event sourced command context. + * + *

Methods annotated with {@link CommandHandler} may take this is a parameter. It allows emitting + * new events in response to a command, along with forwarding the result to other entities, and + * performing side effects on other entities. + */ +public interface CommandContext extends CrudContext, ClientActionContext, EffectContext { + /** + * The current sequence number of events in this entity. + * + * @return The current sequence number. + */ + long sequenceNumber(); + + /** + * The name of the command being executed. + * + * @return The name of the command. + */ + String commandName(); + + /** + * The id of the command being executed. + * + * @return The id of the command. + */ + long commandId(); + + /** + * Emit the given event. The event will be persisted, and the handler of the event defined in the + * current behavior will immediately be executed to pick it up. + * + * @param event The event to emit. + */ + void emit(Object event); +} diff --git a/java-support/src/main/java/io/cloudstate/javasupport/crud/CommandHandler.java b/java-support/src/main/java/io/cloudstate/javasupport/crud/CommandHandler.java new file mode 100644 index 000000000..46f1cbd5c --- /dev/null +++ b/java-support/src/main/java/io/cloudstate/javasupport/crud/CommandHandler.java @@ -0,0 +1,38 @@ +package io.cloudstate.javasupport.crud; + +import io.cloudstate.javasupport.impl.CloudStateAnnotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Marks a method as a command handler. + * + *

This method will be invoked whenever the service call with name that matches this command + * handlers name is invoked. + * + *

The method may take the command object as a parameter, its type must match the gRPC service + * input type. + * + *

The return type of the method must match the gRPC services output type. + * + *

The method may also take a {@link CommandContext}, and/or a {@link + * io.cloudstate.javasupport.EntityId} annotated {@link String} parameter. + */ +@CloudStateAnnotation +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface CommandHandler { + + /** + * The name of the command to handle. + * + *

If not specified, the name of the method will be used as the command name, with the first + * letter capitalized to match the gRPC convention of capitalizing rpc method names. + * + * @return The command name. + */ + String name() default ""; +} diff --git a/java-support/src/main/java/io/cloudstate/javasupport/crud/CrudContext.java b/java-support/src/main/java/io/cloudstate/javasupport/crud/CrudContext.java new file mode 100644 index 000000000..b0bcc3808 --- /dev/null +++ b/java-support/src/main/java/io/cloudstate/javasupport/crud/CrudContext.java @@ -0,0 +1,6 @@ +package io.cloudstate.javasupport.crud; + +import io.cloudstate.javasupport.EntityContext; + +/** Root context for all event sourcing contexts. */ +public interface CrudContext extends EntityContext {} diff --git a/java-support/src/main/java/io/cloudstate/javasupport/crud/CrudEntityCreationContext.java b/java-support/src/main/java/io/cloudstate/javasupport/crud/CrudEntityCreationContext.java new file mode 100644 index 000000000..37c4da96f --- /dev/null +++ b/java-support/src/main/java/io/cloudstate/javasupport/crud/CrudEntityCreationContext.java @@ -0,0 +1,8 @@ +package io.cloudstate.javasupport.crud; + +/** + * Creation context for {@link CrudEntity} annotated entities. + * + *

This may be accepted as an argument to the constructor of an event sourced entity. + */ +public interface CrudEntityCreationContext extends CrudContext {} diff --git a/java-support/src/main/java/io/cloudstate/javasupport/crud/CrudEntityEventHandler.java b/java-support/src/main/java/io/cloudstate/javasupport/crud/CrudEntityEventHandler.java new file mode 100644 index 000000000..3d017d39c --- /dev/null +++ b/java-support/src/main/java/io/cloudstate/javasupport/crud/CrudEntityEventHandler.java @@ -0,0 +1,29 @@ +package io.cloudstate.javasupport.crud; + +import com.google.protobuf.Any; + +import java.util.Optional; + +/** + * Low level interface for handling events and commands on an entity. + * + *

Generally, this should not be needed, instead, a class annotated with the {@link + * EventHandler}, {@link CommandHandler} and similar annotations should be used. + */ +public interface CrudEntityEventHandler { + + /** + * Handle the given snapshot. + * + * @param snapshot The snapshot to handle. + * @param context The snapshot context. + */ + void handleSnapshot(Any snapshot, SnapshotContext context); + + /** + * Snapshot the object. + * + * @return The current snapshot, if this object supports snapshoting, otherwise empty. + */ + Optional snapshot(SnapshotContext context); +} diff --git a/java-support/src/main/java/io/cloudstate/javasupport/crud/CrudEntityFactory.java b/java-support/src/main/java/io/cloudstate/javasupport/crud/CrudEntityFactory.java new file mode 100644 index 000000000..ec58a8942 --- /dev/null +++ b/java-support/src/main/java/io/cloudstate/javasupport/crud/CrudEntityFactory.java @@ -0,0 +1,20 @@ +package io.cloudstate.javasupport.crud; + +import io.cloudstate.javasupport.eventsourced.CommandHandler; +import io.cloudstate.javasupport.eventsourced.EventHandler; + +/** + * Low level interface for handling events and commands on an entity. + * + *

Generally, this should not be needed, instead, a class annotated with the {@link + * EventHandler}, {@link CommandHandler} and similar annotations should be used. + */ +public interface CrudEntityFactory { + /** + * Create an entity handler for the given context. + * + * @param context The context. + * @return The handler for the given context. + */ + CrudEntityHandler create(CrudContext context); +} diff --git a/java-support/src/main/java/io/cloudstate/javasupport/crud/CrudEntityHandler.java b/java-support/src/main/java/io/cloudstate/javasupport/crud/CrudEntityHandler.java new file mode 100644 index 000000000..4bd4b536e --- /dev/null +++ b/java-support/src/main/java/io/cloudstate/javasupport/crud/CrudEntityHandler.java @@ -0,0 +1,39 @@ +package io.cloudstate.javasupport.crud; + +import com.google.protobuf.Any; + +import java.util.Optional; + +/** + * Low level interface for handling events and commands on an entity. + * + *

Generally, this should not be needed, instead, a class annotated with the {@link + * EventHandler}, {@link CommandHandler} and similar annotations should be used. + */ +public interface CrudEntityHandler { + + Optional handleCommand(Any command, CommandContext context); + + Optional handleCreateCommand(Any command, CommandContext context); + + Optional handleFetchCommand(Any command, CommandContext context); + + Optional handleUpdateCommand(Any command, CommandContext context); + + Optional handleDeleteCommand(Any command, CommandContext context); + + /** + * Handle the given state. + * + * @param state The state to handle. + * @param context The snapshot context. + */ + void handleState(Any state, SnapshotContext context); + + /** + * Snapshot the object. + * + * @return The current snapshot, if this object supports snapshoting, otherwise empty. + */ + Optional snapshot(SnapshotContext context); +} diff --git a/java-support/src/main/java/io/cloudstate/javasupport/crud/CrudEventContext.java b/java-support/src/main/java/io/cloudstate/javasupport/crud/CrudEventContext.java new file mode 100644 index 000000000..2958091b7 --- /dev/null +++ b/java-support/src/main/java/io/cloudstate/javasupport/crud/CrudEventContext.java @@ -0,0 +1,11 @@ +package io.cloudstate.javasupport.crud; + +/** Context for an event. */ +public interface CrudEventContext extends CrudContext { + /** + * The sequence number of the current event being processed. + * + * @return The sequence number. + */ + long sequenceNumber(); +} diff --git a/java-support/src/main/java/io/cloudstate/javasupport/crud/EventHandler.java b/java-support/src/main/java/io/cloudstate/javasupport/crud/EventHandler.java new file mode 100644 index 000000000..3fd1bb5c0 --- /dev/null +++ b/java-support/src/main/java/io/cloudstate/javasupport/crud/EventHandler.java @@ -0,0 +1,32 @@ +package io.cloudstate.javasupport.crud; + +import io.cloudstate.javasupport.eventsourced.EventContext; +import io.cloudstate.javasupport.impl.CloudStateAnnotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Marks a method as an event handler. + * + *

This method will be invoked whenever an event matching this event handlers event class is + * either replayed on entity recovery, by a command handler. + * + *

The method may take the event object as a parameter. + * + *

Methods annotated with this may take an {@link EventContext}. + */ +@CloudStateAnnotation +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface EventHandler { + /** + * The event class. Generally, this will be determined by looking at the parameter of the event + * handler method, however if the event doesn't need to be passed to the method (for example, + * perhaps it contains no data), then this can be used to indicate which event this handler + * handles. + */ + Class eventClass() default Object.class; +} diff --git a/java-support/src/main/java/io/cloudstate/javasupport/crud/Snapshot.java b/java-support/src/main/java/io/cloudstate/javasupport/crud/Snapshot.java new file mode 100644 index 000000000..acb44f1d1 --- /dev/null +++ b/java-support/src/main/java/io/cloudstate/javasupport/crud/Snapshot.java @@ -0,0 +1,24 @@ +package io.cloudstate.javasupport.crud; + +import io.cloudstate.javasupport.impl.CloudStateAnnotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Marks a method as a snapshot method. + * + *

An event sourced behavior may have at most one of these. When provided, it will be + * periodically (every n events emitted) be invoked to retrieve a snapshot of the current + * state, to be persisted, so that the event log can be loaded without replaying the entire history. + * + *

The method must return the current state of the entity. + * + *

The method may accept a {@link SnapshotContext} parameter. + */ +@CloudStateAnnotation +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface Snapshot {} diff --git a/java-support/src/main/java/io/cloudstate/javasupport/crud/SnapshotContext.java b/java-support/src/main/java/io/cloudstate/javasupport/crud/SnapshotContext.java new file mode 100644 index 000000000..d998369b7 --- /dev/null +++ b/java-support/src/main/java/io/cloudstate/javasupport/crud/SnapshotContext.java @@ -0,0 +1,11 @@ +package io.cloudstate.javasupport.crud; + +/** A snapshot context. */ +public interface SnapshotContext extends CrudContext { + /** + * The sequence number of the last event that this snapshot includes. + * + * @return The sequence number. + */ + long sequenceNumber(); +} diff --git a/java-support/src/main/java/io/cloudstate/javasupport/crud/SnapshotHandler.java b/java-support/src/main/java/io/cloudstate/javasupport/crud/SnapshotHandler.java new file mode 100644 index 000000000..e83e49e28 --- /dev/null +++ b/java-support/src/main/java/io/cloudstate/javasupport/crud/SnapshotHandler.java @@ -0,0 +1,26 @@ +package io.cloudstate.javasupport.crud; + +import io.cloudstate.javasupport.impl.CloudStateAnnotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Marks a method as a snapshot handler. + * + *

If, when recovering an entity, that entity has a snapshot, the snapshot will be passed to a + * corresponding snapshot handler method whose argument matches its type. The entity must set its + * current state to that snapshot. + * + *

An entity may declare more than one snapshot handler if it wants different handling for + * different types. + * + *

The snapshot handler method may additionally accept a {@link SnapshotContext} parameter, + * allowing it to access context for the snapshot, if required. + */ +@CloudStateAnnotation +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface SnapshotHandler {} diff --git a/java-support/src/main/java/io/cloudstate/javasupport/crud/package-info.java b/java-support/src/main/java/io/cloudstate/javasupport/crud/package-info.java new file mode 100644 index 000000000..df26b3631 --- /dev/null +++ b/java-support/src/main/java/io/cloudstate/javasupport/crud/package-info.java @@ -0,0 +1,14 @@ +/** + * CRUD support. + * + *

Event sourced entities can be annotated with the {@link + * io.cloudstate.javasupport.crud.CrudEntity @CrudEntity} annotation, and supply command handlers + * using the {@link io.cloudstate.javasupport.crud.CommandHandler @CommandHandler} annotation. + * + *

In addition, {@link io.cloudstate.javasupport.crud.EventHandler @EventHandler} annotated + * methods should be defined to handle events, and {@link + * io.cloudstate.javasupport.crud.Snapshot @Snapshot} and {@link + * io.cloudstate.javasupport.crud.SnapshotHandler @SnapshotHandler} annotated methods should be + * defined to produce and handle snapshots respectively. + */ +package io.cloudstate.javasupport.crud; diff --git a/java-support/src/main/java/io/cloudstate/javasupport/crudtwo/CommandContext.java b/java-support/src/main/java/io/cloudstate/javasupport/crudtwo/CommandContext.java new file mode 100644 index 000000000..502d12342 --- /dev/null +++ b/java-support/src/main/java/io/cloudstate/javasupport/crudtwo/CommandContext.java @@ -0,0 +1,41 @@ +package io.cloudstate.javasupport.crudtwo; + +import io.cloudstate.javasupport.ClientActionContext; +import io.cloudstate.javasupport.EffectContext; + +/** + * An event sourced command context. + * + *

Methods annotated with {@link CommandHandler} may take this is a parameter. It allows emitting + * new events in response to a command, along with forwarding the result to other entities, and + * performing side effects on other entities. + */ +public interface CommandContext extends CrudContext, ClientActionContext, EffectContext { + /** + * The current sequence number of events in this entity. + * + * @return The current sequence number. + */ + long sequenceNumber(); + + /** + * The name of the command being executed. + * + * @return The name of the command. + */ + String commandName(); + + /** + * The id of the command being executed. + * + * @return The id of the command. + */ + long commandId(); + + /** + * The state of the entity on which the command is being executed + * + * @return The state of the entity + */ + Object state(); +} diff --git a/java-support/src/main/java/io/cloudstate/javasupport/crudtwo/CommandHandler.java b/java-support/src/main/java/io/cloudstate/javasupport/crudtwo/CommandHandler.java new file mode 100644 index 000000000..fb0eec2e4 --- /dev/null +++ b/java-support/src/main/java/io/cloudstate/javasupport/crudtwo/CommandHandler.java @@ -0,0 +1,38 @@ +package io.cloudstate.javasupport.crudtwo; + +import io.cloudstate.javasupport.impl.CloudStateAnnotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Marks a method as a command handler. + * + *

This method will be invoked whenever the service call with name that matches this command + * handlers name is invoked. + * + *

The method may take the command object as a parameter, its type must match the gRPC service + * input type. + * + *

The return type of the method must match the gRPC services output type. + * + *

The method may also take a {@link CommandContext}, and/or a {@link + * io.cloudstate.javasupport.EntityId} annotated {@link String} parameter. + */ +@CloudStateAnnotation +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface CommandHandler { + + /** + * The name of the command to handle. + * + *

If not specified, the name of the method will be used as the command name, with the first + * letter capitalized to match the gRPC convention of capitalizing rpc method names. + * + * @return The command name. + */ + String name() default ""; +} diff --git a/java-support/src/main/java/io/cloudstate/javasupport/crudtwo/CrudContext.java b/java-support/src/main/java/io/cloudstate/javasupport/crudtwo/CrudContext.java new file mode 100644 index 000000000..f1836d0ab --- /dev/null +++ b/java-support/src/main/java/io/cloudstate/javasupport/crudtwo/CrudContext.java @@ -0,0 +1,6 @@ +package io.cloudstate.javasupport.crudtwo; + +import io.cloudstate.javasupport.EntityContext; + +/** Root context for all event sourcing contexts. */ +public interface CrudContext extends EntityContext {} diff --git a/java-support/src/main/java/io/cloudstate/javasupport/crudtwo/CrudEntity.java b/java-support/src/main/java/io/cloudstate/javasupport/crudtwo/CrudEntity.java new file mode 100644 index 000000000..4a148c87c --- /dev/null +++ b/java-support/src/main/java/io/cloudstate/javasupport/crudtwo/CrudEntity.java @@ -0,0 +1,29 @@ +package io.cloudstate.javasupport.crudtwo; + +import io.cloudstate.javasupport.impl.CloudStateAnnotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** An crud entity. */ +@CloudStateAnnotation +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface CrudEntity { + /** + * The name of the persistence id. + * + *

If not specified, defaults to the entities unqualified classname. It's strongly recommended + * that you specify it explicitly. + */ + String persistenceId() default ""; + + /** + * Specifies how snapshots of the entity state should be made: Zero means use default from + * configuration file. (Default) Any negative value means never snapshot. Any positive value means + * snapshot at-or-after that number of events. + */ + int snapshotEvery() default 0; +} diff --git a/java-support/src/main/java/io/cloudstate/javasupport/crudtwo/CrudEntityCreationContext.java b/java-support/src/main/java/io/cloudstate/javasupport/crudtwo/CrudEntityCreationContext.java new file mode 100644 index 000000000..97b838b5b --- /dev/null +++ b/java-support/src/main/java/io/cloudstate/javasupport/crudtwo/CrudEntityCreationContext.java @@ -0,0 +1,8 @@ +package io.cloudstate.javasupport.crudtwo; + +/** + * Creation context for {@link CrudEntity} annotated entities. + * + *

This may be accepted as an argument to the constructor of an event sourced entity. + */ +public interface CrudEntityCreationContext extends CrudContext {} diff --git a/java-support/src/main/java/io/cloudstate/javasupport/crudtwo/CrudEntityEventHandler.java b/java-support/src/main/java/io/cloudstate/javasupport/crudtwo/CrudEntityEventHandler.java new file mode 100644 index 000000000..41aa36251 --- /dev/null +++ b/java-support/src/main/java/io/cloudstate/javasupport/crudtwo/CrudEntityEventHandler.java @@ -0,0 +1,30 @@ +package io.cloudstate.javasupport.crudtwo; + +import com.google.protobuf.Any; +import io.cloudstate.javasupport.crud.SnapshotContext; + +import java.util.Optional; + +/** + * Low level interface for handling events and commands on an entity. + * + *

Generally, this should not be needed, instead, a class annotated with the {@link + * EventHandler}, {@link CommandHandler} and similar annotations should be used. + */ +public interface CrudEntityEventHandler { + + /** + * Handle the given snapshot. + * + * @param snapshot The snapshot to handle. + * @param context The snapshot context. + */ + void handleSnapshot(Any snapshot, SnapshotContext context); + + /** + * Snapshot the object. + * + * @return The current snapshot, if this object supports snapshoting, otherwise empty. + */ + Optional snapshot(SnapshotContext context); +} diff --git a/java-support/src/main/java/io/cloudstate/javasupport/crudtwo/CrudEntityFactory.java b/java-support/src/main/java/io/cloudstate/javasupport/crudtwo/CrudEntityFactory.java new file mode 100644 index 000000000..f6eef5143 --- /dev/null +++ b/java-support/src/main/java/io/cloudstate/javasupport/crudtwo/CrudEntityFactory.java @@ -0,0 +1,20 @@ +package io.cloudstate.javasupport.crudtwo; + +import io.cloudstate.javasupport.eventsourced.CommandHandler; +import io.cloudstate.javasupport.eventsourced.EventHandler; + +/** + * Low level interface for handling events and commands on an entity. + * + *

Generally, this should not be needed, instead, a class annotated with the {@link + * EventHandler}, {@link CommandHandler} and similar annotations should be used. + */ +public interface CrudEntityFactory { + /** + * Create an entity handler for the given context. + * + * @param context The context. + * @return The handler for the given context. + */ + CrudEntityHandler create(CrudContext context); +} diff --git a/java-support/src/main/java/io/cloudstate/javasupport/crudtwo/CrudEntityHandler.java b/java-support/src/main/java/io/cloudstate/javasupport/crudtwo/CrudEntityHandler.java new file mode 100644 index 000000000..c208475f6 --- /dev/null +++ b/java-support/src/main/java/io/cloudstate/javasupport/crudtwo/CrudEntityHandler.java @@ -0,0 +1,23 @@ +package io.cloudstate.javasupport.crudtwo; + +import com.google.protobuf.Any; +import java.util.Optional; + +/** + * Low level interface for handling events and commands on an entity. + * + *

Generally, this should not be needed, instead, a class annotated with the {@link + * EventHandler}, {@link CommandHandler} and similar annotations should be used. + */ +public interface CrudEntityHandler { + + Optional handleCommand(Any command, CommandContext context); + + /** + * Handle the given state. + * + * @param state The state to handle. + * @param context The snapshot context. + */ + void handleState(Any state, SnapshotContext context); +} diff --git a/java-support/src/main/java/io/cloudstate/javasupport/crudtwo/CrudEventContext.java b/java-support/src/main/java/io/cloudstate/javasupport/crudtwo/CrudEventContext.java new file mode 100644 index 000000000..01bdb9eba --- /dev/null +++ b/java-support/src/main/java/io/cloudstate/javasupport/crudtwo/CrudEventContext.java @@ -0,0 +1,13 @@ +package io.cloudstate.javasupport.crudtwo; + +import io.cloudstate.javasupport.crud.CrudContext; + +/** Context for an event. */ +public interface CrudEventContext extends CrudContext { + /** + * The sequence number of the current event being processed. + * + * @return The sequence number. + */ + long sequenceNumber(); +} diff --git a/java-support/src/main/java/io/cloudstate/javasupport/crudtwo/EventHandler.java b/java-support/src/main/java/io/cloudstate/javasupport/crudtwo/EventHandler.java new file mode 100644 index 000000000..98d85320d --- /dev/null +++ b/java-support/src/main/java/io/cloudstate/javasupport/crudtwo/EventHandler.java @@ -0,0 +1,32 @@ +package io.cloudstate.javasupport.crudtwo; + +import io.cloudstate.javasupport.eventsourced.EventContext; +import io.cloudstate.javasupport.impl.CloudStateAnnotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Marks a method as an event handler. + * + *

This method will be invoked whenever an event matching this event handlers event class is + * either replayed on entity recovery, by a command handler. + * + *

The method may take the event object as a parameter. + * + *

Methods annotated with this may take an {@link EventContext}. + */ +@CloudStateAnnotation +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface EventHandler { + /** + * The event class. Generally, this will be determined by looking at the parameter of the event + * handler method, however if the event doesn't need to be passed to the method (for example, + * perhaps it contains no data), then this can be used to indicate which event this handler + * handles. + */ + Class eventClass() default Object.class; +} diff --git a/java-support/src/main/java/io/cloudstate/javasupport/crudtwo/Snapshot.java b/java-support/src/main/java/io/cloudstate/javasupport/crudtwo/Snapshot.java new file mode 100644 index 000000000..fbfebd772 --- /dev/null +++ b/java-support/src/main/java/io/cloudstate/javasupport/crudtwo/Snapshot.java @@ -0,0 +1,25 @@ +package io.cloudstate.javasupport.crudtwo; + +import io.cloudstate.javasupport.crud.SnapshotContext; +import io.cloudstate.javasupport.impl.CloudStateAnnotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Marks a method as a snapshot method. + * + *

An event sourced behavior may have at most one of these. When provided, it will be + * periodically (every n events emitted) be invoked to retrieve a snapshot of the current + * state, to be persisted, so that the event log can be loaded without replaying the entire history. + * + *

The method must return the current state of the entity. + * + *

The method may accept a {@link SnapshotContext} parameter. + */ +@CloudStateAnnotation +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface Snapshot {} diff --git a/java-support/src/main/java/io/cloudstate/javasupport/crudtwo/SnapshotContext.java b/java-support/src/main/java/io/cloudstate/javasupport/crudtwo/SnapshotContext.java new file mode 100644 index 000000000..a456d334e --- /dev/null +++ b/java-support/src/main/java/io/cloudstate/javasupport/crudtwo/SnapshotContext.java @@ -0,0 +1,11 @@ +package io.cloudstate.javasupport.crudtwo; + +/** A snapshot context. */ +public interface SnapshotContext extends CrudContext { + /** + * The sequence number of the last event that this snapshot includes. + * + * @return The sequence number. + */ + long sequenceNumber(); +} diff --git a/java-support/src/main/java/io/cloudstate/javasupport/crudtwo/SnapshotHandler.java b/java-support/src/main/java/io/cloudstate/javasupport/crudtwo/SnapshotHandler.java new file mode 100644 index 000000000..402c7a8ed --- /dev/null +++ b/java-support/src/main/java/io/cloudstate/javasupport/crudtwo/SnapshotHandler.java @@ -0,0 +1,26 @@ +package io.cloudstate.javasupport.crudtwo; + +import io.cloudstate.javasupport.impl.CloudStateAnnotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Marks a method as a snapshot handler. + * + *

If, when recovering an entity, that entity has a snapshot, the snapshot will be passed to a + * corresponding snapshot handler method whose argument matches its type. The entity must set its + * current state to that snapshot. + * + *

An entity may declare more than one snapshot handler if it wants different handling for + * different types. + * + *

The snapshot handler method may additionally accept a {@link SnapshotContext} parameter, + * allowing it to access context for the snapshot, if required. + */ +@CloudStateAnnotation +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface SnapshotHandler {} diff --git a/java-support/src/main/java/io/cloudstate/javasupport/crudtwo/package-info.java b/java-support/src/main/java/io/cloudstate/javasupport/crudtwo/package-info.java new file mode 100644 index 000000000..7b934dcc4 --- /dev/null +++ b/java-support/src/main/java/io/cloudstate/javasupport/crudtwo/package-info.java @@ -0,0 +1,14 @@ +/** + * CRUD support. + * + *

Event sourced entities can be annotated with the {@link + * io.cloudstate.javasupport.crud.CrudEntity @CrudEntity} annotation, and supply command handlers + * using the {@link io.cloudstate.javasupport.crud.CommandHandler @CommandHandler} annotation. + * + *

In addition, {@link io.cloudstate.javasupport.crud.EventHandler @EventHandler} annotated + * methods should be defined to handle events, and {@link + * io.cloudstate.javasupport.crud.Snapshot @Snapshot} and {@link + * io.cloudstate.javasupport.crud.SnapshotHandler @SnapshotHandler} annotated methods should be + * defined to produce and handle snapshots respectively. + */ +package io.cloudstate.javasupport.crudtwo; diff --git a/java-support/src/main/scala/io/cloudstate/javasupport/impl/crudone/AnnotationBasedCrudSupport.scala b/java-support/src/main/scala/io/cloudstate/javasupport/impl/crudone/AnnotationBasedCrudSupport.scala new file mode 100644 index 000000000..7fe55898b --- /dev/null +++ b/java-support/src/main/scala/io/cloudstate/javasupport/impl/crudone/AnnotationBasedCrudSupport.scala @@ -0,0 +1,348 @@ +package io.cloudstate.javasupport.impl.crudone + +import java.lang.reflect.{Constructor, InvocationTargetException, Method} +import java.util.Optional + +import com.google.protobuf.{Descriptors, Any => JavaPbAny} +import io.cloudstate.javasupport.ServiceCallFactory +import io.cloudstate.javasupport.crud.{CrudContext, CrudEntityCreationContext, CrudEntityHandler, CrudEventContext} +import io.cloudstate.javasupport.crud._ +import io.cloudstate.javasupport.impl.ReflectionHelper.{InvocationContext, MainArgumentParameterHandler} +import io.cloudstate.javasupport.impl.{AnySupport, ReflectionHelper, ResolvedEntityFactory, ResolvedServiceMethod} + +import scala.collection.concurrent.TrieMap + +/** + * Annotation based implementation of the [[CrudEntityFactory]]. + */ +private[impl] class AnnotationBasedCrudSupport( + entityClass: Class[_], + anySupport: AnySupport, + override val resolvedMethods: Map[String, ResolvedServiceMethod[_, _]], + factory: Option[CrudEntityCreationContext => AnyRef] = None +) extends CrudEntityFactory + with ResolvedEntityFactory { + + def this(entityClass: Class[_], anySupport: AnySupport, serviceDescriptor: Descriptors.ServiceDescriptor) = + this(entityClass, anySupport, anySupport.resolveServiceDescriptor(serviceDescriptor)) + + private val behavior = EventBehaviorReflection(entityClass, resolvedMethods) + + override def create(context: CrudContext): CrudEntityHandler = + new EntityHandler(context) + + private val constructor: CrudEntityCreationContext => AnyRef = factory.getOrElse { + entityClass.getConstructors match { + case Array(single) => + new EntityConstructorInvoker(ReflectionHelper.ensureAccessible(single)) + case _ => + throw new RuntimeException(s"Only a single constructor is allowed on CRUD entities: $entityClass") + } + } + + private class EntityHandler(context: CrudContext) extends CrudEntityHandler { + private val entity = { + constructor(new DelegatingCrudContext(context) with CrudEntityCreationContext { + override def entityId(): String = context.entityId() + }) + } + + override def handleCommand(command: JavaPbAny, context: CommandContext): Optional[JavaPbAny] = unwrap { + behavior.commandHandlers.get(context.commandName()).map { handler => + handler.invoke(entity, command, context) + } getOrElse { + throw new RuntimeException( + s"No command handler found for command [${context.commandName()}] on $behaviorsString" + ) + } + } + + override def handleCreateCommand(command: JavaPbAny, context: CommandContext): Optional[JavaPbAny] = unwrap { + behavior.commandHandlers.get(context.commandName()).map { handler => + handler.invoke(entity, command, context) + } getOrElse { + throw new RuntimeException( + s"No command handler found for command [${context.commandName()}] on $behaviorsString" + ) + } + } + override def handleFetchCommand(command: JavaPbAny, context: CommandContext): Optional[JavaPbAny] = unwrap { + behavior.commandHandlers.get(context.commandName()).map { handler => + handler.invoke(entity, command, context) + } getOrElse { + throw new RuntimeException( + s"No command handler found for command [${context.commandName()}] on $behaviorsString" + ) + } + } + + override def handleUpdateCommand(command: JavaPbAny, context: CommandContext): Optional[JavaPbAny] = unwrap { + behavior.commandHandlers.get(context.commandName()).map { handler => + handler.invoke(entity, command, context) + } getOrElse { + throw new RuntimeException( + s"No command handler found for command [${context.commandName()}] on $behaviorsString" + ) + } + } + + override def handleDeleteCommand(command: JavaPbAny, context: CommandContext): Optional[JavaPbAny] = unwrap { + behavior.commandHandlers.get(context.commandName()).map { handler => + handler.invoke(entity, command, context) + } getOrElse { + throw new RuntimeException( + s"No command handler found for command [${context.commandName()}] on $behaviorsString" + ) + } + } + + override def handleState(anyState: JavaPbAny, context: SnapshotContext): Unit = unwrap { + val state = anySupport.decode(anyState).asInstanceOf[AnyRef] + + behavior.getCachedSnapshotHandlerForClass(state.getClass) match { + case Some(handler) => + val ctx = new DelegatingCrudContext(context) with SnapshotContext { + override def sequenceNumber(): Long = context.sequenceNumber() + } + handler.invoke(entity, state, ctx) + case None => + throw new RuntimeException( + s"No state handler found for state ${state.getClass} on $behaviorsString" + ) + } + } + + override def snapshot(context: SnapshotContext): Optional[JavaPbAny] = unwrap { + behavior.snapshotInvoker.map { invoker => + invoker.invoke(entity, context) + } match { + case Some(invoker) => Optional.ofNullable(anySupport.encodeJava(invoker)) + case None => Optional.empty() + } + } + + private def unwrap[T](block: => T): T = + try { + block + } catch { + case ite: InvocationTargetException if ite.getCause != null => + throw ite.getCause + } + + private def behaviorsString = entity.getClass.toString + } + + private abstract class DelegatingCrudContext(delegate: CrudContext) extends CrudContext { + override def entityId(): String = delegate.entityId() + override def serviceCallFactory(): ServiceCallFactory = delegate.serviceCallFactory() + } +} + +private class EventBehaviorReflection( + eventHandlers: Map[Class[_], EventHandlerInvoker], + val commandHandlers: Map[String, ReflectionHelper.CommandHandlerInvoker[CommandContext]], + snapshotHandlers: Map[Class[_], SnapshotHandlerInvoker], + val snapshotInvoker: Option[SnapshotInvoker] +) { + + /** + * We use a cache in addition to the info we've discovered by reflection so that an event handler can be declared + * for a superclass of an event. + */ + private val eventHandlerCache = TrieMap.empty[Class[_], Option[EventHandlerInvoker]] + private val snapshotHandlerCache = TrieMap.empty[Class[_], Option[SnapshotHandlerInvoker]] + + def getCachedEventHandlerForClass(clazz: Class[_]): Option[EventHandlerInvoker] = + eventHandlerCache.getOrElseUpdate(clazz, getHandlerForClass(eventHandlers)(clazz)) + + def getCachedSnapshotHandlerForClass(clazz: Class[_]): Option[SnapshotHandlerInvoker] = + snapshotHandlerCache.getOrElseUpdate(clazz, getHandlerForClass(snapshotHandlers)(clazz)) + + private def getHandlerForClass[T](handlers: Map[Class[_], T])(clazz: Class[_]): Option[T] = + handlers.get(clazz) match { + case some @ Some(_) => some + case None => + clazz.getInterfaces.collectFirst(Function.unlift(getHandlerForClass(handlers))) match { + case some @ Some(_) => some + case None if clazz.getSuperclass != null => getHandlerForClass(handlers)(clazz.getSuperclass) + case None => None + } + } + +} + +private object EventBehaviorReflection { + def apply(behaviorClass: Class[_], + serviceMethods: Map[String, ResolvedServiceMethod[_, _]]): EventBehaviorReflection = { + + val allMethods = ReflectionHelper.getAllDeclaredMethods(behaviorClass) + val eventHandlers = allMethods + .filter(_.getAnnotation(classOf[EventHandler]) != null) + .map { method => + new EventHandlerInvoker(ReflectionHelper.ensureAccessible(method)) + } + .groupBy(_.eventClass) + .map { + case (eventClass, Seq(invoker)) => (eventClass: Any) -> invoker + case (clazz, many) => + throw new RuntimeException( + s"Multiple methods found for handling event of type $clazz: ${many.map(_.method.getName)}" + ) + } + .asInstanceOf[Map[Class[_], EventHandlerInvoker]] + + val commandHandlers = allMethods + .filter(_.getAnnotation(classOf[CommandHandler]) != null) + .map { method => + val annotation = method.getAnnotation(classOf[CommandHandler]) + val name: String = if (annotation.name().isEmpty) { + ReflectionHelper.getCapitalizedName(method) + } else annotation.name() + + val serviceMethod = serviceMethods.getOrElse(name, { + throw new RuntimeException( + s"Command handler method ${method.getName} for command $name found, but the service has no command by that name." + ) + }) + + new ReflectionHelper.CommandHandlerInvoker[CommandContext](ReflectionHelper.ensureAccessible(method), + serviceMethod) + } + .groupBy(_.serviceMethod.name) + .map { + case (commandName, Seq(invoker)) => commandName -> invoker + case (commandName, many) => + throw new RuntimeException( + s"Multiple methods found for handling command of name $commandName: ${many.map(_.method.getName)}" + ) + } + + val snapshotHandlers = allMethods + .filter(_.getAnnotation(classOf[SnapshotHandler]) != null) + .map { method => + new SnapshotHandlerInvoker(ReflectionHelper.ensureAccessible(method)) + } + .groupBy(_.snapshotClass) + .map { + case (snapshotClass, Seq(invoker)) => (snapshotClass: Any) -> invoker + case (clazz, many) => + throw new RuntimeException( + s"Multiple methods found for handling snapshot of type $clazz: ${many.map(_.method.getName)}" + ) + } + .asInstanceOf[Map[Class[_], SnapshotHandlerInvoker]] + + val snapshotInvoker = allMethods + .filter(_.getAnnotation(classOf[Snapshot]) != null) + .map { method => + new SnapshotInvoker(ReflectionHelper.ensureAccessible(method)) + } match { + case Seq() => None + case Seq(single) => + Some(single) + case _ => + throw new RuntimeException(s"Multiple snapshoting methods found on behavior $behaviorClass") + } + + ReflectionHelper.validateNoBadMethods( + allMethods, + classOf[CrudEntity], + Set(classOf[EventHandler], classOf[CommandHandler], classOf[SnapshotHandler], classOf[Snapshot]) + ) + + new EventBehaviorReflection(eventHandlers, commandHandlers, snapshotHandlers, snapshotInvoker) + } +} + +private class EntityConstructorInvoker(constructor: Constructor[_]) extends (CrudEntityCreationContext => AnyRef) { + private val parameters = ReflectionHelper.getParameterHandlers[CrudEntityCreationContext](constructor)() + parameters.foreach { + case MainArgumentParameterHandler(clazz) => + throw new RuntimeException(s"Don't know how to handle argument of type $clazz in constructor") + case _ => + } + + def apply(context: CrudEntityCreationContext): AnyRef = { + val ctx = InvocationContext("", context) + constructor.newInstance(parameters.map(_.apply(ctx)): _*).asInstanceOf[AnyRef] + } +} + +private class EventHandlerInvoker(val method: Method) { + + private val annotation = method.getAnnotation(classOf[EventHandler]) + + private val parameters = ReflectionHelper.getParameterHandlers[CrudEventContext](method)() + + private def annotationEventClass = annotation.eventClass() match { + case obj if obj == classOf[Object] => None + case clazz => Some(clazz) + } + + // Verify that there is at most one event handler + val eventClass: Class[_] = parameters.collect { + case MainArgumentParameterHandler(clazz) => clazz + } match { + case Array() => annotationEventClass.getOrElse(classOf[Object]) + case Array(handlerClass) => + annotationEventClass match { + case None => handlerClass + case Some(annotated) if handlerClass.isAssignableFrom(annotated) || annotated.isInterface => + annotated + case Some(nonAssignable) => + throw new RuntimeException( + s"EventHandler method $method has defined an eventHandler class $nonAssignable that can never be assignable from it's parameter $handlerClass" + ) + } + case other => + throw new RuntimeException( + s"EventHandler method $method must defined at most one non context parameter to handle events, the parameters defined were: ${other + .mkString(",")}" + ) + } + + def invoke(obj: AnyRef, event: AnyRef, context: CrudEventContext): Unit = { + val ctx = InvocationContext(event, context) + method.invoke(obj, parameters.map(_.apply(ctx)): _*) + } +} + +private class SnapshotHandlerInvoker(val method: Method) { + private val parameters = ReflectionHelper.getParameterHandlers[SnapshotContext](method)() + + // Verify that there is at most one event handler + val snapshotClass: Class[_] = parameters.collect { + case MainArgumentParameterHandler(clazz) => clazz + } match { + case Array(handlerClass) => handlerClass + case other => + throw new RuntimeException( + s"SnapshotHandler method $method must defined at most one non context parameter to handle snapshots, the parameters defined were: ${other + .mkString(",")}" + ) + } + + def invoke(obj: AnyRef, snapshot: AnyRef, context: SnapshotContext): Unit = { + val ctx = InvocationContext(snapshot, context) + method.invoke(obj, parameters.map(_.apply(ctx)): _*) + } +} + +private class SnapshotInvoker(val method: Method) { + + private val parameters = ReflectionHelper.getParameterHandlers[SnapshotContext](method)() + + parameters.foreach { + case MainArgumentParameterHandler(clazz) => + throw new RuntimeException( + s"Don't know how to handle argument of type $clazz in snapshot method: " + method.getName + ) + case _ => + } + + def invoke(obj: AnyRef, context: SnapshotContext): AnyRef = { + val ctx = InvocationContext("", context) + method.invoke(obj, parameters.map(_.apply(ctx)): _*) + } + +} diff --git a/java-support/src/main/scala/io/cloudstate/javasupport/impl/crudone/CrudImpl.scala b/java-support/src/main/scala/io/cloudstate/javasupport/impl/crudone/CrudImpl.scala new file mode 100644 index 000000000..3d24fee74 --- /dev/null +++ b/java-support/src/main/scala/io/cloudstate/javasupport/impl/crudone/CrudImpl.scala @@ -0,0 +1,378 @@ +/* + * Copyright 2020 Lightbend Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.cloudstate.javasupport.impl.crudone + +import java.util.Optional + +import akka.NotUsed +import akka.actor.ActorSystem +import akka.stream.scaladsl.{Flow, Source} +import com.google.protobuf.any.{Any => ScalaPbAny} +import com.google.protobuf.{Descriptors, Any => JavaPbAny} +import io.cloudstate.javasupport.CloudStateRunner.Configuration +import io.cloudstate.javasupport.crud._ +import io.cloudstate.javasupport.impl._ +import io.cloudstate.javasupport.{Context, ServiceCallFactory, StatefulService} +import io.cloudstate.protocol.crud_one.CrudStreamIn.Message.{ + Create => InCreate, + Empty => InEmpty, + Event => InEvent, + Fetch => InFetch, + Init => InInit +} +import io.cloudstate.protocol.crud_one.{CrudInit, CrudReply, CrudStreamIn, CrudStreamOut} +import io.cloudstate.protocol.crud_one.CrudStreamOut.Message.{Reply => OutReply} +import io.cloudstate.protocol.crud_one.CrudOne + +final class CrudStatefulService(val factory: CrudEntityFactory, + override val descriptor: Descriptors.ServiceDescriptor, + val anySupport: AnySupport, + override val persistenceId: String, + val snapshotEvery: Int) + extends StatefulService { + + override def resolvedMethods: Option[Map[String, ResolvedServiceMethod[_, _]]] = + factory match { + case resolved: ResolvedEntityFactory => Some(resolved.resolvedMethods) + case _ => None + } + + override final val entityType = CrudOne.name + + final def withSnapshotEvery(snapshotEvery: Int): CrudStatefulService = + if (snapshotEvery != this.snapshotEvery) + new CrudStatefulService(this.factory, this.descriptor, this.anySupport, this.persistenceId, snapshotEvery) + else + this +} + +final class CrudImpl(_system: ActorSystem, + _services: Map[String, CrudStatefulService], + rootContext: Context, + configuration: Configuration) + extends CrudOne { + // how to push the snapshot state to the user function? handleState? + // should snapshot be exposed to the user function? + // How to do snapshot here? + // how to deal with snapshot and events by handleState. Some kind of mapping? + // how to deal with emitted events? handleState is called now, is that right? + + private final val system = _system + private final val services = _services.iterator + .map({ + case (name, crudss) => + // FIXME overlay configuration provided by _system + (name, if (crudss.snapshotEvery == 0) crudss.withSnapshotEvery(configuration.snapshotEvery) else crudss) + }) + .toMap + + override def handle(in: Source[CrudStreamIn, NotUsed]): Source[CrudStreamOut, NotUsed] = + in.prefixAndTail(1) + .flatMapConcat { + case (Seq(CrudStreamIn(InInit(init), _)), source) => + source.via(runEntityCreate(init)) + case _ => + // todo better error + throw new RuntimeException("Expected Init message") + } + .recover { + case e => + // FIXME translate to failure message + throw e + } + + private def runEntityCreate(init: CrudInit): Flow[CrudStreamIn, CrudStreamOut, NotUsed] = { + val service = + services.getOrElse(init.serviceName, throw new RuntimeException(s"Service not found: ${init.serviceName}")) + val handler: CrudEntityHandler = service.factory.create(new CrudContextImpl(init.entityId)) + val entityId = init.entityId + + val startingSequenceNumber = (for { + snapshot <- init.snapshot + any <- snapshot.snapshot + } yield { + val snapshotSequence = snapshot.snapshotSequence + val context = new CrudEventContextImpl(entityId, snapshotSequence) + handler.handleState(ScalaPbAny.toJavaProto(any), context) + snapshotSequence + }).getOrElse(0L) + + Flow[CrudStreamIn] + .map(_.message) + .scan[(Long, Option[CrudStreamOut.Message])]((startingSequenceNumber, None)) { + case (_, InEvent(event)) => + val context = new CrudEventContextImpl(entityId, event.sequence) + val ev = ScalaPbAny.toJavaProto(event.payload.get) // FIXME empty? + handler.handleState(ev, context) + (event.sequence, None) + + case ((sequence, _), InCreate(command)) => + if (entityId != command.entityId) + throw new IllegalStateException("Receiving CRUD entity is not the intended recipient of command") + val cmd = ScalaPbAny.toJavaProto(command.payload.get) + val context = new CommandContextImpl(entityId, + sequence, + command.name, + command.id, + service.anySupport, + handler, + service.snapshotEvery) + + val reply = try { + handler.handleCommand(cmd, context) // FIXME is this allowed to throw + } catch { + case FailInvoked => Optional.empty[JavaPbAny]() + // Ignore, error already captured + } finally { + context.deactivate() // Very important! + } + + val clientAction = context.createClientAction(reply, false) + if (!context.hasError) { + val endSequenceNumber = sequence + context.events.size + + val snapshot = + if (context.performSnapshot) { + val s = handler.snapshot(new SnapshotContext with AbstractContext { + override def entityId: String = entityId + override def sequenceNumber: Long = endSequenceNumber + }) + if (s.isPresent) Option(ScalaPbAny.fromJavaProto(s.get)) else None + } else None + + (endSequenceNumber, + Some( + OutReply( + CrudReply( + command.id, + clientAction, + context.sideEffects, + context.events, + snapshot + ) + ) + )) + } else { + (sequence, + Some( + OutReply( + CrudReply( + commandId = command.id, + clientAction = clientAction + ) + ) + )) + } + + case (_, InInit(i)) => + throw new IllegalStateException("Entity already inited") + + case (_, InEmpty) => + throw new IllegalStateException("Received empty/unknown message") + } + .collect { + case (_, Some(message)) => CrudStreamOut(message) + } + } + private def runEntityOld(init: CrudInit): Flow[CrudStreamIn, CrudStreamOut, NotUsed] = { + val service = + services.getOrElse(init.serviceName, throw new RuntimeException(s"Service not found: ${init.serviceName}")) + val handler: CrudEntityHandler = service.factory.create(new CrudContextImpl(init.entityId)) + val entityId = init.entityId + + val startingSequenceNumber = (for { + snapshot <- init.snapshot + any <- snapshot.snapshot + } yield { + val snapshotSequence = snapshot.snapshotSequence + val context = new CrudEventContextImpl(entityId, snapshotSequence) + handler.handleState(ScalaPbAny.toJavaProto(any), context) + snapshotSequence + }).getOrElse(0L) + + Flow[CrudStreamIn] + .map(_.message) + .scan[(Long, Option[CrudStreamOut.Message])]((startingSequenceNumber, None)) { + case (_, InEvent(event)) => + val context = new CrudEventContextImpl(entityId, event.sequence) + val ev = ScalaPbAny.toJavaProto(event.payload.get) // FIXME empty? + handler.handleState(ev, context) + (event.sequence, None) + + case ((sequence, _), InCreate(command)) => + if (entityId != command.entityId) + throw new IllegalStateException("Receiving CRUD entity is not the intended recipient of command") + val cmd = ScalaPbAny.toJavaProto(command.payload.get) + val context = new CommandContextImpl(entityId, + sequence, + command.name, + command.id, + service.anySupport, + handler, + service.snapshotEvery) + + val reply = try { + handler.handleCommand(cmd, context) // FIXME is this allowed to throw + } catch { + case FailInvoked => Optional.empty[JavaPbAny]() + // Ignore, error already captured + } finally { + context.deactivate() // Very important! + } + + val clientAction = context.createClientAction(reply, false) + if (!context.hasError) { + val endSequenceNumber = sequence + context.events.size + + val snapshot = + if (context.performSnapshot) { + val s = handler.snapshot(new SnapshotContext with AbstractContext { + override def entityId: String = entityId + override def sequenceNumber: Long = endSequenceNumber + }) + if (s.isPresent) Option(ScalaPbAny.fromJavaProto(s.get)) else None + } else None + + (endSequenceNumber, + Some( + OutReply( + CrudReply( + command.id, + clientAction, + context.sideEffects, + context.events, + snapshot + ) + ) + )) + } else { + (sequence, + Some( + OutReply( + CrudReply( + commandId = command.id, + clientAction = clientAction + ) + ) + )) + } + + case ((sequence, _), InFetch(command)) => + if (entityId != command.entityId) + throw new IllegalStateException("Receiving CRUD entity is not the intended recipient of command") + val cmd = ScalaPbAny.toJavaProto(command.payload.get) + val context = new CommandContextImpl(entityId, + sequence, + command.name, + command.id, + service.anySupport, + handler, + service.snapshotEvery) + + val reply = try { + handler.handleCommand(cmd, context) // FIXME is this allowed to throw + } catch { + case FailInvoked => Optional.empty[JavaPbAny]() + // Ignore, error already captured + } finally { + context.deactivate() // Very important! + } + + val clientAction = context.createClientAction(reply, false) + if (!context.hasError) { + val endSequenceNumber = sequence + context.events.size + + val snapshot = + if (context.performSnapshot) { + val s = handler.snapshot(new SnapshotContext with AbstractContext { + override def entityId: String = entityId + override def sequenceNumber: Long = endSequenceNumber + }) + if (s.isPresent) Option(ScalaPbAny.fromJavaProto(s.get)) else None + } else None + + (endSequenceNumber, + Some( + OutReply( + CrudReply( + command.id, + clientAction, + context.sideEffects, + context.events, + snapshot + ) + ) + )) + } else { + (sequence, + Some( + OutReply( + CrudReply( + commandId = command.id, + clientAction = clientAction + ) + ) + )) + } + case (_, InInit(i)) => + throw new IllegalStateException("Entity already inited") + case (_, InEmpty) => + throw new IllegalStateException("Received empty/unknown message") + + //case ((sequence, _), aOrB @ (InCreate(_) | InFetch(_))) => ??? + } + .collect { + case (_, Some(message)) => CrudStreamOut(message) + } + } + + trait AbstractContext extends CrudContext { + override def serviceCallFactory(): ServiceCallFactory = rootContext.serviceCallFactory() + } + + class CommandContextImpl(override val entityId: String, + override val sequenceNumber: Long, + override val commandName: String, + override val commandId: Long, + val anySupport: AnySupport, + val handler: CrudEntityHandler, + val snapshotEvery: Int) + extends CommandContext + with AbstractContext + with AbstractClientActionContext + with AbstractEffectContext + with ActivatableContext { + + final var events: Vector[ScalaPbAny] = Vector.empty + final var performSnapshot: Boolean = false + + override def emit(event: AnyRef): Unit = { + checkActive() + val encoded = anySupport.encodeScala(event) + val nextSequenceNumber = sequenceNumber + events.size + 1 + handler.handleState(ScalaPbAny.toJavaProto(encoded), new CrudEventContextImpl(entityId, nextSequenceNumber)) + events :+= encoded + performSnapshot = (snapshotEvery > 0) && (performSnapshot || (nextSequenceNumber % snapshotEvery == 0)) + } + } + + // FIXME add final val subEntityId: String + class CrudContextImpl(override final val entityId: String) extends CrudContext with AbstractContext + class CrudEventContextImpl(entityId: String, override final val sequenceNumber: Long) + extends CrudContextImpl(entityId) + with CrudEventContext + with SnapshotContext +} diff --git a/java-support/src/main/scala/io/cloudstate/javasupport/impl/crudtwo/AnnotationBasedCrudSupport.scala b/java-support/src/main/scala/io/cloudstate/javasupport/impl/crudtwo/AnnotationBasedCrudSupport.scala new file mode 100644 index 000000000..c6967949d --- /dev/null +++ b/java-support/src/main/scala/io/cloudstate/javasupport/impl/crudtwo/AnnotationBasedCrudSupport.scala @@ -0,0 +1,312 @@ +package io.cloudstate.javasupport.impl.crudtwo + +import java.lang.reflect.{Constructor, InvocationTargetException, Method} +import java.util.Optional + +import com.google.protobuf.{Descriptors, Any => JavaPbAny} +import io.cloudstate.javasupport.ServiceCallFactory +import io.cloudstate.javasupport.crudtwo.{ + CommandContext, + CommandHandler, + CrudContext, + CrudEntity, + CrudEntityCreationContext, + CrudEntityFactory, + CrudEntityHandler, + CrudEventContext, + EventHandler, + Snapshot, + SnapshotContext, + SnapshotHandler +} +import io.cloudstate.javasupport.impl.ReflectionHelper.{InvocationContext, MainArgumentParameterHandler} +import io.cloudstate.javasupport.impl.{AnySupport, ReflectionHelper, ResolvedEntityFactory, ResolvedServiceMethod} + +import scala.collection.concurrent.TrieMap + +/** + * Annotation based implementation of the [[CrudEntityFactory]]. + */ +private[impl] class AnnotationBasedCrudSupport( + entityClass: Class[_], + anySupport: AnySupport, + override val resolvedMethods: Map[String, ResolvedServiceMethod[_, _]], + factory: Option[CrudEntityCreationContext => AnyRef] = None +) extends CrudEntityFactory + with ResolvedEntityFactory { + + def this(entityClass: Class[_], anySupport: AnySupport, serviceDescriptor: Descriptors.ServiceDescriptor) = + this(entityClass, anySupport, anySupport.resolveServiceDescriptor(serviceDescriptor)) + + private val behavior = EventBehaviorReflection(entityClass, resolvedMethods) + + override def create(context: CrudContext): CrudEntityHandler = + new EntityHandler(context) + + private val constructor: CrudEntityCreationContext => AnyRef = factory.getOrElse { + entityClass.getConstructors match { + case Array(single) => + new EntityConstructorInvoker(ReflectionHelper.ensureAccessible(single)) + case _ => + throw new RuntimeException(s"Only a single constructor is allowed on CRUD entities: $entityClass") + } + } + + private class EntityHandler(context: CrudContext) extends CrudEntityHandler { + private val entity = { + constructor(new DelegatingCrudContext(context) with CrudEntityCreationContext { + override def entityId(): String = context.entityId() + }) + } + + override def handleCommand(command: JavaPbAny, context: CommandContext): Optional[JavaPbAny] = unwrap { + behavior.commandHandlers.get(context.commandName()).map { handler => + handler.invoke(entity, command, context) + } getOrElse { + throw new RuntimeException( + s"No command handler found for command [${context.commandName()}] on $behaviorsString" + ) + } + } + + override def handleState(anyState: JavaPbAny, context: SnapshotContext): Unit = unwrap { + val state = anySupport.decode(anyState).asInstanceOf[AnyRef] + + behavior.getCachedSnapshotHandlerForClass(state.getClass) match { + case Some(handler) => + val ctx = new DelegatingCrudContext(context) with SnapshotContext { + override def sequenceNumber(): Long = context.sequenceNumber() + } + handler.invoke(entity, state, ctx) + case None => + throw new RuntimeException( + s"No state handler found for state ${state.getClass} on $behaviorsString" + ) + } + } + + private def unwrap[T](block: => T): T = + try { + block + } catch { + case ite: InvocationTargetException if ite.getCause != null => + throw ite.getCause + } + + private def behaviorsString = entity.getClass.toString + } + + private abstract class DelegatingCrudContext(delegate: CrudContext) extends CrudContext { + override def entityId(): String = delegate.entityId() + override def serviceCallFactory(): ServiceCallFactory = delegate.serviceCallFactory() + } +} + +private class EventBehaviorReflection( + eventHandlers: Map[Class[_], EventHandlerInvoker], + val commandHandlers: Map[String, ReflectionHelper.CommandHandlerInvoker[CommandContext]], + snapshotHandlers: Map[Class[_], SnapshotHandlerInvoker], + val snapshotInvoker: Option[SnapshotInvoker] +) { + + /** + * We use a cache in addition to the info we've discovered by reflection so that an event handler can be declared + * for a superclass of an event. + */ + private val eventHandlerCache = TrieMap.empty[Class[_], Option[EventHandlerInvoker]] + private val snapshotHandlerCache = TrieMap.empty[Class[_], Option[SnapshotHandlerInvoker]] + + def getCachedEventHandlerForClass(clazz: Class[_]): Option[EventHandlerInvoker] = + eventHandlerCache.getOrElseUpdate(clazz, getHandlerForClass(eventHandlers)(clazz)) + + def getCachedSnapshotHandlerForClass(clazz: Class[_]): Option[SnapshotHandlerInvoker] = + snapshotHandlerCache.getOrElseUpdate(clazz, getHandlerForClass(snapshotHandlers)(clazz)) + + private def getHandlerForClass[T](handlers: Map[Class[_], T])(clazz: Class[_]): Option[T] = + handlers.get(clazz) match { + case some @ Some(_) => some + case None => + clazz.getInterfaces.collectFirst(Function.unlift(getHandlerForClass(handlers))) match { + case some @ Some(_) => some + case None if clazz.getSuperclass != null => getHandlerForClass(handlers)(clazz.getSuperclass) + case None => None + } + } + +} + +private object EventBehaviorReflection { + def apply(behaviorClass: Class[_], + serviceMethods: Map[String, ResolvedServiceMethod[_, _]]): EventBehaviorReflection = { + + val allMethods = ReflectionHelper.getAllDeclaredMethods(behaviorClass) + val eventHandlers = allMethods + .filter(_.getAnnotation(classOf[EventHandler]) != null) + .map { method => + new EventHandlerInvoker(ReflectionHelper.ensureAccessible(method)) + } + .groupBy(_.eventClass) + .map { + case (eventClass, Seq(invoker)) => (eventClass: Any) -> invoker + case (clazz, many) => + throw new RuntimeException( + s"Multiple methods found for handling event of type $clazz: ${many.map(_.method.getName)}" + ) + } + .asInstanceOf[Map[Class[_], EventHandlerInvoker]] + + val commandHandlers = allMethods + .filter(_.getAnnotation(classOf[CommandHandler]) != null) + .map { method => + val annotation = method.getAnnotation(classOf[CommandHandler]) + val name: String = if (annotation.name().isEmpty) { + ReflectionHelper.getCapitalizedName(method) + } else annotation.name() + + val serviceMethod = serviceMethods.getOrElse(name, { + throw new RuntimeException( + s"Command handler method ${method.getName} for command $name found, but the service has no command by that name." + ) + }) + + new ReflectionHelper.CommandHandlerInvoker[CommandContext](ReflectionHelper.ensureAccessible(method), + serviceMethod) + } + .groupBy(_.serviceMethod.name) + .map { + case (commandName, Seq(invoker)) => commandName -> invoker + case (commandName, many) => + throw new RuntimeException( + s"Multiple methods found for handling command of name $commandName: ${many.map(_.method.getName)}" + ) + } + + val snapshotHandlers = allMethods + .filter(_.getAnnotation(classOf[SnapshotHandler]) != null) + .map { method => + new SnapshotHandlerInvoker(ReflectionHelper.ensureAccessible(method)) + } + .groupBy(_.snapshotClass) + .map { + case (snapshotClass, Seq(invoker)) => (snapshotClass: Any) -> invoker + case (clazz, many) => + throw new RuntimeException( + s"Multiple methods found for handling snapshot of type $clazz: ${many.map(_.method.getName)}" + ) + } + .asInstanceOf[Map[Class[_], SnapshotHandlerInvoker]] + + val snapshotInvoker = allMethods + .filter(_.getAnnotation(classOf[Snapshot]) != null) + .map { method => + new SnapshotInvoker(ReflectionHelper.ensureAccessible(method)) + } match { + case Seq() => None + case Seq(single) => + Some(single) + case _ => + throw new RuntimeException(s"Multiple snapshoting methods found on behavior $behaviorClass") + } + + ReflectionHelper.validateNoBadMethods( + allMethods, + classOf[CrudEntity], + Set(classOf[EventHandler], classOf[CommandHandler], classOf[SnapshotHandler], classOf[Snapshot]) + ) + + new EventBehaviorReflection(eventHandlers, commandHandlers, snapshotHandlers, snapshotInvoker) + } +} + +private class EntityConstructorInvoker(constructor: Constructor[_]) extends (CrudEntityCreationContext => AnyRef) { + private val parameters = ReflectionHelper.getParameterHandlers[CrudEntityCreationContext](constructor)() + parameters.foreach { + case MainArgumentParameterHandler(clazz) => + throw new RuntimeException(s"Don't know how to handle argument of type $clazz in constructor") + case _ => + } + + def apply(context: CrudEntityCreationContext): AnyRef = { + val ctx = InvocationContext("", context) + constructor.newInstance(parameters.map(_.apply(ctx)): _*).asInstanceOf[AnyRef] + } +} + +private class EventHandlerInvoker(val method: Method) { + + private val annotation = method.getAnnotation(classOf[EventHandler]) + + private val parameters = ReflectionHelper.getParameterHandlers[CrudEventContext](method)() + + private def annotationEventClass = annotation.eventClass() match { + case obj if obj == classOf[Object] => None + case clazz => Some(clazz) + } + + // Verify that there is at most one event handler + val eventClass: Class[_] = parameters.collect { + case MainArgumentParameterHandler(clazz) => clazz + } match { + case Array() => annotationEventClass.getOrElse(classOf[Object]) + case Array(handlerClass) => + annotationEventClass match { + case None => handlerClass + case Some(annotated) if handlerClass.isAssignableFrom(annotated) || annotated.isInterface => + annotated + case Some(nonAssignable) => + throw new RuntimeException( + s"EventHandler method $method has defined an eventHandler class $nonAssignable that can never be assignable from it's parameter $handlerClass" + ) + } + case other => + throw new RuntimeException( + s"EventHandler method $method must defined at most one non context parameter to handle events, the parameters defined were: ${other + .mkString(",")}" + ) + } + + def invoke(obj: AnyRef, event: AnyRef, context: CrudEventContext): Unit = { + val ctx = InvocationContext(event, context) + method.invoke(obj, parameters.map(_.apply(ctx)): _*) + } +} + +private class SnapshotHandlerInvoker(val method: Method) { + private val parameters = ReflectionHelper.getParameterHandlers[SnapshotContext](method)() + + // Verify that there is at most one event handler + val snapshotClass: Class[_] = parameters.collect { + case MainArgumentParameterHandler(clazz) => clazz + } match { + case Array(handlerClass) => handlerClass + case other => + throw new RuntimeException( + s"SnapshotHandler method $method must defined at most one non context parameter to handle snapshots, the parameters defined were: ${other + .mkString(",")}" + ) + } + + def invoke(obj: AnyRef, snapshot: AnyRef, context: SnapshotContext): Unit = { + val ctx = InvocationContext(snapshot, context) + method.invoke(obj, parameters.map(_.apply(ctx)): _*) + } +} + +private class SnapshotInvoker(val method: Method) { + + private val parameters = ReflectionHelper.getParameterHandlers[SnapshotContext](method)() + + parameters.foreach { + case MainArgumentParameterHandler(clazz) => + throw new RuntimeException( + s"Don't know how to handle argument of type $clazz in snapshot method: " + method.getName + ) + case _ => + } + + def invoke(obj: AnyRef, context: SnapshotContext): AnyRef = { + val ctx = InvocationContext("", context) + method.invoke(obj, parameters.map(_.apply(ctx)): _*) + } + +} diff --git a/java-support/src/main/scala/io/cloudstate/javasupport/impl/crudtwo/CrudImpl.scala b/java-support/src/main/scala/io/cloudstate/javasupport/impl/crudtwo/CrudImpl.scala new file mode 100644 index 000000000..fda15c481 --- /dev/null +++ b/java-support/src/main/scala/io/cloudstate/javasupport/impl/crudtwo/CrudImpl.scala @@ -0,0 +1,139 @@ +/* + * Copyright 2020 Lightbend Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.cloudstate.javasupport.impl.crudtwo + +import akka.actor.ActorSystem +import com.google.protobuf.Descriptors +import com.google.protobuf.any.{Any => ScalaPbAny} +import io.cloudstate.javasupport.CloudStateRunner.Configuration +import io.cloudstate.javasupport.crudtwo._ +import io.cloudstate.javasupport.impl._ +import io.cloudstate.javasupport.{Context, ServiceCallFactory, StatefulService} +import io.cloudstate.protocol.crud_two._ + +import scala.concurrent.Future + +final class CrudStatefulService(val factory: CrudEntityFactory, + override val descriptor: Descriptors.ServiceDescriptor, + val anySupport: AnySupport, + override val persistenceId: String, + val snapshotEvery: Int) + extends StatefulService { + + override def resolvedMethods: Option[Map[String, ResolvedServiceMethod[_, _]]] = + factory match { + case resolved: ResolvedEntityFactory => Some(resolved.resolvedMethods) + case _ => None + } + + override final val entityType = CrudTwo.name + + final def withSnapshotEvery(snapshotEvery: Int): CrudStatefulService = + if (snapshotEvery != this.snapshotEvery) + new CrudStatefulService(this.factory, this.descriptor, this.anySupport, this.persistenceId, snapshotEvery) + else + this +} + +final class CrudImpl(_system: ActorSystem, + _services: Map[String, CrudStatefulService], + rootContext: Context, + configuration: Configuration) + extends CrudTwo { + // how to push the snapshot state to the user function? handleState? + // should snapshot be exposed to the user function? + // How to do snapshot here? + // how to deal with snapshot and events by handleState. Some kind of mapping? + // how to deal with emitted events? handleState is called now, is that right? + + private final val system = _system + private final implicit val ec = system.dispatcher + private final val services = _services.iterator.toMap + + private val serviceName = "serviceName" // FIXME where to get the service name from? + private val entityId = "entityId" // FIXME entityId can be extract from command, where to get entityId from when creating the CrudImpl? + private final val service = + services.getOrElse(serviceName, throw new RuntimeException(s"Service not found: $serviceName")) + private var handler: CrudEntityHandler = service.factory.create(new CrudContextImpl(entityId)) // FIXME how to create it? + + override def create(command: CrudCommand): Future[CrudReplies] = { + Future.unit + .map { _ => + val cmd = ScalaPbAny.toJavaProto(command.payload.get) //FIXME payload empty? + val state = ScalaPbAny.toJavaProto(command.state.get.payload.get) // FIXME state empty? FIXME payload empty? + val context = new CommandContextImpl(command.entityId, 0, command.name, command.id, state) + val reply = handler.handleCommand(cmd, context) + val clientAction = context.createClientAction(reply, false) + CrudReplies( + CrudReplies.Message.Reply( + CrudReply( + command.id, + clientAction, + context.sideEffects, + Some(ScalaPbAny.fromJavaProto(reply.get())) // FIXME reply empty? + ) + ) + ) + } + } + + override def fetch(command: CrudCommand): Future[CrudFetchReplies] = { + Future.unit + .map { _ => + val cmd = ScalaPbAny.toJavaProto(command.payload.get) //FIXME payload empty? + val state = ScalaPbAny.toJavaProto(command.state.get.payload.get) // FIXME state empty? FIXME payload empty? + val context = new CommandContextImpl(command.entityId, 0, command.name, command.id, state) + val reply = handler.handleCommand(cmd, context) + val clientAction = context.createClientAction(reply, false) + CrudFetchReplies( + CrudFetchReplies.Message.Reply( + CrudFetchReply( + command.id, + clientAction, + context.sideEffects, + Some(ScalaPbAny.fromJavaProto(reply.get())) // FIXME reply empty? + ) + ) + ) + } + } + + override def update(command: CrudCommand): Future[CrudReplies] = ??? // same as create + + override def delete(command: CrudCommand): Future[CrudReplies] = ??? // same as create + + trait AbstractContext extends CrudContext { + override def serviceCallFactory(): ServiceCallFactory = rootContext.serviceCallFactory() + } + + class CommandContextImpl(override val entityId: String, + override val sequenceNumber: Long, + override val commandName: String, + override val commandId: Long, + override val state: AnyRef) + extends CommandContext + with AbstractContext + with AbstractClientActionContext + with AbstractEffectContext + with ActivatableContext { + + //val encoded = anySupport.encodeScala(event) + // handler.handleState(ScalaPbAny.toJavaProto(encoded), new CrudEventContextImpl(entityId, nextSequenceNumber)) + } + + // FIXME add final val subEntityId: String + class CrudContextImpl(override final val entityId: String) extends CrudContext with AbstractContext +} diff --git a/java-support/src/test/scala/io/cloudstate/javasupport/impl/crudone/AnnotationBasedCrudSupportSpec.scala b/java-support/src/test/scala/io/cloudstate/javasupport/impl/crudone/AnnotationBasedCrudSupportSpec.scala new file mode 100644 index 000000000..d7dbb1b44 --- /dev/null +++ b/java-support/src/test/scala/io/cloudstate/javasupport/impl/crudone/AnnotationBasedCrudSupportSpec.scala @@ -0,0 +1,84 @@ +package io.cloudstate.javasupport.impl.crudone + +import com.example.shoppingcartcrud.Shoppingcart +import com.google.protobuf.{ByteString, Descriptors, Any => JavaPbAny} +import io.cloudstate.javasupport.{Context, ServiceCall, ServiceCallFactory, ServiceCallRef} +import io.cloudstate.javasupport.crud.{CommandContext, CrudContext, CrudEventContext} +import io.cloudstate.javasupport.impl.eventsourced.AnnotationBasedEventSourcedSupport +import io.cloudstate.javasupport.impl.{AnySupport, ResolvedServiceMethod, ResolvedType} +import org.scalatest.{Matchers, WordSpec} +import com.google.protobuf.any.{Any => ScalaPbAny} + +class AnnotationBasedCrudSupportSpec extends WordSpec with Matchers { + trait BaseContext extends Context { + override def serviceCallFactory(): ServiceCallFactory = new ServiceCallFactory { + override def lookup[T](serviceName: String, methodName: String, messageType: Class[T]): ServiceCallRef[T] = + throw new NoSuchElementException + } + } + + object MockContext extends CrudContext with BaseContext { + override def entityId(): String = "foo" + } + + class MockCommandContext extends CommandContext with BaseContext { + var emited = Seq.empty[AnyRef] + override def sequenceNumber(): Long = 10 + override def commandName(): String = "CreateItem" + override def commandId(): Long = 20 + override def emit(event: AnyRef): Unit = emited :+= event + override def entityId(): String = "foo" + override def fail(errorMessage: String): RuntimeException = ??? + override def forward(to: ServiceCall): Unit = ??? + override def effect(effect: ServiceCall, synchronous: Boolean): Unit = ??? + } + + val eventCtx = new CrudEventContext with BaseContext { + override def sequenceNumber(): Long = 10 + override def entityId(): String = "foo" + } + + object WrappedResolvedType extends ResolvedType[Wrapped] { + override def typeClass: Class[Wrapped] = classOf[Wrapped] + override def typeUrl: String = AnySupport.DefaultTypeUrlPrefix + "/wrapped" + override def parseFrom(bytes: ByteString): Wrapped = Wrapped(bytes.toStringUtf8) + override def toByteString(value: Wrapped): ByteString = ByteString.copyFromUtf8(value.value) + } + + object StringResolvedType extends ResolvedType[String] { + override def typeClass: Class[String] = classOf[String] + override def typeUrl: String = AnySupport.DefaultTypeUrlPrefix + "/string" + override def parseFrom(bytes: ByteString): String = bytes.toStringUtf8 + override def toByteString(value: String): ByteString = ByteString.copyFromUtf8(value) + } + + case class Wrapped(value: String) + val anySupport = new AnySupport(Array(Shoppingcart.getDescriptor), this.getClass.getClassLoader) + val descriptor = Shoppingcart.getDescriptor + .findServiceByName("ShoppingCart") + .findMethodByName("AddItem") + val method = ResolvedServiceMethod(descriptor, StringResolvedType, WrappedResolvedType) + + def create(behavior: AnyRef, methods: ResolvedServiceMethod[_, _]*) = + new AnnotationBasedCrudSupport(behavior.getClass, + anySupport, + methods.map(m => m.descriptor.getName -> m).toMap, + Some(_ => behavior)).create(MockContext) + + def create(clazz: Class[_]) = + new AnnotationBasedCrudSupport(clazz, anySupport, Map.empty, None).create(MockContext) + + def command(str: String) = + ScalaPbAny.toJavaProto(ScalaPbAny(StringResolvedType.typeUrl, StringResolvedType.toByteString(str))) + + def decodeWrapped(any: JavaPbAny): Wrapped = { + any.getTypeUrl should ===(WrappedResolvedType.typeUrl) + WrappedResolvedType.parseFrom(any.getValue) + } + + def event(any: Any): JavaPbAny = anySupport.encodeJava(any) + + "support command handlers" when { + + } +} diff --git a/protocols/frontend/cloudstate/sub_entity_key.proto b/protocols/frontend/cloudstate/sub_entity_key.proto new file mode 100644 index 000000000..7a0130764 --- /dev/null +++ b/protocols/frontend/cloudstate/sub_entity_key.proto @@ -0,0 +1,30 @@ +// Copyright 2019 Lightbend Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Extension for specifying which field in a message is to be considered an +// entity key, for the purposes associating gRPC calls with entities and +// sharding. + +syntax = "proto3"; + +import "google/protobuf/descriptor.proto"; + +package cloudstate; + +option java_package = "io.cloudstate"; +option go_package = "github.com/cloudstateio/go-support/cloudstate/;cloudstate"; + +extend google.protobuf.FieldOptions { + bool sub_entity_key = 50004; +} diff --git a/protocols/protocol/cloudstate/crud_one.proto b/protocols/protocol/cloudstate/crud_one.proto new file mode 100644 index 000000000..0598c10fa --- /dev/null +++ b/protocols/protocol/cloudstate/crud_one.proto @@ -0,0 +1,159 @@ +// Copyright 2019 Lightbend Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// gRPC interface for CRUD Entity user functions. + +syntax = "proto3"; + +package cloudstate.crudone; + +// Any is used so that domain events defined according to the functions business domain can be embedded inside +// the protocol. +import "google/protobuf/any.proto"; +import "google/protobuf/empty.proto"; +import "cloudstate/entity.proto"; +//import "cloudstate/sub_entity.proto"; + +option java_package = "io.cloudstate.protocol"; +option go_package = "cloudstate/protocol"; + +// The init message. This will always be the first message sent to the entity when +// it is loaded. +message CrudInit { + + string service_name = 1; + + // The ID of the entity. + string entity_id = 2; + + // If present the entity should initialise its state using this snapshot. + CrudSnapshot snapshot = 3; +} + +// A snapshot +message CrudSnapshot { + + // The sequence number when the snapshot was taken. + int64 snapshot_sequence = 1; + + // The snapshot. + google.protobuf.Any snapshot = 2; +} + +// An event. These will be sent to the entity when the entity starts up. +message CrudEvent { + + // The sequence number of the event. + int64 sequence = 1; + + // The event payload. + google.protobuf.Any payload = 2; +} + +// A reply to a command. +message CrudReply { + + // The id of the command being replied to. Must match the input command. + int64 command_id = 1; + + // The action to take + ClientAction client_action = 2; + + // Any side effects to perform + repeated SideEffect side_effects = 3; + + // A list of events to persist - these will be persisted before the reply + // is sent. + repeated google.protobuf.Any events = 4; + + // An optional snapshot to persist. It is assumed that this snapshot will have + // the state of any events in the events field applied to it. It is illegal to + // send a snapshot without sending any events. + google.protobuf.Any snapshot = 5; +} + +// A CRUD command. For each CRUD command received, a reply must be sent with a matching command id. +message CrudCreateCommand { + + // The ID of the entity. + string entity_id = 1; + + // The ID of a sub entity. + string sub_entity_id = 2; + + // A command id. + int64 id = 3; + + // Command name + string name = 4; + + // The command payload. + google.protobuf.Any payload = 5; + + // Whether the command is streamed or not + bool streamed = 6; +} + +// A CRUD command. For each CRUD command received, a reply must be sent with a matching command id. +message CrudFetchCommand { + + // The ID of the entity. + string entity_id = 1; + + // The ID of a sub entity. + string sub_entity_id = 2; + + // A command id. + int64 id = 3; + + // Command name + string name = 4; + + // The command payload. + google.protobuf.Any payload = 5; + + // Whether the command is streamed or not + bool streamed = 6; +} + +// Input message type for the gRPC stream in. +message CrudStreamIn { + oneof message { + CrudInit init = 1; + CrudEvent event = 2; + CrudCreateCommand create = 3; + CrudFetchCommand fetch = 4; + } +} + +// Output message type for the gRPC stream out. +message CrudStreamOut { + oneof message { + CrudReply reply = 1; + Failure failure = 2; + } +} + +message CrudCommandResponse { + oneof response { + CrudReply reply = 1; + Failure failure = 2; + } +} + +// The CRUD Entity service +service CrudOne { + + rpc handle(stream CrudStreamIn) returns (stream CrudStreamOut) {} +} diff --git a/protocols/protocol/cloudstate/crud_two.proto b/protocols/protocol/cloudstate/crud_two.proto new file mode 100644 index 000000000..db100bb91 --- /dev/null +++ b/protocols/protocol/cloudstate/crud_two.proto @@ -0,0 +1,224 @@ +// Copyright 2019 Lightbend Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// gRPC interface for CRUD Entity user functions. + +syntax = "proto3"; + +package cloudstate.crudtwo; + +// Any is used so that domain events defined according to the functions business domain can be embedded inside +// the protocol. +import "google/protobuf/any.proto"; +import "google/protobuf/empty.proto"; +import "cloudstate/entity.proto"; +//import "cloudstate/sub_entity.proto"; + +option java_package = "io.cloudstate.protocol"; +option go_package = "cloudstate/protocol"; + +enum CrudCommandType { + UNKNOWN = 0; + CREATE = 1; + FETCH = 2; + UPDATE = 3; + DELETE = 4; +} + +message CrudState { + // The state payload + google.protobuf.Any payload = 2; +} + +message CrudInitCommand { + + // The ID of the entity. + string entity_id = 1; + + // The ID of a sub entity. + string sub_entity_id = 2; + + // Command name + string name = 3; + + // The command payload. + google.protobuf.Any payload = 4; + + CrudCommandType type = 5; +} + +message CrudCommand { + + // The ID of the entity. + string entity_id = 1; + + // The ID of a sub entity. + string sub_entity_id = 2; + + // A command id. + int64 id = 3; + + // Command name + string name = 4; + + // The command payload. + google.protobuf.Any payload = 5; + + //CrudCommandType type = 6; + + // persistent state to be conveyed between persistent entity and the user function + CrudState state = 6; +} + +message CreateCommand { + + // The ID of the entity. + string entity_id = 1; + + // The ID of a sub entity. + string sub_entity_id = 2; + + // A command id. + int64 id = 3; + + // Command name + string name = 4; + + // The command payload. + google.protobuf.Any payload = 5; + + CrudCommandType type = 6; + + // persistent state to be conveyed between persistent entity and the user function + CrudState state = 7; +} + +message FetchCommand { + + // The ID of the entity. + string entity_id = 1; + + // The ID of a sub entity. + string sub_entity_id = 2; + + // A command id. + int64 id = 3; + + // Command name + string name = 4; + + // The command payload. + google.protobuf.Any payload = 5; + + CrudCommandType type = 6; + + CrudState state = 7; +} + +message UpdateCommand { + + // The ID of the entity. + string entity_id = 1; + + // The ID of a sub entity. + string sub_entity_id = 2; + + // A command id. + int64 id = 3; + + // Command name + string name = 4; + + // The command payload. + google.protobuf.Any payload = 5; + + CrudCommandType type = 6; + + CrudState state = 7; +} + +message DeleteCommand { + + // The ID of the entity. + string entity_id = 1; + + // The ID of a sub entity. + string sub_entity_id = 2; + + // A command id. + int64 id = 3; + + // Command name + string name = 4; + + CrudCommandType type = 5; + + CrudState state = 6; +} + +// A reply to a command. +message CrudReply { + + // The id of the command being replied to. Must match the input command. + int64 command_id = 1; + + // The action to take + ClientAction client_action = 2; + + // Any side effects to perform + repeated SideEffect side_effects = 3; + + // An optional state to persist. + google.protobuf.Any state = 4; +} + +message CrudReplies { + oneof message { + CrudReply reply = 1; + Failure failure = 2; + } +} + +message CrudFetchReply { + // The id of the command being replied to. Must match the input command. + int64 command_id = 1; + + // The action to take + ClientAction client_action = 2; + + // Any side effects to perform + repeated SideEffect side_effects = 3; + + // An optional state to be return to the caller. + google.protobuf.Any fetch_state = 4; +} + +message CrudFetchReplies { + oneof message { + CrudFetchReply reply = 1; + Failure failure = 2; + } +} + +// The CRUD Entity service +service CrudTwo { + + rpc create(CrudCommand) returns (CrudReplies) {} + + rpc fetch(CrudCommand) returns (CrudFetchReplies) {} + + rpc update(CrudCommand) returns (CrudReplies) {} + + rpc delete(CrudCommand) returns (CrudReplies) {} +} diff --git a/proxy/core/src/main/scala/io/cloudstate/proxy/UserFunctionTypeSupport.scala b/proxy/core/src/main/scala/io/cloudstate/proxy/UserFunctionTypeSupport.scala index a75fccc92..71bbd05a3 100644 --- a/proxy/core/src/main/scala/io/cloudstate/proxy/UserFunctionTypeSupport.scala +++ b/proxy/core/src/main/scala/io/cloudstate/proxy/UserFunctionTypeSupport.scala @@ -2,12 +2,13 @@ package io.cloudstate.proxy import akka.NotUsed import akka.stream.scaladsl.Flow -import com.google.protobuf.Descriptors.{FieldDescriptor, MethodDescriptor, ServiceDescriptor} +import com.google.protobuf.Descriptors.{MethodDescriptor, ServiceDescriptor} import com.google.protobuf.{ByteString, DynamicMessage} -import io.cloudstate.protocol.entity.Entity import io.cloudstate.entity_key.EntityKeyProto +import io.cloudstate.protocol.entity.Entity import io.cloudstate.proxy.entity.{EntityCommand, UserFunctionCommand, UserFunctionReply} import io.cloudstate.proxy.protobuf.Options +import io.cloudstate.sub_entity_key.SubEntityKeyProto import scala.collection.JavaConverters._ import scala.concurrent.Future @@ -57,6 +58,11 @@ final class EntityMethodDescriptor(val method: MethodDescriptor) { .toArray .sortBy(_.getIndex) + private[this] val subEntityKeyFields = method.getInputType.getFields.iterator.asScala + .filter(field => SubEntityKeyProto.subEntityKey.get(Options.convertFieldOptions(field))) + .toArray + .sortBy(_.getIndex) + def keyFieldsCount: Int = keyFields.length def extractId(bytes: ByteString): String = @@ -71,6 +77,17 @@ final class EntityMethodDescriptor(val method: MethodDescriptor) { keyFields.iterator.map(dm.getField).mkString(EntityMethodDescriptor.Separator) } + def extractSubEntityId(bytes: ByteString): String = + subEntityKeyFields.length match { + case 0 => + "" + case 1 => + val dm = DynamicMessage.parseFrom(method.getInputType, bytes) + dm.getField(subEntityKeyFields.head).toString + case _ => + val dm = DynamicMessage.parseFrom(method.getInputType, bytes) + subEntityKeyFields.iterator.map(dm.getField).mkString(EntityMethodDescriptor.Separator) + } } private final class EntityUserFunctionTypeSupport(serviceDescriptor: ServiceDescriptor, diff --git a/proxy/core/src/main/scala/io/cloudstate/proxy/crudone/CrudEntity.scala b/proxy/core/src/main/scala/io/cloudstate/proxy/crudone/CrudEntity.scala new file mode 100644 index 000000000..3c0c86965 --- /dev/null +++ b/proxy/core/src/main/scala/io/cloudstate/proxy/crudone/CrudEntity.scala @@ -0,0 +1,368 @@ +/* + * Copyright 2019 Lightbend Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.cloudstate.proxy.crudone + +import java.net.URLDecoder +import java.util.concurrent.atomic.AtomicLong + +import akka.NotUsed +import akka.actor._ +import akka.cluster.sharding.ShardRegion +import akka.persistence._ +import akka.stream.scaladsl._ +import akka.stream.{Materializer, OverflowStrategy} +import akka.util.Timeout +import com.google.protobuf.any.{Any => pbAny} +import io.cloudstate.protocol.entity._ +import io.cloudstate.protocol.crud_one._ +import io.cloudstate.proxy.ConcurrencyEnforcer.{Action, ActionCompleted} +import io.cloudstate.proxy.StatsCollector +import io.cloudstate.proxy.entity.{EntityCommand, UserFunctionReply} + +import scala.collection.immutable.Queue + +object CrudEntitySupervisor { + + private final case class Relay(actorRef: ActorRef) + + def props(client: CrudOneClient, + configuration: CrudEntity.Configuration, + concurrencyEnforcer: ActorRef, + statsCollector: ActorRef)(implicit mat: Materializer): Props = + Props(new CrudEntitySupervisor(client, configuration, concurrencyEnforcer, statsCollector)) +} + +/** + * This serves two purposes. + * + * Firstly, when the StateManager crashes, we don't want it restarted. Cluster sharding restarts, and there's no way + * to customise that. + * + * Secondly, we need to ensure that we have an Akka Streams actorRef source to publish messages two before Akka + * persistence starts feeding us events. There's a race condition if we do this in the same persistent actor. This + * establishes that connection first. + */ +final class CrudEntitySupervisor(client: CrudOneClient, + configuration: CrudEntity.Configuration, + concurrencyEnforcer: ActorRef, + statsCollector: ActorRef)(implicit mat: Materializer) + extends Actor + with Stash { + + import CrudEntitySupervisor._ + + override final def receive: Receive = PartialFunction.empty + + override final def preStart(): Unit = + client + .handle( + Source + .actorRef[CrudStreamIn](configuration.sendQueueSize, OverflowStrategy.fail) + .mapMaterializedValue { ref => + self ! Relay(ref) + NotUsed + } + ) + .runWith(Sink.actorRef(self, CrudEntity.StreamClosed)) + context.become(waitingForRelay) + + private[this] final def waitingForRelay: Receive = { + case Relay(relayRef) => + // Cluster sharding URL encodes entity ids, so to extract it we need to decode. + val entityId = URLDecoder.decode(self.path.name, "utf-8") + val manager = context.watch( + context + .actorOf(CrudEntity.props(configuration, entityId, relayRef, concurrencyEnforcer, statsCollector), "entity") + ) + context.become(forwarding(manager)) + unstashAll() + case _ => stash() + } + + private[this] final def forwarding(manager: ActorRef): Receive = { + case Terminated(`manager`) => + context.stop(self) + case toParent if sender() == manager => + context.parent ! toParent + case msg => + manager forward msg + } + + override def supervisorStrategy: SupervisorStrategy = SupervisorStrategy.stoppingStrategy +} + +object CrudEntity { + + final case object Stop + + final case object StreamClosed extends DeadLetterSuppression + + final case class Configuration( + serviceName: String, + userFunctionName: String, + passivationTimeout: Timeout, + sendQueueSize: Int + ) + + private final case class OutstandingCommand( + commandId: Long, + actionId: String, + replyTo: ActorRef + ) + + final def props(configuration: Configuration, + entityId: String, + relay: ActorRef, + concurrencyEnforcer: ActorRef, + statsCollector: ActorRef): Props = + Props(new CrudEntity(configuration, entityId, relay, concurrencyEnforcer, statsCollector)) + + /** + * Used to ensure the action ids sent to the concurrency enforcer are indeed unique. + */ + private val actorCounter = new AtomicLong(0) +} + +final class CrudEntity(configuration: CrudEntity.Configuration, + entityId: String, + relay: ActorRef, + concurrencyEnforcer: ActorRef, + statsCollector: ActorRef) + extends PersistentActor + with ActorLogging { + override final def persistenceId: String = configuration.userFunctionName + entityId + + private val actorId = CrudEntity.actorCounter.incrementAndGet() + + private[this] final var stashedCommands = Queue.empty[(EntityCommand, ActorRef)] // PERFORMANCE: look at options for data structures + private[this] final var currentCommand: CrudEntity.OutstandingCommand = null + private[this] final var stopped = false + private[this] final var idCounter = 0L + private[this] final var inited = false + private[this] final var reportedDatabaseOperationStarted = false + private[this] final var databaseOperationStartTime = 0L + private[this] final var commandStartTime = 0L + + // Set up passivation timer + context.setReceiveTimeout(configuration.passivationTimeout.duration) + + // First thing actor will do is access database + reportDatabaseOperationStarted() + + override final def postStop(): Unit = { + if (currentCommand != null) { + log.warning("Stopped but we have a current action id {}", currentCommand.actionId) + reportActionComplete() + } + if (reportedDatabaseOperationStarted) { + reportDatabaseOperationFinished() + } + // This will shutdown the stream (if not already shut down) + relay ! Status.Success(()) + } + + private[this] final def commandHandled(): Unit = { + currentCommand = null + if (stashedCommands.nonEmpty) { + val ((request, sender), newStashedCommands) = stashedCommands.dequeue + stashedCommands = newStashedCommands + handleCommand(request, sender) + } else if (stopped) { + context.stop(self) + } + } + + private[this] final def notifyOutstandingRequests(msg: String): Unit = { + currentCommand match { + case null => + case req => req.replyTo ! createFailure(msg) + } + val errorNotification = createFailure("Entity terminated") + stashedCommands.foreach { + case (_, replyTo) => replyTo ! errorNotification + } + } + + private[this] final def crash(msg: String): Unit = { + notifyOutstandingRequests(msg) + throw new Exception(msg) + } + + private[this] final def reportActionComplete() = + concurrencyEnforcer ! ActionCompleted(currentCommand.actionId, System.nanoTime() - commandStartTime) + + private[this] final def handleCommand(entityCommand: EntityCommand, sender: ActorRef): Unit = { + idCounter += 1 + val command = Command( + entityId = entityId, + id = idCounter, + name = entityCommand.name, + payload = entityCommand.payload + ) + currentCommand = CrudEntity.OutstandingCommand(idCounter, actorId + ":" + entityId + ":" + idCounter, sender) + commandStartTime = System.nanoTime() + concurrencyEnforcer ! Action(currentCommand.actionId, () => { + //relay ! CrudStreamIn(CrudStreamIn.Message.Command(command)) + }) + } + + private final def esReplyToUfReply(reply: CrudReply) = + UserFunctionReply( + clientAction = reply.clientAction, + sideEffects = reply.sideEffects + ) + + private final def createFailure(message: String) = + UserFunctionReply( + clientAction = Some(ClientAction(ClientAction.Action.Failure(Failure(description = message)))) + ) + + override final def receiveCommand: PartialFunction[Any, Unit] = { + + case command: EntityCommand if currentCommand != null => + stashedCommands = stashedCommands.enqueue((command, sender())) + + case command: EntityCommand => + handleCommand(command, sender()) + + case CrudStreamOut(m, _) => + import CrudStreamOut.{Message => ESOMsg} + m match { + + case ESOMsg.Reply(r) if currentCommand == null => + crash(s"Unexpected reply, had no current command: $r") + + case ESOMsg.Reply(r) if currentCommand.commandId != r.commandId => + crash(s"Incorrect command id in reply, expecting ${currentCommand.commandId} but got ${r.commandId}") + + case ESOMsg.Reply(r) => + reportActionComplete() + val commandId = currentCommand.commandId + val events = r.events.toVector + if (events.isEmpty) { + currentCommand.replyTo ! esReplyToUfReply(r) + commandHandled() + } else { + reportDatabaseOperationStarted() + var eventsLeft = events.size + persistAll(events) { _ => + eventsLeft -= 1 + if (eventsLeft <= 0) { // Remove this hack when switching to Akka Persistence Typed + reportDatabaseOperationFinished() + r.snapshot.foreach(saveSnapshot) + // Make sure that the current request is still ours + if (currentCommand == null || currentCommand.commandId != commandId) { + crash("Internal error - currentRequest changed before all events were persisted") + } + currentCommand.replyTo ! esReplyToUfReply(r) + commandHandled() + } + } + } + + case ESOMsg.Failure(f) if f.commandId == 0 => + crash(s"Non command specific error from entity: ${f.description}") + + case ESOMsg.Failure(f) if currentCommand == null => + crash(s"Unexpected failure, had no current command: $f") + + case ESOMsg.Failure(f) if currentCommand.commandId != f.commandId => + crash(s"Incorrect command id in failure, expecting ${currentCommand.commandId} but got ${f.commandId}") + + case ESOMsg.Failure(f) => + reportActionComplete() + currentCommand.replyTo ! createFailure(f.description) + commandHandled() + + case ESOMsg.Empty => + // Either the reply/failure wasn't set, or its set to something unknown. + // todo see if scalapb can give us unknown fields so we can possibly log more intelligently + crash("Empty or unknown message from entity output stream") + } + + case CrudEntity.StreamClosed => + notifyOutstandingRequests("Unexpected entity termination") + context.stop(self) + + case Status.Failure(error) => + notifyOutstandingRequests("Unexpected entity termination") + throw error + + case SaveSnapshotSuccess(metadata) => + // Nothing to do + + case SaveSnapshotFailure(metadata, cause) => + log.error("Error saving snapshot", cause) + + case ReceiveTimeout => + context.parent ! ShardRegion.Passivate(stopMessage = CrudEntity.Stop) + + case CrudEntity.Stop => + stopped = true + if (currentCommand == null) { + context.stop(self) + } + } + + private[this] final def maybeInit(snapshot: Option[SnapshotOffer]): Unit = + if (!inited) { + relay ! CrudStreamIn( + CrudStreamIn.Message.Init( + CrudInit( + serviceName = configuration.serviceName, + entityId = entityId, + snapshot = snapshot.map { + case SnapshotOffer(metadata, offeredSnapshot: pbAny) => + CrudSnapshot(metadata.sequenceNr, Some(offeredSnapshot)) + case other => throw new IllegalStateException(s"Unexpected snapshot type received: ${other.getClass}") + } + ) + ) + ) + inited = true + } + + override final def receiveRecover: PartialFunction[Any, Unit] = { + case offer: SnapshotOffer => + maybeInit(Some(offer)) + + case RecoveryCompleted => + reportDatabaseOperationFinished() + maybeInit(None) + + case event: pbAny => + maybeInit(None) + relay ! CrudStreamIn(CrudStreamIn.Message.Event(CrudEvent(lastSequenceNr, Some(event)))) + } + + private def reportDatabaseOperationStarted(): Unit = + if (reportedDatabaseOperationStarted) { + log.warning("Already reported database operation started") + } else { + databaseOperationStartTime = System.nanoTime() + reportedDatabaseOperationStarted = true + statsCollector ! StatsCollector.DatabaseOperationStarted + } + + private def reportDatabaseOperationFinished(): Unit = + if (!reportedDatabaseOperationStarted) { + log.warning("Hadn't reported database operation started") + } else { + reportedDatabaseOperationStarted = false + statsCollector ! StatsCollector.DatabaseOperationFinished(System.nanoTime() - databaseOperationStartTime) + } +} diff --git a/proxy/core/src/main/scala/io/cloudstate/proxy/crudone/CrudSupportFactory.scala b/proxy/core/src/main/scala/io/cloudstate/proxy/crudone/CrudSupportFactory.scala new file mode 100644 index 000000000..d9930642f --- /dev/null +++ b/proxy/core/src/main/scala/io/cloudstate/proxy/crudone/CrudSupportFactory.scala @@ -0,0 +1,95 @@ +package io.cloudstate.proxy.crudone + +import akka.NotUsed +import akka.actor.{ActorRef, ActorSystem} +import akka.cluster.sharding.ShardRegion.HashCodeMessageExtractor +import akka.cluster.sharding.{ClusterSharding, ClusterShardingSettings} +import akka.event.Logging +import akka.grpc.GrpcClientSettings +import akka.stream.Materializer +import akka.stream.scaladsl.Flow +import akka.util.Timeout +import com.google.protobuf.Descriptors.ServiceDescriptor +import io.cloudstate.protocol.crud_one.CrudOneClient +import io.cloudstate.protocol.entity.Entity +import io.cloudstate.proxy._ +import io.cloudstate.proxy.entity.{EntityCommand, UserFunctionReply} +import io.cloudstate.proxy.eventsourced.DynamicLeastShardAllocationStrategy + +import scala.concurrent.{ExecutionContext, Future} + +class CrudSupportFactory(system: ActorSystem, + config: EntityDiscoveryManager.Configuration, + grpcClientSettings: GrpcClientSettings, + concurrencyEnforcer: ActorRef, + statsCollector: ActorRef)(implicit ec: ExecutionContext, mat: Materializer) + extends EntityTypeSupportFactory { + + private final val log = Logging.getLogger(system, this.getClass) + + private val crudClient = CrudOneClient(grpcClientSettings) + + override def buildEntityTypeSupport(entity: Entity, + serviceDescriptor: ServiceDescriptor, + methodDescriptors: Map[String, EntityMethodDescriptor]): EntityTypeSupport = { + validate(serviceDescriptor, methodDescriptors) + + val stateManagerConfig = CrudEntity.Configuration(entity.serviceName, + entity.persistenceId, + config.passivationTimeout, + config.relayOutputBufferSize) + + log.debug("Starting EventSourcedEntity for {}", entity.persistenceId) + val clusterSharding = ClusterSharding(system) + val clusterShardingSettings = ClusterShardingSettings(system) + val eventSourcedEntity = clusterSharding.start( + typeName = entity.persistenceId, + entityProps = CrudEntitySupervisor.props(crudClient, stateManagerConfig, concurrencyEnforcer, statsCollector), + settings = clusterShardingSettings, + messageExtractor = new EntityIdExtractor(config.numberOfShards), + allocationStrategy = new DynamicLeastShardAllocationStrategy(1, 10, 2, 0.0), + handOffStopMessage = CrudEntity.Stop + ) + + new EventSourcedSupport(eventSourcedEntity, config.proxyParallelism, config.relayTimeout) + } + + private def validate(serviceDescriptor: ServiceDescriptor, + methodDescriptors: Map[String, EntityMethodDescriptor]): Unit = { + val streamedMethods = + methodDescriptors.values.filter(m => m.method.toProto.getClientStreaming || m.method.toProto.getServerStreaming) + if (streamedMethods.nonEmpty) { + val offendingMethods = streamedMethods.map(_.method.getName).mkString(",") + throw EntityDiscoveryException( + s"Event sourced entities do not support streamed methods, but ${serviceDescriptor.getFullName} has the following streamed methods: ${offendingMethods}" + ) + } + val methodsWithoutKeys = methodDescriptors.values.filter(_.keyFieldsCount < 1) + if (methodsWithoutKeys.nonEmpty) { + val offendingMethods = methodsWithoutKeys.map(_.method.getName).mkString(",") + throw new EntityDiscoveryException( + s"Event sourced entities do not support methods whose parameters do not have at least one field marked as entity_key, " + + "but ${serviceDescriptor.getFullName} has the following methods without keys: ${offendingMethods}" + ) + } + } +} + +private class EventSourcedSupport(eventSourcedEntity: ActorRef, + parallelism: Int, + private implicit val relayTimeout: Timeout) + extends EntityTypeSupport { + import akka.pattern.ask + + override def handler(method: EntityMethodDescriptor): Flow[EntityCommand, UserFunctionReply, NotUsed] = + Flow[EntityCommand].mapAsync(parallelism)(command => (eventSourcedEntity ? command).mapTo[UserFunctionReply]) + + override def handleUnary(command: EntityCommand): Future[UserFunctionReply] = + (eventSourcedEntity ? command).mapTo[UserFunctionReply] +} + +private final class EntityIdExtractor(shards: Int) extends HashCodeMessageExtractor(shards) { + override final def entityId(message: Any): String = message match { + case command: EntityCommand => command.entityId + } +} diff --git a/proxy/core/src/main/scala/io/cloudstate/proxy/crudtwo/CrudEntity.scala b/proxy/core/src/main/scala/io/cloudstate/proxy/crudtwo/CrudEntity.scala new file mode 100644 index 000000000..6ccb40960 --- /dev/null +++ b/proxy/core/src/main/scala/io/cloudstate/proxy/crudtwo/CrudEntity.scala @@ -0,0 +1,358 @@ +/* + * Copyright 2019 Lightbend Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.cloudstate.proxy.crudtwo + +import java.net.URLDecoder +import java.util.concurrent.atomic.AtomicLong + +import akka.actor._ +import akka.cluster.sharding.ShardRegion +import akka.persistence._ +import akka.stream.Materializer +import akka.util.Timeout +import com.google.protobuf.any.{Any => pbAny} +import io.cloudstate.protocol.crud_two.{CreateCommand, CrudCommand, CrudCommandType, CrudFetchReplies, CrudFetchReply, CrudInitCommand, CrudReplies, CrudReply, CrudState, CrudTwoClient, DeleteCommand, FetchCommand, UpdateCommand} +import io.cloudstate.protocol.entity._ +import io.cloudstate.proxy.ConcurrencyEnforcer.{Action, ActionCompleted} +import io.cloudstate.proxy.StatsCollector +import io.cloudstate.proxy.entity.{EntityCommand, UserFunctionReply} + +import scala.collection.immutable.Queue + +object CrudEntitySupervisor { + + private final case class Relay(actorRef: ActorRef) + private final case object Start + + def props(client: CrudTwoClient, + configuration: CrudEntity.Configuration, + concurrencyEnforcer: ActorRef, + statsCollector: ActorRef)(implicit mat: Materializer): Props = + Props(new CrudEntitySupervisor(client, configuration, concurrencyEnforcer, statsCollector)) +} + +/** + * This serves two purposes. + * + * Firstly, when the StateManager crashes, we don't want it restarted. Cluster sharding restarts, and there's no way + * to customise that. + * + * Secondly, we need to ensure that we have an Akka Streams actorRef source to publish messages two before Akka + * persistence starts feeding us events. There's a race condition if we do this in the same persistent actor. This + * establishes that connection first. + */ +final class CrudEntitySupervisor(client: CrudTwoClient, + configuration: CrudEntity.Configuration, + concurrencyEnforcer: ActorRef, + statsCollector: ActorRef)(implicit mat: Materializer) + extends Actor + with Stash { + + import CrudEntitySupervisor._ + + override final def receive: Receive = PartialFunction.empty + + override final def preStart(): Unit = { + self ! Start + context.become(waitingForRelay) + } + + private[this] final def waitingForRelay: Receive = { + case Start => + // Cluster sharding URL encodes entity ids, so to extract it we need to decode. + val entityId = URLDecoder.decode(self.path.name, "utf-8") + val manager = context.watch( + context + .actorOf(CrudEntity.props(configuration, entityId, client, concurrencyEnforcer, statsCollector), "entity") + ) + context.become(forwarding(manager)) + unstashAll() + case _ => stash() + } + + private[this] final def forwarding(manager: ActorRef): Receive = { + case Terminated(`manager`) => + context.stop(self) + case toParent if sender() == manager => + context.parent ! toParent + case msg => + manager forward msg + } + + override def supervisorStrategy: SupervisorStrategy = SupervisorStrategy.stoppingStrategy +} + +object CrudEntity { + + final case object Stop + + final case class Configuration( + serviceName: String, + userFunctionName: String, + passivationTimeout: Timeout, + sendQueueSize: Int + ) + + private final case class OutstandingCommand( + commandId: Long, + actionId: String, + replyTo: ActorRef + ) + + final def props(configuration: Configuration, + entityId: String, + client: CrudTwoClient, + concurrencyEnforcer: ActorRef, + statsCollector: ActorRef): Props = + Props(new CrudEntity(configuration, entityId, client, concurrencyEnforcer, statsCollector)) + + /** + * Used to ensure the action ids sent to the concurrency enforcer are indeed unique. + */ + private val actorCounter = new AtomicLong(0) +} + +final class CrudEntity(configuration: CrudEntity.Configuration, + entityId: String, + client: CrudTwoClient, + concurrencyEnforcer: ActorRef, + statsCollector: ActorRef) + extends PersistentActor + with ActorLogging { + + import context.dispatcher + import akka.pattern.pipe + + override final def persistenceId: String = configuration.userFunctionName + entityId + + private val actorId = CrudEntity.actorCounter.incrementAndGet() + + private[this] final var state = CrudState(None) + + private[this] final var stashedCommands = Queue.empty[(CrudInitCommand, ActorRef)] // PERFORMANCE: look at options for data structures + private[this] final var currentCommand: CrudEntity.OutstandingCommand = null + private[this] final var stopped = false + private[this] final var idCounter = 0L + private[this] final var reportedDatabaseOperationStarted = false + private[this] final var databaseOperationStartTime = 0L + private[this] final var commandStartTime = 0L + + // Set up passivation timer + context.setReceiveTimeout(configuration.passivationTimeout.duration) + + // First thing actor will do is access database + reportDatabaseOperationStarted() + + override final def postStop(): Unit = { + if (currentCommand != null) { + log.warning("Stopped but we have a current action id {}", currentCommand.actionId) + reportActionComplete() + } + if (reportedDatabaseOperationStarted) { + reportDatabaseOperationFinished() + } + } + + private[this] final def commandHandled(): Unit = { + currentCommand = null + if (stashedCommands.nonEmpty) { + val ((request, sender), newStashedCommands) = stashedCommands.dequeue + stashedCommands = newStashedCommands + handleCommand(request, sender) + } else if (stopped) { + context.stop(self) + } + } + + private[this] final def notifyOutstandingRequests(msg: String): Unit = { + currentCommand match { + case null => + case req => req.replyTo ! createFailure(msg) + } + val errorNotification = createFailure("Entity terminated") + stashedCommands.foreach { + case (_, replyTo) => replyTo ! errorNotification + } + } + + private[this] final def crash(msg: String): Unit = { + notifyOutstandingRequests(msg) + throw new Exception(msg) + } + + private[this] final def reportActionComplete() = + concurrencyEnforcer ! ActionCompleted(currentCommand.actionId, System.nanoTime() - commandStartTime) + + private[this] final def handleCommand(entityCommand: EntityCommand, sender: ActorRef): Unit = { + idCounter += 1 + val command = CreateCommand( + entityId = entityId, + subEntityId = entityCommand.entityId, + id = idCounter, + name = entityCommand.name, + payload = entityCommand.payload, + CrudCommandType.CREATE, + Some(state) + ) + currentCommand = CrudEntity.OutstandingCommand(idCounter, actorId + ":" + entityId + ":" + idCounter, sender) + commandStartTime = System.nanoTime() + concurrencyEnforcer ! Action(currentCommand.actionId, () => { + self ! command + }) + } + + private[this] final def handleCommand(initCommand: CrudInitCommand, sender: ActorRef): Unit = { + idCounter += 1 + val command = CrudCommand( + entityId = entityId, + subEntityId = initCommand.entityId, + id = idCounter, + name = initCommand.name, + payload = initCommand.payload, + Some(state) + ) + currentCommand = CrudEntity.OutstandingCommand(idCounter, actorId + ":" + entityId + ":" + idCounter, sender) + commandStartTime = System.nanoTime() + concurrencyEnforcer ! Action( + currentCommand.actionId, + () => { + initCommand.`type` match { + case CrudCommandType.CREATE => + client.create(command) pipeTo self + + case CrudCommandType.FETCH => + client.fetch(command) pipeTo self + + case CrudCommandType.UPDATE => + client.update(command) pipeTo self + + case CrudCommandType.DELETE => + client.delete(command) pipeTo self + } + } + ) + } + + private final def esReplyToUfReply(reply: CrudReply): UserFunctionReply = + UserFunctionReply( + clientAction = reply.clientAction, + sideEffects = reply.sideEffects + ) + + private final def esReplyToUfReply(reply: CrudFetchReply): UserFunctionReply = + UserFunctionReply( + clientAction = reply.clientAction, + sideEffects = reply.sideEffects + ) + + private final def createFailure(message: String) = + UserFunctionReply( + clientAction = Some(ClientAction(ClientAction.Action.Failure(Failure(description = message)))) + ) + + override final def receiveCommand: PartialFunction[Any, Unit] = { + + case command: CrudInitCommand if currentCommand != null => + stashedCommands = stashedCommands.enqueue((command, sender())) + + case command: CrudInitCommand => + handleCommand(command, sender()) + + case CrudReplies(m, _) => + m match { + case CrudReplies.Message.Reply(r) => + reportActionComplete() + val commandId = currentCommand.commandId + r.state match { + case None => + currentCommand.replyTo ! esReplyToUfReply(r) + commandHandled() + case Some(event) => + reportDatabaseOperationStarted() + persistAll(List(event)) { _ => + state = CrudState(Some(event)) + reportDatabaseOperationFinished() + // Make sure that the current request is still ours + if (currentCommand == null || currentCommand.commandId != commandId) { + crash("Internal error - currentRequest changed before all events were persisted") + } + currentCommand.replyTo ! esReplyToUfReply(r) + commandHandled() + } + } + + case otherReply => // what to do here? + } + + case CrudFetchReplies(m, _) => + m match { + case CrudFetchReplies.Message.Reply(r) => + reportActionComplete() + currentCommand.replyTo ! esReplyToUfReply(r) + commandHandled() + + case otherReply => // what to do here? + } + + case Status.Failure(error) => + notifyOutstandingRequests("Unexpected entity termination") + throw error + + case SaveSnapshotSuccess(metadata) => + // Nothing to do + + case SaveSnapshotFailure(metadata, cause) => + log.error("Error saving snapshot", cause) + + case ReceiveTimeout => + context.parent ! ShardRegion.Passivate(stopMessage = CrudEntity.Stop) + + case CrudEntity.Stop => + stopped = true + if (currentCommand == null) { + context.stop(self) + } + } + + override final def receiveRecover: PartialFunction[Any, Unit] = { + case offer: SnapshotOffer => + // is it needed?? + + case RecoveryCompleted => + reportDatabaseOperationFinished() + + case event: pbAny => + state = CrudState(Some(event)) + } + + private def reportDatabaseOperationStarted(): Unit = + if (reportedDatabaseOperationStarted) { + log.warning("Already reported database operation started") + } else { + databaseOperationStartTime = System.nanoTime() + reportedDatabaseOperationStarted = true + statsCollector ! StatsCollector.DatabaseOperationStarted + } + + private def reportDatabaseOperationFinished(): Unit = + if (!reportedDatabaseOperationStarted) { + log.warning("Hadn't reported database operation started") + } else { + reportedDatabaseOperationStarted = false + statsCollector ! StatsCollector.DatabaseOperationFinished(System.nanoTime() - databaseOperationStartTime) + } +} diff --git a/proxy/core/src/main/scala/io/cloudstate/proxy/crudtwo/CrudSupportFactory.scala b/proxy/core/src/main/scala/io/cloudstate/proxy/crudtwo/CrudSupportFactory.scala new file mode 100644 index 000000000..169857b1f --- /dev/null +++ b/proxy/core/src/main/scala/io/cloudstate/proxy/crudtwo/CrudSupportFactory.scala @@ -0,0 +1,107 @@ +package io.cloudstate.proxy.crudtwo + +import akka.NotUsed +import akka.actor.{ActorRef, ActorSystem} +import akka.cluster.sharding.ShardRegion.HashCodeMessageExtractor +import akka.cluster.sharding.{ClusterSharding, ClusterShardingSettings} +import akka.event.Logging +import akka.grpc.GrpcClientSettings +import akka.stream.Materializer +import akka.stream.scaladsl.Flow +import akka.util.Timeout +import com.google.protobuf.ByteString +import com.google.protobuf.Descriptors.ServiceDescriptor +import io.cloudstate.protocol.crud_two.{CrudCommandType, CrudInitCommand, CrudTwoClient} +import io.cloudstate.protocol.entity.Entity +import io.cloudstate.proxy._ +import io.cloudstate.proxy.entity.{EntityCommand, UserFunctionReply} +import io.cloudstate.proxy.eventsourced.DynamicLeastShardAllocationStrategy + +import scala.concurrent.{ExecutionContext, Future} + +class CrudSupportFactory(system: ActorSystem, + config: EntityDiscoveryManager.Configuration, + grpcClientSettings: GrpcClientSettings, + concurrencyEnforcer: ActorRef, + statsCollector: ActorRef)(implicit ec: ExecutionContext, mat: Materializer) + extends EntityTypeSupportFactory { + + private final val log = Logging.getLogger(system, this.getClass) + + private val crudClient = CrudTwoClient(grpcClientSettings) + + override def buildEntityTypeSupport(entity: Entity, + serviceDescriptor: ServiceDescriptor, + methodDescriptors: Map[String, EntityMethodDescriptor]): EntityTypeSupport = { + validate(serviceDescriptor, methodDescriptors) + + val stateManagerConfig = CrudEntity.Configuration(entity.serviceName, + entity.persistenceId, + config.passivationTimeout, + config.relayOutputBufferSize) + + log.debug("Starting EventSourcedEntity for {}", entity.persistenceId) + val clusterSharding = ClusterSharding(system) + val clusterShardingSettings = ClusterShardingSettings(system) + val eventSourcedEntity = clusterSharding.start( + typeName = entity.persistenceId, + entityProps = CrudEntitySupervisor.props(crudClient, stateManagerConfig, concurrencyEnforcer, statsCollector), + settings = clusterShardingSettings, + messageExtractor = new EntityIdExtractor(config.numberOfShards), + allocationStrategy = new DynamicLeastShardAllocationStrategy(1, 10, 2, 0.0), + handOffStopMessage = CrudEntity.Stop + ) + + new CrudSupport(eventSourcedEntity, config.proxyParallelism, config.relayTimeout) + } + + private def validate(serviceDescriptor: ServiceDescriptor, + methodDescriptors: Map[String, EntityMethodDescriptor]): Unit = { + val streamedMethods = + methodDescriptors.values.filter(m => m.method.toProto.getClientStreaming || m.method.toProto.getServerStreaming) + if (streamedMethods.nonEmpty) { + val offendingMethods = streamedMethods.map(_.method.getName).mkString(",") + throw EntityDiscoveryException( + s"Event sourced entities do not support streamed methods, but ${serviceDescriptor.getFullName} has the following streamed methods: ${offendingMethods}" + ) + } + val methodsWithoutKeys = methodDescriptors.values.filter(_.keyFieldsCount < 1) + if (methodsWithoutKeys.nonEmpty) { + val offendingMethods = methodsWithoutKeys.map(_.method.getName).mkString(",") + throw new EntityDiscoveryException( + s"Event sourced entities do not support methods whose parameters do not have at least one field marked as entity_key, " + + "but ${serviceDescriptor.getFullName} has the following methods without keys: ${offendingMethods}" + ) + } + } +} + +private class CrudSupport(eventSourcedEntity: ActorRef, parallelism: Int, private implicit val relayTimeout: Timeout) + extends EntityTypeSupport { + import akka.pattern.ask + + override def handler(method: EntityMethodDescriptor): Flow[EntityCommand, UserFunctionReply, NotUsed] = + Flow[EntityCommand].mapAsync(parallelism) { command => + val subEntityId = method.extractSubEntityId(command.payload.fold(ByteString.EMPTY)(_.value)) + val commandType = extractCommandType(command.payload.fold(ByteString.EMPTY)(_.value)) + val initCommand = CrudInitCommand(entityId = command.entityId, + subEntityId = subEntityId, + name = command.name, + payload = command.payload, + `type` = commandType) + (eventSourcedEntity ? initCommand).mapTo[UserFunctionReply] + } + + override def handleUnary(command: EntityCommand): Future[UserFunctionReply] = + (eventSourcedEntity ? command).mapTo[UserFunctionReply] + + private def extractCommandType(string: ByteString): CrudCommandType = + // CRUD TODO be defined + CrudCommandType.CREATE +} + +private final class EntityIdExtractor(shards: Int) extends HashCodeMessageExtractor(shards) { + override final def entityId(message: Any): String = message match { + case command: EntityCommand => command.entityId + } +} From 012c1e3fe3865fac94409ca045f1c3f534594056 Mon Sep 17 00:00:00 2001 From: Guy Youansi Date: Thu, 14 May 2020 23:01:20 +0200 Subject: [PATCH 08/93] remove unused import --- .../javasupport/impl/crudtwo/CrudImpl.scala | 17 ++++++----------- .../cloudstate/proxy/crudtwo/CrudEntity.scala | 13 ++++++++++++- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/java-support/src/main/scala/io/cloudstate/javasupport/impl/crudtwo/CrudImpl.scala b/java-support/src/main/scala/io/cloudstate/javasupport/impl/crudtwo/CrudImpl.scala index fda15c481..d1a95dd9b 100644 --- a/java-support/src/main/scala/io/cloudstate/javasupport/impl/crudtwo/CrudImpl.scala +++ b/java-support/src/main/scala/io/cloudstate/javasupport/impl/crudtwo/CrudImpl.scala @@ -67,9 +67,10 @@ final class CrudImpl(_system: ActorSystem, private val entityId = "entityId" // FIXME entityId can be extract from command, where to get entityId from when creating the CrudImpl? private final val service = services.getOrElse(serviceName, throw new RuntimeException(s"Service not found: $serviceName")) - private var handler: CrudEntityHandler = service.factory.create(new CrudContextImpl(entityId)) // FIXME how to create it? + private var handler + : CrudEntityHandler = service.factory.create(new CrudContextImpl(entityId)) // FIXME how to create it? - override def create(command: CrudCommand): Future[CrudReplies] = { + override def create(command: CrudCommand): Future[CrudReplies] = Future.unit .map { _ => val cmd = ScalaPbAny.toJavaProto(command.payload.get) //FIXME payload empty? @@ -88,9 +89,8 @@ final class CrudImpl(_system: ActorSystem, ) ) } - } - override def fetch(command: CrudCommand): Future[CrudFetchReplies] = { + override def fetch(command: CrudCommand): Future[CrudFetchReplies] = Future.unit .map { _ => val cmd = ScalaPbAny.toJavaProto(command.payload.get) //FIXME payload empty? @@ -108,8 +108,7 @@ final class CrudImpl(_system: ActorSystem, ) ) ) - } - } + } override def update(command: CrudCommand): Future[CrudReplies] = ??? // same as create @@ -128,11 +127,7 @@ final class CrudImpl(_system: ActorSystem, with AbstractContext with AbstractClientActionContext with AbstractEffectContext - with ActivatableContext { - - //val encoded = anySupport.encodeScala(event) - // handler.handleState(ScalaPbAny.toJavaProto(encoded), new CrudEventContextImpl(entityId, nextSequenceNumber)) - } + with ActivatableContext {} // FIXME add final val subEntityId: String class CrudContextImpl(override final val entityId: String) extends CrudContext with AbstractContext diff --git a/proxy/core/src/main/scala/io/cloudstate/proxy/crudtwo/CrudEntity.scala b/proxy/core/src/main/scala/io/cloudstate/proxy/crudtwo/CrudEntity.scala index 6ccb40960..cd59f044a 100644 --- a/proxy/core/src/main/scala/io/cloudstate/proxy/crudtwo/CrudEntity.scala +++ b/proxy/core/src/main/scala/io/cloudstate/proxy/crudtwo/CrudEntity.scala @@ -25,7 +25,18 @@ import akka.persistence._ import akka.stream.Materializer import akka.util.Timeout import com.google.protobuf.any.{Any => pbAny} -import io.cloudstate.protocol.crud_two.{CreateCommand, CrudCommand, CrudCommandType, CrudFetchReplies, CrudFetchReply, CrudInitCommand, CrudReplies, CrudReply, CrudState, CrudTwoClient, DeleteCommand, FetchCommand, UpdateCommand} +import io.cloudstate.protocol.crud_two.{ + CreateCommand, + CrudCommand, + CrudCommandType, + CrudFetchReplies, + CrudFetchReply, + CrudInitCommand, + CrudReplies, + CrudReply, + CrudState, + CrudTwoClient, +} import io.cloudstate.protocol.entity._ import io.cloudstate.proxy.ConcurrencyEnforcer.{Action, ActionCompleted} import io.cloudstate.proxy.StatsCollector From dc810c4451d4134d437925ce03bdede629e312a7 Mon Sep 17 00:00:00 2001 From: Guy Youansi Date: Fri, 15 May 2020 12:54:39 +0200 Subject: [PATCH 09/93] Add comments and change the proto definition --- .../javasupport/crudtwo/CommandContext.java | 18 +++++-- .../crudtwo/CrudEntityExample.java | 37 ++++++++++++++ .../crudtwo/CrudEntityHandler.java | 10 +++- .../javasupport/impl/crudtwo/CrudImpl.scala | 40 ++++++++++----- protocols/protocol/cloudstate/crud_two.proto | 51 +++++++++++-------- .../proxy/UserFunctionTypeSupport.scala | 2 +- .../cloudstate/proxy/crudtwo/CrudEntity.scala | 35 ++----------- .../proxy/crudtwo/CrudSupportFactory.scala | 4 +- 8 files changed, 122 insertions(+), 75 deletions(-) create mode 100644 java-support/src/main/java/io/cloudstate/javasupport/crudtwo/CrudEntityExample.java diff --git a/java-support/src/main/java/io/cloudstate/javasupport/crudtwo/CommandContext.java b/java-support/src/main/java/io/cloudstate/javasupport/crudtwo/CommandContext.java index 502d12342..494c63e62 100644 --- a/java-support/src/main/java/io/cloudstate/javasupport/crudtwo/CommandContext.java +++ b/java-support/src/main/java/io/cloudstate/javasupport/crudtwo/CommandContext.java @@ -1,14 +1,15 @@ package io.cloudstate.javasupport.crudtwo; +import com.google.protobuf.Any; import io.cloudstate.javasupport.ClientActionContext; import io.cloudstate.javasupport.EffectContext; /** - * An event sourced command context. + * An crud command context. * *

Methods annotated with {@link CommandHandler} may take this is a parameter. It allows emitting - * new events in response to a command, along with forwarding the result to other entities, and - * performing side effects on other entities. + * new events (which represents the new persistent state) in response to a command, along with + * forwarding the result to other entities, and performing side effects on other entities. */ public interface CommandContext extends CrudContext, ClientActionContext, EffectContext { /** @@ -33,9 +34,16 @@ public interface CommandContext extends CrudContext, ClientActionContext, Effect long commandId(); /** - * The state of the entity on which the command is being executed + * Emit the given event which represents the new persistent state. The event will be persisted. + * + * @param event The event to emit. + */ + void emit(Object event); + + /** + * The persisted state of the entity on which the command is being executed * * @return The state of the entity */ - Object state(); + Any state(); } diff --git a/java-support/src/main/java/io/cloudstate/javasupport/crudtwo/CrudEntityExample.java b/java-support/src/main/java/io/cloudstate/javasupport/crudtwo/CrudEntityExample.java new file mode 100644 index 000000000..2e6a373ca --- /dev/null +++ b/java-support/src/main/java/io/cloudstate/javasupport/crudtwo/CrudEntityExample.java @@ -0,0 +1,37 @@ +package io.cloudstate.javasupport.crudtwo; + +@CrudEntity +public class CrudEntityExample { + + @CommandHandler + public com.google.protobuf.Empty createCommand(SomeCrudCommandType command, CommandContext ctx) { + SomeState state = new SomeState(ctx.state()); + state.set("YourSate"); + + ctx.emit(state); + return com.google.protobuf.Empty.getDefaultInstance(); + } + + @CommandHandler + public SomeState fetchCommand(SomeCrudCommandType command, CommandContext ctx) { + return new SomeState(ctx.state()); + } + + public static class SomeCrudCommandType { + // with entityId + // with subEntityId + // with crudCommandType + } + + public static class SomeState { + private Object state; + + public SomeState(Object state) { + this.state = state; + } + + public void set(String yourState) { + this.state = yourState; + } + } +} diff --git a/java-support/src/main/java/io/cloudstate/javasupport/crudtwo/CrudEntityHandler.java b/java-support/src/main/java/io/cloudstate/javasupport/crudtwo/CrudEntityHandler.java index c208475f6..0bba6c6e1 100644 --- a/java-support/src/main/java/io/cloudstate/javasupport/crudtwo/CrudEntityHandler.java +++ b/java-support/src/main/java/io/cloudstate/javasupport/crudtwo/CrudEntityHandler.java @@ -4,13 +4,21 @@ import java.util.Optional; /** - * Low level interface for handling events and commands on an entity. + * Low level interface for handling events (which represents the persistent state) and commands on + * an crud entity. * *

Generally, this should not be needed, instead, a class annotated with the {@link * EventHandler}, {@link CommandHandler} and similar annotations should be used. */ public interface CrudEntityHandler { + /** + * Handle the given command. + * + * @param command The command to handle. + * @param context The command context. + * @return The reply to the command, if the command isn't being forwarded elsewhere. + */ Optional handleCommand(Any command, CommandContext context); /** diff --git a/java-support/src/main/scala/io/cloudstate/javasupport/impl/crudtwo/CrudImpl.scala b/java-support/src/main/scala/io/cloudstate/javasupport/impl/crudtwo/CrudImpl.scala index d1a95dd9b..df5d19713 100644 --- a/java-support/src/main/scala/io/cloudstate/javasupport/impl/crudtwo/CrudImpl.scala +++ b/java-support/src/main/scala/io/cloudstate/javasupport/impl/crudtwo/CrudImpl.scala @@ -18,6 +18,7 @@ package io.cloudstate.javasupport.impl.crudtwo import akka.actor.ActorSystem import com.google.protobuf.Descriptors import com.google.protobuf.any.{Any => ScalaPbAny} +import com.google.protobuf.{Any => JavaPbAny} import io.cloudstate.javasupport.CloudStateRunner.Configuration import io.cloudstate.javasupport.crudtwo._ import io.cloudstate.javasupport.impl._ @@ -53,16 +54,17 @@ final class CrudImpl(_system: ActorSystem, rootContext: Context, configuration: Configuration) extends CrudTwo { + // how to deal with snapshot and events by handleState. Some kind of mapping? + // how to deal with emitted events? handleState is called now, is that right? // how to push the snapshot state to the user function? handleState? // should snapshot be exposed to the user function? // How to do snapshot here? - // how to deal with snapshot and events by handleState. Some kind of mapping? - // how to deal with emitted events? handleState is called now, is that right? private final val system = _system private final implicit val ec = system.dispatcher private final val services = _services.iterator.toMap + // One option for accessing the service name and the entityId could be to pass it in the CrudCommand. private val serviceName = "serviceName" // FIXME where to get the service name from? private val entityId = "entityId" // FIXME entityId can be extract from command, where to get entityId from when creating the CrudImpl? private final val service = @@ -75,7 +77,8 @@ final class CrudImpl(_system: ActorSystem, .map { _ => val cmd = ScalaPbAny.toJavaProto(command.payload.get) //FIXME payload empty? val state = ScalaPbAny.toJavaProto(command.state.get.payload.get) // FIXME state empty? FIXME payload empty? - val context = new CommandContextImpl(command.entityId, 0, command.name, command.id, state) + val context = + new CommandContextImpl(command.entityId, 0, command.name, command.id, state, handler, service.anySupport) val reply = handler.handleCommand(cmd, context) val clientAction = context.createClientAction(reply, false) CrudReplies( @@ -84,7 +87,7 @@ final class CrudImpl(_system: ActorSystem, command.id, clientAction, context.sideEffects, - Some(ScalaPbAny.fromJavaProto(reply.get())) // FIXME reply empty? + Some(context.events(0)) // FIXME deal with the events? ) ) ) @@ -95,7 +98,8 @@ final class CrudImpl(_system: ActorSystem, .map { _ => val cmd = ScalaPbAny.toJavaProto(command.payload.get) //FIXME payload empty? val state = ScalaPbAny.toJavaProto(command.state.get.payload.get) // FIXME state empty? FIXME payload empty? - val context = new CommandContextImpl(command.entityId, 0, command.name, command.id, state) + val context = + new CommandContextImpl(command.entityId, 0, command.name, command.id, state, handler, service.anySupport) val reply = handler.handleCommand(cmd, context) val clientAction = context.createClientAction(reply, false) CrudFetchReplies( @@ -103,16 +107,15 @@ final class CrudImpl(_system: ActorSystem, CrudFetchReply( command.id, clientAction, - context.sideEffects, - Some(ScalaPbAny.fromJavaProto(reply.get())) // FIXME reply empty? + context.sideEffects ) ) ) } - override def update(command: CrudCommand): Future[CrudReplies] = ??? // same as create + override def update(command: CrudCommand): Future[CrudReplies] = ??? - override def delete(command: CrudCommand): Future[CrudReplies] = ??? // same as create + override def delete(command: CrudCommand): Future[CrudReplies] = ??? trait AbstractContext extends CrudContext { override def serviceCallFactory(): ServiceCallFactory = rootContext.serviceCallFactory() @@ -122,13 +125,26 @@ final class CrudImpl(_system: ActorSystem, override val sequenceNumber: Long, override val commandName: String, override val commandId: Long, - override val state: AnyRef) + override val state: JavaPbAny, // not sure it is needed + val handler: CrudEntityHandler, + val anySupport: AnySupport) extends CommandContext with AbstractContext with AbstractClientActionContext with AbstractEffectContext - with ActivatableContext {} + with ActivatableContext { + + final var events: Vector[ScalaPbAny] = Vector.empty + + override def emit(event: Any): Unit = { + val encoded = anySupport.encodeScala(event) + // Snapshotting should be done!! + // We want to pass the new persistent state to the User Function and is it the right option (handler.handleState ...) + // The persisted state is already passed as part of the CommandContext of each Command + // handler.handleState(ScalaPbAny.toJavaProto(encoded), null) + events :+= encoded + } + } - // FIXME add final val subEntityId: String class CrudContextImpl(override final val entityId: String) extends CrudContext with AbstractContext } diff --git a/protocols/protocol/cloudstate/crud_two.proto b/protocols/protocol/cloudstate/crud_two.proto index db100bb91..027a9d1f7 100644 --- a/protocols/protocol/cloudstate/crud_two.proto +++ b/protocols/protocol/cloudstate/crud_two.proto @@ -28,6 +28,7 @@ import "cloudstate/entity.proto"; option java_package = "io.cloudstate.protocol"; option go_package = "cloudstate/protocol"; +// The type of the command to be executed enum CrudCommandType { UNKNOWN = 0; CREATE = 1; @@ -36,11 +37,13 @@ enum CrudCommandType { DELETE = 4; } +// The persisted state message CrudState { // The state payload google.protobuf.Any payload = 2; } +// Message for initiating the command execution message CrudInitCommand { // The ID of the entity. @@ -55,9 +58,14 @@ message CrudInitCommand { // The command payload. google.protobuf.Any payload = 4; + // The command type. CrudCommandType type = 5; } +// Message for the command to be execute +// I am not sure we need different command for each service operation like create, fetch, update and remove. +// Perhaps we would use it for clarity because some operation has different semantics +// (see CreateCommand, FetchCommand, UpdateCommand and DeleteCommand). Is it an option? message CrudCommand { // The ID of the entity. @@ -75,9 +83,7 @@ message CrudCommand { // The command payload. google.protobuf.Any payload = 5; - //CrudCommandType type = 6; - - // persistent state to be conveyed between persistent entity and the user function + // The persisted state to be conveyed between persistent entity and the user function. CrudState state = 6; } @@ -98,10 +104,8 @@ message CreateCommand { // The command payload. google.protobuf.Any payload = 5; - CrudCommandType type = 6; - - // persistent state to be conveyed between persistent entity and the user function - CrudState state = 7; + // The persisted state. + CrudState state = 6; } message FetchCommand { @@ -118,12 +122,8 @@ message FetchCommand { // Command name string name = 4; - // The command payload. - google.protobuf.Any payload = 5; - - CrudCommandType type = 6; - - CrudState state = 7; + // The persisted state. + CrudState state = 5; } message UpdateCommand { @@ -143,9 +143,8 @@ message UpdateCommand { // The command payload. google.protobuf.Any payload = 5; - CrudCommandType type = 6; - - CrudState state = 7; + // The persisted state. + CrudState state = 6; } message DeleteCommand { @@ -162,9 +161,8 @@ message DeleteCommand { // Command name string name = 4; - CrudCommandType type = 5; - - CrudState state = 6; + // The persisted state. + CrudState state = 5; } // A reply to a command. @@ -183,6 +181,7 @@ message CrudReply { google.protobuf.Any state = 4; } +// Missing better name. It will be fixed message CrudReplies { oneof message { CrudReply reply = 1; @@ -199,11 +198,9 @@ message CrudFetchReply { // Any side effects to perform repeated SideEffect side_effects = 3; - - // An optional state to be return to the caller. - google.protobuf.Any fetch_state = 4; } +// Missing better name. It will be fixed message CrudFetchReplies { oneof message { CrudFetchReply reply = 1; @@ -221,4 +218,14 @@ service CrudTwo { rpc update(CrudCommand) returns (CrudReplies) {} rpc delete(CrudCommand) returns (CrudReplies) {} + + + // another option + //rpc create(CreateCommand) returns (CrudReplies) {} + + //rpc fetch(FetchCommand) returns (CrudFetchReplies) {} + + //rpc update(UpdateCommand) returns (CrudReplies) {} + + //rpc delete(DeleteCommand) returns (CrudReplies) {} } diff --git a/proxy/core/src/main/scala/io/cloudstate/proxy/UserFunctionTypeSupport.scala b/proxy/core/src/main/scala/io/cloudstate/proxy/UserFunctionTypeSupport.scala index 71bbd05a3..cdefa499d 100644 --- a/proxy/core/src/main/scala/io/cloudstate/proxy/UserFunctionTypeSupport.scala +++ b/proxy/core/src/main/scala/io/cloudstate/proxy/UserFunctionTypeSupport.scala @@ -77,7 +77,7 @@ final class EntityMethodDescriptor(val method: MethodDescriptor) { keyFields.iterator.map(dm.getField).mkString(EntityMethodDescriptor.Separator) } - def extractSubEntityId(bytes: ByteString): String = + def extractCrudSubEntityId(bytes: ByteString): String = subEntityKeyFields.length match { case 0 => "" diff --git a/proxy/core/src/main/scala/io/cloudstate/proxy/crudtwo/CrudEntity.scala b/proxy/core/src/main/scala/io/cloudstate/proxy/crudtwo/CrudEntity.scala index cd59f044a..44eddb6a9 100644 --- a/proxy/core/src/main/scala/io/cloudstate/proxy/crudtwo/CrudEntity.scala +++ b/proxy/core/src/main/scala/io/cloudstate/proxy/crudtwo/CrudEntity.scala @@ -25,22 +25,11 @@ import akka.persistence._ import akka.stream.Materializer import akka.util.Timeout import com.google.protobuf.any.{Any => pbAny} -import io.cloudstate.protocol.crud_two.{ - CreateCommand, - CrudCommand, - CrudCommandType, - CrudFetchReplies, - CrudFetchReply, - CrudInitCommand, - CrudReplies, - CrudReply, - CrudState, - CrudTwoClient, -} +import io.cloudstate.protocol.crud_two._ import io.cloudstate.protocol.entity._ import io.cloudstate.proxy.ConcurrencyEnforcer.{Action, ActionCompleted} import io.cloudstate.proxy.StatsCollector -import io.cloudstate.proxy.entity.{EntityCommand, UserFunctionReply} +import io.cloudstate.proxy.entity.UserFunctionReply import scala.collection.immutable.Queue @@ -145,8 +134,8 @@ final class CrudEntity(configuration: CrudEntity.Configuration, extends PersistentActor with ActorLogging { - import context.dispatcher import akka.pattern.pipe + import context.dispatcher override final def persistenceId: String = configuration.userFunctionName + entityId @@ -208,24 +197,6 @@ final class CrudEntity(configuration: CrudEntity.Configuration, private[this] final def reportActionComplete() = concurrencyEnforcer ! ActionCompleted(currentCommand.actionId, System.nanoTime() - commandStartTime) - private[this] final def handleCommand(entityCommand: EntityCommand, sender: ActorRef): Unit = { - idCounter += 1 - val command = CreateCommand( - entityId = entityId, - subEntityId = entityCommand.entityId, - id = idCounter, - name = entityCommand.name, - payload = entityCommand.payload, - CrudCommandType.CREATE, - Some(state) - ) - currentCommand = CrudEntity.OutstandingCommand(idCounter, actorId + ":" + entityId + ":" + idCounter, sender) - commandStartTime = System.nanoTime() - concurrencyEnforcer ! Action(currentCommand.actionId, () => { - self ! command - }) - } - private[this] final def handleCommand(initCommand: CrudInitCommand, sender: ActorRef): Unit = { idCounter += 1 val command = CrudCommand( diff --git a/proxy/core/src/main/scala/io/cloudstate/proxy/crudtwo/CrudSupportFactory.scala b/proxy/core/src/main/scala/io/cloudstate/proxy/crudtwo/CrudSupportFactory.scala index 169857b1f..b7f466968 100644 --- a/proxy/core/src/main/scala/io/cloudstate/proxy/crudtwo/CrudSupportFactory.scala +++ b/proxy/core/src/main/scala/io/cloudstate/proxy/crudtwo/CrudSupportFactory.scala @@ -82,7 +82,7 @@ private class CrudSupport(eventSourcedEntity: ActorRef, parallelism: Int, privat override def handler(method: EntityMethodDescriptor): Flow[EntityCommand, UserFunctionReply, NotUsed] = Flow[EntityCommand].mapAsync(parallelism) { command => - val subEntityId = method.extractSubEntityId(command.payload.fold(ByteString.EMPTY)(_.value)) + val subEntityId = method.extractCrudSubEntityId(command.payload.fold(ByteString.EMPTY)(_.value)) val commandType = extractCommandType(command.payload.fold(ByteString.EMPTY)(_.value)) val initCommand = CrudInitCommand(entityId = command.entityId, subEntityId = subEntityId, @@ -96,7 +96,7 @@ private class CrudSupport(eventSourcedEntity: ActorRef, parallelism: Int, privat (eventSourcedEntity ? command).mapTo[UserFunctionReply] private def extractCommandType(string: ByteString): CrudCommandType = - // CRUD TODO be defined + // TODO to be defined for extracting from EntityMethodDescriptor CrudCommandType.CREATE } From 078517f0fe42254f4cd21029f1f3e792e9354541 Mon Sep 17 00:00:00 2001 From: Guy Youansi Date: Mon, 18 May 2020 12:54:36 +0200 Subject: [PATCH 10/93] Change the protocol to have one reply type for all operations and implement it the crud entity. Implement create and fetch operations for crud. Cleanup by deleting unused packages. Add a new dir in examples for crud shopping cart. --- .../io/cloudstate/javasupport/CloudState.java | 8 +- .../javasupport/crud/CommandContext.java | 9 +- .../javasupport/crud/CrudEntityHandler.java | 29 +- .../cloudstate/javasupport/crud/KeyValue.java | 248 ------------ .../javasupport/crudtwo/CommandContext.java | 49 --- .../javasupport/crudtwo/CommandHandler.java | 38 -- .../javasupport/crudtwo/CrudContext.java | 6 - .../javasupport/crudtwo/CrudEntity.java | 29 -- .../crudtwo/CrudEntityCreationContext.java | 8 - .../crudtwo/CrudEntityEventHandler.java | 30 -- .../crudtwo/CrudEntityExample.java | 37 -- .../crudtwo/CrudEntityFactory.java | 20 - .../crudtwo/CrudEntityHandler.java | 31 -- .../javasupport/crudtwo/CrudEventContext.java | 13 - .../javasupport/crudtwo/EventHandler.java | 32 -- .../javasupport/crudtwo/Snapshot.java | 25 -- .../javasupport/crudtwo/SnapshotContext.java | 11 - .../javasupport/crudtwo/SnapshotHandler.java | 26 -- .../javasupport/crudtwo/package-info.java | 14 - .../javasupport/CloudStateRunner.scala | 7 + .../AnnotationBasedCrudSupport.scala | 10 +- .../javasupport/impl/crud/CrudImpl.scala | 210 ++++++++++ .../crudone/AnnotationBasedCrudSupport.scala | 348 ---------------- .../javasupport/impl/crudone/CrudImpl.scala | 378 ------------------ .../javasupport/impl/crudtwo/CrudImpl.scala | 150 ------- .../crud/AnnotationBasedCrudSupportSpec.scala | 290 ++++++++++++++ .../AnnotationBasedCrudSupportSpec.scala | 84 ---- .../shoppingcart/persistence/domain.proto | 19 + .../crud/shoppingcart/shoppingcart.proto | 73 ++++ .../crud.persistence/domain.proto | 23 -- protocols/protocol/cloudstate/crud.proto | 126 ++++++ protocols/protocol/cloudstate/crud_one.proto | 159 -------- protocols/protocol/cloudstate/crud_two.proto | 231 ----------- .../proxy/EntityDiscoveryManager.scala | 9 +- .../proxy/{crudtwo => crud}/CrudEntity.scala | 125 +++--- .../CrudSupportFactory.scala | 23 +- .../cloudstate/proxy/crudone/CrudEntity.scala | 368 ----------------- .../proxy/crudone/CrudSupportFactory.scala | 95 ----- .../cloudstate/samples/shoppingcart/Main.java | 7 +- .../shoppingcart/ShoppingCartCrudEntity.java | 154 ++----- 40 files changed, 882 insertions(+), 2670 deletions(-) delete mode 100644 java-support/src/main/java/io/cloudstate/javasupport/crud/KeyValue.java delete mode 100644 java-support/src/main/java/io/cloudstate/javasupport/crudtwo/CommandContext.java delete mode 100644 java-support/src/main/java/io/cloudstate/javasupport/crudtwo/CommandHandler.java delete mode 100644 java-support/src/main/java/io/cloudstate/javasupport/crudtwo/CrudContext.java delete mode 100644 java-support/src/main/java/io/cloudstate/javasupport/crudtwo/CrudEntity.java delete mode 100644 java-support/src/main/java/io/cloudstate/javasupport/crudtwo/CrudEntityCreationContext.java delete mode 100644 java-support/src/main/java/io/cloudstate/javasupport/crudtwo/CrudEntityEventHandler.java delete mode 100644 java-support/src/main/java/io/cloudstate/javasupport/crudtwo/CrudEntityExample.java delete mode 100644 java-support/src/main/java/io/cloudstate/javasupport/crudtwo/CrudEntityFactory.java delete mode 100644 java-support/src/main/java/io/cloudstate/javasupport/crudtwo/CrudEntityHandler.java delete mode 100644 java-support/src/main/java/io/cloudstate/javasupport/crudtwo/CrudEventContext.java delete mode 100644 java-support/src/main/java/io/cloudstate/javasupport/crudtwo/EventHandler.java delete mode 100644 java-support/src/main/java/io/cloudstate/javasupport/crudtwo/Snapshot.java delete mode 100644 java-support/src/main/java/io/cloudstate/javasupport/crudtwo/SnapshotContext.java delete mode 100644 java-support/src/main/java/io/cloudstate/javasupport/crudtwo/SnapshotHandler.java delete mode 100644 java-support/src/main/java/io/cloudstate/javasupport/crudtwo/package-info.java rename java-support/src/main/scala/io/cloudstate/javasupport/impl/{crudtwo => crud}/AnnotationBasedCrudSupport.scala (99%) create mode 100644 java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/CrudImpl.scala delete mode 100644 java-support/src/main/scala/io/cloudstate/javasupport/impl/crudone/AnnotationBasedCrudSupport.scala delete mode 100644 java-support/src/main/scala/io/cloudstate/javasupport/impl/crudone/CrudImpl.scala delete mode 100644 java-support/src/main/scala/io/cloudstate/javasupport/impl/crudtwo/CrudImpl.scala create mode 100644 java-support/src/test/scala/io/cloudstate/javasupport/impl/crud/AnnotationBasedCrudSupportSpec.scala delete mode 100644 java-support/src/test/scala/io/cloudstate/javasupport/impl/crudone/AnnotationBasedCrudSupportSpec.scala create mode 100644 protocols/example/crud/shoppingcart/persistence/domain.proto create mode 100644 protocols/example/crud/shoppingcart/shoppingcart.proto delete mode 100644 protocols/example/shoppingcart/crud.persistence/domain.proto create mode 100644 protocols/protocol/cloudstate/crud.proto delete mode 100644 protocols/protocol/cloudstate/crud_one.proto delete mode 100644 protocols/protocol/cloudstate/crud_two.proto rename proxy/core/src/main/scala/io/cloudstate/proxy/{crudtwo => crud}/CrudEntity.scala (72%) rename proxy/core/src/main/scala/io/cloudstate/proxy/{crudtwo => crud}/CrudSupportFactory.scala (86%) delete mode 100644 proxy/core/src/main/scala/io/cloudstate/proxy/crudone/CrudEntity.scala delete mode 100644 proxy/core/src/main/scala/io/cloudstate/proxy/crudone/CrudSupportFactory.scala diff --git a/java-support/src/main/java/io/cloudstate/javasupport/CloudState.java b/java-support/src/main/java/io/cloudstate/javasupport/CloudState.java index 558d0f37a..a15e9d9a3 100644 --- a/java-support/src/main/java/io/cloudstate/javasupport/CloudState.java +++ b/java-support/src/main/java/io/cloudstate/javasupport/CloudState.java @@ -10,13 +10,13 @@ import io.cloudstate.javasupport.impl.AnySupport; import io.cloudstate.javasupport.impl.crdt.AnnotationBasedCrdtSupport; import io.cloudstate.javasupport.impl.crdt.CrdtStatefulService; +import io.cloudstate.javasupport.impl.crud.AnnotationBasedCrudSupport; +import io.cloudstate.javasupport.impl.crud.CrudStatefulService; import io.cloudstate.javasupport.impl.eventsourced.AnnotationBasedEventSourcedSupport; import io.cloudstate.javasupport.impl.eventsourced.EventSourcedStatefulService; import akka.Done; -import java.util.Arrays; -import java.util.List; import java.util.concurrent.CompletionStage; import java.util.HashMap; import java.util.Map; @@ -194,8 +194,8 @@ public CloudState registerCrudEntity( services.put( descriptor.getFullName(), - new EventSourcedStatefulService( - new AnnotationBasedEventSourcedSupport(entityClass, anySupport, descriptor), + new CrudStatefulService( + new AnnotationBasedCrudSupport(entityClass, anySupport, descriptor), descriptor, anySupport, persistenceId, diff --git a/java-support/src/main/java/io/cloudstate/javasupport/crud/CommandContext.java b/java-support/src/main/java/io/cloudstate/javasupport/crud/CommandContext.java index 38056ac7d..cf53a22a0 100644 --- a/java-support/src/main/java/io/cloudstate/javasupport/crud/CommandContext.java +++ b/java-support/src/main/java/io/cloudstate/javasupport/crud/CommandContext.java @@ -4,11 +4,11 @@ import io.cloudstate.javasupport.EffectContext; /** - * An event sourced command context. + * An crud command context. * *

Methods annotated with {@link CommandHandler} may take this is a parameter. It allows emitting - * new events in response to a command, along with forwarding the result to other entities, and - * performing side effects on other entities. + * new events (which represents the new persistent state) in response to a command, along with + * forwarding the result to other entities, and performing side effects on other entities. */ public interface CommandContext extends CrudContext, ClientActionContext, EffectContext { /** @@ -33,8 +33,7 @@ public interface CommandContext extends CrudContext, ClientActionContext, Effect long commandId(); /** - * Emit the given event. The event will be persisted, and the handler of the event defined in the - * current behavior will immediately be executed to pick it up. + * Emit the given event which represents the new persistent state. The event will be persisted. * * @param event The event to emit. */ diff --git a/java-support/src/main/java/io/cloudstate/javasupport/crud/CrudEntityHandler.java b/java-support/src/main/java/io/cloudstate/javasupport/crud/CrudEntityHandler.java index 4bd4b536e..6425a65ad 100644 --- a/java-support/src/main/java/io/cloudstate/javasupport/crud/CrudEntityHandler.java +++ b/java-support/src/main/java/io/cloudstate/javasupport/crud/CrudEntityHandler.java @@ -5,35 +5,28 @@ import java.util.Optional; /** - * Low level interface for handling events and commands on an entity. + * Low level interface for handling events (which represents the persistent state) and commands on + * an crud entity. * *

Generally, this should not be needed, instead, a class annotated with the {@link - * EventHandler}, {@link CommandHandler} and similar annotations should be used. + * StateHandler}, {@link CommandHandler} and similar annotations should be used. */ public interface CrudEntityHandler { + /** + * Handle the given command. + * + * @param command The command to handle. + * @param context The command context. + * @return The reply to the command, if the command isn't being forwarded elsewhere. + */ Optional handleCommand(Any command, CommandContext context); - Optional handleCreateCommand(Any command, CommandContext context); - - Optional handleFetchCommand(Any command, CommandContext context); - - Optional handleUpdateCommand(Any command, CommandContext context); - - Optional handleDeleteCommand(Any command, CommandContext context); - /** * Handle the given state. * * @param state The state to handle. - * @param context The snapshot context. + * @param context The state context. */ void handleState(Any state, SnapshotContext context); - - /** - * Snapshot the object. - * - * @return The current snapshot, if this object supports snapshoting, otherwise empty. - */ - Optional snapshot(SnapshotContext context); } diff --git a/java-support/src/main/java/io/cloudstate/javasupport/crud/KeyValue.java b/java-support/src/main/java/io/cloudstate/javasupport/crud/KeyValue.java deleted file mode 100644 index 31c782949..000000000 --- a/java-support/src/main/java/io/cloudstate/javasupport/crud/KeyValue.java +++ /dev/null @@ -1,248 +0,0 @@ -package io.cloudstate.javasupport.crud; - -import akka.util.ByteString; -import io.cloudstate.javasupport.eventsourced.EventHandler; -import io.cloudstate.javasupport.eventsourced.Snapshot; -import io.cloudstate.javasupport.eventsourced.SnapshotHandler; -import io.cloudstate.keyvalue.KeyValue.KVEntity; -import io.cloudstate.keyvalue.KeyValue.KVModification; -import io.cloudstate.keyvalue.KeyValue.KVModificationOrBuilder; - -import java.util.Optional; -import java.util.function.Function; - -import static java.util.Objects.requireNonNull; - -public final class KeyValue { - public static final class Key implements Comparable> { - private final String _name; - private final Function _reader; - private final Function _writer; - - Key( - final String name, - final Function reader, - final Function writer) { - this._name = name; - this._reader = reader; - this._writer = writer; - } - - public final String name() { - return this._name; - } - - public final Function reader() { - return this._reader; - } - - public final Function writer() { - return this._writer; - } - - @Override - public final boolean equals(final Object that) { - if (this == that) return true; - else if (that instanceof Key) return ((Key) that).name().equals(this.name()); - else return false; - } - - @Override - public final int hashCode() { - return 403 + name().hashCode(); - } - - @Override - public final int compareTo(final Key other) { - return name().compareTo(other.name()); - } - } - - public static final Key keyOf( - final String name, - final Function reader, - final Function writer) { - return new Key( - requireNonNull(name, "Key name cannot be null"), - requireNonNull(reader, "Key reader cannot be null"), - requireNonNull(writer, "Key writer cannot be null")); - } - - // represents the persistent state for last changed for the key value entity - private static final class ChangeMap { - // updated and removed collections are mutual exclusive meaning a key should be only in one - // collection. - - // used for persisting all last updated by keys - private final java.util.Map updated = new java.util.TreeMap<>(); - // used for persisting all last removed by keys - private final java.util.Set removed = new java.util.TreeSet<>(); - - private void setUpdatedKey(String key, ByteString value) { - updated.put(key, value); - removed.remove(key); - } - - private void setRemovedKey(String key) { - removed.add(key); - updated.remove(key); - } - - private KVModification kvModification() { - final KVModification.Builder builder = KVModification.newBuilder(); - updated.forEach( - (k, v) -> { - builder.putUpdatedEntries(k, com.google.protobuf.ByteString.copyFrom(v.asByteBuffer())); - }); - removed.forEach(builder::addRemovedKeys); - return builder.build(); - } - } - - public static final class Map { - /* - TODO document invariants - */ - private final java.util.Map unparsed; - private final java.util.Map, Object> updated = new java.util.TreeMap<>(); - private final java.util.Set removed = new java.util.TreeSet<>(); - private final ChangeMap lastChanged = new ChangeMap(); - - public Map() { - this.unparsed = new java.util.TreeMap<>(); - } - - public final Optional get(final Key key) { - final T value = (T) updated.get(key); - if (value != null) { - return Optional.of(value); - } else { - final ByteString bytes = unparsed.get(key.name()); - if (bytes != null) { - final T parsed = key.reader().apply(bytes); - requireNonNull(parsed, "Key reader not allowed to read `null`"); - updated.put((Key) key, parsed); - return Optional.of(parsed); - } else { - return Optional.empty(); - } - } - } - - public final void set(final Key key, final T value) { - requireNonNull(key, "Map key must not be null"); - requireNonNull(value, "Map value must not be null"); - updated.put(key, value); - unparsed.remove(key.name()); - removed.remove(key.name()); - } - - public final boolean remove(final Key key) { - requireNonNull(key, "Map key must not be null"); - if (!removed.contains(key.name()) - && (updated.remove(key) != null | unparsed.remove(key.name()) != null)) { - removed.add(key.name()); - return true; - } else { - return false; - } - } - - final KVEntity toProto() { - final KVEntity.Builder builder = KVEntity.newBuilder(); - unparsed.forEach( - (k, v) -> { - builder.putEntries(k, com.google.protobuf.ByteString.copyFrom(v.asByteBuffer())); - }); - updated.forEach( - (k, v) -> { - // Skip as this item is only a parsed un-changed item, and we already added those in the - // previous step - if (!unparsed.containsKey(k.name())) { - builder.putEntries( - k.name(), - com.google.protobuf.ByteString.copyFrom( - ((Key) k).writer().apply(v).asByteBuffer())); - } - }); - return builder.build(); - } - - public final KVModification toProtoModification() { - updated.forEach( - (k, v) -> { - // Skip those which remain as unparsed, as they have not been changed - if (!unparsed.containsKey(k.name())) { - lastChanged.setUpdatedKey(k.name(), ((Key) k).writer().apply(v)); - } - }); - removed.forEach(lastChanged::setRemovedKey); - return lastChanged.kvModification(); - } - - final void resetTo(KVEntity entityState) { - unparsed.clear(); - updated.clear(); - removed.clear(); - entityState - .getEntriesMap() - .forEach( - (k, v) -> - unparsed.put( - k, - v.isEmpty() - ? ByteString.empty() - : ByteString.fromArrayUnsafe(v.toByteArray()))); - } - - final void applyModification(KVModificationOrBuilder modification) { - // Apply new modifications to the base unparsed values - modification - .getUpdatedEntriesMap() - .forEach( - (k, v) -> { - unparsed.put( - k, - v.isEmpty() ? ByteString.empty() : ByteString.fromArrayUnsafe(v.toByteArray())); - - lastChanged.setUpdatedKey( - k, - v.isEmpty() - ? ByteString.empty() - : ByteString.fromArrayUnsafe( - v.toByteArray())); // restore the persisted last updated keys - }); - - modification - .getRemovedKeysList() - .forEach( - k -> { - unparsed.remove(k); - lastChanged.setRemovedKey(k); // restored the persisted last removed keys - }); - } - } - - public abstract static class KeyValueEntity { - private final Map state = new Map(); - - protected Map state() { - return state; - } - - @Snapshot - public KVEntity snapshot() { - return state.toProto(); - } - - @SnapshotHandler - public void handleSnapshot(final KVEntity entityState) { - state.resetTo(entityState); - } - - @EventHandler - public void kVModification(final KVModification modification) { - state.applyModification(modification); - } - } -} diff --git a/java-support/src/main/java/io/cloudstate/javasupport/crudtwo/CommandContext.java b/java-support/src/main/java/io/cloudstate/javasupport/crudtwo/CommandContext.java deleted file mode 100644 index 494c63e62..000000000 --- a/java-support/src/main/java/io/cloudstate/javasupport/crudtwo/CommandContext.java +++ /dev/null @@ -1,49 +0,0 @@ -package io.cloudstate.javasupport.crudtwo; - -import com.google.protobuf.Any; -import io.cloudstate.javasupport.ClientActionContext; -import io.cloudstate.javasupport.EffectContext; - -/** - * An crud command context. - * - *

Methods annotated with {@link CommandHandler} may take this is a parameter. It allows emitting - * new events (which represents the new persistent state) in response to a command, along with - * forwarding the result to other entities, and performing side effects on other entities. - */ -public interface CommandContext extends CrudContext, ClientActionContext, EffectContext { - /** - * The current sequence number of events in this entity. - * - * @return The current sequence number. - */ - long sequenceNumber(); - - /** - * The name of the command being executed. - * - * @return The name of the command. - */ - String commandName(); - - /** - * The id of the command being executed. - * - * @return The id of the command. - */ - long commandId(); - - /** - * Emit the given event which represents the new persistent state. The event will be persisted. - * - * @param event The event to emit. - */ - void emit(Object event); - - /** - * The persisted state of the entity on which the command is being executed - * - * @return The state of the entity - */ - Any state(); -} diff --git a/java-support/src/main/java/io/cloudstate/javasupport/crudtwo/CommandHandler.java b/java-support/src/main/java/io/cloudstate/javasupport/crudtwo/CommandHandler.java deleted file mode 100644 index fb0eec2e4..000000000 --- a/java-support/src/main/java/io/cloudstate/javasupport/crudtwo/CommandHandler.java +++ /dev/null @@ -1,38 +0,0 @@ -package io.cloudstate.javasupport.crudtwo; - -import io.cloudstate.javasupport.impl.CloudStateAnnotation; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/** - * Marks a method as a command handler. - * - *

This method will be invoked whenever the service call with name that matches this command - * handlers name is invoked. - * - *

The method may take the command object as a parameter, its type must match the gRPC service - * input type. - * - *

The return type of the method must match the gRPC services output type. - * - *

The method may also take a {@link CommandContext}, and/or a {@link - * io.cloudstate.javasupport.EntityId} annotated {@link String} parameter. - */ -@CloudStateAnnotation -@Target(ElementType.METHOD) -@Retention(RetentionPolicy.RUNTIME) -public @interface CommandHandler { - - /** - * The name of the command to handle. - * - *

If not specified, the name of the method will be used as the command name, with the first - * letter capitalized to match the gRPC convention of capitalizing rpc method names. - * - * @return The command name. - */ - String name() default ""; -} diff --git a/java-support/src/main/java/io/cloudstate/javasupport/crudtwo/CrudContext.java b/java-support/src/main/java/io/cloudstate/javasupport/crudtwo/CrudContext.java deleted file mode 100644 index f1836d0ab..000000000 --- a/java-support/src/main/java/io/cloudstate/javasupport/crudtwo/CrudContext.java +++ /dev/null @@ -1,6 +0,0 @@ -package io.cloudstate.javasupport.crudtwo; - -import io.cloudstate.javasupport.EntityContext; - -/** Root context for all event sourcing contexts. */ -public interface CrudContext extends EntityContext {} diff --git a/java-support/src/main/java/io/cloudstate/javasupport/crudtwo/CrudEntity.java b/java-support/src/main/java/io/cloudstate/javasupport/crudtwo/CrudEntity.java deleted file mode 100644 index 4a148c87c..000000000 --- a/java-support/src/main/java/io/cloudstate/javasupport/crudtwo/CrudEntity.java +++ /dev/null @@ -1,29 +0,0 @@ -package io.cloudstate.javasupport.crudtwo; - -import io.cloudstate.javasupport.impl.CloudStateAnnotation; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/** An crud entity. */ -@CloudStateAnnotation -@Target(ElementType.TYPE) -@Retention(RetentionPolicy.RUNTIME) -public @interface CrudEntity { - /** - * The name of the persistence id. - * - *

If not specified, defaults to the entities unqualified classname. It's strongly recommended - * that you specify it explicitly. - */ - String persistenceId() default ""; - - /** - * Specifies how snapshots of the entity state should be made: Zero means use default from - * configuration file. (Default) Any negative value means never snapshot. Any positive value means - * snapshot at-or-after that number of events. - */ - int snapshotEvery() default 0; -} diff --git a/java-support/src/main/java/io/cloudstate/javasupport/crudtwo/CrudEntityCreationContext.java b/java-support/src/main/java/io/cloudstate/javasupport/crudtwo/CrudEntityCreationContext.java deleted file mode 100644 index 97b838b5b..000000000 --- a/java-support/src/main/java/io/cloudstate/javasupport/crudtwo/CrudEntityCreationContext.java +++ /dev/null @@ -1,8 +0,0 @@ -package io.cloudstate.javasupport.crudtwo; - -/** - * Creation context for {@link CrudEntity} annotated entities. - * - *

This may be accepted as an argument to the constructor of an event sourced entity. - */ -public interface CrudEntityCreationContext extends CrudContext {} diff --git a/java-support/src/main/java/io/cloudstate/javasupport/crudtwo/CrudEntityEventHandler.java b/java-support/src/main/java/io/cloudstate/javasupport/crudtwo/CrudEntityEventHandler.java deleted file mode 100644 index 41aa36251..000000000 --- a/java-support/src/main/java/io/cloudstate/javasupport/crudtwo/CrudEntityEventHandler.java +++ /dev/null @@ -1,30 +0,0 @@ -package io.cloudstate.javasupport.crudtwo; - -import com.google.protobuf.Any; -import io.cloudstate.javasupport.crud.SnapshotContext; - -import java.util.Optional; - -/** - * Low level interface for handling events and commands on an entity. - * - *

Generally, this should not be needed, instead, a class annotated with the {@link - * EventHandler}, {@link CommandHandler} and similar annotations should be used. - */ -public interface CrudEntityEventHandler { - - /** - * Handle the given snapshot. - * - * @param snapshot The snapshot to handle. - * @param context The snapshot context. - */ - void handleSnapshot(Any snapshot, SnapshotContext context); - - /** - * Snapshot the object. - * - * @return The current snapshot, if this object supports snapshoting, otherwise empty. - */ - Optional snapshot(SnapshotContext context); -} diff --git a/java-support/src/main/java/io/cloudstate/javasupport/crudtwo/CrudEntityExample.java b/java-support/src/main/java/io/cloudstate/javasupport/crudtwo/CrudEntityExample.java deleted file mode 100644 index 2e6a373ca..000000000 --- a/java-support/src/main/java/io/cloudstate/javasupport/crudtwo/CrudEntityExample.java +++ /dev/null @@ -1,37 +0,0 @@ -package io.cloudstate.javasupport.crudtwo; - -@CrudEntity -public class CrudEntityExample { - - @CommandHandler - public com.google.protobuf.Empty createCommand(SomeCrudCommandType command, CommandContext ctx) { - SomeState state = new SomeState(ctx.state()); - state.set("YourSate"); - - ctx.emit(state); - return com.google.protobuf.Empty.getDefaultInstance(); - } - - @CommandHandler - public SomeState fetchCommand(SomeCrudCommandType command, CommandContext ctx) { - return new SomeState(ctx.state()); - } - - public static class SomeCrudCommandType { - // with entityId - // with subEntityId - // with crudCommandType - } - - public static class SomeState { - private Object state; - - public SomeState(Object state) { - this.state = state; - } - - public void set(String yourState) { - this.state = yourState; - } - } -} diff --git a/java-support/src/main/java/io/cloudstate/javasupport/crudtwo/CrudEntityFactory.java b/java-support/src/main/java/io/cloudstate/javasupport/crudtwo/CrudEntityFactory.java deleted file mode 100644 index f6eef5143..000000000 --- a/java-support/src/main/java/io/cloudstate/javasupport/crudtwo/CrudEntityFactory.java +++ /dev/null @@ -1,20 +0,0 @@ -package io.cloudstate.javasupport.crudtwo; - -import io.cloudstate.javasupport.eventsourced.CommandHandler; -import io.cloudstate.javasupport.eventsourced.EventHandler; - -/** - * Low level interface for handling events and commands on an entity. - * - *

Generally, this should not be needed, instead, a class annotated with the {@link - * EventHandler}, {@link CommandHandler} and similar annotations should be used. - */ -public interface CrudEntityFactory { - /** - * Create an entity handler for the given context. - * - * @param context The context. - * @return The handler for the given context. - */ - CrudEntityHandler create(CrudContext context); -} diff --git a/java-support/src/main/java/io/cloudstate/javasupport/crudtwo/CrudEntityHandler.java b/java-support/src/main/java/io/cloudstate/javasupport/crudtwo/CrudEntityHandler.java deleted file mode 100644 index 0bba6c6e1..000000000 --- a/java-support/src/main/java/io/cloudstate/javasupport/crudtwo/CrudEntityHandler.java +++ /dev/null @@ -1,31 +0,0 @@ -package io.cloudstate.javasupport.crudtwo; - -import com.google.protobuf.Any; -import java.util.Optional; - -/** - * Low level interface for handling events (which represents the persistent state) and commands on - * an crud entity. - * - *

Generally, this should not be needed, instead, a class annotated with the {@link - * EventHandler}, {@link CommandHandler} and similar annotations should be used. - */ -public interface CrudEntityHandler { - - /** - * Handle the given command. - * - * @param command The command to handle. - * @param context The command context. - * @return The reply to the command, if the command isn't being forwarded elsewhere. - */ - Optional handleCommand(Any command, CommandContext context); - - /** - * Handle the given state. - * - * @param state The state to handle. - * @param context The snapshot context. - */ - void handleState(Any state, SnapshotContext context); -} diff --git a/java-support/src/main/java/io/cloudstate/javasupport/crudtwo/CrudEventContext.java b/java-support/src/main/java/io/cloudstate/javasupport/crudtwo/CrudEventContext.java deleted file mode 100644 index 01bdb9eba..000000000 --- a/java-support/src/main/java/io/cloudstate/javasupport/crudtwo/CrudEventContext.java +++ /dev/null @@ -1,13 +0,0 @@ -package io.cloudstate.javasupport.crudtwo; - -import io.cloudstate.javasupport.crud.CrudContext; - -/** Context for an event. */ -public interface CrudEventContext extends CrudContext { - /** - * The sequence number of the current event being processed. - * - * @return The sequence number. - */ - long sequenceNumber(); -} diff --git a/java-support/src/main/java/io/cloudstate/javasupport/crudtwo/EventHandler.java b/java-support/src/main/java/io/cloudstate/javasupport/crudtwo/EventHandler.java deleted file mode 100644 index 98d85320d..000000000 --- a/java-support/src/main/java/io/cloudstate/javasupport/crudtwo/EventHandler.java +++ /dev/null @@ -1,32 +0,0 @@ -package io.cloudstate.javasupport.crudtwo; - -import io.cloudstate.javasupport.eventsourced.EventContext; -import io.cloudstate.javasupport.impl.CloudStateAnnotation; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/** - * Marks a method as an event handler. - * - *

This method will be invoked whenever an event matching this event handlers event class is - * either replayed on entity recovery, by a command handler. - * - *

The method may take the event object as a parameter. - * - *

Methods annotated with this may take an {@link EventContext}. - */ -@CloudStateAnnotation -@Target(ElementType.METHOD) -@Retention(RetentionPolicy.RUNTIME) -public @interface EventHandler { - /** - * The event class. Generally, this will be determined by looking at the parameter of the event - * handler method, however if the event doesn't need to be passed to the method (for example, - * perhaps it contains no data), then this can be used to indicate which event this handler - * handles. - */ - Class eventClass() default Object.class; -} diff --git a/java-support/src/main/java/io/cloudstate/javasupport/crudtwo/Snapshot.java b/java-support/src/main/java/io/cloudstate/javasupport/crudtwo/Snapshot.java deleted file mode 100644 index fbfebd772..000000000 --- a/java-support/src/main/java/io/cloudstate/javasupport/crudtwo/Snapshot.java +++ /dev/null @@ -1,25 +0,0 @@ -package io.cloudstate.javasupport.crudtwo; - -import io.cloudstate.javasupport.crud.SnapshotContext; -import io.cloudstate.javasupport.impl.CloudStateAnnotation; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/** - * Marks a method as a snapshot method. - * - *

An event sourced behavior may have at most one of these. When provided, it will be - * periodically (every n events emitted) be invoked to retrieve a snapshot of the current - * state, to be persisted, so that the event log can be loaded without replaying the entire history. - * - *

The method must return the current state of the entity. - * - *

The method may accept a {@link SnapshotContext} parameter. - */ -@CloudStateAnnotation -@Target(ElementType.METHOD) -@Retention(RetentionPolicy.RUNTIME) -public @interface Snapshot {} diff --git a/java-support/src/main/java/io/cloudstate/javasupport/crudtwo/SnapshotContext.java b/java-support/src/main/java/io/cloudstate/javasupport/crudtwo/SnapshotContext.java deleted file mode 100644 index a456d334e..000000000 --- a/java-support/src/main/java/io/cloudstate/javasupport/crudtwo/SnapshotContext.java +++ /dev/null @@ -1,11 +0,0 @@ -package io.cloudstate.javasupport.crudtwo; - -/** A snapshot context. */ -public interface SnapshotContext extends CrudContext { - /** - * The sequence number of the last event that this snapshot includes. - * - * @return The sequence number. - */ - long sequenceNumber(); -} diff --git a/java-support/src/main/java/io/cloudstate/javasupport/crudtwo/SnapshotHandler.java b/java-support/src/main/java/io/cloudstate/javasupport/crudtwo/SnapshotHandler.java deleted file mode 100644 index 402c7a8ed..000000000 --- a/java-support/src/main/java/io/cloudstate/javasupport/crudtwo/SnapshotHandler.java +++ /dev/null @@ -1,26 +0,0 @@ -package io.cloudstate.javasupport.crudtwo; - -import io.cloudstate.javasupport.impl.CloudStateAnnotation; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/** - * Marks a method as a snapshot handler. - * - *

If, when recovering an entity, that entity has a snapshot, the snapshot will be passed to a - * corresponding snapshot handler method whose argument matches its type. The entity must set its - * current state to that snapshot. - * - *

An entity may declare more than one snapshot handler if it wants different handling for - * different types. - * - *

The snapshot handler method may additionally accept a {@link SnapshotContext} parameter, - * allowing it to access context for the snapshot, if required. - */ -@CloudStateAnnotation -@Target(ElementType.METHOD) -@Retention(RetentionPolicy.RUNTIME) -public @interface SnapshotHandler {} diff --git a/java-support/src/main/java/io/cloudstate/javasupport/crudtwo/package-info.java b/java-support/src/main/java/io/cloudstate/javasupport/crudtwo/package-info.java deleted file mode 100644 index 7b934dcc4..000000000 --- a/java-support/src/main/java/io/cloudstate/javasupport/crudtwo/package-info.java +++ /dev/null @@ -1,14 +0,0 @@ -/** - * CRUD support. - * - *

Event sourced entities can be annotated with the {@link - * io.cloudstate.javasupport.crud.CrudEntity @CrudEntity} annotation, and supply command handlers - * using the {@link io.cloudstate.javasupport.crud.CommandHandler @CommandHandler} annotation. - * - *

In addition, {@link io.cloudstate.javasupport.crud.EventHandler @EventHandler} annotated - * methods should be defined to handle events, and {@link - * io.cloudstate.javasupport.crud.Snapshot @Snapshot} and {@link - * io.cloudstate.javasupport.crud.SnapshotHandler @SnapshotHandler} annotated methods should be - * defined to produce and handle snapshots respectively. - */ -package io.cloudstate.javasupport.crudtwo; diff --git a/java-support/src/main/scala/io/cloudstate/javasupport/CloudStateRunner.scala b/java-support/src/main/scala/io/cloudstate/javasupport/CloudStateRunner.scala index eb0e2bae7..9f1a54322 100644 --- a/java-support/src/main/scala/io/cloudstate/javasupport/CloudStateRunner.scala +++ b/java-support/src/main/scala/io/cloudstate/javasupport/CloudStateRunner.scala @@ -28,7 +28,9 @@ import com.google.protobuf.Descriptors import io.cloudstate.javasupport.impl.eventsourced.{EventSourcedImpl, EventSourcedStatefulService} import io.cloudstate.javasupport.impl.{EntityDiscoveryImpl, ResolvedServiceCallFactory, ResolvedServiceMethod} import io.cloudstate.javasupport.impl.crdt.{CrdtImpl, CrdtStatefulService} +import io.cloudstate.javasupport.impl.crud.{CrudImpl, CrudStatefulService} import io.cloudstate.protocol.crdt.CrdtHandler +import io.cloudstate.protocol.crud.CrudHandler import io.cloudstate.protocol.entity.EntityDiscoveryHandler import io.cloudstate.protocol.event_sourced.EventSourcedHandler @@ -100,6 +102,11 @@ final class CloudStateRunner private[this] (_system: ActorSystem, services: Map[ val crdtImpl = new CrdtImpl(system, crdtServices, rootContext) route orElse CrdtHandler.partial(crdtImpl) + case (route, (serviceClass, crudServices: Map[String, CrudStatefulService] @unchecked)) + if serviceClass == classOf[CrudStatefulService] => + val crudImpl = new CrudImpl(system, crudServices, rootContext, configuration) + route orElse CrudHandler.partial(crudImpl) + case (_, (serviceClass, _)) => sys.error(s"Unknown StatefulService: $serviceClass") } diff --git a/java-support/src/main/scala/io/cloudstate/javasupport/impl/crudtwo/AnnotationBasedCrudSupport.scala b/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/AnnotationBasedCrudSupport.scala similarity index 99% rename from java-support/src/main/scala/io/cloudstate/javasupport/impl/crudtwo/AnnotationBasedCrudSupport.scala rename to java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/AnnotationBasedCrudSupport.scala index c6967949d..9f5e0ab8a 100644 --- a/java-support/src/main/scala/io/cloudstate/javasupport/impl/crudtwo/AnnotationBasedCrudSupport.scala +++ b/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/AnnotationBasedCrudSupport.scala @@ -1,11 +1,11 @@ -package io.cloudstate.javasupport.impl.crudtwo +package io.cloudstate.javasupport.impl.crud import java.lang.reflect.{Constructor, InvocationTargetException, Method} import java.util.Optional import com.google.protobuf.{Descriptors, Any => JavaPbAny} import io.cloudstate.javasupport.ServiceCallFactory -import io.cloudstate.javasupport.crudtwo.{ +import io.cloudstate.javasupport.crud.{ CommandContext, CommandHandler, CrudContext, @@ -40,9 +40,6 @@ private[impl] class AnnotationBasedCrudSupport( private val behavior = EventBehaviorReflection(entityClass, resolvedMethods) - override def create(context: CrudContext): CrudEntityHandler = - new EntityHandler(context) - private val constructor: CrudEntityCreationContext => AnyRef = factory.getOrElse { entityClass.getConstructors match { case Array(single) => @@ -52,6 +49,9 @@ private[impl] class AnnotationBasedCrudSupport( } } + override def create(context: CrudContext): CrudEntityHandler = + new EntityHandler(context) + private class EntityHandler(context: CrudContext) extends CrudEntityHandler { private val entity = { constructor(new DelegatingCrudContext(context) with CrudEntityCreationContext { diff --git a/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/CrudImpl.scala b/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/CrudImpl.scala new file mode 100644 index 000000000..ae866b2f3 --- /dev/null +++ b/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/CrudImpl.scala @@ -0,0 +1,210 @@ +/* + * Copyright 2020 Lightbend Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.cloudstate.javasupport.impl.crud + +import java.util.Optional + +import akka.actor.ActorSystem +import com.google.protobuf.any.{Any => ScalaPbAny} +import com.google.protobuf.{Descriptors, Any => JavaPbAny} +import io.cloudstate.javasupport.CloudStateRunner.Configuration +import io.cloudstate.javasupport.crud._ +import io.cloudstate.javasupport.impl._ +import io.cloudstate.javasupport.{Context, ServiceCallFactory, StatefulService} +import io.cloudstate.protocol.crud._ + +import scala.concurrent.Future + +final class CrudStatefulService(val factory: CrudEntityFactory, + override val descriptor: Descriptors.ServiceDescriptor, + val anySupport: AnySupport, + override val persistenceId: String, + val snapshotEvery: Int) + extends StatefulService { + + override def resolvedMethods: Option[Map[String, ResolvedServiceMethod[_, _]]] = + factory match { + case resolved: ResolvedEntityFactory => Some(resolved.resolvedMethods) + case _ => None + } + + override final val entityType = Crud.name + + final def withSnapshotEvery(snapshotEvery: Int): CrudStatefulService = + if (snapshotEvery != this.snapshotEvery) + new CrudStatefulService(this.factory, this.descriptor, this.anySupport, this.persistenceId, snapshotEvery) + else + this +} + +final class CrudImpl(_system: ActorSystem, + _services: Map[String, CrudStatefulService], + rootContext: Context, + configuration: Configuration) + extends Crud { + + private final val system = _system + private final implicit val ec = system.dispatcher + private final val services = _services.iterator.toMap + + private final var serviceInit = false + private final var handlerInit = false + private final var service: CrudStatefulService = _ + private final var handler: CrudEntityHandler = _ + + override def create(command: CrudCommand): Future[CrudReplyOut] = { + maybeInitService(command.serviceName) + maybeInitHandler(command.entityId) + + Future.unit + .map { _ => + command.state.map { state => + // Not sure about the best way to push the state to the user function + // There are two options here. The first is using an annotation which is called on handler.handleState. + // handler.handleState will use a new special context called StateContext (will be implemented). + // The other option is to pass the state in the CommandContext and use emit or something else to publish the new state + handler.handleState(ScalaPbAny.toJavaProto(state.payload.get), new SnapshotContextImpl(command.entityId, 0)) + } + val cmd = ScalaPbAny.toJavaProto(command.payload.get) //FIXME payload empty? + val context = + new CommandContextImpl(command.entityId, 0, command.name, command.id, handler, service.anySupport) + val reply = handleCommand(context, cmd) + val clientAction = context.createClientAction(reply, false) + if (!context.hasError) { + CrudReplyOut( + CrudReplyOut.Message.Reply( + CrudReply( + command.id, + clientAction, + context.sideEffects, + Some(context.events(0)) // FIXME deal with the events? + ) + ) + ) + } else { + CrudReplyOut( + CrudReplyOut.Message.Reply( + CrudReply( + commandId = command.id, + clientAction = clientAction, + state = Some(context.events(0)) // FIXME deal with the events? + ) + ) + ) + } + } + } + + override def fetch(command: CrudCommand): Future[CrudReplyOut] = { + maybeInitService(command.serviceName) + maybeInitHandler(command.entityId) + + Future.unit + .map { _ => + command.state.map { state => + handler.handleState(ScalaPbAny.toJavaProto(state.payload.get), new SnapshotContextImpl(command.entityId, 0)) + } + + val cmd = ScalaPbAny.toJavaProto(command.payload.get) //FIXME payload empty? + val context = + new CommandContextImpl(command.entityId, 0, command.name, command.id, handler, service.anySupport) + val reply = handleCommand(context, cmd) + val clientAction = context.createClientAction(reply, false) + if (!context.hasError) { + CrudReplyOut( + CrudReplyOut.Message.Reply( + CrudReply( + command.id, + clientAction, + context.sideEffects + ) + ) + ) + } else { + CrudReplyOut( + CrudReplyOut.Message.Reply( + CrudReply( + commandId = command.id, + clientAction = clientAction + ) + ) + ) + } + } + } + + override def save(command: CrudCommand): Future[CrudReplyOut] = ??? + + override def delete(command: CrudCommand): Future[CrudReplyOut] = ??? + + override def fetchAll(command: CrudCommand): Future[CrudReplyOut] = ??? + + private def maybeInitService(serviceName: String): Unit = + if (!serviceInit) { + service = services.getOrElse(serviceName, throw new RuntimeException(s"Service not found: $serviceName")) + serviceInit = true + } + + private def maybeInitHandler(entityId: String): Unit = + if (!handlerInit) { + handlerInit = true + handler = service.factory.create(new CrudContextImpl(entityId)) + } + + private def handleCommand(context: CommandContextImpl, command: JavaPbAny): Optional[JavaPbAny] = + try { + handler.handleCommand(command, context) + } catch { + case FailInvoked => + Optional.empty[JavaPbAny]() + } finally { + context.deactivate() + } + + trait AbstractContext extends CrudContext { + override def serviceCallFactory(): ServiceCallFactory = rootContext.serviceCallFactory() + } + + class CommandContextImpl(override val entityId: String, + override val sequenceNumber: Long, + override val commandName: String, + override val commandId: Long, + val handler: CrudEntityHandler, + val anySupport: AnySupport) + extends CommandContext + with AbstractContext + with AbstractClientActionContext + with AbstractEffectContext + with ActivatableContext { + + final var events: Vector[ScalaPbAny] = Vector.empty + + override def emit(event: AnyRef): Unit = { + val encoded = anySupport.encodeScala(event) + // Snapshotting should be done!! + handler.handleState(ScalaPbAny.toJavaProto(encoded), new SnapshotContextImpl(entityId, 0)) + events :+= encoded + } + } + + class CrudContextImpl(override final val entityId: String) extends CrudContext with AbstractContext + + class SnapshotContextImpl(override final val entityId: String, override final val sequenceNumber: Long) + extends SnapshotContext + with AbstractContext + + //class StateContextImpl(override final val entityId: String) extends CrudContext with AbstractContext with StateContext +} diff --git a/java-support/src/main/scala/io/cloudstate/javasupport/impl/crudone/AnnotationBasedCrudSupport.scala b/java-support/src/main/scala/io/cloudstate/javasupport/impl/crudone/AnnotationBasedCrudSupport.scala deleted file mode 100644 index 7fe55898b..000000000 --- a/java-support/src/main/scala/io/cloudstate/javasupport/impl/crudone/AnnotationBasedCrudSupport.scala +++ /dev/null @@ -1,348 +0,0 @@ -package io.cloudstate.javasupport.impl.crudone - -import java.lang.reflect.{Constructor, InvocationTargetException, Method} -import java.util.Optional - -import com.google.protobuf.{Descriptors, Any => JavaPbAny} -import io.cloudstate.javasupport.ServiceCallFactory -import io.cloudstate.javasupport.crud.{CrudContext, CrudEntityCreationContext, CrudEntityHandler, CrudEventContext} -import io.cloudstate.javasupport.crud._ -import io.cloudstate.javasupport.impl.ReflectionHelper.{InvocationContext, MainArgumentParameterHandler} -import io.cloudstate.javasupport.impl.{AnySupport, ReflectionHelper, ResolvedEntityFactory, ResolvedServiceMethod} - -import scala.collection.concurrent.TrieMap - -/** - * Annotation based implementation of the [[CrudEntityFactory]]. - */ -private[impl] class AnnotationBasedCrudSupport( - entityClass: Class[_], - anySupport: AnySupport, - override val resolvedMethods: Map[String, ResolvedServiceMethod[_, _]], - factory: Option[CrudEntityCreationContext => AnyRef] = None -) extends CrudEntityFactory - with ResolvedEntityFactory { - - def this(entityClass: Class[_], anySupport: AnySupport, serviceDescriptor: Descriptors.ServiceDescriptor) = - this(entityClass, anySupport, anySupport.resolveServiceDescriptor(serviceDescriptor)) - - private val behavior = EventBehaviorReflection(entityClass, resolvedMethods) - - override def create(context: CrudContext): CrudEntityHandler = - new EntityHandler(context) - - private val constructor: CrudEntityCreationContext => AnyRef = factory.getOrElse { - entityClass.getConstructors match { - case Array(single) => - new EntityConstructorInvoker(ReflectionHelper.ensureAccessible(single)) - case _ => - throw new RuntimeException(s"Only a single constructor is allowed on CRUD entities: $entityClass") - } - } - - private class EntityHandler(context: CrudContext) extends CrudEntityHandler { - private val entity = { - constructor(new DelegatingCrudContext(context) with CrudEntityCreationContext { - override def entityId(): String = context.entityId() - }) - } - - override def handleCommand(command: JavaPbAny, context: CommandContext): Optional[JavaPbAny] = unwrap { - behavior.commandHandlers.get(context.commandName()).map { handler => - handler.invoke(entity, command, context) - } getOrElse { - throw new RuntimeException( - s"No command handler found for command [${context.commandName()}] on $behaviorsString" - ) - } - } - - override def handleCreateCommand(command: JavaPbAny, context: CommandContext): Optional[JavaPbAny] = unwrap { - behavior.commandHandlers.get(context.commandName()).map { handler => - handler.invoke(entity, command, context) - } getOrElse { - throw new RuntimeException( - s"No command handler found for command [${context.commandName()}] on $behaviorsString" - ) - } - } - override def handleFetchCommand(command: JavaPbAny, context: CommandContext): Optional[JavaPbAny] = unwrap { - behavior.commandHandlers.get(context.commandName()).map { handler => - handler.invoke(entity, command, context) - } getOrElse { - throw new RuntimeException( - s"No command handler found for command [${context.commandName()}] on $behaviorsString" - ) - } - } - - override def handleUpdateCommand(command: JavaPbAny, context: CommandContext): Optional[JavaPbAny] = unwrap { - behavior.commandHandlers.get(context.commandName()).map { handler => - handler.invoke(entity, command, context) - } getOrElse { - throw new RuntimeException( - s"No command handler found for command [${context.commandName()}] on $behaviorsString" - ) - } - } - - override def handleDeleteCommand(command: JavaPbAny, context: CommandContext): Optional[JavaPbAny] = unwrap { - behavior.commandHandlers.get(context.commandName()).map { handler => - handler.invoke(entity, command, context) - } getOrElse { - throw new RuntimeException( - s"No command handler found for command [${context.commandName()}] on $behaviorsString" - ) - } - } - - override def handleState(anyState: JavaPbAny, context: SnapshotContext): Unit = unwrap { - val state = anySupport.decode(anyState).asInstanceOf[AnyRef] - - behavior.getCachedSnapshotHandlerForClass(state.getClass) match { - case Some(handler) => - val ctx = new DelegatingCrudContext(context) with SnapshotContext { - override def sequenceNumber(): Long = context.sequenceNumber() - } - handler.invoke(entity, state, ctx) - case None => - throw new RuntimeException( - s"No state handler found for state ${state.getClass} on $behaviorsString" - ) - } - } - - override def snapshot(context: SnapshotContext): Optional[JavaPbAny] = unwrap { - behavior.snapshotInvoker.map { invoker => - invoker.invoke(entity, context) - } match { - case Some(invoker) => Optional.ofNullable(anySupport.encodeJava(invoker)) - case None => Optional.empty() - } - } - - private def unwrap[T](block: => T): T = - try { - block - } catch { - case ite: InvocationTargetException if ite.getCause != null => - throw ite.getCause - } - - private def behaviorsString = entity.getClass.toString - } - - private abstract class DelegatingCrudContext(delegate: CrudContext) extends CrudContext { - override def entityId(): String = delegate.entityId() - override def serviceCallFactory(): ServiceCallFactory = delegate.serviceCallFactory() - } -} - -private class EventBehaviorReflection( - eventHandlers: Map[Class[_], EventHandlerInvoker], - val commandHandlers: Map[String, ReflectionHelper.CommandHandlerInvoker[CommandContext]], - snapshotHandlers: Map[Class[_], SnapshotHandlerInvoker], - val snapshotInvoker: Option[SnapshotInvoker] -) { - - /** - * We use a cache in addition to the info we've discovered by reflection so that an event handler can be declared - * for a superclass of an event. - */ - private val eventHandlerCache = TrieMap.empty[Class[_], Option[EventHandlerInvoker]] - private val snapshotHandlerCache = TrieMap.empty[Class[_], Option[SnapshotHandlerInvoker]] - - def getCachedEventHandlerForClass(clazz: Class[_]): Option[EventHandlerInvoker] = - eventHandlerCache.getOrElseUpdate(clazz, getHandlerForClass(eventHandlers)(clazz)) - - def getCachedSnapshotHandlerForClass(clazz: Class[_]): Option[SnapshotHandlerInvoker] = - snapshotHandlerCache.getOrElseUpdate(clazz, getHandlerForClass(snapshotHandlers)(clazz)) - - private def getHandlerForClass[T](handlers: Map[Class[_], T])(clazz: Class[_]): Option[T] = - handlers.get(clazz) match { - case some @ Some(_) => some - case None => - clazz.getInterfaces.collectFirst(Function.unlift(getHandlerForClass(handlers))) match { - case some @ Some(_) => some - case None if clazz.getSuperclass != null => getHandlerForClass(handlers)(clazz.getSuperclass) - case None => None - } - } - -} - -private object EventBehaviorReflection { - def apply(behaviorClass: Class[_], - serviceMethods: Map[String, ResolvedServiceMethod[_, _]]): EventBehaviorReflection = { - - val allMethods = ReflectionHelper.getAllDeclaredMethods(behaviorClass) - val eventHandlers = allMethods - .filter(_.getAnnotation(classOf[EventHandler]) != null) - .map { method => - new EventHandlerInvoker(ReflectionHelper.ensureAccessible(method)) - } - .groupBy(_.eventClass) - .map { - case (eventClass, Seq(invoker)) => (eventClass: Any) -> invoker - case (clazz, many) => - throw new RuntimeException( - s"Multiple methods found for handling event of type $clazz: ${many.map(_.method.getName)}" - ) - } - .asInstanceOf[Map[Class[_], EventHandlerInvoker]] - - val commandHandlers = allMethods - .filter(_.getAnnotation(classOf[CommandHandler]) != null) - .map { method => - val annotation = method.getAnnotation(classOf[CommandHandler]) - val name: String = if (annotation.name().isEmpty) { - ReflectionHelper.getCapitalizedName(method) - } else annotation.name() - - val serviceMethod = serviceMethods.getOrElse(name, { - throw new RuntimeException( - s"Command handler method ${method.getName} for command $name found, but the service has no command by that name." - ) - }) - - new ReflectionHelper.CommandHandlerInvoker[CommandContext](ReflectionHelper.ensureAccessible(method), - serviceMethod) - } - .groupBy(_.serviceMethod.name) - .map { - case (commandName, Seq(invoker)) => commandName -> invoker - case (commandName, many) => - throw new RuntimeException( - s"Multiple methods found for handling command of name $commandName: ${many.map(_.method.getName)}" - ) - } - - val snapshotHandlers = allMethods - .filter(_.getAnnotation(classOf[SnapshotHandler]) != null) - .map { method => - new SnapshotHandlerInvoker(ReflectionHelper.ensureAccessible(method)) - } - .groupBy(_.snapshotClass) - .map { - case (snapshotClass, Seq(invoker)) => (snapshotClass: Any) -> invoker - case (clazz, many) => - throw new RuntimeException( - s"Multiple methods found for handling snapshot of type $clazz: ${many.map(_.method.getName)}" - ) - } - .asInstanceOf[Map[Class[_], SnapshotHandlerInvoker]] - - val snapshotInvoker = allMethods - .filter(_.getAnnotation(classOf[Snapshot]) != null) - .map { method => - new SnapshotInvoker(ReflectionHelper.ensureAccessible(method)) - } match { - case Seq() => None - case Seq(single) => - Some(single) - case _ => - throw new RuntimeException(s"Multiple snapshoting methods found on behavior $behaviorClass") - } - - ReflectionHelper.validateNoBadMethods( - allMethods, - classOf[CrudEntity], - Set(classOf[EventHandler], classOf[CommandHandler], classOf[SnapshotHandler], classOf[Snapshot]) - ) - - new EventBehaviorReflection(eventHandlers, commandHandlers, snapshotHandlers, snapshotInvoker) - } -} - -private class EntityConstructorInvoker(constructor: Constructor[_]) extends (CrudEntityCreationContext => AnyRef) { - private val parameters = ReflectionHelper.getParameterHandlers[CrudEntityCreationContext](constructor)() - parameters.foreach { - case MainArgumentParameterHandler(clazz) => - throw new RuntimeException(s"Don't know how to handle argument of type $clazz in constructor") - case _ => - } - - def apply(context: CrudEntityCreationContext): AnyRef = { - val ctx = InvocationContext("", context) - constructor.newInstance(parameters.map(_.apply(ctx)): _*).asInstanceOf[AnyRef] - } -} - -private class EventHandlerInvoker(val method: Method) { - - private val annotation = method.getAnnotation(classOf[EventHandler]) - - private val parameters = ReflectionHelper.getParameterHandlers[CrudEventContext](method)() - - private def annotationEventClass = annotation.eventClass() match { - case obj if obj == classOf[Object] => None - case clazz => Some(clazz) - } - - // Verify that there is at most one event handler - val eventClass: Class[_] = parameters.collect { - case MainArgumentParameterHandler(clazz) => clazz - } match { - case Array() => annotationEventClass.getOrElse(classOf[Object]) - case Array(handlerClass) => - annotationEventClass match { - case None => handlerClass - case Some(annotated) if handlerClass.isAssignableFrom(annotated) || annotated.isInterface => - annotated - case Some(nonAssignable) => - throw new RuntimeException( - s"EventHandler method $method has defined an eventHandler class $nonAssignable that can never be assignable from it's parameter $handlerClass" - ) - } - case other => - throw new RuntimeException( - s"EventHandler method $method must defined at most one non context parameter to handle events, the parameters defined were: ${other - .mkString(",")}" - ) - } - - def invoke(obj: AnyRef, event: AnyRef, context: CrudEventContext): Unit = { - val ctx = InvocationContext(event, context) - method.invoke(obj, parameters.map(_.apply(ctx)): _*) - } -} - -private class SnapshotHandlerInvoker(val method: Method) { - private val parameters = ReflectionHelper.getParameterHandlers[SnapshotContext](method)() - - // Verify that there is at most one event handler - val snapshotClass: Class[_] = parameters.collect { - case MainArgumentParameterHandler(clazz) => clazz - } match { - case Array(handlerClass) => handlerClass - case other => - throw new RuntimeException( - s"SnapshotHandler method $method must defined at most one non context parameter to handle snapshots, the parameters defined were: ${other - .mkString(",")}" - ) - } - - def invoke(obj: AnyRef, snapshot: AnyRef, context: SnapshotContext): Unit = { - val ctx = InvocationContext(snapshot, context) - method.invoke(obj, parameters.map(_.apply(ctx)): _*) - } -} - -private class SnapshotInvoker(val method: Method) { - - private val parameters = ReflectionHelper.getParameterHandlers[SnapshotContext](method)() - - parameters.foreach { - case MainArgumentParameterHandler(clazz) => - throw new RuntimeException( - s"Don't know how to handle argument of type $clazz in snapshot method: " + method.getName - ) - case _ => - } - - def invoke(obj: AnyRef, context: SnapshotContext): AnyRef = { - val ctx = InvocationContext("", context) - method.invoke(obj, parameters.map(_.apply(ctx)): _*) - } - -} diff --git a/java-support/src/main/scala/io/cloudstate/javasupport/impl/crudone/CrudImpl.scala b/java-support/src/main/scala/io/cloudstate/javasupport/impl/crudone/CrudImpl.scala deleted file mode 100644 index 3d24fee74..000000000 --- a/java-support/src/main/scala/io/cloudstate/javasupport/impl/crudone/CrudImpl.scala +++ /dev/null @@ -1,378 +0,0 @@ -/* - * Copyright 2020 Lightbend Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.cloudstate.javasupport.impl.crudone - -import java.util.Optional - -import akka.NotUsed -import akka.actor.ActorSystem -import akka.stream.scaladsl.{Flow, Source} -import com.google.protobuf.any.{Any => ScalaPbAny} -import com.google.protobuf.{Descriptors, Any => JavaPbAny} -import io.cloudstate.javasupport.CloudStateRunner.Configuration -import io.cloudstate.javasupport.crud._ -import io.cloudstate.javasupport.impl._ -import io.cloudstate.javasupport.{Context, ServiceCallFactory, StatefulService} -import io.cloudstate.protocol.crud_one.CrudStreamIn.Message.{ - Create => InCreate, - Empty => InEmpty, - Event => InEvent, - Fetch => InFetch, - Init => InInit -} -import io.cloudstate.protocol.crud_one.{CrudInit, CrudReply, CrudStreamIn, CrudStreamOut} -import io.cloudstate.protocol.crud_one.CrudStreamOut.Message.{Reply => OutReply} -import io.cloudstate.protocol.crud_one.CrudOne - -final class CrudStatefulService(val factory: CrudEntityFactory, - override val descriptor: Descriptors.ServiceDescriptor, - val anySupport: AnySupport, - override val persistenceId: String, - val snapshotEvery: Int) - extends StatefulService { - - override def resolvedMethods: Option[Map[String, ResolvedServiceMethod[_, _]]] = - factory match { - case resolved: ResolvedEntityFactory => Some(resolved.resolvedMethods) - case _ => None - } - - override final val entityType = CrudOne.name - - final def withSnapshotEvery(snapshotEvery: Int): CrudStatefulService = - if (snapshotEvery != this.snapshotEvery) - new CrudStatefulService(this.factory, this.descriptor, this.anySupport, this.persistenceId, snapshotEvery) - else - this -} - -final class CrudImpl(_system: ActorSystem, - _services: Map[String, CrudStatefulService], - rootContext: Context, - configuration: Configuration) - extends CrudOne { - // how to push the snapshot state to the user function? handleState? - // should snapshot be exposed to the user function? - // How to do snapshot here? - // how to deal with snapshot and events by handleState. Some kind of mapping? - // how to deal with emitted events? handleState is called now, is that right? - - private final val system = _system - private final val services = _services.iterator - .map({ - case (name, crudss) => - // FIXME overlay configuration provided by _system - (name, if (crudss.snapshotEvery == 0) crudss.withSnapshotEvery(configuration.snapshotEvery) else crudss) - }) - .toMap - - override def handle(in: Source[CrudStreamIn, NotUsed]): Source[CrudStreamOut, NotUsed] = - in.prefixAndTail(1) - .flatMapConcat { - case (Seq(CrudStreamIn(InInit(init), _)), source) => - source.via(runEntityCreate(init)) - case _ => - // todo better error - throw new RuntimeException("Expected Init message") - } - .recover { - case e => - // FIXME translate to failure message - throw e - } - - private def runEntityCreate(init: CrudInit): Flow[CrudStreamIn, CrudStreamOut, NotUsed] = { - val service = - services.getOrElse(init.serviceName, throw new RuntimeException(s"Service not found: ${init.serviceName}")) - val handler: CrudEntityHandler = service.factory.create(new CrudContextImpl(init.entityId)) - val entityId = init.entityId - - val startingSequenceNumber = (for { - snapshot <- init.snapshot - any <- snapshot.snapshot - } yield { - val snapshotSequence = snapshot.snapshotSequence - val context = new CrudEventContextImpl(entityId, snapshotSequence) - handler.handleState(ScalaPbAny.toJavaProto(any), context) - snapshotSequence - }).getOrElse(0L) - - Flow[CrudStreamIn] - .map(_.message) - .scan[(Long, Option[CrudStreamOut.Message])]((startingSequenceNumber, None)) { - case (_, InEvent(event)) => - val context = new CrudEventContextImpl(entityId, event.sequence) - val ev = ScalaPbAny.toJavaProto(event.payload.get) // FIXME empty? - handler.handleState(ev, context) - (event.sequence, None) - - case ((sequence, _), InCreate(command)) => - if (entityId != command.entityId) - throw new IllegalStateException("Receiving CRUD entity is not the intended recipient of command") - val cmd = ScalaPbAny.toJavaProto(command.payload.get) - val context = new CommandContextImpl(entityId, - sequence, - command.name, - command.id, - service.anySupport, - handler, - service.snapshotEvery) - - val reply = try { - handler.handleCommand(cmd, context) // FIXME is this allowed to throw - } catch { - case FailInvoked => Optional.empty[JavaPbAny]() - // Ignore, error already captured - } finally { - context.deactivate() // Very important! - } - - val clientAction = context.createClientAction(reply, false) - if (!context.hasError) { - val endSequenceNumber = sequence + context.events.size - - val snapshot = - if (context.performSnapshot) { - val s = handler.snapshot(new SnapshotContext with AbstractContext { - override def entityId: String = entityId - override def sequenceNumber: Long = endSequenceNumber - }) - if (s.isPresent) Option(ScalaPbAny.fromJavaProto(s.get)) else None - } else None - - (endSequenceNumber, - Some( - OutReply( - CrudReply( - command.id, - clientAction, - context.sideEffects, - context.events, - snapshot - ) - ) - )) - } else { - (sequence, - Some( - OutReply( - CrudReply( - commandId = command.id, - clientAction = clientAction - ) - ) - )) - } - - case (_, InInit(i)) => - throw new IllegalStateException("Entity already inited") - - case (_, InEmpty) => - throw new IllegalStateException("Received empty/unknown message") - } - .collect { - case (_, Some(message)) => CrudStreamOut(message) - } - } - private def runEntityOld(init: CrudInit): Flow[CrudStreamIn, CrudStreamOut, NotUsed] = { - val service = - services.getOrElse(init.serviceName, throw new RuntimeException(s"Service not found: ${init.serviceName}")) - val handler: CrudEntityHandler = service.factory.create(new CrudContextImpl(init.entityId)) - val entityId = init.entityId - - val startingSequenceNumber = (for { - snapshot <- init.snapshot - any <- snapshot.snapshot - } yield { - val snapshotSequence = snapshot.snapshotSequence - val context = new CrudEventContextImpl(entityId, snapshotSequence) - handler.handleState(ScalaPbAny.toJavaProto(any), context) - snapshotSequence - }).getOrElse(0L) - - Flow[CrudStreamIn] - .map(_.message) - .scan[(Long, Option[CrudStreamOut.Message])]((startingSequenceNumber, None)) { - case (_, InEvent(event)) => - val context = new CrudEventContextImpl(entityId, event.sequence) - val ev = ScalaPbAny.toJavaProto(event.payload.get) // FIXME empty? - handler.handleState(ev, context) - (event.sequence, None) - - case ((sequence, _), InCreate(command)) => - if (entityId != command.entityId) - throw new IllegalStateException("Receiving CRUD entity is not the intended recipient of command") - val cmd = ScalaPbAny.toJavaProto(command.payload.get) - val context = new CommandContextImpl(entityId, - sequence, - command.name, - command.id, - service.anySupport, - handler, - service.snapshotEvery) - - val reply = try { - handler.handleCommand(cmd, context) // FIXME is this allowed to throw - } catch { - case FailInvoked => Optional.empty[JavaPbAny]() - // Ignore, error already captured - } finally { - context.deactivate() // Very important! - } - - val clientAction = context.createClientAction(reply, false) - if (!context.hasError) { - val endSequenceNumber = sequence + context.events.size - - val snapshot = - if (context.performSnapshot) { - val s = handler.snapshot(new SnapshotContext with AbstractContext { - override def entityId: String = entityId - override def sequenceNumber: Long = endSequenceNumber - }) - if (s.isPresent) Option(ScalaPbAny.fromJavaProto(s.get)) else None - } else None - - (endSequenceNumber, - Some( - OutReply( - CrudReply( - command.id, - clientAction, - context.sideEffects, - context.events, - snapshot - ) - ) - )) - } else { - (sequence, - Some( - OutReply( - CrudReply( - commandId = command.id, - clientAction = clientAction - ) - ) - )) - } - - case ((sequence, _), InFetch(command)) => - if (entityId != command.entityId) - throw new IllegalStateException("Receiving CRUD entity is not the intended recipient of command") - val cmd = ScalaPbAny.toJavaProto(command.payload.get) - val context = new CommandContextImpl(entityId, - sequence, - command.name, - command.id, - service.anySupport, - handler, - service.snapshotEvery) - - val reply = try { - handler.handleCommand(cmd, context) // FIXME is this allowed to throw - } catch { - case FailInvoked => Optional.empty[JavaPbAny]() - // Ignore, error already captured - } finally { - context.deactivate() // Very important! - } - - val clientAction = context.createClientAction(reply, false) - if (!context.hasError) { - val endSequenceNumber = sequence + context.events.size - - val snapshot = - if (context.performSnapshot) { - val s = handler.snapshot(new SnapshotContext with AbstractContext { - override def entityId: String = entityId - override def sequenceNumber: Long = endSequenceNumber - }) - if (s.isPresent) Option(ScalaPbAny.fromJavaProto(s.get)) else None - } else None - - (endSequenceNumber, - Some( - OutReply( - CrudReply( - command.id, - clientAction, - context.sideEffects, - context.events, - snapshot - ) - ) - )) - } else { - (sequence, - Some( - OutReply( - CrudReply( - commandId = command.id, - clientAction = clientAction - ) - ) - )) - } - case (_, InInit(i)) => - throw new IllegalStateException("Entity already inited") - case (_, InEmpty) => - throw new IllegalStateException("Received empty/unknown message") - - //case ((sequence, _), aOrB @ (InCreate(_) | InFetch(_))) => ??? - } - .collect { - case (_, Some(message)) => CrudStreamOut(message) - } - } - - trait AbstractContext extends CrudContext { - override def serviceCallFactory(): ServiceCallFactory = rootContext.serviceCallFactory() - } - - class CommandContextImpl(override val entityId: String, - override val sequenceNumber: Long, - override val commandName: String, - override val commandId: Long, - val anySupport: AnySupport, - val handler: CrudEntityHandler, - val snapshotEvery: Int) - extends CommandContext - with AbstractContext - with AbstractClientActionContext - with AbstractEffectContext - with ActivatableContext { - - final var events: Vector[ScalaPbAny] = Vector.empty - final var performSnapshot: Boolean = false - - override def emit(event: AnyRef): Unit = { - checkActive() - val encoded = anySupport.encodeScala(event) - val nextSequenceNumber = sequenceNumber + events.size + 1 - handler.handleState(ScalaPbAny.toJavaProto(encoded), new CrudEventContextImpl(entityId, nextSequenceNumber)) - events :+= encoded - performSnapshot = (snapshotEvery > 0) && (performSnapshot || (nextSequenceNumber % snapshotEvery == 0)) - } - } - - // FIXME add final val subEntityId: String - class CrudContextImpl(override final val entityId: String) extends CrudContext with AbstractContext - class CrudEventContextImpl(entityId: String, override final val sequenceNumber: Long) - extends CrudContextImpl(entityId) - with CrudEventContext - with SnapshotContext -} diff --git a/java-support/src/main/scala/io/cloudstate/javasupport/impl/crudtwo/CrudImpl.scala b/java-support/src/main/scala/io/cloudstate/javasupport/impl/crudtwo/CrudImpl.scala deleted file mode 100644 index df5d19713..000000000 --- a/java-support/src/main/scala/io/cloudstate/javasupport/impl/crudtwo/CrudImpl.scala +++ /dev/null @@ -1,150 +0,0 @@ -/* - * Copyright 2020 Lightbend Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.cloudstate.javasupport.impl.crudtwo - -import akka.actor.ActorSystem -import com.google.protobuf.Descriptors -import com.google.protobuf.any.{Any => ScalaPbAny} -import com.google.protobuf.{Any => JavaPbAny} -import io.cloudstate.javasupport.CloudStateRunner.Configuration -import io.cloudstate.javasupport.crudtwo._ -import io.cloudstate.javasupport.impl._ -import io.cloudstate.javasupport.{Context, ServiceCallFactory, StatefulService} -import io.cloudstate.protocol.crud_two._ - -import scala.concurrent.Future - -final class CrudStatefulService(val factory: CrudEntityFactory, - override val descriptor: Descriptors.ServiceDescriptor, - val anySupport: AnySupport, - override val persistenceId: String, - val snapshotEvery: Int) - extends StatefulService { - - override def resolvedMethods: Option[Map[String, ResolvedServiceMethod[_, _]]] = - factory match { - case resolved: ResolvedEntityFactory => Some(resolved.resolvedMethods) - case _ => None - } - - override final val entityType = CrudTwo.name - - final def withSnapshotEvery(snapshotEvery: Int): CrudStatefulService = - if (snapshotEvery != this.snapshotEvery) - new CrudStatefulService(this.factory, this.descriptor, this.anySupport, this.persistenceId, snapshotEvery) - else - this -} - -final class CrudImpl(_system: ActorSystem, - _services: Map[String, CrudStatefulService], - rootContext: Context, - configuration: Configuration) - extends CrudTwo { - // how to deal with snapshot and events by handleState. Some kind of mapping? - // how to deal with emitted events? handleState is called now, is that right? - // how to push the snapshot state to the user function? handleState? - // should snapshot be exposed to the user function? - // How to do snapshot here? - - private final val system = _system - private final implicit val ec = system.dispatcher - private final val services = _services.iterator.toMap - - // One option for accessing the service name and the entityId could be to pass it in the CrudCommand. - private val serviceName = "serviceName" // FIXME where to get the service name from? - private val entityId = "entityId" // FIXME entityId can be extract from command, where to get entityId from when creating the CrudImpl? - private final val service = - services.getOrElse(serviceName, throw new RuntimeException(s"Service not found: $serviceName")) - private var handler - : CrudEntityHandler = service.factory.create(new CrudContextImpl(entityId)) // FIXME how to create it? - - override def create(command: CrudCommand): Future[CrudReplies] = - Future.unit - .map { _ => - val cmd = ScalaPbAny.toJavaProto(command.payload.get) //FIXME payload empty? - val state = ScalaPbAny.toJavaProto(command.state.get.payload.get) // FIXME state empty? FIXME payload empty? - val context = - new CommandContextImpl(command.entityId, 0, command.name, command.id, state, handler, service.anySupport) - val reply = handler.handleCommand(cmd, context) - val clientAction = context.createClientAction(reply, false) - CrudReplies( - CrudReplies.Message.Reply( - CrudReply( - command.id, - clientAction, - context.sideEffects, - Some(context.events(0)) // FIXME deal with the events? - ) - ) - ) - } - - override def fetch(command: CrudCommand): Future[CrudFetchReplies] = - Future.unit - .map { _ => - val cmd = ScalaPbAny.toJavaProto(command.payload.get) //FIXME payload empty? - val state = ScalaPbAny.toJavaProto(command.state.get.payload.get) // FIXME state empty? FIXME payload empty? - val context = - new CommandContextImpl(command.entityId, 0, command.name, command.id, state, handler, service.anySupport) - val reply = handler.handleCommand(cmd, context) - val clientAction = context.createClientAction(reply, false) - CrudFetchReplies( - CrudFetchReplies.Message.Reply( - CrudFetchReply( - command.id, - clientAction, - context.sideEffects - ) - ) - ) - } - - override def update(command: CrudCommand): Future[CrudReplies] = ??? - - override def delete(command: CrudCommand): Future[CrudReplies] = ??? - - trait AbstractContext extends CrudContext { - override def serviceCallFactory(): ServiceCallFactory = rootContext.serviceCallFactory() - } - - class CommandContextImpl(override val entityId: String, - override val sequenceNumber: Long, - override val commandName: String, - override val commandId: Long, - override val state: JavaPbAny, // not sure it is needed - val handler: CrudEntityHandler, - val anySupport: AnySupport) - extends CommandContext - with AbstractContext - with AbstractClientActionContext - with AbstractEffectContext - with ActivatableContext { - - final var events: Vector[ScalaPbAny] = Vector.empty - - override def emit(event: Any): Unit = { - val encoded = anySupport.encodeScala(event) - // Snapshotting should be done!! - // We want to pass the new persistent state to the User Function and is it the right option (handler.handleState ...) - // The persisted state is already passed as part of the CommandContext of each Command - // handler.handleState(ScalaPbAny.toJavaProto(encoded), null) - events :+= encoded - } - } - - class CrudContextImpl(override final val entityId: String) extends CrudContext with AbstractContext -} diff --git a/java-support/src/test/scala/io/cloudstate/javasupport/impl/crud/AnnotationBasedCrudSupportSpec.scala b/java-support/src/test/scala/io/cloudstate/javasupport/impl/crud/AnnotationBasedCrudSupportSpec.scala new file mode 100644 index 000000000..19dbaef68 --- /dev/null +++ b/java-support/src/test/scala/io/cloudstate/javasupport/impl/crud/AnnotationBasedCrudSupportSpec.scala @@ -0,0 +1,290 @@ +package io.cloudstate.javasupport.impl.crud + +import com.example.crud.shoppingcart.Shoppingcart +import com.google.protobuf.any.{Any => ScalaPbAny} +import com.google.protobuf.{ByteString, Any => JavaPbAny} +import io.cloudstate.javasupport.crud.{ + CommandContext, + CommandHandler, + CrudContext, + CrudEntity, + CrudEntityCreationContext, + CrudEventContext, + SnapshotContext, + SnapshotHandler +} +import io.cloudstate.javasupport.impl.{AnySupport, ResolvedServiceMethod, ResolvedType} +import io.cloudstate.javasupport.{Context, EntityId, ServiceCall, ServiceCallFactory, ServiceCallRef} +import org.scalatest.{Matchers, WordSpec} + +class AnnotationBasedCrudSupportSpec extends WordSpec with Matchers { + trait BaseContext extends Context { + override def serviceCallFactory(): ServiceCallFactory = new ServiceCallFactory { + override def lookup[T](serviceName: String, methodName: String, messageType: Class[T]): ServiceCallRef[T] = + throw new NoSuchElementException + } + } + + object MockContext extends CrudContext with BaseContext { + override def entityId(): String = "foo" + } + + class MockCommandContext extends CommandContext with BaseContext { + var emited = Seq.empty[AnyRef] + override def sequenceNumber(): Long = 10 + override def commandName(): String = "AddItem" + override def commandId(): Long = 20 + override def emit(event: AnyRef): Unit = emited :+= event + override def entityId(): String = "foo" + override def fail(errorMessage: String): RuntimeException = ??? + override def forward(to: ServiceCall): Unit = ??? + override def effect(effect: ServiceCall, synchronous: Boolean): Unit = ??? + } + + val eventCtx = new CrudEventContext with BaseContext { + override def sequenceNumber(): Long = 10 + override def entityId(): String = "foo" + } + + object WrappedResolvedType extends ResolvedType[Wrapped] { + override def typeClass: Class[Wrapped] = classOf[Wrapped] + override def typeUrl: String = AnySupport.DefaultTypeUrlPrefix + "/wrapped" + override def parseFrom(bytes: ByteString): Wrapped = Wrapped(bytes.toStringUtf8) + override def toByteString(value: Wrapped): ByteString = ByteString.copyFromUtf8(value.value) + } + + object StringResolvedType extends ResolvedType[String] { + override def typeClass: Class[String] = classOf[String] + override def typeUrl: String = AnySupport.DefaultTypeUrlPrefix + "/string" + override def parseFrom(bytes: ByteString): String = bytes.toStringUtf8 + override def toByteString(value: String): ByteString = ByteString.copyFromUtf8(value) + } + + case class Wrapped(value: String) + val anySupport = new AnySupport(Array(Shoppingcart.getDescriptor), this.getClass.getClassLoader) + val descriptor = Shoppingcart.getDescriptor + .findServiceByName("ShoppingCart") + .findMethodByName("AddItem") + val method = ResolvedServiceMethod(descriptor, StringResolvedType, WrappedResolvedType) + + def create(behavior: AnyRef, methods: ResolvedServiceMethod[_, _]*) = + new AnnotationBasedCrudSupport(behavior.getClass, + anySupport, + methods.map(m => m.descriptor.getName -> m).toMap, + Some(_ => behavior)).create(MockContext) + + def create(clazz: Class[_]) = + new AnnotationBasedCrudSupport(clazz, anySupport, Map.empty, None).create(MockContext) + + def command(str: String) = + ScalaPbAny.toJavaProto(ScalaPbAny(StringResolvedType.typeUrl, StringResolvedType.toByteString(str))) + + def decodeWrapped(any: JavaPbAny): Wrapped = { + any.getTypeUrl should ===(WrappedResolvedType.typeUrl) + WrappedResolvedType.parseFrom(any.getValue) + } + + def event(any: Any): JavaPbAny = anySupport.encodeJava(any) + + "Crud annotation support" should { + "support entity construction" when { + + "there is a noarg constructor" in { + create(classOf[NoArgConstructorTest]) + } + + "there is a constructor with an EntityId annotated parameter" in { + create(classOf[EntityIdArgConstructorTest]) + } + + "there is a constructor with a EventSourcedEntityCreationContext parameter" in { + create(classOf[CreationContextArgConstructorTest]) + } + + "there is a constructor with multiple parameters" in { + create(classOf[MultiArgConstructorTest]) + } + + "fail if the constructor contains an unsupported parameter" in { + a[RuntimeException] should be thrownBy create(classOf[UnsupportedConstructorParameter]) + } + + } + + "support command handlers" when { + + "no arg command handler" in { + val handler = create(new { + @CommandHandler + def addItem() = Wrapped("blah") + }, method) + decodeWrapped(handler.handleCommand(command("nothing"), new MockCommandContext).get) should ===(Wrapped("blah")) + } + + "single arg command handler" in { + val handler = create(new { + @CommandHandler + def addItem(msg: String) = Wrapped(msg) + }, method) + decodeWrapped(handler.handleCommand(command("blah"), new MockCommandContext).get) should ===(Wrapped("blah")) + } + + "multi arg command handler" in { + val handler = create( + new { + @CommandHandler + def addItem(msg: String, @EntityId eid: String, ctx: CommandContext) = { + eid should ===("foo") + ctx.commandName() should ===("AddItem") + Wrapped(msg) + } + }, + method + ) + decodeWrapped(handler.handleCommand(command("blah"), new MockCommandContext).get) should ===(Wrapped("blah")) + } + + "allow emiting events" in { + val handler = create(new { + @CommandHandler + def addItem(msg: String, ctx: CommandContext) = { + ctx.emit(msg + " event") + ctx.commandName() should ===("AddItem") + Wrapped(msg) + } + }, method) + val ctx = new MockCommandContext + decodeWrapped(handler.handleCommand(command("blah"), ctx).get) should ===(Wrapped("blah")) + ctx.emited should ===(Seq("blah event")) + } + + "fail if there's a bad context type" in { + a[RuntimeException] should be thrownBy create(new { + @CommandHandler + def addItem(msg: String, ctx: SnapshotContext) = + Wrapped(msg) + }, method) + } + + "fail if there's two command handlers for the same command" in { + a[RuntimeException] should be thrownBy create(new { + @CommandHandler + def addItem(msg: String, ctx: CommandContext) = + Wrapped(msg) + @CommandHandler + def addItem(msg: String) = + Wrapped(msg) + }, method) + } + + "fail if there's no command with that name" in { + a[RuntimeException] should be thrownBy create(new { + @CommandHandler + def wrongName(msg: String) = + Wrapped(msg) + }, method) + } + + "fail if there's a CRDT command handler" in { + val ex = the[RuntimeException] thrownBy create(new { + @io.cloudstate.javasupport.crdt.CommandHandler + def addItem(msg: String) = + Wrapped(msg) + }, method) + ex.getMessage should include("Did you mean") + ex.getMessage should include(classOf[CommandHandler].getName) + } + + "unwrap exceptions" in { + val handler = create(new { + @CommandHandler + def addItem(): Wrapped = throw new RuntimeException("foo") + }, method) + val ex = the[RuntimeException] thrownBy handler.handleCommand(command("nothing"), new MockCommandContext) + ex.getMessage should ===("foo") + } + + } + + "support state handlers" when { + val ctx = new SnapshotContext with BaseContext { + override def sequenceNumber(): Long = 10 + override def entityId(): String = "foo" + } + + "single parameter" in { + var invoked = false + val handler = create(new { + @SnapshotHandler + def handleState(snapshot: String) = { + snapshot should ===("snap!") + invoked = true + } + }) + handler.handleState(event("snap!"), ctx) + invoked shouldBe true + } + + "context parameter" in { + var invoked = false + val handler = create(new { + @SnapshotHandler + def handleState(snapshot: String, context: SnapshotContext) = { + snapshot should ===("snap!") + context.sequenceNumber() should ===(10) + invoked = true + } + }) + handler.handleState(event("snap!"), ctx) + invoked shouldBe true + } + + "fail if there's a bad context" in { + a[RuntimeException] should be thrownBy create(new { + @SnapshotHandler + def handleState(snapshot: String, context: CommandContext) = () + }) + } + + "fail if there's no snapshot parameter" in { + a[RuntimeException] should be thrownBy create(new { + @SnapshotHandler + def handleState(context: SnapshotContext) = () + }) + } + + "fail if there's no snapshot handler for the given type" in { + val handler = create(new { + @SnapshotHandler + def handleState(snapshot: Int) = () + }) + a[RuntimeException] should be thrownBy handler.handleState(event(10), ctx) + } + + } + } +} + +import Matchers._ + +@CrudEntity +private class NoArgConstructorTest() {} + +@CrudEntity +private class EntityIdArgConstructorTest(@EntityId entityId: String) { + entityId should ===("foo") +} + +@CrudEntity +private class CreationContextArgConstructorTest(ctx: CrudEntityCreationContext) { + ctx.entityId should ===("foo") +} + +@CrudEntity +private class MultiArgConstructorTest(ctx: CrudContext, @EntityId entityId: String) { + ctx.entityId should ===("foo") + entityId should ===("foo") +} + +@CrudEntity +private class UnsupportedConstructorParameter(foo: String) diff --git a/java-support/src/test/scala/io/cloudstate/javasupport/impl/crudone/AnnotationBasedCrudSupportSpec.scala b/java-support/src/test/scala/io/cloudstate/javasupport/impl/crudone/AnnotationBasedCrudSupportSpec.scala deleted file mode 100644 index d7dbb1b44..000000000 --- a/java-support/src/test/scala/io/cloudstate/javasupport/impl/crudone/AnnotationBasedCrudSupportSpec.scala +++ /dev/null @@ -1,84 +0,0 @@ -package io.cloudstate.javasupport.impl.crudone - -import com.example.shoppingcartcrud.Shoppingcart -import com.google.protobuf.{ByteString, Descriptors, Any => JavaPbAny} -import io.cloudstate.javasupport.{Context, ServiceCall, ServiceCallFactory, ServiceCallRef} -import io.cloudstate.javasupport.crud.{CommandContext, CrudContext, CrudEventContext} -import io.cloudstate.javasupport.impl.eventsourced.AnnotationBasedEventSourcedSupport -import io.cloudstate.javasupport.impl.{AnySupport, ResolvedServiceMethod, ResolvedType} -import org.scalatest.{Matchers, WordSpec} -import com.google.protobuf.any.{Any => ScalaPbAny} - -class AnnotationBasedCrudSupportSpec extends WordSpec with Matchers { - trait BaseContext extends Context { - override def serviceCallFactory(): ServiceCallFactory = new ServiceCallFactory { - override def lookup[T](serviceName: String, methodName: String, messageType: Class[T]): ServiceCallRef[T] = - throw new NoSuchElementException - } - } - - object MockContext extends CrudContext with BaseContext { - override def entityId(): String = "foo" - } - - class MockCommandContext extends CommandContext with BaseContext { - var emited = Seq.empty[AnyRef] - override def sequenceNumber(): Long = 10 - override def commandName(): String = "CreateItem" - override def commandId(): Long = 20 - override def emit(event: AnyRef): Unit = emited :+= event - override def entityId(): String = "foo" - override def fail(errorMessage: String): RuntimeException = ??? - override def forward(to: ServiceCall): Unit = ??? - override def effect(effect: ServiceCall, synchronous: Boolean): Unit = ??? - } - - val eventCtx = new CrudEventContext with BaseContext { - override def sequenceNumber(): Long = 10 - override def entityId(): String = "foo" - } - - object WrappedResolvedType extends ResolvedType[Wrapped] { - override def typeClass: Class[Wrapped] = classOf[Wrapped] - override def typeUrl: String = AnySupport.DefaultTypeUrlPrefix + "/wrapped" - override def parseFrom(bytes: ByteString): Wrapped = Wrapped(bytes.toStringUtf8) - override def toByteString(value: Wrapped): ByteString = ByteString.copyFromUtf8(value.value) - } - - object StringResolvedType extends ResolvedType[String] { - override def typeClass: Class[String] = classOf[String] - override def typeUrl: String = AnySupport.DefaultTypeUrlPrefix + "/string" - override def parseFrom(bytes: ByteString): String = bytes.toStringUtf8 - override def toByteString(value: String): ByteString = ByteString.copyFromUtf8(value) - } - - case class Wrapped(value: String) - val anySupport = new AnySupport(Array(Shoppingcart.getDescriptor), this.getClass.getClassLoader) - val descriptor = Shoppingcart.getDescriptor - .findServiceByName("ShoppingCart") - .findMethodByName("AddItem") - val method = ResolvedServiceMethod(descriptor, StringResolvedType, WrappedResolvedType) - - def create(behavior: AnyRef, methods: ResolvedServiceMethod[_, _]*) = - new AnnotationBasedCrudSupport(behavior.getClass, - anySupport, - methods.map(m => m.descriptor.getName -> m).toMap, - Some(_ => behavior)).create(MockContext) - - def create(clazz: Class[_]) = - new AnnotationBasedCrudSupport(clazz, anySupport, Map.empty, None).create(MockContext) - - def command(str: String) = - ScalaPbAny.toJavaProto(ScalaPbAny(StringResolvedType.typeUrl, StringResolvedType.toByteString(str))) - - def decodeWrapped(any: JavaPbAny): Wrapped = { - any.getTypeUrl should ===(WrappedResolvedType.typeUrl) - WrappedResolvedType.parseFrom(any.getValue) - } - - def event(any: Any): JavaPbAny = anySupport.encodeJava(any) - - "support command handlers" when { - - } -} diff --git a/protocols/example/crud/shoppingcart/persistence/domain.proto b/protocols/example/crud/shoppingcart/persistence/domain.proto new file mode 100644 index 000000000..2c93dfa27 --- /dev/null +++ b/protocols/example/crud/shoppingcart/persistence/domain.proto @@ -0,0 +1,19 @@ +// These are the messages that get persisted - the events, plus the current state (Cart) for snapshots. +syntax = "proto3"; + +package com.example.crud.shoppingcart.persistence; + +option go_package = "crud.shoppingcart.persistence"; + +message LineItem { + string userId = 1; + string productId = 2; + string name = 3; + int32 quantity = 4; +} + +// The shopping cart state. +message Cart { + repeated LineItem items = 1; +} + diff --git a/protocols/example/crud/shoppingcart/shoppingcart.proto b/protocols/example/crud/shoppingcart/shoppingcart.proto new file mode 100644 index 000000000..86c2d6c3b --- /dev/null +++ b/protocols/example/crud/shoppingcart/shoppingcart.proto @@ -0,0 +1,73 @@ +// This is the public API offered by the shopping cart entity. +syntax = "proto3"; + +import "google/protobuf/empty.proto"; +import "cloudstate/entity_key.proto"; +import "cloudstate/sub_entity_key.proto"; +import "cloudstate/eventing.proto"; +import "google/api/annotations.proto"; +import "google/api/http.proto"; +import "google/api/httpbody.proto"; + +package com.example.crud.shoppingcart; + +option go_package = "tck/crudshoppingcart"; + +message AddLineItem { + string shopping_id = 1 [(.cloudstate.entity_key) = true]; + string user_id = 2 [(.cloudstate.sub_entity_key) = true]; + string product_id = 3; + string name = 4; + int32 quantity = 5; +} + +message RemoveLineItem { + string user_id = 1 [(.cloudstate.entity_key) = true]; + string product_id = 2; +} + +message RemoveShoppingCart { + string user_id = 1 [(.cloudstate.entity_key) = true]; +} + +message GetShoppingCart { + string user_id = 1 [(.cloudstate.entity_key) = true]; +} + +message LineItem { + string product_id = 1; + string name = 2; + int32 quantity = 3; +} + +message Cart { + repeated LineItem items = 1; +} + +service ShoppingCart { + rpc AddItem(AddLineItem) returns (google.protobuf.Empty) { + option (google.api.http) = { + post: "/cart/{shopping_id}/{user_id}/items/add", + body: "*", + }; + option (.cloudstate.eventing).in = "items"; + } + + rpc RemoveItem(RemoveLineItem) returns (google.protobuf.Empty) { + option (google.api.http).post = "/cart/{user_id}/items/{product_id}/remove"; + } + + rpc RemoveCart(RemoveShoppingCart) returns (google.protobuf.Empty) { + option (google.api.http).post = "/cart/{user_id}/remove"; + } + + rpc GetCart(GetShoppingCart) returns (Cart) { + option (google.api.http) = { + get: "/carts/{user_id}", + additional_bindings: { + get: "/carts/{user_id}/items", + response_body: "items" + } + }; + } +} diff --git a/protocols/example/shoppingcart/crud.persistence/domain.proto b/protocols/example/shoppingcart/crud.persistence/domain.proto deleted file mode 100644 index 99b3d7fac..000000000 --- a/protocols/example/shoppingcart/crud.persistence/domain.proto +++ /dev/null @@ -1,23 +0,0 @@ -// These are the messages that get persisted - the events, plus the current state (Cart) for snapshots. -syntax = "proto3"; - -package com.example.shoppingcart.crud.persistence; - -option go_package = "crud.persistence"; - -message LineItem { - string productId = 1; - string name = 2; - int32 quantity = 3; -} - -// The shopping cart state. -message Cart { - repeated LineItem items = 1; -} - -// The shopping cart modification event. -message CartModification { - repeated LineItem items = 1; -} - diff --git a/protocols/protocol/cloudstate/crud.proto b/protocols/protocol/cloudstate/crud.proto new file mode 100644 index 000000000..23c18c1ad --- /dev/null +++ b/protocols/protocol/cloudstate/crud.proto @@ -0,0 +1,126 @@ +// Copyright 2019 Lightbend Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// gRPC interface for CRUD Entity user functions. + +syntax = "proto3"; + +package cloudstate.crud; + +// Any is used so that domain events defined according to the functions business domain can be embedded inside +// the protocol. +import "google/protobuf/any.proto"; +import "google/protobuf/empty.proto"; +import "cloudstate/entity.proto"; + +option java_package = "io.cloudstate.protocol"; +option go_package = "cloudstate/protocol"; + +// The type of the command to be executed +enum CrudCommandType { + UNKNOWN = 0; + CREATE = 1; + FETCH = 2; + FETCHALL = 3; + UPDATE = 4; + DELETE = 5; +} + +// The persisted state +message CrudState { + // The state payload + google.protobuf.Any payload = 2; +} + +// Message for initiating the command execution +// which contains the command type to be able to identify the crud operation being called +message CrudEntityCommand { + // The ID of the entity. + string entity_id = 1; + + // The ID of a sub entity. + string sub_entity_id = 2; + + // Command name + string name = 3; + + // The command payload. + google.protobuf.Any payload = 4; + + // The command type. + CrudCommandType type = 5; +} + +// The command to be executed +// which can be for the any of the supported (create, fetch, save, delete, fetchAll) crud operations. +message CrudCommand { + // The name of the service this crud entity is on. + string service_name = 1; + + // The ID of the entity. + string entity_id = 2; + + // The ID of a sub entity. + string sub_entity_id = 3; + + // A command id. + int64 id = 4; + + // Command name + string name = 5; + + // The command payload. + google.protobuf.Any payload = 6; + + // The persisted state to be conveyed between persistent entity and the user function. + CrudState state = 7; +} + +// A reply to a command. +message CrudReply { + + // The id of the command being replied to. Must match the input command. + int64 command_id = 1; + + // The action to take + ClientAction client_action = 2; + + // Any side effects to perform + repeated SideEffect side_effects = 3; + + // An optional state to persist. + google.protobuf.Any state = 4; +} + +// Missing better name. It will be fixed +message CrudReplyOut { + oneof message { + CrudReply reply = 1; + Failure failure = 2; + } +} + +// The CRUD Entity service +service Crud { + + rpc create(CrudCommand) returns (CrudReplyOut) {} + + rpc fetch(CrudCommand) returns (CrudReplyOut) {} + + rpc save(CrudCommand) returns (CrudReplyOut) {} + + rpc delete(CrudCommand) returns (CrudReplyOut) {} + + rpc fetchAll(CrudCommand) returns (CrudReplyOut) {} +} diff --git a/protocols/protocol/cloudstate/crud_one.proto b/protocols/protocol/cloudstate/crud_one.proto deleted file mode 100644 index 0598c10fa..000000000 --- a/protocols/protocol/cloudstate/crud_one.proto +++ /dev/null @@ -1,159 +0,0 @@ -// Copyright 2019 Lightbend Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// gRPC interface for CRUD Entity user functions. - -syntax = "proto3"; - -package cloudstate.crudone; - -// Any is used so that domain events defined according to the functions business domain can be embedded inside -// the protocol. -import "google/protobuf/any.proto"; -import "google/protobuf/empty.proto"; -import "cloudstate/entity.proto"; -//import "cloudstate/sub_entity.proto"; - -option java_package = "io.cloudstate.protocol"; -option go_package = "cloudstate/protocol"; - -// The init message. This will always be the first message sent to the entity when -// it is loaded. -message CrudInit { - - string service_name = 1; - - // The ID of the entity. - string entity_id = 2; - - // If present the entity should initialise its state using this snapshot. - CrudSnapshot snapshot = 3; -} - -// A snapshot -message CrudSnapshot { - - // The sequence number when the snapshot was taken. - int64 snapshot_sequence = 1; - - // The snapshot. - google.protobuf.Any snapshot = 2; -} - -// An event. These will be sent to the entity when the entity starts up. -message CrudEvent { - - // The sequence number of the event. - int64 sequence = 1; - - // The event payload. - google.protobuf.Any payload = 2; -} - -// A reply to a command. -message CrudReply { - - // The id of the command being replied to. Must match the input command. - int64 command_id = 1; - - // The action to take - ClientAction client_action = 2; - - // Any side effects to perform - repeated SideEffect side_effects = 3; - - // A list of events to persist - these will be persisted before the reply - // is sent. - repeated google.protobuf.Any events = 4; - - // An optional snapshot to persist. It is assumed that this snapshot will have - // the state of any events in the events field applied to it. It is illegal to - // send a snapshot without sending any events. - google.protobuf.Any snapshot = 5; -} - -// A CRUD command. For each CRUD command received, a reply must be sent with a matching command id. -message CrudCreateCommand { - - // The ID of the entity. - string entity_id = 1; - - // The ID of a sub entity. - string sub_entity_id = 2; - - // A command id. - int64 id = 3; - - // Command name - string name = 4; - - // The command payload. - google.protobuf.Any payload = 5; - - // Whether the command is streamed or not - bool streamed = 6; -} - -// A CRUD command. For each CRUD command received, a reply must be sent with a matching command id. -message CrudFetchCommand { - - // The ID of the entity. - string entity_id = 1; - - // The ID of a sub entity. - string sub_entity_id = 2; - - // A command id. - int64 id = 3; - - // Command name - string name = 4; - - // The command payload. - google.protobuf.Any payload = 5; - - // Whether the command is streamed or not - bool streamed = 6; -} - -// Input message type for the gRPC stream in. -message CrudStreamIn { - oneof message { - CrudInit init = 1; - CrudEvent event = 2; - CrudCreateCommand create = 3; - CrudFetchCommand fetch = 4; - } -} - -// Output message type for the gRPC stream out. -message CrudStreamOut { - oneof message { - CrudReply reply = 1; - Failure failure = 2; - } -} - -message CrudCommandResponse { - oneof response { - CrudReply reply = 1; - Failure failure = 2; - } -} - -// The CRUD Entity service -service CrudOne { - - rpc handle(stream CrudStreamIn) returns (stream CrudStreamOut) {} -} diff --git a/protocols/protocol/cloudstate/crud_two.proto b/protocols/protocol/cloudstate/crud_two.proto deleted file mode 100644 index 027a9d1f7..000000000 --- a/protocols/protocol/cloudstate/crud_two.proto +++ /dev/null @@ -1,231 +0,0 @@ -// Copyright 2019 Lightbend Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// gRPC interface for CRUD Entity user functions. - -syntax = "proto3"; - -package cloudstate.crudtwo; - -// Any is used so that domain events defined according to the functions business domain can be embedded inside -// the protocol. -import "google/protobuf/any.proto"; -import "google/protobuf/empty.proto"; -import "cloudstate/entity.proto"; -//import "cloudstate/sub_entity.proto"; - -option java_package = "io.cloudstate.protocol"; -option go_package = "cloudstate/protocol"; - -// The type of the command to be executed -enum CrudCommandType { - UNKNOWN = 0; - CREATE = 1; - FETCH = 2; - UPDATE = 3; - DELETE = 4; -} - -// The persisted state -message CrudState { - // The state payload - google.protobuf.Any payload = 2; -} - -// Message for initiating the command execution -message CrudInitCommand { - - // The ID of the entity. - string entity_id = 1; - - // The ID of a sub entity. - string sub_entity_id = 2; - - // Command name - string name = 3; - - // The command payload. - google.protobuf.Any payload = 4; - - // The command type. - CrudCommandType type = 5; -} - -// Message for the command to be execute -// I am not sure we need different command for each service operation like create, fetch, update and remove. -// Perhaps we would use it for clarity because some operation has different semantics -// (see CreateCommand, FetchCommand, UpdateCommand and DeleteCommand). Is it an option? -message CrudCommand { - - // The ID of the entity. - string entity_id = 1; - - // The ID of a sub entity. - string sub_entity_id = 2; - - // A command id. - int64 id = 3; - - // Command name - string name = 4; - - // The command payload. - google.protobuf.Any payload = 5; - - // The persisted state to be conveyed between persistent entity and the user function. - CrudState state = 6; -} - -message CreateCommand { - - // The ID of the entity. - string entity_id = 1; - - // The ID of a sub entity. - string sub_entity_id = 2; - - // A command id. - int64 id = 3; - - // Command name - string name = 4; - - // The command payload. - google.protobuf.Any payload = 5; - - // The persisted state. - CrudState state = 6; -} - -message FetchCommand { - - // The ID of the entity. - string entity_id = 1; - - // The ID of a sub entity. - string sub_entity_id = 2; - - // A command id. - int64 id = 3; - - // Command name - string name = 4; - - // The persisted state. - CrudState state = 5; -} - -message UpdateCommand { - - // The ID of the entity. - string entity_id = 1; - - // The ID of a sub entity. - string sub_entity_id = 2; - - // A command id. - int64 id = 3; - - // Command name - string name = 4; - - // The command payload. - google.protobuf.Any payload = 5; - - // The persisted state. - CrudState state = 6; -} - -message DeleteCommand { - - // The ID of the entity. - string entity_id = 1; - - // The ID of a sub entity. - string sub_entity_id = 2; - - // A command id. - int64 id = 3; - - // Command name - string name = 4; - - // The persisted state. - CrudState state = 5; -} - -// A reply to a command. -message CrudReply { - - // The id of the command being replied to. Must match the input command. - int64 command_id = 1; - - // The action to take - ClientAction client_action = 2; - - // Any side effects to perform - repeated SideEffect side_effects = 3; - - // An optional state to persist. - google.protobuf.Any state = 4; -} - -// Missing better name. It will be fixed -message CrudReplies { - oneof message { - CrudReply reply = 1; - Failure failure = 2; - } -} - -message CrudFetchReply { - // The id of the command being replied to. Must match the input command. - int64 command_id = 1; - - // The action to take - ClientAction client_action = 2; - - // Any side effects to perform - repeated SideEffect side_effects = 3; -} - -// Missing better name. It will be fixed -message CrudFetchReplies { - oneof message { - CrudFetchReply reply = 1; - Failure failure = 2; - } -} - -// The CRUD Entity service -service CrudTwo { - - rpc create(CrudCommand) returns (CrudReplies) {} - - rpc fetch(CrudCommand) returns (CrudFetchReplies) {} - - rpc update(CrudCommand) returns (CrudReplies) {} - - rpc delete(CrudCommand) returns (CrudReplies) {} - - - // another option - //rpc create(CreateCommand) returns (CrudReplies) {} - - //rpc fetch(FetchCommand) returns (CrudFetchReplies) {} - - //rpc update(UpdateCommand) returns (CrudReplies) {} - - //rpc delete(DeleteCommand) returns (CrudReplies) {} -} diff --git a/proxy/core/src/main/scala/io/cloudstate/proxy/EntityDiscoveryManager.scala b/proxy/core/src/main/scala/io/cloudstate/proxy/EntityDiscoveryManager.scala index 409f370e4..6441ef54a 100644 --- a/proxy/core/src/main/scala/io/cloudstate/proxy/EntityDiscoveryManager.scala +++ b/proxy/core/src/main/scala/io/cloudstate/proxy/EntityDiscoveryManager.scala @@ -37,6 +37,7 @@ import com.google.protobuf.Descriptors.{FileDescriptor, ServiceDescriptor} import com.typesafe.config.Config import io.cloudstate.protocol.entity._ import io.cloudstate.protocol.crdt.Crdt +import io.cloudstate.protocol.crud.Crud import io.cloudstate.protocol.event_sourced.EventSourced import io.cloudstate.protocol.function.StatelessFunction import io.cloudstate.proxy.StatsCollector.StatsCollectorSettings @@ -51,6 +52,7 @@ import io.cloudstate.proxy.autoscaler.{ NoScaler } import io.cloudstate.proxy.crdt.CrdtSupportFactory +import io.cloudstate.proxy.crud.CrudSupportFactory import io.cloudstate.proxy.eventsourced.EventSourcedSupportFactory import io.cloudstate.proxy.eventing.EventingManager import io.cloudstate.proxy.function.StatelessFunctionSupportFactory @@ -202,7 +204,12 @@ class EntityDiscoveryManager(config: EntityDiscoveryManager.Configuration)( config, clientSettings, concurrencyEnforcer = concurrencyEnforcer, - statsCollector = statsCollector) + statsCollector = statsCollector), + Crud.name -> new CrudSupportFactory(context.system, + config, + clientSettings, + concurrencyEnforcer = concurrencyEnforcer, + statsCollector = statsCollector) ) else Map.empty } diff --git a/proxy/core/src/main/scala/io/cloudstate/proxy/crudtwo/CrudEntity.scala b/proxy/core/src/main/scala/io/cloudstate/proxy/crud/CrudEntity.scala similarity index 72% rename from proxy/core/src/main/scala/io/cloudstate/proxy/crudtwo/CrudEntity.scala rename to proxy/core/src/main/scala/io/cloudstate/proxy/crud/CrudEntity.scala index 44eddb6a9..d25bdb9af 100644 --- a/proxy/core/src/main/scala/io/cloudstate/proxy/crudtwo/CrudEntity.scala +++ b/proxy/core/src/main/scala/io/cloudstate/proxy/crud/CrudEntity.scala @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.cloudstate.proxy.crudtwo +package io.cloudstate.proxy.crud import java.net.URLDecoder import java.util.concurrent.atomic.AtomicLong @@ -25,10 +25,11 @@ import akka.persistence._ import akka.stream.Materializer import akka.util.Timeout import com.google.protobuf.any.{Any => pbAny} -import io.cloudstate.protocol.crud_two._ +import io.cloudstate.protocol.crud._ import io.cloudstate.protocol.entity._ import io.cloudstate.proxy.ConcurrencyEnforcer.{Action, ActionCompleted} import io.cloudstate.proxy.StatsCollector +import io.cloudstate.proxy.crud.CrudEntity.InternalState import io.cloudstate.proxy.entity.UserFunctionReply import scala.collection.immutable.Queue @@ -38,7 +39,7 @@ object CrudEntitySupervisor { private final case class Relay(actorRef: ActorRef) private final case object Start - def props(client: CrudTwoClient, + def props(client: CrudClient, configuration: CrudEntity.Configuration, concurrencyEnforcer: ActorRef, statsCollector: ActorRef)(implicit mat: Materializer): Props = @@ -55,7 +56,7 @@ object CrudEntitySupervisor { * persistence starts feeding us events. There's a race condition if we do this in the same persistent actor. This * establishes that connection first. */ -final class CrudEntitySupervisor(client: CrudTwoClient, +final class CrudEntitySupervisor(client: CrudClient, configuration: CrudEntity.Configuration, concurrencyEnforcer: ActorRef, statsCollector: ActorRef)(implicit mat: Materializer) @@ -113,9 +114,11 @@ object CrudEntity { replyTo: ActorRef ) + private final case class InternalState(value: pbAny) + final def props(configuration: Configuration, entityId: String, - client: CrudTwoClient, + client: CrudClient, concurrencyEnforcer: ActorRef, statsCollector: ActorRef): Props = Props(new CrudEntity(configuration, entityId, client, concurrencyEnforcer, statsCollector)) @@ -128,7 +131,7 @@ object CrudEntity { final class CrudEntity(configuration: CrudEntity.Configuration, entityId: String, - client: CrudTwoClient, + client: CrudClient, concurrencyEnforcer: ActorRef, statsCollector: ActorRef) extends PersistentActor @@ -141,12 +144,13 @@ final class CrudEntity(configuration: CrudEntity.Configuration, private val actorId = CrudEntity.actorCounter.incrementAndGet() - private[this] final var state = CrudState(None) + private[this] final var state: Option[InternalState] = None - private[this] final var stashedCommands = Queue.empty[(CrudInitCommand, ActorRef)] // PERFORMANCE: look at options for data structures + private[this] final var stashedCommands = Queue.empty[(CrudEntityCommand, ActorRef)] // PERFORMANCE: look at options for data structures private[this] final var currentCommand: CrudEntity.OutstandingCommand = null private[this] final var stopped = false private[this] final var idCounter = 0L + private[this] final var inited = false private[this] final var reportedDatabaseOperationStarted = false private[this] final var databaseOperationStartTime = 0L private[this] final var commandStartTime = 0L @@ -183,7 +187,7 @@ final class CrudEntity(configuration: CrudEntity.Configuration, case null => case req => req.replyTo ! createFailure(msg) } - val errorNotification = createFailure("Entity terminated") + val errorNotification = createFailure("CRUD entity terminated") stashedCommands.foreach { case (_, replyTo) => replyTo ! errorNotification } @@ -197,45 +201,44 @@ final class CrudEntity(configuration: CrudEntity.Configuration, private[this] final def reportActionComplete() = concurrencyEnforcer ! ActionCompleted(currentCommand.actionId, System.nanoTime() - commandStartTime) - private[this] final def handleCommand(initCommand: CrudInitCommand, sender: ActorRef): Unit = { + private[this] final def handleCommand(entityCommand: CrudEntityCommand, sender: ActorRef): Unit = { idCounter += 1 val command = CrudCommand( + serviceName = configuration.serviceName, entityId = entityId, - subEntityId = initCommand.entityId, + subEntityId = entityCommand.entityId, id = idCounter, - name = initCommand.name, - payload = initCommand.payload, - Some(state) + name = entityCommand.name, + payload = entityCommand.payload, + state = state.map(s => CrudState(Some(s.value))) ) currentCommand = CrudEntity.OutstandingCommand(idCounter, actorId + ":" + entityId + ":" + idCounter, sender) commandStartTime = System.nanoTime() concurrencyEnforcer ! Action( currentCommand.actionId, - () => { - initCommand.`type` match { - case CrudCommandType.CREATE => - client.create(command) pipeTo self + () => handleCommand(command, entityCommand.`type`) + ) + } - case CrudCommandType.FETCH => - client.fetch(command) pipeTo self + private[this] final def handleCommand(command: CrudCommand, commandType: CrudCommandType): Unit = + commandType match { + case CrudCommandType.CREATE => + client.create(command) pipeTo self - case CrudCommandType.UPDATE => - client.update(command) pipeTo self + case CrudCommandType.FETCH => + client.fetch(command) pipeTo self - case CrudCommandType.DELETE => - client.delete(command) pipeTo self - } - } - ) - } + case CrudCommandType.UPDATE => + client.save(command) pipeTo self - private final def esReplyToUfReply(reply: CrudReply): UserFunctionReply = - UserFunctionReply( - clientAction = reply.clientAction, - sideEffects = reply.sideEffects - ) + case CrudCommandType.DELETE => + client.delete(command) pipeTo self - private final def esReplyToUfReply(reply: CrudFetchReply): UserFunctionReply = + case CrudCommandType.FETCHALL => + client.fetchAll(command) pipeTo self + } + + private final def esReplyToUfReply(reply: CrudReply): UserFunctionReply = UserFunctionReply( clientAction = reply.clientAction, sideEffects = reply.sideEffects @@ -246,17 +249,34 @@ final class CrudEntity(configuration: CrudEntity.Configuration, clientAction = Some(ClientAction(ClientAction.Action.Failure(Failure(description = message)))) ) + private[this] final def maybeInit(snapshot: Option[SnapshotOffer]): Unit = + if (!inited) { + state = snapshot.map { + case SnapshotOffer(_, offeredSnapshot: pbAny) => + InternalState(offeredSnapshot) + case other => throw new IllegalStateException(s"Unexpected snapshot type received: ${other.getClass}") + } + inited = true + } + override final def receiveCommand: PartialFunction[Any, Unit] = { - case command: CrudInitCommand if currentCommand != null => + case command: CrudEntityCommand if currentCommand != null => stashedCommands = stashedCommands.enqueue((command, sender())) - case command: CrudInitCommand => + case command: CrudEntityCommand => handleCommand(command, sender()) - case CrudReplies(m, _) => + case CrudReplyOut(m, _) => + import CrudReplyOut.{Message => CrudOMsg} m match { - case CrudReplies.Message.Reply(r) => + case CrudOMsg.Reply(r) if currentCommand == null => + crash(s"Unexpected reply, had no current command: $r") + + case CrudOMsg.Reply(r) if currentCommand.commandId != r.commandId => + crash(s"Incorrect command id in reply, expecting ${currentCommand.commandId} but got ${r.commandId}") + + case CrudOMsg.Reply(r) => reportActionComplete() val commandId = currentCommand.commandId r.state match { @@ -266,7 +286,7 @@ final class CrudEntity(configuration: CrudEntity.Configuration, case Some(event) => reportDatabaseOperationStarted() persistAll(List(event)) { _ => - state = CrudState(Some(event)) + state = Some(InternalState(event)) reportDatabaseOperationFinished() // Make sure that the current request is still ours if (currentCommand == null || currentCommand.commandId != commandId) { @@ -277,17 +297,24 @@ final class CrudEntity(configuration: CrudEntity.Configuration, } } - case otherReply => // what to do here? - } + case CrudOMsg.Failure(f) if f.commandId == 0 => + crash(s"Non command specific error from entity: ${f.description}") - case CrudFetchReplies(m, _) => - m match { - case CrudFetchReplies.Message.Reply(r) => + case CrudOMsg.Failure(f) if currentCommand == null => + crash(s"Unexpected failure, had no current command: $f") + + case CrudOMsg.Failure(f) if currentCommand.commandId != f.commandId => + crash(s"Incorrect command id in failure, expecting ${currentCommand.commandId} but got ${f.commandId}") + + case CrudOMsg.Failure(f) => reportActionComplete() - currentCommand.replyTo ! esReplyToUfReply(r) + currentCommand.replyTo ! createFailure(f.description) commandHandled() - case otherReply => // what to do here? + case CrudOMsg.Empty => + // Either the reply/failure wasn't set, or its set to something unknown. + // todo see if scalapb can give us unknown fields so we can possibly log more intelligently + crash("Empty or unknown message from entity output stream") } case Status.Failure(error) => @@ -312,13 +339,15 @@ final class CrudEntity(configuration: CrudEntity.Configuration, override final def receiveRecover: PartialFunction[Any, Unit] = { case offer: SnapshotOffer => - // is it needed?? + maybeInit(Some(offer)) case RecoveryCompleted => reportDatabaseOperationFinished() + maybeInit(None) case event: pbAny => - state = CrudState(Some(event)) + maybeInit(None) + state = Some(InternalState(event)) } private def reportDatabaseOperationStarted(): Unit = diff --git a/proxy/core/src/main/scala/io/cloudstate/proxy/crudtwo/CrudSupportFactory.scala b/proxy/core/src/main/scala/io/cloudstate/proxy/crud/CrudSupportFactory.scala similarity index 86% rename from proxy/core/src/main/scala/io/cloudstate/proxy/crudtwo/CrudSupportFactory.scala rename to proxy/core/src/main/scala/io/cloudstate/proxy/crud/CrudSupportFactory.scala index b7f466968..91332ce0f 100644 --- a/proxy/core/src/main/scala/io/cloudstate/proxy/crudtwo/CrudSupportFactory.scala +++ b/proxy/core/src/main/scala/io/cloudstate/proxy/crud/CrudSupportFactory.scala @@ -1,4 +1,4 @@ -package io.cloudstate.proxy.crudtwo +package io.cloudstate.proxy.crud import akka.NotUsed import akka.actor.{ActorRef, ActorSystem} @@ -11,7 +11,7 @@ import akka.stream.scaladsl.Flow import akka.util.Timeout import com.google.protobuf.ByteString import com.google.protobuf.Descriptors.ServiceDescriptor -import io.cloudstate.protocol.crud_two.{CrudCommandType, CrudInitCommand, CrudTwoClient} +import io.cloudstate.protocol.crud.{CrudClient, CrudCommandType, CrudEntityCommand} import io.cloudstate.protocol.entity.Entity import io.cloudstate.proxy._ import io.cloudstate.proxy.entity.{EntityCommand, UserFunctionReply} @@ -28,7 +28,7 @@ class CrudSupportFactory(system: ActorSystem, private final val log = Logging.getLogger(system, this.getClass) - private val crudClient = CrudTwoClient(grpcClientSettings) + private val crudClient = CrudClient(grpcClientSettings) override def buildEntityTypeSupport(entity: Entity, serviceDescriptor: ServiceDescriptor, @@ -78,30 +78,31 @@ class CrudSupportFactory(system: ActorSystem, private class CrudSupport(eventSourcedEntity: ActorRef, parallelism: Int, private implicit val relayTimeout: Timeout) extends EntityTypeSupport { + import akka.pattern.ask override def handler(method: EntityMethodDescriptor): Flow[EntityCommand, UserFunctionReply, NotUsed] = Flow[EntityCommand].mapAsync(parallelism) { command => val subEntityId = method.extractCrudSubEntityId(command.payload.fold(ByteString.EMPTY)(_.value)) val commandType = extractCommandType(command.payload.fold(ByteString.EMPTY)(_.value)) - val initCommand = CrudInitCommand(entityId = command.entityId, - subEntityId = subEntityId, - name = command.name, - payload = command.payload, - `type` = commandType) + val initCommand = CrudEntityCommand(entityId = command.entityId, + subEntityId = subEntityId, + name = command.name, + payload = command.payload, + `type` = commandType) (eventSourcedEntity ? initCommand).mapTo[UserFunctionReply] } override def handleUnary(command: EntityCommand): Future[UserFunctionReply] = (eventSourcedEntity ? command).mapTo[UserFunctionReply] - private def extractCommandType(string: ByteString): CrudCommandType = - // TODO to be defined for extracting from EntityMethodDescriptor + private def extractCommandType(bytes: ByteString): CrudCommandType = + // TODO to be defined for extracting CrudCommandType from EntityMethodDescriptor CrudCommandType.CREATE } private final class EntityIdExtractor(shards: Int) extends HashCodeMessageExtractor(shards) { override final def entityId(message: Any): String = message match { - case command: EntityCommand => command.entityId + case command: CrudEntityCommand => command.entityId } } diff --git a/proxy/core/src/main/scala/io/cloudstate/proxy/crudone/CrudEntity.scala b/proxy/core/src/main/scala/io/cloudstate/proxy/crudone/CrudEntity.scala deleted file mode 100644 index 3c0c86965..000000000 --- a/proxy/core/src/main/scala/io/cloudstate/proxy/crudone/CrudEntity.scala +++ /dev/null @@ -1,368 +0,0 @@ -/* - * Copyright 2019 Lightbend Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.cloudstate.proxy.crudone - -import java.net.URLDecoder -import java.util.concurrent.atomic.AtomicLong - -import akka.NotUsed -import akka.actor._ -import akka.cluster.sharding.ShardRegion -import akka.persistence._ -import akka.stream.scaladsl._ -import akka.stream.{Materializer, OverflowStrategy} -import akka.util.Timeout -import com.google.protobuf.any.{Any => pbAny} -import io.cloudstate.protocol.entity._ -import io.cloudstate.protocol.crud_one._ -import io.cloudstate.proxy.ConcurrencyEnforcer.{Action, ActionCompleted} -import io.cloudstate.proxy.StatsCollector -import io.cloudstate.proxy.entity.{EntityCommand, UserFunctionReply} - -import scala.collection.immutable.Queue - -object CrudEntitySupervisor { - - private final case class Relay(actorRef: ActorRef) - - def props(client: CrudOneClient, - configuration: CrudEntity.Configuration, - concurrencyEnforcer: ActorRef, - statsCollector: ActorRef)(implicit mat: Materializer): Props = - Props(new CrudEntitySupervisor(client, configuration, concurrencyEnforcer, statsCollector)) -} - -/** - * This serves two purposes. - * - * Firstly, when the StateManager crashes, we don't want it restarted. Cluster sharding restarts, and there's no way - * to customise that. - * - * Secondly, we need to ensure that we have an Akka Streams actorRef source to publish messages two before Akka - * persistence starts feeding us events. There's a race condition if we do this in the same persistent actor. This - * establishes that connection first. - */ -final class CrudEntitySupervisor(client: CrudOneClient, - configuration: CrudEntity.Configuration, - concurrencyEnforcer: ActorRef, - statsCollector: ActorRef)(implicit mat: Materializer) - extends Actor - with Stash { - - import CrudEntitySupervisor._ - - override final def receive: Receive = PartialFunction.empty - - override final def preStart(): Unit = - client - .handle( - Source - .actorRef[CrudStreamIn](configuration.sendQueueSize, OverflowStrategy.fail) - .mapMaterializedValue { ref => - self ! Relay(ref) - NotUsed - } - ) - .runWith(Sink.actorRef(self, CrudEntity.StreamClosed)) - context.become(waitingForRelay) - - private[this] final def waitingForRelay: Receive = { - case Relay(relayRef) => - // Cluster sharding URL encodes entity ids, so to extract it we need to decode. - val entityId = URLDecoder.decode(self.path.name, "utf-8") - val manager = context.watch( - context - .actorOf(CrudEntity.props(configuration, entityId, relayRef, concurrencyEnforcer, statsCollector), "entity") - ) - context.become(forwarding(manager)) - unstashAll() - case _ => stash() - } - - private[this] final def forwarding(manager: ActorRef): Receive = { - case Terminated(`manager`) => - context.stop(self) - case toParent if sender() == manager => - context.parent ! toParent - case msg => - manager forward msg - } - - override def supervisorStrategy: SupervisorStrategy = SupervisorStrategy.stoppingStrategy -} - -object CrudEntity { - - final case object Stop - - final case object StreamClosed extends DeadLetterSuppression - - final case class Configuration( - serviceName: String, - userFunctionName: String, - passivationTimeout: Timeout, - sendQueueSize: Int - ) - - private final case class OutstandingCommand( - commandId: Long, - actionId: String, - replyTo: ActorRef - ) - - final def props(configuration: Configuration, - entityId: String, - relay: ActorRef, - concurrencyEnforcer: ActorRef, - statsCollector: ActorRef): Props = - Props(new CrudEntity(configuration, entityId, relay, concurrencyEnforcer, statsCollector)) - - /** - * Used to ensure the action ids sent to the concurrency enforcer are indeed unique. - */ - private val actorCounter = new AtomicLong(0) -} - -final class CrudEntity(configuration: CrudEntity.Configuration, - entityId: String, - relay: ActorRef, - concurrencyEnforcer: ActorRef, - statsCollector: ActorRef) - extends PersistentActor - with ActorLogging { - override final def persistenceId: String = configuration.userFunctionName + entityId - - private val actorId = CrudEntity.actorCounter.incrementAndGet() - - private[this] final var stashedCommands = Queue.empty[(EntityCommand, ActorRef)] // PERFORMANCE: look at options for data structures - private[this] final var currentCommand: CrudEntity.OutstandingCommand = null - private[this] final var stopped = false - private[this] final var idCounter = 0L - private[this] final var inited = false - private[this] final var reportedDatabaseOperationStarted = false - private[this] final var databaseOperationStartTime = 0L - private[this] final var commandStartTime = 0L - - // Set up passivation timer - context.setReceiveTimeout(configuration.passivationTimeout.duration) - - // First thing actor will do is access database - reportDatabaseOperationStarted() - - override final def postStop(): Unit = { - if (currentCommand != null) { - log.warning("Stopped but we have a current action id {}", currentCommand.actionId) - reportActionComplete() - } - if (reportedDatabaseOperationStarted) { - reportDatabaseOperationFinished() - } - // This will shutdown the stream (if not already shut down) - relay ! Status.Success(()) - } - - private[this] final def commandHandled(): Unit = { - currentCommand = null - if (stashedCommands.nonEmpty) { - val ((request, sender), newStashedCommands) = stashedCommands.dequeue - stashedCommands = newStashedCommands - handleCommand(request, sender) - } else if (stopped) { - context.stop(self) - } - } - - private[this] final def notifyOutstandingRequests(msg: String): Unit = { - currentCommand match { - case null => - case req => req.replyTo ! createFailure(msg) - } - val errorNotification = createFailure("Entity terminated") - stashedCommands.foreach { - case (_, replyTo) => replyTo ! errorNotification - } - } - - private[this] final def crash(msg: String): Unit = { - notifyOutstandingRequests(msg) - throw new Exception(msg) - } - - private[this] final def reportActionComplete() = - concurrencyEnforcer ! ActionCompleted(currentCommand.actionId, System.nanoTime() - commandStartTime) - - private[this] final def handleCommand(entityCommand: EntityCommand, sender: ActorRef): Unit = { - idCounter += 1 - val command = Command( - entityId = entityId, - id = idCounter, - name = entityCommand.name, - payload = entityCommand.payload - ) - currentCommand = CrudEntity.OutstandingCommand(idCounter, actorId + ":" + entityId + ":" + idCounter, sender) - commandStartTime = System.nanoTime() - concurrencyEnforcer ! Action(currentCommand.actionId, () => { - //relay ! CrudStreamIn(CrudStreamIn.Message.Command(command)) - }) - } - - private final def esReplyToUfReply(reply: CrudReply) = - UserFunctionReply( - clientAction = reply.clientAction, - sideEffects = reply.sideEffects - ) - - private final def createFailure(message: String) = - UserFunctionReply( - clientAction = Some(ClientAction(ClientAction.Action.Failure(Failure(description = message)))) - ) - - override final def receiveCommand: PartialFunction[Any, Unit] = { - - case command: EntityCommand if currentCommand != null => - stashedCommands = stashedCommands.enqueue((command, sender())) - - case command: EntityCommand => - handleCommand(command, sender()) - - case CrudStreamOut(m, _) => - import CrudStreamOut.{Message => ESOMsg} - m match { - - case ESOMsg.Reply(r) if currentCommand == null => - crash(s"Unexpected reply, had no current command: $r") - - case ESOMsg.Reply(r) if currentCommand.commandId != r.commandId => - crash(s"Incorrect command id in reply, expecting ${currentCommand.commandId} but got ${r.commandId}") - - case ESOMsg.Reply(r) => - reportActionComplete() - val commandId = currentCommand.commandId - val events = r.events.toVector - if (events.isEmpty) { - currentCommand.replyTo ! esReplyToUfReply(r) - commandHandled() - } else { - reportDatabaseOperationStarted() - var eventsLeft = events.size - persistAll(events) { _ => - eventsLeft -= 1 - if (eventsLeft <= 0) { // Remove this hack when switching to Akka Persistence Typed - reportDatabaseOperationFinished() - r.snapshot.foreach(saveSnapshot) - // Make sure that the current request is still ours - if (currentCommand == null || currentCommand.commandId != commandId) { - crash("Internal error - currentRequest changed before all events were persisted") - } - currentCommand.replyTo ! esReplyToUfReply(r) - commandHandled() - } - } - } - - case ESOMsg.Failure(f) if f.commandId == 0 => - crash(s"Non command specific error from entity: ${f.description}") - - case ESOMsg.Failure(f) if currentCommand == null => - crash(s"Unexpected failure, had no current command: $f") - - case ESOMsg.Failure(f) if currentCommand.commandId != f.commandId => - crash(s"Incorrect command id in failure, expecting ${currentCommand.commandId} but got ${f.commandId}") - - case ESOMsg.Failure(f) => - reportActionComplete() - currentCommand.replyTo ! createFailure(f.description) - commandHandled() - - case ESOMsg.Empty => - // Either the reply/failure wasn't set, or its set to something unknown. - // todo see if scalapb can give us unknown fields so we can possibly log more intelligently - crash("Empty or unknown message from entity output stream") - } - - case CrudEntity.StreamClosed => - notifyOutstandingRequests("Unexpected entity termination") - context.stop(self) - - case Status.Failure(error) => - notifyOutstandingRequests("Unexpected entity termination") - throw error - - case SaveSnapshotSuccess(metadata) => - // Nothing to do - - case SaveSnapshotFailure(metadata, cause) => - log.error("Error saving snapshot", cause) - - case ReceiveTimeout => - context.parent ! ShardRegion.Passivate(stopMessage = CrudEntity.Stop) - - case CrudEntity.Stop => - stopped = true - if (currentCommand == null) { - context.stop(self) - } - } - - private[this] final def maybeInit(snapshot: Option[SnapshotOffer]): Unit = - if (!inited) { - relay ! CrudStreamIn( - CrudStreamIn.Message.Init( - CrudInit( - serviceName = configuration.serviceName, - entityId = entityId, - snapshot = snapshot.map { - case SnapshotOffer(metadata, offeredSnapshot: pbAny) => - CrudSnapshot(metadata.sequenceNr, Some(offeredSnapshot)) - case other => throw new IllegalStateException(s"Unexpected snapshot type received: ${other.getClass}") - } - ) - ) - ) - inited = true - } - - override final def receiveRecover: PartialFunction[Any, Unit] = { - case offer: SnapshotOffer => - maybeInit(Some(offer)) - - case RecoveryCompleted => - reportDatabaseOperationFinished() - maybeInit(None) - - case event: pbAny => - maybeInit(None) - relay ! CrudStreamIn(CrudStreamIn.Message.Event(CrudEvent(lastSequenceNr, Some(event)))) - } - - private def reportDatabaseOperationStarted(): Unit = - if (reportedDatabaseOperationStarted) { - log.warning("Already reported database operation started") - } else { - databaseOperationStartTime = System.nanoTime() - reportedDatabaseOperationStarted = true - statsCollector ! StatsCollector.DatabaseOperationStarted - } - - private def reportDatabaseOperationFinished(): Unit = - if (!reportedDatabaseOperationStarted) { - log.warning("Hadn't reported database operation started") - } else { - reportedDatabaseOperationStarted = false - statsCollector ! StatsCollector.DatabaseOperationFinished(System.nanoTime() - databaseOperationStartTime) - } -} diff --git a/proxy/core/src/main/scala/io/cloudstate/proxy/crudone/CrudSupportFactory.scala b/proxy/core/src/main/scala/io/cloudstate/proxy/crudone/CrudSupportFactory.scala deleted file mode 100644 index d9930642f..000000000 --- a/proxy/core/src/main/scala/io/cloudstate/proxy/crudone/CrudSupportFactory.scala +++ /dev/null @@ -1,95 +0,0 @@ -package io.cloudstate.proxy.crudone - -import akka.NotUsed -import akka.actor.{ActorRef, ActorSystem} -import akka.cluster.sharding.ShardRegion.HashCodeMessageExtractor -import akka.cluster.sharding.{ClusterSharding, ClusterShardingSettings} -import akka.event.Logging -import akka.grpc.GrpcClientSettings -import akka.stream.Materializer -import akka.stream.scaladsl.Flow -import akka.util.Timeout -import com.google.protobuf.Descriptors.ServiceDescriptor -import io.cloudstate.protocol.crud_one.CrudOneClient -import io.cloudstate.protocol.entity.Entity -import io.cloudstate.proxy._ -import io.cloudstate.proxy.entity.{EntityCommand, UserFunctionReply} -import io.cloudstate.proxy.eventsourced.DynamicLeastShardAllocationStrategy - -import scala.concurrent.{ExecutionContext, Future} - -class CrudSupportFactory(system: ActorSystem, - config: EntityDiscoveryManager.Configuration, - grpcClientSettings: GrpcClientSettings, - concurrencyEnforcer: ActorRef, - statsCollector: ActorRef)(implicit ec: ExecutionContext, mat: Materializer) - extends EntityTypeSupportFactory { - - private final val log = Logging.getLogger(system, this.getClass) - - private val crudClient = CrudOneClient(grpcClientSettings) - - override def buildEntityTypeSupport(entity: Entity, - serviceDescriptor: ServiceDescriptor, - methodDescriptors: Map[String, EntityMethodDescriptor]): EntityTypeSupport = { - validate(serviceDescriptor, methodDescriptors) - - val stateManagerConfig = CrudEntity.Configuration(entity.serviceName, - entity.persistenceId, - config.passivationTimeout, - config.relayOutputBufferSize) - - log.debug("Starting EventSourcedEntity for {}", entity.persistenceId) - val clusterSharding = ClusterSharding(system) - val clusterShardingSettings = ClusterShardingSettings(system) - val eventSourcedEntity = clusterSharding.start( - typeName = entity.persistenceId, - entityProps = CrudEntitySupervisor.props(crudClient, stateManagerConfig, concurrencyEnforcer, statsCollector), - settings = clusterShardingSettings, - messageExtractor = new EntityIdExtractor(config.numberOfShards), - allocationStrategy = new DynamicLeastShardAllocationStrategy(1, 10, 2, 0.0), - handOffStopMessage = CrudEntity.Stop - ) - - new EventSourcedSupport(eventSourcedEntity, config.proxyParallelism, config.relayTimeout) - } - - private def validate(serviceDescriptor: ServiceDescriptor, - methodDescriptors: Map[String, EntityMethodDescriptor]): Unit = { - val streamedMethods = - methodDescriptors.values.filter(m => m.method.toProto.getClientStreaming || m.method.toProto.getServerStreaming) - if (streamedMethods.nonEmpty) { - val offendingMethods = streamedMethods.map(_.method.getName).mkString(",") - throw EntityDiscoveryException( - s"Event sourced entities do not support streamed methods, but ${serviceDescriptor.getFullName} has the following streamed methods: ${offendingMethods}" - ) - } - val methodsWithoutKeys = methodDescriptors.values.filter(_.keyFieldsCount < 1) - if (methodsWithoutKeys.nonEmpty) { - val offendingMethods = methodsWithoutKeys.map(_.method.getName).mkString(",") - throw new EntityDiscoveryException( - s"Event sourced entities do not support methods whose parameters do not have at least one field marked as entity_key, " + - "but ${serviceDescriptor.getFullName} has the following methods without keys: ${offendingMethods}" - ) - } - } -} - -private class EventSourcedSupport(eventSourcedEntity: ActorRef, - parallelism: Int, - private implicit val relayTimeout: Timeout) - extends EntityTypeSupport { - import akka.pattern.ask - - override def handler(method: EntityMethodDescriptor): Flow[EntityCommand, UserFunctionReply, NotUsed] = - Flow[EntityCommand].mapAsync(parallelism)(command => (eventSourcedEntity ? command).mapTo[UserFunctionReply]) - - override def handleUnary(command: EntityCommand): Future[UserFunctionReply] = - (eventSourcedEntity ? command).mapTo[UserFunctionReply] -} - -private final class EntityIdExtractor(shards: Int) extends HashCodeMessageExtractor(shards) { - override final def entityId(message: Any): String = message match { - case command: EntityCommand => command.entityId - } -} diff --git a/samples/java-shopping-cart/src/main/java/io/cloudstate/samples/shoppingcart/Main.java b/samples/java-shopping-cart/src/main/java/io/cloudstate/samples/shoppingcart/Main.java index 6b2d8aab4..794405818 100644 --- a/samples/java-shopping-cart/src/main/java/io/cloudstate/samples/shoppingcart/Main.java +++ b/samples/java-shopping-cart/src/main/java/io/cloudstate/samples/shoppingcart/Main.java @@ -1,18 +1,15 @@ package io.cloudstate.samples.shoppingcart; +import com.example.crud.shoppingcart.Shoppingcart; import io.cloudstate.javasupport.*; -import com.example.shoppingcart.Shoppingcart; public final class Main { public static final void main(String[] args) throws Exception { - // it would be better to register this descriptor - // io.cloudstate.keyvalue.KeyValue.getDescriptor() in the registerCrudEntity - // so it is implicit to the user new CloudState() .registerCrudEntity( ShoppingCartCrudEntity.class, Shoppingcart.getDescriptor().findServiceByName("ShoppingCart"), - com.example.shoppingcart.crud.persistence.Domain.getDescriptor(), + com.example.crud.shoppingcart.persistence.Domain.getDescriptor(), io.cloudstate.keyvalue.KeyValue.getDescriptor()) .start() .toCompletableFuture() diff --git a/samples/java-shopping-cart/src/main/java/io/cloudstate/samples/shoppingcart/ShoppingCartCrudEntity.java b/samples/java-shopping-cart/src/main/java/io/cloudstate/samples/shoppingcart/ShoppingCartCrudEntity.java index 466ae0027..e10ae2f9e 100644 --- a/samples/java-shopping-cart/src/main/java/io/cloudstate/samples/shoppingcart/ShoppingCartCrudEntity.java +++ b/samples/java-shopping-cart/src/main/java/io/cloudstate/samples/shoppingcart/ShoppingCartCrudEntity.java @@ -1,141 +1,55 @@ package io.cloudstate.samples.shoppingcart; -import akka.util.ByteString; -import com.example.shoppingcart.Shoppingcart; -import com.example.shoppingcart.crud.persistence.Domain; -import com.google.protobuf.InvalidProtocolBufferException; +import com.example.crud.shoppingcart.Shoppingcart; +import com.example.crud.shoppingcart.persistence.Domain; +import com.google.protobuf.Empty; +import io.cloudstate.javasupport.EntityId; +import io.cloudstate.javasupport.crud.CommandContext; +import io.cloudstate.javasupport.crud.CommandHandler; import io.cloudstate.javasupport.crud.CrudEntity; -import io.cloudstate.javasupport.crud.KeyValue; -import io.cloudstate.javasupport.crud.KeyValue.Key; -import io.cloudstate.javasupport.eventsourced.CommandContext; -import io.cloudstate.javasupport.eventsourced.CommandHandler; +import io.cloudstate.javasupport.crud.SnapshotHandler; -import java.util.Collection; -import java.util.List; -import java.util.stream.Collectors; - -import static io.cloudstate.javasupport.crud.KeyValue.keyOf; +import java.util.LinkedHashMap; +import java.util.Map; +/** A crud entity. */ @CrudEntity -public class ShoppingCartCrudEntity extends KeyValue.KeyValueEntity { - - @CommandHandler - public com.google.protobuf.Empty addItem(Shoppingcart.AddLineItem item, CommandContext ctx) { - if (item.getQuantity() <= 0) { - ctx.fail("Cannot add negative quantity of to item" + item.getProductId()); - } - - Key userId = keyOf(item.getUserId(), ByteString::utf8String, ByteString::fromString); - if (!state().get(userId).isPresent()) { - Domain.Cart cart = - Domain.Cart.newBuilder() - .addItems( - Domain.LineItem.newBuilder() - .setProductId(item.getProductId()) - .setName(item.getName()) - .setQuantity(item.getQuantity()) - .build()) - .build(); +public class ShoppingCartCrudEntity { + private final String entityId; + private final Map cart = new LinkedHashMap<>(); - state().set(userId, cart.toByteString().toStringUtf8()); - } else { - Domain.Cart cart = deserialize(userId); - Domain.LineItem lineItem = - Domain.LineItem.newBuilder() - .setProductId(item.getProductId()) - .setName(item.getName()) - .setQuantity(quantity(cart, item)) - .build(); - List items = - cart.getItemsList().stream() - .filter(i -> !i.getProductId().equals(item.getProductId())) - .collect(Collectors.toList()); - Domain.Cart modifiedCart = - Domain.Cart.newBuilder().addAllItems(items).addItems(lineItem).build(); - state().set(userId, modifiedCart.toByteString().toStringUtf8()); - } - - ctx.emit(state().toProtoModification()); - - return com.google.protobuf.Empty.getDefaultInstance(); + public ShoppingCartCrudEntity(@EntityId String entityId) { + this.entityId = entityId; } - @CommandHandler - public com.google.protobuf.Empty removeItem( - Shoppingcart.RemoveLineItem item, CommandContext ctx) { - Key userId = keyOf(item.getUserId(), ByteString::utf8String, ByteString::fromString); - if (!state().get(userId).isPresent()) { - ctx.fail( - "Cannot remove item " + item.getProductId() + " for unknown user " + item.getUserId()); + @SnapshotHandler + public void handleState(Domain.Cart cart) { + this.cart.clear(); + for (Domain.LineItem item : cart.getItemsList()) { + this.cart.put(item.getProductId(), convert(item)); } - - Domain.Cart cart = deserialize(userId); - if (!containsItem(cart, item)) { - ctx.fail("Cannot remove item " + item.getProductId() + " because it is not in the cart."); - } - - List items = - cart.getItemsList().stream() - .filter(lineItem -> !lineItem.getProductId().equals(item.getProductId())) - .collect(Collectors.toList()); - Domain.Cart modifiedCart = Domain.Cart.newBuilder().addAllItems(items).build(); - - state().set(userId, modifiedCart.toByteString().toStringUtf8()); - ctx.emit(state().toProtoModification()); - - return com.google.protobuf.Empty.getDefaultInstance(); } @CommandHandler - public com.google.protobuf.Empty removeCart( - Shoppingcart.RemoveShoppingCart cart, CommandContext ctx) { - Key userId = keyOf(cart.getUserId(), ByteString::utf8String, ByteString::fromString); - if (!state().get(userId).isPresent()) { - ctx.fail("Cannot remove cart " + cart.getUserId() + " because it is unknown"); - } - - state().remove(userId); - ctx.emit(state().toProtoModification()); - - return com.google.protobuf.Empty.getDefaultInstance(); + public Shoppingcart.Cart getCart() { + return Shoppingcart.Cart.newBuilder().addAllItems(cart.values()).build(); } @CommandHandler - public Shoppingcart.Cart getCart(Shoppingcart.GetShoppingCart cartId, CommandContext ctx) { - Key userId = keyOf(cartId.getUserId(), ByteString::utf8String, ByteString::fromString); - if (!state().get(userId).isPresent()) { - return Shoppingcart.Cart.newBuilder().build(); - } else { - Domain.Cart cart = deserialize(userId); - Collection lineItems = - cart.getItemsList().stream().map(this::convert).collect(Collectors.toList()); - return Shoppingcart.Cart.newBuilder().addAllItems(lineItems).build(); - } - } - - // it should be externalize - private Domain.Cart deserialize(Key userId) { - try { - return Domain.Cart.parseFrom( - com.google.protobuf.ByteString.copyFromUtf8(state().get(userId).get())); - } catch (InvalidProtocolBufferException e) { - return null; + public Empty addItem(Shoppingcart.AddLineItem item, CommandContext ctx) { + if (item.getQuantity() <= 0) { + ctx.fail("Cannot add negative quantity of to item" + item.getProductId()); } - } - - private int quantity(Domain.Cart cart, Shoppingcart.AddLineItem item) { - return cart.getItemsList().stream() - .filter(lineItem -> lineItem.getProductId().equals(item.getProductId())) - .findFirst() - .map(lineItem -> lineItem.getQuantity() + item.getQuantity()) - .orElse(item.getQuantity()); - } - private boolean containsItem(Domain.Cart cart, Shoppingcart.RemoveLineItem item) { - return cart.getItemsList().stream() - .filter(lineItem -> lineItem.getProductId().equals(item.getProductId())) - .findFirst() - .isPresent(); + Domain.LineItem lineItem = + Domain.LineItem.newBuilder() + .setUserId(item.getUserId()) + .setProductId(item.getProductId()) + .setName(item.getName()) + .setQuantity(item.getQuantity()) + .build(); + ctx.emit(Domain.Cart.newBuilder().addItems(lineItem).build()); + return Empty.getDefaultInstance(); } private Shoppingcart.LineItem convert(Domain.LineItem item) { From da4a331a9b0a6e7024e407e06e3d9e5b3681cfa4 Mon Sep 17 00:00:00 2001 From: Guy Youansi Date: Sun, 24 May 2020 21:54:57 +0200 Subject: [PATCH 11/93] add crud command type as proto field option and extend the user function type support to extract add snapshotting in for crud entity removed unused annotation in the annotation based support --- .../javasupport/crud/CrudContext.java | 2 +- .../crud/CrudEntityCreationContext.java | 2 +- .../crud/CrudEntityEventHandler.java | 29 --- .../javasupport/crud/CrudEntityFactory.java | 4 +- .../javasupport/crud/CrudEntityHandler.java | 2 +- .../javasupport/crud/CrudEventContext.java | 11 - .../javasupport/crud/EventHandler.java | 32 --- .../cloudstate/javasupport/crud/Snapshot.java | 24 -- .../javasupport/crud/package-info.java | 9 +- .../crud/AnnotationBasedCrudSupport.scala | 103 +-------- .../javasupport/impl/crud/CrudImpl.scala | 214 ++++++++++++------ .../crud/AnnotationBasedCrudSupportSpec.scala | 6 - .../example/shoppingcart/shoppingcart.proto | 8 - .../cloudstate/crud_command_type.proto | 30 +++ protocols/protocol/cloudstate/crud.proto | 10 +- .../proxy/UserFunctionTypeSupport.scala | 57 +++-- .../io/cloudstate/proxy/crud/CrudEntity.scala | 22 +- .../proxy/crud/CrudSupportFactory.scala | 40 +++- .../proxy/test/ShoppingCartCrudTest.proto | 69 ++++++ .../proxy/EntityMethodDescriptorSpec.scala | 40 ++++ 20 files changed, 379 insertions(+), 335 deletions(-) delete mode 100644 java-support/src/main/java/io/cloudstate/javasupport/crud/CrudEntityEventHandler.java delete mode 100644 java-support/src/main/java/io/cloudstate/javasupport/crud/CrudEventContext.java delete mode 100644 java-support/src/main/java/io/cloudstate/javasupport/crud/EventHandler.java delete mode 100644 java-support/src/main/java/io/cloudstate/javasupport/crud/Snapshot.java create mode 100644 protocols/frontend/cloudstate/crud_command_type.proto create mode 100644 proxy/core/src/test/proto/cloudstate/proxy/test/ShoppingCartCrudTest.proto create mode 100644 proxy/core/src/test/scala/io/cloudstate/proxy/EntityMethodDescriptorSpec.scala diff --git a/java-support/src/main/java/io/cloudstate/javasupport/crud/CrudContext.java b/java-support/src/main/java/io/cloudstate/javasupport/crud/CrudContext.java index b0bcc3808..9affe36c7 100644 --- a/java-support/src/main/java/io/cloudstate/javasupport/crud/CrudContext.java +++ b/java-support/src/main/java/io/cloudstate/javasupport/crud/CrudContext.java @@ -2,5 +2,5 @@ import io.cloudstate.javasupport.EntityContext; -/** Root context for all event sourcing contexts. */ +/** Root context for all crud contexts. */ public interface CrudContext extends EntityContext {} diff --git a/java-support/src/main/java/io/cloudstate/javasupport/crud/CrudEntityCreationContext.java b/java-support/src/main/java/io/cloudstate/javasupport/crud/CrudEntityCreationContext.java index 37c4da96f..bb83dfbbe 100644 --- a/java-support/src/main/java/io/cloudstate/javasupport/crud/CrudEntityCreationContext.java +++ b/java-support/src/main/java/io/cloudstate/javasupport/crud/CrudEntityCreationContext.java @@ -3,6 +3,6 @@ /** * Creation context for {@link CrudEntity} annotated entities. * - *

This may be accepted as an argument to the constructor of an event sourced entity. + *

This may be accepted as an argument to the constructor of an crud entity. */ public interface CrudEntityCreationContext extends CrudContext {} diff --git a/java-support/src/main/java/io/cloudstate/javasupport/crud/CrudEntityEventHandler.java b/java-support/src/main/java/io/cloudstate/javasupport/crud/CrudEntityEventHandler.java deleted file mode 100644 index 3d017d39c..000000000 --- a/java-support/src/main/java/io/cloudstate/javasupport/crud/CrudEntityEventHandler.java +++ /dev/null @@ -1,29 +0,0 @@ -package io.cloudstate.javasupport.crud; - -import com.google.protobuf.Any; - -import java.util.Optional; - -/** - * Low level interface for handling events and commands on an entity. - * - *

Generally, this should not be needed, instead, a class annotated with the {@link - * EventHandler}, {@link CommandHandler} and similar annotations should be used. - */ -public interface CrudEntityEventHandler { - - /** - * Handle the given snapshot. - * - * @param snapshot The snapshot to handle. - * @param context The snapshot context. - */ - void handleSnapshot(Any snapshot, SnapshotContext context); - - /** - * Snapshot the object. - * - * @return The current snapshot, if this object supports snapshoting, otherwise empty. - */ - Optional snapshot(SnapshotContext context); -} diff --git a/java-support/src/main/java/io/cloudstate/javasupport/crud/CrudEntityFactory.java b/java-support/src/main/java/io/cloudstate/javasupport/crud/CrudEntityFactory.java index ec58a8942..603fab452 100644 --- a/java-support/src/main/java/io/cloudstate/javasupport/crud/CrudEntityFactory.java +++ b/java-support/src/main/java/io/cloudstate/javasupport/crud/CrudEntityFactory.java @@ -4,10 +4,10 @@ import io.cloudstate.javasupport.eventsourced.EventHandler; /** - * Low level interface for handling events and commands on an entity. + * Low level interface for handling commands on an crud entity. * *

Generally, this should not be needed, instead, a class annotated with the {@link - * EventHandler}, {@link CommandHandler} and similar annotations should be used. + * CommandHandler} and similar annotations should be used. */ public interface CrudEntityFactory { /** diff --git a/java-support/src/main/java/io/cloudstate/javasupport/crud/CrudEntityHandler.java b/java-support/src/main/java/io/cloudstate/javasupport/crud/CrudEntityHandler.java index 6425a65ad..558d379ba 100644 --- a/java-support/src/main/java/io/cloudstate/javasupport/crud/CrudEntityHandler.java +++ b/java-support/src/main/java/io/cloudstate/javasupport/crud/CrudEntityHandler.java @@ -9,7 +9,7 @@ * an crud entity. * *

Generally, this should not be needed, instead, a class annotated with the {@link - * StateHandler}, {@link CommandHandler} and similar annotations should be used. + * SnapshotHandler}, {@link CommandHandler} and similar annotations should be used. */ public interface CrudEntityHandler { diff --git a/java-support/src/main/java/io/cloudstate/javasupport/crud/CrudEventContext.java b/java-support/src/main/java/io/cloudstate/javasupport/crud/CrudEventContext.java deleted file mode 100644 index 2958091b7..000000000 --- a/java-support/src/main/java/io/cloudstate/javasupport/crud/CrudEventContext.java +++ /dev/null @@ -1,11 +0,0 @@ -package io.cloudstate.javasupport.crud; - -/** Context for an event. */ -public interface CrudEventContext extends CrudContext { - /** - * The sequence number of the current event being processed. - * - * @return The sequence number. - */ - long sequenceNumber(); -} diff --git a/java-support/src/main/java/io/cloudstate/javasupport/crud/EventHandler.java b/java-support/src/main/java/io/cloudstate/javasupport/crud/EventHandler.java deleted file mode 100644 index 3fd1bb5c0..000000000 --- a/java-support/src/main/java/io/cloudstate/javasupport/crud/EventHandler.java +++ /dev/null @@ -1,32 +0,0 @@ -package io.cloudstate.javasupport.crud; - -import io.cloudstate.javasupport.eventsourced.EventContext; -import io.cloudstate.javasupport.impl.CloudStateAnnotation; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/** - * Marks a method as an event handler. - * - *

This method will be invoked whenever an event matching this event handlers event class is - * either replayed on entity recovery, by a command handler. - * - *

The method may take the event object as a parameter. - * - *

Methods annotated with this may take an {@link EventContext}. - */ -@CloudStateAnnotation -@Target(ElementType.METHOD) -@Retention(RetentionPolicy.RUNTIME) -public @interface EventHandler { - /** - * The event class. Generally, this will be determined by looking at the parameter of the event - * handler method, however if the event doesn't need to be passed to the method (for example, - * perhaps it contains no data), then this can be used to indicate which event this handler - * handles. - */ - Class eventClass() default Object.class; -} diff --git a/java-support/src/main/java/io/cloudstate/javasupport/crud/Snapshot.java b/java-support/src/main/java/io/cloudstate/javasupport/crud/Snapshot.java deleted file mode 100644 index acb44f1d1..000000000 --- a/java-support/src/main/java/io/cloudstate/javasupport/crud/Snapshot.java +++ /dev/null @@ -1,24 +0,0 @@ -package io.cloudstate.javasupport.crud; - -import io.cloudstate.javasupport.impl.CloudStateAnnotation; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/** - * Marks a method as a snapshot method. - * - *

An event sourced behavior may have at most one of these. When provided, it will be - * periodically (every n events emitted) be invoked to retrieve a snapshot of the current - * state, to be persisted, so that the event log can be loaded without replaying the entire history. - * - *

The method must return the current state of the entity. - * - *

The method may accept a {@link SnapshotContext} parameter. - */ -@CloudStateAnnotation -@Target(ElementType.METHOD) -@Retention(RetentionPolicy.RUNTIME) -public @interface Snapshot {} diff --git a/java-support/src/main/java/io/cloudstate/javasupport/crud/package-info.java b/java-support/src/main/java/io/cloudstate/javasupport/crud/package-info.java index df26b3631..6442ea7bf 100644 --- a/java-support/src/main/java/io/cloudstate/javasupport/crud/package-info.java +++ b/java-support/src/main/java/io/cloudstate/javasupport/crud/package-info.java @@ -1,14 +1,11 @@ /** * CRUD support. * - *

Event sourced entities can be annotated with the {@link + *

CRUD entities can be annotated with the {@link * io.cloudstate.javasupport.crud.CrudEntity @CrudEntity} annotation, and supply command handlers * using the {@link io.cloudstate.javasupport.crud.CommandHandler @CommandHandler} annotation. * - *

In addition, {@link io.cloudstate.javasupport.crud.EventHandler @EventHandler} annotated - * methods should be defined to handle events, and {@link - * io.cloudstate.javasupport.crud.Snapshot @Snapshot} and {@link - * io.cloudstate.javasupport.crud.SnapshotHandler @SnapshotHandler} annotated methods should be - * defined to produce and handle snapshots respectively. + *

In addition, {@link io.cloudstate.javasupport.crud.SnapshotHandler @SnapshotHandler} annotated + * methods should be defined to handle snapshots. */ package io.cloudstate.javasupport.crud; diff --git a/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/AnnotationBasedCrudSupport.scala b/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/AnnotationBasedCrudSupport.scala index 9f5e0ab8a..16b5a4c1e 100644 --- a/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/AnnotationBasedCrudSupport.scala +++ b/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/AnnotationBasedCrudSupport.scala @@ -13,9 +13,6 @@ import io.cloudstate.javasupport.crud.{ CrudEntityCreationContext, CrudEntityFactory, CrudEntityHandler, - CrudEventContext, - EventHandler, - Snapshot, SnapshotContext, SnapshotHandler } @@ -103,22 +100,16 @@ private[impl] class AnnotationBasedCrudSupport( } private class EventBehaviorReflection( - eventHandlers: Map[Class[_], EventHandlerInvoker], val commandHandlers: Map[String, ReflectionHelper.CommandHandlerInvoker[CommandContext]], - snapshotHandlers: Map[Class[_], SnapshotHandlerInvoker], - val snapshotInvoker: Option[SnapshotInvoker] + snapshotHandlers: Map[Class[_], SnapshotHandlerInvoker] ) { /** * We use a cache in addition to the info we've discovered by reflection so that an event handler can be declared * for a superclass of an event. */ - private val eventHandlerCache = TrieMap.empty[Class[_], Option[EventHandlerInvoker]] private val snapshotHandlerCache = TrieMap.empty[Class[_], Option[SnapshotHandlerInvoker]] - def getCachedEventHandlerForClass(clazz: Class[_]): Option[EventHandlerInvoker] = - eventHandlerCache.getOrElseUpdate(clazz, getHandlerForClass(eventHandlers)(clazz)) - def getCachedSnapshotHandlerForClass(clazz: Class[_]): Option[SnapshotHandlerInvoker] = snapshotHandlerCache.getOrElseUpdate(clazz, getHandlerForClass(snapshotHandlers)(clazz)) @@ -132,7 +123,6 @@ private class EventBehaviorReflection( case None => None } } - } private object EventBehaviorReflection { @@ -140,21 +130,6 @@ private object EventBehaviorReflection { serviceMethods: Map[String, ResolvedServiceMethod[_, _]]): EventBehaviorReflection = { val allMethods = ReflectionHelper.getAllDeclaredMethods(behaviorClass) - val eventHandlers = allMethods - .filter(_.getAnnotation(classOf[EventHandler]) != null) - .map { method => - new EventHandlerInvoker(ReflectionHelper.ensureAccessible(method)) - } - .groupBy(_.eventClass) - .map { - case (eventClass, Seq(invoker)) => (eventClass: Any) -> invoker - case (clazz, many) => - throw new RuntimeException( - s"Multiple methods found for handling event of type $clazz: ${many.map(_.method.getName)}" - ) - } - .asInstanceOf[Map[Class[_], EventHandlerInvoker]] - val commandHandlers = allMethods .filter(_.getAnnotation(classOf[CommandHandler]) != null) .map { method => @@ -165,7 +140,7 @@ private object EventBehaviorReflection { val serviceMethod = serviceMethods.getOrElse(name, { throw new RuntimeException( - s"Command handler method ${method.getName} for command $name found, but the service has no command by that name." + s"Command handler method ${method.getName} for command $name found, but the service has no command with that name." ) }) @@ -196,25 +171,13 @@ private object EventBehaviorReflection { } .asInstanceOf[Map[Class[_], SnapshotHandlerInvoker]] - val snapshotInvoker = allMethods - .filter(_.getAnnotation(classOf[Snapshot]) != null) - .map { method => - new SnapshotInvoker(ReflectionHelper.ensureAccessible(method)) - } match { - case Seq() => None - case Seq(single) => - Some(single) - case _ => - throw new RuntimeException(s"Multiple snapshoting methods found on behavior $behaviorClass") - } - ReflectionHelper.validateNoBadMethods( allMethods, classOf[CrudEntity], - Set(classOf[EventHandler], classOf[CommandHandler], classOf[SnapshotHandler], classOf[Snapshot]) + Set(classOf[CommandHandler], classOf[SnapshotHandler]) ) - new EventBehaviorReflection(eventHandlers, commandHandlers, snapshotHandlers, snapshotInvoker) + new EventBehaviorReflection(commandHandlers, snapshotHandlers) } } @@ -232,45 +195,6 @@ private class EntityConstructorInvoker(constructor: Constructor[_]) extends (Cru } } -private class EventHandlerInvoker(val method: Method) { - - private val annotation = method.getAnnotation(classOf[EventHandler]) - - private val parameters = ReflectionHelper.getParameterHandlers[CrudEventContext](method)() - - private def annotationEventClass = annotation.eventClass() match { - case obj if obj == classOf[Object] => None - case clazz => Some(clazz) - } - - // Verify that there is at most one event handler - val eventClass: Class[_] = parameters.collect { - case MainArgumentParameterHandler(clazz) => clazz - } match { - case Array() => annotationEventClass.getOrElse(classOf[Object]) - case Array(handlerClass) => - annotationEventClass match { - case None => handlerClass - case Some(annotated) if handlerClass.isAssignableFrom(annotated) || annotated.isInterface => - annotated - case Some(nonAssignable) => - throw new RuntimeException( - s"EventHandler method $method has defined an eventHandler class $nonAssignable that can never be assignable from it's parameter $handlerClass" - ) - } - case other => - throw new RuntimeException( - s"EventHandler method $method must defined at most one non context parameter to handle events, the parameters defined were: ${other - .mkString(",")}" - ) - } - - def invoke(obj: AnyRef, event: AnyRef, context: CrudEventContext): Unit = { - val ctx = InvocationContext(event, context) - method.invoke(obj, parameters.map(_.apply(ctx)): _*) - } -} - private class SnapshotHandlerInvoker(val method: Method) { private val parameters = ReflectionHelper.getParameterHandlers[SnapshotContext](method)() @@ -291,22 +215,3 @@ private class SnapshotHandlerInvoker(val method: Method) { method.invoke(obj, parameters.map(_.apply(ctx)): _*) } } - -private class SnapshotInvoker(val method: Method) { - - private val parameters = ReflectionHelper.getParameterHandlers[SnapshotContext](method)() - - parameters.foreach { - case MainArgumentParameterHandler(clazz) => - throw new RuntimeException( - s"Don't know how to handle argument of type $clazz in snapshot method: " + method.getName - ) - case _ => - } - - def invoke(obj: AnyRef, context: SnapshotContext): AnyRef = { - val ctx = InvocationContext("", context) - method.invoke(obj, parameters.map(_.apply(ctx)): _*) - } - -} diff --git a/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/CrudImpl.scala b/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/CrudImpl.scala index ae866b2f3..9703daa4d 100644 --- a/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/CrudImpl.scala +++ b/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/CrudImpl.scala @@ -18,8 +18,7 @@ package io.cloudstate.javasupport.impl.crud import java.util.Optional import akka.actor.ActorSystem -import com.google.protobuf.any.{Any => ScalaPbAny} -import com.google.protobuf.{Descriptors, Any => JavaPbAny} +import com.google.protobuf.Descriptors import io.cloudstate.javasupport.CloudStateRunner.Configuration import io.cloudstate.javasupport.crud._ import io.cloudstate.javasupport.impl._ @@ -58,31 +57,24 @@ final class CrudImpl(_system: ActorSystem, private final val system = _system private final implicit val ec = system.dispatcher - private final val services = _services.iterator.toMap + private final val services = _services.iterator + .map({ + case (name, crudss) => + // FIXME overlay configuration provided by _system + (name, if (crudss.snapshotEvery == 0) crudss.withSnapshotEvery(configuration.snapshotEvery) else crudss) + }) + .toMap - private final var serviceInit = false - private final var handlerInit = false - private final var service: CrudStatefulService = _ - private final var handler: CrudEntityHandler = _ - - override def create(command: CrudCommand): Future[CrudReplyOut] = { - maybeInitService(command.serviceName) - maybeInitHandler(command.entityId) + private final val runner = new EntityHandlerRunner() + override def create(command: CrudCommand): Future[CrudReplyOut] = Future.unit .map { _ => - command.state.map { state => - // Not sure about the best way to push the state to the user function - // There are two options here. The first is using an annotation which is called on handler.handleState. - // handler.handleState will use a new special context called StateContext (will be implemented). - // The other option is to pass the state in the CommandContext and use emit or something else to publish the new state - handler.handleState(ScalaPbAny.toJavaProto(state.payload.get), new SnapshotContextImpl(command.entityId, 0)) - } - val cmd = ScalaPbAny.toJavaProto(command.payload.get) //FIXME payload empty? - val context = - new CommandContextImpl(command.entityId, 0, command.name, command.id, handler, service.anySupport) - val reply = handleCommand(context, cmd) + runner.handleState(command) + val (reply, context) = runner.handleCommand(command) val clientAction = context.createClientAction(reply, false) + runner.endSequenceNumber(context.hasError) + if (!context.hasError) { CrudReplyOut( CrudReplyOut.Message.Reply( @@ -90,7 +82,8 @@ final class CrudImpl(_system: ActorSystem, command.id, clientAction, context.sideEffects, - Some(context.events(0)) // FIXME deal with the events? + runner.event(command.id), + runner.snapshot() ) ) ) @@ -100,36 +93,61 @@ final class CrudImpl(_system: ActorSystem, CrudReply( commandId = command.id, clientAction = clientAction, - state = Some(context.events(0)) // FIXME deal with the events? + state = runner.event(command.id) ) ) ) } } - } - - override def fetch(command: CrudCommand): Future[CrudReplyOut] = { - maybeInitService(command.serviceName) - maybeInitHandler(command.entityId) + override def fetch(command: CrudCommand): Future[CrudReplyOut] = Future.unit .map { _ => - command.state.map { state => - handler.handleState(ScalaPbAny.toJavaProto(state.payload.get), new SnapshotContextImpl(command.entityId, 0)) + runner.handleState(command) + val (reply, context) = runner.handleCommand(command) + val clientAction = context.createClientAction(reply, false) + if (!context.hasError) { + CrudReplyOut( + CrudReplyOut.Message.Reply( + CrudReply( + command.id, + clientAction, + context.sideEffects, + runner.event(command.id), + runner.snapshot() + ) + ) + ) + } else { + CrudReplyOut( + CrudReplyOut.Message.Reply( + CrudReply( + commandId = command.id, + clientAction = clientAction, + state = runner.event(command.id) + ) + ) + ) } + } - val cmd = ScalaPbAny.toJavaProto(command.payload.get) //FIXME payload empty? - val context = - new CommandContextImpl(command.entityId, 0, command.name, command.id, handler, service.anySupport) - val reply = handleCommand(context, cmd) + override def save(command: CrudCommand): Future[CrudReplyOut] = + Future.unit + .map { _ => + runner.handleState(command) + val (reply, context) = runner.handleCommand(command) val clientAction = context.createClientAction(reply, false) + runner.endSequenceNumber(context.hasError) + if (!context.hasError) { CrudReplyOut( CrudReplyOut.Message.Reply( CrudReply( command.id, clientAction, - context.sideEffects + context.sideEffects, + runner.event(command.id), + runner.snapshot() ) ) ) @@ -138,73 +156,121 @@ final class CrudImpl(_system: ActorSystem, CrudReplyOut.Message.Reply( CrudReply( commandId = command.id, - clientAction = clientAction + clientAction = clientAction, + state = runner.event(command.id) ) ) ) } } - } - - override def save(command: CrudCommand): Future[CrudReplyOut] = ??? override def delete(command: CrudCommand): Future[CrudReplyOut] = ??? override def fetchAll(command: CrudCommand): Future[CrudReplyOut] = ??? - private def maybeInitService(serviceName: String): Unit = - if (!serviceInit) { - service = services.getOrElse(serviceName, throw new RuntimeException(s"Service not found: $serviceName")) - serviceInit = true + /* + * Represents a wrapper for the crud service and crud entity handler. + * It creates the service and entity handler once depending on the first command which starts the flow. + * It also deals with snapshotting of events and the deactivation of the command context. + */ + private final class EntityHandlerRunner() { + import com.google.protobuf.{Any => JavaPbAny} + import com.google.protobuf.any.{Any => ScalaPbAny} + + private final var handlerInit = false + private final var service: CrudStatefulService = _ + private final var handler: CrudEntityHandler = _ + + private final var sequenceNumber: Long = 0 + private final var performSnapshot: Boolean = false + private final var events = Map.empty[Long, ScalaPbAny] + + def handleCommand(command: CrudCommand): (Optional[JavaPbAny], CommandContextImpl) = { + maybeInitHandler(command) + + val context = new CommandContextImpl(command.entityId, sequenceNumber, command.name, command.id, this) + try { + command.payload + .map(p => (handler.handleCommand(ScalaPbAny.toJavaProto(p), context), context)) + .getOrElse((Optional.empty[JavaPbAny](), context)) //FIXME payload empty should throw an exception or not? + } catch { + case FailInvoked => + (Optional.empty[JavaPbAny](), context) + } finally { + context.deactivate() + } } - private def maybeInitHandler(entityId: String): Unit = - if (!handlerInit) { - handlerInit = true - handler = service.factory.create(new CrudContextImpl(entityId)) + def handleState(command: CrudCommand): Unit = { + maybeInitHandler(command) + + val context = new SnapshotContextImpl(command.entityId, sequenceNumber) + // Not sure about the best way to push the state to the user function + // There are two options here. The first is using an annotation which is called on runner.handleState. + // runner.handleState will use a new special context called StateContext (will be implemented). + // The other option is to pass the state in the CommandContext and use emit or something else to publish the new state + command.state.map(s => handler.handleState(ScalaPbAny.toJavaProto(s.payload.get), context)) } - private def handleCommand(context: CommandContextImpl, command: JavaPbAny): Optional[JavaPbAny] = - try { - handler.handleCommand(command, context) - } catch { - case FailInvoked => - Optional.empty[JavaPbAny]() - } finally { - context.deactivate() + def emit(event: AnyRef, context: CommandContext): Unit = { + val encoded = service.anySupport.encodeScala(event) + val nextSequenceNumber = context.sequenceNumber() + events.size + 1 + handler.handleState(ScalaPbAny.toJavaProto(encoded), + new SnapshotContextImpl(context.entityId, nextSequenceNumber)) + + events += (context.commandId() -> encoded) + performSnapshot = (service.snapshotEvery > 0) && (performSnapshot || (nextSequenceNumber % service.snapshotEvery == 0)) + } + + def endSequenceNumber(hasError: Boolean): Unit = + if (!hasError) { + sequenceNumber = sequenceNumber + events.size + } + + def snapshot(): Option[ScalaPbAny] = + if (performSnapshot) { + val (_, lastEvent) = events.last + Some(lastEvent) + } else None + + def event(commandId: Long): Option[ScalaPbAny] = { + val e = events.get(commandId) + events -= commandId // remove the event for the command id + e } + private def maybeInitHandler(command: CrudCommand): Unit = + if (!handlerInit) { + service = services.getOrElse(command.serviceName, + throw new RuntimeException(s"Service not found: ${command.serviceName}")) + handler = service.factory.create(new CrudContextImpl(command.entityId)) + sequenceNumber = command.state.map(_.snapshotSequence).getOrElse(0L) + handlerInit = true + } + } + trait AbstractContext extends CrudContext { override def serviceCallFactory(): ServiceCallFactory = rootContext.serviceCallFactory() } - class CommandContextImpl(override val entityId: String, - override val sequenceNumber: Long, - override val commandName: String, - override val commandId: Long, - val handler: CrudEntityHandler, - val anySupport: AnySupport) + private final class CommandContextImpl(override val entityId: String, + override val sequenceNumber: Long, + override val commandName: String, + override val commandId: Long, + private val runner: EntityHandlerRunner) extends CommandContext with AbstractContext with AbstractClientActionContext with AbstractEffectContext with ActivatableContext { - final var events: Vector[ScalaPbAny] = Vector.empty - - override def emit(event: AnyRef): Unit = { - val encoded = anySupport.encodeScala(event) - // Snapshotting should be done!! - handler.handleState(ScalaPbAny.toJavaProto(encoded), new SnapshotContextImpl(entityId, 0)) - events :+= encoded - } + override def emit(event: AnyRef): Unit = + runner.emit(event, this) } - class CrudContextImpl(override final val entityId: String) extends CrudContext with AbstractContext + private final class CrudContextImpl(override final val entityId: String) extends CrudContext with AbstractContext - class SnapshotContextImpl(override final val entityId: String, override final val sequenceNumber: Long) + private final class SnapshotContextImpl(override final val entityId: String, override final val sequenceNumber: Long) extends SnapshotContext with AbstractContext - - //class StateContextImpl(override final val entityId: String) extends CrudContext with AbstractContext with StateContext } diff --git a/java-support/src/test/scala/io/cloudstate/javasupport/impl/crud/AnnotationBasedCrudSupportSpec.scala b/java-support/src/test/scala/io/cloudstate/javasupport/impl/crud/AnnotationBasedCrudSupportSpec.scala index 19dbaef68..6428cf1a9 100644 --- a/java-support/src/test/scala/io/cloudstate/javasupport/impl/crud/AnnotationBasedCrudSupportSpec.scala +++ b/java-support/src/test/scala/io/cloudstate/javasupport/impl/crud/AnnotationBasedCrudSupportSpec.scala @@ -9,7 +9,6 @@ import io.cloudstate.javasupport.crud.{ CrudContext, CrudEntity, CrudEntityCreationContext, - CrudEventContext, SnapshotContext, SnapshotHandler } @@ -41,11 +40,6 @@ class AnnotationBasedCrudSupportSpec extends WordSpec with Matchers { override def effect(effect: ServiceCall, synchronous: Boolean): Unit = ??? } - val eventCtx = new CrudEventContext with BaseContext { - override def sequenceNumber(): Long = 10 - override def entityId(): String = "foo" - } - object WrappedResolvedType extends ResolvedType[Wrapped] { override def typeClass: Class[Wrapped] = classOf[Wrapped] override def typeUrl: String = AnySupport.DefaultTypeUrlPrefix + "/wrapped" diff --git a/protocols/example/shoppingcart/shoppingcart.proto b/protocols/example/shoppingcart/shoppingcart.proto index e8a14d6ed..27e1baf54 100644 --- a/protocols/example/shoppingcart/shoppingcart.proto +++ b/protocols/example/shoppingcart/shoppingcart.proto @@ -24,10 +24,6 @@ message RemoveLineItem { string product_id = 2; } -message RemoveShoppingCart { - string user_id = 1 [(.cloudstate.entity_key) = true]; -} - message GetShoppingCart { string user_id = 1 [(.cloudstate.entity_key) = true]; } @@ -55,10 +51,6 @@ service ShoppingCart { option (google.api.http).post = "/cart/{user_id}/items/{product_id}/remove"; } - rpc RemoveCart(RemoveShoppingCart) returns (google.protobuf.Empty) { - option (google.api.http).post = "/cart/{user_id}/remove"; - } - rpc GetCart(GetShoppingCart) returns (Cart) { option (google.api.http) = { get: "/carts/{user_id}", diff --git a/protocols/frontend/cloudstate/crud_command_type.proto b/protocols/frontend/cloudstate/crud_command_type.proto new file mode 100644 index 000000000..c2f6c89ee --- /dev/null +++ b/protocols/frontend/cloudstate/crud_command_type.proto @@ -0,0 +1,30 @@ +// Copyright 2019 Lightbend Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Extension for specifying which field in a message is to be considered an +// entity key, for the purposes associating gRPC calls with entities and +// sharding. + +syntax = "proto3"; + +import "google/protobuf/descriptor.proto"; + +package cloudstate; + +option java_package = "io.cloudstate"; +option go_package = "github.com/cloudstateio/go-support/cloudstate/;cloudstate"; + +extend google.protobuf.FieldOptions { + bool crud_command_type = 50005; +} diff --git a/protocols/protocol/cloudstate/crud.proto b/protocols/protocol/cloudstate/crud.proto index 23c18c1ad..0c55ad572 100644 --- a/protocols/protocol/cloudstate/crud.proto +++ b/protocols/protocol/cloudstate/crud.proto @@ -37,10 +37,13 @@ enum CrudCommandType { DELETE = 5; } -// The persisted state +// The persisted state with the sequence number of the last snapshot message CrudState { // The state payload google.protobuf.Any payload = 2; + + // The sequence number when the snapshot was taken. + int64 snapshot_sequence = 1; } // Message for initiating the command execution @@ -101,6 +104,11 @@ message CrudReply { // An optional state to persist. google.protobuf.Any state = 4; + + // An optional snapshot to persist. It is assumed that this snapshot will have + // the state of any events in the events field applied to it. It is illegal to + // send a snapshot without sending any events. + google.protobuf.Any snapshot = 5; } // Missing better name. It will be fixed diff --git a/proxy/core/src/main/scala/io/cloudstate/proxy/UserFunctionTypeSupport.scala b/proxy/core/src/main/scala/io/cloudstate/proxy/UserFunctionTypeSupport.scala index cdefa499d..6da5bf4ab 100644 --- a/proxy/core/src/main/scala/io/cloudstate/proxy/UserFunctionTypeSupport.scala +++ b/proxy/core/src/main/scala/io/cloudstate/proxy/UserFunctionTypeSupport.scala @@ -3,12 +3,15 @@ package io.cloudstate.proxy import akka.NotUsed import akka.stream.scaladsl.Flow import com.google.protobuf.Descriptors.{MethodDescriptor, ServiceDescriptor} -import com.google.protobuf.{ByteString, DynamicMessage} +import com.google.protobuf.descriptor.FieldOptions +import com.google.protobuf.{ByteString, Descriptors, DynamicMessage} +import io.cloudstate.crud_command_type.CrudCommandTypeProto import io.cloudstate.entity_key.EntityKeyProto import io.cloudstate.protocol.entity.Entity import io.cloudstate.proxy.entity.{EntityCommand, UserFunctionCommand, UserFunctionReply} import io.cloudstate.proxy.protobuf.Options import io.cloudstate.sub_entity_key.SubEntityKeyProto +import scalapb.GeneratedExtension import scala.collection.JavaConverters._ import scala.concurrent.Future @@ -53,41 +56,47 @@ private object EntityMethodDescriptor { } final class EntityMethodDescriptor(val method: MethodDescriptor) { - private[this] val keyFields = method.getInputType.getFields.iterator.asScala - .filter(field => EntityKeyProto.entityKey.get(Options.convertFieldOptions(field))) - .toArray - .sortBy(_.getIndex) - private[this] val subEntityKeyFields = method.getInputType.getFields.iterator.asScala - .filter(field => SubEntityKeyProto.subEntityKey.get(Options.convertFieldOptions(field))) - .toArray - .sortBy(_.getIndex) + /** represents cloudstate entity key for all entities */ + private[this] val keyFields = commandFieldOptions(EntityKeyProto.entityKey) + + /** represents cloudstate sub entity key for crud entity */ + private[this] val crudSubEntityKeyFields = commandFieldOptions(SubEntityKeyProto.subEntityKey) + + /** represents cloudstate command type for crud entity */ + private[this] val crudCommandTypeFields = commandFieldOptions(CrudCommandTypeProto.crudCommandType) def keyFieldsCount: Int = keyFields.length - def extractId(bytes: ByteString): String = - keyFields.length match { - case 0 => - "" - case 1 => - val dm = DynamicMessage.parseFrom(method.getInputType, bytes) - dm.getField(keyFields.head).toString - case _ => - val dm = DynamicMessage.parseFrom(method.getInputType, bytes) - keyFields.iterator.map(dm.getField).mkString(EntityMethodDescriptor.Separator) - } + def crudSubEntityKeyFieldsCount: Int = crudSubEntityKeyFields.length + + def crudCommandTypeFieldsCount: Int = crudCommandTypeFields.length + + def extractId(bytes: ByteString): String = extract(keyFields, bytes) - def extractCrudSubEntityId(bytes: ByteString): String = - subEntityKeyFields.length match { + def extractCrudSubEntityId(bytes: ByteString): String = extract(crudSubEntityKeyFields, bytes) + + def extractCrudCommandType(bytes: ByteString): String = extract(crudCommandTypeFields, bytes) + + private def extract(fieldOptions: Array[Descriptors.FieldDescriptor], bytes: ByteString): String = + fieldOptions.length match { case 0 => "" case 1 => val dm = DynamicMessage.parseFrom(method.getInputType, bytes) - dm.getField(subEntityKeyFields.head).toString + dm.getField(fieldOptions.head).toString case _ => val dm = DynamicMessage.parseFrom(method.getInputType, bytes) - subEntityKeyFields.iterator.map(dm.getField).mkString(EntityMethodDescriptor.Separator) + fieldOptions.iterator.map(dm.getField).mkString(EntityMethodDescriptor.Separator) } + + private def commandFieldOptions( + optionType: GeneratedExtension[FieldOptions, Boolean] + ): Array[Descriptors.FieldDescriptor] = + method.getInputType.getFields.iterator.asScala + .filter(field => optionType.get(Options.convertFieldOptions(field))) + .toArray + .sortBy(_.getIndex) } private final class EntityUserFunctionTypeSupport(serviceDescriptor: ServiceDescriptor, diff --git a/proxy/core/src/main/scala/io/cloudstate/proxy/crud/CrudEntity.scala b/proxy/core/src/main/scala/io/cloudstate/proxy/crud/CrudEntity.scala index d25bdb9af..0910aa57c 100644 --- a/proxy/core/src/main/scala/io/cloudstate/proxy/crud/CrudEntity.scala +++ b/proxy/core/src/main/scala/io/cloudstate/proxy/crud/CrudEntity.scala @@ -114,7 +114,11 @@ object CrudEntity { replyTo: ActorRef ) - private final case class InternalState(value: pbAny) + // The entity state + private final case class InternalState( + payload: pbAny, // the state payload + sequenceNumber: Long // the sequence number of the last taken snapshot + ) final def props(configuration: Configuration, entityId: String, @@ -210,7 +214,7 @@ final class CrudEntity(configuration: CrudEntity.Configuration, id = idCounter, name = entityCommand.name, payload = entityCommand.payload, - state = state.map(s => CrudState(Some(s.value))) + state = state.map(s => CrudState(Some(s.payload), sequenceNumber())) ) currentCommand = CrudEntity.OutstandingCommand(idCounter, actorId + ":" + entityId + ":" + idCounter, sender) commandStartTime = System.nanoTime() @@ -252,13 +256,16 @@ final class CrudEntity(configuration: CrudEntity.Configuration, private[this] final def maybeInit(snapshot: Option[SnapshotOffer]): Unit = if (!inited) { state = snapshot.map { - case SnapshotOffer(_, offeredSnapshot: pbAny) => - InternalState(offeredSnapshot) + case SnapshotOffer(metadata, offeredSnapshot: pbAny) => + InternalState(offeredSnapshot, metadata.sequenceNr) case other => throw new IllegalStateException(s"Unexpected snapshot type received: ${other.getClass}") } inited = true } + // the sequence number of the last taken snapshot + private[this] final def sequenceNumber(): Long = state.map(_.sequenceNumber).getOrElse(0L) + override final def receiveCommand: PartialFunction[Any, Unit] = { case command: CrudEntityCommand if currentCommand != null => @@ -285,9 +292,10 @@ final class CrudEntity(configuration: CrudEntity.Configuration, commandHandled() case Some(event) => reportDatabaseOperationStarted() - persistAll(List(event)) { _ => - state = Some(InternalState(event)) + persist(event) { _ => reportDatabaseOperationFinished() + state = Some(InternalState(event, sequenceNumber)) + r.snapshot.foreach(saveSnapshot) // Make sure that the current request is still ours if (currentCommand == null || currentCommand.commandId != commandId) { crash("Internal error - currentRequest changed before all events were persisted") @@ -347,7 +355,7 @@ final class CrudEntity(configuration: CrudEntity.Configuration, case event: pbAny => maybeInit(None) - state = Some(InternalState(event)) + state = Some(InternalState(event, sequenceNumber)) } private def reportDatabaseOperationStarted(): Unit = diff --git a/proxy/core/src/main/scala/io/cloudstate/proxy/crud/CrudSupportFactory.scala b/proxy/core/src/main/scala/io/cloudstate/proxy/crud/CrudSupportFactory.scala index 91332ce0f..be63287d0 100644 --- a/proxy/core/src/main/scala/io/cloudstate/proxy/crud/CrudSupportFactory.scala +++ b/proxy/core/src/main/scala/io/cloudstate/proxy/crud/CrudSupportFactory.scala @@ -40,7 +40,7 @@ class CrudSupportFactory(system: ActorSystem, config.passivationTimeout, config.relayOutputBufferSize) - log.debug("Starting EventSourcedEntity for {}", entity.persistenceId) + log.debug("Starting Crud Entity for {}", entity.persistenceId) val clusterSharding = ClusterSharding(system) val clusterShardingSettings = ClusterShardingSettings(system) val eventSourcedEntity = clusterSharding.start( @@ -62,15 +62,33 @@ class CrudSupportFactory(system: ActorSystem, if (streamedMethods.nonEmpty) { val offendingMethods = streamedMethods.map(_.method.getName).mkString(",") throw EntityDiscoveryException( - s"Event sourced entities do not support streamed methods, but ${serviceDescriptor.getFullName} has the following streamed methods: ${offendingMethods}" + s"Crud entities do not support streamed methods, but ${serviceDescriptor.getFullName} has the following streamed methods: ${offendingMethods}" ) } val methodsWithoutKeys = methodDescriptors.values.filter(_.keyFieldsCount < 1) if (methodsWithoutKeys.nonEmpty) { val offendingMethods = methodsWithoutKeys.map(_.method.getName).mkString(",") - throw new EntityDiscoveryException( - s"Event sourced entities do not support methods whose parameters do not have at least one field marked as entity_key, " + - "but ${serviceDescriptor.getFullName} has the following methods without keys: ${offendingMethods}" + throw EntityDiscoveryException( + s"""Crud entities do not support methods whose parameters do not have at least one field marked as entity_key, + |"but ${serviceDescriptor.getFullName} has the following methods without keys: ${offendingMethods}""".stripMargin + ) + } + + val methodsWithoutSubEntityKeys = methodDescriptors.values.filter(_.crudSubEntityKeyFieldsCount < 1) + if (methodsWithoutSubEntityKeys.nonEmpty) { + val offendingMethods = methodsWithoutSubEntityKeys.map(_.method.getName).mkString(",") + throw EntityDiscoveryException( + s"""Crud entities do not support methods whose parameters do not have at least one field marked as sub_entity_key, + |"but ${serviceDescriptor.getFullName} has the following methods without keys: ${offendingMethods}""".stripMargin + ) + } + + val methodsWithoutCommandType = methodDescriptors.values.filter(_.crudCommandTypeFieldsCount < 1) + if (methodsWithoutCommandType.nonEmpty) { + val offendingMethods = methodsWithoutCommandType.map(_.method.getName).mkString(",") + throw EntityDiscoveryException( + s"""Crud entities do not support methods whose parameters do not have at least one field marked as crud_command_type, + |"but ${serviceDescriptor.getFullName} has the following methods without keys: ${offendingMethods}""".stripMargin ) } } @@ -84,7 +102,7 @@ private class CrudSupport(eventSourcedEntity: ActorRef, parallelism: Int, privat override def handler(method: EntityMethodDescriptor): Flow[EntityCommand, UserFunctionReply, NotUsed] = Flow[EntityCommand].mapAsync(parallelism) { command => val subEntityId = method.extractCrudSubEntityId(command.payload.fold(ByteString.EMPTY)(_.value)) - val commandType = extractCommandType(command.payload.fold(ByteString.EMPTY)(_.value)) + val commandType = extractCommandType(method, command) val initCommand = CrudEntityCommand(entityId = command.entityId, subEntityId = subEntityId, name = command.name, @@ -96,9 +114,13 @@ private class CrudSupport(eventSourcedEntity: ActorRef, parallelism: Int, privat override def handleUnary(command: EntityCommand): Future[UserFunctionReply] = (eventSourcedEntity ? command).mapTo[UserFunctionReply] - private def extractCommandType(bytes: ByteString): CrudCommandType = - // TODO to be defined for extracting CrudCommandType from EntityMethodDescriptor - CrudCommandType.CREATE + private def extractCommandType(method: EntityMethodDescriptor, command: EntityCommand): CrudCommandType = { + val commandType = method.extractCrudCommandType(command.payload.fold(ByteString.EMPTY)(_.value)) + CrudCommandType.fromName(commandType.toUpperCase).getOrElse { + // cannot be empty here because the check is made in the validate method of CrudSupportFactory + throw new RuntimeException(s"Command - ${command.name} for CRUD entity should have a command type") + } + } } private final class EntityIdExtractor(shards: Int) extends HashCodeMessageExtractor(shards) { diff --git a/proxy/core/src/test/proto/cloudstate/proxy/test/ShoppingCartCrudTest.proto b/proxy/core/src/test/proto/cloudstate/proxy/test/ShoppingCartCrudTest.proto new file mode 100644 index 000000000..1aa9d2fe5 --- /dev/null +++ b/proxy/core/src/test/proto/cloudstate/proxy/test/ShoppingCartCrudTest.proto @@ -0,0 +1,69 @@ +// This is the public API offered by the shopping cart crud entity. +syntax = "proto3"; + +import "google/protobuf/empty.proto"; +import "cloudstate/entity_key.proto"; +import "cloudstate/sub_entity_key.proto"; +import "cloudstate/crud_command_type.proto"; +import "google/api/annotations.proto"; +import "google/api/http.proto"; +import "google/api/httpbody.proto"; + +package cloudstate.proxy.test.crud; + +option java_package = "io.cloudstate.proxy.test.crud"; + +message AddLineItem { + string shopping_id = 1 [(.cloudstate.entity_key) = true]; + string user_id = 2 [(.cloudstate.sub_entity_key) = true]; + string command_type = 3 [(.cloudstate.crud_command_type) = true]; + string product_id = 4; + string name = 5; + int32 quantity = 6; +} + +message RemoveLineItem { + string user_id = 1 [(.cloudstate.entity_key) = true]; + string product_id = 2; +} + +message GetShoppingCart { + string user_id = 1 [(.cloudstate.entity_key) = true]; +} + +message LineItem { + string product_id = 1; + string name = 2; + int32 quantity = 3; +} + +message Cart { + repeated LineItem items = 1; +} + +service ShoppingCart { + rpc AddItem(AddLineItem) returns (google.protobuf.Empty) { + option (google.api.http) = { + post: "/cart/{user_id}/items/add", + body: "*", + }; + } + + rpc RemoveItem(RemoveLineItem) returns (google.protobuf.Empty) { + option (google.api.http).post = "/cart/{user_id}/items/{product_id}/remove"; + } + + rpc GetCart(GetShoppingCart) returns (Cart) { + option (google.api.http) = { + get: "/carts/{user_id}", + additional_bindings: [{ + get: "/carts/{user_id}/items", + response_body: "items" + }] + }; + } + + rpc ShowCartPage(GetShoppingCart) returns (google.api.HttpBody) { + option (google.api.http).get = "/carts/{user_id}.html"; + } +} diff --git a/proxy/core/src/test/scala/io/cloudstate/proxy/EntityMethodDescriptorSpec.scala b/proxy/core/src/test/scala/io/cloudstate/proxy/EntityMethodDescriptorSpec.scala new file mode 100644 index 000000000..dcb17c7f4 --- /dev/null +++ b/proxy/core/src/test/scala/io/cloudstate/proxy/EntityMethodDescriptorSpec.scala @@ -0,0 +1,40 @@ +package io.cloudstate.proxy + +import io.cloudstate.proxy.test.crud.ShoppingCartCrudTest.{AddLineItem, ShoppingCart} +import org.scalatest.{Matchers, WordSpecLike} + +class EntityMethodDescriptorSpec extends WordSpecLike with Matchers { + + private val addItemDescriptor = ShoppingCart.descriptor + .findServiceByName("ShoppingCart") + .findMethodByName("AddItem") + + private val entityMethodDescriptor = new EntityMethodDescriptor(addItemDescriptor) + + "The EntityMethodDescriptor" should { + + "extract entity key" in { + val subEntityKey = + entityMethodDescriptor.extractId( + AddLineItem("shoppingId", "userId", "productId", "name").toByteString + ) + subEntityKey should ===("shoppingId") + } + + "extract crud sub entity key" in { + val subEntityKey = + entityMethodDescriptor.extractCrudSubEntityId( + AddLineItem("shoppingId", "userId", "productId", "name").toByteString + ) + subEntityKey should ===("userId") + } + + "extract crud command type" in { + val commandType = entityMethodDescriptor.extractCrudCommandType( + AddLineItem("shoppingId", "userId", "create", "productId", "name").toByteString + ) + commandType should ===("create") + } + + } +} From 78345f85e2dfcbf84336e831cdd91ab9a968ebab Mon Sep 17 00:00:00 2001 From: Guy Youansi Date: Mon, 25 May 2020 18:23:52 +0200 Subject: [PATCH 12/93] add some documentation and do some small refactoring --- .../javasupport/impl/crud/CrudImpl.scala | 107 +++++++----------- protocols/protocol/cloudstate/crud.proto | 4 +- 2 files changed, 40 insertions(+), 71 deletions(-) diff --git a/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/CrudImpl.scala b/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/CrudImpl.scala index 9703daa4d..7aeca61a2 100644 --- a/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/CrudImpl.scala +++ b/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/CrudImpl.scala @@ -67,38 +67,7 @@ final class CrudImpl(_system: ActorSystem, private final val runner = new EntityHandlerRunner() - override def create(command: CrudCommand): Future[CrudReplyOut] = - Future.unit - .map { _ => - runner.handleState(command) - val (reply, context) = runner.handleCommand(command) - val clientAction = context.createClientAction(reply, false) - runner.endSequenceNumber(context.hasError) - - if (!context.hasError) { - CrudReplyOut( - CrudReplyOut.Message.Reply( - CrudReply( - command.id, - clientAction, - context.sideEffects, - runner.event(command.id), - runner.snapshot() - ) - ) - ) - } else { - CrudReplyOut( - CrudReplyOut.Message.Reply( - CrudReply( - commandId = command.id, - clientAction = clientAction, - state = runner.event(command.id) - ) - ) - ) - } - } + override def create(command: CrudCommand): Future[CrudReplyOut] = Future.unit.map(_ => handleWriteCommand(command)) override def fetch(command: CrudCommand): Future[CrudReplyOut] = Future.unit @@ -131,43 +100,43 @@ final class CrudImpl(_system: ActorSystem, } } - override def save(command: CrudCommand): Future[CrudReplyOut] = - Future.unit - .map { _ => - runner.handleState(command) - val (reply, context) = runner.handleCommand(command) - val clientAction = context.createClientAction(reply, false) - runner.endSequenceNumber(context.hasError) - - if (!context.hasError) { - CrudReplyOut( - CrudReplyOut.Message.Reply( - CrudReply( - command.id, - clientAction, - context.sideEffects, - runner.event(command.id), - runner.snapshot() - ) - ) - ) - } else { - CrudReplyOut( - CrudReplyOut.Message.Reply( - CrudReply( - commandId = command.id, - clientAction = clientAction, - state = runner.event(command.id) - ) - ) - ) - } - } + override def save(command: CrudCommand): Future[CrudReplyOut] = Future.unit.map(_ => handleWriteCommand(command)) - override def delete(command: CrudCommand): Future[CrudReplyOut] = ??? + override def delete(command: CrudCommand): Future[CrudReplyOut] = Future.unit.map(_ => handleWriteCommand(command)) override def fetchAll(command: CrudCommand): Future[CrudReplyOut] = ??? + private def handleWriteCommand(command: CrudCommand): CrudReplyOut = { + runner.handleState(command) + val (reply, context) = runner.handleCommand(command) + val clientAction = context.createClientAction(reply, false) + runner.endSequenceNumber(context.hasError) + + if (!context.hasError) { + CrudReplyOut( + CrudReplyOut.Message.Reply( + CrudReply( + command.id, + clientAction, + context.sideEffects, + runner.event(command.id), + runner.snapshot() + ) + ) + ) + } else { + CrudReplyOut( + CrudReplyOut.Message.Reply( + CrudReply( + commandId = command.id, + clientAction = clientAction, + state = runner.event(command.id) + ) + ) + ) + } + } + /* * Represents a wrapper for the crud service and crud entity handler. * It creates the service and entity handler once depending on the first command which starts the flow. @@ -183,6 +152,7 @@ final class CrudImpl(_system: ActorSystem, private final var sequenceNumber: Long = 0 private final var performSnapshot: Boolean = false + // map command id to corresponding state changes private final var events = Map.empty[Long, ScalaPbAny] def handleCommand(command: CrudCommand): (Optional[JavaPbAny], CommandContextImpl) = { @@ -205,10 +175,6 @@ final class CrudImpl(_system: ActorSystem, maybeInitHandler(command) val context = new SnapshotContextImpl(command.entityId, sequenceNumber) - // Not sure about the best way to push the state to the user function - // There are two options here. The first is using an annotation which is called on runner.handleState. - // runner.handleState will use a new special context called StateContext (will be implemented). - // The other option is to pass the state in the CommandContext and use emit or something else to publish the new state command.state.map(s => handler.handleState(ScalaPbAny.toJavaProto(s.payload.get), context)) } @@ -222,17 +188,20 @@ final class CrudImpl(_system: ActorSystem, performSnapshot = (service.snapshotEvery > 0) && (performSnapshot || (nextSequenceNumber % service.snapshotEvery == 0)) } + // update the sequence number of the entity def endSequenceNumber(hasError: Boolean): Unit = if (!hasError) { sequenceNumber = sequenceNumber + events.size } + // the snapshot of the entity which is the last state changes so far def snapshot(): Option[ScalaPbAny] = if (performSnapshot) { val (_, lastEvent) = events.last Some(lastEvent) } else None + // the state changes associated with the command id def event(commandId: Long): Option[ScalaPbAny] = { val e = events.get(commandId) events -= commandId // remove the event for the command id diff --git a/protocols/protocol/cloudstate/crud.proto b/protocols/protocol/cloudstate/crud.proto index 0c55ad572..799f23f11 100644 --- a/protocols/protocol/cloudstate/crud.proto +++ b/protocols/protocol/cloudstate/crud.proto @@ -65,8 +65,8 @@ message CrudEntityCommand { CrudCommandType type = 5; } -// The command to be executed -// which can be for the any of the supported (create, fetch, save, delete, fetchAll) crud operations. +// The command to be executed which can be for any of the supported +// (create, fetch, save, delete, fetchAll) crud operations. message CrudCommand { // The name of the service this crud entity is on. string service_name = 1; From d1bdbb10e5f4a9932dedabefdbf12d4308ffbd11 Mon Sep 17 00:00:00 2001 From: Guy Youansi Date: Tue, 26 May 2020 22:32:40 +0200 Subject: [PATCH 13/93] add java doc for crud protocol. extend the shopping cart example. refactoring test --- .../crud/AnnotationBasedCrudSupportSpec.scala | 50 ++++++++-------- .../crud/shoppingcart/shoppingcart.proto | 59 ++++++++++++------- protocols/frontend/cloudstate/key_value.proto | 30 ---------- protocols/protocol/cloudstate/crud.proto | 15 ++++- .../proxy/test/ShoppingCartCrudTest.proto | 29 ++++++--- .../proxy/EntityMethodDescriptorSpec.scala | 8 +-- .../cloudstate/samples/shoppingcart/Main.java | 3 +- .../shoppingcart/ShoppingCartCrudEntity.java | 42 ++++++++++++- 8 files changed, 146 insertions(+), 90 deletions(-) delete mode 100644 protocols/frontend/cloudstate/key_value.proto diff --git a/java-support/src/test/scala/io/cloudstate/javasupport/impl/crud/AnnotationBasedCrudSupportSpec.scala b/java-support/src/test/scala/io/cloudstate/javasupport/impl/crud/AnnotationBasedCrudSupportSpec.scala index 6428cf1a9..679537cba 100644 --- a/java-support/src/test/scala/io/cloudstate/javasupport/impl/crud/AnnotationBasedCrudSupportSpec.scala +++ b/java-support/src/test/scala/io/cloudstate/javasupport/impl/crud/AnnotationBasedCrudSupportSpec.scala @@ -29,11 +29,11 @@ class AnnotationBasedCrudSupportSpec extends WordSpec with Matchers { } class MockCommandContext extends CommandContext with BaseContext { - var emited = Seq.empty[AnyRef] + var emitted = Seq.empty[AnyRef] override def sequenceNumber(): Long = 10 override def commandName(): String = "AddItem" override def commandId(): Long = 20 - override def emit(event: AnyRef): Unit = emited :+= event + override def emit(event: AnyRef): Unit = emitted :+= event override def entityId(): String = "foo" override def fail(errorMessage: String): RuntimeException = ??? override def forward(to: ServiceCall): Unit = ??? @@ -55,6 +55,7 @@ class AnnotationBasedCrudSupportSpec extends WordSpec with Matchers { } case class Wrapped(value: String) + val anySupport = new AnySupport(Array(Shoppingcart.getDescriptor), this.getClass.getClassLoader) val descriptor = Shoppingcart.getDescriptor .findServiceByName("ShoppingCart") @@ -78,7 +79,7 @@ class AnnotationBasedCrudSupportSpec extends WordSpec with Matchers { WrappedResolvedType.parseFrom(any.getValue) } - def event(any: Any): JavaPbAny = anySupport.encodeJava(any) + def state(any: Any): JavaPbAny = anySupport.encodeJava(any) "Crud annotation support" should { "support entity construction" when { @@ -127,7 +128,7 @@ class AnnotationBasedCrudSupportSpec extends WordSpec with Matchers { val handler = create( new { @CommandHandler - def addItem(msg: String, @EntityId eid: String, ctx: CommandContext) = { + def addItem(msg: String, @EntityId eid: String, ctx: CommandContext): Wrapped = { eid should ===("foo") ctx.commandName() should ===("AddItem") Wrapped(msg) @@ -138,18 +139,21 @@ class AnnotationBasedCrudSupportSpec extends WordSpec with Matchers { decodeWrapped(handler.handleCommand(command("blah"), new MockCommandContext).get) should ===(Wrapped("blah")) } - "allow emiting events" in { - val handler = create(new { - @CommandHandler - def addItem(msg: String, ctx: CommandContext) = { - ctx.emit(msg + " event") - ctx.commandName() should ===("AddItem") - Wrapped(msg) - } - }, method) + "allow emitting events" in { + val handler = create( + new { + @CommandHandler + def addItem(msg: String, ctx: CommandContext): Wrapped = { + ctx.emit(msg + " event") + ctx.commandName() should ===("AddItem") + Wrapped(msg) + } + }, + method + ) val ctx = new MockCommandContext decodeWrapped(handler.handleCommand(command("blah"), ctx).get) should ===(Wrapped("blah")) - ctx.emited should ===(Seq("blah event")) + ctx.emitted should ===(Seq("blah event")) } "fail if there's a bad context type" in { @@ -210,12 +214,12 @@ class AnnotationBasedCrudSupportSpec extends WordSpec with Matchers { var invoked = false val handler = create(new { @SnapshotHandler - def handleState(snapshot: String) = { - snapshot should ===("snap!") + def handleState(state: String): Unit = { + state should ===("snap!") invoked = true } }) - handler.handleState(event("snap!"), ctx) + handler.handleState(state("snap!"), ctx) invoked shouldBe true } @@ -223,20 +227,20 @@ class AnnotationBasedCrudSupportSpec extends WordSpec with Matchers { var invoked = false val handler = create(new { @SnapshotHandler - def handleState(snapshot: String, context: SnapshotContext) = { - snapshot should ===("snap!") + def handleState(state: String, context: SnapshotContext): Unit = { + state should ===("snap!") context.sequenceNumber() should ===(10) invoked = true } }) - handler.handleState(event("snap!"), ctx) + handler.handleState(state("snap!"), ctx) invoked shouldBe true } "fail if there's a bad context" in { a[RuntimeException] should be thrownBy create(new { @SnapshotHandler - def handleState(snapshot: String, context: CommandContext) = () + def handleState(state: String, context: CommandContext) = () }) } @@ -250,9 +254,9 @@ class AnnotationBasedCrudSupportSpec extends WordSpec with Matchers { "fail if there's no snapshot handler for the given type" in { val handler = create(new { @SnapshotHandler - def handleState(snapshot: Int) = () + def handleState(state: Int) = () }) - a[RuntimeException] should be thrownBy handler.handleState(event(10), ctx) + a[RuntimeException] should be thrownBy handler.handleState(state(10), ctx) } } diff --git a/protocols/example/crud/shoppingcart/shoppingcart.proto b/protocols/example/crud/shoppingcart/shoppingcart.proto index 86c2d6c3b..edde36331 100644 --- a/protocols/example/crud/shoppingcart/shoppingcart.proto +++ b/protocols/example/crud/shoppingcart/shoppingcart.proto @@ -4,7 +4,7 @@ syntax = "proto3"; import "google/protobuf/empty.proto"; import "cloudstate/entity_key.proto"; import "cloudstate/sub_entity_key.proto"; -import "cloudstate/eventing.proto"; +import "cloudstate/crud_command_type.proto"; import "google/api/annotations.proto"; import "google/api/http.proto"; import "google/api/httpbody.proto"; @@ -14,24 +14,30 @@ package com.example.crud.shoppingcart; option go_package = "tck/crudshoppingcart"; message AddLineItem { - string shopping_id = 1 [(.cloudstate.entity_key) = true]; - string user_id = 2 [(.cloudstate.sub_entity_key) = true]; - string product_id = 3; - string name = 4; - int32 quantity = 5; + string cart_id = 1 [(.cloudstate.entity_key) = true]; + string user_id = 2 [(.cloudstate.sub_entity_key) = true]; // do i need that for create? + string command_type = 3 [(.cloudstate.crud_command_type) = true]; + string product_id = 4; + string name = 5; + int32 quantity = 6; } message RemoveLineItem { - string user_id = 1 [(.cloudstate.entity_key) = true]; - string product_id = 2; + string cart_id = 1 [(.cloudstate.entity_key) = true]; + string user_id = 2 [(.cloudstate.sub_entity_key) = true]; + string command_type = 3 [(.cloudstate.crud_command_type) = true]; + string product_id = 4; } -message RemoveShoppingCart { - string user_id = 1 [(.cloudstate.entity_key) = true]; +message GetShoppingCart { + string cart_id = 1 [(.cloudstate.entity_key) = true]; + string user_id = 2 [(.cloudstate.sub_entity_key) = true]; + string command_type = 3 [(.cloudstate.crud_command_type) = true]; } -message GetShoppingCart { - string user_id = 1 [(.cloudstate.entity_key) = true]; +message GetAllCart { + string cart_id = 1 [(.cloudstate.entity_key) = true]; + string command_type = 2 [(.cloudstate.crud_command_type) = true]; } message LineItem { @@ -44,30 +50,43 @@ message Cart { repeated LineItem items = 1; } +message Carts { + repeated Cart carts = 1; +} + service ShoppingCart { rpc AddItem(AddLineItem) returns (google.protobuf.Empty) { option (google.api.http) = { - post: "/cart/{shopping_id}/{user_id}/items/add", + post: "/cart/{cart_id}/{user_id}/items/add", body: "*", }; - option (.cloudstate.eventing).in = "items"; } - rpc RemoveItem(RemoveLineItem) returns (google.protobuf.Empty) { - option (google.api.http).post = "/cart/{user_id}/items/{product_id}/remove"; + rpc UpdateItem(AddLineItem) returns (google.protobuf.Empty) { + option (google.api.http) = { + post: "/cart/{cart_id}/{user_id}/items/update", + body: "*", + }; } - rpc RemoveCart(RemoveShoppingCart) returns (google.protobuf.Empty) { - option (google.api.http).post = "/cart/{user_id}/remove"; + rpc RemoveItem(RemoveLineItem) returns (google.protobuf.Empty) { + option (google.api.http).post = "/cart/{cart_id}/{user_id}/items/{product_id}/remove"; } rpc GetCart(GetShoppingCart) returns (Cart) { option (google.api.http) = { - get: "/carts/{user_id}", + get: "/carts/{cart_id}/{user_id}", additional_bindings: { - get: "/carts/{user_id}/items", + get: "/carts/{cart_id}/{user_id}/items", response_body: "items" } }; } + + rpc GetAll(GetAllCart) returns (Carts) { + option (google.api.http) = { + get: "/carts/{cart_id}", + response_body: "items" + }; + } } diff --git a/protocols/frontend/cloudstate/key_value.proto b/protocols/frontend/cloudstate/key_value.proto deleted file mode 100644 index 18bd46f10..000000000 --- a/protocols/frontend/cloudstate/key_value.proto +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright 2019 Lightbend Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -syntax = "proto3"; - -import "google/protobuf/any.proto"; - -package cloudstate; - -option java_package = "io.cloudstate.keyvalue"; - -message KVEntity { - map entries = 1; -} - -message KVModification { - map updated_entries = 1; - repeated string removed_keys = 2; -} diff --git a/protocols/protocol/cloudstate/crud.proto b/protocols/protocol/cloudstate/crud.proto index 799f23f11..1ee1b8866 100644 --- a/protocols/protocol/cloudstate/crud.proto +++ b/protocols/protocol/cloudstate/crud.proto @@ -111,7 +111,7 @@ message CrudReply { google.protobuf.Any snapshot = 5; } -// Missing better name. It will be fixed +// A reply message type for the gRPC call. message CrudReplyOut { oneof message { CrudReply reply = 1; @@ -120,15 +120,28 @@ message CrudReplyOut { } // The CRUD Entity service +// Provides read and write operations for managing the entity state. +// For a CRUD entity an event represents also the whole state of the entity. +// A read operation only transport the state from the entity without changing it. +// Typical read operations are fetch and fetchAll. +// A typical write operation might transform the state of the entity and crate a new one. +// Typical write operations are create, save, and delete. +// Each write operation may generate zero or one event which is then sent to the entity. +// The entity is expected to apply these event to its state. service Crud { + // Create a sub entity. rpc create(CrudCommand) returns (CrudReplyOut) {} + // Fetch the state of a sub entity. rpc fetch(CrudCommand) returns (CrudReplyOut) {} + // Save a updated sub entity. rpc save(CrudCommand) returns (CrudReplyOut) {} + // Delete a sub entity. rpc delete(CrudCommand) returns (CrudReplyOut) {} + // Fetch the state of the whole entity. rpc fetchAll(CrudCommand) returns (CrudReplyOut) {} } diff --git a/proxy/core/src/test/proto/cloudstate/proxy/test/ShoppingCartCrudTest.proto b/proxy/core/src/test/proto/cloudstate/proxy/test/ShoppingCartCrudTest.proto index 1aa9d2fe5..d9913e527 100644 --- a/proxy/core/src/test/proto/cloudstate/proxy/test/ShoppingCartCrudTest.proto +++ b/proxy/core/src/test/proto/cloudstate/proxy/test/ShoppingCartCrudTest.proto @@ -14,7 +14,7 @@ package cloudstate.proxy.test.crud; option java_package = "io.cloudstate.proxy.test.crud"; message AddLineItem { - string shopping_id = 1 [(.cloudstate.entity_key) = true]; + string cart_id = 1 [(.cloudstate.entity_key) = true]; string user_id = 2 [(.cloudstate.sub_entity_key) = true]; string command_type = 3 [(.cloudstate.crud_command_type) = true]; string product_id = 4; @@ -23,12 +23,16 @@ message AddLineItem { } message RemoveLineItem { - string user_id = 1 [(.cloudstate.entity_key) = true]; - string product_id = 2; + string cart_id = 1 [(.cloudstate.entity_key) = true]; + string user_id = 2 [(.cloudstate.entity_key) = true]; + string command_type = 3 [(.cloudstate.crud_command_type) = true]; + string product_id = 4; } message GetShoppingCart { - string user_id = 1 [(.cloudstate.entity_key) = true]; + string cart_id = 1 [(.cloudstate.entity_key) = true]; + string user_id = 2 [(.cloudstate.entity_key) = true]; + string command_type = 3 [(.cloudstate.crud_command_type) = true]; } message LineItem { @@ -44,26 +48,33 @@ message Cart { service ShoppingCart { rpc AddItem(AddLineItem) returns (google.protobuf.Empty) { option (google.api.http) = { - post: "/cart/{user_id}/items/add", + post: "/cart/{cart_id}/{user_id}/items/add", + body: "*", + }; + } + + rpc UpdateItem(AddLineItem) returns (google.protobuf.Empty) { + option (google.api.http) = { + post: "/cart/{cart_id}/{user_id}/items/update", body: "*", }; } rpc RemoveItem(RemoveLineItem) returns (google.protobuf.Empty) { - option (google.api.http).post = "/cart/{user_id}/items/{product_id}/remove"; + option (google.api.http).post = "/cart/{cart_id}/{user_id}/items/{product_id}/remove"; } rpc GetCart(GetShoppingCart) returns (Cart) { option (google.api.http) = { - get: "/carts/{user_id}", + get: "/carts/{cart_id}/{user_id}", additional_bindings: [{ - get: "/carts/{user_id}/items", + get: "/carts/{cart_id}/{user_id}/items", response_body: "items" }] }; } rpc ShowCartPage(GetShoppingCart) returns (google.api.HttpBody) { - option (google.api.http).get = "/carts/{user_id}.html"; + option (google.api.http).get = "/carts/{cart_id}/{user_id}.html"; } } diff --git a/proxy/core/src/test/scala/io/cloudstate/proxy/EntityMethodDescriptorSpec.scala b/proxy/core/src/test/scala/io/cloudstate/proxy/EntityMethodDescriptorSpec.scala index dcb17c7f4..1b68fece1 100644 --- a/proxy/core/src/test/scala/io/cloudstate/proxy/EntityMethodDescriptorSpec.scala +++ b/proxy/core/src/test/scala/io/cloudstate/proxy/EntityMethodDescriptorSpec.scala @@ -16,22 +16,22 @@ class EntityMethodDescriptorSpec extends WordSpecLike with Matchers { "extract entity key" in { val subEntityKey = entityMethodDescriptor.extractId( - AddLineItem("shoppingId", "userId", "productId", "name").toByteString + AddLineItem("cartId", "userId", "productId", "name").toByteString ) - subEntityKey should ===("shoppingId") + subEntityKey should ===("cartId") } "extract crud sub entity key" in { val subEntityKey = entityMethodDescriptor.extractCrudSubEntityId( - AddLineItem("shoppingId", "userId", "productId", "name").toByteString + AddLineItem("cartId", "userId", "productId", "name").toByteString ) subEntityKey should ===("userId") } "extract crud command type" in { val commandType = entityMethodDescriptor.extractCrudCommandType( - AddLineItem("shoppingId", "userId", "create", "productId", "name").toByteString + AddLineItem("cartId", "userId", "create", "productId", "name").toByteString ) commandType should ===("create") } diff --git a/samples/java-shopping-cart/src/main/java/io/cloudstate/samples/shoppingcart/Main.java b/samples/java-shopping-cart/src/main/java/io/cloudstate/samples/shoppingcart/Main.java index 794405818..71bc5cb78 100644 --- a/samples/java-shopping-cart/src/main/java/io/cloudstate/samples/shoppingcart/Main.java +++ b/samples/java-shopping-cart/src/main/java/io/cloudstate/samples/shoppingcart/Main.java @@ -9,8 +9,7 @@ public static final void main(String[] args) throws Exception { .registerCrudEntity( ShoppingCartCrudEntity.class, Shoppingcart.getDescriptor().findServiceByName("ShoppingCart"), - com.example.crud.shoppingcart.persistence.Domain.getDescriptor(), - io.cloudstate.keyvalue.KeyValue.getDescriptor()) + com.example.crud.shoppingcart.persistence.Domain.getDescriptor()) .start() .toCompletableFuture() .get(); diff --git a/samples/java-shopping-cart/src/main/java/io/cloudstate/samples/shoppingcart/ShoppingCartCrudEntity.java b/samples/java-shopping-cart/src/main/java/io/cloudstate/samples/shoppingcart/ShoppingCartCrudEntity.java index e10ae2f9e..aff210603 100644 --- a/samples/java-shopping-cart/src/main/java/io/cloudstate/samples/shoppingcart/ShoppingCartCrudEntity.java +++ b/samples/java-shopping-cart/src/main/java/io/cloudstate/samples/shoppingcart/ShoppingCartCrudEntity.java @@ -49,7 +49,38 @@ public Empty addItem(Shoppingcart.AddLineItem item, CommandContext ctx) { .setQuantity(item.getQuantity()) .build(); ctx.emit(Domain.Cart.newBuilder().addItems(lineItem).build()); - return Empty.getDefaultInstance(); + return Empty.getDefaultInstance(); // FIXME change return type + } + + @CommandHandler + public Empty updateItem(Shoppingcart.AddLineItem item, CommandContext ctx) { + if (item.getQuantity() <= 0) { + ctx.fail("Cannot add negative quantity of to item" + item.getProductId()); + } + + Domain.LineItem lineItem = + Domain.LineItem.newBuilder() + .setUserId(item.getUserId()) + .setProductId(item.getProductId()) + .setName(item.getName()) + .setQuantity(item.getQuantity()) + .build(); + ctx.emit(Domain.Cart.newBuilder().addItems(lineItem).build()); + return Empty.getDefaultInstance(); // FIXME change return type + } + + @CommandHandler + public Empty removeItem(Shoppingcart.RemoveLineItem item, CommandContext ctx) { + if (!cart.containsKey(item.getProductId())) { + ctx.fail("Cannot remove item " + item.getProductId() + " because it is not in the cart."); + } + cart.remove(item.getProductId()); + + Domain.Cart.Builder builder = Domain.Cart.newBuilder(); + cart.entrySet().stream() + .forEach(entry -> builder.addItems(convert(entry.getKey(), entry.getValue()))); + ctx.emit(builder.build()); + return Empty.getDefaultInstance(); // FIXME change return type } private Shoppingcart.LineItem convert(Domain.LineItem item) { @@ -59,4 +90,13 @@ private Shoppingcart.LineItem convert(Domain.LineItem item) { .setQuantity(item.getQuantity()) .build(); } + + private Domain.LineItem convert(String userId, Shoppingcart.LineItem item) { + return Domain.LineItem.newBuilder() + .setUserId(userId) + .setProductId(item.getProductId()) + .setName(item.getName()) + .setQuantity(item.getQuantity()) + .build(); + } } From 344d8d9a4050997c85d4a4028933cbc0ba021116 Mon Sep 17 00:00:00 2001 From: Guy Youansi Date: Wed, 10 Jun 2020 01:53:19 +0200 Subject: [PATCH 14/93] Documents the protocol and add new grpc type for command type. implements the use case update, delete and fetch. --- .../javasupport/impl/crud/CrudImpl.scala | 29 +++-- .../crud/shoppingcart/shoppingcart.proto | 58 +++------- protocols/protocol/cloudstate/crud.proto | 100 +++++++++++------- .../proxy/UserFunctionTypeSupport.scala | 25 ++++- .../io/cloudstate/proxy/crud/CrudEntity.scala | 46 ++++---- .../proxy/crud/CrudSupportFactory.scala | 30 ++++-- .../shoppingcart/ShoppingCartCrudEntity.java | 65 ++++++------ 7 files changed, 195 insertions(+), 158 deletions(-) diff --git a/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/CrudImpl.scala b/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/CrudImpl.scala index 7aeca61a2..3eaaa74aa 100644 --- a/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/CrudImpl.scala +++ b/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/CrudImpl.scala @@ -16,6 +16,7 @@ package io.cloudstate.javasupport.impl.crud import java.util.Optional +import java.util.concurrent.atomic.AtomicBoolean import akka.actor.ActorSystem import com.google.protobuf.Descriptors @@ -69,6 +70,7 @@ final class CrudImpl(_system: ActorSystem, override def create(command: CrudCommand): Future[CrudReplyOut] = Future.unit.map(_ => handleWriteCommand(command)) + // it is a read operation, the state and the snapshot are not mandatory for the reply. override def fetch(command: CrudCommand): Future[CrudReplyOut] = Future.unit .map { _ => @@ -81,9 +83,7 @@ final class CrudImpl(_system: ActorSystem, CrudReply( command.id, clientAction, - context.sideEffects, - runner.event(command.id), - runner.snapshot() + context.sideEffects ) ) ) @@ -92,20 +92,17 @@ final class CrudImpl(_system: ActorSystem, CrudReplyOut.Message.Reply( CrudReply( commandId = command.id, - clientAction = clientAction, - state = runner.event(command.id) + clientAction = clientAction ) ) ) } } - override def save(command: CrudCommand): Future[CrudReplyOut] = Future.unit.map(_ => handleWriteCommand(command)) + override def update(command: CrudCommand): Future[CrudReplyOut] = Future.unit.map(_ => handleWriteCommand(command)) override def delete(command: CrudCommand): Future[CrudReplyOut] = Future.unit.map(_ => handleWriteCommand(command)) - override def fetchAll(command: CrudCommand): Future[CrudReplyOut] = ??? - private def handleWriteCommand(command: CrudCommand): CrudReplyOut = { runner.handleState(command) val (reply, context) = runner.handleCommand(command) @@ -146,13 +143,13 @@ final class CrudImpl(_system: ActorSystem, import com.google.protobuf.{Any => JavaPbAny} import com.google.protobuf.any.{Any => ScalaPbAny} - private final var handlerInit = false + private final val handlerInit = new AtomicBoolean(false) private final var service: CrudStatefulService = _ private final var handler: CrudEntityHandler = _ private final var sequenceNumber: Long = 0 private final var performSnapshot: Boolean = false - // map command id to corresponding state changes + // map command id to corresponding state changes. this can be used for replies private final var events = Map.empty[Long, ScalaPbAny] def handleCommand(command: CrudCommand): (Optional[JavaPbAny], CommandContextImpl) = { @@ -173,7 +170,6 @@ final class CrudImpl(_system: ActorSystem, def handleState(command: CrudCommand): Unit = { maybeInitHandler(command) - val context = new SnapshotContextImpl(command.entityId, sequenceNumber) command.state.map(s => handler.handleState(ScalaPbAny.toJavaProto(s.payload.get), context)) } @@ -194,14 +190,16 @@ final class CrudImpl(_system: ActorSystem, sequenceNumber = sequenceNumber + events.size } - // the snapshot of the entity which is the last state changes so far + // entity snapshot which is the last state changes so far def snapshot(): Option[ScalaPbAny] = if (performSnapshot) { val (_, lastEvent) = events.last Some(lastEvent) - } else None + } else { + None + } - // the state changes associated with the command id + // state changes associated with the command id def event(commandId: Long): Option[ScalaPbAny] = { val e = events.get(commandId) events -= commandId // remove the event for the command id @@ -209,12 +207,11 @@ final class CrudImpl(_system: ActorSystem, } private def maybeInitHandler(command: CrudCommand): Unit = - if (!handlerInit) { + if (handlerInit.compareAndSet(false, true)) { service = services.getOrElse(command.serviceName, throw new RuntimeException(s"Service not found: ${command.serviceName}")) handler = service.factory.create(new CrudContextImpl(command.entityId)) sequenceNumber = command.state.map(_.snapshotSequence).getOrElse(0L) - handlerInit = true } } diff --git a/protocols/example/crud/shoppingcart/shoppingcart.proto b/protocols/example/crud/shoppingcart/shoppingcart.proto index edde36331..b88d1fc20 100644 --- a/protocols/example/crud/shoppingcart/shoppingcart.proto +++ b/protocols/example/crud/shoppingcart/shoppingcart.proto @@ -3,7 +3,6 @@ syntax = "proto3"; import "google/protobuf/empty.proto"; import "cloudstate/entity_key.proto"; -import "cloudstate/sub_entity_key.proto"; import "cloudstate/crud_command_type.proto"; import "google/api/annotations.proto"; import "google/api/http.proto"; @@ -14,30 +13,22 @@ package com.example.crud.shoppingcart; option go_package = "tck/crudshoppingcart"; message AddLineItem { - string cart_id = 1 [(.cloudstate.entity_key) = true]; - string user_id = 2 [(.cloudstate.sub_entity_key) = true]; // do i need that for create? - string command_type = 3 [(.cloudstate.crud_command_type) = true]; - string product_id = 4; - string name = 5; - int32 quantity = 6; + string user_id = 1 [(.cloudstate.entity_key) = true]; + string command_type = 2 [(.cloudstate.crud_command_type) = true]; + string product_id = 3; + string name = 4; + int32 quantity = 5; } message RemoveLineItem { - string cart_id = 1 [(.cloudstate.entity_key) = true]; - string user_id = 2 [(.cloudstate.sub_entity_key) = true]; - string command_type = 3 [(.cloudstate.crud_command_type) = true]; - string product_id = 4; + string user_id = 1 [(.cloudstate.entity_key) = true]; + //string command_type = 2 [(.cloudstate.crud_command_type) = true]; // what could be a better option to use it here? + string product_id = 2; } message GetShoppingCart { - string cart_id = 1 [(.cloudstate.entity_key) = true]; - string user_id = 2 [(.cloudstate.sub_entity_key) = true]; - string command_type = 3 [(.cloudstate.crud_command_type) = true]; -} - -message GetAllCart { - string cart_id = 1 [(.cloudstate.entity_key) = true]; - string command_type = 2 [(.cloudstate.crud_command_type) = true]; + string user_id = 1 [(.cloudstate.entity_key) = true]; + //string command_type = 2 [(.cloudstate.crud_command_type) = true]; // what could be a better option to use it here? } message LineItem { @@ -50,43 +41,28 @@ message Cart { repeated LineItem items = 1; } -message Carts { - repeated Cart carts = 1; -} - service ShoppingCart { + // CRUD Update rpc AddItem(AddLineItem) returns (google.protobuf.Empty) { option (google.api.http) = { - post: "/cart/{cart_id}/{user_id}/items/add", - body: "*", - }; - } - - rpc UpdateItem(AddLineItem) returns (google.protobuf.Empty) { - option (google.api.http) = { - post: "/cart/{cart_id}/{user_id}/items/update", + post: "/cart/{user_id}/items/add", body: "*", }; } + // CRUD Delete rpc RemoveItem(RemoveLineItem) returns (google.protobuf.Empty) { - option (google.api.http).post = "/cart/{cart_id}/{user_id}/items/{product_id}/remove"; + option (google.api.http).post = "/cart/{user_id}/items/{product_id}/remove"; } + // CRUD Fetch rpc GetCart(GetShoppingCart) returns (Cart) { option (google.api.http) = { - get: "/carts/{cart_id}/{user_id}", + get: "/carts/{user_id}", additional_bindings: { - get: "/carts/{cart_id}/{user_id}/items", + get: "/carts/{user_id}/items", response_body: "items" } }; } - - rpc GetAll(GetAllCart) returns (Carts) { - option (google.api.http) = { - get: "/carts/{cart_id}", - response_body: "items" - }; - } } diff --git a/protocols/protocol/cloudstate/crud.proto b/protocols/protocol/cloudstate/crud.proto index 1ee1b8866..3627853f8 100644 --- a/protocols/protocol/cloudstate/crud.proto +++ b/protocols/protocol/cloudstate/crud.proto @@ -27,14 +27,23 @@ import "cloudstate/entity.proto"; option java_package = "io.cloudstate.protocol"; option go_package = "cloudstate/protocol"; -// The type of the command to be executed -enum CrudCommandType { - UNKNOWN = 0; - CREATE = 1; - FETCH = 2; - FETCHALL = 3; - UPDATE = 4; - DELETE = 5; +message CreateCommand { +} +message FetchCommand { +} +message UpdateCommand { +} +message DeleteCommand { +} + +// Type of command supported +message CrudCommandType { + oneof command { + CreateCommand create = 1; + FetchCommand fetch = 2; + UpdateCommand update = 3; + DeleteCommand delete = 4; + } } // The persisted state with the sequence number of the last snapshot @@ -46,27 +55,27 @@ message CrudState { int64 snapshot_sequence = 1; } -// Message for initiating the command execution -// which contains the command type to be able to identify the crud operation being called +// Message for initiating the command execution. +// The message contains the command type to be able to identify the crud operation being called message CrudEntityCommand { // The ID of the entity. string entity_id = 1; // The ID of a sub entity. - string sub_entity_id = 2; + //string sub_entity_id = 2; // Command name - string name = 3; + string name = 2; // The command payload. - google.protobuf.Any payload = 4; + google.protobuf.Any payload = 3; // The command type. - CrudCommandType type = 5; + CrudCommandType type = 4; } -// The command to be executed which can be for any of the supported -// (create, fetch, save, delete, fetchAll) crud operations. +// The command to be executed. +// The supported crud operations are create, fetch, update, delete. message CrudCommand { // The name of the service this crud entity is on. string service_name = 1; @@ -86,7 +95,7 @@ message CrudCommand { // The command payload. google.protobuf.Any payload = 6; - // The persisted state to be conveyed between persistent entity and the user function. + // The persisted state to be conveyed between the CRUD entity and the user function. CrudState state = 7; } @@ -105,9 +114,9 @@ message CrudReply { // An optional state to persist. google.protobuf.Any state = 4; - // An optional snapshot to persist. It is assumed that this snapshot will have - // the state of any events in the events field applied to it. It is illegal to - // send a snapshot without sending any events. + // An optional snapshot to persist. It is assumed that this snapshot represents any state in the state field + // persisted before. It is illegal to send a snapshot without sending any events. + // Note that the state overrides the snapshot and the state is not applied to the snapshot. google.protobuf.Any snapshot = 5; } @@ -119,29 +128,46 @@ message CrudReplyOut { } } -// The CRUD Entity service -// Provides read and write operations for managing the entity state. -// For a CRUD entity an event represents also the whole state of the entity. -// A read operation only transport the state from the entity without changing it. -// Typical read operations are fetch and fetchAll. -// A typical write operation might transform the state of the entity and crate a new one. -// Typical write operations are create, save, and delete. -// Each write operation may generate zero or one event which is then sent to the entity. -// The entity is expected to apply these event to its state. +// CRUD Protocol +// +// Each operation sent across this protocol has among others information the state of the CRUD entity. +// The Cloudstate proxy is responsible to load the state, if any exists, from the CRUD entity and to pass it to +// the user function which holds it in memory to handle the operation being executed. The Cloudstate proxy updates +// the state in the user function successfully before executing the operation. The Cloudstate proxy executes one +// operation waits for the result and replies before executing the next operation, the operations are executed in order. +// This way the state is always in sync in the user function. Write operations could emit new state and +// read operations should not. The CRUD entity is backed by an event sourced entity, it means emitted state is an +// event sourced event. +// +// For each operation the first message sent to the CRUD entity is CrudEntityCommand which contains the entity ID and +// the command type to know which operation to call. The CrudEntityCommand is mapped to CrudCommand which contains the +// entity ID, the state and the snapshot sequence number. The state exists if the entity has previously persisted a +// state. It is the same with the snapshot sequence number. Once an operation is called the CrudCommand is passed to it. +// Each operation returns a reply message CrudReplyOut to each CrudCommand. The reply message contains the new state +// and the snapshot to be persisted. For CrudReplyOut the state exists if the user function emits a state change. +// Snapshot for CrudReplyOut exists if the snapshot configuration is fulfilled. The CRUD entity is expected to reply to +// each CrudEntityCommand. +// +// The user function is not responsible for updating its state in memory, it should rather emit the new state to +// the Cloudstate proxy. The Cloudstate proxy is responsible to pass the new state to the CRUD entity, +// so that it can be applied. The Cloudstate proxy loads the persisted state for subsequents operations. +// The user function could emit new state for write operations. +// +// The service could not been initialized before an operation is executed. In this case this operation will init +// the service. Each operation always check if the service is initialized. +// +// service Crud { - // Create a sub entity. + // This operation creates a sub entity. rpc create(CrudCommand) returns (CrudReplyOut) {} - // Fetch the state of a sub entity. + // This operation fetches the CRUD entity or a sub entity. rpc fetch(CrudCommand) returns (CrudReplyOut) {} - // Save a updated sub entity. - rpc save(CrudCommand) returns (CrudReplyOut) {} + // This operation updates the the CRUD entity or a sub entity. + rpc update(CrudCommand) returns (CrudReplyOut) {} - // Delete a sub entity. + // This operation deletes a sub entity. rpc delete(CrudCommand) returns (CrudReplyOut) {} - - // Fetch the state of the whole entity. - rpc fetchAll(CrudCommand) returns (CrudReplyOut) {} } diff --git a/proxy/core/src/main/scala/io/cloudstate/proxy/UserFunctionTypeSupport.scala b/proxy/core/src/main/scala/io/cloudstate/proxy/UserFunctionTypeSupport.scala index 6da5bf4ab..fd89fdcf7 100644 --- a/proxy/core/src/main/scala/io/cloudstate/proxy/UserFunctionTypeSupport.scala +++ b/proxy/core/src/main/scala/io/cloudstate/proxy/UserFunctionTypeSupport.scala @@ -2,12 +2,14 @@ package io.cloudstate.proxy import akka.NotUsed import akka.stream.scaladsl.Flow +import com.google.api.annotations.AnnotationsProto import com.google.protobuf.Descriptors.{MethodDescriptor, ServiceDescriptor} import com.google.protobuf.descriptor.FieldOptions import com.google.protobuf.{ByteString, Descriptors, DynamicMessage} import io.cloudstate.crud_command_type.CrudCommandTypeProto import io.cloudstate.entity_key.EntityKeyProto import io.cloudstate.protocol.entity.Entity +import io.cloudstate.proxy.EntityMethodDescriptor.CrudCommandOptionValue import io.cloudstate.proxy.entity.{EntityCommand, UserFunctionCommand, UserFunctionReply} import io.cloudstate.proxy.protobuf.Options import io.cloudstate.sub_entity_key.SubEntityKeyProto @@ -53,6 +55,16 @@ abstract class EntityTypeSupportFactory extends UserFunctionTypeSupportFactory { private object EntityMethodDescriptor { final val Separator = "-" + + // define the options type supported for CRUD commands + object CrudCommandOptionValue { + final val CREATE = "create" + final val FETCH = "fetch" + final val UPDATE = "update" + final val DELETE = "delete" + final val UNKNOWN = "unknown" // the command is not supported + } + } final class EntityMethodDescriptor(val method: MethodDescriptor) { @@ -76,7 +88,18 @@ final class EntityMethodDescriptor(val method: MethodDescriptor) { def extractCrudSubEntityId(bytes: ByteString): String = extract(crudSubEntityKeyFields, bytes) - def extractCrudCommandType(bytes: ByteString): String = extract(crudCommandTypeFields, bytes) + def extractCrudCommandType(bytes: ByteString): String = + // FIXME hack for checking if the method is a http method without payload like GET, DELETE and POST. + AnnotationsProto.http.get(Options.convertMethodOptions(method)) match { + case Some(rule) if rule.pattern.isGet => CrudCommandOptionValue.FETCH + case Some(rule) if rule.pattern.isDelete => CrudCommandOptionValue.DELETE + case Some(rule) if rule.pattern.isPost && rule.body == "" => CrudCommandOptionValue.DELETE + case Some(_) => + extract(crudCommandTypeFields, bytes) match { + case "" => CrudCommandOptionValue.UNKNOWN + case other => other + } + } private def extract(fieldOptions: Array[Descriptors.FieldDescriptor], bytes: ByteString): String = fieldOptions.length match { diff --git a/proxy/core/src/main/scala/io/cloudstate/proxy/crud/CrudEntity.scala b/proxy/core/src/main/scala/io/cloudstate/proxy/crud/CrudEntity.scala index 0910aa57c..060f6f9e4 100644 --- a/proxy/core/src/main/scala/io/cloudstate/proxy/crud/CrudEntity.scala +++ b/proxy/core/src/main/scala/io/cloudstate/proxy/crud/CrudEntity.scala @@ -65,14 +65,10 @@ final class CrudEntitySupervisor(client: CrudClient, import CrudEntitySupervisor._ - override final def receive: Receive = PartialFunction.empty - - override final def preStart(): Unit = { + override final def preStart(): Unit = self ! Start - context.become(waitingForRelay) - } - private[this] final def waitingForRelay: Receive = { + override final def receive: Receive = { case Start => // Cluster sharding URL encodes entity ids, so to extract it we need to decode. val entityId = URLDecoder.decode(self.path.name, "utf-8") @@ -82,6 +78,7 @@ final class CrudEntitySupervisor(client: CrudClient, ) context.become(forwarding(manager)) unstashAll() + case _ => stash() } @@ -114,10 +111,10 @@ object CrudEntity { replyTo: ActorRef ) - // The entity state + // Represents the entity state with the sequence number fo the last snapshot private final case class InternalState( - payload: pbAny, // the state payload - sequenceNumber: Long // the sequence number of the last taken snapshot + payload: pbAny, + sequenceNumber: Long ) final def props(configuration: Configuration, @@ -220,27 +217,24 @@ final class CrudEntity(configuration: CrudEntity.Configuration, commandStartTime = System.nanoTime() concurrencyEnforcer ! Action( currentCommand.actionId, - () => handleCommand(command, entityCommand.`type`) + () => handleCrudCommand(command, entityCommand.`type`) ) } - private[this] final def handleCommand(command: CrudCommand, commandType: CrudCommandType): Unit = + private[this] final def handleCrudCommand(command: CrudCommand, commandType: Option[CrudCommandType]): Unit = { + import CrudCommandType.{Command => CrudCmd} commandType match { - case CrudCommandType.CREATE => - client.create(command) pipeTo self - - case CrudCommandType.FETCH => - client.fetch(command) pipeTo self - - case CrudCommandType.UPDATE => - client.save(command) pipeTo self - - case CrudCommandType.DELETE => - client.delete(command) pipeTo self - - case CrudCommandType.FETCHALL => - client.fetchAll(command) pipeTo self + case Some(cmdType) => + cmdType.command match { + case CrudCmd.Create(_) => client.create(command) pipeTo self + case CrudCmd.Fetch(_) => client.fetch(command) pipeTo self + case CrudCmd.Update(_) => client.update(command) pipeTo self + case CrudCmd.Delete(_) => client.delete(command) pipeTo self + } + + case _ => // Nothing to do, commandType should not be None } + } private final def esReplyToUfReply(reply: CrudReply): UserFunctionReply = UserFunctionReply( @@ -266,7 +260,7 @@ final class CrudEntity(configuration: CrudEntity.Configuration, // the sequence number of the last taken snapshot private[this] final def sequenceNumber(): Long = state.map(_.sequenceNumber).getOrElse(0L) - override final def receiveCommand: PartialFunction[Any, Unit] = { + override final def receiveCommand: Receive = { case command: CrudEntityCommand if currentCommand != null => stashedCommands = stashedCommands.enqueue((command, sender())) diff --git a/proxy/core/src/main/scala/io/cloudstate/proxy/crud/CrudSupportFactory.scala b/proxy/core/src/main/scala/io/cloudstate/proxy/crud/CrudSupportFactory.scala index be63287d0..a72d65a34 100644 --- a/proxy/core/src/main/scala/io/cloudstate/proxy/crud/CrudSupportFactory.scala +++ b/proxy/core/src/main/scala/io/cloudstate/proxy/crud/CrudSupportFactory.scala @@ -11,8 +11,17 @@ import akka.stream.scaladsl.Flow import akka.util.Timeout import com.google.protobuf.ByteString import com.google.protobuf.Descriptors.ServiceDescriptor -import io.cloudstate.protocol.crud.{CrudClient, CrudCommandType, CrudEntityCommand} +import io.cloudstate.protocol.crud.{ + CreateCommand, + CrudClient, + CrudCommandType, + CrudEntityCommand, + DeleteCommand, + FetchCommand, + UpdateCommand +} import io.cloudstate.protocol.entity.Entity +import io.cloudstate.proxy.EntityMethodDescriptor.CrudCommandOptionValue import io.cloudstate.proxy._ import io.cloudstate.proxy.entity.{EntityCommand, UserFunctionReply} import io.cloudstate.proxy.eventsourced.DynamicLeastShardAllocationStrategy @@ -74,6 +83,9 @@ class CrudSupportFactory(system: ActorSystem, ) } + // FIXME crudSubEntityKey should be removed + // FIXME crudCommandType should be check only for method with payload like GET and DELETE + /* val methodsWithoutSubEntityKeys = methodDescriptors.values.filter(_.crudSubEntityKeyFieldsCount < 1) if (methodsWithoutSubEntityKeys.nonEmpty) { val offendingMethods = methodsWithoutSubEntityKeys.map(_.method.getName).mkString(",") @@ -91,6 +103,7 @@ class CrudSupportFactory(system: ActorSystem, |"but ${serviceDescriptor.getFullName} has the following methods without keys: ${offendingMethods}""".stripMargin ) } + */ } } @@ -101,13 +114,11 @@ private class CrudSupport(eventSourcedEntity: ActorRef, parallelism: Int, privat override def handler(method: EntityMethodDescriptor): Flow[EntityCommand, UserFunctionReply, NotUsed] = Flow[EntityCommand].mapAsync(parallelism) { command => - val subEntityId = method.extractCrudSubEntityId(command.payload.fold(ByteString.EMPTY)(_.value)) val commandType = extractCommandType(method, command) val initCommand = CrudEntityCommand(entityId = command.entityId, - subEntityId = subEntityId, name = command.name, payload = command.payload, - `type` = commandType) + `type` = Some(commandType)) (eventSourcedEntity ? initCommand).mapTo[UserFunctionReply] } @@ -116,9 +127,14 @@ private class CrudSupport(eventSourcedEntity: ActorRef, parallelism: Int, privat private def extractCommandType(method: EntityMethodDescriptor, command: EntityCommand): CrudCommandType = { val commandType = method.extractCrudCommandType(command.payload.fold(ByteString.EMPTY)(_.value)) - CrudCommandType.fromName(commandType.toUpperCase).getOrElse { - // cannot be empty here because the check is made in the validate method of CrudSupportFactory - throw new RuntimeException(s"Command - ${command.name} for CRUD entity should have a command type") + commandType match { + case CrudCommandOptionValue.CREATE => CrudCommandType.of(CrudCommandType.Command.Create(CreateCommand())) + case CrudCommandOptionValue.FETCH => CrudCommandType.of(CrudCommandType.Command.Fetch(FetchCommand())) + case CrudCommandOptionValue.UPDATE => CrudCommandType.of(CrudCommandType.Command.Update(UpdateCommand())) + case CrudCommandOptionValue.DELETE => CrudCommandType.of(CrudCommandType.Command.Delete(DeleteCommand())) + case CrudCommandOptionValue.UNKNOWN => + // cannot be empty here because the check is made in the validate method of CrudSupportFactory + throw new RuntimeException(s"Command - ${command.name} for CRUD entity should have a command type") } } } diff --git a/samples/java-shopping-cart/src/main/java/io/cloudstate/samples/shoppingcart/ShoppingCartCrudEntity.java b/samples/java-shopping-cart/src/main/java/io/cloudstate/samples/shoppingcart/ShoppingCartCrudEntity.java index aff210603..d6eb8ec71 100644 --- a/samples/java-shopping-cart/src/main/java/io/cloudstate/samples/shoppingcart/ShoppingCartCrudEntity.java +++ b/samples/java-shopping-cart/src/main/java/io/cloudstate/samples/shoppingcart/ShoppingCartCrudEntity.java @@ -11,6 +11,7 @@ import java.util.LinkedHashMap; import java.util.Map; +import java.util.stream.Collectors; /** A crud entity. */ @CrudEntity @@ -24,6 +25,7 @@ public ShoppingCartCrudEntity(@EntityId String entityId) { @SnapshotHandler public void handleState(Domain.Cart cart) { + // HINTS: this is called by the proxy for updating the state this.cart.clear(); for (Domain.LineItem item : cart.getItemsList()) { this.cart.put(item.getProductId(), convert(item)); @@ -37,50 +39,48 @@ public Shoppingcart.Cart getCart() { @CommandHandler public Empty addItem(Shoppingcart.AddLineItem item, CommandContext ctx) { + // HINTS: CRUD Update + // HINTS: curl -vi -X POST localhost:9000/cart/{user_id}/items/add -H "Content-Type: + // application/json" -d '{"commandType":"update", "productId":"foo","name":"A + // foo","quantity":10}' if (item.getQuantity() <= 0) { ctx.fail("Cannot add negative quantity of to item" + item.getProductId()); } Domain.LineItem lineItem = - Domain.LineItem.newBuilder() - .setUserId(item.getUserId()) - .setProductId(item.getProductId()) - .setName(item.getName()) - .setQuantity(item.getQuantity()) - .build(); - ctx.emit(Domain.Cart.newBuilder().addItems(lineItem).build()); - return Empty.getDefaultInstance(); // FIXME change return type - } - - @CommandHandler - public Empty updateItem(Shoppingcart.AddLineItem item, CommandContext ctx) { - if (item.getQuantity() <= 0) { - ctx.fail("Cannot add negative quantity of to item" + item.getProductId()); + cart.get(item.getProductId()) == null ? null : convert(cart.get(item.getProductId())); + if (lineItem == null) { + lineItem = + Domain.LineItem.newBuilder() + .setUserId(item.getUserId()) + .setProductId(item.getProductId()) + .setName(item.getName()) + .setQuantity(item.getQuantity()) + .build(); + } else { + lineItem = + lineItem.toBuilder().setQuantity(item.getQuantity() + lineItem.getQuantity()).build(); } + Domain.Cart cart = convert(this.cart).toBuilder().addItems(lineItem).build(); // new state + ctx.emit(cart); // emit new state - Domain.LineItem lineItem = - Domain.LineItem.newBuilder() - .setUserId(item.getUserId()) - .setProductId(item.getProductId()) - .setName(item.getName()) - .setQuantity(item.getQuantity()) - .build(); - ctx.emit(Domain.Cart.newBuilder().addItems(lineItem).build()); - return Empty.getDefaultInstance(); // FIXME change return type + return Empty.getDefaultInstance(); } @CommandHandler public Empty removeItem(Shoppingcart.RemoveLineItem item, CommandContext ctx) { + // HINTS: CRUD Delete + // HINTS: curl -vi -X POST localhost:9000/cart/{user_id}/items/{product_id}/remove -H + // "Content-Type: application/json" -d '{"commandType":"delete", "productId":"foo"}' + if (!cart.containsKey(item.getProductId())) { ctx.fail("Cannot remove item " + item.getProductId() + " because it is not in the cart."); } + cart.remove(item.getProductId()); + ctx.emit(convert(cart)); // emit the new state - Domain.Cart.Builder builder = Domain.Cart.newBuilder(); - cart.entrySet().stream() - .forEach(entry -> builder.addItems(convert(entry.getKey(), entry.getValue()))); - ctx.emit(builder.build()); - return Empty.getDefaultInstance(); // FIXME change return type + return Empty.getDefaultInstance(); } private Shoppingcart.LineItem convert(Domain.LineItem item) { @@ -91,12 +91,17 @@ private Shoppingcart.LineItem convert(Domain.LineItem item) { .build(); } - private Domain.LineItem convert(String userId, Shoppingcart.LineItem item) { + private Domain.LineItem convert(Shoppingcart.LineItem item) { return Domain.LineItem.newBuilder() - .setUserId(userId) .setProductId(item.getProductId()) .setName(item.getName()) .setQuantity(item.getQuantity()) .build(); } + + private Domain.Cart convert(Map cart) { + return Domain.Cart.newBuilder() + .addAllItems(cart.values().stream().map(this::convert).collect(Collectors.toList())) + .build(); + } } From d0215aa0608b908d43bebe58fe40c6bd02309fe5 Mon Sep 17 00:00:00 2001 From: Guy Youansi Date: Tue, 14 Jul 2020 18:35:36 +0200 Subject: [PATCH 15/93] Provide a definition of the protocol with snapshotting capabilities and implement it. the java support and the proxy entity are implemented. --- .../io/cloudstate/javasupport/CloudState.java | 106 ++++-- .../javasupport/crud/CommandContext.java | 35 +- .../javasupport/crud/CommandHandler.java | 16 + .../javasupport/crud/CrudContext.java | 18 +- .../javasupport/crud/CrudEntity.java | 18 +- .../crud/CrudEntityCreationContext.java | 18 +- .../javasupport/crud/CrudEntityFactory.java | 18 +- .../javasupport/crud/CrudEntityHandler.java | 22 +- .../javasupport/crud/SnapshotContext.java | 16 + .../javasupport/crud/SnapshotHandler.java | 16 + .../javasupport/crud/StateContext.java | 27 ++ .../javasupport/crud/StateHandler.java | 39 +++ .../javasupport/crud/package-info.java | 4 +- .../javasupport/CloudStateRunner.scala | 4 +- .../crud/AnnotationBasedCrudSupport.scala | 71 ++-- .../javasupport/impl/crud/CrudImpl.scala | 328 ++++++++++-------- .../crud/AnnotationBasedCrudSupportSpec.scala | 69 ++-- .../shoppingcart/persistence/domain.proto | 34 +- .../crud/shoppingcart/shoppingcart.proto | 47 ++- .../cloudstate/crud_command_type.proto | 30 -- .../frontend/cloudstate/sub_entity_key.proto | 30 -- protocols/protocol/cloudstate/crud.proto | 191 ++++------ .../io/cloudstate/proxy/crud/CrudEntity.scala | 246 +++++++------ .../proxy/crud/CrudSupportFactory.scala | 108 ++---- .../DynamicLeastShardAllocationStrategy.scala | 77 ++++ .../proxy/crud/InMemSnapshotStore.scala | 55 +++ .../proxy/test/ShoppingCartCrudTest.proto | 80 ----- .../proxy/EntityMethodDescriptorSpec.scala | 40 --- .../shoppingcart/ShoppingCartCrudEntity.java | 83 +++-- 29 files changed, 1070 insertions(+), 776 deletions(-) create mode 100644 java-support/src/main/java/io/cloudstate/javasupport/crud/StateContext.java create mode 100644 java-support/src/main/java/io/cloudstate/javasupport/crud/StateHandler.java delete mode 100644 protocols/frontend/cloudstate/crud_command_type.proto delete mode 100644 protocols/frontend/cloudstate/sub_entity_key.proto create mode 100644 proxy/core/src/main/scala/io/cloudstate/proxy/crud/DynamicLeastShardAllocationStrategy.scala create mode 100644 proxy/core/src/main/scala/io/cloudstate/proxy/crud/InMemSnapshotStore.scala delete mode 100644 proxy/core/src/test/proto/cloudstate/proxy/test/ShoppingCartCrudTest.proto delete mode 100644 proxy/core/src/test/scala/io/cloudstate/proxy/EntityMethodDescriptorSpec.scala diff --git a/java-support/src/main/java/io/cloudstate/javasupport/CloudState.java b/java-support/src/main/java/io/cloudstate/javasupport/CloudState.java index 4d496cf49..3271bdc7b 100644 --- a/java-support/src/main/java/io/cloudstate/javasupport/CloudState.java +++ b/java-support/src/main/java/io/cloudstate/javasupport/CloudState.java @@ -18,9 +18,10 @@ import com.typesafe.config.Config; import com.google.protobuf.Descriptors; +import io.cloudstate.javasupport.crud.CrudEntity; import io.cloudstate.javasupport.crdt.CrdtEntity; import io.cloudstate.javasupport.crdt.CrdtEntityFactory; -import io.cloudstate.javasupport.crud.CrudEntity; +import io.cloudstate.javasupport.crud.CrudEntityFactory; import io.cloudstate.javasupport.eventsourced.EventSourcedEntity; import io.cloudstate.javasupport.eventsourced.EventSourcedEntityFactory; import io.cloudstate.javasupport.impl.AnySupport; @@ -32,7 +33,6 @@ import io.cloudstate.javasupport.impl.eventsourced.EventSourcedStatefulService; import akka.Done; - import java.util.concurrent.CompletionStage; import java.util.HashMap; import java.util.Map; @@ -141,7 +141,7 @@ public CloudState registerEventSourcedEntity( } /** - * Register an event sourced entity factor. + * Register an event sourced entity factory. * *

This is a low level API intended for custom (eg, non reflection based) mechanisms for * implementing the entity. @@ -175,9 +175,9 @@ public CloudState registerEventSourcedEntity( } /** - * Register an annotated crud entity. + * Register an annotated CRDT entity. * - *

The entity class must be annotated with {@link io.cloudstate.javasupport.crud.CrudEntity}. + *

The entity class must be annotated with {@link io.cloudstate.javasupport.crdt.CrdtEntity}. * * @param entityClass The entity class. * @param descriptor The descriptor for the service that this entity implements. @@ -185,94 +185,128 @@ public CloudState registerEventSourcedEntity( * types when needed. * @return This stateful service builder. */ - public CloudState registerCrudEntity( + public CloudState registerCrdtEntity( Class entityClass, Descriptors.ServiceDescriptor descriptor, Descriptors.FileDescriptor... additionalDescriptors) { - CrudEntity entity = entityClass.getAnnotation(CrudEntity.class); + CrdtEntity entity = entityClass.getAnnotation(CrdtEntity.class); if (entity == null) { throw new IllegalArgumentException( - entityClass + " does not declare an " + CrudEntity.class + " annotation!"); - } - - final String persistenceId; - final int snapshotEvery; - if (entity.persistenceId().isEmpty()) { - persistenceId = entityClass.getSimpleName(); - snapshotEvery = 0; // Default - } else { - persistenceId = entity.persistenceId(); - snapshotEvery = entity.snapshotEvery(); + entityClass + " does not declare an " + CrdtEntity.class + " annotation!"); } final AnySupport anySupport = newAnySupport(additionalDescriptors); services.put( descriptor.getFullName(), - new CrudStatefulService( - new AnnotationBasedCrudSupport(entityClass, anySupport, descriptor), + new CrdtStatefulService( + new AnnotationBasedCrdtSupport(entityClass, anySupport, descriptor), descriptor, - anySupport, - persistenceId, - snapshotEvery)); + anySupport)); return this; } /** - * Register an annotated CRDT entity. + * Register an CRDT entity factory. * - *

The entity class must be annotated with {@link io.cloudstate.javasupport.crdt.CrdtEntity}. + *

This is a low level API intended for custom (eg, non reflection based) mechanisms for + * implementing the entity. * - * @param entityClass The entity class. + * @param factory The CRDT factory. * @param descriptor The descriptor for the service that this entity implements. * @param additionalDescriptors Any additional descriptors that should be used to look up protobuf * types when needed. * @return This stateful service builder. */ public CloudState registerCrdtEntity( + CrdtEntityFactory factory, + Descriptors.ServiceDescriptor descriptor, + Descriptors.FileDescriptor... additionalDescriptors) { + services.put( + descriptor.getFullName(), + new CrdtStatefulService(factory, descriptor, newAnySupport(additionalDescriptors))); + + return this; + } + + /** + * Register an annotated CRUD entity. + * + *

The entity class must be annotated with {@link io.cloudstate.javasupport.crud.CrudEntity}. + * + * @param entityClass The entity class. + * @param descriptor The descriptor for the service that this entity implements. + * @param additionalDescriptors Any additional descriptors that should be used to look up protobuf + * types when needed. + * @return This stateful service builder. + */ + public CloudState registerCrudEntity( Class entityClass, Descriptors.ServiceDescriptor descriptor, Descriptors.FileDescriptor... additionalDescriptors) { - CrdtEntity entity = entityClass.getAnnotation(CrdtEntity.class); + CrudEntity entity = entityClass.getAnnotation(CrudEntity.class); if (entity == null) { throw new IllegalArgumentException( - entityClass + " does not declare an " + CrdtEntity.class + " annotation!"); + entityClass + " does not declare an " + CrudEntity.class + " annotation!"); + } + + final String persistenceId; + final int snapshotEvery; + if (entity.persistenceId().isEmpty()) { + persistenceId = entityClass.getSimpleName(); + snapshotEvery = 0; // Default + } else { + persistenceId = entity.persistenceId(); + snapshotEvery = entity.snapshotEvery(); } final AnySupport anySupport = newAnySupport(additionalDescriptors); services.put( descriptor.getFullName(), - new CrdtStatefulService( - new AnnotationBasedCrdtSupport(entityClass, anySupport, descriptor), + new CrudStatefulService( + new AnnotationBasedCrudSupport(entityClass, anySupport, descriptor), descriptor, - anySupport)); + anySupport, + persistenceId, + snapshotEvery)); return this; } /** - * Register an CRDt entity factory. + * Register an CRUD entity factory. * *

This is a low level API intended for custom (eg, non reflection based) mechanisms for * implementing the entity. * - * @param factory The CRDT factory. + * @param factory The CRUD factory. * @param descriptor The descriptor for the service that this entity implements. + * @param persistenceId The persistence id for this entity. + * @param snapshotEvery Specifies how snapshots of the entity state should be made: Zero means use + * default from configuration file. (Default) Any negative value means never snapshot. Any + * positive value means snapshot at-or-after that number of events. * @param additionalDescriptors Any additional descriptors that should be used to look up protobuf * types when needed. * @return This stateful service builder. */ - public CloudState registerCrdtEntity( - CrdtEntityFactory factory, + public CloudState registerCrudEntity( + CrudEntityFactory factory, Descriptors.ServiceDescriptor descriptor, + String persistenceId, + int snapshotEvery, Descriptors.FileDescriptor... additionalDescriptors) { services.put( descriptor.getFullName(), - new CrdtStatefulService(factory, descriptor, newAnySupport(additionalDescriptors))); + new CrudStatefulService( + factory, + descriptor, + newAnySupport(additionalDescriptors), + persistenceId, + snapshotEvery)); return this; } diff --git a/java-support/src/main/java/io/cloudstate/javasupport/crud/CommandContext.java b/java-support/src/main/java/io/cloudstate/javasupport/crud/CommandContext.java index cf53a22a0..80d946068 100644 --- a/java-support/src/main/java/io/cloudstate/javasupport/crud/CommandContext.java +++ b/java-support/src/main/java/io/cloudstate/javasupport/crud/CommandContext.java @@ -1,18 +1,34 @@ +/* + * Copyright 2019 Lightbend Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package io.cloudstate.javasupport.crud; import io.cloudstate.javasupport.ClientActionContext; import io.cloudstate.javasupport.EffectContext; /** - * An crud command context. + * An CRUD command context. * - *

Methods annotated with {@link CommandHandler} may take this is a parameter. It allows emitting - * new events (which represents the new persistent state) in response to a command, along with - * forwarding the result to other entities, and performing side effects on other entities. + *

Methods annotated with {@link CommandHandler} may take this is a parameter. It allows updating + * or deleting the entity state in response to a command, along with forwarding the result to other + * entities, and performing side effects on other entities. */ public interface CommandContext extends CrudContext, ClientActionContext, EffectContext { /** - * The current sequence number of events in this entity. + * The current sequence number of state in this entity. * * @return The current sequence number. */ @@ -33,9 +49,12 @@ public interface CommandContext extends CrudContext, ClientActionContext, Effect long commandId(); /** - * Emit the given event which represents the new persistent state. The event will be persisted. + * Update the entity with the new state. The state will be persisted. * - * @param event The event to emit. + * @param state The state to persist. */ - void emit(Object event); + void update(Object state); + + /** Delete the entity. */ + void delete(); } diff --git a/java-support/src/main/java/io/cloudstate/javasupport/crud/CommandHandler.java b/java-support/src/main/java/io/cloudstate/javasupport/crud/CommandHandler.java index 46f1cbd5c..3bf20122b 100644 --- a/java-support/src/main/java/io/cloudstate/javasupport/crud/CommandHandler.java +++ b/java-support/src/main/java/io/cloudstate/javasupport/crud/CommandHandler.java @@ -1,3 +1,19 @@ +/* + * Copyright 2019 Lightbend Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package io.cloudstate.javasupport.crud; import io.cloudstate.javasupport.impl.CloudStateAnnotation; diff --git a/java-support/src/main/java/io/cloudstate/javasupport/crud/CrudContext.java b/java-support/src/main/java/io/cloudstate/javasupport/crud/CrudContext.java index 9affe36c7..b3551c7a6 100644 --- a/java-support/src/main/java/io/cloudstate/javasupport/crud/CrudContext.java +++ b/java-support/src/main/java/io/cloudstate/javasupport/crud/CrudContext.java @@ -1,6 +1,22 @@ +/* + * Copyright 2019 Lightbend Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package io.cloudstate.javasupport.crud; import io.cloudstate.javasupport.EntityContext; -/** Root context for all crud contexts. */ +/** Root context for all CRUD contexts. */ public interface CrudContext extends EntityContext {} diff --git a/java-support/src/main/java/io/cloudstate/javasupport/crud/CrudEntity.java b/java-support/src/main/java/io/cloudstate/javasupport/crud/CrudEntity.java index 1c6205f69..e4a8e6c18 100644 --- a/java-support/src/main/java/io/cloudstate/javasupport/crud/CrudEntity.java +++ b/java-support/src/main/java/io/cloudstate/javasupport/crud/CrudEntity.java @@ -1,3 +1,19 @@ +/* + * Copyright 2019 Lightbend Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package io.cloudstate.javasupport.crud; import io.cloudstate.javasupport.impl.CloudStateAnnotation; @@ -7,7 +23,7 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -/** An crud entity. */ +/** An CRUD entity. */ @CloudStateAnnotation @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) diff --git a/java-support/src/main/java/io/cloudstate/javasupport/crud/CrudEntityCreationContext.java b/java-support/src/main/java/io/cloudstate/javasupport/crud/CrudEntityCreationContext.java index bb83dfbbe..c4eb729df 100644 --- a/java-support/src/main/java/io/cloudstate/javasupport/crud/CrudEntityCreationContext.java +++ b/java-support/src/main/java/io/cloudstate/javasupport/crud/CrudEntityCreationContext.java @@ -1,8 +1,24 @@ +/* + * Copyright 2019 Lightbend Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package io.cloudstate.javasupport.crud; /** * Creation context for {@link CrudEntity} annotated entities. * - *

This may be accepted as an argument to the constructor of an crud entity. + *

This may be accepted as an argument to the constructor of an CRUD entity. */ public interface CrudEntityCreationContext extends CrudContext {} diff --git a/java-support/src/main/java/io/cloudstate/javasupport/crud/CrudEntityFactory.java b/java-support/src/main/java/io/cloudstate/javasupport/crud/CrudEntityFactory.java index 603fab452..6dd86a7d1 100644 --- a/java-support/src/main/java/io/cloudstate/javasupport/crud/CrudEntityFactory.java +++ b/java-support/src/main/java/io/cloudstate/javasupport/crud/CrudEntityFactory.java @@ -1,10 +1,26 @@ +/* + * Copyright 2019 Lightbend Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package io.cloudstate.javasupport.crud; import io.cloudstate.javasupport.eventsourced.CommandHandler; import io.cloudstate.javasupport.eventsourced.EventHandler; /** - * Low level interface for handling commands on an crud entity. + * Low level interface for handling commands on an CRUD entity. * *

Generally, this should not be needed, instead, a class annotated with the {@link * CommandHandler} and similar annotations should be used. diff --git a/java-support/src/main/java/io/cloudstate/javasupport/crud/CrudEntityHandler.java b/java-support/src/main/java/io/cloudstate/javasupport/crud/CrudEntityHandler.java index 558d379ba..036edfb31 100644 --- a/java-support/src/main/java/io/cloudstate/javasupport/crud/CrudEntityHandler.java +++ b/java-support/src/main/java/io/cloudstate/javasupport/crud/CrudEntityHandler.java @@ -1,3 +1,19 @@ +/* + * Copyright 2019 Lightbend Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package io.cloudstate.javasupport.crud; import com.google.protobuf.Any; @@ -6,10 +22,10 @@ /** * Low level interface for handling events (which represents the persistent state) and commands on - * an crud entity. + * an CRUD entity. * *

Generally, this should not be needed, instead, a class annotated with the {@link - * SnapshotHandler}, {@link CommandHandler} and similar annotations should be used. + * StateHandler}, {@link CommandHandler} and similar annotations should be used. */ public interface CrudEntityHandler { @@ -28,5 +44,5 @@ public interface CrudEntityHandler { * @param state The state to handle. * @param context The state context. */ - void handleState(Any state, SnapshotContext context); + void handleState(Optional state, StateContext context); } diff --git a/java-support/src/main/java/io/cloudstate/javasupport/crud/SnapshotContext.java b/java-support/src/main/java/io/cloudstate/javasupport/crud/SnapshotContext.java index d998369b7..6301bdc44 100644 --- a/java-support/src/main/java/io/cloudstate/javasupport/crud/SnapshotContext.java +++ b/java-support/src/main/java/io/cloudstate/javasupport/crud/SnapshotContext.java @@ -1,3 +1,19 @@ +/* + * Copyright 2019 Lightbend Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package io.cloudstate.javasupport.crud; /** A snapshot context. */ diff --git a/java-support/src/main/java/io/cloudstate/javasupport/crud/SnapshotHandler.java b/java-support/src/main/java/io/cloudstate/javasupport/crud/SnapshotHandler.java index e83e49e28..47e1d6b06 100644 --- a/java-support/src/main/java/io/cloudstate/javasupport/crud/SnapshotHandler.java +++ b/java-support/src/main/java/io/cloudstate/javasupport/crud/SnapshotHandler.java @@ -1,3 +1,19 @@ +/* + * Copyright 2019 Lightbend Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package io.cloudstate.javasupport.crud; import io.cloudstate.javasupport.impl.CloudStateAnnotation; diff --git a/java-support/src/main/java/io/cloudstate/javasupport/crud/StateContext.java b/java-support/src/main/java/io/cloudstate/javasupport/crud/StateContext.java new file mode 100644 index 000000000..6b3e2f4a7 --- /dev/null +++ b/java-support/src/main/java/io/cloudstate/javasupport/crud/StateContext.java @@ -0,0 +1,27 @@ +/* + * Copyright 2019 Lightbend Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.cloudstate.javasupport.crud; + +/** A state context. */ +public interface StateContext extends CrudContext { + /** + * The sequence number of this state. + * + * @return The sequence number. + */ + long sequenceNumber(); +} diff --git a/java-support/src/main/java/io/cloudstate/javasupport/crud/StateHandler.java b/java-support/src/main/java/io/cloudstate/javasupport/crud/StateHandler.java new file mode 100644 index 000000000..592180576 --- /dev/null +++ b/java-support/src/main/java/io/cloudstate/javasupport/crud/StateHandler.java @@ -0,0 +1,39 @@ +/* + * Copyright 2019 Lightbend Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.cloudstate.javasupport.crud; + +import io.cloudstate.javasupport.impl.CloudStateAnnotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Marks a method as a state handler. + * + *

If, when recovering an entity, that entity has a state, the state will be passed to a + * corresponding state handler method whose argument matches its type. The entity must set its + * current state to that state. + * + *

The state handler method may additionally accept a {@link StateContext} parameter, allowing it + * to access context for the state, if required. + */ +@CloudStateAnnotation +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface StateHandler {} diff --git a/java-support/src/main/java/io/cloudstate/javasupport/crud/package-info.java b/java-support/src/main/java/io/cloudstate/javasupport/crud/package-info.java index 6442ea7bf..191c48548 100644 --- a/java-support/src/main/java/io/cloudstate/javasupport/crud/package-info.java +++ b/java-support/src/main/java/io/cloudstate/javasupport/crud/package-info.java @@ -5,7 +5,7 @@ * io.cloudstate.javasupport.crud.CrudEntity @CrudEntity} annotation, and supply command handlers * using the {@link io.cloudstate.javasupport.crud.CommandHandler @CommandHandler} annotation. * - *

In addition, {@link io.cloudstate.javasupport.crud.SnapshotHandler @SnapshotHandler} annotated - * methods should be defined to handle snapshots. + *

In addition, {@link io.cloudstate.javasupport.crud.StateHandler @StateHandler} annotated + * methods should be defined to handle entity state. */ package io.cloudstate.javasupport.crud; diff --git a/java-support/src/main/scala/io/cloudstate/javasupport/CloudStateRunner.scala b/java-support/src/main/scala/io/cloudstate/javasupport/CloudStateRunner.scala index 9f1a54322..af32cfaa4 100644 --- a/java-support/src/main/scala/io/cloudstate/javasupport/CloudStateRunner.scala +++ b/java-support/src/main/scala/io/cloudstate/javasupport/CloudStateRunner.scala @@ -58,10 +58,10 @@ object CloudStateRunner { /** * The CloudStateRunner is responsible for handle the bootstrap of entities, - * and is used by [[io.cloudstate.javasupport.CloudState.start()]] to set up the local + * and is used by [[io.cloudstate.javasupport.CloudState#start()]] to set up the local * server with the given configuration. * - * CloudStateRunner can be seen as a low-level API for cases where [[io.cloudstate.javasupport.CloudState.start()]] isn't enough. + * CloudStateRunner can be seen as a low-level API for cases where [[io.cloudstate.javasupport.CloudState#start()]] isn't enough. */ final class CloudStateRunner private[this] (_system: ActorSystem, services: Map[String, StatefulService]) { private[this] implicit final val system = _system diff --git a/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/AnnotationBasedCrudSupport.scala b/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/AnnotationBasedCrudSupport.scala index 16b5a4c1e..f54e43f7b 100644 --- a/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/AnnotationBasedCrudSupport.scala +++ b/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/AnnotationBasedCrudSupport.scala @@ -1,3 +1,19 @@ +/* + * Copyright 2019 Lightbend Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package io.cloudstate.javasupport.impl.crud import java.lang.reflect.{Constructor, InvocationTargetException, Method} @@ -13,8 +29,8 @@ import io.cloudstate.javasupport.crud.{ CrudEntityCreationContext, CrudEntityFactory, CrudEntityHandler, - SnapshotContext, - SnapshotHandler + StateContext, + StateHandler } import io.cloudstate.javasupport.impl.ReflectionHelper.{InvocationContext, MainArgumentParameterHandler} import io.cloudstate.javasupport.impl.{AnySupport, ReflectionHelper, ResolvedEntityFactory, ResolvedServiceMethod} @@ -35,7 +51,7 @@ private[impl] class AnnotationBasedCrudSupport( def this(entityClass: Class[_], anySupport: AnySupport, serviceDescriptor: Descriptors.ServiceDescriptor) = this(entityClass, anySupport, anySupport.resolveServiceDescriptor(serviceDescriptor)) - private val behavior = EventBehaviorReflection(entityClass, resolvedMethods) + private val behavior = CrudBehaviorReflection(entityClass, resolvedMethods) private val constructor: CrudEntityCreationContext => AnyRef = factory.getOrElse { entityClass.getConstructors match { @@ -66,12 +82,13 @@ private[impl] class AnnotationBasedCrudSupport( } } - override def handleState(anyState: JavaPbAny, context: SnapshotContext): Unit = unwrap { - val state = anySupport.decode(anyState).asInstanceOf[AnyRef] + override def handleState(anyState: Optional[JavaPbAny], context: StateContext): Unit = unwrap { + import scala.compat.java8.OptionConverters._ + val state = anyState.asScala.map(s => anySupport.decode(s)).asJava.asInstanceOf[AnyRef] - behavior.getCachedSnapshotHandlerForClass(state.getClass) match { + behavior.getCachedStateHandlerForClass(state.getClass) match { case Some(handler) => - val ctx = new DelegatingCrudContext(context) with SnapshotContext { + val ctx = new DelegatingCrudContext(context) with StateContext { override def sequenceNumber(): Long = context.sequenceNumber() } handler.invoke(entity, state, ctx) @@ -99,19 +116,15 @@ private[impl] class AnnotationBasedCrudSupport( } } -private class EventBehaviorReflection( +private class CrudBehaviorReflection( val commandHandlers: Map[String, ReflectionHelper.CommandHandlerInvoker[CommandContext]], - snapshotHandlers: Map[Class[_], SnapshotHandlerInvoker] + val stateHandlers: Map[Class[_], StateHandlerInvoker] ) { - /** - * We use a cache in addition to the info we've discovered by reflection so that an event handler can be declared - * for a superclass of an event. - */ - private val snapshotHandlerCache = TrieMap.empty[Class[_], Option[SnapshotHandlerInvoker]] + private val stateHandlerCache = TrieMap.empty[Class[_], Option[StateHandlerInvoker]] - def getCachedSnapshotHandlerForClass(clazz: Class[_]): Option[SnapshotHandlerInvoker] = - snapshotHandlerCache.getOrElseUpdate(clazz, getHandlerForClass(snapshotHandlers)(clazz)) + def getCachedStateHandlerForClass(clazz: Class[_]): Option[StateHandlerInvoker] = + stateHandlerCache.getOrElseUpdate(clazz, getHandlerForClass(stateHandlers)(clazz)) private def getHandlerForClass[T](handlers: Map[Class[_], T])(clazz: Class[_]): Option[T] = handlers.get(clazz) match { @@ -125,9 +138,9 @@ private class EventBehaviorReflection( } } -private object EventBehaviorReflection { +private object CrudBehaviorReflection { def apply(behaviorClass: Class[_], - serviceMethods: Map[String, ResolvedServiceMethod[_, _]]): EventBehaviorReflection = { + serviceMethods: Map[String, ResolvedServiceMethod[_, _]]): CrudBehaviorReflection = { val allMethods = ReflectionHelper.getAllDeclaredMethods(behaviorClass) val commandHandlers = allMethods @@ -156,10 +169,10 @@ private object EventBehaviorReflection { ) } - val snapshotHandlers = allMethods - .filter(_.getAnnotation(classOf[SnapshotHandler]) != null) + val stateHandlers = allMethods + .filter(_.getAnnotation(classOf[StateHandler]) != null) .map { method => - new SnapshotHandlerInvoker(ReflectionHelper.ensureAccessible(method)) + new StateHandlerInvoker(ReflectionHelper.ensureAccessible(method)) } .groupBy(_.snapshotClass) .map { @@ -169,15 +182,15 @@ private object EventBehaviorReflection { s"Multiple methods found for handling snapshot of type $clazz: ${many.map(_.method.getName)}" ) } - .asInstanceOf[Map[Class[_], SnapshotHandlerInvoker]] + .asInstanceOf[Map[Class[_], StateHandlerInvoker]] ReflectionHelper.validateNoBadMethods( allMethods, classOf[CrudEntity], - Set(classOf[CommandHandler], classOf[SnapshotHandler]) + Set(classOf[CommandHandler], classOf[StateHandler]) ) - new EventBehaviorReflection(commandHandlers, snapshotHandlers) + new CrudBehaviorReflection(commandHandlers, stateHandlers) } } @@ -195,22 +208,22 @@ private class EntityConstructorInvoker(constructor: Constructor[_]) extends (Cru } } -private class SnapshotHandlerInvoker(val method: Method) { - private val parameters = ReflectionHelper.getParameterHandlers[SnapshotContext](method)() +private class StateHandlerInvoker(val method: Method) { + private val parameters = ReflectionHelper.getParameterHandlers[StateContext](method)() - // Verify that there is at most one event handler + // Verify that there is at most one state handler val snapshotClass: Class[_] = parameters.collect { case MainArgumentParameterHandler(clazz) => clazz } match { case Array(handlerClass) => handlerClass case other => throw new RuntimeException( - s"SnapshotHandler method $method must defined at most one non context parameter to handle snapshots, the parameters defined were: ${other + s"StateHandler method $method must defined at most one non context parameter to handle state, the parameters defined were: ${other .mkString(",")}" ) } - def invoke(obj: AnyRef, snapshot: AnyRef, context: SnapshotContext): Unit = { + def invoke(obj: AnyRef, snapshot: AnyRef, context: StateContext): Unit = { val ctx = InvocationContext(snapshot, context) method.invoke(obj, parameters.map(_.apply(ctx)): _*) } diff --git a/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/CrudImpl.scala b/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/CrudImpl.scala index 3eaaa74aa..0f1b6c550 100644 --- a/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/CrudImpl.scala +++ b/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/CrudImpl.scala @@ -1,5 +1,5 @@ /* - * Copyright 2020 Lightbend Inc. + * Copyright 2019 Lightbend Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,20 +13,24 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.cloudstate.javasupport.impl.crud -import java.util.Optional -import java.util.concurrent.atomic.AtomicBoolean +package io.cloudstate.javasupport.impl.crud +import akka.NotUsed import akka.actor.ActorSystem -import com.google.protobuf.Descriptors +import akka.stream.scaladsl.{Flow, Source} import io.cloudstate.javasupport.CloudStateRunner.Configuration +import com.google.protobuf.{Descriptors, Any => JavaPbAny} +import com.google.protobuf.any.{Any => ScalaPbAny} import io.cloudstate.javasupport.crud._ import io.cloudstate.javasupport.impl._ import io.cloudstate.javasupport.{Context, ServiceCallFactory, StatefulService} +import io.cloudstate.protocol.crud.CrudAction.Action.{Delete, Update} import io.cloudstate.protocol.crud._ +import io.cloudstate.protocol.crud.CrudStreamIn.Message.{Command => InCommand, Empty => InEmpty, Init => InInit} +import io.cloudstate.protocol.entity.Failure -import scala.concurrent.Future +import scala.compat.java8.OptionConverters._ final class CrudStatefulService(val factory: CrudEntityFactory, override val descriptor: Descriptors.ServiceDescriptor, @@ -41,7 +45,7 @@ final class CrudStatefulService(val factory: CrudEntityFactory, case _ => None } - override final val entityType = Crud.name + override final val entityType = io.cloudstate.protocol.crud.Crud.name final def withSnapshotEvery(snapshotEvery: Int): CrudStatefulService = if (snapshotEvery != this.snapshotEvery) @@ -54,7 +58,7 @@ final class CrudImpl(_system: ActorSystem, _services: Map[String, CrudStatefulService], rootContext: Context, configuration: Configuration) - extends Crud { + extends io.cloudstate.protocol.crud.Crud { private final val system = _system private final implicit val ec = system.dispatcher @@ -66,152 +70,140 @@ final class CrudImpl(_system: ActorSystem, }) .toMap - private final val runner = new EntityHandlerRunner() - - override def create(command: CrudCommand): Future[CrudReplyOut] = Future.unit.map(_ => handleWriteCommand(command)) - - // it is a read operation, the state and the snapshot are not mandatory for the reply. - override def fetch(command: CrudCommand): Future[CrudReplyOut] = - Future.unit - .map { _ => - runner.handleState(command) - val (reply, context) = runner.handleCommand(command) - val clientAction = context.createClientAction(reply, false) - if (!context.hasError) { - CrudReplyOut( - CrudReplyOut.Message.Reply( - CrudReply( - command.id, - clientAction, - context.sideEffects + override def handle( + in: akka.stream.scaladsl.Source[CrudStreamIn, akka.NotUsed] + ): akka.stream.scaladsl.Source[CrudStreamOut, akka.NotUsed] = + in.prefixAndTail(1) + .flatMapConcat { + case (Seq(CrudStreamIn(InInit(init), _)), source) => + source.via(runEntity(init)) + case _ => + Source.single( + CrudStreamOut( + CrudStreamOut.Message.Failure( + Failure( + 0, + "Cloudstate protocol failure for CRUD entity: expected init message" + ) ) ) ) - } else { - CrudReplyOut( - CrudReplyOut.Message.Reply( - CrudReply( - commandId = command.id, - clientAction = clientAction + } + .recover { + case e => + system.log.error(e, "Unexpected error, terminating CRUD Entity") + CrudStreamOut( + CrudStreamOut.Message.Failure( + Failure( + 0, + s"Cloudstate protocol failure for CRUD entity: ${e.getMessage}" ) ) ) - } } - override def update(command: CrudCommand): Future[CrudReplyOut] = Future.unit.map(_ => handleWriteCommand(command)) + private def runEntity(init: CrudInit): Flow[CrudStreamIn, CrudStreamOut, NotUsed] = { + val service = + services.getOrElse(init.serviceName, throw new RuntimeException(s"Service not found: ${init.serviceName}")) + val handler = service.factory.create(new CrudContextImpl(init.entityId)) + val thisEntityId = init.entityId - override def delete(command: CrudCommand): Future[CrudReplyOut] = Future.unit.map(_ => handleWriteCommand(command)) + val (startingSequenceNumber, state) = init.state match { + case Some(CrudInitState(Some(payload), stateSequence, _)) => + val encoded = service.anySupport.encodeScala(payload) + (stateSequence, Some(ScalaPbAny.toJavaProto(encoded)).asJava) - private def handleWriteCommand(command: CrudCommand): CrudReplyOut = { - runner.handleState(command) - val (reply, context) = runner.handleCommand(command) - val clientAction = context.createClientAction(reply, false) - runner.endSequenceNumber(context.hasError) + case Some(CrudInitState(None, stateSequence, _)) => (stateSequence, Option.empty[JavaPbAny].asJava) - if (!context.hasError) { - CrudReplyOut( - CrudReplyOut.Message.Reply( - CrudReply( + case None => (0L, Option.empty[JavaPbAny].asJava) + } + handler.handleState(state, new StateContextImpl(thisEntityId, startingSequenceNumber)) + + Flow[CrudStreamIn] + .map(_.message) + .scan[(Long, Option[CrudStreamOut.Message])]((startingSequenceNumber, None)) { + case ((sequence, _), InCommand(command)) if thisEntityId != command.entityId => + (sequence, + Some( + CrudStreamOut.Message.Failure( + Failure( + command.id, + s"""Cloudstate protocol failure for CRUD entity: + |Receiving entity - $thisEntityId is not the intended recipient + |of command with id - ${command.id} and name - ${command.name}""".stripMargin.replaceAll("\n", + " ") + ) + ) + )) + + case ((sequence, _), InCommand(command)) if command.payload.isEmpty => + (sequence, + Some( + CrudStreamOut.Message.Failure( + Failure( + command.id, + s"Cloudstate protocol failure for CRUD entity: Command (id: ${command.id}, name: ${command.name}) should have a payload" + ) + ) + )) + + case ((sequence, _), InCommand(command)) => + val cmd = ScalaPbAny.toJavaProto(command.payload.get) + val context = new CommandContextImpl( + thisEntityId, + sequence, + command.name, command.id, - clientAction, - context.sideEffects, - runner.event(command.id), - runner.snapshot() - ) - ) - ) - } else { - CrudReplyOut( - CrudReplyOut.Message.Reply( - CrudReply( - commandId = command.id, - clientAction = clientAction, - state = runner.event(command.id) + service.anySupport, + handler, + service.snapshotEvery ) - ) - ) - } - } - - /* - * Represents a wrapper for the crud service and crud entity handler. - * It creates the service and entity handler once depending on the first command which starts the flow. - * It also deals with snapshotting of events and the deactivation of the command context. - */ - private final class EntityHandlerRunner() { - import com.google.protobuf.{Any => JavaPbAny} - import com.google.protobuf.any.{Any => ScalaPbAny} - - private final val handlerInit = new AtomicBoolean(false) - private final var service: CrudStatefulService = _ - private final var handler: CrudEntityHandler = _ - - private final var sequenceNumber: Long = 0 - private final var performSnapshot: Boolean = false - // map command id to corresponding state changes. this can be used for replies - private final var events = Map.empty[Long, ScalaPbAny] - - def handleCommand(command: CrudCommand): (Optional[JavaPbAny], CommandContextImpl) = { - maybeInitHandler(command) - - val context = new CommandContextImpl(command.entityId, sequenceNumber, command.name, command.id, this) - try { - command.payload - .map(p => (handler.handleCommand(ScalaPbAny.toJavaProto(p), context), context)) - .getOrElse((Optional.empty[JavaPbAny](), context)) //FIXME payload empty should throw an exception or not? - } catch { - case FailInvoked => - (Optional.empty[JavaPbAny](), context) - } finally { - context.deactivate() - } - } - - def handleState(command: CrudCommand): Unit = { - maybeInitHandler(command) - val context = new SnapshotContextImpl(command.entityId, sequenceNumber) - command.state.map(s => handler.handleState(ScalaPbAny.toJavaProto(s.payload.get), context)) - } - - def emit(event: AnyRef, context: CommandContext): Unit = { - val encoded = service.anySupport.encodeScala(event) - val nextSequenceNumber = context.sequenceNumber() + events.size + 1 - handler.handleState(ScalaPbAny.toJavaProto(encoded), - new SnapshotContextImpl(context.entityId, nextSequenceNumber)) - - events += (context.commandId() -> encoded) - performSnapshot = (service.snapshotEvery > 0) && (performSnapshot || (nextSequenceNumber % service.snapshotEvery == 0)) - } - - // update the sequence number of the entity - def endSequenceNumber(hasError: Boolean): Unit = - if (!hasError) { - sequenceNumber = sequenceNumber + events.size - } - - // entity snapshot which is the last state changes so far - def snapshot(): Option[ScalaPbAny] = - if (performSnapshot) { - val (_, lastEvent) = events.last - Some(lastEvent) - } else { - None + val reply = try { + handler.handleCommand(cmd, context) + } catch { + case FailInvoked => Option.empty[JavaPbAny].asJava + } finally { + context.deactivate() // Very important! + } + + val clientAction = context.createClientAction(reply, false) + if (!context.hasError) { + val endSequenceNumber = context.nextSequenceNumber + val snapshot = if (context.performSnapshot) context.snapshot() else None + + (endSequenceNumber, + Some( + CrudStreamOut.Message.Reply( + CrudReply( + command.id, + clientAction, + context.sideEffects, + context.crudAction(), + snapshot + ) + ) + )) + } else { + (sequence, + Some( + CrudStreamOut.Message.Reply( + CrudReply( + commandId = command.id, + clientAction = clientAction, + crudAction = context.crudAction() + ) + ) + )) + } + + case (_, InInit(_)) => + throw new IllegalStateException("CRUD Entity already inited") + + case (_, InEmpty) => + throw new IllegalStateException("CRUD Entity received empty/unknown message") } - - // state changes associated with the command id - def event(commandId: Long): Option[ScalaPbAny] = { - val e = events.get(commandId) - events -= commandId // remove the event for the command id - e - } - - private def maybeInitHandler(command: CrudCommand): Unit = - if (handlerInit.compareAndSet(false, true)) { - service = services.getOrElse(command.serviceName, - throw new RuntimeException(s"Service not found: ${command.serviceName}")) - handler = service.factory.create(new CrudContextImpl(command.entityId)) - sequenceNumber = command.state.map(_.snapshotSequence).getOrElse(0L) + .collect { + case (_, Some(message)) => CrudStreamOut(message) } } @@ -223,20 +215,66 @@ final class CrudImpl(_system: ActorSystem, override val sequenceNumber: Long, override val commandName: String, override val commandId: Long, - private val runner: EntityHandlerRunner) + val anySupport: AnySupport, + val handler: CrudEntityHandler, + val snapshotEvery: Int) extends CommandContext with AbstractContext with AbstractClientActionContext with AbstractEffectContext with ActivatableContext { - override def emit(event: AnyRef): Unit = - runner.emit(event, this) + private var _performSnapshot: Boolean = false + private var _nextSequenceNumber: Long = sequenceNumber + private var mayBeAction: Option[CrudAction] = None + + override def update(event: AnyRef): Unit = { + checkActive() + + val encoded = anySupport.encodeScala(event) + _nextSequenceNumber += 1 + handler.handleState(Some(ScalaPbAny.toJavaProto(encoded)).asJava, + new StateContextImpl(entityId, _nextSequenceNumber)) + mayBeAction = Some(CrudAction(Update(CrudUpdate(Some(encoded))))) + updatePerformSnapshot() + } + + override def delete(): Unit = { + checkActive() + + _nextSequenceNumber += 1 + handler.handleState(Option.empty[JavaPbAny].asJava, new StateContextImpl(entityId, _nextSequenceNumber)) + mayBeAction = Some(CrudAction(Delete(CrudDelete()))) + updatePerformSnapshot() + } + + def performSnapshot: Boolean = _performSnapshot + + def nextSequenceNumber: Long = _nextSequenceNumber + + def crudAction(): Option[CrudAction] = mayBeAction + + def snapshot(): Option[CrudSnapshot] = + mayBeAction match { + case Some(CrudAction(action, _)) => + action match { + case Update(CrudUpdate(Some(value), _)) => Some(CrudSnapshot(Some(value))) + case Delete(CrudDelete(_)) => Some(CrudSnapshot(None)) + } + case None => + system.log.error( + s"Cloudstate protocol failure for CRUD entity: making a snapshot without performing a crud action for commandId: $commandId and commandName: $commandName" + ) + throw new IllegalStateException("CRUD Entity received snapshot in wrong state") + } + + private def updatePerformSnapshot(): Unit = + _performSnapshot = (snapshotEvery > 0) && (_performSnapshot || (_nextSequenceNumber % snapshotEvery == 0)) } private final class CrudContextImpl(override final val entityId: String) extends CrudContext with AbstractContext - private final class SnapshotContextImpl(override final val entityId: String, override final val sequenceNumber: Long) - extends SnapshotContext + private final class StateContextImpl(override final val entityId: String, override val sequenceNumber: Long) + extends StateContext with AbstractContext } diff --git a/java-support/src/test/scala/io/cloudstate/javasupport/impl/crud/AnnotationBasedCrudSupportSpec.scala b/java-support/src/test/scala/io/cloudstate/javasupport/impl/crud/AnnotationBasedCrudSupportSpec.scala index 679537cba..496506692 100644 --- a/java-support/src/test/scala/io/cloudstate/javasupport/impl/crud/AnnotationBasedCrudSupportSpec.scala +++ b/java-support/src/test/scala/io/cloudstate/javasupport/impl/crud/AnnotationBasedCrudSupportSpec.scala @@ -1,5 +1,23 @@ +/* + * Copyright 2019 Lightbend Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package io.cloudstate.javasupport.impl.crud +import java.util.Optional + import com.example.crud.shoppingcart.Shoppingcart import com.google.protobuf.any.{Any => ScalaPbAny} import com.google.protobuf.{ByteString, Any => JavaPbAny} @@ -9,13 +27,15 @@ import io.cloudstate.javasupport.crud.{ CrudContext, CrudEntity, CrudEntityCreationContext, - SnapshotContext, - SnapshotHandler + StateContext, + StateHandler } import io.cloudstate.javasupport.impl.{AnySupport, ResolvedServiceMethod, ResolvedType} import io.cloudstate.javasupport.{Context, EntityId, ServiceCall, ServiceCallFactory, ServiceCallRef} import org.scalatest.{Matchers, WordSpec} +import scala.compat.java8.OptionConverters._ + class AnnotationBasedCrudSupportSpec extends WordSpec with Matchers { trait BaseContext extends Context { override def serviceCallFactory(): ServiceCallFactory = new ServiceCallFactory { @@ -29,11 +49,12 @@ class AnnotationBasedCrudSupportSpec extends WordSpec with Matchers { } class MockCommandContext extends CommandContext with BaseContext { - var emitted = Seq.empty[AnyRef] + var action: Option[AnyRef] = None override def sequenceNumber(): Long = 10 override def commandName(): String = "AddItem" override def commandId(): Long = 20 - override def emit(event: AnyRef): Unit = emitted :+= event + override def update(state: AnyRef): Unit = action = Some(state) + override def delete(): Unit = action = None override def entityId(): String = "foo" override def fail(errorMessage: String): RuntimeException = ??? override def forward(to: ServiceCall): Unit = ??? @@ -139,12 +160,12 @@ class AnnotationBasedCrudSupportSpec extends WordSpec with Matchers { decodeWrapped(handler.handleCommand(command("blah"), new MockCommandContext).get) should ===(Wrapped("blah")) } - "allow emitting events" in { + "allow updating the state" in { val handler = create( new { @CommandHandler def addItem(msg: String, ctx: CommandContext): Wrapped = { - ctx.emit(msg + " event") + ctx.update(msg + " event") ctx.commandName() should ===("AddItem") Wrapped(msg) } @@ -153,13 +174,13 @@ class AnnotationBasedCrudSupportSpec extends WordSpec with Matchers { ) val ctx = new MockCommandContext decodeWrapped(handler.handleCommand(command("blah"), ctx).get) should ===(Wrapped("blah")) - ctx.emitted should ===(Seq("blah event")) + ctx.action should ===(Some("blah event")) } "fail if there's a bad context type" in { a[RuntimeException] should be thrownBy create(new { @CommandHandler - def addItem(msg: String, ctx: SnapshotContext) = + def addItem(msg: String, ctx: StateContext) = Wrapped(msg) }, method) } @@ -205,7 +226,7 @@ class AnnotationBasedCrudSupportSpec extends WordSpec with Matchers { } "support state handlers" when { - val ctx = new SnapshotContext with BaseContext { + val ctx = new StateContext with BaseContext { override def sequenceNumber(): Long = 10 override def entityId(): String = "foo" } @@ -213,50 +234,50 @@ class AnnotationBasedCrudSupportSpec extends WordSpec with Matchers { "single parameter" in { var invoked = false val handler = create(new { - @SnapshotHandler - def handleState(state: String): Unit = { - state should ===("snap!") + @StateHandler + def handleState(state: Optional[String]): Unit = { + state should ===(Option("state!").asJava) invoked = true } }) - handler.handleState(state("snap!"), ctx) + handler.handleState(Option(state("state!")).asJava, ctx) invoked shouldBe true } "context parameter" in { var invoked = false val handler = create(new { - @SnapshotHandler - def handleState(state: String, context: SnapshotContext): Unit = { - state should ===("snap!") + @StateHandler + def handleState(state: Optional[String], context: StateContext): Unit = { + state should ===(Option("state!").asJava) context.sequenceNumber() should ===(10) invoked = true } }) - handler.handleState(state("snap!"), ctx) + handler.handleState(Option(state("state!")).asJava, ctx) invoked shouldBe true } "fail if there's a bad context" in { a[RuntimeException] should be thrownBy create(new { - @SnapshotHandler - def handleState(state: String, context: CommandContext) = () + @StateHandler + def handleState(state: Optional[String], context: CommandContext) = () }) } "fail if there's no snapshot parameter" in { a[RuntimeException] should be thrownBy create(new { - @SnapshotHandler - def handleState(context: SnapshotContext) = () + @StateHandler + def handleState(context: StateContext) = () }) } - "fail if there's no snapshot handler for the given type" in { + "fail if there's no state handler for the given type" in { val handler = create(new { - @SnapshotHandler + @StateHandler def handleState(state: Int) = () }) - a[RuntimeException] should be thrownBy handler.handleState(state(10), ctx) + a[RuntimeException] should be thrownBy handler.handleState(Option(state(10)).asJava, ctx) } } diff --git a/protocols/example/crud/shoppingcart/persistence/domain.proto b/protocols/example/crud/shoppingcart/persistence/domain.proto index 2c93dfa27..65355cada 100644 --- a/protocols/example/crud/shoppingcart/persistence/domain.proto +++ b/protocols/example/crud/shoppingcart/persistence/domain.proto @@ -1,15 +1,38 @@ +// Copyright 2019 Lightbend Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + // These are the messages that get persisted - the events, plus the current state (Cart) for snapshots. -syntax = "proto3"; + syntax = "proto3"; package com.example.crud.shoppingcart.persistence; option go_package = "crud.shoppingcart.persistence"; message LineItem { - string userId = 1; - string productId = 2; - string name = 3; - int32 quantity = 4; + string productId = 1; + string name = 2; + int32 quantity = 3; +} + +// The item added event. +message ItemAdded { + LineItem item = 1; +} + +// The item removed event. +message ItemRemoved { + string productId = 1; } // The shopping cart state. @@ -17,3 +40,4 @@ message Cart { repeated LineItem items = 1; } + diff --git a/protocols/example/crud/shoppingcart/shoppingcart.proto b/protocols/example/crud/shoppingcart/shoppingcart.proto index b88d1fc20..02ca117c6 100644 --- a/protocols/example/crud/shoppingcart/shoppingcart.proto +++ b/protocols/example/crud/shoppingcart/shoppingcart.proto @@ -1,9 +1,23 @@ +// Copyright 2019 Lightbend Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + // This is the public API offered by the shopping cart entity. syntax = "proto3"; import "google/protobuf/empty.proto"; import "cloudstate/entity_key.proto"; -import "cloudstate/crud_command_type.proto"; +import "cloudstate/eventing.proto"; import "google/api/annotations.proto"; import "google/api/http.proto"; import "google/api/httpbody.proto"; @@ -14,21 +28,22 @@ option go_package = "tck/crudshoppingcart"; message AddLineItem { string user_id = 1 [(.cloudstate.entity_key) = true]; - string command_type = 2 [(.cloudstate.crud_command_type) = true]; - string product_id = 3; - string name = 4; - int32 quantity = 5; + string product_id = 2; + string name = 3; + int32 quantity = 4; } message RemoveLineItem { string user_id = 1 [(.cloudstate.entity_key) = true]; - //string command_type = 2 [(.cloudstate.crud_command_type) = true]; // what could be a better option to use it here? string product_id = 2; } message GetShoppingCart { string user_id = 1 [(.cloudstate.entity_key) = true]; - //string command_type = 2 [(.cloudstate.crud_command_type) = true]; // what could be a better option to use it here? +} + +message RemoveShoppingCart { + string user_id = 1 [(.cloudstate.entity_key) = true]; } message LineItem { @@ -42,27 +57,29 @@ message Cart { } service ShoppingCart { - // CRUD Update rpc AddItem(AddLineItem) returns (google.protobuf.Empty) { option (google.api.http) = { post: "/cart/{user_id}/items/add", body: "*", }; + option (.cloudstate.eventing).in = "items"; } - // CRUD Delete rpc RemoveItem(RemoveLineItem) returns (google.protobuf.Empty) { option (google.api.http).post = "/cart/{user_id}/items/{product_id}/remove"; } - // CRUD Fetch rpc GetCart(GetShoppingCart) returns (Cart) { option (google.api.http) = { - get: "/carts/{user_id}", - additional_bindings: { - get: "/carts/{user_id}/items", - response_body: "items" - } + get: "/carts/{user_id}", + additional_bindings: { + get: "/carts/{user_id}/items", + response_body: "items" + } }; } + + rpc RemoveCart(RemoveShoppingCart) returns (google.protobuf.Empty) { + option (google.api.http).post = "/carts/{user_id}/remove"; + } } diff --git a/protocols/frontend/cloudstate/crud_command_type.proto b/protocols/frontend/cloudstate/crud_command_type.proto deleted file mode 100644 index c2f6c89ee..000000000 --- a/protocols/frontend/cloudstate/crud_command_type.proto +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright 2019 Lightbend Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// Extension for specifying which field in a message is to be considered an -// entity key, for the purposes associating gRPC calls with entities and -// sharding. - -syntax = "proto3"; - -import "google/protobuf/descriptor.proto"; - -package cloudstate; - -option java_package = "io.cloudstate"; -option go_package = "github.com/cloudstateio/go-support/cloudstate/;cloudstate"; - -extend google.protobuf.FieldOptions { - bool crud_command_type = 50005; -} diff --git a/protocols/frontend/cloudstate/sub_entity_key.proto b/protocols/frontend/cloudstate/sub_entity_key.proto deleted file mode 100644 index 7a0130764..000000000 --- a/protocols/frontend/cloudstate/sub_entity_key.proto +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright 2019 Lightbend Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// Extension for specifying which field in a message is to be considered an -// entity key, for the purposes associating gRPC calls with entities and -// sharding. - -syntax = "proto3"; - -import "google/protobuf/descriptor.proto"; - -package cloudstate; - -option java_package = "io.cloudstate"; -option go_package = "github.com/cloudstateio/go-support/cloudstate/;cloudstate"; - -extend google.protobuf.FieldOptions { - bool sub_entity_key = 50004; -} diff --git a/protocols/protocol/cloudstate/crud.proto b/protocols/protocol/cloudstate/crud.proto index 3627853f8..befdb263e 100644 --- a/protocols/protocol/cloudstate/crud.proto +++ b/protocols/protocol/cloudstate/crud.proto @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -// gRPC interface for CRUD Entity user functions. +// gRPC interface for common messages and services for CRUD Entity user functions. syntax = "proto3"; @@ -21,153 +21,102 @@ package cloudstate.crud; // Any is used so that domain events defined according to the functions business domain can be embedded inside // the protocol. import "google/protobuf/any.proto"; -import "google/protobuf/empty.proto"; import "cloudstate/entity.proto"; option java_package = "io.cloudstate.protocol"; option go_package = "cloudstate/protocol"; -message CreateCommand { -} -message FetchCommand { -} -message UpdateCommand { -} -message DeleteCommand { -} +// The CRUD Entity service +service Crud { -// Type of command supported -message CrudCommandType { - oneof command { - CreateCommand create = 1; - FetchCommand fetch = 2; - UpdateCommand update = 3; - DeleteCommand delete = 4; - } + // One stream will be established per active entity. + // Once established, the first message sent will be Init, which contains the entity ID, and, + // a state if the entity has previously persisted one. The entity is expected to apply the + // received state to its state. Once the Init message is sent, one to many commands are sent, + // with new commands being sent as new requests for the entity come in. The entity is expected + // to reply to each command with exactly one reply message. The entity should reply in order + // and any state update that the entity requests to be persisted the entity should handle itself. + // The entity handles state update by replacing its own state with the update, + // as if they had arrived as state update when the stream was being replayed on load. + rpc handle(stream CrudStreamIn) returns (stream CrudStreamOut) {} } -// The persisted state with the sequence number of the last snapshot -message CrudState { - // The state payload - google.protobuf.Any payload = 2; - - // The sequence number when the snapshot was taken. - int64 snapshot_sequence = 1; +// Input message type for the gRPC stream in. +message CrudStreamIn { + oneof message { + CrudInit init = 1; + Command command = 2; + } } -// Message for initiating the command execution. -// The message contains the command type to be able to identify the crud operation being called -message CrudEntityCommand { - // The ID of the entity. - string entity_id = 1; - - // The ID of a sub entity. - //string sub_entity_id = 2; +// The init message. This will always be the first message sent to the entity when it is loaded. +message CrudInit { + // The name of the service that implements this entity. + string service_name = 1; - // Command name - string name = 2; + // The id of the entity. + string entity_id = 2; - // The command payload. - google.protobuf.Any payload = 3; - - // The command type. - CrudCommandType type = 4; + // The initial state of the entity. + CrudInitState state = 3; } -// The command to be executed. -// The supported crud operations are create, fetch, update, delete. -message CrudCommand { - // The name of the service this crud entity is on. - string service_name = 1; - - // The ID of the entity. - string entity_id = 2; - - // The ID of a sub entity. - string sub_entity_id = 3; +// The state of the entity when it is first activated. +message CrudInitState { + // The value of the entity state, if the entity has already been created. + google.protobuf.Any value = 3; - // A command id. - int64 id = 4; - - // Command name - string name = 5; - - // The command payload. - google.protobuf.Any payload = 6; + // The sequence number of the entity state. + int64 sequence = 4; +} - // The persisted state to be conveyed between the CRUD entity and the user function. - CrudState state = 7; +// Output message type for the gRPC stream out. +message CrudStreamOut { + oneof message { + CrudReply reply = 1; + Failure failure = 2; + } } // A reply to a command. message CrudReply { + // The command being replied to + int64 command_id = 1; - // The id of the command being replied to. Must match the input command. - int64 command_id = 1; + // The action to take for the client response + ClientAction client_action = 2; - // The action to take - ClientAction client_action = 2; + // Any side effects to perform + repeated SideEffect side_effects = 3; - // Any side effects to perform - repeated SideEffect side_effects = 3; + // The action to take on the CRUD entity + CrudAction crud_action = 4; - // An optional state to persist. - google.protobuf.Any state = 4; - - // An optional snapshot to persist. It is assumed that this snapshot represents any state in the state field - // persisted before. It is illegal to send a snapshot without sending any events. - // Note that the state overrides the snapshot and the state is not applied to the snapshot. - google.protobuf.Any snapshot = 5; + // An optional snapshot to persist. + // It is assumed that this snapshot will have the state of any actions in the crud action field applied to it. + // It is illegal to send a snapshot without sending any crud action. + CrudSnapshot snapshot = 5; } -// A reply message type for the gRPC call. -message CrudReplyOut { - oneof message { - CrudReply reply = 1; - Failure failure = 2; - } +// A snapshot of the entity. +message CrudSnapshot { + // The value of the snapshot, if a snapshot has already been created. + google.protobuf.Any value = 3; } -// CRUD Protocol -// -// Each operation sent across this protocol has among others information the state of the CRUD entity. -// The Cloudstate proxy is responsible to load the state, if any exists, from the CRUD entity and to pass it to -// the user function which holds it in memory to handle the operation being executed. The Cloudstate proxy updates -// the state in the user function successfully before executing the operation. The Cloudstate proxy executes one -// operation waits for the result and replies before executing the next operation, the operations are executed in order. -// This way the state is always in sync in the user function. Write operations could emit new state and -// read operations should not. The CRUD entity is backed by an event sourced entity, it means emitted state is an -// event sourced event. -// -// For each operation the first message sent to the CRUD entity is CrudEntityCommand which contains the entity ID and -// the command type to know which operation to call. The CrudEntityCommand is mapped to CrudCommand which contains the -// entity ID, the state and the snapshot sequence number. The state exists if the entity has previously persisted a -// state. It is the same with the snapshot sequence number. Once an operation is called the CrudCommand is passed to it. -// Each operation returns a reply message CrudReplyOut to each CrudCommand. The reply message contains the new state -// and the snapshot to be persisted. For CrudReplyOut the state exists if the user function emits a state change. -// Snapshot for CrudReplyOut exists if the snapshot configuration is fulfilled. The CRUD entity is expected to reply to -// each CrudEntityCommand. -// -// The user function is not responsible for updating its state in memory, it should rather emit the new state to -// the Cloudstate proxy. The Cloudstate proxy is responsible to pass the new state to the CRUD entity, -// so that it can be applied. The Cloudstate proxy loads the persisted state for subsequents operations. -// The user function could emit new state for write operations. -// -// The service could not been initialized before an operation is executed. In this case this operation will init -// the service. Each operation always check if the service is initialized. -// -// -service Crud { - - // This operation creates a sub entity. - rpc create(CrudCommand) returns (CrudReplyOut) {} - - // This operation fetches the CRUD entity or a sub entity. - rpc fetch(CrudCommand) returns (CrudReplyOut) {} - - // This operation updates the the CRUD entity or a sub entity. - rpc update(CrudCommand) returns (CrudReplyOut) {} +// An action to take for changing the entity state. +message CrudAction { + oneof action { + CrudUpdate update = 1; + CrudDelete delete = 2; + } +} - // This operation deletes a sub entity. - rpc delete(CrudCommand) returns (CrudReplyOut) {} +// An action which updates the persisted value of the CRUD entity. If the entity is not yet persisted, it will be created. +message CrudUpdate { + // The value to set. + google.protobuf.Any value = 1; } + +// An action which deletes the persisted value of the CRUD entity. +message CrudDelete {} \ No newline at end of file diff --git a/proxy/core/src/main/scala/io/cloudstate/proxy/crud/CrudEntity.scala b/proxy/core/src/main/scala/io/cloudstate/proxy/crud/CrudEntity.scala index 060f6f9e4..58bed6a83 100644 --- a/proxy/core/src/main/scala/io/cloudstate/proxy/crud/CrudEntity.scala +++ b/proxy/core/src/main/scala/io/cloudstate/proxy/crud/CrudEntity.scala @@ -19,25 +19,35 @@ package io.cloudstate.proxy.crud import java.net.URLDecoder import java.util.concurrent.atomic.AtomicLong +import akka.NotUsed import akka.actor._ import akka.cluster.sharding.ShardRegion import akka.persistence._ -import akka.stream.Materializer +import akka.stream.scaladsl._ +import akka.stream.{CompletionStrategy, Materializer, OverflowStrategy} import akka.util.Timeout import com.google.protobuf.any.{Any => pbAny} -import io.cloudstate.protocol.crud._ +import io.cloudstate.protocol.crud.CrudAction.Action.{Delete, Update} +import io.cloudstate.protocol.crud.{ + CrudClient, + CrudInit, + CrudInitState, + CrudReply, + CrudSnapshot, + CrudStreamIn, + CrudStreamOut, + CrudUpdate +} import io.cloudstate.protocol.entity._ import io.cloudstate.proxy.ConcurrencyEnforcer.{Action, ActionCompleted} import io.cloudstate.proxy.StatsCollector -import io.cloudstate.proxy.crud.CrudEntity.InternalState -import io.cloudstate.proxy.entity.UserFunctionReply +import io.cloudstate.proxy.entity.{EntityCommand, UserFunctionReply} import scala.collection.immutable.Queue object CrudEntitySupervisor { private final case class Relay(actorRef: ActorRef) - private final case object Start def props(client: CrudClient, configuration: CrudEntity.Configuration, @@ -65,32 +75,68 @@ final class CrudEntitySupervisor(client: CrudClient, import CrudEntitySupervisor._ - override final def preStart(): Unit = - self ! Start + private var streamTerminated: Boolean = false + + override final def receive: Receive = PartialFunction.empty + + override final def preStart(): Unit = { + client + .handle( + Source + .actorRef[CrudStreamIn](configuration.sendQueueSize, OverflowStrategy.fail) + .mapMaterializedValue { ref => + self ! Relay(ref) + NotUsed + } + ) + .runWith(Sink.actorRef(self, CrudEntity.StreamClosed)) + context.become(waitingForRelay) + } - override final def receive: Receive = { - case Start => + private[this] final def waitingForRelay: Receive = { + case Relay(relayRef) => // Cluster sharding URL encodes entity ids, so to extract it we need to decode. val entityId = URLDecoder.decode(self.path.name, "utf-8") val manager = context.watch( context - .actorOf(CrudEntity.props(configuration, entityId, client, concurrencyEnforcer, statsCollector), "entity") + .actorOf(CrudEntity.props(configuration, entityId, relayRef, concurrencyEnforcer, statsCollector), "entity") ) - context.become(forwarding(manager)) + context.become(forwarding(manager, relayRef)) unstashAll() - case _ => stash() } - private[this] final def forwarding(manager: ActorRef): Receive = { + private[this] final def forwarding(manager: ActorRef, relay: ActorRef): Receive = { case Terminated(`manager`) => - context.stop(self) + if (streamTerminated) { + context.stop(self) + } else { + relay ! Status.Success(CompletionStrategy.draining) + context.become(stopping) + } + case toParent if sender() == manager => context.parent ! toParent + + case CrudEntity.StreamClosed => + streamTerminated = true + manager forward CrudEntity.StreamClosed + + case failed: CrudEntity.StreamFailed => + streamTerminated = true + manager forward failed + case msg => manager forward msg } + private def stopping: Receive = { + case CrudEntity.StreamClosed => + context.stop(self) + case _: CrudEntity.StreamFailed => + context.stop(self) + } + override def supervisorStrategy: SupervisorStrategy = SupervisorStrategy.stoppingStrategy } @@ -98,6 +144,9 @@ object CrudEntity { final case object Stop + final case object StreamClosed extends DeadLetterSuppression + final case class StreamFailed(cause: Throwable) extends DeadLetterSuppression + final case class Configuration( serviceName: String, userFunctionName: String, @@ -111,43 +160,33 @@ object CrudEntity { replyTo: ActorRef ) - // Represents the entity state with the sequence number fo the last snapshot - private final case class InternalState( - payload: pbAny, - sequenceNumber: Long - ) - final def props(configuration: Configuration, entityId: String, - client: CrudClient, + relay: ActorRef, concurrencyEnforcer: ActorRef, statsCollector: ActorRef): Props = - Props(new CrudEntity(configuration, entityId, client, concurrencyEnforcer, statsCollector)) + Props(new CrudEntity(configuration, entityId, relay, concurrencyEnforcer, statsCollector)) /** * Used to ensure the action ids sent to the concurrency enforcer are indeed unique. */ private val actorCounter = new AtomicLong(0) + } final class CrudEntity(configuration: CrudEntity.Configuration, entityId: String, - client: CrudClient, + relay: ActorRef, concurrencyEnforcer: ActorRef, statsCollector: ActorRef) extends PersistentActor with ActorLogging { - - import akka.pattern.pipe - import context.dispatcher - override final def persistenceId: String = configuration.userFunctionName + entityId private val actorId = CrudEntity.actorCounter.incrementAndGet() - private[this] final var state: Option[InternalState] = None - - private[this] final var stashedCommands = Queue.empty[(CrudEntityCommand, ActorRef)] // PERFORMANCE: look at options for data structures + private[this] final var recoveredState: Option[pbAny] = None + private[this] final var stashedCommands = Queue.empty[(EntityCommand, ActorRef)] // PERFORMANCE: look at options for data structures private[this] final var currentCommand: CrudEntity.OutstandingCommand = null private[this] final var stopped = false private[this] final var idCounter = 0L @@ -170,6 +209,8 @@ final class CrudEntity(configuration: CrudEntity.Configuration, if (reportedDatabaseOperationStarted) { reportDatabaseOperationFinished() } + // This will shutdown the stream (if not already shut down) + relay ! Status.Success(()) } private[this] final def commandHandled(): Unit = { @@ -188,7 +229,7 @@ final class CrudEntity(configuration: CrudEntity.Configuration, case null => case req => req.replyTo ! createFailure(msg) } - val errorNotification = createFailure("CRUD entity terminated") + val errorNotification = createFailure("Entity terminated") stashedCommands.foreach { case (_, replyTo) => replyTo ! errorNotification } @@ -202,41 +243,22 @@ final class CrudEntity(configuration: CrudEntity.Configuration, private[this] final def reportActionComplete() = concurrencyEnforcer ! ActionCompleted(currentCommand.actionId, System.nanoTime() - commandStartTime) - private[this] final def handleCommand(entityCommand: CrudEntityCommand, sender: ActorRef): Unit = { + private[this] final def handleCommand(entityCommand: EntityCommand, sender: ActorRef): Unit = { idCounter += 1 - val command = CrudCommand( - serviceName = configuration.serviceName, + val command = Command( entityId = entityId, - subEntityId = entityCommand.entityId, id = idCounter, name = entityCommand.name, - payload = entityCommand.payload, - state = state.map(s => CrudState(Some(s.payload), sequenceNumber())) + payload = entityCommand.payload ) currentCommand = CrudEntity.OutstandingCommand(idCounter, actorId + ":" + entityId + ":" + idCounter, sender) commandStartTime = System.nanoTime() - concurrencyEnforcer ! Action( - currentCommand.actionId, - () => handleCrudCommand(command, entityCommand.`type`) - ) - } - - private[this] final def handleCrudCommand(command: CrudCommand, commandType: Option[CrudCommandType]): Unit = { - import CrudCommandType.{Command => CrudCmd} - commandType match { - case Some(cmdType) => - cmdType.command match { - case CrudCmd.Create(_) => client.create(command) pipeTo self - case CrudCmd.Fetch(_) => client.fetch(command) pipeTo self - case CrudCmd.Update(_) => client.update(command) pipeTo self - case CrudCmd.Delete(_) => client.delete(command) pipeTo self - } - - case _ => // Nothing to do, commandType should not be None - } + concurrencyEnforcer ! Action(currentCommand.actionId, () => { + relay ! CrudStreamIn(CrudStreamIn.Message.Command(command)) + }) } - private final def esReplyToUfReply(reply: CrudReply): UserFunctionReply = + private final def esReplyToUfReply(reply: CrudReply) = UserFunctionReply( clientAction = reply.clientAction, sideEffects = reply.sideEffects @@ -247,49 +269,45 @@ final class CrudEntity(configuration: CrudEntity.Configuration, clientAction = Some(ClientAction(ClientAction.Action.Failure(Failure(description = message)))) ) - private[this] final def maybeInit(snapshot: Option[SnapshotOffer]): Unit = - if (!inited) { - state = snapshot.map { - case SnapshotOffer(metadata, offeredSnapshot: pbAny) => - InternalState(offeredSnapshot, metadata.sequenceNr) - case other => throw new IllegalStateException(s"Unexpected snapshot type received: ${other.getClass}") - } - inited = true - } - - // the sequence number of the last taken snapshot - private[this] final def sequenceNumber(): Long = state.map(_.sequenceNumber).getOrElse(0L) - - override final def receiveCommand: Receive = { + override final def receiveCommand: PartialFunction[Any, Unit] = { - case command: CrudEntityCommand if currentCommand != null => + case command: EntityCommand if currentCommand != null => stashedCommands = stashedCommands.enqueue((command, sender())) - case command: CrudEntityCommand => + case command: EntityCommand => handleCommand(command, sender()) - case CrudReplyOut(m, _) => - import CrudReplyOut.{Message => CrudOMsg} + case CrudStreamOut(m, _) => + import CrudStreamOut.{Message => CrudSOMsg} m match { - case CrudOMsg.Reply(r) if currentCommand == null => + + case CrudSOMsg.Reply(r) if currentCommand == null => crash(s"Unexpected reply, had no current command: $r") - case CrudOMsg.Reply(r) if currentCommand.commandId != r.commandId => + case CrudSOMsg.Reply(r) if currentCommand.commandId != r.commandId => crash(s"Incorrect command id in reply, expecting ${currentCommand.commandId} but got ${r.commandId}") - case CrudOMsg.Reply(r) => + case CrudSOMsg.Reply(r) => reportActionComplete() val commandId = currentCommand.commandId - r.state match { - case None => - currentCommand.replyTo ! esReplyToUfReply(r) - commandHandled() - case Some(event) => - reportDatabaseOperationStarted() - persist(event) { _ => + if (r.crudAction.isEmpty) { + currentCommand.replyTo ! esReplyToUfReply(r) + commandHandled() + } else { + reportDatabaseOperationStarted() + r.crudAction map { a => + // map the CrudAction to state + val state = a.action match { + case Update(CrudUpdate(Some(value), _)) => Some(value) + case Delete(_) => None + } + + persist(state) { _ => reportDatabaseOperationFinished() - state = Some(InternalState(event, sequenceNumber)) - r.snapshot.foreach(saveSnapshot) + // try to save a snapshot + r.snapshot.foreach { + case CrudSnapshot(value, _) => saveSnapshot(value) + } // Make sure that the current request is still ours if (currentCommand == null || currentCommand.commandId != commandId) { crash("Internal error - currentRequest changed before all events were persisted") @@ -297,37 +315,42 @@ final class CrudEntity(configuration: CrudEntity.Configuration, currentCommand.replyTo ! esReplyToUfReply(r) commandHandled() } + } } - case CrudOMsg.Failure(f) if f.commandId == 0 => + case CrudSOMsg.Failure(f) if f.commandId == 0 => crash(s"Non command specific error from entity: ${f.description}") - case CrudOMsg.Failure(f) if currentCommand == null => + case CrudSOMsg.Failure(f) if currentCommand == null => crash(s"Unexpected failure, had no current command: $f") - case CrudOMsg.Failure(f) if currentCommand.commandId != f.commandId => + case CrudSOMsg.Failure(f) if currentCommand.commandId != f.commandId => crash(s"Incorrect command id in failure, expecting ${currentCommand.commandId} but got ${f.commandId}") - case CrudOMsg.Failure(f) => + case CrudSOMsg.Failure(f) => reportActionComplete() currentCommand.replyTo ! createFailure(f.description) commandHandled() - case CrudOMsg.Empty => + case CrudSOMsg.Empty => // Either the reply/failure wasn't set, or its set to something unknown. // todo see if scalapb can give us unknown fields so we can possibly log more intelligently crash("Empty or unknown message from entity output stream") } - case Status.Failure(error) => - notifyOutstandingRequests("Unexpected entity termination") + case CrudEntity.StreamClosed => + notifyOutstandingRequests("Unexpected CRUD entity termination") + context.stop(self) + + case CrudEntity.StreamFailed(error) => + notifyOutstandingRequests("Unexpected CRUD entity termination") throw error case SaveSnapshotSuccess(metadata) => // Nothing to do case SaveSnapshotFailure(metadata, cause) => - log.error("Error saving snapshot", cause) + log.error("Error saving snapshot for CRUD entity", cause) case ReceiveTimeout => context.parent ! ShardRegion.Passivate(stopMessage = CrudEntity.Stop) @@ -341,15 +364,38 @@ final class CrudEntity(configuration: CrudEntity.Configuration, override final def receiveRecover: PartialFunction[Any, Unit] = { case offer: SnapshotOffer => - maybeInit(Some(offer)) + if (!inited) { + // apply snapshot on recoveredState only when the entity is not fully initialized + recoveredState = offer.snapshot match { + case Some(updated: pbAny) => Some(updated) + case other => + throw new IllegalStateException(s"CRUD entity received a unexpected snapshot type : ${other.getClass}") + } + } case RecoveryCompleted => reportDatabaseOperationFinished() - maybeInit(None) + if (!inited) { + relay ! CrudStreamIn( + CrudStreamIn.Message.Init( + CrudInit( + serviceName = configuration.serviceName, + entityId = entityId, + state = Some(CrudInitState(recoveredState, lastSequenceNr)) + ) + ) + ) + inited = true + } - case event: pbAny => - maybeInit(None) - state = Some(InternalState(event, sequenceNumber)) + case event: Any => + if (!inited) { + // apply event on recoveredState only when the entity is not fully initialized + recoveredState = event match { + case Some(updated: pbAny) => Some(updated) + case _ => None + } + } } private def reportDatabaseOperationStarted(): Unit = diff --git a/proxy/core/src/main/scala/io/cloudstate/proxy/crud/CrudSupportFactory.scala b/proxy/core/src/main/scala/io/cloudstate/proxy/crud/CrudSupportFactory.scala index a72d65a34..965dc9b96 100644 --- a/proxy/core/src/main/scala/io/cloudstate/proxy/crud/CrudSupportFactory.scala +++ b/proxy/core/src/main/scala/io/cloudstate/proxy/crud/CrudSupportFactory.scala @@ -1,3 +1,19 @@ +/* + * Copyright 2019 Lightbend Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package io.cloudstate.proxy.crud import akka.NotUsed @@ -9,22 +25,11 @@ import akka.grpc.GrpcClientSettings import akka.stream.Materializer import akka.stream.scaladsl.Flow import akka.util.Timeout -import com.google.protobuf.ByteString import com.google.protobuf.Descriptors.ServiceDescriptor -import io.cloudstate.protocol.crud.{ - CreateCommand, - CrudClient, - CrudCommandType, - CrudEntityCommand, - DeleteCommand, - FetchCommand, - UpdateCommand -} -import io.cloudstate.protocol.entity.Entity -import io.cloudstate.proxy.EntityMethodDescriptor.CrudCommandOptionValue +import io.cloudstate.protocol.crud.CrudClient +import io.cloudstate.protocol.entity.{Entity, Metadata} import io.cloudstate.proxy._ import io.cloudstate.proxy.entity.{EntityCommand, UserFunctionReply} -import io.cloudstate.proxy.eventsourced.DynamicLeastShardAllocationStrategy import scala.concurrent.{ExecutionContext, Future} @@ -37,7 +42,7 @@ class CrudSupportFactory(system: ActorSystem, private final val log = Logging.getLogger(system, this.getClass) - private val crudClient = CrudClient(grpcClientSettings) + private val crudClient = CrudClient(grpcClientSettings)(system) override def buildEntityTypeSupport(entity: Entity, serviceDescriptor: ServiceDescriptor, @@ -49,19 +54,19 @@ class CrudSupportFactory(system: ActorSystem, config.passivationTimeout, config.relayOutputBufferSize) - log.debug("Starting Crud Entity for {}", entity.persistenceId) + log.debug("Starting CrudEntity for {}", entity.persistenceId) val clusterSharding = ClusterSharding(system) val clusterShardingSettings = ClusterShardingSettings(system) - val eventSourcedEntity = clusterSharding.start( + val crudEntity = clusterSharding.start( typeName = entity.persistenceId, entityProps = CrudEntitySupervisor.props(crudClient, stateManagerConfig, concurrencyEnforcer, statsCollector), settings = clusterShardingSettings, - messageExtractor = new EntityIdExtractor(config.numberOfShards), + messageExtractor = new CrudEntityIdExtractor(config.numberOfShards), allocationStrategy = new DynamicLeastShardAllocationStrategy(1, 10, 2, 0.0), handOffStopMessage = CrudEntity.Stop ) - new CrudSupport(eventSourcedEntity, config.proxyParallelism, config.relayTimeout) + new CrudSupport(crudEntity, config.proxyParallelism, config.relayTimeout) } private def validate(serviceDescriptor: ServiceDescriptor, @@ -71,76 +76,39 @@ class CrudSupportFactory(system: ActorSystem, if (streamedMethods.nonEmpty) { val offendingMethods = streamedMethods.map(_.method.getName).mkString(",") throw EntityDiscoveryException( - s"Crud entities do not support streamed methods, but ${serviceDescriptor.getFullName} has the following streamed methods: ${offendingMethods}" + s"CRUD entities do not support streamed methods, but ${serviceDescriptor.getFullName} has the following streamed methods: ${offendingMethods}" ) } val methodsWithoutKeys = methodDescriptors.values.filter(_.keyFieldsCount < 1) if (methodsWithoutKeys.nonEmpty) { val offendingMethods = methodsWithoutKeys.map(_.method.getName).mkString(",") throw EntityDiscoveryException( - s"""Crud entities do not support methods whose parameters do not have at least one field marked as entity_key, - |"but ${serviceDescriptor.getFullName} has the following methods without keys: ${offendingMethods}""".stripMargin - ) - } - - // FIXME crudSubEntityKey should be removed - // FIXME crudCommandType should be check only for method with payload like GET and DELETE - /* - val methodsWithoutSubEntityKeys = methodDescriptors.values.filter(_.crudSubEntityKeyFieldsCount < 1) - if (methodsWithoutSubEntityKeys.nonEmpty) { - val offendingMethods = methodsWithoutSubEntityKeys.map(_.method.getName).mkString(",") - throw EntityDiscoveryException( - s"""Crud entities do not support methods whose parameters do not have at least one field marked as sub_entity_key, - |"but ${serviceDescriptor.getFullName} has the following methods without keys: ${offendingMethods}""".stripMargin - ) - } - - val methodsWithoutCommandType = methodDescriptors.values.filter(_.crudCommandTypeFieldsCount < 1) - if (methodsWithoutCommandType.nonEmpty) { - val offendingMethods = methodsWithoutCommandType.map(_.method.getName).mkString(",") - throw EntityDiscoveryException( - s"""Crud entities do not support methods whose parameters do not have at least one field marked as crud_command_type, - |"but ${serviceDescriptor.getFullName} has the following methods without keys: ${offendingMethods}""".stripMargin + s"""CRUD entities do not support methods whose parameters do not have at least one field marked as entity_key, + |but ${serviceDescriptor.getFullName} has the following methods without keys: $offendingMethods""".stripMargin + .replaceAll("\n", " ") ) } - */ } } -private class CrudSupport(eventSourcedEntity: ActorRef, parallelism: Int, private implicit val relayTimeout: Timeout) +private class CrudSupport(crudEntity: ActorRef, parallelism: Int, private implicit val relayTimeout: Timeout) extends EntityTypeSupport { - import akka.pattern.ask - override def handler(method: EntityMethodDescriptor): Flow[EntityCommand, UserFunctionReply, NotUsed] = - Flow[EntityCommand].mapAsync(parallelism) { command => - val commandType = extractCommandType(method, command) - val initCommand = CrudEntityCommand(entityId = command.entityId, - name = command.name, - payload = command.payload, - `type` = Some(commandType)) - (eventSourcedEntity ? initCommand).mapTo[UserFunctionReply] - } + override def handler(method: EntityMethodDescriptor, + metadata: Metadata): Flow[EntityCommand, UserFunctionReply, NotUsed] = + Flow[EntityCommand].mapAsync(parallelism)( + command => + (crudEntity ? EntityTypeSupport.mergeStreamLevelMetadata(metadata, command)) + .mapTo[UserFunctionReply] + ) override def handleUnary(command: EntityCommand): Future[UserFunctionReply] = - (eventSourcedEntity ? command).mapTo[UserFunctionReply] - - private def extractCommandType(method: EntityMethodDescriptor, command: EntityCommand): CrudCommandType = { - val commandType = method.extractCrudCommandType(command.payload.fold(ByteString.EMPTY)(_.value)) - commandType match { - case CrudCommandOptionValue.CREATE => CrudCommandType.of(CrudCommandType.Command.Create(CreateCommand())) - case CrudCommandOptionValue.FETCH => CrudCommandType.of(CrudCommandType.Command.Fetch(FetchCommand())) - case CrudCommandOptionValue.UPDATE => CrudCommandType.of(CrudCommandType.Command.Update(UpdateCommand())) - case CrudCommandOptionValue.DELETE => CrudCommandType.of(CrudCommandType.Command.Delete(DeleteCommand())) - case CrudCommandOptionValue.UNKNOWN => - // cannot be empty here because the check is made in the validate method of CrudSupportFactory - throw new RuntimeException(s"Command - ${command.name} for CRUD entity should have a command type") - } - } + (crudEntity ? command).mapTo[UserFunctionReply] } -private final class EntityIdExtractor(shards: Int) extends HashCodeMessageExtractor(shards) { +private final class CrudEntityIdExtractor(shards: Int) extends HashCodeMessageExtractor(shards) { override final def entityId(message: Any): String = message match { - case command: CrudEntityCommand => command.entityId + case command: EntityCommand => command.entityId } } diff --git a/proxy/core/src/main/scala/io/cloudstate/proxy/crud/DynamicLeastShardAllocationStrategy.scala b/proxy/core/src/main/scala/io/cloudstate/proxy/crud/DynamicLeastShardAllocationStrategy.scala new file mode 100644 index 000000000..e98ba6b96 --- /dev/null +++ b/proxy/core/src/main/scala/io/cloudstate/proxy/crud/DynamicLeastShardAllocationStrategy.scala @@ -0,0 +1,77 @@ +/* + * Copyright 2019 Lightbend Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.cloudstate.proxy.crud + +import akka.actor.ActorRef +import akka.cluster.sharding.ShardCoordinator.ShardAllocationStrategy +import akka.cluster.sharding.ShardRegion.ShardId + +import scala.collection.immutable +import scala.concurrent.Future + +class DynamicLeastShardAllocationStrategy(rebalanceThreshold: Int, + maxSimultaneousRebalance: Int, + rebalanceNumber: Int, + rebalanceFactor: Double) + extends ShardAllocationStrategy + with Serializable { + + def this(rebalanceThreshold: Int, maxSimultaneousRebalance: Int) = + this(rebalanceThreshold, maxSimultaneousRebalance, rebalanceThreshold, 0.0) + + override def allocateShard( + requester: ActorRef, + shardId: ShardId, + currentShardAllocations: Map[ActorRef, immutable.IndexedSeq[ShardId]] + ): Future[ActorRef] = { + val (regionWithLeastShards, _) = currentShardAllocations.minBy { case (_, v) => v.size } + Future.successful(regionWithLeastShards) + } + + override def rebalance(currentShardAllocations: Map[ActorRef, immutable.IndexedSeq[ShardId]], + rebalanceInProgress: Set[ShardId]): Future[Set[ShardId]] = + if (rebalanceInProgress.size < maxSimultaneousRebalance) { + val (_, leastShards) = currentShardAllocations.minBy { case (_, v) => v.size } + val mostShards = currentShardAllocations + .collect { + case (_, v) => v.filterNot(s => rebalanceInProgress(s)) + } + .maxBy(_.size) + val difference = mostShards.size - leastShards.size + if (difference > rebalanceThreshold) { + + val factoredRebalanceLimit = (rebalanceFactor, rebalanceNumber) match { + // This condition is to maintain semantic backwards compatibility, from when rebalanceThreshold was also + // the number of shards to move. + case (0.0, 0) => rebalanceThreshold + case (0.0, justAbsolute) => justAbsolute + case (justFactor, 0) => math.max((justFactor * mostShards.size).round.toInt, 1) + case (factor, absolute) => math.min(math.max((factor * mostShards.size).round.toInt, 1), absolute) + } + + // The ideal number to rebalance to so these nodes have an even number of shards + val evenRebalance = difference / 2 + + val n = + math.min(math.min(factoredRebalanceLimit, evenRebalance), maxSimultaneousRebalance - rebalanceInProgress.size) + Future.successful(mostShards.sorted.take(n).toSet) + } else + emptyRebalanceResult + } else emptyRebalanceResult + + private[this] final val emptyRebalanceResult = Future.successful(Set.empty[ShardId]) +} diff --git a/proxy/core/src/main/scala/io/cloudstate/proxy/crud/InMemSnapshotStore.scala b/proxy/core/src/main/scala/io/cloudstate/proxy/crud/InMemSnapshotStore.scala new file mode 100644 index 000000000..f544c5781 --- /dev/null +++ b/proxy/core/src/main/scala/io/cloudstate/proxy/crud/InMemSnapshotStore.scala @@ -0,0 +1,55 @@ +/* + * Copyright 2019 Lightbend Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.cloudstate.proxy.crud + +import akka.persistence.snapshot.SnapshotStore +import akka.persistence.{SelectedSnapshot, SnapshotMetadata, SnapshotSelectionCriteria} + +import scala.concurrent.Future + +class InMemSnapshotStore extends SnapshotStore { + + private[this] final var snapshots = Map.empty[String, SelectedSnapshot] + + override def loadAsync(persistenceId: String, criteria: SnapshotSelectionCriteria): Future[Option[SelectedSnapshot]] = + Future.successful( + snapshots + .get(persistenceId) + .filter( + s => + s.metadata.sequenceNr >= criteria.minSequenceNr && + s.metadata.sequenceNr <= criteria.maxSequenceNr && + s.metadata.timestamp >= criteria.minTimestamp && + s.metadata.timestamp <= criteria.maxTimestamp + ) + ) + + override def saveAsync(metadata: SnapshotMetadata, snapshot: Any): Future[Unit] = { + snapshots += metadata.persistenceId -> SelectedSnapshot(metadata, snapshot) + Future.unit + } + + override def deleteAsync(metadata: SnapshotMetadata): Future[Unit] = { + snapshots -= metadata.persistenceId + Future.unit + } + + override def deleteAsync(persistenceId: String, criteria: SnapshotSelectionCriteria): Future[Unit] = { + snapshots -= persistenceId + Future.unit + } +} diff --git a/proxy/core/src/test/proto/cloudstate/proxy/test/ShoppingCartCrudTest.proto b/proxy/core/src/test/proto/cloudstate/proxy/test/ShoppingCartCrudTest.proto deleted file mode 100644 index d9913e527..000000000 --- a/proxy/core/src/test/proto/cloudstate/proxy/test/ShoppingCartCrudTest.proto +++ /dev/null @@ -1,80 +0,0 @@ -// This is the public API offered by the shopping cart crud entity. -syntax = "proto3"; - -import "google/protobuf/empty.proto"; -import "cloudstate/entity_key.proto"; -import "cloudstate/sub_entity_key.proto"; -import "cloudstate/crud_command_type.proto"; -import "google/api/annotations.proto"; -import "google/api/http.proto"; -import "google/api/httpbody.proto"; - -package cloudstate.proxy.test.crud; - -option java_package = "io.cloudstate.proxy.test.crud"; - -message AddLineItem { - string cart_id = 1 [(.cloudstate.entity_key) = true]; - string user_id = 2 [(.cloudstate.sub_entity_key) = true]; - string command_type = 3 [(.cloudstate.crud_command_type) = true]; - string product_id = 4; - string name = 5; - int32 quantity = 6; -} - -message RemoveLineItem { - string cart_id = 1 [(.cloudstate.entity_key) = true]; - string user_id = 2 [(.cloudstate.entity_key) = true]; - string command_type = 3 [(.cloudstate.crud_command_type) = true]; - string product_id = 4; -} - -message GetShoppingCart { - string cart_id = 1 [(.cloudstate.entity_key) = true]; - string user_id = 2 [(.cloudstate.entity_key) = true]; - string command_type = 3 [(.cloudstate.crud_command_type) = true]; -} - -message LineItem { - string product_id = 1; - string name = 2; - int32 quantity = 3; -} - -message Cart { - repeated LineItem items = 1; -} - -service ShoppingCart { - rpc AddItem(AddLineItem) returns (google.protobuf.Empty) { - option (google.api.http) = { - post: "/cart/{cart_id}/{user_id}/items/add", - body: "*", - }; - } - - rpc UpdateItem(AddLineItem) returns (google.protobuf.Empty) { - option (google.api.http) = { - post: "/cart/{cart_id}/{user_id}/items/update", - body: "*", - }; - } - - rpc RemoveItem(RemoveLineItem) returns (google.protobuf.Empty) { - option (google.api.http).post = "/cart/{cart_id}/{user_id}/items/{product_id}/remove"; - } - - rpc GetCart(GetShoppingCart) returns (Cart) { - option (google.api.http) = { - get: "/carts/{cart_id}/{user_id}", - additional_bindings: [{ - get: "/carts/{cart_id}/{user_id}/items", - response_body: "items" - }] - }; - } - - rpc ShowCartPage(GetShoppingCart) returns (google.api.HttpBody) { - option (google.api.http).get = "/carts/{cart_id}/{user_id}.html"; - } -} diff --git a/proxy/core/src/test/scala/io/cloudstate/proxy/EntityMethodDescriptorSpec.scala b/proxy/core/src/test/scala/io/cloudstate/proxy/EntityMethodDescriptorSpec.scala deleted file mode 100644 index 1b68fece1..000000000 --- a/proxy/core/src/test/scala/io/cloudstate/proxy/EntityMethodDescriptorSpec.scala +++ /dev/null @@ -1,40 +0,0 @@ -package io.cloudstate.proxy - -import io.cloudstate.proxy.test.crud.ShoppingCartCrudTest.{AddLineItem, ShoppingCart} -import org.scalatest.{Matchers, WordSpecLike} - -class EntityMethodDescriptorSpec extends WordSpecLike with Matchers { - - private val addItemDescriptor = ShoppingCart.descriptor - .findServiceByName("ShoppingCart") - .findMethodByName("AddItem") - - private val entityMethodDescriptor = new EntityMethodDescriptor(addItemDescriptor) - - "The EntityMethodDescriptor" should { - - "extract entity key" in { - val subEntityKey = - entityMethodDescriptor.extractId( - AddLineItem("cartId", "userId", "productId", "name").toByteString - ) - subEntityKey should ===("cartId") - } - - "extract crud sub entity key" in { - val subEntityKey = - entityMethodDescriptor.extractCrudSubEntityId( - AddLineItem("cartId", "userId", "productId", "name").toByteString - ) - subEntityKey should ===("userId") - } - - "extract crud command type" in { - val commandType = entityMethodDescriptor.extractCrudCommandType( - AddLineItem("cartId", "userId", "create", "productId", "name").toByteString - ) - commandType should ===("create") - } - - } -} diff --git a/samples/java-shopping-cart/src/main/java/io/cloudstate/samples/shoppingcart/ShoppingCartCrudEntity.java b/samples/java-shopping-cart/src/main/java/io/cloudstate/samples/shoppingcart/ShoppingCartCrudEntity.java index d6eb8ec71..efd96f824 100644 --- a/samples/java-shopping-cart/src/main/java/io/cloudstate/samples/shoppingcart/ShoppingCartCrudEntity.java +++ b/samples/java-shopping-cart/src/main/java/io/cloudstate/samples/shoppingcart/ShoppingCartCrudEntity.java @@ -1,3 +1,19 @@ +/* + * Copyright 2019 Lightbend Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package io.cloudstate.samples.shoppingcart; import com.example.crud.shoppingcart.Shoppingcart; @@ -7,15 +23,18 @@ import io.cloudstate.javasupport.crud.CommandContext; import io.cloudstate.javasupport.crud.CommandHandler; import io.cloudstate.javasupport.crud.CrudEntity; -import io.cloudstate.javasupport.crud.SnapshotHandler; +import io.cloudstate.javasupport.crud.StateHandler; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.stream.Collectors; -/** A crud entity. */ +/** An CRUD entity. */ @CrudEntity public class ShoppingCartCrudEntity { + private final String entityId; private final Map cart = new LinkedHashMap<>(); @@ -23,13 +42,15 @@ public ShoppingCartCrudEntity(@EntityId String entityId) { this.entityId = entityId; } - @SnapshotHandler - public void handleState(Domain.Cart cart) { - // HINTS: this is called by the proxy for updating the state + @StateHandler + public void handleState(Optional cart) { this.cart.clear(); - for (Domain.LineItem item : cart.getItemsList()) { - this.cart.put(item.getProductId(), convert(item)); - } + cart.ifPresent( + c -> { + for (Domain.LineItem item : c.getItemsList()) { + this.cart.put(item.getProductId(), convert(item)); + } + }); } @CommandHandler @@ -37,49 +58,53 @@ public Shoppingcart.Cart getCart() { return Shoppingcart.Cart.newBuilder().addAllItems(cart.values()).build(); } + @CommandHandler + public Empty removeCart(Shoppingcart.RemoveShoppingCart cartItem, CommandContext ctx) { + if (!entityId.equals(cartItem.getUserId())) { + ctx.fail("Cannot remove unknown cart " + cartItem.getUserId()); + } + cart.clear(); + + ctx.delete(); + return Empty.getDefaultInstance(); + } + @CommandHandler public Empty addItem(Shoppingcart.AddLineItem item, CommandContext ctx) { - // HINTS: CRUD Update - // HINTS: curl -vi -X POST localhost:9000/cart/{user_id}/items/add -H "Content-Type: - // application/json" -d '{"commandType":"update", "productId":"foo","name":"A - // foo","quantity":10}' if (item.getQuantity() <= 0) { - ctx.fail("Cannot add negative quantity of to item" + item.getProductId()); + ctx.fail("Cannot add negative quantity of to item " + item.getProductId()); } - Domain.LineItem lineItem = - cart.get(item.getProductId()) == null ? null : convert(cart.get(item.getProductId())); + Shoppingcart.LineItem lineItem = cart.get(item.getProductId()); if (lineItem == null) { lineItem = - Domain.LineItem.newBuilder() - .setUserId(item.getUserId()) + Shoppingcart.LineItem.newBuilder() .setProductId(item.getProductId()) .setName(item.getName()) .setQuantity(item.getQuantity()) .build(); } else { lineItem = - lineItem.toBuilder().setQuantity(item.getQuantity() + lineItem.getQuantity()).build(); + lineItem.toBuilder().setQuantity(lineItem.getQuantity() + item.getQuantity()).build(); } - Domain.Cart cart = convert(this.cart).toBuilder().addItems(lineItem).build(); // new state - ctx.emit(cart); // emit new state + cart.put(item.getProductId(), lineItem); + List lineItems = + cart.values().stream().map(this::convert).collect(Collectors.toList()); + ctx.update(Domain.Cart.newBuilder().addAllItems(lineItems).build()); return Empty.getDefaultInstance(); } @CommandHandler public Empty removeItem(Shoppingcart.RemoveLineItem item, CommandContext ctx) { - // HINTS: CRUD Delete - // HINTS: curl -vi -X POST localhost:9000/cart/{user_id}/items/{product_id}/remove -H - // "Content-Type: application/json" -d '{"commandType":"delete", "productId":"foo"}' - if (!cart.containsKey(item.getProductId())) { ctx.fail("Cannot remove item " + item.getProductId() + " because it is not in the cart."); } - cart.remove(item.getProductId()); - ctx.emit(convert(cart)); // emit the new state + List lineItems = + cart.values().stream().map(this::convert).collect(Collectors.toList()); + ctx.update(Domain.Cart.newBuilder().addAllItems(lineItems).build()); return Empty.getDefaultInstance(); } @@ -98,10 +123,4 @@ private Domain.LineItem convert(Shoppingcart.LineItem item) { .setQuantity(item.getQuantity()) .build(); } - - private Domain.Cart convert(Map cart) { - return Domain.Cart.newBuilder() - .addAllItems(cart.values().stream().map(this::convert).collect(Collectors.toList())) - .build(); - } } From 9f72587ef02d36a3d7f44bbf1aa5f217b510e5d2 Mon Sep 17 00:00:00 2001 From: Guy Youansi Date: Tue, 14 Jul 2020 22:24:12 +0200 Subject: [PATCH 16/93] wording fixed --- .../java/io/cloudstate/javasupport/crud/CommandContext.java | 2 +- .../main/java/io/cloudstate/javasupport/crud/CrudEntity.java | 2 +- .../cloudstate/javasupport/crud/CrudEntityCreationContext.java | 2 +- .../java/io/cloudstate/javasupport/crud/CrudEntityFactory.java | 2 +- protocols/example/crud/shoppingcart/persistence/domain.proto | 2 +- protocols/example/crud/shoppingcart/shoppingcart.proto | 2 +- protocols/protocol/cloudstate/crud.proto | 2 +- .../cloudstate/samples/shoppingcart/ShoppingCartCrudEntity.java | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/java-support/src/main/java/io/cloudstate/javasupport/crud/CommandContext.java b/java-support/src/main/java/io/cloudstate/javasupport/crud/CommandContext.java index 80d946068..f394deed0 100644 --- a/java-support/src/main/java/io/cloudstate/javasupport/crud/CommandContext.java +++ b/java-support/src/main/java/io/cloudstate/javasupport/crud/CommandContext.java @@ -20,7 +20,7 @@ import io.cloudstate.javasupport.EffectContext; /** - * An CRUD command context. + * A CRUD command context. * *

Methods annotated with {@link CommandHandler} may take this is a parameter. It allows updating * or deleting the entity state in response to a command, along with forwarding the result to other diff --git a/java-support/src/main/java/io/cloudstate/javasupport/crud/CrudEntity.java b/java-support/src/main/java/io/cloudstate/javasupport/crud/CrudEntity.java index e4a8e6c18..83062d7eb 100644 --- a/java-support/src/main/java/io/cloudstate/javasupport/crud/CrudEntity.java +++ b/java-support/src/main/java/io/cloudstate/javasupport/crud/CrudEntity.java @@ -23,7 +23,7 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -/** An CRUD entity. */ +/** A CRUD entity. */ @CloudStateAnnotation @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) diff --git a/java-support/src/main/java/io/cloudstate/javasupport/crud/CrudEntityCreationContext.java b/java-support/src/main/java/io/cloudstate/javasupport/crud/CrudEntityCreationContext.java index c4eb729df..20afd0cc9 100644 --- a/java-support/src/main/java/io/cloudstate/javasupport/crud/CrudEntityCreationContext.java +++ b/java-support/src/main/java/io/cloudstate/javasupport/crud/CrudEntityCreationContext.java @@ -19,6 +19,6 @@ /** * Creation context for {@link CrudEntity} annotated entities. * - *

This may be accepted as an argument to the constructor of an CRUD entity. + *

This may be accepted as an argument to the constructor of a CRUD entity. */ public interface CrudEntityCreationContext extends CrudContext {} diff --git a/java-support/src/main/java/io/cloudstate/javasupport/crud/CrudEntityFactory.java b/java-support/src/main/java/io/cloudstate/javasupport/crud/CrudEntityFactory.java index 6dd86a7d1..9ac34b20a 100644 --- a/java-support/src/main/java/io/cloudstate/javasupport/crud/CrudEntityFactory.java +++ b/java-support/src/main/java/io/cloudstate/javasupport/crud/CrudEntityFactory.java @@ -20,7 +20,7 @@ import io.cloudstate.javasupport.eventsourced.EventHandler; /** - * Low level interface for handling commands on an CRUD entity. + * Low level interface for handling commands on a CRUD entity. * *

Generally, this should not be needed, instead, a class annotated with the {@link * CommandHandler} and similar annotations should be used. diff --git a/protocols/example/crud/shoppingcart/persistence/domain.proto b/protocols/example/crud/shoppingcart/persistence/domain.proto index 65355cada..eb51b1034 100644 --- a/protocols/example/crud/shoppingcart/persistence/domain.proto +++ b/protocols/example/crud/shoppingcart/persistence/domain.proto @@ -13,7 +13,7 @@ // limitations under the License. // These are the messages that get persisted - the events, plus the current state (Cart) for snapshots. - syntax = "proto3"; +syntax = "proto3"; package com.example.crud.shoppingcart.persistence; diff --git a/protocols/example/crud/shoppingcart/shoppingcart.proto b/protocols/example/crud/shoppingcart/shoppingcart.proto index 02ca117c6..95fa5ff52 100644 --- a/protocols/example/crud/shoppingcart/shoppingcart.proto +++ b/protocols/example/crud/shoppingcart/shoppingcart.proto @@ -24,7 +24,7 @@ import "google/api/httpbody.proto"; package com.example.crud.shoppingcart; -option go_package = "tck/crudshoppingcart"; +option go_package = "tck/crud/shoppingcart"; message AddLineItem { string user_id = 1 [(.cloudstate.entity_key) = true]; diff --git a/protocols/protocol/cloudstate/crud.proto b/protocols/protocol/cloudstate/crud.proto index befdb263e..932514eef 100644 --- a/protocols/protocol/cloudstate/crud.proto +++ b/protocols/protocol/cloudstate/crud.proto @@ -101,7 +101,7 @@ message CrudReply { // A snapshot of the entity. message CrudSnapshot { // The value of the snapshot, if a snapshot has already been created. - google.protobuf.Any value = 3; + google.protobuf.Any value = 1; } // An action to take for changing the entity state. diff --git a/samples/java-shopping-cart/src/main/java/io/cloudstate/samples/shoppingcart/ShoppingCartCrudEntity.java b/samples/java-shopping-cart/src/main/java/io/cloudstate/samples/shoppingcart/ShoppingCartCrudEntity.java index efd96f824..3e9348bb9 100644 --- a/samples/java-shopping-cart/src/main/java/io/cloudstate/samples/shoppingcart/ShoppingCartCrudEntity.java +++ b/samples/java-shopping-cart/src/main/java/io/cloudstate/samples/shoppingcart/ShoppingCartCrudEntity.java @@ -31,7 +31,7 @@ import java.util.Optional; import java.util.stream.Collectors; -/** An CRUD entity. */ +/** A CRUD entity. */ @CrudEntity public class ShoppingCartCrudEntity { From 866941d239689834a6683c42ef550dc6a66a6dbf Mon Sep 17 00:00:00 2001 From: Guy Youansi Date: Sun, 26 Jul 2020 21:08:56 +0200 Subject: [PATCH 17/93] add sample for crud shopping cart and rollback the sample shopping cart for event sourcing --- build.sbt | 23 ++++++++++++- .../cloudstate/samples/shoppingcart/Main.java | 33 +++++++++++++++++++ .../src/main/resources/application.conf | 7 ++++ .../main/resources/simplelogger.properties | 13 ++++++++ .../cloudstate/samples/shoppingcart/Main.java | 8 ++--- 5 files changed, 79 insertions(+), 5 deletions(-) create mode 100644 samples/java-crud-shopping-cart/src/main/java/io/cloudstate/samples/shoppingcart/Main.java create mode 100644 samples/java-crud-shopping-cart/src/main/resources/application.conf create mode 100644 samples/java-crud-shopping-cart/src/main/resources/simplelogger.properties diff --git a/build.sbt b/build.sbt index c9ba1877f..137cd81e3 100644 --- a/build.sbt +++ b/build.sbt @@ -109,6 +109,7 @@ lazy val root = (project in file(".")) `java-support`, `scala-support`, `java-shopping-cart`, + `java-crud-shopping-cart`, `java-pingpong`, `akka-client`, operator, @@ -727,6 +728,26 @@ lazy val `java-shopping-cart` = (project in file("samples/java-shopping-cart")) assemblySettings("java-shopping-cart.jar") ) +lazy val `java-crud-shopping-cart` = (project in file("samples/java-crud-shopping-cart")) + .dependsOn(`java-support`) + .enablePlugins(AkkaGrpcPlugin, AssemblyPlugin, JavaAppPackaging, DockerPlugin, AutomateHeaderPlugin, NoPublish) + .settings( + name := "java-crud-shopping-cart", + dockerSettings, + mainClass in Compile := Some("io.cloudstate.samples.shoppingcart.Main"), + PB.generate in Compile := (PB.generate in Compile).dependsOn(PB.generate in (`java-support`, Compile)).value, + akkaGrpcGeneratedLanguages := Seq(AkkaGrpc.Java), + PB.protoSources in Compile ++= { + val baseDir = (baseDirectory in ThisBuild).value / "protocols" + Seq(baseDir / "frontend", baseDir / "example") + }, + PB.targets in Compile := Seq( + PB.gens.java -> (sourceManaged in Compile).value + ), + javacOptions in Compile ++= Seq("-encoding", "UTF-8", "-source", "1.8", "-target", "1.8"), + assemblySettings("java-crud-shopping-cart.jar") + ) + lazy val `java-pingpong` = (project in file("samples/java-pingpong")) .dependsOn(`java-support`) .enablePlugins(AkkaGrpcPlugin, AssemblyPlugin, JavaAppPackaging, DockerPlugin, AutomateHeaderPlugin, NoPublish) @@ -823,7 +844,7 @@ lazy val `tck` = (project in file("tck")) javaOptions in IntegrationTest := sys.props.get("config.resource").map(r => s"-Dconfig.resource=$r").toSeq, parallelExecution in IntegrationTest := false, executeTests in IntegrationTest := (executeTests in IntegrationTest) - .dependsOn(`proxy-core` / assembly, `java-shopping-cart` / assembly, `scala-shopping-cart` / assembly) + .dependsOn(`proxy-core` / assembly, `java-shopping-cart` / assembly, `java-crud-shopping-cart` / assembly, `scala-shopping-cart` / assembly) .value ) diff --git a/samples/java-crud-shopping-cart/src/main/java/io/cloudstate/samples/shoppingcart/Main.java b/samples/java-crud-shopping-cart/src/main/java/io/cloudstate/samples/shoppingcart/Main.java new file mode 100644 index 000000000..928a41c63 --- /dev/null +++ b/samples/java-crud-shopping-cart/src/main/java/io/cloudstate/samples/shoppingcart/Main.java @@ -0,0 +1,33 @@ +/* + * Copyright 2019 Lightbend Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.cloudstate.samples.shoppingcart; + +import com.example.crud.shoppingcart.Shoppingcart; +import io.cloudstate.javasupport.CloudState; + +public final class Main { + public static final void main(String[] args) throws Exception { + new CloudState() + .registerCrudEntity( + ShoppingCartEntity.class, + Shoppingcart.getDescriptor().findServiceByName("ShoppingCart"), + com.example.crud.shoppingcart.persistence.Domain.getDescriptor()) + .start() + .toCompletableFuture() + .get(); + } +} diff --git a/samples/java-crud-shopping-cart/src/main/resources/application.conf b/samples/java-crud-shopping-cart/src/main/resources/application.conf new file mode 100644 index 000000000..b9d4ac102 --- /dev/null +++ b/samples/java-crud-shopping-cart/src/main/resources/application.conf @@ -0,0 +1,7 @@ +cloudstate { + system { + loggers = ["akka.event.slf4j.Slf4jLogger"] + loglevel = "DEBUG" + logging-filter = "akka.event.slf4j.Slf4jLoggingFilter" + } +} \ No newline at end of file diff --git a/samples/java-crud-shopping-cart/src/main/resources/simplelogger.properties b/samples/java-crud-shopping-cart/src/main/resources/simplelogger.properties new file mode 100644 index 000000000..6cea6c360 --- /dev/null +++ b/samples/java-crud-shopping-cart/src/main/resources/simplelogger.properties @@ -0,0 +1,13 @@ +org.slf4j.simpleLogger.logFile=System.out +org.slf4j.simpleLogger.cacheOutputStream=false +org.slf4j.simpleLogger.defaultLogLevel=debug +org.slf4j.simpleLogger.log.io.cloudstate.javasupport=debug +org.slf4j.simpleLogger.log.io.cloudstate=debug +org.slf4j.simpleLogger.log.akka=debug +org.slf4j.simpleLogger.showDateTime=true +org.slf4j.simpleLogger.dateTimeFormat=yyyy-MM-dd HH:mm:ss.SSS +org.slf4j.simpleLogger.showThreadName=false +org.slf4j.simpleLogger.showLogName=true +org.slf4j.simpleLogger.showShortLogName=false +org.slf4j.simpleLogger.levelInBrackets=false +org.slf4j.simpleLogger.warnLevelString=WARN diff --git a/samples/java-shopping-cart/src/main/java/io/cloudstate/samples/shoppingcart/Main.java b/samples/java-shopping-cart/src/main/java/io/cloudstate/samples/shoppingcart/Main.java index 7b4c4dded..f0aa30588 100644 --- a/samples/java-shopping-cart/src/main/java/io/cloudstate/samples/shoppingcart/Main.java +++ b/samples/java-shopping-cart/src/main/java/io/cloudstate/samples/shoppingcart/Main.java @@ -16,16 +16,16 @@ package io.cloudstate.samples.shoppingcart; -import com.example.crud.shoppingcart.Shoppingcart; import io.cloudstate.javasupport.*; +import com.example.shoppingcart.Shoppingcart; public final class Main { public static final void main(String[] args) throws Exception { new CloudState() - .registerCrudEntity( - ShoppingCartCrudEntity.class, + .registerEventSourcedEntity( + ShoppingCartEntity.class, Shoppingcart.getDescriptor().findServiceByName("ShoppingCart"), - com.example.crud.shoppingcart.persistence.Domain.getDescriptor()) + com.example.shoppingcart.persistence.Domain.getDescriptor()) .start() .toCompletableFuture() .get(); From 6e997f26b1921fa62402eeee03911477ea3d1aa6 Mon Sep 17 00:00:00 2001 From: Guy Youansi Date: Sun, 26 Jul 2020 21:09:32 +0200 Subject: [PATCH 18/93] remove old crud shopping cart --- .../cloudstate/samples/shoppingcart/ShoppingCartEntity.java} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename samples/{java-shopping-cart/src/main/java/io/cloudstate/samples/shoppingcart/ShoppingCartCrudEntity.java => java-crud-shopping-cart/src/main/java/io/cloudstate/samples/shoppingcart/ShoppingCartEntity.java} (97%) diff --git a/samples/java-shopping-cart/src/main/java/io/cloudstate/samples/shoppingcart/ShoppingCartCrudEntity.java b/samples/java-crud-shopping-cart/src/main/java/io/cloudstate/samples/shoppingcart/ShoppingCartEntity.java similarity index 97% rename from samples/java-shopping-cart/src/main/java/io/cloudstate/samples/shoppingcart/ShoppingCartCrudEntity.java rename to samples/java-crud-shopping-cart/src/main/java/io/cloudstate/samples/shoppingcart/ShoppingCartEntity.java index 3e9348bb9..7302000be 100644 --- a/samples/java-shopping-cart/src/main/java/io/cloudstate/samples/shoppingcart/ShoppingCartCrudEntity.java +++ b/samples/java-crud-shopping-cart/src/main/java/io/cloudstate/samples/shoppingcart/ShoppingCartEntity.java @@ -33,12 +33,12 @@ /** A CRUD entity. */ @CrudEntity -public class ShoppingCartCrudEntity { +public class ShoppingCartEntity { private final String entityId; private final Map cart = new LinkedHashMap<>(); - public ShoppingCartCrudEntity(@EntityId String entityId) { + public ShoppingCartEntity(@EntityId String entityId) { this.entityId = entityId; } From 9c9e02bd5f0d064d2b6769816058c97270775d49 Mon Sep 17 00:00:00 2001 From: Guy Youansi Date: Mon, 27 Jul 2020 22:26:33 +0200 Subject: [PATCH 19/93] add StreamFailed in the presstart method --- .../src/main/scala/io/cloudstate/proxy/crud/CrudEntity.scala | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/proxy/core/src/main/scala/io/cloudstate/proxy/crud/CrudEntity.scala b/proxy/core/src/main/scala/io/cloudstate/proxy/crud/CrudEntity.scala index 58bed6a83..fef382dee 100644 --- a/proxy/core/src/main/scala/io/cloudstate/proxy/crud/CrudEntity.scala +++ b/proxy/core/src/main/scala/io/cloudstate/proxy/crud/CrudEntity.scala @@ -89,7 +89,7 @@ final class CrudEntitySupervisor(client: CrudClient, NotUsed } ) - .runWith(Sink.actorRef(self, CrudEntity.StreamClosed)) + .runWith(Sink.actorRef(self, CrudEntity.StreamClosed, CrudEntity.StreamFailed)) context.become(waitingForRelay) } @@ -209,8 +209,6 @@ final class CrudEntity(configuration: CrudEntity.Configuration, if (reportedDatabaseOperationStarted) { reportDatabaseOperationFinished() } - // This will shutdown the stream (if not already shut down) - relay ! Status.Success(()) } private[this] final def commandHandled(): Unit = { From 164a650c9cd4d10854411455048f206ec61d6a63 Mon Sep 17 00:00:00 2001 From: Guy Youansi Date: Mon, 27 Jul 2020 22:27:00 +0200 Subject: [PATCH 20/93] add tests for crud entity --- .../proxy/crud/AbstractCrudEntitySpec.scala | 232 ++++++++++++++++++ .../proxy/crud/CrudEntitySpec.scala | 186 ++++++++++++++ 2 files changed, 418 insertions(+) create mode 100644 proxy/core/src/test/scala/io/cloudstate/proxy/crud/AbstractCrudEntitySpec.scala create mode 100644 proxy/core/src/test/scala/io/cloudstate/proxy/crud/CrudEntitySpec.scala diff --git a/proxy/core/src/test/scala/io/cloudstate/proxy/crud/AbstractCrudEntitySpec.scala b/proxy/core/src/test/scala/io/cloudstate/proxy/crud/AbstractCrudEntitySpec.scala new file mode 100644 index 000000000..8cb9a73b6 --- /dev/null +++ b/proxy/core/src/test/scala/io/cloudstate/proxy/crud/AbstractCrudEntitySpec.scala @@ -0,0 +1,232 @@ +/* + * Copyright 2019 Lightbend Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.cloudstate.proxy.crud + +import akka.actor.{ActorRef, ActorSystem, PoisonPill} +import akka.testkit.{ImplicitSender, TestKit, TestProbe} +import com.google.protobuf.ByteString +import com.google.protobuf.any.{Any => ProtoAny} +import com.typesafe.config.{Config, ConfigFactory} +import io.cloudstate.protocol.crud._ +import io.cloudstate.protocol.entity.{ClientAction, Command, Failure} +import io.cloudstate.proxy.ConcurrencyEnforcer +import io.cloudstate.proxy.ConcurrencyEnforcer.ConcurrencyEnforcerSettings +import io.cloudstate.proxy.entity.{EntityCommand, UserFunctionReply} +import org.scalatest._ +import org.scalatest.concurrent.Eventually +import org.scalatest.time.{Millis, Seconds, Span} + +import scala.concurrent.Await +import scala.concurrent.duration._ + +object AbstractCrudEntitySpec { + + def config: Config = + ConfigFactory + .parseString(""" + | # use in-memory journal for testing + | cloudstate.proxy.journal-enabled = true + | akka.persistence { + | journal.plugin = "akka.persistence.journal.inmem" + | snapshot-store.plugin = inmem-snapshot-store + | } + | inmem-snapshot-store.class = "io.cloudstate.proxy.crud.InMemSnapshotStore" + """.stripMargin) + + final val ServiceName = "some.ServiceName" + final val UserFunctionName = "crud-user-function-name" + + // Some useful anys + final val command = ProtoAny("command", ByteString.copyFromUtf8("foo")) + final val state1 = ProtoAny("state", ByteString.copyFromUtf8("state1")) + final val state2 = ProtoAny("state", ByteString.copyFromUtf8("state2")) + +} + +abstract class AbstractCrudEntitySpec + extends TestKit(ActorSystem("CrudEntityTest", AbstractCrudEntitySpec.config)) + with WordSpecLike + with Matchers + with Inside + with Eventually + with BeforeAndAfter + with BeforeAndAfterAll + with ImplicitSender + with OptionValues { + + import AbstractCrudEntitySpec._ + + // These are set and read in the entities + @volatile protected var userFunction: TestProbe = _ + @volatile private var statsCollector: TestProbe = _ + @volatile private var concurrencyEnforcer: ActorRef = _ + + protected var entity: ActorRef = _ + protected var reactivatedEntity: ActorRef = _ + + // Incremented for each test by before() callback + private var idSeq = 0 + + override implicit def patienceConfig: PatienceConfig = PatienceConfig(Span(5, Seconds), Span(200, Millis)) + + protected def entityId: String = "entity" + idSeq.toString + + protected def sendAndExpectCommand(name: String, payload: ProtoAny, dest: ActorRef = entity): Long = { + dest ! EntityCommand(entityId, name, Some(payload)) + expectCommand(name, payload) + } + + private def expectCommand(name: String, payload: ProtoAny): Long = + inside(userFunction.expectMsgType[CrudStreamIn].message) { + case CrudStreamIn.Message.Command(Command(eid, cid, n, p, s, _, _)) => + eid should ===(entityId) + n should ===(name) + p shouldBe Some(payload) + s shouldBe false + cid + } + + protected def sendAndExpectReply(commandId: Long, + action: Option[CrudAction.Action] = None, + dest: ActorRef = entity): UserFunctionReply = { + sendReply(commandId, action, dest) + val reply = expectMsgType[UserFunctionReply] + reply.clientAction shouldBe None + reply + } + + protected def sendReply(commandId: Long, action: Option[CrudAction.Action] = None, dest: ActorRef = entity) = + dest ! CrudStreamOut( + CrudStreamOut.Message.Reply( + CrudReply( + commandId = commandId, + sideEffects = Nil, + clientAction = None, + crudAction = action.map(a => CrudAction(a)) + ) + ) + ) + + protected def sendAndExpectFailure(commandId: Long, description: String): UserFunctionReply = { + sendFailure(commandId, description) + val reply = expectMsgType[UserFunctionReply] + inside(reply.clientAction) { + case Some(ClientAction(ClientAction.Action.Failure(failure), _)) => + failure should ===(Failure(0, description)) + } + reply + } + + protected def sendFailure(commandId: Long, description: String) = + entity ! CrudStreamOut( + CrudStreamOut.Message.Failure( + Failure( + commandId = commandId, + description = description + ) + ) + ) + + protected def createAndExpectInitState(initState: Option[CrudInitState]): Unit = { + userFunction = TestProbe() + entity = system.actorOf( + CrudEntity.props( + CrudEntity.Configuration(ServiceName, UserFunctionName, 30.seconds, 100), + entityId, + userFunction.ref, + concurrencyEnforcer, + statsCollector.ref + ), + s"crud-test-entity-$entityId" + ) + + val init = userFunction.expectMsgType[CrudStreamIn] + inside(init.message) { + case CrudStreamIn.Message.Init(CrudInit(serviceName, eid, state, _)) => + serviceName should ===(ServiceName) + eid should ===(entityId) + state should ===(initState) + } + } + + protected def reactiveAndExpectInitState(initState: Option[CrudInitState]): Unit = { + cleanUpEntity() + + userFunction = TestProbe() + reactivatedEntity = system.actorOf( + CrudEntity.props( + CrudEntity.Configuration(ServiceName, UserFunctionName, 30.seconds, 100), + entityId, + userFunction.ref, + concurrencyEnforcer, + statsCollector.ref + ), + s"crud-test-entity-reactivated-$entityId" + ) + + val init = userFunction.expectMsgType[CrudStreamIn] + inside(init.message) { + case CrudStreamIn.Message.Init(CrudInit(serviceName, eid, state, _)) => + serviceName should ===(ServiceName) + eid should ===(entityId) + state should ===(initState) + } + } + + private def cleanUpEntity(): Unit = { + userFunction.testActor ! PoisonPill + userFunction = null + entity ! PoisonPill + entity = null + } + + before { + idSeq += 1 + } + + after { + if (entity != null) { + cleanUpEntity() + } + + if (reactivatedEntity != null) { + userFunction.testActor ! PoisonPill + userFunction = null + reactivatedEntity ! PoisonPill + reactivatedEntity = null + } + } + + override protected def beforeAll(): Unit = { + statsCollector = TestProbe() + concurrencyEnforcer = system.actorOf( + ConcurrencyEnforcer.props(ConcurrencyEnforcerSettings(1, 10.second, 5.second), statsCollector.ref), + "concurrency-enforcer" + ) + } + + override protected def afterAll(): Unit = { + statsCollector.testActor ! PoisonPill + statsCollector = null + concurrencyEnforcer ! PoisonPill + concurrencyEnforcer = null + + Await.ready(system.terminate(), 10.seconds) + shutdown() + } + +} diff --git a/proxy/core/src/test/scala/io/cloudstate/proxy/crud/CrudEntitySpec.scala b/proxy/core/src/test/scala/io/cloudstate/proxy/crud/CrudEntitySpec.scala new file mode 100644 index 000000000..b97fbb5fe --- /dev/null +++ b/proxy/core/src/test/scala/io/cloudstate/proxy/crud/CrudEntitySpec.scala @@ -0,0 +1,186 @@ +/* + * Copyright 2019 Lightbend Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.cloudstate.proxy.crud + +import io.cloudstate.protocol.crud.CrudAction.Action.{Delete, Update} +import io.cloudstate.protocol.crud._ +import io.cloudstate.protocol.entity.{ClientAction, Failure} +import io.cloudstate.proxy.crud.CrudEntity.{Stop, StreamClosed, StreamFailed} +import io.cloudstate.proxy.entity.{EntityCommand, UserFunctionReply} + +import scala.concurrent.duration._ + +class CrudEntitySpec extends AbstractCrudEntitySpec { + + import AbstractCrudEntitySpec._ + + "The CrudEntity" should { + + "be initialised successfully" in { + createAndExpectInitState(Some(CrudInitState())) + userFunction.expectNoMessage(200.millis) + expectNoMessage(200.millis) + } + + "handle update commands and reply" in { + createAndExpectInitState(Some(CrudInitState())) + watch(entity) + + val commandId1 = sendAndExpectCommand("cmd", command) + sendAndExpectReply(commandId1, Some(Update(CrudUpdate(Some(state1))))) + + val commandId2 = sendAndExpectCommand("cmd", command) + sendAndExpectReply(commandId2, Some(Update(CrudUpdate(Some(state2))))) + + // passivating the entity + entity ! Stop + expectTerminated(entity) + + // reactivating the entity + reactiveAndExpectInitState(Some(CrudInitState(Some(state2), 2))) + + val commandId3 = sendAndExpectCommand("cmd", command, reactivatedEntity) + sendAndExpectReply(commandId3, Some(Update(CrudUpdate(Some(state2)))), reactivatedEntity) + + userFunction.expectNoMessage(200.millis) + expectNoMessage(200.millis) + } + + "handle delete command and reply" in { + createAndExpectInitState(Some(CrudInitState())) + watch(entity) + + val commandId1 = sendAndExpectCommand("cmd", command) + sendAndExpectReply(commandId1, Some(Update(CrudUpdate(Some(state1))))) + + val commandId2 = sendAndExpectCommand("cmd", command) + sendAndExpectReply(commandId2, Some(Delete(CrudDelete()))) + + // passivating the entity + entity ! Stop + expectTerminated(entity) + + // reactivating the entity + reactiveAndExpectInitState(Some(CrudInitState(None, 2))) + + userFunction.expectNoMessage(200.millis) + expectNoMessage(200.millis) + } + + "handle failure reply" in { + createAndExpectInitState(Some(CrudInitState())) + val cid = sendAndExpectCommand("cmd", command) + sendAndExpectFailure(cid, "description") + userFunction.expectNoMessage(200.millis) + expectNoMessage(200.millis) + } + + "stash commands when another command is being executed" in { + createAndExpectInitState(Some(CrudInitState())) + sendAndExpectCommand("cmd", command) + entity ! EntityCommand(entityId, "cmd", Some(command)) + userFunction.expectNoMessage(200.millis) + expectNoMessage(200.millis) + } + + "crash when there is no command being executed for the reply" in { + createAndExpectInitState(Some(CrudInitState())) + sendReply(-1, None) + // expect crash and restart + userFunction.expectMsgType[CrudStreamIn] + userFunction.expectNoMessage(200.millis) + expectNoMessage(200.millis) + } + + "crash when the command being executed does not have the same command id as the one of the reply" in { + createAndExpectInitState(Some(CrudInitState())) + sendAndExpectCommand("cmd", command) + // send reply with wrong command id + sendReply(-1, None) + + inside(expectMsgType[UserFunctionReply].clientAction) { + case Some(ClientAction(ClientAction.Action.Failure(failure), _)) => + failure should ===(Failure(0, "Incorrect command id in reply, expecting 1 but got -1")) + } + expectNoMessage(200.millis) + } + + "crash on failure reply with command id 0" in { + createAndExpectInitState(Some(CrudInitState())) + sendFailure(0, "description") + expectNoMessage(200.millis) + } + + "crash on failure reply when there is no command being executed" in { + createAndExpectInitState(Some(CrudInitState())) + sendFailure(1, "description") + expectNoMessage(200.millis) + } + + "crash on failure reply when the command being executed does not have the same command id as the one of the reply" in { + createAndExpectInitState(Some(CrudInitState())) + sendAndExpectCommand("cmd", command) + //entity ! EntityCommand(entityId, "cmd", Some(command)) + sendFailure(-1, "description") + inside(expectMsgType[UserFunctionReply].clientAction) { + case Some(ClientAction(ClientAction.Action.Failure(failure), _)) => + failure should ===(Failure(0, "Incorrect command id in failure, expecting 1 but got -1")) + } + expectNoMessage(200.millis) + } + + "crash when output message is empty and no command is being executed" in { + createAndExpectInitState(Some(CrudInitState())) + entity ! CrudStreamOut(CrudStreamOut.Message.Empty) + expectNoMessage(200.millis) + } + + "crash when stream out message is empty and a command is being executed" in { + createAndExpectInitState(Some(CrudInitState())) + sendAndExpectCommand("cmd", command) + entity ! CrudStreamOut(CrudStreamOut.Message.Empty) + inside(expectMsgType[UserFunctionReply].clientAction) { + case Some(ClientAction(ClientAction.Action.Failure(failure), _)) => + failure should ===(Failure(0, "Empty or unknown message from entity output stream")) + } + expectNoMessage(200.millis) + } + + "stop when received StreamClosed message" in { + createAndExpectInitState(Some(CrudInitState())) + watch(entity) + + sendAndExpectCommand("cmd", command) + entity ! StreamClosed + inside(expectMsgType[UserFunctionReply].clientAction) { + case Some(ClientAction(ClientAction.Action.Failure(failure), _)) => + failure should ===(Failure(0, "Unexpected CRUD entity termination")) + } + expectTerminated(entity) + } + + "handle StreamFailed message" in { + createAndExpectInitState(Some(CrudInitState())) + sendAndExpectCommand("cmd", command) + entity ! StreamFailed(new RuntimeException("test stream failed")) + inside(expectMsgType[UserFunctionReply].clientAction) { + case Some(ClientAction(ClientAction.Action.Failure(failure), _)) => + failure should ===(Failure(0, "Unexpected CRUD entity termination")) + } + } + } +} From 9a3cf5354bed0626db73d7b3419d7770e94579f7 Mon Sep 17 00:00:00 2001 From: Guy Youansi Date: Mon, 27 Jul 2020 22:37:01 +0200 Subject: [PATCH 21/93] removed obsolete classes --- .../javasupport/crud/SnapshotContext.java | 27 ------------ .../javasupport/crud/SnapshotHandler.java | 42 ------------------- 2 files changed, 69 deletions(-) delete mode 100644 java-support/src/main/java/io/cloudstate/javasupport/crud/SnapshotContext.java delete mode 100644 java-support/src/main/java/io/cloudstate/javasupport/crud/SnapshotHandler.java diff --git a/java-support/src/main/java/io/cloudstate/javasupport/crud/SnapshotContext.java b/java-support/src/main/java/io/cloudstate/javasupport/crud/SnapshotContext.java deleted file mode 100644 index 6301bdc44..000000000 --- a/java-support/src/main/java/io/cloudstate/javasupport/crud/SnapshotContext.java +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright 2019 Lightbend Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.cloudstate.javasupport.crud; - -/** A snapshot context. */ -public interface SnapshotContext extends CrudContext { - /** - * The sequence number of the last event that this snapshot includes. - * - * @return The sequence number. - */ - long sequenceNumber(); -} diff --git a/java-support/src/main/java/io/cloudstate/javasupport/crud/SnapshotHandler.java b/java-support/src/main/java/io/cloudstate/javasupport/crud/SnapshotHandler.java deleted file mode 100644 index 47e1d6b06..000000000 --- a/java-support/src/main/java/io/cloudstate/javasupport/crud/SnapshotHandler.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright 2019 Lightbend Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.cloudstate.javasupport.crud; - -import io.cloudstate.javasupport.impl.CloudStateAnnotation; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/** - * Marks a method as a snapshot handler. - * - *

If, when recovering an entity, that entity has a snapshot, the snapshot will be passed to a - * corresponding snapshot handler method whose argument matches its type. The entity must set its - * current state to that snapshot. - * - *

An entity may declare more than one snapshot handler if it wants different handling for - * different types. - * - *

The snapshot handler method may additionally accept a {@link SnapshotContext} parameter, - * allowing it to access context for the snapshot, if required. - */ -@CloudStateAnnotation -@Target(ElementType.METHOD) -@Retention(RetentionPolicy.RUNTIME) -public @interface SnapshotHandler {} From c5de37d927a4f46318aa458c7c4907b965f4eafc Mon Sep 17 00:00:00 2001 From: Guy Youansi Date: Wed, 29 Jul 2020 00:46:05 +0200 Subject: [PATCH 22/93] add annotations for update and delete handlers --- .../javasupport/crud/CrudEntityHandler.java | 9 +- .../javasupport/crud/DeleteStateHandler.java | 41 +++++++ .../javasupport/crud/UpdateStateHandler.java | 41 +++++++ .../javasupport/crud/package-info.java | 5 +- .../crud/AnnotationBasedCrudSupport.scala | 89 ++++++++++----- .../javasupport/impl/crud/CrudImpl.scala | 17 +-- .../crud/AnnotationBasedCrudSupportSpec.scala | 105 ++++++++++++++---- .../shoppingcart/ShoppingCartEntity.java | 15 ++- 8 files changed, 260 insertions(+), 62 deletions(-) create mode 100644 java-support/src/main/java/io/cloudstate/javasupport/crud/DeleteStateHandler.java create mode 100644 java-support/src/main/java/io/cloudstate/javasupport/crud/UpdateStateHandler.java diff --git a/java-support/src/main/java/io/cloudstate/javasupport/crud/CrudEntityHandler.java b/java-support/src/main/java/io/cloudstate/javasupport/crud/CrudEntityHandler.java index 036edfb31..c0a13467d 100644 --- a/java-support/src/main/java/io/cloudstate/javasupport/crud/CrudEntityHandler.java +++ b/java-support/src/main/java/io/cloudstate/javasupport/crud/CrudEntityHandler.java @@ -44,5 +44,12 @@ public interface CrudEntityHandler { * @param state The state to handle. * @param context The state context. */ - void handleState(Optional state, StateContext context); + void handleUpdate(Any state, StateContext context); + + /** + * Handle the state deletion. + * + * @param context The state context. + */ + void handleDelete(StateContext context); } diff --git a/java-support/src/main/java/io/cloudstate/javasupport/crud/DeleteStateHandler.java b/java-support/src/main/java/io/cloudstate/javasupport/crud/DeleteStateHandler.java new file mode 100644 index 000000000..42088c2c6 --- /dev/null +++ b/java-support/src/main/java/io/cloudstate/javasupport/crud/DeleteStateHandler.java @@ -0,0 +1,41 @@ +/* + * Copyright 2019 Lightbend Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.cloudstate.javasupport.crud; + +import io.cloudstate.javasupport.impl.CloudStateAnnotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Marks a method as a delete state handler. + * + *

If, when recovering an entity, that entity has a state, the state will be passed to a + * corresponding state handler method whose argument matches its type. The entity must set its + * current state to that state. + * + *

An entity must declare only one delete state handler. + * + *

The delete state handler method may additionally accept a {@link StateContext} parameter, + * allowing it to access context for the state, if required. + */ +@CloudStateAnnotation +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface DeleteStateHandler {} diff --git a/java-support/src/main/java/io/cloudstate/javasupport/crud/UpdateStateHandler.java b/java-support/src/main/java/io/cloudstate/javasupport/crud/UpdateStateHandler.java new file mode 100644 index 000000000..213ea0cdc --- /dev/null +++ b/java-support/src/main/java/io/cloudstate/javasupport/crud/UpdateStateHandler.java @@ -0,0 +1,41 @@ +/* + * Copyright 2019 Lightbend Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.cloudstate.javasupport.crud; + +import io.cloudstate.javasupport.impl.CloudStateAnnotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Marks a method as a update state handler. + * + *

If, when recovering an entity, that entity has a state, the state will be passed to a + * corresponding state handler method whose argument matches its type. The entity must set its + * current state to that state. + * + *

An entity must declare only one update state handler. + * + *

The update state handler method may additionally accept a {@link StateContext} parameter, + * allowing it to access context for the state, if required. + */ +@CloudStateAnnotation +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface UpdateStateHandler {} diff --git a/java-support/src/main/java/io/cloudstate/javasupport/crud/package-info.java b/java-support/src/main/java/io/cloudstate/javasupport/crud/package-info.java index 191c48548..905ee484d 100644 --- a/java-support/src/main/java/io/cloudstate/javasupport/crud/package-info.java +++ b/java-support/src/main/java/io/cloudstate/javasupport/crud/package-info.java @@ -5,7 +5,8 @@ * io.cloudstate.javasupport.crud.CrudEntity @CrudEntity} annotation, and supply command handlers * using the {@link io.cloudstate.javasupport.crud.CommandHandler @CommandHandler} annotation. * - *

In addition, {@link io.cloudstate.javasupport.crud.StateHandler @StateHandler} annotated - * methods should be defined to handle entity state. + *

In addition, {@link io.cloudstate.javasupport.crud.UpdateStateHandler @UpdateStateHandler} and + * {@link io.cloudstate.javasupport.crud.DeleteStateHandler @DeleteStateHandler} annotated methods + * should be defined to handle entity state respectively. */ package io.cloudstate.javasupport.crud; diff --git a/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/AnnotationBasedCrudSupport.scala b/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/AnnotationBasedCrudSupport.scala index f54e43f7b..ee3498d52 100644 --- a/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/AnnotationBasedCrudSupport.scala +++ b/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/AnnotationBasedCrudSupport.scala @@ -29,8 +29,9 @@ import io.cloudstate.javasupport.crud.{ CrudEntityCreationContext, CrudEntityFactory, CrudEntityHandler, + DeleteStateHandler, StateContext, - StateHandler + UpdateStateHandler } import io.cloudstate.javasupport.impl.ReflectionHelper.{InvocationContext, MainArgumentParameterHandler} import io.cloudstate.javasupport.impl.{AnySupport, ReflectionHelper, ResolvedEntityFactory, ResolvedServiceMethod} @@ -82,11 +83,10 @@ private[impl] class AnnotationBasedCrudSupport( } } - override def handleState(anyState: Optional[JavaPbAny], context: StateContext): Unit = unwrap { - import scala.compat.java8.OptionConverters._ - val state = anyState.asScala.map(s => anySupport.decode(s)).asJava.asInstanceOf[AnyRef] + override def handleUpdate(anyState: JavaPbAny, context: StateContext): Unit = unwrap { + val state = anySupport.decode(anyState).asInstanceOf[AnyRef] - behavior.getCachedStateHandlerForClass(state.getClass) match { + behavior.getCachedUpdateHandlerForClass(state.getClass) match { case Some(handler) => val ctx = new DelegatingCrudContext(context) with StateContext { override def sequenceNumber(): Long = context.sequenceNumber() @@ -94,11 +94,19 @@ private[impl] class AnnotationBasedCrudSupport( handler.invoke(entity, state, ctx) case None => throw new RuntimeException( - s"No state handler found for state ${state.getClass} on $behaviorsString" + s"No update state handler found for ${state.getClass} on $behaviorsString" ) } } + override def handleDelete(context: StateContext): Unit = unwrap { + behavior.deleteHandler match { + case Some(handler) => handler.invoke(entity, context) + case None => + throw new RuntimeException(s"No delete state handler found on $behaviorsString") + } + } + private def unwrap[T](block: => T): T = try { block @@ -118,13 +126,14 @@ private[impl] class AnnotationBasedCrudSupport( private class CrudBehaviorReflection( val commandHandlers: Map[String, ReflectionHelper.CommandHandlerInvoker[CommandContext]], - val stateHandlers: Map[Class[_], StateHandlerInvoker] + val updateHandlers: Map[Class[_], UpdateInvoker], + val deleteHandler: Option[DeleteInvoker] ) { - private val stateHandlerCache = TrieMap.empty[Class[_], Option[StateHandlerInvoker]] + private val updateStateHandlerCache = TrieMap.empty[Class[_], Option[UpdateInvoker]] - def getCachedStateHandlerForClass(clazz: Class[_]): Option[StateHandlerInvoker] = - stateHandlerCache.getOrElseUpdate(clazz, getHandlerForClass(stateHandlers)(clazz)) + def getCachedUpdateHandlerForClass(clazz: Class[_]): Option[UpdateInvoker] = + updateStateHandlerCache.getOrElseUpdate(clazz, getHandlerForClass(updateHandlers)(clazz)) private def getHandlerForClass[T](handlers: Map[Class[_], T])(clazz: Class[_]): Option[T] = handlers.get(clazz) match { @@ -169,28 +178,40 @@ private object CrudBehaviorReflection { ) } - val stateHandlers = allMethods - .filter(_.getAnnotation(classOf[StateHandler]) != null) + val updateStateHandlers = allMethods + .filter(_.getAnnotation(classOf[UpdateStateHandler]) != null) .map { method => - new StateHandlerInvoker(ReflectionHelper.ensureAccessible(method)) + new UpdateInvoker(ReflectionHelper.ensureAccessible(method)) } - .groupBy(_.snapshotClass) + .groupBy(_.stateClass) .map { - case (snapshotClass, Seq(invoker)) => (snapshotClass: Any) -> invoker + case (stateClass, Seq(invoker)) => (stateClass: Any) -> invoker case (clazz, many) => throw new RuntimeException( - s"Multiple methods found for handling snapshot of type $clazz: ${many.map(_.method.getName)}" + s"Multiple CRUD update handlers found of type $clazz: ${many.map(_.method.getName)}" ) } - .asInstanceOf[Map[Class[_], StateHandlerInvoker]] + .asInstanceOf[Map[Class[_], UpdateInvoker]] + + val deleteStateHandler = allMethods + .filter(_.getAnnotation(classOf[DeleteStateHandler]) != null) + .map { method => + new DeleteInvoker(ReflectionHelper.ensureAccessible(method)) + } match { + case Seq() => None + case Seq(single) => + Some(single) + case _ => + throw new RuntimeException(s"Multiple CRUD delete methods found on behavior $behaviorClass") + } ReflectionHelper.validateNoBadMethods( allMethods, classOf[CrudEntity], - Set(classOf[CommandHandler], classOf[StateHandler]) + Set(classOf[CommandHandler], classOf[UpdateStateHandler], classOf[DeleteStateHandler]) ) - new CrudBehaviorReflection(commandHandlers, stateHandlers) + new CrudBehaviorReflection(commandHandlers, updateStateHandlers, deleteStateHandler) } } @@ -208,23 +229,41 @@ private class EntityConstructorInvoker(constructor: Constructor[_]) extends (Cru } } -private class StateHandlerInvoker(val method: Method) { +private class UpdateInvoker(val method: Method) { private val parameters = ReflectionHelper.getParameterHandlers[StateContext](method)() - // Verify that there is at most one state handler - val snapshotClass: Class[_] = parameters.collect { + // Verify that there is at most one update state handler + val stateClass: Class[_] = parameters.collect { case MainArgumentParameterHandler(clazz) => clazz } match { case Array(handlerClass) => handlerClass case other => throw new RuntimeException( - s"StateHandler method $method must defined at most one non context parameter to handle state, the parameters defined were: ${other + s"UpdateStateHandler method $method must defined at most one non context parameter to handle state, the parameters defined were: ${other .mkString(",")}" ) } - def invoke(obj: AnyRef, snapshot: AnyRef, context: StateContext): Unit = { - val ctx = InvocationContext(snapshot, context) + def invoke(obj: AnyRef, state: AnyRef, context: StateContext): Unit = { + val ctx = InvocationContext(state, context) + method.invoke(obj, parameters.map(_.apply(ctx)): _*) + } +} + +private class DeleteInvoker(val method: Method) { + + private val parameters = ReflectionHelper.getParameterHandlers[StateContext](method)() + + parameters.foreach { + case MainArgumentParameterHandler(clazz) => + throw new RuntimeException( + s"DeleteStateHandler method $method must defined only a context parameter to handle the state, the parameter defined is: ${clazz.getName}" + ) + case _ => + } + + def invoke(obj: AnyRef, context: StateContext): Unit = { + val ctx = InvocationContext("", context) method.invoke(obj, parameters.map(_.apply(ctx)): _*) } } diff --git a/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/CrudImpl.scala b/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/CrudImpl.scala index 0f1b6c550..38d7486b4 100644 --- a/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/CrudImpl.scala +++ b/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/CrudImpl.scala @@ -108,16 +108,18 @@ final class CrudImpl(_system: ActorSystem, val handler = service.factory.create(new CrudContextImpl(init.entityId)) val thisEntityId = init.entityId - val (startingSequenceNumber, state) = init.state match { + val startingSequenceNumber = init.state match { case Some(CrudInitState(Some(payload), stateSequence, _)) => val encoded = service.anySupport.encodeScala(payload) - (stateSequence, Some(ScalaPbAny.toJavaProto(encoded)).asJava) + handler.handleUpdate(ScalaPbAny.toJavaProto(encoded), new StateContextImpl(thisEntityId, stateSequence)) + stateSequence - case Some(CrudInitState(None, stateSequence, _)) => (stateSequence, Option.empty[JavaPbAny].asJava) + case Some(CrudInitState(None, stateSequence, _)) => + handler.handleDelete(new StateContextImpl(thisEntityId, stateSequence)) + stateSequence - case None => (0L, Option.empty[JavaPbAny].asJava) + case None => 0L // first initialization } - handler.handleState(state, new StateContextImpl(thisEntityId, startingSequenceNumber)) Flow[CrudStreamIn] .map(_.message) @@ -233,8 +235,7 @@ final class CrudImpl(_system: ActorSystem, val encoded = anySupport.encodeScala(event) _nextSequenceNumber += 1 - handler.handleState(Some(ScalaPbAny.toJavaProto(encoded)).asJava, - new StateContextImpl(entityId, _nextSequenceNumber)) + handler.handleUpdate(ScalaPbAny.toJavaProto(encoded), new StateContextImpl(entityId, _nextSequenceNumber)) mayBeAction = Some(CrudAction(Update(CrudUpdate(Some(encoded))))) updatePerformSnapshot() } @@ -243,7 +244,7 @@ final class CrudImpl(_system: ActorSystem, checkActive() _nextSequenceNumber += 1 - handler.handleState(Option.empty[JavaPbAny].asJava, new StateContextImpl(entityId, _nextSequenceNumber)) + handler.handleDelete(new StateContextImpl(entityId, _nextSequenceNumber)) mayBeAction = Some(CrudAction(Delete(CrudDelete()))) updatePerformSnapshot() } diff --git a/java-support/src/test/scala/io/cloudstate/javasupport/impl/crud/AnnotationBasedCrudSupportSpec.scala b/java-support/src/test/scala/io/cloudstate/javasupport/impl/crud/AnnotationBasedCrudSupportSpec.scala index 496506692..3a572b708 100644 --- a/java-support/src/test/scala/io/cloudstate/javasupport/impl/crud/AnnotationBasedCrudSupportSpec.scala +++ b/java-support/src/test/scala/io/cloudstate/javasupport/impl/crud/AnnotationBasedCrudSupportSpec.scala @@ -16,8 +16,6 @@ package io.cloudstate.javasupport.impl.crud -import java.util.Optional - import com.example.crud.shoppingcart.Shoppingcart import com.google.protobuf.any.{Any => ScalaPbAny} import com.google.protobuf.{ByteString, Any => JavaPbAny} @@ -27,15 +25,14 @@ import io.cloudstate.javasupport.crud.{ CrudContext, CrudEntity, CrudEntityCreationContext, + DeleteStateHandler, StateContext, - StateHandler + UpdateStateHandler } import io.cloudstate.javasupport.impl.{AnySupport, ResolvedServiceMethod, ResolvedType} import io.cloudstate.javasupport.{Context, EntityId, ServiceCall, ServiceCallFactory, ServiceCallRef} import org.scalatest.{Matchers, WordSpec} -import scala.compat.java8.OptionConverters._ - class AnnotationBasedCrudSupportSpec extends WordSpec with Matchers { trait BaseContext extends Context { override def serviceCallFactory(): ServiceCallFactory = new ServiceCallFactory { @@ -225,61 +222,123 @@ class AnnotationBasedCrudSupportSpec extends WordSpec with Matchers { } - "support state handlers" when { + "support update state handlers" when { val ctx = new StateContext with BaseContext { override def sequenceNumber(): Long = 10 + override def entityId(): String = "foo" } "single parameter" in { var invoked = false val handler = create(new { - @StateHandler - def handleState(state: Optional[String]): Unit = { - state should ===(Option("state!").asJava) + @UpdateStateHandler + def updateState(state: String): Unit = { + state should ===("state!") invoked = true } }) - handler.handleState(Option(state("state!")).asJava, ctx) + handler.handleUpdate(state("state!"), ctx) invoked shouldBe true } "context parameter" in { var invoked = false val handler = create(new { - @StateHandler - def handleState(state: Optional[String], context: StateContext): Unit = { - state should ===(Option("state!").asJava) + @UpdateStateHandler + def updateState(state: String, context: StateContext): Unit = { + state should ===("state!") context.sequenceNumber() should ===(10) invoked = true } }) - handler.handleState(Option(state("state!")).asJava, ctx) + handler.handleUpdate(state("state!"), ctx) invoked shouldBe true } "fail if there's a bad context" in { a[RuntimeException] should be thrownBy create(new { - @StateHandler - def handleState(state: Optional[String], context: CommandContext) = () + @UpdateStateHandler + def updateState(state: String, context: CommandContext): Unit = Unit }) } - "fail if there's no snapshot parameter" in { + "fail if there's no state parameter" in { a[RuntimeException] should be thrownBy create(new { - @StateHandler - def handleState(context: StateContext) = () + @UpdateStateHandler + def updateState(context: StateContext): Unit = Unit }) } - "fail if there's no state handler for the given type" in { + "fail if there's no update handler for the given type" in { val handler = create(new { - @StateHandler - def handleState(state: Int) = () + @UpdateStateHandler + def updateState(state: Int): Unit = Unit }) - a[RuntimeException] should be thrownBy handler.handleState(Option(state(10)).asJava, ctx) + a[RuntimeException] should be thrownBy handler.handleUpdate(state(10), ctx) } + "fail if there are two update handler methods" in { + a[RuntimeException] should be thrownBy create(new { + @UpdateStateHandler + def updateState1(context: StateContext): Unit = Unit + @UpdateStateHandler + def updateState2(context: StateContext): Unit = Unit + }) + } + } + + "support delete state handlers" when { + val ctx = new StateContext with BaseContext { + override def sequenceNumber(): Long = 10 + override def entityId(): String = "foo" + } + + "no arg parameter" in { + var invoked = false + val handler = create(new { + @DeleteStateHandler + def deleteState(): Unit = + invoked = true + }) + handler.handleDelete(ctx) + invoked shouldBe true + } + + "context parameter" in { + var invoked = false + val handler = create(new { + @DeleteStateHandler + def deleteState(context: StateContext): Unit = + invoked = true + }) + handler.handleDelete(ctx) + invoked shouldBe true + } + + "fail if there's a single argument is not the context" in { + a[RuntimeException] should be thrownBy create(new { + @DeleteStateHandler + def deleteState(state: String): Unit = Unit + }) + } + + "fail if there's two delete methods" in { + a[RuntimeException] should be thrownBy create(new { + @DeleteStateHandler + def deleteState1: Unit = Unit + + @DeleteStateHandler + def deleteState2: Unit = Unit + }) + } + + "fail if there's a bad context" in { + a[RuntimeException] should be thrownBy create(new { + @DeleteStateHandler + def deleteState(context: CommandContext): Unit = Unit + }) + } } } } diff --git a/samples/java-crud-shopping-cart/src/main/java/io/cloudstate/samples/shoppingcart/ShoppingCartEntity.java b/samples/java-crud-shopping-cart/src/main/java/io/cloudstate/samples/shoppingcart/ShoppingCartEntity.java index 7302000be..96d94b1ef 100644 --- a/samples/java-crud-shopping-cart/src/main/java/io/cloudstate/samples/shoppingcart/ShoppingCartEntity.java +++ b/samples/java-crud-shopping-cart/src/main/java/io/cloudstate/samples/shoppingcart/ShoppingCartEntity.java @@ -23,7 +23,8 @@ import io.cloudstate.javasupport.crud.CommandContext; import io.cloudstate.javasupport.crud.CommandHandler; import io.cloudstate.javasupport.crud.CrudEntity; -import io.cloudstate.javasupport.crud.StateHandler; +import io.cloudstate.javasupport.crud.DeleteStateHandler; +import io.cloudstate.javasupport.crud.UpdateStateHandler; import java.util.LinkedHashMap; import java.util.List; @@ -42,8 +43,16 @@ public ShoppingCartEntity(@EntityId String entityId) { this.entityId = entityId; } - @StateHandler - public void handleState(Optional cart) { + @UpdateStateHandler + public void handleUpdateState(Domain.Cart cart) { + this.cart.clear(); + for (Domain.LineItem item : cart.getItemsList()) { + this.cart.put(item.getProductId(), convert(item)); + } + } + + @DeleteStateHandler + public void handleDeleteState(Optional cart) { this.cart.clear(); cart.ifPresent( c -> { From 0979a9ebbedffe017a3695533dac168ac74816dc Mon Sep 17 00:00:00 2001 From: Guy Youansi Date: Wed, 29 Jul 2020 00:56:35 +0200 Subject: [PATCH 23/93] fixed scala format --- build.sbt | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/build.sbt b/build.sbt index 137cd81e3..6e1559646 100644 --- a/build.sbt +++ b/build.sbt @@ -742,8 +742,8 @@ lazy val `java-crud-shopping-cart` = (project in file("samples/java-crud-shoppin Seq(baseDir / "frontend", baseDir / "example") }, PB.targets in Compile := Seq( - PB.gens.java -> (sourceManaged in Compile).value - ), + PB.gens.java -> (sourceManaged in Compile).value + ), javacOptions in Compile ++= Seq("-encoding", "UTF-8", "-source", "1.8", "-target", "1.8"), assemblySettings("java-crud-shopping-cart.jar") ) @@ -844,7 +844,10 @@ lazy val `tck` = (project in file("tck")) javaOptions in IntegrationTest := sys.props.get("config.resource").map(r => s"-Dconfig.resource=$r").toSeq, parallelExecution in IntegrationTest := false, executeTests in IntegrationTest := (executeTests in IntegrationTest) - .dependsOn(`proxy-core` / assembly, `java-shopping-cart` / assembly, `java-crud-shopping-cart` / assembly, `scala-shopping-cart` / assembly) + .dependsOn(`proxy-core` / assembly, + `java-shopping-cart` / assembly, + `java-crud-shopping-cart` / assembly, + `scala-shopping-cart` / assembly) .value ) From 516900317fee12ad3113b569a2e937c4456acb2d Mon Sep 17 00:00:00 2001 From: Guy Youansi Date: Wed, 29 Jul 2020 01:18:16 +0200 Subject: [PATCH 24/93] fixed unit value --- .../crud/AnnotationBasedCrudSupportSpec.scala | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/java-support/src/test/scala/io/cloudstate/javasupport/impl/crud/AnnotationBasedCrudSupportSpec.scala b/java-support/src/test/scala/io/cloudstate/javasupport/impl/crud/AnnotationBasedCrudSupportSpec.scala index 3a572b708..369151fd5 100644 --- a/java-support/src/test/scala/io/cloudstate/javasupport/impl/crud/AnnotationBasedCrudSupportSpec.scala +++ b/java-support/src/test/scala/io/cloudstate/javasupport/impl/crud/AnnotationBasedCrudSupportSpec.scala @@ -259,21 +259,21 @@ class AnnotationBasedCrudSupportSpec extends WordSpec with Matchers { "fail if there's a bad context" in { a[RuntimeException] should be thrownBy create(new { @UpdateStateHandler - def updateState(state: String, context: CommandContext): Unit = Unit + def updateState(state: String, context: CommandContext): Unit = () }) } "fail if there's no state parameter" in { a[RuntimeException] should be thrownBy create(new { @UpdateStateHandler - def updateState(context: StateContext): Unit = Unit + def updateState(context: StateContext): Unit = () }) } "fail if there's no update handler for the given type" in { val handler = create(new { @UpdateStateHandler - def updateState(state: Int): Unit = Unit + def updateState(state: Int): Unit = () }) a[RuntimeException] should be thrownBy handler.handleUpdate(state(10), ctx) } @@ -281,9 +281,9 @@ class AnnotationBasedCrudSupportSpec extends WordSpec with Matchers { "fail if there are two update handler methods" in { a[RuntimeException] should be thrownBy create(new { @UpdateStateHandler - def updateState1(context: StateContext): Unit = Unit + def updateState1(context: StateContext): Unit = () @UpdateStateHandler - def updateState2(context: StateContext): Unit = Unit + def updateState2(context: StateContext): Unit = () }) } } @@ -319,24 +319,24 @@ class AnnotationBasedCrudSupportSpec extends WordSpec with Matchers { "fail if there's a single argument is not the context" in { a[RuntimeException] should be thrownBy create(new { @DeleteStateHandler - def deleteState(state: String): Unit = Unit + def deleteState(state: String): Unit = () }) } "fail if there's two delete methods" in { a[RuntimeException] should be thrownBy create(new { @DeleteStateHandler - def deleteState1: Unit = Unit + def deleteState1: Unit = () @DeleteStateHandler - def deleteState2: Unit = Unit + def deleteState2: Unit = () }) } "fail if there's a bad context" in { a[RuntimeException] should be thrownBy create(new { @DeleteStateHandler - def deleteState(context: CommandContext): Unit = Unit + def deleteState(context: CommandContext): Unit = () }) } } From 1321d1e449ca733fdf28cd4fdc8dbce4c652fb1e Mon Sep 17 00:00:00 2001 From: Guy Youansi Date: Wed, 29 Jul 2020 08:43:18 +0200 Subject: [PATCH 25/93] fixed protobuf attribute index and fixed crud init state handling --- .../cloudstate/javasupport/impl/crud/CrudImpl.scala | 13 ++++++++++++- protocols/protocol/cloudstate/crud.proto | 4 ++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/CrudImpl.scala b/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/CrudImpl.scala index 38d7486b4..58f642496 100644 --- a/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/CrudImpl.scala +++ b/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/CrudImpl.scala @@ -70,6 +70,17 @@ final class CrudImpl(_system: ActorSystem, }) .toMap + /** + * One stream will be established per active entity. + * Once established, the first message sent will be Init, which contains the entity ID, and, + * a state if the entity has previously persisted one. The entity is expected to apply the + * received state to its state. Once the Init message is sent, one to many commands are sent, + * with new commands being sent as new requests for the entity come in. The entity is expected + * to reply to each command with exactly one reply message. The entity should reply in order + * and any state update that the entity requests to be persisted the entity should handle itself. + * The entity handles state update by replacing its own state with the update, + * as if they had arrived as state update when the stream was being replayed on load. + */ override def handle( in: akka.stream.scaladsl.Source[CrudStreamIn, akka.NotUsed] ): akka.stream.scaladsl.Source[CrudStreamOut, akka.NotUsed] = @@ -118,7 +129,7 @@ final class CrudImpl(_system: ActorSystem, handler.handleDelete(new StateContextImpl(thisEntityId, stateSequence)) stateSequence - case None => 0L // first initialization + case ignoredCase => 0L // should not happen! } Flow[CrudStreamIn] diff --git a/protocols/protocol/cloudstate/crud.proto b/protocols/protocol/cloudstate/crud.proto index 932514eef..ebb1ffe39 100644 --- a/protocols/protocol/cloudstate/crud.proto +++ b/protocols/protocol/cloudstate/crud.proto @@ -64,10 +64,10 @@ message CrudInit { // The state of the entity when it is first activated. message CrudInitState { // The value of the entity state, if the entity has already been created. - google.protobuf.Any value = 3; + google.protobuf.Any value = 1; // The sequence number of the entity state. - int64 sequence = 4; + int64 sequence = 2; } // Output message type for the gRPC stream out. From c6e162aab5973afbf06796f4a8bfba227b5de1d0 Mon Sep 17 00:00:00 2001 From: Guy Youansi Date: Thu, 30 Jul 2020 16:22:54 +0200 Subject: [PATCH 26/93] fixed shopping cart entity --- .../shoppingcart/ShoppingCartEntity.java | 53 ++++++++++--------- 1 file changed, 27 insertions(+), 26 deletions(-) diff --git a/samples/java-crud-shopping-cart/src/main/java/io/cloudstate/samples/shoppingcart/ShoppingCartEntity.java b/samples/java-crud-shopping-cart/src/main/java/io/cloudstate/samples/shoppingcart/ShoppingCartEntity.java index 96d94b1ef..ad58ba6b9 100644 --- a/samples/java-crud-shopping-cart/src/main/java/io/cloudstate/samples/shoppingcart/ShoppingCartEntity.java +++ b/samples/java-crud-shopping-cart/src/main/java/io/cloudstate/samples/shoppingcart/ShoppingCartEntity.java @@ -29,13 +29,12 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import java.util.Optional; import java.util.stream.Collectors; +import java.util.stream.Stream; /** A CRUD entity. */ @CrudEntity public class ShoppingCartEntity { - private final String entityId; private final Map cart = new LinkedHashMap<>(); @@ -52,14 +51,8 @@ public void handleUpdateState(Domain.Cart cart) { } @DeleteStateHandler - public void handleDeleteState(Optional cart) { + public void handleDeleteState() { this.cart.clear(); - cart.ifPresent( - c -> { - for (Domain.LineItem item : c.getItemsList()) { - this.cart.put(item.getProductId(), convert(item)); - } - }); } @CommandHandler @@ -67,17 +60,6 @@ public Shoppingcart.Cart getCart() { return Shoppingcart.Cart.newBuilder().addAllItems(cart.values()).build(); } - @CommandHandler - public Empty removeCart(Shoppingcart.RemoveShoppingCart cartItem, CommandContext ctx) { - if (!entityId.equals(cartItem.getUserId())) { - ctx.fail("Cannot remove unknown cart " + cartItem.getUserId()); - } - cart.clear(); - - ctx.delete(); - return Empty.getDefaultInstance(); - } - @CommandHandler public Empty addItem(Shoppingcart.AddLineItem item, CommandContext ctx) { if (item.getQuantity() <= 0) { @@ -96,11 +78,8 @@ public Empty addItem(Shoppingcart.AddLineItem item, CommandContext ctx) { lineItem = lineItem.toBuilder().setQuantity(lineItem.getQuantity() + item.getQuantity()).build(); } - cart.put(item.getProductId(), lineItem); - List lineItems = - cart.values().stream().map(this::convert).collect(Collectors.toList()); - ctx.update(Domain.Cart.newBuilder().addAllItems(lineItems).build()); + ctx.update(Domain.Cart.newBuilder().addAllItems(addItem(item, lineItem)).build()); return Empty.getDefaultInstance(); } @@ -109,14 +88,36 @@ public Empty removeItem(Shoppingcart.RemoveLineItem item, CommandContext ctx) { if (!cart.containsKey(item.getProductId())) { ctx.fail("Cannot remove item " + item.getProductId() + " because it is not in the cart."); } - cart.remove(item.getProductId()); List lineItems = - cart.values().stream().map(this::convert).collect(Collectors.toList()); + cart.values().stream() + .filter(lineItem -> !lineItem.getProductId().equals(item.getProductId())) + .map(this::convert) + .collect(Collectors.toList()); + ctx.update(Domain.Cart.newBuilder().addAllItems(lineItems).build()); return Empty.getDefaultInstance(); } + @CommandHandler + public Empty removeCart(Shoppingcart.RemoveShoppingCart cartItem, CommandContext ctx) { + if (!entityId.equals(cartItem.getUserId())) { + ctx.fail("Cannot remove unknown cart " + cartItem.getUserId()); + } + + ctx.delete(); + return Empty.getDefaultInstance(); + } + + private List addItem( + Shoppingcart.AddLineItem addItem, Shoppingcart.LineItem lineItem) { + Stream stream = + cart.values().stream().filter(li -> !li.getProductId().equals(addItem.getProductId())); + return Stream.concat(stream, Stream.of(lineItem)) + .map(this::convert) + .collect(Collectors.toList()); + } + private Shoppingcart.LineItem convert(Domain.LineItem item) { return Shoppingcart.LineItem.newBuilder() .setProductId(item.getProductId()) From 0675b17ba4beac127399b4e28ad460fbdce42f16 Mon Sep 17 00:00:00 2001 From: Guy Youansi Date: Wed, 26 Aug 2020 21:24:07 +0200 Subject: [PATCH 27/93] call command handler only when the command was successful. Make the CommandContext generic to catch the type of the updateEntity method --- .../javasupport/crud/CommandContext.java | 6 +-- .../javasupport/crud/CrudEntityHandler.java | 2 +- .../crud/AnnotationBasedCrudSupport.scala | 8 ++-- .../javasupport/impl/crud/CrudImpl.scala | 43 +++++++++++++------ .../crud/AnnotationBasedCrudSupportSpec.scala | 20 ++++----- .../shoppingcart/ShoppingCartEntity.java | 13 +++--- 6 files changed, 55 insertions(+), 37 deletions(-) diff --git a/java-support/src/main/java/io/cloudstate/javasupport/crud/CommandContext.java b/java-support/src/main/java/io/cloudstate/javasupport/crud/CommandContext.java index f394deed0..87f059296 100644 --- a/java-support/src/main/java/io/cloudstate/javasupport/crud/CommandContext.java +++ b/java-support/src/main/java/io/cloudstate/javasupport/crud/CommandContext.java @@ -26,7 +26,7 @@ * or deleting the entity state in response to a command, along with forwarding the result to other * entities, and performing side effects on other entities. */ -public interface CommandContext extends CrudContext, ClientActionContext, EffectContext { +public interface CommandContext extends CrudContext, ClientActionContext, EffectContext { /** * The current sequence number of state in this entity. * @@ -53,8 +53,8 @@ public interface CommandContext extends CrudContext, ClientActionContext, Effect * * @param state The state to persist. */ - void update(Object state); + void updateEntity(T state); /** Delete the entity. */ - void delete(); + void deleteEntity(); } diff --git a/java-support/src/main/java/io/cloudstate/javasupport/crud/CrudEntityHandler.java b/java-support/src/main/java/io/cloudstate/javasupport/crud/CrudEntityHandler.java index c0a13467d..d0f68d202 100644 --- a/java-support/src/main/java/io/cloudstate/javasupport/crud/CrudEntityHandler.java +++ b/java-support/src/main/java/io/cloudstate/javasupport/crud/CrudEntityHandler.java @@ -36,7 +36,7 @@ public interface CrudEntityHandler { * @param context The command context. * @return The reply to the command, if the command isn't being forwarded elsewhere. */ - Optional handleCommand(Any command, CommandContext context); + Optional handleCommand(Any command, CommandContext context); /** * Handle the given state. diff --git a/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/AnnotationBasedCrudSupport.scala b/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/AnnotationBasedCrudSupport.scala index ee3498d52..3c4546f99 100644 --- a/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/AnnotationBasedCrudSupport.scala +++ b/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/AnnotationBasedCrudSupport.scala @@ -73,7 +73,7 @@ private[impl] class AnnotationBasedCrudSupport( }) } - override def handleCommand(command: JavaPbAny, context: CommandContext): Optional[JavaPbAny] = unwrap { + override def handleCommand(command: JavaPbAny, context: CommandContext[JavaPbAny]): Optional[JavaPbAny] = unwrap { behavior.commandHandlers.get(context.commandName()).map { handler => handler.invoke(entity, command, context) } getOrElse { @@ -125,7 +125,7 @@ private[impl] class AnnotationBasedCrudSupport( } private class CrudBehaviorReflection( - val commandHandlers: Map[String, ReflectionHelper.CommandHandlerInvoker[CommandContext]], + val commandHandlers: Map[String, ReflectionHelper.CommandHandlerInvoker[CommandContext[JavaPbAny]]], val updateHandlers: Map[Class[_], UpdateInvoker], val deleteHandler: Option[DeleteInvoker] ) { @@ -166,8 +166,8 @@ private object CrudBehaviorReflection { ) }) - new ReflectionHelper.CommandHandlerInvoker[CommandContext](ReflectionHelper.ensureAccessible(method), - serviceMethod) + new ReflectionHelper.CommandHandlerInvoker[CommandContext[JavaPbAny]](ReflectionHelper.ensureAccessible(method), + serviceMethod) } .groupBy(_.serviceMethod.name) .map { diff --git a/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/CrudImpl.scala b/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/CrudImpl.scala index 58f642496..2ca0bdade 100644 --- a/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/CrudImpl.scala +++ b/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/CrudImpl.scala @@ -181,6 +181,7 @@ final class CrudImpl(_system: ActorSystem, val clientAction = context.createClientAction(reply, false) if (!context.hasError) { + context.applyCrudAction() val endSequenceNumber = context.nextSequenceNumber val snapshot = if (context.performSnapshot) context.snapshot() else None @@ -231,33 +232,28 @@ final class CrudImpl(_system: ActorSystem, val anySupport: AnySupport, val handler: CrudEntityHandler, val snapshotEvery: Int) - extends CommandContext + extends CommandContext[JavaPbAny] with AbstractContext with AbstractClientActionContext with AbstractEffectContext with ActivatableContext { - private var _performSnapshot: Boolean = false + // TODO snapshot should removed + // TODO null check for state in updateEntity + + private var _performSnapshot: Boolean = false // NOT needed native CRUD will be used private var _nextSequenceNumber: Long = sequenceNumber private var mayBeAction: Option[CrudAction] = None - override def update(event: AnyRef): Unit = { + override def updateEntity(state: JavaPbAny): Unit = { checkActive() - - val encoded = anySupport.encodeScala(event) - _nextSequenceNumber += 1 - handler.handleUpdate(ScalaPbAny.toJavaProto(encoded), new StateContextImpl(entityId, _nextSequenceNumber)) + val encoded = anySupport.encodeScala(state) mayBeAction = Some(CrudAction(Update(CrudUpdate(Some(encoded))))) - updatePerformSnapshot() } - override def delete(): Unit = { + override def deleteEntity(): Unit = { checkActive() - - _nextSequenceNumber += 1 - handler.handleDelete(new StateContextImpl(entityId, _nextSequenceNumber)) mayBeAction = Some(CrudAction(Delete(CrudDelete()))) - updatePerformSnapshot() } def performSnapshot: Boolean = _performSnapshot @@ -266,6 +262,27 @@ final class CrudImpl(_system: ActorSystem, def crudAction(): Option[CrudAction] = mayBeAction + def applyCrudAction(): Unit = + mayBeAction match { + case Some(CrudAction(action, _)) => + action match { + case Update(CrudUpdate(Some(value), _)) => + _nextSequenceNumber += 1 + handler.handleUpdate(ScalaPbAny.toJavaProto(value), new StateContextImpl(entityId, _nextSequenceNumber)) + updatePerformSnapshot() + + case Delete(CrudDelete(_)) => + _nextSequenceNumber += 1 + handler.handleDelete(new StateContextImpl(entityId, _nextSequenceNumber)) + updatePerformSnapshot() + } + case None => + system.log.error( + s"Cloudstate protocol failure for CRUD entity: applying crud action for commandId: $commandId and commandName: $commandName" + ) + throw new IllegalStateException("CRUD Entity applied crud action in wrong state") + } + def snapshot(): Option[CrudSnapshot] = mayBeAction match { case Some(CrudAction(action, _)) => diff --git a/java-support/src/test/scala/io/cloudstate/javasupport/impl/crud/AnnotationBasedCrudSupportSpec.scala b/java-support/src/test/scala/io/cloudstate/javasupport/impl/crud/AnnotationBasedCrudSupportSpec.scala index 369151fd5..26bc0c9e8 100644 --- a/java-support/src/test/scala/io/cloudstate/javasupport/impl/crud/AnnotationBasedCrudSupportSpec.scala +++ b/java-support/src/test/scala/io/cloudstate/javasupport/impl/crud/AnnotationBasedCrudSupportSpec.scala @@ -45,13 +45,13 @@ class AnnotationBasedCrudSupportSpec extends WordSpec with Matchers { override def entityId(): String = "foo" } - class MockCommandContext extends CommandContext with BaseContext { + class MockCommandContext extends CommandContext[JavaPbAny] with BaseContext { var action: Option[AnyRef] = None override def sequenceNumber(): Long = 10 override def commandName(): String = "AddItem" override def commandId(): Long = 20 - override def update(state: AnyRef): Unit = action = Some(state) - override def delete(): Unit = action = None + override def updateEntity(state: JavaPbAny): Unit = action = Some(state) + override def deleteEntity(): Unit = action = None override def entityId(): String = "foo" override def fail(errorMessage: String): RuntimeException = ??? override def forward(to: ServiceCall): Unit = ??? @@ -146,7 +146,7 @@ class AnnotationBasedCrudSupportSpec extends WordSpec with Matchers { val handler = create( new { @CommandHandler - def addItem(msg: String, @EntityId eid: String, ctx: CommandContext): Wrapped = { + def addItem(msg: String, @EntityId eid: String, ctx: CommandContext[JavaPbAny]): Wrapped = { eid should ===("foo") ctx.commandName() should ===("AddItem") Wrapped(msg) @@ -161,8 +161,8 @@ class AnnotationBasedCrudSupportSpec extends WordSpec with Matchers { val handler = create( new { @CommandHandler - def addItem(msg: String, ctx: CommandContext): Wrapped = { - ctx.update(msg + " event") + def addItem(msg: String, ctx: CommandContext[JavaPbAny]): Wrapped = { + ctx.updateEntity(state(msg + " state")) ctx.commandName() should ===("AddItem") Wrapped(msg) } @@ -171,7 +171,7 @@ class AnnotationBasedCrudSupportSpec extends WordSpec with Matchers { ) val ctx = new MockCommandContext decodeWrapped(handler.handleCommand(command("blah"), ctx).get) should ===(Wrapped("blah")) - ctx.action should ===(Some("blah event")) + ctx.action.get should ===(state("blah state")) } "fail if there's a bad context type" in { @@ -185,7 +185,7 @@ class AnnotationBasedCrudSupportSpec extends WordSpec with Matchers { "fail if there's two command handlers for the same command" in { a[RuntimeException] should be thrownBy create(new { @CommandHandler - def addItem(msg: String, ctx: CommandContext) = + def addItem(msg: String, ctx: CommandContext[JavaPbAny]) = Wrapped(msg) @CommandHandler def addItem(msg: String) = @@ -259,7 +259,7 @@ class AnnotationBasedCrudSupportSpec extends WordSpec with Matchers { "fail if there's a bad context" in { a[RuntimeException] should be thrownBy create(new { @UpdateStateHandler - def updateState(state: String, context: CommandContext): Unit = () + def updateState(state: String, context: CommandContext[JavaPbAny]): Unit = () }) } @@ -336,7 +336,7 @@ class AnnotationBasedCrudSupportSpec extends WordSpec with Matchers { "fail if there's a bad context" in { a[RuntimeException] should be thrownBy create(new { @DeleteStateHandler - def deleteState(context: CommandContext): Unit = () + def deleteState(context: CommandContext[JavaPbAny]): Unit = () }) } } diff --git a/samples/java-crud-shopping-cart/src/main/java/io/cloudstate/samples/shoppingcart/ShoppingCartEntity.java b/samples/java-crud-shopping-cart/src/main/java/io/cloudstate/samples/shoppingcart/ShoppingCartEntity.java index ad58ba6b9..e401e2d5b 100644 --- a/samples/java-crud-shopping-cart/src/main/java/io/cloudstate/samples/shoppingcart/ShoppingCartEntity.java +++ b/samples/java-crud-shopping-cart/src/main/java/io/cloudstate/samples/shoppingcart/ShoppingCartEntity.java @@ -61,7 +61,7 @@ public Shoppingcart.Cart getCart() { } @CommandHandler - public Empty addItem(Shoppingcart.AddLineItem item, CommandContext ctx) { + public Empty addItem(Shoppingcart.AddLineItem item, CommandContext ctx) { if (item.getQuantity() <= 0) { ctx.fail("Cannot add negative quantity of to item " + item.getProductId()); } @@ -79,12 +79,12 @@ public Empty addItem(Shoppingcart.AddLineItem item, CommandContext ctx) { lineItem.toBuilder().setQuantity(lineItem.getQuantity() + item.getQuantity()).build(); } - ctx.update(Domain.Cart.newBuilder().addAllItems(addItem(item, lineItem)).build()); + ctx.updateEntity(Domain.Cart.newBuilder().addAllItems(addItem(item, lineItem)).build()); return Empty.getDefaultInstance(); } @CommandHandler - public Empty removeItem(Shoppingcart.RemoveLineItem item, CommandContext ctx) { + public Empty removeItem(Shoppingcart.RemoveLineItem item, CommandContext ctx) { if (!cart.containsKey(item.getProductId())) { ctx.fail("Cannot remove item " + item.getProductId() + " because it is not in the cart."); } @@ -95,17 +95,18 @@ public Empty removeItem(Shoppingcart.RemoveLineItem item, CommandContext ctx) { .map(this::convert) .collect(Collectors.toList()); - ctx.update(Domain.Cart.newBuilder().addAllItems(lineItems).build()); + ctx.updateEntity(Domain.Cart.newBuilder().addAllItems(lineItems).build()); return Empty.getDefaultInstance(); } @CommandHandler - public Empty removeCart(Shoppingcart.RemoveShoppingCart cartItem, CommandContext ctx) { + public Empty removeCart( + Shoppingcart.RemoveShoppingCart cartItem, CommandContext ctx) { if (!entityId.equals(cartItem.getUserId())) { ctx.fail("Cannot remove unknown cart " + cartItem.getUserId()); } - ctx.delete(); + ctx.deleteEntity(); return Empty.getDefaultInstance(); } From 928933c457770b9db7297066c9b965f2a254df52 Mon Sep 17 00:00:00 2001 From: Guy Youansi Date: Thu, 27 Aug 2020 00:48:58 +0200 Subject: [PATCH 28/93] fixed test --- .../core/src/test/scala/io/cloudstate/proxy/TestProxy.scala | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/proxy/core/src/test/scala/io/cloudstate/proxy/TestProxy.scala b/proxy/core/src/test/scala/io/cloudstate/proxy/TestProxy.scala index d317334ee..47c28da4b 100644 --- a/proxy/core/src/test/scala/io/cloudstate/proxy/TestProxy.scala +++ b/proxy/core/src/test/scala/io/cloudstate/proxy/TestProxy.scala @@ -25,6 +25,9 @@ import io.cloudstate.protocol.entity.ProxyInfo import io.cloudstate.protocol.event_sourced.EventSourced import io.cloudstate.protocol.function.StatelessFunction import java.net.{ConnectException, Socket} + +import io.cloudstate.protocol.crud.Crud + import scala.concurrent.duration._ object TestProxy { @@ -49,7 +52,8 @@ class TestProxy(servicePort: Int) { } """)) - val info: ProxyInfo = EntityDiscoveryManager.proxyInfo(Seq(Crdt.name, StatelessFunction.name, EventSourced.name)) + val info: ProxyInfo = + EntityDiscoveryManager.proxyInfo(Seq(Crdt.name, StatelessFunction.name, EventSourced.name, Crud.name)) val system: ActorSystem = CloudStateProxyMain.start(config) From 2310fc9822c1350add2f87989345d4f8fb2d532e Mon Sep 17 00:00:00 2001 From: Guy Youansi Date: Mon, 7 Sep 2020 15:20:21 +0200 Subject: [PATCH 29/93] add native CRUD support with slick and integrate the in memory CRUD support. adapt the CrudEntity behavior for the native CRUD support. remove the snapshot and the sequence from the GRPC protocol. remove generic for CommandContext. --- build.sbt | 6 +- .../io/cloudstate/javasupport/CloudState.java | 16 +- .../javasupport/crud/CommandContext.java | 7 +- .../javasupport/crud/CrudEntity.java | 7 - .../javasupport/crud/CrudEntityHandler.java | 3 +- .../crud/AnnotationBasedCrudSupport.scala | 8 +- .../javasupport/impl/crud/CrudImpl.scala | 77 +++------- .../crud/AnnotationBasedCrudSupportSpec.scala | 14 +- protocols/protocol/cloudstate/crud.proto | 14 -- proxy/core/src/main/resources/in-memory.conf | 7 +- proxy/core/src/main/resources/reference.conf | 56 +++++++ .../proxy/EntityDiscoveryManager.scala | 11 +- .../io/cloudstate/proxy/crud/CrudEntity.scala | 140 +++++++++--------- .../proxy/crud/CrudSupportFactory.scala | 7 +- .../scala/io/cloudstate/proxy/crud/readme.md | 22 +++ .../proxy/crud/store/JdbcConfig.scala | 64 ++++++++ .../crud/store/JdbcCrudStateQueries.scala | 41 +++++ .../proxy/crud/store/JdbcCrudStateTable.scala | 52 +++++++ .../proxy/crud/store/JdbcInMemoryStore.scala | 47 ++++++ .../proxy/crud/store/JdbcRepository.scala | 69 +++++++++ .../proxy/crud/store/JdbcStore.scala | 82 ++++++++++ .../proxy/crud/store/JdbcStoreFactory.scala | 50 +++++++ .../proxy/crud/store/package-info.java | 4 + .../proxy/crud/AbstractCrudEntitySpec.scala | 6 +- .../proxy/crud/CrudEntitySpec.scala | 4 +- .../shoppingcart/ShoppingCartEntity.java | 7 +- 26 files changed, 633 insertions(+), 188 deletions(-) create mode 100644 proxy/core/src/main/scala/io/cloudstate/proxy/crud/readme.md create mode 100644 proxy/core/src/main/scala/io/cloudstate/proxy/crud/store/JdbcConfig.scala create mode 100644 proxy/core/src/main/scala/io/cloudstate/proxy/crud/store/JdbcCrudStateQueries.scala create mode 100644 proxy/core/src/main/scala/io/cloudstate/proxy/crud/store/JdbcCrudStateTable.scala create mode 100644 proxy/core/src/main/scala/io/cloudstate/proxy/crud/store/JdbcInMemoryStore.scala create mode 100644 proxy/core/src/main/scala/io/cloudstate/proxy/crud/store/JdbcRepository.scala create mode 100644 proxy/core/src/main/scala/io/cloudstate/proxy/crud/store/JdbcStore.scala create mode 100644 proxy/core/src/main/scala/io/cloudstate/proxy/crud/store/JdbcStoreFactory.scala create mode 100644 proxy/core/src/main/scala/io/cloudstate/proxy/crud/store/package-info.java diff --git a/build.sbt b/build.sbt index d17904ab3..b9ca977fb 100644 --- a/build.sbt +++ b/build.sbt @@ -51,6 +51,7 @@ val Slf4jSimpleVersion = "1.7.30" val GraalVersion = "20.1.0" val DockerBaseImageVersion = "adoptopenjdk/openjdk11:debianslim-jre" val DockerBaseImageJavaLibraryPath = "${JAVA_HOME}/lib" +val SlickVersion = "3.3.2" val excludeTheseDependencies: Seq[ExclusionRule] = Seq( ExclusionRule("io.netty", "netty"), // grpc-java is using grpc-netty-shaded @@ -454,8 +455,11 @@ lazy val `proxy-core` = (project in file("proxy/core")) "com.thesamet.scalapb" %% "scalapb-runtime" % scalapb.compiler.Version.scalapbVersion % "protobuf", "io.prometheus" % "simpleclient" % PrometheusClientVersion, "io.prometheus" % "simpleclient_common" % PrometheusClientVersion, - "org.slf4j" % "slf4j-simple" % Slf4jSimpleVersion + "org.slf4j" % "slf4j-simple" % Slf4jSimpleVersion, //"ch.qos.logback" % "logback-classic" % "1.2.3", // Doesn't work well with SubstrateVM: https://github.com/vmencik/akka-graal-native/blob/master/README.md#logging + "com.typesafe.slick" %% "slick" % SlickVersion, //TODO: not sure here!!! + "com.typesafe.slick" %% "slick-hikaricp" % SlickVersion, //TODO: not sure here!!! + //"org.postgresql" % "postgresql" % "42.2.6" ), PB.protoSources in Compile ++= { val baseDir = (baseDirectory in ThisBuild).value / "protocols" diff --git a/java-support/src/main/java/io/cloudstate/javasupport/CloudState.java b/java-support/src/main/java/io/cloudstate/javasupport/CloudState.java index 3271bdc7b..e024e8d50 100644 --- a/java-support/src/main/java/io/cloudstate/javasupport/CloudState.java +++ b/java-support/src/main/java/io/cloudstate/javasupport/CloudState.java @@ -254,13 +254,10 @@ public CloudState registerCrudEntity( } final String persistenceId; - final int snapshotEvery; if (entity.persistenceId().isEmpty()) { persistenceId = entityClass.getSimpleName(); - snapshotEvery = 0; // Default } else { persistenceId = entity.persistenceId(); - snapshotEvery = entity.snapshotEvery(); } final AnySupport anySupport = newAnySupport(additionalDescriptors); @@ -271,8 +268,7 @@ public CloudState registerCrudEntity( new AnnotationBasedCrudSupport(entityClass, anySupport, descriptor), descriptor, anySupport, - persistenceId, - snapshotEvery)); + persistenceId)); return this; } @@ -286,9 +282,6 @@ public CloudState registerCrudEntity( * @param factory The CRUD factory. * @param descriptor The descriptor for the service that this entity implements. * @param persistenceId The persistence id for this entity. - * @param snapshotEvery Specifies how snapshots of the entity state should be made: Zero means use - * default from configuration file. (Default) Any negative value means never snapshot. Any - * positive value means snapshot at-or-after that number of events. * @param additionalDescriptors Any additional descriptors that should be used to look up protobuf * types when needed. * @return This stateful service builder. @@ -297,16 +290,11 @@ public CloudState registerCrudEntity( CrudEntityFactory factory, Descriptors.ServiceDescriptor descriptor, String persistenceId, - int snapshotEvery, Descriptors.FileDescriptor... additionalDescriptors) { services.put( descriptor.getFullName(), new CrudStatefulService( - factory, - descriptor, - newAnySupport(additionalDescriptors), - persistenceId, - snapshotEvery)); + factory, descriptor, newAnySupport(additionalDescriptors), persistenceId)); return this; } diff --git a/java-support/src/main/java/io/cloudstate/javasupport/crud/CommandContext.java b/java-support/src/main/java/io/cloudstate/javasupport/crud/CommandContext.java index 87f059296..6c47bb205 100644 --- a/java-support/src/main/java/io/cloudstate/javasupport/crud/CommandContext.java +++ b/java-support/src/main/java/io/cloudstate/javasupport/crud/CommandContext.java @@ -26,7 +26,10 @@ * or deleting the entity state in response to a command, along with forwarding the result to other * entities, and performing side effects on other entities. */ -public interface CommandContext extends CrudContext, ClientActionContext, EffectContext { +public interface CommandContext extends CrudContext, ClientActionContext, EffectContext { + + // TODO: add generic type for the state in updateEntity + /** * The current sequence number of state in this entity. * @@ -53,7 +56,7 @@ public interface CommandContext extends CrudContext, ClientActionContext, Eff * * @param state The state to persist. */ - void updateEntity(T state); + void updateEntity(Object state); /** Delete the entity. */ void deleteEntity(); diff --git a/java-support/src/main/java/io/cloudstate/javasupport/crud/CrudEntity.java b/java-support/src/main/java/io/cloudstate/javasupport/crud/CrudEntity.java index 83062d7eb..7a36610ea 100644 --- a/java-support/src/main/java/io/cloudstate/javasupport/crud/CrudEntity.java +++ b/java-support/src/main/java/io/cloudstate/javasupport/crud/CrudEntity.java @@ -35,11 +35,4 @@ * that you specify it explicitly. */ String persistenceId() default ""; - - /** - * Specifies how snapshots of the entity state should be made: Zero means use default from - * configuration file. (Default) Any negative value means never snapshot. Any positive value means - * snapshot at-or-after that number of events. - */ - int snapshotEvery() default 0; } diff --git a/java-support/src/main/java/io/cloudstate/javasupport/crud/CrudEntityHandler.java b/java-support/src/main/java/io/cloudstate/javasupport/crud/CrudEntityHandler.java index d0f68d202..2a2bef9c9 100644 --- a/java-support/src/main/java/io/cloudstate/javasupport/crud/CrudEntityHandler.java +++ b/java-support/src/main/java/io/cloudstate/javasupport/crud/CrudEntityHandler.java @@ -36,7 +36,8 @@ public interface CrudEntityHandler { * @param context The command context. * @return The reply to the command, if the command isn't being forwarded elsewhere. */ - Optional handleCommand(Any command, CommandContext context); + Optional handleCommand(Any command, CommandContext context); + // Optional handleCommand(Any command, CommandContext context); /** * Handle the given state. diff --git a/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/AnnotationBasedCrudSupport.scala b/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/AnnotationBasedCrudSupport.scala index 3c4546f99..ee3498d52 100644 --- a/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/AnnotationBasedCrudSupport.scala +++ b/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/AnnotationBasedCrudSupport.scala @@ -73,7 +73,7 @@ private[impl] class AnnotationBasedCrudSupport( }) } - override def handleCommand(command: JavaPbAny, context: CommandContext[JavaPbAny]): Optional[JavaPbAny] = unwrap { + override def handleCommand(command: JavaPbAny, context: CommandContext): Optional[JavaPbAny] = unwrap { behavior.commandHandlers.get(context.commandName()).map { handler => handler.invoke(entity, command, context) } getOrElse { @@ -125,7 +125,7 @@ private[impl] class AnnotationBasedCrudSupport( } private class CrudBehaviorReflection( - val commandHandlers: Map[String, ReflectionHelper.CommandHandlerInvoker[CommandContext[JavaPbAny]]], + val commandHandlers: Map[String, ReflectionHelper.CommandHandlerInvoker[CommandContext]], val updateHandlers: Map[Class[_], UpdateInvoker], val deleteHandler: Option[DeleteInvoker] ) { @@ -166,8 +166,8 @@ private object CrudBehaviorReflection { ) }) - new ReflectionHelper.CommandHandlerInvoker[CommandContext[JavaPbAny]](ReflectionHelper.ensureAccessible(method), - serviceMethod) + new ReflectionHelper.CommandHandlerInvoker[CommandContext](ReflectionHelper.ensureAccessible(method), + serviceMethod) } .groupBy(_.serviceMethod.name) .map { diff --git a/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/CrudImpl.scala b/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/CrudImpl.scala index 2ca0bdade..ae83f4249 100644 --- a/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/CrudImpl.scala +++ b/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/CrudImpl.scala @@ -35,8 +35,7 @@ import scala.compat.java8.OptionConverters._ final class CrudStatefulService(val factory: CrudEntityFactory, override val descriptor: Descriptors.ServiceDescriptor, val anySupport: AnySupport, - override val persistenceId: String, - val snapshotEvery: Int) + override val persistenceId: String) extends StatefulService { override def resolvedMethods: Option[Map[String, ResolvedServiceMethod[_, _]]] = @@ -46,12 +45,6 @@ final class CrudStatefulService(val factory: CrudEntityFactory, } override final val entityType = io.cloudstate.protocol.crud.Crud.name - - final def withSnapshotEvery(snapshotEvery: Int): CrudStatefulService = - if (snapshotEvery != this.snapshotEvery) - new CrudStatefulService(this.factory, this.descriptor, this.anySupport, this.persistenceId, snapshotEvery) - else - this } final class CrudImpl(_system: ActorSystem, @@ -62,13 +55,7 @@ final class CrudImpl(_system: ActorSystem, private final val system = _system private final implicit val ec = system.dispatcher - private final val services = _services.iterator - .map({ - case (name, crudss) => - // FIXME overlay configuration provided by _system - (name, if (crudss.snapshotEvery == 0) crudss.withSnapshotEvery(configuration.snapshotEvery) else crudss) - }) - .toMap + private final val services = _services.iterator.toMap /** * One stream will be established per active entity. @@ -118,18 +105,19 @@ final class CrudImpl(_system: ActorSystem, services.getOrElse(init.serviceName, throw new RuntimeException(s"Service not found: ${init.serviceName}")) val handler = service.factory.create(new CrudContextImpl(init.entityId)) val thisEntityId = init.entityId + val sequenceNumber = 0L //TODO: should be removed every where, CRUD do not need sequence!!! val startingSequenceNumber = init.state match { - case Some(CrudInitState(Some(payload), stateSequence, _)) => + case Some(CrudInitState(Some(payload), _)) => val encoded = service.anySupport.encodeScala(payload) - handler.handleUpdate(ScalaPbAny.toJavaProto(encoded), new StateContextImpl(thisEntityId, stateSequence)) - stateSequence + handler.handleUpdate(ScalaPbAny.toJavaProto(encoded), new StateContextImpl(thisEntityId, sequenceNumber)) + sequenceNumber - case Some(CrudInitState(None, stateSequence, _)) => - handler.handleDelete(new StateContextImpl(thisEntityId, stateSequence)) - stateSequence + case Some(CrudInitState(None, _)) => + handler.handleDelete(new StateContextImpl(thisEntityId, sequenceNumber)) + sequenceNumber - case ignoredCase => 0L // should not happen! + case _ => 0L // should not happen! } Flow[CrudStreamIn] @@ -168,8 +156,7 @@ final class CrudImpl(_system: ActorSystem, command.name, command.id, service.anySupport, - handler, - service.snapshotEvery + handler ) val reply = try { handler.handleCommand(cmd, context) @@ -183,7 +170,6 @@ final class CrudImpl(_system: ActorSystem, if (!context.hasError) { context.applyCrudAction() val endSequenceNumber = context.nextSequenceNumber - val snapshot = if (context.performSnapshot) context.snapshot() else None (endSequenceNumber, Some( @@ -192,8 +178,7 @@ final class CrudImpl(_system: ActorSystem, command.id, clientAction, context.sideEffects, - context.crudAction(), - snapshot + context.crudAction() ) ) )) @@ -230,22 +215,18 @@ final class CrudImpl(_system: ActorSystem, override val commandName: String, override val commandId: Long, val anySupport: AnySupport, - val handler: CrudEntityHandler, - val snapshotEvery: Int) - extends CommandContext[JavaPbAny] + val handler: CrudEntityHandler) + extends CommandContext with AbstractContext with AbstractClientActionContext with AbstractEffectContext with ActivatableContext { - // TODO snapshot should removed - // TODO null check for state in updateEntity - - private var _performSnapshot: Boolean = false // NOT needed native CRUD will be used private var _nextSequenceNumber: Long = sequenceNumber private var mayBeAction: Option[CrudAction] = None - override def updateEntity(state: JavaPbAny): Unit = { + override def updateEntity(state: AnyRef): Unit = { + // TODO null check for state checkActive() val encoded = anySupport.encodeScala(state) mayBeAction = Some(CrudAction(Update(CrudUpdate(Some(encoded))))) @@ -256,8 +237,6 @@ final class CrudImpl(_system: ActorSystem, mayBeAction = Some(CrudAction(Delete(CrudDelete()))) } - def performSnapshot: Boolean = _performSnapshot - def nextSequenceNumber: Long = _nextSequenceNumber def crudAction(): Option[CrudAction] = mayBeAction @@ -269,36 +248,14 @@ final class CrudImpl(_system: ActorSystem, case Update(CrudUpdate(Some(value), _)) => _nextSequenceNumber += 1 handler.handleUpdate(ScalaPbAny.toJavaProto(value), new StateContextImpl(entityId, _nextSequenceNumber)) - updatePerformSnapshot() case Delete(CrudDelete(_)) => _nextSequenceNumber += 1 handler.handleDelete(new StateContextImpl(entityId, _nextSequenceNumber)) - updatePerformSnapshot() } case None => - system.log.error( - s"Cloudstate protocol failure for CRUD entity: applying crud action for commandId: $commandId and commandName: $commandName" - ) - throw new IllegalStateException("CRUD Entity applied crud action in wrong state") + // ignored, nothing to do it is a get request! } - - def snapshot(): Option[CrudSnapshot] = - mayBeAction match { - case Some(CrudAction(action, _)) => - action match { - case Update(CrudUpdate(Some(value), _)) => Some(CrudSnapshot(Some(value))) - case Delete(CrudDelete(_)) => Some(CrudSnapshot(None)) - } - case None => - system.log.error( - s"Cloudstate protocol failure for CRUD entity: making a snapshot without performing a crud action for commandId: $commandId and commandName: $commandName" - ) - throw new IllegalStateException("CRUD Entity received snapshot in wrong state") - } - - private def updatePerformSnapshot(): Unit = - _performSnapshot = (snapshotEvery > 0) && (_performSnapshot || (_nextSequenceNumber % snapshotEvery == 0)) } private final class CrudContextImpl(override final val entityId: String) extends CrudContext with AbstractContext diff --git a/java-support/src/test/scala/io/cloudstate/javasupport/impl/crud/AnnotationBasedCrudSupportSpec.scala b/java-support/src/test/scala/io/cloudstate/javasupport/impl/crud/AnnotationBasedCrudSupportSpec.scala index 26bc0c9e8..0df974c1d 100644 --- a/java-support/src/test/scala/io/cloudstate/javasupport/impl/crud/AnnotationBasedCrudSupportSpec.scala +++ b/java-support/src/test/scala/io/cloudstate/javasupport/impl/crud/AnnotationBasedCrudSupportSpec.scala @@ -45,12 +45,12 @@ class AnnotationBasedCrudSupportSpec extends WordSpec with Matchers { override def entityId(): String = "foo" } - class MockCommandContext extends CommandContext[JavaPbAny] with BaseContext { + class MockCommandContext extends CommandContext with BaseContext { var action: Option[AnyRef] = None override def sequenceNumber(): Long = 10 override def commandName(): String = "AddItem" override def commandId(): Long = 20 - override def updateEntity(state: JavaPbAny): Unit = action = Some(state) + override def updateEntity(state: AnyRef): Unit = action = Some(state) override def deleteEntity(): Unit = action = None override def entityId(): String = "foo" override def fail(errorMessage: String): RuntimeException = ??? @@ -146,7 +146,7 @@ class AnnotationBasedCrudSupportSpec extends WordSpec with Matchers { val handler = create( new { @CommandHandler - def addItem(msg: String, @EntityId eid: String, ctx: CommandContext[JavaPbAny]): Wrapped = { + def addItem(msg: String, @EntityId eid: String, ctx: CommandContext): Wrapped = { eid should ===("foo") ctx.commandName() should ===("AddItem") Wrapped(msg) @@ -161,7 +161,7 @@ class AnnotationBasedCrudSupportSpec extends WordSpec with Matchers { val handler = create( new { @CommandHandler - def addItem(msg: String, ctx: CommandContext[JavaPbAny]): Wrapped = { + def addItem(msg: String, ctx: CommandContext): Wrapped = { ctx.updateEntity(state(msg + " state")) ctx.commandName() should ===("AddItem") Wrapped(msg) @@ -185,7 +185,7 @@ class AnnotationBasedCrudSupportSpec extends WordSpec with Matchers { "fail if there's two command handlers for the same command" in { a[RuntimeException] should be thrownBy create(new { @CommandHandler - def addItem(msg: String, ctx: CommandContext[JavaPbAny]) = + def addItem(msg: String, ctx: CommandContext) = Wrapped(msg) @CommandHandler def addItem(msg: String) = @@ -259,7 +259,7 @@ class AnnotationBasedCrudSupportSpec extends WordSpec with Matchers { "fail if there's a bad context" in { a[RuntimeException] should be thrownBy create(new { @UpdateStateHandler - def updateState(state: String, context: CommandContext[JavaPbAny]): Unit = () + def updateState(state: String, context: CommandContext): Unit = () }) } @@ -336,7 +336,7 @@ class AnnotationBasedCrudSupportSpec extends WordSpec with Matchers { "fail if there's a bad context" in { a[RuntimeException] should be thrownBy create(new { @DeleteStateHandler - def deleteState(context: CommandContext[JavaPbAny]): Unit = () + def deleteState(context: CommandContext): Unit = () }) } } diff --git a/protocols/protocol/cloudstate/crud.proto b/protocols/protocol/cloudstate/crud.proto index ebb1ffe39..02b22d214 100644 --- a/protocols/protocol/cloudstate/crud.proto +++ b/protocols/protocol/cloudstate/crud.proto @@ -65,9 +65,6 @@ message CrudInit { message CrudInitState { // The value of the entity state, if the entity has already been created. google.protobuf.Any value = 1; - - // The sequence number of the entity state. - int64 sequence = 2; } // Output message type for the gRPC stream out. @@ -91,17 +88,6 @@ message CrudReply { // The action to take on the CRUD entity CrudAction crud_action = 4; - - // An optional snapshot to persist. - // It is assumed that this snapshot will have the state of any actions in the crud action field applied to it. - // It is illegal to send a snapshot without sending any crud action. - CrudSnapshot snapshot = 5; -} - -// A snapshot of the entity. -message CrudSnapshot { - // The value of the snapshot, if a snapshot has already been created. - google.protobuf.Any value = 1; } // An action to take for changing the entity state. diff --git a/proxy/core/src/main/resources/in-memory.conf b/proxy/core/src/main/resources/in-memory.conf index b51fac2f9..b631a435f 100644 --- a/proxy/core/src/main/resources/in-memory.conf +++ b/proxy/core/src/main/resources/in-memory.conf @@ -12,4 +12,9 @@ inmem-snapshot-store { class = "io.cloudstate.proxy.eventsourced.InMemSnapshotStore" } -cloudstate.proxy.journal-enabled = true \ No newline at end of file +cloudstate.proxy.journal-enabled = true + +# Configuration for using an in memory CRUD store +cloudstate.proxy.crud-enabled = true + +cloudstate.proxy.crud.store-type = "in-memory" \ No newline at end of file diff --git a/proxy/core/src/main/resources/reference.conf b/proxy/core/src/main/resources/reference.conf index 8df926396..821937399 100644 --- a/proxy/core/src/main/resources/reference.conf +++ b/proxy/core/src/main/resources/reference.conf @@ -134,4 +134,60 @@ cloudstate.proxy { manage-topics-and-subscriptions = "manually" } } + + # Enable the CRUD the functionality by setting it to true + crud-enabled = false + + # Configures the CRUD functionality when crud-enabled is true + crud { + # This property indicate the type of CRUD store to be used. + # Valid options are: "using-jdbc", "in-memory" + # "in-memory" means the data are persisted in memory. + # "jdbc" means the data are persisted in a configured native JDBC database. + store-type = "in-memory" + + # This property indicates which configuration must be used by Slick. + jdbc.database.slick { + # connectionPool = disabled + connectionPool = "HikariCP" + + # This property indicates which profile must be used by Slick. + # Possible values are: slick.jdbc.PostgresProfile$, slick.jdbc.MySQLProfile$ and slick.jdbc.H2Profile$ + # (uncomment and set the property below to match your needs) + # profile = "slick.jdbc.PostgresProfile$" + + # The JDBC driver to use + # (uncomment and set the property below to match your needs) + # driver = "org.postgresql.Driver" + + # The JDBC URL for the chosen database + # (uncomment and set the property below to match your needs) + # url = "jdbc:postgresql://localhost:5432/cloudstate" + + # The database username + # (uncomment and set the property below to match your needs) + # user = "cloudstate" + + # The database password + # (uncomment and set the property below to match your needs) + # password = "cloudstate" + + # TODO: may be more properties here!!! help needed!!! + } + + # This property indicates the CRUD table in use. + jdbc-state-store { + tables { + state { + tableName = "crud_state_entity" + schemaName = "public" + columnNames { + persistentId = "persistent_id" + entityId = "entity_id" + state = "state" + } + } + } + } + } } diff --git a/proxy/core/src/main/scala/io/cloudstate/proxy/EntityDiscoveryManager.scala b/proxy/core/src/main/scala/io/cloudstate/proxy/EntityDiscoveryManager.scala index 81a8856a8..a37c0b969 100644 --- a/proxy/core/src/main/scala/io/cloudstate/proxy/EntityDiscoveryManager.scala +++ b/proxy/core/src/main/scala/io/cloudstate/proxy/EntityDiscoveryManager.scala @@ -76,6 +76,7 @@ object EntityDiscoveryManager { concurrencySettings: ConcurrencyEnforcerSettings, statsCollectorSettings: StatsCollectorSettings, journalEnabled: Boolean, + crudEnabled: Boolean, config: Config ) { validate() @@ -99,6 +100,7 @@ object EntityDiscoveryManager { ), statsCollectorSettings = new StatsCollectorSettings(config.getConfig("stats")), journalEnabled = config.getBoolean("journal-enabled"), + crudEnabled = config.getBoolean("crud-enabled"), config = config) } @@ -199,8 +201,13 @@ class EntityDiscoveryManager(config: EntityDiscoveryManager.Configuration)( config, clientSettings, concurrencyEnforcer = concurrencyEnforcer, - statsCollector = statsCollector), - Crud.name -> new CrudSupportFactory(context.system, + statsCollector = statsCollector) + ) + else Map.empty + } ++ { + if (config.crudEnabled) + Map( + Crud.name -> new CrudSupportFactory(system, config, clientSettings, concurrencyEnforcer = concurrencyEnforcer, diff --git a/proxy/core/src/main/scala/io/cloudstate/proxy/crud/CrudEntity.scala b/proxy/core/src/main/scala/io/cloudstate/proxy/crud/CrudEntity.scala index fef382dee..c6a2d2ef1 100644 --- a/proxy/core/src/main/scala/io/cloudstate/proxy/crud/CrudEntity.scala +++ b/proxy/core/src/main/scala/io/cloudstate/proxy/crud/CrudEntity.scala @@ -22,18 +22,18 @@ import java.util.concurrent.atomic.AtomicLong import akka.NotUsed import akka.actor._ import akka.cluster.sharding.ShardRegion -import akka.persistence._ +import akka.pattern.pipe import akka.stream.scaladsl._ import akka.stream.{CompletionStrategy, Materializer, OverflowStrategy} import akka.util.Timeout import com.google.protobuf.any.{Any => pbAny} import io.cloudstate.protocol.crud.CrudAction.Action.{Delete, Update} import io.cloudstate.protocol.crud.{ + CrudAction, CrudClient, CrudInit, CrudInitState, CrudReply, - CrudSnapshot, CrudStreamIn, CrudStreamOut, CrudUpdate @@ -41,9 +41,12 @@ import io.cloudstate.protocol.crud.{ import io.cloudstate.protocol.entity._ import io.cloudstate.proxy.ConcurrencyEnforcer.{Action, ActionCompleted} import io.cloudstate.proxy.StatsCollector +import io.cloudstate.proxy.crud.store.JdbcRepository +import io.cloudstate.proxy.crud.store.JdbcStore.Key import io.cloudstate.proxy.entity.{EntityCommand, UserFunctionReply} import scala.collection.immutable.Queue +import scala.concurrent.Future object CrudEntitySupervisor { @@ -52,8 +55,9 @@ object CrudEntitySupervisor { def props(client: CrudClient, configuration: CrudEntity.Configuration, concurrencyEnforcer: ActorRef, - statsCollector: ActorRef)(implicit mat: Materializer): Props = - Props(new CrudEntitySupervisor(client, configuration, concurrencyEnforcer, statsCollector)) + statsCollector: ActorRef, + repository: JdbcRepository)(implicit mat: Materializer): Props = + Props(new CrudEntitySupervisor(client, configuration, concurrencyEnforcer, statsCollector, repository)) } /** @@ -69,7 +73,8 @@ object CrudEntitySupervisor { final class CrudEntitySupervisor(client: CrudClient, configuration: CrudEntity.Configuration, concurrencyEnforcer: ActorRef, - statsCollector: ActorRef)(implicit mat: Materializer) + statsCollector: ActorRef, + repository: JdbcRepository)(implicit mat: Materializer) extends Actor with Stash { @@ -99,7 +104,8 @@ final class CrudEntitySupervisor(client: CrudClient, val entityId = URLDecoder.decode(self.path.name, "utf-8") val manager = context.watch( context - .actorOf(CrudEntity.props(configuration, entityId, relayRef, concurrencyEnforcer, statsCollector), "entity") + .actorOf(CrudEntity.props(configuration, entityId, relayRef, concurrencyEnforcer, statsCollector, repository), + "entity") ) context.become(forwarding(manager, relayRef)) unstashAll() @@ -160,12 +166,15 @@ object CrudEntity { replyTo: ActorRef ) + private case object DatabaseOperationSuccess + final def props(configuration: Configuration, entityId: String, relay: ActorRef, concurrencyEnforcer: ActorRef, - statsCollector: ActorRef): Props = - Props(new CrudEntity(configuration, entityId, relay, concurrencyEnforcer, statsCollector)) + statsCollector: ActorRef, + repository: JdbcRepository): Props = + Props(new CrudEntity(configuration, entityId, relay, concurrencyEnforcer, statsCollector, repository)) /** * Used to ensure the action ids sent to the concurrency enforcer are indeed unique. @@ -178,14 +187,18 @@ final class CrudEntity(configuration: CrudEntity.Configuration, entityId: String, relay: ActorRef, concurrencyEnforcer: ActorRef, - statsCollector: ActorRef) - extends PersistentActor + statsCollector: ActorRef, + repository: JdbcRepository) + extends Actor with ActorLogging { - override final def persistenceId: String = configuration.userFunctionName + entityId + + private implicit val ec = context.dispatcher + + private val persistenceId: String = configuration.userFunctionName + entityId private val actorId = CrudEntity.actorCounter.incrementAndGet() - private[this] final var recoveredState: Option[pbAny] = None + private[this] final var initState: Option[pbAny] = None private[this] final var stashedCommands = Queue.empty[(EntityCommand, ActorRef)] // PERFORMANCE: look at options for data structures private[this] final var currentCommand: CrudEntity.OutstandingCommand = null private[this] final var stopped = false @@ -201,6 +214,15 @@ final class CrudEntity(configuration: CrudEntity.Configuration, // First thing actor will do is access database reportDatabaseOperationStarted() + override final def preStart(): Unit = + repository + .get(Key(persistenceId, entityId)) + .map { state => + handleInitState(state) + CrudEntity.DatabaseOperationSuccess + } + .pipeTo(self) + override final def postStop(): Unit = { if (currentCommand != null) { log.warning("Stopped but we have a current action id {}", currentCommand.actionId) @@ -267,7 +289,7 @@ final class CrudEntity(configuration: CrudEntity.Configuration, clientAction = Some(ClientAction(ClientAction.Action.Failure(Failure(description = message)))) ) - override final def receiveCommand: PartialFunction[Any, Unit] = { + override final def receive: PartialFunction[Any, Unit] = { case command: EntityCommand if currentCommand != null => stashedCommands = stashedCommands.enqueue((command, sender())) @@ -294,25 +316,18 @@ final class CrudEntity(configuration: CrudEntity.Configuration, } else { reportDatabaseOperationStarted() r.crudAction map { a => - // map the CrudAction to state - val state = a.action match { - case Update(CrudUpdate(Some(value), _)) => Some(value) - case Delete(_) => None - } - - persist(state) { _ => - reportDatabaseOperationFinished() - // try to save a snapshot - r.snapshot.foreach { - case CrudSnapshot(value, _) => saveSnapshot(value) - } - // Make sure that the current request is still ours - if (currentCommand == null || currentCommand.commandId != commandId) { - crash("Internal error - currentRequest changed before all events were persisted") + performCrudAction(a) + .map { _ => + reportDatabaseOperationFinished() + // Make sure that the current request is still ours + if (currentCommand == null || currentCommand.commandId != commandId) { + crash("Internal error - currentRequest changed before all events were persisted") + } + currentCommand.replyTo ! esReplyToUfReply(r) + commandHandled() + CrudEntity.DatabaseOperationSuccess } - currentCommand.replyTo ! esReplyToUfReply(r) - commandHandled() - } + .pipeTo(self) } } @@ -344,11 +359,8 @@ final class CrudEntity(configuration: CrudEntity.Configuration, notifyOutstandingRequests("Unexpected CRUD entity termination") throw error - case SaveSnapshotSuccess(metadata) => - // Nothing to do - - case SaveSnapshotFailure(metadata, cause) => - log.error("Error saving snapshot for CRUD entity", cause) + case CrudEntity.DatabaseOperationSuccess => + // Nothing to do, access the native crud database was successful case ReceiveTimeout => context.parent ! ShardRegion.Passivate(stopMessage = CrudEntity.Stop) @@ -360,40 +372,36 @@ final class CrudEntity(configuration: CrudEntity.Configuration, } } - override final def receiveRecover: PartialFunction[Any, Unit] = { - case offer: SnapshotOffer => - if (!inited) { - // apply snapshot on recoveredState only when the entity is not fully initialized - recoveredState = offer.snapshot match { - case Some(updated: pbAny) => Some(updated) - case other => - throw new IllegalStateException(s"CRUD entity received a unexpected snapshot type : ${other.getClass}") - } + private def performCrudAction(crudAction: CrudAction): Future[Unit] = + crudAction.action match { + case Update(CrudUpdate(Some(value), _)) => + repository.update(Key(persistenceId, entityId), value) + + case Delete(_) => + repository.delete(Key(persistenceId, entityId)) + } + + private[this] final def handleInitState(state: Option[pbAny]): Unit = { + // related to the first access to the database when the actor starts + reportDatabaseOperationFinished() + if (!inited) { + // apply the initial state only when the entity is not fully initialized + initState = state match { + case Some(updated: pbAny) => Some(updated) + case _ => None } - case RecoveryCompleted => - reportDatabaseOperationFinished() - if (!inited) { - relay ! CrudStreamIn( - CrudStreamIn.Message.Init( - CrudInit( - serviceName = configuration.serviceName, - entityId = entityId, - state = Some(CrudInitState(recoveredState, lastSequenceNr)) - ) + relay ! CrudStreamIn( + CrudStreamIn.Message.Init( + CrudInit( + serviceName = configuration.serviceName, + entityId = entityId, + state = Some(CrudInitState(initState)) ) ) - inited = true - } - - case event: Any => - if (!inited) { - // apply event on recoveredState only when the entity is not fully initialized - recoveredState = event match { - case Some(updated: pbAny) => Some(updated) - case _ => None - } - } + ) + inited = true + } } private def reportDatabaseOperationStarted(): Unit = diff --git a/proxy/core/src/main/scala/io/cloudstate/proxy/crud/CrudSupportFactory.scala b/proxy/core/src/main/scala/io/cloudstate/proxy/crud/CrudSupportFactory.scala index 965dc9b96..c43e74074 100644 --- a/proxy/core/src/main/scala/io/cloudstate/proxy/crud/CrudSupportFactory.scala +++ b/proxy/core/src/main/scala/io/cloudstate/proxy/crud/CrudSupportFactory.scala @@ -29,6 +29,7 @@ import com.google.protobuf.Descriptors.ServiceDescriptor import io.cloudstate.protocol.crud.CrudClient import io.cloudstate.protocol.entity.{Entity, Metadata} import io.cloudstate.proxy._ +import io.cloudstate.proxy.crud.store.{JdbcRepositoryImpl, JdbcStoreFactory} import io.cloudstate.proxy.entity.{EntityCommand, UserFunctionReply} import scala.concurrent.{ExecutionContext, Future} @@ -54,12 +55,16 @@ class CrudSupportFactory(system: ActorSystem, config.passivationTimeout, config.relayOutputBufferSize) + val store = new JdbcStoreFactory(config.config).buildCrudStore() + val repository = new JdbcRepositoryImpl(store) + log.debug("Starting CrudEntity for {}", entity.persistenceId) val clusterSharding = ClusterSharding(system) val clusterShardingSettings = ClusterShardingSettings(system) val crudEntity = clusterSharding.start( typeName = entity.persistenceId, - entityProps = CrudEntitySupervisor.props(crudClient, stateManagerConfig, concurrencyEnforcer, statsCollector), + entityProps = + CrudEntitySupervisor.props(crudClient, stateManagerConfig, concurrencyEnforcer, statsCollector, repository), settings = clusterShardingSettings, messageExtractor = new CrudEntityIdExtractor(config.numberOfShards), allocationStrategy = new DynamicLeastShardAllocationStrategy(1, 10, 2, 0.0), diff --git a/proxy/core/src/main/scala/io/cloudstate/proxy/crud/readme.md b/proxy/core/src/main/scala/io/cloudstate/proxy/crud/readme.md new file mode 100644 index 000000000..2d557210f --- /dev/null +++ b/proxy/core/src/main/scala/io/cloudstate/proxy/crud/readme.md @@ -0,0 +1,22 @@ +### What have been done: +- [x] validating the protocol definition +- [x] validating the implementation of the user facing interface +- [x] validating the protocol implementation based on event sourcing +- [x] write tests for the annotation support and the entity based on event sourcing +- [x] provide an sample in a dedicated project for CRUD + +### What should be reviewed: +- [ ] native CRUD support based on Slick +- [ ] In memory CRUD support added +- [ ] the protocol implementation based on native CRUD +- [ ] remove the snapshot from the GRPC protocol +- [ ] remove the sequence number from the GRPC protocol + +### What are the next steps: +- [ ] add Postgres native CRUD support +- [ ] remove the sequence number from the protocol from the implementation +- [ ] add generic type for io.cloudstate.javasupport.crud.CommandContext +- [ ] deal with null value for io.cloudstate.javasupport.crud.CommandContext#updateEntity +- [ ] deal with the order of call for io.cloudstate.javasupport.crud.CommandContext#deleteEntity and io.cloudstate.javasupport.crud.CommandContext#updateEntity +- [ ] write tests +- [ ] extend the TCK \ No newline at end of file diff --git a/proxy/core/src/main/scala/io/cloudstate/proxy/crud/store/JdbcConfig.scala b/proxy/core/src/main/scala/io/cloudstate/proxy/crud/store/JdbcConfig.scala new file mode 100644 index 000000000..08bdee9ac --- /dev/null +++ b/proxy/core/src/main/scala/io/cloudstate/proxy/crud/store/JdbcConfig.scala @@ -0,0 +1,64 @@ +/* + * Copyright 2019 Lightbend Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.cloudstate.proxy.crud.store + +import com.typesafe.config.Config +import slick.basic.DatabaseConfig +import slick.jdbc.JdbcBackend.Database +import slick.jdbc.{JdbcBackend, JdbcProfile} + +class JdbcCrudStateTableColumnNames(config: Config) { + private val cfg = config.getConfig("tables.state.columnNames") + + val persistentId: String = cfg.getString("persistentId") + val entityId: String = cfg.getString("entityId") + val state: String = cfg.getString("state") + + override def toString: String = s"JdbcCrudStateTableColumnNames($persistentId,$entityId,$state)" +} + +class JdbcCrudStateTableConfiguration(config: Config) { + private val cfg = config.getConfig("tables.state") + + val tableName: String = cfg.getString("tableName") + //TODO: handle the empty schemaName!!! + val schemaName: Option[String] = Option(cfg.getString("schemaName")).map(_.trim) + val columnNames: JdbcCrudStateTableColumnNames = new JdbcCrudStateTableColumnNames(config) + + override def toString: String = s"JdbcCrudStateTableConfiguration($tableName,$schemaName,$columnNames)" +} + +object JdbcSlickDatabase { + + def apply(config: Config): JdbcSlickDatabase = { + val database: JdbcBackend.Database = Database.forConfig( + "crud.jdbc.database.slick", + config + ) + val profile: JdbcProfile = DatabaseConfig + .forConfig[JdbcProfile]( + "crud.jdbc.database.slick", + config + ) + .profile + + JdbcSlickDatabase(database, profile) + } + +} + +case class JdbcSlickDatabase(database: JdbcBackend.Database, profile: JdbcProfile) diff --git a/proxy/core/src/main/scala/io/cloudstate/proxy/crud/store/JdbcCrudStateQueries.scala b/proxy/core/src/main/scala/io/cloudstate/proxy/crud/store/JdbcCrudStateQueries.scala new file mode 100644 index 000000000..877f68a51 --- /dev/null +++ b/proxy/core/src/main/scala/io/cloudstate/proxy/crud/store/JdbcCrudStateQueries.scala @@ -0,0 +1,41 @@ +/* + * Copyright 2019 Lightbend Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.cloudstate.proxy.crud.store + +import io.cloudstate.proxy.crud.store.JdbcCrudStateTable.CrudStateRow +import io.cloudstate.proxy.crud.store.JdbcStore.Key +import slick.jdbc.JdbcProfile + +class JdbcCrudStateQueries(val profile: JdbcProfile, override val crudStateTableCfg: JdbcCrudStateTableConfiguration) + extends JdbcCrudStateTable { + + import profile.api._ + + def selectByKey(key: Key): Query[CrudStateTable, CrudStateRow, Seq] = + CrudStateTableQuery + .filter(_.persistentId === key.persistentId) + .filter(_.entityId === key.entityId) + .take(1) + + def insertOrUpdate(crudState: CrudStateRow) = CrudStateTableQuery.insertOrUpdate(crudState) + + def deleteByKey(key: Key) = + CrudStateTableQuery + .filter(_.persistentId === key.persistentId) + .filter(_.entityId === key.entityId) + .delete +} diff --git a/proxy/core/src/main/scala/io/cloudstate/proxy/crud/store/JdbcCrudStateTable.scala b/proxy/core/src/main/scala/io/cloudstate/proxy/crud/store/JdbcCrudStateTable.scala new file mode 100644 index 000000000..68ea8b23e --- /dev/null +++ b/proxy/core/src/main/scala/io/cloudstate/proxy/crud/store/JdbcCrudStateTable.scala @@ -0,0 +1,52 @@ +/* + * Copyright 2019 Lightbend Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.cloudstate.proxy.crud.store + +import io.cloudstate.proxy.crud.store.JdbcCrudStateTable.CrudStateRow +import io.cloudstate.proxy.crud.store.JdbcStore.Key +import slick.lifted.{MappedProjection, ProvenShape} + +object JdbcCrudStateTable { + + case class CrudStateRow(key: Key, state: String) +} + +trait JdbcCrudStateTable { + + val profile: slick.jdbc.JdbcProfile + + import profile.api._ + + def crudStateTableCfg: JdbcCrudStateTableConfiguration + + class CrudStateTable(tableTag: Tag) + extends Table[CrudStateRow](_tableTag = tableTag, + _schemaName = crudStateTableCfg.schemaName, + _tableName = crudStateTableCfg.tableName) { + def * : ProvenShape[CrudStateRow] = (key, state) <> (CrudStateRow.tupled, CrudStateRow.unapply) + + val persistentId: Rep[String] = + column[String](crudStateTableCfg.columnNames.persistentId, O.Length(255, varying = true)) + val entityId: Rep[String] = column[String](crudStateTableCfg.columnNames.entityId, O.Length(255, varying = true)) + //TODO change state from Rep[String] to Rep[Array[Byte]] + val state: Rep[String] = column[String](crudStateTableCfg.columnNames.state, O.Length(255, varying = true)) + val key: MappedProjection[Key, (String, String)] = (persistentId, entityId) <> (Key.tupled, Key.unapply) + val pk = primaryKey(s"${tableName}_pk", (persistentId, entityId)) + } + + lazy val CrudStateTableQuery = new TableQuery(tag => new CrudStateTable(tag)) +} diff --git a/proxy/core/src/main/scala/io/cloudstate/proxy/crud/store/JdbcInMemoryStore.scala b/proxy/core/src/main/scala/io/cloudstate/proxy/crud/store/JdbcInMemoryStore.scala new file mode 100644 index 000000000..c658b7e86 --- /dev/null +++ b/proxy/core/src/main/scala/io/cloudstate/proxy/crud/store/JdbcInMemoryStore.scala @@ -0,0 +1,47 @@ +/* + * Copyright 2019 Lightbend Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.cloudstate.proxy.crud.store + +import akka.util.ByteString +import io.cloudstate.proxy.crud.store.JdbcStore.Key +import org.slf4j.{Logger, LoggerFactory} + +import scala.concurrent.Future + +final class JdbcInMemoryStore extends JdbcStore[Key, ByteString] { + + private final val logger: Logger = LoggerFactory.getLogger(classOf[JdbcInMemoryStore]) + + private final var store = Map.empty[Key, ByteString] + + override def get(key: Key): Future[Option[ByteString]] = { + logger.info(s"get called with key - $key") + Future.successful(store.get(key)) + } + + override def update(key: Key, value: ByteString): Future[Unit] = { + logger.info(s"update called with key - $key and value - $value") + store += key -> value + Future.unit + } + + override def delete(key: Key): Future[Unit] = { + logger.info(s"delete called with key - $key") + store -= key + Future.unit + } +} diff --git a/proxy/core/src/main/scala/io/cloudstate/proxy/crud/store/JdbcRepository.scala b/proxy/core/src/main/scala/io/cloudstate/proxy/crud/store/JdbcRepository.scala new file mode 100644 index 000000000..6449481c2 --- /dev/null +++ b/proxy/core/src/main/scala/io/cloudstate/proxy/crud/store/JdbcRepository.scala @@ -0,0 +1,69 @@ +/* + * Copyright 2019 Lightbend Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.cloudstate.proxy.crud.store + +import akka.util.ByteString +import com.google.protobuf.any.{Any => ScalaPbAny} +import io.cloudstate.proxy.crud.store.JdbcStore.Key + +import scala.concurrent.{ExecutionContext, Future} + +trait JdbcRepository { + + val store: JdbcStore[Key, ByteString] + + /** + * Retrieve the payload for the given key. + * + * @param key to retrieve payload for + * @return Some(payload) if payload exists for the key and None otherwise + */ + def get(key: Key): Future[Option[ScalaPbAny]] + + /** + * Insert the entity payload with the given key if it not already exists. + * Update the entity payload at the given key if it already exists. + * + * @param key to insert or update the entity + * @param payload that should be persisted + */ + def update(key: Key, payload: ScalaPbAny): Future[Unit] + + /** + * Delete the entity with the given key. + * + * @param key to delete data. + */ + def delete(key: Key): Future[Unit] + +} + +class JdbcRepositoryImpl(val store: JdbcStore[Key, ByteString])(implicit ec: ExecutionContext) extends JdbcRepository { + + def get(key: Key): Future[Option[ScalaPbAny]] = + store + .get(key) + .map { + case Some(value) => Some(ScalaPbAny.parseFrom(value.toByteBuffer.array())) + case None => None + } + + def update(key: Key, entity: ScalaPbAny): Future[Unit] = + store.update(key, ByteString(entity.toByteArray)) + + def delete(key: Key): Future[Unit] = store.delete(key) +} diff --git a/proxy/core/src/main/scala/io/cloudstate/proxy/crud/store/JdbcStore.scala b/proxy/core/src/main/scala/io/cloudstate/proxy/crud/store/JdbcStore.scala new file mode 100644 index 000000000..72799b85e --- /dev/null +++ b/proxy/core/src/main/scala/io/cloudstate/proxy/crud/store/JdbcStore.scala @@ -0,0 +1,82 @@ +/* + * Copyright 2019 Lightbend Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.cloudstate.proxy.crud.store + +import akka.util.ByteString +import io.cloudstate.proxy.crud.store.JdbcCrudStateTable.CrudStateRow +import io.cloudstate.proxy.crud.store.JdbcStore.Key + +import scala.concurrent.{ExecutionContext, Future} + +object JdbcStore { + + case class Key(persistentId: String, entityId: String) + +} + +/** + * Represents an low level interface for accessing a native CRUD database. + * + * @tparam K the type for CRUD database key + * @tparam V the type for CRUD database value + */ +trait JdbcStore[K, V] { + + /** + * Retrieve the data for the given key. + * + * @param key to retrieve data for + * @return Some(data) if data exists for the key and None otherwise + */ + def get(key: K): Future[Option[V]] + + /** + * Insert the data with the given key if it not already exists. + * Update the data at the given key if it already exists. + * + * @param key to insert or update the entity + * @param value that should be persisted + */ + def update(key: K, value: V): Future[Unit] + + /** + * Delete the data for the given key. + * + * @param key to delete data. + */ + def delete(key: K): Future[Unit] + +} + +final class JdbcStoreImpl(slickDatabase: JdbcSlickDatabase, queries: JdbcCrudStateQueries)( + implicit ec: ExecutionContext +) extends JdbcStore[Key, ByteString] { + + import slickDatabase.profile.api._ + + private val db = slickDatabase.database + + override def get(key: Key): Future[Option[ByteString]] = + db.run(queries.selectByKey(key).result.headOption.map(mayBeState => mayBeState.map(s => ByteString(s.state)))) + + override def update(key: Key, value: ByteString): Future[Unit] = + db.run(queries.insertOrUpdate(CrudStateRow(key, value.utf8String))).map(_ => ()) + + override def delete(key: Key): Future[Unit] = + db.run(queries.deleteByKey(key)).map(_ => ()) + +} diff --git a/proxy/core/src/main/scala/io/cloudstate/proxy/crud/store/JdbcStoreFactory.scala b/proxy/core/src/main/scala/io/cloudstate/proxy/crud/store/JdbcStoreFactory.scala new file mode 100644 index 000000000..de49d128c --- /dev/null +++ b/proxy/core/src/main/scala/io/cloudstate/proxy/crud/store/JdbcStoreFactory.scala @@ -0,0 +1,50 @@ +/* + * Copyright 2019 Lightbend Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.cloudstate.proxy.crud.store + +import akka.util.ByteString +import com.typesafe.config.Config +import io.cloudstate.proxy.crud.store.JdbcStore.Key +import io.cloudstate.proxy.crud.store.JdbcStoreFactory.{IN_MEMORY, JDBC} + +import scala.concurrent.ExecutionContext; + +object JdbcStoreFactory { + final val IN_MEMORY = "in-memory" + final val JDBC = "jdbc" +} + +class JdbcStoreFactory(config: Config)(implicit ec: ExecutionContext) { + + def buildCrudStore(): JdbcStore[Key, ByteString] = + config.getString("crud.store-type") match { + case IN_MEMORY => new JdbcInMemoryStore + case JDBC => buildJdbcCrudStore() + case other => + throw new IllegalArgumentException(s"CRUD store-type must be one of: ${IN_MEMORY} or ${JDBC} but is '$other'") + } + + private def buildJdbcCrudStore(): JdbcStore[Key, ByteString] = { + val slickDatabase = JdbcSlickDatabase(config) + val tableConfiguration = new JdbcCrudStateTableConfiguration( + config.getConfig("crud.jdbc-state-store") + ) + val queries = new JdbcCrudStateQueries(slickDatabase.profile, tableConfiguration) + new JdbcStoreImpl(slickDatabase, queries) + } + +} diff --git a/proxy/core/src/main/scala/io/cloudstate/proxy/crud/store/package-info.java b/proxy/core/src/main/scala/io/cloudstate/proxy/crud/store/package-info.java new file mode 100644 index 000000000..ca47638c2 --- /dev/null +++ b/proxy/core/src/main/scala/io/cloudstate/proxy/crud/store/package-info.java @@ -0,0 +1,4 @@ +/** + * Most part of the code in this package has been copied/adapted from https://github.com/akka/akka-persistence-jdbc + */ +package io.cloudstate.proxy.crud.store; \ No newline at end of file diff --git a/proxy/core/src/test/scala/io/cloudstate/proxy/crud/AbstractCrudEntitySpec.scala b/proxy/core/src/test/scala/io/cloudstate/proxy/crud/AbstractCrudEntitySpec.scala index 8cb9a73b6..2f5381ab2 100644 --- a/proxy/core/src/test/scala/io/cloudstate/proxy/crud/AbstractCrudEntitySpec.scala +++ b/proxy/core/src/test/scala/io/cloudstate/proxy/crud/AbstractCrudEntitySpec.scala @@ -149,7 +149,8 @@ abstract class AbstractCrudEntitySpec entityId, userFunction.ref, concurrencyEnforcer, - statsCollector.ref + statsCollector.ref, + null ), s"crud-test-entity-$entityId" ) @@ -173,7 +174,8 @@ abstract class AbstractCrudEntitySpec entityId, userFunction.ref, concurrencyEnforcer, - statsCollector.ref + statsCollector.ref, + null ), s"crud-test-entity-reactivated-$entityId" ) diff --git a/proxy/core/src/test/scala/io/cloudstate/proxy/crud/CrudEntitySpec.scala b/proxy/core/src/test/scala/io/cloudstate/proxy/crud/CrudEntitySpec.scala index b97fbb5fe..4841ec235 100644 --- a/proxy/core/src/test/scala/io/cloudstate/proxy/crud/CrudEntitySpec.scala +++ b/proxy/core/src/test/scala/io/cloudstate/proxy/crud/CrudEntitySpec.scala @@ -51,7 +51,7 @@ class CrudEntitySpec extends AbstractCrudEntitySpec { expectTerminated(entity) // reactivating the entity - reactiveAndExpectInitState(Some(CrudInitState(Some(state2), 2))) + reactiveAndExpectInitState(Some(CrudInitState(Some(state2)))) val commandId3 = sendAndExpectCommand("cmd", command, reactivatedEntity) sendAndExpectReply(commandId3, Some(Update(CrudUpdate(Some(state2)))), reactivatedEntity) @@ -75,7 +75,7 @@ class CrudEntitySpec extends AbstractCrudEntitySpec { expectTerminated(entity) // reactivating the entity - reactiveAndExpectInitState(Some(CrudInitState(None, 2))) + reactiveAndExpectInitState(Some(CrudInitState(None))) userFunction.expectNoMessage(200.millis) expectNoMessage(200.millis) diff --git a/samples/java-crud-shopping-cart/src/main/java/io/cloudstate/samples/shoppingcart/ShoppingCartEntity.java b/samples/java-crud-shopping-cart/src/main/java/io/cloudstate/samples/shoppingcart/ShoppingCartEntity.java index e401e2d5b..cf54fb2fb 100644 --- a/samples/java-crud-shopping-cart/src/main/java/io/cloudstate/samples/shoppingcart/ShoppingCartEntity.java +++ b/samples/java-crud-shopping-cart/src/main/java/io/cloudstate/samples/shoppingcart/ShoppingCartEntity.java @@ -61,7 +61,7 @@ public Shoppingcart.Cart getCart() { } @CommandHandler - public Empty addItem(Shoppingcart.AddLineItem item, CommandContext ctx) { + public Empty addItem(Shoppingcart.AddLineItem item, CommandContext ctx) { if (item.getQuantity() <= 0) { ctx.fail("Cannot add negative quantity of to item " + item.getProductId()); } @@ -84,7 +84,7 @@ public Empty addItem(Shoppingcart.AddLineItem item, CommandContext } @CommandHandler - public Empty removeItem(Shoppingcart.RemoveLineItem item, CommandContext ctx) { + public Empty removeItem(Shoppingcart.RemoveLineItem item, CommandContext ctx) { if (!cart.containsKey(item.getProductId())) { ctx.fail("Cannot remove item " + item.getProductId() + " because it is not in the cart."); } @@ -100,8 +100,7 @@ public Empty removeItem(Shoppingcart.RemoveLineItem item, CommandContext ctx) { + public Empty removeCart(Shoppingcart.RemoveShoppingCart cartItem, CommandContext ctx) { if (!entityId.equals(cartItem.getUserId())) { ctx.fail("Cannot remove unknown cart " + cartItem.getUserId()); } From 919756d653d3985ebaea67792645eb72a449ada0 Mon Sep 17 00:00:00 2001 From: Guy Youansi Date: Mon, 7 Sep 2020 15:36:06 +0200 Subject: [PATCH 30/93] modified markdown --- .../scala/io/cloudstate/proxy/crud/readme.md | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/proxy/core/src/main/scala/io/cloudstate/proxy/crud/readme.md b/proxy/core/src/main/scala/io/cloudstate/proxy/crud/readme.md index 2d557210f..74684c901 100644 --- a/proxy/core/src/main/scala/io/cloudstate/proxy/crud/readme.md +++ b/proxy/core/src/main/scala/io/cloudstate/proxy/crud/readme.md @@ -1,22 +1,22 @@ ### What have been done: -- [x] validating the protocol definition -- [x] validating the implementation of the user facing interface -- [x] validating the protocol implementation based on event sourcing -- [x] write tests for the annotation support and the entity based on event sourcing -- [x] provide an sample in a dedicated project for CRUD +- validating the protocol definition :white_check_mark: +- validating the implementation of the user facing interface :white_check_mark: +- validating the protocol implementation based on event sourcing :white_check_mark: +- write tests for the annotation support and the entity based on event sourcing :white_check_mark: +- provide an sample in a dedicated project for CRUD :white_check_mark: ### What should be reviewed: -- [ ] native CRUD support based on Slick -- [ ] In memory CRUD support added -- [ ] the protocol implementation based on native CRUD -- [ ] remove the snapshot from the GRPC protocol -- [ ] remove the sequence number from the GRPC protocol +- native CRUD support based on Slick +- In memory CRUD support added +- the protocol implementation based on native CRUD +- remove the snapshot from the GRPC protocol +- remove the sequence number from the GRPC protocol ### What are the next steps: -- [ ] add Postgres native CRUD support -- [ ] remove the sequence number from the protocol from the implementation -- [ ] add generic type for io.cloudstate.javasupport.crud.CommandContext -- [ ] deal with null value for io.cloudstate.javasupport.crud.CommandContext#updateEntity -- [ ] deal with the order of call for io.cloudstate.javasupport.crud.CommandContext#deleteEntity and io.cloudstate.javasupport.crud.CommandContext#updateEntity -- [ ] write tests -- [ ] extend the TCK \ No newline at end of file +- add Postgres native CRUD support +- remove the sequence number from the protocol from the implementation +- add generic type for io.cloudstate.javasupport.crud.CommandContext +- deal with null value for io.cloudstate.javasupport.crud.CommandContext#updateEntity +- deal with the order of call for io.cloudstate.javasupport.crud.CommandContext#deleteEntity and io.cloudstate.javasupport.crud.CommandContext#updateEntity +- write tests +- extend the TCK \ No newline at end of file From dd2e4c861bdf2c1cd21f20b8d683673c166352f5 Mon Sep 17 00:00:00 2001 From: Guy Youansi Date: Mon, 7 Sep 2020 19:45:31 +0200 Subject: [PATCH 31/93] add generic to command context and entity handler --- .../javasupport/crud/CommandContext.java | 7 +++---- .../javasupport/crud/CrudEntityHandler.java | 3 +-- .../impl/crud/AnnotationBasedCrudSupport.scala | 8 ++++---- .../javasupport/impl/crud/CrudImpl.scala | 2 +- .../impl/crud/AnnotationBasedCrudSupportSpec.scala | 14 +++++++------- .../samples/shoppingcart/ShoppingCartEntity.java | 7 ++++--- 6 files changed, 20 insertions(+), 21 deletions(-) diff --git a/java-support/src/main/java/io/cloudstate/javasupport/crud/CommandContext.java b/java-support/src/main/java/io/cloudstate/javasupport/crud/CommandContext.java index 6c47bb205..a3739b8cf 100644 --- a/java-support/src/main/java/io/cloudstate/javasupport/crud/CommandContext.java +++ b/java-support/src/main/java/io/cloudstate/javasupport/crud/CommandContext.java @@ -16,6 +16,7 @@ package io.cloudstate.javasupport.crud; +import com.google.protobuf.Any; import io.cloudstate.javasupport.ClientActionContext; import io.cloudstate.javasupport.EffectContext; @@ -26,9 +27,7 @@ * or deleting the entity state in response to a command, along with forwarding the result to other * entities, and performing side effects on other entities. */ -public interface CommandContext extends CrudContext, ClientActionContext, EffectContext { - - // TODO: add generic type for the state in updateEntity +public interface CommandContext extends CrudContext, ClientActionContext, EffectContext { /** * The current sequence number of state in this entity. @@ -56,7 +55,7 @@ public interface CommandContext extends CrudContext, ClientActionContext, Effect * * @param state The state to persist. */ - void updateEntity(Object state); + void updateEntity(T state); /** Delete the entity. */ void deleteEntity(); diff --git a/java-support/src/main/java/io/cloudstate/javasupport/crud/CrudEntityHandler.java b/java-support/src/main/java/io/cloudstate/javasupport/crud/CrudEntityHandler.java index 2a2bef9c9..7bfca6cab 100644 --- a/java-support/src/main/java/io/cloudstate/javasupport/crud/CrudEntityHandler.java +++ b/java-support/src/main/java/io/cloudstate/javasupport/crud/CrudEntityHandler.java @@ -36,8 +36,7 @@ public interface CrudEntityHandler { * @param context The command context. * @return The reply to the command, if the command isn't being forwarded elsewhere. */ - Optional handleCommand(Any command, CommandContext context); - // Optional handleCommand(Any command, CommandContext context); + Optional handleCommand(Any command, CommandContext context); /** * Handle the given state. diff --git a/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/AnnotationBasedCrudSupport.scala b/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/AnnotationBasedCrudSupport.scala index ee3498d52..b34b0c033 100644 --- a/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/AnnotationBasedCrudSupport.scala +++ b/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/AnnotationBasedCrudSupport.scala @@ -73,7 +73,7 @@ private[impl] class AnnotationBasedCrudSupport( }) } - override def handleCommand(command: JavaPbAny, context: CommandContext): Optional[JavaPbAny] = unwrap { + override def handleCommand[T](command: JavaPbAny, context: CommandContext[T]): Optional[JavaPbAny] = unwrap { behavior.commandHandlers.get(context.commandName()).map { handler => handler.invoke(entity, command, context) } getOrElse { @@ -125,7 +125,7 @@ private[impl] class AnnotationBasedCrudSupport( } private class CrudBehaviorReflection( - val commandHandlers: Map[String, ReflectionHelper.CommandHandlerInvoker[CommandContext]], + val commandHandlers: Map[String, ReflectionHelper.CommandHandlerInvoker[CommandContext[_]]], val updateHandlers: Map[Class[_], UpdateInvoker], val deleteHandler: Option[DeleteInvoker] ) { @@ -166,8 +166,8 @@ private object CrudBehaviorReflection { ) }) - new ReflectionHelper.CommandHandlerInvoker[CommandContext](ReflectionHelper.ensureAccessible(method), - serviceMethod) + new ReflectionHelper.CommandHandlerInvoker[CommandContext[_]](ReflectionHelper.ensureAccessible(method), + serviceMethod) } .groupBy(_.serviceMethod.name) .map { diff --git a/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/CrudImpl.scala b/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/CrudImpl.scala index ae83f4249..79fee307d 100644 --- a/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/CrudImpl.scala +++ b/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/CrudImpl.scala @@ -216,7 +216,7 @@ final class CrudImpl(_system: ActorSystem, override val commandId: Long, val anySupport: AnySupport, val handler: CrudEntityHandler) - extends CommandContext + extends CommandContext[AnyRef] with AbstractContext with AbstractClientActionContext with AbstractEffectContext diff --git a/java-support/src/test/scala/io/cloudstate/javasupport/impl/crud/AnnotationBasedCrudSupportSpec.scala b/java-support/src/test/scala/io/cloudstate/javasupport/impl/crud/AnnotationBasedCrudSupportSpec.scala index 0df974c1d..26bc0c9e8 100644 --- a/java-support/src/test/scala/io/cloudstate/javasupport/impl/crud/AnnotationBasedCrudSupportSpec.scala +++ b/java-support/src/test/scala/io/cloudstate/javasupport/impl/crud/AnnotationBasedCrudSupportSpec.scala @@ -45,12 +45,12 @@ class AnnotationBasedCrudSupportSpec extends WordSpec with Matchers { override def entityId(): String = "foo" } - class MockCommandContext extends CommandContext with BaseContext { + class MockCommandContext extends CommandContext[JavaPbAny] with BaseContext { var action: Option[AnyRef] = None override def sequenceNumber(): Long = 10 override def commandName(): String = "AddItem" override def commandId(): Long = 20 - override def updateEntity(state: AnyRef): Unit = action = Some(state) + override def updateEntity(state: JavaPbAny): Unit = action = Some(state) override def deleteEntity(): Unit = action = None override def entityId(): String = "foo" override def fail(errorMessage: String): RuntimeException = ??? @@ -146,7 +146,7 @@ class AnnotationBasedCrudSupportSpec extends WordSpec with Matchers { val handler = create( new { @CommandHandler - def addItem(msg: String, @EntityId eid: String, ctx: CommandContext): Wrapped = { + def addItem(msg: String, @EntityId eid: String, ctx: CommandContext[JavaPbAny]): Wrapped = { eid should ===("foo") ctx.commandName() should ===("AddItem") Wrapped(msg) @@ -161,7 +161,7 @@ class AnnotationBasedCrudSupportSpec extends WordSpec with Matchers { val handler = create( new { @CommandHandler - def addItem(msg: String, ctx: CommandContext): Wrapped = { + def addItem(msg: String, ctx: CommandContext[JavaPbAny]): Wrapped = { ctx.updateEntity(state(msg + " state")) ctx.commandName() should ===("AddItem") Wrapped(msg) @@ -185,7 +185,7 @@ class AnnotationBasedCrudSupportSpec extends WordSpec with Matchers { "fail if there's two command handlers for the same command" in { a[RuntimeException] should be thrownBy create(new { @CommandHandler - def addItem(msg: String, ctx: CommandContext) = + def addItem(msg: String, ctx: CommandContext[JavaPbAny]) = Wrapped(msg) @CommandHandler def addItem(msg: String) = @@ -259,7 +259,7 @@ class AnnotationBasedCrudSupportSpec extends WordSpec with Matchers { "fail if there's a bad context" in { a[RuntimeException] should be thrownBy create(new { @UpdateStateHandler - def updateState(state: String, context: CommandContext): Unit = () + def updateState(state: String, context: CommandContext[JavaPbAny]): Unit = () }) } @@ -336,7 +336,7 @@ class AnnotationBasedCrudSupportSpec extends WordSpec with Matchers { "fail if there's a bad context" in { a[RuntimeException] should be thrownBy create(new { @DeleteStateHandler - def deleteState(context: CommandContext): Unit = () + def deleteState(context: CommandContext[JavaPbAny]): Unit = () }) } } diff --git a/samples/java-crud-shopping-cart/src/main/java/io/cloudstate/samples/shoppingcart/ShoppingCartEntity.java b/samples/java-crud-shopping-cart/src/main/java/io/cloudstate/samples/shoppingcart/ShoppingCartEntity.java index cf54fb2fb..e401e2d5b 100644 --- a/samples/java-crud-shopping-cart/src/main/java/io/cloudstate/samples/shoppingcart/ShoppingCartEntity.java +++ b/samples/java-crud-shopping-cart/src/main/java/io/cloudstate/samples/shoppingcart/ShoppingCartEntity.java @@ -61,7 +61,7 @@ public Shoppingcart.Cart getCart() { } @CommandHandler - public Empty addItem(Shoppingcart.AddLineItem item, CommandContext ctx) { + public Empty addItem(Shoppingcart.AddLineItem item, CommandContext ctx) { if (item.getQuantity() <= 0) { ctx.fail("Cannot add negative quantity of to item " + item.getProductId()); } @@ -84,7 +84,7 @@ public Empty addItem(Shoppingcart.AddLineItem item, CommandContext ctx) { } @CommandHandler - public Empty removeItem(Shoppingcart.RemoveLineItem item, CommandContext ctx) { + public Empty removeItem(Shoppingcart.RemoveLineItem item, CommandContext ctx) { if (!cart.containsKey(item.getProductId())) { ctx.fail("Cannot remove item " + item.getProductId() + " because it is not in the cart."); } @@ -100,7 +100,8 @@ public Empty removeItem(Shoppingcart.RemoveLineItem item, CommandContext ctx) { } @CommandHandler - public Empty removeCart(Shoppingcart.RemoveShoppingCart cartItem, CommandContext ctx) { + public Empty removeCart( + Shoppingcart.RemoveShoppingCart cartItem, CommandContext ctx) { if (!entityId.equals(cartItem.getUserId())) { ctx.fail("Cannot remove unknown cart " + cartItem.getUserId()); } From bbd5e532688031130af539191aa55daae9bdb645 Mon Sep 17 00:00:00 2001 From: Guy Youansi Date: Mon, 7 Sep 2020 20:42:17 +0200 Subject: [PATCH 32/93] remove the use of sequence number in the GRPC CRUD protocol --- .../javasupport/crud/CommandContext.java | 8 - .../javasupport/crud/StateContext.java | 9 +- .../crud/AnnotationBasedCrudSupport.scala | 4 +- .../javasupport/impl/crud/CrudImpl.scala | 163 ++++++++---------- .../crud/AnnotationBasedCrudSupportSpec.scala | 5 - 5 files changed, 71 insertions(+), 118 deletions(-) diff --git a/java-support/src/main/java/io/cloudstate/javasupport/crud/CommandContext.java b/java-support/src/main/java/io/cloudstate/javasupport/crud/CommandContext.java index a3739b8cf..02e13bb26 100644 --- a/java-support/src/main/java/io/cloudstate/javasupport/crud/CommandContext.java +++ b/java-support/src/main/java/io/cloudstate/javasupport/crud/CommandContext.java @@ -16,7 +16,6 @@ package io.cloudstate.javasupport.crud; -import com.google.protobuf.Any; import io.cloudstate.javasupport.ClientActionContext; import io.cloudstate.javasupport.EffectContext; @@ -29,13 +28,6 @@ */ public interface CommandContext extends CrudContext, ClientActionContext, EffectContext { - /** - * The current sequence number of state in this entity. - * - * @return The current sequence number. - */ - long sequenceNumber(); - /** * The name of the command being executed. * diff --git a/java-support/src/main/java/io/cloudstate/javasupport/crud/StateContext.java b/java-support/src/main/java/io/cloudstate/javasupport/crud/StateContext.java index 6b3e2f4a7..ff22690b8 100644 --- a/java-support/src/main/java/io/cloudstate/javasupport/crud/StateContext.java +++ b/java-support/src/main/java/io/cloudstate/javasupport/crud/StateContext.java @@ -17,11 +17,4 @@ package io.cloudstate.javasupport.crud; /** A state context. */ -public interface StateContext extends CrudContext { - /** - * The sequence number of this state. - * - * @return The sequence number. - */ - long sequenceNumber(); -} +public interface StateContext extends CrudContext {} diff --git a/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/AnnotationBasedCrudSupport.scala b/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/AnnotationBasedCrudSupport.scala index b34b0c033..14b2e134c 100644 --- a/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/AnnotationBasedCrudSupport.scala +++ b/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/AnnotationBasedCrudSupport.scala @@ -88,9 +88,7 @@ private[impl] class AnnotationBasedCrudSupport( behavior.getCachedUpdateHandlerForClass(state.getClass) match { case Some(handler) => - val ctx = new DelegatingCrudContext(context) with StateContext { - override def sequenceNumber(): Long = context.sequenceNumber() - } + val ctx = new DelegatingCrudContext(context) with StateContext handler.invoke(entity, state, ctx) case None => throw new RuntimeException( diff --git a/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/CrudImpl.scala b/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/CrudImpl.scala index 79fee307d..20a5e4114 100644 --- a/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/CrudImpl.scala +++ b/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/CrudImpl.scala @@ -105,104 +105,87 @@ final class CrudImpl(_system: ActorSystem, services.getOrElse(init.serviceName, throw new RuntimeException(s"Service not found: ${init.serviceName}")) val handler = service.factory.create(new CrudContextImpl(init.entityId)) val thisEntityId = init.entityId - val sequenceNumber = 0L //TODO: should be removed every where, CRUD do not need sequence!!! - val startingSequenceNumber = init.state match { + init.state match { case Some(CrudInitState(Some(payload), _)) => val encoded = service.anySupport.encodeScala(payload) - handler.handleUpdate(ScalaPbAny.toJavaProto(encoded), new StateContextImpl(thisEntityId, sequenceNumber)) - sequenceNumber + handler.handleUpdate(ScalaPbAny.toJavaProto(encoded), new StateContextImpl(thisEntityId)) case Some(CrudInitState(None, _)) => - handler.handleDelete(new StateContextImpl(thisEntityId, sequenceNumber)) - sequenceNumber + handler.handleDelete(new StateContextImpl(thisEntityId)) - case _ => 0L // should not happen! + case _ => + // should not happen! } Flow[CrudStreamIn] - .map(_.message) - .scan[(Long, Option[CrudStreamOut.Message])]((startingSequenceNumber, None)) { - case ((sequence, _), InCommand(command)) if thisEntityId != command.entityId => - (sequence, - Some( - CrudStreamOut.Message.Failure( - Failure( - command.id, - s"""Cloudstate protocol failure for CRUD entity: - |Receiving entity - $thisEntityId is not the intended recipient - |of command with id - ${command.id} and name - ${command.name}""".stripMargin.replaceAll("\n", - " ") - ) - ) - )) - - case ((sequence, _), InCommand(command)) if command.payload.isEmpty => - (sequence, - Some( - CrudStreamOut.Message.Failure( - Failure( - command.id, - s"Cloudstate protocol failure for CRUD entity: Command (id: ${command.id}, name: ${command.name}) should have a payload" - ) - ) - )) - - case ((sequence, _), InCommand(command)) => - val cmd = ScalaPbAny.toJavaProto(command.payload.get) - val context = new CommandContextImpl( - thisEntityId, - sequence, - command.name, - command.id, - service.anySupport, - handler - ) - val reply = try { - handler.handleCommand(cmd, context) - } catch { - case FailInvoked => Option.empty[JavaPbAny].asJava - } finally { - context.deactivate() // Very important! - } + .map { m => + m.message match { + case InCommand(command) if thisEntityId != command.entityId => + CrudStreamOut.Message.Failure( + Failure( + command.id, + s"""Cloudstate protocol failure for CRUD entity: + |Receiving entity - $thisEntityId is not the intended recipient + |of command with id - ${command.id} and name - ${command.name}""".stripMargin.replaceAll("\n", " ") + ) + ) - val clientAction = context.createClientAction(reply, false) - if (!context.hasError) { - context.applyCrudAction() - val endSequenceNumber = context.nextSequenceNumber + case InCommand(command) if command.payload.isEmpty => + CrudStreamOut.Message.Failure( + Failure( + command.id, + s"Cloudstate protocol failure for CRUD entity: Command (id: ${command.id}, name: ${command.name}) should have a payload" + ) + ) - (endSequenceNumber, - Some( - CrudStreamOut.Message.Reply( - CrudReply( - command.id, - clientAction, - context.sideEffects, - context.crudAction() - ) - ) - )) - } else { - (sequence, - Some( - CrudStreamOut.Message.Reply( - CrudReply( - commandId = command.id, - clientAction = clientAction, - crudAction = context.crudAction() - ) - ) - )) - } + case InCommand(command) => + val cmd = ScalaPbAny.toJavaProto(command.payload.get) + val context = new CommandContextImpl( + thisEntityId, + command.name, + command.id, + service.anySupport, + handler + ) + val reply = try { + handler.handleCommand(cmd, context) + } catch { + case FailInvoked => Option.empty[JavaPbAny].asJava + } finally { + context.deactivate() + } + + val clientAction = context.createClientAction(reply, false) + if (!context.hasError) { + context.applyCrudAction() + CrudStreamOut.Message.Reply( + CrudReply( + command.id, + clientAction, + context.sideEffects, + context.crudAction() + ) + ) + } else { + CrudStreamOut.Message.Reply( + CrudReply( + commandId = command.id, + clientAction = clientAction, + crudAction = context.crudAction() + ) + ) + } - case (_, InInit(_)) => - throw new IllegalStateException("CRUD Entity already inited") + case InInit(_) => + throw new IllegalStateException("CRUD Entity already inited") - case (_, InEmpty) => - throw new IllegalStateException("CRUD Entity received empty/unknown message") + case InEmpty => + throw new IllegalStateException("CRUD Entity received empty/unknown message") + } } .collect { - case (_, Some(message)) => CrudStreamOut(message) + case message: CrudStreamOut.Message => CrudStreamOut(message) } } @@ -211,7 +194,6 @@ final class CrudImpl(_system: ActorSystem, } private final class CommandContextImpl(override val entityId: String, - override val sequenceNumber: Long, override val commandName: String, override val commandId: Long, val anySupport: AnySupport, @@ -222,7 +204,6 @@ final class CrudImpl(_system: ActorSystem, with AbstractEffectContext with ActivatableContext { - private var _nextSequenceNumber: Long = sequenceNumber private var mayBeAction: Option[CrudAction] = None override def updateEntity(state: AnyRef): Unit = { @@ -237,8 +218,6 @@ final class CrudImpl(_system: ActorSystem, mayBeAction = Some(CrudAction(Delete(CrudDelete()))) } - def nextSequenceNumber: Long = _nextSequenceNumber - def crudAction(): Option[CrudAction] = mayBeAction def applyCrudAction(): Unit = @@ -246,12 +225,10 @@ final class CrudImpl(_system: ActorSystem, case Some(CrudAction(action, _)) => action match { case Update(CrudUpdate(Some(value), _)) => - _nextSequenceNumber += 1 - handler.handleUpdate(ScalaPbAny.toJavaProto(value), new StateContextImpl(entityId, _nextSequenceNumber)) + handler.handleUpdate(ScalaPbAny.toJavaProto(value), new StateContextImpl(entityId)) case Delete(CrudDelete(_)) => - _nextSequenceNumber += 1 - handler.handleDelete(new StateContextImpl(entityId, _nextSequenceNumber)) + handler.handleDelete(new StateContextImpl(entityId)) } case None => // ignored, nothing to do it is a get request! @@ -260,7 +237,5 @@ final class CrudImpl(_system: ActorSystem, private final class CrudContextImpl(override final val entityId: String) extends CrudContext with AbstractContext - private final class StateContextImpl(override final val entityId: String, override val sequenceNumber: Long) - extends StateContext - with AbstractContext + private final class StateContextImpl(override final val entityId: String) extends StateContext with AbstractContext } diff --git a/java-support/src/test/scala/io/cloudstate/javasupport/impl/crud/AnnotationBasedCrudSupportSpec.scala b/java-support/src/test/scala/io/cloudstate/javasupport/impl/crud/AnnotationBasedCrudSupportSpec.scala index 26bc0c9e8..f707ed56e 100644 --- a/java-support/src/test/scala/io/cloudstate/javasupport/impl/crud/AnnotationBasedCrudSupportSpec.scala +++ b/java-support/src/test/scala/io/cloudstate/javasupport/impl/crud/AnnotationBasedCrudSupportSpec.scala @@ -47,7 +47,6 @@ class AnnotationBasedCrudSupportSpec extends WordSpec with Matchers { class MockCommandContext extends CommandContext[JavaPbAny] with BaseContext { var action: Option[AnyRef] = None - override def sequenceNumber(): Long = 10 override def commandName(): String = "AddItem" override def commandId(): Long = 20 override def updateEntity(state: JavaPbAny): Unit = action = Some(state) @@ -224,8 +223,6 @@ class AnnotationBasedCrudSupportSpec extends WordSpec with Matchers { "support update state handlers" when { val ctx = new StateContext with BaseContext { - override def sequenceNumber(): Long = 10 - override def entityId(): String = "foo" } @@ -248,7 +245,6 @@ class AnnotationBasedCrudSupportSpec extends WordSpec with Matchers { @UpdateStateHandler def updateState(state: String, context: StateContext): Unit = { state should ===("state!") - context.sequenceNumber() should ===(10) invoked = true } }) @@ -290,7 +286,6 @@ class AnnotationBasedCrudSupportSpec extends WordSpec with Matchers { "support delete state handlers" when { val ctx = new StateContext with BaseContext { - override def sequenceNumber(): Long = 10 override def entityId(): String = "foo" } From 99f8ffcd961d36a36f0386a87842c74c3209fef1 Mon Sep 17 00:00:00 2001 From: Guy Youansi Date: Mon, 7 Sep 2020 20:59:02 +0200 Subject: [PATCH 33/93] remove event sourced in memory snapshot store. adapt log for in memory native CRUD store --- .../proxy/crud/InMemSnapshotStore.scala | 55 ------------------- .../scala/io/cloudstate/proxy/crud/readme.md | 4 +- .../proxy/crud/store/JdbcInMemoryStore.scala | 2 +- 3 files changed, 3 insertions(+), 58 deletions(-) delete mode 100644 proxy/core/src/main/scala/io/cloudstate/proxy/crud/InMemSnapshotStore.scala diff --git a/proxy/core/src/main/scala/io/cloudstate/proxy/crud/InMemSnapshotStore.scala b/proxy/core/src/main/scala/io/cloudstate/proxy/crud/InMemSnapshotStore.scala deleted file mode 100644 index f544c5781..000000000 --- a/proxy/core/src/main/scala/io/cloudstate/proxy/crud/InMemSnapshotStore.scala +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright 2019 Lightbend Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.cloudstate.proxy.crud - -import akka.persistence.snapshot.SnapshotStore -import akka.persistence.{SelectedSnapshot, SnapshotMetadata, SnapshotSelectionCriteria} - -import scala.concurrent.Future - -class InMemSnapshotStore extends SnapshotStore { - - private[this] final var snapshots = Map.empty[String, SelectedSnapshot] - - override def loadAsync(persistenceId: String, criteria: SnapshotSelectionCriteria): Future[Option[SelectedSnapshot]] = - Future.successful( - snapshots - .get(persistenceId) - .filter( - s => - s.metadata.sequenceNr >= criteria.minSequenceNr && - s.metadata.sequenceNr <= criteria.maxSequenceNr && - s.metadata.timestamp >= criteria.minTimestamp && - s.metadata.timestamp <= criteria.maxTimestamp - ) - ) - - override def saveAsync(metadata: SnapshotMetadata, snapshot: Any): Future[Unit] = { - snapshots += metadata.persistenceId -> SelectedSnapshot(metadata, snapshot) - Future.unit - } - - override def deleteAsync(metadata: SnapshotMetadata): Future[Unit] = { - snapshots -= metadata.persistenceId - Future.unit - } - - override def deleteAsync(persistenceId: String, criteria: SnapshotSelectionCriteria): Future[Unit] = { - snapshots -= persistenceId - Future.unit - } -} diff --git a/proxy/core/src/main/scala/io/cloudstate/proxy/crud/readme.md b/proxy/core/src/main/scala/io/cloudstate/proxy/crud/readme.md index 74684c901..519880c9e 100644 --- a/proxy/core/src/main/scala/io/cloudstate/proxy/crud/readme.md +++ b/proxy/core/src/main/scala/io/cloudstate/proxy/crud/readme.md @@ -14,8 +14,8 @@ ### What are the next steps: - add Postgres native CRUD support -- remove the sequence number from the protocol from the implementation -- add generic type for io.cloudstate.javasupport.crud.CommandContext +- remove the sequence number from the protocol from the implementation :white_check_mark: +- add generic type for io.cloudstate.javasupport.crud.CommandContext :white_check_mark: - deal with null value for io.cloudstate.javasupport.crud.CommandContext#updateEntity - deal with the order of call for io.cloudstate.javasupport.crud.CommandContext#deleteEntity and io.cloudstate.javasupport.crud.CommandContext#updateEntity - write tests diff --git a/proxy/core/src/main/scala/io/cloudstate/proxy/crud/store/JdbcInMemoryStore.scala b/proxy/core/src/main/scala/io/cloudstate/proxy/crud/store/JdbcInMemoryStore.scala index c658b7e86..243821d42 100644 --- a/proxy/core/src/main/scala/io/cloudstate/proxy/crud/store/JdbcInMemoryStore.scala +++ b/proxy/core/src/main/scala/io/cloudstate/proxy/crud/store/JdbcInMemoryStore.scala @@ -34,7 +34,7 @@ final class JdbcInMemoryStore extends JdbcStore[Key, ByteString] { } override def update(key: Key, value: ByteString): Future[Unit] = { - logger.info(s"update called with key - $key and value - $value") + logger.info(s"update called with key - $key and value - ${value.utf8String}") store += key -> value Future.unit } From 8692f576d177bea3a3afea8ba3fdf9752be4ac2c Mon Sep 17 00:00:00 2001 From: Guy Youansi Date: Mon, 7 Sep 2020 21:20:57 +0200 Subject: [PATCH 34/93] first version for checking null on updateEntity --- .../cloudstate/javasupport/impl/crud/CrudImpl.scala | 11 +++++++---- .../src/main/scala/io/cloudstate/proxy/crud/readme.md | 3 ++- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/CrudImpl.scala b/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/CrudImpl.scala index 20a5e4114..d09281128 100644 --- a/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/CrudImpl.scala +++ b/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/CrudImpl.scala @@ -149,11 +149,12 @@ final class CrudImpl(_system: ActorSystem, handler ) val reply = try { + //TODO: deal with exception! handler.handleCommand(cmd, context) } catch { - case FailInvoked => Option.empty[JavaPbAny].asJava + case FailInvoked => Option.empty[JavaPbAny].asJava // Ignore, error already captured } finally { - context.deactivate() + context.deactivate() // Very important! } val clientAction = context.createClientAction(reply, false) @@ -178,7 +179,7 @@ final class CrudImpl(_system: ActorSystem, } case InInit(_) => - throw new IllegalStateException("CRUD Entity already inited") + throw new IllegalStateException("CRUD Entity already initialized") case InEmpty => throw new IllegalStateException("CRUD Entity received empty/unknown message") @@ -207,7 +208,9 @@ final class CrudImpl(_system: ActorSystem, private var mayBeAction: Option[CrudAction] = None override def updateEntity(state: AnyRef): Unit = { - // TODO null check for state + if (state == null) + throw new IllegalArgumentException(s"Cannot update a 'null' state for CRUD Entity") + checkActive() val encoded = anySupport.encodeScala(state) mayBeAction = Some(CrudAction(Update(CrudUpdate(Some(encoded))))) diff --git a/proxy/core/src/main/scala/io/cloudstate/proxy/crud/readme.md b/proxy/core/src/main/scala/io/cloudstate/proxy/crud/readme.md index 519880c9e..87e713dd0 100644 --- a/proxy/core/src/main/scala/io/cloudstate/proxy/crud/readme.md +++ b/proxy/core/src/main/scala/io/cloudstate/proxy/crud/readme.md @@ -16,7 +16,8 @@ - add Postgres native CRUD support - remove the sequence number from the protocol from the implementation :white_check_mark: - add generic type for io.cloudstate.javasupport.crud.CommandContext :white_check_mark: -- deal with null value for io.cloudstate.javasupport.crud.CommandContext#updateEntity +- deal with null value for io.cloudstate.javasupport.crud.CommandContext#updateEntity :white_check_mark: - deal with the order of call for io.cloudstate.javasupport.crud.CommandContext#deleteEntity and io.cloudstate.javasupport.crud.CommandContext#updateEntity +- deal with exceptions - write tests - extend the TCK \ No newline at end of file From 18ce9c8b84a2a0409e14c2b3add0c4075c156f3d Mon Sep 17 00:00:00 2001 From: Guy Youansi Date: Mon, 7 Sep 2020 21:25:47 +0200 Subject: [PATCH 35/93] add comments --- .../scala/io/cloudstate/javasupport/impl/crud/CrudImpl.scala | 1 + 1 file changed, 1 insertion(+) diff --git a/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/CrudImpl.scala b/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/CrudImpl.scala index d09281128..75ca3ec7e 100644 --- a/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/CrudImpl.scala +++ b/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/CrudImpl.scala @@ -73,6 +73,7 @@ final class CrudImpl(_system: ActorSystem, ): akka.stream.scaladsl.Source[CrudStreamOut, akka.NotUsed] = in.prefixAndTail(1) .flatMapConcat { + // TODO: check!!! the InInit message not always comes first. it is maybe because of preStart in CrudEntity Actor!!! case (Seq(CrudStreamIn(InInit(init), _)), source) => source.via(runEntity(init)) case _ => From 209648d683f1871d0944f95efb1b3ee0f7ed5975 Mon Sep 17 00:00:00 2001 From: Guy Youansi Date: Wed, 9 Sep 2020 14:04:11 +0200 Subject: [PATCH 36/93] add exception handling based on the one in event sourced --- .../crud/AnnotationBasedCrudSupport.scala | 7 +- .../javasupport/impl/crud/CrudImpl.scala | 117 +++++++++++------- 2 files changed, 75 insertions(+), 49 deletions(-) diff --git a/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/AnnotationBasedCrudSupport.scala b/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/AnnotationBasedCrudSupport.scala index 14b2e134c..895300215 100644 --- a/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/AnnotationBasedCrudSupport.scala +++ b/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/AnnotationBasedCrudSupport.scala @@ -34,6 +34,7 @@ import io.cloudstate.javasupport.crud.{ UpdateStateHandler } import io.cloudstate.javasupport.impl.ReflectionHelper.{InvocationContext, MainArgumentParameterHandler} +import io.cloudstate.javasupport.impl.crud.CrudImpl.EntityException import io.cloudstate.javasupport.impl.{AnySupport, ReflectionHelper, ResolvedEntityFactory, ResolvedServiceMethod} import scala.collection.concurrent.TrieMap @@ -77,7 +78,7 @@ private[impl] class AnnotationBasedCrudSupport( behavior.commandHandlers.get(context.commandName()).map { handler => handler.invoke(entity, command, context) } getOrElse { - throw new RuntimeException( + throw EntityException( s"No command handler found for command [${context.commandName()}] on $behaviorsString" ) } @@ -91,7 +92,7 @@ private[impl] class AnnotationBasedCrudSupport( val ctx = new DelegatingCrudContext(context) with StateContext handler.invoke(entity, state, ctx) case None => - throw new RuntimeException( + throw EntityException( s"No update state handler found for ${state.getClass} on $behaviorsString" ) } @@ -101,7 +102,7 @@ private[impl] class AnnotationBasedCrudSupport( behavior.deleteHandler match { case Some(handler) => handler.invoke(entity, context) case None => - throw new RuntimeException(s"No delete state handler found on $behaviorsString") + throw EntityException(s"No delete state handler found on $behaviorsString") } } diff --git a/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/CrudImpl.scala b/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/CrudImpl.scala index 75ca3ec7e..3fa329967 100644 --- a/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/CrudImpl.scala +++ b/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/CrudImpl.scala @@ -18,19 +18,23 @@ package io.cloudstate.javasupport.impl.crud import akka.NotUsed import akka.actor.ActorSystem -import akka.stream.scaladsl.{Flow, Source} +import akka.event.{Logging, LoggingAdapter} +import akka.stream.scaladsl.Flow import io.cloudstate.javasupport.CloudStateRunner.Configuration import com.google.protobuf.{Descriptors, Any => JavaPbAny} import com.google.protobuf.any.{Any => ScalaPbAny} import io.cloudstate.javasupport.crud._ import io.cloudstate.javasupport.impl._ +import io.cloudstate.javasupport.impl.crud.CrudImpl.{failure, failureMessage, EntityException, ProtocolException} import io.cloudstate.javasupport.{Context, ServiceCallFactory, StatefulService} import io.cloudstate.protocol.crud.CrudAction.Action.{Delete, Update} import io.cloudstate.protocol.crud._ import io.cloudstate.protocol.crud.CrudStreamIn.Message.{Command => InCommand, Empty => InEmpty, Init => InInit} -import io.cloudstate.protocol.entity.Failure +import io.cloudstate.protocol.crud.CrudStreamOut.Message.{Failure => OutFailure, Reply => OutReply} +import io.cloudstate.protocol.entity.{Command, Failure} import scala.compat.java8.OptionConverters._ +import scala.util.control.NonFatal final class CrudStatefulService(val factory: CrudEntityFactory, override val descriptor: Descriptors.ServiceDescriptor, @@ -47,6 +51,46 @@ final class CrudStatefulService(val factory: CrudEntityFactory, override final val entityType = io.cloudstate.protocol.crud.Crud.name } +object CrudImpl { + final case class EntityException(entityId: String, commandId: Long, commandName: String, message: String) + extends RuntimeException(message) + + object EntityException { + def apply(message: String): EntityException = + EntityException(entityId = "", commandId = 0, commandName = "", message) + + def apply(command: Command, message: String): EntityException = + EntityException(command.entityId, command.id, command.name, message) + + def apply(context: CommandContext[_], message: String): EntityException = + EntityException(context.entityId, context.commandId, context.commandName, message) + } + + object ProtocolException { + def apply(message: String): EntityException = + EntityException(entityId = "", commandId = 0, commandName = "", "Protocol error: " + message) + + def apply(init: CrudInit, message: String): EntityException = + EntityException(init.entityId, commandId = 0, commandName = "", "Protocol error: " + message) + + def apply(command: Command, message: String): EntityException = + EntityException(command.entityId, command.id, command.name, "Protocol error: " + message) + } + + def failure(cause: Throwable): Failure = cause match { + case e: EntityException => Failure(e.commandId, e.message) + case e => Failure(description = "Unexpected failure: " + e.getMessage) + } + + def failureMessage(cause: Throwable): String = cause match { + case EntityException(entityId, commandId, commandName, _) => + val commandDescription = if (commandId != 0) s" for command [$commandName]" else "" + val entityDescription = if (entityId.nonEmpty) s"entity [$entityId]" else "entity" + s"Terminating $entityDescription due to unexpected failure$commandDescription" + case _ => "Terminating entity due to unexpected failure" + } +} + final class CrudImpl(_system: ActorSystem, _services: Map[String, CrudStatefulService], rootContext: Context, @@ -56,6 +100,7 @@ final class CrudImpl(_system: ActorSystem, private final val system = _system private final implicit val ec = system.dispatcher private final val services = _services.iterator.toMap + private final val log = Logging(system.eventStream, this.getClass) /** * One stream will be established per active entity. @@ -77,33 +122,17 @@ final class CrudImpl(_system: ActorSystem, case (Seq(CrudStreamIn(InInit(init), _)), source) => source.via(runEntity(init)) case _ => - Source.single( - CrudStreamOut( - CrudStreamOut.Message.Failure( - Failure( - 0, - "Cloudstate protocol failure for CRUD entity: expected init message" - ) - ) - ) - ) + throw ProtocolException("Expected Init message for CRUD entity") } .recover { - case e => - system.log.error(e, "Unexpected error, terminating CRUD Entity") - CrudStreamOut( - CrudStreamOut.Message.Failure( - Failure( - 0, - s"Cloudstate protocol failure for CRUD entity: ${e.getMessage}" - ) - ) - ) + case error => + log.error(error, failureMessage(error)) + CrudStreamOut(OutFailure(failure(error))) } private def runEntity(init: CrudInit): Flow[CrudStreamIn, CrudStreamOut, NotUsed] = { val service = - services.getOrElse(init.serviceName, throw new RuntimeException(s"Service not found: ${init.serviceName}")) + services.getOrElse(init.serviceName, throw ProtocolException(s"Service not found: ${init.serviceName}")) val handler = service.factory.create(new CrudContextImpl(init.entityId)) val thisEntityId = init.entityId @@ -123,22 +152,10 @@ final class CrudImpl(_system: ActorSystem, .map { m => m.message match { case InCommand(command) if thisEntityId != command.entityId => - CrudStreamOut.Message.Failure( - Failure( - command.id, - s"""Cloudstate protocol failure for CRUD entity: - |Receiving entity - $thisEntityId is not the intended recipient - |of command with id - ${command.id} and name - ${command.name}""".stripMargin.replaceAll("\n", " ") - ) - ) + throw ProtocolException(command, "Receiving entity is not the intended recipient for CRUD entity") case InCommand(command) if command.payload.isEmpty => - CrudStreamOut.Message.Failure( - Failure( - command.id, - s"Cloudstate protocol failure for CRUD entity: Command (id: ${command.id}, name: ${command.name}) should have a payload" - ) - ) + throw ProtocolException(command, "No command payload for CRUD entity") case InCommand(command) => val cmd = ScalaPbAny.toJavaProto(command.payload.get) @@ -147,13 +164,16 @@ final class CrudImpl(_system: ActorSystem, command.name, command.id, service.anySupport, - handler + handler, + log ) val reply = try { - //TODO: deal with exception! handler.handleCommand(cmd, context) } catch { case FailInvoked => Option.empty[JavaPbAny].asJava // Ignore, error already captured + case e: EntityException => throw e + case NonFatal(error) => + throw EntityException(command, s"CRUD entity Unexpected failure: ${error.getMessage}") } finally { context.deactivate() // Very important! } @@ -161,7 +181,7 @@ final class CrudImpl(_system: ActorSystem, val clientAction = context.createClientAction(reply, false) if (!context.hasError) { context.applyCrudAction() - CrudStreamOut.Message.Reply( + OutReply( CrudReply( command.id, clientAction, @@ -170,7 +190,7 @@ final class CrudImpl(_system: ActorSystem, ) ) } else { - CrudStreamOut.Message.Reply( + OutReply( CrudReply( commandId = command.id, clientAction = clientAction, @@ -180,10 +200,10 @@ final class CrudImpl(_system: ActorSystem, } case InInit(_) => - throw new IllegalStateException("CRUD Entity already initialized") + throw ProtocolException(init, "CRUD entity already initialized") case InEmpty => - throw new IllegalStateException("CRUD Entity received empty/unknown message") + throw ProtocolException(init, "CRUD entity received empty/unknown message") } } .collect { @@ -199,7 +219,8 @@ final class CrudImpl(_system: ActorSystem, override val commandName: String, override val commandId: Long, val anySupport: AnySupport, - val handler: CrudEntityHandler) + val handler: CrudEntityHandler, + val log: LoggingAdapter) extends CommandContext[AnyRef] with AbstractContext with AbstractClientActionContext @@ -209,10 +230,11 @@ final class CrudImpl(_system: ActorSystem, private var mayBeAction: Option[CrudAction] = None override def updateEntity(state: AnyRef): Unit = { + checkActive() + if (state == null) - throw new IllegalArgumentException(s"Cannot update a 'null' state for CRUD Entity") + throw EntityException("CRUD entity cannot update a 'null' state") - checkActive() val encoded = anySupport.encodeScala(state) mayBeAction = Some(CrudAction(Update(CrudUpdate(Some(encoded))))) } @@ -222,6 +244,9 @@ final class CrudImpl(_system: ActorSystem, mayBeAction = Some(CrudAction(Delete(CrudDelete()))) } + override protected def logError(message: String): Unit = + log.error("Fail invoked for command [{}] for CRUD entity [{}]: {}", commandName, entityId, message) + def crudAction(): Option[CrudAction] = mayBeAction def applyCrudAction(): Unit = From 44175e0e18848cfe887fdfae7bd70377dc12c940 Mon Sep 17 00:00:00 2001 From: Guy Youansi Date: Wed, 9 Sep 2020 14:05:54 +0200 Subject: [PATCH 37/93] add new lines --- .../main/scala/io/cloudstate/proxy/crud/store/package-info.java | 2 +- .../java-crud-shopping-cart/src/main/resources/application.conf | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/proxy/core/src/main/scala/io/cloudstate/proxy/crud/store/package-info.java b/proxy/core/src/main/scala/io/cloudstate/proxy/crud/store/package-info.java index ca47638c2..34ece9716 100644 --- a/proxy/core/src/main/scala/io/cloudstate/proxy/crud/store/package-info.java +++ b/proxy/core/src/main/scala/io/cloudstate/proxy/crud/store/package-info.java @@ -1,4 +1,4 @@ /** * Most part of the code in this package has been copied/adapted from https://github.com/akka/akka-persistence-jdbc */ -package io.cloudstate.proxy.crud.store; \ No newline at end of file +package io.cloudstate.proxy.crud.store; diff --git a/samples/java-crud-shopping-cart/src/main/resources/application.conf b/samples/java-crud-shopping-cart/src/main/resources/application.conf index b9d4ac102..0360ee749 100644 --- a/samples/java-crud-shopping-cart/src/main/resources/application.conf +++ b/samples/java-crud-shopping-cart/src/main/resources/application.conf @@ -4,4 +4,4 @@ cloudstate { loglevel = "DEBUG" logging-filter = "akka.event.slf4j.Slf4jLoggingFilter" } -} \ No newline at end of file +} From b866e710455a71b709371a4dba4eee1f5fdb78fd Mon Sep 17 00:00:00 2001 From: Guy Youansi Date: Mon, 14 Sep 2020 17:20:29 +0200 Subject: [PATCH 38/93] rewrite crud protocol by using oly command handler and passing the state in the command context --- .../javasupport/crud/CommandContext.java | 17 +- .../javasupport/crud/CrudEntityHandler.java | 19 +- .../javasupport/crud/DeleteStateHandler.java | 41 ---- .../javasupport/crud/StateContext.java | 20 -- .../javasupport/crud/StateHandler.java | 39 ---- .../javasupport/crud/UpdateStateHandler.java | 41 ---- .../javasupport/crud/package-info.java | 4 - .../crud/AnnotationBasedCrudSupport.scala | 146 +++---------- .../crud/CrudActionInvocationChecker.scala | 119 +++++++++++ .../javasupport/impl/crud/CrudImpl.scala | 191 +++++++++--------- .../crud/AnnotationBasedCrudSupportSpec.scala | 174 ++++------------ .../io/cloudstate/proxy/crud/CrudEntity.scala | 70 ++++--- .../shoppingcart/ShoppingCartEntity.java | 146 ++++++++----- 13 files changed, 448 insertions(+), 579 deletions(-) delete mode 100644 java-support/src/main/java/io/cloudstate/javasupport/crud/DeleteStateHandler.java delete mode 100644 java-support/src/main/java/io/cloudstate/javasupport/crud/StateContext.java delete mode 100644 java-support/src/main/java/io/cloudstate/javasupport/crud/StateHandler.java delete mode 100644 java-support/src/main/java/io/cloudstate/javasupport/crud/UpdateStateHandler.java create mode 100644 java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/CrudActionInvocationChecker.scala diff --git a/java-support/src/main/java/io/cloudstate/javasupport/crud/CommandContext.java b/java-support/src/main/java/io/cloudstate/javasupport/crud/CommandContext.java index 02e13bb26..146885a93 100644 --- a/java-support/src/main/java/io/cloudstate/javasupport/crud/CommandContext.java +++ b/java-support/src/main/java/io/cloudstate/javasupport/crud/CommandContext.java @@ -19,6 +19,8 @@ import io.cloudstate.javasupport.ClientActionContext; import io.cloudstate.javasupport.EffectContext; +import java.util.Optional; + /** * A CRUD command context. * @@ -42,13 +44,22 @@ public interface CommandContext extends CrudContext, ClientActionContext, Eff */ long commandId(); + /** + * Retrieve the state. + * + * @return the current state or empty if none have been created. + * @throws IllegalStateException If the current entity state have been deleted in the command + * invocation. + */ + Optional getState() throws IllegalStateException; + /** * Update the entity with the new state. The state will be persisted. * * @param state The state to persist. */ - void updateEntity(T state); + void updateState(T state); - /** Delete the entity. */ - void deleteEntity(); + /** Delete the entity state. */ + void deleteState(); } diff --git a/java-support/src/main/java/io/cloudstate/javasupport/crud/CrudEntityHandler.java b/java-support/src/main/java/io/cloudstate/javasupport/crud/CrudEntityHandler.java index 7bfca6cab..49cae1c2b 100644 --- a/java-support/src/main/java/io/cloudstate/javasupport/crud/CrudEntityHandler.java +++ b/java-support/src/main/java/io/cloudstate/javasupport/crud/CrudEntityHandler.java @@ -25,7 +25,7 @@ * an CRUD entity. * *

Generally, this should not be needed, instead, a class annotated with the {@link - * StateHandler}, {@link CommandHandler} and similar annotations should be used. + * CommandHandler} and similar annotations should be used. */ public interface CrudEntityHandler { @@ -36,20 +36,5 @@ public interface CrudEntityHandler { * @param context The command context. * @return The reply to the command, if the command isn't being forwarded elsewhere. */ - Optional handleCommand(Any command, CommandContext context); - - /** - * Handle the given state. - * - * @param state The state to handle. - * @param context The state context. - */ - void handleUpdate(Any state, StateContext context); - - /** - * Handle the state deletion. - * - * @param context The state context. - */ - void handleDelete(StateContext context); + Optional handleCommand(Any command, CommandContext context); } diff --git a/java-support/src/main/java/io/cloudstate/javasupport/crud/DeleteStateHandler.java b/java-support/src/main/java/io/cloudstate/javasupport/crud/DeleteStateHandler.java deleted file mode 100644 index 42088c2c6..000000000 --- a/java-support/src/main/java/io/cloudstate/javasupport/crud/DeleteStateHandler.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright 2019 Lightbend Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.cloudstate.javasupport.crud; - -import io.cloudstate.javasupport.impl.CloudStateAnnotation; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/** - * Marks a method as a delete state handler. - * - *

If, when recovering an entity, that entity has a state, the state will be passed to a - * corresponding state handler method whose argument matches its type. The entity must set its - * current state to that state. - * - *

An entity must declare only one delete state handler. - * - *

The delete state handler method may additionally accept a {@link StateContext} parameter, - * allowing it to access context for the state, if required. - */ -@CloudStateAnnotation -@Target(ElementType.METHOD) -@Retention(RetentionPolicy.RUNTIME) -public @interface DeleteStateHandler {} diff --git a/java-support/src/main/java/io/cloudstate/javasupport/crud/StateContext.java b/java-support/src/main/java/io/cloudstate/javasupport/crud/StateContext.java deleted file mode 100644 index ff22690b8..000000000 --- a/java-support/src/main/java/io/cloudstate/javasupport/crud/StateContext.java +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright 2019 Lightbend Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.cloudstate.javasupport.crud; - -/** A state context. */ -public interface StateContext extends CrudContext {} diff --git a/java-support/src/main/java/io/cloudstate/javasupport/crud/StateHandler.java b/java-support/src/main/java/io/cloudstate/javasupport/crud/StateHandler.java deleted file mode 100644 index 592180576..000000000 --- a/java-support/src/main/java/io/cloudstate/javasupport/crud/StateHandler.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright 2019 Lightbend Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.cloudstate.javasupport.crud; - -import io.cloudstate.javasupport.impl.CloudStateAnnotation; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/** - * Marks a method as a state handler. - * - *

If, when recovering an entity, that entity has a state, the state will be passed to a - * corresponding state handler method whose argument matches its type. The entity must set its - * current state to that state. - * - *

The state handler method may additionally accept a {@link StateContext} parameter, allowing it - * to access context for the state, if required. - */ -@CloudStateAnnotation -@Target(ElementType.METHOD) -@Retention(RetentionPolicy.RUNTIME) -public @interface StateHandler {} diff --git a/java-support/src/main/java/io/cloudstate/javasupport/crud/UpdateStateHandler.java b/java-support/src/main/java/io/cloudstate/javasupport/crud/UpdateStateHandler.java deleted file mode 100644 index 213ea0cdc..000000000 --- a/java-support/src/main/java/io/cloudstate/javasupport/crud/UpdateStateHandler.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright 2019 Lightbend Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.cloudstate.javasupport.crud; - -import io.cloudstate.javasupport.impl.CloudStateAnnotation; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/** - * Marks a method as a update state handler. - * - *

If, when recovering an entity, that entity has a state, the state will be passed to a - * corresponding state handler method whose argument matches its type. The entity must set its - * current state to that state. - * - *

An entity must declare only one update state handler. - * - *

The update state handler method may additionally accept a {@link StateContext} parameter, - * allowing it to access context for the state, if required. - */ -@CloudStateAnnotation -@Target(ElementType.METHOD) -@Retention(RetentionPolicy.RUNTIME) -public @interface UpdateStateHandler {} diff --git a/java-support/src/main/java/io/cloudstate/javasupport/crud/package-info.java b/java-support/src/main/java/io/cloudstate/javasupport/crud/package-info.java index 905ee484d..19393b9a8 100644 --- a/java-support/src/main/java/io/cloudstate/javasupport/crud/package-info.java +++ b/java-support/src/main/java/io/cloudstate/javasupport/crud/package-info.java @@ -4,9 +4,5 @@ *

CRUD entities can be annotated with the {@link * io.cloudstate.javasupport.crud.CrudEntity @CrudEntity} annotation, and supply command handlers * using the {@link io.cloudstate.javasupport.crud.CommandHandler @CommandHandler} annotation. - * - *

In addition, {@link io.cloudstate.javasupport.crud.UpdateStateHandler @UpdateStateHandler} and - * {@link io.cloudstate.javasupport.crud.DeleteStateHandler @DeleteStateHandler} annotated methods - * should be defined to handle entity state respectively. */ package io.cloudstate.javasupport.crud; diff --git a/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/AnnotationBasedCrudSupport.scala b/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/AnnotationBasedCrudSupport.scala index 895300215..cdeffbda2 100644 --- a/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/AnnotationBasedCrudSupport.scala +++ b/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/AnnotationBasedCrudSupport.scala @@ -20,7 +20,7 @@ import java.lang.reflect.{Constructor, InvocationTargetException, Method} import java.util.Optional import com.google.protobuf.{Descriptors, Any => JavaPbAny} -import io.cloudstate.javasupport.ServiceCallFactory +import io.cloudstate.javasupport.{ServiceCall, ServiceCallFactory} import io.cloudstate.javasupport.crud.{ CommandContext, CommandHandler, @@ -28,17 +28,12 @@ import io.cloudstate.javasupport.crud.{ CrudEntity, CrudEntityCreationContext, CrudEntityFactory, - CrudEntityHandler, - DeleteStateHandler, - StateContext, - UpdateStateHandler + CrudEntityHandler } import io.cloudstate.javasupport.impl.ReflectionHelper.{InvocationContext, MainArgumentParameterHandler} import io.cloudstate.javasupport.impl.crud.CrudImpl.EntityException import io.cloudstate.javasupport.impl.{AnySupport, ReflectionHelper, ResolvedEntityFactory, ResolvedServiceMethod} -import scala.collection.concurrent.TrieMap - /** * Annotation based implementation of the [[CrudEntityFactory]]. */ @@ -74,9 +69,11 @@ private[impl] class AnnotationBasedCrudSupport( }) } - override def handleCommand[T](command: JavaPbAny, context: CommandContext[T]): Optional[JavaPbAny] = unwrap { + override def handleCommand(command: JavaPbAny, context: CommandContext[JavaPbAny]): Optional[JavaPbAny] = unwrap { behavior.commandHandlers.get(context.commandName()).map { handler => - handler.invoke(entity, command, context) + val adaptedContext = + new AdaptedCommandContext(context, anySupport) + handler.invoke(entity, command, adaptedContext) } getOrElse { throw EntityException( s"No command handler found for command [${context.commandName()}] on $behaviorsString" @@ -84,28 +81,6 @@ private[impl] class AnnotationBasedCrudSupport( } } - override def handleUpdate(anyState: JavaPbAny, context: StateContext): Unit = unwrap { - val state = anySupport.decode(anyState).asInstanceOf[AnyRef] - - behavior.getCachedUpdateHandlerForClass(state.getClass) match { - case Some(handler) => - val ctx = new DelegatingCrudContext(context) with StateContext - handler.invoke(entity, state, ctx) - case None => - throw EntityException( - s"No update state handler found for ${state.getClass} on $behaviorsString" - ) - } - } - - override def handleDelete(context: StateContext): Unit = unwrap { - behavior.deleteHandler match { - case Some(handler) => handler.invoke(entity, context) - case None => - throw EntityException(s"No delete state handler found on $behaviorsString") - } - } - private def unwrap[T](block: => T): T = try { block @@ -124,27 +99,8 @@ private[impl] class AnnotationBasedCrudSupport( } private class CrudBehaviorReflection( - val commandHandlers: Map[String, ReflectionHelper.CommandHandlerInvoker[CommandContext[_]]], - val updateHandlers: Map[Class[_], UpdateInvoker], - val deleteHandler: Option[DeleteInvoker] -) { - - private val updateStateHandlerCache = TrieMap.empty[Class[_], Option[UpdateInvoker]] - - def getCachedUpdateHandlerForClass(clazz: Class[_]): Option[UpdateInvoker] = - updateStateHandlerCache.getOrElseUpdate(clazz, getHandlerForClass(updateHandlers)(clazz)) - - private def getHandlerForClass[T](handlers: Map[Class[_], T])(clazz: Class[_]): Option[T] = - handlers.get(clazz) match { - case some @ Some(_) => some - case None => - clazz.getInterfaces.collectFirst(Function.unlift(getHandlerForClass(handlers))) match { - case some @ Some(_) => some - case None if clazz.getSuperclass != null => getHandlerForClass(handlers)(clazz.getSuperclass) - case None => None - } - } -} + val commandHandlers: Map[String, ReflectionHelper.CommandHandlerInvoker[CommandContext[AnyRef]]] +) {} private object CrudBehaviorReflection { def apply(behaviorClass: Class[_], @@ -165,8 +121,8 @@ private object CrudBehaviorReflection { ) }) - new ReflectionHelper.CommandHandlerInvoker[CommandContext[_]](ReflectionHelper.ensureAccessible(method), - serviceMethod) + new ReflectionHelper.CommandHandlerInvoker[CommandContext[AnyRef]](ReflectionHelper.ensureAccessible(method), + serviceMethod) } .groupBy(_.serviceMethod.name) .map { @@ -177,40 +133,13 @@ private object CrudBehaviorReflection { ) } - val updateStateHandlers = allMethods - .filter(_.getAnnotation(classOf[UpdateStateHandler]) != null) - .map { method => - new UpdateInvoker(ReflectionHelper.ensureAccessible(method)) - } - .groupBy(_.stateClass) - .map { - case (stateClass, Seq(invoker)) => (stateClass: Any) -> invoker - case (clazz, many) => - throw new RuntimeException( - s"Multiple CRUD update handlers found of type $clazz: ${many.map(_.method.getName)}" - ) - } - .asInstanceOf[Map[Class[_], UpdateInvoker]] - - val deleteStateHandler = allMethods - .filter(_.getAnnotation(classOf[DeleteStateHandler]) != null) - .map { method => - new DeleteInvoker(ReflectionHelper.ensureAccessible(method)) - } match { - case Seq() => None - case Seq(single) => - Some(single) - case _ => - throw new RuntimeException(s"Multiple CRUD delete methods found on behavior $behaviorClass") - } - ReflectionHelper.validateNoBadMethods( allMethods, classOf[CrudEntity], - Set(classOf[CommandHandler], classOf[UpdateStateHandler], classOf[DeleteStateHandler]) + Set(classOf[CommandHandler]) ) - new CrudBehaviorReflection(commandHandlers, updateStateHandlers, deleteStateHandler) + new CrudBehaviorReflection(commandHandlers) } } @@ -228,41 +157,30 @@ private class EntityConstructorInvoker(constructor: Constructor[_]) extends (Cru } } -private class UpdateInvoker(val method: Method) { - private val parameters = ReflectionHelper.getParameterHandlers[StateContext](method)() +/* + * This class is a conversion bridge between CommandContext[JavaPbAny] and CommandContext[AnyRef]. + * It helps for making the conversion from JavaPbAny to AnyRef and backward. + */ +private class AdaptedCommandContext(val delegate: CommandContext[JavaPbAny], anySupport: AnySupport) + extends CommandContext[AnyRef] { - // Verify that there is at most one update state handler - val stateClass: Class[_] = parameters.collect { - case MainArgumentParameterHandler(clazz) => clazz - } match { - case Array(handlerClass) => handlerClass - case other => - throw new RuntimeException( - s"UpdateStateHandler method $method must defined at most one non context parameter to handle state, the parameters defined were: ${other - .mkString(",")}" - ) + override def getState(): Optional[AnyRef] = { + val result = delegate.getState + result.map(anySupport.decode(_).asInstanceOf[AnyRef]) } - def invoke(obj: AnyRef, state: AnyRef, context: StateContext): Unit = { - val ctx = InvocationContext(state, context) - method.invoke(obj, parameters.map(_.apply(ctx)): _*) + override def updateState(state: AnyRef): Unit = { + val encoded = anySupport.encodeJava(state) + delegate.updateState(encoded) } -} - -private class DeleteInvoker(val method: Method) { - private val parameters = ReflectionHelper.getParameterHandlers[StateContext](method)() + override def deleteState(): Unit = delegate.deleteState() - parameters.foreach { - case MainArgumentParameterHandler(clazz) => - throw new RuntimeException( - s"DeleteStateHandler method $method must defined only a context parameter to handle the state, the parameter defined is: ${clazz.getName}" - ) - case _ => - } - - def invoke(obj: AnyRef, context: StateContext): Unit = { - val ctx = InvocationContext("", context) - method.invoke(obj, parameters.map(_.apply(ctx)): _*) - } + override def commandName(): String = delegate.commandName() + override def commandId(): Long = delegate.commandId() + override def entityId(): String = delegate.entityId() + override def effect(effect: ServiceCall, synchronous: Boolean): Unit = delegate.effect(effect, synchronous) + override def fail(errorMessage: String): RuntimeException = delegate.fail(errorMessage) + override def forward(to: ServiceCall): Unit = delegate.forward(to) + override def serviceCallFactory(): ServiceCallFactory = delegate.serviceCallFactory() } diff --git a/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/CrudActionInvocationChecker.scala b/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/CrudActionInvocationChecker.scala new file mode 100644 index 000000000..d19267bb8 --- /dev/null +++ b/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/CrudActionInvocationChecker.scala @@ -0,0 +1,119 @@ +/* + * Copyright 2019 Lightbend Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.cloudstate.javasupport.impl.crud + +import io.cloudstate.javasupport.impl.crud.CrudImpl.EntityException +import io.cloudstate.protocol.crud.CrudAction.Action.{Delete, Update} +import io.cloudstate.protocol.crud.{CrudAction, CrudDelete, CrudUpdate} + +private[impl] object CrudActionInvocationChecker { + private val updateActionName = "updateState" + private val deleteActionName = "deleteState" + + case class CrudActionInvocationContext(entityId: String, commandId: Long, commandName: String) +} + +private[impl] trait CrudActionInvocationChecker { + import CrudActionInvocationChecker._ + + def action: Option[CrudAction] + def stateDeleted: Boolean + + /** + * It should not be possible to call getState when deleteState have been called. + *

    + *
  • + * + * ctx.deleteState(); + * ctx.getState(); + * + *
  • + *
+ */ + def checkStateDeleted(ctx: CrudActionInvocationContext): Unit = + if (stateDeleted) { + throw new IllegalStateException( + s"The CRUD entity [${ctx.entityId}] does not exist and have been deleted with deleteState for command [${ctx.commandName}]" + ) + } + + /** + * It should not be possible to call some combinations of getState, updateState and deleteState because we don't + * what the intention of the caller is. + * Some examples of invocations that will failed: + *
    + *
  • + * + * ctx.updateState(…); + * ctx.updateState(…); + * + *
  • + *
  • + * + * ctx.deleteState(); + * ctx.updateState(…); + * + *
  • + *
  • + * + * ctx.updateState(…); + * ctx.deleteState(); + * + *
  • + * + * ctx.deleteState(); + * ctx.deleteState(); + * + *
  • + *
+ */ + def checkInvocation(ctx: CrudActionInvocationContext, _action: CrudAction): Unit = + action.map { a => + a.action match { + case Update(_) => + throw new EntityException(ctx.entityId, + ctx.commandId, + ctx.commandName, + invocationFailureMessage(ctx, updateActionName, _action)) + + case Delete(_) => + throw new EntityException(ctx.entityId, + ctx.commandId, + ctx.commandName, + invocationFailureMessage(ctx, deleteActionName, _action)) + + case _ => + } + } + + private def invocationFailureMessage(ctx: CrudActionInvocationContext, + firstActionName: String, + _action: CrudAction): String = { + val name = actionName(_action) + //s"CRUD entity [$entityId] command [$commandName] cannot run multiples actions ['$firstActionName', '$name']. Please choose only one action between ['updateState', 'deleteState'] to run" + s"""|CRUD entity [${ctx.entityId}] command [${ctx.commandName}] cannot run multiples actions ['$firstActionName', '$name']. + | Please choose only one action between ['${updateActionName}', '${deleteActionName}'] to run""".stripMargin + .replaceAll("\n", " ") + } + + private def actionName(crudAction: CrudAction): String = + crudAction.action match { + case Update(CrudUpdate(_, _)) => updateActionName + case Delete(CrudDelete(_)) => deleteActionName + case _ => "" + } +} diff --git a/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/CrudImpl.scala b/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/CrudImpl.scala index 3fa329967..ece06b727 100644 --- a/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/CrudImpl.scala +++ b/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/CrudImpl.scala @@ -16,6 +16,8 @@ package io.cloudstate.javasupport.impl.crud +import java.util.Optional + import akka.NotUsed import akka.actor.ActorSystem import akka.event.{Logging, LoggingAdapter} @@ -132,82 +134,80 @@ final class CrudImpl(_system: ActorSystem, private def runEntity(init: CrudInit): Flow[CrudStreamIn, CrudStreamOut, NotUsed] = { val service = - services.getOrElse(init.serviceName, throw ProtocolException(s"Service not found: ${init.serviceName}")) + services.getOrElse(init.serviceName, throw new RuntimeException(s"Service not found: ${init.serviceName}")) val handler = service.factory.create(new CrudContextImpl(init.entityId)) val thisEntityId = init.entityId - init.state match { - case Some(CrudInitState(Some(payload), _)) => - val encoded = service.anySupport.encodeScala(payload) - handler.handleUpdate(ScalaPbAny.toJavaProto(encoded), new StateContextImpl(thisEntityId)) - - case Some(CrudInitState(None, _)) => - handler.handleDelete(new StateContextImpl(thisEntityId)) - - case _ => - // should not happen! + val initState = init.state match { + case Some(CrudInitState(state, _)) => state + case _ => None } Flow[CrudStreamIn] - .map { m => - m.message match { - case InCommand(command) if thisEntityId != command.entityId => - throw ProtocolException(command, "Receiving entity is not the intended recipient for CRUD entity") - - case InCommand(command) if command.payload.isEmpty => - throw ProtocolException(command, "No command payload for CRUD entity") - - case InCommand(command) => - val cmd = ScalaPbAny.toJavaProto(command.payload.get) - val context = new CommandContextImpl( - thisEntityId, - command.name, - command.id, - service.anySupport, - handler, - log - ) - val reply = try { - handler.handleCommand(cmd, context) - } catch { - case FailInvoked => Option.empty[JavaPbAny].asJava // Ignore, error already captured - case e: EntityException => throw e - case NonFatal(error) => - throw EntityException(command, s"CRUD entity Unexpected failure: ${error.getMessage}") - } finally { - context.deactivate() // Very important! - } - - val clientAction = context.createClientAction(reply, false) - if (!context.hasError) { - context.applyCrudAction() - OutReply( - CrudReply( - command.id, - clientAction, - context.sideEffects, - context.crudAction() - ) - ) - } else { - OutReply( - CrudReply( - commandId = command.id, - clientAction = clientAction, - crudAction = context.crudAction() - ) - ) - } - - case InInit(_) => - throw ProtocolException(init, "CRUD entity already initialized") - - case InEmpty => - throw ProtocolException(init, "CRUD entity received empty/unknown message") - } + .map(_.message) + .scan[(Option[ScalaPbAny], Option[CrudStreamOut.Message])]((initState, None)) { + case (_, InCommand(command)) if thisEntityId != command.entityId => + throw ProtocolException(command, "Receiving entity is not the intended recipient for CRUD entity") + + case (_, InCommand(command)) if command.payload.isEmpty => + throw ProtocolException(command, "No command payload for CRUD entity") + + case ((state, _), InCommand(command)) => + val cmd = ScalaPbAny.toJavaProto(command.payload.get) + val context = new CommandContextImpl( + thisEntityId, + command.name, + command.id, + state, + service.anySupport, + log + ) + val reply = try { + handler.handleCommand(cmd, context) + } catch { + case FailInvoked => Option.empty[JavaPbAny].asJava + case e: EntityException => throw e + case NonFatal(error) => + throw EntityException(command, s"CRUD entity Unexpected failure: ${error.getMessage}") + } finally { + context.deactivate() // Very important! + } + + val clientAction = context.createClientAction(reply, false) + if (!context.hasError) { + val nextState = context.currentState() + (nextState, + Some( + OutReply( + CrudReply( + command.id, + clientAction, + context.sideEffects, + context.action + ) + ) + )) + } else { + (state, + Some( + OutReply( + CrudReply( + commandId = command.id, + clientAction = clientAction, + crudAction = context.action + ) + ) + )) + } + + case (_, InInit(_)) => + throw ProtocolException(init, "CRUD Entity already inited") + + case (_, InEmpty) => + throw ProtocolException(init, "CRUD entity received empty/unknown message") } .collect { - case message: CrudStreamOut.Message => CrudStreamOut(message) + case (_, Some(message)) => CrudStreamOut(message) } } @@ -218,53 +218,62 @@ final class CrudImpl(_system: ActorSystem, private final class CommandContextImpl(override val entityId: String, override val commandName: String, override val commandId: Long, + val state: Option[ScalaPbAny], val anySupport: AnySupport, - val handler: CrudEntityHandler, val log: LoggingAdapter) - extends CommandContext[AnyRef] + extends CommandContext[JavaPbAny] with AbstractContext with AbstractClientActionContext with AbstractEffectContext - with ActivatableContext { + with ActivatableContext + with CrudActionInvocationChecker { - private var mayBeAction: Option[CrudAction] = None + final var stateDeleted = false + final var action: Option[CrudAction] = None + private var _currentState: Option[ScalaPbAny] = state - override def updateEntity(state: AnyRef): Unit = { + override def getState(): Optional[JavaPbAny] = { checkActive() + checkStateDeleted(CrudActionInvocationChecker.CrudActionInvocationContext(entityId, commandId, commandName)) + + _currentState.map(ScalaPbAny.toJavaProto(_)).asJava + } + override def updateState(state: JavaPbAny): Unit = { + checkActive() if (state == null) throw EntityException("CRUD entity cannot update a 'null' state") + checkInvocation( + CrudActionInvocationChecker.CrudActionInvocationContext(entityId, commandId, commandName), + CrudAction(Update(CrudUpdate(None))) + ) val encoded = anySupport.encodeScala(state) - mayBeAction = Some(CrudAction(Update(CrudUpdate(Some(encoded))))) + _currentState = Some(encoded) + action = Some(CrudAction(Update(CrudUpdate(Some(encoded))))) + stateDeleted = false } - override def deleteEntity(): Unit = { + override def deleteState(): Unit = { checkActive() - mayBeAction = Some(CrudAction(Delete(CrudDelete()))) + checkInvocation( + CrudActionInvocationChecker.CrudActionInvocationContext(entityId, commandId, commandName), + CrudAction(Delete(CrudDelete())) + ) + + _currentState = None + action = Some(CrudAction(Delete(CrudDelete()))) + stateDeleted = true } override protected def logError(message: String): Unit = log.error("Fail invoked for command [{}] for CRUD entity [{}]: {}", commandName, entityId, message) - def crudAction(): Option[CrudAction] = mayBeAction + def currentState(): Option[ScalaPbAny] = + _currentState - def applyCrudAction(): Unit = - mayBeAction match { - case Some(CrudAction(action, _)) => - action match { - case Update(CrudUpdate(Some(value), _)) => - handler.handleUpdate(ScalaPbAny.toJavaProto(value), new StateContextImpl(entityId)) - - case Delete(CrudDelete(_)) => - handler.handleDelete(new StateContextImpl(entityId)) - } - case None => - // ignored, nothing to do it is a get request! - } } private final class CrudContextImpl(override final val entityId: String) extends CrudContext with AbstractContext - private final class StateContextImpl(override final val entityId: String) extends StateContext with AbstractContext } diff --git a/java-support/src/test/scala/io/cloudstate/javasupport/impl/crud/AnnotationBasedCrudSupportSpec.scala b/java-support/src/test/scala/io/cloudstate/javasupport/impl/crud/AnnotationBasedCrudSupportSpec.scala index f707ed56e..d627797a3 100644 --- a/java-support/src/test/scala/io/cloudstate/javasupport/impl/crud/AnnotationBasedCrudSupportSpec.scala +++ b/java-support/src/test/scala/io/cloudstate/javasupport/impl/crud/AnnotationBasedCrudSupportSpec.scala @@ -16,6 +16,8 @@ package io.cloudstate.javasupport.impl.crud +import java.util.Optional + import com.example.crud.shoppingcart.Shoppingcart import com.google.protobuf.any.{Any => ScalaPbAny} import com.google.protobuf.{ByteString, Any => JavaPbAny} @@ -24,14 +26,12 @@ import io.cloudstate.javasupport.crud.{ CommandHandler, CrudContext, CrudEntity, - CrudEntityCreationContext, - DeleteStateHandler, - StateContext, - UpdateStateHandler + CrudEntityCreationContext } import io.cloudstate.javasupport.impl.{AnySupport, ResolvedServiceMethod, ResolvedType} import io.cloudstate.javasupport.{Context, EntityId, ServiceCall, ServiceCallFactory, ServiceCallRef} import org.scalatest.{Matchers, WordSpec} +import scala.compat.java8.OptionConverters._ class AnnotationBasedCrudSupportSpec extends WordSpec with Matchers { trait BaseContext extends Context { @@ -46,11 +46,12 @@ class AnnotationBasedCrudSupportSpec extends WordSpec with Matchers { } class MockCommandContext extends CommandContext[JavaPbAny] with BaseContext { - var action: Option[AnyRef] = None + var action: Option[JavaPbAny] = None override def commandName(): String = "AddItem" override def commandId(): Long = 20 - override def updateEntity(state: JavaPbAny): Unit = action = Some(state) - override def deleteEntity(): Unit = action = None + override def getState(): Optional[JavaPbAny] = action.asJava + override def updateState(state: JavaPbAny): Unit = action = Some(state) + override def deleteState(): Unit = action = None override def entityId(): String = "foo" override def fail(errorMessage: String): RuntimeException = ??? override def forward(to: ServiceCall): Unit = ??? @@ -156,12 +157,29 @@ class AnnotationBasedCrudSupportSpec extends WordSpec with Matchers { decodeWrapped(handler.handleCommand(command("blah"), new MockCommandContext).get) should ===(Wrapped("blah")) } - "allow updating the state" in { + "reading the state" in { val handler = create( new { @CommandHandler def addItem(msg: String, ctx: CommandContext[JavaPbAny]): Wrapped = { - ctx.updateEntity(state(msg + " state")) + ctx.updateState(state(msg + " state")) + ctx.commandName() should ===("AddItem") + Wrapped(msg) + } + }, + method + ) + val ctx = new MockCommandContext + decodeWrapped(handler.handleCommand(command("blah"), ctx).get) should ===(Wrapped("blah")) + ctx.getState().get should ===(state("blah state")) + } + + "updating the state" in { + val handler = create( + new { + @CommandHandler + def addItem(msg: String, ctx: CommandContext[JavaPbAny]): Wrapped = { + ctx.updateState(state(msg + " state")) ctx.commandName() should ===("AddItem") Wrapped(msg) } @@ -176,7 +194,7 @@ class AnnotationBasedCrudSupportSpec extends WordSpec with Matchers { "fail if there's a bad context type" in { a[RuntimeException] should be thrownBy create(new { @CommandHandler - def addItem(msg: String, ctx: StateContext) = + def addItem(msg: String, ctx: BaseContext) = Wrapped(msg) }, method) } @@ -200,16 +218,6 @@ class AnnotationBasedCrudSupportSpec extends WordSpec with Matchers { }, method) } - "fail if there's a CRDT command handler" in { - val ex = the[RuntimeException] thrownBy create(new { - @io.cloudstate.javasupport.crdt.CommandHandler - def addItem(msg: String) = - Wrapped(msg) - }, method) - ex.getMessage should include("Did you mean") - ex.getMessage should include(classOf[CommandHandler].getName) - } - "unwrap exceptions" in { val handler = create(new { @CommandHandler @@ -219,122 +227,28 @@ class AnnotationBasedCrudSupportSpec extends WordSpec with Matchers { ex.getMessage should ===("foo") } - } - - "support update state handlers" when { - val ctx = new StateContext with BaseContext { - override def entityId(): String = "foo" - } - - "single parameter" in { - var invoked = false - val handler = create(new { - @UpdateStateHandler - def updateState(state: String): Unit = { - state should ===("state!") - invoked = true - } - }) - handler.handleUpdate(state("state!"), ctx) - invoked shouldBe true - } - - "context parameter" in { - var invoked = false - val handler = create(new { - @UpdateStateHandler - def updateState(state: String, context: StateContext): Unit = { - state should ===("state!") - invoked = true - } - }) - handler.handleUpdate(state("state!"), ctx) - invoked shouldBe true - } - - "fail if there's a bad context" in { - a[RuntimeException] should be thrownBy create(new { - @UpdateStateHandler - def updateState(state: String, context: CommandContext[JavaPbAny]): Unit = () - }) - } - - "fail if there's no state parameter" in { - a[RuntimeException] should be thrownBy create(new { - @UpdateStateHandler - def updateState(context: StateContext): Unit = () - }) + "fail if there's a CRDT command handler" in { + val ex = the[RuntimeException] thrownBy create(new { + @io.cloudstate.javasupport.crdt.CommandHandler + def addItem(msg: String) = + Wrapped(msg) + }, method) + ex.getMessage should include("Did you mean") + ex.getMessage should include(classOf[CommandHandler].getName) } - "fail if there's no update handler for the given type" in { - val handler = create(new { - @UpdateStateHandler - def updateState(state: Int): Unit = () - }) - a[RuntimeException] should be thrownBy handler.handleUpdate(state(10), ctx) + "fail if there's a EventSourced command handler" in { + val ex = the[RuntimeException] thrownBy create(new { + @io.cloudstate.javasupport.eventsourced.CommandHandler + def addItem(msg: String) = + Wrapped(msg) + }, method) + ex.getMessage should include("Did you mean") + ex.getMessage should include(classOf[CommandHandler].getName) } - "fail if there are two update handler methods" in { - a[RuntimeException] should be thrownBy create(new { - @UpdateStateHandler - def updateState1(context: StateContext): Unit = () - @UpdateStateHandler - def updateState2(context: StateContext): Unit = () - }) - } } - "support delete state handlers" when { - val ctx = new StateContext with BaseContext { - override def entityId(): String = "foo" - } - - "no arg parameter" in { - var invoked = false - val handler = create(new { - @DeleteStateHandler - def deleteState(): Unit = - invoked = true - }) - handler.handleDelete(ctx) - invoked shouldBe true - } - - "context parameter" in { - var invoked = false - val handler = create(new { - @DeleteStateHandler - def deleteState(context: StateContext): Unit = - invoked = true - }) - handler.handleDelete(ctx) - invoked shouldBe true - } - - "fail if there's a single argument is not the context" in { - a[RuntimeException] should be thrownBy create(new { - @DeleteStateHandler - def deleteState(state: String): Unit = () - }) - } - - "fail if there's two delete methods" in { - a[RuntimeException] should be thrownBy create(new { - @DeleteStateHandler - def deleteState1: Unit = () - - @DeleteStateHandler - def deleteState2: Unit = () - }) - } - - "fail if there's a bad context" in { - a[RuntimeException] should be thrownBy create(new { - @DeleteStateHandler - def deleteState(context: CommandContext[JavaPbAny]): Unit = () - }) - } - } } } diff --git a/proxy/core/src/main/scala/io/cloudstate/proxy/crud/CrudEntity.scala b/proxy/core/src/main/scala/io/cloudstate/proxy/crud/CrudEntity.scala index c6a2d2ef1..f2bb3007d 100644 --- a/proxy/core/src/main/scala/io/cloudstate/proxy/crud/CrudEntity.scala +++ b/proxy/core/src/main/scala/io/cloudstate/proxy/crud/CrudEntity.scala @@ -166,7 +166,10 @@ object CrudEntity { replyTo: ActorRef ) - private case object DatabaseOperationSuccess + private case object LoadInitStateSuccess + private case class LoadInitStateFailure(cause: Throwable) + + private case object SaveStateSuccess final def props(configuration: Configuration, entityId: String, @@ -190,6 +193,7 @@ final class CrudEntity(configuration: CrudEntity.Configuration, statsCollector: ActorRef, repository: JdbcRepository) extends Actor + with Stash with ActorLogging { private implicit val ec = context.dispatcher @@ -198,7 +202,6 @@ final class CrudEntity(configuration: CrudEntity.Configuration, private val actorId = CrudEntity.actorCounter.incrementAndGet() - private[this] final var initState: Option[pbAny] = None private[this] final var stashedCommands = Queue.empty[(EntityCommand, ActorRef)] // PERFORMANCE: look at options for data structures private[this] final var currentCommand: CrudEntity.OutstandingCommand = null private[this] final var stopped = false @@ -218,8 +221,24 @@ final class CrudEntity(configuration: CrudEntity.Configuration, repository .get(Key(persistenceId, entityId)) .map { state => - handleInitState(state) - CrudEntity.DatabaseOperationSuccess + // related to the first access to the database when the actor starts + reportDatabaseOperationFinished() + if (!inited) { + relay ! CrudStreamIn( + CrudStreamIn.Message.Init( + CrudInit( + serviceName = configuration.serviceName, + entityId = entityId, + state = Some(CrudInitState(state)) + ) + ) + ) + inited = true + } + CrudEntity.LoadInitStateSuccess + } + .recover { + case error => CrudEntity.LoadInitStateFailure(error) } .pipeTo(self) @@ -289,7 +308,21 @@ final class CrudEntity(configuration: CrudEntity.Configuration, clientAction = Some(ClientAction(ClientAction.Action.Failure(Failure(description = message)))) ) - override final def receive: PartialFunction[Any, Unit] = { + override final def receive: PartialFunction[Any, Unit] = waitingForInitState + + private def waitingForInitState: PartialFunction[Any, Unit] = { + case CrudEntity.LoadInitStateSuccess => + context.become(initialized) + unstashAll() + + case CrudEntity.LoadInitStateFailure(error) => + log.error(error, s"CRUD Entity cannot load the initial state due to unexpected failure ${error.getMessage}") + throw error + + case _ => stash() + } + + private def initialized: PartialFunction[Any, Unit] = { case command: EntityCommand if currentCommand != null => stashedCommands = stashedCommands.enqueue((command, sender())) @@ -325,7 +358,7 @@ final class CrudEntity(configuration: CrudEntity.Configuration, } currentCommand.replyTo ! esReplyToUfReply(r) commandHandled() - CrudEntity.DatabaseOperationSuccess + CrudEntity.SaveStateSuccess } .pipeTo(self) } @@ -359,7 +392,7 @@ final class CrudEntity(configuration: CrudEntity.Configuration, notifyOutstandingRequests("Unexpected CRUD entity termination") throw error - case CrudEntity.DatabaseOperationSuccess => + case CrudEntity.SaveStateSuccess => // Nothing to do, access the native crud database was successful case ReceiveTimeout => @@ -381,29 +414,6 @@ final class CrudEntity(configuration: CrudEntity.Configuration, repository.delete(Key(persistenceId, entityId)) } - private[this] final def handleInitState(state: Option[pbAny]): Unit = { - // related to the first access to the database when the actor starts - reportDatabaseOperationFinished() - if (!inited) { - // apply the initial state only when the entity is not fully initialized - initState = state match { - case Some(updated: pbAny) => Some(updated) - case _ => None - } - - relay ! CrudStreamIn( - CrudStreamIn.Message.Init( - CrudInit( - serviceName = configuration.serviceName, - entityId = entityId, - state = Some(CrudInitState(initState)) - ) - ) - ) - inited = true - } - } - private def reportDatabaseOperationStarted(): Unit = if (reportedDatabaseOperationStarted) { log.warning("Already reported database operation started") diff --git a/samples/java-crud-shopping-cart/src/main/java/io/cloudstate/samples/shoppingcart/ShoppingCartEntity.java b/samples/java-crud-shopping-cart/src/main/java/io/cloudstate/samples/shoppingcart/ShoppingCartEntity.java index e401e2d5b..5fffdb444 100644 --- a/samples/java-crud-shopping-cart/src/main/java/io/cloudstate/samples/shoppingcart/ShoppingCartEntity.java +++ b/samples/java-crud-shopping-cart/src/main/java/io/cloudstate/samples/shoppingcart/ShoppingCartEntity.java @@ -23,79 +23,108 @@ import io.cloudstate.javasupport.crud.CommandContext; import io.cloudstate.javasupport.crud.CommandHandler; import io.cloudstate.javasupport.crud.CrudEntity; -import io.cloudstate.javasupport.crud.DeleteStateHandler; -import io.cloudstate.javasupport.crud.UpdateStateHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; -import java.util.LinkedHashMap; import java.util.List; -import java.util.Map; +import java.util.Optional; +import java.util.function.Predicate; import java.util.stream.Collectors; -import java.util.stream.Stream; /** A CRUD entity. */ @CrudEntity public class ShoppingCartEntity { + + private final Logger logger = LoggerFactory.getLogger(ShoppingCartEntity.class); private final String entityId; - private final Map cart = new LinkedHashMap<>(); public ShoppingCartEntity(@EntityId String entityId) { this.entityId = entityId; } - @UpdateStateHandler - public void handleUpdateState(Domain.Cart cart) { - this.cart.clear(); - for (Domain.LineItem item : cart.getItemsList()) { - this.cart.put(item.getProductId(), convert(item)); - } - } - - @DeleteStateHandler - public void handleDeleteState() { - this.cart.clear(); - } - @CommandHandler - public Shoppingcart.Cart getCart() { - return Shoppingcart.Cart.newBuilder().addAllItems(cart.values()).build(); + public Shoppingcart.Cart getCart(CommandContext ctx) { + logger.info("getCart called"); + ctx.getState() + .ifPresent( + c -> { + c.getItemsList() + .forEach( + lineItem -> + logger.info( + "getCart called cart line item name - " + + lineItem.getName() + + ", id - " + + lineItem.getProductId())); + }); + // access the state by calling ctx.getState() + Domain.Cart cart = ctx.getState().orElse(Domain.Cart.newBuilder().build()); + List allItems = + cart.getItemsList().stream().map(this::convert).collect(Collectors.toList()); + return Shoppingcart.Cart.newBuilder().addAllItems(allItems).build(); } @CommandHandler public Empty addItem(Shoppingcart.AddLineItem item, CommandContext ctx) { + logger.info( + "addItem called cart AddLineItem name - " + + item.getName() + + ", id - " + + item.getProductId()); if (item.getQuantity() <= 0) { ctx.fail("Cannot add negative quantity of to item " + item.getProductId()); } - Shoppingcart.LineItem lineItem = cart.get(item.getProductId()); - if (lineItem == null) { - lineItem = - Shoppingcart.LineItem.newBuilder() - .setProductId(item.getProductId()) - .setName(item.getName()) - .setQuantity(item.getQuantity()) - .build(); - } else { - lineItem = - lineItem.toBuilder().setQuantity(lineItem.getQuantity() + item.getQuantity()).build(); - } - - ctx.updateEntity(Domain.Cart.newBuilder().addAllItems(addItem(item, lineItem)).build()); + // access the state by calling ctx.getState() + Domain.Cart cart = ctx.getState().orElse(Domain.Cart.newBuilder().build()); + ctx.getState() + .ifPresent( + c -> { + c.getItemsList() + .forEach( + lineItem -> + logger.info( + "addItem called cart line item name - " + + lineItem.getName() + + ", id - " + + lineItem.getProductId())); + }); + logger.info("addItem called lineItemStream"); + Domain.LineItem lineItem = updateItem(item, cart); + + logger.info( + "addItem called lineItem name - " + + lineItem.getName() + + " id - " + + lineItem.getProductId() + + " quantity - " + + lineItem.getQuantity()); + List lineItems = removeItemByProductId(cart, item.getProductId()); + + logger.info("addItem called updateEntity"); + // update the state by calling ctx.updateState(...) + // multiple invocations of ctx.updateState(...) and ctx.deleteState() are not allowed + ctx.updateState(Domain.Cart.newBuilder().addAllItems(lineItems).addItems(lineItem).build()); + ctx.updateState(Domain.Cart.newBuilder().addAllItems(lineItems).addItems(lineItem).build()); + logger.info("addItem called after updateEntity"); return Empty.getDefaultInstance(); } @CommandHandler public Empty removeItem(Shoppingcart.RemoveLineItem item, CommandContext ctx) { - if (!cart.containsKey(item.getProductId())) { + // access the state by calling ctx.getState() + Domain.Cart cart = ctx.getState().orElse(Domain.Cart.newBuilder().build()); + Optional lineItem = findItemByProductId(cart, item.getProductId()); + + if (!lineItem.isPresent()) { ctx.fail("Cannot remove item " + item.getProductId() + " because it is not in the cart."); } - List lineItems = - cart.values().stream() - .filter(lineItem -> !lineItem.getProductId().equals(item.getProductId())) - .map(this::convert) - .collect(Collectors.toList()); + List items = removeItemByProductId(cart, item.getProductId()); - ctx.updateEntity(Domain.Cart.newBuilder().addAllItems(lineItems).build()); + // update the state by calling ctx.updateState(...) + // multiple invocations of ctx.updateState(...) and ctx.deleteState() are not allowed + ctx.updateState(Domain.Cart.newBuilder().addAllItems(items).build()); return Empty.getDefaultInstance(); } @@ -106,16 +135,35 @@ public Empty removeCart( ctx.fail("Cannot remove unknown cart " + cartItem.getUserId()); } - ctx.deleteEntity(); + // delete the state by calling ctx.deleteState() + // multiple invocations of ctx.updateState(...) and ctx.deleteState() are not allowed + ctx.deleteState(); return Empty.getDefaultInstance(); } - private List addItem( - Shoppingcart.AddLineItem addItem, Shoppingcart.LineItem lineItem) { - Stream stream = - cart.values().stream().filter(li -> !li.getProductId().equals(addItem.getProductId())); - return Stream.concat(stream, Stream.of(lineItem)) - .map(this::convert) + private Domain.LineItem updateItem(Shoppingcart.AddLineItem item, Domain.Cart cart) { + return findItemByProductId(cart, item.getProductId()) + .map(li -> li.toBuilder().setQuantity(li.getQuantity() + item.getQuantity()).build()) + .orElse(newItem(item)); + } + + private Domain.LineItem newItem(Shoppingcart.AddLineItem item) { + return Domain.LineItem.newBuilder() + .setProductId(item.getProductId()) + .setName(item.getName()) + .setQuantity(item.getQuantity()) + .build(); + } + + private Optional findItemByProductId(Domain.Cart cart, String productId) { + Predicate lineItemExists = + lineItem -> lineItem.getProductId().equals(productId); + return cart.getItemsList().stream().filter(lineItemExists).findFirst(); + } + + private List removeItemByProductId(Domain.Cart cart, String productId) { + return cart.getItemsList().stream() + .filter(lineItem -> !lineItem.getProductId().equals(productId)) .collect(Collectors.toList()); } From 2731570c74b3a1a4dfdf66254ba856871a127f16 Mon Sep 17 00:00:00 2001 From: Guy Youansi Date: Tue, 15 Sep 2020 18:01:24 +0200 Subject: [PATCH 39/93] change entity initialization --- .../src/main/scala/io/cloudstate/proxy/crud/CrudEntity.scala | 3 +++ 1 file changed, 3 insertions(+) diff --git a/proxy/core/src/main/scala/io/cloudstate/proxy/crud/CrudEntity.scala b/proxy/core/src/main/scala/io/cloudstate/proxy/crud/CrudEntity.scala index f2bb3007d..10d07ab5b 100644 --- a/proxy/core/src/main/scala/io/cloudstate/proxy/crud/CrudEntity.scala +++ b/proxy/core/src/main/scala/io/cloudstate/proxy/crud/CrudEntity.scala @@ -311,6 +311,9 @@ final class CrudEntity(configuration: CrudEntity.Configuration, override final def receive: PartialFunction[Any, Unit] = waitingForInitState private def waitingForInitState: PartialFunction[Any, Unit] = { + case CrudEntity.LoadInitStateSuccess if inited == true => + // ignore entity already initialized + case CrudEntity.LoadInitStateSuccess => context.become(initialized) unstashAll() From 46b54fc34a3051d4839d1dfbe63fde18f0015365 Mon Sep 17 00:00:00 2001 From: Guy Youansi Date: Tue, 15 Sep 2020 19:27:01 +0200 Subject: [PATCH 40/93] change entity initialization and adapt annotation based support test --- .../crud/AnnotationBasedCrudSupportSpec.scala | 71 ++++++++++++------- .../io/cloudstate/proxy/crud/CrudEntity.scala | 2 +- 2 files changed, 45 insertions(+), 28 deletions(-) diff --git a/java-support/src/test/scala/io/cloudstate/javasupport/impl/crud/AnnotationBasedCrudSupportSpec.scala b/java-support/src/test/scala/io/cloudstate/javasupport/impl/crud/AnnotationBasedCrudSupportSpec.scala index d627797a3..96f738fe1 100644 --- a/java-support/src/test/scala/io/cloudstate/javasupport/impl/crud/AnnotationBasedCrudSupportSpec.scala +++ b/java-support/src/test/scala/io/cloudstate/javasupport/impl/crud/AnnotationBasedCrudSupportSpec.scala @@ -45,13 +45,14 @@ class AnnotationBasedCrudSupportSpec extends WordSpec with Matchers { override def entityId(): String = "foo" } - class MockCommandContext extends CommandContext[JavaPbAny] with BaseContext { - var action: Option[JavaPbAny] = None - override def commandName(): String = "AddItem" + class MockCommandContext(override val commandName: String = "AddItem", state: Option[JavaPbAny] = None) + extends CommandContext[JavaPbAny] + with BaseContext { + var currentState: Option[JavaPbAny] = state override def commandId(): Long = 20 - override def getState(): Optional[JavaPbAny] = action.asJava - override def updateState(state: JavaPbAny): Unit = action = Some(state) - override def deleteState(): Unit = action = None + override def getState(): Optional[JavaPbAny] = currentState.asJava + override def updateState(newState: JavaPbAny): Unit = currentState = Some(newState) + override def deleteState(): Unit = currentState = None override def entityId(): String = "foo" override def fail(errorMessage: String): RuntimeException = ??? override def forward(to: ServiceCall): Unit = ??? @@ -75,10 +76,10 @@ class AnnotationBasedCrudSupportSpec extends WordSpec with Matchers { case class Wrapped(value: String) val anySupport = new AnySupport(Array(Shoppingcart.getDescriptor), this.getClass.getClassLoader) - val descriptor = Shoppingcart.getDescriptor - .findServiceByName("ShoppingCart") - .findMethodByName("AddItem") - val method = ResolvedServiceMethod(descriptor, StringResolvedType, WrappedResolvedType) + val serviceDescriptor = Shoppingcart.getDescriptor.findServiceByName("ShoppingCart") + + def method(name: String = "AddItem") = + ResolvedServiceMethod(serviceDescriptor.findMethodByName(name), StringResolvedType, WrappedResolvedType) def create(behavior: AnyRef, methods: ResolvedServiceMethod[_, _]*) = new AnnotationBasedCrudSupport(behavior.getClass, @@ -130,7 +131,7 @@ class AnnotationBasedCrudSupportSpec extends WordSpec with Matchers { val handler = create(new { @CommandHandler def addItem() = Wrapped("blah") - }, method) + }, method()) decodeWrapped(handler.handleCommand(command("nothing"), new MockCommandContext).get) should ===(Wrapped("blah")) } @@ -138,7 +139,7 @@ class AnnotationBasedCrudSupportSpec extends WordSpec with Matchers { val handler = create(new { @CommandHandler def addItem(msg: String) = Wrapped(msg) - }, method) + }, method()) decodeWrapped(handler.handleCommand(command("blah"), new MockCommandContext).get) should ===(Wrapped("blah")) } @@ -152,7 +153,7 @@ class AnnotationBasedCrudSupportSpec extends WordSpec with Matchers { Wrapped(msg) } }, - method + method() ) decodeWrapped(handler.handleCommand(command("blah"), new MockCommandContext).get) should ===(Wrapped("blah")) } @@ -161,17 +162,16 @@ class AnnotationBasedCrudSupportSpec extends WordSpec with Matchers { val handler = create( new { @CommandHandler - def addItem(msg: String, ctx: CommandContext[JavaPbAny]): Wrapped = { - ctx.updateState(state(msg + " state")) - ctx.commandName() should ===("AddItem") + def getCart(msg: String, ctx: CommandContext[JavaPbAny]): Wrapped = { + ctx.getState().asScala.get.asInstanceOf[String] should ===("state") + ctx.commandName() should ===("GetCart") Wrapped(msg) } }, - method + method("GetCart") ) - val ctx = new MockCommandContext + val ctx = new MockCommandContext("GetCart", Some(state("state"))) decodeWrapped(handler.handleCommand(command("blah"), ctx).get) should ===(Wrapped("blah")) - ctx.getState().get should ===(state("blah state")) } "updating the state" in { @@ -184,11 +184,28 @@ class AnnotationBasedCrudSupportSpec extends WordSpec with Matchers { Wrapped(msg) } }, - method + method() ) val ctx = new MockCommandContext decodeWrapped(handler.handleCommand(command("blah"), ctx).get) should ===(Wrapped("blah")) - ctx.action.get should ===(state("blah state")) + ctx.currentState.get should ===(state("blah state")) + } + + "deleting the state" in { + val handler = create( + new { + @CommandHandler + def removeCart(msg: String, ctx: CommandContext[JavaPbAny]): Wrapped = { + ctx.deleteState() + ctx.commandName() should ===("RemoveCart") + Wrapped(msg) + } + }, + method("RemoveCart") + ) + val ctx = new MockCommandContext("RemoveCart") + decodeWrapped(handler.handleCommand(command("blah"), ctx).get) should ===(Wrapped("blah")) + ctx.currentState should ===(None) } "fail if there's a bad context type" in { @@ -196,7 +213,7 @@ class AnnotationBasedCrudSupportSpec extends WordSpec with Matchers { @CommandHandler def addItem(msg: String, ctx: BaseContext) = Wrapped(msg) - }, method) + }, method()) } "fail if there's two command handlers for the same command" in { @@ -207,7 +224,7 @@ class AnnotationBasedCrudSupportSpec extends WordSpec with Matchers { @CommandHandler def addItem(msg: String) = Wrapped(msg) - }, method) + }, method()) } "fail if there's no command with that name" in { @@ -215,14 +232,14 @@ class AnnotationBasedCrudSupportSpec extends WordSpec with Matchers { @CommandHandler def wrongName(msg: String) = Wrapped(msg) - }, method) + }, method()) } "unwrap exceptions" in { val handler = create(new { @CommandHandler def addItem(): Wrapped = throw new RuntimeException("foo") - }, method) + }, method()) val ex = the[RuntimeException] thrownBy handler.handleCommand(command("nothing"), new MockCommandContext) ex.getMessage should ===("foo") } @@ -232,7 +249,7 @@ class AnnotationBasedCrudSupportSpec extends WordSpec with Matchers { @io.cloudstate.javasupport.crdt.CommandHandler def addItem(msg: String) = Wrapped(msg) - }, method) + }, method()) ex.getMessage should include("Did you mean") ex.getMessage should include(classOf[CommandHandler].getName) } @@ -242,7 +259,7 @@ class AnnotationBasedCrudSupportSpec extends WordSpec with Matchers { @io.cloudstate.javasupport.eventsourced.CommandHandler def addItem(msg: String) = Wrapped(msg) - }, method) + }, method()) ex.getMessage should include("Did you mean") ex.getMessage should include(classOf[CommandHandler].getName) } diff --git a/proxy/core/src/main/scala/io/cloudstate/proxy/crud/CrudEntity.scala b/proxy/core/src/main/scala/io/cloudstate/proxy/crud/CrudEntity.scala index 10d07ab5b..f97e3bdcb 100644 --- a/proxy/core/src/main/scala/io/cloudstate/proxy/crud/CrudEntity.scala +++ b/proxy/core/src/main/scala/io/cloudstate/proxy/crud/CrudEntity.scala @@ -312,7 +312,7 @@ final class CrudEntity(configuration: CrudEntity.Configuration, private def waitingForInitState: PartialFunction[Any, Unit] = { case CrudEntity.LoadInitStateSuccess if inited == true => - // ignore entity already initialized + // ignore entity already initialized case CrudEntity.LoadInitStateSuccess => context.become(initialized) From 7ec4ab544218b64ace03cb7767b947c292a2c78d Mon Sep 17 00:00:00 2001 From: Guy Youansi Date: Tue, 15 Sep 2020 20:15:11 +0200 Subject: [PATCH 41/93] change test name --- .../impl/crud/AnnotationBasedCrudSupportSpec.scala | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/java-support/src/test/scala/io/cloudstate/javasupport/impl/crud/AnnotationBasedCrudSupportSpec.scala b/java-support/src/test/scala/io/cloudstate/javasupport/impl/crud/AnnotationBasedCrudSupportSpec.scala index 96f738fe1..74fc863c3 100644 --- a/java-support/src/test/scala/io/cloudstate/javasupport/impl/crud/AnnotationBasedCrudSupportSpec.scala +++ b/java-support/src/test/scala/io/cloudstate/javasupport/impl/crud/AnnotationBasedCrudSupportSpec.scala @@ -158,7 +158,7 @@ class AnnotationBasedCrudSupportSpec extends WordSpec with Matchers { decodeWrapped(handler.handleCommand(command("blah"), new MockCommandContext).get) should ===(Wrapped("blah")) } - "reading the state" in { + "read state" in { val handler = create( new { @CommandHandler @@ -174,7 +174,7 @@ class AnnotationBasedCrudSupportSpec extends WordSpec with Matchers { decodeWrapped(handler.handleCommand(command("blah"), ctx).get) should ===(Wrapped("blah")) } - "updating the state" in { + "update state" in { val handler = create( new { @CommandHandler @@ -191,7 +191,7 @@ class AnnotationBasedCrudSupportSpec extends WordSpec with Matchers { ctx.currentState.get should ===(state("blah state")) } - "deleting the state" in { + "delete state" in { val handler = create( new { @CommandHandler From d076e8604175a6c39d206e9a858dec4efd1b06b3 Mon Sep 17 00:00:00 2001 From: Guy Youansi Date: Tue, 15 Sep 2020 20:22:00 +0200 Subject: [PATCH 42/93] ignore CRUD entity test --- .../test/scala/io/cloudstate/proxy/crud/CrudEntitySpec.scala | 2 ++ 1 file changed, 2 insertions(+) diff --git a/proxy/core/src/test/scala/io/cloudstate/proxy/crud/CrudEntitySpec.scala b/proxy/core/src/test/scala/io/cloudstate/proxy/crud/CrudEntitySpec.scala index 4841ec235..9b26c6955 100644 --- a/proxy/core/src/test/scala/io/cloudstate/proxy/crud/CrudEntitySpec.scala +++ b/proxy/core/src/test/scala/io/cloudstate/proxy/crud/CrudEntitySpec.scala @@ -21,9 +21,11 @@ import io.cloudstate.protocol.crud._ import io.cloudstate.protocol.entity.{ClientAction, Failure} import io.cloudstate.proxy.crud.CrudEntity.{Stop, StreamClosed, StreamFailed} import io.cloudstate.proxy.entity.{EntityCommand, UserFunctionReply} +import org.scalatest.Ignore import scala.concurrent.duration._ +@Ignore class CrudEntitySpec extends AbstractCrudEntitySpec { import AbstractCrudEntitySpec._ From b05f7aafacf512ca1ba9329be98c0927a7d8dff0 Mon Sep 17 00:00:00 2001 From: Guy Youansi Date: Wed, 16 Sep 2020 12:19:28 +0200 Subject: [PATCH 43/93] remove subsequent invocation checking which is not needed --- .../javasupport/crud/CommandContext.java | 2 +- .../crud/CrudActionInvocationChecker.scala | 119 ------------------ .../javasupport/impl/crud/CrudImpl.scala | 28 ++--- 3 files changed, 8 insertions(+), 141 deletions(-) delete mode 100644 java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/CrudActionInvocationChecker.scala diff --git a/java-support/src/main/java/io/cloudstate/javasupport/crud/CommandContext.java b/java-support/src/main/java/io/cloudstate/javasupport/crud/CommandContext.java index 146885a93..56066e040 100644 --- a/java-support/src/main/java/io/cloudstate/javasupport/crud/CommandContext.java +++ b/java-support/src/main/java/io/cloudstate/javasupport/crud/CommandContext.java @@ -51,7 +51,7 @@ public interface CommandContext extends CrudContext, ClientActionContext, Eff * @throws IllegalStateException If the current entity state have been deleted in the command * invocation. */ - Optional getState() throws IllegalStateException; + Optional getState(); /** * Update the entity with the new state. The state will be persisted. diff --git a/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/CrudActionInvocationChecker.scala b/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/CrudActionInvocationChecker.scala deleted file mode 100644 index d19267bb8..000000000 --- a/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/CrudActionInvocationChecker.scala +++ /dev/null @@ -1,119 +0,0 @@ -/* - * Copyright 2019 Lightbend Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.cloudstate.javasupport.impl.crud - -import io.cloudstate.javasupport.impl.crud.CrudImpl.EntityException -import io.cloudstate.protocol.crud.CrudAction.Action.{Delete, Update} -import io.cloudstate.protocol.crud.{CrudAction, CrudDelete, CrudUpdate} - -private[impl] object CrudActionInvocationChecker { - private val updateActionName = "updateState" - private val deleteActionName = "deleteState" - - case class CrudActionInvocationContext(entityId: String, commandId: Long, commandName: String) -} - -private[impl] trait CrudActionInvocationChecker { - import CrudActionInvocationChecker._ - - def action: Option[CrudAction] - def stateDeleted: Boolean - - /** - * It should not be possible to call getState when deleteState have been called. - *
    - *
  • - * - * ctx.deleteState(); - * ctx.getState(); - * - *
  • - *
- */ - def checkStateDeleted(ctx: CrudActionInvocationContext): Unit = - if (stateDeleted) { - throw new IllegalStateException( - s"The CRUD entity [${ctx.entityId}] does not exist and have been deleted with deleteState for command [${ctx.commandName}]" - ) - } - - /** - * It should not be possible to call some combinations of getState, updateState and deleteState because we don't - * what the intention of the caller is. - * Some examples of invocations that will failed: - *
    - *
  • - * - * ctx.updateState(…); - * ctx.updateState(…); - * - *
  • - *
  • - * - * ctx.deleteState(); - * ctx.updateState(…); - * - *
  • - *
  • - * - * ctx.updateState(…); - * ctx.deleteState(); - * - *
  • - * - * ctx.deleteState(); - * ctx.deleteState(); - * - *
  • - *
- */ - def checkInvocation(ctx: CrudActionInvocationContext, _action: CrudAction): Unit = - action.map { a => - a.action match { - case Update(_) => - throw new EntityException(ctx.entityId, - ctx.commandId, - ctx.commandName, - invocationFailureMessage(ctx, updateActionName, _action)) - - case Delete(_) => - throw new EntityException(ctx.entityId, - ctx.commandId, - ctx.commandName, - invocationFailureMessage(ctx, deleteActionName, _action)) - - case _ => - } - } - - private def invocationFailureMessage(ctx: CrudActionInvocationContext, - firstActionName: String, - _action: CrudAction): String = { - val name = actionName(_action) - //s"CRUD entity [$entityId] command [$commandName] cannot run multiples actions ['$firstActionName', '$name']. Please choose only one action between ['updateState', 'deleteState'] to run" - s"""|CRUD entity [${ctx.entityId}] command [${ctx.commandName}] cannot run multiples actions ['$firstActionName', '$name']. - | Please choose only one action between ['${updateActionName}', '${deleteActionName}'] to run""".stripMargin - .replaceAll("\n", " ") - } - - private def actionName(crudAction: CrudAction): String = - crudAction.action match { - case Update(CrudUpdate(_, _)) => updateActionName - case Delete(CrudDelete(_)) => deleteActionName - case _ => "" - } -} diff --git a/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/CrudImpl.scala b/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/CrudImpl.scala index ece06b727..6d6d13fba 100644 --- a/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/CrudImpl.scala +++ b/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/CrudImpl.scala @@ -225,52 +225,38 @@ final class CrudImpl(_system: ActorSystem, with AbstractContext with AbstractClientActionContext with AbstractEffectContext - with ActivatableContext - with CrudActionInvocationChecker { + with ActivatableContext { - final var stateDeleted = false final var action: Option[CrudAction] = None - private var _currentState: Option[ScalaPbAny] = state + private var _state: Option[ScalaPbAny] = state override def getState(): Optional[JavaPbAny] = { checkActive() - checkStateDeleted(CrudActionInvocationChecker.CrudActionInvocationContext(entityId, commandId, commandName)) - - _currentState.map(ScalaPbAny.toJavaProto(_)).asJava + _state.map(ScalaPbAny.toJavaProto(_)).asJava } override def updateState(state: JavaPbAny): Unit = { checkActive() if (state == null) throw EntityException("CRUD entity cannot update a 'null' state") - checkInvocation( - CrudActionInvocationChecker.CrudActionInvocationContext(entityId, commandId, commandName), - CrudAction(Update(CrudUpdate(None))) - ) val encoded = anySupport.encodeScala(state) - _currentState = Some(encoded) - action = Some(CrudAction(Update(CrudUpdate(Some(encoded))))) - stateDeleted = false + _state = Some(encoded) + action = Some(CrudAction(Update(CrudUpdate(_state)))) } override def deleteState(): Unit = { checkActive() - checkInvocation( - CrudActionInvocationChecker.CrudActionInvocationContext(entityId, commandId, commandName), - CrudAction(Delete(CrudDelete())) - ) - _currentState = None + _state = None action = Some(CrudAction(Delete(CrudDelete()))) - stateDeleted = true } override protected def logError(message: String): Unit = log.error("Fail invoked for command [{}] for CRUD entity [{}]: {}", commandName, entityId, message) def currentState(): Option[ScalaPbAny] = - _currentState + _state } From 11135d7409eb232d84de9ca360f3f5e43f787a16 Mon Sep 17 00:00:00 2001 From: Guy Youansi Date: Thu, 17 Sep 2020 16:49:09 +0200 Subject: [PATCH 44/93] adapt the init case for the entity --- .../cloudstate/javasupport/impl/crud/CrudImpl.scala | 1 - .../scala/io/cloudstate/proxy/crud/CrudEntity.scala | 12 +++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/CrudImpl.scala b/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/CrudImpl.scala index 6d6d13fba..36ab66f8e 100644 --- a/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/CrudImpl.scala +++ b/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/CrudImpl.scala @@ -120,7 +120,6 @@ final class CrudImpl(_system: ActorSystem, ): akka.stream.scaladsl.Source[CrudStreamOut, akka.NotUsed] = in.prefixAndTail(1) .flatMapConcat { - // TODO: check!!! the InInit message not always comes first. it is maybe because of preStart in CrudEntity Actor!!! case (Seq(CrudStreamIn(InInit(init), _)), source) => source.via(runEntity(init)) case _ => diff --git a/proxy/core/src/main/scala/io/cloudstate/proxy/crud/CrudEntity.scala b/proxy/core/src/main/scala/io/cloudstate/proxy/crud/CrudEntity.scala index f97e3bdcb..6246e3128 100644 --- a/proxy/core/src/main/scala/io/cloudstate/proxy/crud/CrudEntity.scala +++ b/proxy/core/src/main/scala/io/cloudstate/proxy/crud/CrudEntity.scala @@ -168,8 +168,8 @@ object CrudEntity { private case object LoadInitStateSuccess private case class LoadInitStateFailure(cause: Throwable) - private case object SaveStateSuccess + private case object AlreadyInitialized final def props(configuration: Configuration, entityId: String, @@ -234,8 +234,10 @@ final class CrudEntity(configuration: CrudEntity.Configuration, ) ) inited = true + CrudEntity.LoadInitStateSuccess + } else { + CrudEntity.AlreadyInitialized } - CrudEntity.LoadInitStateSuccess } .recover { case error => CrudEntity.LoadInitStateFailure(error) @@ -311,13 +313,13 @@ final class CrudEntity(configuration: CrudEntity.Configuration, override final def receive: PartialFunction[Any, Unit] = waitingForInitState private def waitingForInitState: PartialFunction[Any, Unit] = { - case CrudEntity.LoadInitStateSuccess if inited == true => - // ignore entity already initialized - case CrudEntity.LoadInitStateSuccess => context.become(initialized) unstashAll() + case CrudEntity.AlreadyInitialized => + // ignore entity already initialized + case CrudEntity.LoadInitStateFailure(error) => log.error(error, s"CRUD Entity cannot load the initial state due to unexpected failure ${error.getMessage}") throw error From cb282ad7280fa1abccb4371e626b44ba352d14dc Mon Sep 17 00:00:00 2001 From: Guy Youansi Date: Thu, 17 Sep 2020 16:49:35 +0200 Subject: [PATCH 45/93] deal with empty schema in the config --- .../scala/io/cloudstate/proxy/crud/store/JdbcConfig.scala | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/proxy/core/src/main/scala/io/cloudstate/proxy/crud/store/JdbcConfig.scala b/proxy/core/src/main/scala/io/cloudstate/proxy/crud/store/JdbcConfig.scala index 08bdee9ac..d092319b4 100644 --- a/proxy/core/src/main/scala/io/cloudstate/proxy/crud/store/JdbcConfig.scala +++ b/proxy/core/src/main/scala/io/cloudstate/proxy/crud/store/JdbcConfig.scala @@ -35,8 +35,10 @@ class JdbcCrudStateTableConfiguration(config: Config) { private val cfg = config.getConfig("tables.state") val tableName: String = cfg.getString("tableName") - //TODO: handle the empty schemaName!!! - val schemaName: Option[String] = Option(cfg.getString("schemaName")).map(_.trim) + val schemaName: Option[String] = Option(cfg.getString("schemaName")).flatMap { + case schema if schema.trim.isEmpty => None + case nonEmptySchema => Some(nonEmptySchema.trim) + } val columnNames: JdbcCrudStateTableColumnNames = new JdbcCrudStateTableColumnNames(config) override def toString: String = s"JdbcCrudStateTableConfiguration($tableName,$schemaName,$columnNames)" From 863dfe3367e8e8c5b2812a363926a7eecfab4fa0 Mon Sep 17 00:00:00 2001 From: Guy Youansi Date: Fri, 18 Sep 2020 13:56:12 +0200 Subject: [PATCH 46/93] add comment for already initialized entity --- .../src/main/scala/io/cloudstate/proxy/crud/CrudEntity.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proxy/core/src/main/scala/io/cloudstate/proxy/crud/CrudEntity.scala b/proxy/core/src/main/scala/io/cloudstate/proxy/crud/CrudEntity.scala index 6246e3128..f56afab46 100644 --- a/proxy/core/src/main/scala/io/cloudstate/proxy/crud/CrudEntity.scala +++ b/proxy/core/src/main/scala/io/cloudstate/proxy/crud/CrudEntity.scala @@ -318,7 +318,7 @@ final class CrudEntity(configuration: CrudEntity.Configuration, unstashAll() case CrudEntity.AlreadyInitialized => - // ignore entity already initialized + // ignore entity already initialized case CrudEntity.LoadInitStateFailure(error) => log.error(error, s"CRUD Entity cannot load the initial state due to unexpected failure ${error.getMessage}") From e511b4bab100b8553e5e882c5d19848d86c1f5a6 Mon Sep 17 00:00:00 2001 From: Guy Youansi Date: Fri, 18 Sep 2020 15:17:51 +0200 Subject: [PATCH 47/93] integrate native crud support in postgres module --- build.sbt | 8 +- proxy/core/src/main/resources/reference.conf | 3 +- .../proxy/crud/store/JdbcConfig.scala | 6 +- .../jdbc/src/main/resources/jdbc-common.conf | 2 + .../proxy/jdbc/CloudStateJdbcProxyMain.scala | 1 + ...SlickEnsureCrudTablesExistReadyCheck.scala | 222 ++++++++++++++++++ .../src/main/resources/application.conf | 27 ++- 7 files changed, 259 insertions(+), 10 deletions(-) create mode 100644 proxy/jdbc/src/main/scala/io/cloudstate/proxy/jdbc/SlickEnsureCrudTablesExistReadyCheck.scala diff --git a/build.sbt b/build.sbt index b9ca977fb..793b4278a 100644 --- a/build.sbt +++ b/build.sbt @@ -52,6 +52,7 @@ val GraalVersion = "20.1.0" val DockerBaseImageVersion = "adoptopenjdk/openjdk11:debianslim-jre" val DockerBaseImageJavaLibraryPath = "${JAVA_HOME}/lib" val SlickVersion = "3.3.2" +val SlickHikariVersion = "3.3.2" val excludeTheseDependencies: Seq[ExclusionRule] = Seq( ExclusionRule("io.netty", "netty"), // grpc-java is using grpc-netty-shaded @@ -457,9 +458,8 @@ lazy val `proxy-core` = (project in file("proxy/core")) "io.prometheus" % "simpleclient_common" % PrometheusClientVersion, "org.slf4j" % "slf4j-simple" % Slf4jSimpleVersion, //"ch.qos.logback" % "logback-classic" % "1.2.3", // Doesn't work well with SubstrateVM: https://github.com/vmencik/akka-graal-native/blob/master/README.md#logging - "com.typesafe.slick" %% "slick" % SlickVersion, //TODO: not sure here!!! - "com.typesafe.slick" %% "slick-hikaricp" % SlickVersion, //TODO: not sure here!!! - //"org.postgresql" % "postgresql" % "42.2.6" + "com.typesafe.slick" %% "slick" % SlickVersion, + "com.typesafe.slick" %% "slick-hikaricp" % SlickHikariVersion, ), PB.protoSources in Compile ++= { val baseDir = (baseDirectory in ThisBuild).value / "protocols" @@ -530,6 +530,8 @@ lazy val `proxy-jdbc` = (project in file("proxy/jdbc")) name := "cloudstate-proxy-jdbc", dependencyOverrides += "io.grpc" % "grpc-netty-shaded" % GrpcNettyShadedVersion, libraryDependencies ++= Seq( + //"com.typesafe.slick" %% "slick" % SlickVersion, // should be here for CRUD native support!! + //"com.typesafe.slick" %% "slick-hikaricp" % SlickHikariVersion, // should be here for CRUD native support!! "com.github.dnvriend" %% "akka-persistence-jdbc" % "3.5.2" ), fork in run := true, diff --git a/proxy/core/src/main/resources/reference.conf b/proxy/core/src/main/resources/reference.conf index 821937399..63928e916 100644 --- a/proxy/core/src/main/resources/reference.conf +++ b/proxy/core/src/main/resources/reference.conf @@ -148,7 +148,6 @@ cloudstate.proxy { # This property indicates which configuration must be used by Slick. jdbc.database.slick { - # connectionPool = disabled connectionPool = "HikariCP" # This property indicates which profile must be used by Slick. @@ -180,7 +179,7 @@ cloudstate.proxy { tables { state { tableName = "crud_state_entity" - schemaName = "public" + schemaName = "" columnNames { persistentId = "persistent_id" entityId = "entity_id" diff --git a/proxy/core/src/main/scala/io/cloudstate/proxy/crud/store/JdbcConfig.scala b/proxy/core/src/main/scala/io/cloudstate/proxy/crud/store/JdbcConfig.scala index d092319b4..8d0f552c7 100644 --- a/proxy/core/src/main/scala/io/cloudstate/proxy/crud/store/JdbcConfig.scala +++ b/proxy/core/src/main/scala/io/cloudstate/proxy/crud/store/JdbcConfig.scala @@ -35,9 +35,9 @@ class JdbcCrudStateTableConfiguration(config: Config) { private val cfg = config.getConfig("tables.state") val tableName: String = cfg.getString("tableName") - val schemaName: Option[String] = Option(cfg.getString("schemaName")).flatMap { - case schema if schema.trim.isEmpty => None - case nonEmptySchema => Some(nonEmptySchema.trim) + val schemaName: Option[String] = cfg.getString("schemaName") match { + case "" => None + case schema => Some(schema.trim) } val columnNames: JdbcCrudStateTableColumnNames = new JdbcCrudStateTableColumnNames(config) diff --git a/proxy/jdbc/src/main/resources/jdbc-common.conf b/proxy/jdbc/src/main/resources/jdbc-common.conf index b64e29b4d..11a8ace9d 100644 --- a/proxy/jdbc/src/main/resources/jdbc-common.conf +++ b/proxy/jdbc/src/main/resources/jdbc-common.conf @@ -2,10 +2,12 @@ include "cloudstate-common" cloudstate.proxy { journal-enabled = true + crud-enabled = true } akka { management.health-checks.readiness-checks.cloudstate-jdbc = "io.cloudstate.proxy.jdbc.SlickEnsureTablesExistReadyCheck" + management.health-checks.readiness-checks.cloudstate-crud-jdbc = "io.cloudstate.proxy.jdbc.SlickEnsureCrudTablesExistReadyCheck" persistence { journal.plugin = "jdbc-journal" diff --git a/proxy/jdbc/src/main/scala/io/cloudstate/proxy/jdbc/CloudStateJdbcProxyMain.scala b/proxy/jdbc/src/main/scala/io/cloudstate/proxy/jdbc/CloudStateJdbcProxyMain.scala index 4e9b211af..11f2b2c1e 100644 --- a/proxy/jdbc/src/main/scala/io/cloudstate/proxy/jdbc/CloudStateJdbcProxyMain.scala +++ b/proxy/jdbc/src/main/scala/io/cloudstate/proxy/jdbc/CloudStateJdbcProxyMain.scala @@ -29,6 +29,7 @@ object CloudStateJdbcProxyMain { val config = new CloudStateProxyMain.Configuration(actorSystem.settings.config.getConfig("cloudstate.proxy")) if (config.devMode) { new SlickEnsureTablesExistReadyCheck(actorSystem) + new SlickEnsureCrudTablesExistReadyCheck(actorSystem) } } diff --git a/proxy/jdbc/src/main/scala/io/cloudstate/proxy/jdbc/SlickEnsureCrudTablesExistReadyCheck.scala b/proxy/jdbc/src/main/scala/io/cloudstate/proxy/jdbc/SlickEnsureCrudTablesExistReadyCheck.scala new file mode 100644 index 000000000..1fd1d3a37 --- /dev/null +++ b/proxy/jdbc/src/main/scala/io/cloudstate/proxy/jdbc/SlickEnsureCrudTablesExistReadyCheck.scala @@ -0,0 +1,222 @@ +/* + * Copyright 2019 Lightbend Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.cloudstate.proxy.jdbc + +import java.sql.Connection + +import akka.Done +import akka.actor.{Actor, ActorLogging, ActorSystem, Props, Status} +import akka.pattern.{BackoffOpts, BackoffSupervisor} +import akka.persistence.jdbc.config.{ConfigKeys, JournalTableConfiguration, SnapshotTableConfiguration} +import akka.persistence.jdbc.journal.dao.JournalTables +import akka.persistence.jdbc.snapshot.dao.SnapshotTables +import akka.persistence.jdbc.util.{SlickDatabase, SlickExtension} +import akka.util.Timeout +import com.typesafe.config.ConfigFactory +import io.cloudstate.proxy.crud.store.{JdbcCrudStateTable, JdbcCrudStateTableConfiguration, JdbcSlickDatabase} +import slick.jdbc.{H2Profile, JdbcProfile, MySQLProfile, PostgresProfile} +import slick.jdbc.meta.MTable + +import scala.collection.JavaConverters._ +import scala.concurrent.Future +import scala.concurrent.duration._ +import scala.util.{Failure, Success, Try} + +class SlickEnsureCrudTablesExistReadyCheck(system: ActorSystem) extends (() => Future[Boolean]) { + + private val crudConfig = system.settings.config.getConfig("cloudstate.proxy") + private val autoCreateTables = crudConfig.getBoolean("jdbc.auto-create-tables") + + private val check: () => Future[Boolean] = if (autoCreateTables) { + // Get a hold of the cloudstate-crud-jdbc slick database instance + val db = JdbcSlickDatabase(crudConfig) + + val actor = system.actorOf( + BackoffSupervisor.props( + BackoffOpts.onFailure( + childProps = Props(new EnsureCrudTablesExistsActor(db)), + childName = "crud-jdbc-table-creator", + minBackoff = 3.seconds, + maxBackoff = 30.seconds, + randomFactor = 0.2 + ) + ), + "crud-jdbc-table-creator-supervisor" + ) + + implicit val timeout = Timeout(10.seconds) // TODO make configurable? + import akka.pattern.ask + + () => (actor ? EnsureCrudTablesExistsActor.Ready).mapTo[Boolean] + } else { () => + Future.successful(true) + } + + override def apply(): Future[Boolean] = check() +} + +private object EnsureCrudTablesExistsActor { + + case object Ready + +} + +/** + * Copied/adapted from https://github.com/lagom/lagom/blob/60897ef752ddbfc28553d3726b8fdb830a3ebdc4/persistence-jdbc/core/src/main/scala/com/lightbend/lagom/internal/persistence/jdbc/SlickProvider.scala + */ +private class EnsureCrudTablesExistsActor(db: JdbcSlickDatabase) extends Actor with ActorLogging { + // TODO refactor this to be in sync with the event sourced one + + import EnsureCrudTablesExistsActor._ + + private val profile = db.profile + + import profile.api._ + + implicit val ec = context.dispatcher + + private val stateCfg = new JdbcCrudStateTableConfiguration( + context.system.settings.config.getConfig("cloudstate.proxy.crud.jdbc-state-store") + ) + + private val stateTable = new JdbcCrudStateTable { + override val crudStateTableCfg: JdbcCrudStateTableConfiguration = stateCfg + override val profile: JdbcProfile = EnsureCrudTablesExistsActor.this.profile + } + + private val stateStatements = stateTable.CrudStateTableQuery.schema.createStatements.toSeq + + import akka.pattern.pipe + + db.database.run { + for { + _ <- createTable(stateStatements, tableExists(stateCfg.schemaName, stateCfg.tableName)) + } yield Done.getInstance() + } pipeTo self + + override def receive: Receive = { + case Done => context become done + case Status.Failure(ex) => throw ex + case Ready => sender() ! false + } + + private def done: Receive = { + case Ready => sender() ! true + } + + private def createTable(schemaStatements: Seq[String], tableExists: (Vector[MTable], Option[String]) => Boolean) = + for { + currentSchema <- getCurrentSchema + tables <- getTables(currentSchema) + _ <- createTableInternal(tables, currentSchema, schemaStatements, tableExists) + } yield Done.getInstance() + + private def createTableInternal( + tables: Vector[MTable], + currentSchema: Option[String], + schemaStatements: Seq[String], + tableExists: (Vector[MTable], Option[String]) => Boolean + ) = + if (tableExists(tables, currentSchema)) { + DBIO.successful(()) + } else { + if (log.isDebugEnabled) { + log.debug("Creating table, executing: " + schemaStatements.mkString("; ")) + } + + DBIO + .sequence(schemaStatements.map { s => + SimpleDBIO { ctx => + val stmt = ctx.connection.createStatement() + try { + stmt.executeUpdate(s) + } finally { + stmt.close() + } + } + }) + .asTry + .flatMap { + case Success(_) => DBIO.successful(()) + case Failure(f) => + getTables(currentSchema).map { tables => + if (tableExists(tables, currentSchema)) { + log.debug("Table creation failed, but table existed after it was created, ignoring failure", f) + () + } else { + throw f + } + } + } + } + + private def getTables(currentSchema: Option[String]) = + // Calling MTable.getTables without parameters fails on MySQL + // See https://github.com/lagom/lagom/issues/446 + // and https://github.com/slick/slick/issues/1692 + profile match { + case _: MySQLProfile => + MTable.getTables(currentSchema, None, Option("%"), None) + case _ => + MTable.getTables(None, currentSchema, Option("%"), None) + } + + private def getCurrentSchema: DBIO[Option[String]] = + SimpleDBIO(ctx => tryGetSchema(ctx.connection).getOrElse(null)).flatMap { schema => + if (schema == null) { + // Not all JDBC drivers support the getSchema method: + // some always return null. + // In that case, fall back to vendor-specific queries. + profile match { + case _: H2Profile => + sql"SELECT SCHEMA();".as[String].headOption + case _: MySQLProfile => + sql"SELECT DATABASE();".as[String].headOption + case _: PostgresProfile => + sql"SELECT current_schema();".as[String].headOption + case _ => + DBIO.successful(None) + } + } else DBIO.successful(Some(schema)) + } + + // Some older JDBC drivers don't implement Connection.getSchema + // (including some builds of H2). This causes them to throw an + // AbstractMethodError at runtime. + // Because Try$.apply only catches NonFatal errors, and AbstractMethodError + // is considered fatal, we need to construct the Try explicitly. + private def tryGetSchema(connection: Connection): Try[String] = + try Success(connection.getSchema) + catch { + case e: AbstractMethodError => + Failure(new IllegalStateException("Database driver does not support Connection.getSchema", e)) + } + + private def tableExists( + schemaName: Option[String], + tableName: String + )(tables: Vector[MTable], currentSchema: Option[String]): Boolean = + tables.exists { t => + profile match { + case _: MySQLProfile => + t.name.catalog.orElse(currentSchema) == schemaName.orElse(currentSchema) && t.name.name == tableName + case _ => + t.name.schema.orElse(currentSchema) == schemaName.orElse(currentSchema) && t.name.name == tableName + } + } + +} diff --git a/proxy/postgres/src/main/resources/application.conf b/proxy/postgres/src/main/resources/application.conf index afae8aae8..c2101e61c 100644 --- a/proxy/postgres/src/main/resources/application.conf +++ b/proxy/postgres/src/main/resources/application.conf @@ -18,14 +18,37 @@ cloudstate.proxy.postgres { password = "cloudstate" password = ${?POSTGRES_PASSWORD} + + profile = "slick.jdbc.PostgresProfile$" + driver = "org.postgresql.Driver" } akka-persistence-jdbc.shared-databases.slick { - profile = "slick.jdbc.PostgresProfile$" + profile = ${cloudstate.proxy.postgres.profile} db { - driver = "org.postgresql.Driver" + driver = ${cloudstate.proxy.postgres.driver} + url = "jdbc:postgresql://"${cloudstate.proxy.postgres.service}":"${cloudstate.proxy.postgres.port}"/"${cloudstate.proxy.postgres.database} + user = ${cloudstate.proxy.postgres.user} + password = ${cloudstate.proxy.postgres.password} + } +} + +cloudstate.proxy.crud { + store-type = "jdbc" + + jdbc.database.slick { + profile = ${cloudstate.proxy.postgres.profile} + driver = ${cloudstate.proxy.postgres.driver} url = "jdbc:postgresql://"${cloudstate.proxy.postgres.service}":"${cloudstate.proxy.postgres.port}"/"${cloudstate.proxy.postgres.database} user = ${cloudstate.proxy.postgres.user} password = ${cloudstate.proxy.postgres.password} } + + jdbc-state-store { + tables { + state { + schemaName = ${cloudstate.proxy.postgres.schema} + } + } + } } From 3bc45a017a2ce66b96319eb2d90ce034c1cad0ac Mon Sep 17 00:00:00 2001 From: Guy Youansi Date: Fri, 18 Sep 2020 16:20:34 +0200 Subject: [PATCH 48/93] save CRUD state as byte array --- .../io/cloudstate/proxy/crud/store/JdbcCrudStateTable.scala | 5 ++--- .../io/cloudstate/proxy/crud/store/JdbcRepository.scala | 4 ++-- .../scala/io/cloudstate/proxy/crud/store/JdbcStore.scala | 2 +- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/proxy/core/src/main/scala/io/cloudstate/proxy/crud/store/JdbcCrudStateTable.scala b/proxy/core/src/main/scala/io/cloudstate/proxy/crud/store/JdbcCrudStateTable.scala index 68ea8b23e..247107b73 100644 --- a/proxy/core/src/main/scala/io/cloudstate/proxy/crud/store/JdbcCrudStateTable.scala +++ b/proxy/core/src/main/scala/io/cloudstate/proxy/crud/store/JdbcCrudStateTable.scala @@ -22,7 +22,7 @@ import slick.lifted.{MappedProjection, ProvenShape} object JdbcCrudStateTable { - case class CrudStateRow(key: Key, state: String) + case class CrudStateRow(key: Key, state: Array[Byte]) } trait JdbcCrudStateTable { @@ -42,8 +42,7 @@ trait JdbcCrudStateTable { val persistentId: Rep[String] = column[String](crudStateTableCfg.columnNames.persistentId, O.Length(255, varying = true)) val entityId: Rep[String] = column[String](crudStateTableCfg.columnNames.entityId, O.Length(255, varying = true)) - //TODO change state from Rep[String] to Rep[Array[Byte]] - val state: Rep[String] = column[String](crudStateTableCfg.columnNames.state, O.Length(255, varying = true)) + val state: Rep[Array[Byte]] = column[Array[Byte]](crudStateTableCfg.columnNames.state) val key: MappedProjection[Key, (String, String)] = (persistentId, entityId) <> (Key.tupled, Key.unapply) val pk = primaryKey(s"${tableName}_pk", (persistentId, entityId)) } diff --git a/proxy/core/src/main/scala/io/cloudstate/proxy/crud/store/JdbcRepository.scala b/proxy/core/src/main/scala/io/cloudstate/proxy/crud/store/JdbcRepository.scala index 6449481c2..fb93e2cc3 100644 --- a/proxy/core/src/main/scala/io/cloudstate/proxy/crud/store/JdbcRepository.scala +++ b/proxy/core/src/main/scala/io/cloudstate/proxy/crud/store/JdbcRepository.scala @@ -58,12 +58,12 @@ class JdbcRepositoryImpl(val store: JdbcStore[Key, ByteString])(implicit ec: Exe store .get(key) .map { - case Some(value) => Some(ScalaPbAny.parseFrom(value.toByteBuffer.array())) + case Some(value) => Some(ScalaPbAny.parseFrom(value.asByteBuffer.array())) //TODO not sure!!! case None => None } def update(key: Key, entity: ScalaPbAny): Future[Unit] = - store.update(key, ByteString(entity.toByteArray)) + store.update(key, ByteString.fromArrayUnsafe(entity.toByteArray)) def delete(key: Key): Future[Unit] = store.delete(key) } diff --git a/proxy/core/src/main/scala/io/cloudstate/proxy/crud/store/JdbcStore.scala b/proxy/core/src/main/scala/io/cloudstate/proxy/crud/store/JdbcStore.scala index 72799b85e..f684b0d0a 100644 --- a/proxy/core/src/main/scala/io/cloudstate/proxy/crud/store/JdbcStore.scala +++ b/proxy/core/src/main/scala/io/cloudstate/proxy/crud/store/JdbcStore.scala @@ -74,7 +74,7 @@ final class JdbcStoreImpl(slickDatabase: JdbcSlickDatabase, queries: JdbcCrudSta db.run(queries.selectByKey(key).result.headOption.map(mayBeState => mayBeState.map(s => ByteString(s.state)))) override def update(key: Key, value: ByteString): Future[Unit] = - db.run(queries.insertOrUpdate(CrudStateRow(key, value.utf8String))).map(_ => ()) + db.run(queries.insertOrUpdate(CrudStateRow(key, value.asByteBuffer.array()))).map(_ => ()) override def delete(key: Key): Future[Unit] = db.run(queries.deleteByKey(key)).map(_ => ()) From 93a59624987da2a1e2c80558bf89cd74f72ef87c Mon Sep 17 00:00:00 2001 From: Guy Youansi Date: Fri, 18 Sep 2020 21:48:48 +0200 Subject: [PATCH 49/93] fixed map on slick dbioaction --- .../io/cloudstate/proxy/crud/store/JdbcStore.scala | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/proxy/core/src/main/scala/io/cloudstate/proxy/crud/store/JdbcStore.scala b/proxy/core/src/main/scala/io/cloudstate/proxy/crud/store/JdbcStore.scala index f684b0d0a..22dd9d1aa 100644 --- a/proxy/core/src/main/scala/io/cloudstate/proxy/crud/store/JdbcStore.scala +++ b/proxy/core/src/main/scala/io/cloudstate/proxy/crud/store/JdbcStore.scala @@ -71,12 +71,18 @@ final class JdbcStoreImpl(slickDatabase: JdbcSlickDatabase, queries: JdbcCrudSta private val db = slickDatabase.database override def get(key: Key): Future[Option[ByteString]] = - db.run(queries.selectByKey(key).result.headOption.map(mayBeState => mayBeState.map(s => ByteString(s.state)))) + for { + rows <- db.run(queries.selectByKey(key).result) + } yield rows.headOption.map(r => ByteString(r.state)) override def update(key: Key, value: ByteString): Future[Unit] = - db.run(queries.insertOrUpdate(CrudStateRow(key, value.asByteBuffer.array()))).map(_ => ()) + for { + _ <- db.run(queries.insertOrUpdate(CrudStateRow(key, value.asByteBuffer.array()))) + } yield () override def delete(key: Key): Future[Unit] = - db.run(queries.deleteByKey(key)).map(_ => ()) + for { + _ <- db.run(queries.deleteByKey(key)) + } yield () } From dd19ed149a80f356a9202d7cfec04c61202c0abd Mon Sep 17 00:00:00 2001 From: Guy Youansi Date: Sun, 20 Sep 2020 16:58:17 +0200 Subject: [PATCH 50/93] change actor name --- .../proxy/jdbc/SlickEnsureCrudTablesExistReadyCheck.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/proxy/jdbc/src/main/scala/io/cloudstate/proxy/jdbc/SlickEnsureCrudTablesExistReadyCheck.scala b/proxy/jdbc/src/main/scala/io/cloudstate/proxy/jdbc/SlickEnsureCrudTablesExistReadyCheck.scala index 1fd1d3a37..eb22e350d 100644 --- a/proxy/jdbc/src/main/scala/io/cloudstate/proxy/jdbc/SlickEnsureCrudTablesExistReadyCheck.scala +++ b/proxy/jdbc/src/main/scala/io/cloudstate/proxy/jdbc/SlickEnsureCrudTablesExistReadyCheck.scala @@ -49,13 +49,13 @@ class SlickEnsureCrudTablesExistReadyCheck(system: ActorSystem) extends (() => F BackoffSupervisor.props( BackoffOpts.onFailure( childProps = Props(new EnsureCrudTablesExistsActor(db)), - childName = "crud-jdbc-table-creator", + childName = "crud-table-creator", minBackoff = 3.seconds, maxBackoff = 30.seconds, randomFactor = 0.2 ) ), - "crud-jdbc-table-creator-supervisor" + "crud-table-creator-supervisor" ) implicit val timeout = Timeout(10.seconds) // TODO make configurable? From e84d4e656f95bc40286e44e27d83157da4bc34d9 Mon Sep 17 00:00:00 2001 From: Guy Youansi Date: Tue, 22 Sep 2020 10:47:26 +0200 Subject: [PATCH 51/93] add infra for CRUD tests in testkit. add exception handling test for CRUD entity --- .../io/cloudstate/proxy/crud/CrudEntity.scala | 29 ++-- .../proxy/crud/ExceptionHandlingSpec.scala | 148 ++++++++++++++++++ .../testkit/crud/CrudMessages.scala | 99 ++++++++++++ .../testkit/crud/TestCrudService.scala | 89 +++++++++++ .../testkit/crud/TestCrudServiceClient.scala | 74 +++++++++ 5 files changed, 426 insertions(+), 13 deletions(-) create mode 100644 proxy/core/src/test/scala/io/cloudstate/proxy/crud/ExceptionHandlingSpec.scala create mode 100644 testkit/src/main/scala/io/cloudstate/testkit/crud/CrudMessages.scala create mode 100644 testkit/src/main/scala/io/cloudstate/testkit/crud/TestCrudService.scala create mode 100644 testkit/src/main/scala/io/cloudstate/testkit/crud/TestCrudServiceClient.scala diff --git a/proxy/core/src/main/scala/io/cloudstate/proxy/crud/CrudEntity.scala b/proxy/core/src/main/scala/io/cloudstate/proxy/crud/CrudEntity.scala index f56afab46..2a725585d 100644 --- a/proxy/core/src/main/scala/io/cloudstate/proxy/crud/CrudEntity.scala +++ b/proxy/core/src/main/scala/io/cloudstate/proxy/crud/CrudEntity.scala @@ -276,9 +276,10 @@ final class CrudEntity(configuration: CrudEntity.Configuration, } } - private[this] final def crash(msg: String): Unit = { + // only the msg is returned to the user, while the details are also part of the exception + private[this] final def crash(msg: String, details: String): Unit = { notifyOutstandingRequests(msg) - throw new Exception(msg) + throw new Exception(s"$msg - $details") } private[this] final def reportActionComplete() = @@ -321,8 +322,8 @@ final class CrudEntity(configuration: CrudEntity.Configuration, // ignore entity already initialized case CrudEntity.LoadInitStateFailure(error) => - log.error(error, s"CRUD Entity cannot load the initial state due to unexpected failure ${error.getMessage}") - throw error + crash("Unexpected CRUD entity failure", + s"(cannot load the initial state due to unexpected failure) - ${error.getMessage}") case _ => stash() } @@ -340,10 +341,11 @@ final class CrudEntity(configuration: CrudEntity.Configuration, m match { case CrudSOMsg.Reply(r) if currentCommand == null => - crash(s"Unexpected reply, had no current command: $r") + crash("Unexpected CRUD entity reply", s"(no current command) - $r") case CrudSOMsg.Reply(r) if currentCommand.commandId != r.commandId => - crash(s"Incorrect command id in reply, expecting ${currentCommand.commandId} but got ${r.commandId}") + crash("Unexpected CRUD entity reply", + s"(expected id ${currentCommand.commandId} but got ${r.commandId}) - $r") case CrudSOMsg.Reply(r) => reportActionComplete() @@ -359,7 +361,7 @@ final class CrudEntity(configuration: CrudEntity.Configuration, reportDatabaseOperationFinished() // Make sure that the current request is still ours if (currentCommand == null || currentCommand.commandId != commandId) { - crash("Internal error - currentRequest changed before all events were persisted") + crash("Unexpected CRUD entity behavior", "currentRequest changed before the state were persisted") } currentCommand.replyTo ! esReplyToUfReply(r) commandHandled() @@ -370,23 +372,24 @@ final class CrudEntity(configuration: CrudEntity.Configuration, } case CrudSOMsg.Failure(f) if f.commandId == 0 => - crash(s"Non command specific error from entity: ${f.description}") + crash("Unexpected CRUD entity failure", s"(not command specific) - ${f.description}") case CrudSOMsg.Failure(f) if currentCommand == null => - crash(s"Unexpected failure, had no current command: $f") + crash("Unexpected CRUD entity failure", s"(no current command) - ${f.description}") case CrudSOMsg.Failure(f) if currentCommand.commandId != f.commandId => - crash(s"Incorrect command id in failure, expecting ${currentCommand.commandId} but got ${f.commandId}") + crash("Unexpected CRUD entity failure", + s"(expected id ${currentCommand.commandId} but got ${f.commandId}) - ${f.description}") case CrudSOMsg.Failure(f) => reportActionComplete() - currentCommand.replyTo ! createFailure(f.description) - commandHandled() + try crash("Unexpected CRUD entity failure", f.description) + finally currentCommand = null // clear command after notifications case CrudSOMsg.Empty => // Either the reply/failure wasn't set, or its set to something unknown. // todo see if scalapb can give us unknown fields so we can possibly log more intelligently - crash("Empty or unknown message from entity output stream") + crash("Unexpected CRUD entity failure", "empty or unknown message from entity output stream") } case CrudEntity.StreamClosed => diff --git a/proxy/core/src/test/scala/io/cloudstate/proxy/crud/ExceptionHandlingSpec.scala b/proxy/core/src/test/scala/io/cloudstate/proxy/crud/ExceptionHandlingSpec.scala new file mode 100644 index 000000000..ac6b83a6a --- /dev/null +++ b/proxy/core/src/test/scala/io/cloudstate/proxy/crud/ExceptionHandlingSpec.scala @@ -0,0 +1,148 @@ +/* + * Copyright 2019 Lightbend Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.cloudstate.proxy.crud + +import akka.Done +import akka.actor.ActorSystem +import akka.http.scaladsl.Http +import akka.http.scaladsl.model.{HttpRequest, Uri} +import akka.http.scaladsl.unmarshalling.Unmarshal +import akka.grpc.{GrpcClientSettings, GrpcServiceException} +import akka.testkit.TestKit +import io.cloudstate.proxy.TestProxy +import org.scalatest.{BeforeAndAfterAll, Matchers, WordSpec} +import org.scalatest.concurrent.ScalaFutures +import io.cloudstate.testkit.crud.{CrudMessages, TestCrudService} +import io.cloudstate.proxy.test.thing.{Key, Thing, ThingClient} +import io.grpc.{Status, StatusRuntimeException} + +class ExceptionHandlingSpec extends WordSpec with Matchers with BeforeAndAfterAll with ScalaFutures { + + import CrudMessages._ + + implicit val system = ActorSystem("CrudExceptionHandlingSpec") + + val service = TestCrudService() + val proxy = TestProxy(service.port) + val client = ThingClient(GrpcClientSettings.connectToServiceAt("localhost", proxy.port).withTls(false)) + val spec = TestCrudService.entitySpec(Thing) + + val discovery = service.expectDiscovery() + discovery.expect(proxy.info) + discovery.send(spec) + proxy.expectOnline() + + override def afterAll(): Unit = { + client.close().futureValue shouldBe Done + TestKit.shutdownActorSystem(system) + proxy.terminate() + service.terminate() + } + + "Cloudstate proxy for CRUD" should { + + "respond with gRPC error for action failure in entity" in { + val call = client.get(Key("one")) + val connection = service.expectConnection() + connection.expect(init(Thing.name, "one")) + connection.expect(command(1, "one", "Get", Key("one"))) + proxy.expectLogError("User Function responded with a failure: description goes here") { + connection.send(actionFailure(1, "description goes here")) + } + val error = call.failed.futureValue + error shouldBe a[StatusRuntimeException] + error.getMessage shouldBe "UNKNOWN: description goes here" + connection.close() + } + + "respond with gRPC error for unexpected failure in entity" in { + val call = client.get(Key("two")) + val connection = service.expectConnection() + connection.expect(init(Thing.name, "two")) + connection.expect(command(1, "two", "Get", Key("two"))) + proxy.expectLogError("User Function responded with a failure: Unexpected CRUD entity failure") { + proxy.expectLogError("Unexpected CRUD entity failure - boom plus details") { + connection.send(failure(1, "boom plus details")) + connection.expectClosed() + } + } + val error = call.failed.futureValue + error shouldBe a[StatusRuntimeException] + error.getMessage shouldBe "UNKNOWN: Unexpected CRUD entity failure" + } + + "respond with gRPC error for stream error in entity" in { + val call = client.get(Key("three")) + val connection = service.expectConnection() + connection.expect(init(Thing.name, "three")) + connection.expect(command(1, "three", "Get", Key("three"))) + proxy.expectLogError("User Function responded with a failure: Unexpected CRUD entity termination") { + proxy.expectLogError("INTERNAL: stream failed") { + connection.sendError(new GrpcServiceException(Status.INTERNAL.withDescription("stream failed"))) + } + } + val error = call.failed.futureValue + error shouldBe a[StatusRuntimeException] + error.getMessage shouldBe "UNKNOWN: Unexpected CRUD entity termination" + } + + "respond with HTTP error for action failure in entity" in { + val call = Http().singleRequest(HttpRequest(uri = Uri(s"http://localhost:${proxy.port}/thing/four"))) + val connection = service.expectConnection() + connection.expect(init(Thing.name, "four")) + connection.expect(command(1, "four", "Get", Key("four"))) + proxy.expectLogError("User Function responded with a failure: description goes here") { + connection.send(actionFailure(1, "description goes here")) + } + val response = call.futureValue + response.status.intValue shouldBe 500 + Unmarshal(response).to[String].futureValue shouldBe "description goes here" + connection.close() + } + + "respond with HTTP error for unexpected failure in entity" in { + val call = Http().singleRequest(HttpRequest(uri = Uri(s"http://localhost:${proxy.port}/thing/five"))) + val connection = service.expectConnection() + connection.expect(init(Thing.name, "five")) + connection.expect(command(1, "five", "Get", Key("five"))) + proxy.expectLogError("User Function responded with a failure: Unexpected CRUD entity failure") { + proxy.expectLogError("Unexpected CRUD entity failure - boom plus details") { + connection.send(failure(1, "boom plus details")) + connection.expectClosed() + } + } + val response = call.futureValue + response.status.intValue shouldBe 500 + Unmarshal(response).to[String].futureValue shouldBe "Unexpected CRUD entity failure" + } + + "respond with HTTP error for stream error in entity" in { + val call = Http().singleRequest(HttpRequest(uri = Uri(s"http://localhost:${proxy.port}/thing/six"))) + val connection = service.expectConnection() + connection.expect(init(Thing.name, "six")) + connection.expect(command(1, "six", "Get", Key("six"))) + proxy.expectLogError("User Function responded with a failure: Unexpected CRUD entity termination") { + proxy.expectLogError("INTERNAL: stream failed") { + connection.sendError(new GrpcServiceException(Status.INTERNAL.withDescription("stream failed"))) + } + } + val response = call.futureValue + response.status.intValue shouldBe 500 + Unmarshal(response).to[String].futureValue shouldBe "Unexpected CRUD entity termination" + } + } +} diff --git a/testkit/src/main/scala/io/cloudstate/testkit/crud/CrudMessages.scala b/testkit/src/main/scala/io/cloudstate/testkit/crud/CrudMessages.scala new file mode 100644 index 000000000..d46d1ddea --- /dev/null +++ b/testkit/src/main/scala/io/cloudstate/testkit/crud/CrudMessages.scala @@ -0,0 +1,99 @@ +/* + * Copyright 2019 Lightbend Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.cloudstate.testkit.crud + +import com.google.protobuf.any.{Any => ScalaPbAny} +import com.google.protobuf.{Empty => JavaPbEmpty, Message => JavaPbMessage} +import io.cloudstate.protocol.entity._ +import io.cloudstate.protocol.crud._ +import scalapb.{GeneratedMessage => ScalaPbMessage} + +object CrudMessages { + import CrudStreamIn.{Message => InMessage} + import CrudStreamOut.{Message => OutMessage} + + val EmptyMessage: InMessage = InMessage.Empty + val EmptyPayload: JavaPbMessage = JavaPbEmpty.getDefaultInstance + val EmptyAny: ScalaPbAny = protobufAny(EmptyPayload) + + def init(serviceName: String, entityId: String): InMessage = + init(serviceName, entityId, CrudInitState()) + + def init(serviceName: String, entityId: String, state: CrudInitState): InMessage = + init(serviceName, entityId, Some(state)) + + def init(serviceName: String, entityId: String, state: Option[CrudInitState]): InMessage = + InMessage.Init(CrudInit(serviceName, entityId, state)) + + def state(payload: JavaPbMessage): Option[ScalaPbAny] = + messagePayload(payload) + + def state(payload: ScalaPbMessage): Option[ScalaPbAny] = + messagePayload(payload) + + def command(id: Long, entityId: String, name: String): InMessage = + command(id, entityId, name, EmptyPayload) + + def command(id: Long, entityId: String, name: String, payload: JavaPbMessage): InMessage = + command(id, entityId, name, messagePayload(payload)) + + def command(id: Long, entityId: String, name: String, payload: ScalaPbMessage): InMessage = + command(id, entityId, name, messagePayload(payload)) + + def command(id: Long, entityId: String, name: String, payload: Option[ScalaPbAny]): InMessage = + InMessage.Command(Command(entityId, id, name, payload)) + + def reply(id: Long, payload: JavaPbMessage, action: CrudAction): OutMessage = + reply(id, messagePayload(payload), Some(action)) + + def reply(id: Long, payload: ScalaPbMessage, action: CrudAction): OutMessage = + reply(id, messagePayload(payload), Some(action)) + + def reply(id: Long, payload: Option[ScalaPbAny], action: Option[CrudAction]): OutMessage = + OutMessage.Reply(CrudReply(id, clientActionReply(payload), Seq.empty, action)) + + def actionFailure(id: Long, description: String): OutMessage = + OutMessage.Reply(CrudReply(id, clientActionFailure(id, description))) + + def failure(description: String): OutMessage = + failure(id = 0, description) + + def failure(id: Long, description: String): OutMessage = + OutMessage.Failure(Failure(id, description)) + + def clientActionReply(payload: Option[ScalaPbAny]): Option[ClientAction] = + Some(ClientAction(ClientAction.Action.Reply(Reply(payload)))) + + def clientActionFailure(description: String): Option[ClientAction] = + clientActionFailure(id = 0, description) + + def clientActionFailure(id: Long, description: String): Option[ClientAction] = + Some(ClientAction(ClientAction.Action.Failure(Failure(id, description)))) + + def messagePayload(message: JavaPbMessage): Option[ScalaPbAny] = + Option(message).map(protobufAny) + + def messagePayload(message: ScalaPbMessage): Option[ScalaPbAny] = + Option(message).map(protobufAny) + + def protobufAny(message: JavaPbMessage): ScalaPbAny = + ScalaPbAny("type.googleapis.com/" + message.getDescriptorForType.getFullName, message.toByteString) + + def protobufAny(message: ScalaPbMessage): ScalaPbAny = + ScalaPbAny("type.googleapis.com/" + message.companion.scalaDescriptor.fullName, message.toByteString) + +} diff --git a/testkit/src/main/scala/io/cloudstate/testkit/crud/TestCrudService.scala b/testkit/src/main/scala/io/cloudstate/testkit/crud/TestCrudService.scala new file mode 100644 index 000000000..22bfa7c1c --- /dev/null +++ b/testkit/src/main/scala/io/cloudstate/testkit/crud/TestCrudService.scala @@ -0,0 +1,89 @@ +/* + * Copyright 2019 Lightbend Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.cloudstate.testkit.crud + +import akka.NotUsed +import akka.actor.ActorSystem +import akka.grpc.ServiceDescription +import akka.http.scaladsl.model.{HttpRequest, HttpResponse} +import akka.stream.scaladsl.Source +import akka.stream.testkit.TestPublisher +import akka.stream.testkit.scaladsl.TestSink +import akka.testkit.TestProbe +import com.google.protobuf.Descriptors.ServiceDescriptor +import io.cloudstate.protocol.crud.{Crud, CrudHandler, CrudStreamIn, CrudStreamOut} +import io.cloudstate.protocol.entity.EntitySpec +import io.cloudstate.testkit.TestService + +import scala.concurrent.Future + +class TestCrudService extends TestService { + private val crud = new TestCrudService.TestCrud(system, probe) + + override protected def handler: PartialFunction[HttpRequest, Future[HttpResponse]] = + super.handler orElse CrudHandler.partial(crud) + + def expectConnection(): TestCrudService.Connection = probe.expectMsgType[TestCrudService.Connection] + + start() +} + +object TestCrudService { + def apply(): TestCrudService = new TestCrudService + + def entitySpec(service: ServiceDescription): EntitySpec = + TestService.entitySpec(Crud.name, service) + + def entitySpec(descriptors: Seq[ServiceDescriptor]): EntitySpec = + TestService.entitySpec(Crud.name, descriptors) + + final class TestCrud(system: ActorSystem, probe: TestProbe) extends Crud { + override def handle(source: Source[CrudStreamIn, NotUsed]): Source[CrudStreamOut, NotUsed] = { + val connection = new Connection(system, source) + probe.ref ! connection + connection.outSource + } + } + + final class Connection(system: ActorSystem, source: Source[CrudStreamIn, NotUsed]) { + private implicit val actorSystem: ActorSystem = system + private val in = source.runWith(TestSink.probe[CrudStreamIn]) + private val out = TestPublisher.probe[CrudStreamOut]() + + in.ensureSubscription() + + private[testkit] def outSource: Source[CrudStreamOut, NotUsed] = Source.fromPublisher(out) + + def expect(message: CrudStreamIn.Message): Unit = + in.request(1).expectNext(CrudStreamIn(message)) + + def send(message: CrudStreamOut.Message): Unit = + out.sendNext(CrudStreamOut(message)) + + def sendError(error: Throwable): Unit = + out.sendError(error) + + def expectClosed(): Unit = { + in.expectComplete() + close() + } + + def close(): Unit = + out.sendComplete() + } + +} diff --git a/testkit/src/main/scala/io/cloudstate/testkit/crud/TestCrudServiceClient.scala b/testkit/src/main/scala/io/cloudstate/testkit/crud/TestCrudServiceClient.scala new file mode 100644 index 000000000..f1d7eef90 --- /dev/null +++ b/testkit/src/main/scala/io/cloudstate/testkit/crud/TestCrudServiceClient.scala @@ -0,0 +1,74 @@ +/* + * Copyright 2019 Lightbend Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.cloudstate.testkit.crud + +import akka.actor.ActorSystem +import akka.grpc.GrpcClientSettings +import akka.stream.scaladsl.Source +import akka.stream.testkit.TestPublisher +import akka.stream.testkit.scaladsl.TestSink +import akka.testkit.TestKit +import com.typesafe.config.{Config, ConfigFactory} +import io.cloudstate.protocol.crud.{CrudClient, CrudStreamIn, CrudStreamOut} + +class TestCrudServiceClient(port: Int) { + private val config: Config = ConfigFactory.load(ConfigFactory.parseString(""" + akka.http.server { + preview.enable-http2 = on + } + """)) + + private implicit val system: ActorSystem = ActorSystem("TestCrudServiceClient", config) + private val client = CrudClient(GrpcClientSettings.connectToServiceAt("localhost", port).withTls(false)) + + def connect: TestCrudServiceClient.Connection = new TestCrudServiceClient.Connection(client, system) + + def terminate(): Unit = { + client.close() + TestKit.shutdownActorSystem(system) + } +} + +object TestCrudServiceClient { + def apply(port: Int) = new TestCrudServiceClient(port) + + final class Connection(client: CrudClient, system: ActorSystem) { + private implicit val actorSystem: ActorSystem = system + private val in = TestPublisher.probe[CrudStreamIn]() + private val out = client.handle(Source.fromPublisher(in)).runWith(TestSink.probe[CrudStreamOut]) + + out.ensureSubscription() + + def send(message: CrudStreamIn.Message): Unit = + in.sendNext(CrudStreamIn(message)) + + def expect(message: CrudStreamOut.Message): Unit = + out.request(1).expectNext(CrudStreamOut(message)) + + def expectClosed(): Unit = { + out.expectComplete() + in.expectCancellation() + } + + def passivate(): Unit = close() + + def close(): Unit = { + in.sendComplete() + out.expectComplete() + } + } +} From 624afbf84f138886a7f538702647f3c19f95bdbc Mon Sep 17 00:00:00 2001 From: Guy Youansi Date: Tue, 22 Sep 2020 18:49:10 +0200 Subject: [PATCH 52/93] fixed conflicts and compile errors when merging master --- build.sbt | 2 +- .../io/cloudstate/javasupport/CloudState.java | 37 ++++++----- .../javasupport/CloudStateRunner.scala | 2 +- .../crud/AnnotationBasedCrudSupport.scala | 4 +- .../javasupport/impl/crud/CrudImpl.scala | 4 +- .../proxy/crud/ExceptionHandlingSpec.scala | 17 ++--- .../io/cloudstate/testkit/TestProtocol.scala | 3 + .../io/cloudstate/testkit/TestService.scala | 6 +- .../testkit/crud/TestCrudProtocol.scala | 65 +++++++++++++++++++ .../testkit/crud/TestCrudService.scala | 28 ++++---- 10 files changed, 119 insertions(+), 49 deletions(-) create mode 100644 testkit/src/main/scala/io/cloudstate/testkit/crud/TestCrudProtocol.scala diff --git a/build.sbt b/build.sbt index 8ae2dca43..b09e3aa7c 100644 --- a/build.sbt +++ b/build.sbt @@ -641,7 +641,7 @@ lazy val `java-support-docs` = (project in file("java-support/docs")) ) lazy val `java-support-tck` = (project in file("java-support/tck")) - .dependsOn(`java-support`, `java-shopping-cart`) + .dependsOn(`java-support`, `java-shopping-cart`, `java-crud-shopping-cart`) .enablePlugins(AkkaGrpcPlugin, AssemblyPlugin, JavaAppPackaging, DockerPlugin, AutomateHeaderPlugin, NoPublish) .settings( name := "cloudstate-java-tck", diff --git a/java-support/src/main/java/io/cloudstate/javasupport/CloudState.java b/java-support/src/main/java/io/cloudstate/javasupport/CloudState.java index ce1534dbc..9587fda4f 100644 --- a/java-support/src/main/java/io/cloudstate/javasupport/CloudState.java +++ b/java-support/src/main/java/io/cloudstate/javasupport/CloudState.java @@ -322,14 +322,14 @@ public CloudState registerAction( * @return This stateful service builder. */ public CloudState registerCrudEntity( - Class entityClass, - Descriptors.ServiceDescriptor descriptor, - Descriptors.FileDescriptor... additionalDescriptors) { + Class entityClass, + Descriptors.ServiceDescriptor descriptor, + Descriptors.FileDescriptor... additionalDescriptors) { CrudEntity entity = entityClass.getAnnotation(CrudEntity.class); if (entity == null) { throw new IllegalArgumentException( - entityClass + " does not declare an " + CrudEntity.class + " annotation!"); + entityClass + " does not declare an " + CrudEntity.class + " annotation!"); } final String persistenceId; @@ -340,20 +340,20 @@ public CloudState registerCrudEntity( } final AnySupport anySupport = newAnySupport(additionalDescriptors); + CrudStatefulService service = + new CrudStatefulService( + new AnnotationBasedCrudSupport(entityClass, anySupport, descriptor), + descriptor, + anySupport, + persistenceId); - services.put( - descriptor.getFullName(), - new CrudStatefulService( - new AnnotationBasedCrudSupport(entityClass, anySupport, descriptor), - descriptor, - anySupport, - persistenceId)); + services.put(descriptor.getFullName(), system -> service); return this; } /** - * Register an CRUD entity factory. + * Register a CRUD entity factory. * *

This is a low level API intended for custom (eg, non reflection based) mechanisms for * implementing the entity. @@ -366,14 +366,15 @@ public CloudState registerCrudEntity( * @return This stateful service builder. */ public CloudState registerCrudEntity( - CrudEntityFactory factory, - Descriptors.ServiceDescriptor descriptor, - String persistenceId, - Descriptors.FileDescriptor... additionalDescriptors) { + CrudEntityFactory factory, + Descriptors.ServiceDescriptor descriptor, + String persistenceId, + Descriptors.FileDescriptor... additionalDescriptors) { services.put( - descriptor.getFullName(), + descriptor.getFullName(), + system -> new CrudStatefulService( - factory, descriptor, newAnySupport(additionalDescriptors), persistenceId)); + factory, descriptor, newAnySupport(additionalDescriptors), persistenceId)); return this; } diff --git a/java-support/src/main/scala/io/cloudstate/javasupport/CloudStateRunner.scala b/java-support/src/main/scala/io/cloudstate/javasupport/CloudStateRunner.scala index ba1c8c7e7..60ee93394 100644 --- a/java-support/src/main/scala/io/cloudstate/javasupport/CloudStateRunner.scala +++ b/java-support/src/main/scala/io/cloudstate/javasupport/CloudStateRunner.scala @@ -117,7 +117,7 @@ final class CloudStateRunner private[this] ( route orElse StatelessFunctionHandler.partial(actionImpl) case (route, (serviceClass, crudServices: Map[String, CrudStatefulService] @unchecked)) - if serviceClass == classOf[CrudStatefulService] => + if serviceClass == classOf[CrudStatefulService] => val crudImpl = new CrudImpl(system, crudServices, rootContext, configuration) route orElse CrudHandler.partial(crudImpl) diff --git a/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/AnnotationBasedCrudSupport.scala b/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/AnnotationBasedCrudSupport.scala index cdeffbda2..3dfe477d8 100644 --- a/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/AnnotationBasedCrudSupport.scala +++ b/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/AnnotationBasedCrudSupport.scala @@ -144,7 +144,7 @@ private object CrudBehaviorReflection { } private class EntityConstructorInvoker(constructor: Constructor[_]) extends (CrudEntityCreationContext => AnyRef) { - private val parameters = ReflectionHelper.getParameterHandlers[CrudEntityCreationContext](constructor)() + private val parameters = ReflectionHelper.getParameterHandlers[AnyRef, CrudEntityCreationContext](constructor)() parameters.foreach { case MainArgumentParameterHandler(clazz) => throw new RuntimeException(s"Don't know how to handle argument of type $clazz in constructor") @@ -152,7 +152,7 @@ private class EntityConstructorInvoker(constructor: Constructor[_]) extends (Cru } def apply(context: CrudEntityCreationContext): AnyRef = { - val ctx = InvocationContext("", context) + val ctx = InvocationContext(null.asInstanceOf[AnyRef], context) constructor.newInstance(parameters.map(_.apply(ctx)): _*).asInstanceOf[AnyRef] } } diff --git a/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/CrudImpl.scala b/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/CrudImpl.scala index 36ab66f8e..f49e53457 100644 --- a/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/CrudImpl.scala +++ b/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/CrudImpl.scala @@ -28,7 +28,7 @@ import com.google.protobuf.any.{Any => ScalaPbAny} import io.cloudstate.javasupport.crud._ import io.cloudstate.javasupport.impl._ import io.cloudstate.javasupport.impl.crud.CrudImpl.{failure, failureMessage, EntityException, ProtocolException} -import io.cloudstate.javasupport.{Context, ServiceCallFactory, StatefulService} +import io.cloudstate.javasupport.{Context, Service, ServiceCallFactory} import io.cloudstate.protocol.crud.CrudAction.Action.{Delete, Update} import io.cloudstate.protocol.crud._ import io.cloudstate.protocol.crud.CrudStreamIn.Message.{Command => InCommand, Empty => InEmpty, Init => InInit} @@ -42,7 +42,7 @@ final class CrudStatefulService(val factory: CrudEntityFactory, override val descriptor: Descriptors.ServiceDescriptor, val anySupport: AnySupport, override val persistenceId: String) - extends StatefulService { + extends Service { override def resolvedMethods: Option[Map[String, ResolvedServiceMethod[_, _]]] = factory match { diff --git a/proxy/core/src/test/scala/io/cloudstate/proxy/crud/ExceptionHandlingSpec.scala b/proxy/core/src/test/scala/io/cloudstate/proxy/crud/ExceptionHandlingSpec.scala index ac6b83a6a..be5e35a15 100644 --- a/proxy/core/src/test/scala/io/cloudstate/proxy/crud/ExceptionHandlingSpec.scala +++ b/proxy/core/src/test/scala/io/cloudstate/proxy/crud/ExceptionHandlingSpec.scala @@ -28,6 +28,7 @@ import org.scalatest.{BeforeAndAfterAll, Matchers, WordSpec} import org.scalatest.concurrent.ScalaFutures import io.cloudstate.testkit.crud.{CrudMessages, TestCrudService} import io.cloudstate.proxy.test.thing.{Key, Thing, ThingClient} +import io.cloudstate.testkit.TestService import io.grpc.{Status, StatusRuntimeException} class ExceptionHandlingSpec extends WordSpec with Matchers with BeforeAndAfterAll with ScalaFutures { @@ -36,12 +37,12 @@ class ExceptionHandlingSpec extends WordSpec with Matchers with BeforeAndAfterAl implicit val system = ActorSystem("CrudExceptionHandlingSpec") - val service = TestCrudService() + val service = TestService() val proxy = TestProxy(service.port) val client = ThingClient(GrpcClientSettings.connectToServiceAt("localhost", proxy.port).withTls(false)) val spec = TestCrudService.entitySpec(Thing) - val discovery = service.expectDiscovery() + val discovery = service.entityDiscovery.expectDiscovery() discovery.expect(proxy.info) discovery.send(spec) proxy.expectOnline() @@ -57,7 +58,7 @@ class ExceptionHandlingSpec extends WordSpec with Matchers with BeforeAndAfterAl "respond with gRPC error for action failure in entity" in { val call = client.get(Key("one")) - val connection = service.expectConnection() + val connection = service.crud.expectConnection() connection.expect(init(Thing.name, "one")) connection.expect(command(1, "one", "Get", Key("one"))) proxy.expectLogError("User Function responded with a failure: description goes here") { @@ -71,7 +72,7 @@ class ExceptionHandlingSpec extends WordSpec with Matchers with BeforeAndAfterAl "respond with gRPC error for unexpected failure in entity" in { val call = client.get(Key("two")) - val connection = service.expectConnection() + val connection = service.crud.expectConnection() connection.expect(init(Thing.name, "two")) connection.expect(command(1, "two", "Get", Key("two"))) proxy.expectLogError("User Function responded with a failure: Unexpected CRUD entity failure") { @@ -87,7 +88,7 @@ class ExceptionHandlingSpec extends WordSpec with Matchers with BeforeAndAfterAl "respond with gRPC error for stream error in entity" in { val call = client.get(Key("three")) - val connection = service.expectConnection() + val connection = service.crud.expectConnection() connection.expect(init(Thing.name, "three")) connection.expect(command(1, "three", "Get", Key("three"))) proxy.expectLogError("User Function responded with a failure: Unexpected CRUD entity termination") { @@ -102,7 +103,7 @@ class ExceptionHandlingSpec extends WordSpec with Matchers with BeforeAndAfterAl "respond with HTTP error for action failure in entity" in { val call = Http().singleRequest(HttpRequest(uri = Uri(s"http://localhost:${proxy.port}/thing/four"))) - val connection = service.expectConnection() + val connection = service.crud.expectConnection() connection.expect(init(Thing.name, "four")) connection.expect(command(1, "four", "Get", Key("four"))) proxy.expectLogError("User Function responded with a failure: description goes here") { @@ -116,7 +117,7 @@ class ExceptionHandlingSpec extends WordSpec with Matchers with BeforeAndAfterAl "respond with HTTP error for unexpected failure in entity" in { val call = Http().singleRequest(HttpRequest(uri = Uri(s"http://localhost:${proxy.port}/thing/five"))) - val connection = service.expectConnection() + val connection = service.crud.expectConnection() connection.expect(init(Thing.name, "five")) connection.expect(command(1, "five", "Get", Key("five"))) proxy.expectLogError("User Function responded with a failure: Unexpected CRUD entity failure") { @@ -132,7 +133,7 @@ class ExceptionHandlingSpec extends WordSpec with Matchers with BeforeAndAfterAl "respond with HTTP error for stream error in entity" in { val call = Http().singleRequest(HttpRequest(uri = Uri(s"http://localhost:${proxy.port}/thing/six"))) - val connection = service.expectConnection() + val connection = service.crud.expectConnection() connection.expect(init(Thing.name, "six")) connection.expect(command(1, "six", "Get", Key("six"))) proxy.expectLogError("User Function responded with a failure: Unexpected CRUD entity termination") { diff --git a/testkit/src/main/scala/io/cloudstate/testkit/TestProtocol.scala b/testkit/src/main/scala/io/cloudstate/testkit/TestProtocol.scala index dc84a02bb..a27d72780 100644 --- a/testkit/src/main/scala/io/cloudstate/testkit/TestProtocol.scala +++ b/testkit/src/main/scala/io/cloudstate/testkit/TestProtocol.scala @@ -20,6 +20,7 @@ import akka.actor.ActorSystem import akka.grpc.GrpcClientSettings import akka.testkit.TestKit import com.typesafe.config.{Config, ConfigFactory} +import io.cloudstate.testkit.crud.TestCrudProtocol import io.cloudstate.testkit.eventsourced.TestEventSourcedProtocol final class TestProtocol(host: String, port: Int) { @@ -28,11 +29,13 @@ final class TestProtocol(host: String, port: Int) { val context = new TestProtocolContext(host, port) val eventSourced = new TestEventSourcedProtocol(context) + val crud = new TestCrudProtocol(context) def settings: GrpcClientSettings = context.clientSettings def terminate(): Unit = { eventSourced.terminate() + crud.terminate() context.terminate() } } diff --git a/testkit/src/main/scala/io/cloudstate/testkit/TestService.scala b/testkit/src/main/scala/io/cloudstate/testkit/TestService.scala index 965ab839a..282038205 100644 --- a/testkit/src/main/scala/io/cloudstate/testkit/TestService.scala +++ b/testkit/src/main/scala/io/cloudstate/testkit/TestService.scala @@ -20,8 +20,10 @@ import akka.actor.ActorSystem import akka.http.scaladsl.Http import akka.testkit.{SocketUtil, TestKit, TestProbe} import com.typesafe.config.{Config, ConfigFactory} +import io.cloudstate.testkit.crud.TestCrudService import io.cloudstate.testkit.discovery.TestEntityDiscoveryService import io.cloudstate.testkit.eventsourced.TestEventSourcedService + import scala.concurrent.Await import scala.concurrent.duration._ @@ -36,11 +38,13 @@ final class TestService { val eventSourced = new TestEventSourcedService(context) + val crud = new TestCrudService(context) + import context.system Await.result( Http().bindAndHandleAsync( - handler = entityDiscovery.handler orElse eventSourced.handler, + handler = entityDiscovery.handler orElse eventSourced.handler orElse crud.handler, interface = "localhost", port = port ), diff --git a/testkit/src/main/scala/io/cloudstate/testkit/crud/TestCrudProtocol.scala b/testkit/src/main/scala/io/cloudstate/testkit/crud/TestCrudProtocol.scala new file mode 100644 index 000000000..7184d5ef5 --- /dev/null +++ b/testkit/src/main/scala/io/cloudstate/testkit/crud/TestCrudProtocol.scala @@ -0,0 +1,65 @@ +/* + * Copyright 2019 Lightbend Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.cloudstate.testkit.crud + +import akka.stream.scaladsl.Source +import akka.stream.testkit.TestPublisher +import akka.stream.testkit.scaladsl.TestSink +import io.cloudstate.protocol.crud.{CrudClient, CrudStreamIn, CrudStreamOut} +import io.cloudstate.testkit.TestProtocol.TestProtocolContext + +final class TestCrudProtocol(context: TestProtocolContext) { + private val client = CrudClient(context.clientSettings)(context.system) + + def connect(): TestCrudProtocol.Connection = new TestCrudProtocol.Connection(client, context) + + def terminate(): Unit = client.close() +} + +object TestCrudProtocol { + + final class Connection(client: CrudClient, context: TestProtocolContext) { + import context.system + + private val in = TestPublisher.probe[CrudStreamIn]() + private val out = client.handle(Source.fromPublisher(in)).runWith(TestSink.probe[CrudStreamOut]) + + out.ensureSubscription() + + def send(message: CrudStreamIn.Message): Connection = { + in.sendNext(CrudStreamIn(message)) + this + } + + def expect(message: CrudStreamOut.Message): Connection = { + out.request(1).expectNext(CrudStreamOut(message)) + this + } + + def expectClosed(): Unit = { + out.expectComplete() + in.expectCancellation() + } + + def passivate(): Unit = close() + + def close(): Unit = { + in.sendComplete() + out.expectComplete() + } + } +} diff --git a/testkit/src/main/scala/io/cloudstate/testkit/crud/TestCrudService.scala b/testkit/src/main/scala/io/cloudstate/testkit/crud/TestCrudService.scala index 22bfa7c1c..bfa5df86c 100644 --- a/testkit/src/main/scala/io/cloudstate/testkit/crud/TestCrudService.scala +++ b/testkit/src/main/scala/io/cloudstate/testkit/crud/TestCrudService.scala @@ -23,38 +23,34 @@ import akka.http.scaladsl.model.{HttpRequest, HttpResponse} import akka.stream.scaladsl.Source import akka.stream.testkit.TestPublisher import akka.stream.testkit.scaladsl.TestSink -import akka.testkit.TestProbe import com.google.protobuf.Descriptors.ServiceDescriptor import io.cloudstate.protocol.crud.{Crud, CrudHandler, CrudStreamIn, CrudStreamOut} import io.cloudstate.protocol.entity.EntitySpec -import io.cloudstate.testkit.TestService +import io.cloudstate.testkit.TestService.TestServiceContext +import io.cloudstate.testkit.discovery.TestEntityDiscoveryService import scala.concurrent.Future -class TestCrudService extends TestService { - private val crud = new TestCrudService.TestCrud(system, probe) +class TestCrudService(context: TestServiceContext) { + private val testCrud = new TestCrudService.TestCrud(context) - override protected def handler: PartialFunction[HttpRequest, Future[HttpResponse]] = - super.handler orElse CrudHandler.partial(crud) + def expectConnection(): TestCrudService.Connection = context.probe.expectMsgType[TestCrudService.Connection] - def expectConnection(): TestCrudService.Connection = probe.expectMsgType[TestCrudService.Connection] - - start() + def handler: PartialFunction[HttpRequest, Future[HttpResponse]] = + CrudHandler.partial(testCrud)(context.system) } object TestCrudService { - def apply(): TestCrudService = new TestCrudService - def entitySpec(service: ServiceDescription): EntitySpec = - TestService.entitySpec(Crud.name, service) + TestEntityDiscoveryService.entitySpec(Crud.name, service) def entitySpec(descriptors: Seq[ServiceDescriptor]): EntitySpec = - TestService.entitySpec(Crud.name, descriptors) + TestEntityDiscoveryService.entitySpec(Crud.name, descriptors) - final class TestCrud(system: ActorSystem, probe: TestProbe) extends Crud { + final class TestCrud(context: TestServiceContext) extends Crud { override def handle(source: Source[CrudStreamIn, NotUsed]): Source[CrudStreamOut, NotUsed] = { - val connection = new Connection(system, source) - probe.ref ! connection + val connection = new Connection(context.system, source) + context.probe.ref ! connection connection.outSource } } From 968c332b1b988024bf1860349dbb22133bf3d0fd Mon Sep 17 00:00:00 2001 From: Guy Youansi Date: Tue, 22 Sep 2020 19:06:47 +0200 Subject: [PATCH 53/93] fixed format error --- build.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index b09e3aa7c..e814230f2 100644 --- a/build.sbt +++ b/build.sbt @@ -410,7 +410,7 @@ lazy val `proxy-core` = (project in file("proxy/core")) "org.slf4j" % "slf4j-simple" % Slf4jSimpleVersion, //"ch.qos.logback" % "logback-classic" % "1.2.3", // Doesn't work well with SubstrateVM: https://github.com/vmencik/akka-graal-native/blob/master/README.md#logging "com.typesafe.slick" %% "slick" % SlickVersion, - "com.typesafe.slick" %% "slick-hikaricp" % SlickHikariVersion, + "com.typesafe.slick" %% "slick-hikaricp" % SlickHikariVersion ), PB.protoSources in Compile ++= { val baseDir = (baseDirectory in ThisBuild).value / "protocols" From 29cdd47e59dc2f847c10f3e0c29bf122aa3d5398 Mon Sep 17 00:00:00 2001 From: Guy Youansi Date: Wed, 23 Sep 2020 20:54:53 +0200 Subject: [PATCH 54/93] add test for the crud java support --- .../crud/AnnotationBasedCrudSupport.scala | 1 + .../javasupport/impl/crud/CrudImpl.scala | 13 +- .../javasupport/impl/crud/CrudImplSpec.scala | 304 ++++++++++++++++++ .../javasupport/impl/crud/TestCrud.scala | 55 ++++ .../proxy/crud/AbstractCrudEntitySpec.scala | 234 -------------- .../proxy/crud/CrudEntitySpec.scala | 188 ----------- .../testkit/crud/CrudMessages.scala | 66 +++- 7 files changed, 417 insertions(+), 444 deletions(-) create mode 100644 java-support/src/test/scala/io/cloudstate/javasupport/impl/crud/CrudImplSpec.scala create mode 100644 java-support/src/test/scala/io/cloudstate/javasupport/impl/crud/TestCrud.scala delete mode 100644 proxy/core/src/test/scala/io/cloudstate/proxy/crud/AbstractCrudEntitySpec.scala delete mode 100644 proxy/core/src/test/scala/io/cloudstate/proxy/crud/CrudEntitySpec.scala diff --git a/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/AnnotationBasedCrudSupport.scala b/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/AnnotationBasedCrudSupport.scala index 3dfe477d8..bab718f7f 100644 --- a/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/AnnotationBasedCrudSupport.scala +++ b/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/AnnotationBasedCrudSupport.scala @@ -76,6 +76,7 @@ private[impl] class AnnotationBasedCrudSupport( handler.invoke(entity, command, adaptedContext) } getOrElse { throw EntityException( + context, s"No command handler found for command [${context.commandName()}] on $behaviorsString" ) } diff --git a/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/CrudImpl.scala b/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/CrudImpl.scala index f49e53457..a2fc7ae03 100644 --- a/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/CrudImpl.scala +++ b/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/CrudImpl.scala @@ -97,7 +97,7 @@ final class CrudImpl(_system: ActorSystem, _services: Map[String, CrudStatefulService], rootContext: Context, configuration: Configuration) - extends io.cloudstate.protocol.crud.Crud { + extends Crud { private final val system = _system private final implicit val ec = system.dispatcher @@ -133,20 +133,20 @@ final class CrudImpl(_system: ActorSystem, private def runEntity(init: CrudInit): Flow[CrudStreamIn, CrudStreamOut, NotUsed] = { val service = - services.getOrElse(init.serviceName, throw new RuntimeException(s"Service not found: ${init.serviceName}")) + services.getOrElse(init.serviceName, throw ProtocolException(init, s"Service not found: ${init.serviceName}")) val handler = service.factory.create(new CrudContextImpl(init.entityId)) val thisEntityId = init.entityId val initState = init.state match { case Some(CrudInitState(state, _)) => state - case _ => None + case _ => None // should not happen!!! } Flow[CrudStreamIn] .map(_.message) .scan[(Option[ScalaPbAny], Option[CrudStreamOut.Message])]((initState, None)) { case (_, InCommand(command)) if thisEntityId != command.entityId => - throw ProtocolException(command, "Receiving entity is not the intended recipient for CRUD entity") + throw ProtocolException(command, "Receiving CRUD entity is not the intended recipient of command") case (_, InCommand(command)) if command.payload.isEmpty => throw ProtocolException(command, "No command payload for CRUD entity") @@ -167,7 +167,7 @@ final class CrudImpl(_system: ActorSystem, case FailInvoked => Option.empty[JavaPbAny].asJava case e: EntityException => throw e case NonFatal(error) => - throw EntityException(command, s"CRUD entity Unexpected failure: ${error.getMessage}") + throw EntityException(command, s"CRUD entity unexpected failure: ${error.getMessage}") } finally { context.deactivate() // Very important! } @@ -192,8 +192,7 @@ final class CrudImpl(_system: ActorSystem, OutReply( CrudReply( commandId = command.id, - clientAction = clientAction, - crudAction = context.action + clientAction = clientAction ) ) )) diff --git a/java-support/src/test/scala/io/cloudstate/javasupport/impl/crud/CrudImplSpec.scala b/java-support/src/test/scala/io/cloudstate/javasupport/impl/crud/CrudImplSpec.scala new file mode 100644 index 000000000..e29bde0e0 --- /dev/null +++ b/java-support/src/test/scala/io/cloudstate/javasupport/impl/crud/CrudImplSpec.scala @@ -0,0 +1,304 @@ +/* + * Copyright 2019 Lightbend Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.cloudstate.javasupport.impl.crud + +import java.util.Optional + +import com.google.protobuf.Empty +import io.cloudstate.javasupport.EntityId +import io.cloudstate.javasupport.crud.{CommandContext, CommandHandler, CrudEntity} +import io.cloudstate.testkit.TestProtocol +import io.cloudstate.testkit.crud.CrudMessages +import org.scalatest.{BeforeAndAfterAll, Matchers, WordSpec} + +import scala.collection.mutable +import scala.reflect.ClassTag + +class CrudImplSpec extends WordSpec with Matchers with BeforeAndAfterAll { + import CrudImplSpec._ + import CrudMessages._ + import ShoppingCart.Item + import ShoppingCart.Protocol._ + + val service: TestCrudService = ShoppingCart.testService + val protocol: TestProtocol = TestProtocol(service.port) + + override def afterAll(): Unit = { + protocol.terminate() + service.terminate() + } + + "CrudImpl" should { + "fail when first message is not init" in { + service.expectLogError("Terminating entity due to unexpected failure") { + val entity = protocol.crud.connect() + entity.send(command(1, "cart", "command")) + entity.expect(failure("Protocol error: Expected Init message for CRUD entity")) + entity.expectClosed() + } + } + + "fail when entity is sent multiple init" in { + service.expectLogError("Terminating entity [cart] due to unexpected failure") { + val entity = protocol.crud.connect() + entity.send(init(ShoppingCart.Name, "cart")) + entity.send(init(ShoppingCart.Name, "cart")) + entity.expect(failure("Protocol error: CRUD Entity already inited")) + entity.expectClosed() + } + } + + "fail when service doesn't exist" in { + service.expectLogError("Terminating entity [foo] due to unexpected failure") { + val entity = protocol.crud.connect() + entity.send(init(serviceName = "DoesNotExist", entityId = "foo")) + entity.expect(failure("Protocol error: Service not found: DoesNotExist")) + entity.expectClosed() + } + } + + "fail when command entity id is incorrect" in { + service.expectLogError("Terminating entity [cart2] due to unexpected failure for command [foo]") { + val entity = protocol.crud.connect() + entity.send(init(ShoppingCart.Name, "cart1")) + entity.send(command(1, "cart2", "foo")) + entity.expect(failure(1, "Protocol error: Receiving CRUD entity is not the intended recipient of command")) + entity.expectClosed() + } + } + + "fail when command payload is missing" in { + service.expectLogError("Terminating entity [cart] due to unexpected failure for command [foo]") { + val entity = protocol.crud.connect() + entity.send(init(ShoppingCart.Name, "cart")) + entity.send(command(1, "cart", "foo", payload = None)) + entity.expect(failure(1, "Protocol error: No command payload for CRUD entity")) + entity.expectClosed() + } + } + + "fail when entity is sent empty message" in { + service.expectLogError("Terminating entity [cart] due to unexpected failure") { + val entity = protocol.crud.connect() + entity.send(init(ShoppingCart.Name, "cart")) + entity.send(EmptyInMessage) + entity.expect(failure("Protocol error: CRUD entity received empty/unknown message")) + entity.expectClosed() + } + } + + "fail when command handler does not exist" in { + service.expectLogError("Terminating entity [cart] due to unexpected failure for command [foo]") { + val entity = protocol.crud.connect() + entity.send(init(ShoppingCart.Name, "cart")) + entity.send(command(1, "cart", "foo")) + entity.expect(failure(1, s"No command handler found for command [foo] on ${ShoppingCart.TestCartClass}")) + entity.expectClosed() + } + } + + "fail action when command handler uses context fail" in { + service.expectLogError( + "Fail invoked for command [AddItem] for CRUD entity [cart]: Cannot add negative quantity of item [foo]" + ) { + val entity = protocol.crud.connect() + entity.send(init(ShoppingCart.Name, "cart")) + entity.send(command(1, "cart", "AddItem", addItem("foo", "bar", -1))) + entity.expect(actionFailure(1, "Cannot add negative quantity of item [foo]")) + entity.send(command(2, "cart", "GetCart")) + entity.expect(reply(2, EmptyCart)) // check update-then-fail doesn't change entity state + + entity.passivate() + val reactivated = protocol.crud.connect() + reactivated.send(init(ShoppingCart.Name, "cart")) + reactivated.send(command(1, "cart", "GetCart")) + reactivated.expect(reply(1, EmptyCart)) + reactivated.passivate() + } + } + + "fail when command handler throws exception" in { + service.expectLogError("Terminating entity [cart] due to unexpected failure for command [RemoveItem]") { + val entity = protocol.crud.connect() + entity.send(init(ShoppingCart.Name, "cart")) + entity.send(command(1, "cart", "RemoveItem", removeItem("foo"))) + entity.expect(failure(1, "CRUD entity unexpected failure: Boom: foo")) + entity.expectClosed() + } + } + + "manage entities with expected update commands" in { + val entity = protocol.crud.connect() + entity.send(init(ShoppingCart.Name, "cart")) + entity.send(command(1, "cart", "GetCart")) + entity.expect(reply(1, EmptyCart)) + entity.send(command(2, "cart", "AddItem", addItem("abc", "apple", 1))) + entity.expect(reply(2, EmptyJavaMessage, update(domainCart(Item("abc", "apple", 1))))) + entity.send(command(3, "cart", "AddItem", addItem("abc", "apple", 2))) + entity.expect(reply(3, EmptyJavaMessage, update(domainCart(Item("abc", "apple", 3))))) + entity.send(command(4, "cart", "GetCart")) + entity.expect(reply(4, cart(Item("abc", "apple", 3)))) + entity.send(command(5, "cart", "AddItem", addItem("123", "banana", 4))) + entity.expect(reply(5, EmptyJavaMessage, update(domainCart(Item("abc", "apple", 3), Item("123", "banana", 4))))) + + entity.passivate() + val reactivated = protocol.crud.connect() + reactivated.send( + init(ShoppingCart.Name, "cart", state(domainCart(Item("abc", "apple", 3), Item("123", "banana", 4)))) + ) + reactivated.send(command(1, "cart", "AddItem", addItem("abc", "apple", 1))) + reactivated.expect( + reply(1, EmptyJavaMessage, update(domainCart(Item("abc", "apple", 4), Item("123", "banana", 4)))) + ) + reactivated.send(command(1, "cart", "GetCart")) + reactivated.expect(reply(1, cart(Item("abc", "apple", 4), Item("123", "banana", 4)))) + reactivated.passivate() + } + + "manage entities with expected delete commands" in { + val entity = protocol.crud.connect() + entity.send(init(ShoppingCart.Name, "cart")) + entity.send(command(1, "cart", "GetCart")) + entity.expect(reply(1, EmptyCart)) + entity.send(command(2, "cart", "AddItem", addItem("abc", "apple", 1))) + entity.expect(reply(2, EmptyJavaMessage, update(domainCart(Item("abc", "apple", 1))))) + entity.send(command(3, "cart", "AddItem", addItem("abc", "apple", 2))) + entity.expect(reply(3, EmptyJavaMessage, update(domainCart(Item("abc", "apple", 3))))) + entity.send(command(4, "cart", "RemoveCart", removeCart("cart"))) + entity.expect(reply(4, EmptyJavaMessage, delete())) + entity.send(command(5, "cart", "GetCart")) + entity.expect(reply(5, EmptyCart)) + entity.passivate() + } + } +} + +object CrudImplSpec { + object ShoppingCart { + + import com.example.crud.shoppingcart.Shoppingcart + import com.example.crud.shoppingcart.persistence.Domain + + val Name: String = Shoppingcart.getDescriptor.findServiceByName("ShoppingCart").getFullName + + def testService: TestCrudService = service[TestCart] + + def service[T: ClassTag]: TestCrudService = + TestCrud.service[T]( + Shoppingcart.getDescriptor.findServiceByName("ShoppingCart"), + Domain.getDescriptor + ) + + case class Item(id: String, name: String, quantity: Int) + + object Protocol { + import scala.jdk.CollectionConverters._ + + val EmptyCart: Shoppingcart.Cart = Shoppingcart.Cart.newBuilder.build + + def cart(items: Item*): Shoppingcart.Cart = + Shoppingcart.Cart.newBuilder.addAllItems(lineItems(items)).build + + def lineItems(items: Seq[Item]): java.lang.Iterable[Shoppingcart.LineItem] = + items.sortBy(_.id).map(item => lineItem(item.id, item.name, item.quantity)).asJava + + def lineItem(id: String, name: String, quantity: Int): Shoppingcart.LineItem = + Shoppingcart.LineItem.newBuilder.setProductId(id).setName(name).setQuantity(quantity).build + + def addItem(id: String, name: String, quantity: Int): Shoppingcart.AddLineItem = + Shoppingcart.AddLineItem.newBuilder.setProductId(id).setName(name).setQuantity(quantity).build + + def removeItem(id: String): Shoppingcart.RemoveLineItem = + Shoppingcart.RemoveLineItem.newBuilder.setProductId(id).build + + def removeCart(id: String): Shoppingcart.RemoveShoppingCart = + Shoppingcart.RemoveShoppingCart.newBuilder.setUserId(id).build + + def domainLineItems(items: Seq[Item]): java.lang.Iterable[Domain.LineItem] = + items.sortBy(_.id).map(item => domainLineItem(item.id, item.name, item.quantity)).asJava + + def domainLineItem(id: String, name: String, quantity: Int): Domain.LineItem = + Domain.LineItem.newBuilder.setProductId(id).setName(name).setQuantity(quantity).build + + def domainCart(items: Item*): Domain.Cart = + Domain.Cart.newBuilder.addAllItems(domainLineItems(items)).build + } + + val TestCartClass: Class[_] = classOf[TestCart] + + @CrudEntity(persistenceId = "crud-shopping-cart") + class TestCart(@EntityId val entityId: String) { + import scala.jdk.OptionConverters._ + import scala.jdk.CollectionConverters._ + + @CommandHandler + def getCart(ctx: CommandContext[Domain.Cart]): Shoppingcart.Cart = + ctx.getState.toScala + .map { c => + val items = c.getItemsList.asScala.map(i => Item(i.getProductId, i.getName, i.getQuantity)).toSeq + Protocol.cart(items: _*) + } + .getOrElse(Protocol.EmptyCart) + + @CommandHandler + def addItem(item: Shoppingcart.AddLineItem, ctx: CommandContext[Domain.Cart]): Empty = { + // update and then fail on negative quantities, for testing atomicity + val cart = updateCart(item, asMap(ctx.getState)) + val items = + cart.values + .map(i => Domain.LineItem.newBuilder().setProductId(i.id).setName(i.name).setQuantity(i.quantity).build) + ctx.updateState(Domain.Cart.newBuilder().addAllItems(items.toList.asJava).build()) + if (item.getQuantity <= 0) ctx.fail(s"Cannot add negative quantity of item [${item.getProductId}]") + Empty.getDefaultInstance + } + + @CommandHandler + def removeItem(item: Shoppingcart.RemoveLineItem, ctx: CommandContext[Domain.Cart]): Empty = { + if (true) throw new RuntimeException("Boom: " + item.getProductId) // always fail for testing + Empty.getDefaultInstance + } + + @CommandHandler + def removeCart(item: Shoppingcart.RemoveShoppingCart, ctx: CommandContext[Domain.Cart]): Empty = { + ctx.deleteState() + Empty.getDefaultInstance + } + + private def updateCart(item: Shoppingcart.AddLineItem, + cart: mutable.Map[String, Item]): mutable.Map[String, Item] = { + val currentQuantity = cart.get(item.getProductId).map(_.quantity).getOrElse(0) + cart.update( + item.getProductId, + Item(item.getProductId, item.getName, currentQuantity + item.getQuantity) + ) + cart + } + + private def asMap(cart: Optional[Domain.Cart]): mutable.Map[String, Item] = { + val map = cart.toScala match { + case Some(c) => + c.getItemsList.asScala + .map(i => i.getProductId -> Item(i.getProductId, i.getName, i.getQuantity)) + .toMap + case None => Map.empty + } + + mutable.Map(map.toSeq: _*) + } + } + } +} diff --git a/java-support/src/test/scala/io/cloudstate/javasupport/impl/crud/TestCrud.scala b/java-support/src/test/scala/io/cloudstate/javasupport/impl/crud/TestCrud.scala new file mode 100644 index 000000000..d34bb5fcd --- /dev/null +++ b/java-support/src/test/scala/io/cloudstate/javasupport/impl/crud/TestCrud.scala @@ -0,0 +1,55 @@ +/* + * Copyright 2019 Lightbend Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.cloudstate.javasupport.impl.crud + +import akka.testkit.{EventFilter, SocketUtil} +import com.google.protobuf.Descriptors.{FileDescriptor, ServiceDescriptor} +import com.typesafe.config.{Config, ConfigFactory} +import io.cloudstate.javasupport.{CloudState, CloudStateRunner} +import scala.reflect.ClassTag + +object TestCrud { + def service[T: ClassTag](descriptor: ServiceDescriptor, fileDescriptors: FileDescriptor*): TestCrudService = + new TestCrudService(implicitly[ClassTag[T]].runtimeClass, descriptor, fileDescriptors) +} + +class TestCrudService(entityClass: Class[_], descriptor: ServiceDescriptor, fileDescriptors: Seq[FileDescriptor]) { + val port: Int = SocketUtil.temporaryLocalPort() + + val config: Config = ConfigFactory.load(ConfigFactory.parseString(s""" + cloudstate.user-function-port = $port + akka { + loglevel = ERROR + loggers = ["akka.testkit.TestEventListener"] + http.server { + preview.enable-http2 = on + idle-timeout = infinite + } + } + """)) + + val runner: CloudStateRunner = new CloudState() + .registerCrudEntity(entityClass, descriptor, fileDescriptors: _*) + .createRunner(config) + + runner.run() + + def expectLogError[T](message: String)(block: => T): T = + EventFilter.error(message, occurrences = 1).intercept(block)(runner.system) + + def terminate(): Unit = runner.terminate() +} diff --git a/proxy/core/src/test/scala/io/cloudstate/proxy/crud/AbstractCrudEntitySpec.scala b/proxy/core/src/test/scala/io/cloudstate/proxy/crud/AbstractCrudEntitySpec.scala deleted file mode 100644 index 2f5381ab2..000000000 --- a/proxy/core/src/test/scala/io/cloudstate/proxy/crud/AbstractCrudEntitySpec.scala +++ /dev/null @@ -1,234 +0,0 @@ -/* - * Copyright 2019 Lightbend Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.cloudstate.proxy.crud - -import akka.actor.{ActorRef, ActorSystem, PoisonPill} -import akka.testkit.{ImplicitSender, TestKit, TestProbe} -import com.google.protobuf.ByteString -import com.google.protobuf.any.{Any => ProtoAny} -import com.typesafe.config.{Config, ConfigFactory} -import io.cloudstate.protocol.crud._ -import io.cloudstate.protocol.entity.{ClientAction, Command, Failure} -import io.cloudstate.proxy.ConcurrencyEnforcer -import io.cloudstate.proxy.ConcurrencyEnforcer.ConcurrencyEnforcerSettings -import io.cloudstate.proxy.entity.{EntityCommand, UserFunctionReply} -import org.scalatest._ -import org.scalatest.concurrent.Eventually -import org.scalatest.time.{Millis, Seconds, Span} - -import scala.concurrent.Await -import scala.concurrent.duration._ - -object AbstractCrudEntitySpec { - - def config: Config = - ConfigFactory - .parseString(""" - | # use in-memory journal for testing - | cloudstate.proxy.journal-enabled = true - | akka.persistence { - | journal.plugin = "akka.persistence.journal.inmem" - | snapshot-store.plugin = inmem-snapshot-store - | } - | inmem-snapshot-store.class = "io.cloudstate.proxy.crud.InMemSnapshotStore" - """.stripMargin) - - final val ServiceName = "some.ServiceName" - final val UserFunctionName = "crud-user-function-name" - - // Some useful anys - final val command = ProtoAny("command", ByteString.copyFromUtf8("foo")) - final val state1 = ProtoAny("state", ByteString.copyFromUtf8("state1")) - final val state2 = ProtoAny("state", ByteString.copyFromUtf8("state2")) - -} - -abstract class AbstractCrudEntitySpec - extends TestKit(ActorSystem("CrudEntityTest", AbstractCrudEntitySpec.config)) - with WordSpecLike - with Matchers - with Inside - with Eventually - with BeforeAndAfter - with BeforeAndAfterAll - with ImplicitSender - with OptionValues { - - import AbstractCrudEntitySpec._ - - // These are set and read in the entities - @volatile protected var userFunction: TestProbe = _ - @volatile private var statsCollector: TestProbe = _ - @volatile private var concurrencyEnforcer: ActorRef = _ - - protected var entity: ActorRef = _ - protected var reactivatedEntity: ActorRef = _ - - // Incremented for each test by before() callback - private var idSeq = 0 - - override implicit def patienceConfig: PatienceConfig = PatienceConfig(Span(5, Seconds), Span(200, Millis)) - - protected def entityId: String = "entity" + idSeq.toString - - protected def sendAndExpectCommand(name: String, payload: ProtoAny, dest: ActorRef = entity): Long = { - dest ! EntityCommand(entityId, name, Some(payload)) - expectCommand(name, payload) - } - - private def expectCommand(name: String, payload: ProtoAny): Long = - inside(userFunction.expectMsgType[CrudStreamIn].message) { - case CrudStreamIn.Message.Command(Command(eid, cid, n, p, s, _, _)) => - eid should ===(entityId) - n should ===(name) - p shouldBe Some(payload) - s shouldBe false - cid - } - - protected def sendAndExpectReply(commandId: Long, - action: Option[CrudAction.Action] = None, - dest: ActorRef = entity): UserFunctionReply = { - sendReply(commandId, action, dest) - val reply = expectMsgType[UserFunctionReply] - reply.clientAction shouldBe None - reply - } - - protected def sendReply(commandId: Long, action: Option[CrudAction.Action] = None, dest: ActorRef = entity) = - dest ! CrudStreamOut( - CrudStreamOut.Message.Reply( - CrudReply( - commandId = commandId, - sideEffects = Nil, - clientAction = None, - crudAction = action.map(a => CrudAction(a)) - ) - ) - ) - - protected def sendAndExpectFailure(commandId: Long, description: String): UserFunctionReply = { - sendFailure(commandId, description) - val reply = expectMsgType[UserFunctionReply] - inside(reply.clientAction) { - case Some(ClientAction(ClientAction.Action.Failure(failure), _)) => - failure should ===(Failure(0, description)) - } - reply - } - - protected def sendFailure(commandId: Long, description: String) = - entity ! CrudStreamOut( - CrudStreamOut.Message.Failure( - Failure( - commandId = commandId, - description = description - ) - ) - ) - - protected def createAndExpectInitState(initState: Option[CrudInitState]): Unit = { - userFunction = TestProbe() - entity = system.actorOf( - CrudEntity.props( - CrudEntity.Configuration(ServiceName, UserFunctionName, 30.seconds, 100), - entityId, - userFunction.ref, - concurrencyEnforcer, - statsCollector.ref, - null - ), - s"crud-test-entity-$entityId" - ) - - val init = userFunction.expectMsgType[CrudStreamIn] - inside(init.message) { - case CrudStreamIn.Message.Init(CrudInit(serviceName, eid, state, _)) => - serviceName should ===(ServiceName) - eid should ===(entityId) - state should ===(initState) - } - } - - protected def reactiveAndExpectInitState(initState: Option[CrudInitState]): Unit = { - cleanUpEntity() - - userFunction = TestProbe() - reactivatedEntity = system.actorOf( - CrudEntity.props( - CrudEntity.Configuration(ServiceName, UserFunctionName, 30.seconds, 100), - entityId, - userFunction.ref, - concurrencyEnforcer, - statsCollector.ref, - null - ), - s"crud-test-entity-reactivated-$entityId" - ) - - val init = userFunction.expectMsgType[CrudStreamIn] - inside(init.message) { - case CrudStreamIn.Message.Init(CrudInit(serviceName, eid, state, _)) => - serviceName should ===(ServiceName) - eid should ===(entityId) - state should ===(initState) - } - } - - private def cleanUpEntity(): Unit = { - userFunction.testActor ! PoisonPill - userFunction = null - entity ! PoisonPill - entity = null - } - - before { - idSeq += 1 - } - - after { - if (entity != null) { - cleanUpEntity() - } - - if (reactivatedEntity != null) { - userFunction.testActor ! PoisonPill - userFunction = null - reactivatedEntity ! PoisonPill - reactivatedEntity = null - } - } - - override protected def beforeAll(): Unit = { - statsCollector = TestProbe() - concurrencyEnforcer = system.actorOf( - ConcurrencyEnforcer.props(ConcurrencyEnforcerSettings(1, 10.second, 5.second), statsCollector.ref), - "concurrency-enforcer" - ) - } - - override protected def afterAll(): Unit = { - statsCollector.testActor ! PoisonPill - statsCollector = null - concurrencyEnforcer ! PoisonPill - concurrencyEnforcer = null - - Await.ready(system.terminate(), 10.seconds) - shutdown() - } - -} diff --git a/proxy/core/src/test/scala/io/cloudstate/proxy/crud/CrudEntitySpec.scala b/proxy/core/src/test/scala/io/cloudstate/proxy/crud/CrudEntitySpec.scala deleted file mode 100644 index 9b26c6955..000000000 --- a/proxy/core/src/test/scala/io/cloudstate/proxy/crud/CrudEntitySpec.scala +++ /dev/null @@ -1,188 +0,0 @@ -/* - * Copyright 2019 Lightbend Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.cloudstate.proxy.crud - -import io.cloudstate.protocol.crud.CrudAction.Action.{Delete, Update} -import io.cloudstate.protocol.crud._ -import io.cloudstate.protocol.entity.{ClientAction, Failure} -import io.cloudstate.proxy.crud.CrudEntity.{Stop, StreamClosed, StreamFailed} -import io.cloudstate.proxy.entity.{EntityCommand, UserFunctionReply} -import org.scalatest.Ignore - -import scala.concurrent.duration._ - -@Ignore -class CrudEntitySpec extends AbstractCrudEntitySpec { - - import AbstractCrudEntitySpec._ - - "The CrudEntity" should { - - "be initialised successfully" in { - createAndExpectInitState(Some(CrudInitState())) - userFunction.expectNoMessage(200.millis) - expectNoMessage(200.millis) - } - - "handle update commands and reply" in { - createAndExpectInitState(Some(CrudInitState())) - watch(entity) - - val commandId1 = sendAndExpectCommand("cmd", command) - sendAndExpectReply(commandId1, Some(Update(CrudUpdate(Some(state1))))) - - val commandId2 = sendAndExpectCommand("cmd", command) - sendAndExpectReply(commandId2, Some(Update(CrudUpdate(Some(state2))))) - - // passivating the entity - entity ! Stop - expectTerminated(entity) - - // reactivating the entity - reactiveAndExpectInitState(Some(CrudInitState(Some(state2)))) - - val commandId3 = sendAndExpectCommand("cmd", command, reactivatedEntity) - sendAndExpectReply(commandId3, Some(Update(CrudUpdate(Some(state2)))), reactivatedEntity) - - userFunction.expectNoMessage(200.millis) - expectNoMessage(200.millis) - } - - "handle delete command and reply" in { - createAndExpectInitState(Some(CrudInitState())) - watch(entity) - - val commandId1 = sendAndExpectCommand("cmd", command) - sendAndExpectReply(commandId1, Some(Update(CrudUpdate(Some(state1))))) - - val commandId2 = sendAndExpectCommand("cmd", command) - sendAndExpectReply(commandId2, Some(Delete(CrudDelete()))) - - // passivating the entity - entity ! Stop - expectTerminated(entity) - - // reactivating the entity - reactiveAndExpectInitState(Some(CrudInitState(None))) - - userFunction.expectNoMessage(200.millis) - expectNoMessage(200.millis) - } - - "handle failure reply" in { - createAndExpectInitState(Some(CrudInitState())) - val cid = sendAndExpectCommand("cmd", command) - sendAndExpectFailure(cid, "description") - userFunction.expectNoMessage(200.millis) - expectNoMessage(200.millis) - } - - "stash commands when another command is being executed" in { - createAndExpectInitState(Some(CrudInitState())) - sendAndExpectCommand("cmd", command) - entity ! EntityCommand(entityId, "cmd", Some(command)) - userFunction.expectNoMessage(200.millis) - expectNoMessage(200.millis) - } - - "crash when there is no command being executed for the reply" in { - createAndExpectInitState(Some(CrudInitState())) - sendReply(-1, None) - // expect crash and restart - userFunction.expectMsgType[CrudStreamIn] - userFunction.expectNoMessage(200.millis) - expectNoMessage(200.millis) - } - - "crash when the command being executed does not have the same command id as the one of the reply" in { - createAndExpectInitState(Some(CrudInitState())) - sendAndExpectCommand("cmd", command) - // send reply with wrong command id - sendReply(-1, None) - - inside(expectMsgType[UserFunctionReply].clientAction) { - case Some(ClientAction(ClientAction.Action.Failure(failure), _)) => - failure should ===(Failure(0, "Incorrect command id in reply, expecting 1 but got -1")) - } - expectNoMessage(200.millis) - } - - "crash on failure reply with command id 0" in { - createAndExpectInitState(Some(CrudInitState())) - sendFailure(0, "description") - expectNoMessage(200.millis) - } - - "crash on failure reply when there is no command being executed" in { - createAndExpectInitState(Some(CrudInitState())) - sendFailure(1, "description") - expectNoMessage(200.millis) - } - - "crash on failure reply when the command being executed does not have the same command id as the one of the reply" in { - createAndExpectInitState(Some(CrudInitState())) - sendAndExpectCommand("cmd", command) - //entity ! EntityCommand(entityId, "cmd", Some(command)) - sendFailure(-1, "description") - inside(expectMsgType[UserFunctionReply].clientAction) { - case Some(ClientAction(ClientAction.Action.Failure(failure), _)) => - failure should ===(Failure(0, "Incorrect command id in failure, expecting 1 but got -1")) - } - expectNoMessage(200.millis) - } - - "crash when output message is empty and no command is being executed" in { - createAndExpectInitState(Some(CrudInitState())) - entity ! CrudStreamOut(CrudStreamOut.Message.Empty) - expectNoMessage(200.millis) - } - - "crash when stream out message is empty and a command is being executed" in { - createAndExpectInitState(Some(CrudInitState())) - sendAndExpectCommand("cmd", command) - entity ! CrudStreamOut(CrudStreamOut.Message.Empty) - inside(expectMsgType[UserFunctionReply].clientAction) { - case Some(ClientAction(ClientAction.Action.Failure(failure), _)) => - failure should ===(Failure(0, "Empty or unknown message from entity output stream")) - } - expectNoMessage(200.millis) - } - - "stop when received StreamClosed message" in { - createAndExpectInitState(Some(CrudInitState())) - watch(entity) - - sendAndExpectCommand("cmd", command) - entity ! StreamClosed - inside(expectMsgType[UserFunctionReply].clientAction) { - case Some(ClientAction(ClientAction.Action.Failure(failure), _)) => - failure should ===(Failure(0, "Unexpected CRUD entity termination")) - } - expectTerminated(entity) - } - - "handle StreamFailed message" in { - createAndExpectInitState(Some(CrudInitState())) - sendAndExpectCommand("cmd", command) - entity ! StreamFailed(new RuntimeException("test stream failed")) - inside(expectMsgType[UserFunctionReply].clientAction) { - case Some(ClientAction(ClientAction.Action.Failure(failure), _)) => - failure should ===(Failure(0, "Unexpected CRUD entity termination")) - } - } - } -} diff --git a/testkit/src/main/scala/io/cloudstate/testkit/crud/CrudMessages.scala b/testkit/src/main/scala/io/cloudstate/testkit/crud/CrudMessages.scala index d46d1ddea..5088c0ba5 100644 --- a/testkit/src/main/scala/io/cloudstate/testkit/crud/CrudMessages.scala +++ b/testkit/src/main/scala/io/cloudstate/testkit/crud/CrudMessages.scala @@ -18,6 +18,8 @@ package io.cloudstate.testkit.crud import com.google.protobuf.any.{Any => ScalaPbAny} import com.google.protobuf.{Empty => JavaPbEmpty, Message => JavaPbMessage} +import io.cloudstate.protocol.crud.CrudAction.Action.Update +import io.cloudstate.protocol.crud.CrudAction.Action.Delete import io.cloudstate.protocol.entity._ import io.cloudstate.protocol.crud._ import scalapb.{GeneratedMessage => ScalaPbMessage} @@ -26,12 +28,28 @@ object CrudMessages { import CrudStreamIn.{Message => InMessage} import CrudStreamOut.{Message => OutMessage} - val EmptyMessage: InMessage = InMessage.Empty - val EmptyPayload: JavaPbMessage = JavaPbEmpty.getDefaultInstance - val EmptyAny: ScalaPbAny = protobufAny(EmptyPayload) + case class Effects(sideEffects: Seq[SideEffect] = Seq.empty, crudAction: Option[CrudAction] = None) { + + def withUpdateAction(message: JavaPbMessage): Effects = + copy(crudAction = Some(CrudAction(Update(CrudUpdate(messagePayload(message)))))) + + def withUpdateAction(message: ScalaPbMessage): Effects = + copy(crudAction = Some(CrudAction(Update(CrudUpdate(messagePayload(message)))))) + + def withDeleteAction(): Effects = + copy(crudAction = Some(CrudAction(Delete(CrudDelete())))) + } + + object Effects { + val empty: Effects = Effects() + } + + val EmptyInMessage: InMessage = InMessage.Empty + val EmptyJavaMessage: JavaPbMessage = JavaPbEmpty.getDefaultInstance + val EmptyScalaMessage: ScalaPbAny = protobufAny(EmptyJavaMessage) def init(serviceName: String, entityId: String): InMessage = - init(serviceName, entityId, CrudInitState()) + init(serviceName, entityId, Some(CrudInitState())) def init(serviceName: String, entityId: String, state: CrudInitState): InMessage = init(serviceName, entityId, Some(state)) @@ -39,14 +57,14 @@ object CrudMessages { def init(serviceName: String, entityId: String, state: Option[CrudInitState]): InMessage = InMessage.Init(CrudInit(serviceName, entityId, state)) - def state(payload: JavaPbMessage): Option[ScalaPbAny] = - messagePayload(payload) + def state(payload: JavaPbMessage): CrudInitState = + CrudInitState(messagePayload(payload)) - def state(payload: ScalaPbMessage): Option[ScalaPbAny] = - messagePayload(payload) + def state(payload: ScalaPbMessage): CrudInitState = + CrudInitState(messagePayload(payload)) def command(id: Long, entityId: String, name: String): InMessage = - command(id, entityId, name, EmptyPayload) + command(id, entityId, name, EmptyJavaMessage) def command(id: Long, entityId: String, name: String, payload: JavaPbMessage): InMessage = command(id, entityId, name, messagePayload(payload)) @@ -57,14 +75,23 @@ object CrudMessages { def command(id: Long, entityId: String, name: String, payload: Option[ScalaPbAny]): InMessage = InMessage.Command(Command(entityId, id, name, payload)) - def reply(id: Long, payload: JavaPbMessage, action: CrudAction): OutMessage = - reply(id, messagePayload(payload), Some(action)) + def reply(id: Long, payload: JavaPbMessage): OutMessage = + reply(id, messagePayload(payload), None) + + def reply(id: Long, payload: JavaPbMessage, effects: Effects): OutMessage = + reply(id, messagePayload(payload), effects) - def reply(id: Long, payload: ScalaPbMessage, action: CrudAction): OutMessage = - reply(id, messagePayload(payload), Some(action)) + def reply(id: Long, payload: ScalaPbMessage): OutMessage = + reply(id, messagePayload(payload), None) - def reply(id: Long, payload: Option[ScalaPbAny], action: Option[CrudAction]): OutMessage = - OutMessage.Reply(CrudReply(id, clientActionReply(payload), Seq.empty, action)) + def reply(id: Long, payload: ScalaPbMessage, effects: Effects): OutMessage = + reply(id, messagePayload(payload), effects) + + def reply(id: Long, payload: Option[ScalaPbAny], crudAction: Option[CrudAction]): OutMessage = + OutMessage.Reply(CrudReply(id, clientActionReply(payload), Seq.empty, crudAction)) + + def reply(id: Long, payload: Option[ScalaPbAny], effects: Effects): OutMessage = + OutMessage.Reply(CrudReply(id, clientActionReply(payload), effects.sideEffects, effects.crudAction)) def actionFailure(id: Long, description: String): OutMessage = OutMessage.Reply(CrudReply(id, clientActionFailure(id, description))) @@ -84,6 +111,15 @@ object CrudMessages { def clientActionFailure(id: Long, description: String): Option[ClientAction] = Some(ClientAction(ClientAction.Action.Failure(Failure(id, description)))) + def update(state: JavaPbMessage): Effects = + Effects.empty.withUpdateAction(state) + + def update(state: ScalaPbMessage): Effects = + Effects.empty.withUpdateAction(state) + + def delete(): Effects = + Effects.empty.withDeleteAction() + def messagePayload(message: JavaPbMessage): Option[ScalaPbAny] = Option(message).map(protobufAny) From 5cf66b7b1f47974d6206c93711eff25e007207da Mon Sep 17 00:00:00 2001 From: Guy Youansi Date: Wed, 23 Sep 2020 22:37:02 +0200 Subject: [PATCH 55/93] remove comments and logs --- .../shoppingcart/ShoppingCartEntity.java | 64 ------------------- 1 file changed, 64 deletions(-) diff --git a/samples/java-crud-shopping-cart/src/main/java/io/cloudstate/samples/shoppingcart/ShoppingCartEntity.java b/samples/java-crud-shopping-cart/src/main/java/io/cloudstate/samples/shoppingcart/ShoppingCartEntity.java index 5fffdb444..1e8350012 100644 --- a/samples/java-crud-shopping-cart/src/main/java/io/cloudstate/samples/shoppingcart/ShoppingCartEntity.java +++ b/samples/java-crud-shopping-cart/src/main/java/io/cloudstate/samples/shoppingcart/ShoppingCartEntity.java @@ -23,8 +23,6 @@ import io.cloudstate.javasupport.crud.CommandContext; import io.cloudstate.javasupport.crud.CommandHandler; import io.cloudstate.javasupport.crud.CrudEntity; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import java.util.List; import java.util.Optional; @@ -35,7 +33,6 @@ @CrudEntity public class ShoppingCartEntity { - private final Logger logger = LoggerFactory.getLogger(ShoppingCartEntity.class); private final String entityId; public ShoppingCartEntity(@EntityId String entityId) { @@ -44,20 +41,6 @@ public ShoppingCartEntity(@EntityId String entityId) { @CommandHandler public Shoppingcart.Cart getCart(CommandContext ctx) { - logger.info("getCart called"); - ctx.getState() - .ifPresent( - c -> { - c.getItemsList() - .forEach( - lineItem -> - logger.info( - "getCart called cart line item name - " - + lineItem.getName() - + ", id - " - + lineItem.getProductId())); - }); - // access the state by calling ctx.getState() Domain.Cart cart = ctx.getState().orElse(Domain.Cart.newBuilder().build()); List allItems = cart.getItemsList().stream().map(this::convert).collect(Collectors.toList()); @@ -66,53 +49,19 @@ public Shoppingcart.Cart getCart(CommandContext ctx) { @CommandHandler public Empty addItem(Shoppingcart.AddLineItem item, CommandContext ctx) { - logger.info( - "addItem called cart AddLineItem name - " - + item.getName() - + ", id - " - + item.getProductId()); if (item.getQuantity() <= 0) { ctx.fail("Cannot add negative quantity of to item " + item.getProductId()); } - // access the state by calling ctx.getState() Domain.Cart cart = ctx.getState().orElse(Domain.Cart.newBuilder().build()); - ctx.getState() - .ifPresent( - c -> { - c.getItemsList() - .forEach( - lineItem -> - logger.info( - "addItem called cart line item name - " - + lineItem.getName() - + ", id - " - + lineItem.getProductId())); - }); - logger.info("addItem called lineItemStream"); Domain.LineItem lineItem = updateItem(item, cart); - - logger.info( - "addItem called lineItem name - " - + lineItem.getName() - + " id - " - + lineItem.getProductId() - + " quantity - " - + lineItem.getQuantity()); List lineItems = removeItemByProductId(cart, item.getProductId()); - - logger.info("addItem called updateEntity"); - // update the state by calling ctx.updateState(...) - // multiple invocations of ctx.updateState(...) and ctx.deleteState() are not allowed - ctx.updateState(Domain.Cart.newBuilder().addAllItems(lineItems).addItems(lineItem).build()); ctx.updateState(Domain.Cart.newBuilder().addAllItems(lineItems).addItems(lineItem).build()); - logger.info("addItem called after updateEntity"); return Empty.getDefaultInstance(); } @CommandHandler public Empty removeItem(Shoppingcart.RemoveLineItem item, CommandContext ctx) { - // access the state by calling ctx.getState() Domain.Cart cart = ctx.getState().orElse(Domain.Cart.newBuilder().build()); Optional lineItem = findItemByProductId(cart, item.getProductId()); @@ -121,9 +70,6 @@ public Empty removeItem(Shoppingcart.RemoveLineItem item, CommandContext items = removeItemByProductId(cart, item.getProductId()); - - // update the state by calling ctx.updateState(...) - // multiple invocations of ctx.updateState(...) and ctx.deleteState() are not allowed ctx.updateState(Domain.Cart.newBuilder().addAllItems(items).build()); return Empty.getDefaultInstance(); } @@ -134,9 +80,6 @@ public Empty removeCart( if (!entityId.equals(cartItem.getUserId())) { ctx.fail("Cannot remove unknown cart " + cartItem.getUserId()); } - - // delete the state by calling ctx.deleteState() - // multiple invocations of ctx.updateState(...) and ctx.deleteState() are not allowed ctx.deleteState(); return Empty.getDefaultInstance(); } @@ -175,11 +118,4 @@ private Shoppingcart.LineItem convert(Domain.LineItem item) { .build(); } - private Domain.LineItem convert(Shoppingcart.LineItem item) { - return Domain.LineItem.newBuilder() - .setProductId(item.getProductId()) - .setName(item.getName()) - .setQuantity(item.getQuantity()) - .build(); - } } From bb98d718a3e23eea0c8ddefe4129b27c8b2ed31d Mon Sep 17 00:00:00 2001 From: Guy Youansi Date: Wed, 23 Sep 2020 23:25:38 +0200 Subject: [PATCH 56/93] changed java 11 for crud sample and fixed conflicting package name --- build.sbt | 4 ++-- .../io/cloudstate/samples/{ => crud}/shoppingcart/Main.java | 2 +- .../samples/{ => crud}/shoppingcart/ShoppingCartEntity.java | 3 +-- 3 files changed, 4 insertions(+), 5 deletions(-) rename samples/java-crud-shopping-cart/src/main/java/io/cloudstate/samples/{ => crud}/shoppingcart/Main.java (95%) rename samples/java-crud-shopping-cart/src/main/java/io/cloudstate/samples/{ => crud}/shoppingcart/ShoppingCartEntity.java (98%) diff --git a/build.sbt b/build.sbt index e814230f2..f5f541a4a 100644 --- a/build.sbt +++ b/build.sbt @@ -680,7 +680,7 @@ lazy val `java-crud-shopping-cart` = (project in file("samples/java-crud-shoppin .settings( name := "java-crud-shopping-cart", dockerSettings, - mainClass in Compile := Some("io.cloudstate.samples.shoppingcart.Main"), + mainClass in Compile := Some("io.cloudstate.samples.crud.shoppingcart.Main"), PB.generate in Compile := (PB.generate in Compile).dependsOn(PB.generate in (`java-support`, Compile)).value, akkaGrpcGeneratedLanguages := Seq(AkkaGrpc.Java), PB.protoSources in Compile ++= { @@ -690,7 +690,7 @@ lazy val `java-crud-shopping-cart` = (project in file("samples/java-crud-shoppin PB.targets in Compile := Seq( PB.gens.java -> (sourceManaged in Compile).value ), - javacOptions in Compile ++= Seq("-encoding", "UTF-8", "-source", "1.8", "-target", "1.8"), + javacOptions in Compile ++= Seq("-encoding", "UTF-8", "-source", "11", "-target", "11"), assemblySettings("java-crud-shopping-cart.jar") ) diff --git a/samples/java-crud-shopping-cart/src/main/java/io/cloudstate/samples/shoppingcart/Main.java b/samples/java-crud-shopping-cart/src/main/java/io/cloudstate/samples/crud/shoppingcart/Main.java similarity index 95% rename from samples/java-crud-shopping-cart/src/main/java/io/cloudstate/samples/shoppingcart/Main.java rename to samples/java-crud-shopping-cart/src/main/java/io/cloudstate/samples/crud/shoppingcart/Main.java index 928a41c63..940ef0583 100644 --- a/samples/java-crud-shopping-cart/src/main/java/io/cloudstate/samples/shoppingcart/Main.java +++ b/samples/java-crud-shopping-cart/src/main/java/io/cloudstate/samples/crud/shoppingcart/Main.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.cloudstate.samples.shoppingcart; +package io.cloudstate.samples.crud.shoppingcart; import com.example.crud.shoppingcart.Shoppingcart; import io.cloudstate.javasupport.CloudState; diff --git a/samples/java-crud-shopping-cart/src/main/java/io/cloudstate/samples/shoppingcart/ShoppingCartEntity.java b/samples/java-crud-shopping-cart/src/main/java/io/cloudstate/samples/crud/shoppingcart/ShoppingCartEntity.java similarity index 98% rename from samples/java-crud-shopping-cart/src/main/java/io/cloudstate/samples/shoppingcart/ShoppingCartEntity.java rename to samples/java-crud-shopping-cart/src/main/java/io/cloudstate/samples/crud/shoppingcart/ShoppingCartEntity.java index 1e8350012..928a55d85 100644 --- a/samples/java-crud-shopping-cart/src/main/java/io/cloudstate/samples/shoppingcart/ShoppingCartEntity.java +++ b/samples/java-crud-shopping-cart/src/main/java/io/cloudstate/samples/crud/shoppingcart/ShoppingCartEntity.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.cloudstate.samples.shoppingcart; +package io.cloudstate.samples.crud.shoppingcart; import com.example.crud.shoppingcart.Shoppingcart; import com.example.crud.shoppingcart.persistence.Domain; @@ -117,5 +117,4 @@ private Shoppingcart.LineItem convert(Domain.LineItem item) { .setQuantity(item.getQuantity()) .build(); } - } From 8acd3c93d56ebba8d16fd427f81cb15cc689b1a7 Mon Sep 17 00:00:00 2001 From: Guy Youansi Date: Sat, 26 Sep 2020 16:08:58 +0200 Subject: [PATCH 57/93] remove concurrency enforcer --- build.sbt | 4 +- .../proxy/EntityDiscoveryManager.scala | 6 +- .../io/cloudstate/proxy/crud/CrudEntity.scala | 69 +++---------------- .../proxy/crud/CrudSupportFactory.scala | 7 +- 4 files changed, 14 insertions(+), 72 deletions(-) diff --git a/build.sbt b/build.sbt index f5f541a4a..b2277fc11 100644 --- a/build.sbt +++ b/build.sbt @@ -106,8 +106,8 @@ headerSources in Compile ++= { lazy val root = (project in file(".")) .enablePlugins(NoPublish) -// Don't forget to add your sbt module here! -// A missing module here can lead to failing Travis test results + // Don't forget to add your sbt module here! + // A missing module here can lead to failing Travis test results .aggregate( `protocols`, `proxy`, diff --git a/proxy/core/src/main/scala/io/cloudstate/proxy/EntityDiscoveryManager.scala b/proxy/core/src/main/scala/io/cloudstate/proxy/EntityDiscoveryManager.scala index 9f7e4d600..e3c8dfeff 100644 --- a/proxy/core/src/main/scala/io/cloudstate/proxy/EntityDiscoveryManager.scala +++ b/proxy/core/src/main/scala/io/cloudstate/proxy/EntityDiscoveryManager.scala @@ -180,11 +180,7 @@ class EntityDiscoveryManager(config: EntityDiscoveryManager.Configuration)( } ++ { if (config.crudEnabled) Map( - Crud.name -> new CrudSupportFactory(system, - config, - clientSettings, - concurrencyEnforcer = concurrencyEnforcer, - statsCollector = statsCollector) + Crud.name -> new CrudSupportFactory(system, config, clientSettings) ) else Map.empty } diff --git a/proxy/core/src/main/scala/io/cloudstate/proxy/crud/CrudEntity.scala b/proxy/core/src/main/scala/io/cloudstate/proxy/crud/CrudEntity.scala index 2a725585d..af6e01a76 100644 --- a/proxy/core/src/main/scala/io/cloudstate/proxy/crud/CrudEntity.scala +++ b/proxy/core/src/main/scala/io/cloudstate/proxy/crud/CrudEntity.scala @@ -39,8 +39,6 @@ import io.cloudstate.protocol.crud.{ CrudUpdate } import io.cloudstate.protocol.entity._ -import io.cloudstate.proxy.ConcurrencyEnforcer.{Action, ActionCompleted} -import io.cloudstate.proxy.StatsCollector import io.cloudstate.proxy.crud.store.JdbcRepository import io.cloudstate.proxy.crud.store.JdbcStore.Key import io.cloudstate.proxy.entity.{EntityCommand, UserFunctionReply} @@ -52,12 +50,10 @@ object CrudEntitySupervisor { private final case class Relay(actorRef: ActorRef) - def props(client: CrudClient, - configuration: CrudEntity.Configuration, - concurrencyEnforcer: ActorRef, - statsCollector: ActorRef, - repository: JdbcRepository)(implicit mat: Materializer): Props = - Props(new CrudEntitySupervisor(client, configuration, concurrencyEnforcer, statsCollector, repository)) + def props(client: CrudClient, configuration: CrudEntity.Configuration, repository: JdbcRepository)( + implicit mat: Materializer + ): Props = + Props(new CrudEntitySupervisor(client, configuration, repository)) } /** @@ -72,8 +68,6 @@ object CrudEntitySupervisor { */ final class CrudEntitySupervisor(client: CrudClient, configuration: CrudEntity.Configuration, - concurrencyEnforcer: ActorRef, - statsCollector: ActorRef, repository: JdbcRepository)(implicit mat: Materializer) extends Actor with Stash { @@ -104,8 +98,7 @@ final class CrudEntitySupervisor(client: CrudClient, val entityId = URLDecoder.decode(self.path.name, "utf-8") val manager = context.watch( context - .actorOf(CrudEntity.props(configuration, entityId, relayRef, concurrencyEnforcer, statsCollector, repository), - "entity") + .actorOf(CrudEntity.props(configuration, entityId, relayRef, repository), "entity") ) context.become(forwarding(manager, relayRef)) unstashAll() @@ -171,13 +164,8 @@ object CrudEntity { private case object SaveStateSuccess private case object AlreadyInitialized - final def props(configuration: Configuration, - entityId: String, - relay: ActorRef, - concurrencyEnforcer: ActorRef, - statsCollector: ActorRef, - repository: JdbcRepository): Props = - Props(new CrudEntity(configuration, entityId, relay, concurrencyEnforcer, statsCollector, repository)) + final def props(configuration: Configuration, entityId: String, relay: ActorRef, repository: JdbcRepository): Props = + Props(new CrudEntity(configuration, entityId, relay, repository)) /** * Used to ensure the action ids sent to the concurrency enforcer are indeed unique. @@ -189,8 +177,6 @@ object CrudEntity { final class CrudEntity(configuration: CrudEntity.Configuration, entityId: String, relay: ActorRef, - concurrencyEnforcer: ActorRef, - statsCollector: ActorRef, repository: JdbcRepository) extends Actor with Stash @@ -207,22 +193,16 @@ final class CrudEntity(configuration: CrudEntity.Configuration, private[this] final var stopped = false private[this] final var idCounter = 0L private[this] final var inited = false - private[this] final var reportedDatabaseOperationStarted = false - private[this] final var databaseOperationStartTime = 0L private[this] final var commandStartTime = 0L // Set up passivation timer context.setReceiveTimeout(configuration.passivationTimeout.duration) - // First thing actor will do is access database - reportDatabaseOperationStarted() - override final def preStart(): Unit = repository .get(Key(persistenceId, entityId)) .map { state => // related to the first access to the database when the actor starts - reportDatabaseOperationFinished() if (!inited) { relay ! CrudStreamIn( CrudStreamIn.Message.Init( @@ -244,15 +224,10 @@ final class CrudEntity(configuration: CrudEntity.Configuration, } .pipeTo(self) - override final def postStop(): Unit = { + override final def postStop(): Unit = if (currentCommand != null) { log.warning("Stopped but we have a current action id {}", currentCommand.actionId) - reportActionComplete() - } - if (reportedDatabaseOperationStarted) { - reportDatabaseOperationFinished() } - } private[this] final def commandHandled(): Unit = { currentCommand = null @@ -282,9 +257,6 @@ final class CrudEntity(configuration: CrudEntity.Configuration, throw new Exception(s"$msg - $details") } - private[this] final def reportActionComplete() = - concurrencyEnforcer ! ActionCompleted(currentCommand.actionId, System.nanoTime() - commandStartTime) - private[this] final def handleCommand(entityCommand: EntityCommand, sender: ActorRef): Unit = { idCounter += 1 val command = Command( @@ -295,9 +267,7 @@ final class CrudEntity(configuration: CrudEntity.Configuration, ) currentCommand = CrudEntity.OutstandingCommand(idCounter, actorId + ":" + entityId + ":" + idCounter, sender) commandStartTime = System.nanoTime() - concurrencyEnforcer ! Action(currentCommand.actionId, () => { - relay ! CrudStreamIn(CrudStreamIn.Message.Command(command)) - }) + relay ! CrudStreamIn(CrudStreamIn.Message.Command(command)) } private final def esReplyToUfReply(reply: CrudReply) = @@ -348,17 +318,14 @@ final class CrudEntity(configuration: CrudEntity.Configuration, s"(expected id ${currentCommand.commandId} but got ${r.commandId}) - $r") case CrudSOMsg.Reply(r) => - reportActionComplete() val commandId = currentCommand.commandId if (r.crudAction.isEmpty) { currentCommand.replyTo ! esReplyToUfReply(r) commandHandled() } else { - reportDatabaseOperationStarted() r.crudAction map { a => performCrudAction(a) .map { _ => - reportDatabaseOperationFinished() // Make sure that the current request is still ours if (currentCommand == null || currentCommand.commandId != commandId) { crash("Unexpected CRUD entity behavior", "currentRequest changed before the state were persisted") @@ -382,7 +349,6 @@ final class CrudEntity(configuration: CrudEntity.Configuration, s"(expected id ${currentCommand.commandId} but got ${f.commandId}) - ${f.description}") case CrudSOMsg.Failure(f) => - reportActionComplete() try crash("Unexpected CRUD entity failure", f.description) finally currentCommand = null // clear command after notifications @@ -421,21 +387,4 @@ final class CrudEntity(configuration: CrudEntity.Configuration, case Delete(_) => repository.delete(Key(persistenceId, entityId)) } - - private def reportDatabaseOperationStarted(): Unit = - if (reportedDatabaseOperationStarted) { - log.warning("Already reported database operation started") - } else { - databaseOperationStartTime = System.nanoTime() - reportedDatabaseOperationStarted = true - statsCollector ! StatsCollector.DatabaseOperationStarted - } - - private def reportDatabaseOperationFinished(): Unit = - if (!reportedDatabaseOperationStarted) { - log.warning("Hadn't reported database operation started") - } else { - reportedDatabaseOperationStarted = false - statsCollector ! StatsCollector.DatabaseOperationFinished(System.nanoTime() - databaseOperationStartTime) - } } diff --git a/proxy/core/src/main/scala/io/cloudstate/proxy/crud/CrudSupportFactory.scala b/proxy/core/src/main/scala/io/cloudstate/proxy/crud/CrudSupportFactory.scala index c43e74074..d47aa1fd6 100644 --- a/proxy/core/src/main/scala/io/cloudstate/proxy/crud/CrudSupportFactory.scala +++ b/proxy/core/src/main/scala/io/cloudstate/proxy/crud/CrudSupportFactory.scala @@ -36,9 +36,7 @@ import scala.concurrent.{ExecutionContext, Future} class CrudSupportFactory(system: ActorSystem, config: EntityDiscoveryManager.Configuration, - grpcClientSettings: GrpcClientSettings, - concurrencyEnforcer: ActorRef, - statsCollector: ActorRef)(implicit ec: ExecutionContext, mat: Materializer) + grpcClientSettings: GrpcClientSettings)(implicit ec: ExecutionContext, mat: Materializer) extends EntityTypeSupportFactory { private final val log = Logging.getLogger(system, this.getClass) @@ -63,8 +61,7 @@ class CrudSupportFactory(system: ActorSystem, val clusterShardingSettings = ClusterShardingSettings(system) val crudEntity = clusterSharding.start( typeName = entity.persistenceId, - entityProps = - CrudEntitySupervisor.props(crudClient, stateManagerConfig, concurrencyEnforcer, statsCollector, repository), + entityProps = CrudEntitySupervisor.props(crudClient, stateManagerConfig, repository), settings = clusterShardingSettings, messageExtractor = new CrudEntityIdExtractor(config.numberOfShards), allocationStrategy = new DynamicLeastShardAllocationStrategy(1, 10, 2, 0.0), From 5a97b747ba04994b46ffa98d1cd65ef3a98c0aee Mon Sep 17 00:00:00 2001 From: Guy Youansi Date: Sat, 26 Sep 2020 17:51:15 +0200 Subject: [PATCH 58/93] disabled CRUD native support for now --- proxy/jdbc/src/main/resources/jdbc-common.conf | 4 ++-- .../io/cloudstate/proxy/jdbc/CloudStateJdbcProxyMain.scala | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/proxy/jdbc/src/main/resources/jdbc-common.conf b/proxy/jdbc/src/main/resources/jdbc-common.conf index 11a8ace9d..930df8edc 100644 --- a/proxy/jdbc/src/main/resources/jdbc-common.conf +++ b/proxy/jdbc/src/main/resources/jdbc-common.conf @@ -2,12 +2,12 @@ include "cloudstate-common" cloudstate.proxy { journal-enabled = true - crud-enabled = true + #crud-enabled = true } akka { management.health-checks.readiness-checks.cloudstate-jdbc = "io.cloudstate.proxy.jdbc.SlickEnsureTablesExistReadyCheck" - management.health-checks.readiness-checks.cloudstate-crud-jdbc = "io.cloudstate.proxy.jdbc.SlickEnsureCrudTablesExistReadyCheck" + #management.health-checks.readiness-checks.cloudstate-crud-jdbc = "io.cloudstate.proxy.jdbc.SlickEnsureCrudTablesExistReadyCheck" persistence { journal.plugin = "jdbc-journal" diff --git a/proxy/jdbc/src/main/scala/io/cloudstate/proxy/jdbc/CloudStateJdbcProxyMain.scala b/proxy/jdbc/src/main/scala/io/cloudstate/proxy/jdbc/CloudStateJdbcProxyMain.scala index 11f2b2c1e..169f122a0 100644 --- a/proxy/jdbc/src/main/scala/io/cloudstate/proxy/jdbc/CloudStateJdbcProxyMain.scala +++ b/proxy/jdbc/src/main/scala/io/cloudstate/proxy/jdbc/CloudStateJdbcProxyMain.scala @@ -29,7 +29,7 @@ object CloudStateJdbcProxyMain { val config = new CloudStateProxyMain.Configuration(actorSystem.settings.config.getConfig("cloudstate.proxy")) if (config.devMode) { new SlickEnsureTablesExistReadyCheck(actorSystem) - new SlickEnsureCrudTablesExistReadyCheck(actorSystem) + //new SlickEnsureCrudTablesExistReadyCheck(actorSystem) } } From 1b163b3a047de6cfde3d465ef75dae0b9d0f2c2b Mon Sep 17 00:00:00 2001 From: Peter Vlugter Date: Tue, 29 Sep 2020 17:12:33 +1300 Subject: [PATCH 59/93] Add CRUD to expected protocols in TCK --- tck/src/main/scala/io/cloudstate/tck/CloudStateTCK.scala | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tck/src/main/scala/io/cloudstate/tck/CloudStateTCK.scala b/tck/src/main/scala/io/cloudstate/tck/CloudStateTCK.scala index 50201b8fa..a6e593b19 100644 --- a/tck/src/main/scala/io/cloudstate/tck/CloudStateTCK.scala +++ b/tck/src/main/scala/io/cloudstate/tck/CloudStateTCK.scala @@ -25,6 +25,7 @@ import com.google.protobuf.DescriptorProtos import com.google.protobuf.any.{Any => ScalaPbAny} import com.typesafe.config.{Config, ConfigFactory} import io.cloudstate.protocol.crdt.Crdt +import io.cloudstate.protocol.crud.Crud import io.cloudstate.protocol.event_sourced._ import io.cloudstate.protocol.function.StatelessFunction import io.cloudstate.testkit.InterceptService.InterceptorSettings @@ -109,6 +110,7 @@ class CloudStateTCK(description: String, settings: CloudStateTCK.Settings) info.supportedEntityTypes must contain theSameElementsAs Seq( EventSourced.name, Crdt.name, + Crud.name, StatelessFunction.name ) From 821fd4c83c253367a44f2dbba7f677811d699837 Mon Sep 17 00:00:00 2001 From: Peter Vlugter Date: Tue, 29 Sep 2020 17:12:35 +1300 Subject: [PATCH 60/93] Add default value for cloudstate.proxy.postgres.schema setting --- proxy/postgres/src/main/resources/application.conf | 1 + 1 file changed, 1 insertion(+) diff --git a/proxy/postgres/src/main/resources/application.conf b/proxy/postgres/src/main/resources/application.conf index c2101e61c..6aec3d842 100644 --- a/proxy/postgres/src/main/resources/application.conf +++ b/proxy/postgres/src/main/resources/application.conf @@ -8,6 +8,7 @@ cloudstate.proxy.postgres { port = ${?POSTGRES_PORT} // Currently this is ignored + schema = "" schema = ${?POSTGRES_SCHEMA} database = "cloudstate" From 5d9a1936574335fdbcad807cccbe20bf7bf84a37 Mon Sep 17 00:00:00 2001 From: Guy Youansi Date: Tue, 29 Sep 2020 23:38:29 +0200 Subject: [PATCH 61/93] enabled CRUD for the proxy and add readiness-checks --- proxy/jdbc/src/main/resources/jdbc-common.conf | 8 +++++--- .../cloudstate/proxy/jdbc/CloudStateJdbcProxyMain.scala | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/proxy/jdbc/src/main/resources/jdbc-common.conf b/proxy/jdbc/src/main/resources/jdbc-common.conf index 930df8edc..4597dd838 100644 --- a/proxy/jdbc/src/main/resources/jdbc-common.conf +++ b/proxy/jdbc/src/main/resources/jdbc-common.conf @@ -2,12 +2,14 @@ include "cloudstate-common" cloudstate.proxy { journal-enabled = true - #crud-enabled = true + crud-enabled = true } akka { - management.health-checks.readiness-checks.cloudstate-jdbc = "io.cloudstate.proxy.jdbc.SlickEnsureTablesExistReadyCheck" - #management.health-checks.readiness-checks.cloudstate-crud-jdbc = "io.cloudstate.proxy.jdbc.SlickEnsureCrudTablesExistReadyCheck" + management.health-checks.readiness-checks { + cloudstate-jdbc = "io.cloudstate.proxy.jdbc.SlickEnsureTablesExistReadyCheck" + cloudstate-crud-jdbc = "io.cloudstate.proxy.jdbc.SlickEnsureCrudTablesExistReadyCheck" + } persistence { journal.plugin = "jdbc-journal" diff --git a/proxy/jdbc/src/main/scala/io/cloudstate/proxy/jdbc/CloudStateJdbcProxyMain.scala b/proxy/jdbc/src/main/scala/io/cloudstate/proxy/jdbc/CloudStateJdbcProxyMain.scala index 169f122a0..11f2b2c1e 100644 --- a/proxy/jdbc/src/main/scala/io/cloudstate/proxy/jdbc/CloudStateJdbcProxyMain.scala +++ b/proxy/jdbc/src/main/scala/io/cloudstate/proxy/jdbc/CloudStateJdbcProxyMain.scala @@ -29,7 +29,7 @@ object CloudStateJdbcProxyMain { val config = new CloudStateProxyMain.Configuration(actorSystem.settings.config.getConfig("cloudstate.proxy")) if (config.devMode) { new SlickEnsureTablesExistReadyCheck(actorSystem) - //new SlickEnsureCrudTablesExistReadyCheck(actorSystem) + new SlickEnsureCrudTablesExistReadyCheck(actorSystem) } } From 0248e53aec9de2781efcb5061053bb75dfd382c4 Mon Sep 17 00:00:00 2001 From: Guy Youansi Date: Wed, 30 Sep 2020 10:34:17 +0200 Subject: [PATCH 62/93] add hikari connection pool properties --- proxy/core/src/main/resources/reference.conf | 49 +++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/proxy/core/src/main/resources/reference.conf b/proxy/core/src/main/resources/reference.conf index 95dc02c62..62bee7326 100644 --- a/proxy/core/src/main/resources/reference.conf +++ b/proxy/core/src/main/resources/reference.conf @@ -160,7 +160,54 @@ cloudstate.proxy { # (uncomment and set the property below to match your needs) # password = "cloudstate" - # TODO: may be more properties here!!! help needed!!! + + ## copy and adapted from akka-persistece-jdbc + + # hikariCP settings; see: https://github.com/brettwooldridge/HikariCP + # Slick will use an async executor with a fixed size queue of 10.000 objects + # The async executor is a connection pool for asynchronous execution of blocking I/O actions. + # This is used for the asynchronous query execution API on top of blocking back-ends like JDBC. + queueSize = 10000 // number of objects that can be queued by the async exector + + # This property controls the maximum number of milliseconds that a client (that's you) will wait for a connection + # from the pool. If this time is exceeded without a connection becoming available, a SQLException will be thrown. + # 1000ms is the minimum value. Default: 180000 (3 minutes) + connectionTimeout = 180000 + + # This property controls the maximum amount of time that a connection will be tested for aliveness. + # This value must be less than the connectionTimeout. The lowest accepted validation timeout is 1000ms (1 second). Default: 5000 + validationTimeout = 5000 + + # 10 minutes: This property controls the maximum amount of time that a connection is allowed to sit idle in the pool. + # Whether a connection is retired as idle or not is subject to a maximum variation of +30 seconds, and average variation + # of +15 seconds. A connection will never be retired as idle before this timeout. A value of 0 means that idle connections + # are never removed from the pool. Default: 600000 (10 minutes) + idleTimeout = 600000 + + # 30 minutes: This property controls the maximum lifetime of a connection in the pool. When a connection reaches this timeout + # it will be retired from the pool, subject to a maximum variation of +30 seconds. An in-use connection will never be retired, + # only when it is closed will it then be removed. We strongly recommend setting this value, and it should be at least 30 seconds + # less than any database-level connection timeout. A value of 0 indicates no maximum lifetime (infinite lifetime), + # subject of course to the idleTimeout setting. Default: 1800000 (30 minutes) + maxLifetime = 1800000 + + # This property controls the amount of time that a connection can be out of the pool before a message is logged indicating a + # possible connection leak. A value of 0 means leak detection is disabled. + # Lowest acceptable value for enabling leak detection is 2000 (2 secs). Default: 0 + leakDetectionThreshold = 0 + + # ensures that the database does not get dropped while we are using it + keepAliveConnection = on + + # See some tips on thread/connection pool sizing on https://github.com/brettwooldridge/HikariCP/wiki/About-Pool-Sizing + # Keep in mind that the number of threads must equal the maximum number of connections. + numThreads = 20 + maxConnections = 20 + minConnections = 20 + + # This property controls a user-defined name for the connection pool and appears mainly in logging and JMX + # management consoles to identify pools and pool configurations. Default: auto-generated + poolName = "cloudstate-crud-connection-pool" } # This property indicates the CRUD table in use. From d8f2e9f2dbee6f2a576a6fba732b7d29f1ea8337 Mon Sep 17 00:00:00 2001 From: Guy Youansi Date: Wed, 30 Sep 2020 15:43:49 +0200 Subject: [PATCH 63/93] fixed protobuf any deserialization --- .../proxy/crud/store/JdbcRepository.scala | 25 ++++++++++++++++--- .../proxy/crud/store/JdbcStore.scala | 2 +- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/proxy/core/src/main/scala/io/cloudstate/proxy/crud/store/JdbcRepository.scala b/proxy/core/src/main/scala/io/cloudstate/proxy/crud/store/JdbcRepository.scala index fb93e2cc3..e67c4b156 100644 --- a/proxy/core/src/main/scala/io/cloudstate/proxy/crud/store/JdbcRepository.scala +++ b/proxy/core/src/main/scala/io/cloudstate/proxy/crud/store/JdbcRepository.scala @@ -16,6 +16,7 @@ package io.cloudstate.proxy.crud.store +import akka.grpc.ProtobufSerializer import akka.util.ByteString import com.google.protobuf.any.{Any => ScalaPbAny} import io.cloudstate.proxy.crud.store.JdbcStore.Key @@ -52,18 +53,36 @@ trait JdbcRepository { } -class JdbcRepositoryImpl(val store: JdbcStore[Key, ByteString])(implicit ec: ExecutionContext) extends JdbcRepository { +object JdbcRepositoryImpl { + + private[store] final object ReplySerializer extends ProtobufSerializer[ScalaPbAny] { + override final def serialize(state: ScalaPbAny): ByteString = + if (state.value.isEmpty) ByteString.empty + else ByteString.fromArrayUnsafe(state.value.toByteArray) + + override final def deserialize(bytes: ByteString): ScalaPbAny = + ScalaPbAny.parseFrom(bytes.toByteBuffer.array()) + } + +} + +class JdbcRepositoryImpl(val store: JdbcStore[Key, ByteString], serializer: ProtobufSerializer[ScalaPbAny])( + implicit ec: ExecutionContext +) extends JdbcRepository { + + def this(store: JdbcStore[Key, ByteString])(implicit ec: ExecutionContext) = + this(store, JdbcRepositoryImpl.ReplySerializer) def get(key: Key): Future[Option[ScalaPbAny]] = store .get(key) .map { - case Some(value) => Some(ScalaPbAny.parseFrom(value.asByteBuffer.array())) //TODO not sure!!! + case Some(value) => Some(serializer.deserialize(value)) case None => None } def update(key: Key, entity: ScalaPbAny): Future[Unit] = - store.update(key, ByteString.fromArrayUnsafe(entity.toByteArray)) + store.update(key, serializer.serialize(entity)) def delete(key: Key): Future[Unit] = store.delete(key) } diff --git a/proxy/core/src/main/scala/io/cloudstate/proxy/crud/store/JdbcStore.scala b/proxy/core/src/main/scala/io/cloudstate/proxy/crud/store/JdbcStore.scala index 22dd9d1aa..00a910710 100644 --- a/proxy/core/src/main/scala/io/cloudstate/proxy/crud/store/JdbcStore.scala +++ b/proxy/core/src/main/scala/io/cloudstate/proxy/crud/store/JdbcStore.scala @@ -77,7 +77,7 @@ final class JdbcStoreImpl(slickDatabase: JdbcSlickDatabase, queries: JdbcCrudSta override def update(key: Key, value: ByteString): Future[Unit] = for { - _ <- db.run(queries.insertOrUpdate(CrudStateRow(key, value.asByteBuffer.array()))) + _ <- db.run(queries.insertOrUpdate(CrudStateRow(key, value.toByteBuffer.array()))) } yield () override def delete(key: Key): Future[Unit] = From 353b41c8038da3ef5a427cb0d583ba37d9b78d5a Mon Sep 17 00:00:00 2001 From: Guy Youansi Date: Sat, 3 Oct 2020 16:39:17 +0200 Subject: [PATCH 64/93] add TCK test for CRUD initial version crud entity handle database access error --- .../javasupport/impl/crud/CrudImpl.scala | 2 +- .../javasupport/impl/crud/TestCrud.scala | 5 +- .../impl/eventsourced/TestEventSourced.scala | 1 - .../javasupport/tck/JavaSupportTck.java | 13 ++ .../tck/model/crud/CrudTckModelEntity.java | 76 +++++++ .../tck/model/crud/CrudTwoEntity.java | 32 +++ protocols/tck/cloudstate/tck/model/crud.proto | 128 ++++++++++++ .../io/cloudstate/proxy/crud/CrudEntity.scala | 123 ++++++------ .../proxy/crud/store/JdbcInMemoryStore.scala | 12 +- .../crud/DatabaseExceptionHandlingSpec.scala | 185 ++++++++++++++++++ tck/src/it/scala/io/cloudstate/tck/TCK.scala | 46 ++++- .../io/cloudstate/tck/CloudStateCrudTCK.scala | 125 ++++++++++++ .../cloudstate/testkit/InterceptService.scala | 8 +- .../testkit/crud/InterceptCrudService.scala | 95 +++++++++ .../testkit/crud/TestCrudService.scala | 6 +- .../discovery/InterceptEntityDiscovery.scala | 4 +- 16 files changed, 779 insertions(+), 82 deletions(-) create mode 100644 java-support/tck/src/main/java/io/cloudstate/javasupport/tck/model/crud/CrudTckModelEntity.java create mode 100644 java-support/tck/src/main/java/io/cloudstate/javasupport/tck/model/crud/CrudTwoEntity.java create mode 100644 protocols/tck/cloudstate/tck/model/crud.proto create mode 100644 proxy/core/src/test/scala/io/cloudstate/proxy/crud/DatabaseExceptionHandlingSpec.scala create mode 100644 tck/src/main/scala/io/cloudstate/tck/CloudStateCrudTCK.scala create mode 100644 testkit/src/main/scala/io/cloudstate/testkit/crud/InterceptCrudService.scala diff --git a/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/CrudImpl.scala b/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/CrudImpl.scala index a2fc7ae03..2c5535b14 100644 --- a/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/CrudImpl.scala +++ b/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/CrudImpl.scala @@ -172,7 +172,7 @@ final class CrudImpl(_system: ActorSystem, context.deactivate() // Very important! } - val clientAction = context.createClientAction(reply, false) + val clientAction = context.createClientAction(reply, false, false) if (!context.hasError) { val nextState = context.currentState() (nextState, diff --git a/java-support/src/test/scala/io/cloudstate/javasupport/impl/crud/TestCrud.scala b/java-support/src/test/scala/io/cloudstate/javasupport/impl/crud/TestCrud.scala index d34bb5fcd..58b59fce8 100644 --- a/java-support/src/test/scala/io/cloudstate/javasupport/impl/crud/TestCrud.scala +++ b/java-support/src/test/scala/io/cloudstate/javasupport/impl/crud/TestCrud.scala @@ -16,11 +16,12 @@ package io.cloudstate.javasupport.impl.crud -import akka.testkit.{EventFilter, SocketUtil} +import akka.testkit.EventFilter import com.google.protobuf.Descriptors.{FileDescriptor, ServiceDescriptor} import com.typesafe.config.{Config, ConfigFactory} import io.cloudstate.javasupport.{CloudState, CloudStateRunner} import scala.reflect.ClassTag +import io.cloudstate.testkit.Sockets object TestCrud { def service[T: ClassTag](descriptor: ServiceDescriptor, fileDescriptors: FileDescriptor*): TestCrudService = @@ -28,7 +29,7 @@ object TestCrud { } class TestCrudService(entityClass: Class[_], descriptor: ServiceDescriptor, fileDescriptors: Seq[FileDescriptor]) { - val port: Int = SocketUtil.temporaryLocalPort() + val port: Int = Sockets.temporaryLocalPort() val config: Config = ConfigFactory.load(ConfigFactory.parseString(s""" cloudstate.user-function-port = $port diff --git a/java-support/src/test/scala/io/cloudstate/javasupport/impl/eventsourced/TestEventSourced.scala b/java-support/src/test/scala/io/cloudstate/javasupport/impl/eventsourced/TestEventSourced.scala index 336ee6c0e..e76e84ab0 100644 --- a/java-support/src/test/scala/io/cloudstate/javasupport/impl/eventsourced/TestEventSourced.scala +++ b/java-support/src/test/scala/io/cloudstate/javasupport/impl/eventsourced/TestEventSourced.scala @@ -16,7 +16,6 @@ package io.cloudstate.javasupport.impl.eventsourced -import akka.actor.ActorSystem import akka.testkit.EventFilter import com.google.protobuf.Descriptors.{FileDescriptor, ServiceDescriptor} import com.typesafe.config.{Config, ConfigFactory} diff --git a/java-support/tck/src/main/java/io/cloudstate/javasupport/tck/JavaSupportTck.java b/java-support/tck/src/main/java/io/cloudstate/javasupport/tck/JavaSupportTck.java index ce8a3a6f3..9ce0f739c 100644 --- a/java-support/tck/src/main/java/io/cloudstate/javasupport/tck/JavaSupportTck.java +++ b/java-support/tck/src/main/java/io/cloudstate/javasupport/tck/JavaSupportTck.java @@ -18,10 +18,13 @@ import com.example.shoppingcart.Shoppingcart; import io.cloudstate.javasupport.CloudState; +import io.cloudstate.javasupport.tck.model.crud.CrudTckModelEntity; +import io.cloudstate.javasupport.tck.model.crud.CrudTwoEntity; import io.cloudstate.javasupport.tck.model.eventsourced.EventSourcedTckModelEntity; import io.cloudstate.javasupport.tck.model.eventsourced.EventSourcedTwoEntity; import io.cloudstate.samples.shoppingcart.ShoppingCartEntity; import io.cloudstate.tck.model.Eventsourced; +import io.cloudstate.tck.model.crud.Crud; public final class JavaSupportTck { public static final void main(String[] args) throws Exception { @@ -37,6 +40,16 @@ public static final void main(String[] args) throws Exception { ShoppingCartEntity.class, Shoppingcart.getDescriptor().findServiceByName("ShoppingCart"), com.example.shoppingcart.persistence.Domain.getDescriptor()) + .registerCrudEntity( + CrudTckModelEntity.class, + Crud.getDescriptor().findServiceByName("CrudTckModel"), + Crud.getDescriptor()) + .registerCrudEntity(CrudTwoEntity.class, Crud.getDescriptor().findServiceByName("CrudTwo")) + .registerCrudEntity( + io.cloudstate.samples.crud.shoppingcart.ShoppingCartEntity.class, + com.example.crud.shoppingcart.Shoppingcart.getDescriptor() + .findServiceByName("ShoppingCart"), + com.example.crud.shoppingcart.persistence.Domain.getDescriptor()) .start() .toCompletableFuture() .get(); diff --git a/java-support/tck/src/main/java/io/cloudstate/javasupport/tck/model/crud/CrudTckModelEntity.java b/java-support/tck/src/main/java/io/cloudstate/javasupport/tck/model/crud/CrudTckModelEntity.java new file mode 100644 index 000000000..73c959503 --- /dev/null +++ b/java-support/tck/src/main/java/io/cloudstate/javasupport/tck/model/crud/CrudTckModelEntity.java @@ -0,0 +1,76 @@ +/* + * Copyright 2019 Lightbend Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.cloudstate.javasupport.tck.model.crud; + +import io.cloudstate.javasupport.Context; +import io.cloudstate.javasupport.ServiceCall; +import io.cloudstate.javasupport.ServiceCallRef; +import io.cloudstate.javasupport.crud.CrudEntity; +import io.cloudstate.javasupport.crud.CommandContext; +import io.cloudstate.javasupport.crud.CommandHandler; +import io.cloudstate.tck.model.crud.Crud; +import io.cloudstate.tck.model.crud.Crud.*; + +import java.util.Optional; + +@CrudEntity(persistenceId = "crud-tck-model") +public class CrudTckModelEntity { + + private final ServiceCallRef serviceTwoCall; + + private String state = ""; + + public CrudTckModelEntity(Context context) { + serviceTwoCall = + context.serviceCallFactory().lookup("cloudstate.tck.model.CrudTwo", "Call", Request.class); + } + + @CommandHandler + public Optional process(Request request, CommandContext context) { + boolean forwarding = false; + for (Crud.RequestAction action : request.getActionsList()) { + switch (action.getActionCase()) { + case UPDATE: + state = action.getUpdate().getValue(); + context.updateState(Persisted.newBuilder().setValue(state).build()); + break; + case DELETE: + context.deleteState(); + state = ""; + break; + case FORWARD: + forwarding = true; + context.forward(serviceTwoRequest(action.getForward().getId())); + break; + case EFFECT: + Crud.Effect effect = action.getEffect(); + context.effect(serviceTwoRequest(effect.getId()), effect.getSynchronous()); + break; + case FAIL: + context.fail(action.getFail().getMessage()); + break; + } + } + return forwarding + ? Optional.empty() + : Optional.of(Response.newBuilder().setMessage(state).build()); + } + + private ServiceCall serviceTwoRequest(String id) { + return serviceTwoCall.createCall(Request.newBuilder().setId(id).build()); + } +} diff --git a/java-support/tck/src/main/java/io/cloudstate/javasupport/tck/model/crud/CrudTwoEntity.java b/java-support/tck/src/main/java/io/cloudstate/javasupport/tck/model/crud/CrudTwoEntity.java new file mode 100644 index 000000000..86f60e315 --- /dev/null +++ b/java-support/tck/src/main/java/io/cloudstate/javasupport/tck/model/crud/CrudTwoEntity.java @@ -0,0 +1,32 @@ +/* + * Copyright 2019 Lightbend Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.cloudstate.javasupport.tck.model.crud; + +import io.cloudstate.javasupport.crud.CrudEntity; +import io.cloudstate.javasupport.crud.CommandHandler; +import io.cloudstate.tck.model.crud.Crud.Request; +import io.cloudstate.tck.model.crud.Crud.Response; + +@CrudEntity +public class CrudTwoEntity { + public CrudTwoEntity() {} + + @CommandHandler + public Response call(Request request) { + return Response.newBuilder().build(); + } +} diff --git a/protocols/tck/cloudstate/tck/model/crud.proto b/protocols/tck/cloudstate/tck/model/crud.proto new file mode 100644 index 000000000..80f259c92 --- /dev/null +++ b/protocols/tck/cloudstate/tck/model/crud.proto @@ -0,0 +1,128 @@ +// Copyright 2019 Lightbend Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// +// == Cloudstate TCK model test for event-sourced entities == +// + +syntax = "proto3"; + +package cloudstate.tck.model.crud; + +import "cloudstate/entity_key.proto"; + +option java_package = "io.cloudstate.tck.model.crud"; + +// +// The `CrudTckModel` service should be implemented in the following ways: +// +// - The entity persistence-id must be `crud-tck-model`. +// - The state of the entity is simply a string. +// - The state string values is wrapped in `Persisted` messages. +// - The command handler must set the state to the value of a `Persisted` message. +// - The `Process` method receives a `Request` message with actions to take. +// - Request actions must be processed in order, and can require updating state, deleting state, forwarding, side effects, or failing. +// - The `Process` method must reply with the state in a `Response`, after taking actions, unless forwarding or failing. +// - Forwarding and side effects must always be made to the second service `CrudTwo`. +// +service CrudTckModel { + rpc Process(Request) returns (Response); +} + +// +// The `CrudTwo` service is only for verifying forward actions and side effects. +// The `Call` method is not required to do anything, and may simply return an empty `Response` message. +// +service CrudTwo { + rpc Call(Request) returns (Response); +} + +// +// A `Request` message contains any actions that the entity should process. +// Actions must be processed in order. Any actions after a `Fail` may be ignored. +// +message Request { + string id = 1 [(.cloudstate.entity_key) = true]; + repeated RequestAction actions = 2; +} + +// +// Each `RequestAction` is one of: +// +// - Update: update the state, with a given value. +// - Delete: delete the state. +// - Forward: forward to another service, in place of replying with a Response. +// - Effect: add a side effect to another service to the reply. +// - Fail: fail the current `Process` command. +// +message RequestAction { + oneof action { + Update update = 1; + Delete delete = 2; + Forward forward = 3; + Effect effect = 4; + Fail fail = 5; + } +} + +// +// Update the state, with the state value in a `Persisted` message. +// +message Update { + string value = 1; +} + +// +// Delete an the state with a `Persisted` message. +// +message Delete {} + +// +// Replace the response with a forward to `cloudstate.tck.model.CrudTwo/Call`. +// The payload must be a `Request` message with the given `id`. +// +message Forward { + string id = 1; +} + +// +// Add a side effect to the reply, to `cloudstate.tck.model.CrudTwo/Call`. +// The payload must be a `Request` message with the given `id`. +// The side effect should be marked synchronous based on the given `synchronous` value. +// +message Effect { + string id = 1; + bool synchronous = 2; +} + +// +// Fail the current command with the given description `message`. +// +message Fail { + string message = 1; +} + +// +// The `Response` message for the `Process` must contain the current state (after processing actions). +// +message Response { + string message = 1; +} + +// +// The `Persisted` message wraps both state value. +// +message Persisted { + string value = 1; +} diff --git a/proxy/core/src/main/scala/io/cloudstate/proxy/crud/CrudEntity.scala b/proxy/core/src/main/scala/io/cloudstate/proxy/crud/CrudEntity.scala index af6e01a76..a01607deb 100644 --- a/proxy/core/src/main/scala/io/cloudstate/proxy/crud/CrudEntity.scala +++ b/proxy/core/src/main/scala/io/cloudstate/proxy/crud/CrudEntity.scala @@ -26,7 +26,6 @@ import akka.pattern.pipe import akka.stream.scaladsl._ import akka.stream.{CompletionStrategy, Materializer, OverflowStrategy} import akka.util.Timeout -import com.google.protobuf.any.{Any => pbAny} import io.cloudstate.protocol.crud.CrudAction.Action.{Delete, Update} import io.cloudstate.protocol.crud.{ CrudAction, @@ -88,7 +87,7 @@ final class CrudEntitySupervisor(client: CrudClient, NotUsed } ) - .runWith(Sink.actorRef(self, CrudEntity.StreamClosed, CrudEntity.StreamFailed)) + .runWith(Sink.actorRef(self, CrudEntity.StreamClosed, CrudEntity.StreamFailed.apply)) context.become(waitingForRelay) } @@ -96,17 +95,17 @@ final class CrudEntitySupervisor(client: CrudClient, case Relay(relayRef) => // Cluster sharding URL encodes entity ids, so to extract it we need to decode. val entityId = URLDecoder.decode(self.path.name, "utf-8") - val manager = context.watch( + val entity = context.watch( context .actorOf(CrudEntity.props(configuration, entityId, relayRef, repository), "entity") ) - context.become(forwarding(manager, relayRef)) + context.become(forwarding(entity, relayRef)) unstashAll() case _ => stash() } - private[this] final def forwarding(manager: ActorRef, relay: ActorRef): Receive = { - case Terminated(`manager`) => + private[this] final def forwarding(entity: ActorRef, relay: ActorRef): Receive = { + case Terminated(`entity`) => if (streamTerminated) { context.stop(self) } else { @@ -114,19 +113,19 @@ final class CrudEntitySupervisor(client: CrudClient, context.become(stopping) } - case toParent if sender() == manager => - context.parent ! toParent + case message if sender() == entity => + context.parent ! message case CrudEntity.StreamClosed => streamTerminated = true - manager forward CrudEntity.StreamClosed + entity forward CrudEntity.StreamClosed case failed: CrudEntity.StreamFailed => streamTerminated = true - manager forward failed + entity forward failed - case msg => - manager forward msg + case message => + entity forward message } private def stopping: Receive = { @@ -159,10 +158,12 @@ object CrudEntity { replyTo: ActorRef ) - private case object LoadInitStateSuccess - private case class LoadInitStateFailure(cause: Throwable) - private case object SaveStateSuccess - private case object AlreadyInitialized + private case class ReadStateSuccess(initialized: Boolean) + private case class ReadStateFailure(cause: Throwable) + + private sealed trait DatabaseOperationWriteStatus + private case object WriteStateSuccess extends DatabaseOperationWriteStatus + private case class WriteStateFailure(cause: Throwable) extends DatabaseOperationWriteStatus final def props(configuration: Configuration, entityId: String, relay: ActorRef, repository: JdbcRepository): Props = Props(new CrudEntity(configuration, entityId, relay, repository)) @@ -202,7 +203,6 @@ final class CrudEntity(configuration: CrudEntity.Configuration, repository .get(Key(persistenceId, entityId)) .map { state => - // related to the first access to the database when the actor starts if (!inited) { relay ! CrudStreamIn( CrudStreamIn.Message.Init( @@ -214,13 +214,11 @@ final class CrudEntity(configuration: CrudEntity.Configuration, ) ) inited = true - CrudEntity.LoadInitStateSuccess - } else { - CrudEntity.AlreadyInitialized } + CrudEntity.ReadStateSuccess(inited) } .recover { - case error => CrudEntity.LoadInitStateFailure(error) + case error => CrudEntity.ReadStateFailure(error) } .pipeTo(self) @@ -245,7 +243,7 @@ final class CrudEntity(configuration: CrudEntity.Configuration, case null => case req => req.replyTo ! createFailure(msg) } - val errorNotification = createFailure("Entity terminated") + val errorNotification = createFailure("CRUD Entity terminated") stashedCommands.foreach { case (_, replyTo) => replyTo ! errorNotification } @@ -281,24 +279,20 @@ final class CrudEntity(configuration: CrudEntity.Configuration, clientAction = Some(ClientAction(ClientAction.Action.Failure(Failure(description = message)))) ) - override final def receive: PartialFunction[Any, Unit] = waitingForInitState - - private def waitingForInitState: PartialFunction[Any, Unit] = { - case CrudEntity.LoadInitStateSuccess => - context.become(initialized) - unstashAll() - - case CrudEntity.AlreadyInitialized => - // ignore entity already initialized + override final def receive: Receive = { + case CrudEntity.ReadStateSuccess(initialize) => + if (initialize) { + context.become(running) + unstashAll() + } - case CrudEntity.LoadInitStateFailure(error) => - crash("Unexpected CRUD entity failure", - s"(cannot load the initial state due to unexpected failure) - ${error.getMessage}") + case CrudEntity.ReadStateFailure(error) => + throw error case _ => stash() } - private def initialized: PartialFunction[Any, Unit] = { + private def running: Receive = { case command: EntityCommand if currentCommand != null => stashedCommands = stashedCommands.enqueue((command, sender())) @@ -323,18 +317,15 @@ final class CrudEntity(configuration: CrudEntity.Configuration, currentCommand.replyTo ! esReplyToUfReply(r) commandHandled() } else { - r.crudAction map { a => - performCrudAction(a) - .map { _ => - // Make sure that the current request is still ours - if (currentCommand == null || currentCommand.commandId != commandId) { - crash("Unexpected CRUD entity behavior", "currentRequest changed before the state were persisted") - } - currentCommand.replyTo ! esReplyToUfReply(r) - commandHandled() - CrudEntity.SaveStateSuccess + r.crudAction.map { a => + performAction(a) { _ => + // Make sure that the current request is still ours + if (currentCommand == null || currentCommand.commandId != commandId) { + crash("Unexpected CRUD entity behavior", "currentRequest changed before the state were persisted") } - .pipeTo(self) + currentCommand.replyTo ! esReplyToUfReply(r) + commandHandled() + }.pipeTo(self) } } @@ -358,6 +349,13 @@ final class CrudEntity(configuration: CrudEntity.Configuration, crash("Unexpected CRUD entity failure", "empty or unknown message from entity output stream") } + case CrudEntity.WriteStateSuccess => + // Nothing to do, database write access the native crud database was successful + + case CrudEntity.WriteStateFailure(error) => + notifyOutstandingRequests("Unexpected CRUD entity failure") + throw error + case CrudEntity.StreamClosed => notifyOutstandingRequests("Unexpected CRUD entity termination") context.stop(self) @@ -366,25 +364,40 @@ final class CrudEntity(configuration: CrudEntity.Configuration, notifyOutstandingRequests("Unexpected CRUD entity termination") throw error - case CrudEntity.SaveStateSuccess => - // Nothing to do, access the native crud database was successful - - case ReceiveTimeout => - context.parent ! ShardRegion.Passivate(stopMessage = CrudEntity.Stop) - case CrudEntity.Stop => stopped = true if (currentCommand == null) { context.stop(self) } + + case ReceiveTimeout => + context.parent ! ShardRegion.Passivate(stopMessage = CrudEntity.Stop) } - private def performCrudAction(crudAction: CrudAction): Future[Unit] = + private def performAction( + crudAction: CrudAction + )(handler: Unit => Unit): Future[CrudEntity.DatabaseOperationWriteStatus] = crudAction.action match { case Update(CrudUpdate(Some(value), _)) => - repository.update(Key(persistenceId, entityId), value) + repository + .update(Key(persistenceId, entityId), value) + .map { _ => + handler(()) + CrudEntity.WriteStateSuccess + } + .recover { + case error => CrudEntity.WriteStateFailure(error) + } case Delete(_) => - repository.delete(Key(persistenceId, entityId)) + repository + .delete(Key(persistenceId, entityId)) + .map { _ => + handler(()) + CrudEntity.WriteStateSuccess + } + .recover { + case error => CrudEntity.WriteStateFailure(error) + } } } diff --git a/proxy/core/src/main/scala/io/cloudstate/proxy/crud/store/JdbcInMemoryStore.scala b/proxy/core/src/main/scala/io/cloudstate/proxy/crud/store/JdbcInMemoryStore.scala index 243821d42..e26d8d8e6 100644 --- a/proxy/core/src/main/scala/io/cloudstate/proxy/crud/store/JdbcInMemoryStore.scala +++ b/proxy/core/src/main/scala/io/cloudstate/proxy/crud/store/JdbcInMemoryStore.scala @@ -18,29 +18,21 @@ package io.cloudstate.proxy.crud.store import akka.util.ByteString import io.cloudstate.proxy.crud.store.JdbcStore.Key -import org.slf4j.{Logger, LoggerFactory} import scala.concurrent.Future final class JdbcInMemoryStore extends JdbcStore[Key, ByteString] { - private final val logger: Logger = LoggerFactory.getLogger(classOf[JdbcInMemoryStore]) + private var store = Map.empty[Key, ByteString] - private final var store = Map.empty[Key, ByteString] - - override def get(key: Key): Future[Option[ByteString]] = { - logger.info(s"get called with key - $key") - Future.successful(store.get(key)) - } + override def get(key: Key): Future[Option[ByteString]] = Future.successful(store.get(key)) override def update(key: Key, value: ByteString): Future[Unit] = { - logger.info(s"update called with key - $key and value - ${value.utf8String}") store += key -> value Future.unit } override def delete(key: Key): Future[Unit] = { - logger.info(s"delete called with key - $key") store -= key Future.unit } diff --git a/proxy/core/src/test/scala/io/cloudstate/proxy/crud/DatabaseExceptionHandlingSpec.scala b/proxy/core/src/test/scala/io/cloudstate/proxy/crud/DatabaseExceptionHandlingSpec.scala new file mode 100644 index 000000000..fe5f162d5 --- /dev/null +++ b/proxy/core/src/test/scala/io/cloudstate/proxy/crud/DatabaseExceptionHandlingSpec.scala @@ -0,0 +1,185 @@ +/* + * Copyright 2019 Lightbend Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.cloudstate.proxy.crud + +import akka.actor.{Actor, ActorRef, ActorSystem} +import akka.grpc.GrpcClientSettings +import akka.testkit.TestEvent.Mute +import akka.testkit.{EventFilter, TestActorRef} +import akka.util.ByteString +import com.google.protobuf.any.{Any => ScalaPbAny} +import io.cloudstate.protocol.crud.CrudClient +import com.google.protobuf.{ByteString => PbByteString} +import io.cloudstate.proxy.crud.store.{JdbcRepositoryImpl, JdbcStore} +import io.cloudstate.proxy.crud.store.JdbcStore.Key +import io.cloudstate.proxy.entity.{EntityCommand, UserFunctionReply} +import io.cloudstate.proxy.telemetry.AbstractTelemetrySpec +import io.cloudstate.testkit.TestService +import io.cloudstate.testkit.crud.CrudMessages + +import scala.concurrent.Future +import scala.concurrent.duration._ + +class DatabaseExceptionHandlingSpec extends AbstractTelemetrySpec { + + private val testkitConfig = """ + | include "test-in-memory" + | akka { + | loglevel = ERROR + | loggers = ["akka.testkit.TestEventListener"] + | remote.artery.canonical.port = 0 + | remote.artery.bind.port = "" + | } + """ + private val service = TestService() + private val entityConfiguration = CrudEntity.Configuration( + serviceName = "service", + userFunctionName = "test", + passivationTimeout = 30.seconds, + sendQueueSize = 100 + ) + + "The CrudEntity" should { + + "crash entity on init when loading state failures" in withTestKit(testkitConfig) { testKit => + import testKit._ + import system.dispatcher + + silentDeadLettersAndUnhandledMessages + + val client = CrudClient(GrpcClientSettings.connectToServiceAt("localhost", service.port).withTls(false)) + val repository = new JdbcRepositoryImpl(TestJdbcStore.storeWithGetFailure()) + val entity = watch(system.actorOf(CrudEntitySupervisor.props(client, entityConfiguration, repository), "entity")) + + val connection = service.crud.expectConnection() + connection.expectClosed() + } + + "crash entity on update state failures" in withTestKit(testkitConfig) { testKit => + import testKit._ + import CrudMessages._ + import system.dispatcher + + silentDeadLettersAndUnhandledMessages + + val forwardReply = forwardReplyActor(testActor) + + val client = CrudClient(GrpcClientSettings.connectToServiceAt("localhost", service.port).withTls(false)) + val repository = new JdbcRepositoryImpl(TestJdbcStore.storeWithUpdateFailure()) + val entity = watch(system.actorOf(CrudEntitySupervisor.props(client, entityConfiguration, repository), "entity")) + val emptyCommand = Some(protobufAny(EmptyJavaMessage)) + + val connection = service.crud.expectConnection() + connection.expect(init("service", "entity")) + entity.tell(EntityCommand(entityId = "test", name = "command1", emptyCommand), forwardReply) + connection.expect(command(1, "entity", "command1")) + + val state = ScalaPbAny("state", PbByteString.copyFromUtf8("state")) + connection.send(reply(1, EmptyJavaMessage, update(state))) + expectMsg(UserFunctionReply(clientActionFailure("Unexpected CRUD entity failure"))) + connection.expectClosed() + } + + "crash entity on delete state failures" in withTestKit(testkitConfig) { testKit => + import testKit._ + import CrudMessages._ + import system.dispatcher + + silentDeadLettersAndUnhandledMessages + + val forwardReply = forwardReplyActor(testActor) + + val client = CrudClient(GrpcClientSettings.connectToServiceAt("localhost", service.port).withTls(false)) + val repository = new JdbcRepositoryImpl(TestJdbcStore.storeWithDeleteFailure()) + val entity = watch(system.actorOf(CrudEntitySupervisor.props(client, entityConfiguration, repository), "entity")) + val emptyCommand = Some(protobufAny(EmptyJavaMessage)) + + val connection = service.crud.expectConnection() + connection.expect(init("service", "entity")) + + entity.tell(EntityCommand(entityId = "test", name = "command1", emptyCommand), forwardReply) + connection.expect(command(1, "entity", "command1")) + connection.send(reply(1, EmptyJavaMessage, update(ScalaPbAny("state", PbByteString.copyFromUtf8("state"))))) + expectMsg(UserFunctionReply(clientActionReply(messagePayload(EmptyJavaMessage)))) + + entity.tell(EntityCommand(entityId = "test", name = "command2", emptyCommand), forwardReply) + connection.expect(command(2, "entity", "command2")) + connection.send(reply(2, EmptyJavaMessage, delete())) + expectMsg(UserFunctionReply(clientActionFailure("Unexpected CRUD entity failure"))) + + connection.expectClosed() + } + } + + private final class TestJdbcStore(status: String) extends JdbcStore[Key, ByteString] { + import TestJdbcStore.JdbcStoreStatus._ + + private var store = Map.empty[Key, ByteString] + + override def get(key: Key): Future[Option[ByteString]] = + status match { + case `getFailure` => Future.failed(new RuntimeException("Database GET access failed because of boom!")) + case _ => Future.successful(store.get(key)) + } + override def update(key: Key, value: ByteString): Future[Unit] = + status match { + case `updateFailure` => Future.failed(new RuntimeException("Database Update access failed because of boom!")) + case _ => + store += key -> value + Future.unit + } + + override def delete(key: Key): Future[Unit] = + status match { + case `deleteFailure` => Future.failed(new RuntimeException("Database Delete access failed because of boom!")) + case _ => + store -= key + Future.unit + } + } + + private object TestJdbcStore { + + private object JdbcStoreStatus { + val normal = "normal" + val getFailure = "GetFailure" + val updateFailure = "UpdateFailure" + val deleteFailure = "DeleteFailure" + } + + def storeWithGetFailure(): JdbcStore[Key, ByteString] = new TestJdbcStore(JdbcStoreStatus.getFailure) + + def storeWithUpdateFailure(): JdbcStore[Key, ByteString] = new TestJdbcStore(JdbcStoreStatus.updateFailure) + + def storeWithDeleteFailure(): JdbcStore[Key, ByteString] = new TestJdbcStore(JdbcStoreStatus.deleteFailure) + } + + private def silentDeadLettersAndUnhandledMessages(implicit system: ActorSystem): Unit = { + // silence any dead letters or unhandled messages during shutdown (when using test event listener) + system.eventStream.publish(Mute(EventFilter.warning(pattern = ".*received dead letter.*"))) + system.eventStream.publish(Mute(EventFilter.warning(pattern = ".*unhandled message.*"))) + } + + private def forwardReplyActor(actor: ActorRef)(implicit system: ActorSystem) = + TestActorRef(new Actor { + def receive: Receive = { + case message => + actor forward message + } + }) + +} diff --git a/tck/src/it/scala/io/cloudstate/tck/TCK.scala b/tck/src/it/scala/io/cloudstate/tck/TCK.scala index 195e75c46..a18e5dfe5 100644 --- a/tck/src/it/scala/io/cloudstate/tck/TCK.scala +++ b/tck/src/it/scala/io/cloudstate/tck/TCK.scala @@ -17,10 +17,29 @@ package io.cloudstate.tck import org.scalatest._ -import com.typesafe.config.ConfigFactory +import com.typesafe.config.{Config, ConfigFactory} import io.cloudstate.testkit.ServiceAddress + import scala.jdk.CollectionConverters._ +object TCK { + + private def tckSuites(combinations: List[Config], verify: Set[String]): Vector[ManagedCloudStateTckSpec] = { + val eventSourced = tckSuite(combinations, verify, c => new ManagedCloudStateTCK(TckConfiguration.fromConfig(c))) + val crud = tckSuite(combinations, verify, c => new ManagedCloudStateCrudTCK(TckConfiguration.fromConfig(c))) + (eventSourced ++ crud).toVector + } + + private def tckSuite(combinations: List[Config], + verify: Set[String], + factory: Config => ManagedCloudStateTckSpec): Iterator[ManagedCloudStateTckSpec] = + combinations + .iterator + .filter(section => verify(section.getString("name"))). + map(c => factory(c)) + +} + class TCK extends Suites({ val config = ConfigFactory.load() val combinations = config.getConfigList("cloudstate-tck.combinations") @@ -33,12 +52,8 @@ class TCK extends Suites({ case _ => // All good } - combinations. - iterator. - asScala. - filter(section => verify(section.getString("name"))). - map(c => new ManagedCloudStateTCK(TckConfiguration.fromConfig(c))). - toVector + TCK.tckSuites(combinations.asScala.toList, verify) + }: _*) with SequentialNestedSuiteExecution object ManagedCloudStateTCK { @@ -51,10 +66,23 @@ object ManagedCloudStateTCK { } } -class ManagedCloudStateTCK(config: TckConfiguration) extends CloudStateTCK("for " + config.name, ManagedCloudStateTCK.settings(config)) { +/* TCK test suite for EventSourcing */ +class ManagedCloudStateTCK(override val config: TckConfiguration) + extends CloudStateTCK("for " + config.name, ManagedCloudStateTCK.settings(config)) + with ManagedCloudStateTckSpec + +/* TCK test suite for CRUD */ +class ManagedCloudStateCrudTCK(override val config: TckConfiguration) + extends CloudStateCrudTCK("for " + config.name, ManagedCloudStateTCK.settings(config)) + with ManagedCloudStateTckSpec + +trait ManagedCloudStateTckSpec extends WordSpecLike with BeforeAndAfterAll { + + val config: TckConfiguration + config.validate() - val processes: TckProcesses = TckProcesses.create(config) + private val processes: TckProcesses = TckProcesses.create(config) override def beforeAll(): Unit = try { processes.service.start() diff --git a/tck/src/main/scala/io/cloudstate/tck/CloudStateCrudTCK.scala b/tck/src/main/scala/io/cloudstate/tck/CloudStateCrudTCK.scala new file mode 100644 index 000000000..d8cc92576 --- /dev/null +++ b/tck/src/main/scala/io/cloudstate/tck/CloudStateCrudTCK.scala @@ -0,0 +1,125 @@ +/* + * Copyright 2019 Lightbend Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.cloudstate.tck + +import akka.actor.ActorSystem +import akka.grpc.ServiceDescription +import akka.testkit.TestKit +import com.example.crud.shoppingcart.shoppingcart.{ShoppingCart, ShoppingCartClient} +import com.google.protobuf.DescriptorProtos +import com.typesafe.config.ConfigFactory +import io.cloudstate.protocol.action.ActionProtocol +import io.cloudstate.protocol.crdt.Crdt +import io.cloudstate.protocol.crud.Crud +import io.cloudstate.protocol.event_sourced.EventSourced +import io.cloudstate.tck.model.crud.crud.{CrudTckModel, CrudTwo} +import io.cloudstate.testkit.InterceptService.InterceptorSettings +import io.cloudstate.testkit.{InterceptService, TestClient, TestProtocol} +import org.scalatest.{BeforeAndAfterAll, MustMatchers, WordSpec} +import org.scalatest.concurrent.ScalaFutures + +import scala.concurrent.duration._ + +class ConfiguredCloudStateCrudTCK extends CloudStateCrudTCK(CloudStateTCK.Settings.fromConfig(ConfigFactory.load())) + +class CloudStateCrudTCK(description: String, settings: CloudStateTCK.Settings) + extends WordSpec + with MustMatchers + with BeforeAndAfterAll + with ScalaFutures { + + def this(settings: CloudStateTCK.Settings) = this("", settings) + + private[this] final val system = ActorSystem("CloudStateCrudTCK", ConfigFactory.load("tck")) + + private[this] final val client = TestClient(settings.proxy.host, settings.proxy.port) + private[this] final val shoppingCartClient = ShoppingCartClient(client.settings)(system) + + private[this] final val protocol = TestProtocol(settings.service.host, settings.service.port) + + @volatile private[this] final var interceptor: InterceptService = _ + @volatile private[this] final var enabledServices = Seq.empty[String] + + override implicit val patienceConfig: PatienceConfig = PatienceConfig(timeout = 3.seconds, interval = 100.millis) + + override def beforeAll(): Unit = + interceptor = new InterceptService(InterceptorSettings(bind = settings.tck, intercept = settings.service)) + + override def afterAll(): Unit = + try shoppingCartClient.close().futureValue + finally try client.terminate() + finally try protocol.terminate() + finally interceptor.terminate() + + def expectProxyOnline(): Unit = + TestKit.awaitCond(client.http.probe(), max = 10.seconds) + + def testFor(services: ServiceDescription*)(test: => Any): Unit = { + val enabled = services.map(_.name).forall(enabledServices.contains) + if (enabled) test else pending + } + + ("Cloudstate Crud TCK " + description) when { + "verifying discovery protocol" must { + "verify proxy info and entity discovery" in { + import scala.jdk.CollectionConverters._ + + expectProxyOnline() + + val discovery = interceptor.expectEntityDiscovery() + + val info = discovery.expectProxyInfo() + + info.protocolMajorVersion mustBe 0 + info.protocolMinorVersion mustBe 2 + + info.supportedEntityTypes must contain theSameElementsAs Seq( + EventSourced.name, + Crdt.name, + Crud.name, + ActionProtocol.name + ) + + val spec = discovery.expectEntitySpec() + + val descriptorSet = DescriptorProtos.FileDescriptorSet.parseFrom(spec.proto) + val serviceNames = descriptorSet.getFileList.asScala.flatMap(_.getServiceList.asScala.map(_.getName)) + + serviceNames.size mustBe spec.entities.size + + spec.entities.find(_.serviceName == CrudTckModel.name).foreach { entity => + serviceNames must contain("CrudTckModel") + entity.entityType mustBe Crud.name + entity.persistenceId mustBe "crud-tck-model" + } + + spec.entities.find(_.serviceName == CrudTwo.name).foreach { entity => + serviceNames must contain("CrudTwo") + entity.entityType mustBe Crud.name + } + + spec.entities.find(_.serviceName == ShoppingCart.name).foreach { entity => + serviceNames must contain("ShoppingCart") + entity.entityType mustBe Crud.name + entity.persistenceId must not be empty + } + + enabledServices = spec.entities.map(_.serviceName) + } + } + } +} diff --git a/testkit/src/main/scala/io/cloudstate/testkit/InterceptService.scala b/testkit/src/main/scala/io/cloudstate/testkit/InterceptService.scala index 182b80689..59d53aa25 100644 --- a/testkit/src/main/scala/io/cloudstate/testkit/InterceptService.scala +++ b/testkit/src/main/scala/io/cloudstate/testkit/InterceptService.scala @@ -22,8 +22,10 @@ import akka.http.scaladsl.Http import akka.testkit.{TestKit, TestProbe} import com.typesafe.config.{Config, ConfigFactory} import io.cloudstate.testkit.InterceptService.InterceptorSettings +import io.cloudstate.testkit.crud.InterceptCrudService import io.cloudstate.testkit.discovery.InterceptEntityDiscovery import io.cloudstate.testkit.eventsourced.InterceptEventSourcedService + import scala.concurrent.Await import scala.concurrent.duration._ @@ -35,6 +37,7 @@ final class InterceptService(settings: InterceptorSettings) { private val context = new InterceptorContext(settings.intercept.host, settings.intercept.port) private val entityDiscovery = new InterceptEntityDiscovery(context) private val eventSourced = new InterceptEventSourcedService(context) + private val crud = new InterceptCrudService(context) import context.system @@ -42,7 +45,7 @@ final class InterceptService(settings: InterceptorSettings) { Await.result( Http().bindAndHandleAsync( - handler = entityDiscovery.handler orElse eventSourced.handler, + handler = entityDiscovery.handler orElse eventSourced.handler orElse crud.handler, interface = settings.bind.host, port = settings.bind.port ), @@ -53,9 +56,12 @@ final class InterceptService(settings: InterceptorSettings) { def expectEventSourcedConnection(): InterceptEventSourcedService.Connection = eventSourced.expectConnection() + def expectCrudConnection(): InterceptCrudService.Connection = crud.expectConnection() + def terminate(): Unit = { entityDiscovery.terminate() eventSourced.terminate() + crud.terminate() context.terminate() } } diff --git a/testkit/src/main/scala/io/cloudstate/testkit/crud/InterceptCrudService.scala b/testkit/src/main/scala/io/cloudstate/testkit/crud/InterceptCrudService.scala new file mode 100644 index 000000000..00e1f2ce8 --- /dev/null +++ b/testkit/src/main/scala/io/cloudstate/testkit/crud/InterceptCrudService.scala @@ -0,0 +1,95 @@ +/* + * Copyright 2019 Lightbend Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.cloudstate.testkit.crud + +import akka.NotUsed +import akka.http.scaladsl.model.{HttpRequest, HttpResponse} +import akka.stream.scaladsl.{Sink, Source} +import akka.testkit.TestProbe +import io.cloudstate.protocol.crud.{Crud, CrudClient, CrudHandler, CrudStreamIn, CrudStreamOut} +import io.cloudstate.testkit.InterceptService.InterceptorContext + +import scala.concurrent.Future +import scala.concurrent.duration._ +import scala.reflect.ClassTag + +final class InterceptCrudService(context: InterceptorContext) { + import InterceptCrudService._ + + private val interceptor = new CrudInterceptor(context) + + def expectConnection(): Connection = context.probe.expectMsgType[Connection] + + def handler: PartialFunction[HttpRequest, Future[HttpResponse]] = CrudHandler.partial(interceptor)(context.system) + + def terminate(): Unit = interceptor.terminate() +} + +object InterceptCrudService { + + final class CrudInterceptor(context: InterceptorContext) extends Crud { + private val client = CrudClient(context.clientSettings)(context.system) + + override def handle(in: Source[CrudStreamIn, NotUsed]): Source[CrudStreamOut, NotUsed] = { + val connection = new Connection(context) + context.probe.ref ! connection + client.handle(in.alsoTo(connection.inSink)).alsoTo(connection.outSink) + } + + def terminate(): Unit = client.close() + } + + object Connection { + case object Complete + final case class Error(cause: Throwable) + } + + final class Connection(context: InterceptorContext) { + import Connection._ + + private[this] val in = TestProbe("CrudInProbe")(context.system) + private[this] val out = TestProbe("CrudOutProbe")(context.system) + + private[testkit] def inSink: Sink[CrudStreamIn, NotUsed] = Sink.actorRef(in.ref, Complete, Error.apply) + private[testkit] def outSink: Sink[CrudStreamOut, NotUsed] = Sink.actorRef(out.ref, Complete, Error.apply) + + def expectClient(message: CrudStreamIn.Message): Connection = { + in.expectMsg(CrudStreamIn(message)) + this + } + + def expectService(message: CrudStreamOut.Message): Connection = { + out.expectMsg(CrudStreamOut(message)) + this + } + + def expectServiceMessage[T](implicit classTag: ClassTag[T]): T = + expectServiceMessageClass(classTag.runtimeClass.asInstanceOf[Class[T]]) + + def expectServiceMessageClass[T](messageClass: Class[T]): T = { + val message = out.expectMsgType[CrudStreamOut].message + assert(messageClass.isInstance(message), s"expected message $messageClass, found ${message.getClass} ($message)") + message.asInstanceOf[T] + } + + def expectNoInteraction(timeout: FiniteDuration = 0.seconds): Connection = { + in.expectNoMessage(timeout) + out.expectNoMessage(timeout) + this + } + } +} diff --git a/testkit/src/main/scala/io/cloudstate/testkit/crud/TestCrudService.scala b/testkit/src/main/scala/io/cloudstate/testkit/crud/TestCrudService.scala index bfa5df86c..2c0fcd787 100644 --- a/testkit/src/main/scala/io/cloudstate/testkit/crud/TestCrudService.scala +++ b/testkit/src/main/scala/io/cloudstate/testkit/crud/TestCrudService.scala @@ -32,9 +32,11 @@ import io.cloudstate.testkit.discovery.TestEntityDiscoveryService import scala.concurrent.Future class TestCrudService(context: TestServiceContext) { - private val testCrud = new TestCrudService.TestCrud(context) + import TestCrudService._ - def expectConnection(): TestCrudService.Connection = context.probe.expectMsgType[TestCrudService.Connection] + private val testCrud = new TestCrud(context) + + def expectConnection(): Connection = context.probe.expectMsgType[Connection] def handler: PartialFunction[HttpRequest, Future[HttpResponse]] = CrudHandler.partial(testCrud)(context.system) diff --git a/testkit/src/main/scala/io/cloudstate/testkit/discovery/InterceptEntityDiscovery.scala b/testkit/src/main/scala/io/cloudstate/testkit/discovery/InterceptEntityDiscovery.scala index 2343e80b1..80c47282f 100644 --- a/testkit/src/main/scala/io/cloudstate/testkit/discovery/InterceptEntityDiscovery.scala +++ b/testkit/src/main/scala/io/cloudstate/testkit/discovery/InterceptEntityDiscovery.scala @@ -21,10 +21,12 @@ import akka.testkit.{TestKit, TestProbe} import com.google.protobuf.empty.{Empty => ScalaPbEmpty} import io.cloudstate.protocol.action.ActionProtocol import io.cloudstate.protocol.crdt.Crdt +import io.cloudstate.protocol.crud.Crud import io.cloudstate.protocol.entity._ import io.cloudstate.protocol.event_sourced.EventSourced import io.cloudstate.testkit.BuildInfo import io.cloudstate.testkit.InterceptService.InterceptorContext + import scala.concurrent.duration.FiniteDuration import scala.concurrent.{Await, Future} import scala.util.{Failure, Success} @@ -81,7 +83,7 @@ object InterceptEntityDiscovery { protocolMinorVersion = BuildInfo.protocolMinorVersion, proxyName = BuildInfo.name, proxyVersion = BuildInfo.version, - supportedEntityTypes = Seq(ActionProtocol.name, Crdt.name, EventSourced.name) + supportedEntityTypes = Seq(ActionProtocol.name, Crdt.name, EventSourced.name, Crud.name) ) def expectOnline(context: InterceptorContext, timeout: FiniteDuration): Unit = { From 47f7cd3b18f658cc1ca8c0ac31f84d18111910d7 Mon Sep 17 00:00:00 2001 From: Guy Youansi Date: Sat, 3 Oct 2020 16:39:17 +0200 Subject: [PATCH 65/93] add TCK test for CRUD and refactoring jdbc store factory --- .../javasupport/impl/crud/CrudImpl.scala | 2 +- .../javasupport/impl/crud/TestCrud.scala | 5 +- .../impl/eventsourced/TestEventSourced.scala | 1 - .../javasupport/tck/JavaSupportTck.java | 13 + .../tck/model/crud/CrudTckModelEntity.java | 78 ++++ .../tck/model/crud/CrudTwoEntity.java | 32 ++ .../shoppingcart/persistence/domain.proto | 10 - .../crud/shoppingcart/shoppingcart.proto | 10 +- protocols/tck/cloudstate/tck/model/crud.proto | 128 ++++++ .../proxy/EntityDiscoveryManager.scala | 26 -- .../io/cloudstate/proxy/crud/CrudEntity.scala | 123 ++--- .../proxy/crud/CrudSupportFactory.scala | 6 +- .../proxy/crud/store/JdbcInMemoryStore.scala | 12 +- .../proxy/crud/store/JdbcStoreFactory.scala | 8 +- .../crud/DatabaseExceptionHandlingSpec.scala | 185 ++++++++ .../crud/shoppingcart/ShoppingCartEntity.java | 8 +- tck/src/it/scala/io/cloudstate/tck/TCK.scala | 4 +- .../io/cloudstate/tck/CloudStateTCK.scala | 423 ++++++++++++++++++ .../tck/CrudShoppingCartVerifier.scala | 119 +++++ .../cloudstate/testkit/InterceptService.scala | 8 +- .../testkit/crud/CrudMessages.scala | 47 +- .../testkit/crud/InterceptCrudService.scala | 95 ++++ .../testkit/crud/TestCrudService.scala | 6 +- .../discovery/InterceptEntityDiscovery.scala | 4 +- 24 files changed, 1218 insertions(+), 135 deletions(-) create mode 100644 java-support/tck/src/main/java/io/cloudstate/javasupport/tck/model/crud/CrudTckModelEntity.java create mode 100644 java-support/tck/src/main/java/io/cloudstate/javasupport/tck/model/crud/CrudTwoEntity.java create mode 100644 protocols/tck/cloudstate/tck/model/crud.proto create mode 100644 proxy/core/src/test/scala/io/cloudstate/proxy/crud/DatabaseExceptionHandlingSpec.scala create mode 100644 tck/src/main/scala/io/cloudstate/tck/CrudShoppingCartVerifier.scala create mode 100644 testkit/src/main/scala/io/cloudstate/testkit/crud/InterceptCrudService.scala diff --git a/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/CrudImpl.scala b/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/CrudImpl.scala index a2fc7ae03..2c5535b14 100644 --- a/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/CrudImpl.scala +++ b/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/CrudImpl.scala @@ -172,7 +172,7 @@ final class CrudImpl(_system: ActorSystem, context.deactivate() // Very important! } - val clientAction = context.createClientAction(reply, false) + val clientAction = context.createClientAction(reply, false, false) if (!context.hasError) { val nextState = context.currentState() (nextState, diff --git a/java-support/src/test/scala/io/cloudstate/javasupport/impl/crud/TestCrud.scala b/java-support/src/test/scala/io/cloudstate/javasupport/impl/crud/TestCrud.scala index d34bb5fcd..58b59fce8 100644 --- a/java-support/src/test/scala/io/cloudstate/javasupport/impl/crud/TestCrud.scala +++ b/java-support/src/test/scala/io/cloudstate/javasupport/impl/crud/TestCrud.scala @@ -16,11 +16,12 @@ package io.cloudstate.javasupport.impl.crud -import akka.testkit.{EventFilter, SocketUtil} +import akka.testkit.EventFilter import com.google.protobuf.Descriptors.{FileDescriptor, ServiceDescriptor} import com.typesafe.config.{Config, ConfigFactory} import io.cloudstate.javasupport.{CloudState, CloudStateRunner} import scala.reflect.ClassTag +import io.cloudstate.testkit.Sockets object TestCrud { def service[T: ClassTag](descriptor: ServiceDescriptor, fileDescriptors: FileDescriptor*): TestCrudService = @@ -28,7 +29,7 @@ object TestCrud { } class TestCrudService(entityClass: Class[_], descriptor: ServiceDescriptor, fileDescriptors: Seq[FileDescriptor]) { - val port: Int = SocketUtil.temporaryLocalPort() + val port: Int = Sockets.temporaryLocalPort() val config: Config = ConfigFactory.load(ConfigFactory.parseString(s""" cloudstate.user-function-port = $port diff --git a/java-support/src/test/scala/io/cloudstate/javasupport/impl/eventsourced/TestEventSourced.scala b/java-support/src/test/scala/io/cloudstate/javasupport/impl/eventsourced/TestEventSourced.scala index 336ee6c0e..e76e84ab0 100644 --- a/java-support/src/test/scala/io/cloudstate/javasupport/impl/eventsourced/TestEventSourced.scala +++ b/java-support/src/test/scala/io/cloudstate/javasupport/impl/eventsourced/TestEventSourced.scala @@ -16,7 +16,6 @@ package io.cloudstate.javasupport.impl.eventsourced -import akka.actor.ActorSystem import akka.testkit.EventFilter import com.google.protobuf.Descriptors.{FileDescriptor, ServiceDescriptor} import com.typesafe.config.{Config, ConfigFactory} diff --git a/java-support/tck/src/main/java/io/cloudstate/javasupport/tck/JavaSupportTck.java b/java-support/tck/src/main/java/io/cloudstate/javasupport/tck/JavaSupportTck.java index ce8a3a6f3..9ce0f739c 100644 --- a/java-support/tck/src/main/java/io/cloudstate/javasupport/tck/JavaSupportTck.java +++ b/java-support/tck/src/main/java/io/cloudstate/javasupport/tck/JavaSupportTck.java @@ -18,10 +18,13 @@ import com.example.shoppingcart.Shoppingcart; import io.cloudstate.javasupport.CloudState; +import io.cloudstate.javasupport.tck.model.crud.CrudTckModelEntity; +import io.cloudstate.javasupport.tck.model.crud.CrudTwoEntity; import io.cloudstate.javasupport.tck.model.eventsourced.EventSourcedTckModelEntity; import io.cloudstate.javasupport.tck.model.eventsourced.EventSourcedTwoEntity; import io.cloudstate.samples.shoppingcart.ShoppingCartEntity; import io.cloudstate.tck.model.Eventsourced; +import io.cloudstate.tck.model.crud.Crud; public final class JavaSupportTck { public static final void main(String[] args) throws Exception { @@ -37,6 +40,16 @@ public static final void main(String[] args) throws Exception { ShoppingCartEntity.class, Shoppingcart.getDescriptor().findServiceByName("ShoppingCart"), com.example.shoppingcart.persistence.Domain.getDescriptor()) + .registerCrudEntity( + CrudTckModelEntity.class, + Crud.getDescriptor().findServiceByName("CrudTckModel"), + Crud.getDescriptor()) + .registerCrudEntity(CrudTwoEntity.class, Crud.getDescriptor().findServiceByName("CrudTwo")) + .registerCrudEntity( + io.cloudstate.samples.crud.shoppingcart.ShoppingCartEntity.class, + com.example.crud.shoppingcart.Shoppingcart.getDescriptor() + .findServiceByName("ShoppingCart"), + com.example.crud.shoppingcart.persistence.Domain.getDescriptor()) .start() .toCompletableFuture() .get(); diff --git a/java-support/tck/src/main/java/io/cloudstate/javasupport/tck/model/crud/CrudTckModelEntity.java b/java-support/tck/src/main/java/io/cloudstate/javasupport/tck/model/crud/CrudTckModelEntity.java new file mode 100644 index 000000000..9c862766d --- /dev/null +++ b/java-support/tck/src/main/java/io/cloudstate/javasupport/tck/model/crud/CrudTckModelEntity.java @@ -0,0 +1,78 @@ +/* + * Copyright 2019 Lightbend Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.cloudstate.javasupport.tck.model.crud; + +import io.cloudstate.javasupport.Context; +import io.cloudstate.javasupport.ServiceCall; +import io.cloudstate.javasupport.ServiceCallRef; +import io.cloudstate.javasupport.crud.CrudEntity; +import io.cloudstate.javasupport.crud.CommandContext; +import io.cloudstate.javasupport.crud.CommandHandler; +import io.cloudstate.tck.model.crud.Crud; +import io.cloudstate.tck.model.crud.Crud.*; + +import java.util.Optional; + +@CrudEntity(persistenceId = "crud-tck-model") +public class CrudTckModelEntity { + + private final ServiceCallRef serviceTwoCall; + + private String state = ""; + + public CrudTckModelEntity(Context context) { + serviceTwoCall = + context + .serviceCallFactory() + .lookup("cloudstate.tck.model.crud.CrudTwo", "Call", Request.class); + } + + @CommandHandler + public Optional process(Request request, CommandContext context) { + boolean forwarding = false; + for (Crud.RequestAction action : request.getActionsList()) { + switch (action.getActionCase()) { + case UPDATE: + state = action.getUpdate().getValue(); + context.updateState(Persisted.newBuilder().setValue(state).build()); + break; + case DELETE: + context.deleteState(); + state = ""; + break; + case FORWARD: + forwarding = true; + context.forward(serviceTwoRequest(action.getForward().getId())); + break; + case EFFECT: + Crud.Effect effect = action.getEffect(); + context.effect(serviceTwoRequest(effect.getId()), effect.getSynchronous()); + break; + case FAIL: + context.fail(action.getFail().getMessage()); + break; + } + } + return forwarding + ? Optional.empty() + : Optional.of(Response.newBuilder().setMessage(state).build()); + } + + private ServiceCall serviceTwoRequest(String id) { + return serviceTwoCall.createCall(Request.newBuilder().setId(id).build()); + } +} diff --git a/java-support/tck/src/main/java/io/cloudstate/javasupport/tck/model/crud/CrudTwoEntity.java b/java-support/tck/src/main/java/io/cloudstate/javasupport/tck/model/crud/CrudTwoEntity.java new file mode 100644 index 000000000..86f60e315 --- /dev/null +++ b/java-support/tck/src/main/java/io/cloudstate/javasupport/tck/model/crud/CrudTwoEntity.java @@ -0,0 +1,32 @@ +/* + * Copyright 2019 Lightbend Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.cloudstate.javasupport.tck.model.crud; + +import io.cloudstate.javasupport.crud.CrudEntity; +import io.cloudstate.javasupport.crud.CommandHandler; +import io.cloudstate.tck.model.crud.Crud.Request; +import io.cloudstate.tck.model.crud.Crud.Response; + +@CrudEntity +public class CrudTwoEntity { + public CrudTwoEntity() {} + + @CommandHandler + public Response call(Request request) { + return Response.newBuilder().build(); + } +} diff --git a/protocols/example/crud/shoppingcart/persistence/domain.proto b/protocols/example/crud/shoppingcart/persistence/domain.proto index eb51b1034..50d58c805 100644 --- a/protocols/example/crud/shoppingcart/persistence/domain.proto +++ b/protocols/example/crud/shoppingcart/persistence/domain.proto @@ -25,16 +25,6 @@ message LineItem { int32 quantity = 3; } -// The item added event. -message ItemAdded { - LineItem item = 1; -} - -// The item removed event. -message ItemRemoved { - string productId = 1; -} - // The shopping cart state. message Cart { repeated LineItem items = 1; diff --git a/protocols/example/crud/shoppingcart/shoppingcart.proto b/protocols/example/crud/shoppingcart/shoppingcart.proto index 95fa5ff52..dc00d6301 100644 --- a/protocols/example/crud/shoppingcart/shoppingcart.proto +++ b/protocols/example/crud/shoppingcart/shoppingcart.proto @@ -59,27 +59,27 @@ message Cart { service ShoppingCart { rpc AddItem(AddLineItem) returns (google.protobuf.Empty) { option (google.api.http) = { - post: "/cart/{user_id}/items/add", + post: "/crud/cart/{user_id}/items/add", body: "*", }; option (.cloudstate.eventing).in = "items"; } rpc RemoveItem(RemoveLineItem) returns (google.protobuf.Empty) { - option (google.api.http).post = "/cart/{user_id}/items/{product_id}/remove"; + option (google.api.http).post = "/crud/cart/{user_id}/items/{product_id}/remove"; } rpc GetCart(GetShoppingCart) returns (Cart) { option (google.api.http) = { - get: "/carts/{user_id}", + get: "/crud/carts/{user_id}", additional_bindings: { - get: "/carts/{user_id}/items", + get: "/crud/carts/{user_id}/items", response_body: "items" } }; } rpc RemoveCart(RemoveShoppingCart) returns (google.protobuf.Empty) { - option (google.api.http).post = "/carts/{user_id}/remove"; + option (google.api.http).post = "/crud/carts/{user_id}/remove"; } } diff --git a/protocols/tck/cloudstate/tck/model/crud.proto b/protocols/tck/cloudstate/tck/model/crud.proto new file mode 100644 index 000000000..80f259c92 --- /dev/null +++ b/protocols/tck/cloudstate/tck/model/crud.proto @@ -0,0 +1,128 @@ +// Copyright 2019 Lightbend Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// +// == Cloudstate TCK model test for event-sourced entities == +// + +syntax = "proto3"; + +package cloudstate.tck.model.crud; + +import "cloudstate/entity_key.proto"; + +option java_package = "io.cloudstate.tck.model.crud"; + +// +// The `CrudTckModel` service should be implemented in the following ways: +// +// - The entity persistence-id must be `crud-tck-model`. +// - The state of the entity is simply a string. +// - The state string values is wrapped in `Persisted` messages. +// - The command handler must set the state to the value of a `Persisted` message. +// - The `Process` method receives a `Request` message with actions to take. +// - Request actions must be processed in order, and can require updating state, deleting state, forwarding, side effects, or failing. +// - The `Process` method must reply with the state in a `Response`, after taking actions, unless forwarding or failing. +// - Forwarding and side effects must always be made to the second service `CrudTwo`. +// +service CrudTckModel { + rpc Process(Request) returns (Response); +} + +// +// The `CrudTwo` service is only for verifying forward actions and side effects. +// The `Call` method is not required to do anything, and may simply return an empty `Response` message. +// +service CrudTwo { + rpc Call(Request) returns (Response); +} + +// +// A `Request` message contains any actions that the entity should process. +// Actions must be processed in order. Any actions after a `Fail` may be ignored. +// +message Request { + string id = 1 [(.cloudstate.entity_key) = true]; + repeated RequestAction actions = 2; +} + +// +// Each `RequestAction` is one of: +// +// - Update: update the state, with a given value. +// - Delete: delete the state. +// - Forward: forward to another service, in place of replying with a Response. +// - Effect: add a side effect to another service to the reply. +// - Fail: fail the current `Process` command. +// +message RequestAction { + oneof action { + Update update = 1; + Delete delete = 2; + Forward forward = 3; + Effect effect = 4; + Fail fail = 5; + } +} + +// +// Update the state, with the state value in a `Persisted` message. +// +message Update { + string value = 1; +} + +// +// Delete an the state with a `Persisted` message. +// +message Delete {} + +// +// Replace the response with a forward to `cloudstate.tck.model.CrudTwo/Call`. +// The payload must be a `Request` message with the given `id`. +// +message Forward { + string id = 1; +} + +// +// Add a side effect to the reply, to `cloudstate.tck.model.CrudTwo/Call`. +// The payload must be a `Request` message with the given `id`. +// The side effect should be marked synchronous based on the given `synchronous` value. +// +message Effect { + string id = 1; + bool synchronous = 2; +} + +// +// Fail the current command with the given description `message`. +// +message Fail { + string message = 1; +} + +// +// The `Response` message for the `Process` must contain the current state (after processing actions). +// +message Response { + string message = 1; +} + +// +// The `Persisted` message wraps both state value. +// +message Persisted { + string value = 1; +} diff --git a/proxy/core/src/main/scala/io/cloudstate/proxy/EntityDiscoveryManager.scala b/proxy/core/src/main/scala/io/cloudstate/proxy/EntityDiscoveryManager.scala index f5c3d5201..0c71fbbd1 100644 --- a/proxy/core/src/main/scala/io/cloudstate/proxy/EntityDiscoveryManager.scala +++ b/proxy/core/src/main/scala/io/cloudstate/proxy/EntityDiscoveryManager.scala @@ -141,32 +141,6 @@ class EntityDiscoveryManager(config: EntityDiscoveryManager.Configuration)( .withChannelBuilderOverrides(_.maxInboundMessageSize(config.maxInboundMessageSize.toInt)) .withTls(false) private[this] final val entityDiscoveryClient = EntityDiscoveryClient(clientSettings) - private[this] final val autoscaler = { - val autoscalerSettings = AutoscalerSettings(system) - if (autoscalerSettings.enabled) { - val managerSettings = ClusterSingletonManagerSettings(system) - val proxySettings = ClusterSingletonProxySettings(system) - - val scalerFactory: ScalerFactory = (autoscaler, factory) => { - if (config.devMode) factory.actorOf(Props(new NoScaler(autoscaler)), "noScaler") - else factory.actorOf(KubernetesDeploymentScaler.props(autoscaler), "kubernetesDeploymentScaler") - } - - val singleton = context.actorOf( - ClusterSingletonManager.props( - Autoscaler.props(autoscalerSettings, scalerFactory, new ClusterMembershipFacadeImpl(Cluster(system))), - terminationMessage = PoisonPill, - managerSettings - ), - "autoscaler" - ) - - context.actorOf(ClusterSingletonProxy.props(singleton.path.toStringWithoutAddress, proxySettings), - "autoscalerProxy") - } else { - context.actorOf(Props(new NoAutoscaler), "noAutoscaler") - } - } private final val supportFactories: Map[String, UserFunctionTypeSupportFactory] = Map( Crdt.name -> new CrdtSupportFactory(system, config, entityDiscoveryClient, clientSettings), diff --git a/proxy/core/src/main/scala/io/cloudstate/proxy/crud/CrudEntity.scala b/proxy/core/src/main/scala/io/cloudstate/proxy/crud/CrudEntity.scala index af6e01a76..a01607deb 100644 --- a/proxy/core/src/main/scala/io/cloudstate/proxy/crud/CrudEntity.scala +++ b/proxy/core/src/main/scala/io/cloudstate/proxy/crud/CrudEntity.scala @@ -26,7 +26,6 @@ import akka.pattern.pipe import akka.stream.scaladsl._ import akka.stream.{CompletionStrategy, Materializer, OverflowStrategy} import akka.util.Timeout -import com.google.protobuf.any.{Any => pbAny} import io.cloudstate.protocol.crud.CrudAction.Action.{Delete, Update} import io.cloudstate.protocol.crud.{ CrudAction, @@ -88,7 +87,7 @@ final class CrudEntitySupervisor(client: CrudClient, NotUsed } ) - .runWith(Sink.actorRef(self, CrudEntity.StreamClosed, CrudEntity.StreamFailed)) + .runWith(Sink.actorRef(self, CrudEntity.StreamClosed, CrudEntity.StreamFailed.apply)) context.become(waitingForRelay) } @@ -96,17 +95,17 @@ final class CrudEntitySupervisor(client: CrudClient, case Relay(relayRef) => // Cluster sharding URL encodes entity ids, so to extract it we need to decode. val entityId = URLDecoder.decode(self.path.name, "utf-8") - val manager = context.watch( + val entity = context.watch( context .actorOf(CrudEntity.props(configuration, entityId, relayRef, repository), "entity") ) - context.become(forwarding(manager, relayRef)) + context.become(forwarding(entity, relayRef)) unstashAll() case _ => stash() } - private[this] final def forwarding(manager: ActorRef, relay: ActorRef): Receive = { - case Terminated(`manager`) => + private[this] final def forwarding(entity: ActorRef, relay: ActorRef): Receive = { + case Terminated(`entity`) => if (streamTerminated) { context.stop(self) } else { @@ -114,19 +113,19 @@ final class CrudEntitySupervisor(client: CrudClient, context.become(stopping) } - case toParent if sender() == manager => - context.parent ! toParent + case message if sender() == entity => + context.parent ! message case CrudEntity.StreamClosed => streamTerminated = true - manager forward CrudEntity.StreamClosed + entity forward CrudEntity.StreamClosed case failed: CrudEntity.StreamFailed => streamTerminated = true - manager forward failed + entity forward failed - case msg => - manager forward msg + case message => + entity forward message } private def stopping: Receive = { @@ -159,10 +158,12 @@ object CrudEntity { replyTo: ActorRef ) - private case object LoadInitStateSuccess - private case class LoadInitStateFailure(cause: Throwable) - private case object SaveStateSuccess - private case object AlreadyInitialized + private case class ReadStateSuccess(initialized: Boolean) + private case class ReadStateFailure(cause: Throwable) + + private sealed trait DatabaseOperationWriteStatus + private case object WriteStateSuccess extends DatabaseOperationWriteStatus + private case class WriteStateFailure(cause: Throwable) extends DatabaseOperationWriteStatus final def props(configuration: Configuration, entityId: String, relay: ActorRef, repository: JdbcRepository): Props = Props(new CrudEntity(configuration, entityId, relay, repository)) @@ -202,7 +203,6 @@ final class CrudEntity(configuration: CrudEntity.Configuration, repository .get(Key(persistenceId, entityId)) .map { state => - // related to the first access to the database when the actor starts if (!inited) { relay ! CrudStreamIn( CrudStreamIn.Message.Init( @@ -214,13 +214,11 @@ final class CrudEntity(configuration: CrudEntity.Configuration, ) ) inited = true - CrudEntity.LoadInitStateSuccess - } else { - CrudEntity.AlreadyInitialized } + CrudEntity.ReadStateSuccess(inited) } .recover { - case error => CrudEntity.LoadInitStateFailure(error) + case error => CrudEntity.ReadStateFailure(error) } .pipeTo(self) @@ -245,7 +243,7 @@ final class CrudEntity(configuration: CrudEntity.Configuration, case null => case req => req.replyTo ! createFailure(msg) } - val errorNotification = createFailure("Entity terminated") + val errorNotification = createFailure("CRUD Entity terminated") stashedCommands.foreach { case (_, replyTo) => replyTo ! errorNotification } @@ -281,24 +279,20 @@ final class CrudEntity(configuration: CrudEntity.Configuration, clientAction = Some(ClientAction(ClientAction.Action.Failure(Failure(description = message)))) ) - override final def receive: PartialFunction[Any, Unit] = waitingForInitState - - private def waitingForInitState: PartialFunction[Any, Unit] = { - case CrudEntity.LoadInitStateSuccess => - context.become(initialized) - unstashAll() - - case CrudEntity.AlreadyInitialized => - // ignore entity already initialized + override final def receive: Receive = { + case CrudEntity.ReadStateSuccess(initialize) => + if (initialize) { + context.become(running) + unstashAll() + } - case CrudEntity.LoadInitStateFailure(error) => - crash("Unexpected CRUD entity failure", - s"(cannot load the initial state due to unexpected failure) - ${error.getMessage}") + case CrudEntity.ReadStateFailure(error) => + throw error case _ => stash() } - private def initialized: PartialFunction[Any, Unit] = { + private def running: Receive = { case command: EntityCommand if currentCommand != null => stashedCommands = stashedCommands.enqueue((command, sender())) @@ -323,18 +317,15 @@ final class CrudEntity(configuration: CrudEntity.Configuration, currentCommand.replyTo ! esReplyToUfReply(r) commandHandled() } else { - r.crudAction map { a => - performCrudAction(a) - .map { _ => - // Make sure that the current request is still ours - if (currentCommand == null || currentCommand.commandId != commandId) { - crash("Unexpected CRUD entity behavior", "currentRequest changed before the state were persisted") - } - currentCommand.replyTo ! esReplyToUfReply(r) - commandHandled() - CrudEntity.SaveStateSuccess + r.crudAction.map { a => + performAction(a) { _ => + // Make sure that the current request is still ours + if (currentCommand == null || currentCommand.commandId != commandId) { + crash("Unexpected CRUD entity behavior", "currentRequest changed before the state were persisted") } - .pipeTo(self) + currentCommand.replyTo ! esReplyToUfReply(r) + commandHandled() + }.pipeTo(self) } } @@ -358,6 +349,13 @@ final class CrudEntity(configuration: CrudEntity.Configuration, crash("Unexpected CRUD entity failure", "empty or unknown message from entity output stream") } + case CrudEntity.WriteStateSuccess => + // Nothing to do, database write access the native crud database was successful + + case CrudEntity.WriteStateFailure(error) => + notifyOutstandingRequests("Unexpected CRUD entity failure") + throw error + case CrudEntity.StreamClosed => notifyOutstandingRequests("Unexpected CRUD entity termination") context.stop(self) @@ -366,25 +364,40 @@ final class CrudEntity(configuration: CrudEntity.Configuration, notifyOutstandingRequests("Unexpected CRUD entity termination") throw error - case CrudEntity.SaveStateSuccess => - // Nothing to do, access the native crud database was successful - - case ReceiveTimeout => - context.parent ! ShardRegion.Passivate(stopMessage = CrudEntity.Stop) - case CrudEntity.Stop => stopped = true if (currentCommand == null) { context.stop(self) } + + case ReceiveTimeout => + context.parent ! ShardRegion.Passivate(stopMessage = CrudEntity.Stop) } - private def performCrudAction(crudAction: CrudAction): Future[Unit] = + private def performAction( + crudAction: CrudAction + )(handler: Unit => Unit): Future[CrudEntity.DatabaseOperationWriteStatus] = crudAction.action match { case Update(CrudUpdate(Some(value), _)) => - repository.update(Key(persistenceId, entityId), value) + repository + .update(Key(persistenceId, entityId), value) + .map { _ => + handler(()) + CrudEntity.WriteStateSuccess + } + .recover { + case error => CrudEntity.WriteStateFailure(error) + } case Delete(_) => - repository.delete(Key(persistenceId, entityId)) + repository + .delete(Key(persistenceId, entityId)) + .map { _ => + handler(()) + CrudEntity.WriteStateSuccess + } + .recover { + case error => CrudEntity.WriteStateFailure(error) + } } } diff --git a/proxy/core/src/main/scala/io/cloudstate/proxy/crud/CrudSupportFactory.scala b/proxy/core/src/main/scala/io/cloudstate/proxy/crud/CrudSupportFactory.scala index d47aa1fd6..b2910aa98 100644 --- a/proxy/core/src/main/scala/io/cloudstate/proxy/crud/CrudSupportFactory.scala +++ b/proxy/core/src/main/scala/io/cloudstate/proxy/crud/CrudSupportFactory.scala @@ -37,7 +37,8 @@ import scala.concurrent.{ExecutionContext, Future} class CrudSupportFactory(system: ActorSystem, config: EntityDiscoveryManager.Configuration, grpcClientSettings: GrpcClientSettings)(implicit ec: ExecutionContext, mat: Materializer) - extends EntityTypeSupportFactory { + extends EntityTypeSupportFactory + with JdbcStoreFactory { private final val log = Logging.getLogger(system, this.getClass) @@ -53,8 +54,7 @@ class CrudSupportFactory(system: ActorSystem, config.passivationTimeout, config.relayOutputBufferSize) - val store = new JdbcStoreFactory(config.config).buildCrudStore() - val repository = new JdbcRepositoryImpl(store) + val repository = new JdbcRepositoryImpl(buildCrudStore(config.config)) log.debug("Starting CrudEntity for {}", entity.persistenceId) val clusterSharding = ClusterSharding(system) diff --git a/proxy/core/src/main/scala/io/cloudstate/proxy/crud/store/JdbcInMemoryStore.scala b/proxy/core/src/main/scala/io/cloudstate/proxy/crud/store/JdbcInMemoryStore.scala index 243821d42..e26d8d8e6 100644 --- a/proxy/core/src/main/scala/io/cloudstate/proxy/crud/store/JdbcInMemoryStore.scala +++ b/proxy/core/src/main/scala/io/cloudstate/proxy/crud/store/JdbcInMemoryStore.scala @@ -18,29 +18,21 @@ package io.cloudstate.proxy.crud.store import akka.util.ByteString import io.cloudstate.proxy.crud.store.JdbcStore.Key -import org.slf4j.{Logger, LoggerFactory} import scala.concurrent.Future final class JdbcInMemoryStore extends JdbcStore[Key, ByteString] { - private final val logger: Logger = LoggerFactory.getLogger(classOf[JdbcInMemoryStore]) + private var store = Map.empty[Key, ByteString] - private final var store = Map.empty[Key, ByteString] - - override def get(key: Key): Future[Option[ByteString]] = { - logger.info(s"get called with key - $key") - Future.successful(store.get(key)) - } + override def get(key: Key): Future[Option[ByteString]] = Future.successful(store.get(key)) override def update(key: Key, value: ByteString): Future[Unit] = { - logger.info(s"update called with key - $key and value - ${value.utf8String}") store += key -> value Future.unit } override def delete(key: Key): Future[Unit] = { - logger.info(s"delete called with key - $key") store -= key Future.unit } diff --git a/proxy/core/src/main/scala/io/cloudstate/proxy/crud/store/JdbcStoreFactory.scala b/proxy/core/src/main/scala/io/cloudstate/proxy/crud/store/JdbcStoreFactory.scala index de49d128c..85a00dd1b 100644 --- a/proxy/core/src/main/scala/io/cloudstate/proxy/crud/store/JdbcStoreFactory.scala +++ b/proxy/core/src/main/scala/io/cloudstate/proxy/crud/store/JdbcStoreFactory.scala @@ -28,17 +28,17 @@ object JdbcStoreFactory { final val JDBC = "jdbc" } -class JdbcStoreFactory(config: Config)(implicit ec: ExecutionContext) { +trait JdbcStoreFactory { - def buildCrudStore(): JdbcStore[Key, ByteString] = + def buildCrudStore(config: Config)(implicit ec: ExecutionContext): JdbcStore[Key, ByteString] = config.getString("crud.store-type") match { case IN_MEMORY => new JdbcInMemoryStore - case JDBC => buildJdbcCrudStore() + case JDBC => buildJdbcCrudStore(config) case other => throw new IllegalArgumentException(s"CRUD store-type must be one of: ${IN_MEMORY} or ${JDBC} but is '$other'") } - private def buildJdbcCrudStore(): JdbcStore[Key, ByteString] = { + private def buildJdbcCrudStore(config: Config)(implicit ec: ExecutionContext): JdbcStore[Key, ByteString] = { val slickDatabase = JdbcSlickDatabase(config) val tableConfiguration = new JdbcCrudStateTableConfiguration( config.getConfig("crud.jdbc-state-store") diff --git a/proxy/core/src/test/scala/io/cloudstate/proxy/crud/DatabaseExceptionHandlingSpec.scala b/proxy/core/src/test/scala/io/cloudstate/proxy/crud/DatabaseExceptionHandlingSpec.scala new file mode 100644 index 000000000..fe5f162d5 --- /dev/null +++ b/proxy/core/src/test/scala/io/cloudstate/proxy/crud/DatabaseExceptionHandlingSpec.scala @@ -0,0 +1,185 @@ +/* + * Copyright 2019 Lightbend Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.cloudstate.proxy.crud + +import akka.actor.{Actor, ActorRef, ActorSystem} +import akka.grpc.GrpcClientSettings +import akka.testkit.TestEvent.Mute +import akka.testkit.{EventFilter, TestActorRef} +import akka.util.ByteString +import com.google.protobuf.any.{Any => ScalaPbAny} +import io.cloudstate.protocol.crud.CrudClient +import com.google.protobuf.{ByteString => PbByteString} +import io.cloudstate.proxy.crud.store.{JdbcRepositoryImpl, JdbcStore} +import io.cloudstate.proxy.crud.store.JdbcStore.Key +import io.cloudstate.proxy.entity.{EntityCommand, UserFunctionReply} +import io.cloudstate.proxy.telemetry.AbstractTelemetrySpec +import io.cloudstate.testkit.TestService +import io.cloudstate.testkit.crud.CrudMessages + +import scala.concurrent.Future +import scala.concurrent.duration._ + +class DatabaseExceptionHandlingSpec extends AbstractTelemetrySpec { + + private val testkitConfig = """ + | include "test-in-memory" + | akka { + | loglevel = ERROR + | loggers = ["akka.testkit.TestEventListener"] + | remote.artery.canonical.port = 0 + | remote.artery.bind.port = "" + | } + """ + private val service = TestService() + private val entityConfiguration = CrudEntity.Configuration( + serviceName = "service", + userFunctionName = "test", + passivationTimeout = 30.seconds, + sendQueueSize = 100 + ) + + "The CrudEntity" should { + + "crash entity on init when loading state failures" in withTestKit(testkitConfig) { testKit => + import testKit._ + import system.dispatcher + + silentDeadLettersAndUnhandledMessages + + val client = CrudClient(GrpcClientSettings.connectToServiceAt("localhost", service.port).withTls(false)) + val repository = new JdbcRepositoryImpl(TestJdbcStore.storeWithGetFailure()) + val entity = watch(system.actorOf(CrudEntitySupervisor.props(client, entityConfiguration, repository), "entity")) + + val connection = service.crud.expectConnection() + connection.expectClosed() + } + + "crash entity on update state failures" in withTestKit(testkitConfig) { testKit => + import testKit._ + import CrudMessages._ + import system.dispatcher + + silentDeadLettersAndUnhandledMessages + + val forwardReply = forwardReplyActor(testActor) + + val client = CrudClient(GrpcClientSettings.connectToServiceAt("localhost", service.port).withTls(false)) + val repository = new JdbcRepositoryImpl(TestJdbcStore.storeWithUpdateFailure()) + val entity = watch(system.actorOf(CrudEntitySupervisor.props(client, entityConfiguration, repository), "entity")) + val emptyCommand = Some(protobufAny(EmptyJavaMessage)) + + val connection = service.crud.expectConnection() + connection.expect(init("service", "entity")) + entity.tell(EntityCommand(entityId = "test", name = "command1", emptyCommand), forwardReply) + connection.expect(command(1, "entity", "command1")) + + val state = ScalaPbAny("state", PbByteString.copyFromUtf8("state")) + connection.send(reply(1, EmptyJavaMessage, update(state))) + expectMsg(UserFunctionReply(clientActionFailure("Unexpected CRUD entity failure"))) + connection.expectClosed() + } + + "crash entity on delete state failures" in withTestKit(testkitConfig) { testKit => + import testKit._ + import CrudMessages._ + import system.dispatcher + + silentDeadLettersAndUnhandledMessages + + val forwardReply = forwardReplyActor(testActor) + + val client = CrudClient(GrpcClientSettings.connectToServiceAt("localhost", service.port).withTls(false)) + val repository = new JdbcRepositoryImpl(TestJdbcStore.storeWithDeleteFailure()) + val entity = watch(system.actorOf(CrudEntitySupervisor.props(client, entityConfiguration, repository), "entity")) + val emptyCommand = Some(protobufAny(EmptyJavaMessage)) + + val connection = service.crud.expectConnection() + connection.expect(init("service", "entity")) + + entity.tell(EntityCommand(entityId = "test", name = "command1", emptyCommand), forwardReply) + connection.expect(command(1, "entity", "command1")) + connection.send(reply(1, EmptyJavaMessage, update(ScalaPbAny("state", PbByteString.copyFromUtf8("state"))))) + expectMsg(UserFunctionReply(clientActionReply(messagePayload(EmptyJavaMessage)))) + + entity.tell(EntityCommand(entityId = "test", name = "command2", emptyCommand), forwardReply) + connection.expect(command(2, "entity", "command2")) + connection.send(reply(2, EmptyJavaMessage, delete())) + expectMsg(UserFunctionReply(clientActionFailure("Unexpected CRUD entity failure"))) + + connection.expectClosed() + } + } + + private final class TestJdbcStore(status: String) extends JdbcStore[Key, ByteString] { + import TestJdbcStore.JdbcStoreStatus._ + + private var store = Map.empty[Key, ByteString] + + override def get(key: Key): Future[Option[ByteString]] = + status match { + case `getFailure` => Future.failed(new RuntimeException("Database GET access failed because of boom!")) + case _ => Future.successful(store.get(key)) + } + override def update(key: Key, value: ByteString): Future[Unit] = + status match { + case `updateFailure` => Future.failed(new RuntimeException("Database Update access failed because of boom!")) + case _ => + store += key -> value + Future.unit + } + + override def delete(key: Key): Future[Unit] = + status match { + case `deleteFailure` => Future.failed(new RuntimeException("Database Delete access failed because of boom!")) + case _ => + store -= key + Future.unit + } + } + + private object TestJdbcStore { + + private object JdbcStoreStatus { + val normal = "normal" + val getFailure = "GetFailure" + val updateFailure = "UpdateFailure" + val deleteFailure = "DeleteFailure" + } + + def storeWithGetFailure(): JdbcStore[Key, ByteString] = new TestJdbcStore(JdbcStoreStatus.getFailure) + + def storeWithUpdateFailure(): JdbcStore[Key, ByteString] = new TestJdbcStore(JdbcStoreStatus.updateFailure) + + def storeWithDeleteFailure(): JdbcStore[Key, ByteString] = new TestJdbcStore(JdbcStoreStatus.deleteFailure) + } + + private def silentDeadLettersAndUnhandledMessages(implicit system: ActorSystem): Unit = { + // silence any dead letters or unhandled messages during shutdown (when using test event listener) + system.eventStream.publish(Mute(EventFilter.warning(pattern = ".*received dead letter.*"))) + system.eventStream.publish(Mute(EventFilter.warning(pattern = ".*unhandled message.*"))) + } + + private def forwardReplyActor(actor: ActorRef)(implicit system: ActorSystem) = + TestActorRef(new Actor { + def receive: Receive = { + case message => + actor forward message + } + }) + +} diff --git a/samples/java-crud-shopping-cart/src/main/java/io/cloudstate/samples/crud/shoppingcart/ShoppingCartEntity.java b/samples/java-crud-shopping-cart/src/main/java/io/cloudstate/samples/crud/shoppingcart/ShoppingCartEntity.java index 928a55d85..68a881fa9 100644 --- a/samples/java-crud-shopping-cart/src/main/java/io/cloudstate/samples/crud/shoppingcart/ShoppingCartEntity.java +++ b/samples/java-crud-shopping-cart/src/main/java/io/cloudstate/samples/crud/shoppingcart/ShoppingCartEntity.java @@ -30,7 +30,7 @@ import java.util.stream.Collectors; /** A CRUD entity. */ -@CrudEntity +@CrudEntity(persistenceId = "crud-shopping-cart") public class ShoppingCartEntity { private final String entityId; @@ -75,11 +75,7 @@ public Empty removeItem(Shoppingcart.RemoveLineItem item, CommandContext ctx) { - if (!entityId.equals(cartItem.getUserId())) { - ctx.fail("Cannot remove unknown cart " + cartItem.getUserId()); - } + public Empty removeCart(Shoppingcart.RemoveShoppingCart cart, CommandContext ctx) { ctx.deleteState(); return Empty.getDefaultInstance(); } diff --git a/tck/src/it/scala/io/cloudstate/tck/TCK.scala b/tck/src/it/scala/io/cloudstate/tck/TCK.scala index 195e75c46..8c1bdaa3d 100644 --- a/tck/src/it/scala/io/cloudstate/tck/TCK.scala +++ b/tck/src/it/scala/io/cloudstate/tck/TCK.scala @@ -33,13 +33,13 @@ class TCK extends Suites({ case _ => // All good } - combinations. + combinations. iterator. asScala. filter(section => verify(section.getString("name"))). map(c => new ManagedCloudStateTCK(TckConfiguration.fromConfig(c))). toVector - }: _*) with SequentialNestedSuiteExecution +}: _*) with SequentialNestedSuiteExecution object ManagedCloudStateTCK { def settings(config: TckConfiguration): CloudStateTCK.Settings = { diff --git a/tck/src/main/scala/io/cloudstate/tck/CloudStateTCK.scala b/tck/src/main/scala/io/cloudstate/tck/CloudStateTCK.scala index 342a6b02c..d8ee7d5d4 100644 --- a/tck/src/main/scala/io/cloudstate/tck/CloudStateTCK.scala +++ b/tck/src/main/scala/io/cloudstate/tck/CloudStateTCK.scala @@ -21,6 +21,14 @@ import akka.grpc.ServiceDescription import akka.testkit.TestKit import com.example.shoppingcart.persistence.domain import com.example.shoppingcart.shoppingcart._ +import com.example.crud.shoppingcart.shoppingcart.{ + AddLineItem => CrudAddLineItem, + Cart => CrudCart, + GetShoppingCart => CrudGetShoppingCart, + RemoveLineItem => CrudRemoveLineItem, + ShoppingCart => CrudShoppingCart, + ShoppingCartClient => CrudShoppingCartClient +} import com.google.protobuf.DescriptorProtos import com.google.protobuf.any.{Any => ScalaPbAny} import com.typesafe.config.{Config, ConfigFactory} @@ -28,13 +36,16 @@ import io.cloudstate.protocol.action.ActionProtocol import io.cloudstate.protocol.crdt.Crdt import io.cloudstate.protocol.crud.Crud import io.cloudstate.protocol.event_sourced._ +import io.cloudstate.tck.model.crud.crud.{CrudTckModel, CrudTwo} import io.cloudstate.testkit.InterceptService.InterceptorSettings import io.cloudstate.testkit.eventsourced.{EventSourcedMessages, InterceptEventSourcedService} +import io.cloudstate.testkit.crud.{CrudMessages} import io.cloudstate.testkit.{InterceptService, ServiceAddress, TestClient, TestProtocol} import io.grpc.StatusRuntimeException import io.cloudstate.tck.model.eventsourced.{EventSourcedTckModel, EventSourcedTwo} import org.scalatest.concurrent.ScalaFutures import org.scalatest.{BeforeAndAfterAll, MustMatchers, WordSpec} + import scala.collection.mutable import scala.concurrent.duration._ @@ -67,6 +78,7 @@ class CloudStateTCK(description: String, settings: CloudStateTCK.Settings) private[this] final val client = TestClient(settings.proxy.host, settings.proxy.port) private[this] final val shoppingCartClient = ShoppingCartClient(client.settings)(system) + private[this] final val crudShoppingCartClient = CrudShoppingCartClient(client.settings)(system) private[this] final val protocol = TestProtocol(settings.service.host, settings.service.port) @@ -138,6 +150,23 @@ class CloudStateTCK(description: String, settings: CloudStateTCK.Settings) entity.persistenceId must not be empty } + spec.entities.find(_.serviceName == CrudTckModel.name).foreach { entity => + serviceNames must contain("CrudTckModel") + entity.entityType mustBe Crud.name + entity.persistenceId mustBe "crud-tck-model" + } + + spec.entities.find(_.serviceName == CrudTwo.name).foreach { entity => + serviceNames must contain("CrudTwo") + entity.entityType mustBe Crud.name + } + + spec.entities.find(_.serviceName == CrudShoppingCart.name).foreach { entity => + serviceNames must contain("ShoppingCart") + entity.entityType mustBe Crud.name + entity.persistenceId must not be empty + } + enabledServices = spec.entities.map(_.serviceName) } } @@ -571,8 +600,402 @@ class CloudStateTCK(description: String, settings: CloudStateTCK.Settings) """[{"productId":"B14623482","name":"Basic","quantity":1.0}]""" } } + + "verify the HTTP API for CRUD ShoppingCart service" in testFor(CrudShoppingCart) { + import CrudShoppingCartVerifier._ + + def checkHttpRequest(path: String, body: String = null)(expected: => String): Unit = { + val response = client.http.request(path, body) + val expectedResponse = expected + response.futureValue mustBe expectedResponse + } + + val session = shoppingCartSession(interceptor) + + checkHttpRequest("crud/carts/foo") { + session.verifyConnection() + session.verifyGetInitialEmptyCart("foo") + """{"items":[]}""" + } + + checkHttpRequest("crud/cart/foo/items/add", """{"productId": "A14362347", "name": "Deluxe", "quantity": 5}""") { + session.verifyAddItem("foo", Item("A14362347", "Deluxe", 5), Cart(Item("A14362347", "Deluxe", 5))) + "{}" + } + + checkHttpRequest("crud/cart/foo/items/add", """{"productId": "B14623482", "name": "Basic", "quantity": 1}""") { + session.verifyAddItem("foo", + Item("B14623482", "Basic", 1), + Cart(Item("A14362347", "Deluxe", 5), Item("B14623482", "Basic", 1))) + "{}" + } + + checkHttpRequest("crud/cart/foo/items/add", """{"productId": "A14362347", "name": "Deluxe", "quantity": 2}""") { + session.verifyAddItem("foo", + Item("A14362347", "Deluxe", 2), + Cart(Item("B14623482", "Basic", 1), Item("A14362347", "Deluxe", 7))) + "{}" + } + + checkHttpRequest("crud/carts/foo") { + session.verifyGetCart("foo", shoppingCart(Item("B14623482", "Basic", 1), Item("A14362347", "Deluxe", 7))) + """{"items":[{"productId":"B14623482","name":"Basic","quantity":1},{"productId":"A14362347","name":"Deluxe","quantity":7}]}""" + } + + checkHttpRequest("crud/carts/foo/items") { + session.verifyGetCart("foo", shoppingCart(Item("B14623482", "Basic", 1), Item("A14362347", "Deluxe", 7))) + """[{"productId":"B14623482","name":"Basic","quantity":1.0},{"productId":"A14362347","name":"Deluxe","quantity":7.0}]""" + } + + checkHttpRequest("crud/cart/foo/items/A14362347/remove", "") { + session.verifyRemoveItem("foo", "A14362347", Cart(Item("B14623482", "Basic", 1))) + "{}" + } + + checkHttpRequest("crud/carts/foo") { + session.verifyGetCart("foo", shoppingCart(Item("B14623482", "Basic", 1))) + """{"items":[{"productId":"B14623482","name":"Basic","quantity":1}]}""" + } + + checkHttpRequest("crud/carts/foo/items") { + session.verifyGetCart("foo", shoppingCart(Item("B14623482", "Basic", 1))) + """[{"productId":"B14623482","name":"Basic","quantity":1.0}]""" + } + + checkHttpRequest("crud/carts/foo/remove", """{"userId": "foo"}""") { + session.verifyRemoveCart("foo") + "{}" + } + + checkHttpRequest("crud/carts/foo") { + session.verifyGetCart("foo", shoppingCart()) + """{"items":[]}""" + } + } + } + + "verifying model test: crud entities" must { + import CrudMessages._ + import io.cloudstate.tck.model.crud.crud._ + + val ServiceTwo = CrudTwo.name + + var entityId: Int = 0 + def nextEntityId(): String = { entityId += 1; s"entity:$entityId" } + + def crudTest(test: String => Any): Unit = + testFor(CrudTckModel, CrudTwo)(test(nextEntityId())) + + def updateState(value: String): RequestAction = + RequestAction(RequestAction.Action.Update(Update(value))) + + def updateStates(values: String*): Seq[RequestAction] = + values.map(updateState) + + def deleteState(): RequestAction = + RequestAction(RequestAction.Action.Delete(Delete())) + + def updateAndDeleteActions(values: String*): Seq[RequestAction] = + values.map(updateState) :+ deleteState() + + def deleteBetweenUpdateActions(first: String, second: String): Seq[RequestAction] = + Seq(updateState(first), deleteState(), updateState(second)) + + def forwardTo(id: String): RequestAction = + RequestAction(RequestAction.Action.Forward(Forward(id))) + + def sideEffectTo(id: String, synchronous: Boolean = false): RequestAction = + RequestAction(RequestAction.Action.Effect(Effect(id, synchronous))) + + def sideEffectsTo(ids: String*): Seq[RequestAction] = + ids.map(id => sideEffectTo(id)) + + def failWith(message: String): RequestAction = + RequestAction(RequestAction.Action.Fail(Fail(message))) + + def persisted(value: String): ScalaPbAny = + protobufAny(Persisted(value)) + + def update(value: String): Effects = + Effects.empty.withUpdateAction(persisted(value)) + + def delete(): Effects = + Effects.empty.withDeleteAction() + + def sideEffects(ids: String*): Effects = + ids.foldLeft(Effects.empty) { case (e, id) => e.withSideEffect(ServiceTwo, "Call", Request(id)) } + + "verify initial empty state" in crudTest { id => + protocol.crud + .connect() + .send(init(CrudTckModel.name, id)) + .send(command(1, id, "Process", Request(id))) + .expect(reply(1, Response())) + .passivate() + } + + "verify update state" in crudTest { id => + protocol.crud + .connect() + .send(init(CrudTckModel.name, id)) + .send(command(1, id, "Process", Request(id, updateStates("A")))) + .expect(reply(1, Response("A"), update("A"))) + .send(command(2, id, "Process", Request(id))) + .expect(reply(2, Response("A"))) + .passivate() + } + + "verify delete state" in crudTest { id => + protocol.crud + .connect() + .send(init(CrudTckModel.name, id)) + .send(command(1, id, "Process", Request(id, updateStates("A")))) + .expect(reply(1, Response("A"), update("A"))) + .send(command(2, id, "Process", Request(id, Seq(deleteState())))) + .expect(reply(2, Response(), delete())) + .send(command(3, id, "Process", Request(id))) + .expect(reply(3, Response())) + .passivate() + } + + "verify sub invocations with multiple update states" in crudTest { id => + protocol.crud + .connect() + .send(init(CrudTckModel.name, id)) + .send(command(1, id, "Process", Request(id, updateStates("A", "B", "C")))) + .expect(reply(1, Response("C"), update("C"))) + .send(command(2, id, "Process", Request(id))) + .expect(reply(2, Response("C"))) + .passivate() + } + + "verify sub invocations with multiple update states and delete states" in crudTest { id => + protocol.crud + .connect() + .send(init(CrudTckModel.name, id)) + .send(command(1, id, "Process", Request(id, updateAndDeleteActions("A", "B")))) + .expect(reply(1, Response(), delete())) + .send(command(2, id, "Process", Request(id))) + .expect(reply(2, Response())) + .passivate() + } + + "verify sub invocations with update, delete and update states" in crudTest { id => + protocol.crud + .connect() + .send(init(CrudTckModel.name, id)) + .send(command(1, id, "Process", Request(id, deleteBetweenUpdateActions("A", "B")))) + .expect(reply(1, Response("B"), update("B"))) + .send(command(2, id, "Process", Request(id))) + .expect(reply(2, Response("B"))) + .passivate() + } + + "verify rehydration after passivation" in crudTest { id => + protocol.crud + .connect() + .send(init(CrudTckModel.name, id)) + .send(command(1, id, "Process", Request(id, updateStates("A")))) + .expect(reply(1, Response("A"), update("A"))) + .send(command(2, id, "Process", Request(id, updateStates("B")))) + .expect(reply(2, Response("B"), update("B"))) + .send(command(3, id, "Process", Request(id, updateStates("C")))) + .expect(reply(3, Response("C"), update("C"))) + .send(command(4, id, "Process", Request(id, updateStates("D")))) + .expect(reply(4, Response("D"), update("D"))) + .passivate() + protocol.crud + .connect() + .send(init(CrudTckModel.name, id, state(persisted("D")))) + .send(command(1, id, "Process", Request(id, updateStates("E")))) + .expect(reply(1, Response("E"), update("E"))) + .send(command(2, id, "Process", Request(id))) + .expect(reply(2, Response("E"))) + .passivate() + } + + "verify reply with multiple side effects" in crudTest { id => + protocol.crud + .connect() + .send(init(CrudTckModel.name, id)) + .send(command(1, id, "Process", Request(id, sideEffectsTo("1", "2", "3")))) + .expect(reply(1, Response(), sideEffects("1", "2", "3"))) + .passivate() + } + + "verify reply with side effect to second service" in crudTest { id => + protocol.crud + .connect() + .send(init(CrudTckModel.name, id)) + .send(command(1, id, "Process", Request(id, Seq(sideEffectTo(id))))) + .expect(reply(1, Response(), sideEffect(ServiceTwo, "Call", Request(id)))) + .passivate() + } + + "verify reply with multiple side effects and state" in crudTest { id => + val actions = updateStates("A", "B", "C", "D", "E") ++ sideEffectsTo("1", "2", "3") + val effects = sideEffects("1", "2", "3").withUpdateAction(persisted("E")) + protocol.crud + .connect() + .send(init(CrudTckModel.name, id)) + .send(command(1, id, "Process", Request(id, actions))) + .expect(reply(1, Response("E"), effects)) + .passivate() + } + + "verify synchronous side effect to second service" in crudTest { id => + protocol.crud + .connect() + .send(init(CrudTckModel.name, id)) + .send(command(1, id, "Process", Request(id, Seq(sideEffectTo(id, synchronous = true))))) + .expect(reply(1, Response(), sideEffect(ServiceTwo, "Call", Request(id), synchronous = true))) + .passivate() + } + + "verify forward to second service" in crudTest { id => + protocol.crud + .connect() + .send(init(CrudTckModel.name, id)) + .send(command(1, id, "Process", Request(id, Seq(forwardTo(id))))) + .expect(forward(1, ServiceTwo, "Call", Request(id))) + .passivate() + } + + "verify forward with updated state to second service" in crudTest { id => + protocol.crud + .connect() + .send(init(CrudTckModel.name, id)) + .send(command(1, id, "Process", Request(id, Seq(updateState("A"), forwardTo(id))))) + .expect(forward(1, ServiceTwo, "Call", Request(id), update("A"))) + .passivate() + } + + "verify forward and side effect to second service" in crudTest { id => + protocol.crud + .connect() + .send(init(CrudTckModel.name, id)) + .send(command(1, id, "Process", Request(id, Seq(sideEffectTo(id), forwardTo(id))))) + .expect(forward(1, ServiceTwo, "Call", Request(id), sideEffect(ServiceTwo, "Call", Request(id)))) + .passivate() + } + + "verify failure action" in crudTest { id => + protocol.crud + .connect() + .send(init(CrudTckModel.name, id)) + .send(command(1, id, "Process", Request(id, Seq(failWith("expected failure"))))) + .expect(actionFailure(1, "expected failure")) + .passivate() + } + + "verify connection after failure action" in crudTest { id => + protocol.crud + .connect() + .send(init(CrudTckModel.name, id)) + .send(command(1, id, "Process", Request(id, Seq(updateState("A"))))) + .expect(reply(1, Response("A"), update("A"))) + .send(command(2, id, "Process", Request(id, Seq(failWith("expected failure"))))) + .expect(actionFailure(2, "expected failure")) + .send(command(3, id, "Process", Request(id))) + .expect(reply(3, Response("A"))) + .passivate() + } + + "verify failure action do not allow side effects" in crudTest { id => + protocol.crud + .connect() + .send(init(CrudTckModel.name, id)) + .send(command(1, id, "Process", Request(id, Seq(sideEffectTo(id), failWith("expected failure"))))) + .expect(actionFailure(1, "expected failure")) + .passivate() + } } + "verifying app test: crud shopping cart" must { + import CrudMessages._ + import CrudShoppingCartVerifier._ + + def verifyGetInitialEmptyCart(session: CrudShoppingCartVerifier, cartId: String): Unit = { + crudShoppingCartClient.getCart(CrudGetShoppingCart(cartId)).futureValue mustBe CrudCart() + session.verifyConnection() + session.verifyGetInitialEmptyCart(cartId) + } + + def verifyGetCart(session: CrudShoppingCartVerifier, cartId: String, expected: Item*): Unit = { + val expectedCart = shoppingCart(expected: _*) + crudShoppingCartClient.getCart(CrudGetShoppingCart(cartId)).futureValue mustBe expectedCart + session.verifyGetCart(cartId, expectedCart) + } + + def verifyAddItem(session: CrudShoppingCartVerifier, cartId: String, item: Item, expected: Cart): Unit = { + val addLineItem = CrudAddLineItem(cartId, item.id, item.name, item.quantity) + crudShoppingCartClient.addItem(addLineItem).futureValue mustBe EmptyScalaMessage + session.verifyAddItem(cartId, item, expected) + } + + def verifyRemoveItem(session: CrudShoppingCartVerifier, cartId: String, itemId: String, expected: Cart): Unit = { + val removeLineItem = CrudRemoveLineItem(cartId, itemId) + crudShoppingCartClient.removeItem(removeLineItem).futureValue mustBe EmptyScalaMessage + session.verifyRemoveItem(cartId, itemId, expected) + } + + def verifyAddItemFailure(session: CrudShoppingCartVerifier, cartId: String, item: Item): Unit = { + val addLineItem = CrudAddLineItem(cartId, item.id, item.name, item.quantity) + val error = crudShoppingCartClient.addItem(addLineItem).failed.futureValue + error mustBe a[StatusRuntimeException] + val description = error.asInstanceOf[StatusRuntimeException].getStatus.getDescription + session.verifyAddItemFailure(cartId, item, description) + } + + def verifyRemoveItemFailure(session: CrudShoppingCartVerifier, cartId: String, itemId: String): Unit = { + val removeLineItem = CrudRemoveLineItem(cartId, itemId) + val error = crudShoppingCartClient.removeItem(removeLineItem).failed.futureValue + error mustBe a[StatusRuntimeException] + val description = error.asInstanceOf[StatusRuntimeException].getStatus.getDescription + session.verifyRemoveItemFailure(cartId, itemId, description) + } + + "verify get cart, add item, remove item, and failures" in testFor(CrudShoppingCart) { + val session = shoppingCartSession(interceptor) + verifyGetInitialEmptyCart(session, "cart:1") // initial empty state + + // add the first product and pass the expected state + verifyAddItem(session, "cart:1", Item("product:1", "Product1", 1), Cart(Item("product:1", "Product1", 1))) + + // add the second product and pass the expected state + verifyAddItem( + session, + "cart:1", + Item("product:2", "Product2", 2), + Cart(Item("product:1", "Product1", 1), Item("product:2", "Product2", 2)) + ) + + // increase first product and pass the expected state + verifyAddItem( + session, + "cart:1", + Item("product:1", "Product1", 11), + Cart(Item("product:2", "Product2", 2), Item("product:1", "Product1", 12)) + ) + + // increase second product and pass the expected state + verifyAddItem( + session, + "cart:1", + Item("product:2", "Product2", 31), + Cart(Item("product:1", "Product1", 12), Item("product:2", "Product2", 33)) + ) + + verifyGetCart(session, "cart:1", Item("product:1", "Product1", 12), Item("product:2", "Product2", 33)) // check state + + // remove first product and pass the expected state + verifyRemoveItem(session, "cart:1", "product:1", Cart(Item("product:2", "Product2", 33))) + verifyAddItemFailure(session, "cart:1", Item("product:2", "Product2", -7)) // add negative quantity + verifyAddItemFailure(session, "cart:1", Item("product:1", "Product1", 0)) // add zero quantity + verifyRemoveItemFailure(session, "cart:1", "product:1") // remove non-existing product + verifyGetCart(session, "cart:1", Item("product:2", "Product2", 33)) // check final state + } + } } } diff --git a/tck/src/main/scala/io/cloudstate/tck/CrudShoppingCartVerifier.scala b/tck/src/main/scala/io/cloudstate/tck/CrudShoppingCartVerifier.scala new file mode 100644 index 000000000..df606b801 --- /dev/null +++ b/tck/src/main/scala/io/cloudstate/tck/CrudShoppingCartVerifier.scala @@ -0,0 +1,119 @@ +/* + * Copyright 2019 Lightbend Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.cloudstate.tck + +import io.cloudstate.testkit.InterceptService +import com.example.crud.shoppingcart.shoppingcart.{ + LineItem, + RemoveShoppingCart, + AddLineItem => CrudAddLineItem, + Cart => CrudCart, + GetShoppingCart => CrudGetShoppingCart, + RemoveLineItem => CrudRemoveLineItem, + ShoppingCart => CrudShoppingCart +} +import com.example.crud.shoppingcart.persistence.domain +import io.cloudstate.protocol.crud.CrudStreamOut +import io.cloudstate.testkit.crud.{CrudMessages, InterceptCrudService} +import org.scalatest.MustMatchers + +import scala.collection.mutable + +object CrudShoppingCartVerifier { + case class Item(id: String, name: String, quantity: Int) + case class Cart(items: Item*) + + def shoppingCartSession(interceptor: InterceptService): CrudShoppingCartVerifier = + new CrudShoppingCartVerifier(interceptor) + + def shoppingCart(items: Item*): CrudCart = CrudCart(items.map(i => LineItem(i.id, i.name, i.quantity))) + + def domainShoppingCart(cart: Cart): domain.Cart = + domain.Cart(cart.items.map(i => domain.LineItem(i.id, i.name, i.quantity))) +} + +class CrudShoppingCartVerifier(interceptor: InterceptService) extends MustMatchers { + import CrudMessages._ + import CrudShoppingCartVerifier._ + + private val commandIds = mutable.Map.empty[String, Long] + private var connection: InterceptCrudService.Connection = _ + + private def nextCommandId(cartId: String): Long = commandIds.updateWith(cartId)(_.map(_ + 1).orElse(Some(1L))).get + + def verifyConnection(): Unit = connection = interceptor.expectCrudConnection() + + def verifyGetInitialEmptyCart(cartId: String): Unit = { + val commandId = nextCommandId(cartId) + connection.expectClient(init(CrudShoppingCart.name, cartId)) + connection.expectClient(command(commandId, cartId, "GetCart", CrudGetShoppingCart(cartId))) + connection.expectService(reply(commandId, CrudCart())) + connection.expectNoInteraction() + } + + def verifyGetCart(cartId: String, expected: CrudCart): Unit = { + val commandId = nextCommandId(cartId) + connection.expectClient(command(commandId, cartId, "GetCart", CrudGetShoppingCart(cartId))) + connection.expectService(reply(commandId, expected)) + connection.expectNoInteraction() + } + + def verifyAddItem(cartId: String, item: Item, expected: Cart): Unit = { + val commandId = nextCommandId(cartId) + val addLineItem = CrudAddLineItem(cartId, item.id, item.name, item.quantity) + val cartUpdated = domainShoppingCart(expected) + connection.expectClient(command(commandId, cartId, "AddItem", addLineItem)) + val replied = connection.expectServiceMessage[CrudStreamOut.Message.Reply] + replied mustBe reply(commandId, EmptyScalaMessage, update(cartUpdated)) + connection.expectNoInteraction() + } + + def verifyRemoveItem(cartId: String, itemId: String, expected: Cart): Unit = { + val commandId = nextCommandId(cartId) + val removeLineItem = CrudRemoveLineItem(cartId, itemId) + val cartUpdated = domainShoppingCart(expected) + connection.expectClient(command(commandId, cartId, "RemoveItem", removeLineItem)) + val replied = connection.expectServiceMessage[CrudStreamOut.Message.Reply] + replied mustBe reply(commandId, EmptyScalaMessage, update(cartUpdated)) + connection.expectNoInteraction() + } + + def verifyRemoveCart(cartId: String): Unit = { + val commandId = nextCommandId(cartId) + val removeCart = RemoveShoppingCart(cartId) + connection.expectClient(command(commandId, cartId, "RemoveCart", removeCart)) + val replied = connection.expectServiceMessage[CrudStreamOut.Message.Reply] + replied mustBe reply(commandId, EmptyScalaMessage, delete()) + connection.expectNoInteraction() + } + + def verifyAddItemFailure(cartId: String, item: Item, failure: String): Unit = { + val commandId = nextCommandId(cartId) + val addLineItem = CrudAddLineItem(cartId, item.id, item.name, item.quantity) + connection.expectClient(command(commandId, cartId, "AddItem", addLineItem)) + connection.expectService(actionFailure(commandId, failure)) + connection.expectNoInteraction() + } + + def verifyRemoveItemFailure(cartId: String, itemId: String, failure: String): Unit = { + val commandId = nextCommandId(cartId) + val removeLineItem = CrudRemoveLineItem(cartId, itemId) + connection.expectClient(command(commandId, cartId, "RemoveItem", removeLineItem)) + connection.expectService(actionFailure(commandId, failure)) + connection.expectNoInteraction() + } +} diff --git a/testkit/src/main/scala/io/cloudstate/testkit/InterceptService.scala b/testkit/src/main/scala/io/cloudstate/testkit/InterceptService.scala index 182b80689..59d53aa25 100644 --- a/testkit/src/main/scala/io/cloudstate/testkit/InterceptService.scala +++ b/testkit/src/main/scala/io/cloudstate/testkit/InterceptService.scala @@ -22,8 +22,10 @@ import akka.http.scaladsl.Http import akka.testkit.{TestKit, TestProbe} import com.typesafe.config.{Config, ConfigFactory} import io.cloudstate.testkit.InterceptService.InterceptorSettings +import io.cloudstate.testkit.crud.InterceptCrudService import io.cloudstate.testkit.discovery.InterceptEntityDiscovery import io.cloudstate.testkit.eventsourced.InterceptEventSourcedService + import scala.concurrent.Await import scala.concurrent.duration._ @@ -35,6 +37,7 @@ final class InterceptService(settings: InterceptorSettings) { private val context = new InterceptorContext(settings.intercept.host, settings.intercept.port) private val entityDiscovery = new InterceptEntityDiscovery(context) private val eventSourced = new InterceptEventSourcedService(context) + private val crud = new InterceptCrudService(context) import context.system @@ -42,7 +45,7 @@ final class InterceptService(settings: InterceptorSettings) { Await.result( Http().bindAndHandleAsync( - handler = entityDiscovery.handler orElse eventSourced.handler, + handler = entityDiscovery.handler orElse eventSourced.handler orElse crud.handler, interface = settings.bind.host, port = settings.bind.port ), @@ -53,9 +56,12 @@ final class InterceptService(settings: InterceptorSettings) { def expectEventSourcedConnection(): InterceptEventSourcedService.Connection = eventSourced.expectConnection() + def expectCrudConnection(): InterceptCrudService.Connection = crud.expectConnection() + def terminate(): Unit = { entityDiscovery.terminate() eventSourced.terminate() + crud.terminate() context.terminate() } } diff --git a/testkit/src/main/scala/io/cloudstate/testkit/crud/CrudMessages.scala b/testkit/src/main/scala/io/cloudstate/testkit/crud/CrudMessages.scala index 5088c0ba5..742790b42 100644 --- a/testkit/src/main/scala/io/cloudstate/testkit/crud/CrudMessages.scala +++ b/testkit/src/main/scala/io/cloudstate/testkit/crud/CrudMessages.scala @@ -17,7 +17,8 @@ package io.cloudstate.testkit.crud import com.google.protobuf.any.{Any => ScalaPbAny} -import com.google.protobuf.{Empty => JavaPbEmpty, Message => JavaPbMessage} +import com.google.protobuf.empty.{Empty => ScalaPbEmpty} +import com.google.protobuf.{Any => JavaPbAny, Empty => JavaPbEmpty, Message => JavaPbMessage} import io.cloudstate.protocol.crud.CrudAction.Action.Update import io.cloudstate.protocol.crud.CrudAction.Action.Delete import io.cloudstate.protocol.entity._ @@ -38,6 +39,12 @@ object CrudMessages { def withDeleteAction(): Effects = copy(crudAction = Some(CrudAction(Delete(CrudDelete())))) + + def withSideEffect(service: String, command: String, message: ScalaPbMessage): Effects = + withSideEffect(service, command, messagePayload(message), synchronous = false) + + def withSideEffect(service: String, command: String, payload: Option[ScalaPbAny], synchronous: Boolean): Effects = + copy(sideEffects = sideEffects :+ SideEffect(service, command, payload, synchronous)) } object Effects { @@ -46,7 +53,7 @@ object CrudMessages { val EmptyInMessage: InMessage = InMessage.Empty val EmptyJavaMessage: JavaPbMessage = JavaPbEmpty.getDefaultInstance - val EmptyScalaMessage: ScalaPbAny = protobufAny(EmptyJavaMessage) + val EmptyScalaMessage: ScalaPbMessage = ScalaPbEmpty.defaultInstance def init(serviceName: String, entityId: String): InMessage = init(serviceName, entityId, Some(CrudInitState())) @@ -93,6 +100,18 @@ object CrudMessages { def reply(id: Long, payload: Option[ScalaPbAny], effects: Effects): OutMessage = OutMessage.Reply(CrudReply(id, clientActionReply(payload), effects.sideEffects, effects.crudAction)) + def replyAction(id: Long, action: Option[ClientAction], effects: Effects): OutMessage = + OutMessage.Reply(CrudReply(id, action, effects.sideEffects, effects.crudAction)) + + def forward(id: Long, service: String, command: String, payload: ScalaPbMessage): OutMessage = + forward(id, service, command, payload, Effects.empty) + + def forward(id: Long, service: String, command: String, payload: ScalaPbMessage, effects: Effects): OutMessage = + forward(id, service, command, messagePayload(payload), effects) + + def forward(id: Long, service: String, command: String, payload: Option[ScalaPbAny], effects: Effects): OutMessage = + replyAction(id, clientActionForward(service, command, payload), effects) + def actionFailure(id: Long, description: String): OutMessage = OutMessage.Reply(CrudReply(id, clientActionFailure(id, description))) @@ -111,6 +130,18 @@ object CrudMessages { def clientActionFailure(id: Long, description: String): Option[ClientAction] = Some(ClientAction(ClientAction.Action.Failure(Failure(id, description)))) + def clientActionForward(service: String, command: String, payload: Option[ScalaPbAny]): Option[ClientAction] = + Some(ClientAction(ClientAction.Action.Forward(Forward(service, command, payload)))) + + def sideEffect(service: String, command: String, payload: ScalaPbMessage, synchronous: Boolean): Effects = + sideEffect(service, command, messagePayload(payload), synchronous) + + def sideEffect(service: String, command: String, payload: ScalaPbMessage): Effects = + sideEffect(service, command, messagePayload(payload), synchronous = false) + + def sideEffect(service: String, command: String, payload: Option[ScalaPbAny], synchronous: Boolean): Effects = + Effects.empty.withSideEffect(service, command, payload, synchronous) + def update(state: JavaPbMessage): Effects = Effects.empty.withUpdateAction(state) @@ -126,10 +157,14 @@ object CrudMessages { def messagePayload(message: ScalaPbMessage): Option[ScalaPbAny] = Option(message).map(protobufAny) - def protobufAny(message: JavaPbMessage): ScalaPbAny = - ScalaPbAny("type.googleapis.com/" + message.getDescriptorForType.getFullName, message.toByteString) + def protobufAny(message: JavaPbMessage): ScalaPbAny = message match { + case javaPbAny: JavaPbAny => ScalaPbAny.fromJavaProto(javaPbAny) + case _ => ScalaPbAny("type.googleapis.com/" + message.getDescriptorForType.getFullName, message.toByteString) + } - def protobufAny(message: ScalaPbMessage): ScalaPbAny = - ScalaPbAny("type.googleapis.com/" + message.companion.scalaDescriptor.fullName, message.toByteString) + def protobufAny(message: ScalaPbMessage): ScalaPbAny = message match { + case scalaPbAny: ScalaPbAny => scalaPbAny + case _ => ScalaPbAny("type.googleapis.com/" + message.companion.scalaDescriptor.fullName, message.toByteString) + } } diff --git a/testkit/src/main/scala/io/cloudstate/testkit/crud/InterceptCrudService.scala b/testkit/src/main/scala/io/cloudstate/testkit/crud/InterceptCrudService.scala new file mode 100644 index 000000000..00e1f2ce8 --- /dev/null +++ b/testkit/src/main/scala/io/cloudstate/testkit/crud/InterceptCrudService.scala @@ -0,0 +1,95 @@ +/* + * Copyright 2019 Lightbend Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.cloudstate.testkit.crud + +import akka.NotUsed +import akka.http.scaladsl.model.{HttpRequest, HttpResponse} +import akka.stream.scaladsl.{Sink, Source} +import akka.testkit.TestProbe +import io.cloudstate.protocol.crud.{Crud, CrudClient, CrudHandler, CrudStreamIn, CrudStreamOut} +import io.cloudstate.testkit.InterceptService.InterceptorContext + +import scala.concurrent.Future +import scala.concurrent.duration._ +import scala.reflect.ClassTag + +final class InterceptCrudService(context: InterceptorContext) { + import InterceptCrudService._ + + private val interceptor = new CrudInterceptor(context) + + def expectConnection(): Connection = context.probe.expectMsgType[Connection] + + def handler: PartialFunction[HttpRequest, Future[HttpResponse]] = CrudHandler.partial(interceptor)(context.system) + + def terminate(): Unit = interceptor.terminate() +} + +object InterceptCrudService { + + final class CrudInterceptor(context: InterceptorContext) extends Crud { + private val client = CrudClient(context.clientSettings)(context.system) + + override def handle(in: Source[CrudStreamIn, NotUsed]): Source[CrudStreamOut, NotUsed] = { + val connection = new Connection(context) + context.probe.ref ! connection + client.handle(in.alsoTo(connection.inSink)).alsoTo(connection.outSink) + } + + def terminate(): Unit = client.close() + } + + object Connection { + case object Complete + final case class Error(cause: Throwable) + } + + final class Connection(context: InterceptorContext) { + import Connection._ + + private[this] val in = TestProbe("CrudInProbe")(context.system) + private[this] val out = TestProbe("CrudOutProbe")(context.system) + + private[testkit] def inSink: Sink[CrudStreamIn, NotUsed] = Sink.actorRef(in.ref, Complete, Error.apply) + private[testkit] def outSink: Sink[CrudStreamOut, NotUsed] = Sink.actorRef(out.ref, Complete, Error.apply) + + def expectClient(message: CrudStreamIn.Message): Connection = { + in.expectMsg(CrudStreamIn(message)) + this + } + + def expectService(message: CrudStreamOut.Message): Connection = { + out.expectMsg(CrudStreamOut(message)) + this + } + + def expectServiceMessage[T](implicit classTag: ClassTag[T]): T = + expectServiceMessageClass(classTag.runtimeClass.asInstanceOf[Class[T]]) + + def expectServiceMessageClass[T](messageClass: Class[T]): T = { + val message = out.expectMsgType[CrudStreamOut].message + assert(messageClass.isInstance(message), s"expected message $messageClass, found ${message.getClass} ($message)") + message.asInstanceOf[T] + } + + def expectNoInteraction(timeout: FiniteDuration = 0.seconds): Connection = { + in.expectNoMessage(timeout) + out.expectNoMessage(timeout) + this + } + } +} diff --git a/testkit/src/main/scala/io/cloudstate/testkit/crud/TestCrudService.scala b/testkit/src/main/scala/io/cloudstate/testkit/crud/TestCrudService.scala index bfa5df86c..2c0fcd787 100644 --- a/testkit/src/main/scala/io/cloudstate/testkit/crud/TestCrudService.scala +++ b/testkit/src/main/scala/io/cloudstate/testkit/crud/TestCrudService.scala @@ -32,9 +32,11 @@ import io.cloudstate.testkit.discovery.TestEntityDiscoveryService import scala.concurrent.Future class TestCrudService(context: TestServiceContext) { - private val testCrud = new TestCrudService.TestCrud(context) + import TestCrudService._ - def expectConnection(): TestCrudService.Connection = context.probe.expectMsgType[TestCrudService.Connection] + private val testCrud = new TestCrud(context) + + def expectConnection(): Connection = context.probe.expectMsgType[Connection] def handler: PartialFunction[HttpRequest, Future[HttpResponse]] = CrudHandler.partial(testCrud)(context.system) diff --git a/testkit/src/main/scala/io/cloudstate/testkit/discovery/InterceptEntityDiscovery.scala b/testkit/src/main/scala/io/cloudstate/testkit/discovery/InterceptEntityDiscovery.scala index 2343e80b1..80c47282f 100644 --- a/testkit/src/main/scala/io/cloudstate/testkit/discovery/InterceptEntityDiscovery.scala +++ b/testkit/src/main/scala/io/cloudstate/testkit/discovery/InterceptEntityDiscovery.scala @@ -21,10 +21,12 @@ import akka.testkit.{TestKit, TestProbe} import com.google.protobuf.empty.{Empty => ScalaPbEmpty} import io.cloudstate.protocol.action.ActionProtocol import io.cloudstate.protocol.crdt.Crdt +import io.cloudstate.protocol.crud.Crud import io.cloudstate.protocol.entity._ import io.cloudstate.protocol.event_sourced.EventSourced import io.cloudstate.testkit.BuildInfo import io.cloudstate.testkit.InterceptService.InterceptorContext + import scala.concurrent.duration.FiniteDuration import scala.concurrent.{Await, Future} import scala.util.{Failure, Success} @@ -81,7 +83,7 @@ object InterceptEntityDiscovery { protocolMinorVersion = BuildInfo.protocolMinorVersion, proxyName = BuildInfo.name, proxyVersion = BuildInfo.version, - supportedEntityTypes = Seq(ActionProtocol.name, Crdt.name, EventSourced.name) + supportedEntityTypes = Seq(ActionProtocol.name, Crdt.name, EventSourced.name, Crud.name) ) def expectOnline(context: InterceptorContext, timeout: FiniteDuration): Unit = { From e7ae59c679e7e3234ef94d24d0eca671781885ff Mon Sep 17 00:00:00 2001 From: Guy Youansi Date: Fri, 16 Oct 2020 21:02:29 +0200 Subject: [PATCH 66/93] removed class --- .../io/cloudstate/tck/CloudStateCrudTCK.scala | 125 ------------------ 1 file changed, 125 deletions(-) delete mode 100644 tck/src/main/scala/io/cloudstate/tck/CloudStateCrudTCK.scala diff --git a/tck/src/main/scala/io/cloudstate/tck/CloudStateCrudTCK.scala b/tck/src/main/scala/io/cloudstate/tck/CloudStateCrudTCK.scala deleted file mode 100644 index d8cc92576..000000000 --- a/tck/src/main/scala/io/cloudstate/tck/CloudStateCrudTCK.scala +++ /dev/null @@ -1,125 +0,0 @@ -/* - * Copyright 2019 Lightbend Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.cloudstate.tck - -import akka.actor.ActorSystem -import akka.grpc.ServiceDescription -import akka.testkit.TestKit -import com.example.crud.shoppingcart.shoppingcart.{ShoppingCart, ShoppingCartClient} -import com.google.protobuf.DescriptorProtos -import com.typesafe.config.ConfigFactory -import io.cloudstate.protocol.action.ActionProtocol -import io.cloudstate.protocol.crdt.Crdt -import io.cloudstate.protocol.crud.Crud -import io.cloudstate.protocol.event_sourced.EventSourced -import io.cloudstate.tck.model.crud.crud.{CrudTckModel, CrudTwo} -import io.cloudstate.testkit.InterceptService.InterceptorSettings -import io.cloudstate.testkit.{InterceptService, TestClient, TestProtocol} -import org.scalatest.{BeforeAndAfterAll, MustMatchers, WordSpec} -import org.scalatest.concurrent.ScalaFutures - -import scala.concurrent.duration._ - -class ConfiguredCloudStateCrudTCK extends CloudStateCrudTCK(CloudStateTCK.Settings.fromConfig(ConfigFactory.load())) - -class CloudStateCrudTCK(description: String, settings: CloudStateTCK.Settings) - extends WordSpec - with MustMatchers - with BeforeAndAfterAll - with ScalaFutures { - - def this(settings: CloudStateTCK.Settings) = this("", settings) - - private[this] final val system = ActorSystem("CloudStateCrudTCK", ConfigFactory.load("tck")) - - private[this] final val client = TestClient(settings.proxy.host, settings.proxy.port) - private[this] final val shoppingCartClient = ShoppingCartClient(client.settings)(system) - - private[this] final val protocol = TestProtocol(settings.service.host, settings.service.port) - - @volatile private[this] final var interceptor: InterceptService = _ - @volatile private[this] final var enabledServices = Seq.empty[String] - - override implicit val patienceConfig: PatienceConfig = PatienceConfig(timeout = 3.seconds, interval = 100.millis) - - override def beforeAll(): Unit = - interceptor = new InterceptService(InterceptorSettings(bind = settings.tck, intercept = settings.service)) - - override def afterAll(): Unit = - try shoppingCartClient.close().futureValue - finally try client.terminate() - finally try protocol.terminate() - finally interceptor.terminate() - - def expectProxyOnline(): Unit = - TestKit.awaitCond(client.http.probe(), max = 10.seconds) - - def testFor(services: ServiceDescription*)(test: => Any): Unit = { - val enabled = services.map(_.name).forall(enabledServices.contains) - if (enabled) test else pending - } - - ("Cloudstate Crud TCK " + description) when { - "verifying discovery protocol" must { - "verify proxy info and entity discovery" in { - import scala.jdk.CollectionConverters._ - - expectProxyOnline() - - val discovery = interceptor.expectEntityDiscovery() - - val info = discovery.expectProxyInfo() - - info.protocolMajorVersion mustBe 0 - info.protocolMinorVersion mustBe 2 - - info.supportedEntityTypes must contain theSameElementsAs Seq( - EventSourced.name, - Crdt.name, - Crud.name, - ActionProtocol.name - ) - - val spec = discovery.expectEntitySpec() - - val descriptorSet = DescriptorProtos.FileDescriptorSet.parseFrom(spec.proto) - val serviceNames = descriptorSet.getFileList.asScala.flatMap(_.getServiceList.asScala.map(_.getName)) - - serviceNames.size mustBe spec.entities.size - - spec.entities.find(_.serviceName == CrudTckModel.name).foreach { entity => - serviceNames must contain("CrudTckModel") - entity.entityType mustBe Crud.name - entity.persistenceId mustBe "crud-tck-model" - } - - spec.entities.find(_.serviceName == CrudTwo.name).foreach { entity => - serviceNames must contain("CrudTwo") - entity.entityType mustBe Crud.name - } - - spec.entities.find(_.serviceName == ShoppingCart.name).foreach { entity => - serviceNames must contain("ShoppingCart") - entity.entityType mustBe Crud.name - entity.persistenceId must not be empty - } - - enabledServices = spec.entities.map(_.serviceName) - } - } - } -} From 3f9789e515f87bb68122f44d4d8c8b4aad2d006d Mon Sep 17 00:00:00 2001 From: Guy Youansi Date: Fri, 16 Oct 2020 21:44:53 +0200 Subject: [PATCH 67/93] add metadata --- .../javasupport/crud/CommandContext.java | 4 +++- .../crud/AnnotationBasedCrudSupport.scala | 3 ++- .../javasupport/impl/crud/CrudImpl.scala | 5 +++- .../crud/AnnotationBasedCrudSupportSpec.scala | 13 +++++++---- protocols/protocol/cloudstate/crud.proto | 2 +- .../proxy/EntityDiscoveryManager.scala | 17 +------------- ...ry.scala => CrudStoreSupportFactory.scala} | 6 ++--- .../scala/io/cloudstate/proxy/crud/readme.md | 23 ------------------- ...reFactory.scala => JdbcStoreSupport.scala} | 12 +++++----- 9 files changed, 28 insertions(+), 57 deletions(-) rename proxy/core/src/main/scala/io/cloudstate/proxy/crud/{CrudSupportFactory.scala => CrudStoreSupportFactory.scala} (97%) delete mode 100644 proxy/core/src/main/scala/io/cloudstate/proxy/crud/readme.md rename proxy/core/src/main/scala/io/cloudstate/proxy/crud/store/{JdbcStoreFactory.scala => JdbcStoreSupport.scala} (79%) diff --git a/java-support/src/main/java/io/cloudstate/javasupport/crud/CommandContext.java b/java-support/src/main/java/io/cloudstate/javasupport/crud/CommandContext.java index 56066e040..56995781e 100644 --- a/java-support/src/main/java/io/cloudstate/javasupport/crud/CommandContext.java +++ b/java-support/src/main/java/io/cloudstate/javasupport/crud/CommandContext.java @@ -18,6 +18,7 @@ import io.cloudstate.javasupport.ClientActionContext; import io.cloudstate.javasupport.EffectContext; +import io.cloudstate.javasupport.MetadataContext; import java.util.Optional; @@ -28,7 +29,8 @@ * or deleting the entity state in response to a command, along with forwarding the result to other * entities, and performing side effects on other entities. */ -public interface CommandContext extends CrudContext, ClientActionContext, EffectContext { +public interface CommandContext + extends CrudContext, ClientActionContext, EffectContext, MetadataContext { /** * The name of the command being executed. diff --git a/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/AnnotationBasedCrudSupport.scala b/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/AnnotationBasedCrudSupport.scala index bab718f7f..e2abf8fd6 100644 --- a/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/AnnotationBasedCrudSupport.scala +++ b/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/AnnotationBasedCrudSupport.scala @@ -20,7 +20,7 @@ import java.lang.reflect.{Constructor, InvocationTargetException, Method} import java.util.Optional import com.google.protobuf.{Descriptors, Any => JavaPbAny} -import io.cloudstate.javasupport.{ServiceCall, ServiceCallFactory} +import io.cloudstate.javasupport.{Metadata, ServiceCall, ServiceCallFactory} import io.cloudstate.javasupport.crud.{ CommandContext, CommandHandler, @@ -179,6 +179,7 @@ private class AdaptedCommandContext(val delegate: CommandContext[JavaPbAny], any override def commandName(): String = delegate.commandName() override def commandId(): Long = delegate.commandId() + override def metadata(): Metadata = delegate.metadata() override def entityId(): String = delegate.entityId() override def effect(effect: ServiceCall, synchronous: Boolean): Unit = delegate.effect(effect, synchronous) override def fail(errorMessage: String): RuntimeException = delegate.fail(errorMessage) diff --git a/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/CrudImpl.scala b/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/CrudImpl.scala index 2c5535b14..fb4d6ad0b 100644 --- a/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/CrudImpl.scala +++ b/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/CrudImpl.scala @@ -28,7 +28,7 @@ import com.google.protobuf.any.{Any => ScalaPbAny} import io.cloudstate.javasupport.crud._ import io.cloudstate.javasupport.impl._ import io.cloudstate.javasupport.impl.crud.CrudImpl.{failure, failureMessage, EntityException, ProtocolException} -import io.cloudstate.javasupport.{Context, Service, ServiceCallFactory} +import io.cloudstate.javasupport.{Context, Metadata, Service, ServiceCallFactory} import io.cloudstate.protocol.crud.CrudAction.Action.{Delete, Update} import io.cloudstate.protocol.crud._ import io.cloudstate.protocol.crud.CrudStreamIn.Message.{Command => InCommand, Empty => InEmpty, Init => InInit} @@ -153,10 +153,12 @@ final class CrudImpl(_system: ActorSystem, case ((state, _), InCommand(command)) => val cmd = ScalaPbAny.toJavaProto(command.payload.get) + val metadata = new MetadataImpl(command.metadata.map(_.entries.toVector).getOrElse(Nil)) val context = new CommandContextImpl( thisEntityId, command.name, command.id, + metadata, state, service.anySupport, log @@ -216,6 +218,7 @@ final class CrudImpl(_system: ActorSystem, private final class CommandContextImpl(override val entityId: String, override val commandName: String, override val commandId: Long, + override val metadata: Metadata, val state: Option[ScalaPbAny], val anySupport: AnySupport, val log: LoggingAdapter) diff --git a/java-support/src/test/scala/io/cloudstate/javasupport/impl/crud/AnnotationBasedCrudSupportSpec.scala b/java-support/src/test/scala/io/cloudstate/javasupport/impl/crud/AnnotationBasedCrudSupportSpec.scala index 74fc863c3..87d9c3c39 100644 --- a/java-support/src/test/scala/io/cloudstate/javasupport/impl/crud/AnnotationBasedCrudSupportSpec.scala +++ b/java-support/src/test/scala/io/cloudstate/javasupport/impl/crud/AnnotationBasedCrudSupportSpec.scala @@ -26,11 +26,13 @@ import io.cloudstate.javasupport.crud.{ CommandHandler, CrudContext, CrudEntity, - CrudEntityCreationContext + CrudEntityCreationContext, + CrudEntityHandler } import io.cloudstate.javasupport.impl.{AnySupport, ResolvedServiceMethod, ResolvedType} -import io.cloudstate.javasupport.{Context, EntityId, ServiceCall, ServiceCallFactory, ServiceCallRef} +import io.cloudstate.javasupport.{Context, EntityId, Metadata, ServiceCall, ServiceCallFactory, ServiceCallRef} import org.scalatest.{Matchers, WordSpec} + import scala.compat.java8.OptionConverters._ class AnnotationBasedCrudSupportSpec extends WordSpec with Matchers { @@ -54,6 +56,7 @@ class AnnotationBasedCrudSupportSpec extends WordSpec with Matchers { override def updateState(newState: JavaPbAny): Unit = currentState = Some(newState) override def deleteState(): Unit = currentState = None override def entityId(): String = "foo" + override def metadata(): Metadata = ??? override def fail(errorMessage: String): RuntimeException = ??? override def forward(to: ServiceCall): Unit = ??? override def effect(effect: ServiceCall, synchronous: Boolean): Unit = ??? @@ -78,16 +81,16 @@ class AnnotationBasedCrudSupportSpec extends WordSpec with Matchers { val anySupport = new AnySupport(Array(Shoppingcart.getDescriptor), this.getClass.getClassLoader) val serviceDescriptor = Shoppingcart.getDescriptor.findServiceByName("ShoppingCart") - def method(name: String = "AddItem") = + def method(name: String = "AddItem"): ResolvedServiceMethod[String, Wrapped] = ResolvedServiceMethod(serviceDescriptor.findMethodByName(name), StringResolvedType, WrappedResolvedType) - def create(behavior: AnyRef, methods: ResolvedServiceMethod[_, _]*) = + def create(behavior: AnyRef, methods: ResolvedServiceMethod[_, _]*): CrudEntityHandler = new AnnotationBasedCrudSupport(behavior.getClass, anySupport, methods.map(m => m.descriptor.getName -> m).toMap, Some(_ => behavior)).create(MockContext) - def create(clazz: Class[_]) = + def create(clazz: Class[_]): CrudEntityHandler = new AnnotationBasedCrudSupport(clazz, anySupport, Map.empty, None).create(MockContext) def command(str: String) = diff --git a/protocols/protocol/cloudstate/crud.proto b/protocols/protocol/cloudstate/crud.proto index 02b22d214..e7a28fbe9 100644 --- a/protocols/protocol/cloudstate/crud.proto +++ b/protocols/protocol/cloudstate/crud.proto @@ -24,7 +24,7 @@ import "google/protobuf/any.proto"; import "cloudstate/entity.proto"; option java_package = "io.cloudstate.protocol"; -option go_package = "cloudstate/protocol"; +option go_package = "github.com/cloudstateio/go-support/cloudstate/entity;entity"; // The CRUD Entity service service Crud { diff --git a/proxy/core/src/main/scala/io/cloudstate/proxy/EntityDiscoveryManager.scala b/proxy/core/src/main/scala/io/cloudstate/proxy/EntityDiscoveryManager.scala index 0c71fbbd1..cb8030f35 100644 --- a/proxy/core/src/main/scala/io/cloudstate/proxy/EntityDiscoveryManager.scala +++ b/proxy/core/src/main/scala/io/cloudstate/proxy/EntityDiscoveryManager.scala @@ -17,19 +17,13 @@ package io.cloudstate.proxy import akka.Done -import akka.actor.{Actor, ActorLogging, CoordinatedShutdown, PoisonPill, Props, Status} +import akka.actor.{Actor, ActorLogging, CoordinatedShutdown, Props, Status} import akka.cluster.Cluster import akka.util.Timeout import akka.pattern.pipe import akka.stream.scaladsl.RunnableGraph import akka.http.scaladsl.Http import akka.http.scaladsl.Http.ServerBinding -import akka.cluster.singleton.{ - ClusterSingletonManager, - ClusterSingletonManagerSettings, - ClusterSingletonProxy, - ClusterSingletonProxySettings -} import akka.grpc.GrpcClientSettings import akka.stream.Materializer import com.google.protobuf.DescriptorProtos @@ -40,15 +34,6 @@ import io.cloudstate.protocol.entity._ import io.cloudstate.protocol.crdt.Crdt import io.cloudstate.protocol.crud.Crud import io.cloudstate.protocol.event_sourced.EventSourced -import io.cloudstate.proxy.autoscaler.Autoscaler.ScalerFactory -import io.cloudstate.proxy.autoscaler.{ - Autoscaler, - AutoscalerSettings, - ClusterMembershipFacadeImpl, - KubernetesDeploymentScaler, - NoAutoscaler, - NoScaler -} import io.cloudstate.proxy.action.ActionProtocolSupportFactory import io.cloudstate.proxy.crdt.CrdtSupportFactory import io.cloudstate.proxy.crud.CrudSupportFactory diff --git a/proxy/core/src/main/scala/io/cloudstate/proxy/crud/CrudSupportFactory.scala b/proxy/core/src/main/scala/io/cloudstate/proxy/crud/CrudStoreSupportFactory.scala similarity index 97% rename from proxy/core/src/main/scala/io/cloudstate/proxy/crud/CrudSupportFactory.scala rename to proxy/core/src/main/scala/io/cloudstate/proxy/crud/CrudStoreSupportFactory.scala index b2910aa98..f3d872544 100644 --- a/proxy/core/src/main/scala/io/cloudstate/proxy/crud/CrudSupportFactory.scala +++ b/proxy/core/src/main/scala/io/cloudstate/proxy/crud/CrudStoreSupportFactory.scala @@ -29,7 +29,7 @@ import com.google.protobuf.Descriptors.ServiceDescriptor import io.cloudstate.protocol.crud.CrudClient import io.cloudstate.protocol.entity.{Entity, Metadata} import io.cloudstate.proxy._ -import io.cloudstate.proxy.crud.store.{JdbcRepositoryImpl, JdbcStoreFactory} +import io.cloudstate.proxy.crud.store.{JdbcRepositoryImpl, JdbcStoreSupport} import io.cloudstate.proxy.entity.{EntityCommand, UserFunctionReply} import scala.concurrent.{ExecutionContext, Future} @@ -38,7 +38,7 @@ class CrudSupportFactory(system: ActorSystem, config: EntityDiscoveryManager.Configuration, grpcClientSettings: GrpcClientSettings)(implicit ec: ExecutionContext, mat: Materializer) extends EntityTypeSupportFactory - with JdbcStoreFactory { + with JdbcStoreSupport { private final val log = Logging.getLogger(system, this.getClass) @@ -54,7 +54,7 @@ class CrudSupportFactory(system: ActorSystem, config.passivationTimeout, config.relayOutputBufferSize) - val repository = new JdbcRepositoryImpl(buildCrudStore(config.config)) + val repository = new JdbcRepositoryImpl(createStore(config.config)) log.debug("Starting CrudEntity for {}", entity.persistenceId) val clusterSharding = ClusterSharding(system) diff --git a/proxy/core/src/main/scala/io/cloudstate/proxy/crud/readme.md b/proxy/core/src/main/scala/io/cloudstate/proxy/crud/readme.md deleted file mode 100644 index 87e713dd0..000000000 --- a/proxy/core/src/main/scala/io/cloudstate/proxy/crud/readme.md +++ /dev/null @@ -1,23 +0,0 @@ -### What have been done: -- validating the protocol definition :white_check_mark: -- validating the implementation of the user facing interface :white_check_mark: -- validating the protocol implementation based on event sourcing :white_check_mark: -- write tests for the annotation support and the entity based on event sourcing :white_check_mark: -- provide an sample in a dedicated project for CRUD :white_check_mark: - -### What should be reviewed: -- native CRUD support based on Slick -- In memory CRUD support added -- the protocol implementation based on native CRUD -- remove the snapshot from the GRPC protocol -- remove the sequence number from the GRPC protocol - -### What are the next steps: -- add Postgres native CRUD support -- remove the sequence number from the protocol from the implementation :white_check_mark: -- add generic type for io.cloudstate.javasupport.crud.CommandContext :white_check_mark: -- deal with null value for io.cloudstate.javasupport.crud.CommandContext#updateEntity :white_check_mark: -- deal with the order of call for io.cloudstate.javasupport.crud.CommandContext#deleteEntity and io.cloudstate.javasupport.crud.CommandContext#updateEntity -- deal with exceptions -- write tests -- extend the TCK \ No newline at end of file diff --git a/proxy/core/src/main/scala/io/cloudstate/proxy/crud/store/JdbcStoreFactory.scala b/proxy/core/src/main/scala/io/cloudstate/proxy/crud/store/JdbcStoreSupport.scala similarity index 79% rename from proxy/core/src/main/scala/io/cloudstate/proxy/crud/store/JdbcStoreFactory.scala rename to proxy/core/src/main/scala/io/cloudstate/proxy/crud/store/JdbcStoreSupport.scala index 85a00dd1b..d3f50e556 100644 --- a/proxy/core/src/main/scala/io/cloudstate/proxy/crud/store/JdbcStoreFactory.scala +++ b/proxy/core/src/main/scala/io/cloudstate/proxy/crud/store/JdbcStoreSupport.scala @@ -19,26 +19,26 @@ package io.cloudstate.proxy.crud.store import akka.util.ByteString import com.typesafe.config.Config import io.cloudstate.proxy.crud.store.JdbcStore.Key -import io.cloudstate.proxy.crud.store.JdbcStoreFactory.{IN_MEMORY, JDBC} +import io.cloudstate.proxy.crud.store.JdbcStoreSupport.{IN_MEMORY, JDBC} import scala.concurrent.ExecutionContext; -object JdbcStoreFactory { +object JdbcStoreSupport { final val IN_MEMORY = "in-memory" final val JDBC = "jdbc" } -trait JdbcStoreFactory { +trait JdbcStoreSupport { - def buildCrudStore(config: Config)(implicit ec: ExecutionContext): JdbcStore[Key, ByteString] = + def createStore(config: Config)(implicit ec: ExecutionContext): JdbcStore[Key, ByteString] = config.getString("crud.store-type") match { case IN_MEMORY => new JdbcInMemoryStore - case JDBC => buildJdbcCrudStore(config) + case JDBC => createJdbcStore(config) case other => throw new IllegalArgumentException(s"CRUD store-type must be one of: ${IN_MEMORY} or ${JDBC} but is '$other'") } - private def buildJdbcCrudStore(config: Config)(implicit ec: ExecutionContext): JdbcStore[Key, ByteString] = { + private def createJdbcStore(config: Config)(implicit ec: ExecutionContext): JdbcStore[Key, ByteString] = { val slickDatabase = JdbcSlickDatabase(config) val tableConfiguration = new JdbcCrudStateTableConfiguration( config.getConfig("crud.jdbc-state-store") From eac2e7f2ddd0fa96f5ada94de4f0db39db4516d7 Mon Sep 17 00:00:00 2001 From: Guy Youansi Date: Sat, 17 Oct 2020 14:46:42 +0200 Subject: [PATCH 68/93] add table creation readiness in native image --- .../cloudstate-proxy-jdbc/reflect-config.json.conf | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/proxy/postgres/src/graal/META-INF/native-image/io.cloudstate/cloudstate-proxy-jdbc/reflect-config.json.conf b/proxy/postgres/src/graal/META-INF/native-image/io.cloudstate/cloudstate-proxy-jdbc/reflect-config.json.conf index ef7f5d8d1..c73cc89c5 100644 --- a/proxy/postgres/src/graal/META-INF/native-image/io.cloudstate/cloudstate-proxy-jdbc/reflect-config.json.conf +++ b/proxy/postgres/src/graal/META-INF/native-image/io.cloudstate/cloudstate-proxy-jdbc/reflect-config.json.conf @@ -3,4 +3,8 @@ name: "io.cloudstate.proxy.jdbc.SlickEnsureTablesExistReadyCheck" methods: [{name: "", parameterTypes: ["akka.actor.ActorSystem"]}] } +{ + name: "io.cloudstate.proxy.jdbc.SlickEnsureCrudTablesExistReadyCheck" + methods: [{name: "", parameterTypes: ["akka.actor.ActorSystem"]}] +} ] From 35e00054f0d0a2c1b033d4c2f7051ed76fa4410d Mon Sep 17 00:00:00 2001 From: Guy Youansi Date: Tue, 20 Oct 2020 21:33:26 +0200 Subject: [PATCH 69/93] rename CRUD to Value Entity --- build.sbt | 12 +- .../io/cloudstate/javasupport/CloudState.java | 34 +-- .../javasupport/crud/package-info.java | 8 - .../{crud => valueentity}/CommandContext.java | 6 +- .../{crud => valueentity}/CommandHandler.java | 4 +- .../ValueEntity.java} | 6 +- .../ValueEntityContext.java} | 6 +- .../ValueEntityCreationContext.java} | 8 +- .../ValueEntityFactory.java} | 9 +- .../ValueEntityHandler.java} | 7 +- .../javasupport/valueentity/package-info.java | 9 + .../javasupport/CloudStateRunner.scala | 12 +- .../AnnotationBasedValueEntitySupport.scala} | 52 ++-- .../ValueEntityImpl.scala} | 95 ++++---- ...notationBasedValueEntitySupportSpec.scala} | 48 ++-- .../TestValueEntity.scala} | 14 +- .../ValueEntityImplSpec.scala} | 72 +++--- .../javasupport/tck/JavaSupportTck.java | 26 +- .../ValueEntityTckModelEntity.java} | 24 +- .../ValueEntityTwoEntity.java} | 16 +- .../shoppingcart/persistence/domain.proto | 6 +- .../shoppingcart/shoppingcart.proto | 14 +- .../{crud.proto => value_entity.proto} | 54 ++--- .../model/{crud.proto => valueentity.proto} | 21 +- .../proxy/EntityDiscoveryManager.scala | 6 +- .../proxy/crud/store/JdbcCrudStateTable.scala | 51 ---- .../DynamicLeastShardAllocationStrategy.scala | 2 +- .../ValueEntity.scala} | 165 ++++++------- .../ValueEntitySupportFactory.scala} | 32 +-- .../store/JdbcConfig.scala | 8 +- .../store/JdbcInMemoryStore.scala | 4 +- .../store/JdbcRepository.scala | 4 +- .../store/JdbcStore.scala | 10 +- .../store/JdbcStoreSupport.scala | 10 +- .../store/JdbcValueEntityQueries.scala} | 19 +- .../store/JdbcValueEntityTable.scala | 51 ++++ .../store/package-info.java | 2 +- .../scala/io/cloudstate/proxy/TestProxy.scala | 4 +- .../DatabaseExceptionHandlingSpec.scala | 43 ++-- .../ExceptionHandlingSpec.scala | 52 ++-- .../jdbc/src/main/resources/jdbc-common.conf | 2 +- .../proxy/jdbc/CloudStateJdbcProxyMain.scala | 2 +- ...ureValueEntityTablesExistReadyCheck.scala} | 22 +- .../reflect-config.json.conf | 2 +- .../valueentity}/shoppingcart/Main.java | 8 +- .../src/main/resources/application.conf | 0 .../main/resources/simplelogger.properties | 0 .../io/cloudstate/tck/CloudStateTCK.scala | 229 +++++++++--------- ... => ValueEntityShoppingCartVerifier.scala} | 58 ++--- .../cloudstate/testkit/InterceptService.scala | 10 +- .../io/cloudstate/testkit/TestProtocol.scala | 6 +- .../io/cloudstate/testkit/TestService.scala | 6 +- .../discovery/InterceptEntityDiscovery.scala | 4 +- .../InterceptValueEntityService.scala} | 39 +-- .../TestValueEntityProtocol.scala} | 26 +- .../TestValueEntityService.scala} | 41 ++-- .../TestValueEntityServiceClient.scala} | 32 +-- .../ValueEntityMessages.scala} | 47 ++-- 58 files changed, 791 insertions(+), 769 deletions(-) delete mode 100644 java-support/src/main/java/io/cloudstate/javasupport/crud/package-info.java rename java-support/src/main/java/io/cloudstate/javasupport/{crud => valueentity}/CommandContext.java (91%) rename java-support/src/main/java/io/cloudstate/javasupport/{crud => valueentity}/CommandHandler.java (94%) rename java-support/src/main/java/io/cloudstate/javasupport/{crud/CrudEntity.java => valueentity/ValueEntity.java} (91%) rename java-support/src/main/java/io/cloudstate/javasupport/{crud/CrudContext.java => valueentity/ValueEntityContext.java} (80%) rename java-support/src/main/java/io/cloudstate/javasupport/{crud/CrudEntityCreationContext.java => valueentity/ValueEntityCreationContext.java} (77%) rename java-support/src/main/java/io/cloudstate/javasupport/{crud/CrudEntityFactory.java => valueentity/ValueEntityFactory.java} (79%) rename java-support/src/main/java/io/cloudstate/javasupport/{crud/CrudEntityHandler.java => valueentity/ValueEntityHandler.java} (85%) create mode 100644 java-support/src/main/java/io/cloudstate/javasupport/valueentity/package-info.java rename java-support/src/main/scala/io/cloudstate/javasupport/impl/{crud/AnnotationBasedCrudSupport.scala => valueentity/AnnotationBasedValueEntitySupport.scala} (81%) rename java-support/src/main/scala/io/cloudstate/javasupport/impl/{crud/CrudImpl.scala => valueentity/ValueEntityImpl.scala} (71%) rename java-support/src/test/scala/io/cloudstate/javasupport/impl/{crud/AnnotationBasedCrudSupportSpec.scala => valueentity/AnnotationBasedValueEntitySupportSpec.scala} (89%) rename java-support/src/test/scala/io/cloudstate/javasupport/impl/{crud/TestCrud.scala => valueentity/TestValueEntity.scala} (76%) rename java-support/src/test/scala/io/cloudstate/javasupport/impl/{crud/CrudImplSpec.scala => valueentity/ValueEntityImplSpec.scala} (83%) rename java-support/tck/src/main/java/io/cloudstate/javasupport/tck/model/{crud/CrudTckModelEntity.java => valuentity/ValueEntityTckModelEntity.java} (73%) rename java-support/tck/src/main/java/io/cloudstate/javasupport/tck/model/{crud/CrudTwoEntity.java => valuentity/ValueEntityTwoEntity.java} (64%) rename protocols/example/{crud => valueentity}/shoppingcart/persistence/domain.proto (76%) rename protocols/example/{crud => valueentity}/shoppingcart/shoppingcart.proto (82%) rename protocols/protocol/cloudstate/{crud.proto => value_entity.proto} (61%) rename protocols/tck/cloudstate/tck/model/{crud.proto => valueentity.proto} (82%) delete mode 100644 proxy/core/src/main/scala/io/cloudstate/proxy/crud/store/JdbcCrudStateTable.scala rename proxy/core/src/main/scala/io/cloudstate/proxy/{crud => valueentity}/DynamicLeastShardAllocationStrategy.scala (98%) rename proxy/core/src/main/scala/io/cloudstate/proxy/{crud/CrudEntity.scala => valueentity/ValueEntity.scala} (65%) rename proxy/core/src/main/scala/io/cloudstate/proxy/{crud/CrudStoreSupportFactory.scala => valueentity/ValueEntitySupportFactory.scala} (78%) rename proxy/core/src/main/scala/io/cloudstate/proxy/{crud => valueentity}/store/JdbcConfig.scala (88%) rename proxy/core/src/main/scala/io/cloudstate/proxy/{crud => valueentity}/store/JdbcInMemoryStore.scala (91%) rename proxy/core/src/main/scala/io/cloudstate/proxy/{crud => valueentity}/store/JdbcRepository.scala (96%) rename proxy/core/src/main/scala/io/cloudstate/proxy/{crud => valueentity}/store/JdbcStore.scala (84%) rename proxy/core/src/main/scala/io/cloudstate/proxy/{crud => valueentity}/store/JdbcStoreSupport.scala (81%) rename proxy/core/src/main/scala/io/cloudstate/proxy/{crud/store/JdbcCrudStateQueries.scala => valueentity/store/JdbcValueEntityQueries.scala} (59%) create mode 100644 proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/JdbcValueEntityTable.scala rename proxy/core/src/main/scala/io/cloudstate/proxy/{crud => valueentity}/store/package-info.java (72%) rename proxy/core/src/test/scala/io/cloudstate/proxy/{crud => valueentity}/DatabaseExceptionHandlingSpec.scala (80%) rename proxy/core/src/test/scala/io/cloudstate/proxy/{crud => valueentity}/ExceptionHandlingSpec.scala (76%) rename proxy/jdbc/src/main/scala/io/cloudstate/proxy/jdbc/{SlickEnsureCrudTablesExistReadyCheck.scala => SlickEnsureValueEntityTablesExistReadyCheck.scala} (89%) rename samples/{java-crud-shopping-cart/src/main/java/io/cloudstate/samples/crud => java-valueentity-shopping-cart/src/main/java/io/cloudstate/samples/valueentity}/shoppingcart/Main.java (80%) rename samples/{java-crud-shopping-cart => java-valueentity-shopping-cart}/src/main/resources/application.conf (100%) rename samples/{java-crud-shopping-cart => java-valueentity-shopping-cart}/src/main/resources/simplelogger.properties (100%) rename tck/src/main/scala/io/cloudstate/tck/{CrudShoppingCartVerifier.scala => ValueEntityShoppingCartVerifier.scala} (66%) rename testkit/src/main/scala/io/cloudstate/testkit/{crud/InterceptCrudService.scala => valuentity/InterceptValueEntityService.scala} (66%) rename testkit/src/main/scala/io/cloudstate/testkit/{crud/TestCrudProtocol.scala => valuentity/TestValueEntityProtocol.scala} (57%) rename testkit/src/main/scala/io/cloudstate/testkit/{crud/TestCrudService.scala => valuentity/TestValueEntityService.scala} (59%) rename testkit/src/main/scala/io/cloudstate/testkit/{crud/TestCrudServiceClient.scala => valuentity/TestValueEntityServiceClient.scala} (58%) rename testkit/src/main/scala/io/cloudstate/testkit/{crud/CrudMessages.scala => valuentity/ValueEntityMessages.scala} (80%) diff --git a/build.sbt b/build.sbt index 14e8a95b8..3134ef944 100644 --- a/build.sbt +++ b/build.sbt @@ -118,7 +118,7 @@ lazy val root = (project in file(".")) `java-support-docs`, `java-support-tck`, `java-shopping-cart`, - `java-crud-shopping-cart`, + `java-valueentity-shopping-cart`, `java-pingpong`, `akka-client`, operator, @@ -638,7 +638,7 @@ lazy val `java-support-docs` = (project in file("java-support/docs")) ) lazy val `java-support-tck` = (project in file("java-support/tck")) - .dependsOn(`java-support`, `java-shopping-cart`, `java-crud-shopping-cart`) + .dependsOn(`java-support`, `java-shopping-cart`, `java-valueentity-shopping-cart`) .enablePlugins(AkkaGrpcPlugin, AssemblyPlugin, JavaAppPackaging, DockerPlugin, AutomateHeaderPlugin, NoPublish) .settings( name := "cloudstate-java-tck", @@ -672,13 +672,13 @@ lazy val `java-shopping-cart` = (project in file("samples/java-shopping-cart")) assemblySettings("java-shopping-cart.jar") ) -lazy val `java-crud-shopping-cart` = (project in file("samples/java-crud-shopping-cart")) +lazy val `java-valueentity-shopping-cart` = (project in file("samples/java-valueentity-shopping-cart")) .dependsOn(`java-support`) .enablePlugins(AkkaGrpcPlugin, AssemblyPlugin, JavaAppPackaging, DockerPlugin, AutomateHeaderPlugin, NoPublish) .settings( - name := "java-crud-shopping-cart", + name := "java-valueentity-shopping-cart", dockerSettings, - mainClass in Compile := Some("io.cloudstate.samples.crud.shoppingcart.Main"), + mainClass in Compile := Some("io.cloudstate.samples.valueentity.shoppingcart.Main"), PB.generate in Compile := (PB.generate in Compile).dependsOn(PB.generate in (`java-support`, Compile)).value, akkaGrpcGeneratedLanguages := Seq(AkkaGrpc.Java), PB.protoSources in Compile ++= { @@ -689,7 +689,7 @@ lazy val `java-crud-shopping-cart` = (project in file("samples/java-crud-shoppin PB.gens.java -> (sourceManaged in Compile).value ), javacOptions in Compile ++= Seq("-encoding", "UTF-8", "-source", "11", "-target", "11"), - assemblySettings("java-crud-shopping-cart.jar") + assemblySettings("java-valueentity-shopping-cart.jar") ) lazy val `java-pingpong` = (project in file("samples/java-pingpong")) diff --git a/java-support/src/main/java/io/cloudstate/javasupport/CloudState.java b/java-support/src/main/java/io/cloudstate/javasupport/CloudState.java index 659e26c6f..d70f096b7 100644 --- a/java-support/src/main/java/io/cloudstate/javasupport/CloudState.java +++ b/java-support/src/main/java/io/cloudstate/javasupport/CloudState.java @@ -20,12 +20,12 @@ import akka.stream.Materializer; import com.typesafe.config.Config; import com.google.protobuf.Descriptors; -import io.cloudstate.javasupport.crud.CrudEntity; +import io.cloudstate.javasupport.valueentity.ValueEntity; import io.cloudstate.javasupport.action.Action; import io.cloudstate.javasupport.action.ActionHandler; import io.cloudstate.javasupport.crdt.CrdtEntity; import io.cloudstate.javasupport.crdt.CrdtEntityFactory; -import io.cloudstate.javasupport.crud.CrudEntityFactory; +import io.cloudstate.javasupport.valueentity.ValueEntityFactory; import io.cloudstate.javasupport.eventsourced.EventSourcedEntity; import io.cloudstate.javasupport.eventsourced.EventSourcedEntityFactory; import io.cloudstate.javasupport.impl.AnySupport; @@ -33,8 +33,8 @@ import io.cloudstate.javasupport.impl.action.ActionService; import io.cloudstate.javasupport.impl.crdt.AnnotationBasedCrdtSupport; import io.cloudstate.javasupport.impl.crdt.CrdtStatefulService; -import io.cloudstate.javasupport.impl.crud.AnnotationBasedCrudSupport; -import io.cloudstate.javasupport.impl.crud.CrudStatefulService; +import io.cloudstate.javasupport.impl.valueentity.AnnotationBasedValueEntitySupport; +import io.cloudstate.javasupport.impl.valueentity.ValueEntityStatefulService; import io.cloudstate.javasupport.impl.eventsourced.AnnotationBasedEventSourcedSupport; import io.cloudstate.javasupport.impl.eventsourced.EventSourcedStatefulService; @@ -305,9 +305,9 @@ public CloudState registerAction( } /** - * Register an annotated CRUD entity. + * Register a annotated value entity. * - *

The entity class must be annotated with {@link io.cloudstate.javasupport.crud.CrudEntity}. + *

The entity class must be annotated with {@link ValueEntity}. * * @param entityClass The entity class. * @param descriptor The descriptor for the service that this entity implements. @@ -315,15 +315,15 @@ public CloudState registerAction( * types when needed. * @return This stateful service builder. */ - public CloudState registerCrudEntity( + public CloudState registerValueEntity( Class entityClass, Descriptors.ServiceDescriptor descriptor, Descriptors.FileDescriptor... additionalDescriptors) { - CrudEntity entity = entityClass.getAnnotation(CrudEntity.class); + ValueEntity entity = entityClass.getAnnotation(ValueEntity.class); if (entity == null) { throw new IllegalArgumentException( - entityClass + " does not declare an " + CrudEntity.class + " annotation!"); + entityClass + " does not declare an " + ValueEntity.class + " annotation!"); } final String persistenceId; @@ -334,9 +334,9 @@ public CloudState registerCrudEntity( } final AnySupport anySupport = newAnySupport(additionalDescriptors); - CrudStatefulService service = - new CrudStatefulService( - new AnnotationBasedCrudSupport(entityClass, anySupport, descriptor), + ValueEntityStatefulService service = + new ValueEntityStatefulService( + new AnnotationBasedValueEntitySupport(entityClass, anySupport, descriptor), descriptor, anySupport, persistenceId); @@ -347,27 +347,27 @@ public CloudState registerCrudEntity( } /** - * Register a CRUD entity factory. + * Register a value entity factory. * *

This is a low level API intended for custom (eg, non reflection based) mechanisms for * implementing the entity. * - * @param factory The CRUD factory. + * @param factory The value entity factory. * @param descriptor The descriptor for the service that this entity implements. * @param persistenceId The persistence id for this entity. * @param additionalDescriptors Any additional descriptors that should be used to look up protobuf * types when needed. * @return This stateful service builder. */ - public CloudState registerCrudEntity( - CrudEntityFactory factory, + public CloudState registerValueEntity( + ValueEntityFactory factory, Descriptors.ServiceDescriptor descriptor, String persistenceId, Descriptors.FileDescriptor... additionalDescriptors) { services.put( descriptor.getFullName(), system -> - new CrudStatefulService( + new ValueEntityStatefulService( factory, descriptor, newAnySupport(additionalDescriptors), persistenceId)); return this; diff --git a/java-support/src/main/java/io/cloudstate/javasupport/crud/package-info.java b/java-support/src/main/java/io/cloudstate/javasupport/crud/package-info.java deleted file mode 100644 index 19393b9a8..000000000 --- a/java-support/src/main/java/io/cloudstate/javasupport/crud/package-info.java +++ /dev/null @@ -1,8 +0,0 @@ -/** - * CRUD support. - * - *

CRUD entities can be annotated with the {@link - * io.cloudstate.javasupport.crud.CrudEntity @CrudEntity} annotation, and supply command handlers - * using the {@link io.cloudstate.javasupport.crud.CommandHandler @CommandHandler} annotation. - */ -package io.cloudstate.javasupport.crud; diff --git a/java-support/src/main/java/io/cloudstate/javasupport/crud/CommandContext.java b/java-support/src/main/java/io/cloudstate/javasupport/valueentity/CommandContext.java similarity index 91% rename from java-support/src/main/java/io/cloudstate/javasupport/crud/CommandContext.java rename to java-support/src/main/java/io/cloudstate/javasupport/valueentity/CommandContext.java index 56995781e..1cf73c364 100644 --- a/java-support/src/main/java/io/cloudstate/javasupport/crud/CommandContext.java +++ b/java-support/src/main/java/io/cloudstate/javasupport/valueentity/CommandContext.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.cloudstate.javasupport.crud; +package io.cloudstate.javasupport.valueentity; import io.cloudstate.javasupport.ClientActionContext; import io.cloudstate.javasupport.EffectContext; @@ -23,14 +23,14 @@ import java.util.Optional; /** - * A CRUD command context. + * A value entity command context. * *

Methods annotated with {@link CommandHandler} may take this is a parameter. It allows updating * or deleting the entity state in response to a command, along with forwarding the result to other * entities, and performing side effects on other entities. */ public interface CommandContext - extends CrudContext, ClientActionContext, EffectContext, MetadataContext { + extends ValueEntityContext, ClientActionContext, EffectContext, MetadataContext { /** * The name of the command being executed. diff --git a/java-support/src/main/java/io/cloudstate/javasupport/crud/CommandHandler.java b/java-support/src/main/java/io/cloudstate/javasupport/valueentity/CommandHandler.java similarity index 94% rename from java-support/src/main/java/io/cloudstate/javasupport/crud/CommandHandler.java rename to java-support/src/main/java/io/cloudstate/javasupport/valueentity/CommandHandler.java index 3bf20122b..b6ed77f6a 100644 --- a/java-support/src/main/java/io/cloudstate/javasupport/crud/CommandHandler.java +++ b/java-support/src/main/java/io/cloudstate/javasupport/valueentity/CommandHandler.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.cloudstate.javasupport.crud; +package io.cloudstate.javasupport.valueentity; import io.cloudstate.javasupport.impl.CloudStateAnnotation; @@ -24,7 +24,7 @@ import java.lang.annotation.Target; /** - * Marks a method as a command handler. + * Marks a method on a value entity as a command handler. * *

This method will be invoked whenever the service call with name that matches this command * handlers name is invoked. diff --git a/java-support/src/main/java/io/cloudstate/javasupport/crud/CrudEntity.java b/java-support/src/main/java/io/cloudstate/javasupport/valueentity/ValueEntity.java similarity index 91% rename from java-support/src/main/java/io/cloudstate/javasupport/crud/CrudEntity.java rename to java-support/src/main/java/io/cloudstate/javasupport/valueentity/ValueEntity.java index 7a36610ea..9f0b10c19 100644 --- a/java-support/src/main/java/io/cloudstate/javasupport/crud/CrudEntity.java +++ b/java-support/src/main/java/io/cloudstate/javasupport/valueentity/ValueEntity.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.cloudstate.javasupport.crud; +package io.cloudstate.javasupport.valueentity; import io.cloudstate.javasupport.impl.CloudStateAnnotation; @@ -23,11 +23,11 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -/** A CRUD entity. */ +/** A value entity. */ @CloudStateAnnotation @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) -public @interface CrudEntity { +public @interface ValueEntity { /** * The name of the persistence id. * diff --git a/java-support/src/main/java/io/cloudstate/javasupport/crud/CrudContext.java b/java-support/src/main/java/io/cloudstate/javasupport/valueentity/ValueEntityContext.java similarity index 80% rename from java-support/src/main/java/io/cloudstate/javasupport/crud/CrudContext.java rename to java-support/src/main/java/io/cloudstate/javasupport/valueentity/ValueEntityContext.java index b3551c7a6..67c9fbb13 100644 --- a/java-support/src/main/java/io/cloudstate/javasupport/crud/CrudContext.java +++ b/java-support/src/main/java/io/cloudstate/javasupport/valueentity/ValueEntityContext.java @@ -14,9 +14,9 @@ * limitations under the License. */ -package io.cloudstate.javasupport.crud; +package io.cloudstate.javasupport.valueentity; import io.cloudstate.javasupport.EntityContext; -/** Root context for all CRUD contexts. */ -public interface CrudContext extends EntityContext {} +/** Root context for all value entity contexts. */ +public interface ValueEntityContext extends EntityContext {} diff --git a/java-support/src/main/java/io/cloudstate/javasupport/crud/CrudEntityCreationContext.java b/java-support/src/main/java/io/cloudstate/javasupport/valueentity/ValueEntityCreationContext.java similarity index 77% rename from java-support/src/main/java/io/cloudstate/javasupport/crud/CrudEntityCreationContext.java rename to java-support/src/main/java/io/cloudstate/javasupport/valueentity/ValueEntityCreationContext.java index 20afd0cc9..2afff732b 100644 --- a/java-support/src/main/java/io/cloudstate/javasupport/crud/CrudEntityCreationContext.java +++ b/java-support/src/main/java/io/cloudstate/javasupport/valueentity/ValueEntityCreationContext.java @@ -14,11 +14,11 @@ * limitations under the License. */ -package io.cloudstate.javasupport.crud; +package io.cloudstate.javasupport.valueentity; /** - * Creation context for {@link CrudEntity} annotated entities. + * Creation context for {@link ValueEntity} annotated entities. * - *

This may be accepted as an argument to the constructor of a CRUD entity. + *

This may be accepted as an argument to the constructor of a value entity. */ -public interface CrudEntityCreationContext extends CrudContext {} +public interface ValueEntityCreationContext extends ValueEntityContext {} diff --git a/java-support/src/main/java/io/cloudstate/javasupport/crud/CrudEntityFactory.java b/java-support/src/main/java/io/cloudstate/javasupport/valueentity/ValueEntityFactory.java similarity index 79% rename from java-support/src/main/java/io/cloudstate/javasupport/crud/CrudEntityFactory.java rename to java-support/src/main/java/io/cloudstate/javasupport/valueentity/ValueEntityFactory.java index 9ac34b20a..6fc600425 100644 --- a/java-support/src/main/java/io/cloudstate/javasupport/crud/CrudEntityFactory.java +++ b/java-support/src/main/java/io/cloudstate/javasupport/valueentity/ValueEntityFactory.java @@ -14,23 +14,22 @@ * limitations under the License. */ -package io.cloudstate.javasupport.crud; +package io.cloudstate.javasupport.valueentity; import io.cloudstate.javasupport.eventsourced.CommandHandler; -import io.cloudstate.javasupport.eventsourced.EventHandler; /** - * Low level interface for handling commands on a CRUD entity. + * Low level interface for handling commands on a value entity. * *

Generally, this should not be needed, instead, a class annotated with the {@link * CommandHandler} and similar annotations should be used. */ -public interface CrudEntityFactory { +public interface ValueEntityFactory { /** * Create an entity handler for the given context. * * @param context The context. * @return The handler for the given context. */ - CrudEntityHandler create(CrudContext context); + ValueEntityHandler create(ValueEntityContext context); } diff --git a/java-support/src/main/java/io/cloudstate/javasupport/crud/CrudEntityHandler.java b/java-support/src/main/java/io/cloudstate/javasupport/valueentity/ValueEntityHandler.java similarity index 85% rename from java-support/src/main/java/io/cloudstate/javasupport/crud/CrudEntityHandler.java rename to java-support/src/main/java/io/cloudstate/javasupport/valueentity/ValueEntityHandler.java index 49cae1c2b..57e0d6db0 100644 --- a/java-support/src/main/java/io/cloudstate/javasupport/crud/CrudEntityHandler.java +++ b/java-support/src/main/java/io/cloudstate/javasupport/valueentity/ValueEntityHandler.java @@ -14,20 +14,19 @@ * limitations under the License. */ -package io.cloudstate.javasupport.crud; +package io.cloudstate.javasupport.valueentity; import com.google.protobuf.Any; import java.util.Optional; /** - * Low level interface for handling events (which represents the persistent state) and commands on - * an CRUD entity. + * Low level interface for handling commands on a value entity. * *

Generally, this should not be needed, instead, a class annotated with the {@link * CommandHandler} and similar annotations should be used. */ -public interface CrudEntityHandler { +public interface ValueEntityHandler { /** * Handle the given command. diff --git a/java-support/src/main/java/io/cloudstate/javasupport/valueentity/package-info.java b/java-support/src/main/java/io/cloudstate/javasupport/valueentity/package-info.java new file mode 100644 index 000000000..13248704a --- /dev/null +++ b/java-support/src/main/java/io/cloudstate/javasupport/valueentity/package-info.java @@ -0,0 +1,9 @@ +/** + * Value entity support. + * + *

CRUD entities can be annotated with the {@link + * io.cloudstate.javasupport.valueentity.ValueEntity @ValueEntity} annotation, and supply command + * handlers using the {@link io.cloudstate.javasupport.valueentity.CommandHandler @CommandHandler} + * annotation. + */ +package io.cloudstate.javasupport.valueentity; diff --git a/java-support/src/main/scala/io/cloudstate/javasupport/CloudStateRunner.scala b/java-support/src/main/scala/io/cloudstate/javasupport/CloudStateRunner.scala index 269b5b0d8..6573e0c78 100644 --- a/java-support/src/main/scala/io/cloudstate/javasupport/CloudStateRunner.scala +++ b/java-support/src/main/scala/io/cloudstate/javasupport/CloudStateRunner.scala @@ -29,10 +29,10 @@ import io.cloudstate.javasupport.impl.action.{ActionProtocolImpl, ActionService} import io.cloudstate.javasupport.impl.eventsourced.{EventSourcedImpl, EventSourcedStatefulService} import io.cloudstate.javasupport.impl.{EntityDiscoveryImpl, ResolvedServiceCallFactory, ResolvedServiceMethod} import io.cloudstate.javasupport.impl.crdt.{CrdtImpl, CrdtStatefulService} -import io.cloudstate.javasupport.impl.crud.{CrudImpl, CrudStatefulService} +import io.cloudstate.javasupport.impl.valueentity.{ValueEntityImpl, ValueEntityStatefulService} import io.cloudstate.protocol.action.ActionProtocolHandler import io.cloudstate.protocol.crdt.CrdtHandler -import io.cloudstate.protocol.crud.CrudHandler +import io.cloudstate.protocol.value_entity.ValueEntityProtocolHandler import io.cloudstate.protocol.entity.EntityDiscoveryHandler import io.cloudstate.protocol.event_sourced.EventSourcedHandler @@ -116,10 +116,10 @@ final class CloudStateRunner private[this] ( val actionImpl = new ActionProtocolImpl(system, actionServices, rootContext) route orElse ActionProtocolHandler.partial(actionImpl) - case (route, (serviceClass, crudServices: Map[String, CrudStatefulService] @unchecked)) - if serviceClass == classOf[CrudStatefulService] => - val crudImpl = new CrudImpl(system, crudServices, rootContext, configuration) - route orElse CrudHandler.partial(crudImpl) + case (route, (serviceClass, valueEntityServices: Map[String, ValueEntityStatefulService] @unchecked)) + if serviceClass == classOf[ValueEntityStatefulService] => + val valueEntityImpl = new ValueEntityImpl(system, valueEntityServices, rootContext, configuration) + route orElse ValueEntityProtocolHandler.partial(valueEntityImpl) case (_, (serviceClass, _)) => sys.error(s"Unknown StatefulService: $serviceClass") diff --git a/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/AnnotationBasedCrudSupport.scala b/java-support/src/main/scala/io/cloudstate/javasupport/impl/valueentity/AnnotationBasedValueEntitySupport.scala similarity index 81% rename from java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/AnnotationBasedCrudSupport.scala rename to java-support/src/main/scala/io/cloudstate/javasupport/impl/valueentity/AnnotationBasedValueEntitySupport.scala index e2abf8fd6..43a651c59 100644 --- a/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/AnnotationBasedCrudSupport.scala +++ b/java-support/src/main/scala/io/cloudstate/javasupport/impl/valueentity/AnnotationBasedValueEntitySupport.scala @@ -14,43 +14,43 @@ * limitations under the License. */ -package io.cloudstate.javasupport.impl.crud +package io.cloudstate.javasupport.impl.valueentity import java.lang.reflect.{Constructor, InvocationTargetException, Method} import java.util.Optional import com.google.protobuf.{Descriptors, Any => JavaPbAny} import io.cloudstate.javasupport.{Metadata, ServiceCall, ServiceCallFactory} -import io.cloudstate.javasupport.crud.{ +import io.cloudstate.javasupport.valueentity.{ CommandContext, CommandHandler, - CrudContext, - CrudEntity, - CrudEntityCreationContext, - CrudEntityFactory, - CrudEntityHandler + ValueEntity, + ValueEntityContext, + ValueEntityCreationContext, + ValueEntityFactory, + ValueEntityHandler } import io.cloudstate.javasupport.impl.ReflectionHelper.{InvocationContext, MainArgumentParameterHandler} -import io.cloudstate.javasupport.impl.crud.CrudImpl.EntityException +import io.cloudstate.javasupport.impl.valueentity.ValueEntityImpl.EntityException import io.cloudstate.javasupport.impl.{AnySupport, ReflectionHelper, ResolvedEntityFactory, ResolvedServiceMethod} /** - * Annotation based implementation of the [[CrudEntityFactory]]. + * Annotation based implementation of the [[ValueEntityFactory]]. */ -private[impl] class AnnotationBasedCrudSupport( +private[impl] class AnnotationBasedValueEntitySupport( entityClass: Class[_], anySupport: AnySupport, override val resolvedMethods: Map[String, ResolvedServiceMethod[_, _]], - factory: Option[CrudEntityCreationContext => AnyRef] = None -) extends CrudEntityFactory + factory: Option[ValueEntityCreationContext => AnyRef] = None +) extends ValueEntityFactory with ResolvedEntityFactory { def this(entityClass: Class[_], anySupport: AnySupport, serviceDescriptor: Descriptors.ServiceDescriptor) = this(entityClass, anySupport, anySupport.resolveServiceDescriptor(serviceDescriptor)) - private val behavior = CrudBehaviorReflection(entityClass, resolvedMethods) + private val behavior = ValueEntityBehaviorReflection(entityClass, resolvedMethods) - private val constructor: CrudEntityCreationContext => AnyRef = factory.getOrElse { + private val constructor: ValueEntityCreationContext => AnyRef = factory.getOrElse { entityClass.getConstructors match { case Array(single) => new EntityConstructorInvoker(ReflectionHelper.ensureAccessible(single)) @@ -59,12 +59,12 @@ private[impl] class AnnotationBasedCrudSupport( } } - override def create(context: CrudContext): CrudEntityHandler = + override def create(context: ValueEntityContext): ValueEntityHandler = new EntityHandler(context) - private class EntityHandler(context: CrudContext) extends CrudEntityHandler { + private class EntityHandler(context: ValueEntityContext) extends ValueEntityHandler { private val entity = { - constructor(new DelegatingCrudContext(context) with CrudEntityCreationContext { + constructor(new DelegatingValueEntityContext(context) with ValueEntityCreationContext { override def entityId(): String = context.entityId() }) } @@ -93,19 +93,19 @@ private[impl] class AnnotationBasedCrudSupport( private def behaviorsString = entity.getClass.toString } - private abstract class DelegatingCrudContext(delegate: CrudContext) extends CrudContext { + private abstract class DelegatingValueEntityContext(delegate: ValueEntityContext) extends ValueEntityContext { override def entityId(): String = delegate.entityId() override def serviceCallFactory(): ServiceCallFactory = delegate.serviceCallFactory() } } -private class CrudBehaviorReflection( +private class ValueEntityBehaviorReflection( val commandHandlers: Map[String, ReflectionHelper.CommandHandlerInvoker[CommandContext[AnyRef]]] ) {} -private object CrudBehaviorReflection { +private object ValueEntityBehaviorReflection { def apply(behaviorClass: Class[_], - serviceMethods: Map[String, ResolvedServiceMethod[_, _]]): CrudBehaviorReflection = { + serviceMethods: Map[String, ResolvedServiceMethod[_, _]]): ValueEntityBehaviorReflection = { val allMethods = ReflectionHelper.getAllDeclaredMethods(behaviorClass) val commandHandlers = allMethods @@ -136,23 +136,23 @@ private object CrudBehaviorReflection { ReflectionHelper.validateNoBadMethods( allMethods, - classOf[CrudEntity], + classOf[ValueEntity], Set(classOf[CommandHandler]) ) - new CrudBehaviorReflection(commandHandlers) + new ValueEntityBehaviorReflection(commandHandlers) } } -private class EntityConstructorInvoker(constructor: Constructor[_]) extends (CrudEntityCreationContext => AnyRef) { - private val parameters = ReflectionHelper.getParameterHandlers[AnyRef, CrudEntityCreationContext](constructor)() +private class EntityConstructorInvoker(constructor: Constructor[_]) extends (ValueEntityCreationContext => AnyRef) { + private val parameters = ReflectionHelper.getParameterHandlers[AnyRef, ValueEntityCreationContext](constructor)() parameters.foreach { case MainArgumentParameterHandler(clazz) => throw new RuntimeException(s"Don't know how to handle argument of type $clazz in constructor") case _ => } - def apply(context: CrudEntityCreationContext): AnyRef = { + def apply(context: ValueEntityCreationContext): AnyRef = { val ctx = InvocationContext(null.asInstanceOf[AnyRef], context) constructor.newInstance(parameters.map(_.apply(ctx)): _*).asInstanceOf[AnyRef] } diff --git a/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/CrudImpl.scala b/java-support/src/main/scala/io/cloudstate/javasupport/impl/valueentity/ValueEntityImpl.scala similarity index 71% rename from java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/CrudImpl.scala rename to java-support/src/main/scala/io/cloudstate/javasupport/impl/valueentity/ValueEntityImpl.scala index fb4d6ad0b..41509f7ed 100644 --- a/java-support/src/main/scala/io/cloudstate/javasupport/impl/crud/CrudImpl.scala +++ b/java-support/src/main/scala/io/cloudstate/javasupport/impl/valueentity/ValueEntityImpl.scala @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.cloudstate.javasupport.impl.crud +package io.cloudstate.javasupport.impl.valueentity import java.util.Optional @@ -25,23 +25,26 @@ import akka.stream.scaladsl.Flow import io.cloudstate.javasupport.CloudStateRunner.Configuration import com.google.protobuf.{Descriptors, Any => JavaPbAny} import com.google.protobuf.any.{Any => ScalaPbAny} -import io.cloudstate.javasupport.crud._ +import io.cloudstate.javasupport.valueentity._ import io.cloudstate.javasupport.impl._ -import io.cloudstate.javasupport.impl.crud.CrudImpl.{failure, failureMessage, EntityException, ProtocolException} import io.cloudstate.javasupport.{Context, Metadata, Service, ServiceCallFactory} -import io.cloudstate.protocol.crud.CrudAction.Action.{Delete, Update} -import io.cloudstate.protocol.crud._ -import io.cloudstate.protocol.crud.CrudStreamIn.Message.{Command => InCommand, Empty => InEmpty, Init => InInit} -import io.cloudstate.protocol.crud.CrudStreamOut.Message.{Failure => OutFailure, Reply => OutReply} +import io.cloudstate.protocol.value_entity.ValueEntityAction.Action.{Delete, Update} +import io.cloudstate.protocol.value_entity.ValueEntityStreamIn.Message.{ + Command => InCommand, + Empty => InEmpty, + Init => InInit +} +import io.cloudstate.protocol.value_entity.ValueEntityStreamOut.Message.{Failure => OutFailure, Reply => OutReply} +import io.cloudstate.protocol.value_entity._ import io.cloudstate.protocol.entity.{Command, Failure} import scala.compat.java8.OptionConverters._ import scala.util.control.NonFatal -final class CrudStatefulService(val factory: CrudEntityFactory, - override val descriptor: Descriptors.ServiceDescriptor, - val anySupport: AnySupport, - override val persistenceId: String) +final class ValueEntityStatefulService(val factory: ValueEntityFactory, + override val descriptor: Descriptors.ServiceDescriptor, + val anySupport: AnySupport, + override val persistenceId: String) extends Service { override def resolvedMethods: Option[Map[String, ResolvedServiceMethod[_, _]]] = @@ -50,10 +53,10 @@ final class CrudStatefulService(val factory: CrudEntityFactory, case _ => None } - override final val entityType = io.cloudstate.protocol.crud.Crud.name + override final val entityType = io.cloudstate.protocol.value_entity.ValueEntityProtocol.name } -object CrudImpl { +object ValueEntityImpl { final case class EntityException(entityId: String, commandId: Long, commandName: String, message: String) extends RuntimeException(message) @@ -72,7 +75,7 @@ object CrudImpl { def apply(message: String): EntityException = EntityException(entityId = "", commandId = 0, commandName = "", "Protocol error: " + message) - def apply(init: CrudInit, message: String): EntityException = + def apply(init: ValueEntityInit, message: String): EntityException = EntityException(init.entityId, commandId = 0, commandName = "", "Protocol error: " + message) def apply(command: Command, message: String): EntityException = @@ -93,11 +96,13 @@ object CrudImpl { } } -final class CrudImpl(_system: ActorSystem, - _services: Map[String, CrudStatefulService], - rootContext: Context, - configuration: Configuration) - extends Crud { +final class ValueEntityImpl(_system: ActorSystem, + _services: Map[String, ValueEntityStatefulService], + rootContext: Context, + configuration: Configuration) + extends ValueEntityProtocol { + + import ValueEntityImpl._ private final val system = _system private final implicit val ec = system.dispatcher @@ -116,40 +121,40 @@ final class CrudImpl(_system: ActorSystem, * as if they had arrived as state update when the stream was being replayed on load. */ override def handle( - in: akka.stream.scaladsl.Source[CrudStreamIn, akka.NotUsed] - ): akka.stream.scaladsl.Source[CrudStreamOut, akka.NotUsed] = + in: akka.stream.scaladsl.Source[ValueEntityStreamIn, akka.NotUsed] + ): akka.stream.scaladsl.Source[ValueEntityStreamOut, akka.NotUsed] = in.prefixAndTail(1) .flatMapConcat { - case (Seq(CrudStreamIn(InInit(init), _)), source) => + case (Seq(ValueEntityStreamIn(InInit(init), _)), source) => source.via(runEntity(init)) case _ => - throw ProtocolException("Expected Init message for CRUD entity") + throw ProtocolException("Expected init message for Value entity") } .recover { case error => log.error(error, failureMessage(error)) - CrudStreamOut(OutFailure(failure(error))) + ValueEntityStreamOut(OutFailure(failure(error))) } - private def runEntity(init: CrudInit): Flow[CrudStreamIn, CrudStreamOut, NotUsed] = { + private def runEntity(init: ValueEntityInit): Flow[ValueEntityStreamIn, ValueEntityStreamOut, NotUsed] = { val service = services.getOrElse(init.serviceName, throw ProtocolException(init, s"Service not found: ${init.serviceName}")) - val handler = service.factory.create(new CrudContextImpl(init.entityId)) + val handler = service.factory.create(new ValueEntityContextImpl(init.entityId)) val thisEntityId = init.entityId val initState = init.state match { - case Some(CrudInitState(state, _)) => state + case Some(ValueEntityInitState(state, _)) => state case _ => None // should not happen!!! } - Flow[CrudStreamIn] + Flow[ValueEntityStreamIn] .map(_.message) - .scan[(Option[ScalaPbAny], Option[CrudStreamOut.Message])]((initState, None)) { + .scan[(Option[ScalaPbAny], Option[ValueEntityStreamOut.Message])]((initState, None)) { case (_, InCommand(command)) if thisEntityId != command.entityId => - throw ProtocolException(command, "Receiving CRUD entity is not the intended recipient of command") + throw ProtocolException(command, "Receiving Value entity is not the intended recipient of command") case (_, InCommand(command)) if command.payload.isEmpty => - throw ProtocolException(command, "No command payload for CRUD entity") + throw ProtocolException(command, "No command payload for Value entity") case ((state, _), InCommand(command)) => val cmd = ScalaPbAny.toJavaProto(command.payload.get) @@ -169,7 +174,7 @@ final class CrudImpl(_system: ActorSystem, case FailInvoked => Option.empty[JavaPbAny].asJava case e: EntityException => throw e case NonFatal(error) => - throw EntityException(command, s"CRUD entity unexpected failure: ${error.getMessage}") + throw EntityException(command, s"Value entity unexpected failure: ${error.getMessage}") } finally { context.deactivate() // Very important! } @@ -180,7 +185,7 @@ final class CrudImpl(_system: ActorSystem, (nextState, Some( OutReply( - CrudReply( + ValueEntityReply( command.id, clientAction, context.sideEffects, @@ -192,7 +197,7 @@ final class CrudImpl(_system: ActorSystem, (state, Some( OutReply( - CrudReply( + ValueEntityReply( commandId = command.id, clientAction = clientAction ) @@ -201,17 +206,17 @@ final class CrudImpl(_system: ActorSystem, } case (_, InInit(_)) => - throw ProtocolException(init, "CRUD Entity already inited") + throw ProtocolException(init, "Value entity already inited") case (_, InEmpty) => - throw ProtocolException(init, "CRUD entity received empty/unknown message") + throw ProtocolException(init, "Value entity received empty/unknown message") } .collect { - case (_, Some(message)) => CrudStreamOut(message) + case (_, Some(message)) => ValueEntityStreamOut(message) } } - trait AbstractContext extends CrudContext { + trait AbstractContext extends ValueEntityContext { override def serviceCallFactory(): ServiceCallFactory = rootContext.serviceCallFactory() } @@ -228,7 +233,7 @@ final class CrudImpl(_system: ActorSystem, with AbstractEffectContext with ActivatableContext { - final var action: Option[CrudAction] = None + final var action: Option[ValueEntityAction] = None private var _state: Option[ScalaPbAny] = state override def getState(): Optional[JavaPbAny] = { @@ -239,28 +244,30 @@ final class CrudImpl(_system: ActorSystem, override def updateState(state: JavaPbAny): Unit = { checkActive() if (state == null) - throw EntityException("CRUD entity cannot update a 'null' state") + throw EntityException("Value entity cannot update a 'null' state") val encoded = anySupport.encodeScala(state) _state = Some(encoded) - action = Some(CrudAction(Update(CrudUpdate(_state)))) + action = Some(ValueEntityAction(Update(ValueEntityUpdate(_state)))) } override def deleteState(): Unit = { checkActive() _state = None - action = Some(CrudAction(Delete(CrudDelete()))) + action = Some(ValueEntityAction(Delete(ValueEntityDelete()))) } override protected def logError(message: String): Unit = - log.error("Fail invoked for command [{}] for CRUD entity [{}]: {}", commandName, entityId, message) + log.error("Fail invoked for command [{}] for Value entity [{}]: {}", commandName, entityId, message) def currentState(): Option[ScalaPbAny] = _state } - private final class CrudContextImpl(override final val entityId: String) extends CrudContext with AbstractContext + private final class ValueEntityContextImpl(override final val entityId: String) + extends ValueEntityContext + with AbstractContext } diff --git a/java-support/src/test/scala/io/cloudstate/javasupport/impl/crud/AnnotationBasedCrudSupportSpec.scala b/java-support/src/test/scala/io/cloudstate/javasupport/impl/valueentity/AnnotationBasedValueEntitySupportSpec.scala similarity index 89% rename from java-support/src/test/scala/io/cloudstate/javasupport/impl/crud/AnnotationBasedCrudSupportSpec.scala rename to java-support/src/test/scala/io/cloudstate/javasupport/impl/valueentity/AnnotationBasedValueEntitySupportSpec.scala index 87d9c3c39..0df7e93c0 100644 --- a/java-support/src/test/scala/io/cloudstate/javasupport/impl/crud/AnnotationBasedCrudSupportSpec.scala +++ b/java-support/src/test/scala/io/cloudstate/javasupport/impl/valueentity/AnnotationBasedValueEntitySupportSpec.scala @@ -14,20 +14,20 @@ * limitations under the License. */ -package io.cloudstate.javasupport.impl.crud +package io.cloudstate.javasupport.impl.valueentity import java.util.Optional -import com.example.crud.shoppingcart.Shoppingcart +import com.example.valueentity.shoppingcart.Shoppingcart import com.google.protobuf.any.{Any => ScalaPbAny} import com.google.protobuf.{ByteString, Any => JavaPbAny} -import io.cloudstate.javasupport.crud.{ +import io.cloudstate.javasupport.valueentity.{ CommandContext, CommandHandler, - CrudContext, - CrudEntity, - CrudEntityCreationContext, - CrudEntityHandler + ValueEntity, + ValueEntityContext, + ValueEntityCreationContext, + ValueEntityHandler } import io.cloudstate.javasupport.impl.{AnySupport, ResolvedServiceMethod, ResolvedType} import io.cloudstate.javasupport.{Context, EntityId, Metadata, ServiceCall, ServiceCallFactory, ServiceCallRef} @@ -35,7 +35,7 @@ import org.scalatest.{Matchers, WordSpec} import scala.compat.java8.OptionConverters._ -class AnnotationBasedCrudSupportSpec extends WordSpec with Matchers { +class AnnotationBasedValueEntitySupportSpec extends WordSpec with Matchers { trait BaseContext extends Context { override def serviceCallFactory(): ServiceCallFactory = new ServiceCallFactory { override def lookup[T](serviceName: String, methodName: String, messageType: Class[T]): ServiceCallRef[T] = @@ -43,7 +43,7 @@ class AnnotationBasedCrudSupportSpec extends WordSpec with Matchers { } } - object MockContext extends CrudContext with BaseContext { + object MockContext extends ValueEntityContext with BaseContext { override def entityId(): String = "foo" } @@ -84,14 +84,14 @@ class AnnotationBasedCrudSupportSpec extends WordSpec with Matchers { def method(name: String = "AddItem"): ResolvedServiceMethod[String, Wrapped] = ResolvedServiceMethod(serviceDescriptor.findMethodByName(name), StringResolvedType, WrappedResolvedType) - def create(behavior: AnyRef, methods: ResolvedServiceMethod[_, _]*): CrudEntityHandler = - new AnnotationBasedCrudSupport(behavior.getClass, - anySupport, - methods.map(m => m.descriptor.getName -> m).toMap, - Some(_ => behavior)).create(MockContext) + def create(behavior: AnyRef, methods: ResolvedServiceMethod[_, _]*): ValueEntityHandler = + new AnnotationBasedValueEntitySupport(behavior.getClass, + anySupport, + methods.map(m => m.descriptor.getName -> m).toMap, + Some(_ => behavior)).create(MockContext) - def create(clazz: Class[_]): CrudEntityHandler = - new AnnotationBasedCrudSupport(clazz, anySupport, Map.empty, None).create(MockContext) + def create(clazz: Class[_]): ValueEntityHandler = + new AnnotationBasedValueEntitySupport(clazz, anySupport, Map.empty, None).create(MockContext) def command(str: String) = ScalaPbAny.toJavaProto(ScalaPbAny(StringResolvedType.typeUrl, StringResolvedType.toByteString(str))) @@ -103,7 +103,7 @@ class AnnotationBasedCrudSupportSpec extends WordSpec with Matchers { def state(any: Any): JavaPbAny = anySupport.encodeJava(any) - "Crud annotation support" should { + "Value entity annotation support" should { "support entity construction" when { "there is a noarg constructor" in { @@ -274,24 +274,24 @@ class AnnotationBasedCrudSupportSpec extends WordSpec with Matchers { import Matchers._ -@CrudEntity +@ValueEntity private class NoArgConstructorTest() {} -@CrudEntity +@ValueEntity private class EntityIdArgConstructorTest(@EntityId entityId: String) { entityId should ===("foo") } -@CrudEntity -private class CreationContextArgConstructorTest(ctx: CrudEntityCreationContext) { +@ValueEntity +private class CreationContextArgConstructorTest(ctx: ValueEntityCreationContext) { ctx.entityId should ===("foo") } -@CrudEntity -private class MultiArgConstructorTest(ctx: CrudContext, @EntityId entityId: String) { +@ValueEntity +private class MultiArgConstructorTest(ctx: ValueEntityContext, @EntityId entityId: String) { ctx.entityId should ===("foo") entityId should ===("foo") } -@CrudEntity +@ValueEntity private class UnsupportedConstructorParameter(foo: String) diff --git a/java-support/src/test/scala/io/cloudstate/javasupport/impl/crud/TestCrud.scala b/java-support/src/test/scala/io/cloudstate/javasupport/impl/valueentity/TestValueEntity.scala similarity index 76% rename from java-support/src/test/scala/io/cloudstate/javasupport/impl/crud/TestCrud.scala rename to java-support/src/test/scala/io/cloudstate/javasupport/impl/valueentity/TestValueEntity.scala index 58b59fce8..4a5e5446b 100644 --- a/java-support/src/test/scala/io/cloudstate/javasupport/impl/crud/TestCrud.scala +++ b/java-support/src/test/scala/io/cloudstate/javasupport/impl/valueentity/TestValueEntity.scala @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.cloudstate.javasupport.impl.crud +package io.cloudstate.javasupport.impl.valueentity import akka.testkit.EventFilter import com.google.protobuf.Descriptors.{FileDescriptor, ServiceDescriptor} @@ -23,12 +23,14 @@ import io.cloudstate.javasupport.{CloudState, CloudStateRunner} import scala.reflect.ClassTag import io.cloudstate.testkit.Sockets -object TestCrud { - def service[T: ClassTag](descriptor: ServiceDescriptor, fileDescriptors: FileDescriptor*): TestCrudService = - new TestCrudService(implicitly[ClassTag[T]].runtimeClass, descriptor, fileDescriptors) +object TestValueEntity { + def service[T: ClassTag](descriptor: ServiceDescriptor, fileDescriptors: FileDescriptor*): TestValueEntityService = + new TestValueEntityService(implicitly[ClassTag[T]].runtimeClass, descriptor, fileDescriptors) } -class TestCrudService(entityClass: Class[_], descriptor: ServiceDescriptor, fileDescriptors: Seq[FileDescriptor]) { +class TestValueEntityService(entityClass: Class[_], + descriptor: ServiceDescriptor, + fileDescriptors: Seq[FileDescriptor]) { val port: Int = Sockets.temporaryLocalPort() val config: Config = ConfigFactory.load(ConfigFactory.parseString(s""" @@ -44,7 +46,7 @@ class TestCrudService(entityClass: Class[_], descriptor: ServiceDescriptor, file """)) val runner: CloudStateRunner = new CloudState() - .registerCrudEntity(entityClass, descriptor, fileDescriptors: _*) + .registerValueEntity(entityClass, descriptor, fileDescriptors: _*) .createRunner(config) runner.run() diff --git a/java-support/src/test/scala/io/cloudstate/javasupport/impl/crud/CrudImplSpec.scala b/java-support/src/test/scala/io/cloudstate/javasupport/impl/valueentity/ValueEntityImplSpec.scala similarity index 83% rename from java-support/src/test/scala/io/cloudstate/javasupport/impl/crud/CrudImplSpec.scala rename to java-support/src/test/scala/io/cloudstate/javasupport/impl/valueentity/ValueEntityImplSpec.scala index e29bde0e0..3942580cb 100644 --- a/java-support/src/test/scala/io/cloudstate/javasupport/impl/crud/CrudImplSpec.scala +++ b/java-support/src/test/scala/io/cloudstate/javasupport/impl/valueentity/ValueEntityImplSpec.scala @@ -14,57 +14,57 @@ * limitations under the License. */ -package io.cloudstate.javasupport.impl.crud +package io.cloudstate.javasupport.impl.valueentity import java.util.Optional import com.google.protobuf.Empty import io.cloudstate.javasupport.EntityId -import io.cloudstate.javasupport.crud.{CommandContext, CommandHandler, CrudEntity} +import io.cloudstate.javasupport.valueentity.{CommandContext, CommandHandler, ValueEntity} import io.cloudstate.testkit.TestProtocol -import io.cloudstate.testkit.crud.CrudMessages +import io.cloudstate.testkit.valuentity.ValueEntityMessages import org.scalatest.{BeforeAndAfterAll, Matchers, WordSpec} import scala.collection.mutable import scala.reflect.ClassTag -class CrudImplSpec extends WordSpec with Matchers with BeforeAndAfterAll { - import CrudImplSpec._ - import CrudMessages._ +class ValueEntityImplSpec extends WordSpec with Matchers with BeforeAndAfterAll { + import ValueEntityImplSpec._ + import ValueEntityMessages._ import ShoppingCart.Item import ShoppingCart.Protocol._ - val service: TestCrudService = ShoppingCart.testService - val protocol: TestProtocol = TestProtocol(service.port) + private val service: TestValueEntityService = ShoppingCart.testService + private val protocol: TestProtocol = TestProtocol(service.port) override def afterAll(): Unit = { protocol.terminate() service.terminate() } - "CrudImpl" should { + "ValueEntityImpl" should { "fail when first message is not init" in { service.expectLogError("Terminating entity due to unexpected failure") { - val entity = protocol.crud.connect() + val entity = protocol.valueEntity.connect() entity.send(command(1, "cart", "command")) - entity.expect(failure("Protocol error: Expected Init message for CRUD entity")) + entity.expect(failure("Protocol error: Expected init message for Value entity")) entity.expectClosed() } } "fail when entity is sent multiple init" in { service.expectLogError("Terminating entity [cart] due to unexpected failure") { - val entity = protocol.crud.connect() + val entity = protocol.valueEntity.connect() entity.send(init(ShoppingCart.Name, "cart")) entity.send(init(ShoppingCart.Name, "cart")) - entity.expect(failure("Protocol error: CRUD Entity already inited")) + entity.expect(failure("Protocol error: Value entity already inited")) entity.expectClosed() } } "fail when service doesn't exist" in { service.expectLogError("Terminating entity [foo] due to unexpected failure") { - val entity = protocol.crud.connect() + val entity = protocol.valueEntity.connect() entity.send(init(serviceName = "DoesNotExist", entityId = "foo")) entity.expect(failure("Protocol error: Service not found: DoesNotExist")) entity.expectClosed() @@ -73,37 +73,37 @@ class CrudImplSpec extends WordSpec with Matchers with BeforeAndAfterAll { "fail when command entity id is incorrect" in { service.expectLogError("Terminating entity [cart2] due to unexpected failure for command [foo]") { - val entity = protocol.crud.connect() + val entity = protocol.valueEntity.connect() entity.send(init(ShoppingCart.Name, "cart1")) entity.send(command(1, "cart2", "foo")) - entity.expect(failure(1, "Protocol error: Receiving CRUD entity is not the intended recipient of command")) + entity.expect(failure(1, "Protocol error: Receiving Value entity is not the intended recipient of command")) entity.expectClosed() } } "fail when command payload is missing" in { service.expectLogError("Terminating entity [cart] due to unexpected failure for command [foo]") { - val entity = protocol.crud.connect() + val entity = protocol.valueEntity.connect() entity.send(init(ShoppingCart.Name, "cart")) entity.send(command(1, "cart", "foo", payload = None)) - entity.expect(failure(1, "Protocol error: No command payload for CRUD entity")) + entity.expect(failure(1, "Protocol error: No command payload for Value entity")) entity.expectClosed() } } "fail when entity is sent empty message" in { service.expectLogError("Terminating entity [cart] due to unexpected failure") { - val entity = protocol.crud.connect() + val entity = protocol.valueEntity.connect() entity.send(init(ShoppingCart.Name, "cart")) entity.send(EmptyInMessage) - entity.expect(failure("Protocol error: CRUD entity received empty/unknown message")) + entity.expect(failure("Protocol error: Value entity received empty/unknown message")) entity.expectClosed() } } "fail when command handler does not exist" in { service.expectLogError("Terminating entity [cart] due to unexpected failure for command [foo]") { - val entity = protocol.crud.connect() + val entity = protocol.valueEntity.connect() entity.send(init(ShoppingCart.Name, "cart")) entity.send(command(1, "cart", "foo")) entity.expect(failure(1, s"No command handler found for command [foo] on ${ShoppingCart.TestCartClass}")) @@ -113,9 +113,9 @@ class CrudImplSpec extends WordSpec with Matchers with BeforeAndAfterAll { "fail action when command handler uses context fail" in { service.expectLogError( - "Fail invoked for command [AddItem] for CRUD entity [cart]: Cannot add negative quantity of item [foo]" + "Fail invoked for command [AddItem] for Value entity [cart]: Cannot add negative quantity of item [foo]" ) { - val entity = protocol.crud.connect() + val entity = protocol.valueEntity.connect() entity.send(init(ShoppingCart.Name, "cart")) entity.send(command(1, "cart", "AddItem", addItem("foo", "bar", -1))) entity.expect(actionFailure(1, "Cannot add negative quantity of item [foo]")) @@ -123,7 +123,7 @@ class CrudImplSpec extends WordSpec with Matchers with BeforeAndAfterAll { entity.expect(reply(2, EmptyCart)) // check update-then-fail doesn't change entity state entity.passivate() - val reactivated = protocol.crud.connect() + val reactivated = protocol.valueEntity.connect() reactivated.send(init(ShoppingCart.Name, "cart")) reactivated.send(command(1, "cart", "GetCart")) reactivated.expect(reply(1, EmptyCart)) @@ -133,16 +133,16 @@ class CrudImplSpec extends WordSpec with Matchers with BeforeAndAfterAll { "fail when command handler throws exception" in { service.expectLogError("Terminating entity [cart] due to unexpected failure for command [RemoveItem]") { - val entity = protocol.crud.connect() + val entity = protocol.valueEntity.connect() entity.send(init(ShoppingCart.Name, "cart")) entity.send(command(1, "cart", "RemoveItem", removeItem("foo"))) - entity.expect(failure(1, "CRUD entity unexpected failure: Boom: foo")) + entity.expect(failure(1, "Value entity unexpected failure: Boom: foo")) entity.expectClosed() } } "manage entities with expected update commands" in { - val entity = protocol.crud.connect() + val entity = protocol.valueEntity.connect() entity.send(init(ShoppingCart.Name, "cart")) entity.send(command(1, "cart", "GetCart")) entity.expect(reply(1, EmptyCart)) @@ -156,7 +156,7 @@ class CrudImplSpec extends WordSpec with Matchers with BeforeAndAfterAll { entity.expect(reply(5, EmptyJavaMessage, update(domainCart(Item("abc", "apple", 3), Item("123", "banana", 4))))) entity.passivate() - val reactivated = protocol.crud.connect() + val reactivated = protocol.valueEntity.connect() reactivated.send( init(ShoppingCart.Name, "cart", state(domainCart(Item("abc", "apple", 3), Item("123", "banana", 4)))) ) @@ -170,7 +170,7 @@ class CrudImplSpec extends WordSpec with Matchers with BeforeAndAfterAll { } "manage entities with expected delete commands" in { - val entity = protocol.crud.connect() + val entity = protocol.valueEntity.connect() entity.send(init(ShoppingCart.Name, "cart")) entity.send(command(1, "cart", "GetCart")) entity.expect(reply(1, EmptyCart)) @@ -187,18 +187,18 @@ class CrudImplSpec extends WordSpec with Matchers with BeforeAndAfterAll { } } -object CrudImplSpec { +object ValueEntityImplSpec { object ShoppingCart { - import com.example.crud.shoppingcart.Shoppingcart - import com.example.crud.shoppingcart.persistence.Domain + import com.example.valueentity.shoppingcart.Shoppingcart + import com.example.valueentity.shoppingcart.persistence.Domain val Name: String = Shoppingcart.getDescriptor.findServiceByName("ShoppingCart").getFullName - def testService: TestCrudService = service[TestCart] + def testService: TestValueEntityService = service[TestCart] - def service[T: ClassTag]: TestCrudService = - TestCrud.service[T]( + def service[T: ClassTag]: TestValueEntityService = + TestValueEntity.service[T]( Shoppingcart.getDescriptor.findServiceByName("ShoppingCart"), Domain.getDescriptor ) @@ -240,7 +240,7 @@ object CrudImplSpec { val TestCartClass: Class[_] = classOf[TestCart] - @CrudEntity(persistenceId = "crud-shopping-cart") + @ValueEntity(persistenceId = "crud-shopping-cart") class TestCart(@EntityId val entityId: String) { import scala.jdk.OptionConverters._ import scala.jdk.CollectionConverters._ diff --git a/java-support/tck/src/main/java/io/cloudstate/javasupport/tck/JavaSupportTck.java b/java-support/tck/src/main/java/io/cloudstate/javasupport/tck/JavaSupportTck.java index 9ce0f739c..cbd8fc6fb 100644 --- a/java-support/tck/src/main/java/io/cloudstate/javasupport/tck/JavaSupportTck.java +++ b/java-support/tck/src/main/java/io/cloudstate/javasupport/tck/JavaSupportTck.java @@ -18,13 +18,13 @@ import com.example.shoppingcart.Shoppingcart; import io.cloudstate.javasupport.CloudState; -import io.cloudstate.javasupport.tck.model.crud.CrudTckModelEntity; -import io.cloudstate.javasupport.tck.model.crud.CrudTwoEntity; +import io.cloudstate.javasupport.tck.model.valuentity.ValueEntityTckModelEntity; +import io.cloudstate.javasupport.tck.model.valuentity.ValueEntityTwoEntity; import io.cloudstate.javasupport.tck.model.eventsourced.EventSourcedTckModelEntity; import io.cloudstate.javasupport.tck.model.eventsourced.EventSourcedTwoEntity; import io.cloudstate.samples.shoppingcart.ShoppingCartEntity; import io.cloudstate.tck.model.Eventsourced; -import io.cloudstate.tck.model.crud.Crud; +import io.cloudstate.tck.model.valuentity.Valueentity; public final class JavaSupportTck { public static final void main(String[] args) throws Exception { @@ -40,16 +40,18 @@ public static final void main(String[] args) throws Exception { ShoppingCartEntity.class, Shoppingcart.getDescriptor().findServiceByName("ShoppingCart"), com.example.shoppingcart.persistence.Domain.getDescriptor()) - .registerCrudEntity( - CrudTckModelEntity.class, - Crud.getDescriptor().findServiceByName("CrudTckModel"), - Crud.getDescriptor()) - .registerCrudEntity(CrudTwoEntity.class, Crud.getDescriptor().findServiceByName("CrudTwo")) - .registerCrudEntity( - io.cloudstate.samples.crud.shoppingcart.ShoppingCartEntity.class, - com.example.crud.shoppingcart.Shoppingcart.getDescriptor() + .registerValueEntity( + ValueEntityTckModelEntity.class, + Valueentity.getDescriptor().findServiceByName("ValueEntityTckModel"), + Valueentity.getDescriptor()) + .registerValueEntity( + ValueEntityTwoEntity.class, + Valueentity.getDescriptor().findServiceByName("ValueEntityTwo")) + .registerValueEntity( + io.cloudstate.samples.valueentity.shoppingcart.ShoppingCartEntity.class, + com.example.valueentity.shoppingcart.Shoppingcart.getDescriptor() .findServiceByName("ShoppingCart"), - com.example.crud.shoppingcart.persistence.Domain.getDescriptor()) + com.example.valueentity.shoppingcart.persistence.Domain.getDescriptor()) .start() .toCompletableFuture() .get(); diff --git a/java-support/tck/src/main/java/io/cloudstate/javasupport/tck/model/crud/CrudTckModelEntity.java b/java-support/tck/src/main/java/io/cloudstate/javasupport/tck/model/valuentity/ValueEntityTckModelEntity.java similarity index 73% rename from java-support/tck/src/main/java/io/cloudstate/javasupport/tck/model/crud/CrudTckModelEntity.java rename to java-support/tck/src/main/java/io/cloudstate/javasupport/tck/model/valuentity/ValueEntityTckModelEntity.java index 9c862766d..9f79af986 100644 --- a/java-support/tck/src/main/java/io/cloudstate/javasupport/tck/model/crud/CrudTckModelEntity.java +++ b/java-support/tck/src/main/java/io/cloudstate/javasupport/tck/model/valuentity/ValueEntityTckModelEntity.java @@ -14,37 +14,37 @@ * limitations under the License. */ -package io.cloudstate.javasupport.tck.model.crud; +package io.cloudstate.javasupport.tck.model.valuentity; import io.cloudstate.javasupport.Context; import io.cloudstate.javasupport.ServiceCall; import io.cloudstate.javasupport.ServiceCallRef; -import io.cloudstate.javasupport.crud.CrudEntity; -import io.cloudstate.javasupport.crud.CommandContext; -import io.cloudstate.javasupport.crud.CommandHandler; -import io.cloudstate.tck.model.crud.Crud; -import io.cloudstate.tck.model.crud.Crud.*; +import io.cloudstate.javasupport.valueentity.ValueEntity; +import io.cloudstate.javasupport.valueentity.CommandContext; +import io.cloudstate.javasupport.valueentity.CommandHandler; +import io.cloudstate.tck.model.valuentity.Valueentity; +import io.cloudstate.tck.model.valuentity.Valueentity.*; import java.util.Optional; -@CrudEntity(persistenceId = "crud-tck-model") -public class CrudTckModelEntity { +@ValueEntity(persistenceId = "value-entity-tck-model") +public class ValueEntityTckModelEntity { private final ServiceCallRef serviceTwoCall; private String state = ""; - public CrudTckModelEntity(Context context) { + public ValueEntityTckModelEntity(Context context) { serviceTwoCall = context .serviceCallFactory() - .lookup("cloudstate.tck.model.crud.CrudTwo", "Call", Request.class); + .lookup("cloudstate.tck.model.valuentity.ValueEntityTwo", "Call", Request.class); } @CommandHandler public Optional process(Request request, CommandContext context) { boolean forwarding = false; - for (Crud.RequestAction action : request.getActionsList()) { + for (Valueentity.RequestAction action : request.getActionsList()) { switch (action.getActionCase()) { case UPDATE: state = action.getUpdate().getValue(); @@ -59,7 +59,7 @@ public Optional process(Request request, CommandContext con context.forward(serviceTwoRequest(action.getForward().getId())); break; case EFFECT: - Crud.Effect effect = action.getEffect(); + Valueentity.Effect effect = action.getEffect(); context.effect(serviceTwoRequest(effect.getId()), effect.getSynchronous()); break; case FAIL: diff --git a/java-support/tck/src/main/java/io/cloudstate/javasupport/tck/model/crud/CrudTwoEntity.java b/java-support/tck/src/main/java/io/cloudstate/javasupport/tck/model/valuentity/ValueEntityTwoEntity.java similarity index 64% rename from java-support/tck/src/main/java/io/cloudstate/javasupport/tck/model/crud/CrudTwoEntity.java rename to java-support/tck/src/main/java/io/cloudstate/javasupport/tck/model/valuentity/ValueEntityTwoEntity.java index 86f60e315..f86e19cc7 100644 --- a/java-support/tck/src/main/java/io/cloudstate/javasupport/tck/model/crud/CrudTwoEntity.java +++ b/java-support/tck/src/main/java/io/cloudstate/javasupport/tck/model/valuentity/ValueEntityTwoEntity.java @@ -14,16 +14,16 @@ * limitations under the License. */ -package io.cloudstate.javasupport.tck.model.crud; +package io.cloudstate.javasupport.tck.model.valuentity; -import io.cloudstate.javasupport.crud.CrudEntity; -import io.cloudstate.javasupport.crud.CommandHandler; -import io.cloudstate.tck.model.crud.Crud.Request; -import io.cloudstate.tck.model.crud.Crud.Response; +import io.cloudstate.javasupport.valueentity.ValueEntity; +import io.cloudstate.javasupport.valueentity.CommandHandler; +import io.cloudstate.tck.model.valuentity.Valueentity.Request; +import io.cloudstate.tck.model.valuentity.Valueentity.Response; -@CrudEntity -public class CrudTwoEntity { - public CrudTwoEntity() {} +@ValueEntity +public class ValueEntityTwoEntity { + public ValueEntityTwoEntity() {} @CommandHandler public Response call(Request request) { diff --git a/protocols/example/crud/shoppingcart/persistence/domain.proto b/protocols/example/valueentity/shoppingcart/persistence/domain.proto similarity index 76% rename from protocols/example/crud/shoppingcart/persistence/domain.proto rename to protocols/example/valueentity/shoppingcart/persistence/domain.proto index 50d58c805..59390d000 100644 --- a/protocols/example/crud/shoppingcart/persistence/domain.proto +++ b/protocols/example/valueentity/shoppingcart/persistence/domain.proto @@ -12,12 +12,12 @@ // See the License for the specific language governing permissions and // limitations under the License. -// These are the messages that get persisted - the events, plus the current state (Cart) for snapshots. +// These are the messages that get persisted - the current state (Cart). syntax = "proto3"; -package com.example.crud.shoppingcart.persistence; +package com.example.valueentity.shoppingcart.persistence; -option go_package = "crud.shoppingcart.persistence"; +option go_package = "github.com/cloudstateio/go-support/example/valueentity/shoppingcart/persistence;persistence"; message LineItem { string productId = 1; diff --git a/protocols/example/crud/shoppingcart/shoppingcart.proto b/protocols/example/valueentity/shoppingcart/shoppingcart.proto similarity index 82% rename from protocols/example/crud/shoppingcart/shoppingcart.proto rename to protocols/example/valueentity/shoppingcart/shoppingcart.proto index dc00d6301..a5290419f 100644 --- a/protocols/example/crud/shoppingcart/shoppingcart.proto +++ b/protocols/example/valueentity/shoppingcart/shoppingcart.proto @@ -22,9 +22,9 @@ import "google/api/annotations.proto"; import "google/api/http.proto"; import "google/api/httpbody.proto"; -package com.example.crud.shoppingcart; +package com.example.valueentity.shoppingcart; -option go_package = "tck/crud/shoppingcart"; +option go_package = "github.com/cloudstateio/go-support/example/valueentity/shoppingcart;shoppingcart"; message AddLineItem { string user_id = 1 [(.cloudstate.entity_key) = true]; @@ -59,27 +59,27 @@ message Cart { service ShoppingCart { rpc AddItem(AddLineItem) returns (google.protobuf.Empty) { option (google.api.http) = { - post: "/crud/cart/{user_id}/items/add", + post: "/ve/cart/{user_id}/items/add", body: "*", }; option (.cloudstate.eventing).in = "items"; } rpc RemoveItem(RemoveLineItem) returns (google.protobuf.Empty) { - option (google.api.http).post = "/crud/cart/{user_id}/items/{product_id}/remove"; + option (google.api.http).post = "/ve/cart/{user_id}/items/{product_id}/remove"; } rpc GetCart(GetShoppingCart) returns (Cart) { option (google.api.http) = { - get: "/crud/carts/{user_id}", + get: "/ve/carts/{user_id}", additional_bindings: { - get: "/crud/carts/{user_id}/items", + get: "/ve/carts/{user_id}/items", response_body: "items" } }; } rpc RemoveCart(RemoveShoppingCart) returns (google.protobuf.Empty) { - option (google.api.http).post = "/crud/carts/{user_id}/remove"; + option (google.api.http).post = "/ve/carts/{user_id}/remove"; } } diff --git a/protocols/protocol/cloudstate/crud.proto b/protocols/protocol/cloudstate/value_entity.proto similarity index 61% rename from protocols/protocol/cloudstate/crud.proto rename to protocols/protocol/cloudstate/value_entity.proto index e7a28fbe9..576f2aabd 100644 --- a/protocols/protocol/cloudstate/crud.proto +++ b/protocols/protocol/cloudstate/value_entity.proto @@ -16,7 +16,7 @@ syntax = "proto3"; -package cloudstate.crud; +package cloudstate.valueentity; // Any is used so that domain events defined according to the functions business domain can be embedded inside // the protocol. @@ -26,31 +26,29 @@ import "cloudstate/entity.proto"; option java_package = "io.cloudstate.protocol"; option go_package = "github.com/cloudstateio/go-support/cloudstate/entity;entity"; -// The CRUD Entity service -service Crud { +// The Value Entity service +service ValueEntityProtocol { // One stream will be established per active entity. // Once established, the first message sent will be Init, which contains the entity ID, and, - // a state if the entity has previously persisted one. The entity is expected to apply the - // received state to its state. Once the Init message is sent, one to many commands are sent, - // with new commands being sent as new requests for the entity come in. The entity is expected - // to reply to each command with exactly one reply message. The entity should reply in order - // and any state update that the entity requests to be persisted the entity should handle itself. - // The entity handles state update by replacing its own state with the update, - // as if they had arrived as state update when the stream was being replayed on load. - rpc handle(stream CrudStreamIn) returns (stream CrudStreamOut) {} + // a state if the entity has previously persisted one. Once the Init message is sent, one to + // many commands are sent to the entity. Each request coming in leads to a new command being sent + // to the entity. The entity is expected to reply to each command with exactly one reply message. + // The entity should process commands and reply to commands in the order they came + // in. When processing a command the entity can read and persist (update or delete) the state. + rpc handle(stream ValueEntityStreamIn) returns (stream ValueEntityStreamOut) {} } // Input message type for the gRPC stream in. -message CrudStreamIn { +message ValueEntityStreamIn { oneof message { - CrudInit init = 1; + ValueEntityInit init = 1; Command command = 2; } } // The init message. This will always be the first message sent to the entity when it is loaded. -message CrudInit { +message ValueEntityInit { // The name of the service that implements this entity. string service_name = 1; @@ -58,25 +56,25 @@ message CrudInit { string entity_id = 2; // The initial state of the entity. - CrudInitState state = 3; + ValueEntityInitState state = 3; } // The state of the entity when it is first activated. -message CrudInitState { +message ValueEntityInitState { // The value of the entity state, if the entity has already been created. google.protobuf.Any value = 1; } // Output message type for the gRPC stream out. -message CrudStreamOut { +message ValueEntityStreamOut { oneof message { - CrudReply reply = 1; + ValueEntityReply reply = 1; Failure failure = 2; } } // A reply to a command. -message CrudReply { +message ValueEntityReply { // The command being replied to int64 command_id = 1; @@ -86,23 +84,23 @@ message CrudReply { // Any side effects to perform repeated SideEffect side_effects = 3; - // The action to take on the CRUD entity - CrudAction crud_action = 4; + // The action to take on the value entity + ValueEntityAction state_action = 4; } // An action to take for changing the entity state. -message CrudAction { +message ValueEntityAction { oneof action { - CrudUpdate update = 1; - CrudDelete delete = 2; + ValueEntityUpdate update = 1; + ValueEntityDelete delete = 2; } } -// An action which updates the persisted value of the CRUD entity. If the entity is not yet persisted, it will be created. -message CrudUpdate { +// An action which updates the persisted value of the Value entity. If the entity is not yet persisted, it will be created. +message ValueEntityUpdate { // The value to set. google.protobuf.Any value = 1; } -// An action which deletes the persisted value of the CRUD entity. -message CrudDelete {} \ No newline at end of file +// An action which deletes the persisted value of the Value entity. +message ValueEntityDelete {} \ No newline at end of file diff --git a/protocols/tck/cloudstate/tck/model/crud.proto b/protocols/tck/cloudstate/tck/model/valueentity.proto similarity index 82% rename from protocols/tck/cloudstate/tck/model/crud.proto rename to protocols/tck/cloudstate/tck/model/valueentity.proto index 80f259c92..eeb6479f0 100644 --- a/protocols/tck/cloudstate/tck/model/crud.proto +++ b/protocols/tck/cloudstate/tck/model/valueentity.proto @@ -13,21 +13,22 @@ // limitations under the License. // -// == Cloudstate TCK model test for event-sourced entities == +// == Cloudstate TCK model test for value-entity entities == // syntax = "proto3"; -package cloudstate.tck.model.crud; +package cloudstate.tck.model.valuentity; import "cloudstate/entity_key.proto"; -option java_package = "io.cloudstate.tck.model.crud"; +option java_package = "io.cloudstate.tck.model.valuentity"; +option go_package = "github.com/cloudstateio/go-support/tck/valuentity;valuentity"; // -// The `CrudTckModel` service should be implemented in the following ways: +// The `ValueEntityTckModel` service should be implemented in the following ways: // -// - The entity persistence-id must be `crud-tck-model`. +// - The entity persistence-id must be `value-entity-tck-model`. // - The state of the entity is simply a string. // - The state string values is wrapped in `Persisted` messages. // - The command handler must set the state to the value of a `Persisted` message. @@ -36,15 +37,15 @@ option java_package = "io.cloudstate.tck.model.crud"; // - The `Process` method must reply with the state in a `Response`, after taking actions, unless forwarding or failing. // - Forwarding and side effects must always be made to the second service `CrudTwo`. // -service CrudTckModel { +service ValueEntityTckModel { rpc Process(Request) returns (Response); } // -// The `CrudTwo` service is only for verifying forward actions and side effects. +// The `ValueEntityTwo` service is only for verifying forward actions and side effects. // The `Call` method is not required to do anything, and may simply return an empty `Response` message. // -service CrudTwo { +service ValueEntityTwo { rpc Call(Request) returns (Response); } @@ -89,7 +90,7 @@ message Update { message Delete {} // -// Replace the response with a forward to `cloudstate.tck.model.CrudTwo/Call`. +// Replace the response with a forward to `cloudstate.tck.model.valuentity.ValueEntityTwo/Call`. // The payload must be a `Request` message with the given `id`. // message Forward { @@ -97,7 +98,7 @@ message Forward { } // -// Add a side effect to the reply, to `cloudstate.tck.model.CrudTwo/Call`. +// Add a side effect to the reply, to `cloudstate.tck.model.valuentity.ValueEntityTwo/Call`. // The payload must be a `Request` message with the given `id`. // The side effect should be marked synchronous based on the given `synchronous` value. // diff --git a/proxy/core/src/main/scala/io/cloudstate/proxy/EntityDiscoveryManager.scala b/proxy/core/src/main/scala/io/cloudstate/proxy/EntityDiscoveryManager.scala index cb8030f35..f36fec8b5 100644 --- a/proxy/core/src/main/scala/io/cloudstate/proxy/EntityDiscoveryManager.scala +++ b/proxy/core/src/main/scala/io/cloudstate/proxy/EntityDiscoveryManager.scala @@ -32,11 +32,11 @@ import com.typesafe.config.Config import io.cloudstate.protocol.action.ActionProtocol import io.cloudstate.protocol.entity._ import io.cloudstate.protocol.crdt.Crdt -import io.cloudstate.protocol.crud.Crud +import io.cloudstate.protocol.value_entity.ValueEntityProtocol import io.cloudstate.protocol.event_sourced.EventSourced import io.cloudstate.proxy.action.ActionProtocolSupportFactory import io.cloudstate.proxy.crdt.CrdtSupportFactory -import io.cloudstate.proxy.crud.CrudSupportFactory +import io.cloudstate.proxy.valueentity.ValueEntitySupportFactory import io.cloudstate.proxy.eventsourced.EventSourcedSupportFactory import scala.concurrent.Future @@ -139,7 +139,7 @@ class EntityDiscoveryManager(config: EntityDiscoveryManager.Configuration)( } ++ { if (config.crudEnabled) Map( - Crud.name -> new CrudSupportFactory(system, config, clientSettings) + ValueEntityProtocol.name -> new ValueEntitySupportFactory(system, config, clientSettings) ) else Map.empty } diff --git a/proxy/core/src/main/scala/io/cloudstate/proxy/crud/store/JdbcCrudStateTable.scala b/proxy/core/src/main/scala/io/cloudstate/proxy/crud/store/JdbcCrudStateTable.scala deleted file mode 100644 index 247107b73..000000000 --- a/proxy/core/src/main/scala/io/cloudstate/proxy/crud/store/JdbcCrudStateTable.scala +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright 2019 Lightbend Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.cloudstate.proxy.crud.store - -import io.cloudstate.proxy.crud.store.JdbcCrudStateTable.CrudStateRow -import io.cloudstate.proxy.crud.store.JdbcStore.Key -import slick.lifted.{MappedProjection, ProvenShape} - -object JdbcCrudStateTable { - - case class CrudStateRow(key: Key, state: Array[Byte]) -} - -trait JdbcCrudStateTable { - - val profile: slick.jdbc.JdbcProfile - - import profile.api._ - - def crudStateTableCfg: JdbcCrudStateTableConfiguration - - class CrudStateTable(tableTag: Tag) - extends Table[CrudStateRow](_tableTag = tableTag, - _schemaName = crudStateTableCfg.schemaName, - _tableName = crudStateTableCfg.tableName) { - def * : ProvenShape[CrudStateRow] = (key, state) <> (CrudStateRow.tupled, CrudStateRow.unapply) - - val persistentId: Rep[String] = - column[String](crudStateTableCfg.columnNames.persistentId, O.Length(255, varying = true)) - val entityId: Rep[String] = column[String](crudStateTableCfg.columnNames.entityId, O.Length(255, varying = true)) - val state: Rep[Array[Byte]] = column[Array[Byte]](crudStateTableCfg.columnNames.state) - val key: MappedProjection[Key, (String, String)] = (persistentId, entityId) <> (Key.tupled, Key.unapply) - val pk = primaryKey(s"${tableName}_pk", (persistentId, entityId)) - } - - lazy val CrudStateTableQuery = new TableQuery(tag => new CrudStateTable(tag)) -} diff --git a/proxy/core/src/main/scala/io/cloudstate/proxy/crud/DynamicLeastShardAllocationStrategy.scala b/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/DynamicLeastShardAllocationStrategy.scala similarity index 98% rename from proxy/core/src/main/scala/io/cloudstate/proxy/crud/DynamicLeastShardAllocationStrategy.scala rename to proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/DynamicLeastShardAllocationStrategy.scala index e98ba6b96..e82c62b68 100644 --- a/proxy/core/src/main/scala/io/cloudstate/proxy/crud/DynamicLeastShardAllocationStrategy.scala +++ b/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/DynamicLeastShardAllocationStrategy.scala @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.cloudstate.proxy.crud +package io.cloudstate.proxy.valueentity import akka.actor.ActorRef import akka.cluster.sharding.ShardCoordinator.ShardAllocationStrategy diff --git a/proxy/core/src/main/scala/io/cloudstate/proxy/crud/CrudEntity.scala b/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/ValueEntity.scala similarity index 65% rename from proxy/core/src/main/scala/io/cloudstate/proxy/crud/CrudEntity.scala rename to proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/ValueEntity.scala index a01607deb..33cedda90 100644 --- a/proxy/core/src/main/scala/io/cloudstate/proxy/crud/CrudEntity.scala +++ b/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/ValueEntity.scala @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.cloudstate.proxy.crud +package io.cloudstate.proxy.valueentity import java.net.URLDecoder import java.util.concurrent.atomic.AtomicLong @@ -26,33 +26,23 @@ import akka.pattern.pipe import akka.stream.scaladsl._ import akka.stream.{CompletionStrategy, Materializer, OverflowStrategy} import akka.util.Timeout -import io.cloudstate.protocol.crud.CrudAction.Action.{Delete, Update} -import io.cloudstate.protocol.crud.{ - CrudAction, - CrudClient, - CrudInit, - CrudInitState, - CrudReply, - CrudStreamIn, - CrudStreamOut, - CrudUpdate -} +import io.cloudstate.protocol.value_entity._ import io.cloudstate.protocol.entity._ -import io.cloudstate.proxy.crud.store.JdbcRepository -import io.cloudstate.proxy.crud.store.JdbcStore.Key +import io.cloudstate.proxy.valueentity.store.JdbcRepository +import io.cloudstate.proxy.valueentity.store.JdbcStore.Key import io.cloudstate.proxy.entity.{EntityCommand, UserFunctionReply} import scala.collection.immutable.Queue import scala.concurrent.Future -object CrudEntitySupervisor { +object ValueEntitySupervisor { private final case class Relay(actorRef: ActorRef) - def props(client: CrudClient, configuration: CrudEntity.Configuration, repository: JdbcRepository)( + def props(client: ValueEntityProtocolClient, configuration: ValueEntity.Configuration, repository: JdbcRepository)( implicit mat: Materializer ): Props = - Props(new CrudEntitySupervisor(client, configuration, repository)) + Props(new ValueEntitySupervisor(client, configuration, repository)) } /** @@ -65,13 +55,13 @@ object CrudEntitySupervisor { * persistence starts feeding us events. There's a race condition if we do this in the same persistent actor. This * establishes that connection first. */ -final class CrudEntitySupervisor(client: CrudClient, - configuration: CrudEntity.Configuration, - repository: JdbcRepository)(implicit mat: Materializer) +final class ValueEntitySupervisor(client: ValueEntityProtocolClient, + configuration: ValueEntity.Configuration, + repository: JdbcRepository)(implicit mat: Materializer) extends Actor with Stash { - import CrudEntitySupervisor._ + import ValueEntitySupervisor._ private var streamTerminated: Boolean = false @@ -81,13 +71,13 @@ final class CrudEntitySupervisor(client: CrudClient, client .handle( Source - .actorRef[CrudStreamIn](configuration.sendQueueSize, OverflowStrategy.fail) + .actorRef[ValueEntityStreamIn](configuration.sendQueueSize, OverflowStrategy.fail) .mapMaterializedValue { ref => self ! Relay(ref) NotUsed } ) - .runWith(Sink.actorRef(self, CrudEntity.StreamClosed, CrudEntity.StreamFailed.apply)) + .runWith(Sink.actorRef(self, ValueEntity.StreamClosed, ValueEntity.StreamFailed.apply)) context.become(waitingForRelay) } @@ -97,7 +87,7 @@ final class CrudEntitySupervisor(client: CrudClient, val entityId = URLDecoder.decode(self.path.name, "utf-8") val entity = context.watch( context - .actorOf(CrudEntity.props(configuration, entityId, relayRef, repository), "entity") + .actorOf(ValueEntity.props(configuration, entityId, relayRef, repository), "entity") ) context.become(forwarding(entity, relayRef)) unstashAll() @@ -116,11 +106,11 @@ final class CrudEntitySupervisor(client: CrudClient, case message if sender() == entity => context.parent ! message - case CrudEntity.StreamClosed => + case ValueEntity.StreamClosed => streamTerminated = true - entity forward CrudEntity.StreamClosed + entity forward ValueEntity.StreamClosed - case failed: CrudEntity.StreamFailed => + case failed: ValueEntity.StreamFailed => streamTerminated = true entity forward failed @@ -129,16 +119,16 @@ final class CrudEntitySupervisor(client: CrudClient, } private def stopping: Receive = { - case CrudEntity.StreamClosed => + case ValueEntity.StreamClosed => context.stop(self) - case _: CrudEntity.StreamFailed => + case _: ValueEntity.StreamFailed => context.stop(self) } override def supervisorStrategy: SupervisorStrategy = SupervisorStrategy.stoppingStrategy } -object CrudEntity { +object ValueEntity { final case object Stop @@ -166,7 +156,7 @@ object CrudEntity { private case class WriteStateFailure(cause: Throwable) extends DatabaseOperationWriteStatus final def props(configuration: Configuration, entityId: String, relay: ActorRef, repository: JdbcRepository): Props = - Props(new CrudEntity(configuration, entityId, relay, repository)) + Props(new ValueEntity(configuration, entityId, relay, repository)) /** * Used to ensure the action ids sent to the concurrency enforcer are indeed unique. @@ -175,10 +165,10 @@ object CrudEntity { } -final class CrudEntity(configuration: CrudEntity.Configuration, - entityId: String, - relay: ActorRef, - repository: JdbcRepository) +final class ValueEntity(configuration: ValueEntity.Configuration, + entityId: String, + relay: ActorRef, + repository: JdbcRepository) extends Actor with Stash with ActorLogging { @@ -187,10 +177,10 @@ final class CrudEntity(configuration: CrudEntity.Configuration, private val persistenceId: String = configuration.userFunctionName + entityId - private val actorId = CrudEntity.actorCounter.incrementAndGet() + private val actorId = ValueEntity.actorCounter.incrementAndGet() private[this] final var stashedCommands = Queue.empty[(EntityCommand, ActorRef)] // PERFORMANCE: look at options for data structures - private[this] final var currentCommand: CrudEntity.OutstandingCommand = null + private[this] final var currentCommand: ValueEntity.OutstandingCommand = null private[this] final var stopped = false private[this] final var idCounter = 0L private[this] final var inited = false @@ -204,21 +194,21 @@ final class CrudEntity(configuration: CrudEntity.Configuration, .get(Key(persistenceId, entityId)) .map { state => if (!inited) { - relay ! CrudStreamIn( - CrudStreamIn.Message.Init( - CrudInit( + relay ! ValueEntityStreamIn( + ValueEntityStreamIn.Message.Init( + ValueEntityInit( serviceName = configuration.serviceName, entityId = entityId, - state = Some(CrudInitState(state)) + state = Some(ValueEntityInitState(state)) ) ) ) inited = true } - CrudEntity.ReadStateSuccess(inited) + ValueEntity.ReadStateSuccess(inited) } .recover { - case error => CrudEntity.ReadStateFailure(error) + case error => ValueEntity.ReadStateFailure(error) } .pipeTo(self) @@ -243,7 +233,7 @@ final class CrudEntity(configuration: CrudEntity.Configuration, case null => case req => req.replyTo ! createFailure(msg) } - val errorNotification = createFailure("CRUD Entity terminated") + val errorNotification = createFailure("Value entity terminated") stashedCommands.foreach { case (_, replyTo) => replyTo ! errorNotification } @@ -263,12 +253,12 @@ final class CrudEntity(configuration: CrudEntity.Configuration, name = entityCommand.name, payload = entityCommand.payload ) - currentCommand = CrudEntity.OutstandingCommand(idCounter, actorId + ":" + entityId + ":" + idCounter, sender) + currentCommand = ValueEntity.OutstandingCommand(idCounter, actorId + ":" + entityId + ":" + idCounter, sender) commandStartTime = System.nanoTime() - relay ! CrudStreamIn(CrudStreamIn.Message.Command(command)) + relay ! ValueEntityStreamIn(ValueEntityStreamIn.Message.Command(command)) } - private final def esReplyToUfReply(reply: CrudReply) = + private final def esReplyToUfReply(reply: ValueEntityReply) = UserFunctionReply( clientAction = reply.clientAction, sideEffects = reply.sideEffects @@ -280,13 +270,13 @@ final class CrudEntity(configuration: CrudEntity.Configuration, ) override final def receive: Receive = { - case CrudEntity.ReadStateSuccess(initialize) => + case ValueEntity.ReadStateSuccess(initialize) => if (initialize) { context.become(running) unstashAll() } - case CrudEntity.ReadStateFailure(error) => + case ValueEntity.ReadStateFailure(error) => throw error case _ => stash() @@ -300,28 +290,28 @@ final class CrudEntity(configuration: CrudEntity.Configuration, case command: EntityCommand => handleCommand(command, sender()) - case CrudStreamOut(m, _) => - import CrudStreamOut.{Message => CrudSOMsg} + case ValueEntityStreamOut(m, _) => + import ValueEntityStreamOut.{Message => ValueEntitySOMsg} m match { - case CrudSOMsg.Reply(r) if currentCommand == null => - crash("Unexpected CRUD entity reply", s"(no current command) - $r") + case ValueEntitySOMsg.Reply(r) if currentCommand == null => + crash("Unexpected Value entity reply", s"(no current command) - $r") - case CrudSOMsg.Reply(r) if currentCommand.commandId != r.commandId => - crash("Unexpected CRUD entity reply", + case ValueEntitySOMsg.Reply(r) if currentCommand.commandId != r.commandId => + crash("Unexpected Value entity reply", s"(expected id ${currentCommand.commandId} but got ${r.commandId}) - $r") - case CrudSOMsg.Reply(r) => + case ValueEntitySOMsg.Reply(r) => val commandId = currentCommand.commandId - if (r.crudAction.isEmpty) { + if (r.stateAction.isEmpty) { currentCommand.replyTo ! esReplyToUfReply(r) commandHandled() } else { - r.crudAction.map { a => + r.stateAction.map { a => performAction(a) { _ => // Make sure that the current request is still ours if (currentCommand == null || currentCommand.commandId != commandId) { - crash("Unexpected CRUD entity behavior", "currentRequest changed before the state were persisted") + crash("Unexpected Value entity behavior", "currentRequest changed before the state were persisted") } currentCommand.replyTo ! esReplyToUfReply(r) commandHandled() @@ -329,64 +319,66 @@ final class CrudEntity(configuration: CrudEntity.Configuration, } } - case CrudSOMsg.Failure(f) if f.commandId == 0 => - crash("Unexpected CRUD entity failure", s"(not command specific) - ${f.description}") + case ValueEntitySOMsg.Failure(f) if f.commandId == 0 => + crash("Unexpected Value entity failure", s"(not command specific) - ${f.description}") - case CrudSOMsg.Failure(f) if currentCommand == null => - crash("Unexpected CRUD entity failure", s"(no current command) - ${f.description}") + case ValueEntitySOMsg.Failure(f) if currentCommand == null => + crash("Unexpected Value entity failure", s"(no current command) - ${f.description}") - case CrudSOMsg.Failure(f) if currentCommand.commandId != f.commandId => - crash("Unexpected CRUD entity failure", + case ValueEntitySOMsg.Failure(f) if currentCommand.commandId != f.commandId => + crash("Unexpected Value entity failure", s"(expected id ${currentCommand.commandId} but got ${f.commandId}) - ${f.description}") - case CrudSOMsg.Failure(f) => - try crash("Unexpected CRUD entity failure", f.description) + case ValueEntitySOMsg.Failure(f) => + try crash("Unexpected Value entity failure", f.description) finally currentCommand = null // clear command after notifications - case CrudSOMsg.Empty => + case ValueEntitySOMsg.Empty => // Either the reply/failure wasn't set, or its set to something unknown. // todo see if scalapb can give us unknown fields so we can possibly log more intelligently - crash("Unexpected CRUD entity failure", "empty or unknown message from entity output stream") + crash("Unexpected Value entity failure", "empty or unknown message from entity output stream") } - case CrudEntity.WriteStateSuccess => + case ValueEntity.WriteStateSuccess => // Nothing to do, database write access the native crud database was successful - case CrudEntity.WriteStateFailure(error) => - notifyOutstandingRequests("Unexpected CRUD entity failure") + case ValueEntity.WriteStateFailure(error) => + notifyOutstandingRequests("Unexpected Value entity failure") throw error - case CrudEntity.StreamClosed => - notifyOutstandingRequests("Unexpected CRUD entity termination") + case ValueEntity.StreamClosed => + notifyOutstandingRequests("Unexpected Value entity termination") context.stop(self) - case CrudEntity.StreamFailed(error) => - notifyOutstandingRequests("Unexpected CRUD entity termination") + case ValueEntity.StreamFailed(error) => + notifyOutstandingRequests("Unexpected Value entity termination") throw error - case CrudEntity.Stop => + case ValueEntity.Stop => stopped = true if (currentCommand == null) { context.stop(self) } case ReceiveTimeout => - context.parent ! ShardRegion.Passivate(stopMessage = CrudEntity.Stop) + context.parent ! ShardRegion.Passivate(stopMessage = ValueEntity.Stop) } private def performAction( - crudAction: CrudAction - )(handler: Unit => Unit): Future[CrudEntity.DatabaseOperationWriteStatus] = - crudAction.action match { - case Update(CrudUpdate(Some(value), _)) => + valueEntityAction: ValueEntityAction + )(handler: Unit => Unit): Future[ValueEntity.DatabaseOperationWriteStatus] = { + import ValueEntityAction.Action._ + + valueEntityAction.action match { + case Update(ValueEntityUpdate(Some(value), _)) => repository .update(Key(persistenceId, entityId), value) .map { _ => handler(()) - CrudEntity.WriteStateSuccess + ValueEntity.WriteStateSuccess } .recover { - case error => CrudEntity.WriteStateFailure(error) + case error => ValueEntity.WriteStateFailure(error) } case Delete(_) => @@ -394,10 +386,11 @@ final class CrudEntity(configuration: CrudEntity.Configuration, .delete(Key(persistenceId, entityId)) .map { _ => handler(()) - CrudEntity.WriteStateSuccess + ValueEntity.WriteStateSuccess } .recover { - case error => CrudEntity.WriteStateFailure(error) + case error => ValueEntity.WriteStateFailure(error) } } + } } diff --git a/proxy/core/src/main/scala/io/cloudstate/proxy/crud/CrudStoreSupportFactory.scala b/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/ValueEntitySupportFactory.scala similarity index 78% rename from proxy/core/src/main/scala/io/cloudstate/proxy/crud/CrudStoreSupportFactory.scala rename to proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/ValueEntitySupportFactory.scala index f3d872544..8c8c13c89 100644 --- a/proxy/core/src/main/scala/io/cloudstate/proxy/crud/CrudStoreSupportFactory.scala +++ b/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/ValueEntitySupportFactory.scala @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.cloudstate.proxy.crud +package io.cloudstate.proxy.valueentity import akka.NotUsed import akka.actor.{ActorRef, ActorSystem} @@ -26,33 +26,35 @@ import akka.stream.Materializer import akka.stream.scaladsl.Flow import akka.util.Timeout import com.google.protobuf.Descriptors.ServiceDescriptor -import io.cloudstate.protocol.crud.CrudClient +import io.cloudstate.protocol.value_entity.ValueEntityProtocolClient import io.cloudstate.protocol.entity.{Entity, Metadata} import io.cloudstate.proxy._ -import io.cloudstate.proxy.crud.store.{JdbcRepositoryImpl, JdbcStoreSupport} +import io.cloudstate.proxy.valueentity.store.{JdbcRepositoryImpl, JdbcStoreSupport} import io.cloudstate.proxy.entity.{EntityCommand, UserFunctionReply} import scala.concurrent.{ExecutionContext, Future} -class CrudSupportFactory(system: ActorSystem, - config: EntityDiscoveryManager.Configuration, - grpcClientSettings: GrpcClientSettings)(implicit ec: ExecutionContext, mat: Materializer) +class ValueEntitySupportFactory( + system: ActorSystem, + config: EntityDiscoveryManager.Configuration, + grpcClientSettings: GrpcClientSettings +)(implicit ec: ExecutionContext, mat: Materializer) extends EntityTypeSupportFactory with JdbcStoreSupport { private final val log = Logging.getLogger(system, this.getClass) - private val crudClient = CrudClient(grpcClientSettings)(system) + private val valueEntityClient = ValueEntityProtocolClient(grpcClientSettings)(system) override def buildEntityTypeSupport(entity: Entity, serviceDescriptor: ServiceDescriptor, methodDescriptors: Map[String, EntityMethodDescriptor]): EntityTypeSupport = { validate(serviceDescriptor, methodDescriptors) - val stateManagerConfig = CrudEntity.Configuration(entity.serviceName, - entity.persistenceId, - config.passivationTimeout, - config.relayOutputBufferSize) + val stateManagerConfig = ValueEntity.Configuration(entity.serviceName, + entity.persistenceId, + config.passivationTimeout, + config.relayOutputBufferSize) val repository = new JdbcRepositoryImpl(createStore(config.config)) @@ -61,14 +63,14 @@ class CrudSupportFactory(system: ActorSystem, val clusterShardingSettings = ClusterShardingSettings(system) val crudEntity = clusterSharding.start( typeName = entity.persistenceId, - entityProps = CrudEntitySupervisor.props(crudClient, stateManagerConfig, repository), + entityProps = ValueEntitySupervisor.props(valueEntityClient, stateManagerConfig, repository), settings = clusterShardingSettings, messageExtractor = new CrudEntityIdExtractor(config.numberOfShards), allocationStrategy = new DynamicLeastShardAllocationStrategy(1, 10, 2, 0.0), - handOffStopMessage = CrudEntity.Stop + handOffStopMessage = ValueEntity.Stop ) - new CrudSupport(crudEntity, config.proxyParallelism, config.relayTimeout) + new ValueEntitySupport(crudEntity, config.proxyParallelism, config.relayTimeout) } private def validate(serviceDescriptor: ServiceDescriptor, @@ -93,7 +95,7 @@ class CrudSupportFactory(system: ActorSystem, } } -private class CrudSupport(crudEntity: ActorRef, parallelism: Int, private implicit val relayTimeout: Timeout) +private class ValueEntitySupport(crudEntity: ActorRef, parallelism: Int, private implicit val relayTimeout: Timeout) extends EntityTypeSupport { import akka.pattern.ask diff --git a/proxy/core/src/main/scala/io/cloudstate/proxy/crud/store/JdbcConfig.scala b/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/JdbcConfig.scala similarity index 88% rename from proxy/core/src/main/scala/io/cloudstate/proxy/crud/store/JdbcConfig.scala rename to proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/JdbcConfig.scala index 8d0f552c7..5909a8538 100644 --- a/proxy/core/src/main/scala/io/cloudstate/proxy/crud/store/JdbcConfig.scala +++ b/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/JdbcConfig.scala @@ -14,14 +14,14 @@ * limitations under the License. */ -package io.cloudstate.proxy.crud.store +package io.cloudstate.proxy.valueentity.store import com.typesafe.config.Config import slick.basic.DatabaseConfig import slick.jdbc.JdbcBackend.Database import slick.jdbc.{JdbcBackend, JdbcProfile} -class JdbcCrudStateTableColumnNames(config: Config) { +class JdbcValueEntityTableColumnNames(config: Config) { private val cfg = config.getConfig("tables.state.columnNames") val persistentId: String = cfg.getString("persistentId") @@ -31,7 +31,7 @@ class JdbcCrudStateTableColumnNames(config: Config) { override def toString: String = s"JdbcCrudStateTableColumnNames($persistentId,$entityId,$state)" } -class JdbcCrudStateTableConfiguration(config: Config) { +class JdbcValueEntityTableConfiguration(config: Config) { private val cfg = config.getConfig("tables.state") val tableName: String = cfg.getString("tableName") @@ -39,7 +39,7 @@ class JdbcCrudStateTableConfiguration(config: Config) { case "" => None case schema => Some(schema.trim) } - val columnNames: JdbcCrudStateTableColumnNames = new JdbcCrudStateTableColumnNames(config) + val columnNames: JdbcValueEntityTableColumnNames = new JdbcValueEntityTableColumnNames(config) override def toString: String = s"JdbcCrudStateTableConfiguration($tableName,$schemaName,$columnNames)" } diff --git a/proxy/core/src/main/scala/io/cloudstate/proxy/crud/store/JdbcInMemoryStore.scala b/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/JdbcInMemoryStore.scala similarity index 91% rename from proxy/core/src/main/scala/io/cloudstate/proxy/crud/store/JdbcInMemoryStore.scala rename to proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/JdbcInMemoryStore.scala index e26d8d8e6..d665ac2f8 100644 --- a/proxy/core/src/main/scala/io/cloudstate/proxy/crud/store/JdbcInMemoryStore.scala +++ b/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/JdbcInMemoryStore.scala @@ -14,10 +14,10 @@ * limitations under the License. */ -package io.cloudstate.proxy.crud.store +package io.cloudstate.proxy.valueentity.store import akka.util.ByteString -import io.cloudstate.proxy.crud.store.JdbcStore.Key +import io.cloudstate.proxy.valueentity.store.JdbcStore.Key import scala.concurrent.Future diff --git a/proxy/core/src/main/scala/io/cloudstate/proxy/crud/store/JdbcRepository.scala b/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/JdbcRepository.scala similarity index 96% rename from proxy/core/src/main/scala/io/cloudstate/proxy/crud/store/JdbcRepository.scala rename to proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/JdbcRepository.scala index e67c4b156..52114edbe 100644 --- a/proxy/core/src/main/scala/io/cloudstate/proxy/crud/store/JdbcRepository.scala +++ b/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/JdbcRepository.scala @@ -14,12 +14,12 @@ * limitations under the License. */ -package io.cloudstate.proxy.crud.store +package io.cloudstate.proxy.valueentity.store import akka.grpc.ProtobufSerializer import akka.util.ByteString import com.google.protobuf.any.{Any => ScalaPbAny} -import io.cloudstate.proxy.crud.store.JdbcStore.Key +import io.cloudstate.proxy.valueentity.store.JdbcStore.Key import scala.concurrent.{ExecutionContext, Future} diff --git a/proxy/core/src/main/scala/io/cloudstate/proxy/crud/store/JdbcStore.scala b/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/JdbcStore.scala similarity index 84% rename from proxy/core/src/main/scala/io/cloudstate/proxy/crud/store/JdbcStore.scala rename to proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/JdbcStore.scala index 00a910710..6a6cd2ed3 100644 --- a/proxy/core/src/main/scala/io/cloudstate/proxy/crud/store/JdbcStore.scala +++ b/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/JdbcStore.scala @@ -14,11 +14,11 @@ * limitations under the License. */ -package io.cloudstate.proxy.crud.store +package io.cloudstate.proxy.valueentity.store import akka.util.ByteString -import io.cloudstate.proxy.crud.store.JdbcCrudStateTable.CrudStateRow -import io.cloudstate.proxy.crud.store.JdbcStore.Key +import io.cloudstate.proxy.valueentity.store.JdbcValueEntityTable.ValueEntityRow +import io.cloudstate.proxy.valueentity.store.JdbcStore.Key import scala.concurrent.{ExecutionContext, Future} @@ -62,7 +62,7 @@ trait JdbcStore[K, V] { } -final class JdbcStoreImpl(slickDatabase: JdbcSlickDatabase, queries: JdbcCrudStateQueries)( +private[store] final class JdbcStoreImpl(slickDatabase: JdbcSlickDatabase, queries: JdbcValueEntityQueries)( implicit ec: ExecutionContext ) extends JdbcStore[Key, ByteString] { @@ -77,7 +77,7 @@ final class JdbcStoreImpl(slickDatabase: JdbcSlickDatabase, queries: JdbcCrudSta override def update(key: Key, value: ByteString): Future[Unit] = for { - _ <- db.run(queries.insertOrUpdate(CrudStateRow(key, value.toByteBuffer.array()))) + _ <- db.run(queries.insertOrUpdate(ValueEntityRow(key, value.toByteBuffer.array()))) } yield () override def delete(key: Key): Future[Unit] = diff --git a/proxy/core/src/main/scala/io/cloudstate/proxy/crud/store/JdbcStoreSupport.scala b/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/JdbcStoreSupport.scala similarity index 81% rename from proxy/core/src/main/scala/io/cloudstate/proxy/crud/store/JdbcStoreSupport.scala rename to proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/JdbcStoreSupport.scala index d3f50e556..02121ca1e 100644 --- a/proxy/core/src/main/scala/io/cloudstate/proxy/crud/store/JdbcStoreSupport.scala +++ b/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/JdbcStoreSupport.scala @@ -14,12 +14,12 @@ * limitations under the License. */ -package io.cloudstate.proxy.crud.store +package io.cloudstate.proxy.valueentity.store import akka.util.ByteString import com.typesafe.config.Config -import io.cloudstate.proxy.crud.store.JdbcStore.Key -import io.cloudstate.proxy.crud.store.JdbcStoreSupport.{IN_MEMORY, JDBC} +import io.cloudstate.proxy.valueentity.store.JdbcStore.Key +import io.cloudstate.proxy.valueentity.store.JdbcStoreSupport.{IN_MEMORY, JDBC} import scala.concurrent.ExecutionContext; @@ -40,10 +40,10 @@ trait JdbcStoreSupport { private def createJdbcStore(config: Config)(implicit ec: ExecutionContext): JdbcStore[Key, ByteString] = { val slickDatabase = JdbcSlickDatabase(config) - val tableConfiguration = new JdbcCrudStateTableConfiguration( + val tableConfiguration = new JdbcValueEntityTableConfiguration( config.getConfig("crud.jdbc-state-store") ) - val queries = new JdbcCrudStateQueries(slickDatabase.profile, tableConfiguration) + val queries = new JdbcValueEntityQueries(slickDatabase.profile, tableConfiguration) new JdbcStoreImpl(slickDatabase, queries) } diff --git a/proxy/core/src/main/scala/io/cloudstate/proxy/crud/store/JdbcCrudStateQueries.scala b/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/JdbcValueEntityQueries.scala similarity index 59% rename from proxy/core/src/main/scala/io/cloudstate/proxy/crud/store/JdbcCrudStateQueries.scala rename to proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/JdbcValueEntityQueries.scala index 877f68a51..0b3ed08d2 100644 --- a/proxy/core/src/main/scala/io/cloudstate/proxy/crud/store/JdbcCrudStateQueries.scala +++ b/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/JdbcValueEntityQueries.scala @@ -14,27 +14,28 @@ * limitations under the License. */ -package io.cloudstate.proxy.crud.store +package io.cloudstate.proxy.valueentity.store -import io.cloudstate.proxy.crud.store.JdbcCrudStateTable.CrudStateRow -import io.cloudstate.proxy.crud.store.JdbcStore.Key +import io.cloudstate.proxy.valueentity.store.JdbcValueEntityTable.ValueEntityRow +import io.cloudstate.proxy.valueentity.store.JdbcStore.Key import slick.jdbc.JdbcProfile -class JdbcCrudStateQueries(val profile: JdbcProfile, override val crudStateTableCfg: JdbcCrudStateTableConfiguration) - extends JdbcCrudStateTable { +private[store] class JdbcValueEntityQueries(val profile: JdbcProfile, + override val valueEntityTableCfg: JdbcValueEntityTableConfiguration) + extends JdbcValueEntityTable { import profile.api._ - def selectByKey(key: Key): Query[CrudStateTable, CrudStateRow, Seq] = - CrudStateTableQuery + def selectByKey(key: Key): Query[ValueEntityTable, ValueEntityRow, Seq] = + ValueEntityTableQuery .filter(_.persistentId === key.persistentId) .filter(_.entityId === key.entityId) .take(1) - def insertOrUpdate(crudState: CrudStateRow) = CrudStateTableQuery.insertOrUpdate(crudState) + def insertOrUpdate(crudState: ValueEntityRow) = ValueEntityTableQuery.insertOrUpdate(crudState) def deleteByKey(key: Key) = - CrudStateTableQuery + ValueEntityTableQuery .filter(_.persistentId === key.persistentId) .filter(_.entityId === key.entityId) .delete diff --git a/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/JdbcValueEntityTable.scala b/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/JdbcValueEntityTable.scala new file mode 100644 index 000000000..190f95786 --- /dev/null +++ b/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/JdbcValueEntityTable.scala @@ -0,0 +1,51 @@ +/* + * Copyright 2019 Lightbend Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.cloudstate.proxy.valueentity.store + +import io.cloudstate.proxy.valueentity.store.JdbcValueEntityTable.ValueEntityRow +import io.cloudstate.proxy.valueentity.store.JdbcStore.Key +import slick.lifted.{MappedProjection, ProvenShape} + +object JdbcValueEntityTable { + + case class ValueEntityRow(key: Key, state: Array[Byte]) +} + +trait JdbcValueEntityTable { + + val profile: slick.jdbc.JdbcProfile + + import profile.api._ + + def valueEntityTableCfg: JdbcValueEntityTableConfiguration + + class ValueEntityTable(tableTag: Tag) + extends Table[ValueEntityRow](_tableTag = tableTag, + _schemaName = valueEntityTableCfg.schemaName, + _tableName = valueEntityTableCfg.tableName) { + def * : ProvenShape[ValueEntityRow] = (key, state) <> (ValueEntityRow.tupled, ValueEntityRow.unapply) + + val persistentId: Rep[String] = + column[String](valueEntityTableCfg.columnNames.persistentId, O.Length(255, varying = true)) + val entityId: Rep[String] = column[String](valueEntityTableCfg.columnNames.entityId, O.Length(255, varying = true)) + val state: Rep[Array[Byte]] = column[Array[Byte]](valueEntityTableCfg.columnNames.state) + val key: MappedProjection[Key, (String, String)] = (persistentId, entityId) <> (Key.tupled, Key.unapply) + val pk = primaryKey(s"${tableName}_pk", (persistentId, entityId)) + } + + lazy val ValueEntityTableQuery = new TableQuery(tag => new ValueEntityTable(tag)) +} diff --git a/proxy/core/src/main/scala/io/cloudstate/proxy/crud/store/package-info.java b/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/package-info.java similarity index 72% rename from proxy/core/src/main/scala/io/cloudstate/proxy/crud/store/package-info.java rename to proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/package-info.java index 34ece9716..1ddfe28a5 100644 --- a/proxy/core/src/main/scala/io/cloudstate/proxy/crud/store/package-info.java +++ b/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/package-info.java @@ -1,4 +1,4 @@ /** * Most part of the code in this package has been copied/adapted from https://github.com/akka/akka-persistence-jdbc */ -package io.cloudstate.proxy.crud.store; +package io.cloudstate.proxy.valueentity.store; diff --git a/proxy/core/src/test/scala/io/cloudstate/proxy/TestProxy.scala b/proxy/core/src/test/scala/io/cloudstate/proxy/TestProxy.scala index ea3d035a0..db7668651 100644 --- a/proxy/core/src/test/scala/io/cloudstate/proxy/TestProxy.scala +++ b/proxy/core/src/test/scala/io/cloudstate/proxy/TestProxy.scala @@ -27,7 +27,7 @@ import io.cloudstate.protocol.action.ActionProtocol import io.cloudstate.testkit.Sockets import java.net.{ConnectException, Socket} -import io.cloudstate.protocol.crud.Crud +import io.cloudstate.protocol.value_entity.ValueEntityProtocol import scala.concurrent.duration._ @@ -54,7 +54,7 @@ class TestProxy(servicePort: Int) { """)) val info: ProxyInfo = - EntityDiscoveryManager.proxyInfo(Seq(Crdt.name, ActionProtocol.name, EventSourced.name, Crud.name)) + EntityDiscoveryManager.proxyInfo(Seq(Crdt.name, ActionProtocol.name, EventSourced.name, ValueEntityProtocol.name)) val system: ActorSystem = CloudStateProxyMain.start(config) diff --git a/proxy/core/src/test/scala/io/cloudstate/proxy/crud/DatabaseExceptionHandlingSpec.scala b/proxy/core/src/test/scala/io/cloudstate/proxy/valueentity/DatabaseExceptionHandlingSpec.scala similarity index 80% rename from proxy/core/src/test/scala/io/cloudstate/proxy/crud/DatabaseExceptionHandlingSpec.scala rename to proxy/core/src/test/scala/io/cloudstate/proxy/valueentity/DatabaseExceptionHandlingSpec.scala index fe5f162d5..81564e7f2 100644 --- a/proxy/core/src/test/scala/io/cloudstate/proxy/crud/DatabaseExceptionHandlingSpec.scala +++ b/proxy/core/src/test/scala/io/cloudstate/proxy/valueentity/DatabaseExceptionHandlingSpec.scala @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.cloudstate.proxy.crud +package io.cloudstate.proxy.valueentity import akka.actor.{Actor, ActorRef, ActorSystem} import akka.grpc.GrpcClientSettings @@ -22,14 +22,14 @@ import akka.testkit.TestEvent.Mute import akka.testkit.{EventFilter, TestActorRef} import akka.util.ByteString import com.google.protobuf.any.{Any => ScalaPbAny} -import io.cloudstate.protocol.crud.CrudClient +import io.cloudstate.protocol.value_entity.ValueEntityProtocolClient import com.google.protobuf.{ByteString => PbByteString} -import io.cloudstate.proxy.crud.store.{JdbcRepositoryImpl, JdbcStore} -import io.cloudstate.proxy.crud.store.JdbcStore.Key +import io.cloudstate.proxy.valueentity.store.{JdbcRepositoryImpl, JdbcStore} +import io.cloudstate.proxy.valueentity.store.JdbcStore.Key import io.cloudstate.proxy.entity.{EntityCommand, UserFunctionReply} import io.cloudstate.proxy.telemetry.AbstractTelemetrySpec import io.cloudstate.testkit.TestService -import io.cloudstate.testkit.crud.CrudMessages +import io.cloudstate.testkit.valuentity.ValueEntityMessages import scala.concurrent.Future import scala.concurrent.duration._ @@ -46,14 +46,14 @@ class DatabaseExceptionHandlingSpec extends AbstractTelemetrySpec { | } """ private val service = TestService() - private val entityConfiguration = CrudEntity.Configuration( + private val entityConfiguration = ValueEntity.Configuration( serviceName = "service", userFunctionName = "test", passivationTimeout = 30.seconds, sendQueueSize = 100 ) - "The CrudEntity" should { + "The ValueEntity" should { "crash entity on init when loading state failures" in withTestKit(testkitConfig) { testKit => import testKit._ @@ -61,54 +61,57 @@ class DatabaseExceptionHandlingSpec extends AbstractTelemetrySpec { silentDeadLettersAndUnhandledMessages - val client = CrudClient(GrpcClientSettings.connectToServiceAt("localhost", service.port).withTls(false)) + val client = + ValueEntityProtocolClient(GrpcClientSettings.connectToServiceAt("localhost", service.port).withTls(false)) val repository = new JdbcRepositoryImpl(TestJdbcStore.storeWithGetFailure()) - val entity = watch(system.actorOf(CrudEntitySupervisor.props(client, entityConfiguration, repository), "entity")) + val entity = watch(system.actorOf(ValueEntitySupervisor.props(client, entityConfiguration, repository), "entity")) - val connection = service.crud.expectConnection() + val connection = service.valueEntity.expectConnection() connection.expectClosed() } "crash entity on update state failures" in withTestKit(testkitConfig) { testKit => import testKit._ - import CrudMessages._ + import ValueEntityMessages._ import system.dispatcher silentDeadLettersAndUnhandledMessages val forwardReply = forwardReplyActor(testActor) - val client = CrudClient(GrpcClientSettings.connectToServiceAt("localhost", service.port).withTls(false)) + val client = + ValueEntityProtocolClient(GrpcClientSettings.connectToServiceAt("localhost", service.port).withTls(false)) val repository = new JdbcRepositoryImpl(TestJdbcStore.storeWithUpdateFailure()) - val entity = watch(system.actorOf(CrudEntitySupervisor.props(client, entityConfiguration, repository), "entity")) + val entity = watch(system.actorOf(ValueEntitySupervisor.props(client, entityConfiguration, repository), "entity")) val emptyCommand = Some(protobufAny(EmptyJavaMessage)) - val connection = service.crud.expectConnection() + val connection = service.valueEntity.expectConnection() connection.expect(init("service", "entity")) entity.tell(EntityCommand(entityId = "test", name = "command1", emptyCommand), forwardReply) connection.expect(command(1, "entity", "command1")) val state = ScalaPbAny("state", PbByteString.copyFromUtf8("state")) connection.send(reply(1, EmptyJavaMessage, update(state))) - expectMsg(UserFunctionReply(clientActionFailure("Unexpected CRUD entity failure"))) + expectMsg(UserFunctionReply(clientActionFailure("Unexpected Value entity failure"))) connection.expectClosed() } "crash entity on delete state failures" in withTestKit(testkitConfig) { testKit => import testKit._ - import CrudMessages._ + import ValueEntityMessages._ import system.dispatcher silentDeadLettersAndUnhandledMessages val forwardReply = forwardReplyActor(testActor) - val client = CrudClient(GrpcClientSettings.connectToServiceAt("localhost", service.port).withTls(false)) + val client = + ValueEntityProtocolClient(GrpcClientSettings.connectToServiceAt("localhost", service.port).withTls(false)) val repository = new JdbcRepositoryImpl(TestJdbcStore.storeWithDeleteFailure()) - val entity = watch(system.actorOf(CrudEntitySupervisor.props(client, entityConfiguration, repository), "entity")) + val entity = watch(system.actorOf(ValueEntitySupervisor.props(client, entityConfiguration, repository), "entity")) val emptyCommand = Some(protobufAny(EmptyJavaMessage)) - val connection = service.crud.expectConnection() + val connection = service.valueEntity.expectConnection() connection.expect(init("service", "entity")) entity.tell(EntityCommand(entityId = "test", name = "command1", emptyCommand), forwardReply) @@ -119,7 +122,7 @@ class DatabaseExceptionHandlingSpec extends AbstractTelemetrySpec { entity.tell(EntityCommand(entityId = "test", name = "command2", emptyCommand), forwardReply) connection.expect(command(2, "entity", "command2")) connection.send(reply(2, EmptyJavaMessage, delete())) - expectMsg(UserFunctionReply(clientActionFailure("Unexpected CRUD entity failure"))) + expectMsg(UserFunctionReply(clientActionFailure("Unexpected Value entity failure"))) connection.expectClosed() } diff --git a/proxy/core/src/test/scala/io/cloudstate/proxy/crud/ExceptionHandlingSpec.scala b/proxy/core/src/test/scala/io/cloudstate/proxy/valueentity/ExceptionHandlingSpec.scala similarity index 76% rename from proxy/core/src/test/scala/io/cloudstate/proxy/crud/ExceptionHandlingSpec.scala rename to proxy/core/src/test/scala/io/cloudstate/proxy/valueentity/ExceptionHandlingSpec.scala index be5e35a15..7b97c15ef 100644 --- a/proxy/core/src/test/scala/io/cloudstate/proxy/crud/ExceptionHandlingSpec.scala +++ b/proxy/core/src/test/scala/io/cloudstate/proxy/valueentity/ExceptionHandlingSpec.scala @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.cloudstate.proxy.crud +package io.cloudstate.proxy.valueentity import akka.Done import akka.actor.ActorSystem @@ -26,23 +26,23 @@ import akka.testkit.TestKit import io.cloudstate.proxy.TestProxy import org.scalatest.{BeforeAndAfterAll, Matchers, WordSpec} import org.scalatest.concurrent.ScalaFutures -import io.cloudstate.testkit.crud.{CrudMessages, TestCrudService} +import io.cloudstate.testkit.valuentity.{TestValueEntityService, ValueEntityMessages} import io.cloudstate.proxy.test.thing.{Key, Thing, ThingClient} import io.cloudstate.testkit.TestService import io.grpc.{Status, StatusRuntimeException} class ExceptionHandlingSpec extends WordSpec with Matchers with BeforeAndAfterAll with ScalaFutures { - import CrudMessages._ + import ValueEntityMessages._ - implicit val system = ActorSystem("CrudExceptionHandlingSpec") + private implicit val system = ActorSystem("ValueEntityExceptionHandlingSpec") - val service = TestService() - val proxy = TestProxy(service.port) - val client = ThingClient(GrpcClientSettings.connectToServiceAt("localhost", proxy.port).withTls(false)) - val spec = TestCrudService.entitySpec(Thing) + private val service = TestService() + private val proxy = TestProxy(service.port) + private val client = ThingClient(GrpcClientSettings.connectToServiceAt("localhost", proxy.port).withTls(false)) + private val spec = TestValueEntityService.entitySpec(Thing) - val discovery = service.entityDiscovery.expectDiscovery() + private val discovery = service.entityDiscovery.expectDiscovery() discovery.expect(proxy.info) discovery.send(spec) proxy.expectOnline() @@ -54,11 +54,11 @@ class ExceptionHandlingSpec extends WordSpec with Matchers with BeforeAndAfterAl service.terminate() } - "Cloudstate proxy for CRUD" should { + "Cloudstate proxy for Value Entity" should { "respond with gRPC error for action failure in entity" in { val call = client.get(Key("one")) - val connection = service.crud.expectConnection() + val connection = service.valueEntity.expectConnection() connection.expect(init(Thing.name, "one")) connection.expect(command(1, "one", "Get", Key("one"))) proxy.expectLogError("User Function responded with a failure: description goes here") { @@ -72,38 +72,38 @@ class ExceptionHandlingSpec extends WordSpec with Matchers with BeforeAndAfterAl "respond with gRPC error for unexpected failure in entity" in { val call = client.get(Key("two")) - val connection = service.crud.expectConnection() + val connection = service.valueEntity.expectConnection() connection.expect(init(Thing.name, "two")) connection.expect(command(1, "two", "Get", Key("two"))) - proxy.expectLogError("User Function responded with a failure: Unexpected CRUD entity failure") { - proxy.expectLogError("Unexpected CRUD entity failure - boom plus details") { + proxy.expectLogError("User Function responded with a failure: Unexpected Value entity failure") { + proxy.expectLogError("Unexpected Value entity failure - boom plus details") { connection.send(failure(1, "boom plus details")) connection.expectClosed() } } val error = call.failed.futureValue error shouldBe a[StatusRuntimeException] - error.getMessage shouldBe "UNKNOWN: Unexpected CRUD entity failure" + error.getMessage shouldBe "UNKNOWN: Unexpected Value entity failure" } "respond with gRPC error for stream error in entity" in { val call = client.get(Key("three")) - val connection = service.crud.expectConnection() + val connection = service.valueEntity.expectConnection() connection.expect(init(Thing.name, "three")) connection.expect(command(1, "three", "Get", Key("three"))) - proxy.expectLogError("User Function responded with a failure: Unexpected CRUD entity termination") { + proxy.expectLogError("User Function responded with a failure: Unexpected Value entity termination") { proxy.expectLogError("INTERNAL: stream failed") { connection.sendError(new GrpcServiceException(Status.INTERNAL.withDescription("stream failed"))) } } val error = call.failed.futureValue error shouldBe a[StatusRuntimeException] - error.getMessage shouldBe "UNKNOWN: Unexpected CRUD entity termination" + error.getMessage shouldBe "UNKNOWN: Unexpected Value entity termination" } "respond with HTTP error for action failure in entity" in { val call = Http().singleRequest(HttpRequest(uri = Uri(s"http://localhost:${proxy.port}/thing/four"))) - val connection = service.crud.expectConnection() + val connection = service.valueEntity.expectConnection() connection.expect(init(Thing.name, "four")) connection.expect(command(1, "four", "Get", Key("four"))) proxy.expectLogError("User Function responded with a failure: description goes here") { @@ -117,33 +117,33 @@ class ExceptionHandlingSpec extends WordSpec with Matchers with BeforeAndAfterAl "respond with HTTP error for unexpected failure in entity" in { val call = Http().singleRequest(HttpRequest(uri = Uri(s"http://localhost:${proxy.port}/thing/five"))) - val connection = service.crud.expectConnection() + val connection = service.valueEntity.expectConnection() connection.expect(init(Thing.name, "five")) connection.expect(command(1, "five", "Get", Key("five"))) - proxy.expectLogError("User Function responded with a failure: Unexpected CRUD entity failure") { - proxy.expectLogError("Unexpected CRUD entity failure - boom plus details") { + proxy.expectLogError("User Function responded with a failure: Unexpected Value entity failure") { + proxy.expectLogError("Unexpected Value entity failure - boom plus details") { connection.send(failure(1, "boom plus details")) connection.expectClosed() } } val response = call.futureValue response.status.intValue shouldBe 500 - Unmarshal(response).to[String].futureValue shouldBe "Unexpected CRUD entity failure" + Unmarshal(response).to[String].futureValue shouldBe "Unexpected Value entity failure" } "respond with HTTP error for stream error in entity" in { val call = Http().singleRequest(HttpRequest(uri = Uri(s"http://localhost:${proxy.port}/thing/six"))) - val connection = service.crud.expectConnection() + val connection = service.valueEntity.expectConnection() connection.expect(init(Thing.name, "six")) connection.expect(command(1, "six", "Get", Key("six"))) - proxy.expectLogError("User Function responded with a failure: Unexpected CRUD entity termination") { + proxy.expectLogError("User Function responded with a failure: Unexpected Value entity termination") { proxy.expectLogError("INTERNAL: stream failed") { connection.sendError(new GrpcServiceException(Status.INTERNAL.withDescription("stream failed"))) } } val response = call.futureValue response.status.intValue shouldBe 500 - Unmarshal(response).to[String].futureValue shouldBe "Unexpected CRUD entity termination" + Unmarshal(response).to[String].futureValue shouldBe "Unexpected Value entity termination" } } } diff --git a/proxy/jdbc/src/main/resources/jdbc-common.conf b/proxy/jdbc/src/main/resources/jdbc-common.conf index 4597dd838..edc699f81 100644 --- a/proxy/jdbc/src/main/resources/jdbc-common.conf +++ b/proxy/jdbc/src/main/resources/jdbc-common.conf @@ -8,7 +8,7 @@ cloudstate.proxy { akka { management.health-checks.readiness-checks { cloudstate-jdbc = "io.cloudstate.proxy.jdbc.SlickEnsureTablesExistReadyCheck" - cloudstate-crud-jdbc = "io.cloudstate.proxy.jdbc.SlickEnsureCrudTablesExistReadyCheck" + cloudstate-crud-jdbc = "io.cloudstate.proxy.jdbc.SlickEnsureValueEntityTablesExistReadyCheck" } persistence { diff --git a/proxy/jdbc/src/main/scala/io/cloudstate/proxy/jdbc/CloudStateJdbcProxyMain.scala b/proxy/jdbc/src/main/scala/io/cloudstate/proxy/jdbc/CloudStateJdbcProxyMain.scala index 33e9675eb..342292efe 100644 --- a/proxy/jdbc/src/main/scala/io/cloudstate/proxy/jdbc/CloudStateJdbcProxyMain.scala +++ b/proxy/jdbc/src/main/scala/io/cloudstate/proxy/jdbc/CloudStateJdbcProxyMain.scala @@ -30,7 +30,7 @@ object CloudStateJdbcProxyMain { val config = new CloudStateProxyMain.Configuration(actorSystem.settings.config.getConfig("cloudstate.proxy")) if (config.devMode) { new SlickEnsureTablesExistReadyCheck(actorSystem) - new SlickEnsureCrudTablesExistReadyCheck(actorSystem) + new SlickEnsureValueEntityTablesExistReadyCheck(actorSystem) } } diff --git a/proxy/jdbc/src/main/scala/io/cloudstate/proxy/jdbc/SlickEnsureCrudTablesExistReadyCheck.scala b/proxy/jdbc/src/main/scala/io/cloudstate/proxy/jdbc/SlickEnsureValueEntityTablesExistReadyCheck.scala similarity index 89% rename from proxy/jdbc/src/main/scala/io/cloudstate/proxy/jdbc/SlickEnsureCrudTablesExistReadyCheck.scala rename to proxy/jdbc/src/main/scala/io/cloudstate/proxy/jdbc/SlickEnsureValueEntityTablesExistReadyCheck.scala index eb22e350d..ef6f24e23 100644 --- a/proxy/jdbc/src/main/scala/io/cloudstate/proxy/jdbc/SlickEnsureCrudTablesExistReadyCheck.scala +++ b/proxy/jdbc/src/main/scala/io/cloudstate/proxy/jdbc/SlickEnsureValueEntityTablesExistReadyCheck.scala @@ -21,22 +21,20 @@ import java.sql.Connection import akka.Done import akka.actor.{Actor, ActorLogging, ActorSystem, Props, Status} import akka.pattern.{BackoffOpts, BackoffSupervisor} -import akka.persistence.jdbc.config.{ConfigKeys, JournalTableConfiguration, SnapshotTableConfiguration} -import akka.persistence.jdbc.journal.dao.JournalTables -import akka.persistence.jdbc.snapshot.dao.SnapshotTables -import akka.persistence.jdbc.util.{SlickDatabase, SlickExtension} import akka.util.Timeout -import com.typesafe.config.ConfigFactory -import io.cloudstate.proxy.crud.store.{JdbcCrudStateTable, JdbcCrudStateTableConfiguration, JdbcSlickDatabase} +import io.cloudstate.proxy.valueentity.store.{ + JdbcSlickDatabase, + JdbcValueEntityTable, + JdbcValueEntityTableConfiguration +} import slick.jdbc.{H2Profile, JdbcProfile, MySQLProfile, PostgresProfile} import slick.jdbc.meta.MTable -import scala.collection.JavaConverters._ import scala.concurrent.Future import scala.concurrent.duration._ import scala.util.{Failure, Success, Try} -class SlickEnsureCrudTablesExistReadyCheck(system: ActorSystem) extends (() => Future[Boolean]) { +class SlickEnsureValueEntityTablesExistReadyCheck(system: ActorSystem) extends (() => Future[Boolean]) { private val crudConfig = system.settings.config.getConfig("cloudstate.proxy") private val autoCreateTables = crudConfig.getBoolean("jdbc.auto-create-tables") @@ -89,16 +87,16 @@ private class EnsureCrudTablesExistsActor(db: JdbcSlickDatabase) extends Actor w implicit val ec = context.dispatcher - private val stateCfg = new JdbcCrudStateTableConfiguration( + private val stateCfg = new JdbcValueEntityTableConfiguration( context.system.settings.config.getConfig("cloudstate.proxy.crud.jdbc-state-store") ) - private val stateTable = new JdbcCrudStateTable { - override val crudStateTableCfg: JdbcCrudStateTableConfiguration = stateCfg + private val stateTable = new JdbcValueEntityTable { + override val valueEntityTableCfg: JdbcValueEntityTableConfiguration = stateCfg override val profile: JdbcProfile = EnsureCrudTablesExistsActor.this.profile } - private val stateStatements = stateTable.CrudStateTableQuery.schema.createStatements.toSeq + private val stateStatements = stateTable.ValueEntityTableQuery.schema.createStatements.toSeq import akka.pattern.pipe diff --git a/proxy/postgres/src/graal/META-INF/native-image/io.cloudstate/cloudstate-proxy-jdbc/reflect-config.json.conf b/proxy/postgres/src/graal/META-INF/native-image/io.cloudstate/cloudstate-proxy-jdbc/reflect-config.json.conf index c73cc89c5..950bbdd15 100644 --- a/proxy/postgres/src/graal/META-INF/native-image/io.cloudstate/cloudstate-proxy-jdbc/reflect-config.json.conf +++ b/proxy/postgres/src/graal/META-INF/native-image/io.cloudstate/cloudstate-proxy-jdbc/reflect-config.json.conf @@ -4,7 +4,7 @@ methods: [{name: "", parameterTypes: ["akka.actor.ActorSystem"]}] } { - name: "io.cloudstate.proxy.jdbc.SlickEnsureCrudTablesExistReadyCheck" + name: "io.cloudstate.proxy.jdbc.SlickEnsureValueEntityTablesExistReadyCheck" methods: [{name: "", parameterTypes: ["akka.actor.ActorSystem"]}] } ] diff --git a/samples/java-crud-shopping-cart/src/main/java/io/cloudstate/samples/crud/shoppingcart/Main.java b/samples/java-valueentity-shopping-cart/src/main/java/io/cloudstate/samples/valueentity/shoppingcart/Main.java similarity index 80% rename from samples/java-crud-shopping-cart/src/main/java/io/cloudstate/samples/crud/shoppingcart/Main.java rename to samples/java-valueentity-shopping-cart/src/main/java/io/cloudstate/samples/valueentity/shoppingcart/Main.java index 940ef0583..b10f4464c 100644 --- a/samples/java-crud-shopping-cart/src/main/java/io/cloudstate/samples/crud/shoppingcart/Main.java +++ b/samples/java-valueentity-shopping-cart/src/main/java/io/cloudstate/samples/valueentity/shoppingcart/Main.java @@ -14,18 +14,18 @@ * limitations under the License. */ -package io.cloudstate.samples.crud.shoppingcart; +package io.cloudstate.samples.valueentity.shoppingcart; -import com.example.crud.shoppingcart.Shoppingcart; +import com.example.valueentity.shoppingcart.Shoppingcart; import io.cloudstate.javasupport.CloudState; public final class Main { public static final void main(String[] args) throws Exception { new CloudState() - .registerCrudEntity( + .registerValueEntity( ShoppingCartEntity.class, Shoppingcart.getDescriptor().findServiceByName("ShoppingCart"), - com.example.crud.shoppingcart.persistence.Domain.getDescriptor()) + com.example.valueentity.shoppingcart.persistence.Domain.getDescriptor()) .start() .toCompletableFuture() .get(); diff --git a/samples/java-crud-shopping-cart/src/main/resources/application.conf b/samples/java-valueentity-shopping-cart/src/main/resources/application.conf similarity index 100% rename from samples/java-crud-shopping-cart/src/main/resources/application.conf rename to samples/java-valueentity-shopping-cart/src/main/resources/application.conf diff --git a/samples/java-crud-shopping-cart/src/main/resources/simplelogger.properties b/samples/java-valueentity-shopping-cart/src/main/resources/simplelogger.properties similarity index 100% rename from samples/java-crud-shopping-cart/src/main/resources/simplelogger.properties rename to samples/java-valueentity-shopping-cart/src/main/resources/simplelogger.properties diff --git a/tck/src/main/scala/io/cloudstate/tck/CloudStateTCK.scala b/tck/src/main/scala/io/cloudstate/tck/CloudStateTCK.scala index d8ee7d5d4..0886bacd8 100644 --- a/tck/src/main/scala/io/cloudstate/tck/CloudStateTCK.scala +++ b/tck/src/main/scala/io/cloudstate/tck/CloudStateTCK.scala @@ -21,28 +21,28 @@ import akka.grpc.ServiceDescription import akka.testkit.TestKit import com.example.shoppingcart.persistence.domain import com.example.shoppingcart.shoppingcart._ -import com.example.crud.shoppingcart.shoppingcart.{ - AddLineItem => CrudAddLineItem, - Cart => CrudCart, - GetShoppingCart => CrudGetShoppingCart, - RemoveLineItem => CrudRemoveLineItem, - ShoppingCart => CrudShoppingCart, - ShoppingCartClient => CrudShoppingCartClient +import com.example.valueentity.shoppingcart.shoppingcart.{ + AddLineItem => ValueEntityAddLineItem, + Cart => ValueEntityCart, + GetShoppingCart => ValueEntityGetShoppingCart, + RemoveLineItem => ValueEntityRemoveLineItem, + ShoppingCart => ValueEntityShoppingCart, + ShoppingCartClient => ValueEntityShoppingCartClient } import com.google.protobuf.DescriptorProtos import com.google.protobuf.any.{Any => ScalaPbAny} import com.typesafe.config.{Config, ConfigFactory} import io.cloudstate.protocol.action.ActionProtocol import io.cloudstate.protocol.crdt.Crdt -import io.cloudstate.protocol.crud.Crud +import io.cloudstate.protocol.value_entity.ValueEntityProtocol import io.cloudstate.protocol.event_sourced._ -import io.cloudstate.tck.model.crud.crud.{CrudTckModel, CrudTwo} +import io.cloudstate.tck.model.valuentity.valueentity.{ValueEntityTckModel, ValueEntityTwo} import io.cloudstate.testkit.InterceptService.InterceptorSettings import io.cloudstate.testkit.eventsourced.{EventSourcedMessages, InterceptEventSourcedService} -import io.cloudstate.testkit.crud.{CrudMessages} import io.cloudstate.testkit.{InterceptService, ServiceAddress, TestClient, TestProtocol} import io.grpc.StatusRuntimeException import io.cloudstate.tck.model.eventsourced.{EventSourcedTckModel, EventSourcedTwo} +import io.cloudstate.testkit.valuentity.ValueEntityMessages import org.scalatest.concurrent.ScalaFutures import org.scalatest.{BeforeAndAfterAll, MustMatchers, WordSpec} @@ -78,7 +78,7 @@ class CloudStateTCK(description: String, settings: CloudStateTCK.Settings) private[this] final val client = TestClient(settings.proxy.host, settings.proxy.port) private[this] final val shoppingCartClient = ShoppingCartClient(client.settings)(system) - private[this] final val crudShoppingCartClient = CrudShoppingCartClient(client.settings)(system) + private[this] final val valueEntityShoppingCartClient = ValueEntityShoppingCartClient(client.settings)(system) private[this] final val protocol = TestProtocol(settings.service.host, settings.service.port) @@ -122,7 +122,7 @@ class CloudStateTCK(description: String, settings: CloudStateTCK.Settings) info.supportedEntityTypes must contain theSameElementsAs Seq( EventSourced.name, Crdt.name, - Crud.name, + ValueEntityProtocol.name, ActionProtocol.name ) @@ -150,20 +150,20 @@ class CloudStateTCK(description: String, settings: CloudStateTCK.Settings) entity.persistenceId must not be empty } - spec.entities.find(_.serviceName == CrudTckModel.name).foreach { entity => - serviceNames must contain("CrudTckModel") - entity.entityType mustBe Crud.name - entity.persistenceId mustBe "crud-tck-model" + spec.entities.find(_.serviceName == ValueEntityTckModel.name).foreach { entity => + serviceNames must contain("ValueEntityTckModel") + entity.entityType mustBe ValueEntityProtocol.name + entity.persistenceId mustBe "value-entity-tck-model" } - spec.entities.find(_.serviceName == CrudTwo.name).foreach { entity => - serviceNames must contain("CrudTwo") - entity.entityType mustBe Crud.name + spec.entities.find(_.serviceName == ValueEntityTwo.name).foreach { entity => + serviceNames must contain("ValueEntityTwo") + entity.entityType mustBe ValueEntityProtocol.name } - spec.entities.find(_.serviceName == CrudShoppingCart.name).foreach { entity => + spec.entities.find(_.serviceName == ValueEntityShoppingCart.name).foreach { entity => serviceNames must contain("ShoppingCart") - entity.entityType mustBe Crud.name + entity.entityType mustBe ValueEntityProtocol.name entity.persistenceId must not be empty } @@ -601,8 +601,8 @@ class CloudStateTCK(description: String, settings: CloudStateTCK.Settings) } } - "verify the HTTP API for CRUD ShoppingCart service" in testFor(CrudShoppingCart) { - import CrudShoppingCartVerifier._ + "verify the HTTP API for Value Entity ShoppingCart service" in testFor(ValueEntityShoppingCart) { + import ValueEntityShoppingCartVerifier._ def checkHttpRequest(path: String, body: String = null)(expected: => String): Unit = { val response = client.http.request(path, body) @@ -612,79 +612,79 @@ class CloudStateTCK(description: String, settings: CloudStateTCK.Settings) val session = shoppingCartSession(interceptor) - checkHttpRequest("crud/carts/foo") { + checkHttpRequest("ve/carts/foo") { session.verifyConnection() session.verifyGetInitialEmptyCart("foo") """{"items":[]}""" } - checkHttpRequest("crud/cart/foo/items/add", """{"productId": "A14362347", "name": "Deluxe", "quantity": 5}""") { + checkHttpRequest("ve/cart/foo/items/add", """{"productId": "A14362347", "name": "Deluxe", "quantity": 5}""") { session.verifyAddItem("foo", Item("A14362347", "Deluxe", 5), Cart(Item("A14362347", "Deluxe", 5))) "{}" } - checkHttpRequest("crud/cart/foo/items/add", """{"productId": "B14623482", "name": "Basic", "quantity": 1}""") { + checkHttpRequest("ve/cart/foo/items/add", """{"productId": "B14623482", "name": "Basic", "quantity": 1}""") { session.verifyAddItem("foo", Item("B14623482", "Basic", 1), Cart(Item("A14362347", "Deluxe", 5), Item("B14623482", "Basic", 1))) "{}" } - checkHttpRequest("crud/cart/foo/items/add", """{"productId": "A14362347", "name": "Deluxe", "quantity": 2}""") { + checkHttpRequest("ve/cart/foo/items/add", """{"productId": "A14362347", "name": "Deluxe", "quantity": 2}""") { session.verifyAddItem("foo", Item("A14362347", "Deluxe", 2), Cart(Item("B14623482", "Basic", 1), Item("A14362347", "Deluxe", 7))) "{}" } - checkHttpRequest("crud/carts/foo") { + checkHttpRequest("ve/carts/foo") { session.verifyGetCart("foo", shoppingCart(Item("B14623482", "Basic", 1), Item("A14362347", "Deluxe", 7))) """{"items":[{"productId":"B14623482","name":"Basic","quantity":1},{"productId":"A14362347","name":"Deluxe","quantity":7}]}""" } - checkHttpRequest("crud/carts/foo/items") { + checkHttpRequest("ve/carts/foo/items") { session.verifyGetCart("foo", shoppingCart(Item("B14623482", "Basic", 1), Item("A14362347", "Deluxe", 7))) """[{"productId":"B14623482","name":"Basic","quantity":1.0},{"productId":"A14362347","name":"Deluxe","quantity":7.0}]""" } - checkHttpRequest("crud/cart/foo/items/A14362347/remove", "") { + checkHttpRequest("ve/cart/foo/items/A14362347/remove", "") { session.verifyRemoveItem("foo", "A14362347", Cart(Item("B14623482", "Basic", 1))) "{}" } - checkHttpRequest("crud/carts/foo") { + checkHttpRequest("ve/carts/foo") { session.verifyGetCart("foo", shoppingCart(Item("B14623482", "Basic", 1))) """{"items":[{"productId":"B14623482","name":"Basic","quantity":1}]}""" } - checkHttpRequest("crud/carts/foo/items") { + checkHttpRequest("ve/carts/foo/items") { session.verifyGetCart("foo", shoppingCart(Item("B14623482", "Basic", 1))) """[{"productId":"B14623482","name":"Basic","quantity":1.0}]""" } - checkHttpRequest("crud/carts/foo/remove", """{"userId": "foo"}""") { + checkHttpRequest("ve/carts/foo/remove", """{"userId": "foo"}""") { session.verifyRemoveCart("foo") "{}" } - checkHttpRequest("crud/carts/foo") { + checkHttpRequest("ve/carts/foo") { session.verifyGetCart("foo", shoppingCart()) """{"items":[]}""" } } } - "verifying model test: crud entities" must { - import CrudMessages._ - import io.cloudstate.tck.model.crud.crud._ + "verifying model test: value entities" must { + import ValueEntityMessages._ + import io.cloudstate.tck.model.valuentity.valueentity._ - val ServiceTwo = CrudTwo.name + val ServiceTwo = ValueEntityTwo.name var entityId: Int = 0 def nextEntityId(): String = { entityId += 1; s"entity:$entityId" } - def crudTest(test: String => Any): Unit = - testFor(CrudTckModel, CrudTwo)(test(nextEntityId())) + def valueEntityTest(test: String => Any): Unit = + testFor(ValueEntityTckModel, ValueEntityTwo)(test(nextEntityId())) def updateState(value: String): RequestAction = RequestAction(RequestAction.Action.Update(Update(value))) @@ -725,19 +725,19 @@ class CloudStateTCK(description: String, settings: CloudStateTCK.Settings) def sideEffects(ids: String*): Effects = ids.foldLeft(Effects.empty) { case (e, id) => e.withSideEffect(ServiceTwo, "Call", Request(id)) } - "verify initial empty state" in crudTest { id => - protocol.crud + "verify initial empty state" in valueEntityTest { id => + protocol.valueEntity .connect() - .send(init(CrudTckModel.name, id)) + .send(init(ValueEntityTckModel.name, id)) .send(command(1, id, "Process", Request(id))) .expect(reply(1, Response())) .passivate() } - "verify update state" in crudTest { id => - protocol.crud + "verify update state" in valueEntityTest { id => + protocol.valueEntity .connect() - .send(init(CrudTckModel.name, id)) + .send(init(ValueEntityTckModel.name, id)) .send(command(1, id, "Process", Request(id, updateStates("A")))) .expect(reply(1, Response("A"), update("A"))) .send(command(2, id, "Process", Request(id))) @@ -745,10 +745,10 @@ class CloudStateTCK(description: String, settings: CloudStateTCK.Settings) .passivate() } - "verify delete state" in crudTest { id => - protocol.crud + "verify delete state" in valueEntityTest { id => + protocol.valueEntity .connect() - .send(init(CrudTckModel.name, id)) + .send(init(ValueEntityTckModel.name, id)) .send(command(1, id, "Process", Request(id, updateStates("A")))) .expect(reply(1, Response("A"), update("A"))) .send(command(2, id, "Process", Request(id, Seq(deleteState())))) @@ -758,10 +758,10 @@ class CloudStateTCK(description: String, settings: CloudStateTCK.Settings) .passivate() } - "verify sub invocations with multiple update states" in crudTest { id => - protocol.crud + "verify sub invocations with multiple update states" in valueEntityTest { id => + protocol.valueEntity .connect() - .send(init(CrudTckModel.name, id)) + .send(init(ValueEntityTckModel.name, id)) .send(command(1, id, "Process", Request(id, updateStates("A", "B", "C")))) .expect(reply(1, Response("C"), update("C"))) .send(command(2, id, "Process", Request(id))) @@ -769,10 +769,10 @@ class CloudStateTCK(description: String, settings: CloudStateTCK.Settings) .passivate() } - "verify sub invocations with multiple update states and delete states" in crudTest { id => - protocol.crud + "verify sub invocations with multiple update states and delete states" in valueEntityTest { id => + protocol.valueEntity .connect() - .send(init(CrudTckModel.name, id)) + .send(init(ValueEntityTckModel.name, id)) .send(command(1, id, "Process", Request(id, updateAndDeleteActions("A", "B")))) .expect(reply(1, Response(), delete())) .send(command(2, id, "Process", Request(id))) @@ -780,10 +780,10 @@ class CloudStateTCK(description: String, settings: CloudStateTCK.Settings) .passivate() } - "verify sub invocations with update, delete and update states" in crudTest { id => - protocol.crud + "verify sub invocations with update, delete and update states" in valueEntityTest { id => + protocol.valueEntity .connect() - .send(init(CrudTckModel.name, id)) + .send(init(ValueEntityTckModel.name, id)) .send(command(1, id, "Process", Request(id, deleteBetweenUpdateActions("A", "B")))) .expect(reply(1, Response("B"), update("B"))) .send(command(2, id, "Process", Request(id))) @@ -791,10 +791,10 @@ class CloudStateTCK(description: String, settings: CloudStateTCK.Settings) .passivate() } - "verify rehydration after passivation" in crudTest { id => - protocol.crud + "verify rehydration after passivation" in valueEntityTest { id => + protocol.valueEntity .connect() - .send(init(CrudTckModel.name, id)) + .send(init(ValueEntityTckModel.name, id)) .send(command(1, id, "Process", Request(id, updateStates("A")))) .expect(reply(1, Response("A"), update("A"))) .send(command(2, id, "Process", Request(id, updateStates("B")))) @@ -804,9 +804,9 @@ class CloudStateTCK(description: String, settings: CloudStateTCK.Settings) .send(command(4, id, "Process", Request(id, updateStates("D")))) .expect(reply(4, Response("D"), update("D"))) .passivate() - protocol.crud + protocol.valueEntity .connect() - .send(init(CrudTckModel.name, id, state(persisted("D")))) + .send(init(ValueEntityTckModel.name, id, state(persisted("D")))) .send(command(1, id, "Process", Request(id, updateStates("E")))) .expect(reply(1, Response("E"), update("E"))) .send(command(2, id, "Process", Request(id))) @@ -814,84 +814,84 @@ class CloudStateTCK(description: String, settings: CloudStateTCK.Settings) .passivate() } - "verify reply with multiple side effects" in crudTest { id => - protocol.crud + "verify reply with multiple side effects" in valueEntityTest { id => + protocol.valueEntity .connect() - .send(init(CrudTckModel.name, id)) + .send(init(ValueEntityTckModel.name, id)) .send(command(1, id, "Process", Request(id, sideEffectsTo("1", "2", "3")))) .expect(reply(1, Response(), sideEffects("1", "2", "3"))) .passivate() } - "verify reply with side effect to second service" in crudTest { id => - protocol.crud + "verify reply with side effect to second service" in valueEntityTest { id => + protocol.valueEntity .connect() - .send(init(CrudTckModel.name, id)) + .send(init(ValueEntityTckModel.name, id)) .send(command(1, id, "Process", Request(id, Seq(sideEffectTo(id))))) .expect(reply(1, Response(), sideEffect(ServiceTwo, "Call", Request(id)))) .passivate() } - "verify reply with multiple side effects and state" in crudTest { id => + "verify reply with multiple side effects and state" in valueEntityTest { id => val actions = updateStates("A", "B", "C", "D", "E") ++ sideEffectsTo("1", "2", "3") val effects = sideEffects("1", "2", "3").withUpdateAction(persisted("E")) - protocol.crud + protocol.valueEntity .connect() - .send(init(CrudTckModel.name, id)) + .send(init(ValueEntityTckModel.name, id)) .send(command(1, id, "Process", Request(id, actions))) .expect(reply(1, Response("E"), effects)) .passivate() } - "verify synchronous side effect to second service" in crudTest { id => - protocol.crud + "verify synchronous side effect to second service" in valueEntityTest { id => + protocol.valueEntity .connect() - .send(init(CrudTckModel.name, id)) + .send(init(ValueEntityTckModel.name, id)) .send(command(1, id, "Process", Request(id, Seq(sideEffectTo(id, synchronous = true))))) .expect(reply(1, Response(), sideEffect(ServiceTwo, "Call", Request(id), synchronous = true))) .passivate() } - "verify forward to second service" in crudTest { id => - protocol.crud + "verify forward to second service" in valueEntityTest { id => + protocol.valueEntity .connect() - .send(init(CrudTckModel.name, id)) + .send(init(ValueEntityTckModel.name, id)) .send(command(1, id, "Process", Request(id, Seq(forwardTo(id))))) .expect(forward(1, ServiceTwo, "Call", Request(id))) .passivate() } - "verify forward with updated state to second service" in crudTest { id => - protocol.crud + "verify forward with updated state to second service" in valueEntityTest { id => + protocol.valueEntity .connect() - .send(init(CrudTckModel.name, id)) + .send(init(ValueEntityTckModel.name, id)) .send(command(1, id, "Process", Request(id, Seq(updateState("A"), forwardTo(id))))) .expect(forward(1, ServiceTwo, "Call", Request(id), update("A"))) .passivate() } - "verify forward and side effect to second service" in crudTest { id => - protocol.crud + "verify forward and side effect to second service" in valueEntityTest { id => + protocol.valueEntity .connect() - .send(init(CrudTckModel.name, id)) + .send(init(ValueEntityTckModel.name, id)) .send(command(1, id, "Process", Request(id, Seq(sideEffectTo(id), forwardTo(id))))) .expect(forward(1, ServiceTwo, "Call", Request(id), sideEffect(ServiceTwo, "Call", Request(id)))) .passivate() } - "verify failure action" in crudTest { id => - protocol.crud + "verify failure action" in valueEntityTest { id => + protocol.valueEntity .connect() - .send(init(CrudTckModel.name, id)) + .send(init(ValueEntityTckModel.name, id)) .send(command(1, id, "Process", Request(id, Seq(failWith("expected failure"))))) .expect(actionFailure(1, "expected failure")) .passivate() } - "verify connection after failure action" in crudTest { id => - protocol.crud + "verify connection after failure action" in valueEntityTest { id => + protocol.valueEntity .connect() - .send(init(CrudTckModel.name, id)) + .send(init(ValueEntityTckModel.name, id)) .send(command(1, id, "Process", Request(id, Seq(updateState("A"))))) .expect(reply(1, Response("A"), update("A"))) .send(command(2, id, "Process", Request(id, Seq(failWith("expected failure"))))) @@ -901,61 +901,64 @@ class CloudStateTCK(description: String, settings: CloudStateTCK.Settings) .passivate() } - "verify failure action do not allow side effects" in crudTest { id => - protocol.crud + "verify failure action do not allow side effects" in valueEntityTest { id => + protocol.valueEntity .connect() - .send(init(CrudTckModel.name, id)) + .send(init(ValueEntityTckModel.name, id)) .send(command(1, id, "Process", Request(id, Seq(sideEffectTo(id), failWith("expected failure"))))) .expect(actionFailure(1, "expected failure")) .passivate() } } - "verifying app test: crud shopping cart" must { - import CrudMessages._ - import CrudShoppingCartVerifier._ + "verifying app test: value entity shopping cart" must { + import ValueEntityMessages._ + import ValueEntityShoppingCartVerifier._ - def verifyGetInitialEmptyCart(session: CrudShoppingCartVerifier, cartId: String): Unit = { - crudShoppingCartClient.getCart(CrudGetShoppingCart(cartId)).futureValue mustBe CrudCart() + def verifyGetInitialEmptyCart(session: ValueEntityShoppingCartVerifier, cartId: String): Unit = { + valueEntityShoppingCartClient.getCart(ValueEntityGetShoppingCart(cartId)).futureValue mustBe ValueEntityCart() session.verifyConnection() session.verifyGetInitialEmptyCart(cartId) } - def verifyGetCart(session: CrudShoppingCartVerifier, cartId: String, expected: Item*): Unit = { + def verifyGetCart(session: ValueEntityShoppingCartVerifier, cartId: String, expected: Item*): Unit = { val expectedCart = shoppingCart(expected: _*) - crudShoppingCartClient.getCart(CrudGetShoppingCart(cartId)).futureValue mustBe expectedCart + valueEntityShoppingCartClient.getCart(ValueEntityGetShoppingCart(cartId)).futureValue mustBe expectedCart session.verifyGetCart(cartId, expectedCart) } - def verifyAddItem(session: CrudShoppingCartVerifier, cartId: String, item: Item, expected: Cart): Unit = { - val addLineItem = CrudAddLineItem(cartId, item.id, item.name, item.quantity) - crudShoppingCartClient.addItem(addLineItem).futureValue mustBe EmptyScalaMessage + def verifyAddItem(session: ValueEntityShoppingCartVerifier, cartId: String, item: Item, expected: Cart): Unit = { + val addLineItem = ValueEntityAddLineItem(cartId, item.id, item.name, item.quantity) + valueEntityShoppingCartClient.addItem(addLineItem).futureValue mustBe EmptyScalaMessage session.verifyAddItem(cartId, item, expected) } - def verifyRemoveItem(session: CrudShoppingCartVerifier, cartId: String, itemId: String, expected: Cart): Unit = { - val removeLineItem = CrudRemoveLineItem(cartId, itemId) - crudShoppingCartClient.removeItem(removeLineItem).futureValue mustBe EmptyScalaMessage + def verifyRemoveItem(session: ValueEntityShoppingCartVerifier, + cartId: String, + itemId: String, + expected: Cart): Unit = { + val removeLineItem = ValueEntityRemoveLineItem(cartId, itemId) + valueEntityShoppingCartClient.removeItem(removeLineItem).futureValue mustBe EmptyScalaMessage session.verifyRemoveItem(cartId, itemId, expected) } - def verifyAddItemFailure(session: CrudShoppingCartVerifier, cartId: String, item: Item): Unit = { - val addLineItem = CrudAddLineItem(cartId, item.id, item.name, item.quantity) - val error = crudShoppingCartClient.addItem(addLineItem).failed.futureValue + def verifyAddItemFailure(session: ValueEntityShoppingCartVerifier, cartId: String, item: Item): Unit = { + val addLineItem = ValueEntityAddLineItem(cartId, item.id, item.name, item.quantity) + val error = valueEntityShoppingCartClient.addItem(addLineItem).failed.futureValue error mustBe a[StatusRuntimeException] val description = error.asInstanceOf[StatusRuntimeException].getStatus.getDescription session.verifyAddItemFailure(cartId, item, description) } - def verifyRemoveItemFailure(session: CrudShoppingCartVerifier, cartId: String, itemId: String): Unit = { - val removeLineItem = CrudRemoveLineItem(cartId, itemId) - val error = crudShoppingCartClient.removeItem(removeLineItem).failed.futureValue + def verifyRemoveItemFailure(session: ValueEntityShoppingCartVerifier, cartId: String, itemId: String): Unit = { + val removeLineItem = ValueEntityRemoveLineItem(cartId, itemId) + val error = valueEntityShoppingCartClient.removeItem(removeLineItem).failed.futureValue error mustBe a[StatusRuntimeException] val description = error.asInstanceOf[StatusRuntimeException].getStatus.getDescription session.verifyRemoveItemFailure(cartId, itemId, description) } - "verify get cart, add item, remove item, and failures" in testFor(CrudShoppingCart) { + "verify get cart, add item, remove item, and failures" in testFor(ValueEntityShoppingCart) { val session = shoppingCartSession(interceptor) verifyGetInitialEmptyCart(session, "cart:1") // initial empty state diff --git a/tck/src/main/scala/io/cloudstate/tck/CrudShoppingCartVerifier.scala b/tck/src/main/scala/io/cloudstate/tck/ValueEntityShoppingCartVerifier.scala similarity index 66% rename from tck/src/main/scala/io/cloudstate/tck/CrudShoppingCartVerifier.scala rename to tck/src/main/scala/io/cloudstate/tck/ValueEntityShoppingCartVerifier.scala index df606b801..ba8b994f4 100644 --- a/tck/src/main/scala/io/cloudstate/tck/CrudShoppingCartVerifier.scala +++ b/tck/src/main/scala/io/cloudstate/tck/ValueEntityShoppingCartVerifier.scala @@ -17,41 +17,41 @@ package io.cloudstate.tck import io.cloudstate.testkit.InterceptService -import com.example.crud.shoppingcart.shoppingcart.{ +import com.example.valueentity.shoppingcart.shoppingcart.{ LineItem, RemoveShoppingCart, - AddLineItem => CrudAddLineItem, - Cart => CrudCart, - GetShoppingCart => CrudGetShoppingCart, - RemoveLineItem => CrudRemoveLineItem, - ShoppingCart => CrudShoppingCart + AddLineItem, + Cart => ValueEntityCart, + GetShoppingCart, + RemoveLineItem, + ShoppingCart } -import com.example.crud.shoppingcart.persistence.domain -import io.cloudstate.protocol.crud.CrudStreamOut -import io.cloudstate.testkit.crud.{CrudMessages, InterceptCrudService} +import com.example.valueentity.shoppingcart.persistence.domain +import io.cloudstate.protocol.value_entity.ValueEntityStreamOut +import io.cloudstate.testkit.valuentity.{InterceptValueEntityService, ValueEntityMessages} import org.scalatest.MustMatchers import scala.collection.mutable -object CrudShoppingCartVerifier { +object ValueEntityShoppingCartVerifier { case class Item(id: String, name: String, quantity: Int) case class Cart(items: Item*) - def shoppingCartSession(interceptor: InterceptService): CrudShoppingCartVerifier = - new CrudShoppingCartVerifier(interceptor) + def shoppingCartSession(interceptor: InterceptService): ValueEntityShoppingCartVerifier = + new ValueEntityShoppingCartVerifier(interceptor) - def shoppingCart(items: Item*): CrudCart = CrudCart(items.map(i => LineItem(i.id, i.name, i.quantity))) + def shoppingCart(items: Item*): ValueEntityCart = ValueEntityCart(items.map(i => LineItem(i.id, i.name, i.quantity))) def domainShoppingCart(cart: Cart): domain.Cart = domain.Cart(cart.items.map(i => domain.LineItem(i.id, i.name, i.quantity))) } -class CrudShoppingCartVerifier(interceptor: InterceptService) extends MustMatchers { - import CrudMessages._ - import CrudShoppingCartVerifier._ +class ValueEntityShoppingCartVerifier(interceptor: InterceptService) extends MustMatchers { + import ValueEntityMessages._ + import ValueEntityShoppingCartVerifier._ private val commandIds = mutable.Map.empty[String, Long] - private var connection: InterceptCrudService.Connection = _ + private var connection: InterceptValueEntityService.Connection = _ private def nextCommandId(cartId: String): Long = commandIds.updateWith(cartId)(_.map(_ + 1).orElse(Some(1L))).get @@ -59,35 +59,35 @@ class CrudShoppingCartVerifier(interceptor: InterceptService) extends MustMatche def verifyGetInitialEmptyCart(cartId: String): Unit = { val commandId = nextCommandId(cartId) - connection.expectClient(init(CrudShoppingCart.name, cartId)) - connection.expectClient(command(commandId, cartId, "GetCart", CrudGetShoppingCart(cartId))) - connection.expectService(reply(commandId, CrudCart())) + connection.expectClient(init(ShoppingCart.name, cartId)) + connection.expectClient(command(commandId, cartId, "GetCart", GetShoppingCart(cartId))) + connection.expectService(reply(commandId, ValueEntityCart())) connection.expectNoInteraction() } - def verifyGetCart(cartId: String, expected: CrudCart): Unit = { + def verifyGetCart(cartId: String, expected: ValueEntityCart): Unit = { val commandId = nextCommandId(cartId) - connection.expectClient(command(commandId, cartId, "GetCart", CrudGetShoppingCart(cartId))) + connection.expectClient(command(commandId, cartId, "GetCart", GetShoppingCart(cartId))) connection.expectService(reply(commandId, expected)) connection.expectNoInteraction() } def verifyAddItem(cartId: String, item: Item, expected: Cart): Unit = { val commandId = nextCommandId(cartId) - val addLineItem = CrudAddLineItem(cartId, item.id, item.name, item.quantity) + val addLineItem = AddLineItem(cartId, item.id, item.name, item.quantity) val cartUpdated = domainShoppingCart(expected) connection.expectClient(command(commandId, cartId, "AddItem", addLineItem)) - val replied = connection.expectServiceMessage[CrudStreamOut.Message.Reply] + val replied = connection.expectServiceMessage[ValueEntityStreamOut.Message.Reply] replied mustBe reply(commandId, EmptyScalaMessage, update(cartUpdated)) connection.expectNoInteraction() } def verifyRemoveItem(cartId: String, itemId: String, expected: Cart): Unit = { val commandId = nextCommandId(cartId) - val removeLineItem = CrudRemoveLineItem(cartId, itemId) + val removeLineItem = RemoveLineItem(cartId, itemId) val cartUpdated = domainShoppingCart(expected) connection.expectClient(command(commandId, cartId, "RemoveItem", removeLineItem)) - val replied = connection.expectServiceMessage[CrudStreamOut.Message.Reply] + val replied = connection.expectServiceMessage[ValueEntityStreamOut.Message.Reply] replied mustBe reply(commandId, EmptyScalaMessage, update(cartUpdated)) connection.expectNoInteraction() } @@ -96,14 +96,14 @@ class CrudShoppingCartVerifier(interceptor: InterceptService) extends MustMatche val commandId = nextCommandId(cartId) val removeCart = RemoveShoppingCart(cartId) connection.expectClient(command(commandId, cartId, "RemoveCart", removeCart)) - val replied = connection.expectServiceMessage[CrudStreamOut.Message.Reply] + val replied = connection.expectServiceMessage[ValueEntityStreamOut.Message.Reply] replied mustBe reply(commandId, EmptyScalaMessage, delete()) connection.expectNoInteraction() } def verifyAddItemFailure(cartId: String, item: Item, failure: String): Unit = { val commandId = nextCommandId(cartId) - val addLineItem = CrudAddLineItem(cartId, item.id, item.name, item.quantity) + val addLineItem = AddLineItem(cartId, item.id, item.name, item.quantity) connection.expectClient(command(commandId, cartId, "AddItem", addLineItem)) connection.expectService(actionFailure(commandId, failure)) connection.expectNoInteraction() @@ -111,7 +111,7 @@ class CrudShoppingCartVerifier(interceptor: InterceptService) extends MustMatche def verifyRemoveItemFailure(cartId: String, itemId: String, failure: String): Unit = { val commandId = nextCommandId(cartId) - val removeLineItem = CrudRemoveLineItem(cartId, itemId) + val removeLineItem = RemoveLineItem(cartId, itemId) connection.expectClient(command(commandId, cartId, "RemoveItem", removeLineItem)) connection.expectService(actionFailure(commandId, failure)) connection.expectNoInteraction() diff --git a/testkit/src/main/scala/io/cloudstate/testkit/InterceptService.scala b/testkit/src/main/scala/io/cloudstate/testkit/InterceptService.scala index 59d53aa25..713e82a2c 100644 --- a/testkit/src/main/scala/io/cloudstate/testkit/InterceptService.scala +++ b/testkit/src/main/scala/io/cloudstate/testkit/InterceptService.scala @@ -22,9 +22,9 @@ import akka.http.scaladsl.Http import akka.testkit.{TestKit, TestProbe} import com.typesafe.config.{Config, ConfigFactory} import io.cloudstate.testkit.InterceptService.InterceptorSettings -import io.cloudstate.testkit.crud.InterceptCrudService import io.cloudstate.testkit.discovery.InterceptEntityDiscovery import io.cloudstate.testkit.eventsourced.InterceptEventSourcedService +import io.cloudstate.testkit.valuentity.InterceptValueEntityService import scala.concurrent.Await import scala.concurrent.duration._ @@ -37,7 +37,7 @@ final class InterceptService(settings: InterceptorSettings) { private val context = new InterceptorContext(settings.intercept.host, settings.intercept.port) private val entityDiscovery = new InterceptEntityDiscovery(context) private val eventSourced = new InterceptEventSourcedService(context) - private val crud = new InterceptCrudService(context) + private val valueEntity = new InterceptValueEntityService(context) import context.system @@ -45,7 +45,7 @@ final class InterceptService(settings: InterceptorSettings) { Await.result( Http().bindAndHandleAsync( - handler = entityDiscovery.handler orElse eventSourced.handler orElse crud.handler, + handler = entityDiscovery.handler orElse eventSourced.handler orElse valueEntity.handler, interface = settings.bind.host, port = settings.bind.port ), @@ -56,12 +56,12 @@ final class InterceptService(settings: InterceptorSettings) { def expectEventSourcedConnection(): InterceptEventSourcedService.Connection = eventSourced.expectConnection() - def expectCrudConnection(): InterceptCrudService.Connection = crud.expectConnection() + def expectCrudConnection(): InterceptValueEntityService.Connection = valueEntity.expectConnection() def terminate(): Unit = { entityDiscovery.terminate() eventSourced.terminate() - crud.terminate() + valueEntity.terminate() context.terminate() } } diff --git a/testkit/src/main/scala/io/cloudstate/testkit/TestProtocol.scala b/testkit/src/main/scala/io/cloudstate/testkit/TestProtocol.scala index a27d72780..427204ace 100644 --- a/testkit/src/main/scala/io/cloudstate/testkit/TestProtocol.scala +++ b/testkit/src/main/scala/io/cloudstate/testkit/TestProtocol.scala @@ -20,8 +20,8 @@ import akka.actor.ActorSystem import akka.grpc.GrpcClientSettings import akka.testkit.TestKit import com.typesafe.config.{Config, ConfigFactory} -import io.cloudstate.testkit.crud.TestCrudProtocol import io.cloudstate.testkit.eventsourced.TestEventSourcedProtocol +import io.cloudstate.testkit.valuentity.TestValueEntityProtocol final class TestProtocol(host: String, port: Int) { import TestProtocol._ @@ -29,13 +29,13 @@ final class TestProtocol(host: String, port: Int) { val context = new TestProtocolContext(host, port) val eventSourced = new TestEventSourcedProtocol(context) - val crud = new TestCrudProtocol(context) + val valueEntity = new TestValueEntityProtocol(context) def settings: GrpcClientSettings = context.clientSettings def terminate(): Unit = { eventSourced.terminate() - crud.terminate() + valueEntity.terminate() context.terminate() } } diff --git a/testkit/src/main/scala/io/cloudstate/testkit/TestService.scala b/testkit/src/main/scala/io/cloudstate/testkit/TestService.scala index 1805f5452..b01220ff2 100644 --- a/testkit/src/main/scala/io/cloudstate/testkit/TestService.scala +++ b/testkit/src/main/scala/io/cloudstate/testkit/TestService.scala @@ -20,9 +20,9 @@ import akka.actor.ActorSystem import akka.http.scaladsl.Http import akka.testkit.{TestKit, TestProbe} import com.typesafe.config.{Config, ConfigFactory} -import io.cloudstate.testkit.crud.TestCrudService import io.cloudstate.testkit.discovery.TestEntityDiscoveryService import io.cloudstate.testkit.eventsourced.TestEventSourcedService +import io.cloudstate.testkit.valuentity.TestValueEntityService import scala.concurrent.Await import scala.concurrent.duration._ @@ -38,13 +38,13 @@ final class TestService { val eventSourced = new TestEventSourcedService(context) - val crud = new TestCrudService(context) + val valueEntity = new TestValueEntityService(context) import context.system Await.result( Http().bindAndHandleAsync( - handler = entityDiscovery.handler orElse eventSourced.handler orElse crud.handler, + handler = entityDiscovery.handler orElse eventSourced.handler orElse valueEntity.handler, interface = "localhost", port = port ), diff --git a/testkit/src/main/scala/io/cloudstate/testkit/discovery/InterceptEntityDiscovery.scala b/testkit/src/main/scala/io/cloudstate/testkit/discovery/InterceptEntityDiscovery.scala index 80c47282f..d55ae0122 100644 --- a/testkit/src/main/scala/io/cloudstate/testkit/discovery/InterceptEntityDiscovery.scala +++ b/testkit/src/main/scala/io/cloudstate/testkit/discovery/InterceptEntityDiscovery.scala @@ -21,7 +21,7 @@ import akka.testkit.{TestKit, TestProbe} import com.google.protobuf.empty.{Empty => ScalaPbEmpty} import io.cloudstate.protocol.action.ActionProtocol import io.cloudstate.protocol.crdt.Crdt -import io.cloudstate.protocol.crud.Crud +import io.cloudstate.protocol.value_entity.ValueEntityProtocol import io.cloudstate.protocol.entity._ import io.cloudstate.protocol.event_sourced.EventSourced import io.cloudstate.testkit.BuildInfo @@ -83,7 +83,7 @@ object InterceptEntityDiscovery { protocolMinorVersion = BuildInfo.protocolMinorVersion, proxyName = BuildInfo.name, proxyVersion = BuildInfo.version, - supportedEntityTypes = Seq(ActionProtocol.name, Crdt.name, EventSourced.name, Crud.name) + supportedEntityTypes = Seq(ActionProtocol.name, Crdt.name, EventSourced.name, ValueEntityProtocol.name) ) def expectOnline(context: InterceptorContext, timeout: FiniteDuration): Unit = { diff --git a/testkit/src/main/scala/io/cloudstate/testkit/crud/InterceptCrudService.scala b/testkit/src/main/scala/io/cloudstate/testkit/valuentity/InterceptValueEntityService.scala similarity index 66% rename from testkit/src/main/scala/io/cloudstate/testkit/crud/InterceptCrudService.scala rename to testkit/src/main/scala/io/cloudstate/testkit/valuentity/InterceptValueEntityService.scala index 00e1f2ce8..5a49b87be 100644 --- a/testkit/src/main/scala/io/cloudstate/testkit/crud/InterceptCrudService.scala +++ b/testkit/src/main/scala/io/cloudstate/testkit/valuentity/InterceptValueEntityService.scala @@ -14,37 +14,44 @@ * limitations under the License. */ -package io.cloudstate.testkit.crud +package io.cloudstate.testkit.valuentity import akka.NotUsed import akka.http.scaladsl.model.{HttpRequest, HttpResponse} import akka.stream.scaladsl.{Sink, Source} import akka.testkit.TestProbe -import io.cloudstate.protocol.crud.{Crud, CrudClient, CrudHandler, CrudStreamIn, CrudStreamOut} +import io.cloudstate.protocol.value_entity.{ + ValueEntityProtocol, + ValueEntityProtocolClient, + ValueEntityProtocolHandler, + ValueEntityStreamIn, + ValueEntityStreamOut +} import io.cloudstate.testkit.InterceptService.InterceptorContext import scala.concurrent.Future import scala.concurrent.duration._ import scala.reflect.ClassTag -final class InterceptCrudService(context: InterceptorContext) { - import InterceptCrudService._ +final class InterceptValueEntityService(context: InterceptorContext) { + import InterceptValueEntityService._ private val interceptor = new CrudInterceptor(context) def expectConnection(): Connection = context.probe.expectMsgType[Connection] - def handler: PartialFunction[HttpRequest, Future[HttpResponse]] = CrudHandler.partial(interceptor)(context.system) + def handler: PartialFunction[HttpRequest, Future[HttpResponse]] = + ValueEntityProtocolHandler.partial(interceptor)(context.system) def terminate(): Unit = interceptor.terminate() } -object InterceptCrudService { +object InterceptValueEntityService { - final class CrudInterceptor(context: InterceptorContext) extends Crud { - private val client = CrudClient(context.clientSettings)(context.system) + final class CrudInterceptor(context: InterceptorContext) extends ValueEntityProtocol { + private val client = ValueEntityProtocolClient(context.clientSettings)(context.system) - override def handle(in: Source[CrudStreamIn, NotUsed]): Source[CrudStreamOut, NotUsed] = { + override def handle(in: Source[ValueEntityStreamIn, NotUsed]): Source[ValueEntityStreamOut, NotUsed] = { val connection = new Connection(context) context.probe.ref ! connection client.handle(in.alsoTo(connection.inSink)).alsoTo(connection.outSink) @@ -64,16 +71,16 @@ object InterceptCrudService { private[this] val in = TestProbe("CrudInProbe")(context.system) private[this] val out = TestProbe("CrudOutProbe")(context.system) - private[testkit] def inSink: Sink[CrudStreamIn, NotUsed] = Sink.actorRef(in.ref, Complete, Error.apply) - private[testkit] def outSink: Sink[CrudStreamOut, NotUsed] = Sink.actorRef(out.ref, Complete, Error.apply) + private[testkit] def inSink: Sink[ValueEntityStreamIn, NotUsed] = Sink.actorRef(in.ref, Complete, Error.apply) + private[testkit] def outSink: Sink[ValueEntityStreamOut, NotUsed] = Sink.actorRef(out.ref, Complete, Error.apply) - def expectClient(message: CrudStreamIn.Message): Connection = { - in.expectMsg(CrudStreamIn(message)) + def expectClient(message: ValueEntityStreamIn.Message): Connection = { + in.expectMsg(ValueEntityStreamIn(message)) this } - def expectService(message: CrudStreamOut.Message): Connection = { - out.expectMsg(CrudStreamOut(message)) + def expectService(message: ValueEntityStreamOut.Message): Connection = { + out.expectMsg(ValueEntityStreamOut(message)) this } @@ -81,7 +88,7 @@ object InterceptCrudService { expectServiceMessageClass(classTag.runtimeClass.asInstanceOf[Class[T]]) def expectServiceMessageClass[T](messageClass: Class[T]): T = { - val message = out.expectMsgType[CrudStreamOut].message + val message = out.expectMsgType[ValueEntityStreamOut].message assert(messageClass.isInstance(message), s"expected message $messageClass, found ${message.getClass} ($message)") message.asInstanceOf[T] } diff --git a/testkit/src/main/scala/io/cloudstate/testkit/crud/TestCrudProtocol.scala b/testkit/src/main/scala/io/cloudstate/testkit/valuentity/TestValueEntityProtocol.scala similarity index 57% rename from testkit/src/main/scala/io/cloudstate/testkit/crud/TestCrudProtocol.scala rename to testkit/src/main/scala/io/cloudstate/testkit/valuentity/TestValueEntityProtocol.scala index 7184d5ef5..dcce90974 100644 --- a/testkit/src/main/scala/io/cloudstate/testkit/crud/TestCrudProtocol.scala +++ b/testkit/src/main/scala/io/cloudstate/testkit/valuentity/TestValueEntityProtocol.scala @@ -14,39 +14,39 @@ * limitations under the License. */ -package io.cloudstate.testkit.crud +package io.cloudstate.testkit.valuentity import akka.stream.scaladsl.Source import akka.stream.testkit.TestPublisher import akka.stream.testkit.scaladsl.TestSink -import io.cloudstate.protocol.crud.{CrudClient, CrudStreamIn, CrudStreamOut} +import io.cloudstate.protocol.value_entity.{ValueEntityProtocolClient, ValueEntityStreamIn, ValueEntityStreamOut} import io.cloudstate.testkit.TestProtocol.TestProtocolContext -final class TestCrudProtocol(context: TestProtocolContext) { - private val client = CrudClient(context.clientSettings)(context.system) +final class TestValueEntityProtocol(context: TestProtocolContext) { + private val client = ValueEntityProtocolClient(context.clientSettings)(context.system) - def connect(): TestCrudProtocol.Connection = new TestCrudProtocol.Connection(client, context) + def connect(): TestValueEntityProtocol.Connection = new TestValueEntityProtocol.Connection(client, context) def terminate(): Unit = client.close() } -object TestCrudProtocol { +object TestValueEntityProtocol { - final class Connection(client: CrudClient, context: TestProtocolContext) { + final class Connection(client: ValueEntityProtocolClient, context: TestProtocolContext) { import context.system - private val in = TestPublisher.probe[CrudStreamIn]() - private val out = client.handle(Source.fromPublisher(in)).runWith(TestSink.probe[CrudStreamOut]) + private val in = TestPublisher.probe[ValueEntityStreamIn]() + private val out = client.handle(Source.fromPublisher(in)).runWith(TestSink.probe[ValueEntityStreamOut]) out.ensureSubscription() - def send(message: CrudStreamIn.Message): Connection = { - in.sendNext(CrudStreamIn(message)) + def send(message: ValueEntityStreamIn.Message): Connection = { + in.sendNext(ValueEntityStreamIn(message)) this } - def expect(message: CrudStreamOut.Message): Connection = { - out.request(1).expectNext(CrudStreamOut(message)) + def expect(message: ValueEntityStreamOut.Message): Connection = { + out.request(1).expectNext(ValueEntityStreamOut(message)) this } diff --git a/testkit/src/main/scala/io/cloudstate/testkit/crud/TestCrudService.scala b/testkit/src/main/scala/io/cloudstate/testkit/valuentity/TestValueEntityService.scala similarity index 59% rename from testkit/src/main/scala/io/cloudstate/testkit/crud/TestCrudService.scala rename to testkit/src/main/scala/io/cloudstate/testkit/valuentity/TestValueEntityService.scala index 2c0fcd787..81ef7ed8d 100644 --- a/testkit/src/main/scala/io/cloudstate/testkit/crud/TestCrudService.scala +++ b/testkit/src/main/scala/io/cloudstate/testkit/valuentity/TestValueEntityService.scala @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.cloudstate.testkit.crud +package io.cloudstate.testkit.valuentity import akka.NotUsed import akka.actor.ActorSystem @@ -24,53 +24,58 @@ import akka.stream.scaladsl.Source import akka.stream.testkit.TestPublisher import akka.stream.testkit.scaladsl.TestSink import com.google.protobuf.Descriptors.ServiceDescriptor -import io.cloudstate.protocol.crud.{Crud, CrudHandler, CrudStreamIn, CrudStreamOut} +import io.cloudstate.protocol.value_entity.{ + ValueEntityProtocol, + ValueEntityProtocolHandler, + ValueEntityStreamIn, + ValueEntityStreamOut +} import io.cloudstate.protocol.entity.EntitySpec import io.cloudstate.testkit.TestService.TestServiceContext import io.cloudstate.testkit.discovery.TestEntityDiscoveryService import scala.concurrent.Future -class TestCrudService(context: TestServiceContext) { - import TestCrudService._ +class TestValueEntityService(context: TestServiceContext) { + import TestValueEntityService._ private val testCrud = new TestCrud(context) def expectConnection(): Connection = context.probe.expectMsgType[Connection] def handler: PartialFunction[HttpRequest, Future[HttpResponse]] = - CrudHandler.partial(testCrud)(context.system) + ValueEntityProtocolHandler.partial(testCrud)(context.system) } -object TestCrudService { +object TestValueEntityService { def entitySpec(service: ServiceDescription): EntitySpec = - TestEntityDiscoveryService.entitySpec(Crud.name, service) + TestEntityDiscoveryService.entitySpec(ValueEntityProtocol.name, service) def entitySpec(descriptors: Seq[ServiceDescriptor]): EntitySpec = - TestEntityDiscoveryService.entitySpec(Crud.name, descriptors) + TestEntityDiscoveryService.entitySpec(ValueEntityProtocol.name, descriptors) - final class TestCrud(context: TestServiceContext) extends Crud { - override def handle(source: Source[CrudStreamIn, NotUsed]): Source[CrudStreamOut, NotUsed] = { + final class TestCrud(context: TestServiceContext) extends ValueEntityProtocol { + override def handle(source: Source[ValueEntityStreamIn, NotUsed]): Source[ValueEntityStreamOut, NotUsed] = { val connection = new Connection(context.system, source) context.probe.ref ! connection connection.outSource } } - final class Connection(system: ActorSystem, source: Source[CrudStreamIn, NotUsed]) { + final class Connection(system: ActorSystem, source: Source[ValueEntityStreamIn, NotUsed]) { private implicit val actorSystem: ActorSystem = system - private val in = source.runWith(TestSink.probe[CrudStreamIn]) - private val out = TestPublisher.probe[CrudStreamOut]() + private val in = source.runWith(TestSink.probe[ValueEntityStreamIn]) + private val out = TestPublisher.probe[ValueEntityStreamOut]() in.ensureSubscription() - private[testkit] def outSource: Source[CrudStreamOut, NotUsed] = Source.fromPublisher(out) + private[testkit] def outSource: Source[ValueEntityStreamOut, NotUsed] = Source.fromPublisher(out) - def expect(message: CrudStreamIn.Message): Unit = - in.request(1).expectNext(CrudStreamIn(message)) + def expect(message: ValueEntityStreamIn.Message): Unit = + in.request(1).expectNext(ValueEntityStreamIn(message)) - def send(message: CrudStreamOut.Message): Unit = - out.sendNext(CrudStreamOut(message)) + def send(message: ValueEntityStreamOut.Message): Unit = + out.sendNext(ValueEntityStreamOut(message)) def sendError(error: Throwable): Unit = out.sendError(error) diff --git a/testkit/src/main/scala/io/cloudstate/testkit/crud/TestCrudServiceClient.scala b/testkit/src/main/scala/io/cloudstate/testkit/valuentity/TestValueEntityServiceClient.scala similarity index 58% rename from testkit/src/main/scala/io/cloudstate/testkit/crud/TestCrudServiceClient.scala rename to testkit/src/main/scala/io/cloudstate/testkit/valuentity/TestValueEntityServiceClient.scala index f1d7eef90..7ef6b993d 100644 --- a/testkit/src/main/scala/io/cloudstate/testkit/crud/TestCrudServiceClient.scala +++ b/testkit/src/main/scala/io/cloudstate/testkit/valuentity/TestValueEntityServiceClient.scala @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.cloudstate.testkit.crud +package io.cloudstate.testkit.valuentity import akka.actor.ActorSystem import akka.grpc.GrpcClientSettings @@ -23,19 +23,21 @@ import akka.stream.testkit.TestPublisher import akka.stream.testkit.scaladsl.TestSink import akka.testkit.TestKit import com.typesafe.config.{Config, ConfigFactory} -import io.cloudstate.protocol.crud.{CrudClient, CrudStreamIn, CrudStreamOut} +import io.cloudstate.protocol.value_entity.{ValueEntityProtocolClient, ValueEntityStreamIn, ValueEntityStreamOut} -class TestCrudServiceClient(port: Int) { +class TestValueEntityServiceClient(port: Int) { private val config: Config = ConfigFactory.load(ConfigFactory.parseString(""" akka.http.server { preview.enable-http2 = on } """)) - private implicit val system: ActorSystem = ActorSystem("TestCrudServiceClient", config) - private val client = CrudClient(GrpcClientSettings.connectToServiceAt("localhost", port).withTls(false)) + private implicit val system: ActorSystem = ActorSystem("TestValueEntityServiceClient", config) + private val client = ValueEntityProtocolClient( + GrpcClientSettings.connectToServiceAt("localhost", port).withTls(false) + ) - def connect: TestCrudServiceClient.Connection = new TestCrudServiceClient.Connection(client, system) + def connect: TestValueEntityServiceClient.Connection = new TestValueEntityServiceClient.Connection(client, system) def terminate(): Unit = { client.close() @@ -43,21 +45,21 @@ class TestCrudServiceClient(port: Int) { } } -object TestCrudServiceClient { - def apply(port: Int) = new TestCrudServiceClient(port) +object TestValueEntityServiceClient { + def apply(port: Int) = new TestValueEntityServiceClient(port) - final class Connection(client: CrudClient, system: ActorSystem) { + final class Connection(client: ValueEntityProtocolClient, system: ActorSystem) { private implicit val actorSystem: ActorSystem = system - private val in = TestPublisher.probe[CrudStreamIn]() - private val out = client.handle(Source.fromPublisher(in)).runWith(TestSink.probe[CrudStreamOut]) + private val in = TestPublisher.probe[ValueEntityStreamIn]() + private val out = client.handle(Source.fromPublisher(in)).runWith(TestSink.probe[ValueEntityStreamOut]) out.ensureSubscription() - def send(message: CrudStreamIn.Message): Unit = - in.sendNext(CrudStreamIn(message)) + def send(message: ValueEntityStreamIn.Message): Unit = + in.sendNext(ValueEntityStreamIn(message)) - def expect(message: CrudStreamOut.Message): Unit = - out.request(1).expectNext(CrudStreamOut(message)) + def expect(message: ValueEntityStreamOut.Message): Unit = + out.request(1).expectNext(ValueEntityStreamOut(message)) def expectClosed(): Unit = { out.expectComplete() diff --git a/testkit/src/main/scala/io/cloudstate/testkit/crud/CrudMessages.scala b/testkit/src/main/scala/io/cloudstate/testkit/valuentity/ValueEntityMessages.scala similarity index 80% rename from testkit/src/main/scala/io/cloudstate/testkit/crud/CrudMessages.scala rename to testkit/src/main/scala/io/cloudstate/testkit/valuentity/ValueEntityMessages.scala index 742790b42..8932208b7 100644 --- a/testkit/src/main/scala/io/cloudstate/testkit/crud/CrudMessages.scala +++ b/testkit/src/main/scala/io/cloudstate/testkit/valuentity/ValueEntityMessages.scala @@ -14,31 +14,30 @@ * limitations under the License. */ -package io.cloudstate.testkit.crud +package io.cloudstate.testkit.valuentity import com.google.protobuf.any.{Any => ScalaPbAny} import com.google.protobuf.empty.{Empty => ScalaPbEmpty} import com.google.protobuf.{Any => JavaPbAny, Empty => JavaPbEmpty, Message => JavaPbMessage} -import io.cloudstate.protocol.crud.CrudAction.Action.Update -import io.cloudstate.protocol.crud.CrudAction.Action.Delete import io.cloudstate.protocol.entity._ -import io.cloudstate.protocol.crud._ +import io.cloudstate.protocol.value_entity._ import scalapb.{GeneratedMessage => ScalaPbMessage} -object CrudMessages { - import CrudStreamIn.{Message => InMessage} - import CrudStreamOut.{Message => OutMessage} +object ValueEntityMessages { + import ValueEntityStreamIn.{Message => InMessage} + import ValueEntityStreamOut.{Message => OutMessage} + import ValueEntityAction.Action._ - case class Effects(sideEffects: Seq[SideEffect] = Seq.empty, crudAction: Option[CrudAction] = None) { + case class Effects(sideEffects: Seq[SideEffect] = Seq.empty, crudAction: Option[ValueEntityAction] = None) { def withUpdateAction(message: JavaPbMessage): Effects = - copy(crudAction = Some(CrudAction(Update(CrudUpdate(messagePayload(message)))))) + copy(crudAction = Some(ValueEntityAction(Update(ValueEntityUpdate(messagePayload(message)))))) def withUpdateAction(message: ScalaPbMessage): Effects = - copy(crudAction = Some(CrudAction(Update(CrudUpdate(messagePayload(message)))))) + copy(crudAction = Some(ValueEntityAction(Update(ValueEntityUpdate(messagePayload(message)))))) def withDeleteAction(): Effects = - copy(crudAction = Some(CrudAction(Delete(CrudDelete())))) + copy(crudAction = Some(ValueEntityAction(Delete(ValueEntityDelete())))) def withSideEffect(service: String, command: String, message: ScalaPbMessage): Effects = withSideEffect(service, command, messagePayload(message), synchronous = false) @@ -56,19 +55,19 @@ object CrudMessages { val EmptyScalaMessage: ScalaPbMessage = ScalaPbEmpty.defaultInstance def init(serviceName: String, entityId: String): InMessage = - init(serviceName, entityId, Some(CrudInitState())) + init(serviceName, entityId, Some(ValueEntityInitState())) - def init(serviceName: String, entityId: String, state: CrudInitState): InMessage = + def init(serviceName: String, entityId: String, state: ValueEntityInitState): InMessage = init(serviceName, entityId, Some(state)) - def init(serviceName: String, entityId: String, state: Option[CrudInitState]): InMessage = - InMessage.Init(CrudInit(serviceName, entityId, state)) + def init(serviceName: String, entityId: String, state: Option[ValueEntityInitState]): InMessage = + InMessage.Init(ValueEntityInit(serviceName, entityId, state)) - def state(payload: JavaPbMessage): CrudInitState = - CrudInitState(messagePayload(payload)) + def state(payload: JavaPbMessage): ValueEntityInitState = + ValueEntityInitState(messagePayload(payload)) - def state(payload: ScalaPbMessage): CrudInitState = - CrudInitState(messagePayload(payload)) + def state(payload: ScalaPbMessage): ValueEntityInitState = + ValueEntityInitState(messagePayload(payload)) def command(id: Long, entityId: String, name: String): InMessage = command(id, entityId, name, EmptyJavaMessage) @@ -94,14 +93,14 @@ object CrudMessages { def reply(id: Long, payload: ScalaPbMessage, effects: Effects): OutMessage = reply(id, messagePayload(payload), effects) - def reply(id: Long, payload: Option[ScalaPbAny], crudAction: Option[CrudAction]): OutMessage = - OutMessage.Reply(CrudReply(id, clientActionReply(payload), Seq.empty, crudAction)) + def reply(id: Long, payload: Option[ScalaPbAny], crudAction: Option[ValueEntityAction]): OutMessage = + OutMessage.Reply(ValueEntityReply(id, clientActionReply(payload), Seq.empty, crudAction)) def reply(id: Long, payload: Option[ScalaPbAny], effects: Effects): OutMessage = - OutMessage.Reply(CrudReply(id, clientActionReply(payload), effects.sideEffects, effects.crudAction)) + OutMessage.Reply(ValueEntityReply(id, clientActionReply(payload), effects.sideEffects, effects.crudAction)) def replyAction(id: Long, action: Option[ClientAction], effects: Effects): OutMessage = - OutMessage.Reply(CrudReply(id, action, effects.sideEffects, effects.crudAction)) + OutMessage.Reply(ValueEntityReply(id, action, effects.sideEffects, effects.crudAction)) def forward(id: Long, service: String, command: String, payload: ScalaPbMessage): OutMessage = forward(id, service, command, payload, Effects.empty) @@ -113,7 +112,7 @@ object CrudMessages { replyAction(id, clientActionForward(service, command, payload), effects) def actionFailure(id: Long, description: String): OutMessage = - OutMessage.Reply(CrudReply(id, clientActionFailure(id, description))) + OutMessage.Reply(ValueEntityReply(id, clientActionFailure(id, description))) def failure(description: String): OutMessage = failure(id = 0, description) From f820f7fb9469d520cbf5ec9193afaaadab145f94 Mon Sep 17 00:00:00 2001 From: Guy Youansi Date: Tue, 20 Oct 2020 21:35:15 +0200 Subject: [PATCH 70/93] rename CRUD to Value Entity --- .../shoppingcart/ShoppingCartEntity.java | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) rename samples/{java-crud-shopping-cart/src/main/java/io/cloudstate/samples/crud => java-valueentity-shopping-cart/src/main/java/io/cloudstate/samples/valueentity}/shoppingcart/ShoppingCartEntity.java (90%) diff --git a/samples/java-crud-shopping-cart/src/main/java/io/cloudstate/samples/crud/shoppingcart/ShoppingCartEntity.java b/samples/java-valueentity-shopping-cart/src/main/java/io/cloudstate/samples/valueentity/shoppingcart/ShoppingCartEntity.java similarity index 90% rename from samples/java-crud-shopping-cart/src/main/java/io/cloudstate/samples/crud/shoppingcart/ShoppingCartEntity.java rename to samples/java-valueentity-shopping-cart/src/main/java/io/cloudstate/samples/valueentity/shoppingcart/ShoppingCartEntity.java index 68a881fa9..36ceffdce 100644 --- a/samples/java-crud-shopping-cart/src/main/java/io/cloudstate/samples/crud/shoppingcart/ShoppingCartEntity.java +++ b/samples/java-valueentity-shopping-cart/src/main/java/io/cloudstate/samples/valueentity/shoppingcart/ShoppingCartEntity.java @@ -14,23 +14,23 @@ * limitations under the License. */ -package io.cloudstate.samples.crud.shoppingcart; +package io.cloudstate.samples.valueentity.shoppingcart; -import com.example.crud.shoppingcart.Shoppingcart; -import com.example.crud.shoppingcart.persistence.Domain; +import com.example.valueentity.shoppingcart.Shoppingcart; +import com.example.valueentity.shoppingcart.persistence.Domain; import com.google.protobuf.Empty; import io.cloudstate.javasupport.EntityId; -import io.cloudstate.javasupport.crud.CommandContext; -import io.cloudstate.javasupport.crud.CommandHandler; -import io.cloudstate.javasupport.crud.CrudEntity; +import io.cloudstate.javasupport.valueentity.CommandContext; +import io.cloudstate.javasupport.valueentity.CommandHandler; +import io.cloudstate.javasupport.valueentity.ValueEntity; import java.util.List; import java.util.Optional; import java.util.function.Predicate; import java.util.stream.Collectors; -/** A CRUD entity. */ -@CrudEntity(persistenceId = "crud-shopping-cart") +/** A value entity. */ +@ValueEntity(persistenceId = "value-entity-shopping-cart") public class ShoppingCartEntity { private final String entityId; From f21b7d0a080522969709ca9ac1dcd46bef8c1872 Mon Sep 17 00:00:00 2001 From: Guy Youansi Date: Wed, 21 Oct 2020 11:04:18 +0200 Subject: [PATCH 71/93] rename CRUD to Value Entity follow up (crud jdbc configuration to value entity configuration) --- proxy/core/src/main/resources/in-memory.conf | 11 ++++---- proxy/core/src/main/resources/reference.conf | 16 ++++++------ .../proxy/EntityDiscoveryManager.scala | 6 ++--- .../proxy/valueentity/store/JdbcConfig.scala | 4 +-- .../valueentity/store/JdbcStoreSupport.scala | 8 +++--- .../jdbc/src/main/resources/jdbc-common.conf | 4 +-- ...sureValueEntityTablesExistReadyCheck.scala | 26 +++++++++---------- .../src/main/resources/application.conf | 2 +- 8 files changed, 40 insertions(+), 37 deletions(-) diff --git a/proxy/core/src/main/resources/in-memory.conf b/proxy/core/src/main/resources/in-memory.conf index b631a435f..20a51650a 100644 --- a/proxy/core/src/main/resources/in-memory.conf +++ b/proxy/core/src/main/resources/in-memory.conf @@ -12,9 +12,10 @@ inmem-snapshot-store { class = "io.cloudstate.proxy.eventsourced.InMemSnapshotStore" } -cloudstate.proxy.journal-enabled = true +cloudstate.proxy { + journal-enabled = true -# Configuration for using an in memory CRUD store -cloudstate.proxy.crud-enabled = true - -cloudstate.proxy.crud.store-type = "in-memory" \ No newline at end of file + # Configuration for using an in memory Value Entity persistence store + value-entity-enabled = true + value-entity-persistence-store.store-type = "in-memory" +} \ No newline at end of file diff --git a/proxy/core/src/main/resources/reference.conf b/proxy/core/src/main/resources/reference.conf index 62bee7326..0cebda4ac 100644 --- a/proxy/core/src/main/resources/reference.conf +++ b/proxy/core/src/main/resources/reference.conf @@ -124,12 +124,12 @@ cloudstate.proxy { } } - # Enable the CRUD the functionality by setting it to true - crud-enabled = false + # Enable the Value Entity functionality by setting it to true + value-entity-enabled = false - # Configures the CRUD functionality when crud-enabled is true - crud { - # This property indicate the type of CRUD store to be used. + # Configures the persistence store for the Value Entity when value-entity-enabled is true + value-entity-persistence-store { + # This property indicate the type of store to be used for Value Entity. # Valid options are: "using-jdbc", "in-memory" # "in-memory" means the data are persisted in memory. # "jdbc" means the data are persisted in a configured native JDBC database. @@ -207,14 +207,14 @@ cloudstate.proxy { # This property controls a user-defined name for the connection pool and appears mainly in logging and JMX # management consoles to identify pools and pool configurations. Default: auto-generated - poolName = "cloudstate-crud-connection-pool" + poolName = "cloudstate-value-entity-connection-pool" } - # This property indicates the CRUD table in use. + # This property indicates the table in use for the Value Entity. jdbc-state-store { tables { state { - tableName = "crud_state_entity" + tableName = "value_entity_state" schemaName = "" columnNames { persistentId = "persistent_id" diff --git a/proxy/core/src/main/scala/io/cloudstate/proxy/EntityDiscoveryManager.scala b/proxy/core/src/main/scala/io/cloudstate/proxy/EntityDiscoveryManager.scala index f36fec8b5..59b15c263 100644 --- a/proxy/core/src/main/scala/io/cloudstate/proxy/EntityDiscoveryManager.scala +++ b/proxy/core/src/main/scala/io/cloudstate/proxy/EntityDiscoveryManager.scala @@ -57,7 +57,7 @@ object EntityDiscoveryManager { numberOfShards: Int, proxyParallelism: Int, journalEnabled: Boolean, - crudEnabled: Boolean, + valueEntityEnabled: Boolean, config: Config ) { validate() @@ -75,7 +75,7 @@ object EntityDiscoveryManager { numberOfShards = config.getInt("number-of-shards"), proxyParallelism = config.getInt("proxy-parallelism"), journalEnabled = config.getBoolean("journal-enabled"), - crudEnabled = config.getBoolean("crud-enabled"), + valueEntityEnabled = config.getBoolean("value-entity-enabled"), config = config) } @@ -137,7 +137,7 @@ class EntityDiscoveryManager(config: EntityDiscoveryManager.Configuration)( ) else Map.empty } ++ { - if (config.crudEnabled) + if (config.valueEntityEnabled) Map( ValueEntityProtocol.name -> new ValueEntitySupportFactory(system, config, clientSettings) ) diff --git a/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/JdbcConfig.scala b/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/JdbcConfig.scala index 5909a8538..f12551a14 100644 --- a/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/JdbcConfig.scala +++ b/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/JdbcConfig.scala @@ -48,12 +48,12 @@ object JdbcSlickDatabase { def apply(config: Config): JdbcSlickDatabase = { val database: JdbcBackend.Database = Database.forConfig( - "crud.jdbc.database.slick", + "value-entity-persistence-store.jdbc.database.slick", config ) val profile: JdbcProfile = DatabaseConfig .forConfig[JdbcProfile]( - "crud.jdbc.database.slick", + "value-entity-persistence-store.jdbc.database.slick", config ) .profile diff --git a/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/JdbcStoreSupport.scala b/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/JdbcStoreSupport.scala index 02121ca1e..5417ea6e8 100644 --- a/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/JdbcStoreSupport.scala +++ b/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/JdbcStoreSupport.scala @@ -31,17 +31,19 @@ object JdbcStoreSupport { trait JdbcStoreSupport { def createStore(config: Config)(implicit ec: ExecutionContext): JdbcStore[Key, ByteString] = - config.getString("crud.store-type") match { + config.getString("value-entity-persistence-store.store-type") match { case IN_MEMORY => new JdbcInMemoryStore case JDBC => createJdbcStore(config) case other => - throw new IllegalArgumentException(s"CRUD store-type must be one of: ${IN_MEMORY} or ${JDBC} but is '$other'") + throw new IllegalArgumentException( + s"Value Entity store-type must be one of: ${IN_MEMORY} or ${JDBC} but is '$other'" + ) } private def createJdbcStore(config: Config)(implicit ec: ExecutionContext): JdbcStore[Key, ByteString] = { val slickDatabase = JdbcSlickDatabase(config) val tableConfiguration = new JdbcValueEntityTableConfiguration( - config.getConfig("crud.jdbc-state-store") + config.getConfig("value-entity-persistence-store.jdbc-state-store") ) val queries = new JdbcValueEntityQueries(slickDatabase.profile, tableConfiguration) new JdbcStoreImpl(slickDatabase, queries) diff --git a/proxy/jdbc/src/main/resources/jdbc-common.conf b/proxy/jdbc/src/main/resources/jdbc-common.conf index edc699f81..677e67ef1 100644 --- a/proxy/jdbc/src/main/resources/jdbc-common.conf +++ b/proxy/jdbc/src/main/resources/jdbc-common.conf @@ -2,13 +2,13 @@ include "cloudstate-common" cloudstate.proxy { journal-enabled = true - crud-enabled = true + value-entity-enabled = true } akka { management.health-checks.readiness-checks { cloudstate-jdbc = "io.cloudstate.proxy.jdbc.SlickEnsureTablesExistReadyCheck" - cloudstate-crud-jdbc = "io.cloudstate.proxy.jdbc.SlickEnsureValueEntityTablesExistReadyCheck" + cloudstate-value-entity-jdbc = "io.cloudstate.proxy.jdbc.SlickEnsureValueEntityTablesExistReadyCheck" } persistence { diff --git a/proxy/jdbc/src/main/scala/io/cloudstate/proxy/jdbc/SlickEnsureValueEntityTablesExistReadyCheck.scala b/proxy/jdbc/src/main/scala/io/cloudstate/proxy/jdbc/SlickEnsureValueEntityTablesExistReadyCheck.scala index ef6f24e23..309cd1cad 100644 --- a/proxy/jdbc/src/main/scala/io/cloudstate/proxy/jdbc/SlickEnsureValueEntityTablesExistReadyCheck.scala +++ b/proxy/jdbc/src/main/scala/io/cloudstate/proxy/jdbc/SlickEnsureValueEntityTablesExistReadyCheck.scala @@ -36,30 +36,30 @@ import scala.util.{Failure, Success, Try} class SlickEnsureValueEntityTablesExistReadyCheck(system: ActorSystem) extends (() => Future[Boolean]) { - private val crudConfig = system.settings.config.getConfig("cloudstate.proxy") - private val autoCreateTables = crudConfig.getBoolean("jdbc.auto-create-tables") + private val valueEntityConfig = system.settings.config.getConfig("cloudstate.proxy") + private val autoCreateTables = valueEntityConfig.getBoolean("jdbc.auto-create-tables") private val check: () => Future[Boolean] = if (autoCreateTables) { - // Get a hold of the cloudstate-crud-jdbc slick database instance - val db = JdbcSlickDatabase(crudConfig) + // Get a hold of the cloudstate.proxy.value-entity-persistence-store.jdbc.database.slick database instance + val db = JdbcSlickDatabase(valueEntityConfig) val actor = system.actorOf( BackoffSupervisor.props( BackoffOpts.onFailure( - childProps = Props(new EnsureCrudTablesExistsActor(db)), - childName = "crud-table-creator", + childProps = Props(new EnsureValueEntityTablesExistsActor(db)), + childName = "value-entity-table-creator", minBackoff = 3.seconds, maxBackoff = 30.seconds, randomFactor = 0.2 ) ), - "crud-table-creator-supervisor" + "value-entity-table-creator-supervisor" ) implicit val timeout = Timeout(10.seconds) // TODO make configurable? import akka.pattern.ask - () => (actor ? EnsureCrudTablesExistsActor.Ready).mapTo[Boolean] + () => (actor ? EnsureValueEntityTablesExistsActor.Ready).mapTo[Boolean] } else { () => Future.successful(true) } @@ -67,7 +67,7 @@ class SlickEnsureValueEntityTablesExistReadyCheck(system: ActorSystem) extends ( override def apply(): Future[Boolean] = check() } -private object EnsureCrudTablesExistsActor { +private object EnsureValueEntityTablesExistsActor { case object Ready @@ -76,10 +76,10 @@ private object EnsureCrudTablesExistsActor { /** * Copied/adapted from https://github.com/lagom/lagom/blob/60897ef752ddbfc28553d3726b8fdb830a3ebdc4/persistence-jdbc/core/src/main/scala/com/lightbend/lagom/internal/persistence/jdbc/SlickProvider.scala */ -private class EnsureCrudTablesExistsActor(db: JdbcSlickDatabase) extends Actor with ActorLogging { +private class EnsureValueEntityTablesExistsActor(db: JdbcSlickDatabase) extends Actor with ActorLogging { // TODO refactor this to be in sync with the event sourced one - import EnsureCrudTablesExistsActor._ + import EnsureValueEntityTablesExistsActor._ private val profile = db.profile @@ -88,12 +88,12 @@ private class EnsureCrudTablesExistsActor(db: JdbcSlickDatabase) extends Actor w implicit val ec = context.dispatcher private val stateCfg = new JdbcValueEntityTableConfiguration( - context.system.settings.config.getConfig("cloudstate.proxy.crud.jdbc-state-store") + context.system.settings.config.getConfig("cloudstate.proxy.value-entity-persistence-store.jdbc-state-store") ) private val stateTable = new JdbcValueEntityTable { override val valueEntityTableCfg: JdbcValueEntityTableConfiguration = stateCfg - override val profile: JdbcProfile = EnsureCrudTablesExistsActor.this.profile + override val profile: JdbcProfile = EnsureValueEntityTablesExistsActor.this.profile } private val stateStatements = stateTable.ValueEntityTableQuery.schema.createStatements.toSeq diff --git a/proxy/postgres/src/main/resources/application.conf b/proxy/postgres/src/main/resources/application.conf index 6aec3d842..d3e85d694 100644 --- a/proxy/postgres/src/main/resources/application.conf +++ b/proxy/postgres/src/main/resources/application.conf @@ -34,7 +34,7 @@ akka-persistence-jdbc.shared-databases.slick { } } -cloudstate.proxy.crud { +cloudstate.proxy.value-entity-persistence-store { store-type = "jdbc" jdbc.database.slick { From f65553c82e6a548db14147b169d4993012af476a Mon Sep 17 00:00:00 2001 From: Guy Youansi Date: Fri, 23 Oct 2020 14:19:40 +0200 Subject: [PATCH 72/93] add entity exceptions factory class. extract grpc entity message creation for TCK --- .../javasupport/impl/EntityExceptions.scala | 70 ++++++++++++++++ .../AnnotationBasedEventSourcedSupport.scala | 2 +- .../impl/eventsourced/EventSourcedImpl.scala | 6 +- .../AnnotationBasedValueEntitySupport.scala | 14 +--- .../impl/valueentity/ValueEntityImpl.scala | 55 ++----------- .../io/cloudstate/tck/CloudStateTCK.scala | 31 ++++++-- .../testkit/entity/EntityMessages.scala | 79 +++++++++++++++++++ .../eventsourced/EventSourcedMessages.scala | 57 +------------ .../valuentity/ValueEntityMessages.scala | 71 +++++------------ 9 files changed, 208 insertions(+), 177 deletions(-) create mode 100644 java-support/src/main/scala/io/cloudstate/javasupport/impl/EntityExceptions.scala create mode 100644 testkit/src/main/scala/io/cloudstate/testkit/entity/EntityMessages.scala diff --git a/java-support/src/main/scala/io/cloudstate/javasupport/impl/EntityExceptions.scala b/java-support/src/main/scala/io/cloudstate/javasupport/impl/EntityExceptions.scala new file mode 100644 index 000000000..ac4f7845b --- /dev/null +++ b/java-support/src/main/scala/io/cloudstate/javasupport/impl/EntityExceptions.scala @@ -0,0 +1,70 @@ +/* + * Copyright 2019 Lightbend Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.cloudstate.javasupport.impl + +import io.cloudstate.javasupport.valueentity +import io.cloudstate.javasupport.eventsourced +import io.cloudstate.protocol.value_entity.ValueEntityInit +import io.cloudstate.protocol.entity.{Command, Failure} +import io.cloudstate.protocol.event_sourced.EventSourcedInit + +object EntityExceptions { + + final case class EntityException(entityId: String, commandId: Long, commandName: String, message: String) + extends RuntimeException(message) + + object EntityException { + def apply(message: String): EntityException = + EntityException(entityId = "", commandId = 0, commandName = "", message) + + def apply(command: Command, message: String): EntityException = + EntityException(command.entityId, command.id, command.name, message) + + def apply(context: valueentity.CommandContext[_], message: String): EntityException = + EntityException(context.entityId, context.commandId, context.commandName, message) + + def apply(context: eventsourced.CommandContext, message: String): EntityException = + EntityException(context.entityId, context.commandId, context.commandName, message) + } + + object ProtocolException { + def apply(message: String): EntityException = + EntityException(entityId = "", commandId = 0, commandName = "", "Protocol error: " + message) + + def apply(command: Command, message: String): EntityException = + EntityException(command.entityId, command.id, command.name, "Protocol error: " + message) + + def apply(init: ValueEntityInit, message: String): EntityException = + EntityException(init.entityId, commandId = 0, commandName = "", "Protocol error: " + message) + + def apply(init: EventSourcedInit, message: String): EntityException = + EntityException(init.entityId, commandId = 0, commandName = "", "Protocol error: " + message) + } + + def failure(cause: Throwable): Failure = cause match { + case e: EntityException => Failure(e.commandId, e.message) + case e => Failure(description = "Unexpected failure: " + e.getMessage) + } + + def failureMessage(cause: Throwable): String = cause match { + case EntityException(entityId, commandId, commandName, _) => + val commandDescription = if (commandId != 0) s" for command [$commandName]" else "" + val entityDescription = if (entityId.nonEmpty) s"entity [$entityId]" else "entity" + s"Terminating $entityDescription due to unexpected failure$commandDescription" + case _ => "Terminating entity due to unexpected failure" + } +} diff --git a/java-support/src/main/scala/io/cloudstate/javasupport/impl/eventsourced/AnnotationBasedEventSourcedSupport.scala b/java-support/src/main/scala/io/cloudstate/javasupport/impl/eventsourced/AnnotationBasedEventSourcedSupport.scala index 8767553fb..41b6d26f0 100644 --- a/java-support/src/main/scala/io/cloudstate/javasupport/impl/eventsourced/AnnotationBasedEventSourcedSupport.scala +++ b/java-support/src/main/scala/io/cloudstate/javasupport/impl/eventsourced/AnnotationBasedEventSourcedSupport.scala @@ -25,7 +25,7 @@ import io.cloudstate.javasupport.impl.{AnySupport, ReflectionHelper, ResolvedEnt import scala.collection.concurrent.TrieMap import com.google.protobuf.{Descriptors, Any => JavaPbAny} -import io.cloudstate.javasupport.impl.eventsourced.EventSourcedImpl.EntityException +import io.cloudstate.javasupport.impl.EntityExceptions.EntityException import io.cloudstate.javasupport.{EntityFactory, ServiceCallFactory} /** diff --git a/java-support/src/main/scala/io/cloudstate/javasupport/impl/eventsourced/EventSourcedImpl.scala b/java-support/src/main/scala/io/cloudstate/javasupport/impl/eventsourced/EventSourcedImpl.scala index ccdb3e06f..94e71d39c 100644 --- a/java-support/src/main/scala/io/cloudstate/javasupport/impl/eventsourced/EventSourcedImpl.scala +++ b/java-support/src/main/scala/io/cloudstate/javasupport/impl/eventsourced/EventSourcedImpl.scala @@ -32,12 +32,12 @@ import io.cloudstate.javasupport.impl.{ AbstractEffectContext, ActivatableContext, AnySupport, + EntityExceptions, FailInvoked, MetadataImpl, ResolvedEntityFactory, ResolvedServiceMethod } -import io.cloudstate.protocol.entity.{Command, Failure} import io.cloudstate.protocol.event_sourced.EventSourcedStreamIn.Message.{ Command => InCommand, Empty => InEmpty, @@ -69,6 +69,7 @@ final class EventSourcedStatefulService(val factory: EventSourcedEntityFactory, this } +/* object EventSourcedImpl { final case class EntityException(entityId: String, commandId: Long, commandName: String, message: String) extends RuntimeException(message) @@ -108,13 +109,14 @@ object EventSourcedImpl { case _ => "Terminating entity due to unexpected failure" } } + */ final class EventSourcedImpl(_system: ActorSystem, _services: Map[String, EventSourcedStatefulService], rootContext: Context, configuration: Configuration) extends EventSourced { - import EventSourcedImpl._ + import EntityExceptions._ private final val system = _system private final val services = _services.iterator diff --git a/java-support/src/main/scala/io/cloudstate/javasupport/impl/valueentity/AnnotationBasedValueEntitySupport.scala b/java-support/src/main/scala/io/cloudstate/javasupport/impl/valueentity/AnnotationBasedValueEntitySupport.scala index 43a651c59..9ddde3a01 100644 --- a/java-support/src/main/scala/io/cloudstate/javasupport/impl/valueentity/AnnotationBasedValueEntitySupport.scala +++ b/java-support/src/main/scala/io/cloudstate/javasupport/impl/valueentity/AnnotationBasedValueEntitySupport.scala @@ -16,22 +16,14 @@ package io.cloudstate.javasupport.impl.valueentity -import java.lang.reflect.{Constructor, InvocationTargetException, Method} +import java.lang.reflect.{Constructor, InvocationTargetException} import java.util.Optional import com.google.protobuf.{Descriptors, Any => JavaPbAny} import io.cloudstate.javasupport.{Metadata, ServiceCall, ServiceCallFactory} -import io.cloudstate.javasupport.valueentity.{ - CommandContext, - CommandHandler, - ValueEntity, - ValueEntityContext, - ValueEntityCreationContext, - ValueEntityFactory, - ValueEntityHandler -} +import io.cloudstate.javasupport.valueentity._ import io.cloudstate.javasupport.impl.ReflectionHelper.{InvocationContext, MainArgumentParameterHandler} -import io.cloudstate.javasupport.impl.valueentity.ValueEntityImpl.EntityException +import io.cloudstate.javasupport.impl.EntityExceptions.EntityException import io.cloudstate.javasupport.impl.{AnySupport, ReflectionHelper, ResolvedEntityFactory, ResolvedServiceMethod} /** diff --git a/java-support/src/main/scala/io/cloudstate/javasupport/impl/valueentity/ValueEntityImpl.scala b/java-support/src/main/scala/io/cloudstate/javasupport/impl/valueentity/ValueEntityImpl.scala index 41509f7ed..f33b3669f 100644 --- a/java-support/src/main/scala/io/cloudstate/javasupport/impl/valueentity/ValueEntityImpl.scala +++ b/java-support/src/main/scala/io/cloudstate/javasupport/impl/valueentity/ValueEntityImpl.scala @@ -36,7 +36,6 @@ import io.cloudstate.protocol.value_entity.ValueEntityStreamIn.Message.{ } import io.cloudstate.protocol.value_entity.ValueEntityStreamOut.Message.{Failure => OutFailure, Reply => OutReply} import io.cloudstate.protocol.value_entity._ -import io.cloudstate.protocol.entity.{Command, Failure} import scala.compat.java8.OptionConverters._ import scala.util.control.NonFatal @@ -56,53 +55,13 @@ final class ValueEntityStatefulService(val factory: ValueEntityFactory, override final val entityType = io.cloudstate.protocol.value_entity.ValueEntityProtocol.name } -object ValueEntityImpl { - final case class EntityException(entityId: String, commandId: Long, commandName: String, message: String) - extends RuntimeException(message) - - object EntityException { - def apply(message: String): EntityException = - EntityException(entityId = "", commandId = 0, commandName = "", message) - - def apply(command: Command, message: String): EntityException = - EntityException(command.entityId, command.id, command.name, message) - - def apply(context: CommandContext[_], message: String): EntityException = - EntityException(context.entityId, context.commandId, context.commandName, message) - } - - object ProtocolException { - def apply(message: String): EntityException = - EntityException(entityId = "", commandId = 0, commandName = "", "Protocol error: " + message) - - def apply(init: ValueEntityInit, message: String): EntityException = - EntityException(init.entityId, commandId = 0, commandName = "", "Protocol error: " + message) - - def apply(command: Command, message: String): EntityException = - EntityException(command.entityId, command.id, command.name, "Protocol error: " + message) - } - - def failure(cause: Throwable): Failure = cause match { - case e: EntityException => Failure(e.commandId, e.message) - case e => Failure(description = "Unexpected failure: " + e.getMessage) - } - - def failureMessage(cause: Throwable): String = cause match { - case EntityException(entityId, commandId, commandName, _) => - val commandDescription = if (commandId != 0) s" for command [$commandName]" else "" - val entityDescription = if (entityId.nonEmpty) s"entity [$entityId]" else "entity" - s"Terminating $entityDescription due to unexpected failure$commandDescription" - case _ => "Terminating entity due to unexpected failure" - } -} - final class ValueEntityImpl(_system: ActorSystem, _services: Map[String, ValueEntityStatefulService], rootContext: Context, configuration: Configuration) extends ValueEntityProtocol { - import ValueEntityImpl._ + import EntityExceptions._ private final val system = _system private final implicit val ec = system.dispatcher @@ -112,13 +71,11 @@ final class ValueEntityImpl(_system: ActorSystem, /** * One stream will be established per active entity. * Once established, the first message sent will be Init, which contains the entity ID, and, - * a state if the entity has previously persisted one. The entity is expected to apply the - * received state to its state. Once the Init message is sent, one to many commands are sent, - * with new commands being sent as new requests for the entity come in. The entity is expected - * to reply to each command with exactly one reply message. The entity should reply in order - * and any state update that the entity requests to be persisted the entity should handle itself. - * The entity handles state update by replacing its own state with the update, - * as if they had arrived as state update when the stream was being replayed on load. + * a state if the entity has previously persisted one. Once the Init message is sent, one to + * many commands are sent to the entity. Each request coming in leads to a new command being sent + * to the entity. The entity is expected to reply to each command with exactly one reply message. + * The entity should process commands and reply to commands in the order they came + * in. When processing a command the entity can read and persist (update or delete) the state. */ override def handle( in: akka.stream.scaladsl.Source[ValueEntityStreamIn, akka.NotUsed] diff --git a/tck/src/main/scala/io/cloudstate/tck/CloudStateTCK.scala b/tck/src/main/scala/io/cloudstate/tck/CloudStateTCK.scala index 0886bacd8..ace185985 100644 --- a/tck/src/main/scala/io/cloudstate/tck/CloudStateTCK.scala +++ b/tck/src/main/scala/io/cloudstate/tck/CloudStateTCK.scala @@ -211,7 +211,13 @@ class CloudStateTCK(description: String, settings: CloudStateTCK.Settings) events(eventValues: _*).withSnapshot(persisted(snapshotValue)) def sideEffects(ids: String*): Effects = - ids.foldLeft(Effects.empty) { case (e, id) => e.withSideEffect(ServiceTwo, "Call", Request(id)) } + createSideEffects(synchronous = false, ids) + + def synchronousSideEffects(ids: String*): Effects = + createSideEffects(synchronous = true, ids) + + def createSideEffects(synchronous: Boolean, ids: Seq[String]): Effects = + ids.foldLeft(Effects.empty) { case (e, id) => e.withSideEffect(ServiceTwo, "Call", Request(id), synchronous) } "verify initial empty state" in eventSourcedTest { id => protocol.eventSourced @@ -346,7 +352,7 @@ class CloudStateTCK(description: String, settings: CloudStateTCK.Settings) .connect() .send(init(EventSourcedTckModel.name, id)) .send(command(1, id, "Process", Request(id, Seq(sideEffectTo(id))))) - .expect(reply(1, Response(), sideEffect(EventSourcedTwo.name, "Call", Request(id)))) + .expect(reply(1, Response(), sideEffects(id))) .passivate() } @@ -355,7 +361,7 @@ class CloudStateTCK(description: String, settings: CloudStateTCK.Settings) .connect() .send(init(EventSourcedTckModel.name, id)) .send(command(1, id, "Process", Request(id, Seq(sideEffectTo(id, synchronous = true))))) - .expect(reply(1, Response(), sideEffect(EventSourcedTwo.name, "Call", Request(id), synchronous = true))) + .expect(reply(1, Response(), synchronousSideEffects(id))) .passivate() } @@ -364,7 +370,7 @@ class CloudStateTCK(description: String, settings: CloudStateTCK.Settings) .connect() .send(init(EventSourcedTckModel.name, id)) .send(command(1, id, "Process", Request(id, Seq(sideEffectTo(id), forwardTo(id))))) - .expect(forward(1, ServiceTwo, "Call", Request(id), sideEffect(ServiceTwo, "Call", Request(id)))) + .expect(forward(1, ServiceTwo, "Call", Request(id), sideEffects(id))) .passivate() } @@ -722,8 +728,17 @@ class CloudStateTCK(description: String, settings: CloudStateTCK.Settings) def delete(): Effects = Effects.empty.withDeleteAction() + //def sideEffects(ids: String*): Effects = + // ids.foldLeft(Effects.empty) { case (e, id) => e.withSideEffect(ServiceTwo, "Call", Request(id)) } + def sideEffects(ids: String*): Effects = - ids.foldLeft(Effects.empty) { case (e, id) => e.withSideEffect(ServiceTwo, "Call", Request(id)) } + createSideEffects(synchronous = false, ids) + + def synchronousSideEffects(ids: String*): Effects = + createSideEffects(synchronous = true, ids) + + def createSideEffects(synchronous: Boolean, ids: Seq[String]): Effects = + ids.foldLeft(Effects.empty) { case (e, id) => e.withSideEffect(ServiceTwo, "Call", Request(id), synchronous) } "verify initial empty state" in valueEntityTest { id => protocol.valueEntity @@ -828,7 +843,7 @@ class CloudStateTCK(description: String, settings: CloudStateTCK.Settings) .connect() .send(init(ValueEntityTckModel.name, id)) .send(command(1, id, "Process", Request(id, Seq(sideEffectTo(id))))) - .expect(reply(1, Response(), sideEffect(ServiceTwo, "Call", Request(id)))) + .expect(reply(1, Response(), sideEffects(id))) .passivate() } @@ -848,7 +863,7 @@ class CloudStateTCK(description: String, settings: CloudStateTCK.Settings) .connect() .send(init(ValueEntityTckModel.name, id)) .send(command(1, id, "Process", Request(id, Seq(sideEffectTo(id, synchronous = true))))) - .expect(reply(1, Response(), sideEffect(ServiceTwo, "Call", Request(id), synchronous = true))) + .expect(reply(1, Response(), synchronousSideEffects(id))) .passivate() } @@ -875,7 +890,7 @@ class CloudStateTCK(description: String, settings: CloudStateTCK.Settings) .connect() .send(init(ValueEntityTckModel.name, id)) .send(command(1, id, "Process", Request(id, Seq(sideEffectTo(id), forwardTo(id))))) - .expect(forward(1, ServiceTwo, "Call", Request(id), sideEffect(ServiceTwo, "Call", Request(id)))) + .expect(forward(1, ServiceTwo, "Call", Request(id), sideEffects(id))) .passivate() } diff --git a/testkit/src/main/scala/io/cloudstate/testkit/entity/EntityMessages.scala b/testkit/src/main/scala/io/cloudstate/testkit/entity/EntityMessages.scala new file mode 100644 index 000000000..37282e703 --- /dev/null +++ b/testkit/src/main/scala/io/cloudstate/testkit/entity/EntityMessages.scala @@ -0,0 +1,79 @@ +/* + * Copyright 2019 Lightbend Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.cloudstate.testkit.entity + +import com.google.protobuf.any.{Any => ScalaPbAny} +import com.google.protobuf.empty.{Empty => ScalaPbEmpty} +import com.google.protobuf.{StringValue, Any => JavaPbAny, Empty => JavaPbEmpty, Message => JavaPbMessage} +import io.cloudstate.protocol.entity._ +import scalapb.{GeneratedMessage => ScalaPbMessage} + +object EntityMessages extends EntityMessages + +trait EntityMessages { + val EmptyJavaMessage: JavaPbMessage = JavaPbEmpty.getDefaultInstance + val EmptyScalaMessage: ScalaPbMessage = ScalaPbEmpty.defaultInstance + + def clientActionReply(payload: Option[ScalaPbAny]): Option[ClientAction] = + Some(ClientAction(ClientAction.Action.Reply(Reply(payload)))) + + def clientActionForward(service: String, command: String, payload: Option[ScalaPbAny]): Option[ClientAction] = + Some(ClientAction(ClientAction.Action.Forward(Forward(service, command, payload)))) + + def clientActionFailure(description: String): Option[ClientAction] = + clientActionFailure(id = 0, description) + + def clientActionFailure(id: Long, description: String): Option[ClientAction] = + clientActionFailure(id, description, restart = false) + + def clientActionFailure(id: Long, description: String, restart: Boolean): Option[ClientAction] = + Some(ClientAction(ClientAction.Action.Failure(Failure(id, description, restart)))) + + def sideEffect(service: String, command: String, payload: JavaPbMessage): SideEffect = + sideEffect(service, command, messagePayload(payload), synchronous = false) + + def sideEffect(service: String, command: String, payload: JavaPbMessage, synchronous: Boolean): SideEffect = + sideEffect(service, command, messagePayload(payload), synchronous) + + def sideEffect(service: String, command: String, payload: ScalaPbMessage): SideEffect = + sideEffect(service, command, messagePayload(payload), synchronous = false) + + def sideEffect(service: String, command: String, payload: ScalaPbMessage, synchronous: Boolean): SideEffect = + sideEffect(service, command, messagePayload(payload), synchronous) + + def sideEffect(service: String, command: String, payload: Option[ScalaPbAny], synchronous: Boolean): SideEffect = + SideEffect(service, command, payload, synchronous) + + def messagePayload(message: JavaPbMessage): Option[ScalaPbAny] = + Option(message).map(protobufAny) + + def messagePayload(message: ScalaPbMessage): Option[ScalaPbAny] = + Option(message).map(protobufAny) + + def protobufAny(message: JavaPbMessage): ScalaPbAny = message match { + case javaPbAny: JavaPbAny => ScalaPbAny.fromJavaProto(javaPbAny) + case _ => ScalaPbAny("type.googleapis.com/" + message.getDescriptorForType.getFullName, message.toByteString) + } + + def protobufAny(message: ScalaPbMessage): ScalaPbAny = message match { + case scalaPbAny: ScalaPbAny => scalaPbAny + case _ => ScalaPbAny("type.googleapis.com/" + message.companion.scalaDescriptor.fullName, message.toByteString) + } + + def primitiveString(value: String): ScalaPbAny = + ScalaPbAny("p.cloudstate.io/string", StringValue.of(value).toByteString) +} diff --git a/testkit/src/main/scala/io/cloudstate/testkit/eventsourced/EventSourcedMessages.scala b/testkit/src/main/scala/io/cloudstate/testkit/eventsourced/EventSourcedMessages.scala index 858e86cc8..b5f416cf3 100644 --- a/testkit/src/main/scala/io/cloudstate/testkit/eventsourced/EventSourcedMessages.scala +++ b/testkit/src/main/scala/io/cloudstate/testkit/eventsourced/EventSourcedMessages.scala @@ -17,13 +17,13 @@ package io.cloudstate.testkit.eventsourced import com.google.protobuf.any.{Any => ScalaPbAny} -import com.google.protobuf.empty.{Empty => ScalaPbEmpty} -import com.google.protobuf.{StringValue, Any => JavaPbAny, Empty => JavaPbEmpty, Message => JavaPbMessage} +import com.google.protobuf.{Message => JavaPbMessage} import io.cloudstate.protocol.entity._ import io.cloudstate.protocol.event_sourced._ +import io.cloudstate.testkit.entity.EntityMessages import scalapb.{GeneratedMessage => ScalaPbMessage} -object EventSourcedMessages { +object EventSourcedMessages extends EntityMessages { import EventSourcedStreamIn.{Message => InMessage} import EventSourcedStreamOut.{Message => OutMessage} @@ -68,8 +68,6 @@ object EventSourcedMessages { } val EmptyInMessage: InMessage = InMessage.Empty - val EmptyJavaMessage: JavaPbMessage = JavaPbEmpty.getDefaultInstance - val EmptyScalaMessage: ScalaPbMessage = ScalaPbEmpty.defaultInstance def init(serviceName: String, entityId: String): InMessage = init(serviceName, entityId, None) @@ -155,58 +153,9 @@ object EventSourcedMessages { def failure(id: Long, description: String): OutMessage = OutMessage.Failure(Failure(id, description)) - def clientActionReply(payload: Option[ScalaPbAny]): Option[ClientAction] = - Some(ClientAction(ClientAction.Action.Reply(Reply(payload)))) - - def clientActionForward(service: String, command: String, payload: Option[ScalaPbAny]): Option[ClientAction] = - Some(ClientAction(ClientAction.Action.Forward(Forward(service, command, payload)))) - - def clientActionFailure(description: String): Option[ClientAction] = - clientActionFailure(id = 0, description) - - def clientActionFailure(id: Long, description: String): Option[ClientAction] = - clientActionFailure(id, description, restart = false) - - def clientActionFailure(id: Long, description: String, restart: Boolean): Option[ClientAction] = - Some(ClientAction(ClientAction.Action.Failure(Failure(id, description, restart)))) - def persist(event: JavaPbMessage, events: JavaPbMessage*): Effects = Effects.empty.withEvents(event, events: _*) def persist(event: ScalaPbMessage, events: ScalaPbMessage*): Effects = Effects.empty.withEvents(event, events: _*) - - def sideEffect(service: String, command: String, payload: JavaPbMessage): Effects = - sideEffect(service, command, messagePayload(payload), synchronous = false) - - def sideEffect(service: String, command: String, payload: JavaPbMessage, synchronous: Boolean): Effects = - sideEffect(service, command, messagePayload(payload), synchronous) - - def sideEffect(service: String, command: String, payload: ScalaPbMessage): Effects = - sideEffect(service, command, messagePayload(payload), synchronous = false) - - def sideEffect(service: String, command: String, payload: ScalaPbMessage, synchronous: Boolean): Effects = - sideEffect(service, command, messagePayload(payload), synchronous) - - def sideEffect(service: String, command: String, payload: Option[ScalaPbAny], synchronous: Boolean): Effects = - Effects.empty.withSideEffect(service, command, payload, synchronous) - - def messagePayload(message: JavaPbMessage): Option[ScalaPbAny] = - Option(message).map(protobufAny) - - def messagePayload(message: ScalaPbMessage): Option[ScalaPbAny] = - Option(message).map(protobufAny) - - def protobufAny(message: JavaPbMessage): ScalaPbAny = message match { - case javaPbAny: JavaPbAny => ScalaPbAny.fromJavaProto(javaPbAny) - case _ => ScalaPbAny("type.googleapis.com/" + message.getDescriptorForType.getFullName, message.toByteString) - } - - def protobufAny(message: ScalaPbMessage): ScalaPbAny = message match { - case scalaPbAny: ScalaPbAny => scalaPbAny - case _ => ScalaPbAny("type.googleapis.com/" + message.companion.scalaDescriptor.fullName, message.toByteString) - } - - def primitiveString(value: String): ScalaPbAny = - ScalaPbAny("p.cloudstate.io/string", StringValue.of(value).toByteString) } diff --git a/testkit/src/main/scala/io/cloudstate/testkit/valuentity/ValueEntityMessages.scala b/testkit/src/main/scala/io/cloudstate/testkit/valuentity/ValueEntityMessages.scala index 8932208b7..fd6921273 100644 --- a/testkit/src/main/scala/io/cloudstate/testkit/valuentity/ValueEntityMessages.scala +++ b/testkit/src/main/scala/io/cloudstate/testkit/valuentity/ValueEntityMessages.scala @@ -17,13 +17,13 @@ package io.cloudstate.testkit.valuentity import com.google.protobuf.any.{Any => ScalaPbAny} -import com.google.protobuf.empty.{Empty => ScalaPbEmpty} -import com.google.protobuf.{Any => JavaPbAny, Empty => JavaPbEmpty, Message => JavaPbMessage} +import com.google.protobuf.{Message => JavaPbMessage} import io.cloudstate.protocol.entity._ import io.cloudstate.protocol.value_entity._ +import io.cloudstate.testkit.entity.EntityMessages import scalapb.{GeneratedMessage => ScalaPbMessage} -object ValueEntityMessages { +object ValueEntityMessages extends EntityMessages { import ValueEntityStreamIn.{Message => InMessage} import ValueEntityStreamOut.{Message => OutMessage} import ValueEntityAction.Action._ @@ -39,10 +39,13 @@ object ValueEntityMessages { def withDeleteAction(): Effects = copy(crudAction = Some(ValueEntityAction(Delete(ValueEntityDelete())))) - def withSideEffect(service: String, command: String, message: ScalaPbMessage): Effects = - withSideEffect(service, command, messagePayload(message), synchronous = false) + def withSideEffect(service: String, command: String, message: ScalaPbMessage, synchronous: Boolean): Effects = + withSideEffect(service, command, messagePayload(message), synchronous) - def withSideEffect(service: String, command: String, payload: Option[ScalaPbAny], synchronous: Boolean): Effects = + private def withSideEffect(service: String, + command: String, + payload: Option[ScalaPbAny], + synchronous: Boolean): Effects = copy(sideEffects = sideEffects :+ SideEffect(service, command, payload, synchronous)) } @@ -51,8 +54,6 @@ object ValueEntityMessages { } val EmptyInMessage: InMessage = InMessage.Empty - val EmptyJavaMessage: JavaPbMessage = JavaPbEmpty.getDefaultInstance - val EmptyScalaMessage: ScalaPbMessage = ScalaPbEmpty.defaultInstance def init(serviceName: String, entityId: String): InMessage = init(serviceName, entityId, Some(ValueEntityInitState())) @@ -93,24 +94,28 @@ object ValueEntityMessages { def reply(id: Long, payload: ScalaPbMessage, effects: Effects): OutMessage = reply(id, messagePayload(payload), effects) - def reply(id: Long, payload: Option[ScalaPbAny], crudAction: Option[ValueEntityAction]): OutMessage = + private def reply(id: Long, payload: Option[ScalaPbAny], crudAction: Option[ValueEntityAction]): OutMessage = OutMessage.Reply(ValueEntityReply(id, clientActionReply(payload), Seq.empty, crudAction)) - def reply(id: Long, payload: Option[ScalaPbAny], effects: Effects): OutMessage = + private def reply(id: Long, payload: Option[ScalaPbAny], effects: Effects): OutMessage = OutMessage.Reply(ValueEntityReply(id, clientActionReply(payload), effects.sideEffects, effects.crudAction)) - def replyAction(id: Long, action: Option[ClientAction], effects: Effects): OutMessage = - OutMessage.Reply(ValueEntityReply(id, action, effects.sideEffects, effects.crudAction)) - def forward(id: Long, service: String, command: String, payload: ScalaPbMessage): OutMessage = forward(id, service, command, payload, Effects.empty) def forward(id: Long, service: String, command: String, payload: ScalaPbMessage, effects: Effects): OutMessage = forward(id, service, command, messagePayload(payload), effects) - def forward(id: Long, service: String, command: String, payload: Option[ScalaPbAny], effects: Effects): OutMessage = + private def forward(id: Long, + service: String, + command: String, + payload: Option[ScalaPbAny], + effects: Effects): OutMessage = replyAction(id, clientActionForward(service, command, payload), effects) + private def replyAction(id: Long, action: Option[ClientAction], effects: Effects): OutMessage = + OutMessage.Reply(ValueEntityReply(id, action, effects.sideEffects, effects.crudAction)) + def actionFailure(id: Long, description: String): OutMessage = OutMessage.Reply(ValueEntityReply(id, clientActionFailure(id, description))) @@ -120,27 +125,6 @@ object ValueEntityMessages { def failure(id: Long, description: String): OutMessage = OutMessage.Failure(Failure(id, description)) - def clientActionReply(payload: Option[ScalaPbAny]): Option[ClientAction] = - Some(ClientAction(ClientAction.Action.Reply(Reply(payload)))) - - def clientActionFailure(description: String): Option[ClientAction] = - clientActionFailure(id = 0, description) - - def clientActionFailure(id: Long, description: String): Option[ClientAction] = - Some(ClientAction(ClientAction.Action.Failure(Failure(id, description)))) - - def clientActionForward(service: String, command: String, payload: Option[ScalaPbAny]): Option[ClientAction] = - Some(ClientAction(ClientAction.Action.Forward(Forward(service, command, payload)))) - - def sideEffect(service: String, command: String, payload: ScalaPbMessage, synchronous: Boolean): Effects = - sideEffect(service, command, messagePayload(payload), synchronous) - - def sideEffect(service: String, command: String, payload: ScalaPbMessage): Effects = - sideEffect(service, command, messagePayload(payload), synchronous = false) - - def sideEffect(service: String, command: String, payload: Option[ScalaPbAny], synchronous: Boolean): Effects = - Effects.empty.withSideEffect(service, command, payload, synchronous) - def update(state: JavaPbMessage): Effects = Effects.empty.withUpdateAction(state) @@ -149,21 +133,4 @@ object ValueEntityMessages { def delete(): Effects = Effects.empty.withDeleteAction() - - def messagePayload(message: JavaPbMessage): Option[ScalaPbAny] = - Option(message).map(protobufAny) - - def messagePayload(message: ScalaPbMessage): Option[ScalaPbAny] = - Option(message).map(protobufAny) - - def protobufAny(message: JavaPbMessage): ScalaPbAny = message match { - case javaPbAny: JavaPbAny => ScalaPbAny.fromJavaProto(javaPbAny) - case _ => ScalaPbAny("type.googleapis.com/" + message.getDescriptorForType.getFullName, message.toByteString) - } - - def protobufAny(message: ScalaPbMessage): ScalaPbAny = message match { - case scalaPbAny: ScalaPbAny => scalaPbAny - case _ => ScalaPbAny("type.googleapis.com/" + message.companion.scalaDescriptor.fullName, message.toByteString) - } - } From 517ee376154537a1d87f3e0771d4ec21f8c84be3 Mon Sep 17 00:00:00 2001 From: Guy Youansi Date: Thu, 29 Oct 2020 13:17:07 +0100 Subject: [PATCH 73/93] integrate tests for entity in circleci --- .circleci/config.yml | 12 ++ bin/run-java-entity-shopping-cart-test.sh | 229 ++++++++++++++++++++++ 2 files changed, 241 insertions(+) create mode 100755 bin/run-java-entity-shopping-cart-test.sh diff --git a/.circleci/config.yml b/.circleci/config.yml index 9fd8deffd..737de609a 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -257,6 +257,12 @@ jobs: - run: name: Run shopping cart test (Cassandra) command: bin/run-java-shopping-cart-test.sh Cassandra --no-build + - run: + name: Run entity shopping cart test (InMemory) + command: bin/run-java-entity-shopping-cart-test.sh InMemory --no-build + - run: + name: Run entity shopping cart test (Postgres) + command: bin/run-java-entity-shopping-cart-test.sh Postgres --no-build native-image-smoke-test: parameters: @@ -300,6 +306,9 @@ jobs: - run: name: Load in memory image command: docker image load < /tmp/workspace/cloudstate-proxy-native-core.tar + - run: + name: Run entity shopping cart test (InMemory) + command: bin/run-java-entity-shopping-cart-test.sh InMemory --no-build - when: condition: equal: [ Postgres, << parameters.proxy-store >> ] @@ -318,6 +327,9 @@ jobs: command: | bin/install-cassandra.sh docker image load < /tmp/workspace/cloudstate-proxy-native-cassandra.tar + - run: + name: Run entity shopping cart test (Postgres) + command: bin/run-java-entity-shopping-cart-test.sh Postgres --no-build - run: name: Run shopping cart test (<< parameters.proxy-store >>) command: bin/run-java-shopping-cart-test.sh << parameters.proxy-store >> --no-build diff --git a/bin/run-java-entity-shopping-cart-test.sh b/bin/run-java-entity-shopping-cart-test.sh new file mode 100755 index 000000000..f8333f8f9 --- /dev/null +++ b/bin/run-java-entity-shopping-cart-test.sh @@ -0,0 +1,229 @@ +#!/usr/bin/env bash +# +# Run the Java Entity shopping cart sample as a test, with a given persistence store. +# +# run-java-entity-shopping-cart-test.sh [inmemory|postgres] + +set -e + +echo +echo "=== Running java-entity-shopping-cart test ===" +echo + +# process arguments +shopt -s nocasematch +build=true +logs=false +delete=true +declare -a residual +while [[ $# -gt 0 ]] ; do + case "$1" in + --no-build ) build=false; shift ;; + --logs ) logs=true; shift ;; + --no-delete ) delete=false; shift ;; + *) residual=("${residual[@]}" "$1"); shift ;; + esac +done +set -- "${residual[@]}" +store="$1" + +# Build the java entity shopping cart +if [ "$build" == true ] ; then + echo + echo "Building java-entity-shopping-cart ..." + [ -f docker-env.sh ] && source docker-env.sh + sbt -Ddocker.username=cloudstatedev -Ddocker.tag=dev java-valueentity-shopping-cart/docker:publishLocal +fi + +statefulstore="inmemory" +statefulservice="shopping-cart-$statefulstore" + +case "$store" in + + postgres ) # deploy the entity-shopping-cart with postgres store + + statefulstore="postgres" + statefulservice="shopping-cart-$statefulstore" + + kubectl apply -f - < /dev/null || [ $attempts -eq 0 ] ; do + ((attempts--)) + sleep 1 +done +kubectl get deployment $deployment + +function fail_with_details { + echo + echo "=== Operator logs ===" + echo + kubectl logs -l control-plane=controller-manager -n cloudstate-system -c manager --tail=-1 + echo + echo "=== Deployment description ===" + echo + kubectl describe deployment/$deployment + echo + echo "=== Pods description ===" + echo + kubectl describe pods + echo + echo "=== Proxy logs ===" + echo + kubectl logs -l cloudstate.io/stateful-service=$statefulservice -c cloudstate-sidecar --tail=-1 + echo + echo "=== User function logs ===" + echo + kubectl logs -l cloudstate.io/stateful-service=$statefulservice -c user-function --tail=-1 + exit 1 +} + +# Wait for the deployment to be available +echo +echo "Waiting for deployment to be ready..." +kubectl wait --for=condition=available --timeout=1m deployment/$deployment || fail_with_details +kubectl get deployment $deployment + +# Scale up the deployment, to test with akka clustering +echo +echo "Scaling deployment..." +kubectl scale --replicas=3 deployment/$deployment +kubectl get deployment $deployment + +# Wait for the scaled deployment to be available +echo +echo "Waiting for deployment to be ready..." +kubectl wait --for=condition=available --timeout=5m deployment/$deployment || fail_with_details +kubectl get deployment $deployment + +# Expose the entity-shopping-cart service +nodeport="$deployment-node-port" +kubectl expose deployment $deployment --name=$nodeport --port=8013 --type=NodePort + +# Get the URL for the entity-shopping-cart service +url=$(minikube service $nodeport --url) + +# Now we use the REST interface to test it (because it's easier to use curl than a grpc +# command line client) +empty_cart='{"items":[]}' +post='{"productId":"foo","name":"A foo","quantity":10}' +non_empty_cart='{"items":[{"productId":"foo","name":"A foo","quantity":10}]}' + +# Iterate over multiple entities to be routed to different nodes +for i in {1..9} ; do + cart_id="test$i" + echo + echo "Testing entity shopping cart $cart_id ..." + + initial_cart=$(curl -s $url/ve/carts/$cart_id) + if [[ "$empty_cart" != "$initial_cart" ]] + then + echo "Expected '$empty_cart'" + echo "But got '$initial_cart'" + fail_with_details + else + echo "Initial request for $cart_id succeeded." + fi + + curl -s -X POST $url/ve/cart/$cart_id/items/add -H "Content-Type: application/json" -d "$post" > /dev/null + + new_cart=$(curl -s $url/ve/carts/$cart_id) + if [[ "$non_empty_cart" != "$new_cart" ]] + then + echo "Expected '$non_empty_cart'" + echo "But got '$new_cart'" + fail_with_details + else + echo "Entity shopping cart update for $cart_id succeeded." + fi + + curl -s -X POST $url/ve/cart/$cart_id/remove -H "Content-Type: application/json" -d '{"userId": "'"$cart_id"'"}' > /dev/null + + deleted_cart=$(curl -s $url/ve/carts/$cart_id) + if [[ "$empty_cart" != "$deleted_cart" ]] + then + echo "Expected '$empty_cart'" + echo "But got '$deleted_cart'" + fail_with_details + else + echo echo "Entity shopping cart delete for $cart_id succeeded." + fi +done + +# Print proxy logs +if [ "$logs" == true ] ; then + echo + echo "=== Proxy logs ===" + echo + kubectl logs -l cloudstate.io/stateful-service=$statefulservice -c cloudstate-sidecar --tail=-1 +fi + +# Delete entity-shopping-cart +if [ "$delete" == true ] ; then + echo + echo "Deleting $statefulservice ..." + kubectl delete service $deployment + kubectl delete service $nodeport + kubectl delete statefulservice $statefulservice + kubectl delete statefulstore $statefulstore +fi From d4b2493efd723f5df25d73734d2bd0d356655140 Mon Sep 17 00:00:00 2001 From: Guy Youansi Date: Thu, 29 Oct 2020 13:40:38 +0100 Subject: [PATCH 74/93] integrate tests for entity in circleci by building the entity image --- .circleci/config.yml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 737de609a..db3ca269c 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -150,7 +150,8 @@ jobs: sbt 'dockerBuildCore publishLocal' \ 'dockerBuildPostgres publishLocal' \ 'dockerBuildCassandra publishLocal' \ - java-shopping-cart/docker:publishLocal + java-shopping-cart/docker:publishLocal \ + java-valueentity-shopping-cart/docker:publishLocal - save_docker_image: tar_file: cloudstate-sbt-images.tar docker_image: cloudstateio/cloudstate-proxy-core:latest cloudstateio/cloudstate-proxy-postgres:latest cloudstateio/cloudstate-proxy-cassandra:latest cloudstateio/java-shopping-cart:latest @@ -318,6 +319,9 @@ jobs: command: | bin/install-postgres.sh docker image load < /tmp/workspace/cloudstate-proxy-native-postgres.tar + - run: + name: Run entity shopping cart test (Postgres) + command: bin/run-java-entity-shopping-cart-test.sh Postgres --no-build - when: condition: equal: [ Cassandra, << parameters.proxy-store >> ] @@ -327,9 +331,6 @@ jobs: command: | bin/install-cassandra.sh docker image load < /tmp/workspace/cloudstate-proxy-native-cassandra.tar - - run: - name: Run entity shopping cart test (Postgres) - command: bin/run-java-entity-shopping-cart-test.sh Postgres --no-build - run: name: Run shopping cart test (<< parameters.proxy-store >>) command: bin/run-java-shopping-cart-test.sh << parameters.proxy-store >> --no-build From a6bc7d985b2ac3687e0f15536eabb4d6b4f4d99d Mon Sep 17 00:00:00 2001 From: Guy Youansi Date: Thu, 29 Oct 2020 13:57:39 +0100 Subject: [PATCH 75/93] integrate tests for entity in circleci by saving the entity image in the tar ball --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index db3ca269c..32171b04c 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -154,7 +154,7 @@ jobs: java-valueentity-shopping-cart/docker:publishLocal - save_docker_image: tar_file: cloudstate-sbt-images.tar - docker_image: cloudstateio/cloudstate-proxy-core:latest cloudstateio/cloudstate-proxy-postgres:latest cloudstateio/cloudstate-proxy-cassandra:latest cloudstateio/java-shopping-cart:latest + docker_image: cloudstateio/cloudstate-proxy-core:latest cloudstateio/cloudstate-proxy-postgres:latest cloudstateio/cloudstate-proxy-cassandra:latest cloudstateio/java-shopping-cart:latest cloudstateio/java-valueentity-shopping-cart:latest - persist_to_workspace: root: /tmp/workspace paths: From 375f9f9de4ab5ac89a6e639052d341d32dfa831e Mon Sep 17 00:00:00 2001 From: Guy Youansi Date: Thu, 29 Oct 2020 18:09:53 +0100 Subject: [PATCH 76/93] add jdbc config tests --- .../proxy/valueentity/store/JdbcConfig.scala | 4 +- .../valueentity/store/JdbcConfigSpec.scala | 105 ++++++++++++++++++ 2 files changed, 107 insertions(+), 2 deletions(-) create mode 100644 proxy/core/src/test/scala/io/cloudstate/proxy/valueentity/store/JdbcConfigSpec.scala diff --git a/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/JdbcConfig.scala b/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/JdbcConfig.scala index f12551a14..6f59d3973 100644 --- a/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/JdbcConfig.scala +++ b/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/JdbcConfig.scala @@ -28,7 +28,7 @@ class JdbcValueEntityTableColumnNames(config: Config) { val entityId: String = cfg.getString("entityId") val state: String = cfg.getString("state") - override def toString: String = s"JdbcCrudStateTableColumnNames($persistentId,$entityId,$state)" + override def toString: String = s"JdbcValueEntityTableColumnNames($persistentId,$entityId,$state)" } class JdbcValueEntityTableConfiguration(config: Config) { @@ -41,7 +41,7 @@ class JdbcValueEntityTableConfiguration(config: Config) { } val columnNames: JdbcValueEntityTableColumnNames = new JdbcValueEntityTableColumnNames(config) - override def toString: String = s"JdbcCrudStateTableConfiguration($tableName,$schemaName,$columnNames)" + override def toString: String = s"JdbcValueEntityTableColumnNames($tableName,$schemaName,$columnNames)" } object JdbcSlickDatabase { diff --git a/proxy/core/src/test/scala/io/cloudstate/proxy/valueentity/store/JdbcConfigSpec.scala b/proxy/core/src/test/scala/io/cloudstate/proxy/valueentity/store/JdbcConfigSpec.scala new file mode 100644 index 000000000..969acc80d --- /dev/null +++ b/proxy/core/src/test/scala/io/cloudstate/proxy/valueentity/store/JdbcConfigSpec.scala @@ -0,0 +1,105 @@ +/* + * Copyright 2019 Lightbend Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.cloudstate.proxy.valueentity.store + +import com.typesafe.config.{Config, ConfigFactory} +import org.scalatest.{Matchers, WordSpecLike} +import slick.jdbc.JdbcProfile + +class JdbcConfigSpec extends WordSpecLike with Matchers { + private val config: Config = ConfigFactory.parseString(""" + |cloudstate.proxy { + | value-entity-enabled = true + | value-entity-persistence-store { + | store-type = "jdbc" + | jdbc.database.slick { + | profile = "slick.jdbc.PostgresProfile$" + | connectionPool = disabled + | driver = "org.postgresql.Driver" + | url = "jdbc:postgresql://localhost:5432/cloudstate" + | user = "cloudstate" + | password = "cloudstate" + | } + | + | jdbc-state-store { + | tables { + | state { + | tableName = "value_entity_state" + | schemaName = "" + | columnNames { + | persistentId = "persistent_id" + | entityId = "entity_id" + | state = "state" + | } + | } + | } + | } + | } + |} + | + """.stripMargin) + + private val tableStateConfig = new JdbcValueEntityTableConfiguration( + config.getConfig("cloudstate.proxy.value-entity-persistence-store.jdbc-state-store") + ) + + private val testTable = new JdbcValueEntityTable { + override val valueEntityTableCfg: JdbcValueEntityTableConfiguration = tableStateConfig + override val profile: JdbcProfile = slick.jdbc.PostgresProfile + } + + "ValueEntityTable" should { + def columnName(tableName: String, columnName: String) = s"$tableName.$columnName" + + "be configured with a schema name" in { + testTable.ValueEntityTableQuery.baseTableRow.schemaName shouldBe tableStateConfig.schemaName + } + + "be configured with a table name" in { + testTable.ValueEntityTableQuery.baseTableRow.tableName shouldBe tableStateConfig.tableName + } + + "be configured with a columns name" in { + testTable.ValueEntityTableQuery.baseTableRow.persistentId.toString shouldBe columnName( + tableStateConfig.tableName, + tableStateConfig.columnNames.persistentId + ) + testTable.ValueEntityTableQuery.baseTableRow.entityId.toString shouldBe columnName( + tableStateConfig.tableName, + tableStateConfig.columnNames.entityId + ) + testTable.ValueEntityTableQuery.baseTableRow.state.toString shouldBe columnName( + tableStateConfig.tableName, + tableStateConfig.columnNames.state + ) + } + } + + "ValueEntityTableConfig" should { + "be correctly represent as string" in { + testTable.valueEntityTableCfg.toString shouldBe "JdbcValueEntityTableColumnNames(value_entity_state,None,JdbcValueEntityTableColumnNames(persistent_id,entity_id,state))" + } + } + + "JdbcSlickDatabase" should { + "be initialized with a schema name" in { + val slickDatabase = JdbcSlickDatabase(config.getConfig("cloudstate.proxy")) + slickDatabase.database should not be null + slickDatabase.profile should not be null + } + } +} From 87dbd804a7f8f0866a90bca49a09d44a257df45e Mon Sep 17 00:00:00 2001 From: Peter Vlugter Date: Mon, 2 Nov 2020 16:15:10 +1300 Subject: [PATCH 77/93] Update entity smoke test script --- bin/run-java-entity-shopping-cart-test.sh | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/bin/run-java-entity-shopping-cart-test.sh b/bin/run-java-entity-shopping-cart-test.sh index f8333f8f9..ca71565ae 100755 --- a/bin/run-java-entity-shopping-cart-test.sh +++ b/bin/run-java-entity-shopping-cart-test.sh @@ -141,20 +141,21 @@ function fail_with_details { # Wait for the deployment to be available echo echo "Waiting for deployment to be ready..." -kubectl wait --for=condition=available --timeout=1m deployment/$deployment || fail_with_details +kubectl rollout status --timeout=1m deployment/$deployment || fail_with_details kubectl get deployment $deployment # Scale up the deployment, to test with akka clustering echo echo "Scaling deployment..." -kubectl scale --replicas=3 deployment/$deployment +kubectl scale --replicas=3 statefulservice/$statefulservice kubectl get deployment $deployment # Wait for the scaled deployment to be available echo echo "Waiting for deployment to be ready..." -kubectl wait --for=condition=available --timeout=5m deployment/$deployment || fail_with_details +kubectl rollout status --timeout=5m deployment/$deployment || fail_with_details kubectl get deployment $deployment +[ $(kubectl get deployment $deployment -o "jsonpath={.status.availableReplicas}") -eq 3 ] || fail_with_details "Expected 3 available replicas" # Expose the entity-shopping-cart service nodeport="$deployment-node-port" @@ -175,7 +176,12 @@ for i in {1..9} ; do echo echo "Testing entity shopping cart $cart_id ..." - initial_cart=$(curl -s $url/ve/carts/$cart_id) + initial_cart='' + for attempt in {1..5} ; do + initial_cart=$(curl -s $url/ve/carts/$cart_id || echo '') + [ -n "$initial_cart" ] && break || sleep 1 + done + if [[ "$empty_cart" != "$initial_cart" ]] then echo "Expected '$empty_cart'" From 65e5fc8758bad0afd7c14e8dba430260fffb8d35 Mon Sep 17 00:00:00 2001 From: Peter Vlugter Date: Mon, 2 Nov 2020 16:15:34 +1300 Subject: [PATCH 78/93] Fix entity delete in smoke test script --- bin/run-java-entity-shopping-cart-test.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bin/run-java-entity-shopping-cart-test.sh b/bin/run-java-entity-shopping-cart-test.sh index ca71565ae..f1059cfe2 100755 --- a/bin/run-java-entity-shopping-cart-test.sh +++ b/bin/run-java-entity-shopping-cart-test.sh @@ -203,7 +203,7 @@ for i in {1..9} ; do echo "Entity shopping cart update for $cart_id succeeded." fi - curl -s -X POST $url/ve/cart/$cart_id/remove -H "Content-Type: application/json" -d '{"userId": "'"$cart_id"'"}' > /dev/null + curl -s -X POST $url/ve/carts/$cart_id/remove -H "Content-Type: application/json" -d '{"userId": "'"$cart_id"'"}' > /dev/null deleted_cart=$(curl -s $url/ve/carts/$cart_id) if [[ "$empty_cart" != "$deleted_cart" ]] @@ -212,7 +212,7 @@ for i in {1..9} ; do echo "But got '$deleted_cart'" fail_with_details else - echo echo "Entity shopping cart delete for $cart_id succeeded." + echo "Entity shopping cart delete for $cart_id succeeded." fi done From 2f6ba2755fcad2ec8dd923bc64e679b4d536400e Mon Sep 17 00:00:00 2001 From: Peter Vlugter Date: Mon, 2 Nov 2020 16:15:55 +1300 Subject: [PATCH 79/93] Add native-image configuration for value entity table --- .../cloudstate-proxy-jdbc/reflect-config.json.conf | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/proxy/postgres/src/graal/META-INF/native-image/io.cloudstate/cloudstate-proxy-jdbc/reflect-config.json.conf b/proxy/postgres/src/graal/META-INF/native-image/io.cloudstate/cloudstate-proxy-jdbc/reflect-config.json.conf index 950bbdd15..87ea4a83a 100644 --- a/proxy/postgres/src/graal/META-INF/native-image/io.cloudstate/cloudstate-proxy-jdbc/reflect-config.json.conf +++ b/proxy/postgres/src/graal/META-INF/native-image/io.cloudstate/cloudstate-proxy-jdbc/reflect-config.json.conf @@ -7,4 +7,8 @@ name: "io.cloudstate.proxy.jdbc.SlickEnsureValueEntityTablesExistReadyCheck" methods: [{name: "", parameterTypes: ["akka.actor.ActorSystem"]}] } +{ + name: "io.cloudstate.proxy.valueentity.store.JdbcValueEntityTable$ValueEntityTable" + allPublicMethods: true +} ] From 8f1835ab12dcdc7445f7cc084ef974ca862aa01f Mon Sep 17 00:00:00 2001 From: Peter Vlugter Date: Mon, 2 Nov 2020 16:44:32 +1300 Subject: [PATCH 80/93] Reduce value entity connection pool size --- proxy/core/src/main/resources/reference.conf | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/proxy/core/src/main/resources/reference.conf b/proxy/core/src/main/resources/reference.conf index 0cebda4ac..acc5c1e6a 100644 --- a/proxy/core/src/main/resources/reference.conf +++ b/proxy/core/src/main/resources/reference.conf @@ -201,9 +201,9 @@ cloudstate.proxy { # See some tips on thread/connection pool sizing on https://github.com/brettwooldridge/HikariCP/wiki/About-Pool-Sizing # Keep in mind that the number of threads must equal the maximum number of connections. - numThreads = 20 - maxConnections = 20 - minConnections = 20 + numThreads = 5 + maxConnections = 5 + minConnections = 5 # This property controls a user-defined name for the connection pool and appears mainly in logging and JMX # management consoles to identify pools and pool configurations. Default: auto-generated From 62f0500fec0b92b45f748202003e4b7a99fa3b4f Mon Sep 17 00:00:00 2001 From: Guy Youansi Date: Mon, 2 Nov 2020 11:34:24 +0100 Subject: [PATCH 81/93] exclude slick dependency from other core persistence module. change value entity behavior regarding passivation. --- build.sbt | 17 ++- proxy/core/src/main/resources/reference.conf | 2 +- .../proxy/valueentity/ValueEntity.scala | 138 ++++++++++++------ .../ValueEntitySupportFactory.scala | 14 +- .../valueentity/store/JdbcInMemoryStore.scala | 3 +- .../valueentity/store/JdbcRepository.scala | 23 ++- .../ValueEntityPassivateSpec.scala | 106 ++++++++++++++ .../store/EntitySerializerSpec.scala | 46 ++++++ 8 files changed, 282 insertions(+), 67 deletions(-) create mode 100644 proxy/core/src/test/scala/io/cloudstate/proxy/valueentity/ValueEntityPassivateSpec.scala create mode 100644 proxy/core/src/test/scala/io/cloudstate/proxy/valueentity/store/EntitySerializerSpec.scala diff --git a/build.sbt b/build.sbt index 8f6a2a83a..aea92ab32 100644 --- a/build.sbt +++ b/build.sbt @@ -56,6 +56,7 @@ val DockerBaseImageVersion = "adoptopenjdk/openjdk11:debianslim-jre" val DockerBaseImageJavaLibraryPath = "${JAVA_HOME}/lib" val SlickVersion = "3.3.2" val SlickHikariVersion = "3.3.2" +val AkkaPersistenceJdbcVersion = "3.5.2" val excludeTheseDependencies: Seq[ExclusionRule] = Seq( ExclusionRule("io.netty", "netty"), // grpc-java is using grpc-netty-shaded @@ -78,6 +79,14 @@ def akkaDiscoveryDependency(name: String, excludeThese: ExclusionRule*) = def akkaPersistenceCassandraDependency(name: String, excludeThese: ExclusionRule*) = "com.typesafe.akka" %% name % AkkaPersistenceCassandraVersion excludeAll ((excludeTheseDependencies ++ excludeThese): _*) +def akkaPersistenceJdbcDependency(name: String, excludeThese: ExclusionRule*) = + "com.github.dnvriend" %% name % AkkaPersistenceJdbcVersion excludeAll (excludeThese: _*) + +val excludeSlickDependencies: Seq[ExclusionRule] = Seq( + ExclusionRule("com.typesafe.slick", "slick"), // slick core + ExclusionRule("com.typesafe.slick", "slick-hikaricp") //slick hikari +) + def common: Seq[Setting[_]] = automateHeaderSettings(Compile, Test) ++ Seq( headerMappings := headerMappings.value ++ Seq( de.heikoseeberger.sbtheader.FileType("proto") -> HeaderCommentStyle.cppStyleLineComment, @@ -435,6 +444,7 @@ lazy val `proxy-spanner` = (project in file("proxy/spanner")) common, name := "cloudstate-proxy-spanner", dependencyOverrides += "io.grpc" % "grpc-netty-shaded" % GrpcNettyShadedVersion, + excludeDependencies ++= excludeSlickDependencies, libraryDependencies ++= Seq( "com.lightbend.akka" %% "akka-persistence-spanner" % AkkaPersistenceSpannerVersion, akkaDependency("akka-cluster-typed"), // Transitive dependency of akka-persistence-spanner @@ -457,6 +467,7 @@ lazy val `proxy-cassandra` = (project in file("proxy/cassandra")) common, name := "cloudstate-proxy-cassandra", dependencyOverrides += "io.grpc" % "grpc-netty-shaded" % GrpcNettyShadedVersion, + excludeDependencies ++= excludeSlickDependencies, libraryDependencies ++= Seq( akkaPersistenceCassandraDependency("akka-persistence-cassandra", ExclusionRule("com.github.jnr")), akkaPersistenceCassandraDependency("akka-persistence-cassandra-launcher") % Test @@ -479,9 +490,9 @@ lazy val `proxy-jdbc` = (project in file("proxy/jdbc")) name := "cloudstate-proxy-jdbc", dependencyOverrides += "io.grpc" % "grpc-netty-shaded" % GrpcNettyShadedVersion, libraryDependencies ++= Seq( - //"com.typesafe.slick" %% "slick" % SlickVersion, // should be here for CRUD native support!! - //"com.typesafe.slick" %% "slick-hikaricp" % SlickHikariVersion, // should be here for CRUD native support!! - "com.github.dnvriend" %% "akka-persistence-jdbc" % "3.5.2" + //"com.github.dnvriend" %% "akka-persistence-jdbc" % "3.5.2" + // remove Slick as dependency from akka-persistence-jdbc which is already in the poxy-core + akkaPersistenceJdbcDependency("akka-persistence-jdbc", ExclusionRule("com.typesafe.slick")) ), fork in run := true, mainClass in Compile := Some("io.cloudstate.proxy.CloudStateProxyMain") diff --git a/proxy/core/src/main/resources/reference.conf b/proxy/core/src/main/resources/reference.conf index acc5c1e6a..fe5bded50 100644 --- a/proxy/core/src/main/resources/reference.conf +++ b/proxy/core/src/main/resources/reference.conf @@ -130,7 +130,7 @@ cloudstate.proxy { # Configures the persistence store for the Value Entity when value-entity-enabled is true value-entity-persistence-store { # This property indicate the type of store to be used for Value Entity. - # Valid options are: "using-jdbc", "in-memory" + # Valid options are: "jdbc", "in-memory" # "in-memory" means the data are persisted in memory. # "jdbc" means the data are persisted in a configured native JDBC database. store-type = "in-memory" diff --git a/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/ValueEntity.scala b/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/ValueEntity.scala index 33cedda90..f284a87e6 100644 --- a/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/ValueEntity.scala +++ b/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/ValueEntity.scala @@ -61,71 +61,111 @@ final class ValueEntitySupervisor(client: ValueEntityProtocolClient, extends Actor with Stash { - import ValueEntitySupervisor._ + private val entityId = URLDecoder.decode(self.path.name, "utf-8") + private val relay = context.watch(context.actorOf(ValueEntityRelay.props(client, configuration), "relay")) + private val entity = + context.watch(context.actorOf(ValueEntity.props(configuration, entityId, relay, repository), "entity")) - private var streamTerminated: Boolean = false + private var relayTerminated: Boolean = false + private var entityTerminated: Boolean = false - override final def receive: Receive = PartialFunction.empty + relay ! ValueEntityRelay.Connect(entity) - override final def preStart(): Unit = { + override def receive: Receive = { + case Terminated(`relay`) => + relayTerminated = true + if (entityTerminated) { + context.stop(self) + } + case Terminated(`entity`) => + entityTerminated = true + if (relayTerminated) { + context.stop(self) + } else relay ! ValueEntityRelay.Disconnect + case toParent if sender() == entity => + context.parent ! toParent + case msg => + entity forward msg + } + + override def supervisorStrategy: SupervisorStrategy = SupervisorStrategy.stoppingStrategy +} + +object ValueEntityRelay { + final case class Connect(entity: ActorRef) + final case class Connected(stream: ActorRef) + final case object Disconnect + final case class Fail(cause: Throwable) + + def props(client: ValueEntityProtocolClient, + configuration: ValueEntity.Configuration)(implicit mat: Materializer): Props = + Props(new ValueEntityRelay(client, configuration)) +} + +final class ValueEntityRelay(client: ValueEntityProtocolClient, configuration: ValueEntity.Configuration)( + implicit mat: Materializer +) extends Actor + with Stash { + import ValueEntityRelay._ + + override def receive: Receive = starting + + def starting: Receive = { + case Connect(entity) => connect(entity) + case _ => stash() + } + + def connect(entity: ActorRef): Unit = { client .handle( Source - .actorRef[ValueEntityStreamIn](configuration.sendQueueSize, OverflowStrategy.fail) + .actorRef[ValueEntityStreamIn]( + { case Disconnect => CompletionStrategy.draining }: PartialFunction[Any, CompletionStrategy], + { case Fail(cause) => cause }: PartialFunction[Any, Throwable], + configuration.sendQueueSize, + OverflowStrategy.fail + ) .mapMaterializedValue { ref => - self ! Relay(ref) + self ! Connected(ref) NotUsed } ) .runWith(Sink.actorRef(self, ValueEntity.StreamClosed, ValueEntity.StreamFailed.apply)) - context.become(waitingForRelay) + + context.become(connecting(entity)) } - private[this] final def waitingForRelay: Receive = { - case Relay(relayRef) => - // Cluster sharding URL encodes entity ids, so to extract it we need to decode. - val entityId = URLDecoder.decode(self.path.name, "utf-8") - val entity = context.watch( - context - .actorOf(ValueEntity.props(configuration, entityId, relayRef, repository), "entity") - ) - context.become(forwarding(entity, relayRef)) + def connecting(entity: ActorRef): Receive = { + case Connected(stream) => + context.become(relaying(entity, stream)) unstashAll() case _ => stash() } - private[this] final def forwarding(entity: ActorRef, relay: ActorRef): Receive = { - case Terminated(`entity`) => - if (streamTerminated) { - context.stop(self) - } else { - relay ! Status.Success(CompletionStrategy.draining) - context.become(stopping) - } - + def relaying(entity: ActorRef, stream: ActorRef): Receive = { + case Disconnect => + stream ! Disconnect + context.become(disconnecting) + case fail: Fail => + stream ! fail + context.become(disconnecting) case message if sender() == entity => - context.parent ! message - - case ValueEntity.StreamClosed => - streamTerminated = true - entity forward ValueEntity.StreamClosed - - case failed: ValueEntity.StreamFailed => - streamTerminated = true - entity forward failed - + stream ! message case message => - entity forward message + entity ! message + if (streamTerminated(message)) context.stop(self) } - private def stopping: Receive = { - case ValueEntity.StreamClosed => - context.stop(self) - case _: ValueEntity.StreamFailed => - context.stop(self) + def disconnecting: Receive = { + case message if streamTerminated(message) => context.stop(self) + case _ => // ignore } - override def supervisorStrategy: SupervisorStrategy = SupervisorStrategy.stoppingStrategy + def streamTerminated(message: Any): Boolean = message match { + case ValueEntity.StreamClosed => true + case ValueEntity.StreamFailed => true + case _ => false + } } object ValueEntity { @@ -203,9 +243,10 @@ final class ValueEntity(configuration: ValueEntity.Configuration, ) ) ) - inited = true + ValueEntity.ReadStateSuccess(false) // not initialized yet! + } else { + ValueEntity.ReadStateSuccess(true) // already initialized! } - ValueEntity.ReadStateSuccess(inited) } .recover { case error => ValueEntity.ReadStateFailure(error) @@ -271,7 +312,8 @@ final class ValueEntity(configuration: ValueEntity.Configuration, override final def receive: Receive = { case ValueEntity.ReadStateSuccess(initialize) => - if (initialize) { + if (!initialize) { + inited = true context.become(running) unstashAll() } @@ -354,14 +396,14 @@ final class ValueEntity(configuration: ValueEntity.Configuration, notifyOutstandingRequests("Unexpected Value entity termination") throw error + case ReceiveTimeout => + context.parent ! ShardRegion.Passivate(stopMessage = ValueEntity.Stop) + case ValueEntity.Stop => stopped = true if (currentCommand == null) { context.stop(self) } - - case ReceiveTimeout => - context.parent ! ShardRegion.Passivate(stopMessage = ValueEntity.Stop) } private def performAction( diff --git a/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/ValueEntitySupportFactory.scala b/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/ValueEntitySupportFactory.scala index 8c8c13c89..9e215ad34 100644 --- a/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/ValueEntitySupportFactory.scala +++ b/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/ValueEntitySupportFactory.scala @@ -58,19 +58,19 @@ class ValueEntitySupportFactory( val repository = new JdbcRepositoryImpl(createStore(config.config)) - log.debug("Starting CrudEntity for {}", entity.persistenceId) + log.debug("Starting ValueEntity for {}", entity.persistenceId) val clusterSharding = ClusterSharding(system) val clusterShardingSettings = ClusterShardingSettings(system) - val crudEntity = clusterSharding.start( + val valueEntity = clusterSharding.start( typeName = entity.persistenceId, entityProps = ValueEntitySupervisor.props(valueEntityClient, stateManagerConfig, repository), settings = clusterShardingSettings, - messageExtractor = new CrudEntityIdExtractor(config.numberOfShards), + messageExtractor = new ValueEntityIdExtractor(config.numberOfShards), allocationStrategy = new DynamicLeastShardAllocationStrategy(1, 10, 2, 0.0), handOffStopMessage = ValueEntity.Stop ) - new ValueEntitySupport(crudEntity, config.proxyParallelism, config.relayTimeout) + new ValueEntitySupport(valueEntity, config.proxyParallelism, config.relayTimeout) } private def validate(serviceDescriptor: ServiceDescriptor, @@ -80,14 +80,14 @@ class ValueEntitySupportFactory( if (streamedMethods.nonEmpty) { val offendingMethods = streamedMethods.map(_.method.getName).mkString(",") throw EntityDiscoveryException( - s"CRUD entities do not support streamed methods, but ${serviceDescriptor.getFullName} has the following streamed methods: ${offendingMethods}" + s"Value entities do not support streamed methods, but ${serviceDescriptor.getFullName} has the following streamed methods: ${offendingMethods}" ) } val methodsWithoutKeys = methodDescriptors.values.filter(_.keyFieldsCount < 1) if (methodsWithoutKeys.nonEmpty) { val offendingMethods = methodsWithoutKeys.map(_.method.getName).mkString(",") throw EntityDiscoveryException( - s"""CRUD entities do not support methods whose parameters do not have at least one field marked as entity_key, + s"""Value entities do not support methods whose parameters do not have at least one field marked as entity_key, |but ${serviceDescriptor.getFullName} has the following methods without keys: $offendingMethods""".stripMargin .replaceAll("\n", " ") ) @@ -111,7 +111,7 @@ private class ValueEntitySupport(crudEntity: ActorRef, parallelism: Int, private (crudEntity ? command).mapTo[UserFunctionReply] } -private final class CrudEntityIdExtractor(shards: Int) extends HashCodeMessageExtractor(shards) { +private final class ValueEntityIdExtractor(shards: Int) extends HashCodeMessageExtractor(shards) { override final def entityId(message: Any): String = message match { case command: EntityCommand => command.entityId } diff --git a/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/JdbcInMemoryStore.scala b/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/JdbcInMemoryStore.scala index d665ac2f8..aad382dbc 100644 --- a/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/JdbcInMemoryStore.scala +++ b/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/JdbcInMemoryStore.scala @@ -19,11 +19,12 @@ package io.cloudstate.proxy.valueentity.store import akka.util.ByteString import io.cloudstate.proxy.valueentity.store.JdbcStore.Key +import scala.collection.concurrent.TrieMap import scala.concurrent.Future final class JdbcInMemoryStore extends JdbcStore[Key, ByteString] { - private var store = Map.empty[Key, ByteString] + private var store = TrieMap.empty[Key, ByteString] override def get(key: Key): Future[Option[ByteString]] = Future.successful(store.get(key)) diff --git a/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/JdbcRepository.scala b/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/JdbcRepository.scala index 52114edbe..db25c23dd 100644 --- a/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/JdbcRepository.scala +++ b/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/JdbcRepository.scala @@ -19,6 +19,7 @@ package io.cloudstate.proxy.valueentity.store import akka.grpc.ProtobufSerializer import akka.util.ByteString import com.google.protobuf.any.{Any => ScalaPbAny} +import com.google.protobuf.{ByteString => PbByteString} import io.cloudstate.proxy.valueentity.store.JdbcStore.Key import scala.concurrent.{ExecutionContext, Future} @@ -55,13 +56,21 @@ trait JdbcRepository { object JdbcRepositoryImpl { - private[store] final object ReplySerializer extends ProtobufSerializer[ScalaPbAny] { - override final def serialize(state: ScalaPbAny): ByteString = - if (state.value.isEmpty) ByteString.empty - else ByteString.fromArrayUnsafe(state.value.toByteArray) + private[store] final object EntitySerializer extends ProtobufSerializer[ScalaPbAny] { + private val separator = ByteString("|") - override final def deserialize(bytes: ByteString): ScalaPbAny = - ScalaPbAny.parseFrom(bytes.toByteBuffer.array()) + override final def serialize(entity: ScalaPbAny): ByteString = + if (entity.value.isEmpty) { + ByteString(entity.typeUrl) ++ separator ++ ByteString.empty + } else { + ByteString(entity.typeUrl) ++ separator ++ ByteString.fromArrayUnsafe(entity.value.toByteArray) + } + + override final def deserialize(bytes: ByteString): ScalaPbAny = { + val separatorIndex = bytes.indexOf(separator.head) + val (typeUrl, value) = bytes.splitAt(separatorIndex) + ScalaPbAny(typeUrl.utf8String, PbByteString.copyFrom(value.tail.toByteBuffer)) + } } } @@ -71,7 +80,7 @@ class JdbcRepositoryImpl(val store: JdbcStore[Key, ByteString], serializer: Prot ) extends JdbcRepository { def this(store: JdbcStore[Key, ByteString])(implicit ec: ExecutionContext) = - this(store, JdbcRepositoryImpl.ReplySerializer) + this(store, JdbcRepositoryImpl.EntitySerializer) def get(key: Key): Future[Option[ScalaPbAny]] = store diff --git a/proxy/core/src/test/scala/io/cloudstate/proxy/valueentity/ValueEntityPassivateSpec.scala b/proxy/core/src/test/scala/io/cloudstate/proxy/valueentity/ValueEntityPassivateSpec.scala new file mode 100644 index 000000000..30b531ae2 --- /dev/null +++ b/proxy/core/src/test/scala/io/cloudstate/proxy/valueentity/ValueEntityPassivateSpec.scala @@ -0,0 +1,106 @@ +/* + * Copyright 2019 Lightbend Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.cloudstate.proxy.valueentity + +import akka.actor.ActorRef +import akka.grpc.GrpcClientSettings +import akka.testkit.TestEvent.Mute +import akka.testkit.EventFilter +import com.google.protobuf.ByteString +import com.google.protobuf.any.{Any => ProtoAny} +import io.cloudstate.protocol.value_entity.ValueEntityProtocolClient +import io.cloudstate.proxy.entity.{EntityCommand, UserFunctionReply} +import io.cloudstate.proxy.telemetry.AbstractTelemetrySpec +import io.cloudstate.proxy.valueentity.store.{JdbcInMemoryStore, JdbcRepositoryImpl} +import io.cloudstate.testkit.TestService +import io.cloudstate.testkit.valuentity.ValueEntityMessages + +import scala.concurrent.duration._ + +class ValueEntityPassivateSpec extends AbstractTelemetrySpec { + + "ValueEntity" should { + + "restart entity after passivation" in withTestKit( + """ + | include "test-in-memory" + | akka { + | loglevel = DEBUG + | loggers = ["akka.testkit.TestEventListener"] + | remote.artery.canonical.port = 0 + | remote.artery.bind.port = "" + | } + """ + ) { testKit => + import ValueEntityMessages._ + import testKit.system.dispatcher + import testKit._ + + // silence any dead letters or unhandled messages during shutdown (when using test event listener) + system.eventStream.publish(Mute(EventFilter.warning(pattern = ".*received dead letter.*"))) + system.eventStream.publish(Mute(EventFilter.warning(pattern = ".*unhandled message.*"))) + + implicit val replyTo: ActorRef = testActor + + val service = TestService() + val client = + ValueEntityProtocolClient(GrpcClientSettings.connectToServiceAt("localhost", service.port).withTls(false)) + + val entityConfiguration = ValueEntity.Configuration( + serviceName = "service", + userFunctionName = "test", + passivationTimeout = 30.seconds, + sendQueueSize = 100 + ) + + val repository = new JdbcRepositoryImpl(new JdbcInMemoryStore) + val entity = system.actorOf(ValueEntitySupervisor.props(client, entityConfiguration, repository), "entity") + + val emptyCommand = Some(protobufAny(EmptyJavaMessage)) + + // init with empty state + val connection = service.valueEntity.expectConnection() + connection.expect(init("service", "entity")) + + // first command command fails + entity ! EntityCommand(entityId = "test", name = "command1", emptyCommand) + connection.expect(command(1, "entity", "command1")) + connection.send(failure(1, "boom! failure")) + expectMsg(UserFunctionReply(clientActionFailure(0, "Unexpected Value entity failure"))) + EventFilter.error("Unexpected Value entity failure - boom! failure", occurrences = 1) + connection.expectClosed() + //expectTerminated(entity) // should be expected! + + // re-init entity with empty state and send command + val recreatedEntity = + system.actorOf(ValueEntitySupervisor.props(client, entityConfiguration, repository), "entity1") + val connection2 = service.valueEntity.expectConnection() + connection2.expect(init("service", "entity1")) + recreatedEntity ! EntityCommand(entityId = "test", name = "command2", emptyCommand) + connection2.expect(command(1, "entity1", "command2")) + val reply1 = ProtoAny("reply", ByteString.copyFromUtf8("reply1")) + connection2.send(reply(1, reply1)) + expectMsg(UserFunctionReply(clientActionReply(messagePayload(reply1)))) + + // passivate + recreatedEntity ! ValueEntity.Stop + connection2.expectClosed() + //expectTerminated(recreatedEntity) // should be expected! + } + + } +} diff --git a/proxy/core/src/test/scala/io/cloudstate/proxy/valueentity/store/EntitySerializerSpec.scala b/proxy/core/src/test/scala/io/cloudstate/proxy/valueentity/store/EntitySerializerSpec.scala new file mode 100644 index 000000000..337e29bb1 --- /dev/null +++ b/proxy/core/src/test/scala/io/cloudstate/proxy/valueentity/store/EntitySerializerSpec.scala @@ -0,0 +1,46 @@ +/* + * Copyright 2019 Lightbend Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.cloudstate.proxy.valueentity.store + +import akka.util.ByteString +import com.google.protobuf.any.{Any => ScalaPbAny} +import com.google.protobuf.{ByteString => ProtobufByteString} +import io.cloudstate.proxy.valueentity.store.JdbcRepositoryImpl.EntitySerializer +import org.scalatest.{Matchers, WordSpecLike} + +class EntitySerializerSpec extends WordSpecLike with Matchers { + + "Entity Serializer" should { + import EntitySerializer._ + + val entity = ScalaPbAny("p.cloudstate.io/string", ProtobufByteString.copyFromUtf8("state")) + + "serialize entity" in { + serialize(entity) shouldBe ByteString("p.cloudstate.io/string|state") + } + + "deserialize state" in { + deserialize(serialize(entity)) shouldBe entity + } + + "not deserialize state" in { + val wrongSerializedEntity = ByteString("p.cloudstate.io/string_state") + deserialize(wrongSerializedEntity) should not be entity + } + } + +} From f7e27aa64ac653de57612acb12aef7341d0086db Mon Sep 17 00:00:00 2001 From: Guy Youansi Date: Fri, 6 Nov 2020 12:14:19 +0100 Subject: [PATCH 82/93] renaming value entity to entity in the java support, the proxy, the tck and testkit. remove duplicates regarding sharding. change structure of the store package by making difference between jdbc and the rest. --- .circleci/config.yml | 32 ++--- ...n-java-eventsourced-shopping-cart-test.sh} | 82 +++++++----- bin/run-java-shopping-cart-test.sh | 72 ++++------- build.sbt | 20 +-- .../io/cloudstate/javasupport/CloudState.java | 28 ++-- .../CommandContext.java | 6 +- .../CommandHandler.java | 4 +- .../ValueEntity.java => entity/Entity.java} | 6 +- .../EntityContext.java} | 8 +- .../EntityCreationContext.java} | 8 +- .../EntityFactory.java} | 10 +- .../EntityHandler.java} | 6 +- .../javasupport/entity/package-info.java | 8 ++ .../javasupport/valueentity/package-info.java | 9 -- .../javasupport/CloudStateRunner.scala | 10 +- .../javasupport/impl/EntityExceptions.scala | 4 +- .../AnnotationBasedEntitySupport.scala} | 43 ++++--- .../EntityImpl.scala} | 20 +-- .../impl/eventsourced/EventSourcedImpl.scala | 42 ------ .../AnnotationBasedEntitySupportSpec.scala} | 43 +++---- .../EntityImplSpec.scala} | 24 ++-- .../TestEntity.scala} | 14 +- .../javasupport/tck/JavaSupportTck.java | 35 +++-- .../ValueEntityTckModelEntity.java | 19 ++- .../ValueEntityTwoEntity.java | 12 +- .../protocol/cloudstate/value_entity.proto | 4 +- .../cloudstate/tck/model/valueentity.proto | 16 +-- .../proxy/EntityDiscoveryManager.scala | 6 +- .../DynamicLeastShardAllocationStrategy.scala | 77 ----------- .../EventSourcedSupportFactory.scala | 3 +- .../DynamicLeastShardAllocationStrategy.scala | 2 +- .../{ValueEntity.scala => Entity.scala} | 25 ++-- ...ctory.scala => EntitySupportFactory.scala} | 27 ++-- ...nMemoryStore.scala => InMemoryStore.scala} | 7 +- .../store/JdbcValueEntityTable.scala | 51 -------- ...{JdbcRepository.scala => Repository.scala} | 19 +-- .../store/{JdbcStore.scala => Store.scala} | 41 +----- ...cStoreSupport.scala => StoreSupport.scala} | 26 ++-- .../store/{ => jdbc}/JdbcConfig.scala | 12 +- .../JdbcEntityQueries.scala} | 20 +-- .../store/jdbc/JdbcEntityTable.scala | 51 ++++++++ .../valueentity/store/jdbc/JdbcStore.scala | 49 +++++++ .../proxy/valueentity/store/package-info.java | 10 +- .../scala/io/cloudstate/proxy/TestProxy.scala | 4 +- .../DatabaseExceptionHandlingSpec.scala | 34 ++--- ...teSpec.scala => EntityPassivateSpec.scala} | 18 +-- .../valueentity/ExceptionHandlingSpec.scala | 8 +- .../store/EntitySerializerSpec.scala | 2 +- .../store/{ => jdbc}/JdbcConfigSpec.scala | 25 ++-- ...sureValueEntityTablesExistReadyCheck.scala | 20 ++- .../reflect-config.json.conf | 2 +- .../eventsourced}/shoppingcart/Main.java | 10 +- .../shoppingcart/ShoppingCartEntity.java | 118 +++++++++++++++++ .../src/main/resources/application.conf | 2 +- .../main/resources/simplelogger.properties | 0 .../cloudstate/samples/shoppingcart/Main.java | 8 +- .../shoppingcart/ShoppingCartEntity.java | 118 +++++++++-------- .../src/main/resources/application.conf | 2 +- .../shoppingcart/ShoppingCartEntity.java | 116 ----------------- .../io/cloudstate/tck/CloudStateTCK.scala | 121 ++++-------------- .../EventSourcedShoppingCartVerifier.scala | 107 ++++++++++++++++ .../tck/ValueEntityShoppingCartVerifier.scala | 13 +- .../cloudstate/testkit/InterceptService.scala | 10 +- .../io/cloudstate/testkit/TestProtocol.scala | 2 +- .../io/cloudstate/testkit/TestService.scala | 2 +- .../discovery/InterceptEntityDiscovery.scala | 4 +- .../InterceptValueEntityService.scala | 22 ++-- .../TestValueEntityProtocol.scala | 8 +- .../TestValueEntityService.scala | 19 +-- .../TestValueEntityServiceClient.scala | 8 +- .../ValueEntityMessages.scala | 2 +- 71 files changed, 867 insertions(+), 949 deletions(-) rename bin/{run-java-entity-shopping-cart-test.sh => run-java-eventsourced-shopping-cart-test.sh} (71%) rename java-support/src/main/java/io/cloudstate/javasupport/{valueentity => entity}/CommandContext.java (91%) rename java-support/src/main/java/io/cloudstate/javasupport/{valueentity => entity}/CommandHandler.java (94%) rename java-support/src/main/java/io/cloudstate/javasupport/{valueentity/ValueEntity.java => entity/Entity.java} (91%) rename java-support/src/main/java/io/cloudstate/javasupport/{valueentity/ValueEntityContext.java => entity/EntityContext.java} (74%) rename java-support/src/main/java/io/cloudstate/javasupport/{valueentity/ValueEntityCreationContext.java => entity/EntityCreationContext.java} (77%) rename java-support/src/main/java/io/cloudstate/javasupport/{valueentity/ValueEntityFactory.java => entity/EntityFactory.java} (77%) rename java-support/src/main/java/io/cloudstate/javasupport/{valueentity/ValueEntityHandler.java => entity/EntityHandler.java} (88%) create mode 100644 java-support/src/main/java/io/cloudstate/javasupport/entity/package-info.java delete mode 100644 java-support/src/main/java/io/cloudstate/javasupport/valueentity/package-info.java rename java-support/src/main/scala/io/cloudstate/javasupport/impl/{valueentity/AnnotationBasedValueEntitySupport.scala => entity/AnnotationBasedEntitySupport.scala} (83%) rename java-support/src/main/scala/io/cloudstate/javasupport/impl/{valueentity/ValueEntityImpl.scala => entity/EntityImpl.scala} (93%) rename java-support/src/test/scala/io/cloudstate/javasupport/impl/{valueentity/AnnotationBasedValueEntitySupportSpec.scala => entity/AnnotationBasedEntitySupportSpec.scala} (90%) rename java-support/src/test/scala/io/cloudstate/javasupport/impl/{valueentity/ValueEntityImplSpec.scala => entity/EntityImplSpec.scala} (94%) rename java-support/src/test/scala/io/cloudstate/javasupport/impl/{valueentity/TestValueEntity.scala => entity/TestEntity.scala} (76%) rename java-support/tck/src/main/java/io/cloudstate/javasupport/tck/model/{valuentity => valuebased}/ValueEntityTckModelEntity.java (76%) rename java-support/tck/src/main/java/io/cloudstate/javasupport/tck/model/{valuentity => valuebased}/ValueEntityTwoEntity.java (69%) delete mode 100644 proxy/core/src/main/scala/io/cloudstate/proxy/eventsourced/DynamicLeastShardAllocationStrategy.scala rename proxy/core/src/main/scala/io/cloudstate/proxy/{valueentity => sharding}/DynamicLeastShardAllocationStrategy.scala (98%) rename proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/{ValueEntity.scala => Entity.scala} (94%) rename proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/{ValueEntitySupportFactory.scala => EntitySupportFactory.scala} (80%) rename proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/{JdbcInMemoryStore.scala => InMemoryStore.scala} (84%) delete mode 100644 proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/JdbcValueEntityTable.scala rename proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/{JdbcRepository.scala => Repository.scala} (86%) rename proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/{JdbcStore.scala => Store.scala} (53%) rename proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/{JdbcStoreSupport.scala => StoreSupport.scala} (70%) rename proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/{ => jdbc}/JdbcConfig.scala (79%) rename proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/{JdbcValueEntityQueries.scala => jdbc/JdbcEntityQueries.scala} (59%) create mode 100644 proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/jdbc/JdbcEntityTable.scala create mode 100644 proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/jdbc/JdbcStore.scala rename proxy/core/src/test/scala/io/cloudstate/proxy/valueentity/{ValueEntityPassivateSpec.scala => EntityPassivateSpec.scala} (87%) rename proxy/core/src/test/scala/io/cloudstate/proxy/valueentity/store/{ => jdbc}/JdbcConfigSpec.scala (74%) rename samples/{java-valueentity-shopping-cart/src/main/java/io/cloudstate/samples/valueentity => java-eventsourced-shopping-cart/src/main/java/io/cloudstate/samples/eventsourced}/shoppingcart/Main.java (76%) create mode 100644 samples/java-eventsourced-shopping-cart/src/main/java/io/cloudstate/samples/eventsourced/shoppingcart/ShoppingCartEntity.java rename samples/{java-valueentity-shopping-cart => java-eventsourced-shopping-cart}/src/main/resources/application.conf (98%) rename samples/{java-valueentity-shopping-cart => java-eventsourced-shopping-cart}/src/main/resources/simplelogger.properties (100%) delete mode 100644 samples/java-valueentity-shopping-cart/src/main/java/io/cloudstate/samples/valueentity/shoppingcart/ShoppingCartEntity.java create mode 100644 tck/src/main/scala/io/cloudstate/tck/EventSourcedShoppingCartVerifier.scala rename testkit/src/main/scala/io/cloudstate/testkit/{valuentity => valueentity}/InterceptValueEntityService.scala (81%) rename testkit/src/main/scala/io/cloudstate/testkit/{valuentity => valueentity}/TestValueEntityProtocol.scala (84%) rename testkit/src/main/scala/io/cloudstate/testkit/{valuentity => valueentity}/TestValueEntityService.scala (83%) rename testkit/src/main/scala/io/cloudstate/testkit/{valuentity => valueentity}/TestValueEntityServiceClient.scala (88%) rename testkit/src/main/scala/io/cloudstate/testkit/{valuentity => valueentity}/ValueEntityMessages.scala (99%) diff --git a/.circleci/config.yml b/.circleci/config.yml index 32171b04c..4f561a06d 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -151,10 +151,10 @@ jobs: 'dockerBuildPostgres publishLocal' \ 'dockerBuildCassandra publishLocal' \ java-shopping-cart/docker:publishLocal \ - java-valueentity-shopping-cart/docker:publishLocal + java-eventsourced-shopping-cart/docker:publishLocal - save_docker_image: tar_file: cloudstate-sbt-images.tar - docker_image: cloudstateio/cloudstate-proxy-core:latest cloudstateio/cloudstate-proxy-postgres:latest cloudstateio/cloudstate-proxy-cassandra:latest cloudstateio/java-shopping-cart:latest cloudstateio/java-valueentity-shopping-cart:latest + docker_image: cloudstateio/cloudstate-proxy-core:latest cloudstateio/cloudstate-proxy-postgres:latest cloudstateio/cloudstate-proxy-cassandra:latest cloudstateio/java-shopping-cart:latest cloudstateio/java-eventsourced-shopping-cart:latest - persist_to_workspace: root: /tmp/workspace paths: @@ -250,20 +250,20 @@ jobs: name: Install Cassandra command: bin/install-cassandra.sh - run: - name: Run shopping cart test (InMemory) + name: Run value-based shopping cart test (InMemory) command: bin/run-java-shopping-cart-test.sh InMemory --no-build - run: - name: Run shopping cart test (Postgres) + name: Run value-based shopping cart test (Postgres) command: bin/run-java-shopping-cart-test.sh Postgres --no-build - run: - name: Run shopping cart test (Cassandra) - command: bin/run-java-shopping-cart-test.sh Cassandra --no-build + name: Run eventsourced shopping cart test (InMemory) + command: bin/run-java-eventsourced-shopping-cart-test.sh InMemory --no-build - run: - name: Run entity shopping cart test (InMemory) - command: bin/run-java-entity-shopping-cart-test.sh InMemory --no-build + name: Run eventsourced shopping cart test (Postgres) + command: bin/run-java-eventsourced-shopping-cart-test.sh Postgres --no-build - run: - name: Run entity shopping cart test (Postgres) - command: bin/run-java-entity-shopping-cart-test.sh Postgres --no-build + name: Run eventsourced shopping cart test (Cassandra) + command: bin/run-java-eventsourced-shopping-cart-test.sh Cassandra --no-build native-image-smoke-test: parameters: @@ -308,8 +308,8 @@ jobs: name: Load in memory image command: docker image load < /tmp/workspace/cloudstate-proxy-native-core.tar - run: - name: Run entity shopping cart test (InMemory) - command: bin/run-java-entity-shopping-cart-test.sh InMemory --no-build + name: Run value-based shopping cart test (InMemory) + command: bin/run-java-shopping-cart-test.sh InMemory --no-build - when: condition: equal: [ Postgres, << parameters.proxy-store >> ] @@ -320,8 +320,8 @@ jobs: bin/install-postgres.sh docker image load < /tmp/workspace/cloudstate-proxy-native-postgres.tar - run: - name: Run entity shopping cart test (Postgres) - command: bin/run-java-entity-shopping-cart-test.sh Postgres --no-build + name: Run value-based shopping cart test (Postgres) + command: bin/run-java-shopping-cart-test.sh Postgres --no-build - when: condition: equal: [ Cassandra, << parameters.proxy-store >> ] @@ -332,8 +332,8 @@ jobs: bin/install-cassandra.sh docker image load < /tmp/workspace/cloudstate-proxy-native-cassandra.tar - run: - name: Run shopping cart test (<< parameters.proxy-store >>) - command: bin/run-java-shopping-cart-test.sh << parameters.proxy-store >> --no-build + name: Run eventsourced shopping cart test (<< parameters.proxy-store >>) + command: bin/run-java-eventsourced-shopping-cart-test.sh << parameters.proxy-store >> --no-build publish-native-images: machine: true diff --git a/bin/run-java-entity-shopping-cart-test.sh b/bin/run-java-eventsourced-shopping-cart-test.sh similarity index 71% rename from bin/run-java-entity-shopping-cart-test.sh rename to bin/run-java-eventsourced-shopping-cart-test.sh index f1059cfe2..7726faf0c 100755 --- a/bin/run-java-entity-shopping-cart-test.sh +++ b/bin/run-java-eventsourced-shopping-cart-test.sh @@ -1,13 +1,13 @@ #!/usr/bin/env bash # -# Run the Java Entity shopping cart sample as a test, with a given persistence store. +# Run the Java Event Sourced shopping cart sample as a test, with a given persistence store. # -# run-java-entity-shopping-cart-test.sh [inmemory|postgres] +# run-java-eventsourced-shopping-cart-test.sh [inmemory|postgres|cassandra] set -e echo -echo "=== Running java-entity-shopping-cart test ===" +echo "=== Running java-eventsourced-shopping-cart test ===" echo # process arguments @@ -27,12 +27,12 @@ done set -- "${residual[@]}" store="$1" -# Build the java entity shopping cart +# Build the java eventsourced shopping cart if [ "$build" == true ] ; then echo - echo "Building java-entity-shopping-cart ..." + echo "Building java-eventsourced-shopping-cart ..." [ -f docker-env.sh ] && source docker-env.sh - sbt -Ddocker.username=cloudstatedev -Ddocker.tag=dev java-valueentity-shopping-cart/docker:publishLocal + sbt -Ddocker.username=cloudstatedev -Ddocker.tag=dev java-eventsourced-shopping-cart/docker:publishLocal fi statefulstore="inmemory" @@ -40,7 +40,7 @@ statefulservice="shopping-cart-$statefulstore" case "$store" in - postgres ) # deploy the entity-shopping-cart with postgres store + postgres ) # deploy the eventsourced shopping-cart with postgres store statefulstore="postgres" statefulservice="shopping-cart-$statefulstore" @@ -66,13 +66,46 @@ spec: statefulStore: name: $statefulstore containers: - - image: cloudstateio/java-valueentity-shopping-cart:latest + - image: cloudstateio/java-eventsourced-shopping-cart:latest imagePullPolicy: Never name: user-function YAML ;; - inmemory | * ) # deploy the entity-shopping-cart with in-memory store + cassandra ) # deploy the eventsourced shopping-cart with cassandra store + + statefulstore="cassandra" + statefulservice="shopping-cart-$statefulstore" + + kubectl apply -f - < /dev/null + curl -s -X POST $url/cart/$cart_id/items/add -H "Content-Type: application/json" -d "$post" > /dev/null - new_cart=$(curl -s $url/ve/carts/$cart_id) + new_cart=$(curl -s $url/carts/$cart_id) if [[ "$non_empty_cart" != "$new_cart" ]] then echo "Expected '$non_empty_cart'" echo "But got '$new_cart'" fail_with_details else - echo "Entity shopping cart update for $cart_id succeeded." - fi - - curl -s -X POST $url/ve/carts/$cart_id/remove -H "Content-Type: application/json" -d '{"userId": "'"$cart_id"'"}' > /dev/null - - deleted_cart=$(curl -s $url/ve/carts/$cart_id) - if [[ "$empty_cart" != "$deleted_cart" ]] - then - echo "Expected '$empty_cart'" - echo "But got '$deleted_cart'" - fail_with_details - else - echo "Entity shopping cart delete for $cart_id succeeded." + echo "EventSourced Shopping cart update for $cart_id succeeded." fi done @@ -224,7 +246,7 @@ if [ "$logs" == true ] ; then kubectl logs -l cloudstate.io/stateful-service=$statefulservice -c cloudstate-sidecar --tail=-1 fi -# Delete entity-shopping-cart +# Delete eventsourced shopping-cart if [ "$delete" == true ] ; then echo echo "Deleting $statefulservice ..." diff --git a/bin/run-java-shopping-cart-test.sh b/bin/run-java-shopping-cart-test.sh index 5162e3f89..f5125739a 100755 --- a/bin/run-java-shopping-cart-test.sh +++ b/bin/run-java-shopping-cart-test.sh @@ -1,8 +1,8 @@ #!/usr/bin/env bash # -# Run the Java shopping cart sample as a test, with a given persistence store. +# Run the Java value-based Entity shopping cart sample as a test, with a given persistence store. # -# run-java-shopping-cart-test.sh [inmemory|postgres|cassandra] +# run-java-shopping-cart-test.sh [inmemory|postgres] set -e @@ -27,7 +27,7 @@ done set -- "${residual[@]}" store="$1" -# Build the java shopping cart +# Build the java value-based entity shopping cart if [ "$build" == true ] ; then echo echo "Building java-shopping-cart ..." @@ -40,7 +40,7 @@ statefulservice="shopping-cart-$statefulstore" case "$store" in - postgres ) # deploy the shopping-cart with postgres store + postgres ) # deploy the value-based entity-shopping-cart with postgres store statefulstore="postgres" statefulservice="shopping-cart-$statefulstore" @@ -72,40 +72,7 @@ spec: YAML ;; - cassandra ) # deploy the shopping-cart with cassandra store - - statefulstore="cassandra" - statefulservice="shopping-cart-$statefulstore" - - kubectl apply -f - < /dev/null + curl -s -X POST $url/ve/cart/$cart_id/items/add -H "Content-Type: application/json" -d "$post" > /dev/null - new_cart=$(curl -s $url/carts/$cart_id) + new_cart=$(curl -s $url/ve/carts/$cart_id) if [[ "$non_empty_cart" != "$new_cart" ]] then echo "Expected '$non_empty_cart'" echo "But got '$new_cart'" fail_with_details else - echo "Shopping cart update for $cart_id succeeded." + echo "Value-based Entity shopping cart update for $cart_id succeeded." + fi + + curl -s -X POST $url/ve/carts/$cart_id/remove -H "Content-Type: application/json" -d '{"userId": "'"$cart_id"'"}' > /dev/null + + deleted_cart=$(curl -s $url/ve/carts/$cart_id) + if [[ "$empty_cart" != "$deleted_cart" ]] + then + echo "Expected '$empty_cart'" + echo "But got '$deleted_cart'" + fail_with_details + else + echo "Value-based Entity shopping cart delete for $cart_id succeeded." fi done @@ -246,7 +224,7 @@ if [ "$logs" == true ] ; then kubectl logs -l cloudstate.io/stateful-service=$statefulservice -c cloudstate-sidecar --tail=-1 fi -# Delete shopping-cart +# Delete value-based entity-shopping-cart if [ "$delete" == true ] ; then echo echo "Deleting $statefulservice ..." diff --git a/build.sbt b/build.sbt index aea92ab32..d45b245ff 100644 --- a/build.sbt +++ b/build.sbt @@ -126,8 +126,8 @@ lazy val root = (project in file(".")) `java-support`, `java-support-docs`, `java-support-tck`, + `java-eventsourced-shopping-cart`, `java-shopping-cart`, - `java-valueentity-shopping-cart`, `java-pingpong`, `akka-client`, operator, @@ -655,7 +655,7 @@ lazy val `java-support-docs` = (project in file("java-support/docs")) ) lazy val `java-support-tck` = (project in file("java-support/tck")) - .dependsOn(`java-support`, `java-shopping-cart`, `java-valueentity-shopping-cart`) + .dependsOn(`java-support`, `java-shopping-cart`, `java-eventsourced-shopping-cart`) .enablePlugins(AkkaGrpcPlugin, AssemblyPlugin, JavaAppPackaging, DockerPlugin, AutomateHeaderPlugin, NoPublish) .settings( name := "cloudstate-java-tck", @@ -669,13 +669,13 @@ lazy val `java-support-tck` = (project in file("java-support/tck")) assemblySettings("cloudstate-java-tck.jar") ) -lazy val `java-shopping-cart` = (project in file("samples/java-shopping-cart")) +lazy val `java-eventsourced-shopping-cart` = (project in file("samples/java-eventsourced-shopping-cart")) .dependsOn(`java-support`) .enablePlugins(AkkaGrpcPlugin, AssemblyPlugin, JavaAppPackaging, DockerPlugin, AutomateHeaderPlugin, NoPublish) .settings( - name := "java-shopping-cart", + name := "java-eventsourced-shopping-cart", dockerSettings, - mainClass in Compile := Some("io.cloudstate.samples.shoppingcart.Main"), + mainClass in Compile := Some("io.cloudstate.samples.eventsourced.shoppingcart.Main"), PB.generate in Compile := (PB.generate in Compile).dependsOn(PB.generate in (`java-support`, Compile)).value, akkaGrpcGeneratedLanguages := Seq(AkkaGrpc.Java), PB.protoSources in Compile ++= { @@ -686,16 +686,16 @@ lazy val `java-shopping-cart` = (project in file("samples/java-shopping-cart")) PB.gens.java -> (sourceManaged in Compile).value ), javacOptions in Compile ++= Seq("-encoding", "UTF-8", "-source", "11", "-target", "11"), - assemblySettings("java-shopping-cart.jar") + assemblySettings("java-eventsourced-shopping-cart.jar") ) -lazy val `java-valueentity-shopping-cart` = (project in file("samples/java-valueentity-shopping-cart")) +lazy val `java-shopping-cart` = (project in file("samples/java-shopping-cart")) .dependsOn(`java-support`) .enablePlugins(AkkaGrpcPlugin, AssemblyPlugin, JavaAppPackaging, DockerPlugin, AutomateHeaderPlugin, NoPublish) .settings( - name := "java-valueentity-shopping-cart", + name := "java-shopping-cart", dockerSettings, - mainClass in Compile := Some("io.cloudstate.samples.valueentity.shoppingcart.Main"), + mainClass in Compile := Some("io.cloudstate.samples.shoppingcart.Main"), PB.generate in Compile := (PB.generate in Compile).dependsOn(PB.generate in (`java-support`, Compile)).value, akkaGrpcGeneratedLanguages := Seq(AkkaGrpc.Java), PB.protoSources in Compile ++= { @@ -706,7 +706,7 @@ lazy val `java-valueentity-shopping-cart` = (project in file("samples/java-value PB.gens.java -> (sourceManaged in Compile).value ), javacOptions in Compile ++= Seq("-encoding", "UTF-8", "-source", "11", "-target", "11"), - assemblySettings("java-valueentity-shopping-cart.jar") + assemblySettings("java-shopping-cart.jar") ) lazy val `java-pingpong` = (project in file("samples/java-pingpong")) diff --git a/java-support/src/main/java/io/cloudstate/javasupport/CloudState.java b/java-support/src/main/java/io/cloudstate/javasupport/CloudState.java index d70f096b7..e37c3e1f4 100644 --- a/java-support/src/main/java/io/cloudstate/javasupport/CloudState.java +++ b/java-support/src/main/java/io/cloudstate/javasupport/CloudState.java @@ -20,12 +20,13 @@ import akka.stream.Materializer; import com.typesafe.config.Config; import com.google.protobuf.Descriptors; -import io.cloudstate.javasupport.valueentity.ValueEntity; +import io.cloudstate.javasupport.impl.entity.AnnotationBasedEntitySupport; +import io.cloudstate.javasupport.entity.Entity; import io.cloudstate.javasupport.action.Action; import io.cloudstate.javasupport.action.ActionHandler; import io.cloudstate.javasupport.crdt.CrdtEntity; import io.cloudstate.javasupport.crdt.CrdtEntityFactory; -import io.cloudstate.javasupport.valueentity.ValueEntityFactory; +import io.cloudstate.javasupport.entity.EntityFactory; import io.cloudstate.javasupport.eventsourced.EventSourcedEntity; import io.cloudstate.javasupport.eventsourced.EventSourcedEntityFactory; import io.cloudstate.javasupport.impl.AnySupport; @@ -33,8 +34,7 @@ import io.cloudstate.javasupport.impl.action.ActionService; import io.cloudstate.javasupport.impl.crdt.AnnotationBasedCrdtSupport; import io.cloudstate.javasupport.impl.crdt.CrdtStatefulService; -import io.cloudstate.javasupport.impl.valueentity.AnnotationBasedValueEntitySupport; -import io.cloudstate.javasupport.impl.valueentity.ValueEntityStatefulService; +import io.cloudstate.javasupport.impl.entity.ValueEntityStatefulService; import io.cloudstate.javasupport.impl.eventsourced.AnnotationBasedEventSourcedSupport; import io.cloudstate.javasupport.impl.eventsourced.EventSourcedStatefulService; @@ -305,9 +305,9 @@ public CloudState registerAction( } /** - * Register a annotated value entity. + * Register an annotated value based entity. * - *

The entity class must be annotated with {@link ValueEntity}. + *

The entity class must be annotated with {@link Entity}. * * @param entityClass The entity class. * @param descriptor The descriptor for the service that this entity implements. @@ -315,15 +315,15 @@ public CloudState registerAction( * types when needed. * @return This stateful service builder. */ - public CloudState registerValueEntity( + public CloudState registerEntity( Class entityClass, Descriptors.ServiceDescriptor descriptor, Descriptors.FileDescriptor... additionalDescriptors) { - ValueEntity entity = entityClass.getAnnotation(ValueEntity.class); + Entity entity = entityClass.getAnnotation(Entity.class); if (entity == null) { throw new IllegalArgumentException( - entityClass + " does not declare an " + ValueEntity.class + " annotation!"); + entityClass + " does not declare an " + Entity.class + " annotation!"); } final String persistenceId; @@ -336,7 +336,7 @@ public CloudState registerValueEntity( final AnySupport anySupport = newAnySupport(additionalDescriptors); ValueEntityStatefulService service = new ValueEntityStatefulService( - new AnnotationBasedValueEntitySupport(entityClass, anySupport, descriptor), + new AnnotationBasedEntitySupport(entityClass, anySupport, descriptor), descriptor, anySupport, persistenceId); @@ -347,20 +347,20 @@ public CloudState registerValueEntity( } /** - * Register a value entity factory. + * Register an value based entity factory. * *

This is a low level API intended for custom (eg, non reflection based) mechanisms for * implementing the entity. * - * @param factory The value entity factory. + * @param factory The value based entity factory. * @param descriptor The descriptor for the service that this entity implements. * @param persistenceId The persistence id for this entity. * @param additionalDescriptors Any additional descriptors that should be used to look up protobuf * types when needed. * @return This stateful service builder. */ - public CloudState registerValueEntity( - ValueEntityFactory factory, + public CloudState registerEntity( + EntityFactory factory, Descriptors.ServiceDescriptor descriptor, String persistenceId, Descriptors.FileDescriptor... additionalDescriptors) { diff --git a/java-support/src/main/java/io/cloudstate/javasupport/valueentity/CommandContext.java b/java-support/src/main/java/io/cloudstate/javasupport/entity/CommandContext.java similarity index 91% rename from java-support/src/main/java/io/cloudstate/javasupport/valueentity/CommandContext.java rename to java-support/src/main/java/io/cloudstate/javasupport/entity/CommandContext.java index 1cf73c364..761699949 100644 --- a/java-support/src/main/java/io/cloudstate/javasupport/valueentity/CommandContext.java +++ b/java-support/src/main/java/io/cloudstate/javasupport/entity/CommandContext.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.cloudstate.javasupport.valueentity; +package io.cloudstate.javasupport.entity; import io.cloudstate.javasupport.ClientActionContext; import io.cloudstate.javasupport.EffectContext; @@ -23,14 +23,14 @@ import java.util.Optional; /** - * A value entity command context. + * A value based entity command context. * *

Methods annotated with {@link CommandHandler} may take this is a parameter. It allows updating * or deleting the entity state in response to a command, along with forwarding the result to other * entities, and performing side effects on other entities. */ public interface CommandContext - extends ValueEntityContext, ClientActionContext, EffectContext, MetadataContext { + extends EntityContext, ClientActionContext, EffectContext, MetadataContext { /** * The name of the command being executed. diff --git a/java-support/src/main/java/io/cloudstate/javasupport/valueentity/CommandHandler.java b/java-support/src/main/java/io/cloudstate/javasupport/entity/CommandHandler.java similarity index 94% rename from java-support/src/main/java/io/cloudstate/javasupport/valueentity/CommandHandler.java rename to java-support/src/main/java/io/cloudstate/javasupport/entity/CommandHandler.java index b6ed77f6a..70a02a710 100644 --- a/java-support/src/main/java/io/cloudstate/javasupport/valueentity/CommandHandler.java +++ b/java-support/src/main/java/io/cloudstate/javasupport/entity/CommandHandler.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.cloudstate.javasupport.valueentity; +package io.cloudstate.javasupport.entity; import io.cloudstate.javasupport.impl.CloudStateAnnotation; @@ -24,7 +24,7 @@ import java.lang.annotation.Target; /** - * Marks a method on a value entity as a command handler. + * Marks a method on an value based entity as a command handler. * *

This method will be invoked whenever the service call with name that matches this command * handlers name is invoked. diff --git a/java-support/src/main/java/io/cloudstate/javasupport/valueentity/ValueEntity.java b/java-support/src/main/java/io/cloudstate/javasupport/entity/Entity.java similarity index 91% rename from java-support/src/main/java/io/cloudstate/javasupport/valueentity/ValueEntity.java rename to java-support/src/main/java/io/cloudstate/javasupport/entity/Entity.java index 9f0b10c19..ff5e738b1 100644 --- a/java-support/src/main/java/io/cloudstate/javasupport/valueentity/ValueEntity.java +++ b/java-support/src/main/java/io/cloudstate/javasupport/entity/Entity.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.cloudstate.javasupport.valueentity; +package io.cloudstate.javasupport.entity; import io.cloudstate.javasupport.impl.CloudStateAnnotation; @@ -23,11 +23,11 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -/** A value entity. */ +/** An value based entity. */ @CloudStateAnnotation @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) -public @interface ValueEntity { +public @interface Entity { /** * The name of the persistence id. * diff --git a/java-support/src/main/java/io/cloudstate/javasupport/valueentity/ValueEntityContext.java b/java-support/src/main/java/io/cloudstate/javasupport/entity/EntityContext.java similarity index 74% rename from java-support/src/main/java/io/cloudstate/javasupport/valueentity/ValueEntityContext.java rename to java-support/src/main/java/io/cloudstate/javasupport/entity/EntityContext.java index 67c9fbb13..70ec51979 100644 --- a/java-support/src/main/java/io/cloudstate/javasupport/valueentity/ValueEntityContext.java +++ b/java-support/src/main/java/io/cloudstate/javasupport/entity/EntityContext.java @@ -14,9 +14,7 @@ * limitations under the License. */ -package io.cloudstate.javasupport.valueentity; +package io.cloudstate.javasupport.entity; -import io.cloudstate.javasupport.EntityContext; - -/** Root context for all value entity contexts. */ -public interface ValueEntityContext extends EntityContext {} +/** Root context for all value based entity contexts. */ +public interface EntityContext extends io.cloudstate.javasupport.EntityContext {} diff --git a/java-support/src/main/java/io/cloudstate/javasupport/valueentity/ValueEntityCreationContext.java b/java-support/src/main/java/io/cloudstate/javasupport/entity/EntityCreationContext.java similarity index 77% rename from java-support/src/main/java/io/cloudstate/javasupport/valueentity/ValueEntityCreationContext.java rename to java-support/src/main/java/io/cloudstate/javasupport/entity/EntityCreationContext.java index 2afff732b..e87d616a7 100644 --- a/java-support/src/main/java/io/cloudstate/javasupport/valueentity/ValueEntityCreationContext.java +++ b/java-support/src/main/java/io/cloudstate/javasupport/entity/EntityCreationContext.java @@ -14,11 +14,11 @@ * limitations under the License. */ -package io.cloudstate.javasupport.valueentity; +package io.cloudstate.javasupport.entity; /** - * Creation context for {@link ValueEntity} annotated entities. + * Creation context for {@link Entity} annotated entities. * - *

This may be accepted as an argument to the constructor of a value entity. + *

This may be accepted as an argument to the constructor of an value based entity. */ -public interface ValueEntityCreationContext extends ValueEntityContext {} +public interface EntityCreationContext extends EntityContext {} diff --git a/java-support/src/main/java/io/cloudstate/javasupport/valueentity/ValueEntityFactory.java b/java-support/src/main/java/io/cloudstate/javasupport/entity/EntityFactory.java similarity index 77% rename from java-support/src/main/java/io/cloudstate/javasupport/valueentity/ValueEntityFactory.java rename to java-support/src/main/java/io/cloudstate/javasupport/entity/EntityFactory.java index 6fc600425..6ea6b3476 100644 --- a/java-support/src/main/java/io/cloudstate/javasupport/valueentity/ValueEntityFactory.java +++ b/java-support/src/main/java/io/cloudstate/javasupport/entity/EntityFactory.java @@ -14,22 +14,20 @@ * limitations under the License. */ -package io.cloudstate.javasupport.valueentity; - -import io.cloudstate.javasupport.eventsourced.CommandHandler; +package io.cloudstate.javasupport.entity; /** - * Low level interface for handling commands on a value entity. + * Low level interface for handling commands on an value based entity. * *

Generally, this should not be needed, instead, a class annotated with the {@link * CommandHandler} and similar annotations should be used. */ -public interface ValueEntityFactory { +public interface EntityFactory { /** * Create an entity handler for the given context. * * @param context The context. * @return The handler for the given context. */ - ValueEntityHandler create(ValueEntityContext context); + EntityHandler create(EntityContext context); } diff --git a/java-support/src/main/java/io/cloudstate/javasupport/valueentity/ValueEntityHandler.java b/java-support/src/main/java/io/cloudstate/javasupport/entity/EntityHandler.java similarity index 88% rename from java-support/src/main/java/io/cloudstate/javasupport/valueentity/ValueEntityHandler.java rename to java-support/src/main/java/io/cloudstate/javasupport/entity/EntityHandler.java index 57e0d6db0..ed2edc856 100644 --- a/java-support/src/main/java/io/cloudstate/javasupport/valueentity/ValueEntityHandler.java +++ b/java-support/src/main/java/io/cloudstate/javasupport/entity/EntityHandler.java @@ -14,19 +14,19 @@ * limitations under the License. */ -package io.cloudstate.javasupport.valueentity; +package io.cloudstate.javasupport.entity; import com.google.protobuf.Any; import java.util.Optional; /** - * Low level interface for handling commands on a value entity. + * Low level interface for handling commands on an value based entity. * *

Generally, this should not be needed, instead, a class annotated with the {@link * CommandHandler} and similar annotations should be used. */ -public interface ValueEntityHandler { +public interface EntityHandler { /** * Handle the given command. diff --git a/java-support/src/main/java/io/cloudstate/javasupport/entity/package-info.java b/java-support/src/main/java/io/cloudstate/javasupport/entity/package-info.java new file mode 100644 index 000000000..0c6dd0e7c --- /dev/null +++ b/java-support/src/main/java/io/cloudstate/javasupport/entity/package-info.java @@ -0,0 +1,8 @@ +/** + * Value based entity support. + * + *

Value based entities can be annotated with the {@link + * io.cloudstate.javasupport.entity.Entity @Entity} annotation, and supply command handlers using + * the {@link io.cloudstate.javasupport.entity.CommandHandler @CommandHandler} annotation. + */ +package io.cloudstate.javasupport.entity; diff --git a/java-support/src/main/java/io/cloudstate/javasupport/valueentity/package-info.java b/java-support/src/main/java/io/cloudstate/javasupport/valueentity/package-info.java deleted file mode 100644 index 13248704a..000000000 --- a/java-support/src/main/java/io/cloudstate/javasupport/valueentity/package-info.java +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Value entity support. - * - *

CRUD entities can be annotated with the {@link - * io.cloudstate.javasupport.valueentity.ValueEntity @ValueEntity} annotation, and supply command - * handlers using the {@link io.cloudstate.javasupport.valueentity.CommandHandler @CommandHandler} - * annotation. - */ -package io.cloudstate.javasupport.valueentity; diff --git a/java-support/src/main/scala/io/cloudstate/javasupport/CloudStateRunner.scala b/java-support/src/main/scala/io/cloudstate/javasupport/CloudStateRunner.scala index 6573e0c78..a2a1e54a5 100644 --- a/java-support/src/main/scala/io/cloudstate/javasupport/CloudStateRunner.scala +++ b/java-support/src/main/scala/io/cloudstate/javasupport/CloudStateRunner.scala @@ -29,12 +29,12 @@ import io.cloudstate.javasupport.impl.action.{ActionProtocolImpl, ActionService} import io.cloudstate.javasupport.impl.eventsourced.{EventSourcedImpl, EventSourcedStatefulService} import io.cloudstate.javasupport.impl.{EntityDiscoveryImpl, ResolvedServiceCallFactory, ResolvedServiceMethod} import io.cloudstate.javasupport.impl.crdt.{CrdtImpl, CrdtStatefulService} -import io.cloudstate.javasupport.impl.valueentity.{ValueEntityImpl, ValueEntityStatefulService} +import io.cloudstate.javasupport.impl.entity.{ValueEntityImpl, ValueEntityStatefulService} import io.cloudstate.protocol.action.ActionProtocolHandler import io.cloudstate.protocol.crdt.CrdtHandler -import io.cloudstate.protocol.value_entity.ValueEntityProtocolHandler import io.cloudstate.protocol.entity.EntityDiscoveryHandler import io.cloudstate.protocol.event_sourced.EventSourcedHandler +import io.cloudstate.protocol.value_entity.ValueEntityHandler import scala.compat.java8.FutureConverters import scala.concurrent.Future @@ -116,10 +116,10 @@ final class CloudStateRunner private[this] ( val actionImpl = new ActionProtocolImpl(system, actionServices, rootContext) route orElse ActionProtocolHandler.partial(actionImpl) - case (route, (serviceClass, valueEntityServices: Map[String, ValueEntityStatefulService] @unchecked)) + case (route, (serviceClass, entityServices: Map[String, ValueEntityStatefulService] @unchecked)) if serviceClass == classOf[ValueEntityStatefulService] => - val valueEntityImpl = new ValueEntityImpl(system, valueEntityServices, rootContext, configuration) - route orElse ValueEntityProtocolHandler.partial(valueEntityImpl) + val valueEntityImpl = new ValueEntityImpl(system, entityServices, rootContext, configuration) + route orElse ValueEntityHandler.partial(valueEntityImpl) case (_, (serviceClass, _)) => sys.error(s"Unknown StatefulService: $serviceClass") diff --git a/java-support/src/main/scala/io/cloudstate/javasupport/impl/EntityExceptions.scala b/java-support/src/main/scala/io/cloudstate/javasupport/impl/EntityExceptions.scala index ac4f7845b..40f4ad256 100644 --- a/java-support/src/main/scala/io/cloudstate/javasupport/impl/EntityExceptions.scala +++ b/java-support/src/main/scala/io/cloudstate/javasupport/impl/EntityExceptions.scala @@ -16,7 +16,7 @@ package io.cloudstate.javasupport.impl -import io.cloudstate.javasupport.valueentity +import io.cloudstate.javasupport.entity import io.cloudstate.javasupport.eventsourced import io.cloudstate.protocol.value_entity.ValueEntityInit import io.cloudstate.protocol.entity.{Command, Failure} @@ -34,7 +34,7 @@ object EntityExceptions { def apply(command: Command, message: String): EntityException = EntityException(command.entityId, command.id, command.name, message) - def apply(context: valueentity.CommandContext[_], message: String): EntityException = + def apply(context: entity.CommandContext[_], message: String): EntityException = EntityException(context.entityId, context.commandId, context.commandName, message) def apply(context: eventsourced.CommandContext, message: String): EntityException = diff --git a/java-support/src/main/scala/io/cloudstate/javasupport/impl/valueentity/AnnotationBasedValueEntitySupport.scala b/java-support/src/main/scala/io/cloudstate/javasupport/impl/entity/AnnotationBasedEntitySupport.scala similarity index 83% rename from java-support/src/main/scala/io/cloudstate/javasupport/impl/valueentity/AnnotationBasedValueEntitySupport.scala rename to java-support/src/main/scala/io/cloudstate/javasupport/impl/entity/AnnotationBasedEntitySupport.scala index 9ddde3a01..701167445 100644 --- a/java-support/src/main/scala/io/cloudstate/javasupport/impl/valueentity/AnnotationBasedValueEntitySupport.scala +++ b/java-support/src/main/scala/io/cloudstate/javasupport/impl/entity/AnnotationBasedEntitySupport.scala @@ -14,49 +14,54 @@ * limitations under the License. */ -package io.cloudstate.javasupport.impl.valueentity +package io.cloudstate.javasupport.impl.entity import java.lang.reflect.{Constructor, InvocationTargetException} import java.util.Optional import com.google.protobuf.{Descriptors, Any => JavaPbAny} import io.cloudstate.javasupport.{Metadata, ServiceCall, ServiceCallFactory} -import io.cloudstate.javasupport.valueentity._ +import io.cloudstate.javasupport.entity.{ + EntityFactory => ValueEntityFactory, + EntityHandler => ValueEntityHandler, + EntityContext => ValueEntityContext +} +import io.cloudstate.javasupport.entity._ import io.cloudstate.javasupport.impl.ReflectionHelper.{InvocationContext, MainArgumentParameterHandler} import io.cloudstate.javasupport.impl.EntityExceptions.EntityException import io.cloudstate.javasupport.impl.{AnySupport, ReflectionHelper, ResolvedEntityFactory, ResolvedServiceMethod} /** - * Annotation based implementation of the [[ValueEntityFactory]]. + * Annotation based implementation of the [[EntityFactory]]. */ -private[impl] class AnnotationBasedValueEntitySupport( +private[impl] class AnnotationBasedEntitySupport( entityClass: Class[_], anySupport: AnySupport, override val resolvedMethods: Map[String, ResolvedServiceMethod[_, _]], - factory: Option[ValueEntityCreationContext => AnyRef] = None + factory: Option[EntityCreationContext => AnyRef] = None ) extends ValueEntityFactory with ResolvedEntityFactory { def this(entityClass: Class[_], anySupport: AnySupport, serviceDescriptor: Descriptors.ServiceDescriptor) = this(entityClass, anySupport, anySupport.resolveServiceDescriptor(serviceDescriptor)) - private val behavior = ValueEntityBehaviorReflection(entityClass, resolvedMethods) + private val behavior = EntityBehaviorReflection(entityClass, resolvedMethods) - private val constructor: ValueEntityCreationContext => AnyRef = factory.getOrElse { + private val constructor: EntityCreationContext => AnyRef = factory.getOrElse { entityClass.getConstructors match { case Array(single) => new EntityConstructorInvoker(ReflectionHelper.ensureAccessible(single)) case _ => - throw new RuntimeException(s"Only a single constructor is allowed on CRUD entities: $entityClass") + throw new RuntimeException(s"Only a single constructor is allowed on value-based entities: $entityClass") } } - override def create(context: ValueEntityContext): ValueEntityHandler = + override def create(context: EntityContext): ValueEntityHandler = new EntityHandler(context) private class EntityHandler(context: ValueEntityContext) extends ValueEntityHandler { private val entity = { - constructor(new DelegatingValueEntityContext(context) with ValueEntityCreationContext { + constructor(new DelegatingEntityContext(context) with EntityCreationContext { override def entityId(): String = context.entityId() }) } @@ -85,19 +90,19 @@ private[impl] class AnnotationBasedValueEntitySupport( private def behaviorsString = entity.getClass.toString } - private abstract class DelegatingValueEntityContext(delegate: ValueEntityContext) extends ValueEntityContext { + private abstract class DelegatingEntityContext(delegate: ValueEntityContext) extends ValueEntityContext { override def entityId(): String = delegate.entityId() override def serviceCallFactory(): ServiceCallFactory = delegate.serviceCallFactory() } } -private class ValueEntityBehaviorReflection( +private class EntityBehaviorReflection( val commandHandlers: Map[String, ReflectionHelper.CommandHandlerInvoker[CommandContext[AnyRef]]] ) {} -private object ValueEntityBehaviorReflection { +private object EntityBehaviorReflection { def apply(behaviorClass: Class[_], - serviceMethods: Map[String, ResolvedServiceMethod[_, _]]): ValueEntityBehaviorReflection = { + serviceMethods: Map[String, ResolvedServiceMethod[_, _]]): EntityBehaviorReflection = { val allMethods = ReflectionHelper.getAllDeclaredMethods(behaviorClass) val commandHandlers = allMethods @@ -128,23 +133,23 @@ private object ValueEntityBehaviorReflection { ReflectionHelper.validateNoBadMethods( allMethods, - classOf[ValueEntity], + classOf[Entity], Set(classOf[CommandHandler]) ) - new ValueEntityBehaviorReflection(commandHandlers) + new EntityBehaviorReflection(commandHandlers) } } -private class EntityConstructorInvoker(constructor: Constructor[_]) extends (ValueEntityCreationContext => AnyRef) { - private val parameters = ReflectionHelper.getParameterHandlers[AnyRef, ValueEntityCreationContext](constructor)() +private class EntityConstructorInvoker(constructor: Constructor[_]) extends (EntityCreationContext => AnyRef) { + private val parameters = ReflectionHelper.getParameterHandlers[AnyRef, EntityCreationContext](constructor)() parameters.foreach { case MainArgumentParameterHandler(clazz) => throw new RuntimeException(s"Don't know how to handle argument of type $clazz in constructor") case _ => } - def apply(context: ValueEntityCreationContext): AnyRef = { + def apply(context: EntityCreationContext): AnyRef = { val ctx = InvocationContext(null.asInstanceOf[AnyRef], context) constructor.newInstance(parameters.map(_.apply(ctx)): _*).asInstanceOf[AnyRef] } diff --git a/java-support/src/main/scala/io/cloudstate/javasupport/impl/valueentity/ValueEntityImpl.scala b/java-support/src/main/scala/io/cloudstate/javasupport/impl/entity/EntityImpl.scala similarity index 93% rename from java-support/src/main/scala/io/cloudstate/javasupport/impl/valueentity/ValueEntityImpl.scala rename to java-support/src/main/scala/io/cloudstate/javasupport/impl/entity/EntityImpl.scala index f33b3669f..5a37c12e4 100644 --- a/java-support/src/main/scala/io/cloudstate/javasupport/impl/valueentity/ValueEntityImpl.scala +++ b/java-support/src/main/scala/io/cloudstate/javasupport/impl/entity/EntityImpl.scala @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.cloudstate.javasupport.impl.valueentity +package io.cloudstate.javasupport.impl.entity import java.util.Optional @@ -25,7 +25,7 @@ import akka.stream.scaladsl.Flow import io.cloudstate.javasupport.CloudStateRunner.Configuration import com.google.protobuf.{Descriptors, Any => JavaPbAny} import com.google.protobuf.any.{Any => ScalaPbAny} -import io.cloudstate.javasupport.valueentity._ +import io.cloudstate.javasupport.entity._ import io.cloudstate.javasupport.impl._ import io.cloudstate.javasupport.{Context, Metadata, Service, ServiceCallFactory} import io.cloudstate.protocol.value_entity.ValueEntityAction.Action.{Delete, Update} @@ -34,13 +34,14 @@ import io.cloudstate.protocol.value_entity.ValueEntityStreamIn.Message.{ Empty => InEmpty, Init => InInit } +import io.cloudstate.protocol.value_entity.ValueEntity import io.cloudstate.protocol.value_entity.ValueEntityStreamOut.Message.{Failure => OutFailure, Reply => OutReply} import io.cloudstate.protocol.value_entity._ import scala.compat.java8.OptionConverters._ import scala.util.control.NonFatal -final class ValueEntityStatefulService(val factory: ValueEntityFactory, +final class ValueEntityStatefulService(val factory: EntityFactory, override val descriptor: Descriptors.ServiceDescriptor, val anySupport: AnySupport, override val persistenceId: String) @@ -52,14 +53,14 @@ final class ValueEntityStatefulService(val factory: ValueEntityFactory, case _ => None } - override final val entityType = io.cloudstate.protocol.value_entity.ValueEntityProtocol.name + override final val entityType = ValueEntity.name } final class ValueEntityImpl(_system: ActorSystem, _services: Map[String, ValueEntityStatefulService], rootContext: Context, configuration: Configuration) - extends ValueEntityProtocol { + extends ValueEntity { import EntityExceptions._ @@ -96,7 +97,7 @@ final class ValueEntityImpl(_system: ActorSystem, private def runEntity(init: ValueEntityInit): Flow[ValueEntityStreamIn, ValueEntityStreamOut, NotUsed] = { val service = services.getOrElse(init.serviceName, throw ProtocolException(init, s"Service not found: ${init.serviceName}")) - val handler = service.factory.create(new ValueEntityContextImpl(init.entityId)) + val handler = service.factory.create(new EntityContextImpl(init.entityId)) val thisEntityId = init.entityId val initState = init.state match { @@ -151,6 +152,7 @@ final class ValueEntityImpl(_system: ActorSystem, ) )) } else { + // rollback the state if something went wrong by using the old state (state, Some( OutReply( @@ -173,7 +175,7 @@ final class ValueEntityImpl(_system: ActorSystem, } } - trait AbstractContext extends ValueEntityContext { + trait AbstractContext extends EntityContext { override def serviceCallFactory(): ServiceCallFactory = rootContext.serviceCallFactory() } @@ -223,8 +225,6 @@ final class ValueEntityImpl(_system: ActorSystem, } - private final class ValueEntityContextImpl(override final val entityId: String) - extends ValueEntityContext - with AbstractContext + private final class EntityContextImpl(override final val entityId: String) extends EntityContext with AbstractContext } diff --git a/java-support/src/main/scala/io/cloudstate/javasupport/impl/eventsourced/EventSourcedImpl.scala b/java-support/src/main/scala/io/cloudstate/javasupport/impl/eventsourced/EventSourcedImpl.scala index 94e71d39c..dca5e2734 100644 --- a/java-support/src/main/scala/io/cloudstate/javasupport/impl/eventsourced/EventSourcedImpl.scala +++ b/java-support/src/main/scala/io/cloudstate/javasupport/impl/eventsourced/EventSourcedImpl.scala @@ -69,48 +69,6 @@ final class EventSourcedStatefulService(val factory: EventSourcedEntityFactory, this } -/* -object EventSourcedImpl { - final case class EntityException(entityId: String, commandId: Long, commandName: String, message: String) - extends RuntimeException(message) - - object EntityException { - def apply(message: String): EntityException = - EntityException(entityId = "", commandId = 0, commandName = "", message) - - def apply(command: Command, message: String): EntityException = - EntityException(command.entityId, command.id, command.name, message) - - def apply(context: CommandContext, message: String): EntityException = - EntityException(context.entityId, context.commandId, context.commandName, message) - } - - object ProtocolException { - def apply(message: String): EntityException = - EntityException(entityId = "", commandId = 0, commandName = "", "Protocol error: " + message) - - def apply(init: EventSourcedInit, message: String): EntityException = - EntityException(init.entityId, commandId = 0, commandName = "", "Protocol error: " + message) - - def apply(command: Command, message: String): EntityException = - EntityException(command.entityId, command.id, command.name, "Protocol error: " + message) - } - - def failure(cause: Throwable): Failure = cause match { - case e: EntityException => Failure(e.commandId, e.message) - case e => Failure(description = "Unexpected failure: " + e.getMessage) - } - - def failureMessage(cause: Throwable): String = cause match { - case EntityException(entityId, commandId, commandName, _) => - val commandDescription = if (commandId != 0) s" for command [$commandName]" else "" - val entityDescription = if (entityId.nonEmpty) s"entity [$entityId]" else "entity" - s"Terminating $entityDescription due to unexpected failure$commandDescription" - case _ => "Terminating entity due to unexpected failure" - } -} - */ - final class EventSourcedImpl(_system: ActorSystem, _services: Map[String, EventSourcedStatefulService], rootContext: Context, diff --git a/java-support/src/test/scala/io/cloudstate/javasupport/impl/valueentity/AnnotationBasedValueEntitySupportSpec.scala b/java-support/src/test/scala/io/cloudstate/javasupport/impl/entity/AnnotationBasedEntitySupportSpec.scala similarity index 90% rename from java-support/src/test/scala/io/cloudstate/javasupport/impl/valueentity/AnnotationBasedValueEntitySupportSpec.scala rename to java-support/src/test/scala/io/cloudstate/javasupport/impl/entity/AnnotationBasedEntitySupportSpec.scala index 0df7e93c0..0fbe27fba 100644 --- a/java-support/src/test/scala/io/cloudstate/javasupport/impl/valueentity/AnnotationBasedValueEntitySupportSpec.scala +++ b/java-support/src/test/scala/io/cloudstate/javasupport/impl/entity/AnnotationBasedEntitySupportSpec.scala @@ -14,21 +14,14 @@ * limitations under the License. */ -package io.cloudstate.javasupport.impl.valueentity +package io.cloudstate.javasupport.impl.entity import java.util.Optional import com.example.valueentity.shoppingcart.Shoppingcart import com.google.protobuf.any.{Any => ScalaPbAny} import com.google.protobuf.{ByteString, Any => JavaPbAny} -import io.cloudstate.javasupport.valueentity.{ - CommandContext, - CommandHandler, - ValueEntity, - ValueEntityContext, - ValueEntityCreationContext, - ValueEntityHandler -} +import io.cloudstate.javasupport.entity._ import io.cloudstate.javasupport.impl.{AnySupport, ResolvedServiceMethod, ResolvedType} import io.cloudstate.javasupport.{Context, EntityId, Metadata, ServiceCall, ServiceCallFactory, ServiceCallRef} import org.scalatest.{Matchers, WordSpec} @@ -43,7 +36,7 @@ class AnnotationBasedValueEntitySupportSpec extends WordSpec with Matchers { } } - object MockContext extends ValueEntityContext with BaseContext { + object MockContext extends EntityContext with BaseContext { override def entityId(): String = "foo" } @@ -84,14 +77,14 @@ class AnnotationBasedValueEntitySupportSpec extends WordSpec with Matchers { def method(name: String = "AddItem"): ResolvedServiceMethod[String, Wrapped] = ResolvedServiceMethod(serviceDescriptor.findMethodByName(name), StringResolvedType, WrappedResolvedType) - def create(behavior: AnyRef, methods: ResolvedServiceMethod[_, _]*): ValueEntityHandler = - new AnnotationBasedValueEntitySupport(behavior.getClass, - anySupport, - methods.map(m => m.descriptor.getName -> m).toMap, - Some(_ => behavior)).create(MockContext) + def create(behavior: AnyRef, methods: ResolvedServiceMethod[_, _]*): EntityHandler = + new AnnotationBasedEntitySupport(behavior.getClass, + anySupport, + methods.map(m => m.descriptor.getName -> m).toMap, + Some(_ => behavior)).create(MockContext) - def create(clazz: Class[_]): ValueEntityHandler = - new AnnotationBasedValueEntitySupport(clazz, anySupport, Map.empty, None).create(MockContext) + def create(clazz: Class[_]): EntityHandler = + new AnnotationBasedEntitySupport(clazz, anySupport, Map.empty, None).create(MockContext) def command(str: String) = ScalaPbAny.toJavaProto(ScalaPbAny(StringResolvedType.typeUrl, StringResolvedType.toByteString(str))) @@ -103,7 +96,7 @@ class AnnotationBasedValueEntitySupportSpec extends WordSpec with Matchers { def state(any: Any): JavaPbAny = anySupport.encodeJava(any) - "Value entity annotation support" should { + "Value based entity annotation support" should { "support entity construction" when { "there is a noarg constructor" in { @@ -274,24 +267,24 @@ class AnnotationBasedValueEntitySupportSpec extends WordSpec with Matchers { import Matchers._ -@ValueEntity +@Entity private class NoArgConstructorTest() {} -@ValueEntity +@Entity private class EntityIdArgConstructorTest(@EntityId entityId: String) { entityId should ===("foo") } -@ValueEntity -private class CreationContextArgConstructorTest(ctx: ValueEntityCreationContext) { +@Entity +private class CreationContextArgConstructorTest(ctx: EntityCreationContext) { ctx.entityId should ===("foo") } -@ValueEntity -private class MultiArgConstructorTest(ctx: ValueEntityContext, @EntityId entityId: String) { +@Entity +private class MultiArgConstructorTest(ctx: EntityContext, @EntityId entityId: String) { ctx.entityId should ===("foo") entityId should ===("foo") } -@ValueEntity +@Entity private class UnsupportedConstructorParameter(foo: String) diff --git a/java-support/src/test/scala/io/cloudstate/javasupport/impl/valueentity/ValueEntityImplSpec.scala b/java-support/src/test/scala/io/cloudstate/javasupport/impl/entity/EntityImplSpec.scala similarity index 94% rename from java-support/src/test/scala/io/cloudstate/javasupport/impl/valueentity/ValueEntityImplSpec.scala rename to java-support/src/test/scala/io/cloudstate/javasupport/impl/entity/EntityImplSpec.scala index 3942580cb..332e7cbb2 100644 --- a/java-support/src/test/scala/io/cloudstate/javasupport/impl/valueentity/ValueEntityImplSpec.scala +++ b/java-support/src/test/scala/io/cloudstate/javasupport/impl/entity/EntityImplSpec.scala @@ -14,27 +14,27 @@ * limitations under the License. */ -package io.cloudstate.javasupport.impl.valueentity +package io.cloudstate.javasupport.impl.entity import java.util.Optional import com.google.protobuf.Empty import io.cloudstate.javasupport.EntityId -import io.cloudstate.javasupport.valueentity.{CommandContext, CommandHandler, ValueEntity} +import io.cloudstate.javasupport.entity.{CommandContext, CommandHandler, Entity} import io.cloudstate.testkit.TestProtocol -import io.cloudstate.testkit.valuentity.ValueEntityMessages +import io.cloudstate.testkit.valueentity.ValueEntityMessages import org.scalatest.{BeforeAndAfterAll, Matchers, WordSpec} import scala.collection.mutable import scala.reflect.ClassTag -class ValueEntityImplSpec extends WordSpec with Matchers with BeforeAndAfterAll { - import ValueEntityImplSpec._ +class EntityImplSpec extends WordSpec with Matchers with BeforeAndAfterAll { + import EntityImplSpec._ import ValueEntityMessages._ import ShoppingCart.Item import ShoppingCart.Protocol._ - private val service: TestValueEntityService = ShoppingCart.testService + private val service: TestEntityService = ShoppingCart.testService private val protocol: TestProtocol = TestProtocol(service.port) override def afterAll(): Unit = { @@ -42,7 +42,7 @@ class ValueEntityImplSpec extends WordSpec with Matchers with BeforeAndAfterAll service.terminate() } - "ValueEntityImpl" should { + "EntityImpl" should { "fail when first message is not init" in { service.expectLogError("Terminating entity due to unexpected failure") { val entity = protocol.valueEntity.connect() @@ -187,7 +187,7 @@ class ValueEntityImplSpec extends WordSpec with Matchers with BeforeAndAfterAll } } -object ValueEntityImplSpec { +object EntityImplSpec { object ShoppingCart { import com.example.valueentity.shoppingcart.Shoppingcart @@ -195,10 +195,10 @@ object ValueEntityImplSpec { val Name: String = Shoppingcart.getDescriptor.findServiceByName("ShoppingCart").getFullName - def testService: TestValueEntityService = service[TestCart] + def testService: TestEntityService = service[TestCart] - def service[T: ClassTag]: TestValueEntityService = - TestValueEntity.service[T]( + def service[T: ClassTag]: TestEntityService = + TestEntity.service[T]( Shoppingcart.getDescriptor.findServiceByName("ShoppingCart"), Domain.getDescriptor ) @@ -240,7 +240,7 @@ object ValueEntityImplSpec { val TestCartClass: Class[_] = classOf[TestCart] - @ValueEntity(persistenceId = "crud-shopping-cart") + @Entity(persistenceId = "valuebased-entity-shopping-cart") class TestCart(@EntityId val entityId: String) { import scala.jdk.OptionConverters._ import scala.jdk.CollectionConverters._ diff --git a/java-support/src/test/scala/io/cloudstate/javasupport/impl/valueentity/TestValueEntity.scala b/java-support/src/test/scala/io/cloudstate/javasupport/impl/entity/TestEntity.scala similarity index 76% rename from java-support/src/test/scala/io/cloudstate/javasupport/impl/valueentity/TestValueEntity.scala rename to java-support/src/test/scala/io/cloudstate/javasupport/impl/entity/TestEntity.scala index 4a5e5446b..a65b9729c 100644 --- a/java-support/src/test/scala/io/cloudstate/javasupport/impl/valueentity/TestValueEntity.scala +++ b/java-support/src/test/scala/io/cloudstate/javasupport/impl/entity/TestEntity.scala @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.cloudstate.javasupport.impl.valueentity +package io.cloudstate.javasupport.impl.entity import akka.testkit.EventFilter import com.google.protobuf.Descriptors.{FileDescriptor, ServiceDescriptor} @@ -23,14 +23,12 @@ import io.cloudstate.javasupport.{CloudState, CloudStateRunner} import scala.reflect.ClassTag import io.cloudstate.testkit.Sockets -object TestValueEntity { - def service[T: ClassTag](descriptor: ServiceDescriptor, fileDescriptors: FileDescriptor*): TestValueEntityService = - new TestValueEntityService(implicitly[ClassTag[T]].runtimeClass, descriptor, fileDescriptors) +object TestEntity { + def service[T: ClassTag](descriptor: ServiceDescriptor, fileDescriptors: FileDescriptor*): TestEntityService = + new TestEntityService(implicitly[ClassTag[T]].runtimeClass, descriptor, fileDescriptors) } -class TestValueEntityService(entityClass: Class[_], - descriptor: ServiceDescriptor, - fileDescriptors: Seq[FileDescriptor]) { +class TestEntityService(entityClass: Class[_], descriptor: ServiceDescriptor, fileDescriptors: Seq[FileDescriptor]) { val port: Int = Sockets.temporaryLocalPort() val config: Config = ConfigFactory.load(ConfigFactory.parseString(s""" @@ -46,7 +44,7 @@ class TestValueEntityService(entityClass: Class[_], """)) val runner: CloudStateRunner = new CloudState() - .registerValueEntity(entityClass, descriptor, fileDescriptors: _*) + .registerEntity(entityClass, descriptor, fileDescriptors: _*) .createRunner(config) runner.run() diff --git a/java-support/tck/src/main/java/io/cloudstate/javasupport/tck/JavaSupportTck.java b/java-support/tck/src/main/java/io/cloudstate/javasupport/tck/JavaSupportTck.java index cbd8fc6fb..a087ed3b3 100644 --- a/java-support/tck/src/main/java/io/cloudstate/javasupport/tck/JavaSupportTck.java +++ b/java-support/tck/src/main/java/io/cloudstate/javasupport/tck/JavaSupportTck.java @@ -16,19 +16,30 @@ package io.cloudstate.javasupport.tck; -import com.example.shoppingcart.Shoppingcart; +import com.example.valueentity.shoppingcart.Shoppingcart; import io.cloudstate.javasupport.CloudState; -import io.cloudstate.javasupport.tck.model.valuentity.ValueEntityTckModelEntity; -import io.cloudstate.javasupport.tck.model.valuentity.ValueEntityTwoEntity; +import io.cloudstate.javasupport.tck.model.valuebased.ValueEntityTckModelEntity; +import io.cloudstate.javasupport.tck.model.valuebased.ValueEntityTwoEntity; import io.cloudstate.javasupport.tck.model.eventsourced.EventSourcedTckModelEntity; import io.cloudstate.javasupport.tck.model.eventsourced.EventSourcedTwoEntity; import io.cloudstate.samples.shoppingcart.ShoppingCartEntity; import io.cloudstate.tck.model.Eventsourced; -import io.cloudstate.tck.model.valuentity.Valueentity; +import io.cloudstate.tck.model.valueentity.Valueentity; public final class JavaSupportTck { public static final void main(String[] args) throws Exception { new CloudState() + .registerEntity( + ValueEntityTckModelEntity.class, + Valueentity.getDescriptor().findServiceByName("ValueEntityTckModel"), + Valueentity.getDescriptor()) + .registerEntity( + ValueEntityTwoEntity.class, + Valueentity.getDescriptor().findServiceByName("ValueEntityTwo")) + .registerEntity( + ShoppingCartEntity.class, + Shoppingcart.getDescriptor().findServiceByName("ShoppingCart"), + com.example.valueentity.shoppingcart.persistence.Domain.getDescriptor()) .registerEventSourcedEntity( EventSourcedTckModelEntity.class, Eventsourced.getDescriptor().findServiceByName("EventSourcedTckModel"), @@ -37,21 +48,9 @@ public static final void main(String[] args) throws Exception { EventSourcedTwoEntity.class, Eventsourced.getDescriptor().findServiceByName("EventSourcedTwo")) .registerEventSourcedEntity( - ShoppingCartEntity.class, - Shoppingcart.getDescriptor().findServiceByName("ShoppingCart"), + io.cloudstate.samples.eventsourced.shoppingcart.ShoppingCartEntity.class, + com.example.shoppingcart.Shoppingcart.getDescriptor().findServiceByName("ShoppingCart"), com.example.shoppingcart.persistence.Domain.getDescriptor()) - .registerValueEntity( - ValueEntityTckModelEntity.class, - Valueentity.getDescriptor().findServiceByName("ValueEntityTckModel"), - Valueentity.getDescriptor()) - .registerValueEntity( - ValueEntityTwoEntity.class, - Valueentity.getDescriptor().findServiceByName("ValueEntityTwo")) - .registerValueEntity( - io.cloudstate.samples.valueentity.shoppingcart.ShoppingCartEntity.class, - com.example.valueentity.shoppingcart.Shoppingcart.getDescriptor() - .findServiceByName("ShoppingCart"), - com.example.valueentity.shoppingcart.persistence.Domain.getDescriptor()) .start() .toCompletableFuture() .get(); diff --git a/java-support/tck/src/main/java/io/cloudstate/javasupport/tck/model/valuentity/ValueEntityTckModelEntity.java b/java-support/tck/src/main/java/io/cloudstate/javasupport/tck/model/valuebased/ValueEntityTckModelEntity.java similarity index 76% rename from java-support/tck/src/main/java/io/cloudstate/javasupport/tck/model/valuentity/ValueEntityTckModelEntity.java rename to java-support/tck/src/main/java/io/cloudstate/javasupport/tck/model/valuebased/ValueEntityTckModelEntity.java index 9f79af986..5ea436f92 100644 --- a/java-support/tck/src/main/java/io/cloudstate/javasupport/tck/model/valuentity/ValueEntityTckModelEntity.java +++ b/java-support/tck/src/main/java/io/cloudstate/javasupport/tck/model/valuebased/ValueEntityTckModelEntity.java @@ -14,20 +14,19 @@ * limitations under the License. */ -package io.cloudstate.javasupport.tck.model.valuentity; +package io.cloudstate.javasupport.tck.model.valuebased; import io.cloudstate.javasupport.Context; import io.cloudstate.javasupport.ServiceCall; import io.cloudstate.javasupport.ServiceCallRef; -import io.cloudstate.javasupport.valueentity.ValueEntity; -import io.cloudstate.javasupport.valueentity.CommandContext; -import io.cloudstate.javasupport.valueentity.CommandHandler; -import io.cloudstate.tck.model.valuentity.Valueentity; -import io.cloudstate.tck.model.valuentity.Valueentity.*; +import io.cloudstate.javasupport.entity.Entity; +import io.cloudstate.javasupport.entity.CommandContext; +import io.cloudstate.javasupport.entity.CommandHandler; +import io.cloudstate.tck.model.valueentity.Valueentity.*; import java.util.Optional; -@ValueEntity(persistenceId = "value-entity-tck-model") +@Entity(persistenceId = "value-entity-tck-model") public class ValueEntityTckModelEntity { private final ServiceCallRef serviceTwoCall; @@ -38,13 +37,13 @@ public ValueEntityTckModelEntity(Context context) { serviceTwoCall = context .serviceCallFactory() - .lookup("cloudstate.tck.model.valuentity.ValueEntityTwo", "Call", Request.class); + .lookup("cloudstate.tck.model.valueentity.ValueEntityTwo", "Call", Request.class); } @CommandHandler public Optional process(Request request, CommandContext context) { boolean forwarding = false; - for (Valueentity.RequestAction action : request.getActionsList()) { + for (RequestAction action : request.getActionsList()) { switch (action.getActionCase()) { case UPDATE: state = action.getUpdate().getValue(); @@ -59,7 +58,7 @@ public Optional process(Request request, CommandContext con context.forward(serviceTwoRequest(action.getForward().getId())); break; case EFFECT: - Valueentity.Effect effect = action.getEffect(); + Effect effect = action.getEffect(); context.effect(serviceTwoRequest(effect.getId()), effect.getSynchronous()); break; case FAIL: diff --git a/java-support/tck/src/main/java/io/cloudstate/javasupport/tck/model/valuentity/ValueEntityTwoEntity.java b/java-support/tck/src/main/java/io/cloudstate/javasupport/tck/model/valuebased/ValueEntityTwoEntity.java similarity index 69% rename from java-support/tck/src/main/java/io/cloudstate/javasupport/tck/model/valuentity/ValueEntityTwoEntity.java rename to java-support/tck/src/main/java/io/cloudstate/javasupport/tck/model/valuebased/ValueEntityTwoEntity.java index f86e19cc7..ef5ecd4d7 100644 --- a/java-support/tck/src/main/java/io/cloudstate/javasupport/tck/model/valuentity/ValueEntityTwoEntity.java +++ b/java-support/tck/src/main/java/io/cloudstate/javasupport/tck/model/valuebased/ValueEntityTwoEntity.java @@ -14,14 +14,14 @@ * limitations under the License. */ -package io.cloudstate.javasupport.tck.model.valuentity; +package io.cloudstate.javasupport.tck.model.valuebased; -import io.cloudstate.javasupport.valueentity.ValueEntity; -import io.cloudstate.javasupport.valueentity.CommandHandler; -import io.cloudstate.tck.model.valuentity.Valueentity.Request; -import io.cloudstate.tck.model.valuentity.Valueentity.Response; +import io.cloudstate.javasupport.entity.Entity; +import io.cloudstate.javasupport.entity.CommandHandler; +import io.cloudstate.tck.model.valueentity.Valueentity.Request; +import io.cloudstate.tck.model.valueentity.Valueentity.Response; -@ValueEntity +@Entity(persistenceId = "value-entity-tck-model-two") public class ValueEntityTwoEntity { public ValueEntityTwoEntity() {} diff --git a/protocols/protocol/cloudstate/value_entity.proto b/protocols/protocol/cloudstate/value_entity.proto index 576f2aabd..109721ce2 100644 --- a/protocols/protocol/cloudstate/value_entity.proto +++ b/protocols/protocol/cloudstate/value_entity.proto @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -// gRPC interface for common messages and services for CRUD Entity user functions. +// gRPC interface for common messages and services for value-based Entity user functions. syntax = "proto3"; @@ -27,7 +27,7 @@ option java_package = "io.cloudstate.protocol"; option go_package = "github.com/cloudstateio/go-support/cloudstate/entity;entity"; // The Value Entity service -service ValueEntityProtocol { +service ValueEntity { // One stream will be established per active entity. // Once established, the first message sent will be Init, which contains the entity ID, and, diff --git a/protocols/tck/cloudstate/tck/model/valueentity.proto b/protocols/tck/cloudstate/tck/model/valueentity.proto index eeb6479f0..cee66f15a 100644 --- a/protocols/tck/cloudstate/tck/model/valueentity.proto +++ b/protocols/tck/cloudstate/tck/model/valueentity.proto @@ -13,17 +13,17 @@ // limitations under the License. // -// == Cloudstate TCK model test for value-entity entities == +// == Cloudstate TCK model test for value-based entities == // syntax = "proto3"; -package cloudstate.tck.model.valuentity; +package cloudstate.tck.model.valueentity; import "cloudstate/entity_key.proto"; -option java_package = "io.cloudstate.tck.model.valuentity"; -option go_package = "github.com/cloudstateio/go-support/tck/valuentity;valuentity"; +option java_package = "io.cloudstate.tck.model.valueentity"; +option go_package = "github.com/cloudstateio/go-support/tck/valueentity;valueentity"; // // The `ValueEntityTckModel` service should be implemented in the following ways: @@ -35,14 +35,14 @@ option go_package = "github.com/cloudstateio/go-support/tck/valuentity;valuentit // - The `Process` method receives a `Request` message with actions to take. // - Request actions must be processed in order, and can require updating state, deleting state, forwarding, side effects, or failing. // - The `Process` method must reply with the state in a `Response`, after taking actions, unless forwarding or failing. -// - Forwarding and side effects must always be made to the second service `CrudTwo`. +// - Forwarding and side effects must always be made to the second service `ValueEntityTwo`. // service ValueEntityTckModel { rpc Process(Request) returns (Response); } // -// The `ValueEntityTwo` service is only for verifying forward actions and side effects. +// The `ValueBasedTwo` service is only for verifying forward actions and side effects. // The `Call` method is not required to do anything, and may simply return an empty `Response` message. // service ValueEntityTwo { @@ -90,7 +90,7 @@ message Update { message Delete {} // -// Replace the response with a forward to `cloudstate.tck.model.valuentity.ValueEntityTwo/Call`. +// Replace the response with a forward to `cloudstate.tck.model.valueentity.ValueEntityTwo/Call`. // The payload must be a `Request` message with the given `id`. // message Forward { @@ -98,7 +98,7 @@ message Forward { } // -// Add a side effect to the reply, to `cloudstate.tck.model.valuentity.ValueEntityTwo/Call`. +// Add a side effect to the reply, to `cloudstate.tck.model.valueentity.ValueEntityTwo/Call`. // The payload must be a `Request` message with the given `id`. // The side effect should be marked synchronous based on the given `synchronous` value. // diff --git a/proxy/core/src/main/scala/io/cloudstate/proxy/EntityDiscoveryManager.scala b/proxy/core/src/main/scala/io/cloudstate/proxy/EntityDiscoveryManager.scala index 59b15c263..3507a2332 100644 --- a/proxy/core/src/main/scala/io/cloudstate/proxy/EntityDiscoveryManager.scala +++ b/proxy/core/src/main/scala/io/cloudstate/proxy/EntityDiscoveryManager.scala @@ -32,12 +32,12 @@ import com.typesafe.config.Config import io.cloudstate.protocol.action.ActionProtocol import io.cloudstate.protocol.entity._ import io.cloudstate.protocol.crdt.Crdt -import io.cloudstate.protocol.value_entity.ValueEntityProtocol +import io.cloudstate.protocol.value_entity.ValueEntity import io.cloudstate.protocol.event_sourced.EventSourced import io.cloudstate.proxy.action.ActionProtocolSupportFactory import io.cloudstate.proxy.crdt.CrdtSupportFactory -import io.cloudstate.proxy.valueentity.ValueEntitySupportFactory import io.cloudstate.proxy.eventsourced.EventSourcedSupportFactory +import io.cloudstate.proxy.valueentity.EntitySupportFactory import scala.concurrent.Future import scala.concurrent.duration._ @@ -139,7 +139,7 @@ class EntityDiscoveryManager(config: EntityDiscoveryManager.Configuration)( } ++ { if (config.valueEntityEnabled) Map( - ValueEntityProtocol.name -> new ValueEntitySupportFactory(system, config, clientSettings) + ValueEntity.name -> new EntitySupportFactory(system, config, clientSettings) ) else Map.empty } diff --git a/proxy/core/src/main/scala/io/cloudstate/proxy/eventsourced/DynamicLeastShardAllocationStrategy.scala b/proxy/core/src/main/scala/io/cloudstate/proxy/eventsourced/DynamicLeastShardAllocationStrategy.scala deleted file mode 100644 index ec21f592c..000000000 --- a/proxy/core/src/main/scala/io/cloudstate/proxy/eventsourced/DynamicLeastShardAllocationStrategy.scala +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright 2019 Lightbend Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.cloudstate.proxy.eventsourced - -import akka.actor.ActorRef -import akka.cluster.sharding.ShardCoordinator.ShardAllocationStrategy -import akka.cluster.sharding.ShardRegion.ShardId - -import scala.collection.immutable -import scala.concurrent.Future - -class DynamicLeastShardAllocationStrategy(rebalanceThreshold: Int, - maxSimultaneousRebalance: Int, - rebalanceNumber: Int, - rebalanceFactor: Double) - extends ShardAllocationStrategy - with Serializable { - - def this(rebalanceThreshold: Int, maxSimultaneousRebalance: Int) = - this(rebalanceThreshold, maxSimultaneousRebalance, rebalanceThreshold, 0.0) - - override def allocateShard( - requester: ActorRef, - shardId: ShardId, - currentShardAllocations: Map[ActorRef, immutable.IndexedSeq[ShardId]] - ): Future[ActorRef] = { - val (regionWithLeastShards, _) = currentShardAllocations.minBy { case (_, v) => v.size } - Future.successful(regionWithLeastShards) - } - - override def rebalance(currentShardAllocations: Map[ActorRef, immutable.IndexedSeq[ShardId]], - rebalanceInProgress: Set[ShardId]): Future[Set[ShardId]] = - if (rebalanceInProgress.size < maxSimultaneousRebalance) { - val (_, leastShards) = currentShardAllocations.minBy { case (_, v) => v.size } - val mostShards = currentShardAllocations - .collect { - case (_, v) => v.filterNot(s => rebalanceInProgress(s)) - } - .maxBy(_.size) - val difference = mostShards.size - leastShards.size - if (difference > rebalanceThreshold) { - - val factoredRebalanceLimit = (rebalanceFactor, rebalanceNumber) match { - // This condition is to maintain semantic backwards compatibility, from when rebalanceThreshold was also - // the number of shards to move. - case (0.0, 0) => rebalanceThreshold - case (0.0, justAbsolute) => justAbsolute - case (justFactor, 0) => math.max((justFactor * mostShards.size).round.toInt, 1) - case (factor, absolute) => math.min(math.max((factor * mostShards.size).round.toInt, 1), absolute) - } - - // The ideal number to rebalance to so these nodes have an even number of shards - val evenRebalance = difference / 2 - - val n = - math.min(math.min(factoredRebalanceLimit, evenRebalance), maxSimultaneousRebalance - rebalanceInProgress.size) - Future.successful(mostShards.sorted.take(n).toSet) - } else - emptyRebalanceResult - } else emptyRebalanceResult - - private[this] final val emptyRebalanceResult = Future.successful(Set.empty[ShardId]) -} diff --git a/proxy/core/src/main/scala/io/cloudstate/proxy/eventsourced/EventSourcedSupportFactory.scala b/proxy/core/src/main/scala/io/cloudstate/proxy/eventsourced/EventSourcedSupportFactory.scala index 5bd5ea629..86791c716 100644 --- a/proxy/core/src/main/scala/io/cloudstate/proxy/eventsourced/EventSourcedSupportFactory.scala +++ b/proxy/core/src/main/scala/io/cloudstate/proxy/eventsourced/EventSourcedSupportFactory.scala @@ -23,13 +23,14 @@ import akka.cluster.sharding.{ClusterSharding, ClusterShardingSettings} import akka.event.Logging import akka.grpc.GrpcClientSettings import akka.stream.Materializer -import akka.stream.scaladsl.{Flow, Source} +import akka.stream.scaladsl.Flow import akka.util.Timeout import com.google.protobuf.Descriptors.ServiceDescriptor import io.cloudstate.protocol.entity.{Entity, Metadata} import io.cloudstate.protocol.event_sourced.EventSourcedClient import io.cloudstate.proxy._ import io.cloudstate.proxy.entity.{EntityCommand, UserFunctionReply} +import io.cloudstate.proxy.sharding.DynamicLeastShardAllocationStrategy import scala.concurrent.{ExecutionContext, Future} import scala.collection.JavaConverters._ diff --git a/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/DynamicLeastShardAllocationStrategy.scala b/proxy/core/src/main/scala/io/cloudstate/proxy/sharding/DynamicLeastShardAllocationStrategy.scala similarity index 98% rename from proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/DynamicLeastShardAllocationStrategy.scala rename to proxy/core/src/main/scala/io/cloudstate/proxy/sharding/DynamicLeastShardAllocationStrategy.scala index e82c62b68..9845f76f9 100644 --- a/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/DynamicLeastShardAllocationStrategy.scala +++ b/proxy/core/src/main/scala/io/cloudstate/proxy/sharding/DynamicLeastShardAllocationStrategy.scala @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.cloudstate.proxy.valueentity +package io.cloudstate.proxy.sharding import akka.actor.ActorRef import akka.cluster.sharding.ShardCoordinator.ShardAllocationStrategy diff --git a/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/ValueEntity.scala b/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/Entity.scala similarity index 94% rename from proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/ValueEntity.scala rename to proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/Entity.scala index f284a87e6..40e3c274f 100644 --- a/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/ValueEntity.scala +++ b/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/Entity.scala @@ -26,11 +26,11 @@ import akka.pattern.pipe import akka.stream.scaladsl._ import akka.stream.{CompletionStrategy, Materializer, OverflowStrategy} import akka.util.Timeout -import io.cloudstate.protocol.value_entity._ import io.cloudstate.protocol.entity._ -import io.cloudstate.proxy.valueentity.store.JdbcRepository -import io.cloudstate.proxy.valueentity.store.JdbcStore.Key +import io.cloudstate.protocol.value_entity._ import io.cloudstate.proxy.entity.{EntityCommand, UserFunctionReply} +import io.cloudstate.proxy.valueentity.store.Repository +import io.cloudstate.proxy.valueentity.store.Store.Key import scala.collection.immutable.Queue import scala.concurrent.Future @@ -39,7 +39,7 @@ object ValueEntitySupervisor { private final case class Relay(actorRef: ActorRef) - def props(client: ValueEntityProtocolClient, configuration: ValueEntity.Configuration, repository: JdbcRepository)( + def props(client: ValueEntityClient, configuration: ValueEntity.Configuration, repository: Repository)( implicit mat: Materializer ): Props = Props(new ValueEntitySupervisor(client, configuration, repository)) @@ -55,9 +55,9 @@ object ValueEntitySupervisor { * persistence starts feeding us events. There's a race condition if we do this in the same persistent actor. This * establishes that connection first. */ -final class ValueEntitySupervisor(client: ValueEntityProtocolClient, +final class ValueEntitySupervisor(client: ValueEntityClient, configuration: ValueEntity.Configuration, - repository: JdbcRepository)(implicit mat: Materializer) + repository: Repository)(implicit mat: Materializer) extends Actor with Stash { @@ -97,12 +97,11 @@ object ValueEntityRelay { final case object Disconnect final case class Fail(cause: Throwable) - def props(client: ValueEntityProtocolClient, - configuration: ValueEntity.Configuration)(implicit mat: Materializer): Props = + def props(client: ValueEntityClient, configuration: ValueEntity.Configuration)(implicit mat: Materializer): Props = Props(new ValueEntityRelay(client, configuration)) } -final class ValueEntityRelay(client: ValueEntityProtocolClient, configuration: ValueEntity.Configuration)( +final class ValueEntityRelay(client: ValueEntityClient, configuration: ValueEntity.Configuration)( implicit mat: Materializer ) extends Actor with Stash { @@ -195,7 +194,7 @@ object ValueEntity { private case object WriteStateSuccess extends DatabaseOperationWriteStatus private case class WriteStateFailure(cause: Throwable) extends DatabaseOperationWriteStatus - final def props(configuration: Configuration, entityId: String, relay: ActorRef, repository: JdbcRepository): Props = + final def props(configuration: Configuration, entityId: String, relay: ActorRef, repository: Repository): Props = Props(new ValueEntity(configuration, entityId, relay, repository)) /** @@ -208,7 +207,7 @@ object ValueEntity { final class ValueEntity(configuration: ValueEntity.Configuration, entityId: String, relay: ActorRef, - repository: JdbcRepository) + repository: Repository) extends Actor with Stash with ActorLogging { @@ -407,11 +406,11 @@ final class ValueEntity(configuration: ValueEntity.Configuration, } private def performAction( - valueEntityAction: ValueEntityAction + action: ValueEntityAction )(handler: Unit => Unit): Future[ValueEntity.DatabaseOperationWriteStatus] = { import ValueEntityAction.Action._ - valueEntityAction.action match { + action.action match { case Update(ValueEntityUpdate(Some(value), _)) => repository .update(Key(persistenceId, entityId), value) diff --git a/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/ValueEntitySupportFactory.scala b/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/EntitySupportFactory.scala similarity index 80% rename from proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/ValueEntitySupportFactory.scala rename to proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/EntitySupportFactory.scala index 9e215ad34..ceee0e385 100644 --- a/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/ValueEntitySupportFactory.scala +++ b/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/EntitySupportFactory.scala @@ -26,25 +26,26 @@ import akka.stream.Materializer import akka.stream.scaladsl.Flow import akka.util.Timeout import com.google.protobuf.Descriptors.ServiceDescriptor -import io.cloudstate.protocol.value_entity.ValueEntityProtocolClient import io.cloudstate.protocol.entity.{Entity, Metadata} -import io.cloudstate.proxy._ -import io.cloudstate.proxy.valueentity.store.{JdbcRepositoryImpl, JdbcStoreSupport} +import io.cloudstate.protocol.value_entity.ValueEntityClient import io.cloudstate.proxy.entity.{EntityCommand, UserFunctionReply} +import io.cloudstate.proxy._ +import io.cloudstate.proxy.sharding.DynamicLeastShardAllocationStrategy +import io.cloudstate.proxy.valueentity.store.{RepositoryImpl, StoreSupport} import scala.concurrent.{ExecutionContext, Future} -class ValueEntitySupportFactory( +class EntitySupportFactory( system: ActorSystem, config: EntityDiscoveryManager.Configuration, grpcClientSettings: GrpcClientSettings )(implicit ec: ExecutionContext, mat: Materializer) extends EntityTypeSupportFactory - with JdbcStoreSupport { + with StoreSupport { private final val log = Logging.getLogger(system, this.getClass) - private val valueEntityClient = ValueEntityProtocolClient(grpcClientSettings)(system) + private val valueEntityClient = ValueEntityClient(grpcClientSettings)(system) override def buildEntityTypeSupport(entity: Entity, serviceDescriptor: ServiceDescriptor, @@ -56,7 +57,7 @@ class ValueEntitySupportFactory( config.passivationTimeout, config.relayOutputBufferSize) - val repository = new JdbcRepositoryImpl(createStore(config.config)) + val repository = new RepositoryImpl(createStore(config.config)) log.debug("Starting ValueEntity for {}", entity.persistenceId) val clusterSharding = ClusterSharding(system) @@ -65,12 +66,12 @@ class ValueEntitySupportFactory( typeName = entity.persistenceId, entityProps = ValueEntitySupervisor.props(valueEntityClient, stateManagerConfig, repository), settings = clusterShardingSettings, - messageExtractor = new ValueEntityIdExtractor(config.numberOfShards), + messageExtractor = new EntityIdExtractor(config.numberOfShards), allocationStrategy = new DynamicLeastShardAllocationStrategy(1, 10, 2, 0.0), handOffStopMessage = ValueEntity.Stop ) - new ValueEntitySupport(valueEntity, config.proxyParallelism, config.relayTimeout) + new EntitySupport(valueEntity, config.proxyParallelism, config.relayTimeout) } private def validate(serviceDescriptor: ServiceDescriptor, @@ -80,14 +81,14 @@ class ValueEntitySupportFactory( if (streamedMethods.nonEmpty) { val offendingMethods = streamedMethods.map(_.method.getName).mkString(",") throw EntityDiscoveryException( - s"Value entities do not support streamed methods, but ${serviceDescriptor.getFullName} has the following streamed methods: ${offendingMethods}" + s"Value based entities do not support streamed methods, but ${serviceDescriptor.getFullName} has the following streamed methods: ${offendingMethods}" ) } val methodsWithoutKeys = methodDescriptors.values.filter(_.keyFieldsCount < 1) if (methodsWithoutKeys.nonEmpty) { val offendingMethods = methodsWithoutKeys.map(_.method.getName).mkString(",") throw EntityDiscoveryException( - s"""Value entities do not support methods whose parameters do not have at least one field marked as entity_key, + s"""Value based entities do not support methods whose parameters do not have at least one field marked as entity_key, |but ${serviceDescriptor.getFullName} has the following methods without keys: $offendingMethods""".stripMargin .replaceAll("\n", " ") ) @@ -95,7 +96,7 @@ class ValueEntitySupportFactory( } } -private class ValueEntitySupport(crudEntity: ActorRef, parallelism: Int, private implicit val relayTimeout: Timeout) +private class EntitySupport(crudEntity: ActorRef, parallelism: Int, private implicit val relayTimeout: Timeout) extends EntityTypeSupport { import akka.pattern.ask @@ -111,7 +112,7 @@ private class ValueEntitySupport(crudEntity: ActorRef, parallelism: Int, private (crudEntity ? command).mapTo[UserFunctionReply] } -private final class ValueEntityIdExtractor(shards: Int) extends HashCodeMessageExtractor(shards) { +private final class EntityIdExtractor(shards: Int) extends HashCodeMessageExtractor(shards) { override final def entityId(message: Any): String = message match { case command: EntityCommand => command.entityId } diff --git a/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/JdbcInMemoryStore.scala b/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/InMemoryStore.scala similarity index 84% rename from proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/JdbcInMemoryStore.scala rename to proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/InMemoryStore.scala index aad382dbc..b86215ec9 100644 --- a/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/JdbcInMemoryStore.scala +++ b/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/InMemoryStore.scala @@ -17,12 +17,15 @@ package io.cloudstate.proxy.valueentity.store import akka.util.ByteString -import io.cloudstate.proxy.valueentity.store.JdbcStore.Key +import io.cloudstate.proxy.valueentity.store.Store.Key import scala.collection.concurrent.TrieMap import scala.concurrent.Future -final class JdbcInMemoryStore extends JdbcStore[Key, ByteString] { +/** + * Represents an in-memory implementation of the store for value-based entity. + */ +final class InMemoryStore extends Store[Key, ByteString] { private var store = TrieMap.empty[Key, ByteString] diff --git a/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/JdbcValueEntityTable.scala b/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/JdbcValueEntityTable.scala deleted file mode 100644 index 190f95786..000000000 --- a/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/JdbcValueEntityTable.scala +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright 2019 Lightbend Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.cloudstate.proxy.valueentity.store - -import io.cloudstate.proxy.valueentity.store.JdbcValueEntityTable.ValueEntityRow -import io.cloudstate.proxy.valueentity.store.JdbcStore.Key -import slick.lifted.{MappedProjection, ProvenShape} - -object JdbcValueEntityTable { - - case class ValueEntityRow(key: Key, state: Array[Byte]) -} - -trait JdbcValueEntityTable { - - val profile: slick.jdbc.JdbcProfile - - import profile.api._ - - def valueEntityTableCfg: JdbcValueEntityTableConfiguration - - class ValueEntityTable(tableTag: Tag) - extends Table[ValueEntityRow](_tableTag = tableTag, - _schemaName = valueEntityTableCfg.schemaName, - _tableName = valueEntityTableCfg.tableName) { - def * : ProvenShape[ValueEntityRow] = (key, state) <> (ValueEntityRow.tupled, ValueEntityRow.unapply) - - val persistentId: Rep[String] = - column[String](valueEntityTableCfg.columnNames.persistentId, O.Length(255, varying = true)) - val entityId: Rep[String] = column[String](valueEntityTableCfg.columnNames.entityId, O.Length(255, varying = true)) - val state: Rep[Array[Byte]] = column[Array[Byte]](valueEntityTableCfg.columnNames.state) - val key: MappedProjection[Key, (String, String)] = (persistentId, entityId) <> (Key.tupled, Key.unapply) - val pk = primaryKey(s"${tableName}_pk", (persistentId, entityId)) - } - - lazy val ValueEntityTableQuery = new TableQuery(tag => new ValueEntityTable(tag)) -} diff --git a/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/JdbcRepository.scala b/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/Repository.scala similarity index 86% rename from proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/JdbcRepository.scala rename to proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/Repository.scala index db25c23dd..0f52d751b 100644 --- a/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/JdbcRepository.scala +++ b/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/Repository.scala @@ -20,13 +20,14 @@ import akka.grpc.ProtobufSerializer import akka.util.ByteString import com.google.protobuf.any.{Any => ScalaPbAny} import com.google.protobuf.{ByteString => PbByteString} -import io.cloudstate.proxy.valueentity.store.JdbcStore.Key +import io.cloudstate.proxy.valueentity.store.Store.Key import scala.concurrent.{ExecutionContext, Future} -trait JdbcRepository { - - val store: JdbcStore[Key, ByteString] +/** + * Represents an interface for persisting value-based entity. + */ +trait Repository { /** * Retrieve the payload for the given key. @@ -54,7 +55,7 @@ trait JdbcRepository { } -object JdbcRepositoryImpl { +object RepositoryImpl { private[store] final object EntitySerializer extends ProtobufSerializer[ScalaPbAny] { private val separator = ByteString("|") @@ -75,12 +76,12 @@ object JdbcRepositoryImpl { } -class JdbcRepositoryImpl(val store: JdbcStore[Key, ByteString], serializer: ProtobufSerializer[ScalaPbAny])( +class RepositoryImpl(store: Store[Key, ByteString], serializer: ProtobufSerializer[ScalaPbAny])( implicit ec: ExecutionContext -) extends JdbcRepository { +) extends Repository { - def this(store: JdbcStore[Key, ByteString])(implicit ec: ExecutionContext) = - this(store, JdbcRepositoryImpl.EntitySerializer) + def this(store: Store[Key, ByteString])(implicit ec: ExecutionContext) = + this(store, RepositoryImpl.EntitySerializer) def get(key: Key): Future[Option[ScalaPbAny]] = store diff --git a/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/JdbcStore.scala b/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/Store.scala similarity index 53% rename from proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/JdbcStore.scala rename to proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/Store.scala index 6a6cd2ed3..07109c2be 100644 --- a/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/JdbcStore.scala +++ b/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/Store.scala @@ -16,25 +16,21 @@ package io.cloudstate.proxy.valueentity.store -import akka.util.ByteString -import io.cloudstate.proxy.valueentity.store.JdbcValueEntityTable.ValueEntityRow -import io.cloudstate.proxy.valueentity.store.JdbcStore.Key +import scala.concurrent.Future -import scala.concurrent.{ExecutionContext, Future} - -object JdbcStore { +object Store { case class Key(persistentId: String, entityId: String) } /** - * Represents an low level interface for accessing a native CRUD database. + * Represents an low level interface for accessing a native database. * - * @tparam K the type for CRUD database key - * @tparam V the type for CRUD database value + * @tparam K the type for database key + * @tparam V the type for database value */ -trait JdbcStore[K, V] { +trait Store[K, V] { /** * Retrieve the data for the given key. @@ -61,28 +57,3 @@ trait JdbcStore[K, V] { def delete(key: K): Future[Unit] } - -private[store] final class JdbcStoreImpl(slickDatabase: JdbcSlickDatabase, queries: JdbcValueEntityQueries)( - implicit ec: ExecutionContext -) extends JdbcStore[Key, ByteString] { - - import slickDatabase.profile.api._ - - private val db = slickDatabase.database - - override def get(key: Key): Future[Option[ByteString]] = - for { - rows <- db.run(queries.selectByKey(key).result) - } yield rows.headOption.map(r => ByteString(r.state)) - - override def update(key: Key, value: ByteString): Future[Unit] = - for { - _ <- db.run(queries.insertOrUpdate(ValueEntityRow(key, value.toByteBuffer.array()))) - } yield () - - override def delete(key: Key): Future[Unit] = - for { - _ <- db.run(queries.deleteByKey(key)) - } yield () - -} diff --git a/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/JdbcStoreSupport.scala b/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/StoreSupport.scala similarity index 70% rename from proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/JdbcStoreSupport.scala rename to proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/StoreSupport.scala index 5417ea6e8..131c9cb4b 100644 --- a/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/JdbcStoreSupport.scala +++ b/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/StoreSupport.scala @@ -18,21 +18,27 @@ package io.cloudstate.proxy.valueentity.store import akka.util.ByteString import com.typesafe.config.Config -import io.cloudstate.proxy.valueentity.store.JdbcStore.Key -import io.cloudstate.proxy.valueentity.store.JdbcStoreSupport.{IN_MEMORY, JDBC} +import StoreSupport.{IN_MEMORY, JDBC} +import io.cloudstate.proxy.valueentity.store.Store.Key +import io.cloudstate.proxy.valueentity.store.jdbc.{ + JdbcEntityQueries, + JdbcEntityTableConfiguration, + JdbcSlickDatabase, + JdbcStore +} import scala.concurrent.ExecutionContext; -object JdbcStoreSupport { +object StoreSupport { final val IN_MEMORY = "in-memory" final val JDBC = "jdbc" } -trait JdbcStoreSupport { +trait StoreSupport { - def createStore(config: Config)(implicit ec: ExecutionContext): JdbcStore[Key, ByteString] = + def createStore(config: Config)(implicit ec: ExecutionContext): Store[Key, ByteString] = config.getString("value-entity-persistence-store.store-type") match { - case IN_MEMORY => new JdbcInMemoryStore + case IN_MEMORY => new InMemoryStore case JDBC => createJdbcStore(config) case other => throw new IllegalArgumentException( @@ -40,13 +46,13 @@ trait JdbcStoreSupport { ) } - private def createJdbcStore(config: Config)(implicit ec: ExecutionContext): JdbcStore[Key, ByteString] = { + private def createJdbcStore(config: Config)(implicit ec: ExecutionContext): Store[Key, ByteString] = { val slickDatabase = JdbcSlickDatabase(config) - val tableConfiguration = new JdbcValueEntityTableConfiguration( + val tableConfiguration = new JdbcEntityTableConfiguration( config.getConfig("value-entity-persistence-store.jdbc-state-store") ) - val queries = new JdbcValueEntityQueries(slickDatabase.profile, tableConfiguration) - new JdbcStoreImpl(slickDatabase, queries) + val queries = new JdbcEntityQueries(slickDatabase.profile, tableConfiguration) + new JdbcStore(slickDatabase, queries) } } diff --git a/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/JdbcConfig.scala b/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/jdbc/JdbcConfig.scala similarity index 79% rename from proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/JdbcConfig.scala rename to proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/jdbc/JdbcConfig.scala index 6f59d3973..8e8540d40 100644 --- a/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/JdbcConfig.scala +++ b/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/jdbc/JdbcConfig.scala @@ -14,24 +14,24 @@ * limitations under the License. */ -package io.cloudstate.proxy.valueentity.store +package io.cloudstate.proxy.valueentity.store.jdbc import com.typesafe.config.Config import slick.basic.DatabaseConfig import slick.jdbc.JdbcBackend.Database import slick.jdbc.{JdbcBackend, JdbcProfile} -class JdbcValueEntityTableColumnNames(config: Config) { +class JdbcEntityTableColumnNames(config: Config) { private val cfg = config.getConfig("tables.state.columnNames") val persistentId: String = cfg.getString("persistentId") val entityId: String = cfg.getString("entityId") val state: String = cfg.getString("state") - override def toString: String = s"JdbcValueEntityTableColumnNames($persistentId,$entityId,$state)" + override def toString: String = s"JdbcEntityTableColumnNames($persistentId,$entityId,$state)" } -class JdbcValueEntityTableConfiguration(config: Config) { +class JdbcEntityTableConfiguration(config: Config) { private val cfg = config.getConfig("tables.state") val tableName: String = cfg.getString("tableName") @@ -39,9 +39,9 @@ class JdbcValueEntityTableConfiguration(config: Config) { case "" => None case schema => Some(schema.trim) } - val columnNames: JdbcValueEntityTableColumnNames = new JdbcValueEntityTableColumnNames(config) + val columnNames: JdbcEntityTableColumnNames = new JdbcEntityTableColumnNames(config) - override def toString: String = s"JdbcValueEntityTableColumnNames($tableName,$schemaName,$columnNames)" + override def toString: String = s"JdbcEntityTableConfiguration($tableName,$schemaName,$columnNames)" } object JdbcSlickDatabase { diff --git a/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/JdbcValueEntityQueries.scala b/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/jdbc/JdbcEntityQueries.scala similarity index 59% rename from proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/JdbcValueEntityQueries.scala rename to proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/jdbc/JdbcEntityQueries.scala index 0b3ed08d2..a594534c8 100644 --- a/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/JdbcValueEntityQueries.scala +++ b/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/jdbc/JdbcEntityQueries.scala @@ -14,28 +14,28 @@ * limitations under the License. */ -package io.cloudstate.proxy.valueentity.store +package io.cloudstate.proxy.valueentity.store.jdbc -import io.cloudstate.proxy.valueentity.store.JdbcValueEntityTable.ValueEntityRow -import io.cloudstate.proxy.valueentity.store.JdbcStore.Key +import io.cloudstate.proxy.valueentity.store.Store.Key +import io.cloudstate.proxy.valueentity.store.jdbc.JdbcEntityTable.EntityRow import slick.jdbc.JdbcProfile -private[store] class JdbcValueEntityQueries(val profile: JdbcProfile, - override val valueEntityTableCfg: JdbcValueEntityTableConfiguration) - extends JdbcValueEntityTable { +private[store] class JdbcEntityQueries(val profile: JdbcProfile, + override val entityTableCfg: JdbcEntityTableConfiguration) + extends JdbcEntityTable { import profile.api._ - def selectByKey(key: Key): Query[ValueEntityTable, ValueEntityRow, Seq] = - ValueEntityTableQuery + def selectByKey(key: Key): Query[EntityTable, EntityRow, Seq] = + Entity .filter(_.persistentId === key.persistentId) .filter(_.entityId === key.entityId) .take(1) - def insertOrUpdate(crudState: ValueEntityRow) = ValueEntityTableQuery.insertOrUpdate(crudState) + def insertOrUpdate(row: EntityRow) = Entity.insertOrUpdate(row) def deleteByKey(key: Key) = - ValueEntityTableQuery + Entity .filter(_.persistentId === key.persistentId) .filter(_.entityId === key.entityId) .delete diff --git a/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/jdbc/JdbcEntityTable.scala b/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/jdbc/JdbcEntityTable.scala new file mode 100644 index 000000000..f917a5602 --- /dev/null +++ b/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/jdbc/JdbcEntityTable.scala @@ -0,0 +1,51 @@ +/* + * Copyright 2019 Lightbend Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.cloudstate.proxy.valueentity.store.jdbc + +import io.cloudstate.proxy.valueentity.store.Store.Key +import io.cloudstate.proxy.valueentity.store.jdbc.JdbcEntityTable.EntityRow +import slick.lifted.{MappedProjection, ProvenShape} + +object JdbcEntityTable { + + case class EntityRow(key: Key, state: Array[Byte]) +} + +trait JdbcEntityTable { + + val profile: slick.jdbc.JdbcProfile + + import profile.api._ + + def entityTableCfg: JdbcEntityTableConfiguration + + class EntityTable(tableTag: Tag) + extends Table[EntityRow](_tableTag = tableTag, + _schemaName = entityTableCfg.schemaName, + _tableName = entityTableCfg.tableName) { + def * : ProvenShape[EntityRow] = (key, state) <> (EntityRow.tupled, EntityRow.unapply) + + val persistentId: Rep[String] = + column[String](entityTableCfg.columnNames.persistentId, O.Length(255, varying = true)) + val entityId: Rep[String] = column[String](entityTableCfg.columnNames.entityId, O.Length(255, varying = true)) + val state: Rep[Array[Byte]] = column[Array[Byte]](entityTableCfg.columnNames.state) + val key: MappedProjection[Key, (String, String)] = (persistentId, entityId) <> (Key.tupled, Key.unapply) + val pk = primaryKey(s"${tableName}_pk", (persistentId, entityId)) + } + + lazy val Entity = new TableQuery(tag => new EntityTable(tag)) +} diff --git a/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/jdbc/JdbcStore.scala b/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/jdbc/JdbcStore.scala new file mode 100644 index 000000000..737ec6d84 --- /dev/null +++ b/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/jdbc/JdbcStore.scala @@ -0,0 +1,49 @@ +/* + * Copyright 2019 Lightbend Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.cloudstate.proxy.valueentity.store.jdbc + +import akka.util.ByteString +import io.cloudstate.proxy.valueentity.store.Store +import io.cloudstate.proxy.valueentity.store.Store.Key +import io.cloudstate.proxy.valueentity.store.jdbc.JdbcEntityTable.EntityRow + +import scala.concurrent.{ExecutionContext, Future} + +private[store] final class JdbcStore(slickDatabase: JdbcSlickDatabase, queries: JdbcEntityQueries)( + implicit ec: ExecutionContext +) extends Store[Key, ByteString] { + + import slickDatabase.profile.api._ + + private val db = slickDatabase.database + + override def get(key: Key): Future[Option[ByteString]] = + for { + rows <- db.run(queries.selectByKey(key).result) + } yield rows.headOption.map(r => ByteString(r.state)) + + override def update(key: Key, value: ByteString): Future[Unit] = + for { + _ <- db.run(queries.insertOrUpdate(EntityRow(key, value.toByteBuffer.array()))) + } yield () + + override def delete(key: Key): Future[Unit] = + for { + _ <- db.run(queries.deleteByKey(key)) + } yield () + +} diff --git a/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/package-info.java b/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/package-info.java index 1ddfe28a5..7006eb2c5 100644 --- a/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/package-info.java +++ b/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/package-info.java @@ -1,4 +1,12 @@ /** - * Most part of the code in this package has been copied/adapted from https://github.com/akka/akka-persistence-jdbc + * Value based entity support for native database. + * + *

Value based entities can use an implementation of {@link + * io.cloudstate.proxy.valueentity.store.Repository Repository} to properly persist the entity. The {@link + * io.cloudstate.proxy.valueentity.store.Repository Repository} should be provide with an implementation + * of {@link io.cloudstate.proxy.valueentity.store.Store Store} to properly access the native the database or + * persistence. + * + *

Most part of the code in this package has been copied/adapted from https://github.com/akka/akka-persistence-jdbc. */ package io.cloudstate.proxy.valueentity.store; diff --git a/proxy/core/src/test/scala/io/cloudstate/proxy/TestProxy.scala b/proxy/core/src/test/scala/io/cloudstate/proxy/TestProxy.scala index db7668651..061605d4a 100644 --- a/proxy/core/src/test/scala/io/cloudstate/proxy/TestProxy.scala +++ b/proxy/core/src/test/scala/io/cloudstate/proxy/TestProxy.scala @@ -27,7 +27,7 @@ import io.cloudstate.protocol.action.ActionProtocol import io.cloudstate.testkit.Sockets import java.net.{ConnectException, Socket} -import io.cloudstate.protocol.value_entity.ValueEntityProtocol +import io.cloudstate.protocol.value_entity.ValueEntity import scala.concurrent.duration._ @@ -54,7 +54,7 @@ class TestProxy(servicePort: Int) { """)) val info: ProxyInfo = - EntityDiscoveryManager.proxyInfo(Seq(Crdt.name, ActionProtocol.name, EventSourced.name, ValueEntityProtocol.name)) + EntityDiscoveryManager.proxyInfo(Seq(Crdt.name, ActionProtocol.name, EventSourced.name, ValueEntity.name)) val system: ActorSystem = CloudStateProxyMain.start(config) diff --git a/proxy/core/src/test/scala/io/cloudstate/proxy/valueentity/DatabaseExceptionHandlingSpec.scala b/proxy/core/src/test/scala/io/cloudstate/proxy/valueentity/DatabaseExceptionHandlingSpec.scala index 81564e7f2..109b8f9f8 100644 --- a/proxy/core/src/test/scala/io/cloudstate/proxy/valueentity/DatabaseExceptionHandlingSpec.scala +++ b/proxy/core/src/test/scala/io/cloudstate/proxy/valueentity/DatabaseExceptionHandlingSpec.scala @@ -22,14 +22,14 @@ import akka.testkit.TestEvent.Mute import akka.testkit.{EventFilter, TestActorRef} import akka.util.ByteString import com.google.protobuf.any.{Any => ScalaPbAny} -import io.cloudstate.protocol.value_entity.ValueEntityProtocolClient import com.google.protobuf.{ByteString => PbByteString} -import io.cloudstate.proxy.valueentity.store.{JdbcRepositoryImpl, JdbcStore} -import io.cloudstate.proxy.valueentity.store.JdbcStore.Key +import io.cloudstate.protocol.value_entity.ValueEntityClient import io.cloudstate.proxy.entity.{EntityCommand, UserFunctionReply} import io.cloudstate.proxy.telemetry.AbstractTelemetrySpec +import io.cloudstate.proxy.valueentity.store.Store.Key +import io.cloudstate.proxy.valueentity.store.{RepositoryImpl, Store} import io.cloudstate.testkit.TestService -import io.cloudstate.testkit.valuentity.ValueEntityMessages +import io.cloudstate.testkit.valueentity.ValueEntityMessages import scala.concurrent.Future import scala.concurrent.duration._ @@ -53,7 +53,7 @@ class DatabaseExceptionHandlingSpec extends AbstractTelemetrySpec { sendQueueSize = 100 ) - "The ValueEntity" should { + "ValueEntity" should { "crash entity on init when loading state failures" in withTestKit(testkitConfig) { testKit => import testKit._ @@ -62,8 +62,8 @@ class DatabaseExceptionHandlingSpec extends AbstractTelemetrySpec { silentDeadLettersAndUnhandledMessages val client = - ValueEntityProtocolClient(GrpcClientSettings.connectToServiceAt("localhost", service.port).withTls(false)) - val repository = new JdbcRepositoryImpl(TestJdbcStore.storeWithGetFailure()) + ValueEntityClient(GrpcClientSettings.connectToServiceAt("localhost", service.port).withTls(false)) + val repository = new RepositoryImpl(TestJdbcStore.storeWithGetFailure()) val entity = watch(system.actorOf(ValueEntitySupervisor.props(client, entityConfiguration, repository), "entity")) val connection = service.valueEntity.expectConnection() @@ -71,8 +71,8 @@ class DatabaseExceptionHandlingSpec extends AbstractTelemetrySpec { } "crash entity on update state failures" in withTestKit(testkitConfig) { testKit => - import testKit._ import ValueEntityMessages._ + import testKit._ import system.dispatcher silentDeadLettersAndUnhandledMessages @@ -80,8 +80,8 @@ class DatabaseExceptionHandlingSpec extends AbstractTelemetrySpec { val forwardReply = forwardReplyActor(testActor) val client = - ValueEntityProtocolClient(GrpcClientSettings.connectToServiceAt("localhost", service.port).withTls(false)) - val repository = new JdbcRepositoryImpl(TestJdbcStore.storeWithUpdateFailure()) + ValueEntityClient(GrpcClientSettings.connectToServiceAt("localhost", service.port).withTls(false)) + val repository = new RepositoryImpl(TestJdbcStore.storeWithUpdateFailure()) val entity = watch(system.actorOf(ValueEntitySupervisor.props(client, entityConfiguration, repository), "entity")) val emptyCommand = Some(protobufAny(EmptyJavaMessage)) @@ -97,8 +97,8 @@ class DatabaseExceptionHandlingSpec extends AbstractTelemetrySpec { } "crash entity on delete state failures" in withTestKit(testkitConfig) { testKit => - import testKit._ import ValueEntityMessages._ + import testKit._ import system.dispatcher silentDeadLettersAndUnhandledMessages @@ -106,8 +106,8 @@ class DatabaseExceptionHandlingSpec extends AbstractTelemetrySpec { val forwardReply = forwardReplyActor(testActor) val client = - ValueEntityProtocolClient(GrpcClientSettings.connectToServiceAt("localhost", service.port).withTls(false)) - val repository = new JdbcRepositoryImpl(TestJdbcStore.storeWithDeleteFailure()) + ValueEntityClient(GrpcClientSettings.connectToServiceAt("localhost", service.port).withTls(false)) + val repository = new RepositoryImpl(TestJdbcStore.storeWithDeleteFailure()) val entity = watch(system.actorOf(ValueEntitySupervisor.props(client, entityConfiguration, repository), "entity")) val emptyCommand = Some(protobufAny(EmptyJavaMessage)) @@ -128,7 +128,7 @@ class DatabaseExceptionHandlingSpec extends AbstractTelemetrySpec { } } - private final class TestJdbcStore(status: String) extends JdbcStore[Key, ByteString] { + private final class TestJdbcStore(status: String) extends Store[Key, ByteString] { import TestJdbcStore.JdbcStoreStatus._ private var store = Map.empty[Key, ByteString] @@ -164,11 +164,11 @@ class DatabaseExceptionHandlingSpec extends AbstractTelemetrySpec { val deleteFailure = "DeleteFailure" } - def storeWithGetFailure(): JdbcStore[Key, ByteString] = new TestJdbcStore(JdbcStoreStatus.getFailure) + def storeWithGetFailure(): Store[Key, ByteString] = new TestJdbcStore(JdbcStoreStatus.getFailure) - def storeWithUpdateFailure(): JdbcStore[Key, ByteString] = new TestJdbcStore(JdbcStoreStatus.updateFailure) + def storeWithUpdateFailure(): Store[Key, ByteString] = new TestJdbcStore(JdbcStoreStatus.updateFailure) - def storeWithDeleteFailure(): JdbcStore[Key, ByteString] = new TestJdbcStore(JdbcStoreStatus.deleteFailure) + def storeWithDeleteFailure(): Store[Key, ByteString] = new TestJdbcStore(JdbcStoreStatus.deleteFailure) } private def silentDeadLettersAndUnhandledMessages(implicit system: ActorSystem): Unit = { diff --git a/proxy/core/src/test/scala/io/cloudstate/proxy/valueentity/ValueEntityPassivateSpec.scala b/proxy/core/src/test/scala/io/cloudstate/proxy/valueentity/EntityPassivateSpec.scala similarity index 87% rename from proxy/core/src/test/scala/io/cloudstate/proxy/valueentity/ValueEntityPassivateSpec.scala rename to proxy/core/src/test/scala/io/cloudstate/proxy/valueentity/EntityPassivateSpec.scala index 30b531ae2..f2a1f5c0e 100644 --- a/proxy/core/src/test/scala/io/cloudstate/proxy/valueentity/ValueEntityPassivateSpec.scala +++ b/proxy/core/src/test/scala/io/cloudstate/proxy/valueentity/EntityPassivateSpec.scala @@ -18,20 +18,20 @@ package io.cloudstate.proxy.valueentity import akka.actor.ActorRef import akka.grpc.GrpcClientSettings -import akka.testkit.TestEvent.Mute import akka.testkit.EventFilter +import akka.testkit.TestEvent.Mute import com.google.protobuf.ByteString import com.google.protobuf.any.{Any => ProtoAny} -import io.cloudstate.protocol.value_entity.ValueEntityProtocolClient +import io.cloudstate.protocol.value_entity.ValueEntityClient import io.cloudstate.proxy.entity.{EntityCommand, UserFunctionReply} import io.cloudstate.proxy.telemetry.AbstractTelemetrySpec -import io.cloudstate.proxy.valueentity.store.{JdbcInMemoryStore, JdbcRepositoryImpl} +import io.cloudstate.proxy.valueentity.store.{InMemoryStore, RepositoryImpl} import io.cloudstate.testkit.TestService -import io.cloudstate.testkit.valuentity.ValueEntityMessages +import io.cloudstate.testkit.valueentity.ValueEntityMessages import scala.concurrent.duration._ -class ValueEntityPassivateSpec extends AbstractTelemetrySpec { +class EntityPassivateSpec extends AbstractTelemetrySpec { "ValueEntity" should { @@ -47,8 +47,8 @@ class ValueEntityPassivateSpec extends AbstractTelemetrySpec { """ ) { testKit => import ValueEntityMessages._ - import testKit.system.dispatcher import testKit._ + import testKit.system.dispatcher // silence any dead letters or unhandled messages during shutdown (when using test event listener) system.eventStream.publish(Mute(EventFilter.warning(pattern = ".*received dead letter.*"))) @@ -58,7 +58,7 @@ class ValueEntityPassivateSpec extends AbstractTelemetrySpec { val service = TestService() val client = - ValueEntityProtocolClient(GrpcClientSettings.connectToServiceAt("localhost", service.port).withTls(false)) + ValueEntityClient(GrpcClientSettings.connectToServiceAt("localhost", service.port).withTls(false)) val entityConfiguration = ValueEntity.Configuration( serviceName = "service", @@ -67,7 +67,7 @@ class ValueEntityPassivateSpec extends AbstractTelemetrySpec { sendQueueSize = 100 ) - val repository = new JdbcRepositoryImpl(new JdbcInMemoryStore) + val repository = new RepositoryImpl(new InMemoryStore) val entity = system.actorOf(ValueEntitySupervisor.props(client, entityConfiguration, repository), "entity") val emptyCommand = Some(protobufAny(EmptyJavaMessage)) @@ -99,7 +99,7 @@ class ValueEntityPassivateSpec extends AbstractTelemetrySpec { // passivate recreatedEntity ! ValueEntity.Stop connection2.expectClosed() - //expectTerminated(recreatedEntity) // should be expected! + //expectTerminated(recreatedEntity) // should be expected! } } diff --git a/proxy/core/src/test/scala/io/cloudstate/proxy/valueentity/ExceptionHandlingSpec.scala b/proxy/core/src/test/scala/io/cloudstate/proxy/valueentity/ExceptionHandlingSpec.scala index 7b97c15ef..4f2dda1df 100644 --- a/proxy/core/src/test/scala/io/cloudstate/proxy/valueentity/ExceptionHandlingSpec.scala +++ b/proxy/core/src/test/scala/io/cloudstate/proxy/valueentity/ExceptionHandlingSpec.scala @@ -18,18 +18,18 @@ package io.cloudstate.proxy.valueentity import akka.Done import akka.actor.ActorSystem +import akka.grpc.{GrpcClientSettings, GrpcServiceException} import akka.http.scaladsl.Http import akka.http.scaladsl.model.{HttpRequest, Uri} import akka.http.scaladsl.unmarshalling.Unmarshal -import akka.grpc.{GrpcClientSettings, GrpcServiceException} import akka.testkit.TestKit import io.cloudstate.proxy.TestProxy -import org.scalatest.{BeforeAndAfterAll, Matchers, WordSpec} -import org.scalatest.concurrent.ScalaFutures -import io.cloudstate.testkit.valuentity.{TestValueEntityService, ValueEntityMessages} import io.cloudstate.proxy.test.thing.{Key, Thing, ThingClient} import io.cloudstate.testkit.TestService +import io.cloudstate.testkit.valueentity.{TestValueEntityService, ValueEntityMessages} import io.grpc.{Status, StatusRuntimeException} +import org.scalatest.concurrent.ScalaFutures +import org.scalatest.{BeforeAndAfterAll, Matchers, WordSpec} class ExceptionHandlingSpec extends WordSpec with Matchers with BeforeAndAfterAll with ScalaFutures { diff --git a/proxy/core/src/test/scala/io/cloudstate/proxy/valueentity/store/EntitySerializerSpec.scala b/proxy/core/src/test/scala/io/cloudstate/proxy/valueentity/store/EntitySerializerSpec.scala index 337e29bb1..5e1155476 100644 --- a/proxy/core/src/test/scala/io/cloudstate/proxy/valueentity/store/EntitySerializerSpec.scala +++ b/proxy/core/src/test/scala/io/cloudstate/proxy/valueentity/store/EntitySerializerSpec.scala @@ -19,7 +19,7 @@ package io.cloudstate.proxy.valueentity.store import akka.util.ByteString import com.google.protobuf.any.{Any => ScalaPbAny} import com.google.protobuf.{ByteString => ProtobufByteString} -import io.cloudstate.proxy.valueentity.store.JdbcRepositoryImpl.EntitySerializer +import io.cloudstate.proxy.valueentity.store.RepositoryImpl.EntitySerializer import org.scalatest.{Matchers, WordSpecLike} class EntitySerializerSpec extends WordSpecLike with Matchers { diff --git a/proxy/core/src/test/scala/io/cloudstate/proxy/valueentity/store/JdbcConfigSpec.scala b/proxy/core/src/test/scala/io/cloudstate/proxy/valueentity/store/jdbc/JdbcConfigSpec.scala similarity index 74% rename from proxy/core/src/test/scala/io/cloudstate/proxy/valueentity/store/JdbcConfigSpec.scala rename to proxy/core/src/test/scala/io/cloudstate/proxy/valueentity/store/jdbc/JdbcConfigSpec.scala index 969acc80d..4b91a0c1e 100644 --- a/proxy/core/src/test/scala/io/cloudstate/proxy/valueentity/store/JdbcConfigSpec.scala +++ b/proxy/core/src/test/scala/io/cloudstate/proxy/valueentity/store/jdbc/JdbcConfigSpec.scala @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.cloudstate.proxy.valueentity.store +package io.cloudstate.proxy.valueentity.store.jdbc import com.typesafe.config.{Config, ConfigFactory} import org.scalatest.{Matchers, WordSpecLike} @@ -53,45 +53,46 @@ class JdbcConfigSpec extends WordSpecLike with Matchers { | """.stripMargin) - private val tableStateConfig = new JdbcValueEntityTableConfiguration( + private val tableStateConfig = new JdbcEntityTableConfiguration( config.getConfig("cloudstate.proxy.value-entity-persistence-store.jdbc-state-store") ) - private val testTable = new JdbcValueEntityTable { - override val valueEntityTableCfg: JdbcValueEntityTableConfiguration = tableStateConfig + private val testTable = new JdbcEntityTable { + override val entityTableCfg: JdbcEntityTableConfiguration = tableStateConfig override val profile: JdbcProfile = slick.jdbc.PostgresProfile } - "ValueEntityTable" should { + "EntityTable" should { def columnName(tableName: String, columnName: String) = s"$tableName.$columnName" "be configured with a schema name" in { - testTable.ValueEntityTableQuery.baseTableRow.schemaName shouldBe tableStateConfig.schemaName + testTable.Entity.baseTableRow.schemaName shouldBe tableStateConfig.schemaName } "be configured with a table name" in { - testTable.ValueEntityTableQuery.baseTableRow.tableName shouldBe tableStateConfig.tableName + testTable.Entity.baseTableRow.tableName shouldBe tableStateConfig.tableName } "be configured with a columns name" in { - testTable.ValueEntityTableQuery.baseTableRow.persistentId.toString shouldBe columnName( + testTable.Entity.baseTableRow.persistentId.toString shouldBe columnName( tableStateConfig.tableName, tableStateConfig.columnNames.persistentId ) - testTable.ValueEntityTableQuery.baseTableRow.entityId.toString shouldBe columnName( + testTable.Entity.baseTableRow.entityId.toString shouldBe columnName( tableStateConfig.tableName, tableStateConfig.columnNames.entityId ) - testTable.ValueEntityTableQuery.baseTableRow.state.toString shouldBe columnName( + testTable.Entity.baseTableRow.state.toString shouldBe columnName( tableStateConfig.tableName, tableStateConfig.columnNames.state ) } } - "ValueEntityTableConfig" should { + "EntityTableConfig" should { "be correctly represent as string" in { - testTable.valueEntityTableCfg.toString shouldBe "JdbcValueEntityTableColumnNames(value_entity_state,None,JdbcValueEntityTableColumnNames(persistent_id,entity_id,state))" + testTable.entityTableCfg.toString shouldBe + "JdbcEntityTableConfiguration(value_entity_state,None,JdbcEntityTableColumnNames(persistent_id,entity_id,state))" } } diff --git a/proxy/jdbc/src/main/scala/io/cloudstate/proxy/jdbc/SlickEnsureValueEntityTablesExistReadyCheck.scala b/proxy/jdbc/src/main/scala/io/cloudstate/proxy/jdbc/SlickEnsureValueEntityTablesExistReadyCheck.scala index 309cd1cad..5ba76c1ba 100644 --- a/proxy/jdbc/src/main/scala/io/cloudstate/proxy/jdbc/SlickEnsureValueEntityTablesExistReadyCheck.scala +++ b/proxy/jdbc/src/main/scala/io/cloudstate/proxy/jdbc/SlickEnsureValueEntityTablesExistReadyCheck.scala @@ -22,11 +22,7 @@ import akka.Done import akka.actor.{Actor, ActorLogging, ActorSystem, Props, Status} import akka.pattern.{BackoffOpts, BackoffSupervisor} import akka.util.Timeout -import io.cloudstate.proxy.valueentity.store.{ - JdbcSlickDatabase, - JdbcValueEntityTable, - JdbcValueEntityTableConfiguration -} +import io.cloudstate.proxy.valueentity.store.jdbc.{JdbcEntityTable, JdbcEntityTableConfiguration, JdbcSlickDatabase} import slick.jdbc.{H2Profile, JdbcProfile, MySQLProfile, PostgresProfile} import slick.jdbc.meta.MTable @@ -36,12 +32,12 @@ import scala.util.{Failure, Success, Try} class SlickEnsureValueEntityTablesExistReadyCheck(system: ActorSystem) extends (() => Future[Boolean]) { - private val valueEntityConfig = system.settings.config.getConfig("cloudstate.proxy") - private val autoCreateTables = valueEntityConfig.getBoolean("jdbc.auto-create-tables") + private val entityConfig = system.settings.config.getConfig("cloudstate.proxy") + private val autoCreateTables = entityConfig.getBoolean("jdbc.auto-create-tables") private val check: () => Future[Boolean] = if (autoCreateTables) { // Get a hold of the cloudstate.proxy.value-entity-persistence-store.jdbc.database.slick database instance - val db = JdbcSlickDatabase(valueEntityConfig) + val db = JdbcSlickDatabase(entityConfig) val actor = system.actorOf( BackoffSupervisor.props( @@ -87,16 +83,16 @@ private class EnsureValueEntityTablesExistsActor(db: JdbcSlickDatabase) extends implicit val ec = context.dispatcher - private val stateCfg = new JdbcValueEntityTableConfiguration( + private val stateCfg = new JdbcEntityTableConfiguration( context.system.settings.config.getConfig("cloudstate.proxy.value-entity-persistence-store.jdbc-state-store") ) - private val stateTable = new JdbcValueEntityTable { - override val valueEntityTableCfg: JdbcValueEntityTableConfiguration = stateCfg + private val stateTable = new JdbcEntityTable { + override val entityTableCfg: JdbcEntityTableConfiguration = stateCfg override val profile: JdbcProfile = EnsureValueEntityTablesExistsActor.this.profile } - private val stateStatements = stateTable.ValueEntityTableQuery.schema.createStatements.toSeq + private val stateStatements = stateTable.Entity.schema.createStatements.toSeq import akka.pattern.pipe diff --git a/proxy/postgres/src/graal/META-INF/native-image/io.cloudstate/cloudstate-proxy-jdbc/reflect-config.json.conf b/proxy/postgres/src/graal/META-INF/native-image/io.cloudstate/cloudstate-proxy-jdbc/reflect-config.json.conf index 87ea4a83a..a21b4a261 100644 --- a/proxy/postgres/src/graal/META-INF/native-image/io.cloudstate/cloudstate-proxy-jdbc/reflect-config.json.conf +++ b/proxy/postgres/src/graal/META-INF/native-image/io.cloudstate/cloudstate-proxy-jdbc/reflect-config.json.conf @@ -8,7 +8,7 @@ methods: [{name: "", parameterTypes: ["akka.actor.ActorSystem"]}] } { - name: "io.cloudstate.proxy.valueentity.store.JdbcValueEntityTable$ValueEntityTable" + name: "io.cloudstate.proxy.valueentity.store.jdbc.JdbcEntityTable$Entity" allPublicMethods: true } ] diff --git a/samples/java-valueentity-shopping-cart/src/main/java/io/cloudstate/samples/valueentity/shoppingcart/Main.java b/samples/java-eventsourced-shopping-cart/src/main/java/io/cloudstate/samples/eventsourced/shoppingcart/Main.java similarity index 76% rename from samples/java-valueentity-shopping-cart/src/main/java/io/cloudstate/samples/valueentity/shoppingcart/Main.java rename to samples/java-eventsourced-shopping-cart/src/main/java/io/cloudstate/samples/eventsourced/shoppingcart/Main.java index b10f4464c..f27138915 100644 --- a/samples/java-valueentity-shopping-cart/src/main/java/io/cloudstate/samples/valueentity/shoppingcart/Main.java +++ b/samples/java-eventsourced-shopping-cart/src/main/java/io/cloudstate/samples/eventsourced/shoppingcart/Main.java @@ -14,18 +14,18 @@ * limitations under the License. */ -package io.cloudstate.samples.valueentity.shoppingcart; +package io.cloudstate.samples.eventsourced.shoppingcart; -import com.example.valueentity.shoppingcart.Shoppingcart; -import io.cloudstate.javasupport.CloudState; +import io.cloudstate.javasupport.*; +import com.example.shoppingcart.Shoppingcart; public final class Main { public static final void main(String[] args) throws Exception { new CloudState() - .registerValueEntity( + .registerEventSourcedEntity( ShoppingCartEntity.class, Shoppingcart.getDescriptor().findServiceByName("ShoppingCart"), - com.example.valueentity.shoppingcart.persistence.Domain.getDescriptor()) + com.example.shoppingcart.persistence.Domain.getDescriptor()) .start() .toCompletableFuture() .get(); diff --git a/samples/java-eventsourced-shopping-cart/src/main/java/io/cloudstate/samples/eventsourced/shoppingcart/ShoppingCartEntity.java b/samples/java-eventsourced-shopping-cart/src/main/java/io/cloudstate/samples/eventsourced/shoppingcart/ShoppingCartEntity.java new file mode 100644 index 000000000..4151d58c5 --- /dev/null +++ b/samples/java-eventsourced-shopping-cart/src/main/java/io/cloudstate/samples/eventsourced/shoppingcart/ShoppingCartEntity.java @@ -0,0 +1,118 @@ +/* + * Copyright 2019 Lightbend Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.cloudstate.samples.eventsourced.shoppingcart; + +import com.example.shoppingcart.Shoppingcart; +import com.example.shoppingcart.persistence.Domain; +import com.google.protobuf.Empty; +import io.cloudstate.javasupport.EntityId; +import io.cloudstate.javasupport.eventsourced.*; + +import java.util.*; +import java.util.stream.Collectors; + +/** An event sourced entity. */ +@EventSourcedEntity(persistenceId = "eventsourced-shopping-cart") +public class ShoppingCartEntity { + private final String entityId; + private final Map cart = new LinkedHashMap<>(); + + public ShoppingCartEntity(@EntityId String entityId) { + this.entityId = entityId; + } + + @Snapshot + public Domain.Cart snapshot() { + return Domain.Cart.newBuilder() + .addAllItems(cart.values().stream().map(this::convert).collect(Collectors.toList())) + .build(); + } + + @SnapshotHandler + public void handleSnapshot(Domain.Cart cart) { + this.cart.clear(); + for (Domain.LineItem item : cart.getItemsList()) { + this.cart.put(item.getProductId(), convert(item)); + } + } + + @EventHandler + public void itemAdded(Domain.ItemAdded itemAdded) { + Shoppingcart.LineItem item = cart.get(itemAdded.getItem().getProductId()); + if (item == null) { + item = convert(itemAdded.getItem()); + } else { + item = + item.toBuilder() + .setQuantity(item.getQuantity() + itemAdded.getItem().getQuantity()) + .build(); + } + cart.put(item.getProductId(), item); + } + + @EventHandler + public void itemRemoved(Domain.ItemRemoved itemRemoved) { + cart.remove(itemRemoved.getProductId()); + } + + @CommandHandler + public Shoppingcart.Cart getCart() { + return Shoppingcart.Cart.newBuilder().addAllItems(cart.values()).build(); + } + + @CommandHandler + public Empty addItem(Shoppingcart.AddLineItem item, CommandContext ctx) { + if (item.getQuantity() <= 0) { + ctx.fail("Cannot add negative quantity of to item" + item.getProductId()); + } + ctx.emit( + Domain.ItemAdded.newBuilder() + .setItem( + Domain.LineItem.newBuilder() + .setProductId(item.getProductId()) + .setName(item.getName()) + .setQuantity(item.getQuantity()) + .build()) + .build()); + return Empty.getDefaultInstance(); + } + + @CommandHandler + public Empty removeItem(Shoppingcart.RemoveLineItem item, CommandContext ctx) { + if (!cart.containsKey(item.getProductId())) { + ctx.fail("Cannot remove item " + item.getProductId() + " because it is not in the cart."); + } + ctx.emit(Domain.ItemRemoved.newBuilder().setProductId(item.getProductId()).build()); + return Empty.getDefaultInstance(); + } + + private Shoppingcart.LineItem convert(Domain.LineItem item) { + return Shoppingcart.LineItem.newBuilder() + .setProductId(item.getProductId()) + .setName(item.getName()) + .setQuantity(item.getQuantity()) + .build(); + } + + private Domain.LineItem convert(Shoppingcart.LineItem item) { + return Domain.LineItem.newBuilder() + .setProductId(item.getProductId()) + .setName(item.getName()) + .setQuantity(item.getQuantity()) + .build(); + } +} diff --git a/samples/java-valueentity-shopping-cart/src/main/resources/application.conf b/samples/java-eventsourced-shopping-cart/src/main/resources/application.conf similarity index 98% rename from samples/java-valueentity-shopping-cart/src/main/resources/application.conf rename to samples/java-eventsourced-shopping-cart/src/main/resources/application.conf index 0360ee749..b9d4ac102 100644 --- a/samples/java-valueentity-shopping-cart/src/main/resources/application.conf +++ b/samples/java-eventsourced-shopping-cart/src/main/resources/application.conf @@ -4,4 +4,4 @@ cloudstate { loglevel = "DEBUG" logging-filter = "akka.event.slf4j.Slf4jLoggingFilter" } -} +} \ No newline at end of file diff --git a/samples/java-valueentity-shopping-cart/src/main/resources/simplelogger.properties b/samples/java-eventsourced-shopping-cart/src/main/resources/simplelogger.properties similarity index 100% rename from samples/java-valueentity-shopping-cart/src/main/resources/simplelogger.properties rename to samples/java-eventsourced-shopping-cart/src/main/resources/simplelogger.properties diff --git a/samples/java-shopping-cart/src/main/java/io/cloudstate/samples/shoppingcart/Main.java b/samples/java-shopping-cart/src/main/java/io/cloudstate/samples/shoppingcart/Main.java index f0aa30588..585434868 100644 --- a/samples/java-shopping-cart/src/main/java/io/cloudstate/samples/shoppingcart/Main.java +++ b/samples/java-shopping-cart/src/main/java/io/cloudstate/samples/shoppingcart/Main.java @@ -16,16 +16,16 @@ package io.cloudstate.samples.shoppingcart; -import io.cloudstate.javasupport.*; -import com.example.shoppingcart.Shoppingcart; +import com.example.valueentity.shoppingcart.Shoppingcart; +import io.cloudstate.javasupport.CloudState; public final class Main { public static final void main(String[] args) throws Exception { new CloudState() - .registerEventSourcedEntity( + .registerEntity( ShoppingCartEntity.class, Shoppingcart.getDescriptor().findServiceByName("ShoppingCart"), - com.example.shoppingcart.persistence.Domain.getDescriptor()) + com.example.valueentity.shoppingcart.persistence.Domain.getDescriptor()) .start() .toCompletableFuture() .get(); diff --git a/samples/java-shopping-cart/src/main/java/io/cloudstate/samples/shoppingcart/ShoppingCartEntity.java b/samples/java-shopping-cart/src/main/java/io/cloudstate/samples/shoppingcart/ShoppingCartEntity.java index 066b19519..2bbe04238 100644 --- a/samples/java-shopping-cart/src/main/java/io/cloudstate/samples/shoppingcart/ShoppingCartEntity.java +++ b/samples/java-shopping-cart/src/main/java/io/cloudstate/samples/shoppingcart/ShoppingCartEntity.java @@ -16,100 +16,98 @@ package io.cloudstate.samples.shoppingcart; -import com.example.shoppingcart.Shoppingcart; -import com.example.shoppingcart.persistence.Domain; +import com.example.valueentity.shoppingcart.Shoppingcart; +import com.example.valueentity.shoppingcart.persistence.Domain; import com.google.protobuf.Empty; import io.cloudstate.javasupport.EntityId; -import io.cloudstate.javasupport.eventsourced.*; +import io.cloudstate.javasupport.entity.CommandContext; +import io.cloudstate.javasupport.entity.CommandHandler; +import io.cloudstate.javasupport.entity.Entity; -import java.util.*; +import java.util.List; +import java.util.Optional; +import java.util.function.Predicate; import java.util.stream.Collectors; -/** An event sourced entity. */ -@EventSourcedEntity +/** An value based entity. */ +@Entity(persistenceId = "value-entity-shopping-cart") public class ShoppingCartEntity { + private final String entityId; - private final Map cart = new LinkedHashMap<>(); public ShoppingCartEntity(@EntityId String entityId) { this.entityId = entityId; } - @Snapshot - public Domain.Cart snapshot() { - return Domain.Cart.newBuilder() - .addAllItems(cart.values().stream().map(this::convert).collect(Collectors.toList())) - .build(); + @CommandHandler + public Shoppingcart.Cart getCart(CommandContext ctx) { + Domain.Cart cart = ctx.getState().orElse(Domain.Cart.newBuilder().build()); + List allItems = + cart.getItemsList().stream().map(this::convert).collect(Collectors.toList()); + return Shoppingcart.Cart.newBuilder().addAllItems(allItems).build(); } - @SnapshotHandler - public void handleSnapshot(Domain.Cart cart) { - this.cart.clear(); - for (Domain.LineItem item : cart.getItemsList()) { - this.cart.put(item.getProductId(), convert(item)); + @CommandHandler + public Empty addItem(Shoppingcart.AddLineItem item, CommandContext ctx) { + if (item.getQuantity() <= 0) { + ctx.fail("Cannot add negative quantity of to item " + item.getProductId()); } - } - @EventHandler - public void itemAdded(Domain.ItemAdded itemAdded) { - Shoppingcart.LineItem item = cart.get(itemAdded.getItem().getProductId()); - if (item == null) { - item = convert(itemAdded.getItem()); - } else { - item = - item.toBuilder() - .setQuantity(item.getQuantity() + itemAdded.getItem().getQuantity()) - .build(); - } - cart.put(item.getProductId(), item); - } - - @EventHandler - public void itemRemoved(Domain.ItemRemoved itemRemoved) { - cart.remove(itemRemoved.getProductId()); + Domain.Cart cart = ctx.getState().orElse(Domain.Cart.newBuilder().build()); + Domain.LineItem lineItem = updateItem(item, cart); + List lineItems = removeItemByProductId(cart, item.getProductId()); + ctx.updateState(Domain.Cart.newBuilder().addAllItems(lineItems).addItems(lineItem).build()); + return Empty.getDefaultInstance(); } @CommandHandler - public Shoppingcart.Cart getCart() { - return Shoppingcart.Cart.newBuilder().addAllItems(cart.values()).build(); - } + public Empty removeItem(Shoppingcart.RemoveLineItem item, CommandContext ctx) { + Domain.Cart cart = ctx.getState().orElse(Domain.Cart.newBuilder().build()); + Optional lineItem = findItemByProductId(cart, item.getProductId()); - @CommandHandler - public Empty addItem(Shoppingcart.AddLineItem item, CommandContext ctx) { - if (item.getQuantity() <= 0) { - ctx.fail("Cannot add negative quantity of to item" + item.getProductId()); + if (!lineItem.isPresent()) { + ctx.fail("Cannot remove item " + item.getProductId() + " because it is not in the cart."); } - ctx.emit( - Domain.ItemAdded.newBuilder() - .setItem( - Domain.LineItem.newBuilder() - .setProductId(item.getProductId()) - .setName(item.getName()) - .setQuantity(item.getQuantity()) - .build()) - .build()); + + List items = removeItemByProductId(cart, item.getProductId()); + ctx.updateState(Domain.Cart.newBuilder().addAllItems(items).build()); return Empty.getDefaultInstance(); } @CommandHandler - public Empty removeItem(Shoppingcart.RemoveLineItem item, CommandContext ctx) { - if (!cart.containsKey(item.getProductId())) { - ctx.fail("Cannot remove item " + item.getProductId() + " because it is not in the cart."); - } - ctx.emit(Domain.ItemRemoved.newBuilder().setProductId(item.getProductId()).build()); + public Empty removeCart(Shoppingcart.RemoveShoppingCart cart, CommandContext ctx) { + ctx.deleteState(); return Empty.getDefaultInstance(); } - private Shoppingcart.LineItem convert(Domain.LineItem item) { - return Shoppingcart.LineItem.newBuilder() + private Domain.LineItem updateItem(Shoppingcart.AddLineItem item, Domain.Cart cart) { + return findItemByProductId(cart, item.getProductId()) + .map(li -> li.toBuilder().setQuantity(li.getQuantity() + item.getQuantity()).build()) + .orElse(newItem(item)); + } + + private Domain.LineItem newItem(Shoppingcart.AddLineItem item) { + return Domain.LineItem.newBuilder() .setProductId(item.getProductId()) .setName(item.getName()) .setQuantity(item.getQuantity()) .build(); } - private Domain.LineItem convert(Shoppingcart.LineItem item) { - return Domain.LineItem.newBuilder() + private Optional findItemByProductId(Domain.Cart cart, String productId) { + Predicate lineItemExists = + lineItem -> lineItem.getProductId().equals(productId); + return cart.getItemsList().stream().filter(lineItemExists).findFirst(); + } + + private List removeItemByProductId(Domain.Cart cart, String productId) { + return cart.getItemsList().stream() + .filter(lineItem -> !lineItem.getProductId().equals(productId)) + .collect(Collectors.toList()); + } + + private Shoppingcart.LineItem convert(Domain.LineItem item) { + return Shoppingcart.LineItem.newBuilder() .setProductId(item.getProductId()) .setName(item.getName()) .setQuantity(item.getQuantity()) diff --git a/samples/java-shopping-cart/src/main/resources/application.conf b/samples/java-shopping-cart/src/main/resources/application.conf index b9d4ac102..0360ee749 100644 --- a/samples/java-shopping-cart/src/main/resources/application.conf +++ b/samples/java-shopping-cart/src/main/resources/application.conf @@ -4,4 +4,4 @@ cloudstate { loglevel = "DEBUG" logging-filter = "akka.event.slf4j.Slf4jLoggingFilter" } -} \ No newline at end of file +} diff --git a/samples/java-valueentity-shopping-cart/src/main/java/io/cloudstate/samples/valueentity/shoppingcart/ShoppingCartEntity.java b/samples/java-valueentity-shopping-cart/src/main/java/io/cloudstate/samples/valueentity/shoppingcart/ShoppingCartEntity.java deleted file mode 100644 index 36ceffdce..000000000 --- a/samples/java-valueentity-shopping-cart/src/main/java/io/cloudstate/samples/valueentity/shoppingcart/ShoppingCartEntity.java +++ /dev/null @@ -1,116 +0,0 @@ -/* - * Copyright 2019 Lightbend Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.cloudstate.samples.valueentity.shoppingcart; - -import com.example.valueentity.shoppingcart.Shoppingcart; -import com.example.valueentity.shoppingcart.persistence.Domain; -import com.google.protobuf.Empty; -import io.cloudstate.javasupport.EntityId; -import io.cloudstate.javasupport.valueentity.CommandContext; -import io.cloudstate.javasupport.valueentity.CommandHandler; -import io.cloudstate.javasupport.valueentity.ValueEntity; - -import java.util.List; -import java.util.Optional; -import java.util.function.Predicate; -import java.util.stream.Collectors; - -/** A value entity. */ -@ValueEntity(persistenceId = "value-entity-shopping-cart") -public class ShoppingCartEntity { - - private final String entityId; - - public ShoppingCartEntity(@EntityId String entityId) { - this.entityId = entityId; - } - - @CommandHandler - public Shoppingcart.Cart getCart(CommandContext ctx) { - Domain.Cart cart = ctx.getState().orElse(Domain.Cart.newBuilder().build()); - List allItems = - cart.getItemsList().stream().map(this::convert).collect(Collectors.toList()); - return Shoppingcart.Cart.newBuilder().addAllItems(allItems).build(); - } - - @CommandHandler - public Empty addItem(Shoppingcart.AddLineItem item, CommandContext ctx) { - if (item.getQuantity() <= 0) { - ctx.fail("Cannot add negative quantity of to item " + item.getProductId()); - } - - Domain.Cart cart = ctx.getState().orElse(Domain.Cart.newBuilder().build()); - Domain.LineItem lineItem = updateItem(item, cart); - List lineItems = removeItemByProductId(cart, item.getProductId()); - ctx.updateState(Domain.Cart.newBuilder().addAllItems(lineItems).addItems(lineItem).build()); - return Empty.getDefaultInstance(); - } - - @CommandHandler - public Empty removeItem(Shoppingcart.RemoveLineItem item, CommandContext ctx) { - Domain.Cart cart = ctx.getState().orElse(Domain.Cart.newBuilder().build()); - Optional lineItem = findItemByProductId(cart, item.getProductId()); - - if (!lineItem.isPresent()) { - ctx.fail("Cannot remove item " + item.getProductId() + " because it is not in the cart."); - } - - List items = removeItemByProductId(cart, item.getProductId()); - ctx.updateState(Domain.Cart.newBuilder().addAllItems(items).build()); - return Empty.getDefaultInstance(); - } - - @CommandHandler - public Empty removeCart(Shoppingcart.RemoveShoppingCart cart, CommandContext ctx) { - ctx.deleteState(); - return Empty.getDefaultInstance(); - } - - private Domain.LineItem updateItem(Shoppingcart.AddLineItem item, Domain.Cart cart) { - return findItemByProductId(cart, item.getProductId()) - .map(li -> li.toBuilder().setQuantity(li.getQuantity() + item.getQuantity()).build()) - .orElse(newItem(item)); - } - - private Domain.LineItem newItem(Shoppingcart.AddLineItem item) { - return Domain.LineItem.newBuilder() - .setProductId(item.getProductId()) - .setName(item.getName()) - .setQuantity(item.getQuantity()) - .build(); - } - - private Optional findItemByProductId(Domain.Cart cart, String productId) { - Predicate lineItemExists = - lineItem -> lineItem.getProductId().equals(productId); - return cart.getItemsList().stream().filter(lineItemExists).findFirst(); - } - - private List removeItemByProductId(Domain.Cart cart, String productId) { - return cart.getItemsList().stream() - .filter(lineItem -> !lineItem.getProductId().equals(productId)) - .collect(Collectors.toList()); - } - - private Shoppingcart.LineItem convert(Domain.LineItem item) { - return Shoppingcart.LineItem.newBuilder() - .setProductId(item.getProductId()) - .setName(item.getName()) - .setQuantity(item.getQuantity()) - .build(); - } -} diff --git a/tck/src/main/scala/io/cloudstate/tck/CloudStateTCK.scala b/tck/src/main/scala/io/cloudstate/tck/CloudStateTCK.scala index ace185985..d86f7d601 100644 --- a/tck/src/main/scala/io/cloudstate/tck/CloudStateTCK.scala +++ b/tck/src/main/scala/io/cloudstate/tck/CloudStateTCK.scala @@ -19,7 +19,6 @@ package io.cloudstate.tck import akka.actor.ActorSystem import akka.grpc.ServiceDescription import akka.testkit.TestKit -import com.example.shoppingcart.persistence.domain import com.example.shoppingcart.shoppingcart._ import com.example.valueentity.shoppingcart.shoppingcart.{ AddLineItem => ValueEntityAddLineItem, @@ -34,19 +33,18 @@ import com.google.protobuf.any.{Any => ScalaPbAny} import com.typesafe.config.{Config, ConfigFactory} import io.cloudstate.protocol.action.ActionProtocol import io.cloudstate.protocol.crdt.Crdt -import io.cloudstate.protocol.value_entity.ValueEntityProtocol +import io.cloudstate.protocol.value_entity.ValueEntity import io.cloudstate.protocol.event_sourced._ -import io.cloudstate.tck.model.valuentity.valueentity.{ValueEntityTckModel, ValueEntityTwo} +import io.cloudstate.tck.model.valueentity.valueentity.{ValueEntityTckModel, ValueEntityTwo} import io.cloudstate.testkit.InterceptService.InterceptorSettings -import io.cloudstate.testkit.eventsourced.{EventSourcedMessages, InterceptEventSourcedService} +import io.cloudstate.testkit.eventsourced.EventSourcedMessages import io.cloudstate.testkit.{InterceptService, ServiceAddress, TestClient, TestProtocol} import io.grpc.StatusRuntimeException import io.cloudstate.tck.model.eventsourced.{EventSourcedTckModel, EventSourcedTwo} -import io.cloudstate.testkit.valuentity.ValueEntityMessages +import io.cloudstate.testkit.valueentity.ValueEntityMessages import org.scalatest.concurrent.ScalaFutures import org.scalatest.{BeforeAndAfterAll, MustMatchers, WordSpec} -import scala.collection.mutable import scala.concurrent.duration._ object CloudStateTCK { @@ -122,7 +120,7 @@ class CloudStateTCK(description: String, settings: CloudStateTCK.Settings) info.supportedEntityTypes must contain theSameElementsAs Seq( EventSourced.name, Crdt.name, - ValueEntityProtocol.name, + ValueEntity.name, ActionProtocol.name ) @@ -152,18 +150,19 @@ class CloudStateTCK(description: String, settings: CloudStateTCK.Settings) spec.entities.find(_.serviceName == ValueEntityTckModel.name).foreach { entity => serviceNames must contain("ValueEntityTckModel") - entity.entityType mustBe ValueEntityProtocol.name + entity.entityType mustBe ValueEntity.name entity.persistenceId mustBe "value-entity-tck-model" } spec.entities.find(_.serviceName == ValueEntityTwo.name).foreach { entity => serviceNames must contain("ValueEntityTwo") - entity.entityType mustBe ValueEntityProtocol.name + entity.entityType mustBe ValueEntity.name + entity.persistenceId mustBe "value-entity-tck-model-two" } spec.entities.find(_.serviceName == ValueEntityShoppingCart.name).foreach { entity => serviceNames must contain("ShoppingCart") - entity.entityType mustBe ValueEntityProtocol.name + entity.entityType mustBe ValueEntity.name entity.persistenceId must not be empty } @@ -461,33 +460,33 @@ class CloudStateTCK(description: String, settings: CloudStateTCK.Settings) // TODO convert this into a ScalaCheck generated test case "verifying app test: event-sourced shopping cart" must { import EventSourcedMessages._ - import ShoppingCartVerifier._ + import EventSourcedShoppingCartVerifier._ - def verifyGetInitialEmptyCart(session: ShoppingCartVerifier, cartId: String): Unit = { + def verifyGetInitialEmptyCart(session: EventSourcedShoppingCartVerifier, cartId: String): Unit = { shoppingCartClient.getCart(GetShoppingCart(cartId)).futureValue mustBe Cart() session.verifyConnection() session.verifyGetInitialEmptyCart(cartId) } - def verifyGetCart(session: ShoppingCartVerifier, cartId: String, expected: Item*): Unit = { + def verifyGetCart(session: EventSourcedShoppingCartVerifier, cartId: String, expected: Item*): Unit = { val expectedCart = shoppingCart(expected: _*) shoppingCartClient.getCart(GetShoppingCart(cartId)).futureValue mustBe expectedCart session.verifyGetCart(cartId, expectedCart) } - def verifyAddItem(session: ShoppingCartVerifier, cartId: String, item: Item): Unit = { + def verifyAddItem(session: EventSourcedShoppingCartVerifier, cartId: String, item: Item): Unit = { val addLineItem = AddLineItem(cartId, item.id, item.name, item.quantity) shoppingCartClient.addItem(addLineItem).futureValue mustBe EmptyScalaMessage session.verifyAddItem(cartId, item) } - def verifyRemoveItem(session: ShoppingCartVerifier, cartId: String, itemId: String): Unit = { + def verifyRemoveItem(session: EventSourcedShoppingCartVerifier, cartId: String, itemId: String): Unit = { val removeLineItem = RemoveLineItem(cartId, itemId) shoppingCartClient.removeItem(removeLineItem).futureValue mustBe EmptyScalaMessage session.verifyRemoveItem(cartId, itemId) } - def verifyAddItemFailure(session: ShoppingCartVerifier, cartId: String, item: Item): Unit = { + def verifyAddItemFailure(session: EventSourcedShoppingCartVerifier, cartId: String, item: Item): Unit = { val addLineItem = AddLineItem(cartId, item.id, item.name, item.quantity) val error = shoppingCartClient.addItem(addLineItem).failed.futureValue error mustBe a[StatusRuntimeException] @@ -495,7 +494,7 @@ class CloudStateTCK(description: String, settings: CloudStateTCK.Settings) session.verifyAddItemFailure(cartId, item, description) } - def verifyRemoveItemFailure(session: ShoppingCartVerifier, cartId: String, itemId: String): Unit = { + def verifyRemoveItemFailure(session: EventSourcedShoppingCartVerifier, cartId: String, itemId: String): Unit = { val removeLineItem = RemoveLineItem(cartId, itemId) val error = shoppingCartClient.removeItem(removeLineItem).failed.futureValue error mustBe a[StatusRuntimeException] @@ -549,8 +548,8 @@ class CloudStateTCK(description: String, settings: CloudStateTCK.Settings) } "verifying proxy test: HTTP API" must { - "verify the HTTP API for ShoppingCart service" in testFor(ShoppingCart) { - import ShoppingCartVerifier._ + "verify the HTTP API for event-sourced ShoppingCart service" in testFor(ShoppingCart) { + import EventSourcedShoppingCartVerifier._ def checkHttpRequest(path: String, body: String = null)(expected: => String): Unit = { val response = client.http.request(path, body) @@ -607,7 +606,7 @@ class CloudStateTCK(description: String, settings: CloudStateTCK.Settings) } } - "verify the HTTP API for Value Entity ShoppingCart service" in testFor(ValueEntityShoppingCart) { + "verify the HTTP API for value-based ShoppingCart service" in testFor(ValueEntityShoppingCart) { import ValueEntityShoppingCartVerifier._ def checkHttpRequest(path: String, body: String = null)(expected: => String): Unit = { @@ -680,9 +679,9 @@ class CloudStateTCK(description: String, settings: CloudStateTCK.Settings) } } - "verifying model test: value entities" must { + "verifying model test: value-based entities" must { import ValueEntityMessages._ - import io.cloudstate.tck.model.valuentity.valueentity._ + import io.cloudstate.tck.model.valueentity.valueentity._ val ServiceTwo = ValueEntityTwo.name @@ -728,9 +727,6 @@ class CloudStateTCK(description: String, settings: CloudStateTCK.Settings) def delete(): Effects = Effects.empty.withDeleteAction() - //def sideEffects(ids: String*): Effects = - // ids.foldLeft(Effects.empty) { case (e, id) => e.withSideEffect(ServiceTwo, "Call", Request(id)) } - def sideEffects(ids: String*): Effects = createSideEffects(synchronous = false, ids) @@ -926,7 +922,7 @@ class CloudStateTCK(description: String, settings: CloudStateTCK.Settings) } } - "verifying app test: value entity shopping cart" must { + "verifying app test: value-based entity shopping cart" must { import ValueEntityMessages._ import ValueEntityShoppingCartVerifier._ @@ -1016,76 +1012,3 @@ class CloudStateTCK(description: String, settings: CloudStateTCK.Settings) } } } - -object ShoppingCartVerifier { - case class Item(id: String, name: String, quantity: Int) - - def shoppingCartSession(interceptor: InterceptService): ShoppingCartVerifier = new ShoppingCartVerifier(interceptor) - - def shoppingCart(items: Item*): Cart = Cart(items.map(i => LineItem(i.id, i.name, i.quantity))) -} - -class ShoppingCartVerifier(interceptor: InterceptService) extends MustMatchers { - import EventSourcedMessages._ - import ShoppingCartVerifier.Item - - private val commandIds = mutable.Map.empty[String, Long] - private def nextCommandId(cartId: String): Long = commandIds.updateWith(cartId)(_.map(_ + 1).orElse(Some(1L))).get - - private var connection: InterceptEventSourcedService.Connection = _ - - def verifyConnection(): Unit = connection = interceptor.expectEventSourcedConnection() - - def verifyGetInitialEmptyCart(cartId: String): Unit = { - val commandId = nextCommandId(cartId) - connection.expectClient(init(ShoppingCart.name, cartId)) - connection.expectClient(command(commandId, cartId, "GetCart", GetShoppingCart(cartId))) - connection.expectService(reply(commandId, Cart())) - connection.expectNoInteraction() - } - - def verifyGetCart(cartId: String, expected: Cart): Unit = { - val commandId = nextCommandId(cartId) - connection.expectClient(command(commandId, cartId, "GetCart", GetShoppingCart(cartId))) - connection.expectService(reply(commandId, expected)) - connection.expectNoInteraction() - } - - def verifyAddItem(cartId: String, item: Item): Unit = { - val commandId = nextCommandId(cartId) - val addLineItem = AddLineItem(cartId, item.id, item.name, item.quantity) - val itemAdded = domain.ItemAdded(Some(domain.LineItem(item.id, item.name, item.quantity))) - connection.expectClient(command(commandId, cartId, "AddItem", addLineItem)) - // shopping cart implementations may or may not have snapshots configured, so match without snapshot - val replied = connection.expectServiceMessage[EventSourcedStreamOut.Message.Reply] - replied.copy(value = replied.value.clearSnapshot) mustBe reply(commandId, EmptyScalaMessage, persist(itemAdded)) - connection.expectNoInteraction() - } - - def verifyRemoveItem(cartId: String, itemId: String): Unit = { - val commandId = nextCommandId(cartId) - val removeLineItem = RemoveLineItem(cartId, itemId) - val itemRemoved = domain.ItemRemoved(itemId) - connection.expectClient(command(commandId, cartId, "RemoveItem", removeLineItem)) - // shopping cart implementations may or may not have snapshots configured, so match without snapshot - val replied = connection.expectServiceMessage[EventSourcedStreamOut.Message.Reply] - replied.copy(value = replied.value.clearSnapshot) mustBe reply(commandId, EmptyScalaMessage, persist(itemRemoved)) - connection.expectNoInteraction() - } - - def verifyAddItemFailure(cartId: String, item: Item, failure: String): Unit = { - val commandId = nextCommandId(cartId) - val addLineItem = AddLineItem(cartId, item.id, item.name, item.quantity) - connection.expectClient(command(commandId, cartId, "AddItem", addLineItem)) - connection.expectService(actionFailure(commandId, failure)) - connection.expectNoInteraction() - } - - def verifyRemoveItemFailure(cartId: String, itemId: String, failure: String): Unit = { - val commandId = nextCommandId(cartId) - val removeLineItem = RemoveLineItem(cartId, itemId) - connection.expectClient(command(commandId, cartId, "RemoveItem", removeLineItem)) - connection.expectService(actionFailure(commandId, failure)) - connection.expectNoInteraction() - } -} diff --git a/tck/src/main/scala/io/cloudstate/tck/EventSourcedShoppingCartVerifier.scala b/tck/src/main/scala/io/cloudstate/tck/EventSourcedShoppingCartVerifier.scala new file mode 100644 index 000000000..1187115cf --- /dev/null +++ b/tck/src/main/scala/io/cloudstate/tck/EventSourcedShoppingCartVerifier.scala @@ -0,0 +1,107 @@ +/* + * Copyright 2019 Lightbend Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.cloudstate.tck + +import com.example.shoppingcart.persistence.domain +import com.example.shoppingcart.shoppingcart.{ + AddLineItem, + Cart, + GetShoppingCart, + LineItem, + RemoveLineItem, + ShoppingCart +} +import io.cloudstate.protocol.event_sourced.EventSourcedStreamOut +import io.cloudstate.testkit.InterceptService +import io.cloudstate.testkit.eventsourced.{EventSourcedMessages, InterceptEventSourcedService} +import org.scalatest.MustMatchers + +import scala.collection.mutable + +object EventSourcedShoppingCartVerifier { + case class Item(id: String, name: String, quantity: Int) + + def shoppingCartSession(interceptor: InterceptService): EventSourcedShoppingCartVerifier = + new EventSourcedShoppingCartVerifier(interceptor) + + def shoppingCart(items: Item*): Cart = Cart(items.map(i => LineItem(i.id, i.name, i.quantity))) +} + +class EventSourcedShoppingCartVerifier(interceptor: InterceptService) extends MustMatchers { + import EventSourcedMessages._ + import EventSourcedShoppingCartVerifier.Item + + private val commandIds = mutable.Map.empty[String, Long] + private var connection: InterceptEventSourcedService.Connection = _ + + private def nextCommandId(cartId: String): Long = commandIds.updateWith(cartId)(_.map(_ + 1).orElse(Some(1L))).get + + def verifyConnection(): Unit = connection = interceptor.expectEventSourcedConnection() + + def verifyGetInitialEmptyCart(cartId: String): Unit = { + val commandId = nextCommandId(cartId) + connection.expectClient(init(ShoppingCart.name, cartId)) + connection.expectClient(command(commandId, cartId, "GetCart", GetShoppingCart(cartId))) + connection.expectService(reply(commandId, Cart())) + connection.expectNoInteraction() + } + + def verifyGetCart(cartId: String, expected: Cart): Unit = { + val commandId = nextCommandId(cartId) + connection.expectClient(command(commandId, cartId, "GetCart", GetShoppingCart(cartId))) + connection.expectService(reply(commandId, expected)) + connection.expectNoInteraction() + } + + def verifyAddItem(cartId: String, item: Item): Unit = { + val commandId = nextCommandId(cartId) + val addLineItem = AddLineItem(cartId, item.id, item.name, item.quantity) + val itemAdded = domain.ItemAdded(Some(domain.LineItem(item.id, item.name, item.quantity))) + connection.expectClient(command(commandId, cartId, "AddItem", addLineItem)) + // shopping cart implementations may or may not have snapshots configured, so match without snapshot + val replied = connection.expectServiceMessage[EventSourcedStreamOut.Message.Reply] + replied.copy(value = replied.value.clearSnapshot) mustBe reply(commandId, EmptyScalaMessage, persist(itemAdded)) + connection.expectNoInteraction() + } + + def verifyRemoveItem(cartId: String, itemId: String): Unit = { + val commandId = nextCommandId(cartId) + val removeLineItem = RemoveLineItem(cartId, itemId) + val itemRemoved = domain.ItemRemoved(itemId) + connection.expectClient(command(commandId, cartId, "RemoveItem", removeLineItem)) + // shopping cart implementations may or may not have snapshots configured, so match without snapshot + val replied = connection.expectServiceMessage[EventSourcedStreamOut.Message.Reply] + replied.copy(value = replied.value.clearSnapshot) mustBe reply(commandId, EmptyScalaMessage, persist(itemRemoved)) + connection.expectNoInteraction() + } + + def verifyAddItemFailure(cartId: String, item: Item, failure: String): Unit = { + val commandId = nextCommandId(cartId) + val addLineItem = AddLineItem(cartId, item.id, item.name, item.quantity) + connection.expectClient(command(commandId, cartId, "AddItem", addLineItem)) + connection.expectService(actionFailure(commandId, failure)) + connection.expectNoInteraction() + } + + def verifyRemoveItemFailure(cartId: String, itemId: String, failure: String): Unit = { + val commandId = nextCommandId(cartId) + val removeLineItem = RemoveLineItem(cartId, itemId) + connection.expectClient(command(commandId, cartId, "RemoveItem", removeLineItem)) + connection.expectService(actionFailure(commandId, failure)) + connection.expectNoInteraction() + } +} diff --git a/tck/src/main/scala/io/cloudstate/tck/ValueEntityShoppingCartVerifier.scala b/tck/src/main/scala/io/cloudstate/tck/ValueEntityShoppingCartVerifier.scala index ba8b994f4..81619a95b 100644 --- a/tck/src/main/scala/io/cloudstate/tck/ValueEntityShoppingCartVerifier.scala +++ b/tck/src/main/scala/io/cloudstate/tck/ValueEntityShoppingCartVerifier.scala @@ -17,18 +17,11 @@ package io.cloudstate.tck import io.cloudstate.testkit.InterceptService -import com.example.valueentity.shoppingcart.shoppingcart.{ - LineItem, - RemoveShoppingCart, - AddLineItem, - Cart => ValueEntityCart, - GetShoppingCart, - RemoveLineItem, - ShoppingCart -} +import com.example.valueentity.shoppingcart.shoppingcart.{Cart => ValueEntityCart} +import com.example.valueentity.shoppingcart.shoppingcart._ import com.example.valueentity.shoppingcart.persistence.domain import io.cloudstate.protocol.value_entity.ValueEntityStreamOut -import io.cloudstate.testkit.valuentity.{InterceptValueEntityService, ValueEntityMessages} +import io.cloudstate.testkit.valueentity.{InterceptValueEntityService, ValueEntityMessages} import org.scalatest.MustMatchers import scala.collection.mutable diff --git a/testkit/src/main/scala/io/cloudstate/testkit/InterceptService.scala b/testkit/src/main/scala/io/cloudstate/testkit/InterceptService.scala index 713e82a2c..b147daef0 100644 --- a/testkit/src/main/scala/io/cloudstate/testkit/InterceptService.scala +++ b/testkit/src/main/scala/io/cloudstate/testkit/InterceptService.scala @@ -24,7 +24,7 @@ import com.typesafe.config.{Config, ConfigFactory} import io.cloudstate.testkit.InterceptService.InterceptorSettings import io.cloudstate.testkit.discovery.InterceptEntityDiscovery import io.cloudstate.testkit.eventsourced.InterceptEventSourcedService -import io.cloudstate.testkit.valuentity.InterceptValueEntityService +import io.cloudstate.testkit.valueentity.InterceptValueEntityService import scala.concurrent.Await import scala.concurrent.duration._ @@ -37,7 +37,7 @@ final class InterceptService(settings: InterceptorSettings) { private val context = new InterceptorContext(settings.intercept.host, settings.intercept.port) private val entityDiscovery = new InterceptEntityDiscovery(context) private val eventSourced = new InterceptEventSourcedService(context) - private val valueEntity = new InterceptValueEntityService(context) + private val valueBased = new InterceptValueEntityService(context) import context.system @@ -45,7 +45,7 @@ final class InterceptService(settings: InterceptorSettings) { Await.result( Http().bindAndHandleAsync( - handler = entityDiscovery.handler orElse eventSourced.handler orElse valueEntity.handler, + handler = entityDiscovery.handler orElse eventSourced.handler orElse valueBased.handler, interface = settings.bind.host, port = settings.bind.port ), @@ -56,12 +56,12 @@ final class InterceptService(settings: InterceptorSettings) { def expectEventSourcedConnection(): InterceptEventSourcedService.Connection = eventSourced.expectConnection() - def expectCrudConnection(): InterceptValueEntityService.Connection = valueEntity.expectConnection() + def expectCrudConnection(): InterceptValueEntityService.Connection = valueBased.expectConnection() def terminate(): Unit = { entityDiscovery.terminate() eventSourced.terminate() - valueEntity.terminate() + valueBased.terminate() context.terminate() } } diff --git a/testkit/src/main/scala/io/cloudstate/testkit/TestProtocol.scala b/testkit/src/main/scala/io/cloudstate/testkit/TestProtocol.scala index 427204ace..0b99e3297 100644 --- a/testkit/src/main/scala/io/cloudstate/testkit/TestProtocol.scala +++ b/testkit/src/main/scala/io/cloudstate/testkit/TestProtocol.scala @@ -21,7 +21,7 @@ import akka.grpc.GrpcClientSettings import akka.testkit.TestKit import com.typesafe.config.{Config, ConfigFactory} import io.cloudstate.testkit.eventsourced.TestEventSourcedProtocol -import io.cloudstate.testkit.valuentity.TestValueEntityProtocol +import io.cloudstate.testkit.valueentity.TestValueEntityProtocol final class TestProtocol(host: String, port: Int) { import TestProtocol._ diff --git a/testkit/src/main/scala/io/cloudstate/testkit/TestService.scala b/testkit/src/main/scala/io/cloudstate/testkit/TestService.scala index b01220ff2..5b2223a0c 100644 --- a/testkit/src/main/scala/io/cloudstate/testkit/TestService.scala +++ b/testkit/src/main/scala/io/cloudstate/testkit/TestService.scala @@ -22,7 +22,7 @@ import akka.testkit.{TestKit, TestProbe} import com.typesafe.config.{Config, ConfigFactory} import io.cloudstate.testkit.discovery.TestEntityDiscoveryService import io.cloudstate.testkit.eventsourced.TestEventSourcedService -import io.cloudstate.testkit.valuentity.TestValueEntityService +import io.cloudstate.testkit.valueentity.TestValueEntityService import scala.concurrent.Await import scala.concurrent.duration._ diff --git a/testkit/src/main/scala/io/cloudstate/testkit/discovery/InterceptEntityDiscovery.scala b/testkit/src/main/scala/io/cloudstate/testkit/discovery/InterceptEntityDiscovery.scala index d55ae0122..c0d8111d0 100644 --- a/testkit/src/main/scala/io/cloudstate/testkit/discovery/InterceptEntityDiscovery.scala +++ b/testkit/src/main/scala/io/cloudstate/testkit/discovery/InterceptEntityDiscovery.scala @@ -21,7 +21,7 @@ import akka.testkit.{TestKit, TestProbe} import com.google.protobuf.empty.{Empty => ScalaPbEmpty} import io.cloudstate.protocol.action.ActionProtocol import io.cloudstate.protocol.crdt.Crdt -import io.cloudstate.protocol.value_entity.ValueEntityProtocol +import io.cloudstate.protocol.value_entity.ValueEntity import io.cloudstate.protocol.entity._ import io.cloudstate.protocol.event_sourced.EventSourced import io.cloudstate.testkit.BuildInfo @@ -83,7 +83,7 @@ object InterceptEntityDiscovery { protocolMinorVersion = BuildInfo.protocolMinorVersion, proxyName = BuildInfo.name, proxyVersion = BuildInfo.version, - supportedEntityTypes = Seq(ActionProtocol.name, Crdt.name, EventSourced.name, ValueEntityProtocol.name) + supportedEntityTypes = Seq(ActionProtocol.name, Crdt.name, EventSourced.name, ValueEntity.name) ) def expectOnline(context: InterceptorContext, timeout: FiniteDuration): Unit = { diff --git a/testkit/src/main/scala/io/cloudstate/testkit/valuentity/InterceptValueEntityService.scala b/testkit/src/main/scala/io/cloudstate/testkit/valueentity/InterceptValueEntityService.scala similarity index 81% rename from testkit/src/main/scala/io/cloudstate/testkit/valuentity/InterceptValueEntityService.scala rename to testkit/src/main/scala/io/cloudstate/testkit/valueentity/InterceptValueEntityService.scala index 5a49b87be..e41dca52f 100644 --- a/testkit/src/main/scala/io/cloudstate/testkit/valuentity/InterceptValueEntityService.scala +++ b/testkit/src/main/scala/io/cloudstate/testkit/valueentity/InterceptValueEntityService.scala @@ -14,19 +14,13 @@ * limitations under the License. */ -package io.cloudstate.testkit.valuentity +package io.cloudstate.testkit.valueentity import akka.NotUsed import akka.http.scaladsl.model.{HttpRequest, HttpResponse} import akka.stream.scaladsl.{Sink, Source} import akka.testkit.TestProbe -import io.cloudstate.protocol.value_entity.{ - ValueEntityProtocol, - ValueEntityProtocolClient, - ValueEntityProtocolHandler, - ValueEntityStreamIn, - ValueEntityStreamOut -} +import io.cloudstate.protocol.value_entity._ import io.cloudstate.testkit.InterceptService.InterceptorContext import scala.concurrent.Future @@ -36,20 +30,20 @@ import scala.reflect.ClassTag final class InterceptValueEntityService(context: InterceptorContext) { import InterceptValueEntityService._ - private val interceptor = new CrudInterceptor(context) + private val interceptor = new ValueEntityInterceptor(context) def expectConnection(): Connection = context.probe.expectMsgType[Connection] def handler: PartialFunction[HttpRequest, Future[HttpResponse]] = - ValueEntityProtocolHandler.partial(interceptor)(context.system) + ValueEntityHandler.partial(interceptor)(context.system) def terminate(): Unit = interceptor.terminate() } object InterceptValueEntityService { - final class CrudInterceptor(context: InterceptorContext) extends ValueEntityProtocol { - private val client = ValueEntityProtocolClient(context.clientSettings)(context.system) + final class ValueEntityInterceptor(context: InterceptorContext) extends ValueEntity { + private val client = ValueEntityClient(context.clientSettings)(context.system) override def handle(in: Source[ValueEntityStreamIn, NotUsed]): Source[ValueEntityStreamOut, NotUsed] = { val connection = new Connection(context) @@ -68,8 +62,8 @@ object InterceptValueEntityService { final class Connection(context: InterceptorContext) { import Connection._ - private[this] val in = TestProbe("CrudInProbe")(context.system) - private[this] val out = TestProbe("CrudOutProbe")(context.system) + private[this] val in = TestProbe("ValueEntityInProbe")(context.system) + private[this] val out = TestProbe("ValueEntityOutProbe")(context.system) private[testkit] def inSink: Sink[ValueEntityStreamIn, NotUsed] = Sink.actorRef(in.ref, Complete, Error.apply) private[testkit] def outSink: Sink[ValueEntityStreamOut, NotUsed] = Sink.actorRef(out.ref, Complete, Error.apply) diff --git a/testkit/src/main/scala/io/cloudstate/testkit/valuentity/TestValueEntityProtocol.scala b/testkit/src/main/scala/io/cloudstate/testkit/valueentity/TestValueEntityProtocol.scala similarity index 84% rename from testkit/src/main/scala/io/cloudstate/testkit/valuentity/TestValueEntityProtocol.scala rename to testkit/src/main/scala/io/cloudstate/testkit/valueentity/TestValueEntityProtocol.scala index dcce90974..4111ff5cf 100644 --- a/testkit/src/main/scala/io/cloudstate/testkit/valuentity/TestValueEntityProtocol.scala +++ b/testkit/src/main/scala/io/cloudstate/testkit/valueentity/TestValueEntityProtocol.scala @@ -14,16 +14,16 @@ * limitations under the License. */ -package io.cloudstate.testkit.valuentity +package io.cloudstate.testkit.valueentity import akka.stream.scaladsl.Source import akka.stream.testkit.TestPublisher import akka.stream.testkit.scaladsl.TestSink -import io.cloudstate.protocol.value_entity.{ValueEntityProtocolClient, ValueEntityStreamIn, ValueEntityStreamOut} +import io.cloudstate.protocol.value_entity.{ValueEntityClient, ValueEntityStreamIn, ValueEntityStreamOut} import io.cloudstate.testkit.TestProtocol.TestProtocolContext final class TestValueEntityProtocol(context: TestProtocolContext) { - private val client = ValueEntityProtocolClient(context.clientSettings)(context.system) + private val client = ValueEntityClient(context.clientSettings)(context.system) def connect(): TestValueEntityProtocol.Connection = new TestValueEntityProtocol.Connection(client, context) @@ -32,7 +32,7 @@ final class TestValueEntityProtocol(context: TestProtocolContext) { object TestValueEntityProtocol { - final class Connection(client: ValueEntityProtocolClient, context: TestProtocolContext) { + final class Connection(client: ValueEntityClient, context: TestProtocolContext) { import context.system private val in = TestPublisher.probe[ValueEntityStreamIn]() diff --git a/testkit/src/main/scala/io/cloudstate/testkit/valuentity/TestValueEntityService.scala b/testkit/src/main/scala/io/cloudstate/testkit/valueentity/TestValueEntityService.scala similarity index 83% rename from testkit/src/main/scala/io/cloudstate/testkit/valuentity/TestValueEntityService.scala rename to testkit/src/main/scala/io/cloudstate/testkit/valueentity/TestValueEntityService.scala index 81ef7ed8d..4ffafeb7a 100644 --- a/testkit/src/main/scala/io/cloudstate/testkit/valuentity/TestValueEntityService.scala +++ b/testkit/src/main/scala/io/cloudstate/testkit/valueentity/TestValueEntityService.scala @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.cloudstate.testkit.valuentity +package io.cloudstate.testkit.valueentity import akka.NotUsed import akka.actor.ActorSystem @@ -24,12 +24,7 @@ import akka.stream.scaladsl.Source import akka.stream.testkit.TestPublisher import akka.stream.testkit.scaladsl.TestSink import com.google.protobuf.Descriptors.ServiceDescriptor -import io.cloudstate.protocol.value_entity.{ - ValueEntityProtocol, - ValueEntityProtocolHandler, - ValueEntityStreamIn, - ValueEntityStreamOut -} +import io.cloudstate.protocol.value_entity._ import io.cloudstate.protocol.entity.EntitySpec import io.cloudstate.testkit.TestService.TestServiceContext import io.cloudstate.testkit.discovery.TestEntityDiscoveryService @@ -39,22 +34,22 @@ import scala.concurrent.Future class TestValueEntityService(context: TestServiceContext) { import TestValueEntityService._ - private val testCrud = new TestCrud(context) + private val testValueEntity = new TestValueEntity(context) def expectConnection(): Connection = context.probe.expectMsgType[Connection] def handler: PartialFunction[HttpRequest, Future[HttpResponse]] = - ValueEntityProtocolHandler.partial(testCrud)(context.system) + ValueEntityHandler.partial(testValueEntity)(context.system) } object TestValueEntityService { def entitySpec(service: ServiceDescription): EntitySpec = - TestEntityDiscoveryService.entitySpec(ValueEntityProtocol.name, service) + TestEntityDiscoveryService.entitySpec(ValueEntity.name, service) def entitySpec(descriptors: Seq[ServiceDescriptor]): EntitySpec = - TestEntityDiscoveryService.entitySpec(ValueEntityProtocol.name, descriptors) + TestEntityDiscoveryService.entitySpec(ValueEntity.name, descriptors) - final class TestCrud(context: TestServiceContext) extends ValueEntityProtocol { + final class TestValueEntity(context: TestServiceContext) extends ValueEntity { override def handle(source: Source[ValueEntityStreamIn, NotUsed]): Source[ValueEntityStreamOut, NotUsed] = { val connection = new Connection(context.system, source) context.probe.ref ! connection diff --git a/testkit/src/main/scala/io/cloudstate/testkit/valuentity/TestValueEntityServiceClient.scala b/testkit/src/main/scala/io/cloudstate/testkit/valueentity/TestValueEntityServiceClient.scala similarity index 88% rename from testkit/src/main/scala/io/cloudstate/testkit/valuentity/TestValueEntityServiceClient.scala rename to testkit/src/main/scala/io/cloudstate/testkit/valueentity/TestValueEntityServiceClient.scala index 7ef6b993d..e93f7c1dd 100644 --- a/testkit/src/main/scala/io/cloudstate/testkit/valuentity/TestValueEntityServiceClient.scala +++ b/testkit/src/main/scala/io/cloudstate/testkit/valueentity/TestValueEntityServiceClient.scala @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.cloudstate.testkit.valuentity +package io.cloudstate.testkit.valueentity import akka.actor.ActorSystem import akka.grpc.GrpcClientSettings @@ -23,7 +23,7 @@ import akka.stream.testkit.TestPublisher import akka.stream.testkit.scaladsl.TestSink import akka.testkit.TestKit import com.typesafe.config.{Config, ConfigFactory} -import io.cloudstate.protocol.value_entity.{ValueEntityProtocolClient, ValueEntityStreamIn, ValueEntityStreamOut} +import io.cloudstate.protocol.value_entity.{ValueEntityClient, ValueEntityStreamIn, ValueEntityStreamOut} class TestValueEntityServiceClient(port: Int) { private val config: Config = ConfigFactory.load(ConfigFactory.parseString(""" @@ -33,7 +33,7 @@ class TestValueEntityServiceClient(port: Int) { """)) private implicit val system: ActorSystem = ActorSystem("TestValueEntityServiceClient", config) - private val client = ValueEntityProtocolClient( + private val client = ValueEntityClient( GrpcClientSettings.connectToServiceAt("localhost", port).withTls(false) ) @@ -48,7 +48,7 @@ class TestValueEntityServiceClient(port: Int) { object TestValueEntityServiceClient { def apply(port: Int) = new TestValueEntityServiceClient(port) - final class Connection(client: ValueEntityProtocolClient, system: ActorSystem) { + final class Connection(client: ValueEntityClient, system: ActorSystem) { private implicit val actorSystem: ActorSystem = system private val in = TestPublisher.probe[ValueEntityStreamIn]() private val out = client.handle(Source.fromPublisher(in)).runWith(TestSink.probe[ValueEntityStreamOut]) diff --git a/testkit/src/main/scala/io/cloudstate/testkit/valuentity/ValueEntityMessages.scala b/testkit/src/main/scala/io/cloudstate/testkit/valueentity/ValueEntityMessages.scala similarity index 99% rename from testkit/src/main/scala/io/cloudstate/testkit/valuentity/ValueEntityMessages.scala rename to testkit/src/main/scala/io/cloudstate/testkit/valueentity/ValueEntityMessages.scala index fd6921273..730c908af 100644 --- a/testkit/src/main/scala/io/cloudstate/testkit/valuentity/ValueEntityMessages.scala +++ b/testkit/src/main/scala/io/cloudstate/testkit/valueentity/ValueEntityMessages.scala @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.cloudstate.testkit.valuentity +package io.cloudstate.testkit.valueentity import com.google.protobuf.any.{Any => ScalaPbAny} import com.google.protobuf.{Message => JavaPbMessage} From a2bf57cdb6ec87b3a27da316fc1f594d9c5d8983 Mon Sep 17 00:00:00 2001 From: Guy Youansi Date: Sat, 7 Nov 2020 16:45:10 +0100 Subject: [PATCH 83/93] fixed native image configuration for value entity --- .../valueentity/store/jdbc/JdbcEntityQueries.scala | 8 ++++---- .../proxy/valueentity/store/jdbc/JdbcEntityTable.scala | 4 ++-- .../proxy/valueentity/store/jdbc/JdbcConfigSpec.scala | 10 +++++----- .../SlickEnsureValueEntityTablesExistReadyCheck.scala | 2 +- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/jdbc/JdbcEntityQueries.scala b/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/jdbc/JdbcEntityQueries.scala index a594534c8..74b96bcd0 100644 --- a/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/jdbc/JdbcEntityQueries.scala +++ b/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/jdbc/JdbcEntityQueries.scala @@ -26,16 +26,16 @@ private[store] class JdbcEntityQueries(val profile: JdbcProfile, import profile.api._ - def selectByKey(key: Key): Query[EntityTable, EntityRow, Seq] = - Entity + def selectByKey(key: Key): Query[Entity, EntityRow, Seq] = + EntityTable .filter(_.persistentId === key.persistentId) .filter(_.entityId === key.entityId) .take(1) - def insertOrUpdate(row: EntityRow) = Entity.insertOrUpdate(row) + def insertOrUpdate(row: EntityRow) = EntityTable.insertOrUpdate(row) def deleteByKey(key: Key) = - Entity + EntityTable .filter(_.persistentId === key.persistentId) .filter(_.entityId === key.entityId) .delete diff --git a/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/jdbc/JdbcEntityTable.scala b/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/jdbc/JdbcEntityTable.scala index f917a5602..58ea664f0 100644 --- a/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/jdbc/JdbcEntityTable.scala +++ b/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/jdbc/JdbcEntityTable.scala @@ -33,7 +33,7 @@ trait JdbcEntityTable { def entityTableCfg: JdbcEntityTableConfiguration - class EntityTable(tableTag: Tag) + class Entity(tableTag: Tag) extends Table[EntityRow](_tableTag = tableTag, _schemaName = entityTableCfg.schemaName, _tableName = entityTableCfg.tableName) { @@ -47,5 +47,5 @@ trait JdbcEntityTable { val pk = primaryKey(s"${tableName}_pk", (persistentId, entityId)) } - lazy val Entity = new TableQuery(tag => new EntityTable(tag)) + lazy val EntityTable = new TableQuery(tag => new Entity(tag)) } diff --git a/proxy/core/src/test/scala/io/cloudstate/proxy/valueentity/store/jdbc/JdbcConfigSpec.scala b/proxy/core/src/test/scala/io/cloudstate/proxy/valueentity/store/jdbc/JdbcConfigSpec.scala index 4b91a0c1e..e4b8a44d2 100644 --- a/proxy/core/src/test/scala/io/cloudstate/proxy/valueentity/store/jdbc/JdbcConfigSpec.scala +++ b/proxy/core/src/test/scala/io/cloudstate/proxy/valueentity/store/jdbc/JdbcConfigSpec.scala @@ -66,23 +66,23 @@ class JdbcConfigSpec extends WordSpecLike with Matchers { def columnName(tableName: String, columnName: String) = s"$tableName.$columnName" "be configured with a schema name" in { - testTable.Entity.baseTableRow.schemaName shouldBe tableStateConfig.schemaName + testTable.EntityTable.baseTableRow.schemaName shouldBe tableStateConfig.schemaName } "be configured with a table name" in { - testTable.Entity.baseTableRow.tableName shouldBe tableStateConfig.tableName + testTable.EntityTable.baseTableRow.tableName shouldBe tableStateConfig.tableName } "be configured with a columns name" in { - testTable.Entity.baseTableRow.persistentId.toString shouldBe columnName( + testTable.EntityTable.baseTableRow.persistentId.toString shouldBe columnName( tableStateConfig.tableName, tableStateConfig.columnNames.persistentId ) - testTable.Entity.baseTableRow.entityId.toString shouldBe columnName( + testTable.EntityTable.baseTableRow.entityId.toString shouldBe columnName( tableStateConfig.tableName, tableStateConfig.columnNames.entityId ) - testTable.Entity.baseTableRow.state.toString shouldBe columnName( + testTable.EntityTable.baseTableRow.state.toString shouldBe columnName( tableStateConfig.tableName, tableStateConfig.columnNames.state ) diff --git a/proxy/jdbc/src/main/scala/io/cloudstate/proxy/jdbc/SlickEnsureValueEntityTablesExistReadyCheck.scala b/proxy/jdbc/src/main/scala/io/cloudstate/proxy/jdbc/SlickEnsureValueEntityTablesExistReadyCheck.scala index 5ba76c1ba..b03bd9e1c 100644 --- a/proxy/jdbc/src/main/scala/io/cloudstate/proxy/jdbc/SlickEnsureValueEntityTablesExistReadyCheck.scala +++ b/proxy/jdbc/src/main/scala/io/cloudstate/proxy/jdbc/SlickEnsureValueEntityTablesExistReadyCheck.scala @@ -92,7 +92,7 @@ private class EnsureValueEntityTablesExistsActor(db: JdbcSlickDatabase) extends override val profile: JdbcProfile = EnsureValueEntityTablesExistsActor.this.profile } - private val stateStatements = stateTable.Entity.schema.createStatements.toSeq + private val stateStatements = stateTable.EntityTable.schema.createStatements.toSeq import akka.pattern.pipe From 469edabbecddea19b0e8bd92ba81829eff1810b5 Mon Sep 17 00:00:00 2001 From: Guy Youansi Date: Mon, 9 Nov 2020 10:49:47 +0100 Subject: [PATCH 84/93] write test for properly accessing the state after passivation --- .../valueentity/EntityPassivateSpec.scala | 42 +++++++++---------- 1 file changed, 19 insertions(+), 23 deletions(-) diff --git a/proxy/core/src/test/scala/io/cloudstate/proxy/valueentity/EntityPassivateSpec.scala b/proxy/core/src/test/scala/io/cloudstate/proxy/valueentity/EntityPassivateSpec.scala index f2a1f5c0e..a78ef3726 100644 --- a/proxy/core/src/test/scala/io/cloudstate/proxy/valueentity/EntityPassivateSpec.scala +++ b/proxy/core/src/test/scala/io/cloudstate/proxy/valueentity/EntityPassivateSpec.scala @@ -35,7 +35,7 @@ class EntityPassivateSpec extends AbstractTelemetrySpec { "ValueEntity" should { - "restart entity after passivation" in withTestKit( + "access the state after passivation" in withTestKit( """ | include "test-in-memory" | akka { @@ -68,38 +68,34 @@ class EntityPassivateSpec extends AbstractTelemetrySpec { ) val repository = new RepositoryImpl(new InMemoryStore) - val entity = system.actorOf(ValueEntitySupervisor.props(client, entityConfiguration, repository), "entity") - + val entity = + watch(system.actorOf(ValueEntitySupervisor.props(client, entityConfiguration, repository), "entity")) val emptyCommand = Some(protobufAny(EmptyJavaMessage)) // init with empty state val connection = service.valueEntity.expectConnection() connection.expect(init("service", "entity")) - // first command command fails + // update entity state entity ! EntityCommand(entityId = "test", name = "command1", emptyCommand) connection.expect(command(1, "entity", "command1")) - connection.send(failure(1, "boom! failure")) - expectMsg(UserFunctionReply(clientActionFailure(0, "Unexpected Value entity failure"))) - EventFilter.error("Unexpected Value entity failure - boom! failure", occurrences = 1) - connection.expectClosed() - //expectTerminated(entity) // should be expected! - - // re-init entity with empty state and send command - val recreatedEntity = - system.actorOf(ValueEntitySupervisor.props(client, entityConfiguration, repository), "entity1") - val connection2 = service.valueEntity.expectConnection() - connection2.expect(init("service", "entity1")) - recreatedEntity ! EntityCommand(entityId = "test", name = "command2", emptyCommand) - connection2.expect(command(1, "entity1", "command2")) - val reply1 = ProtoAny("reply", ByteString.copyFromUtf8("reply1")) - connection2.send(reply(1, reply1)) - expectMsg(UserFunctionReply(clientActionReply(messagePayload(reply1)))) + val entityState = ProtoAny("state", ByteString.copyFromUtf8("state")) + connection.send(reply(1, EmptyJavaMessage, update(entityState))) + expectMsg(UserFunctionReply(clientActionReply(messagePayload(EmptyJavaMessage)))) // passivate - recreatedEntity ! ValueEntity.Stop - connection2.expectClosed() - //expectTerminated(recreatedEntity) // should be expected! + entity ! ValueEntity.Stop + connection.expectClosed() + expectTerminated(entity) + + // recreate the entity + eventually(timeout(5.seconds), interval(100.millis)) { + val recreatedEntity = + system.actorOf(ValueEntitySupervisor.props(client, entityConfiguration, repository), "entity") + val connection2 = service.valueEntity.expectConnection() + connection2.expect(init("service", "entity", state(entityState))) + connection2.close() + } } } From 403aed0a7253cce2882584015244023d8cf0af28 Mon Sep 17 00:00:00 2001 From: Guy Youansi Date: Mon, 9 Nov 2020 11:51:19 +0100 Subject: [PATCH 85/93] fixed grammar --- .../src/main/java/io/cloudstate/javasupport/CloudState.java | 2 +- .../java/io/cloudstate/javasupport/entity/CommandHandler.java | 2 +- .../main/java/io/cloudstate/javasupport/entity/Entity.java | 2 +- .../cloudstate/javasupport/entity/EntityCreationContext.java | 2 +- .../java/io/cloudstate/javasupport/entity/EntityFactory.java | 2 +- .../java/io/cloudstate/javasupport/entity/EntityHandler.java | 2 +- .../io/cloudstate/proxy/valueentity/store/InMemoryStore.scala | 2 +- .../cloudstate/samples/shoppingcart/ShoppingCartEntity.java | 4 ++-- 8 files changed, 9 insertions(+), 9 deletions(-) diff --git a/java-support/src/main/java/io/cloudstate/javasupport/CloudState.java b/java-support/src/main/java/io/cloudstate/javasupport/CloudState.java index e37c3e1f4..166a36934 100644 --- a/java-support/src/main/java/io/cloudstate/javasupport/CloudState.java +++ b/java-support/src/main/java/io/cloudstate/javasupport/CloudState.java @@ -347,7 +347,7 @@ public CloudState registerEntity( } /** - * Register an value based entity factory. + * Register a value based entity factory. * *

This is a low level API intended for custom (eg, non reflection based) mechanisms for * implementing the entity. diff --git a/java-support/src/main/java/io/cloudstate/javasupport/entity/CommandHandler.java b/java-support/src/main/java/io/cloudstate/javasupport/entity/CommandHandler.java index 70a02a710..69f7f3601 100644 --- a/java-support/src/main/java/io/cloudstate/javasupport/entity/CommandHandler.java +++ b/java-support/src/main/java/io/cloudstate/javasupport/entity/CommandHandler.java @@ -24,7 +24,7 @@ import java.lang.annotation.Target; /** - * Marks a method on an value based entity as a command handler. + * Marks a method on a value based entity as a command handler. * *

This method will be invoked whenever the service call with name that matches this command * handlers name is invoked. diff --git a/java-support/src/main/java/io/cloudstate/javasupport/entity/Entity.java b/java-support/src/main/java/io/cloudstate/javasupport/entity/Entity.java index ff5e738b1..c360cf13f 100644 --- a/java-support/src/main/java/io/cloudstate/javasupport/entity/Entity.java +++ b/java-support/src/main/java/io/cloudstate/javasupport/entity/Entity.java @@ -23,7 +23,7 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -/** An value based entity. */ +/** A value based entity. */ @CloudStateAnnotation @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) diff --git a/java-support/src/main/java/io/cloudstate/javasupport/entity/EntityCreationContext.java b/java-support/src/main/java/io/cloudstate/javasupport/entity/EntityCreationContext.java index e87d616a7..21d7a81a5 100644 --- a/java-support/src/main/java/io/cloudstate/javasupport/entity/EntityCreationContext.java +++ b/java-support/src/main/java/io/cloudstate/javasupport/entity/EntityCreationContext.java @@ -19,6 +19,6 @@ /** * Creation context for {@link Entity} annotated entities. * - *

This may be accepted as an argument to the constructor of an value based entity. + *

This may be accepted as an argument to the constructor of a value based entity. */ public interface EntityCreationContext extends EntityContext {} diff --git a/java-support/src/main/java/io/cloudstate/javasupport/entity/EntityFactory.java b/java-support/src/main/java/io/cloudstate/javasupport/entity/EntityFactory.java index 6ea6b3476..d68378b8c 100644 --- a/java-support/src/main/java/io/cloudstate/javasupport/entity/EntityFactory.java +++ b/java-support/src/main/java/io/cloudstate/javasupport/entity/EntityFactory.java @@ -17,7 +17,7 @@ package io.cloudstate.javasupport.entity; /** - * Low level interface for handling commands on an value based entity. + * Low level interface for handling commands on a value based entity. * *

Generally, this should not be needed, instead, a class annotated with the {@link * CommandHandler} and similar annotations should be used. diff --git a/java-support/src/main/java/io/cloudstate/javasupport/entity/EntityHandler.java b/java-support/src/main/java/io/cloudstate/javasupport/entity/EntityHandler.java index ed2edc856..9b30b1d24 100644 --- a/java-support/src/main/java/io/cloudstate/javasupport/entity/EntityHandler.java +++ b/java-support/src/main/java/io/cloudstate/javasupport/entity/EntityHandler.java @@ -21,7 +21,7 @@ import java.util.Optional; /** - * Low level interface for handling commands on an value based entity. + * Low level interface for handling commands on a value based entity. * *

Generally, this should not be needed, instead, a class annotated with the {@link * CommandHandler} and similar annotations should be used. diff --git a/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/InMemoryStore.scala b/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/InMemoryStore.scala index b86215ec9..a4083d61a 100644 --- a/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/InMemoryStore.scala +++ b/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/InMemoryStore.scala @@ -23,7 +23,7 @@ import scala.collection.concurrent.TrieMap import scala.concurrent.Future /** - * Represents an in-memory implementation of the store for value-based entity. + * Represents an in-memory implementation of the store for value-based entities. */ final class InMemoryStore extends Store[Key, ByteString] { diff --git a/samples/java-shopping-cart/src/main/java/io/cloudstate/samples/shoppingcart/ShoppingCartEntity.java b/samples/java-shopping-cart/src/main/java/io/cloudstate/samples/shoppingcart/ShoppingCartEntity.java index 2bbe04238..1e072994f 100644 --- a/samples/java-shopping-cart/src/main/java/io/cloudstate/samples/shoppingcart/ShoppingCartEntity.java +++ b/samples/java-shopping-cart/src/main/java/io/cloudstate/samples/shoppingcart/ShoppingCartEntity.java @@ -29,8 +29,8 @@ import java.util.function.Predicate; import java.util.stream.Collectors; -/** An value based entity. */ -@Entity(persistenceId = "value-entity-shopping-cart") +/** A value based entity. */ +@Entity(persistenceId = "shopping-cart") public class ShoppingCartEntity { private final String entityId; From c9150485427c5d0a945179e7d7dc077b3716cb13 Mon Sep 17 00:00:00 2001 From: Guy Youansi Date: Mon, 9 Nov 2020 14:09:09 +0100 Subject: [PATCH 86/93] merge master and refactoring --- bin/run-java-shopping-cart-test.sh | 3 +- .../javasupport/tck/JavaSupportTck.java | 16 ++++---- .../io/cloudstate/tck/CloudStateTCK.scala | 38 +++++++++++++------ 3 files changed, 36 insertions(+), 21 deletions(-) diff --git a/bin/run-java-shopping-cart-test.sh b/bin/run-java-shopping-cart-test.sh index f5125739a..d6cb94b6f 100755 --- a/bin/run-java-shopping-cart-test.sh +++ b/bin/run-java-shopping-cart-test.sh @@ -115,6 +115,7 @@ done kubectl get deployment $deployment function fail_with_details { + echo "Failed:" "$@" echo echo "=== Operator logs ===" echo @@ -141,7 +142,7 @@ function fail_with_details { # Wait for the deployment to be available echo echo "Waiting for deployment to be ready..." -kubectl rollout status --timeout=1m deployment/$deployment || fail_with_details +kubectl rollout status --timeout=5m deployment/$deployment || fail_with_details kubectl get deployment $deployment # Scale up the deployment, to test with akka clustering diff --git a/java-support/tck/src/main/java/io/cloudstate/javasupport/tck/JavaSupportTck.java b/java-support/tck/src/main/java/io/cloudstate/javasupport/tck/JavaSupportTck.java index b46c2e513..47115f60f 100644 --- a/java-support/tck/src/main/java/io/cloudstate/javasupport/tck/JavaSupportTck.java +++ b/java-support/tck/src/main/java/io/cloudstate/javasupport/tck/JavaSupportTck.java @@ -32,6 +32,14 @@ public final class JavaSupportTck { public static final void main(String[] args) throws Exception { new CloudState() + .registerAction( + new ActionTckModelBehavior(), + Action.getDescriptor().findServiceByName("ActionTckModel"), + Action.getDescriptor()) + .registerAction( + new ActionTwoBehavior(), + Action.getDescriptor().findServiceByName("ActionTwo"), + Action.getDescriptor()) .registerEntity( ValueEntityTckModelEntity.class, Valueentity.getDescriptor().findServiceByName("ValueEntityTckModel"), @@ -43,14 +51,6 @@ public static final void main(String[] args) throws Exception { ShoppingCartEntity.class, Shoppingcart.getDescriptor().findServiceByName("ShoppingCart"), com.example.valueentity.shoppingcart.persistence.Domain.getDescriptor()) - .registerAction( - new ActionTckModelBehavior(), - Action.getDescriptor().findServiceByName("ActionTckModel"), - Action.getDescriptor()) - .registerAction( - new ActionTwoBehavior(), - Action.getDescriptor().findServiceByName("ActionTwo"), - Action.getDescriptor()) .registerEventSourcedEntity( EventSourcedTckModelEntity.class, Eventsourced.getDescriptor().findServiceByName("EventSourcedTckModel"), diff --git a/tck/src/main/scala/io/cloudstate/tck/CloudStateTCK.scala b/tck/src/main/scala/io/cloudstate/tck/CloudStateTCK.scala index ac9fba9a6..0ccc1ef13 100644 --- a/tck/src/main/scala/io/cloudstate/tck/CloudStateTCK.scala +++ b/tck/src/main/scala/io/cloudstate/tck/CloudStateTCK.scala @@ -19,8 +19,14 @@ package io.cloudstate.tck import akka.actor.ActorSystem import akka.grpc.ServiceDescription import akka.testkit.TestKit -import com.example.shoppingcart.shoppingcart._ -import com.example.valueentity.shoppingcart.shoppingcart._ +import com.example.shoppingcart.shoppingcart.{ + ShoppingCart => EventSourcedShoppingCart, + ShoppingCartClient => EventSourcedShoppingCartClient +} +import com.example.valueentity.shoppingcart.shoppingcart.{ + ShoppingCart => ValueEntityShoppingCart, + ShoppingCartClient => ValueEntityShoppingCartClient +} import com.google.protobuf.DescriptorProtos import com.google.protobuf.any.{Any => ScalaPbAny} import com.typesafe.config.{Config, ConfigFactory} @@ -69,7 +75,7 @@ class CloudStateTCK(description: String, settings: CloudStateTCK.Settings) private[this] final val system = ActorSystem("CloudStateTCK", ConfigFactory.load("tck")) private[this] final val client = TestClient(settings.proxy.host, settings.proxy.port) - private[this] final val shoppingCartClient = ShoppingCartClient(client.settings)(system) + private[this] final val eventSourcedShoppingCartClient = EventSourcedShoppingCartClient(client.settings)(system) private[this] final val valueEntityShoppingCartClient = ValueEntityShoppingCartClient(client.settings)(system) private[this] final val protocol = TestProtocol(settings.service.host, settings.service.port) @@ -83,7 +89,8 @@ class CloudStateTCK(description: String, settings: CloudStateTCK.Settings) interceptor = new InterceptService(InterceptorSettings(bind = settings.tck, intercept = settings.service)) override def afterAll(): Unit = - try shoppingCartClient.close().futureValue + try eventSourcedShoppingCartClient.close().futureValue + finally try valueEntityShoppingCartClient.close().futureValue finally try client.terminate() finally try protocol.terminate() finally interceptor.terminate() @@ -146,7 +153,7 @@ class CloudStateTCK(description: String, settings: CloudStateTCK.Settings) entity.entityType mustBe EventSourced.name } - spec.entities.find(_.serviceName == ShoppingCart.name).foreach { entity => + spec.entities.find(_.serviceName == EventSourcedShoppingCart.name).foreach { entity => serviceNames must contain("ShoppingCart") entity.entityType mustBe EventSourced.name entity.persistenceId must not be empty @@ -856,36 +863,37 @@ class CloudStateTCK(description: String, settings: CloudStateTCK.Settings) // TODO convert this into a ScalaCheck generated test case "verifying app test: event-sourced shopping cart" must { + import com.example.shoppingcart.shoppingcart._ import EventSourcedMessages._ import EventSourcedShoppingCartVerifier._ def verifyGetInitialEmptyCart(session: EventSourcedShoppingCartVerifier, cartId: String): Unit = { - shoppingCartClient.getCart(GetShoppingCart(cartId)).futureValue mustBe Cart() + eventSourcedShoppingCartClient.getCart(GetShoppingCart(cartId)).futureValue mustBe Cart() session.verifyConnection() session.verifyGetInitialEmptyCart(cartId) } def verifyGetCart(session: EventSourcedShoppingCartVerifier, cartId: String, expected: Item*): Unit = { val expectedCart = shoppingCart(expected: _*) - shoppingCartClient.getCart(GetShoppingCart(cartId)).futureValue mustBe expectedCart + eventSourcedShoppingCartClient.getCart(GetShoppingCart(cartId)).futureValue mustBe expectedCart session.verifyGetCart(cartId, expectedCart) } def verifyAddItem(session: EventSourcedShoppingCartVerifier, cartId: String, item: Item): Unit = { val addLineItem = AddLineItem(cartId, item.id, item.name, item.quantity) - shoppingCartClient.addItem(addLineItem).futureValue mustBe EmptyScalaMessage + eventSourcedShoppingCartClient.addItem(addLineItem).futureValue mustBe EmptyScalaMessage session.verifyAddItem(cartId, item) } def verifyRemoveItem(session: EventSourcedShoppingCartVerifier, cartId: String, itemId: String): Unit = { val removeLineItem = RemoveLineItem(cartId, itemId) - shoppingCartClient.removeItem(removeLineItem).futureValue mustBe EmptyScalaMessage + eventSourcedShoppingCartClient.removeItem(removeLineItem).futureValue mustBe EmptyScalaMessage session.verifyRemoveItem(cartId, itemId) } def verifyAddItemFailure(session: EventSourcedShoppingCartVerifier, cartId: String, item: Item): Unit = { val addLineItem = AddLineItem(cartId, item.id, item.name, item.quantity) - val error = shoppingCartClient.addItem(addLineItem).failed.futureValue + val error = eventSourcedShoppingCartClient.addItem(addLineItem).failed.futureValue error mustBe a[StatusRuntimeException] val description = error.asInstanceOf[StatusRuntimeException].getStatus.getDescription session.verifyAddItemFailure(cartId, item, description) @@ -893,7 +901,7 @@ class CloudStateTCK(description: String, settings: CloudStateTCK.Settings) def verifyRemoveItemFailure(session: EventSourcedShoppingCartVerifier, cartId: String, itemId: String): Unit = { val removeLineItem = RemoveLineItem(cartId, itemId) - val error = shoppingCartClient.removeItem(removeLineItem).failed.futureValue + val error = eventSourcedShoppingCartClient.removeItem(removeLineItem).failed.futureValue error mustBe a[StatusRuntimeException] val description = error.asInstanceOf[StatusRuntimeException].getStatus.getDescription session.verifyRemoveItemFailure(cartId, itemId, description) @@ -945,7 +953,7 @@ class CloudStateTCK(description: String, settings: CloudStateTCK.Settings) } "verifying proxy test: HTTP API" must { - "verify the HTTP API for event-sourced ShoppingCart service" in testFor(ShoppingCart) { + "verify the HTTP API for event-sourced ShoppingCart service" in testFor(EventSourcedShoppingCart) { import EventSourcedShoppingCartVerifier._ def checkHttpRequest(path: String, body: String = null)(expected: => String): Unit = { @@ -1320,6 +1328,12 @@ class CloudStateTCK(description: String, settings: CloudStateTCK.Settings) } "verifying app test: value-based entity shopping cart" must { + import com.example.valueentity.shoppingcart.shoppingcart.{ + AddLineItem => ValueEntityAddLineItem, + Cart => ValueEntityCart, + GetShoppingCart => ValueEntityGetShoppingCart, + RemoveLineItem => ValueEntityRemoveLineItem + } import ValueEntityMessages._ import ValueEntityShoppingCartVerifier._ From 5bf6a12c851e03009021ee184f3f50144ee9c547 Mon Sep 17 00:00:00 2001 From: Guy Youansi Date: Mon, 9 Nov 2020 21:47:29 +0100 Subject: [PATCH 87/93] combined readiness checks for eventsourced and value entity --- .../jdbc/src/main/resources/jdbc-common.conf | 2 +- proxy/jdbc/src/main/resources/reference.conf | 1 + .../proxy/jdbc/CloudStateJdbcProxyMain.scala | 2 +- .../proxy/jdbc/SlickCreateTables.scala | 231 ++++++++++++++++ .../SlickEnsureTablesExistReadyCheck.scala | 251 +++++------------- 5 files changed, 297 insertions(+), 190 deletions(-) create mode 100644 proxy/jdbc/src/main/scala/io/cloudstate/proxy/jdbc/SlickCreateTables.scala diff --git a/proxy/jdbc/src/main/resources/jdbc-common.conf b/proxy/jdbc/src/main/resources/jdbc-common.conf index 677e67ef1..43345a765 100644 --- a/proxy/jdbc/src/main/resources/jdbc-common.conf +++ b/proxy/jdbc/src/main/resources/jdbc-common.conf @@ -8,7 +8,7 @@ cloudstate.proxy { akka { management.health-checks.readiness-checks { cloudstate-jdbc = "io.cloudstate.proxy.jdbc.SlickEnsureTablesExistReadyCheck" - cloudstate-value-entity-jdbc = "io.cloudstate.proxy.jdbc.SlickEnsureValueEntityTablesExistReadyCheck" + #cloudstate-value-entity-jdbc = "io.cloudstate.proxy.jdbc.SlickEnsureValueEntityTablesExistReadyCheck" } persistence { diff --git a/proxy/jdbc/src/main/resources/reference.conf b/proxy/jdbc/src/main/resources/reference.conf index 38523a095..12ee7a420 100644 --- a/proxy/jdbc/src/main/resources/reference.conf +++ b/proxy/jdbc/src/main/resources/reference.conf @@ -1,3 +1,4 @@ cloudstate.proxy.jdbc { auto-create-tables = true + create-tables-timeout = 10s } \ No newline at end of file diff --git a/proxy/jdbc/src/main/scala/io/cloudstate/proxy/jdbc/CloudStateJdbcProxyMain.scala b/proxy/jdbc/src/main/scala/io/cloudstate/proxy/jdbc/CloudStateJdbcProxyMain.scala index 342292efe..5c71486c0 100644 --- a/proxy/jdbc/src/main/scala/io/cloudstate/proxy/jdbc/CloudStateJdbcProxyMain.scala +++ b/proxy/jdbc/src/main/scala/io/cloudstate/proxy/jdbc/CloudStateJdbcProxyMain.scala @@ -30,7 +30,7 @@ object CloudStateJdbcProxyMain { val config = new CloudStateProxyMain.Configuration(actorSystem.settings.config.getConfig("cloudstate.proxy")) if (config.devMode) { new SlickEnsureTablesExistReadyCheck(actorSystem) - new SlickEnsureValueEntityTablesExistReadyCheck(actorSystem) + //new SlickEnsureValueEntityTablesExistReadyCheck(actorSystem) } } diff --git a/proxy/jdbc/src/main/scala/io/cloudstate/proxy/jdbc/SlickCreateTables.scala b/proxy/jdbc/src/main/scala/io/cloudstate/proxy/jdbc/SlickCreateTables.scala new file mode 100644 index 000000000..1c2111835 --- /dev/null +++ b/proxy/jdbc/src/main/scala/io/cloudstate/proxy/jdbc/SlickCreateTables.scala @@ -0,0 +1,231 @@ +/* + * Copyright 2019 Lightbend Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.cloudstate.proxy.jdbc + +import java.sql.Connection + +import akka.Done +import akka.actor.ActorSystem +import akka.event.Logging +import akka.persistence.jdbc.config.{JournalTableConfiguration, SnapshotTableConfiguration} +import akka.persistence.jdbc.journal.dao.JournalTables +import akka.persistence.jdbc.snapshot.dao.SnapshotTables +import akka.persistence.jdbc.util.SlickDatabase +import io.cloudstate.proxy.jdbc.SlickCreateTables.TableConfiguration +import io.cloudstate.proxy.valueentity.store.jdbc.{JdbcEntityTable, JdbcEntityTableConfiguration, JdbcSlickDatabase} +import slick.jdbc.{H2Profile, JdbcBackend, JdbcProfile, MySQLProfile, PostgresProfile} +import slick.jdbc.meta.MTable + +import scala.concurrent.Future +import scala.util.Failure +import scala.util.Success +import scala.util.Try + +object SlickCreateTables { + + case class TableConfiguration(schemaStatements: Seq[String], schemaName: Option[String], tableName: String) + +} + +trait SlickCreateTables { + val system: ActorSystem + val profile: JdbcProfile + val database: JdbcBackend.Database + def tableConfigurations: Seq[TableConfiguration] + + import profile.api._ + import system.dispatcher + + private final val log = Logging(system.eventStream, classOf[SlickCreateTables]) + + def run(): Future[Done] = + database.run { + for { + _ <- slick.dbio.DBIO.sequence( + tableConfigurations.map(r => createTable(r.schemaStatements, tableExists(r.schemaName, r.tableName))) + ) + } yield Done.getInstance() + } + + private def createTable(schemaStatements: Seq[String], tableExists: (Vector[MTable], Option[String]) => Boolean) = + for { + currentSchema <- getCurrentSchema + tables <- getTables(currentSchema) + _ <- createTableInternal(tables, currentSchema, schemaStatements, tableExists) + } yield Done.getInstance() + + private def createTableInternal( + tables: Vector[MTable], + currentSchema: Option[String], + schemaStatements: Seq[String], + tableExists: (Vector[MTable], Option[String]) => Boolean + ) = + if (tableExists(tables, currentSchema)) { + DBIO.successful(()) + } else { + if (log.isDebugEnabled) { + log.debug("Creating table, executing: " + schemaStatements.mkString("; ")) + } + + DBIO + .sequence(schemaStatements.map { s => + SimpleDBIO { ctx => + val stmt = ctx.connection.createStatement() + try { + stmt.executeUpdate(s) + } finally { + stmt.close() + } + } + }) + .asTry + .flatMap { + case Success(_) => DBIO.successful(()) + case Failure(f) => + getTables(currentSchema).map { tables => + if (tableExists(tables, currentSchema)) { + log.debug("Table creation failed, but table existed after it was created, ignoring failure", f) + () + } else { + throw f + } + } + } + } + + private def getTables(currentSchema: Option[String]) = + // Calling MTable.getTables without parameters fails on MySQL + // See https://github.com/lagom/lagom/issues/446 + // and https://github.com/slick/slick/issues/1692 + profile match { + case _: MySQLProfile => + MTable.getTables(currentSchema, None, Option("%"), None) + case _ => + MTable.getTables(None, currentSchema, Option("%"), None) + } + + private def getCurrentSchema: DBIO[Option[String]] = + SimpleDBIO(ctx => tryGetSchema(ctx.connection).getOrElse(null)).flatMap { schema => + if (schema == null) { + // Not all JDBC drivers support the getSchema method: + // some always return null. + // In that case, fall back to vendor-specific queries. + profile match { + case _: H2Profile => + sql"SELECT SCHEMA();".as[String].headOption + case _: MySQLProfile => + sql"SELECT DATABASE();".as[String].headOption + case _: PostgresProfile => + sql"SELECT current_schema();".as[String].headOption + case _ => + DBIO.successful(None) + } + } else DBIO.successful(Some(schema)) + } + + // Some older JDBC drivers don't implement Connection.getSchema + // (including some builds of H2). This causes them to throw an + // AbstractMethodError at runtime. + // Because Try$.apply only catches NonFatal errors, and AbstractMethodError + // is considered fatal, we need to construct the Try explicitly. + private def tryGetSchema(connection: Connection): Try[String] = + try Success(connection.getSchema) + catch { + case e: AbstractMethodError => + Failure(new IllegalStateException("Database driver does not support Connection.getSchema", e)) + } + + private def tableExists( + schemaName: Option[String], + tableName: String + )(tables: Vector[MTable], currentSchema: Option[String]): Boolean = + tables.exists { t => + profile match { + case _: MySQLProfile => + t.name.catalog.orElse(currentSchema) == schemaName.orElse(currentSchema) && t.name.name == tableName + case _ => + t.name.schema.orElse(currentSchema) == schemaName.orElse(currentSchema) && t.name.name == tableName + } + } +} + +class ValueEntitySlickCreateTable(override val system: ActorSystem, slickDb: JdbcSlickDatabase) + extends SlickCreateTables { + + override val profile = slickDb.profile + override val database = slickDb.database + + import profile.api._ + + private val tableCfg = new JdbcEntityTableConfiguration( + system.settings.config.getConfig("cloudstate.proxy.value-entity-persistence-store.jdbc-state-store") + ) + + private val table = new JdbcEntityTable { + override val entityTableCfg: JdbcEntityTableConfiguration = tableCfg + override val profile: JdbcProfile = ValueEntitySlickCreateTable.this.profile + } + + private val statements = table.EntityTable.schema.createStatements.toSeq + + override val tableConfigurations: Seq[TableConfiguration] = + Seq(TableConfiguration(statements, tableCfg.schemaName, tableCfg.tableName)) +} + +class EventSourcedSlickCreateTable(override val system: ActorSystem, slickDb: SlickDatabase) extends SlickCreateTables { + + override val profile = slickDb.profile + override val database = slickDb.database + + import profile.api._ + + private val journalCfg = new JournalTableConfiguration(system.settings.config.getConfig("jdbc-read-journal")) + private val snapshotCfg = new SnapshotTableConfiguration( + system.settings.config.getConfig("jdbc-snapshot-store") + ) + + private val journalTables = new JournalTables { + override val journalTableCfg: JournalTableConfiguration = journalCfg + override val profile: JdbcProfile = EventSourcedSlickCreateTable.this.profile + } + private val snapshotTables = new SnapshotTables { + override val snapshotTableCfg: SnapshotTableConfiguration = snapshotCfg + override val profile: JdbcProfile = EventSourcedSlickCreateTable.this.profile + } + + private val journalStatements = + profile match { + case H2Profile => + // Work around https://github.com/slick/slick/issues/763 + journalTables.JournalTable.schema.createStatements + .map(_.replace("GENERATED BY DEFAULT AS IDENTITY(START WITH 1)", "AUTO_INCREMENT")) + .toSeq + case MySQLProfile => + // Work around https://github.com/slick/slick/issues/1437 + journalTables.JournalTable.schema.createStatements + .map(_.replace("AUTO_INCREMENT", "AUTO_INCREMENT UNIQUE")) + .toSeq + case _ => journalTables.JournalTable.schema.createStatements.toSeq + } + + private val snapshotStatements = snapshotTables.SnapshotTable.schema.createStatements.toSeq + + override val tableConfigurations: Seq[TableConfiguration] = Seq( + TableConfiguration(journalStatements, journalCfg.schemaName, journalCfg.tableName), + TableConfiguration(snapshotStatements, snapshotCfg.schemaName, snapshotCfg.tableName) + ) +} diff --git a/proxy/jdbc/src/main/scala/io/cloudstate/proxy/jdbc/SlickEnsureTablesExistReadyCheck.scala b/proxy/jdbc/src/main/scala/io/cloudstate/proxy/jdbc/SlickEnsureTablesExistReadyCheck.scala index 5e76fbea7..7a5f23155 100644 --- a/proxy/jdbc/src/main/scala/io/cloudstate/proxy/jdbc/SlickEnsureTablesExistReadyCheck.scala +++ b/proxy/jdbc/src/main/scala/io/cloudstate/proxy/jdbc/SlickEnsureTablesExistReadyCheck.scala @@ -16,61 +16,78 @@ package io.cloudstate.proxy.jdbc -import java.sql.Connection - import akka.Done import akka.actor.{Actor, ActorLogging, ActorSystem, Props, Status} import akka.pattern.{BackoffOpts, BackoffSupervisor} -import akka.persistence.jdbc.config.{ConfigKeys, JournalTableConfiguration, SnapshotTableConfiguration} -import akka.persistence.jdbc.journal.dao.JournalTables -import akka.persistence.jdbc.snapshot.dao.SnapshotTables -import akka.persistence.jdbc.util.{SlickDatabase, SlickExtension} +import akka.persistence.jdbc.config.ConfigKeys +import akka.persistence.jdbc.util.SlickExtension import akka.util.Timeout import com.typesafe.config.ConfigFactory -import slick.jdbc.meta.MTable -import slick.jdbc.H2Profile -import slick.jdbc.JdbcProfile -import slick.jdbc.MySQLProfile -import slick.jdbc.PostgresProfile +import io.cloudstate.proxy.valueentity.store.jdbc.JdbcSlickDatabase -import scala.collection.JavaConverters._ +import scala.jdk.CollectionConverters._ import scala.concurrent.duration._ import scala.concurrent.Future -import scala.util.Failure -import scala.util.Success -import scala.util.Try class SlickEnsureTablesExistReadyCheck(system: ActorSystem) extends (() => Future[Boolean]) { - private val jdbcConfig = system.settings.config.getConfig("cloudstate.proxy.jdbc") - private val autoCreateTables = jdbcConfig.getBoolean("auto-create-tables") - - private val check: () => Future[Boolean] = if (autoCreateTables) { - // Get a hold of the akka-jdbc slick database instance - val db = SlickExtension(system).database(ConfigFactory.parseMap(Map(ConfigKeys.useSharedDb -> "slick").asJava)) - - val actor = system.actorOf( - BackoffSupervisor.props( - BackoffOpts.onFailure( - childProps = Props(new EnsureTablesExistsActor(db)), - childName = "jdbc-table-creator", - minBackoff = 3.seconds, - maxBackoff = 30.seconds, - randomFactor = 0.2 - ) - ), - "jdbc-table-creator-supervisor" - ) - - implicit val timeout = Timeout(10.seconds) // TODO make configurable? - import akka.pattern.ask - - () => (actor ? EnsureTablesExistsActor.Ready).mapTo[Boolean] + private val proxyConfig = system.settings.config.getConfig("cloudstate.proxy") + private val autoCreateTables = proxyConfig.getBoolean("jdbc.auto-create-tables") + + // Get a hold of the akka-jdbc slick database instance + private val eventSourcedSlickDatabase = + SlickExtension(system).database(ConfigFactory.parseMap(Map(ConfigKeys.useSharedDb -> "slick").asJava)) + + // Get a hold of the cloudstate.proxy.value-entity-persistence-store.jdbc.database.slick database instance + private val valueEntitySlickDatabase = JdbcSlickDatabase(proxyConfig) + + private val check: () => Future[Boolean] = if (autoCreateTables) { () => + { + tableCreateCombinations() match { + case Nil => Future.successful(true) + case combinations => + val actor = system.actorOf( + BackoffSupervisor.props( + BackoffOpts.onFailure( + childProps = Props(new EnsureTablesExistsActor(combinations)), + childName = "jdbc-table-creator", + minBackoff = 3.seconds, + maxBackoff = 30.seconds, + randomFactor = 0.2 + ) + ), + "jdbc-table-creator-supervisor" + ) + + implicit val timeout = Timeout(proxyConfig.getDuration("jdbc.create-tables-timeout").toMillis.millis) + import akka.pattern.ask + + (actor ? EnsureTablesExistsActor.Ready).mapTo[Boolean] + } + } } else { () => Future.successful(true) } override def apply(): Future[Boolean] = check() + + private def tableCreateCombinations(): Seq[SlickCreateTables] = { + val config = system.settings.config + val eventSourcedEnabled = config.getBoolean("cloudstate.proxy.journal-enabled") + val valueEntityEnabled = config.getBoolean("cloudstate.proxy.value-entity-enabled") + (eventSourcedEnabled, valueEntityEnabled) match { + case (true, true) => + Seq( + new EventSourcedSlickCreateTable(system, eventSourcedSlickDatabase), + new ValueEntitySlickCreateTable(system, valueEntitySlickDatabase) + ) + case (true, false) => + Seq(new EventSourcedSlickCreateTable(system, eventSourcedSlickDatabase)) + case (false, true) => + Seq(new ValueEntitySlickCreateTable(system, valueEntitySlickDatabase)) + case (false, false) => Seq() + } + } } private object EnsureTablesExistsActor { @@ -82,56 +99,16 @@ private object EnsureTablesExistsActor { /** * Copied/adapted from https://github.com/lagom/lagom/blob/60897ef752ddbfc28553d3726b8fdb830a3ebdc4/persistence-jdbc/core/src/main/scala/com/lightbend/lagom/internal/persistence/jdbc/SlickProvider.scala */ -private class EnsureTablesExistsActor(db: SlickDatabase) extends Actor with ActorLogging { - - import EnsureTablesExistsActor._ - - private val profile = db.profile - - import profile.api._ - - implicit val ec = context.dispatcher - - private val journalCfg = new JournalTableConfiguration(context.system.settings.config.getConfig("jdbc-read-journal")) - private val snapshotCfg = new SnapshotTableConfiguration( - context.system.settings.config.getConfig("jdbc-snapshot-store") - ) - - private val journalTables = new JournalTables { - override val journalTableCfg: JournalTableConfiguration = journalCfg - override val profile: JdbcProfile = EnsureTablesExistsActor.this.profile - } - - private val snapshotTables = new SnapshotTables { - override val snapshotTableCfg: SnapshotTableConfiguration = snapshotCfg - override val profile: JdbcProfile = EnsureTablesExistsActor.this.profile - } - - private val journalStatements = - profile match { - case H2Profile => - // Work around https://github.com/slick/slick/issues/763 - journalTables.JournalTable.schema.createStatements - .map(_.replace("GENERATED BY DEFAULT AS IDENTITY(START WITH 1)", "AUTO_INCREMENT")) - .toSeq - case MySQLProfile => - // Work around https://github.com/slick/slick/issues/1437 - journalTables.JournalTable.schema.createStatements - .map(_.replace("AUTO_INCREMENT", "AUTO_INCREMENT UNIQUE")) - .toSeq - case _ => journalTables.JournalTable.schema.createStatements.toSeq - } - - private val snapshotStatements = snapshotTables.SnapshotTable.schema.createStatements.toSeq +private class EnsureTablesExistsActor(tables: Seq[SlickCreateTables]) extends Actor with ActorLogging { + import context.dispatcher import akka.pattern.pipe + import EnsureTablesExistsActor._ - db.database.run { - for { - _ <- createTable(journalStatements, tableExists(journalCfg.schemaName, journalCfg.tableName)) - _ <- createTable(snapshotStatements, tableExists(snapshotCfg.schemaName, snapshotCfg.tableName)) - } yield Done.getInstance() - } pipeTo self + Future + .sequence(tables.map(c => c.run())) + .map(_ => Done.getInstance()) + .pipeTo(self) override def receive: Receive = { case Done => context become done @@ -142,106 +119,4 @@ private class EnsureTablesExistsActor(db: SlickDatabase) extends Actor with Acto private def done: Receive = { case Ready => sender() ! true } - - private def createTable(schemaStatements: Seq[String], tableExists: (Vector[MTable], Option[String]) => Boolean) = - for { - currentSchema <- getCurrentSchema - tables <- getTables(currentSchema) - _ <- createTableInternal(tables, currentSchema, schemaStatements, tableExists) - } yield Done.getInstance() - - private def createTableInternal( - tables: Vector[MTable], - currentSchema: Option[String], - schemaStatements: Seq[String], - tableExists: (Vector[MTable], Option[String]) => Boolean - ) = - if (tableExists(tables, currentSchema)) { - DBIO.successful(()) - } else { - if (log.isDebugEnabled) { - log.debug("Creating table, executing: " + schemaStatements.mkString("; ")) - } - - DBIO - .sequence(schemaStatements.map { s => - SimpleDBIO { ctx => - val stmt = ctx.connection.createStatement() - try { - stmt.executeUpdate(s) - } finally { - stmt.close() - } - } - }) - .asTry - .flatMap { - case Success(_) => DBIO.successful(()) - case Failure(f) => - getTables(currentSchema).map { tables => - if (tableExists(tables, currentSchema)) { - log.debug("Table creation failed, but table existed after it was created, ignoring failure", f) - () - } else { - throw f - } - } - } - } - - private def getTables(currentSchema: Option[String]) = - // Calling MTable.getTables without parameters fails on MySQL - // See https://github.com/lagom/lagom/issues/446 - // and https://github.com/slick/slick/issues/1692 - profile match { - case _: MySQLProfile => - MTable.getTables(currentSchema, None, Option("%"), None) - case _ => - MTable.getTables(None, currentSchema, Option("%"), None) - } - - private def getCurrentSchema: DBIO[Option[String]] = - SimpleDBIO(ctx => tryGetSchema(ctx.connection).getOrElse(null)).flatMap { schema => - if (schema == null) { - // Not all JDBC drivers support the getSchema method: - // some always return null. - // In that case, fall back to vendor-specific queries. - profile match { - case _: H2Profile => - sql"SELECT SCHEMA();".as[String].headOption - case _: MySQLProfile => - sql"SELECT DATABASE();".as[String].headOption - case _: PostgresProfile => - sql"SELECT current_schema();".as[String].headOption - case _ => - DBIO.successful(None) - } - } else DBIO.successful(Some(schema)) - } - - // Some older JDBC drivers don't implement Connection.getSchema - // (including some builds of H2). This causes them to throw an - // AbstractMethodError at runtime. - // Because Try$.apply only catches NonFatal errors, and AbstractMethodError - // is considered fatal, we need to construct the Try explicitly. - private def tryGetSchema(connection: Connection): Try[String] = - try Success(connection.getSchema) - catch { - case e: AbstractMethodError => - Failure(new IllegalStateException("Database driver does not support Connection.getSchema", e)) - } - - private def tableExists( - schemaName: Option[String], - tableName: String - )(tables: Vector[MTable], currentSchema: Option[String]): Boolean = - tables.exists { t => - profile match { - case _: MySQLProfile => - t.name.catalog.orElse(currentSchema) == schemaName.orElse(currentSchema) && t.name.name == tableName - case _ => - t.name.schema.orElse(currentSchema) == schemaName.orElse(currentSchema) && t.name.name == tableName - } - } - } From 937084ea27786e7c74df66d86058b80e1c5a6670 Mon Sep 17 00:00:00 2001 From: Guy Youansi Date: Mon, 9 Nov 2020 22:18:25 +0100 Subject: [PATCH 88/93] fixed readiness check --- .../SlickEnsureTablesExistReadyCheck.scala | 44 +++++++++---------- 1 file changed, 21 insertions(+), 23 deletions(-) diff --git a/proxy/jdbc/src/main/scala/io/cloudstate/proxy/jdbc/SlickEnsureTablesExistReadyCheck.scala b/proxy/jdbc/src/main/scala/io/cloudstate/proxy/jdbc/SlickEnsureTablesExistReadyCheck.scala index 7a5f23155..e4a4547de 100644 --- a/proxy/jdbc/src/main/scala/io/cloudstate/proxy/jdbc/SlickEnsureTablesExistReadyCheck.scala +++ b/proxy/jdbc/src/main/scala/io/cloudstate/proxy/jdbc/SlickEnsureTablesExistReadyCheck.scala @@ -41,29 +41,27 @@ class SlickEnsureTablesExistReadyCheck(system: ActorSystem) extends (() => Futur // Get a hold of the cloudstate.proxy.value-entity-persistence-store.jdbc.database.slick database instance private val valueEntitySlickDatabase = JdbcSlickDatabase(proxyConfig) - private val check: () => Future[Boolean] = if (autoCreateTables) { () => - { - tableCreateCombinations() match { - case Nil => Future.successful(true) - case combinations => - val actor = system.actorOf( - BackoffSupervisor.props( - BackoffOpts.onFailure( - childProps = Props(new EnsureTablesExistsActor(combinations)), - childName = "jdbc-table-creator", - minBackoff = 3.seconds, - maxBackoff = 30.seconds, - randomFactor = 0.2 - ) - ), - "jdbc-table-creator-supervisor" - ) - - implicit val timeout = Timeout(proxyConfig.getDuration("jdbc.create-tables-timeout").toMillis.millis) - import akka.pattern.ask - - (actor ? EnsureTablesExistsActor.Ready).mapTo[Boolean] - } + private val check: () => Future[Boolean] = if (autoCreateTables) { + tableCreateCombinations() match { + case Nil => () => Future.successful(true) + case combinations => + val actor = system.actorOf( + BackoffSupervisor.props( + BackoffOpts.onFailure( + childProps = Props(new EnsureTablesExistsActor(combinations)), + childName = "jdbc-table-creator", + minBackoff = 3.seconds, + maxBackoff = 30.seconds, + randomFactor = 0.2 + ) + ), + "jdbc-table-creator-supervisor" + ) + + implicit val timeout = Timeout(proxyConfig.getDuration("jdbc.create-tables-timeout").toMillis.millis) + import akka.pattern.ask + + () => (actor ? EnsureTablesExistsActor.Ready).mapTo[Boolean] } } else { () => Future.successful(true) From 5545e6d9d644922b6dabe89fde20b9563083e8a4 Mon Sep 17 00:00:00 2001 From: Guy Youansi Date: Mon, 9 Nov 2020 22:55:50 +0100 Subject: [PATCH 89/93] removed dedicated readiness check for value entity --- .../jdbc/src/main/resources/jdbc-common.conf | 1 - .../proxy/jdbc/CloudStateJdbcProxyMain.scala | 1 - ...sureValueEntityTablesExistReadyCheck.scala | 216 ------------------ .../reflect-config.json.conf | 4 - 4 files changed, 222 deletions(-) delete mode 100644 proxy/jdbc/src/main/scala/io/cloudstate/proxy/jdbc/SlickEnsureValueEntityTablesExistReadyCheck.scala diff --git a/proxy/jdbc/src/main/resources/jdbc-common.conf b/proxy/jdbc/src/main/resources/jdbc-common.conf index 43345a765..8b70c4f45 100644 --- a/proxy/jdbc/src/main/resources/jdbc-common.conf +++ b/proxy/jdbc/src/main/resources/jdbc-common.conf @@ -8,7 +8,6 @@ cloudstate.proxy { akka { management.health-checks.readiness-checks { cloudstate-jdbc = "io.cloudstate.proxy.jdbc.SlickEnsureTablesExistReadyCheck" - #cloudstate-value-entity-jdbc = "io.cloudstate.proxy.jdbc.SlickEnsureValueEntityTablesExistReadyCheck" } persistence { diff --git a/proxy/jdbc/src/main/scala/io/cloudstate/proxy/jdbc/CloudStateJdbcProxyMain.scala b/proxy/jdbc/src/main/scala/io/cloudstate/proxy/jdbc/CloudStateJdbcProxyMain.scala index 5c71486c0..968b396ab 100644 --- a/proxy/jdbc/src/main/scala/io/cloudstate/proxy/jdbc/CloudStateJdbcProxyMain.scala +++ b/proxy/jdbc/src/main/scala/io/cloudstate/proxy/jdbc/CloudStateJdbcProxyMain.scala @@ -30,7 +30,6 @@ object CloudStateJdbcProxyMain { val config = new CloudStateProxyMain.Configuration(actorSystem.settings.config.getConfig("cloudstate.proxy")) if (config.devMode) { new SlickEnsureTablesExistReadyCheck(actorSystem) - //new SlickEnsureValueEntityTablesExistReadyCheck(actorSystem) } } diff --git a/proxy/jdbc/src/main/scala/io/cloudstate/proxy/jdbc/SlickEnsureValueEntityTablesExistReadyCheck.scala b/proxy/jdbc/src/main/scala/io/cloudstate/proxy/jdbc/SlickEnsureValueEntityTablesExistReadyCheck.scala deleted file mode 100644 index b03bd9e1c..000000000 --- a/proxy/jdbc/src/main/scala/io/cloudstate/proxy/jdbc/SlickEnsureValueEntityTablesExistReadyCheck.scala +++ /dev/null @@ -1,216 +0,0 @@ -/* - * Copyright 2019 Lightbend Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.cloudstate.proxy.jdbc - -import java.sql.Connection - -import akka.Done -import akka.actor.{Actor, ActorLogging, ActorSystem, Props, Status} -import akka.pattern.{BackoffOpts, BackoffSupervisor} -import akka.util.Timeout -import io.cloudstate.proxy.valueentity.store.jdbc.{JdbcEntityTable, JdbcEntityTableConfiguration, JdbcSlickDatabase} -import slick.jdbc.{H2Profile, JdbcProfile, MySQLProfile, PostgresProfile} -import slick.jdbc.meta.MTable - -import scala.concurrent.Future -import scala.concurrent.duration._ -import scala.util.{Failure, Success, Try} - -class SlickEnsureValueEntityTablesExistReadyCheck(system: ActorSystem) extends (() => Future[Boolean]) { - - private val entityConfig = system.settings.config.getConfig("cloudstate.proxy") - private val autoCreateTables = entityConfig.getBoolean("jdbc.auto-create-tables") - - private val check: () => Future[Boolean] = if (autoCreateTables) { - // Get a hold of the cloudstate.proxy.value-entity-persistence-store.jdbc.database.slick database instance - val db = JdbcSlickDatabase(entityConfig) - - val actor = system.actorOf( - BackoffSupervisor.props( - BackoffOpts.onFailure( - childProps = Props(new EnsureValueEntityTablesExistsActor(db)), - childName = "value-entity-table-creator", - minBackoff = 3.seconds, - maxBackoff = 30.seconds, - randomFactor = 0.2 - ) - ), - "value-entity-table-creator-supervisor" - ) - - implicit val timeout = Timeout(10.seconds) // TODO make configurable? - import akka.pattern.ask - - () => (actor ? EnsureValueEntityTablesExistsActor.Ready).mapTo[Boolean] - } else { () => - Future.successful(true) - } - - override def apply(): Future[Boolean] = check() -} - -private object EnsureValueEntityTablesExistsActor { - - case object Ready - -} - -/** - * Copied/adapted from https://github.com/lagom/lagom/blob/60897ef752ddbfc28553d3726b8fdb830a3ebdc4/persistence-jdbc/core/src/main/scala/com/lightbend/lagom/internal/persistence/jdbc/SlickProvider.scala - */ -private class EnsureValueEntityTablesExistsActor(db: JdbcSlickDatabase) extends Actor with ActorLogging { - // TODO refactor this to be in sync with the event sourced one - - import EnsureValueEntityTablesExistsActor._ - - private val profile = db.profile - - import profile.api._ - - implicit val ec = context.dispatcher - - private val stateCfg = new JdbcEntityTableConfiguration( - context.system.settings.config.getConfig("cloudstate.proxy.value-entity-persistence-store.jdbc-state-store") - ) - - private val stateTable = new JdbcEntityTable { - override val entityTableCfg: JdbcEntityTableConfiguration = stateCfg - override val profile: JdbcProfile = EnsureValueEntityTablesExistsActor.this.profile - } - - private val stateStatements = stateTable.EntityTable.schema.createStatements.toSeq - - import akka.pattern.pipe - - db.database.run { - for { - _ <- createTable(stateStatements, tableExists(stateCfg.schemaName, stateCfg.tableName)) - } yield Done.getInstance() - } pipeTo self - - override def receive: Receive = { - case Done => context become done - case Status.Failure(ex) => throw ex - case Ready => sender() ! false - } - - private def done: Receive = { - case Ready => sender() ! true - } - - private def createTable(schemaStatements: Seq[String], tableExists: (Vector[MTable], Option[String]) => Boolean) = - for { - currentSchema <- getCurrentSchema - tables <- getTables(currentSchema) - _ <- createTableInternal(tables, currentSchema, schemaStatements, tableExists) - } yield Done.getInstance() - - private def createTableInternal( - tables: Vector[MTable], - currentSchema: Option[String], - schemaStatements: Seq[String], - tableExists: (Vector[MTable], Option[String]) => Boolean - ) = - if (tableExists(tables, currentSchema)) { - DBIO.successful(()) - } else { - if (log.isDebugEnabled) { - log.debug("Creating table, executing: " + schemaStatements.mkString("; ")) - } - - DBIO - .sequence(schemaStatements.map { s => - SimpleDBIO { ctx => - val stmt = ctx.connection.createStatement() - try { - stmt.executeUpdate(s) - } finally { - stmt.close() - } - } - }) - .asTry - .flatMap { - case Success(_) => DBIO.successful(()) - case Failure(f) => - getTables(currentSchema).map { tables => - if (tableExists(tables, currentSchema)) { - log.debug("Table creation failed, but table existed after it was created, ignoring failure", f) - () - } else { - throw f - } - } - } - } - - private def getTables(currentSchema: Option[String]) = - // Calling MTable.getTables without parameters fails on MySQL - // See https://github.com/lagom/lagom/issues/446 - // and https://github.com/slick/slick/issues/1692 - profile match { - case _: MySQLProfile => - MTable.getTables(currentSchema, None, Option("%"), None) - case _ => - MTable.getTables(None, currentSchema, Option("%"), None) - } - - private def getCurrentSchema: DBIO[Option[String]] = - SimpleDBIO(ctx => tryGetSchema(ctx.connection).getOrElse(null)).flatMap { schema => - if (schema == null) { - // Not all JDBC drivers support the getSchema method: - // some always return null. - // In that case, fall back to vendor-specific queries. - profile match { - case _: H2Profile => - sql"SELECT SCHEMA();".as[String].headOption - case _: MySQLProfile => - sql"SELECT DATABASE();".as[String].headOption - case _: PostgresProfile => - sql"SELECT current_schema();".as[String].headOption - case _ => - DBIO.successful(None) - } - } else DBIO.successful(Some(schema)) - } - - // Some older JDBC drivers don't implement Connection.getSchema - // (including some builds of H2). This causes them to throw an - // AbstractMethodError at runtime. - // Because Try$.apply only catches NonFatal errors, and AbstractMethodError - // is considered fatal, we need to construct the Try explicitly. - private def tryGetSchema(connection: Connection): Try[String] = - try Success(connection.getSchema) - catch { - case e: AbstractMethodError => - Failure(new IllegalStateException("Database driver does not support Connection.getSchema", e)) - } - - private def tableExists( - schemaName: Option[String], - tableName: String - )(tables: Vector[MTable], currentSchema: Option[String]): Boolean = - tables.exists { t => - profile match { - case _: MySQLProfile => - t.name.catalog.orElse(currentSchema) == schemaName.orElse(currentSchema) && t.name.name == tableName - case _ => - t.name.schema.orElse(currentSchema) == schemaName.orElse(currentSchema) && t.name.name == tableName - } - } - -} diff --git a/proxy/postgres/src/graal/META-INF/native-image/io.cloudstate/cloudstate-proxy-jdbc/reflect-config.json.conf b/proxy/postgres/src/graal/META-INF/native-image/io.cloudstate/cloudstate-proxy-jdbc/reflect-config.json.conf index a21b4a261..654672005 100644 --- a/proxy/postgres/src/graal/META-INF/native-image/io.cloudstate/cloudstate-proxy-jdbc/reflect-config.json.conf +++ b/proxy/postgres/src/graal/META-INF/native-image/io.cloudstate/cloudstate-proxy-jdbc/reflect-config.json.conf @@ -3,10 +3,6 @@ name: "io.cloudstate.proxy.jdbc.SlickEnsureTablesExistReadyCheck" methods: [{name: "", parameterTypes: ["akka.actor.ActorSystem"]}] } -{ - name: "io.cloudstate.proxy.jdbc.SlickEnsureValueEntityTablesExistReadyCheck" - methods: [{name: "", parameterTypes: ["akka.actor.ActorSystem"]}] -} { name: "io.cloudstate.proxy.valueentity.store.jdbc.JdbcEntityTable$Entity" allPublicMethods: true From 58563941ea0301841b470351409aa3dad8a5f778 Mon Sep 17 00:00:00 2001 From: Guy Youansi Date: Tue, 10 Nov 2020 10:07:33 +0100 Subject: [PATCH 90/93] fixed grammar, remove left crud names and fixed code --- .../cloudstate/javasupport/entity/Entity.java | 2 +- .../cloudstate/proxy/valueentity/Entity.scala | 16 ++++++++-------- .../valueentity/EntitySupportFactory.scala | 6 +++--- .../proxy/valueentity/store/Store.scala | 2 +- .../SlickEnsureTablesExistReadyCheck.scala | 19 +++++++------------ .../tck/ValueEntityShoppingCartVerifier.scala | 2 +- .../cloudstate/testkit/InterceptService.scala | 2 +- .../valueentity/ValueEntityMessages.scala | 12 ++++++------ 8 files changed, 28 insertions(+), 33 deletions(-) diff --git a/java-support/src/main/java/io/cloudstate/javasupport/entity/Entity.java b/java-support/src/main/java/io/cloudstate/javasupport/entity/Entity.java index c360cf13f..4b558767f 100644 --- a/java-support/src/main/java/io/cloudstate/javasupport/entity/Entity.java +++ b/java-support/src/main/java/io/cloudstate/javasupport/entity/Entity.java @@ -31,7 +31,7 @@ /** * The name of the persistence id. * - *

If not specified, defaults to the entities unqualified classname. It's strongly recommended + *

If not specified, defaults to the entity's unqualified classname. It's strongly recommended * that you specify it explicitly. */ String persistenceId() default ""; diff --git a/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/Entity.scala b/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/Entity.scala index 40e3c274f..333f05356 100644 --- a/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/Entity.scala +++ b/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/Entity.scala @@ -198,7 +198,7 @@ object ValueEntity { Props(new ValueEntity(configuration, entityId, relay, repository)) /** - * Used to ensure the action ids sent to the concurrency enforcer are indeed unique. + * Used to ensure the number of entity's actor created. */ private val actorCounter = new AtomicLong(0) @@ -298,7 +298,7 @@ final class ValueEntity(configuration: ValueEntity.Configuration, relay ! ValueEntityStreamIn(ValueEntityStreamIn.Message.Command(command)) } - private final def esReplyToUfReply(reply: ValueEntityReply) = + private final def valueEntityReplyToUfReply(reply: ValueEntityReply) = UserFunctionReply( clientAction = reply.clientAction, sideEffects = reply.sideEffects @@ -345,16 +345,16 @@ final class ValueEntity(configuration: ValueEntity.Configuration, case ValueEntitySOMsg.Reply(r) => val commandId = currentCommand.commandId if (r.stateAction.isEmpty) { - currentCommand.replyTo ! esReplyToUfReply(r) + currentCommand.replyTo ! valueEntityReplyToUfReply(r) commandHandled() } else { r.stateAction.map { a => - performAction(a) { _ => + performAction(a) { () => // Make sure that the current request is still ours if (currentCommand == null || currentCommand.commandId != commandId) { crash("Unexpected Value entity behavior", "currentRequest changed before the state were persisted") } - currentCommand.replyTo ! esReplyToUfReply(r) + currentCommand.replyTo ! valueEntityReplyToUfReply(r) commandHandled() }.pipeTo(self) } @@ -407,7 +407,7 @@ final class ValueEntity(configuration: ValueEntity.Configuration, private def performAction( action: ValueEntityAction - )(handler: Unit => Unit): Future[ValueEntity.DatabaseOperationWriteStatus] = { + )(handler: () => Unit): Future[ValueEntity.DatabaseOperationWriteStatus] = { import ValueEntityAction.Action._ action.action match { @@ -415,7 +415,7 @@ final class ValueEntity(configuration: ValueEntity.Configuration, repository .update(Key(persistenceId, entityId), value) .map { _ => - handler(()) + handler() ValueEntity.WriteStateSuccess } .recover { @@ -426,7 +426,7 @@ final class ValueEntity(configuration: ValueEntity.Configuration, repository .delete(Key(persistenceId, entityId)) .map { _ => - handler(()) + handler() ValueEntity.WriteStateSuccess } .recover { diff --git a/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/EntitySupportFactory.scala b/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/EntitySupportFactory.scala index ceee0e385..65acf27de 100644 --- a/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/EntitySupportFactory.scala +++ b/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/EntitySupportFactory.scala @@ -96,7 +96,7 @@ class EntitySupportFactory( } } -private class EntitySupport(crudEntity: ActorRef, parallelism: Int, private implicit val relayTimeout: Timeout) +private class EntitySupport(valueEntity: ActorRef, parallelism: Int, private implicit val relayTimeout: Timeout) extends EntityTypeSupport { import akka.pattern.ask @@ -104,12 +104,12 @@ private class EntitySupport(crudEntity: ActorRef, parallelism: Int, private impl metadata: Metadata): Flow[EntityCommand, UserFunctionReply, NotUsed] = Flow[EntityCommand].mapAsync(parallelism)( command => - (crudEntity ? EntityTypeSupport.mergeStreamLevelMetadata(metadata, command)) + (valueEntity ? EntityTypeSupport.mergeStreamLevelMetadata(metadata, command)) .mapTo[UserFunctionReply] ) override def handleUnary(command: EntityCommand): Future[UserFunctionReply] = - (crudEntity ? command).mapTo[UserFunctionReply] + (valueEntity ? command).mapTo[UserFunctionReply] } private final class EntityIdExtractor(shards: Int) extends HashCodeMessageExtractor(shards) { diff --git a/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/Store.scala b/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/Store.scala index 07109c2be..e058f7cf9 100644 --- a/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/Store.scala +++ b/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/Store.scala @@ -25,7 +25,7 @@ object Store { } /** - * Represents an low level interface for accessing a native database. + * Represents a low level interface for accessing a native database. * * @tparam K the type for database key * @tparam V the type for database value diff --git a/proxy/jdbc/src/main/scala/io/cloudstate/proxy/jdbc/SlickEnsureTablesExistReadyCheck.scala b/proxy/jdbc/src/main/scala/io/cloudstate/proxy/jdbc/SlickEnsureTablesExistReadyCheck.scala index e4a4547de..035ebbd5c 100644 --- a/proxy/jdbc/src/main/scala/io/cloudstate/proxy/jdbc/SlickEnsureTablesExistReadyCheck.scala +++ b/proxy/jdbc/src/main/scala/io/cloudstate/proxy/jdbc/SlickEnsureTablesExistReadyCheck.scala @@ -73,18 +73,13 @@ class SlickEnsureTablesExistReadyCheck(system: ActorSystem) extends (() => Futur val config = system.settings.config val eventSourcedEnabled = config.getBoolean("cloudstate.proxy.journal-enabled") val valueEntityEnabled = config.getBoolean("cloudstate.proxy.value-entity-enabled") - (eventSourcedEnabled, valueEntityEnabled) match { - case (true, true) => - Seq( - new EventSourcedSlickCreateTable(system, eventSourcedSlickDatabase), - new ValueEntitySlickCreateTable(system, valueEntitySlickDatabase) - ) - case (true, false) => - Seq(new EventSourcedSlickCreateTable(system, eventSourcedSlickDatabase)) - case (false, true) => - Seq(new ValueEntitySlickCreateTable(system, valueEntitySlickDatabase)) - case (false, false) => Seq() - } + + val eventSourcedCombinations = + if (eventSourcedEnabled) Seq(new EventSourcedSlickCreateTable(system, eventSourcedSlickDatabase)) else Seq.empty + val valueEntityCombinations = + if (valueEntityEnabled) Seq(new ValueEntitySlickCreateTable(system, valueEntitySlickDatabase)) else Seq.empty + + eventSourcedCombinations ++ valueEntityCombinations } } diff --git a/tck/src/main/scala/io/cloudstate/tck/ValueEntityShoppingCartVerifier.scala b/tck/src/main/scala/io/cloudstate/tck/ValueEntityShoppingCartVerifier.scala index 81619a95b..8ac9cafef 100644 --- a/tck/src/main/scala/io/cloudstate/tck/ValueEntityShoppingCartVerifier.scala +++ b/tck/src/main/scala/io/cloudstate/tck/ValueEntityShoppingCartVerifier.scala @@ -48,7 +48,7 @@ class ValueEntityShoppingCartVerifier(interceptor: InterceptService) extends Mus private def nextCommandId(cartId: String): Long = commandIds.updateWith(cartId)(_.map(_ + 1).orElse(Some(1L))).get - def verifyConnection(): Unit = connection = interceptor.expectCrudConnection() + def verifyConnection(): Unit = connection = interceptor.expectValueBasedConnection() def verifyGetInitialEmptyCart(cartId: String): Unit = { val commandId = nextCommandId(cartId) diff --git a/testkit/src/main/scala/io/cloudstate/testkit/InterceptService.scala b/testkit/src/main/scala/io/cloudstate/testkit/InterceptService.scala index b147daef0..3a256e037 100644 --- a/testkit/src/main/scala/io/cloudstate/testkit/InterceptService.scala +++ b/testkit/src/main/scala/io/cloudstate/testkit/InterceptService.scala @@ -56,7 +56,7 @@ final class InterceptService(settings: InterceptorSettings) { def expectEventSourcedConnection(): InterceptEventSourcedService.Connection = eventSourced.expectConnection() - def expectCrudConnection(): InterceptValueEntityService.Connection = valueBased.expectConnection() + def expectValueBasedConnection(): InterceptValueEntityService.Connection = valueBased.expectConnection() def terminate(): Unit = { entityDiscovery.terminate() diff --git a/testkit/src/main/scala/io/cloudstate/testkit/valueentity/ValueEntityMessages.scala b/testkit/src/main/scala/io/cloudstate/testkit/valueentity/ValueEntityMessages.scala index 730c908af..fe0c68d4d 100644 --- a/testkit/src/main/scala/io/cloudstate/testkit/valueentity/ValueEntityMessages.scala +++ b/testkit/src/main/scala/io/cloudstate/testkit/valueentity/ValueEntityMessages.scala @@ -28,16 +28,16 @@ object ValueEntityMessages extends EntityMessages { import ValueEntityStreamOut.{Message => OutMessage} import ValueEntityAction.Action._ - case class Effects(sideEffects: Seq[SideEffect] = Seq.empty, crudAction: Option[ValueEntityAction] = None) { + case class Effects(sideEffects: Seq[SideEffect] = Seq.empty, valueEntityAction: Option[ValueEntityAction] = None) { def withUpdateAction(message: JavaPbMessage): Effects = - copy(crudAction = Some(ValueEntityAction(Update(ValueEntityUpdate(messagePayload(message)))))) + copy(valueEntityAction = Some(ValueEntityAction(Update(ValueEntityUpdate(messagePayload(message)))))) def withUpdateAction(message: ScalaPbMessage): Effects = - copy(crudAction = Some(ValueEntityAction(Update(ValueEntityUpdate(messagePayload(message)))))) + copy(valueEntityAction = Some(ValueEntityAction(Update(ValueEntityUpdate(messagePayload(message)))))) def withDeleteAction(): Effects = - copy(crudAction = Some(ValueEntityAction(Delete(ValueEntityDelete())))) + copy(valueEntityAction = Some(ValueEntityAction(Delete(ValueEntityDelete())))) def withSideEffect(service: String, command: String, message: ScalaPbMessage, synchronous: Boolean): Effects = withSideEffect(service, command, messagePayload(message), synchronous) @@ -98,7 +98,7 @@ object ValueEntityMessages extends EntityMessages { OutMessage.Reply(ValueEntityReply(id, clientActionReply(payload), Seq.empty, crudAction)) private def reply(id: Long, payload: Option[ScalaPbAny], effects: Effects): OutMessage = - OutMessage.Reply(ValueEntityReply(id, clientActionReply(payload), effects.sideEffects, effects.crudAction)) + OutMessage.Reply(ValueEntityReply(id, clientActionReply(payload), effects.sideEffects, effects.valueEntityAction)) def forward(id: Long, service: String, command: String, payload: ScalaPbMessage): OutMessage = forward(id, service, command, payload, Effects.empty) @@ -114,7 +114,7 @@ object ValueEntityMessages extends EntityMessages { replyAction(id, clientActionForward(service, command, payload), effects) private def replyAction(id: Long, action: Option[ClientAction], effects: Effects): OutMessage = - OutMessage.Reply(ValueEntityReply(id, action, effects.sideEffects, effects.crudAction)) + OutMessage.Reply(ValueEntityReply(id, action, effects.sideEffects, effects.valueEntityAction)) def actionFailure(id: Long, description: String): OutMessage = OutMessage.Reply(ValueEntityReply(id, clientActionFailure(id, description))) From 18cde26e14974d283f26ec7cd839d1c9d7194b3d Mon Sep 17 00:00:00 2001 From: Peter Vlugter Date: Wed, 11 Nov 2020 18:09:38 +1300 Subject: [PATCH 91/93] Dynamically create value entity stores --- build.sbt | 23 +--- .../reflect-config.json.conf | 4 + proxy/core/src/main/resources/in-memory.conf | 20 ++-- proxy/core/src/main/resources/reference.conf | 113 +++--------------- .../proxy/EntityDiscoveryManager.scala | 15 ++- .../valueentity/EntitySupportFactory.scala | 22 +++- .../valueentity/store/InMemoryStore.scala | 3 +- .../proxy/valueentity/store/Repository.scala | 4 +- .../proxy/valueentity/store/Store.scala | 15 ++- .../valueentity/store/StoreSupport.scala | 58 --------- .../DatabaseExceptionHandlingSpec.scala | 8 +- .../valueentity/EntityPassivateSpec.scala | 2 +- .../jdbc/src/main/resources/jdbc-common.conf | 6 +- proxy/jdbc/src/main/resources/reference.conf | 106 +++++++++++++++- .../proxy/jdbc/SlickCreateTables.scala | 2 +- .../SlickEnsureTablesExistReadyCheck.scala | 6 +- .../valueentity/store/jdbc/JdbcConfig.scala | 4 +- .../store/jdbc/JdbcEntityQueries.scala | 0 .../store/jdbc/JdbcEntityTable.scala | 0 .../valueentity/store/jdbc/JdbcStore.scala | 16 ++- .../store/jdbc/JdbcConfigSpec.scala | 62 +++++----- .../reflect-config.json.conf | 4 + .../src/main/resources/application.conf | 22 ++-- 23 files changed, 248 insertions(+), 267 deletions(-) delete mode 100644 proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/StoreSupport.scala rename proxy/{core => jdbc}/src/main/scala/io/cloudstate/proxy/valueentity/store/jdbc/JdbcConfig.scala (94%) rename proxy/{core => jdbc}/src/main/scala/io/cloudstate/proxy/valueentity/store/jdbc/JdbcEntityQueries.scala (100%) rename proxy/{core => jdbc}/src/main/scala/io/cloudstate/proxy/valueentity/store/jdbc/JdbcEntityTable.scala (100%) rename proxy/{core => jdbc}/src/main/scala/io/cloudstate/proxy/valueentity/store/jdbc/JdbcStore.scala (74%) rename proxy/{core => jdbc}/src/test/scala/io/cloudstate/proxy/valueentity/store/jdbc/JdbcConfigSpec.scala (71%) diff --git a/build.sbt b/build.sbt index 66a8869b9..9a739aeb6 100644 --- a/build.sbt +++ b/build.sbt @@ -45,6 +45,7 @@ val AkkaVersion = "2.6.9" val AkkaHttpVersion = "10.1.12" // Note: sync with Akka HTTP version in Akka gRPC val AkkaManagementVersion = "1.0.8" val AkkaPersistenceCassandraVersion = "0.102" +val AkkaPersistenceJdbcVersion = "3.5.2" val AkkaPersistenceSpannerVersion = "1.0.0-RC4" val PrometheusClientVersion = "0.9.0" val ScalaTestVersion = "3.0.8" @@ -54,9 +55,6 @@ val Slf4jSimpleVersion = "1.7.30" val GraalVersion = "20.1.0" val DockerBaseImageVersion = "adoptopenjdk/openjdk11:debianslim-jre" val DockerBaseImageJavaLibraryPath = "${JAVA_HOME}/lib" -val SlickVersion = "3.3.2" -val SlickHikariVersion = "3.3.2" -val AkkaPersistenceJdbcVersion = "3.5.2" val excludeTheseDependencies: Seq[ExclusionRule] = Seq( ExclusionRule("io.netty", "netty"), // grpc-java is using grpc-netty-shaded @@ -79,14 +77,6 @@ def akkaDiscoveryDependency(name: String, excludeThese: ExclusionRule*) = def akkaPersistenceCassandraDependency(name: String, excludeThese: ExclusionRule*) = "com.typesafe.akka" %% name % AkkaPersistenceCassandraVersion excludeAll ((excludeTheseDependencies ++ excludeThese): _*) -def akkaPersistenceJdbcDependency(name: String, excludeThese: ExclusionRule*) = - "com.github.dnvriend" %% name % AkkaPersistenceJdbcVersion excludeAll (excludeThese: _*) - -val excludeSlickDependencies: Seq[ExclusionRule] = Seq( - ExclusionRule("com.typesafe.slick", "slick"), // slick core - ExclusionRule("com.typesafe.slick", "slick-hikaricp") //slick hikari -) - def common: Seq[Setting[_]] = automateHeaderSettings(Compile, Test) ++ Seq( headerMappings := headerMappings.value ++ Seq( de.heikoseeberger.sbtheader.FileType("proto") -> HeaderCommentStyle.cppStyleLineComment, @@ -414,10 +404,8 @@ lazy val `proxy-core` = (project in file("proxy/core")) "com.thesamet.scalapb" %% "scalapb-runtime" % scalapb.compiler.Version.scalapbVersion % "protobuf", "io.prometheus" % "simpleclient" % PrometheusClientVersion, "io.prometheus" % "simpleclient_common" % PrometheusClientVersion, - "org.slf4j" % "slf4j-simple" % Slf4jSimpleVersion, + "org.slf4j" % "slf4j-simple" % Slf4jSimpleVersion //"ch.qos.logback" % "logback-classic" % "1.2.3", // Doesn't work well with SubstrateVM: https://github.com/vmencik/akka-graal-native/blob/master/README.md#logging - "com.typesafe.slick" %% "slick" % SlickVersion, - "com.typesafe.slick" %% "slick-hikaricp" % SlickHikariVersion ), PB.protoSources in Compile ++= { val baseDir = (baseDirectory in ThisBuild).value / "protocols" @@ -444,7 +432,6 @@ lazy val `proxy-spanner` = (project in file("proxy/spanner")) common, name := "cloudstate-proxy-spanner", dependencyOverrides += "io.grpc" % "grpc-netty-shaded" % GrpcNettyShadedVersion, - excludeDependencies ++= excludeSlickDependencies, libraryDependencies ++= Seq( "com.lightbend.akka" %% "akka-persistence-spanner" % AkkaPersistenceSpannerVersion, akkaDependency("akka-cluster-typed"), // Transitive dependency of akka-persistence-spanner @@ -469,7 +456,6 @@ lazy val `proxy-cassandra` = (project in file("proxy/cassandra")) common, name := "cloudstate-proxy-cassandra", dependencyOverrides += "io.grpc" % "grpc-netty-shaded" % GrpcNettyShadedVersion, - excludeDependencies ++= excludeSlickDependencies, libraryDependencies ++= Seq( akkaPersistenceCassandraDependency("akka-persistence-cassandra", ExclusionRule("com.github.jnr")), akkaPersistenceCassandraDependency("akka-persistence-cassandra-launcher") % Test @@ -492,9 +478,8 @@ lazy val `proxy-jdbc` = (project in file("proxy/jdbc")) name := "cloudstate-proxy-jdbc", dependencyOverrides += "io.grpc" % "grpc-netty-shaded" % GrpcNettyShadedVersion, libraryDependencies ++= Seq( - //"com.github.dnvriend" %% "akka-persistence-jdbc" % "3.5.2" - // remove Slick as dependency from akka-persistence-jdbc which is already in the poxy-core - akkaPersistenceJdbcDependency("akka-persistence-jdbc", ExclusionRule("com.typesafe.slick")) + "com.github.dnvriend" %% "akka-persistence-jdbc" % AkkaPersistenceJdbcVersion, + "org.scalatest" %% "scalatest" % ScalaTestVersion % Test ), fork in run := true, mainClass in Compile := Some("io.cloudstate.proxy.CloudStateProxyMain") diff --git a/proxy/core/src/graal/META-INF/native-image/io.cloudstate/cloudstate-proxy-core/reflect-config.json.conf b/proxy/core/src/graal/META-INF/native-image/io.cloudstate/cloudstate-proxy-core/reflect-config.json.conf index 6ad53b928..b79c3f816 100644 --- a/proxy/core/src/graal/META-INF/native-image/io.cloudstate/cloudstate-proxy-core/reflect-config.json.conf +++ b/proxy/core/src/graal/META-INF/native-image/io.cloudstate/cloudstate-proxy-core/reflect-config.json.conf @@ -62,6 +62,10 @@ allDeclaredFields: true allDeclaredConstructors: true } +{ + name: "io.cloudstate.proxy.valueentity.store.InMemoryStore" + allDeclaredConstructors: true +} { name: "io.cloudstate.proxy.Warmup" } diff --git a/proxy/core/src/main/resources/in-memory.conf b/proxy/core/src/main/resources/in-memory.conf index 44ebb1d4d..a4f38c70e 100644 --- a/proxy/core/src/main/resources/in-memory.conf +++ b/proxy/core/src/main/resources/in-memory.conf @@ -2,20 +2,22 @@ include "cloudstate-common" akka.persistence { - - journal.plugin = "akka.persistence.journal.inmem" - snapshot-store.plugin = inmem-snapshot-store - + journal.plugin = "akka.persistence.journal.inmem" + snapshot-store.plugin = inmem-snapshot-store } inmem-snapshot-store { - class = "io.cloudstate.proxy.eventsourced.InMemSnapshotStore" + class = "io.cloudstate.proxy.eventsourced.InMemSnapshotStore" } cloudstate.proxy { - eventsourced-entity.journal-enabled = true + eventsourced-entity { + journal-enabled = true + } - # Configuration for using an in memory Value Entity persistence store - value-entity-enabled = true - value-entity-persistence-store.store-type = "in-memory" + # Configuration for using an in-memory Value Entity persistence store + value-entity { + enabled = true + persistence.store = "in-memory" + } } diff --git a/proxy/core/src/main/resources/reference.conf b/proxy/core/src/main/resources/reference.conf index 2753392c3..b131de965 100644 --- a/proxy/core/src/main/resources/reference.conf +++ b/proxy/core/src/main/resources/reference.conf @@ -131,104 +131,23 @@ cloudstate.proxy { } } - # Enable the Value Entity functionality by setting it to true - value-entity-enabled = false - - # Configures the persistence store for the Value Entity when value-entity-enabled is true - value-entity-persistence-store { - # This property indicate the type of store to be used for Value Entity. - # Valid options are: "jdbc", "in-memory" - # "in-memory" means the data are persisted in memory. - # "jdbc" means the data are persisted in a configured native JDBC database. - store-type = "in-memory" - - # This property indicates which configuration must be used by Slick. - jdbc.database.slick { - connectionPool = "HikariCP" - - # This property indicates which profile must be used by Slick. - # Possible values are: slick.jdbc.PostgresProfile$, slick.jdbc.MySQLProfile$ and slick.jdbc.H2Profile$ - # (uncomment and set the property below to match your needs) - # profile = "slick.jdbc.PostgresProfile$" - - # The JDBC driver to use - # (uncomment and set the property below to match your needs) - # driver = "org.postgresql.Driver" - - # The JDBC URL for the chosen database - # (uncomment and set the property below to match your needs) - # url = "jdbc:postgresql://localhost:5432/cloudstate" - - # The database username - # (uncomment and set the property below to match your needs) - # user = "cloudstate" - - # The database password - # (uncomment and set the property below to match your needs) - # password = "cloudstate" - - - ## copy and adapted from akka-persistece-jdbc - - # hikariCP settings; see: https://github.com/brettwooldridge/HikariCP - # Slick will use an async executor with a fixed size queue of 10.000 objects - # The async executor is a connection pool for asynchronous execution of blocking I/O actions. - # This is used for the asynchronous query execution API on top of blocking back-ends like JDBC. - queueSize = 10000 // number of objects that can be queued by the async exector - - # This property controls the maximum number of milliseconds that a client (that's you) will wait for a connection - # from the pool. If this time is exceeded without a connection becoming available, a SQLException will be thrown. - # 1000ms is the minimum value. Default: 180000 (3 minutes) - connectionTimeout = 180000 - - # This property controls the maximum amount of time that a connection will be tested for aliveness. - # This value must be less than the connectionTimeout. The lowest accepted validation timeout is 1000ms (1 second). Default: 5000 - validationTimeout = 5000 - - # 10 minutes: This property controls the maximum amount of time that a connection is allowed to sit idle in the pool. - # Whether a connection is retired as idle or not is subject to a maximum variation of +30 seconds, and average variation - # of +15 seconds. A connection will never be retired as idle before this timeout. A value of 0 means that idle connections - # are never removed from the pool. Default: 600000 (10 minutes) - idleTimeout = 600000 - - # 30 minutes: This property controls the maximum lifetime of a connection in the pool. When a connection reaches this timeout - # it will be retired from the pool, subject to a maximum variation of +30 seconds. An in-use connection will never be retired, - # only when it is closed will it then be removed. We strongly recommend setting this value, and it should be at least 30 seconds - # less than any database-level connection timeout. A value of 0 indicates no maximum lifetime (infinite lifetime), - # subject of course to the idleTimeout setting. Default: 1800000 (30 minutes) - maxLifetime = 1800000 - - # This property controls the amount of time that a connection can be out of the pool before a message is logged indicating a - # possible connection leak. A value of 0 means leak detection is disabled. - # Lowest acceptable value for enabling leak detection is 2000 (2 secs). Default: 0 - leakDetectionThreshold = 0 - - # ensures that the database does not get dropped while we are using it - keepAliveConnection = on - - # See some tips on thread/connection pool sizing on https://github.com/brettwooldridge/HikariCP/wiki/About-Pool-Sizing - # Keep in mind that the number of threads must equal the maximum number of connections. - numThreads = 5 - maxConnections = 5 - minConnections = 5 - - # This property controls a user-defined name for the connection pool and appears mainly in logging and JMX - # management consoles to identify pools and pool configurations. Default: auto-generated - poolName = "cloudstate-value-entity-connection-pool" - } + value-entity { + # Enable the Value Entity functionality by setting it to true + enabled = false + + # Passivation timeout for Value Entities + passivation-timeout = 30s + + # Configures the persistence store for the Value Entity when value-entity.enabled is true + persistence { + # This property indicates the type of store to be used for Value Entity. + # Valid options are: "in-memory" or "jdbc" (when using a JDBC proxy like Postgres) + # "in-memory" means the data are persisted in memory. + # "jdbc" means the data are persisted in a configured native JDBC database. + store = "in-memory" - # This property indicates the table in use for the Value Entity. - jdbc-state-store { - tables { - state { - tableName = "value_entity_state" - schemaName = "" - columnNames { - persistentId = "persistent_id" - entityId = "entity_id" - state = "state" - } - } + in-memory { + store-class = "io.cloudstate.proxy.valueentity.store.InMemoryStore" } } } diff --git a/proxy/core/src/main/scala/io/cloudstate/proxy/EntityDiscoveryManager.scala b/proxy/core/src/main/scala/io/cloudstate/proxy/EntityDiscoveryManager.scala index 4133876c6..a519143d3 100644 --- a/proxy/core/src/main/scala/io/cloudstate/proxy/EntityDiscoveryManager.scala +++ b/proxy/core/src/main/scala/io/cloudstate/proxy/EntityDiscoveryManager.scala @@ -55,7 +55,7 @@ object EntityDiscoveryManager { gracefulTerminationTimeout: Timeout, numberOfShards: Int, proxyParallelism: Int, - valueEntityEnabled: Boolean, + valueEntitySettings: ValueEntitySettings, eventSourcedSettings: EventSourcedSettings, crdtSettings: CrdtSettings, config: Config @@ -73,7 +73,7 @@ object EntityDiscoveryManager { gracefulTerminationTimeout = Timeout(config.getDuration("graceful-termination-timeout").toMillis.millis), numberOfShards = config.getInt("number-of-shards"), proxyParallelism = config.getInt("proxy-parallelism"), - valueEntityEnabled = config.getBoolean("value-entity-enabled"), + valueEntitySettings = new ValueEntitySettings(config), eventSourcedSettings = new EventSourcedSettings(config), crdtSettings = new CrdtSettings(config), config = config) @@ -93,6 +93,15 @@ object EntityDiscoveryManager { } } + final case class ValueEntitySettings(enabled: Boolean, passivationTimeout: Timeout) { + def this(config: Config) = { + this( + enabled = config.getBoolean("value-entity.enabled"), + passivationTimeout = Timeout(config.getDuration("value-entity.passivation-timeout").toMillis.millis) + ) + } + } + final case class EventSourcedSettings(journalEnabled: Boolean, passivationTimeout: Timeout) { def this(config: Config) = { this( @@ -154,7 +163,7 @@ class EntityDiscoveryManager(config: EntityDiscoveryManager.Configuration)( ) else Map.empty } ++ { - if (config.valueEntityEnabled) + if (config.valueEntitySettings.enabled) Map( ValueEntity.name -> new EntitySupportFactory(system, config, clientSettings) ) diff --git a/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/EntitySupportFactory.scala b/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/EntitySupportFactory.scala index 65acf27de..457d6f047 100644 --- a/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/EntitySupportFactory.scala +++ b/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/EntitySupportFactory.scala @@ -17,7 +17,7 @@ package io.cloudstate.proxy.valueentity import akka.NotUsed -import akka.actor.{ActorRef, ActorSystem} +import akka.actor.{ActorRef, ActorSystem, ExtendedActorSystem} import akka.cluster.sharding.ShardRegion.HashCodeMessageExtractor import akka.cluster.sharding.{ClusterSharding, ClusterShardingSettings} import akka.event.Logging @@ -31,17 +31,17 @@ import io.cloudstate.protocol.value_entity.ValueEntityClient import io.cloudstate.proxy.entity.{EntityCommand, UserFunctionReply} import io.cloudstate.proxy._ import io.cloudstate.proxy.sharding.DynamicLeastShardAllocationStrategy -import io.cloudstate.proxy.valueentity.store.{RepositoryImpl, StoreSupport} +import io.cloudstate.proxy.valueentity.store.{RepositoryImpl, Store} import scala.concurrent.{ExecutionContext, Future} +import scala.util.{Failure, Success} class EntitySupportFactory( system: ActorSystem, config: EntityDiscoveryManager.Configuration, grpcClientSettings: GrpcClientSettings )(implicit ec: ExecutionContext, mat: Materializer) - extends EntityTypeSupportFactory - with StoreSupport { + extends EntityTypeSupportFactory { private final val log = Logging.getLogger(system, this.getClass) @@ -54,10 +54,20 @@ class EntitySupportFactory( val stateManagerConfig = ValueEntity.Configuration(entity.serviceName, entity.persistenceId, - config.passivationTimeout, + config.valueEntitySettings.passivationTimeout, config.relayOutputBufferSize) - val repository = new RepositoryImpl(createStore(config.config)) + val store: Store = { + val storeType = config.config.getString("value-entity.persistence.store") + val className = config.config.getString(s"value-entity.persistence.$storeType.store-class") + val args = List(classOf[ActorSystem] -> system) + system.asInstanceOf[ExtendedActorSystem].dynamicAccess.createInstanceFor[Store](className, args) match { + case Success(result) => result + case Failure(t) => throw new RuntimeException(s"Failed to create Value Entity store [$storeType]", t) + } + } + + val repository = new RepositoryImpl(store) log.debug("Starting ValueEntity for {}", entity.persistenceId) val clusterSharding = ClusterSharding(system) diff --git a/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/InMemoryStore.scala b/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/InMemoryStore.scala index a4083d61a..9ac18580f 100644 --- a/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/InMemoryStore.scala +++ b/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/InMemoryStore.scala @@ -16,6 +16,7 @@ package io.cloudstate.proxy.valueentity.store +import akka.actor.ActorSystem import akka.util.ByteString import io.cloudstate.proxy.valueentity.store.Store.Key @@ -25,7 +26,7 @@ import scala.concurrent.Future /** * Represents an in-memory implementation of the store for value-based entities. */ -final class InMemoryStore extends Store[Key, ByteString] { +final class InMemoryStore(system: ActorSystem) extends Store { private var store = TrieMap.empty[Key, ByteString] diff --git a/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/Repository.scala b/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/Repository.scala index 0f52d751b..2b3db6de6 100644 --- a/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/Repository.scala +++ b/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/Repository.scala @@ -76,11 +76,11 @@ object RepositoryImpl { } -class RepositoryImpl(store: Store[Key, ByteString], serializer: ProtobufSerializer[ScalaPbAny])( +class RepositoryImpl(store: Store, serializer: ProtobufSerializer[ScalaPbAny])( implicit ec: ExecutionContext ) extends Repository { - def this(store: Store[Key, ByteString])(implicit ec: ExecutionContext) = + def this(store: Store)(implicit ec: ExecutionContext) = this(store, RepositoryImpl.EntitySerializer) def get(key: Key): Future[Option[ScalaPbAny]] = diff --git a/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/Store.scala b/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/Store.scala index e058f7cf9..596b0f41c 100644 --- a/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/Store.scala +++ b/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/Store.scala @@ -16,6 +16,8 @@ package io.cloudstate.proxy.valueentity.store +import akka.util.ByteString + import scala.concurrent.Future object Store { @@ -25,12 +27,9 @@ object Store { } /** - * Represents a low level interface for accessing a native database. - * - * @tparam K the type for database key - * @tparam V the type for database value + * A persistent store for value-based entities. */ -trait Store[K, V] { +trait Store { /** * Retrieve the data for the given key. @@ -38,7 +37,7 @@ trait Store[K, V] { * @param key to retrieve data for * @return Some(data) if data exists for the key and None otherwise */ - def get(key: K): Future[Option[V]] + def get(key: Store.Key): Future[Option[ByteString]] /** * Insert the data with the given key if it not already exists. @@ -47,13 +46,13 @@ trait Store[K, V] { * @param key to insert or update the entity * @param value that should be persisted */ - def update(key: K, value: V): Future[Unit] + def update(key: Store.Key, value: ByteString): Future[Unit] /** * Delete the data for the given key. * * @param key to delete data. */ - def delete(key: K): Future[Unit] + def delete(key: Store.Key): Future[Unit] } diff --git a/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/StoreSupport.scala b/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/StoreSupport.scala deleted file mode 100644 index 131c9cb4b..000000000 --- a/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/StoreSupport.scala +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright 2019 Lightbend Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.cloudstate.proxy.valueentity.store - -import akka.util.ByteString -import com.typesafe.config.Config -import StoreSupport.{IN_MEMORY, JDBC} -import io.cloudstate.proxy.valueentity.store.Store.Key -import io.cloudstate.proxy.valueentity.store.jdbc.{ - JdbcEntityQueries, - JdbcEntityTableConfiguration, - JdbcSlickDatabase, - JdbcStore -} - -import scala.concurrent.ExecutionContext; - -object StoreSupport { - final val IN_MEMORY = "in-memory" - final val JDBC = "jdbc" -} - -trait StoreSupport { - - def createStore(config: Config)(implicit ec: ExecutionContext): Store[Key, ByteString] = - config.getString("value-entity-persistence-store.store-type") match { - case IN_MEMORY => new InMemoryStore - case JDBC => createJdbcStore(config) - case other => - throw new IllegalArgumentException( - s"Value Entity store-type must be one of: ${IN_MEMORY} or ${JDBC} but is '$other'" - ) - } - - private def createJdbcStore(config: Config)(implicit ec: ExecutionContext): Store[Key, ByteString] = { - val slickDatabase = JdbcSlickDatabase(config) - val tableConfiguration = new JdbcEntityTableConfiguration( - config.getConfig("value-entity-persistence-store.jdbc-state-store") - ) - val queries = new JdbcEntityQueries(slickDatabase.profile, tableConfiguration) - new JdbcStore(slickDatabase, queries) - } - -} diff --git a/proxy/core/src/test/scala/io/cloudstate/proxy/valueentity/DatabaseExceptionHandlingSpec.scala b/proxy/core/src/test/scala/io/cloudstate/proxy/valueentity/DatabaseExceptionHandlingSpec.scala index 109b8f9f8..3ca880b8d 100644 --- a/proxy/core/src/test/scala/io/cloudstate/proxy/valueentity/DatabaseExceptionHandlingSpec.scala +++ b/proxy/core/src/test/scala/io/cloudstate/proxy/valueentity/DatabaseExceptionHandlingSpec.scala @@ -128,7 +128,7 @@ class DatabaseExceptionHandlingSpec extends AbstractTelemetrySpec { } } - private final class TestJdbcStore(status: String) extends Store[Key, ByteString] { + private final class TestJdbcStore(status: String) extends Store { import TestJdbcStore.JdbcStoreStatus._ private var store = Map.empty[Key, ByteString] @@ -164,11 +164,11 @@ class DatabaseExceptionHandlingSpec extends AbstractTelemetrySpec { val deleteFailure = "DeleteFailure" } - def storeWithGetFailure(): Store[Key, ByteString] = new TestJdbcStore(JdbcStoreStatus.getFailure) + def storeWithGetFailure(): Store = new TestJdbcStore(JdbcStoreStatus.getFailure) - def storeWithUpdateFailure(): Store[Key, ByteString] = new TestJdbcStore(JdbcStoreStatus.updateFailure) + def storeWithUpdateFailure(): Store = new TestJdbcStore(JdbcStoreStatus.updateFailure) - def storeWithDeleteFailure(): Store[Key, ByteString] = new TestJdbcStore(JdbcStoreStatus.deleteFailure) + def storeWithDeleteFailure(): Store = new TestJdbcStore(JdbcStoreStatus.deleteFailure) } private def silentDeadLettersAndUnhandledMessages(implicit system: ActorSystem): Unit = { diff --git a/proxy/core/src/test/scala/io/cloudstate/proxy/valueentity/EntityPassivateSpec.scala b/proxy/core/src/test/scala/io/cloudstate/proxy/valueentity/EntityPassivateSpec.scala index a78ef3726..ed99dca26 100644 --- a/proxy/core/src/test/scala/io/cloudstate/proxy/valueentity/EntityPassivateSpec.scala +++ b/proxy/core/src/test/scala/io/cloudstate/proxy/valueentity/EntityPassivateSpec.scala @@ -67,7 +67,7 @@ class EntityPassivateSpec extends AbstractTelemetrySpec { sendQueueSize = 100 ) - val repository = new RepositoryImpl(new InMemoryStore) + val repository = new RepositoryImpl(new InMemoryStore(system)) val entity = watch(system.actorOf(ValueEntitySupervisor.props(client, entityConfiguration, repository), "entity")) val emptyCommand = Some(protobufAny(EmptyJavaMessage)) diff --git a/proxy/jdbc/src/main/resources/jdbc-common.conf b/proxy/jdbc/src/main/resources/jdbc-common.conf index 1f4110e73..5e501c7cc 100644 --- a/proxy/jdbc/src/main/resources/jdbc-common.conf +++ b/proxy/jdbc/src/main/resources/jdbc-common.conf @@ -1,7 +1,11 @@ include "cloudstate-common" cloudstate.proxy { - value-entity-enabled = true + value-entity { + enabled = true + persistence.store = "jdbc" + } + eventsourced-entity.journal-enabled = true } diff --git a/proxy/jdbc/src/main/resources/reference.conf b/proxy/jdbc/src/main/resources/reference.conf index 12ee7a420..17c86170d 100644 --- a/proxy/jdbc/src/main/resources/reference.conf +++ b/proxy/jdbc/src/main/resources/reference.conf @@ -1,4 +1,102 @@ -cloudstate.proxy.jdbc { - auto-create-tables = true - create-tables-timeout = 10s -} \ No newline at end of file +cloudstate.proxy { + jdbc { + auto-create-tables = true + create-tables-timeout = 10s + } + + value-entity { + persistence { + # This property indicates which configuration must be used by Slick. + jdbc { + store-class = "io.cloudstate.proxy.valueentity.store.jdbc.JdbcStore" + + slick { + connectionPool = "HikariCP" + + # This property indicates which profile must be used by Slick. + # Possible values are: slick.jdbc.PostgresProfile$, slick.jdbc.MySQLProfile$ and slick.jdbc.H2Profile$ + # (uncomment and set the property below to match your needs) + # profile = "slick.jdbc.PostgresProfile$" + + # The JDBC driver to use + # (uncomment and set the property below to match your needs) + # driver = "org.postgresql.Driver" + + # The JDBC URL for the chosen database + # (uncomment and set the property below to match your needs) + # url = "jdbc:postgresql://localhost:5432/cloudstate" + + # The database username + # (uncomment and set the property below to match your needs) + # user = "cloudstate" + + # The database password + # (uncomment and set the property below to match your needs) + # password = "cloudstate" + + + ## copy and adapted from akka-persistece-jdbc + + # hikariCP settings; see: https://github.com/brettwooldridge/HikariCP + # Slick will use an async executor with a fixed size queue of 10.000 objects + # The async executor is a connection pool for asynchronous execution of blocking I/O actions. + # This is used for the asynchronous query execution API on top of blocking back-ends like JDBC. + queueSize = 10000 // number of objects that can be queued by the async exector + + # This property controls the maximum number of milliseconds that a client (that's you) will wait for a connection + # from the pool. If this time is exceeded without a connection becoming available, a SQLException will be thrown. + # 1000ms is the minimum value. Default: 180000 (3 minutes) + connectionTimeout = 180000 + + # This property controls the maximum amount of time that a connection will be tested for aliveness. + # This value must be less than the connectionTimeout. The lowest accepted validation timeout is 1000ms (1 second). Default: 5000 + validationTimeout = 5000 + + # 10 minutes: This property controls the maximum amount of time that a connection is allowed to sit idle in the pool. + # Whether a connection is retired as idle or not is subject to a maximum variation of +30 seconds, and average variation + # of +15 seconds. A connection will never be retired as idle before this timeout. A value of 0 means that idle connections + # are never removed from the pool. Default: 600000 (10 minutes) + idleTimeout = 600000 + + # 30 minutes: This property controls the maximum lifetime of a connection in the pool. When a connection reaches this timeout + # it will be retired from the pool, subject to a maximum variation of +30 seconds. An in-use connection will never be retired, + # only when it is closed will it then be removed. We strongly recommend setting this value, and it should be at least 30 seconds + # less than any database-level connection timeout. A value of 0 indicates no maximum lifetime (infinite lifetime), + # subject of course to the idleTimeout setting. Default: 1800000 (30 minutes) + maxLifetime = 1800000 + + # This property controls the amount of time that a connection can be out of the pool before a message is logged indicating a + # possible connection leak. A value of 0 means leak detection is disabled. + # Lowest acceptable value for enabling leak detection is 2000 (2 secs). Default: 0 + leakDetectionThreshold = 0 + + # ensures that the database does not get dropped while we are using it + keepAliveConnection = on + + # See some tips on thread/connection pool sizing on https://github.com/brettwooldridge/HikariCP/wiki/About-Pool-Sizing + # Keep in mind that the number of threads must equal the maximum number of connections. + numThreads = 5 + maxConnections = 5 + minConnections = 5 + + # This property controls a user-defined name for the connection pool and appears mainly in logging and JMX + # management consoles to identify pools and pool configurations. Default: auto-generated + poolName = "cloudstate-value-entity-connection-pool" + } + + # These settings describe the database tables for storing Value Entities + tables { + state { + tableName = "value_entity_state" + schemaName = "" + columnNames { + persistentId = "persistent_id" + entityId = "entity_id" + state = "state" + } + } + } + } + } + } +} diff --git a/proxy/jdbc/src/main/scala/io/cloudstate/proxy/jdbc/SlickCreateTables.scala b/proxy/jdbc/src/main/scala/io/cloudstate/proxy/jdbc/SlickCreateTables.scala index 1c2111835..bccbb2e28 100644 --- a/proxy/jdbc/src/main/scala/io/cloudstate/proxy/jdbc/SlickCreateTables.scala +++ b/proxy/jdbc/src/main/scala/io/cloudstate/proxy/jdbc/SlickCreateTables.scala @@ -172,7 +172,7 @@ class ValueEntitySlickCreateTable(override val system: ActorSystem, slickDb: Jdb import profile.api._ private val tableCfg = new JdbcEntityTableConfiguration( - system.settings.config.getConfig("cloudstate.proxy.value-entity-persistence-store.jdbc-state-store") + system.settings.config.getConfig("cloudstate.proxy.value-entity.persistence.jdbc") ) private val table = new JdbcEntityTable { diff --git a/proxy/jdbc/src/main/scala/io/cloudstate/proxy/jdbc/SlickEnsureTablesExistReadyCheck.scala b/proxy/jdbc/src/main/scala/io/cloudstate/proxy/jdbc/SlickEnsureTablesExistReadyCheck.scala index 035ebbd5c..e4c9ec26a 100644 --- a/proxy/jdbc/src/main/scala/io/cloudstate/proxy/jdbc/SlickEnsureTablesExistReadyCheck.scala +++ b/proxy/jdbc/src/main/scala/io/cloudstate/proxy/jdbc/SlickEnsureTablesExistReadyCheck.scala @@ -38,7 +38,7 @@ class SlickEnsureTablesExistReadyCheck(system: ActorSystem) extends (() => Futur private val eventSourcedSlickDatabase = SlickExtension(system).database(ConfigFactory.parseMap(Map(ConfigKeys.useSharedDb -> "slick").asJava)) - // Get a hold of the cloudstate.proxy.value-entity-persistence-store.jdbc.database.slick database instance + // Get a hold of the cloudstate.proxy.value-entity.persistence.jdbc.slick database instance private val valueEntitySlickDatabase = JdbcSlickDatabase(proxyConfig) private val check: () => Future[Boolean] = if (autoCreateTables) { @@ -71,8 +71,8 @@ class SlickEnsureTablesExistReadyCheck(system: ActorSystem) extends (() => Futur private def tableCreateCombinations(): Seq[SlickCreateTables] = { val config = system.settings.config - val eventSourcedEnabled = config.getBoolean("cloudstate.proxy.journal-enabled") - val valueEntityEnabled = config.getBoolean("cloudstate.proxy.value-entity-enabled") + val eventSourcedEnabled = config.getBoolean("cloudstate.proxy.eventsourced-entity.journal-enabled") + val valueEntityEnabled = config.getBoolean("cloudstate.proxy.value-entity.enabled") val eventSourcedCombinations = if (eventSourcedEnabled) Seq(new EventSourcedSlickCreateTable(system, eventSourcedSlickDatabase)) else Seq.empty diff --git a/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/jdbc/JdbcConfig.scala b/proxy/jdbc/src/main/scala/io/cloudstate/proxy/valueentity/store/jdbc/JdbcConfig.scala similarity index 94% rename from proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/jdbc/JdbcConfig.scala rename to proxy/jdbc/src/main/scala/io/cloudstate/proxy/valueentity/store/jdbc/JdbcConfig.scala index 8e8540d40..719589325 100644 --- a/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/jdbc/JdbcConfig.scala +++ b/proxy/jdbc/src/main/scala/io/cloudstate/proxy/valueentity/store/jdbc/JdbcConfig.scala @@ -48,12 +48,12 @@ object JdbcSlickDatabase { def apply(config: Config): JdbcSlickDatabase = { val database: JdbcBackend.Database = Database.forConfig( - "value-entity-persistence-store.jdbc.database.slick", + "value-entity.persistence.jdbc.slick", config ) val profile: JdbcProfile = DatabaseConfig .forConfig[JdbcProfile]( - "value-entity-persistence-store.jdbc.database.slick", + "value-entity.persistence.jdbc.slick", config ) .profile diff --git a/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/jdbc/JdbcEntityQueries.scala b/proxy/jdbc/src/main/scala/io/cloudstate/proxy/valueentity/store/jdbc/JdbcEntityQueries.scala similarity index 100% rename from proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/jdbc/JdbcEntityQueries.scala rename to proxy/jdbc/src/main/scala/io/cloudstate/proxy/valueentity/store/jdbc/JdbcEntityQueries.scala diff --git a/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/jdbc/JdbcEntityTable.scala b/proxy/jdbc/src/main/scala/io/cloudstate/proxy/valueentity/store/jdbc/JdbcEntityTable.scala similarity index 100% rename from proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/jdbc/JdbcEntityTable.scala rename to proxy/jdbc/src/main/scala/io/cloudstate/proxy/valueentity/store/jdbc/JdbcEntityTable.scala diff --git a/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/jdbc/JdbcStore.scala b/proxy/jdbc/src/main/scala/io/cloudstate/proxy/valueentity/store/jdbc/JdbcStore.scala similarity index 74% rename from proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/jdbc/JdbcStore.scala rename to proxy/jdbc/src/main/scala/io/cloudstate/proxy/valueentity/store/jdbc/JdbcStore.scala index 737ec6d84..b19229e86 100644 --- a/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/jdbc/JdbcStore.scala +++ b/proxy/jdbc/src/main/scala/io/cloudstate/proxy/valueentity/store/jdbc/JdbcStore.scala @@ -16,21 +16,25 @@ package io.cloudstate.proxy.valueentity.store.jdbc +import akka.actor.ActorSystem import akka.util.ByteString import io.cloudstate.proxy.valueentity.store.Store import io.cloudstate.proxy.valueentity.store.Store.Key import io.cloudstate.proxy.valueentity.store.jdbc.JdbcEntityTable.EntityRow -import scala.concurrent.{ExecutionContext, Future} +import scala.concurrent.Future -private[store] final class JdbcStore(slickDatabase: JdbcSlickDatabase, queries: JdbcEntityQueries)( - implicit ec: ExecutionContext -) extends Store[Key, ByteString] { - - import slickDatabase.profile.api._ +final class JdbcStore(system: ActorSystem) extends Store { + private val config = system.settings.config.getConfig("cloudstate.proxy") + private val slickDatabase = JdbcSlickDatabase(config) + private val tableConfiguration = new JdbcEntityTableConfiguration(config.getConfig("value-entity.persistence.jdbc")) + private val queries = new JdbcEntityQueries(slickDatabase.profile, tableConfiguration) private val db = slickDatabase.database + import slickDatabase.profile.api._ + import system.dispatcher + override def get(key: Key): Future[Option[ByteString]] = for { rows <- db.run(queries.selectByKey(key).result) diff --git a/proxy/core/src/test/scala/io/cloudstate/proxy/valueentity/store/jdbc/JdbcConfigSpec.scala b/proxy/jdbc/src/test/scala/io/cloudstate/proxy/valueentity/store/jdbc/JdbcConfigSpec.scala similarity index 71% rename from proxy/core/src/test/scala/io/cloudstate/proxy/valueentity/store/jdbc/JdbcConfigSpec.scala rename to proxy/jdbc/src/test/scala/io/cloudstate/proxy/valueentity/store/jdbc/JdbcConfigSpec.scala index e4b8a44d2..e185cd7b3 100644 --- a/proxy/core/src/test/scala/io/cloudstate/proxy/valueentity/store/jdbc/JdbcConfigSpec.scala +++ b/proxy/jdbc/src/test/scala/io/cloudstate/proxy/valueentity/store/jdbc/JdbcConfigSpec.scala @@ -22,39 +22,39 @@ import slick.jdbc.JdbcProfile class JdbcConfigSpec extends WordSpecLike with Matchers { private val config: Config = ConfigFactory.parseString(""" - |cloudstate.proxy { - | value-entity-enabled = true - | value-entity-persistence-store { - | store-type = "jdbc" - | jdbc.database.slick { - | profile = "slick.jdbc.PostgresProfile$" - | connectionPool = disabled - | driver = "org.postgresql.Driver" - | url = "jdbc:postgresql://localhost:5432/cloudstate" - | user = "cloudstate" - | password = "cloudstate" - | } - | - | jdbc-state-store { - | tables { - | state { - | tableName = "value_entity_state" - | schemaName = "" - | columnNames { - | persistentId = "persistent_id" - | entityId = "entity_id" - | state = "state" - | } - | } - | } - | } - | } - |} - | - """.stripMargin) + |cloudstate.proxy { + | value-entity.enabled = true + | value-entity.persistence { + | store = "jdbc" + | jdbc { + | slick { + | profile = "slick.jdbc.PostgresProfile$" + | connectionPool = disabled + | driver = "org.postgresql.Driver" + | url = "jdbc:postgresql://localhost:5432/cloudstate" + | user = "cloudstate" + | password = "cloudstate" + | } + | + | tables { + | state { + | tableName = "value_entity_state" + | schemaName = "" + | columnNames { + | persistentId = "persistent_id" + | entityId = "entity_id" + | state = "state" + | } + | } + | } + | } + | } + |} + | + """.stripMargin) private val tableStateConfig = new JdbcEntityTableConfiguration( - config.getConfig("cloudstate.proxy.value-entity-persistence-store.jdbc-state-store") + config.getConfig("cloudstate.proxy.value-entity.persistence.jdbc") ) private val testTable = new JdbcEntityTable { diff --git a/proxy/postgres/src/graal/META-INF/native-image/io.cloudstate/cloudstate-proxy-jdbc/reflect-config.json.conf b/proxy/postgres/src/graal/META-INF/native-image/io.cloudstate/cloudstate-proxy-jdbc/reflect-config.json.conf index 654672005..c7c9d21b3 100644 --- a/proxy/postgres/src/graal/META-INF/native-image/io.cloudstate/cloudstate-proxy-jdbc/reflect-config.json.conf +++ b/proxy/postgres/src/graal/META-INF/native-image/io.cloudstate/cloudstate-proxy-jdbc/reflect-config.json.conf @@ -7,4 +7,8 @@ name: "io.cloudstate.proxy.valueentity.store.jdbc.JdbcEntityTable$Entity" allPublicMethods: true } +{ + name: "io.cloudstate.proxy.valueentity.store.jdbc.JdbcStore" + allDeclaredConstructors: true +} ] diff --git a/proxy/postgres/src/main/resources/application.conf b/proxy/postgres/src/main/resources/application.conf index d3e85d694..511bc0d1b 100644 --- a/proxy/postgres/src/main/resources/application.conf +++ b/proxy/postgres/src/main/resources/application.conf @@ -34,18 +34,18 @@ akka-persistence-jdbc.shared-databases.slick { } } -cloudstate.proxy.value-entity-persistence-store { - store-type = "jdbc" - - jdbc.database.slick { - profile = ${cloudstate.proxy.postgres.profile} - driver = ${cloudstate.proxy.postgres.driver} - url = "jdbc:postgresql://"${cloudstate.proxy.postgres.service}":"${cloudstate.proxy.postgres.port}"/"${cloudstate.proxy.postgres.database} - user = ${cloudstate.proxy.postgres.user} - password = ${cloudstate.proxy.postgres.password} - } +cloudstate.proxy.value-entity.persistence { + store = "jdbc" + + jdbc { + slick { + profile = ${cloudstate.proxy.postgres.profile} + driver = ${cloudstate.proxy.postgres.driver} + url = "jdbc:postgresql://"${cloudstate.proxy.postgres.service}":"${cloudstate.proxy.postgres.port}"/"${cloudstate.proxy.postgres.database} + user = ${cloudstate.proxy.postgres.user} + password = ${cloudstate.proxy.postgres.password} + } - jdbc-state-store { tables { state { schemaName = ${cloudstate.proxy.postgres.schema} From dc786cad22e6391f4dc36f010d6c015bbc94ee73 Mon Sep 17 00:00:00 2001 From: Guy Youansi Date: Wed, 11 Nov 2020 21:19:27 +0100 Subject: [PATCH 92/93] adapt the store-api to able to persist the tpye url of the entity --- .../valueentity/store/InMemoryStore.scala | 10 ++-- .../proxy/valueentity/store/Repository.scala | 49 +++++-------------- .../proxy/valueentity/store/Store.scala | 7 ++- .../DatabaseExceptionHandlingSpec.scala | 8 +-- .../store/EntitySerializerSpec.scala | 46 ----------------- proxy/jdbc/src/main/resources/reference.conf | 1 + .../valueentity/store/jdbc/JdbcConfig.scala | 3 +- .../store/jdbc/JdbcEntityTable.scala | 5 +- .../valueentity/store/jdbc/JdbcStore.scala | 9 ++-- .../store/jdbc/JdbcConfigSpec.scala | 7 ++- 10 files changed, 43 insertions(+), 102 deletions(-) delete mode 100644 proxy/core/src/test/scala/io/cloudstate/proxy/valueentity/store/EntitySerializerSpec.scala diff --git a/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/InMemoryStore.scala b/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/InMemoryStore.scala index 9ac18580f..3900e3f24 100644 --- a/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/InMemoryStore.scala +++ b/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/InMemoryStore.scala @@ -17,8 +17,8 @@ package io.cloudstate.proxy.valueentity.store import akka.actor.ActorSystem -import akka.util.ByteString import io.cloudstate.proxy.valueentity.store.Store.Key +import io.cloudstate.proxy.valueentity.store.Store.Value import scala.collection.concurrent.TrieMap import scala.concurrent.Future @@ -28,12 +28,12 @@ import scala.concurrent.Future */ final class InMemoryStore(system: ActorSystem) extends Store { - private var store = TrieMap.empty[Key, ByteString] + private var store = TrieMap.empty[Key, Value] - override def get(key: Key): Future[Option[ByteString]] = Future.successful(store.get(key)) + override def get(key: Key): Future[Option[Value]] = Future.successful(store.get(key)) - override def update(key: Key, value: ByteString): Future[Unit] = { - store += key -> value + override def update(key: Key, entity: Value): Future[Unit] = { + store += key -> entity Future.unit } diff --git a/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/Repository.scala b/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/Repository.scala index 2b3db6de6..bc69b5258 100644 --- a/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/Repository.scala +++ b/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/Repository.scala @@ -16,7 +16,6 @@ package io.cloudstate.proxy.valueentity.store -import akka.grpc.ProtobufSerializer import akka.util.ByteString import com.google.protobuf.any.{Any => ScalaPbAny} import com.google.protobuf.{ByteString => PbByteString} @@ -30,69 +29,45 @@ import scala.concurrent.{ExecutionContext, Future} trait Repository { /** - * Retrieve the payload for the given key. + * Retrieve the entity for the given key. * - * @param key to retrieve payload for + * @param key to retrieve entity for * @return Some(payload) if payload exists for the key and None otherwise */ def get(key: Key): Future[Option[ScalaPbAny]] /** - * Insert the entity payload with the given key if it not already exists. - * Update the entity payload at the given key if it already exists. + * Insert the entity with the given key if it not already exists. + * Update the entity at the given key if it already exists. * * @param key to insert or update the entity - * @param payload that should be persisted + * @param entity to persist */ - def update(key: Key, payload: ScalaPbAny): Future[Unit] + def update(key: Key, entity: ScalaPbAny): Future[Unit] /** * Delete the entity with the given key. * - * @param key to delete data. + * @param key to delete the entity. */ def delete(key: Key): Future[Unit] } -object RepositoryImpl { - - private[store] final object EntitySerializer extends ProtobufSerializer[ScalaPbAny] { - private val separator = ByteString("|") - - override final def serialize(entity: ScalaPbAny): ByteString = - if (entity.value.isEmpty) { - ByteString(entity.typeUrl) ++ separator ++ ByteString.empty - } else { - ByteString(entity.typeUrl) ++ separator ++ ByteString.fromArrayUnsafe(entity.value.toByteArray) - } - - override final def deserialize(bytes: ByteString): ScalaPbAny = { - val separatorIndex = bytes.indexOf(separator.head) - val (typeUrl, value) = bytes.splitAt(separatorIndex) - ScalaPbAny(typeUrl.utf8String, PbByteString.copyFrom(value.tail.toByteBuffer)) - } - } - -} - -class RepositoryImpl(store: Store, serializer: ProtobufSerializer[ScalaPbAny])( - implicit ec: ExecutionContext -) extends Repository { - - def this(store: Store)(implicit ec: ExecutionContext) = - this(store, RepositoryImpl.EntitySerializer) +class RepositoryImpl(store: Store)(implicit ec: ExecutionContext) extends Repository { def get(key: Key): Future[Option[ScalaPbAny]] = store .get(key) .map { - case Some(value) => Some(serializer.deserialize(value)) + case Some(entity) => + Some(ScalaPbAny(entity.typeUrl, PbByteString.copyFrom(entity.state.toByteBuffer))) + case None => None } def update(key: Key, entity: ScalaPbAny): Future[Unit] = - store.update(key, serializer.serialize(entity)) + store.update(key, Store.Value(entity.typeUrl, ByteString.fromArrayUnsafe(entity.value.toByteArray))) def delete(key: Key): Future[Unit] = store.delete(key) } diff --git a/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/Store.scala b/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/Store.scala index 596b0f41c..013f84c13 100644 --- a/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/Store.scala +++ b/proxy/core/src/main/scala/io/cloudstate/proxy/valueentity/store/Store.scala @@ -24,6 +24,9 @@ object Store { case class Key(persistentId: String, entityId: String) + /** Value to persist with its type url and its content */ + case class Value(typeUrl: String, state: ByteString) + } /** @@ -37,7 +40,7 @@ trait Store { * @param key to retrieve data for * @return Some(data) if data exists for the key and None otherwise */ - def get(key: Store.Key): Future[Option[ByteString]] + def get(key: Store.Key): Future[Option[Store.Value]] /** * Insert the data with the given key if it not already exists. @@ -46,7 +49,7 @@ trait Store { * @param key to insert or update the entity * @param value that should be persisted */ - def update(key: Store.Key, value: ByteString): Future[Unit] + def update(key: Store.Key, value: Store.Value): Future[Unit] /** * Delete the data for the given key. diff --git a/proxy/core/src/test/scala/io/cloudstate/proxy/valueentity/DatabaseExceptionHandlingSpec.scala b/proxy/core/src/test/scala/io/cloudstate/proxy/valueentity/DatabaseExceptionHandlingSpec.scala index 3ca880b8d..32d67f23b 100644 --- a/proxy/core/src/test/scala/io/cloudstate/proxy/valueentity/DatabaseExceptionHandlingSpec.scala +++ b/proxy/core/src/test/scala/io/cloudstate/proxy/valueentity/DatabaseExceptionHandlingSpec.scala @@ -20,13 +20,13 @@ import akka.actor.{Actor, ActorRef, ActorSystem} import akka.grpc.GrpcClientSettings import akka.testkit.TestEvent.Mute import akka.testkit.{EventFilter, TestActorRef} -import akka.util.ByteString import com.google.protobuf.any.{Any => ScalaPbAny} import com.google.protobuf.{ByteString => PbByteString} import io.cloudstate.protocol.value_entity.ValueEntityClient import io.cloudstate.proxy.entity.{EntityCommand, UserFunctionReply} import io.cloudstate.proxy.telemetry.AbstractTelemetrySpec import io.cloudstate.proxy.valueentity.store.Store.Key +import io.cloudstate.proxy.valueentity.store.Store.Value import io.cloudstate.proxy.valueentity.store.{RepositoryImpl, Store} import io.cloudstate.testkit.TestService import io.cloudstate.testkit.valueentity.ValueEntityMessages @@ -131,14 +131,14 @@ class DatabaseExceptionHandlingSpec extends AbstractTelemetrySpec { private final class TestJdbcStore(status: String) extends Store { import TestJdbcStore.JdbcStoreStatus._ - private var store = Map.empty[Key, ByteString] + private var store = Map.empty[Key, Value] - override def get(key: Key): Future[Option[ByteString]] = + override def get(key: Key): Future[Option[Value]] = status match { case `getFailure` => Future.failed(new RuntimeException("Database GET access failed because of boom!")) case _ => Future.successful(store.get(key)) } - override def update(key: Key, value: ByteString): Future[Unit] = + override def update(key: Key, value: Value): Future[Unit] = status match { case `updateFailure` => Future.failed(new RuntimeException("Database Update access failed because of boom!")) case _ => diff --git a/proxy/core/src/test/scala/io/cloudstate/proxy/valueentity/store/EntitySerializerSpec.scala b/proxy/core/src/test/scala/io/cloudstate/proxy/valueentity/store/EntitySerializerSpec.scala deleted file mode 100644 index 5e1155476..000000000 --- a/proxy/core/src/test/scala/io/cloudstate/proxy/valueentity/store/EntitySerializerSpec.scala +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright 2019 Lightbend Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.cloudstate.proxy.valueentity.store - -import akka.util.ByteString -import com.google.protobuf.any.{Any => ScalaPbAny} -import com.google.protobuf.{ByteString => ProtobufByteString} -import io.cloudstate.proxy.valueentity.store.RepositoryImpl.EntitySerializer -import org.scalatest.{Matchers, WordSpecLike} - -class EntitySerializerSpec extends WordSpecLike with Matchers { - - "Entity Serializer" should { - import EntitySerializer._ - - val entity = ScalaPbAny("p.cloudstate.io/string", ProtobufByteString.copyFromUtf8("state")) - - "serialize entity" in { - serialize(entity) shouldBe ByteString("p.cloudstate.io/string|state") - } - - "deserialize state" in { - deserialize(serialize(entity)) shouldBe entity - } - - "not deserialize state" in { - val wrongSerializedEntity = ByteString("p.cloudstate.io/string_state") - deserialize(wrongSerializedEntity) should not be entity - } - } - -} diff --git a/proxy/jdbc/src/main/resources/reference.conf b/proxy/jdbc/src/main/resources/reference.conf index 17c86170d..56976b77b 100644 --- a/proxy/jdbc/src/main/resources/reference.conf +++ b/proxy/jdbc/src/main/resources/reference.conf @@ -92,6 +92,7 @@ cloudstate.proxy { columnNames { persistentId = "persistent_id" entityId = "entity_id" + typeUrl = "type_url" state = "state" } } diff --git a/proxy/jdbc/src/main/scala/io/cloudstate/proxy/valueentity/store/jdbc/JdbcConfig.scala b/proxy/jdbc/src/main/scala/io/cloudstate/proxy/valueentity/store/jdbc/JdbcConfig.scala index 719589325..0abfa3e9b 100644 --- a/proxy/jdbc/src/main/scala/io/cloudstate/proxy/valueentity/store/jdbc/JdbcConfig.scala +++ b/proxy/jdbc/src/main/scala/io/cloudstate/proxy/valueentity/store/jdbc/JdbcConfig.scala @@ -26,9 +26,10 @@ class JdbcEntityTableColumnNames(config: Config) { val persistentId: String = cfg.getString("persistentId") val entityId: String = cfg.getString("entityId") + val typeUrl: String = cfg.getString("typeUrl") val state: String = cfg.getString("state") - override def toString: String = s"JdbcEntityTableColumnNames($persistentId,$entityId,$state)" + override def toString: String = s"JdbcEntityTableColumnNames($persistentId,$entityId,$typeUrl,$state)" } class JdbcEntityTableConfiguration(config: Config) { diff --git a/proxy/jdbc/src/main/scala/io/cloudstate/proxy/valueentity/store/jdbc/JdbcEntityTable.scala b/proxy/jdbc/src/main/scala/io/cloudstate/proxy/valueentity/store/jdbc/JdbcEntityTable.scala index 58ea664f0..f22bda6cc 100644 --- a/proxy/jdbc/src/main/scala/io/cloudstate/proxy/valueentity/store/jdbc/JdbcEntityTable.scala +++ b/proxy/jdbc/src/main/scala/io/cloudstate/proxy/valueentity/store/jdbc/JdbcEntityTable.scala @@ -22,7 +22,7 @@ import slick.lifted.{MappedProjection, ProvenShape} object JdbcEntityTable { - case class EntityRow(key: Key, state: Array[Byte]) + case class EntityRow(key: Key, typeUrl: String, state: Array[Byte]) } trait JdbcEntityTable { @@ -37,11 +37,12 @@ trait JdbcEntityTable { extends Table[EntityRow](_tableTag = tableTag, _schemaName = entityTableCfg.schemaName, _tableName = entityTableCfg.tableName) { - def * : ProvenShape[EntityRow] = (key, state) <> (EntityRow.tupled, EntityRow.unapply) + def * : ProvenShape[EntityRow] = (key, typeUrl, state) <> (EntityRow.tupled, EntityRow.unapply) val persistentId: Rep[String] = column[String](entityTableCfg.columnNames.persistentId, O.Length(255, varying = true)) val entityId: Rep[String] = column[String](entityTableCfg.columnNames.entityId, O.Length(255, varying = true)) + val typeUrl: Rep[String] = column[String](entityTableCfg.columnNames.typeUrl, O.Length(255, varying = true)) val state: Rep[Array[Byte]] = column[Array[Byte]](entityTableCfg.columnNames.state) val key: MappedProjection[Key, (String, String)] = (persistentId, entityId) <> (Key.tupled, Key.unapply) val pk = primaryKey(s"${tableName}_pk", (persistentId, entityId)) diff --git a/proxy/jdbc/src/main/scala/io/cloudstate/proxy/valueentity/store/jdbc/JdbcStore.scala b/proxy/jdbc/src/main/scala/io/cloudstate/proxy/valueentity/store/jdbc/JdbcStore.scala index b19229e86..364e0fed9 100644 --- a/proxy/jdbc/src/main/scala/io/cloudstate/proxy/valueentity/store/jdbc/JdbcStore.scala +++ b/proxy/jdbc/src/main/scala/io/cloudstate/proxy/valueentity/store/jdbc/JdbcStore.scala @@ -20,6 +20,7 @@ import akka.actor.ActorSystem import akka.util.ByteString import io.cloudstate.proxy.valueentity.store.Store import io.cloudstate.proxy.valueentity.store.Store.Key +import io.cloudstate.proxy.valueentity.store.Store.Value import io.cloudstate.proxy.valueentity.store.jdbc.JdbcEntityTable.EntityRow import scala.concurrent.Future @@ -35,14 +36,14 @@ final class JdbcStore(system: ActorSystem) extends Store { import slickDatabase.profile.api._ import system.dispatcher - override def get(key: Key): Future[Option[ByteString]] = + override def get(key: Key): Future[Option[Value]] = for { rows <- db.run(queries.selectByKey(key).result) - } yield rows.headOption.map(r => ByteString(r.state)) + } yield rows.headOption.map(r => Value(r.typeUrl, ByteString(r.state))) - override def update(key: Key, value: ByteString): Future[Unit] = + override def update(key: Key, value: Value): Future[Unit] = for { - _ <- db.run(queries.insertOrUpdate(EntityRow(key, value.toByteBuffer.array()))) + _ <- db.run(queries.insertOrUpdate(EntityRow(key, value.typeUrl, value.state.toByteBuffer.array()))) } yield () override def delete(key: Key): Future[Unit] = diff --git a/proxy/jdbc/src/test/scala/io/cloudstate/proxy/valueentity/store/jdbc/JdbcConfigSpec.scala b/proxy/jdbc/src/test/scala/io/cloudstate/proxy/valueentity/store/jdbc/JdbcConfigSpec.scala index e185cd7b3..fd6d6e580 100644 --- a/proxy/jdbc/src/test/scala/io/cloudstate/proxy/valueentity/store/jdbc/JdbcConfigSpec.scala +++ b/proxy/jdbc/src/test/scala/io/cloudstate/proxy/valueentity/store/jdbc/JdbcConfigSpec.scala @@ -43,6 +43,7 @@ class JdbcConfigSpec extends WordSpecLike with Matchers { | columnNames { | persistentId = "persistent_id" | entityId = "entity_id" + | typeUrl = "type_url" | state = "state" | } | } @@ -82,6 +83,10 @@ class JdbcConfigSpec extends WordSpecLike with Matchers { tableStateConfig.tableName, tableStateConfig.columnNames.entityId ) + testTable.EntityTable.baseTableRow.typeUrl.toString shouldBe columnName( + tableStateConfig.tableName, + tableStateConfig.columnNames.typeUrl + ) testTable.EntityTable.baseTableRow.state.toString shouldBe columnName( tableStateConfig.tableName, tableStateConfig.columnNames.state @@ -92,7 +97,7 @@ class JdbcConfigSpec extends WordSpecLike with Matchers { "EntityTableConfig" should { "be correctly represent as string" in { testTable.entityTableCfg.toString shouldBe - "JdbcEntityTableConfiguration(value_entity_state,None,JdbcEntityTableColumnNames(persistent_id,entity_id,state))" + "JdbcEntityTableConfiguration(value_entity_state,None,JdbcEntityTableColumnNames(persistent_id,entity_id,type_url,state))" } } From 340325ddb1957c11dd4a5f70eb84611cc1a0cb8e Mon Sep 17 00:00:00 2001 From: Guy Youansi Date: Wed, 11 Nov 2020 21:32:43 +0100 Subject: [PATCH 93/93] add comment for explaining duplicate shoppingcart.proto --- .../example/valueentity/shoppingcart/shoppingcart.proto | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/protocols/example/valueentity/shoppingcart/shoppingcart.proto b/protocols/example/valueentity/shoppingcart/shoppingcart.proto index a5290419f..f5ef39b8f 100644 --- a/protocols/example/valueentity/shoppingcart/shoppingcart.proto +++ b/protocols/example/valueentity/shoppingcart/shoppingcart.proto @@ -12,7 +12,14 @@ // See the License for the specific language governing permissions and // limitations under the License. -// This is the public API offered by the shopping cart entity. +// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +// This is a duplication of the shopping cart in the shoppingcart directory. +// This duplication is because the TCK cannot run multiple services separately yet. +// The tests are run in combination. We need the duplication because it is not possible +// to register multiple services for the same proto file. +// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +// This is the public API offered by the shopping cart value-based entity. syntax = "proto3"; import "google/protobuf/empty.proto";