Merge lp:~robru/friends/keepalive into lp:~super-friends/friends/raring

Proposed by Robert Bruce Park
Status: Merged
Merged at revision: 181
Proposed branch: lp:~robru/friends/keepalive
Merge into: lp:~super-friends/friends/raring
Diff against target: 380 lines (+132/-36)
5 files modified
friends/main.py (+1/-7)
friends/protocols/twitter.py (+0/-1)
friends/service/dispatcher.py (+56/-6)
friends/tests/test_dispatcher.py (+75/-10)
friends/utils/base.py (+0/-12)
To merge this branch: bzr merge lp:~robru/friends/keepalive
Reviewer Review Type Date Requested Status
Ken VanDine Approve
Review via email: mp+154235@code.launchpad.net

Commit message

Keep the Dispatcher alive for 30s beyond the return of the final method invocation.

To post a comment you must log in.
Revision history for this message
Ken VanDine (ken-vandine) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'friends/main.py'
2--- friends/main.py 2013-03-14 19:44:13 +0000
3+++ friends/main.py 2013-03-20 01:26:22 +0000
4@@ -62,11 +62,9 @@
5 GObject.threads_init(None)
6
7 from friends.service.dispatcher import Dispatcher, DBUS_INTERFACE
8-from friends.utils.base import _OperationThread, _publish_lock
9-from friends.utils.base import Base, initialize_caches
10+from friends.utils.base import Base, initialize_caches, _publish_lock
11 from friends.utils.model import Model, prune_model
12 from friends.utils.logging import initialize
13-from friends.utils.avatar import Avatar
14
15
16 # Optional performance profiling module.
17@@ -89,10 +87,6 @@
18 print(class_name)
19 return
20
21- # Our threading implementation needs to know how to quit the
22- # application once all threads have completed.
23- _OperationThread.shutdown = loop.quit
24-
25 # Disallow multiple instances of friends-dispatcher
26 bus = dbus.SessionBus()
27 obj = bus.get_object('org.freedesktop.DBus', '/org/freedesktop/DBus')
28
29=== modified file 'friends/protocols/twitter.py'
30--- friends/protocols/twitter.py 2013-03-19 03:11:28 +0000
31+++ friends/protocols/twitter.py 2013-03-20 01:26:22 +0000
32@@ -26,7 +26,6 @@
33 import logging
34
35 from urllib.parse import quote
36-from gi.repository import GLib
37
38 from friends.utils.avatar import Avatar
39 from friends.utils.base import Base, feature
40
41=== modified file 'friends/service/dispatcher.py'
42--- friends/service/dispatcher.py 2013-03-14 13:29:53 +0000
43+++ friends/service/dispatcher.py 2013-03-20 01:26:22 +0000
44@@ -28,12 +28,13 @@
45 import dbus.service
46
47 from gi.repository import GLib
48+from contextlib import ContextDecorator
49
50 from friends.utils.avatar import Avatar
51 from friends.utils.account import AccountManager
52 from friends.utils.manager import protocol_manager
53 from friends.utils.menus import MenuManager
54-from friends.utils.model import Model
55+from friends.utils.model import Model, persist_model
56 from friends.shorteners import lookup
57
58
59@@ -43,6 +44,48 @@
60 STUB = lambda *ignore, **kwignore: None
61
62
63+# Avoid race condition during shut-down
64+_exit_lock = threading.Lock()
65+
66+
67+class ManageTimers(ContextDecorator):
68+ """Exit the dispatcher 30s after the most recent method call returns."""
69+ timers = set()
70+ callback = STUB
71+
72+ def __enter__(self):
73+ self.clear_all_timers()
74+
75+ def __exit__(self, *ignore):
76+ self.set_new_timer()
77+
78+ def clear_all_timers(self):
79+ log.debug('Clearing {} shutdown timer(s)...'.format(len(self.timers)))
80+ while self.timers:
81+ GLib.source_remove(self.timers.pop())
82+
83+ def set_new_timer(self):
84+ # Concurrency will cause two methods to exit near each other,
85+ # causing two timers to be set, so we have to clear them again.
86+ self.clear_all_timers()
87+ log.debug('Starting new shutdown timer...')
88+ self.timers.add(GLib.timeout_add_seconds(30, self.terminate))
89+
90+ def terminate(self, *ignore):
91+ """Exit the dispatcher, but only if there are no active subthreads."""
92+ with _exit_lock:
93+ if threading.activeCount() < 2:
94+ log.debug('No threads found, shutting down.')
95+ persist_model()
96+ self.timers.add(GLib.idle_add(self.callback))
97+ else:
98+ log.debug('Delaying shutdown because active threads found.')
99+ self.set_new_timer()
100+
101+
102+exit_after_idle = ManageTimers()
103+
104+
105 class Dispatcher(dbus.service.Object):
106 """This is the primary handler of dbus method calls."""
107 __dbus_object_path__ = '/com/canonical/friends/Dispatcher'
108@@ -59,10 +102,13 @@
109 self.menu_manager = MenuManager(self.Refresh, self.mainloop.quit)
110 Model.connect('row-added', self._increment_unread_count)
111
112+ ManageTimers.callback = mainloop.quit
113+
114 def _increment_unread_count(self, model, itr):
115 self._unread_count += 1
116 self.menu_manager.update_unread_count(self._unread_count)
117
118+ @exit_after_idle
119 @dbus.service.method(DBUS_INTERFACE)
120 def Refresh(self):
121 """Download new messages from each connected protocol."""
122@@ -80,6 +126,7 @@
123 # If a protocol doesn't support receive then ignore it.
124 pass
125
126+ @exit_after_idle
127 @dbus.service.method(DBUS_INTERFACE)
128 def ClearIndicators(self):
129 """Indicate that messages have been read.
130@@ -92,8 +139,8 @@
131 service.ClearIndicators()
132 """
133 self.menu_manager.update_unread_count(0)
134- GLib.idle_add(self.mainloop.quit)
135
136+ @exit_after_idle
137 @dbus.service.method(DBUS_INTERFACE,
138 in_signature='sss',
139 out_signature='s',
140@@ -138,6 +185,7 @@
141 if not called:
142 failure('No accounts supporting {} found.'.format(action))
143
144+ @exit_after_idle
145 @dbus.service.method(DBUS_INTERFACE,
146 in_signature='s',
147 out_signature='s',
148@@ -172,6 +220,7 @@
149 if not sent:
150 failure('No send_enabled accounts found.')
151
152+ @exit_after_idle
153 @dbus.service.method(DBUS_INTERFACE,
154 in_signature='sss',
155 out_signature='s',
156@@ -205,6 +254,7 @@
157 failure(message)
158 log.error(message)
159
160+ @exit_after_idle
161 @dbus.service.method(DBUS_INTERFACE,
162 in_signature='sss',
163 out_signature='s',
164@@ -262,6 +312,7 @@
165 failure(message)
166 log.error(message)
167
168+ @exit_after_idle
169 @dbus.service.method(DBUS_INTERFACE, in_signature='s', out_signature='s')
170 def GetFeatures(self, protocol_name):
171 """Returns a list of features supported by service as json string.
172@@ -274,9 +325,9 @@
173 features = json.loads(service.GetFeatures('facebook'))
174 """
175 protocol = protocol_manager.protocols.get(protocol_name)
176- GLib.idle_add(self.mainloop.quit)
177- return json.dumps(protocol.get_features())
178+ return json.dumps(protocol.get_features() if protocol else [])
179
180+ @exit_after_idle
181 @dbus.service.method(DBUS_INTERFACE, in_signature='s', out_signature='s')
182 def URLShorten(self, url):
183 """Shorten a URL.
184@@ -291,7 +342,6 @@
185 service = dbus.Interface(obj, DBUS_INTERFACE)
186 short_url = service.URLShorten(url)
187 """
188- GLib.idle_add(self.mainloop.quit)
189 service_name = self.settings.get_string('urlshorter')
190 log.info('Shortening URL {} with {}'.format(url, service_name))
191 if (lookup.is_shortened(url) or
192@@ -305,7 +355,7 @@
193 log.exception('URL shortening class: {}'.format(service))
194 return url
195
196+ @exit_after_idle
197 @dbus.service.method(DBUS_INTERFACE)
198 def ExpireAvatars(self):
199 Avatar.expire_old_avatars()
200- GLib.idle_add(self.mainloop.quit)
201
202=== modified file 'friends/tests/test_dispatcher.py'
203--- friends/tests/test_dispatcher.py 2013-03-13 04:36:33 +0000
204+++ friends/tests/test_dispatcher.py 2013-03-20 01:26:22 +0000
205@@ -26,7 +26,7 @@
206
207 from dbus.mainloop.glib import DBusGMainLoop
208
209-from friends.service.dispatcher import Dispatcher, STUB
210+from friends.service.dispatcher import Dispatcher, ManageTimers, STUB
211 from friends.tests.mocks import LogMock, mock
212
213
214@@ -61,7 +61,11 @@
215 self.dispatcher.account_manager.get_all.assert_called_once_with()
216 account.protocol.assert_called_once_with('receive')
217
218- self.assertEqual(self.log_mock.empty(), 'Refresh requested\n')
219+ self.assertEqual(self.log_mock.empty(),
220+ 'Clearing 1 shutdown timer(s)...\n'
221+ 'Refresh requested\n'
222+ 'Clearing 0 shutdown timer(s)...\n'
223+ 'Starting new shutdown timer...\n')
224
225 def test_clear_indicators(self):
226 self.dispatcher.menu_manager = mock.Mock()
227@@ -81,7 +85,10 @@
228 'like', '23346356767354626', success=STUB, failure=STUB)
229
230 self.assertEqual(self.log_mock.empty(),
231- '345: like 23346356767354626\n')
232+ 'Clearing 1 shutdown timer(s)...\n'
233+ '345: like 23346356767354626\n'
234+ 'Clearing 0 shutdown timer(s)...\n'
235+ 'Starting new shutdown timer...\n')
236
237 def test_failing_do(self):
238 account = mock.Mock()
239@@ -93,7 +100,10 @@
240 self.assertEqual(account.protocol.call_count, 0)
241
242 self.assertEqual(self.log_mock.empty(),
243- 'Could not find account: 6\n')
244+ 'Clearing 1 shutdown timer(s)...\n'
245+ 'Could not find account: 6\n'
246+ 'Clearing 0 shutdown timer(s)...\n'
247+ 'Starting new shutdown timer...\n')
248
249 def test_send_message(self):
250 account1 = mock.Mock()
251@@ -128,7 +138,10 @@
252 success=STUB, failure=STUB)
253
254 self.assertEqual(self.log_mock.empty(),
255- 'Replying to 2, objid\n')
256+ 'Clearing 1 shutdown timer(s)...\n'
257+ 'Replying to 2, objid\n'
258+ 'Clearing 0 shutdown timer(s)...\n'
259+ 'Starting new shutdown timer...\n')
260
261 def test_send_reply_failed(self):
262 account = mock.Mock()
263@@ -140,8 +153,11 @@
264 self.assertEqual(account.protocol.call_count, 0)
265
266 self.assertEqual(self.log_mock.empty(),
267- 'Replying to 2, objid\n' +
268- 'Could not find account: 2\n')
269+ 'Clearing 1 shutdown timer(s)...\n'
270+ 'Replying to 2, objid\n'
271+ 'Could not find account: 2\n'
272+ 'Clearing 0 shutdown timer(s)...\n'
273+ 'Starting new shutdown timer...\n')
274
275 def test_upload_async(self):
276 account = mock.Mock()
277@@ -166,7 +182,10 @@
278 )
279
280 self.assertEqual(self.log_mock.empty(),
281- 'Uploading file://path/to/image.png to 2\n')
282+ 'Clearing 1 shutdown timer(s)...\n'
283+ 'Uploading file://path/to/image.png to 2\n'
284+ 'Clearing 0 shutdown timer(s)...\n'
285+ 'Starting new shutdown timer...\n')
286
287 def test_get_features(self):
288 self.assertEqual(json.loads(self.dispatcher.GetFeatures('facebook')),
289@@ -205,6 +224,52 @@
290 self.dispatcher.URLShorten(long_url),
291 'short url')
292 lookup_mock.is_shortened.assert_called_once_with(long_url)
293- self.dispatcher.settings.get_boolean.assert_called_once_with('shorten-urls')
294+ self.dispatcher.settings.get_boolean.assert_called_once_with(
295+ 'shorten-urls')
296 lookup_mock.lookup.assert_called_once_with('is.gd')
297- lookup_mock.lookup.return_value.shorten.assert_called_once_with(long_url)
298+ lookup_mock.lookup.return_value.shorten.assert_called_once_with(
299+ long_url)
300+
301+ @mock.patch('friends.service.dispatcher.GLib')
302+ def test_manage_timers_clear(self, glib):
303+ manager = ManageTimers()
304+ manager.timers = {1}
305+ manager.__enter__()
306+ glib.source_remove.assert_called_once_with(1)
307+ manager.timers = {1, 2, 3}
308+ manager.clear_all_timers()
309+ self.assertEqual(glib.source_remove.call_count, 4)
310+
311+ @mock.patch('friends.service.dispatcher.GLib')
312+ def test_manage_timers_set(self, glib):
313+ manager = ManageTimers()
314+ manager.timers = set()
315+ manager.clear_all_timers = mock.Mock()
316+ manager.__exit__()
317+ glib.timeout_add_seconds.assert_called_once_with(30, manager.terminate)
318+ manager.clear_all_timers.assert_called_once_with()
319+ self.assertEqual(len(manager.timers), 1)
320+
321+ @mock.patch('friends.service.dispatcher.persist_model')
322+ @mock.patch('friends.service.dispatcher.threading')
323+ @mock.patch('friends.service.dispatcher.GLib')
324+ def test_manage_timers_terminate(self, glib, thread, persist):
325+ manager = ManageTimers()
326+ manager.timers = set()
327+ thread.activeCount.return_value = 1
328+ manager.terminate()
329+ thread.activeCount.assert_called_once_with()
330+ persist.assert_called_once_with()
331+ glib.idle_add.assert_called_once_with(manager.callback)
332+
333+ @mock.patch('friends.service.dispatcher.persist_model')
334+ @mock.patch('friends.service.dispatcher.threading')
335+ @mock.patch('friends.service.dispatcher.GLib')
336+ def test_manage_timers_dont_kill_threads(self, glib, thread, persist):
337+ manager = ManageTimers()
338+ manager.timers = set()
339+ manager.set_new_timer = mock.Mock()
340+ thread.activeCount.return_value = 10
341+ manager.terminate()
342+ thread.activeCount.assert_called_once_with()
343+ manager.set_new_timer.assert_called_once_with()
344
345=== modified file 'friends/utils/base.py'
346--- friends/utils/base.py 2013-03-19 04:17:58 +0000
347+++ friends/utils/base.py 2013-03-20 01:26:22 +0000
348@@ -92,9 +92,6 @@
349 # publishing new data into the SharedModel.
350 _publish_lock = threading.Lock()
351
352-# Avoid race condition during shut-down
353-_exit_lock = threading.Lock()
354-
355
356 log = logging.getLogger(__name__)
357
358@@ -138,8 +135,6 @@
359
360 class _OperationThread(threading.Thread):
361 """Manage async callbacks, and log subthread exceptions."""
362- # main.py will replace this with a reference to the mainloop.quit method
363- shutdown = lambda: log.error('Failed to exit friends-dispatcher main loop')
364
365 def __init__(self, *args, id=None, success=STUB, failure=STUB, **kws):
366 self._id = id
367@@ -171,13 +166,6 @@
368 log.debug('{} has completed in {:.2f}s, thread exiting.'.format(
369 self._id, elapsed))
370
371- # If this is the last thread to exit, then the refresh is
372- # completed and we should save the model, and then exit.
373- with _exit_lock:
374- if threading.activeCount() < 3:
375- persist_model()
376- GLib.idle_add(self.shutdown)
377-
378
379 class Base:
380 """Parent class for any protocol plugin such as Facebook or Twitter.

Subscribers

People subscribed via source and target branches