Projet

Général

Profil

0001-misc-add-log-mecanism-for-cron-jobs-57604.patch

Lauréline Guérin, 11 octobre 2021 18:02

Télécharger (9,2 ko)

Voir les différences:

Subject: [PATCH 1/2] misc: add log mecanism for cron jobs (#57604)

 tests/test_publisher.py                | 58 +++++++++++++++++++++++++-
 wcs/qommon/cron.py                     | 30 ++++++++++++-
 wcs/qommon/management/commands/cron.py |  5 +++
 3 files changed, 90 insertions(+), 3 deletions(-)
tests/test_publisher.py
1 1
import datetime
2 2
import io
3 3
import json
4
import logging
4 5
import os
5 6
import pickle
6 7
import re
......
206 207
    assert pub.cfg['sp'] == {'what': 'ever'}
207 208

  
208 209

  
209
def test_cron_command():
210
def test_cron_command(caplog, settings):
211
    caplog.set_level(logging.INFO, logger="wcs.cron")
212
    offset = ord(settings.SECRET_KEY[-1]) % 60
210 213
    pub = create_temporary_pub()
211 214
    with mock.patch('tempfile.gettempdir') as gettempdir:
212 215
        gettempdir.side_effect = lambda: pub.app_dir
......
221 224
                mock_tenants.return_value = [
222 225
                    Tenant(os.path.join(pub.app_dir, x)) for x in ('example.net', 'foo.bar', 'something.com')
223 226
                ]
227
                caplog.clear()
224 228
                call_command('cron')
225 229
                assert cron_worker.call_count == 3
230
                assert caplog.record_tuples == [
231
                    ('wcs.cron', logging.INFO, 'starting cron (minutes offset is %s)' % offset),
232
                    ('wcs.cron', logging.INFO, '[tenant example.net] start'),
233
                    ('wcs.cron', logging.INFO, '[tenant foo.bar] start'),
234
                    ('wcs.cron', logging.INFO, '[tenant something.com] start'),
235
                ]
226 236
                cron_worker.reset_mock()
237
                caplog.clear()
227 238
                call_command('cron', domain='example.net')
228 239
                assert cron_worker.call_count == 1
240
                assert caplog.record_tuples == [
241
                    ('wcs.cron', logging.INFO, 'starting cron (minutes offset is %s)' % offset),
242
                    ('wcs.cron', logging.INFO, '[tenant example.net] start'),
243
                ]
229 244
                cron_worker.reset_mock()
230 245

  
231 246
                # disable cron on something.com
......
238 253
                        '''
239 254
                    )
240 255

  
256
                caplog.clear()
241 257
                call_command('cron')
242 258
                assert cron_worker.call_count == 2
259
                assert caplog.record_tuples == [
260
                    ('wcs.cron', logging.INFO, 'starting cron (minutes offset is %s)' % offset),
261
                    ('wcs.cron', logging.INFO, '[tenant example.net] start'),
262
                    ('wcs.cron', logging.INFO, '[tenant foo.bar] start'),
263
                ]
243 264
                cron_worker.reset_mock()
244 265
                os.unlink(site_options_path)
245 266

  
......
289 310
        def job2(pub):
290 311
            jobs.append('job2')
291 312

  
292
        def job3(pub):
313
        def job3(pub, job):
293 314
            jobs.append('job3')
315
            for key in ['foo', 'bar', 'blah']:
316
                with job.log_long_job(key):
317
                    pass
294 318

  
295 319
        @classmethod
296 320
        def register_test_cronjobs(cls):
......
303 327
            call_command('cron', job_name='job1', domain='example.net')
304 328
            assert jobs == []
305 329
            get_publisher_class().cronjobs = []
330
            caplog.clear()
306 331
            call_command('cron', job_name='job2', domain='example.net')
307 332
            assert jobs == ['job2']
333
            assert caplog.record_tuples == [
334
                ('wcs.cron', logging.INFO, 'starting cron (minutes offset is %s)' % offset),
335
                ('wcs.cron', logging.INFO, '[tenant example.net] start'),
336
            ]
337
            get_publisher_class().cronjobs = []
338
            jobs = []
339
            caplog.clear()
340
            with mock.patch('wcs.qommon.cron.CronJob.LONG_JOB_DURATION', 0):
341
                call_command('cron', job_name='job2', domain='example.net')
342
            assert caplog.record_tuples == [
343
                ('wcs.cron', logging.INFO, 'starting cron (minutes offset is %s)' % offset),
344
                ('wcs.cron', logging.INFO, '[tenant example.net] start'),
345
                ('wcs.cron', logging.INFO, '[tenant example.net] long job: job2 (took 0 minutes)'),
346
            ]
347
            assert jobs == ['job2']
348
            get_publisher_class().cronjobs = []
349
            jobs = []
350
            caplog.clear()
351
            with mock.patch('wcs.qommon.cron.CronJob.LONG_JOB_DURATION', 0):
352
                call_command('cron', job_name='job3', domain='example.net')
353
            assert caplog.record_tuples == [
354
                ('wcs.cron', logging.INFO, 'starting cron (minutes offset is %s)' % offset),
355
                ('wcs.cron', logging.INFO, '[tenant example.net] start'),
356
                ('wcs.cron', logging.INFO, '[tenant example.net] job3: running on "foo" took 0 minutes'),
357
                ('wcs.cron', logging.INFO, '[tenant example.net] job3: running on "bar" took 0 minutes'),
358
                ('wcs.cron', logging.INFO, '[tenant example.net] job3: running on "blah" took 0 minutes'),
359
                ('wcs.cron', logging.INFO, '[tenant example.net] long job: job3 (took 0 minutes)'),
360
            ]
361
            assert jobs == ['job3']
308 362

  
309 363

  
310 364
def test_clean_afterjobs():
wcs/qommon/cron.py
14 14
# You should have received a copy of the GNU General Public License
15 15
# along with this program; if not, see <http://www.gnu.org/licenses/>.
16 16

  
17
import inspect
18
import logging
19
from contextlib import contextmanager
20
from time import perf_counter
21

  
17 22
from django.conf import settings
23
from quixote import get_publisher
18 24

  
19 25

  
20 26
class CronJob:
......
25 31
    days = None
26 32
    function = None
27 33

  
34
    logger = logging.getLogger('wcs.cron')
35
    LONG_JOB_DURATION = 2 * 60  # 2 minutes
36

  
28 37
    def __init__(self, function, name=None, hours=None, minutes=None, weekdays=None, days=None):
29 38
        self.function = function
30 39
        self.name = name
......
33 42
        self.weekdays = weekdays
34 43
        self.days = days
35 44

  
45
    @contextmanager
46
    def log_long_job(self, message=None):
47
        start = perf_counter()
48
        yield
49
        duration = perf_counter() - start
50
        if duration > self.LONG_JOB_DURATION:
51
            minutes = int(duration / 60)
52
            if message:
53
                self.log('%s: running on "%s" took %d minutes' % (self.name, message, minutes))
54
            else:
55
                self.log('long job: %s (took %s minutes)' % (self.name, minutes))
56

  
57
    def log(self, message):
58
        self.logger.info('[tenant %s] %s', get_publisher().tenant.hostname, message)
59

  
36 60

  
37 61
def cron_worker(publisher, now, job_name=None):
38 62
    # reindex user and formdata if needed (should only be run once)
......
66 90
        for extra_source in publisher.extra_sources:
67 91
            publisher.substitutions.feed(extra_source(publisher, None))
68 92
        try:
69
            job.function(publisher)
93
            with job.log_long_job():
94
                if 'job' in inspect.getargspec(job.function).args:
95
                    job.function(publisher, job=job)
96
                else:
97
                    job.function(publisher)
70 98
        except Exception as e:
71 99
            publisher.record_error(exception=e, context='[CRON]', notify=True)
wcs/qommon/management/commands/cron.py
14 14
# You should have received a copy of the GNU General Public License
15 15
# along with this program; if not, see <http://www.gnu.org/licenses/>.
16 16

  
17
import logging
17 18
import os
18 19
import tempfile
19 20
import time
......
29 30

  
30 31
class Command(BaseCommand):
31 32
    help = 'Execute cronjobs'
33
    logger = logging.getLogger('wcs.cron')
32 34

  
33 35
    def add_arguments(self, parser):
34 36
        parser.set_defaults(verbosity=0)
......
46 48
            if verbosity > 1:
47 49
                print('Command is ignored because DISABLE_CRON_JOBS is set in settings')
48 50
            return
51
        offset = ord(settings.SECRET_KEY[-1]) % 60
52
        self.logger.info('starting cron (minutes offset is %s)', offset)
49 53
        lockfile = os.path.join(tempfile.gettempdir(), 'wcs-cron-in-progress.lock')
50 54
        if options.get('domain'):
51 55
            domains = [options.get('domain')]
......
67 71
                        continue
68 72
                    if verbosity > 1:
69 73
                        print('cron work on %s' % hostname)
74
                    self.logger.info('[tenant %s] start', hostname)
70 75
                    cron_worker(publisher, now, job_name=options.get('job_name'))
71 76
            if verbosity > 2:
72 77
                print('cron end (release lock %s)' % lockfile)
73
-