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 b37c91cd..a597a904 100644 --- a/cmd/dev/lint_and_format.go +++ b/cmd/dev/lint_and_format.go @@ -228,6 +228,23 @@ func lintAndFormatRubyWorker(ctx context.Context, workerDir string) error { return err } + harnessDir := workerDir + "/harness" + + 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 + } + + fmt.Println("Type checking Ruby harness...") + if err := runCommandInDir(ctx, harnessDir, "bundle", "exec", "steep", "check"); err != nil { + return err + } + fmt.Println("✅ Ruby lint-and-format completed successfully!") return nil } diff --git a/cmd/dev/test.go b/cmd/dev/test.go index 16e34303..cf0f67b6 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,34 @@ func runTypeScriptHarnessTests(ctx context.Context, repoDir string) error { return nil } +func runRubyHarnessTests(ctx context.Context, repoDir string) error { + harnessDir := filepath.Join(repoDir, "workers", "ruby", "harness") + rubyVersion, err := getVersion("ruby") + if err != nil { + return err + } + if err := checkMise(); err != nil { + return err + } + fmt.Println("Running Ruby harness tests...") + 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 +} + func testWorkerLocally(ctx context.Context, repoDir, language, sdkVersion string) error { args := []string{ "go", "run", "./cmd", "run-scenario-with-worker", 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/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= diff --git a/workers/ruby/.rubocop.yml b/workers/ruby/.rubocop.yml index 98762d82..9707db8e 100644 --- a/workers/ruby/.rubocop.yml +++ b/workers/ruby/.rubocop.yml @@ -1,7 +1,9 @@ AllCops: TargetRubyVersion: 3.3 NewCops: enable + SuggestExtensions: false Exclude: + - "harness/**/*" - "protos/**/*" - "vendor/**/*" - "omes-temp-*/**/*" @@ -44,3 +46,6 @@ Metrics/PerceivedComplexity: Metrics/AbcSize: Max: 25 + +Metrics/ParameterLists: + CountKeywordArgs: false diff --git a/workers/ruby/Gemfile b/workers/ruby/Gemfile index 58ce25b8..994b0ec2 100644 --- a/workers/ruby/Gemfile +++ b/workers/ruby/Gemfile @@ -1,7 +1,10 @@ source 'https://rubygems.org' gemspec +gem 'harness', path: 'harness' group :development do + 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 dde9193e..51ce1b9d 100644 --- a/workers/ruby/Gemfile.lock +++ b/workers/ruby/Gemfile.lock @@ -3,15 +3,52 @@ PATH 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) 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) @@ -39,15 +76,54 @@ 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) + 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) @@ -57,6 +133,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) @@ -74,6 +156,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 @@ -95,9 +196,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 @@ -113,13 +220,32 @@ PLATFORMS x86_64-linux-musl DEPENDENCIES + harness! 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 @@ -129,12 +255,27 @@ 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 + harness (0.1.0) + 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 @@ -143,10 +284,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 @@ -154,8 +301,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/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..e0284f40 --- /dev/null +++ b/workers/ruby/harness/harness.gemspec @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +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.required_ruby_version = '>= 3.3' + 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/harness/lib/harness.rb b/workers/ruby/harness/lib/harness.rb new file mode 100644 index 00000000..e55767b0 --- /dev/null +++ b/workers/ruby/harness/lib/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/harness/lib/harness/api/api_pb.rb b/workers/ruby/harness/lib/harness/api/api_pb.rb new file mode 100644 index 00000000..5bd81124 --- /dev/null +++ b/workers/ruby/harness/lib/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 + }.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/harness/lib/harness/main.rb b/workers/ruby/harness/lib/harness/main.rb new file mode 100644 index 00000000..22d9bc25 --- /dev/null +++ b/workers/ruby/harness/lib/harness/main.rb @@ -0,0 +1,29 @@ +# 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.first == 'worker' + WorkerCLI.run_cli(app.worker, app.client_factory, argv.drop(1)) + elsif argv.first == 'project-server' + 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 + raise SystemExit, "Unknown command: #{argv.first(1)}. Expected 'worker' or 'project-server'" + end + end +end diff --git a/workers/ruby/harness/lib/harness/project.rb b/workers/ruby/harness/lib/harness/project.rb new file mode 100644 index 00000000..61807000 --- /dev/null +++ b/workers/ruby/harness/lib/harness/project.rb @@ -0,0 +1,175 @@ +# frozen_string_literal: true + +require 'logger' +require 'optparse' +require 'grpc' + +require_relative 'client' +require_relative 'api/api_pb' +require_relative 'api/api_services_pb' + +module Harness + ProjectRunMetadata = Data.define( + :run_id, + :execution_id + ) + + ProjectInitContext = Data.define( + :logger, + :run, + :task_queue, + :config_json + ) + + ProjectExecuteContext = Data.define( + :logger, + :run, + :task_queue, + :iteration, + :payload + ) + + ProjectHandlers = Data.define( + :execute, + :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 + @run = nil + @logger = Logger.new($stderr) + end + + def init(request, _call) + 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 + 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( + 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 => e + raise grpc_status(GRPC::Core::StatusCodes::INVALID_ARGUMENT, e.message) + end + + begin + client = @client_factory.call(config) + rescue StandardError => e + raise grpc_status(GRPC::Core::StatusCodes::INTERNAL, "failed to create client: #{e}") + 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 => e + raise grpc_status(GRPC::Core::StatusCodes::INTERNAL, "init handler failed: #{e}") + end + end + + @client = client + @run = run + Temporal::Omes::Projects::V1::InitResponse.new + end + + def execute(request, _call) + 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, + ProjectExecuteContext.new( + logger: @logger, + run: @run, + task_queue: request.task_queue, + iteration: request.iteration, + payload: request.payload + ) + ) + Temporal::Omes::Projects::V1::ExecuteResponse.new + rescue StandardError => e + raise if e.is_a?(GRPC::BadStatus) + + raise grpc_status(GRPC::Core::StatusCodes::INTERNAL, "execute handler failed: #{e}") + 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/harness/lib/harness/worker.rb b/workers/ruby/harness/lib/harness/worker.rb new file mode 100644 index 00000000..40870d99 --- /dev/null +++ b/workers/ruby/harness/lib/harness/worker.rb @@ -0,0 +1,179 @@ +# frozen_string_literal: true + +require 'optparse' +require 'temporalio/worker' +require_relative 'client' +require_relative 'helpers' + +module Harness + WorkerContext = Data.define( + :logger, + :task_queue, + :err_on_unimplemented, + :worker_kwargs + ) + + module WorkerCLI + module_function + + def run_cli(worker_factory, client_factory, argv) + options = default_options + build_parser(options).parse!(Array(argv).dup) + + 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( + 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) 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) 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) 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 } + 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 + + 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 [true, false].include?(value) + + %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/harness/sig/harness.rbs b/workers/ruby/harness/sig/harness.rbs new file mode 100644 index 00000000..1d1b943a --- /dev/null +++ b/workers/ruby/harness/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 diff --git a/workers/ruby/harness/tests/test_project.rb b/workers/ruby/harness/tests/test_project.rb new file mode 100644 index 00000000..488a4789 --- /dev/null +++ b/workers/ruby/harness/tests/test_project.rb @@ -0,0 +1,285 @@ +# 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 '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 + ), + 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 + 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 + + Temporalio::Testing::WorkflowEnvironment.start_local 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/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 new file mode 100644 index 00000000..7705b8f2 --- /dev/null +++ b/workers/ruby/kitchen_sink_app.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +require 'harness' +require 'temporalio/worker' +require_relative 'activities' +require_relative 'kitchen_sink' + +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..52a4cf00 100644 --- a/workers/ruby/omes.gemspec +++ b/workers/ruby/omes.gemspec @@ -5,9 +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 'harness', '~> 0.1' s.add_dependency 'temporalio', '~> 1.3' s.metadata['rubygems_mfa_required'] = 'true' end diff --git a/workers/ruby/runner.rb b/workers/ruby/runner.rb index b974bc53..af854adc 100644 --- a/workers/ruby/runner.rb +++ b/workers/ruby/runner.rb @@ -1,192 +1,7 @@ -require 'logger' -require 'json' -require 'optparse' -require 'temporalio/client' -require 'temporalio/runtime' -require 'temporalio/worker' -require_relative 'kitchen_sink' -require_relative 'activities' +# frozen_string_literal: true -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 +require 'bundler/setup' +require 'harness' +require_relative 'kitchen_sink_app' -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") }