Browse Source

Work around CherryPy bug #1200; related cleanups

Spent way too long trying to track down a cryptic error that turned
out to be a CherryPy bug.  Now we catch this using a decorator in the
'extract' and 'intervals' generators that transforms exceptions that
trigger the bugs into one that does not.  fun!
tags/replace-pytables
Jim Paris 11 years ago
parent
commit
c7c65b6542
5 changed files with 43 additions and 14 deletions
  1. +3
    -0
      nilmdb/__init__.py
  2. +1
    -1
      nilmdb/httpclient.py
  3. +33
    -11
      nilmdb/server.py
  4. +5
    -2
      runserver.py
  5. +1
    -0
      tests/test_cmdline.py

+ 3
- 0
nilmdb/__init__.py View File

@@ -7,3 +7,6 @@ from .client import Client
import pyximport; pyximport.install()
import layout
import interval

import cmdline


+ 1
- 1
nilmdb/httpclient.py View File

@@ -86,7 +86,7 @@ class HTTPClient(object):
if code >= 500 and code <= 599:
if args["message"] is None:
args["message"] = ("(no message; try disabling " +
"use_chunked_http_response in " +
"response.stream option in " +
"nilmdb.server for better debugging)")
raise ServerError(**args)
else:


+ 33
- 11
nilmdb/server.py View File

@@ -23,12 +23,35 @@ class NilmApp(object):
def __init__(self, db):
self.db = db

# Set this to False to get better tracebacks from some requests
# (/stream/extract, /stream/intervals)
use_chunked_http_response = False

version = "1.1"

# Decorators
def chunked_response(func):
"""Decorator to enable chunked responses"""
# Set this to False to get better tracebacks from some requests
# (/stream/extract, /stream/intervals).
func._cp_config = { 'response.stream': False }
return func

def workaround_cp_bug_1200(func):
"""Decorator to work around CherryPy bug #1200 in a response
generator"""
# Even if chunked responses are disabled, you may still miss miss
# LookupError, or UnicodeError exceptions due to CherryPy bug
# #1200. This throws them as generic Exceptions insteads.
import functools
import traceback
@functools.wraps(func)
def wrapper(*args, **kwargs):
try:
for val in func(*args, **kwargs):
yield val
except (LookupError, UnicodeError) as e:
raise Exception("bug workaround; real exception is:\n" +
traceback.format_exc())
return wrapper

# CherryPy apps
class Root(NilmApp):
"""Root application for NILM database"""

@@ -217,6 +240,7 @@ class Stream(NilmApp):
# /stream/intervals?path=/newton/prep
# /stream/intervals?path=/newton/prep&start=1234567890.0&end=1234567899.0
@cherrypy.expose
@chunked_response
def intervals(self, path, start = None, end = None):
"""
Get intervals from backend database. Streams the resulting
@@ -238,9 +262,9 @@ class Stream(NilmApp):
if len(streams) != 1:
raise cherrypy.HTTPError("404 Not Found", "No such stream")

@workaround_cp_bug_1200
def content(start, end):
# Note: disable response.stream below to get better debug info
# from tracebacks in this subfunction.
# Note: disable chunked responses to see tracebacks from here.
while True:
(intervals, restart) = self.db.stream_intervals(path,start,end)
response = ''.join([ json.dumps(i) + "\n" for i in intervals ])
@@ -249,10 +273,10 @@ class Stream(NilmApp):
break
start = restart
return content(start, end)
intervals._cp_config = { 'response.stream': use_chunked_http_response }

# /stream/extract?path=/newton/prep&start=1234567890.0&end=1234567899.0
@cherrypy.expose
@chunked_response
def extract(self, path, start = None, end = None, count = False):
"""
Extract data from backend database. Streams the resulting
@@ -282,9 +306,9 @@ class Stream(NilmApp):
# Get formatter
formatter = nilmdb.layout.Formatter(layout)

@workaround_cp_bug_1200
def content(start, end, count):
# Note: disable response.stream below to get better debug info
# from tracebacks in this subfunction.
# Note: disable chunked responses to see tracebacks from here.
if count:
matched = self.db.stream_extract(path, start, end, count)
yield sprintf("%d\n", matched)
@@ -300,8 +324,6 @@ class Stream(NilmApp):
return
start = restart
return content(start, end, count)
extract._cp_config = { 'response.stream': use_chunked_http_response }


class Exiter(object):
"""App that exits the server, for testing"""


+ 5
- 2
runserver.py View File

@@ -3,14 +3,17 @@
import nilmdb
import argparse

parser = argparse.ArgumentParser(description='Run the NILM server')
formatter = argparse.ArgumentDefaultsHelpFormatter
parser = argparse.ArgumentParser(description='Run the NILM server',
formatter_class = formatter)
parser.add_argument('-p', '--port', help='Port number', type=int, default=12380)
parser.add_argument('-d', '--database', help='Database directory', default="db")
parser.add_argument('-y', '--yappi', help='Run with yappi profiler',
action='store_true')
args = parser.parse_args()

# Start web app on a custom port
db = nilmdb.NilmDB("db")
db = nilmdb.NilmDB(args.database)
server = nilmdb.Server(db, host = "127.0.0.1",
port = args.port,
embedded = False)


+ 1
- 0
tests/test_cmdline.py View File

@@ -51,6 +51,7 @@ class TestCmdline(object):
"""Run a cmdline client with the specified argument string,
passing the given input. Returns a tuple with the output and
exit code"""
#print "TZ=UTC ./nilmtool.py " + arg_string
class stdio_wrapper:
def __init__(self, stdin, stdout, stderr):
self.io = (stdin, stdout, stderr)


Loading…
Cancel
Save