-
-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Fix TLA: module should wait for its async dependencies #2402
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
Conversation
@joeldenning Please take a look at this |
File size impactMerging tla-bug into main will impact 3 files in 2 groups. browser (2/2)
node (1/1)
extras (0/8)No impact on files in extras group. |
@@ -242,8 +242,9 @@ var nullContext = Object.freeze(Object.create(null)); | |||
// returns a promise if and only if a top-level await subgraph | |||
// throws on sync errors | |||
function postOrderExec (loader, load, seen) { | |||
if (seen[load.id]) | |||
return; | |||
if (seen[load.id]) { |
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.
If we have seen it. We should return its execution promise or undefined if it's transitively sync.
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 with this approach, in combination with making load.E
the promise for the module and its dependencies, is that it will result in deadlock for async cycles. The reason for this return was to notice when there is a cyclical back edge and exit early.
The problem is that this approach is exiting early for edges are not cyclical back references but parallel dependencies.
I think we can fix this by replacing seen with a parentStack that is unique for each postOrderExec
call, which notes the list of parent modules in the execution. We can then change the check to be if (parentStack.includes(load.id)) return;
to handle the deadlock cycle back edge.
src/system-core.js
Outdated
@@ -270,9 +271,9 @@ function postOrderExec (loader, load, seen) { | |||
} | |||
}); | |||
if (depLoadPromises) | |||
return Promise.all(depLoadPromises).then(doExec); | |||
return load.E = Promise.all(depLoadPromises).then(doExec); |
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.
Previously load.E
the module execution promise. I think it should be depLoadPromises
then execution promise.
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.
As long as we add the new handling for cyclical back edges that would be fine.
@@ -2,7 +2,7 @@ System.register([], function (_export) { | |||
var reporter; | |||
|
|||
var state = 0; | |||
var expected = ['c', 'b', 'c', 'b', 'a', 'a', 'main', 'main']; | |||
var expected = ['c', 'c', 'b', 'b', 'a', 'a', 'main', 'main']; |
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.
You can verify this as I metioned.
@@ -282,7 +283,7 @@ describe('Loading Cases', function() { | |||
loader.instantiate = async function (path) { | |||
const source = await new Promise((resolve, reject) => fs.readFile(path, (err, source) => err ? reject(err) : resolve(source.toString()))); | |||
global.System = loader; | |||
eval(source + '//# sourceURL=' + path); | |||
eval(source + '//# sourceURL=' + pathToFileURL(path).href); |
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 allow me to set breakpoints in fixtures in VSCode. If you don't feel right. I can revert it.
@shrinktofit it seems we are down a maintainer here, in this case I would suggest one of the two following options:
I can work with either approach, thought I would give you the choice between more work upfront or more work down the road - but there is more work needed either way, and we should consciously decide how we tackle that. |
@guybedford I'd prefer your thorough review. |
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.
@shrinktofit I managed to take a look at your case and your approach mostly seems ok to me. There is a deadlock case with your code for the following:
main.js
import './dep.js';
console.log('main loaded');
dep.js
import './main.js';
await new Promise(resolve => setTimeout(resolve, 100));
console.log('dep loaded');
I believe the following will not execute with your PR, and have suggested a fix.
In addition we need to ensure that the tests are fully passing - there seems to be an issue that the CI is flagging that will require fixing before this can land.
@@ -242,8 +242,9 @@ var nullContext = Object.freeze(Object.create(null)); | |||
// returns a promise if and only if a top-level await subgraph | |||
// throws on sync errors | |||
function postOrderExec (loader, load, seen) { | |||
if (seen[load.id]) | |||
return; | |||
if (seen[load.id]) { |
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 with this approach, in combination with making load.E
the promise for the module and its dependencies, is that it will result in deadlock for async cycles. The reason for this return was to notice when there is a cyclical back edge and exit early.
The problem is that this approach is exiting early for edges are not cyclical back references but parallel dependencies.
I think we can fix this by replacing seen with a parentStack that is unique for each postOrderExec
call, which notes the list of parent modules in the execution. We can then change the check to be if (parentStack.includes(load.id)) return;
to handle the deadlock cycle back edge.
src/system-core.js
Outdated
@@ -270,9 +271,9 @@ function postOrderExec (loader, load, seen) { | |||
} | |||
}); | |||
if (depLoadPromises) | |||
return Promise.all(depLoadPromises).then(doExec); | |||
return load.E = Promise.all(depLoadPromises).then(doExec); |
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.
As long as we add the new handling for cyclical back edges that would be fine.
It seems this method can not handle dead lock due to dynamic import? Since // a.js
await import('./b.js');
console.log('b'); // #1
// b.js
await import('./a.js');
console.log('a'); // #2 By the way, I found that both Node.js/Chrome/Firefox behave strangely about the above code -- Node.js siliently terminted the process(exec via |
@shrinktofit yes dynamic import will deadlock, just like any other cyclical promise chain. But in the main static resolver cycles are supported when not using dynamic import reentrancy. Also note that this cycles logic applies to both sync and async. Let me know if you need more information on a graph parent stack approach. |
I assume #2404 supersedes this PR? |
Em.. I'm not sure. Let me test it. |
@shrinktofit I'm going ahead with the release now regardless - if there are further issues or this remains unresolved please do follow up further. |
OK. I'll try to go ahead once I finish my works. |
So, is this bug fixed? |
This bug is still valid -- have a PR that resurrects this. #2488 |
I found a BUG: if module b depends on a but a has already been "seen"(
postOrderExec
-ed), then b won't wait for a's execution if a is an async module.During bug fixing, I also found test case `./test/system-core.js/Core API/Loading Cases/Top-level await/Should may be incorrect test case(which means equivalent module logic doesn't hold same semantic with ESM). I also updated that expected behaviour.
I've upload my test code here: https://github.com/shrinktofit/systemjs-tla-bug-demo :
case 1 show the BUG this PR wants to resolve.
case 2 show the test case problem as described above.
Both cases are written in ESM/TS and compiled into ESM and SystemJS.
Run them one by one in browser: