Threading in plugins

From EsWiki

Jump to: navigation, search

Each room-level plugin in ES4 is a separate instance of the class, where each server-level plugin is a single instance. Both types of plugins are multithreaded. Each request, userExit, userEnter, userSendPublicMessage, etc is handled in a separate thread. This means that it is quite possible that one thread is processing a request at the same time as another is processing a userEnter, etc. This can lead to some headaches, particularly if your code is complicated and you try to protect sections by synchronizing them due to the increased risk of deadlocks.

Contents

Use Concurrent Data Structures

Java includes a number of excellent classes for use in multithreaded applications. ConcurrentHashMap is excellent for storing data that might otherwise be stored in a hash table, map, vector, collection, or array and AtomicInteger similarly for integers. ArrayList and int are still quite useful of course, but if there's a chance that two different threads might be changing the value of the int at the same time, it is best to use AtomicInteger instead. If there's a chance that two different threads might be adding an element to or removing an element from a data structure, ConcurrentHashmap is a far better choice than ArrayList.

Spawn New Threads

Often applications need to access a database when a user enters or leaves a room, such as storing points earned while playing a game in the room. There are part of ES4 that wait until the plugin's userEnter or userExit method finish before cleaning up details, so a service call inside the method is a bad idea. The way to handle this is to spawn a new thread. Here's an example of one way to do it, using a ScheduledCallback:

  1. Create a method that handles the database access call. For example, asynchUpdatePlayerCurrency(String playerName, int newPoints).
  2. Create another method that is named similarly that uses a scheduled callback one millisecond later, one time only, invoking the method from step 1. For example:
         private void updatePlayerCurrency(final String playerName, final int newPoints) {
            int id = getApi().scheduleExecution(1,
                     1,
                     new ScheduledCallback() {
   
                         public void scheduledCallback() {
                            asynchUpdatePlayerCurrency(playerName, newPoints);
                     }
                     });
         } 
  1. Where the call is needed, invoke the second method above, passing in the needed parameters. The database access is handled outside the thread that is executing userExit or whatever other method the access used to be in, so it doesn't hurt ES4 performance as much unless there are so many database accesses that most of the threads ES4 has are tied up in them.


Special Case: LoginEventHandler

Login Event Handlers are trickier. Often a database access is needed to determine whether we allow this user to log in at all, so we can't spawn a separate thread. During times when many users are trying to log in during a short period, this can bring an ES4 to its knees, particularly if the database access times are on the order of 80ms+ each.

The way to solve this dilemma is to use a two-stage login process. The actual LoginEventHandler does not do a database access at all. The client is programmed so that as soon as the successful login response is received, the client sends a plugin request to a server-level plugin with any needed login information. This plugin processes the request by spawning a new thread for the database access. When the access finishes and is processed, if the result is to deny log in, the plugin kicks the user off the server. If the result is to allow log in, a plugin message is sent to the client, and at that point the client's UI shows him that he has logged in (before this he still thought he was logging in).

This makes the log in process take a bit longer, but not much more except during times when many users are trying to log in during a short period. All those database accesses will still slow things down, however it will take many more users trying to log in at the same time before those users already logged in will notice problems.

Second Thread Pool

An application that has a large number of database accesses and a large number of concurrent users will still have problems during heavy usage. The solution here is to create a second thread pool for all those database access threads. This is done easily using a Managed Object. Threads in the second (or third or fourth!) thread pool are in addition to those from the ES4's main thread pool.

ExecutorServiceManagedObjectFactory

First, add a new class to your project similar to this one:

   public class ExecutorServiceManagedObjectFactory extends BaseManagedObjectFactory {
       private ExecutorService executor;
   
       @Override
       public void init( EsObjectRO params ) {
           int poolSize = params.getInteger( "pool-size", 20 );
           executor =
               new ThreadPoolExecutor( poolSize,
                                       poolSize,
                                       60,
                                       TimeUnit.SECONDS,
                                       new LinkedBlockingQueue<Runnable>(),
                                       new ThreadFactory() {
                                           final AtomicInteger threadNumber = new AtomicInteger( 1 );
   
                                           @Override
                                           public Thread newThread( Runnable r ) {
                                               return new Thread( r, "SecondThreadPool " + threadNumber.getAndIncrement() );
                                           }
                                       } );
       }
   
       @Override
       public ExecutorService acquireObject( EsObjectRO params ) {
           return executor;
       }
   
       @Override
       public void destroy() {
           executor.shutdown();
           super.destroy();
       }
   }

This new class will need several imports:

   import java.util.concurrent.ExecutorService;
   import java.util.concurrent.LinkedBlockingQueue;
   import java.util.concurrent.ThreadFactory;
   import java.util.concurrent.ThreadPoolExecutor;
   import java.util.concurrent.TimeUnit;
   import java.util.concurrent.atomic.AtomicInteger;
   
   import com.electrotank.electroserver4.extensions.BaseManagedObjectFactory;
   import com.electrotank.electroserver4.extensions.api.value.EsObjectRO;


Extension.xml

Second, you will need to add the managed object to your Extension.xml. The managed object section goes above the event handlers and plugins and will look something like this:

   <ManagedObjects>
       <ManagedObject>
           <Handle>Executor</Handle>
           <Type>Java</Type>
           <Path>com.packagename.ExecutorServiceManagedObjectFactory</Path>
           <Variables>
               <Variable name="pool-size" type="integer">20
               </Variable>
           </Variables>
       </ManagedObject>
   </ManagedObjects>

The pool-size is the maximum number of threads in this thread pool. The best value to use for it depends on the application and also on the server's hardware specifications.

Using the separate thread pool

The final step is to actually use the separate thread pool in a plugin. Here's one way to do that.

  • Add one import:
   import java.util.concurrent.Executor;
  • Add a class variable of type Executor. For this example we will name it executor.
  • Somewhere in the plugin initialize executor.
   executor = (Executor) getApi().acquireManagedObject( "Executor", null );
  • Add a method to execute the thread.
   protected final void execute( Runnable command ) {
       if ( null == executor ) {
           throw new IllegalStateException( "executor not initialized" );
       }
       executor.execute( command );
   }
  • Instead of using ScheduledCallback to create an asynchronous thread to invoke a method, use something like this:
       execute(new Runnable() {
           @Override
           public void run() {
               invoke the method here. parameters must be final.
           }
       });

Taking our previous example of an asynchronous thread that invokes a method named asynchUpdatePlayerCurrency, the updatePlayerCurrency method would become:

         private void updatePlayerCurrency(final String playerName, final int newPoints) {
            execute(new Runnable() {
               @Override
               public void run() {
                   asynchUpdatePlayerCurrency(playerName, newPoints);
               }
           });
         }
Personal tools
download