]> git.ipfire.org Git - thirdparty/bugzilla.git/commitdiff
Bug 851267: Bugzilla times out when a user has several thousands of votes
authorFrédéric Buclin <LpSolit@gmail.com>
Fri, 27 Sep 2013 22:37:03 +0000 (00:37 +0200)
committerFrédéric Buclin <LpSolit@gmail.com>
Fri, 27 Sep 2013 22:37:03 +0000 (00:37 +0200)
r=dkl a=justdave

Bugzilla/CGI.pm
extensions/Voting/Extension.pm
extensions/Voting/template/en/default/pages/voting/user.html.tmpl

index 991bfc05dac22376a833d2dd1a818d4b97c9eaf7..05863bf02ac41fcf1af755eb5d1e17fbe1ac7571 100644 (file)
@@ -473,9 +473,9 @@ sub redirect_search_url {
 
     # GET requests that lacked a list_id are always redirected. POST requests
     # are only redirected if they're under the CGI_URI_LIMIT though.
-    my $uri_length = length($self->self_url());
-    if ($self->request_method() ne 'POST' or $uri_length < CGI_URI_LIMIT) {
-        print $self->redirect(-url => $self->self_url());
+    my $self_url = $self->self_url();
+    if ($self->request_method() ne 'POST' or length($self_url) < CGI_URI_LIMIT) {
+        print $self->redirect(-url => $self_url);
         exit;
     }
 }
@@ -529,7 +529,7 @@ sub url_is_attachment_base {
         $regex =~ s/\\\%bugid\\\%/\\d+/;
     }
     $regex = "^$regex";
-    return ($self->self_url =~ $regex) ? 1 : 0;
+    return ($self->url =~ $regex) ? 1 : 0;
 }
 
 sub set_dated_content_disp {
index 387d44ee031a80d7cd28b6a0681f6f7b15cf02af..aa93ec235b03930a9bfb7f823002f1c1f50a7fd7 100644 (file)
@@ -21,7 +21,7 @@ use Bugzilla::User;
 use Bugzilla::Util qw(detaint_natural);
 use Bugzilla::Token;
 
-use List::Util qw(min);
+use List::Util qw(min sum);
 
 use constant VERSION => BUGZILLA_VERSION;
 use constant DEFAULT_VOTES_PER_BUG => 1;
@@ -187,7 +187,7 @@ sub bug_end_of_update {
         # If some votes have been removed, RemoveVotes() returns
         # a list of messages to send to voters.
         @msgs = _remove_votes($bug->id, 0, 'votes_bug_moved');
-        _confirm_if_vote_confirmed($bug->id);
+        _confirm_if_vote_confirmed($bug);
 
         foreach my $msg (@msgs) {
             MessageToMTA($msg);
@@ -391,7 +391,7 @@ sub _page_user {
     # If a bug_id is given, and we're editing, we'll add it to the votes list.
     
     my $bug_id = $input->{bug_id};
-    my $bug = Bugzilla::Bug->check($bug_id) if $bug_id;
+    my $bug = Bugzilla::Bug->check({ id => $bug_id, cache => 1 }) if $bug_id;
     my $who_id = $input->{user_id} || $user->id;
 
     # Logged-out users must specify a user_id.
@@ -420,53 +420,38 @@ sub _page_user {
     foreach my $product (@{ $user->get_selectable_products }) {
         next unless ($product->{votesperuser} > 0);
 
-        my @bugs;
-        my @bug_ids;
-        my $total = 0;
-        my $onevoteonly = 0;
-
         my $vote_list =
-            $dbh->selectall_arrayref('SELECT votes.bug_id, votes.vote_count,
-                                             bugs.short_desc
-                                        FROM votes
-                                  INNER JOIN bugs
-                                          ON votes.bug_id = bugs.bug_id
-                                       WHERE votes.who = ?
-                                         AND bugs.product_id = ?
-                                    ORDER BY votes.bug_id',
-                                      undef, ($who->id, $product->id));
-
-        $user->visible_bugs([map { $_->[0] } @$vote_list]);
-        foreach (@$vote_list) {
-            my ($id, $count, $summary) = @$_;
-            $total += $count;
-
-            # Next if user can't see this bug. So, the totals will be correct
-            # and they can see there are votes 'missing', but not on what bug
-            # they are. This seems a reasonable compromise; the alternative is
-            # to lie in the totals.
-            next if !$user->can_see_bug($id);
-
-            push (@bugs, { id => $id,
-                           summary => $summary,
-                           count => $count });
-            push (@bug_ids, $id);
-            push (@all_bug_ids, $id);
-        }
+          $dbh->selectall_arrayref('SELECT votes.bug_id, votes.vote_count
+                                      FROM votes
+                                INNER JOIN bugs
+                                        ON votes.bug_id = bugs.bug_id
+                                     WHERE votes.who = ?
+                                       AND bugs.product_id = ?',
+                                     undef, ($who->id, $product->id));
+
+        my %votes = map { $_->[0] => $_->[1] } @$vote_list;
+        my @bug_ids = sort keys %votes;
+        # Exclude bugs that the user can no longer see.
+        @bug_ids = @{ $user->visible_bugs(\@bug_ids) };
+        next unless scalar @bug_ids;
+
+        push(@all_bug_ids, @bug_ids);
+        my @bugs = @{ Bugzilla::Bug->new_from_list(\@bug_ids) };
+        $_->{count} = $votes{$_->id} foreach @bugs;
+        # We include votes from bugs that the user can no longer see.
+        my $total = sum(values %votes) || 0;
 
+        my $onevoteonly = 0;
         $onevoteonly = 1 if (min($product->{votesperuser},
                                  $product->{maxvotesperbug}) == 1);
 
-        # Only add the product for display if there are any bugs in it.
-        if ($#bugs > -1) {
-            push (@products, { name => $product->name,
-                               bugs => \@bugs,
-                               bug_ids => \@bug_ids,
-                               onevoteonly => $onevoteonly,
-                               total => $total,
-                               maxvotes => $product->{votesperuser},
-                               maxperbug => $product->{maxvotesperbug} });
-        }
+        push(@products, { name => $product->name,
+                          bugs => \@bugs,
+                          bug_ids => \@bug_ids,
+                          onevoteonly => $onevoteonly,
+                          total => $total,
+                          maxvotes => $product->{votesperuser},
+                          maxperbug => $product->{maxvotesperbug} });
     }
 
     if ($canedit && $bug) {
@@ -500,6 +485,7 @@ sub _update_votes {
     # IDs and the field values are the number of votes.
 
     my @buglist = grep {/^\d+$/} keys %$input;
+    my (%bugs, %votes);
 
     # If no bugs are in the buglist, let's make sure the user gets notified
     # that their votes will get nuked if they continue.
@@ -515,20 +501,23 @@ sub _update_votes {
             exit;
         }
     }
+    else {
+        $user->visible_bugs(\@buglist);
+        my $bugs_obj = Bugzilla::Bug->new_from_list(\@buglist);
+        $bugs{$_->id} = $_ foreach @$bugs_obj;
+    }
 
-    # Call check() on each bug ID to make sure it is a positive
-    # integer representing an existing bug that the user is authorized 
-    # to access, and make sure the number of votes submitted is also
-    # a non-negative integer (a series of digits not preceded by a
-    # minus sign).
-    my (%votes, @bugs);
+    # Call check_is_visible() on each bug to make sure it is an existing bug
+    # that the user is authorized to access, and make sure the number of votes
+    # submitted is also an integer.
     foreach my $id (@buglist) {
-      my $bug = Bugzilla::Bug->check($id);
-      push(@bugs, $bug);
-      $id = $bug->id;
-      $votes{$id} = $input->{$id};
-      detaint_natural($votes{$id})
-        || ThrowUserError("voting_must_be_nonnegative");
+        my $bug = $bugs{$id}
+          or ThrowUserError('bug_id_does_not_exist', { bug_id => $id });
+        $bug->check_is_visible;
+        $id = $bug->id;
+        $votes{$id} = $input->{$id};
+        detaint_natural($votes{$id})
+          || ThrowUserError("voting_must_be_nonnegative");
     }
 
     my $token = $cgi->param('token');
@@ -541,10 +530,10 @@ sub _update_votes {
 
     # If the user is voting for bugs, make sure they aren't overstuffing
     # the ballot box.
-    if (scalar @bugs) {
+    if (scalar @buglist) {
         my (%prodcount, %products);
-        foreach my $bug (@bugs) {
-            my $bug_id = $bug->id;
+        foreach my $bug_id (keys %bugs) {
+            my $bug = $bugs{$bug_id};
             my $prod = $bug->product;
             $products{$prod} ||= $bug->product_obj;
             $prodcount{$prod} ||= 0;
@@ -568,56 +557,65 @@ sub _update_votes {
         }
     }
 
-    # Update the user's votes in the database.  If the user did not submit 
-    # any votes, they may be using a form with checkboxes to remove all their
-    # votes (checkboxes are not submitted along with other form data when
-    # they are not checked, and Bugzilla uses them to represent single votes
-    # for products that only allow one vote per bug).  In that case, we still
-    # need to clear the user's votes from the database.
-    my %affected;
+    # Update the user's votes in the database.
     $dbh->bz_start_transaction();
 
-    # Take note of, and delete the user's old votes from the database.
-    my $bug_list = $dbh->selectcol_arrayref('SELECT bug_id FROM votes
+    my $old_list = $dbh->selectall_arrayref('SELECT bug_id, vote_count FROM votes
                                              WHERE who = ?', undef, $who);
 
-    foreach my $id (@$bug_list) {
-        $affected{$id} = 1;
-    }
-    $dbh->do('DELETE FROM votes WHERE who = ?', undef, $who);
+    my %old_votes = map { $_->[0] => $_->[1] } @$old_list;
 
     my $sth_insertVotes = $dbh->prepare('INSERT INTO votes (who, bug_id, vote_count)
                                          VALUES (?, ?, ?)');
+    my $sth_updateVotes = $dbh->prepare('UPDATE votes SET vote_count = ?
+                                         WHERE bug_id = ? AND who = ?');
 
-    # Insert the new values in their place
-    foreach my $id (@buglist) {
-        if ($votes{$id} > 0) {
+    my %affected = map { $_ => 1 } (@buglist, keys %old_votes);
+    my @deleted_votes;
+
+    foreach my $id (keys %affected) {
+        if (!$votes{$id}) {
+            push(@deleted_votes, $id);
+            next;
+        }
+        if ($votes{$id} == ($old_votes{$id} || 0)) {
+            delete $affected{$id};
+            next;
+        }
+        # We use 'defined' in case 0 was accidentally stored in the DB.
+        if (defined $old_votes{$id}) {
+            $sth_updateVotes->execute($votes{$id}, $id, $who);
+        }
+        else {
             $sth_insertVotes->execute($who, $id, $votes{$id});
         }
-        $affected{$id} = 1;
+    }
+
+    if (@deleted_votes) {
+        $dbh->do('DELETE FROM votes WHERE who = ? AND ' .
+                 $dbh->sql_in('bug_id', \@deleted_votes), undef, $who);
     }
 
     # Update the cached values in the bugs table
-    print $cgi->header();
     my @updated_bugs = ();
 
     my $sth_getVotes = $dbh->prepare("SELECT SUM(vote_count) FROM votes
                                       WHERE bug_id = ?");
 
-    my $sth_updateVotes = $dbh->prepare("UPDATE bugs SET votes = ?
-                                         WHERE bug_id = ?");
+    $sth_updateVotes = $dbh->prepare('UPDATE bugs SET votes = ? WHERE bug_id = ?');
 
     foreach my $id (keys %affected) {
         $sth_getVotes->execute($id);
         my $v = $sth_getVotes->fetchrow_array || 0;
         $sth_updateVotes->execute($v, $id);
 
-        my $confirmed = _confirm_if_vote_confirmed($id);
+        my $confirmed = _confirm_if_vote_confirmed($bugs{$id} || $id);
         push (@updated_bugs, $id) if $confirmed;
     }
 
     $dbh->bz_commit_transaction();
 
+    print $cgi->header() if scalar @updated_bugs;
     $vars->{'type'} = "votes";
     $vars->{'title_tag'} = 'change_votes';
     foreach my $bug_id (@updated_bugs) {
@@ -828,7 +826,7 @@ sub _remove_votes {
 # confirm a bug has been reduced, check if the bug is now confirmed.
 sub _confirm_if_vote_confirmed {
     my $id = shift;
-    my $bug = new Bugzilla::Bug($id);
+    my $bug = ref $id ? $id : new Bugzilla::Bug({ id => $id, cache => 1 });
 
     my $ret = 0;
     if (!$bug->everconfirmed
index b1cf53a852db0900431acbe2b848691d194cd4c5..b08b4c878b0a0c52e93edbd747a3d096b2381073 100644 (file)
@@ -85,8 +85,7 @@
         </tr>
 
         [% FOREACH bug = product.bugs %]
-          <tr [% IF bug.id == this_bug.id && canedit %] 
-            class="bz_bug_being_voted_on" [% END %]>
+          <tr [% IF bug.id == this_bug.id && canedit %] class="bz_bug_being_voted_on"[% END %]>
             <td>
               [% IF bug.id == this_bug.id && canedit %]
                 [% IF product.onevoteonly %]
                 [% END %]
               [%- END %]
             </td>
-            <td align="right"><a name="vote_[% bug.id FILTER html %]">
+            <td align="right"><a name="vote_[% bug.id FILTER none %]">
               [% IF canedit %]
                 [% IF product.onevoteonly %]
-                  <input type="checkbox" name="[% bug.id FILTER html %]" value="1"
-                    [% " checked" IF bug.count %] id="bug_[% bug.id FILTER html %]">
+                  <input type="checkbox" name="[% bug.id FILTER none %]" value="1"
+                    [% " checked" IF bug.count %] id="bug_[% bug.id FILTER none %]">
                 [% ELSE %]
-                  <input name="[% bug.id FILTER html %]" value="[% bug.count FILTER html %]"
-                         size="2" id="bug_[% bug.id FILTER html %]">
+                  <input name="[% bug.id FILTER none %]" value="[% bug.count FILTER html %]"
+                         size="2" id="bug_[% bug.id FILTER none %]">
                 [% END %]
               [% ELSE %]
                 [% bug.count FILTER html %]
               [% END %]
             </a></td>
             <td align="center">
-              [% bug.id FILTER bug_link(bug.id) FILTER none %]
+              [% PROCESS bug/link.html.tmpl bug = bug, link_text = bug.id %]
             </td>
             <td>
-              [% bug.summary FILTER html %]
-              (<a href="page.cgi?id=voting/bug.html&amp;bug_id=[% bug.id FILTER uri %]">Show Votes</a>)
+              [% bug.short_desc FILTER html %]
+              (<a href="page.cgi?id=voting/bug.html&amp;bug_id=[% bug.id FILTER none %]">Show Votes</a>)
             </td>
           </tr>
         [% END %]