Actor System

Why?

Actors work as an abstraction over data storage and messaging. It allows for all systems (GUI, Programs, etc.) to work together, and rely on the same features. It reduces work of implementation, and all implementations can use the functions.

Features

  • Petnames
  • OCAP security
  • HMAC message verification

Format

#![allow(unused)]
fn main() {
// Different possible types of actors (more to be added)
enum ActorType {
    GUI(photon::Widget),
    ProgramInterface,
}

// Possible states an actor can be in
enum ActorState {
    Receive,
    Send,
    Work,
    Idle,
}

// Cryptographic keypair
struct KeyPair {
    privkey: u128,
    pubkey: u128,
}

// The actor itself
struct Actor<D: DataInterface> {
    petname: Option<String>, // Human-meaningful petname (explored further down)
    uuid: Uuid, // Unique identifier
    namespace: Uuid, // Parent namespace of this actor
    actor_type: ActorType,
    state: ActorState,
    keys: Option<KeyPair>, // Cryptographic keypair
    creation_date: DateTime,
    modified_date: DateTime,
    data: Option<D>, // Optional data of the generic D type
}

impl Actor {
    fn new(namespace: Uuid, a_type: ActorType) -> Self {
        Actor {
            petname: None,
            uuid: Uuid::new(),
            namespace: namespace,
            actor_type: a_type,
            state: ActorState::Idle,
            keys: None,
            creation_date:: now(),
            modified_date: now(),
            data: None,
        }
    };
}

impl KeyPair {
    async fn generate_keypair(&mut self) -> Self; // Generate a public/private keypair (threaded)
    fn get_pubkey(&self) -> u128; // Return the keypair of an Actor
    async fn sign(&self, &[u8]) -> Result<&[u8], Error>; // Sign some data with a private key (threaded)
    async fn verify_signature(&[u8], u128) -> Result<(), Error>; // Verify signed data (threaded)
}

trait FilesystemInterface { // Interfacing with the filesystem
    async fn read(&mut self) -> Result<(), Error>; // Read the data from the disk into the Actor using the Uuid as a search key
    async fn write(&self) -> Result<(), Error>; // Write the data to the disk using the Uuid as a key
}

trait DataInterface { // Necessary data functions
    async fn to_bytes(&self) -> Result<&[u8], Error>; // Convert the data into a byte array
}

trait MessageInterface { // Sending & receiving messages
    async fn send_message(&self, MessageType, Uuid) -> Result<(), Error>; // Send a message to a recipient
    async fn receive_message(&self, Channel) -> Message; // Asynchronously wait for an incoming message, and deal with the first one we get
}
}

OCAP

TODO

Messages

  • postcard for message passing
  • Priority Queue for processing multiple messages, while dealing with higher-priority ones first

Messages will be fully modelled so an actor can know exactly what they have to deal with, and what they can send. Different channels are used to make each one less clogged up, and used only for a specific purpose. Actors can read from/write to a specific channel, allowing them to ignore the others. They can then also deal with channels in different ways, maybe deprioritizing the Test channel.

#![allow(unused)]
fn main() {
enum Channel { // Channels for sending/receiving messages on
    Graphics, // Low-latency graphics updates
    Test, // Designated channel for testing messages
    Filesystem, // Batch filesystem operations
    Print, // Printing text
    Executable, // Executable-related messages
}

enum ProcessCode {
    Exit, // Exit the process
    Save, // Save data
    Clear, // Clear data
    Restart, // Restart process
}

enum MessageType {
    Ping(String), // Simple test if we can send/recieve a message
    FilesystemUpdate(gravitas::FileOperation), // We want to operate on the filesystem
    GraphicsUpdate(photon::GraphicsOperation), // Update a graphics window
    TextUpdate(String), // Send some text (text mode only)
    ProcessUpdate(ProcessCode), // Send some info about an operation to be done on the current process. Usually kernel -> exe
}

struct Message {
    id: Uuid, // UUID of the message itself
    m_type: MessageType, // Message type & content
    priority: u8, // For priority queueing
    sender: Uuid, // Who is sending the message
    recipient: Uuid, // Who the message is meant for
}
}

An example message handling loop may look like this:

#![allow(unused)]
fn main() {
loop { // Continuously loop through message sending & receiving
    actor.send_message(MessageType::Ping("hello!".to_string())).await; // Block and await until we can send the test message.
    match actor.receive_message(&self, Channel::Test).await.m_type { // Match on a message type
        Ping(s) => println!("We got pinged! {}", s), // Print if we got pinged
        _ => {}, // Ignore other states
    }
}
}

Latency