I’m looking forward to seeing what you guys have come up with. In the meantime, I also want to share a sketch of the design we’ve been iterating on in the Rust implementation that may have some similarities.
High level points:
- Every component is a Service that exposes the same API so they can be chained together into a connector, a standalone STREAM receiver, etc (credit to Carl Lerche’s work on the Rust Tower framework)
- Services use deserialized ILP packets to minimize the number of times the packet is (de)serialized, and because most Services need to look at the data in the packets
-
Settlement messages do not have a separate
sendMoney
abstraction. They are either sent inside ILP packets or they are handled outside of the pipeline for ILP packets (for example, sent to a different HTTP URL path and handled directly by the settlement engine) (credit to @adrianhopebailie) -
Each Service defines a trait (like an interface) to the data store with specific functions it needs (like
get_routing_table
, oratomically_update_balances
). A data store could be in-memory, use a fast external system like Redis, or use a combination of database and pubsub technologies, and each store implementation would implement as many of the traits as possible. -
There is a common
AccountId
that all Services and data stores use (instead of the plugins separately managing account details)
Service interface
The Service is a thing with two methods. poll_ready
is used to determine whether the Service is ready to accept more requests. call
takes a Request
and asynchronously returns either an ILP Fulfill or Reject packet. A Request
is an ILP Prepare packet with a from
and to
attached.
“Servers”, such as a BTP or HTTP server, accept an instance of a Service
and call
it with the deserialized Request
. “Clients”, such as an outgoing HTTP client, implement this trait. “Middleware”, such as a balance updater, are passed a Service
instance and implement the trait. Middleware pass on Request
s by calling their inner Service
and may modify the Request or the response, or respond directly without calling the inner Service
.
pub type AccountId = u64;
pub struct Request {
pub from: Option<AccountId>,
pub to: Option<AccountId>,
pub prepare: Prepare,
}
pub trait Service {
type Future: Future<Item = Fulfill, Error = Reject> + Send + 'static;
fn poll_ready(&mut self) -> Poll<(), ()> {
Ok(Async::Ready(()))
}
fn call(&mut self, request: Request) -> Self::Future;
}
Note that @sentientwaffle and I are also looking into whether the Request
's Option
types can be replaced with traits so that the compiler can statically verify that Services are being chained together correctly (for example, a Service like an outgoing HTTP client should always be passed a to
account.
Connector flow
Notes:
- All of the Services are optional and they can be strung together in different configurations
- The
BTP Server
andHTTP Server
implementService
as a passthrough so they can be chained (credit to @sappenin) - The
BTP Server
andBTP Outgoing
Services use the same pool of open sockets. Incoming connections to the Server are added to the pool, and the pool may be instantiated with URLs to connect to - I was previously working with a model that involved branching services such that the
Router
would decide which service to pass theRequest
to. This got complicated and @sappenin suggested thinking of the Connector as a single chain of Services, such that each Service would decide for itself whether to pass on theRequest
or handle it itself. - If settlement messages are sent in ILP packets, there could be
Services
that handle those and update the balances in the data store. Alternatively, all such packets could be forwarded to a separate settlement engine.