OverviewFilesCommitsIssuesDocsReleasesPeers

atproto-bridge / pds / test / deadline.test.js

62 lines · javascript ref:
// withDeadline — the settle guarantee both doFetch editions were missing.
//
// WHY THIS EXISTS: `doFetch` rejects only on the request's 'error' event. When the upstream HOST is
// gone (VPN blackhole — packets dropped, no RST), bare-http1's global agent times the socket out at
// 5s and destroys it, which reaches the request as a plain `destroy()` with NO error. Nothing rejects,
// so the promise dangles and the proxied XRPC hangs until the CLIENT gives up. Measured against a
// blackholed host: still pending after 30s. A connection REFUSED rejects in ~5ms, so the failure is
// specific to "host unreachable", which is exactly the tabertron-is-off case.
//
// The fix is a deadline that (a) rejects, and (b) hands back a hook so the caller can destroy the
// in-flight request instead of leaking the socket.
import test from 'brittle'
import { withDeadline, UPSTREAM_TIMEOUT } from '../lib/deadline.js'

const never = () => new Promise(() => {})
const after = (ms, v) => new Promise((resolve) => setTimeout(() => resolve(v), ms))

test('passes a value through when it settles inside the deadline', async (t) => {
  const r = await withDeadline(after(5, 'ok'), 200)
  t.is(r, 'ok', 'the resolved value is returned untouched')
})

test('rejects with a typed timeout error when it does not', async (t) => {
  await t.exception(() => withDeadline(never(), 30), /timed out/i, 'rejects rather than dangling')
  try {
    await withDeadline(never(), 30)
  } catch (e) {
    t.is(e.code, UPSTREAM_TIMEOUT, 'carries a code the proxy can branch on (failover vs pass-through)')
    t.is(e.timeoutMs, 30, 'reports the deadline it blew')
  }
})

test('calls the abort hook exactly once on timeout — the socket must not leak', async (t) => {
  let aborts = 0
  await t.exception(() => withDeadline(never(), 30, () => { aborts++ }), /timed out/i)
  await after(60)
  t.is(aborts, 1, 'aborted once, and not again after the rejection')
})

test('does NOT abort when the call finishes in time', async (t) => {
  let aborts = 0
  const r = await withDeadline(after(5, 'ok'), 200, () => { aborts++ })
  await after(250) // outlive the original deadline — a cleared timer must not fire
  t.is(r, 'ok')
  t.is(aborts, 0, 'timer cleared on success')
})

test('an upstream error passes through unchanged, never masked as a timeout', async (t) => {
  const boom = Object.assign(new Error('ECONNREFUSED'), { code: 'ECONNREFUSED' })
  try {
    await withDeadline(Promise.reject(boom), 200)
    t.fail('should have rejected')
  } catch (e) {
    t.is(e.code, 'ECONNREFUSED', 'the real cause survives — refused is not a timeout')
  }
})

test('a non-positive deadline disables the guard (opt-out stays possible)', async (t) => {
  const r = await withDeadline(after(20, 'ok'), 0)
  t.is(r, 'ok', 'ms<=0 means no deadline')
})