sunspot_index_queue 1.0.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.
@@ -0,0 +1,137 @@
1
+ module Sunspot
2
+ class IndexQueue
3
+ # Abstract queue entry interface. All the gory details on actually handling the queue are handled by a
4
+ # specific implementation class. The default implementation will use ActiveRecord as the backing queue.
5
+ #
6
+ # Implementing classes must define attribute readers for +id+, +record_class_name+, +record_id+, +error+,
7
+ # +attempts+, and +is_delete?+.
8
+ module Entry
9
+ autoload :ActiveRecordImpl, File.expand_path('../entry/active_record_impl', __FILE__)
10
+ autoload :DataMapperImpl, File.expand_path('../entry/data_mapper_impl', __FILE__)
11
+ autoload :MongoImpl, File.expand_path('../entry/mongo_impl', __FILE__)
12
+
13
+ attr_writer :processed
14
+
15
+ class << self
16
+ # Set the implementation class to use for the queue. This can be set as either a class object,
17
+ # full class name, or a symbol representing one of the default implementations.
18
+ #
19
+ # # These all set the implementation to use the default ActiveRecord queue.
20
+ # Sunspot::IndexQueue::Entry.implementation = :active_record
21
+ # Sunspot::IndexQueue::Entry.implementation = "Sunspot::IndexQueue::Entry::ActiveRecordImpl"
22
+ # Sunspot::IndexQueue::Entry.implementation = Sunspot::IndexQueue::Entry::ActiveRecordImpl
23
+ #
24
+ # Implementations should support pulling entries in batches by a priority where higher priority
25
+ # entries are processed first. Errors should be automatically retried after an interval specified
26
+ # by the IndexQueue. The batch size set by the IndexQueue should also be honored.
27
+ def implementation= (klass)
28
+ unless klass.is_a?(Class) || klass.nil?
29
+ class_name = klass.to_s
30
+ class_name = Sunspot::Util.camel_case(class_name).gsub('/', '::') unless class_name.include?('::')
31
+ if class_name.include?('::') || !const_defined?("#{class_name}Impl")
32
+ klass = Sunspot::Util.full_const_get(class_name)
33
+ else
34
+ klass = const_get("#{class_name}Impl")
35
+ end
36
+ end
37
+ @implementation = klass
38
+ end
39
+
40
+ # The implementation class used for the queue.
41
+ def implementation
42
+ @implementation ||= ActiveRecordImpl
43
+ end
44
+
45
+ # Get a count of the queue entries for an IndexQueue. Implementations must implement this method.
46
+ def total_count (queue)
47
+ implementation.total_count(queue)
48
+ end
49
+
50
+ # Get a count of the entries ready to be processed for an IndexQueue. Implementations must implement this method.
51
+ def ready_count (queue)
52
+ implementation.ready_count(queue)
53
+ end
54
+
55
+ # Get a count of the error entries for an IndexQueue. Implementations must implement this method.
56
+ def error_count (queue)
57
+ implementation.error_count(queue)
58
+ end
59
+
60
+ # Get the specified number of error entries for an IndexQueue. Implementations must implement this method.
61
+ def errors (queue, limit, offset)
62
+ implementation.errors(queue, limit, offset)
63
+ end
64
+
65
+ # Get the next batch of entries to process for IndexQueue. Implementations must implement this method.
66
+ def next_batch! (queue)
67
+ implementation.next_batch!(queue)
68
+ end
69
+
70
+ # Reset the entries in the queue to be excuted again immediately and clear any errors.
71
+ def reset! (queue)
72
+ implementation.reset!(queue)
73
+ end
74
+
75
+ # Add an entry the queue. +is_delete+ will be true if the entry is a delete. Implementations must implement this method.
76
+ def add (klass, id, delete, options = {})
77
+ raise NotImplementedError.new("add")
78
+ end
79
+
80
+ # Add multiple entries to the queue. +delete+ will be true if the entry is a delete.
81
+ def enqueue (queue, klass, ids, delete, priority)
82
+ klass = Sunspot::Util.full_const_get(klass.to_s) unless klass.is_a?(Class)
83
+ unless queue.class_names.empty? || queue.class_names.include?(klass.name)
84
+ raise ArgumentError.new("Class #{klass.name} is not in the class names allowed for the queue")
85
+ end
86
+ priority = priority.to_i
87
+ if ids.is_a?(Array)
88
+ ids.each do |id|
89
+ implementation.add(klass, id, delete, priority)
90
+ end
91
+ else
92
+ implementation.add(klass, ids, delete, priority)
93
+ end
94
+ end
95
+
96
+ # Delete entries from the queue. Implementations must implement this method.
97
+ def delete_entries (entries)
98
+ implementation.delete_entries(entries)
99
+ end
100
+
101
+ # Load all records in an array of entries. This can be faster than calling load on each DataAccessor
102
+ # depending on them implementation
103
+ def load_all_records (entries)
104
+ classes = entries.collect{|entry| entry.record_class_name}.uniq.collect{|name| Sunspot::Util.full_const_get(name) rescue nil}.compact
105
+ map = entries.inject({}){|hash, entry| hash[entry.record_id.to_s] = entry; hash}
106
+ classes.each do |klass|
107
+ ids = entries.collect{|entry| entry.record_id}
108
+ Sunspot::Adapters::DataAccessor.create(klass).load_all(ids).each do |record|
109
+ entry = map[Sunspot::Adapters::InstanceAdapter.adapt(record).id.to_s]
110
+ entry.instance_variable_set(:@record, record) if entry
111
+ end
112
+ end
113
+ end
114
+ end
115
+
116
+ def processed?
117
+ @processed = false unless defined?(@processed)
118
+ @processed
119
+ end
120
+
121
+ # Get the record represented by this entry.
122
+ def record
123
+ @record ||= Sunspot::Adapters::DataAccessor.create(Sunspot::Util.full_const_get(record_class_name)).load_all([record_id]).first
124
+ end
125
+
126
+ # Set the error message on an entry. Implementations must implement this method.
127
+ def set_error! (error, retry_interval = nil)
128
+ raise NotImplementedError.new("set_error!")
129
+ end
130
+
131
+ # Reset an entry to be executed again immediatel and clear any errors. Implementations must implement this method.
132
+ def reset!
133
+ raise NotImplementedError.new("reset!")
134
+ end
135
+ end
136
+ end
137
+ end
@@ -0,0 +1,140 @@
1
+ require 'active_record'
2
+
3
+ module Sunspot
4
+ class IndexQueue
5
+ module Entry
6
+ # Implementation of an indexing queue backed by ActiveRecord.
7
+ #
8
+ # To create the table, you should have a migration containing the following:
9
+ #
10
+ # self.up
11
+ # Sunspot::IndexQueue::Entry::ActiveRecordImpl.create_table
12
+ # end
13
+ #
14
+ # self.down
15
+ # drop_table Sunspot::IndexQueue::Entry::ActiveRecordImpl.table_name
16
+ # end
17
+ class ActiveRecordImpl < ActiveRecord::Base
18
+ include Entry
19
+
20
+ set_table_name :sunspot_index_queue_entries
21
+
22
+ class << self
23
+ # Implementation of the total_count method.
24
+ def total_count (queue)
25
+ conditions = queue.class_names.empty? ? {} : {:record_class_name => queue.class_names}
26
+ count(:conditions => conditions)
27
+ end
28
+
29
+ # Implementation of the ready_count method.
30
+ def ready_count (queue)
31
+ conditions = ["#{connection.quote_column_name('run_at')} <= ?", Time.now.utc]
32
+ unless queue.class_names.empty?
33
+ conditions.first << " AND #{connection.quote_column_name('record_class_name')} IN (?)"
34
+ conditions << queue.class_names
35
+ end
36
+ count(:conditions => conditions)
37
+ end
38
+
39
+ # Implementation of the error_count method.
40
+ def error_count (queue)
41
+ conditions = ["#{connection.quote_column_name('error')} IS NOT NULL"]
42
+ unless queue.class_names.empty?
43
+ conditions.first << " AND #{connection.quote_column_name('record_class_name')} IN (?)"
44
+ conditions << queue.class_names
45
+ end
46
+ count(:conditions => conditions)
47
+ end
48
+
49
+ # Implementation of the errors method.
50
+ def errors (queue, limit, offset)
51
+ conditions = ["#{connection.quote_column_name('error')} IS NOT NULL"]
52
+ unless queue.class_names.empty?
53
+ conditions.first << " AND #{connection.quote_column_name('record_class_name')} IN (?)"
54
+ conditions << queue.class_names
55
+ end
56
+ all(:conditions => conditions, :limit => limit, :offset => offset, :order => :id)
57
+ end
58
+
59
+ # Implementation of the reset! method.
60
+ def reset! (queue)
61
+ conditions = queue.class_names.empty? ? {} : {:record_class_name => queue.class_names}
62
+ update_all({:run_at => Time.now.utc, :attempts => 0, :error => nil, :lock => nil}, conditions)
63
+ end
64
+
65
+ # Implementation of the next_batch! method.
66
+ def next_batch! (queue)
67
+ conditions = ["#{connection.quote_column_name('run_at')} <= ?", Time.now.utc]
68
+ unless queue.class_names.empty?
69
+ conditions.first << " AND #{connection.quote_column_name('record_class_name')} IN (?)"
70
+ conditions << queue.class_names
71
+ end
72
+ batch_entries = all(:select => "id", :conditions => conditions, :limit => queue.batch_size, :order => 'priority DESC, run_at')
73
+ queue_entry_ids = batch_entries.collect{|entry| entry.id}
74
+ return [] if queue_entry_ids.empty?
75
+ lock = rand(0x7FFFFFFF)
76
+ update_all({:run_at => queue.retry_interval.from_now.utc, :lock => lock, :error => nil}, :id => queue_entry_ids)
77
+ all(:conditions => {:id => queue_entry_ids, :lock => lock})
78
+ end
79
+
80
+ # Implementation of the add method.
81
+ def add (klass, id, delete, priority)
82
+ queue_entry_key = {:record_id => id, :record_class_name => klass.name, :lock => nil}
83
+ queue_entry = first(:conditions => queue_entry_key) || new(queue_entry_key.merge(:priority => priority))
84
+ queue_entry.is_delete = delete
85
+ queue_entry.priority = priority if priority > queue_entry.priority
86
+ queue_entry.run_at = Time.now.utc
87
+ queue_entry.save!
88
+ end
89
+
90
+ # Implementation of the delete_entries method.
91
+ def delete_entries (ids)
92
+ delete_all(:id => ids)
93
+ end
94
+
95
+ # Create the table used to store the queue in the database.
96
+ def create_table
97
+ connection.create_table table_name do |t|
98
+ t.string :record_class_name, :null => false
99
+ t.string :record_id, :null => false
100
+ t.boolean :is_delete, :null => false, :default => false
101
+ t.datetime :run_at, :null => false
102
+ t.integer :priority, :null => false, :default => 0
103
+ t.integer :lock, :null => true
104
+ t.string :error, :null => true, :limit => 4000
105
+ t.integer :attempts, :null => false, :default => 0
106
+ end
107
+
108
+ connection.add_index table_name, [:record_class_name, :record_id], :name => "#{table_name}_record"
109
+ connection.add_index table_name, [:run_at, :record_class_name, :priority], :name => "#{table_name}_run_at"
110
+ end
111
+ end
112
+
113
+ # Implementation of the set_error! method.
114
+ def set_error! (error, retry_interval = nil)
115
+ self.attempts += 1
116
+ self.run_at = (retry_interval * attempts).from_now.utc if retry_interval
117
+ self.error = "#{error.class.name}: #{error.message}\n#{error.backtrace.join("\n")[0, 4000]}"
118
+ self.lock = nil
119
+ begin
120
+ save!
121
+ rescue => e
122
+ if logger
123
+ logger.warn(error)
124
+ logger.warn(e)
125
+ end
126
+ end
127
+ end
128
+
129
+ # Implementation of the reset! method.
130
+ def reset!
131
+ begin
132
+ update_attributes!(:attempts => 0, :error => nil, :lock => nil, :run_at => Time.now.utc)
133
+ rescue => e
134
+ logger.warn(e)
135
+ end
136
+ end
137
+ end
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,122 @@
1
+ require 'dm-core'
2
+ require 'dm-aggregates'
3
+
4
+ module Sunspot
5
+ class IndexQueue
6
+ module Entry
7
+ # Implementation of an indexing queue backed by ActiveRecord.
8
+ #
9
+ # To create the table, you should have a migration containing the following:
10
+ #
11
+ # self.up
12
+ # Sunspot::IndexQueue::Entry::ActiveRecordImpl.create_table
13
+ # end
14
+ #
15
+ # self.down
16
+ # drop_table Sunspot::IndexQueue::Entry::ActiveRecordImpl.table_name
17
+ # end
18
+ class DataMapperImpl
19
+ include DataMapper::Resource
20
+ include Entry
21
+
22
+ storage_names[:default] = "sunspot_index_queue_entries"
23
+ property :id, Serial
24
+ property :run_at, Time, :index => :run_at
25
+ property :record_class_name, String, :index => [:record, :run_at]
26
+ property :record_id, String, :index => [:record]
27
+ property :priority, Integer, :default => 0, :index => :run_at
28
+ property :is_delete, Boolean
29
+ property :lock, Integer
30
+ property :error, String
31
+ property :attempts, Integer, :default => 0
32
+
33
+ class << self
34
+ # Implementation of the total_count method.
35
+ def total_count (queue)
36
+ conditions = queue.class_names.empty? ? {} : {:record_class_name => queue.class_names}
37
+ count(conditions)
38
+ end
39
+
40
+ # Implementation of the ready_count method.
41
+ def ready_count (queue)
42
+ conditions = {:run_at.lte => Time.now.utc}
43
+ conditions[:record_class_name] = queue.class_names unless queue.class_names.empty?
44
+ count(conditions)
45
+ end
46
+
47
+ # Implementation of the error_count method.
48
+ def error_count (queue)
49
+ conditions = {:error.not => nil}
50
+ conditions[:record_class_name] = queue.class_names unless queue.class_names.empty?
51
+ count(conditions)
52
+ end
53
+
54
+ # Implementation of the errors method.
55
+ def errors (queue, limit, offset)
56
+ conditions = {:error.not => nil}
57
+ conditions[:record_class_name] = queue.class_names unless queue.class_names.empty?
58
+ all(conditions.merge(:limit => limit, :offset => offset, :order => :id))
59
+ end
60
+
61
+ # Implementation of the reset! method.
62
+ def reset! (queue)
63
+ conditions = queue.class_names.empty? ? {} : {:record_class_name => queue.class_names}
64
+ all(conditions).update!(:run_at => Time.now.utc, :attempts => 0, :error => nil, :lock => nil)
65
+ end
66
+
67
+ # Implementation of the next_batch! method.
68
+ def next_batch! (queue)
69
+ conditions = {:run_at.lte => Time.now.utc}
70
+ conditions[:record_class_name] = queue.class_names unless queue.class_names.empty?
71
+ batch_entries = all(conditions.merge(:fields => [:id], :limit => queue.batch_size, :order => [:priority.desc, :run_at]))
72
+ queue_entry_ids = batch_entries.collect{|entry| entry.id}
73
+ return [] if queue_entry_ids.empty?
74
+ lock = rand(0x7FFFFFFF)
75
+ all(:id => queue_entry_ids).update!(:run_at => Time.now.utc + queue.retry_interval, :lock => lock, :error => nil)
76
+ all(:id => queue_entry_ids, :lock => lock)
77
+ end
78
+
79
+ # Implementation of the add method.
80
+ def add (klass, id, delete, priority)
81
+ queue_entry_key = {:record_id => id, :record_class_name => klass.name, :lock => nil}
82
+ queue_entry = first(:conditions => queue_entry_key) || new(queue_entry_key.merge(:priority => priority))
83
+ queue_entry.is_delete = delete
84
+ queue_entry.priority = priority if priority > queue_entry.priority
85
+ queue_entry.run_at = Time.now.utc
86
+ queue_entry.save!
87
+ end
88
+
89
+ # Implementation of the delete_entries method.
90
+ def delete_entries (ids)
91
+ all(:id => ids).destroy!
92
+ end
93
+ end
94
+
95
+ # Implementation of the set_error! method.
96
+ def set_error! (error, retry_interval = nil)
97
+ self.attempts += 1
98
+ self.run_at = Time.now.utc + (retry_interval * attempts) if retry_interval
99
+ self.error = "#{error.class.name}: #{error.message}\n#{error.backtrace.join("\n")[0, 4000]}"
100
+ self.lock = nil
101
+ begin
102
+ save!
103
+ rescue => e
104
+ if DataMapper.logger
105
+ DataMapper.logger.warn(error)
106
+ DataMapper.logger.warn(e)
107
+ end
108
+ end
109
+ end
110
+
111
+ # Implementation of the reset! method.
112
+ def reset!
113
+ begin
114
+ update!(:attempts => 0, :error => nil, :lock => nil, :run_at => Time.now.utc)
115
+ rescue => e
116
+ DataMapper.logger.warn(e)
117
+ end
118
+ end
119
+ end
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,276 @@
1
+ require 'mongo'
2
+
3
+ module Sunspot
4
+ class IndexQueue
5
+ module Entry
6
+ # Implementation of an indexing queue backed by MongoDB (http://mongodb.org/). This implementation
7
+ # uses the mongo gem directly and so is independent of any ORM you may be using.
8
+ #
9
+ # To set it up, you need to set the connection and database that it will use.
10
+ #
11
+ # Sunspot::IndexQueue::Entry::MongoImpl.connection = 'localhost'
12
+ # Sunspot::IndexQueue::Entry::MongoImpl.database = 'my_database'
13
+ # # or
14
+ # Sunspot::IndexQueue::Entry::MongoImpl.connection = Mongo::Connection.new('localhost', 27017)
15
+ # Sunspot::IndexQueue::Entry::MongoImpl.database = 'my_database'
16
+ class MongoImpl
17
+ include Entry
18
+
19
+ class << self
20
+ # Set the connection to MongoDB. The args can either be a Mongo::Connection object, or the args
21
+ # that can be used to create a new Mongo::Connection.
22
+ def connection= (*args)
23
+ @connection = args.first.is_a?(Mongo::Connection) ? args.first : Mongo::Connection.new(*args)
24
+ end
25
+
26
+ # Get the connection currently in use.
27
+ def connection
28
+ @connection
29
+ end
30
+
31
+ # Set the name of the database which will contain the queue collection.
32
+ def database_name= (name)
33
+ @collection = nil
34
+ @database_name = name
35
+ end
36
+
37
+ # Get the collection used to store the queue.
38
+ def collection
39
+ unless @collection
40
+ @collection = connection.db(@database_name)["sunspot_index_queue_entries"]
41
+ @collection.create_index([[:record_class_name, Mongo::ASCENDING], [:record_id, Mongo::ASCENDING]])
42
+ @collection.create_index([[:run_at, Mongo::ASCENDING], [:record_class_name, Mongo::ASCENDING], [:priority, Mongo::DESCENDING]])
43
+ end
44
+ @collection
45
+ end
46
+
47
+ # Create a new entry.
48
+ def create (attributes)
49
+ entry = new(attributes)
50
+ entry.save
51
+ entry
52
+ end
53
+
54
+ # Find one entry given a selector or object id.
55
+ def find_one (spec_or_object_id=nil, opts={})
56
+ doc = collection.find_one(spec_or_object_id, opts)
57
+ doc ? new(doc) : nil
58
+ end
59
+
60
+ # Find an array of entries given a selector.
61
+ def find (selector={}, opts={})
62
+ collection.find(selector, opts).collect{|doc| new(doc)}
63
+ end
64
+
65
+ # Logger used to log errors.
66
+ def logger
67
+ @logger
68
+ end
69
+
70
+ # Set the logger used to log errors.
71
+ def logger= (logger)
72
+ @logger = logger
73
+ end
74
+
75
+ # Implementation of the total_count method.
76
+ def total_count (queue)
77
+ conditions = queue.class_names.empty? ? {} : {:record_class_name => {'$in' => queue.class_names}}
78
+ collection.find(conditions).count
79
+ end
80
+
81
+ # Implementation of the ready_count method.
82
+ def ready_count (queue)
83
+ conditions = {:run_at => {'$lte' => Time.now.utc}}
84
+ unless queue.class_names.empty?
85
+ conditions[:record_class_name] = {'$in' => queue.class_names}
86
+ end
87
+ collection.find(conditions).count
88
+ end
89
+
90
+ # Implementation of the error_count method.
91
+ def error_count (queue)
92
+ conditions = {:error => {'$ne' => nil}}
93
+ unless queue.class_names.empty?
94
+ conditions[:record_class_name] = {'$in' => queue.class_names}
95
+ end
96
+ collection.find(conditions).count
97
+ end
98
+
99
+ # Implementation of the errors method.
100
+ def errors (queue, limit, offset)
101
+ conditions = {:error => {'$ne' => nil}}
102
+ unless queue.class_names.empty?
103
+ conditions[:record_class_name] = {'$in' => queue.class_names}
104
+ end
105
+ find(conditions, :limit => limit, :skip => offset, :sort => :id)
106
+ end
107
+
108
+ # Implementation of the reset! method.
109
+ def reset! (queue)
110
+ conditions = queue.class_names.empty? ? {} : {:record_class_name => {'$in' => queue.class_names}}
111
+ collection.update(conditions, {"$set" => {:run_at => Time.now.utc, :attempts => 0, :error => nil}}, :multi => true)
112
+ end
113
+
114
+ # Implementation of the next_batch! method.
115
+ def next_batch! (queue)
116
+ conditions = {:run_at => {'$lte' => Time.now.utc}}
117
+ unless queue.class_names.empty?
118
+ conditions[:record_class_name] = {'$in' => queue.class_names}
119
+ end
120
+ entries = []
121
+ while entries.size < queue.batch_size
122
+ begin
123
+ lock = rand(0x7FFFFFFF)
124
+ doc = collection.find_and_modify(:update => {"$set" => {:run_at => Time.now.utc + queue.retry_interval, :error => nil, :lock => lock}}, :query => conditions, :limit => queue.batch_size, :sort => [[:priority, Mongo::DESCENDING], [:run_at, Mongo::ASCENDING]])
125
+ entries << new(doc)
126
+ rescue Mongo::OperationFailure
127
+ break
128
+ end
129
+ end
130
+ entries
131
+ end
132
+
133
+ # Implementation of the add method.
134
+ def add (klass, id, delete, priority)
135
+ queue_entry_key = {:record_id => id, :record_class_name => klass.name, :lock => nil}
136
+ queue_entry = find_one(queue_entry_key) || new(queue_entry_key.merge(:priority => priority))
137
+ queue_entry.is_delete = delete
138
+ queue_entry.priority = priority if priority > queue_entry.priority
139
+ queue_entry.run_at = Time.now.utc
140
+ queue_entry.save
141
+ end
142
+
143
+ # Implementation of the delete_entries method.
144
+ def delete_entries (ids)
145
+ collection.remove(:_id => {'$in' => ids})
146
+ end
147
+ end
148
+
149
+ attr_reader :doc
150
+
151
+ # Create a new entry from a document hash.
152
+ def initialize (attributes = {})
153
+ @doc = {}
154
+ attributes.each do |key, value|
155
+ @doc[key.to_s] = value
156
+ end
157
+ @doc['priority'] = 0 unless doc['priority']
158
+ @doc['attempts'] = 0 unless doc['attempts']
159
+ end
160
+
161
+ # Get the entry id.
162
+ def id
163
+ doc['_id']
164
+ end
165
+
166
+ # Get the entry id.
167
+ def record_class_name
168
+ doc['record_class_name']
169
+ end
170
+
171
+ # Set the entry record_class_name.
172
+ def record_class_name= (value)
173
+ doc['record_class_name'] = value.nil? ? nil : value.to_s
174
+ end
175
+
176
+ # Get the entry id.
177
+ def record_id
178
+ doc['record_id']
179
+ end
180
+
181
+ # Set the entry record_id.
182
+ def record_id= (value)
183
+ doc['record_id'] = value
184
+ end
185
+
186
+ # Get the entry run_at time.
187
+ def run_at
188
+ doc['run_at']
189
+ end
190
+
191
+ # Set the entry run_at time.
192
+ def run_at= (value)
193
+ value = Time.parse(value.to_s) unless value.nil? || value.is_a?(Time)
194
+ doc['run_at'] = value.nil? ? nil : value.utc
195
+ end
196
+
197
+ # Get the entry priority.
198
+ def priority
199
+ doc['priority']
200
+ end
201
+
202
+ # Set the entry priority.
203
+ def priority= (value)
204
+ doc['priority'] = value.to_i
205
+ end
206
+
207
+ # Get the entry attempts.
208
+ def attempts
209
+ doc['attempts'] || 0
210
+ end
211
+
212
+ # Set the entry attempts.
213
+ def attempts= (value)
214
+ doc['attempts'] = value.to_i
215
+ end
216
+
217
+ # Get the entry error.
218
+ def error
219
+ doc['error']
220
+ end
221
+
222
+ # Set the entry error.
223
+ def error= (value)
224
+ doc['error'] = value.nil? ? nil : value.to_s
225
+ end
226
+
227
+ # Get the entry delete entry flag.
228
+ def is_delete?
229
+ doc['is_delete']
230
+ end
231
+
232
+ # Set the entry delete entry flag.
233
+ def is_delete= (value)
234
+ doc['is_delete'] = !!value
235
+ end
236
+
237
+ # Save the entry to the database.
238
+ def save
239
+ id = self.class.collection.save(doc)
240
+ doc['_id'] = id if id
241
+ end
242
+
243
+ # Implementation of the set_error! method.
244
+ def set_error! (error, retry_interval = nil)
245
+ self.attempts += 1
246
+ self.run_at = (retry_interval * attempts).from_now.utc if retry_interval
247
+ self.error = "#{error.class.name}: #{error.message}\n#{error.backtrace.join("\n")[0, 4000]}"
248
+ begin
249
+ save
250
+ rescue => e
251
+ if self.class.logger
252
+ self.class.logger.warn(error)
253
+ self.class.logger.warn(e)
254
+ end
255
+ end
256
+ end
257
+
258
+ # Implementation of the reset! method.
259
+ def reset!
260
+ begin
261
+ self.error = nil
262
+ self.attempts = 0
263
+ self.run_at = Time.now.utc
264
+ self.save
265
+ rescue => e
266
+ self.class.logger.warn(e) if self.class.logger
267
+ end
268
+ end
269
+
270
+ def == (value)
271
+ value.is_a?(self.class) && ((id && id == value.id) || (doc == value.doc))
272
+ end
273
+ end
274
+ end
275
+ end
276
+ end