From 86dfecc0445a4ebc9f603a32a69a1f0f499c710e Mon Sep 17 00:00:00 2001 From: Thomas Hardy Date: Tue, 21 Apr 2026 09:39:25 -0700 Subject: [PATCH 1/5] Ruby harness --- cmd/dev/test.go | 40 +++ workers/ruby/.rubocop.yml | 1 + workers/ruby/Gemfile | 2 + workers/ruby/Gemfile.lock | 50 +++ workers/ruby/kitchen_sink_app.rb | 36 +++ workers/ruby/omes.gemspec | 1 + workers/ruby/projects/harness.rb | 7 + workers/ruby/projects/harness/api/api_pb.rb | 25 ++ .../projects/harness/api/api_services_pb.rb | 33 ++ workers/ruby/projects/harness/client.rb | 112 +++++++ workers/ruby/projects/harness/helpers.rb | 32 ++ workers/ruby/projects/harness/main.rb | 32 ++ workers/ruby/projects/harness/project.rb | 161 ++++++++++ .../projects/harness/tests/test_project.rb | 290 ++++++++++++++++++ .../projects/harness/tests/test_worker.rb | 128 ++++++++ workers/ruby/projects/harness/worker.rb | 163 ++++++++++ workers/ruby/runner.rb | 194 +----------- workers/run.go | 4 +- 18 files changed, 1118 insertions(+), 193 deletions(-) create mode 100644 workers/ruby/kitchen_sink_app.rb create mode 100644 workers/ruby/projects/harness.rb create mode 100644 workers/ruby/projects/harness/api/api_pb.rb create mode 100644 workers/ruby/projects/harness/api/api_services_pb.rb create mode 100644 workers/ruby/projects/harness/client.rb create mode 100644 workers/ruby/projects/harness/helpers.rb create mode 100644 workers/ruby/projects/harness/main.rb create mode 100644 workers/ruby/projects/harness/project.rb create mode 100644 workers/ruby/projects/harness/tests/test_project.rb create mode 100644 workers/ruby/projects/harness/tests/test_worker.rb create mode 100644 workers/ruby/projects/harness/worker.rb diff --git a/cmd/dev/test.go b/cmd/dev/test.go index 16e34303..be4164ce 100644 --- a/cmd/dev/test.go +++ b/cmd/dev/test.go @@ -92,6 +92,11 @@ func runTestWorker(ctx context.Context, language string) error { return err } } + if language == "ruby" { + if err := runRubyHarnessTests(ctx, repoDir); err != nil { + return err + } + } return testWorkerLocally(ctx, repoDir, language, sdkVersion) } @@ -125,6 +130,41 @@ func runTypeScriptHarnessTests(ctx context.Context, repoDir string) error { return nil } +func runRubyHarnessTests(ctx context.Context, repoDir string) error { + workerDir := filepath.Join(repoDir, "workers", "ruby") + rubyVersion, err := getVersion("ruby") + if err != nil { + return err + } + if err := checkMise(); err != nil { + return err + } + fmt.Println("Running Ruby harness tests...") + testFiles := []string{ + "projects/harness/tests/test_worker.rb", + "projects/harness/tests/test_project.rb", + } + for _, testFile := range testFiles { + fmt.Println("Running Ruby harness test:", testFile) + if err := runCommandInDir( + ctx, + workerDir, + "mise", + "exec", + "ruby@"+rubyVersion, + "--", + "bundle", + "exec", + "ruby", + testFile, + ); err != nil { + return fmt.Errorf("failed Ruby harness tests: %w", err) + } + } + fmt.Println("✅ Ruby harness tests completed successfully!") + return nil +} + func testWorkerLocally(ctx context.Context, repoDir, language, sdkVersion string) error { args := []string{ "go", "run", "./cmd", "run-scenario-with-worker", diff --git a/workers/ruby/.rubocop.yml b/workers/ruby/.rubocop.yml index 98762d82..e7538bc5 100644 --- a/workers/ruby/.rubocop.yml +++ b/workers/ruby/.rubocop.yml @@ -3,6 +3,7 @@ AllCops: NewCops: enable Exclude: - "protos/**/*" + - "projects/harness/api/**/*" - "vendor/**/*" - "omes-temp-*/**/*" diff --git a/workers/ruby/Gemfile b/workers/ruby/Gemfile index 58ce25b8..9e1ff81e 100644 --- a/workers/ruby/Gemfile +++ b/workers/ruby/Gemfile @@ -3,5 +3,7 @@ source 'https://rubygems.org' gemspec group :development do + gem 'grpc-tools', '~> 1.80' + gem 'minitest' gem 'rubocop', '~> 1.0' end diff --git a/workers/ruby/Gemfile.lock b/workers/ruby/Gemfile.lock index dde9193e..8511b2c9 100644 --- a/workers/ruby/Gemfile.lock +++ b/workers/ruby/Gemfile.lock @@ -3,6 +3,7 @@ PATH specs: omes (0.1.0) google-protobuf (~> 4.0) + grpc (~> 1.80) temporalio (~> 1.3) GEM @@ -12,6 +13,7 @@ GEM public_suffix (>= 2.0.2, < 8.0) ast (2.4.3) bigdecimal (4.0.1) + drb (2.2.3) google-protobuf (4.34.1) bigdecimal rake (~> 13.3) @@ -39,6 +41,36 @@ GEM google-protobuf (4.34.1-x86_64-linux-musl) bigdecimal rake (~> 13.3) + googleapis-common-protos-types (1.22.0) + google-protobuf (~> 4.26) + grpc (1.80.0) + google-protobuf (>= 3.25, < 5.0) + googleapis-common-protos-types (~> 1.0) + grpc (1.80.0-aarch64-linux-gnu) + google-protobuf (>= 3.25, < 5.0) + googleapis-common-protos-types (~> 1.0) + grpc (1.80.0-aarch64-linux-musl) + google-protobuf (>= 3.25, < 5.0) + googleapis-common-protos-types (~> 1.0) + grpc (1.80.0-arm64-darwin) + google-protobuf (>= 3.25, < 5.0) + googleapis-common-protos-types (~> 1.0) + grpc (1.80.0-x86-linux-gnu) + google-protobuf (>= 3.25, < 5.0) + googleapis-common-protos-types (~> 1.0) + grpc (1.80.0-x86-linux-musl) + google-protobuf (>= 3.25, < 5.0) + googleapis-common-protos-types (~> 1.0) + grpc (1.80.0-x86_64-darwin) + google-protobuf (>= 3.25, < 5.0) + googleapis-common-protos-types (~> 1.0) + grpc (1.80.0-x86_64-linux-gnu) + google-protobuf (>= 3.25, < 5.0) + googleapis-common-protos-types (~> 1.0) + grpc (1.80.0-x86_64-linux-musl) + google-protobuf (>= 3.25, < 5.0) + googleapis-common-protos-types (~> 1.0) + grpc-tools (1.80.0) json (2.19.2) json-schema (6.2.0) addressable (~> 2.8) @@ -48,6 +80,9 @@ GEM logger (1.7.0) mcp (0.9.0) json-schema (>= 4.1) + minitest (6.0.5) + drb (~> 2.0) + prism (~> 1.5) parallel (1.27.0) parser (3.3.10.2) ast (~> 2.4.1) @@ -113,6 +148,8 @@ PLATFORMS x86_64-linux-musl DEPENDENCIES + grpc-tools (~> 1.80) + minitest omes! rubocop (~> 1.0) @@ -120,6 +157,7 @@ CHECKSUMS addressable (2.8.9) sha256=cc154fcbe689711808a43601dee7b980238ce54368d23e127421753e46895485 ast (2.4.3) sha256=954615157c1d6a382bc27d690d973195e79db7f55e9765ac7c481c60bdb4d383 bigdecimal (4.0.1) sha256=8b07d3d065a9f921c80ceaea7c9d4ae596697295b584c296fe599dd0ad01c4a7 + drb (2.2.3) sha256=0b00d6fdb50995fe4a45dea13663493c841112e4068656854646f418fda13373 google-protobuf (4.34.1) sha256=347181542b8d659c60f028fa3791c9cccce651a91ad27782dbc5c5e374796cdc google-protobuf (4.34.1-aarch64-linux-gnu) sha256=f9c07607dc139c895f2792a7740fcd01cd94d4d7b0e0a939045b50d7999f0b1d google-protobuf (4.34.1-aarch64-linux-musl) sha256=db58e5a4a492b43c6614486aea31b7fb86955b175d1d48f28ebf388f058d78a9 @@ -129,12 +167,24 @@ CHECKSUMS google-protobuf (4.34.1-x86_64-darwin) sha256=4dc498376e218871613589c4d872400d42ad9ae0c700bdb2606fe1c77a593075 google-protobuf (4.34.1-x86_64-linux-gnu) sha256=87088c9fd8e47b5b40ca498fc1195add6149e941ff7e81c532a5b0b8876d4cc9 google-protobuf (4.34.1-x86_64-linux-musl) sha256=8c0e91436fbe504ffc64f0bd621f2e69adbcce8ed2c58439d7a21117069cfdd7 + googleapis-common-protos-types (1.22.0) sha256=f97492b77bd6da0018c860d5004f512fe7cd165554d7019a8f4df6a56fbfc4c7 + grpc (1.80.0) sha256=2ded0c8bc3a1f3d34b8c790e00dd0120768ba0e9f9fd841e1dc67f7a2566d07d + grpc (1.80.0-aarch64-linux-gnu) sha256=1c15048887224575cb38026fea5b9abb14ae955bfce8beb0701e0946959a8520 + grpc (1.80.0-aarch64-linux-musl) sha256=9568f17e848d873fffd27351d4d1c918ea621e901b3dfcd62d7ff86dbae286a3 + grpc (1.80.0-arm64-darwin) sha256=c4b5871ad7673c526b64e54e70a99d84e35e2c26a1fc9a91f9c3341c7821e0c7 + grpc (1.80.0-x86-linux-gnu) sha256=b9fbce2480b2f1e965a6241a5df033c0d20a19f99a637e3cb8a12a6f5c3e68c6 + grpc (1.80.0-x86-linux-musl) sha256=db943737f134e61d88709ba697fe0830e794eb4a2d12bd2bb9203afefd334f21 + grpc (1.80.0-x86_64-darwin) sha256=4484d1280a43f94e8f1ab6ae53d282a4832f8ffb61b6996f43fc50716f464f1c + grpc (1.80.0-x86_64-linux-gnu) sha256=25ba8beb5438152fb60656161dc729c1258c6963ec568361f601e19b2fb7ac23 + grpc (1.80.0-x86_64-linux-musl) sha256=15a62e816c690bece9f5fd320a4b61aa735a2390daa8141596672a6e820ff342 + grpc-tools (1.80.0) sha256=49076fbc7b34556365694202ed7ffd1e959e68ba8c339e99b437ba2a985a2cf2 json (2.19.2) sha256=e7e1bd318b2c37c4ceee2444841c86539bc462e81f40d134cf97826cb14e83cf json-schema (6.2.0) sha256=e8bff46ed845a22c1ab2bd0d7eccf831c01fe23bb3920caa4c74db4306813666 language_server-protocol (3.17.0.5) sha256=fd1e39a51a28bf3eec959379985a72e296e9f9acfce46f6a79d31ca8760803cc lint_roller (1.1.0) sha256=2c0c845b632a7d172cb849cc90c1bce937a28c5c8ccccb50dfd46a485003cc87 logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203 mcp (0.9.0) sha256=a0a3737b0ac9df0772f4ef7e2b013c260ddbcf217a5d50a66bff0baeddf03e47 + minitest (6.0.5) sha256=f007d7246bf4feea549502842cd7c6aba8851cdc9c90ba06de9c476c0d01155c omes (0.1.0) parallel (1.27.0) sha256=4ac151e1806b755fb4e2dc2332cbf0e54f2e24ba821ff2d3dcf86bf6dc4ae130 parser (3.3.10.2) sha256=6f60c84aa4bdcedb6d1a2434b738fe8a8136807b6adc8f7f53b97da9bc4e9357 diff --git a/workers/ruby/kitchen_sink_app.rb b/workers/ruby/kitchen_sink_app.rb new file mode 100644 index 00000000..77834f5f --- /dev/null +++ b/workers/ruby/kitchen_sink_app.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +require 'temporalio/worker' +require_relative 'activities' +require_relative 'kitchen_sink' +require_relative 'projects/harness' + +module KitchenSinkApp + module_function + + def app + Harness::App.new( + worker: method(:build_worker), + client_factory: Harness.method(:default_client_factory) + ) + end + + def build_worker(client, context) + Temporalio::Worker.new( + client: client, + task_queue: context.task_queue, + workflows: [KitchenSinkWorkflow], + activities: [ + NoopActivity.new, + DelayActivity.new, + PayloadActivity.new, + RetryableErrorActivity.new, + TimeoutActivity.new, + HeartbeatActivity.new, + ClientActivity.new(client, err_on_unimplemented: context.err_on_unimplemented) + ], + logger: context.logger, + **context.worker_kwargs + ) + end +end diff --git a/workers/ruby/omes.gemspec b/workers/ruby/omes.gemspec index 613ce8ee..abf84825 100644 --- a/workers/ruby/omes.gemspec +++ b/workers/ruby/omes.gemspec @@ -7,6 +7,7 @@ Gem::Specification.new do |s| s.license = 'MIT' s.files = Dir['**/*.rb'] s.require_paths = ['.'] + s.add_dependency 'grpc', '~> 1.80' s.add_dependency 'google-protobuf', '~> 4.0' s.add_dependency 'temporalio', '~> 1.3' s.metadata['rubygems_mfa_required'] = 'true' diff --git a/workers/ruby/projects/harness.rb b/workers/ruby/projects/harness.rb new file mode 100644 index 00000000..e55767b0 --- /dev/null +++ b/workers/ruby/projects/harness.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +require_relative 'harness/client' +require_relative 'harness/helpers' +require_relative 'harness/project' +require_relative 'harness/worker' +require_relative 'harness/main' diff --git a/workers/ruby/projects/harness/api/api_pb.rb b/workers/ruby/projects/harness/api/api_pb.rb new file mode 100644 index 00000000..5bd81124 --- /dev/null +++ b/workers/ruby/projects/harness/api/api_pb.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: harness/api/api.proto + +require 'google/protobuf' + + +descriptor_data = "\n\x15harness/api/api.proto\x12\x19temporal.omes.projects.v1\"\xcd\x01\n\x0e\x43onnectOptions\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x16\n\x0eserver_address\x18\x02 \x01(\t\x12\x13\n\x0b\x61uth_header\x18\x03 \x01(\t\x12\x12\n\nenable_tls\x18\x04 \x01(\x08\x12\x15\n\rtls_cert_path\x18\x05 \x01(\t\x12\x14\n\x0ctls_key_path\x18\x06 \x01(\t\x12\x17\n\x0ftls_server_name\x18\x07 \x01(\t\x12!\n\x19\x64isable_host_verification\x18\x08 \x01(\x08\"\xa0\x01\n\x0bInitRequest\x12\x14\n\x0c\x65xecution_id\x18\x01 \x01(\t\x12\x0e\n\x06run_id\x18\x02 \x01(\t\x12\x12\n\ntask_queue\x18\x03 \x01(\t\x12\x42\n\x0f\x63onnect_options\x18\x04 \x01(\x0b\x32).temporal.omes.projects.v1.ConnectOptions\x12\x13\n\x0b\x63onfig_json\x18\x05 \x01(\x0c\"\x0e\n\x0cInitResponse\"H\n\x0e\x45xecuteRequest\x12\x11\n\titeration\x18\x01 \x01(\x03\x12\x12\n\ntask_queue\x18\x02 \x01(\t\x12\x0f\n\x07payload\x18\x03 \x01(\x0c\"\x11\n\x0f\x45xecuteResponse2\xcf\x01\n\x0eProjectService\x12Y\n\x04Init\x12&.temporal.omes.projects.v1.InitRequest\x1a\'.temporal.omes.projects.v1.InitResponse\"\x00\x12\x62\n\x07\x45xecute\x12).temporal.omes.projects.v1.ExecuteRequest\x1a*.temporal.omes.projects.v1.ExecuteResponse\"\x00\x42 Logger::FATAL, + 'FATAL' => Logger::FATAL, + 'ERROR' => Logger::ERROR, + 'WARN' => Logger::WARN, + 'INFO' => Logger::INFO, + 'DEBUG' => Logger::DEBUG, + 'NOTSET' => Logger::DEBUG + }.freeze + + module_function + + def configure_logger(log_level, log_encoding) + logger = Logger.new($stderr) + logger.level = NAME_TO_LEVEL.fetch(log_level.upcase, Logger::INFO) + if log_encoding == 'json' + logger.formatter = proc do |severity, datetime, _progname, message| + "#{JSON.generate(message: message, level: severity, timestamp: datetime.iso8601)}\n" + end + end + logger + end + end +end diff --git a/workers/ruby/projects/harness/main.rb b/workers/ruby/projects/harness/main.rb new file mode 100644 index 00000000..31371898 --- /dev/null +++ b/workers/ruby/projects/harness/main.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +require_relative 'project' +require_relative 'worker' + +module Harness + class App + attr_reader :worker, :client_factory, :project + + def initialize(worker:, client_factory:, project: nil) + @worker = worker + @client_factory = client_factory + @project = project + end + end + + def self.run(app, argv = ARGV) + argv = Array(argv).dup + if argv.empty? || argv.first == 'worker' + worker_argv = argv.first == 'worker' ? argv.drop(1) : argv + WorkerCLI.run_cli(app.worker, app.client_factory, worker_argv) + elsif argv.first == 'project-server' + if app.project.nil? + raise SystemExit, 'Wanted project-server but no project handlers registered for this app' + end + + ProjectCLI.run_cli(app.project, app.client_factory, argv.drop(1)) + else + raise SystemExit, "Unknown command: #{argv.first(1)}. Expected 'worker' or 'project-server'" + end + end +end diff --git a/workers/ruby/projects/harness/project.rb b/workers/ruby/projects/harness/project.rb new file mode 100644 index 00000000..0866e6c5 --- /dev/null +++ b/workers/ruby/projects/harness/project.rb @@ -0,0 +1,161 @@ +# frozen_string_literal: true + +require 'logger' +require 'optparse' +require 'grpc' + +$LOAD_PATH.unshift(File.expand_path('..', __dir__)) unless $LOAD_PATH.include?(File.expand_path('..', __dir__)) + +require_relative 'client' +require_relative 'api/api_pb' +require_relative 'api/api_services_pb' + +module Harness + ProjectRunMetadata = Struct.new( + :run_id, + :execution_id, + keyword_init: true + ) + + ProjectInitContext = Struct.new( + :logger, + :run, + :task_queue, + :config_json, + keyword_init: true + ) + + ProjectExecuteContext = Struct.new( + :logger, + :run, + :task_queue, + :iteration, + :payload, + keyword_init: true + ) + + ProjectHandlers = Struct.new( + :execute, + :init, + keyword_init: true + ) + + class ProjectServiceServer < Temporal::Omes::Projects::V1::ProjectService::Service + def initialize(handlers, client_factory) + @handlers = handlers + @client_factory = client_factory + @client = nil + @run = nil + @logger = Logger.new($stderr) + end + + def init(request, _call) + raise grpc_status(GRPC::Core::StatusCodes::INVALID_ARGUMENT, 'task_queue required') if request.task_queue.to_s.empty? + raise grpc_status(GRPC::Core::StatusCodes::INVALID_ARGUMENT, 'execution_id required') if request.execution_id.to_s.empty? + raise grpc_status(GRPC::Core::StatusCodes::INVALID_ARGUMENT, 'run_id required') if request.run_id.to_s.empty? + + connect_options = request.connect_options || Temporal::Omes::Projects::V1::ConnectOptions.new + raise grpc_status(GRPC::Core::StatusCodes::INVALID_ARGUMENT, 'server_address required') if connect_options.server_address.to_s.empty? + raise grpc_status(GRPC::Core::StatusCodes::INVALID_ARGUMENT, 'namespace required') if connect_options.namespace.to_s.empty? + + begin + config = ClientHelpers.build_client_config( + server_address: connect_options.server_address, + namespace: connect_options.namespace, + auth_header: connect_options.auth_header, + tls: connect_options.enable_tls, + tls_cert_path: connect_options.tls_cert_path, + tls_key_path: connect_options.tls_key_path, + tls_server_name: connect_options.tls_server_name.to_s.empty? ? nil : connect_options.tls_server_name, + disable_host_verification: connect_options.disable_host_verification + ) + rescue ArgumentError => error + raise grpc_status(GRPC::Core::StatusCodes::INVALID_ARGUMENT, error.message) + end + + begin + client = @client_factory.call(config) + rescue StandardError => error + raise grpc_status(GRPC::Core::StatusCodes::INTERNAL, "failed to create client: #{error}") + end + + run = ProjectRunMetadata.new( + run_id: request.run_id, + execution_id: request.execution_id + ) + + if @handlers.init + begin + @handlers.init.call( + client, + ProjectInitContext.new( + logger: @logger, + run: run, + task_queue: request.task_queue, + config_json: request.config_json + ) + ) + rescue StandardError => error + raise grpc_status(GRPC::Core::StatusCodes::INTERNAL, "init handler failed: #{error}") + end + end + + @client = client + @run = run + Temporal::Omes::Projects::V1::InitResponse.new + rescue GRPC::BadStatus + raise + end + + def execute(request, _call) + raise grpc_status(GRPC::Core::StatusCodes::INVALID_ARGUMENT, 'task_queue required') if request.task_queue.to_s.empty? + raise grpc_status(GRPC::Core::StatusCodes::FAILED_PRECONDITION, 'Init must be called before Execute') if @client.nil? || @run.nil? + + @handlers.execute.call( + @client, + ProjectExecuteContext.new( + logger: @logger, + run: @run, + task_queue: request.task_queue, + iteration: request.iteration, + payload: request.payload + ) + ) + Temporal::Omes::Projects::V1::ExecuteResponse.new + rescue GRPC::BadStatus + raise + rescue StandardError => error + raise grpc_status(GRPC::Core::StatusCodes::INTERNAL, "execute handler failed: #{error}") + end + + private + + def grpc_status(code, details) + GRPC::BadStatus.new_status_exception(code, details) + end + + end + + module ProjectCLI + module_function + + def run_cli(handlers, client_factory, argv) + options = { port: 8080 } + parser = OptionParser.new do |opts| + opts.banner = 'Usage: runner.rb project-server [options]' + opts.on('--port PORT', Integer, 'gRPC listen port') { |value| options[:port] = value } + end + parser.parse!(Array(argv).dup) + serve(handlers, client_factory, options[:port]) + end + + def serve(handlers, client_factory, port) + logger = Logger.new($stderr) + server = GRPC::RpcServer.new + server.handle(ProjectServiceServer.new(handlers, client_factory)) + server.add_http2_port("0.0.0.0:#{port}", :this_port_is_insecure) + logger.info("Project server listening on port #{port}") + server.run_till_terminated_or_interrupted(['SIGINT']) + end + end +end diff --git a/workers/ruby/projects/harness/tests/test_project.rb b/workers/ruby/projects/harness/tests/test_project.rb new file mode 100644 index 00000000..2f9d1349 --- /dev/null +++ b/workers/ruby/projects/harness/tests/test_project.rb @@ -0,0 +1,290 @@ +# frozen_string_literal: true + +require 'minitest/autorun' +require 'securerandom' +require 'temporalio/testing/workflow_environment' +require 'temporalio/worker' +require 'temporalio/workflow' +require 'grpc' +require_relative '../../harness' + +class HarnessProjectTest < Minitest::Test + class ProjectHarnessEchoWorkflow < Temporalio::Workflow::Definition + def execute(payload) + payload + end + end + + def test_init_rejects_invalid_tls_configuration + server = Harness::ProjectServiceServer.new( + Harness::ProjectHandlers.new(execute: ->(_client, _context) {}), + ->(_config) { Object.new } + ) + + error = assert_raises(GRPC::BadStatus) do + server.init( + make_init_request( + connect_options: Temporal::Omes::Projects::V1::ConnectOptions.new( + namespace: 'default', + server_address: 'localhost:7233', + enable_tls: true, + tls_cert_path: '/tmp/cert.pem', + tls_key_path: '' + ) + ), + nil + ) + end + + assert_equal GRPC::Core::StatusCodes::INVALID_ARGUMENT, error.code + assert_equal 'Client cert specified, but not client key!', error.details + end + + def test_init_passes_run_metadata_to_handler + client = Object.new + init_calls = [] + server = Harness::ProjectServiceServer.new( + Harness::ProjectHandlers.new( + execute: ->(_handler_client, _context) {}, + init: lambda do |handler_client, context| + init_calls << [handler_client, context] + end + ), + ->(given_config) do + assert_equal 'localhost:7233', given_config.target_host + assert_equal 'default', given_config.namespace + assert_equal 'token', given_config.api_key + assert_nil given_config.tls + assert_instance_of Temporalio::Runtime, given_config.runtime + client + end + ) + + response = server.init(make_init_request, nil) + assert_instance_of Temporal::Omes::Projects::V1::InitResponse, response + + assert_equal 1, init_calls.length + handler_client, init_context = init_calls.first + assert_same client, handler_client + assert_equal 'run-id', init_context.run.run_id + assert_equal 'exec-id', init_context.run.execution_id + assert_equal 'task-queue', init_context.task_queue + assert_equal '{"hello":"world"}'.b, init_context.config_json + end + + def test_execute_requires_init + server = Harness::ProjectServiceServer.new( + Harness::ProjectHandlers.new(execute: ->(_client, _context) {}), + ->(_config) { Object.new } + ) + + error = assert_raises(GRPC::BadStatus) do + server.execute(make_execute_request, nil) + end + + assert_equal GRPC::Core::StatusCodes::FAILED_PRECONDITION, error.code + assert_equal 'Init must be called before Execute', error.details + end + + def test_execute_passes_iteration_payload_and_run_metadata + client = Object.new + execute_calls = [] + server = Harness::ProjectServiceServer.new( + Harness::ProjectHandlers.new( + execute: lambda do |handler_client, context| + execute_calls << [handler_client, context] + end + ), + ->(_config) { client } + ) + + server.init(make_init_request, nil) + response = server.execute(make_execute_request, nil) + assert_instance_of Temporal::Omes::Projects::V1::ExecuteResponse, response + + assert_equal 1, execute_calls.length + handler_client, execute_context = execute_calls.first + assert_same client, handler_client + assert_equal 7, execute_context.iteration + assert_equal 'payload'.b, execute_context.payload + assert_equal 'task-queue', execute_context.task_queue + assert_equal 'run-id', execute_context.run.run_id + assert_equal 'exec-id', execute_context.run.execution_id + end + + def test_client_factory_failure_maps_to_internal_error + server = Harness::ProjectServiceServer.new( + Harness::ProjectHandlers.new(execute: ->(_client, _context) {}), + ->(_config) { raise 'boom' } + ) + + error = assert_raises(GRPC::BadStatus) do + server.init(make_init_request, nil) + end + + assert_equal GRPC::Core::StatusCodes::INTERNAL, error.code + assert_equal 'failed to create client: boom', error.details + end + + def test_init_handler_failure_does_not_leave_server_initialized + server = Harness::ProjectServiceServer.new( + Harness::ProjectHandlers.new( + execute: ->(_client, _context) {}, + init: ->(_client, _context) { raise 'bad init' } + ), + ->(_config) { Object.new } + ) + + error = assert_raises(GRPC::BadStatus) do + server.init(make_init_request, nil) + end + + assert_equal GRPC::Core::StatusCodes::INTERNAL, error.code + assert_equal 'init handler failed: bad init', error.details + + execute_error = assert_raises(GRPC::BadStatus) do + server.execute(make_execute_request, nil) + end + assert_equal GRPC::Core::StatusCodes::FAILED_PRECONDITION, execute_error.code + assert_equal 'Init must be called before Execute', execute_error.details + end + + def test_execute_handler_failure_maps_to_internal_error + server = Harness::ProjectServiceServer.new( + Harness::ProjectHandlers.new( + execute: ->(_client, _context) { raise 'bad execute' } + ), + ->(_config) { Object.new } + ) + + server.init(make_init_request, nil) + + error = assert_raises(GRPC::BadStatus) do + server.execute(make_execute_request, nil) + end + + assert_equal GRPC::Core::StatusCodes::INTERNAL, error.code + assert_equal 'execute handler failed: bad execute', error.details + end + + def test_project_server_executes_workflow_against_real_temporal_server + events = [] + task_queue = "project-harness-e2e-#{SecureRandom.uuid}" + + init_handler = lambda do |handler_client, context| + events << [:init, handler_client, context] + end + execute_handler = lambda do |handler_client, context| + result = handler_client.execute_workflow( + ProjectHarnessEchoWorkflow, + context.payload, + id: "#{context.run.execution_id}-#{context.iteration}", + task_queue: context.task_queue + ) + events << [:execute, handler_client, context, result] + end + + cli_path = ENV['TEMPORAL_CLI_PATH'] + cli_path = `which temporal`.strip if cli_path.to_s.empty? + raise 'temporal CLI not found' if cli_path.to_s.empty? + + Temporalio::Testing::WorkflowEnvironment.start_local( + dev_server_existing_path: cli_path + ) do |env| + worker = Temporalio::Worker.new( + client: env.client, + task_queue: task_queue, + workflows: [ProjectHarnessEchoWorkflow] + ) + + worker.run do + grpc_server = GRPC::RpcServer.new + grpc_server.handle( + Harness::ProjectServiceServer.new( + Harness::ProjectHandlers.new(execute: execute_handler, init: init_handler), + Harness.method(:default_client_factory) + ) + ) + port = grpc_server.add_http2_port('127.0.0.1:0', :this_port_is_insecure) + refute_equal 0, port + + server_thread = Thread.new { grpc_server.run } + begin + assert grpc_server.wait_till_running(5) + stub = Temporal::Omes::Projects::V1::ProjectService::Stub.new( + "127.0.0.1:#{port}", + :this_channel_is_insecure + ) + + stub.init( + make_init_request( + task_queue: task_queue, + connect_options: Temporal::Omes::Projects::V1::ConnectOptions.new( + namespace: 'default', + server_address: env.client.connection.target_host + ) + ) + ) + stub.execute(make_execute_request(task_queue: task_queue)) + ensure + grpc_server.stop if grpc_server.running? + server_thread.join + end + end + end + + assert_equal 2, events.length + init_kind, init_client, init_context = events[0] + execute_kind, execute_client, execute_context, execute_result = events[1] + + assert_equal :init, init_kind + assert_equal 'run-id', init_context.run.run_id + assert_equal 'exec-id', init_context.run.execution_id + assert_equal task_queue, init_context.task_queue + assert_equal '{"hello":"world"}'.b, init_context.config_json + + assert_equal :execute, execute_kind + assert_same init_client, execute_client + assert_equal 'run-id', execute_context.run.run_id + assert_equal 'exec-id', execute_context.run.execution_id + assert_equal task_queue, execute_context.task_queue + assert_equal 7, execute_context.iteration + assert_equal 'payload'.b, execute_context.payload + assert_equal 'payload'.b, execute_result + end + + private + + def make_init_request( + execution_id: 'exec-id', + run_id: 'run-id', + task_queue: 'task-queue', + connect_options: nil, + config_json: '{"hello":"world"}'.b + ) + Temporal::Omes::Projects::V1::InitRequest.new( + execution_id: execution_id, + run_id: run_id, + task_queue: task_queue, + connect_options: connect_options || Temporal::Omes::Projects::V1::ConnectOptions.new( + namespace: 'default', + server_address: 'localhost:7233', + auth_header: 'Bearer token', + enable_tls: false + ), + config_json: config_json + ) + end + + def make_execute_request( + iteration: 7, + task_queue: 'task-queue', + payload: 'payload'.b + ) + Temporal::Omes::Projects::V1::ExecuteRequest.new( + iteration: iteration, + task_queue: task_queue, + payload: payload + ) + end +end diff --git a/workers/ruby/projects/harness/tests/test_worker.rb b/workers/ruby/projects/harness/tests/test_worker.rb new file mode 100644 index 00000000..0239a687 --- /dev/null +++ b/workers/ruby/projects/harness/tests/test_worker.rb @@ -0,0 +1,128 @@ +# frozen_string_literal: true + +require 'logger' +require 'minitest/autorun' +require_relative '../../harness' + +class HarnessWorkerTest < Minitest::Test + FakeRunnableWorker = Struct.new( + :on_run, + :run_calls, + :shutdown_calls, + :received_shutdown_signals, + keyword_init: true + ) do + def run(shutdown_signals: []) + self.run_calls += 1 + self.received_shutdown_signals = shutdown_signals + on_run.call + end + + def shutdown + self.shutdown_calls += 1 + end + end + + def test_run_passes_shared_client_and_context_to_each_worker_factory + client = Object.new + built_config = nil + worker_factory_calls = [] + created_workers = [Object.new, Object.new] + captured_workers = nil + captured_kwargs = nil + + worker_factory = lambda do |given_client, context| + worker_factory_calls << [given_client, context] + created_workers.fetch(worker_factory_calls.length - 1) + end + client_factory = lambda do |config| + built_config = config + client + end + + with_stubbed_run_all(lambda do |*workers, **kwargs| + captured_workers = workers + captured_kwargs = kwargs + end) do + Harness::WorkerCLI.run( + worker_factory, + client_factory, + [ + '--task-queue', 'omes', + '--task-queue-suffix-index-start', '1', + '--task-queue-suffix-index-end', '2' + ] + ) + end + + assert_equal 'localhost:7233', built_config.target_host + assert_equal 'default', built_config.namespace + assert_nil built_config.api_key + assert_nil built_config.tls + assert_instance_of Temporalio::Runtime, built_config.runtime + assert_equal created_workers, captured_workers + assert_equal({ shutdown_signals: ['SIGINT'] }, captured_kwargs) + assert_equal 2, worker_factory_calls.length + assert_same client, worker_factory_calls[0][0] + assert_same client, worker_factory_calls[1][0] + assert_equal 'omes-1', worker_factory_calls[0][1].task_queue + assert_equal 'omes-2', worker_factory_calls[1][1].task_queue + assert_instance_of Logger, worker_factory_calls[0][1].logger + end + + def test_run_workers_shuts_down_all_workers_when_one_fails + boom = RuntimeError.new('boom') + failing_worker = FakeRunnableWorker.new( + on_run: -> { raise boom }, + run_calls: 0, + shutdown_calls: 0, + received_shutdown_signals: nil + ) + successful_worker = FakeRunnableWorker.new( + on_run: -> { nil }, + run_calls: 0, + shutdown_calls: 0, + received_shutdown_signals: nil + ) + + error = with_stubbed_run_all(lambda do |*workers, **kwargs| + first_error = nil + workers.each do |worker| + begin + worker.run(shutdown_signals: kwargs[:shutdown_signals]) + rescue StandardError => raised_error + first_error ||= raised_error + end + end + workers.each(&:shutdown) + raise first_error if first_error + end) do + assert_raises(RuntimeError) do + Harness::WorkerCLI.run_workers([failing_worker, successful_worker]) + end + end + + assert_same boom, error + assert_equal ['SIGINT'], failing_worker.received_shutdown_signals + assert_equal ['SIGINT'], successful_worker.received_shutdown_signals + assert_equal 1, failing_worker.run_calls + assert_equal 1, successful_worker.run_calls + assert_equal 1, failing_worker.shutdown_calls + assert_equal 1, successful_worker.shutdown_calls + end + + private + + def with_stubbed_run_all(stub_implementation) + singleton = Temporalio::Worker.singleton_class + singleton.send(:alias_method, :__original_run_all_for_test, :run_all) + singleton.send(:define_method, :run_all, &stub_implementation) + yield + ensure + if singleton.method_defined?(:__original_run_all_for_test) + singleton.send(:remove_method, :run_all) + singleton.send(:alias_method, :run_all, :__original_run_all_for_test) + singleton.send(:remove_method, :__original_run_all_for_test) + end + end +end diff --git a/workers/ruby/projects/harness/worker.rb b/workers/ruby/projects/harness/worker.rb new file mode 100644 index 00000000..5fd97e9a --- /dev/null +++ b/workers/ruby/projects/harness/worker.rb @@ -0,0 +1,163 @@ +# frozen_string_literal: true + +require 'optparse' +require 'temporalio/worker' +require_relative 'client' +require_relative 'helpers' + +module Harness + WorkerContext = Struct.new( + :logger, + :task_queue, + :err_on_unimplemented, + :worker_kwargs, + keyword_init: true + ) + + module WorkerCLI + module_function + + def run_cli(worker_factory, client_factory, argv) + run(worker_factory, client_factory, argv) + end + + def run(worker_factory, client_factory, argv) + options = default_options + build_parser(options).parse!(Array(argv).dup) + + raise ArgumentError, 'Task queue suffix start after end' if options[:task_queue_suffix_index_start] > options[:task_queue_suffix_index_end] + + logger = Helpers.configure_logger(options[:log_level], options[:log_encoding]) + config = ClientHelpers.build_client_config( + server_address: options[:server_address], + namespace: options[:namespace], + auth_header: options[:auth_header], + tls: options[:tls], + tls_cert_path: options[:tls_cert_path], + tls_key_path: options[:tls_key_path], + prom_listen_address: options[:prom_listen_address] + ) + client = client_factory.call(config) + + task_queues = build_task_queues( + logger, + options[:task_queue], + options[:task_queue_suffix_index_start], + options[:task_queue_suffix_index_end] + ) + worker_kwargs = build_worker_kwargs(options) + workers = task_queues.map do |task_queue| + worker_factory.call( + client, + WorkerContext.new( + logger: logger, + task_queue: task_queue, + err_on_unimplemented: options[:err_on_unimplemented], + worker_kwargs: worker_kwargs + ) + ) + end + run_workers(workers) + end + + def run_workers(workers) + # The Ruby SDK already owns/supports coordinated multi-worker shutdown. + Temporalio::Worker.run_all(*workers, shutdown_signals: ['SIGINT']) + end + + def build_parser(options) + OptionParser.new do |opts| + opts.banner = 'Usage: runner.rb [options]' + + opts.on('-q', '--task-queue QUEUE', 'Task queue to use') { |value| options[:task_queue] = value } + opts.on('--task-queue-suffix-index-start N', Integer) { |value| options[:task_queue_suffix_index_start] = value } + opts.on('--task-queue-suffix-index-end N', Integer) { |value| options[:task_queue_suffix_index_end] = value } + opts.on('--max-concurrent-activity-pollers N', Integer) { |value| options[:max_concurrent_activity_pollers] = value } + opts.on('--max-concurrent-workflow-pollers N', Integer) { |value| options[:max_concurrent_workflow_pollers] = value } + opts.on('--activity-poller-autoscale-max N', Integer) { |value| options[:activity_poller_autoscale_max] = value } + opts.on('--workflow-poller-autoscale-max N', Integer) { |value| options[:workflow_poller_autoscale_max] = value } + opts.on('--max-concurrent-activities N', Integer) { |value| options[:max_concurrent_activities] = value } + opts.on('--max-concurrent-workflow-tasks N', Integer) { |value| options[:max_concurrent_workflow_tasks] = value } + opts.on('--activities-per-second N', Float) { |value| options[:activities_per_second] = value } + opts.on('--err-on-unimplemented BOOL') { |value| options[:err_on_unimplemented] = parse_bool(value) } + opts.on('--log-level LEVEL') { |value| options[:log_level] = value } + opts.on('--log-encoding ENCODING') { |value| options[:log_encoding] = value } + opts.on('-n', '--namespace NAMESPACE') { |value| options[:namespace] = value } + opts.on('-a', '--server-address ADDRESS') { |value| options[:server_address] = value } + opts.on('--tls BOOL') { |value| options[:tls] = parse_bool(value) } + opts.on('--tls-cert-path PATH') { |value| options[:tls_cert_path] = value } + opts.on('--tls-key-path PATH') { |value| options[:tls_key_path] = value } + opts.on('--prom-listen-address ADDRESS') { |value| options[:prom_listen_address] = value } + opts.on('--prom-handler-path PATH') { |_value| nil } + opts.on('--auth-header HEADER') { |value| options[:auth_header] = value } + opts.on('--build-id ID') { |_value| nil } + end + end + + def build_task_queues(logger, task_queue, suffix_start, suffix_end) + if suffix_end.zero? + logger.info("Ruby worker will run on task queue #{task_queue}") + return [task_queue] + end + + task_queues = (suffix_start..suffix_end).map { |index| "#{task_queue}-#{index}" } + logger.info("Ruby worker will run on #{task_queues.length} task queue(s)") + task_queues + end + + def build_worker_kwargs(options) + worker_kwargs = {} + if options[:activity_poller_autoscale_max] + worker_kwargs[:activity_task_poller_behavior] = Temporalio::Worker::PollerBehavior::Autoscaling.new( + maximum: options[:activity_poller_autoscale_max] + ) + elsif options[:max_concurrent_activity_pollers] + worker_kwargs[:max_concurrent_activity_task_polls] = options[:max_concurrent_activity_pollers] + end + + if options[:workflow_poller_autoscale_max] + worker_kwargs[:workflow_task_poller_behavior] = Temporalio::Worker::PollerBehavior::Autoscaling.new( + maximum: options[:workflow_poller_autoscale_max] + ) + elsif options[:max_concurrent_workflow_pollers] + worker_kwargs[:max_concurrent_workflow_task_polls] = options[:max_concurrent_workflow_pollers] + end + + worker_kwargs[:max_concurrent_activities] = options[:max_concurrent_activities] if options[:max_concurrent_activities] + worker_kwargs[:max_concurrent_workflow_tasks] = options[:max_concurrent_workflow_tasks] if options[:max_concurrent_workflow_tasks] + worker_kwargs[:max_activities_per_second] = options[:activities_per_second] if options[:activities_per_second] + worker_kwargs + end + + def parse_bool(value) + return value if value == true || value == false + + %w[true 1 yes].include?(value.to_s.downcase) + end + + def default_options + { + task_queue: 'omes', + task_queue_suffix_index_start: 0, + task_queue_suffix_index_end: 0, + max_concurrent_activity_pollers: nil, + max_concurrent_workflow_pollers: nil, + activity_poller_autoscale_max: nil, + workflow_poller_autoscale_max: nil, + max_concurrent_activities: nil, + max_concurrent_workflow_tasks: nil, + activities_per_second: nil, + err_on_unimplemented: false, + log_level: 'info', + log_encoding: 'console', + namespace: 'default', + server_address: 'localhost:7233', + tls: false, + tls_cert_path: '', + tls_key_path: '', + prom_listen_address: nil, + auth_header: '' + } + end + end +end diff --git a/workers/ruby/runner.rb b/workers/ruby/runner.rb index b974bc53..8bc5a4d8 100644 --- a/workers/ruby/runner.rb +++ b/workers/ruby/runner.rb @@ -1,192 +1,4 @@ -require 'logger' -require 'json' -require 'optparse' -require 'temporalio/client' -require 'temporalio/runtime' -require 'temporalio/worker' -require_relative 'kitchen_sink' -require_relative 'activities' +require_relative 'kitchen_sink_app' +require_relative 'projects/harness' -NAME_TO_LEVEL = { - 'PANIC' => Logger::FATAL, - 'FATAL' => Logger::FATAL, - 'ERROR' => Logger::ERROR, - 'WARN' => Logger::WARN, - 'INFO' => Logger::INFO, - 'DEBUG' => Logger::DEBUG, - 'NOTSET' => Logger::DEBUG -}.freeze - -options = { - task_queue: 'omes', - task_queue_suffix_index_start: 0, - task_queue_suffix_index_end: 0, - max_concurrent_activity_pollers: nil, - max_concurrent_workflow_pollers: nil, - activity_poller_autoscale_max: nil, - workflow_poller_autoscale_max: nil, - max_concurrent_activities: nil, - max_concurrent_workflow_tasks: nil, - activities_per_second: nil, - err_on_unimplemented: false, - log_level: 'info', - log_encoding: 'console', - namespace: 'default', - server_address: 'localhost:7233', - tls: false, - tls_cert_path: '', - tls_key_path: '', - prom_listen_address: nil, - auth_header: '' -} - -OptionParser.new do |opts| - opts.banner = 'Usage: runner.rb [options]' - - opts.on('-q', '--task-queue QUEUE', 'Task queue to use') { |v| options[:task_queue] = v } - opts.on('--task-queue-suffix-index-start N', Integer) { |v| options[:task_queue_suffix_index_start] = v } - opts.on('--task-queue-suffix-index-end N', Integer) { |v| options[:task_queue_suffix_index_end] = v } - opts.on('--max-concurrent-activity-pollers N', Integer) { |v| options[:max_concurrent_activity_pollers] = v } - opts.on('--max-concurrent-workflow-pollers N', Integer) { |v| options[:max_concurrent_workflow_pollers] = v } - opts.on('--activity-poller-autoscale-max N', Integer) { |v| options[:activity_poller_autoscale_max] = v } - opts.on('--workflow-poller-autoscale-max N', Integer) { |v| options[:workflow_poller_autoscale_max] = v } - opts.on('--max-concurrent-activities N', Integer) { |v| options[:max_concurrent_activities] = v } - opts.on('--max-concurrent-workflow-tasks N', Integer) { |v| options[:max_concurrent_workflow_tasks] = v } - opts.on('--activities-per-second N', Float) { |v| options[:activities_per_second] = v } - opts.on('--err-on-unimplemented BOOL') { |v| options[:err_on_unimplemented] = %w[true 1 yes].include?(v.downcase) } - opts.on('--log-level LEVEL') { |v| options[:log_level] = v } - opts.on('--log-encoding ENC') { |v| options[:log_encoding] = v } - opts.on('-n', '--namespace NS') { |v| options[:namespace] = v } - opts.on('-a', '--server-address ADDR') { |v| options[:server_address] = v } - opts.on('--tls BOOL') { |v| options[:tls] = %w[true 1 yes].include?(v.downcase) } - opts.on('--tls-cert-path PATH') { |v| options[:tls_cert_path] = v } - opts.on('--tls-key-path PATH') { |v| options[:tls_key_path] = v } - opts.on('--prom-listen-address ADDR') { |v| options[:prom_listen_address] = v } - opts.on('--prom-handler-path PATH') { |_v| nil } - opts.on('--auth-header HEADER') { |v| options[:auth_header] = v } - opts.on('--build-id ID') { |_v| nil } -end.parse! - -if options[:task_queue_suffix_index_start] > options[:task_queue_suffix_index_end] - abort 'Task queue suffix start after end' -end - -tls_options = nil -if !options[:tls_cert_path].empty? - abort 'Client cert specified, but not client key!' if options[:tls_key_path].empty? - tls_options = Temporalio::Client::Connection::TLSOptions.new( - client_cert: File.binread(options[:tls_cert_path]), - client_private_key: File.binread(options[:tls_key_path]) - ) -elsif !options[:tls_key_path].empty? - abort 'Client key specified, but not client cert!' -elsif options[:tls] - tls_options = Temporalio::Client::Connection::TLSOptions.new -end - -api_key = nil -api_key = options[:auth_header].delete_prefix('Bearer ') unless options[:auth_header].empty? - -logger = Logger.new($stderr) -logger.level = NAME_TO_LEVEL.fetch(options[:log_level].upcase, Logger::INFO) -if options[:log_encoding] == 'json' - logger.formatter = proc do |severity, datetime, _progname, msg| - "#{JSON.generate(message: msg, level: severity, timestamp: datetime.iso8601)}\n" - end -end - -prometheus = nil -if options[:prom_listen_address] - prometheus = Temporalio::Runtime::PrometheusMetricsOptions.new( - bind_address: options[:prom_listen_address] - ) -end - -runtime = Temporalio::Runtime.new( - telemetry: Temporalio::Runtime::TelemetryOptions.new( - metrics: prometheus, - logging: Temporalio::Runtime::LoggingOptions.new( - log_filter: Temporalio::Runtime::LoggingFilterOptions.new( - core_level: ENV.fetch('TEMPORAL_CORE_LOG_LEVEL', 'INFO'), - other_level: 'WARN' - ) - ) - ) -) - -client = Temporalio::Client.connect( - options[:server_address], - options[:namespace], - tls: tls_options, - api_key: api_key, - runtime: runtime, - logger: logger -) - -task_queues = if options[:task_queue_suffix_index_end].zero? - logger.info("Ruby worker running for task queue #{options[:task_queue]}") - [options[:task_queue]] - else - tqs = (options[:task_queue_suffix_index_start]..options[:task_queue_suffix_index_end]).map do |i| - "#{options[:task_queue]}-#{i}" - end - logger.info("Ruby worker running for #{tqs.length} task queue(s)") - tqs - end - -worker_opts = {} - -if options[:activity_poller_autoscale_max] - worker_opts[:activity_task_poller_behavior] = Temporalio::Worker::PollerBehavior::Autoscaling.new( - maximum: options[:activity_poller_autoscale_max] - ) -elsif options[:max_concurrent_activity_pollers] - worker_opts[:max_concurrent_activity_task_polls] = options[:max_concurrent_activity_pollers] -end - -if options[:workflow_poller_autoscale_max] - worker_opts[:workflow_task_poller_behavior] = Temporalio::Worker::PollerBehavior::Autoscaling.new( - maximum: options[:workflow_poller_autoscale_max] - ) -elsif options[:max_concurrent_workflow_pollers] - worker_opts[:max_concurrent_workflow_task_polls] = options[:max_concurrent_workflow_pollers] -end - -worker_opts[:max_concurrent_activities] = options[:max_concurrent_activities] if options[:max_concurrent_activities] -if options[:max_concurrent_workflow_tasks] - worker_opts[:max_concurrent_workflow_tasks] = - options[:max_concurrent_workflow_tasks] -end -worker_opts[:max_activities_per_second] = options[:activities_per_second] if options[:activities_per_second] - -client_activity = ClientActivity.new(client, err_on_unimplemented: options[:err_on_unimplemented]) - -activities = [ - NoopActivity.new, - DelayActivity.new, - PayloadActivity.new, - RetryableErrorActivity.new, - TimeoutActivity.new, - HeartbeatActivity.new, - client_activity -] - -workers = task_queues.map do |tq| - Temporalio::Worker.new( - client: client, - task_queue: tq, - workflows: [KitchenSinkWorkflow], - activities: activities, - logger: logger, - **worker_opts - ) -end - -if workers.length == 1 - workers[0].run(shutdown_signals: ['SIGINT']) -else - threads = workers.map do |w| - Thread.new { w.run(shutdown_signals: ['SIGINT']) } - end - threads.each(&:join) -end +Harness.run(KitchenSinkApp.app) if __FILE__ == $PROGRAM_NAME diff --git a/workers/run.go b/workers/run.go index 9c523d04..9d2da136 100644 --- a/workers/run.go +++ b/workers/run.go @@ -130,8 +130,8 @@ func (r *Runner) Run(ctx context.Context, baseDir string) error { case clioptions.LangTypeScript: // Node also needs module before the harness subcommand. args = append(args, "./tslib/omes.js", "worker") - case clioptions.LangDotNet, clioptions.LangGo: - // .NET and Go just need the harness worker subcommand + case clioptions.LangDotNet, clioptions.LangRuby, clioptions.LangGo: + // .NET, Ruby, and Go just need the harness worker subcommand args = append(args, "worker") } From 77dc582a81f285940a1fbc203f410e5f42d29520 Mon Sep 17 00:00:00 2001 From: Thomas Hardy Date: Wed, 22 Apr 2026 15:23:25 -0700 Subject: [PATCH 2/5] cleanup, steep + rbs linting --- cmd/dev/lint_and_format.go | 5 + workers/ruby/.rubocop.yml | 13 +- workers/ruby/Gemfile | 2 + workers/ruby/Gemfile.lock | 97 ++++- workers/ruby/Steepfile | 20 ++ workers/ruby/omes.gemspec | 2 +- workers/ruby/projects/harness/client.rb | 23 +- workers/ruby/projects/harness/helpers.rb | 3 +- workers/ruby/projects/harness/main.rb | 9 +- workers/ruby/projects/harness/project.rb | 83 +++-- .../projects/harness/tests/test_project.rb | 10 +- .../projects/harness/tests/test_worker.rb | 13 +- workers/ruby/projects/harness/worker.rb | 46 ++- workers/ruby/sig/harness.rbs | 335 ++++++++++++++++++ 14 files changed, 572 insertions(+), 89 deletions(-) create mode 100644 workers/ruby/Steepfile create mode 100644 workers/ruby/sig/harness.rbs diff --git a/cmd/dev/lint_and_format.go b/cmd/dev/lint_and_format.go index b37c91cd..456330e7 100644 --- a/cmd/dev/lint_and_format.go +++ b/cmd/dev/lint_and_format.go @@ -228,6 +228,11 @@ func lintAndFormatRubyWorker(ctx context.Context, workerDir string) error { return err } + fmt.Println("Type checking Ruby worker...") + if err := runCommandInDir(ctx, workerDir, "bundle", "exec", "steep", "check"); err != nil { + return err + } + fmt.Println("✅ Ruby lint-and-format completed successfully!") return nil } diff --git a/workers/ruby/.rubocop.yml b/workers/ruby/.rubocop.yml index e7538bc5..de5b2d26 100644 --- a/workers/ruby/.rubocop.yml +++ b/workers/ruby/.rubocop.yml @@ -1,6 +1,7 @@ AllCops: TargetRubyVersion: 3.3 NewCops: enable + SuggestExtensions: false Exclude: - "protos/**/*" - "projects/harness/api/**/*" @@ -29,10 +30,13 @@ Style/GuardClause: # These methods are inherently branchy — they dispatch protobuf action # types via large case statements that mirror the proto schema. Metrics/MethodLength: - Max: 35 + Max: 80 Metrics/ClassLength: - Max: 300 + Max: 400 + +Metrics/ModuleLength: + Max: 200 Metrics/BlockLength: Enabled: false @@ -44,4 +48,7 @@ Metrics/PerceivedComplexity: Max: 15 Metrics/AbcSize: - Max: 25 + Max: 100 + +Metrics/ParameterLists: + CountKeywordArgs: false diff --git a/workers/ruby/Gemfile b/workers/ruby/Gemfile index 9e1ff81e..d8284816 100644 --- a/workers/ruby/Gemfile +++ b/workers/ruby/Gemfile @@ -5,5 +5,7 @@ gemspec group :development do gem 'grpc-tools', '~> 1.80' gem 'minitest' + gem 'rbs', '~> 3.10' gem 'rubocop', '~> 1.0' + gem 'steep', '~> 1.10' end diff --git a/workers/ruby/Gemfile.lock b/workers/ruby/Gemfile.lock index 8511b2c9..a955bfd7 100644 --- a/workers/ruby/Gemfile.lock +++ b/workers/ruby/Gemfile.lock @@ -9,11 +9,38 @@ PATH GEM remote: https://rubygems.org/ specs: + activesupport (8.1.3) + base64 + bigdecimal + concurrent-ruby (~> 1.0, >= 1.3.1) + connection_pool (>= 2.2.5) + drb + i18n (>= 1.6, < 2) + json + logger (>= 1.4.2) + minitest (>= 5.1) + securerandom (>= 0.3) + tzinfo (~> 2.0, >= 2.0.5) + uri (>= 0.13.1) addressable (2.8.9) public_suffix (>= 2.0.2, < 8.0) ast (2.4.3) + base64 (0.3.0) bigdecimal (4.0.1) + concurrent-ruby (1.3.6) + connection_pool (3.0.2) + csv (3.3.5) drb (2.2.3) + ffi (1.17.4) + ffi (1.17.4-aarch64-linux-gnu) + ffi (1.17.4-aarch64-linux-musl) + ffi (1.17.4-arm64-darwin) + ffi (1.17.4-x86-linux-gnu) + ffi (1.17.4-x86-linux-musl) + ffi (1.17.4-x86_64-darwin) + ffi (1.17.4-x86_64-linux-gnu) + ffi (1.17.4-x86_64-linux-musl) + fileutils (1.8.0) google-protobuf (4.34.1) bigdecimal rake (~> 13.3) @@ -71,18 +98,25 @@ GEM google-protobuf (>= 3.25, < 5.0) googleapis-common-protos-types (~> 1.0) grpc-tools (1.80.0) + i18n (1.14.8) + concurrent-ruby (~> 1.0) json (2.19.2) json-schema (6.2.0) addressable (~> 2.8) bigdecimal (>= 3.1, < 5) language_server-protocol (3.17.0.5) lint_roller (1.1.0) + listen (3.10.0) + logger + rb-fsevent (~> 0.10, >= 0.10.3) + rb-inotify (~> 0.9, >= 0.9.10) logger (1.7.0) mcp (0.9.0) json-schema (>= 4.1) minitest (6.0.5) drb (~> 2.0) prism (~> 1.5) + mutex_m (0.3.0) parallel (1.27.0) parser (3.3.10.2) ast (~> 2.4.1) @@ -92,6 +126,12 @@ GEM racc (1.8.1) rainbow (3.1.1) rake (13.3.1) + rb-fsevent (0.11.2) + rb-inotify (0.11.1) + ffi (~> 1.0) + rbs (3.10.4) + logger + tsort regexp_parser (2.11.3) rubocop (1.85.1) json (~> 2.3) @@ -109,6 +149,25 @@ GEM parser (>= 3.3.7.2) prism (~> 1.7) ruby-progressbar (1.13.0) + securerandom (0.4.1) + steep (1.10.0) + activesupport (>= 5.1) + concurrent-ruby (>= 1.1.10) + csv (>= 3.0.9) + fileutils (>= 1.1.0) + json (>= 2.1.0) + language_server-protocol (>= 3.17.0.4, < 4.0) + listen (~> 3.0) + logger (>= 1.3.0) + mutex_m (>= 0.3.0) + parser (>= 3.1) + rainbow (>= 2.2.2, < 4.0) + rbs (~> 3.9) + securerandom (>= 0.1) + strscan (>= 1.0.0) + terminal-table (>= 2, < 5) + uri (>= 0.12.0) + strscan (3.1.8) temporalio (1.3.0) google-protobuf (>= 3.25.0) logger @@ -130,9 +189,15 @@ GEM temporalio (1.3.0-x86_64-linux-musl) google-protobuf (>= 3.25.0) logger + terminal-table (4.0.0) + unicode-display_width (>= 1.1.1, < 4) + tsort (0.2.0) + tzinfo (2.0.6) + concurrent-ruby (~> 1.0) unicode-display_width (3.2.0) unicode-emoji (~> 4.1) unicode-emoji (4.2.0) + uri (1.1.1) PLATFORMS aarch64-linux @@ -151,13 +216,30 @@ DEPENDENCIES grpc-tools (~> 1.80) minitest omes! + rbs (~> 3.10) rubocop (~> 1.0) + steep (~> 1.10) CHECKSUMS + activesupport (8.1.3) sha256=21a5e0dfbd4c3ddd9e1317ec6a4d782fa226e7867dc70b0743acda81a1dca20e addressable (2.8.9) sha256=cc154fcbe689711808a43601dee7b980238ce54368d23e127421753e46895485 ast (2.4.3) sha256=954615157c1d6a382bc27d690d973195e79db7f55e9765ac7c481c60bdb4d383 + base64 (0.3.0) sha256=27337aeabad6ffae05c265c450490628ef3ebd4b67be58257393227588f5a97b bigdecimal (4.0.1) sha256=8b07d3d065a9f921c80ceaea7c9d4ae596697295b584c296fe599dd0ad01c4a7 + concurrent-ruby (1.3.6) sha256=6b56837e1e7e5292f9864f34b69c5a2cbc75c0cf5338f1ce9903d10fa762d5ab + connection_pool (3.0.2) sha256=33fff5ba71a12d2aa26cb72b1db8bba2a1a01823559fb01d29eb74c286e62e0a + csv (3.3.5) sha256=6e5134ac3383ef728b7f02725d9872934f523cb40b961479f69cf3afa6c8e73f drb (2.2.3) sha256=0b00d6fdb50995fe4a45dea13663493c841112e4068656854646f418fda13373 + ffi (1.17.4) sha256=bcd1642e06f0d16fc9e09ac6d49c3a7298b9789bcb58127302f934e437d60acf + ffi (1.17.4-aarch64-linux-gnu) sha256=b208f06f91ffd8f5e1193da3cae3d2ccfc27fc36fba577baf698d26d91c080df + ffi (1.17.4-aarch64-linux-musl) sha256=9286b7a615f2676245283aef0a0a3b475ae3aae2bb5448baace630bb77b91f39 + ffi (1.17.4-arm64-darwin) sha256=19071aaf1419251b0a46852abf960e77330a3b334d13a4ab51d58b31a937001b + ffi (1.17.4-x86-linux-gnu) sha256=38e150df5f4ca555e25beca4090823ae09657bceded154e3c52f8631c1ed72cf + ffi (1.17.4-x86-linux-musl) sha256=fbeec0fc7c795bcf86f623bb18d31ea1820f7bd580e1703a3d3740d527437809 + ffi (1.17.4-x86_64-darwin) sha256=aa70390523cf3235096cf64962b709b4cfbd5c082a2cb2ae714eb0fe2ccda496 + ffi (1.17.4-x86_64-linux-gnu) sha256=9d3db14c2eae074b382fa9c083fe95aec6e0a1451da249eab096c34002bc752d + ffi (1.17.4-x86_64-linux-musl) sha256=3fdf9888483de005f8ef8d1cf2d3b20d86626af206cbf780f6a6a12439a9c49e + fileutils (1.8.0) sha256=8c6b1df54e2540bdb2f39258f08af78853aa70bad52b4d394bbc6424593c6e02 google-protobuf (4.34.1) sha256=347181542b8d659c60f028fa3791c9cccce651a91ad27782dbc5c5e374796cdc google-protobuf (4.34.1-aarch64-linux-gnu) sha256=f9c07607dc139c895f2792a7740fcd01cd94d4d7b0e0a939045b50d7999f0b1d google-protobuf (4.34.1-aarch64-linux-musl) sha256=db58e5a4a492b43c6614486aea31b7fb86955b175d1d48f28ebf388f058d78a9 @@ -178,13 +260,16 @@ CHECKSUMS grpc (1.80.0-x86_64-linux-gnu) sha256=25ba8beb5438152fb60656161dc729c1258c6963ec568361f601e19b2fb7ac23 grpc (1.80.0-x86_64-linux-musl) sha256=15a62e816c690bece9f5fd320a4b61aa735a2390daa8141596672a6e820ff342 grpc-tools (1.80.0) sha256=49076fbc7b34556365694202ed7ffd1e959e68ba8c339e99b437ba2a985a2cf2 + i18n (1.14.8) sha256=285778639134865c5e0f6269e0b818256017e8cde89993fdfcbfb64d088824a5 json (2.19.2) sha256=e7e1bd318b2c37c4ceee2444841c86539bc462e81f40d134cf97826cb14e83cf json-schema (6.2.0) sha256=e8bff46ed845a22c1ab2bd0d7eccf831c01fe23bb3920caa4c74db4306813666 language_server-protocol (3.17.0.5) sha256=fd1e39a51a28bf3eec959379985a72e296e9f9acfce46f6a79d31ca8760803cc lint_roller (1.1.0) sha256=2c0c845b632a7d172cb849cc90c1bce937a28c5c8ccccb50dfd46a485003cc87 + listen (3.10.0) sha256=c6e182db62143aeccc2e1960033bebe7445309c7272061979bb098d03760c9d2 logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203 mcp (0.9.0) sha256=a0a3737b0ac9df0772f4ef7e2b013c260ddbcf217a5d50a66bff0baeddf03e47 minitest (6.0.5) sha256=f007d7246bf4feea549502842cd7c6aba8851cdc9c90ba06de9c476c0d01155c + mutex_m (0.3.0) sha256=cfcb04ac16b69c4813777022fdceda24e9f798e48092a2b817eb4c0a782b0751 omes (0.1.0) parallel (1.27.0) sha256=4ac151e1806b755fb4e2dc2332cbf0e54f2e24ba821ff2d3dcf86bf6dc4ae130 parser (3.3.10.2) sha256=6f60c84aa4bdcedb6d1a2434b738fe8a8136807b6adc8f7f53b97da9bc4e9357 @@ -193,10 +278,16 @@ CHECKSUMS racc (1.8.1) sha256=4a7f6929691dbec8b5209a0b373bc2614882b55fc5d2e447a21aaa691303d62f rainbow (3.1.1) sha256=039491aa3a89f42efa1d6dec2fc4e62ede96eb6acd95e52f1ad581182b79bc6a rake (13.3.1) sha256=8c9e89d09f66a26a01264e7e3480ec0607f0c497a861ef16063604b1b08eb19c + rb-fsevent (0.11.2) sha256=43900b972e7301d6570f64b850a5aa67833ee7d87b458ee92805d56b7318aefe + rb-inotify (0.11.1) sha256=a0a700441239b0ff18eb65e3866236cd78613d6b9f78fea1f9ac47a85e47be6e + rbs (3.10.4) sha256=b17d7c4be4bb31a11a3b529830f0aa206a807ca42f2e7921a3027dfc6b7e5ce8 regexp_parser (2.11.3) sha256=ca13f381a173b7a93450e53459075c9b76a10433caadcb2f1180f2c741fc55a4 rubocop (1.85.1) sha256=3dbcf9e961baa4c376eeeb2a03913dca5e3987033b04d38fa538aa1e7406cc77 rubocop-ast (1.49.1) sha256=4412f3ee70f6fe4546cc489548e0f6fcf76cafcfa80fa03af67098ffed755035 ruby-progressbar (1.13.0) sha256=80fc9c47a9b640d6834e0dc7b3c94c9df37f08cb072b7761e4a71e22cff29b33 + securerandom (0.4.1) sha256=cc5193d414a4341b6e225f0cb4446aceca8e50d5e1888743fac16987638ea0b1 + steep (1.10.0) sha256=1b295b55f9aaff1b8d3ee42453ee55bc2a1078fda0268f288edb2dc014f4d7d1 + strscan (3.1.8) sha256=aae2db611a225559f21ffbb71765c9a4e60fd262534a9ea84f4f11c7f32f679e temporalio (1.3.0) sha256=672260631f419d1ec01a2230cc6a72d665ef9d385c5d96351bc68f639dbdc704 temporalio (1.3.0-aarch64-linux) sha256=1ec4230251bc1771455fa20f1d1e9006639f3da3657ce4d15d09e27970d5a248 temporalio (1.3.0-aarch64-linux-musl) sha256=135a676e60ba8ee6f49c7fa793505fee7479b78a3c0b31298073845560a32aed @@ -204,8 +295,12 @@ CHECKSUMS temporalio (1.3.0-x86_64-darwin) sha256=f2a4b35302564b6d2969a1daf6b2d7f2b86c9d1a50c59d1620b327b2af38c124 temporalio (1.3.0-x86_64-linux) sha256=5122f3c2bd2b540565fc9ec4e2083401c00a7056c8408cd9922ebe570b366eef temporalio (1.3.0-x86_64-linux-musl) sha256=0b9c19a94d6703155618d02facf46481467bde1cdcd86ccb6b4f38896d847560 + terminal-table (4.0.0) sha256=f504793203f8251b2ea7c7068333053f0beeea26093ec9962e62ea79f94301d2 + tsort (0.2.0) sha256=9650a793f6859a43b6641671278f79cfead60ac714148aabe4e3f0060480089f + tzinfo (2.0.6) sha256=8daf828cc77bcf7d63b0e3bdb6caa47e2272dcfaf4fbfe46f8c3a9df087a829b unicode-display_width (3.2.0) sha256=0cdd96b5681a5949cdbc2c55e7b420facae74c4aaf9a9815eee1087cb1853c42 unicode-emoji (4.2.0) sha256=519e69150f75652e40bf736106cfbc8f0f73aa3fb6a65afe62fefa7f80b0f80f + uri (1.1.1) sha256=379fa58d27ffb1387eaada68c749d1426738bd0f654d812fcc07e7568f5c57c6 BUNDLED WITH - 4.0.8 + 4.0.10 diff --git a/workers/ruby/Steepfile b/workers/ruby/Steepfile new file mode 100644 index 00000000..91328441 --- /dev/null +++ b/workers/ruby/Steepfile @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +D = Steep::Diagnostic + +common_diagnostics = { + D::Ruby::UnknownConstant => :information, + D::Ruby::UnannotatedEmptyCollection => :information, + D::Ruby::UndeclaredMethodDefinition => :information +} + +target :harness do + signature 'sig' + check 'projects/harness.rb' + check 'projects/harness' + ignore 'projects/harness/api', 'projects/harness/tests' + library 'json', 'logger', 'optparse', 'time' + configure_code_diagnostics do |hash| + hash.update(common_diagnostics) + end +end diff --git a/workers/ruby/omes.gemspec b/workers/ruby/omes.gemspec index abf84825..0e1a3d7c 100644 --- a/workers/ruby/omes.gemspec +++ b/workers/ruby/omes.gemspec @@ -7,8 +7,8 @@ Gem::Specification.new do |s| s.license = 'MIT' s.files = Dir['**/*.rb'] s.require_paths = ['.'] - s.add_dependency 'grpc', '~> 1.80' s.add_dependency 'google-protobuf', '~> 4.0' + s.add_dependency 'grpc', '~> 1.80' s.add_dependency 'temporalio', '~> 1.3' s.metadata['rubygems_mfa_required'] = 'true' end diff --git a/workers/ruby/projects/harness/client.rb b/workers/ruby/projects/harness/client.rb index 8eb864bd..4f16b68c 100644 --- a/workers/ruby/projects/harness/client.rb +++ b/workers/ruby/projects/harness/client.rb @@ -4,28 +4,17 @@ require 'temporalio/runtime' module Harness - ClientConfig = Struct.new( + ClientConfig = Data.define( :target_host, :namespace, :api_key, :tls, - :runtime, - keyword_init: true + :runtime ) module ClientHelpers module_function - def default_client_factory(config) - Temporalio::Client.connect( - config.target_host, - config.namespace, - api_key: config.api_key, - tls: config.tls, - runtime: config.runtime - ) - end - def build_client_config( server_address:, namespace:, @@ -107,6 +96,12 @@ def build_runtime(prom_listen_address) end def self.default_client_factory(config) - ClientHelpers.default_client_factory(config) + Temporalio::Client.connect( + config.target_host, + config.namespace, + api_key: config.api_key, + tls: config.tls, + runtime: config.runtime + ) end end diff --git a/workers/ruby/projects/harness/helpers.rb b/workers/ruby/projects/harness/helpers.rb index d064e473..df3b36a9 100644 --- a/workers/ruby/projects/harness/helpers.rb +++ b/workers/ruby/projects/harness/helpers.rb @@ -12,8 +12,7 @@ module Helpers 'ERROR' => Logger::ERROR, 'WARN' => Logger::WARN, 'INFO' => Logger::INFO, - 'DEBUG' => Logger::DEBUG, - 'NOTSET' => Logger::DEBUG + 'DEBUG' => Logger::DEBUG }.freeze module_function diff --git a/workers/ruby/projects/harness/main.rb b/workers/ruby/projects/harness/main.rb index 31371898..22d9bc25 100644 --- a/workers/ruby/projects/harness/main.rb +++ b/workers/ruby/projects/harness/main.rb @@ -16,13 +16,10 @@ def initialize(worker:, client_factory:, project: nil) def self.run(app, argv = ARGV) argv = Array(argv).dup - if argv.empty? || argv.first == 'worker' - worker_argv = argv.first == 'worker' ? argv.drop(1) : argv - WorkerCLI.run_cli(app.worker, app.client_factory, worker_argv) + if argv.first == 'worker' + WorkerCLI.run_cli(app.worker, app.client_factory, argv.drop(1)) elsif argv.first == 'project-server' - if app.project.nil? - raise SystemExit, 'Wanted project-server but no project handlers registered for this app' - end + raise SystemExit, 'Wanted project-server but no project handlers registered for this app' if app.project.nil? ProjectCLI.run_cli(app.project, app.client_factory, argv.drop(1)) else diff --git a/workers/ruby/projects/harness/project.rb b/workers/ruby/projects/harness/project.rb index 0866e6c5..de30d7c6 100644 --- a/workers/ruby/projects/harness/project.rb +++ b/workers/ruby/projects/harness/project.rb @@ -4,44 +4,46 @@ require 'optparse' require 'grpc' -$LOAD_PATH.unshift(File.expand_path('..', __dir__)) unless $LOAD_PATH.include?(File.expand_path('..', __dir__)) +harness_projects_dir = File.expand_path('..', __dir__ || '.') +$LOAD_PATH.unshift(harness_projects_dir) unless $LOAD_PATH.include?(harness_projects_dir) require_relative 'client' require_relative 'api/api_pb' require_relative 'api/api_services_pb' module Harness - ProjectRunMetadata = Struct.new( + ProjectRunMetadata = Data.define( :run_id, - :execution_id, - keyword_init: true + :execution_id ) - ProjectInitContext = Struct.new( + ProjectInitContext = Data.define( :logger, :run, :task_queue, - :config_json, - keyword_init: true + :config_json ) - ProjectExecuteContext = Struct.new( + ProjectExecuteContext = Data.define( :logger, :run, :task_queue, :iteration, - :payload, - keyword_init: true + :payload ) - ProjectHandlers = Struct.new( + ProjectHandlers = Data.define( :execute, - :init, - keyword_init: true - ) + :init + ) do + def initialize(execute:, init: nil) + super + end + end class ProjectServiceServer < Temporal::Omes::Projects::V1::ProjectService::Service def initialize(handlers, client_factory) + super() @handlers = handlers @client_factory = client_factory @client = nil @@ -50,13 +52,25 @@ def initialize(handlers, client_factory) end def init(request, _call) - raise grpc_status(GRPC::Core::StatusCodes::INVALID_ARGUMENT, 'task_queue required') if request.task_queue.to_s.empty? - raise grpc_status(GRPC::Core::StatusCodes::INVALID_ARGUMENT, 'execution_id required') if request.execution_id.to_s.empty? + if request.task_queue.to_s.empty? + raise grpc_status(GRPC::Core::StatusCodes::INVALID_ARGUMENT, + 'task_queue required') + end + if request.execution_id.to_s.empty? + raise grpc_status(GRPC::Core::StatusCodes::INVALID_ARGUMENT, + 'execution_id required') + end raise grpc_status(GRPC::Core::StatusCodes::INVALID_ARGUMENT, 'run_id required') if request.run_id.to_s.empty? connect_options = request.connect_options || Temporal::Omes::Projects::V1::ConnectOptions.new - raise grpc_status(GRPC::Core::StatusCodes::INVALID_ARGUMENT, 'server_address required') if connect_options.server_address.to_s.empty? - raise grpc_status(GRPC::Core::StatusCodes::INVALID_ARGUMENT, 'namespace required') if connect_options.namespace.to_s.empty? + if connect_options.server_address.to_s.empty? + raise grpc_status(GRPC::Core::StatusCodes::INVALID_ARGUMENT, + 'server_address required') + end + if connect_options.namespace.to_s.empty? + raise grpc_status(GRPC::Core::StatusCodes::INVALID_ARGUMENT, + 'namespace required') + end begin config = ClientHelpers.build_client_config( @@ -69,14 +83,14 @@ def init(request, _call) tls_server_name: connect_options.tls_server_name.to_s.empty? ? nil : connect_options.tls_server_name, disable_host_verification: connect_options.disable_host_verification ) - rescue ArgumentError => error - raise grpc_status(GRPC::Core::StatusCodes::INVALID_ARGUMENT, error.message) + rescue ArgumentError => e + raise grpc_status(GRPC::Core::StatusCodes::INVALID_ARGUMENT, e.message) end begin client = @client_factory.call(config) - rescue StandardError => error - raise grpc_status(GRPC::Core::StatusCodes::INTERNAL, "failed to create client: #{error}") + rescue StandardError => e + raise grpc_status(GRPC::Core::StatusCodes::INTERNAL, "failed to create client: #{e}") end run = ProjectRunMetadata.new( @@ -95,21 +109,25 @@ def init(request, _call) config_json: request.config_json ) ) - rescue StandardError => error - raise grpc_status(GRPC::Core::StatusCodes::INTERNAL, "init handler failed: #{error}") + rescue StandardError => e + raise grpc_status(GRPC::Core::StatusCodes::INTERNAL, "init handler failed: #{e}") end end @client = client @run = run Temporal::Omes::Projects::V1::InitResponse.new - rescue GRPC::BadStatus - raise end def execute(request, _call) - raise grpc_status(GRPC::Core::StatusCodes::INVALID_ARGUMENT, 'task_queue required') if request.task_queue.to_s.empty? - raise grpc_status(GRPC::Core::StatusCodes::FAILED_PRECONDITION, 'Init must be called before Execute') if @client.nil? || @run.nil? + if request.task_queue.to_s.empty? + raise grpc_status(GRPC::Core::StatusCodes::INVALID_ARGUMENT, + 'task_queue required') + end + if @client.nil? || @run.nil? + raise grpc_status(GRPC::Core::StatusCodes::FAILED_PRECONDITION, + 'Init must be called before Execute') + end @handlers.execute.call( @client, @@ -122,10 +140,10 @@ def execute(request, _call) ) ) Temporal::Omes::Projects::V1::ExecuteResponse.new - rescue GRPC::BadStatus - raise - rescue StandardError => error - raise grpc_status(GRPC::Core::StatusCodes::INTERNAL, "execute handler failed: #{error}") + rescue StandardError => e + raise if e.is_a?(GRPC::BadStatus) + + raise grpc_status(GRPC::Core::StatusCodes::INTERNAL, "execute handler failed: #{e}") end private @@ -133,7 +151,6 @@ def execute(request, _call) def grpc_status(code, details) GRPC::BadStatus.new_status_exception(code, details) end - end module ProjectCLI diff --git a/workers/ruby/projects/harness/tests/test_project.rb b/workers/ruby/projects/harness/tests/test_project.rb index 2f9d1349..63e466e8 100644 --- a/workers/ruby/projects/harness/tests/test_project.rb +++ b/workers/ruby/projects/harness/tests/test_project.rb @@ -50,7 +50,7 @@ def test_init_passes_run_metadata_to_handler init_calls << [handler_client, context] end ), - ->(given_config) do + lambda do |given_config| assert_equal 'localhost:7233', given_config.target_host assert_equal 'default', given_config.namespace assert_equal 'token', given_config.api_key @@ -184,13 +184,7 @@ def test_project_server_executes_workflow_against_real_temporal_server events << [:execute, handler_client, context, result] end - cli_path = ENV['TEMPORAL_CLI_PATH'] - cli_path = `which temporal`.strip if cli_path.to_s.empty? - raise 'temporal CLI not found' if cli_path.to_s.empty? - - Temporalio::Testing::WorkflowEnvironment.start_local( - dev_server_existing_path: cli_path - ) do |env| + Temporalio::Testing::WorkflowEnvironment.start_local do |env| worker = Temporalio::Worker.new( client: env.client, task_queue: task_queue, diff --git a/workers/ruby/projects/harness/tests/test_worker.rb b/workers/ruby/projects/harness/tests/test_worker.rb index 0239a687..77f05c30 100644 --- a/workers/ruby/projects/harness/tests/test_worker.rb +++ b/workers/ruby/projects/harness/tests/test_worker.rb @@ -9,8 +9,7 @@ class HarnessWorkerTest < Minitest::Test :on_run, :run_calls, :shutdown_calls, - :received_shutdown_signals, - keyword_init: true + :received_shutdown_signals ) do def run(shutdown_signals: []) self.run_calls += 1 @@ -79,7 +78,7 @@ def test_run_workers_shuts_down_all_workers_when_one_fails received_shutdown_signals: nil ) successful_worker = FakeRunnableWorker.new( - on_run: -> { nil }, + on_run: -> {}, run_calls: 0, shutdown_calls: 0, received_shutdown_signals: nil @@ -88,11 +87,9 @@ def test_run_workers_shuts_down_all_workers_when_one_fails error = with_stubbed_run_all(lambda do |*workers, **kwargs| first_error = nil workers.each do |worker| - begin - worker.run(shutdown_signals: kwargs[:shutdown_signals]) - rescue StandardError => raised_error - first_error ||= raised_error - end + worker.run(shutdown_signals: kwargs[:shutdown_signals]) + rescue StandardError => e + first_error ||= e end workers.each(&:shutdown) raise first_error if first_error diff --git a/workers/ruby/projects/harness/worker.rb b/workers/ruby/projects/harness/worker.rb index 5fd97e9a..b863b4f2 100644 --- a/workers/ruby/projects/harness/worker.rb +++ b/workers/ruby/projects/harness/worker.rb @@ -6,12 +6,11 @@ require_relative 'helpers' module Harness - WorkerContext = Struct.new( + WorkerContext = Data.define( :logger, :task_queue, :err_on_unimplemented, - :worker_kwargs, - keyword_init: true + :worker_kwargs ) module WorkerCLI @@ -25,7 +24,10 @@ def run(worker_factory, client_factory, argv) options = default_options build_parser(options).parse!(Array(argv).dup) - raise ArgumentError, 'Task queue suffix start after end' if options[:task_queue_suffix_index_start] > options[:task_queue_suffix_index_end] + if options[:task_queue_suffix_index_start] > options[:task_queue_suffix_index_end] + raise ArgumentError, + 'Task queue suffix start after end' + end logger = Helpers.configure_logger(options[:log_level], options[:log_encoding]) config = ClientHelpers.build_client_config( @@ -70,14 +72,26 @@ def build_parser(options) opts.banner = 'Usage: runner.rb [options]' opts.on('-q', '--task-queue QUEUE', 'Task queue to use') { |value| options[:task_queue] = value } - opts.on('--task-queue-suffix-index-start N', Integer) { |value| options[:task_queue_suffix_index_start] = value } + opts.on('--task-queue-suffix-index-start N', Integer) do |value| + options[:task_queue_suffix_index_start] = value + end opts.on('--task-queue-suffix-index-end N', Integer) { |value| options[:task_queue_suffix_index_end] = value } - opts.on('--max-concurrent-activity-pollers N', Integer) { |value| options[:max_concurrent_activity_pollers] = value } - opts.on('--max-concurrent-workflow-pollers N', Integer) { |value| options[:max_concurrent_workflow_pollers] = value } - opts.on('--activity-poller-autoscale-max N', Integer) { |value| options[:activity_poller_autoscale_max] = value } - opts.on('--workflow-poller-autoscale-max N', Integer) { |value| options[:workflow_poller_autoscale_max] = value } + opts.on('--max-concurrent-activity-pollers N', Integer) do |value| + options[:max_concurrent_activity_pollers] = value + end + opts.on('--max-concurrent-workflow-pollers N', Integer) do |value| + options[:max_concurrent_workflow_pollers] = value + end + opts.on('--activity-poller-autoscale-max N', Integer) do |value| + options[:activity_poller_autoscale_max] = value + end + opts.on('--workflow-poller-autoscale-max N', Integer) do |value| + options[:workflow_poller_autoscale_max] = value + end opts.on('--max-concurrent-activities N', Integer) { |value| options[:max_concurrent_activities] = value } - opts.on('--max-concurrent-workflow-tasks N', Integer) { |value| options[:max_concurrent_workflow_tasks] = value } + opts.on('--max-concurrent-workflow-tasks N', Integer) do |value| + options[:max_concurrent_workflow_tasks] = value + end opts.on('--activities-per-second N', Float) { |value| options[:activities_per_second] = value } opts.on('--err-on-unimplemented BOOL') { |value| options[:err_on_unimplemented] = parse_bool(value) } opts.on('--log-level LEVEL') { |value| options[:log_level] = value } @@ -123,14 +137,20 @@ def build_worker_kwargs(options) worker_kwargs[:max_concurrent_workflow_task_polls] = options[:max_concurrent_workflow_pollers] end - worker_kwargs[:max_concurrent_activities] = options[:max_concurrent_activities] if options[:max_concurrent_activities] - worker_kwargs[:max_concurrent_workflow_tasks] = options[:max_concurrent_workflow_tasks] if options[:max_concurrent_workflow_tasks] + if options[:max_concurrent_activities] + worker_kwargs[:max_concurrent_activities] = + options[:max_concurrent_activities] + end + if options[:max_concurrent_workflow_tasks] + worker_kwargs[:max_concurrent_workflow_tasks] = + options[:max_concurrent_workflow_tasks] + end worker_kwargs[:max_activities_per_second] = options[:activities_per_second] if options[:activities_per_second] worker_kwargs end def parse_bool(value) - return value if value == true || value == false + return value if [true, false].include?(value) %w[true 1 yes].include?(value.to_s.downcase) end diff --git a/workers/ruby/sig/harness.rbs b/workers/ruby/sig/harness.rbs new file mode 100644 index 00000000..1d1b943a --- /dev/null +++ b/workers/ruby/sig/harness.rbs @@ -0,0 +1,335 @@ +module GRPC + class BadStatus < StandardError + def self.new_status_exception: (untyped code, String details) -> BadStatus + end + + class RpcServer + def initialize: (**untyped) -> void + def handle: (untyped service) -> void + def add_http2_port: (String port, untyped creds) -> Integer + def run_till_terminated_or_interrupted: (Array[String] signals) -> void + end + + module Core + module StatusCodes + INVALID_ARGUMENT: untyped + FAILED_PRECONDITION: untyped + INTERNAL: untyped + end + end +end + +module Temporalio + class Runtime + class TelemetryOptions + def initialize: (**untyped) -> void + end + + class LoggingOptions + def initialize: (**untyped) -> void + end + + class LoggingFilterOptions + def initialize: (**untyped) -> void + end + + class PrometheusMetricsOptions + attr_reader bind_address: String + attr_reader durations_as_seconds: bool + + def initialize: ( + bind_address: String, + ?durations_as_seconds: bool + ) -> void + end + + def initialize: (**untyped) -> void + end + + class Client + class Connection + class TLSOptions + attr_reader client_cert: String? + attr_reader client_private_key: String? + attr_reader server_root_ca_cert: String? + attr_reader domain: String? + + def initialize: ( + ?client_cert: String?, + ?client_private_key: String?, + ?server_root_ca_cert: String?, + ?domain: String? + ) -> void + end + end + + def self.connect: (*untyped, **untyped) -> Client + end + + class Worker + class PollerBehavior + class Autoscaling + attr_reader maximum: Integer + + def initialize: (maximum: Integer) -> void + end + end + + def self.run_all: (*Worker workers, **untyped) -> untyped + end +end + +module Temporal + module Omes + module Projects + module V1 + class ConnectOptions + attr_reader namespace: String + attr_reader server_address: String + attr_reader auth_header: String + attr_reader enable_tls: bool + attr_reader tls_cert_path: String + attr_reader tls_key_path: String + attr_reader tls_server_name: String + attr_reader disable_host_verification: bool + + def initialize: ( + ?namespace: String, + ?server_address: String, + ?auth_header: String, + ?enable_tls: bool, + ?tls_cert_path: String, + ?tls_key_path: String, + ?tls_server_name: String, + ?disable_host_verification: bool + ) -> void + end + + class InitRequest + attr_reader execution_id: String + attr_reader run_id: String + attr_reader task_queue: String + attr_reader connect_options: ConnectOptions? + attr_reader config_json: String + + def initialize: ( + ?execution_id: String, + ?run_id: String, + ?task_queue: String, + ?connect_options: ConnectOptions?, + ?config_json: String + ) -> void + end + + class InitResponse + def initialize: () -> void + end + + class ExecuteRequest + attr_reader iteration: Integer + attr_reader task_queue: String + attr_reader payload: String + + def initialize: ( + ?iteration: Integer, + ?task_queue: String, + ?payload: String + ) -> void + end + + class ExecuteResponse + def initialize: () -> void + end + + module ProjectService + class Service + def initialize: () -> void + end + end + end + end + end +end + +module Harness + type client_factory = ^(ClientConfig config) -> Temporalio::Client + type worker_factory = ^(Temporalio::Client client, WorkerContext context) -> Temporalio::Worker + type project_execute_handler = ^(Temporalio::Client client, ProjectExecuteContext context) -> void + type project_init_handler = ^(Temporalio::Client client, ProjectInitContext context) -> void + + class ClientConfig + attr_reader target_host: String + attr_reader namespace: String + attr_reader api_key: String? + attr_reader tls: Temporalio::Client::Connection::TLSOptions? + attr_reader runtime: Temporalio::Runtime + + def initialize: ( + target_host: String, + namespace: String, + api_key: String?, + tls: Temporalio::Client::Connection::TLSOptions?, + runtime: Temporalio::Runtime + ) -> void + end + + class App + attr_reader worker: worker_factory + attr_reader client_factory: client_factory + attr_reader project: ProjectHandlers? + + def initialize: ( + worker: worker_factory, + client_factory: client_factory, + ?project: ProjectHandlers? + ) -> void + end + + class WorkerContext + attr_reader logger: Logger + attr_reader task_queue: String + attr_reader err_on_unimplemented: bool + attr_reader worker_kwargs: Hash[Symbol, untyped] + + def initialize: ( + logger: Logger, + task_queue: String, + err_on_unimplemented: bool, + worker_kwargs: Hash[Symbol, untyped] + ) -> void + end + + class ProjectRunMetadata + attr_reader run_id: String + attr_reader execution_id: String + + def initialize: ( + run_id: String, + execution_id: String + ) -> void + end + + class ProjectInitContext + attr_reader logger: Logger + attr_reader run: ProjectRunMetadata + attr_reader task_queue: String + attr_reader config_json: String + + def initialize: ( + logger: Logger, + run: ProjectRunMetadata, + task_queue: String, + config_json: String + ) -> void + end + + class ProjectExecuteContext + attr_reader logger: Logger + attr_reader run: ProjectRunMetadata + attr_reader task_queue: String + attr_reader iteration: Integer + attr_reader payload: String + + def initialize: ( + logger: Logger, + run: ProjectRunMetadata, + task_queue: String, + iteration: Integer, + payload: String + ) -> void + end + + class ProjectHandlers + attr_reader execute: project_execute_handler + attr_reader init: project_init_handler? + + def initialize: ( + execute: project_execute_handler, + ?init: project_init_handler? + ) -> void + end + + module ClientHelpers + def build_client_config: ( + server_address: String, + namespace: String, + auth_header: String, + tls: bool, + tls_cert_path: String, + tls_key_path: String, + ?tls_server_name: String?, + ?disable_host_verification: bool, + ?prom_listen_address: String? + ) -> ClientConfig + def self.build_client_config: ( + server_address: String, + namespace: String, + auth_header: String, + tls: bool, + tls_cert_path: String, + tls_key_path: String, + ?tls_server_name: String?, + ?disable_host_verification: bool, + ?prom_listen_address: String? + ) -> ClientConfig + def build_api_key: (String auth_header) -> String? + def self.build_api_key: (String auth_header) -> String? + def build_tls_config: ( + tls: bool, + tls_cert_path: String, + tls_key_path: String, + ?tls_server_name: String?, + ?disable_host_verification: bool + ) -> Temporalio::Client::Connection::TLSOptions? + def self.build_tls_config: ( + tls: bool, + tls_cert_path: String, + tls_key_path: String, + ?tls_server_name: String?, + ?disable_host_verification: bool + ) -> Temporalio::Client::Connection::TLSOptions? + def build_runtime: (String? prom_listen_address) -> Temporalio::Runtime + def self.build_runtime: (String? prom_listen_address) -> Temporalio::Runtime + end + + module Helpers + def configure_logger: (String log_level, String log_encoding) -> Logger + def self.configure_logger: (String log_level, String log_encoding) -> Logger + end + + module WorkerCLI + def run_cli: (worker_factory, client_factory, Array[String]) -> void + def self.run_cli: (worker_factory, client_factory, Array[String]) -> void + def run: (worker_factory, client_factory, Array[String]) -> void + def self.run: (worker_factory, client_factory, Array[String]) -> void + def run_workers: (Array[untyped] workers) -> void + def self.run_workers: (Array[untyped] workers) -> void + def build_parser: (Hash[Symbol, untyped] options) -> OptionParser + def self.build_parser: (Hash[Symbol, untyped] options) -> OptionParser + def build_task_queues: (Logger logger, String task_queue, Integer suffix_start, Integer suffix_end) -> Array[String] + def self.build_task_queues: (Logger logger, String task_queue, Integer suffix_start, Integer suffix_end) -> Array[String] + def build_worker_kwargs: (Hash[Symbol, untyped] options) -> Hash[Symbol, untyped] + def self.build_worker_kwargs: (Hash[Symbol, untyped] options) -> Hash[Symbol, untyped] + def parse_bool: (untyped value) -> bool + def self.parse_bool: (untyped value) -> bool + def default_options: -> Hash[Symbol, untyped] + def self.default_options: -> Hash[Symbol, untyped] + end + + class ProjectServiceServer + def initialize: (ProjectHandlers handlers, client_factory client_factory) -> void + def init: (Temporal::Omes::Projects::V1::InitRequest request, untyped call) -> Temporal::Omes::Projects::V1::InitResponse + def execute: (Temporal::Omes::Projects::V1::ExecuteRequest request, untyped call) -> Temporal::Omes::Projects::V1::ExecuteResponse + def grpc_status: (untyped code, String details) -> GRPC::BadStatus + end + + module ProjectCLI + def run_cli: (ProjectHandlers, client_factory, Array[String]) -> void + def self.run_cli: (ProjectHandlers, client_factory, Array[String]) -> void + def serve: (ProjectHandlers, client_factory, Integer) -> void + def self.serve: (ProjectHandlers, client_factory, Integer) -> void + end + + def self.default_client_factory: (ClientConfig config) -> Temporalio::Client + def self.run: (App app, ?Array[String] argv) -> void +end From f06046c463e699b4446abf6324ed08933f64076a Mon Sep 17 00:00:00 2001 From: Thomas Hardy Date: Thu, 23 Apr 2026 14:15:03 -0700 Subject: [PATCH 3/5] cleanup harness project files, add test/install --- .gitignore | 1 + cmd/dev/install.go | 7 + cmd/dev/lint_and_format.go | 10 +- cmd/dev/test.go | 35 +- workers/ruby/.rubocop.yml | 11 +- workers/ruby/Gemfile | 3 +- workers/ruby/Gemfile.lock | 14 +- workers/ruby/Steepfile | 20 -- workers/ruby/harness/.rubocop.yml | 37 +++ workers/ruby/harness/Gemfile | 12 + workers/ruby/harness/Gemfile.lock | 300 ++++++++++++++++++ workers/ruby/harness/Rakefile | 10 + workers/ruby/harness/Steepfile | 16 + workers/ruby/harness/harness.gemspec | 14 + .../ruby/{projects => harness/lib}/harness.rb | 0 .../lib}/harness/api/api_pb.rb | 0 .../lib}/harness/api/api_services_pb.rb | 0 .../lib}/harness/client.rb | 0 .../lib}/harness/helpers.rb | 0 .../{projects => harness/lib}/harness/main.rb | 0 .../lib}/harness/project.rb | 3 - .../lib}/harness/worker.rb | 4 - workers/ruby/{ => harness}/sig/harness.rbs | 0 .../harness/tests/test_project.rb | 3 +- workers/ruby/harness/tests/test_worker.rb | 59 ++++ workers/ruby/kitchen_sink_app.rb | 2 +- workers/ruby/omes.gemspec | 4 +- .../projects/harness/tests/test_worker.rb | 125 -------- workers/ruby/runner.rb | 5 +- 29 files changed, 502 insertions(+), 193 deletions(-) delete mode 100644 workers/ruby/Steepfile create mode 100644 workers/ruby/harness/.rubocop.yml create mode 100644 workers/ruby/harness/Gemfile create mode 100644 workers/ruby/harness/Gemfile.lock create mode 100644 workers/ruby/harness/Rakefile create mode 100644 workers/ruby/harness/Steepfile create mode 100644 workers/ruby/harness/harness.gemspec rename workers/ruby/{projects => harness/lib}/harness.rb (100%) rename workers/ruby/{projects => harness/lib}/harness/api/api_pb.rb (100%) rename workers/ruby/{projects => harness/lib}/harness/api/api_services_pb.rb (100%) rename workers/ruby/{projects => harness/lib}/harness/client.rb (100%) rename workers/ruby/{projects => harness/lib}/harness/helpers.rb (100%) rename workers/ruby/{projects => harness/lib}/harness/main.rb (100%) rename workers/ruby/{projects => harness/lib}/harness/project.rb (97%) rename workers/ruby/{projects => harness/lib}/harness/worker.rb (98%) rename workers/ruby/{ => harness}/sig/harness.rbs (100%) rename workers/ruby/{projects => }/harness/tests/test_project.rb (99%) create mode 100644 workers/ruby/harness/tests/test_worker.rb delete mode 100644 workers/ruby/projects/harness/tests/test_worker.rb diff --git a/.gitignore b/.gitignore index 3db1d9fa..bc6c02c1 100644 --- a/.gitignore +++ b/.gitignore @@ -24,6 +24,7 @@ workers/python/**/__pycache__/ workers/typescript/harness/dist/ workers/typescript/harness/dist-test/ workers/typescript/harness/src/generated/ +workers/**/.rubocop_cache/ workers/*/omes-temp-*/ workers/*/prepared/ workers/**/project-build-*/ diff --git a/cmd/dev/install.go b/cmd/dev/install.go index 0129e987..19c33d3d 100644 --- a/cmd/dev/install.go +++ b/cmd/dev/install.go @@ -224,6 +224,13 @@ func installRuby(ctx context.Context) error { return err } fmt.Println("✅ Ruby worker dependencies installed successfully!") + + harnessDir := targetDir + "/harness" + fmt.Println("Installing Ruby harness dependencies...") + if err := runCommandInDir(ctx, harnessDir, "bundle", "install"); err != nil { + return err + } + fmt.Println("✅ Ruby harness dependencies installed successfully!") return nil } diff --git a/cmd/dev/lint_and_format.go b/cmd/dev/lint_and_format.go index 456330e7..0cf1812a 100644 --- a/cmd/dev/lint_and_format.go +++ b/cmd/dev/lint_and_format.go @@ -228,8 +228,14 @@ func lintAndFormatRubyWorker(ctx context.Context, workerDir string) error { return err } - fmt.Println("Type checking Ruby worker...") - if err := runCommandInDir(ctx, workerDir, "bundle", "exec", "steep", "check"); err != nil { + harnessDir := workerDir + "/harness" + fmt.Println("Installing Ruby harness deps for type checking...") + if err := runCommandInDir(ctx, harnessDir, "bundle", "install"); err != nil { + return err + } + + fmt.Println("Type checking Ruby harness...") + if err := runCommandInDir(ctx, harnessDir, "bundle", "exec", "steep", "check"); err != nil { return err } diff --git a/cmd/dev/test.go b/cmd/dev/test.go index be4164ce..cf0f67b6 100644 --- a/cmd/dev/test.go +++ b/cmd/dev/test.go @@ -131,7 +131,7 @@ func runTypeScriptHarnessTests(ctx context.Context, repoDir string) error { } func runRubyHarnessTests(ctx context.Context, repoDir string) error { - workerDir := filepath.Join(repoDir, "workers", "ruby") + harnessDir := filepath.Join(repoDir, "workers", "ruby", "harness") rubyVersion, err := getVersion("ruby") if err != nil { return err @@ -140,26 +140,19 @@ func runRubyHarnessTests(ctx context.Context, repoDir string) error { return err } fmt.Println("Running Ruby harness tests...") - testFiles := []string{ - "projects/harness/tests/test_worker.rb", - "projects/harness/tests/test_project.rb", - } - for _, testFile := range testFiles { - fmt.Println("Running Ruby harness test:", testFile) - if err := runCommandInDir( - ctx, - workerDir, - "mise", - "exec", - "ruby@"+rubyVersion, - "--", - "bundle", - "exec", - "ruby", - testFile, - ); err != nil { - return fmt.Errorf("failed Ruby harness tests: %w", err) - } + if err := runCommandInDir( + ctx, + harnessDir, + "mise", + "exec", + "ruby@"+rubyVersion, + "--", + "bundle", + "exec", + "rake", + "test", + ); err != nil { + return fmt.Errorf("failed Ruby harness tests: %w", err) } fmt.Println("✅ Ruby harness tests completed successfully!") return nil diff --git a/workers/ruby/.rubocop.yml b/workers/ruby/.rubocop.yml index de5b2d26..9707db8e 100644 --- a/workers/ruby/.rubocop.yml +++ b/workers/ruby/.rubocop.yml @@ -3,8 +3,8 @@ AllCops: NewCops: enable SuggestExtensions: false Exclude: + - "harness/**/*" - "protos/**/*" - - "projects/harness/api/**/*" - "vendor/**/*" - "omes-temp-*/**/*" @@ -30,13 +30,10 @@ Style/GuardClause: # These methods are inherently branchy — they dispatch protobuf action # types via large case statements that mirror the proto schema. Metrics/MethodLength: - Max: 80 + Max: 35 Metrics/ClassLength: - Max: 400 - -Metrics/ModuleLength: - Max: 200 + Max: 300 Metrics/BlockLength: Enabled: false @@ -48,7 +45,7 @@ Metrics/PerceivedComplexity: Max: 15 Metrics/AbcSize: - Max: 100 + Max: 25 Metrics/ParameterLists: CountKeywordArgs: false diff --git a/workers/ruby/Gemfile b/workers/ruby/Gemfile index d8284816..994b0ec2 100644 --- a/workers/ruby/Gemfile +++ b/workers/ruby/Gemfile @@ -1,10 +1,9 @@ source 'https://rubygems.org' gemspec +gem 'harness', path: 'harness' group :development do - gem 'grpc-tools', '~> 1.80' - gem 'minitest' gem 'rbs', '~> 3.10' gem 'rubocop', '~> 1.0' gem 'steep', '~> 1.10' diff --git a/workers/ruby/Gemfile.lock b/workers/ruby/Gemfile.lock index a955bfd7..51ce1b9d 100644 --- a/workers/ruby/Gemfile.lock +++ b/workers/ruby/Gemfile.lock @@ -2,6 +2,14 @@ PATH remote: . specs: omes (0.1.0) + google-protobuf (~> 4.0) + harness (~> 0.1) + temporalio (~> 1.3) + +PATH + remote: harness + specs: + harness (0.1.0) google-protobuf (~> 4.0) grpc (~> 1.80) temporalio (~> 1.3) @@ -97,7 +105,6 @@ GEM grpc (1.80.0-x86_64-linux-musl) google-protobuf (>= 3.25, < 5.0) googleapis-common-protos-types (~> 1.0) - grpc-tools (1.80.0) i18n (1.14.8) concurrent-ruby (~> 1.0) json (2.19.2) @@ -213,8 +220,7 @@ PLATFORMS x86_64-linux-musl DEPENDENCIES - grpc-tools (~> 1.80) - minitest + harness! omes! rbs (~> 3.10) rubocop (~> 1.0) @@ -259,7 +265,7 @@ CHECKSUMS grpc (1.80.0-x86_64-darwin) sha256=4484d1280a43f94e8f1ab6ae53d282a4832f8ffb61b6996f43fc50716f464f1c grpc (1.80.0-x86_64-linux-gnu) sha256=25ba8beb5438152fb60656161dc729c1258c6963ec568361f601e19b2fb7ac23 grpc (1.80.0-x86_64-linux-musl) sha256=15a62e816c690bece9f5fd320a4b61aa735a2390daa8141596672a6e820ff342 - grpc-tools (1.80.0) sha256=49076fbc7b34556365694202ed7ffd1e959e68ba8c339e99b437ba2a985a2cf2 + harness (0.1.0) i18n (1.14.8) sha256=285778639134865c5e0f6269e0b818256017e8cde89993fdfcbfb64d088824a5 json (2.19.2) sha256=e7e1bd318b2c37c4ceee2444841c86539bc462e81f40d134cf97826cb14e83cf json-schema (6.2.0) sha256=e8bff46ed845a22c1ab2bd0d7eccf831c01fe23bb3920caa4c74db4306813666 diff --git a/workers/ruby/Steepfile b/workers/ruby/Steepfile deleted file mode 100644 index 91328441..00000000 --- a/workers/ruby/Steepfile +++ /dev/null @@ -1,20 +0,0 @@ -# frozen_string_literal: true - -D = Steep::Diagnostic - -common_diagnostics = { - D::Ruby::UnknownConstant => :information, - D::Ruby::UnannotatedEmptyCollection => :information, - D::Ruby::UndeclaredMethodDefinition => :information -} - -target :harness do - signature 'sig' - check 'projects/harness.rb' - check 'projects/harness' - ignore 'projects/harness/api', 'projects/harness/tests' - library 'json', 'logger', 'optparse', 'time' - configure_code_diagnostics do |hash| - hash.update(common_diagnostics) - end -end diff --git a/workers/ruby/harness/.rubocop.yml b/workers/ruby/harness/.rubocop.yml new file mode 100644 index 00000000..de5fde87 --- /dev/null +++ b/workers/ruby/harness/.rubocop.yml @@ -0,0 +1,37 @@ +AllCops: + TargetRubyVersion: 3.3 + NewCops: enable + SuggestExtensions: false + Exclude: + - "lib/harness/api/**/*" + - "vendor/**/*" + - "omes-temp-*/**/*" + +Style/Documentation: + Enabled: false + +Metrics/MethodLength: + Max: 60 + Exclude: + - "tests/**/*" + +Metrics/ClassLength: + Max: 300 + +Metrics/ModuleLength: + Max: 200 + +Metrics/BlockLength: + Enabled: false + +Metrics/CyclomaticComplexity: + Max: 15 + +Metrics/PerceivedComplexity: + Max: 15 + +Metrics/AbcSize: + Max: 75 + +Metrics/ParameterLists: + CountKeywordArgs: false diff --git a/workers/ruby/harness/Gemfile b/workers/ruby/harness/Gemfile new file mode 100644 index 00000000..570e4474 --- /dev/null +++ b/workers/ruby/harness/Gemfile @@ -0,0 +1,12 @@ +source 'https://rubygems.org' + +gemspec + +group :development do + gem 'grpc-tools', '~> 1.80' + gem 'minitest' + gem 'rake' + gem 'rbs', '~> 3.10' + gem 'rubocop', '~> 1.0' + gem 'steep', '~> 1.10' +end diff --git a/workers/ruby/harness/Gemfile.lock b/workers/ruby/harness/Gemfile.lock new file mode 100644 index 00000000..20e12ca3 --- /dev/null +++ b/workers/ruby/harness/Gemfile.lock @@ -0,0 +1,300 @@ +PATH + remote: . + specs: + harness (0.1.0) + google-protobuf (~> 4.0) + grpc (~> 1.80) + temporalio (~> 1.3) + +GEM + remote: https://rubygems.org/ + specs: + activesupport (8.1.3) + base64 + bigdecimal + concurrent-ruby (~> 1.0, >= 1.3.1) + connection_pool (>= 2.2.5) + drb + i18n (>= 1.6, < 2) + json + logger (>= 1.4.2) + minitest (>= 5.1) + securerandom (>= 0.3) + tzinfo (~> 2.0, >= 2.0.5) + uri (>= 0.13.1) + ast (2.4.3) + base64 (0.3.0) + bigdecimal (4.1.2) + concurrent-ruby (1.3.6) + connection_pool (3.0.2) + csv (3.3.5) + drb (2.2.3) + ffi (1.17.4) + ffi (1.17.4-aarch64-linux-gnu) + ffi (1.17.4-aarch64-linux-musl) + ffi (1.17.4-arm-linux-gnu) + ffi (1.17.4-arm-linux-musl) + ffi (1.17.4-arm64-darwin) + ffi (1.17.4-x86-linux-gnu) + ffi (1.17.4-x86-linux-musl) + ffi (1.17.4-x86_64-darwin) + ffi (1.17.4-x86_64-linux-gnu) + ffi (1.17.4-x86_64-linux-musl) + fileutils (1.8.0) + google-protobuf (4.34.1) + bigdecimal + rake (~> 13.3) + google-protobuf (4.34.1-aarch64-linux-gnu) + bigdecimal + rake (~> 13.3) + google-protobuf (4.34.1-aarch64-linux-musl) + bigdecimal + rake (~> 13.3) + google-protobuf (4.34.1-arm64-darwin) + bigdecimal + rake (~> 13.3) + google-protobuf (4.34.1-x86-linux-gnu) + bigdecimal + rake (~> 13.3) + google-protobuf (4.34.1-x86-linux-musl) + bigdecimal + rake (~> 13.3) + google-protobuf (4.34.1-x86_64-darwin) + bigdecimal + rake (~> 13.3) + google-protobuf (4.34.1-x86_64-linux-gnu) + bigdecimal + rake (~> 13.3) + google-protobuf (4.34.1-x86_64-linux-musl) + bigdecimal + rake (~> 13.3) + googleapis-common-protos-types (1.22.0) + google-protobuf (~> 4.26) + grpc (1.80.0) + google-protobuf (>= 3.25, < 5.0) + googleapis-common-protos-types (~> 1.0) + grpc (1.80.0-aarch64-linux-gnu) + google-protobuf (>= 3.25, < 5.0) + googleapis-common-protos-types (~> 1.0) + grpc (1.80.0-aarch64-linux-musl) + google-protobuf (>= 3.25, < 5.0) + googleapis-common-protos-types (~> 1.0) + grpc (1.80.0-arm64-darwin) + google-protobuf (>= 3.25, < 5.0) + googleapis-common-protos-types (~> 1.0) + grpc (1.80.0-x86-linux-gnu) + google-protobuf (>= 3.25, < 5.0) + googleapis-common-protos-types (~> 1.0) + grpc (1.80.0-x86-linux-musl) + google-protobuf (>= 3.25, < 5.0) + googleapis-common-protos-types (~> 1.0) + grpc (1.80.0-x86_64-darwin) + google-protobuf (>= 3.25, < 5.0) + googleapis-common-protos-types (~> 1.0) + grpc (1.80.0-x86_64-linux-gnu) + google-protobuf (>= 3.25, < 5.0) + googleapis-common-protos-types (~> 1.0) + grpc (1.80.0-x86_64-linux-musl) + google-protobuf (>= 3.25, < 5.0) + googleapis-common-protos-types (~> 1.0) + grpc-tools (1.80.0) + i18n (1.14.8) + concurrent-ruby (~> 1.0) + json (2.19.4) + language_server-protocol (3.17.0.5) + lint_roller (1.1.0) + listen (3.10.0) + logger + rb-fsevent (~> 0.10, >= 0.10.3) + rb-inotify (~> 0.9, >= 0.9.10) + logger (1.7.0) + minitest (6.0.5) + drb (~> 2.0) + prism (~> 1.5) + mutex_m (0.3.0) + parallel (2.0.1) + parser (3.3.11.1) + ast (~> 2.4.1) + racc + prism (1.9.0) + racc (1.8.1) + rainbow (3.1.1) + rake (13.4.2) + rb-fsevent (0.11.2) + rb-inotify (0.11.1) + ffi (~> 1.0) + rbs (3.10.4) + logger + tsort + regexp_parser (2.12.0) + rubocop (1.86.1) + json (~> 2.3) + language_server-protocol (~> 3.17.0.2) + lint_roller (~> 1.1.0) + parallel (>= 1.10) + parser (>= 3.3.0.2) + rainbow (>= 2.2.2, < 4.0) + regexp_parser (>= 2.9.3, < 3.0) + rubocop-ast (>= 1.49.0, < 2.0) + ruby-progressbar (~> 1.7) + unicode-display_width (>= 2.4.0, < 4.0) + rubocop-ast (1.49.1) + parser (>= 3.3.7.2) + prism (~> 1.7) + ruby-progressbar (1.13.0) + securerandom (0.4.1) + steep (1.10.0) + activesupport (>= 5.1) + concurrent-ruby (>= 1.1.10) + csv (>= 3.0.9) + fileutils (>= 1.1.0) + json (>= 2.1.0) + language_server-protocol (>= 3.17.0.4, < 4.0) + listen (~> 3.0) + logger (>= 1.3.0) + mutex_m (>= 0.3.0) + parser (>= 3.1) + rainbow (>= 2.2.2, < 4.0) + rbs (~> 3.9) + securerandom (>= 0.1) + strscan (>= 1.0.0) + terminal-table (>= 2, < 5) + uri (>= 0.12.0) + strscan (3.1.8) + temporalio (1.3.0) + google-protobuf (>= 3.25.0) + logger + temporalio (1.3.0-aarch64-linux) + google-protobuf (>= 3.25.0) + logger + temporalio (1.3.0-aarch64-linux-musl) + google-protobuf (>= 3.25.0) + logger + temporalio (1.3.0-arm64-darwin) + google-protobuf (>= 3.25.0) + logger + temporalio (1.3.0-x86_64-darwin) + google-protobuf (>= 3.25.0) + logger + temporalio (1.3.0-x86_64-linux) + google-protobuf (>= 3.25.0) + logger + temporalio (1.3.0-x86_64-linux-musl) + google-protobuf (>= 3.25.0) + logger + terminal-table (4.0.0) + unicode-display_width (>= 1.1.1, < 4) + tsort (0.2.0) + tzinfo (2.0.6) + concurrent-ruby (~> 1.0) + unicode-display_width (3.2.0) + unicode-emoji (~> 4.1) + unicode-emoji (4.2.0) + uri (1.1.1) + +PLATFORMS + aarch64-linux + aarch64-linux-gnu + aarch64-linux-musl + arm-linux-gnu + arm-linux-musl + arm64-darwin + ruby + x86-linux-gnu + x86-linux-musl + x86_64-darwin + x86_64-linux + x86_64-linux-gnu + x86_64-linux-musl + +DEPENDENCIES + grpc-tools (~> 1.80) + harness! + minitest + rake + rbs (~> 3.10) + rubocop (~> 1.0) + steep (~> 1.10) + +CHECKSUMS + activesupport (8.1.3) sha256=21a5e0dfbd4c3ddd9e1317ec6a4d782fa226e7867dc70b0743acda81a1dca20e + ast (2.4.3) sha256=954615157c1d6a382bc27d690d973195e79db7f55e9765ac7c481c60bdb4d383 + base64 (0.3.0) sha256=27337aeabad6ffae05c265c450490628ef3ebd4b67be58257393227588f5a97b + bigdecimal (4.1.2) sha256=53d217666027eab4280346fba98e7d5b66baaae1b9c3c1c0ffe89d48188a3fbd + concurrent-ruby (1.3.6) sha256=6b56837e1e7e5292f9864f34b69c5a2cbc75c0cf5338f1ce9903d10fa762d5ab + connection_pool (3.0.2) sha256=33fff5ba71a12d2aa26cb72b1db8bba2a1a01823559fb01d29eb74c286e62e0a + csv (3.3.5) sha256=6e5134ac3383ef728b7f02725d9872934f523cb40b961479f69cf3afa6c8e73f + drb (2.2.3) sha256=0b00d6fdb50995fe4a45dea13663493c841112e4068656854646f418fda13373 + ffi (1.17.4) sha256=bcd1642e06f0d16fc9e09ac6d49c3a7298b9789bcb58127302f934e437d60acf + ffi (1.17.4-aarch64-linux-gnu) sha256=b208f06f91ffd8f5e1193da3cae3d2ccfc27fc36fba577baf698d26d91c080df + ffi (1.17.4-aarch64-linux-musl) sha256=9286b7a615f2676245283aef0a0a3b475ae3aae2bb5448baace630bb77b91f39 + ffi (1.17.4-arm-linux-gnu) sha256=d6dbddf7cb77bf955411af5f187a65b8cd378cb003c15c05697f5feee1cb1564 + ffi (1.17.4-arm-linux-musl) sha256=9d4838ded0465bef6e2426935f6bcc93134b6616785a84ffd2a3d82bc3cf6f95 + ffi (1.17.4-arm64-darwin) sha256=19071aaf1419251b0a46852abf960e77330a3b334d13a4ab51d58b31a937001b + ffi (1.17.4-x86-linux-gnu) sha256=38e150df5f4ca555e25beca4090823ae09657bceded154e3c52f8631c1ed72cf + ffi (1.17.4-x86-linux-musl) sha256=fbeec0fc7c795bcf86f623bb18d31ea1820f7bd580e1703a3d3740d527437809 + ffi (1.17.4-x86_64-darwin) sha256=aa70390523cf3235096cf64962b709b4cfbd5c082a2cb2ae714eb0fe2ccda496 + ffi (1.17.4-x86_64-linux-gnu) sha256=9d3db14c2eae074b382fa9c083fe95aec6e0a1451da249eab096c34002bc752d + ffi (1.17.4-x86_64-linux-musl) sha256=3fdf9888483de005f8ef8d1cf2d3b20d86626af206cbf780f6a6a12439a9c49e + fileutils (1.8.0) sha256=8c6b1df54e2540bdb2f39258f08af78853aa70bad52b4d394bbc6424593c6e02 + google-protobuf (4.34.1) sha256=347181542b8d659c60f028fa3791c9cccce651a91ad27782dbc5c5e374796cdc + google-protobuf (4.34.1-aarch64-linux-gnu) sha256=f9c07607dc139c895f2792a7740fcd01cd94d4d7b0e0a939045b50d7999f0b1d + google-protobuf (4.34.1-aarch64-linux-musl) sha256=db58e5a4a492b43c6614486aea31b7fb86955b175d1d48f28ebf388f058d78a9 + google-protobuf (4.34.1-arm64-darwin) sha256=2745061f973119e6e7f3c81a0c77025d291a3caa6585a2cd24a25bbc7bedb267 + google-protobuf (4.34.1-x86-linux-gnu) sha256=b6da7891fe96b13038e5435d8ac8b8a84d78a468147a48a377fe8da40aba1c88 + google-protobuf (4.34.1-x86-linux-musl) sha256=ea0f453e78f4c30d0d9dbaa8cf9b33d2a1ea04ab2cad2c2a07e479411c05f1a9 + google-protobuf (4.34.1-x86_64-darwin) sha256=4dc498376e218871613589c4d872400d42ad9ae0c700bdb2606fe1c77a593075 + google-protobuf (4.34.1-x86_64-linux-gnu) sha256=87088c9fd8e47b5b40ca498fc1195add6149e941ff7e81c532a5b0b8876d4cc9 + google-protobuf (4.34.1-x86_64-linux-musl) sha256=8c0e91436fbe504ffc64f0bd621f2e69adbcce8ed2c58439d7a21117069cfdd7 + googleapis-common-protos-types (1.22.0) sha256=f97492b77bd6da0018c860d5004f512fe7cd165554d7019a8f4df6a56fbfc4c7 + grpc (1.80.0) sha256=2ded0c8bc3a1f3d34b8c790e00dd0120768ba0e9f9fd841e1dc67f7a2566d07d + grpc (1.80.0-aarch64-linux-gnu) sha256=1c15048887224575cb38026fea5b9abb14ae955bfce8beb0701e0946959a8520 + grpc (1.80.0-aarch64-linux-musl) sha256=9568f17e848d873fffd27351d4d1c918ea621e901b3dfcd62d7ff86dbae286a3 + grpc (1.80.0-arm64-darwin) sha256=c4b5871ad7673c526b64e54e70a99d84e35e2c26a1fc9a91f9c3341c7821e0c7 + grpc (1.80.0-x86-linux-gnu) sha256=b9fbce2480b2f1e965a6241a5df033c0d20a19f99a637e3cb8a12a6f5c3e68c6 + grpc (1.80.0-x86-linux-musl) sha256=db943737f134e61d88709ba697fe0830e794eb4a2d12bd2bb9203afefd334f21 + grpc (1.80.0-x86_64-darwin) sha256=4484d1280a43f94e8f1ab6ae53d282a4832f8ffb61b6996f43fc50716f464f1c + grpc (1.80.0-x86_64-linux-gnu) sha256=25ba8beb5438152fb60656161dc729c1258c6963ec568361f601e19b2fb7ac23 + grpc (1.80.0-x86_64-linux-musl) sha256=15a62e816c690bece9f5fd320a4b61aa735a2390daa8141596672a6e820ff342 + grpc-tools (1.80.0) sha256=49076fbc7b34556365694202ed7ffd1e959e68ba8c339e99b437ba2a985a2cf2 + harness (0.1.0) + i18n (1.14.8) sha256=285778639134865c5e0f6269e0b818256017e8cde89993fdfcbfb64d088824a5 + json (2.19.4) sha256=670a7d333fb3b18ca5b29cb255eb7bef099e40d88c02c80bd42a3f30fe5239ac + language_server-protocol (3.17.0.5) sha256=fd1e39a51a28bf3eec959379985a72e296e9f9acfce46f6a79d31ca8760803cc + lint_roller (1.1.0) sha256=2c0c845b632a7d172cb849cc90c1bce937a28c5c8ccccb50dfd46a485003cc87 + listen (3.10.0) sha256=c6e182db62143aeccc2e1960033bebe7445309c7272061979bb098d03760c9d2 + logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203 + minitest (6.0.5) sha256=f007d7246bf4feea549502842cd7c6aba8851cdc9c90ba06de9c476c0d01155c + mutex_m (0.3.0) sha256=cfcb04ac16b69c4813777022fdceda24e9f798e48092a2b817eb4c0a782b0751 + parallel (2.0.1) sha256=337782d3e39f4121e67563bf91dd8ece67f48923d90698614773a0ec9a5b2c7d + parser (3.3.11.1) sha256=d17ace7aabe3e72c3cc94043714be27cc6f852f104d81aa284c2281aecc65d54 + prism (1.9.0) sha256=7b530c6a9f92c24300014919c9dcbc055bf4cdf51ec30aed099b06cd6674ef85 + racc (1.8.1) sha256=4a7f6929691dbec8b5209a0b373bc2614882b55fc5d2e447a21aaa691303d62f + rainbow (3.1.1) sha256=039491aa3a89f42efa1d6dec2fc4e62ede96eb6acd95e52f1ad581182b79bc6a + rake (13.4.2) sha256=cb825b2bd5f1f8e91ca37bddb4b9aaf345551b4731da62949be002fa89283701 + rb-fsevent (0.11.2) sha256=43900b972e7301d6570f64b850a5aa67833ee7d87b458ee92805d56b7318aefe + rb-inotify (0.11.1) sha256=a0a700441239b0ff18eb65e3866236cd78613d6b9f78fea1f9ac47a85e47be6e + rbs (3.10.4) sha256=b17d7c4be4bb31a11a3b529830f0aa206a807ca42f2e7921a3027dfc6b7e5ce8 + regexp_parser (2.12.0) sha256=35a916a1d63190ab5c9009457136ae5f3c0c7512d60291d0d1378ba18ce08ebb + rubocop (1.86.1) sha256=44415f3f01d01a21e01132248d2fd0867572475b566ca188a0a42133a08d4531 + rubocop-ast (1.49.1) sha256=4412f3ee70f6fe4546cc489548e0f6fcf76cafcfa80fa03af67098ffed755035 + ruby-progressbar (1.13.0) sha256=80fc9c47a9b640d6834e0dc7b3c94c9df37f08cb072b7761e4a71e22cff29b33 + securerandom (0.4.1) sha256=cc5193d414a4341b6e225f0cb4446aceca8e50d5e1888743fac16987638ea0b1 + steep (1.10.0) sha256=1b295b55f9aaff1b8d3ee42453ee55bc2a1078fda0268f288edb2dc014f4d7d1 + strscan (3.1.8) sha256=aae2db611a225559f21ffbb71765c9a4e60fd262534a9ea84f4f11c7f32f679e + temporalio (1.3.0) sha256=672260631f419d1ec01a2230cc6a72d665ef9d385c5d96351bc68f639dbdc704 + temporalio (1.3.0-aarch64-linux) sha256=1ec4230251bc1771455fa20f1d1e9006639f3da3657ce4d15d09e27970d5a248 + temporalio (1.3.0-aarch64-linux-musl) sha256=135a676e60ba8ee6f49c7fa793505fee7479b78a3c0b31298073845560a32aed + temporalio (1.3.0-arm64-darwin) sha256=22c1f0fbbbfacf7c61ddd0d75e9ffc86590ba39ccbabb87f670821c9152fff7a + temporalio (1.3.0-x86_64-darwin) sha256=f2a4b35302564b6d2969a1daf6b2d7f2b86c9d1a50c59d1620b327b2af38c124 + temporalio (1.3.0-x86_64-linux) sha256=5122f3c2bd2b540565fc9ec4e2083401c00a7056c8408cd9922ebe570b366eef + temporalio (1.3.0-x86_64-linux-musl) sha256=0b9c19a94d6703155618d02facf46481467bde1cdcd86ccb6b4f38896d847560 + terminal-table (4.0.0) sha256=f504793203f8251b2ea7c7068333053f0beeea26093ec9962e62ea79f94301d2 + tsort (0.2.0) sha256=9650a793f6859a43b6641671278f79cfead60ac714148aabe4e3f0060480089f + tzinfo (2.0.6) sha256=8daf828cc77bcf7d63b0e3bdb6caa47e2272dcfaf4fbfe46f8c3a9df087a829b + unicode-display_width (3.2.0) sha256=0cdd96b5681a5949cdbc2c55e7b420facae74c4aaf9a9815eee1087cb1853c42 + unicode-emoji (4.2.0) sha256=519e69150f75652e40bf736106cfbc8f0f73aa3fb6a65afe62fefa7f80b0f80f + uri (1.1.1) sha256=379fa58d27ffb1387eaada68c749d1426738bd0f654d812fcc07e7568f5c57c6 + +BUNDLED WITH + 4.0.10 diff --git a/workers/ruby/harness/Rakefile b/workers/ruby/harness/Rakefile new file mode 100644 index 00000000..de925192 --- /dev/null +++ b/workers/ruby/harness/Rakefile @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +require 'rake/testtask' + +Rake::TestTask.new(:test) do |t| + t.libs << 'tests' + t.pattern = 'tests/test_*.rb' +end + +task default: :test diff --git a/workers/ruby/harness/Steepfile b/workers/ruby/harness/Steepfile new file mode 100644 index 00000000..9bcad474 --- /dev/null +++ b/workers/ruby/harness/Steepfile @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +D = Steep::Diagnostic + +target :harness do + signature 'sig' + check 'lib/harness.rb' + check 'lib/harness' + ignore 'lib/harness/api', 'tests' + library 'logger', 'optparse' + configure_code_diagnostics do |hash| + hash[D::Ruby::UnknownConstant] = :information + hash[D::Ruby::UnannotatedEmptyCollection] = :information + hash[D::Ruby::UndeclaredMethodDefinition] = :information + end +end diff --git a/workers/ruby/harness/harness.gemspec b/workers/ruby/harness/harness.gemspec new file mode 100644 index 00000000..442846f5 --- /dev/null +++ b/workers/ruby/harness/harness.gemspec @@ -0,0 +1,14 @@ +Gem::Specification.new do |s| + s.name = 'harness' + s.version = '0.1.0' + s.summary = 'Ruby harness for Omes projects' + s.authors = ['Temporal Technologies Inc'] + s.email = ['sdk@temporal.io'] + s.license = 'MIT' + s.files = Dir['lib/**/*.rb'] + Dir['sig/**/*.rbs'] + s.require_paths = ['lib'] + s.add_dependency 'google-protobuf', '~> 4.0' + s.add_dependency 'grpc', '~> 1.80' + s.add_dependency 'temporalio', '~> 1.3' + s.metadata['rubygems_mfa_required'] = 'true' +end diff --git a/workers/ruby/projects/harness.rb b/workers/ruby/harness/lib/harness.rb similarity index 100% rename from workers/ruby/projects/harness.rb rename to workers/ruby/harness/lib/harness.rb diff --git a/workers/ruby/projects/harness/api/api_pb.rb b/workers/ruby/harness/lib/harness/api/api_pb.rb similarity index 100% rename from workers/ruby/projects/harness/api/api_pb.rb rename to workers/ruby/harness/lib/harness/api/api_pb.rb diff --git a/workers/ruby/projects/harness/api/api_services_pb.rb b/workers/ruby/harness/lib/harness/api/api_services_pb.rb similarity index 100% rename from workers/ruby/projects/harness/api/api_services_pb.rb rename to workers/ruby/harness/lib/harness/api/api_services_pb.rb diff --git a/workers/ruby/projects/harness/client.rb b/workers/ruby/harness/lib/harness/client.rb similarity index 100% rename from workers/ruby/projects/harness/client.rb rename to workers/ruby/harness/lib/harness/client.rb diff --git a/workers/ruby/projects/harness/helpers.rb b/workers/ruby/harness/lib/harness/helpers.rb similarity index 100% rename from workers/ruby/projects/harness/helpers.rb rename to workers/ruby/harness/lib/harness/helpers.rb diff --git a/workers/ruby/projects/harness/main.rb b/workers/ruby/harness/lib/harness/main.rb similarity index 100% rename from workers/ruby/projects/harness/main.rb rename to workers/ruby/harness/lib/harness/main.rb diff --git a/workers/ruby/projects/harness/project.rb b/workers/ruby/harness/lib/harness/project.rb similarity index 97% rename from workers/ruby/projects/harness/project.rb rename to workers/ruby/harness/lib/harness/project.rb index de30d7c6..61807000 100644 --- a/workers/ruby/projects/harness/project.rb +++ b/workers/ruby/harness/lib/harness/project.rb @@ -4,9 +4,6 @@ require 'optparse' require 'grpc' -harness_projects_dir = File.expand_path('..', __dir__ || '.') -$LOAD_PATH.unshift(harness_projects_dir) unless $LOAD_PATH.include?(harness_projects_dir) - require_relative 'client' require_relative 'api/api_pb' require_relative 'api/api_services_pb' diff --git a/workers/ruby/projects/harness/worker.rb b/workers/ruby/harness/lib/harness/worker.rb similarity index 98% rename from workers/ruby/projects/harness/worker.rb rename to workers/ruby/harness/lib/harness/worker.rb index b863b4f2..40870d99 100644 --- a/workers/ruby/projects/harness/worker.rb +++ b/workers/ruby/harness/lib/harness/worker.rb @@ -17,10 +17,6 @@ module WorkerCLI module_function def run_cli(worker_factory, client_factory, argv) - run(worker_factory, client_factory, argv) - end - - def run(worker_factory, client_factory, argv) options = default_options build_parser(options).parse!(Array(argv).dup) diff --git a/workers/ruby/sig/harness.rbs b/workers/ruby/harness/sig/harness.rbs similarity index 100% rename from workers/ruby/sig/harness.rbs rename to workers/ruby/harness/sig/harness.rbs diff --git a/workers/ruby/projects/harness/tests/test_project.rb b/workers/ruby/harness/tests/test_project.rb similarity index 99% rename from workers/ruby/projects/harness/tests/test_project.rb rename to workers/ruby/harness/tests/test_project.rb index 63e466e8..488a4789 100644 --- a/workers/ruby/projects/harness/tests/test_project.rb +++ b/workers/ruby/harness/tests/test_project.rb @@ -1,12 +1,13 @@ # frozen_string_literal: true +require 'bundler/setup' require 'minitest/autorun' require 'securerandom' require 'temporalio/testing/workflow_environment' require 'temporalio/worker' require 'temporalio/workflow' require 'grpc' -require_relative '../../harness' +require 'harness' class HarnessProjectTest < Minitest::Test class ProjectHarnessEchoWorkflow < Temporalio::Workflow::Definition diff --git a/workers/ruby/harness/tests/test_worker.rb b/workers/ruby/harness/tests/test_worker.rb new file mode 100644 index 00000000..d1097379 --- /dev/null +++ b/workers/ruby/harness/tests/test_worker.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true + +require 'bundler/setup' +require 'logger' +require 'minitest/autorun' +require 'harness' + +class HarnessWorkerTest < Minitest::Test + def test_run_passes_shared_client_and_context_to_each_worker_factory + client = Object.new + worker_factory_calls = [] + created_workers = [Object.new, Object.new] + captured_workers = nil + + worker_factory = lambda do |given_client, context| + worker_factory_calls << [given_client, context] + created_workers.fetch(worker_factory_calls.length - 1) + end + client_factory = ->(_config) { client } + + with_stubbed_run_all(lambda do |*workers, **_kwargs| + captured_workers = workers + end) do + Harness::WorkerCLI.run_cli( + worker_factory, + client_factory, + [ + '--task-queue', 'omes', + '--task-queue-suffix-index-start', '1', + '--task-queue-suffix-index-end', '2' + ] + ) + end + + assert_equal created_workers, captured_workers + assert_equal 2, worker_factory_calls.length + + assert_same client, worker_factory_calls[0][0] + assert_same client, worker_factory_calls[1][0] + assert_same worker_factory_calls[0][1].worker_kwargs, worker_factory_calls[1][1].worker_kwargs + assert_equal 'omes-1', worker_factory_calls[0][1].task_queue + assert_equal 'omes-2', worker_factory_calls[1][1].task_queue + end + + private + + def with_stubbed_run_all(stub_implementation) + singleton = Temporalio::Worker.singleton_class + singleton.send(:alias_method, :__original_run_all_for_test, :run_all) + singleton.send(:define_method, :run_all, &stub_implementation) + yield + ensure + if singleton.method_defined?(:__original_run_all_for_test) + singleton.send(:remove_method, :run_all) + singleton.send(:alias_method, :run_all, :__original_run_all_for_test) + singleton.send(:remove_method, :__original_run_all_for_test) + end + end +end diff --git a/workers/ruby/kitchen_sink_app.rb b/workers/ruby/kitchen_sink_app.rb index 77834f5f..7705b8f2 100644 --- a/workers/ruby/kitchen_sink_app.rb +++ b/workers/ruby/kitchen_sink_app.rb @@ -1,9 +1,9 @@ # frozen_string_literal: true +require 'harness' require 'temporalio/worker' require_relative 'activities' require_relative 'kitchen_sink' -require_relative 'projects/harness' module KitchenSinkApp module_function diff --git a/workers/ruby/omes.gemspec b/workers/ruby/omes.gemspec index 0e1a3d7c..52a4cf00 100644 --- a/workers/ruby/omes.gemspec +++ b/workers/ruby/omes.gemspec @@ -5,10 +5,10 @@ Gem::Specification.new do |s| s.authors = ['Temporal Technologies Inc'] s.email = ['sdk@temporal.io'] s.license = 'MIT' - s.files = Dir['**/*.rb'] + s.files = Dir['*.rb'] + Dir['protos/**/*.rb'] s.require_paths = ['.'] s.add_dependency 'google-protobuf', '~> 4.0' - s.add_dependency 'grpc', '~> 1.80' + s.add_dependency 'harness', '~> 0.1' s.add_dependency 'temporalio', '~> 1.3' s.metadata['rubygems_mfa_required'] = 'true' end diff --git a/workers/ruby/projects/harness/tests/test_worker.rb b/workers/ruby/projects/harness/tests/test_worker.rb deleted file mode 100644 index 77f05c30..00000000 --- a/workers/ruby/projects/harness/tests/test_worker.rb +++ /dev/null @@ -1,125 +0,0 @@ -# frozen_string_literal: true - -require 'logger' -require 'minitest/autorun' -require_relative '../../harness' - -class HarnessWorkerTest < Minitest::Test - FakeRunnableWorker = Struct.new( - :on_run, - :run_calls, - :shutdown_calls, - :received_shutdown_signals - ) do - def run(shutdown_signals: []) - self.run_calls += 1 - self.received_shutdown_signals = shutdown_signals - on_run.call - end - - def shutdown - self.shutdown_calls += 1 - end - end - - def test_run_passes_shared_client_and_context_to_each_worker_factory - client = Object.new - built_config = nil - worker_factory_calls = [] - created_workers = [Object.new, Object.new] - captured_workers = nil - captured_kwargs = nil - - worker_factory = lambda do |given_client, context| - worker_factory_calls << [given_client, context] - created_workers.fetch(worker_factory_calls.length - 1) - end - client_factory = lambda do |config| - built_config = config - client - end - - with_stubbed_run_all(lambda do |*workers, **kwargs| - captured_workers = workers - captured_kwargs = kwargs - end) do - Harness::WorkerCLI.run( - worker_factory, - client_factory, - [ - '--task-queue', 'omes', - '--task-queue-suffix-index-start', '1', - '--task-queue-suffix-index-end', '2' - ] - ) - end - - assert_equal 'localhost:7233', built_config.target_host - assert_equal 'default', built_config.namespace - assert_nil built_config.api_key - assert_nil built_config.tls - assert_instance_of Temporalio::Runtime, built_config.runtime - assert_equal created_workers, captured_workers - assert_equal({ shutdown_signals: ['SIGINT'] }, captured_kwargs) - assert_equal 2, worker_factory_calls.length - assert_same client, worker_factory_calls[0][0] - assert_same client, worker_factory_calls[1][0] - assert_equal 'omes-1', worker_factory_calls[0][1].task_queue - assert_equal 'omes-2', worker_factory_calls[1][1].task_queue - assert_instance_of Logger, worker_factory_calls[0][1].logger - end - - def test_run_workers_shuts_down_all_workers_when_one_fails - boom = RuntimeError.new('boom') - failing_worker = FakeRunnableWorker.new( - on_run: -> { raise boom }, - run_calls: 0, - shutdown_calls: 0, - received_shutdown_signals: nil - ) - successful_worker = FakeRunnableWorker.new( - on_run: -> {}, - run_calls: 0, - shutdown_calls: 0, - received_shutdown_signals: nil - ) - - error = with_stubbed_run_all(lambda do |*workers, **kwargs| - first_error = nil - workers.each do |worker| - worker.run(shutdown_signals: kwargs[:shutdown_signals]) - rescue StandardError => e - first_error ||= e - end - workers.each(&:shutdown) - raise first_error if first_error - end) do - assert_raises(RuntimeError) do - Harness::WorkerCLI.run_workers([failing_worker, successful_worker]) - end - end - - assert_same boom, error - assert_equal ['SIGINT'], failing_worker.received_shutdown_signals - assert_equal ['SIGINT'], successful_worker.received_shutdown_signals - assert_equal 1, failing_worker.run_calls - assert_equal 1, successful_worker.run_calls - assert_equal 1, failing_worker.shutdown_calls - assert_equal 1, successful_worker.shutdown_calls - end - - private - - def with_stubbed_run_all(stub_implementation) - singleton = Temporalio::Worker.singleton_class - singleton.send(:alias_method, :__original_run_all_for_test, :run_all) - singleton.send(:define_method, :run_all, &stub_implementation) - yield - ensure - if singleton.method_defined?(:__original_run_all_for_test) - singleton.send(:remove_method, :run_all) - singleton.send(:alias_method, :run_all, :__original_run_all_for_test) - singleton.send(:remove_method, :__original_run_all_for_test) - end - end -end diff --git a/workers/ruby/runner.rb b/workers/ruby/runner.rb index 8bc5a4d8..af854adc 100644 --- a/workers/ruby/runner.rb +++ b/workers/ruby/runner.rb @@ -1,4 +1,7 @@ +# frozen_string_literal: true + +require 'bundler/setup' +require 'harness' require_relative 'kitchen_sink_app' -require_relative 'projects/harness' Harness.run(KitchenSinkApp.app) if __FILE__ == $PROGRAM_NAME From da374ac21d2977d4c56c28003ffafd14f6e27c08 Mon Sep 17 00:00:00 2001 From: Thomas Hardy Date: Mon, 27 Apr 2026 16:16:56 -0700 Subject: [PATCH 4/5] cleanup lint-and-format, ruby build, bump features to use latest sdkbuild ruby for local deps --- cmd/dev/lint_and_format.go | 10 ++++++-- go.mod | 2 +- go.sum | 4 +-- workers/build.go | 37 +++++++++------------------- workers/ruby/harness/harness.gemspec | 3 +++ 5 files changed, 25 insertions(+), 31 deletions(-) diff --git a/cmd/dev/lint_and_format.go b/cmd/dev/lint_and_format.go index 0cf1812a..a597a904 100644 --- a/cmd/dev/lint_and_format.go +++ b/cmd/dev/lint_and_format.go @@ -229,8 +229,14 @@ func lintAndFormatRubyWorker(ctx context.Context, workerDir string) error { } harnessDir := workerDir + "/harness" - fmt.Println("Installing Ruby harness deps for type checking...") - if err := runCommandInDir(ctx, harnessDir, "bundle", "install"); err != nil { + + fmt.Println("Formatting Ruby harness...") + if err := runCommandInDir(ctx, harnessDir, "bundle", "exec", "rubocop", "-A"); err != nil { + return err + } + + fmt.Println("Linting Ruby harness...") + if err := runCommandInDir(ctx, harnessDir, "bundle", "exec", "rubocop"); err != nil { return err } diff --git a/go.mod b/go.mod index 9b81b1df..e9e301a6 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( github.com/spf13/cobra v1.7.0 github.com/spf13/pflag v1.0.5 github.com/stretchr/testify v1.11.1 - github.com/temporalio/features v0.0.0-20260331150122-757294c2a9e9 + github.com/temporalio/features v0.0.0-20260427223549-86e4c0deedd7 github.com/temporalio/omes/workers/go/harness/api v0.0.0-00010101000000-000000000000 go.temporal.io/api v1.62.7 go.temporal.io/sdk v1.42.0 diff --git a/go.sum b/go.sum index baefd86f..d050b597 100644 --- a/go.sum +++ b/go.sum @@ -97,8 +97,8 @@ github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -github.com/temporalio/features v0.0.0-20260331150122-757294c2a9e9 h1:9ifP6KcfPhf22qFb4t7L8M11nkWGD8cyAFQ3fUrLqFw= -github.com/temporalio/features v0.0.0-20260331150122-757294c2a9e9/go.mod h1:LsZUh/AkCjnOKrEpnEklGmsJ3g758Hyq2X+hzMofMjw= +github.com/temporalio/features v0.0.0-20260427223549-86e4c0deedd7 h1:gBLwgyi8xw0oqZgxMwxTRGIfP8RxtI7r1igm3G6aXGY= +github.com/temporalio/features v0.0.0-20260427223549-86e4c0deedd7/go.mod h1:BUWwBMK+Ga5h9xPTS7+kmutSIfY4K1gfSH8eG7fSbU0= github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4= github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4= github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso= diff --git a/workers/build.go b/workers/build.go index e8085b8f..f90c4fa6 100644 --- a/workers/build.go +++ b/workers/build.go @@ -285,37 +285,22 @@ func (b *Builder) buildDotNet(ctx context.Context, baseDir string) (sdkbuild.Pro } func (b *Builder) buildRuby(ctx context.Context, baseDir string) (sdkbuild.Program, error) { - // If version not provided, read the version constraint from the gemspec. - version := b.SdkOptions.Version - if version == "" { - gemspecBytes, err := os.ReadFile(filepath.Join(baseDir, "omes.gemspec")) - if err != nil { - return nil, fmt.Errorf("failed reading omes.gemspec: %w", err) - } - for _, line := range strings.Split(string(gemspecBytes), "\n") { - line = strings.TrimSpace(line) - if strings.Contains(line, "'temporalio'") || strings.Contains(line, `"temporalio"`) { - parts := strings.Split(line, ",") - if len(parts) >= 2 { - version = strings.TrimSpace(parts[1]) - version = strings.Trim(version, `"'`) - } - break - } - } - if version == "" { - return nil, fmt.Errorf("version not found in omes.gemspec") - } - } - - prog, err := sdkbuild.BuildRubyProgram(ctx, sdkbuild.BuildRubyProgramOptions{ + options := sdkbuild.BuildRubyProgramOptions{ BaseDir: baseDir, SourceDir: baseDir, DirName: b.DirName, - Version: version, + Version: b.SdkOptions.Version, Stdout: b.stdout, Stderr: b.stderr, - }) + } + if b.ProjectName == "" { + options.MoreDependencies = []sdkbuild.RubyDependency{{ + Name: "harness", + Path: filepath.Join(baseDir, "harness"), + }} + } + + prog, err := sdkbuild.BuildRubyProgram(ctx, options) if err != nil { return nil, fmt.Errorf("failed preparing: %w", err) } diff --git a/workers/ruby/harness/harness.gemspec b/workers/ruby/harness/harness.gemspec index 442846f5..e0284f40 100644 --- a/workers/ruby/harness/harness.gemspec +++ b/workers/ruby/harness/harness.gemspec @@ -1,3 +1,5 @@ +# frozen_string_literal: true + Gem::Specification.new do |s| s.name = 'harness' s.version = '0.1.0' @@ -5,6 +7,7 @@ Gem::Specification.new do |s| s.authors = ['Temporal Technologies Inc'] s.email = ['sdk@temporal.io'] s.license = 'MIT' + s.required_ruby_version = '>= 3.3' s.files = Dir['lib/**/*.rb'] + Dir['sig/**/*.rbs'] s.require_paths = ['lib'] s.add_dependency 'google-protobuf', '~> 4.0' From 051d0ae5a1ce0b1846c9dee3548ba228046f03a8 Mon Sep 17 00:00:00 2001 From: Thomas Hardy Date: Mon, 27 Apr 2026 16:24:46 -0700 Subject: [PATCH 5/5] go mod tidy (features bump) --- workers/go/go.sum | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/workers/go/go.sum b/workers/go/go.sum index 4eb6a92b..a07b287b 100644 --- a/workers/go/go.sum +++ b/workers/go/go.sum @@ -88,8 +88,8 @@ github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -github.com/temporalio/features v0.0.0-20260331150122-757294c2a9e9 h1:9ifP6KcfPhf22qFb4t7L8M11nkWGD8cyAFQ3fUrLqFw= -github.com/temporalio/features v0.0.0-20260331150122-757294c2a9e9/go.mod h1:LsZUh/AkCjnOKrEpnEklGmsJ3g758Hyq2X+hzMofMjw= +github.com/temporalio/features v0.0.0-20260427223549-86e4c0deedd7 h1:gBLwgyi8xw0oqZgxMwxTRGIfP8RxtI7r1igm3G6aXGY= +github.com/temporalio/features v0.0.0-20260427223549-86e4c0deedd7/go.mod h1:BUWwBMK+Ga5h9xPTS7+kmutSIfY4K1gfSH8eG7fSbU0= github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4= github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4= github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso=