serf 0.9.0 → 0.10.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -1,8 +1,11 @@
1
1
  serf
2
2
  ====
3
3
 
4
- Serf is a library that scaffolds distributed systems that are architected using
5
- Event-Driven Service Oriented Architecture design in combinations with
4
+ Serf is simply a Rack-like library that, when called, routes received
5
+ messages (requests or events) to "Command" handlers.
6
+
7
+ The pattern of Command objects and messaging gives us nice primatives
8
+ for Event-Driven Service Oriented Architecture in combination with
6
9
  the Command Query Responsibility Separation pattern.
7
10
 
8
11
  Philosophy
@@ -32,29 +35,28 @@ Handlers may process observed Events that 3rd party services emit.
32
35
  Serf App and Channels
33
36
  =====================
34
37
 
35
- A Serf App is a Rack-like application that accepts an ENV hash as input.
36
- This ENV hash is simply the hash representation of a message to be processed.
37
-
38
- The Serf App, as configured by DSL, will:
39
- 1. route the ENV to the proper Endpoint
40
- 2. The endpoint will create a handler instance.
41
- a. The handler's build class method will be given an ENV, which
42
- may be handled as fit. The Command class will help by parsing it
43
- into a Message object as registered by the implementing subclass.
44
- 3. Push the handler's results to a response channel.
45
- a. Raised errors are caught and pushed to an error channel.
46
- b. These channels are normally message queuing channels.
47
- c. We only require the channel instance to respond to the 'push'
48
- method and accept a message (or message hash) as the argument.
49
- 4. return the handler's results to the caller if it is blocking mode.
50
- a. In non-blocking mode, an MessageAcceptedEvent is returned instead
51
- because the message will be run by the EventMachine deferred threadpool
52
- by default.
53
-
54
- Not implemented as yet:
55
- 1. Create a message queue listener or web endpoint to accept messages
56
- and run them through the built Serf App.
57
- 2. Message Queue channels to route responses to pub/sub locations.
38
+ A Serf App is a Rack-like application that takes in an ENV hash with
39
+ TWO important fields: message and context.
40
+
41
+ The Message is the request or event. The data that is to be processed.
42
+
43
+ The Context is meta data that needs to be taken into account about the
44
+ processing of this data. The main important field may be a current user.
45
+ Though, it is not explicitly defined.
46
+
47
+ A request life cycle involves:
48
+ 1. Submitting a message and context to the SerfApp
49
+ 2. The Serf app will run middleware as defined by the DSL.
50
+ 3. Matched routes are found for the message and context.
51
+ 4. Run each route:
52
+ a. Run the policy chain for each route (This is for ACLs)
53
+ b. If no exception was raised, execute the route.
54
+ 5. Return all non-nil results to the caller.
55
+ a. Each result is a Message.
56
+
57
+ If set in the DSL, the success and error responses may be published
58
+ to a response and/or error channel.
59
+
58
60
 
59
61
  Service Libraries
60
62
  =================
@@ -65,10 +67,9 @@ Service Libraries
65
67
  b. By default, Serf Commands will read in hashes and turn them into
66
68
  more convenient Hashie::Mash objects.
67
69
  2. Serialization of the messages SHOULD BE Json or MessagePack (I hate XML).
68
- a. Serf Builder will create Serf Apps that expect a HASH ENV as input
69
- 3. Handlers MUST implement the 'build' class method, which
70
- MUST receive the ENV Hash as the first parameter, followed by supplemental
71
- arguments as declared in the DSL.
70
+ 3. Handlers MUST implement the 'build' class method.
71
+ a. This can just be aliased to new. But is made explicit in case we have
72
+ custom factories.
72
73
  4. Handler methods SHOULD return zero or more messages.
73
74
  a. Raised errors are caught and pushed to error channels.
74
75
  b. Returned messages MUST be Hash based objects for Serialization.
@@ -78,12 +79,12 @@ Service Libraries
78
79
  generic CaughtExceptionEvents, and are harder to deal with.
79
80
 
80
81
 
81
- Example With GirlFriday
82
- =======================
82
+ Example
83
+ =======
83
84
 
