empipelines 0.2.3 → 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/README.md +13 -2
- data/empipelines.gemspec +5 -3
- data/functional/consuming_events_from_amqp_spec.rb +47 -0
- data/functional/{consuming_events_from_queue_spec.rb → consuming_events_from_batch_spec.rb} +4 -5
- data/functional/consuming_events_from_file_spec.rb +3 -4
- data/functional/consuming_events_from_multiple_sources_spec.rb +3 -4
- data/functional/test_stages.rb +1 -1
- data/lib/empipelines/aggregated_event_source.rb +4 -3
- data/lib/empipelines/amqp_event_source.rb +4 -2
- data/lib/empipelines/pipeline.rb +3 -5
- data/lib/empipelines.rb +1 -1
- data/unit/empipelines/amqp_event_source_spec.rb +42 -4
- data/unit/empipelines/pipeline_spec.rb +6 -7
- metadata +6 -4
data/README.md
CHANGED
@@ -1,11 +1,22 @@
|
|
1
|
-
#
|
1
|
+
# EmPipelines
|
2
|
+
|
3
|
+
## Message States
|
4
|
+
|
5
|
+
* **Consumed:** The message went through all relevant stages and was
|
6
|
+
fully consumed.
|
7
|
+
* **Broken:** The message *left the EventSource* in an invalid
|
8
|
+
state and should not be processed. If the message was corrupted by a
|
9
|
+
previous stage it should not be broken, just rejected.
|
10
|
+
* **Rejected:** The message is correct but this pipeline instance
|
11
|
+
cannot process it, probably due to some temporary problem.
|
2
12
|
|
3
13
|
## TODO
|
4
14
|
* Make wiring easier
|
5
15
|
* Example apps
|
6
|
-
* Control flow for AmqpEventSource
|
7
16
|
* Evented I/O for IOEventSource
|
8
17
|
* Consolidate logger and monitoring
|
9
18
|
* Default monitoring implementation
|
10
19
|
* Performance testing
|
11
20
|
* Detect insonsistency when handler didnt consume or pass message ahead
|
21
|
+
* Is callback in the stage's signature redundant? Could we just return
|
22
|
+
the message?
|
data/empipelines.gemspec
CHANGED
@@ -4,8 +4,8 @@ Gem::Specification.new do |s|
|
|
4
4
|
s.rubygems_version = '1.3.5'
|
5
5
|
## Leave these as is they will be modified for you by the rake gemspec task.
|
6
6
|
s.name = 'empipelines'
|
7
|
-
s.version = '0.2.
|
8
|
-
s.date = '2012-01-
|
7
|
+
s.version = '0.2.4'
|
8
|
+
s.date = '2012-01-10'
|
9
9
|
s.rubyforge_project = 'empipelines'
|
10
10
|
|
11
11
|
s.summary = "Simple Event Handling Pipeline Architecture for EventMachine"
|
@@ -35,10 +35,12 @@ Gem::Specification.new do |s|
|
|
35
35
|
Gemfile.lock
|
36
36
|
README.md
|
37
37
|
Rakefile
|
38
|
+
empipelines-0.2.4.gem
|
38
39
|
empipelines.gemspec
|
40
|
+
functional/consuming_events_from_amqp_spec.rb
|
41
|
+
functional/consuming_events_from_batch_spec.rb
|
39
42
|
functional/consuming_events_from_file_spec.rb
|
40
43
|
functional/consuming_events_from_multiple_sources_spec.rb
|
41
|
-
functional/consuming_events_from_queue_spec.rb
|
42
44
|
functional/events.dat
|
43
45
|
functional/test_stages.rb
|
44
46
|
lib/empipelines.rb
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'eventmachine'
|
2
|
+
require 'empipelines'
|
3
|
+
require 'amqp'
|
4
|
+
require 'json'
|
5
|
+
require File.join(File.dirname(__FILE__), 'test_stages')
|
6
|
+
|
7
|
+
module TestStages
|
8
|
+
ExchangeName = 'empipelines.build'
|
9
|
+
QueueName = 'empipelines.build.queue'
|
10
|
+
|
11
|
+
describe 'Consumption of events from a in-memory batch' do
|
12
|
+
let(:monitoring) { stub(:inform => nil, :debug => nil) }
|
13
|
+
let (:processed) { [] }
|
14
|
+
|
15
|
+
include EmRunner
|
16
|
+
|
17
|
+
it 'consumes all events from a queue' do
|
18
|
+
with_em_run do
|
19
|
+
connection = AMQP.connect()
|
20
|
+
channel = AMQP::Channel.new(connection)
|
21
|
+
channel.prefetch(1)
|
22
|
+
|
23
|
+
exchange = channel.direct(ExchangeName, :durable => true)
|
24
|
+
queue = channel.queue(QueueName, :durable => true)
|
25
|
+
queue.bind(exchange)
|
26
|
+
queue.purge
|
27
|
+
|
28
|
+
messages = (1..1000).map { |i| {:data => i}.to_json }
|
29
|
+
messages.each { |m| exchange.publish(m, :rounting_key => QueueName) }
|
30
|
+
|
31
|
+
pipeline = EmPipelines::Pipeline.new(EM, {:processed => processed}, monitoring)
|
32
|
+
source = EmPipelines::AmqpEventSource.new(EM, queue, 'msg')
|
33
|
+
|
34
|
+
stages = [PassthroughStage, PassthroughStage, PassthroughStage]
|
35
|
+
event_pipeline = EmPipelines::EventPipeline.new(source, pipeline.for(stages), monitoring)
|
36
|
+
|
37
|
+
EM.add_periodic_timer(0.1) do
|
38
|
+
if processed.size == messages.size * stages.size then
|
39
|
+
EM.stop
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
event_pipeline.start!
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -3,15 +3,14 @@ require 'empipelines'
|
|
3
3
|
require File.join(File.dirname(__FILE__), 'test_stages')
|
4
4
|
|
5
5
|
module TestStages
|
6
|
-
describe 'Consumption of events from a
|
7
|
-
let(:monitoring) { stub() }
|
8
|
-
let(:
|
9
|
-
let (:processed) { {} }
|
6
|
+
describe 'Consumption of events from a in-memory batch' do
|
7
|
+
let(:monitoring) { stub(:inform => nil, :debug => nil) }
|
8
|
+
let (:processed) { [] }
|
10
9
|
include EmRunner
|
11
10
|
|
12
11
|
it 'consumes all events from the a queue' do
|
13
12
|
with_em_run do
|
14
|
-
pipeline = EmPipelines::Pipeline.new(EM, {:processed => processed}, monitoring
|
13
|
+
pipeline = EmPipelines::Pipeline.new(EM, {:processed => processed}, monitoring)
|
15
14
|
|
16
15
|
batch = (1...1000).to_a
|
17
16
|
batch_name = "my batch!"
|
@@ -4,14 +4,13 @@ require File.join(File.dirname(__FILE__), 'test_stages')
|
|
4
4
|
|
5
5
|
module TestStages
|
6
6
|
describe 'Consumption of events from a file' do
|
7
|
-
let(:monitoring) { stub() }
|
8
|
-
let(:
|
9
|
-
let (:processed) { {} }
|
7
|
+
let(:monitoring) { stub(:inform => nil, :debug => nil) }
|
8
|
+
let (:processed) { [] }
|
10
9
|
include EmRunner
|
11
10
|
|
12
11
|
it 'consumes all events from the file' do
|
13
12
|
with_em_run do
|
14
|
-
pipeline = EmPipelines::Pipeline.new(EM, {:processed => processed}, monitoring
|
13
|
+
pipeline = EmPipelines::Pipeline.new(EM, {:processed => processed}, monitoring)
|
15
14
|
|
16
15
|
file_name = File.join(File.dirname(__FILE__), 'events.dat')
|
17
16
|
source = EmPipelines::IOEventSource.new(EM, file_name)
|
@@ -4,13 +4,12 @@ require File.join(File.dirname(__FILE__), 'test_stages')
|
|
4
4
|
|
5
5
|
module TestStages
|
6
6
|
describe 'Consumption of events from multiple sources' do
|
7
|
-
let(:monitoring) { stub() }
|
8
|
-
let(:
|
9
|
-
let (:processed) { {} }
|
7
|
+
let(:monitoring) { stub(:inform => nil, :debug => nil) }
|
8
|
+
let (:processed) { [] }
|
10
9
|
include EmRunner
|
11
10
|
|
12
11
|
it 'consumes all events from all sources' do
|
13
|
-
pipeline = EmPipelines::Pipeline.new(EM, {:processed => processed}, monitoring
|
12
|
+
pipeline = EmPipelines::Pipeline.new(EM, {:processed => processed}, monitoring)
|
14
13
|
|
15
14
|
file_name = File.join(File.dirname(__FILE__), 'events.dat')
|
16
15
|
num_events_on_file = IO.readlines(file_name).size
|
data/functional/test_stages.rb
CHANGED
@@ -2,7 +2,7 @@ require 'empipelines/event_source'
|
|
2
2
|
|
3
3
|
module EmPipelines
|
4
4
|
class AggregatedEventSource < EventSource
|
5
|
-
|
5
|
+
|
6
6
|
def initialize(em, *event_sources)
|
7
7
|
@em, @sources = em, event_sources.flatten
|
8
8
|
end
|
@@ -11,13 +11,14 @@ module EmPipelines
|
|
11
11
|
finished = 0
|
12
12
|
@sources.each do |s|
|
13
13
|
s.on_event(event_handler)
|
14
|
-
|
14
|
+
|
15
15
|
s.on_finished do |*ignored|
|
16
16
|
finished += 1
|
17
17
|
finished! if finished == @sources.size
|
18
18
|
end
|
19
|
+
|
19
20
|
@em.next_tick { s.start! }
|
20
|
-
end
|
21
|
+
end
|
21
22
|
end
|
22
23
|
end
|
23
24
|
end
|
@@ -12,14 +12,16 @@ module EmPipelines
|
|
12
12
|
end
|
13
13
|
|
14
14
|
def start!
|
15
|
-
@queue.subscribe do |header, json_payload|
|
15
|
+
@queue.subscribe(:ack => true) do |header, json_payload|
|
16
16
|
message = Message.new({
|
17
|
-
:header => header,
|
18
17
|
:origin => @queue.name,
|
19
18
|
:payload => JSON.parse(json_payload),
|
20
19
|
:event => @event_name,
|
21
20
|
:started_at => Time.now.to_i
|
22
21
|
})
|
22
|
+
message.on_consumed { |m| header.ack }
|
23
|
+
message.on_broken { |m| header.reject(:requeue => true) }
|
24
|
+
message.on_rejected { |m| header.reject(:requeue => true) }
|
23
25
|
event!(message)
|
24
26
|
end
|
25
27
|
end
|
data/lib/empipelines/pipeline.rb
CHANGED
@@ -6,9 +6,8 @@ module EmPipelines
|
|
6
6
|
end
|
7
7
|
end
|
8
8
|
|
9
|
-
def initialize(em, context, monitoring
|
9
|
+
def initialize(em, context, monitoring)
|
10
10
|
@em = em
|
11
|
-
@logger = logger
|
12
11
|
@context = context
|
13
12
|
@monitoring = monitoring
|
14
13
|
end
|
@@ -17,12 +16,11 @@ module EmPipelines
|
|
17
16
|
stages = event_definition.map(&instantiate_with_dependencies)
|
18
17
|
|
19
18
|
monitoring = @monitoring
|
20
|
-
logger = @logger
|
21
19
|
|
22
20
|
first_stage_process = stages.reverse.reduce(TerminatorStage) do |current_head, next_stage|
|
23
21
|
@em.spawn do |input|
|
24
22
|
begin
|
25
|
-
|
23
|
+
monitoring.debug "#{next_stage.class}#notify with #{input}}"
|
26
24
|
next_stage.call(input) do |output|
|
27
25
|
current_head.notify(output)
|
28
26
|
end
|
@@ -32,7 +30,7 @@ module EmPipelines
|
|
32
30
|
end
|
33
31
|
end
|
34
32
|
|
35
|
-
@
|
33
|
+
@monitoring.inform "Pipeline for event_definition is: #{stages.map(&:class).join('->')}"
|
36
34
|
first_stage_process
|
37
35
|
end
|
38
36
|
|
data/lib/empipelines.rb
CHANGED
@@ -3,7 +3,7 @@ require 'empipelines/amqp_event_source'
|
|
3
3
|
module EmPipelines
|
4
4
|
class StubQueue
|
5
5
|
attr_accessor :name
|
6
|
-
def subscribe(&code)
|
6
|
+
def subscribe(ack, &code)
|
7
7
|
@code = code
|
8
8
|
end
|
9
9
|
|
@@ -11,7 +11,7 @@ module EmPipelines
|
|
11
11
|
@code.call(header, payload)
|
12
12
|
end
|
13
13
|
end
|
14
|
-
|
14
|
+
|
15
15
|
describe AmqpEventSource do
|
16
16
|
let (:em) { mock('eventmachine') }
|
17
17
|
|
@@ -23,7 +23,7 @@ module EmPipelines
|
|
23
23
|
header = stub('header')
|
24
24
|
|
25
25
|
received_messages = []
|
26
|
-
|
26
|
+
|
27
27
|
amqp_source = AmqpEventSource.new(em, queue, event_type)
|
28
28
|
amqp_source.on_event { |e| received_messages << e }
|
29
29
|
amqp_source.start!
|
@@ -32,10 +32,48 @@ module EmPipelines
|
|
32
32
|
|
33
33
|
received_messages.size.should eql(1)
|
34
34
|
received_messages.first[:origin].should ==(queue.name)
|
35
|
-
received_messages.first[:header].should ==(header)
|
36
35
|
received_messages.first[:payload].should ==({:key => "value"})
|
37
36
|
received_messages.first[:event].should ==(event_type)
|
38
37
|
received_messages.first[:started_at].should_not be_nil
|
39
38
|
end
|
39
|
+
|
40
|
+
it 'acknowledges consumed messages' do
|
41
|
+
queue = StubQueue.new
|
42
|
+
header = mock('header')
|
43
|
+
|
44
|
+
header.should_receive(:ack)
|
45
|
+
|
46
|
+
amqp_source = AmqpEventSource.new(em, queue, 'event type')
|
47
|
+
amqp_source.on_event { |e| e.consumed! }
|
48
|
+
amqp_source.start!
|
49
|
+
|
50
|
+
queue.publish(header, '{"key":"value"}')
|
51
|
+
end
|
52
|
+
|
53
|
+
it 'rejects broken messages with no requeue' do
|
54
|
+
queue = StubQueue.new
|
55
|
+
header = mock('header')
|
56
|
+
|
57
|
+
header.should_receive(:reject)
|
58
|
+
|
59
|
+
amqp_source = AmqpEventSource.new(em, queue, 'event type')
|
60
|
+
amqp_source.on_event { |e| e.broken! }
|
61
|
+
amqp_source.start!
|
62
|
+
|
63
|
+
queue.publish(header, '{"key":"value"}')
|
64
|
+
end
|
65
|
+
|
66
|
+
it 'rejects rejected messages with requeue' do
|
67
|
+
queue = StubQueue.new
|
68
|
+
header = mock('header')
|
69
|
+
|
70
|
+
header.should_receive(:reject).with({:requeue => true})
|
71
|
+
|
72
|
+
amqp_source = AmqpEventSource.new(em, queue, 'event type')
|
73
|
+
amqp_source.on_event { |e| e.rejected! }
|
74
|
+
amqp_source.start!
|
75
|
+
|
76
|
+
queue.publish(header, '{"key":"value"}')
|
77
|
+
end
|
40
78
|
end
|
41
79
|
end
|
@@ -96,14 +96,14 @@ module EmPipelines
|
|
96
96
|
end
|
97
97
|
|
98
98
|
describe Pipeline do
|
99
|
-
let(:
|
99
|
+
let(:monitoring) { stub(:inform => nil, :debug => nil) }
|
100
100
|
|
101
101
|
it 'chains the actions using processes' do
|
102
102
|
event_chain = [AddOne, SquareIt, GlobalHolder]
|
103
103
|
a_msg = msg({:data =>1})
|
104
104
|
a_msg.should_receive(:consumed!)
|
105
105
|
|
106
|
-
pipelines = Pipeline.new(StubSpawner.new, {},
|
106
|
+
pipelines = Pipeline.new(StubSpawner.new, {}, monitoring)
|
107
107
|
pipeline = pipelines.for(event_chain)
|
108
108
|
pipeline.notify(a_msg)
|
109
109
|
|
@@ -112,7 +112,7 @@ module EmPipelines
|
|
112
112
|
|
113
113
|
it 'does not send to the next if last returned nil' do
|
114
114
|
event_chain = [AddOne, SquareIt, DeadEnd, GlobalHolder]
|
115
|
-
pipelines = Pipeline.new(StubSpawner.new, {},
|
115
|
+
pipelines = Pipeline.new(StubSpawner.new, {}, monitoring)
|
116
116
|
pipeline = pipelines.for(event_chain)
|
117
117
|
pipeline.notify(msg({:data => 1}))
|
118
118
|
GlobalHolder.held.should be_nil
|
@@ -120,7 +120,7 @@ module EmPipelines
|
|
120
120
|
|
121
121
|
it 'makes all objects in the context object available to stages' do
|
122
122
|
event_chain = [NeedsAnApple, NeedsAnOrange, GlobalHolder]
|
123
|
-
pipelines = Pipeline.new(StubSpawner.new, {:apple => :some_apple, :orange => :some_orange},
|
123
|
+
pipelines = Pipeline.new(StubSpawner.new, {:apple => :some_apple, :orange => :some_orange}, monitoring)
|
124
124
|
a_msg = msg({})
|
125
125
|
a_msg.should_receive(:consumed!)
|
126
126
|
|
@@ -132,15 +132,14 @@ module EmPipelines
|
|
132
132
|
end
|
133
133
|
|
134
134
|
it 'sends exception to the proper handler' do
|
135
|
-
monitoring = mock()
|
136
135
|
monitoring.should_receive(:inform_exception!)
|
137
|
-
pipeline = Pipeline.new(StubSpawner.new, {}, monitoring
|
136
|
+
pipeline = Pipeline.new(StubSpawner.new, {}, monitoring)
|
138
137
|
pipeline.for([BrokenStage]).notify(msg({}))
|
139
138
|
end
|
140
139
|
|
141
140
|
it 'flags the message as consumed if goest through all stages' do
|
142
141
|
event_chain = [Passthrough, Passthrough]
|
143
|
-
pipelines = Pipeline.new(StubSpawner.new, {},
|
142
|
+
pipelines = Pipeline.new(StubSpawner.new, {}, monitoring)
|
144
143
|
pipeline = pipelines.for(event_chain)
|
145
144
|
a_msg = msg({:data => :whatevah})
|
146
145
|
a_msg.should_receive(:consumed!)
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: empipelines
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.2.
|
4
|
+
version: 0.2.4
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -10,7 +10,7 @@ authors:
|
|
10
10
|
autorequire:
|
11
11
|
bindir: bin
|
12
12
|
cert_chain: []
|
13
|
-
date: 2012-01-
|
13
|
+
date: 2012-01-10 00:00:00.000000000Z
|
14
14
|
dependencies: []
|
15
15
|
description: Simple Event Handling Pipeline Architecture for EventMachine
|
16
16
|
email: pcalcado+empipelines@gmail.com
|
@@ -23,10 +23,12 @@ files:
|
|
23
23
|
- Gemfile.lock
|
24
24
|
- README.md
|
25
25
|
- Rakefile
|
26
|
+
- empipelines-0.2.4.gem
|
26
27
|
- empipelines.gemspec
|
28
|
+
- functional/consuming_events_from_amqp_spec.rb
|
29
|
+
- functional/consuming_events_from_batch_spec.rb
|
27
30
|
- functional/consuming_events_from_file_spec.rb
|
28
31
|
- functional/consuming_events_from_multiple_sources_spec.rb
|
29
|
-
- functional/consuming_events_from_queue_spec.rb
|
30
32
|
- functional/events.dat
|
31
33
|
- functional/test_stages.rb
|
32
34
|
- lib/empipelines.rb
|
@@ -67,7 +69,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
67
69
|
version: '0'
|
68
70
|
segments:
|
69
71
|
- 0
|
70
|
-
hash:
|
72
|
+
hash: 4556679468386328917
|
71
73
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
72
74
|
none: false
|
73
75
|
requirements:
|