Example #1
0
        /// <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);
        }
Example #2
0
        /// <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;
            }
        }
Example #3
0
        /// <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;
            }
        }
Example #4
0
        /// <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;
            }
        }
Example #5
0
        /// <summary>
        /// Handles workflow invocation.
        /// </summary>
        /// <param name="request">The request message.</param>
        /// <returns>The reply message.</returns>
        internal async Task <WorkflowInvokeReply> OnInvokeAsync(WorkflowInvokeRequest request)
        {
            await SyncContext.Clear;

            Covenant.Requires <ArgumentNullException>(request != null, nameof(request));
            Covenant.Requires <ArgumentException>(request.ReplayStatus != InternalReplayStatus.Unspecified, nameof(request));

            WorkflowBase         workflow;
            WorkflowRegistration registration;

            var contextId = request.ContextId;

            lock (idToWorkflow)
            {
                if (idToWorkflow.TryGetValue(contextId, out workflow))
                {
                    return(new WorkflowInvokeReply()
                    {
                        Error = new TemporalError($"A workflow with [ContextId={contextId}] is already running on this worker.")
                    });
                }
            }

            registration = GetWorkflowRegistration(request.WorkflowType);

            if (registration == null)
            {
                return(new WorkflowInvokeReply()
                {
                    Error = new TemporalError($"Workflow type name [Type={request.WorkflowType}] is not registered for this worker.")
                });
            }

            workflow          = (WorkflowBase)Activator.CreateInstance(registration.WorkflowType);
            workflow.Workflow =
                new Workflow(
                    parent:             (WorkflowBase)workflow,
                    worker:             this,
                    contextId:          contextId,
                    workflowTypeName:   request.WorkflowType,
                    @namespace:         request.Namespace,
                    taskQueue:          request.TaskQueue,
                    workflowId:         request.WorkflowId,
                    runId:              request.RunId,
                    isReplaying:        request.ReplayStatus == InternalReplayStatus.Replaying,
                    methodMap:          registration.MethodMap);

            Workflow.Current = workflow.Workflow;   // Initialize the ambient workflow information.

            lock (idToWorkflow)
            {
                idToWorkflow.Add(contextId, workflow);
            }

            // Register any workflow signal and/or query methods with [temporal-proxy].
            // Note that synchronous signals need special handling further below.

            foreach (var signalName in registration.MethodMap.GetSignalNames())
            {
                var reply = (WorkflowSignalSubscribeReply)await Client.CallProxyAsync(
                    new WorkflowSignalSubscribeRequest()
                {
                    ContextId  = contextId,
                    SignalName = signalName,
                    WorkerId   = this.WorkerId
                });

                reply.ThrowOnError();
            }

            foreach (var queryType in registration.MethodMap.GetQueryTypes())
            {
                var reply = (WorkflowSetQueryHandlerReply)await Client.CallProxyAsync(
                    new WorkflowSetQueryHandlerRequest()
                {
                    ContextId = contextId,
                    QueryName = queryType,
                    WorkerId  = this.WorkerId
                });

                reply.ThrowOnError();
            }

            // $hack(jefflill):
            //
            // We registered synchronous signal names above even though we probably
            // shouldn't have.  This shouldn't really cause any trouble.
            //
            // If the workflow has any synchronous signals, we need to register the
            // special synchronous signal dispatcher as well as the synchronous signal
            // query handler.

            if (registration.MethodMap.HasSynchronousSignals)
            {
                workflow.HasSynchronousSignals = true;

                var signalSubscribeReply = (WorkflowSignalSubscribeReply)await Client.CallProxyAsync(
                    new WorkflowSignalSubscribeRequest()
                {
                    ContextId  = contextId,
                    SignalName = TemporalClient.SignalSync,
                    WorkerId   = this.WorkerId
                });

                signalSubscribeReply.ThrowOnError();

                var querySubscribeReply = (WorkflowSetQueryHandlerReply)await Client.CallProxyAsync(
                    new WorkflowSetQueryHandlerRequest()
                {
                    ContextId = contextId,
                    QueryName = TemporalClient.QuerySyncSignal,
                    WorkerId  = this.WorkerId
                });

                querySubscribeReply.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 [ContinuAsNewAsync()] 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
            {
                WorkflowBase.CallContext.Value = WorkflowCallContext.Entrypoint;

                var workflowMethod   = registration.WorkflowMethod;
                var resultType       = workflowMethod.ReturnType;
                var args             = TemporalHelper.BytesToArgs(Client.DataConverter, request.Args, registration.WorkflowMethodParameterTypes);
                var serializedResult = Array.Empty <byte>();

                if (resultType.IsGenericType)
                {
                    // Workflow method returns: Task<T>

                    var result = await NeonHelper.GetTaskResultAsObjectAsync((Task)workflowMethod.Invoke(workflow, args));

                    serializedResult = Client.DataConverter.ToData(result);
                }
                else
                {
                    // Workflow method returns: Task

                    await(Task) workflowMethod.Invoke(workflow, args);
                }

                await workflow.WaitForPendingWorkflowOperationsAsync();

                return(new WorkflowInvokeReply()
                {
                    Result = serializedResult
                });
            }
            catch (ForceReplayException)
            {
                return(new WorkflowInvokeReply()
                {
                    ForceReplay = true
                });
            }
            catch (ContinueAsNewException e)
            {
                await workflow.WaitForPendingWorkflowOperationsAsync();

                return(new WorkflowInvokeReply()
                {
                    ContinueAsNew = true,
                    ContinueAsNewArgs = e.Args,
                    ContinueAsNewWorkflow = e.Workflow,
                    ContinueAsNewNamespace = e.Namespace,
                    ContinueAsNewTaskQueue = e.TaskQueue,
                    ContinueAsNewExecutionStartToCloseTimeout = TemporalHelper.ToTemporal(e.StartToCloseTimeout),
                    ContinueAsNewScheduleToCloseTimeout = TemporalHelper.ToTemporal(e.ScheduleToCloseTimeout),
                    ContinueAsNewScheduleToStartTimeout = TemporalHelper.ToTemporal(e.ScheduleToStartTimeout),
                    ContinueAsNewStartToCloseTimeout = TemporalHelper.ToTemporal(e.DecisionTaskTimeout),
                });
            }
            catch (TemporalException e)
            {
                log.LogError(e);

                await workflow.WaitForPendingWorkflowOperationsAsync();

                return(new WorkflowInvokeReply()
                {
                    Error = e.ToTemporalError()
                });
            }
            catch (Exception e)
            {
                log.LogError(e);

                await workflow.WaitForPendingWorkflowOperationsAsync();

                return(new WorkflowInvokeReply()
                {
                    Error = new TemporalError(e)
                });
            }
            finally
            {
                WorkflowBase.CallContext.Value = WorkflowCallContext.None;
            }
        }