@@ -38,8 +38,7 @@ use sha2::{Digest, Sha512};
3838use crate::connect::{
3939 connect_data_descriptor_parts, connect_description_option_parts,
4040 validate_connect_descriptor_value, AcceptInfo, ConnectOptions, ConnectTarget,
41- OracleNetConnector, OracleNetServerType, TNS_DEFAULT_SOCKET_TIMEOUT,
42- TNS_MIN_SUPPORTED_PROTOCOL,
41+ OracleNetConnector, OracleNetServerType, TNS_MIN_SUPPORTED_PROTOCOL,
4342};
4443use crate::exec::{
4544 parse_sql_bind_names, sql_dml_returning_has_duplicate_bind, sql_is_dml_returning,
@@ -786,6 +785,10 @@ impl OracleThinSession {
786785 negotiate_protocol(&mut stream, &mut capabilities)?;
787786 negotiate_data_types(&mut stream, &capabilities)?;
788787 let auth = authenticate(&mut stream, &config, &capabilities)?;
788+ // The connect handshake runs under a bounded socket timeout; an
789+ // established session has no call timeout until one is set, so clear it
790+ // the way python-oracledb leaves the socket blocking after login.
791+ clear_socket_timeouts(&stream)?;
789792 Ok(Self {
790793 stream,
791794 config,
@@ -1269,11 +1272,10 @@ impl OracleThinSession {
12691272 }
12701273
12711274 pub fn set_call_timeout(&mut self, timeout: Option<Duration>) -> Result<(), OracleThinError> {
1272- let socket_timeout = Some(
1273- timeout
1274- .filter(|timeout| !timeout.is_zero())
1275- .unwrap_or(TNS_DEFAULT_SOCKET_TIMEOUT),
1276- );
1275+ // python-oracledb `set_call_timeout(0)` maps to `settimeout(None)`, so
1276+ // no call timeout means the socket blocks until the server answers.
1277+ // Falling back to a default socket timeout here would cap every call.
1278+ let socket_timeout = timeout.filter(|timeout| !timeout.is_zero());
12771279 self.stream
12781280 .set_read_timeout(socket_timeout)
12791281 .map_err(|err| {
@@ -3603,6 +3605,18 @@ impl Drop for OracleThinSession {
36033605 }
36043606}
36053607
3608+ /// Drops the bounded socket timeout used for the connect handshake. Matches
3609+ /// python-oracledb `Transport.set_from_socket`, which puts the socket back into
3610+ /// blocking mode once the connection is established.
3611+ fn clear_socket_timeouts(stream: &TcpStream) -> Result<(), OracleThinError> {
3612+ stream.set_read_timeout(None).map_err(|err| {
3613+ OracleThinError::new(format!("failed to clear Oracle read timeout: {err}"))
3614+ })?;
3615+ stream
3616+ .set_write_timeout(None)
3617+ .map_err(|err| OracleThinError::new(format!("failed to clear Oracle write timeout: {err}")))
3618+ }
3619+
36063620/// Cheap liveness probe for a session expected to be idle at a request
36073621/// boundary: peeks the socket without blocking. A clean idle session has no
36083622/// pending bytes, so EOF or a socket error means the server dropped the
@@ -13149,10 +13163,10 @@ fn process_control_packet(data: &[u8]) -> Result<ControlPacketStatus, OracleThin
1314913163 ))),
1315013164 }
1315113165 }
13152- other => Err(OracleThinError::new(format!(
13153- "received unsupported TNS control packet type {other}: {}",
13154- hex_encode_upper(data)
13155- ) )),
13166+ // python-oracledb `_process_control_packet` only reacts to the OOB
13167+ // reset and in-band notification types and discards anything else, so
13168+ // an unknown control type must not break the connection.
13169+ _ => Ok(ControlPacketStatus::default( )),
1315613170 }
1315713171}
1315813172
@@ -14404,6 +14418,7 @@ mod tests {
1440414418 };
1440514419 use std::time::{Duration, Instant};
1440614420
14421+ use super::clear_socket_timeouts;
1440714422 use super::AuthCredentialInputs;
1440814423 use super::CANCEL_RESET_DRAIN_TIMEOUT;
1440914424 use super::{
@@ -14513,6 +14528,7 @@ mod tests {
1451314528 encode_oracle_bind_text, ORACLE_CHARSET_JA16SJIS, ORACLE_CHARSET_KO16KSC5601,
1451414529 ORACLE_CHARSET_KO16MSWIN949, ORACLE_CHARSET_ZHS16GBK, ORACLE_CHARSET_ZHT16BIG5,
1451514530 };
14531+ use crate::connect::TNS_DEFAULT_SOCKET_TIMEOUT;
1451614532 use crate::connect::{AcceptInfo, ConnectOptions, ConnectTarget, OracleNetServerType};
1451714533 use crate::exec::{
1451814534 BindInputValue, BindValue, ColumnMetadata, OracleColumnType, OracleIntervalDaySecond,
@@ -15033,6 +15049,54 @@ mod tests {
1503315049 assert_eq!(release_body, expected);
1503415050 }
1503515051
15052+ #[test]
15053+ fn set_call_timeout_none_leaves_the_socket_blocking() {
15054+ let listener = TcpListener::bind("127.0.0.1:0").unwrap();
15055+ let client = TcpStream::connect(listener.local_addr().unwrap()).unwrap();
15056+ let (_server, _) = listener.accept().unwrap();
15057+ let mut session = test_session_with_stream(client);
15058+
15059+ session
15060+ .set_call_timeout(Some(Duration::from_secs(7)))
15061+ .unwrap();
15062+ assert_eq!(
15063+ session.stream.read_timeout().unwrap(),
15064+ Some(Duration::from_secs(7))
15065+ );
15066+ assert_eq!(
15067+ session.stream.write_timeout().unwrap(),
15068+ Some(Duration::from_secs(7))
15069+ );
15070+
15071+ // python-oracledb maps a zero/absent call timeout to `settimeout(None)`;
15072+ // a default socket timeout here would cap every call instead.
15073+ session.set_call_timeout(None).unwrap();
15074+ assert_eq!(session.stream.read_timeout().unwrap(), None);
15075+ assert_eq!(session.stream.write_timeout().unwrap(), None);
15076+
15077+ session.set_call_timeout(Some(Duration::ZERO)).unwrap();
15078+ assert_eq!(session.stream.read_timeout().unwrap(), None);
15079+ assert_eq!(session.stream.write_timeout().unwrap(), None);
15080+ }
15081+
15082+ #[test]
15083+ fn clear_socket_timeouts_drops_the_connect_handshake_timeout() {
15084+ let listener = TcpListener::bind("127.0.0.1:0").unwrap();
15085+ let client = TcpStream::connect(listener.local_addr().unwrap()).unwrap();
15086+ let (_server, _) = listener.accept().unwrap();
15087+ client
15088+ .set_read_timeout(Some(TNS_DEFAULT_SOCKET_TIMEOUT))
15089+ .unwrap();
15090+ client
15091+ .set_write_timeout(Some(TNS_DEFAULT_SOCKET_TIMEOUT))
15092+ .unwrap();
15093+
15094+ clear_socket_timeouts(&client).unwrap();
15095+
15096+ assert_eq!(client.read_timeout().unwrap(), None);
15097+ assert_eq!(client.write_timeout().unwrap(), None);
15098+ }
15099+
1503615100 #[test]
1503715101 fn is_healthy_detects_server_disconnect_on_idle_socket() {
1503815102 let listener = TcpListener::bind("127.0.0.1:0").unwrap();
@@ -16738,6 +16802,41 @@ mod tests {
1673816802 server.join().unwrap();
1673916803 }
1674016804
16805+ #[test]
16806+ fn read_data_packet_discards_unknown_control_packet_types() {
16807+ let listener = TcpListener::bind("127.0.0.1:0").unwrap();
16808+ let addr = listener.local_addr().unwrap();
16809+ let server = std::thread::spawn(move || {
16810+ let (mut stream, _) = listener.accept().unwrap();
16811+ stream
16812+ .write_all(&tns_test_packet(
16813+ 319,
16814+ TNS_PACKET_TYPE_CONTROL,
16815+ &[0x00, 0x63, 0xde, 0xad],
16816+ ))
16817+ .unwrap();
16818+
16819+ let mut data_body = Vec::new();
16820+ data_body.extend_from_slice(&TNS_DATA_FLAGS_EOF.to_be_bytes());
16821+ data_body.push(TNS_MSG_TYPE_PROTOCOL);
16822+ stream
16823+ .write_all(&tns_test_packet(319, TNS_PACKET_TYPE_DATA, &data_body))
16824+ .unwrap();
16825+ });
16826+ let stream = TcpStream::connect(addr).unwrap();
16827+ let mut session = test_session_with_stream(stream);
16828+ session
16829+ .set_call_timeout(Some(Duration::from_secs(1)))
16830+ .expect("set thin call timeout");
16831+
16832+ let (oob_reset_received, payload) =
16833+ read_data_packet_with_control(&mut session.stream, 319).unwrap();
16834+
16835+ assert!(!oob_reset_received);
16836+ assert_eq!(payload, vec![TNS_MSG_TYPE_PROTOCOL]);
16837+ server.join().unwrap();
16838+ }
16839+
1674116840 #[test]
1674216841 fn decode_oracle_number_trims_fractional_trailing_zeros() {
1674316842 assert_eq!(decode_oracle_number(&[0xc0, 0x33]).unwrap(), "0.5");
0 commit comments