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,310 @@
1
+ require "spec_helper"
2
+
3
+ require_relative "../../lib/plumbing/actor/async"
4
+ require_relative "../../lib/plumbing/actor/threaded"
5
+ require_relative "../../lib/plumbing/actor/rails"
6
+
7
+ RSpec.describe Plumbing::Actor do
8
+ # standard:disable Lint/ConstantDefinitionInBlock
9
+ class Counter
10
+ include Plumbing::Actor
11
+ async :name, :count, :slow_query, "slowly_increment", "raises_error"
12
+ attr_reader :name, :count
13
+
14
+ def initialize name, initial_value: 0
15
+ @name = name
16
+ @count = initial_value
17
+ end
18
+
19
+ protected
20
+
21
+ def slowly_increment
22
+ sleep 0.2
23
+ @count += 1
24
+ end
25
+
26
+ def slow_query
27
+ sleep 0.2
28
+ @count
29
+ end
30
+
31
+ def raises_error = raise "I'm an error"
32
+ end
33
+
34
+ class StepCounter < Counter
35
+ async :step_value
36
+ attr_reader :step_value
37
+
38
+ def initialize name, initial_value: 0, step_value: 5
39
+ super(name, initial_value: initial_value)
40
+ @step_value = step_value
41
+ end
42
+
43
+ protected
44
+
45
+ def slowly_increment
46
+ sleep 0.2
47
+ @count += @step_value
48
+ end
49
+
50
+ def failing_query
51
+ raise "I'm a failure"
52
+ end
53
+ end
54
+
55
+ class WhoAmI
56
+ include Plumbing::Actor
57
+ async :me_as_actor, :me_as_self
58
+
59
+ private
60
+
61
+ def me_as_actor = as_actor
62
+
63
+ def me_as_self = self
64
+
65
+ def prepare = @calling_thread = Thread.current
66
+
67
+ def check = @calling_thread == Thread.current
68
+ end
69
+
70
+ class Actor
71
+ include Plumbing::Actor
72
+ async :get_object_id, :get_object
73
+
74
+ private def get_object_id(record) = record.object_id
75
+ private def get_object(record) = record
76
+ end
77
+
78
+ class SafetyCheck
79
+ include Plumbing::Actor
80
+ async :called_from_actor_thread?
81
+
82
+ def initialize tester
83
+ @tester = tester
84
+ @called_from_actor_thread = false
85
+ configure_safety_check
86
+ end
87
+
88
+ private
89
+
90
+ def called_from_actor_thread? = @called_from_actor_thread
91
+
92
+ def configure_safety_check
93
+ @tester.on_safety_check do
94
+ safely do
95
+ @called_from_actor_thread = proxy.within_actor?
96
+ end
97
+ end
98
+ end
99
+ end
100
+
101
+ class Tester
102
+ include Plumbing::Actor
103
+ async :on_safety_check, :do_safety_check
104
+
105
+ def initialize
106
+ @on_safety_check = nil
107
+ end
108
+
109
+ private
110
+
111
+ def on_safety_check(&block) = @on_safety_check = block
112
+
113
+ def do_safety_check = @on_safety_check&.call
114
+ end
115
+ # standard:enable Lint/ConstantDefinitionInBlock
116
+
117
+ it "knows which async messages are understood" do
118
+ expect(Counter.async_messages).to eq [:name, :count, :slow_query, :slowly_increment, :raises_error]
119
+ end
120
+
121
+ it "reuses existing proxy classes" do
122
+ @counter = Counter.start "inline counter", initial_value: 100
123
+ @proxy_class = @counter.class
124
+
125
+ @counter = Counter.start "another inline counter", initial_value: 200
126
+ expect(@counter.class).to eq @proxy_class
127
+ end
128
+
129
+ it "includes async messages from the superclass" do
130
+ expect(StepCounter.async_messages).to eq [:name, :count, :slow_query, :slowly_increment, :raises_error, :step_value]
131
+
132
+ @step_counter = StepCounter.start "step counter", initial_value: 100, step_value: 10
133
+
134
+ expect(@step_counter.count.value).to eq 100
135
+ expect(@step_counter.step_value.value).to eq 10
136
+ @step_counter.slowly_increment
137
+ expect(@step_counter.count.value).to eq 110
138
+ end
139
+
140
+ it "can access its own proxy" do
141
+ @actor = WhoAmI.start
142
+
143
+ expect(await { @actor.me_as_self }).to_not eq @actor
144
+ expect(await { @actor.me_as_actor }).to eq @actor
145
+ end
146
+
147
+ context "inline" do
148
+ around :example do |example|
149
+ Plumbing.configure mode: :inline, &example
150
+ end
151
+
152
+ it "returns the result from a message immediately" do
153
+ @counter = Counter.start "inline counter", initial_value: 100
154
+ @time = Time.now
155
+
156
+ expect(@counter.name.value).to eq "inline counter"
157
+ expect(@counter.count.value).to eq 100
158
+ expect(Time.now - @time).to be < 0.1
159
+
160
+ expect(@counter.slow_query.value).to eq 100
161
+ expect(Time.now - @time).to be > 0.1
162
+ end
163
+
164
+ it "sends all commands immediately" do
165
+ @counter = Counter.start "inline counter", initial_value: 100
166
+ @time = Time.now
167
+
168
+ @counter.slowly_increment
169
+
170
+ expect(@counter.count.value).to eq 101
171
+ expect(Time.now - @time).to be > 0.1
172
+ end
173
+
174
+ it "can safely access its own data" do
175
+ @tester = Tester.start
176
+ @safety_check = SafetyCheck.start @tester
177
+
178
+ @tester.do_safety_check
179
+
180
+ expect(true).to become_equal_to { @safety_check.called_from_actor_thread?.value }
181
+ end
182
+ end
183
+
184
+ [:threaded, :async].each do |mode|
185
+ context mode.to_s do
186
+ around :example do |example|
187
+ Sync do
188
+ Plumbing.configure mode: mode, &example
189
+ end
190
+ end
191
+
192
+ it "performs queries in the background and waits for the response" do
193
+ @counter = Counter.start "async counter", initial_value: 100
194
+ @time = Time.now
195
+
196
+ expect(@counter.name.value).to eq "async counter"
197
+ expect(@counter.count.value).to eq 100
198
+ expect(Time.now - @time).to be < 0.1
199
+
200
+ expect(@counter.slow_query.value).to eq 100
201
+ expect(Time.now - @time).to be > 0.1
202
+ ensure
203
+ @counter.stop
204
+ end
205
+
206
+ it "performs queries ignoring the response and returning immediately" do
207
+ @counter = Counter.start "threaded counter", initial_value: 100
208
+ @time = Time.now
209
+
210
+ @counter.slow_query
211
+
212
+ expect(Time.now - @time).to be < 0.1
213
+ ensure
214
+ @counter.stop
215
+ end
216
+
217
+ it "performs commands in the background and returning immediately" do
218
+ @counter = Counter.start "threaded counter", initial_value: 100
219
+ @time = Time.now
220
+
221
+ @counter.slowly_increment
222
+ expect(Time.now - @time).to be < 0.1
223
+
224
+ # wait for the background task to complete
225
+ expect(101).to become_equal_to { @counter.count.value }
226
+ expect(Time.now - @time).to be > 0.1
227
+ ensure
228
+ @counter.stop
229
+ end
230
+
231
+ it "re-raises exceptions when checking the result" do
232
+ @counter = Counter.start "failure"
233
+
234
+ expect { @counter.raises_error.value }.to raise_error "I'm an error"
235
+ ensure
236
+ @counter.stop
237
+ end
238
+
239
+ it "does not raise exceptions if ignoring the result" do
240
+ @counter = Counter.start "failure"
241
+
242
+ expect { @counter.raises_error }.not_to raise_error
243
+ ensure
244
+ @counter.stop
245
+ end
246
+ end
247
+ end
248
+
249
+ context "threaded" do
250
+ around :example do |example|
251
+ Plumbing.configure mode: :threaded, &example
252
+ end
253
+
254
+ before do
255
+ GlobalID.app = "rspec"
256
+ GlobalID::Locator.use :rspec do |gid, options|
257
+ Record.new gid.model_id
258
+ end
259
+ end
260
+
261
+ # standard:disable Lint/ConstantDefinitionInBlock
262
+ class Record
263
+ include GlobalID::Identification
264
+ attr_reader :id
265
+ def initialize id
266
+ @id = id
267
+ end
268
+
269
+ def == other
270
+ other.id == @id
271
+ end
272
+ end
273
+ # standard:enable Lint/ConstantDefinitionInBlock
274
+
275
+ it "packs and unpacks arguments when sending them across threads" do
276
+ @actor = Actor.start
277
+ @record = Record.new "999"
278
+
279
+ @object_id = @actor.get_object_id(@record).value
280
+
281
+ expect(@object_id).to_not eq @record.object_id
282
+ ensure
283
+ @actor.stop
284
+ end
285
+
286
+ it "packs and unpacks results when sending them across threads" do
287
+ @actor = Actor.start
288
+ @record = Record.new "999"
289
+
290
+ @object = @actor.get_object(@record).value
291
+
292
+ expect(@object.id).to eq @record.id
293
+ expect(@object.object_id).to_not eq @record.object_id
294
+ ensure
295
+ @actor.stop
296
+ end
297
+
298
+ it "can safely access its own data" do
299
+ @tester = Tester.start
300
+ @safety_check = SafetyCheck.start @tester
301
+
302
+ @tester.do_safety_check
303
+
304
+ expect(true).to become_equal_to { @safety_check.called_from_actor_thread?.value }
305
+ ensure
306
+ @tester.stop
307
+ @safety_check.stop
308
+ end
309
+ end
310
+ end
@@ -15,7 +15,7 @@ RSpec.describe Plumbing::CustomFilter do
15
15
  # standard:enable Lint/ConstantDefinitionInBlock
