Browse Source

HUE-1949 [impala] Friendler query expiration message

Update status of queries that are closed.
Fix error listing.
Abraham Elmahrek 11 years ago
parent
commit
a0f1045

+ 32 - 9
apps/beeswax/src/beeswax/templates/execute.mako

@@ -317,7 +317,7 @@ ${layout.menubar(section='query')}
   </div>
 
   <div class="card card-small scrollable resultsContainer">
-    <div data-bind="visible: !$root.design.results.empty()">
+    <div data-bind="visible: $root.hasResults()">
       <a id="expandResults" href="javascript:void(0)" title="${_('See results in full screen')}" rel="tooltip"
         class="view-query-results hide pull-right"><h4 style="margin-right: 20px"><i class="fa fa-expand"></i></h4></a>
 
@@ -331,7 +331,7 @@ ${layout.menubar(section='query')}
 
       <a id="download-excel" data-bind="attr: {'href': '/${ app_name }/download/' + $root.design.history.id() + '/xls'}" href="javascript:void(0)" title="${_('Download the results in XLS format')}" rel="tooltip"
         class="view-query-results download hide pull-right"><h4 style="margin-right: 20px"><i class="hfo hfo-file-xls"></i></h4></a>
-   </div>
+    </div>
 
     <div class="card-body">
       <ul class="nav nav-tabs">
@@ -340,9 +340,9 @@ ${layout.menubar(section='query')}
         <li><a href="#log" data-toggle="tab">${_('Log')}</a></li>
         <!-- /ko -->
         <!-- ko if: !design.explain() && !design.isRunning() -->
-        <li data-bind="css: {'hide': $root.design.results.empty()}"><a href="#columns" data-toggle="tab">${_('Columns')}</a></li>
+        <li data-bind="css: {'hide': !$root.hasResults()}"><a href="#columns" data-toggle="tab">${_('Columns')}</a></li>
         <li><a href="#results" data-toggle="tab">${_('Results')}</a></li>
-        <li data-bind="css: {'hide': $root.design.results.empty()}"><a href="#chart" data-toggle="tab">${_('Chart')}</a></li>
+        <li data-bind="css: {'hide': !$root.hasResults()}"><a href="#chart" data-toggle="tab">${_('Chart')}</a></li>
         <!-- /ko -->
         <!-- ko if: design.explain() && !design.isRunning() -->
         <li><a href="#explanation" data-toggle="tab">${_('Explanation')}</a></li>
@@ -365,7 +365,7 @@ ${layout.menubar(section='query')}
           <pre data-bind="text: $root.design.watch.logs().join('\n')"></pre>
         </div>
 
-        <div class="tab-pane" id="columns" data-bind="css: {'hide': $root.design.results.empty()}">
+        <div class="tab-pane" id="columns" data-bind="css: {'hide': !$root.hasResults()}">
           <div data-bind="visible: $root.design.results.columns().length > 10">
             <input id="columnFilter" class="input-xlarge" type="text" placeholder="${_('Filter for column name or type...')}" />
           </div>
@@ -388,7 +388,7 @@ ${layout.menubar(section='query')}
             </div>
           </div>
 
-          <div data-bind="css: {'hide': $root.design.results.empty()}">
+          <div data-bind="css: {'hide': !$root.hasResults()}">
             <table class="table table-striped table-condensed resultTable" cellpadding="0" cellspacing="0" data-tablescroller-enforce-height="true">
               <thead>
               <tr data-bind="foreach: $root.design.results.columns">
@@ -398,7 +398,7 @@ ${layout.menubar(section='query')}
             </table>
           </div>
 
-          <div data-bind="css: {'hide': !$root.design.results.empty()}" id="resultEmpty">
+          <div data-bind="css: {'hide': !$root.design.results.empty() || $root.design.results.expired()}" id="resultEmpty">
             <div class="card card-small scrollable">
               <div class="row-fluid">
                 <div class="span10 offset1 center empty-wrapper">
@@ -409,6 +409,18 @@ ${layout.menubar(section='query')}
               </div>
             </div>
           </div>
+
+          <div data-bind="css: {'hide': !$root.design.results.expired()}" id="resultExpired">
+            <div class="card card-small scrollable">
+              <div class="row-fluid">
+                <div class="span10 offset1 center empty-wrapper">
+                  <i class="fa fa-frown-o"></i>
+                  <h1>${_('The results have expired, rerun the query if needed.')}</h1>
+                  <br/>
+                </div>
+              </div>
+            </div>
+          </div>
         </div>
 
          <div class="tab-pane" id="chart">
@@ -1182,6 +1194,8 @@ function reinitializeTable(max) {
       $("#results .dataTables_wrapper").jHueScrollUp();
     } else if ($('#resultEmpty').height() > 0) {
       container.height($('#resultEmpty').height());
+    } else if ($('#resultExpired').height() > 0) {
+      container.height($('#resultExpired').height());
     }
 
     if ($("#results .dataTables_wrapper").data('original-height') == 0 && --_max != 0) {
@@ -2288,8 +2302,10 @@ function queryEvents() {
     routie('query/explanation');
   });
   $(document).on('watched.query', function (e, data) {
-    if (data.status && data.status && data.status != 0 && data.status != 2) {
-      viewModel.design.watch.errors.push(data.error || data.message);
+    if (data.status != 2) {
+      if (data.status && data.status && data.status != 0) {
+        viewModel.design.watch.errors.push(data.error || data.message);
+      }
     }
     routie('query/logs');
   });
@@ -2411,6 +2427,13 @@ ko.applyBindings(viewModel);
     viewModel.watchQueryLoop();
     cacheQueryTextEvents();
   });
+% elif action == 'editor-expired-results':
+  $(document).ready(queryEvents);
+  $(document).one('fetched.query', function(e) {
+    viewModel.design.results.expired(true);
+    $(document).trigger('fetched.results', [ [] ]);
+    cacheQueryTextEvents();
+  });
 % else:
   $(document).ready(queryEvents);
 % endif

+ 19 - 0
apps/beeswax/src/beeswax/tests.py

@@ -174,6 +174,25 @@ for x in sys.stdin:
       [ x.profile for x in self.cluster.jt.all_jobs().jobs
         if x.profile.name == "test_query_with_setting" ][0].user)
 
+  def test_lazy_query_status_update(self):
+    QUERY = """
+      SELECT MIN(foo), MAX(foo), SUM(foo) FROM test;
+    """
+    wait_for_query_to_finish(self.client, _make_query(self.client, QUERY, local=False), max=180.0)
+    self._verify_query_state(beeswax.models.QueryHistory.STATE.available)
+
+    # Make sure expired query states are lazily updated.
+    resp = self.client.get('/beeswax/query_history')
+    history = resp.context['page'].object_list[0]
+    self.db.close_operation(history.get_full_object().get_handle())
+    resp = self.client.get("/beeswax/execute/query/%s" % history.id)
+    assert_true(resp.status_code, 302)
+
+    resp = self.client.get('/beeswax/query_history')
+    history = resp.context['page'].object_list[0]
+    assert_equal(beeswax.models.QueryHistory.STATE[history.last_state], beeswax.models.QueryHistory.STATE.expired)
+
+
   def test_basic_flow(self):
     # Minimal server operation
     assert_equal(['default', 'other_db'], self.db.get_databases())

+ 19 - 13
apps/beeswax/src/beeswax/views.py

@@ -18,6 +18,7 @@
 import json
 import logging
 import re
+import sys
 import time
 
 from django import forms
@@ -47,7 +48,7 @@ import beeswax.management.commands.beeswax_install_examples
 from beeswax import common, data_export, models
 from beeswax.models import SavedQuery, QueryHistory
 from beeswax.server import dbms
-from beeswax.server.dbms import expand_exception, get_query_server_config
+from beeswax.server.dbms import expand_exception, get_query_server_config, QueryServerException
 
 
 LOG = logging.getLogger(__name__)
@@ -368,13 +369,23 @@ def execute_query(request, design_id=None, query_history_id=None):
     query_history = authorized_get_query_history(request, query_history_id, must_exist=True)
     design = query_history.design
 
-    if 'on_success_url' in request.GET:
-      if request.GET.get('on_success_url'):
-        action = 'watch-redirect'
+    try:
+      handle, state = _get_query_handle_and_state(query_history)
+
+      if 'on_success_url' in request.GET:
+        if request.GET.get('on_success_url'):
+          action = 'watch-redirect'
+        else:
+          action = 'watch-results'
       else:
