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