-
Notifications
You must be signed in to change notification settings - Fork 97
WIP-CRUD on top of Event Sourcing #220
Changes from 103 commits
6d3b2c1
0fbd706
27d22ff
2a32aa1
579d1c4
8014961
91f3333
38327d3
6cc25fe
9a7a8bf
db2e4a8
8aa1e97
2374b7b
82c2377
1f741d9
ca6b416
92effcd
473d682
6e23140
3664944
c9cc5f6
cee69f1
8e951e4
170bb0c
78b7840
012c1e3
5647e15
dc810c4
078517f
5e8ec79
da4a331
78345f8
d1bdbb1
d9a5dbd
ab73109
e906657
344d8d9
cda633a
87d1c4f
d0215aa
9f72587
866941d
6e997f2
9c9e02b
164a650
9a3cf53
c5de37d
0979a9e
5169003
b93a3c0
1321d1e
c6e162a
0675b17
1fd017d
928933c
2310fc9
919756d
dd2e4c8
bbd5e53
99f8ffc
8692f57
18ce9c8
209648d
44175e0
b866e71
2731570
46b54fc
7ec4ab5
d076e86
b05f7aa
11135d7
cb282ad
863dfe3
e511b4b
3bc45a0
93a5962
dd19ed1
e84d4e6
6243e4d
624afbf
968c332
29cdd47
5cf66b7
bb98d71
6a2cd86
8acd3c9
5a97b74
1b163b3
821fd4c
5d9a193
0248e53
d8f2e9f
4c90c02
353b41c
47f7cd3
735257c
e7ae59c
41f189a
3f9789e
eac2e7f
35e0005
f820f7f
f21b7d0
f65553c
dd30efb
517ee37
d4b2493
a6bc7d9
375f9f9
a578d6b
87dbd80
65e5fc8
2f6ba27
8f1835a
62f0500
f7e27aa
a2bf57c
469edab
403aed0
10c931e
c915048
5bf6a12
937084e
5545e6d
5856394
427b538
18cde26
dc786ca
340325d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -54,6 +54,8 @@ 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 excludeTheseDependencies: Seq[ExclusionRule] = Seq( | ||
ExclusionRule("io.netty", "netty"), // grpc-java is using grpc-netty-shaded | ||
|
@@ -107,15 +109,16 @@ 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`, | ||
`java-support`, | ||
`java-support-docs`, | ||
`java-support-tck`, | ||
`java-shopping-cart`, | ||
`java-valueentity-shopping-cart`, | ||
`java-pingpong`, | ||
`akka-client`, | ||
operator, | ||
|
@@ -396,8 +399,10 @@ 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why are slick dependencies being added to proxy-core? Seems strange to add here and then exclude in other proxies, rather than have all the slick-based backends together. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I agree with the slick-dependencies only in proxy-jdbc. I am looking for the best option to have it separated. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The common approach for Lightbend projects is to have a setting in config, that includes a fully qualified class name that can be created dynamically. So that there's something like:
I can take a look at adding this. |
||
), | ||
PB.protoSources in Compile ++= { | ||
val baseDir = (baseDirectory in ThisBuild).value / "protocols" | ||
|
@@ -468,6 +473,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, | ||
|
@@ -631,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`) | ||
.dependsOn(`java-support`, `java-shopping-cart`, `java-valueentity-shopping-cart`) | ||
.enablePlugins(AkkaGrpcPlugin, AssemblyPlugin, JavaAppPackaging, DockerPlugin, AutomateHeaderPlugin, NoPublish) | ||
.settings( | ||
name := "cloudstate-java-tck", | ||
|
@@ -665,6 +672,26 @@ lazy val `java-shopping-cart` = (project in file("samples/java-shopping-cart")) | |
assemblySettings("java-shopping-cart.jar") | ||
) | ||
|
||
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-valueentity-shopping-cart", | ||
dockerSettings, | ||
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 ++= { | ||
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", "11", "-target", "11"), | ||
assemblySettings("java-valueentity-shopping-cart.jar") | ||
) | ||
|
||
lazy val `java-pingpong` = (project in file("samples/java-pingpong")) | ||
.dependsOn(`java-support`) | ||
.enablePlugins(AkkaGrpcPlugin, AssemblyPlugin, JavaAppPackaging, DockerPlugin, AutomateHeaderPlugin, NoPublish) | ||
|
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -20,17 +20,21 @@ | |||||
import akka.stream.Materializer; | ||||||
import com.typesafe.config.Config; | ||||||
import com.google.protobuf.Descriptors; | ||||||
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.valueentity.ValueEntityFactory; | ||||||
import io.cloudstate.javasupport.eventsourced.EventSourcedEntity; | ||||||
import io.cloudstate.javasupport.eventsourced.EventSourcedEntityFactory; | ||||||
import io.cloudstate.javasupport.impl.AnySupport; | ||||||
import io.cloudstate.javasupport.impl.action.AnnotationBasedActionSupport; | ||||||
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.eventsourced.AnnotationBasedEventSourcedSupport; | ||||||
import io.cloudstate.javasupport.impl.eventsourced.EventSourcedStatefulService; | ||||||
|
||||||
|
@@ -145,7 +149,7 @@ public CloudState registerEventSourcedEntity( | |||||
} | ||||||
|
||||||
/** | ||||||
* Register an event sourced entity factor. | ||||||
* Register an event sourced entity factory. | ||||||
* | ||||||
* <p>This is a low level API intended for custom (eg, non reflection based) mechanisms for | ||||||
* implementing the entity. | ||||||
|
@@ -300,6 +304,75 @@ public CloudState registerAction( | |||||
return this; | ||||||
} | ||||||
|
||||||
/** | ||||||
* Register a annotated value entity. | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @pvlugter if we want to use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @ralphlaude @pvlugter I'm curious if ValueEntity (previous CRUD) is a ValueEntity or now an Entity, then how does this entity discriminate itself from an "eventsourced" entity, a "crdt" entity and an "action"(?) entity by its naming? I'm writing this as I found having packages for each state model in Go, where those entities are categorized into being: wdyt? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wonder if we made a good choice of names. Initially this was supposed to be a CrudEntity that would be an Entity capable of doing simple CRUD operations that would be implemented on top of EventSourced support (which seems to me to be an EventSourced specialization), however it became a generic and default entity called Entity or ValueEntity which seemed very confusing. I think we should be explicit about the names of the entity and not support a generic or default entity because the user should want to know what he is doing and nothing better than a name that clearly indicates this. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am inclined to agree with @sleipnir and @marcellanz here. Alternatives:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. DurableEntity is another option together with DurableEventSourcedEntity, and eventually DurableReplicatedEntity? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Clearly, a user should not have to think or read more about this (simple?) state model type than she has to for replicated or eventsourced entities. As an example, I'd have not to explain anyone what a KV entity is. ManagedEntity, all my JPA friends will be happy :) Guidance: There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @pvlugter @viktorklang you really didn't came to the durable and persisted entity while I wrote that comment at the same time (into my little iPhone Github App), no? 😅 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @marcellanz Great minds think alike. Cloudstate community ftw! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This is my inclination as well.
I find it rather amazing really—how much value it can provide without having to know about the content
Good point |
||||||
* | ||||||
* <p>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. | ||||||
* @param additionalDescriptors Any additional descriptors that should be used to look up protobuf | ||||||
* types when needed. | ||||||
* @return This stateful service builder. | ||||||
*/ | ||||||
public CloudState registerValueEntity( | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. On the user side, in the language supports, I think we can just use |
||||||
Class<?> entityClass, | ||||||
Descriptors.ServiceDescriptor descriptor, | ||||||
Descriptors.FileDescriptor... additionalDescriptors) { | ||||||
|
||||||
ValueEntity entity = entityClass.getAnnotation(ValueEntity.class); | ||||||
if (entity == null) { | ||||||
throw new IllegalArgumentException( | ||||||
entityClass + " does not declare an " + ValueEntity.class + " annotation!"); | ||||||
} | ||||||
|
||||||
final String persistenceId; | ||||||
if (entity.persistenceId().isEmpty()) { | ||||||
persistenceId = entityClass.getSimpleName(); | ||||||
} else { | ||||||
persistenceId = entity.persistenceId(); | ||||||
} | ||||||
|
||||||
final AnySupport anySupport = newAnySupport(additionalDescriptors); | ||||||
ValueEntityStatefulService service = | ||||||
new ValueEntityStatefulService( | ||||||
new AnnotationBasedValueEntitySupport(entityClass, anySupport, descriptor), | ||||||
descriptor, | ||||||
anySupport, | ||||||
persistenceId); | ||||||
|
||||||
services.put(descriptor.getFullName(), system -> service); | ||||||
|
||||||
return this; | ||||||
} | ||||||
|
||||||
/** | ||||||
* Register a value entity factory. | ||||||
* | ||||||
* <p>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 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, | ||||||
Descriptors.ServiceDescriptor descriptor, | ||||||
String persistenceId, | ||||||
Descriptors.FileDescriptor... additionalDescriptors) { | ||||||
services.put( | ||||||
descriptor.getFullName(), | ||||||
system -> | ||||||
new ValueEntityStatefulService( | ||||||
factory, descriptor, newAnySupport(additionalDescriptors), persistenceId)); | ||||||
|
||||||
return this; | ||||||
} | ||||||
|
||||||
/** | ||||||
* Starts a server with the configured entities. | ||||||
* | ||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
/* | ||
* 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.valueentity; | ||
|
||
import io.cloudstate.javasupport.ClientActionContext; | ||
import io.cloudstate.javasupport.EffectContext; | ||
import io.cloudstate.javasupport.MetadataContext; | ||
|
||
import java.util.Optional; | ||
|
||
/** | ||
* A value entity command context. | ||
* | ||
* <p>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<T> | ||
extends ValueEntityContext, ClientActionContext, EffectContext, MetadataContext { | ||
|
||
/** | ||
* 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(); | ||
|
||
/** | ||
* 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<T> getState(); | ||
|
||
/** | ||
* Update the entity with the new state. The state will be persisted. | ||
* | ||
* @param state The state to persist. | ||
*/ | ||
void updateState(T state); | ||
|
||
/** Delete the entity state. */ | ||
void deleteState(); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
/* | ||
* 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.valueentity; | ||
|
||
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 on a value entity as a command handler. | ||
* | ||
* <p>This method will be invoked whenever the service call with name that matches this command | ||
* handlers name is invoked. | ||
* | ||
* <p>The method may take the command object as a parameter, its type must match the gRPC service | ||
* input type. | ||
* | ||
* <p>The return type of the method must match the gRPC services output type. | ||
* | ||
* <p>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. | ||
* | ||
* <p>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 ""; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +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. | ||
*/ | ||
|
||
package io.cloudstate.javasupport.valueentity; | ||
|
||
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; | ||
|
||
/** A value entity. */ | ||
@CloudStateAnnotation | ||
@Target(ElementType.TYPE) | ||
@Retention(RetentionPolicy.RUNTIME) | ||
public @interface ValueEntity { | ||
/** | ||
* The name of the persistence id. | ||
* | ||
* <p>If not specified, defaults to the entities unqualified classname. It's strongly recommended | ||
* that you specify it explicitly. | ||
*/ | ||
String persistenceId() default ""; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +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.valueentity; | ||
|
||
import io.cloudstate.javasupport.EntityContext; | ||
|
||
/** Root context for all value entity contexts. */ | ||
public interface ValueEntityContext extends EntityContext {} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Given that we'll seeing the value entities as the default, this implementation of the shopping cart could just be
java-shopping-cart
, with the event sourced version updated tojava-eventsourced-shopping-cart
.We can make the switch to value entities as the default later though too.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I will suggest to switch the value entity as default later. I will keep it mind and write an issue for it. the metrics is an open issue here and should be also done. I will also write a issue for that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍 sounds good. Issues for switching default entity and adding metrics would be useful.