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 +106 -305
- data/lib/serf/builder.rb +92 -128
- data/lib/serf/command.rb +36 -73
- data/lib/serf/errors/not_found.rb +8 -0
- data/lib/serf/middleware/girl_friday_async.rb +39 -0
- data/lib/serf/middleware/masherize.rb +25 -0
- data/lib/serf/middleware/uuid_tagger.rb +14 -8
- data/lib/serf/{util → routing}/regexp_matcher.rb +12 -6
- data/lib/serf/routing/route.rb +35 -0
- data/lib/serf/routing/route_set.rb +64 -0
- data/lib/serf/serfer.rb +56 -114
- data/lib/serf/util/error_handling.rb +5 -9
- data/lib/serf/util/options_extraction.rb +16 -5
- data/lib/serf/util/protected_call.rb +3 -2
- data/lib/serf/util/uuidable.rb +28 -4
- data/lib/serf/version.rb +1 -1
- data/serf.gemspec +8 -11
- metadata +35 -38
- data/lib/serf/more/command_worker.rb +0 -45
- data/lib/serf/routing/endpoint.rb +0 -49
- data/lib/serf/routing/registry.rb +0 -66
- data/lib/serf/runners/direct.rb +0 -52
- data/lib/serf/runners/event_machine.rb +0 -69
- data/lib/serf/runners/girl_friday.rb +0 -69
- data/lib/serf/runners/helper.rb +0 -23
- data/lib/serf/util/mash_factory.rb +0 -18
data/README.md
CHANGED
@@ -1,8 +1,11 @@
|
|
1
1
|
serf
|
2
2
|
====
|
3
3
|
|
4
|
-
Serf is a library that
|
5
|
-
|
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
|
36
|
-
|
37
|
-
|
38
|
-
The
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
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
|
-
|
69
|
-
|
70
|
-
|
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
|
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
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
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/
|
123
|
-
class
|
117
|
+
# my_lib/my_policy.rb
|
118
|
+
class MyPolicy
|
124
119
|
|
125
|
-
def
|
126
|
-
raise 'Data is nil' if
|
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
|
-
|
134
|
+
attr_reader :name
|
135
|
+
attr_reader :do_raise
|
136
136
|
|
137
137
|
def initialize(*args)
|
138
|
-
|
139
|
-
|
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
|
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
|
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
|
170
|
-
error_channel MyChannel.new
|
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
|
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: '
|
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
|
-
|
213
|
-
results = app.call
|
214
|
-
|
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
|
-
|
218
|
-
results = app.call
|
219
|
-
|
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
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
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
|
-
|
245
|
-
results = app.call
|
246
|
-
|
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
|
-
|
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
|
-
|
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
|
|