with_advisory_lock 5.1.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.
Files changed (78) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +36 -40
  3. data/.github/workflows/release.yml +1 -4
  4. data/.gitignore +2 -2
  5. data/.release-please-manifest.json +1 -1
  6. data/.ruby-version +2 -0
  7. data/.tool-versions +1 -1
  8. data/CHANGELOG.md +51 -0
  9. data/Gemfile +31 -0
  10. data/LICENSE.txt +4 -4
  11. data/Makefile +10 -0
  12. data/README.md +7 -35
  13. data/Rakefile +5 -2
  14. data/bin/console +11 -0
  15. data/bin/rails +15 -0
  16. data/bin/sanity +20 -0
  17. data/bin/sanity_check +86 -0
  18. data/bin/setup +8 -0
  19. data/bin/setup_test_db +59 -0
  20. data/bin/test_connections +22 -0
  21. data/docker-compose.yml +19 -0
  22. data/lib/with_advisory_lock/concern.rb +27 -16
  23. data/lib/with_advisory_lock/core_advisory.rb +110 -0
  24. data/lib/with_advisory_lock/jruby_adapter.rb +29 -0
  25. data/lib/with_advisory_lock/lock_stack_item.rb +6 -0
  26. data/lib/with_advisory_lock/mysql_advisory.rb +62 -0
  27. data/lib/with_advisory_lock/postgresql_advisory.rb +112 -0
  28. data/lib/with_advisory_lock/result.rb +14 -0
  29. data/lib/with_advisory_lock/version.rb +1 -1
  30. data/lib/with_advisory_lock.rb +38 -9
  31. data/test/dummy/Rakefile +8 -0
  32. data/test/dummy/app/controllers/application_controller.rb +7 -0
  33. data/test/dummy/app/models/application_record.rb +6 -0
  34. data/test/dummy/app/models/label.rb +4 -0
  35. data/test/dummy/app/models/mysql_label.rb +5 -0
  36. data/test/dummy/app/models/mysql_record.rb +6 -0
  37. data/test/dummy/app/models/mysql_tag.rb +10 -0
  38. data/test/dummy/app/models/mysql_tag_audit.rb +5 -0
  39. data/test/dummy/app/models/tag.rb +8 -0
  40. data/test/dummy/app/models/tag_audit.rb +4 -0
  41. data/test/dummy/config/application.rb +31 -0
  42. data/test/dummy/config/boot.rb +3 -0
  43. data/test/dummy/config/database.yml +13 -0
  44. data/test/dummy/config/environment.rb +7 -0
  45. data/test/dummy/config/routes.rb +4 -0
  46. data/test/dummy/config.ru +6 -0
  47. data/test/{test_models.rb → dummy/db/schema.rb} +3 -14
  48. data/test/dummy/db/secondary_schema.rb +15 -0
  49. data/test/dummy/lib/tasks/db.rake +40 -0
  50. data/test/sanity_check_test.rb +63 -0
  51. data/test/test_helper.rb +18 -37
  52. data/test/with_advisory_lock/concern_test.rb +79 -0
  53. data/test/with_advisory_lock/lock_test.rb +197 -0
  54. data/test/with_advisory_lock/multi_adapter_test.rb +17 -0
  55. data/test/with_advisory_lock/parallelism_test.rb +101 -0
  56. data/test/with_advisory_lock/postgresql_race_condition_test.rb +118 -0
  57. data/test/with_advisory_lock/shared_test.rb +129 -0
  58. data/test/with_advisory_lock/thread_test.rb +83 -0
  59. data/test/with_advisory_lock/transaction_test.rb +83 -0
  60. data/with_advisory_lock.gemspec +26 -6
  61. metadata +64 -55
  62. data/Appraisals +0 -45
  63. data/gemfiles/activerecord_6.1.gemfile +0 -21
  64. data/gemfiles/activerecord_7.0.gemfile +0 -21
  65. data/gemfiles/activerecord_7.1.gemfile +0 -14
  66. data/lib/with_advisory_lock/base.rb +0 -118
  67. data/lib/with_advisory_lock/database_adapter_support.rb +0 -26
  68. data/lib/with_advisory_lock/flock.rb +0 -33
  69. data/lib/with_advisory_lock/mysql.rb +0 -27
  70. data/lib/with_advisory_lock/postgresql.rb +0 -43
  71. data/test/concern_test.rb +0 -33
  72. data/test/lock_test.rb +0 -80
  73. data/test/nesting_test.rb +0 -28
  74. data/test/options_test.rb +0 -66
  75. data/test/parallelism_test.rb +0 -75
  76. data/test/shared_test.rb +0 -134
  77. data/test/thread_test.rb +0 -61
  78. data/test/transaction_test.rb +0 -68
@@ -19,30 +19,41 @@ module WithAdvisoryLock
19
19
  end
20
20
 
21
21
  def with_advisory_lock_result(lock_name, options = {}, &block)
22
- impl = impl_class.new(connection, lock_name, options)
23
- impl.with_advisory_lock_if_needed(&block)
22
+ with_connection do |conn|
23
+ conn.with_advisory_lock_if_needed(lock_name, options, &block)
24
+ end
24
25
  end
