/********************************************************************************** * $URL: https://source.sakaiproject.org/svn/kernel/trunk/kernel-impl/src/main/java/org/sakaiproject/cluster/impl/SakaiClusterService.java $ * $Id: SakaiClusterService.java 105077 2012-02-24 22:54:29Z ottenhoff@longsight.com $ *********************************************************************************** * * Copyright (c) 2004, 2005, 2006, 2007, 2008 Sakai Foundation * * Licensed under the Educational Community 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.opensource.org/licenses/ECL-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.sakaiproject.cluster.impl; import java.util.Iterator; import java.util.List; import java.util.Map; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.sakaiproject.cluster.api.ClusterService; import org.sakaiproject.component.api.ServerConfigurationService; import org.sakaiproject.component.cover.ComponentManager; import org.sakaiproject.db.api.SqlService; import org.sakaiproject.event.api.EventTrackingService; import org.sakaiproject.event.api.UsageSessionService; import org.sakaiproject.thread_local.api.ThreadLocalManager; /** * <p> * SakaiClusterService is a Sakai cluster service implementation. * </p> */ public class SakaiClusterService implements ClusterService { /** Our log (commons). */ private static Log M_log = LogFactory.getLog(SakaiClusterService.class); /** The maintenance. */ protected Maintenance m_maintenance = null; /************************************************************************************************************************************************* * Dependencies and their setter methods ************************************************************************************************************************************************/ /** Dependency: ServerConfigurationService. */ protected ServerConfigurationService m_serverConfigurationService = null; /** * Dependency: ServerConfigurationService. * * @param service * The ServerConfigurationService. */ public void setServerConfigurationService(ServerConfigurationService service) { m_serverConfigurationService = service; } /** Dependency: EventTrackingService. */ protected EventTrackingService m_eventTrackingService = null; /** * Dependency: EventTrackingService. * * @param service * The EventTrackingService. */ public void setEventTrackingService(EventTrackingService service) { m_eventTrackingService = service; } /** Dependency: SqlService. */ protected SqlService m_sqlService = null; /** * Dependency: SqlService. * * @param service * The SqlService. */ public void setSqlService(SqlService service) { m_sqlService = service; } /** Dependency: UsageSessionService. */ protected UsageSessionService m_usageSessionService = null; /** * Dependency: UsageSessionService. * * @param service * The UsageSessionService. */ public void setUsageSessionService(UsageSessionService service) { m_usageSessionService = service; } /** Configuration: how often to register that we are alive with the cluster table (seconds). */ protected long m_refresh = 60; /** * Configuration: set the refresh value * * @param value * The refresh value. */ public void setRefresh(String value) { try { m_refresh = Long.parseLong(value); } catch (Exception ignore) { } } /** Configuration: how long we give an app server to respond before it is considered lost (seconds). */ protected long m_expired = 600; /** * Configuration: set the expired value * * @param value * The expired value. */ public void setExpired(String value) { try { m_expired = Long.parseLong(value); } catch (Exception ignore) { } } /** Configuration: to run the ddl on init or not. */ protected boolean m_autoDdl = false; /** * Configuration: to run the ddl on init or not. * * @param value * the auto ddl value. */ public void setAutoDdl(String value) { m_autoDdl = Boolean.valueOf(value).booleanValue(); } /** Dependency: the current manager. */ protected ThreadLocalManager m_threadLocalManager = null; /** * Dependency - set the current manager. * * @param value * The current manager. */ public void setThreadLocalManager(ThreadLocalManager manager) { m_threadLocalManager = manager; } /** Configuration: percent of maintenance passes to run the full de-ghosting / cleanup activities. */ protected int m_ghostingPercent = 100; /** * Configuration: set the percent of maintenance passes to run the full de-ghosting / cleanup activities * * @param value * The percent of maintenance passes to run the full de-ghosting / cleanup activities. */ public void setGhostingPercent(String value) { try { m_ghostingPercent = Integer.parseInt(value); } catch (Exception ignore) { } } public void setDatabaseBeans(Map databaseBeans) { this.databaseBeans = databaseBeans; } /** contains a map of the database dependent handlers. */ protected Map<String, ClusterServiceSql> databaseBeans; /** the handler we are using. */ protected ClusterServiceSql clusterServiceSql; public ClusterServiceSql getClusterServiceSql() { return clusterServiceSql; } /** * sets which bean containing database dependent code should be used depending on the database vendor. */ public void setClusterServiceSql(String vendor) { this.clusterServiceSql = (databaseBeans.containsKey(vendor) ? databaseBeans.get(vendor) : databaseBeans.get("default")); } /************************************************************************************************************************************************* * Init and Destroy ************************************************************************************************************************************************/ /** * Final initialization, once all dependencies are set. */ public void init() { setClusterServiceSql(m_sqlService.getVendor()); try { // if we are auto-creating our schema, check and create if (m_autoDdl) { m_sqlService.ddl(this.getClass().getClassLoader(), "sakai_cluster"); } // start the maintenance thread m_maintenance = new Maintenance(); m_maintenance.start(); M_log.info("init: refresh: " + m_refresh + " expired: " + m_expired + " ghostingPercent: " + m_ghostingPercent); } catch (Exception t) { M_log.warn("init(): ", t); } } /** * Returns to uninitialized state. */ public void destroy() { m_maintenance.stop(); m_maintenance = null; M_log.info("destroy()"); } /************************************************************************************************************************************************* * ClusterService implementation ************************************************************************************************************************************************/ @SuppressWarnings("unchecked") public List<String> getServers() { String statement = clusterServiceSql.getListServersSql(); List<String> servers = m_sqlService.dbRead(statement); return servers; } /************************************************************************************************************************************************* * Maintenance ************************************************************************************************************************************************/ protected class Maintenance implements Runnable { /** My thread running my timeout checker. */ protected Thread m_maintenanceChecker = null; /** Signal to the timeout checker to stop. */ protected boolean m_maintenanceCheckerStop = false; /** * Construct. */ public Maintenance() { } /** * Start the maintenance thread, registering this app server in the cluster table. */ public void start() { if (m_maintenanceChecker != null) return; // register in the cluster table String statement = clusterServiceSql.getInsertServerSql(); Object fields[] = new Object[1]; fields[0] = m_serverConfigurationService.getServerIdInstance(); boolean ok = m_sqlService.dbWrite(statement, fields); if (!ok) { M_log.warn("start(): dbWrite failed"); } m_maintenanceChecker = new Thread(this, "SakaiClusterService.Maintenance"); m_maintenanceChecker.setDaemon(true); m_maintenanceCheckerStop = false; m_maintenanceChecker.start(); } /** * Stop the maintenance thread, removing this app server's registration from the cluster table. */ public void stop() { if (m_maintenanceChecker != null) { m_maintenanceCheckerStop = true; m_maintenanceChecker.interrupt(); try { // wait for it to die m_maintenanceChecker.join(); } catch (InterruptedException ignore) { } m_maintenanceChecker = null; } // close our entry from the database - delete the record String statement = clusterServiceSql.getDeleteServerSql(); Object fields[] = new Object[1]; fields[0] = m_serverConfigurationService.getServerIdInstance(); boolean ok = m_sqlService.dbWrite(statement, fields); if (!ok) { M_log.warn("stop(): dbWrite failed: " + statement); } } /** * Run the maintenance thread. Every REFRESH seconds, re-register this app server as alive in the cluster. Then check for any cluster entries * that are more than EXPIRED seconds old, indicating a failed app server, and remove that record, that server's sessions, * generating appropriate session events so the other app servers know what's going on. The "then" checks need not be done each * iteration - run them on 1 of n randomly choosen iterations. In a clustered environment, this also distributes the work over the cluster * better. */ public void run() { // wait till things are rolling ComponentManager.waitTillConfigured(); if (M_log.isDebugEnabled()) M_log.debug("run()"); while (!m_maintenanceCheckerStop) { try { final String serverIdInstance = m_serverConfigurationService.getServerIdInstance(); if (M_log.isDebugEnabled()) M_log.debug("checking..."); // if we have been closed, reopen! String statement = clusterServiceSql.getReadServerSql(); Object[] fields = new Object[1]; fields[0] = serverIdInstance; List results = m_sqlService.dbRead(statement, fields, null); if (results.isEmpty()) { M_log.warn("run(): server has been closed in cluster table, reopened: " + serverIdInstance); statement = clusterServiceSql.getInsertServerSql(); fields[0] = serverIdInstance; boolean ok = m_sqlService.dbWrite(statement, fields); if (!ok) { M_log.warn("start(): dbWrite failed"); } } // update our alive and well status else { // register that this app server is alive and well statement = clusterServiceSql.getUpdateServerSql(); fields[0] = serverIdInstance; boolean ok = m_sqlService.dbWrite(statement, fields); if (!ok) { M_log.warn("run(): dbWrite failed: " + statement); } } // pick a random number, 0..99, to see if we want to do the full ghosting / cleanup activities now int rand = (int) (Math.random() * 100.0); if (rand < m_ghostingPercent) { // get all expired open app servers not me statement = clusterServiceSql.getListExpiredServers(m_expired); // setup the fields to skip reading me! fields[0] = serverIdInstance; List instances = m_sqlService.dbRead(statement, fields, null); // close any severs found to be expired for (Iterator iInstances = instances.iterator(); iInstances.hasNext();) { String serverId = (String) iInstances.next(); // close the server - delete the record statement = clusterServiceSql.getDeleteServerSql(); fields[0] = serverId; boolean ok = m_sqlService.dbWrite(statement, fields); if (!ok) { M_log.warn("run(): dbWrite failed: " + statement); } M_log.warn("run(): ghost-busting server: " + serverId + " from : " + serverIdInstance); } // Close all sessions left over from deleted servers. int nbrClosed = m_usageSessionService.closeSessionsOnInvalidServers(getServers()); if ((nbrClosed > 0) && M_log.isInfoEnabled()) M_log.info("Closed " + nbrClosed + " orphaned usage session records"); // Delete any orphaned locks from closed or missing sessions. statement = clusterServiceSql.getOrphanedLockSessionsSql(); List sessions = m_sqlService.dbRead(statement); if (sessions.size() > 0) { if (M_log.isInfoEnabled()) M_log.info("Found " + sessions.size() + " closed or deleted sessions in lock table"); statement = clusterServiceSql.getDeleteLocksSql(); for (Iterator iSessions = sessions.iterator(); iSessions.hasNext();) { fields[0] = (String) iSessions.next(); boolean ok = m_sqlService.dbWrite(statement, fields); if (!ok) { M_log.warn("run(): dbWrite failed: " + statement); } } } } } catch (Exception e) { M_log.warn("exception: ", e); } finally { // clear out any current access bindings m_threadLocalManager.clear(); } // cycle every REFRESH seconds if (!m_maintenanceCheckerStop) { try { Thread.sleep(m_refresh * 1000L); } catch (Exception ignore) { } } } if (M_log.isDebugEnabled()) M_log.debug("done"); } } }