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
|
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::Database::Migrations::TestBackgroundRunner, :redis do
include Gitlab::Database::Migrations::BackgroundMigrationHelpers
# In order to test the interaction between queueing sidekiq jobs and seeing those jobs in queues,
# we need to disable sidekiq's testing mode and actually send our jobs to redis
around do |ex|
Sidekiq::Testing.disable! { ex.run }
end
let(:result_dir) { Dir.mktmpdir }
after do
FileUtils.rm_rf(result_dir)
end
context 'without jobs to run' do
it 'returns immediately' do
runner = described_class.new(result_dir: result_dir)
expect(runner).not_to receive(:run_job)
described_class.new(result_dir: result_dir).run_jobs(for_duration: 1.second)
end
end
context 'with jobs to run' do
let(:migration_name) { 'TestBackgroundMigration' }
before do
(1..5).each do |i|
migrate_in(i.minutes, migration_name, [i])
end
end
context 'finding pending background jobs' do
it 'finds all the migrations' do
expect(described_class.new(result_dir: result_dir).traditional_background_migrations.to_a.size).to eq(5)
end
end
context 'running migrations', :freeze_time do
def define_background_migration(name)
klass = Class.new do
# Can't simply def perform here as we won't have access to the block,
# similarly can't define_method(:perform, &block) here as it would change the block receiver
define_method(:perform) { |*args| yield(*args) }
end
stub_const("Gitlab::BackgroundMigration::#{name}", klass)
klass
end
def expect_migration_call_counts(migrations_to_calls)
migrations_to_calls.each do |migration, calls|
expect_next_instances_of(migration, calls) do |m|
expect(m).to receive(:perform).and_call_original
end
end
end
def expect_recorded_migration_runs(migrations_to_runs)
migrations_to_runs.each do |migration, runs|
path = File.join(result_dir, migration.name.demodulize)
num_subdirs = Pathname(path).children.count(&:directory?)
expect(num_subdirs).to eq(runs)
end
end
def expect_migration_runs(migrations_to_run_counts)
expect_migration_call_counts(migrations_to_run_counts)
yield
expect_recorded_migration_runs(migrations_to_run_counts)
end
it 'runs the migration class correctly' do
calls = []
define_background_migration(migration_name) do |i|
calls << i
end
described_class.new(result_dir: result_dir).run_jobs(for_duration: 1.second) # Any time would work here as we do not advance time
expect(calls).to contain_exactly(1, 2, 3, 4, 5)
end
it 'runs the migration for a uniform amount of time' do
migration = define_background_migration(migration_name) do |i|
travel(1.minute)
end
expect_migration_runs(migration => 3) do
described_class.new(result_dir: result_dir).run_jobs(for_duration: 3.minutes)
end
end
context 'with multiple migrations to run' do
let(:other_migration_name) { 'OtherBackgroundMigration' }
before do
(1..5).each do |i|
migrate_in(i.minutes, other_migration_name, [i])
end
end
it 'splits the time between migrations when all migrations use all their time' do
migration = define_background_migration(migration_name) do |i|
travel(1.minute)
end
other_migration = define_background_migration(other_migration_name) do |i|
travel(2.minutes)
end
expect_migration_runs(
migration => 2, # 1 minute jobs for 90 seconds, can finish the first and start the second
other_migration => 1 # 2 minute jobs for 90 seconds, past deadline after a single job
) do
described_class.new(result_dir: result_dir).run_jobs(for_duration: 3.minutes)
end
end
it 'does not give leftover time to extra migrations' do
# This is currently implemented this way for simplicity, but it could make sense to change this behavior.
migration = define_background_migration(migration_name) do
travel(1.second)
end
other_migration = define_background_migration(other_migration_name) do
travel(1.minute)
end
expect_migration_runs(
migration => 5,
other_migration => 2
) do
described_class.new(result_dir: result_dir).run_jobs(for_duration: 3.minutes)
end
end
end
end
end
end
|