with_advisory_lock 5.3.0 → 7.0.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.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +76 -0
- data/.gitignore +2 -2
- data/.release-please-manifest.json +1 -1
- data/CHANGELOG.md +32 -0
- data/Gemfile +31 -0
- data/Makefile +8 -12
- data/README.md +7 -35
- data/Rakefile +5 -2
- data/bin/console +11 -0
- data/bin/rails +15 -0
- data/bin/sanity +20 -0
- data/bin/sanity_check +86 -0
- data/bin/setup +8 -0
- data/bin/setup_test_db +59 -0
- data/bin/test_connections +22 -0
- data/docker-compose.yml +3 -4
- data/lib/with_advisory_lock/concern.rb +26 -19
- data/lib/with_advisory_lock/core_advisory.rb +110 -0
- data/lib/with_advisory_lock/jruby_adapter.rb +29 -0
- data/lib/with_advisory_lock/lock_stack_item.rb +6 -0
- data/lib/with_advisory_lock/mysql_advisory.rb +62 -0
- data/lib/with_advisory_lock/postgresql_advisory.rb +112 -0
- data/lib/with_advisory_lock/result.rb +14 -0
- data/lib/with_advisory_lock/version.rb +1 -1
- data/lib/with_advisory_lock.rb +38 -10
- data/test/dummy/Rakefile +8 -0
- data/test/dummy/app/controllers/application_controller.rb +7 -0
- data/test/dummy/app/models/application_record.rb +6 -0
- data/test/dummy/app/models/label.rb +4 -0
- data/test/dummy/app/models/mysql_label.rb +5 -0
- data/test/dummy/app/models/mysql_record.rb +6 -0
- data/test/dummy/app/models/mysql_tag.rb +10 -0
- data/test/dummy/app/models/mysql_tag_audit.rb +5 -0
- data/test/dummy/app/models/tag.rb +8 -0
- data/test/dummy/app/models/tag_audit.rb +4 -0
- data/test/dummy/config/application.rb +31 -0
- data/test/dummy/config/boot.rb +3 -0
- data/test/dummy/config/database.yml +13 -0
- data/test/dummy/config/environment.rb +7 -0
- data/test/dummy/config/routes.rb +4 -0
- data/test/dummy/config.ru +6 -0
- data/test/{test_models.rb → dummy/db/schema.rb} +2 -17
- data/test/dummy/db/secondary_schema.rb +15 -0
- data/test/dummy/lib/tasks/db.rake +40 -0
- data/test/sanity_check_test.rb +63 -0
- data/test/test_helper.rb +14 -47
- data/test/with_advisory_lock/concern_test.rb +58 -12
- data/test/with_advisory_lock/lock_test.rb +159 -73
- data/test/with_advisory_lock/multi_adapter_test.rb +17 -0
- data/test/with_advisory_lock/parallelism_test.rb +63 -37
- data/test/with_advisory_lock/postgresql_race_condition_test.rb +118 -0
- data/test/with_advisory_lock/shared_test.rb +52 -57
- data/test/with_advisory_lock/thread_test.rb +64 -42
- data/test/with_advisory_lock/transaction_test.rb +55 -40
- data/with_advisory_lock.gemspec +25 -5
- metadata +54 -50
- data/.github/workflows/ci-mysql5.yml +0 -61
- data/.github/workflows/ci-mysql8.yml +0 -62
- data/.github/workflows/ci-postgresql.yml +0 -64
- data/.github/workflows/ci-sqlite3.yml +0 -54
- data/Appraisals +0 -45
- data/gemfiles/activerecord_6.1.gemfile +0 -21
- data/gemfiles/activerecord_7.0.gemfile +0 -21
- data/gemfiles/activerecord_7.1.gemfile +0 -14
- data/lib/with_advisory_lock/base.rb +0 -118
- data/lib/with_advisory_lock/database_adapter_support.rb +0 -23
- data/lib/with_advisory_lock/flock.rb +0 -33
- data/lib/with_advisory_lock/mysql.rb +0 -32
- data/lib/with_advisory_lock/postgresql.rb +0 -66
- data/test/with_advisory_lock/base_test.rb +0 -9
- data/test/with_advisory_lock/nesting_test.rb +0 -28
- data/test/with_advisory_lock/options_test.rb +0 -66
@@ -0,0 +1,110 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'zlib'
|
4
|
+
|
5
|
+
module WithAdvisoryLock
|
6
|
+
module CoreAdvisory
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
|
9
|
+
LOCK_PREFIX_ENV = 'WITH_ADVISORY_LOCK_PREFIX'
|
10
|
+
|
11
|
+
# Thread-local lock stack management
|
12
|
+
def advisory_lock_stack
|
13
|
+
Thread.current[:with_advisory_lock_stack] ||= []
|
14
|
+
end
|
15
|
+
|
16
|
+
def with_advisory_lock_if_needed(lock_name, options = {}, &block)
|
17
|
+
options = { timeout_seconds: options } unless options.respond_to?(:fetch)
|
18
|
+
options.assert_valid_keys :timeout_seconds, :shared, :transaction, :disable_query_cache
|
19
|
+
|
20
|
+
# Validate transaction-level locks are used within a transaction
|
21
|
+
if options.fetch(:transaction, false) && !transaction_open?
|
22
|
+
raise ArgumentError, 'transaction-level advisory locks require an active transaction'
|
23
|
+
end
|
24
|
+
|
25
|
+
lock_str = "#{ENV.fetch(LOCK_PREFIX_ENV, nil)}#{lock_name}"
|
26
|
+
lock_stack_item = LockStackItem.new(lock_str, options.fetch(:shared, false))
|
27
|
+
|
28
|
+
if advisory_lock_stack.include?(lock_stack_item)
|
29
|
+
# Already have this exact lock (same name and type), just yield
|
30
|
+
return Result.new(lock_was_acquired: true, result: yield)
|
31
|
+
end
|
32
|
+
|
33
|
+
# Check if we have a lock with the same name but different type (for upgrade/downgrade)
|
34
|
+
same_name_different_type = advisory_lock_stack.any? do |item|
|
35
|
+
item.name == lock_str && item.shared != options.fetch(:shared, false)
|
36
|
+
end
|
37
|
+
if same_name_different_type && options.fetch(:transaction, false)
|
38
|
+
# PostgreSQL doesn't support upgrading/downgrading transaction-level locks
|
39
|
+
return Result.new(lock_was_acquired: false)
|
40
|
+
end
|
41
|
+
|
42
|
+
disable_query_cache = options.fetch(:disable_query_cache, false)
|
43
|
+
|
44
|
+
if disable_query_cache
|
45
|
+
uncached do
|
46
|
+
advisory_lock_and_yield(lock_name, lock_str, lock_stack_item, options, &block)
|
47
|
+
end
|
48
|
+
else
|
49
|
+
advisory_lock_and_yield(lock_name, lock_str, lock_stack_item, options, &block)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
def advisory_lock_and_yield(lock_name, lock_str, lock_stack_item, options, &)
|
56
|
+
timeout_seconds = options.fetch(:timeout_seconds, nil)
|
57
|
+
shared = options.fetch(:shared, false)
|
58
|
+
transaction = options.fetch(:transaction, false)
|
59
|
+
|
60
|
+
lock_keys = lock_keys_for(lock_name)
|
61
|
+
|
62
|
+
# MySQL supports database-level timeout in GET_LOCK, skip Ruby-level polling
|
63
|
+
if supports_database_timeout? || timeout_seconds&.zero?
|
64
|
+
yield_with_lock(lock_keys, lock_name, lock_str, lock_stack_item, shared, transaction, timeout_seconds, &)
|
65
|
+
else
|
66
|
+
yield_with_lock_and_timeout(lock_keys, lock_name, lock_str, lock_stack_item, shared, transaction,
|
67
|
+
timeout_seconds, &)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def yield_with_lock_and_timeout(lock_keys, lock_name, lock_str, lock_stack_item, shared, transaction,
|
72
|
+
timeout_seconds, &)
|
73
|
+
give_up_at = timeout_seconds ? Time.now + timeout_seconds : nil
|
74
|
+
while give_up_at.nil? || Time.now < give_up_at
|
75
|
+
r = yield_with_lock(lock_keys, lock_name, lock_str, lock_stack_item, shared, transaction, 0, &)
|
76
|
+
return r if r.lock_was_acquired?
|
77
|
+
|
78
|
+
# Randomizing sleep time may help reduce contention.
|
79
|
+
sleep(rand(0.05..0.15))
|
80
|
+
end
|
81
|
+
Result.new(lock_was_acquired: false)
|
82
|
+
end
|
83
|
+
|
84
|
+
def yield_with_lock(lock_keys, lock_name, _lock_str, lock_stack_item, shared, transaction, timeout_seconds = nil)
|
85
|
+
if try_advisory_lock(lock_keys, lock_name: lock_name, shared: shared, transaction: transaction,
|
86
|
+
timeout_seconds: timeout_seconds)
|
87
|
+
begin
|
88
|
+
advisory_lock_stack.push(lock_stack_item)
|
89
|
+
result = block_given? ? yield : nil
|
90
|
+
Result.new(lock_was_acquired: true, result: result)
|
91
|
+
ensure
|
92
|
+
advisory_lock_stack.pop
|
93
|
+
release_advisory_lock(lock_keys, lock_name: lock_name, shared: shared, transaction: transaction)
|
94
|
+
end
|
95
|
+
else
|
96
|
+
Result.new(lock_was_acquired: false)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def stable_hashcode(input)
|
101
|
+
if input.is_a? Numeric
|
102
|
+
input.to_i
|
103
|
+
else
|
104
|
+
# Ruby MRI's String#hash is randomly seeded as of Ruby 1.9 so
|
105
|
+
# make sure we use a deterministic hash.
|
106
|
+
Zlib.crc32(input.to_s, 0)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module WithAdvisoryLock
|
4
|
+
module JRubyAdapter
|
5
|
+
# JRuby compatibility - ensure adapters are patched after they're loaded
|
6
|
+
def self.install!
|
7
|
+
ActiveSupport.on_load :active_record do
|
8
|
+
ActiveRecord::Base.singleton_class.prepend(Module.new do
|
9
|
+
def connection
|
10
|
+
super.tap do |conn|
|
11
|
+
case conn
|
12
|
+
when ActiveRecord::ConnectionAdapters::PostgreSQLAdapter
|
13
|
+
unless conn.class.include?(WithAdvisoryLock::CoreAdvisory)
|
14
|
+
conn.class.prepend WithAdvisoryLock::CoreAdvisory
|
15
|
+
conn.class.prepend WithAdvisoryLock::PostgreSQLAdvisory
|
16
|
+
end
|
17
|
+
when ActiveRecord::ConnectionAdapters::Mysql2Adapter, ActiveRecord::ConnectionAdapters::TrilogyAdapter
|
18
|
+
unless conn.class.include?(WithAdvisoryLock::CoreAdvisory)
|
19
|
+
conn.class.prepend WithAdvisoryLock::CoreAdvisory
|
20
|
+
conn.class.prepend WithAdvisoryLock::MySQLAdvisory
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'securerandom'
|
4
|
+
|
5
|
+
module WithAdvisoryLock
|
6
|
+
module MySQLAdvisory
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
|
9
|
+
LOCK_PREFIX_ENV = 'WITH_ADVISORY_LOCK_PREFIX'
|
10
|
+
|
11
|
+
def try_advisory_lock(lock_keys, lock_name:, shared:, transaction:, timeout_seconds: nil)
|
12
|
+
raise ArgumentError, 'shared locks are not supported on MySQL' if shared
|
13
|
+
raise ArgumentError, 'transaction level locks are not supported on MySQL' if transaction
|
14
|
+
|
15
|
+
# MySQL GET_LOCK supports native timeout:
|
16
|
+
# - timeout_seconds = nil: wait indefinitely (-1)
|
17
|
+
# - timeout_seconds = 0: try once, no wait (0)
|
18
|
+
# - timeout_seconds > 0: wait up to timeout_seconds
|
19
|
+
mysql_timeout = case timeout_seconds
|
20
|
+
when nil then -1
|
21
|
+
when 0 then 0
|
22
|
+
else timeout_seconds.to_i
|
23
|
+
end
|
24
|
+
|
25
|
+
execute_successful?("GET_LOCK(#{quote(lock_keys.first)}, #{mysql_timeout})")
|
26
|
+
end
|
27
|
+
|
28
|
+
def release_advisory_lock(lock_keys, lock_name:, **)
|
29
|
+
execute_successful?("RELEASE_LOCK(#{quote(lock_keys.first)})")
|
30
|
+
rescue ActiveRecord::StatementInvalid => e
|
31
|
+
# If the connection is broken, the lock is automatically released by MySQL
|
32
|
+
# No need to fail the release operation
|
33
|
+
connection_lost = case e.cause
|
34
|
+
when defined?(Mysql2::Error::ConnectionError) && Mysql2::Error::ConnectionError
|
35
|
+
true
|
36
|
+
when defined?(Trilogy::ConnectionError) && Trilogy::ConnectionError
|
37
|
+
true
|
38
|
+
else
|
39
|
+
e.message =~ /Lost connection|MySQL server has gone away|Connection refused/i
|
40
|
+
end
|
41
|
+
|
42
|
+
return if connection_lost
|
43
|
+
|
44
|
+
raise
|
45
|
+
end
|
46
|
+
|
47
|
+
def lock_keys_for(lock_name)
|
48
|
+
lock_str = "#{ENV.fetch(LOCK_PREFIX_ENV, nil)}#{lock_name}"
|
49
|
+
[lock_str]
|
50
|
+
end
|
51
|
+
|
52
|
+
def supports_database_timeout?
|
53
|
+
true
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
def execute_successful?(mysql_function)
|
59
|
+
select_value("SELECT #{mysql_function}") == 1
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,112 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'securerandom'
|
4
|
+
|
5
|
+
module WithAdvisoryLock
|
6
|
+
module PostgreSQLAdvisory
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
|
9
|
+
LOCK_PREFIX_ENV = 'WITH_ADVISORY_LOCK_PREFIX'
|
10
|
+
LOCK_RESULT_VALUES = ['t', true].freeze
|
11
|
+
ERROR_MESSAGE_REGEX = / ERROR: +current transaction is aborted,/
|
12
|
+
|
13
|
+
def try_advisory_lock(lock_keys, lock_name:, shared:, transaction:, timeout_seconds: nil)
|
14
|
+
# timeout_seconds is accepted for compatibility but ignored - PostgreSQL doesn't support
|
15
|
+
# native timeouts with pg_try_advisory_lock, requiring Ruby-level polling instead
|
16
|
+
function = advisory_try_lock_function(transaction, shared)
|
17
|
+
execute_advisory(function, lock_keys, lock_name)
|
18
|
+
end
|
19
|
+
|
20
|
+
def release_advisory_lock(*args)
|
21
|
+
# Handle both signatures - ActiveRecord's built-in and ours
|
22
|
+
if args.length == 1 && args[0].is_a?(Integer)
|
23
|
+
# ActiveRecord's built-in signature: release_advisory_lock(lock_id)
|
24
|
+
super
|
25
|
+
else
|
26
|
+
# Our signature: release_advisory_lock(lock_keys, lock_name:, shared:, transaction:)
|
27
|
+
lock_keys, options = args
|
28
|
+
return if options[:transaction]
|
29
|
+
|
30
|
+
function = advisory_unlock_function(options[:shared])
|
31
|
+
execute_advisory(function, lock_keys, options[:lock_name])
|
32
|
+
end
|
33
|
+
rescue ActiveRecord::StatementInvalid => e
|
34
|
+
# If the connection is broken, the lock is automatically released by PostgreSQL
|
35
|
+
# No need to fail the release operation
|
36
|
+
return if e.cause.is_a?(PG::ConnectionBad) || e.message =~ /PG::ConnectionBad/
|
37
|
+
|
38
|
+
raise unless e.message =~ ERROR_MESSAGE_REGEX
|
39
|
+
|
40
|
+
begin
|
41
|
+
rollback_db_transaction
|
42
|
+
execute_advisory(function, lock_keys, options[:lock_name])
|
43
|
+
ensure
|
44
|
+
begin_db_transaction
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def lock_keys_for(lock_name)
|
49
|
+
[
|
50
|
+
stable_hashcode(lock_name),
|
51
|
+
ENV.fetch(LOCK_PREFIX_ENV, nil)
|
52
|
+
].map { |ea| ea.to_i & 0x7fffffff }
|
53
|
+
end
|
54
|
+
|
55
|
+
def supports_database_timeout?
|
56
|
+
false
|
57
|
+
end
|
58
|
+
|
59
|
+
# Non-blocking check for advisory lock existence to avoid race conditions
|
60
|
+
# This queries pg_locks directly instead of trying to acquire the lock
|
61
|
+
def advisory_lock_exists_for?(lock_name, shared: false)
|
62
|
+
lock_keys = lock_keys_for(lock_name)
|
63
|
+
|
64
|
+
query = <<~SQL.squish
|
65
|
+
SELECT 1 FROM pg_locks
|
66
|
+
WHERE locktype = 'advisory'
|
67
|
+
AND database = (SELECT oid FROM pg_database WHERE datname = CURRENT_DATABASE())
|
68
|
+
AND classid = #{lock_keys.first}
|
69
|
+
AND objid = #{lock_keys.last}
|
70
|
+
AND mode = '#{shared ? 'ShareLock' : 'ExclusiveLock'}'
|
71
|
+
LIMIT 1
|
72
|
+
SQL
|
73
|
+
|
74
|
+
select_value(query).present?
|
75
|
+
rescue ActiveRecord::StatementInvalid
|
76
|
+
# If pg_locks is not accessible, fall back to nil to indicate we should use the default method
|
77
|
+
nil
|
78
|
+
end
|
79
|
+
|
80
|
+
private
|
81
|
+
|
82
|
+
def advisory_try_lock_function(transaction_scope, shared)
|
83
|
+
[
|
84
|
+
'pg_try_advisory',
|
85
|
+
transaction_scope ? '_xact' : nil,
|
86
|
+
'_lock',
|
87
|
+
shared ? '_shared' : nil
|
88
|
+
].compact.join
|
89
|
+
end
|
90
|
+
|
91
|
+
def advisory_unlock_function(shared)
|
92
|
+
[
|
93
|
+
'pg_advisory_unlock',
|
94
|
+
shared ? '_shared' : nil
|
95
|
+
].compact.join
|
96
|
+
end
|
97
|
+
|
98
|
+
def execute_advisory(function, lock_keys, lock_name)
|
99
|
+
result = select_value(prepare_sql(function, lock_keys, lock_name))
|
100
|
+
LOCK_RESULT_VALUES.include?(result)
|
101
|
+
end
|
102
|
+
|
103
|
+
def prepare_sql(function, lock_keys, lock_name)
|
104
|
+
comment = lock_name.to_s.gsub(%r{(/\*)|(\*/)}, '--')
|
105
|
+
"SELECT #{function}(#{lock_keys.join(',')}) AS #{unique_column_name} /* #{comment} */"
|
106
|
+
end
|
107
|
+
|
108
|
+
def unique_column_name
|
109
|
+
"t#{SecureRandom.hex}"
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module WithAdvisoryLock
|
4
|
+
# Result object that indicates whether a lock was acquired and the result of the block
|
5
|
+
Result = Data.define(:lock_was_acquired, :result) do
|
6
|
+
def initialize(lock_was_acquired:, result: nil)
|
7
|
+
super
|
8
|
+
end
|
9
|
+
|
10
|
+
def lock_was_acquired?
|
11
|
+
lock_was_acquired
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
data/lib/with_advisory_lock.rb
CHANGED
@@ -1,18 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'with_advisory_lock/version'
|
2
4
|
require 'active_support'
|
3
|
-
require '
|
4
|
-
|
5
|
-
loader = Zeitwerk::Loader.for_gem
|
6
|
-
loader.inflector.inflect(
|
7
|
-
'mysql' => 'MySQL',
|
8
|
-
'postgresql' => 'PostgreSQL',
|
9
|
-
)
|
10
|
-
loader.setup
|
5
|
+
require 'active_support/concern'
|
11
6
|
|
12
7
|
module WithAdvisoryLock
|
13
|
-
|
8
|
+
autoload :Concern, 'with_advisory_lock/concern'
|
9
|
+
autoload :Result, 'with_advisory_lock/result'
|
10
|
+
autoload :LockStackItem, 'with_advisory_lock/lock_stack_item'
|
11
|
+
|
12
|
+
# Modules for adapter injection
|
13
|
+
autoload :CoreAdvisory, 'with_advisory_lock/core_advisory'
|
14
|
+
autoload :PostgreSQLAdvisory, 'with_advisory_lock/postgresql_advisory'
|
15
|
+
autoload :MySQLAdvisory, 'with_advisory_lock/mysql_advisory'
|
16
|
+
|
17
|
+
autoload :FailedToAcquireLock, 'with_advisory_lock/failed_to_acquire_lock'
|
14
18
|
end
|
15
19
|
|
16
20
|
ActiveSupport.on_load :active_record do
|
17
|
-
|
21
|
+
require 'active_record/connection_adapters/abstract_adapter'
|
22
|
+
ActiveRecord::Base.include WithAdvisoryLock::Concern
|
23
|
+
end
|
24
|
+
|
25
|
+
# JRuby compatibility handling
|
26
|
+
if RUBY_ENGINE == 'jruby'
|
27
|
+
require 'with_advisory_lock/jruby_adapter'
|
28
|
+
WithAdvisoryLock::JRubyAdapter.install!
|
29
|
+
# Don't set up the standard hooks for JRuby
|
30
|
+
else
|
31
|
+
# Standard adapter injection for MRI and TruffleRuby
|
32
|
+
ActiveSupport.on_load :active_record_postgresqladapter do
|
33
|
+
prepend WithAdvisoryLock::CoreAdvisory
|
34
|
+
prepend WithAdvisoryLock::PostgreSQLAdvisory
|
35
|
+
end
|
36
|
+
|
37
|
+
ActiveSupport.on_load :active_record_mysql2adapter do
|
38
|
+
prepend WithAdvisoryLock::CoreAdvisory
|
39
|
+
prepend WithAdvisoryLock::MySQLAdvisory
|
40
|
+
end
|
41
|
+
|
42
|
+
ActiveSupport.on_load :active_record_trilogyadapter do
|
43
|
+
prepend WithAdvisoryLock::CoreAdvisory
|
44
|
+
prepend WithAdvisoryLock::MySQLAdvisory
|
45
|
+
end
|
18
46
|
end
|
data/test/dummy/Rakefile
ADDED
@@ -0,0 +1,8 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Add your own tasks in files placed in lib/tasks ending in .rake,
|
4
|
+
# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
|
5
|
+
|
6
|
+
require_relative 'config/application'
|
7
|
+
|
8
|
+
Rails.application.load_tasks
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require File.expand_path('boot', __dir__)
|
4
|
+
|
5
|
+
require 'rails'
|
6
|
+
require 'active_model/railtie'
|
7
|
+
require 'active_record/railtie'
|
8
|
+
require 'action_controller/railtie'
|
9
|
+
require 'action_view/railtie'
|
10
|
+
|
11
|
+
Bundler.require(*Rails.groups)
|
12
|
+
|
13
|
+
module TestSystemApp
|
14
|
+
class Application < Rails::Application
|
15
|
+
config.load_defaults [Rails::VERSION::MAJOR, Rails::VERSION::MINOR].join('.')
|
16
|
+
config.eager_load = true
|
17
|
+
config.serve_static_files = false
|
18
|
+
config.public_file_server.enabled = false
|
19
|
+
config.public_file_server.headers = { 'Cache-Control' => 'public, max-age=3600' }
|
20
|
+
config.consider_all_requests_local = true
|
21
|
+
config.action_controller.perform_caching = false
|
22
|
+
config.action_dispatch.show_exceptions = false
|
23
|
+
config.action_controller.allow_forgery_protection = false
|
24
|
+
config.active_support.test_order = :random
|
25
|
+
config.active_support.deprecation = :stderr
|
26
|
+
config.active_record.timestamped_migrations = false
|
27
|
+
|
28
|
+
# Disable automatic database setup since we handle it manually
|
29
|
+
config.active_record.maintain_test_schema = false if config.respond_to?(:active_record)
|
30
|
+
end
|
31
|
+
end
|
@@ -4,27 +4,12 @@ ActiveRecord::Schema.define(version: 1) do
|
|
4
4
|
create_table 'tags', force: true do |t|
|
5
5
|
t.string 'name'
|
6
6
|
end
|
7
|
+
|
7
8
|
create_table 'tag_audits', id: false, force: true do |t|
|
8
9
|
t.string 'tag_name'
|
9
10
|
end
|
11
|
+
|
10
12
|
create_table 'labels', id: false, force: true do |t|
|
11
13
|
t.string 'name'
|
12
14
|
end
|
13
15
|
end
|
14
|
-
|
15
|
-
class ApplicationRecord < ActiveRecord::Base
|
16
|
-
self.abstract_class = true
|
17
|
-
end
|
18
|
-
|
19
|
-
class Tag < ApplicationRecord
|
20
|
-
after_save do
|
21
|
-
TagAudit.create(tag_name: name)
|
22
|
-
Label.create(name: name)
|
23
|
-
end
|
24
|
-
end
|
25
|
-
|
26
|
-
class TagAudit < ApplicationRecord
|
27
|
-
end
|
28
|
-
|
29
|
-
class Label < ApplicationRecord
|
30
|
-
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
ActiveRecord::Schema.define(version: 1) do
|
4
|
+
create_table 'mysql_tags', force: true do |t|
|
5
|
+
t.string 'name'
|
6
|
+
end
|
7
|
+
|
8
|
+
create_table 'mysql_tag_audits', id: false, force: true do |t|
|
9
|
+
t.string 'tag_name'
|
10
|
+
end
|
11
|
+
|
12
|
+
create_table 'mysql_labels', id: false, force: true do |t|
|
13
|
+
t.string 'name'
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
namespace :db do
|
4
|
+
namespace :test do
|
5
|
+
desc 'Load schema for all databases'
|
6
|
+
task prepare: :environment do
|
7
|
+
# Load schema for primary database
|
8
|
+
ActiveRecord::Base.establish_connection(:primary)
|
9
|
+
ActiveRecord::Schema.define(version: 1) do
|
10
|
+
create_table 'tags', force: true do |t|
|
11
|
+
t.string 'name'
|
12
|
+
end
|
13
|
+
|
14
|
+
create_table 'tag_audits', id: false, force: true do |t|
|
15
|
+
t.string 'tag_name'
|
16
|
+
end
|
17
|
+
|
18
|
+
create_table 'labels', id: false, force: true do |t|
|
19
|
+
t.string 'name'
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# Load schema for secondary database
|
24
|
+
ActiveRecord::Base.establish_connection(:secondary)
|
25
|
+
ActiveRecord::Schema.define(version: 1) do
|
26
|
+
create_table 'mysql_tags', force: true do |t|
|
27
|
+
t.string 'name'
|
28
|
+
end
|
29
|
+
|
30
|
+
create_table 'mysql_tag_audits', id: false, force: true do |t|
|
31
|
+
t.string 'tag_name'
|
32
|
+
end
|
33
|
+
|
34
|
+
create_table 'mysql_labels', id: false, force: true do |t|
|
35
|
+
t.string 'name'
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|