Welcome to mirror list, hosted at ThFree Co, Russian Federation.

database.rb « backup « lib - gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
blob: f70a7e4186267742b0cccf12210d503e36092243 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
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
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
# frozen_string_literal: true

require 'yaml'

module Backup
  class Database < Task
    extend ::Gitlab::Utils::Override
    include Backup::Helper
    attr_reader :force

    IGNORED_ERRORS = [
      # Ignore warnings
      /WARNING:/,
      # Ignore the DROP errors; recent database dumps will use --if-exists with pg_dump
      /does not exist$/,
      # User may not have permissions to drop extensions or schemas
      /must be owner of/
    ].freeze
    IGNORED_ERRORS_REGEXP = Regexp.union(IGNORED_ERRORS).freeze

    def initialize(progress, force:)
      super(progress)
      @force = force
    end

    override :dump
    def dump(destination_dir, backup_id)
      FileUtils.mkdir_p(destination_dir)

      each_database(destination_dir) do |database_name, current_db|
        model = current_db[:model]
        snapshot_id = current_db[:snapshot_id]

        pg_env = model.config[:pg_env]
        connection = model.connection
        active_record_config = model.config[:activerecord]
        pg_database = active_record_config[:database]

        db_file_name = file_name(destination_dir, database_name)
        FileUtils.rm_f(db_file_name)

        progress.print "Dumping PostgreSQL database #{pg_database} ... "

        pgsql_args = ["--clean"] # Pass '--clean' to include 'DROP TABLE' statements in the DB dump.
        pgsql_args << '--if-exists'
        pgsql_args << "--snapshot=#{snapshot_id}" if snapshot_id

        if Gitlab.config.backup.pg_schema
          pgsql_args << '-n'
          pgsql_args << Gitlab.config.backup.pg_schema

          Gitlab::Database::EXTRA_SCHEMAS.each do |schema|
            pgsql_args << '-n'
            pgsql_args << schema.to_s
          end
        end

        success = with_transient_pg_env(pg_env) do
          Backup::Dump::Postgres.new.dump(pg_database, db_file_name, pgsql_args)
        end

        connection.rollback_transaction if snapshot_id

        raise DatabaseBackupError.new(active_record_config, db_file_name) unless success

        report_success(success)
        progress.flush
      end
    ensure
      ::Gitlab::Database::EachDatabase.each_connection(
        only: base_models_for_backup.keys, include_shared: false
      ) do |connection, _|
        Gitlab::Database::TransactionTimeoutSettings.new(connection).restore_timeouts
      end
    end

    override :restore
    def restore(destination_dir)
      base_models_for_backup.each do |database_name, _base_model|
        backup_model = Backup::DatabaseModel.new(database_name)

        config = backup_model.config[:activerecord]

        db_file_name = file_name(destination_dir, database_name)
        database = config[:database]

        unless File.exist?(db_file_name)
          raise(Backup::Error, "Source database file does not exist #{db_file_name}") if main_database?(database_name)

          progress.puts "Source backup for the database #{database_name} doesn't exist. Skipping the task"
          return false
        end

        unless force
          progress.puts 'Removing all tables. Press `Ctrl-C` within 5 seconds to abort'.color(:yellow)
          sleep(5)
        end

        # Drop all tables Load the schema to ensure we don't have any newer tables
        # hanging out from a failed upgrade
        drop_tables(database_name)

        pg_env = backup_model.config[:pg_env]
        success = with_transient_pg_env(pg_env) do
          decompress_rd, decompress_wr = IO.pipe
          decompress_pid = spawn(*%w[gzip -cd], out: decompress_wr, in: db_file_name)
          decompress_wr.close

          status, @errors =
            case config[:adapter]
            when "postgresql" then
              progress.print "Restoring PostgreSQL database #{database} ... "
              execute_and_track_errors(pg_restore_cmd(database), decompress_rd)
            end
          decompress_rd.close

          Process.waitpid(decompress_pid)
          $?.success? && status.success?
        end

        if @errors.present?
          progress.print "------ BEGIN ERRORS -----\n".color(:yellow)
          progress.print @errors.join.color(:yellow)
          progress.print "------ END ERRORS -------\n".color(:yellow)
        end

        report_success(success)
        raise Backup::Error, 'Restore failed' unless success
      end
    end

    override :pre_restore_warning
    def pre_restore_warning
      return if force

      <<-MSG.strip_heredoc
        Be sure to stop Puma, Sidekiq, and any other process that
        connects to the database before proceeding. For Omnibus
        installs, see the following link for more information:
        https://docs.gitlab.com/ee/raketasks/backup_restore.html#restore-for-omnibus-gitlab-installations

        Before restoring the database, we will remove all existing
        tables to avoid future upgrade problems. Be aware that if you have
        custom tables in the GitLab database these tables and all data will be
        removed.
      MSG
    end

    override :post_restore_warning
    def post_restore_warning
      return unless @errors.present?

      <<-MSG.strip_heredoc
        There were errors in restoring the schema. This may cause
        issues if this results in missing indexes, constraints, or
        columns. Please record the errors above and contact GitLab
        Support if you have questions:
        https://about.gitlab.com/support/
      MSG
    end

    protected

    def base_models_for_backup
      @base_models_for_backup ||= Gitlab::Database.database_base_models_with_gitlab_shared
    end

    def main_database?(database_name)
      database_name.to_sym == :main
    end

    def file_name(base_dir, database_name)
      prefix = if database_name.to_sym != :main
                 "#{database_name}_"
               else
                 ''
               end

      File.join(base_dir, "#{prefix}database.sql.gz")
    end

    def ignore_error?(line)
      IGNORED_ERRORS_REGEXP.match?(line)
    end

    def execute_and_track_errors(cmd, decompress_rd)
      errors = []

      Open3.popen3(ENV, *cmd) do |stdin, stdout, stderr, thread|
        stdin.binmode

        out_reader = Thread.new do
          data = stdout.read
          $stdout.write(data)
        end

        err_reader = Thread.new do
          until (raw_line = stderr.gets).nil?
            warn(raw_line)
            errors << raw_line unless ignore_error?(raw_line)
          end
        end

        begin
          IO.copy_stream(decompress_rd, stdin)
        rescue Errno::EPIPE
        end

        stdin.close
        [thread, out_reader, err_reader].each(&:join)
        [thread.value, errors]
      end
    end

    def report_success(success)
      if success
        progress.puts '[DONE]'.color(:green)
      else
        progress.puts '[FAILED]'.color(:red)
      end
    end

    private

    def drop_tables(database_name)
      puts_time 'Cleaning the database ... '.color(:blue)

      if Rake::Task.task_defined? "gitlab:db:drop_tables:#{database_name}"
        Rake::Task["gitlab:db:drop_tables:#{database_name}"].invoke
      else
        # In single database (single or two connections)
        Rake::Task["gitlab:db:drop_tables"].invoke
      end

      puts_time 'done'.color(:green)
    end

    def with_transient_pg_env(extended_env)
      ENV.merge!(extended_env)
      result = yield
      ENV.reject! { |k, _| extended_env.key?(k) }

      result
    end

    def pg_restore_cmd(database)
      ['psql', database]
    end

    def each_database(destination_dir, &block)
      databases = {}
      ::Gitlab::Database::EachDatabase.each_connection(
        only: base_models_for_backup.keys, include_shared: false
      ) do |_connection, name|
        next if databases[name]

        backup_model = Backup::DatabaseModel.new(name)

        databases[name] = {
          model: backup_model
        }

        next unless Gitlab::Database.database_mode == Gitlab::Database::MODE_MULTIPLE_DATABASES

        connection = backup_model.connection

        begin
          Gitlab::Database::TransactionTimeoutSettings.new(connection).disable_timeouts
          connection.begin_transaction(isolation: :repeatable_read)
          databases[name][:snapshot_id] = connection.select_value("SELECT pg_export_snapshot()")
        rescue ActiveRecord::ConnectionNotEstablished
          raise Backup::DatabaseBackupError.new(backup_model.config[:activerecord], file_name(destination_dir, name))
        end
      end

      databases.each(&block)
    end
  end
end