S is for Service
The basic diagram for a service looks pretty simple. It is a service contract (the circle) and its implementation (the box), looking very much like a component with an interface hiding its internals. One interacts with the circle while the box does all the work.
I like thinking of a “service” this way as it keeps me from getting too hung up on hosting environments and technology stacks or whatever newest greatest framework to end all frameworks has caught the attention of the young rebel programmers, so that I can concentrate more on the functionality it encapsulates and the data crossing at the interface boundary.
When coupled with the idea of model and a distributed runtime environment, the service interface becomes the operational public endpoint for distributed component access. The WCF term for the interface is Service Contract.
As a quick aside, there is no commandment stating a single runtime instance must only host one service endpoint (or one model context), nor be connected to each other across remoting boundaries. Using techniques like inversion-of-control, one service can access other service interfaces whether they are locally instantiated or proxied-in from remote runtime hosts.
Also, model systems can be designed to be able to interact with each other directly within a single runtime instance without going through a service interface; as is the case with the Ikosa framework. In Ikosa, most of the game mechanic sub-systems on the “server” interact directly with each other without any intermediate service interfaces or calls; these are relatively independently modeled systems that all coexist in one over-arching game state context
However, externally, three distinct service interfaces are publicized: login services, visualization services and game operation services. Visualization and game services both rely on the game state and game resources available to the current running game in the process; but present different data and have different concerns. Login services accesses some game state information also; but is not a major component of gameplay or visualization.
Within the Ikosa framework, the service boundary is a natural place to address several concerns of distributed systems. While not an exhaustive list, nor again, authoritative on how any general service *must* be structured, many of these concerns apply to a more general service concept.
Requests from clients coming over a distributed network need to have their authorization checked to ensure the principal originators of the requests have authorization for the requests they are making. This also includes authenticating the security principals to validate their identities as a precursor to checking authorization.
Ikosa users are either master users (with virtually unlimited control), or player users. A user’s login model has a set of one or more player characters that are:
- associated at login
- associated during a request to add a character to a login session
- de-associated during request to remove a character from a login session
The users available for a character are stored with the character itself. Master users can play as any player. Master users also have access to other functions “game-changing” functions, elevated permissions within regular gameplay, and are the notification target for general system events or conditions needing user attention.
In distributed service-oriented systems, the service runtime does not have direct control over the number of clients nor the frequency of demands of any of the client runtimes requesting service. If a service can arbitrarily dispatch new threads (up to operating system and machine limits) and the operations are all read-only, there shouldn’t be any major issue with synchronization as the model access is all invariant on the operations.
Read-write systems with multiple clients are another matter. Read-write access patterns in Ikosa are heavily biased towards multiple frequent readers and infrequent singleton writers. Calls to a game-setting shared instance of the .NET ReaderWriteLockSlim class are used to bracket operations at the service level depending on whether the operation is a read or a write operation.
A quick description of a reader-writer lock is that code bracketed by such a lock queues up read and write operations. More than one section of code can be bracketed by the same lock, but it is generally unwise to attempt to hold locks across multiple service calls.
If a read operations gets hold of the lock, other reads immediately behind it in the queue can enter as well and the lock stays in read mode until there are no more read operations entering, either because there are no more operations queued or the next in the queue is a write operation.
Then, a single write operation holds the lock in write mode until it is releases the lock. Even if multiple writes are queued sequentially, only one will be allowed to hold the lock and enter the protected section at a time.
Operation then continues from there following the same rules as defined already.
Additionally, login services use concurrent collections to maintain session information. These environment defined collection types do not need to use “heavy” lock mechanics to avoid data update/read collisions.
There are also some infrequently updated model states that are snapshotted, updated, and then have their shared reference replaced with the updated copy.
As another “business system” aside: when making a service that deals with an RDBMS for its model-state persistence, transaction control (on data-context commit) in the RDBMS handles most of the synchronization functions you might need for the service operations. If there are multiple RDBMS connections used in a service operation, then distributed transaction coordination handles most of the synchronization functionalities.
While it is possible in a general business service enterprise system to have a completely independent (and message-based asynchronous) notification system operating as its own service and communication framework, the Ikosa framework uses WCF duplex channels and callbacks. This puts the service boundary at the frontline for notifications.
Each service contract that defines a callback contract expects the client to implement and provide a corresponding callback interface when connecting to the service. Since notifications are invariably a result of game state change, they can occur in the context of a write-based operation. To prevent notifying a client of a change while the synchronization state of the model is blocked on a writer, notifications are queued until after the lock is freed so that (at the very least) the writer client can assume it is no longer holding a lock.
Also, as some model state changes may trigger multiple notification events, the queuing of notifications until the write-lock is freed at the end of a service operation also allows the notification system to batch the notifications into single callbacks with larger payload rather than multiple small notifications.
Clients can examine the batched notifications as a whole to decide what data-fetch operations they need to perform to update their view of the game state.
Service operations should not leave the model state indeterminate nor unusable. Do not “block” across calls waiting for another call to finish an operation. More explicitly, when there is a sufficiently “chatty” set of co-dependent operations, then it is a good idea to either withhold the operation until all is complete, or explicitly model and provide services for managing a multi-call workflow operation.
The Ikosa framework’s main asynchronous workflow system consists of a ProcessManager “running” a stack of CoreProcess instances that are effectively a programmatically emitted sequence of CoreSteps. A CoreStep can only be processed when all of the StepPrerequisites it defines (if any) are fulfilled. Many prerequisites are presented to clients through services, and are fulfilled by clients making service calls back. For instance, a spell might require a target of the spell to make a saving throw (roll of a 20-sided die) so that the spell can determine what type of effect to apply.
Users interact with the ProcessManager via instances of CoreActivity which are derived from the previously mentioned CoreProcess but supply initialization AimTargets specific for whatever CoreAction is selected for the CoreActivity.
The Ikosa service contract includes the API for interacting with this workflow system via operations for GetActions(), DoAction(), GetPrerequisites() and SetPrerequisites().