Skip to content

Commit 1de159d

Browse files
authored
GH-15054: [C++] Change s3 finalization to happen after arrow threads finished, add pyarrow exit hook (#33858)
CRITICAL FIX: When statically linking error with AWS it was possible to have a crash on shutdown/exit. Now that should no longer be possible. BREAKING CHANGE: S3 can only be initialized and finalized once. BREAKING CHANGE: S3 (the AWS SDK) will not be finalized until after all CPU & I/O threads are finished. * Closes: #15054 Authored-by: Weston Pace <weston.pace@gmail.com> Signed-off-by: Weston Pace <weston.pace@gmail.com>
1 parent 9626c7d commit 1de159d

8 files changed

Lines changed: 142 additions & 65 deletions

File tree

cpp/src/arrow/filesystem/s3fs.cc

Lines changed: 99 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -2571,100 +2571,137 @@ Result<std::shared_ptr<io::OutputStream>> S3FileSystem::OpenAppendStream(
25712571

25722572
namespace {
25732573

2574-
std::mutex aws_init_lock;
2575-
Aws::SDKOptions aws_options;
2576-
std::atomic<bool> aws_initialized(false);
2574+
struct AwsInstance : public ::arrow::internal::Executor::Resource {
2575+
AwsInstance() : is_initialized_(false), is_finalized_(false) {}
2576+
~AwsInstance() { Finalize(/*from_destructor=*/true); }
2577+
2578+
// Returns true iff the instance was newly initialized with `options`
2579+
Result<bool> EnsureInitialized(const S3GlobalOptions& options) {
2580+
bool expected = false;
2581+
if (is_finalized_.load()) {
2582+
return Status::Invalid("Attempt to initialize S3 after it has been finalized");
2583+
}
2584+
if (is_initialized_.compare_exchange_strong(expected, true)) {
2585+
DoInitialize(options);
2586+
return true;
2587+
}
2588+
return false;
2589+
}
25772590

2578-
Status DoInitializeS3(const S3GlobalOptions& options) {
2579-
Aws::Utils::Logging::LogLevel aws_log_level;
2591+
bool IsInitialized() { return !is_finalized_ && is_initialized_; }
2592+
2593+
void Finalize(bool from_destructor = false) {
2594+
bool expected = true;
2595+
is_finalized_.store(true);
2596+
if (is_initialized_.compare_exchange_strong(expected, false)) {
2597+
if (from_destructor) {
2598+
ARROW_LOG(WARNING)
2599+
<< " arrow::fs::FinalizeS3 was not called even though S3 was initialized. "
2600+
"This could lead to a segmentation fault at exit";
2601+
RegionResolver::ResetDefaultInstance();
2602+
Aws::ShutdownAPI(aws_options_);
2603+
}
2604+
}
2605+
}
2606+
2607+
private:
2608+
void DoInitialize(const S3GlobalOptions& options) {
2609+
Aws::Utils::Logging::LogLevel aws_log_level;
25802610

25812611
#define LOG_LEVEL_CASE(level_name) \
25822612
case S3LogLevel::level_name: \
25832613
aws_log_level = Aws::Utils::Logging::LogLevel::level_name; \
25842614
break;
25852615

2586-
switch (options.log_level) {
2587-
LOG_LEVEL_CASE(Fatal)
2588-
LOG_LEVEL_CASE(Error)
2589-
LOG_LEVEL_CASE(Warn)
2590-
LOG_LEVEL_CASE(Info)
2591-
LOG_LEVEL_CASE(Debug)
2592-
LOG_LEVEL_CASE(Trace)
2593-
default:
2594-
aws_log_level = Aws::Utils::Logging::LogLevel::Off;
2595-
}
2616+
switch (options.log_level) {
2617+
LOG_LEVEL_CASE(Fatal)
2618+
LOG_LEVEL_CASE(Error)
2619+
LOG_LEVEL_CASE(Warn)
2620+
LOG_LEVEL_CASE(Info)
2621+
LOG_LEVEL_CASE(Debug)
2622+
LOG_LEVEL_CASE(Trace)
2623+
default:
2624+
aws_log_level = Aws::Utils::Logging::LogLevel::Off;
2625+
}
25962626

25972627
#undef LOG_LEVEL_CASE
25982628

25992629
#ifdef ARROW_S3_HAS_CRT
2600-
aws_options.ioOptions.clientBootstrap_create_fn =
2601-
[ev_threads = options.num_event_loop_threads]() {
2602-
// https://github.com/aws/aws-sdk-cpp/blob/1.11.15/src/aws-cpp-sdk-core/source/Aws.cpp#L65
2603-
Aws::Crt::Io::EventLoopGroup event_loop_group(ev_threads);
2604-
Aws::Crt::Io::DefaultHostResolver default_host_resolver(
2605-
event_loop_group, /*maxHosts=*/8, /*maxTTL=*/30);
2606-
auto client_bootstrap = Aws::MakeShared<Aws::Crt::Io::ClientBootstrap>(
2607-
"Aws_Init_Cleanup", event_loop_group, default_host_resolver);
2608-
client_bootstrap->EnableBlockingShutdown();
2609-
return client_bootstrap;
2610-
};
2630+
aws_options_.ioOptions.clientBootstrap_create_fn =
2631+
[ev_threads = options.num_event_loop_threads]() {
2632+
// https://github.com/aws/aws-sdk-cpp/blob/1.11.15/src/aws-cpp-sdk-core/source/Aws.cpp#L65
2633+
Aws::Crt::Io::EventLoopGroup event_loop_group(ev_threads);
2634+
Aws::Crt::Io::DefaultHostResolver default_host_resolver(
2635+
event_loop_group, /*maxHosts=*/8, /*maxTTL=*/30);
2636+
auto client_bootstrap = Aws::MakeShared<Aws::Crt::Io::ClientBootstrap>(
2637+
"Aws_Init_Cleanup", event_loop_group, default_host_resolver);
2638+
client_bootstrap->EnableBlockingShutdown();
2639+
return client_bootstrap;
2640+
};
26112641
#endif
2612-
2613-
aws_options.loggingOptions.logLevel = aws_log_level;
2614-
// By default the AWS SDK logs to files, log to console instead
2615-
aws_options.loggingOptions.logger_create_fn = [] {
2616-
return std::make_shared<Aws::Utils::Logging::ConsoleLogSystem>(
2617-
aws_options.loggingOptions.logLevel);
2618-
};
2642+
aws_options_.loggingOptions.logLevel = aws_log_level;
2643+
// By default the AWS SDK logs to files, log to console instead
2644+
aws_options_.loggingOptions.logger_create_fn = [this] {
2645+
return std::make_shared<Aws::Utils::Logging::ConsoleLogSystem>(
2646+
aws_options_.loggingOptions.logLevel);
2647+
};
26192648
#if (defined(AWS_SDK_VERSION_MAJOR) && \
26202649
(AWS_SDK_VERSION_MAJOR > 1 || AWS_SDK_VERSION_MINOR > 9 || \
26212650
(AWS_SDK_VERSION_MINOR == 9 && AWS_SDK_VERSION_PATCH >= 272)))
2622-
// ARROW-18290: escape all special chars for compatibility with non-AWS S3 backends.
2623-
// This configuration options is only available with AWS SDK 1.9.272 and later.
2624-
aws_options.httpOptions.compliantRfc3986Encoding = true;
2651+
// ARROW-18290: escape all special chars for compatibility with non-AWS S3 backends.
2652+
// This configuration options is only available with AWS SDK 1.9.272 and later.
2653+
aws_options_.httpOptions.compliantRfc3986Encoding = true;
26252654
#endif
2626-
Aws::InitAPI(aws_options);
2627-
aws_initialized.store(true);
2628-
return Status::OK();
2655+
Aws::InitAPI(aws_options_);
2656+
}
2657+
2658+
Aws::SDKOptions aws_options_;
2659+
std::atomic<bool> is_initialized_;
2660+
std::atomic<bool> is_finalized_;
2661+
};
2662+
2663+
std::shared_ptr<AwsInstance> CreateAwsInstance() {
2664+
auto instance = std::make_shared<AwsInstance>();
2665+
// Don't let S3 be shutdown until all Arrow threads are done using it
2666+
arrow::internal::GetCpuThreadPool()->KeepAlive(instance);
2667+
io::internal::GetIOThreadPool()->KeepAlive(instance);
2668+
return instance;
26292669
}
26302670

2631-
Status DoFinalizeS3() {
2632-
RegionResolver::ResetDefaultInstance();
2633-
Aws::ShutdownAPI(aws_options);
2634-
aws_initialized.store(false);
2635-
return Status::OK();
2671+
AwsInstance& GetAwsInstance() {
2672+
static auto instance = CreateAwsInstance();
2673+
return *instance;
2674+
}
2675+
2676+
Result<bool> EnsureAwsInstanceInitialized(const S3GlobalOptions& options) {
2677+
return GetAwsInstance().EnsureInitialized(options);
26362678
}
26372679

26382680
} // namespace
26392681

26402682
Status InitializeS3(const S3GlobalOptions& options) {
2641-
std::lock_guard<std::mutex> lock(aws_init_lock);
2642-
return DoInitializeS3(options);
2643-
}
2644-
2645-
Status EnsureS3Initialized() {
2646-
std::lock_guard<std::mutex> lock(aws_init_lock);
2647-
if (!aws_initialized.load()) {
2648-
S3GlobalOptions options{S3LogLevel::Fatal};
2649-
return DoInitializeS3(options);
2683+
ARROW_ASSIGN_OR_RAISE(bool successfully_initialized,
2684+
EnsureAwsInstanceInitialized(options));
2685+
if (!successfully_initialized) {
2686+
return Status::Invalid(
2687+
"S3 was already initialized. It is safe to use but the options passed in this "
2688+
"call have been ignored.");
26502689
}
26512690
return Status::OK();
26522691
}
26532692

2654-
Status FinalizeS3() {
2655-
std::lock_guard<std::mutex> lock(aws_init_lock);
2656-
return DoFinalizeS3();
2693+
Status EnsureS3Initialized() {
2694+
return EnsureAwsInstanceInitialized({S3LogLevel::Fatal}).status();
26572695
}
26582696

2659-
Status EnsureS3Finalized() {
2660-
std::lock_guard<std::mutex> lock(aws_init_lock);
2661-
if (aws_initialized.load()) {
2662-
return DoFinalizeS3();
2663-
}
2697+
Status FinalizeS3() {
2698+
GetAwsInstance().Finalize();
26642699
return Status::OK();
26652700
}
26662701

2667-
bool IsS3Initialized() { return aws_initialized.load(); }
2702+
Status EnsureS3Finalized() { return FinalizeS3(); }
2703+
2704+
bool IsS3Initialized() { return GetAwsInstance().IsInitialized(); }
26682705

26692706
// -----------------------------------------------------------------------
26702707
// Top-level utility functions

cpp/src/arrow/filesystem/s3fs.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -335,6 +335,9 @@ struct ARROW_EXPORT S3GlobalOptions {
335335

336336
/// Initialize the S3 APIs. It is required to call this function at least once
337337
/// before using S3FileSystem.
338+
///
339+
/// Once this function is called you MUST call FinalizeS3 before the end of the
340+
/// application in order to avoid a segmentation fault at shutdown.
338341
ARROW_EXPORT
339342
Status InitializeS3(const S3GlobalOptions& options);
340343

cpp/src/arrow/filesystem/s3fs_test.cc

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,7 @@ class S3TestMixin : public AwsTestMixin {
185185
Status connect_status;
186186
int retries = kNumServerRetries;
187187
do {
188-
InitServerAndClient();
188+
ASSERT_OK(InitServerAndClient());
189189
connect_status = OutcomeToStatus("ListBuckets", client_->ListBuckets());
190190
} while (!connect_status.ok() && --retries > 0);
191191
ASSERT_OK(connect_status);
@@ -198,8 +198,8 @@ class S3TestMixin : public AwsTestMixin {
198198
}
199199

200200
protected:
201-
void InitServerAndClient() {
202-
ASSERT_OK_AND_ASSIGN(minio_, GetMinioEnv()->GetOneServer());
201+
Status InitServerAndClient() {
202+
ARROW_ASSIGN_OR_RAISE(minio_, GetMinioEnv()->GetOneServer());
203203
client_config_.reset(new Aws::Client::ClientConfiguration());
204204
client_config_->endpointOverride = ToAwsString(minio_->connect_string());
205205
client_config_->scheme = Aws::Http::Scheme::HTTP;
@@ -211,6 +211,7 @@ class S3TestMixin : public AwsTestMixin {
211211
new Aws::S3::S3Client(credentials_, *client_config_,
212212
Aws::Client::AWSAuthV4Signer::PayloadSigningPolicy::Never,
213213
use_virtual_addressing));
214+
return Status::OK();
214215
}
215216

216217
// How many times to try launching a server in a row before decreeing failure

python/pyarrow/fs.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@
5959
_not_imported.append("S3FileSystem")
6060
else:
6161
ensure_s3_initialized()
62+
import atexit
63+
atexit.register(finalize_s3)
6264

6365

6466
def __getattr__(name):

r/R/arrow-package.R

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,15 @@ supported_dplyr_methods <- list(
106106
explain = NULL
107107
)
108108

109+
# This should be run at session exit and must be called
110+
# to avoid a segmentation fault at shutdown
111+
finalize_s3 <- function(env) {
112+
FinalizeS3()
113+
}
114+
115+
# Helper environment to register the exit hook
116+
s3_finalizer <- new.env(parent = emptyenv())
117+
109118
#' @importFrom vctrs s3_register vec_size vec_cast vec_unique
110119
.onLoad <- function(...) {
111120
# Make sure C++ knows on which thread it is safe to call the R API
@@ -147,6 +156,11 @@ supported_dplyr_methods <- list(
147156
# Register extension types that we use internally
148157
reregister_extension_type(vctrs_extension_type(vctrs::unspecified()))
149158

159+
# Registers a callback to run at session exit
160+
# This can't be done in .onUnload or .onDetach because those hooks are
161+
# not guaranteed to run (e.g. they only run if the user unloads arrow)
162+
reg.finalizer(s3_finalizer, finalize_s3, onexit = TRUE)
163+
150164
invisible()
151165
}
152166

r/R/arrowExports.R

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

r/src/arrowExports.cpp

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

r/src/filesystem.cpp

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -351,6 +351,13 @@ std::string fs___S3FileSystem__region(const std::shared_ptr<fs::S3FileSystem>& f
351351

352352
#endif
353353

354+
// [[arrow::export]]
355+
void FinalizeS3() {
356+
#if defined(ARROW_R_WITH_S3)
357+
StopIfNotOk(fs::FinalizeS3());
358+
#endif
359+
}
360+
354361
#if defined(ARROW_R_WITH_GCS)
355362

356363
#include <arrow/filesystem/gcsfs.h>

0 commit comments

Comments
 (0)