-        action = 'watch-results'
-    else:
-      action = 'editor-results'
+        action = 'editor-results'
+    except QueryServerException, e:
+      if 'Invalid query handle' in e.message or 'Invalid OperationHandle' in e.message:
+        query_history.save_state(QueryHistory.STATE.expired)
+        LOG.warn("Invalid query handle", exc_info=sys.exc_info())
+        action = 'editor-expired-results'
+      else:
+        raise e
   else:
     # Check perms.
     authorized_get_design(request, design_id)
@@ -824,12 +835,7 @@ def _get_query_handle_and_state(query_history):
   if handle is None:
     raise PopupException(_("Failed to retrieve query state from the Query Server."))
 
-  query_server = query_history.get_query_server_config()
-
-  if query_server['server_name'] == 'impala' and handle.has_result_set:
-    state = QueryHistory.STATE.available
-  else:
-    state = dbms.get(query_history.owner, query_history.get_query_server_config()).get_state(handle)
+  state = dbms.get(query_history.owner, query_history.get_query_server_config()).get_state(handle)
 
   if state is None:
     raise PopupException(_("Failed to contact Server to check query status."))

+ 14 - 3
apps/beeswax/static/js/beeswax.vm.js

@@ -50,6 +50,7 @@ function BeeswaxViewModel(server) {
       'rows': [],
       'columns': [],
       'empty': true,
+      'expired': false,
       'explanation': null,
       'url': null,
       'errors': [],
@@ -69,6 +70,8 @@ function BeeswaxViewModel(server) {
     'isFinished': true
   };
 
+  self.design = ko.mapping.fromJS(DESIGN_DEFAULTS);
+
   self.server = ko.observable(server);
   self.databases = ko.observableArray();
   self.selectedDatabase = ko.observable(0);
@@ -76,7 +79,6 @@ function BeeswaxViewModel(server) {
   // Use a view model attribute so that we don't have to override KO.
   // This allows Hue to disable the execute button until the query placeholder dies.
   self.queryEditorBlank = ko.observable(false);
-  self.design = ko.mapping.fromJS(DESIGN_DEFAULTS);
 
   self.canExecute = ko.computed(function() {
     return !self.design.isRunning() && self.design.isFinished();
@@ -128,6 +130,10 @@ function BeeswaxViewModel(server) {
     return ! hasBlank;
   });
 
+  self.hasResults = ko.computed(function() {
+    return !self.design.results.empty() && !self.design.results.expired();
+  });
+
   self.resetQuery = function() {
     ko.mapping.fromJS(QUERY_DEFAULTS, self.design);
   };
@@ -183,8 +189,8 @@ function BeeswaxViewModel(server) {
 
   self.updateHistory = function(history) {
     self.design.history.id(history.id);
-    viewModel.design.results.url('/' + viewModel.server() + '/results/' + history.id + '/0?format=json');
-    viewModel.design.watch.url('/' + viewModel.server() + '/api/watch/json/' + history.id);
+    self.design.results.url('/' + self.server() + '/results/' + history.id + '/0?format=json');
+    self.design.watch.url('/' + self.server() + '/api/watch/json/' + history.id);
     if (history.design) {
       self.updateDesign(history.design);
     }
@@ -430,6 +436,8 @@ function BeeswaxViewModel(server) {
     self.design.explain(true);
     self.design.isRunning(true);
     self.design.isFinished(true);
+    self.design.results.expired(false);
+    self.design.results.empty(false);
     self.resetErrors();
 
     var data = {
@@ -471,6 +479,8 @@ function BeeswaxViewModel(server) {
     self.design.explain(false);
     self.design.isRunning(true);
     self.design.isFinished(true);
+    self.design.results.expired(false);
+    self.design.results.empty(false);
     self.resetErrors();
 
     var data = {
@@ -631,6 +641,7 @@ function BeeswaxViewModel(server) {
   self.fetchResults = function() {
     $(document).trigger('fetch.results');
     self.design.errors.removeAll();
+    self.design.results.errors.removeAll();
     var request = {
       url: self.design.results.url(),
       dataType: 'json',