SFS2X Docs / ExtensionsJava / advanced-concepts
» Java Extensions: advanced concepts
In this article we discuss advanced aspects of Java Extension development:
- The Server threading model
- Auto load-balancing thread pools
- Tuning thread pools
- Maintaining state in Extensions
- Class loading architecture
- Delayed and scheduled tasks
» The Server threading model
For all users of SFS2X 2.8.x and earlier versions please see the old threading model document.
SmartFoxServer 2X runs all Extensions in a multithreaded environment. There are fundamentally two separate thread pools operating on an Extension: the ExtensionController and the SystemController. The former entity is responsible for processing client requests while the latter dispatches system events such as LOGIN, USER_DISCONNECT, ROOM_VARIABLES_UPDATE, etc.
Since multiple threads can operate concurrently on the Extension code we need to make sure that access to shared state is handled correctly. The standard JDK's concurrent collections and locking features provide robust tools to handle the most common concurrency problems with minimal effort.
We highly recommend concurrent collections over native arrays or old-style collections, such as Vector and Hashtable, which can limit concurrency and create bottlenecks in the code. Also, when atomicity is required we suggest to look into AtomicInteger, AtomicLong, AtomicReference as a valid solution. Finally, locking classes will be required when complex operations need to be run in a mutually exclusive fashion.
We also suggest a couple of external resources if you want to dig deeper into this subject:
The following diagram illustrates what what we have described. The two controllers run separate thread pools that concurrently invoke request and event handlers on the Extension classes.
In addition to this, Extension code can also schedule any number of recurring or delayed tasks which in turn are handled by a different entity, the TaskScheduler, running its own thread pool.
It is important to note that the SFS2X API already take care of concurrency most of the times: all the calls provided bye the main SFSApi class are thread safe and the same goes for the Game API and Buddy List API. There are however a few exceptions: SFSObject and SFSArray, for example, are not thread safe. Since these objects are mostly used for data transport, they are usually not contended by multiple threads. In any case the Javadoc specifies which object require extra care for thread safety.
» Auto load-balancing Thread Pools
One of the question that gets asked very frequently is how to configure the amount of threads running in the system to scale an application correctly. In SmartFoxServer 2.9.0 we have introduced a significant improvement in scalability by removing manually configured thread pools.
The solution we provide is a self-monitoring, auto-scaling Executor that is able to scale on demand providing a balanced, yet configurable, ratio between latency and resources used.
With this feature the amount of manual configuration and fine tuning required to run any application is essentially reduced to zero. More importantly the system automatically watches the state of the message queues and reacts to a lack of threads on demand. This allows to protect the server from creating and destroying threads continuously for small bursts of traffic. At the same time it allows to react to significant increases in traffic and/or slow I/O work. Excess threads are going to be released as soon as they are no longer needed.
» Tuning thread pools
For most usages the server's pools don't need to be touched. However if you're making large use of slow I/O in your extensions, we provide an advanced panel under the AdminTool > Server Configurator that allows experts to fine tune the thread configuration. The following is a list of available parameters for each Executor.
- Core threads: number of threads to use at startup.
- Backup threads: number of threads added every time a backup is triggered.
- Maximum backups: maximum amount of thread backup operations.
- Queue size triggering a backup: the size of the queue that will add a new batch of backup threads.
- Backup triggering time: number of seconds after which a backup is called if the queue size trigger is still active.
- Backup threads expiration: number of seconds after which backup threads are removed, if the queue size is lower than the next value.
- Queue size preventing backup expiration: the size of the queue that allow backup threads to be removed.
- Log activity: add messages in the log files every time the thread pool is resized.
- Full queue warning interval: how many seconds between warnings about the message queue being full.
- Controller Request queue size: the max. size of the controller queue.
- Prestart core threads: starts all core threads immediately, rather than adding them when needed (since 2.13.1).
We recommend not to touch the default configuration values unless you have very good reasons for doing so. The default settings are already fine tuned to provide excellent scalability in a wide array of scenarios and workloads.
» Maintaining state in Extensions
When we employ the SFSExtension class as the base class for our Extensions we end up with one main Extension object and a series of request and event handlers. A common question is: where to keep the application shared state? (score, leaderboards, game data, etc)
Typically there are two logical answers:
- in the main Extension class exposing the game model via getter/setter(s);
- using a Singleton that can be accessed from anywhere in your code.
We would highly recommend the first approach over the second for a series of reasons:
- Singletons are not easily implemented with in an environment with multiple Class Loaders (we explain all the details in the next section of this document);
- Singletons can play nasty tricks across multiple Extension restarts, because of their static nature;
- the main Extension already acts as a Singleton and it's very easy to access it from any request or event handler via the getParentExtension() method; in addition you don't get the disadvantages of the Singleton that we just mentioned.
» Class loading architecture
In the introductory article on Extensions we have mentioned that each Extension is loaded in a different ClassLoader in order to allow hot-redeploy during development or even production. We also illustrated that in order to share dependencies across multiple Extensions these ClassLoaders follow a specific hierarchy:
The diagram shows that each Extension "sees" all its deployed classes in its own ClassLoader, it can access the top global Extension classes thanks to its parent ClassLoader and finally it can use any classes from the SFS2X framework thanks to the topmost element in this hierarchy.
When an extension is reloaded only the bottom ClassLoader is destroyed and rebuilt. This will create new versions of the classes contained in the deployed jar file(s), while the rest of the classes in the top levels are unaffected (this means that they cannot be reloaded).
Let's examine a practical example to see how this can be useful. Suppose Extension A is our main Zone Extension, while B and C are two Room Extensions, governing two different games. We want Extensions B and C to communicate with A in order to access the game leaderboards.
The first problem we encounter is that we need to deploy the same model classes in all three Extensions (A, B and C) in order to properly run the example. This means that each ClassLoader contains a different version of the same Class, which is a well known problem in Java. The infamous ClassCastException is raised by the Java Runtime when you attempt to get an object from another context even if the two classes are the same bytecode. Technically, although they really are the same bytecode, the JVM sees them as different Class definitions that happen to share the same fully qualified name although in three separate contexts (ClassLoaders).
If we didn't lost you up to this point you probably know the basics of ClassLoading in Java. If this sounds confusing we would highly recommend to check a few of articles on this subject:
Fortunately the solution is pretty easy: all you need to do is deploy the model classes in the extensions/__lib__/ folder and you will be able to share these objects across all Extensions.
Example:
Every Extension ClassLoader is now able to access the same model classes because they are reachable in their parent Loader. By providing access to the game model from the main Zone Extension we have created a Singleton-like solution without worrying about static data.
If you still require to create one or more Singleton classes, that need to be shared across multiple Extensions, this approach will work too. You will need to deploy the Singleton(s) in a jar file inside the __lib__/ folder.
» Delayed and Scheduled Tasks
Often times the game logic requires to use timers for recurring client updates (e.g. the end of a turn time, the triggering of specific events, NPC actions, etc).
A quick solution to this problem is using the ScheduledThreadPoolExecutor class provided in the JDK, which offers a convenient task executor backed by a pool of threads. SFS2X already runs its own instance of this Executor (wrapped in a class called TaskScheduler).
The following snippet of Java code shows how to run a looping task using the Server's own TaskScheduler.
public class SchedulerTest extends SFSExtension { private class TaskRunner implements Runnable { private int runningCycles = 0; public void run() { runningCycles++; trace("Inside the running task. Cycle: " + runningCycles); if (runningCycles >= 10) { trace("Time to stop the task!"); taskHandle.cancel(); } } } // Keeps a reference to the task execution ScheduledFuture<?> taskHandle; @Override public void init() { SmartFoxServer sfs = SmartFoxServer.getInstance(); // Schedule the task to run every second, with no initial delay taskHandle = sfs.getTaskScheduler().scheduleAtFixedRate(new TaskRunner(), 0, 1, TimeUnit.SECONDS); } }
The scheduleAtFixedRate method takes four arguments:
- a Runnable object that will execute the Task's code
- the initial delay before the execution starts
- the interval at which the task will be executed
- the time unit used to express the time values
The Scheduler also exposes a schedule method that executes a Runnable task once after the specified amount of time. Finally the Scheduler's thread pool can be resized on-the-fly at runtime via the resizeThreadPool() method.
NOTE
The initial size of the system TaskScheduler's thread pool can be adjusted via the Server Configurator module in the AdminTool.