Azure / azure-functions-durable-js

JavaScript library for using the Durable Functions bindings
https://www.npmjs.com/package/durable-functions
MIT License
129 stars 47 forks source link

context.df.Task.any with createTimer and waitForExternalEvent leaves orchestrator in invalid state #577

Open wanton7 opened 8 months ago

wanton7 commented 8 months ago

Describe the bug

I have this code

df.app.orchestration('queue_orchestrator', {
  handler: function* (context) {
    const response: 'queue_empty' | 'continue' | null = yield context.df.callActivity('pop_queue');

    if (!response) {
      // Activity failed retry in 1 minute
      yield context.df.createTimer(new Date(context.df.currentUtcDateTime.getTime() + 1000 * 60)); // 1 minute
    } else if (response === 'queue_empty') {
      // No more items in queue, wait for new items
      const timeoutTask = context.df.createTimer(
        new Date(context.df.currentUtcDateTime.getTime() + 1000 * 60 * 60 * 5),
      ); // 5 hours
      const hasQueueTask = context.df.waitForExternalEvent('has_queue');
      yield context.df.Task.any([timeoutTask, hasQueueTask]);
    }

    context.df.continueAsNew(undefined);
  },
});
  1. After this code is run under function emulator 4.0.5455 on WIndows 11

    const timeoutTask = context.df.createTimer(
    new Date(context.df.currentUtcDateTime.getTime() + 1000 * 60 * 60 * 5),
    ); // 5 hours
    const hasQueueTask = context.df.waitForExternalEvent('has_queue');
    yield context.df.Task.any([timeoutTask, hasQueueTask]);
  2. this code starts returning old data without calling the activity

    const response: 'queue_empty' | 'continue' | null = yield context.df.callActivity('pop_queue');

If I change code 1. to this everything works

yield context.df.waitForExternalEvent('has_queue');

I think there is a bug in context.df.Task.any that it leaves orchestrator in invalid state if waitForExternalEvent is first task to finish.

Investigative information

Actual behavior When using context.df.Task.any with context.df.createTimer & context.df.waitForExternalEvent and if context.df.waitForExternalEvent task finishes first orchestrator is left in invalid state and starts to receive old values from context.df.callActivity('pop_queue') without even calling the activity meaning code in the activity is never called.