2

Sometimes code would like to know if a particular function (or children) are running or not. For instance, node.js has domains which works for async stuff as well (not sure if this includes async functions).

Some simple code to explain what I need would by like this:

inUpdate = true;
try {
  doUpdate();
} finally {
  inUpdate = false;
}

This could then be used something like:

function modifyThings() {
  if (inUpdate) throw new Error("Can't modify while updating");
}

With the advent of async this code breaks if the doUpdate() function is asynchronous. This was of course already true using callback-style functions.

The doUpdate function could of course be patched to maintain the variable around every await, but even if you have control over the code, this is cumbersome and error prone and this breaks when trying to track async function calls inside doUpdate.

I tried monkey-patching Promise.prototype:

const origThen = Promise.prototype.then;
Promise.prototype.then = function(resolve, reject) {
  const isInUpdate = inUpdate;
  origThen.call(this, function myResolve(value) {
    inUpdate = isInUpdate;
    try {
      return resolve(value);
    } finally {
      inUpdate = false;
    }
  }, reject);
}

Unfortunately this doesn't work. I'm not sure why, but the async continuation code ends up running outside of the resolve call stack (probably using a microtask).

Note that it's not enough to simply do:

function runUpdate(doUpdate) {
  inUpdate = true;
  doUpdate.then(() => inUpdate = false).catch(() => inUpdate = false);
}

The reason is:

runUpdate(longAsyncFunction);
console.log(inUpdate); // incorrectly returns true

Is there any way to track something from outside an async function so it's possible to tell if the function called, or any of its descendant calls are running?

I know that it's possible to simulate async functions with generators and yield, in which case we have control over the call stack (since we can call gen.next()) but this is a kludge which the advent of async functions just got around to solving, so I'm specifically looking for a solution that works with native (not Babel-generated) async functions.

Edit: To clarify the question: Is there's a way for outside code to know if a particular invocation of an async function is running or if it is suspended, assuming that this code is the caller of the async function. Whether it's running or not would be determined by a function that ultimately is called by the async function (somewhere in the stack).

Edit: To clarify some more: The intended functionality would be the same as domains in node.js, but also for the browser. Domains already work with Promises, so async functions probably work as well (not tested).

DDS
  • 4,325
  • 22
  • 33
  • Your question still doesn't make any sense. Again, are you asking for a way to tell if an async invocation is queued? – Jared Smith Mar 12 '18 at 18:05

3 Answers3

2

This code allows me to do what I want to a certain extent:

function installAsyncTrack() {
  /* global Promise: true */
  if (Promise.isAsyncTracker) throw new Error('Only one tracker can be installed');

  const RootPromise = Promise.isAsyncTracker ? Promise.rootPromise : Promise;
  let active = true;

  const tracker = {
    track(f, o, ...args) {
      const prevObj = tracker.trackObj;
      tracker.trackObj = o;
      try {
        return f.apply(this, args);
      } finally {
        tracker.trackObj = prevObj;
      }
    },
    trackObj: undefined,
    uninstall() {
      active = false;
      if (Promise === AsyncTrackPromise.prevPromise) return;
      if (Promise !== AsyncTrackPromise) return;
      Promise = AsyncTrackPromise.prevPromise;
    }
  };

  AsyncTrackPromise.prototype = Object.create(Promise);
  AsyncTrackPromise.rootPromise = RootPromise;
  AsyncTrackPromise.prevPromise = Promise;
  Promise = AsyncTrackPromise;
  AsyncTrackPromise.resolve = value => {
    return new AsyncTrackPromise(resolve => resolve(value));
  };
  AsyncTrackPromise.reject = val => {
    return new AsyncTrackPromise((resolve, reject) => reject(value));
  };
  AsyncTrackPromise.all = iterable => {
    const promises = Array.from(iterable);
    if (!promises.length) return AsyncTrackPromise.resolve();
    return new AsyncTrackPromise((resolve, reject) => {
      let rejected = false;
      let results = new Array(promises.length);
      let done = 0;
      const allPromises = promises.map(promise => {
        if (promise && typeof promise.then === 'function') {
          return promise;
        }
        return new AsyncTrackPromise.resolve(promise);
      });
      allPromises.forEach((promise, ix) => {
        promise.then(value => {
          if (rejected) return;
          results[ix] = value;
          done++;
          if (done === results.length) {
            resolve(results);
          }
        }, reason => {
          if (rejected) return;
          rejected = true;
          reject(reason);
        });
      });
    });
  };
  AsyncTrackPromise.race = iterable => {
    const promises = Array.from(iterable);
    if (!promises.length) return new AsyncTrackPromise(() => {});
    return new AsyncTrackPromise((resolve, reject) => {
      let resolved = false;
      if (promises.some(promise => {
          if (!promise || typeof promise.then !== 'function') {
            resolve(promise);
            return true;
          }
        })) return;
      promises.forEach((promise, ix) => {
        promise.then(value => {
          if (resolved) return;
          resolved = true;
          resolve(value);
        }, reason => {
          if (resolved) return;
          resolved = true;
          reject(reason);
        });
      });
    });
  };

  function AsyncTrackPromise(handler) {
    const promise = new RootPromise(handler);
    promise.trackObj = tracker.trackObj;

    promise.origThen = promise.then;
    promise.then = thenOverride;

    promise.origCatch = promise.catch;
    promise.catch = catchOverride;

    if (promise.finally) {
      promise.origFinally = promise.finally;
      promise.finally = finallyOverride;
    }
    return promise;
  }

  AsyncTrackPromise.isAsyncTracker = true;

  function thenOverride(resolve, reject) {
    const trackObj = this.trackObj;
    if (!active || trackObj === undefined) return this.origThen.apply(this, arguments);
    return this.origThen.call(
      this,
      myResolver(trackObj, resolve),
      reject && myResolver(trackObj, reject)
    );
  }

  function catchOverride(reject) {
    const trackObj = this.trackObj;
    if (!active || trackObj === undefined) return this.origCatch.catch.apply(this, arguments);
    return this.origCatch.call(
      this,
      myResolver(trackObj, reject)
    );
  }

  function finallyOverride(callback) {
    const trackObj = this.trackObj;
    if (!active || trackObj === undefined) return this.origCatch.catch.apply(this, arguments);
    return this.origCatch.call(
      this,
      myResolver(trackObj, reject)
    );
  }

  return tracker;

  function myResolver(trackObj, resolve) {
    return function myResolve(val) {
      if (trackObj === undefined) {
        return resolve(val);
      }
      RootPromise.resolve().then(() => {
        const prevObj = tracker.trackObj;
        tracker.trackObj = trackObj;
        RootPromise.resolve().then(() => {
          tracker.trackObj = prevObj;
        });
      });
      const prevObj = tracker.trackObj;
      tracker.trackObj = trackObj;
      try {
        return resolve(val);
      } finally {
        tracker.trackObj = prevObj;
      }
    };
  }

}

tracker = installAsyncTrack();

function track(func, value, ...args) {
  return tracker.track(func, { value }, value, ...args);
}

function show(where, which) {
  console.log('At call', where, 'from', which, 'the value is: ', tracker.trackObj && tracker.trackObj.value);
}

async function test(which, sub) {
  show(1, which);
  await delay(Math.random() * 100);
  show(2, which);
  if (sub === 'resolve') {
    await Promise.resolve(test('sub'));
    show(3, which);
  }
  if (sub === 'call') {
    await test(which + ' sub');
    show(3, which);
  }
}

function delay(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

track(test, 'test1');
track(test, 'test2');
track(test, 'test3', 'resolve');
track(test, 'test4', 'call');

It replaces the native Promise with my own. This promise stores the current context (taskObj) on the promise.

When the .then callback or its ilk are called, it does the following:

  • It creates a new native promise that immediately resolves. This adds a new microtask to the queue (according to spec, so should be reliable).

  • It calls the original resolve or reject. At least in Chrome and Firefox, this generates another microtask onto the queue that will run next part of the async function. Not sure what the spec has to say about this yet. It also restores the context around the call so that if it's not await that uses it, no microtask gets added here.

  • The first microtask gets executed, which is my first (native) promise being resolved. This code restores the current context (taskObj). It also creates a new resolved promise that queues another microtask

  • The second microtask (if any) gets executed, running the JS in the async function to until it hits the next await or returns.

  • The microtask queued by the first microtask gets executed, which restores the context to what it was before the Promise resolved/rejected (should always be undefined, unless set outside a tracker.track(...) call).

If the intercepted promise is not native (e.g. bluebird), it still works because it restores the state during the resolve(...) (and ilk) call.

There's one situation which I can't seem to find a solution for:

tracker.track(async () => {
  console.log(tracker.taskObj); // 'test'
  await (async () => {})(); //This breaks because the promise generated is native
  console.log(tracker.taskObj); // undefined
}, 'test')

A workaround is to wrap the promise in Promise.resolve():

tracker.track(async () => {
  console.log(tracker.taskObj); // 'test'
  await Promise.resolve((async () => {})());
  console.log(tracker.taskObj); // undefined
}, 'test')

Obviously, a lot of testing for all the different environments is needed and the fact that a workaround for sub-calls is needed is painful. Also, all Promises used need to either be wrapped in Promise.resolve() or use the global Promise.

DDS
  • 4,325
  • 22
  • 33
  • I know this is a really old comment, but what should the above output look like for the sub-calls? They appear to be losing context for me before they are getting called? – Carson Farmer Apr 05 '21 at 21:39
  • 1
    I have given up on this. Note the "to a certain extent" disclaimer. I'm looking for a compiler solution now, that will maintain context by explicitly managing the context around awaits. Note that there is chatter of stack context as a language feature. I don't have a link right now though. It's also not ready to use. – DDS Apr 06 '21 at 15:08
0

[is it] possible to tell if the function called, or any of its descendant calls are running?

Yes. The answer is always no. Cause there is only one piece of code running at a time. Javascript is single threaded per definition.

Jonas Wilms
  • 132,000
  • 20
  • 149
  • 151
  • This doesn't answer the question and I'm not sure why you're mentioning that JS is single threaded as it doesn't seem to be relevant here. – DDS Mar 12 '18 at 17:56
  • 1
    @DDS it's quite relevant, JavaScript has run-to-completion semantics. You don't want to know if those functions are *running*, you seem to want to know if they're *queued*. – Jared Smith Mar 12 '18 at 17:56
  • But `async` functions don't run always completion before dropping back to the event loop, they are suspended. The question is if there's a way for outside code to know if a particular invocation of an async function is running or if it is suspended. – DDS Mar 12 '18 at 17:59
  • 1
    @DDS not true, they always run until `yield`, `return`, or the end of the block. Always. Avoiding the exact scenario you're talking about is why they refused to add coroutines. – Jared Smith Mar 12 '18 at 18:00
  • Indeed. I meant completion of the function. – DDS Mar 12 '18 at 18:02
  • @DDS it's not really clear at all what you're asking here. Are you asking for a way to tell if a generator is exhausted? Because that's the only scenario I can think of where code doesn't run 'to the end of the function'. – Jared Smith Mar 12 '18 at 18:03
  • 1
    @DDS then why `// incorrectly returns true` ? the promise is a pending state there – Jonas Wilms Mar 12 '18 at 18:03
  • It's incorrect because `doUpdate` isn't running, it's suspended. The `inUpdate` variable is meant to indicate whether we're inside the `doUpdate` function. If `doUpdate` calls some function, this function should be able to determine if `doUpdate` called it, or if some other function called it by checking `inUpdate`. – DDS Mar 12 '18 at 18:06
  • 1
    As we've been trying to tell you, there's no such thing as suspension of execution in JavaScript. Not even in a generator, although that's the closest thing. Certainly not in an async function. I'm voting to close this. – Jared Smith Mar 12 '18 at 18:11
  • I'm fairly sure that exactly as in a generator waiting for a `next()` call, an async function is suspended until an awaited promise is resolved. The local variables are still valid, for instance. The function isn't stopped, but it's interrupted nonetheless. I understand that the JS simply drops back to the event loop when an `await` is encountered. But the problem I'm trying to solve is the flag only being set when it's examined from within the call stack of the `doUpdate` function and not from another stack, i.e. `new Error().stack` would contain the `doUpdate` function. – DDS Mar 12 '18 at 19:01
0

Don't make it any more complicated than it needs to be. If doUpdate returns a promise (like when it is an async function), just wait for that:

inUpdate = true;
try {
  await doUpdate();
//^^^^^
} finally {
  inUpdate = false;
}

You can also use the finally Promise method:

var inUpdate = true;
doUpdate().finally(() => {
  inUpdate = false;
});

That'll do just like like your synchronous code, having inUpdate == true while the function call or any of its descendants are running. Of course that only works if the asynchronous function doesn't settle the promise before it is finished doing its thing. And if you feel like the inUpdate flag should only be set during some specific parts of the doUpdate function, then yes the function will need to maintain the flag itself - just like it is the case with synchronous code.

Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • This doesn't work because the `inUpdate` flag will be true also when called from outside the function, until the `doUpdate` function returns/resolves. – DDS Mar 12 '18 at 18:54
  • @DDS What do you mean by "from outside", isn't "*until the `doUpdate` function returns*" exactly what you want? What is your [actual, practical problem](https://meta.stackexchange.com/q/66377) that you need this for? – Bergi Mar 12 '18 at 18:56
  • I currently need it to track changes made by a function as opposed to changes made by some other function, without cooperation of the function. So, if a function calls `makeChange(newData)` I need to attribute it to the correct caller. – DDS Mar 12 '18 at 18:58
  • @DDS That's the callstacks are made for - track who called you. Just use `new Error().stack`, it should work even with `async function`s. There's no better API for callstacks [yet](https://esdiscuss.org/topic/standardizing-error-stack-or-equivalent) unfortunately. – Bergi Mar 12 '18 at 19:01
  • There's no way to know which line matches the function because I don't know ahead of time which function will be passed in. And even if I did, I don't know which invocation instance it is, which is important for tracking changes. – DDS Mar 12 '18 at 19:03
  • @DDS Your question really sounded like you were looking for a semaphore, to place around a function that *you* call, not for something in your function that gets called from a possibly async function. You might want to edit it. – Bergi Mar 12 '18 at 19:03
  • It is, but in particular, the `doUpdate` function is a function passed in and not part of the tracking code I'm trying to build. – DDS Mar 12 '18 at 19:05
  • @DDS It's fundamentally impossible to distinguish call instances from each other without cooperation by the function that is called. Maybe apart from using the debugging protocol of your runtime engine. – Bergi Mar 12 '18 at 19:05
  • In node.js, domains make it work. You can get the current domain and store any data on it, such as an `inUpdate` flag or an object that tracks all modifications made during the call. Apparently it even works with `async` functions. – DDS Mar 12 '18 at 19:06
  • @DDS Well if you can wrap the call that will call your function in your own domain, then yes. Sounds like domains are what you want, so why don't you use them - are you looking for a non-node solution? Maybe you're also interested in [Realms](https://github.com/tc39/proposal-realms). – Bergi Mar 12 '18 at 19:09
  • I'm looking for a general solution that works everywhere. It should work with node.js, native async and Babel async. – DDS Mar 12 '18 at 19:10
  • @DDS I don't think you'll find one, especially for older browsers. Of course, Babel can do anything you want about uncooperative code :-) – Bergi Mar 12 '18 at 21:35