combinaut_stagehand 1.2.1
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 +7 -0
- data/MIT-LICENSE +20 -0
- data/Rakefile +20 -0
- data/lib/combinaut_stagehand.rb +2 -0
- data/lib/stagehand/active_record_extensions.rb +110 -0
- data/lib/stagehand/auditor/checklist_visualizer.rb +90 -0
- data/lib/stagehand/auditor.rb +90 -0
- data/lib/stagehand/cache.rb +12 -0
- data/lib/stagehand/configuration.rb +45 -0
- data/lib/stagehand/connection_adapter_extensions.rb +75 -0
- data/lib/stagehand/controller_extensions.rb +35 -0
- data/lib/stagehand/database.rb +197 -0
- data/lib/stagehand/engine.rb +26 -0
- data/lib/stagehand/key.rb +23 -0
- data/lib/stagehand/production/controller.rb +12 -0
- data/lib/stagehand/production.rb +132 -0
- data/lib/stagehand/schema/statements.rb +53 -0
- data/lib/stagehand/schema.rb +145 -0
- data/lib/stagehand/schema_extensions.rb +10 -0
- data/lib/stagehand/staging/checklist.rb +202 -0
- data/lib/stagehand/staging/commit.rb +168 -0
- data/lib/stagehand/staging/commit_entry.rb +180 -0
- data/lib/stagehand/staging/controller.rb +27 -0
- data/lib/stagehand/staging/model.rb +29 -0
- data/lib/stagehand/staging/synchronizer.rb +198 -0
- data/lib/stagehand/staging.rb +11 -0
- data/lib/stagehand/version.rb +3 -0
- data/lib/stagehand.rb +5 -0
- data/lib/tasks/stagehand_tasks.rake +43 -0
- metadata +166 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: b792d867277a6bc5d64f17e50d808241ddac020d6ad02684c00f1caec51e5f9e
|
|
4
|
+
data.tar.gz: 6affa9483ef3c301aadc83220c606223b3372588c012b2187af388ff1bbbcf57
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: d68610dfb55385f62ea8d3c202ef6e844db4cddef16129e1b57759d724041fdf6e450f5df8c390ec4d512affb7b78684004af87c7263d8a535aa56dc1b9c3490
|
|
7
|
+
data.tar.gz: 55d867ef57194d15c22ca576b798e9727b4204826efd83f2d0a6d92901252f8bb7ddee44e1ac068b8a1420972424667801383b809236b06579cf2c5188e7a447
|
data/MIT-LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Copyright 2016 Nicholas Jakobsen
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
4
|
+
a copy of this software and associated documentation files (the
|
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
9
|
+
the following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be
|
|
12
|
+
included in all copies or substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
begin
|
|
2
|
+
require 'bundler/setup'
|
|
3
|
+
rescue LoadError
|
|
4
|
+
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
APP_RAKEFILE = File.expand_path("../spec/internal/Rakefile", __FILE__)
|
|
8
|
+
load 'rails/tasks/engine.rake'
|
|
9
|
+
|
|
10
|
+
load 'rails/tasks/statistics.rake'
|
|
11
|
+
|
|
12
|
+
Bundler::GemHelper.install_tasks
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# Add Rspec tasks
|
|
16
|
+
require 'rspec/core/rake_task'
|
|
17
|
+
|
|
18
|
+
RSpec::Core::RakeTask.new(:spec)
|
|
19
|
+
|
|
20
|
+
task :default => :spec
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
require 'thread'
|
|
2
|
+
|
|
3
|
+
ActiveRecord::Base.class_eval do
|
|
4
|
+
# SYNC CALLBACKS
|
|
5
|
+
([self] + ActiveSupport::DescendantsTracker.descendants(self)).each do |klass|
|
|
6
|
+
klass.define_model_callbacks :sync, :sync_as_subject, :sync_as_affected
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
# SYNC STATUS
|
|
10
|
+
def self.inherited(subclass)
|
|
11
|
+
super
|
|
12
|
+
|
|
13
|
+
subclass.class_eval do
|
|
14
|
+
has_one :stagehand_unsynced_indicator,
|
|
15
|
+
lambda { where(:stagehand_commit_entries => {:table_name => subclass.table_name}).readonly },
|
|
16
|
+
:class_name => 'Stagehand::Staging::CommitEntry',
|
|
17
|
+
:foreign_key => :record_id
|
|
18
|
+
|
|
19
|
+
has_one :stagehand_unsynced_commit_indicator,
|
|
20
|
+
lambda { where(:stagehand_commit_entries => {:table_name => subclass.table_name}).where.not(commit_id: nil).readonly },
|
|
21
|
+
:class_name => 'Stagehand::Staging::CommitEntry',
|
|
22
|
+
:foreign_key => :record_id
|
|
23
|
+
|
|
24
|
+
def synced?
|
|
25
|
+
stagehand_unsynced_indicator.blank?
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def synced_all_commits?
|
|
29
|
+
stagehand_unsynced_commit_indicator.blank?
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# SCHEMA
|
|
35
|
+
delegate :has_stagehand?, to: :class
|
|
36
|
+
def self.has_stagehand?
|
|
37
|
+
@has_stagehand = Stagehand::Schema.has_stagehand?(table_name) unless defined?(@has_stagehand)
|
|
38
|
+
return @has_stagehand
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# MULTITHREADED CONNECTION HANDLING
|
|
42
|
+
|
|
43
|
+
class_attribute :stagehand_threadsafe_connections
|
|
44
|
+
self.stagehand_threadsafe_connections = true
|
|
45
|
+
|
|
46
|
+
# The original implementation of remove_connection uses @connection_specification_name, which is shared across Threads.
|
|
47
|
+
# We need to pass in the connection that model in the current thread is using if we call remove_connection.
|
|
48
|
+
def self.remove_connection(name = StagehandConnectionMap.get(self))
|
|
49
|
+
return super unless stagehand_threadsafe_connections
|
|
50
|
+
|
|
51
|
+
StagehandConnectionMap.set(self, nil)
|
|
52
|
+
super
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def self.connection_specification_name=(connection_name)
|
|
56
|
+
return super unless stagehand_threadsafe_connections
|
|
57
|
+
|
|
58
|
+
# ActiveRecord sets the connection pool to 'primary' by default, so we want to reuse that connection for staging
|
|
59
|
+
# in order to avoid using a different connection pool after our first swap back to the staging connection.
|
|
60
|
+
connection_name == 'primary' if connection_name == Stagehand::Configuration.staging_connection_name
|
|
61
|
+
|
|
62
|
+
StagehandConnectionMap.set(self, connection_name)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def self.connection_specification_name
|
|
66
|
+
return super unless stagehand_threadsafe_connections
|
|
67
|
+
|
|
68
|
+
StagehandConnectionMap.get(self) || super
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Keep track of the current connection name per-model, per-thread so multithreaded webservers don't overwrite it
|
|
72
|
+
module StagehandConnectionMap
|
|
73
|
+
def self.set(klass, connection_name)
|
|
74
|
+
current_map[klass.name] = connection_name
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def self.get(klass)
|
|
78
|
+
current_map[klass.name]
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def self.current_map
|
|
82
|
+
map = Thread.current.thread_variable_get('StagehandConnectionMap')
|
|
83
|
+
map = Thread.current.thread_variable_set('StagehandConnectionMap', Concurrent::Hash.new) unless map
|
|
84
|
+
return map
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
module StagehandAssociationReflection
|
|
90
|
+
# SOURCE: https://github.com/rails/rails/blob/a4581b53aae93a8dd3205abae0630398cbce9204/activerecord/lib/active_record/reflection.rb#L429
|
|
91
|
+
def initialize(*)
|
|
92
|
+
super
|
|
93
|
+
@association_scope_cache = StagehandAssociationScopeCache.new
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Ensure the association query statements are cached separately for the staging and production connections or else
|
|
97
|
+
# queries for Staging Models may cache the database name for the wrong connection.
|
|
98
|
+
class StagehandAssociationScopeCache < Delegator
|
|
99
|
+
def initialize
|
|
100
|
+
@staging_cache = Concurrent::Map.new
|
|
101
|
+
@production_cache = Concurrent::Map.new
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def __getobj__
|
|
105
|
+
Stagehand::Database.connected_to_production? ? @production_cache : @staging_cache
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
ActiveRecord::Reflection::AssociationReflection.prepend(StagehandAssociationReflection)
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
require 'graphviz'
|
|
2
|
+
|
|
3
|
+
module Stagehand
|
|
4
|
+
module Auditor
|
|
5
|
+
class ChecklistVisualizer
|
|
6
|
+
def initialize(checklist, show_all_commits: false)
|
|
7
|
+
entries = checklist.affected_entries.select(&:commit_id)
|
|
8
|
+
|
|
9
|
+
@graph = GraphViz.new( :G, :type => :graph )
|
|
10
|
+
@commits = Hash.new {|hash, commit_id| hash[commit_id] = Stagehand::Staging::CommitEntry.find(commit_id) }
|
|
11
|
+
nodes = Hash.new
|
|
12
|
+
edges = []
|
|
13
|
+
|
|
14
|
+
# Detect edges
|
|
15
|
+
entries.group_by(&:key).each_value do |entries|
|
|
16
|
+
entries.combination(2).each do |entry_a, entry_b|
|
|
17
|
+
current_edge = [entry_a, entry_b]
|
|
18
|
+
next if edges.detect {|edge| edge.sort == current_edge.sort }
|
|
19
|
+
next if same_node?(entry_a, entry_b)
|
|
20
|
+
next if same_subject?(entry_a, entry_b)
|
|
21
|
+
edges << current_edge
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Create Subgraph nodes for commits with connections to other subjects
|
|
26
|
+
entries = edges.flatten.uniq unless show_all_commits
|
|
27
|
+
entries.group_by {|entry| commit_subject(entry) || entry.commit_id }.each do |group, entries|
|
|
28
|
+
subject = commit_subject(entries.first)
|
|
29
|
+
subgraph = create_subgraph(subject, @graph, id: group)
|
|
30
|
+
entries.each do |entry|
|
|
31
|
+
nodes[entry] = create_node(entry, subgraph)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Create deduplicate edge data in case multiple entries for the same record were part of a single commit
|
|
36
|
+
edges = edges.map do |entry_a, entry_b|
|
|
37
|
+
[[nodes[entry_a], nodes[entry_b]], label: edge_label(entry_a, entry_b)]
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Create edges
|
|
41
|
+
edges.uniq.each do |(node_a, node_b), options|
|
|
42
|
+
create_edge(node_a, node_b, options)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def output(file_name, format: File.extname(file_name)[1..-1])
|
|
47
|
+
@graph.output(format => file_name)
|
|
48
|
+
File.open(file_name)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
def create_subgraph(subject, graph, id: subject)
|
|
54
|
+
graph.add_graph("cluster_#{id}", :label => subject, :style => :filled, :color => :lightgrey)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def create_edge(node_a, node_b, options = {})
|
|
58
|
+
@graph.add_edges(node_a, node_b, options.reverse_merge(:color => :red, :fontcolor => :red))
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def create_node(entry, graph)
|
|
62
|
+
graph.add_nodes(node_name(entry), :shape => :rect, :style => :filled, :fillcolor => :white)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def edge_label(entry_a, entry_b)
|
|
66
|
+
pretty_key(entry_a)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def same_node?(entry_a, entry_b)
|
|
70
|
+
node_name(entry_a) == node_name(entry_b)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def same_subject?(entry_a, entry_b)
|
|
74
|
+
commit_subject(entry_a) == commit_subject(entry_b) && commit_subject(entry_a)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def commit_subject(entry)
|
|
78
|
+
pretty_key(@commits[entry.commit_id])
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def node_name(entry)
|
|
82
|
+
"Commit #{entry.commit_id}"
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def pretty_key(entry)
|
|
86
|
+
"#{entry.table_name.classify} #{entry.record_id}" if entry.record_id && entry.table_name
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
require 'stagehand/auditor/checklist_visualizer'
|
|
2
|
+
|
|
3
|
+
module Stagehand
|
|
4
|
+
module Auditor
|
|
5
|
+
extend self
|
|
6
|
+
|
|
7
|
+
def incomplete_commits
|
|
8
|
+
incomplete = []
|
|
9
|
+
|
|
10
|
+
incomplete_commit_ids.each do |commit_id|
|
|
11
|
+
incomplete << [commit_id, Staging::CommitEntry.where(:commit_id => commit_id)]
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
return incomplete.to_h
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def mismatched_records(options = {})
|
|
18
|
+
output = {}
|
|
19
|
+
|
|
20
|
+
tables = options[:tables] || Database.staging_connection.tables.select {|table_name| Schema::has_stagehand?(table_name) }
|
|
21
|
+
Array(tables).each do |table_name|
|
|
22
|
+
print "\nChecking #{table_name} "
|
|
23
|
+
mismatched = Hash.new {|k,v| k[v] = {} }
|
|
24
|
+
limit = 1000
|
|
25
|
+
|
|
26
|
+
min_id = [
|
|
27
|
+
Database.staging_connection.select_value("SELECT MIN(id) FROM #{table_name}").to_i,
|
|
28
|
+
Database.production_connection.select_value("SELECT MIN(id) FROM #{table_name}").to_i
|
|
29
|
+
].min
|
|
30
|
+
|
|
31
|
+
index = min_id / limit
|
|
32
|
+
|
|
33
|
+
max_id = [
|
|
34
|
+
Database.staging_connection.select_value("SELECT MAX(id) FROM #{table_name}").to_i,
|
|
35
|
+
Database.production_connection.select_value("SELECT MAX(id) FROM #{table_name}").to_i
|
|
36
|
+
].max
|
|
37
|
+
|
|
38
|
+
loop do
|
|
39
|
+
production_records = Database.production_connection.select_all("SELECT * FROM #{table_name} WHERE id BETWEEN #{limit * index} AND #{limit * (index + 1)}")
|
|
40
|
+
staging_records = Database.staging_connection.select_all("SELECT * FROM #{table_name} WHERE id BETWEEN #{limit * index} AND #{limit * (index + 1)}")
|
|
41
|
+
id_column = production_records.columns.index('id')
|
|
42
|
+
|
|
43
|
+
production_differences = production_records.rows - staging_records.rows
|
|
44
|
+
staging_differences = staging_records.rows - production_records.rows
|
|
45
|
+
|
|
46
|
+
production_differences.each do |row|
|
|
47
|
+
id = row[id_column]
|
|
48
|
+
mismatched[id][:production] = row
|
|
49
|
+
end
|
|
50
|
+
staging_differences.each do |row|
|
|
51
|
+
id = row[id_column]
|
|
52
|
+
mismatched[id][:staging] = row
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
if production_differences.present? || staging_differences.present?
|
|
56
|
+
print '!'
|
|
57
|
+
else
|
|
58
|
+
print '.'
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
index += 1
|
|
62
|
+
|
|
63
|
+
break if index * limit > max_id
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
if mismatched.present?
|
|
67
|
+
print " #{mismatched.count} mismatched"
|
|
68
|
+
output[table_name] = mismatched
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
return output
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def visualize(subject, output_file_name, options = {})
|
|
76
|
+
visualize_checklist(Staging::Checklist.new(subject), output_file_name, options)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def visualize_checklist(checklist, output_file_name, options = {})
|
|
80
|
+
ChecklistVisualizer.new(checklist, options).output(output_file_name)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
private
|
|
84
|
+
|
|
85
|
+
# Commit that is missing a start or end operation
|
|
86
|
+
def incomplete_commit_ids
|
|
87
|
+
Staging::CommitEntry.control_operations.group(:commit_id).having("count(*) != 2").pluck("MIN(commit_id)")
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
module Stagehand
|
|
2
|
+
extend self
|
|
3
|
+
|
|
4
|
+
def configuration
|
|
5
|
+
yield Configuration if block_given?
|
|
6
|
+
Configuration
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
module Configuration
|
|
10
|
+
extend self
|
|
11
|
+
|
|
12
|
+
mattr_accessor :checklist_confirmation_filter, :checklist_association_filter, :checklist_relation_filter, :staging_model_tables, :ignored_columns
|
|
13
|
+
self.staging_model_tables = Set.new
|
|
14
|
+
self.ignored_columns = HashWithIndifferentAccess.new
|
|
15
|
+
|
|
16
|
+
def staging_connection_name
|
|
17
|
+
Rails.env.to_sym
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def production_connection_name
|
|
21
|
+
Rails.configuration.x.stagehand.production_connection_name || Rails.env.to_sym
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def ghost_mode?
|
|
25
|
+
!!Rails.configuration.x.stagehand.ghost_mode
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Allow unsynchronized writes directly to the production database? A warning will be logged if set to true.
|
|
29
|
+
def allow_unsynced_production_writes?
|
|
30
|
+
!!Rails.configuration.x.stagehand.allow_unsynced_production_writes
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Returns true if the production and staging connections are the same.
|
|
34
|
+
# Use case: Front-end devs may not have a second database set up as they are only concerned with the front end
|
|
35
|
+
def single_connection?
|
|
36
|
+
staging_connection_name == production_connection_name
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Columns not to copy to the production database
|
|
40
|
+
# e.g. table_name => [column, column, ...]
|
|
41
|
+
def self.ignored_columns=(hash)
|
|
42
|
+
super HashWithIndifferentAccess.new(hash)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
module Stagehand
|
|
2
|
+
module Connection
|
|
3
|
+
def self.with_production_writes(&block)
|
|
4
|
+
state = allow_unsynced_production_writes?
|
|
5
|
+
allow_unsynced_production_writes!(true)
|
|
6
|
+
return block.call
|
|
7
|
+
ensure
|
|
8
|
+
allow_unsynced_production_writes!(state)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def self.allow_unsynced_production_writes!(state = true)
|
|
12
|
+
Thread.current.thread_variable_set(:stagehand_allow_unsynced_production_writes, state)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def self.allow_unsynced_production_writes?
|
|
16
|
+
!!Thread.current.thread_variable_get(:stagehand_allow_unsynced_production_writes)
|
|
17
|
+
end
|
|
18
|
+
|
|
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
|
+
|
|
36
|
+
def exec_insert(sql, *)
|
|
37
|
+
handle_readonly_writes!(sql)
|
|
38
|
+
super
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def exec_update(sql, *)
|
|
42
|
+
handle_readonly_writes!(sql)
|
|
43
|
+
super
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def exec_delete(sql, *)
|
|
47
|
+
handle_readonly_writes!(sql)
|
|
48
|
+
super
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
def write_access?
|
|
54
|
+
Configuration.single_connection? || @config[:database] == Database.staging_database_name || Connection.allow_unsynced_production_writes?
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def handle_readonly_writes!(sql)
|
|
58
|
+
if write_access?
|
|
59
|
+
return
|
|
60
|
+
elsif Configuration.allow_unsynced_production_writes?
|
|
61
|
+
Rails.logger.warn "Writing directly to #{@config[:database]} database using readonly connection"
|
|
62
|
+
else
|
|
63
|
+
raise(UnsyncedProductionWrite, "Attempted to write directly to #{@config[:database]} database using readonly connection: #{sql}")
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# EXCEPTIONS
|
|
71
|
+
|
|
72
|
+
class UnsyncedProductionWrite < StandardError; end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
ActiveRecord::Base.connection.class.prepend(Stagehand::Connection::AdapterExtensions)
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
module Stagehand
|
|
2
|
+
module ControllerExtensions
|
|
3
|
+
extend ActiveSupport::Concern
|
|
4
|
+
|
|
5
|
+
class_methods do
|
|
6
|
+
def use_staging_database(options = {})
|
|
7
|
+
skip_around_action :use_production_database, raise: false, **options
|
|
8
|
+
prepend_around_action :use_staging_database, options
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def use_production_database(options = {})
|
|
12
|
+
skip_around_action :use_staging_database, raise: false, **options
|
|
13
|
+
prepend_around_action :use_production_database, options
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def use_staging_database(&block)
|
|
20
|
+
use_database(Configuration.staging_connection_name, &block)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def use_production_database(&block)
|
|
24
|
+
use_database(Configuration.production_connection_name, &block)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def use_database(connection_name, &block)
|
|
28
|
+
if Configuration.ghost_mode?
|
|
29
|
+
block.call
|
|
30
|
+
else
|
|
31
|
+
Database.with_connection(connection_name, &block)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|