Limit parallel processes in CherryPy?

2019-08-30 18:12发布

问题:

I have a CherryPy server running on a BeagleBone Black. Server generates a simple webpage and does local SPI reads / writes (hardware interface). The application is going to be used on a local network with 1-2 clients at a time. I need to prevent a CherryPy class function being called twice, two or more instances before it completes. Thoughts?

回答1:

As saaj commented, a simple threading.Lock() will prevent the handler from being run at the same time by another client. I might also add, using cherrypy.session.acquire_lock() will prevent the same client from the running two handlers simultaneously.

Refreshing article on Python locks and stuff: http://effbot.org/zone/thread-synchronization.htm

Although I would make saaj's solution much simpler by using a "with" statement in Python, to hide all those fancy lock acquisitions/releases and try/except block.

lock = threading.Lock()

@cherrypy.expose
def index(self):
    with lock:
        # do stuff in the handler.
        # this code will only be run by one client at a time
        return '<html></html>'


回答2:

It is general synchronization question, though CherryPy side has a subtlety. CherryPy is a threaded-server so it is sufficient to have an application level lock, e.g. threading.Lock.

The subtlety is that you can't see the run-or-fail behaviour from within a single browser because of pipelining, Keep-Alive or caching. Which one it is is hard to guess as the behaviour varies in Chromium and Firefox. As far as I can see CherryPy will try to serialize processing of request coming from single TCP connection, which effectively results in subsequent requests waiting for active request in a queue. With some trial-and-error I've found that adding cache-prevention token leads to the desired behaviour (even though Chromium still sends Connection: keep-alive for XHR where Firefox does not).

If run-or-fail in single browser isn't important to you you can safely ignore the previous paragraph and JavaScript code in the following example.

Update

The cause of request serialisation coming from one browser to the same URL doesn't lie in server-side. It's an implementation detail of a browser cache (details). Though, the solution of adding random query string parameter, nc, is correct.

#!/usr/bin/env python
# -*- coding: utf-8 -*-


import threading
import time

import cherrypy


config = {
  'global' : {
    'server.socket_host' : '127.0.0.1',
    'server.socket_port' : 8080,
    'server.thread_pool' : 8
  }
}


class App:

  lock = threading.Lock()


  @cherrypy.expose
  def index(self):
    return '''<!DOCTYPE html>
      <html>
      <head>
        <title>Lock demo</title>
        <script type='text/javascript' src='http://cdnjs.cloudflare.com/ajax/libs/qooxdoo/3.5.1/q.min.js'></script>
        <script type='text/javascript'>
          function runTask(wait)
          {
            var url = (wait ? '/runOrWait' : '/runOrFail') + '?nc=' + Date.now();
            var xhr = q.io.xhr(url);
            xhr.on('loadend', function(xhr) 
            {
              if(xhr.status == 200)
              {
                console.log('success', xhr.responseText)
              }
              else if(xhr.status == 503)
              {
                console.log('busy');
              }
            });
            xhr.send();
          }

          q.ready(function()
          {
            q('p a').on('click', function(event)
            {
              event.preventDefault();

              var wait = parseInt(q(event.getTarget()).getData('wait'));
              runTask(wait);
            });
          });
        </script>
      </head>
      <body>
        <p><a href='#' data-wait='0'>Run or fail</a></p>
        <p><a href='#' data-wait='1'>Run or wait</a></p>
      </body>
      </html>
    '''

  def calculate(self):
    time.sleep(8)
    return 'Long task result'

  @cherrypy.expose
  def runOrWait(self, **kwargs):
    self.lock.acquire()
    try:
      return self.calculate()
    finally:
      self.lock.release()

  @cherrypy.expose
  def runOrFail(self, **kwargs):
    locked = self.lock.acquire(False)
    if not locked:
      raise cherrypy.HTTPError(503, 'Task is already running')
    else:
      try:
        return self.calculate()
      finally:
        self.lock.release()


if __name__ == '__main__':
  cherrypy.quickstart(App(), '/', config)