88# a top-level import componentize-py won't include the encoding in our final artifact
99# and we get runtime LookupErrors when werkzeug tries to use the codec.
1010import encodings .idna # noqa: F401
11+ import gc
1112import sys
1213import traceback
14+ import weakref
1315from collections .abc import Callable
1416from typing import Any
1517from urllib .parse import urlparse
1618
1719from wit_world .exports import HttpIncoming as WitHttpIncoming
18- from wit_world .imports import http_body , http_resp
19- from wit_world .imports .http_downstream import (
20- NextRequestOptions ,
21- await_request ,
22- next_request ,
23- )
24- from wit_world .imports .http_req import send
25- from wit_world .imports .http_resp import send_downstream
20+ from wit_world .imports import async_io , http_body , http_downstream , http_req , http_resp
2621from wit_world .imports .types import Err , Error_CannotRead
2722
2823
@@ -94,7 +89,7 @@ def start_response(
9489 write (body_chunk )
9590
9691 # Send the complete response downstream
97- send_downstream (response , response_body )
92+ http_resp . send_downstream (response , response_body )
9893
9994 except Exception as e :
10095 if not handle_errors :
@@ -110,7 +105,25 @@ def start_response(
110105 error_response .append_header ("content-type" , b"text/plain" )
111106 error_message = f"Internal Server Error: { e } "
112107 http_body .write (error_body , error_message .encode (), http_body .WriteEnd .BACK )
113- send_downstream (error_response , error_body )
108+ http_resp .send_downstream (error_response , error_body )
109+
110+
111+ def _await_request (pending_request : async_io .Pollable ) -> None | (
112+ http_req .Request ,
113+ async_io .Pollabe ,
114+ ):
115+ try :
116+ return http_downstream .await_request (pending_request )
117+ except Err as exc :
118+ # TODO: Improve error design so we can catch only the exceptions
119+ # we're really interested in, per Python's idiom. Rather than
120+ # carting around a Result type that's Union[Ok[T], Err[E]], we
121+ # should probably return T xor raise E.
122+ if isinstance (exc .value , Error_CannotRead ):
123+ return None
124+ else :
125+ # Something went wrong.
126+ raise
114127
115128
116129class WsgiHttpIncoming (WitHttpIncoming ):
@@ -140,6 +153,7 @@ def __init__(
140153 wsgi_app : Callable ,
141154 handle_errors : bool = False ,
142155 reuse_sandboxes_for_ms : int = 0 ,
156+ assert_on_uncollected_resource : bool = False ,
143157 ):
144158 """Construct.
145159
@@ -148,72 +162,70 @@ def __init__(
148162 500-status response.
149163 :arg reuse_sandboxes_for_ms: If non-0, keep the sandbox alive for this
150164 many milliseconds to potentially serve additional requests.
165+ :arg assert_on_uncollected_resource: If True, if there are references
166+ to request that linger past that request being handled, an assertion
167+ will be raised (when reusing connections). If False, connection
168+ reuse will be disabled if this occurs.
151169 """
152170 self .wsgi_app = wsgi_app
153171 self .handle_errors = handle_errors
154172 self .reuse_sandboxes_for_ms = reuse_sandboxes_for_ms
173+ self .assert_on_uncollected_resource = assert_on_uncollected_resource
155174
156175 def __call__ (self ):
157176 return self
158177
159- def handle (self , request : Any , body : Any ) -> None :
160- """Handle incoming HTTP requests by serving them through the WSGI app."""
178+ def _handle_single_request (self , request : http_req .Request , body : Any ) -> None :
161179 serve_wsgi_request (
162180 request ,
163181 body ,
164182 self .wsgi_app ,
165183 handle_errors = self .handle_errors ,
166184 )
167185
168- if not self .reuse_sandboxes_for_ms :
169- return
170-
171- try :
172- # Drop (in the WIT sense) the `request` resource to get ready for
173- # another request. Otherwise, we crash.
174- #
175- # Here we abuse an arbitrary request-consuming function to trigger
176- # the drop. Glue code interposed by wasmtime's linker ensures that
177- # drop happens, but send() otherwise fails before doing anything.
178- send (request , body , "no such backend" )
179-
180- # TODO: Generate a proper drop_whatever() function for each
181- # "whatever" resource.
182- #
183- # Alternately, it might suffice for the runtime to drop() things
184- # that get GC'd (i.e. `del` or otherwise) by Python. If we put an
185- # idiomatic .close() or similar on, for example, a potentially large
186- # request body, we could implement it in terms of `del`.
187- except Err :
188- pass
189- else :
190- raise RuntimeError (
191- "Our use of send() to consume the previous request unexpectedly actually performed a send."
192- )
193-
194- options = NextRequestOptions (timeout_ms = self .reuse_sandboxes_for_ms , extra = None )
186+ def _attempt_reuse (self ):
187+ options = http_downstream .NextRequestOptions (
188+ timeout_ms = self .reuse_sandboxes_for_ms , extra = None
189+ )
195190 while True :
196- pending_request = next_request (options )
197- try :
198- result = await_request (pending_request )
199- except Err as exc :
200- # TODO: Improve error design so we can catch only the exceptions
201- # we're really interested in, per Python's idiom. Rather than
202- # carting around a Result type that's Union[Ok[T], Err[E]], we
203- # should probably return T xor raise E.
204- if isinstance (exc .value , Error_CannotRead ):
205- # There were no more requests within the timeout.
206- break
207- else :
208- # Something went wrong.
209- raise
191+ # See if there's another request incoming
192+ pending_request = http_downstream .next_request (options )
193+ maybe_req_body = _await_request (pending_request )
194+ if not maybe_req_body :
195+ break
196+ request , body = maybe_req_body
197+ reqwref = weakref .ref (request )
198+ with request as request , body as body :
199+ maybe_req_body = None
200+ self ._handle_single_request (request , body )
201+ del request
202+ del body
203+
204+ req_dropped = reqwref () is None
205+ if not req_dropped :
206+ # force a gc in case it is in a cycle; this may not be optimal
207+ # but worth a shot if the user is trying to make enable
208+ # reuse.
209+ gc .collect ()
210+ req_dropped = reqwref () is None
211+
212+ if self .assert_on_uncollected_resource :
213+ assert req_dropped , "Request outlives handler!"
210214 else :
211- if not result :
215+ if not req_dropped :
216+ # TODO: use proper logging at some point
217+ print (
218+ "Request remained referenced past WSGI handler, not doing connection reuse"
219+ )
212220 break
213- request , body = result
214- serve_wsgi_request (
215- request ,
216- body ,
217- self .wsgi_app ,
218- handle_errors = self .handle_errors ,
219- )
221+
222+ def handle (self , request : Any , body : Any ) -> None :
223+ """Handle incoming HTTP requests by serving them through the WSGI app."""
224+ # always handle the first request coming in normally
225+ self ._handle_single_request (request , body )
226+ del request
227+ del body
228+
229+ # then attempt reuse if configured
230+ if self .reuse_sandboxes_for_ms :
231+ return self ._attempt_reuse ()
0 commit comments