Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix non-threadsafe creation of adapter for type with cyclic dependency #1832

Merged
83 changes: 63 additions & 20 deletions gson/src/main/java/com/google/gson/Gson.java
Expand Up @@ -52,7 +52,8 @@
import java.text.DateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
Expand Down Expand Up @@ -161,8 +162,8 @@ public final class Gson {
* lookup would stack overflow. We cheat by returning a proxy type adapter.
* The proxy is wired up once the initial adapter has been created.
*/
private final ThreadLocal<Map<TypeToken<?>, FutureTypeAdapter<?>>> calls
= new ThreadLocal<>();
// Uses LinkedHashMap because iteration order is important, see getAdapter() implementation below
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this could reasonably be inside the doc comment, especially since the field is private.

Can you also say exactly what the map represents in the doc comment? Maybe something like this:

A key in the LinkedHashMap for a thread is a type for which that thread is currently making a TypeAdapter, and the corresponding value is either a successful TypeAdapter or a proxy for a future TypeAdapter.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have reverted usage of LinkedHashMap as part of simplifying this pull request. I hope the newly adjusted doc comment is fine.

private final ThreadLocal<LinkedHashMap<TypeToken<?>, TypeAdapter<?>>> calls = new ThreadLocal<>();

private final ConcurrentMap<TypeToken<?>, TypeAdapter<?>> typeTokenCache = new ConcurrentHashMap<>();

Expand Down Expand Up @@ -523,46 +524,76 @@ public <T> TypeAdapter<T> getAdapter(TypeToken<T> type) {
return adapter;
}

Map<TypeToken<?>, FutureTypeAdapter<?>> threadCalls = calls.get();
boolean requiresThreadLocalCleanup = false;
LinkedHashMap<TypeToken<?>, TypeAdapter<?>> threadCalls = calls.get();
boolean isInitialAdapterRequest = false;
if (threadCalls == null) {
threadCalls = new HashMap<>();
threadCalls = new LinkedHashMap<>();
calls.set(threadCalls);
requiresThreadLocalCleanup = true;
isInitialAdapterRequest = true;
}

// the key and value type parameters always agree
@SuppressWarnings("unchecked")
FutureTypeAdapter<T> ongoingCall = (FutureTypeAdapter<T>) threadCalls.get(type);
TypeAdapter<T> ongoingCall = (TypeAdapter<T>) threadCalls.get(type);
if (ongoingCall != null) {
return ongoingCall;
}

int existingAdaptersCount = threadCalls.size();
boolean foundCandidate = false;
try {
FutureTypeAdapter<T> call = new FutureTypeAdapter<>();
threadCalls.put(type, call);

for (TypeAdapterFactory factory : factories) {
TypeAdapter<T> candidate = factory.create(this, type);
if (candidate != null) {
@SuppressWarnings("unchecked")
TypeAdapter<T> existingAdapter = (TypeAdapter<T>) typeTokenCache.putIfAbsent(type, candidate);
// If other thread concurrently added adapter prefer that one instead
if (existingAdapter != null) {
candidate = existingAdapter;
}

call.setDelegate(candidate);
// Replace future adapter with actual adapter
threadCalls.put(type, candidate);

if (isInitialAdapterRequest) {
// Publish resolved adapters to all threads
// Can only do this for the initial request because cyclic dependency TypeA -> TypeB -> TypeA
// would otherwise publish adapter for TypeB which uses not yet resolved adapter for TypeA
// See https://github.com/google/gson/issues/625
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just so I understand, in a situation like this the second TypeA will get a FutureTypeAdapter, TypeB will reference that FutureTypeAdapter, then the first TypeA will get the actual TypeAdapter returned by the TypeAdapterFactory. TypeB will continue to reference the FutureTypeAdapter it got for TypeA, but its delegate will have been updated to be the actual TypeAdapter. Is that right?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes that is correct. And in the past the issue was that the adapter for TypeB (which references the FutureTypeAdapter) was already published to other threads before the FutureTypeAdapter had been resolved. That was not thread-safe.

for (Map.Entry<TypeToken<?>, TypeAdapter<?>> resolvedAdapterEntry : threadCalls.entrySet()) {
typeTokenCache.putIfAbsent(resolvedAdapterEntry.getKey(), resolvedAdapterEntry.getValue());
}

@SuppressWarnings("unchecked")
TypeAdapter<T> actualAdapter = (TypeAdapter<T>) typeTokenCache.get(type);
Marcono1234 marked this conversation as resolved.
Show resolved Hide resolved
// Prefer the actual adapter, in case putIfAbsent call above had no effect because other
// thread already concurrently added other adapter instance for the same type
candidate = actualAdapter;
}
foundCandidate = true;
return candidate;
}
}
throw new IllegalArgumentException("GSON (" + GsonBuildConfig.VERSION + ") cannot handle " + type);
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have moved this and the return outside the try-finally.

} finally {
threadCalls.remove(type);

if (requiresThreadLocalCleanup) {
if (isInitialAdapterRequest) {
calls.remove();
}
if (!foundCandidate) {
Iterator<TypeAdapter<?>> adaptersIterator = threadCalls.values().iterator();
// Skip existing non-broken adapters
Marcono1234 marked this conversation as resolved.
Show resolved Hide resolved
for (; existingAdaptersCount > 0; existingAdaptersCount--) {
adaptersIterator.next();
}
// Remove this future adapter and all nested ones because they might
// refer to broken adapters
while (adaptersIterator.hasNext()) {
TypeAdapter<?> brokenAdapter = adaptersIterator.next();
if (brokenAdapter instanceof FutureTypeAdapter) {
// Mark adapter as broken so user sees useful exception message in
// case TypeAdapterFactory leaks reference to broken adapter
((FutureTypeAdapter<?>) brokenAdapter).markBroken();
}
adaptersIterator.remove();
}
}
}
}

Expand Down Expand Up @@ -1320,7 +1351,8 @@ public <T> T fromJson(JsonElement json, TypeToken<T> typeOfT) throws JsonSyntaxE
}

static class FutureTypeAdapter<T> extends SerializationDelegatingTypeAdapter<T> {
private TypeAdapter<T> delegate;
private TypeAdapter<T> delegate = null;
private boolean isBroken = false;

public void setDelegate(TypeAdapter<T> typeAdapter) {
if (delegate != null) {
Expand All @@ -1329,9 +1361,20 @@ public void setDelegate(TypeAdapter<T> typeAdapter) {
delegate = typeAdapter;
}

public void markBroken() {
isBroken = true;
}

private TypeAdapter<T> delegate() {
TypeAdapter<T> delegate = this.delegate;
if (isBroken) {
throw new IllegalStateException("Broken adapter has been leaked by TypeAdapterFactory");
}
Marcono1234 marked this conversation as resolved.
Show resolved Hide resolved
if (delegate == null) {
throw new IllegalStateException("Delegate has not been set yet");
// Can occur when adapter is leaked to other thread or when adapter is used for (de-)serialization
// directly within the TypeAdapterFactory which requested it
throw new IllegalStateException("Adapter for type with cyclic dependency has been used"
+ " before dependency has been resolved");
}
return delegate;
}
Expand Down