Summary
A bare INSERT ... VALUES ('literal') RETURNING "ID" INTO :returning_id
issued through connection.insert reliably hangs (~indefinitely) on
Oracle Database 23.26.1.0.0 + ruby-oci8 when the session has
cursor_sharing = FORCE (the adapter default).
The hang is a SQL*Net half-duplex deadlock: the server is stuck in
SQL*Net more data from client while ruby-oci8's main thread is stuck
in OCIStmtExecute -> nttfprd -> read(). Both sides are waiting for
the other to send the next packet.
Reproduction
Surfaced by the spec
spec/active_record/connection_adapters/oracle_enhanced/primary_key_trigger_spec.rb:320
("does not raise NoMethodError for :returning_id Symbol when logging").
That example was skipped in PR #2615 to unblock the branch; reverting
the skip reproduces the hang locally.
Minimal manual reproduction (after bundle exec rspec setup):
ActiveRecord::Base.establish_connection(CONNECTION_PARAMS)
conn = ActiveRecord::Base.lease_connection
conn.execute "CREATE TABLE t (id NUMBER PRIMARY KEY, name VARCHAR2(50))"
conn.insert("INSERT INTO t (name) VALUES ('alpha')", nil, "id")
# hangs forever
Pass cursor_sharing: :exact to establish_connection and the same
INSERT completes normally.
Diagnosis
Server-side (v$session)
SID USERNAME STATUS EVENT BLOCKING_SESSION SECONDS_IN_WAIT SQL_ID
--- ---------------- ------ --------------------------------- ---------------- --------------- -------------
38 ORACLE_ENHANCED ACTIVE SQL*Net more data from client (none) 564 9ymnvkfmz0ay5
SQL_TEXT for 9ymnvkfmz0ay5:
INSERT INTO test_pk_triggers (name) VALUES (:"SYS_B_0") RETURNING "ID" INTO :returning_id
The literal 'alpha' was rewritten to :"SYS_B_0" by Oracle because
oracle-enhanced runs ALTER SESSION SET CURSOR_SHARING = FORCE on
connect (lib/active_record/connection_adapters/oracle_enhanced_adapter.rb:843).
System-wide cursor_sharing is EXACT.
blocking_session is empty, no rows in v$lock with BLOCK = 1 for
this session. There is no row-lock contention, no DDL contention, no
sequence wait.
Client-side (macOS sample of the rspec PID)
Main thread, deepest frames:
oci8_stmt_execute (oci8lib_400.bundle)
oci8_OCIStmtExecute_nb (oci8lib_400.bundle)
oci8_call_without_gvl (oci8lib_400.bundle)
oci8_OCIStmtExecute_cb (oci8lib_400.bundle)
OCIStmtExecute (libclntsh.dylib.23.1)
kpuexec / kpurcsc / upirtrc / nioqwa / ttcdrv / nioqrc / nsbasic_brc
nttfprd (libclntsh.dylib.23.1)
read (libsystem_kernel.dylib)
nttfprd is SQL*Net's transport receive. Ruby is blocked reading from
the TCP socket while Oracle is asking for the client to send more data.
Classic mutual wait.
Versions
- Oracle Database 26ai Free Release 23.26.1.0.0
compatible = 23.6.0, optimizer_features_enable = 23.1.0
- Ruby 4.0.3 (mise install)
- ruby-oci8 from current Gemfile.lock
- macOS / arm64
Likely cause
cursor_sharing = FORCE adds an implicit IN bind (:"SYS_B_0") to a
statement that already declares an explicit OUT bind (:returning_id).
The RETURNING ... INTO OUT bind uses OCI's piecewise / DATA_AT_EXEC
protocol; with the additional implicit bind, the protocol exchange
between ruby-oci8 and the Oracle 23ai server gets out of sync, neither
side flushes, and both block on read.
This is not the Symbol-vs-String bind-name bug the spec was originally
guarding against; the Ruby-side logging path is never reached because
the OCIStmtExecute call itself never returns.
Possible directions for a fix
Listed here for discussion, not as a recommendation:
- Per-statement cursor_sharing toggle. Switch the session to
cursor_sharing = exact around code paths that issue
RETURNING ... INTO, restore on exit. Heavyweight but localized.
- Convert OUT bind name from Symbol to String. Test whether the
ruby-oci8 OUT bind path takes a Symbol-specific shortcut that
trips the protocol; not yet verified.
- Avoid implicit literal capture. Pre-bind the
'alpha'-style
literal in the adapter so cursor_sharing has nothing to rewrite.
- ruby-oci8 / Instant Client upgrade. Confirm whether a newer
ruby-oci8 (or a different OCI client minor version) avoids the
deadlock against 23.26 server.
Workaround for callers
establish_connection(... cursor_sharing: :exact) avoids the hang.
CI impact
The spec is currently skip-ped in PR #2615 with a reference back to
this issue.
Summary
A bare
INSERT ... VALUES ('literal') RETURNING "ID" INTO :returning_idissued through
connection.insertreliably hangs (~indefinitely) onOracle Database 23.26.1.0.0 + ruby-oci8 when the session has
cursor_sharing = FORCE(the adapter default).The hang is a SQL*Net half-duplex deadlock: the server is stuck in
SQL*Net more data from clientwhile ruby-oci8's main thread is stuckin
OCIStmtExecute -> nttfprd -> read(). Both sides are waiting forthe other to send the next packet.
Reproduction
Surfaced by the spec
spec/active_record/connection_adapters/oracle_enhanced/primary_key_trigger_spec.rb:320("does not raise NoMethodError for :returning_id Symbol when logging").
That example was skipped in PR #2615 to unblock the branch; reverting
the
skipreproduces the hang locally.Minimal manual reproduction (after
bundle exec rspecsetup):Pass
cursor_sharing: :exacttoestablish_connectionand the sameINSERT completes normally.
Diagnosis
Server-side (
v$session)SQL_TEXTfor9ymnvkfmz0ay5:The literal
'alpha'was rewritten to:"SYS_B_0"by Oracle becauseoracle-enhanced runs
ALTER SESSION SET CURSOR_SHARING = FORCEonconnect (
lib/active_record/connection_adapters/oracle_enhanced_adapter.rb:843).System-wide
cursor_sharingisEXACT.blocking_sessionis empty, no rows inv$lockwithBLOCK = 1forthis session. There is no row-lock contention, no DDL contention, no
sequence wait.
Client-side (macOS
sampleof the rspec PID)Main thread, deepest frames:
nttfprdis SQL*Net's transport receive. Ruby is blocked reading fromthe TCP socket while Oracle is asking for the client to send more data.
Classic mutual wait.
Versions
compatible = 23.6.0,optimizer_features_enable = 23.1.0Likely cause
cursor_sharing = FORCEadds an implicit IN bind (:"SYS_B_0") to astatement that already declares an explicit OUT bind (
:returning_id).The
RETURNING ... INTOOUT bind uses OCI's piecewise / DATA_AT_EXECprotocol; with the additional implicit bind, the protocol exchange
between ruby-oci8 and the Oracle 23ai server gets out of sync, neither
side flushes, and both block on
read.This is not the Symbol-vs-String bind-name bug the spec was originally
guarding against; the Ruby-side logging path is never reached because
the
OCIStmtExecutecall itself never returns.Possible directions for a fix
Listed here for discussion, not as a recommendation:
cursor_sharing = exactaround code paths that issueRETURNING ... INTO, restore on exit. Heavyweight but localized.ruby-oci8 OUT bind path takes a Symbol-specific shortcut that
trips the protocol; not yet verified.
'alpha'-styleliteral in the adapter so cursor_sharing has nothing to rewrite.
ruby-oci8 (or a different OCI client minor version) avoids the
deadlock against 23.26 server.
Workaround for callers
establish_connection(... cursor_sharing: :exact)avoids the hang.CI impact
The spec is currently
skip-ped in PR #2615 with a reference back tothis issue.