/* ==================================================================
* RestoreFromBackupSQLExceptionHandler.java - 24/07/2016 4:04:23 PM
*
* Copyright 2007-2016 SolarNetwork.net Dev Team
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation; either version 2 of
* the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
* 02111-1307 USA
* ==================================================================
*/
package net.solarnetwork.node.dao.jdbc;
import java.io.File;
import java.io.IOException;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.regex.Pattern;
import org.osgi.framework.BundleContext;
import org.osgi.framework.ServiceReference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.FileSystemUtils;
import org.springframework.util.StringUtils;
import net.solarnetwork.dao.jdbc.SQLExceptionHandler;
import net.solarnetwork.node.backup.Backup;
import net.solarnetwork.node.backup.BackupManager;
import net.solarnetwork.node.backup.BackupService;
/**
* Recover from connection exceptions by restoring from backup.
*
* @author matt
* @version 1.0
*/
public class RestoreFromBackupSQLExceptionHandler implements SQLExceptionHandler {
private final int minimumExceptionCount;
private final BundleContext bundleContext;
private int restoreDelaySeconds = 15;
private String backupResourceProviderFilter;
private List<Pattern> sqlStatePatterns;
private final Logger log = LoggerFactory.getLogger(getClass());
private final AtomicInteger exceptionCount = new AtomicInteger(0);
private final AtomicBoolean restoreScheduled = new AtomicBoolean(false);
/**
* Constructor.
*
* @param bundleContext
* The active bundle context.
* @param minimumExceptionCount
* The minimum number of exceptions to witness before attempting to
* restore from backup.
*/
public RestoreFromBackupSQLExceptionHandler(BundleContext bundleContext, int minimumExceptionCount) {
super();
this.bundleContext = bundleContext;
this.minimumExceptionCount = minimumExceptionCount;
}
@Override
public synchronized void handleGetConnectionException(SQLException e) {
handleConnectionException(null, e);
}
@Override
public void handleConnectionException(Connection conn, SQLException e) {
SQLException root = e;
while ( root.getNextException() != null ) {
root = root.getNextException();
}
String state = root.getSQLState();
if ( state == null ) {
return;
}
List<Pattern> statePatterns = sqlStatePatterns;
if ( statePatterns == null || statePatterns.isEmpty() ) {
return;
}
for ( Pattern pat : statePatterns ) {
if ( pat.matcher(state).matches() ) {
log.error("Recovery triggering error {} on database connection: {}", state,
e.getMessage());
final int count = exceptionCount.incrementAndGet();
if ( count < minimumExceptionCount ) {
return;
}
scheduleRestoreFromBackup(count);
return;
}
}
}
private File getDbDir() {
String dbDir = System.getProperty("derby.system.home");
if ( dbDir == null ) {
return null;
}
// Should we get database name from JDBC connection properties? For now, this is hard-coded.
return new File(dbDir, "solarnode");
}
private void cleanupExistingDatabase() {
File f = getDbDir();
if ( f == null ) {
return;
}
if ( f.isDirectory() ) {
log.warn("Deleting DB dir {}", f.getAbsolutePath());
if ( FileSystemUtils.deleteRecursively(f) ) {
log.warn("Deleted database directory " + f.getAbsolutePath());
} else {
try {
java.nio.file.Files.delete(f.toPath());
} catch ( IOException e ) {
log.warn("Unable to delete database directory " + f.getAbsolutePath(), e);
}
}
}
}
private void scheduleRestoreFromBackup(final int count) {
if ( restoreScheduled.get() ) {
return;
}
log.warn("Scheduling restore from backup in {} seconds due to database connection exception",
restoreDelaySeconds);
Thread t = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(TimeUnit.SECONDS.toMillis(restoreDelaySeconds));
} catch ( InterruptedException e ) {
// ignore and continue
}
BackupService backupService = getBackupService();
if ( backupService != null ) {
Backup backup = getBackupToRestore(backupService);
if ( backup != null && restoreScheduled.compareAndSet(false, true) ) {
Map<String, String> props = Collections.singletonMap(
BackupManager.RESOURCE_PROVIDER_FILTER, backupResourceProviderFilter);
if ( backupService.markBackupForRestore(backup, props) ) {
cleanupExistingDatabase();
shutdown(backup);
}
}
} else {
scheduleRestoreFromBackup(count);
}
}
});
t.setContextClassLoader(Thread.currentThread().getContextClassLoader());
t.start();
}
private BackupService getBackupService() {
BackupManager mgr = backupManager();
if ( mgr == null ) {
log.debug("No BackupManager available to restore from");
return null;
}
BackupService result = mgr.activeBackupService();
return result;
}
private Backup getBackupToRestore(BackupService backupService) {
if ( backupService == null ) {
log.debug("No BackupService available to restore from");
return null;
}
Collection<Backup> backups = backupService.getAvailableBackups();
if ( backups == null || backups.isEmpty() ) {
log.debug("No Backup available to restore from");
return null;
}
Backup backup = null;
for ( Backup b : backups ) {
if ( b.isComplete() ) {
backup = b;
break;
}
}
return backup;
}
private BackupManager backupManager() {
ServiceReference<BackupManager> mgrRef = bundleContext.getServiceReference(BackupManager.class);
if ( mgrRef == null ) {
return null;
}
return bundleContext.getService(mgrRef);
}
private void shutdown(Backup backup) {
log.warn("Shutting down now to force restore from backup {}", backup.getKey());
// graceful would be bundleContext.getBundle(0).stop();, but we don't need to wait for that here
System.exit(0);
}
/**
* Set the number of seconds to delay the restore from backup. This is
* mainly to give the framework a time to boot up and provide the
* {@link BackupManager} service.
*
* @param restoreDelaySeconds
* The number of seconds to delay attempting the restore from backup.
*/
public void setRestoreDelaySeconds(int restoreDelaySeconds) {
this.restoreDelaySeconds = restoreDelaySeconds;
}
/**
* Set a filter to pass as {@link BackupManager#RESOURCE_PROVIDER_FILTER} to
* limit the scope of the backup.
*
* @param backupResourceProviderFilter
* The filter to set.
*/
public void setBackupResourceProviderFilter(String backupResourceProviderFilter) {
this.backupResourceProviderFilter = backupResourceProviderFilter;
}
/**
* Set a list of regular expressions that should trigger a restore from
* backup.
*
* @param regexes
* The regular expressions that should trigger a restore from backup.
*/
public void setSqlStatePatterns(List<Pattern> sqlStatePatterns) {
this.sqlStatePatterns = sqlStatePatterns;
}
/**
* Set a comma-delimited list of regular expressions that should trigger a
* restore from backup.
*
* @param regexes
* A comma-delimited list of regular expressions that should trigger
* a restore from backup.
* @see #setSqlStatePatterns(List)
*/
public void setSqlStateRegex(String regexes) {
List<Pattern> pats = null;
String[] list = StringUtils.delimitedListToStringArray(regexes, ",");
if ( regexes != null && list.length > 0 ) {
pats = new ArrayList<Pattern>();
for ( String regex : list ) {
pats.add(Pattern.compile(regex));
}
}
setSqlStatePatterns(pats);
}
}