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

apitrou pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/arrow.git


The following commit(s) were added to refs/heads/main by this push:
     new 0f7b5e573e GH-44071: [C++] Leak S3 structures if finalization happens 
too late (#44090)
0f7b5e573e is described below

commit 0f7b5e573ec857cf54708a6805cd942a77eff7fa
Author: Antoine Pitrou <[email protected]>
AuthorDate: Mon Sep 23 10:30:28 2024 +0200

    GH-44071: [C++] Leak S3 structures if finalization happens too late (#44090)
    
    ### Rationale for this change
    
    Leaking S3 structures at shutdown can be better than inducing a segfault 
because those structures' destructors run too late at process exit.
    
    This seems to avoid the crash when run under `uwsgi` in 
https://github.com/apache/arrow/issues/44071
    
    ### Are these changes tested?
    
    Yes.
    
    ### Are there any user-facing changes?
    
    Hopefully not.
    
    * GitHub Issue: #44071
    
    Authored-by: Antoine Pitrou <[email protected]>
    Signed-off-by: Antoine Pitrou <[email protected]>
---
 cpp/src/arrow/filesystem/s3fs.cc      | 15 +++++++++++++-
 python/pyarrow/tests/test_fs.py       | 39 +++++++++++++++++++++++++++++++++++
 python/pyarrow/tests/wsgi_examples.py | 35 +++++++++++++++++++++++++++++++
 python/requirements-test.txt          |  1 +
 python/requirements-wheel-test.txt    |  1 +
 5 files changed, 90 insertions(+), 1 deletion(-)

diff --git a/cpp/src/arrow/filesystem/s3fs.cc b/cpp/src/arrow/filesystem/s3fs.cc
index 96c771aeb6..77b111f61b 100644
--- a/cpp/src/arrow/filesystem/s3fs.cc
+++ b/cpp/src/arrow/filesystem/s3fs.cc
@@ -3389,6 +3389,12 @@ struct AwsInstance {
         ARROW_LOG(WARNING)
             << " arrow::fs::FinalizeS3 was not called even though S3 was 
initialized.  "
                "This could lead to a segmentation fault at exit";
+        // Leak the S3ClientFinalizer to avoid crashes when destroying 
remaining
+        // S3Client instances (GH-44071).
+        auto* leaked_shared_ptr =
+            new std::shared_ptr<S3ClientFinalizer>(GetClientFinalizer());
+        ARROW_UNUSED(leaked_shared_ptr);
+        return;
       }
       GetClientFinalizer()->Finalize();
 #ifdef ARROW_S3_HAS_S3CLIENT_CONFIGURATION
@@ -3480,7 +3486,14 @@ Status EnsureS3Initialized() {
 }
 
 Status FinalizeS3() {
-  GetAwsInstance()->Finalize();
+  auto instance = GetAwsInstance();
+  // The AWS instance might already be destroyed in case FinalizeS3
+  // is called from an atexit handler (which is a bad idea anyway as the
+  // AWS SDK is not safe anymore to shutdown by this time). See GH-44071.
+  if (instance == nullptr) {
+    return Status::Invalid("FinalizeS3 called too late");
+  }
+  instance->Finalize();
   return Status::OK();
 }
 
diff --git a/python/pyarrow/tests/test_fs.py b/python/pyarrow/tests/test_fs.py
index f8ce74700d..1c639412cd 100644
--- a/python/pyarrow/tests/test_fs.py
+++ b/python/pyarrow/tests/test_fs.py
@@ -19,8 +19,10 @@ from datetime import datetime, timezone, timedelta
 import gzip
 import os
 import pathlib
+from urllib.request import urlopen
 import subprocess
 import sys
+import time
 
 import pytest
 import weakref
@@ -34,6 +36,10 @@ from pyarrow.fs import (FileType, FileInfo, FileSelector, 
FileSystem,
                         LocalFileSystem, SubTreeFileSystem, _MockFileSystem,
                         FileSystemHandler, PyFileSystem, FSSpecHandler,
                         copy_files)
+from pyarrow.util import find_free_port
+
+
+here = os.path.dirname(os.path.abspath(__file__))
 
 
 class DummyHandler(FileSystemHandler):
@@ -2010,3 +2016,36 @@ def test_concurrent_s3fs_init():
         finalize_s3()
         """
     subprocess.check_call([sys.executable, "-c", code])
+
+
[email protected]
+def test_uwsgi_integration():
+    # GH-44071: using S3FileSystem under uwsgi shouldn't lead to a crash at 
shutdown
+    try:
+        subprocess.check_call(["uwsgi", "--version"])
+    except FileNotFoundError:
+        pytest.skip("uwsgi not installed on this Python")
+
+    port = find_free_port()
+    args = ["uwsgi", "-i", "--http", f"127.0.0.1:{port}",
+            "--wsgi-file", os.path.join(here, "wsgi_examples.py")]
+    proc = subprocess.Popen(args, stdin=subprocess.DEVNULL)
+    # Try to fetch URL, it should return 200 Ok...
+    try:
+        url = f"http://127.0.0.1:{port}/s3/";
+        start_time = time.time()
+        error = None
+        while time.time() < start_time + 5:
+            try:
+                with urlopen(url) as resp:
+                    assert resp.status == 200
+                break
+            except OSError as e:
+                error = e
+                time.sleep(0.1)
+        else:
+            pytest.fail(f"Could not fetch {url!r}: {error}")
+    finally:
+        proc.terminate()
+    # ... and uwsgi should gracefully shutdown after it's been asked above
+    assert proc.wait() == 30  # UWSGI_END_CODE = 30
diff --git a/python/pyarrow/tests/wsgi_examples.py 
b/python/pyarrow/tests/wsgi_examples.py
new file mode 100644
index 0000000000..440b107abe
--- /dev/null
+++ b/python/pyarrow/tests/wsgi_examples.py
@@ -0,0 +1,35 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import pyarrow.fs
+
+
+def application(env, start_response):
+    path = env['PATH_INFO']
+    members = path.split('/')
+    assert members[0] == ''
+    assert len(members) >= 2
+    root = members[1]
+    if root == 's3':
+        # See test_fs::test_uwsgi_integration
+        start_response('200 OK', [('Content-Type', 'text/html')])
+        # flake8: noqa
+        fs = pyarrow.fs.S3FileSystem()
+        return [b"Hello World\n"]
+    else:
+        start_response('404 Not Found', [('Content-Type', 'text/html')])
+        return [f"Path {path!r} not found\n".encode()]
diff --git a/python/requirements-test.txt b/python/requirements-test.txt
index 975477c422..48422f86cc 100644
--- a/python/requirements-test.txt
+++ b/python/requirements-test.txt
@@ -3,3 +3,4 @@ hypothesis
 pandas
 pytest
 pytz
+uwsgi; sys.platform != 'win32' and python_version < '3.13'
diff --git a/python/requirements-wheel-test.txt 
b/python/requirements-wheel-test.txt
index 98ec2bd4fd..bad3e251d4 100644
--- a/python/requirements-wheel-test.txt
+++ b/python/requirements-wheel-test.txt
@@ -10,6 +10,7 @@ hypothesis
 pytest
 pytz
 tzdata; sys_platform == 'win32'
+uwsgi; sys.platform != 'win32' and python_version < '3.13'
 
 # We generally test with the oldest numpy version that supports a given Python
 # version. However, there is no need to make this strictly the oldest version,

Reply via email to