standard-procedure-plumbing 0.3.3 → 0.4.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,93 @@
1
+ require_relative "actor/kernel"
2
+ require_relative "actor/inline"
3
+
4
+ module Plumbing
5
+ module Actor
6
+ def safely(&)
7
+ proxy.safely(&)
8
+ nil
9
+ end
10
+
11
+ def within_actor? = proxy.within_actor?
12
+
13
+ def stop = proxy.stop
14
+
15
+ def self.included base
16
+ base.extend ClassMethods
17
+ end
18
+
19
+ module ClassMethods
20
+ # Create a new actor instance and build a proxy for it using the current mode
21
+ # @return [Object] the proxy for the actor instance
22
+ def start(...)
23
+ instance = new(...)
24
+ build_proxy_for(instance).tap do |proxy|
25
+ instance.send :"proxy=", proxy
26
+ end
27
+ end
28
+
29
+ # Define the async messages that this actor can respond to
30
+ # @param names [Array<Symbol>] the names of the async messages
31
+ def async(*names) = async_messages.concat(names.map(&:to_sym))
32
+
33
+ # List the async messages that this actor can respond to
34
+ def async_messages = @async_messages ||= []
35
+
36
+ def inherited subclass
37
+ subclass.async_messages.concat async_messages
38
+ end
39
+
40
+ private
41
+
42
+ def build_proxy_for(target)
43
+ proxy_class_for(target.class).new(target)
44
+ end
45
+
46
+ def proxy_class_for target_class
47
+ Plumbing.config.actor_proxy_class_for(target_class) || register_actor_proxy_class_for(target_class)
48
+ end
49
+
50
+ def proxy_base_class = const_get PROXY_BASE_CLASSES[Plumbing.config.mode]
51
+
52
+ PROXY_BASE_CLASSES = {
53
+ inline: "Plumbing::Actor::Inline",
54
+ async: "Plumbing::Actor::Async",
55
+ threaded: "Plumbing::Actor::Threaded",
56
+ threaded_rails: "Plumbing::Actor::Rails"
57
+ }.freeze
58
+ private_constant :PROXY_BASE_CLASSES
59
+
60
+ def register_actor_proxy_class_for target_class
61
+ Plumbing.config.register_actor_proxy_class_for(target_class, build_proxy_class)
62
+ end
63
+
64
+ def build_proxy_class
65
+ Class.new(proxy_base_class).tap do |proxy_class|
66
+ async_messages.each do |message|
67
+ proxy_class.define_method message do |*args, &block|
68
+ send_message(message, *args, &block)
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
74
+
75
+ private
76
+
77
+ def proxy= proxy
78
+ @proxy = proxy
79
+ end
80
+
81
+ def proxy = @proxy
82
+ alias_method :as_actor, :proxy
83
+ alias_method :async, :proxy
84
+
85
+ def perform_safely(&)
86
+ instance_eval(&)
87
+ nil
88
+ rescue => ex
89
+ puts ex
90
+ nil
91
+ end
92
+ end
93
+ end
@@ -1,12 +1,12 @@
1
- # Pipes, pipelines, valves and rubber ducks
1
+ # Pipes, pipelines, actors and rubber ducks
2
2
  module Plumbing
3
- Config = Data.define :mode, :valve_proxy_classes, :timeout do
4
- def valve_proxy_class_for target_class
5
- valve_proxy_classes[target_class]
3
+ Config = Data.define :mode, :actor_proxy_classes, :timeout do
4
+ def actor_proxy_class_for target_class
5
+ actor_proxy_classes[target_class]
6
6
  end
7
7
 
8
- def register_valve_proxy_class_for target_class, proxy_class
9
- valve_proxy_classes[target_class] = proxy_class
8
+ def register_actor_proxy_class_for target_class, proxy_class
9
+ actor_proxy_classes[target_class] = proxy_class
10
10
  end
11
11
  end
12
12
  private_constant :Config
@@ -23,7 +23,7 @@ module Plumbing
23
23
  # @option timeout [Integer] the timeout (in seconds) to use (30s is the default)
24
24
  # @yield optional block - after the block has completed its execution, the configuration is restored to its previous state (useful for test suites)