16
16
 
17
17
  @pipe = Plumbing::Pipe.start
18
- @filter = ReversingFilter.new(source: @pipe)
18
+ @filter = ReversingFilter.start(source: @pipe)
19
19
  @result = []
20
20
  @filter.add_observer do |event|
21
21
  @result << event.type
metadata CHANGED
@@ -1,15 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: standard-procedure-plumbing
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.3
4
+ version: 0.4.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rahoul Baruah
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-09-14 00:00:00.000000000 Z
12
- dependencies: []
11
+ date: 2024-09-16 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: globalid
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
13
27
  description: A composable event pipeline and sequential pipelines of operations
14
28
  email:
15
29
  - rahoulb@echodek.co
@@ -20,6 +34,13 @@ files:
20
34
  - README.md
21
35
  - Rakefile
22
36
  - lib/plumbing.rb
37
+ - lib/plumbing/actor.rb
38
+ - lib/plumbing/actor/async.rb
39
+ - lib/plumbing/actor/inline.rb
40
+ - lib/plumbing/actor/kernel.rb
41
+ - lib/plumbing/actor/rails.rb
42
+ - lib/plumbing/actor/threaded.rb
43
+ - lib/plumbing/actor/transporter.rb
23
44
  - lib/plumbing/config.rb
24
45
  - lib/plumbing/custom_filter.rb
