/************************************************************************* * Copyright 2009-2014 Eucalyptus Systems, Inc. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see http://www.gnu.org/licenses/. * * Please contact Eucalyptus Systems, Inc., 6755 Hollister Ave., Goleta * CA 93117, USA or visit http://www.eucalyptus.com/licenses/ if you need * additional information or have any questions. ************************************************************************/ package com.eucalyptus.simpleworkflow.stateful; import java.util.List; import java.util.concurrent.BlockingQueue; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.Future; import java.util.concurrent.LinkedBlockingDeque; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicLong; import com.eucalyptus.simpleworkflow.common.stateful.PolledNotificationChecker; import com.eucalyptus.simpleworkflow.common.stateful.PolledNotifications; import org.apache.log4j.Logger; import org.jboss.netty.channel.local.LocalChannel; import com.eucalyptus.bootstrap.Bootstrap; import com.eucalyptus.component.Topology; import com.eucalyptus.component.annotation.ComponentNamed; import com.eucalyptus.context.Context; import com.eucalyptus.context.Contexts; import com.eucalyptus.context.NoSuchContextException; import com.eucalyptus.event.ClockTick; import com.eucalyptus.event.EventListener; import com.eucalyptus.event.Listeners; import com.eucalyptus.util.EucalyptusCloudException; import com.eucalyptus.util.async.CheckedListenableFuture; import com.eucalyptus.util.async.Futures; import com.google.common.base.Objects; import com.google.common.base.Predicate; import com.google.common.base.Predicates; import com.google.common.collect.Lists; /** * */ @ComponentNamed public class PolledNotificationService { private static final Logger logger = Logger.getLogger( PolledNotificationService.class ); private static final ConcurrentMap<String,Pollers> pollersByChannel = new ConcurrentHashMap<>( ); private static final ConcurrentMap<String,PendingNotification> pendingNotificationsByChannel = new ConcurrentHashMap<>( ); private static final PolledNotificationChecker checker = new PolledNotificationChecker(){ @Override public boolean apply( final String channel ) { return Predicates.or( PolledNotificationCheckerDiscovery.supplier( ).get( ) ).apply( channel ); } }; public NotifyResponseType submitNotify( final NotifyType notify ) throws EucalyptusCloudException { final NotifyResponseType response = notify.getReply( ); final Context context = Contexts.lookup( ); if ( context.hasAdministrativePrivileges( ) ) { if ( !notifyPollers( notify.getChannel( ), notify.getDetails( ) ) ) { pendingNotificationsByChannel.put( notify.getChannel( ), new PendingNotification( notify.getChannel( ), notify.getDetails() ) ); } } return response; } public PollForNotificationResponseType pollForNotification( final PollForNotificationType poll ) throws EucalyptusCloudException { final Context context = Contexts.lookup( ); if ( context.hasAdministrativePrivileges( ) ) { final Future<PollForNotificationResponseType> response = addPoller( poll.getChannel( ), new Poller( poll.getChannel( ), poll.getCorrelationId( ), poll.getTimeout() ) ); checkNotify( poll.getChannel( ) ); try { return response.get( ); } catch ( final Exception e ) { return poll.getReply( ); } } else { return poll.getReply( ); } } private static void checkNotify( final String channel ) { final PendingNotification pendingNotification = pendingNotificationsByChannel.remove( channel ); if ( pendingNotification != null && !pendingNotification.isExpired( System.currentTimeMillis( ) ) ) { notifyPollers( channel, pendingNotification.getDetails( ) ); } else if ( checker.apply( channel ) ) { notifyPollers( channel, null ); } } private static boolean notifyPollers( final String channel, final String details ) { return getPollers( channel ).notifyPollers( details ); } private static Future<PollForNotificationResponseType> addPoller( final String channel, final Poller poller ) { getPollers( channel ).addPoller( poller ); return poller.getFuture(); } private static Pollers getPollers( final String channel ) { Pollers pollers = pollersByChannel.get( channel ); if ( pollers != null ) { pollers.touch( ); // get again to ensure not evicted due to expiry before touched pollers = pollersByChannel.get( channel ); } if ( pollers == null ) { pollersByChannel.putIfAbsent( channel, new Pollers( channel ) ); pollers = pollersByChannel.get( channel ); } return pollers; } static void evacuate( ) { timeoutPollers( Long.MAX_VALUE ); } private static void periodicWork( ) { final long time = System.currentTimeMillis(); timeoutPollers( time ); timeoutPollerMetadata( time ); timeoutPendingNotifications( time ); } private static void timeoutPollers( final long time ) { for ( final Pollers pollers : pollersByChannel.values( ) ) { pollers.notifyExpiredPollers( time ); } } private static void timeoutPollerMetadata( final long time ) { for ( final Pollers pollers : pollersByChannel.values( ) ) { if ( pollers.isEmpty( ) && pollers.isExpired( time ) ) { pollersByChannel.remove( pollers.getChannel( ), pollers ); } } } private static void timeoutPendingNotifications( final long time ) { for ( final PendingNotification pendingNotification : pendingNotificationsByChannel.values( ) ) { if ( pendingNotification.isExpired( time ) ) { pendingNotificationsByChannel.remove( pendingNotification.getChannel( ), pendingNotification ); } } } private static final class Pollers { private static final long EXPIRY_MILLIS = TimeUnit.MINUTES.toMillis( 2 ); private final AtomicLong timestamp = new AtomicLong( System.currentTimeMillis( ) ); private final BlockingQueue<Poller> pollersQueue = new LinkedBlockingDeque<>(); private final String channel; private Pollers( final String channel ) { this.channel = channel; } public String getChannel( ) { return channel; } public void addPoller( final Poller poller ) { touch( ); pollersQueue.remove( poller ); pollersQueue.add( poller ); } public boolean notifyPollers( final String details ) { return notifyPollers( queuedPollers( ), new Predicate<PollForNotificationResponseType>() { @Override public boolean apply( final PollForNotificationResponseType response ) { response.setNotified( true ); response.setDetails( details ); return true; } } ); } public void notifyExpiredPollers( long time ) { notifyPollers( expiredPollers( time ), new Predicate<PollForNotificationResponseType>() { @Override public boolean apply( final PollForNotificationResponseType response ) { response.setNotified( false ); return true; } } ); } public boolean isEmpty( ) { return pollersQueue.isEmpty( ); } public boolean isExpired( final long time ) { return ( timestamp.get( ) + EXPIRY_MILLIS ) < time; } private boolean notifyPollers( final Iterable<Poller> pollers, final Predicate<PollForNotificationResponseType> responsePredicate ) { touch( ); boolean notified = false; for ( final Poller poller : pollers ) try { final PollForNotificationResponseType response = new PollForNotificationResponseType( ); response.setCorrelationId( poller.getCorrelationId( ) ); if ( responsePredicate.apply( response ) ) { poller.response( response ); notified = true; } } catch ( final Exception e ){ logger.error( "Error notifying poller " + poller, e ); } return notified; } private Iterable<Poller> queuedPollers( ) { final List<Poller> pollers = Lists.newArrayList( ); pollersQueue.drainTo( pollers ); return pollers; } private Iterable<Poller> expiredPollers( final long time ) { final List<Poller> pollers = Lists.newArrayList(); for ( final Poller poller : pollersQueue ) { if ( poller.isExpired( time ) && pollersQueue.remove( poller ) ) { pollers.add( poller ); } } return pollers; } private void touch( ) { timestamp.set( System.currentTimeMillis( ) ); } public String toString( ) { return Objects.toStringHelper( this ) .add( "channel", getChannel( ) ) .add( "pollers", pollersQueue ) .add( "timestamp", timestamp.get() ) .toString(); } } private static final class PendingNotification { private static final long EXPIRY_MILLIS = TimeUnit.MINUTES.toMillis( 1 ); private final long timestamp; private final String channel; private final String details; private PendingNotification( final String channel, final String details ) { this.timestamp = System.currentTimeMillis( ); this.channel = channel; this.details = details; } public long getTimestamp() { return timestamp; } public String getChannel() { return channel; } public String getDetails() { return details; } public boolean isExpired( final long time ) { return ( timestamp + EXPIRY_MILLIS ) < time; } public String toString( ) { return Objects.toStringHelper( this ) .add( "channel", getChannel() ) .add( "details", getDetails() ) .add( "timestamp", getTimestamp() ) .toString( ); } } private static final class Poller { private static final long EXPIRY_MILLIS = TimeUnit.SECONDS.toMillis( 30 ); private long timeout; private final String channel; private final String correlationId; private final CheckedListenableFuture<PollForNotificationResponseType> future; private Poller( final String channel, final String correlationId, Long timeout) { if (timeout == null) this.timeout = System.currentTimeMillis() + EXPIRY_MILLIS; else this.timeout = timeout; this.channel = channel; this.correlationId = correlationId; this.future = Futures.newGenericeFuture( ); try { //TODO:STEVE: clean up async for local transport if ( !(Contexts.lookup( correlationId ).getChannel( ) instanceof LocalChannel) ) { future.set( null ); } } catch ( NoSuchContextException e ) { // leave future unset } } public String getChannel() { return channel; } public String getCorrelationId() { return correlationId; } public Future<PollForNotificationResponseType> getFuture() { return future; } public void response( final PollForNotificationResponseType response ) { if ( !future.isDone( ) ) { future.set( response ); } else { Contexts.response( response ); } } public boolean isExpired( final long time ) { return timeout < time; } public String toString( ) { return Objects.toStringHelper( this ) .add( "channel", getChannel( ) ) .add( "correlationId", getCorrelationId( ) ) .toString(); } @Override public boolean equals(final Object other) { if (other == null) return false; if (!(other instanceof Poller)) return false; final Poller otherPoller = (Poller) other; if (this.getChannel()!= null) { if (! this.getChannel().equals(otherPoller.getChannel())) { return false; } } if (this.getCorrelationId()!=null) { if (! this.getCorrelationId().equals(otherPoller.getCorrelationId())) { return false; } } return true; } @Override public int hashCode() { final int prime = 31; int result = super.hashCode( ); result = prime * result + ( (this.channel == null ) ? 0 : this.channel.hashCode() ); result = prime * result + ( (this.correlationId == null) ? 0 : this.correlationId.hashCode() ); return result; } } public static class PollerClockTickEventListener implements EventListener<ClockTick> { public static void register( ) { Listeners.register( ClockTick.class, new PollerClockTickEventListener() ); } @Override public void fireEvent( final ClockTick event ) { if ( Bootstrap.isOperational( ) ) { if ( !Topology.isEnabledLocally( PolledNotifications.class ) ) { PolledNotificationService.evacuate(); } PolledNotificationService.periodicWork( ); } } } }