diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index b250e3a6..e2f3a266 100755 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -4,16 +4,17 @@ set -e set -u set -x +export PYTHONPATH="/app:${PYTHONPATH-}" + if [ z"$1" = "zmigrate" ]; then - COMMAND="/app/.venv/bin/django-admin migrate --settings firetower.settings" + exec /app/.venv/bin/ddtrace-run /app/.venv/bin/django-admin migrate --settings firetower.settings elif [ z"$1" = "zserver" ]; then - COMMAND="/app/.venv/bin/granian --interface wsgi --host 0.0.0.0 --port $PORT firetower.wsgi:application" + exec /app/.venv/bin/ddtrace-run /app/.venv/bin/granian --interface wsgi --host 0.0.0.0 --port "${PORT}" firetower.wsgi:application elif [ z"$1" = "zslack-bot" ]; then - COMMAND="/app/.venv/bin/django-admin run_slack_bot --settings firetower.settings" + exec /app/.venv/bin/ddtrace-run /app/.venv/bin/django-admin run_slack_bot --settings firetower.settings +elif [ z"$1" = "zworker" ]; then + exec /app/.venv/bin/ddtrace-run /app/.venv/bin/django-admin qcluster --settings firetower.settings else - echo "Usage: $0 (migrate|server|slack-bot)" + echo "Usage: $0 (migrate|server|slack-bot|worker)" exit 1 fi - -export PYTHONPATH=/app:\$PYTHONPATH -/app/.venv/bin/ddtrace-run $COMMAND diff --git a/pyproject.toml b/pyproject.toml index e2eb1555..868fcf2e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,6 +19,7 @@ dependencies = [ "pyserde[toml]>=0.28.0", "notion-client>=3.0.0,<4.0.0", "requests>=2.32.0", + "django-q2>=1.7.4", "slack-bolt>=1.27.0", "slack-sdk>=3.31.0", "sentry-sdk>=2.47.0", diff --git a/src/firetower/config.py b/src/firetower/config.py index 791e6325..b7c4d12c 100644 --- a/src/firetower/config.py +++ b/src/firetower/config.py @@ -159,7 +159,7 @@ def __init__(self) -> None: self.pagerduty = None self.statuspage = None self.project_key = "" - self.django_secret_key = "" + self.django_secret_key = "dummy_value_DO_NOT_USE" self.sentry_dsn = "" self.region_grouping: list[list[str]] = [] self.firetower_base_url = "" diff --git a/src/firetower/incidents/migrations/0015_schedule_demo.py b/src/firetower/incidents/migrations/0015_schedule_demo.py new file mode 100644 index 00000000..da10d77f --- /dev/null +++ b/src/firetower/incidents/migrations/0015_schedule_demo.py @@ -0,0 +1,28 @@ +from django.db import migrations + +from firetower.incidents.tasks import SCHEDULES + + +def create_schedule(apps, schema_editor): + Schedule = apps.get_model("django_q", "Schedule") + schedule_name = "schedule_demo" + Schedule.objects.get_or_create( + name=schedule_name, defaults=SCHEDULES[schedule_name] + ) + + +def delete_schedule(apps, schema_editor): + Schedule = apps.get_model("django_q", "Schedule") + schedule_name = "schedule_demo" + Schedule.objects.filter(name=schedule_name).delete() + + +class Migration(migrations.Migration): + dependencies = [ + ("incidents", "0014_add_total_downtime"), + ("django_q", "0018_task_success_index"), + ] + + operations = [ + migrations.RunPython(create_schedule, delete_schedule), + ] diff --git a/src/firetower/incidents/tasks.py b/src/firetower/incidents/tasks.py new file mode 100644 index 00000000..6cec5ddf --- /dev/null +++ b/src/firetower/incidents/tasks.py @@ -0,0 +1,20 @@ +from logging import info + +from firetower.incidents.models import Incident + +SCHEDULES = { + "schedule_demo": { + "func": "firetower.incidents.tasks.schedule_demo", + "schedule_type": "I", # Minutes + "minutes": 5, + "repeats": -1, # repeat indefinitely + }, +} + + +def schedule_demo() -> None: + incident = Incident.objects.order_by("-created_at").first() + if incident: + info(f"Most recent incident: INC-{incident.id}: {incident.title}") + else: + info("No incidents found.") diff --git a/src/firetower/settings.py b/src/firetower/settings.py index 62b7e07f..10f1522c 100644 --- a/src/firetower/settings.py +++ b/src/firetower/settings.py @@ -123,6 +123,7 @@ def _coerce_region_grouping(raw: list[Any]) -> list[list[str]]: "firetower.incidents", "firetower.integrations", "firetower.slack_app", + "django_q", ] MIDDLEWARE = [ @@ -374,3 +375,13 @@ class StatuspageSettings(TypedDict): }, }, } + +Q_CLUSTER = { + "name": "firetower", + "orm": "default", + "workers": 4, + "timeout": 180, + "retry": 210, + "queue_limit": 50, + "bulk": 10, +} diff --git a/uv.lock b/uv.lock index 4d83d82f..da5d2da4 100644 --- a/uv.lock +++ b/uv.lock @@ -479,6 +479,31 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fd/d0/14e763857e44fc8d846f786527c777d6b85d649e5d990e29cf2b468a91d9/django_kubernetes-1.1.0-py3-none-any.whl", hash = "sha256:068cca992a6b3f8030774618ce23ee22cf68b565a35faa46bb6ccd97f034c029", size = 13731, upload-time = "2025-06-18T16:46:59.95Z" }, ] +[[package]] +name = "django-picklefield" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "django" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/93/03/13114bccbd1ec8c026ac1ff33dae75ae6c6a5632e4769ee9cda283b9f57e/django_picklefield-3.4.0.tar.gz", hash = "sha256:3a1f740536c0e60d0dba43aa89ccdbe86760d4c3f8ec47799eae122baa741d0a", size = 12555, upload-time = "2025-11-27T03:11:53.13Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/b7/139eb1419ca7b27fd714925b8d0eed6efb592479dcf2155fed6c0c87c956/django_picklefield-3.4.0-py3-none-any.whl", hash = "sha256:929bcfbae5b48bd22a52bc04521fdfdd152eee36abb9f20228f9480f9df65f45", size = 10031, upload-time = "2025-11-27T03:11:51.937Z" }, +] + +[[package]] +name = "django-q2" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "django" }, + { name = "django-picklefield" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/e6/21375bed54a4be1339f6ee31e4173d361d457dbe91db7bff130b52566126/django_q2-1.9.0.tar.gz", hash = "sha256:ef7facca96fae9c11ddf2c5252d3817975c7a9a6d989fa0d65487d8823d57799", size = 77218, upload-time = "2025-12-04T22:11:29.336Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3e/b7/8282f9815fc9df3187d9303a6f54e0388e02742255dee1fed7b4019a03ae/django_q2-1.9.0-py3-none-any.whl", hash = "sha256:4eded27644b0ffb291839c9f9c12fea6c0dec63ebd891fa6881b0b446098a49d", size = 89615, upload-time = "2025-12-04T22:11:28.079Z" }, +] + [[package]] name = "django-stubs" version = "5.2.7" @@ -549,6 +574,7 @@ dependencies = [ { name = "django" }, { name = "django-cors-headers" }, { name = "django-kubernetes" }, + { name = "django-q2" }, { name = "djangorestframework" }, { name = "google-auth" }, { name = "google-genai" }, @@ -588,6 +614,7 @@ requires-dist = [ { name = "django", specifier = ">=5.2.9,<6" }, { name = "django-cors-headers", specifier = ">=4.9.0" }, { name = "django-kubernetes", specifier = ">=1.1.0" }, + { name = "django-q2", specifier = ">=1.7.4" }, { name = "djangorestframework", specifier = ">=3.15.2" }, { name = "google-auth", specifier = ">=2.37.0" }, { name = "google-genai", specifier = ">=1.0.0" },