/// <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; } }