standard-procedure-plumbing 0.4.5 → 0.4.6

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 14d6355c882a36f026eb602842b5024b58a653d698352239b283cac6703e042f
4
- data.tar.gz: 0e3944c06292f6747cff1e816843c9eeb8d3f3969cf46619389745b6a2c3a890
3
+ metadata.gz: 82a3aa2f2a6718e748faf14e12dcf8df50928cfcdb19bf75641731e38122c694
4
+ data.tar.gz: d13610908d60830323e6921f77628c73ce9920475086a1475f21323def38f3a3
5
5
  SHA512:
6
- metadata.gz: ebd0ce6803ce1d06d99eb4941bbdcc0f0f047a5f2fd2fb3df744ae18db948b9ce6bdd093e7ba5048e43417fc3420cce4185d52ee7cc7d092dba5794c4afdad82
7
- data.tar.gz: d40d1334d75bcfe44b64dc2f5b8ebc6f8b9299cfce653f1b9264caea3016c215a2fa2285ffff82171b9cea2e46ce34de15dcd68c56a8047775374368c2fb8d95
6
+ metadata.gz: e54c7820faf040901935c80797d737cc89dd1b6c7da873621361295446b35d2dab4987dee6ee423086a9dc3a7b6fb3ce20f699a5ff92edf1c7673ef071f089f9
7
+ data.tar.gz: f47b41311a3aa11fc38479d98f0fc11ce1f370f23ceea127ac44462ea7bf148c66d190662dcd05d50f955cab117b9802cd4d8bb639d747a91db8136bcda16e43
data/README.md CHANGED
@@ -2,509 +2,25 @@
2
2
 
3
3
  Actors, Observers and Data Pipelines.
4
4
 
5
- ## Configuration
5
+ ## Usage
6
6
 
7
- The most important configuration setting is the `mode`, which governs how background tasks are handled.
7
+ Start off by [configuring Plumbing](/docs/config.md) and selecting your `mode`.
8
8
 
9
- By default it is `:inline`, so every command or query is handled synchronously. This is the ruby behaviour you know and love (although see the section on `await` below).
9
+ ## Pipelines
10
10
 
