1
0
Fork 0
mirror of https://github.com/airsonic/airsonic.git synced 2025-10-03 09:49:17 +02:00

Add logic for seamless HSQLDB upgrades

This commit is contained in:
François-Xavier Thomas 2019-10-16 23:07:20 +02:00
parent 81c23e4806
commit 0ffc0f262a
5 changed files with 204 additions and 20 deletions

View file

@ -1,6 +1,7 @@
package org.airsonic.player; package org.airsonic.player;
import org.airsonic.player.filter.*; import org.airsonic.player.filter.*;
import org.airsonic.player.util.LegacyHsqlUtil;
import org.directwebremoting.servlet.DwrServlet; import org.directwebremoting.servlet.DwrServlet;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -13,11 +14,13 @@ import org.springframework.boot.autoconfigure.jmx.JmxAutoConfiguration;
import org.springframework.boot.autoconfigure.liquibase.LiquibaseAutoConfiguration; import org.springframework.boot.autoconfigure.liquibase.LiquibaseAutoConfiguration;
import org.springframework.boot.autoconfigure.web.servlet.MultipartAutoConfiguration; import org.springframework.boot.autoconfigure.web.servlet.MultipartAutoConfiguration;
import org.springframework.boot.builder.SpringApplicationBuilder; import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent;
import org.springframework.boot.web.server.WebServerFactoryCustomizer; import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.boot.web.servlet.ServletRegistrationBean; import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.boot.web.servlet.server.ConfigurableServletWebServerFactory; import org.springframework.boot.web.servlet.server.ConfigurableServletWebServerFactory;
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer; import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
import org.springframework.context.ApplicationListener;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.util.ReflectionUtils; import org.springframework.util.ReflectionUtils;
@ -163,6 +166,11 @@ public class Application extends SpringBootServletInitializer implements WebServ
} }
private static SpringApplicationBuilder doConfigure(SpringApplicationBuilder application) { private static SpringApplicationBuilder doConfigure(SpringApplicationBuilder application) {
// Handle HSQLDB database upgrades from 1.8 to 2.x before any beans are started.
application.application().addListeners((ApplicationListener<ApplicationEnvironmentPreparedEvent>) event -> {
LegacyHsqlUtil.upgradeHsqldbDatabaseSafely();
});
// Customize the application or call application.sources(...) to add sources // Customize the application or call application.sources(...) to add sources
// Since our example is itself a @Configuration class (via @SpringBootApplication) // Since our example is itself a @Configuration class (via @SpringBootApplication)
// we actually don't need to override this method. // we actually don't need to override this method.

View file

@ -31,29 +31,26 @@ public class LegacyHsqlDaoHelper extends GenericDaoHelper {
LOG.debug("Database checkpoint complete."); LOG.debug("Database checkpoint complete.");
} }
@PreDestroy /**
public void onDestroy() { * Shutdown the embedded HSQLDB database. After this has run, the database cannot be accessed again from the same DataSource.
Connection conn = null; */
private void shutdownHsqldbDatabase() {
try { try {
// Properly shutdown the embedded HSQLDB database.
LOG.debug("Database shutdown in progress..."); LOG.debug("Database shutdown in progress...");
JdbcTemplate jdbcTemplate = getJdbcTemplate(); JdbcTemplate jdbcTemplate = getJdbcTemplate();
conn = DataSourceUtils.getConnection(jdbcTemplate.getDataSource()); try (Connection conn = DataSourceUtils.getConnection(jdbcTemplate.getDataSource())) {
conn.setAutoCommit(true); conn.setAutoCommit(true);
jdbcTemplate.execute("SHUTDOWN"); jdbcTemplate.execute("SHUTDOWN");
}
LOG.debug("Database shutdown complete."); LOG.debug("Database shutdown complete.");
} catch (SQLException e) { } catch (SQLException e) {
LOG.error("Database shutdown failed: " + e); LOG.error("Database shutdown failed", e);
e.printStackTrace(); }
}
} finally { @PreDestroy
try { public void onDestroy() {
if (conn != null) shutdownHsqldbDatabase();
conn.close();
} catch (Exception ex) {
ex.printStackTrace();
}
}
} }
} }

View file

