Ingeniería

El ejecutor que parecía libre: cómo corregimos un bug en el taskpool de Asterisk

Gian Diego JavesCTO, Galcyon Solutions14 de agosto de 20268 min de lectura
El ejecutor que parecía libre: cómo corregimos un bug en el taskpool de Asterisk

El ejecutor que parecía libre: cómo corregimos un bug en el taskpool de Asterisk

Gian Diego Javes, CTO de Galcyon Solutions · 14 de agosto de 2026

En varios caminos internos, Asterisk distribuye trabajo entre un conjunto de hilos ejecutores. Para decidir cuál debe recibir la siguiente tarea, su taskpool compara la carga de cada uno.

El problema era que esa medición solo consideraba las tareas que seguían en cola. No contaba la que un ejecutor ya estaba procesando. Por eso, un hilo ocupado con una operación larga podía parecer libre y continuar recibiendo trabajo mientras otros permanecían disponibles.

En Galcyon lo reproducimos de forma determinista en laboratorio, lo reportamos al proyecto Asterisk y enviamos una corrección. Tras la revisión de Joshua Colp, autor del componente y líder del proyecto, una parte de la propuesta fue descartada por razones de diseño y el arreglo específico del selector fue aceptado. El cambio ya forma parte del código oficial en master y en las ramas 20, 22 y 23.

Este artículo explica el defecto, la prueba y la decisión de diseño. El resultado descrito es de laboratorio y no se atribuye a ningún incidente de producción.

Cómo distribuye trabajo el taskpool

El taskpool agrupa varios ejecutores, cada uno con su propia cola. Cuando llega una tarea, un selector decide a qué ejecutor asignarla y la encola directamente allí.

En el código existen dos selectores principales:

  • Sequential: distribuye las tareas por turnos, sin consultar la carga.
  • Least full: recorre los ejecutores y selecciona el que aparenta tener menos trabajo. Si encuentra uno con carga cero, detiene la búsqueda porque no puede encontrar una opción mejor.

Los dos selectores del taskpool. El secuencial distribuye por turnos; least full compara la carga y se detiene al encontrar un ejecutor con carga cero.

La decisión depende de una pregunta aparentemente sencilla: ¿cómo se mide la carga de un ejecutor?

El punto ciego

La carga se calculaba utilizando el tamaño de la cola. Sin embargo, cuando un ejecutor toma una tarea, esta deja de estar encolada y pasa a estar en ejecución.

Esto produce dos estados distintos que reportaban el mismo valor:

  • Un ejecutor realmente ocioso: cola igual a cero.
  • Un ejecutor procesando una tarea larga: cola igual a cero.

Para el selector, ambos parecían disponibles. Como least full se detiene al encontrar el primer ejecutor con carga cero, podía asignar nuevas tareas al hilo ocupado sin evaluar a los demás.

Antes, el ejecutor ocupado reportaba carga cero y recibía nuevas tareas. Después de la corrección, la tarea en ejecución también forma parte de la carga.

Este comportamiento importa especialmente cuando una tarea puede retener un ejecutor durante más tiempo de lo habitual. En el flujo de PJSIP existen operaciones sincrónicas y serializers que comparten el mismo conjunto de ejecutores. Un hilo retenido no solo demora la operación que está procesando: durante ese tiempo tampoco puede drenar trabajo de otros serializers.

Una prueba que falla siempre

Sin una reproducción determinista, el comportamiento podía quedarse en una hipótesis. Para comprobarlo construimos una prueba con cuatro ejecutores:

  1. Uno ejecuta una tarea bloqueada de forma controlada.
  2. Los otros tres permanecen disponibles.
  3. Se envía una tarea nueva al pool.
  4. Se registra qué ejecutor la procesa.

Antes del arreglo, la nueva tarea terminaba en el ejecutor ocupado en las diez ejecuciones de la prueba.

La primera versión utilizaba una pausa fija para esperar el final del bloqueo. Durante la revisión, Joshua Colp pidió reemplazar esa espera ciega por coordinación explícita. La tarea pasó a notificar su finalización mediante una variable de condición, manteniendo un timeout únicamente como protección.

El tiempo de la prueba bajó de 403 ms a 2 ms. No cambia el defecto que demuestra, pero sí mejora la calidad de una prueba que debe ejecutarse en cada integración.

La corrección

Asterisk ya utiliza en otra parte del taskprocessor la semántica de que una tarea en ejecución sigue representando trabajo pendiente. La corrección extendió ese criterio al selector del taskpool.

La carga pasó a ser:

tareas en cola + 1 si el ejecutor está procesando una tarea

static long taskpool_taskprocessor_load(struct taskpool_taskprocessor *tp)
{
	return ast_taskprocessor_size(tp->taskprocessor)
		+ ast_taskprocessor_is_executing(tp->taskprocessor);
}

