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

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