tc39 / proposal-cancelable-promises

Former home of the now-withdrawn cancelable promises proposal for JavaScript
Other
376 stars 29 forks source link

Cancel token usage examples appreciated #16

Closed domenic closed 8 years ago

domenic commented 8 years ago

In particular, justifying cancelIfRequested and requested is not terribly easy. Most of the .NET examples seem to have to do with threading.

See https://github.com/domenic/cancelable-promise/blob/master/Cancel%20Tokens.md#for-the-consumer for what I have so far.

benjamingr commented 8 years ago

requested is commonly used in async functions in .NET, here is some real code we have (some slightly simplified). I posted 3 examples let me know if you need more:


Repeat IEnumerable that stops repeating on cancellation, we have enumerables of functions that we repeat for ongoing tasks, this is a completely synchronous use case:

 private static IEnumerable<T> RepeatEnumerable<T>(IEnumerable<T> toRepeat, CancellationToken token)
        {
            var asList = toRepeat.ToList();
            while (!token.IsCancellationRequested)
            {
                foreach (var item in asList.TakeWhile(item => !token.IsCancellationRequested))
                {
                    yield return item;
                }
            }
        } 
function repeat(source, token) { 
  var asList = [...source];
  while(true) { 
  for(var item of asList) {
    if(token.requested) return;
    yield item;
  }
}

Giving us exit points in code that otherwise doesn't support cancellation, this pattern is quite common in some of our code bases:

public async Task UpdateLiveFeedCache(CancellationToken ct)
{

   foreach (var expertType in expertTypeEnums) {
     if (ct.IsCancellationRequested) continue;
     await LiveFeed.GetLiveFeedOperationsData(from, to, expertType);
   }
}        
async function update(token) {
   for(var value of enumeration) {
     if(token.requested) continue;
     await expensiveUpdateThatDoesntSupportCancellation(value);
   }
}

Cleanup (we'd probably use catch cancel for that in JS):

public async Task UpdateCacheQueue(CancellationToken ct, string message)
    await RegularQueue.ClearQueueAsync(ct);
    EmailGenerator.MailCacheUpdateStartedBackground(Constants.BackendMailingList);
    foreach (var cacheKey in _redisDb.GetCacheKeysIterator().Partition(100)) {
        if (ct.IsCancellationRequested) {
            await RegularQueue.ClearQueueAsync(); // clear items we've added
            await ManagementQueue.Notify(message);
        }
        RegularQueue.Notify(cacheKey.ToString()))
    }
}
async function updateCacheQueue(token) {
  try {
    for await (const key of _redisDb.getCacheKeysIterator()) {
      await queue.notify(key);
      // some additional logic
    }
  } catch cancel { 
     queue.clear(); // update cache interrupted in the middle   
  }
}

domenic commented 8 years ago

Repeat IEnumerable that stops repeating on cancellation, we have enumerables of functions that we repeat for ongoing tasks, this is a completely synchronous use case:

This seems better served by using generator.return().

Giving us exit points in code that otherwise doesn't support cancellation, this pattern is quite common in some of our code bases:

Thanks, this is similar to the crypto example I have. However it's notable that in both my crypto example and in yours, it would probably be better to do these operations in parallel using Promise.all.

In my example I used cancelIfRequested() which I guess uses requested under the covers.

Cleanup (we'd probably use catch cancel for that in JS):

Yeah, this is a good example of why a special catch block is nice :).

I posted 3 examples let me know if you need more:

More would be nice!

benjamingr commented 8 years ago

This seems better served by using generator.return().

Yes, probably - generator .return is essentially cancellation after all. It is exactly that "third state" in some regard. I can come up with examples where I'd want to do cleanup in this case - but I can do that by composing something I'd close with a .return

Thanks, this is similar to the crypto example I have. However it's notable that in both my crypto example and in yours, it would probably be better to do these operations in parallel using Promise.all.

In my example doesn't benefit from doing it in parallel (since it's pushing to the queue) and in fact there is a hard limit on the number of things we can do in parallel with that service- doing more in parallel means more network and more actions at once and would force us to pay more money to Azure for higher tiers.


Here are some more examples:

A "blocking" reader reading. (An await read where read waits until cancellation happens):

public async Task<T> Consume(CancellationToken t = default(CancellationToken), bool shouldDeleteMessage = true)
{
  T ans;
  do
  {
    ans = await TryConsume(t, shouldDeleteMessage);
    if (t.IsCancellationRequested)
    {
      throw new BusConsumeException("Cancellation requested in consume method of NotificationBus");
    }
    if (ans == null)
    {
      await Task.Delay(1000, t);
    }
  } while (ans == null);
  return ans;
}

We need the IsCancellationRequested because TryConsume might not do something cancellable in a subclass.

async consume(token) { 
   do {
     ans = await tryConsume(token);
     if(ans) return ans;
     if(t.requested) throw new Error(":(");
     await delay(1000, t);
   } while(true);
}

Initializing a batched service, .NET web apps have a QueueBackgroundWorkItem hook function which schedules a work item to be executed at some point in the future. It takes a cancellation token which is cancelled when the server shuts down. Here is the init method of a batching component of the notification item.

private void Init()
{
  BackgroundWork.QueueWorkItem(async ct =>
  {
    await NotifyInternal(); // does I/O
    if (ct.IsCancellationRequested) return; // don't refresh again
    await Task.Delay(TimeSpan.FromMinutes(10), ct);
    BackgroundWork.QueueWorkItem(c =>
    { // schedule a refresh of the app, note that this is only recursive because I have to use the platform
      Init();
      return Task.FromResult(false); // this just pleases the type system
    });
  });
}
function init() {
  setImmediateWithCancellation(async ct => {
    await refreshItems();
    if(ct.requested) return; // no need to refresh again
    await delay(1000 * 60);
    setImmediateWithCancellation(init);
  });
}

Another good example is last with cancellation where some items do and some don't support cancellation.

domenic commented 8 years ago

A "blocking" reader reading.

Thanks, this one worked out!

Initializing a batched service

This wasn't as clear to me. It seems like just a recursive version of the iterative consume example?

Another good example is last with cancellation where some items do and some don't support cancellation.

Hmm, how so? Here is how I would envision last in a world with cancelation:

So e.g. https://gist.github.com/domenic/46776bb71a2f885f79013130cd5301aa.

benjamingr commented 8 years ago

This wasn't as clear to me. It seems like just a recursive version of the iterative consume example?

Basically, we have a web-server that needs to update a small in-memory cache from storage every 10 minutes. The code has to run on the platform scheduling to not block actual work the server has to do.

It's background work that I need to cancel and I can't write it in a straightforward way with await. (Optimally, I'd like to be able to configure a scheduler for an async function zone - but that's another story for another idea that's been brewing in my head for a while).

Hmm, how so? Here is how I would envision last in a world with cancelation:

That's how I envision it too assuming operation can be cancelled. What if operation can be cancelled sometimes? In that case you'd have to guard against functions that are not actually cancellable and check if the token's cancellation is requested and "normalize" the output to be like in a truly cancellable function (Or at least, we needed to in our .NET version of it).

benjamingr commented 8 years ago

Also pinging @zenparsing since well, cancellation token use cases.

domenic commented 8 years ago

https://github.com/domenic/cancelable-promise/blob/master/Cancel%20Tokens.md is looking OK, so I'll close this. It's still not entirely clear whether/why we need all the different parts of the API, especially if some of the fun in https://github.com/domenic/cancelable-promise/blob/master/New%20Ideas%20Brainstorming.md makes it in, but I guess it's basically exposing all the primitives, so it's fine.