culturecode_stagehand 0.1.4 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 72f51c6303b63f139ef0af10cd15021907df8a1d
4
- data.tar.gz: 104d5725ec17b18f858ee42d02cdb08be33406fd
3
+ metadata.gz: 80028dbb24f83c131a67022d24ab3c84dc64edb7
4
+ data.tar.gz: c307e846e18fafc50f6987e9208b2dd6601d753c
5
5
  SHA512:
6
- metadata.gz: ce29783f1db29d1fbe6575d35a9335cca2ae38eb3cefa5f87f1403bb01faa7afbe9660a9d8823e687bd4e77b6035409118756a51e55d9399daa9f2e82c53e911
7
- data.tar.gz: b23952031a353d566de6c590d55ca5be648e3b0cd868b77b204139326c43def85065cbb8467616461321257b00dca01fa0a4af28a2f533d2febbb728958df2bc
6
+ metadata.gz: 8e1f6ceba25e85f6dd598b2b6ce52ca9c36e09af45ed327a7ef56a5799c8a57b2e9d76f2d8aa7d52c32783de0008c78adfc8900847127a15bea19cb6c6d76c32
7
+ data.tar.gz: d1c21179ad1014dd99208ca1936ab57306aaa76a6ecb070d3fb4306dd606649afc632f5005972388e767ebfddd5586820a917000f79690507361145dbf66c562
@@ -0,0 +1,12 @@
1
+ module Stagehand
2
+ module Cache
3
+ def cache(key, &block)
4
+ @cache ||= {}
5
+ if @cache.key?(key)
6
+ @cache[key]
7
+ else
8
+ @cache[key] = block.call
9
+ end
10
+ end
11
+ end
12
+ end
@@ -19,6 +19,12 @@ module Stagehand
19
19
  def ghost_mode?
20
20
  !!Rails.configuration.x.stagehand.ghost_mode
21
21
  end
22
+
23
+ # Returns true if the production and staging connections are the same.
24
+ # Use case: Front-end devs may not have a second database set up as they are only concerned with the front end
25
+ def single_connection?
26
+ staging_connection_name == production_connection_name
27
+ end
22
28
  end
23
29
 
24
30
  # EXCEPTIONS
@@ -1,30 +1,24 @@
1
1
  module Stagehand
2
- module Key
2
+ module Database
3
3
  extend self
4
4
 
5
- def generate(staging_record, options = {})
6
- case staging_record
7
- when Staging::CommitEntry
8
- id = staging_record.record_id || staging_record.id
9
- table_name = staging_record.table_name || staging_record.class.table_name
10
- when ActiveRecord::Base
11
- id = staging_record.id
12
- table_name = staging_record.class.table_name
13
- else
14
- id = staging_record
15
- table_name = options[:table_name]
16
- end
5
+ @@connection_name_stack = [Rails.env.to_sym]
17
6
 
18
- raise 'Invalid input' unless table_name && id
7
+ def connected_to_production?
8
+ current_connection_name == Configuration.production_connection_name
9
+ end
19
10
 
20
- return [table_name, id]
11
+ def connected_to_staging?
12
+ current_connection_name == Configuration.staging_connection_name
21
13
  end
22
- end
23
14
 
24
- module Database
25
- extend self
15
+ def staging_connection
16
+ StagingProbe.connection
17
+ end
26
18
 
27
- @@connection_name_stack = [Rails.env.to_sym]
19
+ def production_connection
20
+ ProductionProbe.connection
21
+ end
28
22
 
29
23
  def with_connection(connection_name)
30
24
  different = !Configuration.ghost_mode? && current_connection_name != connection_name.to_sym
@@ -53,5 +47,28 @@ module Stagehand
53
47
  def current_connection_name
54
48
  @@connection_name_stack.last
55
49
  end
