Skip to content

Advanced Concepts

In this article we discuss advanced aspects of Extension development that apply to all supported languages.

Server multi-thread model

As mentioned in other chapters SmartFoxServer 3 makes full use of virtual threads (officially introduced in Java 21). This helps greatly to simplify how the server scales with the hardware available and removes lots of burden from the developer, such as configuring thread pools, creating custom executors or schedulers, etc.

This is particularly true when Extensions make frequent use of blocking I/O such as databases, web services, RPC calls and similar. Virtual threads are light-weight resources that are managed efficiently by the internal JVM scheduler, and can be created in hundreds of thousands (or more) without risks of blowing up the RAM.

Extensions in SFS3 handle each client request and server event via a separate virtual thread, therefore it is less likely that a slow database call or web service could cause a bottleneck with high traffic.

Note:

Even with this premise it is still very important to make sure that external services working with SmartFoxServer are responsive enough, to avoid system-wide bottlenecks. For instance a database with an average latency of 300ms will necessarily cause a bottleneck at the application login level, reducing the number of players that can enter to ~3 Users/sec.

You can see why it is still of the utmost importance to make sure that all systems work together with as little latency as possible.

With this in mind, your Extension code should never try to create new threads or thread pools by hand. Instead use the SmartFoxServer API, which provide several ways to run or schedule tasks in separate (virtual) threads.

Running a task in a separate thread:

In SmartFoxServer 3 every request or server event is already handled by a separate virtual thread therefore the need for an Extension to create new threads is drastically reduced.

In most cases we recommend to let the current thread deal with whatever task needs to be handled, be it a quick operation or a blocking one. If you still find yourself in need of running other operations concurrently you can use the following calls:

    Future<?> task = getApi().runInSeparateThread(() -> {
        // code ...
    });

Here we can pass either a Runnable or a lambda to spawn a new virtual thread running our code. There's also another version of the method taking a Callable<T> rather than Runnable in case you need a return value.

    Future<String> task = getApi().runInSeparateThread(() -> {
        return "Hello World";
    });

Scheduling a task for future execution:

Here's an example of a delayed task that will run once after the specified amount of time:

    getApi().getScheduler().schedule(() -> {

        // task logic...

    }, 10, TimeUnit.SECONDS);

You can also run a repeating task that will trigger at the specified interval:

public class SchedulerTestExtension extends SFSExtension
{
    private class TaskRunner implements Runnable
    {
        private int runningCycles = 0;

        public void run()
        {
            try
            {
                runningCycles++;
                trace("Inside the running task. Cycle:  " + runningCycles);
            }
            catch (Exception e)
            {
                // Handle exceptions here
            }
        }
    }

    ScheduledFuture<?> taskHandle;

    @Override
    public void init()
    {
        // Schedule the task to run every second, with no initial delay
        taskHandle = getApi().getScheduler().scheduleAtFixedRate(new TaskRunner(), 0, 1, TimeUnit.SECONDS);
    }

    @Override
    public void destroy()
    {
        super.destroy();

        if (taskHandle != null);
            taskHandle.cancel();
    }
}

This looks more complex because we build a separate Runnable instance that manages its own state internal state, something that is not easily done with a simple lambda.

Note:

It's highly recommended to wrap your task logic in a try/catch block. This is because Java schedulers have a nasty "feature" of swallowing any Exception, unless you don't catch it yourself. In other words your code may die silently if you don't handle exceptions manually.

Class Loader architecture

By default every Extension is loaded by the server in a separate Class Loader. This allows to separate each Extension (and its dependencies) and to reload them at runtime without restarting SmartFoxServer.

Another interesting feature of this approach is that it allows to isolate dependencies so that two Extensions could use two different versions of the same library without collisions or other side effects.

The SmartFoxServer 3 Class Loader architecture is exemplified in this diagram:

CL-Arichitecture

Each Class Loader is connected to a parent, with the exception of the System Class Loader which sits at the top and manages the application classpath.

The default class loading mechanism in the JVM is top-down: when a class is required, the JVM will first ask its parent to find it before searching itself.

When an extension is reloaded the specific Extension Class Loader is destroyed and recreated. This generates a new versions of the classes contained in the deployed jar file(s), while the rest of the classes in the top levels are unaffected (which implies that they cannot be reloaded).

All this is not particularly important until you need to share data across different Extensions.

Keeping the diagram above in mind, let's see an example:

  • Extension A is our main Zone Extension
  • Extensions B and C are two Room Extensions, managing 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 Class Loader contains a different version of the same classes, which can cause unexpected consequences such as raising a ClassCastException.

This will happen when we attempt to get an object from another context (i.e. Class Loader) even if the two classes are the same bytecode. Technically, even with the same bytecode, the JVM sees them as different class definitions sharing the same fully qualified name, in three separate contexts.

Fortunately there is a rather simple solution, illustrated below: deploying the model classes in the extensions/_shared/ folder which shares these objects across all Extensions.

CL-Arichitecture

Maintaining state in Extensions

A SmartFoxServer Extension is usually comprised of one main Extension class and a series of request and event handlers. A common question that arises is: where to keep the application shared state (scores, game map, game state...)?

Typically there are two recommended solutions:

  • 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.

The first approach is usually the best a number 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.