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

async_hooks: move statement #14054

Closed
wants to merge 2 commits into from
Closed

Conversation

BridgeAR
Copy link
Member

@BridgeAR BridgeAR commented Jul 3, 2017

The fatalError function is overloaded to receive either an error or a string. Instead, it would be simpler to just always provide a error. Removed due to a conflict.

I also moved the restoreTmpHooks call. It seemed to be more appropriate in the init function. Ref #13755 (comment)

Checklist
  • make -j4 test (UNIX), or vcbuild test (Windows) passes
  • tests and/or benchmarks are included
  • documentation is changed or added
  • commit message follows commit guidelines
Affected core subsystem(s)

async_hooks

@nodejs-github-bot nodejs-github-bot added the async_hooks Issues and PRs related to the async hooks subsystem. label Jul 3, 2017
@AndreasMadsen
Copy link
Member

The simplification conflicts with #14051

If you think this simplification is better, could you split up the PR so we can review the commits seperately?

@BridgeAR
Copy link
Member Author

BridgeAR commented Jul 3, 2017

@AndreasMadsen Oops. Hm, I don't have a strong opinion. As you've worked far more with async_hooks, I'd say it's your call.

Error.captureStackTrace(o, fatalError);
process._rawDebug(o.stack);
}
process._rawDebug(e.stack);
Copy link
Contributor

@refack refack Jul 3, 2017

Choose a reason for hiding this comment

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

