Skip to content

Commit 70e2e16

Browse files
author
prxssh
committed
chore: docs
1 parent a38e16a commit 70e2e16

13 files changed

Lines changed: 25529 additions & 17 deletions

File tree

README.md

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
# Django WebSocket Blue/Green Deployment
2+
3+
A production-ready Django-based WebSocket service with zero-downtime blue/green
4+
Docker Compose deployment, observability, and health checks.
5+
6+
## 🚀 Features
7+
8+
* **Blue/Green Deployment** via Docker Compose (nginx reverse proxy with
9+
`app_blue` and `app_green` services)
10+
* **WebSocket Endpoint** at `/ws/chat/` powered by Uvicorn + Django Channels
11+
* **Observability** with Prometheus metrics (`/metrics`) and Grafana dashboards
12+
* **Health Checks**: `/healthz` (liveness) and `/readyz` (readiness)
13+
* **Automation Scripts**:
14+
15+
* `scripts/promote.sh`: Flip traffic from blue to green
16+
* `scripts/monitor.sh`: Tail logs for errors and print top‑5 Prometheus
17+
counters every 10 s
18+
* `scripts/ws_test.js`: k6 load test script for WebSocket performance
19+
20+
## ⚙️ Prerequisites
21+
22+
* Docker >= 20.10
23+
* Docker Compose >= 1.29
24+
* k6 (for load testing)
25+
26+
## 🛠️ Quick Start
27+
28+
### 1. Clone the repository
29+
30+
```bash
31+
git clone git@github.com:prxssh/python-django-ws && cd python-django-ws
32+
```
33+
34+
### 2. Build and start the full stack
35+
36+
This command first spins up the required prerequisites for the application
37+
(Grafana, dashboards, k6, nginx) and then starts both app_blue and app_green
38+
services.
39+
40+
```bash
41+
docker-compose -f docker/compose.yml up --build -d
42+
```
43+
44+
### 3. Verify Services
45+
46+
* **WebSocket**: `ws://localhost:80/ws/chat/?session=<uid>`
47+
* **Liveness**: `http://localhost:80/healthz`
48+
* **Readiness**: `http://localhost:80/readyz`
49+
* **Metrics**: `http://localhost:80/metrics`
50+
* **Grafana**: `http://localhost:3000` (default credentials `admin/admin`)
51+
52+
## ⚡ Blue/Green Deployment
53+
54+
To flip traffic from the current color to the standby color:
55+
56+
```bash
57+
./scripts/promote.sh
58+
```
59+
60+
This script will:
61+
62+
1. Build and start the next color stack
63+
2. Run smoke tests against `/healthz`
64+
3. Update nginx to route traffic
65+
4. Tear down the old color stack
66+
67+
## 📊 Monitoring
68+
69+
Tail real-time logs for errors and see top‑5 Prometheus counters:
70+
71+
```bash
72+
./scripts/monitor.sh
73+
```
74+
75+
## 💥 Load Testing
76+
77+
By default, Docker Compose will run the k6 load test automatically when the
78+
application starts, writing results to k6-results/ (or you can view them via
79+
the k6 container logs).
80+
81+
To run the load test manually, execute:
82+
83+
```bash
84+
k6 run scripts/ws_test.js
85+
```
86+
87+
## 📚 Further Reading
88+
89+
* See [DESIGN.md](docs/DESIGN.md) for concurrency and deployment rationale
90+
* See [OBSERVABILITY.md](docs/OBSERVABILITY.md) for Prometheus/Grafana setup

