serf 0.6.1 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,16 @@
1
+ Serf drives the incoming requests using runners. Each runner
2
+ is implemented to either perform tasks synchronously or asynchronously.
3
+ For asynchronous background processing, we rely on third party
4
+ libraries. The following is a list of current and (hopefully) future
5
+ libraries we hope to support.
6
+
7
+ Implemented Runners:
8
+ * EventMachine http://rubyeventmachine.com/
9
+ * GirlFriday https://github.com/mperham/girl_friday
10
+
11
+ TBD ThreadPool Libraries:
12
+ * ThreadPool https://github.com/danielbush/ThreadPool
13
+ * Threadz https://github.com/nanodeath/threadz
14
+ * Celluloid https://github.com/tarcieri/celluloid
15
+ * Resque https://github.com/defunkt/resque
16
+ * DelayedJob https://github.com/tobi/delayed_job
data/lib/serf/builder.rb CHANGED
@@ -1,14 +1,15 @@
1
+ require 'serf/routing/endpoint'
2
+ require 'serf/routing/registry'
3
+ require 'serf/runners/direct'
1
4
  require 'serf/serfer'
2
- require 'serf/runners/direct_runner'
3
- require 'serf/runners/em_runner'
4
5
  require 'serf/util/null_object'
5
- require 'serf/util/route_set'
6
+ require 'serf/util/options_extraction'
6
7
 
7
8
  module Serf
8
9
 
9
10
  ##
10
11
  # A Serf Builder that processes the SerfUp DSL to build a rack-like
11
- # app to route and process received messages. This builder is
12
+ # app to endpoint and process received messages. This builder is
12
13
  # implemented based on code from Rack::Builder.
13
14
  #
14
15
  # builder = Serf::Builder.parse_file 'examples/config.su'
@@ -22,6 +23,8 @@ module Serf
22
23
  # builder.to_app
23
24
  #
24
25
  class Builder
26
+ include Serf::Util::OptionsExtraction
27
+
25
28
  def self.parse_file(config)
26
29
  cfgfile = ::File.read(config)
27
30
  builder = eval "::Serf::Builder.new {\n" + cfgfile + "\n}",
@@ -29,33 +32,27 @@ module Serf
29
32
  return builder
30
33
  end
31
34
 
32
- def initialize(app=nil, &block)
33
- # Configuration about the routes and apps to run.
34
- @use = []
35
- @route_maps = []
36
- @handlers = {}
37
- @message_parsers = {}
38
- @not_found = app || proc do
39
- raise ArgumentError, 'Handler Not Found'
40
- end
41
-
42
- # Default option in route_configs for background is 'false'
43
- @background = false
35
+ def initialize(*args, &block)
36
+ extract_options! args
44
37
 
45
- # Factories to build objects that wire our Serf App together.
46
- # Note that these default implementing classes are also factories
47
- # of their own objects (i.e. - define a 'build' class method).
48
- @serfer_factory = ::Serf::Serfer
49
- @foreground_runner_factory = ::Serf::Runners::DirectRunner
50
- @background_runner_factory = ::Serf::Runners::EmRunner
51
- @route_set_factory = ::Serf::Util::RouteSet
38
+ # Configuration about the endpoints and apps to run.
39
+ @use = []
40
+ @not_found = opts :not_found, lambda { |env|
41
+ raise ArgumentError, 'Endpoints Not Found'
42
+ }
52
43
 
53
- # Utility and messaging channels for our Runners
54
- # NOTE: these are only used if the builder needs to instantiage runners.
55
- @results_channel = ::Serf::Util::NullObject.new
44
+ # Utility and messaging channels that get passed as options
45
+ # when building our Runners and Handlers.
46
+ @response_channel = ::Serf::Util::NullObject.new
56
47
  @error_channel = ::Serf::Util::NullObject.new
57
48
  @logger = ::Serf::Util::NullObject.new
58
49
 
50
+ # Set up the starting state for our DSL calls.
51
+ @runner_matcher_endpoint_map = {}
52
+ @runner_params = {}
53
+ runner :direct
54
+ @matcher = nil
55
+
59
56
  # configure based on a given block.
