wasmtime_wasi_http/handler.rs
1//! Provides utilities useful for dispatching incoming HTTP requests
2//! `wasi:http/handler` guest instances.
3
4#[cfg(feature = "p2")]
5use crate::p2;
6#[cfg(feature = "p2")]
7use crate::p2::bindings::http::types as p2_types;
8#[cfg(feature = "p3")]
9use crate::p3;
10use crate::{WasiBody, WasiHttpCtxView};
11use futures::{
12 channel::oneshot,
13 future::{Either, FutureExt},
14 stream::{FuturesUnordered, Stream},
15};
16#[cfg(feature = "p3")]
17use p3::bindings::http::types as p3_types;
18use std::collections::VecDeque;
19use std::collections::btree_map::{BTreeMap, Entry};
20use std::error;
21use std::fmt;
22use std::future;
23use std::mem;
24use std::ops::DerefMut;
25use std::pin::{Pin, pin};
26use std::sync::{
27 Arc, Mutex,
28 atomic::{AtomicBool, AtomicUsize, Ordering::Relaxed},
29};
30use std::task::{Context, Poll};
31use std::time::Instant;
32use tokio::sync::Notify;
33use wasmtime::component::{Accessor, Resource, TaskGroupId, TypedFuncCallConcurrent};
34#[cfg(feature = "p2")]
35use wasmtime::error::Context as _;
36use wasmtime::{AsContextMut, Result, Store, StoreContextMut, format_err};
37
38/// A Request to be handled using `ProxyHandler::handle`.
39pub type Request = http::Request<WasiBody>;
40
41/// A Response returned by `ProxyHandler::handle`.
42pub type Response = http::Response<WasiBody>;
43
44/// Represents either a `wasi:http/incoming-handler@0.2.x` or
45/// `wasi:http/handler@0.3.x` pre-instance.
46pub enum ProxyPre<T: 'static> {
47 /// A `wasi:http/incoming-handler@0.2.x` pre-instance.
48 #[cfg(feature = "p2")]
49 P2(p2::bindings::ProxyPre<T>),
50 /// A `wasi:http/handler@0.3.x` pre-instance.
51 #[cfg(feature = "p3")]
52 P3(p3::bindings::ServicePre<T>),
53}
54
55impl<T: 'static> ProxyPre<T> {
56 /// Instantiates the pre-instance.
57 pub async fn instantiate_async(&self, store: impl AsContextMut<Data = T>) -> Result<Proxy>
58 where
59 T: Send,
60 {
61 Ok(match self {
62 #[cfg(feature = "p2")]
63 Self::P2(pre) => Proxy::P2(pre.instantiate_async(store).await?),
64 #[cfg(feature = "p3")]
65 Self::P3(pre) => Proxy::P3(pre.instantiate_async(store).await?),
66 })
67 }
68}
69
70/// Represents either a `wasi:http/incoming-handler@0.2.x` or
71/// `wasi:http/handler@0.3.x` instance.
72pub enum Proxy {
73 /// A `wasi:http/incoming-handler@0.2.x` instance.
74 #[cfg(feature = "p2")]
75 P2(p2::bindings::Proxy),
76 /// A `wasi:http/handler@0.3.x` instance.
77 #[cfg(feature = "p3")]
78 P3(p3::bindings::Service),
79}
80
81/// Async MPMC channel where each item is delivered to at most one consumer.
82struct Queue<T> {
83 queue: Mutex<VecDeque<T>>,
84 notify_push: Notify,
85}
86
87impl<T> Default for Queue<T> {
88 fn default() -> Self {
89 Self {
90 queue: Default::default(),
91 notify_push: Default::default(),
92 }
93 }
94}
95
96impl<T> Queue<T> {
97 fn is_empty(&self) -> bool {
98 self.queue.lock().unwrap().is_empty()
99 }
100
101 fn try_pop(&self) -> Option<T> {
102 self.queue.lock().unwrap().pop_front()
103 }
104
105 async fn pop(&self) -> T {
106 // This code comes from the Unbounded MPMC Channel example in [the
107 // `tokio::sync::Notify`
108 // docs](https://docs.rs/tokio/latest/tokio/sync/struct.Notify.html).
109
110 let mut notified = pin!(self.notify_push.notified());
111
112 loop {
113 notified.as_mut().enable();
114 if let Some(item) = self.try_pop() {
115 return item;
116 }
117 notified.as_mut().await;
118 notified.set(self.notify_push.notified());
119 }
120 }
121}
122
123/// Represents the status of a `ProxyHandler` worker task.
124#[derive(Clone, Copy, Eq, PartialEq, Debug)]
125pub enum WorkerStatus {
126 /// The worker is not handling any requests, nor is it doing any post-return
127 /// work. It _might_ be doing background work which the guest has indicated
128 /// can be interrupted and/or abandoned at any time, i.e. does not prevent
129 /// the instance from being disposed.
130 Idle,
131 /// The instance is handling one or more requests, waiting for each to
132 /// either produce a response or expire.
133 Requests,
134 /// All requests handled so far have either produced a response or expired,
135 /// but the guest has post-return work which needs to finish before the
136 /// instance can be considered idle.
137 PostReturn,
138}
139
140/// Represents the application-specific state of a `ProxyHandler` worker.
141///
142/// [`HandlerState::instantiate`] returns an implementation of this trait for
143/// each component instance (and thus each worker) created. The worker uses it
144/// to determine when to exit.
145pub trait WorkerExpiration: 'static + Send + Sync {
146 /// Poll whether the worker has expired.
147 ///
148 /// This will return `Poll::Ready(())` if the worker has expired, meaning
149 /// the component instance should be dropped. Otherwise, it will return
150 /// `Poll::Pending` and wake the `Waker` if and when it should be polled
151 /// again.
152 ///
153 /// `state` represents the current state of the worker, and `start`
154 /// represents when it transitioned into that state (or in the case of
155 /// `WorkerState::Requests`, when the most recent outstanding request
156 /// was accepted).
157 fn poll(
158 self: Pin<&mut Self>,
159 cx: &mut Context<'_>,
160 state: WorkerStatus,
161 start: Instant,
162 ) -> Poll<()>;
163}
164
165/// Represents the application-specific state of a `ProxyHandler` worker.
166///
167/// [`HandlerState::instantiate`] returns an implementation of this trait for
168/// each component instance (and thus each worker) created. The worker uses it
169/// to determine how many requests to accept, how long to wait for the guest to
170/// produce responses, etc.
171pub trait WorkerState: 'static + Send + Sync {
172 /// The type of the associated data for [`Store`] belonging to this worker.
173 type StoreData: Send;
174
175 /// Opaque data that hosts can attach to requests which is threaded from
176 /// [`ProxyHandler::handle`] into [`WorkerState::on_request_start`].
177 type RequestData: Send + Sync;
178
179 /// Indicate whether the worker should accept another request given the
180 /// current number it is already handling concurrently and the total it has
181 /// handled so far.
182 fn should_accept_request(&self, concurrent_count: usize, total_count: usize) -> ShouldAccept;
183
184 /// Notification that a request has been accepted by the worker.
185 ///
186 /// This method can be used to record anything within `store`, if necessary.
187 /// If the `task-group-hook` feature is enabled, the task group
188 /// corresponding to the component-model-level async task about to be
189 /// created is additionally passed here.
190 ///
191 /// If the future returned by this function resolves before the guest has
192 /// produced a response, the request will be considered "expired" and the
193 /// original `ProxyHandler::handle` future will resolve to an
194 /// `Err(ExpirationError.into())`. In addition, the worker
195 /// will stop accepting new requests but will continue running until all
196 /// requests that have been accepted by the worker have either produced a
197 /// response or expired, at which point the state of the worker will
198 /// transition to either `WorkerState::PostReturn` or `WorkerState::Idle`.
199 ///
200 /// Note that the returned future is polled from within the
201 /// `Store::run_concurrent` event loop, and due to #11869 and #11870, it may
202 /// not be polled at all for arbitrary lengths of time. Consequently, the
203 /// `Self::Expiration` implementation (which is polled from _outside_ the
204 /// `Store::run_concurrent` event loop) must also enforce request expiration
205 /// as a second level of defence if desired.
206 ///
207 /// For example, if a request timeout of N seconds is to be enforced, the
208 /// `Self::Expiration::poll` implementation, when called with
209 /// `WorkerState::Requests` should calculate the time elapsed since the most
210 /// recent outstanding request was accepted as indicated by the `start`
211 /// parameter. If that time is greater than N seconds, we can expire the
212 /// instance immediately, confident that all outstanding requests have
213 /// expired.
214 ///
215 /// Once #11869 and #11870 have been addressed, this "second level of
216 /// defence" will no longer be necessary.
217 fn on_request_start(
218 &self,
219 store: StoreContextMut<'_, Self::StoreData>,
220 data: Self::RequestData,
221 task_group: TaskGroupId,
222 ) -> Pin<Box<dyn Future<Output = ()> + 'static + Send + Sync>>;
223
224 /// Dispose of the store belonging to the now-exited worker.
225 ///
226 /// This may be used to e.g. collect metrics from the store or its
227 /// associated data before the store is dropped, as well as e.g. retry
228 /// failed instantiations after the store is dropped.
229 ///
230 /// If the store is being dropped due to an error (e.g. a guest trap or a
231 /// host panic) `result` will be `Err(_)`; otherwise it will be `Ok(())`.
232 fn drop(&self, store: Store<Self::StoreData>, result: Result<(), wasmtime::Error>);
233
234 /// Poll the specified future representing the event loop for the store to
235 /// which the worker belongs.
236 fn poll<T>(&self, future: Pin<&mut impl Future<Output = T>>, cx: &mut Context) -> Poll<T> {
237 future.poll(cx)
238 }
239}
240
241/// Represents the combination of a store and instance with which to handle
242/// requests.
243pub struct Instance<T: 'static, E: WorkerExpiration, S: WorkerState> {
244 /// The store to use to handle requests.
245 pub store: Store<T>,
246 /// The instance to use to handle requests.
247 pub proxy: Proxy,
248 /// `WasiHttpCtxView` getter function.
249 pub view: fn(&mut T) -> WasiHttpCtxView<'_>,
250 /// See [`WorkerExpiration`].
251 pub expiration: E,
252 /// See [`WorkerState`].
253 pub state: S,
254}
255
256/// Indicates whether a worker should accept new requests.
257pub enum ShouldAccept {
258 /// Yes, it should.
259 Yes,
260 /// No, it shouldn't (but ask again later).
261 No,
262 /// No, it shouldn't (and don't ask again).
263 Never,
264}
265
266/// Represents the application-specific state of a web server.
267pub trait HandlerState: 'static + Sync + Send + Sized {
268 /// The type of the associated data for [`Store`]s created using
269 /// [`Self::instantiate`].
270 type StoreData: Send;
271 /// The type of the `WorkerExpiration` implementation to be returned from
272 /// [`Self::instantiate`].
273 type WorkerExpiration: WorkerExpiration;
274 /// The type of the `WorkerState` implementation to be returned from
275 /// [`Self::instantiate`].
276 type WorkerState: WorkerState<StoreData = Self::StoreData>;
277
278 /// Create a new store and instance for handling one or more requests.
279 ///
280 /// Note that the implementer is responsible for applying a timeout to the
281 /// guest instantiation if appropriate (e.g. as part of an overall request
282 /// timeout).
283 fn instantiate(
284 &self,
285 ) -> impl Future<
286 Output = Result<Instance<Self::StoreData, Self::WorkerExpiration, Self::WorkerState>>,
287 > + Send;
288}
289
290struct ProxyHandlerInner<S: HandlerState> {
291 state: S,
292 request_queue: Queue<WorkerRequest<S>>,
293 worker_count: AtomicUsize,
294}
295
296/// Tracks request start times.
297///
298/// This is useful for keeping a [`WorkerState`] appraised of the most recently
299/// accepted outstanding request.
300#[derive(Default)]
301struct StartTimes(BTreeMap<Instant, usize>);
302
303impl StartTimes {
304 fn add(&mut self, time: Instant) {
305 *self.0.entry(time).or_insert(0) += 1;
306 }
307
308 fn remove(&mut self, time: Instant) {
309 let Entry::Occupied(mut entry) = self.0.entry(time) else {
310 unreachable!()
311 };
312 match *entry.get() {
313 0 => unreachable!(),
314 1 => {
315 entry.remove();
316 }
317 _ => {
318 *entry.get_mut() -= 1;
319 }
320 }
321 }
322
323 fn most_recent(&self) -> Option<Instant> {
324 self.0.last_key_value().map(|(&k, _)| k)
325 }
326}
327
328type WorkerRequest<S> = (
329 <<S as HandlerState>::WorkerState as WorkerState>::RequestData,
330 Request,
331 oneshot::Sender<Result<Response, wasmtime::Error>>,
332);
333
334struct Worker<S>
335where
336 S: HandlerState,
337{
338 handler: ProxyHandler<S>,
339 available: bool,
340}
341
342impl<S> Worker<S>
343where
344 S: HandlerState,
345{
346 fn set_available(&mut self, available: bool) {
347 if available != self.available {
348 self.available = available;
349 if available {
350 self.handler.0.worker_count.fetch_add(1, Relaxed);
351 } else {
352 // Decrement the count _before_ checking if the request queue is
353 // empty. This helps ensure that `ProxyHandler::spawn` sees the
354 // new value before deciding whether to spawn a new worker.
355 let count = self.handler.0.worker_count.fetch_sub(1, Relaxed);
356 assert!(count >= 1);
357
358 // This addresses what would otherwise be a race condition in
359 // `ProxyHandler::spawn` where it only starts a worker if the
360 // available worker count is zero. If we decrement the count to
361 // zero right after `ProxyHandler::spawn` checks it, then no
362 // worker will be started; thus it becomes our responsibility to
363 // start a worker here instead.
364 if count == 1 && !self.handler.0.request_queue.is_empty() {
365 self.handler.start_worker(None);
366 }
367 }
368 }
369 }
370
371 async fn run(self, request: Option<WorkerRequest<S>>) {
372 match self.handler.0.state.instantiate().await {
373 Ok(Instance {
374 store,
375 proxy,
376 view,
377 expiration,
378 state,
379 }) => {
380 self.run_(store, proxy, view, expiration, state, request)
381 .await
382 }
383
384 Err(error) => {
385 let error = Arc::new(error);
386 if let Some((request_data, request, tx)) = request {
387 _ = tx.send(Err(InstantiationError {
388 request_data,
389 request: Mutex::new(request),
390 error,
391 }
392 .into()));
393 } else {
394 // In this case, the worker was spawned to handle any queued
395 // requests. Since we can't handle those requests, we send
396 // them all an instantiation error.
397 for (request_data, request, tx) in mem::take(
398 self.handler
399 .0
400 .request_queue
401 .queue
402 .lock()
403 .unwrap()
404 .deref_mut(),
405 ) {
406 _ = tx.send(Err(InstantiationError {
407 request_data,
408 request: Mutex::new(request),
409 error: error.clone(),
410 }
411 .into()));
412 }
413 }
414 }
415 }
416 }
417
418 async fn run_(
419 mut self,
420 store: Store<S::StoreData>,
421 proxy: Proxy,
422 view: fn(&mut S::StoreData) -> WasiHttpCtxView<'_>,
423 expiration: S::WorkerExpiration,
424 state: S::WorkerState,
425 request: Option<WorkerRequest<S>>,
426 ) {
427 // NB: The code the follows is rather subtle in that it is structured
428 // carefully to give the `HandlerState` implementation full control over
429 // the component instance lifetime. Specifically, we must keep the
430 // `HandlerState` informed of the worker's state and how long it has
431 // been in that state, as well as allow it to expire the instance based
432 // on whatever combination of timeouts, dynamic resource usage, etc. it
433 // may take into consideration.
434 //
435 // Note that, when more than one request is handled concurrently in the
436 // same instance, we must stop accepting new requests as soon as any
437 // existing request reaches its expiration. This serves to cap the
438 // amount of time we need to keep the instance alive before _all_
439 // requests have either completed or expired.
440 //
441 // As of this writing, there's an additional wrinkle that makes tracking
442 // expiration particularly tricky: per #11869 and #11870, busy guest
443 // loops, epoch interruption, and host functions registered using
444 // `Linker::func_{wrap,new}_async` all require blocking, exclusive
445 // access to the `Store`, which effectively prevents the
446 // `StoreContextMut::run_concurrent` event loop from making progress.
447 // That, in turn, prevents any concurrent tasks from executing, and also
448 // prevents the `AsyncFnOnce` passed to `run_concurrent` from being
449 // polled. Consequently, we must poll `S::WorkerState` from _outside_
450 // the `run_concurrent` future to ensure expirations are enforced. Once
451 // the aforementioned issues have been addressed, we'll be able to
452 // simplify the code and eliminate the need for communication between
453 // the "inside" future and the "outside" one.
454
455 // Wrap `store` in an object which, prior to leaving this scope, will
456 // pass the `store` to `HandlerState::drop`.
457 struct Dropper<S: HandlerState> {
458 state: S::WorkerState,
459 store: Option<Store<S::StoreData>>,
460 }
461
462 impl<S: HandlerState> Drop for Dropper<S> {
463 fn drop(&mut self) {
464 if let Some(store) = self.store.take() {
465 self.state
466 .drop(store, Err(wasmtime::format_err!("worker panicked")));
467 }
468 }
469 }
470
471 let mut dropper = Dropper::<S> {
472 state,
473 store: Some(store),
474 };
475
476 let proxy = &proxy;
477
478 let accept_concurrent = AtomicBool::new(true);
479 let status = Mutex::new((WorkerStatus::Idle, Instant::now()));
480 let mut expiration = pin!(expiration);
481
482 let function = async |accessor: &Accessor<_>| {
483 let mut reuse_count = 0;
484 let mut may_accept = true;
485 let mut futures = FuturesUnordered::new();
486 let mut start_times = StartTimes::default();
487
488 let accept_request = |(request_data, request, tx): WorkerRequest<S>,
489 futures: &mut FuturesUnordered<_>,
490 start_times: &mut StartTimes,
491 reuse_count: &mut usize| {
492 // Set `accept_concurrent` to false, conservatively assuming
493 // that the new task will be CPU-bound, at least to begin with.
494 // Only once the `StoreContextMut::run_concurrent` event loop
495 // returns `Pending` will we set `accept_concurrent` back to
496 // true and consider accepting more requests.
497 //
498 // This approach avoids taking on more than one CPU-bound task
499 // at a time, which would hurt throughput vs. leaving the
500 // additional requests for other workers to handle.
501 accept_concurrent.store(false, Relaxed);
502 *reuse_count += 1;
503
504 let prepared = accessor.with(|mut store| {
505 let prepared = Prepared::new(store.as_context_mut(), proxy, request, view, tx);
506 match prepared {
507 Ok(prepared) => {
508 // Notify the `HandlerState` that we're starting to
509 // handle a request and retrieve the deadline by
510 // which it must produce a response.
511 //
512 // If it fails to produce a response by the
513 // deadline, we'll stop accepting new requests and
514 // eventually exit the worker.
515 let expiration = dropper.state.on_request_start(
516 store.as_context_mut(),
517 request_data,
518 prepared.group(),
519 );
520 Ok((prepared, expiration))
521 }
522 Err(e) => Err(e),
523 }
524 });
525
526 let start_time = Instant::now();
527 start_times.add(start_time);
528 *status.try_lock().unwrap() = (WorkerStatus::Requests, start_time);
529
530 futures.push(async move {
531 let (prepared, expiration) = prepared?;
532 let sent = prepared.run(accessor, expiration).await?;
533 wasmtime::error::Ok((sent, start_time))
534 });
535 };
536
537 if let Some(req) = request {
538 accept_request(req, &mut futures, &mut start_times, &mut reuse_count);
539 }
540
541 // This is the main driver loop for this worker. This is modeled as
542 // a `poll_fn` which internally loops around the possible events.
543 // Events are sourced from the locals here, pinned outside of the
544 // `poll_fn` closure.
545 let mut futures = pin!(futures);
546 let handler = self.handler.clone();
547 let mut incoming_requests = pin!(futures::stream::unfold(
548 &handler.0.request_queue,
549 |queue| async move {
550 let pair = queue.pop().await;
551 Some((pair, queue))
552 }
553 ));
554 let func = match proxy {
555 #[cfg(feature = "p3")]
556 Proxy::P3(guest) => *guest.wasi_http_handler().func_handle().func(),
557 #[cfg(feature = "p2")]
558 Proxy::P2(guest) => *guest.wasi_http_incoming_handler().func_handle().func(),
559 };
560 future::poll_fn(|cx| {
561 loop {
562 // First, and crucially first, poll `futures`. This way
563 // we'll discover any tasks that may have timed out, at
564 // which point we'll stop accepting new tasks altogether
565 // (see below for details). This is especially important in
566 // the case where the task was blocked on a synchronous call
567 // to a host function which has exclusive access to the
568 // `Store`; once that call finishes, the first thing we need
569 // to do is time out the task. If we were to poll for a new
570 // task first, then we'd have to wait for _that_ task to
571 // finish or time out before we could kill the instance.
572 match futures.as_mut().poll_next(cx) {
573 // A request either produced a response or expired.
574 Poll::Ready(Some(Ok((responded, start_time)))) => {
575 // Remove its start time from the map and update the
576 // state.
577 start_times.remove(start_time);
578 *status.try_lock().unwrap() =
579 if let Some(start_time) = start_times.most_recent() {
580 (WorkerStatus::Requests, start_time)
581 } else {
582 (WorkerStatus::PostReturn, Instant::now())
583 };
584
585 if responded {
586 // Response produced; carry on!
587 } else {
588 // Request expired; stop accepting new requests, but
589 // continue polling until any other, in-progress
590 // tasks until they have either finished or expired.
591 // This effectively kicks off a "graceful shutdown"
592 // of the worker, allowing any other concurrent
593 // tasks time to finish before we drop the instance.
594 may_accept = false;
595 }
596 }
597
598 // Instance trapped.
599 Poll::Ready(Some(Err(error))) => {
600 break Poll::Ready(Err(error));
601 }
602
603 Poll::Ready(None) | Poll::Pending => {}
604 }
605
606 let is_ready = accessor.poll_ready_for_concurrent_call(func, cx).is_ready();
607
608 // At this point `futures` is either empty or it's `Pending`
609 // meaning nothing is ready. Note that `Pending` here
610 // doesn't necessarily mean all tasks are blocked on I/O.
611 // They might simply be waiting for some deferred work to be
612 // done by the next turn of the
613 // `StoreContextMut::run_concurrent` event loop. Therefore,
614 // we check `accept_concurrent` here and only advertise we
615 // have capacity for another task if either we have no tasks
616 // at all or all our tasks really are blocked on I/O.
617 self.set_available(
618 may_accept
619 && is_ready
620 && match dropper
621 .state
622 .should_accept_request(futures.len(), reuse_count)
623 {
624 ShouldAccept::Yes => {
625 futures.is_empty() || accept_concurrent.load(Relaxed)
626 }
627 ShouldAccept::No => false,
628 ShouldAccept::Never => {
629 may_accept = false;
630 false
631 }
632 },
633 );
634
635 // If we're available for accepting more requests after the
636 // deduction above, then try to accept a new task. If that's
637 // successful then push it into `futures` and turn this loop
638 // again to see where we're at next time around.
639 if self.available
640 && let Poll::Ready(Some(req)) = incoming_requests.as_mut().poll_next(cx)
641 {
642 accept_request(req, &mut futures, &mut start_times, &mut reuse_count);
643 continue;
644 }
645
646 // If, at this point, we still have some requests that are
647 // being processed then go ahead and bail out of this
648 // singular call to `poll` by saying we're not ready yet.
649 // This means we unconditionally wait for events within
650 // `futures` and we're also registered, optionally, for
651 // listening for incoming connections. That's all the events
652 // we're interested in, so this iteration of `poll` is complete.
653 if !futures.is_empty() {
654 break Poll::Pending;
655 }
656
657 // At this point `futures` is empty, and we haven't gotten
658 // any incoming tasks. Check the store we're using to see if
659 // there are any "interesting" tasks around. These are tasks
660 // which act as effectively strong references to this worker
661 // to keep it running. If there are still interesting tasks,
662 // then we're done with this iteration of `poll`. We'll get
663 // woken up when anything changes, but otherwise it's time
664 // to let something else happen.
665 if accessor.poll_no_interesting_tasks(cx).is_pending() {
666 break Poll::Pending;
667 }
668
669 // And now at this point we (a) have no `futures`, (b) no
670 // new requests are available, and (c) the store is
671 // completely devoid of interesting work. In this situation
672 // if we're not actually capable of accepting any more work,
673 // then we're completely done and it's time to exit this
674 // worker.
675 if !(may_accept && is_ready) {
676 break Poll::Ready(Ok(()));
677 }
678
679 // Finally, at this point we're idle but still eligible to
680 // accept new work, so update the state if appropriate and
681 // then return pending while we wait for new work.
682 {
683 let mut status = status.try_lock().unwrap();
684 if status.0 != WorkerStatus::Idle {
685 *status = (WorkerStatus::Idle, Instant::now());
686 }
687 }
688 break Poll::Pending;
689 }
690 })
691 .await
692 };
693
694 let result = {
695 let mut future = pin!(
696 dropper
697 .store
698 .as_mut()
699 .unwrap()
700 .run_concurrent(function)
701 .map(|v| v.flatten())
702 );
703
704 future::poll_fn(|cx| {
705 let poll = dropper.state.poll(future.as_mut(), cx);
706 if poll.is_pending() {
707 // If the future returns `Pending`, that's either because it's
708 // idle (in which case it can definitely accept a new request) or
709 // because all its tasks are awaiting I/O, in which case it may
710 // have capacity for additional tasks to run concurrently.
711 //
712 // However, per #11869 and #11870, if one of the tasks is
713 // blocked on a sync call to a host function which has exclusive
714 // access to the `Store`, the `StoreContextMut::run_concurrent`
715 // event loop will be unable to make progress until that call
716 // finishes. Similarly, if the task loops indefinitely, subject
717 // only to epoch interruption, the event loop will also be
718 // stuck. Either way, any request expirations created inside
719 // the `AsyncFnOnce` we passed to `run_concurrent` won't have a
720 // chance to trigger. Consequently, we poll for instance
721 // expiration here, outside the event loop, based on the most
722 // recently recorded state of the worker.
723
724 let (status, start) = *status.try_lock().unwrap();
725
726 if let Poll::Ready(()) = expiration.as_mut().poll(cx, status, start) {
727 return Poll::Ready(match status {
728 WorkerStatus::Requests | WorkerStatus::PostReturn => {
729 Err(format_err!("guest timed out"))
730 }
731 WorkerStatus::Idle => Ok(()),
732 });
733 }
734
735 // Otherwise, if the instance has not yet expired, we set
736 // `accept_concurrent` to true and, if it wasn't already true
737 // before, poll the future one more time so it can ask for
738 // another request if appropriate.
739 if !accept_concurrent.swap(true, Relaxed) {
740 return dropper.state.poll(future.as_mut(), cx);
741 }
742 }
743
744 poll
745 })
746 .await
747 };
748
749 dropper.state.drop(dropper.store.take().unwrap(), result);
750 }
751}
752
753impl<S> Drop for Worker<S>
754where
755 S: HandlerState,
756{
757 fn drop(&mut self) {
758 self.set_available(false);
759 }
760}
761
762/// Represents the state of a web server.
763///
764/// Note that this supports optional instance reuse, enabled when
765/// `S::WorkerState::should_accept_request` returns [`ShouldAccept::Yes`] more
766/// than once for a given instance. See [`WorkerState`] for details.
767pub struct ProxyHandler<S: HandlerState>(Arc<ProxyHandlerInner<S>>);
768
769impl<S: HandlerState> Clone for ProxyHandler<S> {
770 fn clone(&self) -> Self {
771 Self(self.0.clone())
772 }
773}
774
775/// This error is returned if, when handling the request, a new worker and
776/// associated instance needed to be created, but instantiation failed, e.g. due
777/// to reaching a pooling allocator limit or running out of memory. In this
778/// case, the caller may be able to recover and retry (e.g. after waiting for
779/// existing instances to be dropped and/or freeing memory used by caches,
780/// etc.). Otherwise, it will probably need to return an HTTP 500 error.
781pub struct InstantiationError<T> {
782 /// The host data originally passed with the request.
783 pub request_data: T,
784 /// The original request passed to `ProxyHandler::handle`.
785 ///
786 /// This is wrapped in a `Mutex` to satisfy the `Send + Sync` bounds
787 /// required by `wasmtime::Error`.
788 pub request: Mutex<Request>,
789 /// The original instantiation error.
790 ///
791 /// This is wrapped in an `Arc` because a single instantiation error may
792 /// affect multiple requests, and each caller will be given a clone.
793 pub error: Arc<wasmtime::Error>,
794}
795
796impl<T> fmt::Display for InstantiationError<T> {
797 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
798 write!(f, "instantiation error: {}", self.error)
799 }
800}
801
802impl<T> fmt::Debug for InstantiationError<T> {
803 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
804 write!(f, "instantiation error: {:?}", self.error)
805 }
806}
807
808impl<T> error::Error for InstantiationError<T> {}
809
810/// Returned when the guest failed to produce a response before the expiration
811/// returned by `HandlerState::on_request_start` elapsed.
812pub struct ExpirationError;
813
814impl fmt::Display for ExpirationError {
815 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
816 fmt::Debug::fmt(self, f)
817 }
818}
819
820impl fmt::Debug for ExpirationError {
821 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
822 write!(f, "guest timed out")
823 }
824}
825
826impl error::Error for ExpirationError {}
827
828/// A worker trapped or panicked and failed to produce a result.
829pub struct TrapOrPanicError;
830
831impl fmt::Display for TrapOrPanicError {
832 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
833 fmt::Debug::fmt(self, f)
834 }
835}
836
837impl fmt::Debug for TrapOrPanicError {
838 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
839 write!(f, "worker trapped or panicked")
840 }
841}
842
843impl error::Error for TrapOrPanicError {}
844
845impl<S> ProxyHandler<S>
846where
847 S: HandlerState,
848{
849 /// Create a new `ProxyHandler` with the specified application state and
850 /// pre-instance.
851 pub fn new(state: S) -> Self {
852 Self(Arc::new(ProxyHandlerInner {
853 state,
854 request_queue: Default::default(),
855 worker_count: AtomicUsize::from(0),
856 }))
857 }
858
859 /// Handle the specified request, returning a response on success or the
860 /// tuple of the request and error on failure.
861 ///
862 /// This function will return a `wasmtime::Error` on failure, which may be
863 /// downcast to a more specific type in certain scenarios:
864 ///
865 /// - [`InstantiationError`] if a new worker was created to handle the
866 /// request but could not instantiate the guest component.
867 ///
868 /// - [`ExpirationError`] if the request expired before it produced a
869 /// response. See [`WorkerState::on_request_start`] for details.
870 ///
871 /// - [`ErrorResponse`](crate::ErrorResponse) if the request could not be
872 /// converted into a guest request.
873 ///
874 /// - [`TrapOrPanicError`] if the worker responsible for handling the
875 /// request trapped or panicked before it produced a response. This may be
876 /// used when a trap occurs but cannot be traced to a specific request,
877 /// e.g. during concurrent request handling.
878 ///
879 /// In other failure cases (e.g. `wasi:http/types#error-code` return values
880 /// and/or traps when executing synchronous WASIp2 handler functions), the
881 /// original error returned by the handler will be returned.
882 ///
883 /// # Backpressure
884 ///
885 /// Note that this API does not implement any form of backpressure to limit
886 /// the number of in-flight `Request`s being processed. This function
887 /// may spawn new tokio tasks, instantiate new modules under new stores, and
888 /// queue up pending `Request`s while waiting for previous instances. In all
889 /// of these situations invoking this function will consume some host-side
890 /// resources until the request is done.
891 ///
892 /// Embedders using this API must ensure to take this into account. If an
893 /// infinite number of requests can be fed into this function then it's
894 /// recommended to take a semaphore, for example, around this function call
895 /// to limit the number of concurrent requests that are being processed.
896 pub async fn handle(
897 &self,
898 data: <S::WorkerState as WorkerState>::RequestData,
899 request: Request,
900 ) -> Result<Response, wasmtime::Error> {
901 let (tx, rx) = oneshot::channel();
902 let req = (data, request, tx);
903 if self.0.worker_count.load(Relaxed) == 0 {
904 // There are no available workers; skip the queue and pass
905 // the request directly to the worker, which improves
906 // performance as measured by `wasmtime-server-rps.sh` by
907 // about 15%.
908 self.start_worker(Some(req));
909 } else {
910 let mut queue = self.0.request_queue.queue.lock().unwrap();
911 queue.push_back(req);
912
913 // Start a new worker to handle the request if the last worker just
914 // went unavailable. See also `Worker::set_available` for what
915 // happens if the available worker count goes to zero right after we
916 // check it here, and note that we only check the count _after_
917 // we've pushed the request to the queue.
918 //
919 // The upshot is that at least one (or more) of the
920 // following will happen:
921 //
922 // - An existing worker will accept the request
923 // - We'll start a new worker here to accept the request
924 // - `Worker::set_available` will start a new worker to accept the request
925 //
926 // I.e. it should not be possible for the request to be orphaned
927 // indefinitely in the queue without being accepted except in the
928 // case of a panic or an instantiation error. In the case of an
929 // instantiation error, we'll give the request back to the caller in
930 // an `Err(_)`, allowing the application to decide what to do next.
931 if self.0.worker_count.load(Relaxed) == 0 {
932 let req = queue.pop_back().unwrap();
933 drop(queue);
934 self.start_worker(Some(req));
935 } else {
936 drop(queue);
937 self.0.request_queue.notify_push.notify_one();
938 }
939 }
940
941 rx.await.map_err(|_| TrapOrPanicError)?
942 }
943
944 /// Return a reference to the application state.
945 pub fn state(&self) -> &S {
946 &self.0.state
947 }
948
949 fn start_worker(&self, request: Option<WorkerRequest<S>>) {
950 tokio::spawn(
951 Worker {
952 handler: self.clone(),
953 available: false,
954 }
955 .run(request),
956 );
957 }
958}
959
960/// Representation of a "prepared" call for a guest, used to extract the
961/// `TaskGroupId` before actually executing any handlers.
962///
963/// Right now this is a bit gross since it has to type out a bunch of types by
964/// hand.
965pub enum Prepared<'a, T: 'static> {
966 #[doc(hidden)]
967 #[cfg(feature = "p2")]
968 P2 {
969 guest: &'a p2::bindings::Proxy,
970 call: TypedFuncCallConcurrent<
971 T,
972 (
973 Resource<p2_types::IncomingRequest>,
974 Resource<p2_types::ResponseOutparam>,
975 ),
976 (),
977 >,
978 tx: Arc<Mutex<Option<oneshot::Sender<Result<Response, wasmtime::Error>>>>>,
979 },
980 #[doc(hidden)]
981 #[cfg(feature = "p3")]
982 P3 {
983 guest: &'a p3::bindings::Service,
984 call: TypedFuncCallConcurrent<
985 T,
986 (Resource<p3_types::Request>,),
987 (Result<Resource<p3_types::Response>, p3_types::ErrorCode>,),
988 >,
989 tx: oneshot::Sender<Result<Response, wasmtime::Error>>,
990 request_io_result: Pin<Box<dyn Future<Output = Result<(), crate::Error>> + Send>>,
991 view: fn(&mut T) -> crate::WasiHttpCtxView,
992 },
993}
994
995impl<'a, T: Send> Prepared<'a, T> {
996 /// Creates a new prepared request.
997 pub fn new(
998 mut store: StoreContextMut<'_, T>,
999 proxy: &'a Proxy,
1000 request: Request,
1001 view: fn(&mut T) -> WasiHttpCtxView<'_>,
1002 tx: oneshot::Sender<Result<Response, wasmtime::Error>>,
1003 ) -> Result<Prepared<'a, T>> {
1004 match proxy {
1005 #[cfg(feature = "p3")]
1006 Proxy::P3(guest) => {
1007 use http::header::HOST;
1008 use http::uri::Authority;
1009
1010 let (parts, body) = request.into_parts();
1011 let http::request::Parts {
1012 method,
1013 uri,
1014 headers: request_headers,
1015 ..
1016 } = parts;
1017 let http::uri::Parts {
1018 authority,
1019 path_and_query,
1020 ..
1021 } = uri.into_parts();
1022 // HTTP/1.1 requires an authority in the request target or the
1023 // `Host` header (RFC 9112 section 3.2.2), so fall back to the latter.
1024 let authority = match authority {
1025 Some(authority) => Some(authority),
1026 None => request_headers
1027 .get(HOST)
1028 .and_then(|host| host.to_str().ok())
1029 .and_then(|host| host.parse::<Authority>().ok()),
1030 };
1031 let hooks = view(store.data_mut()).hooks;
1032 let headers = crate::FieldMap::new_immutable(hooks, request_headers);
1033 // `serve` speaks plaintext HTTP, so that is the scheme of every
1034 // request, whatever scheme its request target names (as in p2).
1035 let (request, request_io_result) = p3::Request::new(
1036 method,
1037 Some(http::uri::Scheme::HTTP),
1038 authority,
1039 path_and_query,
1040 headers,
1041 None,
1042 body,
1043 );
1044 let request = view(store.data_mut()).table.push(request)?;
1045
1046 Ok(Prepared::P3 {
1047 tx,
1048 request_io_result: Box::pin(request_io_result),
1049 guest,
1050 view,
1051 call: guest
1052 .wasi_http_handler()
1053 .func_handle()
1054 .start_call_concurrent(store, (request,))?,
1055 })
1056 }
1057 #[cfg(feature = "p2")]
1058 Proxy::P2(guest) => {
1059 // Here we wrap the sender in an `Arc<Mutex<Option<_>>>`, with one
1060 // clone used in the `response-outparam` and the other used to send
1061 // an error if the request expires or the handler returns without
1062 // producing a response.
1063 let tx = Arc::new(Mutex::new(Some(tx)));
1064
1065 let request = match view(store.data_mut())
1066 .new_incoming_request(p2_types::Scheme::Http, request)
1067 {
1068 Ok(request) => request,
1069 Err(e) => {
1070 // The request never reached the guest, so there is no
1071 // `response-outparam` to report the failure with, and
1072 // `tx` would otherwise be dropped along with the error.
1073 if let Some(tx) = tx.lock().unwrap().take() {
1074 _ = tx.send(Err(e));
1075 }
1076 wasmtime::bail!(
1077 "request was rejected before it could be turned into a guest request"
1078 );
1079 }
1080 };
1081
1082 let out = view(store.data_mut()).new_response_outparam_from_callback({
1083 let tx = tx.clone();
1084 move |value| {
1085 if let Some(tx) = tx.lock().unwrap().take() {
1086 _ = tx.send(value.map_err(|e| e.into()));
1087 }
1088 }
1089 })?;
1090
1091 Ok(Prepared::P2 {
1092 guest,
1093 tx,
1094 call: guest
1095 .wasi_http_incoming_handler()
1096 .func_handle()
1097 .start_call_concurrent(store, (request, out))?,
1098 })
1099 }
1100 }
1101 }
1102
1103 fn group(&self) -> TaskGroupId {
1104 match self {
1105 #[cfg(feature = "p3")]
1106 Prepared::P3 { call, .. } => call.group(),
1107 #[cfg(feature = "p2")]
1108 Prepared::P2 { call, .. } => call.group(),
1109 }
1110 }
1111
1112 /// Executes this request to completion.
1113 pub async fn run(
1114 self,
1115 accessor: &Accessor<T>,
1116 expiration: impl Future<Output = ()>,
1117 ) -> Result<bool> {
1118 let expiration = pin!(expiration);
1119
1120 match self {
1121 #[cfg(feature = "p3")]
1122 Prepared::P3 {
1123 guest,
1124 call,
1125 tx,
1126 request_io_result,
1127 view,
1128 } => {
1129 let handle = pin!(async move {
1130 let response = guest
1131 .wasi_http_handler()
1132 .func_handle()
1133 .finish_call_concurrent(accessor, call)
1134 .await?
1135 .0?;
1136
1137 accessor.with(|mut store| {
1138 let response = view(store.get()).table.delete(response)?;
1139 response.into_http_with_getter(&mut store, request_io_result, view)
1140 })
1141 });
1142
1143 // TODO: We should also use `oneshot::Sender::poll_close` to be
1144 // notified when the receiver is dropped, in which case we should
1145 // expire the request since the response is no longer of interest to
1146 // the original `ProxyHandler::handle` caller.
1147 let (result, sent) = match futures::future::select(handle, expiration).await {
1148 Either::Left((result, _)) => (result, true),
1149 // TODO: We should also send a cancel request to the expired
1150 // task to give it a chance to shut down gracefully, but as of
1151 // this writing Wasmtime does not yet provide an API for doing
1152 // that. See issue #11833. Instead, we let it continue running
1153 // as a background task until it either returns a response
1154 // (which we'll ignore) or the instance itself has expired.
1155 Either::Right(((), _)) => (Err(ExpirationError.into()), false),
1156 };
1157
1158 _ = tx.send(result);
1159
1160 Ok(sent)
1161 }
1162 #[cfg(feature = "p2")]
1163 Prepared::P2 { guest, call, tx } => {
1164 let handle = pin!(
1165 guest
1166 .wasi_http_incoming_handler()
1167 .func_handle()
1168 .finish_call_concurrent(accessor, call)
1169 );
1170
1171 const MESSAGE: &str = "guest never invoked `response-outparam::set` method";
1172
1173 struct Dropper(
1174 Arc<Mutex<Option<oneshot::Sender<Result<Response, wasmtime::Error>>>>>,
1175 );
1176
1177 impl Drop for Dropper {
1178 fn drop(&mut self) {
1179 if let Some(tx) = self.0.lock().unwrap().take() {
1180 _ = tx.send(Err(format_err!("{MESSAGE}")));
1181 }
1182 }
1183 }
1184
1185 let tx = Dropper(tx);
1186
1187 // See corresponding TODO comment for the p3 case above.
1188 let (result, sent) = match futures::future::select(handle, expiration).await {
1189 Either::Left((result, _)) => (result.context(MESSAGE), true),
1190 // See corresponding TODO comment for the p3 case above.
1191 Either::Right(((), _)) => (Err(ExpirationError.into()), false),
1192 };
1193
1194 if let Some(tx) = tx.0.lock().unwrap().take() {
1195 _ = tx.send(result.and_then(|()| Err(format_err!("{MESSAGE}"))));
1196 }
1197
1198 Ok(sent)
1199 }
1200 }
1201 }
1202}