11
- `:async` mode handles tasks using fibers (via the [Async gem](https://socketry.github.io/async/index.html)). Your code should include the "async" gem in its bundle, as Plumbing does not load it by default.
11
+ [Data transformations](/docs/pipelines.md) similar to unix pipes.
12
12
 
13
- `:threaded` mode handles tasks using a thread pool via [Concurrent Ruby](https://ruby-concurrency.github.io/concurrent-ruby/master/Concurrent/Promises.html)). Your code should include the "concurrent-ruby" gem in its bundle, as Plumbing does not load it by default.
13
+ ## Actors
14
14
 
15
- However, `:threaded` mode is not safe for Ruby on Rails applications. In this case, use `:threaded_rails` mode, which is identical to `:threaded`, except it wraps the tasks in the Rails executor. This ensures your actors do not interfere with the Rails framework. Note that the Concurrent Ruby's default `:io` scheduler will create extra threads at times of high demand, which may put pressure on the ActiveRecord database connection pool. A future version of plumbing will allow the thread pool to be adjusted with a maximum number of threads, preventing contention with the connection pool.
15
+ [Asynchronous, thread-safe, objects](/docs/actors.md).
16
16
 
17
- The `timeout` setting is used when performing queries - it defaults to 30s.
17
+ ## Pipes
18
18
 
19
- ```ruby
20
- require "plumbing"
21
- puts Plumbing.config.mode
22
- # => :inline
23
-
24
- Plumbing.configure mode: :async, timeout: 10
25
-
26
- puts Plumbing.config.mode
27
- # => :async
28
- ```
29
-
30
- If you are running a test suite, you can temporarily update the configuration by passing a block.
31
-
32
- ```ruby
33
- require "plumbing"
34
- puts Plumbing.config.mode
35
- # => :inline
36
-
37
- Plumbing.configure mode: :async do
38
- puts Plumbing.config.mode
39
- # => :async
40
- first_test
41
- second_test
42
- end
43
-
44
- puts Plumbing.config.mode
45
- # => :inline
46
- ```
47
-
48
- ## Plumbing::Pipeline - transform data through a pipeline
49
-
50
- Define a sequence of operations that proceed in order, passing their output from one operation as the input to another. [Unix pipes](https://en.wikipedia.org/wiki/Pipeline_(Unix)) in Ruby.
51
-
52
- Use `perform` to define a step that takes some input and returns a different output.
53
- Specify `using` to re-use an existing `Plumbing::Pipeline` as a step within this pipeline.
54
- Use `execute` to define a step that takes some input, performs an action but passes the input, unchanged, to the next step.
55
-
56
- If you have [dry-validation](https://dry-rb.org/gems/dry-validation/1.10/) installed, you can validate your input using a `Dry::Validation::Contract`. Alternatively, you can define a `pre_condition` to test that the inputs are valid.
57
-
58
- You can also verify that the output generated is as expected by defining a `post_condition`.
59
-
60
- ### Usage:
61
-
62
- [Building an array using multiple steps with a pre-condition and post-condition](/spec/examples/pipeline_spec.rb)
63
-
64
- ```ruby
65
- require "plumbing"
66
- class BuildArray < Plumbing::Pipeline
67
- perform :add_first
68
- perform :add_second
69
- perform :add_third
70
-
71
- pre_condition :must_be_an_array do |input|
72
- input.is_a? Array
73
- end
74
-
75
- post_condition :must_have_three_elements do |output|
76
- output.length == 3
77
- end
78
-
79
- private
80
-
81
- def add_first(input) = input << "first"
82
-
83
- def add_second(input) = input << "second"
84
-
85
- def add_third(input) = input << "third"
86
- end
87
-
88
- BuildArray.new.call []
89
- # => ["first", "second", "third"]
90
-
91
- BuildArray.new.call 1
92
- # => Plumbing::PreconditionError("must_be_an_array")
93
-
94
- BuildArray.new.call ["extra element"]
95
- # => Plumbing::PostconditionError("must_have_three_elements")
96
- ```
97
-
98
- [Validating input parameters with a contract](/spec/examples/pipeline_spec.rb)
99
- ```ruby
100
- require "plumbing"
101
- require "dry/validation"
102
-
103
- class SayHello < Plumbing::Pipeline
104
- validate_with "SayHello::Input"
105
- perform :say_hello
106
-
107
- private
108
-
109
- def say_hello input
110
- "Hello #{input[:name]} - I will now send a load of annoying marketing messages to #{input[:email]}"
111
- end
112
-
113
- class Input < Dry::Validation::Contract
114
- params do
115
- required(:name).filled(:string)
116
- required(:email).filled(:string)
117
- end
118
- rule :email do
119
- key.failure("must be a valid email") unless /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i.match? value
120
- end
121
- end
122
- end
123
-
124
- SayHello.new.call(name: "Alice", email: "alice@example.com")
125
- # => Hello Alice - I will now send a load of annoying marketing messages to alice@example.com
126
-
127
- SayHello.new.call(some: "other data")
128
- # => Plumbing::PreConditionError
129
- ```
130
-
131
- [Building a pipeline through composition](/spec/examples/pipeline_spec.rb)
132
-
133
- ```ruby
134
- require "plumbing"
135
- class ExternalStep < Plumbing::Pipeline
136
- perform :add_item_to_array
137
-
138
- private
139
-
140
- def add_item_to_array(input) = input << "external"
141
- end
142
-
143
- class BuildSequenceWithExternalStep < Plumbing::Pipeline
144
- perform :add_first
145
- perform :add_second, using: "ExternalStep"
146
- perform :add_third
19
+ [Composable observers](/docs/pipes.md).
147
20
 
148
- private
21
+ ## Rubber ducks
149
22
 
150
- def add_first(input) = input << "first"
151
-
152
- def add_third(input) = input << "third"
153
- end
154
-
155
- BuildSequenceWithExternalStep.new.call([])
156
- # => ["first", "external", "third"]
157
- ```
158
-
159
- ## Plumbing::Actor - safe asynchronous objects
160
-
161
- An [actor](https://en.wikipedia.org/wiki/Actor_model) defines the messages an object can receive, similar to a regular object.
162
- However, in traditional object-orientated programming, a thread of execution moves from one object to another. If there are multiple threads, then each object may be accessed concurrently, leading to race conditions or data-integrity problems - and very hard to track bugs.
163
-
164
- Actors are different. Conceptually, each actor has it's own thread of execution, isolated from every other actor in the system. When one actor sends a message to another actor, the receiver does not execute its method in the caller's thread. Instead, it places the message on a queue and waits until its own thread is free to process the work. If the caller would like to access the return value from the method, then it must wait until the receiver has finished processing.
165
-
166
- This means each actor is only ever accessed by a single thread and the vast majority of concurrency issues are eliminated.
167
-
168
- [Plumbing::Actor](/lib/plumbing/actor.rb) allows you to define the `async` public interface to your objects. Calling `.start` builds a proxy to the actual instance of your object and ensures that any messages sent are handled in a manner appropriate to the current mode - immediately for inline mode, using fibers for async mode and using threads for threaded and threaded_rails mode.
169
-
170
- When sending messages to an actor, this just works.
171
-
172
- However, as the caller, you do not have direct access to the return values of the messages that you send. Instead, you must call `#value` - or alternatively, wrap your call in `await { ... }`. The block form of `await` is added in to ruby's `Kernel` so it is available everywhere. It is also safe to use with non-actors (in which case it just returns the original value from the block).
173
-
174
- ```ruby
175
- @actor = MyActor.start name: "Alice"
176
-
177
- @actor.name.value
178
- # => "Alice"
179
-
180
- await { @actor.name }
181
- # => "Alice"
182
-
183
- await { "Bob" }
184
- # => "Bob"
185
- ```
186
-
187
- This then makes the caller's thread block until the receiver's thread has finished its work and returned a value. Or if the receiver raises an exception, that exception is then re-raised in the calling thread.
188
-
189
- The actor model does not eliminate every possible concurrency issue. If you use `value` or `await`, it is possible to deadlock yourself.
190
-
191
- Actor A, running in Thread 1, sends a message to Actor B and then awaits the result, meaning Thread 1 is blocked. Actor B, running in Thread 2, starts to work, but needs to ask Actor A a question. So it sends a message to Actor A and awaits the result. Thread 2 is now blocked, waiting for Actor A to respond. But Actor A, running in Thread 1, is blocked, waiting for Actor B to respond.
192
-
193
- This potential deadlock only occurs if you use `value` or `await` and have actors that call back in to each other. If your objects are strictly layered, or you never use `value` or `await` (perhaps, instead using a Pipe to observe events), then this particular deadlock should not occur. However, just in case, every call to `value` has a timeout defaulting to 30s.
194
-
195
- ### Inline actors
196
-
197
- Even though inline mode is not asynchronous, you must still use `value` or `await` to access the results from another actor. However, as deadlocks are impossible in a single thread, there is no timeout.
198
-
199
- ### Async actors
200
-
201
- Using async mode is probably the easiest way to add concurrency to your application. It uses fibers to allow for "concurrency but not parallelism" - that is execution will happen in the background but your objects or data will never be accessed by two things at the exact same time.
202
-
203
- ### Threaded actors
204
-
205
- Using threaded (or threaded_rails) mode gives you concurrency and parallelism. If all your public objects are actors and you are careful about callbacks then the actor model will keep your code safe. But there are a couple of extra things to consider.
206
-
207
- Firstly, when you pass parameters or return results between threads, those objects are "transported" across the boundaries.
208
- Most objects are cloned. Hashes, keyword arguments and arrays have their contents recursively transported. And any object that uses `GlobalID::Identification` (for example, ActiveRecord models) are marshalled into a GlobalID, then unmarshalled back in to their original object. This is to prevent the same object from being amended in both the caller and receiver's threads.
209
-
210
- Secondly, when you pass a block (or Proc parameter) to another actor, the block/proc will be executed in the receiver's thread. This means you must not access any variables that would normally be in scope for your block (whether local variables or instance variables of other objects - see note below) This is because you will be accessing them from a different thread to where they were defined, leading to potential race conditions. And, if you access any actors, you must not use `value` or `await` or you risk a deadlock. If you are within an actor and need to pass a block or proc parameter, you should use the `safely` method to ensure that your block is run within the context of the calling actor, not the receiving actor.
211
-
212
- For example, when defining a custom filter, the filter adds itself as an observer to its source. The source triggers the `received` method on the filter, which will run in the context of the source. So the custom filter uses `safely` to move back into its own context and access its instance variables.
213
-
214
- ```ruby
215
- class EveryThirdEvent < Plumbing::CustomFilter
216
- def initialize source:
217
- super
218
- @events = []
219
- end
220
-
221
- def received event
222
- safely do
223
- @events << event
224
- if @events.count >= 3
225
- @events.clear
226
- self << event
227
- end
228
- end
229
- end
230
- end
231
- ```
232
-
233
- (Note: we break that rule in the specs for Pipe objects - we use a block observer that sets the value on a local variable. That's because it is a controlled situation where we know there are only two threads involved and we are explicitly waiting for the second thread to complete. For almost every app that uses actors, there will be multiple threads and it will be impossible to predict the access patterns).
234
-
235
- ### Constructing actors
236
-
237
- Instead of constructing your object with `.new`, use `.start`. This builds a proxy object that wraps the target instance and dispatches messages through a safe mechanism. Only messages that have been defined as part of the actor are available in this proxy - so you don't have to worry about callers bypassing the actor's internal context.
238
-
239
- ### Referencing actors
240
-
241
- If you're within a method inside your actor and you want to pass a reference to yourself, instead of using `self`, you should use `proxy` (which is also aliased as `as_actor` or `async`).
242
-
243
- Also be aware that if you use actors in one place, you need to use them everywhere - especially if you're using threads. This is because as the actor sends messages to its collaborators, those calls will be made from within the actor's internal context. If the collaborators are also actors, the subsequent messages will be handled correctly, if not, data consistency bugs could occur. This does not mean that every class needs to be an actor, just your "public API" classes which may be accessed from multiple actors or other threads.
244
-
245
- ### Usage
246
-
247
- [Defining an actor](/spec/examples/actor_spec.rb)
248
-
249
- ```ruby
250
- require "plumbing"
251
-
252
- class Employee
253
- include Plumbing::Actor
254
- async :name, :job_title, :greet_slowly, :promote
255
- attr_reader :name, :job_title
256
-
257
- def initialize(name)
258
- @name = name
259
- @job_title = "Sales assistant"
260
- end
261
-
262
- private
263
-
264
- def promote
265
- sleep 0.5
266
- @job_title = "Sales manager"
267
- end
268
-
269
- def greet_slowly
270
- sleep 0.2
271
- "H E L L O"
272
- end
273
- end
274
-
275
- @person = Employee.start "Alice"
276
-
277
- await { @person.name }
278
- # => "Alice"
279
- await { @person.job_title }
280
- # => "Sales assistant"
281
-
282
- # by using `await`, we will block until `greet_slowly` has returned a value
283
- await { @person.greet_slowly }
284
- # => "H E L L O"
285
-
286
- # this time, we're not awaiting the result, so this will run in the background (unless we're using inline mode)
287
- @person.greet_slowly
288
-
289
- # this will run in the background
290
- @person.promote
291
- # this will block - it will not return until the previous calls, #greet_slowly, #promote, and this call to #job_title have completed
292
- await { @person.job_title }
293
- # => "Sales manager"
294
- ```
295
-
296
- ## Plumbing::Pipe - a composable observer
297
-
298
- [Observers](https://ruby-doc.org/3.3.0/stdlibs/observer/Observable.html) in Ruby are a pattern where objects (observers) register their interest in another object (the observable). This pattern is common throughout programming languages (event listeners in Javascript, the dependency protocol in [Smalltalk](https://en.wikipedia.org/wiki/Smalltalk)).
299
-
300
- [Plumbing::Pipe](lib/plumbing/pipe.rb) makes observers "composable". Instead of simply just registering for notifications from a single observable, we can build sequences of pipes. These sequences can filter notifications and route them to different listeners, or merge multiple sources into a single stream of notifications.
301
-
302
- Pipes are implemented as actors, meaning that event notifications can be dispatched asynchronously. The observer's callback will be triggered from within the pipe's internal context so you should immediately trigger a command on another actor to maintain safety.
303
-
304
- ### Usage
305
-
306
- [A simple observer](/spec/examples/pipe_spec.rb):
307
- ```ruby
308
- require "plumbing"
309
-
310
- @source = Plumbing::Pipe.start
311
- @observer = @source.add_observer do |event|
312
- puts event.type
313
- end
314
-
315
- @source.notify "something_happened", message: "But what was it?"
316
- # => "something_happened"
317
- ```
318
-
319
- [Simple filtering](/spec/examples/pipe_spec.rb):
320
- ```ruby
321
- require "plumbing"
322
-
323
- @source = Plumbing::Pipe.start
324
- @filter = Plumbing::Filter.start source: @source do |event|
325
- %w[important urgent].include? event.type
326
- end
327
- @observer = @filter.add_observer do |event|
328
- puts event.type
329
- end
330
-
331
- @source.notify "important", message: "ALERT! ALERT!"
332
- # => "important"
333
- @source.notify "unimportant", message: "Nothing to see here"
334
- # => <no output>
335
- ```
336
-
337
- [Custom filtering](/spec/examples/pipe_spec.rb):
338
- ```ruby
339
- require "plumbing"
340
- class EveryThirdEvent < Plumbing::CustomFilter
341
- def initialize source:
342
- super source: source
343
- @events = []
344
- end
345
-
346
- def received event
347
- # #received is called in the context of the `source` actor
348
- # in order to safely access the `EveryThirdEvent` instance variables
349
- # we need to move into the context of our own actor
350
- safely do
351
- # store this event into our buffer
352
- @events << event
353
- # if this is the third event we've received then clear the buffer and broadcast the latest event
354
- if @events.count >= 3
355
- @events.clear
356
- self << event
357
- end
358
- end
359
- end
360
- end
361
-
362
- @source = Plumbing::Pipe.start
363
- @filter = EveryThirdEvent.start(source: @source)
364
- @observer = @filter.add_observer do |event|
365
- puts event.type
366
- end
367
-
368
- 1.upto 10 do |i|
369
- @source.notify i.to_s
370
- end
371
- # => "3"
372
- # => "6"
373
- # => "9"
374
- ```
375
-
376
- [Joining multiple sources](/spec/examples/pipe_spec.rb):
377
- ```ruby
378
- require "plumbing"
379
-
380
- @first_source = Plumbing::Pipe.start
381
- @second_source = Plumbing::Pipe.start
382
-
383
- @junction = Plumbing::Junction.start @first_source, @second_source
384
-
385
- @observer = @junction.add_observer do |event|
386
- puts event.type
387
- end
388
-
389
- @first_source.notify "one"
390
- # => "one"
391
- @second_source.notify "two"
392
- # => "two"
393
- ```
394
- ## Plumbing::RubberDuck - duck types and type-casts
395
-
396
- Define an [interface or protocol](https://en.wikipedia.org/wiki/Interface_(object-oriented_programming)) specifying which messages you expect to be able to send.
397
-
398
- Then cast an object into that type. This first tests that the object can respond to those messages and then builds a proxy that responds to those messages (and no others). However, if you take one of these proxies, you can safely re-cast it as another type (as long as the original target object responds to the correct messages).
399
-
400
- ### Usage
401
-
402
- Define your interface (Person in this example), then cast your objects (instances of PersonData and CarData).
403
-
404
- [Casting objects as duck-types](/spec/examples/rubber_duck_spec.rb):
405
- ```ruby
406
- require "plumbing"
407
-
408
- Person = Plumbing::RubberDuck.define :first_name, :last_name, :email
409
- LikesFood = Plumbing::RubberDuck.define :favourite_food
410
-
411
- PersonData = Struct.new(:first_name, :last_name, :email, :favourite_food)
412
- CarData = Struct.new(:make, :model, :colour)
413
-
414
- @porsche_911 = CarData.new "Porsche", "911", "black"
415
- @person = @porsche_911.as Person
416
- # => Raises a TypeError as CarData does not respond_to #first_name, #last_name, #email
417
-
418
- @alice = PersonData.new "Alice", "Aardvark", "alice@example.com", "Ice cream"
419
- @person = @alice.as Person
420
- @person.first_name
421
- # => "Alice"
422
- @person.email
423
- # => "alice@example.com"
424
- @person.favourite_food
425
- # => NoMethodError - #favourite_food is not part of the Person rubber duck (even though it is part of the underlying PersonData struct)
426
-
427
- # Cast our Person into a LikesFood rubber duck
428
- @hungry = @person.as LikesFood
429
- @hungry.favourite_food
430
- # => "Ice cream"
431
- ```
432
-
433
- You can also use the same `@object.as type` to type-check instances against modules or classes. This creates a RubberDuck proxy based on the module or class you're casting into. So the cast will pass if the object responds to the correct messages, even if a strict `.is_a?` test would fail.
434
-
435
- ```ruby
436
- require "plumbing"
437
-
438
- module Person
439
- def first_name = @first_name
440
-
441
- def last_name = @last_name
442
-
443
- def email = @email
444
- end
445
-
446
- module LikesFood
447
- def favourite_food = @favourite_food
448
- end
449
-
450
- PersonData = Struct.new(:first_name, :last_name, :email, :favourite_food)
451
- CarData = Struct.new(:make, :model, :colour)
452
-
453
- @porsche_911 = CarData.new "Porsche", "911", "black"
454
- expect { @porsche_911.as Person }.to raise_error(TypeError)
455
-
456
- @alice = PersonData.new "Alice", "Aardvark", "alice@example.com", "Ice cream"
457
-
458
- @alics.is_a? Person
459
- # => false - PersonData does not `include Person`
460
- @person = @alice.as Person
461
- # This cast is OK because PersonData responds to :first_name, :last_name and :email
462
- expect(@person.first_name).to eq "Alice"
463
- expect(@person.email).to eq "alice@example.com"
464
- expect { @person.favourite_food }.to raise_error(NoMethodError)
465
-
466
- @hungry = @person.as LikesFood
467
- expect(@hungry.favourite_food).to eq "Ice cream"
468
- ```
469
-
470
- ## Testing
471
-
472
- As soon as you're working in :async or :threaded mode, you'll find your tests become unpredictable.
473
-
474
- To help with this there are some helpers that you can include in your code.
475
-
476
- Firstly, you can wait for something to become true. The `#wait_for` method is added into `Kernel` so it is available everywhere. It repeatedly executes the given block until a truthy value is returned or the timeout is reached (at which point it raises a Timeout::Error). Note that you still need to use `await` (or call `#value`) to access return values from messages sent to actors.
477
-
478
- ```ruby
479
- @target = SomeActor.start
480
- @subject = SomeOtherActor.start
481
-
482
- @subject.do_something_to @target
483
-
484
- wait_for do
485
- await { @target.was_updated? }
486
- end
487
-
488
- ```
489
-
490
- Secondly, if you're using RSpec, you can `require "plumbing/spec/become_matchers"` to add some extra expectation matchers. These matchers use `wait_for` to repeatedly evaluate the given block until it reaches the expected value or times out. The matchers are `become(value)`, `become_true`, `become_false`, `become_truthy` and `become_falsey`. Note that you still need to use `await` (or call `#value`) to access return values from messages sent to actors.
491
-
492
- ```ruby
493
- @target = SomeActor.start
494
- @subject = SomeOtherActor.start
495
-
496
- @subject.do_something_to @target
497
-
498
- expect { @target.was_updated?.value }.to become_true
499
-
500
- @employee = Employee.start
501
-
502
- expect { @employee.job_title.value }.to become "Sales assistant"
503
-
504
- @employee.promote!
505
-
506
- expect { @employee.job_title.value }.to become "Manager"
507
- ```
23
+ [Type-safety the ruby way](/docs/rubber_ducks.md).
508
24
 
509
25
  ## Installation
510
26
 
@@ -525,6 +41,8 @@ require 'plumbing'
525
41
  Plumbing.config mode: :async
526
42
  ```
527
43
 
44
+
45
+
528
46
  ## Development
529
47
 
530
48
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
@@ -9,20 +9,24 @@ module Plumbing
9
9
 
10
10
  def initialize target
11
11
  @target = target
12
- @queue = []
13
- @semaphore = ::Async::Semaphore.new(1)
12
+ @semaphore = ::Async::Semaphore.new(Plumbing.config.max_concurrency)
14
13
  end
15
14
 
16
15
  # Send the message to the target and wrap the result
17
- def send_message(message_name, *, **, &)
16
+ def send_message(message_name, *args, **params, &block)
17
+ Plumbing.config.logger.debug { "-> #{@target.class}##{message_name}(#{args.inspect}, #{params.inspect})" }
18
18
  task = @semaphore.async do
19
- @target.send(message_name, *, **, &)
19
+ Plumbing.config.logger.debug { "---> #{@target.class}##{message_name}(#{args.inspect}, #{params.inspect})" }
20
+ @target.send(message_name, *args, **params, &block)
20
21
  end
22
+ sleep 0.01
21
23
  Result.new(task)
22
24
  end
23
25
 
24
26
  def safely(&)
27
+ Plumbing.config.logger.debug { "-> #{@target.class}#perform_safely" }
25
28
  send_message(:perform_safely, &)
29
+ sleep 0.01
26
30
  nil
27
31
  end
28
32
 
@@ -32,6 +36,7 @@ module Plumbing
32
36
 
33
37
  Result = Data.define(:task) do
34
38
  def value
39
+ sleep 0.01
35
40
  Timeout.timeout(Plumbing::Actor.timeout) do
36
41
  task.wait
37
42
  end
@@ -6,14 +6,19 @@ module Plumbing
6
6
  end
7
7
 
8
8
  # Send the message to the target and wrap the result
9
- def send_message(message_name, *, **, &)
10
- value = @target.send(message_name, *, **, &)
9
+ def send_message(message_name, *args, **params, &)
10
+ Plumbing.config.logger.debug { "-> #{@target.class}##{message_name}(#{args.inspect}, #{params.inspect})" }
11
+ Plumbing.config.logger.debug { "---> #{@target.class}##{message_name}(#{args.inspect}, #{params.inspect})" }
12
+ value = @target.send(message_name, *args, **params, &)
13
+ Plumbing.config.logger.debug { "===> #{@target.class}##{message_name} => #{value}" }
11
14
  Result.new(value)
12
15
  rescue => ex
16
+ Plumbing.config.logger.debug { "!!!! #{@target.class}##{message_name} => #{ex}" }
13
17
  Result.new(ex)
14
18
  end
15
19
 
16
20
  def safely(&)
21
+ Plumbing.config.logger.debug { "-> #{@target.class}#perform_safely" }
17
22
  send_message(:perform_safely, &)
18
23
  nil
19
24
  end
@@ -8,8 +8,6 @@ require_relative "transporter"
8
8
  module Plumbing
9
9
  module Actor
10
10
  class Threaded
11
- attr_reader :target
12
-
13
11
  def initialize target
14
12
  @target = target
15
13
  @queue = Concurrent::Array.new
@@ -18,7 +16,7 @@ module Plumbing
18
16
 
19
17
  # Send the message to the target and wrap the result
20
18
  def send_message(message_name, *args, **params, &block)
21
- puts "->#{@target.class}##{message_name}(#{args.inspect}, #{params.inspect})\n#{Thread.current.name}" if Plumbing.config.debug
19
+ Plumbing.config.logger.debug { "-> #{@target.class}##{message_name}(#{args.inspect}, #{params.inspect})\n#{Thread.current.name}" }
22
20
  Message.new(@target, message_name, Plumbing::Actor.transporter.marshal(*args), Plumbing::Actor.transporter.marshal(params).first, block, Concurrent::MVar.new).tap do |message|
23
21
  @queue << message
24
22
  send_messages
@@ -26,6 +24,7 @@ module Plumbing
26
24
  end
27
25
 
28
26
  def safely(&)
27
+ Plumbing.config.logger.debug { "-> #{@target.class}#perform_safely\n#{Thread.current.name}" }
29
28
  send_message(:perform_safely, &)
30
29
  nil
31
30
  end
@@ -58,13 +57,13 @@ module Plumbing
58
57
  def call
59
58
  args = Plumbing::Actor.transporter.unmarshal(*packed_args)
60
59
  params = Plumbing::Actor.transporter.unmarshal(packed_params)
61
- puts "=> #{target.class}##{message_name}(#{args.first.inspect}, #{params.first.inspect}, &#{!unsafe_block.nil?})\n#{Thread.current.name}" if Plumbing.config.debug
60
+ Plumbing.config.logger.debug { "---> #{target.class}##{message_name}(#{args.first.inspect}, #{params.first.inspect}, &#{!unsafe_block.nil?})\n#{Thread.current.name}" }
62
61
  value = target.send message_name, *args, **params.first, &unsafe_block
62
+ Plumbing.config.logger.debug { "===> #{target.class}##{message_name} => #{value}\n#{Thread.current.name}" }
63
63
 
64
64
  result.put Plumbing::Actor.transporter.marshal(value)
65
65
  rescue => ex
66
- puts ex
67
- puts ex.backtrace
66
+ Plumbing.config.logger.debug { "!!!! #{target.class}##{message_name} => #{ex}\n#{Thread.current.name}" }
68
67
  result.put ex
69
68
  end
70
69
 
@@ -86,7 +86,7 @@ module Plumbing
86
86
  instance_eval(&)
87
87
  nil
88
88
  rescue => ex
89
- puts ex
89
+ Plumbing.config.logger.error { "!!!! #{self.class}#perform_safely => #{ex}" }
90
90
  nil
91
91
  end
92
92
  end
@@ -1,16 +1,7 @@
1
+ require "logger"
2
+
1
3
  # Pipes, pipelines, actors and rubber ducks
2
4
  module Plumbing
3
- Config = Data.define :mode, :actor_proxy_classes, :timeout, :debug do
4
- def actor_proxy_class_for target_class
5
- actor_proxy_classes[target_class]
6
- end
7
-
8
- def register_actor_proxy_class_for target_class, proxy_class
9
- actor_proxy_classes[target_class] = proxy_class
10
- end
11
- end
12
- private_constant :Config
13
-
14
5
  # Access the current configuration
15
6
  # @return [Config]
16
7
  def self.config
@@ -45,7 +36,23 @@ module Plumbing
45
36
  private_class_method :set_configuration_and_yield
46
37
 
47
38
  def self.configs
48
- @configs ||= [Config.new(mode: :inline, timeout: 30, actor_proxy_classes: {}, debug: false)]
39
+ @configs ||= [Config.new(mode: :inline, timeout: 30, actor_proxy_classes: {}, max_concurrency: 12, logger: logger)]
49
40
  end
50
41
  private_class_method :configs
42
+
43
+ def self.logger
44
+ @logger = Logger.new($stdout, level: Logger::INFO)
45
+ end
46
+ private_class_method :logger
47
+
48
+ Config = Data.define :mode, :actor_proxy_classes, :timeout, :max_concurrency, :logger do
49
+ def actor_proxy_class_for target_class
50
+ actor_proxy_classes[target_class]
51
+ end
52
+
53
+ def register_actor_proxy_class_for target_class, proxy_class
54
+ actor_proxy_classes[target_class] = proxy_class
55
+ end
56
+ end
57
+ private_constant :Config
51
58
  end
@@ -1,15 +1,17 @@
1
1
  module Plumbing
2
2
  # A pipe that can be subclassed to filter events from a source pipe
3
- class CustomFilter < Pipe
3
+ class Pipe::CustomFilter < Pipe
4
4
  # Chain this pipe to the source pipe
5
5
  # @param source [Plumbing::Observable] the source from which to receive and filter events
6
6
  def initialize source:
7
7
  super()
8
- source.as(Observable).add_observer { |event| received event }
8
+ source.as(Observable).add_observer do |event_name, data|
9
+ received event_name, data
10
+ end
9
11
  end
10
12
 
11
13
  protected
12
14
 
13
- def received(event) = raise NoMethodError.new("Subclass should define #received")
15
+ def received(event_name, data) = raise NoMethodError.new("Subclass should define #received")
14
16
  end
15
17
  end
@@ -1,7 +1,7 @@
1
1
  require_relative "custom_filter"
2
2
  module Plumbing
3
3
  # A pipe that filters events from a source pipe
4
- class Filter < CustomFilter
4
+ class Pipe::Filter < Pipe::CustomFilter
5
5
  # Chain this pipe to the source pipe
6
6
  # @param source [Plumbing::Observable] the source from which to receive and filter events
7
7
  # @param &accepts [Block] a block that returns a boolean value - true to accept the event, false to reject it
@@ -14,9 +14,10 @@ module Plumbing
14
14
 
15
15
  protected
16
16
 
17
- def received(event)
18
- return nil unless @accepts.call event
19
- dispatch event
17
+ def received(event_name, data)
18
+ safely do
19
+ notify event_name, **data if @accepts.call(event_name, data)
20
+ end
20
21
  end
21
22
  end
22
23
  end
@@ -1,6 +1,6 @@
1
1
  module Plumbing
2
2
  # A pipe that filters events from a source pipe
3
- class Junction < Pipe
3
+ class Pipe::Junction < Pipe
4
4
  # Chain multiple sources to this pipe
5
5
  # @param sources [Array<Plumbing::Observable>] the sources which will be joined and relayed
6
6
  def initialize *sources
@@ -11,9 +11,9 @@ module Plumbing
11
11
  private
12
12
 
13
13
  def add source
14
- source.as(Observable).add_observer do |event|
14
+ source.as(Observable).add_observer do |event_name, **data|
15
15
  safely do
16
- dispatch event
16
+ notify event_name, **data
17
17
  end
18
18
  end
19
19
  end
data/lib/plumbing/pipe.rb CHANGED
@@ -3,21 +3,19 @@ module Plumbing
3
3
  class Pipe
4
4
  include Plumbing::Actor
5
5
 
6
- async :notify, :<<, :remove_observer, :add_observer, :is_observer?, :shutdown
6
+ async :notify, :remove_observer, :add_observer, :is_observer?, :shutdown
7
7
 
8
- # Push an event into the pipe
9
- # @param event [Plumbing::Event] the event to push into the pipe
10
- def << event
11
- raise Plumbing::InvalidEvent.new event unless event.is_a? Plumbing::Event
12
- dispatch event
13
- end
14
-
15
- # A shortcut to creating and then pushing an event
16
- # @param event_type [String] representing the type of event this is
8
+ # Notify observers about an event
9
+ # @param event_name [String] representing the type of event this is
17
10
  # @param data [Hash] representing the event-specific data to be passed to the observers
18
- def notify event_type, data = nil
19
- Event.new(type: event_type, data: data).tap do |event|
20
- self << event
11
+ def notify event_name, **data
12
+ Plumbing.config.logger.debug { "-> #{self.class}#notify #{event_name}" }
13
+ observers.each do |observer|
14
+ Plumbing.config.logger.debug { "===> #{self.class}#dispatch #{event_name}(#{data}) to #{observer}" }
15
+ observer.call event_name, data
16
+ rescue => ex
17
+ Plumbing.config.logger.error { "!!!! #{self.class}#dispatch #{event_name} => #{ex}" }
18
+ ex
21
19
  end
22
20
  end
23
21
 
@@ -52,23 +50,14 @@ module Plumbing
52
50
  stop
53
51
  end
54
52
 
55
- protected
56
-
57
- # Dispatch an event to all observers
58
- # @param event [Plumbing::Event]
59
- # Enumerates all observers and `calls` them with this event
60
- # Discards any errors raised by the observer so that all observers will be successfully notified
61
- def dispatch event
62
- observers.each do |observer|
63
- observer.call event
64
- rescue => ex
65
- puts ex
66
- ex
67
- end
68
- end
53
+ private
69
54
 
70
55
  def observers
71
56
  @observers ||= []
72
57
  end
58
+
59
+ require_relative "pipe/filter"
60
+ require_relative "pipe/junction"
61
+
73
62
  end
74
63
  end
@@ -0,0 +1,14 @@
1
+ module Plumbing
2
+ module Spec
3
+ def self.modes(inline: true, async: true, threaded: true, &)
4
+ Plumbing.configure(mode: :inline, &) if inline
5
+ Sync { Plumbing.configure(mode: :async, &) } if async
6
+ Plumbing.configure(mode: thread_mode, &) if threaded
7
+ end
8
+
9
+ def self.thread_mode
10
+ defined?(::Rails) ? :threaded_rails : :threaded
11
+ end
12
+ private_class_method :thread_mode
13
+ end
14
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Plumbing
4
- VERSION = "0.4.5"
4
+ VERSION = "0.4.6"
5
5
  end
data/lib/plumbing.rb CHANGED
@@ -6,10 +6,7 @@ module Plumbing
6
6
  require_relative "plumbing/rubber_duck"
7
7
  require_relative "plumbing/types"
8
8
  require_relative "plumbing/error"
9
- require_relative "plumbing/event"
10
9
  require_relative "plumbing/pipe"
11
- require_relative "plumbing/filter"
12
- require_relative "plumbing/junction"
13
10
  require_relative "plumbing/pipeline"
14
11
  require_relative "plumbing/version"
15
12
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: standard-procedure-plumbing
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.5
4
+ version: 0.4.6
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-18 00:00:00.000000000 Z
11
+ date: 2024-09-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: globalid
@@ -43,12 +43,11 @@ files:
43
43
  - lib/plumbing/actor/threaded.rb
44
44
  - lib/plumbing/actor/transporter.rb
45
45
  - lib/plumbing/config.rb
46
- - lib/plumbing/custom_filter.rb
47
46
  - lib/plumbing/error.rb
48
- - lib/plumbing/event.rb
49
- - lib/plumbing/filter.rb
50
- - lib/plumbing/junction.rb
51
47
  - lib/plumbing/pipe.rb
48
+ - lib/plumbing/pipe/custom_filter.rb
49
+ - lib/plumbing/pipe/filter.rb
50
+ - lib/plumbing/pipe/junction.rb
52
51
  - lib/plumbing/pipeline.rb
53
52
  - lib/plumbing/pipeline/contracts.rb
54
53
  - lib/plumbing/pipeline/operations.rb
@@ -57,6 +56,7 @@ files:
57
56
  - lib/plumbing/rubber_duck/object.rb
58
57
  - lib/plumbing/rubber_duck/proxy.rb
59
58
  - lib/plumbing/spec/become_matchers.rb
59
+ - lib/plumbing/spec/modes.rb
60
60
  - lib/plumbing/types.rb
61
61
  - lib/plumbing/version.rb
62
62
  homepage: https://github.com/standard-procedure/plumbing
@@ -1,4 +0,0 @@
1
- module Plumbing
2
- # An immutable data structure representing an Event
3
- Event = Data.define :type, :data
4
- end