restforce-db 0.1.4

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.
Files changed (58) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +14 -0
  3. data/.rubocop/custom/method_documentation.rb +65 -0
  4. data/.rubocop.yml +39 -0
  5. data/.travis.yml +3 -0
  6. data/Gemfile +4 -0
  7. data/LICENSE.txt +21 -0
  8. data/README.md +68 -0
  9. data/Rakefile +13 -0
  10. data/bin/console +14 -0
  11. data/bin/setup +7 -0
  12. data/lib/generators/restforce_generator.rb +19 -0
  13. data/lib/generators/templates/config.yml +8 -0
  14. data/lib/generators/templates/script +6 -0
  15. data/lib/restforce/db/command.rb +98 -0
  16. data/lib/restforce/db/configuration.rb +50 -0
  17. data/lib/restforce/db/instances/active_record.rb +48 -0
  18. data/lib/restforce/db/instances/base.rb +66 -0
  19. data/lib/restforce/db/instances/salesforce.rb +46 -0
  20. data/lib/restforce/db/mapping.rb +106 -0
  21. data/lib/restforce/db/model.rb +50 -0
  22. data/lib/restforce/db/record_type.rb +77 -0
  23. data/lib/restforce/db/record_types/active_record.rb +80 -0
  24. data/lib/restforce/db/record_types/base.rb +44 -0
  25. data/lib/restforce/db/record_types/salesforce.rb +94 -0
  26. data/lib/restforce/db/synchronizer.rb +57 -0
  27. data/lib/restforce/db/version.rb +10 -0
  28. data/lib/restforce/db/worker.rb +156 -0
  29. data/lib/restforce/db.rb +77 -0
  30. data/lib/restforce/extensions.rb +19 -0
  31. data/restforce-db.gemspec +41 -0
  32. data/test/cassettes/Restforce_DB/accessing_Salesforce/uses_the_configured_credentials.yml +43 -0
  33. data/test/cassettes/Restforce_DB_Instances_Salesforce/_copy_/updates_the_record_with_the_attributes_from_the_copied_object.yml +193 -0
  34. data/test/cassettes/Restforce_DB_Instances_Salesforce/_update_/updates_the_local_record_with_the_passed_attributes.yml +193 -0
  35. data/test/cassettes/Restforce_DB_Instances_Salesforce/_update_/updates_the_record_in_Salesforce_with_the_passed_attributes.yml +232 -0
  36. data/test/cassettes/Restforce_DB_RecordTypes_Salesforce/_create_/creates_a_record_in_Salesforce_from_the_passed_database_record_s_attributes.yml +158 -0
  37. data/test/cassettes/Restforce_DB_RecordTypes_Salesforce/_create_/updates_the_database_record_with_the_Salesforce_record_s_ID.yml +158 -0
  38. data/test/cassettes/Restforce_DB_RecordTypes_Salesforce/_find/finds_existing_records_in_Salesforce.yml +157 -0
  39. data/test/cassettes/Restforce_DB_RecordTypes_Salesforce/_find/returns_nil_when_no_matching_record_exists.yml +81 -0
  40. data/test/cassettes/Restforce_DB_Synchronizer/_run/given_a_Salesforce_record_with_an_existing_record_in_the_database/updates_the_database_record.yml +158 -0
  41. data/test/cassettes/Restforce_DB_Synchronizer/_run/given_an_existing_Salesforce_record/populates_the_database_with_the_new_record.yml +158 -0
  42. data/test/cassettes/Restforce_DB_Synchronizer/_run/given_an_existing_database_record/populates_Salesforce_with_the_new_record.yml +235 -0
  43. data/test/lib/restforce/db/configuration_test.rb +38 -0
  44. data/test/lib/restforce/db/instances/active_record_test.rb +39 -0
  45. data/test/lib/restforce/db/instances/salesforce_test.rb +51 -0
  46. data/test/lib/restforce/db/mapping_test.rb +70 -0
  47. data/test/lib/restforce/db/model_test.rb +48 -0
  48. data/test/lib/restforce/db/record_type_test.rb +26 -0
  49. data/test/lib/restforce/db/record_types/active_record_test.rb +85 -0
  50. data/test/lib/restforce/db/record_types/salesforce_test.rb +46 -0
  51. data/test/lib/restforce/db/synchronizer_test.rb +84 -0
  52. data/test/lib/restforce/db_test.rb +24 -0
  53. data/test/support/active_record.rb +20 -0
  54. data/test/support/database_cleaner.rb +3 -0
  55. data/test/support/salesforce.rb +48 -0
  56. data/test/support/vcr.rb +23 -0
  57. data/test/test_helper.rb +25 -0
  58. metadata +287 -0
