sunspot_index_queue 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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