mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-08 10:44:09 -04:00
Server: Run the actual request handling code in threads so as not to block the servers event loop
This commit is contained in:
parent
5b2049c3c8
commit
ab81fe992b
@ -9,3 +9,6 @@ __copyright__ = '2015, Kovid Goyal <kovid at kovidgoyal.net>'
|
|||||||
|
|
||||||
class HTTP404(Exception):
|
class HTTP404(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
class JobQueueFull(Exception):
|
||||||
|
pass
|
||||||
|
@ -316,14 +316,45 @@ class HTTPConnection(HTTPRequest):
|
|||||||
outheaders, self.response_protocol, self.static_cache, self.opts,
|
outheaders, self.response_protocol, self.static_cache, self.opts,
|
||||||
self.remote_addr, self.remote_port
|
self.remote_addr, self.remote_port
|
||||||
)
|
)
|
||||||
try:
|
self.queue_job(self.run_request_handler, data)
|
||||||
output = self.request_handler(data)
|
|
||||||
except HTTP404 as e:
|
|
||||||
return self.simple_response(httplib.NOT_FOUND, msg=e.message or '', close_after_response=False)
|
|
||||||
|
|
||||||
|
def run_request_handler(self, data):
|
||||||
|
result = self.request_handler(data)
|
||||||
|
return data, result
|
||||||
|
|
||||||
|
def send_range_not_satisfiable(self, content_length):
|
||||||
|
buf = [
|
||||||
|
'%s %d %s' % (self.response_protocol, httplib.REQUESTED_RANGE_NOT_SATISFIABLE, httplib.responses[httplib.REQUESTED_RANGE_NOT_SATISFIABLE]),
|
||||||
|
"Date: " + http_date(),
|
||||||
|
"Content-Range: bytes */%d" % content_length,
|
||||||
|
]
|
||||||
|
self.response_ready(header_list_to_file(buf))
|
||||||
|
|
||||||
|
def send_not_modified(self, etag=None):
|
||||||
|
buf = [
|
||||||
|
'%s %d %s' % (self.response_protocol, httplib.NOT_MODIFIED, httplib.responses[httplib.NOT_MODIFIED]),
|
||||||
|
"Content-Length: 0",
|
||||||
|
"Date: " + http_date(),
|
||||||
|
]
|
||||||
|
if etag is not None:
|
||||||
|
buf.append('ETag: ' + etag)
|
||||||
|
self.response_ready(header_list_to_file(buf))
|
||||||
|
|
||||||
|
def report_busy(self):
|
||||||
|
self.simple_response(httplib.SERVICE_UNAVAILABLE)
|
||||||
|
|
||||||
|
def job_done(self, ok, result):
|
||||||
|
if not ok:
|
||||||
|
etype, e, tb = result
|
||||||
|
if isinstance(e, HTTP404):
|
||||||
|
return self.simple_response(httplib.NOT_FOUND, msg=e.message or '', close_after_response=False)
|
||||||
|
raise e, None, tb
|
||||||
|
|
||||||
|
data, output = result
|
||||||
output = self.finalize_output(output, data, self.method is HTTP1)
|
output = self.finalize_output(output, data, self.method is HTTP1)
|
||||||
if output is None:
|
if output is None:
|
||||||
return
|
return
|
||||||
|
outheaders = data.outheaders
|
||||||
|
|
||||||
outheaders.set('Date', http_date(), replace_all=True)
|
outheaders.set('Date', http_date(), replace_all=True)
|
||||||
outheaders.set('Server', 'calibre %s' % __version__, replace_all=True)
|
outheaders.set('Server', 'calibre %s' % __version__, replace_all=True)
|
||||||
@ -348,24 +379,6 @@ class HTTPConnection(HTTPRequest):
|
|||||||
buf.append('')
|
buf.append('')
|
||||||
self.response_ready(BytesIO(b''.join((x + '\r\n').encode('ascii') for x in buf)), output=output)
|
self.response_ready(BytesIO(b''.join((x + '\r\n').encode('ascii') for x in buf)), output=output)
|
||||||
|
|
||||||
def send_range_not_satisfiable(self, content_length):
|
|
||||||
buf = [
|
|
||||||
'%s %d %s' % (self.response_protocol, httplib.REQUESTED_RANGE_NOT_SATISFIABLE, httplib.responses[httplib.REQUESTED_RANGE_NOT_SATISFIABLE]),
|
|
||||||
"Date: " + http_date(),
|
|
||||||
"Content-Range: bytes */%d" % content_length,
|
|
||||||
]
|
|
||||||
self.response_ready(header_list_to_file(buf))
|
|
||||||
|
|
||||||
def send_not_modified(self, etag=None):
|
|
||||||
buf = [
|
|
||||||
'%s %d %s' % (self.response_protocol, httplib.NOT_MODIFIED, httplib.responses[httplib.NOT_MODIFIED]),
|
|
||||||
"Content-Length: 0",
|
|
||||||
"Date: " + http_date(),
|
|
||||||
]
|
|
||||||
if etag is not None:
|
|
||||||
buf.append('ETag: ' + etag)
|
|
||||||
self.response_ready(header_list_to_file(buf))
|
|
||||||
|
|
||||||
def response_ready(self, header_file, output=None):
|
def response_ready(self, header_file, output=None):
|
||||||
self.response_started = True
|
self.response_started = True
|
||||||
self.optimize_for_sending_packet()
|
self.optimize_for_sending_packet()
|
||||||
|
@ -8,10 +8,13 @@ __copyright__ = '2015, Kovid Goyal <kovid at kovidgoyal.net>'
|
|||||||
|
|
||||||
import ssl, socket, select, os, traceback
|
import ssl, socket, select, os, traceback
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
|
from Queue import Empty, Full
|
||||||
from functools import partial
|
from functools import partial
|
||||||
|
|
||||||
from calibre import as_unicode
|
from calibre import as_unicode
|
||||||
from calibre.ptempfile import TemporaryDirectory
|
from calibre.ptempfile import TemporaryDirectory
|
||||||
|
from calibre.srv.errors import JobQueueFull
|
||||||
|
from calibre.srv.pool import ThreadPool
|
||||||
from calibre.srv.opts import Options
|
from calibre.srv.opts import Options
|
||||||
from calibre.srv.utils import (
|
from calibre.srv.utils import (
|
||||||
socket_errors_socket_closed, socket_errors_nonblocking, HandleInterrupt,
|
socket_errors_socket_closed, socket_errors_nonblocking, HandleInterrupt,
|
||||||
@ -21,7 +24,7 @@ from calibre.utils.socket_inheritance import set_socket_inherit
|
|||||||
from calibre.utils.logging import ThreadSafeLog
|
from calibre.utils.logging import ThreadSafeLog
|
||||||
from calibre.utils.monotonic import monotonic
|
from calibre.utils.monotonic import monotonic
|
||||||
|
|
||||||
READ, WRITE, RDWR = 'READ', 'WRITE', 'RDWR'
|
READ, WRITE, RDWR, WAIT = 'READ', 'WRITE', 'RDWR', 'WAIT'
|
||||||
WAKEUP, JOB_DONE = bytes(bytearray(xrange(2)))
|
WAKEUP, JOB_DONE = bytes(bytearray(xrange(2)))
|
||||||
|
|
||||||
class ReadBuffer(object): # {{{
|
class ReadBuffer(object): # {{{
|
||||||
@ -113,8 +116,8 @@ class ReadBuffer(object): # {{{
|
|||||||
|
|
||||||
class Connection(object): # {{{
|
class Connection(object): # {{{
|
||||||
|
|
||||||
def __init__(self, socket, opts, ssl_context, tdir, addr):
|
def __init__(self, socket, opts, ssl_context, tdir, addr, pool):
|
||||||
self.opts = opts
|
self.opts, self.pool = opts, pool
|
||||||
try:
|
try:
|
||||||
self.remote_addr = addr[0]
|
self.remote_addr = addr[0]
|
||||||
self.remote_port = addr[1]
|
self.remote_port = addr[1]
|
||||||
@ -234,6 +237,21 @@ class Connection(object): # {{{
|
|||||||
except socket.error:
|
except socket.error:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def queue_job(self, func, *args):
|
||||||
|
if args:
|
||||||
|
func = partial(func, *args)
|
||||||
|
try:
|
||||||
|
self.pool.put_nowait(self.socket.fileno(), func)
|
||||||
|
except Full:
|
||||||
|
raise JobQueueFull()
|
||||||
|
self.set_state(WAIT, self._job_done)
|
||||||
|
|
||||||
|
def _job_done(self, event):
|
||||||
|
self.job_done(*event)
|
||||||
|
|
||||||
|
def job_done(self, ok, result):
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def state_description(self):
|
def state_description(self):
|
||||||
return ''
|
return ''
|
||||||
@ -241,6 +259,9 @@ class Connection(object): # {{{
|
|||||||
def report_unhandled_exception(self, e, formatted_traceback):
|
def report_unhandled_exception(self, e, formatted_traceback):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def report_busy(self):
|
||||||
|
pass
|
||||||
|
|
||||||
def connection_ready(self):
|
def connection_ready(self):
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
@ -286,6 +307,7 @@ class ServerLoop(object):
|
|||||||
self.bind_address = self.pre_activated_socket.getsockname()
|
self.bind_address = self.pre_activated_socket.getsockname()
|
||||||
|
|
||||||
self.create_control_connection()
|
self.create_control_connection()
|
||||||
|
self.pool = ThreadPool(self.log, self.job_completed, count=self.opts.worker_count)
|
||||||
|
|
||||||
def create_control_connection(self):
|
def create_control_connection(self):
|
||||||
self.control_in, self.control_out = create_sock_pair()
|
self.control_in, self.control_out = create_sock_pair()
|
||||||
@ -343,6 +365,7 @@ class ServerLoop(object):
|
|||||||
self.bound_address = ba = self.socket.getsockname()
|
self.bound_address = ba = self.socket.getsockname()
|
||||||
if isinstance(ba, tuple):
|
if isinstance(ba, tuple):
|
||||||
ba = ':'.join(map(type(''), ba))
|
ba = ':'.join(map(type(''), ba))
|
||||||
|
self.pool.start()
|
||||||
with TemporaryDirectory(prefix='srv-') as tdir:
|
with TemporaryDirectory(prefix='srv-') as tdir:
|
||||||
self.tdir = tdir
|
self.tdir = tdir
|
||||||
self.ready = True
|
self.ready = True
|
||||||
@ -351,13 +374,14 @@ class ServerLoop(object):
|
|||||||
while self.ready:
|
while self.ready:
|
||||||
try:
|
try:
|
||||||
self.tick()
|
self.tick()
|
||||||
except (KeyboardInterrupt, SystemExit) as e:
|
except SystemExit:
|
||||||
self.shutdown()
|
self.shutdown()
|
||||||
if isinstance(e, SystemExit):
|
raise
|
||||||
raise
|
except KeyboardInterrupt:
|
||||||
break
|
break
|
||||||
except:
|
except:
|
||||||
self.log.exception('Error in ServerLoop.tick')
|
self.log.exception('Error in ServerLoop.tick')
|
||||||
|
self.shutdown()
|
||||||
|
|
||||||
def setup_socket(self):
|
def setup_socket(self):
|
||||||
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||||
@ -388,14 +412,17 @@ class ServerLoop(object):
|
|||||||
read_needed, write_needed, readable, remove = [], [], [], []
|
read_needed, write_needed, readable, remove = [], [], [], []
|
||||||
for s, conn in self.connection_map.iteritems():
|
for s, conn in self.connection_map.iteritems():
|
||||||
if now - conn.last_activity > self.opts.timeout:
|
if now - conn.last_activity > self.opts.timeout:
|
||||||
if not conn.handle_timeout():
|
if conn.handle_timeout():
|
||||||
|
conn.last_activity = now
|
||||||
|
else:
|
||||||
remove.append((s, conn))
|
remove.append((s, conn))
|
||||||
continue
|
continue
|
||||||
if conn.wait_for is READ:
|
wf = conn.wait_for
|
||||||
|
if wf is READ:
|
||||||
(readable if conn.read_buffer.has_data else read_needed).append(s)
|
(readable if conn.read_buffer.has_data else read_needed).append(s)
|
||||||
elif conn.wait_for is WRITE:
|
elif wf is WRITE:
|
||||||
write_needed.append(s)
|
write_needed.append(s)
|
||||||
else:
|
elif wf is RDWR:
|
||||||
write_needed.append(s)
|
write_needed.append(s)
|
||||||
(readable if conn.read_buffer.has_data else read_needed).append(s)
|
(readable if conn.read_buffer.has_data else read_needed).append(s)
|
||||||
|
|
||||||
@ -434,10 +461,20 @@ class ServerLoop(object):
|
|||||||
conn.handle_event(event)
|
conn.handle_event(event)
|
||||||
if not conn.ready:
|
if not conn.ready:
|
||||||
self.close(s, conn)
|
self.close(s, conn)
|
||||||
|
except JobQueueFull:
|
||||||
|
self.log.exception('Server busy handling request: ' % conn.state_description)
|
||||||
|
if conn.ready:
|
||||||
|
if conn.response_started:
|
||||||
|
self.close(s, conn)
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
conn.report_busy()
|
||||||
|
except Exception:
|
||||||
|
self.close(s, conn)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
ignore.add(s)
|
ignore.add(s)
|
||||||
|
self.log.exception('Unhandled exception in state: %s' % conn.state_description)
|
||||||
if conn.ready:
|
if conn.ready:
|
||||||
self.log.exception('Unhandled exception in state: %s' % conn.state_description)
|
|
||||||
if conn.response_started:
|
if conn.response_started:
|
||||||
self.close(s, conn)
|
self.close(s, conn)
|
||||||
else:
|
else:
|
||||||
@ -452,6 +489,19 @@ class ServerLoop(object):
|
|||||||
def wakeup(self):
|
def wakeup(self):
|
||||||
self.control_in.sendall(WAKEUP)
|
self.control_in.sendall(WAKEUP)
|
||||||
|
|
||||||
|
def job_completed(self):
|
||||||
|
self.control_in.sendall(JOB_DONE)
|
||||||
|
|
||||||
|
def dispatch_job_results(self):
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
s, ok, result = self.pool.get_nowait()
|
||||||
|
except Empty:
|
||||||
|
break
|
||||||
|
conn = self.connection_map.get(s)
|
||||||
|
if conn is not None:
|
||||||
|
yield s, conn, (ok, result)
|
||||||
|
|
||||||
def close(self, s, conn):
|
def close(self, s, conn):
|
||||||
self.connection_map.pop(s, None)
|
self.connection_map.pop(s, None)
|
||||||
conn.close()
|
conn.close()
|
||||||
@ -465,7 +515,7 @@ class ServerLoop(object):
|
|||||||
if sock is not None:
|
if sock is not None:
|
||||||
s = sock.fileno()
|
s = sock.fileno()
|
||||||
if s > -1:
|
if s > -1:
|
||||||
self.connection_map[s] = conn = self.handler(sock, self.opts, self.ssl_context, self.tdir, addr)
|
self.connection_map[s] = conn = self.handler(sock, self.opts, self.ssl_context, self.tdir, addr, self.pool)
|
||||||
if self.ssl_context is not None:
|
if self.ssl_context is not None:
|
||||||
yield s, conn, RDWR
|
yield s, conn, RDWR
|
||||||
elif s == control:
|
elif s == control:
|
||||||
@ -478,7 +528,8 @@ class ServerLoop(object):
|
|||||||
self.create_control_connection()
|
self.create_control_connection()
|
||||||
continue
|
continue
|
||||||
if c == JOB_DONE:
|
if c == JOB_DONE:
|
||||||
pass
|
for s, conn, event in self.dispatch_job_results():
|
||||||
|
yield s, conn, event
|
||||||
elif c == WAKEUP:
|
elif c == WAKEUP:
|
||||||
pass
|
pass
|
||||||
elif not c:
|
elif not c:
|
||||||
@ -510,6 +561,7 @@ class ServerLoop(object):
|
|||||||
pass
|
pass
|
||||||
for s, conn in tuple(self.connection_map.iteritems()):
|
for s, conn in tuple(self.connection_map.iteritems()):
|
||||||
self.close(s, conn)
|
self.close(s, conn)
|
||||||
|
self.pool.stop(self.opts.shutdown_timeout)
|
||||||
|
|
||||||
class EchoLine(Connection): # {{{
|
class EchoLine(Connection): # {{{
|
||||||
|
|
||||||
|
@ -52,6 +52,10 @@ raw_options = (
|
|||||||
'compress_min_size', 1024,
|
'compress_min_size', 1024,
|
||||||
None,
|
None,
|
||||||
|
|
||||||
|
'Number of worker threads to use to process requests',
|
||||||
|
'worker_count', 10,
|
||||||
|
None,
|
||||||
|
|
||||||
'Use zero copy file transfers for increased performance',
|
'Use zero copy file transfers for increased performance',
|
||||||
'use_sendfile', True,
|
'use_sendfile', True,
|
||||||
'This will use zero-copy in-kernel transfers when sending files over the network,'
|
'This will use zero-copy in-kernel transfers when sending files over the network,'
|
||||||
|
83
src/calibre/srv/pool.py
Normal file
83
src/calibre/srv/pool.py
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
#!/usr/bin/env python2
|
||||||
|
# vim:fileencoding=utf-8
|
||||||
|
from __future__ import (unicode_literals, division, absolute_import,
|
||||||
|
print_function)
|
||||||
|
|
||||||
|
__license__ = 'GPL v3'
|
||||||
|
__copyright__ = '2015, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||||
|
|
||||||
|
import sys, time
|
||||||
|
from Queue import Queue, Full
|
||||||
|
from threading import Thread
|
||||||
|
|
||||||
|
class Worker(Thread):
|
||||||
|
|
||||||
|
daemon = True
|
||||||
|
|
||||||
|
def __init__(self, log, notify_server, num, request_queue, result_queue):
|
||||||
|
self.request_queue, self.result_queue = request_queue, result_queue
|
||||||
|
self.notify_server = notify_server
|
||||||
|
self.log = log
|
||||||
|
self.working = False
|
||||||
|
Thread.__init__(self, name='ServerWorker%d' % num)
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
while True:
|
||||||
|
x = self.request_queue.get()
|
||||||
|
if x is None:
|
||||||
|
break
|
||||||
|
job_id, func = x
|
||||||
|
self.working = True
|
||||||
|
try:
|
||||||
|
result = func()
|
||||||
|
except Exception:
|
||||||
|
self.handle_error(job_id) # must be a separate function to avoid reference cycles with sys.exc_info()
|
||||||
|
else:
|
||||||
|
self.result_queue.put((job_id, True, result))
|
||||||
|
finally:
|
||||||
|
self.working = False
|
||||||
|
try:
|
||||||
|
self.notify_server()
|
||||||
|
except Exception:
|
||||||
|
self.log.exception('ServerWorker failed to notify server on job completion')
|
||||||
|
|
||||||
|
def handle_error(self, job_id):
|
||||||
|
self.result_queue.put((job_id, False, sys.exc_info()))
|
||||||
|
|
||||||
|
class ThreadPool(object):
|
||||||
|
|
||||||
|
def __init__(self, log, notify_server, count=10, queue_size=1000):
|
||||||
|
self.request_queue, self.result_queue = Queue(queue_size), Queue(queue_size)
|
||||||
|
self.workers = [Worker(log, notify_server, i, self.request_queue, self.result_queue) for i in xrange(count)]
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
for w in self.workers:
|
||||||
|
w.start()
|
||||||
|
|
||||||
|
def put_nowait(self, job_id, func):
|
||||||
|
self.request_queue.put_nowait((job_id, func))
|
||||||
|
|
||||||
|
def get_nowait(self):
|
||||||
|
return self.result_queue.get_nowait()
|
||||||
|
|
||||||
|
def stop(self, shutdown_timeout):
|
||||||
|
end = time.time() + shutdown_timeout
|
||||||
|
for w in self.workers:
|
||||||
|
try:
|
||||||
|
self.request_queue.put_nowait(None)
|
||||||
|
except Full:
|
||||||
|
break
|
||||||
|
for w in self.workers:
|
||||||
|
now = time.time()
|
||||||
|
if now >= end:
|
||||||
|
break
|
||||||
|
w.join(end - now)
|
||||||
|
self.workers = [w for w in self.workers if w.is_alive()]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def busy(self):
|
||||||
|
return sum(int(w.working) for w in self.workers)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def idle(self):
|
||||||
|
return sum(int(not w.working) for w in self.workers)
|
@ -21,6 +21,14 @@ from calibre.ptempfile import TemporaryDirectory
|
|||||||
|
|
||||||
class LoopTest(BaseTest):
|
class LoopTest(BaseTest):
|
||||||
|
|
||||||
|
def test_workers(self):
|
||||||
|
' Test worker semantics '
|
||||||
|
with TestServer(lambda data:(data.path[0] + data.read()), worker_count=3) as server:
|
||||||
|
self.ae(3, sum(int(w.is_alive()) for w in server.loop.pool.workers))
|
||||||
|
server.loop.stop()
|
||||||
|
server.join()
|
||||||
|
self.ae(0, sum(int(w.is_alive()) for w in server.loop.pool.workers))
|
||||||
|
|
||||||
@skipIf(create_server_cert is None, 'certgen module not available')
|
@skipIf(create_server_cert is None, 'certgen module not available')
|
||||||
def test_ssl(self):
|
def test_ssl(self):
|
||||||
'Test serving over SSL'
|
'Test serving over SSL'
|
||||||
|
Loading…
x
Reference in New Issue
Block a user