Skip to content

Added cache lock + configurable wait option #24

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 9 commits into
base: develop
Choose a base branch
from
Open
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
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions maintenancemode/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ class MaintenanceSettings(AppConf):
LOCKFILE_PATH = os.path.join(
os.path.abspath(os.path.dirname(__file__)), 'maintenance.lock')
MODE = False
LOCKING_METHOD = 'file'
CACHE_KEY = "DJANGO_MAINTENANCE_MODE_ON"
CACHE_TTL = 60 * 60 * 24
CACHE_BACKEND = "default"
MAX_WAIT_FOR_END = 0

class Meta:
prefix = 'maintenance'
Expand Down
20 changes: 18 additions & 2 deletions maintenancemode/management/commands/maintenance.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,33 @@ def handle(self, *args, **options):
verbosity = int(options.get('verbosity'))

if command is not None:
if command.lower() in ('on', 'activate'):
try:
maintenance_duration = int(command)
except ValueError:
maintenance_duration = None
if maintenance_duration:
maintenance.activate(maintenance_duration)
if verbosity > 0:
self.stdout.write(
"Maintenance mode was activated "
"succesfully for %s seconds" % maintenance_duration
)
return
elif command.lower() in ('on', 'activate'):
maintenance.activate()
if verbosity > 0:
self.stdout.write(
"Maintenance mode was activated succesfully")
return
elif command.lower() in ('off', 'deactivate'):
maintenance.deactivate()
if verbosity > 0:
self.stdout.write(
"Maintenance mode was deactivated succesfully")
return

if command not in self.opts:
raise CommandError(
"Allowed commands are: %s" % '|'.join(self.opts))
"Allowed commands are: %s or maintenance duration in seconds"
% '|'.join(self.opts)
)
63 changes: 57 additions & 6 deletions maintenancemode/middleware.py
Original file line number Diff line number Diff line change
@@ -1,25 +1,69 @@
# -*- coding: utf-8 -*-

import re
import django
from datetime import datetime
from time import mktime
from wsgiref.handlers import format_date_time
from time import sleep
import logging

import django
from django.conf import urls
from django.core import urlresolvers

from .conf import settings
from . import utils as maintenance
from .conf import settings

urls.handler503 = 'maintenancemode.views.temporary_unavailable'
urls.__all__.append('handler503')

IGNORE_URLS = tuple([re.compile(u) for u in settings.MAINTENANCE_IGNORE_URLS])
IGNORE_URLS = tuple([re.compile(u) for u in getattr(settings, 'MAINTENANCE_IGNORE_URLS', [])])

MAX_WAIT_FOR_END = getattr(settings, 'MAINTENANCE_MAX_WAIT_FOR_END', 60)

logger = logging.getLogger(__name__)

class MaintenanceModeMiddleware(object):

def cond_wait_for_end_of_maintenance(self, request, retry_after):
"""
Wait for remaining maintenance time if waiting time is
less than MAX_WAIT_FOR_END
"""
ends_in = (retry_after - datetime.now()).total_seconds()
max_wait = MAX_WAIT_FOR_END
if ends_in > 0 and ends_in < max_wait:
logger.info(
u"[%s] waiting for %ss" % (
request.path, ends_in
)
)
sleep(ends_in)
return

def process_request(self, request):
# Allow access if middleware is not activated
if not (settings.MAINTENANCE_MODE or maintenance.status()):
if not hasattr(settings, 'MAINTENANCE_MODE'):
# package not setup
return None

value = getattr(settings, 'MAINTENANCE_MODE', False) or maintenance.status()
if not value:
# maintenance not active
return None

if isinstance(value, datetime):
retry_after = value
else:
retry_after = None

# used by template
request.retry_after = retry_after

if retry_after:
self.cond_wait_for_end_of_maintenance(request, retry_after)

if retry_after and datetime.now() > retry_after:
# maintenance ended
return None

INTERNAL_IPS = maintenance.IPList(settings.INTERNAL_IPS)
Expand Down Expand Up @@ -52,4 +96,11 @@ def process_request(self, request):
else:
callback, param_dict = resolver.resolve_error_handler('503')

return callback(request, **param_dict)
response = callback(request, **param_dict)

if retry_after:
response["Retry-After"] = format_date_time(
mktime(retry_after.timetuple())
)

return response
61 changes: 52 additions & 9 deletions maintenancemode/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,19 @@

import os

from dateutil.relativedelta import relativedelta
from datetime import datetime
from django.core.cache import get_cache
from django.core.exceptions import ImproperlyConfigured

from .conf import settings

LOCKING_METHOD = settings.MAINTENANCE_LOCKING_METHOD

CACHE_BACKEND = settings.MAINTENANCE_CACHE_BACKEND

cache = get_cache(CACHE_BACKEND)


class IPList(list):
"""Stolen from https://djangosnippets.org/snippets/1362/"""
Expand All @@ -26,18 +37,50 @@ def __contains__(self, ip):
return False


def activate():
try:
open(settings.MAINTENANCE_LOCKFILE_PATH, 'ab', 0).close()
except OSError:
pass # shit happens
def activate(maintenance_duration=None):
if maintenance_duration:
cache_value = (
datetime.now() + relativedelta(seconds=maintenance_duration)
)
else:
cache_value = True
if LOCKING_METHOD == "file":
try:
open(settings.MAINTENANCE_LOCKFILE_PATH, 'ab', 0).close()
except OSError:
pass # shit happens
elif LOCKING_METHOD == "cache":
cache.set(
settings.MAINTENANCE_CACHE_KEY,
cache_value,
settings.MAINTENANCE_CACHE_TTL
)
else:
raise ImproperlyConfigured(
"Unknown locking method %s" % LOCKING_METHOD)


def deactivate():
if os.path.isfile(settings.MAINTENANCE_LOCKFILE_PATH):
os.remove(settings.MAINTENANCE_LOCKFILE_PATH)
LOCKING_METHOD = settings.MAINTENANCE_LOCKING_METHOD
if LOCKING_METHOD == "file":
if os.path.isfile(settings.MAINTENANCE_LOCKFILE_PATH):
os.remove(settings.MAINTENANCE_LOCKFILE_PATH)
elif LOCKING_METHOD == "cache":
cache.delete(
settings.MAINTENANCE_CACHE_KEY
)
else:
raise ImproperlyConfigured(
"Unknown locking method %s" % LOCKING_METHOD)


def status():
return settings.MAINTENANCE_MODE or os.path.isfile(
settings.MAINTENANCE_LOCKFILE_PATH)
LOCKING_METHOD = settings.MAINTENANCE_LOCKING_METHOD
if LOCKING_METHOD == "file":
return settings.MAINTENANCE_MODE or os.path.isfile(
settings.MAINTENANCE_LOCKFILE_PATH)
elif LOCKING_METHOD == "cache":
return cache.get(settings.MAINTENANCE_CACHE_KEY)
else:
raise ImproperlyConfigured(
"Unknown locking method %s" % LOCKING_METHOD)
1 change: 1 addition & 0 deletions maintenancemode/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ def temporary_unavailable(request, template_name='503.html'):
"""
context = {
'request_path': request.path,
'request': request
}
return http.HttpResponseTemporaryUnavailable(
render_to_string(template_name, context))
2 changes: 0 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,10 @@ def find_version(*parts):
license='BSD License',

install_requires=[
'django>=1.4.2',
'django-appconf',
'ipy',
],
requires=[
'Django (>=1.4.2)',
],

description="django-maintenancemode allows you to temporary shutdown your site for maintenance work",
Expand Down