Skip to content
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

Docs: add a design document for the Client #78

Open
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

CuriousGeorgiy
Copy link
Member

This patch adds a document that states the requirements and use cases we have in mind for the Client, and then proposes a design that fulfills them.

docs/client-design.md Outdated Show resolved Hide resolved
docs/client-design.md Outdated Show resolved Hide resolved
docs/client-design.md Outdated Show resolved Hide resolved
docs/client-design.md Outdated Show resolved Hide resolved
@CuriousGeorgiy CuriousGeorgiy marked this pull request as ready for review January 22, 2024 08:57
@CuriousGeorgiy CuriousGeorgiy removed the do not merge Not ready to be merged label Jan 22, 2024
docs/client-design.md Outdated Show resolved Hide resolved
Copy link
Collaborator

@drewdzzz drewdzzz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Will we have combinators (any, all, waitFor)?
  2. We've given up on template sockets?

docs/client-design.md Outdated Show resolved Hide resolved
docs/client-design.md Outdated Show resolved Hide resolved
docs/client-design.md Show resolved Hide resolved
docs/client-design.md Show resolved Hide resolved
docs/client-design.md Outdated Show resolved Hide resolved
docs/client-design.md Outdated Show resolved Hide resolved
docs/client-design.md Outdated Show resolved Hide resolved
* 3. The response has not already been retrieved previously.
*/
std::optional<Response> get_response();
};
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could create extract-once class:

  1. add isReady method
  2. set_callback should be rvalue method
  3. get_response -> get_result (result is actually variant<Error, Response>) - rvalue method
  4. reset_callback will be dropped. Anyway, I don't see any use cases for it.

Then, user cannot get response after callback is set and vice versa.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. add isReady method

I don't understand, why we do need this method, if we have get_status, which we could check for request readiness?

  1. set_callback should be rvalue method

This sounds like a cool solution. I suggest we keep the request object and pass it to the callback later on.

  1. get_response -> get_result (result is actually variant<Error, Response>) - rvalue method

I don't like this, since it would be nice to share the ConnectionError object between the connection and the requests (a variant does not allow to store references).

  1. reset_callback will be dropped. Anyway, I don't see any use cases for it.

Initially, I thought there might be some conditions when an application would want to remove the callback. But now I come to think that it is indeed redundant.

Copy link
Collaborator

@drewdzzz drewdzzz Jan 24, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could store a pointer to an error in variant instead of reference - it is private anyway.

Yeah, isReady is not needed.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now, you cannot do anything to the Request when callback is set, but you can access it when response is taken.

I won't resist to current design, but we could do the same for get_response (Result is needed for this purpose) to make Request completely safe. I'd leave it up to @alyapunov.

@CuriousGeorgiy
Copy link
Member Author

@drewdzzz

  1. Will we have combinators (any, all, waitFor)?

I don't see how we can implement them without access to the event loop. As far as I understand, @alyapunov just wanted something like the FanOut class I proposed.

  1. We've given up on template sockets?

Yeah, I see no value in this abstraction. We can add a typedef later on if we want to support other platforms like Windows.

@CuriousGeorgiy CuriousGeorgiy force-pushed the client-design-doc branch 2 times, most recently from e83db6a to 4231fbb Compare January 24, 2024 11:51
* 3. The response has not already been retrieved previously.
*/
std::optional<Response> get_response();
};
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now, you cannot do anything to the Request when callback is set, but you can access it when response is taken.

I won't resist to current design, but we could do the same for get_response (Result is needed for this purpose) to make Request completely safe. I'd leave it up to @alyapunov.

* Tarantool instance.
*/
template<class IOEventProvider>
class Connection {
Copy link
Collaborator

@drewdzzz drewdzzz Jan 24, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will we have different classes for a plain connection and SSL connection?

Copy link
Member Author

@CuriousGeorgiy CuriousGeorgiy Jan 29, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, forgot about this detail. I guess we could either add a StreamProvider template parameter or we could use dynamic polymorphism. In the first case the user won't be able to store different connections in one container. Not sure what's best here. @alyapunov

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can do both if we add a StreamProvider. In general overview:

