/* * 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.sling.event.dea.impl; import java.util.Calendar; import java.util.Dictionary; import java.util.HashMap; import java.util.HashSet; import java.util.Hashtable; import java.util.Iterator; import java.util.Map; import java.util.Set; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.atomic.AtomicLong; import org.apache.sling.api.resource.LoginException; import org.apache.sling.api.resource.PersistenceException; import org.apache.sling.api.resource.Resource; import org.apache.sling.api.resource.ResourceResolver; import org.apache.sling.api.resource.ResourceResolverFactory; import org.apache.sling.api.resource.ResourceUtil; import org.apache.sling.discovery.InstanceDescription; import org.apache.sling.discovery.TopologyEvent; import org.apache.sling.discovery.TopologyEvent.Type; import org.apache.sling.discovery.TopologyEventListener; import org.apache.sling.event.dea.DEAConstants; import org.apache.sling.settings.SlingSettingsService; import org.osgi.framework.BundleContext; import org.osgi.framework.Constants; import org.osgi.framework.ServiceRegistration; import org.osgi.service.event.Event; import org.osgi.service.event.EventConstants; import org.osgi.service.event.EventHandler; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * This is the distributed event receiver. * It listens for all distributable events and stores them in the * repository for other cluster instance to pick them up. * <p> * This component is scheduled to run some clean up tasks in the * background periodically. * <p> */ public class DistributedEventReceiver implements EventHandler, Runnable, TopologyEventListener { /** Special topic to stop the queue. */ private static final String TOPIC_STOPPED = "org/apache/sling/event/dea/impl/STOPPED"; /** Logger */ private final Logger logger = LoggerFactory.getLogger(this.getClass()); /** A local queue for writing received events into the repository. */ private final BlockingQueue<Event> writeQueue = new LinkedBlockingQueue<Event>(); /** The resource resolver factory. */ private final ResourceResolverFactory resourceResolverFactory; /** The current instance id. */ private final String slingId; /** The root path for events . */ private final String rootPath; /** The root path for events written by this instance. */ private final String ownRootPath; /** The cleanup period. */ private final int cleanupPeriod; /** Resolver used for writing. */ private volatile ResourceResolver writerResolver; /** Is the background task still running? */ private volatile boolean running; /** The current instances if this is the leader. */ private volatile Set<String> instances; /** The service registration. */ private volatile ServiceRegistration<?> serviceRegistration; public DistributedEventReceiver(final BundleContext bundleContext, final String rootPath, final String ownRootPath, final int cleanupPeriod, final ResourceResolverFactory rrFactory, final SlingSettingsService settings) { this.rootPath = rootPath; this.ownRootPath = ownRootPath; this.resourceResolverFactory = rrFactory; this.slingId = settings.getSlingId(); this.cleanupPeriod = cleanupPeriod; this.running = true; // start writer thread final Thread writerThread = new Thread(new Runnable() { @Override public void run() { // create service registration properties final Dictionary<String, Object> props = new Hashtable<String, Object>(); props.put(Constants.SERVICE_VENDOR, "The Apache Software Foundation"); // listen for all OSGi events with the distributable flag props.put(EventConstants.EVENT_TOPIC, "*"); props.put(EventConstants.EVENT_FILTER, "(" + DEAConstants.PROPERTY_DISTRIBUTE + "=*)"); // schedule this service every 30 minutes props.put("scheduler.period", 1800L); props.put("scheduler.concurrent", Boolean.FALSE); final ServiceRegistration<?> reg = bundleContext.registerService(new String[] {EventHandler.class.getName(), Runnable.class.getName(), TopologyEventListener.class.getName()}, DistributedEventReceiver.this, props); DistributedEventReceiver.this.serviceRegistration = reg; try { writerResolver = resourceResolverFactory.getServiceResourceResolver(null); ResourceUtil.getOrCreateResource(writerResolver, ownRootPath, DistributedEventAdminImpl.RESOURCE_TYPE_FOLDER, DistributedEventAdminImpl.RESOURCE_TYPE_FOLDER, true); } catch (final Exception e) { // there is nothing we can do except log! logger.error("Error during resource resolver creation.", e); running = false; } try { processWriteQueue(); } catch (final Throwable t) { //NOSONAR logger.error("Writer thread stopped with exception: " + t.getMessage(), t); running = false; } if ( writerResolver != null ) { writerResolver.close(); writerResolver = null; } } }); writerThread.start(); } /** * Deactivate this component. */ public void stop() { if ( this.serviceRegistration != null ) { this.serviceRegistration.unregister(); this.serviceRegistration = null; } // stop background threads by putting empty objects into the queue this.running = false; try { this.writeQueue.put(new Event(TOPIC_STOPPED, (Dictionary<String, Object>)null)); } catch (final InterruptedException e) { this.ignoreException(e); Thread.currentThread().interrupt(); } } /** * Background thread writing events into the queue. */ private void processWriteQueue() { while ( this.running ) { // so let's wait/get the next event from the queue Event event = null; try { event = this.writeQueue.take(); } catch (final InterruptedException e) { this.ignoreException(e); Thread.currentThread().interrupt(); this.running = false; } if ( event != null && this.running ) { try { this.writeEvent(event); } catch (final Exception e) { this.logger.error("Exception during writing the event to the resource tree.", e); } } } } /** Counter for events. */ private final AtomicLong eventCounter = new AtomicLong(0); /** * Write an event to the resource tree. * @param event The event * @throws PersistenceException */ private void writeEvent(final Event event) throws PersistenceException { final Calendar now = Calendar.getInstance(); final StringBuilder sb = new StringBuilder(this.ownRootPath); sb.append('/'); sb.append(now.get(Calendar.YEAR)); sb.append('/'); sb.append(now.get(Calendar.MONTH) + 1); sb.append('/'); sb.append(now.get(Calendar.DAY_OF_MONTH)); sb.append('/'); sb.append(now.get(Calendar.HOUR_OF_DAY)); sb.append('/'); sb.append(now.get(Calendar.MINUTE)); sb.append('/'); sb.append("event-"); sb.append(String.valueOf(eventCounter.getAndIncrement())); // create properties final Map<String, Object> properties = new HashMap<String, Object>(); final String[] propNames = event.getPropertyNames(); if ( propNames != null && propNames.length > 0 ) { for(final String propName : propNames) { properties.put(propName, event.getProperty(propName)); } } properties.remove(DEAConstants.PROPERTY_DISTRIBUTE); properties.put(EventConstants.EVENT_TOPIC, event.getTopic()); properties.put(DEAConstants.PROPERTY_APPLICATION, this.slingId); final Object oldRT = properties.get(ResourceResolver.PROPERTY_RESOURCE_TYPE); if ( oldRT != null ) { properties.put("event.dea." + ResourceResolver.PROPERTY_RESOURCE_TYPE, oldRT); } properties.put(ResourceResolver.PROPERTY_RESOURCE_TYPE, DistributedEventAdminImpl.RESOURCE_TYPE_EVENT); ResourceUtil.getOrCreateResource(this.writerResolver, sb.toString(), properties, DistributedEventAdminImpl.RESOURCE_TYPE_FOLDER, true); } /** * @see org.osgi.service.event.EventHandler#handleEvent(org.osgi.service.event.Event) */ @Override public void handleEvent(final Event event) { try { this.writeQueue.put(event); } catch (final InterruptedException ex) { this.ignoreException(ex); Thread.currentThread().interrupt(); } } /** * Helper method which just logs the exception in debug mode. * @param e */ private void ignoreException(final Exception e) { if ( this.logger.isDebugEnabled() ) { this.logger.debug("Ignored exception " + e.getMessage(), e); } } /** * This method is invoked periodically. * @see java.lang.Runnable#run() */ @Override public void run() { this.cleanUpObsoleteInstances(); this.cleanUpObsoleteEvents(); } private void cleanUpObsoleteInstances() { final Set<String> slingIds = this.instances; if ( slingIds != null ) { this.instances = null; this.logger.debug("Checking for old instance trees for distributed events."); ResourceResolver resolver = null; try { resolver = this.resourceResolverFactory.getServiceResourceResolver(null); final Resource baseResource = resolver.getResource(this.rootPath); // sanity check - should never be null if ( baseResource != null ) { final ResourceHelper.BatchResourceRemover brr = ResourceHelper.getBatchResourceRemover(50); final Iterator<Resource> iter = baseResource.listChildren(); while ( iter.hasNext() ) { final Resource rootResource = iter.next(); if ( !slingIds.contains(rootResource.getName()) ) { brr.delete(rootResource); } } // final commit for outstanding deletes resolver.commit(); } } catch (final PersistenceException pe) { // in the case of an error, we just log this as a warning this.logger.warn("Exception during job resource tree cleanup.", pe); } catch (final LoginException ignore) { this.ignoreException(ignore); } finally { if ( resolver != null ) { resolver.close(); } } } } private void cleanUpObsoleteEvents() { if ( this.cleanupPeriod > 0 ) { this.logger.debug("Cleaning up distributed events, removing all entries older than {} minutes.", this.cleanupPeriod); ResourceResolver resolver = null; try { resolver = this.resourceResolverFactory.getServiceResourceResolver(null); final ResourceHelper.BatchResourceRemover brr = ResourceHelper.getBatchResourceRemover(50); final Resource baseResource = resolver.getResource(this.ownRootPath); // sanity check - should never be null if ( baseResource != null ) { final Calendar oldDate = Calendar.getInstance(); oldDate.add(Calendar.MINUTE, -1 * this.cleanupPeriod); // check years final int oldYear = oldDate.get(Calendar.YEAR); final Iterator<Resource> yearIter = baseResource.listChildren(); while ( yearIter.hasNext() ) { final Resource yearResource = yearIter.next(); final int year = Integer.valueOf(yearResource.getName()); if ( year < oldYear ) { brr.delete(yearResource); } else if ( year == oldYear ) { // same year - check months final int oldMonth = oldDate.get(Calendar.MONTH) + 1; final Iterator<Resource> monthIter = yearResource.listChildren(); while ( monthIter.hasNext() ) { final Resource monthResource = monthIter.next(); final int month = Integer.valueOf(monthResource.getName()); if ( month < oldMonth ) { brr.delete(monthResource); } else if ( month == oldMonth ) { // same month - check days final int oldDay = oldDate.get(Calendar.DAY_OF_MONTH); final Iterator<Resource> dayIter = monthResource.listChildren(); while ( dayIter.hasNext() ) { final Resource dayResource = dayIter.next(); final int day = Integer.valueOf(dayResource.getName()); if ( day < oldDay ) { brr.delete(dayResource); } else if ( day == oldDay ) { // same day - check hours final int oldHour = oldDate.get(Calendar.HOUR_OF_DAY); final Iterator<Resource> hourIter = dayResource.listChildren(); while ( hourIter.hasNext() ) { final Resource hourResource = hourIter.next(); final int hour = Integer.valueOf(hourResource.getName()); if ( hour < oldHour ) { brr.delete(hourResource); } else if ( hour == oldHour ) { // same hour - check minutes final int oldMinute = oldDate.get(Calendar.MINUTE); final Iterator<Resource> minuteIter = hourResource.listChildren(); while ( minuteIter.hasNext() ) { final Resource minuteResource = minuteIter.next(); final int minute = Integer.valueOf(minuteResource.getName()); if ( minute < oldMinute ) { brr.delete(minuteResource); } } } } } } } } } } } // final commit for outstanding resources resolver.commit(); } catch (final PersistenceException pe) { // in the case of an error, we just log this as a warning this.logger.warn("Exception during job resource tree cleanup.", pe); } catch (final LoginException ignore) { this.ignoreException(ignore); } finally { if ( resolver != null ) { resolver.close(); } } } } /** * @see org.apache.sling.discovery.TopologyEventListener#handleTopologyEvent(org.apache.sling.discovery.TopologyEvent) */ @Override public void handleTopologyEvent(final TopologyEvent event) { if ( event.getType() == Type.TOPOLOGY_CHANGING ) { this.instances = null; } else if ( event.getType() == Type.TOPOLOGY_CHANGED || event.getType() == Type.TOPOLOGY_INIT ) { if ( event.getNewView().getLocalInstance().isLeader() ) { final Set<String> set = new HashSet<String>(); for(final InstanceDescription desc : event.getNewView().getInstances() ) { set.add(desc.getSlingId()); } this.instances = set; } } } }