Commit 2e283c5d667f0c9324b06a78454b651eca481e4f
1 parent
a3ed065c
Exists in
master
and in
1 other branch
Refactor the problem merge system
* Extract the Problem#merge! in his own class * Extract the update cache system from problem in his own class * Welcome back to the backtrace_line_spec not found by a rspec spec because no end by _spec.rb
Showing
11 changed files
with
365 additions
and
84 deletions
Show diff stats
... | ... | @@ -0,0 +1,26 @@ |
1 | +require 'problem_destroy' | |
2 | + | |
3 | +class ProblemMerge | |
4 | + def initialize(*problems) | |
5 | + problems = problems.flatten.uniq | |
6 | + @merged_problem = problems[0] | |
7 | + @child_problems = problems[1..-1] | |
8 | + raise ArgumentError.new("need almost 2 uniq different problems") if @child_problems.empty? | |
9 | + end | |
10 | + attr_reader :merged_problem, :child_problems | |
11 | + | |
12 | + def merge | |
13 | + child_problems.each do |problem| | |
14 | + merged_problem.errs.concat Err.where(:problem_id => problem.id) | |
15 | + ProblemDestroy.execute(problem) | |
16 | + end | |
17 | + reset_cached_attributes | |
18 | + merged_problem | |
19 | + end | |
20 | + | |
21 | + private | |
22 | + | |
23 | + def reset_cached_attributes | |
24 | + ProblemUpdaterCache.new(merged_problem).update | |
25 | + end | |
26 | +end | ... | ... |
... | ... | @@ -0,0 +1,90 @@ |
1 | +class ProblemUpdaterCache | |
2 | + def initialize(problem, notice=nil) | |
3 | + @problem = problem | |
4 | + @notice = notice | |
5 | + end | |
6 | + attr_reader :problem | |
7 | + | |
8 | + ## | |
9 | + # Update cache information about child associate to this problem | |
10 | + # | |
11 | + # update the notices count, and some notice informations | |
12 | + # | |
13 | + # @return [ Problem ] the problem with this update | |
14 | + # | |
15 | + def update | |
16 | + update_notices_count | |
17 | + update_notices_cache | |
18 | + problem | |
19 | + end | |
20 | + | |
21 | + private | |
22 | + | |
23 | + def update_notices_count | |
24 | + if @notice | |
25 | + problem.inc(:notices_count, 1) | |
26 | + else | |
27 | + problem.update_attribute( | |
28 | + :notices_count, problem.notices.count | |
29 | + ) | |
30 | + end | |
31 | + end | |
32 | + | |
33 | + ## | |
34 | + # Update problem statistique from some notice information | |
35 | + # | |
36 | + def update_notices_cache | |
37 | + first_notice = notices.first | |
38 | + last_notice = notices.last | |
39 | + notice ||= @notice || first_notice | |
40 | + | |
41 | + attrs = {} | |
42 | + attrs[:first_notice_at] = first_notice.created_at if first_notice | |
43 | + attrs[:last_notice_at] = last_notice.created_at if last_notice | |
44 | + attrs.merge!( | |
45 | + :message => notice.message, | |
46 | + :environment => notice.environment_name, | |
47 | + :error_class => notice.error_class, | |
48 | + :where => notice.where, | |
49 | + :messages => attribute_count(:message, messages), | |
50 | + :hosts => attribute_count(:host, hosts), | |
51 | + :user_agents => attribute_count(:user_agent_string, user_agents) | |
52 | + ) if notice | |
53 | + problem.update_attributes!(attrs) | |
54 | + end | |
55 | + | |
56 | + def notices | |
57 | + @notices ||= @notice ? [@notice].sort(&:created_at) : problem.notices.order_by([:created_at, :asc]) | |
58 | + end | |
59 | + | |
60 | + def messages | |
61 | + @notice ? problem.messages : {} | |
62 | + end | |
63 | + | |
64 | + def hosts | |
65 | + @notice ? problem.hosts : {} | |
66 | + end | |
67 | + | |
68 | + def user_agents | |
69 | + @notice ? problem.user_agents : {} | |
70 | + end | |
71 | + | |
72 | + private | |
73 | + | |
74 | + def attribute_count(value, init) | |
75 | + init.tap do |counts| | |
76 | + notices.each do |notice| | |
77 | + counts[attribute_index(notice.send(value))] ||= { | |
78 | + 'value' => notice.send(value), | |
79 | + 'count' => 0 | |
80 | + } | |
81 | + counts[attribute_index(notice.send(value))]['count'] += 1 | |
82 | + end | |
83 | + end | |
84 | + end | |
85 | + | |
86 | + def attribute_index(value) | |
87 | + @attributes_index ||= {} | |
88 | + @attributes_index[value.to_s] ||= Digest::MD5.hexdigest(value.to_s) | |
89 | + end | |
90 | +end | ... | ... |
app/models/notice.rb
... | ... | @@ -26,7 +26,7 @@ class Notice |
26 | 26 | ] |
27 | 27 | ) |
28 | 28 | |
29 | - after_create :increase_counter_cache, :cache_attributes_on_problem, :unresolve_problem | |
29 | + after_create :cache_attributes_on_problem, :unresolve_problem | |
30 | 30 | before_save :sanitize |
31 | 31 | before_destroy :decrease_counter_cache, :remove_cached_attributes_from_problem |
32 | 32 | |
... | ... | @@ -116,10 +116,6 @@ class Notice |
116 | 116 | |
117 | 117 | protected |
118 | 118 | |
119 | - def increase_counter_cache | |
120 | - problem.inc(:notices_count, 1) | |
121 | - end | |
122 | - | |
123 | 119 | def decrease_counter_cache |
124 | 120 | problem.inc(:notices_count, -1) if err |
125 | 121 | end |
... | ... | @@ -133,7 +129,7 @@ class Notice |
133 | 129 | end |
134 | 130 | |
135 | 131 | def cache_attributes_on_problem |
136 | - problem.cache_notice_attributes(self) | |
132 | + ProblemUpdaterCache.new(problem, self).update | |
137 | 133 | end |
138 | 134 | |
139 | 135 | def sanitize | ... | ... |
app/models/problem.rb
... | ... | @@ -83,15 +83,7 @@ class Problem |
83 | 83 | |
84 | 84 | |
85 | 85 | def self.merge!(*problems) |
86 | - problems = problems.flatten.uniq | |
87 | - merged_problem = problems.shift | |
88 | - problems.each do |problem| | |
89 | - merged_problem.errs.concat Err.where(:problem_id => problem.id) | |
90 | - problem.errs(true) # reload problem.errs (should be empty) before problem.destroy | |
91 | - problem.destroy | |
92 | - end | |
93 | - merged_problem.reset_cached_attributes | |
94 | - merged_problem | |
86 | + ProblemMerge.new(problems).merge | |
95 | 87 | end |
96 | 88 | |
97 | 89 | def merged? |
... | ... | @@ -128,9 +120,7 @@ class Problem |
128 | 120 | |
129 | 121 | |
130 | 122 | def reset_cached_attributes |
131 | - update_attribute(:notices_count, notices.count) | |
132 | - cache_app_attributes | |
133 | - cache_notice_attributes | |
123 | + ProblemUpdaterCache.new(self).update | |
134 | 124 | end |
135 | 125 | |
136 | 126 | def cache_app_attributes |
... | ... | @@ -145,26 +135,6 @@ class Problem |
145 | 135 | end |
146 | 136 | end |
147 | 137 | |
148 | - def cache_notice_attributes(notice=nil) | |
149 | - first_notice = notices.order_by([:created_at, :asc]).first | |
150 | - last_notice = notices.order_by([:created_at, :asc]).last | |
151 | - notice ||= first_notice | |
152 | - | |
153 | - attrs = {} | |
154 | - attrs[:first_notice_at] = first_notice.created_at if first_notice | |
155 | - attrs[:last_notice_at] = last_notice.created_at if last_notice | |
156 | - attrs.merge!( | |
157 | - :message => notice.message, | |
158 | - :environment => notice.environment_name, | |
159 | - :error_class => notice.error_class, | |
160 | - :where => notice.where, | |
161 | - :messages => attribute_count_increase(:messages, notice.message), | |
162 | - :hosts => attribute_count_increase(:hosts, notice.host), | |
163 | - :user_agents => attribute_count_increase(:user_agents, notice.user_agent_string) | |
164 | - ) if notice | |
165 | - update_attributes!(attrs) | |
166 | - end | |
167 | - | |
168 | 138 | def remove_cached_notice_attributes(notice) |
169 | 139 | update_attributes!( |
170 | 140 | :messages => attribute_count_descrease(:messages, notice.message), |
... | ... | @@ -184,15 +154,6 @@ class Problem |
184 | 154 | end |
185 | 155 | |
186 | 156 | private |
187 | - def attribute_count_increase(name, value) | |
188 | - counter, index = send(name), attribute_index(value) | |
189 | - if counter[index].nil? | |
190 | - counter[index] = {'value' => value, 'count' => 1} | |
191 | - else | |
192 | - counter[index]['count'] += 1 | |
193 | - end | |
194 | - counter | |
195 | - end | |
196 | 157 | |
197 | 158 | def attribute_count_descrease(name, value) |
198 | 159 | counter, index = send(name), attribute_index(value) | ... | ... |
lib/tasks/errbit/database.rake
... | ... | @@ -6,7 +6,9 @@ namespace :errbit do |
6 | 6 | desc "Updates cached attributes on Problem" |
7 | 7 | task :update_problem_attrs => :environment do |
8 | 8 | puts "Updating problems" |
9 | - Problem.all.each(&:cache_notice_attributes) | |
9 | + Problem.all.each{|problem| | |
10 | + ProblemUpdaterCache.new(self).update | |
11 | + } | |
10 | 12 | end |
11 | 13 | |
12 | 14 | desc "Updates Problem#notices_count" | ... | ... |
spec/fabricators/problem_fabricator.rb
... | ... | @@ -10,3 +10,13 @@ Fabricator(:problem_with_comments, :from => :problem) do |
10 | 10 | end |
11 | 11 | } |
12 | 12 | end |
13 | + | |
14 | +Fabricator(:problem_with_errs, :from => :problem) do | |
15 | + after_create { |parent| | |
16 | + 3.times do | |
17 | + Fabricate(:err, :problem => parent) | |
18 | + end | |
19 | + } | |
20 | +end | |
21 | + | |
22 | + | ... | ... |
... | ... | @@ -0,0 +1,48 @@ |
1 | +require 'spec_helper' | |
2 | + | |
3 | +describe ProblemMerge do | |
4 | + let(:problem) { Fabricate(:problem_with_errs) } | |
5 | + let(:problem_1) { Fabricate(:problem_with_errs) } | |
6 | + | |
7 | + describe "#initialize" do | |
8 | + it 'failed if less than 2 uniq problem pass in args' do | |
9 | + expect { | |
10 | + ProblemMerge.new(problem) | |
11 | + }.to raise_error(ArgumentError) | |
12 | + end | |
13 | + | |
14 | + it 'extract first problem like merged_problem' do | |
15 | + problem_merge = ProblemMerge.new(problem, problem, problem_1) | |
16 | + expect(problem_merge.merged_problem).to eql problem | |
17 | + end | |
18 | + it 'extract other problem like child_problems' do | |
19 | + problem_merge = ProblemMerge.new(problem, problem, problem_1) | |
20 | + expect(problem_merge.child_problems).to eql [problem_1] | |
21 | + end | |
22 | + end | |
23 | + | |
24 | + describe "#merge" do | |
25 | + let!(:problem_merge) { | |
26 | + ProblemMerge.new(problem, problem_1) | |
27 | + } | |
28 | + let(:first_errs) { problem.errs } | |
29 | + let(:merged_errs) { problem_1.errs } | |
30 | + let!(:notice) { Fabricate(:notice, :err => first_errs.first) } | |
31 | + let!(:notice_1) { Fabricate(:notice, :err => merged_errs.first) } | |
32 | + it 'delete one of problem' do | |
33 | + expect { | |
34 | + problem_merge.merge | |
35 | + }.to change(Problem, :count).by(-1) | |
36 | + end | |
37 | + | |
38 | + it 'move all err in one problem' do | |
39 | + problem_merge.merge | |
40 | + problem.reload.errs.should eq (first_errs | merged_errs) | |
41 | + end | |
42 | + | |
43 | + it 'update problem cache' do | |
44 | + ProblemUpdaterCache.should_receive(:new).with(problem).and_return(mock(:update => true)) | |
45 | + problem_merge.merge | |
46 | + end | |
47 | + end | |
48 | +end | ... | ... |
... | ... | @@ -0,0 +1,153 @@ |
1 | +require 'spec_helper' | |
2 | + | |
3 | +describe ProblemUpdaterCache do | |
4 | + let(:problem) { Fabricate(:problem_with_errs) } | |
5 | + let(:first_errs) { problem.errs } | |
6 | + let!(:notice) { Fabricate(:notice, :err => first_errs.first) } | |
7 | + | |
8 | + describe "#update" do | |
9 | + context "without notice pass args" do | |
10 | + before do | |
11 | + problem.update_attribute(:notices_count, 0) | |
12 | + end | |
13 | + | |
14 | + it 'update the notice_count' do | |
15 | + expect { | |
16 | + ProblemUpdaterCache.new(problem).update | |
17 | + }.to change{ | |
18 | + problem.notices_count | |
19 | + }.from(0).to(1) | |
20 | + end | |
21 | + | |
22 | + context "with only one notice" do | |
23 | + before do | |
24 | + problem.update_attributes!(:messages => {}) | |
25 | + ProblemUpdaterCache.new(problem).update | |
26 | + end | |
27 | + | |
28 | + it 'update information about this notice' do | |
29 | + expect(problem.message).to eq notice.message | |
30 | + expect(problem.environment).to eq notice.environment_name | |
31 | + expect(problem.error_class).to eq notice.error_class | |
32 | + expect(problem.where).to eq notice.where | |
33 | + end | |
34 | + | |
35 | + it 'update first_notice_at' do | |
36 | + expect(problem.first_notice_at).to eq notice.created_at | |
37 | + end | |
38 | + | |
39 | + it 'update last_notice_at' do | |
40 | + expect(problem.last_notice_at).to eq notice.created_at | |
41 | + end | |
42 | + | |
43 | + it 'update stats messages' do | |
44 | + expect(problem.messages).to eq({ | |
45 | + Digest::MD5.hexdigest(notice.message) => {'value' => notice.message, 'count' => 1} | |
46 | + }) | |
47 | + end | |
48 | + | |
49 | + it 'update stats hosts' do | |
50 | + expect(problem.hosts).to eq({ | |
51 | + Digest::MD5.hexdigest(notice.host) => {'value' => notice.host, 'count' => 1} | |
52 | + }) | |
53 | + end | |
54 | + | |
55 | + it 'update stats user_agents' do | |
56 | + expect(problem.user_agents).to eq({ | |
57 | + Digest::MD5.hexdigest(notice.user_agent_string) => {'value' => notice.user_agent_string, 'count' => 1} | |
58 | + }) | |
59 | + end | |
60 | + end | |
61 | + | |
62 | + context "with several notices" do | |
63 | + let!(:notice_2) { Fabricate(:notice, :err => first_errs.first) } | |
64 | + let!(:notice_3) { Fabricate(:notice, :err => first_errs.first) } | |
65 | + before do | |
66 | + problem.update_attributes!(:messages => {}) | |
67 | + ProblemUpdaterCache.new(problem).update | |
68 | + end | |
69 | + it 'update information about this notice' do | |
70 | + expect(problem.message).to eq notice.message | |
71 | + expect(problem.environment).to eq notice.environment_name | |
72 | + expect(problem.error_class).to eq notice.error_class | |
73 | + expect(problem.where).to eq notice.where | |
74 | + end | |
75 | + | |
76 | + it 'update first_notice_at' do | |
77 | + expect(problem.first_notice_at.to_time).to eq notice.created_at.to_time | |
78 | + end | |
79 | + | |
80 | + it 'update last_notice_at' do | |
81 | + expect(problem.last_notice_at.to_i).to be_within(1).of(notice.created_at.to_i) | |
82 | + end | |
83 | + | |
84 | + it 'update stats messages' do | |
85 | + expect(problem.messages).to eq({ | |
86 | + Digest::MD5.hexdigest(notice.message) => {'value' => notice.message, 'count' => 3} | |
87 | + }) | |
88 | + end | |
89 | + | |
90 | + it 'update stats hosts' do | |
91 | + expect(problem.hosts).to eq({ | |
92 | + Digest::MD5.hexdigest(notice.host) => {'value' => notice.host, 'count' => 3} | |
93 | + }) | |
94 | + end | |
95 | + | |
96 | + it 'update stats user_agents' do | |
97 | + expect(problem.user_agents).to eq({ | |
98 | + Digest::MD5.hexdigest(notice.user_agent_string) => {'value' => notice.user_agent_string, 'count' => 3} | |
99 | + }) | |
100 | + end | |
101 | + | |
102 | + end | |
103 | + end | |
104 | + | |
105 | + context "with notice pass in args" do | |
106 | + | |
107 | + before do | |
108 | + ProblemUpdaterCache.new(problem, notice).update | |
109 | + end | |
110 | + | |
111 | + it 'increase notices_count by 1' do | |
112 | + expect { | |
113 | + ProblemUpdaterCache.new(problem, notice).update | |
114 | + }.to change{ | |
115 | + problem.notices_count | |
116 | + }.by(1) | |
117 | + end | |
118 | + | |
119 | + it 'update information about this notice' do | |
120 | + expect(problem.message).to eq notice.message | |
121 | + expect(problem.environment).to eq notice.environment_name | |
122 | + expect(problem.error_class).to eq notice.error_class | |
123 | + expect(problem.where).to eq notice.where | |
124 | + end | |
125 | + | |
126 | + it 'update first_notice_at' do | |
127 | + expect(problem.first_notice_at).to eq notice.created_at | |
128 | + end | |
129 | + | |
130 | + it 'update last_notice_at' do | |
131 | + expect(problem.last_notice_at).to eq notice.created_at | |
132 | + end | |
133 | + | |
134 | + it 'inc stats messages' do | |
135 | + expect(problem.messages).to eq({ | |
136 | + Digest::MD5.hexdigest(notice.message) => {'value' => notice.message, 'count' => 2} | |
137 | + }) | |
138 | + end | |
139 | + | |
140 | + it 'inc stats hosts' do | |
141 | + expect(problem.hosts).to eq({ | |
142 | + Digest::MD5.hexdigest(notice.host) => {'value' => notice.host, 'count' => 2} | |
143 | + }) | |
144 | + end | |
145 | + | |
146 | + it 'inc stats user_agents' do | |
147 | + expect(problem.user_agents).to eq({ | |
148 | + Digest::MD5.hexdigest(notice.user_agent_string) => {'value' => notice.user_agent_string, 'count' => 2} | |
149 | + }) | |
150 | + end | |
151 | + end | |
152 | + end | |
153 | +end | ... | ... |
spec/models/backtrace_line.rb
... | ... | @@ -1,12 +0,0 @@ |
1 | -require 'spec_helper' | |
2 | - | |
3 | -describe BacktraceLine do | |
4 | - subject { described_class.new(raw_line) } | |
5 | - | |
6 | - describe "root at the start of decorated filename" do | |
7 | - let(:raw_line) { { 'number' => rand(999), 'file' => '[PROJECT_ROOT]/app/controllers/pages_controller.rb', 'method' => ActiveSupport.methods.shuffle.first.to_s } } | |
8 | - it "should leave leading root symbol in filepath" do | |
9 | - subject.decorated_path.should == '/app/controllers/' | |
10 | - end | |
11 | - end | |
12 | -end |
... | ... | @@ -0,0 +1,12 @@ |
1 | +require 'spec_helper' | |
2 | + | |
3 | +describe BacktraceLine do | |
4 | + subject { described_class.new(raw_line) } | |
5 | + | |
6 | + describe "root at the start of decorated filename" do | |
7 | + let(:raw_line) { { 'number' => rand(999), 'file' => '[PROJECT_ROOT]/app/controllers/pages_controller.rb', 'method' => ActiveSupport.methods.shuffle.first.to_s } } | |
8 | + it "should leave leading root symbol in filepath" do | |
9 | + subject.decorated_path.should == 'app/controllers/' | |
10 | + end | |
11 | + end | |
12 | +end | ... | ... |
spec/models/problem_spec.rb
... | ... | @@ -23,6 +23,17 @@ describe Problem do |
23 | 23 | end.should change(Comment, :count).by(3) |
24 | 24 | end |
25 | 25 | end |
26 | + | |
27 | + context "Fabricate(:problem_with_errs)" do | |
28 | + it 'should be valid' do | |
29 | + Fabricate.build(:problem_with_errs).should be_valid | |
30 | + end | |
31 | + it 'should have 3 errs' do | |
32 | + lambda do | |
33 | + Fabricate(:problem_with_errs) | |
34 | + end.should change(Err, :count).by(3) | |
35 | + end | |
36 | + end | |
26 | 37 | end |
27 | 38 | |
28 | 39 | context '#last_notice_at' do |
... | ... | @@ -49,7 +60,7 @@ describe Problem do |
49 | 60 | problem.first_notice_at.should == notice1.created_at |
50 | 61 | |
51 | 62 | notice2 = Fabricate(:notice, :err => err) |
52 | - problem.first_notice_at.should == notice1.created_at | |
63 | + problem.first_notice_at.to_time.should eq notice1.created_at | |
53 | 64 | end |
54 | 65 | end |
55 | 66 | |
... | ... | @@ -134,22 +145,6 @@ describe Problem do |
134 | 145 | end |
135 | 146 | end |
136 | 147 | |
137 | - | |
138 | - context ".merge!" do | |
139 | - it "collects the Errs from several problems into one and deletes the other problems" do | |
140 | - problem1 = Fabricate(:err).problem | |
141 | - problem2 = Fabricate(:err).problem | |
142 | - problem1.errs.length.should == 1 | |
143 | - problem2.errs.length.should == 1 | |
144 | - | |
145 | - lambda { | |
146 | - merged_problem = Problem.merge!(problem1, problem2) | |
147 | - merged_problem.reload.errs.length.should == 2 | |
148 | - }.should change(Problem, :count).by(-1) | |
149 | - end | |
150 | - end | |
151 | - | |
152 | - | |
153 | 148 | context "#unmerge!" do |
154 | 149 | it "creates a separate problem for each err" do |
155 | 150 | problem1 = Fabricate(:notice).problem |
... | ... | @@ -188,17 +183,17 @@ describe Problem do |
188 | 183 | |
189 | 184 | context "searching" do |
190 | 185 | it 'finds the correct record' do |
191 | - find = Fabricate(:problem, :resolved => false, :error_class => 'theErrorclass::other', | |
186 | + find = Fabricate(:problem, :resolved => false, :error_class => 'theErrorclass::other', | |
192 | 187 | :message => "other", :where => 'errorclass', :environment => 'development', :app_name => 'other') |
193 | - dont_find = Fabricate(:problem, :resolved => false, :error_class => "Batman", | |
188 | + dont_find = Fabricate(:problem, :resolved => false, :error_class => "Batman", | |
194 | 189 | :message => 'todo', :where => 'classerror', :environment => 'development', :app_name => 'other') |
195 | 190 | Problem.search("theErrorClass").unresolved.should include(find) |
196 | 191 | Problem.search("theErrorClass").unresolved.should_not include(dont_find) |
197 | 192 | end |
198 | 193 | end |
199 | 194 | end |
200 | - | |
201 | - | |
195 | + | |
196 | + | |
202 | 197 | context "notice counter cache" do |
203 | 198 | before do |
204 | 199 | @app = Fabricate(:app) |
... | ... | @@ -213,15 +208,15 @@ describe Problem do |
213 | 208 | it "adding a notice increases #notices_count by 1" do |
214 | 209 | lambda { |
215 | 210 | Fabricate(:notice, :err => @err, :message => 'ERR 1') |
216 | - }.should change(@problem, :notices_count).from(0).to(1) | |
211 | + }.should change(@problem.reload, :notices_count).from(0).to(1) | |
217 | 212 | end |
218 | 213 | |
219 | 214 | it "removing a notice decreases #notices_count by 1" do |
220 | 215 | notice1 = Fabricate(:notice, :err => @err, :message => 'ERR 1') |
221 | - lambda { | |
216 | + expect { | |
222 | 217 | @err.notices.first.destroy |
223 | 218 | @problem.reload |
224 | - }.should change(@problem, :notices_count).from(1).to(0) | |
219 | + }.to change(@problem, :notices_count).from(1).to(0) | |
225 | 220 | end |
226 | 221 | end |
227 | 222 | ... | ... |