Skip to content

Commit b855fa5

Browse files
letspurify-uxclaude
andcommitted
Oracle thin 프로토콜 정합성 및 드라이버 비교 하네스 개선
- 접속 핸드셰이크 후 소켓 타임아웃 해제, call timeout 0을 블로킹으로 매핑 (python-oracledb settimeout(None) 동작 일치) - 미지원 TNS control 패킷 타입을 오류 없이 무시 (_process_control_packet 일치) - final.sql 비교: INTERVAL/TIMESTAMP-TZ select 항목을 서버측 TO_CHAR로 정규화 하여 OCI/thin 표시 차이를 제거 (JSON 정규화와 동일 패턴, 인메모리 재작성) - oqt_pkg.p_implicit 스킵을 식별자/인자 기반 매칭으로 강화 (원본/자동포맷 텍스트 모두) 314/315/318/319 프로토콜 라이브 테스트 및 전체 fixture 비교 통과 검증 완료 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AEbS4bMvL3u6qjBUMucaJr
1 parent 09c9a45 commit b855fa5

4 files changed

Lines changed: 434 additions & 20 deletions

File tree

crates/tns-thin/src/session.rs

Lines changed: 110 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,7 @@ use sha2::{Digest, Sha512};
3838
use 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
};
4443
use 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");

crates/tns-thin/tests/live_tns.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -528,6 +528,30 @@ fn python_oracledb_1131_call_timeout_aborts_slow_call() {
528528
);
529529
}
530530

531+
// python-oracledb maps a zero call timeout to `settimeout(None)`, so a
532+
// connection with no call timeout blocks until the server answers. The bounded
533+
// socket timeout used for the connect handshake must not leak into calls and
534+
// cap them.
535+
#[test]
536+
#[ignore = "requires local Oracle listener via ORACLE_THIN_TEST_* environment variables"]
537+
fn no_call_timeout_lets_a_long_call_finish() {
538+
let mut conn = connect();
539+
assert_eq!(conn.call_timeout().expect("read call timeout"), None);
540+
541+
let sleep = Duration::from_secs(33);
542+
let started = Instant::now();
543+
conn.query_drop(&format!(
544+
"BEGIN DBMS_SESSION.SLEEP({}); END;",
545+
sleep.as_secs()
546+
))
547+
.expect("long call should finish when no call timeout is set");
548+
549+
assert!(
550+
started.elapsed() >= sleep - Duration::from_secs(1),
551+
"call returned before the server finished sleeping"
552+
);
553+
}
554+
531555
#[test]
532556
#[ignore = "requires local Oracle listener via ORACLE_THIN_TEST_* environment variables"]
533557
fn python_oracledb_1135_1138_to_1140_1142_server_metadata_matches_queries() {

0 commit comments

Comments
 (0)