Browse Source

Merge branch 'master' of github.com:tracim/tracim into integration_react_sidebarleft

Skylsmoi 8 years ago
parent
commit
61ec173f10

+ 3 - 0
tracim/tracim/config/app_cfg.py View File

@@ -14,6 +14,7 @@ convert them into boolean, for example, you should use the
14 14
 """
15 15
 import imp
16 16
 import importlib
17
+import os
17 18
 from urllib.parse import urlparse
18 19
 
19 20
 import tg
@@ -22,6 +23,7 @@ from tg.configuration.milestones import environment_loaded
22 23
 
23 24
 from tgext.pluggable import plug
24 25
 from tgext.pluggable import replace_template
26
+from tracim.lib.system import InterruptManager
25 27
 
26 28
 from tracim.lib.utils import lazy_ugettext as l_
27 29
 
@@ -109,6 +111,7 @@ def start_daemons(manager: DaemonsManager):
109 111
         manager.run('mail_sender', MailSenderDaemon)
110 112
 
111 113
 environment_loaded.register(lambda: start_daemons(daemons))
114
+interrupt_manager = InterruptManager(os.getpid(), daemons_manager=daemons)
112 115
 
113 116
 # Note: here are fake translatable strings that allow to translate messages for reset password email content
114 117
 duplicated_email_subject = l_('Password reset request')

+ 27 - 11
tracim/tracim/lib/daemons.py View File

@@ -1,9 +1,8 @@
1 1
 import threading
2
+import collections
2 3
 from configparser import DuplicateSectionError
3
-from wsgiref.simple_server import make_server
4
-import signal
5 4
 
6
-import collections
5
+from wsgiref.simple_server import make_server
7 6
 
8 7
 from radicale import Application as RadicaleApplication
9 8
 from radicale import HTTPServer as BaseRadicaleHTTPServer
@@ -13,16 +12,18 @@ from radicale import config as radicale_config
13 12
 from rq import Connection as RQConnection
14 13
 from rq import Worker as BaseRQWorker
15 14
 from redis import Redis
15
+from rq.dummy import do_nothing
16
+from rq.worker import StopRequested
16 17
 
17 18
 from tracim.lib.base import logger
18 19
 from tracim.lib.exceptions import AlreadyRunningDaemon
19
-from tracim.lib.utils import add_signal_handler
20
+
21
+from tracim.lib.utils import get_rq_queue
20 22
 
21 23
 
22 24
 class DaemonsManager(object):
23 25
     def __init__(self):
24 26
         self._running_daemons = {}
25
-        add_signal_handler(signal.SIGTERM, self.stop_all)
26 27
 
27 28
     def run(self, name: str, daemon_class: object, **kwargs) -> None:
28 29
         """
@@ -61,7 +62,7 @@ class DaemonsManager(object):
61 62
             logger.info(self, 'Stopping daemon with name "{0}": OK'
62 63
                               .format(name))
63 64
 
64
-    def stop_all(self, *args, **kwargs) -> None:
65
+    def stop_all(self) -> None:
65 66
         """
66 67
         Stop all started daemons and wait for them.
67 68
         """
@@ -71,6 +72,10 @@ class DaemonsManager(object):
71 72
             daemon.stop()
72 73
 
73 74
         for name, daemon in self._running_daemons.items():
75
+            logger.info(
76
+                self,
77
+                'Stopping daemon "{0}" waiting confirmation'.format(name),
78
+            )
74 79
             daemon.join()
75 80
             logger.info(self, 'Stopping daemon "{0}" OK'.format(name))
76 81
 
@@ -158,7 +163,12 @@ class MailSenderDaemon(Daemon):
158 163
         pass
159 164
 
160 165
     def stop(self) -> None:
161
-        self.worker.request_stop('TRACIM STOP', None)
166
+        # When _stop_requested at False, tracim.lib.daemons.RQWorker
167
+        # will raise StopRequested exception in worker thread after receive a
168
+        # job.
169
+        self.worker._stop_requested = True
170
+        queue = get_rq_queue('mail_sender')
171
+        queue.enqueue(do_nothing)
162 172
 
