-
Notifications
You must be signed in to change notification settings - Fork 107
Important performance and correctness fixes for wait_until #183
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,5 @@ | ||
| require 'temporal/errors' | ||
|
|
||
| module Temporal | ||
| class Workflow | ||
| # This provides a generic event dispatcher mechanism. There are two main entry | ||
|
|
@@ -13,18 +15,44 @@ class Workflow | |
| # the event_name. The order of this dispatch is not guaranteed. | ||
| # | ||
| class Dispatcher | ||
| # Raised if a duplicate ID is encountered during dispatch handling. | ||
| # This likely indicates a bug in temporal-ruby or that unsupported multithreaded | ||
| # workflow code is being used. | ||
| class DuplicateIDError < InternalError; end | ||
|
|
||
| # Tracks a registered handle so that it can be unregistered later | ||
| # The handlers are passed by reference here to be mutated (removed) by the | ||
| # unregister call below. | ||
| class RegistrationHandle | ||
| def initialize(handlers_for_target, id) | ||
| @handlers_for_target = handlers_for_target | ||
| @id = id | ||
| end | ||
|
|
||
| # Unregister the handler from the dispatcher | ||
| def unregister | ||
| handlers_for_target.delete(id) | ||
| end | ||
|
|
||
| private | ||
|
|
||
| attr_reader :handlers_for_target, :id | ||
| end | ||
|
|
||
| WILDCARD = '*'.freeze | ||
| TARGET_WILDCARD = '*'.freeze | ||
|
|
||
| EventStruct = Struct.new(:event_name, :handler) | ||
|
|
||
| def initialize | ||
| @handlers = Hash.new { |hash, key| hash[key] = [] } | ||
| @handlers = Hash.new { |hash, key| hash[key] = {} } | ||
| @next_id = 0 | ||
| end | ||
|
|
||
| def register_handler(target, event_name, &handler) | ||
| handlers[target] << EventStruct.new(event_name, handler) | ||
| self | ||
| @next_id += 1 | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This isn't thread-safe on non-CRuby implementations. Do we need to worry about handler registration being called from multiple threads @antstorm ?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This would require using multi-threading in workflow code, right? That doesn't seem idiomatic, but I don't know if it's possible. It also seems like this wouldn't work with other parts of the library either since synchronization is not widely used. Would the current version that does array appending even be expected to work correctly in this case? Or is that operation guaranteed to be atomic?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure. Hoping Anthony responds.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, the workflow code is definitely expected to be all single-threaded, so I wouldn't worry too much about it. Besides the way we write to |
||
| handlers[target][@next_id] = EventStruct.new(event_name, handler) | ||
| RegistrationHandle.new(handlers[target], @next_id) | ||
| end | ||
|
|
||
| def dispatch(target, event_name, args = nil) | ||
|
|
@@ -39,9 +67,10 @@ def dispatch(target, event_name, args = nil) | |
|
|
||
| def handlers_for(target, event_name) | ||
| handlers[target] | ||
| .concat(handlers[TARGET_WILDCARD]) | ||
| .select { |event_struct| match?(event_struct, event_name) } | ||
| .map(&:handler) | ||
| .merge(handlers[TARGET_WILDCARD]) { raise DuplicateIDError.new('Cannot resolve duplicate dispatcher handler IDs') } | ||
| .select { |_, event_struct| match?(event_struct, event_name) } | ||
| .sort | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Kind of surprised that this does the right thing since
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The naming here is confusing because there's a
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I see what you mean. Yes, that confused me for sure. Renaming would help. |
||
| .map { |_, event_struct| event_struct.handler } | ||
| end | ||
|
|
||
| def match?(event_struct, event_name) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,7 +8,10 @@ | |
| describe '#register_handler' do | ||
| let(:block) { -> { 'handler body' } } | ||
| let(:event_name) { 'signaled' } | ||
| let(:dispatcher) { subject.register_handler(target, event_name, &block) } | ||
| let(:dispatcher) do | ||
| subject.register_handler(target, event_name, &block) | ||
| subject | ||
| end | ||
| let(:handlers) { dispatcher.send(:handlers) } | ||
|
|
||
| context 'with default handler_name' do | ||
|
|
@@ -19,17 +22,17 @@ | |
| end | ||
|
|
||
| it 'stores the target and handler once' do | ||
| expect(handlers[target]).to be_kind_of(Array) | ||
| expect(handlers[target]).to be_kind_of(Hash) | ||
| expect(handlers[target].count).to eq 1 | ||
| end | ||
|
|
||
| it 'associates the event name with the target' do | ||
| event = handlers[target].first | ||
| event = handlers[target][1] | ||
| expect(event.event_name).to eq(event_name) | ||
| end | ||
|
|
||
| it 'associates the handler with the target' do | ||
| event = handlers[target].first | ||
| event = handlers[target][1] | ||
| expect(event.handler).to eq(block) | ||
| end | ||
| end | ||
|
|
@@ -43,20 +46,44 @@ | |
| end | ||
|
|
||
| it 'stores the target and handler once' do | ||
| expect(handlers[target]).to be_kind_of(Array) | ||
| expect(handlers[target]).to be_kind_of(Hash) | ||
| expect(handlers[target].count).to eq 1 | ||
| end | ||
|
|
||
| it 'associates the event name and handler name with the target' do | ||
| event = handlers[target].first | ||
| event = handlers[target][1] | ||
| expect(event.event_name).to eq(event_name) | ||
| end | ||
|
|
||
| it 'associates the handler with the target' do | ||
| event = handlers[target].first | ||
| event = handlers[target][1] | ||
| expect(event.handler).to eq(block) | ||
| end | ||
| end | ||
|
|
||
| it 'removes a given handler against the target' do | ||
| block1 = -> { 'handler body' } | ||
| block2 = -> { 'other handler body' } | ||
| block3 = -> { 'yet another handler body' } | ||
|
|
||
| handle1 = subject.register_handler(target, 'signaled', &block1) | ||
| subject.register_handler(target, 'signaled', &block2) | ||
| subject.register_handler(other_target, 'signaled', &block3) | ||
|
|
||
| expect(subject.send(:handlers)[target][1].event_name).to eq('signaled') | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I dislike asserting behavior against private structures like this. When a method is declared private, the "private" aspect is about code organization. Private methods should be able to get refactored, renamed, changed, etc without breaking unit tests at all. If this is the only way to test it then the code structure is a code smell and needs some love. IMHO. Perhaps write unit tests against the private As for testing the ordering, that's a stickier problem that I'd have to think about.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd originally written tests only against the public interface, but other tests were added in #157 that further inspected the private contents. That was merged between when I first started writing this and began upstreaming this PR. So I decided to stick with that convention, and update the tests. Happy to rework this if everyone agrees we should only test the public interface here. This would make the tests less brittle. The ordering tests are critical, as that's the crux of one of the bugs where the order was not deterministic. I agree the contents of the dispatcher's internal state don't need to be in a particular order, as it's an implementation detail. All that needs to be guaranteed is dispatch order follows registration order. There is a test for this farther down the file already. Do you think that's sufficient? Or are there more cases I could cover there?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ha, yes, and it's particularly annoying to me that I did it in my own PR. :) Now I'm inspired to go back and fix that because I can't allow myself to be a hypocrite. As for ordering, I think the later tests cover it sufficiently.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Happy to leave this in order to get the PR merged, but we can refactor this (in a separate PR) and put the expectations inside the handler blocks, so instead of checking the internal state we just make sure that the right ones are firing when an even is dispatched (fully conforming to a public interface of a Dispatcher)
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'll leave this for now since at least it has good coverage |
||
| expect(subject.send(:handlers)[target][1].handler).to be(block1) | ||
|
|
||
| expect(subject.send(:handlers)[target][2].event_name).to eq('signaled') | ||
| expect(subject.send(:handlers)[target][2].handler).to be(block2) | ||
|
|
||
| expect(subject.send(:handlers)[other_target][3].event_name).to eq('signaled') | ||
| expect(subject.send(:handlers)[other_target][3].handler).to be(block3) | ||
|
|
||
| handle1.unregister | ||
| expect(subject.send(:handlers)[target][1]).to be(nil) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: Might wanna check that the others are still there. If you empty the hash completely it won't fail this spec |
||
| expect(subject.send(:handlers)[target][2]).to_not be(nil) | ||
| expect(subject.send(:handlers)[other_target][3]).to_not be(nil) | ||
| end | ||
| end | ||
|
|
||
| describe '#dispatch' do | ||
|
|
@@ -114,10 +141,13 @@ | |
|
|
||
| context 'with TARGET_WILDCARD target handler' do | ||
| let(:handler_6) { -> { 'sixth block' } } | ||
| let(:handler_7) { -> { 'seventh block' } } | ||
| before do | ||
| allow(handler_6).to receive(:call) | ||
| allow(handler_7).to receive(:call) | ||
|
|
||
| subject.register_handler(described_class::TARGET_WILDCARD, described_class::WILDCARD, &handler_6) | ||
| subject.register_handler(target, 'completed', &handler_7) | ||
| end | ||
|
|
||
| it 'calls the handler' do | ||
|
|
@@ -127,6 +157,7 @@ | |
| expect(handler_1).to have_received(:call).ordered | ||
| expect(handler_4).to have_received(:call).ordered | ||
| expect(handler_6).to have_received(:call).ordered | ||
| expect(handler_7).to have_received(:call).ordered | ||
| end | ||
|
|
||
| it 'TARGET_WILDCARD can be compared to an EventTarget object' do | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: It would be handy to add a comment saying that handlers are passed by reference and are mutated here