Game updates improvements

* Added a set of tables which define game updates and their targets.
These definitions replace the old enumerate type. Added a set of
triggers which automatically create specific update tables, insert
missing entries, etc... when game update types are being manipulated.

* Removed manual insertion of game updates from empire creation
function and universe generator.

* Added registration of core update targets (i.e. planets and empires),
updated all existing game update processing functions and added type
registrations

* Created Maven project for game updates control components, moved
existing components from the -simple project, rewritten most of what
they contained, added new components for server-side update batch
processing
This commit is contained in:
Emmanuel BENOîT 2012-02-03 16:25:03 +01:00
parent ba6a1e2b41
commit 56eddcc4f0
93 changed files with 4004 additions and 578 deletions

View file

@ -0,0 +1,238 @@
package com.deepclone.lw.beans.updates;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.support.TransactionTemplate;
import com.deepclone.lw.cmd.admin.logs.LogLevel;
import com.deepclone.lw.interfaces.eventlog.Logger;
import com.deepclone.lw.interfaces.eventlog.SystemLogger;
import com.deepclone.lw.interfaces.game.updates.GameUpdateProcessor;
import com.deepclone.lw.interfaces.game.updates.UpdatesDAO;
import com.deepclone.lw.interfaces.sys.MaintenanceStatusException;
import com.deepclone.lw.interfaces.sys.SystemStatus;
import com.deepclone.lw.interfaces.sys.TickStatusException;
/**
* Game update processor component
*
* <p>
* This component implements game updates processing. It will update the system status as necessary,
* checking for maintenance mode, and of course implement the necessary operations for game updates.
*
* @author <a href="mailto:tseeker@legacyworlds.com">E. Benoît</a>
*/
class GameUpdateProcessorBean
implements GameUpdateProcessor
{
/** System state management component */
private SystemStatus systemStatus;
/** Logger used by this component */
private SystemLogger logger;
/** Transaction template used to run update batches */
private TransactionTemplate tTemplate;
/** Update processing database interface */
private UpdatesDAO updatesDao;
/** Server-level update processors registry */
private ServerProcessorRegistry registry;
/** Game update processor lock */
private boolean executing;
/**
* Dependency injector that sets the sytstem state management component
*
* @param systemStatus
* the sytstem state management component
*/
@Autowired( required = true )
public void setSystemStatus( SystemStatus systemStatus )
{
this.systemStatus = systemStatus;
}
/**
* Dependency injector that initialises the logger from the logging component
*
* @param logger
* the logging component
*/
@Autowired( required = true )
public void setLogger( Logger logger )
{
this.logger = logger.getSystemLogger( "GameUpdate" );
}
/**
* Dependency injector that initialises the transaction template from the transaction manager
*
* @param transactionManager
* the transaction manager
*/
@Autowired( required = true )
public void setTransactionManager( PlatformTransactionManager transactionManager )
{
this.tTemplate = new TransactionTemplate( transactionManager );
}
/**
* Dependency injector that sets the database access component for game updates
*
* @param updatesDao
* the database access component for game updates
*/
@Autowired( required = true )
public void setUpdatesDAO( UpdatesDAO updatesDao )
{
this.updatesDao = updatesDao;
}
/**
* Dependency injector that sets the processor registry
*
* @param registry
* the processor registry
*/
@Autowired( required = true )
public void setRegistry( ServerProcessorRegistry registry )
{
this.registry = registry;
}
/**
* Try and lock the processor
*
* <p>
* Test the {@link #executing} field, setting it to <code>true</code> and returning
* <code>true</code> if it wasn't or returning <code>false</code> if it was.
*/
@Override
public synchronized boolean tryLock( )
{
if ( this.executing ) {
return false;
}
this.executing = true;
return true;
}
/**
* Unlock the processor
*
* <p>
* Set {@link #executing} back to <code>false</code>. If it was already set to
* <code>false</code>, throw an {@link IllegalStateException}.
*/
@Override
public synchronized void unlock( )
throws IllegalStateException
{
if ( !this.executing ) {
throw new IllegalStateException( "Trying to unlock game update processor, but it isn't locked" );
}
this.executing = false;
}
/**
* Check for an unfinished update cycle and terminate it
*
* <p>
* Check for a "stuck" update cycle. If there is one, issue a warning and process it through the
* {@link #executeTick(long)} method.
*/
@Override
public boolean endPreviousCycle( )
{
Long currentTick;
try {
currentTick = this.systemStatus.checkStuckTick( );
} catch ( MaintenanceStatusException e ) {
return true;
}
if ( currentTick == null ) {
return false;
}
this.logger.log( LogLevel.WARNING , "Tick " + currentTick + " restarted" ).flush( );
this.executeTick( currentTick );
return true;
}
/**
* Execute an update cycle
*
* <p>
* Start by checking for, and executing if necessary, a "stuck" update cycle. If one is found,
* return. Otherwise, start a new tick through the system state manager, and call
* {@link #executeTick(long)} to process it.
*
* <p>
* If maintenance mode is active, the new tick will not be started.
*/
@Override
public void executeUpdateCycle( )
{
// Attempt to end the previous tick, if e.g. maintenance mode was initiated while it was
// being processed
if ( this.endPreviousCycle( ) ) {
return;
}
// Initiate next tick
long tickId;
try {
tickId = this.systemStatus.startTick( );
} catch ( TickStatusException e ) {
throw new RuntimeException( "tick initiated while previous tick still being processed" , e );
} catch ( MaintenanceStatusException e ) {
return;
}
// Execute tick
this.logger.log( LogLevel.DEBUG , "Tick " + tickId + " started" ).flush( );
this.executeTick( tickId );
}
/**
* Process all remaining update batches for an update cycle
*
* <p>
* This method will try and process all update batches using a {@link GameUpdateTransaction}
* instance. It loops until the transaction indicates that no further updates are to be
* processed. However, the loop will exit if maintenance mode becomes active between update
* batches.
*
* @param tickId
* the game update cycle's identifier
*/
private void executeTick( final long tickId )
{
GameUpdateTransaction transaction = new GameUpdateTransaction( this.updatesDao , this.registry , tickId );
while ( !this.tTemplate.execute( transaction ) ) {
if ( this.systemStatus.checkMaintenance( ) != null ) {
this.logger.log( LogLevel.INFO , "Tick " + tickId + " interrupted by system maintenance" ).flush( );
return;
}
}
this.logger.log( LogLevel.TRACE , "Tick " + tickId + " completed" ).flush( );
}
}

