draisine 0.7.10
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/.gitignore +9 -0
- data/.rspec +2 -0
- data/.travis.yml +4 -0
- data/Gemfile +4 -0
- data/Guardfile +70 -0
- data/LICENSE.txt +21 -0
- data/README.md +134 -0
- data/Rakefile +6 -0
- data/app/controllers/draisine/soap_controller.rb +49 -0
- data/bin/console +7 -0
- data/bin/setup +6 -0
- data/config/routes.rb +4 -0
- data/draisine.gemspec +32 -0
- data/lib/draisine/active_record.rb +191 -0
- data/lib/draisine/auditor/result.rb +48 -0
- data/lib/draisine/auditor.rb +130 -0
- data/lib/draisine/concerns/array_setter.rb +23 -0
- data/lib/draisine/concerns/attributes_mapping.rb +46 -0
- data/lib/draisine/concerns/import.rb +36 -0
- data/lib/draisine/conflict_detector.rb +38 -0
- data/lib/draisine/conflict_resolver.rb +97 -0
- data/lib/draisine/engine.rb +6 -0
- data/lib/draisine/importer.rb +111 -0
- data/lib/draisine/ip_checker.rb +15 -0
- data/lib/draisine/jobs/inbound_delete_job.rb +8 -0
- data/lib/draisine/jobs/inbound_update_job.rb +8 -0
- data/lib/draisine/jobs/job_base.rb +39 -0
- data/lib/draisine/jobs/outbound_create_job.rb +7 -0
- data/lib/draisine/jobs/outbound_delete_job.rb +7 -0
- data/lib/draisine/jobs/outbound_update_job.rb +7 -0
- data/lib/draisine/jobs/soap_delete_job.rb +7 -0
- data/lib/draisine/jobs/soap_update_job.rb +7 -0
- data/lib/draisine/partitioner.rb +73 -0
- data/lib/draisine/poller.rb +101 -0
- data/lib/draisine/query_mechanisms/base.rb +15 -0
- data/lib/draisine/query_mechanisms/default.rb +13 -0
- data/lib/draisine/query_mechanisms/last_modified_date.rb +18 -0
- data/lib/draisine/query_mechanisms/system_modstamp.rb +18 -0
- data/lib/draisine/query_mechanisms.rb +18 -0
- data/lib/draisine/registry.rb +22 -0
- data/lib/draisine/setup.rb +97 -0
- data/lib/draisine/soap_handler.rb +79 -0
- data/lib/draisine/syncer.rb +52 -0
- data/lib/draisine/type_mapper.rb +105 -0
- data/lib/draisine/util/caching_client.rb +73 -0
- data/lib/draisine/util/hash_diff.rb +39 -0
- data/lib/draisine/util/parse_time.rb +14 -0
- data/lib/draisine/util/salesforce_comparisons.rb +53 -0
- data/lib/draisine/version.rb +3 -0
- data/lib/draisine.rb +48 -0
- data/lib/ext/databasedotcom.rb +98 -0
- data/lib/generators/draisine/delta_migration_generator.rb +77 -0
- data/lib/generators/draisine/integration_generator.rb +53 -0
- data/lib/generators/draisine/templates/delta_migration.rb +24 -0
- data/lib/generators/draisine/templates/migration.rb +21 -0
- data/lib/generators/draisine/templates/model.rb +11 -0
- data/salesforce/sample_delete_trigger.apex +7 -0
- data/salesforce/sample_test_class_for_delete_trigger.apex +15 -0
- metadata +242 -0
@@ -0,0 +1,130 @@
|
|
1
|
+
require "draisine/auditor/result"
|
2
|
+
|
3
|
+
module Draisine
|
4
|
+
class Auditor
|
5
|
+
def self.run(model_class:, start_date: Time.current.beginning_of_day, end_date: Time.current, mechanism: :default)
|
6
|
+
# TODO: instead of using one huge partition, combine multiple results into one
|
7
|
+
partitions = partition(model_class: model_class, start_date: start_date, end_date: end_date, partition_size: 10**12, mechanism: mechanism)
|
8
|
+
run_partition(partitions.first)
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.run_partition(partition)
|
12
|
+
new(partition).run
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.partition(model_class:, start_date:, end_date:, partition_size: 100, mechanism: :default)
|
16
|
+
Partitioner.partition(
|
17
|
+
model_class: model_class,
|
18
|
+
start_date: start_date,
|
19
|
+
end_date: end_date,
|
20
|
+
partition_size: partition_size,
|
21
|
+
mechanism: mechanism)
|
22
|
+
end
|
23
|
+
|
24
|
+
attr_reader :partition, :model_class, :start_date, :end_date, :result
|
25
|
+
def initialize(partition)
|
26
|
+
@partition = partition
|
27
|
+
@model_class = partition.model_class
|
28
|
+
@start_date = partition.start_date
|
29
|
+
@end_date = partition.end_date
|
30
|
+
end
|
31
|
+
|
32
|
+
def run
|
33
|
+
@result = Result.new
|
34
|
+
|
35
|
+
check_unpersisted_records
|
36
|
+
check_deletes
|
37
|
+
check_modifications
|
38
|
+
|
39
|
+
result.calculate_result!
|
40
|
+
rescue => e
|
41
|
+
result.error!(e)
|
42
|
+
raise
|
43
|
+
end
|
44
|
+
|
45
|
+
def check_unpersisted_records
|
46
|
+
return unless partition.unpersisted_ids.present?
|
47
|
+
|
48
|
+
bad_records = model_class.where(id: partition.unpersisted_ids)
|
49
|
+
bad_records.each do |record|
|
50
|
+
result.discrepancy(
|
51
|
+
type: :local_record_without_salesforce_id,
|
52
|
+
salesforce_type: salesforce_object_name,
|
53
|
+
salesforce_id: nil,
|
54
|
+
local_id: record.id,
|
55
|
+
local_type: record.class.name,
|
56
|
+
local_attributes: record.attributes)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def check_deletes
|
61
|
+
return unless partition.deleted_ids.present?
|
62
|
+
|
63
|
+
ghost_models = model_class.where(salesforce_id: partition.deleted_ids).all
|
64
|
+
ghost_models.each do |ghost_model|
|
65
|
+
result.discrepancy(
|
66
|
+
type: :remote_delete_kept_locally,
|
67
|
+
salesforce_type: salesforce_object_name,
|
68
|
+
salesforce_id: ghost_model.salesforce_id,
|
69
|
+
local_id: ghost_model.id,
|
70
|
+
local_type: ghost_model.class.name,
|
71
|
+
local_attributes: ghost_model.attributes)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def check_modifications
|
76
|
+
updated_ids = partition.updated_ids
|
77
|
+
return unless updated_ids.present?
|
78
|
+
|
79
|
+
local_records = model_class.where(salesforce_id: updated_ids).to_a
|
80
|
+
remote_records = client.fetch_multiple(salesforce_object_name, updated_ids)
|
81
|
+
|
82
|
+
local_records_map = build_map(local_records) {|record| record.salesforce_id }
|
83
|
+
remote_records_map = build_map(remote_records) {|record| record.Id }
|
84
|
+
|
85
|
+
missing_ids = updated_ids - local_records_map.keys
|
86
|
+
missing_ids.each do |id|
|
87
|
+
result.discrepancy(
|
88
|
+
type: :remote_record_missing_locally,
|
89
|
+
salesforce_type: salesforce_object_name,
|
90
|
+
salesforce_id: id,
|
91
|
+
remote_attributes: remote_records_map.fetch(id))
|
92
|
+
end
|
93
|
+
|
94
|
+
attr_list = model_class.salesforce_audited_attributes
|
95
|
+
local_records_map.each do |salesforce_id, local_record|
|
96
|
+
remote_record = remote_records_map[salesforce_id]
|
97
|
+
next unless remote_record
|
98
|
+
conflict_detector = ConflictDetector.new(local_record, remote_record, attr_list)
|
99
|
+
|
100
|
+
if conflict_detector.conflict?
|
101
|
+
result.discrepancy(
|
102
|
+
type: :mismatching_records,
|
103
|
+
salesforce_type: salesforce_object_name,
|
104
|
+
salesforce_id: salesforce_id,
|
105
|
+
local_id: local_record.id,
|
106
|
+
local_type: local_record.class.name,
|
107
|
+
local_attributes: local_record.salesforce_attributes,
|
108
|
+
remote_attributes: remote_record.attributes,
|
109
|
+
diff_keys: conflict_detector.diff.diff_keys)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
protected
|
115
|
+
|
116
|
+
def client
|
117
|
+
Draisine.salesforce_client
|
118
|
+
end
|
119
|
+
|
120
|
+
def build_map(list_of_hashes, &key_block)
|
121
|
+
list_of_hashes.each_with_object({}) do |item, rs|
|
122
|
+
rs[key_block.call(item)] = item
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def salesforce_object_name
|
127
|
+
model_class.salesforce_object_name
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require "active_support/concern"
|
2
|
+
|
3
|
+
module Draisine
|
4
|
+
module Concerns
|
5
|
+
module ArraySetter
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
module ClassMethods
|
9
|
+
def salesforce_array_setter(attr)
|
10
|
+
mod = Module.new do
|
11
|
+
define_method "#{attr}=" do |value|
|
12
|
+
value = [] if value.nil?
|
13
|
+
value = value.split(';') if value.kind_of?(String)
|
14
|
+
super(value)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
prepend mod
|
18
|
+
attr
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require "active_support/concern"
|
2
|
+
|
3
|
+
module Draisine
|
4
|
+
module Concerns
|
5
|
+
module AttributesMapping
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
module ClassMethods
|
9
|
+
attr_accessor :salesforce_mapping
|
10
|
+
|
11
|
+
def salesforce_synced_attributes
|
12
|
+
@salesforce_synced_attributes ||= salesforce_mapping.keys
|
13
|
+
end
|
14
|
+
|
15
|
+
def salesforce_reverse_mapping
|
16
|
+
@salesforce_reverse_mapping ||= salesforce_mapping.map(&:reverse).to_h
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def salesforce_mapped_attributes(attributes, mapping = self.class.salesforce_mapping)
|
21
|
+
attributes.slice(*mapping.keys).each_with_object({}) do |(key, value), acc|
|
22
|
+
acc[mapping.fetch(key)] = value
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def salesforce_assign_attributes(attributes)
|
27
|
+
salesforce_mapped_attributes(attributes.with_indifferent_access).each do |key, value|
|
28
|
+
method_name = "#{key}="
|
29
|
+
if respond_to?(method_name)
|
30
|
+
value = Draisine::SalesforceComparisons.salesforce_cleanup(value)
|
31
|
+
__send__(method_name, value)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def salesforce_reverse_mapped_attributes(attributes)
|
37
|
+
salesforce_mapped_attributes(attributes, self.class.salesforce_reverse_mapping)
|
38
|
+
end
|
39
|
+
|
40
|
+
def salesforce_attributes
|
41
|
+
salesforce_reverse_mapped_attributes(attributes)
|
42
|
+
.with_indifferent_access
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require "active_support/concern"
|
2
|
+
|
3
|
+
module Draisine
|
4
|
+
module Concerns
|
5
|
+
module Import
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
module ClassMethods
|
9
|
+
# Doesn't update record if found
|
10
|
+
def import_with_attrs(sf_id, attrs)
|
11
|
+
find_or_initialize_by(salesforce_id: sf_id) do |model|
|
12
|
+
model.salesforce_update_without_sync(attrs)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
# Does update record if found
|
17
|
+
def import_or_update_with_attrs(sf_id, attrs, check_modstamp = false)
|
18
|
+
find_or_initialize_by(salesforce_id: sf_id).tap do |model|
|
19
|
+
model.salesforce_update_without_sync(attrs, check_modstamp)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def salesforce_update_without_sync(attributes, check_modstamp = false)
|
25
|
+
salesforce_skipping_sync do
|
26
|
+
modstamp = attributes["SystemModstamp"]
|
27
|
+
own_modstamp = self.attributes["SystemModstamp"]
|
28
|
+
if !check_modstamp || !modstamp || !own_modstamp || own_modstamp < modstamp
|
29
|
+
salesforce_assign_attributes(attributes)
|
30
|
+
save!
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module Draisine
|
2
|
+
class ConflictDetector
|
3
|
+
attr_reader :model, :remote_model, :attributes_list
|
4
|
+
def initialize(model, remote_model, attributes_list = model.class.salesforce_audited_attributes)
|
5
|
+
@model = model
|
6
|
+
@remote_model = remote_model
|
7
|
+
@attributes_list = attributes_list
|
8
|
+
end
|
9
|
+
|
10
|
+
def conflict?
|
11
|
+
conflict_type != :no_conflict
|
12
|
+
end
|
13
|
+
|
14
|
+
def conflict_type
|
15
|
+
if model && remote_model
|
16
|
+
if diff.diff_keys.empty?
|
17
|
+
:no_conflict
|
18
|
+
else
|
19
|
+
:mismatching_records
|
20
|
+
end
|
21
|
+
elsif model
|
22
|
+
:remote_record_missing
|
23
|
+
elsif remote_model
|
24
|
+
:local_record_missing
|
25
|
+
else
|
26
|
+
:no_conflict
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def diff
|
31
|
+
return unless model && remote_model
|
32
|
+
|
33
|
+
@diff ||= HashDiff.sf_diff(
|
34
|
+
model.salesforce_attributes.slice(*attributes_list).compact,
|
35
|
+
remote_model.attributes.slice(*attributes_list).compact)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
module Draisine
|
2
|
+
class ConflictResolver
|
3
|
+
ALLOWED_RESOLUTIONS = %w[
|
4
|
+
remote_push remote_pull local_delete merge
|
5
|
+
]
|
6
|
+
|
7
|
+
attr_reader :model_class, :client, :salesforce_object_name,
|
8
|
+
:local_id, :salesforce_id
|
9
|
+
|
10
|
+
def initialize(model_class, client, local_id, salesforce_id)
|
11
|
+
@model_class = model_class
|
12
|
+
@client = client
|
13
|
+
@salesforce_object_name = model_class.salesforce_object_name
|
14
|
+
@local_id = local_id
|
15
|
+
@salesforce_id = salesforce_id
|
16
|
+
end
|
17
|
+
|
18
|
+
def conflict?
|
19
|
+
ConflictDetector.new(model, remote_model, model_class.salesforce_synced_attributes).conflict?
|
20
|
+
end
|
21
|
+
|
22
|
+
def resolve(resolution_type, options = {})
|
23
|
+
resolution_type = resolution_type.to_s
|
24
|
+
fail ArgumentError, "Unknown resolution type '#{resolution_type}'" unless allowed_resolution?(resolution_type)
|
25
|
+
|
26
|
+
__send__(resolution_type, options)
|
27
|
+
end
|
28
|
+
|
29
|
+
def allowed_resolution?(resolution_type)
|
30
|
+
ALLOWED_RESOLUTIONS.include?(resolution_type)
|
31
|
+
end
|
32
|
+
|
33
|
+
def remote_pull(_options = {})
|
34
|
+
fail ArgumentError, "remote model is required for remote pull" unless remote_model
|
35
|
+
|
36
|
+
model_class.salesforce_inbound_update(remote_model.attributes)
|
37
|
+
end
|
38
|
+
|
39
|
+
def remote_push(_options = {})
|
40
|
+
fail ArgumentError, "local model is required for remote push" unless model
|
41
|
+
|
42
|
+
if model.salesforce_id.present?
|
43
|
+
model.salesforce_outbound_update(model.salesforce_attributes)
|
44
|
+
else
|
45
|
+
model.salesforce_outbound_create
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def remote_delete(_options = {})
|
50
|
+
fail ArgumentError, "local model is required for remote delete" unless model
|
51
|
+
|
52
|
+
model.salesforce_outbound_delete
|
53
|
+
end
|
54
|
+
|
55
|
+
def local_delete(_options = {})
|
56
|
+
model_class.salesforce_inbound_delete(salesforce_id)
|
57
|
+
end
|
58
|
+
|
59
|
+
def merge(options)
|
60
|
+
fail ArgumentError unless model && remote_model
|
61
|
+
assert_required_options!(options, [:local_attributes, :remote_attributes])
|
62
|
+
|
63
|
+
local_attrs_to_merge = options.fetch(:local_attributes)
|
64
|
+
remote_attrs_to_merge = options.fetch(:remote_attributes)
|
65
|
+
|
66
|
+
model.salesforce_outbound_update(
|
67
|
+
model.salesforce_attributes.slice(*local_attrs_to_merge))
|
68
|
+
model.salesforce_inbound_update(
|
69
|
+
remote_model.attributes.slice(*remote_attrs_to_merge), false)
|
70
|
+
end
|
71
|
+
|
72
|
+
def model
|
73
|
+
@model ||= if local_id
|
74
|
+
model_class.find_by(id: local_id)
|
75
|
+
else
|
76
|
+
model_class.find_by(salesforce_id: salesforce_id)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def remote_model
|
81
|
+
return @remote_model unless @remote_model.nil?
|
82
|
+
@remote_model = begin
|
83
|
+
client.find(salesforce_object_name, salesforce_id)
|
84
|
+
rescue Databasedotcom::SalesForceError
|
85
|
+
false
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
protected
|
90
|
+
|
91
|
+
def assert_required_options!(options, keys)
|
92
|
+
keys.each do |key|
|
93
|
+
fail ArgumentError, "missing required option #{key}" unless options.key?(key)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,111 @@
|
|
1
|
+
module Draisine
|
2
|
+
class Importer
|
3
|
+
attr_reader :model_class
|
4
|
+
|
5
|
+
def initialize(model_class)
|
6
|
+
@model_class = model_class
|
7
|
+
end
|
8
|
+
|
9
|
+
def import(start_id: nil, start_date: nil, batch_size: 500)
|
10
|
+
find_each(batch_size: batch_size, start_id: start_id, start_date: start_date) do |sobj|
|
11
|
+
attrs = sobj.attributes
|
12
|
+
model_class.import_with_attrs(
|
13
|
+
attrs.fetch("Id"),
|
14
|
+
attrs.slice(*model_class.salesforce_synced_attributes))
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def import_new(batch_size: 500, start_date_window_size: 20.minutes)
|
19
|
+
last_model = model_class.order("salesforce_id DESC").first
|
20
|
+
start_id = last_model.try(:salesforce_id)
|
21
|
+
start_date = last_model.try(:CreatedDate)
|
22
|
+
start_date -= start_date_window_size if start_date
|
23
|
+
|
24
|
+
import(start_id: start_id, batch_size: batch_size, start_date: start_date)
|
25
|
+
end
|
26
|
+
|
27
|
+
def import_fields(batch_size: 500, fields:)
|
28
|
+
model_class.find_in_batches(batch_size: batch_size) do |batch|
|
29
|
+
attempt do
|
30
|
+
sobjs = client.fetch_multiple(model_class.salesforce_object_name, batch.map(&:salesforce_id), batch_size, fields)
|
31
|
+
sobjs_map = sobjs.map {|sobj| [sobj.Id, sobj] }.to_h
|
32
|
+
batch.each do |model|
|
33
|
+
sobject = sobjs_map[model.salesforce_id]
|
34
|
+
next unless sobject
|
35
|
+
model.salesforce_assign_attributes(sobject.attributes.slice(*fields))
|
36
|
+
model.salesforce_skipping_sync { model.save! }
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
protected
|
43
|
+
|
44
|
+
def find_each(batch_size:, start_id:, start_date: nil, &block)
|
45
|
+
salesforce_model = client.materialize(salesforce_object_name)
|
46
|
+
# if we have start_date set, only use id starting from the second query
|
47
|
+
last_id = start_id unless start_date
|
48
|
+
|
49
|
+
counter = 0
|
50
|
+
loop do
|
51
|
+
query = import_query(salesforce_model, salesforce_object_name, batch_size, last_id, start_date)
|
52
|
+
collection = attempt { client.query(query) }
|
53
|
+
break unless collection.count > 0
|
54
|
+
|
55
|
+
model_class.transaction do
|
56
|
+
collection.each do |sobj|
|
57
|
+
yield sobj
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
counter += collection.count
|
62
|
+
last_id = collection.last.attributes.fetch("Id")
|
63
|
+
logger.info "[#{model_class} import] Imported #{counter} records, last record id #{last_id}"
|
64
|
+
end
|
65
|
+
logger.info "[#{model_class} import] Finished, imported a total of #{counter} records"
|
66
|
+
end
|
67
|
+
|
68
|
+
def client
|
69
|
+
Draisine.salesforce_client
|
70
|
+
end
|
71
|
+
|
72
|
+
def salesforce_object_name
|
73
|
+
model_class.salesforce_object_name
|
74
|
+
end
|
75
|
+
|
76
|
+
def import_query(salesforce_model, salesforce_object_name, batch_size, start_id = nil, start_date = nil)
|
77
|
+
conds = [
|
78
|
+
start_id && "Id > '#{start_id}'",
|
79
|
+
start_date && "CreatedDate >= #{start_date.iso8601}"
|
80
|
+
].compact
|
81
|
+
where_clause = conds.presence && "WHERE #{conds.join(" AND ")}"
|
82
|
+
|
83
|
+
<<-QUERY
|
84
|
+
SELECT #{salesforce_model.field_list}
|
85
|
+
FROM #{salesforce_object_name}
|
86
|
+
#{where_clause}
|
87
|
+
ORDER BY Id ASC
|
88
|
+
LIMIT #{batch_size}
|
89
|
+
QUERY
|
90
|
+
end
|
91
|
+
|
92
|
+
def attempt(times = 5)
|
93
|
+
attempts ||= 0
|
94
|
+
yield
|
95
|
+
rescue => e
|
96
|
+
attempts += 1
|
97
|
+
logger.error "#{e.class}: #{e.message}"
|
98
|
+
if attempts < times
|
99
|
+
logger.error "Retrying... (attempt ##{attempts})"
|
100
|
+
retry
|
101
|
+
else
|
102
|
+
logger.error "Too many attempts, failing..."
|
103
|
+
raise
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def logger
|
108
|
+
@logger ||= Rails.logger || Logger.new($stdout)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require "ipaddr"
|
2
|
+
|
3
|
+
module Draisine
|
4
|
+
class IpChecker
|
5
|
+
attr_reader :ip_ranges
|
6
|
+
def initialize(ip_ranges)
|
7
|
+
@ip_ranges = ip_ranges.map {|net| IPAddr.new(net) }
|
8
|
+
end
|
9
|
+
|
10
|
+
def check(ip)
|
11
|
+
addr = IPAddr.new(ip)
|
12
|
+
ip_ranges.any? {|range| range.include?(addr) }
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module Draisine
|
2
|
+
class JobBase < ActiveJob::Base
|
3
|
+
queue_as :draisine_job
|
4
|
+
|
5
|
+
def perform(*args)
|
6
|
+
_perform(*args)
|
7
|
+
rescue Exception => ex
|
8
|
+
logger.error "#{ex.class}: #{ex}\n#{ex.backtrace.join("\n")}"
|
9
|
+
|
10
|
+
if retry_attempt < retries_count
|
11
|
+
@retry_attempt = retry_attempt + 1
|
12
|
+
logger.error "Retrying (attempt #{retry_attempt})"
|
13
|
+
retry_job
|
14
|
+
else
|
15
|
+
logger.error "Too many attempts, no more retries"
|
16
|
+
Draisine.job_error_handler.call(ex, self, arguments)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def _perform(*args)
|
21
|
+
end
|
22
|
+
|
23
|
+
def retries_count
|
24
|
+
Draisine.job_retry_attempts
|
25
|
+
end
|
26
|
+
|
27
|
+
def retry_attempt
|
28
|
+
@retry_attempt ||= 0
|
29
|
+
end
|
30
|
+
|
31
|
+
def serialize
|
32
|
+
super.merge('_retry_attempt' => retry_attempt)
|
33
|
+
end
|
34
|
+
|
35
|
+
def deserialize(job_data)
|
36
|
+
@retry_attempt = job_data.fetch('_retry_attempt', 0)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
module Draisine
|
2
|
+
Partition = Struct.new(:model_class, :start_date, :end_date, :updated_ids, :deleted_ids, :unpersisted_ids) do
|
3
|
+
def initialize(model_class, *args)
|
4
|
+
model_class = model_class.constantize if model_class.is_a?(String)
|
5
|
+
super(model_class, *args)
|
6
|
+
end
|
7
|
+
|
8
|
+
def as_json(*)
|
9
|
+
model_class, *fields = to_a
|
10
|
+
[model_class.name, *fields].as_json
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.from_json(fields)
|
14
|
+
new(*fields)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
class Partitioner
|
19
|
+
def self.partition(model_class:, start_date:, end_date:, partition_size: 100, mechanism: :default)
|
20
|
+
new(model_class, mechanism).partition(start_date, end_date, partition_size: partition_size)
|
21
|
+
end
|
22
|
+
|
23
|
+
attr_reader :model_class, :mechanism
|
24
|
+
def initialize(model_class, mechanism = :default)
|
25
|
+
@model_class = model_class
|
26
|
+
@mechanism = QueryMechanisms.fetch(mechanism).new(model_class)
|
27
|
+
end
|
28
|
+
|
29
|
+
def partition(start_date, end_date, partition_size: 100)
|
30
|
+
updated_ids = get_updated_ids(start_date, end_date)
|
31
|
+
deleted_ids = get_deleted_ids(start_date, end_date)
|
32
|
+
unpersisted_ids = get_unpersisted_ids(start_date, end_date)
|
33
|
+
|
34
|
+
# if anyone knows how to do this packing procedure better, please tell me
|
35
|
+
all_ids = updated_ids.map {|id| [:updated, id] } +
|
36
|
+
deleted_ids.map {|id| [:deleted, id] } +
|
37
|
+
unpersisted_ids.map {|id| [:unpersisted, id] }
|
38
|
+
|
39
|
+
if all_ids.present?
|
40
|
+
all_ids.each_slice(partition_size).map do |slice|
|
41
|
+
part = slice.group_by(&:first).map {|k,v| [k, v.map(&:last)] }.to_h
|
42
|
+
Partition.new(model_class.name, start_date, end_date, part[:updated], part[:deleted], part[:unpersisted])
|
43
|
+
end
|
44
|
+
else
|
45
|
+
[Partition.new(model_class.name, start_date, end_date)]
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
protected
|
50
|
+
|
51
|
+
def get_updated_ids(start_date, end_date)
|
52
|
+
mechanism.get_updated_ids(start_date, end_date) |
|
53
|
+
model_class
|
54
|
+
.where("updated_at >= ? AND updated_at <= ?", start_date, end_date)
|
55
|
+
.uniq.pluck(:salesforce_id).compact
|
56
|
+
end
|
57
|
+
|
58
|
+
def get_deleted_ids(start_date, end_date)
|
59
|
+
mechanism.get_deleted_ids(start_date, end_date)
|
60
|
+
end
|
61
|
+
|
62
|
+
def get_unpersisted_ids(start_date, end_date)
|
63
|
+
model_class
|
64
|
+
.where("salesforce_id IS NULL OR salesforce_id = ?", '')
|
65
|
+
.where("updated_at >= ? and updated_at <= ?", start_date, end_date)
|
66
|
+
.pluck(:id)
|
67
|
+
end
|
68
|
+
|
69
|
+
def client
|
70
|
+
Draisine.salesforce_client
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|