/*************************************************************************
* Copyright 2009-2016 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.
*
* This file may incorporate work covered under the following copyright
* and permission notice:
*
* Software License Agreement (BSD License)
*
* Copyright (c) 2008, Regents of the University of California
* All rights reserved.
*
* Redistribution and use of this software in source and binary forms,
* with or without modification, are permitted provided that the
* following conditions are met:
*
* Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer
* in the documentation and/or other materials provided with the
* distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
* FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
* COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
* BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
* ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE. USERS OF THIS SOFTWARE ACKNOWLEDGE
* THE POSSIBLE PRESENCE OF OTHER OPEN SOURCE LICENSED MATERIAL,
* COPYRIGHTED MATERIAL OR PATENTED MATERIAL IN THIS SOFTWARE,
* AND IF ANY SUCH MATERIAL IS DISCOVERED THE PARTY DISCOVERING
* IT MAY INFORM DR. RICH WOLSKI AT THE UNIVERSITY OF CALIFORNIA,
* SANTA BARBARA WHO WILL THEN ASCERTAIN THE MOST APPROPRIATE REMEDY,
* WHICH IN THE REGENTS' DISCRETION MAY INCLUDE, WITHOUT LIMITATION,
* REPLACEMENT OF THE CODE SO IDENTIFIED, LICENSING OF THE CODE SO
* IDENTIFIED, OR WITHDRAWAL OF THE CODE CAPABILITY TO THE EXTENT
* NEEDED TO COMPLY WITH ANY SUCH LICENSES OR RIGHTS.
************************************************************************/
package com.eucalyptus.util.fsm;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicMarkableReference;
import java.util.concurrent.atomic.AtomicReference;
import org.apache.log4j.Logger;
import com.eucalyptus.records.EventRecord;
import com.eucalyptus.records.EventType;
import com.eucalyptus.records.Logs;
import com.eucalyptus.system.Threads;
import com.eucalyptus.util.Callback;
import com.eucalyptus.util.Exceptions;
import com.eucalyptus.util.HasFullName;
import com.eucalyptus.util.HasName;
import com.eucalyptus.util.async.CheckedListenableFuture;
import com.eucalyptus.util.async.Futures;
import com.eucalyptus.util.fsm.Automata.State;
import com.google.common.base.Supplier;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Multimap;
public class AtomicMarkedState<P extends HasName<P>, S extends Automata.State, T extends Automata.Transition> implements StateMachine<P, S, T> {
private static Logger LOG = Logger.getLogger( AtomicMarkedState.class );
private final P parent;
private final String name;
private final S startState;
private final ImmutableList<S> immutableStates;
private final Multimap<S, Callback<P>> inStateListeners = ArrayListMultimap.create( );
private final Multimap<S, Callback<P>> outStateListeners = ArrayListMultimap.create( );
private volatile ImmutableList<TransitionHandler<P, S, T>> immutableTransitions = null;
private final Multimap<T, TransitionHandler<P, S, T>> transitions = ArrayListMultimap.create( );
private final Map<S, Map<S, TransitionHandler<P, S, T>>> stateTransitions;
private final AtomicMarkableReference<S> state;
private static final AtomicLong id = new AtomicLong( 0l );
private final AtomicReference<ActiveTransition> currentTransition = new AtomicReference<ActiveTransition>( null );
public AtomicMarkedState( S startState, P parent, Set<TransitionHandler<P, S, T>> transitions, //
Multimap<S, Callback<P>> inStateListeners, Multimap<S, Callback<P>> outStateListeners ) {
this.startState = startState;
String tempName = null;
if ( parent instanceof HasFullName ) {
try {
tempName = ( ( HasFullName ) parent ).getFullName( ).toString( );
} catch ( Exception ex ) {}
}
this.name = ( tempName != null ? tempName : String.format( "%s-%s", parent.getClass( ).getSimpleName( ), parent.getName( ) ) );
this.parent = parent;
final S[] states = State.asEnum.getEnumConstants( startState );
this.stateTransitions = new HashMap<S, Map<S, TransitionHandler<P, S, T>>>( ) {
{
for ( S s : states ) {
this.put( s, new HashMap<S, TransitionHandler<P, S, T>>( ) );
}
}
};
this.immutableStates = ImmutableList.copyOf( states );
this.state = new AtomicMarkableReference<S>( this.startState, false );
this.immutableTransitions = ImmutableList.copyOf( transitions );
for ( TransitionHandler<P, S, T> t : transitions ) {
this.transitions.put( t.getName( ), t );
this.stateTransitions.get( t.getRule( ).getFromState( ) ).put( t.getRule( ).getToState( ), t );
}
this.inStateListeners.putAll( inStateListeners );
this.outStateListeners.putAll( outStateListeners );
}
@Override
public boolean isLegalTransition( T transitionName ) {
try {
this.lookupTransition( transitionName );
return true;
} catch ( NoSuchElementException ex ) {
return false;
}
}
/**
* TODO:GRZE: remove this in the future.
*/
@Deprecated
public CheckedListenableFuture<P> transitionByName( T transitionName ) throws IllegalStateException, ExistingTransitionException {
if ( this.state.isMarked( ) ) {
throw new ExistingTransitionException( "Transition request transition=" + transitionName
+ " rejected because of an ongoing transition: "
+ this.toString( ) );
} else if ( !this.transitions.containsKey( transitionName ) ) {
throw new NoSuchElementException( "No such transition named: " + transitionName.toString( )
+ ". Known transitions: "
+ this.getTransitions( ) );
} else {
this.checkTransition( transitionName );
final ActiveTransition tid = this.beforeLeave( transitionName );
CheckedListenableFuture<P> future = this.afterLeave( transitionName, tid );
return future;
}
}
@Override
public CheckedListenableFuture<P> transition( S nextState ) throws IllegalStateException, ExistingTransitionException {
if ( this.state.isMarked( ) ) {
throw new ExistingTransitionException( "Transition request state=" + nextState
+ " rejected because of an ongoing transition: "
+ this.toString( ) );
} else if ( !this.stateTransitions.get( this.state.getReference( ) ).containsKey( nextState ) ) {
throw new NoSuchElementException( "No transition to " + nextState.toString( )
+ " from current state "
+ this.toString( )
+ ". Known transitions: "
+ this.getTransitions( ) );
} else {
T transitionName = this.stateTransitions.get( this.state.getReference( ) ).get( nextState ).getName( );
this.checkTransition( transitionName );
final ActiveTransition tid = this.beforeLeave( transitionName );
return this.afterLeave( transitionName, tid );
}
}
/**
* @see com.eucalyptus.util.fsm.State#request(com.eucalyptus.util.fsm.TransitionRule)
* @param rule
* @return
* @throws ExistingTransitionException
*/
protected ActiveTransition request( T transitionName ) throws ExistingTransitionException {
TransitionHandler<P, S, T> transition = lookupTransition( transitionName );
TransitionRule<S, T> rule = transition.getRule( );
if ( !this.state.compareAndSet( rule.getFromState( ), rule.getFromState( ), rule.getFromStateMark( ), true ) ) {
throw new ExistingTransitionException( "Transition request " + transitionName
+ " rejected because of an ongoing transition: "
+ this.toString( ) );
} else {
this.currentTransition.set( this.create( rule, transition ) );
return this.currentTransition.get( );
}
}
private TransitionHandler<P, S, T> lookupTransition( T transitionName ) {
if ( !this.transitions.containsKey( transitionName ) ) {
throw new NoSuchElementException( "No such transition: " + transitionName );
}
S fromState = null;
boolean[] mark = new boolean[1];
for ( TransitionHandler<P, S, T> transition : this.transitions.get( transitionName ) ) {
if ( transition.getRule( ).getFromState( ).equals( fromState = this.state.get( mark ) ) && transition.getRule( ).getFromStateMark( ) == mark[0] ) {
return transition;
}
}
throw new NoSuchElementException( "No such transition: " + transitionName
+ " for the current state "
+ fromState
+ " and mark "
+ mark[0]
+ " for parent "
+ this.parent.toString( ) );
}
/**
* @see com.eucalyptus.util.fsm.State#commit()
*/
private void commit( ) {
Logs.exhaust( ).trace( "Transition commit(): " + this.currentTransition.get( ) );
if ( !this.state.isMarked( ) ) {
IllegalStateException ex = new IllegalStateException( "commit() called when there is no currently pending transition: " + this.toString( ) );
Logs.exhaust( ).error( ex, ex );
} else {
ActiveTransition tr = this.currentTransition.getAndSet( null );
this.state.set( tr.getTransitionRule( ).getToState( ), tr.getTransitionRule( ).getToStateMark( ) );
if ( !tr.getTransitionRule( ).getFromState( ).equals( tr.getTransitionRule( ).getToState( ) ) ) {
this.state.set( tr.getTransitionRule( ).getToState( ), false );
this.fireInListeners( tr.getTransitionRule( ).getToState( ) );
} else {
this.state.set( tr.getTransitionRule( ).getToState( ), false );
}
tr.getTransitionFuture( ).set( this.parent );
try {
EventRecord.caller( this.getClass( ), EventType.TRANSITION_FUTURE,
"set(" + this.parent.toString( )
+ ":"
+ this.parent.getClass( ).getCanonicalName( )
+ ")" ).trace( );
} catch ( Exception ex ) {
LOG.error( ex );
Logs.extreme( ).error( ex, ex );
}
}
}
private void error( Throwable t ) {
Logs.extreme( ).error( "Transition error(): " + this.toString( ), t );
if ( !this.state.isMarked( ) ) {
IllegalStateException ex = new IllegalStateException( "error() called when there is no currently pending transition: "
+ this.toString( ), t );
Logs.exhaust( ).error( ex );
ActiveTransition tr = this.currentTransition.getAndSet( null );
if ( tr != null ) {
tr.getTransitionFuture( ).setException( t );
this.state.set( tr.getTransitionRule( ).getErrorState( ), false );
}
} else {
ActiveTransition tr = this.currentTransition.getAndSet( null );
if ( tr != null ) {
Logs.extreme( ).error( "Transition error(): " + this.toString( ) + "Transition error(): START STACK\n" + tr.startStackTrace );
Logs.extreme( ).error( "Transition error(): " + this.toString( ) + "Transition error(): END STACK" + tr.endStackTrace.get( ) );
this.state.set( tr.getTransitionRule( ).getErrorState( ), tr.getTransitionRule( ).getErrorStateMark( ) );
if ( !tr.getTransitionRule( ).getFromState( ).equals( tr.getTransitionRule( ).getErrorState( ) ) ) {
this.state.set( tr.getTransitionRule( ).getErrorState( ), false );
this.fireInListeners( tr.getTransitionRule( ).getErrorState( ) );
} else {
this.state.set( tr.getTransitionRule( ).getErrorState( ), false );
}
EventRecord.caller( this.getClass( ), EventType.TRANSITION_FUTURE, "setException(" + t.getClass( ).getCanonicalName( )
+ "): "
+ t.getMessage( ) ).trace( );
tr.getTransitionFuture( ).setException( t );
}
}
}
private void rollback( Throwable t ) {
LOG.debug( "Transition rollback(): " + this.toString( ) );
if ( !this.state.isMarked( ) ) {
Exceptions.trace( new IllegalStateException( "rollback() called when there is no currently pending transition: " + this.toString( ) ) );
} else {
ActiveTransition tr = this.currentTransition.getAndSet( null );
if ( tr != null ) {
this.state.set( tr.getTransitionRule( ).getFromState( ), false );
}
}
}
protected void fireInListeners( S state ) {
for ( Callback<P> cb : AtomicMarkedState.this.inStateListeners.get( state ) ) {
try {
Logs.exhaust( ).debug( "Firing state-in listener: " + cb.getClass( )
+ " for "
+ this.toString( ) );
cb.fire( this.parent );
} catch ( Exception t ) {
Exceptions.trace( "Firing state-in listeners failed for :" + cb.getClass( ).getCanonicalName( ), Exceptions.filterStackTrace( t ) );
}
}
}
protected void fireOutListeners( S state ) {
for ( Callback<P> cb : AtomicMarkedState.this.outStateListeners.get( state ) ) {
try {
Logs.exhaust( ).debug( "Firing state-in listener: " + cb.getClass( )
+ " for "
+ this.toString( ) );
cb.fire( this.parent );
} catch ( Exception t ) {
Exceptions.trace( "Firing state-out listeners failed for :" + cb.getClass( ).getCanonicalName( ), Exceptions.filterStackTrace( t ) );
}
}
}
private final void checkTransition( final T transitionName ) {
try {
if ( !this.lookupTransition( transitionName ).before( this.parent ) ) {
throw Exceptions.trace( new IllegalStateException( String.format( "Failed to apply transition %s because before() returned false.",
transitionName.toString( ) ) ) );
}
} catch ( Exception t ) {
throw Exceptions.trace( new IllegalStateException( String.format( "Failed to apply transition %s because before() threw an exception: %s",
transitionName.toString( ), t.getMessage( ) ), t ) );
}
}
private final CheckedListenableFuture<P> afterLeave( final T transitionName, final ActiveTransition tid ) throws IllegalStateException {
try {
CheckedListenableFuture<P> result = tid.leave( );
try {
this.fireOutListeners( tid.getTransitionRule( ).getFromState( ) );
} catch ( Exception ex ) {
Logs.extreme( ).error( ex, ex );
}
return result;
} catch ( Exception t ) {
this.error( t );
throw Exceptions.trace( new IllegalStateException( String.format( "Failed to apply transition %s because leave() threw an exception: %s",
transitionName.toString( ), t.getMessage( ) ), t ) );
}
}
private final ActiveTransition beforeLeave( final T transitionName ) throws IllegalStateException, ExistingTransitionException {
final ActiveTransition tid;
try {
tid = this.request( transitionName );
} catch ( ExistingTransitionException t ) {
throw t;
} catch ( Exception t ) {
this.error( t );
throw Exceptions.trace( new IllegalStateException( String.format( "Failed to apply transition %s because request() threw an exception.",
transitionName.toString( ) ), t ) );
}
return tid;
}
/**
* TODO: DOCUMENT
*
* @see com.eucalyptus.util.fsm.StateMachine#getState()
* @return
*/
@Override
public S getState( ) {
return this.state.getReference( );
}
/**
* TODO: DOCUMENT
*
* @see com.eucalyptus.util.fsm.StateMachine#isBusy()
* @return
*/
@Override
public boolean isBusy( ) {
return this.state.isMarked( );
}
/**
* TODO: DOCUMENT
*
* @see com.eucalyptus.util.fsm.StateMachine#getStates()
* @return
*/
@Override
public ImmutableList<S> getStates( ) {
return this.immutableStates;
}
/**
* TODO: DOCUMENT
*
* @see com.eucalyptus.util.fsm.StateMachine#getTransitions()
* @return
*/
@Override
public ImmutableList<TransitionHandler<P, S, T>> getTransitions( ) {
return immutableTransitions;
}
/**
* @see java.lang.Object#toString()
* @return
*/
public String toString( ) {
ActiveTransition t = this.currentTransition.get( );
return String.format(
"State:name=%s:state=%s:mark=%s:transition=%s",
this.name,
this.state.getReference( ),
this.state.isMarked( ),
( t != null
? t.toString( )
: "idle" ) );
}
/**
* @return the name
*/
public String getName( ) {
return this.name;
}
private ActiveTransition create( TransitionRule<S, T> rule, TransitionAction<P> transition ) {
return new ActiveTransition( AtomicMarkedState.id.incrementAndGet( ), rule, transition );
}
private class ActiveTransition extends Callback.Completion implements HasName<ActiveTransition>, TransitionRecord {
private final Long txId;
private final String txName;
private final Long startTime;
private Long endTime = 0l;
private final TransitionAction<P> transition;
private final String startStackTrace;
private final Supplier<String> endStackTrace;
private final CheckedListenableFuture<P> transitionFuture = Futures.newGenericeFuture( );
private final TransitionRule<S, T> rule;
private ActiveTransition( Long id, TransitionRule<S, T> rule, TransitionAction<P> transition ) {
this.txId = id;
this.startTime = System.currentTimeMillis( );
this.endTime = 0l;
this.rule = rule;
this.transition = transition;
this.txName = AtomicMarkedState.this.getName( ) + " #" + this.txId + " " + this.rule.getName( );
if ( Logs.isExtrrreeeme( ) ) {
this.startStackTrace = Threads.currentStackRange( 0, 32 );
} else {
this.startStackTrace = Threads.currentStackFrame( 0 ).toString( );
}
this.endStackTrace = new Supplier<String>( ) {
@Override
public String get( ) {
if ( Logs.isExtrrreeeme( ) ) {
return Threads.currentStackRange( 0, 32 );
} else {
return Threads.currentStackFrame( 3 ).toString( );
}
}
};
}
CheckedListenableFuture<P> getTransitionFuture( ) {
return this.transitionFuture;
}
@Override
public boolean isDone( ) {
return this.transitionFuture.isDone( );
}
public void fire( ) {
try {
if ( !this.isDone( ) ) {
this.transition.enter( AtomicMarkedState.this.parent );
this.transition.after( AtomicMarkedState.this.parent );
try {
AtomicMarkedState.this.commit( );
} catch ( Exception ex ) {}
}
} catch ( Exception t ) {
this.fireException( t );
// } finally {
// this.transitionFuture.set( AtomicMarkedState.this.parent );
}
}
public void fireException( Throwable t ) {
try {
if ( Logs.isExtrrreeeme( ) ) {
Logs.extreme( ).error( this.startStackTrace );
Logs.extreme( ).error( this.endStackTrace.get( ) );
}
AtomicMarkedState.this.error( t );
} finally {
this.transitionFuture.setException( t );
}
}
public final Long getId( ) {
return this.txId;
}
public TransitionRule<S, T> getTransitionRule( ) {
return this.rule;
}
public CheckedListenableFuture<P> leave( ) {
try {
this.transition.leave( AtomicMarkedState.this.parent, this );
return this.transitionFuture;
} catch ( Exception ex ) {
this.transitionFuture.setException( ex );
return this.transitionFuture;
}
}
public String getName( ) {
return this.txName;
}
public int compareTo( ActiveTransition that ) {
return this.txId.compareTo( that.txId );
}
public String toString( ) {
StringBuilder sb = new StringBuilder( );
sb.append( EventType.TRANSITION ).append( " " ).append( this.txName ).append( " Active" ).append( this.transition != null
? this.transition.toString( )
: "null" ).append( " id=" ).append(
this.txId ).append( " startTime=" ).append( new Date( this.startTime ) );
Logs.exhaust( ).trace( sb.toString( ) );
return sb.toString( );
}
@Override
public Long getTxId( ) {
return this.txId;
}
@Override
public String getTxName( ) {
return this.txName;
}
public Long getStartTime( ) {
return this.startTime;
}
public Long getEndTime( ) {
return this.endTime;
}
@Override
public TransitionAction<P> getTransition( ) {
return this.transition;
}
public String getStartStackTrace( ) {
return this.startStackTrace;
}
public Supplier<String> getEndStackTrace( ) {
return this.endStackTrace;
}
@Override
public TransitionRule<S, T> getRule( ) {
return this.rule;
}
}
public P getParent( ) {
return this.parent;
}
public TransitionRecord getTransitionRecord( ) {
return this.currentTransition.get( );
}
}