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

Tutorial for writing a service with an asynchronous client node #4903

Open
wants to merge 9 commits into
base: rolling
Choose a base branch
from

Conversation

santiago-tapia
Copy link

This PR is connected to another PR at example repository: Async Service Example.

This PR includes an intermediate tutorial to write an asynchronous client that sends a request inside a callback. The key feature about the example is that the node execution is not waiting in any where. It is just executing callbacks or spinning, all time.

Besides, it includes some experiments on executing the client in combination with a delayed server, those experiments are designed to help in the understanding of the concept of asynchronism.

Signed-off-by: Santiago Tapia-Fernández <[email protected]>
Signed-off-by: Santiago Tapia-Fernández <[email protected]>
Signed-off-by: Santiago Tapia-Fernández <[email protected]>
For information on how to write a basic client/server service see
:doc:`checkout this tutorial <../../Beginner-Client-Libraries/Writing-A-Simple-Cpp-Service-And-Client>`.

**All** service **clients** in ROS2 are **asynchronous**. In this tutorial, an example
Copy link
Collaborator

@kscottz kscottz Dec 16, 2024

Choose a reason for hiding this comment

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

Suggested change
**All** service **clients** in ROS2 are **asynchronous**. In this tutorial, an example
**All** service **clients** in ROS 2 are **asynchronous**.
In this tutorial, an example

Copy link
Collaborator

Choose a reason for hiding this comment

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

@santiago-tapia we usually do one sentence per line and ROS 2 is always stylized ROS 2.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I wouldn't assume all readers fully understand what is meant by "asynchronous." We get a lot of undergraduate students reading the docs who may not have reached that point in their education. It would help readability to quickly define your terms and then move on to discussing everything else.

the response, meaning its thread is not running but blocked until the
response is returned.

In contrast, the following diagram illustrates an asynchronous ROS2 client:
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
In contrast, the following diagram illustrates an asynchronous ROS2 client:
In contrast, the following diagram illustrates an asynchronous ROS 2 client:

:alt: A sequence diagram that show how a sync-client waits for the response

It shows a **synchronous** client. The client makes a request and waits for
the response, meaning its thread is not running but blocked until the
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
the response, meaning its thread is not running but blocked until the
the response, meaning its thread is blocked (i.e. not running) until the

Comment on lines 50 to 51
Of course, it could be stopped by invoking a waiting function or another
blocking function, but this diagram shows a ROS2 client, and in ROS2, to receive
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
Of course, it could be stopped by invoking a waiting function or another
blocking function, but this diagram shows a ROS2 client, and in ROS2, to receive
Of course, it could be stopped by invoking a waiting function or another
blocking function, but this diagram shows a ROS 2 client, and in ROS 2, to receive

ll 50-53 is a bit of a run-on sentence. I suggest breaking this up into two sentences.

(the client is *activated*), and afterward, the response is received, and the
client executes the callback for the response.

Since any service client in ROS2 needs to be spinning to receive the server's
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
Since any service client in ROS2 needs to be spinning to receive the server's
Since a service client in ROS 2 needs to be spinning to receive the server's

client executes the callback for the response.