@@ -0,0 +1,106 @@
1
+ module Restforce
2
+
3
+ module DB
4
+
5
+ # Restforce::DB::Mapping captures a set of mappings between database columns
6
+ # and Salesforce fields, providing utilities to transform hashes of
7
+ # attributes from one to the other.
8
+ class Mapping
9
+
10
+ attr_reader :mappings
11
+
12
+ # Public: Initialize a new Restforce::DB::Mapping.
13
+ #
14
+ # mappings - A Hash, with keys corresponding to the names of Ruby object
15
+ # attributes, and the values of those keys corresponding to the
16
+ # names of the related Salesforce fields.
17
+ def initialize(mappings = {})
18
+ @mappings = mappings
19
+ end
20
+
21
+ # Public: Append a new set of attribute mappings to the current set.
22
+ #
23
+ # mappings - A Hash, with keys corresponding to the names of Ruby object
24
+ # attributes, and the values of those keys corresponding to the
25
+ # names of the related Salesforce fields.
26
+ #
27
+ # Returns nothing.
28
+ def add_mappings(mappings = {})
29
+ @mappings.merge!(mappings)
30
+ end
31
+
32
+ # Public: Get a list of the relevant Salesforce field names for this
33
+ # mapping.
34
+ #
35
+ # Returns an Array.
36
+ def salesforce_fields
37
+ @mappings.values
38
+ end
39
+
40
+ # Public: Get a list of the relevant database column names for this
41
+ # mapping.
42
+ #
43
+ # Returns an Array.
44
+ def database_fields
45
+ @mappings.keys
46
+ end
47
+
48
+ # Public: Build a normalized Hash of attributes from the appropriate set
49
+ # of mappings. The keys of the resulting mapping Hash will correspond to
50
+ # the database column names.
51
+ #
52
+ # in_format - A Symbol reflecting the expected attribute list. Accepted
53
+ # values are :database and :salesforce.
54
+ #
55
+ # Yields the attribute name.
56
+ # Returns a Hash.
57
+ def attributes(from_format)
58
+ use_mappings =
59
+ case from_format
60
+ when :salesforce
61
+ @mappings
62
+ when :database
63
+ # Generate a mapping of database column names to record attributes.
64
+ database_fields.zip(database_fields)
65
+ end
66
+
67
+ use_mappings.each_with_object({}) do |(attribute, mapping), values|
68
+ values[attribute] = yield(mapping)
69
+ end
70
+ end
71
+
72
+ # Public: Convert a Hash of attributes to a format compatible with a
73
+ # specific platform.
74
+ #
75
+ # to_format - A Symbol reflecting the expected format. Accepted values
76
+ # are :database and :salesforce.
77
+ # attributes - A Hash, with keys corresponding to the attribute names in
78
+ # the format to convert away from.
79
+ #
80
+ # Examples
81
+ #
82
+ # mapping = Mapping.new(some_key: "Some_Field__c")
83
+ # mapping.convert(:salesforce, some_key: "some value")
84
+ # # => { "Some_Field__c" => "some value" }
85
+ #
86
+ # mapping.convert(:database, some_key: "some other value")
87
+ # # => { some_key: "some other value" }
88
+ #
89
+ # Returns a Hash.
90
+ def convert(to_format, attributes)
91
+ case to_format
92
+ when :database
93
+ attributes.dup
94
+ when :salesforce
95
+ @mappings.each_with_object({}) do |(attribute, mapping), converted|
96
+ next unless attributes.key?(attribute)
97
+ converted[mapping] = attributes[attribute]
98
+ end
99
+ end
100
+ end
101
+
102
+ end
103
+
104
+ end
105
+
106
+ end
@@ -0,0 +1,50 @@
1
+ module Restforce
2
+
3
+ module DB
4
+
5
+ # Restforce::DB::Model is a helper module which attaches some special
6
+ # DSL-style methods to an ActiveRecord class, allowing for easier mapping
7
+ # of the ActiveRecord class to an object type in Salesforce.
8
+ module Model
9
+
10
+ # :nodoc:
11
+ def self.included(base)
12
+ base.extend(ClassMethods)
13
+ end
14
+
15
+ # :nodoc:
16
+ module ClassMethods
17
+
18
+ # Public: Initializes a Restforce::DB::RecordType defining this model's
19
+ # relationship to a Salesforce object type.
20
+ #
21
+ # salesforce_model - A String name of an object type in Salesforce.
22
+ # mappings - A Hash of mappings between database columns and
23
+ # fields in Salesforce.
24
+ #
25
+ # Returns a Restforce::DB::RecordType.
26
+ def map_to(salesforce_model, **mappings)
27
+ RecordType.new(
28
+ self,
29
+ salesforce_model,
30
+ mappings,
31
+ )
32
+ end
33
+
34
+ # Public: Append the passed mappings to this model.
35
+ #
36
+ # mappings - A Hash of database column names mapped to Salesforce
37
+ # fields.
38
+ #
39
+ # Returns nothing.
40
+ def add_mappings(mappings)
41
+ RecordType[self].add_mappings(mappings)
42
+ end
43
+
44
+ end
45
+
46
+ end
47
+
48
+ end
49
+
50
+ end
@@ -0,0 +1,77 @@
1
+ module Restforce
2
+
3
+ module DB
4
+
5
+ # Restforce::DB::RecordType is an abstraction for a two-way binding between
6
+ # an ActiveRecord class and a Salesforce object type. It provides an
7
+ # interface for mapping database columns to Salesforce fields.
8
+ class RecordType
9
+
10
+ class << self
11
+
12
+ include Enumerable
13
+ attr_accessor :collection
14
+
15
+ # Public: Get the Restforce::DB::RecordType entry for the specified
16
+ # database model.
17
+ #
18
+ # database_model - A Class compatible with ActiveRecord::Base.
19
+ #
20
+ # Returns a Restforce::DB::RecordType.
21
+ def [](database_model)
22
+ collection[database_model]
23
+ end
24
+
25
+ # Public: Iterate through all registered Restforce::DB::RecordTypes.
26
+ #
27
+ # Yields one RecordType for each database-to-Salesforce mapping.
28
+ # Returns nothing.
29
+ def each
30
+ collection.each do |database_model, record_type|
31
+ yield database_model.name, record_type
32
+ end
33
+ end
34
+
35
+ end
36
+
37
+ self.collection ||= {}
38
+ attr_reader :mapping, :synchronizer
39
+
40
+ # Public: Initialize and register a Restforce::DB::RecordType.
41
+ #
42
+ # database_model - A Class compatible with ActiveRecord::Base.
43
+ # salesforce_model - A String name of an object type in Salesforce.
44
+ # mappings - A Hash of mappings between database columns and
45
+ # fields in Salesforce.
46
+ def initialize(database_model, salesforce_model, **mappings)
47
+ @mapping = Mapping.new(mappings)
48
+ @database_record_type = RecordTypes::ActiveRecord.new(database_model, @mapping)
49
+ @salesforce_record_type = RecordTypes::Salesforce.new(salesforce_model, @mapping)
50
+ @synchronizer = Synchronizer.new(@database_record_type, @salesforce_record_type)
51
+
52
+ self.class.collection[database_model] = self
53
+ end
54
+
55
+ # Public: Append the passed mappings to this model.
56
+ #
57
+ # mappings - A Hash of database column names mapped to Salesforce fields.
58
+ #
59
+ # Returns nothing.
60
+ def add_mappings(mappings)
61
+ @mapping.add_mappings mappings
62
+ end
63
+
64
+ # Public: Synchronize the records between the database and Salesforce.
65
+ #
66
+ # options - A Hash of options to pass to the synchronizer.
67
+ #
68
+ # Returns nothing.
69
+ def synchronize(options = {})
70
+ @synchronizer.run(options)
71
+ end
72
+
73
+ end
74
+
75
+ end
76
+
77
+ end
@@ -0,0 +1,80 @@
1
+ module Restforce
2
+
3
+ module DB
4
+
5
+ module RecordTypes
6
+
7
+ # Restforce::DB::RecordTypes::ActiveRecord serves as a wrapper for a
8
+ # single ActiveRecord::Base-compatible class, allowing for standard record
9
+ # lookups and attribute mappings.
10
+ class ActiveRecord < Base
11
+
12
+ # Public: Create an instance of this ActiveRecord model for the passed
13
+ # Salesforce instance.
14
+ #
15
+ # from_record - A Restforce::DB::Instances::Salesforce instance.
16
+ #
17
+ # Returns a Restforce::DB::Instances::ActiveRecord instance.
18
+ # Raises on any validation or database error.
19
+ def create!(from_record)
20
+ attributes = @mapping.convert(:database, from_record.attributes)
21
+ record = @record_type.create!(
22
+ attributes.merge(salesforce_id: from_record.id),
23
+ )
24
+
25
+ Instances::ActiveRecord.new(record, @mapping)
26
+ end
27
+
28
+ # Public: Find the instance of this ActiveRecord model corresponding to
29
+ # the passed salesforce_id.
30
+ #
31
+ # salesforce_id - The id of the record in Salesforce.
32
+ #
33
+ # Returns nil or a Restforce::DB::Instances::ActiveRecord instance.
34
+ def find(id)
35
+ record = @record_type.find_by(salesforce_id: id)
36
+ return nil unless record
37
+
38
+ Instances::ActiveRecord.new(record, @mapping)
39
+ end
40
+
41
+ # Public: Iterate through all ActiveRecord records of this type.
42
+ #
43
+ # options - A Hash of options which should be applied to the set of
44
+ # fetched records. Allowed options are:
45
+ # :before - A Time object defining the most recent update
46
+ # timestamp for which records should be returned.
47
+ # :after - A Time object defining the least recent update
48
+ # timestamp for which records should be returned.
49
+ #
50
+ # Yields a series of Restforce::DB::Instances::ActiveRecord instances.
51
+ # Returns nothing.
52
+ def each(options = {})
53
+ scope = @record_type
54
+ scope = scope.where("updated_at > ?", options[:after]) if options[:after]
55
+ scope = scope.where("updated_at < ?", options[:before]) if options[:before]
56
+
57
+ scope.find_each do |record|
58
+ yield Instances::ActiveRecord.new(record, @mapping)
59
+ end
60
+ end
61
+
62
+ private
63
+
64
+ # Internal: Has this Salesforce record already been linked to a database
65
+ # record?
66
+ #
67
+ # record - A Restforce::DB::Instances::Salesforce instance.
68
+ #
69
+ # Returns a Boolean.
70
+ def synced?(record)
71
+ @record_type.exists?(salesforce_id: record.id)
72
+ end
73
+
74
+ end
75
+
76
+ end
77
+
78
+ end
79
+
80
+ end
@@ -0,0 +1,44 @@
1
+ module Restforce
2
+
3
+ module DB
4
+
5
+ module RecordTypes
6
+
7
+ # Restforce::DB::RecordTypes::Base defines common behavior for the other
8
+ # models defined in the Restforce::DB::RecordTypes namespace.
9
+ class Base
10
+
11
+ # Public: Initialize a new Restforce::DB::RecordTypes::Base.
12
+ #
13
+ # record_type - The name or class of the system record type.
14
+ # mapping - An instance of Restforce::DB::Mapping.
15
+ def initialize(record_type, mapping = Mapping.new)
16
+ @record_type = record_type
17
+ @mapping = mapping
18
+ end
19
+
20
+ # Public: Synchronize the passed record to the record type defined by
21
+ # this class.
22
+ #
23
+ # from_record - A Restforce::DB::Instances::Base instance.
24
+ #
25
+ # Returns a Restforce::DB::Instances::Base instance.
26
+ # Raises on any validation or external error.
27
+ def sync!(from_record)
28
+ if synced?(from_record)
29
+ record = find(from_record.id)
30
+ record.copy!(from_record)
31
+
32
+ record
33
+ else
34
+ create!(from_record)
35
+ end
36
+ end
37
+
38
+ end
39
+
40
+ end
41
+
42
+ end
43
+
44
+ end
@@ -0,0 +1,94 @@
1
+ module Restforce
2
+
3
+ module DB
4
+
5
+ module RecordTypes
6
+
7
+ # Restforce::DB::RecordTypes::Salesforce serves as a wrapper for a single
8
+ # Salesforce object class, allowing for standard record lookups and
9
+ # attribute mappings.
10
+ class Salesforce < Base
11
+
12
+ # Public: Create an instance of this Salesforce model for the passed
13
+ # database record.
14
+ #
15
+ # from_record - A Restforce::DB::Instances::ActiveRecord instance.
16
+ #
17
+ # Returns a Restforce::DB::Instances::Salesforce instance.
18
+ # Raises on any error from Salesforce.
19
+ def create!(from_record)
20
+ attributes = @mapping.convert(:salesforce, from_record.attributes)
21
+ record_id = DB.client.create!(@record_type, attributes)
22
+ from_record.update!(salesforce_id: record_id)
23
+
24
+ find(record_id)
25
+ end
26
+
27
+ # Public: Find the Salesforce record corresponding to the passed id.
28
+ #
29
+ # id - The id of the record in Salesforce.
30
+ #
31
+ # Returns nil or a Restforce::DB::Instances::Salesforce instance.
32
+ def find(id)
33
+ record = DB.client.query(
34
+ "select #{lookups} from #{@record_type} where Id = '#{id}'",
35
+ ).first
36
+
37
+ return unless record
38
+
39
+ Instances::Salesforce.new(record, @mapping)
40
+ end
41
+
42
+ # Public: Iterate through all Salesforce records of this type.
43
+ #
44
+ # options - A Hash of options which should be applied to the set of
45
+ # fetched records. Allowed options are:
46
+ # :before - A Time object defining the most recent update
47
+ # timestamp for which records should be returned.
48
+ # :after - A Time object defining the least recent update
49
+ # timestamp for which records should be returned.
50
+ #
51
+ # Yields a series of Restforce::DB::Instances::Salesforce instances.
52
+ # Returns nothing.
53
+ def each(options = {})
54
+ constraints = [
55
+ ("SystemModstamp <= #{options[:before].utc.iso8601}" if options[:before]),
56
+ ("SystemModstamp > #{options[:after].utc.iso8601}" if options[:after]),
57
+ ].compact.join(" and ")
58
+ constraints = " where #{constraints}" unless constraints.empty?
59
+
60
+ query = "select #{lookups} from #{@record_type}#{constraints}"
61
+
62
+ DB.client.query(query).each do |record|
63
+ yield Instances::Salesforce.new(record, @mapping)
64
+ end
65
+ end
66
+
67
+ private
68
+
69
+ # Internal: Get a String of values to look up when the record is
70
+ # fetched from Salesforce. Includes all configured mappings and a
71
+ # handful of attributes for internal use.
72
+ #
73
+ # Returns a String.
74
+ def lookups
75
+ (Instances::Salesforce::INTERNAL_ATTRIBUTES + @mapping.salesforce_fields).join(", ")
76
+ end
77
+
78
+ # Internal: Has this database record already been linked to a Salesforce
79
+ # record?
80
+ #
81
+ # record - A Restforce::DB::Instances::Salesforce instance.
82
+ #
83
+ # Returns a Boolean.
84
+ def synced?(record)
85
+ record.synced?
86
+ end
87
+
88
+ end
89
+
90
+ end
91
+
92
+ end
93
+
94
+ end
@@ -0,0 +1,57 @@
1
+ module Restforce
2
+
3
+ module DB
4
+
5
+ # Restforce::DB::Synchronizer is responsible for synchronizing the records
6
+ # in Salesforce with the records in the database. It relies on the mappings
7
+ # configured in instances of Restforce::DB::RecordTypes::Base to create and
8
+ # update records with the appropriate values.
9
+ class Synchronizer
10
+
11
+ attr_reader :last_run
12
+
13
+ # Public: Initialize a new Restforce::DB::Synchronizer.
14
+ #
15
+ # database_record_type - A Restforce::DB::RecordTypes::ActiveRecord
16
+ # instance.
17
+ # salesforce_record_type - A Restforce::DB::RecordTypes::Salesforce
18
+ # instance.
19
+ # last_run_time - A Time object reflecting the time of the most
20
+ # recent synchronization run. Runs will only
21
+ # synchronize data more recent than this stamp.
22
+ def initialize(database_record_type, salesforce_record_type, last_run_time = nil)
23
+ @database_record_type = database_record_type
24
+ @salesforce_record_type = salesforce_record_type
25
+ @last_run = last_run_time
26
+ end
27
+
28
+ # Public: Run the synchronize process, pulling in records from Salesforce
29
+ # and the database to determine which records need to be created and/or
30
+ # updated.
31
+ #
32
+ # NOTE: We bootstrap our record lookups to the exact same timespan, and
33
+ # run the Salesforce sync into the database first. This has the effect of
34
+ # overwriting recent changes to the database, in the event that Salesforce
35
+ # has also been updated since the last sync.
36
+ #
37
+ # options - A Hash of options for configuring the run. Currently unused.
38
+ #
39
+ # Returns the Time the run was performed.
40
+ def run(_options = {})
41
+ run_time = Time.now
42
+
43
+ @salesforce_record_type.each(after: last_run, before: run_time) do |record|
44
+ @database_record_type.sync!(record)
45
+ end
46
+ @database_record_type.each(after: last_run, before: run_time) do |record|
47
+ @salesforce_record_type.sync!(record)
48
+ end
49
+
50
+ @last_run = run_time
51
+ end
52
+
53
+ end
54
+
55
+ end
56
+
57
+ end
@@ -0,0 +1,10 @@
1
+ module Restforce
2
+
3
+ # :nodoc:
4
+ module DB
5
+
6
+ VERSION = "0.1.4"
7
+
8
+ end
9
+
10
+ end
@@ -0,0 +1,156 @@
1
+ module Restforce
2
+
3
+ module DB
4
+
5
+ # Restforce::DB::Worker represents the primary polling loop through which
6
+ # all record synchronization occurs.
7
+ class Worker
8
+
9
+ DEFAULT_INTERVAL = 5
10
+
11
+ class << self
12
+
13
+ attr_accessor :logger, :interval
14
+
15
+ # Public: Store the list of currently open file descriptors so that they
16
+ # may be reopened when a new process is spawned.
17
+ #
18
+ # Returns nothing.
19
+ def before_fork
20
+ return if @files_to_reopen
21
+
22
+ @files_to_reopen = []
23
+ ObjectSpace.each_object(File) do |file|
24
+ @files_to_reopen << file unless file.closed?
25
+ end
26
+ end
27
+
28
+ # Public: Reopen all file descriptors that have been stored through the
29
+ # before_fork hook.
30
+ #
31
+ # Returns nothing.
32
+ def after_fork
33
+ @files_to_reopen.each do |file|
34
+ begin
35
+ file.reopen file.path, "a+"
36
+ file.sync = true
37
+ rescue ::Exception # rubocop:disable HandleExceptions, RescueException
38
+ end
39
+ end
40
+ end
41
+
42
+ end
43
+
44
+ # Public: Initialize a new Restforce::DB::Worker.
45
+ #
46
+ # options - A Hash of options to configure the worker's run. Currently
47
+ # supported options are:
48
+ # interval - The maximum polling loop rest time.
49
+ # config - The path to a client configuration file.
50
+ # verbose - Display command line output? Defaults to false.
51
+ def initialize(options = {})
52
+ @verbose = options.fetch(:verbose) { false }
53
+ self.class.interval = options.fetch(:interval) { DEFAULT_INTERVAL }
54
+
55
+ Restforce::DB.configure { |config| config.parse(options[:config]) }
56
+ end
57
+
58
+ # Public: Start the polling loop for this Worker. Synchronizes all
59
+ # registered record types between the database and Salesforce, looping
60
+ # indefinitely until processing is interrupted by a signal.
61
+ #
62
+ # Returns nothing.
63
+ def start
64
+ trap("TERM") do
65
+ Thread.new { log "Exiting..." }
66
+ stop
67
+ end
68
+
69
+ trap("INT") do
70
+ Thread.new { log "Exiting..." }
71
+ stop
72
+ end
73
+
74
+ log "Starting synchronization..."
75
+
76
+ loop do
77
+ runtime = Benchmark.realtime do
78
+ Restforce::DB::RecordType.each do |name, record_type|
79
+ synchronize name, record_type
80
+ end
81
+ end
82
+
83
+ if runtime < self.class.interval && !stop?
84
+ sleep(self.class.interval - runtime)
85
+ end
86
+
87
+ break if stop?
88
+ end
89
+ end
90
+
91
+ # Public: Instruct the worker to stop running at the end of the current
92
+ # processing loop.
93
+ #
94
+ # Returns nothing.
95
+ def stop
96
+ @exit = true
97
+ end
98
+
99
+ private
100
+
101
+ # Internal: Synchronize the objects in the database and Salesforce
102
+ # corresponding to the passed record type.
103
+ #
104
+ # name - The String name of the record type to synchronize.
105
+ # record_type - A Restforce::DB::RecordType.
106
+ #
107
+ # Returns a Boolean.
108
+ def synchronize(name, record_type)
109
+ log "(#{name}) SYNCHRONIZING"
110
+ runtime = Benchmark.realtime { record_type.synchronize }
111
+ log format("(#{name}) COMPLETED after %.4f", runtime)
112
+
113
+ return true
114
+ rescue => e
115
+ error(e)
116
+
117
+ return false
118
+ end
119
+
120
+ # Internal: Has this worker been instructed to stop?
121
+ #
122
+ # Returns a boolean.
123
+ def stop?
124
+ @exit == true
125
+ end
126
+
127
+ # Internal: Log the passed text at the specified level.
128
+ #
129
+ # text - The piece of text which should be logged for this worker.
130
+ # level - The level at which the text should be logged. Defaults to :info.
131
+ #
132
+ # Returns nothing.
133
+ def log(text, level = :info)
134
+ text = "[Restforce::DB] #{text}"
135
+ puts text if @verbose
136
+
137
+ return unless self.class.logger
138
+
139
+ self.class.logger.send(level, "#{Time.now.strftime('%FT%T%z')}: #{text}")
140
+ end
141
+
142
+ # Internal: Log an error for the worker, outputting the entire error
143
+ # stacktrace and applying the appropriate log level.
144
+ #
145
+ # exception - An Exception object.
146
+ #
147
+ # Returns nothing.
148
+ def error(exception)
149
+ log "#{exception.message}\n#{exception.backtrace.join("\n")}", :error
150
+ end
151
+
152
+ end
153
+
154
+ end
155
+
156
+ end