Skip to content

Commit bffcbae

Browse files
expose explicit AWS SDK lifecycle for SigV4
1 parent 20047a4 commit bffcbae

4 files changed

Lines changed: 126 additions & 29 deletions

File tree

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
#pragma once
21+
22+
#include "iceberg/catalog/rest/iceberg_rest_export.h"
23+
#include "iceberg/result.h"
24+
25+
/// \file iceberg/catalog/rest/auth/aws_sdk.h
26+
/// \brief Process-wide AWS SDK lifecycle for SigV4 authentication.
27+
///
28+
/// Applications using SigV4 should call InitializeAwsSdk() at startup and
29+
/// FinalizeAwsSdk() before exit. If never called, the SDK is lazily
30+
/// initialized on first SigV4 use and leaked at process exit. FinalizeAwsSdk()
31+
/// is intended for process-shutdown sequencing, not concurrent teardown.
32+
33+
namespace iceberg::rest::auth {
34+
35+
/// \brief Initialize the AWS SDK. Idempotent.
36+
ICEBERG_REST_EXPORT Status InitializeAwsSdk();
37+
38+
/// \brief Shut down the AWS SDK. Refuses if any SigV4 sessions are alive.
39+
ICEBERG_REST_EXPORT Status FinalizeAwsSdk();
40+
41+
ICEBERG_REST_EXPORT bool IsAwsSdkInitialized();
42+
ICEBERG_REST_EXPORT bool IsAwsSdkFinalized();
43+
44+
} // namespace iceberg::rest::auth

src/iceberg/catalog/rest/auth/sigv4_auth_manager.cc

Lines changed: 62 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,13 @@
1818
*/
1919

2020
#include "iceberg/catalog/rest/auth/auth_manager_internal.h"
21+
#include "iceberg/catalog/rest/auth/aws_sdk.h"
2122
#include "iceberg/catalog/rest/auth/sigv4_auth_manager_internal.h"
2223

2324
#ifdef ICEBERG_SIGV4
2425

26+
# include <atomic>
27+
# include <mutex>
2528
# include <sstream>
2629

