[DO NOT MERGE] Simplified API - #128
Conversation
|
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #128 +/- ##
==========================================
+ Coverage 90.67% 92.76% +2.08%
==========================================
Files 7 8 +1
Lines 708 926 +218
==========================================
+ Hits 642 859 +217
- Misses 66 67 +1 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
I have spent some time thinking about the naming of operations on the handle type and realized the key issue is that I originally wanted to make it clear how the simple API wraps the inner API. For example, if a method on I've outlined all operations on the handle object in the table below.
For some of these operations, we could either overload functions in Some notes:
I'm also wondering about the safety of checking |
|
I added some examples of the lifecycles of different types of calls. Basic examples are in the auto-generated docstrings at the end of |
|
@csvance I think this is at the point where all functionality (except trival things like forwarding keywords) is implemented. Remaining work looks straightforward to me (to-do-list in description updated), but for example the level of documentation depends on to what extent this can be considered a replacement or not as we discussed previously. I would appreciate if you could have a look on the overall design decisions before I go too far. As an update on previous comments, I took a closer look at how to safely poll the status of |
|
@johroj we will push out 1.1.0 so people have the improved stability / cancellation without having to work from [sources]. Once I have that registered I'm going to take a look at this and getting it refactored. We can then do a 1.2.0 release sometime in the next week or so. I think its justified to have two different minor releases, one that basically fixed almost all historical stability problems and made streaming work on all supported julia versions, and one that significantly improves the interface. |
| end | ||
| PB.default_values(::Type{TestResponse}) = (; data = Vector{UInt64}()) | ||
| PB.field_numbers(::Type{TestResponse}) = (; data = 1) | ||
| PB.default_values(::Type{TestResponse}) = (;data = Vector{UInt64}()) |
There was a problem hiding this comment.
I think this is just about ProtoBuf.jl not generating files with Runic formatting.
There was a problem hiding this comment.
We should just add a Runic pre commit hook so its not an issue going forward. Although I would say just hold off on that till we rebase ontop of main since it risks making the rebase more difficult.
| end | ||
| end | ||
|
|
||
| Base.isopen(req::gRPCRequest) = !(@atomic req.ready.set) # TODO dont rely on fieldnames of Base.Event? |
There was a problem hiding this comment.
I think it would be good to have some better mechanic to poll the status of the request, acquiring the full lock would be too inefficient. If we want to check private fields of Base.Event, would it be ok to add an atomic field req.isdone or similar? Or can you see some other way forward?
Same thing regarding checking if req has an exception. Could make sense to have a helper function in Curl.jl.
There was a problem hiding this comment.
That's a reasonable trade off. Pretty sure it won't increase allocations either.
There was a problem hiding this comment.
CI should also warn if this field is removed in an upcoming release.
| # Until julia gets a dedicated syntax for importing from parent module without | ||
| # knowing its name, we need to use `parentmodule`. Otherwise the generated file | ||
| # will only work if included from the correct generated toplevel package file. | ||
| push!(import_mod_list, "const $(modname)::Module = Base.parentmodule($service_name).$(modname)") |
There was a problem hiding this comment.
We could enforce the better syntax import ..A: B if we require that protojl runs with always_use_modules = true and users always include the top-level package. But I noticed that this is not used in e.g. the unit tests of gRPCClient.jl, where the _pb file is included directly. I took that as a signal that this is something users might want to do.
There was a problem hiding this comment.
Maybe this can be a target for 2.0.0 combined with improved documentation / examples on the subject. When working with a very small number of protobufs its nice to be able to do include("myproto_pb.jl") because its easy to understand without reading anything.
| host::String | ||
| port::Int | ||
| grpc::gRPCCURL | ||
| function gRPCChannel(host::AbstractString, port::Integer; grpc = gRPCCURL()) |
There was a problem hiding this comment.
I think we should change grpc = gRPCCURL() to just use grpc_global_handle() as default instead. Setting up a new gRPCCURL is not cheap, the benchmarks did not look good until ensured a new instance is not spawned in each workload.
There was a problem hiding this comment.
@johroj I believe the is the default behavior in general with 1.0.0 - 1.1.0; we re-use by default, and let the user put things on different multi if they want to. That should make the additional overhead associated with creating a gRPCChannel quite minimal.
There was a problem hiding this comment.
Yes, exactly. I think what made me initially go for independent instances is that I have mainly used the channel sort of as a global for all communication to a specific server. I think this it would be more intuitive that if you then set up multiple channels, you want them to be able to operate independently. But if someone really cares about this, they will probably have had a look in the documentation, so lets make the default simpler.
Thanks, then we can give these interface changes the time it takes. I added some comments about behavior where I'm still not certain what the best choice is. You may of course find more things. TODO-list in description is updated. |
| import gRPCClient | ||
| import Base | ||
|
|
||
| Base.@static if Base.:!(Base.isless(Base.pkgversion(gRPCClient), Base.VersionNumber("1.2.0-rc1"))) |
There was a problem hiding this comment.
@csvance I added this to the feature list. Let me know if you think it is overkill. This if-statement will only be included if both APIs are generated (default, for backwards compatibility). It means new generated code will run with older versions of gRPCClient without complaining about missing functions. Unfortunately, VSCode completion does not work for methods inside a @static, so it will only work if only the new API is generated (which still can be recommended in documentation or default in a 2.0).
Then there's also gRPCClient.check_codegen_compat which can be used to error or warn on incompatibilities. Even if this function does not do anything today, its probably good to include the capability already now.
How to handle partially encoded messages?@csvance I realized I previously overlooked the ability to bypass encoding/decoding and just send raw 1. Keyword argumentsOptional keyword arguments 2. Singleton struct flagsA type stable way to could control the message types is to use singleton structs such as 3. A combination approachA good, but slightly more segmented approach is to control the types in different ways depending on the types of request/response. For unary, encoding/decoding is not done in a separate task so there is no channel which needs a predetermined type. We can simply provide a For streaming requests, we can use a For streaming response, using a 4. Encode/decode in main threadMay need some input on the overall design choices to tell whether this option is feasible. I can see that if the current API wants a user-facing 5. Dont implement it for nowAlso an option, of course. |
|
@csvance I took a closer look on how the the streams work and understood that the messages need to be encoded in the pump task since multiple requests may be sent to Curl in the same buffer. But I could not see anything similar for responses. So I went ahead and used a The only thing remaining is documentation. Do you think it would be the easiest for you to review this as-is and we take the documentation in a follow-up PR? In order to write the full documentation, I would first need your overall input on which direction to take as a whole, but I could probably add some parts during the upcoming week if it would be helpful for a review. |
|
@johroj I should have time to work on this next week. I'm extremely close to registering gRPCServer.jl. Once that is pushed through, I will have the bandwidth for gRPCClient 1.2.0. I agree we should review it as is and push the documentation changes into a follow up PR. |
ea331e1 to
7854444
Compare
Ok, this is a basic draft of what was discussed in #122
Codegen
This feels pretty much like what I had in mind. Some generic function signatures are written explicitly, with type assertions for readability. Some types are left abstract to leave room for future changes. All valid usecases (unary sync, unary async, unary channel and streams) are supported. You can see the new code in the updated
test_pb.jl.Internal logic
The API called directly from the generated code should probably be considered public, but does not need to be exported. The logic is highly based on the traits defined in the generated code - I found this easy to work with and meant that all properties of an RPC could be found by using the function type
typeof(MyService.MyRPC)as a single type parameter.The handle type
For all asynchronous calls, a handle is returned, with types depending on the kind of RPC(
gRPCUnaryHandle,gRPCStreamResponseHandle) etc. This should be a single object with methods for all operations one may need after opening a request. I'm still a bit hesitant on the current names, but the following functionality is necessary:put!will block (isfull, not available in 1.10)put!(or equivalent).isready).fetch).take!).kill)close).isopen)The approach I was going for was to overload methods from
Baseto make operation as similar to a channel as possible. This is good because it allows using short simple names without cluttering the namespace. But there are some cases where there is no clear choice, for exampleisopenand could very well refer to both the response channel or thegRPCRequest. Same problem withwait. If you have any thoughts, please let me know.Remaining items:
RenamingDespite the similarity togRPCChannelBase.Channel, I now lean towards channel being the correct term with gRPC terminology, so we should keep this anyway.Should we always throw errors from theYes, this does not seem to affect performance whatsoever.gRPCRequestwhen doingput!ortake!on the handle?gRPCConnectionOptions. AgRPCChannelshould be able to carry default options as well.Vector{UInt8}responses/requestsgRPCClientUtilsRunic