Con este cambio, un ejecutor ocupado deja de parecer ocioso al momento de elegir el destino de una tarea. La señal también participa en la política de carga existente, pero no redefine por sí sola cuándo debe crecer el pool.

El costo de sincronización

El nuevo acceso al estado de ejecución no toma el lock del taskprocessor. Esa decisión se midió antes de enviar el cambio porque se encuentra en un camino especialmente sensible al rendimiento.

Microbenchmark de 30 segundosSin lockCon lock
Envío directo al pool325 mil tareas/s53 mil tareas/s
Envío mediante serializer217 mil tareas/s38 mil tareas/s

Tomar el lock reducía el rendimiento del microbenchmark aproximadamente seis veces. La versión sin lock quedó prácticamente al nivel del código base. Como el selector trabaja con una fotografía instantánea de un sistema concurrente, la lectura puede quedar desactualizada de inmediato; esa condición también existe al consultar el tamaño de las colas.

Exponer este trade-off desde el inicio permitió que la revisión se concentrara en la decisión real: mejorar la señal de carga sin introducir una penalización considerable en el camino de asignación.

La mitad del parche que retiramos

La propuesta inicial incluía un segundo cambio: hacer crecer el pool cuando todos los ejecutores estuvieran ocupados con tareas largas, incluso si sus colas todavía no superaban el umbral configurado.

Joshua Colp explicó que ese comportamiento era contrario al diseño del taskpool. El componente fue concebido bajo la premisa de que mantener trabajo en cola es aceptable y, según las mediciones del proyecto, más eficiente que crear hilos ante cada operación lenta.

Retiramos esa parte y la prueba asociada. Una vez aclarada la intención del diseño, ya no describían un defecto.

Sobre el problema del selector, la conclusión del maintainer fue directa:

“The fact that a taskpool taskprocessor executing isn't considered in use is a bug however.”

“Que un taskprocessor del taskpool en ejecución no se considere en uso sí es un bug.”

Aceptar una revisión no significa defender cada línea enviada. En este caso, reducir el alcance hizo que el cambio final fuera más preciso: corregir el defecto confirmado sin alterar la política de crecimiento diseñada por el proyecto.

Ese es también el valor de contribuir upstream. Un parche local puede resolver una necesidad inmediata, pero deja una diferencia que alguien tendrá que mantener. Llevarlo al proyecto permite validar el razonamiento con quienes conocen el diseño, mejorar la solución y convertirla en una corrección disponible para toda la comunidad.

Estado del cambio

El PR fue integrado el 11 de agosto de 2026 en master y aplicado también a las ramas 20, 22 y 23.

Al 14 de agosto de 2026, la corrección todavía no aparece en una versión estable publicada ni en los release candidates actuales, que fueron generados antes del merge. Antes de actualizar, conviene revisar el changelog de la versión correspondiente y confirmar que incluya el cambio.

No requiere una opción nueva de configuración: una vez incorporado en una versión, el selector utilizará la carga corregida.

Qué revisar si operas Asterisk

  • Ejecuta taskprocessor show stats bajo condiciones representativas y revisa la profundidad máxima y los tiempos de procesamiento de las colas.
  • Si una cola acumula valores inusuales, investiga qué componente la alimenta antes de aumentar el número de ejecutores.
  • Revisa las operaciones sincrónicas en el camino de señalización, especialmente consultas DNS, HTTP o de base de datos, y configura timeouts explícitos.
  • Recuerda que no todos los cuellos de Asterisk pertenecen al taskpool: algunos componentes son seriales por diseño y requieren otro enfoque.

De la contribución a lo que estamos construyendo

El hallazgo apareció mientras desarrollábamos galcymedia, una librería con la que estamos simplificando la integración de agentes de voz con IA y Asterisk mediante WebSocket. La publicaremos cuando sus transportes, ejemplos y documentación estén listos para reproducirse de extremo a extremo.

En Galcyon diseñamos e integramos soluciones de telefonía e IA sobre Asterisk. Este artículo abre una serie en la que compartiremos problemas reales de ingeniería con el mismo criterio: diseño explicado, evidencia reproducible y código verificable.

Conoce la contribución completa en GitHub y descubre cómo construimos soluciones de telefonía e IA desde Galcyon.


Referencias

Nota de metodología: el análisis y la preparación inicial del reporte contaron con asistencia de IA, y así se declaró en el pull request, como pide la política del proyecto. Verificamos cada referencia contra el código de Asterisk, descartamos las hipótesis que no se sostenían y asumimos la responsabilidad técnica de lo publicado.

Gian Diego Javes

CTO, Galcyon Solutions

¿Te interesa conocer nuestros productos?