Skip to content

Commit 6410cfc

Browse files
committed
Merge branch 'release/0.6'
2 parents 277cf7a + ae8a232 commit 6410cfc

File tree

12 files changed

+115
-47
lines changed

12 files changed

+115
-47
lines changed

.travis.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ install:
1212
before_script:
1313
- flake8 celery_haystack --ignore=E501
1414
script:
15-
- coverage run --branch --source=celery_haystack --omit=celery_haystack/tests `which django-admin.py` test celery_haystack
16-
- coverage report
15+
- coverage run --branch --source=celery_haystack `which django-admin.py` test celery_haystack
16+
- coverage report --omit=celery_haystack/test*
1717
env:
1818
- DJANGO=1.3.1 HAYSTACK=v1
1919
- DJANGO=1.3.1 HAYSTACK=v2

AUTHORS

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
1+
Josh Bohde
2+
Germán M. Bravo
13
Jannis Leidel <[email protected]>
24
Daniel Lindsley

LICENSE

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
Copyright (c) 2011-2012, Jannis Leidel.
1+
Copyright (c) 2011-2012, Jannis Leidel and other contributors.
22
All rights reserved.
33

44
Redistribution and use in source and binary forms, with or without modification,

README.rst

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22
celery-haystack
33
===============
44

5+
.. image:: https://secure.travis-ci.org/jezdez/celery-haystack.png?branch=develop
6+
:alt: Build Status
7+
:target: http://travis-ci.org/jezdez/celery-haystack
8+
59
This Django app allows you to utilize Celery for automatically updating and
610
deleting objects in a Haystack_ search index.
711

@@ -30,11 +34,13 @@ By default a few dependencies will automatically be installed:
3034

3135
- django-appconf_ -- An app to gracefully handle application settings.
3236

33-
- versiontools_ -- A library to help staying compatible to `PEP 386`_.
37+
- `django-celery-transactions`_ -- An app that "holds on to Celery tasks
38+
until the current database transaction is committed, avoiding potential
39+
race conditions as described in `Celery's user guide`_."
3440

3541
.. _django-appconf: http://pypi.python.org/pypi/django-appconf
36-
.. _versiontools: http://pypi.python.org/pypi/versiontools
37-
.. _`PEP 386`: http://www.python.org/dev/peps/pep-0386/
42+
.. _`django-celery-transactions`: https://github.com/chrisdoble/django-celery-transactions
43+
.. _`Celery's user guide`: http://celery.readthedocs.org/en/latest/userguide/tasks.html#database-transactions
3844

3945
Setup
4046
-----

celery_haystack/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = '0.5'
1+
__version__ = '0.6'

celery_haystack/conf.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ class CeleryHaystack(AppConf):
88
RETRY_DELAY = 5 * 60
99
MAX_RETRIES = 1
1010
DEFAULT_TASK = 'celery_haystack.tasks.CeleryHaystackSignalHandler'
11+
TRANSACTION_SAFE = True
1112

1213
COMMAND_BATCH_SIZE = None
1314
COMMAND_AGE = None

celery_haystack/indexes.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,19 +25,27 @@ def handle_model(self, model):
2525
# operation.
2626
def _setup_save(self, model=None):
2727
model = self.handle_model(model)
28-
signals.post_save.connect(self.enqueue_save, sender=model)
28+
signals.post_save.connect(self._enqueue_save, sender=model, dispatch_uid=CelerySearchIndex)
2929

3030
def _setup_delete(self, model=None):
3131
model = self.handle_model(model)
32-
signals.post_delete.connect(self.enqueue_delete, sender=model)
32+
signals.post_delete.connect(self._enqueue_delete, sender=model, dispatch_uid=CelerySearchIndex)
3333

3434
def _teardown_save(self, model=None):
3535
model = self.handle_model(model)
36-
signals.post_save.disconnect(self.enqueue_save, sender=model)
36+
signals.post_save.disconnect(self._enqueue_save, sender=model, dispatch_uid=CelerySearchIndex)
3737

3838
def _teardown_delete(self, model=None):
3939
model = self.handle_model(model)
40-
signals.post_delete.disconnect(self.enqueue_delete, sender=model)
40+
signals.post_delete.disconnect(self._enqueue_delete, sender=model, dispatch_uid=CelerySearchIndex)
41+
42+
def _enqueue_save(self, instance, **kwargs):
43+
if not getattr(instance, 'skip_indexing', False):
44+
self.enqueue_save(instance, **kwargs)
45+
46+
def _enqueue_delete(self, instance, **kwargs):
47+
if not getattr(instance, 'skip_indexing', False):
48+
self.enqueue_delete(instance, **kwargs)
4149

4250
def enqueue_save(self, instance, **kwargs):
4351
return self.enqueue('update', instance)

celery_haystack/tasks.py

Lines changed: 58 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,39 @@
1+
import logging
12
from django.core.exceptions import ImproperlyConfigured
23
from django.core.management import call_command
34
from django.db.models.loading import get_model
45

5-
from celery.task import Task
66
from celery_haystack.conf import settings
77

88
try:
9-
from haystack import connections
10-
index_holder = connections['default'].get_unified_index()
9+
from haystack import connections, connection_router
1110
from haystack.exceptions import NotHandled as IndexNotFoundException
1211
legacy = False
1312
except ImportError:
1413
try:
15-
from haystack import site as index_holder
14+
from haystack import site
1615
from haystack.exceptions import NotRegistered as IndexNotFoundException # noqa
1716
legacy = True
1817
except ImportError, e:
1918
raise ImproperlyConfigured("Haystack couldn't be imported: %s" % e)
2019

20+
if settings.CELERY_HAYSTACK_TRANSACTION_SAFE and not settings.CELERY_ALWAYS_EAGER:
21+
from djcelery_transactions import PostTransactionTask as Task
22+
else:
23+
from celery.task import Task # noqa
24+
2125

2226
class CeleryHaystackSignalHandler(Task):
2327
using = settings.CELERY_HAYSTACK_DEFAULT_ALIAS
2428
max_retries = settings.CELERY_HAYSTACK_MAX_RETRIES
2529
default_retry_delay = settings.CELERY_HAYSTACK_RETRY_DELAY
2630

31+
def get_logger(self, *args, **kwargs):
32+
logger = super(CeleryHaystackSignalHandler, self).get_logger(*args, **kwargs)
33+
if settings.DEBUG:
34+
logger.setLogger(logging.DEBUG)
35+
return logger
36+
2737
def split_identifier(self, identifier, **kwargs):
2838
"""
2939
Break down the identifier representing the instance.
@@ -53,39 +63,40 @@ def get_model_class(self, object_path, **kwargs):
5363
model_class = get_model(app_name, classname)
5464

5565
if model_class is None:
56-
logger = self.get_logger(**kwargs)
57-
logger.error("Could not load model "
58-
"from '%s'. Moving on..." % object_path)
59-
return None
60-
66+
raise ImproperlyConfigured("Could not load model '%s'." %
67+
object_path)
6168
return model_class
6269

6370
def get_instance(self, model_class, pk, **kwargs):
6471
"""
6572
Fetch the instance in a standarized way.
6673
"""
6774
logger = self.get_logger(**kwargs)
75+
instance = None
6876
try:
69-
instance = model_class.objects.get(pk=pk)
77+
instance = model_class._default_manager.get(pk=int(pk))
7078
except model_class.DoesNotExist:
71-
logger.error("Couldn't load model instance "
72-
"with pk #%s. Somehow it went missing?" % pk)
73-
return None
79+
logger.error("Couldn't load %s.%s.%s. Somehow it went missing?" %
80+
(model_class._meta.app_label.lower(),
81+
model_class._meta.object_name.lower(), pk))
7482
except model_class.MultipleObjectsReturned:
75-
logger.error("More than one object with pk #%s. Oops?" % pk)
76-
return None
77-
83+
logger.error("More than one object with pk %s. Oops?" % pk)
7884
return instance
7985

8086
def get_index(self, model_class, **kwargs):
8187
"""
8288
Fetch the model's registered ``SearchIndex`` in a standarized way.
8389
"""
84-
logger = self.get_logger(**kwargs)
8590
try:
91+
if legacy:
92+
index_holder = site
93+
else:
94+
backend_alias = connection_router.for_write(**{'models': [model_class]})
95+
index_holder = connections[backend_alias].get_unified_index() # noqa
8696
return index_holder.get_index(model_class)
8797
except IndexNotFoundException:
88-
logger.error("Couldn't find a SearchIndex for %s." % model_class)
98+
raise ImproperlyConfigured("Couldn't find a SearchIndex for %s." %
99+
model_class)
89100
return None
90101

91102
def get_handler_options(self, **kwargs):
@@ -104,58 +115,69 @@ def run(self, action, identifier, **kwargs):
104115
# First get the object path and pk (e.g. ('notes.note', 23))
105116
object_path, pk = self.split_identifier(identifier, **kwargs)
106117
if object_path is None or pk is None:
107-
logger.error("Skipping.")
108-
return
118+
msg = "Couldn't handle object with identifier %s" % identifier
119+
logger.error(msg)
120+
raise ValueError(msg)
109121

110122
# Then get the model class for the object path
111123
model_class = self.get_model_class(object_path, **kwargs)
112124
current_index = self.get_index(model_class, **kwargs)
125+
current_index_name = ".".join([current_index.__class__.__module__,
126+
current_index.__class__.__name__])
113127

114128
if action == 'delete':
115-
# If the object is gone, we'll use just the identifier against the
116-
# index.
129+
# If the object is gone, we'll use just the identifier
130+
# against the index.
117131
try:
118132
handler_options = self.get_handler_options(**kwargs)
119133
current_index.remove_object(identifier, **handler_options)
120134
except Exception, exc:
121135
logger.error(exc)
122-
self.retry([action, identifier], kwargs, exc=exc)
136+
self.retry(exc=exc)
123137
else:
124-
logger.debug("Deleted '%s' from index" % identifier)
125-
return
126-
138+
msg = ("Deleted '%s' (with %s)" %
139+
(identifier, current_index_name))
140+
logger.debug(msg)
141+
return msg
127142
elif action == 'update':
128143
# and the instance of the model class with the pk
129144
instance = self.get_instance(model_class, pk, **kwargs)
130145
if instance is None:
131-
logger.debug("Didn't update index for '%s'" % identifier)
132-
return
146+
logger.debug("Failed updating '%s' (with %s)" %
147+
(identifier, current_index_name))
148+
raise ValueError("Couldn't load object '%s'" % identifier)
133149

134150
# Call the appropriate handler of the current index and
135151
# handle exception if neccessary
136-
logger.debug("Indexing '%s'." % instance)
137152
try:
138153
handler_options = self.get_handler_options(**kwargs)
139154
current_index.update_object(instance, **handler_options)
140155
except Exception, exc:
141156
logger.error(exc)
142-
self.retry([action, identifier], kwargs, exc=exc)
157+
self.retry(exc=exc)
143158
else:
144-
logger.debug("Updated index with '%s'" % instance)
159+
msg = ("Updated '%s' (with %s)" %
160+
(identifier, current_index_name))
161+
logger.debug(msg)
162+
return msg
145163
else:
146164
logger.error("Unrecognized action '%s'. Moving on..." % action)
147-
self.retry([action, identifier], kwargs, exc=exc)
165+
raise ValueError("Unrecognized action %s" % action)
148166

149167

150168
class CeleryHaystackUpdateIndex(Task):
151169
"""
152170
A celery task class to be used to call the update_index management
153171
command from Celery.
154172
"""
173+
def get_logger(self, *args, **kwargs):
174+
logger = super(CeleryHaystackUpdateIndex, self).get_logger(*args, **kwargs)
175+
if settings.DEBUG:
176+
logger.setLogger(logging.DEBUG)
177+
return logger
178+
155179
def run(self, apps=None, **kwargs):
156180
logger = self.get_logger(**kwargs)
157-
logger.info("Starting update index")
158-
# Run the update_index management command
159181
defaults = {
160182
'batchsize': settings.CELERY_HAYSTACK_COMMAND_BATCH_SIZE,
161183
'age': settings.CELERY_HAYSTACK_COMMAND_AGE,
@@ -167,5 +189,7 @@ def run(self, apps=None, **kwargs):
167189
defaults.update(kwargs)
168190
if apps is None:
169191
apps = settings.CELERY_HAYSTACK_COMMAND_APPS
192+
# Run the update_index management command
193+
logger.info("Starting update index")
170194
call_command('update_index', *apps, **defaults)
171195
logger.info("Finishing update index")

celery_haystack/test_settings.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import os
22

3+
DEBUG = True
4+
35
TEST_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), 'tests'))
46

57
INSTALLED_APPS = [

celery_haystack/utils.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,4 @@ def get_update_task(task_path=None):
1717
except AttributeError:
1818
raise ImproperlyConfigured('Module "%s" does not define a "%s" '
1919
'class.' % (module, attr))
20-
return Task()
20+
return Task

docs/changelog.rst

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,30 @@
11
Changelog
22
=========
33

4+
v0.6 (2012-06-27)
5+
-----------------
6+
7+
* *backwards incompatible change* Added support for
8+
`django-celery-transactions`_ to make sure the tasks are respecting
9+
Django's transaction management. It holds on to Celery tasks
10+
until the current database transaction is committed, avoiding potential
11+
race conditions as described in `Celery's user guide`_.
12+
13+
This is **enabled by default** but can be disabled in case you want
14+
to manually manage the tranasctions:
15+
16+
CELERY_HAYSTACK_TRANSACTION_SAFE = False
17+
18+
* Refactored the error handling to always return a message about what
19+
happened in every step of the index interaction. Raise exception about
20+
misconfiguration and wrong parameters quicker.
21+
22+
* Improved support for multiple search indexes as implemented by
23+
Haystack 2.X. Many thanks to Germán M. Bravo (Kronuz).
24+
25+
.. _`django-celery-transactions`: https://github.com/chrisdoble/django-celery-transactions
26+
.. _`Celery's user guide`: http://celery.readthedocs.org/en/latest/userguide/tasks.html#database-transactions
27+
428
v0.5 (2012-05-23)
529
-----------------
630

setup.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,8 @@ def find_version(*file_paths):
3939
'Topic :: Utilities',
4040
],
4141
install_requires=[
42-
'django-appconf >= 0.4.1',
42+
'django-appconf>=0.4.1',
43+
'django-celery-transactions>=0.1.2'
4344
],
4445
zip_safe=False,
4546
)

0 commit comments

Comments
 (0)