/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.karaf.main.lock; import java.sql.*; import org.apache.felix.utils.properties.Properties; import java.util.logging.Level; import java.util.logging.Logger; import org.apache.karaf.main.util.BootstrapLogManager; /** * Represents an exclusive lock on a database, * used to avoid multiple Karaf instances attempting * to become master. */ public class DefaultJDBCLock implements Lock { final Logger LOG = Logger.getLogger(this.getClass().getName()); private static final String PROPERTY_LOCK_URL = "karaf.lock.jdbc.url"; private static final String PROPERTY_LOCK_JDBC_DRIVER = "karaf.lock.jdbc.driver"; private static final String PROPERTY_LOCK_JDBC_USER = "karaf.lock.jdbc.user"; private static final String PROPERTY_LOCK_JDBC_PASSWORD = "karaf.lock.jdbc.password"; private static final String PROPERTY_LOCK_JDBC_TABLE = "karaf.lock.jdbc.table"; private static final String PROPERTY_LOCK_JDBC_CLUSTERNAME = "karaf.lock.jdbc.clustername"; private static final String PROPERTY_LOCK_JDBC_TIMEOUT = "karaf.lock.jdbc.timeout"; private static final String DEFAULT_PASSWORD = ""; private static final String DEFAULT_USER = ""; private static final String DEFAULT_TABLE = "KARAF_LOCK"; private static final String DEFAULT_CLUSTERNAME = "karaf"; private static final String DEFAULT_TIMEOUT = "10"; // in seconds final Statements statements; Connection lockConnection; String url; String driver; String user; String password; String table; String clusterName; int timeout; public DefaultJDBCLock(Properties props) { BootstrapLogManager.configureLogger(LOG); this.url = props.getProperty(PROPERTY_LOCK_URL); this.driver = props.getProperty(PROPERTY_LOCK_JDBC_DRIVER); this.user = props.getProperty(PROPERTY_LOCK_JDBC_USER, DEFAULT_USER); this.password = props.getProperty(PROPERTY_LOCK_JDBC_PASSWORD, DEFAULT_PASSWORD); this.table = props.getProperty(PROPERTY_LOCK_JDBC_TABLE, DEFAULT_TABLE); this.clusterName = props.getProperty(PROPERTY_LOCK_JDBC_CLUSTERNAME, DEFAULT_CLUSTERNAME); this.timeout = Integer.parseInt(props.getProperty(PROPERTY_LOCK_JDBC_TIMEOUT, DEFAULT_TIMEOUT)); this.statements = createStatements(); init(); } /** * This method is called to create an instance of the Statements instance. * * @return an instance of a Statements object */ Statements createStatements() { Statements statements = new Statements(); statements.setTableName(table); statements.setNodeName(clusterName); return statements; } void init() { try { createDatabase(); createSchema(); } catch (Exception e) { LOG.log(Level.SEVERE, "Error occured while attempting to obtain connection", e); } } void createDatabase() { // do nothing in the default implementation } /** * This method is called to check and create the required schemas that are used by this instance. */ void createSchema() { if (schemaExists()) { return; } String[] createStatments = this.statements.getLockCreateSchemaStatements(getCurrentTimeMillis()); Statement statement = null; Connection connection = null; try { connection = getConnection(); statement = connection.createStatement(); for (String stmt : createStatments) { LOG.info("Executing statement: " + stmt); statement.execute(stmt); } getConnection().commit(); } catch (Exception e) { LOG.log(Level.SEVERE, "Could not create schema", e); try { // Rollback transaction if and only if there was a failure... if (connection != null) connection.rollback(); } catch (Exception ie) { // Do nothing.... } } finally { closeSafely(statement); } } /** * This method is called to determine if the required database schemas have already been created or not. * * @return true, if the schemas are available else false. */ boolean schemaExists() { return schemaExist(statements.getFullLockTableName()); } /** * This method is called to determine if the required table is available or not. * * @param tableName The name of the table to determine if it exists * * @return true, if the table exists else false */ boolean schemaExist(String tableName) { try { DatabaseMetaData metadata = getConnection().getMetaData(); return metadata != null && (checkTableExists(tableName.toLowerCase(), metadata)); } catch (Exception ignore) { return false; //throw new RuntimeException("Error testing for db table", ignore); } } private boolean checkTableExists(String tableName, DatabaseMetaData metadata) throws SQLException { try (ResultSet rs = metadata.getTables(null, null, tableName, new String[] {"TABLE"})) { return rs.next(); } } /* * (non-Javadoc) * @see org.apache.karaf.main.Lock#lock() */ public boolean lock() { boolean result = aquireLock(); if (result) { result = updateLock(); } return result; } boolean aquireLock() { String lockCreateStatement = statements.getLockCreateStatement(); PreparedStatement preparedStatement = null; boolean lockAquired = false; try { preparedStatement = getConnection().prepareStatement(lockCreateStatement); preparedStatement.setQueryTimeout(timeout); lockAquired = preparedStatement.execute(); } catch (Exception e) { // Do we want to display this message everytime??? log(Level.WARNING, "Failed to acquire database lock", e); } finally { closeSafely(preparedStatement); } return lockAquired; } boolean updateLock() { String lockUpdateStatement = statements.getLockUpdateStatement(getCurrentTimeMillis()); PreparedStatement preparedStatement = null; boolean lockUpdated = false; try { preparedStatement = getConnection().prepareStatement(lockUpdateStatement); preparedStatement.setQueryTimeout(timeout); int rows = preparedStatement.executeUpdate(); lockUpdated = (rows == 1); } catch (Exception e) { log(Level.WARNING, "Failed to update database lock", e); } finally { closeSafely(preparedStatement); } return lockUpdated; } /** * Can be overridden to suppress logs in tests */ public void log(Level level, String msg, Exception e) { LOG.log(level, msg, e); } /* * (non-Javadoc) * @see org.apache.karaf.main.Lock#release() */ public void release() throws Exception { if (isConnected()) { try { getConnection().rollback(); } catch (SQLException e) { LOG.log(Level.SEVERE, "Exception while rollbacking the connection on release", e); } finally { try { getConnection().close(); } catch (SQLException ignored) { LOG.log(Level.FINE, "Exception while closing connection on release", ignored); } } } lockConnection = null; } /* * (non-Javadoc) * @see org.apache.karaf.main.Lock#isAlive() */ public boolean isAlive() throws Exception { if (!isConnected()) { LOG.severe("Lost lock!"); return false; } return updateLock(); } /** * This method is called to determine if this instance jdbc connection is * still connected. * * @return true, if the connection is still connected else false * * @throws SQLException */ boolean isConnected() throws SQLException { return lockConnection != null && !lockConnection.isClosed(); } /** * This method is called to safely close a Statement. * * @param preparedStatement The statement to be closed */ void closeSafely(Statement preparedStatement) { if (preparedStatement != null) { try { preparedStatement.close(); } catch (SQLException e) { LOG.log(Level.SEVERE, "Failed to close statement", e); } } } /** * This method is called to safely close a ResultSet instance. * * @param rs The result set to be closed */ void closeSafely(ResultSet rs) { if (rs != null) { try { rs.close(); } catch (SQLException e) { LOG.log(Level.SEVERE, "Error occured while releasing ResultSet", e); } } } /** * This method will return an active connection for this given jdbc driver. * * @return jdbc Connection instance * * @throws Exception */ Connection getConnection() throws Exception { if (!isConnected()) { lockConnection = createConnection(driver, url, user, password); lockConnection.setAutoCommit(false); } return lockConnection; } /** * Create a new jdbc connection. * * @param driver * @param url * @param username * @param password * @return a new jdbc connection * @throws Exception */ Connection createConnection(String driver, String url, String username, String password) throws Exception { if (url.toLowerCase().startsWith("jdbc:derby")) { url = (url.toLowerCase().contains("create=true")) ? url : url + ";create=true"; } try { return doCreateConnection(driver, url, username, password); } catch (Exception e) { LOG.log(Level.SEVERE, "Error occured while setting up JDBC connection", e); throw e; } } /** * This method could be used to inject a mock jdbc connection for testing purposes. * * @param driver * @param url * @param username * @param password * @return * @throws ClassNotFoundException * @throws SQLException */ Connection doCreateConnection(String driver, String url, String username, String password) throws ClassNotFoundException, SQLException { Class.forName(driver); // results in a closed connection in Derby if the update lock table request timed out // DriverManager.setLoginTimeout(timeout); return DriverManager.getConnection(url, username, password); } long getCurrentTimeMillis() { return System.currentTimeMillis(); } }