diff --git a/.travis.yml b/.travis.yml index 70790f7..5adf9f5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,16 +14,12 @@ jobs: after_success: - if [ "$TRAVIS_OS_NAME" = "linux" ]; then bash <(curl -s https://codecov.io/bash); fi + - stage: validate docs + script: make -C docs + - stage: deploy docs - name: deploy release docs to cloudstate.io if: tag =~ ^v - language: scala - script: cd docs && sbt deploy - - stage: deploy docs - name: deploy snapshot docs to cloudstate.io - if: branch = master AND type = push - language: scala - script: cd docs && sbt deploy + script: make -C docs deploy env: global: diff --git a/cloudstate/event.go b/cloudstate/event.go index d0b1ced..c29d20d 100644 --- a/cloudstate/event.go +++ b/cloudstate/event.go @@ -86,30 +86,30 @@ func (e *eventEmitter) Clear() { e.events = make([]interface{}, 0) } -//#event-handler +// tag::event-handler[] type EventHandler interface { HandleEvent(ctx context.Context, event interface{}) (handled bool, err error) } -//#event-handler +// end::event-handler[] -//#command-handler +// tag::command-handler[] type CommandHandler interface { HandleCommand(ctx context.Context, command interface{}) (handled bool, reply interface{}, err error) } -//#command-handler +// end::command-handler[] -//#snapshotter +// tag::snapshotter[] type Snapshotter interface { Snapshot() (snapshot interface{}, err error) } -//#snapshotter +// end::snapshotter[] -//#snapshot-handler +// tag::snapshot-handler[] type SnapshotHandler interface { HandleSnapshot(snapshot interface{}) (handled bool, err error) } -//#snapshot-handler +// end::snapshot-handler[] diff --git a/cloudstate/eventsourced.go b/cloudstate/eventsourced.go index 72fc0b1..6c5c75c 100644 --- a/cloudstate/eventsourced.go +++ b/cloudstate/eventsourced.go @@ -40,7 +40,7 @@ const snapshotEveryDefault = 100 // EventSourcedEntity captures an Entity, its ServiceName and PersistenceID. // It is used to be registered as an event sourced entity on a CloudState instance. -//#event-sourced-entity-type +// tag::event-sourced-entity-type[] type EventSourcedEntity struct { // ServiceName is the fully qualified name of the service that implements this entities interface. // Setting it is mandatory. @@ -59,15 +59,15 @@ type EventSourcedEntity struct { SnapshotEvery int64 // EntityFactory is a factory method which generates a new Entity. - //#event-sourced-entity-func + // tag::event-sourced-entity-func[] EntityFunc func() Entity - //#event-sourced-entity-func + // end::event-sourced-entity-func[] // internal registerOnce sync.Once } -//#event-sourced-entity-type +// end::event-sourced-entity-type[] // init get its Entity type and Zero-Value it to // something we can use as an initializer. diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 0000000..a38c490 --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1,3 @@ +/.cache/ +/.deploy/ +/build/ diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..676be5a --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,71 @@ +# Make Cloudstate Go documentation + +module := go +upstream := cloudstateio/go-support +branch := docs/current +sources := src build/src/managed + +cloudstate_antora_download := https://github.com/cloudstateio/cloudstate-antora/raw/master/cloudstate-antora +cloudstate_antora := .cache/bin/cloudstate-antora +descriptor := build/site.yml +root_dir := $(shell git rev-parse --show-toplevel) +src_managed := build/src/managed +managed_examples := ${src_managed}/modules/go/examples +managed_partials := ${src_managed}/modules/go/partials + +define copy_example + mkdir -p "${managed_examples}/$$(dirname $1)" + cp "${root_dir}/$1" "${managed_examples}/$1" +endef + +.SILENT: + +build: clean managed validate html + +${cloudstate_antora}: + mkdir -p $$(dirname ${cloudstate_antora}) + curl -Lo ${cloudstate_antora} ${cloudstate_antora_download} + chmod +x ${cloudstate_antora} + +clean-cache: + rm -rf .cache + +update: clean-cache ${cloudstate_antora} + +clean: ${cloudstate_antora} + ${cloudstate_antora} clean + +managed: attributes examples + mkdir -p "${src_managed}" + cp src/antora.yml "${src_managed}/antora.yml" + +attributes: ${cloudstate_antora} + mkdir -p "${managed_partials}" + ${cloudstate_antora} version | xargs -0 printf ":cloudstate-go-lib-version: %s" \ + > "${managed_partials}/attributes.adoc" + +examples: + # also create empty go module to ignore example sources + mkdir -p "${managed_examples}" + touch "${managed_examples}/go.mod" + $(call copy_example,cloudstate/event.go) + $(call copy_example,cloudstate/eventsourced.go) + $(call copy_example,protobuf/example/shoppingcart/persistence/domain.proto) + $(call copy_example,tck/cmd/tck_shoppingcart/shoppingcart.go) + +${descriptor}: ${cloudstate_antora} + mkdir -p $$(dirname ${descriptor}) + ${cloudstate_antora} source --preview --upstream ${upstream} ${sources} > build/source.yml + ${cloudstate_antora} site --preview --exclude ${module} build/source.yml > ${descriptor} + +validate: ${descriptor} + ${cloudstate_antora} validate ${descriptor} + +html: ${descriptor} + ${cloudstate_antora} build ${descriptor} + +validate-links: ${cloudstate_antora} + ${cloudstate_antora} validate --no-xrefs --links + +deploy: clean managed + ${cloudstate_antora} deploy --module ${module} --upstream ${upstream} --branch ${branch} ${sources} diff --git a/docs/README.md b/docs/README.md index 0599435..c8dd7e1 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,23 +1,19 @@ # Cloudstate Go documentation -Documentation source for Cloudstate Go, published to https://cloudstate.io/docs/go/current/ +The Cloudstate documentation is built using [Antora](https://antora.org) with Asciidoc sources. -To build the docs with [sbt](https://www.scala-sbt.org): +The build is defined in the [Makefile](Makefile) and requires `make`, `bash`, and `docker`. -``` -sbt paradox -``` - -Can also first start the sbt interactive shell with `sbt`, then run commands. - -The documentation can be viewed locally by opening the generated pages: +To build the documentation run: ``` -open target/paradox/site/main/index.html +make ``` -To watch files for changes and rebuild docs automatically: +The generated documentation site will be available in the `build/site` directory: ``` -sbt ~paradox +open build/site/index.html ``` + +Documentation will be automatically deployed on tagged versions, in the Travis CI builds. diff --git a/docs/build.sbt b/docs/build.sbt deleted file mode 100644 index e891fe2..0000000 --- a/docs/build.sbt +++ /dev/null @@ -1,12 +0,0 @@ -lazy val docs = project - .in(file(".")) - .enablePlugins(CloudstateParadoxPlugin) - .settings( - deployModule := "go", - paradoxProperties in Compile ++= Map( - "cloudstate.go.version" -> "1.14", - "cloudstate.go.lib.version" -> { if (isSnapshot.value) previousStableVersion.value.getOrElse("0.0.0") else version.value }, - "extref.cloudstate.base_url" -> "https://cloudstate.io/docs/core/current/%s", - "snip.base.base_dir" -> s"${(baseDirectory in ThisBuild).value.getAbsolutePath}/../", - ) - ) diff --git a/docs/project/build.properties b/docs/project/build.properties deleted file mode 100644 index 654fe70..0000000 --- a/docs/project/build.properties +++ /dev/null @@ -1 +0,0 @@ -sbt.version=1.3.12 diff --git a/docs/project/plugins.sbt b/docs/project/plugins.sbt deleted file mode 100644 index 2afe97a..0000000 --- a/docs/project/plugins.sbt +++ /dev/null @@ -1,2 +0,0 @@ -addSbtPlugin("com.dwijnand" % "sbt-dynver" % "4.0.0") -addSbtPlugin("io.cloudstate" % "sbt-cloudstate-paradox" % "0.1.2") diff --git a/docs/src/antora.yml b/docs/src/antora.yml new file mode 100644 index 0000000..3f6c7c3 --- /dev/null +++ b/docs/src/antora.yml @@ -0,0 +1,2 @@ +name: "" +version: master diff --git a/docs/src/main/paradox/api.md b/docs/src/main/paradox/api.md deleted file mode 100644 index 410f8d5..0000000 --- a/docs/src/main/paradox/api.md +++ /dev/null @@ -1,3 +0,0 @@ -# Go API docs - -The Go API docs can be found [here](https://pkg.go.dev/github.com/cloudstateio/go-support/cloudstate). \ No newline at end of file diff --git a/docs/src/main/paradox/eventsourced.md b/docs/src/main/paradox/eventsourced.md deleted file mode 100644 index 4b39456..0000000 --- a/docs/src/main/paradox/eventsourced.md +++ /dev/null @@ -1,118 +0,0 @@ -# Event sourcing - -This page documents how to implement Cloudstate event sourced entities in Go. For information on what Cloudstate event sourced entities are, please read the general @extref[Event sourcing](cloudstate:user/features/eventsourced.html) documentation first. - -An event sourced entity can be created by embedding the `cloudstate.EventEmitter` type and also implementing the `cloudstate.Entity` interface. - -@@snip [shoppingcart.go]($base$/tck/cmd/tck_shoppingcart/shoppingcart.go) { #entity-type } - -Then by composing the Cloudstate entity with a `cloudstate.EventSourcedEntity` and register it with `CloudState.Register()`, your entity gets configured to be an event sourced entity and handled by the Cloudstate instance from now on. - -@@snip [shoppingcart.go]($base$/cloudstate/eventsourced.go) { #event-sourced-entity-type } - -The `ServiceName` is the fully qualified name of the gRPC service that implements this entity's interface. Setting it is mandatory. - -The `PersistenceID` is used to namespace events in the journal, useful for when you share the same database between multiple entities. It is recommended to be the name for the entity type (in this case, `ShoppingCart`). Setting it is mandatory. - -The `SnapshotEvery` parameter controls how often snapshots are taken, so that the entity doesn't need to be recovered from the whole journal each time it's loaded. If left unset, it defaults to 100. Setting it to a negative number will result in snapshots never being taken. - -The `EntityFunc` is a factory method which generates a new Entity whenever Cloudstate has to initialize a new entity. - -## Persistence types and serialization - -Event sourced entities persist events and snapshots, and these need to be serialized when persisted. The most straightforward way to persist events and snapshots is to use protobufs. Cloudstate will automatically detect if an emitted event is a protobuf, and serialize it as such. For other serialization options, including JSON, see @extref:[Serialization](cloudstate:developer/language-support/serialization.html). - -While protobufs are the recommended format for persisting events, it is recommended that you do not persist your service's protobuf messages, rather, you should create new messages, even if they are identical to the service's. While this may introduce some overhead in needing to convert from one type to the other, the reason for doing this is that it will allow the service's public interface to evolve independently from its data storage format, which should be private. - -For our shopping cart example, we'll create a new file called `domain.proto`, the name domain is selected to indicate that these are my application's domain objects: - -@@snip [domain.proto]($base$/protobuf/example/shoppingcart/persistence/domain.proto) - -## State - -Each entity should store its state locally in a mutable variable, either a mutable field or a multiple structure such as an array type or slice. For our shopping cart, the state is a slice of products, so we'll create a slice of LineItems to contain that: - -@@snip [shoppingcart.go]($base$/tck/cmd/tck_shoppingcart/shoppingcart.go) { #entity-state } - -## Constructing - -The Cloudstate Go Support Library needs to know how to construct and initialize entities. For this, an entity has to provide a factory function, `EntityFunc`, which is set during registration of the event sourced entity. - -@@snip [shoppingcart.go]($base$/tck/cmd/tck_shoppingcart/shoppingcart.go) { #register } - -The entity factory function returns a `cloudstate.Entity` which is composed of two interfaces to handle commands and events. - -@@snip [shoppingcart.go]($base$/tck/cmd/tck_shoppingcart/shoppingcart.go) { #constructing } - -## Handling commands - -An event sourced entity implements the composed `cloudstate.Entity` interface. `cloudstate.Entity` embeds the `cloudstate.EventHandler` interface and therefore entities implementing it get commands from Cloudstate through the event handler's `HandleCommand` method. - -The command types received by an event sourced entity are declared by the gRPC Server interface which is generated from the protobuf definitions. The Cloudstate Go Support library together with the registered `cloudstate.EventSourcedEntity` is then able to dispatch commands it gets from the Cloudstate proxy to the event sourced entity. - -@@snip [shoppingcart.go]($base$/tck/cmd/tck_shoppingcart/shoppingcart.go) { #handle-command } - -The return type of the command handler is by definition of the service interface, the output type for the gRPC service call, this will be sent as the reply. - -The following shows the implementation of the `GetCart` command handler. This command handler is a read-only command handler, it doesn't emit any events, it just returns some state: - -@@snip [shoppingcart.go]($base$/tck/cmd/tck_shoppingcart/shoppingcart.go) { #get-cart } - -### Emitting events - -Commands that modify the state may do so by emitting events. - -@@@ warning -The **only** way a command handler may modify its state is by emitting an event. Any modifications made directly to the state from the command handler will not be persisted, and when the entity is passivated and next reloaded, those modifications will not be present. -@@@ - -A command handler may emit an event by using the embedded `cloudstate.EventEmitter` and invoking the `Emit` method on it. Calling `Emit` will immediately invoke the associated event handler for that event - this both validates that the event can be applied to the current state, as well as updates the state so that subsequent processing in the command handler can use it. - -Here's an example of a command handler that emits an event: - -@@snip [shoppingcart.go]($base$/tck/cmd/tck_shoppingcart/shoppingcart.go) { #add-item } - -This command handler also validates the command, ensuring the quantity of items added is greater than zero. Returning an `error` fails the command and the support library takes care of signaling that back to the requesting proxy as a `Failure` reply. - -## Handling events - -Event handlers are invoked at two points, when restoring entities from the journal, before any commands are handled, and each time a new event is emitted. An event handler's responsibility is to update the state of the entity according to the event. Event handlers are the only place where it's safe to mutate the state of the entity at all. - -Event handlers are declared by implementing the `cloudstate.EventHandler` interface. - -@@snip [shoppingcart.go]($base$/cloudstate/event.go) { #event-handler } - -Events emitted by command handlers get dispatched to the implemented event handler which then decides how to proceed with the event. - -@@snip [shoppingcart.go]($base$/tck/cmd/tck_shoppingcart/shoppingcart.go) { #handle-event } - -Here's an example of a concrete event handler for the `ItemAdded` event. - -@@snip [shoppingcart.go]($base$/tck/cmd/tck_shoppingcart/shoppingcart.go) { #item-added } - -## Producing and handling snapshots - -Snapshots are an important optimisation for event sourced entities that may contain many events, to ensure that they can be loaded quickly even when they have very long journals. To produce a snapshot, the `cloudstate.Snapshotter` interface has to be implemented that must return a snapshot of the current state in serializable form. - -@@snip [shoppingcart.go]($base$/cloudstate/event.go) { #snapshotter } - -Here is an example of the TCK shopping cart example creating snapshots for the current `domain.Cart` state of the shopping cart. - -@@snip [shoppingcart.go]($base$/tck/cmd/tck_shoppingcart/shoppingcart.go) { #snapshotter } - -When the entity is loaded again, the snapshot will first be loaded before any other events are received, and passed to a snapshot handler. Snapshot handlers are declared by implementing the `cloudstate.SnapshotHandler` interface. - -@@snip [shoppingcart.go]($base$/cloudstate/event.go) { #snapshot-handler } - -A snapshot handler then can type-switch over types the corresponding `cloudstate.Snapshotter` interface has implemented. - -@@snip [shoppingcart.go]($base$/tck/cmd/tck_shoppingcart/shoppingcart.go) { #handle-snapshot } - -## Registering the entity - -Once you've created your entity, you can register it with the `cloudstate.Cloudstate` server, by invoking the `Register` method of a Cloudstate instance. In addition to passing your entity type and service name, you also need to pass any descriptors that you use for persisting events, for example, the `domain.proto` descriptor. - -During registration the optional ServiceName and the ServiceVersion can be configured. -(TODO: give an example on how to pick values for these after the spec defines semantics ) - -@@snip [shoppingcart.go]($base$/tck/cmd/tck_shoppingcart/shoppingcart.go) { #register } diff --git a/docs/src/main/paradox/gettingstarted.md b/docs/src/main/paradox/gettingstarted.md deleted file mode 100644 index f5c019f..0000000 --- a/docs/src/main/paradox/gettingstarted.md +++ /dev/null @@ -1,78 +0,0 @@ -# Getting started with stateful services in Go - -## Prerequisites - -Go version -: Cloudstate Go support requires at least Go $cloudstate.go.version$ - -Build tool -: Cloudstate does not require any particular build tool, you can select your own. - -protoc -: Since Cloudstate is based on gRPC, you need a protoc compiler to compile gRPC protobuf descriptors. This can be done manually through the [Protocol Buffer Compiler project](https://github.com/protocolbuffers/protobuf#user-content-protocol-compiler-installation). - -docker -: Cloudstate runs in Kubernetes with [Docker](https://www.docker.com), hence you will need Docker to build a container that you can deploy to Kubernetes. Most popular build tools have plugins that assist in building Docker images. - -In addition to the above, you will need to install the Cloudstate Go support library by issuing `go get -u github.com/cloudstateio/go-support/cloudstate` or with Go module support let the dependency be downloaded by `go [build|run|test]`. - -By using the Go module support your go.mod file will reference the latest version of the support library or you can define which version you like to use. - -go get -: @@@vars -```text -go get -u github.com/cloudstateio/go-support/cloudstate -``` -@@@ - -import path -: @@@vars -```text -import "github.com/cloudstateio/go-support/cloudstate" -``` -@@@ - -go.mod -: @@@vars -``` -module example.com/yourpackage - require ( - github.com/cloudstateio/go-support $cloudstate.go.lib.version$ - ) -go $cloudstate.go.version$ -``` -@@@ - -## Protobuf files - -The Cloudstate Go Support Library provides no dedicated tool beside the protoc compiler to build your protobuf files. The Cloudstate protocol protobuf files as well as the shopping cart example application protobuf files are provided by the Cloudstate Repository. - -In addition to the `protoc` compiler, the gRPC Go plugin is needed to compile the protobuf file to `*.pb.go` files. Please follow the instructions at the [Go support for Protocol Buffers](https://github.com/golang/protobuf) project page to install the protoc compiler as well as the `protoc-gen-go` plugin which also includes the Google standard protobuf types. - -To build the example shopping cart application shown earlier in @extref:[gRPC descriptors](cloudstate:user/features/grpc.html), you could simply paste that protobuf into `protos/shoppingcart.proto`. You may wish to also define the Go package using the `go_package` proto option, to ensure the package name used conforms to Go package naming conventions. - -```proto -option go_package = "example/shoppingcart"; -``` - -Now if you place your protobuf files under `protobuf/` and run `protoc --go_out=. --proto_path=protobuf ./protobuf/*.proto`, you'll find your generated protobuf files in `example/shoppingcart`. - -## Creating a main package - -Your main package will be responsible for creating the Cloudstate gRPC server, registering the entities for it to serve, and starting it. To do this, you can use the CloudState server type, for example: - -@@snip [shoppingcart.go]($base$/tck/cmd/tck_shoppingcart/shoppingcart.go) { #shopping-cart-main } - -We will see more details on registering entities in the coming pages. - -## Interfaces to be implemented - -Cloudstate entities in Go work by implementing interfaces and composing types. - -To get support for the Cloudstate event emission the Cloudstate entity should embed the `cloudstate.EventEmitter` type. The EventEmitter allows the entity to emit events during the handling of commands. - -Second, during registration of the entity, an entity factory function has to be provided so Cloudstate gets to know how to create and initialize an event sourced entity. - -@@snip [shoppingcart.go]($base$/cloudstate/eventsourced.go) { #event-sourced-entity-func } - -This entity factory function returns a `cloudstate.Entity` which itself is a composite interface of a `cloudstate.CommandHandler` and a `cloudstate.EventHandler`. Every event sourced entity has to implement these two interfaces. diff --git a/docs/src/main/paradox/index.md b/docs/src/main/paradox/index.md deleted file mode 100644 index 13b7305..0000000 --- a/docs/src/main/paradox/index.md +++ /dev/null @@ -1,18 +0,0 @@ -# Cloudstate Go - -Cloudstate offers an idiomatic Go support library for writing stateful services. - -@@toc { depth=1 } - -@@@ index - -* [Getting started](gettingstarted.md) -* [Event sourcing](eventsourced.md) -* [Conflict-free Replicated Data Types](crdt.md) -* [Forwarding and effects](effects.md) -* [Serialization](serialization.md) -* [API docs](api.md) - -@@@ - -Link to @extref:[Cloudstate Documentation](cloudstate:index.html) diff --git a/docs/src/main/paradox/serialization.md b/docs/src/main/paradox/serialization.md deleted file mode 100644 index e884c47..0000000 --- a/docs/src/main/paradox/serialization.md +++ /dev/null @@ -1,37 +0,0 @@ -# Serialization - -Cloudstate functions serve gRPC interfaces, and naturally the input messages and output messages are protobuf messages that get serialized to the protobuf wire format. However, in addition to these messages, there are a number of places where Cloudstate needs to serialize other objects, for persistence and replication. This includes: - -* Event sourced @ref[events and snapshots](eventsourced.md#persistence-types-and-serialization). -* CRDT @ref[map keys and set elements](crdt.md), and @ref[LWWRegister values](crdt.md). - -Cloudstate supports a number of types and serialization options for these values. - -## Primitive types - -Cloudstate supports serializing the following primitive types: - -| Protobuf type | Go type | -|---------------|-------------| -| string | string | -| bytes | []byte | -| int32 | int32 | -| int64 | int64 | -| float | float32 | -| double | float64 | -| bool | bool | - -The details of how these are serialized can be found @extref[here](cloudstate:developer/language-support/serialization.html#primitive-values). - -@@@ note { title=Important } -Go has a set of [predeclared numeric](https://golang.org/ref/spec#Numeric_types) types with implementation-specific sizes. One of them is `int` which would be an int64 on 64-bit systems CPU architectures. Cloudstate does not support implicit conversion between an `int` and the corresponding `int64` as an input type for the serialization. The main reason not to support it is, that an `int` is not the same type as an `int64` and therefore a de-serialized value would have to be converted back to an `int` as it is of type `int64` during its serialized state. -@@@ - -## JSON - -Cloudstate uses the standard library package [`encoding/json`](https://golang.org/pkg/encoding/json/) to serialize JSON. Any type that has a field declared with a string literal tag ``json:"fieldname"`` will be serialized to and from JSON using the [Marshaller and Unmarshaller](https://golang.org/pkg/encoding/json/#Marshal) from the Go standard library package `encoding/json`. - -The details of how these are serialized can be found @extref[here](cloudstate:developer/language-support/serialization.html#json-values). - -Note that if you are using JSON values in CRDT sets or maps, the serialization of these values **must** be stable. This means you must not use maps or sets in your value, and you should define an explicit ordering for the fields in your objects. -**(TODO: mention the ordering of fields here by the Go standard library implementation).** diff --git a/docs/src/modules/go/nav.adoc b/docs/src/modules/go/nav.adoc new file mode 100644 index 0000000..8c9034b --- /dev/null +++ b/docs/src/modules/go/nav.adoc @@ -0,0 +1,7 @@ +* xref:index.adoc[Implementing in Go] +** xref:gettingstarted.adoc[Getting started] +** xref:eventsourced.adoc[Event sourcing] +** xref:crdt.adoc[Conflict-free Replicated Data Types] +** xref:effects.adoc[Forwarding and effects] +** xref:serialization.adoc[Serialization] +** xref:api.adoc[API docs] diff --git a/docs/src/modules/go/pages/api.adoc b/docs/src/modules/go/pages/api.adoc new file mode 100644 index 0000000..90c235c --- /dev/null +++ b/docs/src/modules/go/pages/api.adoc @@ -0,0 +1,3 @@ += Go API docs + +The Go API docs can be found https://pkg.go.dev/github.com/cloudstateio/go-support/cloudstate[here]. diff --git a/docs/src/main/paradox/crdt.md b/docs/src/modules/go/pages/crdt.adoc similarity index 82% rename from docs/src/main/paradox/crdt.md rename to docs/src/modules/go/pages/crdt.adoc index 5af6c3d..3729011 100644 --- a/docs/src/main/paradox/crdt.md +++ b/docs/src/modules/go/pages/crdt.adoc @@ -1,4 +1,4 @@ -# Conflict-free Replicated Data Types += Conflict-free Replicated Data Types * Explain how to use the CRDT API * Explain how to use CrdtFactory and where it comes from diff --git a/docs/src/main/paradox/effects.md b/docs/src/modules/go/pages/effects.adoc similarity index 83% rename from docs/src/main/paradox/effects.md rename to docs/src/modules/go/pages/effects.adoc index 5600c8a..afb58fb 100644 --- a/docs/src/main/paradox/effects.md +++ b/docs/src/modules/go/pages/effects.adoc @@ -1,4 +1,4 @@ -# Forwarding and effects += Forwarding and effects * Explain the ServiceCallFactory interface * Explain how to forward replies to another service. diff --git a/docs/src/modules/go/pages/eventsourced.adoc b/docs/src/modules/go/pages/eventsourced.adoc new file mode 100644 index 0000000..7880739 --- /dev/null +++ b/docs/src/modules/go/pages/eventsourced.adoc @@ -0,0 +1,190 @@ += Event sourcing + +This page documents how to implement Cloudstate event sourced entities in Go. +For information on what Cloudstate event sourced entities are, please read the general xref:concepts:eventsourced.adoc[Event sourcing] documentation first. + +An event sourced entity can be created by embedding the `cloudstate.EventEmitter` type and also implementing the `cloudstate.Entity` interface. + +[source,go] +---- +include::example$tck/cmd/tck_shoppingcart/shoppingcart.go[tag=entity-type] +---- + +Then by composing the Cloudstate entity with a `cloudstate.EventSourcedEntity` and register it with `CloudState.Register()`, your entity gets configured to be an event sourced entity and handled by the Cloudstate instance from now on. + +[source,go] +---- +include::example$cloudstate/eventsourced.go[tag=event-sourced-entity-type] +---- + +The `ServiceName` is the fully qualified name of the gRPC service that implements this entity's interface. +Setting it is mandatory. + +The `PersistenceID` is used to namespace events in the journal, useful for when you share the same database between multiple entities. +It is recommended to be the name for the entity type (in this case, `ShoppingCart`). +Setting it is mandatory. + +The `SnapshotEvery` parameter controls how often snapshots are taken, so that the entity doesn't need to be recovered from the whole journal each time it's loaded. +If left unset, it defaults to 100. +Setting it to a negative number will result in snapshots never being taken. + +The `EntityFunc` is a factory method which generates a new Entity whenever Cloudstate has to initialize a new entity. + +== Persistence types and serialization + +Event sourced entities persist events and snapshots, and these need to be serialized when persisted. +The most straightforward way to persist events and snapshots is to use protobufs. +Cloudstate will automatically detect if an emitted event is a protobuf, and serialize it as such. +For other serialization options, including JSON, see xref:contribute:serialization.adoc[Serialization]. + +While protobufs are the recommended format for persisting events, it is recommended that you do not persist your service's protobuf messages, rather, you should create new messages, even if they are identical to the service's. +While this may introduce some overhead in needing to convert from one type to the other, the reason for doing this is that it will allow the service's public interface to evolve independently from its data storage format, which should be private. + +For our shopping cart example, we'll create a new file called `domain.proto`, the name domain is selected to indicate that these are my application's domain objects: + +[source,protobuf] +---- +include::example$protobuf/example/shoppingcart/persistence/domain.proto[] +---- + +== State + +Each entity should store its state locally in a mutable variable, either a mutable field or a multiple structure such as an array type or slice. +For our shopping cart, the state is a slice of products, so we'll create a slice of LineItems to contain that: + +[source,go] +---- +include::example$tck/cmd/tck_shoppingcart/shoppingcart.go[tag=entity-state] +---- + +== Constructing + +The Cloudstate Go Support Library needs to know how to construct and initialize entities. +For this, an entity has to provide a factory function, `EntityFunc`, which is set during registration of the event sourced entity. + +[source,go] +---- +include::example$tck/cmd/tck_shoppingcart/shoppingcart.go[tag=register] +---- + +The entity factory function returns a `cloudstate.Entity` which is composed of two interfaces to handle commands and events. + +[source,go] +---- +include::example$tck/cmd/tck_shoppingcart/shoppingcart.go[tag=constructing] +---- + +== Handling commands + +An event sourced entity implements the composed `cloudstate.Entity` interface. +`cloudstate.Entity` embeds the `cloudstate.EventHandler` interface and therefore entities implementing it get commands from Cloudstate through the event handler's `HandleCommand` method. + +The command types received by an event sourced entity are declared by the gRPC Server interface which is generated from the protobuf definitions. +The Cloudstate Go Support library together with the registered `cloudstate.EventSourcedEntity` is then able to dispatch commands it gets from the Cloudstate proxy to the event sourced entity. + +[source,go] +---- +include::example$tck/cmd/tck_shoppingcart/shoppingcart.go[tag=handle-command] +---- + +The return type of the command handler is by definition of the service interface, the output type for the gRPC service call, this will be sent as the reply. + +The following shows the implementation of the `GetCart` command handler. +This command handler is a read-only command handler, it doesn't emit any events, it just returns some state: + +[source,go] +---- +include::example$tck/cmd/tck_shoppingcart/shoppingcart.go[tag=get-cart] +---- + +=== Emitting events + +Commands that modify the state may do so by emitting events. + +WARNING: The *only* way a command handler may modify its state is by emitting an event. +Any modifications made directly to the state from the command handler will not be persisted, and when the entity is passivated and next reloaded, those modifications will not be present. + +A command handler may emit an event by using the embedded `cloudstate.EventEmitter` and invoking the `Emit` method on it. +Calling `Emit` will immediately invoke the associated event handler for that event - this both validates that the event can be applied to the current state, as well as updates the state so that subsequent processing in the command handler can use it. + +Here's an example of a command handler that emits an event: + +[source,go] +---- +include::example$tck/cmd/tck_shoppingcart/shoppingcart.go[tag=add-item] +---- + +This command handler also validates the command, ensuring the quantity of items added is greater than zero. +Returning an `error` fails the command and the support library takes care of signaling that back to the requesting proxy as a `Failure` reply. + +== Handling events + +Event handlers are invoked at two points, when restoring entities from the journal, before any commands are handled, and each time a new event is emitted. +An event handler's responsibility is to update the state of the entity according to the event. +Event handlers are the only place where it's safe to mutate the state of the entity at all. + +Event handlers are declared by implementing the `cloudstate.EventHandler` interface. + +[source,go] +---- +include::example$cloudstate/event.go[tag=event-handler] +---- + +Events emitted by command handlers get dispatched to the implemented event handler which then decides how to proceed with the event. + +[source,go] +---- +include::example$tck/cmd/tck_shoppingcart/shoppingcart.go[tag=handle-event] +---- + +Here's an example of a concrete event handler for the `ItemAdded` event. + +[source,go] +---- +include::example$tck/cmd/tck_shoppingcart/shoppingcart.go[tag=item-added] +---- + +== Producing and handling snapshots + +Snapshots are an important optimisation for event sourced entities that may contain many events, to ensure that they can be loaded quickly even when they have very long journals. +To produce a snapshot, the `cloudstate.Snapshotter` interface has to be implemented that must return a snapshot of the current state in serializable form. + +[source,go] +---- +include::example$cloudstate/event.go[tag=snapshotter] +---- + +Here is an example of the TCK shopping cart example creating snapshots for the current `domain.Cart` state of the shopping cart. + +[source,go] +---- +include::example$tck/cmd/tck_shoppingcart/shoppingcart.go[tag=snapshotter] +---- + +When the entity is loaded again, the snapshot will first be loaded before any other events are received, and passed to a snapshot handler. +Snapshot handlers are declared by implementing the `cloudstate.SnapshotHandler` interface. + +[source,go] +---- +include::example$cloudstate/event.go[tag=snapshot-handler] +---- + +A snapshot handler then can type-switch over types the corresponding `cloudstate.Snapshotter` interface has implemented. + +[source,go] +---- +include::example$tck/cmd/tck_shoppingcart/shoppingcart.go[tag=handle-snapshot] +---- + +== Registering the entity + +Once you've created your entity, you can register it with the `cloudstate.Cloudstate` server, by invoking the `Register` method of a Cloudstate instance. +In addition to passing your entity type and service name, you also need to pass any descriptors that you use for persisting events, for example, the `domain.proto` descriptor. + +During registration the optional ServiceName and the ServiceVersion can be configured. +(TODO: give an example on how to pick values for these after the spec defines semantics ) + +[source,go] +---- +include::example$tck/cmd/tck_shoppingcart/shoppingcart.go[tag=register] +---- diff --git a/docs/src/modules/go/pages/gettingstarted.adoc b/docs/src/modules/go/pages/gettingstarted.adoc new file mode 100644 index 0000000..57a9b30 --- /dev/null +++ b/docs/src/modules/go/pages/gettingstarted.adoc @@ -0,0 +1,96 @@ += Getting started with Cloudstate services in Go + +include::partial$attributes.adoc[] +include::partial$include.adoc[] + +== Prerequisites + +Go version:: Cloudstate Go support requires at least Go {cloudstate-go-version} + +Build tool:: Cloudstate does not require any particular build tool, you can select your own. + +protoc:: +Since Cloudstate is based on gRPC, you need a protoc compiler to compile gRPC protobuf descriptors. +This can be done manually through the https://github.com/protocolbuffers/protobuf#user-content-protocol-compiler-installation[Protocol Buffer Compiler project]. + +docker:: +Cloudstate runs in Kubernetes with https://www.docker.com[Docker], hence you will need Docker to build a container that you can deploy to Kubernetes. +Most popular build tools have plugins that assist in building Docker images. + +In addition to the above, you will need to install the Cloudstate Go support library by issuing `go get -u github.com/cloudstateio/go-support/cloudstate` or with Go module support let the dependency be downloaded by `go [build|run|test]`. + +By using the Go module support your go.mod file will reference the latest version of the support library or you can define which version you like to use. + +[.tabset] +go get:: ++ +[source,shell"] +---- +go get -u github.com/cloudstateio/go-support/cloudstate +---- + +import path:: ++ +[source,go] +---- +import "github.com/cloudstateio/go-support/cloudstate" +---- + +go.mod:: ++ +[source,go,subs="attributes+"] +---- +module example.com/yourpackage + require ( + github.com/cloudstateio/go-support {cloudstate-go-lib-version} + ) +go {cloudstate-go-version} +---- + +== Protobuf files + +The Cloudstate Go Support Library provides no dedicated tool beside the protoc compiler to build your protobuf files. +The Cloudstate protocol protobuf files as well as the shopping cart example application protobuf files are provided by the Cloudstate Repository. + +In addition to the `protoc` compiler, the gRPC Go plugin is needed to compile the protobuf file to `*.pb.go` files. +Please follow the instructions at the https://github.com/golang/protobuf[Go support for Protocol Buffers] project page to install the protoc compiler as well as the `protoc-gen-go` plugin which also includes the Google standard protobuf types. + +To build the example shopping cart application shown earlier in xref:concepts:grpc.adoc[gRPC descriptors], you could simply paste that protobuf into `protos/shoppingcart.proto`. +You may wish to also define the Go package using the `go_package` proto option, to ensure the package name used conforms to Go package naming conventions. + +[source,protobuf] +---- +option go_package = "example/shoppingcart"; +---- + +Now if you place your protobuf files under `protobuf/` and run `protoc --go_out=. +--proto_path=protobuf ./protobuf/*.proto`, you'll find your generated protobuf files in `example/shoppingcart`. + +== Creating a main package + +Your main package will be responsible for creating the Cloudstate gRPC server, registering the entities for it to serve, and starting it. +To do this, you can use the CloudState server type, for example: + +[source,go] +---- +include::example$tck/cmd/tck_shoppingcart/shoppingcart.go[tag=shopping-cart-main] +---- + +We will see more details on registering entities in the coming pages. + +== Interfaces to be implemented + +Cloudstate entities in Go work by implementing interfaces and composing types. + +To get support for the Cloudstate event emission the Cloudstate entity should embed the `cloudstate.EventEmitter` type. +The EventEmitter allows the entity to emit events during the handling of commands. + +Second, during registration of the entity, an entity factory function has to be provided so Cloudstate gets to know how to create and initialize an event sourced entity. + +[source,go] +---- +include::example$cloudstate/eventsourced.go[tag=event-sourced-entity-func] +---- + +This entity factory function returns a `cloudstate.Entity` which itself is a composite interface of a `cloudstate.CommandHandler` and a `cloudstate.EventHandler`. +Every event sourced entity has to implement these two interfaces. diff --git a/docs/src/modules/go/pages/index.adoc b/docs/src/modules/go/pages/index.adoc new file mode 100644 index 0000000..0ed9d13 --- /dev/null +++ b/docs/src/modules/go/pages/index.adoc @@ -0,0 +1,3 @@ += Cloudstate Go + +Cloudstate offers an idiomatic Go support library for writing Cloudstate services. diff --git a/docs/src/modules/go/pages/serialization.adoc b/docs/src/modules/go/pages/serialization.adoc new file mode 100644 index 0000000..a991612 --- /dev/null +++ b/docs/src/modules/go/pages/serialization.adoc @@ -0,0 +1,58 @@ += Serialization + +Cloudstate functions serve gRPC interfaces, and naturally the input messages and output messages are protobuf messages that get serialized to the protobuf wire format. +However, in addition to these messages, there are a number of places where Cloudstate needs to serialize other objects, for persistence and replication. +This includes: + +* Event sourced xref:eventsourced.adoc#persistence-types-and-serialization[events and snapshots]. +* CRDT xref:crdt.adoc[map keys and set elements], and xref:crdt.adoc[LWWRegister values]. + +Cloudstate supports a number of types and serialization options for these values. + +== Primitive types + +Cloudstate supports serializing the following primitive types: + +|=== +| Protobuf type | Go type + +| string +| string + +| bytes +| []byte + +| int32 +| int32 + +| int64 +| int64 + +| float +| float32 + +| double +| float64 + +| bool +| bool +|=== + +The details of how these are serialized can be found xref:contribute:serialization.adoc#primitive-values[here]. + +IMPORTANT: Go has a set of https://golang.org/ref/spec#Numeric_types[predeclared numeric] types with implementation-specific sizes. +One of them is `int` which would be an int64 on 64-bit systems CPU architectures. +Cloudstate does not support implicit conversion between an `int` and the corresponding `int64` as an input type for the serialization. +The main reason not to support it is, that an `int` is not the same type as an `int64` and therefore a de-serialized value would have to be converted back to an `int` as it is of type `int64` during its serialized state. + +== JSON + +Cloudstate uses the standard library package https://golang.org/pkg/encoding/json/[`encoding/json`] to serialize JSON. +Any type that has a field declared with a string literal tag `json:"fieldname"` will be serialized to and from JSON using the https://golang.org/pkg/encoding/json/#Marshal[Marshaller and Unmarshaller] from the Go standard library package `encoding/json`. + +The details of how these are serialized can be found xref:contribute:serialization.adoc#json-values[here]. + +Note that if you are using JSON values in CRDT sets or maps, the serialization of these values *must* be stable. +This means you must not use maps or sets in your value, and you should define an explicit ordering for the fields in your objects. + +// TODO: mention the ordering of fields here by the Go standard library implementation diff --git a/docs/src/modules/go/partials/include.adoc b/docs/src/modules/go/partials/include.adoc new file mode 100644 index 0000000..982d3cf --- /dev/null +++ b/docs/src/modules/go/partials/include.adoc @@ -0,0 +1 @@ +:cloudstate-go-version: 1.14 diff --git a/tck/cmd/tck_shoppingcart/shoppingcart.go b/tck/cmd/tck_shoppingcart/shoppingcart.go index 44270d6..9e5122a 100644 --- a/tck/cmd/tck_shoppingcart/shoppingcart.go +++ b/tck/cmd/tck_shoppingcart/shoppingcart.go @@ -30,7 +30,7 @@ import ( // main creates a CloudState instance and registers the ShoppingCart // as a event sourced entity. -//#shopping-cart-main +// tag::shopping-cart-main[] func main() { server, err := cloudstate.New(cloudstate.Config{ ServiceName: "shopping-cart", @@ -39,7 +39,7 @@ func main() { if err != nil { log.Fatalf("CloudState.New failed: %v", err) } - //#register + // tag::register[] err = server.RegisterEventSourcedEntity( &cloudstate.EventSourcedEntity{ ServiceName: "com.example.shoppingcart.ShoppingCart", @@ -50,7 +50,7 @@ func main() { Service: "shoppingcart/shoppingcart.proto", }.AddDomainDescriptor("domain.proto"), ) - //#register + // end::register[] if err != nil { log.Fatalf("CloudState failed to register entity: %v", err) } @@ -60,25 +60,25 @@ func main() { } } -//#shopping-cart-main +// end::shopping-cart-main[] // A CloudState event sourced entity. -//#entity-type -//#compose-entity +// tag::entity-type[] +// tag::compose-entity[] type ShoppingCart struct { // our domain object - //#entity-state + // tag::entity-state[] cart []*domain.LineItem - //#entity-state + // end::entity-state[] // as an Emitter we can emit events cloudstate.EventEmitter } -//#compose-entity -//#entity-type +// end::compose-entity[] +// end::entity-type[] // NewShoppingCart returns a new and initialized instance of the ShoppingCart entity. -//#constructing +// tag::constructing[] func NewShoppingCart() cloudstate.Entity { return &ShoppingCart{ cart: make([]*domain.LineItem, 0), @@ -86,10 +86,10 @@ func NewShoppingCart() cloudstate.Entity { } } -//#constructing +// end::constructing[] // ItemAdded is a event handler function for the ItemAdded event. -//#item-added +// tag::item-added[] func (sc *ShoppingCart) ItemAdded(added *domain.ItemAdded) error { // TODO: enable handling for values if item, _ := sc.find(added.Item.ProductId); item != nil { item.Quantity += added.Item.Quantity @@ -103,7 +103,7 @@ func (sc *ShoppingCart) ItemAdded(added *domain.ItemAdded) error { // TODO: enab return nil } -//#item-added +// end::item-added[] // ItemRemoved is a event handler function for the ItemRemoved event. func (sc *ShoppingCart) ItemRemoved(removed *domain.ItemRemoved) error { @@ -118,7 +118,7 @@ func (sc *ShoppingCart) ItemRemoved(removed *domain.ItemRemoved) error { // // returns handle set to true if we have handled the event // and any error that happened during the handling -//#handle-event +// tag::handle-event[] func (sc *ShoppingCart) HandleEvent(_ context.Context, event interface{}) (handled bool, err error) { switch e := event.(type) { case *domain.ItemAdded: @@ -130,10 +130,10 @@ func (sc *ShoppingCart) HandleEvent(_ context.Context, event interface{}) (handl } } -//#handle-event +// end::handle-event[] // AddItem implements the AddItem command handling of the shopping cart service. -//#add-item +// tag::add-item[] func (sc *ShoppingCart) AddItem(_ context.Context, li *shoppingcart.AddLineItem) (*empty.Empty, error) { if li.GetQuantity() <= 0 { return nil, fmt.Errorf("cannot add negative quantity of to item %s", li.GetProductId()) @@ -147,7 +147,7 @@ func (sc *ShoppingCart) AddItem(_ context.Context, li *shoppingcart.AddLineItem) return &empty.Empty{}, nil } -//#add-item +// end::add-item[] // RemoveItem implements the RemoveItem command handling of the shopping cart service. func (sc *ShoppingCart) RemoveItem(_ context.Context, li *shoppingcart.RemoveLineItem) (*empty.Empty, error) { @@ -159,7 +159,7 @@ func (sc *ShoppingCart) RemoveItem(_ context.Context, li *shoppingcart.RemoveLin } // GetCart implements the GetCart command handling of the shopping cart service. -//#get-cart +// tag::get-cart[] func (sc *ShoppingCart) GetCart(_ context.Context, _ *shoppingcart.GetShoppingCart) (*shoppingcart.Cart, error) { cart := &shoppingcart.Cart{} for _, item := range sc.cart { @@ -172,9 +172,9 @@ func (sc *ShoppingCart) GetCart(_ context.Context, _ *shoppingcart.GetShoppingCa return cart, nil } -//#get-cart +// end::get-cart[] -//#handle-command +// tag::handle-command[] func (sc *ShoppingCart) HandleCommand(ctx context.Context, command interface{}) (handled bool, reply interface{}, err error) { switch cmd := command.(type) { case *shoppingcart.GetShoppingCart: @@ -191,18 +191,18 @@ func (sc *ShoppingCart) HandleCommand(ctx context.Context, command interface{}) } } -//#handle-command +// end::handle-command[] -//#snapshotter +// tag::snapshotter[] func (sc *ShoppingCart) Snapshot() (snapshot interface{}, err error) { return domain.Cart{ Items: append(make([]*domain.LineItem, len(sc.cart)), sc.cart...), }, nil } -//#snapshotter +// end::snapshotter[] -//#handle-snapshot +// tag::handle-snapshot[] func (sc *ShoppingCart) HandleSnapshot(snapshot interface{}) (handled bool, err error) { switch value := snapshot.(type) { case domain.Cart: @@ -213,7 +213,7 @@ func (sc *ShoppingCart) HandleSnapshot(snapshot interface{}) (handled bool, err } } -//#handle-snapshot +// end::handle-snapshot[] // find finds a product in the shopping cart by productId and returns it as a LineItem. func (sc *ShoppingCart) find(productId string) (item *domain.LineItem, index int) {