bullet 2.0.0.beta.2 → 2.0.0.beta.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. data/.rspec +1 -0
  2. data/Hacking.textile +100 -0
  3. data/README.textile +36 -5
  4. data/README_for_rails2.textile +17 -0
  5. data/Rakefile +33 -16
  6. data/VERSION +1 -1
  7. data/autotest/discover.rb +1 -0
  8. data/bullet.gemspec +32 -9
  9. data/lib/bullet.rb +69 -38
  10. data/lib/bullet/action_controller2.rb +4 -4
  11. data/lib/bullet/active_record2.rb +16 -16
  12. data/lib/bullet/active_record3.rb +16 -25
  13. data/lib/bullet/detector.rb +9 -0
  14. data/lib/bullet/detector/association.rb +135 -0
  15. data/lib/bullet/detector/base.rb +19 -0
  16. data/lib/bullet/detector/counter.rb +43 -0
  17. data/lib/bullet/detector/n_plus_one_query.rb +39 -0
  18. data/lib/bullet/detector/unused_eager_association.rb +39 -0
  19. data/lib/bullet/notification.rb +4 -79
  20. data/lib/bullet/notification/base.rb +59 -0
  21. data/lib/bullet/notification/counter_cache.rb +13 -0
  22. data/lib/bullet/notification/n_plus_one_query.rb +32 -0
  23. data/lib/bullet/notification/unused_eager_loading.rb +14 -0
  24. data/lib/bullet/notification_collector.rb +25 -0
  25. data/lib/bullet/presenter.rb +13 -0
  26. data/lib/bullet/presenter/base.rb +9 -0
  27. data/lib/bullet/presenter/bullet_logger.rb +28 -0
  28. data/lib/bullet/presenter/growl.rb +40 -0
  29. data/lib/bullet/presenter/javascript_alert.rb +15 -0
  30. data/lib/bullet/presenter/javascript_console.rb +28 -0
  31. data/lib/bullet/presenter/javascript_helpers.rb +13 -0
  32. data/lib/bullet/presenter/rails_logger.rb +15 -0
  33. data/lib/bullet/presenter/xmpp.rb +56 -0
  34. data/lib/bullet/rack.rb +42 -0
  35. data/lib/bullet/registry.rb +7 -0
  36. data/lib/bullet/registry/association.rb +16 -0
  37. data/lib/bullet/registry/base.rb +39 -0
  38. data/lib/bullet/registry/object.rb +15 -0
  39. data/spec/bullet/association_for_chris_spec.rb +6 -6
  40. data/spec/bullet/association_for_peschkaj_spec.rb +6 -6
  41. data/spec/bullet/association_spec.rb +118 -262
  42. data/spec/bullet/counter_spec.rb +10 -10
  43. data/spec/spec_helper.rb +51 -17
  44. metadata +32 -9
  45. data/lib/bullet/association.rb +0 -294
  46. data/lib/bullet/counter.rb +0 -101
  47. data/lib/bullet/logger.rb +0 -9
  48. data/lib/bulletware.rb +0 -42
  49. data/spec/spec.opts +0 -3
@@ -3,7 +3,8 @@ module Bullet
3
3
  def self.enable
4
4
  require 'action_controller'
5
5
  case Rails.version
6
- when /^2.3/
6
+ when /^2.3/
7
+ ::ActionController::Dispatcher.middleware.use Bullet::Rack
7
8
  ::ActionController::Dispatcher.class_eval do
8
9
  class <<self
9
10
  alias_method :origin_reload_application, :reload_application
@@ -30,12 +31,11 @@ module Bullet
30
31
 
31
32
  if Bullet.notification?
32
33
  if response.headers["type"] and response.headers["type"].include? 'text/html' and response.body =~ %r{<html.*</html>}m
33
- response.body <<= Bullet.javascript_notification
34
+ response.body <<= Bullet.gather_inline_notifications
34
35
  response.headers["Content-Length"] = response.body.length.to_s
35
36
  end
36
37
 
37
- Bullet.growl_notification
38
- Bullet.log_notification(request.params['PATH_INFO'])
38
+ Bullet.perform_bullet_out_of_channel_notifications
39
39
  end
40
40
  Bullet.end_request
41
41
  response
@@ -12,11 +12,11 @@ module Bullet
12
12
 
13
13
  if records
