Skip to content

Latest commit

 

History

History

README.md

Sorbet Generic

This sample shows a proof of concept on how to use advanced Sorbet generics for activities, workflows, signals, queries, and updates. The Temporal Ruby SDK does not have Sorbet signatures natively, so this is just a proof of concept on how Sorbet users could consume the library if they have advanced needs.

⚠️ Users that are not concerned with advanced generic use cases can just use traditional tapioca generation to generate the RBI files for Temporal Ruby SDK. This sample is only here to demonstrate advanced generic use cases.

See the "Issues" section for details on issues encountered during development.

To run, first see README.md for prerequisites. Then, in this directory:

bundle install

To check types, run:

bundle exec srb tc

Note how you can change a parameter type when calling activity, workflow, signal, query, or update and type checking will fail if it doesn't match expectation.

To actually run the code, from a terminal, start the worker:

bundle exec ruby worker.rb

Then, from another terminal, run the workflow:

bundle exec ruby starter.rb

Issues

There are a few issues with Sorbet and advanced use that had to be worked around. See each section below.

Generated Code Parameter Arity

The Temporal Ruby SDK does not use Sorbet itself and does not take a sorbet-runtime dependency or have inline signatures. Therefore, Temporal must be treated like any other non-Sorbet Ruby library. To generate the signatures herein, we:

  • Ran bundle exec tapioca init
  • Ran bundle exec tapioca require
  • Added require 'google/protobuf' to tapioca/require.rb
  • Ran bundle exec tapioca gem temporalio google-protobuf

Unfortunately, due to the fact that the SDK supports splatted positional parameters (e.g. for a workflow) where a user may want a well-typed limited set of parameters, we have to alter the generated code. As documented at https://srb.help/4010:

In cases like these, usually the solution is to remove the foo definition from autogenerated/some_gem.rbi

This means we had to hand-mutate the tapioca-generated RBI files, specifically we had to comment out all methods we wanted to refine parameter arity on. Look for "NOTE: Manually removed" comments in sorbet/rbi/gems/temporalio@0.3.0.rbi.

Arity Mismatch

For generic reasons we had to define the base classes for workflows and activities as having a single input (which Temporal encourages anyways). However, it is also normal to have no input, but Sorbet disallows overrides to change the parameter count, so a _ placeholder param with a default has to be used to ignore it.

Use of Decorator-Like Approach

When a signal, query, or update is defined on a method, the Ruby SDK also makes an associated class method for the "definition" of that handler for use by clients. So when this is present:

workflow_signal
sig { params(some_value: String).void }
def my_signal(some_value)
  @some_value = some_value
end

That creates a my_signal class method on the workflow class dynamically. However, due to an issue in Sorbet defining singleton methods in instance on_method_added, this needs to change to:

workflow_signal
T::Sig::WithoutRuntime.sig { params(some_value: String).void }
def my_signal(some_value)
  @some_value = some_value
end

This disables runtime behavior to prevent the bug.

Referencing Generic Classes

In Sorbet a generic class is referenced using brackets. For example, to define a class method stub on a workflow to represent the above-section-mentioned class methods created, one might have:

sig { returns(Temporalio::Workflow::Definition::Signal[String]) }
def self.my_signal = T.unsafe(nil)

But this will fail at runtime because the Temporal Ruby SDK doesn't define []. So you have to change this to:

T::Sig::WithoutRuntime.sig { returns(Temporalio::Workflow::Definition::Signal[String]) }
def self.my_signal = T.unsafe(nil)

This will avoid processing the invalid body.