Esempio n. 1
0
        // 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());
        }