use std::future::Future;
use std::pin::pin;
use std::sync::atomic::{AtomicU8, Ordering};
use std::sync::Arc;
use std::task::{Context, Poll, Wake};
use std::time::{Duration, Instant};
use asupersync::io::{AsyncRead, AsyncWrite};
use asupersync::sync::Mutex as AsyncMutex;
use asupersync::types::{CancelKind, CancelReason};
use asupersync::{time, Cx};
use oracledb_protocol::wire::ProtocolLimits;
use crate::{
break_and_drain_wire_unbounded_with_limits, drain_cancel_wire_unbounded_with_limits,
duration_to_millis_saturating, Error, ErrorKind, Result,
};
#[derive(Clone, Copy, Debug)]
pub(crate) enum RecoveryWireAction {
BreakAndDrain,
DrainCancel,
}
impl RecoveryWireAction {
fn timeout_message(self) -> &'static str {
match self {
Self::BreakAndDrain => "socket timed out while recovering from previous call timeout",
Self::DrainCancel => "socket timed out while draining cancel response",
}
}
fn wire_error_prefix(self) -> &'static str {
match self {
Self::BreakAndDrain => "wire error while recovering from call timeout",
Self::DrainCancel => "wire error while draining cancel response",
}
}
}
struct RecoveryThreadWaker {
thread: std::thread::Thread,
}
impl Wake for RecoveryThreadWaker {
fn wake(self: Arc<Self>) {
self.thread.unpark();
}
fn wake_by_ref(self: &Arc<Self>) {
self.thread.unpark();
}
}
fn block_on_recovery_deadline<F>(future: F, recovery_timeout: Duration) -> Option<F::Output>
where
F: Future,
{
let start = Instant::now();
let deadline = start.checked_add(recovery_timeout).unwrap_or(start);
let waker = std::task::Waker::from(Arc::new(RecoveryThreadWaker {
thread: std::thread::current(),
}));
let mut cx = Context::from_waker(&waker);
let mut future = pin!(future);
loop {
match future.as_mut().poll(&mut cx) {
Poll::Ready(output) => return Some(output),
Poll::Pending => {
let now = Instant::now();
if now >= deadline {
return None;
}
std::thread::park_timeout((deadline - now).min(Duration::from_millis(10)));
}
}
}
}
pub(crate) fn classify_recovery_result(
action: RecoveryWireAction,
result: Option<Result<()>>,
) -> Result<()> {
match result {
Some(Ok(())) => Ok(()),
Some(Err(Error::ConnectionClosed(message))) => Err(Error::ConnectionClosed(message)),
Some(Err(err)) => Err(Error::ConnectionClosed(format!(
"{}: {err}",
action.wire_error_prefix()
))),
None => Err(Error::ConnectionClosed(
action.timeout_message().to_string(),
)),
}
}
pub(crate) fn run_recovery_without_current_cx<R, W>(
read: &mut R,
write: &Arc<AsyncMutex<W>>,
action: RecoveryWireAction,
recovery_timeout: Duration,
limits: ProtocolLimits,
classic: bool,
) -> Result<()>
where
R: AsyncRead + Send + Unpin + 'static,
W: AsyncWrite + std::fmt::Debug + Send + Unpin + 'static,
{
let result = block_on_recovery_deadline(
async {
match action {
RecoveryWireAction::BreakAndDrain => {
break_and_drain_wire_unbounded_with_limits(read, write, limits, classic).await
}
RecoveryWireAction::DrainCancel => {
drain_cancel_wire_unbounded_with_limits(read, write, limits, classic).await
}
}
},
recovery_timeout,
);
classify_recovery_result(action, result)
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum CancelDisposition {
Timeout,
Close,
Cancel,
}
impl CancelDisposition {
pub(crate) fn from_kind(kind: CancelKind) -> Self {
match kind {
CancelKind::Timeout
| CancelKind::Deadline
| CancelKind::PollQuota
| CancelKind::CostBudget => CancelDisposition::Timeout,
CancelKind::Shutdown | CancelKind::ResourceUnavailable | CancelKind::LinkedExit => {
CancelDisposition::Close
}
CancelKind::User
| CancelKind::RaceLost
| CancelKind::FailFast
| CancelKind::ParentCancelled => CancelDisposition::Cancel,
}
}
pub(crate) fn into_error(self, timeout_ms: u32) -> Error {
match self {
CancelDisposition::Timeout => Error::CallTimeout(timeout_ms),
CancelDisposition::Close => {
Error::ConnectionClosed("operation cancelled by runtime shutdown".into())
}
CancelDisposition::Cancel => Error::Cancelled,
}
}
}
pub(crate) fn cancel_disposition(reason: Option<CancelReason>) -> CancelDisposition {
reason
.map(|reason| CancelDisposition::from_kind(reason.kind))
.unwrap_or(CancelDisposition::Cancel)
}
pub(crate) fn observe_cancellation_between_round_trips(cx: &Cx) -> Result<()> {
if cx.checkpoint().is_ok() {
return Ok(());
}
let timeout_ms = cx
.budget()
.remaining_time(time::wall_now())
.map(duration_to_millis_saturating)
.unwrap_or(0);
Err(cancel_disposition(cx.cancel_reason()).into_error(timeout_ms))
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[repr(u8)]
pub(crate) enum SessionRecoveryPhase {
Ready = 0,
InFlight = 1,
BreakSent = 2,
Draining = 3,
Dead = 4,
}
impl SessionRecoveryPhase {
fn from_u8(value: u8) -> Self {
match value {
0 => Self::Ready,
1 => Self::InFlight,
2 => Self::BreakSent,
3 => Self::Draining,
_ => Self::Dead,
}
}
}
#[derive(Debug)]
pub(crate) struct SessionRecovery {
phase: AtomicU8,
}
impl SessionRecovery {
pub(crate) fn new() -> Self {
Self {
phase: AtomicU8::new(SessionRecoveryPhase::Ready as u8),
}
}
pub(crate) fn phase(&self) -> SessionRecoveryPhase {
SessionRecoveryPhase::from_u8(self.phase.load(Ordering::SeqCst))
}
pub(crate) fn is_dead(&self) -> bool {
self.phase() == SessionRecoveryPhase::Dead
}
pub(crate) fn begin_operation(&self) -> Result<()> {
match self.phase.compare_exchange(
SessionRecoveryPhase::Ready as u8,
SessionRecoveryPhase::InFlight as u8,
Ordering::SeqCst,
Ordering::SeqCst,
) {
Ok(_) => Ok(()),
Err(current) => match SessionRecoveryPhase::from_u8(current) {
SessionRecoveryPhase::InFlight => Err(Error::ConnectionClosed(
"operation attempted while a response is still in flight".into(),
)),
SessionRecoveryPhase::BreakSent | SessionRecoveryPhase::Draining => {
Err(Error::ConnectionClosed(
"operation attempted while session recovery is pending".into(),
))
}
SessionRecoveryPhase::Dead => {
Err(Error::ConnectionClosed("connection is closed".into()))
}
SessionRecoveryPhase::Ready => Ok(()),
},
}
}
pub(crate) fn begin_or_adopt_operation(&self) -> Result<()> {
match self.phase.compare_exchange(
SessionRecoveryPhase::Ready as u8,
SessionRecoveryPhase::InFlight as u8,
Ordering::SeqCst,
Ordering::SeqCst,
) {
Ok(_) => Ok(()),
Err(current) => match SessionRecoveryPhase::from_u8(current) {
SessionRecoveryPhase::InFlight => Ok(()),
SessionRecoveryPhase::BreakSent | SessionRecoveryPhase::Draining => {
Err(Error::ConnectionClosed(
"operation attempted while session recovery is pending".into(),
))
}
SessionRecoveryPhase::Dead => {
Err(Error::ConnectionClosed("connection is closed".into()))
}
SessionRecoveryPhase::Ready => Ok(()),
},
}
}
pub(crate) fn complete_operation(&self) {
let _ = self.phase.compare_exchange(
SessionRecoveryPhase::InFlight as u8,
SessionRecoveryPhase::Ready as u8,
Ordering::SeqCst,
Ordering::SeqCst,
);
}
pub(crate) fn mark_break_required(&self) {
let _ = self.phase.compare_exchange(
SessionRecoveryPhase::InFlight as u8,
SessionRecoveryPhase::BreakSent as u8,
Ordering::SeqCst,
Ordering::SeqCst,
);
}
pub(crate) fn mark_break_sent(&self) -> Result<()> {
loop {
let current = self.phase.load(Ordering::SeqCst);
match SessionRecoveryPhase::from_u8(current) {
SessionRecoveryPhase::Dead => {
return Err(Error::ConnectionClosed("connection is closed".into()));
}
SessionRecoveryPhase::BreakSent | SessionRecoveryPhase::Draining => return Ok(()),
SessionRecoveryPhase::Ready | SessionRecoveryPhase::InFlight => {
if self
.phase
.compare_exchange(
current,
SessionRecoveryPhase::BreakSent as u8,
Ordering::SeqCst,
Ordering::SeqCst,
)
.is_ok()
{
return Ok(());
}
}
}
}
}
pub(crate) fn begin_pending_drain(&self) -> Result<bool> {
match self.phase.compare_exchange(
SessionRecoveryPhase::BreakSent as u8,
SessionRecoveryPhase::Draining as u8,
Ordering::SeqCst,
Ordering::SeqCst,
) {
Ok(_) => Ok(true),
Err(current) => match SessionRecoveryPhase::from_u8(current) {
SessionRecoveryPhase::Ready => Ok(false),
SessionRecoveryPhase::InFlight => Err(Error::ConnectionClosed(
"operation attempted while a response is still in flight".into(),
)),
SessionRecoveryPhase::Draining => Err(Error::ConnectionClosed(
"session recovery is already draining".into(),
)),
SessionRecoveryPhase::BreakSent => Ok(false),
SessionRecoveryPhase::Dead => {
Err(Error::ConnectionClosed("connection is closed".into()))
}
},
}
}
pub(crate) fn begin_drain_after_break(&self) -> Result<()> {
self.mark_break_sent()?;
match self.begin_pending_drain()? {
true => Ok(()),
false => Err(Error::ConnectionClosed(
"session recovery did not enter draining state".into(),
)),
}
}
pub(crate) fn finish_drain_ready(&self) {
self.phase
.store(SessionRecoveryPhase::Ready as u8, Ordering::SeqCst);
}
pub(crate) fn mark_dead(&self) {
self.phase
.store(SessionRecoveryPhase::Dead as u8, Ordering::SeqCst);
}
}
pub(crate) const SESSION_DEAD_ORA_CODES: &[u32] = &[
22, 28, 31, 45, 378, 600, 602, 603, 609, 1012, 1041, 1043, 1089, 1092, 2396, 3113, 3114, 3122,
3135, 12153, 12537, 12547, 12570, 12583, 27146, 28511, 56600,
];
pub(crate) const TNS_CCAP_FIELD_VERSION_18_1_EXT_1: u8 = 11;
pub(crate) const fn server_version_number_uses_extended_layout(ttc_field_version: u8) -> bool {
ttc_field_version >= TNS_CCAP_FIELD_VERSION_18_1_EXT_1
}
pub(crate) fn decode_server_version_number(full: u32, new_format: bool) -> (u8, u8, u8, u8, u8) {
if new_format {
(
((full >> 24) & 0xFF) as u8,
((full >> 16) & 0xFF) as u8,
((full >> 12) & 0x0F) as u8,
((full >> 4) & 0xFF) as u8,
(full & 0x0F) as u8,
)
} else {
(
((full >> 24) & 0xFF) as u8,
((full >> 20) & 0x0F) as u8,
((full >> 12) & 0x0F) as u8,
((full >> 8) & 0x0F) as u8,
(full & 0x0F) as u8,
)
}
}
pub(crate) const TRANSIENT_ORA_CODES: &[u32] =
&[54, 60, 104, 257, 12516, 12520, 12526, 12528, 30006, 51535];
pub(crate) const CONNECTION_LOST_ORA_CODES: &[u32] = &[
28, 1012, 1041, 1089, 2396, 3113, 3114, 3135, 12537, 12547, 12570, 28511,
];
pub(crate) fn parse_ora_code_from_message(message: &str) -> Option<u32> {
let start = message.find("ORA-")?;
let digits: String = message[start + 4..]
.chars()
.take_while(|ch| ch.is_ascii_digit())
.collect();
digits.parse::<u32>().ok()
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum PostSyncProtocolDisposition {
Ready,
Dead,
}
pub(crate) fn post_sync_protocol_error_disposition(
err: &oracledb_protocol::ProtocolError,
) -> PostSyncProtocolDisposition {
match err {
oracledb_protocol::ProtocolError::ResourceLimit { .. } => PostSyncProtocolDisposition::Dead,
_ if protocol_error_ora_code(err)
.is_some_and(|code| SESSION_DEAD_ORA_CODES.contains(&code)) =>
{
PostSyncProtocolDisposition::Dead
}
_ => PostSyncProtocolDisposition::Ready,
}
}
pub(crate) fn protocol_error_is_session_dead(err: &oracledb_protocol::ProtocolError) -> bool {
post_sync_protocol_error_disposition(err) == PostSyncProtocolDisposition::Dead
}
pub(crate) fn protocol_error_kind(err: &oracledb_protocol::ProtocolError) -> ErrorKind {
match err {
oracledb_protocol::ProtocolError::ResourceLimit { .. } => ErrorKind::ResourceLimit,
oracledb_protocol::ProtocolError::ServerError(_)
| oracledb_protocol::ProtocolError::ServerErrorWithRowCount { .. }
| oracledb_protocol::ProtocolError::ServerErrorInfo(_) => ErrorKind::Database,
_ => ErrorKind::Protocol,
}
}
pub(crate) fn protocol_error_ora_code(err: &oracledb_protocol::ProtocolError) -> Option<u32> {
match err {
oracledb_protocol::ProtocolError::ServerError(message) => {
parse_ora_code_from_message(message)
}
oracledb_protocol::ProtocolError::ServerErrorWithRowCount { message, .. } => {
parse_ora_code_from_message(message)
}
oracledb_protocol::ProtocolError::ServerErrorInfo(details) => Some(details.code),
_ => None,
}
}
pub(crate) fn protocol_error_offset(err: &oracledb_protocol::ProtocolError) -> Option<i32> {
match err {
oracledb_protocol::ProtocolError::ServerErrorInfo(details) if details.pos != 0 => {
Some(details.pos)
}
_ => None,
}
}
#[cfg(test)]
mod boundary_tests {
use super::*;
#[test]
fn server_version_number_layout_flips_at_18_1_ext_1() {
let full = 0x1234_5678_u32;
let decode_at =
|fv: u8| decode_server_version_number(full, fv >= TNS_CCAP_FIELD_VERSION_18_1_EXT_1);
let below = decode_at(TNS_CCAP_FIELD_VERSION_18_1_EXT_1 - 1);
assert_eq!(below, (18, 3, 5, 6, 8), "pre-18 nibble layout");
let at = decode_at(TNS_CCAP_FIELD_VERSION_18_1_EXT_1);
assert_eq!(at, (18, 52, 5, 103, 8), "18+ wide-field layout");
assert_ne!(
below, at,
"the 18_1_EXT_1 gate must flip the AUTH_VERSION_NO layout"
);
assert_eq!(decode_server_version_number(full, false), below);
assert_eq!(decode_server_version_number(full, true), at);
}
}