Skip to content

Commit

Permalink
Add context associated with an HTTP request or response (#1904)
Browse files Browse the repository at this point in the history
Motivation:

Currently, there is no way to pass any additional information when we
execute a request-response exchange, except using `AsyncContext`, which
has the following cons: overhead, can be disabled by users, not visible
from netty’s threads, gone after blocking client returns. Users need a
way to associate a state with an HTTP exchange and retrieve it after
request-response completes.

Modifications:

- Add a new `servicetalk-context-api` module to define an API for
contexts (request-response context for now, but will also be used for
`AsyncContext` later);
- Add `ContextMapUtils` in `servicetalk-concurrent-internal` module that
will be shared with `servicetalk-concurrent-api` module in a follow-up;
- `DefaultContextMap` implementation based on `HashMap`;
- `HttpMetaData` extends `ContextMapHolder`;
- Add support for `ContextMap` in all request and response types;
- Add `RequestResponseContextTest` to verify new behavior;

Result:

Users can associate a context with each HTTP request and response
instance.
  • Loading branch information
idelpivnitskiy committed Nov 5, 2021
1 parent ebe2911 commit cfe7007
Show file tree
Hide file tree
Showing 46 changed files with 1,309 additions and 64 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright © 2020 Apple Inc. and the ServiceTalk project authors
* Copyright © 2020-2021 Apple Inc. and the ServiceTalk project authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -52,7 +52,7 @@ public void setup() {
for (int i = 0; i < values.length; ++i) {
values[i] = baseValue + i;
}
stMetaData = new DefaultHttpRequestMetaData(GET, "", HTTP_1_1, headers);
stMetaData = new DefaultHttpRequestMetaData(GET, "", HTTP_1_1, headers, null);
}

@Benchmark
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright © 2020 Apple Inc. and the ServiceTalk project authors
* Copyright © 2020-2021 Apple Inc. and the ServiceTalk project authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -70,7 +70,7 @@ public void setup() {
}
}
value = sb.toString();
stMetaData = new DefaultHttpRequestMetaData(GET, RELATIVE_URI, HTTP_1_1, headers);
stMetaData = new DefaultHttpRequestMetaData(GET, RELATIVE_URI, HTTP_1_1, headers, null);
}

@Benchmark
Expand Down
1 change: 1 addition & 0 deletions servicetalk-concurrent-internal/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ dependencies {
testFixturesImplementation platform("org.junit:junit-bom:$junit5Version")

api project(":servicetalk-concurrent")
api project(":servicetalk-context-api")

implementation project(":servicetalk-annotations")
implementation project(":servicetalk-utils-internal")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/*
* Copyright © 2021 Apple Inc. and the ServiceTalk project authors
*
* Licensed 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 io.servicetalk.concurrent.internal;

import io.servicetalk.context.api.ContextMap;
import io.servicetalk.context.api.ContextMap.Key;

import javax.annotation.Nullable;

import static java.lang.Integer.toHexString;
import static java.lang.System.identityHashCode;
import static java.util.Objects.requireNonNull;

/**
* Shared utilities for {@link ContextMap}.
*/
public final class ContextMapUtils {
private ContextMapUtils() {
// no instances
}

/**
* {@link Object#toString()} implementation for {@link ContextMap}.
*
* @param map {@link ContextMap} to convert
* @return {@link String} representation of the context map
*/
public static String toString(final ContextMap map) {
final String simpleName = map.getClass().getSimpleName();
final int size = map.size();
if (size == 0) {
return simpleName + '@' + toHexString(identityHashCode(map)) + ":{}";
}
// 12 is 1 characters for '@' + 8 hash code integer in hex form + 1 character for ':' + 2 characters for "{}".
// Assume size of 90 for each key/value pair: 42 overhead characters for formatting + 16 characters for key
// name + 16 characters for key type + 16 characters for value.
StringBuilder sb = new StringBuilder(simpleName.length() + 12 + size * 90);
sb.append(simpleName)
.append('@')
// There are many copies of these maps around, the content maybe equal but a differentiating factor is
// the object reference. this may help folks understand why state is not visible across AsyncContext
// boundaries.
.append(toHexString(identityHashCode(map)))
.append(":{");

map.forEach((key, value) -> {
sb.append(key).append('=').append(value == map ? "(this Map)" : value).append(',').append(' ');
return true;
});
sb.setLength(sb.length() - 2);
return sb.append('}').toString();
}

/**
* {@link java.util.Objects#equals(Object, Object)} alternative for {@link ContextMap}.
*
* @param first the first {@link ContextMap}
* @param second the second {@link ContextMap} to compare equality with the first one
* @return {@code true} if both {@link ContextMap}(s) are equal (contains the same elements), {@code false}
* otherwise.
*/
public static boolean equals(final ContextMap first, final ContextMap second) {
if (first.size() != second.size()) {
return false;
}
@SuppressWarnings("unchecked")
final Key<?> stopped = first.forEach((key, value) -> second.contains((Key<? super Object>) key, value));
return stopped == null;
}

/**
* Make sure that the {@code value} type matches with the {@link Key#type()}.
*
* @param key the {@link Key} to verify
* @param value the value to verify
* @throws NullPointerException if {@code key == null}
* @throws IllegalArgumentException if type of the {@code value} does not match with {@link Key#type()}
*/
public static void ensureType(final Key<?> key, @Nullable final Object value) {
requireNonNull(key);
if (value != null && !key.type().isInstance(value)) {
throw new IllegalArgumentException("Type of the value " + value + '(' + value.getClass() + ')' +
" does mot match with " + key);
}
}
}
23 changes: 23 additions & 0 deletions servicetalk-context-api/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*
* Copyright © 2021 Apple Inc. and the ServiceTalk project authors
*
* Licensed 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.
*/

apply plugin: "io.servicetalk.servicetalk-gradle-plugin-internal-library"

dependencies {
implementation project(":servicetalk-annotations")

implementation "com.google.code.findbugs:jsr305:$jsr305Version"
}
24 changes: 24 additions & 0 deletions servicetalk-context-api/gradle/spotbugs/main-exclusions.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Copyright © 2021 Apple Inc. and the ServiceTalk project authors
~
~ Licensed 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.
-->
<FindBugsFilter>
<!-- Expose Class<T> -->
<Match>
<Class name="io.servicetalk.context.api.ContextMap$Key"/>
<Method name="type"/>
<Bug pattern="EI_EXPOSE_REP"/>
</Match>
</FindBugsFilter>
Loading

0 comments on commit cfe7007

Please sign in to comment.