warp-thinking-sphinx 1.2.12

Sign up to get free protection for your applications and to get access to all the features.
Files changed (190) hide show
  1. data/LICENCE +20 -0
  2. data/README.textile +144 -0
  3. data/VERSION.yml +4 -0
  4. data/features/a.rb +17 -0
  5. data/features/alternate_primary_key.feature +27 -0
  6. data/features/attribute_transformation.feature +22 -0
  7. data/features/attribute_updates.feature +33 -0
  8. data/features/datetime_deltas.feature +66 -0
  9. data/features/delayed_delta_indexing.feature +37 -0
  10. data/features/deleting_instances.feature +64 -0
  11. data/features/direct_attributes.feature +11 -0
  12. data/features/excerpts.feature +13 -0
  13. data/features/extensible_delta_indexing.feature +9 -0
  14. data/features/facets.feature +76 -0
  15. data/features/facets_across_model.feature +29 -0
  16. data/features/handling_edits.feature +92 -0
  17. data/features/retry_stale_indexes.feature +24 -0
  18. data/features/searching_across_models.feature +20 -0
  19. data/features/searching_by_model.feature +175 -0
  20. data/features/searching_with_find_arguments.feature +56 -0
  21. data/features/sphinx_detection.feature +25 -0
  22. data/features/sphinx_scopes.feature +35 -0
  23. data/features/step_definitions/alpha_steps.rb +3 -0
  24. data/features/step_definitions/beta_steps.rb +7 -0
  25. data/features/step_definitions/common_steps.rb +178 -0
  26. data/features/step_definitions/datetime_delta_steps.rb +15 -0
  27. data/features/step_definitions/delayed_delta_indexing_steps.rb +7 -0
  28. data/features/step_definitions/extensible_delta_indexing_steps.rb +7 -0
  29. data/features/step_definitions/facet_steps.rb +92 -0
  30. data/features/step_definitions/find_arguments_steps.rb +36 -0
  31. data/features/step_definitions/gamma_steps.rb +15 -0
  32. data/features/step_definitions/scope_steps.rb +11 -0
  33. data/features/step_definitions/search_steps.rb +89 -0
  34. data/features/step_definitions/sphinx_steps.rb +31 -0
  35. data/features/sti_searching.feature +14 -0
  36. data/features/support/db/active_record.rb +40 -0
  37. data/features/support/db/database.example.yml +3 -0
  38. data/features/support/db/fixtures/alphas.rb +10 -0
  39. data/features/support/db/fixtures/authors.rb +1 -0
  40. data/features/support/db/fixtures/betas.rb +10 -0
  41. data/features/support/db/fixtures/boxes.rb +9 -0
  42. data/features/support/db/fixtures/categories.rb +1 -0
  43. data/features/support/db/fixtures/cats.rb +3 -0
  44. data/features/support/db/fixtures/comments.rb +24 -0
  45. data/features/support/db/fixtures/delayed_betas.rb +10 -0
  46. data/features/support/db/fixtures/developers.rb +29 -0
  47. data/features/support/db/fixtures/dogs.rb +3 -0
  48. data/features/support/db/fixtures/extensible_betas.rb +10 -0
  49. data/features/support/db/fixtures/gammas.rb +10 -0
  50. data/features/support/db/fixtures/people.rb +1001 -0
  51. data/features/support/db/fixtures/posts.rb +6 -0
  52. data/features/support/db/fixtures/robots.rb +14 -0
  53. data/features/support/db/fixtures/tags.rb +27 -0
  54. data/features/support/db/fixtures/thetas.rb +10 -0
  55. data/features/support/db/migrations/create_alphas.rb +7 -0
  56. data/features/support/db/migrations/create_animals.rb +5 -0
  57. data/features/support/db/migrations/create_authors.rb +3 -0
  58. data/features/support/db/migrations/create_authors_posts.rb +6 -0
  59. data/features/support/db/migrations/create_betas.rb +5 -0
  60. data/features/support/db/migrations/create_boxes.rb +5 -0
  61. data/features/support/db/migrations/create_categories.rb +3 -0
  62. data/features/support/db/migrations/create_comments.rb +10 -0
  63. data/features/support/db/migrations/create_delayed_betas.rb +17 -0
  64. data/features/support/db/migrations/create_developers.rb +9 -0
  65. data/features/support/db/migrations/create_extensible_betas.rb +5 -0
  66. data/features/support/db/migrations/create_gammas.rb +3 -0
  67. data/features/support/db/migrations/create_people.rb +13 -0
  68. data/features/support/db/migrations/create_posts.rb +5 -0
  69. data/features/support/db/migrations/create_robots.rb +5 -0
  70. data/features/support/db/migrations/create_taggings.rb +5 -0
  71. data/features/support/db/migrations/create_tags.rb +4 -0
  72. data/features/support/db/migrations/create_thetas.rb +5 -0
  73. data/features/support/db/mysql.rb +3 -0
  74. data/features/support/db/postgresql.rb +3 -0
  75. data/features/support/env.rb +6 -0
  76. data/features/support/lib/generic_delta_handler.rb +8 -0
  77. data/features/support/models/alpha.rb +10 -0
  78. data/features/support/models/animal.rb +5 -0
  79. data/features/support/models/author.rb +3 -0
  80. data/features/support/models/beta.rb +8 -0
  81. data/features/support/models/box.rb +8 -0
  82. data/features/support/models/cat.rb +3 -0
  83. data/features/support/models/category.rb +4 -0
  84. data/features/support/models/comment.rb +10 -0
  85. data/features/support/models/delayed_beta.rb +7 -0
  86. data/features/support/models/developer.rb +16 -0
  87. data/features/support/models/dog.rb +3 -0
  88. data/features/support/models/extensible_beta.rb +9 -0
  89. data/features/support/models/gamma.rb +5 -0
  90. data/features/support/models/person.rb +23 -0
  91. data/features/support/models/post.rb +20 -0
  92. data/features/support/models/robot.rb +8 -0
  93. data/features/support/models/tag.rb +3 -0
  94. data/features/support/models/tagging.rb +4 -0
  95. data/features/support/models/theta.rb +7 -0
  96. data/features/support/post_database.rb +43 -0
  97. data/features/support/z.rb +19 -0
  98. data/lib/thinking_sphinx.rb +212 -0
  99. data/lib/thinking_sphinx/active_record.rb +306 -0
  100. data/lib/thinking_sphinx/active_record/attribute_updates.rb +48 -0
  101. data/lib/thinking_sphinx/active_record/delta.rb +87 -0
  102. data/lib/thinking_sphinx/active_record/has_many_association.rb +28 -0
  103. data/lib/thinking_sphinx/active_record/scopes.rb +39 -0
  104. data/lib/thinking_sphinx/adapters/abstract_adapter.rb +42 -0
  105. data/lib/thinking_sphinx/adapters/mysql_adapter.rb +54 -0
  106. data/lib/thinking_sphinx/adapters/postgresql_adapter.rb +136 -0
  107. data/lib/thinking_sphinx/association.rb +243 -0
  108. data/lib/thinking_sphinx/attribute.rb +340 -0
  109. data/lib/thinking_sphinx/class_facet.rb +15 -0
  110. data/lib/thinking_sphinx/configuration.rb +282 -0
  111. data/lib/thinking_sphinx/core/array.rb +7 -0
  112. data/lib/thinking_sphinx/core/string.rb +15 -0
  113. data/lib/thinking_sphinx/deltas.rb +30 -0
  114. data/lib/thinking_sphinx/deltas/datetime_delta.rb +50 -0
  115. data/lib/thinking_sphinx/deltas/default_delta.rb +68 -0
  116. data/lib/thinking_sphinx/deltas/delayed_delta.rb +30 -0
  117. data/lib/thinking_sphinx/deltas/delayed_delta/delta_job.rb +24 -0
  118. data/lib/thinking_sphinx/deltas/delayed_delta/flag_as_deleted_job.rb +27 -0
  119. data/lib/thinking_sphinx/deltas/delayed_delta/job.rb +26 -0
  120. data/lib/thinking_sphinx/deploy/capistrano.rb +100 -0
  121. data/lib/thinking_sphinx/excerpter.rb +22 -0
  122. data/lib/thinking_sphinx/facet.rb +125 -0
  123. data/lib/thinking_sphinx/facet_search.rb +134 -0
  124. data/lib/thinking_sphinx/field.rb +81 -0
  125. data/lib/thinking_sphinx/index.rb +99 -0
  126. data/lib/thinking_sphinx/index/builder.rb +286 -0
  127. data/lib/thinking_sphinx/index/faux_column.rb +110 -0
  128. data/lib/thinking_sphinx/property.rb +163 -0
  129. data/lib/thinking_sphinx/rails_additions.rb +150 -0
  130. data/lib/thinking_sphinx/search.rb +708 -0
  131. data/lib/thinking_sphinx/search_methods.rb +421 -0
  132. data/lib/thinking_sphinx/source.rb +152 -0
  133. data/lib/thinking_sphinx/source/internal_properties.rb +46 -0
  134. data/lib/thinking_sphinx/source/sql.rb +127 -0
  135. data/lib/thinking_sphinx/tasks.rb +165 -0
  136. data/rails/init.rb +14 -0
  137. data/spec/lib/thinking_sphinx/active_record/delta_spec.rb +130 -0
  138. data/spec/lib/thinking_sphinx/active_record/has_many_association_spec.rb +49 -0
  139. data/spec/lib/thinking_sphinx/active_record/scopes_spec.rb +96 -0
  140. data/spec/lib/thinking_sphinx/active_record_spec.rb +353 -0
  141. data/spec/lib/thinking_sphinx/association_spec.rb +239 -0
  142. data/spec/lib/thinking_sphinx/attribute_spec.rb +507 -0
  143. data/spec/lib/thinking_sphinx/configuration_spec.rb +268 -0
  144. data/spec/lib/thinking_sphinx/core/array_spec.rb +9 -0
  145. data/spec/lib/thinking_sphinx/core/string_spec.rb +9 -0
  146. data/spec/lib/thinking_sphinx/deltas/job_spec.rb +32 -0
  147. data/spec/lib/thinking_sphinx/excerpter_spec.rb +57 -0
  148. data/spec/lib/thinking_sphinx/facet_search_spec.rb +176 -0
  149. data/spec/lib/thinking_sphinx/facet_spec.rb +333 -0
  150. data/spec/lib/thinking_sphinx/field_spec.rb +154 -0
  151. data/spec/lib/thinking_sphinx/index/builder_spec.rb +455 -0
  152. data/spec/lib/thinking_sphinx/index/faux_column_spec.rb +30 -0
  153. data/spec/lib/thinking_sphinx/index_spec.rb +45 -0
  154. data/spec/lib/thinking_sphinx/rails_additions_spec.rb +203 -0
  155. data/spec/lib/thinking_sphinx/search_methods_spec.rb +152 -0
  156. data/spec/lib/thinking_sphinx/search_spec.rb +1101 -0
  157. data/spec/lib/thinking_sphinx/source_spec.rb +227 -0
  158. data/spec/lib/thinking_sphinx_spec.rb +162 -0
  159. data/tasks/distribution.rb +55 -0
  160. data/tasks/rails.rake +1 -0
  161. data/tasks/testing.rb +83 -0
  162. data/vendor/after_commit/LICENSE +20 -0
  163. data/vendor/after_commit/README +16 -0
  164. data/vendor/after_commit/Rakefile +22 -0
  165. data/vendor/after_commit/init.rb +8 -0
  166. data/vendor/after_commit/lib/after_commit.rb +45 -0
  167. data/vendor/after_commit/lib/after_commit/active_record.rb +114 -0
  168. data/vendor/after_commit/lib/after_commit/connection_adapters.rb +103 -0
  169. data/vendor/after_commit/test/after_commit_test.rb +53 -0
  170. data/vendor/delayed_job/lib/delayed/job.rb +251 -0
  171. data/vendor/delayed_job/lib/delayed/message_sending.rb +7 -0
  172. data/vendor/delayed_job/lib/delayed/performable_method.rb +55 -0
  173. data/vendor/delayed_job/lib/delayed/worker.rb +54 -0
  174. data/vendor/riddle/lib/riddle.rb +30 -0
  175. data/vendor/riddle/lib/riddle/client.rb +635 -0
  176. data/vendor/riddle/lib/riddle/client/filter.rb +53 -0
  177. data/vendor/riddle/lib/riddle/client/message.rb +66 -0
  178. data/vendor/riddle/lib/riddle/client/response.rb +84 -0
  179. data/vendor/riddle/lib/riddle/configuration.rb +33 -0
  180. data/vendor/riddle/lib/riddle/configuration/distributed_index.rb +48 -0
  181. data/vendor/riddle/lib/riddle/configuration/index.rb +142 -0
  182. data/vendor/riddle/lib/riddle/configuration/indexer.rb +19 -0
  183. data/vendor/riddle/lib/riddle/configuration/remote_index.rb +17 -0
  184. data/vendor/riddle/lib/riddle/configuration/searchd.rb +25 -0
  185. data/vendor/riddle/lib/riddle/configuration/section.rb +43 -0
  186. data/vendor/riddle/lib/riddle/configuration/source.rb +23 -0
  187. data/vendor/riddle/lib/riddle/configuration/sql_source.rb +34 -0
  188. data/vendor/riddle/lib/riddle/configuration/xml_source.rb +28 -0
  189. data/vendor/riddle/lib/riddle/controller.rb +53 -0
  190. metadata +267 -0
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2008 Nick Muerdter
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,16 @@
1
+ after_commit
2
+ ===========
3
+
4
+ A Ruby on Rails plugin to add after_commit callbacks. The callbacks that are provided can be used
5
+ to trigger events that run only after the entire transaction is complete. This is beneficial
6
+ in situations where you are doing asynchronous processing and need committed objects.
7
+
8
+ The following callbacks are provided:
9
+
10
+ * (1) after_commit
11
+ * (2) after_commit_on_create
12
+ * (3) after_commit_on_update
13
+ * (4) after_commit_on_destroy
14
+
15
+ The after_commit callback is run for any object that has just been committed. You can obtain finer
16
+ callback control by using the additional <tt>after_commit_on_*</tt> callbacks.
@@ -0,0 +1,22 @@
1
+ require 'rake'
2
+ require 'rake/testtask'
3
+ require 'rake/rdoctask'
4
+
5
+ desc 'Default: run unit tests.'
6
+ task :default => :test
7
+
8
+ desc 'Test the after_commit plugin.'
9
+ Rake::TestTask.new(:test) do |t|
10
+ t.libs << 'lib'
11
+ t.pattern = 'test/**/*_test.rb'
12
+ t.verbose = true
13
+ end
14
+
15
+ desc 'Generate documentation for the after_commit plugin.'
16
+ Rake::RDocTask.new(:rdoc) do |rdoc|
17
+ rdoc.rdoc_dir = 'rdoc'
18
+ rdoc.title = 'AfterCommit'
19
+ rdoc.options << '--line-numbers' << '--inline-source'
20
+ rdoc.rdoc_files.include('README')
21
+ rdoc.rdoc_files.include('lib/**/*.rb')
22
+ end
@@ -0,0 +1,8 @@
1
+ ActiveRecord::Base.send(:include, AfterCommit::ActiveRecord)
2
+
3
+ Object.subclasses_of(ActiveRecord::ConnectionAdapters::AbstractAdapter).each do |klass|
4
+ klass.send(:include, AfterCommit::ConnectionAdapters)
5
+ end
6
+ if defined?(JRUBY_VERSION) and defined?(JdbcSpec::MySQL)
7
+ JdbcSpec::MySQL.send :include, AfterCommit::ConnectionAdapters
8
+ end
@@ -0,0 +1,45 @@
1
+ require 'after_commit/active_record'
2
+ require 'after_commit/connection_adapters'
3
+
4
+ module AfterCommit
5
+ def self.committed_records
6
+ @@committed_records ||= []
7
+ end
8
+
9
+ def self.committed_records=(committed_records)
10
+ @@committed_records = committed_records
11
+ end
12
+
13
+ def self.committed_records_on_create
14
+ @@committed_records_on_create ||= []
15
+ end
16
+
17
+ def self.committed_records_on_create=(committed_records)
18
+ @@committed_records_on_create = committed_records
19
+ end
20
+
21
+ def self.committed_records_on_update
22
+ @@committed_records_on_update ||= []
23
+ end
24
+
25
+ def self.committed_records_on_update=(committed_records)
26
+ @@committed_records_on_update = committed_records
27
+ end
28
+
29
+ def self.committed_records_on_destroy
30
+ @@committed_records_on_destroy ||= []
31
+ end
32
+
33
+ def self.committed_records_on_destroy=(committed_records)
34
+ @@committed_records_on_destroy = committed_records
35
+ end
36
+ end
37
+
38
+ ActiveRecord::Base.send(:include, AfterCommit::ActiveRecord)
39
+
40
+ Object.subclasses_of(ActiveRecord::ConnectionAdapters::AbstractAdapter).each do |klass|
41
+ klass.send(:include, AfterCommit::ConnectionAdapters)
42
+ end
43
+ if defined?(JRUBY_VERSION) and defined?(JdbcSpec::MySQL)
44
+ JdbcSpec::MySQL.send :include, AfterCommit::ConnectionAdapters
45
+ end
@@ -0,0 +1,114 @@
1
+ module AfterCommit
2
+ module ActiveRecord
3
+ # Based on the code found in Thinking Sphinx:
4
+ # http://ts.freelancing-gods.com/ which was based on code written by Eli
5
+ # Miller:
6
+ # http://elimiller.blogspot.com/2007/06/proper-cache-expiry-with-aftercommit.html
7
+ # with slight modification from Joost Hietbrink. And now me! Whew.
8
+ def self.included(base)
9
+ base.class_eval do
10
+ # The define_callbacks method was added post Rails 2.0.2 - if it
11
+ # doesn't exist, we define the callback manually
12
+ if respond_to?(:define_callbacks)
13
+ define_callbacks :after_commit,
14
+ :after_commit_on_create,
15
+ :after_commit_on_update,
16
+ :after_commit_on_destroy
17
+ else
18
+ class << self
19
+ # Handle after_commit callbacks - call all the registered callbacks.
20
+ def after_commit(*callbacks, &block)
21
+ callbacks << block if block_given?
22
+ write_inheritable_array(:after_commit, callbacks)
23
+ end
24
+
25
+ def after_commit_on_create(*callbacks, &block)
26
+ callbacks << block if block_given?
27
+ write_inheritable_array(:after_commit_on_create, callbacks)
28
+ end
29
+
30
+ def after_commit_on_update(*callbacks, &block)
31
+ callbacks << block if block_given?
32
+ write_inheritable_array(:after_commit_on_update, callbacks)
33
+ end
34
+
35
+ def after_commit_on_destroy(*callbacks, &block)
36
+ callbacks << block if block_given?
37
+ write_inheritable_array(:after_commit_on_destroy, callbacks)
38
+ end
39
+ end
40
+ end
41
+
42
+ after_save :add_committed_record
43
+ after_create :add_committed_record_on_create
44
+ after_update :add_committed_record_on_update
45
+ after_destroy :add_committed_record_on_destroy
46
+
47
+ # We need to keep track of records that have been saved or destroyed
48
+ # within this transaction.
49
+ def add_committed_record
50
+ AfterCommit.committed_records << self
51
+ end
52
+
53
+ def add_committed_record_on_create
54
+ AfterCommit.committed_records_on_create << self
55
+ end
56
+
57
+ def add_committed_record_on_update
58
+ AfterCommit.committed_records_on_update << self
59
+ end
60
+
61
+ def add_committed_record_on_destroy
62
+ AfterCommit.committed_records << self
63
+ AfterCommit.committed_records_on_destroy << self
64
+ end
65
+
66
+ def after_commit
67
+ # Deliberately blank.
68
+ end
69
+
70
+ # Wraps a call to the private callback method so that the the
71
+ # after_commit callback can be made from the ConnectionAdapters when
72
+ # the commit for the transaction has finally succeeded.
73
+ def after_commit_callback
74
+ call_after_commit_callback :after_commit
75
+ end
76
+
77
+ def after_commit_on_create_callback
78
+ call_after_commit_callback :after_commit_on_create
79
+ end
80
+
81
+ def after_commit_on_update_callback
82
+ call_after_commit_callback :after_commit_on_update
83
+ end
84
+
85
+ def after_commit_on_destroy_callback
86
+ call_after_commit_callback :after_commit_on_destroy
87
+ end
88
+
89
+ private
90
+
91
+ def call_after_commit_callback(call)
92
+ if can_call_after_commit call
93
+ callback call
94
+ clear_after_commit_call call
95
+ end
96
+ end
97
+
98
+ def can_call_after_commit(call)
99
+ @calls ||= {}
100
+ @calls[call] ||= false
101
+ if @calls[call]
102
+ return false
103
+ else
104
+ @calls[call] = true
105
+ end
106
+ end
107
+
108
+ def clear_after_commit_call(call)
109
+ @calls[call] = false
110
+ end
111
+ end
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,103 @@
1
+ module AfterCommit
2
+ module ConnectionAdapters
3
+ def self.included(base)
4
+ base.class_eval do
5
+ # The commit_db_transaction method gets called when the outermost
6
+ # transaction finishes and everything inside commits. We want to
7
+ # override it so that after this happens, any records that were saved
8
+ # or destroyed within this transaction now get their after_commit
9
+ # callback fired.
10
+ def commit_db_transaction_with_callback
11
+ commit_db_transaction_without_callback
12
+ trigger_after_commit_callbacks
13
+ trigger_after_commit_on_create_callbacks
14
+ trigger_after_commit_on_update_callbacks
15
+ trigger_after_commit_on_destroy_callbacks
16
+ end
17
+ alias_method_chain :commit_db_transaction, :callback
18
+
19
+ # In the event the transaction fails and rolls back, nothing inside
20
+ # should recieve the after_commit callback.
21
+ def rollback_db_transaction_with_callback
22
+ rollback_db_transaction_without_callback
23
+
24
+ AfterCommit.committed_records = []
25
+ AfterCommit.committed_records_on_create = []
26
+ AfterCommit.committed_records_on_update = []
27
+ AfterCommit.committed_records_on_destroy = []
28
+ end
29
+ alias_method_chain :rollback_db_transaction, :callback
30
+
31
+ protected
32
+ def trigger_after_commit_callbacks
33
+ # Trigger the after_commit callback for each of the committed
34
+ # records.
35
+ if AfterCommit.committed_records.any?
36
+ AfterCommit.committed_records.each do |record|
37
+ begin
38
+ record.after_commit_callback
39
+ rescue
40
+ end
41
+ end
42
+ end
43
+
44
+ # Make sure we clear out our list of committed records now that we've
45
+ # triggered the callbacks for each one.
46
+ AfterCommit.committed_records = []
47
+ end
48
+
49
+ def trigger_after_commit_on_create_callbacks
50
+ # Trigger the after_commit_on_create callback for each of the committed
51
+ # records.
52
+ if AfterCommit.committed_records_on_create.any?
53
+ AfterCommit.committed_records_on_create.each do |record|
54
+ begin
55
+ record.after_commit_on_create_callback
56
+ rescue
57
+ end
58
+ end
59
+ end
60
+
61
+ # Make sure we clear out our list of committed records now that we've
62
+ # triggered the callbacks for each one.
63
+ AfterCommit.committed_records_on_create = []
64
+ end
65
+
66
+ def trigger_after_commit_on_update_callbacks
67
+ # Trigger the after_commit_on_update callback for each of the committed
68
+ # records.
69
+ if AfterCommit.committed_records_on_update.any?
70
+ AfterCommit.committed_records_on_update.each do |record|
71
+ begin
72
+ record.after_commit_on_update_callback
73
+ rescue
74
+ end
75
+ end
76
+ end
77
+
78
+ # Make sure we clear out our list of committed records now that we've
79
+ # triggered the callbacks for each one.
80
+ AfterCommit.committed_records_on_update = []
81
+ end
82
+
83
+ def trigger_after_commit_on_destroy_callbacks
84
+ # Trigger the after_commit_on_destroy callback for each of the committed
85
+ # records.
86
+ if AfterCommit.committed_records_on_destroy.any?
87
+ AfterCommit.committed_records_on_destroy.each do |record|
88
+ begin
89
+ record.after_commit_on_destroy_callback
90
+ rescue
91
+ end
92
+ end
93
+ end
94
+
95
+ # Make sure we clear out our list of committed records now that we've
96
+ # triggered the callbacks for each one.
97
+ AfterCommit.committed_records_on_destroy = []
98
+ end
99
+ #end protected
100
+ end
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,53 @@
1
+ $LOAD_PATH.unshift(File.dirname(__FILE__) + '/../lib')
2
+ require 'test/unit'
3
+ require 'rubygems'
4
+ require 'activerecord'
5
+ require 'after_commit'
6
+ require 'after_commit/active_record'
7
+ require 'after_commit/connection_adapters'
8
+
9
+ ActiveRecord::Base.establish_connection({"adapter" => "sqlite3", "database" => 'test.sqlite3'})
10
+ begin
11
+ ActiveRecord::Base.connection.execute("drop table mock_records");
12
+ rescue
13
+ end
14
+ ActiveRecord::Base.connection.execute("create table mock_records(id int)");
15
+
16
+ require File.dirname(__FILE__) + '/../init.rb'
17
+
18
+ class MockRecord < ActiveRecord::Base
19
+ attr_accessor :after_commit_on_create_called
20
+ attr_accessor :after_commit_on_update_called
21
+ attr_accessor :after_commit_on_destroy_called
22
+
23
+ after_commit_on_create :do_create
24
+ def do_create
25
+ self.after_commit_on_create_called = true
26
+ end
27
+
28
+ after_commit_on_update :do_update
29
+ def do_update
30
+ self.after_commit_on_update_called = true
31
+ end
32
+
33
+ after_commit_on_create :do_destroy
34
+ def do_destroy
35
+ self.after_commit_on_destroy_called = true
36
+ end
37
+ end
38
+
39
+ class AfterCommitTest < Test::Unit::TestCase
40
+ def test_after_commit_on_create_is_called
41
+ assert_equal true, MockRecord.create!.after_commit_on_create_called
42
+ end
43
+
44
+ def test_after_commit_on_update_is_called
45
+ record = MockRecord.create!
46
+ record.save
47
+ assert_equal true, record.after_commit_on_update_called
48
+ end
49
+
50
+ def test_after_commit_on_destroy_is_called
51
+ assert_equal true, MockRecord.create!.destroy.after_commit_on_destroy_called
52
+ end
53
+ end
@@ -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