25
25
  def self.configure(**params, &block)
26
- new_config = Config.new(**config.to_h.merge(params).merge(valve_proxy_classes: {}))
26
+ new_config = Config.new(**config.to_h.merge(params).merge(actor_proxy_classes: {}))
27
27
  if block.nil?
28
28
  set_configuration_to new_config
29
29
  else
@@ -45,7 +45,7 @@ module Plumbing
45
45
  private_class_method :set_configuration_and_yield
46
46
 
47
47
  def self.configs
48
- @configs ||= [Config.new(mode: :inline, timeout: 30, valve_proxy_classes: {})]
48
+ @configs ||= [Config.new(mode: :inline, timeout: 30, actor_proxy_classes: {})]
49
49
  end
50
50
  private_class_method :configs
51
51
  end
@@ -5,16 +5,17 @@ module Plumbing
5
5
  # @param sources [Array<Plumbing::Observable>] the sources which will be joined and relayed
6
6
  def initialize *sources
7
7
  super()
8
- @sources = sources.collect { |source| add(source) }
8
+ sources.each { |source| add(source) }
9
9
  end
10
10
 
11
11
  private
12
12
 
13
13
  def add source
14
14
  source.as(Observable).add_observer do |event|
15
- dispatch event
15
+ safely do
16
+ dispatch event
17
+ end
16
18
  end
17
- source
18
19
  end
19
20
  end
20
21
  end
data/lib/plumbing/pipe.rb CHANGED
@@ -1,10 +1,9 @@
1
1
  module Plumbing
2
2
  # A basic pipe
3
3
  class Pipe
4
- include Plumbing::Valve
4
+ include Plumbing::Actor
5
5
 
6
- command :notify, :<<, :remove_observer, :shutdown
7
- query :add_observer, :is_observer?
6
+ async :notify, :<<, :remove_observer, :add_observer, :is_observer?, :shutdown
8
7
 
9
8
  # Push an event into the pipe
10
9
  # @param event [Plumbing::Event] the event to push into the pipe
@@ -50,6 +49,7 @@ module Plumbing
50
49
  # Subclasses should override this to perform their own shutdown routines and call `super` to ensure everything is tidied up
51
50
  def shutdown
52
51
  observers.clear
52
+ stop
53
53
  end
54
54
 
55
55
  protected
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Plumbing
4
- VERSION = "0.3.3"
4
+ VERSION = "0.4.1"
5
5
  end
data/lib/plumbing.rb CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Plumbing
4
4
  require_relative "plumbing/config"
5
- require_relative "plumbing/valve"
5
+ require_relative "plumbing/actor"
6
6
  require_relative "plumbing/rubber_duck"
7
7
  require_relative "plumbing/types"
8
8
  require_relative "plumbing/error"
@@ -9,12 +9,13 @@ require "rspec/expectations"
9
9
  #
10
10
  RSpec::Matchers.define :become_equal_to do
11
11
  match do |expected|
12
+ max = Plumbing.config.timeout * 10
12
13
  counter = 0
13
14
  matched = false
14
- while (counter < 50) && (matched == false)
15
- matched = true if (@result = block_arg.call) == expected
15
+ while (counter < max) && (matched == false)
16
16
  sleep 0.1
17
17
  counter += 1
18
+ matched = true if (@result = block_arg.call) == expected
18
19
  end
19
20
  matched
20
21
  end
@@ -1,38 +1,12 @@
1
1
  require "spec_helper"
2
+ require "plumbing/actor/async"
3
+ require "plumbing/actor/threaded"
2
4
 
3
- RSpec.shared_examples "an example valve" do |runs_in_background|
4
- it "queries an object" do
5
- @person = Employee.start "Alice"
6
-
7
- expect(@person.name).to eq "Alice"
8
- expect(@person.job_title).to eq "Sales assistant"
9
-
10
- @time = Time.now
11
- # `greet_slowly` is a query so will block until a response is received
12
- expect(@person.greet_slowly).to eq "H E L L O"
13
- expect(Time.now - @time).to be > 0.1
14
-
15
- @time = Time.now
16
- # we're ignoring the result so this will not block (except :inline mode which does not run in the background)
17
- expect(@person.greet_slowly(ignore_result: true)).to be_nil
18
- expect(Time.now - @time).to be < 0.1 if runs_in_background
19
- expect(Time.now - @time).to be > 0.1 if !runs_in_background
20
- end
21
-
22
- it "commands an object" do
23
- @person = Employee.start "Alice"
24
- @person.promote
25
- @job_title = @person.job_title
26
- expect(@job_title).to eq "Sales manager"
27
- end
28
- end
29
-
30
- RSpec.describe "Valve example: " do
5
+ RSpec.shared_examples "an example actor" do |runs_in_background|
31
6
  # standard:disable Lint/ConstantDefinitionInBlock