60
57
  instance_eval(&block) if block_given?
61
58
  end
@@ -68,126 +65,130 @@ module Serf
68
65
  @use << proc { |app| middleware.new(app, *args, &block) }
69
66
  end
70
67
 
71
- def routes(route_map)
72
- @route_maps << route_map
73
- end
68
+ def not_found(app); @not_found = app; end
74
69
 
75
- def handler(handler_name, handler)
76
- @handlers[handler_name] = handler
77
- end
70
+ def response_channel(channel); @response_channel = channel; end
71
+ def error_channel(channel); @error_channel = channel; end
72
+ def logger(logger); @logger = logger; end
78
73
 
79
- def message_parser(message_parser_name, message_parser)
80
- @message_parsers[message_parser_name] = message_parser
81
- end
74
+ ##
75
+ # DSL Method to change our current context to use the given matcher.
76
+ #
77
+ def match(matcher); @matcher = matcher; end
82
78
 
83
- def not_found(app)
84
- @not_found = app
85
- end
79
+ ##
80
+ # Mount and endpoint to the current context's Runner and Matcher.
81
+ # Connected so the endpoint will pass serf_options to the handler's build.
82
+ def run(*args, &block); mount true, *args, &block; end
86
83
 
87
- def background(run_in_background)
88
- @background = run_in_background
89
- end
84
+ ##
85
+ # Mount and endpoint to the current context's Runner and Matcher.
86
+ # Unconnected so the endpoint will omit serf_options to the handler's build.
87
+ def run_unconnected(*args, &block); mount false, *args, &block; end
90
88
 
91
- def serfer_factory(serfer_factory)
92
- @serfer_factory = serfer_factory
89
+ ##
90
+ # The generic mount method used by run & run_unconnected to create an
91
+ # endpoint to be associated with the current context's Runner and Matcher.
92
+ def mount(connected, handler_factory, *args, &block)
93
+ raise 'No matcher defined yet' unless @matcher
94
+ @runner_matcher_endpoint_map[@runner_factory] ||= {}
95
+ @runner_matcher_endpoint_map[@runner_factory][@matcher] ||= []
96
+ @runner_matcher_endpoint_map[@runner_factory][@matcher] <<
97
+ Serf::Routing::Endpoint.new(
98
+ connected,
99
+ handler_factory,
100
+ *args,
101
+ &block)
93
102
  end
94
103
 
95
- def foreground_runner_factory(foreground_runner_factory)
96
- @foreground_runner_factory = foreground_runner_factory
104
+ ##
105
+ # DSL Method to change our current context to use the given runner.
106
+ #
107
+ def runner(type)
108
+ @runner_factory = case type
109
+ when :direct
110
+ ::Serf::Runners::Direct
111
+ when :event_machine
112
+ begin
113
+ require 'serf/runners/event_machine'
114
+ Serf::Runners::EventMachine
115
+ rescue NameError => e
116
+ e.extend Serf::Error
117
+ raise e
118
+ end
119
+ when :girl_friday
120
+ begin
121
+ require 'serf/runners/girl_friday'
122
+ Serf::Runners::GirlFriday
123
+ rescue NameError => e
124
+ e.extend Serf::Error
125
+ raise e
126
+ end
127
+ else
128
+ raise 'No callable runner' unless type.respond_to? :build
129
+ type
130
+ end
97
131
  end
98
132
 
99
- def background_runner_factory(background_runner_factory)
100
- @background_runner_factory = background_runner_factory
133
+ def params(*args)
134
+ @runner_params[@runner_factory] = args
101
135
  end
102
136
 
103
- def route_set_factory(route_set_factory)
104
- @route_set_factory = route_set_factory
137
+ ##
138
+ # Returns a hash of our current serf infrastructure options
139
+ # to be passed to Endpoint#build methods.
140
+ def serf_options
141
+ {
142
+ response_channel: @error_channel,
143
+ error_channel: @error_channel,
144
+ logger: @logger
145
+ }
105
146
  end
