mogbak 0.1.2 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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