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.
- data/.gitignore +1 -0
- data/MIT_LICENSE +20 -0
- data/README.rdoc +133 -0
- data/Rakefile +52 -0
- data/VERSION +1 -0
- data/lib/sunspot/index_queue.rb +168 -0
- data/lib/sunspot/index_queue/batch.rb +120 -0
- data/lib/sunspot/index_queue/entry.rb +137 -0
- data/lib/sunspot/index_queue/entry/active_record_impl.rb +140 -0
- data/lib/sunspot/index_queue/entry/data_mapper_impl.rb +122 -0
- data/lib/sunspot/index_queue/entry/mongo_impl.rb +276 -0
- data/lib/sunspot/index_queue/session_proxy.rb +111 -0
- data/lib/sunspot_index_queue.rb +5 -0
- data/spec/active_record_impl_spec.rb +44 -0
- data/spec/batch_spec.rb +118 -0
- data/spec/data_mapper_impl_spec.rb +37 -0
- data/spec/entry_impl_examples.rb +184 -0
- data/spec/entry_spec.rb +148 -0
- data/spec/index_queue_spec.rb +150 -0
- data/spec/integration_spec.rb +110 -0
- data/spec/mongo_impl_spec.rb +35 -0
- data/spec/session_proxy_spec.rb +174 -0
- data/spec/spec_helper.rb +94 -0
- data/sunspot_index_queue.gemspec +99 -0
- metadata +237 -0
@@ -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
|