Skip to content

Commit 2bacd29

Browse files
asfaltboyauvipy
authored andcommitted
Delete results immediately, not in two steps (celery#118)
* Delete results immediately, not in two steps Takes inspiration from the benchmark test in the related ticket, to remove the slowest running operation in the cleanup task. The same, direct deletion operation, is done in the built-in database backend of celery: https://github.com/celery/celery/blob/c780e3a954579ee5b7243b9cb7444e44a6398d5b/celery/backends/database/__init__.py#L207-L217 To test, creates some records, and run clean up task: def create_old_results(records=200000, age=5): """ takes 40s to create 200k old records on my machine "" from datetime import datetime, timedelta from django.db import connection from celery import states, uuid from django_celery_results.models import * old_date = datetime.now() - timedelta(days=age) bunch = [TaskResult(task_id=uuid()) for _ in range(records)] created = TaskResult.objects.bulk_create(bunch) TaskResult.objects.filter(id__gte=created[0].id).update(date_done=old_date) Before the modification > Task celery.backend_cleanup[4cd0ad59-f241-4d3b-b848-45dfc9564725] succeeded in 8.506982273000176s: None After the modification > Task celery.backend_cleanup[069fe103-f894-4b96-ad1a-720a6bbbec6f] succeeded in 0.3088349770041532s: None fixes celery#117 * Use the new pytest execution form #dropthedot Ref: https://stackoverflow.com/a/41893170/484127 * Add benchmark for delete_expired in test suite * Remove the TaskResult.hidden field
1 parent df7c44f commit 2bacd29

File tree

14 files changed

+158
-20
lines changed

14 files changed

+158
-20
lines changed

Diff for: .travis.yml

+1
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ matrix:
2727
- { python: 2.7, env: TOXENV=flakeplus }
2828
- { python: 3.6, env: TOXENV=pydocstyle }
2929
- { python: 3.6, env: TOXENV=cov }
30+
- { python: 3.6, env: TOXENV=integration }
3031
# disabled temporarily due to upstream bug
3132
# https://github.com/celery/sphinx_celery/issues/9
3233
# - { python: 3.5, env: TOXENV=apicheck }

Diff for: Makefile

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
PROJ=django_celery_results
22
PGPIDENT="Celery Security Team"
33
PYTHON=python
4-
PYTEST=py.test
4+
PYTEST=pytest
55
GIT=git
66
TOX=tox
77
ICONV=iconv
@@ -141,7 +141,7 @@ test:
141141
$(PYTHON) setup.py test
142142

143143
cov: covbuild
144-
(cd $(TESTDIR); py.test -x --cov=django_celery_results --cov-report=html)
144+
(cd $(TESTDIR); pytest -x --cov=django_celery_results --cov-report=html)
145145

146146
build:
147147
$(PYTHON) setup.py sdist bdist_wheel

Diff for: conftest.py

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import pytest
2+
3+
4+
def pytest_addoption(parser):
5+
parser.addoption(
6+
'-B',
7+
'--run-benchmarks',
8+
action='store_true',
9+
default=False,
10+
help='run benchmarks',
11+
)
12+
13+
14+
def pytest_runtest_setup(item):
15+
"""
16+
Skip tests marked benchmark unless --run-benchmark is given to pytest
17+
"""
18+
run_benchmarks = item.config.getoption('--run-benchmarks')
19+
20+
is_benchmark = any(item.iter_markers(name="benchmark"))
21+
22+
if is_benchmark:
23+
if run_benchmarks:
24+
return
25+
26+
pytest.skip(
27+
'need --run-benchmarks to run benchmarks'
28+
)
29+
30+
31+
def pytest_collection_modifyitems(items):
32+
"""
33+
Add the "benchmark" mark to tests that start with "benchmark_".
34+
"""
35+
for item in items:
36+
test_class_name = item.cls.__name__
37+
if test_class_name.startswith("benchmark_"):
38+
item.add_marker(pytest.mark.benchmark)

Diff for: django_celery_results/admin.py

+1-2
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ class TaskResultAdmin(admin.ModelAdmin):
2222
date_hierarchy = 'date_done'
2323
list_display = ('task_id', 'task_name', 'date_done', 'status', 'worker')
2424
list_filter = ('status', 'date_done', 'task_name',)
25-
readonly_fields = ('date_created', 'date_done', 'result', 'hidden', 'meta')
25+
readonly_fields = ('date_created', 'date_done', 'result', 'meta')
2626
search_fields = ('task_name', 'task_id', 'status')
2727
fieldsets = (
2828
(None, {
@@ -49,7 +49,6 @@ class TaskResultAdmin(admin.ModelAdmin):
4949
'date_created',
5050
'date_done',
5151
'traceback',
52-
'hidden',
5352
'meta',
5453
),
5554
'classes': ('extrapretty', 'wide')

Diff for: django_celery_results/managers.py

+1-2
Original file line numberDiff line numberDiff line change
@@ -173,5 +173,4 @@ def get_all_expired(self, expires):
173173
def delete_expired(self, expires):
174174
"""Delete all expired results."""
175175
with transaction.atomic():
176-
self.get_all_expired(expires).update(hidden=True)
177-
self.filter(hidden=True).delete()
176+
self.get_all_expired(expires).delete()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# -*- coding: utf-8 -*-
2+
# Generated by Django 2.2.6 on 2019-10-27 11:29
3+
4+
# this file is auto-generated so don't do flake8 on it
5+
# flake8: noqa
6+
7+
from __future__ import absolute_import, unicode_literals
8+
9+
from django.db import migrations
10+
11+
12+
class Migration(migrations.Migration):
13+
14+
dependencies = [
15+
('django_celery_results', '0006_taskresult_date_created'),
16+
]
17+
18+
operations = [
19+
migrations.RemoveField(
20+
model_name='taskresult',
21+
name='hidden',
22+
),
23+
]

Diff for: django_celery_results/models.py

-5
Original file line numberDiff line numberDiff line change
@@ -75,11 +75,6 @@ class TaskResult(models.Model):
7575
blank=True, null=True,
7676
verbose_name=_('Traceback'),
7777
help_text=_('Text of the traceback if the task generated one'))
78-
hidden = models.BooleanField(
79-
editable=False, default=False, db_index=True,
80-
verbose_name=_('Hidden'),
81-
help_text=_('Soft Delete flag that can be used '
82-
'instead of full delete'))
8378
meta = models.TextField(
8479
null=True, default=None, editable=False,
8580
verbose_name=_('Task Meta Information'),

Diff for: requirements/test.txt

+1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
case>=1.3.1
22
pytest>=4.3
33
pytest-django
4+
pytest-benchmark
45
pytz>dev
56
psycopg2cffi

Diff for: setup.cfg

+5-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
[tool:pytest]
2-
testpaths = t/unit
2+
testpaths = t/
33
python_classes = test_*
4-
DJANGO_SETTINGS_MODULE=t.proj.settings
4+
python_files = test_* benchmark_*
5+
DJANGO_SETTINGS_MODULE = t.proj.settings
6+
markers =
7+
benchmark: mark a test as a benchmark
58

69
[flake8]
710
# classes can be lowercase, arguments and variables can be uppercase

Diff for: setup.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ def reqs(*f):
121121

122122

123123
class pytest(setuptools.command.test.test):
124-
user_options = [('pytest-args=', 'a', 'Arguments to pass to py.test')]
124+
user_options = [('pytest-args=', 'a', 'Arguments to pass to pytest')]
125125

126126
def initialize_options(self):
127127
setuptools.command.test.test.initialize_options(self)

Diff for: t/unit/conftest.py renamed to t/conftest.py

-3
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,8 @@
22

33
import pytest
44

5-
from celery.contrib.pytest import depends_on_current_app
65
from celery.contrib.testing.app import TestApp, Trap
76

8-
__all__ = ['app', 'depends_on_current_app']
9-
107

118
@pytest.fixture(scope='session', autouse=True)
129
def setup_default_app_trap():

Diff for: t/integration/__init__.py

Whitespace-only changes.

Diff for: t/integration/benchmark_models.py

+75
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
from __future__ import absolute_import, unicode_literals
2+
3+
import pytest
4+
5+
from datetime import timedelta
6+
import time
7+
8+
from django.test import TransactionTestCase
9+
10+
from celery import uuid
11+
12+
from django_celery_results.models import TaskResult
13+
from django_celery_results.utils import now
14+
15+
RECORDS_COUNT = 100000
16+
17+
18+
@pytest.fixture()
19+
def use_benchmark(request, benchmark):
20+
def wrapped(a=10, b=5):
21+
return a + b
22+
request.cls.benchmark = benchmark
23+
24+
25+
@pytest.mark.usefixtures('use_benchmark')
26+
@pytest.mark.usefixtures('depends_on_current_app')
27+
class benchmark_Models(TransactionTestCase):
28+
multi_db = True
29+
30+
@pytest.fixture(autouse=True)
31+
def setup_app(self, app):
32+
self.app = app
33+
self.app.conf.result_serializer = 'pickle'
34+
self.app.conf.result_backend = (
35+
'django_celery_results.backends:DatabaseBackend')
36+
37+
def create_many_task_result(self, count):
38+
start = time.time()
39+
draft_results = [TaskResult(task_id=uuid()) for _ in range(count)]
40+
drafted = time.time()
41+
results = TaskResult.objects.bulk_create(draft_results)
42+
done_creating = time.time()
43+
44+
print((
45+
'drafting time: {drafting:.2f}\n'
46+
'bulk_create time: {done:.2f}\n'
47+
'------'
48+
).format(drafting=drafted - start, done=done_creating - drafted))
49+
return results
50+
51+
def setup_records_to_delete(self):
52+
self.create_many_task_result(count=RECORDS_COUNT)
53+
mid_point = TaskResult.objects.order_by('id')[int(RECORDS_COUNT / 2)]
54+
todelete = TaskResult.objects.filter(id__gte=mid_point.id)
55+
todelete.update(date_done=now() - timedelta(days=10))
56+
57+
def test_taskresult_delete_expired(self):
58+
start = time.time()
59+
self.setup_records_to_delete()
60+
after_setup = time.time()
61+
self.benchmark.pedantic(
62+
TaskResult.objects.delete_expired,
63+
args=(self.app.conf.result_expires,),
64+
iterations=1,
65+
rounds=1,
66+
)
67+
done = time.time()
68+
assert TaskResult.objects.count() == int(RECORDS_COUNT / 2)
69+
70+
print((
71+
'------'
72+
'setup time: {setup:.2f}\n'
73+
'bench time: {bench:.2f}\n'
74+
).format(setup=after_setup - start, bench=done - after_setup))
75+
assert self.benchmark.stats.stats.max < 1

Diff for: tox.ini

+10-3
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ envlist =
1212
apicheck
1313
pydocstyle
1414
cov
15+
integration
1516

1617
[travis:env]
1718
DJANGO =
@@ -29,7 +30,7 @@ deps=
2930
django21: -r{toxinidir}/requirements/test-django21.txt
3031
django22: -r{toxinidir}/requirements/test-django22.txt
3132

32-
cov: -r{toxinidir}/requirements/test-django.txt
33+
cov,integration: -r{toxinidir}/requirements/test-django.txt
3334

3435
linkcheck,apicheck: -r{toxinidir}/requirements/docs.txt
3536
flake8,flakeplus,pydocstyle: -r{toxinidir}/requirements/pkgutils.txt
@@ -38,7 +39,7 @@ recreate = True
3839
commands =
3940
pip install -U celery==4.3
4041
pip install -U kombu==4.5
41-
py.test -xv
42+
pytest -xv
4243

4344
[testenv:apicheck]
4445
commands =
@@ -64,4 +65,10 @@ commands =
6465
usedevelop = true
6566
commands = pip install -U celery==4.3
6667
pip install -U kombu==4.5
67-
py.test --cov=django_celery_results --cov-report=xml --no-cov-on-fail
68+
pytest --cov=django_celery_results --cov-report=xml --no-cov-on-fail
69+
70+
[testenv:integration]
71+
commands =
72+
pip install -U celery==4.3
73+
pip install -U kombu==4.5
74+
pytest -B -xv

0 commit comments

Comments
 (0)