serf 0.6.1 → 0.7.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.
@@ -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