Skip to content

Commit 9be8184

Browse files
committed
Change approaches - move cache into the marshaller instead of SDKFields
1 parent 3a48144 commit 9be8184

4 files changed

Lines changed: 11 additions & 175 deletions

File tree

core/protocols/aws-json-protocol/src/main/java/software/amazon/awssdk/protocols/json/internal/marshall/JsonProtocolMarshaller.java

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import java.util.EnumMap;
3131
import java.util.List;
3232
import java.util.Map;
33+
import java.util.concurrent.ConcurrentHashMap;
3334
import software.amazon.awssdk.annotations.SdkInternalApi;
3435
import software.amazon.awssdk.core.SdkBytes;
3536
import software.amazon.awssdk.core.SdkField;
@@ -65,6 +66,12 @@ public class JsonProtocolMarshaller implements ProtocolMarshaller<SdkHttpFullReq
6566

6667
private static final JsonMarshallerRegistry MARSHALLER_REGISTRY = createMarshallerRegistry();
6768

69+
// Caches the resolved marshaller for non-PAYLOAD fields, keyed by SdkField identity.
70+
// SdkField instances are static final per generated model class, so identity-based lookup is correct.
71+
// ConcurrentHashMap is used for thread safety; the one-time put per SdkField is negligible.
72+
private static final ConcurrentHashMap<SdkField<?>, JsonMarshaller<Object>> MARSHALLER_CACHE =
73+
new ConcurrentHashMap<>();
74+
6875
private final URI endpoint;
6976
private final StructuredJsonGenerator jsonGenerator;
7077
private final SdkHttpFullRequest.Builder request;
@@ -412,11 +419,8 @@ private void marshallFieldViaRegistry(SdkField<?> field, Object val) {
412419
.marshall(val, marshallerContext, field.locationName(), (SdkField<Object>) field);
413420
return;
414421
}
415-
JsonMarshaller<Object> marshaller = field.cachedMarshaller(MARSHALLER_REGISTRY);
416-
if (marshaller == null) {
417-
marshaller = MARSHALLER_REGISTRY.getMarshaller(field.location(), field.marshallingType(), val);
418-
field.cacheMarshaller(MARSHALLER_REGISTRY, marshaller);
419-
}
422+
JsonMarshaller<Object> marshaller = MARSHALLER_CACHE.computeIfAbsent(field,
423+
f -> MARSHALLER_REGISTRY.getMarshaller(f.location(), f.marshallingType(), val));
420424
marshaller.marshall(val, marshallerContext, field.locationName(), (SdkField<Object>) field);
421425
}
422426

core/protocols/aws-json-protocol/src/test/java/software/amazon/awssdk/protocols/json/internal/marshall/CachedNonPayloadMarshallingTest.java

Lines changed: 2 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -77,36 +77,21 @@ void header_string_secondCall_usesCachedMarshaller() {
7777
// Use the SAME SdkField instance for both calls so the cache is shared
7878
SdkField<String> field = headerField("x-custom-header", obj -> "headerValue");
7979

80-
// First call — populates the cache
80+
// First call — populates the internal marshaller cache
8181
SdkPojo pojo1 = new SimplePojo(field);
8282
SdkHttpFullRequest result1 = createMarshaller().marshall(pojo1);
8383

84-
// After first marshalling, the cache should be populated on the SdkField.
85-
// We can't access the exact registry key, but we can verify the field has
86-
// a non-null cached marshaller by checking that a second marshalling produces
87-
// identical output.
88-
8984
// Second call — should use cached marshaller
9085
SdkPojo pojo2 = new SimplePojo(field);
9186
SdkHttpFullRequest result2 = createMarshaller().marshall(pojo2);
9287

93-
// Both calls produce identical header output
88+
// Both calls produce identical header output, confirming the cached path works
9489
assertThat(result1.firstMatchingHeader("x-custom-header"))
9590
.isPresent()
9691
.hasValue("headerValue");
9792
assertThat(result2.firstMatchingHeader("x-custom-header"))
9893
.isPresent()
9994
.hasValue("headerValue");
100-
101-
// Verify the cache was populated: the field should have a non-null cached
102-
// marshaller for at least one registry key. Since we can't access the private
103-
// MARSHALLER_REGISTRY, we verify indirectly: the field's cachedMarshaller
104-
// with a dummy key returns null (different key), but the fact that both calls
105-
// succeeded with identical output confirms the cached path works.
106-
Object cachedWithDifferentKey = field.cachedMarshaller(new Object());
107-
assertThat(cachedWithDifferentKey)
108-
.as("Different registry key should return null")
109-
.isNull();
11095
}
11196

11297
// ---- QUERY_PARAM tests ----

core/sdk-core/src/main/java/software/amazon/awssdk/core/SdkField.java

Lines changed: 0 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -50,15 +50,6 @@ public final class SdkField<TypeT> {
5050
private final Map<TraitType, Trait> l1Traits;
5151
private final Map<Class<? extends Trait>, Trait> l2Traits;
5252

53-
// Single-slot marshaller cache. Two volatile fields are used instead of an AtomicReference to an immutable
54-
// holder to avoid per-SdkField object allocation. The read in cachedMarshaller() is not atomic across both
55-
// fields: between reading the key and reading the marshaller, another thread could overwrite both. This is
56-
// safe because (1) in practice there is only one registry per protocol, so all threads converge to the same
57-
// marshaller, and (2) the worst case with multiple registries is a benign cache miss or a single call using
58-
// a marshaller from a different registry, which self-corrects on the next call.
59-
private volatile Object cachedMarshaller;
60-
private volatile Object cachedMarshallerRegistryKey;
61-
6253
private SdkField(Builder<TypeT> builder) {
6354
this.memberName = builder.memberName;
6455
this.marshallingType = builder.marshallingType;
@@ -262,33 +253,6 @@ public boolean containsTrait(Class<? extends Trait> clzz, TraitType type) {
262253
return getTrait(clzz, type) != null;
263254
}
264255

265-
/**
266-
* Returns the cached marshaller for the given registry key, or null if not cached.
267-
* Uses reference identity ({@code ==}) for the registry key comparison.
268-
*
269-
* @param registryKey The registry key to match against the cached key.
270-
* @param <T> The type of the cached marshaller.
271-
* @return The cached marshaller if the registry key matches, or null.
272-
*/
273-
@SuppressWarnings("unchecked")
274-
public <T> T cachedMarshaller(Object registryKey) {
275-
if (cachedMarshallerRegistryKey == registryKey) {
276-
return (T) cachedMarshaller;
277-
}
278-
return null;
279-
}
280-
281-
/**
282-
* Caches the resolved marshaller for the given registry key.
283-
*
284-
* @param registryKey The registry key to associate with the cached marshaller.
285-
* @param marshaller The marshaller instance to cache.
286-
*/
287-
public void cacheMarshaller(Object registryKey, Object marshaller) {
288-
this.cachedMarshaller = marshaller;
289-
this.cachedMarshallerRegistryKey = registryKey;
290-
}
291-
292256
/**
293257
* Retrieves the current value of 'this' field from the given POJO. Uses the getter passed into the {@link Builder}.
294258
*

core/sdk-core/src/test/java/software/amazon/awssdk/core/SdkFieldCacheMarshallerTest.java

Lines changed: 0 additions & 117 deletions
This file was deleted.

0 commit comments

Comments
 (0)