Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
24 changes: 19 additions & 5 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ jobs:
uses: actions/setup-node@v4
with:
node-version: "20"
cache: "npm"

- id: get_tag
run: |
Expand All @@ -111,14 +112,14 @@ jobs:

# Build browser JS bundle
cd packages/karriojs
npm install
npm ci
npx gulp build --output "${GITHUB_WORKSPACE}/apps/api/karrio/server/static/karrio/js/karrio.js"
cd -

- name: Build embeddable elements
run: |
cd packages/elements
npm install
npm ci
npm run build

# Copy built elements to Django static directory
Expand Down Expand Up @@ -163,15 +164,28 @@ jobs:
steps:
- uses: actions/checkout@v4

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- id: get_tag
run: |
cat ./apps/api/karrio/server/VERSION
echo "tag=$(cat ./apps/api/karrio/server/VERSION)" >> "$GITHUB_ENV"

- name: Build karrio dashboard image
run: |
echo 'Building karrio dashboard:${{ env.tag }}...'
./bin/build-dashboard-image ${{ env.tag }}
uses: docker/build-push-action@v6
with:
context: .
file: ./docker/dashboard/Dockerfile
push: false
load: true
tags: karrio/dashboard:${{ env.tag }}
build-args: |
VERSION=${{ env.tag }}
NEXT_PUBLIC_DASHBOARD_VERSION=${{ env.tag }}
SOURCE=https://github.com/karrioapi/karrio
cache-from: type=gha,scope=dashboard
cache-to: type=gha,mode=max,scope=dashboard

- name: Push karrio dashboard image
run: |
Expand Down
28 changes: 21 additions & 7 deletions .github/workflows/insiders-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ jobs:
uses: actions/setup-node@v4
with:
node-version: "20"
cache: "npm"

- id: get_tag
run: |
Expand All @@ -112,14 +113,14 @@ jobs:
--additional-properties=useSingleRequestParameter=true

cd packages/karriojs
npm install
npm ci
npx gulp build --output "${GITHUB_WORKSPACE}/apps/api/karrio/server/static/karrio/js/karrio.js"
cd -

- name: Build embeddable elements
run: |
cd packages/elements
npm install
npm ci
npm run build

ELEMENTS_STATIC="${GITHUB_WORKSPACE}/apps/api/karrio/server/static/karrio/elements"
Expand Down Expand Up @@ -156,14 +157,27 @@ jobs:
submodules: recursive
token: ${{ secrets.GH_PAT }}

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- id: get_tag
run: |
cat ./apps/api/karrio/server/VERSION
echo "tag=$(cat ./apps/api/karrio/server/VERSION)" >> "$GITHUB_ENV"

- name: Login to GHCR
run: echo ${{ secrets.GH_PAT }} | docker login ghcr.io -u USERNAME --password-stdin

- name: Build insider dashboard image
run: |
echo 'Build and push karrio-insiders dashboard:${{ env.tag }}...'
echo ${{ secrets.GH_PAT }} | docker login ghcr.io -u USERNAME --password-stdin
KARRIO_IMAGE=ghcr.io/karrioapi/dashboard SOURCE=https://github.com/karrioapi/karrio-insiders ./bin/build-dashboard-image ${{ env.tag }} &&
docker push ghcr.io/karrioapi/dashboard:${{ env.tag }} || exit 1
uses: docker/build-push-action@v6
with:
context: .
file: ./docker/dashboard/Dockerfile
push: true
tags: ghcr.io/karrioapi/dashboard:${{ env.tag }}
build-args: |
VERSION=${{ env.tag }}
NEXT_PUBLIC_DASHBOARD_VERSION=${{ env.tag }}
SOURCE=https://github.com/karrioapi/karrio-insiders
cache-from: type=gha,scope=insiders-dashboard
cache-to: type=gha,mode=max,scope=insiders-dashboard
11 changes: 9 additions & 2 deletions .github/workflows/platform-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ jobs:
uses: actions/setup-node@v4
with:
node-version: "20"
cache: "npm"

- id: get_tag
run: |
Expand All @@ -57,14 +58,14 @@ jobs:
--additional-properties=useSingleRequestParameter=true

cd packages/karriojs
npm install
npm ci
npx gulp build --output "${GITHUB_WORKSPACE}/apps/api/karrio/server/static/karrio/js/karrio.js"
cd -

- name: Build embeddable elements
run: |
cd packages/elements
npm install
npm ci
npm run build

