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

Update Intermediate course #225

Merged
merged 1 commit into from
Apr 5, 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
18 changes: 9 additions & 9 deletions docs/03-intermediate/01-course-content.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,23 @@ 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
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.

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.
- 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.
- Introduction to the `exec::wait()` and `exec::wake()` functions;
- Mastery of halting message processing and awakening it as necessary;
- Analysis of scenarios where responses are not received and resolution using the `exec::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.
- In-depth exploration of delayed messages;
- Resolution of issues related to lack of response through the use of delayed messages;
- Testing of the program featuring delayed messages.

156 changes: 106 additions & 50 deletions docs/03-intermediate/02-message-receiving/handle_reply.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,101 +5,157 @@ 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:
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.

![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.

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.

## Echo program
## First program

The echo program is very simple, using the `msg::reply_input()` it will reply with the same message that came to it:
The task of the first program is to communicate with the second program, so the following structure will be needed:

```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");
struct Session {
second_program: ActorId, // second program address
msg_id_to_actor: (MessageId, ActorId), // tuple of message identifiers and message source address
}
```

## Main program

The structure of the program is as follows:
The following actions and events will also be necessary to simulate a dialogue between programs:

```rust
struct Program {
echo_address: ActorId, // echo program address
msg_id_to_actor: (MessageId, ActorId), // tuple of message identifiers and message source address
#[derive(TypeInfo, Encode, Decode)]
#[codec(crate = gstd::codec)]
#[scale_info(crate = gstd::scale_info)]
pub enum Action {
Hello,
HowAreYou,
MakeRandomNumber{
range: u8,
},
}

#[derive(TypeInfo, Encode, Decode, Debug)]
#[codec(crate = gstd::codec)]
#[scale_info(crate = gstd::scale_info)]
pub enum Event {
Hello,
Fine,
Number(u8),
MessageSent,
}
```

When the program is initialized, an echo address is sent:
During initialization, it is necessary to pass the address of the second program:

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

Now let's look at sending messages using the `handle()` function:
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 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.
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;
4. Finally, send a reply message indicating that the message was sent to the second program.

```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");
let action: Action = msg::load().expect("Unable to decode ");
let session = unsafe { SESSION.as_mut().expect("The session is not initialized") };
let msg_id = msg::send(session.second_program, action, 0).expect("Error in sending a message");
session.msg_id_to_actor = (msg_id, msg::source());
msg::reply(Event::MessageSent, 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:
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:

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().
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.

It is important to emphasize that calling `msg::reply()` inside the `handle_reply` function is not allowed.
**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");
let session = unsafe { SESSION.as_mut().expect("The session is not initialized") };
let (msg_id, actor) = session.msg_id_to_actor;
if reply_message_id == msg_id {
let reply: Event = msg::load().expect("Unable to decode ");
msg::send(actor, reply, 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.
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.


## 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.

```rust
#![no_std]
use gstd::{exec, msg, Encode, Decode, TypeInfo};

static mut SEED: u8 = 0;

#[derive(TypeInfo, Encode, Decode)]
#[codec(crate = gstd::codec)]
#[scale_info(crate = gstd::scale_info)]
pub enum Action {
Hello,
HowAreYou,
MakeRandomNumber{
range: u8,
},
}

#[derive(TypeInfo, Encode, Decode)]
#[codec(crate = gstd::codec)]
#[scale_info(crate = gstd::scale_info)]
pub enum Event {
Hello,
Fine,
Number(u8),
}

#[no_mangle]
extern "C" fn handle() {
let action: Action = msg::load().expect("Error in decode message");
let reply = match action {
Action::Hello => Event::Hello,
Action::HowAreYou => Event::Fine,
Action::MakeRandomNumber {range} => {
let seed = unsafe { SEED };
unsafe { SEED = SEED.wrapping_add(1) };
let mut random_input: [u8; 32] = exec::program_id().into();
random_input[0] = random_input[0].wrapping_add(seed);
let (random, _) = exec::random(random_input).expect("Error in getting random number");
Event::Number(random[0] % range)
}
};
msg::reply(reply, 0).expect("Error in sending a reply");
}
```
66 changes: 36 additions & 30 deletions docs/03-intermediate/02-message-receiving/testing_handle_reply.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,77 +12,81 @@ The first thing to do is to create a testing environment:
let system = System::new();
```

Get program of the root crate with provided `system` and `echo` program instance from wasm file.
Get first program of the root crate with provided `system` and the second 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");
let first_program = Program::current(&system);
let second_program = Program::from_file(&system, "target/wasm32-unknown-unknown/release/second_program.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.
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 echo_result = echo.send_bytes(USER, []);
let echo_address: ActorId = ECHO_ADDRESS.into();
let res = program.send(USER, echo_address);
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 "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;
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;

```rust
let result = program.send(USER, "Hello".to_string());
let result = first_program.send(USER, Action::MakeRandomNumber {range: 1});
let log = Log::builder()
.source(1)
.dest(3)
.payload("Sent to echo address".to_string());
.payload(Event::MessageSent);
assert!(result.contains(&log));

```

Extract the user's mailbox with the specified identifier and check that the "Hello" reply message has been sent back to the user.
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);
let log = Log::builder()
.source(1)
.dest(3)
.payload("HELLO".to_string());
.payload(Event::Number(0));
assert!(mailbox.contains(&log));
```

The complete test code looks as follows:

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

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

#[test]
fn test() {
fn success_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);
// Get first program of the root crate with provided system.
let first_program = Program::current(&system);
// Get second program
let second_program = Program::from_file(&system, "target/wasm32-unknown-unknown/release/second_program.opt.wasm");
// The second program is initialized with an empty payload message
let result = second_program.send_bytes(USER, []);
assert!(!result.main_failed());

let second_program_address: ActorId = SECOND_PROGRAM_ADDRESS.into();
// The first program is initialized using second_program in the payload message
let res = first_program.send(USER, second_program_address);
assert!(!res.main_failed());

// Send the message wanted to receive in reply
let result = program.send(USER, "HELLO".to_string());
// Send with the message we want to receive back
let result = first_program.send(USER, Action::MakeRandomNumber {range: 1});
assert!(!result.main_failed());

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

// check that the second message has arrived at the mailbox,
Expand All @@ -91,8 +95,10 @@ fn test() {
let log = Log::builder()
.source(1)
.dest(3)
.payload("HELLO".to_string());
.payload(Event::Number(0));

assert!(mailbox.contains(&log));
}
```

It will be good practice if you implement these programmes and test on your own.
Loading