14
14
  if records.size > 1
15
- Bullet::Association.add_possible_objects(records)
16
- Bullet::Counter.add_possible_objects(records)
15
+ Bullet::Detector::Association.add_possible_objects(records)
16
+ Bullet::Detector::Counter.add_possible_objects(records)
17
17
  elsif records.size == 1
18
- Bullet::Association.add_impossible_object(records.first)
19
- Bullet::Counter.add_impossible_object(records.first)
18
+ Bullet::Detector::Association.add_impossible_object(records.first)
19
+ Bullet::Detector::Counter.add_impossible_object(records.first)
20
20
  end
21
21
  end
22
22
 
@@ -33,9 +33,9 @@ module Bullet
33
33
  records = [records].flatten.compact.uniq
34
34
  return if records.empty?
35
35
  records.each do |record|
36
- Bullet::Association.add_object_associations(record, associations)
36
+ Bullet::Detector::Association.add_object_associations(record, associations)
37
37
  end
38
- Bullet::Association.add_eager_loadings(records, associations)
38
+ Bullet::Detector::Association.add_eager_loadings(records, associations)
39
39
  origin_preload_associations(records, associations, preload_options={})
40
40
  end
41
41
  end
@@ -47,10 +47,10 @@ module Bullet
47
47
  records = origin_find_with_associations(options)
48
48
  associations = merge_includes(scope(:find, :include), options[:include])
49
49
  records.each do |record|
50
- Bullet::Association.add_object_associations(record, associations)
51
- Bullet::Association.call_association(record, associations)
50
+ Bullet::Detector::Association.add_object_associations(record, associations)
51
+ Bullet::Detector::NPlusOneQuery.call_association(record, associations)
52
52
  end
53
- Bullet::Association.add_eager_loadings(records, associations)
53
+ Bullet::Detector::Association.add_eager_loadings(records, associations)
54
54
  records
55
55
  end
56
56
  end
@@ -60,8 +60,8 @@ module Bullet
60
60
  alias_method :origin_construct_association, :construct_association
61
61
  def construct_association(record, join, row)
62
62
  associations = join.reflection.name
63
- Bullet::Association.add_object_associations(record, associations)
64
- Bullet::Association.call_association(record, associations)
63
+ Bullet::Detector::Association.add_object_associations(record, associations)
64
+ Bullet::Detector::NPlusOneQuery.call_association(record, associations)
65
65
  origin_construct_association(record, join, row)
66
66
  end
67
67
  end
@@ -70,7 +70,7 @@ module Bullet
70
70
  # call one to many associations
71
71
  alias_method :origin_load_target, :load_target
72
72
  def load_target
73
- Bullet::Association.call_association(@owner, @reflection.name)
73
+ Bullet::Detector::NPlusOneQuery.call_association(@owner, @reflection.name)
74
74
  origin_load_target
75
75
  end
76
76
  end
@@ -81,8 +81,8 @@ module Bullet
81
81
  def load_target
82
82
  # avoid stack level too deep
83
83
  result = origin_load_target
84
- Bullet::Association.call_association(@owner, @reflection.name) unless caller.to_s.include? 'load_target'
85
- Bullet::Association.add_possible_objects(result)
84
+ Bullet::Detector::NPlusOneQuery.call_association(@owner, @reflection.name) unless caller.to_s.include? 'load_target'
85
+ Bullet::Detector::Association.add_possible_objects(result)
86
86
  result
87
87
  end
88
88
  end
@@ -91,7 +91,7 @@ module Bullet
91
91
  alias_method :origin_has_cached_counter?, :has_cached_counter?
92
92
  def has_cached_counter?
93
93
  result = origin_has_cached_counter?
94
- Bullet::Counter.add_counter_cache(@owner, @reflection.name) unless result
94
+ Bullet::Detector::Counter.add_counter_cache(@owner, @reflection.name) unless result
95
95
  result
96
96
  end
97
97
  end
@@ -100,7 +100,7 @@ module Bullet
100
100
  alias_method :origin_has_cached_counter?, :has_cached_counter?
101
101
  def has_cached_counter?
102
102
  result = origin_has_cached_counter?
103
- Bullet::Counter.add_counter_cache(@owner, @reflection.name) unless result
103
+ Bullet::Detector::Counter.add_counter_cache(@owner, @reflection.name) unless result
104
104
  result