ELEMENTS_STATIC="${GITHUB_WORKSPACE}/apps/api/karrio/server/static/karrio/elements"
Expand Down Expand Up @@ -126,6 +127,12 @@ jobs:
submodules: recursive
token: ${{ secrets.GH_PAT }}

- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
cache: "npm"

- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
Expand Down
12 changes: 10 additions & 2 deletions apps/api/gunicorn-cfg.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
# -*- encoding: utf-8 -*-
import decouple

KARRIO_HOST = decouple.config("KARRIO_HTTP_HOST", default="0.0.0.0")
KARRIO_HOST = decouple.config("KARRIO_HTTP_HOST", default="0.0.0.0") # noqa: S104
KARRIO_PORT = decouple.config("KARRIO_HTTP_PORT", default=5002)

bind = f"{KARRIO_HOST}:{KARRIO_PORT}"
Expand All @@ -10,3 +9,12 @@
capture_output = True
enable_stdio_inheritance = True
workers = decouple.config("KARRIO_WORKERS", default=2, cast=int)

# NOTE: preload_app is intentionally NOT set here.
# With UvicornWorker (ASGI), preload_app=True causes:
# 1. asyncio.CancelledError in django-health-check (stale event loop from master)
# 2. psycopg "BAD" connections (DB pool forked from master)
# UvicornWorker manages its own lifecycle and ignores gunicorn's post_fork,
# so these issues cannot be fixed via post_fork hooks.
# Cold-start CPU optimization should be addressed at the k8s level
# (resource requests/limits, startup probes) instead.
2 changes: 1 addition & 1 deletion apps/api/karrio/server/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__path__ = __import__('pkgutil').extend_path(__path__, __name__) # type: ignore
__path__ = __import__("pkgutil").extend_path(__path__, __name__) # type: ignore
5 changes: 3 additions & 2 deletions apps/api/karrio/server/__main__.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
#!/usr/bin/env python
"""Karrio's command-line utility for administrative tasks."""

import os
import sys


def main():
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'karrio.server.settings')
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "karrio.server.settings")
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
Expand All @@ -17,5 +18,5 @@ def main():
execute_from_command_line(sys.argv)


if __name__ == '__main__':
if __name__ == "__main__":
main()
52 changes: 48 additions & 4 deletions apps/api/karrio/server/asgi.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,61 @@
"""
ASGI config for karrio.server project.

It exposes the ASGI callable as a module-level variable named ``application``.
Exposes the ASGI callable as a module-level variable named ``application``.

The callable is a thin async wrapper around Django's ASGI app that
captures the worker's running event loop on the first http/websocket
request. The captured loop is handed to the ``karrio.server.servicebus``
publisher (if installed) so its async producer can schedule publishes
on the worker's event loop via ``asyncio.run_coroutine_threadsafe``.

Why first-request capture and not the ASGI lifespan protocol: our
custom ``UvicornWorker`` (karrio/apps/api/karrio/server/workers.py) sets
``"lifespan": "off"`` explicitly because Django's stock ASGI app does
not handle lifespan messages. First-request capture is one global +
a handful of lines, no worker config change needed.

For more information on this file, see
https://docs.djangoproject.com/en/3.0/howto/deployment/asgi/
"""

import asyncio
import logging
import os

from django.core.asgi import get_asgi_application

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'karrio.server.settings')
os.environ.setdefault('DJANGO_ALLOW_ASYNC_UNSAFE', 'true')
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "karrio.server.settings")
os.environ.setdefault("DJANGO_ALLOW_ASYNC_UNSAFE", "true")

logger = logging.getLogger(__name__)

_django_app = get_asgi_application()
_loop_captured = False


async def application(scope, receive, send):
"""ASGI application with first-request event-loop capture.

Non-http/websocket scopes pass straight through to Django. The
capture happens once per worker; subsequent requests are zero-cost
fast-path (a single boolean check).
"""
global _loop_captured
if not _loop_captured and scope.get("type") in ("http", "websocket"):
_loop_captured = True
try:
from karrio.server.servicebus import set_async_loop

application = get_asgi_application()
set_async_loop(asyncio.get_running_loop())
logger.debug("ASGI: captured event loop for servicebus async publisher")
except ImportError:
# servicebus extension not installed in this deployment; that's
# fine — bridge dual-publish will use the sync path.
pass
except Exception:
logger.warning(
"ASGI: failed to capture event loop for servicebus async publisher",
exc_info=True,
)
await _django_app(scope, receive, send)
Loading
Loading