Transport
In order to execute choreographies, we need to be able to send messages between locations. ChoRus provides a trait Transport
that abstracts over message transports.
Built-in Transports
ChoRus provides two built-in transports: local
and http
.
The Local Transport
The local
transport is used to execute choreographies on the same machine on different threads. This is useful for testing and prototyping.
To use the local transport, we first need to create a LocalTransportChannel
, which works as a channel between threads and allows them to send messages to each other. To do so, we use the LocalTransportChannelBuilder
struct from the chorus_lib
crate.
#![allow(unused)] fn main() { extern crate chorus_lib; use chorus_lib::core::{ChoreographyLocation, LocationSet}; #[derive(ChoreographyLocation)] struct Alice; #[derive(ChoreographyLocation)] struct Bob; use chorus_lib::transport::local::LocalTransportChannelBuilder; let transport_channel = LocalTransportChannelBuilder::new() .with(Alice) .with(Bob) .build(); }
Using the with
method, we add locations to the channel. When we call build
, it will create an instance of LocalTransportChannel
.
Then, create a transport by using the LocalTransport::new
function, which takes a target location (explained in the Projector section) and the LocalTransportChannel
.
#![allow(unused)] fn main() { extern crate chorus_lib; use chorus_lib::core::{ChoreographyLocation, LocationSet}; #[derive(ChoreographyLocation)] struct Alice; use chorus_lib::transport::local::LocalTransportChannelBuilder; let transport_channel = LocalTransportChannelBuilder::new().with(Alice).build(); use chorus_lib::transport::local::{LocalTransport}; let alice_transport = LocalTransport::new(Alice, transport_channel.clone()); }
Because of the nature of the Local
transport, you must use the same LocalTransportChannel
instance for all locations. You can clone
the LocalTransportChannel
instance and pass it to each Projector::new
constructor.
#![allow(unused)] fn main() { extern crate chorus_lib; use chorus_lib::transport::local::{LocalTransport, LocalTransportChannelBuilder}; use std::thread; use chorus_lib::core::{ChoreographyLocation, ChoreoOp, Choreography, Projector, LocationSet}; #[derive(ChoreographyLocation)] struct Alice; #[derive(ChoreographyLocation)] struct Bob; struct HelloWorldChoreography; impl Choreography for HelloWorldChoreography { type L = LocationSet!(Alice, Bob); fn run(self, op: &impl ChoreoOp<Self::L>) { } } let transport_channel = LocalTransportChannelBuilder::new() .with(Alice) .with(Bob) .build(); let mut handles: Vec<thread::JoinHandle<()>> = Vec::new(); { // create a transport for Alice let transport = LocalTransport::new(Alice, transport_channel.clone()); handles.push(thread::spawn(move || { let p = Projector::new(Alice, transport); p.epp_and_run(HelloWorldChoreography); })); } { // create another for Bob let transport = LocalTransport::new(Bob, transport_channel.clone()); handles.push(thread::spawn(move || { let p = Projector::new(Bob, transport); p.epp_and_run(HelloWorldChoreography); })); } }
The HTTP Transport
The http
transport is used to execute choreographies on different machines. This is useful for executing choreographies in a distributed system.
To use the http
transport, import HttpTransport
and HttpTransportConfigBuilder
from the chorus_lib
crate.
#![allow(unused)] fn main() { extern crate chorus_lib; use chorus_lib::transport::http::{HttpTransport, HttpTransportConfigBuilder}; }
We need to construct a HttpTransportConfig
using the HttpTransportConfigBuilder
. First, we specify the target location and the hostname and port to listen on using the for_target
method. Then, we specify the other locations and their (hostname, port)
pairs using the with
method.
#![allow(unused)] fn main() { extern crate chorus_lib; use chorus_lib::core::{ChoreoOp, Choreography, ChoreographyLocation, Projector, Located, MultiplyLocated, Runner, LocationSet, Serialize, Deserialize}; use chorus_lib::transport::local::{LocalTransport, LocalTransportChannelBuilder}; #[derive(ChoreographyLocation)] struct Alice; #[derive(ChoreographyLocation)] struct Bob; #[derive(ChoreographyLocation)] struct Carol; let transport_channel = LocalTransportChannelBuilder::new().with(Alice).with(Bob).with(Carol).build(); let alice_transport = LocalTransport::new(Alice, transport_channel.clone()); let bob_transport = LocalTransport::new(Bob, transport_channel.clone()); let carol_transport = LocalTransport::new(Carol, transport_channel.clone()); use chorus_lib::transport::http::{HttpTransport, HttpTransportConfigBuilder}; // `Alice` listens on port 8080 on localhost let config = HttpTransportConfigBuilder::for_target(Alice, ("localhost", 8080)) // Connect to `Bob` on port 8081 on localhost .with(Bob, ("localhost", 8081)) .build(); let transport = HttpTransport::new(config); }
In the above example, the transport will start the HTTP server on port 8080 on localhost. If Alice needs to send a message to Bob, it will use http://localhost:8081
as the destination.
Creating a Custom Transport
You can also create your own transport by implementing the Transport
trait. It might be helpful to first build a TransportConfig
to have the the information that you need for each ChoreographyLocation
, and then have a constructor that takes the TransportConfig
and builds the Transport
based on it. While the syntax is similar to HttpTransportConfig
, which is HttpTransportConfigBuilder::for_target(target_location, target_information)
, chained with information about other locations using the .with(other_location, other_location_information)
, the type of information for each ChoreographyLocation
might diverge from the (host_name, port)
format presented in HttpTransport
. In some cases, the target_information
could even have a different type than the following other_location_information
types. But all the other_location_information
s should have the same type.
#![allow(unused)] fn main() { extern crate chorus_lib; use chorus_lib::core::{ChoreoOp, Choreography, ChoreographyLocation, Projector, Located, MultiplyLocated, Runner, LocationSet, Serialize, Deserialize}; use chorus_lib::transport::local::{LocalTransport, LocalTransportChannelBuilder}; #[derive(ChoreographyLocation)] struct Alice; #[derive(ChoreographyLocation)] struct Bob; #[derive(ChoreographyLocation)] struct Carol; let transport_channel = LocalTransportChannelBuilder::new().with(Alice).with(Bob).with(Carol).build(); let alice_transport = LocalTransport::new(Alice, transport_channel.clone()); let bob_transport = LocalTransport::new(Bob, transport_channel.clone()); let carol_transport = LocalTransport::new(Carol, transport_channel.clone()); use chorus_lib::transport::TransportConfigBuilder; let config = TransportConfigBuilder::for_target(Alice, ()) .with(Bob, ("localhost", 8081)) .with(Carol, ("localhost", 8082)) .build(); }
See the API documentation for more details.
Note on the location set of the Choreography
Note that when calling epp_and_run
on a Projector
, you will get a compile error if the location set of the Choreography
is not a subset of the location set of the Transport
. In other words, the Transport
should have information about every ChoreographyLocation
that Choreography
can talk about. So this will fail:
#![allow(unused)] fn main() { extern crate chorus_lib; use chorus_lib::transport::local::{LocalTransport, LocalTransportChannelBuilder}; use chorus_lib::core::{ChoreographyLocation, Projector, Choreography, ChoreoOp, LocationSet}; #[derive(ChoreographyLocation)] struct Alice; #[derive(ChoreographyLocation)] struct Bob; struct HelloWorldChoreography; impl Choreography for HelloWorldChoreography { type L = LocationSet!(Alice, Bob); fn run(self, op: &impl ChoreoOp<Self::L>) { } } let transport_channel = LocalTransportChannelBuilder::new().with(Alice).build(); let transport = LocalTransport::new(Alice, transport_channel.clone()); let projector = Projector::new(Alice, transport); projector.epp_and_run(HelloWorldChoreography); }