/api/qmail/local/beacon/control

GET POST

Runtime control for the background beacon polling thread. Lets a tester or the GUI pause, resume, or query the thread without restarting the daemon. Useful for deterministic /api/qmail/net/beacon/ping testing (the manual long-poll won't race the background thread) and for in-prod troubleshooting.

Description

The /api/qmail/local/beacon/control endpoint is a thin wrapper over the singleton g_qmail_beacon_state background thread. The same JSON response is returned for all three actions, so a caller can confirm the state transition with a single call.

Why You'd Stop the Beacon
  • Deterministic /ping testing: the background thread polls the beacon RAIDA on its own cadence. If the thread fires while a test is awaiting /ping, the manual call returns immediately with whatever the thread just consumed. Stop the thread first to pin down behavior.
  • Triage: stop the beacon for a minute, manually drive PEEK/PING/DOWNLOAD, then resume to confirm the background loop recovers cleanly.
  • Quiet logs: the background thread logs every poll cycle. Stopping it temporarily makes main.log easier to read while reproducing an unrelated issue.
No Persistence

This endpoint changes runtime state only — it does not update any config file. After a daemon restart, the beacon thread is started or skipped according to the boot-time beacon_auto_start config option (Section E1 of the QMail v3 plan).

Parameters

Parameter Type Required Description
action enum No One of start, stop, or status. Defaults to status if omitted or empty. Any other value returns HTTP 400.
Idempotent Actions

start on an already-running thread is a no-op. stop on an already-stopped thread is a no-op. Both still return the current status, so you can poll ?action=status to confirm before issuing a transition.

Response

All three actions return the same JSON shape — the current beacon state. The running field reflects state after the requested action.

Success Response Properties

success boolean
Always true on a successful transition or query.
running boolean
true if the background beacon polling thread is currently running.
beacon_raida integer
RAIDA index (0–24) the local beacon polls.
serial_number integer
Mailbox serial number associated with this client.
denomination integer
Denomination code of the mailbox identity coin.
last_tell_timestamp integer
Unix timestamp (seconds) of the most recent Tell consumed by the beacon. 0 if none has arrived since startup.
backoff_seconds integer
Current poll backoff. Starts at 1 and doubles to a 60-second cap on consecutive errors; resets to 1 on a successful poll.
consecutive_errors integer
Number of consecutive failed poll attempts. 0 when the beacon is healthy.

Success Response Example (status — running)

{
  "success": true,
  "running": true,
  "beacon_raida": 11,
  "serial_number": 55077,
  "denomination": 1,
  "last_tell_timestamp": 1735689812,
  "backoff_seconds": 1,
  "consecutive_errors": 0
}

Success Response Example (after stop)

{
  "success": true,
  "running": false,
  "beacon_raida": 11,
  "serial_number": 55077,
  "denomination": 1,
  "last_tell_timestamp": 1735689812,
  "backoff_seconds": 1,
  "consecutive_errors": 0
}

Error Response — Invalid Action (HTTP 400)

{
  "error": true,
  "message": "Invalid action: use start, stop, or status",
  "code": 400
}

Error Response — Restart Failed (HTTP 500)

{
  "error": true,
  "message": "Failed to start beacon thread",
  "code": 500
}

Interactive API Tester

Test this Endpoint

Examples

cURL — All Three Actions

# Query without changing state
curl "http://localhost:8082/api/qmail/local/beacon/control?action=status"

# Pause the background polling thread
curl "http://localhost:8082/api/qmail/local/beacon/control?action=stop"

# Resume it
curl "http://localhost:8082/api/qmail/local/beacon/control?action=start"

JavaScript (fetch)

// Pause the beacon, run a deterministic /ping test, then resume.
async function withBeaconStopped(testFn) {
    await fetch('http://localhost:8082/api/qmail/local/beacon/control?action=stop').then(r => r.json());
    try {
        return await testFn();
    } finally {
        await fetch('http://localhost:8082/api/qmail/local/beacon/control?action=start').then(r => r.json());
    }
}

const result = await withBeaconStopped(async () => {
    return fetch('/api/qmail/net/beacon/ping?timeout=10').then(r => r.json());
});
console.log('Ping result:', result);

Python

import contextlib
import requests

BASE = 'http://localhost:8082/api'

@contextlib.contextmanager
def beacon_stopped():
    requests.get(f'{BASE}/qmail/local/beacon/control', params={'action': 'stop'})
    try:
        yield
    finally:
        requests.get(f'{BASE}/qmail/local/beacon/control', params={'action': 'start'})

with beacon_stopped():
    # The background thread is paused — /ping won't race it.
    r = requests.get(f'{BASE}/qmail/net/beacon/ping', params={'timeout': 10})
    print('Ping result:', r.json())

Important Notes

Defaults to status

If action is missing or empty, the handler treats it as status — a side-effect-free read. Hit the URL without any params for a quick health check.

Race-Free for Test Harnesses

The background beacon thread polls on its own cadence and consumes pending Tells from the beacon RAIDA. If a test is set up to fire a /ping and assert what it sees, an unrelated background poll between fixture setup and the assertion can race the test. Stopping the thread for the duration of the test fixes that — see the example above.

Don't Forget to Resume

While the beacon is stopped, the daemon does not consume new Tell notifications. If you stop the thread and forget to resume it, downstream features like inbox auto-population stop working until the next daemon restart. Use the context-manager pattern shown in the JavaScript/Python examples to ensure resume always happens.