Metadata-Version: 2.5
Name: celery-multibeat-scheduler
Version: 0.1.0
Summary: Redis-backed leader election for redundant Celery Beat schedulers
Project-URL: Repository, https://github.com/inueni/celery-multibeat-scheduler
Project-URL: Issues, https://github.com/inueni/celery-multibeat-scheduler/issues
Author-email: Mitja Pagon <mitja@inueni.com>
License-Expression: MIT
License-File: LICENSE
Requires-Python: >=3.10
Requires-Dist: celery>=5.5
Requires-Dist: django-celery-beat>=2.8.1
Requires-Dist: redis>=6.4.0
Description-Content-Type: text/markdown

# celery-multibeat-scheduler

A Celery Beat scheduler with automatic failover, backed by Redis leader election
for multi-server deployments.

## Features

- [Automatic failover across servers with atomic leader election.](#automatic-failover)
- [Configurable lease and failover timing.](#lease-timing)
- [Watchdog detection of stalled schedulers.](#watchdog)
- [Redis TLS and Sentinel support.](#redis-connections)
- [Extensive test coverage.](#development)

## Requirements

- Python 3.10+
- Celery 5.5+
- django-celery-beat 2.8.1+
- redis-py 6.4.0+

All Beat instances need access to the same Redis instance and Django database.

## Quick start

```sh
pip install celery-multibeat-scheduler
```

In Django settings, with Celery’s `namespace="CELERY"`:

```python
INSTALLED_APPS += ["django_celery_beat"]

CELERY_BEAT_SCHEDULER = "multibeat.django.MultiBeatDatabaseScheduler"
CELERY_MULTIBEAT_URL = "redis://redis:6379/0"
CELERY_MULTIBEAT_LOCK_KEY = "my-service:beat:lock"
```

Apply migrations:

```sh
python manage.py migrate
```

Start Beat on each server, replacing `your_project` with your Celery application:

```sh
celery -A your_project beat
```

> [!NOTE]
> Run Beat as a separate process with automatic restart. Fatal watchdog or
> lock-manager failures terminate the entire process, including the worker if
> Beat runs inside one.

All instances must use the same schedule database, Redis database, and lock key.

## How it works

### Leader election

Each Beat instance competes for the same Redis lock. The lock grants a time-limited
lease to one instance, which becomes the active scheduler.

Lock acquisition and renewal use an atomic Redis Lua script, so ownership checks
and lease updates happen as one operation.

The active scheduler renews its lease in a background thread. Standby instances
keep checking for an opportunity to take over.

### Automatic failover

When the active scheduler releases the lock or its lease expires, a standby can
acquire it.

Before dispatching tasks, the new leader reloads the schedule and execution history
from `django-celery-beat`. Each takeover uses the shared database, rather than the
standby’s cached state.

> [!NOTE]
> After a crash, takeover waits for the remaining lease to expire and the next
> standby acquisition attempt. With default settings, this can take up to
> 90 seconds, plus Redis and database delays. Normal shutdown releases the lock
> without waiting for expiry.

Lease checks prevent an instance with expired leadership from starting a new
dispatch.

> [!NOTE]
> Make tasks safe to run more than once. Process pauses and Redis failover can
> cause duplicate dispatches. Lease checks cannot stop task publication or
> database writes already in progress. Use idempotent tasks or application-level
> deduplication where repeated execution matters.

### Watchdog

The lock manager monitors scheduler heartbeats. If the scheduler stalls beyond the
watchdog deadline, the process exits with status `1`.

## Configuration

Settings below use Celery’s `CELERY` namespace.

| Setting | Default | Purpose |
| --- | --- | --- |
| `CELERY_MULTIBEAT_URL` | Broker URL | Redis connection for leader election |
| `CELERY_MULTIBEAT_LOCK_KEY` | `celery:multibeat:lock` | Lock shared by Beat instances |
| `CELERY_MULTIBEAT_DB` | From the URL | Override the Redis database |
| `CELERY_MULTIBEAT_LOCK_TIMEOUT` | `60` | Lease duration in seconds |
| `CELERY_MULTIBEAT_WATCHDOG_GRACE_SECONDS` | `30` | Additional time before the watchdog fires |

Use a different lock key for each independent service or environment.

### Lease timing

By default, the active scheduler renews its 60-second lease every 45 seconds.
Standby instances attempt acquisition every 30 seconds.

A shorter lease reduces failover time but leaves less room for network delays and
process pauses. Refresh and standby intervals scale with the lease, subject to
configured minimums.

### Redis connections

For TLS, use a `rediss://` URL. Additional connection options belong in
`CELERY_MULTIBEAT_TRANSPORT_OPTIONS`.

For Sentinel, list the nodes and set the master name:

```python
CELERY_MULTIBEAT_URL = "sentinel://first:26379/0;sentinel://second:26379/0"
CELERY_MULTIBEAT_TRANSPORT_OPTIONS = {
    "master_name": "beat-master",
}
```

See the [configuration reference](docs/reference.md) for all settings, timing
constraints, TLS certificates, Sentinel credentials, and retry policies.

## Development

The suite covers leadership changes, dispatch safety, watchdog failures, and
failover between live scheduler processes.

From a local checkout:

```sh
uv sync
uv run pytest
uv build
```

CI tests installed wheels on Python 3.10–3.14 against the minimum and latest
supported redis-py versions.

### File scheduler

The package also includes `MultiBeatPersistentScheduler`, an optional file
scheduler for development and testing:

```sh
celery -A your_project beat \
    --scheduler multibeat.schedulers.MultiBeatPersistentScheduler \
    --schedule=/path/to/local.schedule
```

It uses Redis for leader election but stores the schedule and execution history
locally. Give each instance its own file. Without shared history, it is unsuitable
for multi-server deployments.
