-
Notifications
You must be signed in to change notification settings - Fork 25
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
Client/server (req-rep) specification #213
Conversation
31d680f
to
199bfc2
Compare
See latest diff for a variant of asynchronous requests. The zmq API (req-rep-dealer-router) and doc is "flexible" for this type of use cases. I hope I got it now for the impl. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some general thoughts:
- There is an apparent contradiction between "Certain operations are by necessity synchronous" and "Requirements: Asynchronous remote procedure call".
- If the API is asynchronous it must return a future so that users can wait on the request completion.
I see the following use cases (requirements?) to shape the API:
-
get a remote object, no parameters. This is the most trivial and common use case, such as the livreGUI connecting to a renderer and requesting initial state.
-
get a remote object from a collection, with parameters passing for "filtering/selection", such as getting a specific block in the livre::RemoteDataSource example here or a group of morphologies from a circuit.
-
set a remote object directly, i.e. without requiring that the server has "subscribed" to the client.
-
basic RPC (execute a remote function) with or without return value and with or without parameters. It's easy enough to do basic RPC but it quickly becomes a complicated topic with future chaining, etc, better addressed by frameworks such as https://capnproto.org/rpc.html or HPX.
-
-
- are the equivalent of remote getters-setters for a property, and PUB-SUB can form the equivalent of notifyChanged signals.
-
Relationship with http::Server:
- and 2) are like handle(GET) in http::Server. 3) is like the handle(PUT) in http server. 4) doesn't have an exact http equivalent.
The exchange is synchronous, the API is not. Will clarify. |
No, it can also take a functor which is called on completion. I picked this approach since it does not require the reply object (if any) to be passed into the get(). |
I'll pick your use cases into the Examples section. |
On reading them, maybe not: 1, 2 and 3 are special cases of 4:
|
Or, to phrase it differently: The get Serializable is both your function name (the type uuid) and parameters (object data, may be empty), and the ReplyFunc is your return value. With this you can do any remote call. |
doc/feature/ReqRep.md
Outdated
|
||
using ReplyFunc = std::function<void(const void*, size_t)>; | ||
|
||
class Client : public Receiver |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is the inheritance from Receiver important from the user's point of view or is it just convenient for the implementation? If it's the latter replace it with implementetion-defined in the spec.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, it is important: The user can call a single receive() on a set of publishers, http::servers, clients and servers.
doc/feature/ReqRep.md
Outdated
|
||
## API | ||
|
||
using ReplyFunc = std::function<void(const void*, size_t)>; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Refresh my memory, do Zerobuf objects check the type ID during deserialization? Otherwise we are lacking type safety here, however I don't have a good suggestion to fix it (except add the type check in Zerobuf).
In any case, if the server doesn't use Zerobuf or some other typesafe serialization engine, we are in trouble.
doc/feature/ReqRep.md
Outdated
|
||
### 2: Is the client a receiver? | ||
|
||
_Resolution: Yes:_ The get method only sends the request, the reply is received in receive() and correlated to the request. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If a client issues millions of requests and doesn't call receive at all, what does it happen on the server?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Implementation-specific. zmq has high-water marks on pending requests, in this case the server reply will block eventually.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But can the server block in a single reply if the client doesn't process it? Or the behaviour in this case is a socket parameter?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's a socket parameter, and afaik a single reply won't block.
doc/feature/ReqRep.md
Outdated
class Client : public Receiver | ||
{ | ||
// ctors same as Subscriber | ||
void get(const Serializable& request, ReplyFunc& func); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How are server side errors propagated to the client?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See below: func( nullptr, 0 ) is called.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's not entirely satisfactory. In a request that has an empty reply (item 3 in the list from Raphael) how can I distinguish a success from a failure?
doc/feature/ReqRep.md
Outdated
memcpy data to block | ||
}); | ||
|
||
_client.receive( TIMEOUT ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To me it looks a lot more natural getting a future from _client.get and calling future.get to resolve it. Is there a way to achieve this without falling into thread-safety traps, or there's a fundamental reason for not doing it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When does the receive happen in your case? The app still needs to call it, i.e., future.wait() would deadlock in a single-threaded application. The user can still wrap it in a future with a lambda function.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I didn't even think about it because that's an implementation detail. I'd argue that mixing request/reply (which are synchronous) with pub/sub receivers or even receivers from different servers doesn't look like a good idea. I don't know about the implementation details from zmq, but isn't the call to receive the reply blocking? Can you still poll multiple request sockets for the reply?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wrong thread, I guess. It is not an implementation detail. Client::request() is async, and Client::receive() is the sync point. In the sync point the app handles the reply from the server, which is symmetric with the subscription handling.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't have a strong opinion here. I was just wondering if it could be more natural with the future as Raphael suggested. Right now it looks quite symmetric with the pub, sub, but error handling still looks clunky this way. Callback oriented APIs normally have hooks also for errors.
doc/feature/ReqRep.md
Outdated
{ | ||
ConstURIPtr uri = livre::URI::create(data, size); | ||
if( openAndCacheDatasource( *uri )) | ||
return dataSource.getVolumeInfo().toBinary(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The binary buffer returned will be moved or copied?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
shared, Serializable::Data has a shared_ptr + size.
doc/feature/ReqRep.md
Outdated
routing to a function. This requires that the client and server use the same | ||
object and version for this, i.e., the hard versioning of ZeroBuf wrt member | ||
layout is lost. The ID is not part of the ZeroBuf, so would need separate | ||
transport and checking in server/client. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see, this "answers" my first comment.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The first comment from my POV does not make sense. Which one do you mean?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
BTW: Would be happy to get KISS ideas on how to resolve this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My first comment is the first comment from my review, which even from your point of view is unique because it's at the very top.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess it's the one at the very bottom!?:
Using a future and a template...
See discussion there.
@@ -1,5 +1,5 @@ | |||
|
|||
/* Copyright (c) 2014-2016, Human Brain Project | |||
/* Copyright (c) 2014-2017, Human Brain Project |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good that you renamed it, I never liked the name if this file.
Why not call get, request? That name doesn't imply there's a return value at all. Using a future and a template function would allow to introduce some type safety at least in the API. Even if type-safety is not implemented initially, it could be implemented later. The future also provides a natural way of propagating exceptions from the server and allows proper handling in the client. Right now it's not clear at all how errors are handled. |
Good idea. Will change. |
Some, but not all. The bigger issue is that the reply object changes memory layout and we get a client-server mismatch. Since it would still be the same class, a future would not catch (ha!) this. The reply type id needs to be part of the protocol. |
The future forces the client to tell the implementation which is the expected type, the implementation still needs some mechanism to gurantee that (which at the moment is missing). But since it also depends on changing the protocol, it's not so different from the current proposal in terms of guarantees anyway. |
The implementation should not care about the type, but about the type id which is a stronger guarantee and the only one used in other parts of the API. One gains exactly nothing with the template method, because even when using the concrete type it does not prevent sending another type from the server. Note that the sync API in the first iteration used |
reply typing addressed in a1193a7 |
See https://github.com/eile/ZeroEQ/tree/reqrep for WIP |
Ping: What's left to discuss here? |
* @throw std::runtime_error if ZeroConf is not available or if session name | ||
* is invalid | ||
*/ | ||
ZEROEQ_API Subscriber(const URI& uri, const std::string& session); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Needs version bump. we should already go to the version 1.0.0 now I guess.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done, to 0.9 for now. Also squashed commits in preparation of rebase-and-merge
I'll eventually need an approval... |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good for now, maybe we will reevaluate some details after having had some real world experience with the API
No description provided.