Skip to content

Commit

Permalink
Intermediate course (#217)
Browse files Browse the repository at this point in the history
Co-authored-by: AndrePanin <[email protected]>
  • Loading branch information
MedovTimur and AndrePanin authored Mar 26, 2024
1 parent 9c36a47 commit 8bb1e72
Show file tree
Hide file tree
Showing 20 changed files with 871 additions and 1 deletion.
27 changes: 27 additions & 0 deletions docs/03-intermediate/01-course-content.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
---
sidebar_position: 1
hide_table_of_contents: true
---

# Course content

This course is a continuation of the basic course. Let's delve deeper into the implementation of programs using Gear technologies, in particular how programs can communicate with each other, receive and process requests, as well as how to handle cases when one of the parties does not respond

The course material is structured into three sections:

1. Message Acquisition and Processing:
- Detailed examination of the handle_reply() function.
- Acquisition of skills in processing messages received as responses.
- Testing of the developed program.

2. Asynchronous Logic Utilizing the Waiting List:
- Introduction to the wait() and wake() functions.
- Mastery of halting message processing and awakening it as necessary.
- Analysis of scenarios where responses are not received and resolution using the wait_for() function.
- Testing of the implemented programs.

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

1 change: 1 addition & 0 deletions docs/03-intermediate/02-message-receiving/_category_.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
label: Message receiving
105 changes: 105 additions & 0 deletions docs/03-intermediate/02-message-receiving/handle_reply.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
---
sidebar_position: 1
hide_table_of_contents: true
---

# Message receiving

In this tutorial, you will acquire knowledge on how a program can effectively handle request messages. Let's illustrate this concept with an example of interaction between two programs: one program will act as an echo, responding with the received message, while the second program will initiate the communication by sending a message to the echo program and then receiving a response.

Before delving into the analysis of program code, it is useful to illustrate the operation of our programs schematically:

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

1. The user sends a "Hello" message to Program №1, which is processed by the handle() function;
2. This message is then forwarded to Program №2 (the echo program);
3. Program №1 sends a confirmation message to the user, indicating that the message has been successfully transmitted to Program №2;
4. Program №2 receives the message from Program №1 and responds with a reply message;
5. Program №1 receives the reply message from Program №2 through the handle_reply entrypoint;
6. Finally, the "Hello" message is relayed from the handle_reply function to the user.


## Echo program

The echo program is very simple, using the `msg::reply_input()` it will reply with the same message that came to it:

```rust
#[no_mangle]
extern "C" fn handle() {
// Reply to the incoming message
msg::reply_input(0, 0..msg::size()).expect("Error in sending a reply");
}
```

## Main program

The structure of the program is as follows:

```rust
struct Program {
echo_address: ActorId, // echo program address
msg_id_to_actor: (MessageId, ActorId), // tuple of message identifiers and message source address
}
```

When the program is initialized, an echo address is sent:

```rust
#[no_mangle]
extern fn init() {
let echo_address = msg::load().expect("Unable to decode init");
unsafe {
PROGRAM = Some(Program {
echo_address,
msg_id_to_actor: (MessageId::zero(), ActorId::zero()),
});
}
}
```

Now let's look at sending messages using the `handle()` function:

1. Receive the message with the function `msg::load()`;
2. Send a message to the echo address using the `msg::send()`;
3. An important step is to store the identifier of the message that the `msg::send()` returns, so that the `handle_reply()` function can determine which message was responded to;
4. At the end send a reply message notifying that the message was sent to the echo address.

```rust
#[no_mangle]
extern "C" fn handle() {
let message: String = msg::load().expect("Unable to decode");
let program = unsafe { PROGRAM.as_mut().expect("The program is not initialized")};
let msg_id = msg::send(program.echo_address, message, 0).expect("Error in sending a message");
program.msg_id_to_actor = (msg_id, msg::source());
msg::reply("Sent to echo address", 0).expect("Error in sending a reply");
}
```

The Gear program processes the reply to the message using the `handle_reply` function, so let's now look at how to handle the response message from the "echo" program:

1. Using the `msg::reply_to()` function to get the identifier of the message for which the `handle_reply` function is called;
2. Check that the message identifier is the same as the identifier of the message that was sent from the `handle()` function, in order to find out that the response came to that particular message;
3. At the end a reply message is sent to the sender's address().

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

```rust
#[no_mangle]
extern "C" fn handle_reply() {
let reply_message_id = msg::reply_to().expect("Failed to query reply_to data");
let program = unsafe { PROGRAM.as_mut().expect("The program is not initialized") };
let (msg_id, actor) = program.msg_id_to_actor;
if reply_message_id == msg_id{
let reply_message: String = msg::load().expect("Unable to decode ");
msg::send(actor, reply_message, 0).expect("Error in sending a message");
}

}
```

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



98 changes: 98 additions & 0 deletions docs/03-intermediate/02-message-receiving/testing_handle_reply.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
---
sidebar_position: 2
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.

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

Get program of the root crate with provided `system` and `echo` program instance from wasm file.
```rust
let program = Program::current(&system);
let echo_program = Program::from_file(&system, "target/wasm32-unknown-unknown/debug/echo.opt.wasm");
```

Initialize the "echo" program by sending an empty message and then initialize the main program by passing the address of the echo program to it.

```rust
let echo_result = echo.send_bytes(USER, []);
let echo_address: ActorId = ECHO_ADDRESS.into();
let res = program.send(USER, echo_address);
```

Send the message "Hello" to the main program and check for the response "Sent to echo address", which means that the message was successfully sent to the echo program address;

```rust
let result = program.send(USER, "Hello".to_string());
let log = Log::builder()
.source(1)
.dest(3)
.payload("Sent to echo address".to_string());
```

Extract the user's mailbox with the specified identifier and check that the "Hello" reply message has been sent back to the user.

```rust
let mailbox = system.get_mailbox(USER);
let log = Log::builder()
.source(1)
.dest(3)
.payload("HELLO".to_string());
```

The complete test code looks as follows:

```rust
use gstd::ActorId;
use gtest::{Log, Program, System};

const USER: u64 = 3;
const ECHO_ADDRESS: u64 = 2;

#[test]
fn test() {
// Create a new testing environment.
let system = System::new();

// Get program of the root crate with provided system.
let program = Program::current(&system);
// Get "echo" program
let echo = Program::from_file(&system, "target/wasm32-unknown-unknown/debug/echo.opt.wasm");
// The "echo" program is initialized with an empty payload message
let echo_result = echo.send_bytes(USER, []);
assert!(!echo_result.main_failed());

let echo_address: ActorId = ECHO_ADDRESS.into();
// The program is initialized using echo_address in the payload message
let res = program.send(USER, echo_address);
assert!(!res.main_failed());

// Send the message wanted to receive in reply
let result = program.send(USER, "HELLO".to_string());
assert!(!result.main_failed());

// check that the first message has arrived,
// which means that the message was successfully sent to the "echo" program
let log = Log::builder()
.source(1)
.dest(3)
.payload("Sent to echo address".to_string());
assert!(result.contains(&log));

// check that the second message has arrived at the mailbox,
// which means that a reply has been received.
let mailbox = system.get_mailbox(USER);
let log = Log::builder()
.source(1)
.dest(3)
.payload("HELLO".to_string());

assert!(mailbox.contains(&log));
}
```
2 changes: 2 additions & 0 deletions docs/03-intermediate/03-wait-wake-system/_category_.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
label: Asynchronous logic

138 changes: 138 additions & 0 deletions docs/03-intermediate/03-wait-wake-system/handle-reply-wait-wake.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
---
sidebar_position: 2
hide_table_of_contents: true
---

# Handle reply with wait() and wake()

Now let's use the knowledge about `wait()`/`wake()` functions and try to improve the program that was shown in the previous lesson.

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

As you may observe, the user will no longer receive two separate messages; instead, a single reply will be dispatched once the entire process is finalized.

## Main program

Since the "echo" program remains unchanged, let's proceed directly to the consideration of changes in the main program::

```rust
type MessageSentId = MessageId;
type OriginalMessageId = MessageId;

struct Program {
echo_address: ActorId,
msg_ids: (MessageSentId, OriginalMessageId),
status: Status,
}
```
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 echo address;
- `OriginalMessageId` - identifier of the message to be sent to the main program (required for using the wake() function).
- `status` - program status (required to track program activity stages).

```rust
enum Status {
Waiting,
Sent,
Received(String),
}
```
- `Waiting` — the program is waiting for a message;
- `Sent` - the program has sent a message to the "echo" program and has not yet received a response;
- `Received(String)` - the program received a reply to a message.

Considering the new fields in the program structure, initialization appears as follows:

```rust
#[no_mangle]
extern fn init() {
let echo_address = msg::load().expect("Unable to decode Init");
unsafe {
PROGRAM = Some(Program {
echo_address,
msg_ids: (MessageId::zero(), MessageId::zero()),
status: Status::Waiting
});
}
}
```

This time let's include debugging in our program to understand the whole process of the program during testing

```rust
#[no_mangle]
extern "C" fn handle() {
debug!("!!!! HANDLE !!!!");
debug!("Message ID: {:?}", msg::id());
let message: String = msg::load().expect("Unable to decode ");
let program = unsafe { PROGRAM.as_mut().expect("The program is not initialized") };

// match status
match &program.status {
Status::Received(reply_message) => {
debug!("HANDLE: Status::Received");
msg::reply(reply_message, 0).expect("Error in sending a reply");
debug!("HANDLE: Status::Waiting");
program.status = Status::Waiting;
}
Status::Waiting | Status::Sent => {
debug!("HANDLE: Status != Received");
let msg_id = msg::send(program.echo_address, message.clone(), 0)
.expect("Error in sending a message");
debug!("HANDLE: Status::Sent");
program.status = Status::Sent;
program.msg_ids = (msg_id, msg::id());
debug!("HANDLE: WAIT");
exec::wait();
}
}
debug!("HANDLE: END");
}

```

At the beginning, as you may have noticed, the program is in a `Status::Waiting` status, and when a `match` occurs, the code moves to the second variant. The program sends a message, sets the program status to `Status::Sent` and saves the identifiers of the current message and the sent message. After all this call the function `exec::wait()` function, which pauses the code and adds the current message to the waiting list until `exec::wake(message_id)` is called or the gas runs out. The message identifier is passed to the `exec::wake()` function, which is why `msg::id()` is stored in `program.msg_ids`.

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

```rust
#[no_mangle]
extern "C" fn handle_reply() {
debug!("HANDLE_REPLY");
let reply_message: String = msg::load().expect("Unable to decode ");
let reply_to = msg::reply_to().expect("Failed to query reply_to data");
let program = unsafe { PROGRAM.as_mut().expect("The program is not initialized") };

if reply_to == program.msg_ids.0 && program.status == Status::Sent {
debug!("HANDLE_REPLY: Status::Received");
program.status = Status::Received(reply_message);
let original_message_id = program.msg_ids.1;
debug!("HANDLE: WAKE");
exec::wake(original_message_id).expect("Failed to wake message");
}
}
```

Сondition `if reply_to == program.msg_ids.0 && program.status == Status::Sent` gives a guarantee that the expected message has arrived and arrived at the right moment, i.e. at the correct program status.
After that the status is set to `Status::Received(reply_message)` and the response message is saved; get the ID of the original message and call the `exec::wake()` function, which retrieves the message from the waiting list and the suspended message is restarted in `handle()`.

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

```rust
// ...
match &program.status {
Status::Received(reply_message) => {
debug!("HANDLE: Status::Received");
msg::reply(reply_message, 0).expect("Error in sending a reply");
debug!("HANDLE: Status::Waiting");
program.status = Status::Waiting;
}
// ...
```
However, this time it will go into the first variant, send a response and set the status to `Status::Waiting`.

Now, let's examine this process as a whole:

![Code part 2](../img/03/wait_wake_code.gif)

Loading

0 comments on commit 8bb1e72

Please sign in to comment.