mongo-oplog-backup 0.0.12 → 0.0.13

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 4c98c007c62b80bc6c87b1f141deba9d27933865
4
- data.tar.gz: 9a731f45965d4218bbef98e711908d236d41d29c
3
+ metadata.gz: 614d931c8a55e33ca69b78eb20c38204c2668d50
4
+ data.tar.gz: 5807c75c2173be443b36ceee09ca5e38c545a4c6
5
5
  SHA512:
6
- metadata.gz: 5642b1a2790b9d2493c1d5e7f90e8baae53bc6d9c6a81084475d03100903b6cb704124f7a4336060f7c9256900db00a677b06c548c665f5ccdcbae7989f28bea
7
- data.tar.gz: cbba467fb5f8c9ffe9c30a79298c6d3779d5e590ebab20bf874139b76b1adb288560caf7910f734093f0dfe5ce5e563a041c8b7ce5bc4af0f75ae8f8d3a3aef6
6
+ metadata.gz: dd246853e1c69ceac5a503d6bf9a684c9bc6ab44debdffaa0ced2389521194cdaf9751b9c72f61dd96b912fe2f3857a19c9c4e99b979975f51443ace77e4fd74
7
+ data.tar.gz: 7713c2c5de857b47eaaddd6b8304bad7d7c97f56aa176ba71abfddcba9159de003f1b5c72718de2ff825c350aaea15bed215a6401dfa3de683afcc2be135e381
data/README.md CHANGED
@@ -66,6 +66,19 @@ That said, there have been bugs in the past that caused the oplog to not be idem
66
66
  in some edge cases. Therefore it is recommended to stop the secondary before performing
67
67
  a full backup.
68
68
 
69
+ ## Rotation
70
+
71
+ Older backups can be automatically deleted with `mongo-oplog-backup rotate`.
72
+
73
+ For example, given the full backup schedule above and a recovery point objective of 4 weeks, any full backup sets
74
+ older than 4 weeks (excluding the currently active backup set and the previous backup set) can be deleted.
75
+
76
+ A sample cron script to clean-up any older backups about an hour before the weekly full backup:
77
+
78
+ 0 23 * * 0 /path/to/ruby/bin/mongo-oplog-backup rotate --dir /path/to/backup/location --keep_days=28 >> /path/to/backup.log
79
+
80
+ It is strongly recommended to use the `dryRun` option to thoroughly test your particular configuration and schedule.
81
+
69
82
  ## To restore
70
83
 
71
84
  mongo-oplog-backup merge --dir mybackup/backup-<timestamp>
@@ -73,6 +73,26 @@ opts = Slop.parse(help: true, strict: true) do
73
73
  end
74
74
  end
75
75
 
76
+ command 'rotate' do
77
+ banner "Delete old backup sets.\nUsage: mongo-oplog-backup rotate [options]"
78
+ on :v, :verbose, "Enable verbose mode"
79
+ on :d, :dir, "Directory where backup files are stored. Defaults to 'backup'.", argument: :required
80
+ on :keepDays, "Minimum number of days of backups to keep (recovery point objective). This should be AT LEAST equal to the Full backup schedule.", default: '32', argument: :required
81
+ on :dryRun, "Perform a trial run with no changes made."
82
+
83
+ run do |opts, args|
84
+ dir = opts[:dir] || 'backup'
85
+ config_opts = {
86
+ dir: dir,
87
+ keepDays: (opts[:keepDays].to_i rescue nil),
88
+ dryRun: opts.dryRun?
89
+ }
90
+ config = MongoOplogBackup::Config.new(config_opts)
91
+ rotate = MongoOplogBackup::Rotate.new(config)
92
+ rotate.perform
93
+ end
94
+ end
95
+
76
96
  command 'restore' do
77
97
  banner 'Usage: mongo-oplog-backup restore [options]'
78
98
  on :v, :verbose, 'Enable verbose mode'
@@ -4,11 +4,13 @@ require 'mongo_oplog_backup/version'
4
4
  require 'mongo_oplog_backup/ext/enumerable'
5
5
  require 'mongo_oplog_backup/ext/timestamp'
6
6
 
7
+ require 'mongo_oplog_backup/lock'
7
8
  require 'mongo_oplog_backup/command'
8
9
  require 'mongo_oplog_backup/config'
9
10
  require 'mongo_oplog_backup/backup'
10
11
  require 'mongo_oplog_backup/oplog'
11
12
  require 'mongo_oplog_backup/restore'
13
+ require 'mongo_oplog_backup/rotate'
12
14
 
13
15
  module MongoOplogBackup
14
16
  def self.log
@@ -2,11 +2,9 @@ require 'json'
2
2
  require 'fileutils'
3
3
  require 'mongo_oplog_backup/oplog'
4
4
 
5
- class LockError < StandardError
6
- end
7
-
8
5
  module MongoOplogBackup
