Skip to content

Commit c9a73fe

Browse files
Merge pull request #38 from druling/gracefull_shutdown
adding gracefull shutdown command
2 parents 1543971 + 50c9bd6 commit c9a73fe

File tree

9 files changed

+133
-5
lines changed

9 files changed

+133
-5
lines changed

.github/workflows/build.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ name: Build and Push
33
on:
44
push:
55
branches:
6-
- health_check
6+
- gracefull_shutdown
77

88
jobs:
99
build-and-push:

build/docker/Dockerfile

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,17 @@ RUN pip install --no-cache-dir -r requirements.txt
2121
# Copy the rest of the application code to the container
2222
COPY ../../ /app/
2323

24-
# Expose port 8000 for Django development server
24+
# Copy the supervisord configuration file
25+
COPY /build/docker/supervisord.conf /etc/supervisor/conf.d/supervisord.conf
26+
27+
# Copy the entrypoint configuration file
28+
COPY /build/docker/entrypoint.sh /app/entrypoint.sh
29+
30+
# Make the entrypoint script executable
31+
RUN chmod +x /app/entrypoint.sh
32+
33+
# Expose port 8000 for the Django server
2534
EXPOSE 8000
2635

27-
# Command to run the application
28-
CMD ["sh", "-c", "python manage.py migrate && python manage.py runserver 0.0.0.0:8000"]
36+
# Use the entrypoint script
37+
ENTRYPOINT ["/app/entrypoint.sh"]

build/docker/entrypoint.sh

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
#!/bin/bash
2+
3+
set -e
4+
5+
# Apply database migrations
6+
echo "Applying database migrations..."
7+
python manage.py migrate
8+
9+
# Start supervisord
10+
echo "Starting supervisord..."
11+
exec supervisord -c /etc/supervisor/conf.d/supervisord.conf

build/docker/supervisord.conf

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
[supervisord]
2+
nodaemon=true
3+
4+
[program:django]
5+
command=python manage.py runserver 0.0.0.0:8000
6+
directory=/app
7+
autostart=true
8+
autorestart=true
9+
10+
[program:spot_handler]
11+
command=python manage.py spot_handler
12+
directory=/app
13+
autostart=true
14+
autorestart=true

handlers/__init__.py

Whitespace-only changes.

handlers/apps.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from django.apps import AppConfig
2+
3+
4+
class HandlersConfig(AppConfig):
5+
name = "handlers"

mail_template/management/commands/setup_email_templates.py renamed to handlers/management/commands/setup_email_templates.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from django.core.management.base import BaseCommand
2-
from ...service import TemplateSetup
2+
from mail_template.service import TemplateSetup
33
import logging
44

55
logger = logging.getLogger(__name__)
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import logging
2+
import os
3+
import signal
4+
import threading
5+
import time
6+
import requests
7+
import sys
8+
from django.core.management.base import BaseCommand
9+
from django.db import connections
10+
from django.conf import settings
11+
12+
logger = logging.getLogger(__name__)
13+
14+
15+
class SpotTerminationHandler:
16+
def __init__(self):
17+
self.terminating = threading.Event()
18+
self.spot_termination_url = "http://169.254.170.2/v2/spot/termination-notice"
19+
self.check_interval = 10 # Polling interval in seconds
20+
self.grace_period = 30 # Grace period in seconds
21+
22+
def check_termination_notice(self):
23+
"""Check if spot instance is scheduled for termination"""
24+
try:
25+
response = requests.get(self.spot_termination_url, timeout=2)
26+
return response.status_code == 200
27+
except requests.RequestException as e:
28+
logger.warning(f"Error checking termination notice: {e}")
29+
return False
30+
31+
def graceful_shutdown(self, signum, frame):
32+
"""Handle graceful shutdown of the Django application"""
33+
if self.terminating.is_set():
34+
return
35+
36+
self.terminating.set()
37+
logger.info("Received termination notice, starting graceful shutdown...")
38+
39+
try:
40+
# Close database connections
41+
for conn in connections.all():
42+
conn.close_if_unusable_or_obsolete()
43+
44+
# Stop accepting new requests (if using Gunicorn)
45+
if hasattr(settings, "GUNICORN_PID_FILE") and os.path.exists(
46+
settings.GUNICORN_PID_FILE
47+
):
48+
with open(settings.GUNICORN_PID_FILE) as f:
49+
gunicorn_pid = int(f.read())
50+
os.kill(gunicorn_pid, signal.SIGTERM)
51+
52+
# Wait for ongoing requests to complete
53+
logger.info(
54+
f"Waiting {self.grace_period} seconds for ongoing requests to complete..."
55+
)
56+
time.sleep(self.grace_period)
57+
58+
logger.info("Graceful shutdown completed")
59+
except Exception as e:
60+
logger.error(f"Error during shutdown: {e}")
61+
finally:
62+
sys.exit(0)
63+
64+
def start_monitoring(self):
65+
logger.info("Starting Fargate Spot termination monitoring...")
66+
67+
# Register signal handlers
68+
signal.signal(signal.SIGTERM, self.graceful_shutdown)
69+
signal.signal(signal.SIGINT, self.graceful_shutdown)
70+
71+
while not self.terminating.is_set():
72+
if self.check_termination_notice():
73+
self.graceful_shutdown(None, None)
74+
time.sleep(self.check_interval)
75+
76+
77+
class Command(BaseCommand):
78+
help = "Starts the Fargate Spot termination handler"
79+
80+
def handle(self, *args, **options):
81+
handler = SpotTerminationHandler()
82+
monitoring_thread = threading.Thread(target=handler.start_monitoring)
83+
monitoring_thread.daemon = True
84+
monitoring_thread.start()
85+
86+
# Keep the main thread alive
87+
while not handler.terminating.is_set():
88+
time.sleep(1)

setup/settings.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
# Project apps
5151
"health_check",
5252
"commons",
53+
"handlers",
5354
"item",
5455
"user",
5556
"restaurant",

0 commit comments

Comments
 (0)