lib/backup/model.rb in backup-3.5.1 vs lib/backup/model.rb in backup-3.6.0
- old
+ new
@@ -32,39 +32,39 @@
##
# The label (stored as a String) is used for a more friendly user output
attr_reader :label
##
- # The databases attribute holds an array of database objects
+ # Array of configured Database objects.
attr_reader :databases
##
- # The archives attr_accessor holds an array of archive objects
+ # Array of configured Archive objects.
attr_reader :archives
##
- # The notifiers attr_accessor holds an array of notifier objects
+ # Array of configured Notifier objects.
attr_reader :notifiers
##
- # The storages attribute holds an array of storage objects
+ # Array of configured Storage objects.
attr_reader :storages
##
- # The syncers attribute holds an array of syncer objects
+ # Array of configured Syncer objects.
attr_reader :syncers
##
- # Holds the configured Compressor
+ # The configured Compressor, if any.
attr_reader :compressor
##
- # Holds the configured Encryptor
+ # The configured Encryptor, if any.
attr_reader :encryptor
##
- # Holds the configured Splitter
+ # The configured Splitter, if any.
attr_reader :splitter
##
# The final backup Package this model will create.
attr_reader :package
@@ -72,21 +72,35 @@
##
# The time when the backup initiated (in format: 2011.02.20.03.29.59)
attr_reader :time
##
- # Takes a trigger, label and the configuration block.
- # After the instance has evaluated the configuration block
- # to configure the model, it will be appended to Model.all
+ # Result of this model's backup process.
+ #
+ # 0 = Job was successful
+ # 1 = Job was successful, but issued warnings
+ # 2 = Job failed, additional triggers may be performed
+ # 3 = Job failed, additional triggers will not be performed
+ attr_reader :exit_status
+
+ ##
+ # Exception raised by either a +before+ hook or one of the model's
+ # procedures that caused the model to fail. An exception raised by an
+ # +after+ hook would not be stored here. Therefore, it is possible for
+ # this to be +nil+ even if #exit_status is 2 or 3.
+ attr_reader :exception
+
def initialize(trigger, label, &block)
@trigger = trigger.to_s
@label = label.to_s
@package = Package.new(self)
- procedure_instance_variables.each do |variable|
- instance_variable_set(variable, Array.new)
- end
+ @databases = []
+ @archives = []
+ @storages = []
+ @notifiers = []
+ @syncers = []
instance_eval(&block) if block_given?
# trigger all defined databases to generate their #dump_filename
# so warnings may be logged if `backup perform --check` is used
@@ -94,35 +108,31 @@
Model.all << self
end
##
- # Adds an archive to the array of archives
- # to store during the backup process
+ # Adds an Archive. Multiple Archives may be added to the model.
def archive(name, &block)
@archives << Archive.new(self, name, &block)
end
##
- # Adds a database to the array of databases
- # to dump during the backup process
+ # Adds an Database. Multiple Databases may be added to the model.
def database(name, database_id = nil, &block)
@databases << get_class_from_scope(Database, name).
new(self, database_id, &block)
end
##
- # Adds a storage method to the array of storage
- # methods to use during the backup process
+ # Adds an Storage. Multiple Storages may be added to the model.
def store_with(name, storage_id = nil, &block)
@storages << get_class_from_scope(Storage, name).
new(self, storage_id, &block)
end
##
- # Adds a syncer method to the array of syncer
- # methods to use during the backup process
+ # Adds an Syncer. Multiple Syncers may be added to the model.
def sync_with(name, syncer_id = nil, &block)
##
# Warn user of DSL changes
case name.to_s
when 'Backup::Config::RSync'
@@ -145,33 +155,33 @@
end
@syncers << get_class_from_scope(Syncer, name).new(syncer_id, &block)
end
##
- # Adds a notifier to the array of notifiers
- # to use during the backup process
+ # Adds an Notifier. Multiple Notifiers may be added to the model.
def notify_by(name, &block)
@notifiers << get_class_from_scope(Notifier, name).new(self, &block)
end
##
- # Adds an encryptor to use during the backup process
+ # Adds an Encryptor. Only one Encryptor may be added to the model.
+ # This will be used to encrypt the final backup package.
def encrypt_with(name, &block)
@encryptor = get_class_from_scope(Encryptor, name).new(&block)
end
##
- # Adds a compressor to use during the backup process
+ # Adds an Compressor. Only one Compressor may be added to the model.
+ # This will be used to compress each individual Archive and Database
+ # stored within the final backup package.
def compress_with(name, &block)
@compressor = get_class_from_scope(Compressor, name).new(&block)
end
##
- # Adds a method that allows the user to configure this backup model
- # to use a Splitter, with the given +chunk_size+
- # The +chunk_size+ (in megabytes) will later determine
- # in how many chunks the backup needs to be split into
+ # Adds a Splitter with the given +chunk_size+ in MB.
+ # This will split the final backup package into multiple files.
def split_into_chunks_of(chunk_size)
if chunk_size.is_a?(Integer)
@splitter = Splitter.new(self, chunk_size)
else
raise Errors::Model::ConfigurationError, <<-EOS
@@ -180,84 +190,102 @@
EOS
end
end
##
- # Performs the backup process
+ # Defines a block of code to run before the model's procedures.
+ #
+ # Warnings logged within the before hook will elevate the model's
+ # exit_status to 1 and cause warning notifications to be sent.
+ #
+ # Raising an exception will abort the model and cause failure notifications
+ # to be sent. If the exception is a StandardError, exit_status will be 2.
+ # If the exception is not a StandardError, exit_status will be 3.
+ #
+ # If any exception is raised, any defined +after+ hook will be skipped.
+ def before(&block)
+ @before ||= block
+ end
+
##
- # [Databases]
- # Runs all (if any) database objects to dump the databases
+ # Defines a block of code to run after the model's procedures.
+ #
+ # This code is ensured to run, even if the model failed, **unless** a
+ # +before+ hook raised an exception and aborted the model.
+ #
+ # The code block will be passed the model's current exit_status:
+ #
+ # `0`: Success, no warnings.
+ # `1`: Success, but warnings were logged.
+ # `2`: Failure, but additional models/triggers will still be processed.
+ # `3`: Failure, no additional models/triggers will be processed.
+ #
+ # The model's exit_status may be elevated based on the after hook's
+ # actions, but will never be decreased.
+ #
+ # Warnings logged within the after hook may elevate the model's
+ # exit_status to 1 and cause warning notifications to be sent.
+ #
+ # Raising an exception may elevate the model's exit_status and cause
+ # failure notifications to be sent. If the exception is a StandardError,
+ # the exit_status will be elevated to 2. If the exception is not a
+ # StandardError, the exit_status will be elevated to 3.
+ def after(&block)
+ @after ||= block
+ end
+
##
- # [Archives]
- # Runs all (if any) archive objects to package all their
- # paths in to a single tar file and places it in the backup folder
- ##
- # [Packaging]
- # After all the database dumps and archives are placed inside
- # the folder, it'll make a single .tar package (archive) out of it
- ##
- # [Encryption]
- # Optionally encrypts the packaged file with the configured encryptor
- ##
- # [Compression]
- # Optionally compresses the each Archive and Database dump with the configured compressor
- ##
- # [Splitting]
- # Optionally splits the backup file in to multiple smaller chunks before transferring them
- ##
- # [Storages]
- # Runs all (if any) storage objects to store the backups to remote locations
- # and (if configured) it'll cycle the files on the remote location to limit the
- # amount of backups stored on each individual location
- ##
- # [Syncers]
- # Runs all (if any) sync objects to store the backups to remote locations.
- # A Syncer does not go through the process of packaging, compressing, encrypting backups.
- # A Syncer directly transfers data from the filesystem to the remote location
- ##
- # [Notifiers]
- # Runs all (if any) notifier objects when a backup proces finished with or without
- # any errors.
- ##
- # [Cleaning]
- # Once the final Packaging is complete, the temporary folder used will be removed.
- # Then, once all Storages have run, the final packaged files will be removed.
- # If any errors occur during the backup process, all temporary files will be left in place.
- # If the error occurs before Packaging, then the temporary folder (tmp_path/trigger)
- # will remain and may contain all or some of the configured Archives and/or Database dumps.
- # If the error occurs after Packaging, but before the Storages complete, then the final
+ # Performs the backup process
+ #
+ # Once complete, #exit_status will indicate the result of this process.
+ #
+ # If any errors occur during the backup process, all temporary files will
+ # be left in place. If the error occurs before Packaging, then the
+ # temporary folder (tmp_path/trigger) will remain and may contain all or
+ # some of the configured Archives and/or Database dumps. If the error
+ # occurs after Packaging, but before the Storages complete, then the final
# packaged files (located in the root of tmp_path) will remain.
- # *** Important *** If an error occurs and any of the above mentioned temporary files remain,
- # those files *** will be removed *** before the next scheduled backup for the same trigger.
#
+ # *** Important ***
+ # If an error occurs and any of the above mentioned temporary files remain,
+ # those files *** will be removed *** before the next scheduled backup for
+ # the same trigger.
def perform!
@started_at = Time.now
@time = package.time = @started_at.strftime("%Y.%m.%d.%H.%M.%S")
+
log!(:started)
+ before_hook
- prepare!
-
- if databases.any? or archives.any?
- procedures.each do |procedure|
- (procedure.call; next) if procedure.is_a?(Proc)
- procedure.each(&:perform!)
- end
+ procedures.each do |procedure|
+ procedure.is_a?(Proc) ? procedure.call : procedure.each(&:perform!)
end
syncers.each(&:perform!)
- notifiers.each(&:perform!)
- log!(:finished)
rescue Exception => err
- log!(:failure, err)
- send_failure_notifications
- exit(3) unless err.is_a?(StandardError)
+ @exception = err
+
+ ensure
+ set_exit_status
+ log!(:finished)
+ after_hook
end
private
##
+ # Returns an array of procedures that will be performed if any
+ # Archives or Databases are configured for the model.
+ def procedures
+ return [] unless databases.any? || archives.any?
+
+ [lambda { prepare! }, databases, archives,
+ lambda { package! }, storages, lambda { clean! }]
+ end
+
+ ##
# Clean any temporary files and/or package files left over
# from the last time this model/trigger was performed.
# Logs warnings if files exist and are cleaned.
def prepare!
Cleaner.prepare(self)
@@ -279,22 +307,10 @@
def clean!
Cleaner.remove_package(package)
end
##
- # Returns an array of procedures
- def procedures
- [databases, archives, lambda { package! }, storages, lambda { clean! }]
- end
-
- ##
- # Returns an Array of the names (String) of the procedure instance variables
- def procedure_instance_variables
- [:@databases, :@archives, :@storages, :@notifiers, :@syncers]
- end
-
- ##
# Returns the class/model specified by +name+ inside of +scope+.
# +scope+ should be a Class/Module.
# +name+ may be Class/Module or String representation
# of any namespace which exists under +scope+.
#
@@ -317,47 +333,88 @@
end
klass
end
##
- # Logs messages when the backup starts, finishes or fails
- def log!(action, exception = nil)
+ # Sets or updates the model's #exit_status.
+ def set_exit_status
+ @exit_status = if exception
+ exception.is_a?(StandardError) ? 2 : 3
+ else
+ Logger.has_warnings? ? 1 : 0
+ end
+ end
+
+ ##
+ # Runs the +before+ hook.
+ # Any exception raised will be wrapped and re-raised, where it will be
+ # handled by #perform the same as an exception raised while performing
+ # the model's #procedures. Only difference is that an exception raised
+ # here will prevent any +after+ hook from being run.
+ def before_hook
+ return unless before
+
+ Logger.info 'Before Hook Starting...'
+ before.call
+ Logger.info 'Before Hook Finished.'
+
+ rescue Exception => err
+ @before_hook_failed = true
+ ex = err.is_a?(StandardError) ?
+ Errors::Model::HookError : Errors::Model::HookFatalError
+ raise ex.wrap(err, 'Before Hook Failed!')
+ end
+
+ ##
+ # Runs the +after+ hook.
+ # Any exception raised here will be logged only and the model's
+ # #exit_status will be elevated if neccessary.
+ def after_hook
+ return unless after && !@before_hook_failed
+
+ Logger.info 'After Hook Starting...'
+ after.call(exit_status)
+ Logger.info 'After Hook Finished.'
+
+ set_exit_status # in case hook logged warnings
+
+ rescue Exception => err
+ fatal = !err.is_a?(StandardError)
+ ex = fatal ? Errors::Model::HookFatalError : Errors::Model::HookError
+ Logger.error ex.wrap(err, 'After Hook Failed!')
+ # upgrade exit_status if needed
+ (@exit_status = fatal ? 3 : 2) unless exit_status == 3
+ end
+
+ ##
+ # Logs messages when the model starts and finishes.
+ #
+ # #exception will be set here if #exit_status is > 1,
+ # since log(:finished) is called before the +after+ hook.
+ def log!(action)
case action
when :started
- Logger.info "Performing Backup for '#{label} (#{trigger})'!\n" +
+ Logger.info "Performing Backup for '#{ label } (#{ trigger })'!\n" +
"[ backup #{ VERSION } : #{ RUBY_DESCRIPTION } ]"
when :finished
- msg = "Backup for '#{ label } (#{ trigger })' " +
- "Completed %s in #{ elapsed_time }"
- if Logger.has_warnings?
- Logger.warn msg % 'Successfully (with Warnings)'
- else
- Logger.info msg % 'Successfully'
- end
+ if exit_status > 1
+ ex = exit_status == 2 ? Errors::ModelError : Errors::ModelFatalError
+ err = ex.wrap(exception, "Backup for #{ label } (#{ trigger }) Failed!")
+ Logger.error err
+ Logger.error "\nBacktrace:\n\s\s" + err.backtrace.join("\n\s\s") + "\n\n"
- when :failure
- err = Errors::ModelError.wrap(exception, <<-EOS)
- Backup for #{label} (#{trigger}) Failed!
- An Error occured which has caused this Backup to abort before completion.
- EOS
- Logger.error err
- Logger.error "\nBacktrace:\n\s\s" + err.backtrace.join("\n\s\s") + "\n\n"
-
- Cleaner.warnings(self)
-
- if exception.is_a?(StandardError)
- Logger.info Errors::ModelError.new(<<-EOS)
- If you have other Backup jobs (triggers) configured to run,
- Backup will now attempt to continue...
- EOS
+ Cleaner.warnings(self)
else
- Logger.error Errors::ModelError.new(<<-EOS)
- This Error was Fatal and Backup will now exit.
- If you have other Backup jobs (triggers) configured to run,
- they will not be processed.
- EOS
+ msg = "Backup for '#{ label } (#{ trigger })' "
+ if exit_status == 1
+ msg << "Completed Successfully (with Warnings) in #{ elapsed_time }"
+ Logger.warn msg
+ else
+ msg << "Completed Successfully in #{ elapsed_time }"
+ Logger.info msg
+ end
end
end
end
##
@@ -367,25 +424,9 @@
hours = duration / 3600
remainder = duration - (hours * 3600)
minutes = remainder / 60
seconds = remainder - (minutes * 60)
'%02d:%02d:%02d' % [hours, minutes, seconds]
- end
-
- ##
- # Sends notifications when a backup fails.
- # Errors are logged and rescued, since the error that caused the
- # backup to fail could have been an error with a notifier.
- def send_failure_notifications
- notifiers.each do |n|
- begin
- n.perform!(true)
- rescue Exception => err
- Logger.error Errors::ModelError.wrap(err, <<-EOS)
- #{ n.class } Failed to send notification of backup failure.
- EOS
- end
- end
end
end
end