-
Notifications
You must be signed in to change notification settings - Fork 397
Fix #5180: Fix race condition in class loading cycle detection #5236
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
base: main
Are you sure you want to change the base?
Conversation
The key insight here is that we need an intermediate loading state in which the infos are loaded but the class' parent chain is not linked yet. We must wait for all the infos of an inheritance cycle to be loaded to detect the cycle reliably. However, we must not wait for the linking to be completed, otherwise we end up with a future-cycle.
@@ -71,6 +71,14 @@ class AnalyzerTest { | |||
} | |||
} | |||
|
|||
/* Note about inheritance cycles: | |||
* We do not have a guarantee about the starting point of the cycle that is | |||
* detected. In fact, we don't even have a guarantee that only cycle is |
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.
* detected. In fact, we don't even have a guarantee that only cycle is | |
* detected. In fact, we don't even have a guarantee that only one cycle is |
def checkParentChain(): Future[Option[List[ClassName]]] = | ||
checkParentChain(Set()).map(_.map(_.cycle)) |
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.
This method deserves a comment about its result. What does the result mean?
@@ -495,7 +535,7 @@ private class AnalyzerRun(config: CommonPhaseConfig, initial: Boolean, | |||
unvalidatedInterfaces: List[ClassInfo], | |||
val syntheticKind: Option[SyntheticClassKind], | |||
val nonExistent: Boolean) | |||
extends Analysis.ClassInfo with ClassLoadingState with LoadingResult with ModuleUnit { | |||
extends Analysis.ClassInfo with LoadingResult with InfoLoadedState with ModuleUnit { |
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.
This seems redundant. IIUC LoadingResult extends InfoLoadedState
, so:
extends Analysis.ClassInfo with LoadingResult with InfoLoadedState with ModuleUnit { | |
extends Analysis.ClassInfo with LoadingResult with ModuleUnit { |
?
var loading: LoadingInfos = null | ||
val state = _classInfos.getOrElseUpdate(className, { | ||
loading = new LoadingClass(className) | ||
loading = new LoadingInfos | ||
loading | ||
}) |
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.
Looking at this, I wonder if we need to mark loading
as @volatile
, to ensure safe publication of the assignment across threads? Or perhaps an AtomicReference
?
val parentsFuture = Future.traverse(parentNames) { | ||
ensureLoading(_, syntheticKind = None) match { |
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.
Consider naming the lambda argument here; it's a bit non-obvious:
val parentsFuture = Future.traverse(parentNames) { | |
ensureLoading(_, syntheticKind = None) match { | |
val parentsFuture = Future.traverse(parentNames) { parentName => | |
ensureLoading(parentName, syntheticKind = None) match { |
private sealed trait ClassLoadingState | ||
private sealed trait InfoLoadedState extends ClassLoadingState | ||
private sealed trait LoadingResult extends InfoLoadedState |
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.
Perhaps consider adding methods on these types to resolve them to the possible tiers. IIUC the tiers we need are
private sealed trait ClassLoadingState {
def resolveToInfoLoadedState: Future[InfoLoadedState]
def resolveToLoadingResult: Future[LoadingResult]
}
This would lighten the notation inside the traverse
calls. More importantly, IMO it would ease understanding that all cases are really covered.
(They could be implemented either in an OO fashion or with pattern matching.)
The key insight here is that we need an intermediate loading state in which the infos are loaded but the class' parent chain is not linked yet.
We must wait for all the infos of an inheritance cycle to be loaded to detect the cycle reliably. However, we must not wait for the linking to be completed, otherwise we end up with a future-cycle.