System signals
The Vara network ensures system and program's state consistency via introducing special handling mechanisms for potential issues and corner cases.
Actors have three common entry points - init
, handle
, handle_reply
. Another special system entry point introduced by the Gear Protocol is handle_signal
. It allows the system to communicate with programs if it is necessary to notify (signal) that some event related to the program's messages has happened. Only the system (Vara node runtime) can send signal messages to a program.
First of all, it can be useful to free up resources occupied by the program. A custom async logic in Vara implies storing Futures
in a program's memory. The execution context of Futures
can occupy some significant amount of memory in the case of many futures. When a program sends a message and waits for a reply to be woken, the reply can not be received. So there might be the case that if the initial message in the waitlist runs out of gas or the gas amount is not enough to properly finish the execution, the program’s state will be rolled back and Future
will never be freed.
In this case, Futures
remain in memory pages forever. Other messages are not aware of Futures
associated with other messages. Over time, Futures
accumulate in the program's memory so eventually a large amount of Futures limits the max amount of space the program can use.
In case a message has been removed from the waitlist due to gas constraints, the system sends a system message (signal) that is baked by an amount of reserved gas, which informs the program that it’s message was removed from the waitlist. Based on this info, a program can clean up its used system resources (Futures
).
The gstd
library provides a separate exec::system_reserve_gas
function for reserving gas specifically for system signal messages. It cannot be used for sending other regular cross-actor messages:
exec::system_reserve_gas(1_000_000_000).expect("Error during system gas reservation");
Even if this function hasn't been called, the system will reserve gas for system messages automatically with the default amount of 1_000_000_000
.
If a signal message appears, it uses gas specifically reserved for such kinds of messages. If no gas has been reserved for system messages, they are just skipped and the program will not receive them.
If gas has been reserved but no system messages occur during the current execution, then this gas returns back from where it was taken. The same relates to gas reserved for non-system messages - gas returns back after a defined number of blocks or by the program’s command.
handle_signal
has a default implementation if the program has async init
or/and async main
functions (see Asynchronous Programming for more details about async entry points). To define a custom signal handler, use the gstd::async_init
or gstd::async_main
macro with the specified handle_signal
argument. For example:
#[gstd::async_main(handle_signal = my_handle_signal)]
async fn main() {
// ...
}
fn my_handle_signal() {
// ...
}
Note that the custom signal handler derives its default behavior.
Some useful functions that can be used in handle_signal
:
msg::signal_from
- returns an identifier of the message which caused the signal;msg::signal_code
- returns the reason code of the signal (seeSignalCode
enum for more details).
It can be useful for a developer when writing communication between programs. Developer can define my_handle_signal
function and implement some logic there. For example, Program A
sent a message to Program B
. Program A
is waiting for a reply from Program B
but Program B
runs out of gas. The current execution will be interrupted, but the system will send a signal to Program A
and indicates the message identifier during which the execution was interrupted.
So, Program A
sends a message and saves the message identifier:
exec::system_reserve_gas(2_000_000_000)
.expect("Error during system gas reservation");
let result = msg::send_for_reply(address, payload, value, reply_deposit);
let (msg_id, msg_future) = if let Ok(msg_future) = result {
(msg_future.waiting_reply_to, msg_future)
} else {
// handle the error here
};
// save the `msg_id` in program state
unsafe { STATE.msg_id == msg::id() };
let reply = msg_future.await;
The execution fails in Program B
, and Program A
receives a signal:
#[no_mangle]
extern "C" fn my_handle_signal() {
if unsafe { STATE.msg_id == msg::signal_from() } {
// write logic here
}
}
However, it is important to understand that the execution of my_handle_signal
should not be long and should not consume a lot of gas. It can be used for tracking failures during the transaction. The program can use the information about failures the next time it is executed.
For programs written using the Gear Protocol's gstd
library, such signals can be sent to programs automatically under the hood when applicable. If a developer implements a program using gcore
or Gear's syscalls, then such signals should be considered in the program's code explicitly.