9
6
  class Backup
7
+ include Lockable
10
8
  attr_reader :config, :backup_name
11
9
 
12
10
  def backup_folder
@@ -33,17 +31,6 @@ module MongoOplogBackup
33
31
  File.write(state_file, state.to_json)
34
32
  end
35
33
 
36
- def lock(lockname, &block)
37
- File.open(lockname, File::RDWR|File::CREAT, 0644) do |file|
38
- # Get a non-blocking lock
39
- got_lock = file.flock(File::LOCK_EX|File::LOCK_NB)
40
- if got_lock == false
41
- raise LockError, "Failed to acquire lock - another backup may be busy"
42
- end
43
- yield
44
- end
45
- end
46
-
47
34
  def backup_oplog(options={})
48
35
  raise ArgumentError, "No state in #{backup_name}" unless File.exists? state_file
49
36
 
@@ -0,0 +1,21 @@
1
+ module MongoOplogBackup
2
+ class LockError < StandardError
3
+ end
4
+
5
+ module Lockable
6
+ def self.included(base)
7
+ base.extend(self)
8
+ end
9
+
10
+ def lock(lockname, &block)
11
+ File.open(lockname, File::RDWR|File::CREAT, 0644) do |file|
12
+ # Get a non-blocking lock
13
+ got_lock = file.flock(File::LOCK_EX|File::LOCK_NB)
14
+ if got_lock == false
15
+ raise LockError, "Failed to acquire lock - another backup may be busy"
16
+ end
17
+ yield
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,90 @@
1
+ require 'pathname'
2
+
3
+ module MongoOplogBackup
4
+ class Rotate
5
+ include Lockable
6
+ attr_reader :config, :backup_list
7
+ DAY = 86400
8
+ RECOVERY_POINT_OBJECTIVE = 32 * DAY # Longest month + 1
9
+ KEEP_MINIMUM_SETS = 2 # Current & Previous
10
+
11
+ BACKUP_DIR_NAME_FORMAT = /\Abackup-\d+:\d+\z/
12
+
13
+ def initialize(config)
14
+ @config = config
15
+ @dry_run = !!@config.options[:dryRun]
16
+ @backup_list = find_backup_directories
17
+ if @config.options[:keepDays].nil?
18
+ @recovery_point_objective = RECOVERY_POINT_OBJECTIVE
19
+ else
20
+ @recovery_point_objective = @config.options[:keepDays] * DAY
21
+ end
22
+ end
23
+
24
+ def current_backup_name
25
+ if @current_backup_name.nil?
26
+ state_file = config.global_state_file
27
+ state = JSON.parse(File.read(state_file)) rescue nil
28
+ state ||= {}
29
+ @current_backup_name = state['backup']
30
+ end
31
+ @current_backup_name
32
+ end
33
+
34
+ def perform
35
+ lock(config.global_lock_file) do
36
+ MongoOplogBackup.log.info "Rotating out old backups."
37
+
38
+ if @backup_list.count >= 0 && @backup_list.count <= KEEP_MINIMUM_SETS
39
+ MongoOplogBackup.log.info "Too few backup sets to automatically rotate."
40
+ elsif @backup_list.count > KEEP_MINIMUM_SETS
41
+ filter_for_deletion(@backup_list).each do |path|
42
+ MongoOplogBackup.log.info "#{@dry_run ? '[DRYRUN] Would delete' : 'Deleting'} #{path}."
43
+ begin
44
+ FileUtils.remove_entry_secure(path) unless @dry_run
45
+ rescue StandardError => e
46
+ MongoOplogBackup.log.error "Delete failed: #{e.message}"
47
+ end
48
+ end
49
+ end
50
+
51
+ MongoOplogBackup.log.info "Rotating out old backups completed."
52
+ end
53
+ end
54
+
55
+ # Lists subdirectories in the backup location that match the backup set naming format and appear to have completed
56
+ # successfully.
57
+ # @return [Array<Pathname>] backup directories.
58
+ def find_backup_directories
59
+ dirs = Pathname.new(@config.backup_dir).children.select(&:directory?)
60
+ dirs = dirs.select { |dir| dir.basename.to_s =~ BACKUP_DIR_NAME_FORMAT }
61
+ dirs = dirs.select { |dir| File.exist?(File.join(dir, 'state.json')) }
62
+ dirs.sort
63
+ end
64
+
65
+ # @param [Array<Pathname>] source_list List of Pathnames for the full backup sets in the backup directory.
66
+ # @return [Array<Pathname]
67
+ def filter_for_deletion(source_list)
68
+ source_list = source_list.sort.reverse.drop(KEEP_MINIMUM_SETS) # Keep a minimum number of full backups
69
+ # The most recent dir might not be the active one (eg. if the mongodump fails). Ensure that it is excluded.
70
+ source_list = source_list.reject { |path| path.basename.to_s == current_backup_name }
71
+
72
+ source_list.select {|path| age_of_backup_in_seconds(path.basename) > @recovery_point_objective }
73
+ end
74
+
75
+ private
76
+
77
+ def age_of_backup_in_seconds(path)
78
+ Time.now.to_i - timestamp_from_string(path.to_s).seconds
79
+ end
80
+
81
+ # Accepts: <seconds>[:ordinal]
82
+ def timestamp_from_string(string)
83
+ match = /(\d+)(?::(\d+))?/.match(string)
84
+ return nil unless match
85
+ s1 = match[1].to_i
86
+ i1 = match[2].to_i
87
+ BSON::Timestamp.new(s1,i1)
88
+ end
89
+ end
90
+ end
@@ -1,3 +1,3 @@
1
1
  module MongoOplogBackup
