-
Notifications
You must be signed in to change notification settings - Fork 598
Protect execute from errors caused by 2 interleaved calls #3413
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
PR HealthBreaking changes ✔️
This check can be disabled by tagging the PR with Changelog Entry ✔️
Changes to files need to be accounted for in their respective changelogs. This check can be disabled by tagging the PR with Coverage ✔️
This check for test coverage is informational (issues shown here will not fail the PR). This check can be disabled by tagging the PR with API leaks ✔️The following packages contain symbols visible in the public API, but not exported by the library. Export these symbols or remove them from your publicly visible API.
This check can be disabled by tagging the PR with License Headers ✔️
All source files should start with a license header. Unrelated files missing license headers
This check can be disabled by tagging the PR with |
It's worth noting this only really works for 2 concurrent calls to 'execute'. But this is the only case we're seeing at the moment so this simpler solution should be okay than something more general but more complicated. |
@@ -37,11 +38,20 @@ class ExecutionServiceImpl implements ExecutionService { | |||
required bool isNewDDC, | |||
required bool isFlutter, | |||
}) async { | |||
if (_activeExecuteCompleter != null) { |
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 think we must have a general design issue to allow us request an execution while already performing one. The UI does disable the 'run' button when executing; perhaps this was introduced as a side effect of the genai preview feature? We may want to file an issue to track down how nested executions are possible.
In any case, guarding against it in the execution impl (here) seems reasonable. You're waiting (a bit) for the first execution to finish. I think that's reasonable; you could also just do an early exit if you see that there's a non-null completer (not start a 2nd execution). I don't have an opinion which to do.
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.
In this case I think the double execute is from the embedding context. I tried to track down what's triggering the two events but wasn't very successful.
But given there are probably other ways to introduce the same effect, making this atomic seems like a safety measure to avoid these double calls everywhere.
I was worried about the second execute actually containing different code than the first. For example, if the embedding environment sends an "empty" request followed by a real request in quick succession. We likely just want the second one.
Maybe we could throw out the first request, that seems like a safe alternative here. I don't have any strong opinions on that solution vs the one I have here. Do you have a preference?
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.
It would be simplest to do an early exit if _activeExecuteCompleter
is non-null and assume that the existing completer will complete correctly in the future, but no, no real preference.
if (!reload) { | ||
await _reset(); | ||
} | ||
|
||
return _send(reload ? 'executeReload' : 'execute', { | ||
await _send(reload ? 'executeReload' : 'execute', { |
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 the implementation of _send
, this will complete very quickly - it's not doing a lot of computation, loading resources, ... anything expensive. I'm not sure that putting a completer here will actually serialize execution requests.
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 completer is more for the Future
returned by _reset
. We have to make sure _send
and _reset
(specifically the iframe part of _reset
) happen in an atomic action.
I agree _send
should be very fast so I'm not particularly worried about that one completing in time. But it can introduce an async gap so we still have to include it in the atomic block.
_reset
has a timeout so that part should be fine. But if _reset
throws then the first call will never complete its completer and the timeout would get triggered. So the timeout is really just a safety measure.
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.
Ah! I missed that this also included the _reset()
. You might wrap both the call to _reset, and to _send, in a try/finally, so that the completer gets finished even if reset throws an exception.
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.
lgtm, w/ a suggestion to wrap some code in a try { } finally { } block.
@@ -37,11 +38,20 @@ class ExecutionServiceImpl implements ExecutionService { | |||
required bool isNewDDC, | |||
required bool isFlutter, | |||
}) async { | |||
if (_activeExecuteCompleter != null) { |
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.
It would be simplest to do an early exit if _activeExecuteCompleter
is non-null and assume that the existing completer will complete correctly in the future, but no, no real preference.
if (!reload) { | ||
await _reset(); | ||
} | ||
|
||
return _send(reload ? 'executeReload' : 'execute', { | ||
await _send(reload ? 'executeReload' : 'execute', { |
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.
Ah! I missed that this also included the _reset()
. You might wrap both the call to _reset, and to _send, in a try/finally, so that the completer gets finished even if reset throws an exception.
#3366 seems to be caused by two different blobs getting loaded into the same iframe.
This change uses a Completer to make the 'execute' operation to make it atomic.
The
execute
command which triggers the blobs to load should be resetting the iframe. However, there is an async gap after the iframe as been reset. During which another 'execute' command can be triggered and the iframe would again synchronously be reset. But both blobs would then be injected into the same iframe breaking some invariants later on.Also add a timeout to the blocking call just in case an earlier 'execute' never completes. This will still allow the newer call to progress eventually.