Access Guards

Script modules are designed for script semantic analysis in multi-threaded applications. Even though you can use this interface perfectly well in a single-threaded application, we need to discuss its API a little further.

The design of the ScriptModule is somewhat similar to a read-write lock: it is an object that can be shared between threads (e.g., by wrapping it in Arc<ScriptModule>), and the threads access the underlying data through a system of read and write guards.

The read guards provide read-only operations on the module content. This kind of access is non-exclusive, allowing as many simultaneous read guards across independent threads as needed, provided there is no active write guard.

The write guard provides both read and write operations. This kind of access is exclusive, meaning that when write access is granted, no other read or write guards can be active.

let handle = TriggerHandle::new();
let read_guard = module.read(&handle, 1).expect("Module read error.");

The ScriptModule::read and ScriptModule::write functions request read and write access, respectively.

Both functions require a handle argument (TriggerHandle) and a guard priority (1).

The handle is an object that allows you to manually revoke the access grant. For instance, you can clone this object, transfer it to another thread, and trigger it to revoke access.

Typically, you should use a unique instance of the handle for each read and write access request.

The second argument, the priority number, determines the priority of the operations you intend to perform with this guard.

For example, if there are active read guards with a priority of 2, and another thread attempts to request a write guard with a priority of 3, the read guards will be revoked. However, if the write access priority is 1, the request will block the current thread until all read guards with a priority of 2 are released.

Multi-Threaded Applications

In multi-threaded applications, where threads may simultaneously request different types of access operations with distinct priorities, any function returning a ModuleResult may result in a ModuleError::Interrupted error.

This result indicates that the access grant has been revoked. In this event, you should typically drop the access guard (if any), put the current thread on hold for a short amount of time to allow another thread to obtain access with higher priority, and then retry the operation.

loop {
    let handle = TriggerHandle::new();

    let read_guard = match module.read(&handle, 5) {
        Ok(guard) => guard,
        Err(ModuleError::Interrupted(_)) => {
            sleep(Duration::from_millis(100));
            continue;
        }
        Err(other) => todo!("{other}"),
    };

    let diagnostics = match read_guard.diagnostics(2) {
        Ok(diagnostics) => diagnostics,
        Err(ModuleError::Interrupted(_)) => {
            sleep(Duration::from_millis(100));
            continue;
        }
        Err(other) => todo!("{other}"),
    };

    return diagnostics;
}

Single-Threaded Applications

In a single-threaded application, or in a multi-threaded application where each script module is managed exclusively by a dedicated thread, the situation of simultaneous access is practically impossible, and the ModuleError::Interrupted error should never occur.

Therefore, in practice, you can more confidently unwrap the results of the analysis API functions, which simplifies the overall design.

For instance, the Runner Example application is a single-threaded application that unwraps module results.

let handle = TriggerHandle::new();

let read_guard = module.read(&handle, 5).expect("Module read error.");

let diagnostics = read_guard.diagnostics(2).expect("Module analysis error.");

return diagnostics;