# HG changeset patch # User David Barts # Date 1559231975 25200 # Node ID 0bd9b9ae99984b3e430087287a9020497b9247a5 # Parent 8948020c54fd6f8f20c9aa15e66652b83170e7f4 Make it thread-safe. diff -r 8948020c54fd -r 0bd9b9ae9998 tincan.py --- a/tincan.py Wed May 29 16:51:33 2019 -0700 +++ b/tincan.py Thu May 30 08:59:35 2019 -0700 @@ -17,6 +17,7 @@ import py_compile from stat import S_ISDIR, S_ISREG from string import whitespace +from threading import Lock import time import traceback import urllib @@ -356,17 +357,20 @@ raise ValueError("file does not end in {0}".format(_IEXTEN)) # Using a cache is likely to help efficiency a lot, since many pages -# will typically #load the same standard stuff. +# will typically #load the same standard stuff. Except if we're +# multithreading, then we want each page's templates to be private. _tcache = {} -def _get_template(name, direct, coding): +def _get_template_cache(name, direct, coding): aname = os.path.abspath(os.path.join(direct, name)) if aname not in _tcache: tmpl = ChameleonTemplate(name=name, lookup=[direct], encoding=coding) - tmpl.prepare() assert aname == tmpl.filename _tcache[aname] = tmpl return _tcache[aname] +def _get_template_nocache(name, direct, coding): + return ChameleonTemplate(name=name, lookup=[direct], encoding=coding) + # R o u t e s # # Represents a route in TinCan. Our launcher creates these on-the-fly based @@ -381,7 +385,36 @@ _FORIG = "tincan.origin" _FTYPE = "tincan.iserror" -class _TinCanStaticRoute(object): +class _TinCanBaseRoute(object): + """ + The base class for all NON ERROR routes. Error routes are just a little + bit different. + """ + def __init__(self, launcher, name, subdir): + global _get_template_cache, _get_template_nocache + if launcher.multithread: + self.lock = Lock() + self.get_template = _get_template_nocache + else: + self.lock = _DummyLock() + self.get_template = _get_template_cache + + def urljoin(self, *args): + """ + Normalize a parsed-out URL fragment. + """ + args = list(args) + if args[0] == '/': + args[0] = '' + return '/'.join(args) + + def launch(self): + raise NotImplementedError("This must be overridden.") + + def __call__(self): + raise NotImplementedError("This must be overridden.") + +class _TinCanStaticRoute(_TinCanBaseRoute): """ A route to a static file. These are useful for test servers. For production servers, one is better off using a real web server and @@ -390,9 +423,10 @@ directly because it is undocumented and thus subject to change). """ def __init__(self, launcher, name, subdir): + super().__init__(launcher, name, subdir) self._app = launcher.app self._fspath = os.path.join(launcher.fsroot, *subdir, name) - self._urlpath = self._urljoin(launcher.urlroot, *subdir, name) + self._urlpath = self.urljoin(launcher.urlroot, *subdir, name) self._type = mimetypes.guess_type(name)[0] if self._type is None: self._type = "application/octet-stream" @@ -406,12 +440,6 @@ print("adding static route:", self._urlpath) # debug self._app.route(self._urlpath, 'GET', self) - def _urljoin(self, *args): - args = list(args) - if args[0] == '/': - args[0] = '' - return '/'.join(args) - def _parse_date(self, ims): """ Parse rfc1123, rfc850 and asctime timestamps and return UTC epoch. @@ -460,11 +488,12 @@ custom code-behind, only two variables are available to your template: request (bottle.Request) and error (bottle.HTTPError). """ - def __init__(self, template, loads, klass): + def __init__(self, template, loads, klass, lock): self._template = template self._template.prepare() self._loads = loads self._class = klass + self.lock = lock def __call__(self, e): bottle.request.environ[_FTYPE] = True @@ -473,7 +502,8 @@ obj.handle() tvars = self._loads.copy() tvars.update(obj.export()) - return self._template.render(tvars).lstrip('\n') + with self.lock: + return self._template.render(tvars).lstrip('\n') except bottle.HTTPResponse as e: return e except Exception as e: @@ -485,17 +515,18 @@ # ignored. raise bottle.HTTPError(status=500, exception=e) -class _TinCanRoute(object): +class _TinCanRoute(_TinCanBaseRoute): """ A route created by the TinCan launcher. """ def __init__(self, launcher, name, subdir): + super().__init__(launcher, name, subdir) self._fsroot = launcher.fsroot self._urlroot = launcher.urlroot self._name = name self._python = name + _PEXTEN self._fspath = os.path.join(launcher.fsroot, *subdir, name + _TEXTEN) - self._urlpath = self._urljoin(launcher.urlroot, *subdir, name + _TEXTEN) + self._urlpath = self.urljoin(launcher.urlroot, *subdir, name + _TEXTEN) self._origin = self._urlpath self._subdir = subdir self._seen = set() @@ -572,7 +603,7 @@ else: fdir = os.path.join(self._fsroot, *self._subdir) try: - tmpl = _get_template(load.fname, fdir, self._encoding) + tmpl = self.get_template(load.fname, fdir, self._encoding) except Exception as e: raise TinCanError("{0}: bad #load: {1!s}".format(self._urlpath, e)) from e self._loads[load.vname] = tmpl.tpl @@ -603,7 +634,7 @@ errors = range(_ERRMIN, _ERRMAX+1) route = _TinCanErrorRoute( ChameleonTemplate(source=self._template.body, encoding=self._encoding), - self._loads, self._class) + self._loads, self._class, self.lock) for error in errors: if error < _ERRMIN or error > _ERRMAX: raise TinCanError("{0}: bad #errors code".format(self._urlpath)) @@ -700,13 +731,7 @@ self._subdir = rlist self._python = name + _PEXTEN self._fspath = os.path.join(self._fsroot, *self._subdir, rname) - self._urlpath = '/' + self._urljoin(*self._subdir, rname) - - def _urljoin(self, *args): - args = list(args) - if args[0] == '/': - args[0] = '' - return '/'.join(args) + self._urlpath = '/' + self.urljoin(*self._subdir, rname) def __call__(self): """ @@ -718,7 +743,8 @@ obj.handle() tvars = self._loads.copy() tvars.update(obj.export()) - return self._body.render(tvars).lstrip('\n') + with self.lock: + return self._body.render(tvars).lstrip('\n') except ForwardException as fwd: target = fwd.target except bottle.HTTPResponse as e: @@ -749,6 +775,25 @@ environ['route.url_args'] = args return route.call(**args) +# M u t e x +# +# A dummy lock class, which is what we use if we don't need locking. + +class _DummyLock(object): + def acquire(self, blocking=True, timeout=-1): + pass + + def release(self): + pass + + def __enter__(self): + self.acquire() + return self + + def __exit__(self, exc_type, exc_value, traceback): + self.release() + return False + # L a u n c h e r _WINF = "WEB-INF" @@ -760,7 +805,7 @@ """ Helper class for launching webapps. """ - def __init__(self, fsroot, urlroot, logger): + def __init__(self, fsroot, urlroot, logger, multithread=True): """ Lightweight constructor. The real action happens in .launch() below. """ @@ -772,6 +817,7 @@ self.debug = False self.encoding = ENCODING self.static = False + self.multithread = multithread def launch(self): """