diff --git a/Makefile b/Makefile index 6967cb5b..0ba418f5 100644 --- a/Makefile +++ b/Makefile @@ -2,8 +2,14 @@ PROTO_ROOT := proto PROTO_FILES = $(shell find $(PROTO_ROOT) -name "*.proto") PROTO_DIRS = $(sort $(dir $(PROTO_FILES))) PROTO_OUT := lib/gen +PROTO_TESTING_ROOT := lib/temporal/testing/proto +PROTO_TESTING_FILES = $(shell find $(PROTO_TESTING_ROOT) -name "*.proto") +PROTO_TESTING_DIRS = $(sort $(dir $(PROTO_TESTING_FILES))) proto: $(foreach PROTO_DIR,$(PROTO_DIRS),bundle exec grpc_tools_ruby_protoc -Iproto --ruby_out=$(PROTO_OUT) --grpc_out=$(PROTO_OUT) $(PROTO_DIR)*.proto;) -.PHONY: proto +proto-testing: + $(foreach PROTO_DIR,$(PROTO_TESTING_DIRS),bundle exec grpc_tools_ruby_protoc --proto_path="lib/temporal/testing/proto" --ruby_out=$(PROTO_OUT) --grpc_out=$(PROTO_OUT) $(PROTO_DIR)*.proto;) + +.PHONY: proto, proto-testing \ No newline at end of file diff --git a/lib/gen/dependencies/gogoproto/gogo_pb.rb b/lib/gen/dependencies/gogoproto/gogo_pb.rb index d63bf773..2d29c45b 100644 --- a/lib/gen/dependencies/gogoproto/gogo_pb.rb +++ b/lib/gen/dependencies/gogoproto/gogo_pb.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true # Generated by the protocol buffer compiler. DO NOT EDIT! # source: dependencies/gogoproto/gogo.proto @@ -5,10 +6,11 @@ require 'google/protobuf/descriptor_pb' -Google::Protobuf::DescriptorPool.generated_pool.build do - add_file("dependencies/gogoproto/gogo.proto", :syntax => :proto2) do - end -end + +descriptor_data = "\n!dependencies/gogoproto/gogo.proto\x12\tgogoproto\x1a google/protobuf/descriptor.proto:;\n\x13goproto_enum_prefix\x12\x1c.google.protobuf.EnumOptions\x18\xb1\xe4\x03 \x01(\x08:=\n\x15goproto_enum_stringer\x12\x1c.google.protobuf.EnumOptions\x18\xc5\xe4\x03 \x01(\x08:5\n\renum_stringer\x12\x1c.google.protobuf.EnumOptions\x18\xc6\xe4\x03 \x01(\x08:7\n\x0f\x65num_customname\x12\x1c.google.protobuf.EnumOptions\x18\xc7\xe4\x03 \x01(\t:0\n\x08\x65numdecl\x12\x1c.google.protobuf.EnumOptions\x18\xc8\xe4\x03 \x01(\x08:A\n\x14\x65numvalue_customname\x12!.google.protobuf.EnumValueOptions\x18\xd1\x83\x04 \x01(\t:;\n\x13goproto_getters_all\x12\x1c.google.protobuf.FileOptions\x18\x99\xec\x03 \x01(\x08:?\n\x17goproto_enum_prefix_all\x12\x1c.google.protobuf.FileOptions\x18\x9a\xec\x03 \x01(\x08:<\n\x14goproto_stringer_all\x12\x1c.google.protobuf.FileOptions\x18\x9b\xec\x03 \x01(\x08:9\n\x11verbose_equal_all\x12\x1c.google.protobuf.FileOptions\x18\x9c\xec\x03 \x01(\x08:0\n\x08\x66\x61\x63\x65_all\x12\x1c.google.protobuf.FileOptions\x18\x9d\xec\x03 \x01(\x08:4\n\x0cgostring_all\x12\x1c.google.protobuf.FileOptions\x18\x9e\xec\x03 \x01(\x08:4\n\x0cpopulate_all\x12\x1c.google.protobuf.FileOptions\x18\x9f\xec\x03 \x01(\x08:4\n\x0cstringer_all\x12\x1c.google.protobuf.FileOptions\x18\xa0\xec\x03 \x01(\x08:3\n\x0bonlyone_all\x12\x1c.google.protobuf.FileOptions\x18\xa1\xec\x03 \x01(\x08:1\n\tequal_all\x12\x1c.google.protobuf.FileOptions\x18\xa5\xec\x03 \x01(\x08:7\n\x0f\x64\x65scription_all\x12\x1c.google.protobuf.FileOptions\x18\xa6\xec\x03 \x01(\x08:3\n\x0btestgen_all\x12\x1c.google.protobuf.FileOptions\x18\xa7\xec\x03 \x01(\x08:4\n\x0c\x62\x65nchgen_all\x12\x1c.google.protobuf.FileOptions\x18\xa8\xec\x03 \x01(\x08:5\n\rmarshaler_all\x12\x1c.google.protobuf.FileOptions\x18\xa9\xec\x03 \x01(\x08:7\n\x0funmarshaler_all\x12\x1c.google.protobuf.FileOptions\x18\xaa\xec\x03 \x01(\x08:<\n\x14stable_marshaler_all\x12\x1c.google.protobuf.FileOptions\x18\xab\xec\x03 \x01(\x08:1\n\tsizer_all\x12\x1c.google.protobuf.FileOptions\x18\xac\xec\x03 \x01(\x08:A\n\x19goproto_enum_stringer_all\x12\x1c.google.protobuf.FileOptions\x18\xad\xec\x03 \x01(\x08:9\n\x11\x65num_stringer_all\x12\x1c.google.protobuf.FileOptions\x18\xae\xec\x03 \x01(\x08:<\n\x14unsafe_marshaler_all\x12\x1c.google.protobuf.FileOptions\x18\xaf\xec\x03 \x01(\x08:>\n\x16unsafe_unmarshaler_all\x12\x1c.google.protobuf.FileOptions\x18\xb0\xec\x03 \x01(\x08:B\n\x1agoproto_extensions_map_all\x12\x1c.google.protobuf.FileOptions\x18\xb1\xec\x03 \x01(\x08:@\n\x18goproto_unrecognized_all\x12\x1c.google.protobuf.FileOptions\x18\xb2\xec\x03 \x01(\x08:8\n\x10gogoproto_import\x12\x1c.google.protobuf.FileOptions\x18\xb3\xec\x03 \x01(\x08:6\n\x0eprotosizer_all\x12\x1c.google.protobuf.FileOptions\x18\xb4\xec\x03 \x01(\x08:3\n\x0b\x63ompare_all\x12\x1c.google.protobuf.FileOptions\x18\xb5\xec\x03 \x01(\x08:4\n\x0ctypedecl_all\x12\x1c.google.protobuf.FileOptions\x18\xb6\xec\x03 \x01(\x08:4\n\x0c\x65numdecl_all\x12\x1c.google.protobuf.FileOptions\x18\xb7\xec\x03 \x01(\x08:<\n\x14goproto_registration\x12\x1c.google.protobuf.FileOptions\x18\xb8\xec\x03 \x01(\x08:7\n\x0fmessagename_all\x12\x1c.google.protobuf.FileOptions\x18\xb9\xec\x03 \x01(\x08:=\n\x15goproto_sizecache_all\x12\x1c.google.protobuf.FileOptions\x18\xba\xec\x03 \x01(\x08:;\n\x13goproto_unkeyed_all\x12\x1c.google.protobuf.FileOptions\x18\xbb\xec\x03 \x01(\x08::\n\x0fgoproto_getters\x12\x1f.google.protobuf.MessageOptions\x18\x81\xf4\x03 \x01(\x08:;\n\x10goproto_stringer\x12\x1f.google.protobuf.MessageOptions\x18\x83\xf4\x03 \x01(\x08:8\n\rverbose_equal\x12\x1f.google.protobuf.MessageOptions\x18\x84\xf4\x03 \x01(\x08:/\n\x04\x66\x61\x63\x65\x12\x1f.google.protobuf.MessageOptions\x18\x85\xf4\x03 \x01(\x08:3\n\x08gostring\x12\x1f.google.protobuf.MessageOptions\x18\x86\xf4\x03 \x01(\x08:3\n\x08populate\x12\x1f.google.protobuf.MessageOptions\x18\x87\xf4\x03 \x01(\x08:3\n\x08stringer\x12\x1f.google.protobuf.MessageOptions\x18\xc0\x8b\x04 \x01(\x08:2\n\x07onlyone\x12\x1f.google.protobuf.MessageOptions\x18\x89\xf4\x03 \x01(\x08:0\n\x05\x65qual\x12\x1f.google.protobuf.MessageOptions\x18\x8d\xf4\x03 \x01(\x08:6\n\x0b\x64\x65scription\x12\x1f.google.protobuf.MessageOptions\x18\x8e\xf4\x03 \x01(\x08:2\n\x07testgen\x12\x1f.google.protobuf.MessageOptions\x18\x8f\xf4\x03 \x01(\x08:3\n\x08\x62\x65nchgen\x12\x1f.google.protobuf.MessageOptions\x18\x90\xf4\x03 \x01(\x08:4\n\tmarshaler\x12\x1f.google.protobuf.MessageOptions\x18\x91\xf4\x03 \x01(\x08:6\n\x0bunmarshaler\x12\x1f.google.protobuf.MessageOptions\x18\x92\xf4\x03 \x01(\x08:;\n\x10stable_marshaler\x12\x1f.google.protobuf.MessageOptions\x18\x93\xf4\x03 \x01(\x08:0\n\x05sizer\x12\x1f.google.protobuf.MessageOptions\x18\x94\xf4\x03 \x01(\x08:;\n\x10unsafe_marshaler\x12\x1f.google.protobuf.MessageOptions\x18\x97\xf4\x03 \x01(\x08:=\n\x12unsafe_unmarshaler\x12\x1f.google.protobuf.MessageOptions\x18\x98\xf4\x03 \x01(\x08:A\n\x16goproto_extensions_map\x12\x1f.google.protobuf.MessageOptions\x18\x99\xf4\x03 \x01(\x08:?\n\x14goproto_unrecognized\x12\x1f.google.protobuf.MessageOptions\x18\x9a\xf4\x03 \x01(\x08:5\n\nprotosizer\x12\x1f.google.protobuf.MessageOptions\x18\x9c\xf4\x03 \x01(\x08:2\n\x07\x63ompare\x12\x1f.google.protobuf.MessageOptions\x18\x9d\xf4\x03 \x01(\x08:3\n\x08typedecl\x12\x1f.google.protobuf.MessageOptions\x18\x9e\xf4\x03 \x01(\x08:6\n\x0bmessagename\x12\x1f.google.protobuf.MessageOptions\x18\xa1\xf4\x03 \x01(\x08:<\n\x11goproto_sizecache\x12\x1f.google.protobuf.MessageOptions\x18\xa2\xf4\x03 \x01(\x08::\n\x0fgoproto_unkeyed\x12\x1f.google.protobuf.MessageOptions\x18\xa3\xf4\x03 \x01(\x08:1\n\x08nullable\x12\x1d.google.protobuf.FieldOptions\x18\xe9\xfb\x03 \x01(\x08:.\n\x05\x65mbed\x12\x1d.google.protobuf.FieldOptions\x18\xea\xfb\x03 \x01(\x08:3\n\ncustomtype\x12\x1d.google.protobuf.FieldOptions\x18\xeb\xfb\x03 \x01(\t:3\n\ncustomname\x12\x1d.google.protobuf.FieldOptions\x18\xec\xfb\x03 \x01(\t:0\n\x07jsontag\x12\x1d.google.protobuf.FieldOptions\x18\xed\xfb\x03 \x01(\t:1\n\x08moretags\x12\x1d.google.protobuf.FieldOptions\x18\xee\xfb\x03 \x01(\t:1\n\x08\x63\x61sttype\x12\x1d.google.protobuf.FieldOptions\x18\xef\xfb\x03 \x01(\t:0\n\x07\x63\x61stkey\x12\x1d.google.protobuf.FieldOptions\x18\xf0\xfb\x03 \x01(\t:2\n\tcastvalue\x12\x1d.google.protobuf.FieldOptions\x18\xf1\xfb\x03 \x01(\t:0\n\x07stdtime\x12\x1d.google.protobuf.FieldOptions\x18\xf2\xfb\x03 \x01(\x08:4\n\x0bstdduration\x12\x1d.google.protobuf.FieldOptions\x18\xf3\xfb\x03 \x01(\x08:3\n\nwktpointer\x12\x1d.google.protobuf.FieldOptions\x18\xf4\xfb\x03 \x01(\x08\x42$Z\"github.com/gogo/protobuf/gogoproto" + +pool = Google::Protobuf::DescriptorPool.generated_pool +pool.add_serialized_file(descriptor_data) module Gogoproto end diff --git a/lib/gen/temporal/api/testservice/v1/request_response_pb.rb b/lib/gen/temporal/api/testservice/v1/request_response_pb.rb new file mode 100644 index 00000000..b515f9c5 --- /dev/null +++ b/lib/gen/temporal/api/testservice/v1/request_response_pb.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: temporal/api/testservice/v1/request_response.proto + +require 'google/protobuf' + +require 'google/protobuf/duration_pb' +require 'google/protobuf/timestamp_pb' +require 'dependencies/gogoproto/gogo_pb' + + +descriptor_data = "\n2temporal/api/testservice/v1/request_response.proto\x12\x1btemporal.api.testservice.v1\x1a\x1egoogle/protobuf/duration.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a!dependencies/gogoproto/gogo.proto\"\x19\n\x17LockTimeSkippingRequest\"\x1a\n\x18LockTimeSkippingResponse\"\x1b\n\x19UnlockTimeSkippingRequest\"\x1c\n\x1aUnlockTimeSkippingResponse\"H\n\x11SleepUntilRequest\x12\x33\n\ttimestamp\x18\x01 \x01(\x0b\x32\x1a.google.protobuf.TimestampB\x04\x90\xdf\x1f\x01\"A\n\x0cSleepRequest\x12\x31\n\x08\x64uration\x18\x01 \x01(\x0b\x32\x19.google.protobuf.DurationB\x04\x98\xdf\x1f\x01\"\x0f\n\rSleepResponse\"H\n\x16GetCurrentTimeResponse\x12.\n\x04time\x18\x01 \x01(\x0b\x32\x1a.google.protobuf.TimestampB\x04\x90\xdf\x1f\x01\x42\xa6\x01\n\x1eio.temporal.api.testservice.v1B\x14RequestResponseProtoP\x01Z-go.temporal.io/api/testservice/v1;testservice\xaa\x02\x1bTemporal.Api.TestService.V1\xea\x02\x1eTemporal::Api::TestService::V1b\x06proto3" + +pool = Google::Protobuf::DescriptorPool.generated_pool +pool.add_serialized_file(descriptor_data) + +module Temporal + module Api + module TestService + module V1 + LockTimeSkippingRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.testservice.v1.LockTimeSkippingRequest").msgclass + LockTimeSkippingResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.testservice.v1.LockTimeSkippingResponse").msgclass + UnlockTimeSkippingRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.testservice.v1.UnlockTimeSkippingRequest").msgclass + UnlockTimeSkippingResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.testservice.v1.UnlockTimeSkippingResponse").msgclass + SleepUntilRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.testservice.v1.SleepUntilRequest").msgclass + SleepRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.testservice.v1.SleepRequest").msgclass + SleepResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.testservice.v1.SleepResponse").msgclass + GetCurrentTimeResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("temporal.api.testservice.v1.GetCurrentTimeResponse").msgclass + end + end + end +end diff --git a/lib/gen/temporal/api/testservice/v1/service_pb.rb b/lib/gen/temporal/api/testservice/v1/service_pb.rb new file mode 100644 index 00000000..84073991 --- /dev/null +++ b/lib/gen/temporal/api/testservice/v1/service_pb.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: temporal/api/testservice/v1/service.proto + +require 'google/protobuf' + +require 'temporal/api/testservice/v1/request_response_pb' +require 'google/protobuf/empty_pb' + + +descriptor_data = "\n)temporal/api/testservice/v1/service.proto\x12\x1btemporal.api.testservice.v1\x1a\x32temporal/api/testservice/v1/request_response.proto\x1a\x1bgoogle/protobuf/empty.proto2\xc2\x05\n\x0bTestService\x12\x81\x01\n\x10LockTimeSkipping\x12\x34.temporal.api.testservice.v1.LockTimeSkippingRequest\x1a\x35.temporal.api.testservice.v1.LockTimeSkippingResponse\"\x00\x12\x87\x01\n\x12UnlockTimeSkipping\x12\x36.temporal.api.testservice.v1.UnlockTimeSkippingRequest\x1a\x37.temporal.api.testservice.v1.UnlockTimeSkippingResponse\"\x00\x12`\n\x05Sleep\x12).temporal.api.testservice.v1.SleepRequest\x1a*.temporal.api.testservice.v1.SleepResponse\"\x00\x12j\n\nSleepUntil\x12..temporal.api.testservice.v1.SleepUntilRequest\x1a*.temporal.api.testservice.v1.SleepResponse\"\x00\x12v\n\x1bUnlockTimeSkippingWithSleep\x12).temporal.api.testservice.v1.SleepRequest\x1a*.temporal.api.testservice.v1.SleepResponse\"\x00\x12_\n\x0eGetCurrentTime\x12\x16.google.protobuf.Empty\x1a\x33.temporal.api.testservice.v1.GetCurrentTimeResponse\"\x00\x42\x9e\x01\n\x1eio.temporal.api.testservice.v1B\x0cServiceProtoP\x01Z-go.temporal.io/api/testservice/v1;testservice\xaa\x02\x1bTemporal.Api.TestService.V1\xea\x02\x1eTemporal::Api::TestService::V1b\x06proto3" + +pool = Google::Protobuf::DescriptorPool.generated_pool +pool.add_serialized_file(descriptor_data) + +module Temporal + module Api + module TestService + module V1 + end + end + end +end diff --git a/lib/gen/temporal/api/testservice/v1/service_services_pb.rb b/lib/gen/temporal/api/testservice/v1/service_services_pb.rb new file mode 100644 index 00000000..3fcd1afc --- /dev/null +++ b/lib/gen/temporal/api/testservice/v1/service_services_pb.rb @@ -0,0 +1,91 @@ +# Generated by the protocol buffer compiler. DO NOT EDIT! +# Source: temporal/api/testservice/v1/service.proto for package 'Temporal.Api.TestService.V1' +# Original file comments: +# The MIT License +# +# Copyright (c) 2020 Temporal Technologies Inc. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# + +require 'grpc' +require 'temporal/api/testservice/v1/service_pb' + +module Temporal + module Api + module TestService + module V1 + module TestService + # TestService API defines an interface supported only by the Temporal Test Server. + # It provides functionality needed or supported for testing purposes only. + # + # This is an EXPERIMENTAL API. + class Service + + include ::GRPC::GenericService + + self.marshal_class_method = :encode + self.unmarshal_class_method = :decode + self.service_name = 'temporal.api.testservice.v1.TestService' + + # LockTimeSkipping increments Time Locking Counter by one. + # + # If Time Locking Counter is positive, time skipping is locked (disabled). + # When time skipping is disabled, the time in test server is moving normally, with a real time pace. + # Test Server is typically started with locked time skipping and Time Locking Counter = 1. + # + # LockTimeSkipping and UnlockTimeSkipping calls are counted. + rpc :LockTimeSkipping, ::Temporal::Api::TestService::V1::LockTimeSkippingRequest, ::Temporal::Api::TestService::V1::LockTimeSkippingResponse + # UnlockTimeSkipping decrements Time Locking Counter by one. + # + # If the counter reaches 0, it unlocks time skipping and fast forwards time. + # LockTimeSkipping and UnlockTimeSkipping calls are counted. Calling UnlockTimeSkipping does not + # guarantee that time is going to be fast forwarded as another lock can be holding it. + # + # Time Locking Counter can't be negative, unbalanced calls to UnlockTimeSkipping will lead to rpc call failure + rpc :UnlockTimeSkipping, ::Temporal::Api::TestService::V1::UnlockTimeSkippingRequest, ::Temporal::Api::TestService::V1::UnlockTimeSkippingResponse + # This call returns only when the Test Server Time advances by the specified duration. + # This is an EXPERIMENTAL API. + rpc :Sleep, ::Temporal::Api::TestService::V1::SleepRequest, ::Temporal::Api::TestService::V1::SleepResponse + # This call returns only when the Test Server Time advances to the specified timestamp. + # If the current Test Server Time is beyond the specified timestamp, returns immediately. + # This is an EXPERIMENTAL API. + rpc :SleepUntil, ::Temporal::Api::TestService::V1::SleepUntilRequest, ::Temporal::Api::TestService::V1::SleepResponse + # UnlockTimeSkippingWhileSleep decreases time locking counter by one and increases it back + # once the Test Server Time advances by the duration specified in the request. + # + # This call returns only when the Test Server Time advances by the specified duration. + # + # If it is called when Time Locking Counter is + # - more than 1 and no other unlocks are coming in, rpc call will block for the specified duration, time will not be fast forwarded. + # - 1, it will lead to fast forwarding of the time by the duration specified in the request and quick return of this rpc call. + # - 0 will lead to rpc call failure same way as an unbalanced UnlockTimeSkipping. + rpc :UnlockTimeSkippingWithSleep, ::Temporal::Api::TestService::V1::SleepRequest, ::Temporal::Api::TestService::V1::SleepResponse + # GetCurrentTime returns the current Temporal Test Server time + # + # This time might not be equal to {@link System#currentTimeMillis()} due to time skipping. + rpc :GetCurrentTime, ::Google::Protobuf::Empty, ::Temporal::Api::TestService::V1::GetCurrentTimeResponse + end + + Stub = Service.rpc_stub_class + end + end + end + end +end diff --git a/lib/temporal/testing/client.rb b/lib/temporal/testing/client.rb new file mode 100644 index 00000000..239ec480 --- /dev/null +++ b/lib/temporal/testing/client.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true +module Temporal + module Testing + # TestingClient expects the java `temporal-test-server` to be running and to have been + # enabled with timeskipping with + # `./temporal-test-server {port} --enable-time-skipping` + class Client < Temporal::Client + def initialize(config = {}) + super(config) + @timeskipping_connection = Connection.new(config.host, config.port) + end + + def sleep(duration) + @timeskipping_connection.unlock_time_skipping_with_sleep(duration) + end + + def await_workflow_result(workflow, workflow_id:, run_id: nil, timeout: nil, namespace: nil) + begin + # WorkflowTaskTimeout seems to happen with this SDK so sleep as workaround + sleep(0.25) + @timeskipping_connection.unlock_time_skipping + super + ensure + @timeskipping_connection.lock_time_skipping + end + end + end + end + +end \ No newline at end of file diff --git a/lib/temporal/testing/connection.rb b/lib/temporal/testing/connection.rb new file mode 100644 index 00000000..75c95d96 --- /dev/null +++ b/lib/temporal/testing/connection.rb @@ -0,0 +1,84 @@ +# frozen_string_literal: true +require 'temporal/api/testservice/v1/service_services_pb' +require 'temporal/api/testservice/v1/request_response_pb' +require 'temporal/api/testservice/v1//service_pb' +require 'grpc' + +module Temporal + module Testing + class Connection + CONNECTION_TIMEOUT_SECONDS = 60 + def initialize(host, port) + @url = "#{host}:#{port}" + end + + def lock_time_skipping + request = Temporal::Api::TestService::V1::LockTimeSkippingRequest.new + client.lock_time_skipping(request) + end + def unlock_time_skipping + request = Temporal::Api::TestService::V1::UnlockTimeSkippingRequest.new + client.unlock_time_skipping(request) + end + + def unlock_time_skipping_with_sleep(duration) + request = Temporal::Api::TestService::V1::SleepRequest.new(duration:duration) + client.unlock_time_skipping_with_sleep(request) + end + + def sleep(duration) + request = Temporal::Api::TestService::V1::SleepRequest.new(duration:duration) + client.sleep(request) + end + + def sleep_until(timestamp) + request = Temporal::Api::TestService::V1::SleepUntilRequest.new(timestamp:timestamp) + client.sleep_until(request) + end + def client + return @client if @client + + channel_args = {} + options = {} + + if options[:keepalive_time_ms] + channel_args["grpc.keepalive_time_ms"] = options[:keepalive_time_ms] + end + + if options[:retry_connection] || options[:retry_policy] + channel_args["grpc.enable_retries"] = 1 + + retry_policy = options[:retry_policy] || { + retryableStatusCodes: ["UNAVAILABLE"], + maxAttempts: 3, + initialBackoff: "0.1s", + backoffMultiplier: 2.0, + maxBackoff: "0.3s" + } + + channel_args["grpc.service_config"] = ::JSON.generate( + methodConfig: [ + { + name: [ + { + service: "temporal.api.testservice.v1.TestService", + } + ], + retryPolicy: retry_policy + } + ] + ) + end + + @client = Temporal::Api::TestService::V1::TestService::Stub.new( + @url, + :this_channel_is_insecure, + timeout: CONNECTION_TIMEOUT_SECONDS, + # interceptors: [ClientNameVersionInterceptor.new], + channel_args: channel_args + ) + end + + end + end +end diff --git a/lib/temporal/testing/proto/dependencies/gogoproto/gogo.proto b/lib/temporal/testing/proto/dependencies/gogoproto/gogo.proto new file mode 100644 index 00000000..49837cc6 --- /dev/null +++ b/lib/temporal/testing/proto/dependencies/gogoproto/gogo.proto @@ -0,0 +1,141 @@ +// Protocol Buffers for Go with Gadgets +// +// Copyright (c) 2013, The GoGo Authors. All rights reserved. +// http://github.com/temporalio/gogo-protobuf +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +syntax = "proto2"; +package gogoproto; + +import "google/protobuf/descriptor.proto"; + +option go_package = "github.com/gogo/protobuf/gogoproto"; + +extend google.protobuf.EnumOptions { + optional bool goproto_enum_prefix = 62001; + optional bool goproto_enum_stringer = 62021; + optional bool enum_stringer = 62022; + optional string enum_customname = 62023; + optional bool enumdecl = 62024; +} + +extend google.protobuf.EnumValueOptions { + optional string enumvalue_customname = 66001; +} + +extend google.protobuf.FileOptions { + optional bool goproto_getters_all = 63001; + optional bool goproto_enum_prefix_all = 63002; + optional bool goproto_stringer_all = 63003; + optional bool verbose_equal_all = 63004; + optional bool face_all = 63005; + optional bool gostring_all = 63006; + optional bool populate_all = 63007; + optional bool stringer_all = 63008; + optional bool onlyone_all = 63009; + + optional bool equal_all = 63013; + optional bool description_all = 63014; + optional bool testgen_all = 63015; + optional bool benchgen_all = 63016; + optional bool marshaler_all = 63017; + optional bool unmarshaler_all = 63018; + optional bool stable_marshaler_all = 63019; + + optional bool sizer_all = 63020; + + optional bool goproto_enum_stringer_all = 63021; + optional bool enum_stringer_all = 63022; + + optional bool unsafe_marshaler_all = 63023; + optional bool unsafe_unmarshaler_all = 63024; + + optional bool goproto_extensions_map_all = 63025; + optional bool goproto_unrecognized_all = 63026; + optional bool gogoproto_import = 63027; + optional bool protosizer_all = 63028; + optional bool compare_all = 63029; + optional bool typedecl_all = 63030; + optional bool enumdecl_all = 63031; + + optional bool goproto_registration = 63032; + optional bool messagename_all = 63033; + + optional bool goproto_sizecache_all = 63034; + optional bool goproto_unkeyed_all = 63035; +} + +extend google.protobuf.MessageOptions { + optional bool goproto_getters = 64001; + optional bool goproto_stringer = 64003; + optional bool verbose_equal = 64004; + optional bool face = 64005; + optional bool gostring = 64006; + optional bool populate = 64007; + optional bool stringer = 67008; + optional bool onlyone = 64009; + + optional bool equal = 64013; + optional bool description = 64014; + optional bool testgen = 64015; + optional bool benchgen = 64016; + optional bool marshaler = 64017; + optional bool unmarshaler = 64018; + optional bool stable_marshaler = 64019; + + optional bool sizer = 64020; + + optional bool unsafe_marshaler = 64023; + optional bool unsafe_unmarshaler = 64024; + + optional bool goproto_extensions_map = 64025; + optional bool goproto_unrecognized = 64026; + + optional bool protosizer = 64028; + optional bool compare = 64029; + + optional bool typedecl = 64030; + + optional bool messagename = 64033; + + optional bool goproto_sizecache = 64034; + optional bool goproto_unkeyed = 64035; +} + +extend google.protobuf.FieldOptions { + optional bool nullable = 65001; + optional bool embed = 65002; + optional string customtype = 65003; + optional string customname = 65004; + optional string jsontag = 65005; + optional string moretags = 65006; + optional string casttype = 65007; + optional string castkey = 65008; + optional string castvalue = 65009; + + optional bool stdtime = 65010; + optional bool stdduration = 65011; + optional bool wktpointer = 65012; +} diff --git a/lib/temporal/testing/proto/temporal/api/testservice/v1/request_response.proto b/lib/temporal/testing/proto/temporal/api/testservice/v1/request_response.proto new file mode 100644 index 00000000..572b337b --- /dev/null +++ b/lib/temporal/testing/proto/temporal/api/testservice/v1/request_response.proto @@ -0,0 +1,63 @@ +// The MIT License +// +// Copyright (c) 2020 Temporal Technologies Inc. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +syntax = "proto3"; + +package temporal.api.testservice.v1; + +option go_package = "go.temporal.io/api/testservice/v1;testservice"; +option java_package = "io.temporal.api.testservice.v1"; +option java_multiple_files = true; +option java_outer_classname = "RequestResponseProto"; +option ruby_package = "Temporal::Api::TestService::V1"; +option csharp_namespace = "Temporal.Api.TestService.V1"; + +import "google/protobuf/duration.proto"; +import "google/protobuf/timestamp.proto"; +import "dependencies/gogoproto/gogo.proto"; + +message LockTimeSkippingRequest { +} + +message LockTimeSkippingResponse { +} + +message UnlockTimeSkippingRequest { +} + +message UnlockTimeSkippingResponse { +} + +message SleepUntilRequest { + google.protobuf.Timestamp timestamp = 1 [(gogoproto.stdtime) = true]; +} + +message SleepRequest { + google.protobuf.Duration duration = 1 [(gogoproto.stdduration) = true]; +} + +message SleepResponse { +} + +message GetCurrentTimeResponse { + google.protobuf.Timestamp time = 1 [(gogoproto.stdtime) = true]; +} \ No newline at end of file diff --git a/lib/temporal/testing/proto/temporal/api/testservice/v1/service.proto b/lib/temporal/testing/proto/temporal/api/testservice/v1/service.proto new file mode 100644 index 00000000..5085ad87 --- /dev/null +++ b/lib/temporal/testing/proto/temporal/api/testservice/v1/service.proto @@ -0,0 +1,90 @@ +// The MIT License +// +// Copyright (c) 2020 Temporal Technologies Inc. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +syntax = "proto3"; + +package temporal.api.testservice.v1; + +option go_package = "go.temporal.io/api/testservice/v1;testservice"; +option java_package = "io.temporal.api.testservice.v1"; +option java_multiple_files = true; +option java_outer_classname = "ServiceProto"; +option ruby_package = "Temporal::Api::TestService::V1"; +option csharp_namespace = "Temporal.Api.TestService.V1"; + +import "temporal/api/testservice/v1/request_response.proto"; +import "google/protobuf/empty.proto"; + +// TestService API defines an interface supported only by the Temporal Test Server. +// It provides functionality needed or supported for testing purposes only. +// +// This is an EXPERIMENTAL API. +service TestService { + // LockTimeSkipping increments Time Locking Counter by one. + // + // If Time Locking Counter is positive, time skipping is locked (disabled). + // When time skipping is disabled, the time in test server is moving normally, with a real time pace. + // Test Server is typically started with locked time skipping and Time Locking Counter = 1. + // + // LockTimeSkipping and UnlockTimeSkipping calls are counted. + rpc LockTimeSkipping (LockTimeSkippingRequest) returns (LockTimeSkippingResponse) { + } + + // UnlockTimeSkipping decrements Time Locking Counter by one. + // + // If the counter reaches 0, it unlocks time skipping and fast forwards time. + // LockTimeSkipping and UnlockTimeSkipping calls are counted. Calling UnlockTimeSkipping does not + // guarantee that time is going to be fast forwarded as another lock can be holding it. + // + // Time Locking Counter can't be negative, unbalanced calls to UnlockTimeSkipping will lead to rpc call failure + rpc UnlockTimeSkipping (UnlockTimeSkippingRequest) returns (UnlockTimeSkippingResponse) { + } + + // This call returns only when the Test Server Time advances by the specified duration. + // This is an EXPERIMENTAL API. + rpc Sleep (SleepRequest) returns (SleepResponse) { + } + + // This call returns only when the Test Server Time advances to the specified timestamp. + // If the current Test Server Time is beyond the specified timestamp, returns immediately. + // This is an EXPERIMENTAL API. + rpc SleepUntil (SleepUntilRequest) returns (SleepResponse) { + } + + // UnlockTimeSkippingWhileSleep decreases time locking counter by one and increases it back + // once the Test Server Time advances by the duration specified in the request. + // + // This call returns only when the Test Server Time advances by the specified duration. + // + // If it is called when Time Locking Counter is + // - more than 1 and no other unlocks are coming in, rpc call will block for the specified duration, time will not be fast forwarded. + // - 1, it will lead to fast forwarding of the time by the duration specified in the request and quick return of this rpc call. + // - 0 will lead to rpc call failure same way as an unbalanced UnlockTimeSkipping. + rpc UnlockTimeSkippingWithSleep (SleepRequest) returns (SleepResponse) { + } + + // GetCurrentTime returns the current Temporal Test Server time + // + // This time might not be equal to {@link System#currentTimeMillis()} due to time skipping. + rpc GetCurrentTime (google.protobuf.Empty) returns (GetCurrentTimeResponse) { + } +} diff --git a/spec/integration/timeskippable_workflow.rb b/spec/integration/timeskippable_workflow.rb new file mode 100644 index 00000000..624c9426 --- /dev/null +++ b/spec/integration/timeskippable_workflow.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +class TimeskippableWorkflow < Temporal::Workflow + + def execute(timer_timeout) + timer = workflow.start_timer(timer_timeout) + unblocked = false + workflow.on_signal('unblock') do |signal_value| + unblocked = true + end + workflow.wait_until do + timer.finished? || unblocked + end + timer.cancel if unblocked + "unblocked:#{unblocked};timer:#{timer.finished?}" + end +end + diff --git a/spec/integration/timeskipping_support_spec.rb b/spec/integration/timeskipping_support_spec.rb new file mode 100644 index 00000000..ee3a0262 --- /dev/null +++ b/spec/integration/timeskipping_support_spec.rb @@ -0,0 +1,78 @@ +# frozen_string_literal: true + +require 'temporal/worker' +require 'temporal/testing/connection' +require 'temporal/testing/client' +require_relative './timeskippable_workflow' +require 'timeout' +describe 'Timeskipping' do + before :all do + Thread.abort_on_exception = true + Temporal.configure do |config| + config.host = 'localhost' + config.port = 7233 + config.namespace = 'default' + config.task_queue = 'default' + end + + @worker = Temporal::Worker.new + @worker.register_workflow TimeskippableWorkflow + @worker_thread = Thread.new do + @worker.start + end + end + after :all do + @worker.stop + @worker_thread.kill.join + end + it 'should work with timeskipping api directly' do + + workflow_id = SecureRandom.uuid + conn = Temporal::Testing::Connection.new("localhost", 7233) + run_id = Temporal.start_workflow( + TimeskippableWorkflow, + 25_000_000, + options: { + workflow_id: workflow_id, + task_queue: Temporal.configuration.task_queue, + }, + ) + conn.unlock_time_skipping_with_sleep(20_000_000) + Temporal.signal_workflow(TimeskippableWorkflow, 'unblock', workflow_id, run_id) + begin + # WorkflowTaskTimeout seems to happen with this SDK so sleep as workaround + sleep(0.5) + conn.unlock_time_skipping + + result = Temporal.await_workflow_result( + TimeskippableWorkflow, + workflow_id: workflow_id, + ) + ensure + conn.lock_time_skipping + end + expect(result).to eq "unblocked:true;timer:false" + + end + it 'should use testing client' do + + workflow_id = SecureRandom.uuid + # extract the config from the default Temporal client + client = Temporal::Testing::Client.new(Temporal.send :config) + run_id = Temporal.start_workflow( + TimeskippableWorkflow, + 25_000_000, + options: { + workflow_id: workflow_id, + task_queue: Temporal.configuration.task_queue, + }, + ) + client.sleep(20_000_000) + client.signal_workflow(TimeskippableWorkflow, 'unblock', workflow_id, run_id) + result = client.await_workflow_result( + TimeskippableWorkflow, + workflow_id: workflow_id, + ) + expect(result).to eq "unblocked:true;timer:false" + end +end \ No newline at end of file