84
85
  # Require our libraries
85
- require 'log4r'
86
86
  require 'json'
87
+ require 'yell'
87
88
 
88
89
  require 'serf/builder'
89
90
  require 'serf/command'
@@ -91,16 +92,10 @@ Example With GirlFriday
91
92
  require 'serf/util/options_extraction'
92
93
 
93
94
  # create a simple logger for this example
94
- outputter = Log4r::FileOutputter.new(
95
- 'fileOutputter',
96
- filename: 'log.txt')
97
- ['tick',
98
- 'serf',
99
- 'hndl',
100
- 'resp',
101
- 'errr'].each do |name|
102
- logger = Log4r::Logger.new name
103
- logger.outputters = outputter
95
+ my_logger = Yell.new do |l|
96
+ l.level = :debug
97
+ l.adapter :datefile, 'my_production.log', :level => [:debug, :info, :warn]
98
+ l.adapter :datefile, 'my_error.log', :level => Yell.level.gte(:error)
104
99
  end
105
100
 
106
101
  # Helper class for this example to receive result or error messages
@@ -112,18 +107,22 @@ Example With GirlFriday
112
107
  end
113
108
  def push(message)
114
109
  if @error
115
- @logger.fatal "#{message}"
110
+ @logger.fatal "ERROR CHANNEL: #{message.to_json}"
116
111
  else
117
- @logger.debug "#{message}"
112
+ @logger.debug "RESP CHANNEL: #{message.to_json}"
118
113
  end
119
114
  end
120
115
  end
121
116
 
122
- # my_lib/my_validator.rb
123
- class MyValidator
117
+ # my_lib/my_policy.rb
118
+ class MyPolicy
124
119
 
125
- def self.validate!(data)
126
- raise 'Data is nil' if data[:data].nil?
120
+ def check!(message, context)
121
+ raise 'EXPECTED ERROR: Data is nil' if message[:data].nil?
122
+ end
123
+
124
+ def self.build(*args, &block)
125
+ new *args, &block
127
126
  end
128
127
 
129
128
  end
@@ -132,30 +131,23 @@ Example With GirlFriday
132
131
  class MyOverloadedCommand
133
132
  include Serf::Command
134
133
 
135
- self.request_validator = MyValidator
134
+ attr_reader :name
135
+ attr_reader :do_raise
136
136
 
137
137
  def initialize(*args)
138
- super
139
- raise "Constructor Error: #{opts(:name)}" if opts :raises_in_new
138
+ extract_options! args
139
+ @name = opts! :name
140
+ @do_raise = opts :raises, false
140
141
  end
141
142
 
142
- def call
143
- # Set our logger
144
- logger = ::Log4r::Logger['hndl']
145
-
143
+ def call(request, context)
146
144
  # Just our name to sort things out
147
- name = opts! :name
148
- logger.info "#{name}: #{request.to_json}"
149
145
 
150
- raise "Forcing Error in #{name}" if opts(:raises, false)
146
+ raise "EXPECTED ERROR: Forcing Error in #{name}" if do_raise
151
147
 
152
148
  # Do work Here...
153
149
  # And return 0 or more messages as result. Nil is valid response.
154
- return { kind: "#{name}_result", input: request.to_hash }
155
- end
156
-
157
- def inspect
158
- "MyOverloadedCommand: #{opts(:name,'notnamed')}, #{request.to_json}"
150
+ return { kind: "#{name}_result", input: request }
159
151
  end
160
152
 
161
153
  end
@@ -163,279 +155,88 @@ Example With GirlFriday
163
155
  # Create a new builder for this serf app.
164
156
  builder = Serf::Builder.new do
165
157
  # Include some middleware
158
+ use Serf::Middleware::Masherize
166
159
  use Serf::Middleware::UuidTagger
160
+ #use Serf::Middleware::GirlFridayAsync
167
161
 
168
162
  # Create response and error channels for the handler result messages.
169
- response_channel MyChannel.new(::Log4r::Logger['resp'])
170
- error_channel MyChannel.new(::Log4r::Logger['errr'], true)
163
+ response_channel MyChannel.new my_logger
164
+ error_channel MyChannel.new my_logger, true
171
165
 
