Changeset 738

Show
Ignore:
Timestamp:
07/06/07 20:15:22 (2 years ago)
Author:
robin
Message:

* wsgienviron, by default, caches certain request processing values globaly

for performance reasons. This change set makes wsgienviron thread safe by:

  • Providing a means to use an arbitrary custom instance to hold all the
    cache variables.
  • Adding a threadlocal context to the connection_started api
  • Makes httpservice and asycamore.httpservicecontext.HTTPServiceContext use
    the above facilities to make the default server use wsgienviron in a
    thread safe manner.
Files:

Legend:

Unmodified
Added
Removed
Modified
Copied
Moved
  • asycamore/trunk/asycamore/asycwsgi.py

    r736 r738  
    101101        else: 
    102102            default_app = wsgiservicectxcls.default_app 
    103         def connection_started(model, o, event): 
     103        def connection_started(model, o, event, threadlocal=None): 
    104104            return wsgiservicectxcls.connection_started( 
    105                     model, o, event, default_app) 
     105                    model, o, event, default_app, 
     106                    threadlocal=threadlocal 
     107                    ) 
    106108 
    107109        connection_factory = httpserviceconn.connection_factory 
  • asycamore/trunk/asycamore/commandline.py

    r733 r738  
    55from os.path import abspath, normpath, expanduser, join 
    66from os.path import isfile, exists, isabs 
     7 
     8# Be useful in the absence of asycamore 
     9try: 
     10    from asycamore.exc_string import exc_string 
     11except ImportError: 
     12    import traceback 
     13    def exc_string(einfo=None): 
     14        if not einfo: 
     15            traceback.print_exc() 
     16        else: 
     17            traceback.print_exception(*einfo) 
    718 
    819import logging 
     
    8697        log.critical('stopped') 
    8798        return -1 
    88   
     99    except: 
     100        msg = exc_string() 
     101        if not log.isEnabledFor(logging.CRITICAL): 
     102            print msg 
     103        else: 
     104            log.critical(exc_string()) 
     105        return -1 
     106 
    89107#----------------------------------------------------------------------------- 
    90108# logging configuration 
  • asycamore/trunk/asycamore/httpbench.py

    r736 r738  
    2323from tempfile import mkstemp 
    2424from urlparse import urlsplit 
     25from asycamore.exc_string import exc_string 
    2526import asycamore.chunkedtransfer as chunkedtransfer 
    2627import asycamore.httpclient as httpclient 
     
    567568        if not opts.pdb: 
    568569            raise 
    569         exc_info = sys.exc_info() 
     570        einfo = sys.exc_info() 
    570571        import pdb, traceback 
    571         traceback.print_exception(*exc_info) 
     572        print exc_string(einfo) 
    572573        sys.stderr.write('\nStarting pdb:\n') 
    573574        pdb.post_mortem(exc_info[2]) 
  • asycamore/trunk/asycamore/httpservice.py

    r736 r738  
    280280    def __init__(self, **kw): 
    281281 
    282         self.socket_closed = kw['socket_closed'] 
    283282        self.http_pipeline_limit = kw.pop('http_pipeline_limit', 
    284283                self.http_pipeline_limit) 
    285284        self.multi_accept = kw.pop('multi_accept', self.multi_accept) 
    286285        self.threadteam_sz = kw.pop('threadteam_sz', self.threadteam_sz) 
     286        self.threadlocal = None 
     287        self.socket_closed = kw['socket_closed'] 
     288 
    287289        self.stat_report_phase = kw.pop('report_phase', self.stat_report_phase) 
    288290 
     
    306308    def start_thread_team(self, teamsize, cancel_io_wait): 
    307309        assert teamsize > 0 
    308  
     310        import threading 
    309311        import asycamore.threadpool as threadpool 
    310312 
     
    316318        team.set_team_size(teamsize) 
    317319        self.threadteam_sz = teamsize 
     320        self.threadlocal = threading.local() 
    318321 
    319322        #_test_thread_delegate(self.thread_delegate) 
     
    603606        """ 
    604607        conn_started = self.connectionfactories[o.server_addr][1] 
    605         self.connectioncontexts[o.dispatch_id 
    606                 ] = conn_started(self, o, event) 
     608        if not self.threadteam_sz: 
     609            self.connectioncontexts[o.dispatch_id 
     610                    ] = conn_started(self, o, event) 
     611        else: 
     612            self.connectioncontexts[o.dispatch_id 
     613                    ] = conn_started( 
     614                            self, o, event, self.threadlocal 
     615                            ) 
    607616 
    608617 
  • asycamore/trunk/asycamore/httpservicecontext.py

    r737 r738  
    5656class HTTPServiceContext(object): 
    5757 
    58     def __init__(self, model, o): 
     58    def __init__(self, model, o, threadlocal): 
    5959 
    6060        self.is_paused = False 
     
    6464 
    6565        self.model = model 
     66        self.cachevars = wsgienviron.initialise_cachevars(threadlocal) 
     67 
    6668        self.o = o 
    6769        self.read_sentinel = object() 
     
    9092 
    9193    @classmethod 
    92     def connection_started(cls, servicemodel, o, event): 
    93         return cls(servicemodel, o, event
     94    def connection_started(cls, servicemodel, o, event, threadlocal=None): 
     95        return cls(servicemodel, o, event, threadlocal
    9496 
    9597 
     
    377379            ) 
    378380 
    379     def __init__(self, servicemodel, o, event, wsgi_app=None): 
     381    def __init__(self, servicemodel, o, event, 
     382            wsgi_app=None, threadlocal=None): 
    380383        super(WSGIServiceContext, self).__init__( 
    381                 servicemodel, o
     384                servicemodel, o, threadlocal
    382385        self.wsgi_app = wsgi_app or self.default_app 
    383386 
     
    554557 
    555558    @classmethod 
    556     def connection_started(cls, servicemodel, o, event, wsgi_app=None): 
    557         return cls(servicemodel, o, event, wsgi_app) 
     559    def connection_started(cls, servicemodel, o, event, 
     560            wsgi_app=None, threadlocal=None): 
     561        return cls(servicemodel, o, event, 
     562                wsgi_app=wsgi_app, threadlocal=threadlocal 
     563                ) 
    558564 
    559565    def start_request(self, d): 
  • asycamore/trunk/asycamore/wsgienviron.py

    r737 r738  
    1515from urllib import unquote 
    1616from urlparse import urlparse 
     17 
     18# Weekday and month names for HTTP date/time formatting; always English! 
     19_weekdayname = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"] 
     20_monthname = [None, # Dummy so we can use 1-based month numbers 
     21              "Jan", "Feb", "Mar", "Apr", "May", "Jun", 
     22              "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] 
     23 
     24def format_date_time(timestamp): 
     25    """This is faster than the rfc822 method by ~20%""" 
     26    year, month, day, hh, mm, ss, wd, y, z = time.gmtime(timestamp) 
     27    return "%s, %02d %3s %4d %02d:%02d:%02d GMT" % ( 
     28        _weekdayname[wd], day, _monthname[month], year, hh, mm, ss 
     29    ) 
     30 
     31 
     32COMMA_SEPARATED_HEADERS = ['ACCEPT', 'ACCEPT-CHARSET', 'ACCEPT-ENCODING', 
     33    'ACCEPT-LANGUAGE', 'ACCEPT-RANGES', 'ALLOW', 'CACHE-CONTROL', 
     34    'CONNECTION', 'CONTENT-ENCODING', 'CONTENT-LANGUAGE', 'EXPECT', 
     35    'IF-MATCH', 'IF-NONE-MATCH', 'PRAGMA', 'PROXY-AUTHENTICATE', 'TE', 
     36    'TRAILER', 'TRANSFER-ENCODING', 'UPGRADE', 'VARY', 'VIA', 'WARNING', 
     37    'WWW-AUTHENTICATE'] 
     38 
     39def initialise_cachevars(o): 
     40    """Populate an instance with fresh cache variables. 
     41 
     42    Initialisation is idempotent: Only those attributes which are not 
     43    present in `o` and are necessary to the api in this module are set. If 
     44    an attribute exists in `o` it is *not* reset. 
     45 
     46    By default, some of the api's in this module cache certain results gobaly 
     47    accross all requests. This may not be what you want, but more importantly 
     48    it is *not* thread safe. 
     49 
     50    This method should be used to ensure proper initialisation of the various 
     51    cache variables in cases where you want to control what instances are used 
     52    by those api's that accept a `cachevars` parameter. 
     53 
     54    The expected use is for threaded servers, in which case you should pass an 
     55    instance of threading.local as the `o` parameter. Having done this you 
     56    should then pass the pertinent attributes of your thread local instance to 
     57    the apis that accept `_cache` arguments. 
     58 
     59    For your convenience, `o` may be none in which case an instance is created 
     60    for you using ``type('Bunch', (object,), {})``. 
     61 
     62    So to setup conditionaly based on threading being enabled do something 
     63    like:: 
     64 
     65        cachevars = None 
     66        if threading_enabled: 
     67            cachevars = threading.local() 
     68        cachevars = initialise_cachevars(cachevars) 
     69 
     70    And to call `populate_server_environ` without having to worry about 
     71    whether threading is enabled or not do:: 
     72 
     73        populate_server_environ(environ, request_line, cachevars) 
     74 
     75    NOTE: calling initialise_cachevars a second time, on the same instance, 
     76    will *not* reset the attributes. 
     77 
     78    >>> o = initialise_cachevars() 
     79    >>> a1 = o.encoded_path 
     80    >>> a1['/foo']='/bar' 
     81    >>> o2 = initialise_cachevars(o) 
     82    >>> assert o is o2 and a1 is o2.encoded_path 
     83    >>> assert o2.encoded_path['/foo'] == '/bar' 
     84 
     85    """ 
     86 
     87    if o is None: 
     88        o = type('Bunch', (object,), {}) 
     89 
     90    for dictvar in 'encoded_path resource_path http_k'.split(): 
     91        if not hasattr(o, dictvar): 
     92            setattr(o, dictvar, {}) 
     93 
     94    if not hasattr(o, 'date'): 
     95        t = time.time() 
     96        o.date = (t, format_date_time(t)) 
     97    if not hasattr(o, 'comma_separated_headers'): 
     98        o.comma_separated_headers = frozenset([ 
     99                'HTTP_' + k.replace('-', '_') for k in COMMA_SEPARATED_HEADERS] 
     100                ) 
     101    return o 
     102 
     103_cachevars = initialise_cachevars(None) 
    17104 
    18105 
     
    42129_resource_path_cache={} 
    43130 
    44 def populate_server_environ(environ, request_line, 
    45          quoted_slash=quoted_slash, 
    46         server_software='ASYCAMORE-WSGI/dev', 
    47         _encoded_path_cache=_encoded_path_cache 
    48         ): 
     131def populate_server_environ(environ, request_line, cachevars=_cachevars, 
     132    quoted_slash=quoted_slash, server_software='ASYCAMORE-WSGI/dev'): 
    49133    """`consider_request_lin` without app selection.""" 
    50134 
     
    87171    # before the escaped characters within those components can be 
    88172    # safely decoded." http://www.ietf.org/rfc/rfc2396.txt, sec 2.4.2 
    89     if path in _encoded_path_cache
    90         path = _encoded_path_cache[path] 
     173    if path in cachevars.encoded_path
     174        path = cachevars.encoded_path[path] 
    91175    else: 
    92176        atoms = [unquote(x) for x in quoted_slash.split(path)] 
    93         path = _encoded_path_cache[path] = "%2F".join(atoms) 
     177        path = cachevars.encoded_path[path] = "%2F".join(atoms) 
    94178    environ["SCRIPT_NAME"] = "" 
    95179    environ["PATH_INFO"] = path 
     
    119203 
    120204 
    121 # Weekday and month names for HTTP date/time formatting; always English! 
    122 _weekdayname = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"] 
    123 _monthname = [None, # Dummy so we can use 1-based month numbers 
    124               "Jan", "Feb", "Mar", "Apr", "May", "Jun", 
    125               "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] 
    126  
    127 def format_date_time(timestamp): 
    128     """This is faster than the rfc822 method by ~20%""" 
    129     year, month, day, hh, mm, ss, wd, y, z = time.gmtime(timestamp) 
    130     return "%s, %02d %3s %4d %02d:%02d:%02d GMT" % ( 
    131         _weekdayname[wd], day, _monthname[month], year, hh, mm, ss 
    132     ) 
    133  
    134 _cached_date=time.time() 
    135 _cached_date=(_cached_date, format_date_time(_cached_date)) 
    136  
    137 def process_message_headers(environ, headers): 
     205def process_message_headers(environ, headers, cachevars=_cachevars): 
    138206 
    139207    """Populate environ keys that are derived from the request headers. 
     
    144212    """ 
    145213 
    146     global _cached_date 
    147  
    148214    try: 
    149         consume_headers(environ, headers) 
     215        _consume_headers(environ, headers, 
     216                cachevars.comma_separated_headers, cachevars.http_k) 
    150217    except ValueError, e: 
    151218        response = build_simple_response( 
     
    154221        return response 
    155222 
     223    # Updating the date stamp at a sensible frequency makes a surprising 
     224    # difference to the over all request rate when the server is under load. 
    156225    now=time.time() 
    157     if now - _cached_date[0] > 0.999: 
     226    if now - cachevars.date[0] > 0.999: 
    158227        Date = format_date_time(now) 
    159         _cached_date=(now, Date) 
    160     else: 
    161         Date = _cached_date[1] 
     228        cachevars.date=(now, Date) 
     229    else: 
     230        Date = cachevars.date[1] 
     231 
    162232    response_headers=[ 
    163233        ('Server', environ["SERVER_SOFTWARE"]), 
     
    204274 
    205275    cl = environ.get("CONTENT_LENGTH") 
    206     if (method == "POST" or method == "PUT") and not read_chunked and cl is None: 
     276    if ((method == "POST" or method == "PUT") 
     277            and not read_chunked and cl is None): 
    207278        # No Content-Length header supplied. This will hang 
    208279        # cgi.FieldStorage, since it cannot determine when to 
     
    237308    return response_headers 
    238309 
    239  
    240 COMMA_SEPARATED_HEADERS = ['ACCEPT', 'ACCEPT-CHARSET', 'ACCEPT-ENCODING', 
    241     'ACCEPT-LANGUAGE', 'ACCEPT-RANGES', 'ALLOW', 'CACHE-CONTROL', 
    242     'CONNECTION', 'CONTENT-ENCODING', 'CONTENT-LANGUAGE', 'EXPECT', 
    243     'IF-MATCH', 'IF-NONE-MATCH', 'PRAGMA', 'PROXY-AUTHENTICATE', 'TE', 
    244     'TRAILER', 'TRANSFER-ENCODING', 'UPGRADE', 'VARY', 'VIA', 'WARNING', 
    245     'WWW-AUTHENTICATE'] 
    246 _http_k_cache=dict() 
    247 def consume_headers(environ, messageheaders, 
    248         COMMA_SEPARATED_HEADERS=frozenset([ 
    249             'HTTP_' + k.replace('-', '_') for k in COMMA_SEPARATED_HEADERS]), 
    250         _http_k_cache=_http_k_cache): 
     310def consume_headers(environ, messageheaders, cachevars=_cachevars): 
     311    return _consume_headers(environ, messageheaders, 
     312            cachevars.comma_separated_headers, cachevars.http_k 
     313            ) 
     314 
     315def _consume_headers(environ, messageheaders, 
     316        comma_separated_headers, http_k_cache): 
    251317 
    252318    """Set up the header keys in environ using the provided Message-Headers 
     
    286352            # the rest of this function. and this section of code is very 
    287353            # high on the profile when stressing request through put. 
    288             HTTP_K=_http_k_cache.get(k, None) 
     354            HTTP_K=http_k_cache.get(k, None) 
    289355            if not HTTP_K: 
    290356                HTTP_K = 'HTTP_' + k.strip().upper().replace('-', '_') 
    291                 _http_k_cache[k]=HTTP_K 
     357                http_k_cache[k]=HTTP_K 
    292358 
    293359        if HTTP_K in COMMA_SEPARATED_HEADERS: 
     
    325391        return 'http' 
    326392 
    327 #EOF 
     393#