fix(ingest/oracle): make thick_mode_lib_dir work on Linux via ctypes preload#17265
Merged
fix(ingest/oracle): make thick_mode_lib_dir work on Linux via ctypes preload#17265
Conversation
…preload Oracle ships libclntsh.so on Linux without RUNPATH=$ORIGIN, so even when python-oracledb dlopens it via an absolute path (the lib_dir branch), the loader still resolves its DT_NEEDED deps (libnnz*, libclntshcore, libons, ...) through LD_LIBRARY_PATH / ld.so.cache. Hosts without those configured fall over with DPI-1047. Setting LD_LIBRARY_PATH from Python doesn't help because glibc reads it once at process startup. Preload every .so in thick_mode_lib_dir with ctypes.CDLL(path, RTLD_GLOBAL) before init_oracle_client(). Once the deps are mapped, the linker resolves them by SONAME on the subsequent dlopen() inside ODPI-C. Effect is per process; nothing escapes the worker. See oracle/python-oracledb#578. Made-with: Cursor
Contributor
|
Linear: ING-2520 |
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
david-leifker
approved these changes
Apr 30, 2026
sgomezvillamor
approved these changes
May 4, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
📋 Summary
Make
thick_mode_lib_diractually work on Linux by preloading the Oracle Instant Client shared libraries viactypesbefore callingoracledb.init_oracle_client(), so customers no longer needLD_LIBRARY_PATH/ldconfigconfigured on the executor host.🎯 Motivation
A customer running Oracle ingestion in thick mode on a remote executor hits
DPI-1047: Cannot locate a 64-bit Oracle Client libraryeven after settingthick_mode_lib_dir. Today the Linux branch ignores that config entirely and relies onldconfig/LD_LIBRARY_PATH, which is brittle on locked-down or containerised executor images.Root cause is upstream: Oracle ships
libclntsh.soon Linux withoutRUNPATH=$ORIGIN, so even when ODPI-Cdlopenslibclntsh.soby absolute path (which is what passinglib_dirdoes), the dynamic linker still has to resolve itsDT_NEEDEDdeps (libnnz*,libclntshcore,libons, …) throughLD_LIBRARY_PATH/ld.so.cache. SettingLD_LIBRARY_PATHfrom Python doesn't help — glibc reads it once at process startup. The python-oracledb maintainer confirms this in oracle/python-oracledb#578 and says it can only be fixed client-side or by patching the rpath oflibclntsh.soitself.So just forwarding
lib_dir=toinit_oracle_client()on Linux would change the error fromcannot find libclntsh.sotocannot find libnnz.so— net zero. We have to load the dependent libs into the process address space ourselves.🔧 Changes Overview
Modifications
OracleSource.__init__now branches on platform:thick_mode_lib_dirset → preload all.sos in that directory by absolute path withctypes.CDLL(..., RTLD_GLOBAL), then calloracledb.init_oracle_client()with no args.thick_mode_lib_dirunset → unchanged (rely onldconfig/LD_LIBRARY_PATH).init_oracle_client(lib_dir=...)).thick_mode_lib_dirfield description updated to document that it now works on Linux.New helper
_preload_oracle_client_libs(lib_dir)inoracle.py. Loadslibnnz*,libclntshcore,libons, optional satellites, andlibclntshlast — order matters becauselibclntshhasDT_NEEDEDentries on the others. Surfaces a clearConfigurationErrorwhen the directory is missing or contains no Oracle libs (much more useful than the cryptic DPI-1047 we'd otherwise hit downstream). Per-file load failures are logged at DEBUG and tolerated, since e.g.libipc1isn't shipped in every Instant Client release.🏗️ Architecture/Design Notes
Why
ctypes.CDLL(path, RTLD_GLOBAL)instead of e.g. setting an env var?Once an
.sois mapped viadlopenwithRTLD_GLOBAL, the dynamic linker registers the resolved object by SONAME in the process's symbol namespace. Whenlibclntsh.solaterdlopens itsDT_NEEDEDdeps from inside ODPI-C, the linker sees them already resident and reuses them — completely bypassing the missingRUNPATH. This is the same trick LD preloading uses; we're just doing it explicitly from Python.Scope of the side effect
The preload is per-process, not global:
/etc/ld.so.cache, env vars, sibling processes, or processes spawned later viasubprocess/exec.Trade-offs considered
lib_dirtoinit_oracle_client()on Linux: rejected, see motivation.patchelf --set-rpath '$ORIGIN'onlibclntsh.soindocker/snippets/oracle_instantclient.sh: complementary, only fixes images we build. Worth doing as a follow-up for defense in depth, but doesn't help customers running their own executor images.🧪 Testing
Added 7 unit tests in
tests/unit/test_oracle_source.py:Helper-level (
_preload_oracle_client_libs):libclntsh(preserving the order that makes the workaround actually work).libipc1,libociei).ConfigurationErrorfor missing directory and for empty directory.OSErrorinstead of aborting the whole preload.Call-site (
OracleSource.__init__):thick_mode_lib_dirset → preload runs,init_oracle_client()is called withoutlib_dir.thick_mode_lib_dirunset → preload skipped, unchanged behavior.thick_mode_lib_dirset → no preload,init_oracle_client(lib_dir=...)as before.All 51 tests in
test_oracle_source.pypass;ruffandmypyclean.📊 Impact Assessment
enable_thick_mode == True, so default (thin-mode) ingestion is completely untouched.thick_mode_lib_diris unset is identical to before. When it is set, the previous behavior was to silently ignore it and fall through toldconfigresolution; we now actually honor it. Anyone who was settingthick_mode_lib_diron Linux today was either a) already working becauseldconfigwas set up correctly (still works), or b) hitting DPI-1047 (now fixed).dlopen()calls at source initialization. Negligible.🚀 Deployment Notes
ctypesandglobare stdlib.thick_mode_lib_dirin their recipe and have it work without touching the host'sldconfig/LD_LIBRARY_PATH.docker/snippets/oracle_instantclient.sh) is unaffected — it already configuresldconfig, so the unset-thick_mode_lib_dirpath keeps working.🔗 References
docker/snippets/oracle_instantclient.sh(potential follow-up:patchelf --set-rpath '\$ORIGIN' libclntsh.sofor defense in depth)❓ Questions for Reviewers
patchelfchange to our base image as a follow-up PR? It would be self-fixing for our images but only ours.thick_mode_lib_dirnow applies to all three platforms — happy to split it back into platform-specific guidance if that reads better.Made with Cursor