nandi 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +483 -0
  3. data/Rakefile +8 -0
  4. data/exe/nandi-enforce +36 -0
  5. data/lib/generators/nandi/check_constraint/USAGE +19 -0
  6. data/lib/generators/nandi/check_constraint/check_constraint_generator.rb +52 -0
  7. data/lib/generators/nandi/check_constraint/templates/add_check_constraint.rb +15 -0
  8. data/lib/generators/nandi/check_constraint/templates/validate_check_constraint.rb +9 -0
  9. data/lib/generators/nandi/compile/USAGE +19 -0
  10. data/lib/generators/nandi/compile/compile_generator.rb +62 -0
  11. data/lib/generators/nandi/foreign_key/USAGE +47 -0
  12. data/lib/generators/nandi/foreign_key/foreign_key_generator.rb +91 -0
  13. data/lib/generators/nandi/foreign_key/templates/add_foreign_key.rb +13 -0
  14. data/lib/generators/nandi/foreign_key/templates/add_reference.rb +11 -0
  15. data/lib/generators/nandi/foreign_key/templates/validate_foreign_key.rb +9 -0
  16. data/lib/generators/nandi/migration/USAGE +9 -0
  17. data/lib/generators/nandi/migration/migration_generator.rb +24 -0
  18. data/lib/generators/nandi/migration/templates/migration.rb +13 -0
  19. data/lib/generators/nandi/not_null_check/USAGE +19 -0
  20. data/lib/generators/nandi/not_null_check/not_null_check_generator.rb +56 -0
  21. data/lib/generators/nandi/not_null_check/templates/add_not_null_check.rb +11 -0
  22. data/lib/generators/nandi/not_null_check/templates/validate_not_null_check.rb +9 -0
  23. data/lib/nandi.rb +35 -0
  24. data/lib/nandi/compiled_migration.rb +86 -0
  25. data/lib/nandi/config.rb +126 -0
  26. data/lib/nandi/file_diff.rb +32 -0
  27. data/lib/nandi/file_matcher.rb +72 -0
  28. data/lib/nandi/formatting.rb +79 -0
  29. data/lib/nandi/instructions.rb +21 -0
  30. data/lib/nandi/instructions/add_check_constraint.rb +23 -0
  31. data/lib/nandi/instructions/add_column.rb +24 -0
  32. data/lib/nandi/instructions/add_foreign_key.rb +40 -0
  33. data/lib/nandi/instructions/add_index.rb +50 -0
  34. data/lib/nandi/instructions/add_reference.rb +23 -0
  35. data/lib/nandi/instructions/change_column_default.rb +23 -0
  36. data/lib/nandi/instructions/create_table.rb +83 -0
  37. data/lib/nandi/instructions/drop_constraint.rb +22 -0
  38. data/lib/nandi/instructions/drop_table.rb +21 -0
  39. data/lib/nandi/instructions/irreversible_migration.rb +15 -0
  40. data/lib/nandi/instructions/remove_column.rb +23 -0
  41. data/lib/nandi/instructions/remove_index.rb +41 -0
  42. data/lib/nandi/instructions/remove_not_null_constraint.rb +22 -0
  43. data/lib/nandi/instructions/remove_reference.rb +23 -0
  44. data/lib/nandi/instructions/validate_constraint.rb +22 -0
  45. data/lib/nandi/lockfile.rb +58 -0
  46. data/lib/nandi/migration.rb +388 -0
  47. data/lib/nandi/renderers.rb +7 -0
  48. data/lib/nandi/renderers/active_record.rb +13 -0
  49. data/lib/nandi/renderers/active_record/generate.rb +59 -0
  50. data/lib/nandi/renderers/active_record/instructions.rb +146 -0
  51. data/lib/nandi/safe_migration_enforcer.rb +143 -0
  52. data/lib/nandi/timeout_policies.rb +38 -0
  53. data/lib/nandi/timeout_policies/access_exclusive.rb +54 -0
  54. data/lib/nandi/timeout_policies/concurrent.rb +64 -0
  55. data/lib/nandi/validation.rb +11 -0
  56. data/lib/nandi/validation/add_column_validator.rb +43 -0
  57. data/lib/nandi/validation/add_reference_validator.rb +38 -0
  58. data/lib/nandi/validation/each_validator.rb +34 -0
  59. data/lib/nandi/validation/failure_helpers.rb +35 -0
  60. data/lib/nandi/validation/remove_index_validator.rb +30 -0
  61. data/lib/nandi/validation/result.rb +30 -0
  62. data/lib/nandi/validation/timeout_validator.rb +37 -0
  63. data/lib/nandi/validator.rb +102 -0
  64. data/lib/templates/nandi/renderers/active_record/generate/show.rb.erb +27 -0
  65. data/lib/templates/nandi/renderers/active_record/instructions/add_check_constraint/show.rb.erb +7 -0
  66. data/lib/templates/nandi/renderers/active_record/instructions/add_column/show.rb.erb +6 -0
  67. data/lib/templates/nandi/renderers/active_record/instructions/add_foreign_key/show.rb.erb +5 -0
  68. data/lib/templates/nandi/renderers/active_record/instructions/add_index/show.rb.erb +5 -0
  69. data/lib/templates/nandi/renderers/active_record/instructions/add_reference/show.rb.erb +5 -0
  70. data/lib/templates/nandi/renderers/active_record/instructions/change_column_default/show.rb.erb +1 -0
  71. data/lib/templates/nandi/renderers/active_record/instructions/create_table/show.rb.erb +8 -0
  72. data/lib/templates/nandi/renderers/active_record/instructions/drop_constraint/show.rb.erb +4 -0
  73. data/lib/templates/nandi/renderers/active_record/instructions/drop_table/show.rb.erb +1 -0
  74. data/lib/templates/nandi/renderers/active_record/instructions/irreversible_migration/show.rb.erb +1 -0
  75. data/lib/templates/nandi/renderers/active_record/instructions/remove_column/show.rb.erb +5 -0
  76. data/lib/templates/nandi/renderers/active_record/instructions/remove_index/show.rb.erb +4 -0
  77. data/lib/templates/nandi/renderers/active_record/instructions/remove_not_null_constraint/show.rb.erb +1 -0
  78. data/lib/templates/nandi/renderers/active_record/instructions/remove_reference/show.rb.erb +5 -0
  79. data/lib/templates/nandi/renderers/active_record/instructions/validate_constraint/show.rb.erb +3 -0
  80. metadata +317 -0
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nandi
4
+ module Instructions
5
+ class RemoveIndex
6
+ def initialize(table:, field:)
7
+ @table = table
8
+ @field = field
9
+ end
10
+
11
+ def procedure
12
+ :remove_index
13
+ end
14
+
15
+ def extra_args
16
+ if field.is_a?(Hash)
17
+ field.merge(algorithm: :concurrently)
18
+ else
19
+ { column: columns, algorithm: :concurrently }
20
+ end
21
+ end
22
+
23
+ def lock
24
+ Nandi::Migration::LockWeights::SHARE
25
+ end
26
+
27
+ attr_reader :table
28
+
29
+ private
30
+
31
+ attr_reader :field
32
+
33
+ def columns
34
+ columns = Array(field)
35
+ columns = columns.first if columns.one?
36
+
37
+ columns
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nandi
4
+ module Instructions
5
+ class RemoveNotNullConstraint
6
+ attr_reader :table, :column
7
+
8
+ def initialize(table:, column:)
9
+ @table = table
10
+ @column = column
11
+ end
12
+
13
+ def procedure
14
+ :remove_not_null_constraint
15
+ end
16
+
17
+ def lock
18
+ Nandi::Migration::LockWeights::ACCESS_EXCLUSIVE
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nandi
4
+ module Instructions
5
+ class RemoveReference
6
+ attr_reader :table, :ref_name, :extra_args
7
+
8
+ def initialize(table:, ref_name:, **kwargs)
9
+ @table = table
10
+ @ref_name = ref_name
11
+ @extra_args = kwargs
12
+ end
13
+
14
+ def procedure
15
+ :remove_reference
16
+ end
17
+
18
+ def lock
19
+ Nandi::Migration::LockWeights::ACCESS_EXCLUSIVE
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nandi
4
+ module Instructions
5
+ class ValidateConstraint
6
+ attr_reader :table, :name
7
+
8
+ def initialize(table:, name:)
9
+ @table = table
10
+ @name = name
11
+ end
12
+
13
+ def lock
14
+ Nandi::Migration::LockWeights::SHARE
15
+ end
16
+
17
+ def procedure
18
+ :validate_constraint
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/hash/indifferent_access"
4
+
5
+ module Nandi
6
+ class Lockfile
7
+ class << self
8
+ def file_present?
9
+ File.exist?(path)
10
+ end
11
+
12
+ def create!
13
+ return if file_present?
14
+
15
+ File.write(path, {}.to_yaml)
16
+ end
17
+
18
+ def add(file_name:, source_digest:, compiled_digest:)
19
+ load!
20
+
21
+ lockfile[file_name] = {
22
+ source_digest: source_digest,
23
+ compiled_digest: compiled_digest,
24
+ }
25
+ end
26
+
27
+ def get(file_name)
28
+ load!
29
+
30
+ {
31
+ source_digest: lockfile.dig(file_name, :source_digest),
32
+ compiled_digest: lockfile.dig(file_name, :compiled_digest),
33
+ }
34
+ end
35
+
36
+ def load!
37
+ return lockfile if lockfile
38
+
39
+ Nandi::Lockfile.create! unless Nandi::Lockfile.file_present?
40
+
41
+ @lockfile = YAML.safe_load(File.read(path)).with_indifferent_access
42
+ end
43
+
44
+ def persist!
45
+ File.write(path, lockfile.to_h.deep_stringify_keys.to_yaml)
46
+ end
47
+
48
+ def path
49
+ File.join(
50
+ Nandi.config.lockfile_directory,
51
+ ".nandilock.yml",
52
+ )
53
+ end
54
+
55
+ attr_accessor :lockfile
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,388 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "nandi/instructions"
4
+ require "nandi/validator"
5
+ require "nandi/validation/failure_helpers"
6
+
7
+ module Nandi
8
+ # @abstract A migration must implement #up (the forward migration), and may
9
+ # also implement #down (the rollback sequence).
10
+ # The base class for migrations; Nandi's equivalent of ActiveRecord::Migration.
11
+ # All the statements in the migration are statically analysed together to rule
12
+ # out migrations with a high risk of causing availability issues. Additionally,
13
+ # our implementations of some statements will rule out certain common footguns
14
+ # (for example, creating an index without using the `CONCURRENTLY` parameter.)
15
+ # @example
16
+ # class CreateWidgetsTable < Nandi::Migration
17
+ # def up
18
+ # create_table :widgets do |t|
19
+ # t.column :weight, :number
20
+ # t.column :name, :text, default: "Unknown widget"
21
+ # end
22
+ # end
23
+ #
24
+ # def down
25
+ # drop_table :widgets
26
+ # end
27
+ # end
28
+ class Migration
29
+ include Nandi::Validation::FailureHelpers
30
+
31
+ module LockWeights
32
+ ACCESS_EXCLUSIVE = 1
33
+ SHARE = 0
34
+ end
35
+
36
+ class InstructionSet < SimpleDelegator
37
+ def strictest_lock
38
+ return LockWeights::SHARE if empty?
39
+
40
+ map { |i| i.respond_to?(:lock) ? i.lock : LockWeights::ACCESS_EXCLUSIVE }.max
41
+ end
42
+ end
43
+
44
+ class << self
45
+ attr_reader :lock_timeout, :statement_timeout
46
+ # For sake both of correspondence with Postgres syntax and familiarity
47
+ # with activerecord-safe_migrations's identically named macros, we
48
+ # disable this cop.
49
+
50
+ # rubocop:disable Naming/AccessorMethodName
51
+
52
+ # Override the default lock timeout for the duration of the migration.
53
+ # This may be helpful when making changes to very busy tables, when a
54
+ # lock is less likely to be immediately available.
55
+ # @param timeout [Integer] New lock timeout in ms
56
+ def set_lock_timeout(timeout)
57
+ @lock_timeout = timeout
58
+ end
59
+
60
+ # Override the default statement timeout for the duration of the migration.
61
+ # This may be helpful when making changes that are likely to take a lot
62
+ # of time, like adding a new index on a large table.
63
+ # @param timeout [Integer] New lock timeout in ms
64
+ def set_statement_timeout(timeout)
65
+ @statement_timeout = timeout
66
+ end
67
+ # rubocop:enable Naming/AccessorMethodName
68
+ end
69
+
70
+ # @param validator [Nandi::Validator]
71
+ def initialize(validator)
72
+ @validator = validator
73
+ @instructions = Hash.new { |h, k| h[k] = InstructionSet.new([]) }
74
+ validate
75
+ end
76
+
77
+ # @api private
78
+ def up_instructions
79
+ compile_instructions(:up)
80
+ end
81
+
82
+ # @api private
83
+ def down_instructions
84
+ compile_instructions(:down)
85
+ end
86
+
87
+ # The current lock timeout.
88
+ def lock_timeout
89
+ self.class.lock_timeout || default_lock_timeout
90
+ end
91
+
92
+ # The current statement timeout.
93
+ def statement_timeout
94
+ self.class.statement_timeout || default_statement_timeout
95
+ end
96
+
97
+ # @api private
98
+ def strictest_lock
99
+ @instructions.values.map(&:strictest_lock).max
100
+ end
101
+
102
+ # @abstract
103
+ def up
104
+ raise NotImplementedError
105
+ end
106
+
107
+ def down; end
108
+
109
+ # Adds a new index to the database.
110
+ #
111
+ # Nandi will:
112
+ # * add the `CONCURRENTLY` option, which means the change takes a less
113
+ # restrictive lock at the cost of not running in a DDL transaction
114
+ # * use the `BTREE` index type which is the safest to create.
115
+ #
116
+ # Because index creation is particularly failure-prone, and because
117
+ # we cannot run in a transaction and therefore risk partially applied
118
+ # migrations that (in a Rails environment) require manual intervention,
119
+ # Nandi Validates that, if there is a add_index statement in the
120
+ # migration, it must be the only statement.
121
+ # @param table [Symbol, String] The name of the table to add the index to
122
+ # @param fields [Symbol, String, Array] The field or fields to use in the
123
+ # index
124
+ # @param kwargs [Hash] Arbitrary options to pass to the backend adapter.
125
+ # Attempts to remove `CONCURRENTLY` or change the index type will be ignored.
126
+ def add_index(table, fields, **kwargs)
127
+ current_instructions << Instructions::AddIndex.new(
128
+ **kwargs,
129
+ table: table,
130
+ fields: fields,
131
+ )
132
+ end
133
+
134
+ # Drop an index from the database.
135
+ #
136
+ # Nandi will add the `CONCURRENTLY` option, which means the change
137
+ # takes a less restrictive lock at the cost of not running in a DDL
138
+ # transaction.
139
+ #
140
+ # Because we cannot run in a transaction and therefore risk partially
141
+ # applied migrations that (in a Rails environment) require manual
142
+ # intervention, Nandi Validates that, if there is a remove_index statement
143
+ # in the migration, it must be the only statement.
144
+ # @param table [Symbol, String] The name of the table to add the index to
145
+ # @param target [Symbol, String, Array, Hash] This can be either the field (or
146
+ # array of fields) in the index to be dropped, or a hash of options, which
147
+ # must include either a `column` key (which is the same: a field or list
148
+ # of fields) or a `name` key, which is the name of the index to be dropped.
149
+ def remove_index(table, target)
150
+ current_instructions << Instructions::RemoveIndex.new(table: table, field: target)
151
+ end
152
+
153
+ # Creates a new table. Yields a ColumnsReader object as a block, to allow adding
154
+ # columns.
155
+ # @example
156
+ # create_table :widgets do |t|
157
+ # t.text :foo, default: true
158
+ # end
159
+ # @param table [String, Symbol] The name of the new table
160
+ # @yieldparam columns_reader [Nandi::Instructions::CreateTable::ColumnsReader]
161
+ def create_table(table, **kwargs, &block)
162
+ current_instructions << Instructions::CreateTable.new(
163
+ **kwargs,
164
+ table: table,
165
+ columns_block: block,
166
+ )
167
+ end
168
+
169
+ # Drops an existing table
170
+ # @param table [String, Symbol] The name of the table to drop.
171
+ def drop_table(table)
172
+ current_instructions << Instructions::DropTable.new(table: table)
173
+ end
174
+
175
+ # Adds a new column. Nandi will explicitly set the column to be NULL,
176
+ # as validating a new NOT NULL constraint can be very expensive on large
177
+ # tables and cause availability issues.
178
+ # @param table [Symbol, String] The name of the table to add the column to
179
+ # @param name [Symbol, String] The name of the column
180
+ # @param type [Symbol, String] The type of the column
181
+ # @param kwargs [Hash] Arbitrary options to be passed to the backend.
182
+ def add_column(table, name, type, **kwargs)
183
+ current_instructions << Instructions::AddColumn.new(
184
+ table: table,
185
+ name: name,
186
+ type: type,
187
+ **kwargs,
188
+ )
189
+ end
190
+
191
+ # Adds a new reference column. Nandi will validate that the foreign key flag
192
+ # is not set to true; use `add_foreign_key` and `validate_foreign_key` instead!
193
+ # @param table [Symbol, String] The name of the table to add the column to
194
+ # @param ref_name [Symbol, String] The referenced column name
195
+ # @param kwargs [Hash] Arbitrary options to be passed to the backend.
196
+ def add_reference(table, ref_name, **kwargs)
197
+ current_instructions << Instructions::AddReference.new(
198
+ table: table,
199
+ ref_name: ref_name,
200
+ **kwargs,
201
+ )
202
+ end
203
+
204
+ # Removes a reference column.
205
+ # @param table [Symbol, String] The name of the table to remove the reference from
206
+ # @param ref_name [Symbol, String] The referenced column name
207
+ # @param kwargs [Hash] Arbitrary options to be passed to the backend.
208
+ def remove_reference(table, ref_name, **kwargs)
209
+ current_instructions << Instructions::RemoveReference.new(
210
+ table: table,
211
+ ref_name: ref_name,
212
+ **kwargs,
213
+ )
214
+ end
215
+
216
+ # Remove an existing column.
217
+ # @param table [Symbol, String] The name of the table to remove the column
218
+ # from.
219
+ # @param name [Symbol, String] The name of the column
220
+ # @param extra_args [Hash] Arbitrary options to be passed to the backend.
221
+ def remove_column(table, name, **extra_args)
222
+ current_instructions << Instructions::RemoveColumn.new(
223
+ **extra_args,
224
+ table: table,
225
+ name: name,
226
+ )
227
+ end
228
+
229
+ # Add a foreign key constraint. The generated SQL will include the NOT VALID
230
+ # parameter, which will prevent immediate validation of the constraint, which
231
+ # locks the target table for writes potentially for a long time. Use the separate
232
+ # #validate_constraint method, in a separate migration; this only takes a row-level
233
+ # lock as it scans through.
234
+ # @param table [Symbol, String] The name of the table with the reference column
235
+ # @param target [Symbol, String] The name of the referenced table
236
+ # @param column [Symbol, String] The name of the reference column. If omitted, will
237
+ # default to the singular of target + "_id"
238
+ # @param name [Symbol, String] The name of the constraint to create. Defaults to
239
+ # table_target_fk
240
+ def add_foreign_key(table, target, column: nil, name: nil)
241
+ current_instructions << Instructions::AddForeignKey.new(
242
+ table: table,
243
+ target: target,
244
+ column: column,
245
+ name: name,
246
+ )
247
+ end
248
+
249
+ # Add a check constraint, in the NOT VALID state.
250
+ # @param table [Symbol, String] The name of the table with the column
251
+ # @param name [Symbol, String] The name of the constraint to create
252
+ # @param check [Symbol, String] The predicate to check
253
+ def add_check_constraint(table, name, check)
254
+ current_instructions << Instructions::AddCheckConstraint.new(
255
+ table: table,
256
+ name: name,
257
+ check: check,
258
+ )
259
+ end
260
+
261
+ # Validates an existing foreign key constraint.
262
+ # @param table [Symbol, String] The name of the table with the constraint
263
+ # @param name [Symbol, String] The name of the constraint
264
+ def validate_constraint(table, name)
265
+ current_instructions << Instructions::ValidateConstraint.new(
266
+ table: table,
267
+ name: name,
268
+ )
269
+ end
270
+
271
+ # Drops an existing constraint.
272
+ # @param table [Symbol, String] The name of the table with the constraint
273
+ # @param name [Symbol, String] The name of the constraint
274
+ def drop_constraint(table, name)
275
+ current_instructions << Instructions::DropConstraint.new(
276
+ table: table,
277
+ name: name,
278
+ )
279
+ end
280
+
281
+ # Drops an existing NOT NULL constraint. Please note that this migration is
282
+ # not safely reversible; to enforce NOT NULL like behaviour, use a CHECK
283
+ # constraint and validate it in a separate migration.
284
+ # @param table [Symbol, String] The name of the table with the constraint
285
+ # @param column [Symbol, String] The name of the column to remove NOT NULL
286
+ # constraint from
287
+ def remove_not_null_constraint(table, column)
288
+ current_instructions << Instructions::RemoveNotNullConstraint.new(
289
+ table: table,
290
+ column: column,
291
+ )
292
+ end
293
+
294
+ # Changes the default value for this column when new rows are inserted into
295
+ # the table.
296
+ # @param table [Symbol, String] The name of the table with the column
297
+ # @param column [Symbol, String] The name of the column to change
298
+ # @param value [Object] The new default value
299
+ def change_column_default(table, column, value)
300
+ current_instructions << Instructions::ChangeColumnDefault.new(
301
+ table: table,
302
+ column: column,
303
+ value: value,
304
+ )
305
+ end
306
+
307
+ # Raises an `ActiveRecord::IrreversibleMigration` error for use in
308
+ # irreversible migrations
309
+ def irreversible_migration
310
+ current_instructions << Instructions::IrreversibleMigration.new
311
+ end
312
+
313
+ # @api private
314
+ def compile_instructions(direction)
315
+ @direction = direction
316
+
317
+ public_send(direction) unless current_instructions.any?
318
+
319
+ current_instructions
320
+ end
321
+
322
+ # @api private
323
+ def validate
324
+ validator.call(self)
325
+ rescue NotImplementedError => e
326
+ Validation::Result.new << failure(e.message)
327
+ end
328
+
329
+ def disable_lock_timeout?
330
+ if self.class.lock_timeout.nil?
331
+ strictest_lock == LockWeights::SHARE
332
+ else
333
+ false
334
+ end
335
+ end
336
+
337
+ def disable_statement_timeout?
338
+ if self.class.statement_timeout.nil?
339
+ strictest_lock == LockWeights::SHARE
340
+ else
341
+ false
342
+ end
343
+ end
344
+
345
+ def name
346
+ self.class.name
347
+ end
348
+
349
+ def respond_to_missing?(name)
350
+ Nandi.config.custom_methods.key?(name) || super
351
+ end
352
+
353
+ def mixins
354
+ (up_instructions + down_instructions).inject([]) do |mixins, i|
355
+ i.respond_to?(:mixins) ? [*mixins, *i.mixins] : mixins
356
+ end.uniq
357
+ end
358
+
359
+ def method_missing(name, *args, &block)
360
+ if Nandi.config.custom_methods.key?(name)
361
+ invoke_custom_method(name, *args, &block)
362
+ else
363
+ super
364
+ end
365
+ end
366
+
367
+ private
368
+
369
+ attr_reader :validator
370
+
371
+ def current_instructions
372
+ @instructions[@direction]
373
+ end
374
+
375
+ def default_statement_timeout
376
+ Nandi.config.access_exclusive_statement_timeout
377
+ end
378
+
379
+ def default_lock_timeout
380
+ Nandi.config.access_exclusive_lock_timeout
381
+ end
382
+
383
+ def invoke_custom_method(name, *args, &block)
384
+ klass = Nandi.config.custom_methods[name]
385
+ current_instructions << klass.new(*args, &block)
386
+ end
387
+ end
388
+ end