32
7
  class Employee
33
- include Plumbing::Valve
34
- query :name, :job_title, :greet_slowly
35
- command :promote
8
+ include Plumbing::Actor
9
+ async :name, :job_title, :greet_slowly, :promote
36
10
 
37
11
  def initialize(name)
38
12
  @name = name
@@ -53,12 +27,43 @@ RSpec.describe "Valve example: " do
53
27
  end
54
28
  # standard:enable Lint/ConstantDefinitionInBlock
55
29
 
30
+ it "queries an object" do
31
+ @person = Employee.start "Alice"
32
+
33
+ expect(await { @person.name }).to eq "Alice"
34
+ expect(await { @person.job_title }).to eq "Sales assistant"
35
+
36
+ @time = Time.now
37
+ # `greet_slowly` is a query so will block until a response is received
38
+ expect(await { @person.greet_slowly }).to eq "H E L L O"
39
+ expect(Time.now - @time).to be > 0.1
40
+
41
+ @time = Time.now
42
+ # we're not awaiting the result, so this should run in the background (unless we're using inline mode)
43
+ @person.greet_slowly
44
+
45
+ expect(Time.now - @time).to be < 0.1 if runs_in_background
46
+ expect(Time.now - @time).to be > 0.1 if !runs_in_background
47
+ ensure
48
+ @person.stop
49
+ end
50
+
51
+ it "commands an object" do
52
+ @person = Employee.start "Alice"
53
+ @person.promote
54
+ expect(@person.job_title.value).to eq "Sales manager"
55
+ ensure
56
+ @person.stop
57
+ end
58
+ end
59
+
60
+ RSpec.describe "Actor example: " do
56
61
  context "inline mode" do
57
62
  around :example do |example|
58
63
  Plumbing.configure mode: :inline, &example
59
64
  end
60
65
 
61
- it_behaves_like "an example valve", false
66
+ it_behaves_like "an example actor", false
62
67
  end
63
68
 
64
69
  context "async mode" do
@@ -68,7 +73,7 @@ RSpec.describe "Valve example: " do
68
73
  end
69
74
  end
70
75
 
71
- it_behaves_like "an example valve", true
76
+ it_behaves_like "an example actor", true
72
77
  end
73
78
 
74
79
  context "threaded mode" do
@@ -76,6 +81,6 @@ RSpec.describe "Valve example: " do
76
81
  Plumbing.configure mode: :threaded, &example
77
82
  end
78
83
 
79
- it_behaves_like "an example valve", true
84
+ it_behaves_like "an example actor", true
80
85
  end
81
86
  end
@@ -0,0 +1,43 @@
1
+ require "spec_helper"
2
+ require "plumbing/actor/async"
3
+ require "plumbing/actor/threaded"
4
+
5
+ RSpec.describe "await" do
6
+ # standard:disable Lint/ConstantDefinitionInBlock
7
+ class Person
8
+ include Plumbing::Actor
9
+ async :name
10
+ def initialize name
11
+ @name = name
12
+ end
13
+ attr_reader :name
14
+ end
15
+ # standard:enable Lint/ConstantDefinitionInBlock
16
+
17
+ [:inline, :async, :threaded].each do |mode|
18
+ context "#{mode} mode" do
19
+ around :example do |example|
20
+ Sync do
21
+ Plumbing.configure mode: mode, &example
22
+ end
23
+ end
24
+
25
+ it "awaits a result from the actor directly" do
26
+ @person = Person.start "Alice"
27
+
28
+ expect(@person.name.value).to eq "Alice"
29
+ end
30
+
31
+ it "uses a block to await the result from the actor" do
32
+ @person = Person.start "Alice"
33
+
34
+ expect(await { @person.name }).to eq "Alice"
35
+ end
36
+
37
+ it "uses a block to immediately access non-actor objects" do
38
+ @person = "Bob"
39
+ expect(await { @person }).to eq "Bob"
40
+ end
41
+ end
42
+ end
43
+ end
@@ -1,12 +1,14 @@
1
1
  require "spec_helper"
