culturecode_stagehand 1.1.5 → 1.1.10
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/lib/stagehand/active_record_extensions.rb +4 -4
- data/lib/stagehand/configuration.rb +2 -1
- data/lib/stagehand/connection_adapter_extensions.rb +18 -2
- data/lib/stagehand/database.rb +46 -18
- data/lib/stagehand/production.rb +50 -14
- data/lib/stagehand/schema.rb +18 -2
- data/lib/stagehand/staging/checklist.rb +1 -1
- data/lib/stagehand/staging/commit_entry.rb +3 -2
- data/lib/stagehand/staging/model.rb +12 -2
- data/lib/stagehand/staging/synchronizer.rb +1 -1
- data/lib/stagehand/version.rb +1 -1
- data/lib/tasks/stagehand_tasks.rake +2 -0
- metadata +3 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a254ae2d2cba86110f9584be58f6975ca777958bec8c1bc6e71f18195080d43e
|
4
|
+
data.tar.gz: 1a4a94337e918823c620bda226fba92789a9e47b9067a6daf6fff5676e33648b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1c2a0b58055a9033a86a4f7fce1a3b83f92140e3390f39aa8b745dc8d9b3e0bcc01e2c8724c810c85d67080ff6c2810a61f711735716269143098b04a2c38523
|
7
|
+
data.tar.gz: 1355afb448d1f0c5549bb814ef1c9a34f36983ce3c78a81f7bf3edc93f0501bc1349718a5a076548682b604193ac3a991093c2cab24b8f8937f980afa4091b0a
|
@@ -71,16 +71,16 @@ ActiveRecord::Base.class_eval do
|
|
71
71
|
# Keep track of the current connection name per-model, per-thread so multithreaded webservers don't overwrite it
|
72
72
|
module StagehandConnectionMap
|
73
73
|
def self.set(klass, connection_name)
|
74
|
-
|
74
|
+
current_map[klass.name] = connection_name
|
75
75
|
end
|
76
76
|
|
77
77
|
def self.get(klass)
|
78
|
-
|
78
|
+
current_map[klass.name]
|
79
79
|
end
|
80
80
|
|
81
|
-
def self.
|
81
|
+
def self.current_map
|
82
82
|
map = Thread.current.thread_variable_get('StagehandConnectionMap')
|
83
|
-
map = Thread.current.thread_variable_set('StagehandConnectionMap',
|
83
|
+
map = Thread.current.thread_variable_set('StagehandConnectionMap', Concurrent::Hash.new) unless map
|
84
84
|
return map
|
85
85
|
end
|
86
86
|
end
|
@@ -9,7 +9,8 @@ module Stagehand
|
|
9
9
|
module Configuration
|
10
10
|
extend self
|
11
11
|
|
12
|
-
mattr_accessor :checklist_confirmation_filter, :checklist_association_filter, :checklist_relation_filter, :ignored_columns
|
12
|
+
mattr_accessor :checklist_confirmation_filter, :checklist_association_filter, :checklist_relation_filter, :staging_model_tables, :ignored_columns
|
13
|
+
self.staging_model_tables = Set.new
|
13
14
|
self.ignored_columns = HashWithIndifferentAccess.new
|
14
15
|
|
15
16
|
def staging_connection_name
|
@@ -9,14 +9,30 @@ module Stagehand
|
|
9
9
|
end
|
10
10
|
|
11
11
|
def self.allow_unsynced_production_writes!(state = true)
|
12
|
-
Thread.current
|
12
|
+
Thread.current.thread_variable_set(:stagehand_allow_unsynced_production_writes, state)
|
13
13
|
end
|
14
14
|
|
15
15
|
def self.allow_unsynced_production_writes?
|
16
|
-
!!Thread.current
|
16
|
+
!!Thread.current.thread_variable_get(:stagehand_allow_unsynced_production_writes)
|
17
17
|
end
|
18
18
|
|
19
19
|
module AdapterExtensions
|
20
|
+
def quote_table_name(table_name)
|
21
|
+
if prefix_table_name_with_database?(table_name)
|
22
|
+
super("#{Stagehand::Database.staging_database_name}.#{table_name}")
|
23
|
+
else
|
24
|
+
super
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def prefix_table_name_with_database?(table_name)
|
29
|
+
return false if Configuration.single_connection?
|
30
|
+
return false unless Database.connected_to_production?
|
31
|
+
return false if Connection.allow_unsynced_production_writes?
|
32
|
+
return false unless Configuration.staging_model_tables.include?(table_name)
|
33
|
+
true
|
34
|
+
end
|
35
|
+
|
20
36
|
def exec_insert(*)
|
21
37
|
handle_readonly_writes!
|
22
38
|
super
|
data/lib/stagehand/database.rb
CHANGED
@@ -5,6 +5,31 @@ module Stagehand
|
|
5
5
|
module Database
|
6
6
|
extend self
|
7
7
|
|
8
|
+
def transaction
|
9
|
+
raise InvalidConnectionError, "Calling Stagehand::Database.transaction is not valid unless connected to staging" unless connected_to_staging?
|
10
|
+
|
11
|
+
success = false
|
12
|
+
attempts = 0
|
13
|
+
output = nil
|
14
|
+
ActiveRecord::Base.transaction do
|
15
|
+
Production::Record.transaction do
|
16
|
+
attempts += 1
|
17
|
+
|
18
|
+
raise NoRetryError, "Retrying is not allowed in Stagehand::Database.transaction" if attempts > 1
|
19
|
+
|
20
|
+
output = yield
|
21
|
+
|
22
|
+
success = true
|
23
|
+
end
|
24
|
+
|
25
|
+
raise ActiveRecord::Rollback unless success
|
26
|
+
end
|
27
|
+
|
28
|
+
return output
|
29
|
+
ensure
|
30
|
+
Rails.logger.warn "Stagehand::Database transaction was rolled back" unless success
|
31
|
+
end
|
32
|
+
|
8
33
|
def each(&block)
|
9
34
|
with_production_connection(&block) unless Configuration.single_connection?
|
10
35
|
with_staging_connection(&block)
|
@@ -62,30 +87,17 @@ module Stagehand
|
|
62
87
|
return output
|
63
88
|
end
|
64
89
|
|
65
|
-
def transaction
|
66
|
-
success = false
|
67
|
-
output = nil
|
68
|
-
ActiveRecord::Base.transaction do
|
69
|
-
Production::Record.transaction do
|
70
|
-
output = yield
|
71
|
-
success = true
|
72
|
-
end
|
73
|
-
raise ActiveRecord::Rollback unless success
|
74
|
-
end
|
75
|
-
return output
|
76
|
-
end
|
77
|
-
|
78
90
|
private
|
79
91
|
|
80
92
|
def swap_connection(connection_name)
|
93
|
+
pushed = ConnectionStack.push(connection_name.to_sym)
|
81
94
|
cache = ActiveRecord::Base.connection_pool.query_cache_enabled
|
82
|
-
ConnectionStack.push(connection_name.to_sym)
|
83
95
|
ActiveRecord::Base.connection_specification_name = current_connection_name
|
84
96
|
ActiveRecord::Base.connection_pool.enable_query_cache! if cache
|
85
97
|
|
86
98
|
yield connection_name
|
87
99
|
ensure
|
88
|
-
ConnectionStack.pop
|
100
|
+
ConnectionStack.pop if pushed
|
89
101
|
ActiveRecord::Base.connection_specification_name = current_connection_name
|
90
102
|
ActiveRecord::Base.connection_pool.enable_query_cache! if cache
|
91
103
|
end
|
@@ -128,6 +140,14 @@ module Stagehand
|
|
128
140
|
super(Configuration.staging_connection_name)
|
129
141
|
end
|
130
142
|
|
143
|
+
def self.connection
|
144
|
+
if Stagehand::Database.connected_to_staging?
|
145
|
+
ActiveRecord::Base.connection # Reuse existing connection so we stay within the current transaction
|
146
|
+
else
|
147
|
+
super
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
131
151
|
init_connection
|
132
152
|
end
|
133
153
|
|
@@ -143,8 +163,6 @@ module Stagehand
|
|
143
163
|
|
144
164
|
# Threadsafe tracking of the connection stack
|
145
165
|
module ConnectionStack
|
146
|
-
@@connection_name_stack = Hash.new { |h,k| h[k] = [ Rails.env.to_sym ] }
|
147
|
-
|
148
166
|
def self.push(connection_name)
|
149
167
|
current_stack.push connection_name
|
150
168
|
end
|
@@ -158,8 +176,18 @@ module Stagehand
|
|
158
176
|
end
|
159
177
|
|
160
178
|
def self.current_stack
|
161
|
-
|
179
|
+
if stack = Thread.current.thread_variable_get('sparkle_connection_name_stack')
|
180
|
+
stack
|
181
|
+
else
|
182
|
+
stack = Concurrent::Array.new
|
183
|
+
stack << Rails.env.to_sym
|
184
|
+
Thread.current.thread_variable_set('sparkle_connection_name_stack', stack)
|
185
|
+
stack
|
186
|
+
end
|
162
187
|
end
|
163
188
|
end
|
189
|
+
|
190
|
+
class InvalidConnectionError < StandardError; end
|
191
|
+
class NoRetryError < StandardError; end
|
164
192
|
end
|
165
193
|
end
|
data/lib/stagehand/production.rb
CHANGED
@@ -24,16 +24,19 @@ module Stagehand
|
|
24
24
|
end
|
25
25
|
|
26
26
|
def write(staging_record, attributes, table_name = nil)
|
27
|
-
|
28
|
-
is_new = matching(staging_record, table_name).update_all(attributes).zero?
|
27
|
+
table_name, id = Stagehand::Key.generate(staging_record, :table_name => table_name)
|
29
28
|
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
29
|
+
production_record = Connection.with_production_writes do
|
30
|
+
prepare_to_modify(table_name)
|
31
|
+
|
32
|
+
if update(table_name, id, attributes).nonzero?
|
33
|
+
Record.find(id)
|
34
|
+
else
|
35
|
+
Record.find(insert(table_name, attributes))
|
35
36
|
end
|
36
37
|
end
|
38
|
+
|
39
|
+
return production_record
|
37
40
|
end
|
38
41
|
|
39
42
|
def delete(staging_record, table_name = nil)
|
@@ -67,25 +70,58 @@ module Stagehand
|
|
67
70
|
|
68
71
|
private
|
69
72
|
|
70
|
-
def prepare_to_modify(table_name)
|
71
|
-
raise "Can't prepare to modify production records without knowning the table_name" unless table_name.present?
|
72
|
-
Record.table_name = table_name
|
73
|
-
end
|
74
|
-
|
75
73
|
def production_record_attributes(staging_record, table_name = nil)
|
76
74
|
Record.connection.select_one(matching(staging_record, table_name))
|
77
75
|
end
|
78
76
|
|
79
77
|
def staging_record_attributes(staging_record, table_name = nil)
|
80
78
|
table_name, id = Stagehand::Key.generate(staging_record, :table_name => table_name)
|
81
|
-
hash =
|
82
|
-
hash
|
79
|
+
hash = select(table_name, id)
|
80
|
+
hash.except(*ignored_columns(table_name)) if hash
|
83
81
|
end
|
84
82
|
|
85
83
|
def ignored_columns(table_name)
|
86
84
|
Array.wrap(Configuration.ignored_columns[table_name]).map(&:to_s)
|
87
85
|
end
|
88
86
|
|
87
|
+
def select(table_name, id)
|
88
|
+
table = Arel::Table.new(table_name)
|
89
|
+
statement = Arel::SelectManager.new
|
90
|
+
statement.from table
|
91
|
+
statement.project Arel.star
|
92
|
+
statement.where table[:id].eq(id)
|
93
|
+
|
94
|
+
Stagehand::Database::StagingProbe.connection.select_one(statement)
|
95
|
+
end
|
96
|
+
|
97
|
+
def update(table_name, id, attributes)
|
98
|
+
table = Arel::Table.new(table_name)
|
99
|
+
statement = Arel::UpdateManager.new
|
100
|
+
statement.table table
|
101
|
+
statement.set attributes.map {|attribute, value| [table[attribute], value] }
|
102
|
+
statement.where table[:id].eq(id)
|
103
|
+
|
104
|
+
Record.connection.update(statement)
|
105
|
+
end
|
106
|
+
|
107
|
+
def insert(table_name, attributes)
|
108
|
+
table = Arel::Table.new(table_name)
|
109
|
+
statement = Arel::InsertManager.new
|
110
|
+
statement.into table
|
111
|
+
statement.insert attributes.map {|attribute, value| [table[attribute], value] }
|
112
|
+
|
113
|
+
Record.connection.insert(statement)
|
114
|
+
end
|
115
|
+
|
116
|
+
def prepare_to_modify(table_name)
|
117
|
+
raise "Can't prepare to modify production records without knowning the table_name" unless table_name.present?
|
118
|
+
|
119
|
+
return if Record.table_name == table_name
|
120
|
+
|
121
|
+
Record.table_name = table_name
|
122
|
+
Record.reset_column_information
|
123
|
+
end
|
124
|
+
|
89
125
|
# CLASSES
|
90
126
|
|
91
127
|
class Record < Stagehand::Database::ProductionProbe
|
data/lib/stagehand/schema.rb
CHANGED
@@ -28,6 +28,8 @@ module Stagehand
|
|
28
28
|
end
|
29
29
|
|
30
30
|
def add_stagehand!(options = {})
|
31
|
+
return if Database.connected_to_production? && !Stagehand::Configuration.single_connection?
|
32
|
+
|
31
33
|
ActiveRecord::Schema.define do
|
32
34
|
Stagehand::Schema.send :each_table, options do |table_name|
|
33
35
|
Stagehand::Schema.send :create_operation_trigger, table_name, 'insert', 'NEW'
|
@@ -40,6 +42,7 @@ module Stagehand
|
|
40
42
|
def remove_stagehand!(options = {})
|
41
43
|
ActiveRecord::Schema.define do
|
42
44
|
Stagehand::Schema.send :each_table, options do |table_name|
|
45
|
+
next unless Stagehand::Schema.send :has_stagehand_triggers?, table_name
|
43
46
|
Stagehand::Schema.send :drop_trigger, table_name, 'insert'
|
44
47
|
Stagehand::Schema.send :drop_trigger, table_name, 'update'
|
45
48
|
Stagehand::Schema.send :drop_trigger, table_name, 'delete'
|
@@ -53,9 +56,9 @@ module Stagehand
|
|
53
56
|
if UNTRACKED_TABLES.include?(table_name.to_s)
|
54
57
|
return false
|
55
58
|
elsif table_name
|
56
|
-
|
59
|
+
has_stagehand_triggers?(table_name)
|
57
60
|
else
|
58
|
-
ActiveRecord::Base.
|
61
|
+
ActiveRecord::Base.connection.table_exists?(Stagehand::Staging::CommitEntry.table_name)
|
59
62
|
end
|
60
63
|
end
|
61
64
|
|
@@ -106,8 +109,21 @@ module Stagehand
|
|
106
109
|
ActiveRecord::Base.connection.select_one("SHOW TRIGGERS where `trigger` = '#{trigger_name(table_name, trigger_event)}'").present?
|
107
110
|
end
|
108
111
|
|
112
|
+
def has_stagehand_triggers?(table_name)
|
113
|
+
get_triggers(table_name).present?
|
114
|
+
end
|
115
|
+
|
109
116
|
def trigger_name(table_name, trigger_event)
|
110
117
|
"stagehand_#{trigger_event}_trigger_#{table_name}".downcase
|
111
118
|
end
|
119
|
+
|
120
|
+
def get_triggers(table_name = nil)
|
121
|
+
statement = <<~SQL
|
122
|
+
SHOW TRIGGERS WHERE `Trigger` LIKE 'stagehand_%'
|
123
|
+
SQL
|
124
|
+
statement << " AND `Table` LIKE #{ActiveRecord::Base.connection.quote(table_name)}" if table_name.present?
|
125
|
+
|
126
|
+
return ActiveRecord::Base.connection.select_all(statement)
|
127
|
+
end
|
112
128
|
end
|
113
129
|
end
|
@@ -33,7 +33,7 @@ module Stagehand
|
|
33
33
|
end
|
34
34
|
|
35
35
|
# Also include uncontained commit entries that matched
|
36
|
-
related_entries.concat(CommitEntry.uncontained.matching(entries + related_entries))
|
36
|
+
related_entries.concat(CommitEntry.uncontained.not_in_progress.matching(entries + related_entries))
|
37
37
|
related_entries.uniq!
|
38
38
|
|
39
39
|
return related_entries
|
@@ -61,8 +61,9 @@ module Stagehand
|
|
61
61
|
end
|
62
62
|
|
63
63
|
def self.infer_base_class(table_name)
|
64
|
-
classes = ActiveRecord::Base.descendants
|
65
|
-
classes.
|
64
|
+
classes = ActiveRecord::Base.descendants
|
65
|
+
classes.select! {|klass| klass.table_name == table_name }
|
66
|
+
classes.reject! {|klass| klass < Stagehand::Database::Probe }
|
66
67
|
return classes.first || table_name.classify.constantize.base_class # Try loading the class if it isn't loaded yet
|
67
68
|
rescue NameError
|
68
69
|
raise(IndeterminateRecordClass, "Can't determine class from table name: #{table_name}")
|
@@ -3,12 +3,22 @@ module Stagehand
|
|
3
3
|
module Model
|
4
4
|
extend ActiveSupport::Concern
|
5
5
|
|
6
|
+
included do
|
7
|
+
Stagehand::Configuration.staging_model_tables << table_name
|
8
|
+
end
|
9
|
+
|
6
10
|
class_methods do
|
11
|
+
def quoted_table_name
|
12
|
+
if connection.prefix_table_name_with_database?(table_name)
|
13
|
+
@prefixed_quoted_table_name ||= connection.quote_table_name(table_name)
|
14
|
+
else
|
15
|
+
super
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
7
19
|
def connection
|
8
20
|
if Configuration.ghost_mode?
|
9
21
|
super
|
10
|
-
elsif Stagehand::Database.connected_to_staging?
|
11
|
-
ActiveRecord::Base.connection
|
12
22
|
else
|
13
23
|
Stagehand::Database::StagingProbe.connection
|
14
24
|
end
|
@@ -133,7 +133,7 @@ module Stagehand
|
|
133
133
|
raise SchemaMismatch unless schemas_match?
|
134
134
|
|
135
135
|
run_sync_callbacks(entry, callbacks) do
|
136
|
-
Rails.logger.info "Synchronizing #{entry.table_name} #{entry.record_id}" if entry.content_operation?
|
136
|
+
Rails.logger.info "Synchronizing #{entry.table_name} #{entry.record_id} (#{entry.operation})" if entry.content_operation?
|
137
137
|
if Configuration.single_connection?
|
138
138
|
next # Avoid deadlocking if the databases are the same
|
139
139
|
elsif entry.delete_operation?
|
data/lib/stagehand/version.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: culturecode_stagehand
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.1.
|
4
|
+
version: 1.1.10
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Nicholas Jakobsen
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date:
|
12
|
+
date: 2021-11-16 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: rails
|
@@ -166,8 +166,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
166
166
|
- !ruby/object:Gem::Version
|
167
167
|
version: '0'
|
168
168
|
requirements: []
|
169
|
-
|
170
|
-
rubygems_version: 2.7.9
|
169
|
+
rubygems_version: 3.0.3
|
171
170
|
signing_key:
|
172
171
|
specification_version: 4
|
173
172
|
summary: Simplify the management of a sandbox database that can sync content to a
|