172
166
  # We pass in a logger to our Serf code: Serfer and Runners.
173
- logger ::Log4r::Logger['serf']
174
-
175
- runner :direct
167
+ logger my_logger
176
168
 
169
+ # Here, we define a route.
170
+ # We are matching the kind for 'my_message', and we have the MyPolicy
171
+ # to filter for this route.
177
172
  match 'my_message'
173
+ policy MyPolicy
178
174
  run MyOverloadedCommand, name: 'my_message_command'
179
175
 
180
- match 'raise_error_message'
181
- run MyOverloadedCommand, name: 'foreground_raises_error', raises: true
182
- run MyOverloadedCommand, name: 'constructor_error', raises_in_new: true
183
-
184
176
  match 'other_message'
185
- run MyOverloadedCommand, name: 'foreground_other_message'
186
-
187
- runner :girl_friday
188
-
189
- # This message kind is handled by multiple handlers.
190
- match 'other_message'
191
- run MyOverloadedCommand, name: 'background_other_message'
192
- run MyOverloadedCommand, name: 'background_raises error', raises: true
177
+ run MyOverloadedCommand, name: 'raises_error', raises: true
178
+ run MyOverloadedCommand, name: 'good_other_handler'
193
179
 
194
180
  match /^events\/.*$/
195
181
  run MyOverloadedCommand, name: 'regexp_matched_command'
196
-
197
- # Optionally define a not found handler...
198
- # Defaults to raising an ArgumentError, 'Not Found'.
199
- #not_found lambda {|x| puts x}
200
182
  end
201
183
  app = builder.to_app
202
184
 
203
- # Start event machine loop.
204
- logger = ::Log4r::Logger['tick']
205
-
206
- logger.info "Start Tick #{Thread.current.object_id}"
207
-
208
185
  # This will submit a 'my_message' message (as a hash) to the Serf App.
209
186
  # NOTE: We should get an error message pushed to the error channel
210
187
  # because no 'data' field was put in my_message as required
211
188
  # And the Result should have a CaughtExceptionEvent.
212
- logger.info "BEG MyMessage w/o Data"
213
- results = app.call 'kind' => 'my_message'
214
- logger.info "END MyMessage w/o Data: #{results.size} #{results.to_json}"
189
+ my_logger.info 'Call 1: Start'
190
+ results = app.call(
191
+ message: {
192
+ kind: 'my_message'
193
+ },
194
+ context: nil)
195
+ my_logger.info "Call 1: #{results.size} #{results.to_json}"
215
196
 
216
197
  # Here is good result
217
- logger.info "BEG MyMessage w/ Data"
218
- results = app.call 'kind' => 'my_message', 'data' => '1'
219
- logger.info "END MyMessage w/ Data: #{results.size} #{results.to_json}"
198
+ my_logger.info 'Call 2: Start'
199
+ results = app.call(
200
+ message: {
201
+ kind: 'my_message',
202
+ data: '2'
203
+ },
204
+ context: nil)
205
+ my_logger.info "Call 2: #{results.size} #{results.to_json}"
220
206
 
221
- # Here is a result that will raise an error in foreground
222
207
  # We should get two event messages in the results because we
223
- # mounted two commands to the raise_error_message kind.
224
- # Each shows errors being raised in two separate stages.
225
- # 1. Error in creating the instance of the command.
226
- # 2. Error when the command was executed by the foreground runner.
227
- logger.info "BEG RaisesErrorMessage"
228
- results = app.call 'kind' => 'raise_error_message', 'data' => '2'
229
- logger.info "END RaisesErrorMessage: #{results.size} #{results.to_json}"
230
-
231
- # This submission will be executed by THREE commands.
232
- # One in the foreground, two in the background.
233
- #
234
- # The foreground results should be:
235
- # * MessageAcceptedEvent
236
- # * And return result of one command call
237
- #
238
- # The error channel should output an error from one background command.
239
- logger.info "BEG OtherMessage"
240
- results = app.call 'kind' => 'other_message', 'data' => '3'
241
- logger.info "END OtherMessage: #{results.size} #{results.to_json}"
208
+ # mounted two commands to the other_message kind.
209
+ my_logger.info 'Call 3: Start'
210
+ results = app.call(
211
+ message: {
212
+ kind: 'other_message',
213
+ data: '3'
214
+ },
215
+ context: nil)
216
+ my_logger.info "Call 3: #{results.size} #{results.to_json}"
242
217
 
