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