culturecode_stagehand 1.1.5 → 1.1.10
Sign up to get free protection for your applications and to get access to all the features.
- 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
|