Skip to main content

opcua_client/session/
event_loop.rs

1use std::{
2    sync::{atomic::Ordering, Arc},
3    time::{Duration, Instant},
4};
5
6use futures::{future::BoxFuture, stream::BoxStream, FutureExt, Stream, StreamExt, TryStreamExt};
7use tracing::warn;
8
9use crate::{
10    retry::{ExponentialBackoff, SessionRetryPolicy},
11    session::{session_error, session_warn},
12    transport::{Connector, SecureChannelEventLoop, Transport, TransportPollResult},
13};
14use opcua_types::{
15    AttributeId, Error, QualifiedName, ReadValueId, StatusCode, TimestampsToReturn, VariableId,
16};
17
18use super::{
19    connect::{SessionConnectMode, SessionConnector},
20    services::subscriptions::event_loop::{SubscriptionActivity, SubscriptionEventLoop},
21    Session, SessionState,
22};
23
24/// A list of possible events that happens while polling the session.
25/// The client can use this list to monitor events such as disconnects,
26/// publish failures, etc.
27#[derive(Debug)]
28#[non_exhaustive]
29pub enum SessionPollResult {
30    /// A message was sent to or received from the server.
31    Transport(TransportPollResult),
32    /// Connection was lost with the inner [`StatusCode`].
33    ConnectionLost(StatusCode),
34    /// Reconnecting to the server failed with the inner [`StatusCode`].
35    ReconnectFailed(StatusCode),
36    /// Session was reconnected, the mode is given by the innner [`SessionConnectMode`]
37    Reconnected(SessionConnectMode),
38    /// The session performed some periodic activity.
39    SessionActivity(SessionActivity),
40    /// The session performed some subscription-related activity.
41    Subscription(SubscriptionActivity),
42    /// The session begins (re)connecting to the server.
43    BeginConnect,
44    /// Disconnect due to a keep alive terminated.
45    FinishedDisconnect,
46}
47
48struct ConnectedState<T: Transport + Send + Sync + 'static> {
49    channel: SecureChannelEventLoop<T>,
50    keep_alive: BoxStream<'static, SessionActivity>,
51    subscriptions: BoxStream<'static, SubscriptionActivity>,
52    current_failed_keep_alive_count: u64,
53    currently_closing: bool,
54    disconnect_fut: BoxFuture<'static, Result<(), Error>>,
55}
56
57// The way this is passed around, the Connected state being larger is
58// not generally a problem, since it should be the most common state by far.
59#[allow(clippy::large_enum_variant)]
60enum SessionEventLoopState<T: Transport + Send + Sync + 'static> {
61    Connected(ConnectedState<T>),
62    Connecting(SessionConnector, ExponentialBackoff, Instant),
63    Disconnected,
64}
65
66/// The session event loop drives the client. It must be polled for anything to happen at all.
67#[must_use = "The session event loop must be started for the session to work"]
68pub struct SessionEventLoop<T: Connector + Send + Sync + 'static> {
69    inner: Arc<Session>,
70    trigger_publish_recv: tokio::sync::watch::Receiver<Instant>,
71    close_rx: tokio::sync::watch::Receiver<bool>,
72    retry: SessionRetryPolicy,
73    keep_alive_interval: Duration,
74    max_failed_keep_alive_count: u64,
75    connector: T,
76}
77
78impl<T: Connector + Send + Sync + 'static> SessionEventLoop<T> {
79    pub(crate) fn new(
80        inner: Arc<Session>,
81        retry: SessionRetryPolicy,
82        trigger_publish_recv: tokio::sync::watch::Receiver<Instant>,
83        close_rx: tokio::sync::watch::Receiver<bool>,
84        keep_alive_interval: Duration,
85        max_failed_keep_alive_count: u64,
86        connector: T,
87    ) -> Self {
88        Self {
89            inner,
90            retry,
91            trigger_publish_recv,
92            close_rx,
93            keep_alive_interval,
94            max_failed_keep_alive_count,
95            connector,
96        }
97    }
98
99    /// Convenience method for running the session event loop until completion,
100    /// this method will return once the session is closed manually, or
101    /// after it fails to reconnect.
102    ///
103    /// # Returns
104    ///
105    /// * `StatusCode` - [Status code](StatusCode) indicating how the session terminated.
106    pub async fn run(self) -> StatusCode {
107        let stream = self.enter();
108        tokio::pin!(stream);
109        loop {
110            let r = stream.try_next().await;
111
112            match r {
113                Ok(None) => break StatusCode::Good,
114                Err(e) => break e,
115                _ => (),
116            }
117        }
118    }
119
120    /// Convenience method for running the session event loop until completion on a tokio task.
121    /// This method will return a [`JoinHandle`](tokio::task::JoinHandle) that will terminate
122    /// once the session is closed manually, or after it fails to reconnect.
123    ///
124    /// # Returns
125    ///
126    /// * `JoinHandle<StatusCode>` - Handle to a tokio task wrapping the event loop.
127    pub fn spawn(self) -> tokio::task::JoinHandle<StatusCode> {
128        tokio::task::spawn(self.run())
129    }
130
131    /// Start the event loop, returning a stream that must be polled until it is closed.
132    /// The stream will return `None` when the transport is closed manually, or
133    /// `Some(Err(StatusCode))` when the stream fails to reconnect after a loss of connection.
134    ///
135    /// It yields events from normal session operation, which can be used to take specific actions
136    /// based on changes to the session state.
137    pub fn enter(self) -> impl Stream<Item = Result<SessionPollResult, StatusCode>> {
138        futures::stream::try_unfold(
139            (self, SessionEventLoopState::Disconnected),
140            |(mut slf, state)| async move {
141                // If SessionDropGuard was dropped, stop the loop immediately.
142                if *slf.close_rx.borrow_and_update() {
143                    return Ok(None);
144                }
145
146                let (res, state) = match state {
147                    SessionEventLoopState::Connected(mut state) => {
148                        tokio::select! {
149                            r = state.channel.poll() => {
150                                if let TransportPollResult::Closed(code) = r {
151                                    session_warn!(slf.inner, "Transport disconnected: {code}");
152                                    let _ = slf.inner.state_watch_tx.send(SessionState::Disconnected);
153
154                                    let should_reconnect = slf.inner.should_reconnect.load(Ordering::Relaxed);
155                                    if !should_reconnect {
156                                        return Ok(None);
157                                    }
158
159                                    Ok((
160                                        SessionPollResult::ConnectionLost(code),
161                                        SessionEventLoopState::Disconnected,
162                                    ))
163                                } else {
164                                    Ok((
165                                        SessionPollResult::Transport(r),
166                                        SessionEventLoopState::Connected(state),
167                                    ))
168                                }
169                            }
170                            r = state.keep_alive.next() => {
171                                // Should never be null, fail out
172                                let Some(r) = r else {
173                                    session_error!(slf.inner, "Session activity loop ended unexpectedly");
174                                    return Err(StatusCode::BadUnexpectedError);
175                                };
176
177                                match r {
178                                    SessionActivity::KeepAliveSucceeded => state.current_failed_keep_alive_count = 0,
179                                    SessionActivity::KeepAliveFailed(status_code) => {
180                                        session_warn!(slf.inner, "Keep alive failed: {status_code}");
181                                        state.current_failed_keep_alive_count += 1;
182                                        if !state.currently_closing
183                                            && state.current_failed_keep_alive_count >= slf.max_failed_keep_alive_count
184                                            && slf.max_failed_keep_alive_count != 0
185                                        {
186                                            session_error!(slf.inner, "Maximum number of failed keep-alives exceed limit, session will be closed.");
187                                            state.currently_closing = true;
188                                            let s = slf.inner.clone();
189                                            state.disconnect_fut = async move {
190                                                s.disconnect_inner(false, false).await
191                                            }.boxed();
192                                        }
193                                    },
194                                }
195
196                                Ok((
197                                    SessionPollResult::SessionActivity(r),
198                                    SessionEventLoopState::Connected(state),
199                                ))
200                            }
201                            r = state.subscriptions.next() => {
202                                // Should never be null, fail out
203                                let Some(r) = r else {
204                                    session_error!(slf.inner, "Subscription event loop ended unexpectedly");
205                                    return Err(StatusCode::BadUnexpectedError);
206                                };
207
208                                if let SubscriptionActivity::FatalFailure(e) = &r {
209                                    if !state.currently_closing {
210                                        session_error!(slf.inner, "Fatal error from subscription loop ({e}), session will be closed.");
211                                        state.currently_closing = true;
212                                        let s = slf.inner.clone();
213                                        state.disconnect_fut = async move {
214                                            s.disconnect_inner(false, false).await
215                                        }.boxed();
216                                    }
217                                }
218
219                                Ok((
220                                    SessionPollResult::Subscription(r),
221                                    SessionEventLoopState::Connected(state),
222                                ))
223                            }
224                            _ = &mut state.disconnect_fut => {
225                                // Do nothing, if this terminates we will very soon be transitioning
226                                // to a disconnected state.
227                                Ok((
228                                    SessionPollResult::FinishedDisconnect,
229                                    SessionEventLoopState::Connected(state)
230                                ))
231                            }
232                            _ = slf.close_rx.changed(), if !state.currently_closing => {
233                                if *slf.close_rx.borrow_and_update() {
234                                    state.currently_closing = true;
235                                    let s = slf.inner.clone();
236                                    state.disconnect_fut = async move {
237                                        s.disconnect_inner(true, false).await
238                                    }.boxed();
239                                }
240                                Ok((
241                                    SessionPollResult::FinishedDisconnect,
242                                    SessionEventLoopState::Connected(state),
243                                ))
244                            }
245                        }
246                    }
247                    SessionEventLoopState::Disconnected => {
248                        let connector = SessionConnector::new(slf.inner.clone());
249
250                        let _ = slf.inner.state_watch_tx.send(SessionState::Connecting);
251
252                        Ok((
253                            SessionPollResult::BeginConnect,
254                            SessionEventLoopState::Connecting(
255                                connector,
256                                slf.retry.new_backoff(),
257                                Instant::now(),
258                            ),
259                        ))
260                    }
261                    SessionEventLoopState::Connecting(connector, mut backoff, next_try) => {
262                        tokio::time::sleep_until(next_try.into()).await;
263
264                        match connector.try_connect(&slf.connector).await {
265                            Ok((channel, result)) => {
266                                let _ = slf.inner.state_watch_tx.send(SessionState::Connected);
267                                Ok((
268                                    SessionPollResult::Reconnected(result),
269                                    SessionEventLoopState::Connected(ConnectedState {
270                                        channel,
271                                        keep_alive: SessionActivityLoop::new(
272                                            slf.inner.clone(),
273                                            slf.keep_alive_interval,
274                                        )
275                                        .run()
276                                        .boxed(),
277                                        subscriptions: SubscriptionEventLoop::new(
278                                            slf.inner.clone(),
279                                            slf.trigger_publish_recv.clone(),
280                                        )
281                                        .run()
282                                        .boxed(),
283                                        current_failed_keep_alive_count: 0,
284                                        currently_closing: false,
285                                        disconnect_fut: futures::future::pending().boxed(),
286                                    }),
287                                ))
288                            }
289                            Err(e) => {
290                                warn!("Failed to connect to server: {e}");
291                                match backoff.next() {
292                                    Some(x) => Ok((
293                                        SessionPollResult::ReconnectFailed(e.status()),
294                                        SessionEventLoopState::Connecting(
295                                            connector,
296                                            backoff,
297                                            Instant::now() + x,
298                                        ),
299                                    )),
300                                    None => Err(e),
301                                }
302                            }
303                        }
304                    }
305                }?;
306
307                Ok(Some((res, (slf, state))))
308            },
309        )
310    }
311}
312
313/// Periodic activity performed by the session.
314#[derive(Debug, Clone)]
315pub enum SessionActivity {
316    /// A keep alive request was sent to the server and a response was received with a successful state.
317    KeepAliveSucceeded,
318    /// A keep alive request was sent to the server, but it failed or the server was in an invalid state.
319    KeepAliveFailed(StatusCode),
320}
321
322enum SessionTickEvent {
323    KeepAlive,
324}
325
326struct SessionIntervals {
327    keep_alive: tokio::time::Interval,
328}
329
330impl SessionIntervals {
331    fn new(keep_alive_interval: Duration) -> Self {
332        let mut keep_alive = tokio::time::interval(keep_alive_interval);
333        keep_alive.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
334
335        Self { keep_alive }
336    }
337
338    async fn next(&mut self) -> SessionTickEvent {
339        tokio::select! {
340            _ = self.keep_alive.tick() => SessionTickEvent::KeepAlive
341        }
342    }
343}
344
345struct SessionActivityLoop {
346    inner: Arc<Session>,
347    tick_gen: SessionIntervals,
348}
349
350impl SessionActivityLoop {
351    fn new(inner: Arc<Session>, keep_alive_interval: Duration) -> Self {
352        Self {
353            inner,
354            tick_gen: SessionIntervals::new(keep_alive_interval),
355        }
356    }
357
358    fn run(self) -> impl Stream<Item = SessionActivity> {
359        futures::stream::unfold(self, |mut slf| async move {
360            match slf.tick_gen.next().await {
361                SessionTickEvent::KeepAlive => {
362                    let now = Instant::now();
363                    let res = slf
364                        .inner
365                        .read(
366                            &[ReadValueId {
367                                node_id: VariableId::Server_ServerStatus_State.into(),
368                                attribute_id: AttributeId::Value as u32,
369                                index_range: Default::default(),
370                                data_encoding: QualifiedName::null(),
371                            }],
372                            TimestampsToReturn::Server,
373                            1f64,
374                        )
375                        .await;
376                    let elapsed = now.elapsed();
377
378                    let data_value = match res.map(|r| r.into_iter().next()) {
379                        Ok(Some(data_value)) => {
380                            // Only update if the request was successful to avoid
381                            // skewing the roundtrip time by processing timeouts.
382                            slf.inner
383                                .publish_limits_watch_tx
384                                .send_modify(|limits| limits.update_message_roundtrip(elapsed));
385                            data_value
386                        }
387                        // Should not be possible, this would be a bug in
388                        // the server, assume everything is terrible.
389                        Ok(None) => {
390                            return Some((
391                                SessionActivity::KeepAliveFailed(StatusCode::BadUnknownResponse),
392                                slf,
393                            ))
394                        }
395                        Err(e) => return Some((SessionActivity::KeepAliveFailed(e.status()), slf)),
396                    };
397
398                    match data_value.value.and_then(|v| v.try_cast_to().ok()) {
399                        Some(0) => Some((SessionActivity::KeepAliveSucceeded, slf)),
400                        Some(s) => {
401                            warn!("Keep alive failed, non-running status code {s}");
402                            Some((
403                                SessionActivity::KeepAliveFailed(StatusCode::BadServerHalted),
404                                slf,
405                            ))
406                        }
407                        None => Some((
408                            SessionActivity::KeepAliveFailed(StatusCode::BadUnknownResponse),
409                            slf,
410                        )),
411                    }
412                }
413            }
414        })
415    }
416}