HADOOP-19299. HttpReferrerAuditHeader resilience (#7095)

* HttpReferrerAuditHeader is thread safe, copying the lists/maps passed
  in and using synchronized methods when necessary.
* All exceptions raised when building referrer header are caught
  and swallowed.
* The first such error is logged at warn,  
* all errors plus stack are logged at debug

Contributed by Steve Loughran
This commit is contained in:
Steve Loughran 2024-10-07 13:53:01 +01:00 committed by GitHub
parent 1f0d9df887
commit 50e6b49e05
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 146 additions and 12 deletions

View File

@ -29,6 +29,7 @@
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.StringJoiner; import java.util.StringJoiner;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Supplier; import java.util.function.Supplier;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -40,6 +41,7 @@
import org.apache.hadoop.classification.InterfaceStability; import org.apache.hadoop.classification.InterfaceStability;
import org.apache.hadoop.fs.audit.CommonAuditContext; import org.apache.hadoop.fs.audit.CommonAuditContext;
import org.apache.hadoop.fs.store.LogExactlyOnce; import org.apache.hadoop.fs.store.LogExactlyOnce;
import org.apache.hadoop.thirdparty.com.google.common.collect.ImmutableSet;
import org.apache.http.NameValuePair; import org.apache.http.NameValuePair;
import org.apache.http.client.utils.URLEncodedUtils; import org.apache.http.client.utils.URLEncodedUtils;
@ -57,6 +59,13 @@
* {@code org.apache.hadoop.fs.s3a.audit.TestHttpReferrerAuditHeader} * {@code org.apache.hadoop.fs.s3a.audit.TestHttpReferrerAuditHeader}
* so as to verify that header generation in the S3A auditors, and * so as to verify that header generation in the S3A auditors, and
* S3 log parsing, all work. * S3 log parsing, all work.
* <p>
* This header may be shared across multiple threads at the same time.
* so some methods are marked as synchronized, specifically those reading
* or writing the attribute map.
* <p>
* For the same reason, maps and lists passed down during construction are
* copied into thread safe structures.
*/ */
@InterfaceAudience.Private @InterfaceAudience.Private
@InterfaceStability.Unstable @InterfaceStability.Unstable
@ -81,6 +90,14 @@ public final class HttpReferrerAuditHeader {
private static final LogExactlyOnce WARN_OF_URL_CREATION = private static final LogExactlyOnce WARN_OF_URL_CREATION =
new LogExactlyOnce(LOG); new LogExactlyOnce(LOG);
/**
* Log for warning of an exception raised when building
* the referrer header, including building the evaluated
* attributes.
*/
private static final LogExactlyOnce ERROR_BUILDING_REFERRER_HEADER =
new LogExactlyOnce(LOG);
/** Context ID. */ /** Context ID. */
private final String contextId; private final String contextId;
@ -122,7 +139,11 @@ public final class HttpReferrerAuditHeader {
/** /**
* Instantiate. * Instantiate.
* * <p>
* All maps/enums passed down are copied into thread safe equivalents.
* as their origin is unknown and cannot be guaranteed to
* not be shared.
* <p>
* Context and operationId are expected to be well formed * Context and operationId are expected to be well formed
* numeric/hex strings, at least adequate to be * numeric/hex strings, at least adequate to be
* used as individual path elements in a URL. * used as individual path elements in a URL.
@ -130,15 +151,15 @@ public final class HttpReferrerAuditHeader {
private HttpReferrerAuditHeader( private HttpReferrerAuditHeader(
final Builder builder) { final Builder builder) {
this.contextId = requireNonNull(builder.contextId); this.contextId = requireNonNull(builder.contextId);
this.evaluated = builder.evaluated; this.evaluated = new ConcurrentHashMap<>(builder.evaluated);
this.filter = builder.filter; this.filter = ImmutableSet.copyOf(builder.filter);
this.operationName = requireNonNull(builder.operationName); this.operationName = requireNonNull(builder.operationName);
this.path1 = builder.path1; this.path1 = builder.path1;
this.path2 = builder.path2; this.path2 = builder.path2;
this.spanId = requireNonNull(builder.spanId); this.spanId = requireNonNull(builder.spanId);
// copy the parameters from the builder and extend // copy the parameters from the builder and extend
attributes = builder.attributes; attributes = new ConcurrentHashMap<>(builder.attributes);
addAttribute(PARAM_OP, operationName); addAttribute(PARAM_OP, operationName);
addAttribute(PARAM_PATH, path1); addAttribute(PARAM_PATH, path1);
@ -166,17 +187,18 @@ private HttpReferrerAuditHeader(
* per entry, and "" returned. * per entry, and "" returned.
* @return a referrer string or "" * @return a referrer string or ""
*/ */
public String buildHttpReferrer() { public synchronized String buildHttpReferrer() {
String header; String header;
try { try {
Map<String, String> requestAttrs = new HashMap<>(attributes);
String queries; String queries;
// Update any params which are dynamically evaluated // Update any params which are dynamically evaluated
evaluated.forEach((key, eval) -> evaluated.forEach((key, eval) ->
addAttribute(key, eval.get())); requestAttrs.put(key, eval.get()));
// now build the query parameters from all attributes, static and // now build the query parameters from all attributes, static and
// evaluated, stripping out any from the filter // evaluated, stripping out any from the filter
queries = attributes.entrySet().stream() queries = requestAttrs.entrySet().stream()
.filter(e -> !filter.contains(e.getKey())) .filter(e -> !filter.contains(e.getKey()))
.map(e -> e.getKey() + "=" + e.getValue()) .map(e -> e.getKey() + "=" + e.getValue())
.collect(Collectors.joining("&")); .collect(Collectors.joining("&"));
@ -189,7 +211,14 @@ public String buildHttpReferrer() {
} catch (URISyntaxException e) { } catch (URISyntaxException e) {
WARN_OF_URL_CREATION.warn("Failed to build URI for auditor: " + e, e); WARN_OF_URL_CREATION.warn("Failed to build URI for auditor: " + e, e);
header = ""; header = "";
} catch (RuntimeException e) {
// do not let failure to build the header stop the request being
// issued.
ERROR_BUILDING_REFERRER_HEADER.warn("Failed to construct referred header {}", e.toString());
LOG.debug("Full stack", e);
header = "";
} }
return header; return header;
} }
@ -200,7 +229,7 @@ public String buildHttpReferrer() {
* @param key query key * @param key query key
* @param value query value * @param value query value
*/ */
private void addAttribute(String key, private synchronized void addAttribute(String key,
String value) { String value) {
if (StringUtils.isNotEmpty(value)) { if (StringUtils.isNotEmpty(value)) {
attributes.put(key, value); attributes.put(key, value);

View File

@ -700,7 +700,8 @@ public SdkResponse modifyResponse(Context.ModifyResponse context,
* span is deactivated. * span is deactivated.
* Package-private for testing. * Package-private for testing.
*/ */
private final class WrappingAuditSpan extends AbstractAuditSpanImpl { @VisibleForTesting
final class WrappingAuditSpan extends AbstractAuditSpanImpl {
/** /**
* Inner span. * Inner span.
@ -792,6 +793,15 @@ public boolean isValidSpan() {
return isValid && span.isValidSpan(); return isValid && span.isValidSpan();
} }
/**
* Get the inner span.
* @return the span.
*/
@VisibleForTesting
AuditSpanS3A getSpan() {
return span;
}
/** /**
* Forward to the inner span. * Forward to the inner span.
* {@inheritDoc} * {@inheritDoc}

View File

@ -38,6 +38,7 @@
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.apache.hadoop.classification.InterfaceAudience; import org.apache.hadoop.classification.InterfaceAudience;
import org.apache.hadoop.classification.VisibleForTesting;
import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.audit.AuditConstants; import org.apache.hadoop.fs.audit.AuditConstants;
import org.apache.hadoop.fs.audit.CommonAuditContext; import org.apache.hadoop.fs.audit.CommonAuditContext;
@ -252,6 +253,17 @@ private void setLastHeader(final String lastHeader) {
this.lastHeader = lastHeader; this.lastHeader = lastHeader;
} }
/**
* Get the referrer provided the span is an instance or
* subclass of LoggingAuditSpan.
* @param span span
* @return the referrer
* @throws ClassCastException if a different span type was passed in
*/
@VisibleForTesting
HttpReferrerAuditHeader getReferrer(AuditSpanS3A span) {
return ((LoggingAuditSpan) span).getReferrer();
}
/** /**
* Span which logs at debug and sets the HTTP referrer on * Span which logs at debug and sets the HTTP referrer on
* invocations. * invocations.
@ -441,10 +453,10 @@ public String toString() {
} }
/** /**
* Get the referrer; visible for tests. * Get the referrer.
* @return the referrer. * @return the referrer.
*/ */
HttpReferrerAuditHeader getReferrer() { private HttpReferrerAuditHeader getReferrer() {
return referrer; return referrer;
} }

View File

@ -24,6 +24,7 @@
import java.util.Map; import java.util.Map;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import org.assertj.core.api.Assertions;
import software.amazon.awssdk.http.SdkHttpRequest; import software.amazon.awssdk.http.SdkHttpRequest;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
@ -32,6 +33,7 @@
import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.s3a.audit.impl.LoggingAuditor; import org.apache.hadoop.fs.s3a.audit.impl.LoggingAuditor;
import org.apache.hadoop.fs.s3a.audit.impl.ReferrerExtractor;
import org.apache.hadoop.fs.store.audit.AuditSpan; import org.apache.hadoop.fs.store.audit.AuditSpan;
import org.apache.hadoop.fs.audit.CommonAuditContext; import org.apache.hadoop.fs.audit.CommonAuditContext;
import org.apache.hadoop.fs.store.audit.HttpReferrerAuditHeader; import org.apache.hadoop.fs.store.audit.HttpReferrerAuditHeader;
@ -417,4 +419,33 @@ private void expectStrippedField(final String str,
.describedAs("Stripped <%s>", str) .describedAs("Stripped <%s>", str)
.isEqualTo(ex); .isEqualTo(ex);
} }
}
/**
* Verify that exceptions raised when building referrer headers
* do not result in failures, just an empty header.
*/
@Test
public void testSpanResilience() throws Throwable {
final CommonAuditContext auditContext = CommonAuditContext.currentAuditContext();
final String failing = "failing";
auditContext.put(failing, () -> {
throw new RuntimeException("raised");
});
try {
final HttpReferrerAuditHeader referrer = ReferrerExtractor.getReferrer(auditor, span());
Assertions.assertThat(referrer.buildHttpReferrer())
.describedAs("referrer header")
.isBlank();
// repeat
LOG.info("second attempt: there should be no second warning below");
Assertions.assertThat(referrer.buildHttpReferrer())
.describedAs("referrer header 2")
.isBlank();
referrer.buildHttpReferrer();
} finally {
// critical to remove this so it doesn't interfere with any other
// tests
auditContext.remove(failing);
}
}
}

View File

@ -0,0 +1,52 @@
/*
* 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.
*/
package org.apache.hadoop.fs.s3a.audit.impl;
import org.apache.hadoop.fs.s3a.audit.AuditSpanS3A;
import org.apache.hadoop.fs.store.audit.HttpReferrerAuditHeader;
/**
* Extract the referrer from a LoggingAuditor through a package-private
* method.
*/
public final class ReferrerExtractor {
private ReferrerExtractor() {
}
/**
* Get the referrer provided the span is an instance or
* subclass of LoggingAuditSpan.
* If wrapped by a {@code WrappingAuditSpan}, it will be extracted.
* @param auditor the auditor.
* @param span span
* @return the referrer
* @throws ClassCastException if a different span type was passed in
*/
public static HttpReferrerAuditHeader getReferrer(LoggingAuditor auditor,
AuditSpanS3A span) {
AuditSpanS3A sp;
if (span instanceof ActiveAuditManagerS3A.WrappingAuditSpan) {
sp = ((ActiveAuditManagerS3A.WrappingAuditSpan) span).getSpan();
} else {
sp = span;
}
return auditor.getReferrer(sp);
}
}