RPC¶
Kamodo includes a Remote Call Procedure (RPC) interface, based on capnproto. This allows Kamodo objects to both serve and connect to other kamodo functions hosted on external servers.
We can test this functionality using docker compose (See the docker-compose.yaml
file in the base of this repo).
Our docker-compose configuration contains two services we can use to test this functionality:
- kamodo-rpc-py37 (runs kamodo-core with python 3.7)
- kamodo-rpc-py38 (runs kamodo-core with python 3.8)
Here are the corresponding definitions in docker-compose.yaml
:
kamodo-rpc-py37:
build:
context: .
dockerfile: dockerfiles/kamodo-rpc-py37.Dockerfile
volumes:
- type: bind
source: ${PWD}
target: /kamodo-core
ports:
- "60001:60000"
kamodo-rpc-py38:
build:
context: .
dockerfile: dockerfiles/kamodo-rpc-py38.Dockerfile
volumes:
- type: bind
source: ${PWD}
target: /kamodo-core
ports:
- "60000:60000"
command:
- kamodo-rpc
- rpc_conf=kamodo/rpc/kamodo_rpc_test.yaml
- host=0.0.0.0
- port='60000'
Either one will mount the base of the repo into the corresponding container at run time. Note that kamodo-rpc-py37
will use port 60001
.
Configuring a Kamodo server¶
To configure a Kamodo server, create a kamodo_rpc.yaml
file. A simple example is given in the root of the kamodo-core repo:
!Kamodo
f: x**2 - x -1
The !Kamodo
line informs our yaml parser that the next line defines a kamodo object with the following key-value pairs.
Note
The kamodo specification file is under rapid development
Starting an Kamodo server¶
To build a container compatible with python 3.7
, run
docker compose build kamodo-rpc-py37
This will pull the latest version of kamodo-core
and build it inside a container ready for deployment. Feel free to build on top of the resulting image.
Next, start the container with:
docker compose up kamodo-rpc-py37
Click to expand console output
[+] Running 1/0
⠿ Container kamodo-core-kamodo-rpc-py37-1 Created 0.0s
Attaching to kamodo-core-kamodo-rpc-py37-1
kamodo-core-kamodo-rpc-py37-1 | {'rpc_conf': 'kamodo/rpc/kamodo_rpc_test.yaml', 'host': '0.0.0.0', 'port': '60000'}
kamodo-core-kamodo-rpc-py37-1 | serving arg_units lhs rhs symbol units
kamodo-core-kamodo-rpc-py37-1 | f {} f x**2 - x - 1 f(x)
kamodo-core-kamodo-rpc-py37-1 | certfile not supplied
kamodo-core-kamodo-rpc-py37-1 | keyfile not supplied
kamodo-core-kamodo-rpc-py37-1 | using default certificate
kamodo-core-kamodo-rpc-py37-1 | Using selfsigned cert from: selfsigned.cert
kamodo-core-kamodo-rpc-py37-1 | [2022-05-04 22:23:23,543][rpc.proto][DEBUG] - Try IPv4
At startup, the container automatically generates an SSL key and certificate (when one is not already present).
Starting a client¶
Since the root of the repo is mapped into the container, we can access the cert from the host selfsigned.cert
.
Open a separate terminal and connect to the container from your host system (use the same directory where selfsigned.cert
i.e. the root of the mounted kamodo-core repo):
In [1]: from kamodo import KamodoClient
In [2]: k = KamodoClient(port=60001)
In [3]: k.f(5)
Out[3]: 19 # (5)**2-(5)-1
RPC Spec¶
Kamodo uses capnproto to communicate binary data between functions hosted on different systems. This avoids the need for json serialization and allows for server-side function pipelining while minimizing data transfers.
Kamodo's RPC specification file is located in kamodo/rpc/kamodo.capnp
:
@0xbfd16a03c247aaa9;
interface Kamodo {
struct Map(Key, Value) {
entries @0 :List(Entry);
struct Entry {
key @0 :Key;
value @1 :Value;
}
}
getFields @0 () -> (fields :Map(Text, Field));
getMath @1 () -> (math :Map(Text, Function));
evaluate @2 (expression: Expression) -> (value: Value);
interface Value {
# Wraps a numeric value in an RPC object. This allows the value
# to be used in subsequent evaluate() requests without the client
# waiting for the evaluate() that returns the Value to finish.
read @0 () -> (value :Literal);
# Read back the raw numeric value.
}
struct Expression {
# A numeric expression.
union {
literal @0 :Literal;
# A literal numeric value.
store @1 :Value;
# A value that was (or, will be) returned by a previous
# evaluate().
parameter @2 :UInt32;
# A parameter to the function (only valid in function bodies;
# see defFunction).
call :group {
# Call a function on a list of parameters.
function @3 :Function;
params @4 :List(Expression);
}
}
}
# Void: Void
# Boolean: Bool
# Integers: Int8, Int16, Int32, Int64
# Unsigned integers: UInt8, UInt16, UInt32, UInt64
# Floating-point: Float32, Float64
# Blobs: Text, Data
# Lists: List(T)
struct Literal {
union {
void @0 :Void;
bool @1 :Bool;
int8 @2 :Int8;
int16 @3 :Int16;
int32 @4 :Int32;
int64 @5 :Int64;
uint8 @6 :UInt8;
uint16 @7 :UInt16;
uint32 @8 :UInt32;
uint64 @9 :UInt64;
float32 @10 :Float32;
float64 @11 :Float64;
text @12 :Text;
data @13 :Data;
list @14 :List(Literal);
array @15 :Array;
int @16 :Text;
listint64 @17 :List(Int64);
listfloat64 @18 :List(Float64);
rational @19 :Rational;
}
}
struct Rational {
p @0 :Int64;
q @1 :Int64;
}
# everything needed for registration
struct Field {
func @0 :Function;
meta @1 :Meta;
data @2 :Array;
}
# match kamodo's meta attribute
struct Meta {
units @0 :Text;
argUnits @1 :Map(Text, Text);
citation @2 :Text;
equation @3 :Text; # latex expression
hiddenArgs @4 :List(Text);
}
struct Argument {
name @0 :Text;
value @1 :Literal;
}
# needs to be an interface
struct Array {
data @0 :Data;
shape @1 :List(UInt32);
dtype @2 :Text;
}
interface Function {
# A pythonic function f(*args, **kwargs)
call @0 (args :List(Literal), kwargs :List(Argument)) -> (result: Literal);
getArgs @1 () -> (args :List(Text));
getKwargs @2 () -> (kwargs: List(Argument));
}
}
The above spec allows a Kamodo client (or server) to be implemented in many languages, including C++, C# (.NET Core), Erlang, Go, Haskell, JavaScript, OCaml, and Rust.
Further reading on capnproto may be found here:
- Overview
- Schema language
- RPC
- Python implementation - pycapnp