[FIX] component: tricky concurrency issue

This is a really interesting problem: there was a situation where a
component would not have an event handler properly bound. The reason was
that we were in a situation where the scheduler task queue was flushed
at an unfortunate timing:

- parent component template is rendered
- subcomponent is prepared:
   - sub component is willStarted
   - it is rendered (so, fiber counter is set to 0)
- scheduler task queue is flushed => we patch the DOM
- the code registered in the __prepare.then(handler) is executed, which
  add the createHook to the vnode (but after the dom is patched)

Usually, the last two steps happen in a different order, but in an
environment with connected components, we sometimes flush the task queue
at various moments, so some code could be executed between the end of
__prepare and the registered handler.

The fix is interesting: we give a callback to the __prepare function
instead of registering a .then handler to the deferred.  This callback
is then guaranteed to be called at exactly the proper timing.

Thanks seb for the hard work of finding the root cause of this issue

closes #520
This commit is contained in:
Géry Debongnie
2019-12-02 15:47:49 +01:00
committed by aab-odoo
parent 556a4644f0
commit ed4ab51c17
9 changed files with 836 additions and 875 deletions
+6 -4
View File
@@ -304,7 +304,7 @@ export class Component<T extends Env, Props extends {}> {
const fiber = new Fiber(null, this, false, target);
fiber.shouldPatch = false;
if (!__owl__.vnode) {
this.__prepareAndRender(fiber);
this.__prepareAndRender(fiber, () => {});
} else {
this.__render(fiber);
}
@@ -544,7 +544,7 @@ export class Component<T extends Env, Props extends {}> {
* subcomponent is created. It gets its scope and vars, if any, from the
* parent template.
*/
__prepare(parentFiber: Fiber, scope: any, vars: any) {
__prepare(parentFiber: Fiber, scope: any, vars: any, cb: CallableFunction): Fiber {
this.__owl__.scope = scope;
this.__owl__.vars = vars;
const fiber = new Fiber(parentFiber, this, parentFiber.force, null);
@@ -555,7 +555,8 @@ export class Component<T extends Env, Props extends {}> {
parentFiber.lastChild!.sibling = fiber;
}
parentFiber.lastChild = fiber;
return this.__prepareAndRender(fiber);
this.__prepareAndRender(fiber, cb);
return fiber;
}
__getTemplate(qweb: QWeb): string {
@@ -577,7 +578,7 @@ export class Component<T extends Env, Props extends {}> {
}
return p._template;
}
async __prepareAndRender(fiber: Fiber) {
async __prepareAndRender(fiber: Fiber, cb: CallableFunction) {
try {
await Promise.all([this.willStart(), this.__owl__.willStartCB && this.__owl__.willStartCB()]);
} catch (e) {
@@ -589,6 +590,7 @@ export class Component<T extends Env, Props extends {}> {
}
if (!fiber.isCompleted) {
this.__render(fiber);
cb();
}
}
+3 -6
View File
@@ -226,7 +226,6 @@ QWeb.addDirective({
let propStr = Object.keys(props)
.map(k => k + ":" + props[k])
.join(",");
let defID = ctx.generateID();
let componentID = ctx.generateID();
const templateKey = ctx.generateTemplateKey();
@@ -439,16 +438,14 @@ QWeb.addDirective({
}
}
ctx.addLine(`let def${defID} = w${componentID}.__prepare(extra.fiber, ${scopeVars});`);
ctx.addLine(
`let fiber = w${componentID}.__prepare(extra.fiber, ${scopeVars}, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; ${createHook}});`
);
// hack: specify empty remove hook to prevent the node from being removed from the DOM
const insertHook = refExpr ? `insert(vn) {${refExpr}},` : "";
ctx.addLine(
`let pvnode = h('dummy', {key: ${templateKey}, hook: {${insertHook}remove() {},destroy(vn) {${finalizeComponentCode}}}});`
);
ctx.addLine(`const fiber = w${componentID}.__owl__.currentFiber;`);
ctx.addLine(
`def${defID}.then(function () { if (fiber.isCompleted) { return; } const vnode = fiber.vnode; pvnode.sel = vnode.sel; ${createHook}});`
);
if (registerCode) {
ctx.addLine(registerCode);
}