50
+
51
+
52
+ # CLASSES
53
+
54
+ class StagingProbe < ActiveRecord::Base
55
+ self.abstract_class = true
56
+
57
+ def self.init_connection
58
+ establish_connection(Configuration.staging_connection_name)
59
+ end
60
+
61
+ init_connection
62
+ end
63
+
64
+ class ProductionProbe < ActiveRecord::Base
65
+ self.abstract_class = true
66
+
67
+ def self.init_connection
68
+ establish_connection(Configuration.production_connection_name)
69
+ end
70
+
71
+ init_connection
72
+ end
56
73
  end
57
74
  end
@@ -9,11 +9,13 @@ module Stagehand
9
9
  # These require the rails application to be intialized because configuration variables are used
10
10
  initializer "stagehand.load_modules" do
11
11
  require "stagehand/configuration"
12
+ require "stagehand/cache"
13
+ require "stagehand/key"
14
+ require "stagehand/database"
12
15
  require "stagehand/controller_extensions"
13
16
  require "stagehand/staging"
14
17
  require "stagehand/schema"
15
18
  require "stagehand/production"
16
- require "stagehand/helpers"
17
19
  end
18
20
  end
19
21
  end
@@ -0,0 +1,23 @@
1
+ module Stagehand
2
+ module Key
3
+ extend self
4
+
5
+ def generate(staging_record, options = {})
6
+ case staging_record
7
+ when Staging::CommitEntry
8
+ id = staging_record.record_id || staging_record.id
9
+ table_name = staging_record.table_name || staging_record.class.table_name
10
+ when ActiveRecord::Base
11
+ id = staging_record.id
12
+ table_name = staging_record.class.table_name
13
+ else
14
+ id = staging_record
15
+ table_name = options[:table_name]
16
+ end
17
+
18
+ raise 'Invalid input' unless table_name && id
19
+
20
+ return [table_name, id]
21
+ end
22
+ end
23
+ end
@@ -56,7 +56,6 @@ module Stagehand
56
56
 
57
57
  def prepare_to_modify(table_name)
58
58
  raise "Can't prepare to modify production records without knowning the table_name" unless table_name.present?
59
- Record.establish_connection(Configuration.production_connection_name) and @connection_established = true unless @connection_established
60
59
  Record.table_name = table_name
61
60
  end
62
61
 
@@ -71,7 +70,7 @@ module Stagehand
71
70
 
72
71
  # CLASSES
73
72
 
74
- class Record < ActiveRecord::Base
73
+ class Record < Stagehand::Database::ProductionProbe
75
74
  self.record_timestamps = false
76
75
  end
77
76
  end
@@ -5,9 +5,11 @@ module Stagehand
5
5
  def create_table(table_name, options = {})
6
6
  super
7
7
 
8
- unless options.symbolize_keys[:stagehand] == false || UNTRACKED_TABLES.include?(table_name)
9
- Schema.add_stagehand! :only => table_name
10
- end
8
+ return if options.symbolize_keys[:stagehand] == false
9
+ return if UNTRACKED_TABLES.include?(table_name)
10
+ return if Database.connected_to_production?
11
+
12
+ Schema.add_stagehand! :only => table_name
11
13
  end
12
14
  end
13
15
  end
@@ -2,9 +2,11 @@ require "stagehand/schema/statements"
2
2
 
3
3
  module Stagehand
4
4
  module Schema
5
+ extend self
6
+
5
7
  UNTRACKED_TABLES = ['schema_migrations', Stagehand::Staging::CommitEntry.table_name]
6
8
 
7
- def self.init_stagehand!(options = {})
9
+ def init_stagehand!(options = {})
8
10
  ActiveRecord::Schema.define do
9
11
  create_table :stagehand_commit_entries do |t|
10
12
  t.integer :record_id
@@ -28,44 +30,49 @@ module Stagehand
28
30
  add_stagehand!(options)
29
31
  end
30
32
 
31
- def self.add_stagehand!(options = {})
33
+ def add_stagehand!(options = {})
32
34
  ActiveRecord::Schema.define do
