You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sedona.apache.org by ja...@apache.org on 2026/08/10 18:11:04 UTC

(sedona-db) branch main updated: fix(sedona-gdal): skip GDALClose during interpreter shutdown (#1135)

This is an automated email from the ASF dual-hosted git repository.

james-willis pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/sedona-db.git


The following commit(s) were added to refs/heads/main by this push:
     new 4f844cf14 fix(sedona-gdal): skip GDALClose during interpreter shutdown (#1135)
4f844cf14 is described below

commit 4f844cf14fe4cb38a7a849937303b980f8b6502c
Author: James Willis <ja...@users.noreply.github.com>
AuthorDate: Mon Aug 10 11:10:57 2026 -0700

    fix(sedona-gdal): skip GDALClose during interpreter shutdown (#1135)
---
 c/sedona-gdal/src/dataset.rs                |  6 +++++-
 c/sedona-gdal/src/global.rs                 | 24 ++++++++++++++++++++++++
 python/sedonadb/python/sedonadb/__init__.py |  7 +++++++
 python/sedonadb/src/lib.rs                  | 10 ++++++++++
 4 files changed, 46 insertions(+), 1 deletion(-)

diff --git a/c/sedona-gdal/src/dataset.rs b/c/sedona-gdal/src/dataset.rs
index 4577aa851..d6ce7990f 100644
--- a/c/sedona-gdal/src/dataset.rs
+++ b/c/sedona-gdal/src/dataset.rs
@@ -46,7 +46,11 @@ unsafe impl Send for Dataset {}
 
 impl Drop for Dataset {
     fn drop(&mut self) {
-        if !self.c_dataset.is_null() {
+        // Once the process is shutting down, leave the handle open. On Windows,
+        // closing a dataset while GDAL's shared library is being unloaded
+        // corrupts teardown and aborts the process (0xC0000409); leaking the
+        // handle at process exit is harmless since the OS reclaims it.
+        if !self.c_dataset.is_null() && !crate::global::is_gdal_shutting_down() {
             unsafe { call_gdal_api!(self.api, GDALClose, self.c_dataset) };
         }
     }
diff --git a/c/sedona-gdal/src/global.rs b/c/sedona-gdal/src/global.rs
index 5b9581163..3de26d7a2 100644
--- a/c/sedona-gdal/src/global.rs
+++ b/c/sedona-gdal/src/global.rs
@@ -19,8 +19,32 @@ use crate::errors::GdalInitLibraryError;
 use crate::gdal::Gdal;
 use crate::gdal_api::GdalApi;
 use std::path::PathBuf;
+use std::sync::atomic::{AtomicBool, Ordering};
 use std::sync::{Mutex, OnceLock};
 
+/// Set once the process is shutting down so GDAL datasets are left open during
+/// teardown instead of being closed.
+///
+/// On Windows, running `GDALClose` from a (thread-local) dataset cache's
+/// destructor while the GDAL shared library is being unloaded corrupts the
+/// teardown and aborts the process (`0xC0000409`). Leaking the handles at
+/// process exit is harmless — the OS reclaims them — so once shutdown begins
+/// [`crate::dataset::Dataset`]'s `Drop` skips `GDALClose`.
+static GDAL_SHUTTING_DOWN: AtomicBool = AtomicBool::new(false);
+
+/// Signal that the process is shutting down; see [`GDAL_SHUTTING_DOWN`].
+///
+/// Intended to run on the interpreter thread during finalization (e.g. a Python
+/// `atexit` callback), before GDAL's library is unloaded.
+pub fn begin_gdal_shutdown() {
+    GDAL_SHUTTING_DOWN.store(true, Ordering::SeqCst);
+}
+
+/// Whether [`begin_gdal_shutdown`] has been called.
+pub fn is_gdal_shutting_down() -> bool {
+    GDAL_SHUTTING_DOWN.load(Ordering::SeqCst)
+}
+
 /// Minimum GDAL version required by sedona-gdal.
 const MIN_GDAL_VERSION_MAJOR: i32 = 3;
 const MIN_GDAL_VERSION_MINOR: i32 = 5;
diff --git a/python/sedonadb/python/sedonadb/__init__.py b/python/sedonadb/python/sedonadb/__init__.py
index 94cdf73d4..a66e244cb 100644
--- a/python/sedonadb/python/sedonadb/__init__.py
+++ b/python/sedonadb/python/sedonadb/__init__.py
@@ -14,6 +14,8 @@
 # KIND, either express or implied.  See the License for the
 # specific language governing permissions and limitations
 # under the License.
+import atexit
+
 from sedonadb import _lib
 from sedonadb.context import connect, configure_proj, configure_gdal
 
@@ -30,3 +32,8 @@ __all__ = ["connect", "options"]
 # GDAL-backed operation (e.g., raster I/O).
 configure_proj("auto")
 configure_gdal("auto")
+
+# During interpreter shutdown, leave GDAL datasets open instead of closing them:
+# on Windows, closing a dataset while GDAL's library is being unloaded aborts the
+# process. Registered here so it runs before the extension is torn down.
+atexit.register(_lib.begin_gdal_shutdown)
diff --git a/python/sedonadb/src/lib.rs b/python/sedonadb/src/lib.rs
index 6663e3153..43bb50790 100644
--- a/python/sedonadb/src/lib.rs
+++ b/python/sedonadb/src/lib.rs
@@ -140,11 +140,21 @@ fn gdal_version() -> Result<Option<String>, PySedonaError> {
     }
 }
 
+/// Signal that GDAL is shutting down so datasets are left open during
+/// interpreter/library teardown. Registered as a Python `atexit` callback to
+/// avoid a Windows process-exit abort (`0xC0000409`) when a GDAL dataset is
+/// closed while the library is being unloaded.
+#[pyfunction]
+fn begin_gdal_shutdown() {
+    sedona_gdal::global::begin_gdal_shutdown();
+}
+
 #[pymodule(gil_used = false)]
 fn _lib(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
     #[cfg(feature = "mimalloc")]
     configure_tg_allocator();
 
+    m.add_function(wrap_pyfunction!(begin_gdal_shutdown, m)?)?;
     m.add_function(wrap_pyfunction!(configure_gdal_shared, m)?)?;
     m.add_function(wrap_pyfunction!(configure_proj_shared, m)?)?;
     m.add_function(wrap_pyfunction!(expr::expr_binary, m)?)?;