Browse files

Merge branch 'release/v0.8.0'

* release/v0.8.0:
  Version bump to v0.8.0 and regenerated gemspec.
  CommandWorker is a 'command' that runs commands.
  Remove redundancy: obj.send(:__send__, ...)
  Enhances ErrorHandling with ProtectedCall and custom errback.
  Adds Serf::Util::ProtectedCall module.
  CodedUUID is now Timestamp based.
  Adds Serf::More::UuidFields
  Uuid fields are now 'coded' (base64'd).
  Fix: Pass response channel to serf runners.
  Fix: GirlFriday pushing results to response channel.
  Remove 'kind' instance method because it is shadowed.
  • Loading branch information...
2 parents 5e318f9 + 9fac142 commit 78118e213e5dd8f5bb73aae23be26ca1f3dd9468 @byu committed Apr 19, 2012
View
2 lib/serf/builder.rb
@@ -139,7 +139,7 @@ def params(*args)
# to be passed to Endpoint#build methods.
def serf_options
{
- response_channel: @error_channel,
+ response_channel: @response_channel,
error_channel: @error_channel,
logger: @logger
}
View
4 lib/serf/command.rb
@@ -73,7 +73,7 @@ def build(*args, &block)
# Now we allocate the object, and do some options extraction that may
# modify the args array by popping off the last element if it is a hash.
obj = allocate
- obj.send :__send__, :extract_options!, args
+ obj.__send__ :extract_options!, args
# If the request was a hash, we MAY be able to convert it into a
# request object. We only do this if a request_factory was set either
@@ -92,7 +92,7 @@ def build(*args, &block)
obj.validate_request!
# Finalize the object's construction with the rest of the args & block.
- obj.send :__send__, :initialize, *args, &block
+ obj.__send__ :initialize, *args, &block
return obj
end
View
4 lib/serf/message.rb
@@ -19,10 +19,6 @@ module Message
send 'model_name=', self.to_s
end
- def kind
- self.class.kind
- end
-
def to_hash
attributes.merge kind: kind
end
View
6 lib/serf/messages/caught_exception_event.rb
@@ -13,8 +13,8 @@ module Messages
# error channel for out of band processing/notification.
#
class CaughtExceptionEvent
- include ::Serf::Message
- include ::Serf::Util::Uuidable
+ include Serf::Message
+ include Serf::Util::Uuidable
attr_accessor :context
attr_accessor :error
@@ -35,7 +35,7 @@ def attributes
'error' => @error,
'message' => @message,
'backtrace' => @backtrace,
- 'uuid' => uuid
+ 'uuid' => (@uuid || create_coded_uuid)
}
end
View
6 lib/serf/messages/message_accepted_event.rb
@@ -11,8 +11,8 @@ module Messages
# than any errors given.
#
class MessageAcceptedEvent
- include ::Serf::Message
- include ::Serf::Util::Uuidable
+ include Serf::Message
+ include Serf::Util::Uuidable
attr_accessor :message
@@ -24,7 +24,7 @@ def initialize(options={})
def attributes
{
'message' => @message,
- 'uuid' => uuid
+ 'uuid' => (@uuid || create_coded_uuid)
}
end
View
9 lib/serf/middleware/uuid_tagger.rb
@@ -1,4 +1,4 @@
-require 'uuidtools'
+require 'serf/util/uuidable'
module Serf
module Middleware
@@ -9,19 +9,22 @@ module Middleware
# if the incoming request already has it.
#
class UuidTagger
+ include Serf::Util::Uuidable
##
# @param app the app
# @options opts [String] :field the ENV field to set with a UUID.
#
def initialize(app, options={})
@app = app
- @field = options.fetch(:field) { 'serf.request_uuid' }
+ @field = options.fetch(:field) { 'uuid' }
end
def call(env)
env = env.dup
- env[@field] ||= UUIDTools::UUID.random_create.to_s
+ unless env[@field.to_sym] || env[@field.to_s]
+ env[@field] = create_coded_uuid
+ end
@app.call env
end
View
51 lib/serf/more/command_worker.rb
@@ -0,0 +1,51 @@
+require 'serf/command'
+require 'serf/util/error_handling'
+
+module Serf
+module More
+
+ ##
+ # Creates a proc (CommandWorker.worker) that will be used by
+ # GirlFriday WorkQueues. This proc will receive messages from
+ # GirlFriday and will (1) create a CommandWorker instance with the
+ # given message and (2) execute (#call) said CommandWorker instance.
+ # The CommandWorker instance assumes that the received message is
+ # a callable (#call) object, and will execute that object's 'call' method.
+ #
+ # The purpose of this is so we can build Command objects in one
+ # context/thread and have it actually executed in a separate threadpool.
+ #
+ # Example:
+ #
+ # # Creates the GirlFriday work queue.
+ # command_worker_queue = GirlFriday::WorkQueue.new(
+ # CommandWorker.worker(
+ # logger: my_logger,
+ # response_channel: response_channel,
+ # error_channel: error_channel))
+ #
+ # # In some place that receives requests and generates commands.
+ # # Push the command into the command workqueue for async processing.
+ # command_worker_queue.push MyCommand.build(REQUEST_HASH)
+ #
+ class CommandWorker
+ include Serf::Command
+ include Serf::ErrorHandling
+
+ def call
+ ok, results = with_error_handling do
+ request.call
+ end
+ return results
+ end
+
+ # Overriding the Serf::Command validate_request! so it doesn't
+ # call #valid? on the request object. This is because the incoming
+ # request is another Command object itself.
+ def validate_request!
+ end
+
+ end
+
+end
+end
View
29 lib/serf/more/uuid_fields.rb
@@ -0,0 +1,29 @@
+require 'serf/util/uuidable'
+
+module Serf
+module More
+
+ ##
+ # Assumes that Virtus has already been included in the class that
+ # also includes UuidFields.
+ #
+ module UuidFields
+ extend Serf::Util::Uuidable
+
+ def self.included(base)
+ base.attribute :uuid, String, default: lambda { |o,a| create_coded_uuid }
+ base.attribute :parent_uuid, String
+ base.attribute :origin_uuid, String
+ end
+
+ def create_child_uuids
+ {
+ uuid: UuidFields.create_coded_uuid,
+ parent_uuid: uuid,
+ origin_uuid: (origin_uuid || parent_uuid || uuid)
+ }
+ end
+ end
+
+end
+end
View
6 lib/serf/runners/girl_friday.rb
@@ -60,11 +60,11 @@ def self.build(*args)
# queue.
#
def perform(task)
- with_error_handling(task[:context]) do
+ ok, run_result = with_error_handling(task[:context]) do
task[:handler].call
- run_result = run_result.is_a?(Hash) ? [run_result] : Array(run_result)
- push_results run_result
end
+ run_result = run_result.is_a?(Hash) ? [run_result] : Array(run_result)
+ push_results run_result if ok
end
end
View
46 lib/serf/util/error_handling.rb
@@ -3,6 +3,7 @@
require 'serf/messages/caught_exception_event'
require 'serf/util/null_object'
require 'serf/util/options_extraction'
+require 'serf/util/protected_call'
module Serf
module Util
@@ -11,21 +12,34 @@ module Util
# Helper module to rescues exceptions from executing blocks of
# code, and then logs+pushes the error event.
#
+ # Including classes may have the following instance variables
+ # to override the default values:
+ # * @error_event_class - ::Serf::Messages::CaughtExceptionEvent
+ # * @logger - ::Serf::Util::NullObject.new
+ # * @error_channel - ::Serf::Util::NullObject.new
module ErrorHandling
- include ::Serf::Util::OptionsExtraction
+ include Serf::Util::OptionsExtraction
+ include Serf::Util::ProtectedCall
##
# A block wrapper to handle errors when executing a block.
#
- # Including classes may have the following instance variables
- # to override the default values:
- # * @error_event_class - ::Serf::Messages::CaughtExceptionEvent
- # * @logger - ::Serf::Util::NullObject.new
- # * @error_channel - ::Serf::Util::NullObject.new
+ def with_error_handling(context=nil, errback=:handle_error, &block)
+ ok, results = protected_call &block
+ return ok, (ok ?
+ results :
+ ((errback.is_a?(Proc) ?
+ errback.call(results, context) :
+ send(errback, results, context))))
+ end
+
+ ##
+ # Including classes may override this method to do alternate error
+ # handling. By default, this method will create a new caught exception
+ # event and publish it to the error channel. This method will also
+ # log the exception itself to the logger.
#
- def with_error_handling(context=nil)
- return true, yield
- rescue => e
+ def handle_error(e, context=nil)
eec = opts :error_event_class, ::Serf::Messages::CaughtExceptionEvent
logger = opts :logger, ::Serf::Util::NullObject.new
error_channel = opts :error_channel, ::Serf::Util::NullObject.new
@@ -35,20 +49,18 @@ def with_error_handling(context=nil)
message: e.message,
backtrace: e.backtrace.join("\n"))
- # log the error to our logger, and to our error channel.
- logger.error error_event
+ # log the error to our logger
+ logger.error e
+
+ # log the error event to our error channel.
begin
error_channel.push error_event
rescue => e1
- logger.error("
- Failed pushing to ErrorChannel:
- #{e1.message}
- #{e1.backtrace.join('\n')}
- ")
+ logger.error e1
end
# We're done, so just return this error.
- return false, error_event
+ return error_event
end
end
View
35 lib/serf/util/protected_call.rb
@@ -0,0 +1,35 @@
+module Serf
+module Util
+
+ ##
+ # Rescues exceptions raised when calling blocks.
+ #
+ module ProtectedCall
+
+ ##
+ # A block wrapper to catch errors when executing a block. Instead of
+ # raising the error, the error is caught and returned in place of
+ # the block's results.
+ #
+ # ok, results = protected_call do
+ # 1 + 1
+ # end
+ # => [true, 2]
+ #
+ # ok, results = protected_call do
+ # raise 'My Error'
+ # end
+ # => [false, RuntimeError]
+ #
+ # @return boolean success and the block's (or caught exception) results.
+ #
+ def protected_call
+ return true, yield
+ rescue => e
+ return false, e
+ end
+
+ end
+
+end
+end
View
28 lib/serf/util/uuidable.rb
@@ -1,15 +1,37 @@
+require 'base64'
require 'uuidtools'
module Serf
module Util
##
- # Helper module to include UUIDs as message fields.
+ # Helper module to for various UUID tasks.
+ #
+ # 1. Primarily to create and parse 'coded' UUIDs, which are just
+ # base64 encoded UUIDs without trailing '='.
#
module Uuidable
- def uuid
- @uuid ||= UUIDTools::UUID.random_create.to_s
+ ##
+ # Creates a Timestamp UUID, base64 encoded.
+ #
+ # NOTE: UUIDTools TimeStamp code creates a UTC based timestamp UUID.
+ #
+ def create_coded_uuid
+ # All raw UUIDs are 16 bytes long. Base64 lengthens the string to
+ # 24 bytes long. We chomp off the last two equal signs '==' to
+ # trim the string length to 22 bytes. This gives us an overhead
+ # of an extra 6 bytes over raw UUID, but with the readability
+ # benefit. And saves us 14 bytes of size from the 'standard'
+ # string hex representation of UUIDs.
+ Base64.urlsafe_encode64(UUIDTools::UUID.timestamp_create.raw).chomp('==')
+ end
+
+ ##
+ # @param coded_uuid a coded uuid to parse.
+ #
+ def parse_coded_uuid(coded_uuid)
+ UUIDTools::UUID.parse_raw Base64.urlsafe_decode64("#{coded_uuid}==")
end
end
View
2 lib/serf/version.rb
@@ -2,7 +2,7 @@ module Serf
module Version
MAJOR = 0
- MINOR = 7
+ MINOR = 8
PATCH = 0
BUILD = nil
STRING = [MAJOR, MINOR, PATCH, BUILD].compact.join '.'
View
7 serf.gemspec
@@ -5,11 +5,11 @@
Gem::Specification.new do |s|
s.name = "serf"
- s.version = "0.7.0"
+ s.version = "0.8.0"
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
s.authors = ["Benjamin Yu"]
- s.date = "2012-03-13"
+ s.date = "2012-04-19"
s.description = "Event-Driven SOA with CQRS"
s.email = "benjaminlyu@gmail.com"
s.extra_rdoc_files = [
@@ -34,6 +34,8 @@ Gem::Specification.new do |s|
"lib/serf/messages/caught_exception_event.rb",
"lib/serf/messages/message_accepted_event.rb",
"lib/serf/middleware/uuid_tagger.rb",
+ "lib/serf/more/command_worker.rb",
+ "lib/serf/more/uuid_fields.rb",
"lib/serf/routing/endpoint.rb",
"lib/serf/routing/registry.rb",
"lib/serf/runners/direct.rb",
@@ -44,6 +46,7 @@ Gem::Specification.new do |s|
"lib/serf/util/error_handling.rb",
"lib/serf/util/null_object.rb",
"lib/serf/util/options_extraction.rb",
+ "lib/serf/util/protected_call.rb",
"lib/serf/util/regexp_matcher.rb",
"lib/serf/util/uuidable.rb",
"lib/serf/version.rb",

0 comments on commit 78118e2

Please sign in to comment.