Public bug reported: Exponential ajax calls when refreshing multiple rows in horizon tables. When you launch multiple instances, each instance status refresh makes an ajax call to any status_unknown instance, when the number is big (>50) it kill the client CPU.
horizon/static/horizon/js/horizon.tables.js Original: horizon.datatables = { update: function () { var $rows_to_update = $('tr.status_unknown.ajax-update'); if ($rows_to_update.length) { var interval = $rows_to_update.attr('data-update-interval'), $table = $rows_to_update.closest('table'), decay_constant = $table.attr('decay_constant'); // Do not update this row if the action column is expanded if ($rows_to_update.find('.actions_column .btn-group.open').length) { // Wait and try to update again in next interval instead setTimeout(horizon.datatables.update, interval); // Remove interval decay, since this will not hit server $table.removeAttr('decay_constant'); return; } // Trigger the update handlers. $rows_to_update.each(function(index, row) { var $row = $(this), $table = $row.closest('table.datatable'); horizon.ajax.queue({ url: $row.attr('data-update-url'), error: function (jqXHR, textStatus, errorThrown) { switch (jqXHR.status) { // A 404 indicates the object is gone, and should be removed from the table case 404: // Update the footer count and reset to default empty row if needed var $footer, row_count, footer_text, colspan, template, params, $empty_row; // existing count minus one for the row we're removing row_count = horizon.datatables.update_footer_count($table, -1); if(row_count === 0) { colspan = $table.find('th[colspan]').attr('colspan'); template = horizon.templates.compiled_templates["#empty_row_template"]; params = { "colspan": colspan, no_items_label: gettext("No items to display.") }; empty_row = template.render(params); $row.replaceWith(empty_row); } else { $row.remove(); } // Reset tablesorter's data cache. $table.trigger("update"); // Enable launch action if quota is not exceeded horizon.datatables.update_actions(); break; default: horizon.utils.log(gettext("An error occurred while updating.")); $row.removeClass("ajax-update"); $row.find("i.ajax-updating").remove(); break; } }, success: function (data, textStatus, jqXHR) { var $new_row = $(data); if ($new_row.hasClass('status_unknown')) { var spinner_elm = $new_row.find("td.status_unknown:last"); var imagePath = $new_row.find('.btn-action-required').length > 0 ? "dashboard/img/action_required.png": "dashboard/img/loading.gif"; imagePath = STATIC_URL + imagePath; spinner_elm.prepend( $("<div>") .addClass("loading_gif") .append($("<img>").attr("src", imagePath))); } // Only replace row if the html content has changed if($new_row.html() !== $row.html()) { if($row.find('.table-row-multi-select:checkbox').is(':checked')) { // Preserve the checkbox if it's already clicked $new_row.find('.table-row-multi-select:checkbox').prop('checked', true); } $row.replaceWith($new_row); // Reset tablesorter's data cache. $table.trigger("update"); // Reset decay constant. $table.removeAttr('decay_constant'); // Check that quicksearch is enabled for this table // Reset quicksearch's data cache. if ($table.attr('id') in horizon.datatables.qs) { horizon.datatables.qs[$table.attr('id')].cache(); } } }, complete: function (jqXHR, textStatus) { // Revalidate the button check for the updated table horizon.datatables.validate_button(); // Set interval decay to this table, and increase if it already exist if(decay_constant === undefined) { decay_constant = 1; } else { decay_constant++; } $table.attr('decay_constant', decay_constant); // Poll until there are no rows in an "unknown" state on the page. next_poll = interval * decay_constant; // Limit the interval to 30 secs if(next_poll > 30 * 1000) { next_poll = 30 * 1000; } setTimeout(horizon.datatables.update, next_poll); } }); }); } }, ___________________________________________________________________________ Our fix: horizon.datatables = { update: function (row_id) { var $rows_to_update = undefined; //Filter by row_id to prevent exponential ajax calls if(row_id!=undefined) { $rows_to_update = $('tr#' + row_id + '.status_unknown.ajax-update'); } else { $rows_to_update = $('tr.status_unknown.ajax-update'); } if ($rows_to_update.length) { var interval = $rows_to_update.attr('data-update-interval'), $table = $rows_to_update.closest('table'), decay_constant = $table.attr('decay_constant'); // Do not update this row if the action column is expanded if ($rows_to_update.find('.actions_column .btn-group.open').length) { // Wait and try to update again in next interval instead setTimeout(horizon.datatables.update, interval); return; } // Set interval decay to this table, and increase if it already exist if(decay_constant === undefined) { decay_constant = 1; } else { decay_constant++; } $table.attr('decay_constant', decay_constant); // Poll until there are no rows in an "unknown" state on the page. next_poll = interval * decay_constant; // Limit the interval to 30 secs if(next_poll > 30 * 1000) { next_poll = 30 * 1000; } // Trigger the update handlers. $rows_to_update.each(function(index, row) { var $row = $(this), $table = $row.closest('table.datatable'); horizon.ajax.queue({ url: $row.attr('data-update-url'), error: function (jqXHR, textStatus, errorThrown) { switch (jqXHR.status) { // A 404 indicates the object is gone, and should be removed from the table case 404: // Update the footer count and reset to default empty row if needed var $footer, row_count, footer_text, colspan, template, params, $empty_row; // existing count minus one for the row we're removing row_count = horizon.datatables.update_footer_count($table, -1); if(row_count === 0) { colspan = $table.find('th[colspan]').attr('colspan'); template = horizon.templates.compiled_templates["#empty_row_template"]; params = { "colspan": colspan, no_items_label: gettext("No items to display.") }; empty_row = template.render(params); $row.replaceWith(empty_row); } else { $row.remove(); } // Reset tablesorter's data cache. $table.trigger("update"); // Enable launch action if quota is not exceeded horizon.datatables.update_actions(); break; default: horizon.utils.log(gettext("An error occurred while updating.")); $row.removeClass("ajax-update"); $row.find("i.ajax-updating").remove(); break; } }, success: function (data, textStatus, jqXHR) { var $new_row = $(data); if ($new_row.hasClass('status_unknown')) { var spinner_elm = $new_row.find("td.status_unknown:last"); var imagePath = $new_row.find('.btn-action-required').length > 0 ? "dashboard/img/action_required.png": "dashboard/img/loading.gif"; imagePath = STATIC_URL + imagePath; spinner_elm.prepend( $("<div>") .addClass("loading_gif") .append($("<img>").attr("src", imagePath))); } // Only replace row if the html content has changed if($new_row.html() !== $row.html()) { if($row.find('.table-row-multi-select:checkbox').is(':checked')) { // Preserve the checkbox if it's already clicked $new_row.find('.table-row-multi-select:checkbox').prop('checked', true); } $row.replaceWith($new_row); // Reset tablesorter's data cache. $table.trigger("update"); // Check that quicksearch is enabled for this table // Reset quicksearch's data cache. if ($table.attr('id') in horizon.datatables.qs) { horizon.datatables.qs[$table.attr('id')].cache(); } } }, complete: function (jqXHR, textStatus) { // Revalidate the button check for the updated table horizon.datatables.validate_button(); setTimeout(horizon.datatables.update($row.attr('id')), next_poll); } }); }); } }, ** Affects: horizon Importance: Undecided Status: New -- You received this bug notification because you are a member of Yahoo! Engineering Team, which is subscribed to OpenStack Dashboard (Horizon). https://bugs.launchpad.net/bugs/1447781 Title: Exponential ajax calls when refreshing multiple rows in horizon tables. Status in OpenStack Dashboard (Horizon): New Bug description: Exponential ajax calls when refreshing multiple rows in horizon tables. When you launch multiple instances, each instance status refresh makes an ajax call to any status_unknown instance, when the number is big (>50) it kill the client CPU. horizon/static/horizon/js/horizon.tables.js Original: horizon.datatables = { update: function () { var $rows_to_update = $('tr.status_unknown.ajax-update'); if ($rows_to_update.length) { var interval = $rows_to_update.attr('data-update-interval'), $table = $rows_to_update.closest('table'), decay_constant = $table.attr('decay_constant'); // Do not update this row if the action column is expanded if ($rows_to_update.find('.actions_column .btn-group.open').length) { // Wait and try to update again in next interval instead setTimeout(horizon.datatables.update, interval); // Remove interval decay, since this will not hit server $table.removeAttr('decay_constant'); return; } // Trigger the update handlers. $rows_to_update.each(function(index, row) { var $row = $(this), $table = $row.closest('table.datatable'); horizon.ajax.queue({ url: $row.attr('data-update-url'), error: function (jqXHR, textStatus, errorThrown) { switch (jqXHR.status) { // A 404 indicates the object is gone, and should be removed from the table case 404: // Update the footer count and reset to default empty row if needed var $footer, row_count, footer_text, colspan, template, params, $empty_row; // existing count minus one for the row we're removing row_count = horizon.datatables.update_footer_count($table, -1); if(row_count === 0) { colspan = $table.find('th[colspan]').attr('colspan'); template = horizon.templates.compiled_templates["#empty_row_template"]; params = { "colspan": colspan, no_items_label: gettext("No items to display.") }; empty_row = template.render(params); $row.replaceWith(empty_row); } else { $row.remove(); } // Reset tablesorter's data cache. $table.trigger("update"); // Enable launch action if quota is not exceeded horizon.datatables.update_actions(); break; default: horizon.utils.log(gettext("An error occurred while updating.")); $row.removeClass("ajax-update"); $row.find("i.ajax-updating").remove(); break; } }, success: function (data, textStatus, jqXHR) { var $new_row = $(data); if ($new_row.hasClass('status_unknown')) { var spinner_elm = $new_row.find("td.status_unknown:last"); var imagePath = $new_row.find('.btn-action-required').length > 0 ? "dashboard/img/action_required.png": "dashboard/img/loading.gif"; imagePath = STATIC_URL + imagePath; spinner_elm.prepend( $("<div>") .addClass("loading_gif") .append($("<img>").attr("src", imagePath))); } // Only replace row if the html content has changed if($new_row.html() !== $row.html()) { if($row.find('.table-row-multi-select:checkbox').is(':checked')) { // Preserve the checkbox if it's already clicked $new_row.find('.table-row-multi-select:checkbox').prop('checked', true); } $row.replaceWith($new_row); // Reset tablesorter's data cache. $table.trigger("update"); // Reset decay constant. $table.removeAttr('decay_constant'); // Check that quicksearch is enabled for this table // Reset quicksearch's data cache. if ($table.attr('id') in horizon.datatables.qs) { horizon.datatables.qs[$table.attr('id')].cache(); } } }, complete: function (jqXHR, textStatus) { // Revalidate the button check for the updated table horizon.datatables.validate_button(); // Set interval decay to this table, and increase if it already exist if(decay_constant === undefined) { decay_constant = 1; } else { decay_constant++; } $table.attr('decay_constant', decay_constant); // Poll until there are no rows in an "unknown" state on the page. next_poll = interval * decay_constant; // Limit the interval to 30 secs if(next_poll > 30 * 1000) { next_poll = 30 * 1000; } setTimeout(horizon.datatables.update, next_poll); } }); }); } }, ___________________________________________________________________________ Our fix: horizon.datatables = { update: function (row_id) { var $rows_to_update = undefined; //Filter by row_id to prevent exponential ajax calls if(row_id!=undefined) { $rows_to_update = $('tr#' + row_id + '.status_unknown.ajax-update'); } else { $rows_to_update = $('tr.status_unknown.ajax-update'); } if ($rows_to_update.length) { var interval = $rows_to_update.attr('data-update-interval'), $table = $rows_to_update.closest('table'), decay_constant = $table.attr('decay_constant'); // Do not update this row if the action column is expanded if ($rows_to_update.find('.actions_column .btn-group.open').length) { // Wait and try to update again in next interval instead setTimeout(horizon.datatables.update, interval); return; } // Set interval decay to this table, and increase if it already exist if(decay_constant === undefined) { decay_constant = 1; } else { decay_constant++; } $table.attr('decay_constant', decay_constant); // Poll until there are no rows in an "unknown" state on the page. next_poll = interval * decay_constant; // Limit the interval to 30 secs if(next_poll > 30 * 1000) { next_poll = 30 * 1000; } // Trigger the update handlers. $rows_to_update.each(function(index, row) { var $row = $(this), $table = $row.closest('table.datatable'); horizon.ajax.queue({ url: $row.attr('data-update-url'), error: function (jqXHR, textStatus, errorThrown) { switch (jqXHR.status) { // A 404 indicates the object is gone, and should be removed from the table case 404: // Update the footer count and reset to default empty row if needed var $footer, row_count, footer_text, colspan, template, params, $empty_row; // existing count minus one for the row we're removing row_count = horizon.datatables.update_footer_count($table, -1); if(row_count === 0) { colspan = $table.find('th[colspan]').attr('colspan'); template = horizon.templates.compiled_templates["#empty_row_template"]; params = { "colspan": colspan, no_items_label: gettext("No items to display.") }; empty_row = template.render(params); $row.replaceWith(empty_row); } else { $row.remove(); } // Reset tablesorter's data cache. $table.trigger("update"); // Enable launch action if quota is not exceeded horizon.datatables.update_actions(); break; default: horizon.utils.log(gettext("An error occurred while updating.")); $row.removeClass("ajax-update"); $row.find("i.ajax-updating").remove(); break; } }, success: function (data, textStatus, jqXHR) { var $new_row = $(data); if ($new_row.hasClass('status_unknown')) { var spinner_elm = $new_row.find("td.status_unknown:last"); var imagePath = $new_row.find('.btn-action-required').length > 0 ? "dashboard/img/action_required.png": "dashboard/img/loading.gif"; imagePath = STATIC_URL + imagePath; spinner_elm.prepend( $("<div>") .addClass("loading_gif") .append($("<img>").attr("src", imagePath))); } // Only replace row if the html content has changed if($new_row.html() !== $row.html()) { if($row.find('.table-row-multi-select:checkbox').is(':checked')) { // Preserve the checkbox if it's already clicked $new_row.find('.table-row-multi-select:checkbox').prop('checked', true); } $row.replaceWith($new_row); // Reset tablesorter's data cache. $table.trigger("update"); // Check that quicksearch is enabled for this table // Reset quicksearch's data cache. if ($table.attr('id') in horizon.datatables.qs) { horizon.datatables.qs[$table.attr('id')].cache(); } } }, complete: function (jqXHR, textStatus) { // Revalidate the button check for the updated table horizon.datatables.validate_button(); setTimeout(horizon.datatables.update($row.attr('id')), next_poll); } }); }); } }, To manage notifications about this bug go to: https://bugs.launchpad.net/horizon/+bug/1447781/+subscriptions -- Mailing list: https://launchpad.net/~yahoo-eng-team Post to : yahoo-eng-team@lists.launchpad.net Unsubscribe : https://launchpad.net/~yahoo-eng-team More help : https://help.launchpad.net/ListHelp