Skip to content

Instantly share code, notes, and snippets.

@kentonv
Last active December 19, 2015 02:08
Show Gist options
  • Save kentonv/5880714 to your computer and use it in GitHub Desktop.
Save kentonv/5880714 to your computer and use it in GitHub Desktop.
Cap'n Proto RPC protocol straw man
# Recall that Cap'n Proto RPC allows messages to contain references to remote objects that
# implement interfaces. These references are called "capabilities", because they both designate
# the remote object to use and confer permission to use it.
#
# Cap'n Proto RPC takes place over a connection -- any bi-directional stream. The protocol does
# not distinguish between client and server as both ends can receive capabilities from the other
# and subsequently make calls to those capabilities.
#
# Capabilities are, at least initially, tied to connections. If the connection dies, all
# capabilities received through it are lost. The ability to persist capabilities across
# connections will be introduced later.
#
# On initial connection, each end of the stream exports a default capability. This allows the two
# ends to begin talking. Typically, the "client" end of the stream will use a default capability
# that has no methods.
#
# This protocol is intended to be amenable to out-of-order delivery of frames and unreliable
# transports. Unreliable transports will need a mechanism to acknowledge each frame and retry sends
# until acknowledged.
message Frame {
# An RPC connection is a bi-directional stream of Frames. There is no distinction between client
# and server. However, documentation here sometimes uses the word "client" to mean "caller" or
# "capability holder" and the word "server" to mean "callee" or "capability endpoint" (i.e. the
# intuitive meanings in context).
#
# Frames may be delivered out-of-order.
#
# When the RPC system wants to send a Frame, it instructs the transport layer to keep trying to
# send the frame until the transport can guarantee that one of the following is true:
# 1) The Frame was received, possibly multiple times.
# 2) The sessing is broken, and no further Frames can be sent.
# 3) The RPC system has asked to stop trying to send the Frame (e.g. because the call was canceled
# or timed out).
#
# In case 1, the Frame may have been received multiple times. In cases 2 and 3, it may have been
# received any number of times, including zero. The RPC system must handle all of these cases
# with some modicum of grace. Moreover, applications should design their interfaces such that
# methods are idempotent.
union {
call :Call; # Begin a method call.
cancel :Cancel; # Cancel an outstanding call.
return :Return; # Complete a method call.
fail :Fail; # Signal that a call failed with an error/exception.
release :Release; # Release a capability so that the remote object can be deallocated.
}
}
message Call {
# Frame type initiating a method call on a capability.
callId :UInt32;
# A number, chosen by the caller, which identifies this call in future messages. This number
# must be different from all other calls originating from the same end of the connection (but
# may overlap with call IDs originating from the opposite end). A fine strategy is to use
# sequential call IDs, but the recipient should not assume this.
#
# TODO: Decide if it is safe to reuse a call ID. If not, extend to 64 bits.
union target {
capId :Object;
# This call is to a capability received previously over this connection.
defaultCap :Void;
# This call is to the default capability.
promise :PromisedCap;
# This call is to a capability that is expected to be returned by another call that has not
# yet been completed.
}
interfaceId :UInt64;
# The type ID of the interface being called. Each capability may implement multiple interfaces.
methodId :UInt16;
# The ordinal number of the method to call within the requested interface.
request :Object;
# The request struct. The fields of this struct correspond to the parameters of the method.
#
# The request may contain capabilities. These capabilities are automatically released when the
# call returns *unless* the Return frame explicitly indicates that they are being retained.
}
message PromisedCap {
# PromisedCap designates a capability which is expected to be returned by an in-flight call
# which has not yet completed. Using PromisedCap allows a sequence of dependent calls to be
# executed without waiting for network round-trips.
dependencyCallId :UInt32;
# ID of the call whose response is expected to contain the capability.
path :List(UInt16);
# Path to the capability in the response. This is a list of field ordinals, starting from the
# root of the response. If any element of the path is a list, the call will be applied to every
# element of the list. If any element is a union member, the call will only be applied if the
# union ends up containing that member. Thus the response to a pipeline call is always a List --
# even if no lists or unions are in the path, the response is a single-element list for
# consistency.
}
message Return {
# Frame type sent from callee to caller indicating that the call has completed.
callId :UInt32;
# Call ID as given in the corresponding Call.
response :Object;
# Response object. If the interface returns a struct, this is it. Otherwise, this points to
# a struct which contains exactly one field, corresponding to the interface's return type.
# (This implies that an interface's return type can be upgraded from a non-struct to a struct
# without breaking wire compatibility.)
#
# If the response contains any capabilities, the caller is expected to send a Release frame for
# each one when done with them.
retainedCaps :List(RetainedCap);
# List of capabilities from the request to which the callee continues to hold references. Any
# other capabilities from the request ase implicitly released.
message RetainedCap {
capId: Object;
}
}
message Fail {
# Frame type sent from callee to caller indicating that a call failed with an error/exception.
#
# All capabilities in the request are implicitly released if the call fails.
callId :UInt32;
# Call ID as given in the corresponding Call.
reason :Text;
# Human-readable failure description.
isPermanent :Bool;
# In the best estimate of the callee, is this error likely to repeat if the same call is executed
# again? Callers might use this to decide when to retry a request.
isOverloaded :Bool;
# In the best estimate of the callee, is it likely this error was caused by the system being
# overloaded? If so, the caller probably should not retry the request now, but may consider
# retrying it later.
nature :Nature;
# The nature of the failure. This is intended mostly to allow classification of errors for
# reporting and monitoring purposes -- the caller is not expected to handle different natures
# differently.
enum Nature {
# These correspond to kj::Exception::Nature.
precondition;
localBug;
osError;
networkFailure;
canceled;
other;
}
}
message Cancel {
# Frame type sent to indicate the caller whishes to cancel an outstanding call. This is a hint
# to the callee that the result of the call will be ignored, so it might as well stop processing
# now and return a Fail if it hasn't returned already. If a Cancel message is received for a
# callId that is not currently active, it should be ignored.
callId :UInt32;
# Call ID as given in the corresponding Call.
}
message Release {
# Frame type sent to indicate that the sender is done with the given capability and the receiver
# can free resources allocated to it.
capId :Object;
# ID of the capability to be released.
}
message CapDescriptor {
# An interface pointer is encoded the same as a struct pointer except that the least-significant
# two bits of the pointer are 1's instead of 0's. The pointer points to an instance of
# CapDescriptor. The runtime API should not reveal the CapDescriptor directly to the application,
# but should instead wrap them in some kind of callable object with methods corresponding to the
# expected interface.
capId :Object;
# ID of the capability, used to address it when making calls. This is an arbitrary object defined
# by the capability's origin and should be considered opaque.
#
# Since capabilities are tied to network connections, it is reasonable for the cap ID to simply
# contain an integer assigned sequentially from the start of the conneciton.
#
# Another strategy is to encode a persistent ID for the target object, or even encode the target
# object's entire state (if it is immutable), so that the origin does not need to maintain that
# state locally. This should be done with caution: a malicious caller could potentially
# manipulate the ID, so it is important to make sure it is encrypted or signed in some way.
needsRelease :Boolean;
# Whether or not the client is required to send a Release message to the server when done with
# this cap. This could be false in the case of stateless caps whose content is entirely encoded
# in the capId. It could also be false in the case of capabilities which are owned by someone
# else, and will be deleted when the owner feels like it (after which, calls to the capability
# will always fail). Note that if this is false then the cap should never appear in
# Return.retainedCaps; the interface should document whether the capability remains valid after
# return.
#
# Another way to think of this is that this bit indicates whether ownership of the capability was
# passed to the recipient, in the sense of pointer ownership as in C++.
#
# TODO: Perhaps ownership transfer should be defined statically in the interface rather than
# dynamically via this flag? Though some dynamic indicator is needed regardless for the sake
# of backward-compatibility.
interfaces :List(UInt64);
# Type IDs of interfaces supported by this descriptor. This must include at least the interface
# type as defined in the schema file, but could include others. The runtime API should allow
# the interface wrapper to be dynamically cast to these other types (probably not using the
# language's built-in cast syntax, but something equivalent).
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment