serf 0.4.1 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -65,12 +65,10 @@ Service Libraries
65
65
  hashes that define at least one attribute: 'kind'.
66
66
  2. Serialization of the messages SHOULD BE Json or MessagePack (I hate XML).
67
67
  a. `to_hash` is also included.
68
- 3. Service Libraries SHOULD implement handler classes that include the
69
- ::Serf::Handler helper module.
70
- 4. Handler methods MUST receive a message as either as an options hash
68
+ 3. Handler methods MUST receive a message as either as an options hash
71
69
  (with symbolized keys) or an instance of a declared Message.
72
- 5. Handler methods MUST return zero or more messages.
73
- 6. Handler methods SHOULD handle catch their business logic exceptions and
70
+ 4. Handler methods MUST return zero or more messages.
71
+ 5. Handler methods SHOULD handle catch their business logic exceptions and
74
72
  return them as specialized messages that can be forwarded down error channels.
75
73
  Uncaught exceptions that are then caught by Serf are published as
76
74
  generic Serf::CaughtExceptionEvents, and are harder to deal with.
@@ -105,7 +103,7 @@ aware that:
105
103
  1. Message classes MUST implement `Message.parse(env={})` class method
106
104
  if said message class is to be used as the target object representation
107
105
  of a received message (from ENV hash).
108
- a. Serf::Handler code makes this assumption when it finds an ENV
106
+ a. Serf code makes this assumption when it finds an ENV
109
107
  hash that is to be parsed into a message object.
110
108
 
111
109
 
@@ -115,15 +113,35 @@ Example
115
113
  # Require our libraries
116
114
  require 'active_model'
117
115
  require 'log4r'
118
- require 'serf/handler'
119
- require 'serf/message'
116
+ require 'json'
117
+
120
118
  require 'serf/builder'
119
+ require 'serf/message'
120
+ require 'serf/middleware/uuid_tagger'
121
121
 
122
122
  # create a simple logger for this example
