View Source SpawnSdk (spawn_sdk v0.5.0-alpha.3)
Spawn Elixir SDK is the support library for the Spawn Actors System.
Spawn is a Stateful Serverless Platform for provide the multi-language Actor Model. For a broader understanding of Spawn please consult its official repository.
The advantage of the Elixir SDK over other SDKs is in Elixir's native ability to connect directly to an Erlang network. For this reason the Elixir SDK allows any valid Elixir application to be part of a Spawn network without the need for a sidecar attached.
installation
Installation
Available in Hex, the package can be installed
by adding spawn_sdk
and spawn_statestores_*
to your list of dependencies in mix.exs
:
def deps do
[
{:spawn_sdk, "~> 0.5.0"},
# You can uncomment one of those dependencies if you are going to use Persistent Actors
#{:spawn_statestores_mysql, "~> 0.5.0"},
#{:spawn_statestores_postgres, "~> 0.5.0"},
#{:spawn_statestores_mssql, "~> 0.5.0"},
#{:spawn_statestores_cockroachdb, "~> 0.5.0"},
#{:spawn_statestores_sqlite, "~> 0.5.0"},
]
end
deploy
Deploy
Following steps below you will have a valid Elixir application to use in a Spawn cluster. However, you will still need to generate a container image with your application so that you can use it together with the Spawn Operator for Kubernetes.
how-to-use
How to use
After creating an elixir application project create the protobuf files for your business domain. It is common practice to do this under the priv/ folder. Let's demonstrate an example:
syntax = "proto3";
package io.eigr.spawn.example;
message MyState {
int32 value = 1;
}
message MyBusinessMessage {
int32 value = 1;
}
It is important to try to separate the type of message that must be stored as the actors' state from the type of messages that will be exchanged between their actors' operations calls. In other words, the Actor's internal state is also represented as a protobuf type, and it is a good practice to separate these types of messages from the others in its business domain.
In the above case MyState
is the type protobuf that represents the state of the Actor that we will create later
while MyBusiness
is the type of message that we will send and receive from this Actor.
Now that we have defined our input and output types as Protobuf types we will need to compile these files to generate their respective Elixir modules. An example of how to do this can be found here
NOTE: You need to have installed the elixir plugin for protoc. More information on how to obtain and install the necessary tools can be found here here
Now that the protobuf types have been created we can proceed with the code. Example definition of an Actor:
defmodule SpawnSdkExample.Actors.MyActor do
use SpawnSdk.Actor,
name: "jose",
persistent: true,
state_type: Io.Eigr.Spawn.Example.MyState,
deactivate_timeout: 30_000,
snapshot_timeout: 2_000
require Logger
alias Io.Eigr.Spawn.Example.{MyState, MyBusinessMessage}
defact sum(
%MyBusinessMessage{value: value} = data,
%Context{state: state} = ctx
) do
Logger.info("Received Request: #{inspect(data)}. Context: #{inspect(ctx)}")
new_value = if is_nil(state), do: value, else: (state.value || 0) + value
%Value{}
|> Value.of(%MyBusinessMessage{value: new_value}, %MyState{value: new_value})
|> Value.reply!()
end
end
In this example we are creating an actor in an Named/Eager way ie it is a known actor at compile time. We can also create Unnamed Dyncamic/Lazy actors, that is, despite having its abstract behavior defined at compile time, a Lazy actor will only have a concrete instance when it is associated with an identifier/name at runtime. Below follows the same previous actor being defined as abstract.
defmodule SpawnSdkExample.Actors.AbstractActor do
use SpawnSdk.Actor,
abstract: true,
persistent: true,
state_type: Io.Eigr.Spawn.Example.MyState
require Logger
alias Io.Eigr.Spawn.Example.{MyState, MyBusinessMessage}
defact sum(
%MyBusinessMessage{value: value} = data,
%Context{state: state} = ctx
) do
Logger.info("Received Request: #{inspect(data)}. Context: #{inspect(ctx)}")
new_value = if is_nil(state), do: value, else: (state.value || 0) + value
%Value{}
|> Value.of(%MyBusinessMessage{value: new_value}, %MyState{value: new_value})
|> Value.reply!()
end
end
Notice that the only thing that has changed is the absence of the name argument definition and the abstract argument definition being set to true.
NOTE: Can Elixir programmers think in terms of named vs abstract actors as more or less known at startup vs dynamically supervised/registered? That is, defining your actors directly in the supervision tree or using a Dynamic Supervisor for that.
side-effects
Side Effects
Actors can also emit side effects to other Actors as part of their response. See an example:
defmodule SpawnSdkExample.Actors.AbstractActor do
use SpawnSdk.Actor,
abstract: true,
persistent: false,
state_type: Io.Eigr.Spawn.Example.MyState
require Logger
alias Io.Eigr.Spawn.Example.{MyState, MyBusinessMessage}
alias SpawnSdk.Flow.SideEffect
defact sum(%MyBusinessMessage{value: value} = data, %Context{state: state} = ctx) do
Logger.info("Received Request: #{inspect(data)}. Context: #{inspect(ctx)}")
new_value = if is_nil(state), do: value, else: (state.value || 0) + value
result = %MyBusinessMessage{value: new_value}
new_state = %MyState{value: new_value}
Value.of()
|> Value.value(result)
|> Value.state(new_state)
|> Value.effects(
# This returns a list of side effects. In this case containing only one effect. However, multiple effects can be chained together,
# just by calling the effect function as shown here.
# If only one effect is desired, you can also choose to use the to/3 function together with Value.effect().
# Example: Values.effect(SideEffect.to(name, func, payload))
SideEffect.of()
|> SideEffect.effect("joe", :sum, result)
)
|> Value.reply!()
end
end
In the example above we see that the Actor joe will receive a request as a side effect from the Actor who issued this effect.
Side effects do not interfere with an actor's request-response flow. They will "always" be processed asynchronously and any response sent back from the Actor receiving the effect will be ignored by the effector.
pipe-and-forward
Pipe and Forward
Actors can also route some commands to other actors as part of their response. See an example:
defmodule SpawnSdkExample.Actors.ForwardPipeActor do
use SpawnSdk.Actor,
name: "pipeforward",
abstract: false,
persistent: false,
state_type: Io.Eigr.Spawn.Example.MyState
require Logger
alias Io.Eigr.Spawn.Example.MyBusinessMessage
defact forward_example(%MyBusinessMessage{} = msg, _ctx) do
Logger.info("Received request with #{msg.value}")
Value.of()
|> Value.value(MyBusinessMessage.new(value: 999))
|> Value.forward(
Forward.to("second_actor", "sum_plus_one")
)
|> Value.void()
end
defact pipe_example(%MyBusinessMessage{} = msg, _ctx) do
Logger.info("Received request with #{msg.value}")
Value.of()
|> Value.value(MyBusinessMessage.new(value: 999))
|> Value.pipe(
Pipe.to("second_actor", "sum_plus_one")
)
|> Value.void()
end
end
defmodule SpawnSdkExample.Actors.SecondActorExample do
use SpawnSdk.Actor,
name: "second_actor",
abstract: false,
persistent: false,
state_type: Io.Eigr.Spawn.Example.MyState
require Logger
alias Io.Eigr.Spawn.Example.MyBusinessMessage
defact sum_plus_one(%MyBusinessMessage{} = msg, _ctx) do
Logger.info("Received request with #{msg.value}")
Value.of()
|> Value.value(MyBusinessMessage.new(value: msg.value + 1))
|> Value.void()
end
end
We are returning void in both examples so we dont care about what is being stored in the actor state.
In the case above, every time you call the forward_example
the second_actor's sum_plus_one
function will receive the value forwarded originally in the invocation as its input. The end result will be:
iex> SpawnSdk.invoke("pipeforward", system: "spawn-system", command: "forward_example", payload: %Io.Eigr.Spawn.Example.MyBusinessMessage{value: 1})
{:ok, %Io.Eigr.Spawn.Example.MyBusinessMessage{value: 2}}
For the Pipe example, the the second_actor's sum_plus_one
function will always receive %MyBusinessMessage{value: 999}
due to getting the value from the previous specification in the pipe_example
command, the end result will be:
iex> SpawnSdk.invoke("pipeforward", system: "spawn-system", command: "pipe_example", payload: %Io.Eigr.Spawn.Example.MyBusinessMessage{value: 1})
{:ok, %Io.Eigr.Spawn.Example.MyBusinessMessage{value: 1000}}
broadcast
Broadcast
Actors can also send messages to a group of actors at once as an action callback. See the example below:
defmodule Fleet.Actors.Driver do
use SpawnSdk.Actor,
abstract: true,
# Set ´driver´ channel for all actors of the same type (Fleet.Actors.Driver)
channel: "drivers",
state_type: Fleet.Domain.Driver
alias Fleet.Domain.{
Driver,
OfferRequest,
OfferResponse,
Point
}
require Logger
@brain_actor_channel "fleet-controllers"
defact update_position(%Point{} = position, %Context{state: %Driver{id: name} = driver} = ctx) do
Logger.info(
"Driver [#{name}] Received Update Position Event. Position: [#{inspect(position)}]. Context: #{inspect(ctx)}"
)
driver_state = %Driver{driver | position: position}
%Value{}
|> Value.of(driver_state, driver_state)
|> Value.broadcast(
Broadcast.to(
@brain_actor_channel,
"driver_position",
driver_state
)
)
|> Value.reply!()
end
end
In the case above, every time an Actor "driver" executes the update_position action it will send a message to all the actors participating in the channel called "fleet-controllers".
timers
Timers
Actors can also declare Actions that act recursively as timers. See an example below:
defmodule SpawnSdkExample.Actors.ClockActor do
use SpawnSdk.Actor,
name: "clock_actor",
state_type: Io.Eigr.Spawn.Example.MyState,
deactivate_timeout: 86_400_000
require Logger
alias Io.Eigr.Spawn.Example.MyState
@set_timer 15_000
defact clock(%Context{state: state} = ctx) do
Logger.info("[clock] Clock Actor Received Request. Context: #{inspect(ctx)}")
new_value = if is_nil(state), do: 0, else: state.value + 1
new_state = MyState.new(value: new_value)
Value.of()
|> Value.state(new_state)
|> Value.noreply!()
end
end
NOTE: Timers Actions are ephemeral and only exist while the Actor is Enabled, ie running. Therefore Timers are not persistent and will not reactivate a timer's Actor after it is deactivated. Note that in the example above we set the value of deactivate timeout to an exceptionally high number, this is done to make the Actor remain active.
In the example above the ´clock´ action will be called every 15 seconds.
declaring-the-supervision-tree
Declaring the supervision tree
Once we define our actors we can now declare our supervision tree:
defmodule SpawnSdkExample.Application do
@moduledoc false
use Application
@impl true
def start(_type, _args) do
children = [
{
SpawnSdk.System.Supervisor,
system: "spawn-system",
actors: [
SpawnSdkExample.Actors.MyActor,
SpawnSdkExample.Actors.AbstractActor
]
}
]
opts = [strategy: :one_for_one, name: SpawnSdkExample.Supervisor]
Supervisor.start_link(children, opts)
end
end
To run the application via iex we can use the following command:
MIX_ENV=prod USER_FUNCTION_PORT=8092 PROXY_DATABASE_TYPE=mysql SPAWN_STATESTORE_KEY=3Jnb0hZiHIzHTOih7t2cTEPEpY98Tu1wvQkPfq/XwqE= iex --name spawn_a2@127.0.0.1 -S mix
NOTE: This example uses the MySQL database as persistent storage for its actors. And it is also expected that you have previously created a database called eigr-functions-db in the MySQL instance.
The full example of this application can be found here.
client-api-examples
Client API Examples
To invoke Actors, use:
iex> SpawnSdk.invoke("joe", system: "spawn-system", command: "sum", payload: %Io.Eigr.Spawn.Example.MyBusinessMessage{value: 1})
{:ok, %Io.Eigr.Spawn.Example.MyBusinessMessage{value: 12}}
You can invoke actor default functions like "get" to get its current state
SpawnSdk.invoke("joe", system: "spawn-system", command: "get")
Spawning Actors:
iex> SpawnSdk.spawn_actor("robert", system: "spawn-system", actor: SpawnSdkExample.Actors.AbstractActor)
{:ok, %{"robert" => SpawnSdkExample.Actors.AbstractActor}}
Invoke Spawned Actors:
iex> SpawnSdk.invoke("robert", system: "spawn-system", command: "sum", payload: %Io.Eigr.Spawn.Example.MyBusinessMessage{value: 1})
{:ok, %Io.Eigr.Spawn.Example.MyBusinessMessage{value: 16}}
Invoke Actors in a lazy way without having to spawn them before:
iex> SpawnSdk.invoke("robert_lazy", ref: SpawnSdkExample.Actors.AbstractActor, system: "spawn-system", command: "sum", payload: %Io.Eigr.Spawn.Example.MyBusinessMessage{value: 1})
{:ok, %Io.Eigr.Spawn.Example.MyBusinessMessage{value: 1}}
Link to this section Summary
Link to this section Functions
Invokes a function for a actor_name
opts
Opts
system
this is requiredref
attribute attribute will always lookup to see if the referenced actor is already started or not.payload
attribute is optional.command
has default values that you can use to get current actor state- get, get_state, Get, getState, GetState
examples
Examples
iex> SpawnSdk.invoke(
"actor_name",
ref: SpawnSdkExample.Actors.AbstractActor,
system: "spawn-system",
command: "sum", # "sum" or :sum
payload: %Io.Eigr.Spawn.Example.MyBusinessMessage{value: 5}
)
{:ok, %Io.Eigr.Spawn.Example.MyBusinessMessage{value: 5}}
iex> SpawnSdk.invoke("actor_name", system: "spawn-system", command: "get")
{:ok, %Io.Eigr.Spawn.Example.MyBusinessMessage{value: 5}}
Spawns a abstract actor
A abstract actor means that you can spawn dynamically the same actor for multiple different names.
It is analog to DynamicSupervisor
opts
Opts
system
this is requiredactor
which actor you will register first argument to
examples
Examples
iex> SpawnSdk.spawn_actor(
"actor_name",
system: "spawn-system",
actor: SpawnSdkExample.Actors.AbstractActor
)