Class: MongoOplogBackup::Backup

Inherits:
Object
  • Object
show all
Defined in:
lib/mongo_oplog_backup/backup.rb

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(config) ⇒ Backup

Returns a new instance of Backup.



12
13
14
# File 'lib/mongo_oplog_backup/backup.rb', line 12

def initialize(config)
  @config = config
end

Instance Attribute Details

#configObject (readonly)

Returns the value of attribute config.



10
11
12
# File 'lib/mongo_oplog_backup/backup.rb', line 10

def config
  @config
end

Instance Method Details

#backup_fullObject



95
96
97
98
99
100
101
102
103
104
105
# File 'lib/mongo_oplog_backup/backup.rb', line 95

def backup_full
  position = latest_oplog_timestamp
  raise "Cannot backup with empty oplog" if position.nil?
  backup_name = "backup-#{position}"
  dump_folder = File.join(config.backup_dir, backup_name, 'dump')
  config.mongodump("--out #{dump_folder}")
  return {
    position: position,
    backup: backup_name
  }
end

#backup_oplog(options = {}) ⇒ Object

Raises:

  • (ArgumentError)


26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
# File 'lib/mongo_oplog_backup/backup.rb', line 26

def backup_oplog(options={})
  start_at = options[:start]
  backup = options[:backup]
  raise ArgumentError, ":backup is required" unless backup
  raise ArgumentError, ":start is required" unless start_at

  if start_at
    query = "--query \"{ts : { \\$gte : { \\$timestamp : { t : #{start_at.seconds}, i : #{start_at.increment} } } }}\""
  else
    query = ""
  end
  config.mongodump("--out #{config.oplog_dump_folder} --db local --collection oplog.rs #{query}")

  unless File.exists? config.oplog_dump
    raise "mongodump failed"
  end
  MongoOplogBackup.log.debug "Checking timestamps..."
  timestamps = Oplog.oplog_timestamps(config.oplog_dump)

  unless timestamps.increasing?
    raise "Something went wrong - oplog is not ordered."
  end

  first = timestamps[0]
  last = timestamps[-1]

  if first > start_at
    raise "Expected first oplog entry to be #{start_at.inspect} but was #{first.inspect}\n" +
      "The oplog is probably too small.\n" +
      "Increase the oplog size, the start with another full backup."
  elsif first < start_at
    raise "Expected first oplog entry to be #{start_at.inspect} but was #{first.inspect}\n" +
      "Something went wrong in our query."
  end

  result = {
    entries: timestamps.count,
    first: first,
    position: last
  }

  if timestamps.count == 1
    result[:empty] = true
  else
    outfile = "oplog-#{first}-#{last}.bson"
    full_path = File.join(config.backup_dir, backup, outfile)
    FileUtils.mkdir_p File.join(config.backup_dir, backup)
    FileUtils.mv config.oplog_dump, full_path

    result[:file] = full_path
    result[:empty] = false
  end

  FileUtils.rm_r config.oplog_dump_folder rescue nil
  result
end

#latest_oplog_timestampObject



83
84
85
86
87
88
89
90
91
92
93
# File 'lib/mongo_oplog_backup/backup.rb', line 83

def latest_oplog_timestamp
  script = File.expand_path('../../oplog-last-timestamp.js', File.dirname(__FILE__))
  result_text = config.mongo('admin', script)
  begin
    response = JSON.parse(result_text)
    return nil unless response['position']
    BSON::Timestamp.from_json(response['position'])
  rescue JSON::ParserError => e
    raise StandardError, "Failed to connect to MongoDB: #{result_text}"
  end
end

#latest_oplog_timestamp_mopedObject



166
167
168
169
170
171
172
173
174
175
176
177
178
# File 'lib/mongo_oplog_backup/backup.rb', line 166

def latest_oplog_timestamp_moped
  # Alternative implementation for `latest_oplog_timestamp`
  require 'moped'
  session = Moped::Session.new([ "127.0.0.1:27017" ])
  session.use 'local'
  oplog = session['oplog.rs']
  entry = oplog.find.limit(1).sort('$natural' => -1).one
  if entry
    entry['ts']
  else
    nil
  end
end

#lock(lockname, &block) ⇒ Object



16
17
18
19
20
21
22
23
24
# File 'lib/mongo_oplog_backup/backup.rb', line 16

def lock(lockname, &block)
  File.open(lockname, File::RDWR|File::CREAT, 0644) do |file|
    got_lock = file.flock(File::LOCK_EX|File::LOCK_NB)
    if got_lock == false
      raise LockError, "Failed to acquire lock - another backup may be busy"
    end
    yield
  end
end

#perform(mode = :auto, options = {}) ⇒ Object



107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
# File 'lib/mongo_oplog_backup/backup.rb', line 107

def perform(mode=:auto, options={})
  if_not_busy = options[:if_not_busy] || false

  perform_oplog_afterwards = false

  FileUtils.mkdir_p config.backup_dir
  lock(config.lock_file) do
    state_file = config.state_file
    state = JSON.parse(File.read(state_file)) rescue nil
    state ||= {}
    have_position = (state['position'] && state['backup'])

    if mode == :auto
      if have_position
        mode = :oplog
      else
        mode = :full
      end
    end

    if mode == :oplog
      raise "Unknown backup position - cannot perform oplog backup." unless have_position
      MongoOplogBackup.log.info "Performing incremental oplog backup"
      position = BSON::Timestamp.from_json(state['position'])
      result = backup_oplog(start: position, backup: state['backup'])
      unless result[:empty]
        new_entries = result[:entries] - 1
        state['position'] = result[:position]
        File.write(state_file, state.to_json)
        MongoOplogBackup.log.info "Backed up #{new_entries} new entries to #{result[:file]}"
      else
        MongoOplogBackup.log.info "Nothing new to backup"
      end
    elsif mode == :full
      MongoOplogBackup.log.info "Performing full backup"
      result = backup_full
      state = result
      File.write(state_file, state.to_json)
      MongoOplogBackup.log.info "Performed full backup"

      perform_oplog_afterwards = true
    end
  end

  # Has to be outside the lock
  if perform_oplog_afterwards
    # Oplog backup
    perform(:oplog, options)
  end

rescue LockError => e
  if if_not_busy
    MongoOplogBackup.log.info e.message
    MongoOplogBackup.log.info 'Not performing backup'
  else
    raise
  end
end