At present, the Swim server application can only be run using tokio
(it uses tokio timers and tokio::spawn
internally.) You will need to add the following dependencies to your application:
[dependencies]
swim = { { git = "https://github.com/swimos/swim-rust" }, features = ["server", "agent"] }
tokio = { version = "1.22", features = ["rt", "macros", "signal"]}
The enabled features for swim
are:
server
- Includes to server application itself.agent
- Includes the API and macros for defining Swim agents in Rust.
The enabled tokio
features are:
rt
- This provides a single threaded runtime to run the server. It is also possible to specifyrt-multi-thread
for the multi-threaded runtime.macros
- The provides the#[tokio::main]
attribute macro that we will use to define themain
method of our application.signal
- This allows us to attach a signal handler to shut down the application with Ctrl-C.
We will start by creating a server application with no agent routes defined:
use std::error::Error;
use tokio::signal::ctrl_c;
use swimos::server::{Server, ServerBuilder, ServerHandle};
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
let addr = "127.0.0.1:8080".parse()?;
let server = ServerBuilder::with_plane_name("My Server")
.set_bind_addr(addr)
.build()
.await?;
let (task, handle) = server.run();
let mut shutdown = Box::pin(async move {
ctrl_c()
.await
.expect("Failed to register interrupt handler.")
.await;
println!("Stopping server.");
handle.stop();
});
println!("Starting server.");
let (_, result) = tokio::join!(shutdown, task);
result?;
println!("Server stopped successfully.");
Ok(())
}
The ServerBuilder
allows us to specify the routes that the server will be able to handle and to modify the default
configuration parameters. In this case however, we call build
immediately creating a default, empty server. By
default, the server will bind to a random free port on all devices; here we request that it binds to port 8080
on the
local host.
Calling run
on the resulting server returns an async task that will run the server application and a handle that can
be used to stop it. We associate the handler for Ctrl-C we we can request that the server stops from the terminal.
Next we will need an agent and accompanying lifecycle to bind to a route in the server. To learn how to do this see
the guide on writing agents. For the purposes of this section, we wil assume that there is such an agent
type at agent::ExampleAgent
and agent::ExampleLifecycle
.
An agent routes is a URI path, potentially containing path variables of the form {name}
.
-
The route
"/node/a"
will match the exact path "/node/a" and binds no parameters. -
The route
"/node/{x}"
with match the paths:/node/a
withx = 'a'
./node/b
withx = 'b'
./node/name
withx = 'name'
.- etc
Hence, a path with no variables defines a route to a singleton agent and a path with one or more variables defines a route to a parameterized family of agents.
To bind our example agent as a singleton agent we would do the following:
use swimos::route::RoutePattern;
use swimos::agent::agent_model::AgentModel;
use self::agent::{ExampleAgent, ExampleLifecycle};
let route = RoutePattern::parse_str("/example")?;
let lc = ExampleLifecycle::default();
let model = AgentModel::new(ExampleAgent::default, lc.into_lifecycle());
let server = ServerBuilder::with_plane_name("My Server")
.set_bind_addr(addr)
.add_route(route, model)
.build()
.await?;
Similarly, if we wanted our agent to be used as a parameterized family, we could replace the pattern with:
let route = RoutePattern::parse_str("/example/{name}")?;
Each running agent has access to its specific URI using a special HandlerAction
, available to it through
the HandlerContext
for each event handler.
TODO
The swim server can support deflate compression it the web socket frames that it sends (where the client also supports it). This can be enabled with:
ServerBuilder::with_plane_name("My Server")
.add_deflate_support()
...
Alternatively, to specify custom configuration for the parameters:
use swimos::server::DeflateConfig;
let config: DeflateConfig = ...;
ServerBuilder::with_plane_name("My Server")
.configure_deflate_support(config)
...
By default, all agent instance are entirely transient. Stopping and restarting the application will reset the state of all agents. Similarly, an agent instance stopping and then restarting within a running process will also reset the state.
To persist agent state across restarts, enable the in-memory store:
ServerBuilder::with_plane_name("My Server")
.with_in_memory_store()
...
With the in-memory store, state will be lost when the process stops. To persist agent state to disk there
is another state implementation backed by rocksdb. This must be enabled with the optional feature rocks_store
on
the swim
dependency:
swim = { { git = "https://github.com/swimos/swim-rust" }, features = ["server", "agent", "rocks_store"] }
Persistence, implemented using a RocksDB database can then be enabled when constructing the server with:
ServerBuilder::with_plane_name("My Server")
.enable_rocks_store()
...
This will use a database in a temporary folder in your system's default location for temporary files. State will be kept for all agents if they stop and restart within a single process. However, if the server is entirely stopped, the state will be lost.
To support persistence between different executions of the server, instead pass a path when enabling the RocksDB store:
ServerBuilder::with_plane_name("My Server")
.set_rocks_store_path("/path/to/store")
...
The server has an introspection system that will run additional meta-agents which provide information about the lanes on running agents. This system is disabled by default and can be enabled by:
ServerBuilder::with_plane_name("My Server")
.enable_introspection()
...