Closed hspaay closed 2 years ago
Yeah, we're aware the documentation isn't great, and shoring it up is something I want to do before we finally tag 3.0. I'll try to dump a few key ideas here to get you moving; maybe when I've got the time I can cannibalize some of this post to make something more official:
Re: ServerToClient:
A "server" in capnp terminology is an object which handles incoming method calls, while a "client" is an object used to invoke methods on some server, which it points to.
These don't necessarily line up with with any notion of network server & client (in fact, they usually don't): you can have many clients and servers floating around on both sides of a network connection, the clients can point at servers on the other host, or at servers existing in the same process.
Only clients can be passed around via the protocol, so to actually offer your bootstrap capability to a network peer, you first have to get a client for it. This is where the *_ServerToClient
functions come in; the code generator emits one of these for each interface type in your schema. It takes something that satisfies the server interface for that type, spawns a goroutine to handle incoming calls for that object (one at a time), and returns a client, which you can set as your bootstrap interface, or pass to a method call, or return from one or...
Minor thing to be aware of: the server.Policy type has been removed on main
; the example code should be updated to reflect that (I'm going to just do that now before I forget...)
Re: the call parameter, It offers three things:
Args()
returns the struct containing the arguments to the method call.AllocResults()
returns a newly-allocated struct where you can put the return values of the call.Ack()
(which is a bad name for it, see #289) forks off a new goroutine to handle further method calls on the server object; this let's you write code where a method call waits on some long-running thing without blocking other calls on the same object.It's mostly its own type because un-bundling these things would result in huge method signatures. The v2 API went even further in that the context was stored there as well, but there's such a strong expectation of that being the first argument that we figured the extra verbosity was worth it there...
Hopefully this is helpful, sorry for the current state of affairs, and thanks for the feedback.
@zenhack Thank you kindly for the explanation and the fast response. This helps a lot. I'll give it another try.
Can't get my code to compile as 'main' doesn't have a member called Client.
// Listen for calls, using the logger as the bootstrap interface.
conn := rpc.NewConn(rpc.NewStreamTransport(rwc), &rpc.Options{
BootstrapClient: main.Client,
})
I noticed that BootstrapClient needs to be of type capnp.Client, which is puzzling as 'main' is an instance of this type. Is there a better name than 'main' to convey its purpose? To get it to compile this change was needed:
import capnp "capnproto.org/go/capnp/v3"
...
BootstrapClient: capnp.Client(main),
The import is not there in the example, but it is in the generated code. So maybe this is not the right approach. ... moving on...
Ah, yeah, that's also bitrot -- we recently changed the generated client types from being defined as type Foo struct { Client capnp.Client }
to type Foo capnp.Client
, and I guess forgot to update the wiki.
@zenhack almost there, but for one more problem...
In the example, when creating the client, the field 'Client' doesn't exist.
// now we generate the "client"
// it's just called "logger" to make it a bit easier to understand
logger := pkg.Logger{Client: clientConn.Bootstrap(ctx)}
This produces a compiler error.
If I remove the field, then the client call fails with 'capnp: call on null client'.
The Logger (of type capnp.Client) does have an internal '*client' field but that is not accessible.
Just a suggestion, maybe try to get the example code to run as it exposes quite a few issues... just say'in ;-)
Quoting Henk (2022-08-29 14:54:28)
Almost there, but for one more problem shows In the example, when creating the client, the field 'Client' doesn't exist. // now we generate the "client" // it's just called "logger" to make it a bit easier to understand logger := pkg.Logger{Client: clientConn.Bootstrap(ctx)}
This produces a compiler error.
Ah, that'd be due to the same API change I mentioned. So you want
pkg.Logger(clientConn.Bootstrap(ctx))
.
Thanks @zenhack. One more issue cropped up with this:
The call to resp.Struct() returns this error in my own code:
capnp/svc/CertSvc.capnp:CertServiceCap.createClientCert: vat does not expose a public/bootstrap interface
Code snippet:
resp, release := certClient.CreateClientCert(ctx,
func(params svc2.CertServiceCap_createClientCert_Params) error {
fmt.Println("CertServiceCap_createClientCert_Params")
err = params.SetClientID(clientID)
err = params.SetPubKeyPEM(pubKeyPEM)
return err
})
defer release()
result, err := resp.Struct()
The server side does have the bootstrap client registered:
rwc, _ := lis.Accept()
transport := rpc.NewStreamTransport(rwc)
conn := rpc.NewConn(transport, &rpc.Options{
BootstrapClient: capnp.Client(main),
})
defer conn.Close()
I tried stepping through the code but got lost unfortunately. Any thoughts where to look?
Hunch: try changing to capnp.Client(main.AddRef())
; I'm hazarding a guess that what's happening is that the client gets shut down after the first connection closes, so future connections are using a dead client as their bootstrap interface.
The refcounting model is something we also know we need to document better, but there's this wiki page at least: https://github.com/capnproto/go-capnproto2/wiki/Ref-Counting
@zenhack your hunch foo is strong! That did the trick. You were also right about the connection closing. After use AddRef() when the client call has completed, the connection still closes however. Should the server run in a loop, like this?:
for {
rwc, _ := lis.Accept()
transport := rpc.NewStreamTransport(rwc)
conn := rpc.NewConn(transport, &rpc.Options{
BootstrapClient: capnp.Client(main),
})
defer conn.Close()
// Wait for connection to abort.
select {
case <-ctx.Done():
return conn.Close()
}
}
I'm wondering whether creating new transports for each request is the intended way or not? Thanks again,
@hspaay I've been quietly monitoring this thread, and it's prompted me to revise our our Getting Started guide. Your feedback would be most appreciated! https://github.com/capnproto/go-capnproto2/wiki/Getting-Started
@zenhack your hunch foo is strong! That did the trick.
Ah yes, Ian's capnp hunches are second to none 😄
Should the server run in a loop, like this?:
That seems sensible. I would recommend forking a goroutine to handle the select
block, so that you don't block the main loop, but other than that 👍.
I'm wondering whether creating new transports for each request is the intended way or not?
Definitely not, unless those requests are one-off and short-lived (e.g. like an HTTP GET request). Bear in mind that capnp-rpc has a very strong object-oriented model. To wit: you aren't just making RPC calls, here. You are (1) exporting an object over the network and (2) making method calls against that object. As such, the typical pattern is to create a connection for an initial object that you want to export (called the "bootstrap interface"), and for the client to hold it for an indeterminate period of time. Methods on that object can return other objects, which all use the same underlying connection.
I would strongly encourage you to read this excellent article on object capabilities, to understand what this achieves. I think it'll help you think creatively with Cap'n Proto. 🙂
(P.S.: welcome to the community!)
Quoting Henk (2022-08-29 19:37:58)
Should the server run in a loop, like this?: for { rwc, _ := lis.Accept() transport := rpc.NewStreamTransport(rwc) conn := rpc.NewConn(transport, &rpc.Options{ BootstrapClient: capnp.Client(main), }) defer conn.Close() // Wait for connection to abort. select { case <-ctx.Done(): return conn.Close() } }
I would probably spawn a goroutine after the Accept(), so you can handle more than one connection at a time. But otherwise this looks right.
I'm wondering whether creating new transports for each request is the intended way or not?
You should have a new one for each connection; I'm not sure what you mean by request. Do you mean a method call? If so, no, you'd normally just make more calls on the same client once you've got it.
Note also that @lthibault just rewrote the rpc section of the Getting Started wiki page; wouldn't mind your feedback on that.
@zenhack @lthibault thank you again for the quick responses.
I would strongly encourage you to read this excellent article on object capabilities, to understand what this achieves. I think it'll help you think creatively with Cap'n Proto. slightly_smiling_face
Wow that is quite the article. Brilliant! Still reading but so-far it scratches an itch that has been growing larger for years. The security aspect is very interesting.
@hspaay I've been quietly monitoring this thread, and it's prompted me to revise our our Getting Started guide. Your feedback would be most appreciated! https://github.com/capnproto/go-capnproto2/wiki/Getting-Started
I'll take a look once I've completed the article :)
To wit: you aren't just making RPC calls, here. You are (1) exporting an object over the network and (2) making method calls against that object.
I originally thought that the request and response operate using a proxy on the object. Sounds like there is a bit more to it.
I would probably spawn a goroutine after the Accept(), so you can handle more than one connection at a time.
Yep that seems to work.
I'm putting an echo service example together that works with gRPC and with capnpro. Maybe it is useful as another example, plus I'm curious about a performance comparison. Hopefully I get to a level of understanding that would allow me to assist with some documentation and examples.
I originally thought that the request and response operate using a proxy on the object. Sounds like there is a bit more to it.
That's effectively it. I'm using the phrase "export an object" in the conceptual sense, to emphasize the fact that Cap'n Proto wants you to think in terms of networked objects.
Maybe it is useful as another example, plus I'm curious about a performance comparison.
As are we! Looking forward to it!
Still reading that document, but also had some fun with a test echo service comparing gRPC and go-capnproto2 (v3):
This test compares various payloads and unix domain sockets vs TCP sockets. Performance is fairly evenly matched. Multiple runs provide a 10% variability:
The echo test repo can be found here: https://github.com/hiveot/echorpc (disclaimer: the code is still very crude. It needs better commenting and probably a bunch of bug fixes)
UPDATE 2022-08-31: Use plain 'echo' as 'upper' slows the performance measurement for documents 10K and up. Now a difference between gRPC and Capnproto starts to show for larger documents.
$ go run pkg/main.go
--- test with Hello World payload ---
10000 calls using gRPC on unix:///tmp/echoservice-grpc.socket: 950 millisec
10000 calls using Capnp on /tmp/echoservice-capnp.socket: 950 millisec
10000 calls using gRPC on :8991: 1150 millisec
10000 calls using Capnp on :8992: 1430 millisec
--- test with 1K payload ---
10000 calls using gRPC on unix:///tmp/echoservice-grpc.socket: 870 millisec
10000 calls using Capnp on /tmp/echoservice-capnp.socket: 1140 millisec
10000 calls using gRPC on :8991: 1260 millisec
10000 calls using Capnp on :8992: 1670 millisec
--- test with 10K payload ---
10000 calls using gRPC on unix:///tmp/echoservice-grpc.socket: 1410 millisec
10000 calls using Capnp on /tmp/echoservice-capnp.socket: 1310 millisec
10000 calls using gRPC on :8991: 1170 millisec
10000 calls using Capnp on :8992: 1690 millisec
--- test with 100K payload ---
10000 calls using gRPC on unix:///tmp/echoservice-grpc.socket: 4670 millisec
10000 calls using Capnp on /tmp/echoservice-capnp.socket: 3270 millisec
10000 calls using gRPC on :8991: 5190 millisec
10000 calls using Capnp on :8992: 3850 millisec
@hspaay Neat! I just had a look at the code and one thing you might try is to call call.Ack()
inside of the server method. This will cause the call to be handled in a separate goroutine, which might improve performance. I doubt it'll be the case for such a trivial RPC call as Echo
, though.
Also, Ian will be doing some performance tuning of the RPC system once #294 is finished, so it'll be interesting to run these microbenchmarks again afterwards.
@lthibault FYI Just tried the Ack() and there is little difference as you expected. If anything it slowed things down a bit on smaller payloads. Anyways, I'm veering off the original topic, which is documentation. Maybe a separate thread on performance would be useful. I'll continue the reading (the article is a slow read or maybe I'm developing a case of ADHD ;)) and will get you feedback on the example page.
UPDATE: Apparently 'upper' does start to slow things down significantly for 10K and 100K documents so I changed the test to just do echo. This shows a clearer difference between gRPC and Capnproto for documents 10K and 100K.
@lthibault Some feedback on the first pass of the getting started guide:
Overall, it has some nice improvements and explains some things better. A nice touch is that it shows what to do in case of a compile error 'no such plugin'.
There does seem to be two trains of thought mixed together. One is to show how to use capnp as an RPC. The other to show how to use it for capabilities. Just a thought, but may best to split those two concepts in two examples. The RPC is a stepping stone towards showing capabilities to access a resource like a book store. I don't think you can get away with having to explain capabilities a bit further for golang developers, as the capnp document does a limited job of this.
To compile this schema into Go code, run the following command: capnp compile -I$GOPATH/src/capnproto.org/go/capnp/std -ogo foo/books.capnp This generates the file foo/books.capnp.go. Note that the path matches the one from the import annotation.
This last line wasn't immediately clear to me as it suggests that the destination folder is determined by the import statement. That isn't the case as it goes into the same folder as the capnp IDL file.
For those that prefer to separate the capnp file from the generated code, the next question is how to change the destination folder? Eg: add the destination folder to the -ogo:
capnp compile -I$GOPATH/src/capnproto.org/go/capnp/std -ogo:/path/to/destinationFolder foo/books.capnp
Also worth mentioning the catch that it repeats the path to the capnp file in the destination folder, eg /path/to/destinationFolder/foo/books.capnp.go. To compile straight into the destinationFolder add the option --src-prefix=foo. This is equivalent to grpc's "paths=source_relative" option.
This section confused the heck out of me as I thought this was necessary to send or receive a message. Apparently it isn't needed so it threw me off. Some questions that come to mind:
Please consider that I'm reading this from the perspective of a developer that wants to get going with the example program. Everything that isn't needed is in the way. I'm sure its useful but maybe in a next chapter or in a FAQ.
The intro is good, links to more info and focus on the example. Two notes on those links (please keep them, just an FYI):
The explanation of the compiler generated types is very useful. Maybe elaborate a bit further to explain the purpose of each type.
The confusing bit to is that the example seems to be disconnected from it (it isn't but that isn't obvious).
In a nutshell, while showing 'do this' to make it work is important, the reader's understanding would improve further with a short explanation as to what is idiomatic and what it tries to achieve as mentioned above.
I found this example only serves a single client connection, then quits. Maybe useful to show the loop to serve multiple client connections, otherwise it ain't a real server ya'know :)
The comments explain what the code is doing. The questions that did come to mind:
Thank you kindly for working on an elaborate getting started. I think it is a key piece to get people unfamiliar with capabilities (like myself) started with it. Hopefully this feedback helps.
@hspaay Firstly, thank you so much for this detailed feedback. This is incredibly helpful!
There does seem to be two trains of thought mixed together. One is to show how to use capnp as an RPC. The other to show how to use it for capabilities.
I'm having trouble seeing where the two could be more cleanly separated. Do you have an example of what RPC sans capabilities might look like?
[...] I don't think you can get away with having to explain capabilities a bit further for golang developers, as the capnp document does a limited job of this.
Just to clarify: are you saying that we need to add a section about capabilities in addition to the existing tutorial, or are you also saying that the current demo is confusing because it conflates RPC with capabilities?
This last line wasn't immediately clear to me as it suggests that the destination folder is determined by the import statement. That isn't the case as it goes into the same folder as the capnp IDL file.
Fixed. ✅
For those that prefer to separate the capnp file from the generated code, the next question is how to change the destination folder?
There's always some tension between between being clear and complete. I've found that people are rapidly overwhelmed by the build process, so I'm of the opinion that we should not address this here. Besides, it's documented in the capnp
command itself. @zenhack Agree?
As a compromise, I've added a small callout to the tutorial that tells readers where they can read about compilation options.
This section confused the heck out of me as I thought this was necessary to send or receive a message. Apparently it isn't needed so it threw me off.
Also very useful! I did not expect this section to cause such confusion, so thanks for bringing it to light.
I've unfortunately run out of time for today, but will go over the rest of your feedback carefully tomorrow.
In the meantime, it's become apparent that some additional explanation of concepts will be needed, which means we need to split the wiki page into several tutorials. I've done so, and linked them all together in a (hopefully) intuitive way: https://github.com/capnproto/go-capnproto2/wiki/Getting-Started
I'm having trouble seeing where the two could be more cleanly separated. Do you have an example of what RPC sans capabilities might look like?
Hah good question. I currently don't have the knowledge to provide a good answer. The simple RPC call case is clear and described in the arith example. Capabilities add an aspect that I don't fully comprehend but think it could address a key concern, that of security. (My use-case is around IoT). For example, in a book store owner 'bob' can add books and reader 'charlie' can read purchased books. How does this play out in a capabilities based system? In an ACL system there is a authorization step that verifies that the caller has permissions to add books. The capabilities article explains that the ACL mechanism is flawed and claims that capabilities solves this. It isn't clear how this is solved though. Seeing this in an example would be a great demonstration of Capabilities based development. Is out of scope for capnproto?
Just to clarify: are you saying that we need to add a section about capabilities in addition to the existing tutorial, or are you also saying that the current demo is confusing because it conflates RPC with capabilities?
An additional section/chapter about capabilities. The current demo presents an API that is different from conventional RPC. Eg the client supplying a method to set the parameters for example. Purely from an RPC point of view is this really a necessary complication? Normally you'd pass the parameter to the method to invoke. It probably makes a lot of sense from a capabilities point of view but I don't understand it enough to be sure. After reading the articles it hasn't sunk in why this approach is better, other than 'it is how capabilities work'. What is gained doing it this way? This is for me the most important question to help understand this API.
There's always some tension between between being clear and complete Besides, it's documented in the capnp command itself.
Yes, true on both accounts.
Thank you for considering my comments.
@hspaay Continuing yesterday's discussion:
Marshaling and Unmarshaling Data
I've reworked this quite a bit in an attempt to separate three distinct ideas:
[]byte
I've moved this section to its own page, under the umbrella title of Working with Cap'n Proto Types. Hopefully this is clearer!
Remote calls using interfaces
Points are duly noted. I think I'm going to address this in two steps:
Schema
Any reason not to continue with the book theme?
No particular reason. That said, Ian suggested the Arith
example might feed nicely into this existing calculator example from the C++ capnp implementation.
This would provide a good setting to demonstrate advanced features such as pipelining, along with examples of capability-based access control.
"Capnp RPC operates on interface types in your schema" ... what does this mean?
Agreed this could be clearer. I spent a disproportionate amount of time on that sentence, despite the idea being very simple: RPC in Cap'n Proto is performed by method calls on special objects. You define such an object in the IDL using the interface
keyword.
Do you have any thoughts on wording?
The explanation of the compiler generated types is very useful. Maybe elaborate a bit further to explain the purpose of each type.
👍 Will do. Your specific points are also noted (and extremely helpful).
I found this example only serves a single client connection, then quits. Maybe useful to show the loop to serve multiple client connections, otherwise it ain't a real server ya'know :)
Fair point! I'll add a short, final section in which we write the server loop. Explicit is better than implicit, after all 🙂
Your specific questions are noted, and I'll address those in the docs shortly.
Thank you for considering my comments.
And thank you once again for your thoughtful feedback! Writing good documentation is frankly much harder than writing a correct implementation, so your perspective as a newcomer is most precious.
By the way, we have a dedicated Matrix channel that you are welcome to join, if chatting about this stuff in real-time would be helpful. (The above link will direct you to a Web client.)
I've reworked this quite a bit in an attempt to separate three distinct ideas: I've moved this section to its own page, under the umbrella title of Working with Cap'n Proto Types. Hopefully this is clearer!
Yes this approach works very well and it has an excellent description of what is going on. Well done!
RPC operates on interface types in your schema" ... what does this mean?
Agreed this could be clearer. I spent a disproportionate amount of time on that sentence,...
LOL, indeed, the simplest concepts are sometimes the hardest to explain... Let me give it a try, based on my current understanding ... (which is still rather limited)
"In the Capnp IDL file you define a schema using 'struct's for objects and interfaces for RPC methods. The go-capnp compiler generates a corresponding golang struct type with getter and setter methods for each of the struct members. The RPC interface is used to generate a golang interface for the server implementation, and a client types to invoke the server."
Maybe a bit too wordy though.
I see you're working furiously to add and improve various sections of the documentation. Looking much better already.
One last question, is the v3 API already written in stone? I wonder if there is an opportunity to simplify its usage, or have a higher level API for the main use-cases. I'll get in touch on Matrix. Thanks!
I see you're working furiously to add and improve various sections of the documentation. Looking much better already.
Good to hear! 😃
One last question, is the v3 API already written in stone? I wonder if there is an opportunity to simplify its usage, or have a higher level API for the main use-cases.
You're asking all the right questions 😉 The v3.0 API is not yet set in stone, and we have a couple of open issues (Exhibit A) regarding opportunities to improve the RPC API.
I'll get in touch on Matrix. Thanks!
See you soon! 👋
Docs are better than they were before, so closing 😃
Hi, I'm trying out go-capnproto2 using a simple service that is currently working using protobuf-v3. One of the reasons protobuf doesn't work for me is the lack of constants and the requirement of using a different port for every single service. UDS could work, but the memmap and pipes communication in go-capnproto is attractive.
Following the example on the wiki here: https://github.com/capnproto/go-capnproto2/wiki/Getting-Started#rpc-serving, I get stuck on writing the server adapter. Up to this point my own code works including compilation of the .capnp file, importing it in my code, writing an adapter interface. Using the adapter pattern makes a lot of sense.
Implementing the adapter itself however makes no sense at all. I've looked for documentation on how to implement a service but other than the example that has little explanation there isn't much. I tried reading the generated code but am a bit overwhelmed where the capnp file of 30 lines explodes to 890 lines of go code.
Specifically the following example bits are confusing:
The function 'pkg.Logger_ServerToClient' seems to come out of nowhere. Why is it here? It seems to be related to the client but it isn't clear how and why. Any documentation on this apparently essential function?
Implementing the adapter code
While it is obvious that this is the adapter implementation of the Debug logging function, it is a mystery where this API is coming from and how to use it. What is the 'call' parameter? what does it do. Any documentation on how to use it?
Example code is great and important but it doesn't replace proper documentation that describes what the steps are to implement a server with links to API documentation. Something that does not require reverse engineering the generated code? I can probably guess some stuff and eventually get things working but it feels I'm missing important documentation.
Any pointers in the right direction would be appreciated.