empipelines 0.2.4 → 0.2.5
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/empipelines.gemspec +2 -3
- data/functional/consuming_events_from_amqp_spec.rb +65 -12
- data/functional/test_stages.rb +27 -0
- data/lib/empipelines/amqp_event_source.rb +17 -12
- data/lib/empipelines/pipeline.rb +11 -11
- data/lib/empipelines.rb +1 -1
- data/unit/empipelines/amqp_event_source_spec.rb +21 -6
- data/unit/empipelines/pipeline_spec.rb +12 -8
- metadata +3 -4
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.5'
|
8
|
+
s.date = '2012-01-13'
|
9
9
|
s.rubyforge_project = 'empipelines'
|
10
10
|
|
11
11
|
s.summary = "Simple Event Handling Pipeline Architecture for EventMachine"
|
@@ -35,7 +35,6 @@ Gem::Specification.new do |s|
|
|
35
35
|
Gemfile.lock
|
36
36
|
README.md
|
37
37
|
Rakefile
|
38
|
-
empipelines-0.2.4.gem
|
39
38
|
empipelines.gemspec
|
40
39
|
functional/consuming_events_from_amqp_spec.rb
|
41
40
|
functional/consuming_events_from_batch_spec.rb
|
@@ -4,32 +4,38 @@ require 'amqp'
|
|
4
4
|
require 'json'
|
5
5
|
require File.join(File.dirname(__FILE__), 'test_stages')
|
6
6
|
|
7
|
+
ExchangeName = 'empipelines.build'
|
8
|
+
QueueName = 'empipelines.build.queue'
|
9
|
+
|
10
|
+
def setup_queues
|
11
|
+
connection = AMQP.connect()
|
12
|
+
channel = AMQP::Channel.new(connection)
|
13
|
+
channel.prefetch(1)
|
14
|
+
|
15
|
+
exchange = channel.direct(ExchangeName, :durable => true)
|
16
|
+
queue = channel.queue(QueueName, :durable => true)
|
17
|
+
queue.bind(exchange)
|
18
|
+
queue.purge
|
19
|
+
[exchange, queue]
|
20
|
+
end
|
21
|
+
|
7
22
|
module TestStages
|
8
|
-
ExchangeName = 'empipelines.build'
|
9
|
-
QueueName = 'empipelines.build.queue'
|
10
23
|
|
11
24
|
describe 'Consumption of events from a in-memory batch' do
|
12
|
-
let(:monitoring) {
|
25
|
+
let(:monitoring) { Monitoring.new }
|
13
26
|
let (:processed) { [] }
|
14
27
|
|
15
28
|
include EmRunner
|
16
29
|
|
17
30
|
it 'consumes all events from a queue' do
|
18
31
|
with_em_run do
|
19
|
-
|
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
|
32
|
+
exchange, queue = setup_queues
|
27
33
|
|
28
34
|
messages = (1..1000).map { |i| {:data => i}.to_json }
|
29
35
|
messages.each { |m| exchange.publish(m, :rounting_key => QueueName) }
|
30
36
|
|
31
37
|
pipeline = EmPipelines::Pipeline.new(EM, {:processed => processed}, monitoring)
|
32
|
-
source = EmPipelines::AmqpEventSource.new(EM, queue, 'msg')
|
38
|
+
source = EmPipelines::AmqpEventSource.new(EM, queue, 'msg', monitoring)
|
33
39
|
|
34
40
|
stages = [PassthroughStage, PassthroughStage, PassthroughStage]
|
35
41
|
event_pipeline = EmPipelines::EventPipeline.new(source, pipeline.for(stages), monitoring)
|
@@ -43,5 +49,52 @@ module TestStages
|
|
43
49
|
event_pipeline.start!
|
44
50
|
end
|
45
51
|
end
|
52
|
+
|
53
|
+
it 'discards broken messages' do
|
54
|
+
with_em_run do
|
55
|
+
exchange, queue = setup_queues
|
56
|
+
|
57
|
+
messages = (1..1000).map { |i| {:data => i}.to_json }
|
58
|
+
messages.each { |m| exchange.publish(m, :rounting_key => QueueName) }
|
59
|
+
|
60
|
+
pipeline = EmPipelines::Pipeline.new(EM, {:processed => processed}, monitoring)
|
61
|
+
source = EmPipelines::AmqpEventSource.new(EM, queue, 'msg', monitoring)
|
62
|
+
|
63
|
+
stages = [BrokenMessageStage, ShouldNotBeReachedStage]
|
64
|
+
event_pipeline = EmPipelines::EventPipeline.new(source, pipeline.for(stages), monitoring)
|
65
|
+
|
66
|
+
EM.add_periodic_timer(0.1) do
|
67
|
+
if processed.size == messages.size then
|
68
|
+
EM.stop
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
event_pipeline.start!
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
it 'retries messages if told to do so' do
|
77
|
+
with_em_run do
|
78
|
+
exchange, queue = setup_queues
|
79
|
+
|
80
|
+
messages = [ {:data => "b"}.to_json ]
|
81
|
+
messages.each { |m| exchange.publish(m, :rounting_key => QueueName) }
|
82
|
+
|
83
|
+
pipeline = EmPipelines::Pipeline.new(EM, {:processed => processed}, monitoring)
|
84
|
+
source = EmPipelines::AmqpEventSource.new(EM, queue, 'msg', monitoring)
|
85
|
+
|
86
|
+
stages = [RetryOscillator]
|
87
|
+
event_pipeline = EmPipelines::EventPipeline.new(source, pipeline.for(stages), monitoring)
|
88
|
+
|
89
|
+
EM.add_periodic_timer(0.1) do
|
90
|
+
if processed.size == 2 then
|
91
|
+
EM.stop
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
event_pipeline.start!
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
46
99
|
end
|
47
100
|
end
|
data/functional/test_stages.rb
CHANGED
@@ -1,4 +1,22 @@
|
|
1
1
|
module TestStages
|
2
|
+
class Monitoring
|
3
|
+
def initialize(output = false)
|
4
|
+
@output = output
|
5
|
+
end
|
6
|
+
|
7
|
+
def inform(txt)
|
8
|
+
puts "#{Time.now.usec} INFO: #{txt}" if @output
|
9
|
+
end
|
10
|
+
|
11
|
+
def debug(txt)
|
12
|
+
puts "#{Time.now.usec} DEBUG: #{txt}" if @output
|
13
|
+
end
|
14
|
+
|
15
|
+
def inform_exception!(exc, origin, extra = nil)
|
16
|
+
puts "#{Time.now.usec} ERROR: #{exc} at #{origin} - #{extra}" if @output
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
2
20
|
module EmRunner
|
3
21
|
def with_em_run(&test_body)
|
4
22
|
EM.run do
|
@@ -33,6 +51,15 @@ module TestStages
|
|
33
51
|
end
|
34
52
|
end
|
35
53
|
|
54
|
+
class RetryOscillator
|
55
|
+
include SomeStage
|
56
|
+
|
57
|
+
def process(message, callback)
|
58
|
+
@flip = !@flip || false
|
59
|
+
message.rejected! if @flip
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
36
63
|
class ShouldNotBeReachedStage
|
37
64
|
include SomeStage
|
38
65
|
|
@@ -7,22 +7,27 @@ module EmPipelines
|
|
7
7
|
#this must have a on_finished!
|
8
8
|
class AmqpEventSource < EventSource
|
9
9
|
|
10
|
-
def initialize(em, queue, event_name)
|
11
|
-
@em, @queue, @event_name = em, queue, event_name
|
10
|
+
def initialize(em, queue, event_name, monitoring)
|
11
|
+
@em, @queue, @event_name, @monitoring = em, queue, event_name, monitoring
|
12
12
|
end
|
13
13
|
|
14
14
|
def start!
|
15
15
|
@queue.subscribe(:ack => true) do |header, json_payload|
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
16
|
+
begin
|
17
|
+
message = Message.new({
|
18
|
+
:origin => @queue.name,
|
19
|
+
:payload => JSON.parse(json_payload),
|
20
|
+
:event => @event_name,
|
21
|
+
:started_at => Time.now.to_i
|
22
|
+
})
|
23
|
+
message.on_consumed { |m| header.ack }
|
24
|
+
message.on_broken { |m| header.reject(:requeue => false) }
|
25
|
+
message.on_rejected { |m| header.reject(:requeue => true) }
|
26
|
+
event!(message)
|
27
|
+
rescue => exc
|
28
|
+
@monitoring.inform_exception!(exc, self, 'removing message from queue')
|
29
|
+
header.reject(:requeue => false)
|
30
|
+
end
|
26
31
|
end
|
27
32
|
end
|
28
33
|
end
|
data/lib/empipelines/pipeline.rb
CHANGED
@@ -5,35 +5,35 @@ module EmPipelines
|
|
5
5
|
message.consumed!
|
6
6
|
end
|
7
7
|
end
|
8
|
-
|
8
|
+
|
9
9
|
def initialize(em, context, monitoring)
|
10
10
|
@em = em
|
11
11
|
@context = context
|
12
12
|
@monitoring = monitoring
|
13
13
|
end
|
14
|
-
|
14
|
+
|
15
15
|
def for(event_definition)
|
16
16
|
stages = event_definition.map(&instantiate_with_dependencies)
|
17
17
|
|
18
18
|
monitoring = @monitoring
|
19
|
-
|
19
|
+
|
20
20
|
first_stage_process = stages.reverse.reduce(TerminatorStage) do |current_head, next_stage|
|
21
|
-
@em.spawn do |
|
21
|
+
@em.spawn do |input_message|
|
22
22
|
begin
|
23
|
-
monitoring.debug "#{next_stage.class}#notify with #{
|
24
|
-
next_stage.call(
|
23
|
+
monitoring.debug "#{next_stage.class}#notify with #{input_message}}"
|
24
|
+
next_stage.call(input_message) do |output|
|
25
25
|
current_head.notify(output)
|
26
26
|
end
|
27
27
|
rescue => exception
|
28
|
-
monitoring.inform_exception!(exception, next_stage)
|
28
|
+
monitoring.inform_exception!(exception, next_stage, "Message #{input_message} is broken")
|
29
|
+
input_message.broken!
|
29
30
|
end
|
30
|
-
end
|
31
|
+
end
|
31
32
|
end
|
32
|
-
|
33
|
-
@monitoring.inform "Pipeline for event_definition is: #{stages.map(&:class).join('->')}"
|
33
|
+
@monitoring.inform "Pipeline for event_definition is: #{stages.map(&:class).join('->')}"
|
34
34
|
first_stage_process
|
35
35
|
end
|
36
|
-
|
36
|
+
|
37
37
|
private
|
38
38
|
def instantiate_with_dependencies
|
39
39
|
lambda do |stage_class|
|
data/lib/empipelines.rb
CHANGED
@@ -14,6 +14,7 @@ module EmPipelines
|
|
14
14
|
|
15
15
|
describe AmqpEventSource do
|
16
16
|
let (:em) { mock('eventmachine') }
|
17
|
+
let (:monitoring) { mock('monitoring') }
|
17
18
|
|
18
19
|
it 'wraps each AMQP message and send to listeners' do
|
19
20
|
json_payload = '{"key":"value"}'
|
@@ -24,7 +25,7 @@ module EmPipelines
|
|
24
25
|
|
25
26
|
received_messages = []
|
26
27
|
|
27
|
-
amqp_source = AmqpEventSource.new(em, queue, event_type)
|
28
|
+
amqp_source = AmqpEventSource.new(em, queue, event_type, monitoring)
|
28
29
|
amqp_source.on_event { |e| received_messages << e }
|
29
30
|
amqp_source.start!
|
30
31
|
|
@@ -43,20 +44,34 @@ module EmPipelines
|
|
43
44
|
|
44
45
|
header.should_receive(:ack)
|
45
46
|
|
46
|
-
amqp_source = AmqpEventSource.new(em, queue, 'event type')
|
47
|
+
amqp_source = AmqpEventSource.new(em, queue, 'event type', monitoring)
|
47
48
|
amqp_source.on_event { |e| e.consumed! }
|
48
49
|
amqp_source.start!
|
49
50
|
|
50
51
|
queue.publish(header, '{"key":"value"}')
|
51
52
|
end
|
52
53
|
|
53
|
-
it '
|
54
|
+
it 'marks message as broken if cannot be parsed' do
|
54
55
|
queue = StubQueue.new
|
55
56
|
header = mock('header')
|
56
57
|
|
57
|
-
header.should_receive(:reject)
|
58
|
+
header.should_receive(:reject).with({:requeue => false})
|
59
|
+
monitoring.should_receive(:inform_exception!)
|
58
60
|
|
59
|
-
amqp_source = AmqpEventSource.new(em, queue, 'event type')
|
61
|
+
amqp_source = AmqpEventSource.new(em, queue, 'event type', monitoring)
|
62
|
+
amqp_source.on_event { raise 'should never happen' }
|
63
|
+
amqp_source.start!
|
64
|
+
|
65
|
+
queue.publish(header, 'some junk')
|
66
|
+
end
|
67
|
+
|
68
|
+
it 'rejects broken messages with no requeue' do
|
69
|
+
queue = StubQueue.new
|
70
|
+
header = mock('header')
|
71
|
+
|
72
|
+
header.should_receive(:reject).with({:requeue => false})
|
73
|
+
|
74
|
+
amqp_source = AmqpEventSource.new(em, queue, 'event type', monitoring)
|
60
75
|
amqp_source.on_event { |e| e.broken! }
|
61
76
|
amqp_source.start!
|
62
77
|
|
@@ -69,7 +84,7 @@ module EmPipelines
|
|
69
84
|
|
70
85
|
header.should_receive(:reject).with({:requeue => true})
|
71
86
|
|
72
|
-
amqp_source = AmqpEventSource.new(em, queue, 'event type')
|
87
|
+
amqp_source = AmqpEventSource.new(em, queue, 'event type', monitoring)
|
73
88
|
amqp_source.on_event { |e| e.rejected! }
|
74
89
|
amqp_source.start!
|
75
90
|
|
@@ -10,7 +10,7 @@ module EmPipelines
|
|
10
10
|
def consumed!
|
11
11
|
raise 'unexpected call'
|
12
12
|
end
|
13
|
-
|
13
|
+
|
14
14
|
def rejected!
|
15
15
|
raise 'unexpected call'
|
16
16
|
end
|
@@ -61,7 +61,7 @@ module EmPipelines
|
|
61
61
|
next_stage.call(input.merge!({:orange => orange}))
|
62
62
|
end
|
63
63
|
end
|
64
|
-
|
64
|
+
|
65
65
|
class GlobalHolder
|
66
66
|
@@value = nil
|
67
67
|
def GlobalHolder.held
|
@@ -71,7 +71,7 @@ module EmPipelines
|
|
71
71
|
def initialize
|
72
72
|
@@value = nil
|
73
73
|
end
|
74
|
-
|
74
|
+
|
75
75
|
def call(input, &next_step)
|
76
76
|
@@value = input
|
77
77
|
next_step.call(input)
|
@@ -79,17 +79,17 @@ module EmPipelines
|
|
79
79
|
end
|
80
80
|
|
81
81
|
class StubSpawner
|
82
|
-
|
82
|
+
|
83
83
|
class StubProcess
|
84
84
|
def initialize(block)
|
85
85
|
@block = block
|
86
86
|
end
|
87
|
-
|
87
|
+
|
88
88
|
def notify(input)
|
89
89
|
@block.call(input)
|
90
90
|
end
|
91
91
|
end
|
92
|
-
|
92
|
+
|
93
93
|
def spawn(&block)
|
94
94
|
StubProcess.new(block)
|
95
95
|
end
|
@@ -131,10 +131,14 @@ module EmPipelines
|
|
131
131
|
GlobalHolder.held[:orange].should ==(:some_orange)
|
132
132
|
end
|
133
133
|
|
134
|
-
it '
|
134
|
+
it 'marks message as broken if uncaught exception' do
|
135
|
+
a_msg = msg({})
|
135
136
|
monitoring.should_receive(:inform_exception!)
|
137
|
+
|
138
|
+
a_msg.should_receive(:broken!)
|
139
|
+
|
136
140
|
pipeline = Pipeline.new(StubSpawner.new, {}, monitoring)
|
137
|
-
pipeline.for([BrokenStage]).notify(
|
141
|
+
pipeline.for([BrokenStage]).notify(a_msg)
|
138
142
|
end
|
139
143
|
|
140
144
|
it 'flags the message as consumed if goest through all stages' do
|
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.5
|
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-13 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,7 +23,6 @@ files:
|
|
23
23
|
- Gemfile.lock
|
24
24
|
- README.md
|
25
25
|
- Rakefile
|
26
|
-
- empipelines-0.2.4.gem
|
27
26
|
- empipelines.gemspec
|
28
27
|
- functional/consuming_events_from_amqp_spec.rb
|
29
28
|
- functional/consuming_events_from_batch_spec.rb
|
@@ -69,7 +68,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
69
68
|
version: '0'
|
70
69
|
segments:
|
71
70
|
- 0
|
72
|
-
hash:
|
71
|
+
hash: 3829933959180740476
|
73
72
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
74
73
|
none: false
|
75
74
|
requirements:
|