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
eamonnmcmanus
merged 9 commits into
google:master
from
Marcono1234:marcono1234/threadsafe-cyclic-adapter
Dec 5, 2022
Merged
Changes from 4 commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
07b27f1
Fix non-threadsafe creation of adapter for type with cyclic dependency
Marcono1234 9408145
Improve handling of broken adapters during Gson.getAdapter(...) call
Marcono1234 eba98ca
Merge branch 'master' into marcono1234/threadsafe-cyclic-adapter
Marcono1234 1bb129d
Improve test
Marcono1234 96e609b
Slightly improve implementation and extend tests
Marcono1234 92e4f12
Merge branch 'master' into marcono1234/threadsafe-cyclic-adapter
Marcono1234 29a6969
Simplify getAdapter implementation
Marcono1234 8f2faa6
Convert GsonTest to JUnit 4 test
Marcono1234 a5ba266
Clarify getAdapter concurrency behavior
Marcono1234 File filter
Filter by extension
Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -28,10 +28,12 @@ | |
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.concurrent.ConcurrentHashMap; | ||
import java.util.concurrent.ConcurrentMap; | ||
import java.util.concurrent.atomic.AtomicLong; | ||
import java.util.concurrent.atomic.AtomicLongArray; | ||
|
||
|
@@ -127,10 +129,10 @@ 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<Map<TypeToken<?>, FutureTypeAdapter<?>>>(); | ||
private final ThreadLocal<LinkedHashMap<TypeToken<?>, FutureTypeAdapter<?>>> calls | ||
= new ThreadLocal<LinkedHashMap<TypeToken<?>, FutureTypeAdapter<?>>>(); | ||
|
||
private final Map<TypeToken<?>, TypeAdapter<?>> typeTokenCache = new ConcurrentHashMap<TypeToken<?>, TypeAdapter<?>>(); | ||
private final ConcurrentMap<TypeToken<?>, TypeAdapter<?>> typeTokenCache = new ConcurrentHashMap<TypeToken<?>, TypeAdapter<?>>(); | ||
|
||
private final ConstructorConstructor constructorConstructor; | ||
private final JsonAdapterAnnotationTypeAdapterFactory jsonAdapterFactory; | ||
|
@@ -479,20 +481,23 @@ public <T> TypeAdapter<T> getAdapter(TypeToken<T> type) { | |
return (TypeAdapter<T>) cached; | ||
} | ||
|
||
Map<TypeToken<?>, FutureTypeAdapter<?>> threadCalls = calls.get(); | ||
boolean requiresThreadLocalCleanup = false; | ||
LinkedHashMap<TypeToken<?>, FutureTypeAdapter<?>> threadCalls = calls.get(); | ||
boolean isInitialAdapterRequest = false; | ||
if (threadCalls == null) { | ||
threadCalls = new HashMap<TypeToken<?>, FutureTypeAdapter<?>>(); | ||
threadCalls = new LinkedHashMap<TypeToken<?>, FutureTypeAdapter<?>>(); | ||
calls.set(threadCalls); | ||
requiresThreadLocalCleanup = true; | ||
isInitialAdapterRequest = true; | ||
} | ||
|
||
// the key and value type parameters always agree | ||
FutureTypeAdapter<T> ongoingCall = (FutureTypeAdapter<T>) threadCalls.get(type); | ||
if (ongoingCall != null) { | ||
return ongoingCall; | ||
TypeAdapter<T> resolvedAdapter = ongoingCall.delegate; | ||
return resolvedAdapter != null ? resolvedAdapter : ongoingCall; | ||
} | ||
|
||
int existingAdaptersCount = threadCalls.size(); | ||
boolean foundCandidate = false; | ||
try { | ||
FutureTypeAdapter<T> call = new FutureTypeAdapter<T>(); | ||
threadCalls.put(type, call); | ||
|
@@ -501,17 +506,42 @@ public <T> TypeAdapter<T> getAdapter(TypeToken<T> type) { | |
TypeAdapter<T> candidate = factory.create(this, type); | ||
if (candidate != null) { | ||
call.setDelegate(candidate); | ||
typeTokenCache.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 | ||
for (Map.Entry<TypeToken<?>, FutureTypeAdapter<?>> resolvedAdapterEntry : threadCalls.entrySet()) { | ||
TypeAdapter<?> resolvedAdapter = resolvedAdapterEntry.getValue().delegate; | ||
typeTokenCache.putIfAbsent(resolvedAdapterEntry.getKey(), resolvedAdapter); | ||
} | ||
} | ||
foundCandidate = true; | ||
return candidate; | ||
} | ||
} | ||
throw new IllegalArgumentException("GSON (" + GsonBuildConfig.VERSION + ") cannot handle " + type); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Have moved this and the |
||
} finally { | ||
threadCalls.remove(type); | ||
|
||
if (requiresThreadLocalCleanup) { | ||
if (isInitialAdapterRequest) { | ||
calls.remove(); | ||
} | ||
if (!foundCandidate) { | ||
Iterator<FutureTypeAdapter<?>> 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()) { | ||
FutureTypeAdapter<?> brokenAdapter = adaptersIterator.next(); | ||
// Mark adapter as broken so user sees useful exception message in | ||
// case TypeAdapterFactory leaks reference to broken adapter | ||
brokenAdapter.markBroken(); | ||
adaptersIterator.remove(); | ||
} | ||
} | ||
} | ||
} | ||
|
||
|
@@ -1063,7 +1093,8 @@ public <T> T fromJson(JsonElement json, Type typeOfT) throws JsonSyntaxException | |
} | ||
|
||
static class FutureTypeAdapter<T> extends TypeAdapter<T> { | ||
private TypeAdapter<T> delegate; | ||
TypeAdapter<T> delegate = null; | ||
private boolean isBroken = false; | ||
|
||
public void setDelegate(TypeAdapter<T> typeAdapter) { | ||
if (delegate != null) { | ||
|
@@ -1072,18 +1103,28 @@ public void setDelegate(TypeAdapter<T> typeAdapter) { | |
delegate = typeAdapter; | ||
} | ||
|
||
@Override public T read(JsonReader in) throws IOException { | ||
public void markBroken() { | ||
isBroken = true; | ||
} | ||
|
||
private TypeAdapter<T> getResolvedDelegate() { | ||
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(); | ||
throw new IllegalStateException("Adapter for type with cyclic dependency has been leaked to " | ||
+ "other thread before dependency has been resolved"); | ||
} | ||
return delegate.read(in); | ||
return delegate; | ||
} | ||
|
||
@Override public T read(JsonReader in) throws IOException { | ||
return getResolvedDelegate().read(in); | ||
} | ||
|
||
@Override public void write(JsonWriter out, T value) throws IOException { | ||
if (delegate == null) { | ||
throw new IllegalStateException(); | ||
} | ||
delegate.write(out, value); | ||
getResolvedDelegate().write(out, value); | ||
} | ||
} | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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 aFutureTypeAdapter
,TypeB
will reference thatFutureTypeAdapter
, then the firstTypeA
will get the actualTypeAdapter
returned by theTypeAdapterFactory
.TypeB
will continue to reference theFutureTypeAdapter
it got forTypeA
, but its delegate will have been updated to be the actualTypeAdapter
. Is that right?There was a problem hiding this comment.
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 theFutureTypeAdapter
) was already published to other threads before theFutureTypeAdapter
had been resolved. That was not thread-safe.