Skip to content

Commit

Permalink
added a last_update date to models and propagator modular for update …
Browse files Browse the repository at this point in the history
…all config in multi-process envs.
  • Loading branch information
darius BERNARD committed Feb 10, 2017
1 parent 61fa24c commit e664e97
Show file tree
Hide file tree
Showing 10 changed files with 188 additions and 19 deletions.
27 changes: 18 additions & 9 deletions dynamic_logging/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import logging

from django.apps.config import AppConfig
from django.db.models.signals import post_delete, post_save
from django.core.signals import setting_changed
from django.db.utils import OperationalError

from dynamic_logging.settings import get_setting
Expand All @@ -17,6 +17,21 @@ class DynamicLoggingConfig(AppConfig):
name = 'dynamic_logging'
auto_signal_handler = AutoSignalsHandler()

def __init__(self, *args, **kwargs):
self.propagator = None
super(DynamicLoggingConfig, self).__init__(*args, **kwargs)

def on_settings_changed(self, sender, setting, *args, **kwargs):
if setting == 'DYNAMIC_LOGGING':
self.setup_propagator()

def setup_propagator(self):
from dynamic_logging.propagator import Propagator
if self.propagator is not None:
self.propagator.teardown()
self.propagator = Propagator.get_current()
self.propagator.setup()

def ready(self):
# import at ready time to prevent model loading before app ready
from dynamic_logging.scheduler import main_scheduler
Expand All @@ -27,12 +42,6 @@ def ready(self):
# setup signals for Trigger changes. it will reload the current trigger and next one

self.auto_signal_handler.apply(get_setting('signals_auto'))
from .signals import reload_timers_on_trigger_change
Trigger = self.get_model('Trigger')
Config = self.get_model('Config')

post_save.connect(reload_timers_on_trigger_change, sender=Trigger)
post_delete.connect(reload_timers_on_trigger_change, sender=Trigger)
self.setup_propagator()

