mongo-oplog-backup 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 3acf779f860fdb9a7fc941b68f7755c36f215ad6
4
+ data.tar.gz: 5a1a19a402c4d337b90c81efc693aeb7d583e454
5
+ SHA512:
6
+ metadata.gz: 5c01fe3144e8a2c891c363bac256082c17f836d71fa2b097d8f3e737afcaba51d41034b6c7dda7025ac5d0ee282a35908796441bbb9bf41cafc0aa89ac9e35af
7
+ data.tar.gz: 94b42dac6b402b476c83ff6f1f131884d266d2b8bc545b389440574fc36f084bcbd7df9152d6149ee1d2f9a2c37e47864822d795c0d619109c93a3de2d1a17e1
@@ -0,0 +1,19 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ test.log
19
+ spec-tmp
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
@@ -0,0 +1 @@
1
+ 2.1.1
@@ -0,0 +1,11 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.1.1
4
+ before_script:
5
+ - mkdir testdb
6
+ - mongod --port 27017 --dbpath testdb --replSet rs0 --oplogSize 20 --noprealloc --fork --smallfiles --logpath mongodb.log
7
+ - sleep 3
8
+ - mongo admin --eval 'printjson(rs.initiate());'
9
+ - sleep 20
10
+ script:
11
+ - bundle exec rspec
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in mongo-oplog-backup.gemspec
4
+ gemspec
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Ralf Kistner
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,34 @@
1
+ # MongoOplogBackup
2
+
3
+ **Experimental** incremental backup system for MongoDB based on the oplog.
4
+
5
+ Not ready for any important data yet.
6
+
7
+ ## Installation
8
+
9
+ git clone git@github.com:journeyapps/mongo-oplog-backup.git
10
+ cd mongo-oplog-backup
11
+ rake install
12
+
13
+ ## Usage
14
+
15
+ mongo-oplog-backup backup --dir mybackup
16
+
17
+ TODO: Write usage instructions here
18
+
19
+ ## Backup structure
20
+
21
+ * `backup.json` - Stores the current state (oplog timestamp and backup folder).
22
+ The only file required to perform incremental backups. It is not used for restoring a backup.
23
+ * `backup-<timestamp>` - The current backup folder.
24
+ * `dump` - a full mongodump
25
+ * `oplog-<start>-<end>.bson` - The oplog from the start timestamp until the end timestamp (inclusive).
26
+
27
+ Each time a full backup is performed, a new backup folder is created.
28
+ ## Contributing
29
+
30
+ 1. Fork it ( http://github.com/<my-github-username>/mongo-oplog-backup/fork )
31
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
32
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
33
+ 4. Push to the branch (`git push origin my-new-feature`)
34
+ 5. Create new Pull Request
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
@@ -0,0 +1,61 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'mongo_oplog_backup'
4
+ require 'slop'
5
+
6
+ opts = Slop.parse(help: true, strict: true) do
7
+ banner 'Usage: mongo-oplog-backup [options]'
8
+
9
+ command 'backup' do
10
+ banner 'Usage: mongo-oplog-backup backup [options]'
11
+ on :v, :verbose, 'Enable verbose mode'
12
+ on :d, :dir, "Directory to store backup files. Defaults to 'backup'.", argument: :required
13
+ on :full, 'Force full backup'
14
+ on :oplog, 'Force oplog backup'
15
+
16
+ on :ssl, "Connect to a mongod instance over an SSL connection"
17
+ on :host, "Specifies a resolvable hostname for the mongod that you wish to backup.", default: 'localhost', argument: :required
18
+ on :port, "Specifies the port that mongod is running on", default: '27017', argument: :required
19
+ on :u, :username, "Specifies a username to authenticate to the MongoDB instance, if your database requires authentication. Use in conjunction with the --password option to supply a password.", argument: :required
20
+ on :p, :password, "Specifies a password to authenticate to the MongoDB instance. Use in conjunction with the --username option to supply a username. Note. the password will not be prompted for, so must be passed as an argument", argument: :required
21
+
22
+ run do |opts, args|
23
+ dir = opts[:dir] || 'backup'
24
+ config_opts = {
25
+ dir: dir,
26
+ ssl: opts.ssl?
27
+ }
28
+ config_opts[:host] = opts[:host]
29
+ config_opts[:port] = opts[:port]
30
+ config_opts[:username] = opts[:username]
31
+ config_opts[:password] = opts[:password]
32
+
33
+ mode = :auto
34
+ if opts.full?
35
+ mode = :full
36
+ elsif opts.oplog?
37
+ mode = :oplog
38
+ end
39
+ config = MongoOplogBackup::Config.new(config_opts)
40
+ backup = MongoOplogBackup::Backup.new(config)
41
+ backup.perform(mode)
42
+ end
43
+ end
44
+
45
+ command 'merge' do
46
+ banner 'Usage: mongo-oplog-backup merge [options]'
47
+ on :v, :verbose, 'Enable verbose mode'
48
+ on :d, :dir, "Directory containing the backup to restore. Must contain a 'dump' folder.", argument: :required
49
+
50
+ run do |opts, args|
51
+ dir = opts[:dir]
52
+ raise ArgumentError, 'dir must be specified' unless dir
53
+ raise ArgumentError, 'dir must contain a dump subfolder' unless File.directory?(File.join(dir, 'dump'))
54
+
55
+ MongoOplogBackup::Oplog.merge_backup(dir)
56
+ puts
57
+ puts "Restore the backup with: "
58
+ puts "mongorestore [--drop] --oplogReplay #{File.join(dir, 'dump')}"
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,21 @@
1
+ require 'logger'
2
+
3
+ require 'mongo_oplog_backup/version'
4
+ require 'mongo_oplog_backup/ext/enumerable'
5
+ require 'mongo_oplog_backup/ext/timestamp'
6
+
7
+ require 'mongo_oplog_backup/config'
8
+ require 'mongo_oplog_backup/backup'
9
+ require 'mongo_oplog_backup/oplog'
10
+
11
+ module MongoOplogBackup
12
+ def self.log
13
+ @@log
14
+ end
15
+
16
+ def self.log= log
17
+ @@log = log
18
+ end
19
+
20
+ @@log = Logger.new STDOUT
21
+ end
@@ -0,0 +1,148 @@
1
+ require 'json'
2
+ require 'fileutils'
3
+ require 'mongo_oplog_backup/oplog'
4
+
5
+ module MongoOplogBackup
6
+ class Backup
7
+ attr_reader :config
8
+
9
+ def initialize(config)
10
+ @config = config
11
+ end
12
+
13
+ def backup_oplog(options={})
14
+ start_at = options[:start]
15
+ backup = options[:backup]
16
+ raise ArgumentError, ":backup is required" unless backup
17
+ raise ArgumentError, ":start is required" unless start_at
18
+
19
+ if start_at
20
+ query = "--query \"{ts : { \\$gte : { \\$timestamp : { t : #{start_at.seconds}, i : #{start_at.increment} } } }}\""
21
+ else
22
+ query = ""
23
+ end
24
+ config.mongodump("--out #{config.oplog_dump_folder} --db local --collection oplog.rs #{query}")
25
+
26
+ unless File.exists? config.oplog_dump
27
+ raise "mongodump failed"
28
+ end
29
+ MongoOplogBackup.log.debug "Checking timestamps..."
30
+ timestamps = Oplog.oplog_timestamps(config.oplog_dump)
31
+
32
+ unless timestamps.increasing?
33
+ raise "Something went wrong - oplog is not ordered."
34
+ end
35
+
36
+ first = timestamps[0]
37
+ last = timestamps[-1]
38
+
39
+ if first > start_at
40
+ raise "Expected first oplog entry to be #{start_at.inspect} but was #{first.inspect}\n" +
41
+ "The oplog is probably too small.\n" +
42
+ "Increase the oplog size, the start with another full backup."
43
+ elsif first < start_at
44
+ raise "Expected first oplog entry to be #{start_at.inspect} but was #{first.inspect}\n" +
45
+ "Something went wrong in our query."
46
+ end
47
+
48
+ result = {
49
+ entries: timestamps.count,
50
+ first: first,
51
+ position: last
52
+ }
53
+
54
+ if timestamps.count == 1
55
+ result[:empty] = true
56
+ else
57
+ outfile = "oplog-#{first}-#{last}.bson"
58
+ full_path = File.join(config.backup_dir, backup, outfile)
59
+ FileUtils.mkdir_p File.join(config.backup_dir, backup)
60
+ FileUtils.mv config.oplog_dump, full_path
61
+
62
+ result[:file] = full_path
63
+ result[:empty] = false
64
+ end
65
+
66
+ FileUtils.rm_r config.oplog_dump_folder rescue nil
67
+ result
68
+ end
69
+
70
+ def latest_oplog_timestamp
71
+ script = File.expand_path('../../oplog-last-timestamp.js', File.dirname(__FILE__))
72
+ result_text = config.mongo('local', script)
73
+ begin
74
+ response = JSON.parse(result_text)
75
+ return nil unless response['position']
76
+ BSON::Timestamp.from_json(response['position'])
77
+ rescue JSON::ParserError => e
78
+ raise StandardError, "Failed to connect to MongoDB: #{result_text}"
79
+ end
80
+ end
81
+
82
+ def backup_full
83
+ position = latest_oplog_timestamp
84
+ raise "Cannot backup with empty oplog" if position.nil?
85
+ backup_name = "backup-#{position}"
86
+ dump_folder = File.join(config.backup_dir, backup_name, 'dump')
87
+ config.mongodump("--out #{dump_folder}")
88
+ return {
89
+ position: position,
90
+ backup: backup_name
91
+ }
92
+ end
93
+
94
+ def perform(mode=:auto)
95
+ state_file = config.state_file
96
+ state = JSON.parse(File.read(state_file)) rescue nil
97
+ state ||= {}
98
+ have_position = (state['position'] && state['backup'])
99
+
100
+ if mode == :auto
101
+ if have_position
102
+ mode = :oplog
103
+ else
104
+ mode = :full
105
+ end
106
+ end
107
+
108
+ if mode == :oplog
109
+ raise "Unknown backup position - cannot perform oplog backup." unless have_position
110
+ MongoOplogBackup.log.info "Performing incremental oplog backup"
111
+ position = BSON::Timestamp.from_json(state['position'])
112
+ result = backup_oplog(start: position, backup: state['backup'])
113
+ unless result[:empty]
114
+ new_entries = result[:entries] - 1
115
+ state['position'] = result[:position]
116
+ File.write(state_file, state.to_json)
117
+ MongoOplogBackup.log.info "Backed up #{new_entries} new entries to #{result[:file]}"
118
+ else
119
+ MongoOplogBackup.log.info "Nothing new to backup"
120
+ end
121
+ elsif mode == :full
122
+ MongoOplogBackup.log.info "Performing full backup"
123
+ result = backup_full
124
+ state = result
125
+ File.write(state_file, state.to_json)
126
+ MongoOplogBackup.log.info "Performed full backup"
127
+
128
+ # Oplog backup
129
+ perform(:oplog)
130
+ end
131
+ end
132
+
133
+ def latest_oplog_timestamp_moped
134
+ # Alternative implementation for `latest_oplog_timestamp`
135
+ require 'moped'
136
+ session = Moped::Session.new([ "127.0.0.1:27017" ])
137
+ session.use 'local'
138
+ oplog = session['oplog.rs']
139
+ entry = oplog.find.limit(1).sort('$natural' => -1).one
140
+ if entry
141
+ entry['ts']
142
+ else
143
+ nil
144
+ end
145
+ end
146
+
147
+ end
148
+ end
@@ -0,0 +1,47 @@
1
+ module MongoOplogBackup
2
+ class Config
3
+ attr_reader :options
4
+
5
+ def initialize(options)
6
+ @options = options
7
+ end
8
+
9
+ def backup_dir
10
+ options[:dir]
11
+ end
12
+
13
+ def command_line_options
14
+ ssl = options[:ssl] ? '--ssl ' : ''
15
+ host = options[:host] ? "--host #{options[:host].strip} " : ''
16
+ port = options[:port] ? "--port #{options[:port].strip} " : ''
17
+ username = options[:username] ? "--username #{options[:username].strip} " : ''
18
+ password = options[:password] ? "--password #{options[:password].strip} " : ''
19
+ "#{host}#{port}#{ssl}#{username}#{password}"
20
+ end
21
+
22
+ def oplog_dump_folder
23
+ File.join(backup_dir, 'dump')
24
+ end
25
+
26
+ def oplog_dump
27
+ File.join(oplog_dump_folder, 'local/oplog.rs.bson')
28
+ end
29
+
30
+ def state_file
31
+ File.join(backup_dir, 'backup.json')
32
+ end
33
+
34
+ def exec(cmd)
35
+ MongoOplogBackup.log.debug ">>> #{cmd}"
36
+ `#{cmd}`
37
+ end
38
+
39
+ def mongodump(args)
40
+ MongoOplogBackup.log.info exec("mongodump #{command_line_options} #{args}")
41
+ end
42
+
43
+ def mongo(db, script)
44
+ exec("mongo #{command_line_options} --quiet --norc #{db} #{script}")
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,17 @@
1
+ # Define Enumerable#sorted?
2
+
3
+ module Enumerable
4
+ # Sorted in ascending order
5
+ def sorted?
6
+ each_cons(2).all? do |a, b|
7
+ (a <=> b) <= 0
8
+ end
9
+ end
10
+
11
+ # Strictly increasing, in other words sorted and unique
12
+ def increasing?
13
+ each_cons(2).all? do |a, b|
14
+ (a <=> b) < 0
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,34 @@
1
+ # Make BSON::Timestamp comparable
2
+ require 'bson'
3
+
4
+ module MongoOplogBackup::Ext
5
+ module Timestamp
6
+ def <=> other
7
+ [seconds, increment] <=> [other.seconds, other.increment]
8
+ end
9
+
10
+ def to_s
11
+ "#{seconds}:#{increment}"
12
+ end
13
+
14
+ def hash
15
+ to_s.hash
16
+ end
17
+
18
+ def eql? other
19
+ self == other
20
+ end
21
+
22
+ module ClassMethods
23
+ # Accepts {'t' => seconds, 'i' => increment}
24
+ def from_json(data)
25
+ self.new(data['t'], data['i'])
26
+ end
27
+ end
28
+
29
+ end
30
+ end
31
+
32
+ ::BSON::Timestamp.__send__(:include, Comparable)
33
+ ::BSON::Timestamp.__send__(:include, MongoOplogBackup::Ext::Timestamp)
34
+ ::BSON::Timestamp.extend(MongoOplogBackup::Ext::Timestamp::ClassMethods)
@@ -0,0 +1,110 @@
1
+ module MongoOplogBackup
2
+ module Oplog
3
+ def self.each_document(filename)
4
+ File.open(filename, 'rb') do |stream|
5
+ while !stream.eof?
6
+ yield BSON::Document.from_bson(stream)
7
+ end
8
+ end
9
+ end
10
+
11
+ def self.oplog_timestamps(filename)
12
+ timestamps = []
13
+ each_document(filename) do |doc|
14
+ # This can be optimized by only decoding the timestamp
15
+ # (first field), instead of decoding the entire document.
16
+ timestamps << doc['ts']
17
+ end
18
+ timestamps
19
+ end
20
+
21
+ FILENAME_RE = /\/oplog-(\d+):(\d+)-(\d+):(\d+)\.bson\z/
22
+
23
+ def self.timestamps_from_filename filename
24
+ match = FILENAME_RE.match(filename)
25
+ return nil unless match
26
+ s1 = match[1].to_i
27
+ i1 = match[2].to_i
28
+ s2 = match[3].to_i
29
+ i2 = match[4].to_i
30
+ first = BSON::Timestamp.new(s1, i1)
31
+ last = BSON::Timestamp.new(s2, i2)
32
+ {
33
+ first: first,
34
+ last: last
35
+ }
36
+ end
37
+
38
+ def self.merge(target, source_files, options={})
39
+ limit = options[:limit] # TODO: use
40
+ force = options[:force]
41
+
42
+ File.open(target, 'wb') do |output|
43
+ last_timestamp = nil
44
+ first = true
45
+
46
+ source_files.each do |filename|
47
+ timestamps = timestamps_from_filename(filename)
48
+ if timestamps
49
+ expected_first = timestamps[:first]
50
+ expected_last = timestamps[:last]
51
+ else
52
+ expected_first = nil
53
+ expected_last = nil
54
+ end
55
+
56
+ # Optimize:
57
+ # We can assume that the timestamps are in order.
58
+ # This means we only need to find the first non-overlapping point,
59
+ # and the rest we can pass through directly.
60
+ MongoOplogBackup.log.debug "Reading #{filename}"
61
+ last_file_timestamp = nil
62
+ skipped = 0
63
+ wrote = 0
64
+ first_file_timestamp = nil
65
+ Oplog.each_document(filename) do |doc|
66
+ timestamp = doc['ts']
67
+ first_file_timestamp = timestamp if first_file_timestamp.nil?
68
+ if !last_timestamp.nil? && timestamp <= last_timestamp
69
+ skipped += 1
70
+ elsif !last_file_timestamp.nil? && timestamp <= last_file_timestamp
71
+ raise "Timestamps out of order in #{filename}"
72
+ else
73
+ output.write(doc.to_bson)
74
+ wrote += 1
75
+ last_timestamp = timestamp
76
+ end
77
+ last_file_timestamp = timestamp
78
+ end
79
+
80
+ if expected_first && first_file_timestamp != expected_first
81
+ raise "#{expected_first} was not the first timestamp in #{filename}"
82
+ end
83
+
84
+ if expected_last && last_file_timestamp != expected_last
85
+ raise "#{expected_last} was not the last timestamp in #{filename}"
86
+ end
87
+
88
+ MongoOplogBackup.log.info "Wrote #{wrote} and skipped #{skipped} oplog entries from #{filename}"
89
+ raise "Overlap must be exactly 1" unless first || skipped == 1 || force
90
+ first = false
91
+ end
92
+ end
93
+ end
94
+
95
+ def self.find_oplogs(dir)
96
+ files = Dir.glob(File.join(dir, 'oplog-*.bson'))
97
+ files.keep_if {|name| name =~ FILENAME_RE}
98
+ files.sort! {|a, b| timestamps_from_filename(a)[:first] <=> timestamps_from_filename(b)[:first]}
99
+ files
100
+ end
101
+
102
+ def self.merge_backup(dir)
103
+ oplogs = find_oplogs(dir)
104
+ target = File.join(dir, 'dump', 'oplog.bson')
105
+ FileUtils.mkdir_p(File.join(dir, 'dump'))
106
+ merge(target, oplogs)
107
+ end
108
+
109
+ end
110
+ end
@@ -0,0 +1,3 @@
1
+ module MongoOplogBackup
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,28 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'mongo_oplog_backup/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "mongo-oplog-backup"
8
+ spec.version = MongoOplogBackup::VERSION
9
+ spec.authors = ["Ralf Kistner"]
10
+ spec.email = ["ralf@journeyapps.com"]
11
+ spec.summary = %q{Incremental backups for MongoDB using the oplog.}
12
+ spec.description = %q{Periodically backup new sections of the oplog for incremental backups.}
13
+ spec.homepage = ""
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_dependency "bson", "~> 2.3"
22
+ spec.add_dependency "slop", "~> 3.6"
23
+
24
+ spec.add_development_dependency "bundler", "~> 1.5"
25
+ spec.add_development_dependency "rake"
26
+ spec.add_development_dependency "rspec", "~> 3.0"
27
+ spec.add_development_dependency "moped", "~> 2.0"
28
+ end
@@ -0,0 +1,11 @@
1
+ // Get the timestamp of the last oplog entry.
2
+ // Usage: mongo --quiet --norc local oplog-last-timestamp.js
3
+
4
+ var local = db.getSiblingDB('local');
5
+ var last = local['oplog.rs'].find().sort({'$natural': -1}).limit(1)[0];
6
+ var result = {};
7
+ if(last != null) {
8
+ result = {position: last['ts']};
9
+ }
10
+
11
+ print(JSON.stringify(result));
@@ -0,0 +1,55 @@
1
+ require 'spec_helper'
2
+ require 'moped'
3
+
4
+ describe MongoOplogBackup do
5
+ it 'should have a version number' do
6
+ MongoOplogBackup::VERSION.should_not be_nil
7
+ end
8
+
9
+ let(:backup) { MongoOplogBackup::Backup.new(MongoOplogBackup::Config.new dir: 'spec-tmp/backup') }
10
+
11
+ before(:all) do
12
+ # We need one entry in the oplog to start with
13
+ SESSION.with(safe: true) do |session|
14
+ session['test'].insert({a: 1})
15
+ end
16
+ end
17
+
18
+ it 'should get the latest oplog entry' do
19
+ ts1 = backup.latest_oplog_timestamp
20
+ ts2 = backup.latest_oplog_timestamp_moped
21
+
22
+ ts1.should == ts2
23
+ end
24
+
25
+ it 'should error on latest oplog entry with invalid port' do
26
+ b2 = MongoOplogBackup::Backup.new(MongoOplogBackup::Config.new({
27
+ dir: 'spec-tmp/backup', port: '12345'}))
28
+ -> { b2.latest_oplog_timestamp }.should raise_error
29
+ end
30
+
31
+ it 'should error on latest oplog entry with invalid password' do
32
+ b2 = MongoOplogBackup::Backup.new(MongoOplogBackup::Config.new({
33
+ dir: 'spec-tmp/backup', username: 'foo', password: '123'}))
34
+ -> { b2.latest_oplog_timestamp }.should raise_error
35
+ end
36
+
37
+
38
+ it "should perform an oplog backup" do
39
+ first = backup.latest_oplog_timestamp
40
+ first.should_not be_nil
41
+ SESSION.with(safe: true) do |session|
42
+ 5.times do
43
+ session['test'].insert({a: 1})
44
+ end
45
+ end
46
+ last = backup.latest_oplog_timestamp
47
+ result = backup.backup_oplog(start: first, backup: 'backup1')
48
+ file = result[:file]
49
+ timestamps = MongoOplogBackup::Oplog.oplog_timestamps(file)
50
+ timestamps.count.should == 6
51
+ timestamps.first.should == first
52
+ timestamps.last.should == last
53
+
54
+ end
55
+ end
@@ -0,0 +1,21 @@
1
+ require 'spec_helper'
2
+
3
+ describe Enumerable do
4
+ it 'should define sorted?' do
5
+ [1, 2, 3, 4, 6].sorted?.should == true
6
+ [1, 2, 3, 6, 4].sorted?.should == false
7
+ [1, 2, 3, 4, 4].sorted?.should == true
8
+ [6, 4, 3, 2, 1].sorted?.should == false
9
+ [1].sorted?.should == true
10
+ [].sorted?.should == true
11
+ end
12
+
13
+ it 'should define increasing?' do
14
+ [1, 2, 3, 4, 6].increasing?.should == true
15
+ [1, 2, 3, 6, 4].increasing?.should == false
16
+ [1, 2, 3, 4, 4].increasing?.should == false
17
+ [6, 4, 3, 2, 1].increasing?.should == false
18
+ [1].increasing?.should == true
19
+ [].increasing?.should == true
20
+ end
21
+ end
@@ -0,0 +1,58 @@
1
+ require 'spec_helper'
2
+ require 'fileutils'
3
+
4
+ describe MongoOplogBackup::Oplog do
5
+ let(:oplog1) { 'spec/fixtures/oplog-1408088734:1-1408088740:1.bson'}
6
+ let(:oplog2) { 'spec/fixtures/oplog-1408088740:1-1408088810:1.bson'}
7
+ let(:oplog3) { 'spec/fixtures/oplog-1408088810:1-1408088928:1.bson'}
8
+ let(:oplog_merged) { 'spec/fixtures/oplog-merged.bson'}
9
+
10
+ it 'should extract oplog timestamps' do
11
+ timestamps = MongoOplogBackup::Oplog.oplog_timestamps(oplog1)
12
+ timestamps.should == [
13
+ BSON::Timestamp.new(1408088734, 1),
14
+ BSON::Timestamp.new(1408088738, 1),
15
+ BSON::Timestamp.new(1408088739, 1),
16
+ BSON::Timestamp.new(1408088740, 1)
17
+ ]
18
+ end
19
+
20
+ it 'should merge oplogs' do
21
+ merged_out = 'spec-tmp/oplog-merged.bson'
22
+ MongoOplogBackup::Oplog.merge(merged_out, [oplog1, oplog2, oplog3])
23
+
24
+ expected_timestamps =
25
+ MongoOplogBackup::Oplog.oplog_timestamps(oplog1) +
26
+ MongoOplogBackup::Oplog.oplog_timestamps(oplog2) +
27
+ MongoOplogBackup::Oplog.oplog_timestamps(oplog3)
28
+
29
+ expected_timestamps.uniq!
30
+ expected_timestamps.sort! # Not sure if uniq! modifies the order
31
+
32
+ actual_timestamps = MongoOplogBackup::Oplog.oplog_timestamps(merged_out)
33
+ actual_timestamps.should == expected_timestamps
34
+
35
+ merged_out.should be_same_oplog_as oplog_merged
36
+ end
37
+
38
+ it 'should parse timestamps from a filename' do
39
+ timestamps = MongoOplogBackup::Oplog.timestamps_from_filename('some/oplog-1408088734:1-1408088740:52.bson')
40
+ timestamps.should == {
41
+ first: BSON::Timestamp.new(1408088734, 1),
42
+ last: BSON::Timestamp.new(1408088740, 52)
43
+ }
44
+ end
45
+ it 'should sort oplogs in a folder' do
46
+ oplogs = MongoOplogBackup::Oplog.find_oplogs('spec/fixtures')
47
+ oplogs.should == [oplog1, oplog2, oplog3]
48
+ end
49
+
50
+ it "should merge a backup folder" do
51
+ FileUtils.mkdir_p 'spec-tmp/backup'
52
+ FileUtils.cp_r Dir['spec/fixtures/oplog-*.bson'], 'spec-tmp/backup/'
53
+
54
+ MongoOplogBackup::Oplog.merge_backup('spec-tmp/backup')
55
+
56
+ 'spec-tmp/backup/dump/oplog.bson'.should be_same_oplog_as oplog_merged
57
+ end
58
+ end
@@ -0,0 +1,54 @@
1
+ $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
2
+ require 'mongo_oplog_backup'
3
+ require 'fileutils'
4
+
5
+ FileUtils.rm_rf 'test.log'
6
+ MongoOplogBackup.log = Logger.new('test.log')
7
+
8
+ #https://gist.github.com/mattwynne/736421
9
+ RSpec::Matchers.define(:be_same_file_as) do |exected_file_path|
10
+ match do |actual_file_path|
11
+ md5_hash(actual_file_path).should == md5_hash(exected_file_path)
12
+ end
13
+
14
+ def md5_hash(file_path)
15
+ Digest::MD5.hexdigest(File.read(file_path))
16
+ end
17
+ end
18
+
19
+ RSpec::Matchers.define(:be_same_oplog_as) do |exected_file_path|
20
+ match do |actual_file_path|
21
+ timestamps(actual_file_path).should == timestamps(exected_file_path)
22
+ actual_file_path.should be_same_file_as exected_file_path
23
+ end
24
+
25
+ failure_message do |actual_file_path|
26
+ ets = timestamps(exected_file_path).join("\n")
27
+ ats = timestamps(actual_file_path).join("\n")
28
+ "expected that #{actual_file_path} would be the same as #{exected_file_path}\n" +
29
+ "Expected timestamps:\n#{ets}\n" +
30
+ "Actual timestamps:\n#{ats}"
31
+ end
32
+
33
+ def timestamps(file_path)
34
+ MongoOplogBackup::Oplog.oplog_timestamps(file_path)
35
+ end
36
+ end
37
+
38
+ RSpec.configure do |config|
39
+ config.expect_with :rspec do |c|
40
+ c.syntax = :should
41
+ end
42
+
43
+ config.before(:each) do
44
+ FileUtils.mkdir_p 'spec-tmp'
45
+ end
46
+
47
+ config.after(:each) do
48
+ FileUtils.rm_rf 'spec-tmp'
49
+ end
50
+ end
51
+
52
+ require 'moped'
53
+ SESSION = Moped::Session.new([ "127.0.0.1:27017" ])
54
+ SESSION.use 'backup-test'
@@ -0,0 +1,49 @@
1
+ require 'spec_helper'
2
+
3
+ describe MongoOplogBackup::Ext::Timestamp do
4
+ it 'should be comparable' do
5
+ a = BSON::Timestamp.new(1408004593, 1)
6
+ b = BSON::Timestamp.new(1408004593, 2)
7
+ c = BSON::Timestamp.new(1408004594, 1)
8
+ (a <=> a).should == 0
9
+ (b <=> b).should == 0
10
+ (a <=> b).should == -1
11
+ (b <=> a).should == 1
12
+ (b <=> c).should == -1
13
+ (a <=> c).should == -1
14
+ (c <=> a).should == 1
15
+ end
16
+
17
+ it "should have a consistent hash and eql?" do
18
+ a1 = BSON::Timestamp.new(1408004593, 1)
19
+ a2 = BSON::Timestamp.new(1408004593, 1)
20
+ b = BSON::Timestamp.new(1408004593, 2)
21
+ c = BSON::Timestamp.new(1408004594, 1)
22
+
23
+ a1.hash.should == a2.hash
24
+ a1.hash.should_not == b.hash
25
+ a1.hash.should_not == c.hash
26
+
27
+ a1.eql?(a2).should == true
28
+ a1.eql?(b).should == false
29
+ a1.eql?(c).should == false
30
+
31
+ (a1 == a2).should == true
32
+ (a1 == b).should == false
33
+ (a1 == c).should == false
34
+ end
35
+
36
+ it 'should define from_json' do
37
+ json = {"t" => 1408004593, "i" => 20}
38
+ ts = BSON::Timestamp.from_json(json)
39
+ ts.seconds.should == 1408004593
40
+ ts.increment.should == 20
41
+ ts.as_json.should == json
42
+ end
43
+
44
+ it 'should define to_s' do
45
+ ts = BSON::Timestamp.new(1408004593, 2)
46
+ ts.to_s.should == '1408004593:2'
47
+ "#{ts}".should == '1408004593:2'
48
+ end
49
+ end
metadata ADDED
@@ -0,0 +1,165 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: mongo-oplog-backup
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Ralf Kistner
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-08-15 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bson
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '2.3'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '2.3'
27
+ - !ruby/object:Gem::Dependency
28
+ name: slop
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '3.6'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '3.6'
41
+ - !ruby/object:Gem::Dependency
42
+ name: bundler
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.5'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.5'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rake
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rspec
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '3.0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '3.0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: moped
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '2.0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '2.0'
97
+ description: Periodically backup new sections of the oplog for incremental backups.
98
+ email:
99
+ - ralf@journeyapps.com
100
+ executables:
101
+ - mongo-oplog-backup
102
+ extensions: []
103
+ extra_rdoc_files: []
104
+ files:
105
+ - ".gitignore"
106
+ - ".rspec"
107
+ - ".ruby-version"
108
+ - ".travis.yml"
109
+ - Gemfile
110
+ - LICENSE.txt
111
+ - README.md
112
+ - Rakefile
113
+ - bin/mongo-oplog-backup
114
+ - lib/mongo_oplog_backup.rb
115
+ - lib/mongo_oplog_backup/backup.rb
116
+ - lib/mongo_oplog_backup/config.rb
117
+ - lib/mongo_oplog_backup/ext/enumerable.rb
118
+ - lib/mongo_oplog_backup/ext/timestamp.rb
119
+ - lib/mongo_oplog_backup/oplog.rb
120
+ - lib/mongo_oplog_backup/version.rb
121
+ - mongo-oplog-backup.gemspec
122
+ - oplog-last-timestamp.js
123
+ - spec/backup_spec.rb
124
+ - spec/enumerable_spec.rb
125
+ - spec/fixtures/oplog-1408088734:1-1408088740:1.bson
126
+ - spec/fixtures/oplog-1408088740:1-1408088810:1.bson
127
+ - spec/fixtures/oplog-1408088810:1-1408088928:1.bson
128
+ - spec/fixtures/oplog-merged.bson
129
+ - spec/oplog_spec.rb
130
+ - spec/spec_helper.rb
131
+ - spec/timestamp_spec.rb
132
+ homepage: ''
133
+ licenses:
134
+ - MIT
135
+ metadata: {}
136
+ post_install_message:
137
+ rdoc_options: []
138
+ require_paths:
139
+ - lib
140
+ required_ruby_version: !ruby/object:Gem::Requirement
141
+ requirements:
142
+ - - ">="
143
+ - !ruby/object:Gem::Version
144
+ version: '0'
145
+ required_rubygems_version: !ruby/object:Gem::Requirement
146
+ requirements:
147
+ - - ">="
148
+ - !ruby/object:Gem::Version
149
+ version: '0'
150
+ requirements: []
151
+ rubyforge_project:
152
+ rubygems_version: 2.2.2
153
+ signing_key:
154
+ specification_version: 4
155
+ summary: Incremental backups for MongoDB using the oplog.
156
+ test_files:
157
+ - spec/backup_spec.rb
158
+ - spec/enumerable_spec.rb
159
+ - spec/fixtures/oplog-1408088734:1-1408088740:1.bson
160
+ - spec/fixtures/oplog-1408088740:1-1408088810:1.bson
161
+ - spec/fixtures/oplog-1408088810:1-1408088928:1.bson
162
+ - spec/fixtures/oplog-merged.bson
163
+ - spec/oplog_spec.rb
164
+ - spec/spec_helper.rb
165
+ - spec/timestamp_spec.rb