In a series of posts we’ll do “Firebase From Scratch”, an introduction to Firebase and its concepts and ideas. Hopefully, reading this series will give you a firm grasp of what Firebase is and what it can do.
- Part 1: Introduction
- Part 2: Diving In
- Part 3: A Maven Interlude
- Part 4: Games, Services and Tournaments
- Part 5: What’s In a Server Game Anyway?
- Part 6: Activating Games
- Part 7: Actions On The Table
- Part 8: Services
- Part 9: Custom Authentication
Having talked a bit about the server side game the last three episodes, let’s have a look at services. These are extensions to Firebase you can write yourself to provide cross-game functionality and common behavior.
What It Is
Internally Firebase uses services extensively for it’s own purposes.They are code extensions that you can write and deploy separately in Firebase to take care of common behavior. This might include wallets, reporting, RNG’s etc.
We like to say that services are “first class citizens” in Firebase. By this we mean the following:
- A deployed service is guaranteed to be started before any game and to be available during the lifetime of the server. Furthermore, Firebase will refuse to start if any service reports error on start-up.
By this strict definition you’re guaranteed that a particular service will always be available for your game to use as long as it has been deployed and the server is started. This is important for them to function, and is what makes them a part of Firebase itself.
Lifetime and Contracts
A service is divided into two interfaces, one external contract and one internal implementation. The external interface is what every code interacting with the service will see, and the internal interface is what Firebase sees and uses. Let’s have a look at a very simple service shall we?
public interface EchoService extends Contract { public String echo(String msg); }
The above service extends “contract” which is the public interface for the service. So any code using this service will only see the above interface, not the implementation, which looks like this:
public class ServiceImpl implements Service, EchoService { public void init(ServiceContext con) throws SystemException { } public void start() { } @Override public String echo(String msg) { return msg; } public void stop() { } public void destroy() { } }
You immediately can see above that we’ve inherited a lot of methods from the Service interface. These are the lifetime methods of the internal implementation of the service and will be used by Firebase.
- init(ServiceContext) – This is the first thing that happens and the service context gives the implementation access to configuration, other services etc.
- start() – If all services have been initiated, they’re started. A service can be multithreaded and start/stop is where any internal threads should be managed.
- stop() – As the server starts closing down “stop” will called to halt any internal threads.
- destroy() – The server is closed and any lingering resources should be dealt with.
Exported Classes
So the public interface which extends Contract is what the using code will see and handle, but here’s a limitation: As Firebase packages everything into different class loaders, how can anyone use the service at all? Won’t you have to duplicate all classes? The answer is no, but you do have to “export” them from the services itself.
- Any class that is “exported” by a service is available everywhere in Firebase.
This has several implications which we’ll get back to when it’s time to discuss the class loading in more detail. But for now we’ll go through the check list for a service public interface:
- The Contract interface itself is always exported by Firebase.
- Any classes or dependency the Contract have must be explicitly exported by the developer.
Relating to the second point above: it is only dependencies of the Contract that must be exported, not the internal implementation of the Service interface. Anything that is public must be exported. Let’s have a look at an example:
public interface WalletConfigService extends Contract { public WalletConfig get(int operatorId) throws ConfigException; }
The above class extends Contract, so it is the public interface of the service and will automatically be exported. However, it depends on “WalletConfig” and “ConfigException” and these must be exported manually.
The actual export mechanism is via the service deployment file (which is called “META-INF/service.xml” in the packaged SAR file) in a section that may look something like this:
<exported> <package>org.myapp.wallet.service.api.*</package> <class>org.myapp.wallet.service.ConfigException</class> </exported>
The above example exports all classes in the API package (but no subpackages, use “-” instead of “*” to include those), and the single class “ConfigException”. More on this will follow when we talk about packaging.
Access
So we have a service, and it’s deployed, now how do we actually use it? In games, tournaments and services there is a service registry which you get hold of via the context, and this registry holds all services. For example, accessing our fictional wallet configuration service in a game may look like this:
public void init(GameContext con) throws SystemException { ServiceRegistry reg = con.getServices(); Class<WalletConfigService> key = WalletConfigService.class; WalletConfigService service = reg.getServiceInstance(key); [...]
As you can see a service is looked up via its Contract and you will not actually have access to the internal service implementation. You can also look up service by their ID and iterate over services with the same Contract.
Plugin Services
Here’s a pattern Firebase uses extensively: Imagine your service depends on behavior that should be configured later or may change depending on environment. In these instances Firebase uses what we call “plugin services”. These service have a defined Contract but no implementation, and the using code will try to look up the Service in the service registry in runtime, and if not found fall back to some default. This way you can customize complex behavior by simply deploying different services.
The most pertinent example of this pattern is the “LoginLocator” service Contract provided in the Firebase API. Firebase has a very simple default login behavior where it assumes that a password is an integer which it immediately use as a player ID, so if you login with “adam” as username and “666” as password, 666 becomes the player ID. This is useful for testing and running bots, but in reality you want to authenticate against a real database, and here’s where plugin services comes to the rescue. All you need to to is to implement the “LoginLocator” interface in a service and deploy it, and hey presto, you’ve just cutsomized the login.
More on that in a later installation. For now, let’s just remember that “plugin services” are service implementation that customize behavior in Firebase or indeed in your own codebase.