empipelines 0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|