2
2
  require "async"
3
+ require "plumbing/actor/async"
4
+ require "plumbing/actor/threaded"
3
5
 
4
6
  RSpec.describe "Pipe examples" do
5
7
  it "observes events" do
6
8
  @source = Plumbing::Pipe.start
7
9
 
8
10
  @result = []
9
- @observer = @source.add_observer do |event|
11
+ @source.add_observer do |event|
10
12
  @result << event.type
11
13
  end
12
14
 
@@ -22,7 +24,7 @@ RSpec.describe "Pipe examples" do
22
24
  end
23
25
 
24
26
  @result = []
25
- @observer = @filter.add_observer do |event|
27
+ @filter.add_observer do |event|
26
28
  @result << event.type
27
29
  end
28
30
 
@@ -42,20 +44,22 @@ RSpec.describe "Pipe examples" do
42
44
  end
43
45
 
44
46
  def received event
45
- @events << event
46
- if @events.count >= 3
47
- @events.clear
48
- self << event
47
+ safely do
48
+ @events << event
49
+ if @events.count >= 3
50
+ @events.clear
51
+ self << event
52
+ end
49
53
  end
50
54
  end
51
55
  end
52
56
  # standard:enable Lint/ConstantDefinitionInBlock
53
57
 
54
58
  @source = Plumbing::Pipe.start
55
- @filter = EveryThirdEvent.new(source: @source)
59
+ @filter = EveryThirdEvent.start(source: @source)
56
60
 
57
61
  @result = []
58
- @observer = @filter.add_observer do |event|
62
+ @filter.add_observer do |event|
59
63
  @result << event.type
60
64
  end
61
65
 
@@ -73,7 +77,7 @@ RSpec.describe "Pipe examples" do
73
77
  @junction = Plumbing::Junction.start @first_source, @second_source
74
78
 
75
79
  @result = []
76
- @observer = @junction.add_observer do |event|
80
+ @junction.add_observer do |event|
77
81
  @result << event.type
78
82
  end
79
83
 
@@ -83,7 +87,7 @@ RSpec.describe "Pipe examples" do
83
87
  expect(@result).to eq ["one", "two"]
84
88
  end
85
89
 
86
- it "dispatches events asynchronously using fibers" do
90
+ it "dispatches events asynchronously using async" do
87
91
  Plumbing.configure mode: :async do
88
92
  Sync do
89
93
  @first_source = Plumbing::Pipe.start
@@ -106,4 +110,36 @@ RSpec.describe "Pipe examples" do
106
110
  end
107
111
  end
108
112
  end
113
+
114
+ it "dispatches events asynchronously using threads" do
115
+ Plumbing.configure mode: :threaded do
116
+ @result = []
117
+
118
+ @first_source = Plumbing::Pipe.start
119
+ @second_source = Plumbing::Pipe.start
120
+ @junction = Plumbing::Junction.start @first_source, @second_source
121
+
122
+ @filter = Plumbing::Filter.start source: @junction do |event|
123
+ %w[one-one two-two].include? event.type
124
+ end
125
+ await do
126
+ @filter.add_observer do |event|
127
+ puts "observing #{event.type}"
128
+ @result << event.type
129
+ end
130
+ end
131
+
132
+ @first_source.notify "one-one"
133
+ @first_source.notify "one-two"
134
+ @second_source.notify "two-one"
135
+ @second_source.notify "two-two"
136
+
137
+ expect(["one-one", "two-two"]).to become_equal_to { @result.sort }
138
+ ensure
139
+ @first_source.shutdown
140
+ @second_source.shutdown
141
+ @junction.shutdown
142
+ @filter.shutdown
143
+ end
144
+ end
109
145
  end