25
46
  - lib/plumbing/error.rb
@@ -35,26 +56,22 @@ files:
35
56
  - lib/plumbing/rubber_duck/object.rb
36
57
  - lib/plumbing/rubber_duck/proxy.rb
37
58
  - lib/plumbing/types.rb
38
- - lib/plumbing/valve.rb
39
- - lib/plumbing/valve/async.rb
40
- - lib/plumbing/valve/inline.rb
41
- - lib/plumbing/valve/message.rb
42
- - lib/plumbing/valve/rails.rb
43
- - lib/plumbing/valve/threaded.rb
44
59
  - lib/plumbing/version.rb
45
60
  - spec/become_equal_to_matcher.rb
61
+ - spec/examples/actor_spec.rb
62
+ - spec/examples/await_spec.rb
46
63
  - spec/examples/pipe_spec.rb
47
64
  - spec/examples/pipeline_spec.rb
48
65
  - spec/examples/rubber_duck_spec.rb
49
- - spec/examples/valve_spec.rb
50
66
  - spec/plumbing/a_pipe.rb
67
+ - spec/plumbing/actor/transporter_spec.rb
68
+ - spec/plumbing/actor_spec.rb
51
69
  - spec/plumbing/custom_filter_spec.rb
52
70
  - spec/plumbing/filter_spec.rb
53
71
  - spec/plumbing/junction_spec.rb
54
72
  - spec/plumbing/pipe_spec.rb
55
73
  - spec/plumbing/pipeline_spec.rb
56
74
  - spec/plumbing/rubber_duck_spec.rb
57
- - spec/plumbing/valve_spec.rb
58
75
  - spec/plumbing_spec.rb
59
76
  - spec/spec_helper.rb
60
77
  homepage: https://github.com/standard-procedure/plumbing