2730
# include <aws/core/Aws.h>
@@ -41,23 +44,17 @@ namespace iceberg::rest::auth {
4144

4245
namespace {
4346

44-
/// \brief Ensures the AWS SDK is initialized exactly once per process.
45-
///
46-
/// Aws::InitAPI / ShutdownAPI must bracket the process lifetime, which a
47-
/// library cannot enforce, so we never call ShutdownAPI (leak by design).
48-
class AwsSdkGuard {
49-
public:
50-
static void EnsureInitialized() {
51-
static AwsSdkGuard instance;
52-
(void)instance;
53-
}
47+
enum class LifecycleState : uint8_t { kUninitialized, kInitialized, kFinalized };
5448

55-
private:
56-
AwsSdkGuard() {
57-
Aws::SDKOptions options;
58-
Aws::InitAPI(options);
59-
}
60-
};
49+
std::atomic<LifecycleState> g_state{LifecycleState::kUninitialized};
50+
std::mutex g_lifecycle_mutex;
51+
Aws::SDKOptions g_sdk_options;
52+
std::atomic<size_t> g_active_session_count{0};
53+
54+
Status EnsureSdkInitialized() {
55+
if (g_state.load() == LifecycleState::kInitialized) return {};
56+
return InitializeAwsSdk();
57+
}
6158

6259
Aws::Http::HttpMethod ToAwsMethod(HttpMethod method) {
6360
switch (method) {
@@ -114,9 +111,14 @@ SigV4AuthSession::SigV4AuthSession(
114111
signing_name_(std::move(signing_name)),
115112
credentials_provider_(std::move(credentials_provider)),
116113
signer_(std::make_unique<RestSigV4Signer>(
117-
credentials_provider_, signing_name_.c_str(), signing_region_.c_str())) {}
114+
credentials_provider_, signing_name_.c_str(), signing_region_.c_str())) {
115+
// Counted so FinalizeAwsSdk() refuses to ShutdownAPI while sessions exist.
116+
g_active_session_count.fetch_add(1, std::memory_order_relaxed);
117+
}
118118

119-
SigV4AuthSession::~SigV4AuthSession() = default;
119+
SigV4AuthSession::~SigV4AuthSession() {
120+
g_active_session_count.fetch_sub(1, std::memory_order_relaxed);
121+
}
120122

121123
Result<HttpRequest> SigV4AuthSession::Authenticate(const HttpRequest& request) {
122124
ICEBERG_ASSIGN_OR_RAISE(auto delegate_request, delegate_->Authenticate(request));
@@ -189,7 +191,7 @@ SigV4AuthManager::~SigV4AuthManager() = default;
189191
Result<std::shared_ptr<AuthSession>> SigV4AuthManager::InitSession(
190192
HttpClient& init_client,
191193
const std::unordered_map<std::string, std::string>& properties) {
192-
AwsSdkGuard::EnsureInitialized();
194+
ICEBERG_RETURN_UNEXPECTED(EnsureSdkInitialized());
193195
ICEBERG_ASSIGN_OR_RAISE(auto delegate_session,
194196
delegate_->InitSession(init_client, properties));
195197
return WrapSession(std::move(delegate_session), properties);
@@ -198,7 +200,7 @@ Result<std::shared_ptr<AuthSession>> SigV4AuthManager::InitSession(
198200
Result<std::shared_ptr<AuthSession>> SigV4AuthManager::CatalogSession(
199201
HttpClient& shared_client,
200202
const std::unordered_map<std::string, std::string>& properties) {
201-
AwsSdkGuard::EnsureInitialized();
203+
ICEBERG_RETURN_UNEXPECTED(EnsureSdkInitialized());
202204
catalog_properties_ = properties;
203205
ICEBERG_ASSIGN_OR_RAISE(auto delegate_session,
204206
delegate_->CatalogSession(shared_client, properties));
@@ -323,6 +325,35 @@ Result<std::unique_ptr<AuthManager>> MakeSigV4AuthManager(
323325
return std::make_unique<SigV4AuthManager>(std::move(delegate));
324326
}
325327

328+
Status InitializeAwsSdk() {
329+
std::lock_guard<std::mutex> lock(g_lifecycle_mutex);
330+
auto state = g_state.load();
331+
if (state == LifecycleState::kInitialized) return {};
332+
if (state == LifecycleState::kFinalized) {
333+
return InvalidArgument("AWS SDK has already been finalized; cannot reinitialize");
334+
}
335+
Aws::InitAPI(g_sdk_options);
336+
g_state.store(LifecycleState::kInitialized);
337+
return {};
338+
}
339+
340+
Status FinalizeAwsSdk() {
341+
std::lock_guard<std::mutex> lock(g_lifecycle_mutex);
342+
if (g_state.load() != LifecycleState::kInitialized) return {};
343+
auto live = g_active_session_count.load();
344+
if (live != 0) {
345+
return Invalid(
346+
"Cannot finalize AWS SDK while {} SigV4 auth session(s) are still alive", live);
347+
}
348+
Aws::ShutdownAPI(g_sdk_options);
349+
g_state.store(LifecycleState::kFinalized);
350+
return {};
351+
}
352+
353+
bool IsAwsSdkInitialized() { return g_state.load() == LifecycleState::kInitialized; }
354+
355+
bool IsAwsSdkFinalized() { return g_state.load() == LifecycleState::kFinalized; }
356+
326357
} // namespace iceberg::rest::auth
327358

328359
#else // !ICEBERG_SIGV4
@@ -336,6 +367,17 @@ Result<std::unique_ptr<AuthManager>> MakeSigV4AuthManager(
336367
"SigV4 authentication is not built; configure with -DICEBERG_SIGV4=ON");
337368
}
338369

370+
Status InitializeAwsSdk() {
371+
return NotSupported(
372+
"SigV4 authentication is not built; configure with -DICEBERG_SIGV4=ON");
373+
}
374+
375+
Status FinalizeAwsSdk() { return {}; }
376+
377+
bool IsAwsSdkInitialized() { return false; }
378+
379+
bool IsAwsSdkFinalized() { return false; }
380+
339381
} // namespace iceberg::rest::auth
340382

341383
#endif // ICEBERG_SIGV4

src/iceberg/catalog/rest/meson.build

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ iceberg_rest_auth_headers = [
9696
'auth/auth_managers.h',
9797
'auth/auth_properties.h',
9898
'auth/auth_session.h',
99+
'auth/aws_sdk.h',
99100
'auth/oauth2_util.h',
100101
]
101102
install_headers(iceberg_rest_auth_headers, subdir: 'iceberg/catalog/rest/auth')

src/iceberg/test/sigv4_auth_test.cc

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,13 @@
2222
# include <string>
2323
# include <unordered_map>
2424

25-
# include <aws/core/Aws.h>
2625
# include <aws/core/auth/AWSCredentialsProvider.h>
2726
# include <gtest/gtest.h>
2827

2928
# include "iceberg/catalog/rest/auth/auth_managers.h"
3029
# include "iceberg/catalog/rest/auth/auth_properties.h"
3130
# include "iceberg/catalog/rest/auth/auth_session.h"
31+
# include "iceberg/catalog/rest/auth/aws_sdk.h"
3232
# include "iceberg/catalog/rest/auth/sigv4_auth_manager_internal.h"
3333
# include "iceberg/catalog/rest/http_client.h"
3434
# include "iceberg/table_identifier.h"
@@ -38,14 +38,7 @@ namespace iceberg::rest::auth {
3838

3939
class SigV4AuthTest : public ::testing::Test {
4040
protected:
41-
static void SetUpTestSuite() {
42-
static bool initialized = false;
43-
if (!initialized) {
44-
Aws::SDKOptions options;
45-
Aws::InitAPI(options);
46-
initialized = true;
47-
}
48-
}
41+
static void SetUpTestSuite() { ASSERT_THAT(InitializeAwsSdk(), IsOk()); }
4942

5043
HttpClient client_{{}};
5144

@@ -61,6 +54,23 @@ class SigV4AuthTest : public ::testing::Test {
6154
}
6255
};
6356

57+
TEST_F(SigV4AuthTest, LifecycleInitializeIsIdempotent) {
58+
EXPECT_THAT(InitializeAwsSdk(), IsOk());
59+
EXPECT_TRUE(IsAwsSdkInitialized());
60+
EXPECT_FALSE(IsAwsSdkFinalized());
61+
}
62+
63+
TEST_F(SigV4AuthTest, LifecycleFinalizeRefusesWhileSessionsAlive) {
64+
auto properties = MakeSigV4Properties();
65+
auto manager_result = AuthManagers::Load("test-catalog", properties);
66+
ASSERT_THAT(manager_result, IsOk());
67+
auto session_result = manager_result.value()->CatalogSession(client_, properties);
68+
ASSERT_THAT(session_result, IsOk());
69+
70+
EXPECT_THAT(FinalizeAwsSdk(), IsError(ErrorKind::kInvalid));
71+
EXPECT_TRUE(IsAwsSdkInitialized());
72+
}
73+
6474
TEST_F(SigV4AuthTest, LoadSigV4AuthManager) {
6575
auto properties = MakeSigV4Properties();
6676
auto manager_result = AuthManagers::Load("test-catalog", properties);

0 commit comments

Comments
 (0)