105
105
  end
106
106
  end
@@ -9,11 +9,11 @@ module Bullet
9
9
  def to_a
10
10
  records = origin_to_a
11
11
  if records.size > 1
12
- Bullet::Association.add_possible_objects(records)
13
- Bullet::Counter.add_possible_objects(records)
12
+ Bullet::Detector::Association.add_possible_objects(records)
13
+ Bullet::Detector::Counter.add_possible_objects(records)
14
14
  elsif records.size == 1
15
- Bullet::Association.add_impossible_object(records.first)
16
- Bullet::Counter.add_impossible_object(records.first)
15
+ Bullet::Detector::Association.add_impossible_object(records.first)
16
+ Bullet::Detector::Counter.add_impossible_object(records.first)
17
17
  end
18
18
  records
19
19
  end
@@ -27,9 +27,9 @@ module Bullet
27
27
  records = [records].flatten.compact.uniq
28
28
  return if records.empty?
29
29
  records.each do |record|
30
- Bullet::Association.add_object_associations(record, associations)
30
+ Bullet::Detector::Association.add_object_associations(record, associations)
31
31
  end
32
- Bullet::Association.add_eager_loadings(records, associations)
32
+ Bullet::Detector::Association.add_eager_loadings(records, associations)
33
33
  origin_preload_associations(records, associations, preload_options={})
34
34
  end
35
35
  end
@@ -41,10 +41,10 @@ module Bullet
41
41
  records = origin_find_with_associations
42
42
  associations = (@eager_load_values + @includes_values).uniq
43
43
  records.each do |record|
44
- Bullet::Association.add_object_associations(record, associations)
45
- Bullet::Association.call_association(record, associations)
44
+ Bullet::Detector::Association.add_object_associations(record, associations)
45
+ Bullet::Detector::NPlusOneQuery.call_association(record, associations)
46
46
  end
47
- Bullet::Association.add_eager_loadings(records, associations)
47
+ Bullet::Detector::Association.add_eager_loadings(records, associations)
48
48
  records
49
49
  end
50
50
  end
@@ -54,8 +54,8 @@ module Bullet
54
54
  # call join associations
55
55
  def construct_association(record, join, row)
56
56
  associations = join.reflection.name
57
- Bullet::Association.add_object_associations(record, associations)
58
- Bullet::Association.call_association(record, associations)
57
+ Bullet::Detector::Association.add_object_associations(record, associations)
58
+ Bullet::Detector::NPlusOneQuery.call_association(record, associations)
59
59
  origin_construct_association(record, join, row)
60
60
  end
61
61
  end
@@ -64,7 +64,7 @@ module Bullet
64
64
  # call one to many associations
65
65
  alias_method :origin_load_target, :load_target
66
66
  def load_target
67
- Bullet::Association.call_association(@owner, @reflection.name)
67
+ Bullet::Detector::NPlusOneQuery.call_association(@owner, @reflection.name)
68
68
  origin_load_target
69
69
  end
70
70
  end
@@ -75,8 +75,8 @@ module Bullet
75
75
  def load_target
76
76
  # avoid stack level too deep
77
77
  result = origin_load_target
78
- Bullet::Association.call_association(@owner, @reflection.name) unless caller.to_s.include? 'load_target'
79
- Bullet::Association.add_possible_objects(result)
78
+ Bullet::Detector::NPlusOneQuery.call_association(@owner, @reflection.name) unless caller.to_s.include? 'load_target'
79
+ Bullet::Detector::Association.add_possible_objects(result)
80
80
  result
81
81
  end
82
82
  end
@@ -86,7 +86,7 @@ module Bullet
86
86
 
87
87
  def has_cached_counter?
88
88
  result = origin_has_cached_counter?
89
- Bullet::Counter.add_counter_cache(@owner, @reflection.name) unless result
89
+ Bullet::Detector::Counter.add_counter_cache(@owner, @reflection.name) unless result
90
90
  result
91
91
  end
92
92
  end
@@ -95,16 +95,7 @@ module Bullet
95
95
  alias_method :origin_has_cached_counter?, :has_cached_counter?
96
96
  def has_cached_counter?
97
97
  result = origin_has_cached_counter?
