Skip to main content

Fast and simple async watchdog to monitor heartbeats in asyncio applications.

Project description

async-watchdog

PyPI version Python License: MIT

A fast, simple asyncio watchdog for monitoring heartbeats in async Python applications. If a monitored process stops reporting activity within the configured timeout, async-watchdog fires a callback or logs a warning — keeping your system observable without adding runtime dependencies.


Installation

pip install async-watchdog

Quick Start

import asyncio
from async_watchdog import Watchdog

async def main():
    wd = Watchdog(timeout=5.0)
    await wd.start()

    # Signal that the process is alive
    wd.beat()

    # ... do work, call wd.beat() periodically ...

    await wd.stop()

asyncio.run(main())

The watchdog waits for the first beat() before starting the timeout clock, so slow-starting processes are not penalised.


Usage

Async callback on timeout

The recommended pattern for I/O-bound reactions (sending alerts, closing connections, restarting tasks):

import asyncio
from async_watchdog import Watchdog

async def on_timeout():
    print("No heartbeat received — restarting worker...")
    # await send_alert(...)  # safe: async I/O is fine here

async def worker(wd: Watchdog):
    for _ in range(5):
        await asyncio.sleep(1)
        wd.beat()
    # Worker stops sending beats — timeout will fire after this

async def main():
    wd = Watchdog(timeout=3.0, on_timeout=on_timeout)
    await wd.start()
    try:
        await worker(wd)
        await asyncio.sleep(5)  # Wait for timeout to trigger
    finally:
        await wd.stop()

asyncio.run(main())

Sync callback on timeout

Synchronous callbacks work too. Use them for lightweight, non-blocking reactions such as setting a flag or incrementing a counter:

import asyncio
from async_watchdog import Watchdog

timeout_count = 0

def on_timeout():
    global timeout_count
    timeout_count += 1
    print(f"Timeout #{timeout_count} detected")

async def main():
    wd = Watchdog(timeout=2.0, on_timeout=on_timeout)
    await wd.start()
    try:
        wd.beat()           # First beat starts the clock
        await asyncio.sleep(6)  # No more beats — callback fires ~every 2 s
    finally:
        await wd.stop()

asyncio.run(main())

Warning: Sync callbacks must not perform blocking operations (see Security).

No callback — logging only

Omit on_timeout to get a WARNING log entry on every missed heartbeat window. Useful during development or when you only need observability:

import asyncio
from async_watchdog import Watchdog

async def main():
    wd = Watchdog(timeout=5.0)  # No on_timeout
    await wd.start()
    try:
        wd.beat()
        await asyncio.sleep(12)  # Watchdog logs warnings at t=5, t=10
    finally:
        await wd.stop()

asyncio.run(main())

Output:

[Watchdog] WARNING  Timeout of 5.0 seconds reached without heartbeat.
[Watchdog] WARNING  Timeout of 5.0 seconds reached without heartbeat.

Integration in a real worker loop

A typical pattern for a long-running background worker that must report liveness to the watchdog on every iteration:

import asyncio
from async_watchdog import Watchdog

async def on_worker_timeout():
    print("Worker is unresponsive — triggering recovery")
    # await notify_ops_team(...)

async def process_item(item):
    await asyncio.sleep(0.1)  # Simulate work
    return item * 2

async def worker(wd: Watchdog, queue: asyncio.Queue):
    while True:
        item = await queue.get()
        await process_item(item)
        wd.beat()           # Report liveness after each successful iteration
        queue.task_done()

async def main():
    queue = asyncio.Queue()
    wd = Watchdog(timeout=10.0, on_timeout=on_worker_timeout)

    await wd.start()
    worker_task = asyncio.create_task(worker(wd, queue))

    try:
        for i in range(20):
            await queue.put(i)
            await asyncio.sleep(0.5)

        await queue.join()
    finally:
        worker_task.cancel()
        await wd.stop()

asyncio.run(main())

API Reference

Watchdog(timeout, on_timeout=None, logger=None)

Creates a new watchdog instance.