Since any service client in ROS2 needs to be spinning to receive the server's
response, all clients in ROS2 are asynchronous by design (they cannot simply
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
response, all clients in ROS2 are asynchronous by design (they cannot simply
response, all clients in ROS 2 are asynchronous by design (they cannot simply

In :doc:`Writing a Simple cpp Service and Client <../../Beginner-Client-Libraries/Writing-A-Simple-Cpp-Service-And-Client>` you
learned how to define a custom service that adds two ints, we will use that service again.

The source code in this tutorial is at `rclcpp examples <https://github.com/ros2/examples/tree/{REPOS_FILE_BRANCH}/rclcpp/services>`,
Copy link
Collaborator

@kscottz kscottz Dec 16, 2024

Choose a reason for hiding this comment

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

Suggested change
The source code in this tutorial is at `rclcpp examples <https://github.com/ros2/examples/tree/{REPOS_FILE_BRANCH}/rclcpp/services>`,
The source code for this tutorial is located at `rclcpp examples <https://github.com/ros2/examples/tree/{REPOS_FILE_BRANCH}/rclcpp/services>`.

learned how to define a custom service that adds two ints, we will use that service again.

The source code in this tutorial is at `rclcpp examples <https://github.com/ros2/examples/tree/{REPOS_FILE_BRANCH}/rclcpp/services>`,
you might get the code there directly.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
you might get the code there directly.

delay in responding to requests. Nevertheless, it could
serve as an example if the delay is removed.

Actually, there are no particularly relevant elements here. The main
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 you could simplify this quite a bit. Just say that you've created a server with an arbitrary and artificial delay..

of a service client is analyzed to provide insight into this fact, explaining
the reasons for its asynchronous nature and some of the timing-related
consequences.

Copy link
Collaborator

Choose a reason for hiding this comment

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

There's a saying in technical writing that you should, "tell people what you are going to do, tell them what to do, and then tell them what they did."

It may help readability if you preface this tutorial with that you are going to do. Perhaps something like, "In this tutorials we are going to create A and B, and use it evaluate the impact of C on D."

also be used to track the *state* of the request if necessary.

The second callback is for receiving the server response. Note that,
being a callback, it will be executed at the spinning thread. The code is
Copy link
Collaborator

@kscottz kscottz Dec 16, 2024

Choose a reason for hiding this comment

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

"at the spinning thread"

This phrasing is a bit unclear to me. Perhaps something like, "Note that since this function uses a callback, it will be executed promptly by the node's main thread while still allowing the node to accomplish other tasks."


.. note::

Compared to the code of a hypothetical synchronous client, the key
Copy link
Collaborator

Choose a reason for hiding this comment

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

This block, 412-416 could use a bit of revision. It might help to state plainly what the code does, and then contrast with the alternative version.

Discover available components
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

To see what packages that contains *examples_* are registered and available
Copy link
Collaborator

@kscottz kscottz Dec 16, 2024

Choose a reason for hiding this comment

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

Suggested change
To see what packages that contains *examples_* are registered and available
To see what packages contain the term *examples_* are available


ros2 pkg list | grep examples_

The terminal will show a list of packages from ros2_examples, actually,
Copy link
Collaborator

Choose a reason for hiding this comment

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

This could be shortened. Try to keep it simple for the new users. 😉

Copy link
Collaborator

@kscottz kscottz left a comment

Choose a reason for hiding this comment

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

@santiago-tapia thanks for sending us this pull request! I think this contribution could be very helpful for new ROS users learning how to build their first asynchronous client / server pair.

I am going to need you to do a few things so we can help you get this approved.

First, by convention, we request that each sentence gets it own line in your pull request. The reason we request this is that it makes it much easier for us to suggest changes in the file. There were a few places I wanted to make a suggestion but it was difficult to do because of the line breaks. Also, by convention, we stylize it "ROS 2" when written in the docs.

In terms of code I didn't see anything wrong with the pull request. However, the tutorial starts out a little weak, but finishes strong (see my notes at the start of the file). Please keep in mind the audience for the ROS docs are students and developers across the globe, so the shorter and simpler you can make your sentences the better.

If you could address the line break issue in the PR I can make another pass. Once you address those suggestions and we get a second review we can you merged into the docs. Thanks!

@santiago-tapia
Copy link
Author

@kscottz, Thank very much for your comments.

I am going to try to solve the issues you mentioned. Just to confirm:

  • One sentence per line (without limit to line length?)
  • Replace ROS2 with ROS 2
  • Make an introduction on Asynchronism at the beginning of the tutorial.
  • Other fixes in concrete sentences in the comments above.
  • Improve sentences to make them shorter and more concise.

I was wondering if it could be interesting to make a "Concept" article about *asynchronism" and other related concepts. This way the explanations here could be shorter since the reader could find more details somewhere else.

It will take me a while because I have other urgent matters, but I am going to do it as soon as possible.

@kscottz
Copy link
Collaborator

kscottz commented Dec 17, 2024

One sentence per line (without limit to line length?)

Yep. Generally let your editor deal with line wrapping. Using this approach makes suggestions a whole lot easier on our side.

Replace ROS2 with ROS 2
Make an introduction on Asynchronism at the beginning of the tutorial.
Other fixes in concrete sentences in the comments above.
Improve sentences to make them shorter and more concise.

That sounds about right. Don't work too hard on shortening sentences, I can lend a hand with editing once we have the one sentence per line fixed. One other thing to keep in mind is that our linter is really picky. It tends to complain about trailing whitespace at the end of a line, so I would recommend making whitespace visible in your editor if you can. I can spot fix a few things but it really sucks if every line has an extra space or two at the end.

It will take me a while because I have other urgent matters, but I am going to do it as soon as possible.

No worries! It is the holidays. We really appreciate your contribution to the documentation!

I was wondering if it could be interesting to make a "Concept" article about *asynchronism" and other related concepts. This way the explanations here could be shorter since the reader could find more details somewhere else.

I would be open to it but I think it would help if we were all on the same page before you put a lot of effort into it. What I would recommend is that you file an issue and describe what you want to do with a detailed outline. We can discuss the outline before you sink a lot of effort into writing.

Thanks for being flexible about the idiosyncrasies in our docs contribution process.

Comment on lines 1 to 4
.. redirect-from::

Async-Service
Tutorials/Async-Service
Copy link
Collaborator

Choose a reason for hiding this comment

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

this is whole new section, why we need this redirection?

it specifies one or more alternate URLs from which the page can be redirected. It's useful for maintaining backward compatibility when documentation pages are reorganized or renamed.

Comment on lines 22 to 23
For information on how to write a basic client/server service see
:doc:`checkout this tutorial <../../Beginner-Client-Libraries/Writing-A-Simple-Cpp-Service-And-Client>`.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
For information on how to write a basic client/server service see
:doc:`checkout this tutorial <../../Beginner-Client-Libraries/Writing-A-Simple-Cpp-Service-And-Client>`.
For information on how to write a basic client/server service see :doc:`Writing a Simple Service and Client <../../Beginner-Client-Libraries/Writing-A-Simple-Cpp-Service-And-Client>`.

Server --> Client : Return Response
deactivate Server

@enduml
Copy link
Collaborator

Choose a reason for hiding this comment

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

line feed is required in the last?

Suggested change
@enduml
@enduml

@santiago-tapia
Copy link
Author

Hi again,

I have reviewed the whole tutorial according to the comments from @kscottz and @fujitatomoya.

I am afraid it must be very difficult to track the changes between my previous commit and this last commit because there are lots of changes. That is because I have written every sentence in a line, and, besides, I have reviewed almost every sentence to try to simplify the statements (just, please, remember that English is not my native language and that is not my fault...).

I have also reworked the introduction to say what the reader is going to do and to make a short explanation about asynchronism. That means I have moved some paragraphs and rewritten some others. I have also corrected some other minor details like code indentation. Of course I have replaced ROS2 with ROS 2.

This is my last commit, there is another commit but it is just a merge commit due to a fork synchronization, since this tutorial is completely new, it is not relevant.

Regards,

Santiago

Copy link
Collaborator

@fujitatomoya fujitatomoya left a comment

Choose a reason for hiding this comment

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

To be honest, i am not really convinced to take this example in the documentation... i would like to get more feedback from the community.

Comment on lines 1 to 20
@startuml
participant Client
participant Server

Client -> Server : Make Async Request
note left of Client : Client is spinning
activate Server
note right of Server : Server is running the callback
--> Client : Incoming Event A
activate Client
deactivate Client
Server --> Client : Return Response
deactivate Server

activate Client
note left of Client : Client is running receive callback
Client -[#white]> Client :
deactivate Client

@enduml
Copy link
Collaborator

Choose a reason for hiding this comment

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

maybe we can use mermaid instead? ros2 doc already has the extension which renders the mermaid in the documentation without having the image files.

something like the following?

sequenceDiagram
    participant Client
    participant Server

    Client ->> Server: Make Async Request
    Note left of Client: Client is spinning
    activate Server
    Note right of Server: Server is running the callback
    Server ->> Client: Return Response
    deactivate Server

    activate Client
    Note left of Client: Client is running receive callback
    deactivate Client
Loading

Copy link
Author

Choose a reason for hiding this comment

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

Done

@@ -0,0 +1,11 @@
@startuml
Copy link
Collaborator

Choose a reason for hiding this comment

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

same here, it would be easier to maintain if we can use mermaid for sequence.

Copy link
Author

Choose a reason for hiding this comment

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

Done

Comment on lines 35 to 37
.. image:: images/sync-client-diagram.png
:target: images/sync-client-diagram.png
:alt: A sequence diagram that show how a sync-client waits for the response
Copy link
Collaborator

Choose a reason for hiding this comment

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

this is where mermaid diagram can be rendered instead of image insertion.


The delay in the server will make it easier to observe client activity; that activity is the difference between synchronous and asynchronous requests.

A **synchronous** client is idle (i.e. **doing nothing**) after making a request.
Copy link
Collaborator

Choose a reason for hiding this comment

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

i am not sure if this is the correct description. IMO there is no such thing like sync client or async client, it is more like how client wants to send the request either async or sync manner? maybe we can ignore this.

Copy link
Author

Choose a reason for hiding this comment

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

I don't think async or sync concepts have a wide and standard definition for every possible context. But, well, I also think there is consensus about that a synchronous request freezes the execution in the client until the response is received.
I don't think "ignore it" is a good idea. People should know what they are doing.

Copy link
Author

Choose a reason for hiding this comment

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

I don't think async or sync concepts have a wide and standard definition for every possible context. But, well, I also think there is consensus about that a synchronous request freezes the execution in the client until the response is received.
I don't think "ignore it" is a good idea. People should know what they are doing.


.. attention::

**All** service **clients** in ROS 2 are **asynchronous**.
Copy link
Collaborator

Choose a reason for hiding this comment

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

then why we are introducing sync client here above?

and this is true, there is no such API for sync request for the service. rclpy actually has sync one that is why we have https://docs.ros.org/en/rolling/How-To-Guides/Sync-Vs-Async.html.

now i think the explanation above can be moved to https://docs.ros.org/en/rolling/How-To-Guides/Sync-Vs-Async.html.

what do you think?

Copy link
Author

Choose a reason for hiding this comment

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

To your first question: As I said before, people should know what they are doing and what they can or can not do (should do or should not do). Understanding async request could be explained by showing the difference with sync requests. That is even more important in this case because as far as I know, in ROS 1 service request were sync.

An additional comment in this matter: even if there is an interface for doing a sync request in python, I think it is only in apparence, as I said somewhere it is not possible to send a truely sync request because ROS 2 has to spin to receive the response => the program is not idle => it is not sync.

To your second question: Well, we could. But regarding the sync api... Well, in my very personal and humble opinion that api should be avoided at all cost (Something that has in the first two paragraphs of its description the words: "risks", "deadlock" and "pitfalls" should be avoided)... Even more I think the origin of all these "risks", "pitfalls" is trying to convert something that is essentially async into sync.

That could be the subject of a very thoughfull discussion and this is not the appropiate forum. Anyway, that is my reason for not contribuiting there.

Copy link
Contributor

Choose a reason for hiding this comment

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

as I said somewhere it is not possible to send a truely sync request because ROS 2 has to spin to receive the response => the program is not idle => it is not sync.

This isn't really true. It is definitely subtle, but if you spin in one thread, and do a client.call in another thread, then it can be (and often is) the case that both threads are idle until the response to the client comes in. In that case, it truly is "synchronous" in the thread that is calling client.call.

That said, my personal opinion is that the rclpy client.call API was a mistake, and we never should have added it. If you understand what you are doing, and you truly want to do what client.call does, the equivalent code is:

future = client.call_async()
rclpy.spin_until_future_complete(future)

Copy link
Author

Choose a reason for hiding this comment

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

Thanks @clalancette for your comment.
Agreed, definitely is subtle. I was not considering multi-threading. And if you think about these issues deeper, you might find this question interesting: why to launch a thread just to keep it on hold?
And agreed again, actually I would mark that api as "deprecated" to discourage its use. But that is another discussion.

@@ -0,0 +1,563 @@
.. Tutorials/Intermediate/Async-Service
Copy link
Collaborator

Choose a reason for hiding this comment

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

why do we need this redirection? since this is new doc, can we remove this?

Copy link
Author

Choose a reason for hiding this comment

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

Of course.

Copy link
Author

Choose a reason for hiding this comment

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

Done

Comment on lines +188 to +189
This package is a service server with an arbitrary and artificial delay in responding to requests.
It should not used unless the delay is removed.
Copy link
Collaborator

Choose a reason for hiding this comment

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

i do not really understand why this delayed service server is required to explain this? if the client waits on the request in the callback, that will be deadlock regardless the server implementation. why dose this delayed server needs to be introduced as example?

Copy link
Author

Choose a reason for hiding this comment

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

Well, I was already sure about the relevance of this tutorial, but reading your comments I am even more sure.

There is NOT such deadlock. An async client is NOT waiting, idle or hold, it is spinning normally. I am not sure if it is clear enough, it is said in this way: "It is non-blocking, meaning it returns almost immediately without stopping the execution of the thread." And it is also said that: "the best approach is to allow the callback to finish" and that is what it is done in the example. Of course, you can not get your response until that response is computed and received, but that is not a deadlock. A basic principle for a framework like ROS 2 is "when event A occurs Callback_A will be executed", here the event is "receiving the response", so an async client is based on "when response A is received, Callback_A will be executed". Mind that I say "will" and not "is" because Callback_A will be executed as soon as other callbacks are resolved (depends on the executor scheduler).

On the other hand, I am worried, apparently you have not understood the actual concepts I am trying to explain and I am not sure on how to handle that. Besides, I don't understand your problem with the delayed server, if I use a server example that makes one million of matrix inversions, will it be an acceptable example?

Honestly, I will not be able to work in this matter much longer, so, please, tell me if changing the uml diagrams to mermaid will be enough because I will only do that if you are going to accept the PR.

Copy link
Collaborator

Choose a reason for hiding this comment

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

@santiago-tapia thanks for being patient and iteration.

Ah sorry i was meaning the deadlock case but that is wrong since you used async_send_request in the topic callback.

what i mean here is, do we really need to have this delayed service to explain this tutorial in addition to topic callback? i would just use timer callback to send the async request to the already existed https://github.com/ros2/examples/tree/rolling/rclcpp/services/minimal_service that also explains the async example that you are trying to explain here? it would be much less code and maintenance cost effective, does that make sense? (this also goes to ros2/examples#398)

please, tell me if changing the uml diagrams to mermaid will be enough because I will only do that if you are going to accept the PR.

i cannot guarantee this, sorry.

Copy link
Author

Choose a reason for hiding this comment

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

@fujitatomoya, thanks again for your comment, and I apology if I sound impatient yesterday. Let's keep positive.

All right, I will change the diagram to mermaid.

And now, I will try to explain why "delayed server" is necessary to explain async.

First, I would like to mention possible use case for services: querying a data base, do some image processing, make some slam computation, etc. With respect to the computing time to produce a response they have in common two features: the computation time is not immediate and it is not predictable. So the first reason to make "delayed server" is including in the example an emulator of a non-trivial service server (at least it will not produce a response immediately).

Second, the whole tutorial is about understanding async service calls and its differences from sync calls (sync calls in general, as I say, there is no such a thing in ROS 2 due to its design). Even if there might be some subtle details according to the context, I think we could agree that:

  1. An async call returns immediately.
  2. So It can not have the response as an actual returned result (that result is in the "future").
  3. There must be some language / framework mechanism for retrieving the response in the "future".
  4. The thread keeps running immediately at the next sentence.

All of these features are important, and should be understood. The role of "delayed server" is to introduce an observable time interval between the moment the async request is sent and the moment in which is received. And it should help in understanding the previous points because:

a) The node is capable of processing another topic callback before it gets the response from the server (in single thread). That confirms 1) and 4). By the way, I might add a LOG just before ending the callback to make it more explicit. The delay here is absolutely necessary because we need "plenty" of time to produce an incoming message to the client node. This also illustrates why a deadlock is impossible, the node is not waiting or hold in any way (apart from "waiting" inside the spin method).
b) The node is also capable of sending two requests in a row and afterwards retrieving the two responses without problem. This explains why callbacks in 3) are superior in retrieving responses over other alternatives (for instance to waiting). The role of the delay here is to have "plenty" of time to send the second request without receiving the response to the first one.

