culturecode_stagehand 0.3.1 → 0.4.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: 88258f8ab5b38d62a941024ddeef50d3e66ac13b
4
- data.tar.gz: 9f1571c1823366b2a5b576d9e213a7585713e883
3
+ metadata.gz: cd5f6596af77820e4b0fda648cf3a575cef70dd8
4
+ data.tar.gz: 73711aa60b690c1a9a7715d5ce26934d79e7727f
5
5
  SHA512:
6
- metadata.gz: a8020df0d22695e6fe842996690431bff25595e5dc3c76f46cb3ff7054dc8e56fad5dab0dc93007d6cc81d84b9875e922f2d70f0b98a37f28a479e50b63e1c55
7
- data.tar.gz: 391d375fd5dcc015f8ad867cc7495c476e2e1d652beddd18584ca2a4df262baf5aa0938f6ebb323357fd823055b73f8b32c8200b1dffb72829503e91bc32c176
6
+ metadata.gz: 9b685a7c6bd6f6c8638c86f8ff3a316ad2147942b7dcaa378daea28fbc31421cce4105097ac1a3a418d8a6639891e107c5663ee9a5177bdb25770f2d3cd555ac
7
+ data.tar.gz: 7f629766d1d1af995dc0870bf15bdc6a2c04c96105859cc5cdc4388cb104c4be11b7f0df4c17987f1b8e54a1fd6447e6847fd792196e346ee8f49da17a558cbe
@@ -20,7 +20,7 @@ module Stagehand
20
20
 
21
21
  return unless attributes.present?
22
22
 
23
- is_new = lookup(staging_record, table_name).update_all(attributes).zero?
23
+ is_new = matching(staging_record, table_name).update_all(attributes).zero?
24
24
 
25
25
  # Ensure we always return a record, even when updating instead of creating
26
26
  Record.new.tap do |record|
@@ -30,11 +30,11 @@ module Stagehand
30
30
  end
31
31
 
32
32
  def delete(staging_record, table_name = nil)
33
- lookup(staging_record, table_name).delete_all
33
+ matching(staging_record, table_name).delete_all
34
34
  end
35
35
 
36
36
  def exists?(staging_record, table_name = nil)
37
- lookup(staging_record, table_name).exists?
37
+ matching(staging_record, table_name).exists?
38
38
  end
39
39
 
40
40
  # Returns true if the staging record's attributes are different from the production record's attributes
@@ -44,9 +44,13 @@ module Stagehand
44
44
  production_record_attributes(staging_record, table_name) != staging_record_attributes(staging_record, table_name)
45
45
  end
46
46
 
47
+ def find(*args)
48
+ matching(*args).first
49
+ end
50
+
47
51
  # Returns a scope that limits results any occurrences of the specified record.
48
52
  # Record can be specified by passing a staging record, or an id and table_name.
49
- def lookup(staging_record, table_name = nil)
53
+ def matching(staging_record, table_name = nil)
50
54
  table_name, id = Stagehand::Key.generate(staging_record, :table_name => table_name)
51
55
  prepare_to_modify(table_name)
52
56
  return Record.where(:id => id)
@@ -60,7 +64,7 @@ module Stagehand
60
64
  end
61
65
 
62
66
  def production_record_attributes(staging_record, table_name = nil)
63
- Record.connection.select_one(lookup(staging_record, table_name))
67
+ Record.connection.select_one(matching(staging_record, table_name))
64
68
  end
65
69
 
66
70
  def staging_record_attributes(staging_record, table_name = nil)
@@ -109,7 +109,7 @@ module Stagehand
109
109
  private
110
110
 
111
111
  def build_production_record
112
- production_record = Stagehand::Production.lookup(record_id, table_name).first
112
+ production_record = Stagehand::Production.find(record_id, table_name)
113
113
  return unless production_record
114
114
 
115
115
  production_record = record_class.new(production_record.attributes)
@@ -2,6 +2,9 @@ module Stagehand
2
2
  module Staging
3
3
  module Synchronizer
4
4
  extend self
5
+ mattr_accessor :schemas_match
6
+
7
+ BATCH_SIZE = 1000
5
8
 
6
9
  # Immediately attempt to sync the changes from the block if possible
7
10
  # The block is wrapped in a transaction to prevent changes to records while being synced
@@ -14,22 +17,33 @@ module Stagehand
14
17
  end
15
18
  end
16
19
 
17
- def auto_sync(delay = 5.seconds)
18
- scope = autosyncable_entries.limit(1000)
19
-
20
+ def auto_sync(polling_delay = 5.seconds)
20
21
  loop do
21
- Rails.logger.info "Synced #{sync_entries(scope.reload)} entries"
22
- sleep(delay) if delay
22
+ sync(BATCH_SIZE)
23
+ sleep(polling_delay) if polling_delay
23
24
  end
24
25
  end
25
26
 
26
27
  def sync(limit = nil)
27
- sync_entries(autosyncable_entries.limit(limit))
28
+ synced_count = 0
29
+ deleted_count = 0
30
+
31
+ iterate_autosyncable_entries do |entry|
32
+ sync_entries(entry)
33
+ synced_count += 1
34
+ deleted_count += CommitEntry.matching(entry).delete_all
35
+ break if synced_count == limit
36
+ end
37
+
38
+ Rails.logger.info "Synced #{synced_count} entries"
39
+ Rails.logger.info "Removed #{deleted_count} stale entries"
40
+
41
+ return synced_count
28
42
  end
