diff tincan.py @ 43:0bd9b9ae9998 draft

Make it thread-safe.
author David Barts <n5jrn@me.com>
date Thu, 30 May 2019 08:59:35 -0700
parents 8948020c54fd
children 7261459351fa
line wrap: on
line diff
--- 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):
         """