freelancing-god-thinking-sphinx 0.9.13 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/thinking_sphinx.rb +4 -3
- data/lib/thinking_sphinx/active_record.rb +1 -1
- data/lib/thinking_sphinx/active_record/delta.rb +2 -17
- data/lib/thinking_sphinx/deltas.rb +19 -0
- data/lib/thinking_sphinx/deltas/default_delta.rb +50 -0
- data/lib/thinking_sphinx/deltas/delayed_delta.rb +25 -0
- data/lib/thinking_sphinx/deltas/delayed_delta/delta_job.rb +24 -0
- data/lib/thinking_sphinx/deltas/delayed_delta/flag_as_deleted_job.rb +27 -0
- data/lib/thinking_sphinx/deltas/delayed_delta/job.rb +24 -0
- data/lib/thinking_sphinx/index.rb +32 -38
- data/lib/thinking_sphinx/rails_additions.rb +54 -1
- data/spec/unit/thinking_sphinx/active_record/delta_spec.rb +7 -7
- data/tasks/thinking_sphinx_tasks.rb +14 -0
- data/vendor/delayed_job/lib/delayed/job.rb +251 -0
- data/vendor/delayed_job/lib/delayed/message_sending.rb +7 -0
- data/vendor/delayed_job/lib/delayed/performable_method.rb +55 -0
- data/vendor/delayed_job/lib/delayed/worker.rb +54 -0
- metadata +15 -2
data/lib/thinking_sphinx.rb
CHANGED
@@ -15,6 +15,7 @@ require 'thinking_sphinx/field'
|
|
15
15
|
require 'thinking_sphinx/index'
|
16
16
|
require 'thinking_sphinx/rails_additions'
|
17
17
|
require 'thinking_sphinx/search'
|
18
|
+
require 'thinking_sphinx/deltas'
|
18
19
|
|
19
20
|
require 'thinking_sphinx/adapters/abstract_adapter'
|
20
21
|
require 'thinking_sphinx/adapters/mysql_adapter'
|
@@ -28,9 +29,9 @@ Merb::Plugins.add_rakefiles(
|
|
28
29
|
|
29
30
|
module ThinkingSphinx
|
30
31
|
module Version #:nodoc:
|
31
|
-
Major =
|
32
|
-
Minor =
|
33
|
-
Tiny =
|
32
|
+
Major = 1
|
33
|
+
Minor = 1
|
34
|
+
Tiny = 0
|
34
35
|
|
35
36
|
String = [Major, Minor, Tiny].join('.')
|
36
37
|
end
|
@@ -234,7 +234,7 @@ module ThinkingSphinx
|
|
234
234
|
{self.sphinx_document_id => 1}
|
235
235
|
) if ThinkingSphinx.deltas_enabled? &&
|
236
236
|
self.class.sphinx_indexes.any? { |index| index.delta? } &&
|
237
|
-
self.delta
|
237
|
+
self.delta
|
238
238
|
rescue ::ThinkingSphinx::ConnectionError
|
239
239
|
# nothing
|
240
240
|
end
|
@@ -42,22 +42,7 @@ module ThinkingSphinx
|
|
42
42
|
# if running in the test environment.
|
43
43
|
#
|
44
44
|
def index_delta(instance = nil)
|
45
|
-
|
46
|
-
ThinkingSphinx.deltas_enabled?
|
47
|
-
|
48
|
-
config = ThinkingSphinx::Configuration.instance
|
49
|
-
client = Riddle::Client.new config.address, config.port
|
50
|
-
|
51
|
-
client.update(
|
52
|
-
"#{self.sphinx_indexes.first.name}_core",
|
53
|
-
['sphinx_deleted'],
|
54
|
-
{instance.sphinx_document_id => 1}
|
55
|
-
) if instance && instance.in_core_index?
|
56
|
-
|
57
|
-
output = `#{config.bin_path}indexer --config #{config.config_file} --rotate #{self.sphinx_indexes.first.name}_delta`
|
58
|
-
puts output unless ThinkingSphinx.suppress_delta_output?
|
59
|
-
|
60
|
-
true
|
45
|
+
self.sphinx_indexes.first.delta_object.index(self, instance)
|
61
46
|
end
|
62
47
|
end
|
63
48
|
|
@@ -65,7 +50,7 @@ module ThinkingSphinx
|
|
65
50
|
|
66
51
|
# Set the delta value for the model to be true.
|
67
52
|
def toggle_delta
|
68
|
-
self.
|
53
|
+
self.class.sphinx_indexes.first.delta_object.toggle(self)
|
69
54
|
end
|
70
55
|
|
71
56
|
# Build the delta index for the related model. This won't be called
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'thinking_sphinx/deltas/default_delta'
|
2
|
+
require 'thinking_sphinx/deltas/delayed_delta'
|
3
|
+
|
4
|
+
module ThinkingSphinx
|
5
|
+
module Deltas
|
6
|
+
def self.parse(index, options)
|
7
|
+
case options.delete(:delta)
|
8
|
+
when TrueClass, :default
|
9
|
+
DefaultDelta.new index, options
|
10
|
+
when :delayed
|
11
|
+
DelayedDelta.new index, options
|
12
|
+
when FalseClass, nil
|
13
|
+
nil
|
14
|
+
else
|
15
|
+
raise "Unknown delta type"
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module ThinkingSphinx
|
2
|
+
module Deltas
|
3
|
+
class DefaultDelta
|
4
|
+
attr_accessor :column
|
5
|
+
|
6
|
+
def initialize(index, options)
|
7
|
+
@index = index
|
8
|
+
@column = options.delete(:column) || :delta
|
9
|
+
end
|
10
|
+
|
11
|
+
def index(model, instance = nil)
|
12
|
+
return true unless ThinkingSphinx.updates_enabled? &&
|
13
|
+
ThinkingSphinx.deltas_enabled?
|
14
|
+
|
15
|
+
config = ThinkingSphinx::Configuration.instance
|
16
|
+
client = Riddle::Client.new config.address, config.port
|
17
|
+
|
18
|
+
client.update(
|
19
|
+
core_index_name(model),
|
20
|
+
['sphinx_deleted'],
|
21
|
+
{instance.sphinx_document_id => [1]}
|
22
|
+
) if instance && ThinkingSphinx.sphinx_running? && instance.in_core_index?
|
23
|
+
|
24
|
+
output = `#{config.bin_path}indexer --config #{config.config_file} --rotate #{delta_index_name model}`
|
25
|
+
puts output unless ThinkingSphinx.suppress_delta_output?
|
26
|
+
|
27
|
+
true
|
28
|
+
end
|
29
|
+
|
30
|
+
def toggle(instance)
|
31
|
+
instance.delta = true
|
32
|
+
end
|
33
|
+
|
34
|
+
def clause(model, toggled)
|
35
|
+
"#{model.quoted_table_name}.#{@index.quote_column(@column.to_s)}" +
|
36
|
+
" = #{@index.db_boolean(toggled)}"
|
37
|
+
end
|
38
|
+
|
39
|
+
protected
|
40
|
+
|
41
|
+
def core_index_name(model)
|
42
|
+
"#{model.source_of_sphinx_index.name.underscore.tr(':/\\', '_')}_core"
|
43
|
+
end
|
44
|
+
|
45
|
+
def delta_index_name(model)
|
46
|
+
"#{model.source_of_sphinx_index.name.underscore.tr(':/\\', '_')}_delta"
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'delayed/job'
|
2
|
+
|
3
|
+
require 'thinking_sphinx/deltas/delayed_delta/delta_job'
|
4
|
+
require 'thinking_sphinx/deltas/delayed_delta/flag_as_deleted_job'
|
5
|
+
require 'thinking_sphinx/deltas/delayed_delta/job'
|
6
|
+
|
7
|
+
module ThinkingSphinx
|
8
|
+
module Deltas
|
9
|
+
class DelayedDelta < ThinkingSphinx::Deltas::DefaultDelta
|
10
|
+
def index(model, instance = nil)
|
11
|
+
ThinkingSphinx::Deltas::Job.enqueue(
|
12
|
+
ThinkingSphinx::Deltas::DeltaJob.new(delta_index_name(model))
|
13
|
+
)
|
14
|
+
|
15
|
+
Delayed::Job.enqueue(
|
16
|
+
ThinkingSphinx::Deltas::FlagAsDeletedJob.new(
|
17
|
+
core_index_name(model), instance.sphinx_document_id
|
18
|
+
)
|
19
|
+
) if instance
|
20
|
+
|
21
|
+
true
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module ThinkingSphinx
|
2
|
+
module Deltas
|
3
|
+
class DeltaJob
|
4
|
+
attr_accessor :index
|
5
|
+
|
6
|
+
def initialize(index)
|
7
|
+
@index = index
|
8
|
+
end
|
9
|
+
|
10
|
+
def perform
|
11
|
+
return true unless ThinkingSphinx.updates_enabled? &&
|
12
|
+
ThinkingSphinx.deltas_enabled?
|
13
|
+
|
14
|
+
config = ThinkingSphinx::Configuration.instance
|
15
|
+
client = Riddle::Client.new config.address, config.port
|
16
|
+
|
17
|
+
output = `#{config.bin_path}indexer --config #{config.config_file} --rotate #{index}`
|
18
|
+
puts output unless ThinkingSphinx.suppress_delta_output?
|
19
|
+
|
20
|
+
true
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module ThinkingSphinx
|
2
|
+
module Deltas
|
3
|
+
class FlagAsDeletedJob
|
4
|
+
attr_accessor :index, :document_id
|
5
|
+
|
6
|
+
def initialize(index, document_id)
|
7
|
+
@index, @document_id = index, document_id
|
8
|
+
end
|
9
|
+
|
10
|
+
def perform
|
11
|
+
return true unless ThinkingSphinx.updates_enabled?
|
12
|
+
|
13
|
+
config = ThinkingSphinx::Configuration.instance
|
14
|
+
client = Riddle::Client.new config.address, config.port
|
15
|
+
|
16
|
+
client.update(
|
17
|
+
@index,
|
18
|
+
['sphinx_deleted'],
|
19
|
+
{@document_id => [1]}
|
20
|
+
) if ThinkingSphinx.sphinx_running? &&
|
21
|
+
ThinkingSphinx::Search.search_for_id(@document_id, @index)
|
22
|
+
|
23
|
+
true
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module ThinkingSphinx
|
2
|
+
module Deltas
|
3
|
+
class Job < Delayed::Job
|
4
|
+
def self.enqueue(object, priority = 0)
|
5
|
+
super unless duplicates_exist(object)
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.cancel_thinking_sphinx_jobs
|
9
|
+
delete_all("handler LIKE '--- !ruby/object:ThinkingSphinx::Deltas::%'")
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
|
14
|
+
def self.duplicates_exist(object)
|
15
|
+
count(
|
16
|
+
:conditions => {
|
17
|
+
:handler => object.to_yaml,
|
18
|
+
:locked_at => nil
|
19
|
+
}
|
20
|
+
) > 0
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -10,7 +10,7 @@ module ThinkingSphinx
|
|
10
10
|
#
|
11
11
|
class Index
|
12
12
|
attr_accessor :model, :fields, :attributes, :conditions, :groupings,
|
13
|
-
:
|
13
|
+
:delta_object, :options
|
14
14
|
|
15
15
|
# Create a new index instance by passing in the model it is tied to, and
|
16
16
|
# a block to build it with (optional but recommended). For documentation
|
@@ -34,7 +34,7 @@ module ThinkingSphinx
|
|
34
34
|
@conditions = []
|
35
35
|
@groupings = []
|
36
36
|
@options = {}
|
37
|
-
@
|
37
|
+
@delta_object = nil
|
38
38
|
|
39
39
|
initialize_from_builder(&block) if block_given?
|
40
40
|
end
|
@@ -127,7 +127,7 @@ module ThinkingSphinx
|
|
127
127
|
|
128
128
|
where_clause = ""
|
129
129
|
if self.delta?
|
130
|
-
where_clause << " AND #{@
|
130
|
+
where_clause << " AND #{@delta_object.clause(@model, options[:delta])}"
|
131
131
|
end
|
132
132
|
unless @conditions.empty?
|
133
133
|
where_clause << " AND " << @conditions.join(" AND ")
|
@@ -190,23 +190,14 @@ GROUP BY #{ (
|
|
190
190
|
|
191
191
|
sql = "SELECT #{min_statement}, #{max_statement} " +
|
192
192
|
"FROM #{@model.quoted_table_name} "
|
193
|
-
sql << "WHERE #{@
|
194
|
-
"= #{options[:delta] ? db_boolean(true) : db_boolean(false)}" if self.delta?
|
193
|
+
sql << "WHERE #{@delta_object.clause(@model, options[:delta])}" if self.delta?
|
195
194
|
sql
|
196
195
|
end
|
197
196
|
|
198
|
-
# Returns the SQL query to run before a full index - ie: nothing unless the
|
199
|
-
# index has a delta, and then it's an update statement to set delta values
|
200
|
-
# back to 0.
|
201
|
-
#
|
202
|
-
def to_sql_query_pre
|
203
|
-
self.delta? ? "UPDATE #{@model.quoted_table_name} SET #{quote_column('delta')} = #{db_boolean(false)}" : ""
|
204
|
-
end
|
205
|
-
|
206
197
|
# Flag to indicate whether this index has a corresponding delta index.
|
207
198
|
#
|
208
199
|
def delta?
|
209
|
-
|
200
|
+
!@delta_object.nil?
|
210
201
|
end
|
211
202
|
|
212
203
|
def adapter
|
@@ -244,16 +235,27 @@ GROUP BY #{ (
|
|
244
235
|
all_source_options
|
245
236
|
end
|
246
237
|
|
238
|
+
def quote_column(column)
|
239
|
+
@model.connection.quote_column_name(column)
|
240
|
+
end
|
241
|
+
|
242
|
+
# Returns the proper boolean value string literal for the
|
243
|
+
# current database adapter.
|
244
|
+
#
|
245
|
+
def db_boolean(val)
|
246
|
+
if adapter == :postgres
|
247
|
+
val ? 'TRUE' : 'FALSE'
|
248
|
+
else
|
249
|
+
val ? '1' : '0'
|
250
|
+
end
|
251
|
+
end
|
252
|
+
|
247
253
|
private
|
248
254
|
|
249
255
|
def utf8?
|
250
256
|
self.index_options[:charset_type] == "utf-8"
|
251
257
|
end
|
252
258
|
|
253
|
-
def quote_column(column)
|
254
|
-
@model.connection.quote_column_name(column)
|
255
|
-
end
|
256
|
-
|
257
259
|
# Does all the magic with the block provided to the base #initialize.
|
258
260
|
# Creates a new class subclassed from Builder, and evaluates the block
|
259
261
|
# on it, then pulls all relevant settings - fields, attributes, conditions,
|
@@ -272,12 +274,12 @@ GROUP BY #{ (
|
|
272
274
|
builder.where("#{@model.quoted_table_name}.#{quote_column(@model.inheritance_column)} = '#{stored_class}'")
|
273
275
|
end
|
274
276
|
|
275
|
-
@fields
|
276
|
-
@attributes
|
277
|
-
@conditions
|
278
|
-
@groupings
|
279
|
-
@
|
280
|
-
@options
|
277
|
+
@fields = builder.fields
|
278
|
+
@attributes = builder.attributes
|
279
|
+
@conditions = builder.conditions
|
280
|
+
@groupings = builder.groupings
|
281
|
+
@delta_object = ThinkingSphinx::Deltas.parse self, builder.properties
|
282
|
+
@options = builder.properties
|
281
283
|
|
282
284
|
# We want to make sure that if the database doesn't exist, then Thinking
|
283
285
|
# Sphinx doesn't mind when running non-TS tasks (like db:create, db:drop
|
@@ -338,17 +340,6 @@ GROUP BY #{ (
|
|
338
340
|
@associations[key] ||= Association.children(@model, key)
|
339
341
|
end
|
340
342
|
|
341
|
-
# Returns the proper boolean value string literal for the
|
342
|
-
# current database adapter.
|
343
|
-
#
|
344
|
-
def db_boolean(val)
|
345
|
-
if adapter == :postgres
|
346
|
-
val ? 'TRUE' : 'FALSE'
|
347
|
-
else
|
348
|
-
val ? '1' : '0'
|
349
|
-
end
|
350
|
-
end
|
351
|
-
|
352
343
|
def crc_column
|
353
344
|
if @model.column_names.include?(@model.inheritance_column)
|
354
345
|
case adapter
|
@@ -423,7 +414,7 @@ GROUP BY #{ (
|
|
423
414
|
source.sql_query_range = to_sql_query_range(:delta => delta)
|
424
415
|
source.sql_query_info = to_sql_query_info(offset)
|
425
416
|
|
426
|
-
source.sql_query_pre += send(delta ? :sql_query_pre_for_core : :sql_query_pre_for_delta)
|
417
|
+
source.sql_query_pre += send(!delta ? :sql_query_pre_for_core : :sql_query_pre_for_delta)
|
427
418
|
|
428
419
|
if @options[:group_concat_max_len]
|
429
420
|
source.sql_query_pre << "SET SESSION group_concat_max_len = #{@options[:group_concat_max_len]}"
|
@@ -445,8 +436,11 @@ GROUP BY #{ (
|
|
445
436
|
end
|
446
437
|
|
447
438
|
def sql_query_pre_for_core
|
448
|
-
|
449
|
-
|
439
|
+
if self.delta?
|
440
|
+
["UPDATE #{@model.quoted_table_name} SET #{@delta_object.clause(@model, false)}"]
|
441
|
+
else
|
442
|
+
[]
|
443
|
+
end
|
450
444
|
end
|
451
445
|
|
452
446
|
def sql_query_pre_for_delta
|
@@ -65,4 +65,57 @@ end
|
|
65
65
|
|
66
66
|
ActiveRecord::Base.extend(
|
67
67
|
ThinkingSphinx::ActiveRecordStoreFullSTIClass
|
68
|
-
) unless ActiveRecord::Base.respond_to?(:store_full_sti_class)
|
68
|
+
) unless ActiveRecord::Base.respond_to?(:store_full_sti_class)
|
69
|
+
|
70
|
+
module ThinkingSphinx
|
71
|
+
module ClassAttributeMethods
|
72
|
+
def cattr_reader(*syms)
|
73
|
+
syms.flatten.each do |sym|
|
74
|
+
next if sym.is_a?(Hash)
|
75
|
+
class_eval(<<-EOS, __FILE__, __LINE__)
|
76
|
+
unless defined? @@#{sym}
|
77
|
+
@@#{sym} = nil
|
78
|
+
end
|
79
|
+
|
80
|
+
def self.#{sym}
|
81
|
+
@@#{sym}
|
82
|
+
end
|
83
|
+
|
84
|
+
def #{sym}
|
85
|
+
@@#{sym}
|
86
|
+
end
|
87
|
+
EOS
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def cattr_writer(*syms)
|
92
|
+
options = syms.extract_options!
|
93
|
+
syms.flatten.each do |sym|
|
94
|
+
class_eval(<<-EOS, __FILE__, __LINE__)
|
95
|
+
unless defined? @@#{sym}
|
96
|
+
@@#{sym} = nil
|
97
|
+
end
|
98
|
+
|
99
|
+
def self.#{sym}=(obj)
|
100
|
+
@@#{sym} = obj
|
101
|
+
end
|
102
|
+
|
103
|
+
#{"
|
104
|
+
def #{sym}=(obj)
|
105
|
+
@@#{sym} = obj
|
106
|
+
end
|
107
|
+
" unless options[:instance_writer] == false }
|
108
|
+
EOS
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def cattr_accessor(*syms)
|
113
|
+
cattr_reader(*syms)
|
114
|
+
cattr_writer(*syms)
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
Class.extend(
|
120
|
+
ThinkingSphinx::ClassAttributeMethods
|
121
|
+
) unless Class.respond_to?(:cattr_reader)
|
@@ -22,7 +22,7 @@ describe "ThinkingSphinx::ActiveRecord::Delta" do
|
|
22
22
|
describe "suspended_delta method" do
|
23
23
|
before :each do
|
24
24
|
ThinkingSphinx.stub_method(:deltas_enabled? => true)
|
25
|
-
Person.stub_method(:` => "")
|
25
|
+
Person.sphinx_indexes.first.delta_object.stub_method(:` => "")
|
26
26
|
end
|
27
27
|
|
28
28
|
it "should execute the argument block with deltas disabled" do
|
@@ -71,8 +71,8 @@ describe "ThinkingSphinx::ActiveRecord::Delta" do
|
|
71
71
|
describe "index_delta method" do
|
72
72
|
before :each do
|
73
73
|
ThinkingSphinx::Configuration.stub_method(:environment => "spec")
|
74
|
-
ThinkingSphinx.stub_method(:deltas_enabled? => true)
|
75
|
-
Person.stub_method(:` => "")
|
74
|
+
ThinkingSphinx.stub_method(:deltas_enabled? => true, :sphinx_running? => true)
|
75
|
+
Person.sphinx_indexes.first.delta_object.stub_method(:` => "")
|
76
76
|
|
77
77
|
@person = Person.new
|
78
78
|
@person.stub_method(
|
@@ -89,7 +89,7 @@ describe "ThinkingSphinx::ActiveRecord::Delta" do
|
|
89
89
|
|
90
90
|
@person.send(:index_delta)
|
91
91
|
|
92
|
-
Person.should_not have_received(:`)
|
92
|
+
Person.sphinx_indexes.first.delta_object.should_not have_received(:`)
|
93
93
|
@client.should_not have_received(:update)
|
94
94
|
end
|
95
95
|
|
@@ -98,7 +98,7 @@ describe "ThinkingSphinx::ActiveRecord::Delta" do
|
|
98
98
|
|
99
99
|
@person.send(:index_delta)
|
100
100
|
|
101
|
-
Person.should_not have_received(:`)
|
101
|
+
Person.sphinx_indexes.first.delta_object.should_not have_received(:`)
|
102
102
|
end
|
103
103
|
|
104
104
|
it "shouldn't index if the environment is 'test'" do
|
@@ -108,13 +108,13 @@ describe "ThinkingSphinx::ActiveRecord::Delta" do
|
|
108
108
|
|
109
109
|
@person.send(:index_delta)
|
110
110
|
|
111
|
-
Person.should_not have_received(:`)
|
111
|
+
Person.sphinx_indexes.first.delta_object.should_not have_received(:`)
|
112
112
|
end
|
113
113
|
|
114
114
|
it "should call indexer for the delta index" do
|
115
115
|
@person.send(:index_delta)
|
116
116
|
|
117
|
-
Person.should have_received(:`).with(
|
117
|
+
Person.sphinx_indexes.first.delta_object.should have_received(:`).with(
|
118
118
|
"#{ThinkingSphinx::Configuration.instance.bin_path}indexer --config #{ThinkingSphinx::Configuration.instance.config_file} --rotate person_delta"
|
119
119
|
)
|
120
120
|
end
|
@@ -54,6 +54,8 @@ namespace :thinking_sphinx do
|
|
54
54
|
|
55
55
|
desc "Index data for Sphinx using Thinking Sphinx's settings"
|
56
56
|
task :index => :app_env do
|
57
|
+
ThinkingSphinx::Deltas::Job.cancel_thinking_sphinx_jobs
|
58
|
+
|
57
59
|
config = ThinkingSphinx::Configuration.instance
|
58
60
|
unless ENV["INDEX_ONLY"] == "true"
|
59
61
|
puts "Generating Configuration to #{config.config_file}"
|
@@ -66,6 +68,16 @@ namespace :thinking_sphinx do
|
|
66
68
|
puts cmd
|
67
69
|
system cmd
|
68
70
|
end
|
71
|
+
|
72
|
+
desc "Process stored delta index requests"
|
73
|
+
task :delta => :app_env do
|
74
|
+
require 'delayed/worker'
|
75
|
+
|
76
|
+
Delayed::Worker.new(
|
77
|
+
:min_priority => ENV['MIN_PRIORITY'],
|
78
|
+
:max_priority => ENV['MAX_PRIORITY']
|
79
|
+
).start
|
80
|
+
end
|
69
81
|
end
|
70
82
|
|
71
83
|
namespace :ts do
|
@@ -85,6 +97,8 @@ namespace :ts do
|
|
85
97
|
task :conf => "thinking_sphinx:configure"
|
86
98
|
desc "Generate the Sphinx configuration file using Thinking Sphinx's settings"
|
87
99
|
task :config => "thinking_sphinx:configure"
|
100
|
+
desc "Process stored delta index requests"
|
101
|
+
task :delta => "thinking_sphinx:delta"
|
88
102
|
end
|
89
103
|
|
90
104
|
def sphinx_pid
|
@@ -0,0 +1,251 @@
|
|
1
|
+
module Delayed
|
2
|
+
|
3
|
+
class DeserializationError < StandardError
|
4
|
+
end
|
5
|
+
|
6
|
+
class Job < ActiveRecord::Base
|
7
|
+
MAX_ATTEMPTS = 25
|
8
|
+
MAX_RUN_TIME = 4.hours
|
9
|
+
set_table_name :delayed_jobs
|
10
|
+
|
11
|
+
# By default failed jobs are destroyed after too many attempts.
|
12
|
+
# If you want to keep them around (perhaps to inspect the reason
|
13
|
+
# for the failure), set this to false.
|
14
|
+
cattr_accessor :destroy_failed_jobs
|
15
|
+
self.destroy_failed_jobs = true
|
16
|
+
|
17
|
+
# Every worker has a unique name which by default is the pid of the process.
|
18
|
+
# There are some advantages to overriding this with something which survives worker retarts:
|
19
|
+
# Workers can safely resume working on tasks which are locked by themselves. The worker will assume that it crashed before.
|
20
|
+
cattr_accessor :worker_name
|
21
|
+
self.worker_name = "host:#{Socket.gethostname} pid:#{Process.pid}" rescue "pid:#{Process.pid}"
|
22
|
+
|
23
|
+
NextTaskSQL = '(run_at <= ? AND (locked_at IS NULL OR locked_at < ?) OR (locked_by = ?)) AND failed_at IS NULL'
|
24
|
+
NextTaskOrder = 'priority DESC, run_at ASC'
|
25
|
+
|
26
|
+
ParseObjectFromYaml = /\!ruby\/\w+\:([^\s]+)/
|
27
|
+
|
28
|
+
cattr_accessor :min_priority, :max_priority
|
29
|
+
self.min_priority = nil
|
30
|
+
self.max_priority = nil
|
31
|
+
|
32
|
+
class LockError < StandardError
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.clear_locks!
|
36
|
+
update_all("locked_by = null, locked_at = null", ["locked_by = ?", worker_name])
|
37
|
+
end
|
38
|
+
|
39
|
+
def failed?
|
40
|
+
failed_at
|
41
|
+
end
|
42
|
+
alias_method :failed, :failed?
|
43
|
+
|
44
|
+
def payload_object
|
45
|
+
@payload_object ||= deserialize(self['handler'])
|
46
|
+
end
|
47
|
+
|
48
|
+
def name
|
49
|
+
@name ||= begin
|
50
|
+
payload = payload_object
|
51
|
+
if payload.respond_to?(:display_name)
|
52
|
+
payload.display_name
|
53
|
+
else
|
54
|
+
payload.class.name
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def payload_object=(object)
|
60
|
+
self['handler'] = object.to_yaml
|
61
|
+
end
|
62
|
+
|
63
|
+
def reschedule(message, backtrace = [], time = nil)
|
64
|
+
if self.attempts < MAX_ATTEMPTS
|
65
|
+
time ||= Job.db_time_now + (attempts ** 4) + 5
|
66
|
+
|
67
|
+
self.attempts += 1
|
68
|
+
self.run_at = time
|
69
|
+
self.last_error = message + "\n" + backtrace.join("\n")
|
70
|
+
self.unlock
|
71
|
+
save!
|
72
|
+
else
|
73
|
+
logger.info "* [JOB] PERMANENTLY removing #{self.name} because of #{attempts} consequetive failures."
|
74
|
+
destroy_failed_jobs ? destroy : update_attribute(:failed_at, Time.now)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def self.enqueue(*args, &block)
|
79
|
+
object = block_given? ? EvaledJob.new(&block) : args.shift
|
80
|
+
|
81
|
+
unless object.respond_to?(:perform) || block_given?
|
82
|
+
raise ArgumentError, 'Cannot enqueue items which do not respond to perform'
|
83
|
+
end
|
84
|
+
|
85
|
+
priority = args[0] || 0
|
86
|
+
run_at = args[1]
|
87
|
+
|
88
|
+
Job.create(:payload_object => object, :priority => priority.to_i, :run_at => run_at)
|
89
|
+
end
|
90
|
+
|
91
|
+
def self.find_available(limit = 5, max_run_time = MAX_RUN_TIME)
|
92
|
+
|
93
|
+
time_now = db_time_now
|
94
|
+
|
95
|
+
sql = NextTaskSQL.dup
|
96
|
+
|
97
|
+
conditions = [time_now, time_now - max_run_time, worker_name]
|
98
|
+
|
99
|
+
if self.min_priority
|
100
|
+
sql << ' AND (priority >= ?)'
|
101
|
+
conditions << min_priority
|
102
|
+
end
|
103
|
+
|
104
|
+
if self.max_priority
|
105
|
+
sql << ' AND (priority <= ?)'
|
106
|
+
conditions << max_priority
|
107
|
+
end
|
108
|
+
|
109
|
+
conditions.unshift(sql)
|
110
|
+
|
111
|
+
records = ActiveRecord::Base.silence do
|
112
|
+
find(:all, :conditions => conditions, :order => NextTaskOrder, :limit => limit)
|
113
|
+
end
|
114
|
+
|
115
|
+
records.sort_by { rand() }
|
116
|
+
end
|
117
|
+
|
118
|
+
# Get the payload of the next job we can get an exclusive lock on.
|
119
|
+
# If no jobs are left we return nil
|
120
|
+
def self.reserve(max_run_time = MAX_RUN_TIME, &block)
|
121
|
+
|
122
|
+
# We get up to 5 jobs from the db. In face we cannot get exclusive access to a job we try the next.
|
123
|
+
# this leads to a more even distribution of jobs across the worker processes
|
124
|
+
find_available(5, max_run_time).each do |job|
|
125
|
+
begin
|
126
|
+
logger.info "* [JOB] aquiring lock on #{job.name}"
|
127
|
+
job.lock_exclusively!(max_run_time, worker_name)
|
128
|
+
runtime = Benchmark.realtime do
|
129
|
+
invoke_job(job.payload_object, &block)
|
130
|
+
job.destroy
|
131
|
+
end
|
132
|
+
logger.info "* [JOB] #{job.name} completed after %.4f" % runtime
|
133
|
+
|
134
|
+
return job
|
135
|
+
rescue LockError
|
136
|
+
# We did not get the lock, some other worker process must have
|
137
|
+
logger.warn "* [JOB] failed to aquire exclusive lock for #{job.name}"
|
138
|
+
rescue StandardError => e
|
139
|
+
job.reschedule e.message, e.backtrace
|
140
|
+
log_exception(job, e)
|
141
|
+
return job
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
nil
|
146
|
+
end
|
147
|
+
|
148
|
+
# This method is used internally by reserve method to ensure exclusive access
|
149
|
+
# to the given job. It will rise a LockError if it cannot get this lock.
|
150
|
+
def lock_exclusively!(max_run_time, worker = worker_name)
|
151
|
+
now = self.class.db_time_now
|
152
|
+
affected_rows = if locked_by != worker
|
153
|
+
# We don't own this job so we will update the locked_by name and the locked_at
|
154
|
+
self.class.update_all(["locked_at = ?, locked_by = ?", now, worker], ["id = ? and (locked_at is null or locked_at < ?)", id, (now - max_run_time.to_i)])
|
155
|
+
else
|
156
|
+
# We already own this job, this may happen if the job queue crashes.
|
157
|
+
# Simply resume and update the locked_at
|
158
|
+
self.class.update_all(["locked_at = ?", now], ["id = ? and locked_by = ?", id, worker])
|
159
|
+
end
|
160
|
+
raise LockError.new("Attempted to aquire exclusive lock failed") unless affected_rows == 1
|
161
|
+
|
162
|
+
self.locked_at = now
|
163
|
+
self.locked_by = worker
|
164
|
+
end
|
165
|
+
|
166
|
+
def unlock
|
167
|
+
self.locked_at = nil
|
168
|
+
self.locked_by = nil
|
169
|
+
end
|
170
|
+
|
171
|
+
# This is a good hook if you need to report job processing errors in additional or different ways
|
172
|
+
def self.log_exception(job, error)
|
173
|
+
logger.error "* [JOB] #{job.name} failed with #{error.class.name}: #{error.message} - #{job.attempts} failed attempts"
|
174
|
+
logger.error(error)
|
175
|
+
end
|
176
|
+
|
177
|
+
def self.work_off(num = 100)
|
178
|
+
success, failure = 0, 0
|
179
|
+
|
180
|
+
num.times do
|
181
|
+
job = self.reserve do |j|
|
182
|
+
begin
|
183
|
+
j.perform
|
184
|
+
success += 1
|
185
|
+
rescue
|
186
|
+
failure += 1
|
187
|
+
raise
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
break if job.nil?
|
192
|
+
end
|
193
|
+
|
194
|
+
return [success, failure]
|
195
|
+
end
|
196
|
+
|
197
|
+
# Moved into its own method so that new_relic can trace it.
|
198
|
+
def self.invoke_job(job, &block)
|
199
|
+
block.call(job)
|
200
|
+
end
|
201
|
+
|
202
|
+
private
|
203
|
+
|
204
|
+
def deserialize(source)
|
205
|
+
handler = YAML.load(source) rescue nil
|
206
|
+
|
207
|
+
unless handler.respond_to?(:perform)
|
208
|
+
if handler.nil? && source =~ ParseObjectFromYaml
|
209
|
+
handler_class = $1
|
210
|
+
end
|
211
|
+
attempt_to_load(handler_class || handler.class)
|
212
|
+
handler = YAML.load(source)
|
213
|
+
end
|
214
|
+
|
215
|
+
return handler if handler.respond_to?(:perform)
|
216
|
+
|
217
|
+
raise DeserializationError,
|
218
|
+
'Job failed to load: Unknown handler. Try to manually require the appropiate file.'
|
219
|
+
rescue TypeError, LoadError, NameError => e
|
220
|
+
raise DeserializationError,
|
221
|
+
"Job failed to load: #{e.message}. Try to manually require the required file."
|
222
|
+
end
|
223
|
+
|
224
|
+
# Constantize the object so that ActiveSupport can attempt
|
225
|
+
# its auto loading magic. Will raise LoadError if not successful.
|
226
|
+
def attempt_to_load(klass)
|
227
|
+
klass.constantize
|
228
|
+
end
|
229
|
+
|
230
|
+
def self.db_time_now
|
231
|
+
(ActiveRecord::Base.default_timezone == :utc) ? Time.now.utc : Time.now
|
232
|
+
end
|
233
|
+
|
234
|
+
protected
|
235
|
+
|
236
|
+
def before_save
|
237
|
+
self.run_at ||= self.class.db_time_now
|
238
|
+
end
|
239
|
+
|
240
|
+
end
|
241
|
+
|
242
|
+
class EvaledJob
|
243
|
+
def initialize
|
244
|
+
@job = yield
|
245
|
+
end
|
246
|
+
|
247
|
+
def perform
|
248
|
+
eval(@job)
|
249
|
+
end
|
250
|
+
end
|
251
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module Delayed
|
2
|
+
class PerformableMethod < Struct.new(:object, :method, :args)
|
3
|
+
CLASS_STRING_FORMAT = /^CLASS\:([A-Z][\w\:]+)$/
|
4
|
+
AR_STRING_FORMAT = /^AR\:([A-Z][\w\:]+)\:(\d+)$/
|
5
|
+
|
6
|
+
def initialize(object, method, args)
|
7
|
+
raise NoMethodError, "undefined method `#{method}' for #{self.inspect}" unless object.respond_to?(method)
|
8
|
+
|
9
|
+
self.object = dump(object)
|
10
|
+
self.args = args.map { |a| dump(a) }
|
11
|
+
self.method = method.to_sym
|
12
|
+
end
|
13
|
+
|
14
|
+
def display_name
|
15
|
+
case self.object
|
16
|
+
when CLASS_STRING_FORMAT then "#{$1}.#{method}"
|
17
|
+
when AR_STRING_FORMAT then "#{$1}##{method}"
|
18
|
+
else "Unknown##{method}"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def perform
|
23
|
+
load(object).send(method, *args.map{|a| load(a)})
|
24
|
+
rescue ActiveRecord::RecordNotFound
|
25
|
+
# We cannot do anything about objects which were deleted in the meantime
|
26
|
+
true
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def load(arg)
|
32
|
+
case arg
|
33
|
+
when CLASS_STRING_FORMAT then $1.constantize
|
34
|
+
when AR_STRING_FORMAT then $1.constantize.find($2)
|
35
|
+
else arg
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def dump(arg)
|
40
|
+
case arg
|
41
|
+
when Class then class_to_string(arg)
|
42
|
+
when ActiveRecord::Base then ar_to_string(arg)
|
43
|
+
else arg
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def ar_to_string(obj)
|
48
|
+
"AR:#{obj.class}:#{obj.id}"
|
49
|
+
end
|
50
|
+
|
51
|
+
def class_to_string(obj)
|
52
|
+
"CLASS:#{obj.name}"
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module Delayed
|
2
|
+
class Worker
|
3
|
+
SLEEP = 5
|
4
|
+
|
5
|
+
cattr_accessor :logger
|
6
|
+
self.logger = if defined?(Merb::Logger)
|
7
|
+
Merb.logger
|
8
|
+
elsif defined?(RAILS_DEFAULT_LOGGER)
|
9
|
+
RAILS_DEFAULT_LOGGER
|
10
|
+
end
|
11
|
+
|
12
|
+
def initialize(options={})
|
13
|
+
@quiet = options[:quiet]
|
14
|
+
Delayed::Job.min_priority = options[:min_priority] if options.has_key?(:min_priority)
|
15
|
+
Delayed::Job.max_priority = options[:max_priority] if options.has_key?(:max_priority)
|
16
|
+
end
|
17
|
+
|
18
|
+
def start
|
19
|
+
say "*** Starting job worker #{Delayed::Job.worker_name}"
|
20
|
+
|
21
|
+
trap('TERM') { say 'Exiting...'; $exit = true }
|
22
|
+
trap('INT') { say 'Exiting...'; $exit = true }
|
23
|
+
|
24
|
+
loop do
|
25
|
+
result = nil
|
26
|
+
|
27
|
+
realtime = Benchmark.realtime do
|
28
|
+
result = Delayed::Job.work_off
|
29
|
+
end
|
30
|
+
|
31
|
+
count = result.sum
|
32
|
+
|
33
|
+
break if $exit
|
34
|
+
|
35
|
+
if count.zero?
|
36
|
+
sleep(SLEEP)
|
37
|
+
else
|
38
|
+
say "#{count} jobs processed at %.4f j/s, %d failed ..." % [count / realtime, result.last]
|
39
|
+
end
|
40
|
+
|
41
|
+
break if $exit
|
42
|
+
end
|
43
|
+
|
44
|
+
ensure
|
45
|
+
Delayed::Job.clear_locks!
|
46
|
+
end
|
47
|
+
|
48
|
+
def say(text)
|
49
|
+
puts text unless @quiet
|
50
|
+
logger.info text if logger
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
54
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: freelancing-god-thinking-sphinx
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 1.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Pat Allan
|
@@ -9,7 +9,7 @@ autorequire:
|
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
11
|
|
12
|
-
date:
|
12
|
+
date: 2009-01-03 00:00:00 -08:00
|
13
13
|
default_executable:
|
14
14
|
dependencies: []
|
15
15
|
|
@@ -33,6 +33,12 @@ files:
|
|
33
33
|
- lib/thinking_sphinx/attribute.rb
|
34
34
|
- lib/thinking_sphinx/collection.rb
|
35
35
|
- lib/thinking_sphinx/configuration.rb
|
36
|
+
- lib/thinking_sphinx/deltas/default_delta.rb
|
37
|
+
- lib/thinking_sphinx/deltas/delayed_delta/delta_job.rb
|
38
|
+
- lib/thinking_sphinx/deltas/delayed_delta/flag_as_deleted_job.rb
|
39
|
+
- lib/thinking_sphinx/deltas/delayed_delta/job.rb
|
40
|
+
- lib/thinking_sphinx/deltas/delayed_delta.rb
|
41
|
+
- lib/thinking_sphinx/deltas.rb
|
36
42
|
- lib/thinking_sphinx/field.rb
|
37
43
|
- lib/thinking_sphinx/index/builder.rb
|
38
44
|
- lib/thinking_sphinx/index/faux_column.rb
|
@@ -56,6 +62,13 @@ files:
|
|
56
62
|
- vendor/after_commit/README
|
57
63
|
- vendor/after_commit/test
|
58
64
|
- vendor/after_commit/test/after_commit_test.rb
|
65
|
+
- vendor/delayed_job
|
66
|
+
- vendor/delayed_job/lib
|
67
|
+
- vendor/delayed_job/lib/delayed
|
68
|
+
- vendor/delayed_job/lib/delayed/job.rb
|
69
|
+
- vendor/delayed_job/lib/delayed/message_sending.rb
|
70
|
+
- vendor/delayed_job/lib/delayed/performable_method.rb
|
71
|
+
- vendor/delayed_job/lib/delayed/worker.rb
|
59
72
|
- vendor/riddle
|
60
73
|
- vendor/riddle/lib
|
61
74
|
- vendor/riddle/lib/riddle
|