33
-
34
35
  table_names = ActiveRecord::Base.connection.tables
35
36
  table_names -= UNTRACKED_TABLES
36
37
  table_names -= Array(options[:except]).collect(&:to_s)
37
38
  table_names &= Array(options[:only]).collect(&:to_s) if options[:only].present?
38
39
 
39
40
  table_names.each do |table_name|
40
- Stagehand::Schema.drop_trigger(table_name, 'insert')
41
- Stagehand::Schema.drop_trigger(table_name, 'update')
42
- Stagehand::Schema.drop_trigger(table_name, 'delete')
43
-
44
- Stagehand::Schema.create_trigger(table_name, 'insert', 'NEW')
45
- Stagehand::Schema.create_trigger(table_name, 'update', 'NEW')
46
- Stagehand::Schema.create_trigger(table_name, 'delete', 'OLD')
41
+ Stagehand::Schema.send :create_trigger, table_name, 'insert', 'NEW'
42
+ Stagehand::Schema.send :create_trigger, table_name, 'update', 'NEW'
43
+ Stagehand::Schema.send :create_trigger, table_name, 'delete', 'OLD'
47
44
  end
48
45
  end
49
46
  end
50
47
 
51
- def self.remove_stagehand!(options = {})
48
+ def remove_stagehand!(options = {})
52
49
  ActiveRecord::Schema.define do
53
50
  table_names = ActiveRecord::Base.connection.tables
54
51
  table_names &= Array(options[:only]).collect(&:to_s) if options[:only].present?
55
52
 
56
53
  table_names.each do |table_name|
57
- Stagehand::Schema.drop_trigger(table_name, 'insert')
58
- Stagehand::Schema.drop_trigger(table_name, 'update')
59
- Stagehand::Schema.drop_trigger(table_name, 'delete')
54
+ Stagehand::Schema.send :drop_trigger, table_name, 'insert'
55
+ Stagehand::Schema.send :drop_trigger, table_name, 'update'
56
+ Stagehand::Schema.send :drop_trigger, table_name, 'delete'
60
57
  end
61
58
 
62
59
  drop_table :stagehand_commit_entries unless options[:only].present?
63
60
  end
64
61
  end
65
62
 
63
+ def has_stagehand?(table_name = nil)
64
+ if table_name
65
+ trigger_exists?(table_name, 'insert')
66
+ else
67
+ ActiveRecord::Base.Connection.table_exists?(Stagehand::Staging::CommitEntry.table_name)
68
+ end
69
+ end
70
+
66
71
  private
67
72
 
