Adding Server-Only Synchronization
The code in the preceding topic is sufficient to perform the work of the simple service, but requires the addition of a pair of modules to source and sink the synchronizable state when so requested by RPC Manager:
{***** Required Synchronization modules, which RPC Manager will call *****}
GetServerChanges Module;
SetHeartbeat Module;
GetServerChanges() is launched by RPC Manager, on the server, when the service must provide its synchronizable state for a client. RPC Manager is careful to launch this module on the same execution thread as the service so that, while executing a script in GetServerChanges(), nothing else can execute on the service’s thread. This can greatly simplify the acquisition of the synchronizable state. It is still possible, however, to have a launched RPC or a module launched by an RPC subroutine run during execution of a script in GetServerChanges(), as launched modules typically do not perform all their work in one script. If any such modules can modify the synchronizable state of the service, it is essential to either place the acquisition of the synchronizable state by GetServerChanges() within a CriticalSection, or to wait in GetServerChanges() until all such modules have completed execution.
On entry to GetServerChanges(), all service RPCs for the service are suspended (RPCs for other services and directed RPCs are still processed), allowing GetServerChanges() to wait for any launched modules that can modify the synchronizable state to finish. It is good design to only have your synchronizable state modified by RPC subroutines or modules launched by RPC subroutines. In this way, as no more service RPCs will be started until GetServerChanges() indicates that they can, you can guarantee that the synchronizable state of your service will not change while GetServerChanges() samples it.
GetServerChanges passes the service’s synchronizable state to RPC Manager by returning a "packed stream" of RPC calls, which will be made on the client. This enables the re-construction of the synchronizable state to be achieved by making a sequence of RPC calls on the client, ensuring that the entire sequence of RPC calls is delivered to the client as one package, without scope for communication interruptions causing a partial update of the synchronizable state on the client. On the client, each component of the RPC package is executed in the strict sequence that they are packed into the stream on the server. No other RPC call can interpose in this sequence on the client.
The second of the two modules is SetHeartbeat(). This is simply a service-specified RPC subroutine that, in our simple service case, receives, as a parameter, the synchronizable state of the service and stores it on the client. This RPC subroutine is the only RPC in the call package delivered from the server.
Let’s look at the code for GetServerChanges():
<
{======================== GetServerChanges ===================================}
{ Called by RPC Manager during startup sync, on a server, to get the package }
{ of RPCs which create a synchronizable state on the client which is in step }
{ with the server. }
{=============================================================================}
GetServerChanges
(
RevisionInfo { Revision info from GetClientRevision call
made on synchronising client };
PackStreamRef { Pointer to var to receive changes };
ClientName { Name of client };
Guid { GUID of the syncing application... };
SyncMonitorObj { Object value of my RPCManager\ServerSync.
Goes Invalid if sync aborts...VTS 10.0 on };
)
[
Stream { Stream of changes to go to client };
]
Sample [
{***** This delay is not necessary. It is only here to prove that our
synchronizable state modification RPCs are suspended until we
call SetDivert. *****}
If Timeout(1, 5) Wait;
[
{***** Sample the synchronizable state and build an RPC package to send to
the synchronizing client. *****}
Stream = \RPCManager\PackRPC(Stream, "SetHeartbeat" { module },
Invalid { scope },
{ Parameters: } Cast(LastHeartbeatCount, 4));
{***** Start diverting all RPCs for this client from here onwards. RPC
Manager will release the divert when synchronization done. This
also enables the flow of service RPCs for this machine (the server).
Expect a flood of 5 service modification RPCs to arrive! *****}
\RPCManager\SetDivert(SvcName, ClientName);
]
{***** If we lost the synchronizing session, abandon all hope! *****}
If !Valid(SyncMonitorObj) Done;
]
Wait [
{***** This delay is not necessary. It is only here to prove that the service
RPCs can continue after the RPC package has been built and SetDivert
called. At the end of this delay, this module will pass the sampled
state to RPC Manager which, in turn, passes it to the client. If we lose the synchronizing session, abandon. ******}
If Timeout(1, 10) || !Valid(SyncMonitorObj) Done;
]
Done [
If 1;
[
*PackStreamRef = Stream;
Slay(Self(), 0);
]
]
{ End of SampleService\GetServerChanges }
>
As stated above, RPC Manager launches this module when the server instance must provide the synchronized state of the service for a client. The parameters to GetServerChanges are:
RevisionInfo |
See Client Revision Information. |
PackStreamRef |
A reference to a variable into which GetServerChanges() will store a packed stream of RPC calls that will be executed on the client. |
ClientName |
The workstation name of the client instance that is synchronizing with the server instance |
Guid |
The GUID of the synchronizing application. For cross-application RPC services, this will be different from the GUID of the application under which GetServerChanges runs. |
SyncMonitorObj |
The object value of the RPCManager object that is monitoring the synchronization sequence. If this goes Invalid, you should abort the current synchronization. Typically this will be because communications with the client have been lost. Only available on VTS 10.0 onwards. |
The script in state Sample does the important work of this code. While this script is running, nothing else on the service’s execution thread can run. The first job is to pack together all the RPC calls that have to be made on the client to generate the same synchronizable state as is present on the server. In our simple case, this consists of a single call to SetHeartbeat with the current count as its only parameter:
Stream = \RPCManager\PackRPC(Stream, "SetHeartbeat" { module },
Invalid { scope },
{ Parameters: } Cast(LastHeartbeatCount, 4));
After the RPC package has been built into a temporary stream, the RPC Manager module SetDivert() is called:
\RPCManager\SetDivert(SvcName, ClientName);
This causes two things to happen:
- All RPCs for the service that are destined for the synchronizing client are held in a queue by RPC Manager until synchronization is complete. The effect of this is to allow the server instance to continue to perform its normal work, including updating other clients, without the risk of updates arriving at the synchronizing client instance before the synchronizable state has been stored there.
- RPCs arriving for this service were held in abeyance when GetServerChanges() was launched. These RPCs are now permitted to flow.
The intent is that the synchronizable state of the service is now allowed to change. Changes to the state will be routed to in-sync clients and will be buffered for the synchronizing client until the client has processed the RPCs that bring it up to the same state as was sampled at the server.
Although the sample GetServerChanges shown is a launched module, (specifically to incorporate the code in state Wait to demonstrate the function of SetDivert), you can also write it as a subroutine module. In this case, it must return Invalid when it is finished its work. If you return a valid value, RPC Manager will hang. There is no particular advantage in choosing a subroutine over a launched module. You can simply choose the form that suits your needs best. The same is true of two other service synchronization modules, called by RPC Manager, named GetClientRevision and GetClientChanges. (Described later.)
The client stores the synchronizable state when RPC Manager unpacks the RPC package on the client. In this case, the only call in the package is to SetHeartBeat():
<
{=============================== SetHeartbeat ================================}
{ Called, on the client, by the RPC contained in the package generated by the }
{ server, during GetServerChanges. }
{=============================================================================}
SetHeartbeat
(
Sequence { Info regarding call };
)
SetHeartbeat [
If 1;
[
HeartbeatCountIn = Sequence;
\RPCManager\SetSyncComplete(SvcName, Invalid, 1);
Return(Invalid);
]
]
{ End of SampleService\SetHeartbeat }
>
{ End of SampleService }
This simply stores the synchronizable state in HeartbeatCountIn and then informs RPC Manager that it is now synchronized. This is achieved by the call to SetSyncComplete(). It is vital that the client makes this call. Service synchronization will hang if this call is not made. Note that SetHeartbeat() does not have to make this call. It can be a separate call within the RPC package, but it must be made.
After SetSyncComplete() is called, RPC Manager releases the queue of service requests (on the server) for this client and all subsequent service RPCs start flowing normally to the newly synchronized client.