Skip to content

Suggetions on signal receiver API #3023

Description

@alisabzevari

Is your feature request related to a problem? Please describe.

I have a workflow that I want to wait for a signal to arrive but only in the middle of the execution in a certain point. In other words, calling the signal if the workflow is not in the state to listen to that signal is an error.

In jvm sdk signals are just methods on workflow and in order to implement this behavior I need to have a shared mutable variable (like a flag) that I can check and fail if the flag is not set.

Here is an example:
Today in an order workflow if I want to wait for the payment received, I have to implement it like this (The code is in Kotlin but the idea is the same):

// TODAY — current Temporal API
class OrderWorkflowImpl : OrderWorkflow {
    private var payment: Payment? = null

    override fun paymentReceived(payment: Payment) {
        this.payment = payment
    }

    override fun run(orderId: OrderId) {
        reserveInventory(orderId)

        Workflow.await { payment != null }
        val receipt = chargeCustomer(payment!!)  // unsafe !!

        shipOrder(orderId, receipt)
    }
}

There are issues with this approach:

  • I can't control when to start listening to a signal. Any time paymentReceived method can be called and it will mutate my state. We can guard against that but the code will become more verbose.
  • I need to have a shared mutable variable that usually is nullable (payment in the example).
  • In general, the code logic jumps around which makes it hard to understand.

Describe the solution you'd like

I would propose to have a receive function that only enables listening to a signal at a certain point. Here is an hypothetical syntax (again in Kotlin):

// PROPOSED — with Signal
class OrderWorkflowImpl : OrderWorkflow {
    private val paymentReceived = Workflow.signal<Payment>("paymentReceived")

    override fun run(orderId: OrderId) {
        reserveInventory(orderId)

        val receipt = paymentReceived.receive { payment ->
            chargeCustomer(payment)  // runs immediately when the signal arrives
        }

        shipOrder(orderId, receipt)
    }
}

In this approach:

  • the execution awaits on paymentReceived.receive waiting for the signal to be received.
  • The code in the callback will be executed before receive function returns.
  • receive function returns the return value of the callback.

Describe alternatives you've considered

I built Signal class that improves the code readability a little bit but does not completely solve the problem:

/**
 * A typed, one-shot signal channel for Temporal workflows.
 *
 * Encapsulates the nullable mutable field + `Workflow.await` pattern so callers
 * get a typed `await()` return value instead of `T?` that must be `!!`-cast at
 * every use site. The `!!` lives here, behind a precondition that makes it safe:
 * `await` only returns after the value is confirmed non-null.
 *
 * Usage:
 *   val signal = Signal<MyType>()
 *   // In signal handler:
 *   override fun onMySignal(value: MyType) = signal.send(value)
 *   // In workflow body:
 *   val value = signal.await()  // blocks until the signal arrives
 *   signal.reset()              // clear before the next round
 */
class Signal<T : Any> {
    private var value: T? = null

    fun send(value: T) {
        if (this.value != null) throw IllegalStateException("Signal received twice: already=${this.value}, incoming=$value")
        this.value = value
    }

    fun hasValue(): Boolean = value != null

    fun await(condition: () -> Boolean = { true }): T {
        Workflow.await { condition() && value != null }
        return value!!.also { value = null }
    }

    fun reset() {
        value = null
    }
}

This alternative still receives the signal at any time in the workflow process. It would be great if you could provide an alternative for receiving signals in a workflow.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions