lib/bumbleworks/configuration.rb
module Bumbleworks
# Stores configuration information
#
# Configuration information is loaded from a configuration block defined within
# the client application.
#
# @example Standard settings
# Bumbleworks.configure do |c|
# c.definitions_directory = '/path/to/ruote/definitions/directory'
# c.storage = Redis.new(:host => '127.0.0.1', :db => 0, :thread_safe => true)
# # ...
# end
#
class Configuration
attr_reader :storage_adapters
class << self
def define_setting(name)
defined_settings << name
attr_accessor name
end
def defined_settings
@defined_settings ||= []
end
end
# Path to the root folder where Bumbleworks assets can be found. By default,
# this path will be the root returned by the detected framework (see the
# #root method below), with "/lib/bumbleworks" appended, but you can override
# this by defining root explicitly.
# The definitions, tasks, and participants directories should exist here, if
# you're using the defaults for these directories, or if they're overridden
# with relative paths. So in a default install, the hierarchy should look
# like this:
# [defined root, or framework root]
# /lib
# /bumbleworks
# /participants
# /processes
# /tasks
#
# default: ${Framework root}/lib/bumbleworks (or no default if not in framework)
# Exceptions: raises Bumbleworks::UndefinedSetting if no framework and not
# defined by the client
#
define_setting :root
# Path to the folder which holds the ruote definition files. Bumbleworks
# will load all definition files by recursively traversing the directory
# tree under this folder. No specific loading order is guaranteed.
# These definition files will be loaded when Bumbleworks.bootstrap! is
# called, not Bootstrap.initialize! (since you don't want to re-register
# the participant list every time Bumbleworks is set up, but rather as an
# explicit task, for instance on deploy).
#
# default: ${Bumbleworks.root}/process_definitions then ${Bumbleworks.root}/processes
define_setting :definitions_directory
# Path to the folder which holds the ruote participant files. Bumbleworks
# will recursively traverse the directory tree under this folder and ensure
# that all found files are autoloaded before registration of participants.
#
# default: ${Bumbleworks.root}/participants
define_setting :participants_directory
# Path to the folder which holds the optional task module files, which are
# used to dynamically extend tasks (to override methods, or implement
# callbacks). Bumbleworks will recursively traverse the directory tree under
# this folder and ensure that all found files are autoloaded.
#
# default: ${Bumbleworks.root}/tasks
define_setting :tasks_directory
# Path to the file in which participant registration is defined. This file will
# be `load`ed when Bumbleworks.bootstrap! is called, not Bootstrap.initialize!
# (since you don't want to re-register the participant list every time Bumbleworks
# is set up, but rather as an explicit task, for instance on deploy).
#
# default: ${Bumbleworks.root}/participants.rb
define_setting :participant_registration_file
# Bumbleworks requires a dedicated key-value storage for process information. Three
# storage solutions are currently supported: Hash, Redis and Sequel. The latter
# two require the bumbleworks-redis and bumbleworks-sequel gems, respectively.
# You can set the storage as follows:
#
# @Example: Redis
# Bumbleworks.storage = Redis.new(:host => '127.0.0.1', :db => 0, :thread_safe => true)
#
# @Example: Sequel with Postgres db
# Bumbleworks.storage = Sequel.connect('postgres://user:password@host:port/database_name')
#
define_setting :storage
# Normally, the first adapter in the storage_adapters list that can #use? the
# configured storage will be used automatically. This may not be what you want;
# if you have multiple adapters that can use a Redis database or a Hash, for
# example, you may want to specify explicitly which one to use. Use this setting
# to override the automatic adapter selection.
#
# @Example:
# Bumbleworks.storage_adapter = Bumbleworks::OtherHashStorage
#
define_setting :storage_adapter
# This setting will be sent to the storage adapter's .new_storage method when
# initializing the storage. The base adapter (and the Hash adapter) ignore the
# options argument, but for the Redis and Sequel adapters, this is a handy way
# to pass through any options that Ruote's drivers understand.
#
# @Example:
# Bumbleworks.storage_options = { 'sequel_table_name' => 'bunnies_table' }
#
define_setting :storage_options
# Bumbleworks will attempt to log a variety of events (tasks becoming
# available, being claimed/released/completed, etc), and to do so it uses
# the logger registered in configuration. If no logger has been registered,
# Bumbleworks will use its SimpleLogger, which just adds the entries to an
# array. See the SimpleLogger for hints on implementing your own logger;
# notably, you can also use an instance Ruby's built-in Logger class.
#
# default: Bumbleworks::SimpleLogger
define_setting :logger
# All before_* and after_* callback methods prototyped in Tasks::Base will
# also be called on all registered observers.
define_setting :observers
# Cancelling a process or waiting for a task to become available are both asynchronous actions
# performed by Ruote. Bumbleworks waits for the specified timeout before giving up and raising
# the appropriate Timeout error.
#
# default: 5 seconds
define_setting :timeout
# When errors occur during the execution of a process, errors are captured and dispatched to
# the registered error handlers. An error handler must take a single initialization argument
# (the workitem at the point of error), and implement the #on_error method. You can subclass the
# Bumbleworks::ErrorHandler class for the initializer and workitem entity storage. The default
# handler (Bumbleworks::ErrorLogger) will simply send the configured logger an ERROR log entry.
#
# class MySpecialHandler < Bumbleworks::ErrorHandler
# def on_error
# p @workitem.error
# end
# end
#
# For exclusive use:
# Bumbleworks.error_handlers = [MySpecialHandler, MySpecialHandler2]
#
# To append to exisiting handlers:
# Bumbleworks.error_handlers << MySpecialHandler
#
# default: Bumbleworks::ErrorLogger
define_setting :error_handlers
# If #store_history is true, all messages will be logged in the storage under a special
# "history" key. These messages will remain in the history even after a process has been
# cancelled or completed, so the history can be used for auditing.
#
# If #store_history is false, history will be stored in-memory, but only the last 1000 messages,
# and since this is in memory, it's useless for multiple workers in separate processes.
#
# Important Note: This setting is *ignored* if the storage is a HashStorage, since having a
# persistent storage in this case wouldn't make sense (the Hash itself being in-memory).
#
define_setting :store_history
# The set of registered entity classes. This can be manually set in configuration,
# but if you include Bumbleworks::Entity in any class, that class will automatically
# be loaded into this array at the time of module inclusion.
#
# default: []
define_setting :entity_classes
def initialize
@storage_adapters = []
@storage_options = {}
@cached_paths = {}
@timeout ||= 5
end
# Path where Bumbleworks will look for ruote process defintiions to load.
# The path can be relative or absolute. Relative paths are
# relative to Bumbleworks.root.
#
def definitions_directory
@cached_paths[:definitions_directory] ||= look_up_configured_path(
:definitions_directory,
:defaults => ['process_definitions', 'processes']
)
end
# Path where Bumbleworks will look for ruote participants to load.
# The path can be relative or absolute. Relative paths are
# relative to Bumbleworks.root.
#
def participants_directory
look_up_configured_path(
:participants_directory,
:defaults => ['participants']
)
end
# Path where Bumbleworks will look for task modules to load.
# The path can be relative or absolute. Relative paths are
# relative to Bumbleworks.root.
#
def tasks_directory
@cached_paths[:tasks_directory] ||= look_up_configured_path(
:tasks_directory,
:defaults => ['tasks']
)
end
# Path where Bumbleworks will look for the participant registration
# file. The path can be relative or absolute. Relative paths are
# relative to Bumbleworks.root.
#
def participant_registration_file
@cached_paths[:participant_registration_file] ||= look_up_configured_path(
:participant_registration_file,
:defaults => ['participants.rb'],
:file => true
)
end
# Default history storage to true
def store_history
@store_history.nil? ? true : @store_history
end
# Default entity_classes to empty array
def entity_classes
@entity_classes ||= []
end
# Root folder where Bumbleworks looks for ruote assets (participants,
# process_definitions, etc.) The root path must be absolute.
# It can be defined through a configuration block:
# Bumbleworks.configure { |c| c.root = '/somewhere' }
#
# Or directly:
# Bumbleworks.root = '/somewhere/else/'
#
# If the root is not defined, Bumbleworks will use the root of known
# frameworks (Rails, Sinatra and Rory), appending "lib/bumbleworks".
# Otherwise, it will raise an error if not defined.
#
def root
@root ||= begin
raise UndefinedSetting.new("Bumbleworks.root must be set") unless framework_root
File.join(framework_root, "lib", "bumbleworks")
end
end
# Add a storage adapter to the set of possible adapters. Takes an object
# that responds to `driver`, `use?`, `storage_class`, and `display_name`.
#
def add_storage_adapter(adapter)
raise ArgumentError, "#{adapter} is not a Bumbleworks storage adapter" unless
[:driver, :use?, :new_storage, :allow_history_storage?, :storage_class, :display_name].all? { |m| adapter.respond_to?(m) }
@storage_adapters << adapter
@storage_adapters
end
# If storage_adapter is not explicitly set, find first registered adapter that
# can use Bumbleworks.storage.
#
def storage_adapter
@storage_adapter ||= begin
all_adapters = storage_adapters
raise UndefinedSetting, "No storage adapters configured" if all_adapters.empty?
adapter = all_adapters.detect do |potential_adapter|
potential_adapter.use?(storage)
end
raise UndefinedSetting, "Storage is missing or not supported. Supported: #{all_adapters.map(&:display_name).join(', ')}" unless adapter
adapter
end
end
def logger
@logger ||= Bumbleworks::SimpleLogger
end
def observers
@observers ||= []
end
# Clears all memoize variables and configuration settings
#
def clear!
defined_settings.each {|setting| instance_variable_set("@#{setting}", nil)}
@storage_adapters = []
@cached_paths = {}
end
def error_handlers
@error_handlers ||= [Bumbleworks::ErrorLogger]
end
private
def defined_settings
self.class.defined_settings
end
def framework_root
case
when defined?(::Rails) then ::Rails.root
when defined?(::Rory) then ::Rory.root
when defined?(::Padrino) then ::Padrino.root
when defined?(::Sinatra::Application) then ::Sinatra::Application.root
end
end
def path_resolves?(path, options = {})
if options[:file]
File.file?(path.to_s)
else
File.directory?(path.to_s)
end
end
def user_configured_path(path_type)
user_defined_path = instance_variable_get("@#{path_type}")
if user_defined_path
if user_defined_path[0] == '/'
user_defined_path
else
File.join(root, user_defined_path)
end
end
end
def first_existing_default_path(possible_paths, options = {})
defaults = [possible_paths].flatten.compact.map { |d| File.join(root, d) }
defaults.detect do |default|
path_resolves?(default, :file => options[:file])
end
end
# If the user explicitly declared a path, raises an exception if the
# path was not found. Missing default paths do not raise an exception
# since no paths are required.
def look_up_configured_path(path_type, options = {})
return @cached_paths[path_type] if @cached_paths.has_key?(path_type)
if user_defined_path = user_configured_path(path_type)
if path_resolves?(user_defined_path, :file => options[:file])
return user_defined_path
else
raise Bumbleworks::InvalidSetting, "#{Bumbleworks::Support.humanize(path_type)} not found (looked for #{user_defined_path || defaults.join(', ')})"
end
end
first_existing_default_path(options[:defaults], :file => options[:file])
end
end
end