app/asgi.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,12 @@
88
"""
99

1010
import os
11+
import asyncio
1112
import threading, time
1213

14+
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'app.settings')
15+
16+
from concurrent.futures import ThreadPoolExecutor
1317
from django.core.asgi import get_asgi_application
1418
from channels.routing import ProtocolTypeRouter, URLRouter
1519
from app.chat.routing import websocket_urlpatterns
@@ -18,8 +22,12 @@
1822
from datetime import datetime, timezone
1923
from app.shutdown import register_shutdown
2024

25+
26+
loop = asyncio.get_event_loop()
27+
max_workers = int(os.getenv("THREADPOOL_MAX_WORKERS", "50"))
28+
loop.set_default_executor(ThreadPoolExecutor(max_workers=max_workers))
29+
2130
register_shutdown()
22-
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'app.settings')
2331

2432
def start_heartbeat():
2533
layer = get_channel_layer()

devops/grafana/dashboards/websocket.json

Lines changed: 120 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,20 @@
11
{
2+
"annotations": {
3+
"list": [
4+
{
5+
"builtIn": 1,
6+
"datasource": {
7+
"type": "grafana",
8+
"uid": "-- Grafana --"
9+
},
10+
"enable": true,
11+
"hide": true,
12+
"iconColor": "rgba(0, 211, 255, 1)",
13+
"name": "Annotations & Alerts",
14+
"type": "dashboard"
15+
}
16+
]
17+
},
218
"editable": true,
319
"fiscalYearStartMonth": 0,
420
"graphTooltip": 0,
@@ -70,7 +86,7 @@
7086
"x": 0,
7187
"y": 0
7288
},
73-
"id": 4,
89+
"id": 5,
7490
"options": {
7591
"legend": {
7692
"calcs": [],
@@ -89,16 +105,16 @@
89105
{
90106
"disableTextWrap": false,
91107
"editorMode": "builder",
92-
"expr": "rate(total_messages_total[$__rate_interval])",
108+
"expr": "active_connections",
93109
"fullMetaSearch": false,
94-
"includeNullMetadata": false,
110+
"includeNullMetadata": true,
95111
"legendFormat": "__auto",
96112
"range": true,
97113
"refId": "A",
98114
"useBackend": false
99115
}
100116
],
101-
"title": "Messages",
117+
"title": "New panel",
102118
"type": "timeseries"
103119
},
104120
{
@@ -266,7 +282,7 @@
266282
"x": 0,
267283
"y": 8
268284
},
269-
"id": 2,
285+
"id": 4,
270286
"options": {
271287
"legend": {
272288
"calcs": [],
@@ -285,7 +301,7 @@
285301
{
286302
"disableTextWrap": false,
287303
"editorMode": "builder",
288-
"expr": "rate(error_count_total[$__rate_interval])",
304+
"expr": "rate(total_messages_total[$__rate_interval])",
289305
"fullMetaSearch": false,
290306
"includeNullMetadata": false,
291307
"legendFormat": "__auto",
@@ -294,7 +310,7 @@
294310
"useBackend": false
295311
}
296312
],
297-
"title": "Error Count - Rate",
313+
"title": "Messages",
298314
"type": "timeseries"
299315
},
300316
{
@@ -392,6 +408,102 @@
392408
],
393409
"title": "Shutdown Time",
394410
"type": "timeseries"
411+
},
412+
{
413+
"datasource": {
414+
"type": "prometheus",
415+
"uid": "prometheus"
416+
},
417+
"fieldConfig": {
418+
"defaults": {
419+
"color": {
420+
"mode": "palette-classic"
421+
},
422+
"custom": {
423+
"axisBorderShow": false,
424+
"axisCenteredZero": false,
425+
"axisColorMode": "text",
426+
"axisLabel": "",
427+
"axisPlacement": "auto",
428+
"barAlignment": 0,
429+
"barWidthFactor": 0.6,
430+
"drawStyle": "line",
431+
"fillOpacity": 0,
432+
"gradientMode": "none",
433+
"hideFrom": {
434+
"legend": false,
435+
"tooltip": false,
436+
"viz": false
437+
},
438+
"insertNulls": false,
439+
"lineInterpolation": "linear",
440+
"lineWidth": 1,
441+
"pointSize": 5,
442+
"scaleDistribution": {
443+
"type": "linear"
444+
},
445+
"showPoints": "auto",
446+
"spanNulls": false,
447+
"stacking": {
448+
"group": "A",
449+
"mode": "none"
450+
},
451+
"thresholdsStyle": {
452+
"mode": "off"
453+
}
454+
},
455+
"mappings": [],
456+
"thresholds": {
457+
"mode": "absolute",
458+
"steps": [
459+
{
460+
"color": "green"
461+
},
462+
{
463+
"color": "red",
464+
"value": 80
465+
}
466+
]
467+
}
468+
},
469+
"overrides": []
470+
},
471+
"gridPos": {
472+
"h": 8,
473+
"w": 12,
474+
"x": 0,
475+
"y": 16
476+
},
477+
"id": 2,
478+
"options": {
479+
"legend": {
480+
"calcs": [],
481+
"displayMode": "list",
482+
"placement": "bottom",
483+
"showLegend": true
484+
},
485+
"tooltip": {
486+
"hideZeros": false,
487+
"mode": "single",
488+
"sort": "none"
489+
}
490+
},
491+
"pluginVersion": "12.0.0+security-01",
492+
"targets": [
493+
{
494+
"disableTextWrap": false,
495+
"editorMode": "builder",
496+
"expr": "rate(error_count_total[$__rate_interval])",
497+
"fullMetaSearch": false,
498+
"includeNullMetadata": false,
499+
"legendFormat": "__auto",
500+
"range": true,
501+
"refId": "A",
502+
"useBackend": false
503+
}
504+
],
505+
"title": "Error Count - Rate",
506+
"type": "timeseries"
395507
}
396508
],
397509
"preload": false,
@@ -410,4 +522,4 @@
410522
"title": "WebSocket App",
411523
"uid": "541066da-8c9d-47f2-aea9-65360f38dd09",
412524
"version": 1
413-
}
525+
}

devops/nginx/default.blue.conf

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,13 @@
1+
user nginx;
2+
worker_processes auto;
3+
worker_rlimit_nofile 100000;
4+
5+
events {
6+
use epoll;
7+
worker_connections 2000;
8+
multi_accept on;
9+
}
10+
111
upstream app_upstream {
212
server app_blue:8000;
313
keepalive 2048;

devops/nginx/default.conf

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ server {
1717
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
1818

1919
# allow long‐lived idle connections
20-
proxy_read_timeout 600;
20+
proxy_read_timeout 600s;
2121
proxy_buffering off;
2222
}
2323

devops/nginx/default.green.conf

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,13 @@
1+
user nginx;
2+
worker_processes auto;
3+
worker_rlimit_nofile 100000;
4+
5+
events {
6+
use epoll;
7+
worker_connections 2000;
8+
multi_accept on;
9+
}
10+
111
upstream app_upstream {
212
server app_green:8000;
313
keepalive 2048;

docker/compose.yml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,20 @@ services:
7676
- "9090:9090"
7777
networks:
7878
- web
79+
k6:
80+
image: grafana/k6:latest
81+
container_name: k6
82+
depends_on:
83+
- nginx
84+
volumes:
85+
- '../scripts/ws_test.js:/scripts/ws_test.js:ro'
86+
- '../k6-results:/results'
87+
networks:
88+
- web
89+
entrypoint: >
90+
k6 run
91+
--out json=/results/result.json
92+
/scripts/ws_test.js
7993
8094
networks:
8195
web:

0 commit comments

Comments
 (0)