106
147
 
107
- def results_channel(results_channel)
108
- @results_channel = results_channel
109
- end
148
+ ##
149
+ # Create our app.
150
+ #
151
+ def to_app
152
+ # By default, we go to the not_found app.
153
+ app = @not_found
110
154
 
111
- def error_channel(error_channel)
112
- @error_channel = error_channel
113
- end
155
+ registries = {}
114
156
 
115
- def logger(logger)
116
- @logger = logger
117
- end
157
+ # Set additional options for all the mounts
158
+ @runner_matcher_endpoint_map.each do |runner_factory, matcher_endpoints|
118
159
 
119
- def to_app
120
- bg_route_set = @route_set_factory.build
121
- fg_route_set = @route_set_factory.build
122
-
123
- @route_maps.each do |route_map|
124
- route_map.each do |matcher, route_configs|
125
- route_configs_iterator(route_configs).each do |route_config|
126
- # If the passed in route_config was a String, then we place
127
- # it in an route config as the 'target' field and leave all
128
- # other options as default.
129
- config = (route_config.is_a?(String) ?
130
- { target: route_config } :
131
- route_config)
132
-
133
- # Get the required handler.
134
- # Raises error if handler wasn't declared in config.
135
- target = config.fetch :target
136
- handler_name, action = handler_and_action target
137
-
138
- # Raises error if handler wasn't registered with builder.
139
- handler = @handlers.fetch handler_name
140
-
141
- # Lookup the parser if it was defined.
142
- # The Parser MAY be either an object or string.
143
- # If String, then we're going to look up in parser map.
144
- # Raises an error if a parser (string) was declared, but not
145
- # registered with the builder.
146
- parser = config[:message_parser]
147
- parser = @message_parsers.fetch(parser) if parser.is_a?(String)
148
-
149
- # We have the handler, action and parser.
150
- # Now we're going to add that route to either the background
151
- # or foreground route_set.
152
- background = config.fetch(:background) { @background }
153
- (background ? bg_route_set : fg_route_set).add_route(
154
- matcher: matcher,
155
- handler: handler,
156
- action: action,
157
- message_parser: parser)
158
- end
160
+ # 1. Create a registry for our given hash of matchers to endpoints.
161
+ # 2. Convert the hash of matcher to endpoints into a useable registry
162
+ # for lookups.
163
+ registry = ::Serf::Routing::Registry.new
164
+ matcher_endpoints.each do |matcher, endpoints|
165
+ registry.add matcher, endpoints
159
166
  end
160
- end
161
-
162
- # Get or make our foreground runner
163
- fg_runner = @foreground_runner_factory.build(
164
- results_channel: @results_channel,
165
- error_channel: @error_channel,
166
- logger: @logger)
167
-
168
- # Get or make our background runner
169
- bg_runner = @background_runner_factory.build(
170
- results_channel: @results_channel,
171
- error_channel: @error_channel,
172
- logger: @logger)
173
167
 
174
- # We create the route_sets dependent on built routes.
175
- route_sets = {}
176
- if fg_route_set.size > 0
177
- route_sets[fg_route_set] = fg_runner
178
- end
179
- if bg_route_set.size > 0
180
- route_sets[bg_route_set] = bg_runner
168
+ # Ok, we'll create the runner and add it to our registries hash
169
+ # if we actually have endpoints here.
170
+ if registry.size > 0
171
+ runner_params = @runner_params[runner_factory] ?
172
+ @runner_params[runner_factory] :
173
+ []
174
+ runner_params << (runner_params.last.is_a?(Hash) ?
175
+ runner_params.pop.merge(serf_options) :
176
+ serf_options)
177
+ runner = runner_factory.build *runner_params
178
+ registries[runner] = registry
179
+ end
181
180
  end
182
181
 
