Skip to main content

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}