New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Design parameters ('epic issue') #1
Comments
As the one who is writing an explorative implementation of Spritely Goblins and one who has specified an msgpck captp schema, spent too much time digging around in the captp pages on erights.org and in its E on Java code base I have quite some opinions on these matters. What I have found out is that I do detest IDLs or other non-self descriptive formats like JOSS that make architectural (both design and ISA) and coding environment assumptions. (For instance capnproto struct datum pack ordering has yet to be specified in other form than template heavy c++ code.) Such IDL based systems just require too much 'hacktivation' energy to get started. Meanwhile writing a Syrup parser was stupidly simple and took about half a day. Which datum and data forms to support? Probably more thoughts and comments later. |
I wanted to add a couple notes from the E-flavored object-capability world. In Monte, we've experimented with several flavors of CapTP. AMP and JSON were sufficient to allow me to implement multiprocessing for Monte, over AMP. This suggests that we don't need to invent a new serialization format (and IDL, etc.) merely for transporting capabilities. I wish that I could say "let's use Capn Proto" and be done. However, it turns out that implementing a full Capn Proto subsystem is a lot of work! We've needed a That said, we do already have toolchain support for compiling Capn Proto IDL to Monte bytecode. So let's use Capn Proto and be done with wire details. |
Thanks @MostAwesomeDude. Can you confirm that “Let’s use Cap’n’Proto” implies you’re in favor of requiring an IDL? |
I agree we should not invent a wire format and that I’d like to frame the conversation around choosing a format that satisfies the rest of the requirements, when we have a firmer notion of which branch of the design space we’re wandering down. If we’re leaning binary, MsgPack or CBOR are likely sufficient. I like the Protobuf varint because it’s precision agnostic and can conceivably scale up to BigInt and allows for precision increases over schema migration, but these would-be-nice not necessary. I don’t know Cap’n’Proto’s wire protocol enough to judge what constraints it puts on JavaScript or Scheme idioms in particular, so at some point I hope to ask an expert how it (and any other binary protocol) would behave for specific edge cases. I also don’t know whether you can run Cap’n’Proto without an IDL. Agoric runs without IDL today, which is great for rapid iteration. |
I'd second @MostAwesomeDude's suggestion to just pick capnp for serialization and be done with it, and I'd take it one step further: let's use capnproto rpc as the basis for the protocol, and figure out what extensions are needed to do the things people want to do. Unfortunately, capnproto exists already, so if we do anything other than build something compatible with it, we end up with a world where there is more than one CapTP protocol in active use -- so I would like to thoroughly explore the idea of building the things people want on top of Cap'n Proto RPC, possibly with extensions where needed and feasible, before going ahead and building something incompatible. Maybe there will be true dealbreakers, but maybe not. The biggest advantage of this approach is that if we can make it work, we can potentially avoid gateways, which would segment the network when it comes to three-party handoff; they become bottlenecks through which all inter-protocol traffic must pass, which is unfortunate. If we can't make it work, we can potentially build gateways, but I suspect that we can. This leaves open a couple questions that I've thought about:
struct KV {
key @0 :Text;
value @1 :Value;
}
struct Value {
union {
number @0 :Float64;
string @1 :Text;
array @2 :List(Value);
null @3 :Void;
undefined @4 :Void;
object @5 :List(KV); # Unfortunately capnp doesn't have a built-in map type, but we can layer semantics on top for that.
function @6 :Function;
# ...
}
}
interface Function {
call @0(args :List(Value)) -> (result :Value);
} ...and the libraries can hide the IDL from their users, embedding js types in capnproto like the above, but full-capnproto implementations can still use this schema to talk to programs using the extra js layer.
interface JsPipeline {
getProperty @0 (name :Text) -> (value :Value);
# ...
} If the remote vat doesn't understand the method, it will throw an exception with type = unimplemented, in which case the operation can just be performed locally when the promise resolved. This allows us to experiment with all sorts of possible operators, without needing to graft each one on to the protocol in an ad-hoc way. This still leaves open the question of what I'm sure there are various other considerations I haven't thought of. |
If I may recap @zenhart, I believe you are proposing the Agoric and Goblins communicate using a Cap’n Proto meta-IDL. Other languages would codegen a client from this meta-IDL in order to interact with Agoric and Goblins vats and would have the option of communicating amongst themselves with their own hand-rolled IDL’s. The hand-rolled IDL’s provide a superior developer experience for languages using generated clients, since the IDL maps more directly to language idioms. This creates an incentive for such implementations to provide both their own IDL and a meta-IDL bridge. These are effectively hand-rolled gateways embedded in services. Protobuf 3 similarly provides a JSON equivalence, but the conversion is well-defined and not hand-rolled. Do we believe that it would be possible for Agoric/Goblin intercommunication to occur without an IDL, using a partially self-describing wire protocol, that can be fully-described for the purpose of generated clients not built upon Agoric or Goblins? |
I want to summon @kentonv, who can steelman my claims (and more likely will remind me how wrong I am!) I have three points to address: Whether the IDL is required to traverse buffers, whether dynamic languages can quickly adopt an interface with the IDL,, and whether Goblin, Agoric, Monte, etc. could talk with clients in other languages. It's possible to traverse Capn Proto buffers without an IDL. The resulting data structure is a tree. (Technically, it can be a cyclic graph, but @zenhack or @kentonv would ask implementations to not do that. Monte doesn't mind cyclic structures though.) The tree has full type information WRT the buffer's layout; everything can be pulled out and read. However, the names of unions and enums are missing; it's all numbers. I've done this before, because writing a Capn Proto implementation requires hand-parsing buffers in a bootstrap. After the necessary support library is factored out, the bootstrap module is pretty small, but it was a difficult time. For JS, there's an official upstream Node.js implementation. For Racket, I can't find anything, but Racket's got all of Monte's tools and more, so it sounds feasible although difficult. Capn Proto RPC offers a sort of high-level memory-safe polymorphism via Those were not great words. In better words: If you know Monte's schema for messages, then you can send messages to Monte vats speaking Capn Proto RPC. Two things to note: The Monte schema is very plain and would be easy to reverse-engineer (read: sort of self-describing), and also monte-language/typhon#220 would allow us to accept multiple schemata which are indexed by those hexadecimal UUIDs that are on the first line of the IDL (Goblins, Agoric, Sandstorm, etc.) (Here is where my persistent vat implementation would go -- if I had one~! A persistent vat is just one which can host meaningful SturdyRefs with durability and the ability to take backups and time-warp and etc.) I gotta endorse @zenhack's point that Capn Proto RPC has seen real-world use, mostly via Sandstorm. |
I haven't had a chance to read this whole thread, but wanted to throw a couple things out there... Cloudflare's Durable Objects is an actor-model global distributed compute platform that is today implemented using Cap'n Proto RPC. At present we don't directly expose Cap'n Proto to the application layer, instead requiring apps to use HTTP-shaped interactions. We do, however, implement e-order (it's even in the docs). We'd like to expose non-HTTP-shaped RPC to the application layer. It's likely we'd do this using V8 serialization to encode JavaScript objects. V8 serialization implements the "structured clone" algorithm that appears commonly in the web platform. This is the best thing for us to use because V8's implementation is well-optimized, well-tested, and supports a well-defined and widely-understood subset of types. In our system, the serialized bytes will never be exposed directly to the application, so it doesn't matter than it's V8's particular format. This format is also available in Node.js. But I guess defining a common standard based on V8 serialization -- which itself is not documented as a standard -- might be awkward.
It's possible to build distributed gateways. Complicated, but possible. |
Note that some of us had a bunch of useful discussions last evening (somewhat impromptu on a call that wasn't planned for this purpose specifically), and there's another meeting crossing pretty much the Agoric, Spritely, CapnProto worlds on an upcoming call. Foolishly I did not take notes, but I'm going to write down what I remember:
There's a lot else to discuss obviously I think. Also my memory, as usual, may have gotten something wrong. Let me know if something needs correcting. |
Oh one other thing that was an interesting and useful observation: this might not be an Agoric/Spritely CapTP vs CapnProto duality debate. CapTP is a membrane... so that means OCapN CapTP is also a membrane. Likewise, CapnProto is a membrane. So it could be possible to write the same program that works in either. Indeed this is exactly the idea for supplying ActivityPub support in Spritely: make it a membrane. One interesting bit though is that even if we had two protocols/membranes, the idea of the OCapN netlayer interface idea could work for both... it makes sense to layer both CapnProto and OCapN CapTP over the same base connection types. So, the mechanism to either connect over Tor Onion Services, I2P, DNS+TLS, etc with OCapN goblin-chat could also be generalized for CapnProto to be able to use. In this sense, if we added a separate layer for CapnProto, we could still get a big win so that Spritely and Agoric programs could easily still talk to CapnProto applications, even if the default for Spritely / Agoric programs might not be as IDL-centric and might not have the same memory management assumptions. |
BTW, one of the reasons I chose Syrup for my initial implementation is exactly because I wanted something simple that could be written in just 2-3 hours on any platform that wasn't aiming to be the best encoding of all time. It's a huge bikeshed and we're going to be very vulnerable to spending all our time painting it if we're not careful. This is really the least important layer for us to spend time on (well, maybe that's not true if you start with a static/IDL centric approach, but it's true from my perspective); the really hard design decisions aren't in the layer that could be swapped out in about three or so lines of code if necessary later. I'd prefer we could pick something and move on. Since we have two implementations already using Syrup and the implementors of those seem to think it was decent enough, I'd say let's focus on the other much more interesting and difficult bits. |
Regarding GC vs. manual memory management, I strongly suspect there's a misunderstanding somewhere, because you seem to be suggesting that this has protocol level implications, and I do not think that's the case -- capnproto uses the same refcounting design at the protocol level as everything else. In a GC'd language there's absolutely no reason you couldn't just rig up a finalizer to drop the refcount and call it a day, as I have in haskell-capnp so far. If the race condition you mention is the one I think it is, capnproto does indeed deal with this. As far as I can tell the "viable direction" you describe is where we already are. But we should make sure we're on the same page.
I guess I'd envisioned dealing with the other direction via reflection, which still needs designing/implementing on the capnp side, but would allow calling into capnp from dynamically typed captp by mapping the dynamic data to the methods described by the capnproto schema. |
Right. Cap'n Proto, as a protocol, implements exactly the same refcounting approach as CapTP. The difference is philosophical: I don't believe you can simply hook up this refcounting to local GC finalization hooks and achieve acceptable results. Good GC implementations depend heavily on memory pressure notifications. In order to have distributed GC, you need a notion of distributed memory pressure. Otherwise, a machine where no memory pressure exists will simply hold on to all its capabilities forever, and the remote machines hosting the target objects will not be able to collect them no matter how much memory pressure they are seeing. So unless someone has a proposal for actual distributed GC that solves these problems, I think it's necessary for applications to explicitly drop their capabilities when they no longer need them. But that's really up to the application; if you disagree and think finalization callbacks are fine, there's no reason you can't do it that way on top of Cap'n Proto... That said, I do agree that the choice of philosophy here can heavily influence how application interfaces are designed. |
Note that GHC's runtime will start a GC cycle if it is idle for some (relatively short) amount of time, and it's very rare to have activity going on that's non-allocating. I'm not 100% convinced kenton's concern applies to any GC design (for that matter, the refcounted pointers that capnproto-c++ uses internally are arguably a form of GC, an approach shared by CPython, though not otherwise terribly common), but it does seem reasonable to worry that depending on the GC design and the idioms of the language, relying on finalizers may result in leaking memory on remote machines. I also wonder if triggering network communication with finalizers opens us up to sidechannel attacks, by leaking information about unrelated allocation activity inside the vat. I don't really know how to think about trying to quantify this. But, again, all of this is entirely orthogonal to the protocol. |
Note also that capnproto-c++ uses refcounted smart pointers for capabilities, so the code you write does not really spend any text explicitly freeing things. Arguably refcounting is a form of GC, which trades throughput and the ability to reclaim cycles for predictability and simplicity. Most language runtimes make a different trade-off, but CPython is a notable exception. |
Yes, I am in the RAII camp, which is very different from "manual" memory management. Please don't lump us together. 😃 |
That's good news if true re: the memory management stuff being the same. I got the impression from @erights that Cap'n Proto did something different about memory management than captp did... I think @erights has thought that a very "different assumption is being taken with cap'n proto's handling of memory. If that's not true, that's one major point of concern removed. |
I may indeed be confused about this. I would love to find out that this is not a barrier. Looking forward to clarity on this! |
See https://github.com/Agoric/agoric-sdk/pull/2909/files for a draft more detailed semantics of the Agoric CapTP's Although somewhat biased towards JS, the abstract semantics above the divider is intended to be language independent enough to serve as a basis of inter-language interoperability, e.g., with Goblins and perhaps with Cap'n Proto. We'll see. |
I already commented on the above PR, but this is really great! I agree with describing the passables in terms of their abstractions, as opposed to in terms of a specific marshalling. (Perhaps that's the definition of a recent pun: when a serialization system too heavily leaks into the abstraction of the datatypes, maybe we have entered into "marshall law".) We should thus do the same also for all the operations, as well as the certificate structures for handoffs. If we can describe this all abstractly, we'll have a lot of flexibility in terms of swapping out the serialization, and can focus on it less. (Of course the serialization in terms of canonicalization will matter a lot on the wire, since that will affect signatures of certificates... but it should not affect seriously the way we write our code.) |
Something like this abstract spec-wise, @cwebber ? op := deliverOp | deliverOnlyOp | gcAnswerOp | gcExportOp |
listenOp | terminateOp | bootstrapOp | eventualGetOp |
deliverFuncOp | deliverFuncOnlyOp
deliverOp := deliverOpMarker answerPos redirector target verb arguments kwarguments
deliverFuncOp := deliverFuncOpMarker answerPos redirector target arguments kwarguments
deliverOnlyOp := deliverOnlyOpMarker target verb arguments kwarguments
deliverFuncOnlyOp := deliverFuncOnlyOpMarker target arguments kwarguments
gcAnswerOp := gcAnswerOpMarker answerPos
gcExportOp := gcExportOpMarker exportPos wireDelta
listenOp := listenOpMarker remotePromise resolver
terminateOp := terminateOpMarker terminationReason
bootstrapOp := bootstrapOpMarker answerPos resolver
eventualGetOp := eventualGetOpMarker answerPos redirector target prop
redirector := anyDesc
resolver := anyDesc
remotePromise := anyDesc
target := anyDesc
verb := string | symbol
arguments := argumentsMarker args
args := arg [ args ]
arg := any
kwarguments := kwargumentsMarker kwargs
kwargs := argKey argValue [ kwargs ]
argKey := any
argValue := any
prop := any
any := anyDesc | datum | compoundData
anyDesc := answerDesc | exportDesc | importDesc | handoffDesc
answerDesc := answerDescMarker answerPos
exportDesc := exportDescMarker exportPos
importDesc := importObjDesc | importPromiseDesc | newImportObjDesc | newImportPromiseDesc
importObjDesc := importObjDescMarker importPos
importPromiseDesc := importPromiseDescMarker importPos
newImportObjDesc := newImportObjDescMarker importPos
newImportPromiseDesc := newImportPromiseDescMarker importPos
... (I probably edit and add more when I have nenna/gumption for it) |
Wow that's a great start! I have more comments but incredible work @zarutian! |
Note that re: I see you added This seems like a good start... maybe we have enough to go off of to start working on some fresh docs. |
As xentrac on IRC pointed out forcefully the other night, relying on canonicalization for signatures is probably a bad idea; any signature checking should be on an existing blob, and we should not rely on being able to reproduce the exact bytes for the purpose of checking a signature. So I think this is not really a concern either. |
Let’s consider this massive ticket an epic and consider breaking out individual design threads as linked issues. Please feel free to join me in evolving the ticket description.
Wire protocols have a bunch of non-orthogonal design dimensions, and cross-language communication has a lot of gotchas. A protocol designed for high fidelity communication among JavaScript workers won’t necessarily (but might) be just as suitable for JavaScript workers communicating with Racket workers, and vice versa. A different protocol might be suited for communication among close relative languages like Python, Ruby, and shell scripts. A different protocol might generalize to all those languages plus C, C♯, Java, Go, and Rust. That last language class might be the first where it’s necessary to introduce an IDL to participate idiomatically.
We might also be in need of multiple coherent CapTP protocols, possibly with the assistance of gateways.
I would much rather design a wire protocol or IDL for a closed set of design languages than pretend that the design is suitable for any! Every new language brings some limiting quirk to the table, like JavaScript’s null and undefined, JavaScript’s 53 bit integers, JavaScript’s conflation of objects-as-structs and objects-as-dictionaries, Java not having unsigned integers, Go’s zero-value idiom, C’s struct packing, Python’s snake case, Perl’s advanced dementia.
So I’d like to collect some opinions about the goals and non-goals of OCapN CapTP.
I’d suggest we would at minimum need support for the following data types:
number
and IDL might specify for languages that care.BigInt
and IDL might specify for languages that care.number
, with suitable representations for Infinity, -Infinity, NaN, and maybe even -0, unlike JSON.The text was updated successfully, but these errors were encountered: