Skip to content

Commit 19bf4e8

Browse files
committed
Made SearchIndex classes thread-safe. Thanks to craigds for the report & original patch.
1 parent 49627a6 commit 19bf4e8

File tree

3 files changed

+54
-1
lines changed

3 files changed

+54
-1
lines changed

haystack/indexes.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import copy
22
import sys
3+
import threading
34
from django.db.models import signals
45
from django.utils.encoding import force_unicode
56
from haystack.constants import ID, DJANGO_CT, DJANGO_ID
@@ -56,7 +57,7 @@ def __new__(cls, name, bases, attrs):
5657
return super(DeclarativeMetaclass, cls).__new__(cls, name, bases, attrs)
5758

5859

59-
class SearchIndex(object):
60+
class SearchIndex(threading.local):
6061
"""
6162
Base class for building indexes.
6263

tests/core/tests/indexes.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
import datetime
2+
import Queue
3+
from threading import Thread
4+
import time
25
from django.test import TestCase
36
from haystack.indexes import *
47
from core.models import MockModel, AThirdMockModel
@@ -195,6 +198,54 @@ def test_custom_prepare(self):
195198
self.assertEqual(len(self.cmi.full_prepare(mock)), 11)
196199
self.assertEqual(sorted(self.cmi.full_prepare(mock).keys()), ['author', 'author_exact', 'content', 'django_ct', 'django_id', 'extra', 'hello', 'id', 'pub_date', 'pub_date_exact', 'whee'])
197200

201+
def test_thread_safety(self):
202+
# This is a regression. ``SearchIndex`` used to write to
203+
# ``self.prepared_data``, which would leak between threads if things
204+
# went too fast.
205+
exceptions = []
206+
207+
def threaded_prepare(queue, index, model):
208+
try:
209+
index.queue = queue
210+
prepped = index.prepare(model)
211+
except Exception, e:
212+
exceptions.append(e)
213+
raise
214+
215+
class ThreadedSearchIndex(GoodMockSearchIndex):
216+
def prepare_author(self, obj):
217+
if obj.pk == 20:
218+
time.sleep(0.1)
219+
else:
220+
time.sleep(0.5)
221+
222+
queue.put(self.prepared_data['author'])
223+
return self.prepared_data['author']
224+
225+
tmi = ThreadedSearchIndex(MockModel, backend=self.msb)
226+
queue = Queue.Queue()
227+
mock_1 = MockModel()
228+
mock_1.pk = 20
229+
mock_1.author = 'foo'
230+
mock_1.pub_date = datetime.datetime(2009, 1, 31, 4, 19, 0)
231+
mock_2 = MockModel()
232+
mock_2.pk = 21
233+
mock_2.author = 'daniel%s' % mock_2.id
234+
mock_2.pub_date = datetime.datetime(2009, 1, 31, 4, 19, 0)
235+
236+
th1 = Thread(target=threaded_prepare, args=(queue, tmi, mock_1))
237+
th2 = Thread(target=threaded_prepare, args=(queue, tmi, mock_2))
238+
239+
th1.start()
240+
th2.start()
241+
th1.join()
242+
th2.join()
243+
244+
mock_1_result = queue.get()
245+
mock_2_result = queue.get()
246+
self.assertEqual(mock_1_result, u'foo')
247+
self.assertEqual(mock_2_result, u'daniel21')
248+
198249
def test_custom_prepare_author(self):
199250
mock = MockModel()
200251
mock.pk = 20

tests/settings.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
'core',
1717
]
1818

19+
SITE_ID = 1
1920
ROOT_URLCONF = 'core.urls'
2021

2122
HAYSTACK_SITECONF = 'core.search_sites'

0 commit comments

Comments
 (0)