  1. UnixPlainStream and UnixSSLStream for users who doesn't need both SSL and Plain streams at once.
  2. New UnixDynamicStream for others - there is already transport field in ConnectOpts, we will just check it in connect, send and recv.

Thus, only users who need both connection types pay the performance price.

*/
tnt::List<Request> collect();
};
```
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

About any, all, waitFor.

I don't see how we can implement them without access to the event loop.

When IOEventProvider fires the connector callback, it reads the data from socket and then places it somewhere (creating a Response).
At this moment, we could fire combinator any, associated with the request, or bump and check counter for combinator all.

What's about waitFor, we could just check deadlines for all combinators on every callback from IOEventProvider.

Our combinators will be faster then user-created ones, since we can manipulate with intrusive lists of Requests under the hood, and user will have to use callbacks.

Copy link
Member Author

@CuriousGeorgiy CuriousGeorgiy Jan 29, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's about waitFor, we could just check deadlines for all combinators on every callback from IOEventProvider.

I believe we discussed that we don't want to implement timers, because the user has more precise time information than we do.

At this moment, we could fire combinator any, associated with the request, or bump and check counter for combinator all.

What interface do you propose for these combinators? It seems like this will create a lot of additional overhead (i.e., the connection objects will have to manage the combinator objects).

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see many overhead. There will be a snippet somewhere in the connector:

if (req.hasCallback())
    req.cb();
else
    req.setResponse();

With combinator, it will turn into switch case:

switch (req.ret_type) {
    case COMBINATOR:
        /** Can be one function - is separated for clarity. */
        combinators[req.combinator_id]->list.insert(req);
        combinators[req.combinator_id]->checkReady();
    case CALLBACK:
        req.cb();
    case RAW:
        req.setResponse();
}

Combinator is just a list of ready requests with methods setCallback, isReady, getResponse - just like an usual request, but Response can be an array of responses (for all combinator, for example).

There will be no overhead for users who don't use combinators.

What's about waitFor, let's drop it - it can be implemented later if it will be needed - we will need to check time on every response.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As for me, we could design efficient combinators while we are here (they are efficient because they use intrusive lists instead of callbacks). But it's just my opinion, so I can leave this decision to @alyapunov.

* Return a list of ready request, and cancel the requests for which a
* response is not available.
*/
tnt::List<Request> collect();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this solution of myTarget's problem? If so:

  1. You mustn't cancel requests here.
  2. Passed requests must be dynamic: you poll for ready requests, then you create a bunch of new requests and poll them along with the previous ones.
  3. From the previous point - you must allow to populate the list of requests.
  4. Need to come up with a better name - it is a tool polling for ready request, not for "FanOut" requests.

P.S. The idea was to create something like epoll - you ask for ready requests and get only them (no need to scan for all requests and find ready ones), but all not ready requests must be still available.

Copy link
Member Author

@CuriousGeorgiy CuriousGeorgiy Jan 29, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps I still don't understand the problem statement. AFAIC, there is no dynamic scenario, we just want to create a bunch of synchronization points (i.e., collect points).

In the scenario you propose the mapping between the business logic and the responses is not clear. Let's say I have requests about a VK and Mail.ru profile mixed up (which correspond to 2 different Tarantool instances and 2 different connections). After collecting the mixed up responses, how I am going to understand what the sources of the requests were?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll explain my understanding from the beginning.

We have myTarget's problem - collect as much information as we can in a limited amount of time, for example, 1 second. We have only anonymous user id, so, firstly we send a bunch of requests to Tarantool to get all actions with this id (email authorization, visiting book page on litres and so on). When we have received data from mail.ru email, we can send a new bunch of requests to Tarantool storing mail.ru data (name, surname, age, etc). When we have received data from litres with user's books ids, we can send another bunch to Tarantool storing litres data (we want to find book's genre by its id). After 1 second all the requests are canceled, and myTarget knows that the guy with session_id = 143 is actually Ivan Ivanov who is keen on Steven King books.

Firstly, I just can't implement such service with this FanOut. For example, I call collect after 0.1s, and no responses are ready - the job has died even before it started. So, we must not cancel requests that are not ready.

Secondly, I have many different request types (search by anonymous id, mail.ru, litres and so on). I see only two solutions:

  1. We will have a lot of FanOuts - one per request type for every job (collecting info by id): N*M combinators.
  2. We will have something like epoll - the combinator will associate a value with every request. It can be void * or a template parameter of the combinator. Then, every job will wake up every 0.05s, for example, and call single collect method for all request types. Here we will use only N combinators.

I would prefer the second variant.

Thirdly, let's imagine that we have access to Tarantool storing Delivery Club order history. Now myTarget knows my user id in Delivery Club and wants to fetch all my orders. By user id it fetches a lot of reponses with order id. Then, by order id, it fetches information about food to find out about my food preferences. If we decide to have a FanOut per request type, we will need an opportunity to append new requests resolving order_id into its contents - collection of requests of this type is filled dynamically.

@drewdzzz drewdzzz assigned CuriousGeorgiy and unassigned drewdzzz Jan 24, 2024
Add a document that states the requirements for the Client, and then
proposes a design that fulfills them.
@CuriousGeorgiy CuriousGeorgiy removed their assignment Jan 29, 2024
@drewdzzz drewdzzz assigned CuriousGeorgiy and unassigned drewdzzz Feb 7, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants