Django 6.1 integration¶
goatbeat-django is a regular Django app. It needs PostgreSQL: the coordination SQL relies on
row locks and LISTEN/NOTIFY.
Setup¶
INSTALLED_APPS = [..., "goatbeat.django"]
GOATBEAT = { # every key is optional
"database": "default", # a PostgreSQL alias
"timezone": "Europe/Paris", # defaults to TIME_ZONE
"lease_ttl": "30s",
"lease_renew_interval": "10s",
"max_sleep": "60s",
"batch_size": 500,
"max_submit_retries": 5,
"scheduler_id": "web-1", # defaults to host:pid:random
}
The setting is validated when Django starts. Any invalid value raises ImproperlyConfigured
listing every problem at once; nothing falls back to a default silently.
python manage.py migrate creates the tables. Their schema is identical to the one
goatbeat migrate creates for plain Python projects; a test keeps the two in sync.
Declaring schedules¶
Each app may have a schedules.py defining register(beat), discovered like admin.py:
from goatbeat.django import DjangoRegistry, call
from myapp.tasks import generate_report, send_email
def register(beat: DjangoRegistry) -> None:
beat.schedule(name="daily-report", task=generate_report, cron="0 9 * * *")
beat.schedule(name="ping", task=call(send_email, 42), every="5m", queue="emails")
The management command¶
python manage.py goatbeat scheduler # run until SIGTERM; --once for one tick
python manage.py goatbeat schedule list --json # publish code schedules, list all
python manage.py goatbeat schedule run NAME # ask the leader for an extra run
python manage.py goatbeat schedule prune # disable code schedules no longer declared
python manage.py goatbeat import_celery_beat # see the migration guide
The standalone goatbeat command also works: with DJANGO_SETTINGS_MODULE set and neither
--app nor GOATBEAT_APP, it uses the Django project.
Give the scheduler process persistent connections (CONN_MAX_AGE, CONN_HEALTH_CHECKS).
Goatbeat reuses its connection and replaces it only after an error.
Logging¶
Goatbeat logs to the goatbeat logger. The event name is the message and the details
(schedule, idempotency_key, scheduler_id, ...) are structured fields, which a plain
formatter drops. Use the JSON formatter to keep them:
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"formatters": {"json": {"()": "goatbeat.core.observability.JsonFormatter"}},
"handlers": {"goatbeat": {"class": "logging.StreamHandler", "formatter": "json"}},
"loggers": {"goatbeat": {"handlers": ["goatbeat"], "level": "INFO", "propagate": False}},
}
Admin¶
- Schedules: list, filters, enabled flag, next run and last status, recent executions inline.
Actions: enable, disable, run now (they need the
changepermission). - Creating and editing: cron or interval, timezone, JSON arguments, misfire policy and
routing. Every value is validated by the same core types as code declarations, and saves go
through the store, so
next_run_atfollows the scheduling rules. - Code schedules are read-only except
enabled: their definition lives in your code. - Executions are read-only history, filterable by status.