What are Actors?

Actors are independent units of computation that:

  • Have private state - no other code can access it directly
  • Communicate via messages - the only way to interact
  • Process one message at a time - no internal concurrency issues
  • Can spawn child actors - creating supervision hierarchies

Your First Actor

Let's create a simple counter actor:

counter.sx
actor Counter {
    var count: i64 = 0

    // Fire-and-forget message
    receive Increment {
        count += 1
    }

    // Message with parameter
    receive Add(n: i64) {
        count += n
    }

    // Request-response message
    receive GetCount -> i64 {
        count
    }
}

Spawning Actors

Use the spawn keyword to create actor instances:

spawning.sx
fn main() {
    // Spawn a new counter actor
    let counter = spawn Counter

    // Send messages
    send(counter, Increment)
    send(counter, Add(5))

    // Request and wait for response
    let current = ask(counter, GetCount)
    print("Count: {current}")  // Count: 6
}

Send vs Ask

Two ways to communicate with actors:

Method Behavior Use When
send Fire-and-forget, returns immediately You don't need a response
ask Waits for response You need data back from the actor

Lifecycle Hooks

Actors can respond to lifecycle events:

lifecycle.sx
actor Worker {
    var task_count: i64 = 0

    // Called when actor starts
    on_start() {
        print("Worker starting up...")
    }

    // Called when actor stops
    on_stop() {
        print("Worker shutting down. Processed {task_count} tasks.")
    }

    // Called when recovering from checkpoint
    on_resume() {
        print("Worker resuming from checkpoint...")
    }

    receive DoTask(task: Task) {
        task_count += 1
        // Process task...
    }
}

Supervision

Supervisors monitor child actors and handle failures:

supervisor.sx
supervisor AppSupervisor {
    // What to do when a child fails
    strategy: OneForOne,  // Only restart the failed child

    // Restart limits
    max_restarts: 5,
    within: Duration::seconds(60),

    // Child actors to supervise
    children: [
        child(DatabaseConnection, restart: Always),
        child(WebServer, restart: Always),
        child(BackgroundWorker, restart: Transient)
    ]
}

Restart Strategies

OneForOne: Only restart the failed child
OneForAll: Restart all children when one fails
RestForOne: Restart failed child and all started after it

Practical Example: Task Queue

Let's build a simple task queue with a worker pool:

task-queue.sx
struct Task {
    id: String,
    data: String
}

actor Worker {
    receive Process(task: Task) -> String {
        print("Processing task {task.id}")
        // Simulate work
        Thread::sleep(Duration::ms(100))
        "Completed: {task.id}"
    }
}

actor TaskQueue {
    var workers: List<ActorRef<Worker>>
    var next: i64 = 0

    fn new(worker_count: i64) -> Self {
        let workers = (0..worker_count)
            .map(|_| spawn Worker)
            .collect()
        TaskQueue { workers, next: 0 }
    }

    receive Submit(task: Task) -> String {
        // Round-robin to workers
        let worker = workers[next % workers.len()]
        next += 1
        ask(worker, Process(task))
    }
}

fn main() {
    let queue = spawn TaskQueue::new(4)

    for i in 1..10 {
        let task = Task { id: "task-{i}", data: "data" }
        let result = ask(queue, Submit(task))
        print(result)
    }
}

Try It Yourself

Extend the TaskQueue to track completed tasks. Add a GetStats message that returns the total number of tasks processed by each worker.

Summary

In this tutorial, you learned:

  • The actor model and its benefits for concurrency
  • Defining actors with receive handlers
  • Spawning actors and sending messages
  • The difference between send and ask
  • Lifecycle hooks for initialization and cleanup
  • Building supervised actor hierarchies

In the next tutorial, we'll integrate AI into our actors by building an AI-enhanced data processing pipeline.