68
- def self.create_trigger(table_name, trigger_action, record)
73
+ def create_trigger(table_name, trigger_action, record)
74
+ return if trigger_exists?(table_name, trigger_action)
75
+
69
76
  ActiveRecord::Base.connection.execute("
70
77
  CREATE TRIGGER #{trigger_name(table_name, trigger_action)} AFTER #{trigger_action.upcase} ON #{table_name}
71
78
  FOR EACH ROW
@@ -76,12 +83,16 @@ module Stagehand
76
83
  ")
77
84
  end
78
85
 
79
- def self.drop_trigger(table_name, trigger_action)
86
+ def drop_trigger(table_name, trigger_action)
80
87
  ActiveRecord::Base.connection.execute("DROP TRIGGER IF EXISTS #{trigger_name(table_name, trigger_action)};")
81
88
  end
82
89
 
83
- def self.trigger_name(table_name, trigger_action)
84
- "stagehand_#{trigger_action}_trigger_#{table_name}"
90
+ def trigger_exists?(table_name, trigger_action)
91
+ ActiveRecord::Base.connection.select_one("SHOW TRIGGERS where `trigger` = '#{trigger_name(table_name, trigger_action)}'").present?
92
+ end
93
+
94
+ def trigger_name(table_name, trigger_action)
95
+ "stagehand_#{trigger_action}_trigger_#{table_name}".downcase
85
96
  end
86
97
  end
87
98
  end
@@ -0,0 +1,64 @@
1
+ module Stagehand
2
+ module Staging
3
+ module Auditor
4
+ extend self
5
+
6
+ def incomplete_commits
7
+ incomplete = []
8
+
9
+ incomplete_start_operations.each do |start_operation|
10
+ entries = records_until_match(start_operation, :asc, :operation => CommitEntry::START_OPERATION).to_a
11
+ incomplete << [start_operation.id, entries]
12
+ end
13
+
14
+ incomplete_end_operations.each do |end_operation|
15
+ entries = records_through_match(end_operation, :desc, :operation => CommitEntry::START_OPERATION).to_a
16
+ incomplete << [entries.last.id, entries]
17
+ end
18
+
19
+ return incomplete.to_h
20
+ end
21
+
22
+ private
23
+
24
+ # Incomplete End Operation that are not the last entry in their session
25
+ def incomplete_end_operations
26
+ last_entry_per_session = CommitEntry.group(:session).select('MAX(id) AS id')
27
+ return CommitEntry.uncontained.end_operations.where.not(:id => last_entry_per_session)
28
+ end
29
+
30
+ # Incomplete Start on the same session as a subsequent start operation
31
+ def incomplete_start_operations
32
+ last_start_entry_per_session = CommitEntry.start_operations.group(:session).select('MAX(id) AS id')
33
+ return CommitEntry.uncontained.start_operations.where.not(:id => last_start_entry_per_session)
34
+ end
35
+
36
+ def records_until_match(start_entry, direction, match_attributes)
37
+ records_through_match(start_entry, direction, match_attributes)[0..-2]
38
+ end
39
+
40
+ def records_through_match(start_entry, direction, match_attributes)
41
+ last_entry = next_match(start_entry, direction, match_attributes)
42
+ return records_from(start_entry, direction).where.not("id #{exclusive_comparator(direction)} ?", last_entry)
43
+ end
44
+
45
+ def next_match(start_entry, direction, match_attributes)
46
+ records_from(start_entry, direction).where.not(:id => start_entry.id).where(match_attributes).first
47
+ end
48
+
49
+ def records_from(start_entry, direction)
50
+ scope = CommitEntry.where(:session => start_entry.session).where("id #{comparator(direction)} ?", start_entry.id)
51
+ scope = scope.reverse_order if direction == :desc
52
+ return scope
53
+ end
54
+
55
+ def comparator(direction)
56
+ exclusive_comparator(direction) + '='
57
+ end
58
+
59
+ def exclusive_comparator(direction)
60
+ direction == :asc ? '>' : '<'
61
+ end
62
+ end
63
+ end
64
+ end
@@ -1,6 +1,9 @@
1
1
  module Stagehand
2
2
  module Staging
3
3
  class Checklist
4
+ extend Cache
5
+ include Cache
6
+
4
7
  def self.related_commits(commit)
5
8
  Commit.find(related_commit_ids(commit))
6
9
  end
@@ -34,10 +37,67 @@ module Stagehand
34
37
  return related_entries
35
38
  end
36
39
 
40
+ def self.associated_records(entries)
41
+ records = preload_records(compact_entries(entries)).select(&:record).flat_map do |entry|
42
+ associated_associations(entry.record_class).flat_map do |association|
43
+ entry.record.send(association)
44
+ end
45
+ end
46
+
47
+ records.uniq!
48
+ records.compact!
49
+ records.select! {|record| stagehand_class?(record.class) }
50
+
51
+ return records
52
+ end
53
+
54
+ # Returns a list of entries that only includes a single entry for each record.
55
+ # The type of entry chosen prioritizes creates over updates, and deletes over creates.
56
+ def self.compact_entries(entries)
57
+ compact_entries = group_entries(entries)
58
+ compact_entries = compact_entries[:delete] + compact_entries[:insert] + compact_entries[:update]
59
+ compact_entries.uniq!(&:key)
60
+
61
+ return compact_entries
62
+ end
63
+
64
+ # Groups entries by their operation
65
+ def self.group_entries(entries)
66
+ group_entries = Hash.new {|h,k| h[k] = [] }
67
+ group_entries.merge! entries.group_by(&:operation).symbolize_keys!
68
+
69
+ return group_entries
70
+ end
71
+
72
+ def self.preload_records(entries)
73
+ entries.group_by(&:table_name).each do |table_name, group_entries|
74
+ klass = CommitEntry.infer_class(table_name)
75
+ records = klass.where(:id => group_entries.collect(&:record_id))
76
+ records = records.includes(associated_associations(klass))
77
+ records_by_id = records.collect {|r| [r.id, r] }.to_h
78
+ group_entries.each do |entry|
79
+ entry.record = records_by_id[entry.record_id]
80
+ end
81
+ end
82
+
83
+ return entries
84
+ end
85
+
86
+ private
87
+
88
+ def self.associated_associations(klass)
89
+ cache("#{klass.name}_associated_associations") { klass.reflect_on_all_associations(:belongs_to).collect(&:name) }
90
+ end
91
+
92
+ def self.stagehand_class?(klass)
93
+ cache("#{klass.name}_stagehand_class?") { Schema.has_stagehand?(klass.table_name) }
94
+ end
95
+
96
+ public
97
+
37
98
  def initialize(subject, &confirmation_filter)
38
99
  @subject = subject
39
100
  @confirmation_filter = confirmation_filter
40
- @cache = {}
41
101
  affected_entries # Init the affected_entries changes can be rolled back without affecting the checklist
42
102
  end
43
103
 
@@ -64,7 +124,7 @@ module Stagehand
64
124
  end
65
125
 
66
126
  def syncing_entries
67
- cache(:syncing_entries) { compact_entries(affected_entries) }
127
+ cache(:syncing_entries) { self.class.compact_entries(affected_entries) }
68
128
  end
69
129
 
70
130
  def affected_records
@@ -72,7 +132,13 @@ module Stagehand
72
132
  end
73
133
 
74
134
  def affected_entries
75
- cache(:affected_entries) { self.class.related_entries(@subject) }
135
+ cache(:affected_entries) do
136
+ related = self.class.related_entries(@subject)
137
+ associated = self.class.associated_records(related)
138
+ associated_related = self.class.related_entries(associated)
139
+
140
+ related + associated_related
141
+ end
76
142
  end
77
143
 
78
144
  private
@@ -89,59 +155,15 @@ module Stagehand
89
155
  # Don't need to confirm entries that were not part of a commit
90
156
  entries = entries.select(&:commit_id)
91
157
 
92
- entries = compact_entries(entries)
93
- entries = preload_records(entries)
158
+ entries = self.class.compact_entries(entries)
94
159
  entries = filter_entries(entries)
95
- entries = group_entries(entries)
160
+ entries = self.class.group_entries(entries)
96
161
  end
97
162
  end
98
163
 
99
164
  def filter_entries(entries)
100
165
  @confirmation_filter ? entries.select {|entry| @confirmation_filter.call(entry.record) } : entries
101
166
  end
102
-
103
- # Returns a list of entries that only includes a single entry for each record.
104
- # The type of entry chosen prioritizes creates over updates, and deletes over creates.
105
- def compact_entries(entries)
106
- compact_entries = group_entries(entries)
107
- compact_entries = compact_entries[:delete] + compact_entries[:insert] + compact_entries[:update]
108
- compact_entries.uniq!(&:key)
109
-
110
- return compact_entries
111
- end
112
-
113
- # Groups entries by their operation
114
- def group_entries(entries)
115
- group_entries = Hash.new {|h,k| h[k] = [] }
116
- group_entries.merge! entries.group_by(&:operation).symbolize_keys!
117
-
118
- return group_entries
119
- end
120
-
121
- def preload_records(entries)
122
- entries.group_by(&:table_name).each do |table_name, group_entries|
123
- klass = CommitEntry.infer_class(table_name)
124
- records = klass.where(:id => group_entries.collect(&:record_id))
125
- records_by_id = records.collect {|r| [r.id, r] }.to_h
126
- group_entries.each do |entry|
127
- entry.record = records_by_id[entry.record_id]
128
- end
129
- end
130
-
131
- return entries
132
- end
133
-
134
- def cache(key, &block)
135
- if @cache.key?(key)
136
- @cache[key]
137
- else
138
- @cache[key] = block.call
139
- end
140
- end
141
-
142
- def clear_cache
143
- @cache.clear
144
- end
145
167
  end
146
168
  end
147
169
  end
@@ -40,6 +40,10 @@ module Stagehand
40
40
  end
41
41
 
42
42
  def sync_entries(entries)
43
+ return 0 if Configuration.single_connection? # Avoid deadlocking if the databases are the same
44
+
45
+ raise SchemaMismatch unless schemas_match?
46
+
43
47
  ActiveRecord::Base.transaction do
44
48
  entries.each do |entry|
45
49
  Rails.logger.info "Synchronizing #{entry.table_name} #{entry.record_id}"
@@ -62,10 +66,18 @@ module Stagehand
62
66
  CommitEntry.select('MAX(id) AS id').content_operations.not_in_progress.group('record_id, table_name').having('count(commit_id) = 0'))