View file

@ -0,0 +1,96 @@
package com.deepclone.lw.beans.updates;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import com.deepclone.lw.interfaces.game.updates.GameUpdateProcessor;
import com.deepclone.lw.interfaces.sys.Ticker;
import com.deepclone.lw.interfaces.sys.Ticker.Frequency;
/**
* Game update ticker task
*
* <p>
* This component will register itself as a ticker task to be executed once per minute. When it
* runs, it will try and execute an update cycle.
*
* @author <a href="mailto:tseeker@legacyworlds.com">E. Benoît</a>
*/
class GameUpdateTaskBean
implements InitializingBean , Runnable
{
/** The ticker component */
private Ticker ticker;
/** The game updates processor */
private GameUpdateProcessor gameUpdateProcessor;
/**
* Dependency injector that sets the ticker component
*
* @param ticker
* the ticker component
*/
@Autowired( required = true )
public void setTicker( Ticker ticker )
{
this.ticker = ticker;
}
/**
* Dependency injector that sets the game update processor
*
* @param gameUpdateProcessor
* the game update processor
*/
@Autowired( required = true )
public void setGameUpdateProcessor( GameUpdateProcessor gameUpdateProcessor )
{
this.gameUpdateProcessor = gameUpdateProcessor;
}
/**
* Finish a previous update cycle if there is one, then register the component to the ticker
*/
@Override
public void afterPropertiesSet( )
{
if ( this.gameUpdateProcessor.tryLock( ) ) {
try {
this.gameUpdateProcessor.endPreviousCycle( );
} finally {
this.gameUpdateProcessor.unlock( );
}
}
this.ticker.registerTask( Frequency.MINUTE , "Game update" , this );
}
/**
* Run a game update cycle
*
* <p>
* Try locking the game update processor. On success, execute an update cycle, then unlock the
* processor.
*/
@Override
public void run( )
{
if ( !this.gameUpdateProcessor.tryLock( ) ) {
return;
}
try {
this.gameUpdateProcessor.executeUpdateCycle( );
} finally {
this.gameUpdateProcessor.unlock( );
}
}
}

