mogbak 0.1.2 → 0.2.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/CHANGES ADDED
@@ -0,0 +1,10 @@
1
+ 2012-04-03: Release version 0.2.0
2
+
3
+ * Graceful handling of SIGINT and SIGTERM have been added. Upon receiving, mogbak will finish
4
+ the files being transfered and will exit. (Jesse Angell <jesse.angell@firespring.com>)
5
+
6
+ * Backup now has an optional --non-stop switch. This will cause mogbak to perform backup after backup
7
+ without exiting until SIGTERM or SIGINT is received. (Jesse Angell <jesse.angell@firespring.com>)
8
+
9
+ * --log-file option added to Restore, List, and Backup. Optional parameter that will redirect
10
+ output to a log file. Works well in conjunction with --non-stop. (Jesse Angell <jesse.angell@firespring.com>)
data/Gemfile.lock CHANGED
@@ -1,20 +1,44 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- mogbak (0.0.1)
4
+ mogbak (0.1.3)
5
+ activerecord-import (>= 0.2.9)
6
+ gli (>= 1.5.1)
7
+ mogilefs-client (>= 3.1.1)
8
+ mysql2 (>= 0.3.11)
9
+ sqlite3 (>= 1.3.5)
5
10
 
6
11
  GEM
7
12
  remote: http://rubygems.org/
8
13
  specs:
9
- json (1.6.1)
10
- rake (0.9.2.2)
11
- rdoc (3.11)
12
- json (~> 1.4)
14
+ activemodel (3.2.2)
15
+ activesupport (= 3.2.2)
16
+ builder (~> 3.0.0)
17
+ activerecord (3.2.2)
18
+ activemodel (= 3.2.2)
19
+ activesupport (= 3.2.2)
20
+ arel (~> 3.0.2)
21
+ tzinfo (~> 0.3.29)
22
+ activerecord-import (0.2.9)
23
+ activerecord (~> 3.0)
24
+ activerecord (~> 3.0)
25
+ activesupport (3.2.2)
26
+ i18n (~> 0.6)
27
+ multi_json (~> 1.0)
28
+ arel (3.0.2)
29
+ awesome_print (1.0.2)
30
+ builder (3.0.0)
31
+ gli (1.5.1)
32
+ i18n (0.6.0)
33
+ mogilefs-client (3.1.1)
34
+ multi_json (1.2.0)
35
+ mysql2 (0.3.11)
36
+ sqlite3 (1.3.5)
37
+ tzinfo (0.3.32)
13
38
 
14
39
  PLATFORMS
15
40
  ruby
16
41
 
17
42
  DEPENDENCIES
43
+ awesome_print
18
44
  mogbak!
19
- rake
20
- rdoc
data/bin/mogbak CHANGED
@@ -26,6 +26,11 @@ require 'sqlite3'
26
26
  require 'yaml'
27
27
  require 'forkinator'
28
28
  require 'path_helper'
29
+ require 'signal_handler'
30
+ require 'log'
31
+
32
+ #activate signal handler
33
+ SignalHandler.instance
29
34
 
30
35
 
31
36
  #Set master process name
@@ -102,15 +107,25 @@ command :backup do |c|
102
107
  c.desc 'do not remove deleted files from the backup (faster)'
103
108
  c.switch ['no-delete']
104
109
 
110
+ c.desc 'after backup is complete, start another backup. This causes mogbak to run non-stop. Send SIGINT or SIGTERM to exit cleanly'
111
+ c.switch ['non-stop']
112
+
105
113
  c.desc 'Number of worker processes'
106
114
  c.default_value 1
107
115
  c.flag :workers
108
116
 
117
+ c.desc 'send backup output to log file'
118
+ c.flag ['log-file']
119
+
109
120
  c.action do |global_options, options, args|
110
121
  raise '[backup_path] is required - see: mogbak help backup' unless args[0]
111
122
  $backup_path = args[0]
123
+
124
+ #setup a logger for output
125
+ Log.instance(options[:'log-file'])
126
+
112
127
  mog = Backup.new(:backup_path => args[0], :workers => options[:workers].to_i)
113
- mog.backup(:no_delete => options[:"no-delete"])
128
+ mog.backup(:no_delete => options[:"no-delete"], :non_stop => options[:"non-stop"])
114
129
  end
115
130
 
116
131
  end
@@ -142,11 +157,17 @@ command :restore do |c|
142
157
  c.desc 'restore a single file by dkey'
