Show a loading spinner while the startup tasks are running.

master
Pacman Ghost 3 years ago
parent ff87e2ba00
commit af08d936ff
  1. 35
      asl_rulebook2/webapp/search.py
  2. 68
      asl_rulebook2/webapp/startup.py
  3. 22
      asl_rulebook2/webapp/static/MainApp.js
  4. 4
      asl_rulebook2/webapp/static/NavPane.js
  5. 5
      asl_rulebook2/webapp/static/css/MainApp.css
  6. BIN
      asl_rulebook2/webapp/static/images/loading.gif
  7. 1
      asl_rulebook2/webapp/templates/index.html
  8. 2
      asl_rulebook2/webapp/tests/utils.py

@ -1,6 +1,7 @@
""" Manage the search engine. """
import os
import threading
import sqlite3
import json
import re
@ -17,12 +18,12 @@ import lxml.html
from asl_rulebook2.utils import plural
from asl_rulebook2.webapp import app
import asl_rulebook2.webapp.startup as webapp_startup
from asl_rulebook2.webapp.content import tag_ruleids
from asl_rulebook2.webapp.utils import make_config_path, make_data_path, split_strip
_sqlite_path = None
_fts_index = None
_fixup_content_lock = threading.Lock()
_logger = logging.getLogger( "search" )
@ -76,11 +77,11 @@ def search() :
_logger.info( "- %s: %s", key, val )
# run the search
# NOTE: We can't use the search index nor in-memory data structures if the "fix content" thread
# is still running (and possible updating them). However, the tasks running in that thread
# relinquish the lock regularly, to give the user a chance to jump in and grab it here, if they
# want to do a search while that thread is still running.
with webapp_startup.fixup_content_lock:
# NOTE: We can't use the search index nor in-memory data structures if the startup tasks thread
# is still running (and possible updating them, as it fixes up content). However, the tasks running
# in that thread relinquish the lock regularly, to give the user a chance to jump in and grab it here,
# if they want to do a search while that thread is still running.
with _fixup_content_lock:
try:
return _do_search( args )
except Exception as exc: #pylint: disable=broad-except
@ -507,8 +508,8 @@ def _init_content_sets( conn, curs, content_sets, logger ):
_tag_ruleids_in_field( index_entry, "subtitle", cset_id )
_tag_ruleids_in_field( index_entry, "content", cset_id )
return index_entry
from asl_rulebook2.webapp.startup import add_fixup_content_task
add_fixup_content_task( "index searchable content",
from asl_rulebook2.webapp.startup import _add_startup_task
_add_startup_task( "index searchable content",
lambda: _fixup_searchable_content( sr_type, fixup_index_entry, make_fields )
)
@ -556,8 +557,8 @@ def _init_qa( curs, qa, logger ):
for answer in content.get( "answers", [] ):
_tag_ruleids_in_field( answer, 0, cset_id )
return qa_entry
from asl_rulebook2.webapp.startup import add_fixup_content_task
add_fixup_content_task( "Q+A searchable content",
from asl_rulebook2.webapp.startup import _add_startup_task
_add_startup_task( "Q+A searchable content",
lambda: _fixup_searchable_content( sr_type, fixup_qa, make_fields )
)
@ -602,8 +603,8 @@ def _do_init_anno( curs, anno, atype ):
anno = _fts_index[ sr_type ][ rowid ]
_tag_ruleids_in_field( anno, "content", cset_id )
return anno
from asl_rulebook2.webapp.startup import add_fixup_content_task
add_fixup_content_task( atype+" searchable content",
from asl_rulebook2.webapp.startup import _add_startup_task
_add_startup_task( atype+" searchable content",
lambda: _fixup_searchable_content( sr_type, fixup_anno, make_fields )
)
@ -655,8 +656,8 @@ def _init_asop( curs, asop, asop_preambles, asop_content, logger ):
return entry
def make_fields( entry ):
return { "content": entry }
from asl_rulebook2.webapp.startup import add_fixup_content_task
add_fixup_content_task( "ASOP searchable content", fixup_content )
from asl_rulebook2.webapp.startup import _add_startup_task
_add_startup_task( "ASOP searchable content", fixup_content )
def _extract_section_entries( content ):
"""Separate out each entry from the section's content."""
@ -802,7 +803,7 @@ def _fixup_searchable_content( sr_type, fixup_row, make_fields ):
# minimum amount of time.
new_row = fixup_row( row[0], row[1] )
with webapp_startup.fixup_content_lock:
with _fixup_content_lock:
# NOTE: The make_fields() callback will usually be accessing the fields we want to fixup,
# so we need to protect them with the lock.
fields = make_fields( new_row )
@ -834,10 +835,10 @@ def _tag_ruleids_in_field( obj, key, cset_id ):
# they have been loaded, so the only thread-safety we need to worry about is when we read
# the original value from an object, and when we update it with a new value. The actual process
# of tagging ruleid's in a piece of content is done outside the lock, since it's quite slow.
with webapp_startup.fixup_content_lock:
with _fixup_content_lock:
val = obj[key]
new_val = tag_ruleids( val, cset_id )
with webapp_startup.fixup_content_lock:
with _fixup_content_lock:
obj[key] = new_val
# FUDGE! Give other threads a chance to run :-/
global _last_sleep_time

@ -5,6 +5,7 @@ import datetime
import threading
import logging
import traceback
import enum
from collections import defaultdict
from flask import jsonify
@ -18,14 +19,23 @@ from asl_rulebook2.webapp.utils import parse_int
_capabilities = None
fixup_content_lock = threading.Lock()
_fixup_content_tasks = None
_startup_tasks = None
_logger = logging.getLogger( "startup" )
_startup_msgs = None
# ---------------------------------------------------------------------
class StartupStatusEnum( enum.IntEnum ): #pylint: disable=missing-class-docstring
NOT_STARTED = 0
STARTED = 1
TASKS_RUNNING = 2
COMPLETED = -1
_startup_status = StartupStatusEnum.NOT_STARTED
# ---------------------------------------------------------------------
def init_webapp():
"""Initialize the webapp.
@ -34,10 +44,11 @@ def init_webapp():
"""
# initialize
global _startup_msgs, _capabilities, _fixup_content_tasks
global _startup_status, _startup_msgs, _capabilities, _startup_tasks
_startup_status = StartupStatusEnum.STARTED
_startup_msgs = StartupMsgs()
_capabilities = {}
_fixup_content_tasks = []
_startup_tasks = []
# initialize the webapp
content_sets = load_content_sets( _startup_msgs, _logger )
@ -67,26 +78,29 @@ def init_webapp():
# eventually start to be returned as search results. We could do this process once, and save the results
# in a file, then reload everything at startup, which will obviously be much faster, but we then have to
# figure out when that file needs to be rebuolt :-/
if app.config.get( "BLOCKING_FIXUP_CONTENT" ):
if app.config.get( "BLOCKING_STARTUP_TASKS" ):
# NOTE: It's useful to do this synchronously when running the test suite, since if the tests
# need the linkified ruleid's, they can't start until the fixup has finished (and if they don't
# it won't really matter, since there will be so little data, this process will be fast).
_do_fixup_content( False )
_do_startup_tasks( False )
else:
threading.Thread( target=_do_fixup_content, args=(True,) ).start()
threading.Thread( target=_do_startup_tasks, args=(True,) ).start()
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
def add_fixup_content_task( ctype, func ):
"""Register a function to fixup content after startup has finished."""
if app.config.get( "DISABLE_FIXUP_CONTENT" ):
def _add_startup_task( ctype, func ):
"""Register a function to run at startup."""
if app.config.get( "DISABLE_STARTUP_TASKS" ):
return
_fixup_content_tasks.append( ( ctype, func ) )
_startup_tasks.append( ( ctype, func ) )
def _do_fixup_content( delay ):
"""Run each task to fixup content."""
def _do_startup_tasks( delay ):
"""Run each registered startup task."""
if not _fixup_content_tasks:
# initialize
global _startup_status
if not _startup_tasks:
_startup_status = StartupStatusEnum.COMPLETED
return
# FUDGE! If we start processing straight away, the main PDF loads very slowly because of us :-/,
@ -97,25 +111,28 @@ def _do_fixup_content( delay ):
# very slow loads. To work around this, _tag_ruleids_in_field() sleeps periodically, to give
# other threads a chance to run. The PDF's load a bit slowly, but it's acceptable.
if delay:
delay = parse_int( app.config.get( "FIXUP_CONTENT_DELAY" ), 5 )
delay = parse_int( app.config.get( "STARTUP_TASKS_DELAY" ), 5 )
time.sleep( delay )
# process each fixup task
_logger.info( "Processing fixup tasks..." )
# process each startup task
_startup_status = StartupStatusEnum.TASKS_RUNNING
_logger.info( "Processing startup tasks..." )
start_time = time.time()
for task_no, (ctype, func) in enumerate( _fixup_content_tasks ):
_logger.debug( "Fixing up %s (%d/%d)...", ctype, 1+task_no, len(_fixup_content_tasks) )
for task_no, (ctype, func) in enumerate( _startup_tasks ):
_logger.debug( "Running startup task '%s' (%d/%d)...", ctype, 1+task_no, len(_startup_tasks) )
start_time2 = time.time()
try:
msg = func()
except Exception as ex: #pylint: disable=broad-except
_logger.error( "Couldn't fixup %s: %s\n%s", ctype, ex, traceback.format_exc() )
_logger.error( "Startup task '%s' failed: %s\n%s", ctype, ex, traceback.format_exc() )
continue
elapsed_time = datetime.timedelta( seconds = int( time.time() - start_time2 ) )
_logger.debug( "- Finished fixing up %s (%s): %s", ctype, elapsed_time, msg )
_logger.debug( "- Finished startup task '%s' (%s): %s", ctype, elapsed_time, msg )
# finish up
elapsed_time = datetime.timedelta( seconds = int( time.time() - start_time ) )
_logger.info( "All fixup tasks completed (%s).", elapsed_time )
_logger.info( "All startup tasks completed (%s).", elapsed_time )
_startup_status = StartupStatusEnum.COMPLETED
# ---------------------------------------------------------------------
@ -149,6 +166,13 @@ def get_startup_msgs():
"""Return any messages issued during startup."""
return jsonify( _startup_msgs.msgs )
@app.route( "/startup-status" )
def get_startup_status():
"""Return the current startup status."""
return jsonify( {
"status": _startup_status
} )
# ---------------------------------------------------------------------
class StartupMsgs:

@ -70,6 +70,9 @@ gMainApp.component( "main-app", {
$( "body" ).bind( "keydown", "alt+c", function( evt ) { selectNav( "chapters" ) ; evt.preventDefault() ; } ) ;
$( "body" ).bind( "keydown", "alt+a", function( evt ) { selectNav( "asop" ) ; evt.preventDefault() ; } ) ;
// monitor the backend startup process
this.startupTimerId = setInterval( this.checkStartupStatus, 1*1000 ) ;
// initialze the webapp
Promise.all( [
this.getAppConfig(),
@ -275,6 +278,25 @@ gMainApp.component( "main-app", {
gEventBus.emit( "app-loaded" ) ;
},
checkStartupStatus() {
// check the backend startup status
getJSON( gGetStartupStatusUrl ).then( (resp) => { //eslint-disable-line no-undef
if ( resp.status < 0 ) {
// startup has finished
$( "#startup-tasks-loading" ).fadeOut() ;
clearInterval( this.startupTimerId ) ;
this.startupTimerId = -1 ;
} else if ( resp.status == 2 ) {
// startup is in progress
// NOTE: We don't show the loading spinner for STARTED, since for most users, the startup process
// will be fast, and we don't want to show the spinner at all.
$( "#startup-tasks-loading" ).show() ;
}
} ).catch( (errorMsg) => { //eslint-disable-line no-unused-vars
// NOTE: We occasionally get an error on the first call, but we can probably ignore errors here.
} ) ;
},
onEscapePressed() {
// check if there are any notification balloons open
if ( $( ".growl" ).length > 0 ) {

@ -1,5 +1,5 @@
import { gMainApp, gAppConfig, gContentDocs, gEventBus, gUrlParams } from "./MainApp.js" ;
import { getJSON, getURL, linkifyAutoRuleids, getASOPChapterIdFromSectionId, showWarningMsg } from "./utils.js" ;
import { getJSON, getURL, linkifyAutoRuleids, getASOPChapterIdFromSectionId, showWarningMsg, makeImageUrl } from "./utils.js" ;
// --------------------------------------------------------------------
@ -8,6 +8,7 @@ gMainApp.component( "nav-pane", {
props: [ "asop" ],
data() { return {
ruleInfo: [],
startupTasksImageUrl: makeImageUrl( "loading.gif" ),
} ; },
template: `
@ -26,6 +27,7 @@ gMainApp.component( "nav-pane", {
</tabbed-page>
</tabbed-pages>
<div id="watermark" />
<img :src=startupTasksImageUrl id="startup-tasks-loading" title="Preparing searchable content..." />
<rule-info :ruleInfo=ruleInfo @close=closeRuleInfo />
</div>`,

@ -13,3 +13,8 @@
opacity: 0 ; z-index: -99 ;
transition: opacity 10s ;
}
#startup-tasks-loading {
position: absolute ; right: 2px ; bottom: 4px ;
height: 20px ; opacity: 0.4 ;
display: none ;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

@ -65,6 +65,7 @@ gGetASOPFooterUrl = "{{ url_for( 'get_asop_footer' ) }}" ;
gGetASOPPreambleUrl = "{{ url_for( 'get_asop_preamble', chapter_id='CHAPTER_ID' ) }}" ;
gGetASOPSectionUrl = "{{ url_for( 'get_asop_section', section_id='SECTION_ID' ) }}" ;
gGetStartupMsgsUrl = "{{ url_for( 'get_startup_msgs' ) }}" ;
gGetStartupStatusUrl = "{{ url_for( 'get_startup_status' ) }}" ;
gSearchUrl = "{{ url_for( 'search' ) }}" ;
gGetRuleInfoUrl = "{{ url_for( 'get_rule_info', ruleid='RULEID' ) }}" ;
gGetQAImageUrl = "{{ url_for( 'get_qa_image', fname='FNAME' ) }}" ;

@ -35,7 +35,7 @@ def init_webapp( webapp, webdriver, **options ):
}
# load the webapp
webapp.control_tests.set_app_config_val( "BLOCKING_FIXUP_CONTENT", True )
webapp.control_tests.set_app_config_val( "BLOCKING_STARTUP_TASKS", True )
webdriver.get( make_webapp_main_url( options ) )
_wait_for_webapp()

Loading…
Cancel
Save