Merge lp:~ben-hutchings/ensoft-sextant/render_timeout into lp:ensoft-sextant

Proposed by Ben Hutchings
Status: Merged
Approved by: Robert
Approved revision: 29
Merged at revision: 29
Proposed branch: lp:~ben-hutchings/ensoft-sextant/render_timeout
Merge into: lp:ensoft-sextant
Diff against target: 383 lines (+177/-131)
4 files modified
etc/sextant.conf (+4/-0)
src/sextant/__main__.py (+1/-0)
src/sextant/environment.py (+13/-3)
src/sextant/web/server.py (+159/-128)
To merge this branch: bzr merge lp:~ben-hutchings/ensoft-sextant/render_timeout
Reviewer Review Type Date Requested Status
Robert Approve
Review via email: mp+237046@code.launchpad.net

This proposal supersedes a proposal from 2014-10-01.

Commit message

Refactoring of the _render_plot method - now has a more obvious flow of logic (i.e. will always get to the bottom of the function rather than dropping out in one of the many if/elses).

It should be more straightforward to add more query functions - they are now stored in a dictionary which can be easily added to without changing any of the logic.

Description of the change

Refactoring of the _render_plot method - now has a more obvious flow of logic (i.e. will always get to the bottom of the function rather than dropping out in one of the many if/elses).

It should be more straightforward to add more query functions - they are now stored in a dictionary which can be easily added to without changing any of the logic.

The bulk of the new code is in a block at the bottom of the diff - the whole function has been re-written.

