/********************************************************************************** * $URL: https://source.sakaiproject.org/svn/courier/trunk/courier-impl/impl/src/java/org/sakaiproject/courier/impl/BasicCourierService.java $ * $Id: BasicCourierService.java 131633 2013-11-15 21:36:42Z arwhyte@umich.edu $ *********************************************************************************** * * Copyright (c) 2005, 2006, 2007, 2008 The 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.courier.impl; import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Timer; import java.util.TimerTask; import java.util.Vector; import java.util.concurrent.ConcurrentHashMap; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.sakaiproject.component.cover.ComponentManager; import org.sakaiproject.component.cover.ServerConfigurationService; import org.sakaiproject.courier.api.CourierService; import org.sakaiproject.courier.api.Delivery; import org.sakaiproject.courier.api.DeliveryProvider; import org.sakaiproject.courier.api.Expirable; import org.sakaiproject.util.StringUtil; /** * <p> * BasicCourierService is the implementation for the CourierService. * </p> * * @deprecated The BasicCourierService has stability issues and will be removed * in a future release (after 10.0) * <a href="https://jira.sakaiproject.org/browse/SAK-22053">SAK-22053</a> * @see CourierService */ @Deprecated public class BasicCourierService implements CourierService { /** Our logger. */ private static final Log M_log = LogFactory.getLog(BasicCourierService.class); protected static final int nLocks = 100; /** Stores a List of Deliveries for each address, keyed by address. */ protected Map<String, List<Delivery>> m_addresses = new ConcurrentHashMap<String, List<Delivery>>(nLocks, 0.75f, nLocks); /** ** Array of objects for locking. You take the hash MOD the size of the ** array to find the element in the array to lock. This allows better ** concurrency than a global lock on the hash table. I still use ** ConcurrentHashMap because otherwise I worry about two attempts ** to add an entry that happen to use the same lock. So I'm ** depending upon ConcurrentHashMap to maintain the soundness of ** the data structure. Unfortunately the synchronization in it doesn't ** help against situations where I get a value and then test or ** modify it. That needs real locks. **/ protected Object[] locks; private List<DeliveryProvider> deliveryProviders = new Vector<DeliveryProvider>(); /** Configuration: how often to check for inactive deliveries (seconds). 0 means no checking*/ protected int m_checkEvery = 300; /** The maintenance. */ protected Maintenance m_maintenance = null; /** Maintenance Timer object */ protected Timer m_maintenanceTimer = new Timer(true); /** * Configuration: do maintenance cleanup aggressively * True indicates that the entire address will be removed from the m_addresses map, false means that only the expired delivery will be removed **/ protected boolean m_aggressiveCleanup = false; /** * Configuration: set how often to check for inactive deliveries (seconds). * * @param value * The how often to check for inactive deliveries (seconds) value. */ public void setCheckEvery(int value) { m_checkEvery = value; } /********************************************************************************************************************************************************************************************************************************************************** * Dependencies and their setter methods *********************************************************************************************************************************************************************************************************************************************************/ /********************************************************************************************************************************************************************************************************************************************************** * Init and Destroy *********************************************************************************************************************************************************************************************************************************************************/ /** * Final initialization, once all dependencies are set. */ public void init() { M_log.info("init()"); m_addresses.clear(); locks = new Object[nLocks]; for (int i = 0; i < nLocks; i++) locks[i] = new Object(); m_checkEvery = ServerConfigurationService.getInt("courier.maintThreadChecks", 300); m_aggressiveCleanup = ServerConfigurationService.getBoolean("courier.aggressiveCleanup", false); // start the maintenance thread if (m_checkEvery > 0) { m_maintenance = new Maintenance(); m_maintenanceTimer.schedule(m_maintenance, 0, (m_checkEvery * 1000) ); m_maintenance.start(); } } /** * Returns to uninitialized state. */ public void destroy() { M_log.info("destroy()"); m_addresses.clear(); locks = null; if (m_maintenance != null) { m_maintenance.stop(); m_maintenance = null; } } /********************************************************************************************************************************************************************************************************************************************************** * CourierService implementation *********************************************************************************************************************************************************************************************************************************************************/ protected int slot(String s) { int hash = Math.abs(s.hashCode()); return hash % nLocks; } /** * Queue up a delivery for the client window identified in the Delivery * object. The particular form of delivery is determined by the type of * Delivery object sent. * * @param delivery * The Delivery (or extension) object to deliver. */ public void deliver(Delivery delivery) { if (M_log.isDebugEnabled()) M_log.debug("deliver(Delivery " + delivery + ")"); final String address = delivery.getAddress(); synchronized(locks[slot(address)]) { // find the entry in m_addresses List<Delivery> deliveries = m_addresses.get(address); // create if needed if (deliveries == null) { deliveries = new ArrayList<Delivery>(); m_addresses.put(address, deliveries); } // if this doesn't exist in the list already, add it if (!deliveries.contains(delivery)) { deliveries.add(delivery); } } } /** * Clear any pending delivery requests to the particular client window for * this element. * * @param address * The address of the client window. * @param elementId * The id of the html element. */ public void clear(String address, String elementId) { if (M_log.isDebugEnabled()) M_log.debug("clear(String " + address + ", String " + elementId + ")"); synchronized(locks[slot(address)]) { // find the entry in m_addresses List<Delivery> deliveries = m_addresses.get(address); // if not there we are done if (deliveries == null) return; // remove any Deliveries with this elementId for (Iterator<Delivery> it = deliveries.iterator(); it.hasNext();) { Delivery delivery = it.next(); if (!StringUtil.different(delivery.getElement(), elementId)) { it.remove(); } } // if none left, remove it from the list if (deliveries.isEmpty()) { m_addresses.remove(address); } } } /** * Clear any pending delivery requests to this session client window. * * @param address * The address of client window. */ public void clear(String address) { if (M_log.isDebugEnabled()) M_log.debug("clear(String " + address + ")"); synchronized(locks[slot(address)]) { // remove this portal from m_addresses m_addresses.remove(address); } } /** * Access and de-queue the Deliveries queued up for a particular session client window. * * @param address * The address of client window. * @return a List of Delivery objects addressed to this session client window. */ @SuppressWarnings("unchecked") public List getDeliveries(String address) { if (M_log.isDebugEnabled()) M_log.debug("getDeliveries(String " + address + ")"); List<Delivery> deliveries; synchronized(locks[slot(address)]) { // find the entry in m_addresses deliveries = m_addresses.get(address); if (deliveries == null) { // if null, return something return Collections.EMPTY_LIST; // this should return null! } else { m_addresses.remove(address); } } // do this outside of the sync. no reason to hold // other operations now that we've atomically gotten the list // I'm worried about delays and maybe even deadlocks if we // do arbitrary code while holding a lock. // "act" all the deliveries for (Delivery delivery : deliveries) { delivery.act(); } return deliveries; } /** * Check to see if there are any deliveries queued up for a particular * session client window. * * @param address * The address of the client window. * @return true if there are deliveries for this client window, false if * not. * * WARNING: This method is almost certainly not what you want. * I can see no way to use it that won't have synchronization problems. */ public boolean hasDeliveries(String address) { if (M_log.isDebugEnabled()) M_log.debug("hasDeliveries(String " + address + ")"); List<Delivery> deliveries; // find the entry in m_addresses synchronized(locks[slot(address)]) { deliveries = m_addresses.get(address); } if (deliveries == null) return false; return (!deliveries.isEmpty()); } /* * (non-Javadoc) * @see org.sakaiproject.courier.api.CourierService#getDeliveryProviders() */ public List<DeliveryProvider> getDeliveryProviders() { if(M_log.isDebugEnabled()) { M_log.debug("getDeliveryProviders()"); } if(deliveryProviders.isEmpty()) { return null; } return new ArrayList<DeliveryProvider>(deliveryProviders ); } /********************************************************************************************************************************************************************************************************************************************************** * Maintenance *********************************************************************************************************************************************************************************************************************************************************/ protected class Maintenance extends TimerTask { /** 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() { M_log.info("Maintenance()"); } /** * Start the maintenance thread. */ public void start() { if (m_maintenanceChecker != null) return; m_maintenanceChecker = new Thread("Sakai.BasicCourierService.Maintenance"); m_maintenanceCheckerStop = false; m_maintenanceChecker.setDaemon(true); m_maintenanceChecker.start(); } /** * Stop the maintenance thread. */ 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; } } /** * Run the maintenance thread. Every m_checkEvery seconds, check for expired deliveries. */ public void run() { // since we might be running while the component manager is still being created and populated, such as at server // startup, wait here for a complete component manager ComponentManager.waitTillConfigured(); M_log.debug("Maintenance thread running..."); try { long now = System.currentTimeMillis(); for (List<Delivery> deliveries : m_addresses.values()) { for (Iterator<Delivery> iter = deliveries.iterator(); iter.hasNext();) { Delivery delivery = iter.next(); if (delivery instanceof Expirable) { Expirable expDelivery = (Expirable)delivery; long created = expDelivery.getCreated(); int ttl = expDelivery.getTtl(); //Don't need to worry about it if ttl is not set. if (ttl > 0) { long sDiff = (now - created) / 1000; // If the time since creation is larger than the ttl, remove it if (sDiff > ttl) { if (m_aggressiveCleanup) { M_log.debug("Removing all (" + deliveries.size() + ") expired deliveries for address: " + delivery.getAddress()); clear(delivery.getAddress()); break; } else { M_log.debug("Removing a single expired delivery for address: " + delivery.getAddress()); iter.remove(); } } } } } } } catch (Exception e) { M_log.warn("run(): exception: " + e); } } } /* * (non-Javadoc) * @see org.sakaiproject.courier.api.CourierService#registerDeliveryProvider(org.sakaiproject.courier.api.DeliveryProvider) */ public void registerDeliveryProvider(DeliveryProvider provider) { if(M_log.isDebugEnabled()) { M_log.debug("registerDeliveryProvider(DeliveryProvider " + provider + ")"); } this.deliveryProviders.add(provider); } }