I have an app under development which uses a database for a subset of its functionality. During development we have a higher than normal expectation that the database won't be available. Once launched, we would still like the main body of the application to work even if there is a database outage.
All of my database-facing classes extend a base class which grants them database access, however in the concrete classes there are some methods which perform selects
directly (meaning not through the base classes methods).
Short of wrapping every database call in if
statements, how can I gracefully degrade functionality when the database is not available?
EDIT
Failing gracefully for this app would be displaying a "service unavailable" message and stopping classes from attempting to access the database (which would throw errors).
IMHO this falls under the wider scope of general error and exception handling.
The database is essentially a global variable input in your code. If it's unavailable this is the very definition of an unexpected, "exceptional" condition; it should result in an exception. Of course, you explicitly asked how to handle the situation "short of wrapping every database call in if statements." Wrapping each and every DB operation in a try/catch
block is basically the same as wrapping each operation in if
statements.
Further, the mysql_connect
function will raise a PHP error in addition to returning FALSE
if it can't connect. You should really be using PDO
instead of the mysql dinosaur, but that's another topic entirely. Hopefully you aren't using the error suppression operator @
or not reporting E_WARNING
errors to avoid this.
Personally, I would have nothing to do with error handling in PHP. You should define a custom error handler function using set_error_handler
docs that throws an exception on any PHP error and handle exceptions instead of errors.
When you set up your error handling strategy in this manner it's then simple to wrap any functionality that could cause problems or unexpectedly throw in a try
block and handle the unexpected events in your catch
blocks.
Since you stated that, "All of [your] database-facing classes extend a base class which grants them database access," I would suggest adding a simple boolean property to the base class to act as a flag for whether or not the database connection has failed. Then, just check the value of the flag before each attempt to query the database. If you're correctly passing around the database instance in your classes (and not breaking references), they'll all have access to the same property when they need it.
If the database connection failed, you would just set the hypothetical $dbConn->connError = TRUE;
flag in the try block.
So, to summarize, while the specifics of how to structure the actual code depend on exactly what you're doing, using exceptions to handle error conditions is IMHO the most robust way to go about it.
Is AOP a possibility? That could allow you to write code that intercepts any part of code that connects to the DB, and insert your error handling code.
I don't think retrofitting my solution in a project is easily done, but I do it like this:
When I want a failure to be recognized as critical in my application, I use Exceptions. In my error handler I have some logic which determines whether the exceptions matches a http-status code like 4xx, if not a generic 500 status code is generated.
Non-critical failures are captured by a less disruptive mechanism using a custom result-class. It's basic structure is similar to an Exception, in that a code
and message
are passed as constructor-arguments. Additionally, a result-object can be passed as an optional argument.
The Result-class looks like this:
class Result
{
/**
* @var int
*/
protected $resultCode;
/**
* @var string
*/
protected $resultMessage;
/**
* @var mixed
*/
protected $resultObject;
/**
* Inject object values on construction.
*
* @param int $code
* @param string $message
* @param mixed $result
*/
public function __construct($code = 0, $message = null, $result = null)
{
$this->resultCode = intval($code);
$this->resultMessage = (string) $message;
$this->resultObject = $result;
}
/**
* Returns true when result is considered a success, determined by the
* resultCode, otherwise false.
*
* @return bool
*/
public function isSuccess()
{
if ($this->resultCode > 0) {
return true;
}
return false;
}
/**
* Returns true when result is considered an error, determined by the
* resultCode, otherwise false.
*
* @return bool
*/
public function isError()
{
if ($this->resultCode < 1) {
return true;
}
return false;
}
/**
* Returns the result message.
*
* @return string
*/
public function getMessage()
{
return $this->resultMessage;
}
/**
* Returns the actual result object.
*
* @return mixed
*/
public function getResult()
{
return $this->resultObject;
}
}
Only the result code is required. The result is considered successful if the code is > 0, otherwise it is considered to be an error. The result-object can either be the actual result (if successful), e.g. a row or rowset from a query or (if error) the input or parts of the input, which are relevant, e.g. the row-id you were looking for.
As this result-class does not trigger the errorHandler, the MVC-application is trusted to handle the errors. either by throwing an exception, or gracefully reacting to the failure-result by showing a message or whatever was passed as result-object.
If I pass this result to my view I can react whether $result->isSuccess()
is true or false and just get the actual result via $result->getResult()
or display an error message via $result->getMessage()
.