]> git.ipfire.org Git - thirdparty/rspamd.git/commit
[Feature] lua_tcp: phase-specific timeouts and on_error callback 6034/head
authorVsevolod Stakhov <vsevolod@rspamd.com>
Sun, 10 May 2026 09:25:14 +0000 (10:25 +0100)
committerVsevolod Stakhov <vsevolod@rspamd.com>
Sun, 10 May 2026 09:25:14 +0000 (10:25 +0100)
commit25545db649b45a8071e2917a9a2fcb2205c62151
treea88474a06c29ccb7ece1203e94d78409026051a8
parent550e601fe4150259f6a20eaf4d309d9bb484dd54
[Feature] lua_tcp: phase-specific timeouts and on_error callback

Two opt-in additions to rspamd_tcp.new, motivated by issue #6032 (mx_check
probe shapes — connect-vs-read budget independence and connect-phase error
routing without dummy-queueing a read handler).

A. Phase-specific timeouts.

  * New options: connect_timeout, read_timeout, write_timeout. Setting any
    of them switches the request to phased mode: each phase gets its own
    budget, unset phase fields fall back to `timeout`. The watcher is
    re-armed from the appropriate field on every plan_handler_event entry
    (LUA_WANT_READ / LUA_WANT_WRITE / LUA_WANT_CONNECT).

  * Backwards compat: existing callers passing only `timeout` keep the
    current single-deducted-budget contract by construction. A new
    `use_deduction` flag gates both the `elapsed` deduction in
    lua_tcp_handler and the per-phase reset in plan_handler_event. No call
    site changes its observable behaviour unless it actively sets a phase
    field.

  * Rationale (Option 2 from the issue): lua_tcp underpins every AV scanner
    and lualib helper. The HTTP-style "no deduction" alternative would
    silently shift their wall-clock from `<= timeout` to `<= N x timeout`;
    Option 2 avoids that surprise for one extra bool and one extra branch.

B. on_error callback for connect-phase errors.

  * New `on_error(err, conn)` callback fires at most once for failures
    that occur before LUA_TCP_FLAG_CONNECTED is set: DNS resolution, socket
    creation, connect refused/timeout, SSL handshake. Once the connection
    is established, errors continue to flow through the queued read/write
    callback unchanged.

  * Routing is exclusive: when on_error is set and we are pre-CONNECTED,
    the error goes there alone (no queue-walking fanout). One-shot — the
    ref is dropped on first fire so subsequent failures fall through to
    the regular handler path. SSL handshake errors land here because
    LUA_TCP_FLAG_CONNECTED is only set after the handshake completes.

  * Pure-probe support: a request with `read = false`, no `data`, and an
    on_error/on_connect would previously short-circuit (empty handler
    queue -> "no handlers left, finish session" before the dial ever
    completed). The constructor now pushes a LUA_WANT_CONNECT marker in
    that shape so plan_handler_event arms EV_WRITE; lua_tcp_connect_helper
    handles the async case (shift the marker, re-plan, let the empty queue
    drive the FINISHED tear-down) — previously it dereferenced cbd->thread
    unconditionally and was sync-only.

C. Tests (test/functional/lua/tcp.lua + cases/230_tcp.robot).

  * PHASED_TIMEOUT_TEST — phased timeouts on the success path emit
    PHASED_TCP_OK.
  * ON_ERROR_REFUSED_TEST — connect to closed port 1, no read/data; only
    the on_error callback fires (regular callback must not).
  * ON_ERROR_POST_CONNECT_TEST — connect succeeds against dummy_http
    /timeout, read_timeout=0.5 trips post-CONNECTED; the read callback
    receives the timeout, on_error must NOT fire.
src/lua/lua_tcp.c
test/functional/cases/230_tcp.robot
test/functional/lua/tcp.lua