serf 0.2.0.alpha1 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +7 -1
- data/Gemfile.lock +3 -0
- data/README.md +177 -10
- data/lib/serf/builder.rb +99 -28
- data/lib/serf/error.rb +11 -0
- data/lib/serf/handler.rb +7 -2
- data/lib/serf/message.rb +10 -1
- data/lib/serf/messages/caught_exception_event.rb +46 -0
- data/lib/serf/messages/message_accepted_event.rb +38 -0
- data/lib/serf/runners/direct_runner.rb +74 -0
- data/lib/serf/runners/em_runner.rb +51 -0
- data/lib/serf/serfer.rb +32 -56
- data/lib/serf/util/uuidable.rb +18 -0
- data/lib/serf/version.rb +2 -2
- data/serf.gemspec +18 -6
- metadata +54 -26
data/Gemfile
CHANGED
@@ -3,9 +3,12 @@ source 'http://rubygems.org'
|
|
3
3
|
# Example:
|
4
4
|
# gem 'activesupport', '>= 2.3.5'
|
5
5
|
|
6
|
+
# Requirements for both clients and servers.
|
6
7
|
gem 'activemodel', '~> 3.1.3'
|
7
8
|
gem 'activesupport', '~> 3.1.3'
|
8
|
-
gem '
|
9
|
+
gem 'i18n', '~> 0.6.0' # For ActiveSupport
|
10
|
+
# Used by Serf::Messages::*
|
11
|
+
gem 'uuidtools', '~> 2.1.2'
|
9
12
|
|
10
13
|
# Add dependencies to develop your gem here.
|
11
14
|
# Include everything needed to run rake, tests, features, etc.
|
@@ -20,4 +23,7 @@ group :development, :test do
|
|
20
23
|
#gem 'log4r', '~> 1.1.9'
|
21
24
|
gem 'msgpack', '~> 0.4.6'
|
22
25
|
#gem 'multi_json', '~> 1.0.3'
|
26
|
+
|
27
|
+
# For Server Side of things
|
28
|
+
gem 'eventmachine', '~> 0.12.10'
|
23
29
|
end
|
data/Gemfile.lock
CHANGED
@@ -28,6 +28,7 @@ GEM
|
|
28
28
|
rspec-expectations (2.3.0)
|
29
29
|
diff-lcs (~> 1.1.2)
|
30
30
|
rspec-mocks (2.3.0)
|
31
|
+
uuidtools (2.1.2)
|
31
32
|
yard (0.6.8)
|
32
33
|
|
33
34
|
PLATFORMS
|
@@ -38,8 +39,10 @@ DEPENDENCIES
|
|
38
39
|
activesupport (~> 3.1.3)
|
39
40
|
bundler (~> 1.0.0)
|
40
41
|
eventmachine (~> 0.12.10)
|
42
|
+
i18n (~> 0.6.0)
|
41
43
|
jeweler (~> 1.6.4)
|
42
44
|
msgpack (~> 0.4.6)
|
43
45
|
rcov
|
44
46
|
rspec (~> 2.3.0)
|
47
|
+
uuidtools (~> 2.1.2)
|
45
48
|
yard (~> 0.6.0)
|
data/README.md
CHANGED
@@ -5,23 +5,190 @@ Serf is a library that scaffolds distributed systems that are architected using
|
|
5
5
|
Event-Driven Service Oriented Architecture design in combinations with
|
6
6
|
the Command Query Responsibility Separation pattern.
|
7
7
|
|
8
|
-
|
8
|
+
Philosophy
|
9
9
|
==========
|
10
10
|
|
11
|
-
|
12
|
-
|
11
|
+
The underlying idea of Serf is to define a standard Ruby code interface
|
12
|
+
that standalone gems can follow to create a business verticals services.
|
13
|
+
These gems are intended to run as standalone processes in a distributed
|
14
|
+
manner.
|
13
15
|
|
14
|
-
|
16
|
+
Fundamentally, a service creates an abstraction of:
|
17
|
+
1. Messages
|
18
|
+
2. Handlers
|
15
19
|
|
16
|
-
|
17
|
-
|
18
|
-
|
20
|
+
Messages are the representation of data, documents, etc that are
|
21
|
+
marshalled from client to service, which represents the business
|
22
|
+
level interaction of the service.
|
23
|
+
|
24
|
+
Messages may be commands that a service must handle.
|
25
|
+
Messages may be events that a service emits.
|
26
|
+
Messages may be documents that represent state or business data.
|
27
|
+
|
28
|
+
Handlers are the code that is executed over Messages.
|
29
|
+
Handlers may process Command Messages.
|
30
|
+
Handlers may process observed Events that 3rd party services emit.
|
31
|
+
|
32
|
+
Services SHOULD declare a manifest to map messages to handlers, which
|
33
|
+
allows Serf to route messages and to determine blocking or non-blocking modes.
|
34
|
+
|
35
|
+
Serf App and Channels
|
36
|
+
=====================
|
37
|
+
|
38
|
+
A Serf App is a Rack-like application that accepts an ENV hash as input.
|
39
|
+
This ENV hash is simply the hash representation of a message to be processed.
|
40
|
+
The Serf App, as configured by registered manifests, will:
|
41
|
+
1. validate the message
|
42
|
+
2. route the message to the proper handler
|
43
|
+
3. run the handler in blocking or non-blocking mode.
|
44
|
+
4. publish the handler's results to results or error channels.
|
45
|
+
a. These channels are normally message queuing channels.
|
46
|
+
b. We only require the channel instance to respond to the 'publish'
|
47
|
+
method and accept a message (or message hash) as the argument.
|
48
|
+
5. return the handler's results to the caller if it is blocking mode.
|
49
|
+
a. In non-blocking mode, an MessageAcceptedEvent is returned instead
|
50
|
+
because the message will be run by the EventMachine deferred threadpool
|
51
|
+
by default.
|
52
|
+
|
53
|
+
Not implemented as yet:
|
54
|
+
1. Create a message queue listener or web endpoint to accept messages
|
55
|
+
and run them through the built Serf App.
|
56
|
+
2. Message Queue channels to route responses to pub/sub locations.
|
57
|
+
|
58
|
+
Service Libraries
|
59
|
+
=================
|
60
|
+
|
61
|
+
1. Service Libraries SHOULD implement message classes that include the
|
62
|
+
::Serf::Message helper module.
|
63
|
+
a. Barring that, they should conform to the idea that messages may be
|
64
|
+
hashes that define at least one attribute: 'kind'.
|
65
|
+
2. Serialization of the messages SHOULD BE Json or MessagePack (I hate XML).
|
66
|
+
3. Service Libraries SHOULD implement handler classes that include the
|
67
|
+
::Serf::Handler helper module.
|
68
|
+
4. Handler methods MUST receive a message as an options hash, symbolized keys.
|
69
|
+
5. Handler methods MUST return zero or more messages.
|
70
|
+
6. Handler methods SHOULD handle catch their business logic exceptions and
|
71
|
+
return them as specialized messages that can be forwarded down error channels.
|
72
|
+
Uncaught exceptions that are then caught by Serf are published as
|
73
|
+
generic Serf::CaughtExceptionEvents, and are harder to deal with.
|
74
|
+
|
75
|
+
Example
|
76
|
+
=======
|
77
|
+
|
78
|
+
# Require our libraries
|
79
|
+
require 'log4r'
|
80
|
+
require 'serf/handler'
|
81
|
+
require 'serf/message'
|
82
|
+
require 'serf/builder'
|
83
|
+
|
84
|
+
# create a simple logger for this example
|
85
|
+
logger = Log4r::Logger.new 'my_logger'
|
86
|
+
logger.outputters = Log4r::FileOutputter.new(
|
87
|
+
'fileOutputter',
|
88
|
+
filename: 'log.txt')
|
89
|
+
|
90
|
+
# my_lib/my_handler.rb
|
91
|
+
class MyHandler
|
92
|
+
include Serf::Handler
|
93
|
+
|
94
|
+
receives(
|
95
|
+
'my_message',
|
96
|
+
with: :submit_my_message)
|
97
|
+
|
98
|
+
def initialize(options={})
|
99
|
+
@logger = options[:logger]
|
100
|
+
end
|
101
|
+
|
102
|
+
def submit_my_message(message={})
|
103
|
+
@logger.info "In Submit Match Result: #{message.inspect.to_s}"
|
104
|
+
# Do work Here...
|
105
|
+
# And return other messages as results of work, or nil for nothing.
|
106
|
+
return nil
|
107
|
+
end
|
108
|
+
|
109
|
+
end
|
110
|
+
|
111
|
+
# my_lib/my_message.rb
|
112
|
+
class MyMessage
|
113
|
+
include Serf::Message
|
114
|
+
|
115
|
+
attr_accessor :data
|
116
|
+
|
117
|
+
validates_presence_of :data
|
118
|
+
|
119
|
+
def initialize(options={})
|
120
|
+
@hi = options[:data] || 'some data here'
|
121
|
+
end
|
19
122
|
|
20
|
-
Airbrake.configure do |config|
|
21
|
-
config.api_key = 'my_api_key'
|
22
123
|
end
|
23
124
|
|
24
|
-
|
125
|
+
# my_lib/manifest.rb
|
126
|
+
MANIFEST = {
|
127
|
+
'my_message' => {
|
128
|
+
# Optional Definition of an implementation class.
|
129
|
+
#message_class => 'other_name_space/my_other_message_implementation',
|
130
|
+
# Declares which handler
|
131
|
+
handler: 'my_handler',
|
132
|
+
# Default is true to process in background.
|
133
|
+
async: true
|
134
|
+
}
|
135
|
+
}
|
136
|
+
|
137
|
+
# Helper class for this example to receive result or error messages
|
138
|
+
# and pipe it into our logger.
|
139
|
+
class MyChannel
|
140
|
+
def initialize(name, logger)
|
141
|
+
@name = name
|
142
|
+
@logger = logger
|
143
|
+
end
|
144
|
+
def publish(message)
|
145
|
+
@logger.info "#{@name} #{message}"
|
146
|
+
#@logger.info message
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
# Create a new builder for this serf app.
|
151
|
+
builder = Serf::Builder.new do
|
152
|
+
# Registers different service libary manifests.
|
153
|
+
register MANIFEST
|
154
|
+
# Can define arguments to pass to the 'my_handler' initialize method.
|
155
|
+
config(
|
156
|
+
'my_handler',
|
157
|
+
logger: logger)
|
158
|
+
# Create result and error channels for the handler result messages.
|
159
|
+
error_channel MyChannel.new('errorchannel: ', logger)
|
160
|
+
results_channel MyChannel.new('resultschannel: ', logger)
|
161
|
+
# We pass in a logger to our Serf code: Serfer and Runners.
|
162
|
+
logger logger
|
163
|
+
# Optionally define a not found handler...
|
164
|
+
# Defaults to raising an ArgumentError, 'Not Found'.
|
165
|
+
#not_found lambda {|x| puts x}
|
166
|
+
end
|
167
|
+
app = builder.to_app
|
168
|
+
|
169
|
+
# Start event machine loop.
|
170
|
+
EM.run do
|
171
|
+
# On the next tick
|
172
|
+
EM.next_tick do
|
173
|
+
logger.info "Start Tick #{Thread.current.object_id}"
|
174
|
+
# This will submit a 'my_message' message (as a hash) to the Serf App.
|
175
|
+
results = app.call('kind' => 'my_message')
|
176
|
+
# Because we declared the 'my_message' kind to be handled async, we
|
177
|
+
# should get a MessageAcceptedEvent as the results.
|
178
|
+
logger.info "In Tick Results: #{results.inspect}"
|
179
|
+
begin
|
180
|
+
# Here, we're going to submit a message that we don't handle.
|
181
|
+
# By default, an exception will be raised.
|
182
|
+
app.call('kind' => 'unhandled_message_kind')
|
183
|
+
rescue => e
|
184
|
+
puts "Caught in Tick: #{e.inspect}"
|
185
|
+
end
|
186
|
+
logger.info "End Tick #{Thread.current.object_id}"
|
187
|
+
end
|
188
|
+
EM.add_timer 2 do
|
189
|
+
EM.stop
|
190
|
+
end
|
191
|
+
end
|
25
192
|
|
26
193
|
|
27
194
|
Contributing to serf
|
data/lib/serf/builder.rb
CHANGED
@@ -1,4 +1,7 @@
|
|
1
1
|
require 'serf/serfer'
|
2
|
+
require 'serf/runners/direct_runner'
|
3
|
+
require 'serf/runners/em_runner'
|
4
|
+
require 'serf/util/null_object'
|
2
5
|
|
3
6
|
module Serf
|
4
7
|
|
@@ -25,13 +28,27 @@ module Serf
|
|
25
28
|
return builder
|
26
29
|
end
|
27
30
|
|
28
|
-
def initialize(
|
29
|
-
|
31
|
+
def initialize(app=nil, &block)
|
32
|
+
# Configuration about the routes and apps to run.
|
30
33
|
@manifest = {}
|
31
34
|
@config = {}
|
32
|
-
@not_found =
|
33
|
-
|
34
|
-
|
35
|
+
@not_found = app
|
36
|
+
|
37
|
+
# Implementing classes of our app
|
38
|
+
# NOTE: runner_class and async_runner_class are only used if actual
|
39
|
+
# runner instances are omitted in the configuration.
|
40
|
+
@serfer_class = ::Serf::Serfer
|
41
|
+
@serfer_options = {}
|
42
|
+
@runner_class = ::Serf::Runners::DirectRunner
|
43
|
+
@async_runner_class = ::Serf::Runners::EmRunner
|
44
|
+
|
45
|
+
# Utility and messaging channels for our Runners
|
46
|
+
# NOTE: these are only used if the builder needs to instantiage runners.
|
47
|
+
@results_channel = ::Serf::Util::NullObject.new
|
48
|
+
@error_channel = ::Serf::Util::NullObject.new
|
49
|
+
@logger = ::Serf::Util::NullObject.new
|
50
|
+
|
51
|
+
# configure based on a given block.
|
35
52
|
instance_eval(&block) if block_given?
|
36
53
|
end
|
37
54
|
|
@@ -39,10 +56,6 @@ module Serf
|
|
39
56
|
self.new(default_app, &block).to_app
|
40
57
|
end
|
41
58
|
|
42
|
-
def use(middleware, *args, &block)
|
43
|
-
@use << proc { |app| middleware.new(app, *args, &block) }
|
44
|
-
end
|
45
|
-
|
46
59
|
def register(manifest)
|
47
60
|
@manifest.merge! manifest
|
48
61
|
end
|
@@ -55,18 +68,42 @@ module Serf
|
|
55
68
|
@not_found = app
|
56
69
|
end
|
57
70
|
|
58
|
-
def
|
59
|
-
|
60
|
-
|
71
|
+
def serfer_class(serfer_class)
|
72
|
+
@serfer_class = serfer_class
|
73
|
+
end
|
74
|
+
|
75
|
+
def serfer_class(serfer_options)
|
76
|
+
@serfer_options = serfer_options
|
77
|
+
end
|
78
|
+
|
79
|
+
def runner(runner)
|
80
|
+
@runner = runner
|
81
|
+
end
|
82
|
+
|
83
|
+
def async_runner(runner)
|
84
|
+
@async_runner = runner
|
85
|
+
end
|
86
|
+
|
87
|
+
def results_channel(results_channel)
|
88
|
+
@results_channel = results_channel
|
89
|
+
end
|
90
|
+
|
91
|
+
def error_channel(error_channel)
|
92
|
+
@error_channel = error_channel
|
61
93
|
end
|
62
94
|
|
63
|
-
|
95
|
+
def logger(logger)
|
96
|
+
@logger = logger
|
97
|
+
end
|
64
98
|
|
65
|
-
def
|
99
|
+
def to_app
|
100
|
+
# Our async and sync messages & handlers.
|
66
101
|
kinds = {}
|
67
102
|
handlers = {}
|
103
|
+
async_kinds = {}
|
68
104
|
async_handlers = {}
|
69
105
|
|
106
|
+
# Iterate our manifests to build out handlers and message classes
|
70
107
|
@manifest.each do |kind, options|
|
71
108
|
# Instantiate our handler with any possible configuration.
|
72
109
|
handler_str = options.fetch(:handler)
|
@@ -74,28 +111,62 @@ module Serf
|
|
74
111
|
args, block = @config.fetch(handler_str) { [[], nil] }
|
75
112
|
handler = handler_class.new *args, &block
|
76
113
|
|
77
|
-
# Then put it into the proper map of handlers for either
|
78
|
-
# synchronous or asynchronous processing.
|
79
|
-
async = options.fetch(:async) { true }
|
80
|
-
(async ? async_handlers : handlers)[kind] = handler
|
81
|
-
|
82
114
|
# Get the implementing message serialization class.
|
83
115
|
# For a given message kind, we may have a different (or nil)
|
84
116
|
# implementing class. If nil, then we're not going to try to
|
85
117
|
# create a message class to validate before passing to handler.
|
86
118
|
message_class = options.fetch(:message_class) { kind }
|
87
|
-
|
119
|
+
message_class = message_class && message_class.camelize.constantize
|
120
|
+
|
121
|
+
# Put handlers and kinds into the proper map of handlers for either
|
122
|
+
# synchronous or asynchronous processing.
|
123
|
+
async = options.fetch(:async) { true }
|
124
|
+
if async
|
125
|
+
async_kinds[kind] = message_class if message_class
|
126
|
+
async_handlers[kind] = handler
|
127
|
+
else
|
128
|
+
kinds[kind] = message_class if message_class
|
129
|
+
handlers[kind] = handler
|
130
|
+
end
|
88
131
|
end
|
89
132
|
|
90
|
-
#
|
91
|
-
|
92
|
-
@
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
133
|
+
# Get or make our runner
|
134
|
+
runner = @runner || @runner_class.new(
|
135
|
+
results_channel: @results_channel,
|
136
|
+
error_channel: @error_channel,
|
137
|
+
logger: @logger)
|
138
|
+
|
139
|
+
# By default, we go to the not_found app.
|
140
|
+
app = @not_found
|
141
|
+
|
142
|
+
# If we have synchronous handlers, insert before not_found app.
|
143
|
+
if handlers.size > 0
|
144
|
+
# create the serfer class to run synchronous handlers
|
145
|
+
app = @serfer_class.new(
|
146
|
+
@serfer_options.merge(
|
147
|
+
kinds: kinds,
|
148
|
+
handlers: handlers,
|
149
|
+
runner: runner,
|
150
|
+
not_found: app))
|
151
|
+
end
|
98
152
|
|
153
|
+
# If we have async handlers, insert before current app stack.
|
154
|
+
if async_handlers.size > 0
|
155
|
+
# Get or make our async wrapper
|
156
|
+
async_runner = @async_runner || @async_runner_class.new(
|
157
|
+
runner: runner,
|
158
|
+
logger: @logger)
|
159
|
+
# create the serfer class to run async handlers
|
160
|
+
app = @serfer_class.new(
|
161
|
+
@serfer_options.merge(
|
162
|
+
kinds: async_kinds,
|
163
|
+
handlers: async_handlers,
|
164
|
+
runner: async_runner,
|
165
|
+
not_found: app))
|
166
|
+
end
|
167
|
+
|
168
|
+
return app
|
169
|
+
end
|
99
170
|
end
|
100
171
|
|
101
172
|
end
|
data/lib/serf/error.rb
ADDED
data/lib/serf/handler.rb
CHANGED
@@ -4,6 +4,8 @@ require 'active_support/core_ext/hash/keys'
|
|
4
4
|
require 'active_support/core_ext/object/blank'
|
5
5
|
require 'active_support/ordered_options'
|
6
6
|
|
7
|
+
require 'serf/error'
|
8
|
+
|
7
9
|
module Serf
|
8
10
|
|
9
11
|
module Handler
|
@@ -37,14 +39,17 @@ module Serf
|
|
37
39
|
#
|
38
40
|
def call(env={})
|
39
41
|
# Just to stringify the environment keys
|
40
|
-
env = env.
|
42
|
+
env = env.symbolize_keys
|
41
43
|
# Make sure a kind was set, and that we can handle it.
|
42
|
-
message_kind = env[
|
44
|
+
message_kind = env[:kind]
|
43
45
|
raise ArgumentError, 'No "kind" in call env' if message_kind.blank?
|
44
46
|
method = self.class.serf_actions[message_kind]
|
45
47
|
raise ArgumentError, "#{message_kind} not found" if method.blank?
|
46
48
|
# Now execute the method with the environment parameters
|
47
49
|
self.send method, env
|
50
|
+
rescue => e
|
51
|
+
e.extend ::Serf::Error
|
52
|
+
raise e
|
48
53
|
end
|
49
54
|
end
|
50
55
|
|
data/lib/serf/message.rb
CHANGED
@@ -19,13 +19,14 @@ module Serf
|
|
19
19
|
included do
|
20
20
|
class_attribute :kind
|
21
21
|
send 'kind=', self.to_s.tableize.singularize
|
22
|
+
self.include_root_in_json = false
|
22
23
|
end
|
23
24
|
|
24
25
|
module InstanceMethods
|
25
26
|
|
26
27
|
def attributes
|
27
28
|
{
|
28
|
-
|
29
|
+
:kind => kind
|
29
30
|
}
|
30
31
|
end
|
31
32
|
|
@@ -39,6 +40,14 @@ module Serf
|
|
39
40
|
|
40
41
|
end
|
41
42
|
|
43
|
+
module ClassMethods
|
44
|
+
|
45
|
+
def parse(*args)
|
46
|
+
self.new *args
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
50
|
+
|
42
51
|
end
|
43
52
|
|
44
53
|
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'serf/message'
|
2
|
+
require 'serf/util/uuidable'
|
3
|
+
|
4
|
+
module Serf
|
5
|
+
module Messages
|
6
|
+
|
7
|
+
##
|
8
|
+
# An event message to signal that the serf code caught an
|
9
|
+
# exception during the processing of some message, which is
|
10
|
+
# represented by the context field.
|
11
|
+
#
|
12
|
+
# Instances of this class are norminally published to an
|
13
|
+
# error channel for out of band processing/notification.
|
14
|
+
#
|
15
|
+
class CaughtExceptionEvent
|
16
|
+
include ::Serf::Message
|
17
|
+
include ::Serf::Util::Uuidable
|
18
|
+
|
19
|
+
attr_accessor :context
|
20
|
+
attr_accessor :error_message
|
21
|
+
attr_accessor :error_backtrace
|
22
|
+
|
23
|
+
def initialize(options={})
|
24
|
+
@context = options[:context]
|
25
|
+
@error_message = options[:error_message]
|
26
|
+
@error_backtrace = options[:error_backtrace]
|
27
|
+
@uuid = options[:uuid]
|
28
|
+
end
|
29
|
+
|
30
|
+
def attributes
|
31
|
+
{
|
32
|
+
'context' => @context,
|
33
|
+
'error_message' => @error_message,
|
34
|
+
'error_backtrace' => @error_backtrace,
|
35
|
+
'uuid' => uuid
|
36
|
+
}.merge!(super)
|
37
|
+
end
|
38
|
+
|
39
|
+
def to_s
|
40
|
+
attributes.to_s
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'serf/message'
|
2
|
+
require 'serf/util/uuidable'
|
3
|
+
|
4
|
+
module Serf
|
5
|
+
module Messages
|
6
|
+
|
7
|
+
##
|
8
|
+
# This event signals that the serf process has accepted a message
|
9
|
+
# for processing at a later time. These events are normally sent
|
10
|
+
# in response to calling clients, which is a separate signal
|
11
|
+
# than any errors given.
|
12
|
+
#
|
13
|
+
class MessageAcceptedEvent
|
14
|
+
include ::Serf::Message
|
15
|
+
include ::Serf::Util::Uuidable
|
16
|
+
|
17
|
+
attr_accessor :message
|
18
|
+
|
19
|
+
def initialize(options={})
|
20
|
+
@message = options[:message]
|
21
|
+
@uuid = options[:uuid]
|
22
|
+
end
|
23
|
+
|
24
|
+
def attributes
|
25
|
+
{
|
26
|
+
'message' => @message,
|
27
|
+
'uuid' => uuid
|
28
|
+
}.merge!(super)
|
29
|
+
end
|
30
|
+
|
31
|
+
def to_s
|
32
|
+
attributes.to_s
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
require 'serf/messages/caught_exception_event'
|
2
|
+
require 'serf/util/null_object'
|
3
|
+
|
4
|
+
module Serf
|
5
|
+
module Runners
|
6
|
+
|
7
|
+
##
|
8
|
+
# Direct runner drives the execution of a handler for given messages.
|
9
|
+
# This class deals with error handling and publishing handler results
|
10
|
+
# to proper error or results channels.
|
11
|
+
#
|
12
|
+
class DirectRunner
|
13
|
+
|
14
|
+
def initialize(options={})
|
15
|
+
# Mandatory, we want both results and error channels.
|
16
|
+
@results_channel = options.fetch(:results_channel)
|
17
|
+
@error_channel = options.fetch(:error_channel)
|
18
|
+
|
19
|
+
# For caught exceptions, we're going to publish an error event
|
20
|
+
# over our error channel. This defines our error event message class.
|
21
|
+
@error_event_class = options.fetch(:error_event_class) {
|
22
|
+
::Serf::Messages::CaughtExceptionEvent
|
23
|
+
}
|
24
|
+
|
25
|
+
# Our default logger
|
26
|
+
@logger = options.fetch(:logger) { ::Serf::Util::NullObject.new }
|
27
|
+
end
|
28
|
+
|
29
|
+
def run(handler, params)
|
30
|
+
with_error_handling(params) do
|
31
|
+
results = handler.call params
|
32
|
+
publish_results results
|
33
|
+
return results
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
protected
|
38
|
+
|
39
|
+
##
|
40
|
+
# Loop over the results and publish them to the results channel.
|
41
|
+
# Any error in publishing individual messages will result in
|
42
|
+
# a log event and an error channel event.
|
43
|
+
def publish_results(results)
|
44
|
+
results = Array(results)
|
45
|
+
results.each do |message|
|
46
|
+
with_error_handling(message) do
|
47
|
+
@results_channel.publish message
|
48
|
+
end
|
49
|
+
end
|
50
|
+
return nil
|
51
|
+
end
|
52
|
+
|
53
|
+
##
|
54
|
+
# A block wrapper to handle errors when executing a block.
|
55
|
+
#
|
56
|
+
def with_error_handling(context=nil)
|
57
|
+
yield
|
58
|
+
rescue => e
|
59
|
+
error_event = @error_event_class.new(
|
60
|
+
context: context,
|
61
|
+
error_message: e.inspect,
|
62
|
+
error_backtrace: e.backtrace.join("\n"))
|
63
|
+
|
64
|
+
# log the error to our logger, and to our error channel.
|
65
|
+
@logger.error error_event
|
66
|
+
@error_channel.publish error_event
|
67
|
+
|
68
|
+
# We're done, so just return this error.
|
69
|
+
return error_event
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'eventmachine'
|
2
|
+
|
3
|
+
require 'serf/messages/message_accepted_event'
|
4
|
+
|
5
|
+
module Serf
|
6
|
+
module Runners
|
7
|
+
|
8
|
+
##
|
9
|
+
# This runner simply wraps another runner to execute in the
|
10
|
+
# EventMachine deferred threadpool.
|
11
|
+
#
|
12
|
+
# NOTE: Because the Serfer class validates messages before
|
13
|
+
# sending them to runners (and handlers), this class simply
|
14
|
+
# responds to the calling client with an 'MessageAcceptedEvent'
|
15
|
+
# to signal that the message will be processed later.
|
16
|
+
#
|
17
|
+
# Errors caught here will simply be logged. This is because
|
18
|
+
# the wrapped runner *MUST* handle its own errors. If an error
|
19
|
+
# should propagate up here, then it was most likely an error
|
20
|
+
# that occurred in a rescue block... we don't want to complicate
|
21
|
+
# any extra publishing to error channels because that may have
|
22
|
+
# been the cause of the error.
|
23
|
+
#
|
24
|
+
class EmRunner
|
25
|
+
|
26
|
+
def initialize(options={})
|
27
|
+
# Manditory: Need a runner because EmRunner is just a wrapper.
|
28
|
+
@runner = options.fetch(:runner)
|
29
|
+
|
30
|
+
@mae_class = options.fetch(:message_accepted_event_class) {
|
31
|
+
::Serf::Messages::MessageAcceptedEvent
|
32
|
+
}
|
33
|
+
@evm = options.fetch(:event_machine) { ::EventMachine }
|
34
|
+
@logger = options.fetch(:logger) { ::Serf::Util::NullObject.new }
|
35
|
+
end
|
36
|
+
|
37
|
+
def run(handler, params)
|
38
|
+
@evm.defer(proc do
|
39
|
+
begin
|
40
|
+
@runner.run handler, params
|
41
|
+
rescue => e
|
42
|
+
@logger.error "#{e.inspect}\n\n#{e.backtrace.join("\n")}"
|
43
|
+
end
|
44
|
+
end)
|
45
|
+
return @mae_class.new message: params
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
end
|
data/lib/serf/serfer.rb
CHANGED
@@ -1,78 +1,54 @@
|
|
1
|
-
require '
|
2
|
-
|
3
|
-
require 'serf/util/null_object'
|
1
|
+
require 'serf/error'
|
4
2
|
|
5
3
|
module Serf
|
6
4
|
|
7
|
-
##
|
8
|
-
# The Serfer is a rack app endpoint that:
|
9
5
|
class Serfer
|
10
6
|
|
11
7
|
def initialize(options={})
|
8
|
+
# Manditory, needs a runner.
|
9
|
+
@runner = options.fetch(:runner)
|
10
|
+
|
12
11
|
# Options for handling the requests
|
13
12
|
@kinds = options[:kinds] || {}
|
14
13
|
@handlers = options[:handlers] || {}
|
15
|
-
@
|
16
|
-
|
17
|
-
|
18
|
-
# Other processing aspects
|
19
|
-
@em = options.fetch(:event_machine) { ::EM }
|
20
|
-
@logger = options.fetch(:logger) { ::Serf::Util::NullObject.new }
|
14
|
+
@not_found = options[:not_found] || proc do
|
15
|
+
raise ArgumentError, 'Handler Not Found'
|
16
|
+
end
|
21
17
|
end
|
22
18
|
|
23
19
|
##
|
24
20
|
# Rack-like call to handle a message
|
25
21
|
#
|
26
22
|
def call(env)
|
27
|
-
|
23
|
+
# We normalize by symbolizing the env keys
|
24
|
+
params = env.symbolize_keys
|
28
25
|
|
29
|
-
#
|
30
|
-
|
31
|
-
|
32
|
-
message_class = @kinds[kind]
|
33
|
-
if message_class
|
34
|
-
message = message_class.new env
|
35
|
-
raise message.errors.full_messages.join('. ') unless message.valid?
|
36
|
-
params = message.attributes
|
37
|
-
else
|
38
|
-
params = env.stringify_keys
|
39
|
-
end
|
40
|
-
|
41
|
-
# Run an asynchronous handler if we have it.
|
42
|
-
handler = @async_handlers[kind]
|
26
|
+
# Pull the kind out of the env.
|
27
|
+
kind = params[:kind]
|
28
|
+
handler = @handlers[kind]
|
43
29
|
if handler
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
30
|
+
# Do a message_class validation if we have it listed.
|
31
|
+
# And use the message attributes instead of raw env when passing
|
32
|
+
# to message handler.
|
33
|
+
message_class = @kinds[kind]
|
34
|
+
if message_class
|
35
|
+
message = message_class.parse params
|
36
|
+
unless message.valid?
|
37
|
+
raise ArgumentError, message.errors.full_messages.join('. ')
|
49
38
|
end
|
50
|
-
|
51
|
-
|
52
|
-
202,
|
53
|
-
{
|
54
|
-
'Content-Type' => 'text/plain'
|
55
|
-
},
|
56
|
-
['Accepted']
|
57
|
-
]
|
58
|
-
end
|
59
|
-
|
60
|
-
# Run a synchronous handler if we have it.
|
61
|
-
handler = @handlers[kind]
|
62
|
-
return handler.call(params) if handler
|
63
|
-
|
64
|
-
# run a not found
|
65
|
-
return @not_found.call(params) if @not_found
|
39
|
+
params = message.attributes.symbolize_keys
|
40
|
+
end
|
66
41
|
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
42
|
+
# Let's run the handler
|
43
|
+
return @runner.run(handler, params) if handler
|
44
|
+
else
|
45
|
+
# we can't handle this kind.
|
46
|
+
return @not_found.call env
|
47
|
+
end
|
48
|
+
rescue => e
|
49
|
+
e.extend(::Serf::Error)
|
50
|
+
raise e
|
76
51
|
end
|
77
52
|
end
|
53
|
+
|
78
54
|
end
|
data/lib/serf/version.rb
CHANGED
data/serf.gemspec
CHANGED
@@ -5,11 +5,11 @@
|
|
5
5
|
|
6
6
|
Gem::Specification.new do |s|
|
7
7
|
s.name = "serf"
|
8
|
-
s.version = "0.
|
8
|
+
s.version = "0.3.0"
|
9
9
|
|
10
|
-
s.required_rubygems_version = Gem::Requirement.new("
|
10
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
11
|
s.authors = ["Benjamin Yu"]
|
12
|
-
s.date = "2011-12-
|
12
|
+
s.date = "2011-12-23"
|
13
13
|
s.description = "Event-Driven SOA with CQRS"
|
14
14
|
s.email = "benjaminlyu@gmail.com"
|
15
15
|
s.extra_rdoc_files = [
|
@@ -27,10 +27,16 @@ Gem::Specification.new do |s|
|
|
27
27
|
"Rakefile",
|
28
28
|
"lib/serf.rb",
|
29
29
|
"lib/serf/builder.rb",
|
30
|
+
"lib/serf/error.rb",
|
30
31
|
"lib/serf/handler.rb",
|
31
32
|
"lib/serf/message.rb",
|
33
|
+
"lib/serf/messages/caught_exception_event.rb",
|
34
|
+
"lib/serf/messages/message_accepted_event.rb",
|
35
|
+
"lib/serf/runners/direct_runner.rb",
|
36
|
+
"lib/serf/runners/em_runner.rb",
|
32
37
|
"lib/serf/serfer.rb",
|
33
38
|
"lib/serf/util/null_object.rb",
|
39
|
+
"lib/serf/util/uuidable.rb",
|
34
40
|
"lib/serf/version.rb",
|
35
41
|
"serf.gemspec",
|
36
42
|
"spec/serf_spec.rb",
|
@@ -48,34 +54,40 @@ Gem::Specification.new do |s|
|
|
48
54
|
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
|
49
55
|
s.add_runtime_dependency(%q<activemodel>, ["~> 3.1.3"])
|
50
56
|
s.add_runtime_dependency(%q<activesupport>, ["~> 3.1.3"])
|
51
|
-
s.add_runtime_dependency(%q<
|
57
|
+
s.add_runtime_dependency(%q<i18n>, ["~> 0.6.0"])
|
58
|
+
s.add_runtime_dependency(%q<uuidtools>, ["~> 2.1.2"])
|
52
59
|
s.add_development_dependency(%q<rspec>, ["~> 2.3.0"])
|
53
60
|
s.add_development_dependency(%q<yard>, ["~> 0.6.0"])
|
54
61
|
s.add_development_dependency(%q<bundler>, ["~> 1.0.0"])
|
55
62
|
s.add_development_dependency(%q<jeweler>, ["~> 1.6.4"])
|
56
63
|
s.add_development_dependency(%q<rcov>, [">= 0"])
|
57
64
|
s.add_development_dependency(%q<msgpack>, ["~> 0.4.6"])
|
65
|
+
s.add_development_dependency(%q<eventmachine>, ["~> 0.12.10"])
|
58
66
|
else
|
59
67
|
s.add_dependency(%q<activemodel>, ["~> 3.1.3"])
|
60
68
|
s.add_dependency(%q<activesupport>, ["~> 3.1.3"])
|
61
|
-
s.add_dependency(%q<
|
69
|
+
s.add_dependency(%q<i18n>, ["~> 0.6.0"])
|
70
|
+
s.add_dependency(%q<uuidtools>, ["~> 2.1.2"])
|
62
71
|
s.add_dependency(%q<rspec>, ["~> 2.3.0"])
|
63
72
|
s.add_dependency(%q<yard>, ["~> 0.6.0"])
|
64
73
|
s.add_dependency(%q<bundler>, ["~> 1.0.0"])
|
65
74
|
s.add_dependency(%q<jeweler>, ["~> 1.6.4"])
|
66
75
|
s.add_dependency(%q<rcov>, [">= 0"])
|
67
76
|
s.add_dependency(%q<msgpack>, ["~> 0.4.6"])
|
77
|
+
s.add_dependency(%q<eventmachine>, ["~> 0.12.10"])
|
68
78
|
end
|
69
79
|
else
|
70
80
|
s.add_dependency(%q<activemodel>, ["~> 3.1.3"])
|
71
81
|
s.add_dependency(%q<activesupport>, ["~> 3.1.3"])
|
72
|
-
s.add_dependency(%q<
|
82
|
+
s.add_dependency(%q<i18n>, ["~> 0.6.0"])
|
83
|
+
s.add_dependency(%q<uuidtools>, ["~> 2.1.2"])
|
73
84
|
s.add_dependency(%q<rspec>, ["~> 2.3.0"])
|
74
85
|
s.add_dependency(%q<yard>, ["~> 0.6.0"])
|
75
86
|
s.add_dependency(%q<bundler>, ["~> 1.0.0"])
|
76
87
|
s.add_dependency(%q<jeweler>, ["~> 1.6.4"])
|
77
88
|
s.add_dependency(%q<rcov>, [">= 0"])
|
78
89
|
s.add_dependency(%q<msgpack>, ["~> 0.4.6"])
|
90
|
+
s.add_dependency(%q<eventmachine>, ["~> 0.12.10"])
|
79
91
|
end
|
80
92
|
end
|
81
93
|
|
metadata
CHANGED
@@ -1,19 +1,19 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: serf
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
5
|
-
prerelease:
|
4
|
+
version: 0.3.0
|
5
|
+
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
8
8
|
- Benjamin Yu
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2011-12-
|
12
|
+
date: 2011-12-23 00:00:00.000000000Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: activemodel
|
16
|
-
requirement: &
|
16
|
+
requirement: &70304875304400 !ruby/object:Gem::Requirement
|
17
17
|
none: false
|
18
18
|
requirements:
|
19
19
|
- - ~>
|
@@ -21,10 +21,10 @@ dependencies:
|
|
21
21
|
version: 3.1.3
|
22
22
|
type: :runtime
|
23
23
|
prerelease: false
|
24
|
-
version_requirements: *
|
24
|
+
version_requirements: *70304875304400
|
25
25
|
- !ruby/object:Gem::Dependency
|
26
26
|
name: activesupport
|
27
|
-
requirement: &
|
27
|
+
requirement: &70304875303800 !ruby/object:Gem::Requirement
|
28
28
|
none: false
|
29
29
|
requirements:
|
30
30
|
- - ~>
|
@@ -32,21 +32,32 @@ dependencies:
|
|
32
32
|
version: 3.1.3
|
33
33
|
type: :runtime
|
34
34
|
prerelease: false
|
35
|
-
version_requirements: *
|
35
|
+
version_requirements: *70304875303800
|
36
36
|
- !ruby/object:Gem::Dependency
|
37
|
-
name:
|
38
|
-
requirement: &
|
37
|
+
name: i18n
|
38
|
+
requirement: &70304875303260 !ruby/object:Gem::Requirement
|
39
39
|
none: false
|
40
40
|
requirements:
|
41
41
|
- - ~>
|
42
42
|
- !ruby/object:Gem::Version
|
43
|
-
version: 0.
|
43
|
+
version: 0.6.0
|
44
44
|
type: :runtime
|
45
45
|
prerelease: false
|
46
|
-
version_requirements: *
|
46
|
+
version_requirements: *70304875303260
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: uuidtools
|
49
|
+
requirement: &70304875302780 !ruby/object:Gem::Requirement
|
50
|
+
none: false
|
51
|
+
requirements:
|
52
|
+
- - ~>
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 2.1.2
|
55
|
+
type: :runtime
|
56
|
+
prerelease: false
|
57
|
+
version_requirements: *70304875302780
|
47
58
|
- !ruby/object:Gem::Dependency
|
48
59
|
name: rspec
|
49
|
-
requirement: &
|
60
|
+
requirement: &70304875302300 !ruby/object:Gem::Requirement
|
50
61
|
none: false
|
51
62
|
requirements:
|
52
63
|
- - ~>
|
@@ -54,10 +65,10 @@ dependencies:
|
|
54
65
|
version: 2.3.0
|
55
66
|
type: :development
|
56
67
|
prerelease: false
|
57
|
-
version_requirements: *
|
68
|
+
version_requirements: *70304875302300
|
58
69
|
- !ruby/object:Gem::Dependency
|
59
70
|
name: yard
|
60
|
-
requirement: &
|
71
|
+
requirement: &70304875301820 !ruby/object:Gem::Requirement
|
61
72
|
none: false
|
62
73
|
requirements:
|
63
74
|
- - ~>
|
@@ -65,10 +76,10 @@ dependencies:
|
|
65
76
|
version: 0.6.0
|
66
77
|
type: :development
|
67
78
|
prerelease: false
|
68
|
-
version_requirements: *
|
79
|
+
version_requirements: *70304875301820
|
69
80
|
- !ruby/object:Gem::Dependency
|
70
81
|
name: bundler
|
71
|
-
requirement: &
|
82
|
+
requirement: &70304875301340 !ruby/object:Gem::Requirement
|
72
83
|
none: false
|
73
84
|
requirements:
|
74
85
|
- - ~>
|
@@ -76,10 +87,10 @@ dependencies:
|
|
76
87
|
version: 1.0.0
|
77
88
|
type: :development
|
78
89
|
prerelease: false
|
79
|
-
version_requirements: *
|
90
|
+
version_requirements: *70304875301340
|
80
91
|
- !ruby/object:Gem::Dependency
|
81
92
|
name: jeweler
|
82
|
-
requirement: &
|
93
|
+
requirement: &70304875206820 !ruby/object:Gem::Requirement
|
83
94
|
none: false
|
84
95
|
requirements:
|
85
96
|
- - ~>
|
@@ -87,10 +98,10 @@ dependencies:
|
|
87
98
|
version: 1.6.4
|
88
99
|
type: :development
|
89
100
|
prerelease: false
|
90
|
-
version_requirements: *
|
101
|
+
version_requirements: *70304875206820
|
91
102
|
- !ruby/object:Gem::Dependency
|
92
103
|
name: rcov
|
93
|
-
requirement: &
|
104
|
+
requirement: &70304875206340 !ruby/object:Gem::Requirement
|
94
105
|
none: false
|
95
106
|
requirements:
|
96
107
|
- - ! '>='
|
@@ -98,10 +109,10 @@ dependencies:
|
|
98
109
|
version: '0'
|
99
110
|
type: :development
|
100
111
|
prerelease: false
|
101
|
-
version_requirements: *
|
112
|
+
version_requirements: *70304875206340
|
102
113
|
- !ruby/object:Gem::Dependency
|
103
114
|
name: msgpack
|
104
|
-
requirement: &
|
115
|
+
requirement: &70304875205860 !ruby/object:Gem::Requirement
|
105
116
|
none: false
|
106
117
|
requirements:
|
107
118
|
- - ~>
|
@@ -109,7 +120,18 @@ dependencies:
|
|
109
120
|
version: 0.4.6
|
110
121
|
type: :development
|
111
122
|
prerelease: false
|
112
|
-
version_requirements: *
|
123
|
+
version_requirements: *70304875205860
|
124
|
+
- !ruby/object:Gem::Dependency
|
125
|
+
name: eventmachine
|
126
|
+
requirement: &70304875205280 !ruby/object:Gem::Requirement
|
127
|
+
none: false
|
128
|
+
requirements:
|
129
|
+
- - ~>
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: 0.12.10
|
132
|
+
type: :development
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: *70304875205280
|
113
135
|
description: Event-Driven SOA with CQRS
|
114
136
|
email: benjaminlyu@gmail.com
|
115
137
|
executables: []
|
@@ -128,10 +150,16 @@ files:
|
|
128
150
|
- Rakefile
|
129
151
|
- lib/serf.rb
|
130
152
|
- lib/serf/builder.rb
|
153
|
+
- lib/serf/error.rb
|
131
154
|
- lib/serf/handler.rb
|
132
155
|
- lib/serf/message.rb
|
156
|
+
- lib/serf/messages/caught_exception_event.rb
|
157
|
+
- lib/serf/messages/message_accepted_event.rb
|
158
|
+
- lib/serf/runners/direct_runner.rb
|
159
|
+
- lib/serf/runners/em_runner.rb
|
133
160
|
- lib/serf/serfer.rb
|
134
161
|
- lib/serf/util/null_object.rb
|
162
|
+
- lib/serf/util/uuidable.rb
|
135
163
|
- lib/serf/version.rb
|
136
164
|
- serf.gemspec
|
137
165
|
- spec/serf_spec.rb
|
@@ -151,13 +179,13 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
151
179
|
version: '0'
|
152
180
|
segments:
|
153
181
|
- 0
|
154
|
-
hash:
|
182
|
+
hash: -3406748221232182748
|
155
183
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
156
184
|
none: false
|
157
185
|
requirements:
|
158
|
-
- - ! '
|
186
|
+
- - ! '>='
|
159
187
|
- !ruby/object:Gem::Version
|
160
|
-
version:
|
188
|
+
version: '0'
|
161
189
|
requirements: []
|
162
190
|
rubyforge_project:
|
163
191
|
rubygems_version: 1.8.10
|