@@ -1,43 +0,0 @@
1
- require "async"
2
- require "async/semaphore"
3
- require "timeout"
4
-
5
- module Plumbing
6
- module Valve
7
- class Async
8
- attr_reader :target
9
-
10
- def initialize target
11
- @target = target
12
- @queue = []
13
- @semaphore = ::Async::Semaphore.new(1)
14
- end
15
-
16
- # Ask the target to answer the given message
17
- def ask(message, *args, **params, &block)
18
- task = @semaphore.async do
19
- @target.send message, *args, **params, &block
20
- end
21
- Timeout.timeout(timeout) do
22
- task.wait
23
- end
24
- end
25
-
26
- # Tell the target to execute the given message
27
- def tell(message, *args, **params, &block)
28
- @semaphore.async do |task|
29
- @target.send message, *args, **params, &block
30
- rescue
31
- nil
32
- end
33
- nil
34
- end
35
-
36
- private
37
-
38
- def timeout
39
- Plumbing.config.timeout
40
- end
41
- end
42
- end
43
- end
@@ -1,20 +0,0 @@
1
- module Plumbing
2
- module Valve
3
- class Inline
4
- def initialize target
5
- @target = target
6
- end
7
-
8
- # Ask the target to answer the given message
9
- def ask(message, ...)
10
- @target.send(message, ...)
11
- end
12
-
13
- # Tell the target to execute the given message
14
- def tell(message, ...)
15
- @target.send(message, ...)
16
- nil
17
- end
18
- end
19
- end
20
- end
@@ -1,5 +0,0 @@
1
- module Plumbing
2
- module Valve
3
- Message = Struct.new :message, :args, :params, :block, :result, :status
4
- end
5
- end
@@ -1,67 +0,0 @@
1
- require "concurrent/array"
2
- require "concurrent/mvar"
3
- require "concurrent/immutable_struct"
4
- require "concurrent/promises"
5
-
6
- module Plumbing
7
- module Valve
8
- class Threaded
9
- attr_reader :target
10
-
11
- def initialize target
12
- @target = target
13
- @queue = Concurrent::Array.new
14
- end
15
-
16
- # Ask the target to answer the given message
17
- def ask(message, *, **, &)
18
- add_message_to_queue(message, *, **, &).value
19
- end
20
-
21
- # Tell the target to execute the given message
22
- def tell(message, *, **, &)
23
- add_message_to_queue(message, *, **, &)
24
- nil
25
- rescue
26
- nil
27
- end
28
-
29
- protected
30
-
31
- def future(&)
32
- Concurrent::Promises.future(&)
33
- end
34
-
35
- private
36
-
37
- def send_messages
38
- future do
39
- while (message = @queue.shift)
40
- message.call
41
- end
42
- end
43
- end
44
-
45
- def add_message_to_queue message_name, *args, **params, &block
46
- Message.new(@target, message_name, args, params, block, Concurrent::MVar.new).tap do |message|
47
- @queue << message
48
- send_messages if @queue.size == 1
49
- end
50
- end
51
-
52
- class Message < Concurrent::ImmutableStruct.new(:target, :name, :args, :params, :block, :result)
53
- def value
54
- result.take(Plumbing.config.timeout).tap do |value|
55
- raise value if value.is_a? Exception
56
- end
57
- end
58
-
59
- def call
60
- result.put target.send(name, *args, **params, &block)
61
- rescue => ex
62
- result.put ex
63
- end
64
- end
65
- end
66
- end
67
- end
@@ -1,71 +0,0 @@
1
- require_relative "valve/inline"
2
-
3
- module Plumbing
4
- module Valve
5
- def self.included base
6
- base.extend ClassMethods
7
- end
8
-
9
- module ClassMethods
10
- # Create a new valve instance and build a proxy for it using the current mode
11
- # @return [Plumbing::Valve::Base] the proxy for the valve instance
12
- def start(*, **, &)
13
- build_proxy_for(new(*, **, &))
14
- end
15
-
16
- # Define the queries that this valve can answer
17
- # @param names [Array<Symbol>] the names of the queries
18
- def query(*names) = queries.concat(names.map(&:to_sym))
19
-
20
- # List the queries that this valve can answer
21
- def queries = @queries ||= []
22
-
23
- # Define the commands that this valve can execute
24
- # @param names [Array<Symbol>] the names of the commands
25
- def command(*names) = commands.concat(names.map(&:to_sym))
26
-
27
- # List the commands that this valve can execute
28
- def commands = @commands ||= []
29
-
30
- def inherited subclass
31
- subclass.commands.concat commands
32
- subclass.queries.concat queries
33
- end
34
-
35
- private
36
-
37
- def build_proxy_for(target)
38
- proxy_class_for(target.class).new(target)
39
- end
40
-
41
- def proxy_class_for target_class
42
- Plumbing.config.valve_proxy_class_for(target_class) || register_valve_proxy_class_for(target_class)
43
- end
44
-
45
- def proxy_base_class = const_get "Plumbing::Valve::#{Plumbing.config.mode.to_s.capitalize}"
46
-
47
- def register_valve_proxy_class_for target_class
48
- Plumbing.config.register_valve_proxy_class_for(target_class, build_proxy_class)
49
- end
50
-
51
- def build_proxy_class
52
- Class.new(proxy_base_class).tap do |proxy_class|
53
- queries.each do |query|
54
- proxy_class.define_method query do |*args, ignore_result: false, **params, &block|
55
- ignore_result ? tell(query, *args, **params, &block) : ask(query, *args, **params, &block)
56
- end
57
- end
58
-
59
- commands.each do |command|
60
- proxy_class.define_method command do |*args, **params, &block|
61
- tell(command, *args, **params, &block)
62
- nil
63
- rescue
64
- nil
65
- end
66
- end
67
- end
68
- end
69
- end
70
- end
71
- end