/// <summary> /// Registers a workflow implementation with Temporal. /// </summary> /// <typeparam name="TWorkflow">The <see cref="WorkflowBase"/> derived class implementing the workflow.</typeparam> /// <param name="disableDuplicateCheck">Disable checks for duplicate registrations.</param> /// <returns>The tracking <see cref="Task"/>.</returns> /// <exception cref="InvalidOperationException"> /// Thrown if the worker has already been started. You must register workflow /// and activity implementations before starting workers. /// </exception> /// <remarks> /// <note> /// Be sure to register all of your workflow implementations before starting a worker. /// </note> /// </remarks> public async Task RegisterWorkflowAsync <TWorkflow>(bool disableDuplicateCheck = false) where TWorkflow : WorkflowBase { await SyncContext.Clear; TemporalHelper.ValidateWorkflowImplementation(typeof(TWorkflow)); EnsureNotDisposed(); EnsureCanRegister(); var workflowType = typeof(TWorkflow); lock (registeredWorkflowTypes) { if (registeredWorkflowTypes.Contains(workflowType)) { if (disableDuplicateCheck) { return; } else { throw new RegistrationException($"Workflow implementation [{workflowType.FullName}] has already been registered."); } } registeredWorkflowTypes.Add(workflowType); } }
/// <summary> /// Registers an activity implementation with Temporal. /// </summary> /// <typeparam name="TActivity">The <see cref="ActivityBase"/> derived class implementing the activity.</typeparam> /// <param name="disableDuplicateCheck">Disable checks for duplicate activity registrations.</param> /// <returns>The tracking <see cref="Task"/>.</returns> /// <exception cref="InvalidOperationException"> /// Thrown if the worker has already been started. You must register workflow /// and activity implementations before starting a worker. /// </exception> /// <exception cref="RegistrationException">Thrown when there's a problem with the registration.</exception> /// <remarks> /// <note> /// Be sure to register all services you will be injecting into activities via /// <see cref="NeonHelper.ServiceContainer"/> before you call this as well as /// registering of your activity implementations before starting a worker. /// </note> /// </remarks> public async Task RegisterActivityAsync <TActivity>(bool disableDuplicateCheck = false) where TActivity : ActivityBase { await SyncContext.Clear; TemporalHelper.ValidateActivityImplementation(typeof(TActivity)); EnsureNotDisposed(); EnsureCanRegister(); lock (registeredActivityTypes) { var activityType = typeof(TActivity); if (registeredActivityTypes.Contains(activityType)) { if (disableDuplicateCheck) { return; } else { throw new RegistrationException($"Activity implementation [{typeof(TActivity).FullName}] has already been registered."); } } registeredActivityTypes.Add(activityType); } }
/// <summary> /// Queries the workflow. /// </summary> /// <typeparam name="TQueryResult">The query result type.</typeparam> /// <param name="queryName">Identifies the query.</param> /// <param name="args">The query arguments.</param> /// <returns>The query result.</returns> /// <remarks> /// <note> /// <b>IMPORTANT:</b> You need to take care to ensure that the parameters and /// result type passed are compatible with the target workflow query arguments. /// </note> /// </remarks> public async Task <TQueryResult> QueryAsync <TQueryResult>(string queryName, params object[] args) { await SyncContext.Clear; Covenant.Requires <ArgumentNullException>(!string.IsNullOrEmpty(queryName), nameof(queryName)); Covenant.Requires <ArgumentNullException>(args != null, nameof(args)); if (execution == null) { throw new InvalidOperationException("The stub must be started first."); } var reply = (WorkflowQueryReply)await client.CallProxyAsync( new WorkflowQueryRequest() { WorkflowId = execution.WorkflowId, RunId = execution.RunId, Namespace = options.Namespace, QueryName = queryName, QueryArgs = TemporalHelper.ArgsToBytes(client.DataConverter, args) }); reply.ThrowOnError(); return(client.DataConverter.FromData <TQueryResult>(reply.Result)); }
/// <summary> /// Executes the target activity method. /// </summary> /// <param name="client">The associated Temporal client.</param> /// <param name="argBytes">The encoded activity arguments.</param> /// <returns>The encoded activity results.</returns> private async Task <byte[]> InvokeAsync(TemporalClient client, byte[] argBytes) { await SyncContext.Clear; var parameters = activityMethod.GetParameters(); var parameterTypes = new Type[parameters.Length]; for (int i = 0; i < parameters.Length; i++) { parameterTypes[i] = parameters[i].ParameterType; } var resultType = activityMethod.ReturnType; var args = TemporalHelper.BytesToArgs(dataConverter, argBytes, parameterTypes); var serializedResult = Array.Empty <byte>(); if (resultType.IsGenericType) { // Activity method returns: Task<T> var result = await NeonHelper.GetTaskResultAsObjectAsync((Task)activityMethod.Invoke(this, args)); serializedResult = client.DataConverter.ToData(result); } else { // Activity method returns: Task await(Task) activityMethod.Invoke(this, args); } return(serializedResult); }
/// <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 = TemporalHelper.ArgsToBytes(client.DataConverter, args) })); }); reply.ThrowOnError(); }
/// <summary> /// Scans the assembly passed looking for activity implementations derived from /// <see cref="ActivityBase"/> and tagged by <see cref="ActivityAttribute"/> and /// registers them with Temporal. /// </summary> /// <param name="assembly">The target assembly.</param> /// <param name="disableDuplicateCheck">Disable checks for duplicate activity registrations.</param> /// <returns>The tracking <see cref="Task"/>.</returns> /// <exception cref="TypeLoadException"> /// Thrown for types tagged by <see cref="ActivityAttribute"/> that are not /// derived from <see cref="ActivityBase"/>. /// </exception> /// <exception cref="InvalidOperationException">Thrown if one of the tagged classes conflict with an existing registration.</exception> /// <exception cref="InvalidOperationException"> /// Thrown if the worker has already been started. You must register workflow /// and activity implementations before starting workers. /// </exception> /// <exception cref="RegistrationException">Thrown when there's a problem with the registration.</exception> /// <remarks> /// <note> /// Be sure to register all services you will be injecting into activities via /// <see cref="NeonHelper.ServiceContainer"/> before you call this as well as /// registering of your activity implementations before starting a worker. /// </note> /// </remarks> public async Task RegisterAssemblyActivitiesAsync(Assembly assembly, bool disableDuplicateCheck = false) { await SyncContext.Clear; Covenant.Requires <ArgumentNullException>(assembly != null, nameof(assembly)); EnsureNotDisposed(); EnsureCanRegister(); lock (registeredActivityTypes) { foreach (var activityType in assembly.GetTypes().Where(type => type.IsClass)) { var activityAttribute = activityType.GetCustomAttribute <ActivityAttribute>(); if (activityAttribute != null && activityAttribute.AutoRegister) { var activityTypeName = TemporalHelper.GetActivityTypeName(activityType, activityAttribute); if (registeredActivityTypes.Contains(activityType)) { if (disableDuplicateCheck) { continue; } else { throw new RegistrationException($"Activity implementation [{activityType.FullName}] has already been registered."); } } registeredActivityTypes.Add(activityType); } } } }
/// <summary> /// Starts the target activity that returns <c>void</c>, passing the specified arguments. /// </summary> /// <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 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> StartAsync(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] = TemporalHelper.ConvertArg(parameters[i].ParameterType, args[i]); } // Start the activity. var client = parentWorkflow.Client; var dataConverter = client.DataConverter; var activityConstructor = typeof(TActivityImplementation).GetConstructor(Type.EmptyTypes); var activityId = parentWorkflow.GetNextActivityId(); var activityActionId = parentWorkflow.RegisterActivityAction(typeof(TActivityImplementation), activityConstructor, targetMethod); var reply = await parentWorkflow.ExecuteNonParallel( async() => { return((ActivityStartLocalReply)await client.CallProxyAsync( new ActivityStartLocalRequest() { ContextId = parentWorkflow.ContextId, WorkerId = parentWorkflow.Worker.WorkerId, ActivityId = activityId, ActivityTypeId = activityActionId, Args = TemporalHelper.ArgsToBytes(dataConverter, args), Options = options })); }); reply.ThrowOnError(); parentWorkflow.UpdateReplay(reply); // Create and return the future. return(new AsyncFuture(parentWorkflow, activityId)); }
public async Task SignalAsync(WorkflowExecution execution, string signalName, params object[] args) { await SyncContext.Clear; var dataConverter = Activity.Client.DataConverter; await Activity.Client.SignalWorkflowAsync(execution, signalName, TemporalHelper.ArgsToBytes(dataConverter, args)); }
/// <summary> /// Starts the target workflow that returns <typeparamref name="TResult"/>, passing any specified arguments. /// </summary> /// <typeparam name="TResult">The workflow result type.</typeparam> /// <param name="args">The arguments to be passed to the workflow.</param> /// <returns>The <see cref="ChildWorkflowFuture{T}"/> with the <see cref="ChildWorkflowFuture{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 workflow parameters. /// These are checked at runtime but not while compiling. /// </para> /// <note> /// Any given <see cref="ChildWorkflowStub{TWorkflowInterface}"/> may only be executed once. /// </note> /// </remarks> public async Task <ChildWorkflowFuture <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] = TemporalHelper.ConvertArg(parameters[i].ParameterType, args[i]); } // Start the child workflow and then construct and return the future. var client = parentWorkflow.Client; var execution = await client.StartChildWorkflowAsync(parentWorkflow, workflowTypeName, TemporalHelper.ArgsToBytes(client.DataConverter, args), options); // Initialize the type-safe stub property such that developers can call // any query or signal methods. Stub = StubManager.NewChildWorkflowStub <TWorkflowInterface>(client, parentWorkflow, workflowTypeName, execution); // Create and return the future. var resultType = targetMethod.ReturnType; if (resultType == typeof(Task)) { throw new ArgumentException($"Workflow method [{nameof(TWorkflowInterface)}.{targetMethod.Name}()] does not return [void].", nameof(TWorkflowInterface)); } resultType = resultType.GenericTypeArguments.First(); if (!resultType.IsAssignableFrom(typeof(TResult))) { throw new ArgumentException($"Workflow method [{nameof(TWorkflowInterface)}.{targetMethod.Name}()] returns [{resultType.FullName}] which is not compatible with [{nameof(TResult)}].", nameof(TWorkflowInterface)); } return(new ChildWorkflowFuture <TResult>(parentWorkflow, execution)); }
/// <summary> /// Internal constructor. /// </summary> /// <param name="client">The associated client.</param> /// <param name="methodName"> /// Optionally identifies the target workflow method by the name specified in /// the <c>[WorkflowMethod]</c> attribute tagging the method. Pass a <c>null</c> /// or empty string to target the default method. /// </param> /// <param name="options">Optional workflow options.</param> internal WorkflowFutureStub(TemporalClient client, string methodName = null, StartWorkflowOptions options = null) { Covenant.Requires <ArgumentNullException>(client != null, nameof(client)); var workflowInterface = typeof(WorkflowInterface); var method = TemporalHelper.GetWorkflowMethod(workflowInterface, methodName); TemporalHelper.ValidateWorkflowInterface(workflowInterface); this.client = client; this.workflowTypeName = TemporalHelper.GetWorkflowTarget(workflowInterface, methodName).WorkflowTypeName; this.options = StartWorkflowOptions.Normalize(client, options, workflowInterface, method); }
/// <summary> /// Internal constructor. /// </summary> /// <param name="parentWorkflow">The associated parent workflow.</param> /// <param name="methodName"> /// Optionally identifies the target activity method by the name specified in /// the <c>[ActivityMethod]</c> attribute tagging the method. Pass a <c>null</c> /// or empty string to specify the default method. /// </param> /// <param name="options">The activity options or <c>null</c>.</param> internal LocalActivityFutureStub(Workflow parentWorkflow, string methodName = null, LocalActivityOptions options = null) { Covenant.Requires <ArgumentNullException>(parentWorkflow != null, nameof(parentWorkflow)); var activityInterface = typeof(TActivityInterface); TemporalHelper.ValidateActivityInterface(activityInterface); this.parentWorkflow = parentWorkflow; this.hasStarted = false; this.targetMethod = TemporalHelper.GetActivityTarget(activityInterface, methodName).TargetMethod; this.options = LocalActivityOptions.Normalize(parentWorkflow.Client, options); }
/// <summary> /// Executes the associated workflow and waits for it to complete, /// returning the workflow result. /// </summary> /// <typeparam name="TResult">The workflow result type.</typeparam> /// <param name="args">The workflow arguments.</param> /// <returns>The tracking <see cref="Task"/>.</returns> public async Task <TResult> ExecuteAsync <TResult>(params object[] args) { await SyncContext.Clear; Covenant.Requires <ArgumentNullException>(args != null, nameof(args)); EnsureNotStarted(); var argBytes = TemporalHelper.ArgsToBytes(client.DataConverter, args); Execution = await client.StartWorkflowAsync(WorkflowTypeName, argBytes, Options); return(await GetResultAsync <TResult>()); }
/// <summary> /// Signals the associated workflow, starting it if it hasn't already been started. /// </summary> /// <param name="signalName">Specifies the signal name.</param> /// <param name="signalArgs">Specifies the signal arguments.</param> /// <param name="startArgs">Specifies the workflow start arguments.</param> /// <returns>The tracking <see cref="Task"/>.</returns> public async Task <WorkflowExecution> SignalWithStartAsync(string signalName, object[] signalArgs, object[] startArgs) { await SyncContext.Clear; Covenant.Requires <ArgumentNullException>(!string.IsNullOrEmpty(signalName), nameof(signalName)); Covenant.Requires <ArgumentNullException>(signalArgs != null, nameof(signalArgs)); Covenant.Requires <ArgumentNullException>(startArgs != null, nameof(startArgs)); var signalArgBytes = TemporalHelper.ArgsToBytes(client.DataConverter, signalArgs); var startArgBytes = TemporalHelper.ArgsToBytes(client.DataConverter, startArgs); return(await client.SignalWorkflowWithStartAsync(this.WorkflowTypeName, signalName, signalArgBytes, startArgBytes, this.Options)); }
/// <summary> /// Constructs an instance from a <see cref="LinearRetryPolicy"/>. /// </summary> /// <param name="policy">The policy.</param> public RetryPolicy(LinearRetryPolicy policy) { Covenant.Requires <ArgumentNullException>(policy != null, nameof(policy)); this.InitialInterval = TemporalHelper.Normalize(policy.RetryInterval); this.BackoffCoefficient = 1.0; if (policy.Timeout.HasValue) { this.ExpirationInterval = TemporalHelper.Normalize(policy.Timeout.Value); } this.MaximumAttempts = policy.MaxAttempts; }
/// <summary> /// Signals the associated workflow. /// </summary> /// <param name="signalName">Specifies the signal name.</param> /// <param name="args">Specifies the signal arguments.</param> /// <returns>The tracking <see cref="Task"/>.</returns> 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)); EnsureStarted(); if (Execution == null) { throw new InvalidOperationException("Signal cannot be sent because the stub doesn't have the workflow execution."); } var argBytes = TemporalHelper.ArgsToBytes(client.DataConverter, args); await client.SignalWorkflowAsync(Execution, signalName, argBytes, client.ResolveNamespace(Options?.Namespace)); }
/// <summary> /// Starts the workflow, returning an <see cref="IAsyncFuture"/> that can be used /// to wait for the the workflow to complete and obtain its result. /// </summary> /// <typeparam name="TResult">The workflow result type.</typeparam> /// <param name="args">The workflow arguments.</param> /// <returns>An <see cref="ExternalWorkflowFuture{TResult}"/> that can be used to retrieve the workflow result as an <c>object</c>.</returns> /// <exception cref="InvalidOperationException">Thrown if the workflow has already been started.</exception> /// <remarks> /// <note> /// <b>IMPORTANT:</b> You need to take care to ensure that the parameters passed /// and the result type are compatible with the target workflow method. /// </note> /// </remarks> public async Task <ExternalWorkflowFuture <TResult> > StartAsync <TResult>(params object[] args) { await SyncContext.Clear; Covenant.Requires <ArgumentNullException>(args != null, nameof(args)); if (execution != null) { throw new InvalidOperationException("Cannot start a future stub more than once."); } execution = await client.StartWorkflowAsync(workflowTypeName, TemporalHelper.ArgsToBytes(client.DataConverter, args), options); // Create and return the future. return(new ExternalWorkflowFuture <TResult>(client, execution, options.Namespace)); }
/// <summary> /// Queries the associated workflow specifying the expected result type as /// a parameter. /// </summary> /// <param name="resultType">Specifies the query result type.</param> /// <param name="queryType">Specifies the query type.</param> /// <param name="args">Specifies the query arguments.</param> /// <returns>The query result as a <c>dynamic</c>.</returns> public async Task <object> QueryAsync(Type resultType, string queryType, params object[] args) { await SyncContext.Clear; Covenant.Requires <ArgumentNullException>(!string.IsNullOrEmpty(queryType), nameof(queryType)); Covenant.Requires <ArgumentNullException>(args != null, nameof(args)); EnsureStarted(); if (Execution == null) { throw new InvalidOperationException("Query cannot be sent because the stub doesn't have the workflow execution."); } var argBytes = TemporalHelper.ArgsToBytes(client.DataConverter, args); return(client.DataConverter.FromData(resultType, await client.QueryWorkflowAsync(Execution, queryType, argBytes, client.ResolveNamespace(Options?.Namespace)))); }
/// <summary> /// Internal constructor. /// </summary> /// <param name="parentWorkflow">The associated parent workflow.</param> /// <param name="methodName">Identifies the target workflow method or <c>null</c> or empty.</param> /// <param name="options">The child workflow options or <c>null</c>.</param> internal ChildWorkflowStub(Workflow parentWorkflow, string methodName, ChildWorkflowOptions options) { Covenant.Requires <ArgumentNullException>(parentWorkflow != null, nameof(parentWorkflow)); var workflowInterface = typeof(TWorkflowInterface); TemporalHelper.ValidateWorkflowInterface(workflowInterface); this.parentWorkflow = parentWorkflow; this.options = options; this.hasStarted = false; var workflowTarget = TemporalHelper.GetWorkflowTarget(workflowInterface, methodName); this.workflowTypeName = workflowTarget.WorkflowTypeName; this.targetMethod = workflowTarget.TargetMethod; }
/// <summary> /// Signals the workflow. /// </summary> /// <param name="signalName">Specifies the signal name.</param> /// <param name="args">Specifies the signal arguments.</param> /// <returns>The tracking <see cref="Task"/>.</returns> 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 (parentWorkflow != null) { var stub = parentWorkflow.NewLocalActivityStub <ILocalOperations, LocalOperations>(); await stub.SignalAsync(Execution, signalName, args); } else { await client.SignalWorkflowAsync(Execution, signalName, TemporalHelper.ArgsToBytes(client.DataConverter, args)); } }
/// <summary> /// <b>EXPERIMENTAL:</b> This method synchronously signals the workflow and returns /// only after the workflow has processed received and processed the signal as opposed /// to <see cref="SignalAsync"/> which is fire-and-forget and does not wait for the /// signal to be processed. /// </summary> /// <param name="signalName"> /// The signal name as defined by the <see cref="SignalMethodAttribute"/> /// decorating the workflow signal method. /// </param> /// <param name="args">The signal arguments.</param> /// <remarks> /// <note> /// <b>IMPORTANT:</b> You need to take care to ensure that the parameters passed /// are compatible with the target workflow arguments. No compile-time type checking /// is performed for this method. /// </note> /// </remarks> public async Task SyncSignalAsync(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 signalId = Guid.NewGuid().ToString("d"); var argBytes = TemporalHelper.ArgsToBytes(client.DataConverter, args); var signalCall = new SyncSignalCall(signalName, signalId, argBytes); var signalCallBytes = TemporalHelper.ArgsToBytes(client.DataConverter, new object[] { signalCall }); await client.SyncSignalWorkflowAsync(execution, signalName, signalId, signalCallBytes, options.Namespace); }
/// <summary> /// Internal constructor. /// </summary> /// <param name="parentWorkflow">The associated parent workflow.</param> /// <param name="methodName"> /// Optionally identifies the target activity method by the name specified in /// the <c>[ActivityMethod]</c> attribute tagging the method. Pass a <c>null</c> /// or empty string to target the default method. /// </param> /// <param name="options">The activity options or <c>null</c>.</param> internal ActivityFutureStub(Workflow parentWorkflow, string methodName = null, ActivityOptions options = null) { Covenant.Requires <ArgumentNullException>(parentWorkflow != null, nameof(parentWorkflow)); var activityInterface = typeof(TActivityInterface); TemporalHelper.ValidateActivityInterface(activityInterface); this.parentWorkflow = parentWorkflow; this.client = parentWorkflow.Client; this.hasStarted = false; var activityTarget = TemporalHelper.GetActivityTarget(activityInterface, methodName); var methodAttribute = activityTarget.MethodAttribute; this.activityTypeName = activityTarget.ActivityTypeName; this.targetMethod = activityTarget.TargetMethod; this.options = ActivityOptions.Normalize(client, options, typeof(TActivityInterface), activityTarget.TargetMethod); }
/// <summary> /// Called internally to initialize the activity. /// </summary> /// <param name="worker">The worker hosting the activity.</param> /// <param name="activityType">Specifies the target activity type.</param> /// <param name="activityMethod">Specifies the target activity method.</param> /// <param name="dataConverter">Specifies the data converter to be used for parameter and result serilization.</param> /// <param name="contextId">The activity's context ID.</param> internal void Initialize(Worker worker, Type activityType, MethodInfo activityMethod, IDataConverter dataConverter, long contextId) { Covenant.Requires <ArgumentNullException>(worker != null, nameof(worker)); Covenant.Requires <ArgumentNullException>(activityType != null, nameof(activityType)); Covenant.Requires <ArgumentNullException>(activityMethod != null, nameof(activityMethod)); Covenant.Requires <ArgumentNullException>(dataConverter != null, nameof(dataConverter)); TemporalHelper.ValidateActivityImplementation(activityType); this.worker = worker; this.Client = worker.Client; this.Activity = new Activity(this); this.activityType = activityType; this.activityMethod = activityMethod; this.dataConverter = dataConverter; this.ContextId = contextId; this.CancellationTokenSource = new CancellationTokenSource(); this.CancellationToken = CancellationTokenSource.Token; this.logger = LogManager.Default.GetLogger(module: activityType.FullName); }
/// <summary> /// Starts the target activity that returns <c>void</c>, passing the specified arguments. /// </summary> /// <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> StartAsync(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."); } hasStarted = true; // Start the activity. var client = parentWorkflow.Client; 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 = TemporalHelper.ArgsToBytes(dataConverter, args), Options = options })); }); reply.ThrowOnError(); parentWorkflow.UpdateReplay(reply); // Create and return the future. return(new AsyncFuture(parentWorkflow, activityId)); }
/// <summary> /// Creates a local activity stub instance suitable for executing a non-local activity. /// </summary> /// <param name="client">The associated <see cref="TemporalClient"/>.</param> /// <param name="workflow">The parent workflow.</param> /// <param name="activityType">The activity implementation type.</param> /// <param name="options">Specifies the <see cref="LocalActivityOptions"/> or <c>null</c>.</param> /// <returns>The activity stub as an <see cref="object"/>.</returns> public object CreateLocal(TemporalClient client, Workflow workflow, Type activityType, LocalActivityOptions options) { Covenant.Requires <ArgumentNullException>(client != null, nameof(client)); Covenant.Requires <ArgumentNullException>(workflow != null, nameof(workflow)); Covenant.Requires <ArgumentNullException>(activityType != null, nameof(activityType)); options = options ?? new LocalActivityOptions(); return(localConstructor.Invoke(new object[] { client, client.DataConverter, workflow, activityType, options, TemporalHelper.GetActivityInterface(activityType) })); }
/// <summary> /// Constructor. /// </summary> /// <param name="name"> /// Optionally specifies the activity type name used to /// register an activity implementation with Temporal. /// </param> public ActivityAttribute(string name = null) { TemporalHelper.ValidateActivityTypeName(name); this.Name = name; }
/// <summary> /// Handles internal <see cref="TemporalClient.SignalSync"/> workflow signals. /// </summary> /// <param name="request">The request message.</param> /// <returns>The reply message.</returns> internal async Task <WorkflowSignalInvokeReply> OnSyncSignalAsync(WorkflowSignalInvokeRequest request) { await SyncContext.Clear; Covenant.Requires <ArgumentNullException>(request != null, nameof(request)); try { WorkflowBase.CallContext.Value = WorkflowCallContext.Signal; var workflow = GetWorkflow(request.ContextId); if (workflow != null) { Workflow.Current = workflow.Workflow; // Initialize the ambient workflow information. // The signal arguments should be just a single [SyncSignalCall] that specifies // the target signal and also includes its encoded arguments. var signalCallArgs = TemporalHelper.BytesToArgs(JsonDataConverter.Instance, request.SignalArgs, new Type[] { typeof(SyncSignalCall) }); var signalCall = (SyncSignalCall)signalCallArgs[0]; var signalMethod = workflow.Workflow.MethodMap.GetSignalMethod(signalCall.TargetSignal); var userSignalArgs = TemporalHelper.BytesToArgs(Client.DataConverter, signalCall.UserArgs, signalMethod.GetParameterTypes()); Workflow.Current.SignalId = signalCall.SignalId; // Create a dictionary with the signal method arguments keyed by parameter name. var args = new Dictionary <string, object>(); var parameters = signalMethod.GetParameters(); for (int i = 0; i < parameters.Length; i++) { args.Add(parameters[i].Name, userSignalArgs[i]); } // Persist the state that the signal status queries will examine. // We're also going to use the presence of this state to make // synchronous signal calls idempotent by ensuring that we'll // only call the signal method once per signal ID. // // Note that it's possible that a record has already exists. var signalStatus = workflow.SetSignalStatus(signalCall.SignalId, args, out var newSignal); if (newSignal && signalMethod != null) { Workflow.Current = workflow.Workflow; // Initialize the ambient workflow information for workflow library code. var result = (object)null; var exception = (Exception)null; // Execute the signal method (if there is one). try { if (TemporalHelper.IsTask(signalMethod.ReturnType)) { // Method returns [Task]: AKA void. await(Task)(signalMethod.Invoke(workflow, userSignalArgs)); } else { // Method returns [Task<T>]: AKA a result. // $note(jefflill): // // I would have liked to do something like this: // // result = await (Task<object>)(method.Invoke(workflow, userArgs)); // // here, but that not going to work because the Task<T> // being returned won't typically be a Task<object>, // so the cast will fail. // // So instead, I'm going to use reflection to obtain the // Task.Result property and then obtain the result from that. var task = (Task)(signalMethod.Invoke(workflow, userSignalArgs)); var resultProperty = task.GetType().GetProperty("Result"); await task; result = resultProperty.GetValue(task); } } catch (Exception e) { exception = e; } if (exception?.GetType() == typeof(WaitForSignalReplyException)) { // This will be thrown by synchronous signal handlers that marshalled // the signal to the workflow logic. We're going to ignore the signal // method result in this case and NOT MARK THE SIGNAL AS COMPLETED. } else { var syncSignalStatus = workflow.GetSignalStatus(signalCall.SignalId); if (syncSignalStatus != null) { if (exception == null) { syncSignalStatus.Result = Client.DataConverter.ToData(result); } else { log.LogError(exception); syncSignalStatus.Error = SyncSignalException.GetError(exception); } syncSignalStatus.Completed = true; } else { Covenant.Assert(false); // This should never happen. } } return(new WorkflowSignalInvokeReply() { RequestId = request.RequestId }); } else { return(new WorkflowSignalInvokeReply() { Error = new EntityNotExistsException($"Workflow type [{workflow.GetType().FullName}] does not define a signal handler for [signalName={request.SignalName}].").ToTemporalError() }); } } else { // I don't believe we'll ever land here because that would mean that // Temporal sends signals to a workflow that hasn't started running // on a worker (which wouldn't make sense). // // We're going go ahead and send a reply, just in case. return(new WorkflowSignalInvokeReply()); } } catch (Exception e) { log.LogError(e); return(new WorkflowSignalInvokeReply() { Error = new TemporalError(e) }); } finally { WorkflowBase.CallContext.Value = WorkflowCallContext.None; } }
/// <summary> /// Handles workflow queries. /// </summary> /// <param name="request">The request message.</param> /// <returns>The reply message.</returns> internal async Task <WorkflowQueryInvokeReply> OnQueryAsync(WorkflowQueryInvokeRequest request) { await SyncContext.Clear; Covenant.Requires <ArgumentNullException>(request != null, nameof(request)); try { WorkflowBase.CallContext.Value = WorkflowCallContext.Query; var workflow = GetWorkflow(request.ContextId); if (workflow != null) { Workflow.Current = workflow.Workflow; // Initialize the ambient workflow information for workflow library code. // Handle built-in queries. switch (request.QueryName) { case TemporalClient.QueryStack: var trace = string.Empty; if (workflow.StackTrace != null) { trace = workflow.StackTrace.ToString(); } return(new WorkflowQueryInvokeReply() { RequestId = request.RequestId, Result = Client.DataConverter.ToData(trace) }); case TemporalClient.QuerySyncSignal: // The arguments for this signal is the (string) ID of the target // signal being polled for status. var syncSignalArgs = TemporalHelper.BytesToArgs(JsonDataConverter.Instance, request.QueryArgs, new Type[] { typeof(string) }); var syncSignalId = (string)(syncSignalArgs.Length > 0 ? syncSignalArgs[0] : null); var syncSignalStatus = workflow.GetSignalStatus(syncSignalId); Covenant.Assert(false); // This should never happen if (syncSignalStatus.Completed) { // Indicate that the completed signal has reported the status // to the calling client as well as returned the result, if any. syncSignalStatus.Acknowledged = true; syncSignalStatus.AcknowledgeTime = DateTime.UtcNow; } return(new WorkflowQueryInvokeReply() { RequestId = request.RequestId, Result = Client.DataConverter.ToData(syncSignalStatus) }); } // Handle user queries. var method = workflow.Workflow.MethodMap.GetQueryMethod(request.QueryName); if (method != null) { var resultType = method.ReturnType; var methodParameterTypes = method.GetParameterTypes(); var serializedResult = Array.Empty <byte>(); if (resultType.IsGenericType) { // Query method returns: Task<T> var result = await NeonHelper.GetTaskResultAsObjectAsync((Task)method.Invoke(workflow, TemporalHelper.BytesToArgs(Client.DataConverter, request.QueryArgs, methodParameterTypes))); serializedResult = Client.DataConverter.ToData(result); } else { // Query method returns: Task await(Task) method.Invoke(workflow, TemporalHelper.BytesToArgs(Client.DataConverter, request.QueryArgs, methodParameterTypes)); } return(new WorkflowQueryInvokeReply() { RequestId = request.RequestId, Result = serializedResult }); } else { return(new WorkflowQueryInvokeReply() { Error = new EntityNotExistsException($"Workflow type [{workflow.GetType().FullName}] does not define a query handler for [queryType={request.QueryName}].").ToTemporalError() }); } } else { return(new WorkflowQueryInvokeReply() { Error = new EntityNotExistsException($"Workflow with [contextID={request.ContextId}] does not exist.").ToTemporalError() }); } } catch (Exception e) { log.LogError(e); return(new WorkflowQueryInvokeReply() { Error = new TemporalError(e) }); } finally { WorkflowBase.CallContext.Value = WorkflowCallContext.None; } }
/// <summary> /// Registers a workflow implementation with temporal-proxy. /// </summary> /// <param name="workflowType">The workflow implementation type.</param> /// <exception cref="RegistrationException">Thrown when there's a problem with the registration.</exception> private async Task RegisterWorkflowImplementationAsync(Type workflowType) { await SyncContext.Clear; TemporalHelper.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 workflowTypeName = TemporalHelper.GetWorkflowTypeName(workflowType, workflowMethodAttribute); lock (nameToWorkflowRegistration) { if (nameToWorkflowRegistration.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 { nameToWorkflowRegistration[workflowTypeName] = new WorkflowRegistration() { WorkflowType = workflowType, WorkflowMethod = method, WorkflowMethodParameterTypes = method.GetParameterTypes(), MethodMap = methodMap }; } } var reply = (WorkflowRegisterReply)await Client.CallProxyAsync( new WorkflowRegisterRequest() { WorkerId = WorkerId, Name = workflowTypeName, }); reply.ThrowOnError(); } }
//--------------------------------------------------------------------- // Static members /// <summary> /// <b>INTERNAL USE ONLY:</b> Normalizes the options passed by creating or cloning a new /// instance as required and filling unset properties using default client settings. /// </summary> /// <param name="client">The associated Temporal client.</param> /// <param name="options">The input options or <c>null</c>.</param> /// <param name="workflowInterface">Optionally specifies the workflow interface definition.</param> /// /// <param name="method">Optionally specifies the target workflow method.</param> /// <returns>The normalized options.</returns> /// <exception cref="ArgumentNullException">Thrown if a valid task queue is not specified.</exception> public static ChildWorkflowOptions Normalize(TemporalClient client, ChildWorkflowOptions options, Type workflowInterface = null, MethodInfo method = null) { Covenant.Requires <ArgumentNullException>(client != null, nameof(client)); WorkflowInterfaceAttribute interfaceAttribute = null; WorkflowMethodAttribute methodAttribute = null; if (options == null) { options = new ChildWorkflowOptions(); } else { options = options.Clone(); } if (workflowInterface != null) { TemporalHelper.ValidateWorkflowInterface(workflowInterface); interfaceAttribute = workflowInterface.GetCustomAttribute <WorkflowInterfaceAttribute>(); } if (method != null) { methodAttribute = method.GetCustomAttribute <WorkflowMethodAttribute>(); } if (string.IsNullOrEmpty(options.Namespace)) { if (!string.IsNullOrEmpty(methodAttribute?.Namespace)) { options.Namespace = methodAttribute.Namespace; } if (string.IsNullOrEmpty(options.Namespace) && !string.IsNullOrEmpty(interfaceAttribute?.Namespace)) { options.Namespace = interfaceAttribute.Namespace; } } if (string.IsNullOrEmpty(options.TaskQueue)) { if (!string.IsNullOrEmpty(methodAttribute?.TaskQueue)) { options.TaskQueue = methodAttribute.TaskQueue; } if (string.IsNullOrEmpty(options.TaskQueue) && !string.IsNullOrEmpty(interfaceAttribute?.TaskQueue)) { options.TaskQueue = interfaceAttribute.TaskQueue; } if (string.IsNullOrEmpty(options.TaskQueue)) { options.TaskQueue = client.Settings.TaskQueue; } } if (options.WorkflowExecutionTimeout <= TimeSpan.Zero) { if (methodAttribute != null && methodAttribute.WorkflowExecutionTimeoutSeconds > 0) { options.WorkflowExecutionTimeout = TimeSpan.FromSeconds(methodAttribute.WorkflowExecutionTimeoutSeconds); } if (options.WorkflowExecutionTimeout <= TimeSpan.Zero) { options.WorkflowExecutionTimeout = client.Settings.WorkflowExecutionTimeout; } } if (options.WorkflowRunTimeout <= TimeSpan.Zero) { if (methodAttribute != null && methodAttribute.WorkflowRunTimeoutSeconds > 0) { options.WorkflowRunTimeout = TimeSpan.FromSeconds(methodAttribute.WorkflowRunTimeoutSeconds); } if (options.WorkflowRunTimeout <= TimeSpan.Zero) { options.WorkflowRunTimeout = client.Settings.WorkflowRunTimeout; } } if (options.WorkflowTaskTimeout <= TimeSpan.Zero) { if (methodAttribute != null && methodAttribute.WorkflowTaskTimeoutSeconds > 0) { options.WorkflowTaskTimeout = TimeSpan.FromSeconds(methodAttribute.WorkflowTaskTimeoutSeconds); } if (options.WorkflowTaskTimeout <= TimeSpan.Zero) { options.WorkflowTaskTimeout = client.Settings.WorkflowTaskTimeout; } } if (options.WorkflowIdReusePolicy == Temporal.WorkflowIdReusePolicy.UseDefault) { if (methodAttribute != null && methodAttribute.WorkflowIdReusePolicy != WorkflowIdReusePolicy.UseDefault) { options.WorkflowIdReusePolicy = methodAttribute.WorkflowIdReusePolicy; } if (options.WorkflowIdReusePolicy == Temporal.WorkflowIdReusePolicy.UseDefault) { options.WorkflowIdReusePolicy = client.Settings.WorkflowIdReusePolicy; } } return(options); }
/// <summary> /// Handles workflow signals. /// </summary> /// <param name="request">The request message.</param> /// <returns>The reply message.</returns> internal async Task <WorkflowSignalInvokeReply> OnSignalAsync(WorkflowSignalInvokeRequest request) { await SyncContext.Clear; Covenant.Requires <ArgumentNullException>(request != null, nameof(request)); // Handle synchronous signals in a specialized method. if (request.SignalName == TemporalClient.SignalSync) { return(await OnSyncSignalAsync(request)); } try { WorkflowBase.CallContext.Value = WorkflowCallContext.Signal; var workflow = GetWorkflow(request.ContextId); if (workflow != null) { Workflow.Current = workflow.Workflow; // Initialize the ambient workflow information. var method = workflow.Workflow.MethodMap.GetSignalMethod(request.SignalName); if (method != null) { await(Task)(method.Invoke(workflow, TemporalHelper.BytesToArgs(Client.DataConverter, request.SignalArgs, method.GetParameterTypes()))); return(new WorkflowSignalInvokeReply() { RequestId = request.RequestId }); } else { return(new WorkflowSignalInvokeReply() { Error = new EntityNotExistsException($"Workflow type [{workflow.GetType().FullName}] does not define a signal handler for [signalName={request.SignalName}].").ToTemporalError() }); } } else { // I don't believe we'll ever land here because that would mean that // Temporal sends signals to a workflow that hasn't started running // on a worker (which wouldn't make sense). // // We're going go ahead and send a reply, just in case. return(new WorkflowSignalInvokeReply()); } } catch (Exception e) { log.LogError(e); return(new WorkflowSignalInvokeReply() { Error = new TemporalError(e) }); } finally { WorkflowBase.CallContext.Value = WorkflowCallContext.None; } }