So... We might want this exposed as the standard way to fail from a callback or Promise.catch... so failing with just a message might be useful... Also we're trying to remove non NodeErrors from the code (even if they are just stack containers).
IMHO restore the weird captureStackTrace case, and please add it to the module.exports = {
Ref: #13839 (comment)

Copy link
Contributor

Choose a reason for hiding this comment

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

Nevermind re #14051 (comment), I'll move it to test/common

Copy link
Member

@AndreasMadsen AndreasMadsen Jul 3, 2017

Choose a reason for hiding this comment

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

IMHO restore the weird captureStackTrace case, and please add it to the module.exports = {

I completely disagree, there is no good reason for stack trace to start in fatalError, it should start where the error occurred. And async_hooks should definitely not export fatalError, as fatalError has nothing to do with async_hooks.

Copy link
Contributor

Choose a reason for hiding this comment

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

@AndreasMadsen just beat me by a minute 😄

Copy link
Contributor

@refack refack left a comment

Choose a reason for hiding this comment

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

Only first commit

@refack
Copy link
Contributor

refack commented Jul 3, 2017

as fatalError has nothing to do with async_hooks.

A bit off topic but your comment made me think we might at some point want to create long stacks for our exceptions, probably just for test/, but maybe for all?

@AndreasMadsen
Copy link
Member

What are "long stacks"?

@AndreasMadsen
Copy link
Member

Oops. Hm, I don't have a strong opinion. As you've worked far more with async_hooks, I'd say it's your call.

I have now confirmed that the issue in #14051 has nothing to do with my changes. So I think I prefer my approach in #14051. This is how it is done in the async_hooks C++, I think it makes sense to use the same approach in async_hooks JS.

@refack
Copy link
Contributor

refack commented Jul 3, 2017

What are "long stacks"?

async-context stacktraces. But that discussion should be in a separate thread.

@BridgeAR BridgeAR changed the title async_hooks: simplify fatalError and move statement async_hooks: move statement Jul 3, 2017
@AndreasMadsen
Copy link
Member

AndreasMadsen commented Jul 3, 2017

async_hooks: move restoreTmpHooks call to init

It should be possible to provoke an error in the previous implementation, by:

  1. listen to init
  2. start an native handle, fs.open() or similar should do.
  3. create a new init listener in the current init listener. This should not emit by fs.open() is step (not a bug).
  4. start an native handle, fs.open() or similar should do.
  5. If I'm right, the second init hook (3.) will not fire (bug). This fix should cause the second init hook to be fired.

[refack: fixed typo]

@trevnorris
Copy link
Contributor

@AndreasMadsen Here's a test for what I believe you're talking about:

'use strict';

const print = process._rawDebug;
const init_calls = { hook1: 0, hook2: 0 };
let hook2_enabled = true;

process.on('exit', () => {
  require('assert').strictEqual(init_calls.hook1, init_calls.hook2);
});

const hook1 = require('async_hooks').createHook({
  init(id, type) {
    init_calls.hook1++;
    print('hook1 init:', id, type);
    if (hook2_enabled) {
      hook2_enabled = false;
      hook2.enable();
      require('crypto').randomBytes(1024, () => {});
    }
  }
});

const hook2 = require('async_hooks').createHook({
  init(id, type) {
    init_calls.hook2++;
    print('hook2 init:', id, type);
  },
});

setImmediate(() => {
  hook1.enable();
  hook2.enable();
  require('fs').open('/dev/null', 'r', (er, fd) => {});
});

This shows that init still runs for both hook1 and hook2 for both events. This test passes for v8.1.3, but fails with the following when applying this patch:

assert.js:60
  throw new errors.AssertionError({
  ^

AssertionError [ERR_ASSERTION]: 2 === 1
    at process.on (/tmp/async-fs-init.js:8:21)
    at emitOne (events.js:115:13)
    at process.emit (events.js:210:7)

@AndreasMadsen
Copy link
Member

@trevnorris there is a comment in the code that says:

// The set of callbacks for a hook should be the same regardless of whether
// enable()/disable() are run during their execution.

If I understand that comment correctly, it says that hook2.enable(); should be delayed to after the current init hook(s) are done executing. Thus, init_calls.hook1 !== init_calls.hook2 is the expected behavior.

@trevnorris
Copy link
Contributor

@AndreasMadsen Here's a more generalized test for discussion:

'use strict';

const { createHook } = require('async_hooks');
const { randomBytes } = require('crypto');
const print = process._rawDebug;
const hooks_list = [];
const start_enabled = process.argv[2] === '--start-enabled';
let cntr = -1;

for (let i = 0; i < 2; i++) {
  hooks_list.push(createHook({
    init(id, type) { print(`hook${i} init`); },
  }));
  if (start_enabled) hooks_list[hooks_list.length - 1].enable();
}

const hook = createHook({
  init(id, type) {
    print('hook init:', id, type);
    if (++cntr < hooks_list.length) {
      if (start_enabled)
        hooks_list[cntr].disable();
      else
        hooks_list[cntr].enable();
      randomBytes(1, () => {});
    }
  }
}).enable();

randomBytes(1, () => {});

First running without --start-enabled:

v8.1.3                            pr-1405
------                            -------
hook init: 2 RANDOMBYTESREQUEST   hook init: 2 RANDOMBYTESREQUEST
hook init: 3 RANDOMBYTESREQUEST   hook init: 3 RANDOMBYTESREQUEST
hook init: 4 RANDOMBYTESREQUEST   hook init: 4 RANDOMBYTESREQUEST
                                  hook0 init
                                  hook1 init
                                  hook0 init
                                  hook1 init

Now running with --start-enabled:

v8.1.3                            pr-1405
------                            -------
hook0 init                        hook0 init
hook1 init                        hook1 init
hook init: 2 RANDOMBYTESREQUEST   hook init: 2 RANDOMBYTESREQUEST
hook0 init                        hook0 init
hook1 init                        hook1 init
hook init: 3 RANDOMBYTESREQUEST   hook init: 3 RANDOMBYTESREQUEST
hook0 init                        hook0 init
hook1 init                        hook1 init
hook init: 4 RANDOMBYTESREQUEST   hook init: 4 RANDOMBYTESREQUEST

First thing to address is that I wouldn't expect the output for running with --start-enabled to be the same between v8.1.3 and this patch. Similar to how it's different with running without --start-enabled. Instead I would have expected the output of this patch with --start-enabled to be something like:

hook0 init
hook1 init
hook init: 2 RANDOMBYTESREQUEST
hook1 init
hook init: 3 RANDOMBYTESREQUEST
hook init: 4 RANDOMBYTESREQUEST

As we can see from the first results table there's a strange timing issue. Namely I would have expected the output for running without --start-enabled to look like:

hook init: 2 RANDOMBYTESREQUEST
hook init: 3 RANDOMBYTESREQUEST
hook0 init
hook init: 4 RANDOMBYTESREQUEST
hook0 init
hook1 init

For simplicity I would interpret that comment to mean the set of callbacks from the entry point of init(), and recursive calls to init() will always call the same set of hook callbacks as its parent. Honestly the idea of recursive init() calls didn't cross my mind when writing that comment, but IMO having each recursive call to init() always call the same set of callbacks that were enabled/disabled at the time of the calling of its parent is the easiest to code and to reason about.

@AndreasMadsen
Copy link
Member

AndreasMadsen commented Jul 9, 2017

First of all: I really struggle to understand what you are arguing for. Are you against this PR? If this is the case, try logging when storeActiveHooks and restoreTmpHooks are called, then the bug is quite obvious.

without --start-enabled:

hook init: 2 RANDOMBYTESREQUEST
storeActiveHooks
hook init: 3 RANDOMBYTESREQUEST
hook init: 4 RANDOMBYTESREQUEST

You will immediately see that when init is called from native, restoreTmpHooks is never called. This means that we are stuck in the temp state, that is a bug.


Assuming you don't think this PR is wrong but something is odd about the output:

In the case without --start-enabled we see hook0 init and hook1 init, this is odd. As no hooks should be enabled during the execution of hook (only after all initial init hooks should they be enabled).

The reason for this is a bug, is the use of a single flag for processing_hook. A single flag is not enough when dealing with nested init calls. Let's consider the simpler case where hooks_list.length === 1.

enable
hook init: 2 RANDOMBYTESREQUEST
enable
storeActiveHooks
hook init: 3 RANDOMBYTESREQUEST
restoreTmpHooks
hook0 init

On the surface, this might look fine, but what actually happens is that restoreTmpHooks is called from the inner emitInitN hooks (caused by randomBytes in init). This means in the loop for the outer emitInitN hooks (caused by the first randomBytes) the temp_arrays now becomes the active_hooks_array. Since active_hooks_array.length is accessed dynamically, the for-loop now sees a longer active_hooks_array and continues to execute the hooks.

To put it differently, active_hooks_array.length should never change but it does because restoreTmpHooks is called too early.

The solution as I see it, is to only let the outer emitInitN call restoreTmpHooks. This can be done by using a counter instead of a flag. With such a fix, the results are now:

without --start-enabled.

hook init: 2 RANDOMBYTESREQUEST
hook init: 3 RANDOMBYTESREQUEST
hook init: 4 RANDOMBYTESREQUEST

with --start-enabled.

hook0 init
hook1 init
hook init: 2 RANDOMBYTESREQUEST
hook0 init
hook1 init
hook init: 3 RANDOMBYTESREQUEST
hook0 init
hook1 init
hook init: 4 RANDOMBYTESREQUEST

I don't know why you would expect the number of called hooks to change in the "with --start-enabled" case. This behavior seems correct to me since the hooks array should never change during processing.

I will create a pull request that depends on this PR and contains the additional fix I'm suggesting.

@AndreasMadsen
Copy link
Member

PR #14143 contains the suggested fix.

@AndreasMadsen
Copy link
Member

AndreasMadsen commented Jul 9, 2017

@BridgeAR Having spend some time debugging @trevnorris's issue I think this test better highlights the issue this PR solves:

'use strict';

const common = require('../common');
const assert = require('assert');
const async_hooks = require('async_hooks');
const crypto = require('crypto');

const nestedHook = async_hooks.createHook({
  init: common.mustCall()
});

const rootHook = async_hooks.createHook({
  init: common.mustCall(function (id, type) {
    nestedHook.enable();
  }, 2)
}).enable();

crypto.randomBytes(1, common.mustCall(function () {
  crypto.randomBytes(1, common.mustCall());
}));

@BridgeAR
Copy link
Member Author

BridgeAR commented Jul 9, 2017

@AndreasMadsen thanks a lot for the test. As you opened the follow up PR and did pretty much all work here, I could also close this one and you add the test to your PR?

@AndreasMadsen
Copy link
Member

Thanks a lot for the test. As you opened the follow up PR and did pretty much all work here, I could also close this one and you add the test to your PR?

You have my permission to just copy and paste the test case I wrote. If @trevnorris thinks we should land this and #14143 together for it to make sense, then I will just rebase #14143 again.

This fixes an error that could occure by nesting async_hooks calls
@BridgeAR
Copy link
Member Author

Rebased (I changed the commit message a bit) and incorporated.

@AndreasMadsen
Copy link
Member


const common = require('../common');
const async_hooks = require('async_hooks');
const crypto = require('crypto');
Copy link
Contributor

Choose a reason for hiding this comment

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

if you're using crypto you need:

if (!common.hasCrypto)
  common.skip('missing crypto');

AFAICT this should work with fs or stream...

Copy link
Member

@AndreasMadsen AndreasMadsen Jul 10, 2017

Choose a reason for hiding this comment

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

Good point, fs should work (for example fs.access(__filename, () => {})).

@refack refack self-assigned this Jul 11, 2017
Copy link
Member

@AndreasMadsen AndreasMadsen 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
Contributor

@trevnorris trevnorris left a comment

Choose a reason for hiding this comment

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

@AndreasMadsen Thanks for the additional fix. I'm cool to land this along side your other fix.

@AndreasMadsen
Copy link
Member

landed in 8a83035

@addaleax addaleax mentioned this pull request Jul 18, 2017
addaleax pushed a commit that referenced this pull request Jul 18, 2017
This fixes an error that could occure by nesting async_hooks calls

PR-URL: #14054
Ref: #13755 (comment)
Reviewed-By: Refael Ackermann <[email protected]>
Reviewed-By: Trevor Norris <[email protected]>
Reviewed-By: James M Snell <[email protected]>
Reviewed-By: Andreas Madsen <[email protected]>
addaleax pushed a commit that referenced this pull request Jul 18, 2017
In some cases restoreTmpHooks is called too early, this causes
active_hooks_array to change during execution of the init hooks.

PR-URL: #14143
Ref: #14054 (comment)
Reviewed-By: Refael Ackermann <[email protected]>
Reviewed-By: Trevor Norris <[email protected]>
Reviewed-By: Anna Henningsen <[email protected]>
Fishrock123 pushed a commit that referenced this pull request Jul 19, 2017
This fixes an error that could occure by nesting async_hooks calls

PR-URL: #14054
Ref: #13755 (comment)
Reviewed-By: Refael Ackermann <[email protected]>
Reviewed-By: Trevor Norris <[email protected]>
Reviewed-By: James M Snell <[email protected]>
Reviewed-By: Andreas Madsen <[email protected]>
Fishrock123 pushed a commit that referenced this pull request Jul 19, 2017
In some cases restoreTmpHooks is called too early, this causes
active_hooks_array to change during execution of the init hooks.

PR-URL: #14143
Ref: #14054 (comment)
Reviewed-By: Refael Ackermann <[email protected]>
Reviewed-By: Trevor Norris <[email protected]>
Reviewed-By: Anna Henningsen <[email protected]>
@refack refack removed their assignment Oct 20, 2018
@BridgeAR BridgeAR deleted the async_hooks-stuff branch April 1, 2019 23:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
async_hooks Issues and PRs related to the async hooks subsystem.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants