serf 0.2.0.alpha1 → 0.3.0
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 +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
|