post_save.connect(reload_timers_on_trigger_change, sender=Config)
post_delete.connect(reload_timers_on_trigger_change, sender=Config)
setting_changed.connect(self.on_settings_changed)
14 changes: 8 additions & 6 deletions dynamic_logging/migrations/0001_initial.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,20 +16,22 @@ class Migration(migrations.Migration):
migrations.CreateModel(
name='Config',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('id', models.AutoField(auto_created=True, serialize=False, primary_key=True, verbose_name='ID')),
('name', models.CharField(max_length=255)),
('config_json', models.TextField()),
('config_json', models.TextField(default='{}', validators=[dynamic_logging.models.json_value])),
('last_update', models.DateTimeField(auto_now=True)),
],
),
migrations.CreateModel(
name='Trigger',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('id', models.AutoField(auto_created=True, serialize=False, primary_key=True, verbose_name='ID')),
('name', models.CharField(max_length=255)),
('is_active', models.BooleanField(default=True)),
('start_date', models.DateTimeField(default=django.utils.timezone.now, null=True)),
('end_date', models.DateTimeField(default=dynamic_logging.models.now_plus_2hours, null=True)),
('config', models.ForeignKey(to='dynamic_logging.Config', related_name='triggers')),
('start_date', models.DateTimeField(default=django.utils.timezone.now, null=True, blank=True)),
('end_date', models.DateTimeField(default=dynamic_logging.models.now_plus_2hours, null=True, blank=True)),
('last_update', models.DateTimeField(auto_now=True)),
('config', models.ForeignKey(related_name='triggers', to='dynamic_logging.Config')),
],
options={
'get_latest_by': 'start_date',
Expand Down
3 changes: 3 additions & 0 deletions dynamic_logging/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ class Trigger(models.Model):
end_date = models.DateTimeField(default=now_plus_2hours, blank=True, null=True)

config = models.ForeignKey('Config', related_name='triggers')
last_update = models.DateTimeField(auto_now=True)

@classmethod
def default(cls):
Expand Down Expand Up @@ -111,6 +112,8 @@ class Config(models.Model):

config_json = models.TextField(validators=[json_value], default='{}')

last_update = models.DateTimeField(auto_now=True)

@classmethod
def default(cls):
if not hasattr(cls, "_default_settings"):
Expand Down
136 changes: 136 additions & 0 deletions dynamic_logging/propagator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import, print_function, unicode_literals

import logging
import threading

from django.db.models.signals import post_delete, post_save
from django.utils import timezone
from django.utils.module_loading import import_string

from dynamic_logging.models import Config, Trigger
from dynamic_logging.scheduler import main_scheduler
from dynamic_logging.settings import get_setting

logger = logging.getLogger(__name__)


class Propagator(object):
"""
this object is in charge to call main_scheduler.reload each time a config or a trigger
is updated
"""

@staticmethod
def get_current():
current = get_setting('upgrade_propagator')
cls = import_string(current['class'])
return cls(current['config'])

def __init__(self, conf):
self.conf = conf

def setup(self):
"""
called a application start
:return:
"""
post_save.connect(self.on_config_changed, sender=Trigger)
post_delete.connect(self.on_config_changed, sender=Trigger)

post_save.connect(self.on_config_changed, sender=Config)
post_delete.connect(self.on_config_changed, sender=Config)

def teardown(self):
post_save.disconnect(self.on_config_changed)
post_delete.disconnect(self.on_config_changed)

def on_config_changed(self, *args, **kwargs):
"""
called each time a local config is changed
"""
self.propagate()

def propagate(self):
"""
propagate the signal to reload the config.
:return:
"""
raise NotImplementedError()

def reload_scheduler(self):
"""
called whene we recieved a propagated order to reload
:return:
"""
main_scheduler.reload()


class DummyPropagator(Propagator):
def setup(self):
pass

def propagate(self):
pass


class ThreadSignalPropagator(Propagator):
"""
this propagator is for single process only. it will reload the scheduler localy to
the process that has updated the config/trigger.
in multi process env, all other process won't be triggered
"""

def propagate(self, *args, **kwargs):
self.reload_scheduler()


class RepeatTimer(threading.Thread):
def __init__(self, interval, function, *args, **kwargs):
self.interval = interval
self.function = function
super(RepeatTimer, self).__init__(*args, **kwargs)
self.stopped = threading.Event()
self.daemon = True

def cancel(self):
self.stopped.set()

def run(self):
while not self.stopped.wait(self.interval):
self.function()


class TimerPropagator(Propagator):
"""
this propagator is a fallback for small website that can't use message queue.
it will check for config update each minutes (customisable) and will reload if a trigger/config
was updated since the last time
"""

def __init__(self, conf):
super(TimerPropagator, self).__init__(conf)
self.timer = None
self.last_wake = timezone.now()

def setup(self):
# we don't call super since we will update this process each n sec
self.timer = RepeatTimer(
self.conf.get("interval", 60),
self.check_new_config,
name='TimerPropagator_timer')
self.timer.start()

def teardown(self):
self.timer.cancel()

def check_new_config(self):
now = timezone.now()
if (Trigger.objects.filter(last_update__gte=self.last_wake).exists() or
Config.objects.filter(last_update__gte=self.last_wake).exists()):
self.last_wake = now
self.reload_scheduler()

def propagate(self):
pass
2 changes: 1 addition & 1 deletion dynamic_logging/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

DEFAULT_VALUES = {
"signals_auto": ('db_debug',), # setup all automatic signal handlers
"config_upgrade_propagator": "dynamic_logging.propagator.ThreadSignalPropagator",
"upgrade_propagator": {'class': "dynamic_logging.propagator.ThreadSignalPropagator", 'config': {}}
}


Expand Down
1 change: 0 additions & 1 deletion dynamic_logging/signals.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,4 +72,3 @@ def db_debug_handler(sender, config, **kwargs):
connections[alias].force_debug_cursor = val
del old_cnx_val[alias]
config_applied.connect(db_debug_handler, weak=False)

8 changes: 7 additions & 1 deletion dynamic_logging/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
from django.test.testcases import TestCase
from django.test.utils import override_settings
from django.utils import timezone

from dynamic_logging.handlers import MockHandler
Expand Down Expand Up @@ -88,6 +89,10 @@ def test_all_bases_values(self):
self.assertTriggerForDate('27-02-2017', 'default settings', None)


@override_settings(
DYNAMIC_LOGGING={"upgrade_propagator": {'class': "dynamic_logging.propagator.ThreadSignalPropagator", 'config': {}}
}
)
class TestSchedulerTimers(TestCase):
def setUp(self):
self.config = Config.objects.create(name='nothing', config={})
Expand Down Expand Up @@ -537,6 +542,7 @@ def test_auto_signal_handler(self):

def test_auto_signal_customise(self):
cnt = []

def wrapper():
cnt.append(1)
a = AutoSignalsHandler()
Expand All @@ -546,6 +552,7 @@ def wrapper():

def test_auto_signal_overwrite(self):
cnt = []

def wrapper():
cnt.append(1)
a = AutoSignalsHandler()
Expand Down Expand Up @@ -598,4 +605,3 @@ def test_db_debug_debug_ok(self):
Config.objects.count()
self.assertEqual(len(msg['debug']), 1)
self.assertTrue('SELECT COUNT(*)' in msg['debug'][0])

6 changes: 6 additions & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,19 @@ description-file = README.rst
[bdist_wheel]
universal = 1

[flake8]
exclude=
dynamic_logging/migrations/*.py


[coverage:run]
include =
dynamic_logging
testproject
omit =
setup.py
manage.py
*/migrations/*.py

[isort]
line_length=119
Expand Down
3 changes: 3 additions & 0 deletions testproject/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -185,3 +185,6 @@
}

INTERNAL_IPS = ['127.0.0.1']
DYNAMIC_LOGGING = {
"upgrade_propagator": {'class': "dynamic_logging.propagator.TimerPropagator", 'config': {}}
}
7 changes: 6 additions & 1 deletion testproject/testapp/tests.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
from django.contrib.auth import get_user_model
from django.core.urlresolvers import reverse
from django.test import TestCase

# Create your tests here.
from django.test.utils import override_settings

from dynamic_logging.handlers import MockHandler
from dynamic_logging.models import Config, Trigger
from dynamic_logging.scheduler import main_scheduler
Expand Down Expand Up @@ -48,6 +49,10 @@ def test_log_bad_level(self):
self.assertRaises(Exception, self.client.get, '/testapp/log/OOPS/testproject.testapp/')


@override_settings(
DYNAMIC_LOGGING={"upgrade_propagator": {'class': "dynamic_logging.propagator.ThreadSignalPropagator", 'config': {}}
}
)
class TestAdminContent(TestCase):

def setUp(self):
Expand Down

0 comments on commit e664e97

Please sign in to comment.