123
- logger = Log4r::Logger.new 'my_logger'
124
- logger.outputters = Log4r::FileOutputter.new(
123
+ outputter = Log4r::FileOutputter.new(
125
124
  'fileOutputter',
126
125
  filename: 'log.txt')
126
+ ['top_level',
127
+ 'serf',
128
+ 'handler',
129
+ 'results_channel',
130
+ 'error_channel'].each do |name|
131
+ logger = Log4r::Logger.new name
132
+ logger.outputters = outputter
133
+ end
134
+
135
+ # Helper class for this example to receive result or error messages
136
+ # and pipe it into our logger.
137
+ class MyChannel
138
+ def initialize(logger)
139
+ @logger = logger
140
+ end
141
+ def publish(message)
142
+ @logger.info "#{message}"
143
+ end
144
+ end
127
145
 
128
146
  # my_lib/my_message.rb
129
147
  class MyMessage
@@ -135,92 +153,107 @@ Example
135
153
  validates_presence_of :data
136
154
 
137
155
  def initialize(options={})
138
- @hi = options[:data] || 'some data here'
156
+ @data = options[:data]
157
+ end
158
+
159
+ # We define this for Serf::Message serialization.
160
+ def attributes
161
+ { 'data' => data }
139
162
  end
140
163
 
141
164
  end
142
165
 
143
166
  # my_lib/my_handler.rb
144
167
  class MyHandler
145
- include Serf::Handler
146
-
147
- # Declare handlers for a 'my_message' message kind with a Message class.
148
- receives(
149
- 'my_message',
150
- as: MyMessage,
151
- with: :submit_my_message)
152
-
153
- # This handler of 'other_message' doesn't need a Message class,
154
- # and will just work off the ENV hash.
155
- receives(
156
- 'other_message',
157
- with: :submit_other_message)
158
168
 
159
169
  def initialize(options={})
160
170
  @logger = options[:logger]
161
171
  end
162
172
 
163
173
  def submit_my_message(message)
164
- @logger.info "In Submit My Message: #{message.inspect.to_s}"
174
+ @logger.info "In Submit My Message: #{message.to_json}"
165
175
  # Validate message because we have implement my_message with it.
166
176
  unless message.valid?
167
177
  raise ArgumentError, message.errors.full_messages.join(',')
168
178
  end
169
179
  # Do work Here...
170
- # And return other messages as results of work, or nil for nothing.
171
- return nil
180
+ # And return 0 or more messages as result. Nil is valid response.
181
+ return { kind: 'my_message_results' }
172
182
  end
173
183
 
174
184
  def submit_other_message(message={})
175
185
  # The message here is the ENV hash because we didn't declare
176
186
  # an :as option with `receives`.
177
- @logger.info "In Submit Other Result: #{message.inspect.to_s}"
178
- return nil
187
+ @logger.info "In Submit OtherMessage: #{message.inspect.to_s}"
188
+ return [
189
+ { kind: 'other_message_result1' },
190
+ { kind: 'other_message_result2' }
191
+ ]
179
192
  end
180
193
 
194
+ def raises_error(message={})
195
+ @logger.info 'In Raises Error, about to raise error'
196
+ raise 'My Handler Runtime Error'
197
+ end
198
+
199
+ def regexp_matched(message={})
200
+ @logger.info "RegExp Matched #{message.inspect}"
201
+ nil
202
+ end
181
203
  end
182
204
 
183
- # my_lib/manifest.rb
184
- MANIFEST = {
185
- 'my_message' => {
205
+ # my_lib/routes.rb
206
+ ROUTES = {
207
+ # Declare a matcher and a list of routes to endpoints.
208
+ 'my_message' => [{
186
209
  # Declares which handler to use. This is the tableized
187
210
  # name of the class. It will be constantized by the serf code.
188
211
  handler: 'my_handler',
189
- # Default is true to process in background.
190
- async: true
191
- },
192
- 'other_message' => {
212
+ action: :submit_my_message,
213
+
214
+ # Define a parser that will build up a message object.
215
+ # Default: nil, no parsing done.
216
+ # Or name of registered parser to use.
217
+ message_parser: 'my_parser',
218
+
219
+ # Default is process in foreground.
220
+ #background: false
221
+ }],
222
+ 'other_message' => [{
223
+ handler: 'my_handler',
224
+ action: :submit_other_message,
225
+ background: true
226
+ }, {
227
+ handler: 'my_handler',
228
+ action: :raises_error,
229
+ background: true
230
+ }],
231
+ /^events\/.*$/ => [{
193
232
  handler: 'my_handler',
194
- async: false
195
- }
233
+ action: :regexp_matched,
234
+ background: true
235
+ }]
196
236
  }
197
237
 
198
- # Helper class for this example to receive result or error messages
199
- # and pipe it into our logger.
200
- class MyChannel
201
- def initialize(name, logger)
202
- @name = name
203
- @logger = logger
204
- end
205
- def publish(message)
206
- @logger.info "#{@name} #{message}"
207
- #@logger.info message
208
- end
209
- end
210
-
211
238
  # Create a new builder for this serf app.
212
239
  builder = Serf::Builder.new do
213
- # Registers different service libary manifests.
214
- register MANIFEST
240
+ # Include some middleware
241
+ use Serf::Middleware::UuidTagger
242
+
243
+ # Registers routes from different service libary manifests.
244
+ routes ROUTES
245
+
215
246
  # Can define arguments to pass to the 'my_handler' initialize method.
216
- config(
217
- 'my_handler',
218
- logger: logger)
247
+ handler 'my_handler', MyHandler.new(logger: ::Log4r::Logger['handler'])
248
+ message_parser 'my_parser', MyMessage
249
+
219
250
  # Create result and error channels for the handler result messages.
220
- error_channel MyChannel.new('errorchannel: ', logger)
221
- results_channel MyChannel.new('resultschannel: ', logger)
251
+ error_channel MyChannel.new(::Log4r::Logger['error_channel'])
252
+ results_channel MyChannel.new(::Log4r::Logger['results_channel'])
253
+
222
254
  # We pass in a logger to our Serf code: Serfer and Runners.
223
- logger logger
255
+ logger ::Log4r::Logger['serf']
256
+
224
257
  # Optionally define a not found handler...
225
258
  # Defaults to raising an ArgumentError, 'Not Found'.
226
259
  #not_found lambda {|x| puts x}
@@ -228,21 +261,39 @@ Example
228
261
  app = builder.to_app
229
262
 
230
263
  # Start event machine loop.
264
+ logger = ::Log4r::Logger['top_level']
231
265
  EM.run do
232
266
  # On the next tick
233
267
  EM.next_tick do
234
268
  logger.info "Start Tick #{Thread.current.object_id}"
269
+
235
270
  # This will submit a 'my_message' message (as a hash) to the Serf App.
271
+ # NOTE: We should get an error message pushed to the error channel
272
+ # because no 'data' field was put in my_message as required
273
+ # And the Result should have a CaughtExceptionEvent.
236
274
  results = app.call('kind' => 'my_message')
237
- # Because we declared the 'my_message' kind to be handled async, we
275
+ logger.info "In Tick, MyMessage Results: #{results.inspect}"
276
+
277
+ # Here is good result
278
+ results = app.call('kind' => 'my_message', 'data' => '1234')
279
+ logger.info "In Tick, MyMessage Results: #{results.inspect}"
280
+
281
+ # This will submit 'other_message' to be handled in foreground
282
+ # Because we declared the 'other_message' kind to be handled async, we
238
283
  # should get a MessageAcceptedEvent as the results.
239
- logger.info "In Tick Results: #{results.inspect}"
284
+ results = app.call('kind' => 'other_message')
285
+ logger.info "In Tick, OtherMessage Results: #{results.inspect}"
286
+
287
+ # This will match a regexp
288
+ results = app.call('kind' => 'events/my_event')
289
+ logger.info "In Tick, Regexp Results: #{results.inspect}"
290
+
240
291
  begin
241
292
  # Here, we're going to submit a message that we don't handle.
242
293
  # By default, an exception will be raised.
243
294
  app.call('kind' => 'unhandled_message_kind')
244
295
  rescue => e
245
- puts "Caught in Tick: #{e.inspect}"
296
+ logger.warn "Caught in Tick: #{e.inspect}"
246
297
  end
247
298
  logger.info "End Tick #{Thread.current.object_id}"
248
299
  end
@@ -251,7 +302,6 @@ Example
251
302
  end
252
303
  end
253
304
 
254
-
255
305
  Contributing to serf
256
306
  ====================
257
307
 
data/lib/serf/builder.rb CHANGED
@@ -2,13 +2,14 @@ require 'serf/serfer'
2
2
  require 'serf/runners/direct_runner'
3
3
  require 'serf/runners/em_runner'
4
4
  require 'serf/util/null_object'
5
+ require 'serf/util/route_set'
5
6
 
6
7
  module Serf
7
8
 
8
9
  ##
9
10
  # A Serf Builder that processes the SerfUp DSL to build a rack-like
10
11
  # app to route and process received messages. This builder is
11
- # implemented with lots of code from Rack::Builder.
12
+ # implemented based on code from Rack::Builder.
12
13
  #
13
14
  # builder = Serf::Builder.parse_file 'examples/config.su'
14
15
  # builder.to_app
@@ -16,7 +17,7 @@ module Serf
16
17
  # or
17
18
  #
18
19
  # builder = Serf::Builder.new do
19
- # ... A SerfUp Config block here. See the examples/config.su
20
+ # ... A SerfUp Config block here.
20
21
  # end
21
22
  # builder.to_app
22
23
  #
@@ -30,17 +31,19 @@ module Serf
30
31
 
31
32
  def initialize(app=nil, &block)
32
33
  # Configuration about the routes and apps to run.
33
- @manifest = {}
34
- @config = {}
34
+ @use = []
35
+ @route_maps = []
36
+ @handlers = {}
37
+ @message_parsers = {}
35
38
  @not_found = app
36
39
 
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
40
+ # Factories to build objects that wire our Serf App together.
41
+ # Note that these default implementing classes are also factories
42
+ # of their own objects (i.e. - define a 'build' class method).
43
+ @serfer_factory = ::Serf::Serfer
44
+ @foreground_runner_factory = ::Serf::Runners::DirectRunner
45
+ @background_runner_factory = ::Serf::Runners::EmRunner
46
+ @route_set_factory = ::Serf::Util::RouteSet
44
47
 
45
48
  # Utility and messaging channels for our Runners
46
49
  # NOTE: these are only used if the builder needs to instantiage runners.
@@ -56,32 +59,40 @@ module Serf
56
59
  self.new(default_app, &block).to_app
57
60
  end
58
61
 
59
- def register(manifest)
60
- @manifest.merge! manifest
62
+ def use(middleware, *args, &block)
63
+ @use << proc { |app| middleware.new(app, *args, &block) }
61
64
  end
62
65
 
63
- def config(handler, *args, &block)
64
- @config[handler] = [args, block]
66
+ def routes(route_map)
67
+ @route_maps << route_map
68
+ end
69
+
70
+ def handler(handler_name, handler)
71
+ @handlers[handler_name] = handler
72
+ end
73
+
74
+ def message_parser(message_parser_name, message_parser)
75
+ @message_parsers[message_parser_name] = message_parser
65
76
  end
66
77
 
67
78
  def not_found(app)
68
79
  @not_found = app
69
80
  end
70
81
 
71
- def serfer_class(serfer_class)
72
- @serfer_class = serfer_class
82
+ def serfer_factory(serfer_factory)
83
+ @serfer_factory = serfer_factory
73
84
  end
74
85
 
75
- def serfer_class(serfer_options)
76
- @serfer_options = serfer_options
86
+ def foreground_runner_factory(foreground_runner_factory)
87
+ @foreground_runner_factory = foreground_runner_factory
77
88
  end
78
89
 
79
- def runner(runner)
80
- @runner = runner
90
+ def background_runner_factory(background_runner_factory)
91
+ @background_runner_factory = background_runner_factory
81
92
  end
82
93
 
83
- def async_runner(runner)
84
- @async_runner = runner
94
+ def route_set_factory(route_set_factory)
95
+ @route_set_factory = route_set_factory
85
96
  end
86
97
 
87
98
  def results_channel(results_channel)
@@ -97,61 +108,79 @@ module Serf
97
108
  end
98
109
 
99
110
  def to_app
100
- # Our async and sync messages & handlers.
101
- handlers = {}
102
- async_handlers = {}
103
-
104
- # Iterate our manifests to build out handlers and message classes
105
- @manifest.each do |kind, options|
106
- # Instantiate our handler with any possible configuration.
107
- handler_str = options.fetch(:handler)
108
- handler_class = handler_str.camelize.constantize
109
- args, block = @config.fetch(handler_str) { [[], nil] }
110
- handler = handler_class.new *args, &block
111
-
112
- # Put handlers and kinds into the proper map of handlers for either
113
- # synchronous or asynchronous processing.
114
- async = options.fetch(:async) { true }
115
- if async
116
- async_handlers[kind] = handler
117
- else
118
- handlers[kind] = handler
111
+ bg_route_set = @route_set_factory.build
112
+ fg_route_set = @route_set_factory.build
113
+
114
+ @route_maps.each do |route_map|
115
+ route_map.each do |matcher, route_configs|
116
+ route_configs.each do |route_config|
117
+ # Get the required handler.
118
+ # Raises error if handler wasn't declared in config.
119
+ # Raises error if handler wasn't registered with builder.
120
+ handler_name = route_config.fetch :handler
121
+ handler = @handlers.fetch handler_name
122
+
123
+ # Get the required action/method of the handler.
124
+ # Raises error if route_config doesn't have it.
125
+ action = route_config.fetch :action
126
+
127
+ # Lookup the parser if it was defined.
128
+ # The Parser MAY be either an object or string.
129
+ # If String, then we're going to look up in parser map.
130
+ # Raises an error if a parser (string) was declared, but not
131
+ # registered with the builder.
132
+ parser = route_config[:message_parser]
133
+ parser = @message_parsers.fetch(parser) if parser.is_a?(String)
134
+
135
+ # We have the handler, action and parser.
136
+ # Now we're going to add that route to either the background
137
+ # or foreground route_set.
138
+ background = route_config.fetch(:background) { false }
139
+ (background ? bg_route_set : fg_route_set).add_route(
140
+ matcher: matcher,
141
+ handler: handler,
142
+ action: action,
143
+ message_parser: parser)
144
+ end
119
145
  end
120
146
  end
121
147
 
122
- # Get or make our runner
123
- runner = @runner || @runner_class.new(
148
+ # Get or make our foreground runner
149
+ fg_runner = @foreground_runner_factory.build(
124
150
  results_channel: @results_channel,
125
151
  error_channel: @error_channel,
126
152
  logger: @logger)
127
153
 
128
- # By default, we go to the not_found app.
129
- app = @not_found
154
+ # Get or make our background runner
155
+ bg_runner = @background_runner_factory.build(
156
+ results_channel: @results_channel,
157
+ error_channel: @error_channel,
158
+ logger: @logger)
130
159
 
131
- # If we have synchronous handlers, insert before not_found app.
132
- if handlers.size > 0
133
- # create the serfer class to run synchronous handlers
134
- app = @serfer_class.new(
135
- @serfer_options.merge(
136
- handlers: handlers,
137
- runner: runner,
138
- not_found: app))
160
+ # We create the route_sets dependent on built routes.
161
+ route_sets = {}
162
+ if fg_route_set.size > 0
163
+ route_sets[fg_route_set] = fg_runner
164
+ end
165
+ if bg_route_set.size > 0
166
+ route_sets[bg_route_set] = bg_runner
139
167
  end
140
168
 
141
- # If we have async handlers, insert before current app stack.
142
- if async_handlers.size > 0
143
- # Get or make our async wrapper
144
- async_runner = @async_runner || @async_runner_class.new(
145
- runner: runner,
169
+ # By default, we go to the not_found app.
170
+ app = @not_found
171
+
172
+ # But if we have routes, then we'll build a serfer to handle it.
173
+ if route_sets.size > 0
174
+ app = @serfer_factory.build(
175
+ route_sets: route_sets,
176
+ not_found: app,
177
+ error_channel: @error_channel,
146
178
  logger: @logger)
147
- # create the serfer class to run async handlers
148
- app = @serfer_class.new(
149
- @serfer_options.merge(
150
- handlers: async_handlers,
151
- runner: async_runner,
152
- not_found: app))
153
179
  end
154
180
 
181
+ # We're going to inject middleware here.
182
+ app = @use.reverse.inject(app) { |a,e| e[a] } if @use.size > 0
183
+
155
184
  return app
156
185
  end
157
186
  end
data/lib/serf/message.rb CHANGED
@@ -17,24 +17,20 @@ module Serf
17
17
  send 'kind=', self.to_s.tableize.singularize
18
18
  end
19
19
 
20
- module InstanceMethods
21
-
22
- def kind
23
- self.class.kind
24
- end
25
-
26
- def to_hash
27
- attributes.merge kind: kind
28
- end
20
+ def kind
21
+ self.class.kind
22
+ end
29
23
 
30
- def to_msgpack
31
- to_hash.to_msgpack
32
- end
24
+ def to_hash
25
+ attributes.merge kind: kind
26
+ end
33
27
 
34
- def to_json(*args)
35
- to_hash.to_json *args
36
- end
28
+ def to_msgpack
29
+ to_hash.to_msgpack
30
+ end
37
31
 
32
+ def to_json(*args)
33
+ to_hash.to_json *args
38
34
  end
39
35
 
40
36
  module ClassMethods
@@ -0,0 +1,31 @@
1
+ require 'uuidtools'
2
+
3
+ module Serf
4
+ module Middleware
5
+
6
+ ##
7
+ # Middleware to add a request uuid the ENV Hash to uniquely identify
8
+ # the handling of this input. But it won't overwrite the uuid field
9
+ # if the incoming request already has it.
10
+ #
11
+ class UuidTagger
12
+
13
+ ##
14
+ # @param app the app
15
+ # @options opts [String] :field the ENV field to set with a UUID.
16
+ #
17
+ def initialize(app, options={})
18
+ @app = app
19
+ @field = options.fetch(:field) { 'serf.request_uuid' }
20
+ end
21
+
22
+ def call(env)
23
+ env = env.dup
24
+ env[@field] ||= UUIDTools::UUID.random_create.to_s
25
+ @app.call env
26
+ end
27
+
28
+ end
29
+
30
+ end
31
+ end
@@ -1,5 +1,4 @@
1
- require 'serf/messages/caught_exception_event'
2
- require 'serf/util/null_object'
1
+ require 'serf/util/with_error_handling'
3
2
 
4
3
  module Serf
5
4
  module Runners
@@ -10,28 +9,33 @@ module Runners
10
9
  # to proper error or results channels.
11
10
  #
12
11
  class DirectRunner
12
+ include ::Serf::Util::WithErrorHandling
13
13
 
14
14
  def initialize(options={})
15
15
  # Mandatory, we want both results and error channels.
16
16
  @results_channel = options.fetch(:results_channel)
17
17
  @error_channel = options.fetch(:error_channel)
18
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 }
19
+ # Optional overrides for error handling
20
+ @error_event_class = options[:error_event_class]
21
+ @logger = options[:logger]
27
22
  end
28
23
 
29
- def run(handler, params)
30
- with_error_handling(params) do
31
- results = handler.call params
32
- publish_results results
33
- return results
24
+ def run(endpoints, env)
25
+ results = []
26
+ endpoints.each do |ep|
27
+ run_results = with_error_handling(env) do
28
+ params = ep.message_parser ? ep.message_parser.parse(env) : env
29
+ ep.handler.send(ep.action, params)
30
+ end
31
+ results.concat Array(run_results)
32
+ publish_results run_results
34
33
  end
34
+ return results
35
+ end
36
+
37
+ def self.build(options={})
38
+ self.new options
35
39
  end
36
40
 
37
41
  protected
@@ -50,24 +54,6 @@ module Runners
50
54
  return nil
51
55
  end
52
56
 
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
57
  end
72
58
 
73
59
  end
@@ -1,6 +1,7 @@
1
1
  require 'eventmachine'
2
2
 
3
3
  require 'serf/messages/message_accepted_event'
4
+ require 'serf/runners/direct_runner'
4
5
 
5
6
  module Serf
6
7
  module Runners
@@ -34,15 +35,25 @@ module Runners
34
35
  @logger = options.fetch(:logger) { ::Serf::Util::NullObject.new }
35
36
  end
36
37
 
37
- def run(handler, params)
38
+ def run(endpoints, env)
39
+ endpoints = endpoints.dup
40
+ env = env.dup
38
41
  @evm.defer(proc do
39
42
  begin
40
- @runner.run handler, params
43
+ @runner.run endpoints, env
41
44
  rescue => e
42
45
  @logger.error "#{e.inspect}\n\n#{e.backtrace.join("\n")}"
43
46
  end
44
47
  end)
45
- return @mae_class.new message: params
48
+ return @mae_class.new message: env
49
+ end
50
+
51
+ def self.build(options={})
52
+ options[:runner] = options.fetch(:runner) {
53
+ factory = options[:runner_factory] || ::Serf::Runners::DirectRunner
54
+ factory.build options
55
+ }
56
+ self.new options
46
57
  end
47
58
 
48
59
  end
data/lib/serf/serfer.rb CHANGED
@@ -1,41 +1,60 @@
1
1
  require 'serf/error'
2
+ require 'serf/util/with_error_handling'
2
3
 
3
4
  module Serf
4
5
 
5
6
  class Serfer
7
+ include ::Serf::Util::WithErrorHandling
6
8
 
7
9
  def initialize(options={})
8
- # Manditory, needs a runner.
9
- @runner = options.fetch(:runner)
10
+ # Each route_set has a runner.
11
+ @route_sets = options[:route_sets] || {}
12
+
13
+ # Optional overrides for WithErrorHandling
14
+ @error_channel = options[:error_channel]
15
+ @error_event_class = options[:error_event_class]
16
+ @logger = options[:logger]
10
17
 
11
18
  # Options for handling the requests
12
- @handlers = options[:handlers] || {}
13
19
  @not_found = options[:not_found] || proc do
14
20
  raise ArgumentError, 'Handler Not Found'
15
21
  end
16
22
  end
17
23
 
18
24
  ##
19
- # Rack-like call to handle a message
25
+ # Rack-like call to run set of handlers for a message
20
26
  #
21
27
  def call(env)
22
28
  # We normalize by symbolizing the env keys
23
- params = env.symbolize_keys
24
-
25
- # Pull the kind out of the env.
26
- kind = params[:kind]
27
- handler = @handlers[kind]
28
- if handler
29
- # Let's run the handler
30
- return @runner.run(handler, params) if handler
31
- else
32
- # we can't handle this kind.
33
- return @not_found.call env
29
+ env = env.symbolize_keys
30
+
31
+ # We're going to concat all the results
32
+ matched_routes = false
33
+ results = []
34
+ @route_sets.each do |route_set, runner|
35
+ with_error_handling(env) do
36
+ endpoints = route_set.match_routes env
37
+ if endpoints.size > 0
38
+ matched_routes = true
39
+ results.concat Array(runner.run(endpoints, env))
40
+ end
41
+ end
34
42
  end
43
+
44
+ # If we don't have any handlers, do the not_found call.
45
+ # NOTE: Purposefully not wrapping this in exception handling.
46
+ return @not_found.call env unless matched_routes
47
+
48
+ return results
35
49
  rescue => e
36
50
  e.extend(::Serf::Error)
37
51
  raise e
38
52
  end
53
+
54
+ def self.build(options={})
55
+ self.new options
56
+ end
57
+
39
58
  end
40
59
 
41
60
  end
@@ -0,0 +1,29 @@
1
+ module Serf
2
+ module Util
3
+
4
+ ##
5
+ # A matcher that does a regexp match on a specific field
6
+ # in the given Env. By default, we use this to do regexp matching
7
+ # on message kinds for routing.
8
+ #
9
+ class RegexpMatcher
10
+ attr_reader :regexp
11
+ attr_reader :field
12
+
13
+ def initialize(regexp, options={})
14
+ @regexp = regexp
15
+ @field = options.fetch(:field) { :kind }
16
+ end
17
+
18
+ def ===(env)
19
+ return @regexp === env[@field]
20
+ end
21
+
22
+ def self.build(regexp)
23
+ return self.new regexp
24
+ end
25
+
26
+ end
27
+
28
+ end
29
+ end
@@ -0,0 +1,37 @@
1
+ module Serf
2
+ module Util
3
+
4
+ ##
5
+ # A simple class to represent route endpoints. RouteSets
6
+ # store a list of endpoints per matcher, and Runners use endpoints
7
+ # to then execute the handlers.
8
+ #
9
+ class RouteEndpoint
10
+ # Actual handler object that defines the action method.
11
+ attr_accessor :handler
12
+ # The method to call.
13
+ attr_accessor :action
14
+ # A parser that turns the message ENV hash into a Message object
15
+ # that the handler#action uses.
16
+ attr_accessor :message_parser
17
+
18
+ def initialize(options={})
19
+ # Mandatory parameters
20
+ @handler = options.fetch :handler
21
+ @action = options.fetch :action
22
+
23
+ # Optional parameters
24
+ @message_parser = options[:message_parser]
25
+ end
26
+
27
+ ##
28
+ # Default factory method.
29
+ #
30
+ def self.build(options={})
31
+ return self.new options
32
+ end
33
+
34
+ end
35
+
36
+ end
37
+ end
@@ -0,0 +1,82 @@
1
+ require 'serf/util/route_endpoint'
2
+ require 'serf/util/regexp_matcher'
3
+
4
+ module Serf
5
+ module Util
6
+
7
+ ##
8
+ # RouteSets hold routing information for ENV hashes to matched endpoints.
9
+ #
10
+ class RouteSet
11
+
12
+ def initialize(options={})
13
+ @routes = {}
14
+ @matchers = []
15
+ @regexp_matcher_factory = options.fetch(:regexp_matcher_factory) {
16
+ ::Serf::Util::RegexpMatcher
17
+ }
18
+ @route_endpoint_factory = options.fetch(:route_endpoint_factory) {
19
+ ::Serf::Util::RouteEndpoint
20
+ }
21
+ end
22
+
23
+ ##
24
+ # Connects a matcher (String or an Object implementing ===) to an endpoint.
25
+ #
26
+ # @option opts [Obj, String] :matcher Matches ENV Hashes to endpoints.
27
+ # Note that String and Regexp values are set up to match the
28
+ # :kind key from ENV Hashes.
29
+ # @option opts [Obj] :handler Receiver of the action.
30
+ # @option opts [Symbol, String] :action Method to call on handler.
31
+ # @option opts [#parse] :message_parser Translates ENV Hash to Message Obj.
32
+ #
33
+ def add_route(options={})
34
+ # We create our endpoint representation.
35
+ endpoint = @route_endpoint_factory.build(
36
+ handler: options.fetch(:handler),
37
+ action: options.fetch(:action),
38
+ message_parser: options[:message_parser])
39
+
40
+ # Maybe we have an non-String matcher. Handle the Regexp case.
41
+ # We only keep track of matchers if it isn't a string because
42
+ # string matchers are just pulled out of routes by key lookup.
43
+ matcher = options.fetch :matcher
44
+ matcher = @regexp_matcher_factory.build matcher if matcher.kind_of? Regexp
45
+ @matchers << matcher unless matcher.is_a? String
46
+
47
+ # We add the route (matcher+endpoint) into our routes
48
+ @routes[matcher] ||= []
49
+ @routes[matcher] << endpoint
50
+ end
51
+
52
+ ##
53
+ # @param [Hash] env The input message environment to match for routes.
54
+ # @return [Array] List of endpoints that matched.
55
+ #
56
+ def match_routes(env={})
57
+ kind = env[:kind]
58
+ routes = []
59
+ routes.concat Array(@routes[kind])
60
+ @matchers.each do |matcher|
61
+ routes.concat Array(@routes[matcher]) if matcher === env
62
+ end
63
+ return routes
64
+ end
65
+
66
+ ##
67
+ # @return [Integer] Number of routes this RouteSet tracks.
68
+ #
69
+ def size
70
+ return @routes.size
71
+ end
72
+
73
+ ##
74
+ # Default factory method.
75
+ #
76
+ def self.build(options={})
77
+ self.new options
78
+ end
79
+ end
80
+
81
+ end
82
+ end
@@ -0,0 +1,44 @@
1
+ require 'serf/messages/caught_exception_event'
2
+ require 'serf/util/null_object'
3
+
4
+ module Serf
5
+ module Util
6
+
7
+ ##
8
+ # Helper module to rescues exceptions from executing blocks of
9
+ # code, and then logs+publishes the error event.
10
+ #
11
+ module WithErrorHandling
12
+
13
+ ##
14
+ # A block wrapper to handle errors when executing a block.
15
+ #
16
+ # Including classes may have the following instance variables
17
+ # to override the default values:
18
+ # * @error_event_class - ::Serf::Messages::CaughtExceptionEvent
19
+ # * @logger - ::Serf::Util::NullObject.new
20
+ # * @error_channel - ::Serf::Util::NullObject.new
21
+ #
22
+ def with_error_handling(context=nil)
23
+ yield
24
+ rescue => e
25
+ eec = @error_event_class || ::Serf::Messages::CaughtExceptionEvent
26
+ logger = @logger || ::Serf::Util::NullObject.new
27
+ error_channel = @error_channel || ::Serf::Util::NullObject.new
28
+ error_event = eec.new(
29
+ context: context,
30
+ error_message: e.inspect,
31
+ error_backtrace: e.backtrace.join("\n"))
32
+
33
+ # log the error to our logger, and to our error channel.
34
+ logger.error error_event
35
+ error_channel.publish error_event
36
+
37
+ # We're done, so just return this error.
38
+ return error_event
39
+ end
40
+
41
+ end
42
+
43
+ end
44
+ end
data/lib/serf/version.rb CHANGED
@@ -2,8 +2,8 @@ module Serf
2
2
 
3
3
  module Version
4
4
  MAJOR = 0
5
- MINOR = 4
6
- PATCH = 1
5
+ MINOR = 5
6
+ PATCH = 0
7
7
  BUILD = nil
8
8
  STRING = [MAJOR, MINOR, PATCH, BUILD].compact.join '.'
9
9
  end
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.4.1"
8
+ s.version = "0.5.0"
9
9
 
10
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 = "2012-01-22"
12
+ s.date = "2012-01-26"
13
13
  s.description = "Event-Driven SOA with CQRS"
14
14
  s.email = "benjaminlyu@gmail.com"
15
15
  s.extra_rdoc_files = [
@@ -28,15 +28,19 @@ Gem::Specification.new do |s|
28
28
  "lib/serf.rb",
29
29
  "lib/serf/builder.rb",
30
30
  "lib/serf/error.rb",
31
- "lib/serf/handler.rb",
32
31
  "lib/serf/message.rb",
33
32
  "lib/serf/messages/caught_exception_event.rb",
34
33
  "lib/serf/messages/message_accepted_event.rb",
34
+ "lib/serf/middleware/uuid_tagger.rb",
35
35
  "lib/serf/runners/direct_runner.rb",
36
36
  "lib/serf/runners/em_runner.rb",
37
37
  "lib/serf/serfer.rb",
38
38
  "lib/serf/util/null_object.rb",
39
+ "lib/serf/util/regexp_matcher.rb",
40
+ "lib/serf/util/route_endpoint.rb",
41
+ "lib/serf/util/route_set.rb",
39
42
  "lib/serf/util/uuidable.rb",
43
+ "lib/serf/util/with_error_handling.rb",
40
44
  "lib/serf/version.rb",
41
45
  "serf.gemspec",
42
46
  "spec/serf_spec.rb",
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: serf
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.1
4
+ version: 0.5.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,11 +9,11 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-01-22 00:00:00.000000000Z
12
+ date: 2012-01-26 00:00:00.000000000Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: activesupport
16
- requirement: &70284496039920 !ruby/object:Gem::Requirement
16
+ requirement: &70145106081500 !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
19
  - - ! '>='
@@ -21,10 +21,10 @@ dependencies:
21
21
  version: 3.2.0
22
22
  type: :runtime
23
23
  prerelease: false
24
- version_requirements: *70284496039920
24
+ version_requirements: *70145106081500
25
25
  - !ruby/object:Gem::Dependency
26
26
  name: i18n
27
- requirement: &70284496037020 !ruby/object:Gem::Requirement
27
+ requirement: &70145106073960 !ruby/object:Gem::Requirement
28
28
  none: false
29
29
  requirements:
30
30
  - - ! '>='
@@ -32,10 +32,10 @@ dependencies:
32
32
  version: 0.6.0
33
33
  type: :runtime
34
34
  prerelease: false
35
- version_requirements: *70284496037020
35
+ version_requirements: *70145106073960
36
36
  - !ruby/object:Gem::Dependency
37
37
  name: uuidtools
38
- requirement: &70284496034860 !ruby/object:Gem::Requirement
38
+ requirement: &70145106072920 !ruby/object:Gem::Requirement
39
39
  none: false
40
40
  requirements:
41
41
  - - ! '>='
@@ -43,10 +43,10 @@ dependencies:
43
43
  version: 2.1.2
44
44
  type: :runtime
45
45
  prerelease: false
46
- version_requirements: *70284496034860
46
+ version_requirements: *70145106072920
47
47
  - !ruby/object:Gem::Dependency
48
48
  name: rspec
49
- requirement: &70284496020980 !ruby/object:Gem::Requirement
49
+ requirement: &70145106071740 !ruby/object:Gem::Requirement
50
50
  none: false
51
51
  requirements:
52
52
  - - ~>
@@ -54,10 +54,10 @@ dependencies:
54
54
  version: 2.3.0
55
55
  type: :development
56
56
  prerelease: false
57
- version_requirements: *70284496020980
57
+ version_requirements: *70145106071740
58
58
  - !ruby/object:Gem::Dependency
59
59
  name: yard
60
- requirement: &70284496019460 !ruby/object:Gem::Requirement
60
+ requirement: &70145106070620 !ruby/object:Gem::Requirement
61
61
  none: false
62
62
  requirements:
63
63
  - - ~>
@@ -65,10 +65,10 @@ dependencies:
65
65
  version: 0.6.0
66
66
  type: :development
67
67
  prerelease: false
68
- version_requirements: *70284496019460
68
+ version_requirements: *70145106070620
69
69
  - !ruby/object:Gem::Dependency
70
70
  name: bundler
71
- requirement: &70284496018360 !ruby/object:Gem::Requirement
71
+ requirement: &70145106069640 !ruby/object:Gem::Requirement
72
72
  none: false
73
73
  requirements:
74
74
  - - ~>
@@ -76,10 +76,10 @@ dependencies:
76
76
  version: 1.0.0
77
77
  type: :development
78
78
  prerelease: false
79
- version_requirements: *70284496018360
79
+ version_requirements: *70145106069640
80
80
  - !ruby/object:Gem::Dependency
81
81
  name: jeweler
82
- requirement: &70284496017420 !ruby/object:Gem::Requirement
82
+ requirement: &70145106068740 !ruby/object:Gem::Requirement
83
83
  none: false
84
84
  requirements:
85
85
  - - ~>
@@ -87,10 +87,10 @@ dependencies:
87
87
  version: 1.6.4
88
88
  type: :development
89
89
  prerelease: false
90
- version_requirements: *70284496017420
90
+ version_requirements: *70145106068740
91
91
  - !ruby/object:Gem::Dependency
92
92
  name: simplecov
93
- requirement: &70284496015800 !ruby/object:Gem::Requirement
93
+ requirement: &70145106067300 !ruby/object:Gem::Requirement
94
94
  none: false
95
95
  requirements:
96
96
  - - ! '>='
@@ -98,10 +98,10 @@ dependencies:
98
98
  version: '0'
99
99
  type: :development
100
100
  prerelease: false
101
- version_requirements: *70284496015800
101
+ version_requirements: *70145106067300
102
102
  - !ruby/object:Gem::Dependency
103
103
  name: msgpack
104
- requirement: &70284496014740 !ruby/object:Gem::Requirement
104
+ requirement: &70145106053600 !ruby/object:Gem::Requirement
105
105
  none: false
106
106
  requirements:
107
107
  - - ! '>='
@@ -109,10 +109,10 @@ dependencies:
109
109
  version: 0.4.6
110
110
  type: :development
111
111
  prerelease: false
112
- version_requirements: *70284496014740
112
+ version_requirements: *70145106053600
113
113
  - !ruby/object:Gem::Dependency
114
114
  name: eventmachine
115
- requirement: &70284496013960 !ruby/object:Gem::Requirement
115
+ requirement: &70145106052540 !ruby/object:Gem::Requirement
116
116
  none: false
117
117
  requirements:
118
118
  - - ! '>='
@@ -120,7 +120,7 @@ dependencies:
120
120
  version: 0.12.10
121
121
  type: :development
122
122
  prerelease: false
123
- version_requirements: *70284496013960
123
+ version_requirements: *70145106052540
124
124
  description: Event-Driven SOA with CQRS
125
125
  email: benjaminlyu@gmail.com
126
126
  executables: []
@@ -140,15 +140,19 @@ files:
140
140
  - lib/serf.rb
141
141
  - lib/serf/builder.rb
142
142
  - lib/serf/error.rb
143
- - lib/serf/handler.rb
144
143
  - lib/serf/message.rb
145
144
  - lib/serf/messages/caught_exception_event.rb
146
145
  - lib/serf/messages/message_accepted_event.rb
146
+ - lib/serf/middleware/uuid_tagger.rb
147
147
  - lib/serf/runners/direct_runner.rb
148
148
  - lib/serf/runners/em_runner.rb
149
149
  - lib/serf/serfer.rb
150
150
  - lib/serf/util/null_object.rb
151
+ - lib/serf/util/regexp_matcher.rb
152
+ - lib/serf/util/route_endpoint.rb
153
+ - lib/serf/util/route_set.rb
151
154
  - lib/serf/util/uuidable.rb
155
+ - lib/serf/util/with_error_handling.rb
152
156
  - lib/serf/version.rb
153
157
  - serf.gemspec
154
158
  - spec/serf_spec.rb
@@ -168,7 +172,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
168
172
  version: '0'
169
173
  segments:
170
174
  - 0
171
- hash: -616776169548584213
175
+ hash: -650473328553199241
172
176
  required_rubygems_version: !ruby/object:Gem::Requirement
173
177
  none: false
174
178
  requirements:
data/lib/serf/handler.rb DELETED
@@ -1,91 +0,0 @@
1
- require 'active_support/concern'
2
- require 'active_support/core_ext/class/attribute'
3
- require 'active_support/core_ext/hash/keys'
4
- require 'active_support/core_ext/object/blank'
5
- require 'active_support/ordered_options'
6
-
7
- require 'serf/error'
8
-
9
- module Serf
10
-
11
- module Handler
12
- extend ActiveSupport::Concern
13
-
14
- included do
15
- # In the class that includes this module, we're going to
16
- # create an inheritable class attribute that will store
17
- # our mappings between messages and the methods to call.
18
- class_attribute :serf_actions
19
- class_attribute :serf_message_classes
20
- send(
21
- 'serf_actions=',
22
- ActiveSupport::InheritableOptions.new)
23
- send(
24
- 'serf_message_classes=',
25
- ActiveSupport::InheritableOptions.new)
26
-
27
- def self.inherited(kls) #:nodoc:
28
- super
29
- # Sets the current subclass class attribute to be an
30
- # inheritable copy of the superclass options.
31
- kls.send(
32
- 'serf_actions=',
33
- self.serf_actions.inheritable_copy)
34
- kls.send(
35
- 'serf_message_classes=',
36
- self.serf_message_classes.inheritable_copy)
37
- end
38
-
39
- end
40
-
41
- module InstanceMethods
42
-
43
- ##
44
- # Rack-like call. It receives an environment hash, which we
45
- # assume is a message.
46
- #
47
- def call(env={})
48
- # Just to stringify the environment keys
49
- env = env.symbolize_keys
50
- # Make sure a kind was set, and that we can handle it.
51
- message_kind = env[:kind]
52
- raise ArgumentError, 'No "kind" in call env' if message_kind.blank?
53
- method = self.class.serf_actions[message_kind]
54
- raise ArgumentError, "#{message_kind} not found" if method.blank?
55
- # Optionally convert the env into a Message class.
56
- # Let the actual handler method validate if they want.
57
- message_class = self.class.serf_message_classes[message_kind]
58
- env = message_class.parse env if message_class
59
- # Now execute the method with the environment parameters
60
- self.send method, env
61
- rescue => e
62
- e.extend ::Serf::Error
63
- raise e
64
- end
65
- end
66
-
67
- module ClassMethods
68
-
69
- ##
70
- # registers a method to handle the receipt of a message type.
71
- # @param *args splat list of message kinds
72
- # @options opts [Symbol] :with The method to call.
73
- # @options opts [Object] :as The Message class to call `parse`.
74
- #
75
- def receives(*args)
76
- options = args.last.kind_of?(Hash) ? args.pop : {}
77
- exposed_method = options[:with]
78
- raise ArgumentError, 'Missing "with" option' if exposed_method.blank?
79
- message_class = options[:as]
80
- args.each do |kind|
81
- raise ArgumentError, 'Blank kind' if kind.blank?
82
- self.serf_actions[kind] = exposed_method
83
- self.serf_message_classes[kind] = message_class if message_class
84
- end
85
- end
86
-
87
- end
88
-
89
- end
90
-
91
- end