1use std::{
5 sync::{Arc, Weak, atomic::AtomicU32},
6 time::Duration,
7};
8
9use anyhow::{Context, Result, ensure};
10use dashmap::DashMap;
11use once_cell::sync::OnceCell;
12use tokio_util::sync::CancellationToken;
13use tracing::{debug, info, warn};
14
15use crate::{
16 cfg::config::{AuthConfig, Config},
17 client::client::ClientConnection,
18 models::{data_fromat, logout::common::LogoutReason, nop::response::NopInResponse},
19 state_machine::{
20 common::StateMachineCtx, login::common::LoginCtx, logout_states::LogoutCtx,
21 nop_states::NopCtx,
22 },
23 utils::generate_isid,
24};
25
26#[derive(Debug)]
32pub struct Connection {
33 pub cid: u16,
35 pub conn: Arc<ClientConnection>,
37 pub exp_stat_sn: Arc<AtomicU32>,
40}
41
42#[derive(Debug)]
49pub struct Session {
50 pub tsih: u16,
52 pub isid: [u8; 6],
55 pub target_name: Arc<str>,
57 pub conns: DashMap<u16, Arc<Connection>>,
59
60 cmd_sn: Arc<AtomicU32>,
63 itt_gen: Arc<AtomicU32>,
66}
67
68pub struct Pool {
74 pub sessions: DashMap<u16, Arc<Session>>,
76 max_sessions: u32,
78 max_connections: u16,
80 self_weak: OnceCell<Weak<Pool>>,
82
83 cancel: CancellationToken,
87}
88
89const MAX_CONNECTION_RECOVERY_ATTEMPTS: usize = 3;
90
91impl Pool {
92 pub fn new(cfg: &Config) -> Self {
94 Self {
95 sessions: DashMap::with_capacity(cfg.runtime.max_sessions as usize),
96 max_sessions: cfg.runtime.max_sessions,
97 max_connections: cfg.login.limits.max_connections,
98 self_weak: OnceCell::new(),
99 cancel: CancellationToken::new(),
100 }
101 }
102
103 pub fn with_cancel(cfg: &Config, cancel: CancellationToken) -> Self {
105 Self {
106 sessions: DashMap::with_capacity(cfg.runtime.max_sessions as usize),
107 max_sessions: cfg.runtime.max_sessions,
108 max_connections: cfg.login.limits.max_connections,
109 self_weak: OnceCell::new(),
110 cancel,
111 }
112 }
113
114 #[inline]
116 pub fn cancel_token(&self) -> CancellationToken {
117 self.cancel.clone()
118 }
119
120 pub fn attach_self(self: &Arc<Self>) {
122 let _ = self.self_weak.set(Arc::downgrade(self));
123 }
124
125 pub async fn login_sessions_from_cfg(&self, cfg: &Config) -> Result<Vec<u16>> {
127 ensure!(self.max_sessions > 0, "max_sessions must be > 0");
128
129 let target_name: Arc<str> = Arc::from(cfg.login.identity.target_name.clone());
130 let mut tsihs = Vec::with_capacity(self.max_sessions as usize);
131
132 for _ in 0..self.max_sessions {
133 let child = self.cancel.child_token();
134 let conn = ClientConnection::connect(cfg.clone(), child).await?;
135 let (isid, _) = generate_isid();
136
137 let tsih = self
138 .login_and_insert(target_name.clone(), isid, 0u16, conn)
139 .await?;
140
141 tsihs.push(tsih);
142 }
143
144 Ok(tsihs)
145 }
146
147 pub async fn login_and_insert(
150 &self,
151 target_name: Arc<str>,
152 isid: [u8; 6],
153 cid: u16,
154 conn: Arc<ClientConnection>,
155 ) -> Result<u16> {
156 self.login_one_and_insert_impl(
157 target_name,
158 isid,
159 0,
160 cid,
161 conn,
162 )
163 .await
164 }
165
166 pub async fn add_connection_to_session(
168 &self,
169 tsih: u16,
170 cid: u16,
171 conn: Arc<ClientConnection>,
172 ) -> Result<()> {
173 let (target_name, isid) = {
175 let sess = self
176 .sessions
177 .get(&tsih)
178 .ok_or_else(|| anyhow::anyhow!("unknown TSIH={tsih}"))?;
179 (sess.target_name.clone(), sess.isid)
180 };
181 let _ = self
182 .login_one_and_insert_impl(target_name, isid, tsih, cid, conn)
183 .await?;
184 Ok(())
185 }
186
187 fn drop_connection_local(&self, tsih: u16, cid: u16) {
188 let should_remove_session = if let Some(sess) = self.sessions.get(&tsih) {
189 sess.conns.remove(&cid);
190 sess.conns.is_empty()
191 } else {
192 false
193 };
194
195 if should_remove_session {
196 self.sessions.remove(&tsih);
197 }
198 }
199
200 async fn recover_connection(
201 &self,
202 tsih: u16,
203 cid: u16,
204 expected: Arc<Connection>,
205 ) -> Result<()> {
206 let sess = self
207 .sessions
208 .get(&tsih)
209 .with_context(|| format!("unknown TSIH={tsih}"))?
210 .clone();
211
212 if let Some(current) = sess.conns.get(&cid).map(|entry| entry.clone())
213 && !Arc::ptr_eq(¤t, &expected)
214 && !current.conn.is_poisoned()
215 {
216 return Ok(());
217 }
218
219 let target_name = sess.target_name.clone();
220 let isid = sess.isid;
221 let cfg = expected.conn.cfg.clone();
222 let mut removed = None;
223
224 if let Some(current) = sess.conns.get(&cid).map(|entry| entry.clone()) {
225 if Arc::ptr_eq(¤t, &expected) || current.conn.is_poisoned() {
226 removed = sess.conns.remove(&cid).map(|(_, conn)| conn);
227 } else {
228 return Ok(());
229 }
230 }
231
232 let child = self.cancel.child_token();
233 let recovery = async {
234 let conn = ClientConnection::connect(cfg, child).await?;
235 let _ = self
236 .login_one_and_insert_impl(target_name, isid, tsih, cid, conn)
237 .await?;
238 Ok(())
239 }
240 .await;
241
242 if recovery.is_err()
243 && let Some(previous) = removed
244 && sess.conns.get(&cid).is_none()
245 {
246 sess.conns.insert(cid, previous);
247 }
248
249 recovery
250 }
251
252 async fn login_one_and_insert_impl(
253 &self,
254 target_name: Arc<str>,
255 isid: [u8; 6],
256 tsih_hint: u16,
257 cid: u16,
258 conn: Arc<ClientConnection>,
259 ) -> Result<u16> {
260 let mut l = LoginCtx::new(conn.clone(), isid, cid, tsih_hint);
261 match &conn.cfg.login.auth {
262 AuthConfig::Chap(_) => l.set_chap_login(),
263 AuthConfig::None => l.set_plain_login(),
264 }
265
266 let login_pdu = l.execute(&self.cancel).await.context("login failed")?;
267 let hdr = login_pdu.header_view()?;
268
269 let tsih = hdr.tsih.get();
270 ensure!(tsih != 0, "TSIH=0 in final Login Response");
271
272 let sess = self
273 .sessions
274 .entry(tsih)
275 .or_insert_with(|| {
276 Arc::new(Session {
277 tsih,
278 isid,
279 target_name: target_name.clone(),
280 conns: DashMap::with_capacity(self.max_connections as usize),
281 cmd_sn: Arc::new(AtomicU32::new(hdr.exp_cmd_sn.get())),
282 itt_gen: Arc::new(AtomicU32::new(
283 hdr.initiator_task_tag.get().wrapping_add(1),
284 )),
285 })
286 })
287 .clone();
288
289 let inserted = sess.conns.insert(
290 cid,
291 Arc::new(Connection {
292 cid,
293 conn: conn.clone(),
294 exp_stat_sn: Arc::new(AtomicU32::new(hdr.stat_sn.get().wrapping_add(1))),
295 }),
296 );
297 ensure!(
298 inserted.is_none(),
299 "CID={cid} already exists in TSIH={tsih}"
300 );
301
302 if let Some(w) = self.self_weak.get().cloned() {
303 conn.bind_pool_session(w, tsih, cid);
304 } else {
305 warn!(
306 "Pool::attach_self() was not called; unsolicited NOP auto-reply will be \
307 disabled"
308 );
309 }
310
311 Ok(tsih)
312 }
313
314 async fn logout_connection(
316 &self,
317 tsih: u16,
318 cid: u16,
319 reason: LogoutReason,
320 ) -> Result<()> {
321 let sess = self
322 .sessions
323 .get(&tsih)
324 .with_context(|| format!("unknown TSIH={tsih}"))?
325 .clone();
326 let conn = sess
327 .conns
328 .get(&cid)
329 .with_context(|| format!("CID={cid} not found in TSIH={tsih}"))?
330 .clone();
331
332 let mut lo = LogoutCtx::new(
333 conn.conn.clone(),
334 sess.itt_gen.clone(),
335 sess.cmd_sn.clone(),
336 conn.exp_stat_sn.clone(),
337 cid,
338 reason.clone(),
339 );
340 lo.execute(&conn.conn.stop_writes)
341 .await
342 .context("logout (CloseConnection) failed")?;
343
344 if reason != LogoutReason::RemoveConnectionForRecovery {
346 sess.conns.remove(&cid);
347 if sess.conns.is_empty() {
348 self.sessions.remove(&tsih);
349 }
350 }
351 Ok(())
352 }
353
354 pub async fn logout_session(&self, tsih: u16) -> Result<()> {
356 let sess = self
357 .sessions
358 .get(&tsih)
359 .with_context(|| format!("unknown TSIH={tsih}"))?
360 .clone();
361
362 if let Some(cid0) = sess.conns.iter().map(|e| *e.key()).min() {
363 let conn = sess
364 .conns
365 .get(&cid0)
366 .expect("CID just collected must exist")
367 .clone();
368
369 let mut lo = LogoutCtx::new(
370 conn.conn.clone(),
371 sess.itt_gen.clone(),
372 sess.cmd_sn.clone(),
373 conn.exp_stat_sn.clone(),
374 cid0,
375 LogoutReason::CloseSession,
376 );
377 lo.execute(&conn.conn.stop_writes)
378 .await
379 .context("logout (CloseSession) failed")?;
380 }
381
382 if let Some((_, s)) = self.sessions.remove(&tsih) {
383 for cid in s.conns.iter().map(|kv| *kv.key()).collect::<Vec<_>>() {
385 let _ = s.conns.remove(&cid);
386 }
387 }
388 Ok(())
389 }
390
391 pub async fn logout(
401 &self,
402 tsih: u16,
403 reason: LogoutReason,
404 cid: Option<u16>,
405 ) -> Result<()> {
406 match reason {
407 LogoutReason::CloseSession => self.logout_session(tsih).await,
408 LogoutReason::CloseConnection | LogoutReason::RemoveConnectionForRecovery => {
409 self.logout_connection(tsih, cid.context("failed to get cid")?, reason)
410 .await
411 },
412 }
413 }
414
415 pub async fn shutdown_gracefully(&self, max_wait_per_conn: Duration) -> Result<()> {
423 let all_connections: Vec<Arc<Connection>> = self
424 .sessions
425 .iter()
426 .flat_map(|s| {
427 s.conns
428 .iter()
429 .map(|c| c.value().clone())
430 .collect::<Vec<_>>()
431 })
432 .collect();
433
434 debug!("notify state machines to stop writing ti socket");
435 for c in &all_connections {
436 if let Err(e) = c.conn.graceful_quiesce(max_wait_per_conn).await {
437 warn!("drain failed on TSIH={}?, CID={}: {}", c.cid, c.cid, e);
438 }
439 }
440
441 debug!("call logout session for 1 connectionf of all sessions");
442 let tsihs = self.sessions.iter().map(|e| *e.key()).collect::<Vec<_>>();
443 for tsih in tsihs {
444 if let Err(e) = self.logout_session(tsih).await {
445 warn!(
446 "logout_session(TSIH={}) failed during shutdown: {}",
447 tsih, e
448 );
449 if let Some((_, s)) = self.sessions.remove(&tsih) {
450 for cid in s.conns.iter().map(|kv| *kv.key()).collect::<Vec<_>>() {
451 let _ = s.conns.remove(&cid);
452 }
453 }
454 }
455 }
456
457 debug!("close socket to target on connection");
458 for c in &all_connections {
459 if let Err(e) = c.conn.half_close_writes().await {
460 warn!("half_close_writes failed on CID={}: {}", c.cid, e);
461 }
462 }
463
464 self.sessions.clear();
465
466 debug!("Set cancel enable");
467 self.cancel.cancel();
468 info!("Pool graceful shutdown completed.");
469 Ok(())
470 }
471
472 pub async fn execute_with<Ctx, Res, Build>(
480 &self,
481 tsih: u16,
482 cid: u16,
483 build: Build,
484 ) -> Result<Res>
485 where
486 Build: for<'a> Fn(
487 Arc<ClientConnection>,
488 Arc<AtomicU32>, Arc<AtomicU32>, Arc<AtomicU32>, ) -> Ctx,
492 Ctx: StateMachineCtx<Ctx, Res>,
493 {
494 for attempt in 0..=MAX_CONNECTION_RECOVERY_ATTEMPTS {
495 let sess = self
496 .sessions
497 .get(&tsih)
498 .with_context(|| format!("unknown TSIH={tsih}"))?
499 .clone();
500 let conn = sess
501 .conns
502 .get(&cid)
503 .with_context(|| format!("CID={cid} not found in TSIH={tsih}"))?
504 .clone();
505
506 if conn.conn.is_poisoned() {
507 warn!(
508 "TSIH={}, CID={} is poisoned before execute attempt {}",
509 tsih,
510 cid,
511 attempt + 1,
512 );
513 } else {
514 let mut ctx = build(
515 conn.conn.clone(),
516 sess.itt_gen.clone(),
517 sess.cmd_sn.clone(),
518 conn.exp_stat_sn.clone(),
519 );
520 match ctx.execute(&conn.conn.stop_writes).await {
521 Ok(res) => return Ok(res),
522 Err(error) if conn.conn.is_poisoned() => {
523 warn!(
524 "TSIH={}, CID={} poisoned during execute attempt {}: {}",
525 tsih,
526 cid,
527 attempt + 1,
528 error
529 );
530 },
531 Err(error) => return Err(error),
532 }
533 }
534
535 if attempt == MAX_CONNECTION_RECOVERY_ATTEMPTS {
536 self.drop_connection_local(tsih, cid);
537 return Err(anyhow::anyhow!(
538 "connection recovery attempts exhausted for TSIH={}, CID={}",
539 tsih,
540 cid
541 ));
542 }
543
544 match self.recover_connection(tsih, cid, conn.clone()).await {
545 Ok(()) => {
546 debug!(
547 "recovered TSIH={}, CID={} after poisoned connection",
548 tsih, cid
549 );
550 },
551 Err(error) => {
552 warn!(
553 "failed to recover TSIH={}, CID={} on attempt {}: {}",
554 tsih,
555 cid,
556 attempt + 1,
557 error
558 );
559 },
560 }
561 }
562
563 Err(anyhow::anyhow!(
564 "connection recovery attempts exhausted for TSIH={}, CID={}",
565 tsih,
566 cid
567 ))
568 }
569
570 pub(crate) async fn execute_nop_reply(
571 &self,
572 tsih: u16,
573 cid: u16,
574 pdu: data_fromat::PduResponse<NopInResponse>,
575 ) -> Result<()> {
576 let sess = self
577 .sessions
578 .get(&tsih)
579 .with_context(|| format!("unknown TSIH={tsih}"))?
580 .clone();
581 let conn = sess
582 .conns
583 .get(&cid)
584 .with_context(|| format!("CID={cid} not found in TSIH={tsih}"))?
585 .clone();
586
587 let mut ctx = NopCtx::for_reply(
588 conn.conn.clone(),
589 sess.itt_gen.clone(),
590 sess.cmd_sn.clone(),
591 conn.exp_stat_sn.clone(),
592 pdu,
593 )
594 .expect("failed to build NopCtx::for_reply");
595 ctx.execute(&conn.conn.stop_writes).await.map(|_| ())
596 }
597}
598
599impl Drop for Pool {
600 fn drop(&mut self) {
601 for sess in self.sessions.iter() {
604 for c in sess.conns.iter() {
605 c.value().conn.stop_writes.cancel();
606 }
607 }
608 self.cancel.cancel();
610 }
611}