##// END OF EJS Templates
http-proto: added some more logging.
marcink -
r745:9b5d377a default
parent child Browse files
Show More
@@ -1,619 +1,621 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 2 # Copyright (C) 2014-2019 RhodeCode GmbH
3 3 #
4 4 # This program is free software; you can redistribute it and/or modify
5 5 # it under the terms of the GNU General Public License as published by
6 6 # the Free Software Foundation; either version 3 of the License, or
7 7 # (at your option) any later version.
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 General Public License
15 15 # along with this program; if not, write to the Free Software Foundation,
16 16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17 17
18 18 import os
19 19 import sys
20 20 import base64
21 21 import locale
22 22 import logging
23 23 import uuid
24 24 import wsgiref.util
25 25 import traceback
26 26 import tempfile
27 27 from itertools import chain
28 28
29 29 import simplejson as json
30 30 import msgpack
31 31 from pyramid.config import Configurator
32 32 from pyramid.settings import asbool, aslist
33 33 from pyramid.wsgi import wsgiapp
34 34 from pyramid.compat import configparser
35 35
36 36
37 37 log = logging.getLogger(__name__)
38 38
39 39 # due to Mercurial/glibc2.27 problems we need to detect if locale settings are
40 40 # causing problems and "fix" it in case they do and fallback to LC_ALL = C
41 41
42 42 try:
43 43 locale.setlocale(locale.LC_ALL, '')
44 44 except locale.Error as e:
45 45 log.error(
46 46 'LOCALE ERROR: failed to set LC_ALL, fallback to LC_ALL=C, org error: %s', e)
47 47 os.environ['LC_ALL'] = 'C'
48 48
49 49 import vcsserver
50 50 from vcsserver import remote_wsgi, scm_app, settings, hgpatches
51 51 from vcsserver.git_lfs.app import GIT_LFS_CONTENT_TYPE, GIT_LFS_PROTO_PAT
52 52 from vcsserver.echo_stub import remote_wsgi as remote_wsgi_stub
53 53 from vcsserver.echo_stub.echo_app import EchoApp
54 54 from vcsserver.exceptions import HTTPRepoLocked, HTTPRepoBranchProtected
55 55 from vcsserver.lib.exc_tracking import store_exception
56 56 from vcsserver.server import VcsServer
57 57
58 58 try:
59 59 from vcsserver.git import GitFactory, GitRemote
60 60 except ImportError:
61 61 GitFactory = None
62 62 GitRemote = None
63 63
64 64 try:
65 65 from vcsserver.hg import MercurialFactory, HgRemote
66 66 except ImportError:
67 67 MercurialFactory = None
68 68 HgRemote = None
69 69
70 70 try:
71 71 from vcsserver.svn import SubversionFactory, SvnRemote
72 72 except ImportError:
73 73 SubversionFactory = None
74 74 SvnRemote = None
75 75
76 76
77 77 def _is_request_chunked(environ):
78 78 stream = environ.get('HTTP_TRANSFER_ENCODING', '') == 'chunked'
79 79 return stream
80 80
81 81
82 82 def _int_setting(settings, name, default):
83 83 settings[name] = int(settings.get(name, default))
84 84 return settings[name]
85 85
86 86
87 87 def _bool_setting(settings, name, default):
88 88 input_val = settings.get(name, default)
89 89 if isinstance(input_val, unicode):
90 90 input_val = input_val.encode('utf8')
91 91 settings[name] = asbool(input_val)
92 92 return settings[name]
93 93
94 94
95 95 def _list_setting(settings, name, default):
96 96 raw_value = settings.get(name, default)
97 97
98 98 # Otherwise we assume it uses pyramids space/newline separation.
99 99 settings[name] = aslist(raw_value)
100 100 return settings[name]
101 101
102 102
103 103 def _string_setting(settings, name, default, lower=True, default_when_empty=False):
104 104 value = settings.get(name, default)
105 105
106 106 if default_when_empty and not value:
107 107 # use default value when value is empty
108 108 value = default
109 109
110 110 if lower:
111 111 value = value.lower()
112 112 settings[name] = value
113 113 return settings[name]
114 114
115 115
116 116 class VCS(object):
117 117 def __init__(self, locale=None, cache_config=None):
118 118 self.locale = locale
119 119 self.cache_config = cache_config
120 120 self._configure_locale()
121 121
122 122 if GitFactory and GitRemote:
123 123 git_factory = GitFactory()
124 124 self._git_remote = GitRemote(git_factory)
125 125 else:
126 126 log.info("Git client import failed")
127 127
128 128 if MercurialFactory and HgRemote:
129 129 hg_factory = MercurialFactory()
130 130 self._hg_remote = HgRemote(hg_factory)
131 131 else:
132 132 log.info("Mercurial client import failed")
133 133
134 134 if SubversionFactory and SvnRemote:
135 135 svn_factory = SubversionFactory()
136 136
137 137 # hg factory is used for svn url validation
138 138 hg_factory = MercurialFactory()
139 139 self._svn_remote = SvnRemote(svn_factory, hg_factory=hg_factory)
140 140 else:
141 141 log.info("Subversion client import failed")
142 142
143 143 self._vcsserver = VcsServer()
144 144
145 145 def _configure_locale(self):
146 146 if self.locale:
147 147 log.info('Settings locale: `LC_ALL` to %s', self.locale)
148 148 else:
149 149 log.info(
150 150 'Configuring locale subsystem based on environment variables')
151 151 try:
152 152 # If self.locale is the empty string, then the locale
153 153 # module will use the environment variables. See the
154 154 # documentation of the package `locale`.
155 155 locale.setlocale(locale.LC_ALL, self.locale)
156 156
157 157 language_code, encoding = locale.getlocale()
158 158 log.info(
159 159 'Locale set to language code "%s" with encoding "%s".',
160 160 language_code, encoding)
161 161 except locale.Error:
162 162 log.exception(
163 163 'Cannot set locale, not configuring the locale system')
164 164
165 165
166 166 class WsgiProxy(object):
167 167 def __init__(self, wsgi):
168 168 self.wsgi = wsgi
169 169
170 170 def __call__(self, environ, start_response):
171 171 input_data = environ['wsgi.input'].read()
172 172 input_data = msgpack.unpackb(input_data)
173 173
174 174 error = None
175 175 try:
176 176 data, status, headers = self.wsgi.handle(
177 177 input_data['environment'], input_data['input_data'],
178 178 *input_data['args'], **input_data['kwargs'])
179 179 except Exception as e:
180 180 data, status, headers = [], None, None
181 181 error = {
182 182 'message': str(e),
183 183 '_vcs_kind': getattr(e, '_vcs_kind', None)
184 184 }
185 185
186 186 start_response(200, {})
187 187 return self._iterator(error, status, headers, data)
188 188
189 189 def _iterator(self, error, status, headers, data):
190 190 initial_data = [
191 191 error,
192 192 status,
193 193 headers,
194 194 ]
195 195
196 196 for d in chain(initial_data, data):
197 197 yield msgpack.packb(d)
198 198
199 199
200 200 def not_found(request):
201 201 return {'status': '404 NOT FOUND'}
202 202
203 203
204 204 class VCSViewPredicate(object):
205 205 def __init__(self, val, config):
206 206 self.remotes = val
207 207
208 208 def text(self):
209 209 return 'vcs view method = %s' % (self.remotes.keys(),)
210 210
211 211 phash = text
212 212
213 213 def __call__(self, context, request):
214 214 """
215 215 View predicate that returns true if given backend is supported by
216 216 defined remotes.
217 217 """
218 218 backend = request.matchdict.get('backend')
219 219 return backend in self.remotes
220 220
221 221
222 222 class HTTPApplication(object):
223 223 ALLOWED_EXCEPTIONS = ('KeyError', 'URLError')
224 224
225 225 remote_wsgi = remote_wsgi
226 226 _use_echo_app = False
227 227
228 228 def __init__(self, settings=None, global_config=None):
229 229 self._sanitize_settings_and_apply_defaults(settings)
230 230
231 231 self.config = Configurator(settings=settings)
232 232 self.global_config = global_config
233 233 self.config.include('vcsserver.lib.rc_cache')
234 234
235 235 settings_locale = settings.get('locale', '') or 'en_US.UTF-8'
236 236 vcs = VCS(locale=settings_locale, cache_config=settings)
237 237 self._remotes = {
238 238 'hg': vcs._hg_remote,
239 239 'git': vcs._git_remote,
240 240 'svn': vcs._svn_remote,
241 241 'server': vcs._vcsserver,
242 242 }
243 243 if settings.get('dev.use_echo_app', 'false').lower() == 'true':
244 244 self._use_echo_app = True
245 245 log.warning("Using EchoApp for VCS operations.")
246 246 self.remote_wsgi = remote_wsgi_stub
247 247
248 248 self._configure_settings(global_config, settings)
249 249 self._configure()
250 250
251 251 def _configure_settings(self, global_config, app_settings):
252 252 """
253 253 Configure the settings module.
254 254 """
255 255 settings_merged = global_config.copy()
256 256 settings_merged.update(app_settings)
257 257
258 258 git_path = app_settings.get('git_path', None)
259 259 if git_path:
260 260 settings.GIT_EXECUTABLE = git_path
261 261 binary_dir = app_settings.get('core.binary_dir', None)
262 262 if binary_dir:
263 263 settings.BINARY_DIR = binary_dir
264 264
265 265 # Store the settings to make them available to other modules.
266 266 vcsserver.PYRAMID_SETTINGS = settings_merged
267 267 vcsserver.CONFIG = settings_merged
268 268
269 269 def _sanitize_settings_and_apply_defaults(self, settings):
270 270 temp_store = tempfile.gettempdir()
271 271 default_cache_dir = os.path.join(temp_store, 'rc_cache')
272 272
273 273 # save default, cache dir, and use it for all backends later.
274 274 default_cache_dir = _string_setting(
275 275 settings,
276 276 'cache_dir',
277 277 default_cache_dir, lower=False, default_when_empty=True)
278 278
279 279 # ensure we have our dir created
280 280 if not os.path.isdir(default_cache_dir):
281 281 os.makedirs(default_cache_dir, mode=0o755)
282 282
283 283 # exception store cache
284 284 _string_setting(
285 285 settings,
286 286 'exception_tracker.store_path',
287 287 temp_store, lower=False, default_when_empty=True)
288 288
289 289 # repo_object cache
290 290 _string_setting(
291 291 settings,
292 292 'rc_cache.repo_object.backend',
293 293 'dogpile.cache.rc.memory_lru')
294 294 _int_setting(
295 295 settings,
296 296 'rc_cache.repo_object.expiration_time',
297 297 300)
298 298 _int_setting(
299 299 settings,
300 300 'rc_cache.repo_object.max_size',
301 301 1024)
302 302
303 303 def _configure(self):
304 304 self.config.add_renderer(name='msgpack', factory=self._msgpack_renderer_factory)
305 305
306 306 self.config.add_route('service', '/_service')
307 307 self.config.add_route('status', '/status')
308 308 self.config.add_route('hg_proxy', '/proxy/hg')
309 309 self.config.add_route('git_proxy', '/proxy/git')
310 310 self.config.add_route('vcs', '/{backend}')
311 311 self.config.add_route('stream_git', '/stream/git/*repo_name')
312 312 self.config.add_route('stream_hg', '/stream/hg/*repo_name')
313 313
314 314 self.config.add_view(self.status_view, route_name='status', renderer='json')
315 315 self.config.add_view(self.service_view, route_name='service', renderer='msgpack')
316 316
317 317 self.config.add_view(self.hg_proxy(), route_name='hg_proxy')
318 318 self.config.add_view(self.git_proxy(), route_name='git_proxy')
319 319 self.config.add_view(self.vcs_view, route_name='vcs', renderer='msgpack',
320 320 vcs_view=self._remotes)
321 321
322 322 self.config.add_view(self.hg_stream(), route_name='stream_hg')
323 323 self.config.add_view(self.git_stream(), route_name='stream_git')
324 324
325 325 self.config.add_view_predicate('vcs_view', VCSViewPredicate)
326 326
327 327 self.config.add_notfound_view(not_found, renderer='json')
328 328
329 329 self.config.add_view(self.handle_vcs_exception, context=Exception)
330 330
331 331 self.config.add_tween(
332 332 'vcsserver.tweens.request_wrapper.RequestWrapperTween',
333 333 )
334 334
335 335 def wsgi_app(self):
336 336 return self.config.make_wsgi_app()
337 337
338 338 def vcs_view(self, request):
339 339 remote = self._remotes[request.matchdict['backend']]
340 340 payload = msgpack.unpackb(request.body, use_list=True)
341 341 method = payload.get('method')
342 342 params = payload.get('params')
343 343 wire = params.get('wire')
344 344 args = params.get('args')
345 345 kwargs = params.get('kwargs')
346 346 context_uid = None
347 347
348 348 if wire:
349 349 try:
350 350 wire['context'] = context_uid = uuid.UUID(wire['context'])
351 351 except KeyError:
352 352 pass
353 353 args.insert(0, wire)
354 354
355 355 # NOTE(marcink): trading complexity for slight performance
356 356 if log.isEnabledFor(logging.DEBUG):
357 357 no_args_methods = [
358 358 'archive_repo'
359 359 ]
360 360 if method in no_args_methods:
361 361 call_args = ''
362 362 else:
363 363 call_args = args[1:]
364 log.debug('method called:%s with args:%s kwargs:%s context_uid: %s',
365 method, call_args, kwargs, context_uid)
364
365 repo_state_uid = wire.get('repo_state_uid') if wire else None
366 log.debug('method called:%s with args:%s kwargs:%s context_uid: %s, repo_state_uid:%s',
367 method, call_args, kwargs, context_uid, repo_state_uid)
366 368
367 369 try:
368 370 resp = getattr(remote, method)(*args, **kwargs)
369 371 except Exception as e:
370 372 exc_info = list(sys.exc_info())
371 373 exc_type, exc_value, exc_traceback = exc_info
372 374
373 375 org_exc = getattr(e, '_org_exc', None)
374 376 org_exc_name = None
375 377 org_exc_tb = ''
376 378 if org_exc:
377 379 org_exc_name = org_exc.__class__.__name__
378 380 org_exc_tb = getattr(e, '_org_exc_tb', '')
379 381 # replace our "faked" exception with our org
380 382 exc_info[0] = org_exc.__class__
381 383 exc_info[1] = org_exc
382 384
383 385 store_exception(id(exc_info), exc_info)
384 386
385 387 tb_info = ''.join(
386 388 traceback.format_exception(exc_type, exc_value, exc_traceback))
387 389
388 390 type_ = e.__class__.__name__
389 391 if type_ not in self.ALLOWED_EXCEPTIONS:
390 392 type_ = None
391 393
392 394 resp = {
393 395 'id': payload.get('id'),
394 396 'error': {
395 397 'message': e.message,
396 398 'traceback': tb_info,
397 399 'org_exc': org_exc_name,
398 400 'org_exc_tb': org_exc_tb,
399 401 'type': type_
400 402 }
401 403 }
402 404 try:
403 405 resp['error']['_vcs_kind'] = getattr(e, '_vcs_kind', None)
404 406 except AttributeError:
405 407 pass
406 408 else:
407 409 resp = {
408 410 'id': payload.get('id'),
409 411 'result': resp
410 412 }
411 413
412 414 return resp
413 415
414 416 def status_view(self, request):
415 417 import vcsserver
416 418 return {'status': 'OK', 'vcsserver_version': vcsserver.__version__,
417 419 'pid': os.getpid()}
418 420
419 421 def service_view(self, request):
420 422 import vcsserver
421 423
422 424 payload = msgpack.unpackb(request.body, use_list=True)
423 425
424 426 try:
425 427 path = self.global_config['__file__']
426 428 config = configparser.ConfigParser()
427 429 config.read(path)
428 430 parsed_ini = config
429 431 if parsed_ini.has_section('server:main'):
430 432 parsed_ini = dict(parsed_ini.items('server:main'))
431 433 except Exception:
432 434 log.exception('Failed to read .ini file for display')
433 435 parsed_ini = {}
434 436
435 437 resp = {
436 438 'id': payload.get('id'),
437 439 'result': dict(
438 440 version=vcsserver.__version__,
439 441 config=parsed_ini,
440 442 payload=payload,
441 443 )
442 444 }
443 445 return resp
444 446
445 447 def _msgpack_renderer_factory(self, info):
446 448 def _render(value, system):
447 449 request = system.get('request')
448 450 if request is not None:
449 451 response = request.response
450 452 ct = response.content_type
451 453 if ct == response.default_content_type:
452 454 response.content_type = 'application/x-msgpack'
453 455 return msgpack.packb(value)
454 456 return _render
455 457
456 458 def set_env_from_config(self, environ, config):
457 459 dict_conf = {}
458 460 try:
459 461 for elem in config:
460 462 if elem[0] == 'rhodecode':
461 463 dict_conf = json.loads(elem[2])
462 464 break
463 465 except Exception:
464 466 log.exception('Failed to fetch SCM CONFIG')
465 467 return
466 468
467 469 username = dict_conf.get('username')
468 470 if username:
469 471 environ['REMOTE_USER'] = username
470 472 # mercurial specific, some extension api rely on this
471 473 environ['HGUSER'] = username
472 474
473 475 ip = dict_conf.get('ip')
474 476 if ip:
475 477 environ['REMOTE_HOST'] = ip
476 478
477 479 if _is_request_chunked(environ):
478 480 # set the compatibility flag for webob
479 481 environ['wsgi.input_terminated'] = True
480 482
481 483 def hg_proxy(self):
482 484 @wsgiapp
483 485 def _hg_proxy(environ, start_response):
484 486 app = WsgiProxy(self.remote_wsgi.HgRemoteWsgi())
485 487 return app(environ, start_response)
486 488 return _hg_proxy
487 489
488 490 def git_proxy(self):
489 491 @wsgiapp
490 492 def _git_proxy(environ, start_response):
491 493 app = WsgiProxy(self.remote_wsgi.GitRemoteWsgi())
492 494 return app(environ, start_response)
493 495 return _git_proxy
494 496
495 497 def hg_stream(self):
496 498 if self._use_echo_app:
497 499 @wsgiapp
498 500 def _hg_stream(environ, start_response):
499 501 app = EchoApp('fake_path', 'fake_name', None)
500 502 return app(environ, start_response)
501 503 return _hg_stream
502 504 else:
503 505 @wsgiapp
504 506 def _hg_stream(environ, start_response):
505 507 log.debug('http-app: handling hg stream')
506 508 repo_path = environ['HTTP_X_RC_REPO_PATH']
507 509 repo_name = environ['HTTP_X_RC_REPO_NAME']
508 510 packed_config = base64.b64decode(
509 511 environ['HTTP_X_RC_REPO_CONFIG'])
510 512 config = msgpack.unpackb(packed_config)
511 513 app = scm_app.create_hg_wsgi_app(
512 514 repo_path, repo_name, config)
513 515
514 516 # Consistent path information for hgweb
515 517 environ['PATH_INFO'] = environ['HTTP_X_RC_PATH_INFO']
516 518 environ['REPO_NAME'] = repo_name
517 519 self.set_env_from_config(environ, config)
518 520
519 521 log.debug('http-app: starting app handler '
520 522 'with %s and process request', app)
521 523 return app(environ, ResponseFilter(start_response))
522 524 return _hg_stream
523 525
524 526 def git_stream(self):
525 527 if self._use_echo_app:
526 528 @wsgiapp
527 529 def _git_stream(environ, start_response):
528 530 app = EchoApp('fake_path', 'fake_name', None)
529 531 return app(environ, start_response)
530 532 return _git_stream
531 533 else:
532 534 @wsgiapp
533 535 def _git_stream(environ, start_response):
534 536 log.debug('http-app: handling git stream')
535 537 repo_path = environ['HTTP_X_RC_REPO_PATH']
536 538 repo_name = environ['HTTP_X_RC_REPO_NAME']
537 539 packed_config = base64.b64decode(
538 540 environ['HTTP_X_RC_REPO_CONFIG'])
539 541 config = msgpack.unpackb(packed_config)
540 542
541 543 environ['PATH_INFO'] = environ['HTTP_X_RC_PATH_INFO']
542 544 self.set_env_from_config(environ, config)
543 545
544 546 content_type = environ.get('CONTENT_TYPE', '')
545 547
546 548 path = environ['PATH_INFO']
547 549 is_lfs_request = GIT_LFS_CONTENT_TYPE in content_type
548 550 log.debug(
549 551 'LFS: Detecting if request `%s` is LFS server path based '
550 552 'on content type:`%s`, is_lfs:%s',
551 553 path, content_type, is_lfs_request)
552 554
553 555 if not is_lfs_request:
554 556 # fallback detection by path
555 557 if GIT_LFS_PROTO_PAT.match(path):
556 558 is_lfs_request = True
557 559 log.debug(
558 560 'LFS: fallback detection by path of: `%s`, is_lfs:%s',
559 561 path, is_lfs_request)
560 562
561 563 if is_lfs_request:
562 564 app = scm_app.create_git_lfs_wsgi_app(
563 565 repo_path, repo_name, config)
564 566 else:
565 567 app = scm_app.create_git_wsgi_app(
566 568 repo_path, repo_name, config)
567 569
568 570 log.debug('http-app: starting app handler '
569 571 'with %s and process request', app)
570 572
571 573 return app(environ, start_response)
572 574
573 575 return _git_stream
574 576
575 577 def handle_vcs_exception(self, exception, request):
576 578 _vcs_kind = getattr(exception, '_vcs_kind', '')
577 579 if _vcs_kind == 'repo_locked':
578 580 # Get custom repo-locked status code if present.
579 581 status_code = request.headers.get('X-RC-Locked-Status-Code')
580 582 return HTTPRepoLocked(
581 583 title=exception.message, status_code=status_code)
582 584
583 585 elif _vcs_kind == 'repo_branch_protected':
584 586 # Get custom repo-branch-protected status code if present.
585 587 return HTTPRepoBranchProtected(title=exception.message)
586 588
587 589 exc_info = request.exc_info
588 590 store_exception(id(exc_info), exc_info)
589 591
590 592 traceback_info = 'unavailable'
591 593 if request.exc_info:
592 594 exc_type, exc_value, exc_tb = request.exc_info
593 595 traceback_info = ''.join(traceback.format_exception(exc_type, exc_value, exc_tb))
594 596
595 597 log.error(
596 598 'error occurred handling this request for path: %s, \n tb: %s',
597 599 request.path, traceback_info)
598 600 raise exception
599 601
600 602
601 603 class ResponseFilter(object):
602 604
603 605 def __init__(self, start_response):
604 606 self._start_response = start_response
605 607
606 608 def __call__(self, status, response_headers, exc_info=None):
607 609 headers = tuple(
608 610 (h, v) for h, v in response_headers
609 611 if not wsgiref.util.is_hop_by_hop(h))
610 612 return self._start_response(status, headers, exc_info)
611 613
612 614
613 615 def main(global_config, **settings):
614 616 if MercurialFactory:
615 617 hgpatches.patch_largefiles_capabilities()
616 618 hgpatches.patch_subrepo_type_mapping()
617 619
618 620 app = HTTPApplication(settings=settings, global_config=global_config)
619 621 return app.wsgi_app()
@@ -1,149 +1,150 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 2 # Copyright (C) 2014-2019 RhodeCode GmbH
3 3 #
4 4 # This program is free software; you can redistribute it and/or modify
5 5 # it under the terms of the GNU General Public License as published by
6 6 # the Free Software Foundation; either version 3 of the License, or
7 7 # (at your option) any later version.
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 General Public License
15 15 # along with this program; if not, write to the Free Software Foundation,
16 16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17 17
18 18 import os
19 19 import logging
20 20 import functools
21 21
22 22 from dogpile.cache import CacheRegion
23 23 from dogpile.cache.util import compat
24 24
25 25 from vcsserver.utils import safe_str, sha1
26 26
27 27
28 28 log = logging.getLogger(__name__)
29 29
30 30
31 31 class RhodeCodeCacheRegion(CacheRegion):
32 32
33 33 def conditional_cache_on_arguments(
34 34 self, namespace=None,
35 35 expiration_time=None,
36 36 should_cache_fn=None,
37 37 to_str=compat.string_type,
38 38 function_key_generator=None,
39 39 condition=True):
40 40 """
41 41 Custom conditional decorator, that will not touch any dogpile internals if
42 42 condition isn't meet. This works a bit different than should_cache_fn
43 43 And it's faster in cases we don't ever want to compute cached values
44 44 """
45 45 expiration_time_is_callable = compat.callable(expiration_time)
46 46
47 47 if function_key_generator is None:
48 48 function_key_generator = self.function_key_generator
49 49
50 50 def decorator(fn):
51 51 if to_str is compat.string_type:
52 52 # backwards compatible
53 53 key_generator = function_key_generator(namespace, fn)
54 54 else:
55 55 key_generator = function_key_generator(namespace, fn, to_str=to_str)
56 56
57 57 @functools.wraps(fn)
58 58 def decorate(*arg, **kw):
59 59 key = key_generator(*arg, **kw)
60 60
61 61 @functools.wraps(fn)
62 62 def creator():
63 log.debug('Calling cached fn:%s', fn)
63 64 return fn(*arg, **kw)
64 65
65 66 if not condition:
66 67 return creator()
67 68
68 69 timeout = expiration_time() if expiration_time_is_callable \
69 70 else expiration_time
70 71
71 72 return self.get_or_create(key, creator, timeout, should_cache_fn)
72 73
73 74 def invalidate(*arg, **kw):
74 75 key = key_generator(*arg, **kw)
75 76 self.delete(key)
76 77
77 78 def set_(value, *arg, **kw):
78 79 key = key_generator(*arg, **kw)
79 80 self.set(key, value)
80 81
81 82 def get(*arg, **kw):
82 83 key = key_generator(*arg, **kw)
83 84 return self.get(key)
84 85
85 86 def refresh(*arg, **kw):
86 87 key = key_generator(*arg, **kw)
87 88 value = fn(*arg, **kw)
88 89 self.set(key, value)
89 90 return value
90 91
91 92 decorate.set = set_
92 93 decorate.invalidate = invalidate
93 94 decorate.refresh = refresh
94 95 decorate.get = get
95 96 decorate.original = fn
96 97 decorate.key_generator = key_generator
97 98 decorate.__wrapped__ = fn
98 99
99 100 return decorate
100 101
101 102 return decorator
102 103
103 104
104 105 def make_region(*arg, **kw):
105 106 return RhodeCodeCacheRegion(*arg, **kw)
106 107
107 108
108 109 def get_default_cache_settings(settings, prefixes=None):
109 110 prefixes = prefixes or []
110 111 cache_settings = {}
111 112 for key in settings.keys():
112 113 for prefix in prefixes:
113 114 if key.startswith(prefix):
114 115 name = key.split(prefix)[1].strip()
115 116 val = settings[key]
116 117 if isinstance(val, compat.string_types):
117 118 val = val.strip()
118 119 cache_settings[name] = val
119 120 return cache_settings
120 121
121 122
122 123 def compute_key_from_params(*args):
123 124 """
124 125 Helper to compute key from given params to be used in cache manager
125 126 """
126 127 return sha1("_".join(map(safe_str, args)))
127 128
128 129
129 130 def backend_key_generator(backend):
130 131 """
131 132 Special wrapper that also sends over the backend to the key generator
132 133 """
133 134 def wrapper(namespace, fn):
134 135 return key_generator(backend, namespace, fn)
135 136 return wrapper
136 137
137 138
138 139 def key_generator(backend, namespace, fn):
139 140 fname = fn.__name__
140 141
141 142 def generate_key(*args):
142 143 backend_prefix = getattr(backend, 'key_prefix', None) or 'backend_prefix'
143 144 namespace_pref = namespace or 'default_namespace'
144 145 arg_key = compute_key_from_params(*args)
145 146 final_key = "{}:{}:{}_{}".format(backend_prefix, namespace_pref, fname, arg_key)
146 147
147 148 return final_key
148 149
149 150 return generate_key
General Comments 0
You need to be logged in to leave comments. Login now