98
- Bullet::Counter.add_counter_cache(@owner, @reflection.name) unless result
99
- result
100
- end
101
- end
102
-
103
- ::ActiveRecord::Associations::HasManyThroughAssociation.class_eval do
104
- alias_method :origin_has_cached_counter?, :has_cached_counter?
105
- def has_cached_counter?
106
- result = origin_has_cached_counter?
107
- Bullet::Counter.add_counter_cache(@owner, @reflection.name) unless result
98
+ Bullet::Detector::Counter.add_counter_cache(@owner, @reflection.name) unless result
108
99
  result
109
100
  end
110
101
  end
@@ -0,0 +1,9 @@
1
+ module Bullet
2
+ module Detector
3
+ autoload :Base, 'bullet/detector/base'
4
+ autoload :Association, 'bullet/detector/association'
5
+ autoload :NPlusOneQuery, 'bullet/detector/n_plus_one_query'
6
+ autoload :UnusedEagerAssociation, 'bullet/detector/unused_eager_association'
7
+ autoload :Counter, 'bullet/detector/counter'
8
+ end
9
+ end
@@ -0,0 +1,135 @@
1
+ module Bullet
2
+ module Detector
3
+ class Association < Base
4
+ class <<self
5
+ def start_request
6
+ @@checked = false
7
+ end
8
+
9
+ def clear
10
+ # Note that under ruby class variables are shared among the class
11
+ # that declares them and all classes derived from that class.
12
+ # The following variables are accessible by all classes that
13
+ # derive from Bullet::Detector::Association - changing the variable
14
+ # in one subclass will make the change visible to all subclasses!
15
+ @@object_associations = nil
16
+ @@callers = nil
17
+ @@possible_objects = nil
18
+ @@impossible_objects = nil
19
+ @@call_object_associations = nil
20
+ @@eager_loadings = nil
21
+ end
22
+
23
+ def add_object_associations(object, associations)
24
+ object_associations.add( object, associations )
25
+ end
26
+
27
+ def add_call_object_associations(object, associations)
28
+ call_object_associations.add( object, associations )
29
+ end
30
+
31
+ def add_possible_objects(objects)
32
+ possible_objects.add objects
33
+ end
34
+
35
+ def add_impossible_object(object)
36
+ impossible_objects.add object
37
+ end
38
+
39
+ def add_eager_loadings(objects, associations)
40
+ objects = Array(objects)
41
+
42
+ eager_loadings.each do |k, v|
43
+ key_objects_overlap = k & objects
44
+
45
+ next if key_objects_overlap.empty?
46
+
47
+ if key_objects_overlap == k
48
+ eager_loadings.add k, associations
49
+ break
50
+
51
+ else
52
+ eager_loadings.merge key_objects_overlap, ( eager_loadings[k].dup << associations )
53
+
54
+ keys_without_objects = k - objects
55
+ eager_loadings.merge keys_without_objects, eager_loadings[k] unless keys_without_objects.empty?
56
+
57
+ eager_loadings.delete(k)
58
+ objects = objects - k
59
+ end
60
+ end
61
+
62
+ eager_loadings.add objects, associations unless objects.empty?
63
+ end
64
+
65
+ private
66
+ def possible?(object)
67
+ possible_objects.contains? object
68
+ end
69
+
70
+ def impossible?(object)
71
+ impossible_objects.contains? object
72
+ end
73
+
74
+ # check if object => associations already exists in object_associations.
75
+ def association?(object, associations)
76
+ object_associations.each do |key, value|
77
+ next unless key == object
78
+
79
+ value.each do |v|
80
+ result = v.is_a?(Hash) ? v.has_key?(associations) : v == associations
81
+ return true if result
82
+ end
83
+
84
+ end
85
+ return false
86
+ end
87
+
88
+ # object_associations keep the object relationships
89
+ # that the object has many associations.
90
+ # e.g. { <Post id:1> => [:comments] }
91
+ # the object_associations keep all associations that may be or may no be
92
+ # unpreload associations or unused preload associations.
93
+ def object_associations
94
+ @@object_associations ||= Bullet::Registry::Base.new
95
+ end
96
+
97
+ # call_object_assciations keep the object relationships
98
+ # that object.associations is called.
99
+ # e.g. { <Post id:1> => [:comments] }
100
+ # they are used to detect unused preload associations.
101
+ def call_object_associations
102
+ @@call_object_associations ||= Bullet::Registry::Base.new
103
+ end
104
+
105
+ # possible_objects keep the class to object relationships
106
+ # that the objects may cause N+1 query.
107
+ # e.g. { Post => [<Post id:1>, <Post id:2>] }
108
+ def possible_objects
109
+ @@possible_objects ||= Bullet::Registry::Object.new
110
+ end
111
+
112
+ # impossible_objects keep the class to objects relationships
113
+ # that the objects may not cause N+1 query.
114
+ # e.g. { Post => [<Post id:1>, <Post id:2>] }
115
+ # Notice: impossible_objects are not accurate,
116
+ # if find collection returns only one object, then the object is impossible object,
117
+ # impossible_objects are used to avoid treating 1+1 query to N+1 query.
118
+ def impossible_objects
119
+ @@impossible_objects ||= Bullet::Registry::Object.new
120
+ end
121
+
122
+ # eager_loadings keep the object relationships
123
+ # that the associations are preloaded by find :include.
124
+ # e.g. { [<Post id:1>, <Post id:2>] => [:comments, :user] }
125
+ def eager_loadings
126
+ @@eager_loadings ||= Bullet::Registry::Association.new
127
+ end
128
+
129
+ def callers
130
+ @@callers ||= []
131
+ end
132
+ end
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,19 @@
1
+ module Bullet
2
+ module Detector
3
+ class Base
4
+ def self.start_request
5
+ end
6
+
7
+ def self.end_request
8
+ clear
9
+ end
10
+
11
+ protected
12
+ def self.unique( array )
13
+ array.flatten!
14
+ array.uniq!
15
+ end
16
+
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,43 @@
1
+ module Bullet
2
+ module Detector
3
+ class Counter < Base
4
+ def self.clear
5
+ @@possible_objects = nil
6
+ @@impossible_objects = nil
7
+ end
8
+
9
+ def self.add_counter_cache(object, associations)
10
+ if conditions_met?( object, associations )
11
+ create_notification object.class, associations
12
+ end
13
+ end
14
+
15
+ def self.add_possible_objects(objects)
16
+ possible_objects.add objects
17
+ end
18
+
19
+ def self.add_impossible_object(object)
20
+ impossible_objects.add object
21
+ end
22
+
23
+ private
24
+ def self.create_notification( klazz, associations )
25
+ notice = Bullet::Notification::CounterCache.new klazz, associations
26
+ Bullet.notification_collector.add notice
27
+ end
28
+
29
+ def self.possible_objects
30
+ @@possible_objects ||= Bullet::Registry::Object.new
31
+ end
32
+
33
+ def self.impossible_objects
34
+ @@impossible_objects ||= Bullet::Registry::Object.new
35
+ end
36
+
37
+ def self.conditions_met?( object, associations )
38
+ possible_objects.contains?( object ) and
39
+ !impossible_objects.contains?( object )
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,39 @@
1
+ module Bullet
2
+ module Detector
3
+ class NPlusOneQuery < Association
4
+ # executed when object.assocations is called.
5
+ # first, it keeps this method call for object.association.
6
+ # then, it checks if this associations call is unpreload.
7
+ # if it is, keeps this unpreload associations and caller.
8
+ def self.call_association(object, associations)
9
+ @@checked = true
10
+ add_call_object_associations(object, associations)
11
+
12
+ if conditions_met?(object, associations)
13
+ caller_in_project
14
+ create_notification object.class, associations
15
+ end
16
+ end
17
+
18
+ private
19
+ def self.create_notification(klazz, associations)
20
+ notice = Bullet::Notification::NPlusOneQuery.new( callers, klazz, associations )
21
+ Bullet.notification_collector.add( notice )
22
+ end
23
+
24
+ # decide whether the object.associations is unpreloaded or not.
25
+ def self.conditions_met?(object, associations)
26
+ possible?(object) and
27
+ !impossible?(object) and
28
+ !association?(object, associations)
29
+ end
30
+
31
+ def self.caller_in_project
32
+ vender_root ||= File.join(Rails.root, 'vendor')
33
+ callers << caller.select { |c| c =~ /#{Rails.root}/ }.
34
+ reject { |c| c =~ /#{vender_root}/ }
35
+ callers.uniq!
36
+ end
37
+ end
38
+ end
39
+ end