Skip to content

fix: re-arm process timeout on each retry - #5

Open
raprav wants to merge 2 commits into
runnerty:mainfrom
raprav:fix/rearm-process-timeout-on-retry
Open

fix: re-arm process timeout on each retry#5
raprav wants to merge 2 commits into
runnerty:mainfrom
raprav:fix/rearm-process-timeout-on-retry

Conversation

@raprav

@raprav raprav commented Aug 17, 2026

Copy link
Copy Markdown
Member

🇬🇧 English

Problem

The process-level timeout is armed once by runnerty core, right before the first execution (lib/classes/process.js), and cleared at the top of Executor.end(). The retry branch of end() re-runs the executor via execMain() without re-arming it, so from the first retry onwards the process runs completely unbounded. If a retry hangs (e.g. a connection that never settles), the process promise never resolves, the chain stays running forever and the scheduler silently skips every subsequent execution.

The failure is also invisible by default: with notificate_only_last_fail the intermediate error emits nothing, on_retry is rarely configured, and a retry does not log PROCESS START again — a hung retry produces zero output.

This is not theoretical. It is the mechanism behind three production outages in the Páez chains (Jun 30, Jul 1 and Aug 2): processes configured with retries: 3 and timeout: { delay: '30s', action: 'error' } died with no trace at all. A forensic sweep over 53 days of logs shows the process timeout firing exactly once in production — always on a first attempt, never on the stalled ones, consistent with the stalls being silent retries.

Fix

Re-arm the timer inside the retry branch, right before execMain(), mirroring the arming code in runnerty core (killMain('timeout', { end: action }) + process.time_out()). 12 lines, no new options, no behaviour change for processes without timeout or without retries.

Reproduction

test/executor-timeout-retry.test.js drives the real Executor class with an exec() that never settles (no database needed) and the exact process contract used by runnerty core:

node --test        # or: node test/executor-timeout-retry.test.js

On main the timeout fires once, the first retry runs unbounded and the process never settles (the test fails after its 3s watchdog). With this fix, every attempt (initial + retries) is bounded by the timeout and the process finally settles with an error.

It can also be seen with a real chain: a SELECT SLEEP(600) process with retries: 3 and timeout: { delay: '5s', action: 'error' } — on main runnerty hangs forever after the first retry; with the fix it logs 4 timeouts, exhausts the retries and releases the chain in ~35s.


🇪🇸 Español

Problema

El timeout a nivel de proceso lo arma runnerty core una sola vez, justo antes de la primera ejecución (lib/classes/process.js), y Executor.end() lo limpia nada más entrar. La rama de retries de end() relanza el executor vía execMain() sin re-armarlo: a partir del primer retry el proceso corre sin ningún límite. Si un retry se cuelga (p. ej. una conexión que nunca responde), la promesa del proceso no se resuelve jamás, la cadena se queda running para siempre y el scheduler omite en silencio todas las ejecuciones siguientes.

Además el fallo es invisible por defecto: con notificate_only_last_fail el error intermedio no emite nada, on_retry rara vez está configurado, y un retry no vuelve a loguear PROCESS START — un retry colgado produce cero output.

No es teórico: es el mecanismo de tres paradas de producción en las cadenas de Páez (30-jun, 1-jul y 2-ago): procesos con retries: 3 y timeout: { delay: '30s', action: 'error' } murieron sin dejar una sola línea. Un barrido forense de 53 días de logs muestra el timeout de proceso disparando exactamente una vez en producción — siempre en un primer intento, nunca en los cuelgues, consistente con que los cuelgues fueran retries silenciosos.

Fix

Re-armar el timer dentro de la rama de retry, justo antes de execMain(), espejando el código de armado de runnerty core (killMain('timeout', { end: action }) + process.time_out()). 12 líneas, sin opciones nuevas, sin cambio de comportamiento para procesos sin timeout o sin retries.

Reproducción

test/executor-timeout-retry.test.js ejercita la clase Executor real con un exec() que nunca termina (no necesita base de datos) y el contrato de proceso exacto que usa runnerty core:

node --test        # o: node test/executor-timeout-retry.test.js

En main el timeout dispara una vez, el primer retry corre sin límite y el proceso no settlea nunca (el test falla tras su watchdog de 3s). Con el fix, cada intento (inicial + retries) queda acotado por el timeout y el proceso termina finalmente con error.

También se ve con una cadena real: un proceso SELECT SLEEP(600) con retries: 3 y timeout: { delay: '5s', action: 'error' } — en main runnerty se queda colgado para siempre tras el primer retry; con el fix loguea 4 timeouts, agota los retries y libera la cadena en ~35s.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant