##// END OF EJS Templates
pyro: remove db Session when callback finishes to avoid leaving...
dan -
r670:8607f5e9 stable
parent child Browse files
Show More
@@ -1,278 +1,279 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2016 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import json
22 22 import logging
23 23 import urlparse
24 24 import threading
25 25 from BaseHTTPServer import BaseHTTPRequestHandler
26 26 from SocketServer import TCPServer
27 27 from routes.util import URLGenerator
28 28
29 29 import Pyro4
30 30 import pylons
31 31 import rhodecode
32 32
33 33 from rhodecode.model import meta
34 34 from rhodecode.lib import hooks_base
35 35 from rhodecode.lib.utils2 import (
36 36 AttributeDict, safe_str, get_routes_generator_for_server_url)
37 37
38 38
39 39 log = logging.getLogger(__name__)
40 40
41 41
42 42 class HooksHttpHandler(BaseHTTPRequestHandler):
43 43 def do_POST(self):
44 44 method, extras = self._read_request()
45 45 try:
46 46 result = self._call_hook(method, extras)
47 47 except Exception as e:
48 48 result = {
49 49 'exception': e.__class__.__name__,
50 50 'exception_args': e.args
51 51 }
52 52 self._write_response(result)
53 53
54 54 def _read_request(self):
55 55 length = int(self.headers['Content-Length'])
56 56 body = self.rfile.read(length).decode('utf-8')
57 57 data = json.loads(body)
58 58 return data['method'], data['extras']
59 59
60 60 def _write_response(self, result):
61 61 self.send_response(200)
62 62 self.send_header("Content-type", "text/json")
63 63 self.end_headers()
64 64 self.wfile.write(json.dumps(result))
65 65
66 66 def _call_hook(self, method, extras):
67 67 hooks = Hooks()
68 68 try:
69 69 result = getattr(hooks, method)(extras)
70 70 finally:
71 71 meta.Session.remove()
72 72 return result
73 73
74 74 def log_message(self, format, *args):
75 75 """
76 76 This is an overriden method of BaseHTTPRequestHandler which logs using
77 77 logging library instead of writing directly to stderr.
78 78 """
79 79
80 80 message = format % args
81 81
82 82 # TODO: mikhail: add different log levels support
83 83 log.debug(
84 84 "%s - - [%s] %s", self.client_address[0],
85 85 self.log_date_time_string(), message)
86 86
87 87
88 88 class DummyHooksCallbackDaemon(object):
89 89 def __init__(self):
90 90 self.hooks_module = Hooks.__module__
91 91
92 92 def __enter__(self):
93 93 log.debug('Running dummy hooks callback daemon')
94 94 return self
95 95
96 96 def __exit__(self, exc_type, exc_val, exc_tb):
97 97 log.debug('Exiting dummy hooks callback daemon')
98 98
99 99
100 100 class ThreadedHookCallbackDaemon(object):
101 101
102 102 _callback_thread = None
103 103 _daemon = None
104 104 _done = False
105 105
106 106 def __init__(self):
107 107 self._prepare()
108 108
109 109 def __enter__(self):
110 110 self._run()
111 111 return self
112 112
113 113 def __exit__(self, exc_type, exc_val, exc_tb):
114 114 self._stop()
115 115
116 116 def _prepare(self):
117 117 raise NotImplementedError()
118 118
119 119 def _run(self):
120 120 raise NotImplementedError()
121 121
122 122 def _stop(self):
123 123 raise NotImplementedError()
124 124
125 125
126 126 class Pyro4HooksCallbackDaemon(ThreadedHookCallbackDaemon):
127 127 """
128 128 Context manager which will run a callback daemon in a background thread.
129 129 """
130 130
131 131 hooks_uri = None
132 132
133 133 def _prepare(self):
134 134 log.debug("Preparing callback daemon and registering hook object")
135 135 self._daemon = Pyro4.Daemon()
136 136 hooks_interface = Hooks()
137 137 self.hooks_uri = str(self._daemon.register(hooks_interface))
138 138 log.debug("Hooks uri is: %s", self.hooks_uri)
139 139
140 140 def _run(self):
141 141 log.debug("Running event loop of callback daemon in background thread")
142 142 callback_thread = threading.Thread(
143 143 target=self._daemon.requestLoop,
144 144 kwargs={'loopCondition': lambda: not self._done})
145 145 callback_thread.daemon = True
146 146 callback_thread.start()
147 147 self._callback_thread = callback_thread
148 148
149 149 def _stop(self):
150 150 log.debug("Waiting for background thread to finish.")
151 151 self._done = True
152 152 self._callback_thread.join()
153 153 self._daemon.close()
154 154 self._daemon = None
155 155 self._callback_thread = None
156 156
157 157
158 158 class HttpHooksCallbackDaemon(ThreadedHookCallbackDaemon):
159 159 """
160 160 Context manager which will run a callback daemon in a background thread.
161 161 """
162 162
163 163 hooks_uri = None
164 164
165 165 IP_ADDRESS = '127.0.0.1'
166 166
167 167 # From Python docs: Polling reduces our responsiveness to a shutdown
168 168 # request and wastes cpu at all other times.
169 169 POLL_INTERVAL = 0.1
170 170
171 171 def _prepare(self):
172 172 log.debug("Preparing callback daemon and registering hook object")
173 173
174 174 self._done = False
175 175 self._daemon = TCPServer((self.IP_ADDRESS, 0), HooksHttpHandler)
176 176 _, port = self._daemon.server_address
177 177 self.hooks_uri = '{}:{}'.format(self.IP_ADDRESS, port)
178 178
179 179 log.debug("Hooks uri is: %s", self.hooks_uri)
180 180
181 181 def _run(self):
182 182 log.debug("Running event loop of callback daemon in background thread")
183 183 callback_thread = threading.Thread(
184 184 target=self._daemon.serve_forever,
185 185 kwargs={'poll_interval': self.POLL_INTERVAL})
186 186 callback_thread.daemon = True
187 187 callback_thread.start()
188 188 self._callback_thread = callback_thread
189 189
190 190 def _stop(self):
191 191 log.debug("Waiting for background thread to finish.")
192 192 self._daemon.shutdown()
193 193 self._callback_thread.join()
194 194 self._daemon = None
195 195 self._callback_thread = None
196 196
197 197
198 198 def prepare_callback_daemon(extras, protocol=None, use_direct_calls=False):
199 199 callback_daemon = None
200 200 protocol = protocol.lower() if protocol else None
201 201
202 202 if use_direct_calls:
203 203 callback_daemon = DummyHooksCallbackDaemon()
204 204 extras['hooks_module'] = callback_daemon.hooks_module
205 205 else:
206 206 if protocol == 'pyro4':
207 207 callback_daemon = Pyro4HooksCallbackDaemon()
208 208 elif protocol == 'http':
209 209 callback_daemon = HttpHooksCallbackDaemon()
210 210 else:
211 211 log.error('Unsupported callback daemon protocol "%s"', protocol)
212 212 raise Exception('Unsupported callback daemon protocol.')
213 213
214 214 extras['hooks_uri'] = callback_daemon.hooks_uri
215 215 extras['hooks_protocol'] = protocol
216 216
217 217 return callback_daemon, extras
218 218
219 219
220 220 class Hooks(object):
221 221 """
222 222 Exposes the hooks for remote call backs
223 223 """
224 224
225 225 @Pyro4.callback
226 226 def repo_size(self, extras):
227 227 log.debug("Called repo_size of Hooks object")
228 228 return self._call_hook(hooks_base.repo_size, extras)
229 229
230 230 @Pyro4.callback
231 231 def pre_pull(self, extras):
232 232 log.debug("Called pre_pull of Hooks object")
233 233 return self._call_hook(hooks_base.pre_pull, extras)
234 234
235 235 @Pyro4.callback
236 236 def post_pull(self, extras):
237 237 log.debug("Called post_pull of Hooks object")
238 238 return self._call_hook(hooks_base.post_pull, extras)
239 239
240 240 @Pyro4.callback
241 241 def pre_push(self, extras):
242 242 log.debug("Called pre_push of Hooks object")
243 243 return self._call_hook(hooks_base.pre_push, extras)
244 244
245 245 @Pyro4.callback
246 246 def post_push(self, extras):
247 247 log.debug("Called post_push of Hooks object")
248 248 return self._call_hook(hooks_base.post_push, extras)
249 249
250 250 def _call_hook(self, hook, extras):
251 251 extras = AttributeDict(extras)
252 252 pylons_router = get_routes_generator_for_server_url(extras.server_url)
253 253 pylons.url._push_object(pylons_router)
254 254
255 255 try:
256 256 result = hook(extras)
257 257 except Exception as error:
258 258 log.exception('Exception when handling hook %s', hook)
259 259 error_args = error.args
260 260 return {
261 261 'status': 128,
262 262 'output': '',
263 263 'exception': type(error).__name__,
264 264 'exception_args': error_args,
265 265 }
266 266 finally:
267 267 pylons.url._pop_object()
268 meta.Session.remove()
268 269
269 270 return {
270 271 'status': result.status,
271 272 'output': result.output,
272 273 }
273 274
274 275 def __enter__(self):
275 276 return self
276 277
277 278 def __exit__(self, exc_type, exc_val, exc_tb):
278 279 pass
General Comments 0
You need to be logged in to leave comments. Login now