El problema fundamental que esta arquitectura resuelve es la gestión de lógica de negocio compleja y crítica que debe residir en el cliente (SDKs) y ser consistente a través de múltiples lenguajes de programación. La duplicación de miles de líneas de código de máquinas de estado complejas en siete o más lenguajes es insostenible en términos de fiabilidad, mantenibilidad y consistencia. La solución propuesta, un core compartido en Rust con capas de 'bridge' específicas para cada lenguaje, aborda directamente este desafío, permitiendo una única fuente de verdad para la lógica crítica mientras se ofrece una experiencia idiomática a los usuarios.
La necesidad de esta arquitectura surge en sistemas distribuidos donde las garantías de durabilidad y resiliencia requieren una lógica de cliente 'fat' que no puede depender de llamadas RPC de baja latencia al servidor para cada operación. Este patrón es una evolución de la necesidad de compartir código crítico, similar a cómo las bibliotecas de criptografía o compresión se han implementado históricamente en C para ser consumidas por otros lenguajes, pero modernizado con Rust para mejorar la seguridad y la expresividad.
Arquitectura del Sistema
La arquitectura se compone de tres capas principales: el Rust Core, la capa de 'Bridge' y la capa de SDK específica del lenguaje. El Rust Core encapsula toda la lógica de negocio compleja, como las máquinas de estado de ejecución duradera de Temporal, y se compila como una biblioteca nativa. Esta capa es la única fuente de verdad para la lógica crítica y está diseñada para ser agnóstica al lenguaje de consumo.
La capa de 'Bridge' es una biblioteca Rust (crate) específica para cada lenguaje de destino (Python, TypeScript, Ruby, .NET, Swift). Su función principal es actuar como pegamento entre el Rust Core y el SDK del lenguaje, manejando la conversión de tipos, la gestión de la asincronía y la memoria a través de la interfaz Foreign Function Interface (FFI) de C. Esta capa es donde se realizan las llamadas FFI, convirtiendo los tipos de datos entre las representaciones del lenguaje host y Rust, y gestionando los punteros de memoria. Para lenguajes como Python y TypeScript, se utilizan 'helper libraries' (PyO3, Neon) que abstraen gran parte de la complejidad de FFI y la gestión de asincronía. Para otros, como .NET y Swift, la implementación FFI es manual. La gestión de la asincronía implica mapear conceptos como 'futures' de Rust a 'promises' o 'fibers' del lenguaje host, a menudo utilizando callbacks o event loops para garantizar la ejecución en el hilo correcto.
Finalmente, la capa de SDK específica del lenguaje expone APIs idiomáticas a los usuarios, ocultando completamente la existencia del Rust Core y la capa de 'Bridge'. Esta capa se enfoca en la ergonomía y la integración natural con las convenciones del lenguaje. Las decisiones de diseño clave incluyen el uso de Rust por su seguridad de memoria, rendimiento y portabilidad, y la minimización de la lógica en la capa de 'Bridge' para reducir la superficie de error y la complejidad del código 'unsafe'.
Flujo de Llamada de SDK a Rust Core
- 1 SDK (Lenguaje Host) El código de usuario invoca una API del SDK (ej. Python, TypeScript).
- 2 Bridge (Rust Crate) La API del SDK llama a una función en la capa de Bridge (Rust).
- 3 Conversión de Tipos El Bridge convierte los tipos del lenguaje host a tipos compatibles con Rust ...
- 4 FFI Call (C ABI) El Bridge realiza una llamada FFI al Rust Core, pasando punteros y datos.
- 5 Rust Core El Rust Core ejecuta la lógica de negocio, posiblemente con operaciones asínc...
- 6 FFI Return (C ABI) El Rust Core devuelve un puntero o resultado a la capa de Bridge.
- 7 Conversión de Tipos El Bridge convierte los tipos de Rust FFI de vuelta a tipos del lenguaje host.
- 8 SDK (Lenguaje Host) El SDK recibe el resultado y lo devuelve al código de usuario.
| Capa | Tecnología | Justificación |
|---|---|---|
| compute | Rust | Lenguaje principal para el core de lógica de negocio compartida, elegido por seguridad de memoria, rendimiento, expresividad y portabilidad. vs C, Zig |
| networking | gRPC | Protocolo de comunicación entre el worker del usuario y el servicio Temporal, utilizando Protobuf para la definición de mensajes. |
| data-processing | Protobuf | IDL (Interface Definition Language) utilizado para definir estructuras de datos y servicios, facilitando la interoperabilidad con el servicio Temporal. También se usa para la generación de código de tipos, aunque con el coste de serialización/deserialización. vs FlatBuffers, Cap'n Proto |
| orchestration | PyO3 | Helper library para la integración de Rust con Python, facilitando la gestión de FFI, conversión de tipos y asincronía. |
| orchestration | Neon | Helper library para la integración de Rust con Node.js (TypeScript/JavaScript), simplificando la interacción FFI y la gestión de asincronía. |
| orchestration | Magnus | Helper library para la integración de Rust con Ruby, ofreciendo algunas facilidades para FFI. |
Trade-offs
Ganancias
- ▲ Consistencia de la lógica de negocio
- ▲ Mantenibilidad del código
- ▲ Tiempo de desarrollo para nuevos lenguajes
- ▲ Reducción de bugs
- ▲ Rendimiento del core
Costes
- ▲ Complejidad en la capa de 'Bridge'
- ▲ Dificultad en la inyección de comportamiento personalizado desde el lenguaje host
- ▲ Desafíos en la distribución de binarios nativos para múltiples plataformas/arquitecturas
- ▲ Resistencia de algunas comunidades (Java, Go) a usar extensiones nativas
- △ Overhead de serialización/deserialización con Protobuf
macro_rules! convert_type {
($from_type:ty, $to_type:ty, $field:ident) => {
impl From<$from_type> for $to_type {
fn from(value: $from_type) -> Self {
Self { $field: value.$field.into() }
}
}
};
// ... más reglas para diferentes estructuras o campos
}pub extern "C" fn execute_async_op(
input: *mut CChar,
callback_ptr: *mut c_void,
callback_fn: extern "C" fn(*mut c_void, *mut CChar)
) {
let input_str = unsafe { CStr::from_ptr(input).to_string_lossy().into_owned() };
tokio::spawn(async move {
// Simular operación asíncrona
let result = format!("Processed: {}", input_str);
let c_result = CString::new(result).unwrap();
callback_fn(callback_ptr, c_result.into_raw());
});
}Fundamentos Teóricos
La problemática de la consistencia de la lógica de negocio a través de múltiples implementaciones de cliente se relaciona con los principios de 'Single Source of Truth' y 'Don't Repeat Yourself' (DRY), fundamentales en la ingeniería de software. A nivel más profundo, la necesidad de máquinas de estado complejas en el cliente para garantizar la durabilidad de la ejecución remite a conceptos de sistemas distribuidos como la tolerancia a fallos y la resiliencia, donde la lógica de reintento, compensación y coordinación debe ser robusta. Esto se alinea con la investigación en 'durable execution' y 'workflow engines', que a menudo se basan en modelos de máquinas de estado finitas o Petri nets para garantizar la corrección semántica de las operaciones distribuidas.
La elección de FFI para la interoperabilidad entre lenguajes es una técnica clásica en la computación, permitiendo la reutilización de componentes de bajo nivel escritos en lenguajes como C o C++ desde entornos de alto nivel. La gestión de la memoria y la asincronía a través de FFI son desafíos bien conocidos, abordados en la literatura sobre 'language interoperation' y 'runtime systems'. La aparición de Rust como un lenguaje con garantías de seguridad de memoria sin un recolector de basura ofrece una alternativa moderna y más segura a C para la implementación de estos componentes compartidos, mitigando clases enteras de errores que son comunes en las implementaciones FFI tradicionales.