-
Notifications
You must be signed in to change notification settings - Fork 914
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
Re-use runtime workers #1733
Re-use runtime workers #1733
Conversation
}); | ||
} | ||
|
||
getIdleWorker(triggerId: string | undefined): RuntimeWorker | undefined { |
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.
why does this accept undefined
? I think I can see the use of returning undefined, but I don't see any use case in your code of accepting undefined
...
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.
FunctionsRuntimeBundle
has triggerId?: string
. A bundle with no triggerId
means "I want to run the runtime but no function in particular. It's used to diagnose the user environment.
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.
Mostly LGTM with a few comments.
// this log entry to happen during the readying. | ||
const triggerLogPromise = waitForLog(runtime.events, "SYSTEM", "triggers-parsed"); | ||
|
||
// TODO(samstern): Is this a race condition? Could 'ready' happen before we're listening for it? |
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's not a race condition right now since we attach the listener synchronously but it's super hard to tell until you go and inspect the code. If we use observables, I'd suggest BehaviorSubject for the state so we don't have to do this. Otherwise, please make the worker expose a promise like worker.ready
and use the listener in the worker constructor to maintain it, so we keep the concern local and easy to reason about. In fact, I may suggest to drop the waitForSystemLog
and just expose promises for anything we want to listen to.
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.
Ok this sounds like a big (but good) change so I need your help unpacking it:
- What is
BehaviorSubject
? - What do you mean "use the listener in the worker constructor to maintain it"?
- In fact, I may suggest to drop the waitForSystemLog and just expose promises for anything we want to listen to. --> waitForSystemLog waits for the next log of a certain type so a promise isn't a suitable replacement
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.
Also since I see you approved: do you think these improvements should be made in this PR or should they be future improvements?
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.
First, the improvements can totally be done in a separate PR, and I approve this PR as-is. The log race condition issue is not the focus here and we can address that later.
I was suggesting BehaviorSubject
from rxjs, but since we don't actually use Observables, I'd drop that idea since I don't want more things that we need to keep in mind when navigating the code base.
In details, I'd suggest that we keep the logic to wait on logs within FunctionsRuntimeInstance
. In addition to .exit
, it should also expose .ready
and .triggerParsed
, both promises. The promises themselves can of course be driven by logs, but we should not call waitForLog
outside FunctionsRuntimeInstance
. If we need more waits, we should expose each as a promise. In this way, we don't need to reason about the ordering outside.
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.
👍 that makes sense, thanks for the explanation! Will do those as a follow-up.
Just curious, why not implement the nodejs cluster api? |
@lookfirst in our case the "hub" and the runtime process need to be totally separate environments. In the runtime processes we need to be able to have a separate require cache, mock out libraries, etc. As far as I know this is not possible with |
Description
Fixes #1353
New architecture:
The functions emulator manages a
RuntimeWorkerPool
full ofRuntimeWorkers
. These wrap an instance ofFunctionsRuntimeInstance
which were previously launched as one-time-use.A
RuntimeWorker
listens for important changes within theFunctionsRuntimeInstance
and summarizes them into a state. This worker is also responsible for sending IPC messages to the child process.A
RuntimeWorker
has four possible states:IDLE
- ready to receive a request.BUSY
- currently executing a request.FINISHING
-BUSY
but will never return toIDLE
. Basically "die when you're done please".FINISHED
- the process is dead. Will never be revived, this worker is trash now.Within the runtime I changed the execution flow. The
main()
function only does the trigger discovery and stubbing at first. Then it listens for an IPC message telling it to run a function. This message handler can support mutliple invocations as long as the function doesn't experience any unhandled errors.Scenarios Tested
All scenarios use this test code:
Repeated execution, same code
Notice the code above the function definition is only executed one time.
Repeated execution with a code change in the middle
Notice that the code reloads and then the console.log is different. Unfortunately we need to do two reloads of the code after a file save: one "diagnostic" to do trigger discovery and then a second one to actually run the function for the first time.
Function that sometimes throws errors
This test is meant to show how an error thrown in a function will cause the worker to be discarded.
Slow Function
Here I call the same function twice before the first one finishes, which means that we need a second worker (scale up) and therefore the globals are loaded twice. However a third invocation that waits for one to finish re-uses an existing idling worker.
Background Triggers
Just to show that this all works for non-HTTP triggers. Each separate trigger has its own worker pool so running this chain twice causes two code reloads (would be 4 in the old system).
Sample Commands
N/A