diff --git a/samples/django-celery/.devcontainer/Dockerfile b/samples/django-celery/.devcontainer/Dockerfile new file mode 100644 index 00000000..ec4e707f --- /dev/null +++ b/samples/django-celery/.devcontainer/Dockerfile @@ -0,0 +1 @@ +FROM mcr.microsoft.com/devcontainers/python:3.12-bookworm diff --git a/samples/django-celery/.devcontainer/devcontainer.json b/samples/django-celery/.devcontainer/devcontainer.json new file mode 100644 index 00000000..67cac5b2 --- /dev/null +++ b/samples/django-celery/.devcontainer/devcontainer.json @@ -0,0 +1,11 @@ +{ + "build": { + "dockerfile": "Dockerfile", + "context": ".." + }, + "features": { + "ghcr.io/defanglabs/devcontainer-feature/defang-cli:1.0.4": {}, + "ghcr.io/devcontainers/features/docker-in-docker:2": {}, + "ghcr.io/devcontainers/features/aws-cli:1": {} + } +} \ No newline at end of file diff --git a/samples/django-celery/.github/workflows/deploy.yaml b/samples/django-celery/.github/workflows/deploy.yaml new file mode 100644 index 00000000..2dd9749d --- /dev/null +++ b/samples/django-celery/.github/workflows/deploy.yaml @@ -0,0 +1,26 @@ +name: Deploy + +on: + push: + branches: + - main + +jobs: + deploy: + environment: playground + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write + + steps: + - name: Checkout Repo + uses: actions/checkout@v4 + + - name: Deploy + uses: DefangLabs/defang-github-action@v1.2.0 + with: + config-env-vars: POSTGRES_PASSWORD SECRET_KEY + env: + POSTGRES_PASSWORD: ${{ secrets.POSTGRES_PASSWORD }} + SECRET_KEY: ${{ secrets.SECRET_KEY }} \ No newline at end of file diff --git a/samples/django-celery/README.md b/samples/django-celery/README.md new file mode 100644 index 00000000..2066bba6 --- /dev/null +++ b/samples/django-celery/README.md @@ -0,0 +1,63 @@ +# Django Celery + +[![1-click-deploy](https://defang.io/deploy-with-defang.svg)](https://portal.defang.dev/redirect?url=https%3A%2F%2Fgithub.com%2Fnew%3Ftemplate_name%3Dsample-django-celery-template%26template_owner%3DDefangSamples) + +This is a sample Django application that uses Celery for background tasks. It uses Postgres as the database and Redis as the message broker. + +## Prerequisites + +1. Download [Defang CLI](https://github.com/DefangLabs/defang) +2. (Optional) If you are using [Defang BYOC](https://docs.defang.io/docs/concepts/defang-byoc) authenticate with your cloud provider account +3. (Optional for local development) [Docker CLI](https://docs.docker.com/engine/install/) + +## Development + +To run the application locally, you can use the following command: + +```bash +docker compose -f compose.dev.yaml up --build +``` + +## Configuration + +For this sample, you will need to provide the following [configuration](https://docs.defang.io/docs/concepts/configuration): + +> Note that if you are using the 1-click deploy option, you can set these values as secrets in your GitHub repository and the action will automatically deploy them for you. + +### `POSTGRES_PASSWORD` +The password for the Postgres database. +```bash +defang config set POSTGRES_PASSWORD +``` + +### `SECRET_KEY` +The [secret key](https://docs.djangoproject.com/en/5.1/ref/settings/#std-setting-SECRET_KEY) for the Django application. +```bash +defang config set SECRET_KEY +``` + +## Deployment + +> [!NOTE] +> Download [Defang CLI](https://github.com/DefangLabs/defang) + +### Defang Playground + +Deploy your application to the Defang Playground by opening up your terminal and typing: +```bash +defang compose up +``` + +### BYOC + +If you want to deploy to your own cloud account, you can [use Defang BYOC](https://docs.defang.io/docs/tutorials/deploy-to-your-cloud). + +--- + +Title: Django Celery + +Short Description: A Django application that uses Celery for background tasks, Postgres as the database, and Redis as the message broker. + +Tags: Django, Celery, Postgres, Redis + +Languages: python, sql diff --git a/samples/django-celery/app/.dockerignore b/samples/django-celery/app/.dockerignore new file mode 100644 index 00000000..ac4f9453 --- /dev/null +++ b/samples/django-celery/app/.dockerignore @@ -0,0 +1,73 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg + +# Django +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal +media/ +static/ +staticfiles/ + +# Virtual Environment +venv/ +ENV/ +.env +.venv + +# Version Control +.git +.gitignore +.gitattributes + +# IDE/Editor files +.idea/ +.vscode/ +*.swp +*.swo +*~ +.DS_Store + +# Testing +.coverage +htmlcov/ +.tox/ +.pytest_cache/ + +# Documentation +docs/ +README.md +LICENSE + +# Docker +Dockerfile* +docker-compose*.yml +.docker/ + +# Celery +celerybeat-schedule +celerybeat.pid + +# Temporary files +*.tmp +.cache/ diff --git a/samples/django-celery/app/.gitignore b/samples/django-celery/app/.gitignore new file mode 100644 index 00000000..93d85a8d --- /dev/null +++ b/samples/django-celery/app/.gitignore @@ -0,0 +1,84 @@ +# Django # +*.log +*.pot +*.pyc +__pycache__/ +local_settings.py +db.sqlite3 +db.sqlite3-journal +media + +# If you are using PostgreSQL +*.psql +*.pgsql +*.sql + +# Python # +*.py[cod] +*$py.class +*.so +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# Virtual Environment # +venv/ +env/ +ENV/ +.env + +# IDE # +.idea/ +.vscode/ +*.swp +*.swo +.DS_Store + +# Celery # +celerybeat-schedule +celerybeat.pid + +# Coverage # +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +coverage.xml +*.cover +.pytest_cache/ + +# Django stuff: +staticfiles/ +staticfiles.json +.static_storage/ +.media/ + +# Jupyter Notebook +.ipynb_checkpoints diff --git a/samples/django-celery/app/Dockerfile b/samples/django-celery/app/Dockerfile new file mode 100644 index 00000000..8707cc5a --- /dev/null +++ b/samples/django-celery/app/Dockerfile @@ -0,0 +1,43 @@ +FROM python:3.12-slim + +# Set environment variables +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + DJANGO_SETTINGS_MODULE=django_celery.settings + +# Create a non-root user +RUN adduser --disabled-password --gecos "" django + +# Install system dependencies (merged from both stages) +RUN apt-get update && apt-get install -y --no-install-recommends \ + libpq5 \ + curl \ + build-essential \ + libpq-dev \ + && rm -rf /var/lib/apt/lists/* + +# Set working directory +WORKDIR /app + +# Install dependencies +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt \ + gunicorn \ + whitenoise \ + dj-database-url \ + psycopg2-binary \ + redis + +# Copy project files +COPY --chown=django:django . . + +# Collect static files +RUN python manage.py collectstatic --noinput + +# Switch to non-root user +USER django + +# Expose port 8000 +EXPOSE 8000 + +CMD [ "./command.sh" ] \ No newline at end of file diff --git a/samples/django-celery/app/command.sh b/samples/django-celery/app/command.sh new file mode 100755 index 00000000..395498cf --- /dev/null +++ b/samples/django-celery/app/command.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +# Apply database migrations +python manage.py migrate + +# Create superuser if not exists +python manage.py createsuperauto + +# Start the Django development server if DEBUG is True +if [ "$DEBUG" = "True" ]; then + python manage.py runserver 0.0.0.0:8000 +else + gunicorn django_celery.wsgi:application --bind 0.0.0.0:8000 --workers 1 --threads 2 --timeout 120 +fi diff --git a/samples/django-celery/app/django_celery/__init__.py b/samples/django-celery/app/django_celery/__init__.py new file mode 100644 index 00000000..070e835d --- /dev/null +++ b/samples/django-celery/app/django_celery/__init__.py @@ -0,0 +1,7 @@ +from __future__ import absolute_import, unicode_literals + +# This will make sure the app is always imported when +# Django starts so that shared_task will use this app. +from .celery import app as celery_app + +__all__ = ('celery_app',) diff --git a/samples/django-celery/app/django_celery/asgi.py b/samples/django-celery/app/django_celery/asgi.py new file mode 100644 index 00000000..c60ce7cb --- /dev/null +++ b/samples/django-celery/app/django_celery/asgi.py @@ -0,0 +1,16 @@ +""" +ASGI config for django_celery project. + +It exposes the ASGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/5.1/howto/deployment/asgi/ +""" + +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "django_celery.settings") + +application = get_asgi_application() diff --git a/samples/django-celery/app/django_celery/celery.py b/samples/django-celery/app/django_celery/celery.py new file mode 100644 index 00000000..5c651538 --- /dev/null +++ b/samples/django-celery/app/django_celery/celery.py @@ -0,0 +1,17 @@ +from __future__ import absolute_import, unicode_literals +import os +from celery import Celery + +# set the default Django settings module +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'django_celery.settings') + +# create the Celery app +app = Celery('django_celery') + +# Using a string here means the worker doesn't have to serialize +# the configuration object to child processes. +app.config_from_object('django.conf:settings', namespace='CELERY') + +# Load task modules from all registered Django app configs. +app.autodiscover_tasks() + diff --git a/samples/django-celery/app/django_celery/settings.py b/samples/django-celery/app/django_celery/settings.py new file mode 100644 index 00000000..3a20be08 --- /dev/null +++ b/samples/django-celery/app/django_celery/settings.py @@ -0,0 +1,193 @@ +""" +Django settings for django_celery project. + +Generated by 'django-admin startproject' using Django 5.1.7. + +For more information on this file, see +https://docs.djangoproject.com/en/5.1/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/5.1/ref/settings/ +""" + +import os +from pathlib import Path +import dj_database_url +import logging + +logger = logging.getLogger(__name__) + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/5.1/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = os.environ.get("SECRET_KEY") + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = os.environ.get("DEBUG") == "True" + +if DEBUG: + ALLOWED_HOSTS = ['*'] +else: + ALLOWED_HOSTS = ['*.prod1a.defang.dev', '*'] # TODO: Update this with your own domain + +if not DEBUG: + SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') + + CSRF_TRUSTED_ORIGINS = [ + 'https://raphaeltm-web--8000.prod1a.defang.dev', # TODO: Update this with your own domain + ] + + +# Application definition + +INSTALLED_APPS = [ + "tasks", + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", +] + +MIDDLEWARE = [ + "django.middleware.security.SecurityMiddleware", + "whitenoise.middleware.WhiteNoiseMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", +] + +ROOT_URLCONF = "django_celery.urls" + +TEMPLATES = [ + { + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", + ], + }, + }, +] + +WSGI_APPLICATION = "django_celery.wsgi.application" + + +# Database +# https://docs.djangoproject.com/en/5.1/ref/settings/#databases + +POSTGRES_URL = os.environ.get("POSTGRES_URL") + +logger.info(f"POSTGRES_URL: {POSTGRES_URL}") + +DATABASES = { + "default": dj_database_url.config( + default=POSTGRES_URL, + conn_max_age=600, + ) +} + + +# Password validation +# https://docs.djangoproject.com/en/5.1/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", + }, +] + + +# Internationalization +# https://docs.djangoproject.com/en/5.1/topics/i18n/ + +LANGUAGE_CODE = "en-us" + +TIME_ZONE = "UTC" + +USE_I18N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/5.1/howto/static-files/ + +STATIC_URL = "static/" +STATIC_ROOT = os.path.join(BASE_DIR, "staticfiles") + +if DEBUG: + STATICFILES_STORAGE = "django.contrib.staticfiles.storage.StaticFilesStorage" +else: + STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage" + +# Default primary key field type +# https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field + +DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" + +# Celery Configuration +CELERY_BROKER_URL = os.environ.get("REDIS_URL") +CELERY_RESULT_BACKEND = os.environ.get("REDIS_URL") +CELERY_ACCEPT_CONTENT = ['json'] +CELERY_TASK_SERIALIZER = 'json' +CELERY_RESULT_SERIALIZER = 'json' +CELERY_TIMEZONE = TIME_ZONE + +if not DEBUG: + # New logging configuration for production + LOGGING = { + 'version': 1, + 'disable_existing_loggers': False, + 'filters': { + 'require_debug_false': { + '()': 'django.utils.log.RequireDebugFalse', + }, + }, + 'handlers': { + 'console': { + 'level': 'INFO', + 'class': 'logging.StreamHandler', + }, + 'mail_admins': { + 'level': 'ERROR', + 'filters': ['require_debug_false'], + 'class': 'django.utils.log.AdminEmailHandler', + }, + }, + 'loggers': { + 'django': { + 'handlers': ['console', 'mail_admins'], + 'level': 'INFO', + 'propagate': True, + }, + # Catch-all logger for other parts of the application + '': { + 'handlers': ['console'], + 'level': 'INFO', + }, + }, + } diff --git a/samples/django-celery/app/django_celery/urls.py b/samples/django-celery/app/django_celery/urls.py new file mode 100644 index 00000000..5dd52d2d --- /dev/null +++ b/samples/django-celery/app/django_celery/urls.py @@ -0,0 +1,27 @@ +""" +URL configuration for django_celery project. + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/5.1/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: path('', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.urls import include, path + 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) +""" + +from django.contrib import admin +from django.urls import path +from tasks.views import add, health, home + +urlpatterns = [ + path("", home), + path("admin/", admin.site.urls), + path("add/", add), + path("health/", health), +] diff --git a/samples/django-celery/app/django_celery/wsgi.py b/samples/django-celery/app/django_celery/wsgi.py new file mode 100644 index 00000000..cb0a41d1 --- /dev/null +++ b/samples/django-celery/app/django_celery/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for django_celery project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/5.1/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "django_celery.settings") + +application = get_wsgi_application() diff --git a/samples/django-celery/app/manage.py b/samples/django-celery/app/manage.py new file mode 100755 index 00000000..302ce4b1 --- /dev/null +++ b/samples/django-celery/app/manage.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" +import os +import sys + + +def main(): + """Run administrative tasks.""" + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "django_celery.settings") + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == "__main__": + main() diff --git a/samples/django-celery/app/requirements.txt b/samples/django-celery/app/requirements.txt new file mode 100644 index 00000000..f737f6af --- /dev/null +++ b/samples/django-celery/app/requirements.txt @@ -0,0 +1,26 @@ +amqp==5.3.1 +asgiref==3.8.1 +billiard==4.2.1 +celery==5.4.0 +click==8.1.8 +click-didyoumean==0.3.1 +click-plugins==1.1.1 +click-repl==0.3.0 +dj-database-url==2.3.0 +Django==5.1.7 +gitdb==4.0.11 +GitPython==3.1.41 +kombu==5.5.0 +prompt_toolkit==3.0.50 +psycopg2-binary==2.9.10 +python-dateutil==2.9.0.post0 +setuptools==69.0.3 +six==1.17.0 +smmap==5.0.1 +sqlparse==0.5.3 +typing_extensions==4.12.2 +tzdata==2025.1 +vine==5.1.0 +watchdog==6.0.0 +wcwidth==0.2.13 +whitenoise==6.9.0 diff --git a/samples/django-celery/app/tasks/__init__.py b/samples/django-celery/app/tasks/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/samples/django-celery/app/tasks/admin.py b/samples/django-celery/app/tasks/admin.py new file mode 100644 index 00000000..8c38f3f3 --- /dev/null +++ b/samples/django-celery/app/tasks/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/samples/django-celery/app/tasks/apps.py b/samples/django-celery/app/tasks/apps.py new file mode 100644 index 00000000..88a2d125 --- /dev/null +++ b/samples/django-celery/app/tasks/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class TasksConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "tasks" diff --git a/samples/django-celery/app/tasks/management/commands/createsuperauto.py b/samples/django-celery/app/tasks/management/commands/createsuperauto.py new file mode 100644 index 00000000..d72d5be0 --- /dev/null +++ b/samples/django-celery/app/tasks/management/commands/createsuperauto.py @@ -0,0 +1,8 @@ +from django.core.management.base import BaseCommand +from django.contrib.auth import get_user_model + +class Command(BaseCommand): + def handle(self, *args, **options): + User = get_user_model() + if not User.objects.filter(username='admin').exists(): + User.objects.create_superuser('admin', 'admin@example.com', 'admin') \ No newline at end of file diff --git a/samples/django-celery/app/tasks/migrations/__init__.py b/samples/django-celery/app/tasks/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/samples/django-celery/app/tasks/models.py b/samples/django-celery/app/tasks/models.py new file mode 100644 index 00000000..71a83623 --- /dev/null +++ b/samples/django-celery/app/tasks/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/samples/django-celery/app/tasks/static/tasks/images/defang-blue.svg b/samples/django-celery/app/tasks/static/tasks/images/defang-blue.svg new file mode 100644 index 00000000..1dee279a --- /dev/null +++ b/samples/django-celery/app/tasks/static/tasks/images/defang-blue.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/samples/django-celery/app/tasks/tasks.py b/samples/django-celery/app/tasks/tasks.py new file mode 100644 index 00000000..27fb13cd --- /dev/null +++ b/samples/django-celery/app/tasks/tasks.py @@ -0,0 +1,19 @@ +import random +from celery import shared_task +import time +from celery.utils.log import get_task_logger + +logger = get_task_logger(__name__) + +@shared_task +def sample_task(param=None): + """ + A sample task that can be called asynchronously + """ + # artificial delay as if the task is doing something + # random between 1 and 5 sec + sleep = random.randint(1, 5) + + time.sleep(sleep) + + return f"Task completed with param: {param}" diff --git a/samples/django-celery/app/tasks/templates/tasks/home.html b/samples/django-celery/app/tasks/templates/tasks/home.html new file mode 100644 index 00000000..7da333e8 --- /dev/null +++ b/samples/django-celery/app/tasks/templates/tasks/home.html @@ -0,0 +1,151 @@ + + + + Task Overview + {% load static %} + + + + + + + +
+
+ {% if added_task %} +