@@ -1,10 +1,12 @@
1
1
  RSpec.shared_examples "a pipe" do
2
2
  it "adds a block observer" do
3
3
  @pipe = described_class.start
4
- @observer = @pipe.add_observer do |event|
5
- puts event.type
4
+ @observer = await do
5
+ @pipe.add_observer do |event|
6
+ puts event.type
7
+ end
6
8
  end
7
- expect(@pipe.is_observer?(@observer)).to eq true
9
+ expect(await { @pipe.is_observer?(@observer) }).to eq true
8
10
  end
9
11
 
10
12
  it "adds a callable observer" do
@@ -13,13 +15,13 @@ RSpec.shared_examples "a pipe" do
13
15
 
14
16
  @pipe.add_observer @proc
15
17
 
16
- expect(@pipe.is_observer?(@proc)).to eq true
18
+ expect(await { @pipe.is_observer?(@proc) }).to eq true
17
19
  end
18
20
 
19
21
  it "does not allow an observer without a #call method" do
20
22
  @pipe = described_class.start
21
23
 
22
- expect { @pipe.add_observer(Object.new) }.to raise_error(TypeError)
24
+ expect { await { @pipe.add_observer(Object.new) } }.to raise_error(TypeError)
23
25
  end
24
26
 
25
27
  it "removes an observer" do
@@ -28,10 +30,10 @@ RSpec.shared_examples "a pipe" do
28
30
 
29
31
  @pipe.remove_observer @proc
30
32
 
31
- expect(@pipe.is_observer?(@proc)).to eq false
33
+ expect(await { @pipe.is_observer?(@proc) }).to eq false
32
34
  end
33
35
 
34
- it "does not send notifications for objects which are not events" do
36
+ it "does not send notifications for objects which are not events" do
35
37
  @pipe = described_class.start
36
38
  @results = []
37
39
  @observer = @pipe.add_observer do |event|
@@ -40,7 +42,7 @@ RSpec.shared_examples "a pipe" do
40
42
 
41
43
  @pipe << Object.new
42
44
 
43
- sleep 0.5
45
+ sleep 0.1
44
46
  expect(@results).to eq []
45
47
  end
46
48
 
@@ -96,11 +98,13 @@ RSpec.shared_examples "a pipe" do
96
98
 
97
99
  it "shuts down the pipe" do
98
100
  @pipe = described_class.start
101
+ @results = []
99
102
  @observer = ->(event) { @results << event }
100
103
  @pipe.add_observer @observer
101
104
 
102
105
  @pipe.shutdown
103
-
104
- expect(@pipe.is_observer?(@observer)).to eq false
106
+ @pipe.notify "ignore_me"
107
+ sleep 0.2
108
+ expect(@results).to be_empty
105
109
  end
106
110
  end