63
67
  end
64
68
  end
69
+
70
+ def schemas_match?
71
+ versions_scope = ActiveRecord::SchemaMigration.order(:version)
72
+ staging_versions = Stagehand::Database.staging_connection.select_values(versions_scope)
73
+ production_versions = Stagehand::Database.production_connection.select_values(versions_scope)
74
+ return staging_versions == production_versions
75
+ end
65
76
  end
66
77
  end
67
78
 
68
79
  # EXCEPTIONS
69
80
 
70
81
  class SyncBlockRequired < StandardError; end
82
+ class SchemaMismatch < StandardError; end
71
83
  end
@@ -4,6 +4,7 @@ require 'stagehand/staging/checklist'
4
4
  require 'stagehand/staging/controller'
5
5
  require 'stagehand/staging/model'
6
6
  require 'stagehand/staging/synchronizer'
7
+ require 'stagehand/staging/auditor'
7
8
 
8
9
  module Stagehand
9
10
  module Staging
@@ -1,3 +1,3 @@
1
1
  module Stagehand
2
- VERSION = "0.1.4"
2
+ VERSION = "0.2.0"
3
3
  end
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: 0.1.4
4
+ version: 0.2.0
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: 2016-04-01 00:00:00.000000000 Z
12
+ date: 2016-04-07 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rails
@@ -67,15 +67,18 @@ files:
67
67
  - Rakefile
68
68
  - lib/culturecode_stagehand.rb
69
69
  - lib/stagehand.rb
70
+ - lib/stagehand/cache.rb
70
71
  - lib/stagehand/configuration.rb
71
72
  - lib/stagehand/controller_extensions.rb
73
+ - lib/stagehand/database.rb
72
74
  - lib/stagehand/engine.rb
73
- - lib/stagehand/helpers.rb
75
+ - lib/stagehand/key.rb
74
76
  - lib/stagehand/production.rb
75
77
  - lib/stagehand/production/controller.rb
76
78
  - lib/stagehand/schema.rb
77
79
  - lib/stagehand/schema/statements.rb
78
80
  - lib/stagehand/staging.rb
81
+ - lib/stagehand/staging/auditor.rb
79
82
  - lib/stagehand/staging/checklist.rb
80
83
  - lib/stagehand/staging/commit.rb
81
84
  - lib/stagehand/staging/commit_entry.rb