{{ added_task }}

+ {% endif %} + +

Add Task

+
+ + +
+ + +
+ +
+
+
+ {% if show_status %} + +

Active Tasks

+

Total: {{ active.total }}

+ + +

Scheduled Tasks

+

Total: {{ scheduled.total }}

+ + +

Reserved Tasks

+

Total: {{ reserved.total }}

+ + {% else %} + Show Queue Status + {% endif %} +
+
+ + + \ No newline at end of file diff --git a/samples/django-celery/app/tasks/tests.py b/samples/django-celery/app/tasks/tests.py new file mode 100644 index 00000000..7ce503c2 --- /dev/null +++ b/samples/django-celery/app/tasks/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/samples/django-celery/app/tasks/views.py b/samples/django-celery/app/tasks/views.py new file mode 100644 index 00000000..1faece27 --- /dev/null +++ b/samples/django-celery/app/tasks/views.py @@ -0,0 +1,84 @@ +from django.shortcuts import render +from .tasks import sample_task +from django.http import HttpResponse, HttpRequest, HttpResponseRedirect, JsonResponse +from django_celery.celery import app as celery_app +import logging + +logger = logging.getLogger(__name__) + +def add(request: HttpRequest): + """ + Add a task to the queue. + """ + get = dict(request.GET) + sample_task.delay(get) + + # set session var to show the task has been added + request.session["task_added"] = f'Task added with param: {get}' + + # redirect to home + return HttpResponseRedirect("/") + +def home(request: HttpRequest): + """ + Show counts of tasks in the queue. + """ + inspector = celery_app.control.inspect() + + added_task = request.session.get("task_added") + if added_task: + del request.session["task_added"] + + # Initialize default values + active_counts = {} + scheduled_counts = {} + reserved_counts = {} + total_active = 0 + total_scheduled = 0 + total_reserved = 0 + + show_status = request.GET.get('status') == 'true' + + # Only load inspector data if status=true in the URL + if show_status: + # Get the raw data + active_tasks = inspector.active() or {} + scheduled_tasks = inspector.scheduled() or {} + reserved_tasks = inspector.reserved() or {} + + # Calculate counts per worker + active_counts = {worker: len(tasks) for worker, tasks in active_tasks.items()} + scheduled_counts = {worker: len(tasks) for worker, tasks in scheduled_tasks.items()} + reserved_counts = {worker: len(tasks) for worker, tasks in reserved_tasks.items()} + + # Calculate totals + total_active = sum(active_counts.values()) + total_scheduled = sum(scheduled_counts.values()) + total_reserved = sum(reserved_counts.values()) + + context = { + "active": { + "per_worker": active_counts, + "total": total_active + }, + "scheduled": { + "per_worker": scheduled_counts, + "total": total_scheduled + }, + "reserved": { + "per_worker": reserved_counts, + "total": total_reserved + }, + "added_task": added_task, + "show_status": show_status + } + + return render(request, "tasks/home.html", context) + +def health(request: HttpRequest): + """ + Health check endpoint. + """ + return HttpResponse("Healthy") + + diff --git a/samples/django-celery/compose.dev.yaml b/samples/django-celery/compose.dev.yaml new file mode 100644 index 00000000..12c3a307 --- /dev/null +++ b/samples/django-celery/compose.dev.yaml @@ -0,0 +1,43 @@ +x-env: &app-env + environment: + - DEBUG=True + - SECRET_KEY=dev-secret-key + - POSTGRES_URL=postgres://postgres:password@database:5432/postgres + +services: + web: + extends: + file: compose.yaml + service: web + <<: *app-env + volumes: + - ./app:/app + + worker: + extends: + file: compose.yaml + service: worker + <<: *app-env + command: watchmedo auto-restart -d . -p "*.py" --recursive -- celery -A django_celery worker --loglevel=info + volumes: + - ./app:/app + + database: + extends: + file: compose.yaml + service: database + environment: + - POSTGRES_PASSWORD=password + volumes: + - postgres_data:/var/lib/postgresql/data + + broker: + extends: + file: compose.yaml + service: broker + volumes: + - redis_data:/data + +volumes: + postgres_data: + redis_data: diff --git a/samples/django-celery/compose.yaml b/samples/django-celery/compose.yaml new file mode 100644 index 00000000..605c133e --- /dev/null +++ b/samples/django-celery/compose.yaml @@ -0,0 +1,69 @@ +services: + web: + restart: unless-stopped + build: + context: ./app + dockerfile: Dockerfile + environment: + - SECRET_KEY + - REDIS_URL=redis://broker:6379/0 + - POSTGRES_URL=postgres://postgres:${POSTGRES_PASSWORD}@database:5432/postgres + depends_on: + - database + - broker + ports: + - mode: ingress + target: 8000 + published: 8000 + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8000/health/"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 20s + + worker: + restart: unless-stopped + build: + context: ./app + dockerfile: Dockerfile + # If you need more than one worker, uncomment the following lines + # deploy: + # replicas: 2 + environment: + - SECRET_KEY + - REDIS_URL=redis://broker:6379/0 + - POSTGRES_URL=postgres://postgres:${POSTGRES_PASSWORD}@database:5432/postgres + depends_on: + - database + - broker + command: celery -A django_celery worker --loglevel=info + + database: + image: postgres:16 + x-defang-postgres: true + ports: + - mode: host + target: 5432 + published: 5432 + environment: + - POSTGRES_PASSWORD + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres"] + interval: 10s + timeout: 5s + retries: 5 + + broker: + image: redis:7 + x-defang-redis: true + ports: + - mode: host + target: 6379 + published: 6379 + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 10s + timeout: 5s + retries: 5 +