2
- VERSION = "0.0.12"
2
+ VERSION = "0.0.13"
3
3
  end
@@ -23,6 +23,7 @@ Gem::Specification.new do |spec|
23
23
 
24
24
  spec.add_development_dependency "bundler", "~> 1.5"
25
25
  spec.add_development_dependency "rake"
26
- spec.add_development_dependency "rspec", "~> 3.0"
26
+ spec.add_development_dependency "rspec", "~> 3.0.0"
27
27
  spec.add_development_dependency "moped", "~> 2.0"
28
+ spec.add_development_dependency "timecop", "~> 0.9.1"
28
29
  end
@@ -0,0 +1 @@
1
+ {"backup": "backup-1504217100:18"}
@@ -0,0 +1,92 @@
1
+ require 'spec_helper'
2
+ require 'fileutils'
3
+ require 'timecop'
4
+
5
+ describe MongoOplogBackup::Rotate do
6
+ SPEC_TMP='spec-tmp/backup'
7
+
8
+ let(:rotate) { MongoOplogBackup::Rotate.new(MongoOplogBackup::Config.new(dir: SPEC_TMP)) }
9
+ let(:backup_dir) { Pathname.new(SPEC_TMP) }
10
+
11
+ before(:each) do
12
+ FileUtils.mkdir_p SPEC_TMP
13
+ FileUtils.cp_r Dir['spec/fixtures/rotation/backup/*'], SPEC_TMP
14
+ end
15
+
16
+ describe '#find_backup_directories' do
17
+ subject { rotate.find_backup_directories }
18
+
19
+ it 'excludes directories with unexpected name format' do
20
+ expect(subject).to_not include(backup_dir.join('ignore-me'))
21
+ end
22
+
23
+ it 'excludes directories from unsuccessful backups' do
24
+ expect(subject).to_not include(backup_dir.join('backup-1507117250:55'))
25
+ end
26
+
27
+ it 'includes directories from successful backups' do
28
+ expect(subject).to include(backup_dir.join('backup-1498860301:15'))
29
+ expect(subject).to include(backup_dir.join('backup-1501538704:28'))
30
+ expect(subject).to include(backup_dir.join('backup-1504217100:18'))
31
+ expect(subject.count).to eq(3)
32
+ end
33
+ end
34
+
35
+ context 'with defaults' do
36
+ before do
37
+ Timecop.freeze(Time.utc(2017,9,1,23,0,0))
38
+ end
39
+ after do
40
+ Timecop.return
41
+ end
42
+
43
+ # Fixtures created on the 1st at 00:05:00 GMT+2
44
+ # 1498860301:15 2017-06-30 22:05:01 GMT
45
+ # 1501538704:28 2017-07-31 22:05:04 GMT
46
+ # 1504217100:18 2017-08-31 22:05:00 GMT
47
+ it 'excludes the current and previous backup set' do
48
+ filtered_list = rotate.filter_for_deletion(rotate.backup_list)
49
+
50
+
51
+ expect(filtered_list).to eq([backup_dir.join('backup-1498860301:15')])
52
+ # Does not include the incomplete/broken backup job: backup-1507117250:55
53
+ # Does not include arbitrary other directories
54
+ end
55
+
56
+ it 'deletes only the correct directory' do
57
+ rotate.perform
58
+
59
+ expect( File.exist?(File.join(SPEC_TMP, 'backup-1501538704:28' )) ).to eq(true)
60
+ expect( File.exist?(File.join(SPEC_TMP, 'backup-1504217100:18' )) ).to eq(true)
61
+ expect( File.exist?(File.join(SPEC_TMP, 'backup-1498860301:15' )) ).to eq(false)
62
+ end
63
+
64
+ end
65
+
66
+ context 'as a dry run' do
67
+ before do
68
+ Timecop.freeze(Time.utc(2017,9,1,23,0,0))
69
+ end
70
+ after do
71
+ Timecop.return
72
+ end
73
+
74
+ let(:config) do
75
+ {
76
+ dir: SPEC_TMP,
77
+ dryRun: true
78
+ }
79
+ end
80
+ let(:rotate) { MongoOplogBackup::Rotate.new(MongoOplogBackup::Config.new(config)) }
81
+
82
+ it 'does not delete anything.' do
83
+ rotate.perform
84
+
85
+ expect( File.exist?(File.join(SPEC_TMP, 'backup-1501538704:28' )) ).to eq(true)
86
+ expect( File.exist?(File.join(SPEC_TMP, 'backup-1504217100:18' )) ).to eq(true)
87
+ expect( File.exist?(File.join(SPEC_TMP, 'backup-1498860301:15' )) ).to eq(true)
88
+ end
89
+ end
90
+
91
+
92
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mongo-oplog-backup
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.12
4
+ version: 0.0.13
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ralf Kistner
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-08-10 00:00:00.000000000 Z
11
+ date: 2017-10-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bson
@@ -72,14 +72,14 @@ dependencies:
72
72
  requirements:
73
73
  - - "~>"
74
74
  - !ruby/object:Gem::Version
75
- version: '3.0'
75
+ version: 3.0.0
76
76
  type: :development
77
77
  prerelease: false
78
78
  version_requirements: !ruby/object:Gem::Requirement
79
79
  requirements:
80
80
  - - "~>"
81
81
  - !ruby/object:Gem::Version
82
- version: '3.0'
82
+ version: 3.0.0
83
83
  - !ruby/object:Gem::Dependency
84
84
  name: moped
85
85
  requirement: !ruby/object:Gem::Requirement
@@ -94,6 +94,20 @@ dependencies:
94
94
  - - "~>"
95
95
  - !ruby/object:Gem::Version
96
96
  version: '2.0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: timecop
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: 0.9.1
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: 0.9.1
97
111
  description: Periodically backup new sections of the oplog for incremental backups.
98
112
  email:
99
113
  - ralf@journeyapps.com
@@ -117,8 +131,10 @@ files:
117
131
  - lib/mongo_oplog_backup/config.rb
118
132
  - lib/mongo_oplog_backup/ext/enumerable.rb
119
133
  - lib/mongo_oplog_backup/ext/timestamp.rb
134
+ - lib/mongo_oplog_backup/lock.rb
120
135
  - lib/mongo_oplog_backup/oplog.rb
121
136
  - lib/mongo_oplog_backup/restore.rb
137
+ - lib/mongo_oplog_backup/rotate.rb
122
138
  - lib/mongo_oplog_backup/version.rb
123
139
  - mongo-oplog-backup.gemspec
124
140
  - oplog-last-timestamp.js
@@ -134,7 +150,13 @@ files:
134
150
  - spec/fixtures/oplog-1408088740:1-1408088810:1.bson
135
151
  - spec/fixtures/oplog-1408088810:1-1408088928:1.bson
136
152
  - spec/fixtures/oplog-merged.bson
153
+ - spec/fixtures/rotation/backup/backup-1498860301:15/state.json
154
+ - spec/fixtures/rotation/backup/backup-1501538704:28/state.json
155
+ - spec/fixtures/rotation/backup/backup-1504217100:18/state.json
156
+ - spec/fixtures/rotation/backup/backup-1507117250:55/dump/incomplete
157
+ - spec/fixtures/rotation/backup/backup.json
137
158
  - spec/oplog_spec.rb
159
+ - spec/rotate_spec.rb
138
160
  - spec/spec_helper.rb
139
161
  - spec/timestamp_spec.rb
140
162
  - testing.sh
@@ -158,7 +180,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
158
180
  version: '0'
159
181
  requirements: []
160
182
  rubyforge_project:
161
- rubygems_version: 2.4.5.1
183
+ rubygems_version: 2.6.13
162
184
  signing_key:
163
185
  specification_version: 4
164
186
  summary: Incremental backups for MongoDB using the oplog.
@@ -174,6 +196,12 @@ test_files:
174
196
  - spec/fixtures/oplog-1408088740:1-1408088810:1.bson
175
197
  - spec/fixtures/oplog-1408088810:1-1408088928:1.bson
176
198
  - spec/fixtures/oplog-merged.bson
199
+ - spec/fixtures/rotation/backup/backup-1498860301:15/state.json
200
+ - spec/fixtures/rotation/backup/backup-1501538704:28/state.json
201
+ - spec/fixtures/rotation/backup/backup-1504217100:18/state.json
202
+ - spec/fixtures/rotation/backup/backup-1507117250:55/dump/incomplete
203
+ - spec/fixtures/rotation/backup/backup.json
177
204
  - spec/oplog_spec.rb
205
+ - spec/rotate_spec.rb
178
206
  - spec/spec_helper.rb
179
207
  - spec/timestamp_spec.rb