@ -286,14 +286,26 @@ public class SettingsService {
return home.contains("libresonic") ? "libresonic" : "airsonic"; return home.contains("libresonic") ? "libresonic" : "airsonic";
} }
public static String getDefaultJDBCPath() {
return getAirsonicHome().getPath() + "/db/" + getFileSystemAppName();
}
public static String getDefaultJDBCUrl() { public static String getDefaultJDBCUrl() {
return "jdbc:hsqldb:file:" + getAirsonicHome().getPath() + "/db/" + getFileSystemAppName() + ";sql.enforce_size=false"; return "jdbc:hsqldb:file:" + getDefaultJDBCPath() + ";sql.enforce_size=false";
} }
public static int getDefaultUPnPPort() { public static int getDefaultUPnPPort() {
return Optional.ofNullable(System.getProperty(KEY_UPNP_PORT)).map(x -> Integer.parseInt(x)).orElse(DEFAULT_UPNP_PORT); return Optional.ofNullable(System.getProperty(KEY_UPNP_PORT)).map(x -> Integer.parseInt(x)).orElse(DEFAULT_UPNP_PORT);
} }
public static String getDefaultJDBCUsername() {
return "sa";
}
public static String getDefaultJDBCPassword() {
return "";
}
public static File getLogFile() { public static File getLogFile() {
File airsonicHome = SettingsService.getAirsonicHome(); File airsonicHome = SettingsService.getAirsonicHome();
return new File(airsonicHome, getFileSystemAppName() + ".log"); return new File(airsonicHome, getFileSystemAppName() + ".log");

View file

@ -51,8 +51,8 @@ public class DatabaseConfiguration {
BasicDataSource dataSource = new BasicDataSource(); BasicDataSource dataSource = new BasicDataSource();
dataSource.setDriverClassName("org.hsqldb.jdbcDriver"); dataSource.setDriverClassName("org.hsqldb.jdbcDriver");
dataSource.setUrl(SettingsService.getDefaultJDBCUrl()); dataSource.setUrl(SettingsService.getDefaultJDBCUrl());
dataSource.setUsername("sa"); dataSource.setUsername(SettingsService.getDefaultJDBCUsername());
dataSource.setPassword(""); dataSource.setPassword(SettingsService.getDefaultJDBCPassword());
return dataSource; return dataSource;
} }

View file

@ -0,0 +1,167 @@
package org.airsonic.player.util;
import org.airsonic.player.service.SettingsService;
import org.apache.commons.io.FileUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.sql.*;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Properties;
public class LegacyHsqlUtil {
private static final Logger LOG = LoggerFactory.getLogger(LegacyHsqlUtil.class);
/**
* Return the current version of the HSQLDB database, as reported by the database properties file.
*/
public static String getHsqldbDatabaseVersion() {
Properties prop = new Properties();
File configFile = new File(SettingsService.getDefaultJDBCPath() + ".properties");
if (!configFile.exists()) {
LOG.debug("HSQLDB database doesn't exist, cannot determine version");
return null;
}
try (InputStream stream = new FileInputStream(configFile)) {
prop.load(stream);
return prop.getProperty("version");
} catch (IOException e) {
LOG.error("Failed to determine HSQLDB database version", e);
return null;
}
}
/**
* Create a new connection to the HSQLDB database.
*/
public static Connection getHsqldbDatabaseConnection() throws SQLException {
String url = SettingsService.getDefaultJDBCUrl();
Properties properties = new Properties();
properties.put("user", SettingsService.getDefaultJDBCUsername());
properties.put("password", SettingsService.getDefaultJDBCPassword());
return DriverManager.getConnection(url, properties);
}
/**
* Check if a HSQLDB database upgrade will occur and backups are needed.
*
* DB Driver Likely reason Decision
* null - new db or non-legacy false
* - null or !2 something went wrong, we better make copies true
* 1.x 2.x this is the big upgrade true
* 2.x 2.x already up to date false
*
* @return true if a database backup/migration should be performed
*/
public static boolean isHsqldbDatabaseUpgradeNeeded() {
// Check the current database version
String currentVersion = getHsqldbDatabaseVersion();
if (currentVersion == null) {
LOG.debug("HSQLDB database not found, skipping upgrade checks");
return false;
}
// Check the database driver version
String driverVersion = null;
try {
Driver driver = DriverManager.getDriver(SettingsService.getDefaultJDBCUrl());
driverVersion = String.format("%d.%d", driver.getMajorVersion(), driver.getMinorVersion());
if (driver.getMajorVersion() != 2) {
LOG.warn("HSQLDB database driver version {} is untested ; trying to connect anyway, this may upgrade the database from version {}", driverVersion, currentVersion);
return true;
}
} catch (SQLException e) {
LOG.warn("HSQLDB database driver version cannot be determined ; trying to connect anyway, this may upgrade the database from version {}", currentVersion, e);
return true;
}
// Log what we're about to do and determine if we should perform a controlled upgrade with backups.
if (currentVersion.startsWith(driverVersion)) {
// If we're already on the same version as the driver, nothing should happen.
LOG.debug("HSQLDB database upgrade unneeded, already on version {}", driverVersion);
return false;
} else if (currentVersion.startsWith("2.")) {
// If the database version is 2.x but older than the driver, the upgrade should be relatively painless.
LOG.debug("HSQLDB database will be silently upgraded from version {} to {}", currentVersion, driverVersion);
return false;
} else if ("1.8.0".equals(currentVersion) || "1.8.1".equals(currentVersion)) {
// If we're on a 1.8.0 or 1.8.1 database and upgrading to 2.x, we're going to handle this manually and check what we're doing.
LOG.info("HSQLDB database upgrade needed, from version {} to {}", currentVersion, driverVersion);
return true;
} else {
// If this happens we're on a completely untested version and we don't know what will happen.
LOG.warn("HSQLDB database upgrade needed, from version {} to {}", currentVersion, driverVersion);
return true;
}
}
/**
* Perform a backup of the HSQLDB database, to a timestamped directory.
* @return the path to the backup directory
*/
public static Path performHsqldbDatabaseBackup() throws IOException {
String timestamp = DateTimeFormatter.ofPattern("yyyyMMddHHmmss").format(LocalDateTime.now());
Path source = Paths.get(SettingsService.getDefaultJDBCPath()).getParent();
Path destination = Paths.get(String.format("%s.backup.%s", SettingsService.getDefaultJDBCPath(), timestamp));
LOG.debug("Performing HSQLDB database backup...");
FileUtils.copyDirectory(source.toFile(), destination.toFile());
LOG.info("HSQLDB database backed up to {}", destination.toString());
return destination;
}
/**
* Perform an in-place database upgrade from HSQLDB 1.x to 2.x.
*/
public static void performHsqldbDatabaseUpgrade() throws SQLException {
LOG.debug("Performing HSQLDB database upgrade...");
// This will upgrade HSQLDB on the first connection. This does not
// use Spring's DataSource, as running SHUTDOWN against it will
// prevent further connections to the database.
try (Connection conn = getHsqldbDatabaseConnection()) {
LOG.debug("Database connection established. Current version is: {}", conn.getMetaData().getDatabaseProductVersion());
// On upgrade, the official documentation recommends that we
// run 'SHUTDOWN SCRIPT' to compact all the database into a
// single SQL file.
//
// In practice, if we don't do that, we did not observe issues
// immediately but after the upgrade.
LOG.debug("Shutting down database (SHUTDOWN SCRIPT)...");
try (Statement st = conn.createStatement()) {
st.execute("SHUTDOWN SCRIPT");
}
}
LOG.info("HSQLDB database has been upgraded to version {}", getHsqldbDatabaseVersion());
}
/**
* If needed, perform an in-place database upgrade from HSQLDB 1.x to 2.x after having created backups.
*/
public static void upgradeHsqldbDatabaseSafely() {
if (LegacyHsqlUtil.isHsqldbDatabaseUpgradeNeeded()) {
try {
performHsqldbDatabaseBackup();
} catch (Exception e) {
throw new RuntimeException("Failed to backup HSQLDB database before upgrade", e);
}
try {
performHsqldbDatabaseUpgrade();
} catch (Exception e) {
throw new RuntimeException("Failed to upgrade HSQLDB database", e);
}
}
}
}