ntl-orchestra 0.9.2 → 0.9.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +3 -0
- data/Gemfile +1 -1
- data/README.md +91 -90
- data/lib/orchestra/conductor.rb +6 -6
- data/lib/orchestra/dsl/object_adapter.rb +16 -16
- data/lib/orchestra/dsl/operations.rb +35 -35
- data/lib/orchestra/dsl/{nodes.rb → steps.rb} +7 -9
- data/lib/orchestra/execution.rb +158 -0
- data/lib/orchestra/operation.rb +16 -16
- data/lib/orchestra/recording/playback.rb +47 -0
- data/lib/orchestra/recording.rb +5 -45
- data/lib/orchestra/run_list.rb +49 -49
- data/lib/orchestra/{node → step}/output.rb +8 -8
- data/lib/orchestra/{node.rb → step.rb} +22 -25
- data/lib/orchestra/thread_pool.rb +3 -3
- data/lib/orchestra/version.rb +1 -1
- data/lib/orchestra.rb +2 -2
- data/test/examples/fizz_buzz.rb +5 -5
- data/test/examples/invitation_service.rb +9 -9
- data/test/integration/multithreading_test.rb +5 -5
- data/test/integration/recording_telemetry_test.rb +3 -6
- data/test/integration/replayable_operation_test.rb +4 -4
- data/test/lib/console.rb +1 -1
- data/test/support/telemetry_recorder.rb +7 -7
- data/test/unit/dsl_test.rb +26 -26
- data/test/unit/object_adapter_test.rb +14 -14
- data/test/unit/operation_test.rb +45 -45
- data/test/unit/run_list_test.rb +12 -12
- data/test/unit/step_test.rb +122 -0
- data/test/unit/thread_pool_test.rb +2 -2
- metadata +15 -13
- data/lib/orchestra/performance.rb +0 -137
- data/test/unit/node_test.rb +0 -122
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 04d7c524d1821f1d2dcaec41a3a567c906b69830
|
4
|
+
data.tar.gz: 7be0bf35060a1714d0edb8d6489d49f46011ac13
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 82a9d1d61b21cbd09464027b6a72c43fc433979ec851be8906b3f7eb590c7baad62ef200cae5cb21a86ab4a20551c34c744aa801d30601d9d7e2b147fb9a0d89
|
7
|
+
data.tar.gz: b8101d3196af1ffbb1f7faa578c3f665d76d102395331fcda4242b6b9e0e48cfa5d71976c91cc24f00ecdd3d0a91508733eb4be58b89e587b32df799d5f2f70a
|
data/.travis.yml
ADDED
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -1,41 +1,25 @@
|
|
1
|
-
# Orchestra
|
1
|
+
# Orchestra ![Build Status](https://travis-ci.org/ntl/orchestra.svg?branch=master)
|
2
2
|
|
3
3
|
Seamlessly chain multiple command or query objects together with a simple, lightweight framework.
|
4
4
|
|
5
|
-
## Installation
|
6
|
-
|
7
|
-
Add this line to your application's Gemfile:
|
8
|
-
|
9
|
-
```ruby
|
10
|
-
gem 'ntl-orchestra'
|
11
|
-
```
|
12
|
-
|
13
|
-
And then execute:
|
14
|
-
|
15
|
-
$ bundle
|
16
|
-
|
17
|
-
Or install it yourself as:
|
18
|
-
|
19
|
-
$ gem install ntl-orchestra
|
20
|
-
|
21
5
|
## Usage
|
22
6
|
|
23
7
|
Here's a simple example without a lot of context:
|
24
8
|
|
25
9
|
```ruby
|
26
10
|
operation = Orchestra::Operation.new do
|
27
|
-
|
11
|
+
step :make_array do
|
28
12
|
depends_on :up_to
|
29
13
|
provides :array
|
30
|
-
|
14
|
+
execute do
|
31
15
|
limit.times.to_a
|
32
16
|
end
|
33
17
|
end
|
34
18
|
|
35
|
-
|
19
|
+
step :apply_fizzbuzz do
|
36
20
|
iterates_over :array
|
37
21
|
provides :fizzbuzz
|
38
|
-
|
22
|
+
execute do |num|
|
39
23
|
next if num == 0 # filter 0 from the output
|
40
24
|
str = ''
|
41
25
|
str << "Fizz" if num.mod 3 == 0
|
@@ -47,13 +31,13 @@ operation = Orchestra::Operation.new do
|
|
47
31
|
|
48
32
|
finally do
|
49
33
|
iterates_over :fizzbuzz
|
50
|
-
|
34
|
+
execute do |str|
|
51
35
|
puts str
|
52
36
|
end
|
53
37
|
end
|
54
38
|
end
|
55
39
|
|
56
|
-
Orchestra.
|
40
|
+
Orchestra.execute operation, :up_to => 31
|
57
41
|
```
|
58
42
|
|
59
43
|
There is an easy way to take this gem for a test drive. Clone the repo, and at the project root:
|
@@ -81,8 +65,8 @@ Finished in 0.379958s, 126.3298 runs/s, 1526.4845 assertions/s.
|
|
81
65
|
Also, you can access the examples:
|
82
66
|
|
83
67
|
```ruby
|
84
|
-
[1] pry(Orchestra)> Orchestra.
|
85
|
-
[1] pry(Orchestra)> Orchestra.
|
68
|
+
[1] pry(Orchestra)> Orchestra.execute FizzBuzz, :up_to => 31
|
69
|
+
[1] pry(Orchestra)> Orchestra.execute InvitationService, :account_name => 'realntl`
|
86
70
|
```
|
87
71
|
|
88
72
|
## Why?
|
@@ -155,25 +139,25 @@ InvitationService = Orchestra::Operation.new do
|
|
155
139
|
DEFAULT_MESSAGE = "I would really love for you to try out MyApp."
|
156
140
|
ROBOT_FOLLOWER_THRESHHOLD = 500
|
157
141
|
|
158
|
-
|
142
|
+
step :fetch_followers do
|
159
143
|
depends_on :account_name
|
160
144
|
provides :followers
|
161
|
-
|
145
|
+
execute do
|
162
146
|
FlutterAPI.get_account account_name
|
163
147
|
end
|
164
148
|
end
|
165
149
|
|
166
|
-
|
150
|
+
step :fetch_blacklist do
|
167
151
|
provides :blacklist
|
168
|
-
|
152
|
+
execute do
|
169
153
|
Blacklist.pluck :flutter_account_name
|
170
154
|
end
|
171
155
|
end
|
172
156
|
|
173
|
-
|
157
|
+
step :remove_blacklisted_followers do
|
174
158
|
depends_on :blacklist
|
175
159
|
modifies :followers
|
176
|
-
|
160
|
+
execute do
|
177
161
|
followers.reject! do |follower|
|
178
162
|
account_name = follower.fetch 'username'
|
179
163
|
blacklist.include? account_name
|
@@ -181,9 +165,9 @@ InvitationService = Orchestra::Operation.new do
|
|
181
165
|
end
|
182
166
|
end
|
183
167
|
|
184
|
-
|
168
|
+
step :filter_robots do
|
185
169
|
modifies :followers, :collection => true
|
186
|
-
|
170
|
+
execute do |follower|
|
187
171
|
account_name = follower.fetch 'username'
|
188
172
|
account = FlutterAPI.get_account account_name
|
189
173
|
next unless account['following'] > ROBOT_FOLLOWER_THRESHHOLD
|
@@ -195,63 +179,63 @@ InvitationService = Orchestra::Operation.new do
|
|
195
179
|
finally :deliver_emails do
|
196
180
|
depends_on :message => DEFAULT_MESSAGE
|
197
181
|
iterates_over :followers
|
198
|
-
|
182
|
+
execute do |follower|
|
199
183
|
EmailDelivery.send message, :to => follower
|
200
184
|
end
|
201
185
|
end
|
202
186
|
end
|
203
187
|
```
|
204
188
|
|
205
|
-
At first sight, that very likely appears to be a giant pile of ruby DSL goop. And you would be correct. I'll show you how to plug in POROs in a bit, and there are some further improvements that will make the indirection worth while. For now, here is how you would
|
189
|
+
At first sight, that very likely appears to be a giant pile of ruby DSL goop. And you would be correct. I'll show you how to plug in POROs in a bit, and there are some further improvements that will make the indirection worth while. For now, here is how you would execute this command:
|
206
190
|
|
207
191
|
```ruby
|
208
192
|
# Use the default message
|
209
|
-
Orchestra.
|
193
|
+
Orchestra.execute InvitationService, :account_name => 'realntl'
|
210
194
|
|
211
195
|
# Override the default message
|
212
|
-
Orchestra.
|
196
|
+
Orchestra.execute InvitationService, :account_name => 'realntl', :message => 'Say cheese!'
|
213
197
|
```
|
214
198
|
|
215
199
|
There's a lot more to learn, but let's start by building an understanding of the DSL.
|
216
200
|
|
217
|
-
## Breaking down the DSL:
|
201
|
+
## Breaking down the DSL: Steps
|
218
202
|
|
219
|
-
An *operation* is a collection of
|
203
|
+
An *operation* is a collection of steps that each individually take part in producing some larger behavior. A step, essentially, accepts input, processes, and provides output. Let's look at the first one, called `:fetch_followers`.
|
220
204
|
|
221
205
|
```ruby
|
222
|
-
|
206
|
+
step :fetch_followers do
|
223
207
|
depends_on :account_name
|
224
208
|
provides :followers
|
225
|
-
|
209
|
+
execute do
|
226
210
|
FlutterAPI.get_account account_name
|
227
211
|
end
|
228
212
|
end
|
229
213
|
```
|
230
214
|
|
231
|
-
This node depends on something called an `account_name`. This means you must supply `:account_name` when you
|
215
|
+
This node depends on something called an `account_name`. This means you must supply `:account_name` when you execute the operation, otherwise, `:fetch_followers` won't work. `orchestra` ensures that your invokation can't commence without the required input:
|
232
216
|
|
233
217
|
```ruby
|
234
|
-
|
235
|
-
operation.
|
218
|
+
# Raises Orchestra::MissingInputError: Missing input :account_name
|
219
|
+
operation.execute
|
236
220
|
# Works correctly
|
237
|
-
operation.
|
221
|
+
operation.execute :account_name => 'realntl'
|
238
222
|
```
|
239
223
|
|
240
224
|
Dependencies can be optional. In the above example, `deliver_emails` defaults the `:message` input to `DEFAULT_MESSAGE`.
|
241
225
|
|
242
|
-
Often, you will need the output of one operation to feed into the input of another. The annotations `depends_on`, `iterates_over`, and `modifies` all describe the inputs, and `provides` describes the output. When `provides` is omitted, the name of the output is set to match the name of the
|
226
|
+
Often, you will need the output of one operation to feed into the input of another. The annotations `depends_on`, `iterates_over`, and `modifies` all describe the inputs, and `provides` describes the output. When `provides` is omitted, the name of the output is set to match the name of the step. Orchestra actually uses the annotations to sort the ordering of the steps at runtime to ensure that all dependencies are satisfied. In the above example, `remove_blacklisted_followers` would not execute before `fetch_blacklist`, no matter where their definitions were placed within the operation. Orchestra detects that `remove_blacklisted_followers` *depends on* `blacklist`, and `fetch_blacklist` actually provides a `blacklist`, so it knows to run `fetch_blacklist` before `remove_blacklisted_followers`. Similarly, if you had your own list of blacklisted account names lying around, you could bypass the `fetch_blacklist` step altogether, since there is no sense fetching a `blacklist` when you've already got one:
|
243
227
|
|
244
228
|
```ruby
|
245
229
|
# Never invokes fetch_blacklist
|
246
|
-
operation.
|
230
|
+
operation.execute :account_name => 'realntl', :blacklist => %w(dhh unclebobmartin)
|
247
231
|
```
|
248
232
|
|
249
233
|
This allows your operations to be reused in cases where some of the dependencies can already be satisfied.
|
250
234
|
|
251
|
-
Finally, the `modifies` annotiation deserves some explaining. When a
|
235
|
+
Finally, the `modifies` annotiation deserves some explaining. When a step merely mutates an input, you are certainly welcome to declare distinct `depends_on` and `modifies` annotations:
|
252
236
|
|
253
237
|
```ruby
|
254
|
-
|
238
|
+
step :remove_blacklisted_followers do
|
255
239
|
depends_on :blacklist, :followers
|
256
240
|
provides :followers
|
257
241
|
end
|
@@ -260,7 +244,7 @@ end
|
|
260
244
|
However, `modifies` simply condenses the two into one. The following example is identical to the previous:
|
261
245
|
|
262
246
|
```ruby
|
263
|
-
|
247
|
+
step :remove_blacklisted_followers do
|
264
248
|
depends_on :blacklist
|
265
249
|
modifies :followers
|
266
250
|
end
|
@@ -268,16 +252,16 @@ end
|
|
268
252
|
|
269
253
|
## Breaking down the DSL: the operation itself
|
270
254
|
|
271
|
-
Configuring the operation is rather simple. You define the various
|
255
|
+
Configuring the operation is rather simple. You define the various steps, and then specify the result. There are three ways to specify the result.
|
272
256
|
|
273
257
|
The first is very straightforward:
|
274
258
|
|
275
259
|
```ruby
|
276
260
|
Orchestra::Operation.new do
|
277
|
-
|
261
|
+
step :foo do
|
278
262
|
depends_on :bar
|
279
|
-
provides :foo # optional, since the
|
280
|
-
|
263
|
+
provides :foo # optional, since the step is called :foo
|
264
|
+
execute do … end
|
281
265
|
end
|
282
266
|
|
283
267
|
self.result = :foo
|
@@ -288,38 +272,38 @@ The second is just a shortened form of the first:
|
|
288
272
|
|
289
273
|
```ruby
|
290
274
|
Orchestra::Operation.new do
|
291
|
-
# Define a
|
275
|
+
# Define a step called :foo and make it the result
|
292
276
|
result :foo do
|
293
277
|
depends_on :bar
|
294
|
-
|
278
|
+
execute do … end
|
295
279
|
end
|
296
280
|
end
|
297
281
|
```
|
298
282
|
|
299
|
-
The third is a minor variation of the second. The only difference is that the operation will always return `true`. `finally` makes sense for operations that
|
283
|
+
The third is a minor variation of the second. The only difference is that the operation will always return `true`. `finally` makes sense for operations that execute side effects (e.g. Command objects), whereas `result` will make sense for queries.
|
300
284
|
|
301
285
|
```ruby
|
302
286
|
Orchestra::Operation.new do
|
303
287
|
finally :foo do
|
304
288
|
depends_on :bar
|
305
|
-
|
289
|
+
execute do … end
|
306
290
|
end
|
307
291
|
end
|
308
292
|
```
|
309
293
|
|
310
294
|
## Hooking in POROs
|
311
295
|
|
312
|
-
You can also hook up POROs to operations as
|
296
|
+
You can also hook up POROs to operations as steps. This is important both to manage complex steps as well as leveraging existing objects in the system. The `filter_robots` step could be expressed as a PORO rather easily:
|
313
297
|
|
314
298
|
```ruby
|
315
|
-
|
299
|
+
step FilterRobots, :iterates_over => :followers, :collection => true
|
316
300
|
|
317
301
|
class FilterRobots
|
318
302
|
def initialize followers
|
319
303
|
@followers = followers
|
320
304
|
end
|
321
305
|
|
322
|
-
def
|
306
|
+
def execute follower
|
323
307
|
account_name = follower.fetch 'account_name'
|
324
308
|
account = FlutterAPI.get_account account_name
|
325
309
|
next unless account['following'] > ROBOT_FOLLOWER_THRESHHOLD
|
@@ -329,16 +313,16 @@ class FilterRobots
|
|
329
313
|
end
|
330
314
|
```
|
331
315
|
|
332
|
-
Orchestra infers the dependencies from `FilterRobots#initialize`, and automatically instantiates the object for you during
|
316
|
+
Orchestra infers the dependencies from `FilterRobots#initialize`, and automatically instantiates the object for you during execution. You can alter the name of the method:
|
333
317
|
|
334
318
|
```ruby
|
335
|
-
|
319
|
+
step MyPoro, :method => :call
|
336
320
|
```
|
337
321
|
|
338
|
-
You can also hook into singletons like `Module` (or a `Class` that implements `self.
|
322
|
+
You can also hook into singletons like `Module` (or a `Class` that implements `self.execute`):
|
339
323
|
|
340
324
|
```ruby
|
341
|
-
|
325
|
+
step MySingleton, :method => :invoke
|
342
326
|
|
343
327
|
module MySingleton
|
344
328
|
def self.invoke … end
|
@@ -349,9 +333,9 @@ By default, the name of the provision will be inferred from the object name.
|
|
349
333
|
|
350
334
|
## Multithreading
|
351
335
|
|
352
|
-
Two of the
|
336
|
+
Two of the steps in the `InvitationService` orchestration -- `filter_robots` and `deliver_emails` -- actually operate on *collections*. `deliver_emails` indicates that `:followers` is a collection by using the `iterates_over` annotation instead of `depends_on`. In fact, the two annotations are identical *except* that `iterates_over` indicates that the dependency is in fact going to be a list. Collections can be defined on a `modifies` annotation, as well, by supplying `:collection => true` as in the case of `filter_robots`.
|
353
337
|
|
354
|
-
When
|
338
|
+
When steps iterate over collections, Orchestra invokes `execute do … end` block once for each item in the collection passed in. It also spreads out each invokation across a thread pool. By default, there is only one thread in the thread pool. You can reconfigure that globally in an initializer of some kind:
|
355
339
|
|
356
340
|
```ruby
|
357
341
|
Orchestra.configure do
|
@@ -360,15 +344,15 @@ Orchestra.configure do
|
|
360
344
|
end
|
361
345
|
```
|
362
346
|
|
363
|
-
These collections can operate as filters; the output list is a mapping of the input list *transformed* by the `
|
347
|
+
These collections can operate as filters; the output list is a mapping of the input list *transformed* by the `execute` block. When the `execute` block returns `nil`, the output shrinks by one element. Consider the FizzBuzz example at the top of this document. Notice that `0` doesn't get printed out. This is because the `execute` block in `apply_fizzbuzz` returned nil when the `num` was zero. `nil` values get `compact`'ed.
|
364
348
|
|
365
349
|
## Invoking an operation through a conductor
|
366
350
|
|
367
|
-
Now that you understand how to define operations, we can do some cool things with them. First, though, we need to change the way we invoke operations. Let's instantiate a `Conductor`, and have *that*
|
351
|
+
Now that you understand how to define operations, we can do some cool things with them. First, though, we need to change the way we invoke operations. Let's instantiate a `Conductor`, and have *that* execute our operations for us:
|
368
352
|
|
369
353
|
```ruby
|
370
354
|
conductor = Orchestra::Conductor.new
|
371
|
-
conductor.
|
355
|
+
conductor.execute InvitationService, :account_name => 'realntl'
|
372
356
|
```
|
373
357
|
|
374
358
|
What did that buy us? First, we can configure the size of the thread pool specifically for this conductor:
|
@@ -380,20 +364,20 @@ conductor.thread_count = 5
|
|
380
364
|
Second, we can inject *services* into our operation. Our operation needs to be modified such that our database connections and API access are passed in as dependencies:
|
381
365
|
|
382
366
|
```ruby
|
383
|
-
|
367
|
+
step :fetch_followers do
|
384
368
|
depends_on :account_name, :flutter_api
|
385
369
|
provides :followers
|
386
|
-
|
370
|
+
execute do
|
387
371
|
flutter_api.get_account account_name
|
388
372
|
end
|
389
373
|
end
|
390
374
|
|
391
375
|
# and
|
392
376
|
|
393
|
-
|
377
|
+
step :fetch_blacklist do
|
394
378
|
depends_on :blacklist_table
|
395
379
|
provides :blacklist
|
396
|
-
|
380
|
+
execute do
|
397
381
|
blacklist_table.pluck :flutter_account_name
|
398
382
|
end
|
399
383
|
end
|
@@ -408,21 +392,21 @@ conductor = Orchestra::Conductor.new(
|
|
408
392
|
)
|
409
393
|
```
|
410
394
|
|
411
|
-
We can also override the conductor's service registry by supplying them into the
|
395
|
+
We can also override the conductor's service registry by supplying them into the execution itself, as we do any other dependency like `account_name`:
|
412
396
|
|
413
397
|
```ruby
|
414
|
-
conductor.
|
398
|
+
conductor.execute InvitationService, :account_name => 'realntl', :blacklist_table => mock
|
415
399
|
```
|
416
400
|
|
417
|
-
What did this buy us? Two big things. We can now attach *observers* to the
|
401
|
+
What did this buy us? Two big things. We can now attach *observers* to the execution, and we can actually record all calls in and out of the `flutter_api` and `blacklist_table` services. The former allows us to share the internal operation of the execution with the rest of the system without breaking encapsulation, and the latter allows us to actually replay the operation against recordings of live performances.
|
418
402
|
|
419
|
-
Additionally, you can pass the `conductor` into
|
403
|
+
Additionally, you can pass the `conductor` into steps. In this way you can embed one operation inside another:
|
420
404
|
|
421
405
|
```ruby
|
422
406
|
inner_operation = Orchestra::Operation.new do
|
423
407
|
result :foo do
|
424
408
|
provides :bar
|
425
|
-
|
409
|
+
execute do
|
426
410
|
bar * 2
|
427
411
|
end
|
428
412
|
end
|
@@ -432,14 +416,14 @@ outer_operation = Orchestra::Operation.new do
|
|
432
416
|
result :baz do
|
433
417
|
depends_on :conductor
|
434
418
|
provides :qux
|
435
|
-
|
436
|
-
conductor.
|
419
|
+
execute do
|
420
|
+
conductor.execute inner_operation
|
437
421
|
end
|
438
422
|
end
|
439
423
|
end
|
440
424
|
|
441
425
|
conductor = Conductor.new
|
442
|
-
conductor.
|
426
|
+
conductor.execute outer_operation
|
443
427
|
```
|
444
428
|
|
445
429
|
To shorten this, the inner operation can be "mounted" inside the outer operation:
|
@@ -448,7 +432,7 @@ To shorten this, the inner operation can be "mounted" inside the outer operation
|
|
448
432
|
inner_operation = Orchestra::Operation.new do
|
449
433
|
result :foo do
|
450
434
|
provides :bar
|
451
|
-
|
435
|
+
execute do
|
452
436
|
bar * 2
|
453
437
|
end
|
454
438
|
end
|
@@ -471,9 +455,10 @@ class MyObserver
|
|
471
455
|
case event_name
|
472
456
|
when :operation_entered then "Hello"
|
473
457
|
when :operation_exited then "World!"
|
474
|
-
when :
|
475
|
-
when :
|
458
|
+
when :step_entered then "Hello from within a step"
|
459
|
+
when :step_exited then "Goodbye from within a step"
|
476
460
|
when :error_raised then "Ruh roh!"
|
461
|
+
when :service_accessed then "Yay, service call"
|
477
462
|
end
|
478
463
|
end
|
479
464
|
end
|
@@ -485,17 +470,18 @@ The arguments passed to `update` will vary based on the event:
|
|
485
470
|
| ------------------------ | ------------------------------------ | --------------------------------- |
|
486
471
|
| `:operation_entered` | The name of the operation starting | Input going into the operation |
|
487
472
|
| `:operation_exited` | The name of the operation finishing | Output of the operation |
|
488
|
-
| `:
|
489
|
-
| `:
|
473
|
+
| `:step_entered` | The name of the step | Input going into the step |
|
474
|
+
| `:step_exited` | The name of the step | Output of the step |
|
490
475
|
| `:error_raised` | The error itself | `nil` |
|
476
|
+
| `:service_accessed` | The name of the service | Recording of the service call |
|
491
477
|
|
492
|
-
|
478
|
+
All observers attached to the execution of the outer operation will also attach to the inner operation.
|
493
479
|
|
494
480
|
## Recording and playing back services
|
495
481
|
|
496
|
-
The final main feature of Orchestra is the ability to record the service calls throughout an operation. These recordings can then be used to replay operations. This could be helpful, for instance, to attach to exceptions in your exception logging service so that programmers can replay failed
|
482
|
+
The final main feature of Orchestra is the ability to record the service calls throughout an operation. These recordings can then be used to replay operations. This could be helpful, for instance, to attach to exceptions in your exception logging service so that programmers can replay failed executions on their development environments. In addition, these recordings could be used to drive integration testing. Thus, instead of using separate tools such as ActiveRecord fixtures, FactoryGirl, and VCR for every service dependency, you can test your operations with one single setup artifact.
|
497
483
|
|
498
|
-
You can record a performance on any `Conductor` by calling `#record` instead of `#
|
484
|
+
You can record a performance put on by any `Conductor` by calling `#record` instead of `#execute`:
|
499
485
|
|
500
486
|
```ruby
|
501
487
|
recording = conductor.record InvitationService, :account_name => 'realntl'
|
@@ -529,6 +515,21 @@ recording = JSON.load json
|
|
529
515
|
Orchestra.replay_recording InvitationService, recording
|
530
516
|
```
|
531
517
|
|
518
|
+
## Installation
|
519
|
+
|
520
|
+
Add this line to your application's Gemfile:
|
521
|
+
|
522
|
+
```ruby
|
523
|
+
gem 'ntl-orchestra'
|
524
|
+
```
|
525
|
+
|
526
|
+
And then execute:
|
527
|
+
|
528
|
+
$ bundle
|
529
|
+
|
530
|
+
Or install it yourself as:
|
531
|
+
|
532
|
+
$ gem install ntl-orchestra
|
532
533
|
|
533
534
|
## Contributing
|
534
535
|
|
data/lib/orchestra/conductor.rb
CHANGED
@@ -9,18 +9,18 @@ module Orchestra
|
|
9
9
|
self.thread_count = Configuration.thread_count
|
10
10
|
end
|
11
11
|
|
12
|
-
def
|
13
|
-
operation.
|
14
|
-
copy_observers
|
15
|
-
yield
|
12
|
+
def execute operation, input = {}
|
13
|
+
operation.execute self, input do |execution|
|
14
|
+
copy_observers execution
|
15
|
+
yield execution if block_given?
|
16
16
|
end
|
17
17
|
end
|
18
18
|
|
19
19
|
def record *args
|
20
20
|
recording = Recording.new
|
21
21
|
add_observer recording
|
22
|
-
|
23
|
-
|
22
|
+
execute *args do |execution|
|
23
|
+
execution.add_observer recording
|
24
24
|
end
|
25
25
|
recording
|
26
26
|
ensure
|
@@ -1,12 +1,12 @@
|
|
1
1
|
module Orchestra
|
2
2
|
module DSL
|
3
3
|
class ObjectAdapter
|
4
|
-
def self.
|
5
|
-
method_name = args.delete :method do :
|
4
|
+
def self.build_step object, args = {}
|
5
|
+
method_name = args.delete :method do :execute end
|
6
6
|
collection = args.delete :iterates_over
|
7
7
|
adapter_type = determine_type object, method_name
|
8
8
|
adapter = adapter_type.new object, method_name, collection
|
9
|
-
|
9
|
+
StepFactory.build adapter, args
|
10
10
|
end
|
11
11
|
|
12
12
|
def self.determine_type object, method_name
|
@@ -23,12 +23,12 @@ module Orchestra
|
|
23
23
|
|
24
24
|
def initialize object, method_name, collection
|
25
25
|
@collection = collection
|
26
|
-
@method_name = method_name || :
|
26
|
+
@method_name = method_name || :execute
|
27
27
|
@object = object
|
28
28
|
end
|
29
29
|
|
30
30
|
def build_context state
|
31
|
-
|
31
|
+
ObjectContext.new self, state
|
32
32
|
end
|
33
33
|
|
34
34
|
def collection?
|
@@ -36,7 +36,7 @@ module Orchestra
|
|
36
36
|
end
|
37
37
|
|
38
38
|
def context_class
|
39
|
-
@context_class ||=
|
39
|
+
@context_class ||= Step.build_execution_context_class dependencies
|
40
40
|
end
|
41
41
|
|
42
42
|
def dependencies
|
@@ -56,7 +56,7 @@ module Orchestra
|
|
56
56
|
end
|
57
57
|
end
|
58
58
|
|
59
|
-
def
|
59
|
+
def execute state
|
60
60
|
deps = object_method.dependencies
|
61
61
|
input = state.select do |key, _| deps.include? key end
|
62
62
|
Invokr.invoke :method => method_name, :on => object, :with => input
|
@@ -74,7 +74,7 @@ module Orchestra
|
|
74
74
|
"#{object} does not implement instance method `#{method_name}'"
|
75
75
|
end
|
76
76
|
|
77
|
-
def
|
77
|
+
def execute state, maybe_item = nil
|
78
78
|
instance = Invokr.inject object, :using => state
|
79
79
|
args = [method_name]
|
80
80
|
args << maybe_item if collection?
|
@@ -86,10 +86,10 @@ module Orchestra
|
|
86
86
|
end
|
87
87
|
end
|
88
88
|
|
89
|
-
class
|
89
|
+
class StepFactory
|
90
90
|
def self.build *args
|
91
91
|
instance = new *args
|
92
|
-
instance.
|
92
|
+
instance.build_step
|
93
93
|
end
|
94
94
|
|
95
95
|
attr :adapter, :compact, :provides, :thread_count
|
@@ -100,12 +100,12 @@ module Orchestra
|
|
100
100
|
:provides => nil, :compact => false, :thread_count => nil
|
101
101
|
end
|
102
102
|
|
103
|
-
def
|
103
|
+
def build_step
|
104
104
|
adapter.validate!
|
105
|
-
|
105
|
+
Step::ObjectStep.new adapter, build_step_args
|
106
106
|
end
|
107
107
|
|
108
|
-
def
|
108
|
+
def build_step_args
|
109
109
|
hsh = {
|
110
110
|
:dependencies => adapter.dependencies,
|
111
111
|
:provides => Array(provides),
|
@@ -115,7 +115,7 @@ module Orchestra
|
|
115
115
|
end
|
116
116
|
end
|
117
117
|
|
118
|
-
class
|
118
|
+
class ObjectContext
|
119
119
|
def initialize adapter, state
|
120
120
|
@__adapter__ = adapter
|
121
121
|
@__state__ = state
|
@@ -125,8 +125,8 @@ module Orchestra
|
|
125
125
|
end
|
126
126
|
end
|
127
127
|
|
128
|
-
def
|
129
|
-
@__adapter__.
|
128
|
+
def execute *args
|
129
|
+
@__adapter__.execute @__state__, *args
|
130
130
|
end
|
131
131
|
end
|
132
132
|
|