TrueAsync en Laravel: Arquitectura para concurrencia real sin Swoole
Las soluciones tradicionales para la asincronía en PHP, como Swoole o RoadRunner, requieren modificaciones profundas en el código e introducen funciones coloreadas. TrueAsync ofrece un enfoque fundamentalmente diferente: modificar el núcleo de Zend para asincronía transparente. Desglosaremos cómo adaptar Laravel a este modelo, superando problemas con servicios con estado y permitiendo el manejo concurrente de operaciones limitadas por I/O.
Fundamentos de TrueAsync: Cómo funciona la asincronía transparente
TrueAsync es un núcleo alternativo de PHP que modifica el Zend Engine, las bibliotecas de I/O y las operaciones de base de datos. Su principal ventaja es eliminar los problemas de coloreado de funciones. A diferencia de Node.js o Python asyncio, las funciones estándar de PHP (PDO::query(), file_get_contents()) ceden automáticamente el control al planificador durante las esperas de I/O. Las corrutinas se suspenden a nivel de función C, permitiendo manejar cientos de solicitudes en un solo hilo.
Importante entender: esto no es multihilo. El modelo se basa en multitarea cooperativa: cambio de contexto mientras se esperan recursos externos. Es ineficiente para tareas limitadas por CPU (cálculos pesados), pero ideal para escenarios web típicos que involucran bases de datos y llamadas a API. Aquí un ejemplo básico de uso:
use function Async\spawn;
use function Async\await;
use function Async\delay;
$a = spawn(function() {
delay(100);
return 'result A';
});
$b = spawn(function() {
delay(100);
return 'result B';
});
// Obschee time vypolneniya ~100ms
echo await($a);
echo await($b);
El problema de los servicios con estado en Laravel
Laravel fue diseñado originalmente para el modelo FPM, donde cada solicitud se maneja en un proceso aislado. Los servicios con estado (AuthManager, Session, Router) almacenan el estado en propiedades de objetos, que se destruyen después del procesamiento de la solicitud. En modo asíncrono, el proceso vive indefinidamente, lo que provoca fugas de estado:
// Coroutine A: request from user_1
Auth::loginUsingId(1);
delay(200);
// Coroutine B: request from user_2
Auth::loginUsingId(2);
// Return in korutinu A
echo Auth::id(); // Vozvraschaet 2 vmesto 1
Las propiedades estáticas (Model::$dispatcher, Facade::$resolvedInstance) y los servicios en el contenedor IoC se comparten entre todas las solicitudes. Octane aborda esto clonando el contenedor, pero introduce sobrecarga y no cubre las propiedades estáticas.
Aislamiento contextual: La arquitectura de la solución
TrueAsync proporciona current_context(): un almacén de datos ligado al ciclo de vida de la solicitud. Cuando una corrutina termina, el contexto se borra automáticamente. La arquitectura divide Laravel en dos zonas:
- Núcleo estático: router, config, contenedor IoC. Cargado una vez, compartido entre todas las solicitudes.
- Servicios por solicitud: Auth, Session, Cookie. Creados en el contexto de cada corrutina.
El mecanismo clave es interceptar la resolución de servicios mediante AsyncApplication::resolve():
protected function resolve($abstract, ...)
{
if ($this->asyncMode && $this->isScoped($abstract)) {
$ctx = current_context();
if ($instance = $ctx->find($abstract)) {
return $instance;
}
$instance = $this->buildFresh($abstract);
$ctx->set($abstract, $instance);
return $instance;
}
return parent::resolve($abstract, ...);
}
Claves contextuales mediante Enum y la solución de Facade
Para evitar colisiones y garantizar acceso seguro, se usan claves enum:
enum ScopedService: string
{
case REQUEST = 'request';
case SESSION = 'session';
case AUTH = 'auth';
case AUTH_DRIVER = 'auth.driver';
case COOKIE = 'cookie';
}
Esto proporciona:
- Aislamiento de acceso (solo el propietario del enum puede leer/escribir datos)
- Garantías sin colisiones (incluso con valores de cadena idénticos)
- Soporte para análisis estático (PHPStan/IDE rastrea el uso)
Las Facades requerían una solución separada debido al caché de resolvedInstance en un array estático. Entra ScopedServiceProxy:
class ScopedServiceProxy
{
public function __call($method, $args)
{
return ($this->resolver)()->$method(...$args);
}
}
El proxy reenvía las llamadas al contexto de la corrutina actual mientras mantiene el caché a nivel de proxy.
Adaptaciones críticas y Pool de PDO
Después del aislamiento básico, se identificaron servicios que almacenan estado por solicitud internamente, requiriendo versiones asíncronas especializadas:
- AsyncRouter (aislamiento de ruta actual)
- AsyncDispatcher (eventos diferidos por corrutina)
- AsyncTranslator (locale de traducción)
- AsyncViewFactory (View::share())
- AsyncConfig (cambios de config en tiempo de ejecución)
- AsyncDatabaseSessionHandler (escrituras de sesión atómicas)
- Async*Connection (contadores de transacciones)
Se prestó especial atención al manejo de bases de datos. Sin un pool de conexiones, las consultas concurrentes de corrutinas rompen el protocolo de PostgreSQL. TrueAsync lo resuelve con un Pool de PDO integrado:
$pdo = new PDO($dsn, $user, $password, [
PDO::ATTR_POOL_ENABLED => true,
PDO::ATTR_POOL_MIN => 0,
PDO::ATTR_POOL_MAX => 10,
PDO::ATTR_POOL_HEALTHCHECK_INTERVAL => 30,
]);
El pool asigna dinámicamente conexiones: después de un resultado SELECT, la conexión regresa al pool, sirviendo más corrutinas que el tamaño máximo del pool. Para transacciones, la conexión se bloquea hasta commit/rollback.
Puntos clave
- TrueAsync elimina el coloreado de funciones mediante modificaciones en el núcleo de PHP
- Los servicios con estado de Laravel requieren aislamiento contextual mediante current_context()
- El Pool de PDO es esencial para la operación correcta de bases de datos en entornos concurrentes
- Las claves enum aseguran seguridad y trazabilidad de los datos contextuales
- La solución no reemplaza el multihilo para tareas limitadas por CPU
— Editorial Team
Aún no hay comentarios.