/
checker.rb
209 lines (181 loc) · 5.62 KB
/
checker.rb
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
module StrongMigrations
class Checker
include Checks
include SafeMethods
attr_accessor :direction, :transaction_disabled, :timeouts_set
class << self
attr_accessor :safe
end
def initialize(migration)
@migration = migration
@new_tables = []
@new_columns = []
@timeouts_set = false
@committed = false
end
def self.safety_assured
previous_value = safe
begin
self.safe = true
yield
ensure
self.safe = previous_value
end
end
def perform(method, *args)
check_version_supported
set_timeouts
check_lock_timeout
if !safe? || safe_by_default_method?(method)
# TODO better pattern
# see checks.rb for methods
case method
when :add_check_constraint
check_add_check_constraint(*args)
when :add_column
check_add_column(*args)
when :add_exclusion_constraint
check_add_exclusion_constraint(*args)
when :add_foreign_key
check_add_foreign_key(*args)
when :add_index
check_add_index(*args)
when :add_reference, :add_belongs_to
check_add_reference(method, *args)
when :change_column
check_change_column(*args)
when :change_column_default
check_change_column_default(*args)
when :change_column_null
check_change_column_null(*args)
when :change_table
check_change_table
when :create_join_table
check_create_join_table(*args)
when :create_table
check_create_table(*args)
when :execute
check_execute
when :remove_column, :remove_columns, :remove_timestamps, :remove_reference, :remove_belongs_to
check_remove_column(method, *args)
when :remove_index
check_remove_index(*args)
when :rename_column
check_rename_column
when :rename_table
check_rename_table
when :validate_check_constraint
check_validate_check_constraint
when :validate_foreign_key
check_validate_foreign_key
when :commit_db_transaction
# if committed, likely no longer in DDL transaction
# and no longer eligible to be retried at migration level
# okay to have false positives
@committed = true
end
if !safe?
# custom checks
StrongMigrations.checks.each do |check|
@migration.instance_exec(method, args, &check)
end
end
end
result =
if retry_lock_timeouts?(method)
# TODO figure out how to handle methods that generate multiple statements
# like add_reference(table, ref, index: {algorithm: :concurrently})
# lock timeout after first statement will cause retry to fail
retry_lock_timeouts { yield }
else
yield
end
# outdated statistics + a new index can hurt performance of existing queries
if StrongMigrations.auto_analyze && direction == :up && method == :add_index
adapter.analyze_table(args[0])
end
result
end
def retry_lock_timeouts(check_committed: false)
retries = 0
begin
yield
rescue ActiveRecord::LockWaitTimeout => e
if retries < StrongMigrations.lock_timeout_retries && !(check_committed && @committed)
retries += 1
delay = StrongMigrations.lock_timeout_retry_delay
@migration.say("Lock timeout. Retrying in #{delay} seconds...")
sleep(delay)
retry
end
raise e
end
end
def version_safe?
version && version <= StrongMigrations.start_after
end
private
def check_version_supported
return if defined?(@version_checked)
min_version = adapter.min_version
if min_version
version = adapter.server_version
if version < Gem::Version.new(min_version)
raise UnsupportedVersion, "#{adapter.name} version (#{version}) not supported in this version of Strong Migrations (#{StrongMigrations::VERSION})"
end
end
@version_checked = true
end
def set_timeouts
return if @timeouts_set
if StrongMigrations.statement_timeout
adapter.set_statement_timeout(StrongMigrations.statement_timeout)
end
if StrongMigrations.lock_timeout
adapter.set_lock_timeout(StrongMigrations.lock_timeout)
end
@timeouts_set = true
end
def check_lock_timeout
return if defined?(@lock_timeout_checked)
if StrongMigrations.lock_timeout_limit
adapter.check_lock_timeout(StrongMigrations.lock_timeout_limit)
end
@lock_timeout_checked = true
end
def safe?
self.class.safe || ENV["SAFETY_ASSURED"] || (direction == :down && !StrongMigrations.check_down) || version_safe? || @migration.reverting?
end
def version
@migration.version
end
def adapter
@adapter ||= begin
cls =
case connection.adapter_name
when /postg/i # PostgreSQL, PostGIS
Adapters::PostgreSQLAdapter
when /mysql|trilogy/i
if connection.try(:mariadb?)
Adapters::MariaDBAdapter
else
Adapters::MySQLAdapter
end
else
Adapters::AbstractAdapter
end
cls.new(self)
end
end
def connection
@migration.connection
end
def retry_lock_timeouts?(method)
(
StrongMigrations.lock_timeout_retries > 0 &&
!in_transaction? &&
method != :transaction
)
end
end
end