Timing out the neo4j query has proved difficult - it apparently needs a header to
be set on the http request with 'max-execution-time': <milliseconds>
(http://www.markhneedham.com/blog/2013/10/17/neo4j-setting-query-timeout/),
but attempts to modify the neo4j restclient library to allow this have been thus
far unsuccessful.

To post a comment you must log in.
Revision history for this message
Robert (rjwills) wrote : Posted in a previous version of this proposal

This is a bigger fix. In the description, you should explain how you've tested it.

Also, have you tested to see what happens if the Neo4J query takes ages? Ideally we want to abort the query if that is what is taking the time.

review: Needs Fixing
Revision history for this message
Robert (rjwills) wrote : Posted in a previous version of this proposal

Just some small stylistic improvements.

review: Needs Fixing
Revision history for this message
Robert (rjwills) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'etc/sextant.conf'
--- etc/sextant.conf 2014-09-04 13:04:53 +0000
+++ etc/sextant.conf 2014-10-03 11:50:56 +0000
@@ -17,3 +17,7 @@
17server_bind = 127.0.0.117server_bind = 127.0.0.1
18# Port on which to serve Sextant Web18# Port on which to serve Sextant Web
19#port = 290519#port = 2905
20
21[Server]
22# How long to wait before timing out the render
23#render_timeout = 15
2024
=== modified file 'src/sextant/__main__.py'
--- src/sextant/__main__.py 2014-09-30 11:01:00 +0000
+++ src/sextant/__main__.py 2014-10-03 11:50:56 +0000
@@ -61,6 +61,7 @@
61 # allowing non-web functionality to work with Python 3.61 # allowing non-web functionality to work with Python 3.
62 if sys.version_info[0] == 2:62 if sys.version_info[0] == 2:
63 from .web import server63 from .web import server
64 server.render_timeout = config.render_timeout
64 else: # twisted won't be available - Python 2 required65 else: # twisted won't be available - Python 2 required
65 logging.error('Web server must be run on Python 2.')66 logging.error('Web server must be run on Python 2.')
66 return67 return
6768
=== modified file 'src/sextant/environment.py'
--- src/sextant/environment.py 2014-09-03 16:38:00 +0000
+++ src/sextant/environment.py 2014-10-03 11:50:56 +0000
@@ -53,7 +53,7 @@
5353
54Config = collections.namedtuple('Config', ['web_port', 'remote_neo4j',54Config = collections.namedtuple('Config', ['web_port', 'remote_neo4j',
55 'common_cutoff', 'use_ssh_tunnel',55 'common_cutoff', 'use_ssh_tunnel',
56 'ssh_user'])56 'ssh_user', 'render_timeout'])
5757
5858
59def load_config(check_here=''):59def load_config(check_here=''):
@@ -109,7 +109,8 @@
109 'port': '2905',109 'port': '2905',
110 'common_function_calls': '10',110 'common_function_calls': '10',
111 'use_ssh_tunnel': 'False',111 'use_ssh_tunnel': 'False',
112 'ssh_username': ''})112 'ssh_username': '',
113 'render_timeout': '15'})
113 # It turns out that getint, getboolean etc want the defaults to be strings.114 # It turns out that getint, getboolean etc want the defaults to be strings.
114 # When getboolean is called and the default is not a string, and the115 # When getboolean is called and the default is not a string, and the
115 # default is used, it complains that it can't work with non-strings.116 # default is used, it complains that it can't work with non-strings.
@@ -123,6 +124,7 @@
123 common_def = 0 # definition of a 'common' node124 common_def = 0 # definition of a 'common' node
124 use_ssh_tunnel = True125 use_ssh_tunnel = True
125 ssh_user = ''126 ssh_user = ''
127 render_timeout = 0;
126 try:128 try:
127 conf.options('Neo4j')129 conf.options('Neo4j')
128 except ConfigParser.NoSectionError:130 except ConfigParser.NoSectionError:
@@ -132,6 +134,7 @@
132 common_def = conf.getint('Neo4j', 'common_function_calls')134 common_def = conf.getint('Neo4j', 'common_function_calls')
133 use_ssh_tunnel = conf.getboolean('Neo4j', 'use_ssh_tunnel')135 use_ssh_tunnel = conf.getboolean('Neo4j', 'use_ssh_tunnel')
134 ssh_user = conf.get('Neo4j', 'ssh_username')136 ssh_user = conf.get('Neo4j', 'ssh_username')
137
135 try:138 try:
136 global SERVER_BIND139 global SERVER_BIND
137 SERVER_BIND = conf.get('Web', 'server_bind')140 SERVER_BIND = conf.get('Web', 'server_bind')
@@ -139,7 +142,14 @@
139 except ConfigParser.NoSectionError:142 except ConfigParser.NoSectionError:
140 pass143 pass
141144
145 try:
146 conf.options('Server')
147 except ConfigParser.NoSectionError:
148 pass
149 else:
150 render_timeout = conf.getint('Server', 'render_timeout')
151
142 return Config(remote_neo4j=remote_neo4j, web_port=web_port,152 return Config(remote_neo4j=remote_neo4j, web_port=web_port,
143 common_cutoff=common_def, use_ssh_tunnel=use_ssh_tunnel,153 common_cutoff=common_def, use_ssh_tunnel=use_ssh_tunnel,
144 ssh_user=ssh_user)154 ssh_user=ssh_user, render_timeout=render_timeout)
145155
146156
=== modified file 'src/sextant/web/server.py'
--- src/sextant/web/server.py 2014-09-05 12:25:37 +0000
+++ src/sextant/web/server.py 2014-10-03 11:50:56 +0000
@@ -28,6 +28,32 @@
2828
29database_url = None # the URL to access the database instance29database_url = None # the URL to access the database instance
3030
31RESPONSE_CODE_OK = 200
32RESPONSE_CODE_BAD_REQUEST = 400
33RESPONSE_CODE_NOT_FOUND = 404
34RESPONSE_CODE_BAD_GATEWAY = 502
35RESPONSE_CODE_NO_CONTENT = 204
36
37def set_timeout(deferred, timeout=5.0):
38 # cancel the given deferred instance after timeout seconds
39
40 timeout = reactor.callLater(timeout, deferred.cancel)
41
42 def got_result(result):
43 # if the deferred returns before the timeout calls, cancel the timeout
44 # and return the result of the deferred
45 if timeout.active():
46 timeout.cancel()
47
48 return result
49
50 return deferred.addBoth(got_result)
51
52def defer_to_thread_with_timeout(timeout, function, *args, **kwargs):
53 result = deferToThread(function, *args, **kwargs)
54 return set_timeout(result, timeout)
55
56
31class Echoer(Resource):57class Echoer(Resource):
32 # designed to take one name argument58 # designed to take one name argument
3359
@@ -78,134 +104,139 @@
78104
79 @defer.inlineCallbacks105 @defer.inlineCallbacks
80 def _render_plot(self, request):106 def _render_plot(self, request):
81 if "program_name" not in request.args:107 # the items in the args dict are lists - so use .get()[0] to retrieve
82 request.setResponseCode(400)108 args = request.args
83 request.write("Supply 'program_name' parameter.")109
84 request.finish()110 res_code = RESPONSE_CODE_OK
85 defer.returnValue(None)111 res_msg = None # set this in the logic
86112
87 name = request.args["program_name"][0]113 #
88114 # Get program name and database connection, check if program exists
89 try:115 #
90 suppress_common = request.args["suppress_common"][0]116
91 except KeyError:117 name = args.get('program_name', [None])[0]
92 suppress_common = False118
93119 if "program_name" is None:
94 if suppress_common == 'null' or suppress_common == 'true':120 res_code = RESPONSE_CODE_BAD_REQUEST
95 suppress_common = True121 res_msg = "Supply 'program_name' parameter."
96 else:122
97 suppress_common = False123 if res_code is RESPONSE_CODE_OK:
98124 try:
99 try:125 conn = yield deferToThread(self.create_neo4j_connection)
100 neo4jconnection = yield deferToThread(self.create_neo4j_connection)126 except requests.exceptions.ConnectionError:
101 except requests.exceptions.ConnectionError:127 res_code = RESPONSE_CODE_BAD_GATEWAY
102 request.setResponseCode(502) # Bad Gateway128 res_fmt = "Could not reach Neo4j server at {}"
103 request.write("Could not reach Neo4j server at {}".format(database_url))129 res_msg = res_fmt.format(database_url)
104 request.finish()130 conn = None
105 defer.returnValue(None)131
106 neo4jconnection = None # to silence the "referenced before assignment" warnings later132 if res_code is RESPONSE_CODE_OK:
107133 exists = yield deferToThread(self.check_program_exists, conn, name)
108 exists = yield deferToThread(self.check_program_exists, neo4jconnection, name)134 if not exists:
109 if not exists:135 res_code = RESPONSE_CODE_NOT_FOUND
110 request.setResponseCode(404)136 res_fmt = "Program {} not found in database."
111 logging.debug('returning nonexistent')137 res_msg = res_fmt.format(escape(name))
112 request.write("Name %s not found." % (escape(name)))138
113 request.finish()139 #
114 defer.returnValue(None)140 # We have a connection and a valid program - now setup the query
115141 #
116 allowed_queries = ("whole_program", "functions_calling", "functions_called_by", "all_call_paths", "shortest_call_path")142
117143 # We store query info as:
118 if "query" not in request.args:144 # <query_name>: (<function>, (<known args>), (<kwargs>)
119 query = "whole_program"145 # where <known args> are local variables and <kwargs> are the keys we
120 else:146 # look for in request.args, both tuples
121 query = request.args["query"][0]147 queries = {
122148 'whole_program': (
123 if query not in allowed_queries:149 self.get_whole_program,
124 # raise 400 Bad Request error150 (conn, name),
125 request.setResponseCode(400)151 ()
126 request.write("Supply 'query' parameter, default is whole_program, allowed %s." % str(allowed_queries))152 ),
127 request.finish()153 'functions_calling': (
128 defer.returnValue(None)154 self.get_functions_calling,
129155 (conn, name),
130 if query == 'whole_program':156 ('func1',)
131 program = yield deferToThread(self.get_whole_program, neo4jconnection, name)157 ),
132 elif query == 'functions_calling':158 'functions_called_by': (
133 if 'func1' not in request.args:159 conn.get_all_functions_called,
134 # raise 400 Bad Request error160 (name,),
135 request.setResponseCode(400)161 ('func1',)
136 request.write("Supply 'func1' parameter to functions_calling.")162 ),
137 request.finish()163 'all_call_paths': (
138 defer.returnValue(None)164 conn.get_call_paths,
139 func1 = request.args['func1'][0]165 (name,),
140 program = yield deferToThread(self.get_functions_calling, neo4jconnection, name, func1)166 ('func1', 'func2')
141 elif query == 'functions_called_by':167 ),
142 if 'func1' not in request.args:168 'shortest_call_path': (
143 # raise 400 Bad Request error169 conn.get_shortest_path_between_functions,
144 request.setResponseCode(400)170 (name,),
145 request.write("Supply 'func1' parameter to functions_called_by.")171 ('func1', 'func2')
146 request.finish()172 )
147 defer.returnValue(None)173 }
148 func1 = request.args['func1'][0]174
149 program = yield deferToThread(neo4jconnection.get_all_functions_called, name, func1)175 # default to whole_program if query is in request.args but has no value
150 elif query == 'all_call_paths':176 query_name = args.get('query', ['whole_program'])[0]
151 if 'func1' not in request.args:177
152 # raise 400 Bad Request error178 # check for an invalid query
153 request.setResponseCode(400)179 query = queries.get(query_name, None)
154 request.write("Supply 'func1' parameter to all_call_paths.")180
155 request.finish()181 if query is None:
156 defer.returnValue(None)182 # we do not have a query function for this query name
157 if 'func2' not in request.args:183 res_code = RESPONSE_CODE_BAD_REQUEST
158 # raise 400 Bad Request error184 res_fmt = "Invalid 'query' parameter, allowed: {}"
159 request.setResponseCode(400)185 res_msg = res_fmt.format(queries.keys())
160 request.write("Supply 'func2' parameter to call_paths.")186
161 request.finish()187 # extract any required keyword arguments from request.args
162 defer.returnValue(None)188 if res_code is RESPONSE_CODE_OK:
163189 fn, known_args, kwargs = query
164 func1 = request.args['func1'][0]190
165 func2 = request.args['func2'][0]191 # all args will be strings - use None to indicate missing argument
166 program = yield deferToThread(neo4jconnection.get_call_paths, name, func1, func2)192 req_args = tuple(args.get(kwarg, [None])[0] for kwarg in kwargs)
167 elif query == 'shortest_call_path':193 missing_args = [kwarg for (kwarg, req_arg) in zip(kwargs, req_args)
168 if 'func1' not in request.args:194 if req_arg is None]
169 # raise 400 Bad Request error195
170 request.setResponseCode(400)196 if missing_args:
171 request.write("Supply 'func1' parameter to shortest_call_path.")197 # missing a kwarg from request.args
172 request.finish()198 res_code = RESPONSE_CODE_BAD_REQUEST
173 defer.returnValue(None)199 res_fmt = "Missing arguments {} to {}."
174 if 'func2' not in request.args:200 res_msg = res_fmt.format(', '.join(missing_args), name)
175 # raise 400 Bad Request error201
176 request.setResponseCode(400)202 # if we are okay here we have a valid query with all required arguments
177 request.write("Supply 'func2' parameter to shortest_call_path.")203 if res_code is RESPONSE_CODE_OK:
178 request.finish()204 try:
179 defer.returnValue(None)205 all_args = known_args + req_args
180206 program = yield defer_to_thread_with_timeout(render_timeout, fn,
181 func1 = request.args['func1'][0]207 *all_args)
182 func2 = request.args['func2'][0]208 except defer.CancelledError:
183 program = yield deferToThread(neo4jconnection.get_shortest_path_between_functions, name, func1, func2)209 # the timeout has fired and cancelled the request
184210 res_code = RESPONSE_CODE_BAD_REQUEST
185 else: # unrecognised query, so we need to raise a Bad Request response211 res_fmt = "The request timed out after {} seconds."
186 request.setResponseCode(400)212 res_msg = res_fmt.format(render_timeout)
187 request.write("Query %s not recognised." % escape(query))213
188 request.finish()214 if res_code is RESPONSE_CODE_OK:
189 defer.returnValue(None)215 # we have received a response to our request
190 program = None # silences the "referenced before assignment" warnings216 if program is None:
191217 res_code = RESPONSE_CODE_NOT_FOUND
192 if program is None:218 res_fmt = ("At least one of the input functions '{}' was not "
193 request.setResponseCode(404)219 "found in program {}.")
194 request.write("At least one of the input functions was not found in program %s." % (escape(name)))220 res_msg = res_fmt.format(', '.join(req_args), escape(name))
195 request.finish()221 elif not program.functions:
196 defer.returnValue(None)222 res_code = RESPONSE_CODE_NO_CONTENT
197223 res_fmt = ("The program {} is in the database but has no "
198 logging.debug('getting plot')224 "functions.")
199 if not program.functions: # we got an empty program back: the program is in the Sextant but has no functions225 res_msg = res_fmt.format(name)
200 request.setResponseCode(204)226
201 request.finish()227 if res_code is RESPONSE_CODE_OK:
202 defer.returnValue(None)228 # True if 'null' or 'true', otherwise False
203 output = yield deferToThread(self.get_plot, program, suppress_common,229 suppress_common_arg = args.get('suppress_common', ['false'])[0]
204 remove_self_calls=False)230 suppress_common = suppress_common_arg in ('null', 'true')
205 request.setHeader("content-type", "image/svg+xml")231
206232 # we have a non-empty return - render it
207 logging.debug('SVG: return')233 res_msg = yield deferToThread(self.get_plot, program,
208 request.write(output)234 suppress_common,
235 remove_self_calls=False)
236 request.setHeader('content-type', 'image/svg+xml')
237
238 request.setResponseCode(res_code)
239 request.write(res_msg)
209 request.finish()240 request.finish()
210241
211 def render_GET(self, request):242 def render_GET(self, request):

Subscribers

People subscribed via source and target branches