183
- # By default, we go to the not_found app.
184
- app = @not_found
185
-
186
- # But if we have routes, then we'll build a serfer to handle it.
187
- if route_sets.size > 0
188
- app = @serfer_factory.build(
189
- route_sets: route_sets,
182
+ if registries.size > 0
183
+ app = Serf::Serfer.build(
184
+ # The registries to match, and their runners to execute.
185
+ registries: registries,
186
+ # App if no endpoints were found.
190
187
  not_found: app,
188
+ # Serf infrastructure options to pass to 'connected' Endpoints
189
+ # to build a handler instance for each env hash received.
190
+ serf_options: serf_options,
191
+ # Options to use by serfer because it includes ErrorHandling.
191
192
  error_channel: @error_channel,
192
193
  logger: @logger)
193
194
  end
@@ -197,42 +198,5 @@ module Serf
197
198
 
198
199
  return app
199
200
  end
200
-
201
- private
202
-
203
- ##
204
- # This handles route_configs that are Array, Hash or String.
205
- # We want to create a proper iterator to run over the route_configs.
206
- def route_configs_iterator(route_configs)
207
- case route_configs
208
- when String
209
- return Array(route_configs)
210
- when Hash
211
- return [route_configs]
212
- else
213
- return route_configs
214
- end
215
- end
216
-
217
- ##
218
- # Extracts the handler_name and action from the 'target' using
219
- # the shortcut convention similar to Rails routing.
220
- #
221
- # 'my_handler#my_method' => # my_method action.
222
- # 'my_handler#' => # action defaults to 'call' method.
223
- # 'my_handler' => # action defaults to 'call' method.
224
- # '#my_method' => # some registered handler name with empty string.
225
- #
226
- # @param [String] target the handler and action description.
227
- # @return the splat handler and action.
228
- #
229
- def handler_and_action(target)
230
- handler, action = target.split '#', 2
231
- handler = handler.to_s.strip
232
- action = action.to_s.strip
233
- action = :call if action.size == 0
234
- return handler, action
235
- end
236
201
  end
237
-
238
202
  end
@@ -0,0 +1,113 @@
1
+ require 'active_support/concern'
2
+ require 'active_support/core_ext/class/attribute'
3
+
4
+ require 'serf/util/options_extraction'
5
+
6
+ module Serf
7
+
8
+ ##
9
+ # A base class for Serf users to implement a Command pattern.
10
+ #
11
+ # class MyCommand
12
+ # include Serf::Command
13
+ #
14
+ # # Set a default Message Parser for the class.
15
+ # self.request_factory = MySerfRequestMessage
16
+ #
17
+ # def initialize(*args, &block)
18
+ # # Do some validation here, or extra parameter setting with the args
19
+ # end
20
+ #
21
+ # def call
22
+ # # Do something w/ @request and @opts
23
+ # return nil # e.g. MySerfMessage
24
+ # end
25
+ # end
26
+ #
27
+ # MyCommand.call(REQUEST_ENV, some, extra, params, options_hash, &block)
28
+ #
29
+ # # Built in lambda wrapping to use the MyCommand with GirlFriday.
30
+ # worker = MyCommand.worker some, extra, params, options_hash, &block
31
+ # work_queue = GirlFriday::WorkQueue.new &worker
32
+ # work_queue.push REQUEST_ENV
33
+ #
34
+ module Command
35
+ extend ActiveSupport::Concern
36
+ include Serf::Util::OptionsExtraction
37
+
38
+ included do
39
+ class_attribute :request_factory
40
+ attr_reader :request
41
+ end
42
+
43
+ def call
44
+ raise NotImplementedError
45
+ end
46
+
47
+ def validate_request!
48
+ # We must verify that the request is valid, but only if the
49
+ # request object isn't a hash.
50
+ unless request.is_a?(Hash) || request.valid?
51
+ raise ArgumentError, request.full_error_messages
52
+ end
53
+ end
54
+
55
+ module ClassMethods
56
+
57
+ ##
58
+ # Class method that both builds then executes the unit of work.
59
+ #
60
+ def call(*args, &block)
61
+ self.build(*args, &block).call
62
+ end
63
+
64
+ ##
65
+ # Factory build method that creates an object of the implementing
66
+ # class' unit of work with the given parameters.
67
+ #
68
+ def build(*args, &block)
69
+ # The very first argument is the Request, we shift it off the args var.
70
+ req = args.shift
71
+ req = {} if req.nil?
72
+
73
+ # Now we allocate the object, and do some options extraction that may
74
+ # modify the args array by popping off the last element if it is a hash.
75
+ obj = allocate
76
+ obj.send :__send__, :extract_options!, args
77
+
78
+ # If the request was a hash, we MAY be able to convert it into a
79
+ # request object. We only do this if a request_factory was set either
80
+ # in the options, or if the request_factory class attribute is set.
81
+ # Otherwise, just give the command the hash, and it is up to them
82
+ # to understand what was given to it.
83
+ factory = obj.opts :request_factory, self.request_factory
84
+ request = (req.is_a?(Hash) && factory ? factory.build(req) : req)
85
+
86
+ # Set the request instance variable to whatever type of request we got.
87
+ obj.instance_variable_set :@request, request
88
+
89
+ # Now validate that the request is ok.
90
+ # Implementing classes MAY override this method to do different
91
+ # kind of request validation.
92
+ obj.validate_request!
93
+
94
+ # Finalize the object's construction with the rest of the args & block.
95
+ obj.send :__send__, :initialize, *args, &block
96
+
97
+ return obj
98
+ end
99
+
100
+ ##
101
+ # Generates a curried function to execute a Command's call class method.
102
+ #
103
+ # @returns lambda block to execute a call.
104
+ #
105
+ def worker(*args, &block)
106
+ lambda { |message|
107
+ self.call message, *args, &block
108
+ }
109
+ end
110
+
111
+ end
112
+ end
113
+ end
data/lib/serf/message.rb CHANGED
@@ -15,6 +15,8 @@ module Serf
15
15
  included do
16
16
  class_attribute :kind
17
17
  send 'kind=', self.to_s.tableize.singularize
18
+ class_attribute :model_name
19
+ send 'model_name=', self.to_s
18
20
  end
19
21
 
20
22
  def kind
@@ -33,10 +35,22 @@ module Serf
33
35
  to_hash.to_json *args
34
36
  end
35
37
 
38
+ def model
39
+ self.class
40
+ end
41
+
42
+ def full_error_messages
43
+ errors.full_messages.join '. '
44
+ end
45
+
36
46
  module ClassMethods
37
47
 
38
- def parse(*args)
39
- self.new *args
48
+ def parse(*args, &block)
49
+ self.new *args, &block
50
+ end
51
+
52
+ def build(*args, &block)
53
+ self.new *args, &block
40
54
  end
41
55
 
42
56
  end
@@ -9,7 +9,7 @@ module Messages
9
9
  # exception during the processing of some message, which is
10
10
  # represented by the context field.
11
11
  #
12
- # Instances of this class are norminally published to an
12
+ # Instances of this class are norminally pushed to an
13
13
  # error channel for out of band processing/notification.
14
14
  #
15
15
  class CaughtExceptionEvent
@@ -17,21 +17,24 @@ module Messages
17
17
  include ::Serf::Util::Uuidable
18
18
 
19
19
  attr_accessor :context
20
- attr_accessor :error_message
21
- attr_accessor :error_backtrace
20
+ attr_accessor :error
21
+ attr_accessor :message
22
+ attr_accessor :backtrace
22
23
 
23
24
  def initialize(options={})
24
25
  @context = options[:context]
25
- @error_message = options[:error_message]
26
- @error_backtrace = options[:error_backtrace]
26
+ @error = options[:error]
27
+ @message = options[:message]
28
+ @backtrace = options[:backtrace]
27
29
  @uuid = options[:uuid]
28
30
  end
29
31
 
30
32
  def attributes
31
33
  {
32
34
  'context' => @context,
33
- 'error_message' => @error_message,
34
- 'error_backtrace' => @error_backtrace,
35
+ 'error' => @error,
36
+ 'message' => @message,
37
+ 'backtrace' => @backtrace,
35
38
  'uuid' => uuid
36
39
  }
37
40
  end
@@ -0,0 +1,49 @@
1
+ require 'serf/util/options_extraction'
2
+
3
+ module Serf
4
+ module Routing
5
+
6
+ ##
7
+ # An endpoint is the description of how to build a Unit of Work
8
+ # for a given matched message. It builds an instance that
9
+ # responds to the `call` method that will actually execute the work.
10
+ # Units of work is built on every received message with the request,
11
+ # given arguments, options (merged with serf infrastructure options)
12
+ # and given block.
13
+ #
14
+ class Endpoint
15
+ include Serf::Util::OptionsExtraction
16
+
17
+ def initialize(connect, handler_factory, *args, &block)
18
+ # If we want to connect serf options, then we try to extract
19
+ # any possible options from the args list. If a hash exists at the
20
+ # end of the args list, then we'll merge into it. Otherwise a new hash
21
+ # will be added on.
22
+ extract_options! args if @connect = connect
23
+
24
+ @handler_factory= handler_factory
25
+ @args = args
26
+ @block = block
27
+ end
28
+
29
+ ##
30
+ # Builds a Unit of Work object.
31
+ #
32
+ def build(env, serf_options={})
33
+ # If we are connecting serf options, then we need to pass these
34
+ # options on to the builder.
35
+ if @connect
36
+ @handler_factory.build(
37
+ env.dup,
38
+ *@args,
39
+ options.merge(serf_options),
40
+ &@block)
41
+ else
42
+ @handler_factory.build env.dup, *@args, &@block
43
+ end
44
+ end
45
+
46
+ end
47
+
48
+ end
49
+ end
@@ -0,0 +1,66 @@
1
+ require 'serf/util/regexp_matcher'
2
+
3
+ module Serf
4
+ module Routing
5
+
6
+ ##
7
+ # EndpointRegistry returns list of Endpoints to execute that match
8
+ # criteria based on the Endpoints' associated 'matcher' object
9
+ # with the input of the ENV Hash (passed to match).
10
+ #
11
+ class Registry
12
+
13
+ def initialize(options={})
14
+ @endpoints = {}
15
+ @matchers = []
16
+ @regexp_matcher_factory = options.fetch(:regexp_matcher_factory) {
17
+ ::Serf::Util::RegexpMatcher
18
+ }
19
+ end
20
+
21
+ ##
22
+ # Connects a matcher (String or an Object implementing ===) to endpoints.
23
+ #
24
+ def add(matcher, endpoints)
25
+ # Maybe we have an non-String matcher. Handle the Regexp case.
26
+ # We only keep track of matchers if it isn't a string because
27
+ # string matchers are just pulled out of endpoints by key lookup.
28
+ matcher = @regexp_matcher_factory.build matcher if matcher.kind_of? Regexp
29
+ @matchers << matcher unless matcher.is_a? String
30
+
31
+ # We add the (matcher+endpoint) into our endpoints
32
+ @endpoints[matcher] ||= []
33
+ @endpoints[matcher].concat endpoints
34
+ end
35
+
36
+ ##
37
+ # @param [Hash] env The input message environment to match for endpoints.
38
+ # @return [Array] List of endpoints that matched.
39
+ #
40
+ def match(env={})
41
+ kind = env[:kind]
42
+ endpoints = []
43
+ endpoints.concat @endpoints.fetch(kind) { [] }
44
+ @matchers.each do |matcher|
45
+ endpoints.concat @endpoints[matcher] if matcher === env
46
+ end
47
+ return endpoints
48
+ end
49
+
50
+ ##
51
+ # @return [Integer] Number of matchers this EndpointsMap tracks.
52
+ #
53
+ def size
54
+ return @endpoints.size
55
+ end
56
+
57
+ ##
58
+ # Default factory method.
59
+ #
60
+ def self.build(options={})
61
+ self.new options
62
+ end
63
+ end
64
+
65
+ end
66
+ end