Skip to main content

aws_smithy_http_client/client/pool/
partition.rs

1/*
2 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 * SPDX-License-Identifier: Apache-2.0
4 */
5
6//! Connection placement declarations.
7//!
8//! A [`Partition`] combines stable identity, driver placement, and an optional
9//! network-interface binding. It declares where connections live; sharing a
10//! connection never moves that connection's I/O or driver.
11
12use std::fmt;
13use std::future::Future;
14use std::pin::Pin;
15use std::sync::Arc;
16
17/// Declares where a client's connections are established and driven.
18#[derive(Clone)]
19pub struct Partition {
20    /// Stable identity used to resolve clients and index cells.
21    id: PartitionId,
22    /// Runtime placement for connection-owned tasks.
23    spawner: DriverSpawner,
24    /// Optional network-interface binding for new connections.
25    interface: Option<Arc<str>>,
26}
27
28impl Partition {
29    /// Creates a partition with a stable identity and driver spawner.
30    pub fn new(id: PartitionId, spawner: DriverSpawner) -> Self {
31        Self {
32            id,
33            spawner,
34            interface: None,
35        }
36    }
37
38    /// Binds connections established by this partition to an interface.
39    ///
40    /// The binding is applied before connect. On Linux, using this setting
41    /// sets `SO_BINDTODEVICE` and may require `CAP_NET_RAW` or root
42    /// privileges. Apple platforms, illumos, and Solaris use `IP_BOUND_IF`.
43    ///
44    /// This method is available on Linux and Android, Fuchsia, illumos,
45    /// Solaris, macOS, iOS, tvOS, visionOS, and watchOS.
46    #[cfg(any(
47        target_os = "android",
48        target_os = "fuchsia",
49        target_os = "illumos",
50        target_os = "ios",
51        target_os = "linux",
52        target_os = "macos",
53        target_os = "solaris",
54        target_os = "tvos",
55        target_os = "visionos",
56        target_os = "watchos",
57    ))]
58    #[cfg_attr(
59        docsrs,
60        doc(cfg(any(
61            target_os = "android",
62            target_os = "fuchsia",
63            target_os = "illumos",
64            target_os = "ios",
65            target_os = "linux",
66            target_os = "macos",
67            target_os = "solaris",
68            target_os = "tvos",
69            target_os = "visionos",
70            target_os = "watchos",
71        )))
72    )]
73    pub fn interface(mut self, interface: impl Into<String>) -> Self {
74        self.interface = Some(Arc::from(interface.into()));
75        self
76    }
77
78    /// Returns the configured network-interface name for validation.
79    #[cfg(any(
80        target_os = "android",
81        target_os = "fuchsia",
82        target_os = "illumos",
83        target_os = "ios",
84        target_os = "linux",
85        target_os = "macos",
86        target_os = "solaris",
87        target_os = "tvos",
88        target_os = "visionos",
89        target_os = "watchos",
90    ))]
91    pub(super) fn interface_name(&self) -> Option<&str> {
92        self.interface.as_deref()
93    }
94
95    /// Returns this partition's declared identity.
96    pub(super) fn id(&self) -> PartitionId {
97        self.id
98    }
99
100    /// Decomposes this declaration for immutable registry storage.
101    pub(super) fn into_parts(self) -> (PartitionId, DriverSpawner, Option<Arc<str>>) {
102        (self.id, self.spawner, self.interface)
103    }
104}
105
106impl fmt::Debug for Partition {
107    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
108        f.debug_struct("Partition")
109            .field("id", &self.id)
110            .field("spawner", &self.spawner)
111            .field("interface", &self.interface)
112            .finish()
113    }
114}
115
116/// Identifies one declared connection partition.
117#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
118pub struct PartitionId(usize);
119
120impl PartitionId {
121    /// Reserved identity for the implicit default partition.
122    pub const ANONYMOUS: Self = Self(usize::MAX);
123
124    /// Creates a partition identity from a caller-owned index.
125    ///
126    /// `usize::MAX` is reserved for [`Self::ANONYMOUS`] and is rejected when
127    /// used in an explicit partition declaration.
128    pub const fn from_index(index: usize) -> Self {
129        Self(index)
130    }
131
132    /// Returns whether this is the reserved anonymous partition identity.
133    pub const fn is_anonymous(self) -> bool {
134        self.0 == Self::ANONYMOUS.0
135    }
136}
137
138/// Runtime placement for one partition's connection-owned work.
139///
140/// Protocol drivers, establishment completion, deferred HTTP/1 readiness, and
141/// idle maintenance for a partition run through its spawner, even when another
142/// partition dispatches on one of its connections. The underlying I/O never
143/// moves to the requesting runtime.
144///
145/// A spawner is placement, not runtime ownership. It does not keep the runtime
146/// alive and does not participate in shutdown. When the owning runtime drops
147/// spawned work, the pool observes that through the connection's own close
148/// path.
149///
150/// Callers on Tokio use [`DriverSpawner::tokio`] with the handle of the runtime
151/// that should own the partition's connections.
152#[derive(Clone)]
153pub struct DriverSpawner {
154    inner: Arc<dyn Spawn>,
155}
156
157impl DriverSpawner {
158    /// Places connection-owned work on a specific Tokio runtime.
159    ///
160    /// Work is spawned on `handle` regardless of which thread invokes the
161    /// pool. Pass `tokio::runtime::Handle::current()` to use the runtime that
162    /// is constructing the partition.
163    #[cfg(feature = "rt-tokio")]
164    #[cfg_attr(docsrs, doc(cfg(feature = "rt-tokio")))]
165    pub fn tokio(handle: tokio::runtime::Handle) -> Self {
166        Self::new(TokioSpawn { handle })
167    }
168
169    /// Places connection-owned work through a caller-provided spawn function.
170    ///
171    /// Each call transfers ownership of a connection-owned task. The callback
172    /// must submit it to the runtime this partition represents and return
173    /// promptly. Calls may arrive concurrently from different threads.
174    ///
175    /// Tasks must make progress while that runtime is running. Dropping a task
176    /// during shutdown follows the pool's existing connection-close behavior.
177    /// The callback returns neither a join handle nor a submission error.
178    /// Runtime ownership and shutdown remain the caller's responsibility.
179    pub fn from_fn<F>(spawn: F) -> Self
180    where
181        F: Fn(Pin<Box<dyn Future<Output = ()> + Send + 'static>>) + Send + Sync + 'static,
182    {
183        Self::new(FnSpawn(spawn))
184    }
185
186    /// Wraps one crate-internal placement implementation.
187    pub(in crate::client::pool) fn new(spawn: impl Spawn) -> Self {
188        Self {
189            inner: Arc::new(spawn),
190        }
191    }
192
193    /// Spawns connection-owned work on this partition's runtime.
194    pub(in crate::client::pool) fn spawn(
195        &self,
196        driver: Pin<Box<dyn Future<Output = ()> + Send + 'static>>,
197    ) {
198        self.inner.spawn(driver);
199    }
200
201    /// Returns whether both values name the same underlying spawner.
202    #[cfg(test)]
203    pub(in crate::client::pool) fn ptr_eq(&self, other: &Self) -> bool {
204        Arc::ptr_eq(&self.inner, &other.inner)
205    }
206}
207
208impl fmt::Debug for DriverSpawner {
209    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
210        f.debug_tuple("DriverSpawner").field(&self.inner).finish()
211    }
212}
213
214/// Placement capability behind [`DriverSpawner`].
215///
216/// This trait is crate-internal so the pool can extend the placement contract
217/// without changing the public spawner type.
218pub(in crate::client::pool) trait Spawn:
219    fmt::Debug + Send + Sync + 'static
220{
221    /// Spawns connection-owned work on the partition's runtime.
222    fn spawn(&self, driver: Pin<Box<dyn Future<Output = ()> + Send + 'static>>);
223}
224
225/// Placement on a captured Tokio runtime handle.
226#[cfg(feature = "rt-tokio")]
227#[derive(Debug)]
228struct TokioSpawn {
229    /// Runtime that receives connection-owned tasks.
230    handle: tokio::runtime::Handle,
231}
232
233#[cfg(feature = "rt-tokio")]
234impl Spawn for TokioSpawn {
235    fn spawn(&self, driver: Pin<Box<dyn Future<Output = ()> + Send + 'static>>) {
236        drop(self.handle.spawn(driver));
237    }
238}
239
240/// Placement through a caller-provided spawn function.
241struct FnSpawn<F>(F);
242
243impl<F> fmt::Debug for FnSpawn<F> {
244    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
245        f.write_str("FnSpawn")
246    }
247}
248
249impl<F> Spawn for FnSpawn<F>
250where
251    F: Fn(Pin<Box<dyn Future<Output = ()> + Send + 'static>>) + Send + Sync + 'static,
252{
253    fn spawn(&self, driver: Pin<Box<dyn Future<Output = ()> + Send + 'static>>) {
254        (self.0)(driver);
255    }
256}
257
258/// Controls which partitions may dispatch through each other's connections.
259///
260/// The default is [`NetworkInterface`](Self::NetworkInterface): partitions
261/// with the same interface binding, including all partitions with no binding,
262/// may borrow each other's idle HTTP/1 connections and route requests to each
263/// other's HTTP/2 connections. Reuse scope never changes the bounded
264/// connection budget, which is shared by every partition for one origin.
265#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
266#[non_exhaustive]
267pub enum ConnectionReuseScope {
268    /// A connection serves only requests from its owning partition.
269    Partition,
270    /// Partitions with the same interface binding may share connections.
271    #[default]
272    NetworkInterface,
273    /// Every partition in the pool may share connections.
274    Pool,
275}
276
277/// Identifies the exact set of partitions allowed to share a connection.
278///
279/// The configured [`ConnectionReuseScope`] maps each partition to one group:
280/// partition-local scope creates a group for one partition, network-interface
281/// scope groups equal interface bindings, and pool scope creates one group for
282/// the whole pool.
283#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
284pub(in crate::client::pool) enum EligibilityGroup {
285    /// Only the partition with this identity is eligible.
286    Partition(PartitionId),
287    /// Partitions with this exact interface binding are eligible.
288    NetworkInterface(Option<Arc<str>>),
289    /// Every partition in the pool is eligible.
290    Pool,
291}
292
293#[cfg(test)]
294mod tests {
295    use super::*;
296    #[cfg(feature = "rt-tokio")]
297    use std::sync::atomic::{AtomicBool, Ordering};
298
299    #[derive(Debug)]
300    struct TestSpawner;
301
302    impl Spawn for TestSpawner {
303        fn spawn(&self, driver: Pin<Box<dyn Future<Output = ()> + Send + 'static>>) {
304            drop(driver);
305        }
306    }
307
308    #[test]
309    fn anonymous_identity_is_reserved() {
310        assert!(PartitionId::ANONYMOUS.is_anonymous());
311        assert!(PartitionId::from_index(usize::MAX).is_anonymous());
312        assert!(!PartitionId::from_index(0).is_anonymous());
313    }
314
315    #[test]
316    fn partition_retains_placement() {
317        let spawner = DriverSpawner::new(TestSpawner);
318        let partition = Partition::new(PartitionId::from_index(7), spawner.clone());
319        let (id, retained, interface) = partition.into_parts();
320        assert_eq!(PartitionId::from_index(7), id);
321        assert_eq!(None, interface);
322        assert!(spawner.ptr_eq(&retained));
323
324        let driver = Box::pin(async {});
325        retained.spawn(driver);
326    }
327
328    #[test]
329    fn from_fn_receives_each_driver() {
330        let submitted = Arc::new(std::sync::atomic::AtomicUsize::new(0));
331        let observed = submitted.clone();
332        let spawner = DriverSpawner::from_fn(move |driver| {
333            observed.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
334            drop(driver);
335        });
336
337        spawner.spawn(Box::pin(async {}));
338        spawner.spawn(Box::pin(async {}));
339        assert_eq!(2, submitted.load(std::sync::atomic::Ordering::SeqCst));
340        assert_eq!("DriverSpawner(FnSpawn)", format!("{spawner:?}"));
341    }
342
343    #[cfg(feature = "rt-tokio")]
344    #[test]
345    fn callback_spawner_submits_to_its_target_from_another_runtime() {
346        let owner = tokio::runtime::Builder::new_current_thread()
347            .build()
348            .unwrap();
349        let foreign = tokio::runtime::Builder::new_current_thread()
350            .build()
351            .unwrap();
352        let owner_id = owner.handle().id();
353        let handle = owner.handle().clone();
354        let spawner = DriverSpawner::from_fn(move |task| {
355            let task_handle = handle.spawn(task);
356            drop(task_handle);
357        });
358        let (completed, completion) = tokio::sync::oneshot::channel();
359
360        foreign.block_on(async {
361            spawner.spawn(Box::pin(async move {
362                completed
363                    .send(tokio::runtime::Handle::current().id())
364                    .unwrap();
365            }));
366        });
367        assert_eq!(owner_id, owner.block_on(completion).unwrap());
368    }
369
370    #[cfg(feature = "rt-tokio")]
371    #[test]
372    fn tokio_spawner_uses_captured_runtime() {
373        let owner = tokio::runtime::Builder::new_current_thread()
374            .build()
375            .unwrap();
376        let owner_id = owner.handle().id();
377        let ran = Arc::new(AtomicBool::new(false));
378        let task_ran = ran.clone();
379        let spawner = DriverSpawner::tokio(owner.handle().clone());
380
381        owner.block_on(async {
382            spawner.spawn(Box::pin(async move {
383                assert_eq!(owner_id, tokio::runtime::Handle::current().id());
384                task_ran.store(true, Ordering::SeqCst);
385            }));
386            for _ in 0..10 {
387                if ran.load(Ordering::SeqCst) {
388                    break;
389                }
390                tokio::task::yield_now().await;
391            }
392        });
393        assert!(ran.load(Ordering::SeqCst));
394    }
395
396    #[cfg(feature = "rt-tokio")]
397    #[test]
398    fn tokio_spawner_accepts_work_from_a_foreign_runtime() {
399        let owner = tokio::runtime::Builder::new_current_thread()
400            .build()
401            .unwrap();
402        let foreign = tokio::runtime::Builder::new_current_thread()
403            .build()
404            .unwrap();
405        let owner_id = owner.handle().id();
406        let ran = Arc::new(AtomicBool::new(false));
407        let task_ran = ran.clone();
408        let spawner = DriverSpawner::tokio(owner.handle().clone());
409
410        foreign.block_on(async {
411            spawner.spawn(Box::pin(async move {
412                assert_eq!(owner_id, tokio::runtime::Handle::current().id());
413                task_ran.store(true, Ordering::SeqCst);
414            }));
415        });
416        owner.block_on(async {
417            for _ in 0..10 {
418                if ran.load(Ordering::SeqCst) {
419                    break;
420                }
421                tokio::task::yield_now().await;
422            }
423        });
424        assert!(ran.load(Ordering::SeqCst));
425    }
426}