1use std::sync::Arc;
2use std::time::Duration;
3
4use spdy_mux::{
5 split_fastws,
6 split_raw_spdy,
7};
8use tokio_util::sync::CancellationToken;
9
10use crate::connect::upgrade_spdy_with_fallback;
11use crate::error::Error;
12use crate::recovery::RecoveryCallback;
13use crate::session::Session;
14use crate::subprotocol::Subprotocol;
15
16const DEFAULT_PING: Duration = Duration::from_secs(15);
17const DEFAULT_WATCHDOG: Duration = Duration::from_secs(30);
18const DEFAULT_DRAIN: Duration = Duration::from_secs(2);
19
20const DEFAULT_SPDY_POOL_SIZE: usize = 6;
22
23#[derive(Clone)]
25pub struct Client {
26 kube: kube::Client,
27 cluster_url: http::Uri,
28}
29
30impl Client {
31 pub const fn new(kube_client: kube::Client, cluster_url: http::Uri) -> Self {
32 Self {
33 kube: kube_client,
34 cluster_url,
35 }
36 }
37
38 pub fn builder() -> ClientBuilder {
39 ClientBuilder::default()
40 }
41
42 pub fn session(
43 &self, namespace: impl Into<String>, pod: impl Into<String>, port: u16,
44 ) -> SessionBuilder<'_> {
45 SessionBuilder {
46 client: self,
47 namespace: namespace.into(),
48 pod: pod.into(),
49 port,
50 ping_interval: DEFAULT_PING,
51 watchdog_timeout: DEFAULT_WATCHDOG,
52 drain_timeout: DEFAULT_DRAIN,
53 cancel: None,
54 recovery_callback: None,
55 spdy_pool_size: DEFAULT_SPDY_POOL_SIZE,
56 }
57 }
58
59 pub const fn kube_client(&self) -> &kube::Client {
60 &self.kube
61 }
62
63 pub const fn cluster_url(&self) -> &http::Uri {
64 &self.cluster_url
65 }
66}
67
68#[derive(Default)]
69pub struct ClientBuilder {
70 kube: Option<kube::Client>,
71 cluster_url: Option<http::Uri>,
72}
73
74impl ClientBuilder {
75 pub fn kube_client(mut self, c: kube::Client) -> Self {
76 self.kube = Some(c);
77 self
78 }
79
80 pub fn cluster_url(mut self, u: http::Uri) -> Self {
81 self.cluster_url = Some(u);
82 self
83 }
84
85 pub fn build(self) -> Result<Client, Error> {
86 let kube = self
87 .kube
88 .ok_or_else(|| Error::Configuration("kube_client is required".into()))?;
89 let cluster_url = self
90 .cluster_url
91 .ok_or_else(|| Error::Configuration("cluster_url is required".into()))?;
92 Ok(Client::new(kube, cluster_url))
93 }
94}
95
96pub struct SessionBuilder<'c> {
98 client: &'c Client,
99 namespace: String,
100 pod: String,
101 port: u16,
102 #[allow(dead_code)] ping_interval: Duration,
104 #[allow(dead_code)] watchdog_timeout: Duration,
106 #[allow(dead_code)] drain_timeout: Duration,
108 cancel: Option<CancellationToken>,
109 recovery_callback: Option<RecoveryCallback>,
110 spdy_pool_size: usize,
111}
112
113impl SessionBuilder<'_> {
114 pub const fn capacity(self, _n: usize) -> Self {
116 self
117 }
118
119 pub const fn keepalive(mut self, ping: Duration, watchdog: Duration) -> Self {
122 self.ping_interval = ping;
123 self.watchdog_timeout = watchdog;
124 self
125 }
126
127 pub const fn shutdown_grace(mut self, drain: Duration) -> Self {
128 self.drain_timeout = drain;
129 self
130 }
131
132 pub fn cancellation_token(mut self, t: CancellationToken) -> Self {
133 self.cancel = Some(t);
134 self
135 }
136
137 pub fn spdy_pool_size(mut self, n: usize) -> Self {
140 self.spdy_pool_size = n.max(1);
141 self
142 }
143
144 pub fn on_recovery<F>(mut self, cb: F) -> Self
145 where
146 F: Fn(crate::recovery::RecoverySignal) + Send + Sync + 'static,
147 {
148 self.recovery_callback = Some(Arc::new(cb));
149 self
150 }
151
152 pub async fn open(self) -> Result<Session, Error> {
153 let cancel = self.cancel.unwrap_or_default();
154 let recovery_callback: RecoveryCallback = self
155 .recovery_callback
156 .unwrap_or_else(|| Arc::new(|_signal| {}));
157
158 open_spdy_session(
159 self.client,
160 &self.namespace,
161 &self.pod,
162 self.port,
163 self.spdy_pool_size,
164 cancel,
165 recovery_callback,
166 )
167 .await
168 }
169}
170
171async fn open_spdy_session(
174 client: &Client, namespace: &str, pod: &str, port: u16, pool_size: usize,
175 cancel: CancellationToken, recovery_callback: RecoveryCallback,
176) -> Result<Session, Error> {
177 let first = upgrade_spdy_with_fallback(
178 client.kube_client(),
179 client.cluster_url(),
180 namespace,
181 pod,
182 &recovery_callback,
183 )
184 .await?;
185 let chosen_protocol = first.protocol;
186 let first_upgraded = first.upgraded;
187
188 tracing::info!(
189 pod = %pod,
190 pool_size,
191 protocol = %chosen_protocol,
192 "SPDY tunnel: probe succeeded"
193 );
194
195 let extra_upgrades = if pool_size > 1 {
196 let t_parallel = std::time::Instant::now();
197 let mut join_set = tokio::task::JoinSet::new();
198 for i in 1..pool_size {
199 let kube = client.kube_client().clone();
200 let url = client.cluster_url().clone();
201 let ns = namespace.to_owned();
202 let pod_name = pod.to_owned();
203 join_set.spawn(async move {
204 let result = match chosen_protocol {
205 Subprotocol::Spdy31Tunnel => {
206 crate::connect::upgrade_spdy_tunnel(&kube, &url, &ns, &pod_name).await
207 }
208 Subprotocol::LegacySpdy => {
209 crate::connect::upgrade_legacy_spdy(&kube, &url, &ns, &pod_name).await
210 }
211 };
212 (i, result)
213 });
214 }
215 let mut succeeded = Vec::with_capacity(pool_size - 1);
216 while let Some(join_result) = join_set.join_next().await {
217 match join_result {
218 Ok((_, Ok(upgraded))) => succeeded.push(upgraded.upgraded),
219 Ok((i, Err(e))) => {
220 tracing::debug!("SPDY pool: connection {i}/{pool_size} failed: {e}");
221 }
222 Err(e) => {
223 tracing::debug!("SPDY pool: connection task panicked: {e}");
224 }
225 }
226 }
227 tracing::info!(
228 pool_opened = succeeded.len() + 1,
229 pool_target = pool_size,
230 elapsed_ms = u64::try_from(t_parallel.elapsed().as_millis()).unwrap_or(u64::MAX),
231 "SPDY pool: parallel connections opened"
232 );
233 succeeded
234 } else {
235 Vec::new()
236 };
237
238 let config = spdy_mux::MuxConfig {
239 pool_size: extra_upgrades.len() + 1,
240 ..Default::default()
241 };
242 let all_upgrades = std::iter::once(first_upgraded).chain(extra_upgrades);
243 let t_pool = std::time::Instant::now();
244 let spdy_session = match chosen_protocol {
245 Subprotocol::Spdy31Tunnel => {
246 let pairs: Vec<_> = all_upgrades.map(split_fastws).collect();
247 spdy_mux::Session::with_config(pairs, cancel.clone(), config).await
248 }
249 Subprotocol::LegacySpdy => {
250 let pairs: Vec<_> = all_upgrades.map(split_raw_spdy).collect();
251 spdy_mux::Session::with_config(pairs, cancel.clone(), config).await
252 }
253 }
254 .map_err(Error::from)?;
255
256 tracing::info!(
257 pod = %pod,
258 pool_healthy = spdy_session.capacity() > 0,
259 pool_init_ms = u64::try_from(t_pool.elapsed().as_millis()).unwrap_or(u64::MAX),
260 protocol = %chosen_protocol,
261 "SPDY session ready"
262 );
263 Ok(Session::from_spdy(spdy_session, chosen_protocol, port))
264}