29
43
 
30
44
  def sync_all
31
45
  loop do
32
- entries = CommitEntry.order(:id => :desc).limit(1000).to_a
46
+ entries = CommitEntry.order(:id => :desc).limit(BATCH_SIZE).to_a
33
47
  break unless entries.present?
34
48
 
35
49
  latest_entries = entries.uniq(&:key)
@@ -48,44 +62,78 @@ module Stagehand
48
62
 
49
63
  private
50
64
 
65
+ # Lazily iterate through millions of commit entries
66
+ # Returns commit entries in ID descending order
67
+ def iterate_autosyncable_entries(&block)
68
+ sessions = CommitEntry.order(:id => :desc).distinct.pluck(:session)
69
+ offset = 0
70
+
71
+ while sessions.present?
72
+ autosyncable_entries(:session => sessions.shift(30)).offset(offset).limit(BATCH_SIZE).each do |entry|
73
+ with_confirmed_autosyncability(entry, &block)
74
+ end
75
+ offset += BATCH_SIZE
76
+ end
77
+ end
78
+
79
+ # Executes the code in the block if the record referred to by the entry is in fact, autosyncable.
80
+ # This confirmation is used to guard against writes to the record that occur after loading an initial list of
81
+ # entries that are autosyncable, but before the record is actually synced. To prevent this, a lock on the record
82
+ # is acquired and then the record's autosync eligibility is rechecked before calling the block.
83
+ # NOTE: This method must be called from within a transaction
84
+ def with_confirmed_autosyncability(entry, &block)
85
+ ActiveRecord::Base.transaction do
86
+ CommitEntry.connection.execute("SELECT 1 FROM #{entry.table_name} WHERE id = #{entry.record_id}")
87
+ block.call(entry) if autosyncable_entries(:record_id => entry.record_id, :table_name => entry.table_name).exists?
88
+ end
89
+ end
90
+
91
+ # Returns commit entries in ID descending order
92
+ def autosyncable_entries(scope = nil)
93
+ entries = CommitEntry.content_operations.not_in_progress
94
+
95
+ unless Configuration.ghost_mode?
96
+ subquery = CommitEntry.group('record_id, table_name').having('count(commit_id) = 0').where(scope)
97
+ entries = entries.joins("JOIN (#{subquery.select('MAX(id) AS max_id').to_sql}) subquery ON id = max_id")
98
+ end
99
+
100
+ return entries.order(:id => :desc)
101
+ end
102
+
51
103
  def sync_checklist(checklist)
52
- sync_entries(checklist.syncing_entries)
53
- CommitEntry.delete(checklist.affected_entries)
104
+ ActiveRecord::Base.transaction do
105
+ sync_entries(checklist.syncing_entries)
106
+ CommitEntry.delete(checklist.affected_entries)
107
+ end
54
108
  end
55
109
 
56
110
  def sync_entries(entries)
57
111
  return 0 if Configuration.single_connection? # Avoid deadlocking if the databases are the same
58
-
59
112
  raise SchemaMismatch unless schemas_match?
60
113
 
61
- ActiveRecord::Base.transaction do
62
- entries.each do |entry|
63
- Rails.logger.info "Synchronizing #{entry.table_name} #{entry.record_id}" if entry.content_operation?
64
- if entry.delete_operation?
65
- Stagehand::Production.delete(entry)
66
- elsif entry.save_operation?
67
- Stagehand::Production.save(entry)
68
- end
114
+ entries = Array.wrap(entries)
115
+
116
+ entries.each do |entry|
117
+ Rails.logger.info "Synchronizing #{entry.table_name} #{entry.record_id}" if entry.content_operation?
118
+ if entry.delete_operation?
119
+ Stagehand::Production.delete(entry)
120
+ elsif entry.save_operation?
121
+ Stagehand::Production.save(entry)
69
122
  end
70
123
  end
71
124
 
72
125
  return entries.length
73
126
  end
74
127
 
75
- def autosyncable_entries
76
- if Configuration.ghost_mode?
77
- CommitEntry
78
- else
79
- CommitEntry.where(:id =>
80
- CommitEntry.select('MAX(id) AS id').content_operations.not_in_progress.group('record_id, table_name').having('count(commit_id) = 0'))
81
- end
82
- end
83
-
84
128
  def schemas_match?
129
+ return schemas_match unless schemas_match.nil?
130
+
85
131
  versions_scope = ActiveRecord::SchemaMigration.order(:version)
86
132
  staging_versions = Stagehand::Database.staging_connection.select_values(versions_scope)
87
133
  production_versions = Stagehand::Database.production_connection.select_values(versions_scope)
88
- return staging_versions == production_versions
134
+ self.schemas_match = staging_versions == production_versions
135
+
136
+ return schemas_match
89
137
  end
90
138
  end
91
139
  end
@@ -1,3 +1,3 @@
1
1
  module Stagehand
2
- VERSION = "0.3.1"
2
+ VERSION = "0.4.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.3.1
4
+ version: 0.4.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-16 00:00:00.000000000 Z
12
+ date: 2016-04-20 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rails