All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.karaf.main.lock.GenericJDBCLock Maven / Gradle / Ivy

Go to download

This bundle is the main Karaf launcher. It's responsible of the Karaf startup including the console, branding, etc bootstrap.

There is a newer version: 4.4.6
Show newest version
/*
 * 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.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import org.apache.felix.utils.properties.Properties;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.apache.karaf.main.ConfigProperties;
import org.apache.karaf.main.util.BootstrapLogManager;

/**
 * This classs is the base class used to provide a master/slave configuration for
 * a given set of active karaf instances using JDBC. 

* * This implementation uses two different tables. A KARAF_NODE_ID, and KARAF_LOCK tables. The * KARAF_NODE_ID table is used to generate a unique id for each instance in the cluster. While * the KARAF_LOCK table is used to determine who is the master of these instances.

* * The tables configurations for the different tables are.

* *
 *   CREATE TABLE KARAF_NODE_ID ( ID INTEGER DEFAULT 0 )
 *   CREATE TABLE KARAF_LOCK ( ID INTEGER DEFAULT 0, STATE INTEGER DEFAULT 0, LOCK_DELAY INTEGER DEFAULT 0 )
 * 
* * The two tables will include a single row each that is created by a single instance in the cluster.

* * The KARAF_NODE_ID table will be updated once for each active karaf instance with there unique id compared * to the other instances within the cluster. The single row will contain the next available unique id and * will not include each clustered instance unique id since these instances can come and go throughout the * system lifetime.

* * The KARAF_LOCK table will be used to determine which of the instances will become the master. The master * will set the STATE to an initial value and the LOCK_DELAY to a time in milliseconds of when the * table will be updated. It is the responsibility of the master instance to update the STATE field by the * allocated lock delay by incrementing the state value. If the STATE value has not been updated by the * LOCK_DELAY time then a slave has permission to attempt to become the master.

* * While the overview does not describe exactly how this is implemented. Here is a description of how this * is done and what is provides as a fail safe solution.

* * Each instance of this class provides an initialization step, a lock, isAlive and release interface.

* * INITIALIZE:

* * During the initialization step it will determine if the given tables exist within the database. We only * check for a single table since we assume that if one is available then the other must exist. We then * add a row to each of the tables. The added row to the KARAF_NODE_ID table will set the ID to zero since * this is consider a non-existent karaf instance. The added row to the KARAF_LOCK will set the ID to zero * which allows a karaf instances to acquire the lock and become the master instance.

* * * LOCK:

* * The current instance will try to acquire the master lock by using the following sql statement.

* *
 *   UPDATE KARAF_LOCK SET ID = unique_id, STATE = state, LOCK_DELAY = lock_delay 
 *       WHERE ID = 0 OR ID = curId
 * 
* * Now you must be asking why are we using this update statement? The reason is that the statement will * guarantee that only one instance will be able to update this row. The curId is set to this instance * unique id or to the prior master unique id if the row was not updated within that master lock_delay.

* * The current update command will set the curId to this instance unique id. If this fails then it will * determine if the current master has not updated the row within its lock_delay. If it hasn't updated * the row within the allocated time then this instance can try to become the master.

* * The current slave instance will then try to steal the lock from the master instance. Why are we trying * to steal the lock from the master? The reason is that it is possible that the master instance had a * hard failure and there is no mechanisms to determine if that is the case. We then assume that it has * crashed without releasing the lock gracefully. The slave instance then used the following update statement.

* *
 *   UPDATE KARAF_LOCK SET ID = unique_id, STATE = state, LOCK_DELAY = lock_delay 
 *       WHERE ( ID = 0 OR ID = curId ) AND STATE = curState
 * 
* * Now why are we using the state value as part of the where clause? The reason that even though the row was * not updated by the allocated delay time. It is possible that the update statement was performed just after * the current slave check. This update will insure that the row will be updated if and only if the state was * also not updated. It is possible that the master instance updated the row after the current slave check * and we do not want the slave to update the row and make itself the master. This will insure that that will * not be the case.

* * ISALIVE:

* * This just checks if the connection is active and then just updates the row's STATE by using the lock * update call mentioned above.

* * RELEASE:

* * The release process just updates the KARAF_LOCK ID to zero so that other instances will have a chance * to become the master.

* * There are two main scenarios that we need to worry about. Soft and Hard failures. The soft failure * basically allows the master instance to release the master lock and allow other instances to become * the master. As for a hard failure, the current karaf instance crashes and does not release the lock * then the other karaf instances will notice that the KARAF_LOCK has not been updated for the current * master id and then they can compete for the master lock.

* * @author Claudio Corsi * */ public class GenericJDBCLock implements Lock { final Logger LOG = Logger.getLogger(this.getClass().getName()); public static final String PROPERTY_LOCK_URL = "karaf.lock.jdbc.url"; public static final String PROPERTY_LOCK_JDBC_DRIVER = "karaf.lock.jdbc.driver"; public static final String PROPERTY_LOCK_JDBC_USER = "karaf.lock.jdbc.user"; public static final String PROPERTY_LOCK_JDBC_PASSWORD = "karaf.lock.jdbc.password"; public static final String PROPERTY_LOCK_JDBC_TABLE = "karaf.lock.jdbc.table"; public static final String PROPERTY_LOCK_JDBC_TABLE_ID = "karaf.lock.jdbc.table_id"; public static final String PROPERTY_LOCK_JDBC_CLUSTERNAME = "karaf.lock.jdbc.clustername"; public static final String DEFAULT_PASSWORD = ""; public static final String DEFAULT_USER = ""; public static final String DEFAULT_TABLE = "KARAF_LOCK"; public static final String DEFAULT_TABLE_ID = "KARAF_NODE_ID"; public static final String DEFAULT_CLUSTERNAME = "karaf"; final GenericStatements statements; Connection lockConnection; String url; String driver; String user; String password; String table; String clusterName; String table_id; int lock_delay; // My lock settings private int uniqueId = 0; private int state = 0; // Current master instance lock settings private int currentId = 0; private int currentState = 0; // The last clock time that the master instance state was updated as noticed by this instance. private long currentStateTime; // The master lock delay time in milliseconds that the master is expected to update the karaf_lock // table state private int currentLockDelay; public GenericJDBCLock(Properties props) { try { LOG.addHandler(BootstrapLogManager.getDefaultHandler()); } catch (Exception e) { e.printStackTrace(); } 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.table_id = props.getProperty(PROPERTY_LOCK_JDBC_TABLE_ID, DEFAULT_TABLE_ID); this.lock_delay = Integer.parseInt(props.getProperty(ConfigProperties.PROPERTY_LOCK_DELAY, ConfigProperties.DEFAULT_LOCK_DELAY)); this.statements = createStatements(); init(); } /** * This method is called to create an instance of the JDBCStatements instance. * * @return an instance of a JDBCStatement object */ GenericStatements createStatements() { GenericStatements statements = new GenericStatements(table, table_id, clusterName); return statements; } void init() { try { createDatabase(); createSchema(); generateUniqueId(); } 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(System.currentTimeMillis()); Statement statement = null; Connection connection = null; try { connection = getConnection(); statement = connection.createStatement(); connection.setAutoCommit(false); for (String stmt : createStatments) { LOG.info("Executing statement: " + stmt); statement.execute(stmt); } connection.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); try { // Reset the auto commit to true connection.setAutoCommit(true); } catch (SQLException ignored) { LOG.log(Level.FINE, "Exception while setting the connection auto commit", ignored); } } } /** * 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(this.statements.getLockTableName()) && schemaExist(this.statements.getLockIdTableName()); } /** * 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 */ private boolean schemaExist(String tableName) { ResultSet rs = null; boolean schemaExists = false; try { rs = getConnection().getMetaData().getTables(null, null, tableName, new String[] {"TABLE"}); schemaExists = rs.next(); } catch (Exception ignore) { LOG.log(Level.SEVERE, "Error testing for db table", ignore); } finally { closeSafely(rs); } return schemaExists; } /** * This method will generate a unique id for this instance that is part of an active set of instances. * This method uses a simple algorithm to insure that the id will be unique for all cases. */ void generateUniqueId() { boolean uniqueIdSet = false; String selectString = this.statements.getLockIdSelectStatement(); PreparedStatement selectStatement = null, updateStatement = null; try { selectStatement = getConnection().prepareStatement(selectString); // This loop can only be performed for so long and the chances that this will be // looping for more than a few times is unlikely since there will always be at // least one instance that is successful. while (!uniqueIdSet) { ResultSet rs = null; try { // Get the current ID from the karaf ids table rs = selectStatement.executeQuery(); // Check if we were able to retrieve the result... if (rs.next()) { // Update the row with the next available id int currentId = this.statements.getIdFromLockIdSelectStatement(rs); String updateString = this.statements.getLockIdUpdateIdStatement(currentId + 1, currentId); updateStatement = getConnection().prepareStatement(updateString); int count = updateStatement.executeUpdate(); // Set the uniqueId if and only if is it greater that zero uniqueId = ( uniqueIdSet = count > 0 ) ? currentId + 1 : 0; if (count > 1) { LOG.severe("OOPS there are more than one row within the table ids..."); } } else { LOG.severe("No rows were found...."); } } catch (SQLException e) { LOG.log(Level.SEVERE, "Received an SQL exception while processing result set", e); } finally { this.closeSafely(rs); } } } catch (SQLException e) { LOG.log(Level.SEVERE, "Received an SQL exception while generating a prepate statement", e); } catch (Exception e) { LOG.log(Level.SEVERE, "Received an exception while trying to get a reference to a connection", e); } finally { closeSafely(selectStatement); } LOG.info("INSTANCE unique id: " + uniqueId); } /** * 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 */ protected Connection getConnection() throws Exception { if (!isConnected()) { lockConnection = createConnection(driver, url, user, password); } return lockConnection; } /** * Create a new jdbc connection. * * @param driver The fully qualified driver class name * @param url The database connection url * @param username The username for the database * @param password The password for the data * @return a new jdbc connection * @throws Exception */ protected 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 */ protected Connection doCreateConnection(String driver, String url, String username, String password) throws ClassNotFoundException, SQLException { Class.forName(driver); return DriverManager.getConnection(url, username, password); } /** * This method is called whenever we want to acquire/steal or update the lock. * The different option depend if we are the competing for the master or the lock delay time * has been exceeded or that we are the master and are update the state. * * @return true, if we are the master instance else false * * @see org.apache.karaf.main.lock.Lock#lock() */ public boolean lock() throws Exception { // Try to acquire/update the lock state boolean lockAquired = acquireLock(statements.getLockUpdateIdStatement(uniqueId, ++state, lock_delay, uniqueId)); if (!lockAquired) { String lockSelectStatement = statements.getLockSelectStatement(); PreparedStatement statement = null; ResultSet rs = null; try { statement = getConnection().prepareStatement(lockSelectStatement); // Get the current master id and compare with information that we have locally.... rs = statement.executeQuery(); if (rs.next()) { int currentId = statements.getIdFromLockSelectStatement(rs); // The current master unique id or 0 int currentState = statements.getStateFromLockSelectStatement(rs); // The current master state or whatever if (this.currentId == currentId) { // It is the same instance that locked the table if (this.currentState == currentState) { // Its state has not been updated.... if ( (this.currentStateTime + this.currentLockDelay + this.currentLockDelay) < System.currentTimeMillis() ) { // The state was not been updated for more than twice the lock_delay value of the current master... // Try to steal the lock.... lockAquired = acquireLock(statements.getLockUpdateIdStatementToStealLock(uniqueId, state, lock_delay, currentId, currentState)); } } else { // Set the current time to be used to determine if we can // try to steal the lock later... this.currentStateTime = System.currentTimeMillis(); this.currentState = currentState; } } else { // This is a different currentId that is being used... // at this time, it does not matter if the new master id is zero we can try to acquire it // during the next lock call... this.currentId = currentId; this.currentState = currentState; // Update the current state time since this is a new lock service... this.currentStateTime = System.currentTimeMillis(); // Get the lock delay value which is specific to the current master... this.currentLockDelay = statements.getLockDelayFromLockSelectStatement(rs); } } } catch( Exception e ) { LOG.log(Level.SEVERE, "Unable to determine if the lock was obtain", e); } finally { closeSafely(statement); closeSafely(rs); } } return lockAquired; } /** * This method is called to try and acquire the lock and/or update the state for when this instance * is already the master instance. It will try to update the row given the passed data and will * succeed if and only if the generated where clause was valid else it would not update the row. * * @param lockUpdateIdStatement The sql statement used to execute the update * * @return true, if the row was updated else false */ private boolean acquireLock(String lockUpdateIdStatement) { PreparedStatement preparedStatement = null; boolean lockAquired = false; try { preparedStatement = getConnection().prepareStatement(lockUpdateIdStatement); // This will only update the row that contains the ID of 0 or curId lockAquired = preparedStatement.executeUpdate() > 0; } catch (Exception e) { // Do we want to display this message everytime??? LOG.log(Level.WARNING, "Failed to acquire database lock", e); } finally { closeSafely(preparedStatement); } return lockAquired; } /** * This method will release the lock that the current master has by setting the karaf_lock table * id to 0. This tells the others that the master has relinquished the lock and someone else can * try to acquire that lock and become a master. * * @see org.apache.karaf.main.lock.Lock#release() */ public void release() throws Exception { if (isConnected()) { String lockResetIdStatement = statements.getLockResetIdStatement(uniqueId); PreparedStatement preparedStatement = null; try { preparedStatement = getConnection().prepareStatement(lockResetIdStatement); // This statement will set the ID to 0 and allow others to steal the lock... preparedStatement.executeUpdate(); } catch (SQLException e) { LOG.log(Level.SEVERE, "Exception while rollbacking the connection on release", e); } finally { closeSafely(preparedStatement); try { getConnection().close(); } catch (SQLException ignored) { LOG.log(Level.FINE, "Exception while closing connection on release", ignored); } } } lockConnection = null; } /** * This method will check if the jdbc connection is still active and if we were able to * acquire or update the karaf_table information. * * @return true, if the connection is still active and we still have the lock * * @see org.apache.karaf.main.lock.Lock#isAlive() * */ public boolean isAlive() throws Exception { if (!isConnected()) { LOG.severe("Lost lock!"); return false; } return lock(); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy