// Lists all allowed Task Hubs. The returned HashSet is configured to ignore case. public static async Task <HashSet <string> > GetAllowedTaskHubNamesAsync() { // Respecting DFM_HUB_NAME, if it is set string dfmHubName = Environment.GetEnvironmentVariable(EnvVariableNames.DFM_HUB_NAME); if (!string.IsNullOrEmpty(dfmHubName)) { return(new HashSet <string>(dfmHubName.Split(','), StringComparer.InvariantCultureIgnoreCase)); } // Also respecting host.json setting, when set dfmHubName = TryGetHubNameFromHostJson(); if (!string.IsNullOrEmpty(dfmHubName)) { return(new HashSet <string>(new string[] { dfmHubName }, StringComparer.InvariantCultureIgnoreCase)); } // Otherwise trying to load table names from the Storage try { var tableNames = await TableClient.GetTableClient().ListTableNamesAsync(); var hubNames = new HashSet <string>(tableNames .Where(n => n.EndsWith("Instances")) .Select(n => n.Remove(n.Length - "Instances".Length)), StringComparer.InvariantCultureIgnoreCase); hubNames.IntersectWith(tableNames .Where(n => n.EndsWith("History")) .Select(n => n.Remove(n.Length - "History".Length))); return(hubNames); } catch (Exception) { // Intentionally returning null. Need to skip validation, if for some reason list of tables // cannot be loaded from Storage. But only in that case. return(null); } }
/// <summary> /// Fetches orchestration instance history directly from XXXHistory table /// Tries to mimic this algorithm: https://github.com/Azure/azure-functions-durable-extension/blob/main/src/WebJobs.Extensions.DurableTask/ContextImplementations/DurableClient.cs#L718 /// Intentionally returns IEnumerable<>, because the consuming code not always iterates through all of it. /// </summary> public static IEnumerable <HistoryEvent> GetHistoryDirectlyFromTable(IDurableClient durableClient, string connName, string hubName, string instanceId) { var tableClient = TableClient.GetTableClient(connName); // Need to fetch executionId first var instanceEntity = tableClient.ExecuteAsync($"{hubName}Instances", TableOperation.Retrieve(instanceId, string.Empty)) .Result.Result as DynamicTableEntity; string executionId = instanceEntity.Properties.ContainsKey("ExecutionId") ? instanceEntity.Properties["ExecutionId"].StringValue : null; var instanceIdFilter = TableQuery.CombineFilters ( TableQuery.GenerateFilterCondition("PartitionKey", QueryComparisons.Equal, instanceId), TableOperators.And, TableQuery.GenerateFilterCondition("ExecutionId", QueryComparisons.Equal, executionId) ); // Fetching _all_ correlated events with a separate parallel query. This seems to be the only option. var correlatedEventsQuery = new TableQuery <HistoryEntity>().Where ( TableQuery.CombineFilters ( instanceIdFilter, TableOperators.And, TableQuery.GenerateFilterConditionForInt("TaskScheduledId", QueryComparisons.GreaterThanOrEqual, 0) ) ); var correlatedEventsTask = tableClient.GetAllAsync($"{hubName}History", correlatedEventsQuery) .ContinueWith(t => t.Result.ToDictionary(e => e.TaskScheduledId)); // Memorizing 'ExecutionStarted' event, to further correlate with 'ExecutionCompleted' HistoryEntity executionStartedEvent = null; // Fetching the history var query = new TableQuery <HistoryEntity>().Where(instanceIdFilter); foreach (var evt in tableClient.GetAll($"{hubName}History", query)) { switch (evt.EventType) { case "TaskScheduled": case "SubOrchestrationInstanceCreated": // Trying to match the completion event correlatedEventsTask.Result.TryGetValue(evt.EventId, out var correlatedEvt); if (correlatedEvt != null) { yield return(correlatedEvt.ToHistoryEvent ( evt._Timestamp, evt.Name, correlatedEvt.EventType == "GenericEvent" ? evt.EventType : null, evt.InstanceId )); } else { yield return(evt.ToHistoryEvent()); } break; case "ExecutionStarted": executionStartedEvent = evt; yield return(evt.ToHistoryEvent(null, evt.Name)); break; case "ExecutionCompleted": case "ExecutionFailed": case "ExecutionTerminated": yield return(evt.ToHistoryEvent(executionStartedEvent?._Timestamp)); break; case "ContinueAsNew": case "TimerCreated": case "TimerFired": case "EventRaised": case "EventSent": yield return(evt.ToHistoryEvent()); break; } } }
public static async Task <IActionResult> DfmPostOrchestrationFunction( [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = Globals.ApiRoutePrefix + "/orchestrations('{instanceId}')/{action?}")] HttpRequest req, string instanceId, string action, [DurableClient(TaskHub = Globals.TaskHubRouteParamName)] IDurableClient durableClient, ILogger log) { // Checking that the call is authenticated properly try { await Auth.ValidateIdentityAsync(req.HttpContext.User, req.Headers, durableClient.TaskHubName); } catch (Exception ex) { log.LogError(ex, "Failed to authenticate request"); return(new UnauthorizedResult()); } // Checking that we're not in ReadOnly mode if (DfmEndpoint.Settings.Mode == DfmMode.ReadOnly) { log.LogError("Endpoint is in ReadOnly mode"); return(new StatusCodeResult(403)); } string bodyString = await req.ReadAsStringAsync(); switch (action) { case "purge": await durableClient.PurgeInstanceHistoryAsync(instanceId); break; case "rewind": await durableClient.RewindAsync(instanceId, bodyString); break; case "terminate": await durableClient.TerminateAsync(instanceId, bodyString); break; case "raise-event": dynamic bodyObject = JObject.Parse(bodyString); string eventName = bodyObject.name; JObject eventData = bodyObject.data; var match = ExpandedOrchestrationStatus.EntityIdRegex.Match(instanceId); // if this looks like an Entity if (match.Success) { // then sending signal var entityId = new EntityId(match.Groups[1].Value, match.Groups[2].Value); await durableClient.SignalEntityAsync(entityId, eventName, eventData); } else { // otherwise raising event await durableClient.RaiseEventAsync(instanceId, eventName, eventData); } break; case "set-custom-status": // Updating the table directly, as there is no other known way var table = TableClient.GetTableClient().GetTableReference($"{durableClient.TaskHubName}Instances"); var orcEntity = (await table.ExecuteAsync(TableOperation.Retrieve(instanceId, string.Empty))).Result as DynamicTableEntity; if (string.IsNullOrEmpty(bodyString)) { orcEntity.Properties.Remove("CustomStatus"); } else { // Ensuring that it is at least a valid JSON string customStatus = JObject.Parse(bodyString).ToString(); orcEntity.Properties["CustomStatus"] = new EntityProperty(customStatus); } await table.ExecuteAsync(TableOperation.Replace(orcEntity)); break; case "restart": bool restartWithNewInstanceId = ((dynamic)JObject.Parse(bodyString)).restartWithNewInstanceId; await durableClient.RestartAsync(instanceId, restartWithNewInstanceId); break; default: return(new NotFoundResult()); } return(new OkResult()); }