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