I think that's all. Please, just tell me if it is clear enough now.

Best

Copy link
Collaborator

Choose a reason for hiding this comment

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

I apology if I sound impatient yesterday

no worries.

the computation time is not immediate and it is not predictable.

those are what i meant. what do you mean by immediate? that all depends on application and use cases, i think.
and my point here is not predictable, i mean server behavior or implementation is technically irrelevant to what you are trying to explain here on client async behavior.
this is the reason that i still think we can keep this only with client async example by calling async request via timer callback.

make "delayed server" is including in the example an emulator of a non-trivial service server

i think just adding sleep in the server process can be still trivial.

All of these features are important, and should be understood.

agree, but again all these are client side behavior, not relevant to server implementation.

after all, i still think we would not want to maintain delayed server as example here.
i would recommend more fundamental and simplified tutorial how to handle the async request for the server via user callback of the entity such as timer.
i think that is easier to understand, reusable and primitive for developers and users.

and i do understand you are trying to explain this with your expectation for the application, but that is my opinion.
we can keep this open for more comments to see either way we proceed.

Signed-off-by: Santiago Tapia-Fernández <[email protected]>
Signed-off-by: Santiago Tapia-Fernández <[email protected]>
@santiago-tapia
Copy link
Author

Hi again:

I have converted the plantuml to mermaid and removed the initial useless reference.

I would recommend including a note about the diagram format in the contributing file. I would also recommend saying that the documentation should be written using a line for each sentence even if this produces lines too long. 

Following our conversation on Delay Server: although it was not my original plan, I am going to make another PR to modify the minimal client service. In that PR, I will include the suggestions from @fujitatomoya about using a timer for sending async requests. 

But, I am afraid I have to insist on the convenience of this tutorial and the importance of the Delay Server. Just a note on its importance: the whole section "3 Run the asynchronous client" should be rewritten because it will be impossible to get that particular sequence of events.

@kscottz
Copy link
Collaborator

kscottz commented Feb 3, 2025

@santiago-tapia good catch on saying we should make our requests clear!

I added this clarification at your request.

Thanks for being patient.

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.

4 participants