serf 0.9.0 → 0.10.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.
- 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
|
|