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.
@@ -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 = 0
32
- Minor = 9
33
- Tiny = 13
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
- return true unless ThinkingSphinx.updates_enabled? &&
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.delta = true
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
- :delta, :options
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
- @delta = false
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 #{@model.quoted_table_name}.#{quote_column('delta')}" +" = #{options[:delta] ? db_boolean(true) : db_boolean(false)}"
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 #{@model.quoted_table_name}.#{quote_column('delta')} " +
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
- @delta
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 = builder.fields
276
- @attributes = builder.attributes
277
- @conditions = builder.conditions
278
- @groupings = builder.groupings
279
- @delta = builder.properties[:delta]
280
- @options = builder.properties.except(:delta)
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
- delta? ? ["UPDATE #{@model.quoted_table_name} SET #{quote_column('delta')} = #{db_boolean(false)}"] : []
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,7 @@
1
+ module Delayed
2
+ module MessageSending
3
+ def send_later(method, *args)
4
+ Delayed::Job.enqueue Delayed::PerformableMethod.new(self, method.to_sym, args)
5
+ end
6
+ end
7
+ 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: 0.9.13
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: 2008-12-27 00:00:00 -08:00
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