163 173
     def run(self) -> None:
164 174
         from tracim.config.app_cfg import CFG
@@ -175,13 +185,19 @@ class MailSenderDaemon(Daemon):
175 185
 
176 186
 class RQWorker(BaseRQWorker):
177 187
     def _install_signal_handlers(self):
178
-        # TODO BS 20170126: RQ WWorker is designed to work in main thread
188
+        # RQ Worker is designed to work in main thread
179 189
         # So we have to disable these signals (we implement server stop in
180
-        # MailSenderDaemon.stop method). When bug
181
-        # https://github.com/tracim/tracim/issues/166 will be fixed, ensure
182
-        # This worker terminate correctly.
190
+        # MailSenderDaemon.stop method).
183 191
         pass
184 192
 
193
+    def dequeue_job_and_maintain_ttl(self, timeout):
194
+        # RQ Worker is designed to work in main thread, so we add behaviour
195
+        # here: if _stop_requested has been set to True, raise the standard way
196
+        # StopRequested exception to stop worker.
197
+        if self._stop_requested:
198
+            raise StopRequested()
199
+        return super().dequeue_job_and_maintain_ttl(timeout)
200
+
185 201
 
186 202
 class RadicaleHTTPSServer(TracimSocketServerMixin, BaseRadicaleHTTPSServer):
187 203
     pass

+ 7 - 11
tracim/tracim/lib/email.py View File

@@ -6,37 +6,33 @@ from email.mime.text import MIMEText
6 6
 
7 7
 import typing
8 8
 from mako.template import Template
9
-from redis import Redis
10
-from rq import Queue
11 9
 from tg.i18n import ugettext as _
12 10
 
13 11
 from tracim.lib.base import logger
14 12
 from tracim.model import User
15 13
 
14
+from tracim.lib.utils import get_rq_queue
15
+
16 16
 
17 17
 def send_email_through(
18
-        send_callable: typing.Callable[[Message], None],
18
+        sendmail_callable: typing.Callable[[Message], None],
19 19
         message: Message,
20 20
 ) -> None:
21 21
     """
22 22
     Send mail encapsulation to send it in async or sync mode.
23 23
     TODO BS 20170126: A global mail/sender management should be a good
24 24
                       thing. Actually, this method is an fast solution.
25
-    :param send_callable: A callable who get message on first parameter
25
+    :param sendmail_callable: A callable who get message on first parameter
26 26
     :param message: The message who have to be sent
27 27
     """
28 28
     from tracim.config.app_cfg import CFG
29 29
     cfg = CFG.get_instance()
30 30
 
31 31
     if cfg.EMAIL_PROCESSING_MODE == CFG.CST.SYNC:
32
-        send_callable(message)
32
+        sendmail_callable(message)
33 33
     elif cfg.EMAIL_PROCESSING_MODE == CFG.CST.ASYNC:
34
-        queue = Queue('mail_sender', connection=Redis(
35
-            host=cfg.EMAIL_SENDER_REDIS_HOST,
36
-            port=cfg.EMAIL_SENDER_REDIS_PORT,
37
-            db=cfg.EMAIL_SENDER_REDIS_DB,
38
-        ))
39
-        queue.enqueue(send_callable, message)
34
+        queue = get_rq_queue('mail_sender')
35
+        queue.enqueue(sendmail_callable, message)
40 36
     else:
41 37
         raise NotImplementedError(
42 38
             'Mail sender processing mode {} is not implemented'.format(

+ 54 - 0
tracim/tracim/lib/system.py View File

@@ -0,0 +1,54 @@
1
+# -*- coding: utf-8 -*-
2
+import os
3
+import signal
4
+
5
+from tracim.lib.daemons import DaemonsManager
6
+
7
+
8
+class InterruptManager(object):
9
+    """
10
+    Manager interruption of tracim components.
11
+
12
+    Stop all tracim daemons, then:
13
+
14
+    With a specific production server like uWSGI, we should use master
15
+    FIFO system to exit properly the program:
16
+    https://github.com/unbit/uwsgi/issues/849. But to be generic, we resend the
17
+    signal after intercept it.
18
+    """
19
+    def __init__(
20
+            self,
21
+            tracim_process_pid: int,
22
+            daemons_manager: DaemonsManager,
23
+    ) -> None:
24
+        """
25
+        :param tracim_process_pid: pid of tracim.
26
+        :param daemons_manager: Tracim daemons manager
27
+        """
28
+        self.daemons_manager = daemons_manager
29
+        self.tracim_process_pid = tracim_process_pid
30
+        self._install_sgnal_handlers()
31
+
32
+    def _install_sgnal_handlers(self) -> None:
33
+        """
34
+        Install signal handler to intercept SIGINT and SIGTERM signals
35
+        """
36
+        signal.signal(signal.SIGTERM, self.stop)
37
+        signal.signal(signal.SIGINT, self.stop)
38
+
39
+    def _remove_signal_handlers(self) -> None:
40
+        """
41
+        Remove installed signals to permit stop of main thread.
42
+        """
43
+        signal.signal(signal.SIGTERM, signal.SIG_DFL)
44
+        signal.signal(signal.SIGINT, signal.SIG_DFL)
45
+
46
+    def stop(self, signum, frame) -> None:
47
+        """
48
+        Run stopping process needed when tracim have to stop.
49
+        :param signum: signal interruption value
50
+        :param frame: frame of signal origin
51
+        """
52
+        self._remove_signal_handlers()
53
+        self.daemons_manager.stop_all()
54
+        os.kill(self.tracim_process_pid, signum)

+ 17 - 14
tracim/tracim/lib/utils.py View File

@@ -13,6 +13,8 @@ from tg.i18n import ugettext
13 13
 from tg.support.registry import StackedObjectProxy
14 14
 from tg.util import LazyString as BaseLazyString
15 15
 from tg.util import lazify
16
+from redis import Redis
17
+from rq import Queue
16 18
 
17 19
 from tracim.lib.base import logger
18 20
 from webob import Response
@@ -63,20 +65,6 @@ def NotImplemented():
63 65
     raise NotImplementedError()
64 66
 
65 67
 
66
-def add_signal_handler(signal_id, handler) -> None:
67
-    """
68
-    Add a callback attached to python signal.
69
-    :param signal_id: signal identifier (eg. signal.SIGTERM)
70
-    :param handler: callback to execute when signal trig
71
-    """
72
-    def _handler(*args, **kwargs):
73
-        handler()
74
-        signal.signal(signal_id, signal.SIG_DFL)
75
-        os.kill(os.getpid(), signal_id)  # Rethrow signal
76
-
77
-    signal.signal(signal_id, _handler)
78
-
79
-
80 68
 class APIWSGIHTTPException(WSGIHTTPException):
81 69
     def json_formatter(self, body, status, title, environ):
82 70
         if self.comment:
@@ -171,3 +159,18 @@ def _lazy_ugettext(text: str):
171 159
         return text
172 160
 
173 161
 lazy_ugettext = lazify(_lazy_ugettext)
162
+
163
+
164
+def get_rq_queue(queue_name: str= 'default') -> Queue:
165
+    """
166
+    :param queue_name: name of queue
167
+    :return: wanted queue
168
+    """
169
+    from tracim.config.app_cfg import CFG
170
+    cfg = CFG.get_instance()
171
+
172
+    return Queue(queue_name, connection=Redis(
173
+        host=cfg.EMAIL_SENDER_REDIS_HOST,
174
+        port=cfg.EMAIL_SENDER_REDIS_PORT,
175
+        db=cfg.EMAIL_SENDER_REDIS_DB,
176
+    ))