Skip to main content

sark_client/connector/
session.rs

1use std::cell::Cell;
2use std::io::{self, Read};
3use std::pin::Pin;
4use std::rc::Rc;
5use std::time::{Duration, Instant};
6
7use cartel_core::{Arena, Limits};
8use dope::driver::token::Token;
9use dope::manifold::connector;
10use dope::manifold::timer::Timer;
11use dope::runtime::Idle;
12use dope_fiber::WaitQueue;
13use o3::buffer::{Lease, Pool as BufferPool, PoolLayout};
14use o3::cell::RawCell;
15use o3::collections::SlotQueue;
16use o3::mem::ByteBudget;
17use sark_core::http::Response;
18use sark_core::http::codec::{DecodeMode, HeaderLookup, Parse};
19
20use crate::connector::codec::{self, Head};
21use crate::connector::error::Error;
22use crate::connector::retry::RetryPolicy;
23
24pub(super) type Outcome = Result<Response, Error>;
25
26pub(super) const DEFAULT_IDLE_TIMEOUT: Duration = Duration::from_secs(30);
27
28pub(super) const DEFAULT_REQUEST_TIMEOUT: Duration = Duration::from_secs(15);
29
30pub const DEFAULT_MAX_INFLIGHT_PER_CONNECTION: usize = 256;
31
32pub const DEFAULT_RESPONSE_BUFFER_CAPACITY: usize = 64 * 1024 * 1024;
33
34pub const DEFAULT_REQUEST_SLOTS: u32 = 256;
35
36pub const DEFAULT_REQUEST_CAPACITY: u32 = 64 * 1024;
37
38const KEEPALIVE_MARGIN: Duration = Duration::from_secs(1);
39
40#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
41pub enum DecompressionPolicy {
42    #[default]
43    Strict,
44    Lenient,
45}
46#[derive(Default)]
47pub struct ConnState {
48    pending_close: bool,
49}
50
51impl connector::Lifecycle for ConnState {
52    fn wants_close(&self) -> connector::Close {
53        if self.pending_close {
54            connector::Close::Reconnect
55        } else {
56            connector::Close::Keep
57        }
58    }
59
60    fn defer_close(&self) -> bool {
61        false
62    }
63
64    fn is_drained(&self) -> bool {
65        true
66    }
67}
68
69struct ConnEntry<'d> {
70    conn_id: Cell<Option<Token>>,
71    arena: Arena<'d, Outcome>,
72    last_activity: Cell<Option<Instant>>,
73    keepalive: Cell<Option<Duration>>,
74    queued: Cell<bool>,
75}
76
77struct Pool<'d> {
78    entries: Box<[ConnEntry<'d>]>,
79    ready: RawCell<SlotQueue<Token>>,
80    live: Cell<usize>,
81    _budget: Pin<Rc<ByteBudget>>,
82}
83
84impl<'d> Pool<'d> {
85    fn new(capacity: usize, max_inflight: usize, limit: usize) -> Self {
86        let budget = Rc::pin(ByteBudget::new(limit));
87        let limits = Limits::new(1, limit, 1);
88        Self {
89            entries: (0..capacity)
90                .map(|_| ConnEntry {
91                    conn_id: Cell::new(None),
92                    arena: Arena::with_shared_budget(max_inflight, limits, budget.clone()),
93                    last_activity: Cell::new(None),
94                    keepalive: Cell::new(None),
95                    queued: Cell::new(false),
96                })
97                .collect(),
98            ready: RawCell::new(SlotQueue::with_capacity(capacity)),
99            live: Cell::new(0),
100            _budget: budget,
101        }
102    }
103
104    fn entry<'a>(&'a self, conn_id: Token) -> Option<&'a ConnEntry<'d>> {
105        let entry = self.entries.get(conn_id.slot().raw() as usize)?;
106        (entry.conn_id.get() == Some(conn_id)).then_some(entry)
107    }
108
109    fn push_ready(&self, entry: &ConnEntry<'d>, conn_id: Token) {
110        if entry.queued.replace(true) {
111            return;
112        }
113        unsafe {
114            self.ready.with_mut(|ready| {
115                ready
116                    .vacant_entry(conn_id.slot().raw() as usize)
117                    .expect("ready queue entry must be vacant")
118                    .push_back(conn_id);
119            })
120        };
121    }
122
123    fn pop_ready(&self) -> Option<Token> {
124        unsafe { self.ready.with_mut(SlotQueue::pop_front) }
125    }
126
127    fn remove_ready(&self, conn_id: Token) {
128        unsafe {
129            self.ready
130                .with_mut(|ready| ready.remove(conn_id.slot().raw() as usize))
131        };
132    }
133}
134
135pub struct Shared<'d> {
136    pool: Pool<'d>,
137    active_waiters: Pin<Box<WaitQueue>>,
138    pub(super) host: String,
139    pub(super) origin: http::Uri,
140    pub(super) decompression: DecompressionPolicy,
141    pub(super) max_redirects: u32,
142    pub(super) retry: RetryPolicy,
143    pub(super) idle_timeout: Duration,
144    pub(super) request_timeout: Duration,
145}
146
147impl<'d> Shared<'d> {
148    pub fn has_connection(&self) -> bool {
149        self.pool.live.get() != 0
150    }
151
152    pub(super) fn connection_count(&self) -> usize {
153        self.pool.live.get()
154    }
155
156    pub(super) fn wake(&self) {
157        self.active_waiters.as_ref().wake();
158    }
159
160    pub(super) fn try_register_active(
161        &self,
162        waiter: Pin<&dope_fiber::Waiter<'d>>,
163        context: Pin<&dope_fiber::Context<'_, 'd>>,
164    ) -> bool {
165        self.active_waiters.as_ref().try_register(waiter, context)
166    }
167
168    fn note_connect(&self, conn_id: Token, now: Instant) {
169        let entry = &self.pool.entries[conn_id.slot().raw() as usize];
170        if entry.conn_id.replace(Some(conn_id)).is_none() {
171            self.pool.live.set(self.pool.live.get() + 1);
172        }
173        entry.last_activity.set(Some(now));
174        entry.keepalive.set(None);
175        self.pool.push_ready(entry, conn_id);
176        self.wake();
177    }
178
179    fn push_response(
180        &self,
181        conn_id: Token,
182        outcome: Outcome,
183        bytes: usize,
184        keepalive: Option<Duration>,
185        now: Instant,
186    ) {
187        let Some(entry) = self.pool.entry(conn_id) else {
188            return;
189        };
190        entry.last_activity.set(Some(now));
191        if keepalive.is_some() {
192            entry.keepalive.set(keepalive);
193        }
194        entry.arena.try_push(outcome, bytes, 1);
195        entry.arena.complete();
196        if entry.arena.can_register() {
197            self.pool.push_ready(entry, conn_id);
198        }
199        self.wake();
200    }
201
202    pub(super) fn close_connection(&self, conn_id: Token) {
203        let Some(entry) = self.pool.entry(conn_id) else {
204            return;
205        };
206        entry.arena.fail_all(|| Err(Error::Closed));
207        entry.conn_id.set(None);
208        self.pool.live.set(self.pool.live.get() - 1);
209        entry.last_activity.set(None);
210        entry.keepalive.set(None);
211        entry.queued.set(false);
212        self.pool.remove_ready(conn_id);
213        self.wake();
214    }
215
216    pub fn acquire(
217        &self,
218        now: Instant,
219        idle_timeout: Duration,
220        mut recycle: impl FnMut(Token),
221    ) -> Option<Token> {
222        while let Some(conn_id) = self.pool.pop_ready() {
223            let Some(entry) = self.pool.entry(conn_id) else {
224                continue;
225            };
226            entry.queued.set(false);
227            let limit = entry
228                .keepalive
229                .get()
230                .map(|k| k.saturating_sub(KEEPALIVE_MARGIN))
231                .unwrap_or(idle_timeout);
232            let stale = entry
233                .last_activity
234                .get()
235                .is_some_and(|last| now.saturating_duration_since(last) >= limit);
236            if stale {
237                self.close_connection(conn_id);
238                recycle(conn_id);
239                continue;
240            }
241            if !entry.arena.can_register() {
242                continue;
243            }
244            return Some(conn_id);
245        }
246        None
247    }
248
249    pub fn arena(&'d self, conn_id: Token) -> Option<&'d Arena<'d, Outcome>> {
250        Some(&self.pool.entry(conn_id)?.arena)
251    }
252
253    pub fn submitted(&self, conn_id: Token, now: Instant) {
254        if let Some(entry) = self.pool.entry(conn_id) {
255            entry.last_activity.set(Some(now));
256            if entry.arena.can_register() {
257                self.pool.push_ready(entry, conn_id);
258            }
259        }
260    }
261
262    pub fn make_available(&self, conn_id: Token) {
263        if let Some(entry) = self.pool.entry(conn_id)
264            && entry.arena.can_register()
265        {
266            self.pool.push_ready(entry, conn_id);
267        }
268    }
269}
270
271pub struct Config {
272    codec: codec::Codec,
273    host: String,
274    origin: http::Uri,
275    decompression: DecompressionPolicy,
276    max_redirects: u32,
277    retry: RetryPolicy,
278    idle_timeout: Duration,
279    request_timeout: Duration,
280    max_inflight_per_connection: usize,
281    response_buffer_capacity: usize,
282    request_slots: u32,
283    request_capacity: u32,
284}
285
286impl Config {
287    pub fn new(host: impl Into<String>) -> Self {
288        let host = host.into();
289        let origin = format!("http://{host}/")
290            .parse()
291            .expect("invalid HTTP host");
292        Self {
293            codec: codec::Codec::default(),
294            host,
295            origin,
296            decompression: DecompressionPolicy::Strict,
297            max_redirects: 10,
298            retry: RetryPolicy::default(),
299            idle_timeout: DEFAULT_IDLE_TIMEOUT,
300            request_timeout: DEFAULT_REQUEST_TIMEOUT,
301            max_inflight_per_connection: DEFAULT_MAX_INFLIGHT_PER_CONNECTION,
302            response_buffer_capacity: DEFAULT_RESPONSE_BUFFER_CAPACITY,
303            request_slots: DEFAULT_REQUEST_SLOTS,
304            request_capacity: DEFAULT_REQUEST_CAPACITY,
305        }
306    }
307
308    pub fn with_decompression(host: impl Into<String>, policy: DecompressionPolicy) -> Self {
309        let mut config = Self::new(host);
310        config.decompression = policy;
311        config
312    }
313
314    pub fn max_response_body(mut self, cap: usize) -> Self {
315        self.codec.max_response_body = cap;
316        self
317    }
318
319    pub fn max_redirects(mut self, max: u32) -> Self {
320        self.max_redirects = max;
321        self
322    }
323
324    pub fn retry(mut self, retry: RetryPolicy) -> Self {
325        self.retry = retry;
326        self
327    }
328
329    pub fn idle_timeout(mut self, idle: Duration) -> Self {
330        self.idle_timeout = idle;
331        self
332    }
333
334    pub fn request_timeout(mut self, dur: Duration) -> Self {
335        self.request_timeout = dur;
336        self
337    }
338
339    pub fn max_inflight_per_connection(mut self, max: usize) -> Self {
340        self.max_inflight_per_connection = max;
341        self
342    }
343
344    pub fn response_buffer_capacity(mut self, capacity: usize) -> Self {
345        self.response_buffer_capacity = capacity;
346        self
347    }
348
349    pub fn request_pool(mut self, slots: u32, capacity: u32) -> Self {
350        self.request_slots = slots;
351        self.request_capacity = capacity;
352        self
353    }
354
355    fn request_pool_layout(&self) -> io::Result<PoolLayout> {
356        if self.request_slots == 0 {
357            return Err(io::Error::new(
358                io::ErrorKind::InvalidInput,
359                "request pool must have slots",
360            ));
361        }
362        PoolLayout::new(self.request_slots, self.request_capacity)
363            .map_err(|error| io::Error::new(io::ErrorKind::InvalidInput, error))
364    }
365}
366
367pub struct Port<'d> {
368    pub(super) io: connector::Port<'d, Lease<'d>>,
369    pub(super) shared: Shared<'d>,
370    codec: codec::Codec,
371    timer: Timer<'d>,
372    pub(super) requests: Pin<Box<BufferPool>>,
373}
374
375pub struct PortFactory {
376    config: Config,
377    capacity: usize,
378    timer_capacity: usize,
379    request_pool: PoolLayout,
380}
381
382impl<'d> Port<'d> {
383    pub fn new(
384        config: Config,
385        capacity: usize,
386        timer_capacity: usize,
387        driver: dope::DriverRef<'d>,
388    ) -> io::Result<Self> {
389        let request_pool = config.request_pool_layout()?;
390        Ok(Self::new_with_pool(
391            config,
392            capacity,
393            timer_capacity,
394            request_pool,
395            driver,
396        ))
397    }
398
399    fn new_with_pool(
400        config: Config,
401        capacity: usize,
402        timer_capacity: usize,
403        request_pool: PoolLayout,
404        driver: dope::DriverRef<'d>,
405    ) -> Self {
406        let Config {
407            codec,
408            host,
409            origin,
410            decompression,
411            max_redirects,
412            retry,
413            idle_timeout,
414            request_timeout,
415            max_inflight_per_connection,
416            response_buffer_capacity,
417            request_slots: _,
418            request_capacity: _,
419        } = config;
420        let shared = Shared {
421            pool: Pool::new(
422                capacity,
423                max_inflight_per_connection,
424                response_buffer_capacity,
425            ),
426            active_waiters: Box::pin(WaitQueue::with_capacity(timer_capacity)),
427            host,
428            origin,
429            decompression,
430            max_redirects,
431            retry,
432            idle_timeout,
433            request_timeout,
434        };
435        Self {
436            io: connector::Port::with_capacity(capacity, driver),
437            shared,
438            codec,
439            timer: Timer::with_capacity(timer_capacity, driver),
440            requests: Box::pin(BufferPool::new(request_pool)),
441        }
442    }
443
444    pub fn capacity(&self) -> usize {
445        self.io.capacity()
446    }
447
448    pub fn factory(
449        config: Config,
450        capacity: usize,
451        timer_capacity: usize,
452    ) -> io::Result<PortFactory> {
453        let request_pool = config.request_pool_layout()?;
454        Ok(PortFactory {
455            config,
456            capacity,
457            timer_capacity,
458            request_pool,
459        })
460    }
461
462    pub(super) fn timer(&'d self) -> &'d Timer<'d> {
463        &self.timer
464    }
465}
466
467impl dope::runtime::StorageFactory for PortFactory {
468    type Output<'d> = Port<'d>;
469
470    fn build<'d>(self, driver: &mut dope::DriverContext<'_, 'd>) -> Self::Output<'d> {
471        Port::new_with_pool(
472            self.config,
473            self.capacity,
474            self.timer_capacity,
475            self.request_pool,
476            driver.driver_ref(),
477        )
478    }
479}
480
481pub struct Session<'d> {
482    pub(super) port: &'d Port<'d>,
483}
484
485impl<'d> Session<'d> {
486    pub fn new(port: &'d Port<'d>) -> Self {
487        Self { port }
488    }
489}
490
491#[dope_gen::connector_session(codec = port.codec, io = port.io)]
492impl<'d> connector::Session<'d> for Session<'d> {
493    type Codec = codec::Codec;
494    type ConnState = ConnState;
495    type Send = Lease<'d>;
496
497    fn connect(&mut self, ctx: &mut connector::Ctx<'_, 'd, Self>) {
498        self.port.shared.note_connect(ctx.conn_id, Instant::now());
499    }
500
501    fn response(&mut self, head: Head, ctx: &mut connector::Ctx<'_, 'd, Self>) {
502        if let Some(reason) = head.error {
503            let bytes = head.full.len();
504            self.port.shared.push_response(
505                ctx.conn_id,
506                Err(Error::Parse(reason.into())),
507                bytes,
508                None,
509                Instant::now(),
510            );
511            ctx.state.pending_close = true;
512            return;
513        }
514        let buffered = head.full.len();
515        let bytes = head.full.as_ref();
516        let (outcome, keep_alive, keepalive_timeout) =
517            match Parse::response(bytes, DecodeMode::Response) {
518                Ok(Some(mut resp)) => {
519                    let keep = Self::should_keep_alive(&resp);
520                    let timeout = Self::keepalive_timeout(&resp);
521                    let outcome = match Self::decompress(
522                        &mut resp,
523                        self.port.shared.decompression,
524                        self.port.codec.max_response_body,
525                    ) {
526                        Ok(()) => Ok(resp),
527                        Err(e) => Err(e),
528                    };
529                    (outcome, keep, timeout)
530                }
531                Ok(None) => (
532                    Err(Error::Parse("incomplete response frame".into())),
533                    true,
534                    None,
535                ),
536                Err(e) => (Err(Error::Parse(e.to_string())), true, None),
537            };
538        if !keep_alive {
539            ctx.state.pending_close = true;
540        }
541        let buffered = outcome
542            .as_ref()
543            .map_or(buffered, |response| buffered.max(response.body().len()));
544        self.port.shared.push_response(
545            ctx.conn_id,
546            outcome,
547            buffered,
548            keepalive_timeout,
549            Instant::now(),
550        );
551    }
552
553    fn disconnect(&mut self, ctx: &mut connector::Ctx<'_, 'd, Self>) {
554        self.port.io.deactivate(ctx.conn_id);
555        self.port.shared.close_connection(ctx.conn_id);
556        ctx.state.pending_close = false;
557    }
558
559    fn pre_park(&mut self) {
560        self.port.timer.expire(Instant::now());
561    }
562
563    fn idle(&self) -> Idle {
564        Idle::Park(self.port.timer.earliest())
565    }
566}
567
568impl Session<'_> {
569    fn should_keep_alive(resp: &Response) -> bool {
570        let headers = resp.headers();
571        if headers.has_token(http::header::CONNECTION, "close")
572            || headers.has_token(http::header::CONNECTION, "upgrade")
573        {
574            return false;
575        }
576        if headers.has_token(http::header::CONNECTION, "keep-alive") {
577            return true;
578        }
579
580        let status = resp.status().as_u16();
581        if status < 200 || status == 204 || status == 304 {
582            return true;
583        }
584
585        headers.contains_key(http::header::CONTENT_LENGTH)
586            || headers.value_eq_ascii_case(http::header::TRANSFER_ENCODING, "chunked")
587    }
588
589    fn keepalive_timeout(resp: &Response) -> Option<Duration> {
590        let name = http::header::HeaderName::from_static("keep-alive");
591        let raw = resp.headers().get(name)?;
592        let value = raw.to_str().ok()?;
593        for part in value.split(',') {
594            let part = part.trim();
595            if let Some(rest) = part.strip_prefix("timeout=")
596                && let Ok(secs) = rest.trim().parse::<u64>()
597            {
598                return Some(Duration::from_secs(secs));
599            }
600        }
601        None
602    }
603
604    fn decompress(
605        resp: &mut Response,
606        policy: DecompressionPolicy,
607        max_body: usize,
608    ) -> Result<(), Error> {
609        let is_gzip = resp
610            .headers()
611            .value_eq_ascii_case(http::header::CONTENT_ENCODING, "gzip");
612        if !is_gzip || resp.body().is_empty() {
613            return Ok(());
614        }
615
616        let limit = max_body as u64;
617        let mut decoder = flate2::read::GzDecoder::new(resp.body()).take(limit + 1);
618        let mut decompressed = Vec::new();
619        match decoder.read_to_end(&mut decompressed) {
620            Ok(_) if decompressed.len() as u64 > limit => Err(Error::Parse(
621                "decompressed response body exceeds size limit".into(),
622            )),
623            Ok(_) => {
624                resp.set_body(decompressed);
625                resp.headers_mut().remove("content-encoding");
626                resp.headers_mut().remove("content-length");
627                Ok(())
628            }
629            Err(e) if policy == DecompressionPolicy::Strict => {
630                Err(Error::Parse(format!("gzip decompression failed: {e}")))
631            }
632            Err(_) => Ok(()),
633        }
634    }
635}