-
Notifications
You must be signed in to change notification settings - Fork 318
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
Experiment in lock-free module invalidation #9639
Conversation
Experimenting with invalidating modules' indexes without requiring full write-context locks. That should significantly improve the execution.
Deserialization of suggestions sounds like a fully parallelizable job that be done independently for individual libraries. In fact it looks like all jobs have sufficient synchronization primitives to avoid problems and we can easily increase the pool.
@@ -60,8 +61,13 @@ final class AnalyzeModuleInScopeJob( | |||
exports = ModuleExportsDiff.compute(prevExports, newExports), | |||
updates = SuggestionDiff.compute(Tree.empty, newSuggestions) | |||
) | |||
sendModuleUpdate(notification) | |||
module.setIndexed(true) | |||
if (!module.indexed()) { |
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.
Now I see that because it is effectively the only place that marks the module as indexed, just synchronizing around the indexed flag should be enough 👍
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.
Line 44 is setting an internal module field and leaving synchronized section...
Now we are changing the field again. Meanwhile the state wasn't synchronized
. What makes you believe the result of such operation is going to be consistent? Btw.
What is the invariant to be consistent with?
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.
Please find some smaller issues spread as inline comments. The bigger concern is expressed here.
Thanks for bringing this isIndex()
and setIndexed()
flag to my attention. I never knew what it is good for. Now it looks like being related to suggestion database. I am scared whenever I see synchronization being spread across multiple files. I am horrified seeing synchronization being spread across multiple projects (Module
is in runtime
while the main logic is in runtime-instrument-common
).
We desperately need some encapsulation.
It is probably acceptable (in current state of affairs) for Module
to hold some internal state needed by suggestion DB and let runtime-instrument-common
module invalidate it and schedule its recomputation. However it is not acceptable for a different module to poke around the Module
flag and change its value randomly. Making such field synchronized
doesn't improve the situation at all.
engine/runtime/src/main/java/org/enso/interpreter/runtime/ThreadExecutors.java
Outdated
Show resolved
Hide resolved
engine/runtime/src/main/java/org/enso/interpreter/runtime/Module.java
Outdated
Show resolved
Hide resolved
engine/runtime/src/main/java/org/enso/interpreter/runtime/Module.java
Outdated
Show resolved
Hide resolved
...ent-common/src/main/scala/org/enso/interpreter/instrument/execution/JobExecutionEngine.scala
Show resolved
Hide resolved
false | ||
) | ||
|
||
private val backgroundJobExecutor: ExecutorService = | ||
context.newFixedThreadPool(1, "background-job-pool", false) | ||
context.newCachedThreadPool("background-job-pool", 1, 4, 20, false) |
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.
Using more CPUs is probably desirable, but it also opens a new space for race conditions. I'd like to know the benefits, before we go that route.
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.
Deserialization jobs are quite expensive in terms of execution time and there is no need to execute them sequentially.
This is particularly important when modules' invalidation is triggered.
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.
Deserialization jobs are quite expensive in terms of execution time
Are they? We are supposed to have lazy deserialization - in such case most of the deserialization is happening later, during code execution, anyway. However I can see that SuggestionsCache
is still relying on old java.io.ObjectInputStream
mechanism - probably because we are not benchmarking SuggestionsCache
in our startup benchmarks.
This is particularly important when modules' invalidation is triggered.
Maybe you want to share some profiling output to demonstrate how important this is.
Btw. background-job-pool
is probably used for many other tasks, than deserialization - are they all ready for parallel processing?
...ment-common/src/main/scala/org/enso/interpreter/instrument/job/AnalyzeModuleInScopeJob.scala
Outdated
Show resolved
Hide resolved
@@ -60,8 +61,13 @@ final class AnalyzeModuleInScopeJob( | |||
exports = ModuleExportsDiff.compute(prevExports, newExports), | |||
updates = SuggestionDiff.compute(Tree.empty, newSuggestions) | |||
) | |||
sendModuleUpdate(notification) | |||
module.setIndexed(true) | |||
if (!module.indexed()) { |
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.
Line 44 is setting an internal module field and leaving synchronized section...
Now we are changing the field again. Meanwhile the state wasn't synchronized
. What makes you believe the result of such operation is going to be consistent? Btw.
What is the invariant to be consistent with?
Indexed | ||
} | ||
|
||
private IndexState indexState; |
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.
Let me assume the point of this IndexState
is to have a tri-state status which can be invalidated, scheduled for recomputation and consumed, when it is available. I believe such a concept is best expressed by a Future. If we change the code in Module
to just:
private volatile Future<Object> index;
public final void indexing(Future<Object> newIndex) {
this.index = newIndex;
}
public final Future<Object> index() {
return this.index;
}
I'll consider it to be encapsulated enough (for now). @hubertp, is such a future based interface enough for your purposes?
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.
I don't believe that encapsulated the whole logic as it wouldn't express a situation when in-progress indexing would be marked as dirty and require re-indexing. I can be obviously wrong but there was a reason why logic was added to Analyze*Job
to deal with this scenario without locking everything, as it was.
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.
I continue to believe Future
is the best concept to represent the desired behavior.
it wouldn't express a situation when in-progress indexing would be marked as dirty and require re-indexing
No, (cancellable) Future
represents such concepts neatly, I believe.
Extracted logic used for indexing modules to a separate map available through `RuntimeContext`. That wasy should avoid mixing in LS logic into runtime `Module`. Adjusted threadpools to better reflect the needs.
This was a useful feedback although I don't believe I have made it worse than it used to be. Previously we would use a rather broad |
From a code perspective, such a change doesn't have real impact on the behavior. Whatever flaws were present so far, are still present. The only real impact is organizational one: This PR moves the code away from |
val NotIndexed, NeedsIndexing, Indexed = Value | ||
} | ||
|
||
private val modules: ConcurrentMap[Module, IndexState.Value] = |
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.
The code is now rewritten into a new ModuleIndexing
Scala class that uses ConcurrentHashMap
. Good encapsulation, but I still miss the answer to the fundamental question: What is the invariant to be consistent with?
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.
Visual inspection of Analyze*Job
line 44 and line 64 still hints there is a possibility of race condition. The code might have been working mostly correctly so far (as there was just a single threaded background pool). However, as the throughput of the background thread pool is about to be increased from 1 the race conditions are likely to appear - until we justify by spelling out some invariants/constraints why they cannot...
@@ -60,8 +61,13 @@ final class AnalyzeModuleInScopeJob( | |||
exports = ModuleExportsDiff.compute(prevExports, newExports), | |||
updates = SuggestionDiff.compute(Tree.empty, newSuggestions) | |||
) | |||
sendModuleUpdate(notification) | |||
module.setIndexed(true) | |||
if (!ctx.state.suggestions.markAsIndexed(module)) { |
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.
The problem I see continues to be the same. Line 44 deals with state.suggestions
and line 64 does that as well. But meanwhile anything can happen. What if a new request to markAsNotIndexed
comes in another thread?
there was a reason why logic was added to Analyze*Job to deal with this scenario
My experience with Enso code base and its readiness for concurrent execution doesn't support claims that advocate reason and/or logic. Even if they were present, it is questionable they were used in a sound way. Except using the most advanced concurrent building blocks, I only see attempts to avoid answers to the basic question: how that is supposed to work at all?
Introducing an immutable `IndexState` record that allows us to determine if the just computed index is really up-to-date and could be sent to the clients.
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.
the trick with previous index state is cute, but I am afraid there is a GC problem. We need to be able to GC no longer needed IndexState
instances.
...nstrument-common/src/main/java/org/enso/interpreter/instrument/execution/ModuleIndexing.java
Show resolved
Hide resolved
...nstrument-common/src/main/java/org/enso/interpreter/instrument/execution/ModuleIndexing.java
Outdated
Show resolved
Hide resolved
val notification = Api.SuggestionsDatabaseModuleUpdateNotification( | ||
module = moduleName.toString, | ||
actions = | ||
Vector(Api.SuggestionsDatabaseAction.Clean(moduleName.toString)), | ||
exports = ModuleExportsDiff.compute(prevExports, newExports), | ||
updates = SuggestionDiff.compute(Tree.empty, newSuggestions) | ||
) | ||
sendModuleUpdate(notification) | ||
module.setIndexed(true) | ||
if (ctx.state.suggestions.markAsIndexed(module, state)) { |
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.
OK, this looks race condition free.
...nstrument-common/src/main/java/org/enso/interpreter/instrument/execution/ModuleIndexing.java
Outdated
Show resolved
Hide resolved
...nstrument-common/src/main/java/org/enso/interpreter/instrument/execution/ModuleIndexing.java
Outdated
Show resolved
Hide resolved
} | ||
|
||
/** | ||
* @return true, if module has been isIndexed. False otherwise. |
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.
The return value isn't true
anymore.
c14d1f0
to
99d6560
Compare
Pull Request Description
Checklist
Please ensure that the following checklist has been satisfied before submitting the PR:
Scala,
Java,
and
Rust
style guides. In case you are using a language not listed above, follow the Rust style guide.
./run ide build
.