243
218
  # This will match a regexp call
244
- logger.info "BEG Regexp"
245
- results = app.call 'kind' => 'events/my_event', 'data' => '4'
246
- logger.info "END Regexp Results: #{results.size} #{results.to_json}"
219
+ my_logger.info 'Call 4: Start'
220
+ results = app.call(
221
+ message: {
222
+ kind: 'events/my_event',
223
+ data: '4'
224
+ },
225
+ context: nil)
226
+ my_logger.info "Call 4: #{results.size} #{results.to_json}"
247
227
 
248
228
  begin
249
229
  # Here, we're going to submit a message that we don't handle.
250
230
  # By default, an exception will be raised.
251
- app.call 'kind' => 'unhandled_message_kind'
231
+ my_logger.info 'Call 5: Start'
232
+ app.call(
233
+ message: {
234
+ kind: 'unhandled_message_kind'
235
+ },
236
+ context: nil)
237
+ my_logger.fatal 'OOOPS: Should not get here'
252
238
  rescue => e
253
- logger.warn "Caught in Tick: #{e.inspect}"
254
- end
255
- logger.info "End Tick #{Thread.current.object_id}"
256
-
257
-
258
- Example With EventMachine
259
- =========================
260
-
261
- # Require our libraries
262
- require 'log4r'
263
- require 'json'
264
-
265
- require 'serf/builder'
266
- require 'serf/command'
267
- require 'serf/middleware/uuid_tagger'
268
- require 'serf/util/options_extraction'
269
-
270
- # create a simple logger for this example
271
- outputter = Log4r::FileOutputter.new(
272
- 'fileOutputter',
273
- filename: 'log.txt')
274
- ['tick',
275
- 'serf',
276
- 'hndl',
277
- 'resp',
278
- 'errr'].each do |name|
279
- logger = Log4r::Logger.new name
280
- logger.outputters = outputter
281
- end
282
-
283
- # Helper class for this example to receive result or error messages
284
- # and pipe it into our logger.
285
- class MyChannel
286
- def initialize(logger, error=false)
287
- @logger = logger
288
- @error = error
289
- end
290
- def push(message)
291
- if @error
292
- @logger.fatal "#{message}"
293
- else
294
- @logger.debug "#{message}"
295
- end
296
- end
297
- end
298
-
299
- # my_lib/my_message.rb
300
- class MyValidator
301
-
302
- def self.validate!(data)
303
- raise 'Data Missing Error' if data[:data].nil?
304
- end
305
-
306
- end
307
-
308
- # my_lib/my_overloaded_command.rb
309
- class MyOverloadedCommand
310
- include Serf::Command
311
-
312
- self.request_validator = MyValidator
313
-
314
- def initialize(*args)
315
- super
316
- raise "Constructor Error: #{opts(:name)}" if opts :raises_in_new
317
- end
318
-
319
- def call
320
- # Set our logger
321
- logger = ::Log4r::Logger['hndl']
322
-
323
- # Just our name to sort things out
324
- name = opts! :name
325
- logger.info "#{name}: #{request.to_json}"
326
-
327
- raise "Forcing Error in #{name}" if opts(:raises, false)
328
-
329
- # Do work Here...
330
- # And return 0 or more messages as result. Nil is valid response.
331
- return { kind: "#{name}_result", input: request.to_hash }
332
- end
333
-
334
- def inspect
335
- "MyOverloadedCommand: #{opts(:name,'notnamed')}, #{request.to_json}"
336
- end
337
-
338
- end
339
-
340
- # Create a new builder for this serf app.
341
- builder = Serf::Builder.new do
342
- # Include some middleware
343
- use Serf::Middleware::UuidTagger
344
-
345
- # Create response and error channels for the handler result messages.
346
- response_channel MyChannel.new(::Log4r::Logger['resp'])
347
- error_channel MyChannel.new(::Log4r::Logger['errr'], true)
348
-
349
- # We pass in a logger to our Serf code: Serfer and Runners.
350
- logger ::Log4r::Logger['serf']
351
-
352
- runner :direct
353
-
354
- match 'my_message'
355
- run MyOverloadedCommand, name: 'my_message_command'
356
-
357
- match 'raise_error_message'
358
- run MyOverloadedCommand, name: 'foreground_raises_error', raises: true
359
- run MyOverloadedCommand, name: 'constructor_error', raises_in_new: true
360
-
361
- match 'other_message'
362
- run MyOverloadedCommand, name: 'foreground_other_message'
363
-
364
- runner :event_machine
365
-
366
- # This message kind is handled by multiple handlers.
367
- match 'other_message'
368
- run MyOverloadedCommand, name: 'background_other_message'
369
- run MyOverloadedCommand, name: 'background_raises error', raises: true
370
-
371
- match /^events\/.*$/
372
- run MyOverloadedCommand, name: 'regexp_matched_command'
373
-
374
- # Optionally define a not found handler...
375
- # Defaults to raising an ArgumentError, 'Not Found'.
376
- #not_found lambda {|x| puts x}
377
- end
378
- app = builder.to_app
379
-
380
- # Start event machine loop.
381
- logger = ::Log4r::Logger['tick']
382
- EM.run do
383
- # On the next tick
384
- EM.next_tick do
385
- logger.info "Start Tick #{Thread.current.object_id}"
386
-
387
- # This will submit a 'my_message' message (as a hash) to the Serf App.
388
- # NOTE: We should get an error message pushed to the error channel
389
- # because no 'data' field was put in my_message as required
390
- # And the Result should have a CaughtExceptionEvent.
391
- logger.info "BEG MyMessage w/o Data"
392
- results = app.call 'kind' => 'my_message'
393
- logger.info "END MyMessage w/o Data: #{results.size} #{results.to_json}"
394
-
395
- # Here is good result
396
- logger.info "BEG MyMessage w/ Data"
397
- results = app.call 'kind' => 'my_message', 'data' => '1'
398
- logger.info "END MyMessage w/ Data: #{results.size} #{results.to_json}"
399
-
400
- # Here is a result that will raise an error in foreground
401
- # We should get two event messages in the results because we
402
- # mounted two commands to the raise_error_message kind.
403
- # Each shows errors being raised in two separate stages.
404
- # 1. Error in creating the instance of the command.
405
- # 2. Error when the command was executed by the foreground runner.
406
- logger.info "BEG RaisesErrorMessage"
407
- results = app.call 'kind' => 'raise_error_message', 'data' => '2'
408
- logger.info "END RaisesErrorMessage: #{results.size} #{results.to_json}"
409
-
410
- # This submission will be executed by THREE commands.
411
- # One in the foreground, two in the background.
412
- #
413
- # The foreground results should be:
414
- # * MessageAcceptedEvent
415
- # * And return result of one command call
416
- #
417
- # The error channel should output an error from one background command.
418
- logger.info "BEG OtherMessage"
419
- results = app.call 'kind' => 'other_message', 'data' => '3'
420
- logger.info "END OtherMessage: #{results.size} #{results.to_json}"
421
-
422
- # This will match a regexp call
423
- logger.info "BEG Regexp"
424
- results = app.call 'kind' => 'events/my_event', 'data' => '4'
425
- logger.info "END Regexp Results: #{results.size} #{results.to_json}"
426
-
427
- begin
428
- # Here, we're going to submit a message that we don't handle.
429
- # By default, an exception will be raised.
430
- app.call 'kind' => 'unhandled_message_kind'
431
- rescue => e
432
- logger.warn "Caught in Tick: #{e.inspect}"
433
- end
434
- logger.info "End Tick #{Thread.current.object_id}"
435
- end
436
- EM.add_timer 2 do
437
- EM.stop
438
- end
239
+ my_logger.info "Call 5: Caught in main: #{e.inspect}"
439
240
  end
440
241
 
441
242