Echo Service Design
Summary
This document presents the design for the echo service. This service is a simple service that can be used for testing a circuit between nodes. An echo service sends messages to its peers at a set frequency.
Guide-level explanation
When a splinter circuit is created with an echo service the echo service will send messages to its peer services at the set frequency as well as simulate errors at a set rate. When an echo service receives a new message from a peer it will send a response back. Echo services send simple messages that contain a string and an ID unique to the message.
Echo Arguments
Echo service takes four arguments that determine how the service will act.
- 
peers- This is a list of the services that echo service will attempt to send messages to
- 
frequency- This is the amount of time, given in seconds, that an echo service will wait between sending messages
- 
jitter- This is an amount of time, given in seconds, that is used to generate a random number in the range [-jitter, jitter]. A new value is generated each time a message is sent and added to the frequency value to ensure that the amount of time between each message varies.
- 
error_rate- This is the number of errors per second, given as a float, that should be emulated by the echo service.
These arguments are set using the splinter CLI when creating a circuit.
Service API
Echo service implements the new service API in splinter. The principle traits
included in the new service API and implemented in echo service are:
Lifecycle, MessageHandler, TimerFilter, TimerHandler. Other required
traits include the converters, MessageConverter and ArgumentsConverter, and
the factories, MessageHandlerFactory and TimerHandlerFactory.
Reference-level explanation
EchoTimerFilter
The EchoTimerFilter contains an echo store factory and implements the
TimerFilter trait.
The filter method returns the list of service IDs of services that need to be
handled. A service needs to be handled if it has at least one peer service and
is in the EchoServiceStatus::Finalized state.
pub struct EchoTimerFilter {
    store_factory: Box<dyn PooledEchoStoreFactory>,
}
impl EchoTimerFilter {
    pub fn new(store_factory: Box<dyn PooledEchoStoreFactory>) -> Self {
        Self { store_factory }
    }
}
impl TimerFilter for EchoTimerFilter {
    fn filter(&self) -> Result<Vec<FullyQualifiedServiceId>, InternalError> {
        …
    }
}
EchoArguments
The EchoArguments struct contains the information to be used by the
EchoTimerHandler to send EchoMessages. The peers field is a list of
ServiceIds that the echo service will attempt to send messages to. frequency
is the amount of time that an echo service should wait between sending messages.
jitter is used as a range [-jitter, jitter] to generate a random number which
will be added to the frequency to create variation in the wait time between
messages. error_rate, given as a float, is the number of errors per second
that should be emulated by the echo service.
pub struct EchoArguments {
    peers: Vec<ServiceId>,
    frequency: Duration,
    jitter: Duration,
    error_rate: f32,
}
EchoMessage
The EchoMessage enum represents the messages sent between echo services. The
enum has two variants Request and Response. A Request is the initial type
of message sent by an echo service and a Response is what is sent back to the
sender when a Request is received.
pub enum EchoMessage {
    Request {
        message: String,
        correlation_id: u64,
    },
    Response {
        message: String,
        correlation_id: u64,
    },
}
EchoTimerHandler
The EchoTimerHandler struct contains an EchoStore and a time stamp used to
determine when to simulate an error. EchoTimerHandler implements the
TimerHandler trait.
The handle_timer function in the implementation sends messages to a service’s
peers at a given rate, emulating errors periodically, based on the
EchoArguments set for that service.
pub struct EchoTimerHandler {
    store: Box<dyn EchoStore>,
    stamp: Instant,
}
impl EchoTimerHandler {
    pub fn new(store: Box<dyn EchoStore>, stamp: Instant) -> Self {
        EchoTimerHandler { store, stamp }
    }
}
impl TimerHandler for EchoTimerHandler {
    type Message = EchoMessage;
    fn handle_timer(
        &mut self,
        sender: &dyn MessageSender<Self::Message>,
        service: FullyQualifiedServiceId,
    ) -> Result<(), InternalError> {
        …
    }
}
EchoMessageHandler
The EchoMessageHandler struct contains an EchoStore and implements the
MessageHandler trait.
The handle_message function takes a sender, the service ID of the sending
service, the service ID of the receiving service and a message, as arguments.
Depending on the type of EchoMessage, either a response message will be sent
to the sender service or the database will be updated to reflect that a response
was received.
pub struct EchoMessageHandler {
    store: Box<dyn EchoStore>,
}
impl EchoMessageHandler {
    pub fn new(store: Box<dyn EchoStore>) -> Self {
        EchoMessageHandler { store }
    }
}
impl MessageHandler for EchoMessageHandler {
    type Message = EchoMessage;
    fn handle_message(
        &mut self,
        sender: &dyn MessageSender<Self::Message>,
        to_service: FullyQualifiedServiceId,
        from_service: FullyQualifiedServiceId,
        message: Self::Message,
    ) -> Result<(), InternalError> {
        …
    }
}
EchoLifecycle
The EchoLifecycle struct contains an EchoStoreFactory and implements the
Lifecycle trait.
Each of the methods in the Lifecycle implementation
returns a
StoreCommand
which when executed, will make the appropriate updates to the underlying
database via the EchoStore.
pub struct EchoLifecycle<K> {
    store_factory: Arc<dyn EchoStoreFactory<K>>,
}
impl<K> EchoLifecycle<K> {
    pub fn new(store_factory: Arc<dyn EchoStoreFactory<K>>) -> Self {
        EchoLifecycle { store_factory }
    }
}
impl<K> Lifecycle<K> for EchoLifecycle<K>
where
    K: 'static,
{
    type Arguments = EchoArguments;
    fn command_to_prepare(
        &self,
        service: FullyQualifiedServiceId,
        arguments: Self::Arguments,
    ) -> Result<Box<dyn StoreCommand<Context = K>>, InternalError> {
        …
    }
    fn command_to_finalize(
        &self,
        service: FullyQualifiedServiceId,
    ) -> Result<Box<dyn StoreCommand<Context = K>>, InternalError> {
        …
    }
    fn command_to_retire(
        &self,
        service: FullyQualifiedServiceId,
    ) -> Result<Box<dyn StoreCommand<Context = K>>, InternalError> {
        …
    }
    fn command_to_purge(
        &self,
        service: FullyQualifiedServiceId,
    ) -> Result<Box<dyn StoreCommand<Context = K>>, InternalError> {
        …
    }
}
EchoMessageByteConverter
The EchoMessageByteConverter implements the
MessageConverter trait.
The to_left method converts bytes to an EchoMessage and the to_right
method does the inverse.
#[derive(Clone)]
pub struct EchoMessageByteConverter {}
impl MessageConverter<EchoMessage, Vec<u8>> for EchoMessageByteConverter {
    fn to_left(&self, right: Vec<u8>) -> Result<EchoMessage, InternalError> {
        …
    }
    fn to_right(&self, left: EchoMessage) -> Result<Vec<u8>, InternalError> {
        …
    }
}
EchoArgumentsVecConverter
The EchoArgumentsVecConverter implements the
ArgumentsConverter trait.
The to_left method converts a list of tuples to EchoArguments and the
to_right method does the inverse.
pub struct EchoArgumentsVecConverter {}
impl ArgumentsConverter<EchoArguments, Vec<(String, String)>> for EchoArgumentsVecConverter {
    fn to_right(&self, left: EchoArguments) -> Result<Vec<(String, String)>, InternalError> {
        …
    }
    fn to_left(&self, right: Vec<(String, String)>) -> Result<EchoArguments, InternalError> {
        …
    }
}
Prior art
This service implements the new service API design for splinter. For more information see: