public MethodInfo Resolve(IServiceDefinition serviceDefinition, RoutineMethodId methodId) { #warning Should use Service interface type to get the interface mapping to resolve the proper method? #warning Must use versioning to redirect to a different method. var serviceType = serviceDefinition.Implementation; if (serviceType == null) { throw new InvalidOperationException($"Impossible to resolve a method implementation on the external service '{serviceDefinition.Name}'."); } var methods = serviceType.GetMethods( BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static | #warning if not found in declared on this type, go to base type #warning also neede to check implemented interfaces if the input type is an interface BindingFlags.DeclaredOnly); var method = methods.SingleOrDefault(mi => mi.Name == methodId.MethodName); if (method == null) { throw new MissingMethodException($"The service '{serviceDefinition.Name}' does not have a method '{methodId.MethodName}'."); } return(method); }
public async void NotifyCompletion( ServiceId serviceId, RoutineMethodId methodId, string intentId, TaskCompletionSource <TaskResult> completionSink, CancellationToken ct) { FabricConnectorAndRoutineInfo fabricConnectorAndRoutineInfo; while (true) { lock (_committedRoutines) { if (_committedRoutines.TryGetValue(intentId, out fabricConnectorAndRoutineInfo)) { _committedRoutines.Remove(intentId); break; } } await Task.Delay(5); } #warning Add infrastructure exception handling for (var i = 0; fabricConnectorAndRoutineInfo.RoutineInfo.Result == null; i++) { #warning Ideally need to handle 'fire an forget' cases - the continuation is never set? //if (!proxyTask.continuation == null after 1 min?) // return; fabricConnectorAndRoutineInfo.RoutineInfo = await fabricConnectorAndRoutineInfo.FabricConnector.PollRoutineResultAsync(fabricConnectorAndRoutineInfo.RoutineInfo, ct); if (fabricConnectorAndRoutineInfo.RoutineInfo.Result != null) { break; } TimeSpan delayInterval; if (fabricConnectorAndRoutineInfo.RoutineInfo is IRoutinePollInterval routinePollInterval) { delayInterval = routinePollInterval.Suggest(i); } else { delayInterval = TimeSpan.FromSeconds(0.5); } await Task.Delay(delayInterval); } lock (_routineResults) _routineResults.Add(intentId, fabricConnectorAndRoutineInfo.RoutineInfo.Result); Task.Run(() => completionSink.SetResult(fabricConnectorAndRoutineInfo.RoutineInfo.Result) ); }
private async Task HandleResultPoll(HttpContext context, string serviceName, string methodName, string intentId) { var rfc7240Preferences = GetPreferences(context.Request.Headers); var serviceId = new ServiceId { ServiceName = serviceName }; var methodId = new RoutineMethodId { MethodName = methodName }; TaskResult taskResult = null; var waitTime = rfc7240Preferences.Wait; if (waitTime > MaxLongPollTime) { waitTime = MaxLongPollTime; } if (waitTime <= TimeSpan.Zero) { taskResult = await _routineCompletionNotifier.TryPollCompletionAsync(serviceId, methodId, intentId, default); } else { var cts = new CancellationTokenSource(); try { var completionSink = new TaskCompletionSource <TaskResult>(); _routineCompletionNotifier.NotifyCompletion(serviceId, methodId, intentId, completionSink, cts.Token); taskResult = await completionSink.Task.WithTimeout(waitTime); } catch (TaskCanceledException) { cts.Cancel(); } } if (taskResult == null) { context.Response.Headers.Add("Date", DateTimeOffset.Now.ToString("u")); context.Response.StatusCode = 304; // Not Modified } else { var contentType = context.Request.GetContentType(); var isDasyncJsonRequest = string.Equals(contentType.MediaType, "application/dasync+json", StringComparison.OrdinalIgnoreCase); await RespondWithRoutineResult(context, taskResult, isDasyncJsonRequest); } }
public Task <TaskResult> TryPollCompletionAsync( ServiceId serviceId, RoutineMethodId methodId, string intentId, CancellationToken ct) { lock (_routineResults) { _routineResults.TryGetValue(intentId, out var taskResult); return(Task.FromResult(taskResult)); } }
public MethodInfo Resolve(Type serviceType, RoutineMethodId methodId) { #warning Should use Service interface type to get the interface mapping to resolve the proper method? #warning Must use versioning to redirect to a different method. var methods = serviceType.GetMethods( BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static | #warning if not found in declared on this type, go to base type #warning also neede to check implemented interfaces if the input type is an interface BindingFlags.DeclaredOnly); var method = methods.SingleOrDefault(mi => mi.Name == methodId.MethodName); if (method == null) { throw new Exception($"method '{methodId.MethodName}' not found on type '{serviceType.FullName}'"); } return(method); }
public void NotifyCompletion( ServiceId serviceId, RoutineMethodId methodId, string intentId, TaskCompletionSource <TaskResult> completionSink, CancellationToken ct) { var taskResult = TryPollCompletionAsync(serviceId, methodId, intentId, default).Result; if (taskResult != null) { completionSink.SetResult(taskResult); return; } lock (_sinks) { if (!_sinks.TryGetValue(intentId, out var set)) { _sinks.Add(intentId, set = new List <TaskCompletionSource <TaskResult> >()); } set.Add(completionSink); } }
public async Task <HttpResponseMessage> ProcessRequestAsync( HttpRequestMessage request, FunctionExecutionContext context, DateTimeOffset requestStartTime, ILogger logger, CancellationToken ct) { _currentFunction.Value = new ExecutingFuncionInfo { FunctionName = context.FunctionName }; try { #warning Create an HTTP connector if (request.Method == HttpMethod.Post) { string serviceName = null; string routineName = null; long? intentId = null; bool @volatile = false; TimeSpan pollTime = TimeSpan.Zero; foreach (var parameter in request.GetQueryNameValuePairs()) { switch (parameter.Key) { case "service": serviceName = parameter.Value; break; case "routine": routineName = parameter.Value; break; case "intent": if (long.TryParse(parameter.Value, out var parsedIntentId)) { intentId = parsedIntentId; } break; } } foreach (var header in request.Headers) { var firstValue = header.Value.FirstOrDefault(); switch (header.Key) { case "Volatile": if (string.IsNullOrEmpty(firstValue) || !bool.TryParse(firstValue, out @volatile)) { @volatile = true; } break; case "Poll-Time": if (!string.IsNullOrEmpty(firstValue)) { if (double.TryParse(firstValue, out var pollTimeInSeconds)) { pollTime = TimeSpan.FromSeconds(pollTimeInSeconds); } else if (TimeSpan.TryParse(firstValue, out var pollTimeTimeSpan)) { pollTime = pollTimeTimeSpan; } } break; } } var serviceId = new ServiceId { ServiceName = serviceName }; var routineMethodId = new RoutineMethodId { MethodName = routineName }; var registration = _serviceRegistry.AllRegistrations .SingleOrDefault(r => r.ServiceName == serviceId.ServiceName); if (registration == null || registration.IsExternal) { return new HttpResponseMessage(HttpStatusCode.BadRequest) { Content = new StringContent( registration == null ? "Service not found" : "Cannot invoke external service") } } ; IValueContainer parameterContainer = null; if (request.Content != null) { var routineMethod = _routineMethodResolver.Resolve(registration.ServiceType, routineMethodId); var methodInvoker = _methodInvokerFactory.Create(routineMethod); parameterContainer = methodInvoker.CreateParametersContainer(); #warning change to use a serializer based on the format var paramsJson = await request.Content.ReadAsStringAsync(); if (!string.IsNullOrWhiteSpace(paramsJson)) { JsonConvert.PopulateObject(paramsJson, parameterContainer); } } var intent = new ExecuteRoutineIntent { #warning Generate intent ID from function request ID? any benefit (like on re-try)? Id = intentId ?? _idGenerator.NewId(), ServiceId = serviceId, MethodId = routineMethodId, Parameters = parameterContainer, Continuation = null }; var connector = GetConnector(serviceId); var routineInfo = await connector.ScheduleRoutineAsync(intent, ct); if (pollTime > TimeSpan.Zero) { routineInfo = await PollRoutineAsync(routineInfo, connector, requestStartTime, pollTime, ct); } if (routineInfo.Result != null) { return(CreateResponseFromRoutineResult(routineInfo.Result)); } var response = new HttpResponseMessage(HttpStatusCode.Accepted); var location = $"{request.RequestUri.AbsolutePath}?service={serviceId.ServiceName}&routineId={routineInfo.RoutineId}"; response.Headers.Add("Location", location); #warning ETag must be in certain format //if (!string.IsNullOrEmpty(routineInfo.ETag)) // response.Headers.ETag = new EntityTagHeaderValue(routineInfo.ETag); return(response); } else if (request.Method == HttpMethod.Get) { string serviceName = null; string routineId = null; string routineETag = null; TimeSpan pollTime = TimeSpan.Zero; foreach (var parameter in request.GetQueryNameValuePairs()) { switch (parameter.Key) { case "service": serviceName = parameter.Value; break; case "routineId": routineId = parameter.Value; break; case "routineTag": routineETag = parameter.Value; break; } } foreach (var header in request.Headers) { var firstValue = header.Value.FirstOrDefault(); switch (header.Key) { case "Poll-Time": if (!string.IsNullOrEmpty(firstValue)) { if (double.TryParse(firstValue, out var pollTimeInSeconds)) { pollTime = TimeSpan.FromSeconds(pollTimeInSeconds); } else if (TimeSpan.TryParse(firstValue, out var pollTimeTimeSpan)) { pollTime = pollTimeTimeSpan; } } break; } } var serviceId = new ServiceId { ServiceName = serviceName }; var routineInfo = new ActiveRoutineInfo { RoutineId = routineId, ETag = routineETag }; var connector = GetConnector(serviceId); routineInfo = await PollRoutineAsync(routineInfo, connector, requestStartTime, pollTime, ct); if (routineInfo.Result != null) { return(CreateResponseFromRoutineResult(routineInfo.Result)); } var response = new HttpResponseMessage(HttpStatusCode.NoContent); #warning ETag must be in certain format //if (!string.IsNullOrEmpty(routineInfo.ETag)) // response.Headers.ETag = new EntityTagHeaderValue(routineInfo.ETag); return(response); } } catch (Exception ex) { return(CreateInfrastructureErrorResponse(ex)); } finally { _currentFunction.Value = default; } return(new HttpResponseMessage(HttpStatusCode.NotFound)); }
private async Task HandleEventReactionAsync(IServiceDefinition serviceDefinitionFilter, HttpContext context, CancellationToken ct) { if (context.Request.Method != "PUT" && context.Request.Method != "POST") { context.Response.StatusCode = 405; // Method Not Allowed context.Response.ContentType = "text/plain"; await context.Response.Body.WriteAsync(Encoding.UTF8.GetBytes("To invoke a reaction to an event, use one of these HTTP verbs: GET, POST, PUT")); return; } var eventName = (context.Request.Query.TryGetValue("event", out var eventValues) && eventValues.Count == 1) ? eventValues[0] : null; if (string.IsNullOrWhiteSpace(eventName)) { context.Response.StatusCode = 404; context.Response.ContentType = "text/plain"; await context.Response.Body.WriteAsync(Encoding.UTF8.GetBytes("Missing 'event=xxx' in the URL query.")); return; } var serviceName = (context.Request.Query.TryGetValue("service", out var serviceValues) && serviceValues.Count == 1) ? serviceValues[0] : null; if (string.IsNullOrWhiteSpace(serviceName)) { context.Response.StatusCode = 404; context.Response.ContentType = "text/plain"; await context.Response.Body.WriteAsync(Encoding.UTF8.GetBytes("Missing 'service=xxx' in the URL query.")); return; } var eventDesc = new EventDescriptor { EventId = new EventId { EventName = eventName }, ServiceId = new ServiceId { ServiceName = serviceName } }; var eventHandlers = _eventDispatcher.GetEventHandlers(eventDesc); if ((eventHandlers?.Count ?? 0) == 0) { context.Response.StatusCode = DasyncHttpCodes.Succeeded; return; } var contentType = context.Request.GetContentType(); var isJsonRequest = string.Equals(contentType.MediaType, "application/json", StringComparison.OrdinalIgnoreCase); var isDasyncJsonRequest = string.Equals(contentType.MediaType, "application/dasync+json", StringComparison.OrdinalIgnoreCase); if (!isJsonRequest && !isDasyncJsonRequest) { context.Response.StatusCode = 406; // Not Acceptable context.Response.ContentType = "text/plain"; await context.Response.Body.WriteAsync(Encoding.UTF8.GetBytes("The request content type is either not specified or not supported")); return; } string parametersJson = null; if (isJsonRequest) { parametersJson = await context.Request.ReadBodyAsStringAsync(contentType); } else if (isDasyncJsonRequest) { var envelopeJson = await context.Request.ReadBodyAsStringAsync(contentType); var envelope = JsonConvert.DeserializeObject <EventEnvelope>(envelopeJson, JsonSettings); parametersJson = envelope.Parameters; } var results = new List <RaiseEventResult>(); _transitionUserContext.Current = GetUserContext(context); foreach (var subscriber in eventHandlers) { if (serviceDefinitionFilter != null && !string.Equals(subscriber.ServiceId.ServiceName, serviceDefinitionFilter.Name, StringComparison.OrdinalIgnoreCase)) { continue; } var subscriberServiceDefinition = _communicationModelProvider.Model.FindServiceByName(subscriber.ServiceId.ServiceName); #warning Use model for method resolution instead var eventHandlerRoutineMethodId = new RoutineMethodId { MethodName = subscriber.MethodId.MethodName }; MethodInfo routineMethod = _routineMethodResolver.Resolve(subscriberServiceDefinition, eventHandlerRoutineMethodId); var methodInvoker = _methodInvokerFactory.Create(routineMethod); var parameterContainer = methodInvoker.CreateParametersContainer(); if (!string.IsNullOrWhiteSpace(parametersJson)) { if (isDasyncJsonRequest) { _dasyncJsonSerializer.Populate(parametersJson, parameterContainer); } else { JsonConvert.PopulateObject(parametersJson, parameterContainer, JsonSettings); } } var intentId = _idGenerator.NewId(); _intentPreprocessor.PrepareContext(context); if (await _intentPreprocessor.PreprocessAsync(context, subscriberServiceDefinition, subscriber.MethodId, parameterContainer).ConfigureAwait(false)) { return; } foreach (var postAction in _transitionActions) { await postAction.OnRoutineStartAsync(subscriberServiceDefinition, subscriber.ServiceId, subscriber.MethodId, intentId); } var serviceInstance = _domainServiceProvider.GetService(subscriberServiceDefinition.Implementation); Task task; try { task = methodInvoker.Invoke(serviceInstance, parameterContainer); if (routineMethod.ReturnType != typeof(void)) { try { await task; } catch { } } } catch (Exception ex) { if (ex is TargetInvocationException) { ex = ex.InnerException; } task = Task.FromException(ex); } var taskResult = task?.ToTaskResult() ?? new TaskResult(); foreach (var postAction in _transitionActions) { await postAction.OnRoutineCompleteAsync(subscriberServiceDefinition, subscriber.ServiceId, subscriber.MethodId, intentId, taskResult); } results.Add(new RaiseEventResult { ServiceName = subscriber.ServiceId.ServiceName, MethodName = subscriber.MethodId.MethodName, Result = taskResult }); } context.Response.StatusCode = DasyncHttpCodes.Succeeded; if (isJsonRequest) { context.Response.ContentType = "application/json"; await context.Response.Body.WriteAsync(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(results, JsonSettings))); } else if (isDasyncJsonRequest) { context.Response.ContentType = "application/dasync+json"; _dasyncJsonSerializer.Serialize(context.Response.Body, results); } }
public async Task HandleAsync(PathString basePath, HttpContext context, CancellationToken ct) { var headers = context.Response.Headers; headers.Add(DasyncHttpHeaders.PoweredBy, "D-ASYNC"); var basePathSegmentCount = basePath.Value.Split('/', StringSplitOptions.RemoveEmptyEntries).Length; var pathSegments = context.Request.Path.Value.Split('/', StringSplitOptions.RemoveEmptyEntries).Skip(basePathSegmentCount).ToArray(); if (pathSegments.Length == 0 && context.Request.Query.ContainsKey("react")) { await HandleEventReactionAsync(null, context, ct); return; } if (pathSegments.Length == 0) { context.Response.StatusCode = 404; context.Response.ContentType = "text/plain"; await context.Response.Body.WriteAsync(Encoding.UTF8.GetBytes("Empty request URL")); return; } var serviceName = pathSegments[0]; var serviceDefinition = _communicationModelProvider.Model.FindServiceByName(serviceName); if (serviceDefinition == null) { context.Response.StatusCode = 404; context.Response.ContentType = "text/plain"; await context.Response.Body.WriteAsync(Encoding.UTF8.GetBytes($"Service '{serviceName}' is not registered")); return; } if (serviceDefinition.Type == ServiceType.External) { context.Response.StatusCode = 403; // Forbidden context.Response.ContentType = "text/plain"; await context.Response.Body.WriteAsync(Encoding.UTF8.GetBytes($"Cannot invoke external service '{serviceName}' on your behalf")); return; } if (_eventDispatcher != null && pathSegments.Length == 1 && context.Request.Query.ContainsKey("react")) { await HandleEventReactionAsync(serviceDefinition, context, ct); return; } if (pathSegments.Length == 1) { context.Response.StatusCode = 404; context.Response.ContentType = "text/plain"; await context.Response.Body.WriteAsync(Encoding.UTF8.GetBytes("The request URL does not contain a service method")); return; } if (_eventDispatcher != null && context.Request.Query.ContainsKey("subscribe")) { await HandleEventSubsciptionAsync(serviceDefinition, pathSegments[1], context, ct); return; } var methodName = pathSegments[1]; if (pathSegments.Length == 3) { await HandleResultPoll(context, serviceName, methodName, intentId : pathSegments[2]); return; } if (pathSegments.Length > 3) { context.Response.StatusCode = 404; context.Response.ContentType = "text/plain"; await context.Response.Body.WriteAsync(Encoding.UTF8.GetBytes("The request URL contains extra segments")); return; } var isQueryRequest = context.Request.Method == "GET"; var isCommandRequest = context.Request.Method == "PUT" || context.Request.Method == "POST"; var contentType = context.Request.GetContentType(); var isJsonRequest = string.Equals(contentType.MediaType, "application/json", StringComparison.OrdinalIgnoreCase); var isDasyncJsonRequest = string.Equals(contentType.MediaType, "application/dasync+json", StringComparison.OrdinalIgnoreCase); if (!isQueryRequest && !isJsonRequest && !isDasyncJsonRequest) { context.Response.StatusCode = 406; // Not Acceptable context.Response.ContentType = "text/plain"; await context.Response.Body.WriteAsync(Encoding.UTF8.GetBytes("The request content type is either not specified or not supported")); return; } if (!isQueryRequest && !isCommandRequest) { context.Response.StatusCode = 405; // Method Not Allowed context.Response.ContentType = "text/plain"; await context.Response.Body.WriteAsync(Encoding.UTF8.GetBytes("The service method invocation must use one of these HTTP verbs: GET, POST, PUT")); return; } var routineMethodId = new RoutineMethodId { MethodName = methodName }; #warning Use model for method resolution instead MethodInfo routineMethod; try { routineMethod = _routineMethodResolver.Resolve(serviceDefinition, routineMethodId); } catch { context.Response.StatusCode = 404; context.Response.ContentType = "text/plain"; await context.Response.Body.WriteAsync(Encoding.UTF8.GetBytes($"The service '{serviceDefinition.Name}' does not have method '{methodName}'")); return; } #warning Use model to determine if the method is command or query var isQueryMethod = GetWordAndSynonyms.Contains(GetFirstWord(methodName)); if (isQueryRequest && !isQueryMethod) { context.Response.StatusCode = 404; context.Response.ContentType = "text/plain"; await context.Response.Body.WriteAsync(Encoding.UTF8.GetBytes($"The method '{methodName}' of service '{serviceDefinition.Name}' is a command, but not a query, thus must be invoked with POST or PUT verb")); return; } string parametersJson = null; ContinuationDescriptor continuationDescriptor = null; if (isQueryRequest) { var inputObject = new JObject(); foreach (var kvPair in context.Request.Query) { if (kvPair.Value.Count == 0) { continue; } var parameterName = kvPair.Key; if (kvPair.Value.Count == 1) { var parameterValue = kvPair.Value[0]; inputObject.Add(parameterName, parameterValue); } else { var values = new JArray(); foreach (var value in kvPair.Value) { values.Add(value); } inputObject.Add(parameterName, values); } } parametersJson = inputObject.ToString(); } else if (isJsonRequest) { parametersJson = await context.Request.ReadBodyAsStringAsync(contentType); } else { var inputJson = await context.Request.ReadBodyAsStringAsync(contentType); var envelope = JsonConvert.DeserializeObject <CommandEnvelope>(inputJson, JsonSettings); parametersJson = envelope.Parameters; continuationDescriptor = envelope.Continuation; } var methodInvoker = _methodInvokerFactory.Create(routineMethod); var parameterContainer = methodInvoker.CreateParametersContainer(); if (!string.IsNullOrWhiteSpace(parametersJson)) { if (isDasyncJsonRequest) { _dasyncJsonSerializer.Populate(parametersJson, parameterContainer); } else { JsonConvert.PopulateObject(parametersJson, parameterContainer, JsonSettings); } } var externalRequestId = context.Request.Headers.TryGetValue(DasyncHttpHeaders.RequestId, out var requestIdValues) && requestIdValues.Count > 0 ? requestIdValues[0] : null; var externalCorrelationId = context.Request.Headers.TryGetValue(DasyncHttpHeaders.CorrelationId, out var correlationIdValues) && correlationIdValues.Count > 0 ? correlationIdValues[0] : null; var rfc7240Preferences = GetPreferences(context.Request.Headers); var isHttpRequestBlockingExecution = !(rfc7240Preferences.RespondAsync == true); _transitionUserContext.Current = GetUserContext(context); _intentPreprocessor.PrepareContext(context); if (await _intentPreprocessor.PreprocessAsync(context, serviceDefinition, routineMethodId, parameterContainer).ConfigureAwait(false)) { return; } var serviceId = new ServiceId { ServiceName = serviceDefinition.Name }; var intentId = _idGenerator.NewId(); if (isQueryMethod) { var serviceInstance = _domainServiceProvider.GetService(serviceDefinition.Implementation); foreach (var postAction in _transitionActions) { await postAction.OnRoutineStartAsync(serviceDefinition, serviceId, routineMethodId, intentId); } Task task; try { task = methodInvoker.Invoke(serviceInstance, parameterContainer); if (routineMethod.ReturnType != typeof(void)) { try { await task; } catch { } } } catch (Exception ex) { if (ex is TargetInvocationException) { ex = ex.InnerException; } task = Task.FromException(ex); } var taskResult = task?.ToTaskResult() ?? new TaskResult(); foreach (var postAction in _transitionActions) { await postAction.OnRoutineCompleteAsync(serviceDefinition, serviceId, routineMethodId, intentId, taskResult); } await RespondWithRoutineResult(context, taskResult, isDasyncJsonRequest); } else { var intent = new ExecuteRoutineIntent { Id = intentId, ServiceId = new ServiceId { ServiceName = serviceDefinition.Name }, MethodId = routineMethodId, Parameters = parameterContainer, Continuation = continuationDescriptor }; var actions = new ScheduledActions { ExecuteRoutineIntents = new List <ExecuteRoutineIntent> { intent } }; var options = new TransitionCommitOptions { NotifyOnRoutineCompletion = isHttpRequestBlockingExecution, RequestId = externalRequestId, CorrelationId = externalCorrelationId ?? externalRequestId }; await _transitionCommitter.CommitAsync(actions, null, options, default); var waitTime = rfc7240Preferences.Wait; if (isHttpRequestBlockingExecution || waitTime > MaxLongPollTime) { waitTime = MaxLongPollTime; } if (waitTime > TimeSpan.Zero) { var cts = new CancellationTokenSource(); var completionSink = new TaskCompletionSource <TaskResult>(); _routineCompletionNotifier.NotifyCompletion(intent.ServiceId, intent.MethodId, intent.Id, completionSink, cts.Token); try { var taskResult = await completionSink.Task.WithTimeout(waitTime); await RespondWithRoutineResult(context, taskResult, isDasyncJsonRequest); return; } catch (TaskCanceledException) { cts.Cancel(); } } var location = context.Request.Path.ToString() + "/" + intent.Id; context.Response.Headers.Add("Location", location); context.Response.StatusCode = DasyncHttpCodes.Scheduled; return; } }