25
26
 
26
27
  def advisory_lock_exists?(lock_name)
27
- impl = impl_class.new(connection, lock_name, 0)
28
- impl.already_locked? || !impl.yield_with_lock.lock_was_acquired?
28
+ with_connection do |conn|
29
+ lock_str = "#{ENV.fetch(CoreAdvisory::LOCK_PREFIX_ENV, nil)}#{lock_name}"
30
+ lock_stack_item = LockStackItem.new(lock_str, false)
31
+
32
+ if conn.advisory_lock_stack.include?(lock_stack_item)
33
+ true
34
+ else
35
+ # For PostgreSQL, try non-blocking query first to avoid race conditions
36
+ if conn.respond_to?(:advisory_lock_exists_for?)
37
+ query_result = conn.advisory_lock_exists_for?(lock_name)
38
+ return query_result unless query_result.nil?
39
+ end
40
+
41
+ # Fall back to the original implementation
42
+ result = conn.with_advisory_lock_if_needed(lock_name, { timeout_seconds: 0 })
43
+ !result.lock_was_acquired?
44
+ end
45
+ end
29
46
  end
30
47
 
31
48
  def current_advisory_lock
32
- lock_stack_key = WithAdvisoryLock::Base.lock_stack.first
33
- lock_stack_key && lock_stack_key[0]
49
+ with_connection do |conn|
50
+ conn.advisory_lock_stack.first&.name
51
+ end
34
52
  end
35
53
 
36
- private
37
-
38
- def impl_class
39
- adapter = WithAdvisoryLock::DatabaseAdapterSupport.new(connection)
40
- if adapter.postgresql?
41
- WithAdvisoryLock::PostgreSQL
42
- elsif adapter.mysql?
43
- WithAdvisoryLock::MySQL
44
- else
45
- WithAdvisoryLock::Flock
54
+ def current_advisory_locks
55
+ with_connection do |conn|
56
+ conn.advisory_lock_stack.map(&:name)
46
57
  end
47
58
  end
48
59
  end
@@ -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,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WithAdvisoryLock
4
+ # Lock stack item to track acquired locks
5
+ LockStackItem = Data.define(:name, :shared)
6
+ 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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module WithAdvisoryLock
4
- VERSION = Gem::Version.new('5.1.0')
4
+ VERSION = Gem::Version.new('7.0.0')
5
5
  end
@@ -1,17 +1,46 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'with_advisory_lock/version'
2
4
  require 'active_support'
3
- require 'zeitwerk'
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
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'
13
18
  end
14
19
 
15
20
  ActiveSupport.on_load :active_record do
16
- include WithAdvisoryLock::Concern
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
17
46
  end
@@ -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,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ApplicationController < ActionController::Base
4
+ # Prevent CSRF attacks by raising an exception.
5
+ # For APIs, you may want to use :null_session instead.
6
+ protect_from_forgery with: :exception
7
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ApplicationRecord < ActiveRecord::Base
4
+ self.abstract_class = true
5
+ establish_connection(:primary)
6
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Label < ApplicationRecord
4
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ class MysqlLabel < MysqlRecord
4
+ self.table_name = 'mysql_labels'
5
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ class MysqlRecord < ActiveRecord::Base
4
+ self.abstract_class = true
5
+ establish_connection :secondary
6
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ class MysqlTag < MysqlRecord
4
+ self.table_name = 'mysql_tags'
5
+
6
+ after_save do
7
+ MysqlTagAudit.create(tag_name: name)
8
+ MysqlLabel.create(name: name)
9
+ end
10
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ class MysqlTagAudit < MysqlRecord
4
+ self.table_name = 'mysql_tag_audits'
5
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Tag < ApplicationRecord
4
+ after_save do
5
+ TagAudit.create(tag_name: name)
6
+ Label.create(name: name)
7
+ end
8
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ class TagAudit < ApplicationRecord
4
+ end
@@ -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
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/setup'
@@ -0,0 +1,13 @@
1
+ default: &default
2
+ pool: 20
3
+
4
+
5
+ test:
6
+ primary:
7
+ <<: *default
8
+ url: "<%= ENV['DATABASE_URL_PG'] %>"
9
+ secondary:
10
+ <<: *default
11
+ url: "<%= ENV['DATABASE_URL_MYSQL'] %>"
12
+ properties:
13
+ allowPublicKeyRetrieval: true
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Load the Rails application.
4
+ require File.expand_path('application', __dir__)
5
+
6
+ # Initialize the Rails application.
7
+ Rails.application.initialize!
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ Rails.application.routes.draw do
4
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'config/environment'
4
+
5
+ run Rails.application
6
+ Rails.application.load_server
@@ -1,26 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- ActiveRecord::Schema.define(version: 0) do
3
+ 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 Tag < ActiveRecord::Base
16
- after_save do
17
- TagAudit.create(tag_name: name)
18
- Label.create(name: name)
19
- end
20
- end
21
-
22
- class TagAudit < ActiveRecord::Base
23
- end
24
-
25
- class Label < ActiveRecord::Base
26
- 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