fastly/http/serve.rs
1//! Support for reusable sandboxes.
2//!
3//! Normally, each incoming HTTP request spins up a new Compute instance.
4//! This provides good reproducibility properties... but it means that if an instance has expensive
5//! initialization, that initialization must be repeated for each request.
6//! If a service has an expensive initialization routine, reusable instances can provide an
7//! efficiency improvement (and a cost improvement!)
8//!
9//! ## Timeouts
10//!
11//! A Compute instance is not a general-purpose server; it does not and should not indefinitely
12//! wait for new requests. Therefore, the time spent waiting for a new request is bounded in
13//! three different ways.
14//!
15//! - At the platform level, there is a maximum time over which a `RequestPromise` can be
16//! unfulfilled. The timer starts when the `RequestPromise` is created.
17//! After this timeout, `RequestPromise::wait` returns
18//! `NextRequestError::EndOfSession`, and the promise is no longer valid.
19//!
20//! This timeout is not publicly disclosed, and may change.
21//!
22//! - When a `RequestPromise` is created, the promise-level timeout can be configured
23//! in `NextRequestOptions::timeout`. The effective promise-level timeout is the minimum of the
24//! provided value and the platform-level timeout.
25//!
26//! - Separately from the promise-level timeout, `wait_timeout` can be used to time out a
27//! particular `wait` call. If `wait_timeout` times out while the promise is still valid,
28//! `wait_timeout` will return `RequestPromise::Timeout`, indicating the promise can be retried.
29//!
30//! ## Billing
31//!
32//! When a WASM instance waits on a `RequestPromise`, it continues to consume memory and wall-clock
33//! time. The memory is only freed when the WASM instance exits, as usual.
34//!
35//! If your service is billed by WASM memory * wall-clock time, you will be billed for the usage while
36//! waiting for a `RequestPromise`, even if the `RequestPromise` does not resolve to a request
37//! (i.e. times out). You can limit this usage by timing out the `RequestPromise`, as described
38//! above.
39//!
40//! A WASM instance that is blocked waiting on a `RequestPromise` does not consume vCPU time.
41//!
42//! ## Examples
43//!
44//! ```no_run
45//! # use fastly::http::serve::Serve;
46//! # use fastly::{Error, Response, Request};
47//! fn handler(_req: Request) -> Result<Response, Error> {
48//! Ok(Response::from_body("hello")
49//! .with_header("hello", "world!")
50//! .with_status(200))
51//! }
52//!
53//! fn main() -> Result<(), Error> {
54//! Serve::new()
55//! .with_max_requests(5)
56//! .run(handler)
57//! .into_result()
58//! }
59//! ```
60use std::time::{Duration, Instant};
61
62use fastly_shared::{FastlyStatus, INVALID_REQUEST_PROMISE_HANDLE};
63use fastly_sys::{fastly_http_downstream::NextRequestOptionsMask, BodyHandle, RequestHandle};
64
65use crate::compute_runtime::heap_memory_snapshot_mib;
66use crate::http::request::PendingRequest;
67use crate::{Request, Response};
68
69/// Trait for supporting more flexible return types for the handler passed to [Serve::run].
70///
71/// The types that this is implemented for by default are:
72///
73/// - `Response`, for a handler that always generates a response.
74/// - `()`, for a handler that has already called either [Response::send_to_client] or
75/// [Response::stream_to_client].
76/// - `Result<(), E>`, for a handler that has already called either [Response::send_to_client] or
77/// [Response::stream_to_client], but might fail with an error of type `E` along the way.
78/// - `Result<Response, E>`, for a handle the might generate a response to send or an error.
79///
80/// For the `Result` implementations, a return value containing an error will attempt to send
81/// a 500 response status with a body containing the [ToString::to_string] representation of the
82/// error, in the same way that [fastly::main][crate::main] does.
83pub trait HandlerResult {
84 /// The error type generated by the handler.
85 type Error;
86
87 /// Perform any remaining work to send this response downstream.
88 fn send(self) -> Result<(), Self::Error>;
89}
90
91impl HandlerResult for Response {
92 type Error = std::convert::Infallible;
93
94 fn send(self) -> Result<(), Self::Error> {
95 self.send_to_client();
96 Ok(())
97 }
98}
99
100impl HandlerResult for PendingRequest {
101 type Error = std::convert::Infallible;
102
103 fn send(self) -> Result<(), Self::Error> {
104 if let Err(e) = self.send_to_client() {
105 send_internal_server_err(&e);
106 }
107
108 Ok(())
109 }
110}
111
112impl HandlerResult for () {
113 type Error = std::convert::Infallible;
114
115 fn send(self) -> Result<(), Self::Error> {
116 Ok(())
117 }
118}
119
120impl<E> HandlerResult for Result<Response, E>
121where
122 E: std::fmt::Display,
123{
124 type Error = E;
125
126 fn send(self) -> Result<(), Self::Error> {
127 match self {
128 Ok(resp) => {
129 resp.send_to_client();
130 Ok(())
131 }
132 Err(e) => {
133 send_internal_server_err(&e);
134 Err(e)
135 }
136 }
137 }
138}
139
140impl<E> HandlerResult for Result<PendingRequest, E>
141where
142 E: std::fmt::Display,
143{
144 type Error = E;
145
146 fn send(self) -> Result<(), Self::Error> {
147 match self {
148 Ok(pending) => {
149 let Ok(_) = HandlerResult::send(pending);
150 Ok(())
151 }
152 Err(e) => {
153 send_internal_server_err(&e);
154 Err(e)
155 }
156 }
157 }
158}
159
160impl<E> HandlerResult for Result<(), E>
161where
162 E: std::fmt::Display,
163{
164 type Error = E;
165
166 fn send(self) -> Result<(), Self::Error> {
167 self.inspect_err(send_internal_server_err)
168 }
169}
170
171fn send_internal_server_err<E: std::fmt::Display>(e: &E) {
172 Response::from_body(e.to_string())
173 .with_status(crate::http::StatusCode::INTERNAL_SERVER_ERROR)
174 .send_to_client();
175}
176
177/// A summary of information from running [Serve].
178pub struct ServeSummary<E> {
179 error: Option<E>,
180 requests: usize,
181 time_wait: Duration,
182 time_handler: Duration,
183}
184
185impl<E> ServeSummary<E> {
186 fn new() -> Self {
187 Self {
188 error: None,
189 requests: 0,
190 time_wait: Duration::ZERO,
191 time_handler: Duration::ZERO,
192 }
193 }
194
195 /// Returns how many requests were processed by [Serve].
196 pub fn requests(&self) -> usize {
197 self.requests
198 }
199
200 /// Returns the total amount of time that [Serve] spent running the handler callback.
201 pub fn time_handler(&self) -> Duration {
202 self.time_handler
203 }
204
205 /// Returns the total amount of time that [Serve] spent waiting for another request.
206 pub fn time_waited(&self) -> Duration {
207 self.time_wait
208 }
209
210 /// Inspect the error returned by the handler that caused [Serve] to stop.
211 pub fn error(&self) -> Option<&E> {
212 self.error.as_ref()
213 }
214
215 /// Convert this summary into a [Result] containing the handler error that stopped [Serve].
216 pub fn into_result(self) -> Result<(), E> {
217 self.error.map_or(Ok(()), Err)
218 }
219
220 fn record_handler(&mut self, started: Instant) {
221 self.time_handler += started.elapsed();
222 }
223
224 fn record_wait(&mut self, started: Instant) {
225 self.time_wait += started.elapsed();
226 }
227}
228
229/// Support for processing multiple requests from a single sandbox.
230pub struct Serve {
231 created: Instant,
232 max_lifetime: Duration,
233 max_memory: u32,
234 max_requests: usize,
235 timeout: Duration,
236}
237
238impl Serve {
239 /// Prepare a new [Serve] instance for handling requests in this sandbox.
240 pub fn new() -> Self {
241 Self {
242 created: Instant::now(),
243 max_lifetime: Duration::MAX,
244 max_memory: 0,
245 max_requests: 0,
246 timeout: Duration::MAX,
247 }
248 }
249
250 /// Configure when to stop accepting additional requests.
251 ///
252 /// Defaults to [Duration::MAX].
253 pub fn with_max_lifetime(mut self, limit: Duration) -> Self {
254 self.max_lifetime = limit;
255 self
256 }
257
258 /// Configure the maximum amount of memory (in mebibytes) to allow this instance
259 /// to use before stopping, as reported by [heap_memory_snapshot_mib].
260 ///
261 /// A `limit` of `0` (the default) will be treated as unlimited.
262 pub fn with_max_memory(mut self, mib: u32) -> Self {
263 self.max_memory = mib;
264 self
265 }
266
267 /// Configure the maximum number of requests to serve before stopping.
268 ///
269 /// A `limit` of `0` (the default) will be treated as unlimited.
270 pub fn with_max_requests(mut self, limit: usize) -> Self {
271 self.max_requests = limit;
272 self
273 }
274
275 /// Configure how long to wait for another request before stopping.
276 ///
277 /// Defaults to [Duration::MAX].
278 pub fn with_timeout(mut self, timeout: Duration) -> Self {
279 self.timeout = timeout;
280 self
281 }
282
283 /// Begin serving requests in this sandbox.
284 pub fn run<F, MaybeResult>(self, mut handler: F) -> ServeSummary<MaybeResult::Error>
285 where
286 F: FnMut(Request) -> MaybeResult,
287 MaybeResult: HandlerResult,
288 {
289 self.run_with_context(move |r, ()| handler(r), &mut ())
290 }
291
292 /// Begin serving requests in this sandbox, but with a contextual argument passed
293 /// to every invocation of the handler callback.
294 ///
295 /// The callback should return a type that implements [HandlerResult], such as:
296 ///
297 /// - `Response`, if it always returns a response.
298 /// - `Result<Response, E>`, if it might fail with an error of type `E` instead of generating a response
299 pub fn run_with_context<F, C, MaybeResult>(
300 self,
301 mut handler: F,
302 cx: &mut C,
303 ) -> ServeSummary<MaybeResult::Error>
304 where
305 F: FnMut(Request, &mut C) -> MaybeResult,
306 MaybeResult: HandlerResult,
307 {
308 let options = NextRequestOptions::default().with_timeout(self.timeout);
309 let mut summary = ServeSummary::new();
310 let mut req = Request::from_client();
311
312 loop {
313 summary.requests += 1;
314
315 let start_handler = Instant::now();
316 let resp = handler(req, cx);
317 summary.record_handler(start_handler);
318
319 if let Err(e) = resp.send() {
320 summary.error = Some(e);
321 break;
322 }
323
324 // Check if we've hit the user-defined request limit, and stop if so:
325 if self.max_requests > 0 && summary.requests >= self.max_requests {
326 break;
327 }
328
329 // Check if we've hit the user-defined lifetime limit, and stop if so:
330 if self.created.elapsed() >= self.max_lifetime {
331 break;
332 }
333
334 // Check if we've hit the user-defined memory limit, and stop if so. If
335 // we are unable to read our memory usage, error on the side of caution
336 // and assume that it's too large for us to continue.
337 if self.max_memory > 0 {
338 let usage = heap_memory_snapshot_mib().unwrap_or(u32::MAX);
339
340 if usage > self.max_memory {
341 break;
342 }
343 }
344
345 let start_wait = Instant::now();
346
347 let next = match RequestPromise::new(&options) {
348 Ok(p) => p,
349 Err(e) => {
350 // Failed to register a promise, which should never happen unless
351 // there is already an existing handle! Something has clearly gone
352 // wrong in the program, so panic.
353 panic!("failed to register promise for receiving another request: {e}");
354 }
355 };
356
357 let res = next.wait();
358 summary.record_wait(start_wait);
359
360 if let Ok(r) = res {
361 req = r;
362 } else {
363 break;
364 }
365 }
366
367 summary
368 }
369}
370
371impl Default for Serve {
372 fn default() -> Self {
373 Self::new()
374 }
375}
376
377#[derive(Debug)]
378struct RequestPromiseHandle(fastly_sys::RequestPromiseHandle);
379
380impl Default for RequestPromiseHandle {
381 fn default() -> Self {
382 RequestPromiseHandle(INVALID_REQUEST_PROMISE_HANDLE)
383 }
384}
385
386impl From<fastly_sys::RequestPromiseHandle> for RequestPromiseHandle {
387 fn from(value: fastly_sys::RequestPromiseHandle) -> Self {
388 RequestPromiseHandle(value)
389 }
390}
391
392impl Drop for RequestPromiseHandle {
393 fn drop(&mut self) {
394 if self.0 == INVALID_REQUEST_PROMISE_HANDLE {
395 // Already invalidated.
396 return;
397 }
398 let status = unsafe { fastly_sys::fastly_http_downstream::next_request_abandon(self.0) };
399 if status != FastlyStatus::OK {
400 panic!("failed to abandon request promise: {status:?}");
401 }
402 self.0 = INVALID_REQUEST_PROMISE_HANDLE;
403 }
404}
405
406/// A promise of a future Request from a customer.
407///
408/// When .wait() is called, this will resolve to either:
409/// - a `Request`, if an additional user request has been assigned to this instance
410/// - an `Error`, if:
411/// - The current `Request` has not been completed
412/// - A limit has been reached (number of requests per instance, timeout waiting for the next
413/// request)
414#[derive(Debug)]
415pub struct RequestPromise {
416 handle: RequestPromiseHandle,
417}
418
419impl TryFrom<RequestPromiseHandle> for RequestPromise {
420 type Error = FastlyStatus;
421
422 fn try_from(handle: RequestPromiseHandle) -> Result<Self, Self::Error> {
423 if handle.0 == INVALID_REQUEST_PROMISE_HANDLE {
424 Err(FastlyStatus::BADF)
425 } else {
426 Ok(RequestPromise { handle })
427 }
428 }
429}
430
431/// Settings to use when waiting for a second (third, fourth, ...) request.
432#[derive(Default)]
433pub struct NextRequestOptions {
434 /// How long the promise will wait for a new request.
435 ///
436 /// This timer starts at the creation of the RequestPromise.
437 /// If you want to bound how long a given .wait call lasts, use .wait_timeout.
438 timeout: Option<Duration>,
439}
440
441impl NextRequestOptions {
442 /// Set a bound on how long a RequestPromise can be outstanding.
443 ///
444 /// This timer starts at the creation of the RequestPromise.
445 /// After the timeout, if not request is ready, the RequestPromise will report
446 /// [NextRequestError::EndOfSession].
447 ///
448 /// If you want to bound how long a given .wait call lasts, use .wait_timeout.
449 pub fn with_timeout(mut self, timeout: Duration) -> Self {
450 self.timeout = Some(timeout);
451 self
452 }
453
454 /// Shorthand for `default().with_timeout(timeout)`;
455 /// see [NextRequestOptions::with_timeout].
456 pub fn from_timeout(timeout: Duration) -> Self {
457 Self {
458 timeout: Some(timeout),
459 }
460 }
461}
462
463impl From<&NextRequestOptions>
464 for (
465 fastly_sys::fastly_http_downstream::NextRequestOptionsMask,
466 fastly_sys::fastly_http_downstream::NextRequestOptions,
467 )
468{
469 fn from(value: &NextRequestOptions) -> Self {
470 let mut options = fastly_sys::fastly_http_downstream::NextRequestOptions::default();
471 let mut mask = fastly_sys::fastly_http_downstream::NextRequestOptionsMask::default();
472
473 if let Some(timeout) = value.timeout {
474 options.timeout_ms = timeout.as_millis().try_into().unwrap_or(u64::MAX);
475 mask |= NextRequestOptionsMask::TIMEOUT;
476 }
477
478 (mask, options)
479 }
480}
481
482/// Errors that can occur when asking for or resolving a RequestPromise.
483#[derive(Debug, thiserror::Error)]
484pub enum NextRequestError {
485 /// No request was available, but one may become available in the future.
486 ///
487 /// Note that the RequestPromise is still valid, and may provide a request in the
488 /// future. If you no longer intend to accept an additional request, drop the RequestPromise
489 /// promptly after receiving the timeout.
490 #[error("no request available (yet)")]
491 Timeout(RequestPromise),
492
493 /// No request is available because the response for the current request has not completed.
494 ///
495 /// The response for the current request must be completed before a promise can be fulfilled.
496 // How can you have any pudding if you don't eat your meat?
497 #[error("current response is not yet finished")]
498 OutstandingResponse,
499
500 /// No more requests will be delivered to this sandbox.
501 ///
502 /// This can occur if the RequestPromise has exceeded its overall timeout or if another limit
503 /// has been reached.
504 #[error("no future requests available for this instance")]
505 EndOfSession,
506
507 /// Any other error.
508 #[error("error while waiting for next request: {0:?}")]
509 Other(FastlyStatus),
510}
511
512impl RequestPromise {
513 /// Create a new promise for receiving a subsequent downstream request.
514 pub fn new(reuse_settings: &NextRequestOptions) -> Result<RequestPromise, NextRequestError> {
515 let (options_mask, options) = reuse_settings.into();
516 let mut handle = RequestPromiseHandle::default();
517 let status = unsafe {
518 fastly_sys::fastly_http_downstream::next_request(
519 options_mask,
520 &options as *const fastly_sys::fastly_http_downstream::NextRequestOptions,
521 &mut handle.0,
522 )
523 };
524 if status == FastlyStatus::OK {
525 Ok(handle.try_into().unwrap())
526 } else {
527 Err(NextRequestError::Other(status))
528 }
529 }
530
531 /// Wait for the next request, up to the timeout provided here.
532 ///
533 /// The timeout here represents how long this method waits, and not whether the handle
534 /// has become invalid. If a timeout of zero is provided, this acts as a poll-once, and
535 /// returns immediately with [NextRequestError::Timeout] if there is not a request ready.
536 ///
537 /// Note that if the timeout is reached, this instance is still registered to receive a future
538 /// request; the RequestPromise can be recovered from the [NextRequestError::Timeout]
539 /// and another `wait_timeout` or `wait` call may be attempted on it.
540 ///
541 /// If this instance needs to shut down, dropping the [RequestPromise] or
542 /// [NextRequestError] will abandon the promise and avoid queueing future requests for
543 /// the session.
544 ///
545 /// Note that the timeout has an unspecified minimum resolution.
546 /// If a nonzero timeout smaller than the minimum resolution is provided, the time will be
547 /// rounded up to this resolution.
548 pub fn wait_timeout(self, timeout: Duration) -> Result<Request, NextRequestError> {
549 // "borrow" the handle without deconstructing it;
550 // we are just checking its readiness.
551 let raw = self.handle.0;
552
553 // async_io::select uses "timeout of zero" to mean "wait indefinitely".
554 // To provide "timeout at 0" behavior to the callers here,
555 // we use `is_ready` instead of `select` if the timeout is zero.
556 if timeout.is_zero() {
557 let mut ready = 0u32;
558 let status = unsafe { fastly_sys::fastly_async_io::is_ready(raw, &mut ready) };
559
560 match (status, ready != 0) {
561 // No errors, reported ready
562 (e, true) if e.is_ok() => (),
563 // No errors, reported nonready: it's OK to try again with this handle
564 (e, false) if e.is_ok() => return Err(NextRequestError::Timeout(self)),
565 // Some other error: pass it through.
566 // Note that this will drop the RequestPromise. The types reflect this, preventing
567 // re-use.
568 (e, _) => return Err(NextRequestError::Other(e)),
569 }
570 } else {
571 // If asked for a sub-millisecond time, round up to 1ms for the _select_ call.
572 let millis = timeout.as_millis().try_into().unwrap_or(u32::MAX).max(1);
573
574 let raw_handles = [raw];
575 let mut done_index = 0u32;
576
577 let status = unsafe {
578 fastly_sys::fastly_async_io::select(
579 &raw_handles as *const u32,
580 raw_handles.len(),
581 millis,
582 &mut done_index as *mut u32,
583 )
584 };
585
586 match status {
587 FastlyStatus::OK if done_index == 0 => (),
588 FastlyStatus::OK => return Err(NextRequestError::Timeout(self)),
589 e => return Err(NextRequestError::Other(e)),
590 }
591 };
592
593 // We have an indicator that the request is ready, so this won't block.
594 self.wait()
595 }
596
597 /// Wait for the next request.
598 ///
599 /// This returns if a next request is available; if an error occurs; or if the RequestPromise
600 /// exceeds its deadline.
601 pub fn wait(self) -> Result<Request, NextRequestError> {
602 // Invalidate the wrapper, since next_request_wait consumes the handle on success.
603 let RequestPromise { mut handle } = self;
604 let raw = handle.0;
605 handle.0 = INVALID_REQUEST_PROMISE_HANDLE;
606
607 let mut req_handle = RequestHandle::default();
608 let mut body_handle = BodyHandle::default();
609 let status = unsafe {
610 fastly_sys::fastly_http_downstream::next_request_wait(
611 raw,
612 &mut req_handle,
613 &mut body_handle,
614 )
615 };
616 match status {
617 FastlyStatus::OK => Ok(Request::from_client_handles(req_handle, body_handle)),
618 FastlyStatus::UNSUPPORTED => Err(NextRequestError::OutstandingResponse),
619 FastlyStatus::NONE => Err(NextRequestError::EndOfSession),
620 e => Err(NextRequestError::Other(e)),
621 }
622 }
623}