Last active
December 19, 2015 02:08
-
-
Save kentonv/5880714 to your computer and use it in GitHub Desktop.
Cap'n Proto RPC protocol straw man
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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