/**
* Copyright 2014 Comcast Cable Communications Management, LLC
*
* This file is part of CATS.
*
* CATS 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, either version 3 of the License, or
* (at your option) any later version.
*
* CATS 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 CATS. If not, see <http://www.gnu.org/licenses/>.
*/
package com.comcast.cats.provider;
import java.io.IOException;
import javax.annotation.PostConstruct;
import javax.inject.Inject;
import javax.inject.Named;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import com.comcast.cats.Settop;
import com.comcast.cats.domain.exception.AllocationException;
import com.comcast.cats.domain.exception.AllocationNotFoundException;
import com.comcast.cats.domain.exception.SettopNotFoundException;
import com.comcast.cats.domain.service.AllocationService;
import com.comcast.cats.event.AllocationEvent;
import com.comcast.cats.event.CatsEventDispatcher;
import com.comcast.cats.event.ReacquireEvent;
import com.comcast.cats.provider.exceptions.ExclusiveAccessException;
import com.comcast.cats.provider.impl.SettopExclusiveAccessToken;
/**
* This class provides the implementation specifics for managing settop locking
* including the background thread management for verifying settops are still
* usable. By reducing the service call frequency to every few minutes, a
* reduction in network bandwidth and load will result when the allocation
* service is bound to an external system.
*
* A threaded implementation for verifying and updating allocations was chosen
* to increase Settop operation performance by limiting the synchronous calls
* during lock checking.
*
* If a non threaded approach was used, at some point when a settop operation is
* invoked it would be necessary to verify the current state directly against
* the Allocation service. This would result in delay associated with potential
* network latency for verifying the current allocation for the given settop.
*
* @author cfrede001
* @since 2.0.0
*/
@Named
public class ExclusiveAccessManager extends SettopLockHandler implements ExclusiveAccessProvider, Runnable
{
private static Logger logger = LoggerFactory.getLogger( ExclusiveAccessManager.class );
/**
* Check the Allocation service every TIME_DELAY for each active allocation.
*/
public static final Long TIME_DELAY = 2L * 60L * 1000L;
public static final Integer RETRY_COUNT = 3;
private boolean breakAllocationCheck = true;
private final CatsEventDispatcher dispatcher;
private Object waitObject = new Object();
/**
* constructor
*
* @param allocationService
* {@linkplain AllocationService}
* @param authToken
* @param catsEventDispatcher
* {@linkplain CatsEventDispatcher}
*/
@Inject
public ExclusiveAccessManager( AllocationService allocationService, @Value( "#{catsProperties.authToken}" )
String authToken, CatsEventDispatcher catsEventDispatcher )
{
super( allocationService, authToken );
this.dispatcher = catsEventDispatcher;
logger.trace( "ExclusiveAccessManager Constructor" );
}
/**
* Checks to see if object being checked is a Settop or BaseProvider that
* contains a parent of Settop.
*
* @param obj
* - Object being checked.
* @return Settop object from class.
* @throws SettopNotFoundException
* - If settop object is unattainable by BaseProvider call.
*/
private Settop retrieveSettop( Object obj ) throws SettopNotFoundException
{
Settop settop = null;
/**
* We now need to determine if this is a Settop object through either
* the BaseProvider reference or if the action was performed directly on
* the Settop. Move from general Settop to more specific BaseProvider.
*/
if ( obj instanceof Settop )
{
logger.trace( "retrieveSettop object instanceof Settop" );
settop = ( Settop ) obj;
}
else if ( obj instanceof BaseProvider )
{
logger.trace( "retrieveSettop object instanceof BaseProvider" );
BaseProvider bp = ( BaseProvider ) obj;
if ( bp != null && ( bp.getParent() instanceof Settop ) )
{
logger.trace( "Object is BaseProvider has a parent of Settop" );
settop = ( Settop ) bp.getParent();
}
else
{
throw new SettopNotFoundException( "Settop Not Found" );
}
}
else
{
logger.trace( "Object instanceof " + obj.getClass() );
}
return settop;
}
/**
* {@inheritDoc}
*/
@Override
public boolean checkExclusiveAccess( Object obj ) throws ExclusiveAccessException
{
Settop settop;
try
{
settop = retrieveSettop( obj );
}
catch ( SettopNotFoundException snfe )
{
logger.error( snfe.getMessage() );
throw new ExclusiveAccessException( snfe.getMessage() );
}
logger.trace( "Check to settop lock status." );
if ( !isLocked( settop ) )
{
logger.trace( "SettopToken not found" );
throw new ExclusiveAccessException(
"SettopToken not found. The lock(allocation) on this settop is no more valid" );
}
return true;
}
/**
* post constructor
*/
@PostConstruct
public void construct()
{
logger.info( "Starting SettopExclusiveAccessEnforcer implementation Thread." );
Thread exclusiveAccessManagerThread = new Thread( this );
exclusiveAccessManagerThread.setDaemon( true );
exclusiveAccessManagerThread.start();
}
/**
* Background thread that will run forever keeping tabs on any allocations
* against the AllocationService.
*/
@Override
public void run()
{
while ( breakAllocationCheck )
{
try
{
logger.info( "Checking existing Allocations : " + allocations.values() );
for ( SettopExclusiveAccessToken token : allocations.values() )
{
try
{
logger.info( "Checking existing Allocations for token : " + token );
boolean isValidAllocation = verifyAllocation( token );
if ( !isValidAllocation )
{
cleanupExpiredToken( token, true );
}
}
catch ( IOException ioException )
{
logger.error( "Verify call on allocation list failed with:" + ioException.getMessage() );
handleTokenRetry( token );
}
catch ( ExclusiveAccessException eae )
{
logger.error( "Got ExclusiveAccessException for :" + token.getSettopId() + " retry:"
+ token.getRetries() );
handleTokenRetry( token );
}
}
synchronized ( waitObject )
{
// If the nofication happens during the above for loop, adding the below check to avoid undesirable wait of 'TIME_DELAY'
if(breakAllocationCheck)
{
waitObject.wait( TIME_DELAY );
}
}
}
catch ( Exception e )
{
logger.error( "Thread.sleep exception caught", e );
}
}
}
private void handleTokenRetry( SettopExclusiveAccessToken token )
{
if ( hasRetryExpired( token ) )
{
cleanupExpiredToken( token, false );
}
}
private void cleanupExpiredToken( SettopExclusiveAccessToken token, boolean isReleaseConfirmed )
{
logger.error( "Going to cleanup allocation for :" + token.getSettop() );
token.getSettop().setLocked( false );
allocations.remove( token.getSettopId() );
/**
* If cleanup is happening as a result of expired retires, we are not
* sure if the lock has actually been released . To avoid
* zombie locks, we are doing a last try to release the
* lock.This is done for acquired locks alone.
*/
if ( !token.isReaquire() && !isReleaseConfirmed )
{
try
{
logger.error( "Retry expired,going to call release on [" + token + "]" );
release( token.getSettop() );
}
catch ( AllocationException e )
{
logger.error( "Release call after retry expired failed for[" + token + "] got exception" + e );
}
}
// Don't forget to send the Allocation break event.
sendAllocationBreakEvent( token );
}
/**
* Check to see if we've reached the retry count. Otherwise update the retry
* count.
*
* @param token
* @return
*/
private boolean hasRetryExpired( SettopExclusiveAccessToken token )
{
if ( token.getRetries() > RETRY_COUNT )
{
return true;
}
else
{
token.incrementRetries();
}
return false;
}
/**
* Stops the Break Allocation Monitoring Thread.
*/
public void killBreakAllocationThread()
{
logger.info( "Break Allocation Monitor Thread Flag Set to false" );
breakAllocationCheck = false;
synchronized ( waitObject )
{
waitObject.notifyAll();
}
}
private void sendAllocationBreakEvent( SettopExclusiveAccessToken token )
{
AllocationEvent ae = null;
// TODO - CEF Comment: I'm not sure why it is necessary to define a new
// class for Reaquire events. This is still an allocation.
// Boby: The re acquire breakage has to be differentiated so that we can
// show appropriate message in CATS vision.
if ( token.isReaquire() )
{
ae = new ReacquireEvent( token.getSettopId(), token.getAllocationId(), true, this );
}
else
{
ae = new AllocationEvent( token.getSettopId(), token.getAllocationId(), true, this );
}
if ( null != ae )
{
dispatcher.sendCatsEvent( ae );
}
}
/**
* Verify the allocation against the AllocationService if the time is right.
* TODO - A background thread responsible for updating the allocation
* information and verifying the allocation would be preferred. Then utilize
* this data to allow or deny operations.
*
* @param token
* {@linkplain SettopExclusiveAccessToken}
* @throws {@link ExclusiveAccessException}
* @throws IOException
*/
protected boolean verifyAllocation( SettopExclusiveAccessToken token ) throws ExclusiveAccessException, IOException
{
// Initial assumption is that the allocation is valid.
// So if we cant connect to the server, we wont be breaking any
// allocations.
boolean isvalid = true;
try
{
if ( !token.isReaquire() )
{
logger.trace( "Attempting to update allocation information for ExclusiveAccessToken: "
+ token.toString() );
allocationService.update( token.getAllocationId() );
}
else
{
logger.trace( "Settop was reaquired,doing a verify call for ExclusiveAccessToken:" + token.toString() );
isvalid = allocationService.verify( token.getAllocationId() );
}
token.clearRetries();
token.update();
}
catch ( AllocationNotFoundException e )
{
// An Allocation not found exception means we invalidate the settop.
isvalid = false;
}// Catch All other exceptions to be on the safe side.
catch ( Exception e )
{
// TODO - CEF comment: This feels very hacky to me...-Boby: will
// revisit once i get this working
if ( e.getMessage().contains( "I/O error" ) )
{
throw new IOException( e.getMessage() );
}
else
{
logger.error( "AllocationException caught", e );
token.clearRetries();
throw new ExclusiveAccessException( "Settop[" + token.getSettopId()
+ "] failed exclusive access checking" );
}
}
return isvalid;
}
}