143
158
  c.flag :"single-file"
144
159
 
160
+ c.desc 'send restore output to log file'
161
+ c.flag ['log-file']
162
+
145
163
  c.action do |global_options, options, args|
146
164
  raise 'domain parameter is required - see: mogbak help restore' unless options[:domain]
147
165
  raise '[backup_path] is required - see: mogbak help restore' unless args[0]
148
166
  $backup_path = args[0]
149
167
 
168
+ #setup a logger for output
169
+ Log.instance(options[:'log-file'])
170
+
150
171
  restore = Restore.new(:tracker_ip => options[:trackerip],
151
172
  :tracker_port => options[:trackerport],
152
173
  :domain => options[:domain],
@@ -162,18 +183,28 @@ Output is: fid,dkey,length,classname
162
183
  EOS
163
184
  arg_name '[backup_path]'
164
185
  command :list do |c|
186
+
187
+ c.desc 'send list output to log file'
188
+ c.flag ['log-file']
189
+
165
190
  c.action do |global_options, options, args|
166
191
  raise '[backup_path] is required - see: mogbak help list' unless args[0]
167
192
  $backup_path = args[0]
168
193
 
194
+ #setup a logger for output
195
+ Log.instance(options[:'log-file'])
196
+
169
197
  list = List.new(:backup_path => args[0])
170
198
  list.list
171
199
  end
172
200
  end
173
201
 
174
- #If the user passes in --debug we can detect it here.
202
+ #Handle --debug
175
203
  pre do |global_options, command, options, args|
204
+
205
+ #This is used to enable some more verbose output
176
206
  $debug = global_options[:debug]
207
+
177
208
  true
178
209
  end
179
210
 
data/lib/backup.rb CHANGED
@@ -22,7 +22,7 @@ class Backup
22
22
 
23
23
 
24
24
  #run validations and setup
25
- raise unless check_backup_path
25
+ raise unless check_backup_path
26
26
  create_sqlite_db
27
27
  connect_sqlite
28
28
  migrate_sqlite
@@ -43,9 +43,9 @@ class Backup
43
43
  def bak_file(file)
44
44
  saved = file.bak_it
45
45
  if saved
46
- puts "Backed up: FID #{file.fid}"
46
+ Log.instance.info("Backed up: FID #{file.fid}")
47
47
  else
48
- puts "Error - will try again on next run: FID #{file.fid}"
48
+ Log.instance.info("Error - will try again on next run: FID #{file.fid}")
49
49
  end
50
50
 
51
51
  return saved
@@ -72,11 +72,13 @@ class Backup
72
72
  SqliteActiveRecord.clear_active_connections!
73
73
  }
74
74
 