@@ -0,0 +1,159 @@
1
+ require "spec_helper"
2
+ require_relative "../../../lib/plumbing/actor/transporter"
3
+
4
+ RSpec.describe Plumbing::Actor::Transporter do
5
+ # standard:disable Lint/ConstantDefinitionInBlock
6
+ class Record
7
+ include GlobalID::Identification
8
+ attr_reader :id
9
+ def initialize id
10
+ @id = id
11
+ end
12
+
13
+ def == other
14
+ other.id == @id
15
+ end
16
+ end
17
+ # standard:enable Lint/ConstantDefinitionInBlock
18
+
19
+ before do
20
+ GlobalID.app = "rspec"
21
+ GlobalID::Locator.use :rspec do |gid, options|
22
+ Record.new gid.model_id
23
+ end
24
+ end
25
+
26
+ context "marshalling" do
27
+ it "passes simple arguments" do
28
+ @transporter = described_class.new
29
+
30
+ @transport = @transporter.marshal "Hello"
31
+ expect(@transport).to eq ["Hello"]
32
+
33
+ @transport = @transporter.marshal 1, 2, 3
34
+ expect(@transport).to eq [1, 2, 3]
35
+ end
36
+
37
+ it "copies arrays" do
38
+ @transporter = described_class.new
39
+
40
+ @source = [[1, 2, 3], [:this, :that]]
41
+
42
+ @transport = @transporter.marshal(*@source)
43
+ expect(@transport).to eq @source
44
+ expect(@transport.first.object_id).to_not eq @source.first.object_id
45
+ expect(@transport.last.object_id).to_not eq @source.last.object_id
46
+ end
47
+
48
+ it "copies hashss" do
49
+ @transporter = described_class.new
50
+
51
+ @source = [{first: "1", second: 2}]
52
+
53
+ @transport = @transporter.marshal(*@source)
54
+ expect(@transport).to eq @source
55
+ expect(@transport.first.object_id).to_not eq @source.first.object_id
56
+ end
57
+
58
+ it "converts objects to Global ID strings" do
59
+ @transporter = described_class.new
60
+
61
+ @record = Record.new 123
62
+ @global_id = @record.to_global_id.to_s
63
+
64
+ @transport = @transporter.marshal @record
65
+
66
+ expect(@transport).to eq [@global_id]
67
+ end
68
+
69
+ it "converts objects within arrays to Global ID strings" do
70
+ @transporter = described_class.new
71
+
72
+ @record = Record.new 123
73
+ @global_id = @record.to_global_id.to_s
74
+
75
+ @transport = @transporter.marshal [:this, @record]
76
+
77
+ expect(@transport).to eq [[:this, @global_id]]
78
+ end
79
+
80
+ it "converts objects within hashes to Global ID strings" do
81
+ @transporter = described_class.new
82
+
83
+ @record = Record.new 123
84
+ @global_id = @record.to_global_id.to_s
85
+
86
+ @transport = @transporter.marshal this: "that", the_other: {embedded: @record}
87
+
88
+ expect(@transport).to eq [{this: "that", the_other: {embedded: @global_id}}]
89
+ end
90
+ end
91
+
92
+ context "unmarshalling" do
93
+ it "passes simple arguments" do
94
+ @transporter = described_class.new
95
+
96
+ @transport = @transporter.unmarshal "Hello"
97
+ expect(@transport).to eq ["Hello"]
98
+
99
+ @transport = @transporter.unmarshal 1, 2, 3
100
+ expect(@transport).to eq [1, 2, 3]
101
+ end
102
+
103
+ it "passes arrays" do
104
+ @transporter = described_class.new
105
+
106
+ @transport = @transporter.unmarshal [1, 2, 3], [:this, :that]
107
+
108
+ expect(@transport.first.object_id).to_not eq [1, 2, 3]
109
+ expect(@transport.last.object_id).to_not eq [:this, :that]
110
+ end
111
+
112
+ it "passes hashss and keyword arguments" do
113
+ @transporter = described_class.new
114
+
115
+ @transport = @transporter.unmarshal first: "1", second: 2
116
+ expect(@transport).to eq [{first: "1", second: 2}]
117
+ end
118
+
119
+ it "passes mixtures of arrays and hashes" do
120
+ @transporter = described_class.new
121
+
122
+ @transport = @transporter.unmarshal :this, :that, first: "1", second: 2
123
+ expect(@transport).to eq [:this, :that, {first: "1", second: 2}]
124
+ end
125
+
126
+ it "converts Global ID strings to objects" do
127
+ @transporter = described_class.new
128
+
129
+ @record = Record.new "123"
130
+ @global_id = @record.to_global_id.to_s
131
+
132
+ @transport = @transporter.unmarshal @global_id
133
+
134
+ expect(@transport).to eq [@record]
135
+ end
136
+
137
+ it "converts Global ID strings within arrays to objects" do
138
+ @transporter = described_class.new
139
+
140
+ @record = Record.new "123"
141
+ @global_id = @record.to_global_id.to_s
142
+
143
+ @transport = @transporter.unmarshal :this, @global_id
144
+
145
+ expect(@transport).to eq [:this, @record]
146
+ end
147
+
148
+ it "converts Global ID strings within hashes to objects" do
149
+ @transporter = described_class.new
150
+
151
+ @record = Record.new "123"
152
+ @global_id = @record.to_global_id.to_s
153
+
154
+ @transport = @transporter.unmarshal this: "that", the_other: {embedded: @global_id}
155
+
156
+ expect(@transport).to eq [{this: "that", the_other: {embedded: @record}}]
157
+ end
158
+ end
159
+ end