View file

@ -0,0 +1,89 @@
package com.deepclone.lw.beans.updates;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionCallback;
import com.deepclone.lw.interfaces.game.updates.UpdateBatchProcessor;
import com.deepclone.lw.interfaces.game.updates.UpdatesDAO;
import com.deepclone.lw.sqld.sys.GameUpdateResult;
/**
* Game update transaction
*
* <p>
* This class is responsible for executing the necessary operations during a game update
* transaction. It is used by the {@link GameUpdateProcessorBean} component to implement its main
* processing loop.
*
* @author <a href="mailto:tseeker@legacyworlds.com">E. Benoît</a>
*/
class GameUpdateTransaction
implements TransactionCallback< Boolean >
{
/** The game updates database access interface */
private final UpdatesDAO updatesDao;
/** The registry of server-side update batch processors */
private final ServerProcessorRegistry registry;
/** The identifier of the current update cycle */
private final long tickId;
/**
* Initialise the various dependencies and set the identifier of the current cycle
*
* @param updatesDao
* the game updates database access interface
* @param registry
* the registry of server-side update batch processors
* @param tickId
* the identifier of the current update cycle
*/
GameUpdateTransaction( UpdatesDAO updatesDao , ServerProcessorRegistry registry , long tickId )
{
this.updatesDao = updatesDao;
this.registry = registry;
this.tickId = tickId;
}
/**
* Update batch processing transaction
*
* <p>
* Call the database's game update processing function, returning immediately if the function
* indicates that the cycle has been completed. Otherwise, execute a server-side processor if
* necessary.
*
* <p>
* If the database requests server-side processing but no processor is found, an
* {@link UnsupportedUpdateException} will be thrown.
*
* @return <code>true</code> if the current update cycle has been completed, <code>false</code>
* if more updates are needed.
*/
@Override
public Boolean doInTransaction( TransactionStatus status )
{
GameUpdateResult result = this.updatesDao.processUpdates( this.tickId );
String local = result.getLocalExecution( );
if ( result.isFinished( ) ) {
return true;
} else if ( local == null ) {
return false;
}
UpdateBatchProcessor processor = this.registry.getProcessorFor( local );
if ( processor == null ) {
throw new UnsupportedUpdateException( local );
}
processor.processBatch( this.tickId );
return false;
}
}

View file

@ -0,0 +1,33 @@
package com.deepclone.lw.beans.updates;
import com.deepclone.lw.interfaces.game.updates.UpdateBatchProcessor;
/**
* Server-side update processor registry
*
* <p>
* This interface allows server-side game update batch processors to be looked up depending on the
* type of the updates to process.
*
* @author <a href="mailto:tseeker@legacyworlds.com">E. Benoît</a>
*
*/
interface ServerProcessorRegistry
{
/**
* Get a batch processor for some type of update
*
* @param type
* the type of updates to process, as stored in the <code>sys.update_types</code>
* database table
*
* @return <code>null</code> if no processor for the specified type has been registered, or the
* processor to use.
*/
public UpdateBatchProcessor getProcessorFor( String type );
}

View file