Parameter Type Required Description
timeout float Yes Seconds to wait for a heartbeat before triggering. Must be in [0.001, 86400]. inf, nan, zero, and negatives are rejected.
on_timeout callable | None No Callback invoked on each missed heartbeat window. Accepts both sync and async callables. If None, a WARNING is logged instead.
logger logging.Logger | None No Custom logger instance. Defaults to the package logger with [Watchdog] prefix.

Raises:

  • TypeError — if timeout is not a numeric int or float (booleans excluded), or if on_timeout is not callable.
  • ValueError — if timeout is outside [0.001, 86400] or is not finite.

Methods

await wd.start()

Starts the watchdog background task. The watchdog blocks until the first beat() is received before beginning timeout monitoring.

  • Idempotent: safe to call multiple times; only one background task is ever created.
  • Concurrency-safe: protected by an asyncio.Lock.

wd.beat()

Signals that the monitored process is alive. Resets the timeout window. Synchronous — call it from anywhere in your async code without await.

await wd.stop()

Cancels the background task and cleans up. Safe to call even if start() was never called. After stop(), the watchdog can be restarted with start().


Callback behaviour

Scenario Behaviour
Async callback Awaited directly; non-blocking for the event loop
Sync callback Called inline; must not block (see Security)
Callback raises an exception Exception type logged at ERROR; watchdog continues running
Callback exceeds timeout Cancelled and logged at ERROR; watchdog continues running
No callback provided WARNING logged on every missed heartbeat window

Security

Blocking sync callbacks

Synchronous on_timeout callbacks run directly on the asyncio event loop. Any blocking operation inside a sync callback will freeze the entire event loop for the duration of the call, preventing all other coroutines from making progress.

Do not use in sync callbacks:

  • time.sleep()
  • Blocking file or network I/O
  • CPU-intensive computation

Safe alternatives:

# Option 1: Use an async callback for I/O
async def on_timeout():
    await asyncio.sleep(1)          # Non-blocking
    await send_alert_via_http()     # Non-blocking

# Option 2: Offload blocking work to a thread pool
import asyncio

def blocking_work():
    time.sleep(1)  # Runs in a thread, not the event loop

async def on_timeout():
    loop = asyncio.get_running_loop()
    await loop.run_in_executor(None, blocking_work)

Log injection

All exception messages written to logs are sanitised (control characters stripped) to prevent log injection attacks.


Requirements

  • Python: >= 3.10
  • Runtime dependencies: none
  • Optional: uvloop for higher throughput (drop-in replacement for the default asyncio event loop)

License

MIT — Álvaro Suárez Lozano

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

async_watchdog-0.0.1.tar.gz (10.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

async_watchdog-0.0.1-py3-none-any.whl (7.1 kB view details)

Uploaded Python 3

File details

Details for the file async_watchdog-0.0.1.tar.gz.

File metadata

  • Download URL: async_watchdog-0.0.1.tar.gz
  • Upload date:
  • Size: 10.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.11.13

File hashes

Hashes for async_watchdog-0.0.1.tar.gz
Algorithm Hash digest
SHA256 7136da4354cbec543953ccf440b3f10516864036af3477ecb08075662ccc4f4a
MD5 ca0ad81046a029c59b69acf468537404
BLAKE2b-256 cd0d223dfaf870bacec8a65354d6897ac949eb31cde060962fb949e24c7b816c

See more details on using hashes here.

File details

Details for the file async_watchdog-0.0.1-py3-none-any.whl.

File metadata

  • Download URL: async_watchdog-0.0.1-py3-none-any.whl
  • Upload date:
  • Size: 7.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.11.13

File hashes

Hashes for async_watchdog-0.0.1-py3-none-any.whl
Algorithm Hash digest
SHA256 312d457aa4f156d24ea5f116f453cc39b71fdcc73e8368bbda25ae65d57deecf
MD5 53bfc4281d427f339bf9d46bac3df866
BLAKE2b-256 234108ea05a0742b032e0c182e0f05281baf5117afd5c689f06802f393228843

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page