Skip to main content

bhtune_server/routes/opc/
mod.rs

1//! OPC DA diagnostic routes: `GET /api/opc/servers`, `GET /api/opc/capabilities`,
2//! `GET /api/opc/browse`, `DELETE /api/opc/browse/sessions/:id`, `GET /api/opc/search`,
3//! `GET /api/opc/read`, and the indexed-search status/refresh/control routes -- back the GUI's
4//! server dropdown, typed tag-tree browser, namespace search, index management, and connection
5//! test.
6//!
7//! Read-only diagnostics remain independent of [`crate::state::AppState::active_run`], and every
8//! OPC operation is bounded by an explicit `OPC_QUERY_TIMEOUT_SECS` timeout (see that constant's
9//! doc comment for why one is needed at all). Index management does not acquire the active-tune
10//! lock either: starting or controlling a gateway inventory must not block, or be blocked by, an
11//! in-flight tune.
12
13use std::convert::Infallible;
14use std::future::Future;
15use std::time::Duration;
16
17use axum::extract::{Path, Query, State};
18use axum::response::sse::{Event, KeepAlive, Sse};
19use axum::routing::{delete, get, post};
20use axum::{Json, Router};
21use bhtune_driver::{
22    BrowseNodeKind, BrowsePageRequest, BrowseSource, Driver, DriverResult, NamespaceOrganization,
23    OpcDaDriver, OpcDaGatewayCompatibility, SearchEvent, SearchIndexControlAction,
24    SearchIndexRequest, SearchMatch, SearchMatchMode, SearchRequest, check_gateway_compatibility,
25    list_opcda_servers,
26};
27use bhtune_runtime::tune::sample_quality_from_driver;
28
29use crate::error::{ApiError, ErrorBody};
30use crate::state::AppState;
31
32/// Bounds every OPC DA call this module makes. `opcda_bridge::Client::connect` has no
33/// connect timeout of its own (plain `tonic`, no `connect_timeout` configured), so a
34/// firewalled or black-holed gateway host would otherwise hang a request for however long
35/// the OS's own TCP-connect timeout is -- potentially minutes. 30s matches the runtime's
36/// built-in operation and restore timeout defaults in `config::tuning`, rather than
37/// inventing a new number. Each connect/browse/read call
38/// gets its own separate budget via [`with_timeout`], not one combined timeout spanning
39/// connect *and* the operation together.
40const OPC_QUERY_TIMEOUT_SECS: u64 = 30;
41
42/// Runs `fut` (one OPC DA driver call) under an [`OPC_QUERY_TIMEOUT_SECS`] deadline, mapping
43/// both a [`bhtune_driver::DriverError`] and an elapsed deadline to [`ApiError::BadRequest`]
44/// -- every failure this module can produce is "the gateway/tag couldn't be reached in
45/// time", a client-actionable diagnostic outcome, never an [`ApiError::Internal`] bug in this
46/// server. `what` names the attempted operation (e.g. `"connect to OPC server 'X'"`) so the
47/// error message identifies which step failed.
48enum TimedDriverCall<T> {
49    Ready(T),
50    Incompatible { operation: &'static str },
51    Failed(ApiError),
52}
53
54async fn timed_driver_call<T>(
55    what: &str,
56    timeout: Duration,
57    fut: impl Future<Output = DriverResult<T>>,
58) -> TimedDriverCall<T> {
59    match tokio::time::timeout(timeout, fut).await {
60        Ok(Ok(value)) => TimedDriverCall::Ready(value),
61        Ok(Err(bhtune_driver::DriverError::IncompatibleGateway { operation })) => {
62            TimedDriverCall::Incompatible { operation }
63        }
64        Ok(Err(err)) => TimedDriverCall::Failed(ApiError::BadRequest(format!("{what}: {err}"))),
65        Err(_) => TimedDriverCall::Failed(ApiError::BadRequest(format!(
66            "{what}: no response within {}s",
67            timeout.as_secs()
68        ))),
69    }
70}
71
72async fn with_timeout<T>(
73    what: &str,
74    fut: impl Future<Output = DriverResult<T>>,
75) -> Result<T, ApiError> {
76    match timed_driver_call(what, Duration::from_secs(OPC_QUERY_TIMEOUT_SECS), fut).await {
77        TimedDriverCall::Ready(value) => Ok(value),
78        TimedDriverCall::Incompatible { operation } => Err(ApiError::BadRequest(format!(
79            "{what}: {}",
80            bhtune_driver::DriverError::IncompatibleGateway { operation }
81        ))),
82        TimedDriverCall::Failed(error) => Err(error),
83    }
84}
85
86mod dto;
87mod handlers;
88mod helpers;
89mod validation;
90
91pub use dto::*;
92pub(crate) use handlers::{
93    __path_browse, __path_capabilities, __path_close_browse_session, __path_control_search_index,
94    __path_delete_search_index, __path_read, __path_refresh_search_index, __path_search,
95    __path_search_index, __path_search_index_status, __path_servers,
96    __path_set_search_index_auto_refresh,
97};
98pub(crate) use handlers::{
99    browse, capabilities, close_browse_session, control_search_index, delete_search_index, read,
100    refresh_search_index, search, search_index, search_index_status, servers,
101    set_search_index_auto_refresh,
102};
103
104pub fn router() -> Router<AppState> {
105    Router::new()
106        .route("/api/opc/servers", get(servers))
107        .route("/api/opc/capabilities", get(capabilities))
108        .route("/api/opc/browse", get(browse))
109        .route(
110            "/api/opc/browse/sessions/{session_id}",
111            delete(close_browse_session),
112        )
113        .route("/api/opc/search", get(search))
114        .route("/api/opc/search-index/status", get(search_index_status))
115        .route("/api/opc/search-index/search", get(search_index))
116        .route("/api/opc/search-index/refresh", post(refresh_search_index))
117        .route(
118            "/api/opc/search-index/auto-refresh",
119            post(set_search_index_auto_refresh),
120        )
121        .route("/api/opc/search-index/control", post(control_search_index))
122        .route("/api/opc/search-index", delete(delete_search_index))
123        .route("/api/opc/read", get(read))
124}