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

Intermediate course edits #226

Merged
merged 2 commits into from
Apr 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/03-intermediate/01-course-content.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ hide_table_of_contents: true

# Course content

This course serves as a continuation of the basic course and delves deeper into the implementation of programs using Gear technologies. Specifically, it focuses on how programs can communicate with each other, receive and process requests, and handle scenarios where one party does not respond.
This course serves as a continuation of the [basic course](https://academy.gear.foundation/courses/basic_course), delving deeper into the implementation of programs using [Gear](https://gear-tech.io/) technologies. Specifically, it focuses on how programs can communicate with each other, receive and process requests, and handle scenarios where one party does not respond.

The course material is structured into three sections:

Expand All @@ -22,6 +22,6 @@ The course material is structured into three sections:

3. Handling of Delayed Messages:
- In-depth exploration of delayed messages;
- Resolution of issues related to lack of response through the use of delayed messages;
- Resolution of issues related to the absence of responses through the use of delayed messages;
- Testing of the program featuring delayed messages.

49 changes: 25 additions & 24 deletions docs/03-intermediate/02-message-receiving/handle_reply.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,22 @@ hide_table_of_contents: true

# Message receiving

In this lesson, you will learn how a program can efficiently handle request messages. We will illustrate this concept with an example of interaction between two programs. Before delving into the analysis of the program code, it is useful to illustrate the operation of our programs schematically.
In this lesson, you will learn how a program can efficiently handle request messages. This concept will be illustrated through an example of interaction between two programs.

Before analyzing the program code in detail, it will be helpful to first present a schematic overview of how Gear programs operate:

![gif 1](../img/02/handle_reply.gif)

1. The user sends some `Action` message to program №1, which is processed by `handle()`;
2. This message is then passed to programme №2;
3. Programme №1 sends an `Event` message to the user indicating that the message was successfully passed to Programme №2;
4. Programme №2 receives the message from Programme №1 processes it and responds;
5. Programme №1 receives the reply message from Programme №2 via the `handle_reply()` entry point;
6. Finally, from the `handle_reply()` function, send the message to the user.
1. The user sends an `Action` message to Program #1, which is processed by the `handle()` function.
2. This message is then passed to Program #2.
3. Program #1 sends an `Event` message to the user, indicating that the message was successfully passed to Program #2.
4. Program #2 receives the message from Program #1, processes it, and responds.
5. Program #1 receives the reply message from Program #2 via the `handle_reply()` entry point.
6. Finally, from the `handle_reply()` function, Program #1 sends the response to the user.

## First program

The task of the first program is to communicate with the second program, so the following structure will be needed:
The primary task of the first program is to communicate with the second program, requiring the following structure:

```rust
struct Session {
Expand All @@ -27,7 +29,7 @@ struct Session {
}
```

The following actions and events will also be necessary to simulate a dialogue between programs:
The following actions and events will be necessary to simulate a dialogue between the programs:

```rust
#[derive(TypeInfo, Encode, Decode)]
Expand All @@ -52,7 +54,7 @@ pub enum Event {
}
```

During initialization, it is necessary to pass the address of the second program:
During initialization, it is necessary to pass the address of the second program.

```rust
#[no_mangle]
Expand All @@ -69,9 +71,9 @@ extern "C" fn init() {

Let's focus on processing requests in the `handle()` function:

1. Receive the message with the function `msg::load()`;
2. Send a message to the second program using the `msg::send()`;
3. An important step is to store the identifier of the message returned by `msg::send()`, allowing the `handle_reply()` function to identify which message received a response;
1. Receive the message with the `msg::load()` function.
2. Send a message to the second program using `msg::send()`.
3. An important step is to store the identifier of the message returned by `msg::send()`. This allows the `handle_reply()` function to identify which message received a response.
4. Finally, send a reply message indicating that the message was sent to the second program.

```rust
Expand All @@ -85,13 +87,13 @@ extern "C" fn handle() {
}
```

The Gear program handles the reply to the message using the `handle_reply()` function. Now, let's examine how to manage the response message from the second program:
The Gear program utilizes the `handle_reply()` function to handle replies to messages. Let’s delve into managing the response message from the second program:

1. Utilize the `msg::reply_to()` function to obtain the identifier of the message for which the `handle_reply()` function is invoked.
2. Verify that the message identifier matches the identifier of the message sent from the `handle()` function, ensuring that the response corresponds to that specific message.
3. Finally, send a reply message to the sender's address.
1. Use the `msg::reply_to()` function to retrieve the identifier of the message for which the `handle_reply()` function was invoked.
2. Ensure that the message identifier matches the identifier of the message sent from the `handle()` function. This step verifies that the response corresponds to the specific message sent earlier.
3. Finally, send a reply message to the original senders address.

**It is important to emphasize that calling `msg::reply()` inside the `handle_reply` function is not allowed.**
**It is crucial to note that calling `msg::reply()` inside the `handle_reply()` function is not permitted.**

```rust
#[no_mangle]
Expand All @@ -106,14 +108,13 @@ extern "C" fn handle_reply() {
}
```

Just a reminder that the sender of the message will receive two messages:
- The first message is sent from the `handle()` function to indicate that the message has been sent to the second program.
- The second message will come from the `handle_reply()` function with the response from the second program.

Just a reminder: the sender of the message will receive two messages:
- The first message, originating from the `handle()` function, indicates that the message has been forwarded to the second program.
- The second message, sent by the `handle_reply()` function, contains the response from the second program.

## Second program
## Second Program

The first program is straightforward; it can accept various variants of actions and respond to them with corresponding events. These responses can range from simple replies such as `Action::HowAreYou` and `Event::Fine` to more complex logic, such as generating a random number.
The first program is straightforward; it can accept various types of actions and respond with corresponding events. These responses can range from simple replies, such as `Action::HowAreYou` and `Event::Fine`, to more complex logic, such as generating a random number.

```rust
#![no_std]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,28 +5,29 @@ hide_table_of_contents: true

# Testing

Let's verify the functionality of the programs discussed in the preceding section by employing the `gtest` library, a subject you should be familiar with from the basic course.
Let's verify the functionality of the programs discussed in the preceding section by using the `gtest` library, which you should already be familiar with from the basic course.

The first thing to do is to create a testing environment:
1. Create a testing environment:
```rust
let system = System::new();
```

Get first program of the root crate with provided `system` and the second program instance from wasm file.
2. Retrieve the first program from the root crate using the provided `system` and the second program instance from the wasm file.

```rust
let first_program = Program::current(&system);
let second_program = Program::from_file(&system, "target/wasm32-unknown-unknown/release/second_program.opt.wasm");
```

Initialize the second program by sending an empty message and then initialize the first program by passing the address of the second program to it.
3. Initialize the second program by sending an empty message, and then initialize the first program by passing the address of the second program to it.

```rust
let result = second_program.send_bytes(USER, []);
let second_program_address: ActorId = SECOND_PROGRAM_ADDRESS.into();
let res = first_program.send(USER, second_program_address);
```

Send the message with `Action::MakeRandomNumber {range: 1}` to the first program and check for the response `Event::MessageSent`, which means that the message was successfully sent to the second program address;
4. Send a message with `Action::MakeRandomNumber { range: 1 }` to the first program and check for the response `Event::MessageSent`, indicating that the message was successfully sent to the second program's address.

```rust
let result = first_program.send(USER, Action::MakeRandomNumber {range: 1});
Expand All @@ -38,7 +39,7 @@ assert!(result.contains(&log));

```

Retrieve the user's mailbox with the specified ID and verify that a reply message has been sent back to the user
5. Retrieve the user's mailbox with the specified ID and verify that a reply message has been sent back to the user.

```rust
let mailbox = system.get_mailbox(USER);
Expand Down Expand Up @@ -101,4 +102,4 @@ fn success_test() {
}
```

It will be good practice if you implement these programmes and test on your own.
It will be beneficial for you to implement these programs and test them on your own.
47 changes: 24 additions & 23 deletions docs/03-intermediate/03-wait-wake-system/handle-reply-wait-wake.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,17 @@ sidebar_position: 2
hide_table_of_contents: true
---

# Handle reply with wait() and wake()
# Handle Reply with wait() and wake()

Now let's use the knowledge about `exec::wait()`/`exec::wake()` functions and try to improve the program that was shown in the previous lesson.
Now, let's apply understanding of the `exec::wait()`/`exec::wake()` functions to enhance the program introduced in the previous lesson:

![gif 2](../img/03/wait_wake.gif)

As you can see, the user will no longer receive two separate messages; instead, a single reply will be sent at the end of the entire process.
The user will now receive a single reply at the end of the entire process, instead of two separate messages.

## First program
## First Program

As the second programme remains unchanged, let's proceed directly to examining the modifications in the first program:
Since the second program remains unchanged, let's take a closer look at the changes in the first program:

```rust
type MessageSentId = MessageId;
Expand All @@ -25,11 +25,12 @@ struct Session {
message_status: MessageStatus,
}
```
New fields have been created
- `msg_ids` — a tuple consisting of two elements: MessageSentId and OriginalMessageId;
- `MessageSentId` - identifier of the message to be sent to the second program address;
- `OriginalMessageId` - identifier of the message to be sent to the first program (required for using the wake() function).
- `message_status` - session status (required to track session activity stages).

New fields have been introduced:
- `msg_ids` — a tuple consisting of two elements: `MessageSentId` and `OriginalMessageId`;
- `MessageSentId` is the identifier of the message sent to the second program's address.
- `OriginalMessageId` is the identifier of the message sent to the first program (required for using the `wake()` function).
- `message_status` - the session status (required to track the stages of session activity).

```rust
enum MessageStatus {
Expand All @@ -38,11 +39,12 @@ enum MessageStatus {
Received(Event),
}
```
- `Waiting` — the session is in a waiting state;
- `Sent` - the intermediate state of the session in which the message was sent to the second program, but the response has not yet been received;
- `Received(String)` - the session state in which the reply message was received.

Considering the new fields in the program structure, initialization appears as follows:
- `Waiting` — the session is in a waiting state.
- `Sent` - the intermediate session state in which the message was sent to the second program, but the response has not yet been received.
- `Received(String)` - the session state when the reply message has been received.

With these new fields in the program structure, initialization is as follows:

```rust
#[no_mangle]
Expand All @@ -58,7 +60,7 @@ extern "C" fn init() {
}
```

This time, let's incorporate debugging into our program to gain a comprehensive understanding of the entire process.
To gain a comprehensive understanding of the process, let's incorporate debugging into the program.

```rust
#[no_mangle]
Expand Down Expand Up @@ -95,9 +97,9 @@ extern "C" fn handle() {
}
```

At the very beginning, as you may have noticed, the session is in `MessageStatus::Waiting` state. When a match occurs, the code switches to the first option. The program sends a message, sets the session status to `MessageStatus::Sent` and records the identifiers of the current and sent message. Then `exec::wait()` is called, which pauses message processing and adds the current message to the waiting list until `exec::wake(message_id)` is called or the gas runs out. Subsequent waking of a message requires its id, so `msg::id()` is stored in `session.msg_ids`.
Initially, the session is in `MessageStatus::Waiting` state. Upon a match, the code switches to the first option. The program sends a message, sets the session status to `MessageStatus::Sent`, and records the identifiers of the current and sent messages. Then, `exec::wait()` is called, pausing message processing and adding the current message to the waiting list until `exec::wake(message_id)` is called or the gas runs out. The ID of the waking message is crucial, hence `msg::id()` is stored in `session.msg_ids`.

Let's move on to the `handle_reply()` function:
Moving to the `handle_reply()` function:

```rust
#[no_mangle]
Expand All @@ -117,10 +119,9 @@ extern "C" fn handle_reply() {
}
```

Сondition `if reply_to == session.msg_ids.0 && session.message_status == MessageStatus::Sent` gives a guarantee that the expected message has arrived and arrived at the right moment, i.e. at the correct session status.
After that the status is set to `MessageStatus::Received(reply_message)` and the response message is saved. The ID of the original message is retrieved, and the `exec::wake()` function is called. This function retrieves the message from the waiting list, and the suspended message is resumed in the `handle()` function.
The condition `if reply_to == session.msg_ids.0 && session.message_status == MessageStatus::Sent` ensures the expected message has arrived at the right moment, i.e., when the session is in the correct status. The status is then set to `MessageStatus::Received(reply_message)`, and the reply message is saved. The ID of the original message is retrieved, and the `exec::wake()` function is called. This function takes the message from the waiting list, and the suspended message resumes in the `handle()` function.

*Important note*: when `exec::wake()` is called, the message is taken from the waiting list, it returns to the `handle()` entrypoint, and message processing will start handling the message from the beginning, i.e. the program code will get into the `match` again:
*Important note*: When `exec::wake()` is called, and the message returns to the `handle()` entry point, processing starts from the beginning. The program enters the `match` again:

```rust
// ...
Expand All @@ -133,9 +134,9 @@ match &session.message_status {
}
// ...
```
However, this time it will go into the third variant, send a response and set the status to `MessageStatus::Waiting`.

Now, let's examine this process as a whole:
However, this time, it proceeds to the third variant, sends a response, and sets the status to `MessageStatus::Waiting`.

![Code part 2](../img/03/wait_wake_code.gif)
Now, let's review this process as a whole:

![Code part 2](../img/03/wait_wake_code.gif)
4 changes: 2 additions & 2 deletions docs/03-intermediate/03-wait-wake-system/testing_wait_for.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ hide_table_of_contents: true

# Testing wait_for()

Let's use the function `system.spend_blocks()`, which allows to spend blocks and return all results:
Utilize the function `system.spend_blocks()` to advance through blocks and retrieve all results:

```rust
use gstd::ActorId;
Expand Down Expand Up @@ -50,7 +50,7 @@ fn test() {
}
```

Upon running the test, you will encounter the following debug messages. Examine them attentively to ensure that the program executed as intended.
When running the test, observe the following debug messages carefully to verify the program executed as planned.

```console
[DEBUG test] [handle(0x0fc8..ced9)] 0x0100..0000: !!!! HANDLE !!!!
Expand Down
4 changes: 2 additions & 2 deletions docs/03-intermediate/03-wait-wake-system/testing_wait_wake.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ hide_table_of_contents: true

# Testing wait() and wake()

Let's use `system.init_logger()` to start the environment in debug mode and see the debugs written.
To begin, use `system.init_logger()` to initiate the environment in debug mode, enabling you to see the debug messages.

```rust
use gstd::ActorId;
Expand Down Expand Up @@ -41,7 +41,7 @@ fn test() {
}
```

Upon running the test, you will encounter the following debug messages. Examine them attentively to ensure that the program executed as intended.
When you run the test, pay close attention to the following debug messages. Carefully review them to verify that the program has executed as expected.

```console
[DEBUG test] [handle(0x0fc8..ced9)] 0x0100..0000: !!!! HANDLE !!!!
Expand Down
19 changes: 9 additions & 10 deletions docs/03-intermediate/03-wait-wake-system/wait_for.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,18 @@ sidebar_position: 4
hide_table_of_contents: true
---

# Breakdowns in communication
# Breakdowns in Communication

Sometimes the second program may not answer for any reason and it is necessary to be able to handle this case correctly.
Sometimes, the second program may not respond for various reasons, and it's crucial to handle such scenarios appropriately.

## wait_for()

Let's try to solve this problem using `wait_for(DURATION_IN_BLOCK)`.
*Reminder: this function itself wakes up the message from the waiting list after a certain number of blocks, if `exec::wake()` was not called.*
To address this issue, let's use `wait_for(DURATION_IN_BLOCKS)`.
*Reminder: This function automatically wakes up the message from the waiting list after a specified number of blocks if `exec::wake()` has not been called.*

## Second program
## Second Program

Let's add `exec::wait()` so that the second program does not reply to incoming messages:
Add `exec::wait()` to the second program so that it does not reply to incoming messages.

```rust
#[no_mangle]
Expand All @@ -25,9 +25,9 @@ extern "C" fn handle() {

```

## First program
## First Program

In the first program, the `handle()` function will be modified. Instead of `exec::wait()`, we will use `exec::wait_for(3)`. This ensures that if `exec::wake()` in `handle_reply()` is not called within 3 blocks, message processing will automatically resume.
In the first program, the `handle()` function will be modified. Instead of `exec::wait()`, let's use `exec::wait_for(3)`. This ensures that if `exec::wake()` in `handle_reply()` is not called within 3 blocks, message processing will automatically resume.

```rust
#[no_mangle]
Expand Down Expand Up @@ -73,5 +73,4 @@ extern "C" fn handle() {
}
```

In this case the program will go to `handle()` again, but this time the session status will be `MessageStatus::Sent`, so a message will be sent to the user that there was no response and the status will be set to `MessageStatus::Waiting`.

In such a case, the program will return to `handle()`, but this time with the session status as `MessageStatus::Sent`. Consequently, a message will be sent to the user indicating the absence of a response, and the status will be updated to `MessageStatus::Waiting`.
Loading