Skip to content
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

fix: delay plugin loading until next tick #177

Merged
merged 5 commits into from
Mar 3, 2022

Conversation

chrskrchr
Copy link
Contributor

@chrskrchr chrskrchr commented Mar 1, 2022

Description

This PR is a followup to #173. That original PR attempted to fix the async plugin loading issue described in fastify/3726 by delaying each plugin's after promise resolution until the next tick. However, this resulted in a subtle but significant change where subsequent plugin registration functions were now being enqueued in this.q and executed on the same tick of the event loop as registration, a consequence of the registration function being the only item in this.q. We observed a symptom of this change in the unrelated unit test linked below:

https://github.com/fastify/avvio/pull/173/files#r814200533

We didn't think the above was a major/breaking change in behavior, but that proved to be incorrect per this bug report:

fastify/fastify-cors#189

This PR improves on the initial PR by delaying the entire loading of each plugin to the next tick rather than just the plugin's after promise resolution. This change has been tested in @pvanagtmaal's test repo and produces the expected output for both the working and previously-broken test cases:

➜  fastify-test git:(main) ✗ node working.js
cors registered
└── /
    ├── /
    ├── * (OPTIONS)
    └── v
        ├── 1/user (GET)
        └── 2/user (GET)

➜  fastify-test git:(main) ✗ node broken.js
└── /
    ├── /
    ├── * (OPTIONS)
    └── v
        ├── 1/user (GET)
        └── 2/user (GET)

It also doesn't require any unexpected changes in unrelated test cases like the previous PR.

Checklist

plugin.js Show resolved Hide resolved
test/await-after.test.js Show resolved Hide resolved
plugin.js Outdated
@@ -40,6 +40,13 @@ function promise () {
return obj
}

function loadPluginNextTick (toLoad, cb) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add a comment explaining why this is necessary?

Copy link
Contributor Author

@chrskrchr chrskrchr Mar 1, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated

plugin.js Outdated
const parent = this
process.nextTick(() => {
loadPlugin.call(parent, toLoad, cb)
})
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please avoid creating this function.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hey @mcollina - apologies if I'm being dense here, but is the issue here that this work is being delayed until the next tick? or that it's being done within an anonymous function

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just with the anonymous function. Instead of allocating a new closure you can use a top-level function and pass arguments to it:

process.nextTick(console.log, 42)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh wow - I had no idea that process.nextTick() could accept a list of args 🤦

I just push a commit with this refactor. The function still needs to use .bind(parent) to set the right context for the loadPlugin call (which still creates a new function internally), but the refactor is a bit more readable IMO.

t.notOk(thirdLoaded, 'third is not loaded')
t.notOk(fourthLoaded, 'fourth is not loaded')

const contents = await fs.readFile(path.join(__dirname, 'fixtures', 'dummy.txt'), 'utf-8')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is your intention reading it twice?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just wanting to mix in a bit of async FS I/O in various contexts (e.g., both at the top-level and within a plugin) per @Eomm's request on the original PR:

#173 (comment)

Copy link
Contributor Author

@chrskrchr chrskrchr Mar 2, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK - so it looks like this top-level async FS read (a late addition to the original PR) was what caused the test to pass without my changes. It prevented the bug from occurring. I've removed the top-level FS read from the test and verified that test fails as expected without my changes.

@Eomm - can you verify that the remaining FS read that occurs within the plugin registration function on L363 still satisfies your original request to mix in some async FS I/O in the test?

Copy link
Member

@RafaelGSS RafaelGSS left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The tests added are passing even without your change. Could you take a look?

@chrskrchr
Copy link
Contributor Author

The tests added are passing even without your change. Could you take a look?

ooof - that's obviously unexpected. Will take a look ASAP.

Copy link
Contributor

@xtx1130 xtx1130 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this case can be added in test cases:

const t = require('tap')
const avvio = require('avvio')
let record = ''
const app = {}
avvio(app, { autostart: false })
function sleep(timeMs){
  return new Promise((resolve) => setTimeout(resolve, timeMs));
}

(async () => {
  await app.use(async function plugin0() {
    t.pass("plugin0 init");
    record += 'plugin0|'
  })
  await app.use(async function plugin1() {
    t.pass("plugin1 init");
    await sleep(500);
    record += 'plugin1|'
  })
  await sleep(1);
  await app.use(async function plugin2() {
    t.pass("plugin2 init");
    await sleep(500);
    record += 'plugin2|'
  })
  record += 'ready'
  t.equal(record, 'plugin0|plugin1|plugin2|ready')
})()

before:
image

after:
image

@chrskrchr
Copy link
Contributor Author

I think this case can be added in test cases:

...

Nice! Would you like to see that test in place of the existing test added in this PR? Or alongside it? It's more or less testing the same behavior AFAICT, but with a different style of assertions.

@chrskrchr
Copy link
Contributor Author

chrskrchr commented Mar 3, 2022

It's more or less testing the same behavior AFAICT, but with a different style of assertions.

Actually, I see that your test case is testing the await app.use() behavior rather than app.after(). I've added it to the await-use.test.js suite.

@chrskrchr
Copy link
Contributor Author

The tests added are passing even without your change. Could you take a look?

ooof - that's obviously unexpected. Will take a look ASAP.

Also, the false negative mentioned above was addressed by removing the top-level async FS read that was preventing the underlying bug from occurring when my code wasn't present:

#177 (comment)

Copy link
Member

@mcollina mcollina left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

Copy link
Member

@RafaelGSS RafaelGSS left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM.

@mcollina mcollina merged commit a8dcf0e into fastify:master Mar 3, 2022
mcollina pushed a commit that referenced this pull request Mar 3, 2022
* fix: delay plugin loading until next tick

* move `loadPluginNextTick` function alongside `loadPlugin` and add comment

* refactor loadPluginNextTick to use a bound function rather than an anonymous closure

* remove top-level FS read from unit test that was preventing the bug from occurring

* add additional test case
@chrskrchr
Copy link
Contributor Author

Merged #177 into master.

😬 I mean, what are the odds I screw this up twice?

@chrskrchr chrskrchr deleted the ck.asyncPluginFix2 branch March 3, 2022 18:55
Copy link
Contributor

@xtx1130 xtx1130 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great work!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants