/// <summary> /// Handle response for a task including heartbeats /// The returned status property identifies the type of response /// </summary> /// <param name="taskToken"></param> /// <param name="json"></param> public string ProcessTaskResponse(Guid taskToken, string json) { try { var taskResponse = JsonConvert.DeserializeObject <ActivityTaskResponse>(json); // Note that the task may have been deleted by the ActivityTaskTimeoutChecker var task = TaskLists.SingleOrDefault(q => q.TaskToken == taskToken); string response = null; int i; for (i = 0; i < RETRIES; i++) { switch (taskResponse.Status.ToLower()) { case "success": if (task == null) { log.Info($"Ignoring success response for deleted task with token: {taskToken}"); break; } var result = taskResponse.Result; var atce = new ActivityTaskCompletedEvent { Result = result, SchedulingEventId = task.TaskScheduledEventId, }; InsertHistory(this, task.Execution, atce); task.Execution.AwaitingDecision = true; TaskLists.DeleteOnSubmit(task); break; case "failure": if (task == null) { log.Info($"Ignoring failure response for deleted task with token: {taskToken}"); break; } var atfe = new ActivityTaskFailedEvent { Reason = taskResponse.Reason, Details = taskResponse.Details, SchedulingEventId = task.TaskScheduledEventId }; InsertHistory(this, task.Execution, atfe); task.Execution.AwaitingDecision = true; TaskLists.DeleteOnSubmit(task); break; case "cancelled": if (task == null) { log.Info($"Ignoring cancellation response for deleted task with token: {taskToken}"); break; } var atxe = new ActivityTaskCancelledEvent { SchedulingEventId = task.TaskScheduledEventId }; InsertHistory(this, task.Execution, atxe); task.Execution.AwaitingDecision = true; TaskLists.DeleteOnSubmit(task); break; case "heartbeat": if (task != null) { if (taskResponse.Progress.HasValue) { task.Progress = taskResponse.Progress.Value; } if (taskResponse.ProgressMessage != null) { task.ProgressMessage = taskResponse.ProgressMessage; } if (task.HeartbeatTimeout.HasValue) { task.HeartbeatAlarm = DateTime.UtcNow.AddSeconds(task.HeartbeatTimeout.Value); } // Send a progress notification to the updater var data = new JObject( new JProperty("type", "ActivityTaskHeartbeat"), new JProperty("progress", task.Progress ?? -1), new JProperty("message", task.ProgressMessage ?? ""), new JProperty("progressData", JToken.Parse(task.ProgressData))); var qt = CreateUpdaterNotification(task.Execution, data); TaskLists.InsertOnSubmit(qt); SubmitChanges(); } else { log.Info($"Received heartbeat for deleted task with token: {taskToken}, cancelling"); } // If the task has been cancelled or deleted then send a cancellation request response = new JObject { new JProperty("cancellationRequested", task == null || task.Cancelling) }.ToString(Formatting.None); break; case "rescheduled": // Rescheduled tasks are left in the tasklist with an updated scheduling time // effectively pushing them to the back of the queue if (task != null) { task.ScheduledAt = DateTime.UtcNow; task.WorkerId = null; } break; default: log.ErrorFormat("Invalid activity task response status: {0}", taskResponse.Status); break; } // Nothing to update if the task has been deleted if (task == null) { return(response); } try { SubmitChanges(); break; } catch (ChangeConflictException) { Refresh(RefreshMode.KeepCurrentValues, task.Execution); } } if (i == RETRIES) { log.Error($"Failed to process {taskResponse.Status} response from task {taskToken}"); return(null); } return(response); } catch (Exception ex) { log.Error("Failed to process activity task response", ex); return(null); } }
/// <summary> /// Process task completion by scheduling the next task or completing the workflow /// </summary> /// <param name="db"></param> /// <param name="workflow"></param> /// <param name="execution"></param> /// <param name="history"></param> /// <param name="decisions"></param> private static void ProcessActivityTaskCompletedEvent(Database db, WorkflowObj workflow, Execution execution, History history, List <History> decisions) { // There should(!) be no contention for the data modified in this process var evt = ActivityTaskCompletedEvent.Create(history); var se = ActivityTaskScheduledEvent.Create(db.GetHistoryEvent(execution.ExecutionId, evt.SchedulingEventId)); var completedTask = workflow.Tasks.Single(t => t.TaskId == se.TaskId); // Default task outflow var outflow = "Out"; // Update variables if (evt.Result != null) { // Update the variables if there are any normal results (not prefixed with "$") if (evt.Result.Properties().Any(p => !p.Name.StartsWith("$"))) { var variables = db.Variables.Where(v => v.Execution == execution).ToArray(); foreach (var o in completedTask.Outputs.Where(o => o.Value.Var != null)) { // If the activity has not returned a value for an output then we don't update the mapped variable // In general it is probably best if activities return values for all outputs to avoid confusion JToken value; if (evt.Result.TryGetValue(o.Key, out value)) { variables.Single(v => v.Name == o.Value.Var).Json = value.ToString(Formatting.None); } } } // Get the correct outflow JToken outflowToken; if (evt.Result.TryGetValue("$outflow", out outflowToken)) { if (outflowToken.Type == JTokenType.String) { outflow = (string)outflowToken; } else { throw new ApplicationException("Task outflow identifier must be a string"); } } } var nextTaskId = completedTask.Outflows.Single(o => o.Name == outflow).Target; var nextTask = workflow.Tasks.Single(t => t.TaskId == nextTaskId); // A task with no outflows is an end if (nextTask.Outflows.Length == 0) { Console.WriteLine($"Execution state = {execution.ExecutionState.State}"); if (ExState.Create(execution.ExecutionState.State) != ExState.Cleanup) { CreateWorkflowCompletedEvent(execution, decisions); } AttemptCleanup(db, execution, workflow, decisions); } else { CreateTaskScheduledEvent(db, execution, nextTask, decisions); } }