Java tutorial
/* * Copyright 2011 Janrain, Inc. * * Licensed 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 com.janrain.backplane.server.config; import com.janrain.backplane.server.ApplicationException; import com.janrain.backplane.server.metrics.MetricMessage; import com.janrain.backplane.server.metrics.MetricsAccumulator; import com.janrain.crypto.HmacHashUtils; import com.janrain.message.AbstractMessage; import com.janrain.message.AbstractNamedMap; import com.janrain.message.NamedMap; import com.janrain.simpledb.SimpleDBException; import com.janrain.simpledb.SuperSimpleDB; import com.janrain.util.AwsUtility; import com.yammer.metrics.Metrics; import com.yammer.metrics.core.TimerMetric; import org.apache.commons.lang.StringUtils; import org.apache.log4j.Logger; import org.springframework.context.annotation.Scope; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; import javax.inject.Inject; import javax.naming.InitialContext; import javax.naming.NamingException; import java.io.IOException; import java.text.SimpleDateFormat; import java.util.*; import java.util.concurrent.Callable; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import static com.janrain.backplane.server.BackplaneMessage.Field.*; import static com.janrain.backplane.server.config.BusConfig.Field.*; /** * Holds configuration settings for the Backplane server * * @author Jason Cowley, Johnny Bufu */ @Scope(value = "singleton") public class BackplaneConfig { // - PUBLIC public enum BUS_PERMISSION { GETALL, POST, GETPAYLOAD, IDENTITY } public static final SimpleDateFormat ISO8601 = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'") { { setTimeZone(TimeZone.getTimeZone("GMT")); } }; public void checkAdminAuth(String user, String password) throws AuthException { checkAuth(getAdminAuthTableName(), user, password); } public void checkMetricAuth(String user, String password) throws AuthException { checkAuth(getMetricAuthTableName(), user, password); } public <T extends NamedMap> String getTableNameForType(Class<T> type) { return bpInstanceId + "_" + type.getSimpleName(); } public String getMetricsTableName() { return bpInstanceId + BP_METRICS_TABLE_SUFFIX; } public String getMessagesTableName() { return bpInstanceId + BP_MESSAGES_TABLE_SUFFIX; } public String getSamplesTableName() { return bpInstanceId + BP_SAMPLES_TABLE_SUFFIX; } /** * Retrieve a configuration entity by its name * * * @param entityName The entity name for the configuration * @return The entity configuration * @throws ApplicationException if no matching entity configuration is found */ public <T extends AbstractMessage> T getConfig(String entityName, Class<T> entityType) throws SimpleDBException { T config = superSimpleDb.retrieve(getTableNameForType(entityType), entityType, entityName); if (config == null) { throw new ApplicationException("Error looking up " + entityType.getSimpleName() + " " + entityName); } return config; } /** * @return the debugMode */ public boolean isDebugMode() throws SimpleDBException { return Boolean.valueOf(cachedGet(BpServerProperty.DEBUG_MODE)); } /** * @return the server default max message value per channel * @throws SimpleDBException */ public long getDefaultMaxMessageLimit() throws SimpleDBException { Long max = Long.valueOf(cachedGet(BpServerProperty.DEFAULT_MESSAGES_MAX)); return max == null ? BackplaneConfig.BP_MAX_MESSAGES_DEFAULT : max; } public Exception getDebugException(Exception e) { try { return isDebugMode() ? e : null; } catch (SimpleDBException sdbe) { logger.error("Error getting debug mode", sdbe); // shouldn't happen return e; } } public String getInstanceId() { return bpInstanceId; } public String getBuildVersion() { return buildProperties.getProperty(BUILD_VERSION_PROPERTY); } /** * Retrieve the server instance id Amazon assigned * @return */ public static String getEC2InstanceId() { return EC2InstanceId; } // - PACKAGE BackplaneConfig(String instanceId) { this.bpInstanceId = instanceId; } /** * Load system property * @param propParamName * @return */ static String getAwsProp(String propParamName) { String result = System.getProperty(propParamName); if (StringUtils.isBlank(result)) { throw new RuntimeException("Required system property configuration missing: " + propParamName); } return result; } // - PRIVATE private static final Logger logger = Logger.getLogger(BackplaneConfig.class); private static final String BUILD_PROPERTIES = "/build.properties"; private static final String BUILD_VERSION_PROPERTY = "build.version"; private static final Properties buildProperties = new Properties(); private static final String BP_SERVER_CONFIG_TABLE_SUFFIX = "_bpserverconfig"; private static final String BP_ADMIN_AUTH_TABLE_SUFFIX = "_Admin"; private static final String BP_CONFIG_ENTRY_NAME = "bpserverconfig"; private static final String BP_MESSAGES_TABLE_SUFFIX = "_messages"; private static final String BP_SAMPLES_TABLE_SUFFIX = "_samples"; private static final String BP_METRICS_TABLE_SUFFIX = "_metrics"; private static final String BP_METRIC_AUTH_TABLE_SUFFIX = "_bpMetricAuth"; private static final long BP_MAX_MESSAGES_DEFAULT = 100; private final String bpInstanceId; private ScheduledExecutorService cleanup; // Amazon specific instance-id value private static String EC2InstanceId = "n/a"; private final TimerMetric getMessagesTime = Metrics.newTimer(BackplaneConfig.class, "cleanup_messages_time", TimeUnit.MILLISECONDS, TimeUnit.MINUTES); private static enum BpServerProperty { DEBUG_MODE, CONFIG_CACHE_AGE_SECONDS, CLEANUP_INTERVAL_MINUTES, DEFAULT_MESSAGES_MAX } @SuppressWarnings({ "UnusedDeclaration" }) private BackplaneConfig() { this.bpInstanceId = getAwsProp(InitSystemProps.BP_AWS_INSTANCE_ID); this.EC2InstanceId = new AwsUtility().retrieveEC2InstanceId(); try { buildProperties.load(BackplaneConfig.class.getResourceAsStream(BUILD_PROPERTIES)); } catch (IOException e) { String err = "Error loading build properties from " + BUILD_PROPERTIES; logger.error(err, e); throw new RuntimeException(err, e); } logger.info("Configured Backplane Server instance: " + bpInstanceId); } private ScheduledExecutorService createCleanupTask() { long cleanupIntervalMinutes; logger.info("calling createCleanupTask()"); try { cleanupIntervalMinutes = Long.valueOf(cachedGet(BpServerProperty.CLEANUP_INTERVAL_MINUTES)); } catch (SimpleDBException e) { throw new RuntimeException("Error getting server property " + BpServerProperty.CLEANUP_INTERVAL_MINUTES, e); } ScheduledExecutorService cleanupTask = Executors.newScheduledThreadPool(1); cleanupTask.scheduleAtFixedRate(new Runnable() { @Override public void run() { compileMetrics(); try { getMessagesTime.time(new Callable<Object>() { @Override public Object call() throws Exception { deleteExpiredMessages(); return null; } }); } catch (Exception e) { logger.error("Error while cleaning up expired messages, " + e.getMessage(), e); } } }, cleanupIntervalMinutes, cleanupIntervalMinutes, TimeUnit.MINUTES); return cleanupTask; } @PostConstruct private void init() { this.cleanup = createCleanupTask(); } @PreDestroy private void cleanup() { try { this.cleanup.shutdown(); if (this.cleanup.awaitTermination(10, TimeUnit.SECONDS)) { logger.info("Background thread shutdown properly"); } else { this.cleanup.shutdownNow(); if (!this.cleanup.awaitTermination(10, TimeUnit.SECONDS)) { logger.error("Background thread did not terminate"); } } Metrics.defaultRegistry().threadPools().shutdownThreadPools(); } catch (InterruptedException e) { logger.error("cleanup() threw an exception", e); this.cleanup.shutdownNow(); Thread.currentThread().interrupt(); } } private void compileMetrics() { try { MetricMessage metric = metricAccumulator.prepareSummary(); logger.debug("Storing metrics for instance " + MetricsAccumulator.getInstanceUuid()); // Create the _metrics table if it doesn't already exist. This is a light-weight call. superSimpleDb.checkDomain(getMetricsTableName()); MetricMessage oldMetric = superSimpleDb.retrieveAndDelete(getMetricsTableName(), MetricMessage.class, metric.getIdValue()); superSimpleDb.store(getMetricsTableName(), MetricMessage.class, metric, true); } catch (Exception e) { logger.error("Error compiling metrics " + e.getMessage(), e); } } private void deleteExpiredMessages() { try { logger.info("Backplane message cleanup task started."); String messagesTable = getMessagesTableName(); for (BusConfig busConfig : superSimpleDb.retrieve(getTableNameForType(BusConfig.class), BusConfig.class)) { try { // non-sticky superSimpleDb.deleteWhere(messagesTable, getExpiredMessagesClause(busConfig.get(BUS_NAME), false, busConfig.get(RETENTION_TIME_SECONDS))); // sticky superSimpleDb.deleteWhere(messagesTable, getExpiredMessagesClause(busConfig.get(BUS_NAME), true, busConfig.get(RETENTION_STICKY_TIME_SECONDS))); } catch (SimpleDBException sdbe) { logger.error("Error cleaning up expired messages on bus " + busConfig.get(BUS_NAME) + ", " + sdbe.getMessage(), sdbe); } } try { // remove old metrics superSimpleDb.deleteWhere(getMetricsTableName(), getExpiredMetricClause()); } catch (SimpleDBException sdbe) { logger.error("Error while removing expired metrics, " + sdbe.getMessage(), sdbe); } } catch (Exception e) { // catch-all, else cleanup thread stops logger.error("Backplane messages cleanup task error: " + e.getMessage(), e); } finally { logger.info("Backplane messages cleanup task finished."); } } private String getExpiredMetricClause() { int interval = 0; try { interval = Integer.valueOf(cachedGet(BpServerProperty.CLEANUP_INTERVAL_MINUTES)); } catch (SimpleDBException e) { throw new RuntimeException("Error getting server property " + BpServerProperty.CLEANUP_INTERVAL_MINUTES, e); } Calendar now = Calendar.getInstance(); // Cleanup metrics that may be lingering due to a shutdown server instance now.roll(Calendar.MINUTE, -(interval + 2)); return "time < '" + ISO8601.format(now.getTime()) + "'"; } private String getExpiredMessagesClause(String busId, boolean sticky, String retentionTimeSeconds) { return BUS.getFieldName() + " = '" + busId + "' AND " + // "is (not) null" is low-performance on simpledb apparently // http://practicalcloudcomputing.com/post/722621724/simpledb-essentials-for-high-performance-users-part-2 STICKY.getFieldName() + " = '" + Boolean.toString(sticky) + "' AND " + ID.getFieldName() + " < '" + ISO8601.format(new Date(System.currentTimeMillis() - Long.valueOf(retentionTimeSeconds) * 1000)) + "'"; } @Inject @SuppressWarnings({ "UnusedDeclaration" }) private SuperSimpleDB superSimpleDb; @Inject private MetricsAccumulator metricAccumulator; private Pair<BpServerConfigMap, Long> bpServerConfigCache; private String cachedGet(BpServerProperty property) throws SimpleDBException { Pair<BpServerConfigMap, Long> result = bpServerConfigCache; Long maxCacheAge = getMaxCacheAge(); if (result == null || result.left == null || result.right == null || maxCacheAge == null || result.right + maxCacheAge < System.currentTimeMillis()) { synchronized (this) { result = bpServerConfigCache; if (result == null || result.left == null || result.right == null || maxCacheAge == null || result.right + maxCacheAge < System.currentTimeMillis()) { result = new Pair<BpServerConfigMap, Long>(superSimpleDb.retrieve(getBpServerConfigTableName(), BpServerConfigMap.class, BP_CONFIG_ENTRY_NAME), System.currentTimeMillis()); bpServerConfigCache = result; } } } return result.left == null ? null : result.left.get(property.name()); } private String getBpServerConfigTableName() { return bpInstanceId + BP_SERVER_CONFIG_TABLE_SUFFIX; } private String getAdminAuthTableName() { return bpInstanceId + BP_ADMIN_AUTH_TABLE_SUFFIX; } private String getMetricAuthTableName() { return bpInstanceId + BP_METRIC_AUTH_TABLE_SUFFIX; } private Long getMaxCacheAge() { return bpServerConfigCache != null && bpServerConfigCache.left != null ? Long.valueOf(bpServerConfigCache.left.get(BpServerProperty.CONFIG_CACHE_AGE_SECONDS.name())) : null; } public void checkAuth(String authTable, String user, String password) throws AuthException { try { User userEntry = superSimpleDb.retrieve(authTable, User.class, user); String authKey = userEntry == null ? null : userEntry.get(User.Field.PWDHASH); if (!HmacHashUtils.checkHmacHash(password, authKey)) { throw new AuthException("User " + user + " not authorized in " + authTable); } } catch (SimpleDBException e) { throw new AuthException("User " + user + " not authorized in " + authTable + " , " + e.getMessage(), e); } } public static class BpServerConfigMap extends AbstractNamedMap { @SuppressWarnings({ "UnusedDeclaration" }) // instantiation through reflection public BpServerConfigMap() { } @Override public void setName(String name) { } @Override public String getName() { return BP_CONFIG_ENTRY_NAME; } } private static class Pair<L, R> { public Pair(L left, R right) { this.left = left; this.right = right; } public final L left; public final R right; } }