require 'madeleine' require 'madeleine/automatic' require 'madeleine/zmarshal' require 'singleton' require 'yaml' class MadeleineService include Madeleine::Automatic::Interceptor @@storage_path = self.name.downcase + "_storage" automatic_read_only :snapshot_interval_hours, :take_snapshot, :clean_old_snapshots, :restart, :request_stop class << self def storage_path @@storage_path end def storage_path=(storage_path) @@storage_path = storage_path end def instance if @system.nil? @madeleine_server = MadeleineServer.new(self) @system = @madeleine_server.system end @system end def restart MadeleineServer.delete_storage(self) @system = nil instance end def clean_old_snapshots instance @madeleine_server.clean_storage(self) end def take_snapshot instance @madeleine_server.force_snapshot end def snapshot_interval_hours instance @madeleine_server.snapshot_interval.div MadeleineServer::ONE_HOUR rescue 1 end def snapshot_interval_hours= hours instance @madeleine_server.snapshot_interval = hours.to_i * MadeleineServer::ONE_HOUR rescue MadeleineServer::ONE_HOUR end def request_stop instance @madeleine_server.request_stop end end end require 'fileutils' class MadeleineServer attr_reader :storage_path attr_accessor :snapshot_interval # Clears all the command_log and snapshot files located in the storage directory, so the # database is essentially dropped and recreated as blank. Used in tests. def self.delete_storage(service) if (File.directory?(service.storage_path)) FileUtils.rm_rf(Dir[service.storage_path + '/*.command_log']) FileUtils.rm_rf(Dir[service.storage_path + '/*.snapshot']) else FileUtils.mkdir_p(service.storage_path) end end def clean_storage(service) force_snapshot command_logs = Dir[service.storage_path + '/*.command_log'] raise 'Error: existing command_logs after snapshot' unless command_logs.empty? snapshots = Dir[service.storage_path + '/*.snapshot'] FileUtils.rm_rf(snapshots.sort[0..-2]) end def initialize(service) @storage_path = service.storage_path @snapshot_interval = ONE_HOUR marshaller = Madeleine::ZMarshal.new() @server = Madeleine::Automatic::AutomaticSnapshotMadeleine.new(service.storage_path, marshaller) { service.new } start_snapshot_thread end def system @server.system end def command_log_present? not Dir[File.join(File.expand_path(storage_path), '*.command_log')].empty? end def force_snapshot begin hours_since_last_snapshot = 0 @server.take_snapshot rescue => e sleep(ONE_MINUTE) retry end end ONE_MINUTE = 60 ONE_HOUR = ONE_MINUTE * 60 MAX_INTERVAL_HOURS = 24 * 2 def start_snapshot_thread @snapshot_thread = Thread.new(@server) { hours_since_last_snapshot = 0 while not @request_stop sleep(snapshot_interval) hours_since_last_snapshot += snapshot_interval.div ONE_HOUR begin # Take a snapshot if there is a command log if command_log_present? or hours_since_last_snapshot > MAX_INTERVAL_HOURS # 'Taking a Madeleine snapshot' @server.take_snapshot hours_since_last_snapshot = 0 puts "[#{DateTime.now.strftime '%F %T'}] INFO Taking snapshot" else puts "[#{DateTime.now.strftime '%F %T'}] INFO Skipping snapshot (no command logs)" end rescue => e # wait for a minute (not to spoof the log with the same error) # and go back into the loop, to keep trying sleep(ONE_MINUTE) retry end end } end def request_stop begin @request_stop = true if @snapshot_thread and @snapshot_thread.alive? @snapshot_thread.wakeup @snapshot_thread.join end @server.take_snapshot if command_log_present? rescue => detail puts detail end end end