Distributed Actors en Swift: actores distribuidos, sistemas de clústeres y Swift Distributed

Los distributed actors de Swift, introducidos en Swift 5.7, extienden el modelo de actores para que puedan vivir en procesos o máquinas distintas. La llamada a un método de un actor remoto se ve igual que la de uno local, con una única diferencia sintáctica: todas las llamadas deben ser async throws. El sistema de tipos garantiza que no puedes olvidarte de gestionar la red o la serialización.

Declarar un actor distribuido

La única diferencia con un actor normal es la palabra clave distributed. Los métodos que quieras llamar desde otro proceso también deben marcarse con distributed:

import Distributed

// Sistema de actores distribuidos (necesario para el runtime)
typealias DefaultDistributedActorSystem = LocalTestingDistributedActorSystem

distributed actor ContadorDistribuido {
    var valor: Int = 0

    // Este método puede llamarse desde otro proceso
    distributed func incrementar() {
        valor += 1
    }

    distributed func obtenerValor() -> Int {
        valor
    }

    // Este método solo es accesible localmente
    func resetear() {
        valor = 0
    }
}

// Uso:
let contador = ContadorDistribuido(actorSystem: sistema)
try await contador.incrementar()
let v = try await contador.obtenerValor()  // siempre async throws en distributed

ClusterSystem de swift-distributed-actors

Para producción, el paquete oficial swift-distributed-actors proporciona un sistema de clústeres TCP:

import DistributedCluster

// Nodo 1 (servidor)
let sistema = await ClusterSystem("nodo1") { settings in
    settings.bindHost = "127.0.0.1"
    settings.bindPort = 2550
}

let inventario = InventarioActor(actorSystem: sistema)

// Nodo 2 (cliente)
let cliente = await ClusterSystem("nodo2") { settings in
    settings.bindHost = "127.0.0.1"
    settings.bindPort = 2551
}

// Conectar al primer nodo
await cliente.cluster.join(host: "127.0.0.1", port: 2550)

Serialización: Codable en distributed actors

Los parámetros y valores de retorno de los métodos distribuidos deben conformar Codable (o el serializador que configure el sistema). El compilador lo verifica en tiempo de compilación:

struct Pedido: Codable, Sendable {
    let id: UUID
    let producto: String
    let cantidad: Int
    let total: Double
}

distributed actor GestorPedidos {
    private var pedidos: [UUID: Pedido] = [:]

    // Codable verificado en compilación
    distributed func crear(pedido: Pedido) throws {
        guard pedido.cantidad > 0 else {
            throw PedidoError.cantidadInvalida
        }
        pedidos[pedido.id] = pedido
    }

    distributed func listar() -> [Pedido] {
        Array(pedidos.values)
    }
}

Resolver actores remotos

Para obtener una referencia a un actor que vive en otro nodo, se usa el ID del actor:

// El actor se crea en el servidor y su ID se envía al cliente
// (por ejemplo, a través de un mensaje inicial o un registro de nombres)
let actorID = inventario.id

// En el cliente: resolver la referencia remota
let inventarioRemoto = try InventarioActor.resolve(id: actorID, using: clienteSistema)

// La llamada se ve igual que si fuera local
let stock = try await inventarioRemoto.stockActual(producto: "Swift Book")

Ejemplo real: juego multiplayer

distributed actor SalaDeJuego {
    var jugadores: [String: Int] = [:]  // nombre ? puntuación

    distributed func unirse(jugador: String) {
        jugadores[jugador] = 0
        print("(jugador) se unió. Jugadores: (jugadores.count)")
    }

    distributed func anotar(jugador: String, puntos: Int) throws {
        guard jugadores[jugador] != nil else {
            throw JuegoError.jugadorNoRegistrado(jugador)
        }
        jugadores[jugador, default: 0] += puntos
    }

    distributed func clasificacion() -> [(nombre: String, puntos: Int)] {
        jugadores.map { ($0.key, $0.value) }
            .sorted { $0.puntos > $1.puntos }
    }
}

Ejemplo real: microservicios en Swift puro

distributed actor ServicioInventario {
    private var stock: [String: Int] = [:]

    distributed func verificarDisponibilidad(producto: String) -> Bool {
        (stock[producto] ?? 0) > 0
    }

    distributed func reservar(producto: String, cantidad: Int) throws {
        guard let actual = stock[producto], actual >= cantidad else {
            throw InventarioError.stockInsuficiente(producto)
        }
        stock[producto] = actual - cantidad
    }
}

distributed actor ServicioPedidos {
    let inventario: ServicioInventario

    distributed func procesarPedido(_ pedido: Pedido) async throws {
        // Llamada entre microservicios como si fueran locales
        guard try await inventario.verificarDisponibilidad(producto: pedido.producto) else {
            throw PedidoError.sinStock
        }
        try await inventario.reservar(producto: pedido.producto, cantidad: pedido.cantidad)
        // ... procesar pago, notificar, etc.
    }
}

Resumen

Los distributed actors de Swift convierten la distribución en una propiedad del tipo, no un detalle de implementación. La declaración es idéntica a un actor normal salvo el modificador distributed; el compilador verifica que todos los parámetros son serializables; y las llamadas remotas se ven sintácticamente iguales a las locales excepto por el async throws obligatorio. El paquete swift-distributed-actors proporciona el sistema de clústeres TCP para producción, y el LocalTestingDistributedActorSystem integrado en la librería estándar facilita los tests sin infraestructura.

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP