Build speed is very important to our users. Should be run plugins in parallel by default?
The following would still happen serially:
build always happen before anything in buildFunctionsHowever, for a specific lifecycle hook, everything else should be run in parallel.
One downside would be that, by default, plugins would only be able to depend on other plugins that are defined in previous lifecycle hooks. We could introduce a plugin property after to overcome this.
Logs would need to be buffered so that they are displayed to the user as if the plugins had be run serially.
This would provide with a major speed boost.
oof. speed is important, but predictable and debuggable execution is more important.
suggest offering a function tag to indicate parallelism eg parallel([plugin1, plugin2]) and that itself runs as a plugin
Yes @sw-yx you're right, running plugins in parallel by default is probably a bad idea, for the reasons you mentioned. :(
What about this: plugins run serially unless the plugin.parallel boolean option is set to true. This can be set either by the user (in netlify.yml plugins.pluginName.parallel) or by the plugin author (in the returned plugin object), with the user having priority. When true, this plugin would run in parallel with the other plugins of the same lifecycle hook.
Orchestration and series/parallel order can be a little mind-bending. I think following we should make it as simple as possible to the user. A boolean option is very easy to understand.
What do you think?
you’ll find i’m usually not a fan of adding boolean flags :) bc it leaves no room to evolve api if we need a third behavior, plus is hard to manage combinations of booleans unless they are truly orthogonal (which requires a lot of foresight)
wdyt about the “plugin parallelizer” function i proposed above? it takes an array of plugins, but is itself a plugin, and when it is invoked runs its array of plugins in parallel.
The boolean flag could be a two-value string option too, that works as well. Although in that case it does look like there won't be a third behavior.
Could you provide an example of what you meant? I did not see how this would look in the netlify.yml or for plugin authors.
There are a couple cases to consider:
There will likely need to be 2 ways parallelism is defined
Need to ponder how this looks.
Here are some ideas:
Plugin author defining single parallel lifecycle:
module.exports = function exampleOne() {
return {
init: () => {
console.log('I am normal blocking lifecycle')
},
postbuild: {
parrallel: true,
run: () => {
console.log('I run and dont block other things!')
}
},
}
}
Plugin author defining all lifecycle method parallel:
(this example is less verbose but perhaps more confusing b/c its not explicit / easy to see everything is parallel)
module.exports = function exampleEverythingParallel() {
return {
parrallel: true,
init: () => {
console.log('I am normal blocking lifecycle')
},
postbuild: () => {
console.log('I run and dont block other things!')
}
}
}
...
ohhh. i didnt think about how it would look in the yml. yeah then a flag would work 😂
@DavidWells this sounds good. What about merging those two syntaxes:
module.exports = function exampleEverythingParallel() {
return {
// This defaults to an empty array
parallel: ['init', 'postbuild'],
init: () => {
console.log('I am normal blocking lifecycle')
},
postbuild: () => {
console.log('I run and dont block other things!')
}
}
}
Like this, hooks can remain simple functions?
- Overrides from the consumer (from netlify.yml)
What about the following?
plugins:
pluginName:
enabled: true
parallel: true # This defaults to whatever the plugin author has set
^ this looks good. I like having the same syntax in both spots.
module.exports = function exampleEverythingParallel() {
return {
parallel: ['init'], // only init is parallel, postBuild is sync
init: () => {
console.log('I dont block other things!')
},
postBuild: () => {
console.log('I block other things!')
}
}
}
Setting individual things as parallel
plugins:
pluginName:
parallel: ['init'] # // only init is parallel, other lifecycle hooks are whatever their default is
Setting everything things as parallel via true
plugins:
pluginName:
parallel: true # // if true make all lifecycle hooks parallel
I like it! :+1:
A follow-up question: logs of parallel plugins should be shown in serial and predictable order (as opposed to interleaved). To do that, they have to be buffered. To be buffered, plugins should be run in child processes.
Running plugins in child processes should be easy, using Node.js stdio: "ipc" to trigger specific hooks. It also has other advantages (see #166 and #167). What do you think?
Its looking more and more like we need child processes.
If we use child processes we need to make sure a few things happen:
Child processes might make it easier to scope env vars and other things. So I think its worth checking out
This is an attempt to summarize some of the problems with this issue for @raeesbhatti and @Benaiah to join in.
In order to make builds as fast as possible, we want to run plugins in parallel as much as possible.
Plugin outputs (#186) might impact this issue since they add a requirement for some plugins to run serially.
Plugins usually have global side effects. Running them in parallel creates race conditions.
Plugins might access/modify shared resources such as files or global variables, requiring atomic writes. Doing it in parallel creates race conditions.
Possible solutions (each of those solutions have some problems in itself):
write-file-atomic instead of fs.writeFile(). Or providing our own (if it does more than already existing libraries, which I doubt of).Logs would be non-deterministic.
Possible solution: buffer logs and output them in a deterministic order
Should this be opt-in or opt-out?
If opt-in, who should decide which plugin to parallelize: the user, the plugin author or both?
If opt-in, should all lifecycle of a plugin be parallellized, or should specific lifecycles be selected instead?