Skip to content

Commit bef8554

Browse files
authored
Delay S3 repo warning if default region absent (elastic#133848) (elastic#133917)
Today we emit a log message at startup if we fail to look up the default region for use by the S3 repository. However, this is only a potential issue if the default region is needed. If the user doesn't use `repository-s3` at all, or specifies the `region` for each S3 repository, then the absence of the default region is not an issue. With this commit we delay logging anything until we encounter a situation where we actually need the default region for something.
1 parent 081bb4c commit bef8554

File tree

10 files changed

+290
-70
lines changed

10 files changed

+290
-70
lines changed

docs/changelog/133848.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
pr: 133848
2+
summary: Delay S3 repo warning if default region absent
3+
area: Snapshot/Restore
4+
type: bug
5+
issues: []

modules/repository-s3/qa/insecure-credentials/src/test/java/org/elasticsearch/repositories/s3/RepositoryCredentialsTests.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import software.amazon.awssdk.auth.credentials.AwsCredentials;
1313
import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
1414
import software.amazon.awssdk.http.SdkHttpClient;
15+
import software.amazon.awssdk.regions.Region;
1516
import software.amazon.awssdk.services.s3.S3Client;
1617

1718
import org.apache.logging.log4j.LogManager;
@@ -306,7 +307,7 @@ public static final class ProxyS3Service extends S3Service {
306307
ProjectResolver projectResolver,
307308
ResourceWatcherService resourceWatcherService
308309
) {
309-
super(environment, clusterService, projectResolver, resourceWatcherService, () -> null);
310+
super(environment, clusterService, projectResolver, resourceWatcherService, () -> Region.of(randomIdentifier()));
310311
}
311312

312313
@Override
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the "Elastic License
4+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
10+
package org.elasticsearch.repositories.s3;
11+
12+
import software.amazon.awssdk.regions.Region;
13+
14+
import org.elasticsearch.common.util.concurrent.RunOnce;
15+
import org.elasticsearch.logging.LogManager;
16+
import org.elasticsearch.logging.Logger;
17+
18+
import java.util.function.Supplier;
19+
20+
/**
21+
* Holds onto the {@link Region} provided by the given supplier (think: the AWS SDK's default provider chain) in case it's needed for a S3
22+
* repository. If the supplier fails with an exception, the first call to {@link #getDefaultRegion} will log a warning message recording
23+
* the exception.
24+
*/
25+
class S3DefaultRegionHolder {
26+
27+
private static final Logger logger = LogManager.getLogger(S3DefaultRegionHolder.class);
28+
29+
// no synchronization required, assignments happen in start() which happens-before all reads
30+
private Region defaultRegion;
31+
private Runnable defaultRegionFailureLogger = () -> {};
32+
33+
private final Runnable initializer;
34+
35+
/**
36+
* @param delegateRegionSupplier Supplies a non-null {@link Region} or throws a {@link RuntimeException}.
37+
* <p>
38+
* Retained until its first-and-only invocation when {@link #start()} is called, and then released.
39+
*/
40+
S3DefaultRegionHolder(Supplier<Region> delegateRegionSupplier) {
41+
initializer = new RunOnce(() -> {
42+
try {
43+
defaultRegion = delegateRegionSupplier.get();
44+
assert defaultRegion != null;
45+
} catch (Exception e) {
46+
defaultRegion = null;
47+
defaultRegionFailureLogger = new RunOnce(() -> logger.warn("failed to obtain region from default provider chain", e));
48+
}
49+
});
50+
}
51+
52+
void start() {
53+
initializer.run();
54+
}
55+
56+
Region getDefaultRegion() {
57+
assert defaultRegion != null || defaultRegionFailureLogger instanceof RunOnce : "not initialized";
58+
defaultRegionFailureLogger.run();
59+
return defaultRegion;
60+
}
61+
}

modules/repository-s3/src/main/java/org/elasticsearch/repositories/s3/S3RepositoryPlugin.java

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -98,12 +98,7 @@ S3Service s3Service(
9898
}
9999

100100
private static Region getDefaultRegion() {
101-
try {
102-
return DefaultAwsRegionProviderChain.builder().build().getRegion();
103-
} catch (Exception e) {
104-
logger.info("failed to obtain region from default provider chain", e);
105-
return null;
106-
}
101+
return DefaultAwsRegionProviderChain.builder().build().getRegion();
107102
}
108103

109104
@Override

modules/repository-s3/src/main/java/org/elasticsearch/repositories/s3/S3Service.java

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,6 @@
4848
import org.elasticsearch.common.component.AbstractLifecycleComponent;
4949
import org.elasticsearch.common.settings.Setting;
5050
import org.elasticsearch.common.settings.Settings;
51-
import org.elasticsearch.common.util.concurrent.RunOnce;
5251
import org.elasticsearch.core.Nullable;
5352
import org.elasticsearch.core.Releasable;
5453
import org.elasticsearch.core.Releasables;
@@ -94,8 +93,7 @@ class S3Service extends AbstractLifecycleComponent {
9493
Setting.Property.NodeScope
9594
);
9695

97-
private final Runnable defaultRegionSetter;
98-
private volatile Region defaultRegion;
96+
private final S3DefaultRegionHolder defaultRegionHolder;
9997

10098
/**
10199
* Use a signer that does not require to pre-read (and checksum) the body of PutObject and UploadPart requests since we can rely on
@@ -129,7 +127,7 @@ class S3Service extends AbstractLifecycleComponent {
129127
compareAndExchangeTimeToLive = REPOSITORY_S3_CAS_TTL_SETTING.get(nodeSettings);
130128
compareAndExchangeAntiContentionDelay = REPOSITORY_S3_CAS_ANTI_CONTENTION_DELAY_SETTING.get(nodeSettings);
131129
isStateless = DiscoveryNode.isStateless(nodeSettings);
132-
defaultRegionSetter = new RunOnce(() -> defaultRegion = defaultRegionSupplier.get());
130+
defaultRegionHolder = new S3DefaultRegionHolder(defaultRegionSupplier);
133131
s3ClientsManager = new S3ClientsManager(
134132
nodeSettings,
135133
this::buildClientReference,
@@ -266,7 +264,7 @@ Region getClientRegion(S3ClientSettings clientSettings) {
266264
} else {
267265
endpointDescription = "no configured endpoint";
268266
}
269-
final var defaultRegion = this.defaultRegion;
267+
final var defaultRegion = defaultRegionHolder.getDefaultRegion();
270268
if (defaultRegion != null) {
271269
LOGGER.debug("""
272270
found S3 client with no configured region and {}, using region [{}] from SDK""", endpointDescription, defaultRegion);
@@ -415,7 +413,7 @@ public void onBlobStoreClose(@Nullable ProjectId projectId) {
415413

416414
@Override
417415
protected void doStart() {
418-
defaultRegionSetter.run();
416+
defaultRegionHolder.start();
419417
}
420418

421419
@Override

modules/repository-s3/src/test/java/org/elasticsearch/repositories/s3/S3BlobContainerRetriesTests.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@
101101
import static org.elasticsearch.repositories.s3.S3ClientSettings.MAX_CONNECTIONS_SETTING;
102102
import static org.elasticsearch.repositories.s3.S3ClientSettings.MAX_RETRIES_SETTING;
103103
import static org.elasticsearch.repositories.s3.S3ClientSettings.READ_TIMEOUT_SETTING;
104+
import static org.elasticsearch.repositories.s3.S3ClientSettingsTests.DEFAULT_REGION_UNAVAILABLE;
104105
import static org.hamcrest.Matchers.allOf;
105106
import static org.hamcrest.Matchers.anyOf;
106107
import static org.hamcrest.Matchers.contains;
@@ -134,7 +135,7 @@ public void setUp() throws Exception {
134135
ClusterServiceUtils.createClusterService(new DeterministicTaskQueue().getThreadPool()),
135136
TestProjectResolvers.DEFAULT_PROJECT_ONLY,
136137
Mockito.mock(ResourceWatcherService.class),
137-
() -> null
138+
DEFAULT_REGION_UNAVAILABLE
138139
) {
139140
private InetAddress[] resolveHost(String host) throws UnknownHostException {
140141
assertEquals("127.0.0.1", host);
@@ -1323,7 +1324,7 @@ public void testRetryOn403InStateless() {
13231324
),
13241325
TestProjectResolvers.DEFAULT_PROJECT_ONLY,
13251326
Mockito.mock(ResourceWatcherService.class),
1326-
() -> null
1327+
DEFAULT_REGION_UNAVAILABLE
13271328
);
13281329
service.start();
13291330
recordingMeterRegistry = new RecordingMeterRegistry();

modules/repository-s3/src/test/java/org/elasticsearch/repositories/s3/S3ClientSettingsTests.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import software.amazon.awssdk.auth.credentials.AwsSessionCredentials;
1414
import software.amazon.awssdk.regions.Region;
1515

16+
import org.elasticsearch.ElasticsearchException;
1617
import org.elasticsearch.cluster.project.TestProjectResolvers;
1718
import org.elasticsearch.common.settings.MockSecureSettings;
1819
import org.elasticsearch.common.settings.Settings;
@@ -24,6 +25,7 @@
2425
import org.mockito.Mockito;
2526

2627
import java.util.Map;
28+
import java.util.function.Supplier;
2729

2830
import static org.hamcrest.Matchers.contains;
2931
import static org.hamcrest.Matchers.emptyString;
@@ -190,9 +192,11 @@ public void testRegionCanBeSet() {
190192
ClusterServiceUtils.createClusterService(new DeterministicTaskQueue().getThreadPool()),
191193
TestProjectResolvers.DEFAULT_PROJECT_ONLY,
192194
Mockito.mock(ResourceWatcherService.class),
193-
() -> null
195+
DEFAULT_REGION_UNAVAILABLE
194196
)
195197
) {
198+
s3Service.start();
199+
196200
var otherSettings = settings.get("other");
197201
Region otherRegion = s3Service.getClientRegion(otherSettings);
198202
assertEquals(randomRegion, otherRegion.toString());
@@ -213,4 +217,8 @@ public void testMaxConnectionsCanBeSet() {
213217
// the default appears in the docs so let's make sure it doesn't change:
214218
assertEquals(50, S3ClientSettings.Defaults.MAX_CONNECTIONS);
215219
}
220+
221+
public static final Supplier<Region> DEFAULT_REGION_UNAVAILABLE = () -> {
222+
throw new ElasticsearchException("default region unavailable in this test");
223+
};
216224
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the "Elastic License
4+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
10+
package org.elasticsearch.repositories.s3;
11+
12+
import software.amazon.awssdk.regions.Region;
13+
14+
import org.apache.logging.log4j.Level;
15+
import org.apache.logging.log4j.core.LogEvent;
16+
import org.elasticsearch.ElasticsearchException;
17+
import org.elasticsearch.test.ESTestCase;
18+
import org.elasticsearch.test.MockLog;
19+
20+
import java.util.concurrent.atomic.AtomicBoolean;
21+
22+
public class S3DefaultRegionHolderTests extends ESTestCase {
23+
public void testSuccess() {
24+
try (var mockLog = MockLog.capture(S3DefaultRegionHolder.class)) {
25+
mockLog.addExpectation(new NoLogEventsExpectation());
26+
27+
final var region = Region.of(randomIdentifier());
28+
final var regionSupplied = new AtomicBoolean();
29+
final var regionHolder = new S3DefaultRegionHolder(() -> {
30+
assertTrue(regionSupplied.compareAndSet(false, true)); // only called once
31+
return region;
32+
});
33+
34+
regionHolder.start();
35+
assertTrue(regionSupplied.get());
36+
assertSame(region, regionHolder.getDefaultRegion());
37+
assertSame(region, regionHolder.getDefaultRegion());
38+
39+
mockLog.assertAllExpectationsMatched();
40+
}
41+
}
42+
43+
public void testFailure() {
44+
try (var mockLog = MockLog.capture(S3DefaultRegionHolder.class)) {
45+
final var warningSeenExpectation = new MockLog.EventuallySeenEventExpectation(
46+
"warning",
47+
S3DefaultRegionHolder.class.getCanonicalName(),
48+
Level.WARN,
49+
"failed to obtain region from default provider chain"
50+
);
51+
mockLog.addExpectation(warningSeenExpectation);
52+
53+
final var regionSupplied = new AtomicBoolean();
54+
final var regionHolder = new S3DefaultRegionHolder(() -> {
55+
assertTrue(regionSupplied.compareAndSet(false, true)); // only called once
56+
throw new ElasticsearchException("simulated");
57+
});
58+
59+
regionHolder.start();
60+
assertTrue(regionSupplied.get());
61+
62+
warningSeenExpectation.setExpectSeen(); // not seen yet, but will be seen now
63+
regionHolder.getDefaultRegion();
64+
65+
mockLog.addExpectation(new NoLogEventsExpectation()); // log message not duplicated
66+
regionHolder.getDefaultRegion();
67+
68+
mockLog.assertAllExpectationsMatched();
69+
}
70+
}
71+
72+
private static class NoLogEventsExpectation implements MockLog.LoggingExpectation {
73+
private boolean seenLogEvent;
74+
75+
@Override
76+
public void match(LogEvent event) {
77+
seenLogEvent = true;
78+
}
79+
80+
@Override
81+
public void assertMatched() {
82+
assertFalse(seenLogEvent);
83+
}
84+
}
85+
}

modules/repository-s3/src/test/java/org/elasticsearch/repositories/s3/S3RepositoryTests.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
package org.elasticsearch.repositories.s3;
1111

1212
import software.amazon.awssdk.http.SdkHttpClient;
13+
import software.amazon.awssdk.regions.Region;
1314
import software.amazon.awssdk.services.s3.S3Client;
1415

1516
import org.elasticsearch.cluster.metadata.ProjectId;
@@ -68,7 +69,7 @@ private static class DummyS3Service extends S3Service {
6869
ProjectResolver projectResolver,
6970
ResourceWatcherService resourceWatcherService
7071
) {
71-
super(environment, clusterService, projectResolver, resourceWatcherService, () -> null);
72+
super(environment, clusterService, projectResolver, resourceWatcherService, () -> Region.of(randomIdentifier()));
7273
}
7374

7475
@Override

0 commit comments

Comments
 (0)