Skip to content

Commit ebaaf07

Browse files
authored
GH-49146: [C++] Add option to disable atfork handlers (#49148)
### Rationale for this change The atfork handlers we register in Arrow C++ are generally useful if the Arrow APIs are meant to be used from the child process, but they also have the unfortunate effect of executing non-async-signal-safe code in the child process even if Arrow is not be used there. That is [not allowed by POSIX](https://pubs.opengroup.org/onlinepubs/9699919799/functions/pthread_atfork.html) if the parent process is multi-threaded. There are situations where fork() is only called just before exec(), and therefore it is not necessary to run any atfork handler. ### What changes are included in this PR? 1. Add a `GetEnvVarInteger` utility function to automate parsing of a numeric environment variable 2. Remove hard-coded size limitations for environment variable values on Windows 3. Add basic unit tests for our APIs for getting and setting environment variables 4. Add an environment variable `ARROW_REGISTER_ATFORK` to disable the registration of atfork handlers at runtime ### Are these changes tested? The new environment variable cannot be easily tested automatically, so I've checked it manually. ### Are there any user-facing changes? No, only a new feature. * GitHub Issue: #49146 Authored-by: Antoine Pitrou <antoine@python.org> Signed-off-by: Antoine Pitrou <antoine@python.org>
1 parent 7c45228 commit ebaaf07

12 files changed

Lines changed: 176 additions & 67 deletions

File tree

cpp/src/arrow/filesystem/s3fs.cc

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,6 @@
119119
#include "arrow/util/string.h"
120120
#include "arrow/util/task_group.h"
121121
#include "arrow/util/thread_pool.h"
122-
#include "arrow/util/value_parsing.h"
123122

124123
namespace arrow::fs {
125124

@@ -3579,9 +3578,10 @@ S3GlobalOptions S3GlobalOptions::Defaults() {
35793578
log_level = S3LogLevel::Off;
35803579
}
35813580

3582-
value = arrow::internal::GetEnvVar("ARROW_S3_THREADS").ValueOr("1");
3583-
if (uint32_t u; ::arrow::internal::ParseUnsigned(value.data(), value.size(), &u)) {
3584-
num_event_loop_threads = u;
3581+
auto maybe_num_threads =
3582+
arrow::internal::GetEnvVarInteger("ARROW_S3_THREADS", /*min_value=*/1);
3583+
if (maybe_num_threads.ok()) {
3584+
num_event_loop_threads = static_cast<int>(*maybe_num_threads);
35853585
}
35863586
return S3GlobalOptions{log_level, num_event_loop_threads};
35873587
}

cpp/src/arrow/io/interfaces.cc

Lines changed: 8 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -390,23 +390,15 @@ namespace {
390390
constexpr int kDefaultNumIoThreads = 8;
391391

392392
std::shared_ptr<ThreadPool> MakeIOThreadPool() {
393-
int threads = 0;
394-
auto maybe_env_var = ::arrow::internal::GetEnvVar("ARROW_IO_THREADS");
395-
if (maybe_env_var.ok()) {
396-
auto str = *std::move(maybe_env_var);
397-
if (!str.empty()) {
398-
try {
399-
threads = std::stoi(str);
400-
} catch (...) {
401-
}
402-
if (threads <= 0) {
403-
ARROW_LOG(WARNING)
404-
<< "ARROW_IO_THREADS does not contain a valid number of threads "
405-
"(should be an integer > 0)";
406-
}
407-
}
393+
int threads = kDefaultNumIoThreads;
394+
auto maybe_num_threads = ::arrow::internal::GetEnvVarInteger(
395+
"ARROW_IO_THREADS", /*min_value=*/1, /*max_value=*/std::numeric_limits<int>::max());
396+
if (maybe_num_threads.ok()) {
397+
threads = static_cast<int>(*maybe_num_threads);
398+
} else if (!maybe_num_threads.status().IsKeyError()) {
399+
maybe_num_threads.status().Warn();
408400
}
409-
auto maybe_pool = ThreadPool::MakeEternal(threads > 0 ? threads : kDefaultNumIoThreads);
401+
auto maybe_pool = ThreadPool::MakeEternal(threads);
410402
if (!maybe_pool.ok()) {
411403
maybe_pool.status().Abort("Failed to create global IO thread pool");
412404
}

cpp/src/arrow/testing/gtest_util.cc

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -660,21 +660,24 @@ LocaleGuard::LocaleGuard(const char* new_locale) : impl_(new Impl(new_locale)) {
660660

661661
LocaleGuard::~LocaleGuard() {}
662662

663-
EnvVarGuard::EnvVarGuard(const std::string& name, const std::string& value)
664-
: name_(name) {
665-
auto maybe_value = arrow::internal::GetEnvVar(name);
663+
EnvVarGuard::EnvVarGuard(std::string name, std::optional<std::string> value)
664+
: name_(std::move(name)) {
665+
auto maybe_value = arrow::internal::GetEnvVar(name_);
666666
if (maybe_value.ok()) {
667-
was_set_ = true;
668667
old_value_ = *std::move(maybe_value);
669668
} else {
670-
was_set_ = false;
669+
old_value_ = std::nullopt;
670+
}
671+
if (value.has_value()) {
672+
ARROW_CHECK_OK(arrow::internal::SetEnvVar(name_, *value));
673+
} else {
674+
ARROW_CHECK_OK(arrow::internal::DelEnvVar(name_));
671675
}
672-
ARROW_CHECK_OK(arrow::internal::SetEnvVar(name, value));
673676
}
674677

675678
EnvVarGuard::~EnvVarGuard() {
676-
if (was_set_) {
677-
ARROW_CHECK_OK(arrow::internal::SetEnvVar(name_, old_value_));
679+
if (old_value_.has_value()) {
680+
ARROW_CHECK_OK(arrow::internal::SetEnvVar(name_, *old_value_));
678681
} else {
679682
ARROW_CHECK_OK(arrow::internal::DelEnvVar(name_));
680683
}

cpp/src/arrow/testing/gtest_util.h

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -418,9 +418,11 @@ ARROW_TESTING_EXPORT
418418
void AssertChildExit(int child_pid, int expected_exit_status = 0);
419419
#endif
420420

421-
// A RAII-style object that switches to a new locale, and switches back
422-
// to the old locale when going out of scope. Doesn't do anything if the
423-
// new locale doesn't exist on the local machine.
421+
// A RAII-style object that temporarily switches to a new locale
422+
//
423+
// The guard switches back to the old locale when going out of scope.
424+
// It doesn't do anything if the new locale doesn't exist on the local machine.
425+
//
424426
// ATTENTION: may crash with an assertion failure on Windows debug builds.
425427
// See ARROW-6108, also https://gerrit.libreoffice.org/#/c/54110/
426428
class ARROW_TESTING_EXPORT LocaleGuard {
@@ -433,15 +435,20 @@ class ARROW_TESTING_EXPORT LocaleGuard {
433435
std::unique_ptr<Impl> impl_;
434436
};
435437

438+
// A RAII-style object that temporarily sets an environment variable
439+
//
440+
// The guard restores the variable's previous value when going out of scope,
441+
// or deletes the variable if it was not initially set.
442+
// The environment variable can also be temporarily deleted if std::nullopt
443+
// is passed instead of a string value.
436444
class ARROW_TESTING_EXPORT EnvVarGuard {
437445
public:
438-
EnvVarGuard(const std::string& name, const std::string& value);
446+
EnvVarGuard(std::string name, std::optional<std::string> value);
439447
~EnvVarGuard();
440448

441449
protected:
442-
const std::string name_;
443-
std::string old_value_;
444-
bool was_set_;
450+
std::string name_;
451+
std::optional<std::string> old_value_;
445452
};
446453

447454
namespace internal {

cpp/src/arrow/util/atfork_internal.cc

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,22 @@ namespace internal {
3434

3535
namespace {
3636

37+
bool IsAtForkEnabled() {
38+
static bool is_enabled = [] {
39+
auto maybe_value =
40+
GetEnvVarInteger("ARROW_REGISTER_ATFORK", /*min_value=*/0, /*max_value=*/1);
41+
if (maybe_value.ok()) {
42+
return *maybe_value != 0;
43+
}
44+
if (!maybe_value.status().IsKeyError()) {
45+
maybe_value.status().Warn();
46+
}
47+
// Enabled by default
48+
return true;
49+
}();
50+
return is_enabled;
51+
}
52+
3753
// Singleton state for at-fork management.
3854
// We do not use global variables because of initialization order issues (ARROW-18383).
3955
// Instead, a function-local static ensures the state is initialized
@@ -147,7 +163,11 @@ AtForkState* GetAtForkState() {
147163
}; // namespace
148164

149165
void RegisterAtFork(std::weak_ptr<AtForkHandler> weak_handler) {
150-
GetAtForkState()->RegisterAtFork(std::move(weak_handler));
166+
// Only fetch the atfork state (and thus lazily call pthread_atfork) if enabled at all,
167+
// to minimize potential nastiness with fork and threads.
168+
if (IsAtForkEnabled()) {
169+
GetAtForkState()->RegisterAtFork(std::move(weak_handler));
170+
}
151171
}
152172

153173
} // namespace internal

cpp/src/arrow/util/atfork_test.cc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,9 @@ TEST_F(TestAtFork, SingleThread) {
190190
ASSERT_THAT(child_after_, ElementsAre());
191191
}
192192

193+
// XXX we would like to test the ARROW_REGISTER_ATFORK environment variable,
194+
// but that would require spawning a test subprocess
195+
193196
# if !(defined(ARROW_VALGRIND) || defined(ADDRESS_SANITIZER) || \
194197
defined(THREAD_SANITIZER))
195198

cpp/src/arrow/util/fuzz_internal.cc

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -36,17 +36,16 @@ MemoryPool* fuzzing_memory_pool() {
3636

3737
void LogFuzzStatus(const Status& st, const uint8_t* data, int64_t size) {
3838
static const int kVerbosity = []() {
39-
auto maybe_env_value = GetEnvVar("ARROW_FUZZING_VERBOSITY");
40-
if (maybe_env_value.status().IsKeyError()) {
41-
return 0;
39+
auto maybe_env_value =
40+
GetEnvVarInteger("ARROW_FUZZING_VERBOSITY", /*min_value=*/0, /*max_value=*/1);
41+
if (maybe_env_value.ok()) {
42+
return static_cast<int>(*maybe_env_value);
4243
}
43-
auto env_value = std::move(maybe_env_value).ValueOrDie();
44-
int32_t value;
45-
if (!ParseValue<Int32Type>(env_value.data(), env_value.length(), &value)) {
46-
Status::Invalid("Invalid value for ARROW_FUZZING_VERBOSITY: '", env_value, "'")
47-
.Abort();
44+
if (!maybe_env_value.status().IsKeyError()) {
45+
maybe_env_value.status().Abort();
4846
}
49-
return value;
47+
// Quiet by default
48+
return 0;
5049
}();
5150

5251
if (!st.ok() && kVerbosity >= 1) {

cpp/src/arrow/util/io_util.cc

Lines changed: 49 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@
9999
#include "arrow/util/io_util.h"
100100
#include "arrow/util/logging_internal.h"
101101
#include "arrow/util/mutex.h"
102+
#include "arrow/util/value_parsing.h"
102103

103104
// For filename conversion
104105
#if defined(_WIN32)
@@ -1762,38 +1763,54 @@ Result<std::string> GetEnvVar(std::string_view name) {
17621763
#ifdef _WIN32
17631764
// On Windows, getenv() reads an early copy of the process' environment
17641765
// which doesn't get updated when SetEnvironmentVariable() is called.
1765-
constexpr int32_t bufsize = 2000;
1766-
char c_str[bufsize];
1767-
auto res = GetEnvironmentVariableA(name.data(), c_str, bufsize);
1768-
if (res >= bufsize) {
1769-
return Status::CapacityError("environment variable value too long");
1770-
} else if (res == 0) {
1771-
return Status::KeyError("environment variable '", name, "'undefined");
1772-
}
1773-
return std::string(c_str);
1766+
std::string value(100, '\0');
1767+
1768+
uint32_t res = GetEnvironmentVariableA(name.data(), value.data(),
1769+
static_cast<uint32_t>(value.size()));
1770+
if (res >= value.size()) {
1771+
// Value buffer too small, need to upsize
1772+
// (`res` includes the null-terminating character in this case)
1773+
value.resize(res);
1774+
res = GetEnvironmentVariableA(name.data(), value.data(),
1775+
static_cast<uint32_t>(value.size()));
1776+
}
1777+
if (res == 0) {
1778+
return Status::KeyError("environment variable '", name, "' undefined");
1779+
}
1780+
// On success, `res` does not include the null-terminating character
1781+
DCHECK_EQ(value.data()[res], 0);
1782+
value.resize(res);
1783+
return value;
17741784
#else
17751785
char* c_str = getenv(name.data());
17761786
if (c_str == nullptr) {
1777-
return Status::KeyError("environment variable '", name, "'undefined");
1787+
return Status::KeyError("environment variable '", name, "' undefined");
17781788
}
17791789
return std::string(c_str);
17801790
#endif
17811791
}
17821792

17831793
#ifdef _WIN32
17841794
Result<NativePathString> GetEnvVarNative(std::string_view name) {
1785-
NativePathString w_name;
1786-
constexpr int32_t bufsize = 2000;
1787-
wchar_t w_str[bufsize];
1795+
ARROW_ASSIGN_OR_RAISE(std::wstring w_name, StringToNative(name));
1796+
std::wstring value(100, '\0');
17881797

1789-
ARROW_ASSIGN_OR_RAISE(w_name, StringToNative(name));
1790-
auto res = GetEnvironmentVariableW(w_name.c_str(), w_str, bufsize);
1791-
if (res >= bufsize) {
1792-
return Status::CapacityError("environment variable value too long");
1793-
} else if (res == 0) {
1794-
return Status::KeyError("environment variable '", name, "'undefined");
1798+
uint32_t res = GetEnvironmentVariableW(w_name.data(), value.data(),
1799+
static_cast<uint32_t>(value.size()));
1800+
if (res >= value.size()) {
1801+
// Value buffer too small, need to upsize
1802+
// (`res` includes the null-terminating character in this case)
1803+
value.resize(res);
1804+
res = GetEnvironmentVariableW(w_name.data(), value.data(),
1805+
static_cast<uint32_t>(value.size()));
1806+
}
1807+
if (res == 0) {
1808+
return Status::KeyError("environment variable '", name, "' undefined");
17951809
}
1796-
return NativePathString(w_str);
1810+
// On success, `res` does not include the null-terminating character
1811+
DCHECK_EQ(value.data()[res], 0);
1812+
value.resize(res);
1813+
return value;
17971814
}
17981815

17991816
#else
@@ -1804,6 +1821,18 @@ Result<NativePathString> GetEnvVarNative(std::string_view name) {
18041821

18051822
#endif
18061823

1824+
Result<int64_t> GetEnvVarInteger(std::string_view name, std::optional<int64_t> min_value,
1825+
std::optional<int64_t> max_value) {
1826+
ARROW_ASSIGN_OR_RAISE(auto env_string, GetEnvVar(name));
1827+
int64_t value;
1828+
if (!ParseValue<Int64Type>(env_string.data(), env_string.length(), &value) ||
1829+
(min_value.has_value() && value < *min_value) ||
1830+
(max_value.has_value() && value > *max_value)) {
1831+
return Status::Invalid("Invalid value for ", name, ": '", env_string, "'");
1832+
}
1833+
return value;
1834+
}
1835+
18071836
Status SetEnvVar(std::string_view name, std::string_view value) {
18081837
#ifdef _WIN32
18091838
if (SetEnvironmentVariableA(name.data(), value.data())) {

cpp/src/arrow/util/io_util.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,12 @@ ARROW_EXPORT
244244
Result<std::string> GetEnvVar(std::string_view name);
245245
ARROW_EXPORT
246246
Result<NativePathString> GetEnvVarNative(std::string_view name);
247+
// Returns KeyError if the environment variable doesn't exist,
248+
// Invalid if it's not a valid integer in the given range.
249+
ARROW_EXPORT
250+
Result<int64_t> GetEnvVarInteger(std::string_view name,
251+
std::optional<int64_t> min_value = {},
252+
std::optional<int64_t> max_value = {});
247253

248254
ARROW_EXPORT
249255
Status SetEnvVar(std::string_view name, std::string_view value);

cpp/src/arrow/util/io_util_test.cc

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1134,5 +1134,44 @@ TEST(CpuAffinity, NumberOfCores) {
11341134
#endif
11351135
}
11361136

1137+
TEST(Environment, GetEnvVar) {
1138+
// An environment variable that should exist on roughly all platforms
1139+
ASSERT_OK_AND_ASSIGN(auto v, GetEnvVar("PATH"));
1140+
ASSERT_FALSE(v.empty());
1141+
ASSERT_OK_AND_ASSIGN(auto w, GetEnvVarNative("PATH"));
1142+
ASSERT_FALSE(w.empty());
1143+
// An environment variable that most probably does not exist
1144+
ASSERT_RAISES(KeyError, GetEnvVar("BZZT_NONEXISTENT_VAR"));
1145+
ASSERT_RAISES(KeyError, GetEnvVarNative("BZZT_NONEXISTENT_VAR"));
1146+
// (we try not to rely on EnvVarGuard here as that would be circular)
1147+
}
1148+
1149+
TEST(Environment, GetEnvVarInteger) {
1150+
{
1151+
EnvVarGuard guard("FOOBAR", "5");
1152+
ASSERT_OK_AND_EQ(5, GetEnvVarInteger("FOOBAR"));
1153+
ASSERT_OK_AND_EQ(5, GetEnvVarInteger("FOOBAR", /*min_value=*/5, /*max_value=*/7));
1154+
ASSERT_RAISES(Invalid, GetEnvVarInteger("FOOBAR", /*min_value=*/6, /*max_value=*/7));
1155+
ASSERT_RAISES(Invalid, GetEnvVarInteger("FOOBAR", /*min_value=*/3, /*max_value=*/4));
1156+
}
1157+
{
1158+
EnvVarGuard guard("FOOBAR", "BAZ");
1159+
ASSERT_RAISES(Invalid, GetEnvVarInteger("FOOBAR"));
1160+
}
1161+
{
1162+
EnvVarGuard guard("FOOBAR", std::nullopt);
1163+
ASSERT_RAISES(KeyError, GetEnvVarInteger("FOOBAR"));
1164+
}
1165+
}
1166+
1167+
TEST(Environment, SetEnvVar) {
1168+
EnvVarGuard guard("FOOBAR", "one");
1169+
ASSERT_OK_AND_EQ("one", GetEnvVar("FOOBAR"));
1170+
ASSERT_OK(SetEnvVar("FOOBAR", "two"));
1171+
ASSERT_OK_AND_EQ("two", GetEnvVar("FOOBAR"));
1172+
ASSERT_OK(DelEnvVar("FOOBAR"));
1173+
ASSERT_RAISES(KeyError, GetEnvVar("FOOBAR"));
1174+
}
1175+
11371176
} // namespace internal
11381177
} // namespace arrow

0 commit comments

Comments
 (0)