empipelines 0.1
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/Gemfile +15 -0
- data/Gemfile.lock +69 -0
- data/README.md +1 -0
- data/Rakefile +168 -0
- data/empipelines.gemspec +61 -0
- data/lib/empipelines/amqp_event_source.rb +25 -0
- data/lib/empipelines/batch_event_source.rb +44 -0
- data/lib/empipelines/event_pipeline.rb +15 -0
- data/lib/empipelines/list_event_source.rb +17 -0
- data/lib/empipelines/message.rb +90 -0
- data/lib/empipelines/periodic_event_source.rb +24 -0
- data/lib/empipelines/pipeline.rb +50 -0
- data/lib/empipelines.rb +3 -0
- data/spec/empipelines/amqp_event_source_spec.rb +36 -0
- data/spec/empipelines/batch_event_source_spec.rb +78 -0
- data/spec/empipelines/event_pipeline_spec.rb +31 -0
- data/spec/empipelines/list_event_source_spec.rb +17 -0
- data/spec/empipelines/message_spec.rb +138 -0
- data/spec/empipelines/periodic_event_source_spec.rb +28 -0
- data/spec/empipelines/pipeline_spec.rb +106 -0
- data/spec/spec_helper.rb +0 -0
- data/spec/stage_helper.rb +54 -0
- metadata +72 -0
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,69 @@
|
|
1
|
+
GEM
|
2
|
+
remote: http://rubygems.org/
|
3
|
+
specs:
|
4
|
+
activesupport (3.1.3)
|
5
|
+
multi_json (~> 1.0)
|
6
|
+
addressable (2.2.6)
|
7
|
+
amq-client (0.8.7)
|
8
|
+
amq-protocol (>= 0.8.4)
|
9
|
+
eventmachine
|
10
|
+
amq-protocol (0.8.4)
|
11
|
+
amqp (0.8.4)
|
12
|
+
amq-client (~> 0.8.7)
|
13
|
+
amq-protocol (~> 0.8.4)
|
14
|
+
eventmachine
|
15
|
+
builder (3.0.0)
|
16
|
+
bunny (0.7.8)
|
17
|
+
cucumber (1.1.4)
|
18
|
+
builder (>= 2.1.2)
|
19
|
+
diff-lcs (>= 1.1.2)
|
20
|
+
gherkin (~> 2.7.1)
|
21
|
+
json (>= 1.4.6)
|
22
|
+
term-ansicolor (>= 1.0.6)
|
23
|
+
diff-lcs (1.1.3)
|
24
|
+
em-http-request (1.0.0)
|
25
|
+
addressable (>= 2.2.3)
|
26
|
+
em-socksify
|
27
|
+
eventmachine (>= 1.0.0.beta.3)
|
28
|
+
http_parser.rb (>= 0.5.2)
|
29
|
+
em-socksify (0.1.0)
|
30
|
+
eventmachine
|
31
|
+
eventmachine (1.0.0.beta.4)
|
32
|
+
gherkin (2.7.1)
|
33
|
+
json (>= 1.4.6)
|
34
|
+
http_parser.rb (0.5.3)
|
35
|
+
json (1.6.3)
|
36
|
+
multi_json (1.0.4)
|
37
|
+
rack (1.3.5)
|
38
|
+
rack-protection (1.1.4)
|
39
|
+
rack
|
40
|
+
rake (0.9.2.2)
|
41
|
+
rspec (2.7.0)
|
42
|
+
rspec-core (~> 2.7.0)
|
43
|
+
rspec-expectations (~> 2.7.0)
|
44
|
+
rspec-mocks (~> 2.7.0)
|
45
|
+
rspec-core (2.7.1)
|
46
|
+
rspec-expectations (2.7.0)
|
47
|
+
diff-lcs (~> 1.1.2)
|
48
|
+
rspec-mocks (2.7.0)
|
49
|
+
sinatra (1.3.1)
|
50
|
+
rack (~> 1.3, >= 1.3.4)
|
51
|
+
rack-protection (~> 1.1, >= 1.1.2)
|
52
|
+
tilt (~> 1.3, >= 1.3.3)
|
53
|
+
term-ansicolor (1.0.7)
|
54
|
+
tilt (1.3.3)
|
55
|
+
|
56
|
+
PLATFORMS
|
57
|
+
ruby
|
58
|
+
|
59
|
+
DEPENDENCIES
|
60
|
+
activesupport
|
61
|
+
amqp
|
62
|
+
bunny
|
63
|
+
cucumber
|
64
|
+
em-http-request
|
65
|
+
eventmachine
|
66
|
+
json
|
67
|
+
rake
|
68
|
+
rspec
|
69
|
+
sinatra
|
data/README.md
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
# EM Pipelines
|
data/Rakefile
ADDED
@@ -0,0 +1,168 @@
|
|
1
|
+
$:.unshift(File.dirname(__FILE__) + '/../lib')
|
2
|
+
|
3
|
+
require 'cucumber/rake/task'
|
4
|
+
require 'rubygems'
|
5
|
+
require 'rake'
|
6
|
+
require 'date'
|
7
|
+
|
8
|
+
#############################################################################
|
9
|
+
#
|
10
|
+
# Helper functions
|
11
|
+
#
|
12
|
+
#############################################################################
|
13
|
+
|
14
|
+
def name
|
15
|
+
@name ||= Dir['*.gemspec'].first.split('.').first
|
16
|
+
end
|
17
|
+
|
18
|
+
def version
|
19
|
+
line = File.read("lib/#{name}.rb")[/^\s*VERSION\s*=\s*.*/]
|
20
|
+
line.match(/.*VERSION\s*=\s*['"](.*)['"]/)[1]
|
21
|
+
end
|
22
|
+
|
23
|
+
def date
|
24
|
+
Date.today.to_s
|
25
|
+
end
|
26
|
+
|
27
|
+
def rubyforge_project
|
28
|
+
name
|
29
|
+
end
|
30
|
+
|
31
|
+
def gemspec_file
|
32
|
+
"#{name}.gemspec"
|
33
|
+
end
|
34
|
+
|
35
|
+
def gemspec
|
36
|
+
@gemspec ||= eval(IO.read(gemspec_file))
|
37
|
+
end
|
38
|
+
|
39
|
+
def gem_file
|
40
|
+
gemspec.file_name
|
41
|
+
end
|
42
|
+
|
43
|
+
def replace_header(head, header_name)
|
44
|
+
head.sub!(/(\.#{header_name}\s*= ').*'/) { "#{$1}#{send(header_name)}'"}
|
45
|
+
end
|
46
|
+
|
47
|
+
#############################################################################
|
48
|
+
#
|
49
|
+
# Standard tasks
|
50
|
+
#
|
51
|
+
#############################################################################
|
52
|
+
task :default => :pre_checkin
|
53
|
+
|
54
|
+
Cucumber::Rake::Task.new(:non_wip_features) do |t|
|
55
|
+
t.cucumber_opts = "--format pretty --tag ~@wip"
|
56
|
+
end
|
57
|
+
|
58
|
+
Cucumber::Rake::Task.new(:all_features) do |t|
|
59
|
+
t.cucumber_opts = "--format pretty"
|
60
|
+
end
|
61
|
+
|
62
|
+
desc "Runs specs"
|
63
|
+
task :specs do
|
64
|
+
all = FileList['spec/**/*_spec.rb']
|
65
|
+
sh "rspec --color #{all}"
|
66
|
+
end
|
67
|
+
|
68
|
+
desc "Resets localhost's rabbitmq"
|
69
|
+
task :reset_rabbitmq do
|
70
|
+
sh 'rabbitmqctl stop_app'
|
71
|
+
sh 'rabbitmqctl reset'
|
72
|
+
sh 'rabbitmqctl start_app'
|
73
|
+
end
|
74
|
+
|
75
|
+
desc "Run cucumber tests for finished features"
|
76
|
+
task :features do
|
77
|
+
|
78
|
+
end
|
79
|
+
|
80
|
+
desc "Run cucumber tests for all features, including work in progress"
|
81
|
+
task :all_features do
|
82
|
+
|
83
|
+
end
|
84
|
+
|
85
|
+
task :ci => [:specs, :features, :build]
|
86
|
+
|
87
|
+
desc "MUST BE RUN (AND PASS!) BEFORE CHECKING IN CODE!"
|
88
|
+
task :pre_checkin => [:reset_rabbitmq, :ci]
|
89
|
+
|
90
|
+
#############################################################################
|
91
|
+
#
|
92
|
+
# Custom tasks (add your own tasks here)
|
93
|
+
#
|
94
|
+
#############################################################################
|
95
|
+
|
96
|
+
|
97
|
+
|
98
|
+
#############################################################################
|
99
|
+
#
|
100
|
+
# Packaging tasks
|
101
|
+
#
|
102
|
+
#############################################################################
|
103
|
+
|
104
|
+
task :release => :ci do
|
105
|
+
Dir.chdir File.dirname(__FILE__)
|
106
|
+
unless `git branch` =~ /^\* master$/
|
107
|
+
puts "You must be on the master branch to release!"
|
108
|
+
exit!
|
109
|
+
end
|
110
|
+
if `git fetch --tags && git tag`.split(/\n/).include?(gem_file)
|
111
|
+
raise "Version #{gem_file} already deployed"
|
112
|
+
end
|
113
|
+
sh <<-END
|
114
|
+
git commit -a --allow-empty -m 'Release #{gem_file}'
|
115
|
+
git tag -a #{gem_file} -m 'Version #{gem_file}'
|
116
|
+
git push origin master
|
117
|
+
git push origin --tags
|
118
|
+
END
|
119
|
+
end
|
120
|
+
|
121
|
+
desc "Build #{gem_file} into the pkg directory"
|
122
|
+
task :build => :gemspec do
|
123
|
+
sh "mkdir -p pkg"
|
124
|
+
sh "gem build #{gemspec_file}"
|
125
|
+
sh "mv #{gem_file} pkg"
|
126
|
+
end
|
127
|
+
|
128
|
+
desc "Generate #{gemspec_file}"
|
129
|
+
task :gemspec => :validate do
|
130
|
+
# read spec file and split out manifest section
|
131
|
+
spec = File.read(gemspec_file)
|
132
|
+
head, manifest, tail = spec.split(" # = MANIFEST =\n")
|
133
|
+
|
134
|
+
# replace name version and date
|
135
|
+
replace_header(head, :name)
|
136
|
+
replace_header(head, :version)
|
137
|
+
replace_header(head, :date)
|
138
|
+
#comment this out if your rubyforge_project has a different name
|
139
|
+
replace_header(head, :rubyforge_project)
|
140
|
+
|
141
|
+
# determine file list from git ls-files
|
142
|
+
files = `git ls-files`.
|
143
|
+
split("\n").
|
144
|
+
sort.
|
145
|
+
reject { |file| file =~ /^\./ }.
|
146
|
+
reject { |file| file =~ /^(rdoc|pkg)/ }.
|
147
|
+
map { |file| " #{file}" }.
|
148
|
+
join("\n")
|
149
|
+
|
150
|
+
# piece file back together and write
|
151
|
+
manifest = " s.files = %w[\n#{files}\n ]\n"
|
152
|
+
spec = [head, manifest, tail].join(" # = MANIFEST =\n")
|
153
|
+
File.open(gemspec_file, 'w') { |io| io.write(spec) }
|
154
|
+
puts "Updated #{gemspec_file}"
|
155
|
+
end
|
156
|
+
|
157
|
+
desc "Validate #{gemspec_file}"
|
158
|
+
task :validate do
|
159
|
+
libfiles = Dir['lib/*'] - ["lib/#{name}.rb", "lib/#{name}"]
|
160
|
+
unless libfiles.empty?
|
161
|
+
puts "Directory `lib` should only contain a `#{name}.rb` file and `#{name}` dir."
|
162
|
+
exit!
|
163
|
+
end
|
164
|
+
unless Dir['VERSION*'].empty?
|
165
|
+
puts "A `VERSION` file at root level violates Gem best practices."
|
166
|
+
exit!
|
167
|
+
end
|
168
|
+
end
|
data/empipelines.gemspec
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
Gem::Specification.new do |s|
|
2
|
+
s.specification_version = 2 if s.respond_to? :specification_version=
|
3
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
4
|
+
s.rubygems_version = '1.3.5'
|
5
|
+
## Leave these as is they will be modified for you by the rake gemspec task.
|
6
|
+
s.name = 'empipelines'
|
7
|
+
s.version = '0.1'
|
8
|
+
s.date = '2011-12-19'
|
9
|
+
s.rubyforge_project = 'empipelines'
|
10
|
+
|
11
|
+
s.summary = "Simple Event Handling Pipeline Architecture for EventMachine"
|
12
|
+
s.description = "Simple Event Handling Pipeline Architecture for EventMachine"
|
13
|
+
|
14
|
+
s.authors = ["Tobias Schmidt", "Phil Calcado"]
|
15
|
+
s.email = 'pcalcado+empipelines@gmail.com'
|
16
|
+
s.homepage = 'http://github.com/pcalcado/empipelines'
|
17
|
+
|
18
|
+
s.require_paths = %w[lib]
|
19
|
+
#s.executables = ["empipelines"]
|
20
|
+
#s.default_executable = 'empipelines'
|
21
|
+
|
22
|
+
## Specify any RDoc options here.
|
23
|
+
s.rdoc_options = ["--charset=UTF-8"]
|
24
|
+
s.extra_rdoc_files = %w[README.md]
|
25
|
+
|
26
|
+
## List your runtime dependencies here.
|
27
|
+
#s.add_dependency('DEPNAME', [">= 1.1.0", "< 2.0.0"])
|
28
|
+
#s.add_development_dependency('bacon', [">= 1.1.0])
|
29
|
+
#s.add_development_dependency('rspec', [">= 1.3.0"])
|
30
|
+
|
31
|
+
## DO NOT REMOVE THE MANIFEST COMMENTS, they are used as delimiters by the task.
|
32
|
+
# = MANIFEST =
|
33
|
+
s.files = %w[
|
34
|
+
Gemfile
|
35
|
+
Gemfile.lock
|
36
|
+
README.md
|
37
|
+
Rakefile
|
38
|
+
empipelines.gemspec
|
39
|
+
lib/empipelines.rb
|
40
|
+
lib/empipelines/amqp_event_source.rb
|
41
|
+
lib/empipelines/batch_event_source.rb
|
42
|
+
lib/empipelines/event_pipeline.rb
|
43
|
+
lib/empipelines/list_event_source.rb
|
44
|
+
lib/empipelines/message.rb
|
45
|
+
lib/empipelines/periodic_event_source.rb
|
46
|
+
lib/empipelines/pipeline.rb
|
47
|
+
spec/empipelines/amqp_event_source_spec.rb
|
48
|
+
spec/empipelines/batch_event_source_spec.rb
|
49
|
+
spec/empipelines/event_pipeline_spec.rb
|
50
|
+
spec/empipelines/list_event_source_spec.rb
|
51
|
+
spec/empipelines/message_spec.rb
|
52
|
+
spec/empipelines/periodic_event_source_spec.rb
|
53
|
+
spec/empipelines/pipeline_spec.rb
|
54
|
+
spec/spec_helper.rb
|
55
|
+
spec/stage_helper.rb
|
56
|
+
]
|
57
|
+
# = MANIFEST =
|
58
|
+
|
59
|
+
## Test files will be grabbed from the file list.
|
60
|
+
s.test_files = s.files.select { |path| path =~ /^spec\/*_spec\.rb/ }
|
61
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
module Pipelines
|
4
|
+
class AmqpEventSource
|
5
|
+
def initialize(queue, event_name)
|
6
|
+
@queue, @event_name = queue, event_name
|
7
|
+
end
|
8
|
+
|
9
|
+
def on_event(&handler)
|
10
|
+
@handler = handler
|
11
|
+
end
|
12
|
+
|
13
|
+
def start!
|
14
|
+
@queue.subscribe do |header, json_payload|
|
15
|
+
message = Message.new ({
|
16
|
+
:header => header,
|
17
|
+
:payload => JSON.parse(json_payload),
|
18
|
+
:event => @event_name,
|
19
|
+
:started_at => Time.now.to_i
|
20
|
+
})
|
21
|
+
@handler.call(message)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module Pipelines
|
2
|
+
class BatchEventSource
|
3
|
+
def initialize(em, events)
|
4
|
+
@em, @events = em, events
|
5
|
+
end
|
6
|
+
|
7
|
+
def on_event(&handler)
|
8
|
+
@handler = handler
|
9
|
+
end
|
10
|
+
|
11
|
+
def on_batch_finished(&batch_finished_handler)
|
12
|
+
@batch_finished_handler = batch_finished_handler
|
13
|
+
end
|
14
|
+
|
15
|
+
def start!
|
16
|
+
@finalised = []
|
17
|
+
check_if_finished
|
18
|
+
|
19
|
+
message_finished = lambda do |m|
|
20
|
+
@finalised << m
|
21
|
+
check_if_finished
|
22
|
+
end
|
23
|
+
|
24
|
+
@events.each do |e|
|
25
|
+
message = Message.new({:payload => e})
|
26
|
+
|
27
|
+
message.on_rejected_broken(message_finished)
|
28
|
+
message.on_rejected(message_finished)
|
29
|
+
message.on_consumed(message_finished)
|
30
|
+
|
31
|
+
@handler.call(message)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
def check_if_finished
|
37
|
+
finished = (@finalised.size == @events.size)
|
38
|
+
|
39
|
+
if finished and @batch_finished_handler
|
40
|
+
@em.next_tick { @batch_finished_handler.call(@finalised) }
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Pipelines
|
2
|
+
class EventPipeline
|
3
|
+
def initialize(source, pipeline, monitoring)
|
4
|
+
@source, @pipeline, @monitoring = source, pipeline, monitoring
|
5
|
+
|
6
|
+
@source.on_event do |event_data|
|
7
|
+
@pipeline.notify(event_data)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def start!
|
12
|
+
@source.start!
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
module Pipelines
|
2
|
+
class Message
|
3
|
+
def initialize(base_hash={})
|
4
|
+
hash!(base_hash)
|
5
|
+
created!
|
6
|
+
end
|
7
|
+
|
8
|
+
def [](key)
|
9
|
+
hash[key]
|
10
|
+
end
|
11
|
+
|
12
|
+
def []=(key, value)
|
13
|
+
check_if_mutation_allowed
|
14
|
+
hash[key] = value
|
15
|
+
end
|
16
|
+
|
17
|
+
def delete(key)
|
18
|
+
check_if_mutation_allowed
|
19
|
+
hash.delete key
|
20
|
+
end
|
21
|
+
|
22
|
+
def merge(other_hash)
|
23
|
+
check_if_mutation_allowed
|
24
|
+
Message.new(hash.merge(other_hash))
|
25
|
+
end
|
26
|
+
|
27
|
+
def on_consumed(callback=nil, &callback_block)
|
28
|
+
@consumed_callback = block_given? ? callback_block : callback
|
29
|
+
end
|
30
|
+
|
31
|
+
def on_rejected(callback=nil, &callback_block)
|
32
|
+
@rejected_callback = block_given? ? callback_block : callback
|
33
|
+
end
|
34
|
+
|
35
|
+
def on_rejected_broken(callback=nil, &callback_block)
|
36
|
+
@rejected_broken_callback = block_given? ? callback_block : callback
|
37
|
+
end
|
38
|
+
|
39
|
+
def consumed!
|
40
|
+
check_if_mutation_allowed
|
41
|
+
@state = :consumed
|
42
|
+
invoke(@consumed_callback)
|
43
|
+
end
|
44
|
+
|
45
|
+
def rejected!
|
46
|
+
check_if_mutation_allowed
|
47
|
+
@state = :rejected
|
48
|
+
invoke(@rejected_callback)
|
49
|
+
end
|
50
|
+
|
51
|
+
def broken!
|
52
|
+
check_if_mutation_allowed
|
53
|
+
@state = :rejected_broken
|
54
|
+
invoke(@rejected_broken_callback)
|
55
|
+
end
|
56
|
+
|
57
|
+
def hash
|
58
|
+
@backing_hash
|
59
|
+
end
|
60
|
+
|
61
|
+
def to_s
|
62
|
+
"#{self.class.name} state:#{@state} hash:#{hash}"
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
def hash!(other)
|
68
|
+
@backing_hash = symbolised(other)
|
69
|
+
end
|
70
|
+
|
71
|
+
def symbolised(raw_hash)
|
72
|
+
raw_hash.reduce({}) do |acc, (key, value)|
|
73
|
+
acc[key.to_s.to_sym] = value.is_a?(Hash) ? symbolised(value) : value
|
74
|
+
acc
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def created!
|
79
|
+
@state = :created
|
80
|
+
end
|
81
|
+
|
82
|
+
def check_if_mutation_allowed
|
83
|
+
raise "Cannot mutate #{self}" unless @state == :created
|
84
|
+
end
|
85
|
+
|
86
|
+
def invoke(callback)
|
87
|
+
callback.call(self) if callback
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Pipelines
|
2
|
+
class PeriodicEventSource
|
3
|
+
def initialize(em, interval_in_secs, &event_sourcing_code)
|
4
|
+
@em, @interval_in_secs, @event_sourcing_code = em, interval_in_secs, event_sourcing_code
|
5
|
+
end
|
6
|
+
|
7
|
+
def start!
|
8
|
+
event_sourcing_code = @event_sourcing_code
|
9
|
+
|
10
|
+
@em.add_periodic_timer(@interval_in_secs) do
|
11
|
+
tick!
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def on_event(&handler)
|
16
|
+
@handler = handler
|
17
|
+
end
|
18
|
+
|
19
|
+
def tick!
|
20
|
+
event = @event_sourcing_code.call
|
21
|
+
@handler.call(event) if event
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module Pipelines
|
2
|
+
class Pipeline
|
3
|
+
class TerminatorStage
|
4
|
+
def self.notify(ignored, also_ignored = {})
|
5
|
+
#noop
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
def initialize(spawner, context, monitoring, logger)
|
10
|
+
@spawner = spawner
|
11
|
+
@logger = logger
|
12
|
+
@context = context
|
13
|
+
@monitoring = monitoring
|
14
|
+
end
|
15
|
+
|
16
|
+
def for(event_definition)
|
17
|
+
stages = event_definition.map(&instantiate_with_dependencies)
|
18
|
+
|
19
|
+
monitoring = @monitoring
|
20
|
+
logger = @logger
|
21
|
+
|
22
|
+
first_stage_process = stages.reverse.reduce(TerminatorStage) do |current_head, next_stage|
|
23
|
+
@spawner.spawn do |input|
|
24
|
+
begin
|
25
|
+
logger.debug "#{next_stage.class}#notify with #{input}}"
|
26
|
+
next_stage.call(input) do |output|
|
27
|
+
current_head.notify(output)
|
28
|
+
end
|
29
|
+
rescue => exception
|
30
|
+
monitoring.inform_exception!(exception, next_stage)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
@logger.info "Pipeline for event_definition is: #{stages.map(&:class).join('->')}"
|
36
|
+
first_stage_process
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
def instantiate_with_dependencies
|
41
|
+
lambda do |stage_class|
|
42
|
+
stage_instance = stage_class.new
|
43
|
+
@context.each do |name, value|
|
44
|
+
stage_instance.define_singleton_method(name) { value }
|
45
|
+
end
|
46
|
+
stage_instance
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
data/lib/empipelines.rb
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'empipelines/amqp_event_source'
|
2
|
+
|
3
|
+
module Pipelines
|
4
|
+
class StubQueue
|
5
|
+
def subscribe(&code)
|
6
|
+
@code = code
|
7
|
+
end
|
8
|
+
|
9
|
+
def publish(header, payload)
|
10
|
+
@code.call(header, payload)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
describe AmqpEventSource do
|
15
|
+
it "adds payload, time and event type to message and sends to listener" do
|
16
|
+
json_payload = '{"key":"value"}'
|
17
|
+
queue = StubQueue.new
|
18
|
+
event_type = "NuclearWar"
|
19
|
+
header = stub('header')
|
20
|
+
|
21
|
+
received_messages = []
|
22
|
+
|
23
|
+
amqp_source = AmqpEventSource.new(queue, event_type)
|
24
|
+
amqp_source.on_event { |e| received_messages << e }
|
25
|
+
amqp_source.start!
|
26
|
+
|
27
|
+
queue.publish(header, json_payload)
|
28
|
+
|
29
|
+
received_messages.size.should eql(1)
|
30
|
+
received_messages.first[:header].should ==(header)
|
31
|
+
received_messages.first[:payload].should ==({:key => "value"})
|
32
|
+
received_messages.first[:event].should ==(event_type)
|
33
|
+
received_messages.first[:started_at].should_not be_nil
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
require 'empipelines/batch_event_source'
|
2
|
+
|
3
|
+
module Pipelines
|
4
|
+
describe BatchEventSource do
|
5
|
+
|
6
|
+
let (:em) do
|
7
|
+
em = mock('eventmachine')
|
8
|
+
em.stub(:next_tick).and_yield
|
9
|
+
em
|
10
|
+
end
|
11
|
+
|
12
|
+
it "sends each element on the list as a payload to the listener" do
|
13
|
+
events = [1,2,3,4,5,6,7,8,9,10]
|
14
|
+
source = BatchEventSource.new(em, events)
|
15
|
+
|
16
|
+
received = []
|
17
|
+
source.on_event do |e|
|
18
|
+
received << e
|
19
|
+
end
|
20
|
+
|
21
|
+
source.start!
|
22
|
+
|
23
|
+
received.map{ |i| i[:payload] }.should ==(events)
|
24
|
+
end
|
25
|
+
|
26
|
+
it "calls the batch finished callback when all items were processed" do
|
27
|
+
events = [1,2,3,4,5,6,7,8,9,10]
|
28
|
+
source = BatchEventSource.new(em, events)
|
29
|
+
|
30
|
+
has_finished = []
|
31
|
+
|
32
|
+
source.on_batch_finished do |messages|
|
33
|
+
has_finished << messages
|
34
|
+
end
|
35
|
+
|
36
|
+
source.on_event do |e|
|
37
|
+
e.consumed!
|
38
|
+
end
|
39
|
+
|
40
|
+
source.start!
|
41
|
+
|
42
|
+
has_finished.first.map{ |i| i[:payload] }.should ==(events)
|
43
|
+
end
|
44
|
+
|
45
|
+
it "finishes straight away if there are no events to process" do
|
46
|
+
source = BatchEventSource.new(em, [])
|
47
|
+
|
48
|
+
has_finished = []
|
49
|
+
source.on_batch_finished do |messages|
|
50
|
+
has_finished << true
|
51
|
+
end
|
52
|
+
|
53
|
+
source.on_event do |e|
|
54
|
+
raise 'should not be called!'
|
55
|
+
end
|
56
|
+
|
57
|
+
source.start!
|
58
|
+
|
59
|
+
has_finished.first.should be_true
|
60
|
+
end
|
61
|
+
|
62
|
+
it "does not call the batch finished callback if not all of the items were processed" do
|
63
|
+
events = [1,2,3,4,5,6,7,8,9,10]
|
64
|
+
source = BatchEventSource.new(em, events)
|
65
|
+
|
66
|
+
source.on_batch_finished do |messages|
|
67
|
+
raise "should not be called"
|
68
|
+
end
|
69
|
+
|
70
|
+
count = 0
|
71
|
+
source.on_event do |e|
|
72
|
+
e.consumed! if (count=+1) > 1
|
73
|
+
end
|
74
|
+
|
75
|
+
source.start!
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'empipelines/event_pipeline'
|
2
|
+
|
3
|
+
module Pipelines
|
4
|
+
class StubSource
|
5
|
+
def initialize(event_data)
|
6
|
+
@event_data = event_data
|
7
|
+
end
|
8
|
+
|
9
|
+
def start!
|
10
|
+
@handler.call(@event_data)
|
11
|
+
end
|
12
|
+
|
13
|
+
def on_event(&event_handler)
|
14
|
+
@handler = event_handler
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
describe EventPipeline do
|
19
|
+
it "binds a source to a pipeline" do
|
20
|
+
monitoring = stub(:increment => nil)
|
21
|
+
event = stub('event')
|
22
|
+
pipeline = stub('processing pipeline')
|
23
|
+
source = StubSource.new(event)
|
24
|
+
|
25
|
+
pipeline.should_receive(:notify).with(event)
|
26
|
+
|
27
|
+
event_pipeline = EventPipeline.new(source, pipeline, monitoring)
|
28
|
+
event_pipeline.start!
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'empipelines/list_event_source'
|
2
|
+
|
3
|
+
module Pipelines
|
4
|
+
describe ListEventSource do
|
5
|
+
it 'sends each element of the list to the handler' do
|
6
|
+
items = [1, 2, 3, 4, 5, 6]
|
7
|
+
expected_messages = items.map { |i| {:payload => i} }
|
8
|
+
received_messages = []
|
9
|
+
|
10
|
+
source = ListEventSource.new(items)
|
11
|
+
source.on_event { |msg| received_messages << msg}
|
12
|
+
source.start!
|
13
|
+
|
14
|
+
received_messages.should eql(expected_messages)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,138 @@
|
|
1
|
+
require 'empipelines/message'
|
2
|
+
|
3
|
+
module Pipelines
|
4
|
+
describe Message do
|
5
|
+
context "mostly behaves like a hashmap" do
|
6
|
+
it "stores values under symbolised keys" do
|
7
|
+
original_hash = {:a => 1, :b => 2}
|
8
|
+
|
9
|
+
message = Message.new(original_hash)
|
10
|
+
|
11
|
+
message[:a].should ==(original_hash[:a])
|
12
|
+
message[:b].should ==(original_hash[:b])
|
13
|
+
message[:doesntexist].should ==(original_hash[:doesntexist])
|
14
|
+
end
|
15
|
+
|
16
|
+
it "symbolises keys of all maps in the message" do
|
17
|
+
message = Message.new({
|
18
|
+
:a => 1,
|
19
|
+
"b" => 2,
|
20
|
+
3 => 3 ,
|
21
|
+
"d" => {
|
22
|
+
"d1" => {"e" => 5},
|
23
|
+
"d2" => nil}
|
24
|
+
})
|
25
|
+
message[:a].should ==(1)
|
26
|
+
message[:b].should ==(2)
|
27
|
+
message["3".to_sym].should ==(3)
|
28
|
+
message[:d][:d1][:e].should ==(5)
|
29
|
+
message[:d][:d2].should be_nil
|
30
|
+
end
|
31
|
+
|
32
|
+
it "allows for values to be CRUD" do
|
33
|
+
original_hash = {:a => 1, :b => 2, :c => 0}
|
34
|
+
|
35
|
+
message = Message.new(original_hash)
|
36
|
+
|
37
|
+
message[:a] = 666
|
38
|
+
message.delete :b
|
39
|
+
message[:z] = 999
|
40
|
+
|
41
|
+
message[:a].should ==(666)
|
42
|
+
message[:b].should be_nil
|
43
|
+
message[:c].should ==(original_hash[:c])
|
44
|
+
message[:z].should ==(999)
|
45
|
+
end
|
46
|
+
|
47
|
+
it "can be merged with a map, symbolising keys" do
|
48
|
+
original = Message.new({'a' => 1})
|
49
|
+
merged = original.merge({'b' => 2})
|
50
|
+
|
51
|
+
original[:b].should be_nil
|
52
|
+
|
53
|
+
merged[:a].should ==(original[:a])
|
54
|
+
merged[:b].should_not be_nil
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
context "message status" do
|
59
|
+
|
60
|
+
let (:handler_that_should_never_be_called) { lambda { raise "This shouldnt happen" } }
|
61
|
+
|
62
|
+
it "doesnt do anything if no state callback specified" do
|
63
|
+
Message.new.consumed!
|
64
|
+
Message.new.rejected!
|
65
|
+
Message.new.broken!
|
66
|
+
end
|
67
|
+
|
68
|
+
it "is possible to reject a message if broken"do
|
69
|
+
called = []
|
70
|
+
|
71
|
+
message = Message.new
|
72
|
+
message.on_rejected_broken do |msg|
|
73
|
+
called << msg
|
74
|
+
end
|
75
|
+
|
76
|
+
message.on_rejected(handler_that_should_never_be_called)
|
77
|
+
message.on_consumed(handler_that_should_never_be_called)
|
78
|
+
|
79
|
+
message.broken!
|
80
|
+
|
81
|
+
called.should==([message])
|
82
|
+
end
|
83
|
+
|
84
|
+
it "is possible to reject a message if consumer cant handle it" do
|
85
|
+
called = []
|
86
|
+
|
87
|
+
message = Message.new
|
88
|
+
message.on_rejected do |msg|
|
89
|
+
called << msg
|
90
|
+
end
|
91
|
+
|
92
|
+
message.on_rejected_broken(handler_that_should_never_be_called)
|
93
|
+
message.on_consumed(handler_that_should_never_be_called)
|
94
|
+
|
95
|
+
message.rejected!
|
96
|
+
|
97
|
+
called.should==([message])
|
98
|
+
end
|
99
|
+
|
100
|
+
it "is possible to mark a message as consumed" do
|
101
|
+
called = []
|
102
|
+
|
103
|
+
message = Message.new
|
104
|
+
message.on_consumed do |msg|
|
105
|
+
called << msg
|
106
|
+
end
|
107
|
+
|
108
|
+
message.on_rejected_broken(handler_that_should_never_be_called)
|
109
|
+
message.on_rejected(handler_that_should_never_be_called)
|
110
|
+
|
111
|
+
message.consumed!
|
112
|
+
|
113
|
+
called.should==([message])
|
114
|
+
end
|
115
|
+
|
116
|
+
it "is not possible to change a message after marking as consumed or rejected" do
|
117
|
+
read = lambda { |m| m[:some_key] }
|
118
|
+
mutate = lambda { |m| m[:some_key] = :some_value }
|
119
|
+
|
120
|
+
consumed = Message.new
|
121
|
+
consumed.consumed!
|
122
|
+
|
123
|
+
rejected = Message.new
|
124
|
+
rejected.rejected!
|
125
|
+
|
126
|
+
broken = Message.new
|
127
|
+
broken.broken!
|
128
|
+
|
129
|
+
lambda{ read.call(consumed) }.should_not raise_error
|
130
|
+
lambda{ mutate.call(consumed) }.should raise_error
|
131
|
+
lambda{ read.call(rejected) }.should_not raise_error
|
132
|
+
lambda{ mutate.call(rejected) }.should raise_error
|
133
|
+
lambda{ read.call(broken) }.should_not raise_error
|
134
|
+
lambda{ mutate.call(broken) }.should raise_error
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'empipelines/periodic_event_source'
|
2
|
+
|
3
|
+
module Pipelines
|
4
|
+
describe PeriodicEventSource do
|
5
|
+
it 'schedules itself with eventmachine' do
|
6
|
+
em = stub('eventmachine')
|
7
|
+
em.should_receive(:add_periodic_timer).with(5)
|
8
|
+
PeriodicEventSource.new(em, 5){ "something cool!" }.start!
|
9
|
+
end
|
10
|
+
|
11
|
+
it 'sends the result of the periodic action to the handler' do
|
12
|
+
expected_message = { :something => "goes here" }
|
13
|
+
received_messages = []
|
14
|
+
|
15
|
+
source = PeriodicEventSource.new(stub('eventmachine'), 666){ expected_message }
|
16
|
+
source.on_event { |msg| received_messages << msg}
|
17
|
+
source.tick!
|
18
|
+
|
19
|
+
received_messages.should eql([expected_message])
|
20
|
+
end
|
21
|
+
|
22
|
+
it 'doesnt o anything if no event was generated' do
|
23
|
+
source = PeriodicEventSource.new(stub('eventmachine'), 666){ nil }
|
24
|
+
source.on_event { |m| raise "should not be called" }
|
25
|
+
source.tick!
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
require "empipelines/pipeline"
|
2
|
+
|
3
|
+
module Pipelines
|
4
|
+
class AddOne
|
5
|
+
def call(input, &next_stage)
|
6
|
+
next_stage.call({:data => (input[:data] + 1)})
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
class SquareIt
|
11
|
+
def call(input, &next_stage)
|
12
|
+
next_stage.call({:data => (input[:data] * input[:data])})
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
class BrokenStage
|
17
|
+
def call(ignore, &ignored_too)
|
18
|
+
raise "Boo!"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
class DeadEnd
|
23
|
+
def call(input, &also_ignore)
|
24
|
+
#noop
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
class NeedsAnApple
|
29
|
+
def call(input, &next_stage)
|
30
|
+
next_stage.call(input.merge({:apple => apple}))
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
class NeedsAnOrange
|
35
|
+
def call(input, &next_stage)
|
36
|
+
next_stage.call(input.merge({:orange => orange}))
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
class GlobalHolder
|
41
|
+
@@value = nil
|
42
|
+
def GlobalHolder.held
|
43
|
+
@@value
|
44
|
+
end
|
45
|
+
|
46
|
+
def initialize
|
47
|
+
@@value = nil
|
48
|
+
end
|
49
|
+
|
50
|
+
def call(input, &next_step)
|
51
|
+
@@value = input
|
52
|
+
next_step.call(self.class)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
class StubSpawner
|
57
|
+
|
58
|
+
class StubProcess
|
59
|
+
def initialize(block)
|
60
|
+
@block = block
|
61
|
+
end
|
62
|
+
|
63
|
+
def notify(input)
|
64
|
+
@block.call(input)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def spawn(&block)
|
69
|
+
StubProcess.new(block)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
describe Pipeline do
|
74
|
+
let(:logger) {stub(:info => true, :debug => true)}
|
75
|
+
it "chains the actions using processes" do
|
76
|
+
event_chain = [AddOne, SquareIt, GlobalHolder]
|
77
|
+
pipelines = Pipeline.new(StubSpawner.new, {}, stub('monitoring'), logger)
|
78
|
+
pipeline = pipelines.for(event_chain)
|
79
|
+
pipeline.notify({:data =>1})
|
80
|
+
GlobalHolder.held.should eql({:data => 4})
|
81
|
+
end
|
82
|
+
|
83
|
+
it "does not send to the next if last returned nil" do
|
84
|
+
event_chain = [AddOne, SquareIt, DeadEnd, GlobalHolder]
|
85
|
+
pipelines = Pipeline.new(StubSpawner.new, {}, stub('monitoring'), logger)
|
86
|
+
pipeline = pipelines.for(event_chain)
|
87
|
+
pipeline.notify({:data => 1})
|
88
|
+
GlobalHolder.held.should be_nil
|
89
|
+
end
|
90
|
+
|
91
|
+
it "makes all objects in the context object available to stages" do
|
92
|
+
event_chain = [NeedsAnApple, NeedsAnOrange, GlobalHolder]
|
93
|
+
pipelines = Pipeline.new(StubSpawner.new, {:apple => :some_apple, :orange => :some_orange}, stub('monitoring'), logger)
|
94
|
+
pipeline = pipelines.for(event_chain)
|
95
|
+
pipeline.notify({})
|
96
|
+
GlobalHolder.held.should eql({:apple => :some_apple, :orange => :some_orange})
|
97
|
+
end
|
98
|
+
|
99
|
+
it "sends exception to the proper handler" do
|
100
|
+
monitoring = mock()
|
101
|
+
monitoring.should_receive(:inform_exception!)
|
102
|
+
pipeline = Pipeline.new(StubSpawner.new, {}, monitoring, logger)
|
103
|
+
pipeline.for([BrokenStage]).notify({})
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
data/spec/spec_helper.rb
ADDED
File without changes
|
@@ -0,0 +1,54 @@
|
|
1
|
+
def request(path = path)
|
2
|
+
responses = []
|
3
|
+
described_class.request(path) { |response| responses << response }
|
4
|
+
responses
|
5
|
+
end
|
6
|
+
|
7
|
+
def process(message = message, stage_class = described_class)
|
8
|
+
messages = []
|
9
|
+
stage_instance = stage_class.new
|
10
|
+
|
11
|
+
#TODO: should this be here? Is there an easy way?
|
12
|
+
context.each {|k,v| stage_instance.define_singleton_method(k) {v} }
|
13
|
+
|
14
|
+
stage_instance.call(message) { |message| messages << message }
|
15
|
+
messages
|
16
|
+
end
|
17
|
+
|
18
|
+
RSpec::Matchers.define :return_response do |expected|
|
19
|
+
match do |responses|
|
20
|
+
responses.include?(expected)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
RSpec::Matchers.define :return_any_response do |expected|
|
25
|
+
match do |responses|
|
26
|
+
responses.any?
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
RSpec::Matchers.define :send_messages_including do |expected|
|
31
|
+
match do |messages|
|
32
|
+
messages.any? do |message|
|
33
|
+
expected.all? do |key, value|
|
34
|
+
message[key] == value
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
RSpec::Matchers.define :send_some_message do |expected|
|
41
|
+
match do |messages|
|
42
|
+
messages.any?
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
RSpec::Matchers.define :send_no_message do |expected|
|
47
|
+
match do |messages|
|
48
|
+
messages.empty?
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
RSpec::Matchers.define :send_message do |expected|
|
53
|
+
match { |messages| messages.include?(expected) }
|
54
|
+
end
|
metadata
ADDED
@@ -0,0 +1,72 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: empipelines
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: '0.1'
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Tobias Schmidt
|
9
|
+
- Phil Calcado
|
10
|
+
autorequire:
|
11
|
+
bindir: bin
|
12
|
+
cert_chain: []
|
13
|
+
date: 2011-12-19 00:00:00.000000000Z
|
14
|
+
dependencies: []
|
15
|
+
description: Simple Event Handling Pipeline Architecture for EventMachine
|
16
|
+
email: pcalcado+empipelines@gmail.com
|
17
|
+
executables: []
|
18
|
+
extensions: []
|
19
|
+
extra_rdoc_files:
|
20
|
+
- README.md
|
21
|
+
files:
|
22
|
+
- Gemfile
|
23
|
+
- Gemfile.lock
|
24
|
+
- README.md
|
25
|
+
- Rakefile
|
26
|
+
- empipelines.gemspec
|
27
|
+
- lib/empipelines.rb
|
28
|
+
- lib/empipelines/amqp_event_source.rb
|
29
|
+
- lib/empipelines/batch_event_source.rb
|
30
|
+
- lib/empipelines/event_pipeline.rb
|
31
|
+
- lib/empipelines/list_event_source.rb
|
32
|
+
- lib/empipelines/message.rb
|
33
|
+
- lib/empipelines/periodic_event_source.rb
|
34
|
+
- lib/empipelines/pipeline.rb
|
35
|
+
- spec/empipelines/amqp_event_source_spec.rb
|
36
|
+
- spec/empipelines/batch_event_source_spec.rb
|
37
|
+
- spec/empipelines/event_pipeline_spec.rb
|
38
|
+
- spec/empipelines/list_event_source_spec.rb
|
39
|
+
- spec/empipelines/message_spec.rb
|
40
|
+
- spec/empipelines/periodic_event_source_spec.rb
|
41
|
+
- spec/empipelines/pipeline_spec.rb
|
42
|
+
- spec/spec_helper.rb
|
43
|
+
- spec/stage_helper.rb
|
44
|
+
homepage: http://github.com/pcalcado/empipelines
|
45
|
+
licenses: []
|
46
|
+
post_install_message:
|
47
|
+
rdoc_options:
|
48
|
+
- --charset=UTF-8
|
49
|
+
require_paths:
|
50
|
+
- lib
|
51
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
52
|
+
none: false
|
53
|
+
requirements:
|
54
|
+
- - ! '>='
|
55
|
+
- !ruby/object:Gem::Version
|
56
|
+
version: '0'
|
57
|
+
segments:
|
58
|
+
- 0
|
59
|
+
hash: -303490141068781308
|
60
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
61
|
+
none: false
|
62
|
+
requirements:
|
63
|
+
- - ! '>='
|
64
|
+
- !ruby/object:Gem::Version
|
65
|
+
version: '0'
|
66
|
+
requirements: []
|
67
|
+
rubyforge_project: empipelines
|
68
|
+
rubygems_version: 1.8.10
|
69
|
+
signing_key:
|
70
|
+
specification_version: 2
|
71
|
+
summary: Simple Event Handling Pipeline Architecture for EventMachine
|
72
|
+
test_files: []
|