@ -0,0 +1,68 @@
package com.deepclone.lw.beans.updates;
import java.util.HashMap;
import java.util.Map;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanInitializationException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import com.deepclone.lw.interfaces.game.updates.UpdateBatchProcessor;
/**
* Server-side update processor registry component
*
* <p>
* This component will look through registered Spring components for update processors, and use the
* results as the registry's contents.
*
* @author <a href="mailto:tseeker@legacyworlds.com">E. Benoît</a>
*/
class ServerProcessorRegistryBean
implements ServerProcessorRegistry , BeanPostProcessor
{
/** Known server-side update batch processors */
private final Map< String , UpdateBatchProcessor > processors = new HashMap< String , UpdateBatchProcessor >( );
/** Do nothing - we're not interested in uninitialised components */
@Override
public Object postProcessBeforeInitialization( Object bean , String beanName )
{
return bean;
}
/**
* If an initialised component implements the {@link UpdateBatchProcessor} interface, add it to
* the {@link #processors} map. A {@link BeanInitializationException} will be thrown if two
* components seem to process the same update type.
*/
@Override
public Object postProcessAfterInitialization( Object bean , String beanName )
throws BeansException
{
if ( bean instanceof UpdateBatchProcessor ) {
UpdateBatchProcessor processor = (UpdateBatchProcessor) bean;
String updType = processor.getUpdateType( );
if ( this.processors.containsKey( updType ) ) {
throw new BeanInitializationException( "Duplicate update processor for update type " + updType );
}
this.processors.put( updType , processor );
}
return bean;
}
/** Obtain the processor from the {@link #processors} map */
@Override
public UpdateBatchProcessor getProcessorFor( String type )
{
return this.processors.get( type );
}
}

View file

@ -0,0 +1,29 @@
package com.deepclone.lw.beans.updates;
/**
* Unsupported update exception
*
* <p>
* This exception is thrown by the {@link GameUpdateTransaction} when there is no server-side
* processor for some update type.
*
* @author <a href="mailto:tseeker@legacyworlds.com">E. Benoît</a>
*/
@SuppressWarnings( "serial" )
public class UnsupportedUpdateException
extends RuntimeException
{
/**
* Initialise the exception
*
* @param name
* the name of the update type for which no processor is registered.
*/
UnsupportedUpdateException( String name )
{
super( "No processor found for update type " + name );
}
}

View file

@ -0,0 +1,69 @@
package com.deepclone.lw.beans.updates;
import java.sql.Types;
import java.util.Map;
import javax.sql.DataSource;
import org.springframework.beans.factory.annotation.Autowired;
import com.deepclone.lw.interfaces.game.updates.UpdatesDAO;
import com.deepclone.lw.sqld.sys.GameUpdateResult;
import com.deepclone.lw.utils.StoredProc;
/**
* Updates data access component
*
* <p>
* This component accesses the database procedures and views which constitute the game update
* system.
*
* @author <a href="mailto:tseeker@legacyworlds.com">E. Benoît</a>
*/
class UpdatesDAOBean
implements UpdatesDAO
{
/** Game update processor stored procedure */
private StoredProc process;
/**
* Dependency injector for the data source
*
* <p>
* When Spring injects the data source component, initialise the stored procedures used by the
* component.
*
* @param dataSource
* the data source component
*/
@Autowired( required = true )
public void setDataSource( DataSource dataSource )
{
this.process = new StoredProc( dataSource , "sys" , "process_updates" ).addParameter( "tick_id" , Types.BIGINT )
.addOutput( "_has_more" , Types.BOOLEAN ).addOutput( "_process_externally" , Types.VARCHAR );
}
/**
* Execute the processor's stored procedure
*
* <p>
* This implementation simply runs the database's <code>sys.process_updates()</code> stored
* procedure, and converts the two values it returns into a {@link GameUpdateResult} instance.
*/
@Override
public GameUpdateResult processUpdates( long tickId )
{
Map< String , Object > m = this.process.execute( tickId );
if ( !(Boolean) m.get( "_has_more" ) ) {
return new GameUpdateResult( );
}
return new GameUpdateResult( (String) m.get( "_process_externally" ) );
}
}

View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd">
<import resource="game/updates.xml" />
</beans>

View file

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">
<bean id="gameUpdateDAO" class="com.deepclone.lw.beans.updates.UpdatesDAOBean" />
<bean id="gameUpdateProcessor" class="com.deepclone.lw.beans.updates.GameUpdateProcessorBean" />
<bean id="gameUpdateRegistry"
class="com.deepclone.lw.beans.updates.ServerProcessorRegistryBean" />
<bean id="gameUpdateTask" class="com.deepclone.lw.beans.updates.GameUpdateTaskBean" />
</beans>