75
- #This proc receives an array of BakFiles, proccesses them, and returns a result array to the parent proc
75
+ #This proc receives an array of BakFiles, proccesses them, and returns a result array to the parent proc. We will break
76
+ #from the files if the signal handler says so.
76
77
  child = Proc.new { |files|
77
78
  result = []
78
79
  files.each do |file|
79
80
  break if file.nil?
81
+ break if SignalHandler.instance.should_quit
80
82
  saved = bak_file(file)
81
83
  result << {:saved => saved, :file => file}
82
84
  end
@@ -91,16 +93,18 @@ class Backup
91
93
  #param [Array] files must be an array of BakFiles that need to be deleted
92
94
  def launch_delete_workers(fids)
93
95
 
94
- #This proc receives an array of BakFiles, handles them, and spits them back to the parent.
96
+ #This proc receives an array of BakFiles, handles them, and spits them back to the parent, break from the fids if
97
+ #the signal handler says so.
95
98
  child = Proc.new { |fids|
96
99
  result = []
97
100
  fids.each do |fid|
98
101
  break if fid.nil?
102
+ break if SignalHandler.instance.should_quit
99
103
  deleted = BakFile.delete_from_fs(fid)
100
104
  if deleted
101
- puts "Deleting from backup: FID #{fid}"
105
+ Log.instance.info("Deleting from backup: FID #{fid}")
102
106
  else
103
- puts "Failed to delete from backup: FID #{fid}"
107
+ Log.instance.info("Failed to delete from backup: FID #{fid}")
104
108
  end
105
109
 
106
110
  result << fid
@@ -133,64 +137,75 @@ class Backup
133
137
  #@param [Hash] o if :no_delete then don't remove deleted files from the backup (intensive process)
134
138
  def backup(o = {})
135
139
 
136
- files = []
137
- #first we retry files that we haven't been able to backup successfully, if any.
138
- BakFile.find_each(:conditions => ['saved = ?', false]) do |bak_file|
139
- files << bak_file
140
- end
140
+ #Loop over the main backup logic. We'll break out at the end unless o[:non_stop] is set
141
+ loop do
142
+ files = []
143
+ #first we retry files that we haven't been able to backup successfully, if any.
144
+ BakFile.find_each(:conditions => ['saved = ?', false]) do |bak_file|
145
+ files << bak_file
146
+ end
141
147
 
142
- launch_backup_workers(files)
148
+ launch_backup_workers(files)
143
149
 
144
- #now back up any new files. if they fail to be backed up we'll retry them the next time the backup
145
- #command is ran.
146
- dmid = Domain.find_by_namespace(self.domain)
147
- results = Fid.find_in_batches(:conditions => ['dmid = ? AND fid > ?', dmid, BakFile.max_fid], :batch_size => 500 * self.workers.to_i, :include => [:domain, :fileclass]) do |batch|
150
+ #now back up any new files. if they fail to be backed up we'll retry them the next time the backup
151
+ #command is ran.
152
+ dmid = Domain.find_by_namespace(self.domain)
153
+ results = Fid.find_in_batches(:conditions => ['dmid = ? AND fid > ?', dmid, BakFile.max_fid], :batch_size => 500 * self.workers.to_i, :include => [:domain, :fileclass]) do |batch|
154
+
155
+ #Insert all the files into our bak db with :saved false so that we don't think we backed up something that crashed
156
+ files = []
157
+ batch.each do |file|
158
+ files << BakFile.new(:fid => file.fid,
159
+ :domain => file.domain.namespace,
160
+ :dkey => file.dkey,
161
+ :length => file.length,
162
+ :classname => file.classname,
163
+ :saved => false)
164
+ end
148
165
 
149
- #Insert all the files into our bak db with :saved false so that we don't think we backed up something that crashed
150
- files = []
151
- batch.each do |file|
152
- files << BakFile.new(:fid => file.fid,
153
- :domain => file.domain.namespace,
154
- :dkey => file.dkey,
155
- :length => file.length,
156
- :classname => file.classname,
157
- :saved => false)
158
- end
166
+ #There is no way to do a bulk insert in sqlite so this generates a lot of inserts. wrapping all of the inserts
167
+ #inside a single transaction makes it much much faster.
168
+ BakFile.transaction do
169
+ BakFile.import files, :validate => false
170
+ end
159
171
 
160
- #There is no way to do a bulk insert in sqlite so this generates a lot of inserts. wrapping all of the inserts
161
- #inside a single transaction makes it much much faster.
162
- BakFile.transaction do
163
- BakFile.import files, :validate => false
172
+ #Fire up the workers now that we have work for them to do
173
+ launch_backup_workers(files)
174
+
175
+ #Terminate program if the signal handler says so and this is a clean place to do it
176
+ return true if SignalHandler.instance.should_quit
164
177
  end
165
178
 
166
- #Fire up the workers now that we have work for them to do
167
- launch_backup_workers(files)
179
+ #Delete files from the backup that no longer exist in the mogilefs domain. Unfortunently there is no easy way to detect
180
+ #which files have been deleted from the MogileFS domain. Our only option is to brute force our way through. This is a bulk
181
+ #query that checks a thousand files in each query against the MogileFS database server. The query is kind of tricky because
182
+ #I wanted to do this with nothing but SELECT privileges which meant I couldn't create a temporary table (which would require,
183
+ #create temporary table and insert privleges). You might want to only run this operation every once and awhile if you have a
184
+ #very large domain. In my testing, it is able to get through domains with millions of files in a matter of a second. So
185
+ #all in all it's not so bad
186
+ if !o[:no_delete]
168
187
 
169
- end
188
+ BakFile.find_in_batches { |bak_files|
189
+ union = "SELECT #{bak_files.first.fid} as fid"
190
+ bak_files.shift
191
+ bak_files.each do |bakfile|
192
+ union = "#{union} UNION SELECT #{bakfile.fid}"
193
+ end
194
+ connection = ActiveRecord::Base.connection
195
+ files = connection.select_values("SELECT t1.fid FROM (#{union}) as t1 LEFT JOIN file on t1.fid = file.fid WHERE file.fid IS NULL")
196
+ launch_delete_workers(files)
170
197
 
171
- #Delete files from the backup that no longer exist in the mogilefs domain. Unfortunently there is no easy way to detect
172
- #which files have been deleted from the MogileFS domain. Our only option is to brute force our way through. This is a bulk
173
- #query that checks a thousand files in each query against the MogileFS database server. The query is kind of tricky because
174
- #I wanted to do this with nothing but SELECT privileges which meant I couldn't create a temporary table (which would require,
175
- #create temporary table and insert privleges). You might want to only run this operation every once and awhile if you have a
176
- #very large domain. In my testing, it is able to get through domains with millions of files in a matter of a second. So
177
- #all in all it's not so bad
178
- if !o[:no_delete]
179
- files_to_delete = Array.new
180
- BakFile.find_in_batches { |bak_files|
181
-
182
- union = "SELECT #{bak_files.first.fid} as fid"
183
- bak_files.shift
184
- bak_files.each do |bakfile|
185
- union = "#{union} UNION SELECT #{bakfile.fid}"
186
- end
187
- connection = ActiveRecord::Base.connection
188
- files = connection.select_values("SELECT t1.fid FROM (#{union}) as t1 LEFT JOIN file on t1.fid = file.fid WHERE file.fid IS NULL")
189
- files_to_delete += files
190
- }
198
+ #Terminate program if the signal handler says so and this is a clean place to do it
199
+ return true if SignalHandler.instance.should_quit
200
+ }
191
201
 
192
- launch_delete_workers(files_to_delete)
193
202
 
203
+
204
+ end
205
+
206
+ #Break out of infinite loop unless o[:non_stop] is set
207
+ break unless o[:non_stop]
208
+ sleep 1
194
209
  end
195
210
 
196
211
  end
data/lib/forkinator.rb CHANGED
@@ -88,13 +88,6 @@ class Forkinator
88
88
  children = []
89
89
  qty.times { children << make_child(child_proc)}
90
90
 
91
- #register signal handler so that children kill if program receives a SIGINT
92
- #which will happen if the user ctrl c's the parent process
93
- Signal.trap :SIGINT do
94
- children.each { |child| Process.kill(:KILL, child[:pid]) if child[:pid]}
95
- exit 1
96
- end
97
-
98
91
  #For each worker
99
92
  qty.times do |i|
100
93
 
data/lib/list.rb CHANGED
@@ -22,7 +22,8 @@ class List
22
22
  #fid,key,length,class
23
23
  def list
24
24
  files = BakFile.find_each(:conditions => ['saved = ?', true]) do |file|
25
- puts "#{file.fid},#{file.dkey},#{file.length},#{file.classname}"
25
+ Log.instance.info("#{file.fid},#{file.dkey},#{file.length},#{file.classname}")
26
+ break if SignalHandler.instance.should_quit
26
27
  end
27
28
  end
28
29
  end
data/lib/log.rb ADDED
@@ -0,0 +1,17 @@
1
+ require 'logger'
2
+ #Create a Logger that is a singleton provided by the instance method
3
+ class Log
4
+ def self.instance(log_file = nil)
5
+ @@instance ||= create_logger(log_file)
6
+ end
7
+
8
+ def self.create_logger(log_file)
9
+ log_file = STDOUT if log_file == nil
10
+ logger = Logger.new(log_file)
11
+ logger.datetime_format = "%Y-%m-%d %H:%M:%S"
12
+ logger.formatter = proc do |severity, datetime, progname, msg|
13
+ "#{datetime}: #{msg}\n"
14
+ end
15
+ logger
16
+ end
17
+ end
@@ -1,3 +1,3 @@
1
1
  module Mogbak
2
- VERSION = '0.1.2'
2
+ VERSION = '0.2.0'
3
3
  end
data/lib/restore.rb CHANGED
@@ -28,9 +28,9 @@ class Restore
28
28
 
29
29
  def output_save(save, fid)
30
30
  if save
31
- puts "Restored: FID #{fid}"
31
+ Log.instance.info("Restored: FID #{fid}")
32
32
  else
33
- puts "Error: FID #{fid}"
33
+ Log.instance.info("Error: FID #{fid}")
34
34
  end
35
35
  end
36
36
 
@@ -39,6 +39,7 @@ class Restore
39
39
  results = []
40
40
  files.each do |file|
41
41
  break if file.nil?
42
+ break if SignalHandler.instance.should_quit
42
43
  save = file.restore
43
44
  output_save(save, file.fid)
44
45
  results << {:restored => save, :fid => file.fid}
@@ -64,6 +65,7 @@ class Restore
64
65
 
65
66
  BakFile.find_in_batches(:conditions => ['saved = ?', true], :batch_size => 2000) do |batch|
66
67
  launch_restore_workers(batch)
68
+ break if SignalHandler.instance.should_quit
67
69
  end
68
70
 
69
71
  end
@@ -0,0 +1,19 @@
1
+ require 'singleton'
2
+
3
+ #Singleton class used to intercept signals. If a SIGINT or SIGTERM is received a message is outputted and @should_quit
4
+ #is set to true
5
+ class SignalHandler
6
+ include Singleton
7
+ attr_reader :should_quit
8
+
9
+ def handle_signal
10
+ puts "PID #{Process.pid} is gracefully shutting down..."
11
+ @should_quit = true
12
+ end
13
+
14
+ def initialize
15
+ @should_quit = false
16
+ Signal.trap("SIGINT") { handle_signal }
17
+ Signal.trap("SIGTERM") { handle_signal }
18
+ end
19
+ end
data/lib/validations.rb CHANGED
@@ -45,7 +45,7 @@ module Validations
45
45
  #@return [Bool]
46
46
  def connect_sqlite(raise_msg = nil)
47
47
  begin
48
- ActiveRecord::Base.establish_connection(:adapter => 'sqlite3', :database => "#{$backup_path}/db.sqlite", :timeout => 1000)
48
+ ActiveRecord::Base.establish_connection(:adapter => 'sqlite3', :database => "#{$backup_path}/db.sqlite", :timeout => 10000)
49
49
  rescue Exception => e
50
50
  raise raise_msg if raise_msg
51
51
  raise e if $debug
data/mogbak.gemspec CHANGED
@@ -19,9 +19,6 @@ spec = Gem::Specification.new do |s|
19
19
  s.add_runtime_dependency('gli', '>= 1.5.1')
20
20
  s.add_runtime_dependency('mysql2', '>= 0.3.11')
21
21
  s.add_runtime_dependency('mogilefs-client','>= 3.1.1')
22
- s.add_runtime_dependency('json','>= 1.6.5')
23
22
  s.add_runtime_dependency('sqlite3','>=1.3.5')
24
23
  s.add_runtime_dependency('activerecord-import','>=0.2.9')
25
-
26
-
27
24
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mogbak
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.2.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-03-27 00:00:00.000000000 Z
12
+ date: 2012-04-03 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: awesome_print
@@ -75,22 +75,6 @@ dependencies:
75
75
  - - ! '>='
76
76
  - !ruby/object:Gem::Version
77
77
  version: 3.1.1
78
- - !ruby/object:Gem::Dependency
79
- name: json
80
- requirement: !ruby/object:Gem::Requirement
81
- none: false
82
- requirements:
83
- - - ! '>='
84
- - !ruby/object:Gem::Version
85
- version: 1.6.5
86
- type: :runtime
87
- prerelease: false
88
- version_requirements: !ruby/object:Gem::Requirement
89
- none: false
90
- requirements:
91
- - - ! '>='
92
- - !ruby/object:Gem::Version
93
- version: 1.6.5
94
78
  - !ruby/object:Gem::Dependency
95
79
  name: sqlite3
96
80
  requirement: !ruby/object:Gem::Requirement
@@ -131,6 +115,7 @@ extensions: []
131
115
  extra_rdoc_files: []
132
116
  files:
133
117
  - .gitignore
118
+ - CHANGES
134
119
  - Gemfile
135
120
  - Gemfile.lock
136
121
  - LICENSE
@@ -145,10 +130,12 @@ files:
145
130
  - lib/fileclass.rb
146
131
  - lib/forkinator.rb
147
132
  - lib/list.rb
133
+ - lib/log.rb
148
134
  - lib/mogbak_version.rb
149
135
  - lib/monkey_patch.rb
150
136
  - lib/path_helper.rb
151
137
  - lib/restore.rb
138
+ - lib/signal_handler.rb
152
139
  - lib/validations.rb
153
140
  - mogbak.gemspec
154
141
  homepage: http://www.github.com/firespring/mogbak