culturecode_stagehand 0.1.4 → 0.2.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.
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