Add Graceful Shutdown Support#1321
Conversation
| private _collect() { | ||
| this._meter.collect(); | ||
| shutdown() { | ||
| this._collect(); |
There was a problem hiding this comment.
Why does shutdown call an async collect but not await it?
There was a problem hiding this comment.
Could you give an example of when this could have negative side effects? I was under the impression that async functions could continue to run even if the parent function had finished.
There was a problem hiding this comment.
It's not that it will have a negative side-effect, but that some users may want to manually call shutdown and wait for it to finish.
There was a problem hiding this comment.
I can make shutdown async as well and add awaits so users can wait for it to finish if they call it manually.
|
|
||
| shutdown(): void { | ||
| if (this._config['exporter']) { | ||
| this._config['exporter'].shutdown(); |
There was a problem hiding this comment.
I believe exporter shutdown takes a callback param? how will we know when its done?
There was a problem hiding this comment.
Is this another case where a user may want to call shutdown manually?
There was a problem hiding this comment.
yes. I can't think of a reason not to support the use-case
There was a problem hiding this comment.
Metric exporter does not take a callback param currently.
| logger: this.logger, | ||
| resource: this.resource, | ||
| }); | ||
| if (this._config['gracefulShutdown']) { |
There was a problem hiding this comment.
I usually program in C/C++, I am not attached to any one notation and if the project does not use this I have no issue switching. Thanks for bringing this to my attention!
There was a problem hiding this comment.
typically the bracket notation in typescript means you are accessing a private property
| } | ||
| } | ||
|
|
||
| shutdown(): void { |
There was a problem hiding this comment.
no callback? not waiting for the shutdown to finish?
There was a problem hiding this comment.
I had implemented all of this assuming users would not be calling shutdown manually. I can add a callback parameter if you think that would be a good feature.
There was a problem hiding this comment.
What do you mean by "not waiting for the shutdown to finish"? I assumed shutdown was a blocking function. I added a callback option which will be passed to the shutdown function in the processor but do I need to make this async and add an await or should that be enough?
There was a problem hiding this comment.
Issue with adding the callback option in these shutdown methods: any function used as a signal handler cannot have any args so adding callbacks is a bit tricky. We can get around this by adding @ts-ignore to ignore strict function typing but this may be ill advised? I compiled it using this and the test cases worked fine. Another solution I could try would be to have a private onShutdown function which is bound to the signal handler and a public shutdown function which accepts a callback - they would have identical functionality aside from the callback arg but this may look confusing as it requires duplicated code (I wish typescript had overloaded functions!)
There was a problem hiding this comment.
I am strongly against ts-ignore
There was a problem hiding this comment.
I ended up not using @ts-ignore, I also added callbacks and make the meter provider shutdown blocking.
Codecov Report
@@ Coverage Diff @@
## master #1321 +/- ##
==========================================
+ Coverage 94.12% 94.21% +0.09%
==========================================
Files 145 146 +1
Lines 4341 4377 +36
Branches 886 893 +7
==========================================
+ Hits 4086 4124 +38
+ Misses 255 253 -2
|
Co-authored-by: Daniel Dyla <dyladan@users.noreply.github.com>
Co-authored-by: Daniel Dyla <dyladan@users.noreply.github.com>
Co-authored-by: Daniel Dyla <dyladan@users.noreply.github.com>
Co-authored-by: Daniel Dyla <dyladan@users.noreply.github.com>
dyladan
left a comment
There was a problem hiding this comment.
process is not supported in web environments, which means this PR will make the meter provider not web compatible
obecny
left a comment
There was a problem hiding this comment.
The process is not part of browser and MeterProvider.ts is meant to be used in both environment, so you can't use it like this. If you want to implement mechanism that will execute something when process is closed you should mimic the same behaviour in browser (probably event unload on window). But to do that you should create some platform dependent mechanism. Put in inside core sdk in folder platform and create some easy to share function for example
// platform/browser/ShutdownNotifier.ts
export function onGlobalShutdown(callback) {
window.addEventListener('unload', callback);
}
// platform/node/ShutdownNotifier.ts
export function onGlobalShutdown(callback) {
process.once('SIGTERM', callback);
}
//and then in your code
onGlobalShutdown(function(){
this._shutdownAllMeters.bind(this)
}.bind(this));
obecny
left a comment
There was a problem hiding this comment.
Last concerns around async / await / promise and callback - I added comments, after this I think it will be good to go, thx
|
|
||
| private async _collect() { | ||
| await this._meter.collect(); | ||
| await this._exporter.export( |
There was a problem hiding this comment.
"export" function from exporters return "void" there is no reason to call "await" in front of it as it will not block anything.
IF you really want to wait for exporter to finish you should create here a promise and resolve or reject it in a callback where you added "@todo
There was a problem hiding this comment.
The todo was actually there before my time, I removed this await - you make a good point about it not being async.
There was a problem hiding this comment.
but you can return a promise to be resolved when exporter exports something (or rejected depends on callback) so this way the _collect will really wait until the exporter finishes - what was the original plan I believe
|
@obecny Hopefully this addresses your concerns. |
| this._config.exporter.shutdown(); | ||
| } | ||
| await Promise.all( | ||
| return Promise.all( |
There was a problem hiding this comment.
this is fine, but you can remove async now from the function _shutdownAllMeters as nothing is async anymore here
| if (result !== ExportResult.SUCCESS) { | ||
| // @todo: log error | ||
| } | ||
| this._exporter.export(this._meter.getBatcher().checkPointSet(), result => { |
There was a problem hiding this comment.
| this._exporter.export(this._meter.getBatcher().checkPointSet(), result => { | |
| return new Promise((resolve, reject) => { | |
| this._exporter.export( | |
| this._meter.getBatcher().checkPointSet(), | |
| result => { | |
| if (result === ExportResult.SUCCESS) { | |
| resolve(); | |
| } else { | |
| reject(); | |
| } | |
| } | |
| ); | |
| }); |
There was a problem hiding this comment.
I have added a follow up PR which fixes the remaining issues but also unifies and fixes the shutdown across all exporters, metric, spans, processors, so that the shutdown will be correctly handled in whole pipeline. It will also return promise that will resolve correctly when all tasks that depend on it will resolve including http, xmlhttprequest, grpc. Callbacks has been replaced with Promises.
@open-telemetry/javascript-approvers please have a look at follow up PR ^^
dyladan
left a comment
There was a problem hiding this comment.
This looks good. Sorry for the long review process
No worries, thanks for all the help and feedback along the way! How many more reviewers do I need for this PR? |
|
One more |
|
@dyladan have you seen #1321 (review) ? |
Yes I saw it but I assumed since you opened it against jonah's master it wasn't ready for reviews and that you would open a PR here when you were ready. |
Well I can't open the PR against otel as it requires this PR, otherwise it will show changes from here and mine changes and it will be impossible to see differences. |
|
@obecny A common pattern is to open a draft PR against otel anyway, and point out that only the last commit is subject to review. Then just rebase and force-push once this PR is merged. Not ideal, but it offers good visibility. |
| /** Metric batcher. */ | ||
| batcher?: Batcher; | ||
|
|
||
| /** Bool for whether or not graceful shutdown is enabled */ |
There was a problem hiding this comment.
It would be nice to highlight the pitfall of disabling the gracefulShutdown option.
There was a problem hiding this comment.
I added a little more emphasis about why it's good to enable graceful shutdown.
| // reset spans in memory. | ||
| memoryExporter.reset(); | ||
| beforeEach(() => { | ||
| memoryExporter = new InMemorySpanExporter(); |
There was a problem hiding this comment.
Just curious to know the reasoning behind this?
There was a problem hiding this comment.
Only having one instantiation of the in-memory exporter caused some strange bugs in testing. This approach resolves the bug and doesn't complicate the test case too much.
| it('should call an async callback when shutdown is complete', done => { | ||
| let exportedSpans = 0; | ||
| sinon.stub(exporter, 'export').callsFake((spans, callback) => { | ||
| console.log('uh, export?'); |
|
I think this can be merged, but I will let @obecny do it as he seems to have a plan for follow up. |
Graceful shutdown support was removed via open-telemetry#1522 Remove the corresponding configuration because it is not used anymore. Refs: open-telemetry#1321
Graceful shutdown support was removed via open-telemetry/opentelemetry-js#1522 Remove the corresponding configuration because it is not used anymore. Refs: open-telemetry/opentelemetry-js#1321
* chore(mongo): add DB_OPERATION attribute * chore(mongo): replace 'remove' with 'delete' * chore(mongo): add tests * chore(mongo): add condition in case cmd=unknown * chore(mongo): make code more readable
Which problem is this PR solving?
Short description of the changes
collectmethod to wait for collection to happen before export (I think this may fix an undocumented bug?)