/// <summary> /// Signals the workflow. /// </summary> /// <param name="signalName">The signal name.</param> /// <param name="args">The signal arguments.</param> /// <returns>The tracking <see cref="Task"/>.</returns> /// <exception cref="InvalidOperationException">Thrown if the child workflow has not been started.</exception> /// <remarks> /// <note> /// <b>IMPORTANT:</b> You need to take care to ensure that the parameters passed /// are compatible with the target workflow signal arguments. /// </note> /// </remarks> public async Task SignalAsync(string signalName, params object[] args) { await SyncContext.Clear; Covenant.Requires <ArgumentNullException>(!string.IsNullOrEmpty(signalName), nameof(signalName)); Covenant.Requires <ArgumentNullException>(args != null, nameof(args)); if (Execution == null) { throw new InvalidOperationException("The stub must be started first."); } var reply = await parentWorkflow.ExecuteNonParallel( async() => { return((WorkflowSignalChildReply)await client.CallProxyAsync( new WorkflowSignalChildRequest() { ContextId = parentWorkflow.ContextId, ChildId = childExecution.ChildId, SignalName = signalName, SignalArgs = CadenceHelper.ArgsToBytes(client.DataConverter, args) })); }); reply.ThrowOnError(); }
/// <summary> /// Signals the workflow. /// </summary> /// <param name="signalName">The signal name.</param> /// <param name="args">The signal arguments.</param> /// <returns>The tracking <see cref="Task"/>.</returns> /// <exception cref="InvalidOperationException">Thrown if the child workflow has not been started.</exception> /// <remarks> /// <note> /// <b>IMPORTANT:</b> You need to take care to ensure that the parameters passed /// are compatible with the target workflow arguments. /// </note> /// </remarks> public async Task SignalAsync(string signalName, params object[] args) { await SyncContext.ClearAsync; Covenant.Requires <ArgumentNullException>(!string.IsNullOrEmpty(signalName), nameof(signalName)); Covenant.Requires <ArgumentNullException>(args != null, nameof(args)); if (Execution == null) { throw new InvalidOperationException("The stub must be started first."); } var reply = (WorkflowSignalReply)await client.CallProxyAsync( new WorkflowSignalRequest() { WorkflowId = execution.WorkflowId, RunId = execution.RunId, Domain = options.Domain, SignalName = signalName, SignalArgs = client.DataConverter.ToData(args) }); reply.ThrowOnError(); }
/// <summary> /// Handles workflow invocation. /// </summary> /// <param name="client">The associated cadence client.</param> /// <param name="request">The request message.</param> /// <returns>The reply message.</returns> internal static async Task <WorkflowInvokeReply> OnInvokeAsync(CadenceClient client, WorkflowInvokeRequest request) { Covenant.Requires <ArgumentNullException>(client != null); Covenant.Requires <ArgumentNullException>(request != null); Covenant.Requires <ArgumentException>(request.ReplayStatus != InternalReplayStatus.Unspecified); IWorkflowBase workflow; WorkflowRegistration registration; var contextId = request.ContextId; var workflowKey = new WorkflowInstanceKey(client, contextId); lock (syncLock) { if (request.ReplayStatus != InternalReplayStatus.Unspecified) { return(new WorkflowInvokeReply() { Error = new CadenceError($"[{nameof(WorkflowInvokeRequest)}] did not specify Workflow type name [Type={request.WorkflowType}] is not registered for this worker.") }); } if (idToWorkflow.TryGetValue(workflowKey, out workflow)) { return(new WorkflowInvokeReply() { Error = new CadenceError($"A workflow with [ID={workflowKey}] is already running on this worker.") }); } registration = GetWorkflowRegistration(client, request.WorkflowType); if (registration == null) { return(new WorkflowInvokeReply() { Error = new CadenceError($"Workflow type name [Type={request.WorkflowType}] is not registered for this worker.") }); } } workflow = (IWorkflowBase)Activator.CreateInstance(registration.WorkflowType); workflow.Workflow = new Workflow( parent: (WorkflowBase)workflow, client: client, contextId: contextId, workflowTypeName: request.WorkflowType, domain: request.Domain, taskList: request.TaskList, workflowId: request.WorkflowId, runId: request.RunId, isReplaying: request.ReplayStatus == InternalReplayStatus.Replaying, methodMap: registration.MethodMap); lock (syncLock) { idToWorkflow.Add(workflowKey, workflow); } // Register any workflow signal and/or query handlers with cadence-proxy foreach (var signalName in registration.MethodMap.GetSignalNames()) { var reply = (WorkflowSignalSubscribeReply)await client.CallProxyAsync( new WorkflowSignalSubscribeRequest() { ContextId = contextId, SignalName = signalName }); reply.ThrowOnError(); } foreach (var queryType in registration.MethodMap.GetQueryTypes()) { var reply = (WorkflowSetQueryHandlerReply)await client.CallProxyAsync( new WorkflowSetQueryHandlerRequest() { ContextId = contextId, QueryName = queryType }); reply.ThrowOnError(); } // Start the workflow by calling its workflow entry point method. // This method will indicate that it has completed via one of these // techniques: // // 1. The method returns normally with the workflow result. // // 2. The method calls [RestartAsync(result, args)] which throws an // [InternalWorkflowRestartException] which will be caught and // handled here. // // 3. The method throws another exception which will be caught // and be used to indicate that the workflow failed. try { var workflowMethod = registration.WorkflowMethod; var resultType = workflowMethod.ReturnType; var args = client.DataConverter.FromDataArray(request.Args, registration.WorkflowMethodParameterTypes); var serializedResult = emptyBytes; if (resultType.IsGenericType) { // Method returns: Task<T> var result = await(Task <object>) workflowMethod.Invoke(workflow, args); serializedResult = client.DataConverter.ToData(result); } else { // Method returns: Task await(Task <object>) workflowMethod.Invoke(workflow, args); serializedResult = emptyBytes; } return(new WorkflowInvokeReply() { Result = serializedResult }); } catch (CadenceWorkflowRestartException e) { return(new WorkflowInvokeReply() { ContinueAsNew = true, ContinueAsNewArgs = e.Args, ContinueAsNewDomain = e.Domain, ContinueAsNewTaskList = e.TaskList, ContinueAsNewExecutionStartToCloseTimeout = CadenceHelper.ToCadence(e.ExecutionStartToCloseTimeout), ContinueAsNewScheduleToCloseTimeout = CadenceHelper.ToCadence(e.ScheduleToCloseTimeout), ContinueAsNewScheduleToStartTimeout = CadenceHelper.ToCadence(e.ScheduleToStartTimeout), ContinueAsNewStartToCloseTimeout = CadenceHelper.ToCadence(e.TaskStartToCloseTimeout), }); } catch (CadenceException e) { return(new WorkflowInvokeReply() { Error = e.ToCadenceError() }); } catch (Exception e) { return(new WorkflowInvokeReply() { Error = new CadenceError(e) }); } }
/// <summary> /// Starts the target activity that returns <typeparamref name="TActivityInterface"/>, passing the specified arguments. /// </summary> /// <typeparam name="TResult">The activity result type.</typeparam> /// <param name="args">The arguments to be passed to the activity.</param> /// <returns>The <see cref="IAsyncFuture{T}"/> with the <see cref="IAsyncFuture{T}.GetAsync"/> that can be used to retrieve the workfow result.</returns> /// <exception cref="InvalidOperationException">Thrown when attempting to start a future stub more than once.</exception> /// <remarks> /// <para> /// You must take care to pass parameters that are compatible with the target activity parameters. /// These are checked at runtime but not while compiling. /// </para> /// <note> /// Any given <see cref="ActivityFutureStub{TActivityInterface}"/> may only be executed once. /// </note> /// </remarks> public async Task <IAsyncFuture <TResult> > StartAsync <TResult>(params object[] args) { await SyncContext.Clear; Covenant.Requires <ArgumentNullException>(parentWorkflow != null, nameof(parentWorkflow)); parentWorkflow.SetStackTrace(); if (hasStarted) { throw new InvalidOperationException("Cannot start a future stub more than once."); } var parameters = targetMethod.GetParameters(); if (parameters.Length != args.Length) { throw new ArgumentException($"Invalid number of parameters: [{parameters.Length}] expected but [{args.Length}] were passed.", nameof(parameters)); } hasStarted = true; // Cast the input parameters to the target types so that developers won't need to expicitly // cast things like integers into longs, floats into doubles, etc. for (int i = 0; i < args.Length; i++) { args[i] = CadenceHelper.ConvertArg(parameters[i].ParameterType, args[i]); } // Validate the return type. var resultType = targetMethod.ReturnType; if (resultType == typeof(Task)) { throw new ArgumentException($"Activity method [{nameof(TActivityInterface)}.{targetMethod.Name}()] does not return [void].", nameof(TActivityInterface)); } resultType = resultType.GenericTypeArguments.First(); if (!resultType.IsAssignableFrom(typeof(TResult))) { throw new ArgumentException($"Activity method [{nameof(TActivityInterface)}.{targetMethod.Name}()] returns [{resultType.FullName}] which is not compatible with [{nameof(TResult)}].", nameof(TActivityInterface)); } // Start the activity. var dataConverter = client.DataConverter; var activityId = parentWorkflow.GetNextActivityId(); var reply = await parentWorkflow.ExecuteNonParallel( async() => { return((ActivityStartReply)await client.CallProxyAsync( new ActivityStartRequest() { ContextId = parentWorkflow.ContextId, ActivityId = activityId, Activity = activityTypeName, Args = CadenceHelper.ArgsToBytes(dataConverter, args), Options = options.ToInternal(), Domain = options.Domain })); }); reply.ThrowOnError(); parentWorkflow.UpdateReplay(reply); // Create and return the future. return(new AsyncFuture <TResult>(parentWorkflow, activityId)); }
/// <summary> /// Registers a workflow implementation. /// </summary> /// <param name="client">The associated client.</param> /// <param name="workflowType">The workflow implementation type.</param> /// <param name="workflowTypeName">The name used to identify the implementation.</param> /// <param name="domain">Specifies the target domain.</param> /// <exception cref="InvalidOperationException">Thrown if a different workflow class has already been registered for <paramref name="workflowTypeName"/>.</exception> internal static async Task RegisterAsync(CadenceClient client, Type workflowType, string workflowTypeName, string domain) { Covenant.Requires<ArgumentNullException>(client != null, nameof(client)); Covenant.Requires<ArgumentNullException>(!string.IsNullOrEmpty(domain), nameof(domain)); CadenceHelper.ValidateWorkflowImplementation(workflowType); var methodMap = WorkflowMethodMap.Create(workflowType); // We need to register each workflow method that implements a workflow interface method // with the same signature that that was tagged by [WorkflowMethod]. // // First, we'll create a dictionary that maps method signatures from any inherited // interfaces that are tagged by [WorkflowMethod] to the attribute. var methodSignatureToAttribute = new Dictionary<string, WorkflowMethodAttribute>(); foreach (var interfaceType in workflowType.GetInterfaces()) { foreach (var method in interfaceType.GetMethods(BindingFlags.Public | BindingFlags.Instance)) { var workflowMethodAttribute = method.GetCustomAttribute<WorkflowMethodAttribute>(); if (workflowMethodAttribute == null) { continue; } var signature = method.ToString(); if (methodSignatureToAttribute.ContainsKey(signature)) { throw new NotSupportedException($"Workflow type [{workflowType.FullName}] cannot implement the [{signature}] method from two different interfaces."); } methodSignatureToAttribute.Add(signature, workflowMethodAttribute); } } // Next, we need to register the workflow methods that implement the // workflow interface. foreach (var method in workflowType.GetMethods()) { if (!methodSignatureToAttribute.TryGetValue(method.ToString(), out var workflowMethodAttribute)) { continue; } var workflowTypeKey = GetWorkflowTypeKey(client, workflowTypeName, workflowMethodAttribute); lock (syncLock) { if (nameToRegistration.TryGetValue(workflowTypeName, out var existingRegistration)) { if (!object.ReferenceEquals(existingRegistration.WorkflowType, workflowType)) { throw new InvalidOperationException($"Conflicting workflow interface registration: Workflow interface [{workflowType.FullName}] is already registered for workflow type name [{workflowTypeName}]."); } } else { nameToRegistration[workflowTypeKey] = new WorkflowRegistration() { WorkflowType = workflowType, WorkflowMethod = method, WorkflowMethodParameterTypes = method.GetParameterTypes(), MethodMap = methodMap }; } } var reply = (WorkflowRegisterReply)await client.CallProxyAsync( new WorkflowRegisterRequest() { Name = GetWorkflowTypeNameFromKey(workflowTypeKey), Domain = client.ResolveDomain(domain) }); // $hack(jefflill): // // We're going to ignore any errors here to handle: // // https://github.com/nforgeio/neonKUBE/issues/668 // reply.ThrowOnError(); } }
/// <summary> /// Registers an activity type. /// </summary> /// <param name="client">The associated client.</param> /// <param name="activityType">The activity type.</param> /// <param name="activityTypeName">The name used to identify the implementation.</param> /// <param name="domain">Specifies the target domain.</param> /// <returns><c>true</c> if the activity was already registered.</returns> /// <exception cref="InvalidOperationException">Thrown if a different activity class has already been registered for <paramref name="activityTypeName"/>.</exception> internal async static Task RegisterAsync(CadenceClient client, Type activityType, string activityTypeName, string domain) { Covenant.Requires <ArgumentNullException>(client != null, nameof(client)); Covenant.Requires <ArgumentNullException>(!string.IsNullOrEmpty(domain), nameof(domain)); CadenceHelper.ValidateActivityImplementation(activityType); var constructor = activityType.GetConstructor(Type.EmptyTypes); if (constructor == null) { throw new ArgumentException($"Activity type [{activityType.FullName}] does not have a default constructor.", nameof(activityType)); } // We need to register each activity method that implements an activity interface method // with the same signature that that was tagged by [ActivityMethod]. // // First, we'll create a dictionary that maps method signatures from any inherited // interfaces that are tagged by [ActivityMethod] to the attribute. var methodSignatureToAttribute = new Dictionary <string, ActivityMethodAttribute>(); foreach (var interfaceType in activityType.GetInterfaces()) { foreach (var method in interfaceType.GetMethods(BindingFlags.Public | BindingFlags.Instance)) { var activityMethodAttribute = method.GetCustomAttribute <ActivityMethodAttribute>(); if (activityMethodAttribute == null) { continue; } var signature = method.ToString(); if (methodSignatureToAttribute.ContainsKey(signature)) { throw new NotSupportedException($"Activity type [{activityType.FullName}] cannot implement the [{signature}] method from two different interfaces."); } methodSignatureToAttribute.Add(signature, activityMethodAttribute); } } // Next, we need to register the activity methods that implement the // activity interface. foreach (var method in activityType.GetMethods()) { if (!methodSignatureToAttribute.TryGetValue(method.ToString(), out var activityMethodAttribute)) { continue; } var activityTypeKey = GetActivityTypeKey(client, activityTypeName, activityMethodAttribute); lock (syncLock) { if (nameToRegistration.TryGetValue(activityTypeKey, out var registration)) { if (!object.ReferenceEquals(registration.ActivityType, registration.ActivityType)) { throw new InvalidOperationException($"Conflicting activity type registration: Activity type [{activityType.FullName}] is already registered for activity type name [{activityTypeKey}]."); } } else { nameToRegistration[activityTypeKey] = new ActivityRegistration() { ActivityType = activityType, ActivityConstructor = constructor, ActivityMethod = method, ActivityMethodParamaterTypes = method.GetParameterTypes() }; } } var reply = (ActivityRegisterReply)await client.CallProxyAsync( new ActivityRegisterRequest() { Name = GetActivityTypeNameFromKey(activityTypeKey), Domain = client.ResolveDomain(domain) }); // $hack(jefflill): // // We're going to ignore any errors here to handle: // // https://github.com/nforgeio/neonKUBE/issues/668 // reply.ThrowOnError(); } }