/** * 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.flume.channel.jdbc.impl; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; import java.util.Enumeration; import java.util.HashMap; import java.util.Map; import java.util.Properties; import java.util.concurrent.atomic.AtomicLong; import javax.sql.DataSource; import org.apache.commons.dbcp.ConnectionFactory; import org.apache.commons.dbcp.DriverManagerConnectionFactory; import org.apache.commons.dbcp.PoolableConnectionFactory; import org.apache.commons.dbcp.PoolingDataSource; import org.apache.commons.pool.KeyedObjectPoolFactory; import org.apache.commons.pool.impl.GenericKeyedObjectPoolFactory; import org.apache.commons.pool.impl.GenericObjectPool; import org.apache.flume.Context; import org.apache.flume.Event; import org.apache.flume.channel.jdbc.ConfigurationConstants; import org.apache.flume.channel.jdbc.DatabaseType; import org.apache.flume.channel.jdbc.JdbcChannelException; import org.apache.flume.channel.jdbc.JdbcChannelProvider; import org.apache.flume.channel.jdbc.TransactionIsolation; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class JdbcChannelProviderImpl implements JdbcChannelProvider { private static final Logger LOGGER = LoggerFactory.getLogger(JdbcChannelProviderImpl.class); private static final String EMBEDDED_DERBY_DRIVER_CLASSNAME = "org.apache.derby.jdbc.EmbeddedDriver"; private static final String DEFAULT_DRIVER_CLASSNAME = EMBEDDED_DERBY_DRIVER_CLASSNAME; private static final String DEFAULT_USERNAME = "sa"; private static final String DEFAULT_PASSWORD = ""; private static final String DEFAULT_DBTYPE = "DERBY"; /** The connection pool. */ private GenericObjectPool connectionPool; /** The statement cache pool */ private KeyedObjectPoolFactory statementPool; /** The data source. */ private DataSource dataSource; /** The database type. */ private DatabaseType databaseType; /** The schema handler. */ private SchemaHandler schemaHandler; /** Transaction factory */ private JdbcTransactionFactory txFactory; /** Connection URL */ private String connectUrl; /** Driver Class Name */ private String driverClassName; /** Capacity Counter if one is needed */ private long maxCapacity = 0L; /** The current size of the channel. */ private AtomicLong currentSize = new AtomicLong(0L); @Override public void initialize(Context context) { LOGGER.debug("Initializing JDBC Channel provider"); initializeSystemProperties(context); initializeDataSource(context); initializeSchema(context); initializeChannelState(context); } private void initializeSystemProperties(Context context) { Map<String, String> sysProps = new HashMap<String, String>(); Map<String, String> sysPropsOld = context.getSubProperties( ConfigurationConstants.OLD_CONFIG_JDBC_SYSPROP_PREFIX); if (sysPropsOld.size() > 0) { LOGGER.warn("Long form configuration prefix \"" + ConfigurationConstants.OLD_CONFIG_JDBC_SYSPROP_PREFIX + "\" is deprecated. Please use the short form prefix \"" + ConfigurationConstants.CONFIG_JDBC_SYSPROP_PREFIX + "\" instead."); sysProps.putAll(sysPropsOld); } Map<String, String> sysPropsNew = context.getSubProperties( ConfigurationConstants.CONFIG_JDBC_SYSPROP_PREFIX); // Override the deprecated values with the non-deprecated if (sysPropsNew.size() > 0) { sysProps.putAll(sysPropsNew); } for (String key: sysProps.keySet()) { String value = sysProps.get(key); if (key != null && value != null) { System.setProperty(key, value); } } } private void initializeChannelState(Context context) { String maxCapacityStr = getConfigurationString(context, ConfigurationConstants.CONFIG_MAX_CAPACITY, ConfigurationConstants.OLD_CONFIG_MAX_CAPACITY, "0"); long maxCapacitySpecified = 0; try { maxCapacitySpecified = Long.parseLong(maxCapacityStr); } catch (NumberFormatException nfe) { LOGGER.warn("Invalid value specified for maximum channel capacity: " + maxCapacityStr, nfe); } if (maxCapacitySpecified > 0) { this.maxCapacity = maxCapacitySpecified; LOGGER.info("Maximum channel capacity: {}", maxCapacity); } else { LOGGER.warn("JDBC channel will operate without a capacity limit."); } if (maxCapacity > 0) { // Initialize current size JdbcTransactionImpl tx = null; try { tx = getTransaction(); tx.begin(); Connection conn = tx.getConnection(); currentSize.set(schemaHandler.getChannelSize(conn)); tx.commit(); } catch (Exception ex) { tx.rollback(); throw new JdbcChannelException("Failed to initialize current size", ex); } finally { if (tx != null) { tx.close(); } } long currentSizeLong = currentSize.get(); if (currentSizeLong > maxCapacity) { LOGGER.warn("The current size of channel (" + currentSizeLong + ") is more than the specified maximum capacity (" + maxCapacity + "). If this situation persists, it may require resizing and " + "replanning of your deployment."); } LOGGER.info("Current channel size: {}", currentSizeLong); } } private void initializeSchema(Context context) { String createSchemaFlag = getConfigurationString(context, ConfigurationConstants.CONFIG_CREATE_SCHEMA, ConfigurationConstants.OLD_CONFIG_CREATE_SCHEMA, "true"); boolean createSchema = Boolean.valueOf(createSchemaFlag); LOGGER.debug("Create schema flag set to: " + createSchema); // First check if the schema exists schemaHandler = SchemaHandlerFactory.getHandler(databaseType, dataSource); if (!schemaHandler.schemaExists()) { if (!createSchema) { throw new JdbcChannelException("Schema does not exist and " + "auto-generation is disabled. Please enable auto-generation of " + "schema and try again."); } String createIndexFlag = getConfigurationString(context, ConfigurationConstants.CONFIG_CREATE_INDEX, ConfigurationConstants.OLD_CONFIG_CREATE_INDEX, "true"); String createForeignKeysFlag = getConfigurationString(context, ConfigurationConstants.CONFIG_CREATE_FK, ConfigurationConstants.OLD_CONFIG_CREATE_FK, "true"); boolean createIndex = Boolean.valueOf(createIndexFlag); if (!createIndex) { LOGGER.warn("Index creation is disabled, indexes will not be created."); } boolean createForeignKeys = Boolean.valueOf(createForeignKeysFlag); if (createForeignKeys) { LOGGER.info("Foreign Key Constraints will be enabled."); } else { LOGGER.info("Foreign Key Constratins will be disabled."); } // Now create schema schemaHandler.createSchemaObjects(createForeignKeys, createIndex); } // Validate all schema objects are as expected schemaHandler.validateSchema(); } @Override public void close() { try { connectionPool.close(); } catch (Exception ex) { throw new JdbcChannelException("Unable to close connection pool", ex); } if (databaseType.equals(DatabaseType.DERBY) && driverClassName.equals(EMBEDDED_DERBY_DRIVER_CLASSNAME)) { // Need to shut down the embedded Derby instance if (connectUrl.startsWith("jdbc:derby:")) { int index = connectUrl.indexOf(";"); String baseUrl = null; if (index != -1) { baseUrl = connectUrl.substring(0, index + 1); } else { baseUrl = connectUrl + ";"; } String shutDownUrl = baseUrl + "shutdown=true"; LOGGER.debug("Attempting to shutdown embedded Derby using URL: " + shutDownUrl); try { DriverManager.getConnection(shutDownUrl); } catch (SQLException ex) { // Shutdown for one db instance is expected to raise SQL STATE 45000 if (ex.getErrorCode() != 45000) { throw new JdbcChannelException( "Unable to shutdown embedded Derby: " + shutDownUrl + " Error Code: " + ex.getErrorCode(), ex); } LOGGER.info("Embedded Derby shutdown raised SQL STATE " + "45000 as expected."); } } else { LOGGER.warn("Even though embedded Derby drvier was loaded, the connect " + "URL is of an unexpected form: " + connectUrl + ". Therfore no " + "attempt will be made to shutdown embedded Derby instance."); } } dataSource = null; txFactory = null; schemaHandler = null; } @Override public void persistEvent(String channel, Event event) { PersistableEvent persistableEvent = new PersistableEvent(channel, event); JdbcTransactionImpl tx = null; try { tx = getTransaction(); tx.begin(); if (maxCapacity > 0) { long currentSizeLong = currentSize.get(); if (currentSizeLong >= maxCapacity) { throw new JdbcChannelException("Channel capacity reached: " + "maxCapacity: " + maxCapacity + ", currentSize: " + currentSizeLong); } } // Persist the persistableEvent schemaHandler.storeEvent(persistableEvent, tx.getConnection()); tx.incrementPersistedEventCount(); tx.commit(); } catch (Exception ex) { tx.rollback(); throw new JdbcChannelException("Failed to persist event", ex); } finally { if (tx != null) { tx.close(); } } LOGGER.debug("Persisted event: {}", persistableEvent.getEventId()); } @Override public Event removeEvent(String channelName) { PersistableEvent result = null; JdbcTransactionImpl tx = null; try { tx = getTransaction(); tx.begin(); // Retrieve the persistableEvent result = schemaHandler.fetchAndDeleteEvent( channelName, tx.getConnection()); if (result != null) { tx.incrementRemovedEventCount(); } tx.commit(); } catch (Exception ex) { tx.rollback(); throw new JdbcChannelException("Failed to persist event", ex); } finally { if (tx != null) { tx.close(); } } if (result != null) { LOGGER.debug("Removed event: {}", ((PersistableEvent) result).getEventId()); } else { LOGGER.debug("No event found for removal"); } return result; } @Override public JdbcTransactionImpl getTransaction() { return txFactory.get(); } /** * Initializes the datasource and the underlying connection pool. * @param context */ private void initializeDataSource(Context context) { driverClassName = getConfigurationString(context, ConfigurationConstants.CONFIG_JDBC_DRIVER_CLASS, ConfigurationConstants.OLD_CONFIG_JDBC_DRIVER_CLASS, null); connectUrl = getConfigurationString(context, ConfigurationConstants.CONFIG_URL, ConfigurationConstants.OLD_CONFIG_URL, null); String userName = getConfigurationString(context, ConfigurationConstants.CONFIG_USERNAME, ConfigurationConstants.OLD_CONFIG_USERNAME, null); String password = getConfigurationString(context, ConfigurationConstants.CONFIG_PASSWORD, ConfigurationConstants.OLD_CONFIG_PASSWORD, null); String jdbcPropertiesFile = getConfigurationString(context, ConfigurationConstants.CONFIG_JDBC_PROPS_FILE, ConfigurationConstants.OLD_CONFIG_JDBC_PROPS_FILE, null); String dbTypeName = getConfigurationString(context, ConfigurationConstants.CONFIG_DATABASE_TYPE, ConfigurationConstants.OLD_CONFIG_DATABASE_TYPE, null); // If connect URL is not specified, use embedded Derby if (connectUrl == null || connectUrl.trim().length() == 0) { LOGGER.warn("No connection URL specified. " + "Using embedded derby database instance."); driverClassName = DEFAULT_DRIVER_CLASSNAME; userName = DEFAULT_USERNAME; password = DEFAULT_PASSWORD; dbTypeName = DEFAULT_DBTYPE; String homePath = System.getProperty("user.home").replace('\\', '/'); String defaultDbDir = homePath + "/.flume/jdbc-channel"; File dbDir = new File(defaultDbDir); String canonicalDbDirPath = null; try { canonicalDbDirPath = dbDir.getCanonicalPath(); } catch (IOException ex) { throw new JdbcChannelException("Unable to find canonical path of dir: " + defaultDbDir, ex); } if (!dbDir.exists()) { if (!dbDir.mkdirs()) { throw new JdbcChannelException("unable to create directory: " + canonicalDbDirPath); } } connectUrl = "jdbc:derby:" + canonicalDbDirPath + "/db;create=true"; // No jdbc properties file will be used jdbcPropertiesFile = null; LOGGER.warn("Overriding values for - driver: " + driverClassName + ", user: " + userName + "connectUrl: " + connectUrl + ", jdbc properties file: " + jdbcPropertiesFile + ", dbtype: " + dbTypeName); } // Right now only Derby and MySQL supported databaseType = DatabaseType.getByName(dbTypeName); switch (databaseType) { case DERBY: case MYSQL: break; default: throw new JdbcChannelException("Database " + databaseType + " not supported at this time"); } // Register driver if (driverClassName == null || driverClassName.trim().length() == 0) { throw new JdbcChannelException("No jdbc driver specified"); } try { Class.forName(driverClassName); } catch (ClassNotFoundException ex) { throw new JdbcChannelException("Unable to load driver: " + driverClassName, ex); } // JDBC Properties Properties jdbcProps = new Properties(); if (jdbcPropertiesFile != null && jdbcPropertiesFile.trim().length() > 0) { File jdbcPropsFile = new File(jdbcPropertiesFile.trim()); if (!jdbcPropsFile.exists()) { throw new JdbcChannelException("Jdbc properties file does not exist: " + jdbcPropertiesFile); } InputStream inStream = null; try { inStream = new FileInputStream(jdbcPropsFile); jdbcProps.load(inStream); } catch (IOException ex) { throw new JdbcChannelException("Unable to load jdbc properties " + "from file: " + jdbcPropertiesFile, ex); } finally { if (inStream != null) { try { inStream.close(); } catch (IOException ex) { LOGGER.error("Unable to close file: " + jdbcPropertiesFile, ex); } } } } if (userName != null) { Object oldUser = jdbcProps.put("user", userName); if (oldUser != null) { LOGGER.warn("Overriding user from: " + oldUser + " to: " + userName); } } if (password != null) { Object oldPass = jdbcProps.put("password", password); if (oldPass != null) { LOGGER.warn("Overriding password from the jdbc properties with " + " the one specified explicitly."); } } if (LOGGER.isDebugEnabled()) { StringBuilder sb = new StringBuilder("JDBC Properties {"); boolean first = true; Enumeration<?> propertyKeys = jdbcProps.propertyNames(); while (propertyKeys.hasMoreElements()) { if (first) { first = false; } else { sb.append(", "); } String key = (String) propertyKeys.nextElement(); sb.append(key).append("="); if (key.equalsIgnoreCase("password")) { sb.append("*******"); } else { sb.append(jdbcProps.get(key)); } } sb.append("}"); LOGGER.debug(sb.toString()); } // Transaction Isolation String txIsolation = getConfigurationString(context, ConfigurationConstants.CONFIG_TX_ISOLATION_LEVEL, ConfigurationConstants.OLD_CONFIG_TX_ISOLATION_LEVEL, TransactionIsolation.READ_COMMITTED.getName()); TransactionIsolation txIsolationLevel = TransactionIsolation.getByName(txIsolation); LOGGER.debug("Transaction isolation will be set to: " + txIsolationLevel); // Setup Datasource ConnectionFactory connFactory = new DriverManagerConnectionFactory(connectUrl, jdbcProps); connectionPool = new GenericObjectPool(); String maxActiveConnections = getConfigurationString(context, ConfigurationConstants.CONFIG_MAX_CONNECTIONS, ConfigurationConstants.OLD_CONFIG_MAX_CONNECTIONS, "10"); int maxActive = 10; if (maxActiveConnections != null && maxActiveConnections.length() > 0) { try { maxActive = Integer.parseInt(maxActiveConnections); } catch (NumberFormatException nfe) { LOGGER.warn("Max active connections has invalid value: " + maxActiveConnections + ", Using default: " + maxActive); } } LOGGER.debug("Max active connections for the pool: " + maxActive); connectionPool.setMaxActive(maxActive); statementPool = new GenericKeyedObjectPoolFactory(null); // Creating the factory instance automatically wires the connection pool new PoolableConnectionFactory(connFactory, connectionPool, statementPool, databaseType.getValidationQuery(), false, false, txIsolationLevel.getCode()); dataSource = new PoolingDataSource(connectionPool); txFactory = new JdbcTransactionFactory(dataSource, this); } /** * A callback method invoked from individual transaction instances after * a successful commit. The argument passed is the net number of events to * be added to the current size as tracked by the provider. * @param delta the net number of events to be added to reflect the current * size of the channel */ protected void updateCurrentChannelSize(long delta) { long currentSizeLong = currentSize.addAndGet(delta); LOGGER.debug("channel size updated to: " + currentSizeLong); } /** * Helper method to transition the configuration from the old long form * style configuration to the new short form. If the value is specified for * both the old and the new forms, the one associated with the new form * takes precedence. * * @param context * @param key the expected configuration key * @param oldKey the deprecated configuration key * @param defaultValue default value, null if no default * @return the value associated with the key */ private String getConfigurationString(Context context, String key, String oldKey, String defaultValue) { String oldValue = context.getString(oldKey); if (oldValue != null && oldValue.length() > 0) { LOGGER.warn("Long form configuration key \"" + oldKey + "\" is deprecated. Please use the short form key \"" + key + "\" instead."); } String value = context.getString(key); if (value == null) { if (oldValue != null) { value = oldValue; } else { value = defaultValue; } } return value; } }