Check if we have a working database connection at startup.

master
Pacman Ghost 4 years ago
parent 9b2bdac85b
commit 3778e181b9
  1. 19
      asl_articles/__init__.py
  2. 22
      asl_articles/startup.py
  3. 36
      asl_articles/tests/test_startup.py
  4. 68
      web/src/App.js

@ -7,6 +7,7 @@ import logging.config
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy.exc import SQLAlchemyError
import yaml
from asl_articles.config.constants import BASE_DIR
@ -17,6 +18,23 @@ from asl_articles.utils import to_bool
def _on_startup():
"""Do startup initialization."""
# check if we have a working database connection
if _dbconn_string.startswith( "sqlite:///" ):
# NOTE: If the SQLite database is not there, a zero-byte file will be created the first time we try to use it,
# so we can't use the normal check we use below for other databases.
# NOTE: We could automatically set up a new database file and install the schema into it, but that's probably
# more trouble than it's worth, and possibly a cause of problems in itself :-/
fname = _dbconn_string[10:]
if not os.path.isfile( fname ):
asl_articles.startup.log_startup_msg( "error", "Missing SQLite database:\n{}", fname )
return
else:
try:
db.session.execute( "SELECT 1" )
except SQLAlchemyError as ex:
asl_articles.startup.log_startup_msg( "error", "Can't connect to the database:\n{}", ex )
return
# initialize the search index
_logger = logging.getLogger( "startup" )
asl_articles.search.init_search( db.session, _logger )
@ -80,6 +98,7 @@ db = SQLAlchemy( app )
# load the application
import asl_articles.globvars #pylint: disable=cyclic-import
import asl_articles.startup #pylint: disable=cyclic-import
import asl_articles.main #pylint: disable=cyclic-import
import asl_articles.search #pylint: disable=cyclic-import
import asl_articles.publishers #pylint: disable=cyclic-import

@ -0,0 +1,22 @@
""" Manage the startup process. """
from flask import jsonify
from asl_articles import app
_startup_msgs = {
"info": [],
"warning": [],
"error": []
}
# ---------------------------------------------------------------------
@app.route( "/startup-messages" )
def get_startup_msgs():
"""Return any messages logged during startup."""
return jsonify( _startup_msgs )
def log_startup_msg( msg_type, msg, *args, **kwargs ):
"""Log a startup message."""
_startup_msgs[ msg_type ].append( msg.format( *args, **kwargs ) )

@ -0,0 +1,36 @@
""" Test the startup process. """
import asl_articles.startup
from asl_articles.tests.utils import init_tests, wait_for, find_child, set_toast_marker, check_toast
# ---------------------------------------------------------------------
def test_startup_messages( webdriver, flask_app, dbconn ):
"""Test startup messages."""
# initialize
init_tests( webdriver, flask_app, dbconn )
startup_msgs = asl_articles.startup._startup_msgs #pylint: disable=protected-access
def do_test( msg_type ):
# check that the startup message was shown in the UI correctly
set_toast_marker( msg_type )
assert startup_msgs[ msg_type ] == []
asl_articles.startup.log_startup_msg( msg_type, "TEST: {}", msg_type )
webdriver.refresh()
expected = startup_msgs[ msg_type ][0]
wait_for( 2, lambda: check_toast( msg_type, expected ) )
startup_msgs[ msg_type ] = []
# check if the webapp started up or not
if msg_type == "error":
assert not find_child( "#search-form" )
else:
assert find_child( "#search-form" )
# test each type of startup message
do_test( "info" )
do_test( "warning" )
do_test( "error" )

@ -121,24 +121,60 @@ export default class App extends React.Component
}
componentDidMount() {
// initialize the caches
// NOTE: We maintain caches of key objects, so that we can quickly populate droplists. The backend server returns
// updated lists after any operation that could change them (create/update/delete), which is simpler and less error-prone
// than trying to manually keep our caches in sync. It's less efficient, but it won't happen too often, there won't be
// too many entries, and the database server is local.
this.caches = {} ;
["publishers","publications","authors","scenarios","tags"].forEach( (type) => {
axios.get( this.makeFlaskUrl( "/" + type ) )
.then( resp => {
this.caches[ type ] = resp.data ;
this._onStartupTask( "caches." + type ) ;
} )
.catch( err => {
this.showErrorToast( <div> Couldn't load the {type}: <div className="monospace"> {err.toString()} </div> </div> ) ;
} ) ;
} ) ;
// install our key handler
window.addEventListener( "keydown", this.onKeyDown.bind( this ) ) ;
// check if the server started up OK
let on_startup_ok = () => {
// the backend server started up OK, continue our startup process
// initialize the caches
// NOTE: We maintain caches of key objects, so that we can quickly populate droplists. The backend server returns
// updated lists after any operation that could change them (create/update/delete), which is simpler and less error-prone
// than trying to manually keep our caches in sync. It's less efficient, but it won't happen too often, there won't be
// too many entries, and the database server is local.
this.caches = {} ;
[ "publishers", "publications", "authors", "scenarios", "tags" ].forEach( type => {
axios.get( this.makeFlaskUrl( "/" + type ) )
.then( resp => {
this.caches[ type ] = resp.data ;
this._onStartupTask( "caches." + type ) ;
} )
.catch( err => {
this.showErrorToast( <div> Couldn't load the {type}: <div className="monospace"> {err.toString()} </div> </div> ) ;
} ) ;
} ) ;
}
let on_startup_failure = () => {
// the backend server had problems during startup; we hide the spinner
// and leave the error message(s) on-screen.
document.getElementById( "loading" ).style.display = "none" ;
}
axios.get( this.makeFlaskUrl( "/startup-messages" ) )
.then( resp => {
// show any messages logged by the backend server as it started up
[ "info", "warning", "error" ].forEach( msgType => {
if ( resp.data[ msgType ] ) {
resp.data[ msgType ].forEach( msg => {
const pos = msg.indexOf( ":\n" ) ;
if ( pos !== -1 ) {
msg = ( <div> {msg.substr(0,pos+1)}
<div className="monospace"> {msg.substr(pos+2)} </div>
</div> ) ;
}
const funcName = "show" + msgType[0].toUpperCase() + msgType.substr(1) + "Toast" ;
this[ funcName ]( msg ) ;
} ) ;
}
} ) ;
if ( resp.data.error && resp.data.error.length > 0 )
on_startup_failure() ;
else
on_startup_ok() ;
} )
.catch( err => {
this.showErrorToast( <div> Couldn't get the startup messages: <div className="monospace"> {err.toString()} </div> </div> ) ;
on_startup_failure() ;
} ) ;
}
componentDidUpdate() {

Loading…
Cancel
Save