In addition to the asynchronous RPC, the Massiv also implements the synchronous RPC (also called SRPC). Unlike asynchronous call, synchronous call will block execution of the current thread until the request is delivered, callee method returns and call results are delivered back to the caller node. Context may switch to different threads while the current thread is blocked. See Section 17.1, “The Model Used By the Core” for more information about the Massiv threading model.
Synchronous calls in the Massiv have best effort semantics, they will be performed at most once. The call request may have several outcomes: its delivery may either succeed or fail, and if it succeeds, the call itself may either succeed or fail. The Massiv will always try to inform the ResultObject about the call outcome (see Section 8.2, “RPC Model”). If the results are delivered to the ResultObject, it will either return them to the caller (in case of success), or throw an exception. If the delivery of the results fails, information about the call will be lost, the call will timeout and the relevant exception will be thrown too.
To call a method FUNCNAME synchronously, use the sync_FUNCNAME method of the stub object:
/* This variable will hold value of method out argument "result". Int32 ires; /* Call method bar of object foo, passing 1 as param. Store the value of the out argument "result" into variable ires, and the value of the return type into variable bres. */ bool bres = foo->sync_bar( ires, 1 ); |
Unlike asynchronous method stubs, synchronous method stubs have the same signature as the method they call. The Massiv will pass all in and inout arguments over the network to the callee method, and transfer the results (out and inout arguments and return type value) back to the caller. The semantics of synchronous call is nearly the same as that of standard local C++ call. However, there are several differences. The most obvious one is that the call may block execution of current thread for quite a long time, and it may even timeout. The SRPC is also the only place where the Massiv may switch context to another thread. More differences and limitations are explained in the following sections.
As already mentioned several times, the sync_ stub method may throw an exception. This section tries to clarify when and why this may happen. For more generic information about the Massiv exceptions, see Section 4.3.5, “Throwable Objects and Exceptions”.
There are two reasons why an exception may be thrown:
Call request has been successfully delivered, the method has been called and it has thrown an exception. The exception will be delivered back to the callee node and rethrown there.
The call has failed. The Massiv Core throws an exception that indicates the failure.
The Massiv Core is able to deliver exceptions thrown by the callee method back to the caller. Because the standard migration is used to implement the RPC, the Massiv exceptions transferred over the network must be throwable managed objects (see Section 4.3.5, “Throwable Objects and Exceptions”). (which means, besides others, that they must be properly described in the relevant IDL). If a callee throws such exception, it will be delivered back to the caller and rethrown there.
Other exceptions will be remapped to a single managed exception, Massiv::Core::Lib::CoreException. If the exception inherits standard std::exception, its what() message will be copied to the CoreException.
If the call fails, an exception will be thrown too. The Core may throw one of the following exceptions:
Massiv::Core::Lib::IllegalPointerConversionException : Thrown when the Massiv is unable to perform conversion from pointer to Massiv::Core::Object to pointer to object of type of the class the method is defined in. That would be Foo in the example above. There may be two reasons why this conversion can't be performed:
The conversion is ambiguous, because the object inherits the class multiple times. In the example this would mean that object foo is of type that inherits class Foo multiple times. You should avoid such inheritance hierachies, check Chapter 5, Pointers for more reasons.
The pointer used in the call points to class of incompatible type. In the example above it would mean that the object foo does not inherit class Foo. While the Massiv tries to check all potentially dangerous pointer assignments at run-time, it's not always able to determine if value assigned to a pointer actually points to an object of a compatible type, if the object is not local. Again, see Chapter 5, Pointers for in-depth information about pointers.
Massiv::Core::Lib::RemoteCallFailedException : Many reasons why this exception may be thrown exist:
The call has been cancelled. This happens when the node shuts down (or when the call timeouts, see below). Note that the remote method may have been called even if this exception is thrown.
The call timed out and has been cancelled by the Core. Something weird happened, or either the request or the reply has been thrown away because of security reasons (see Section 8.4.3, “SRPC Security and Limitations”).
The request can't be delivered to the callee object. It no longer exists, or its localization has failed (but probability of that happening is nearly zero, something like million-to-one).
The caller tried to use wrong combination of call options and call variant. Actually, the Massiv is a bit messy regarding these problems. Massiv::Core::Lib::InvalidArgumentException should be probably thrown instead. Actually, the Massiv may even fail with an assert or throw a different exception if really dumb combination of arguments is used. To be safe, always use arguments that make sense. (This is not a bug, it's a feature - you don't want to pass random arguments to the Massiv and then catch exceptions to see what happened. Bad arguments are considered internal error by many parts of the Massiv API, even if they originated in code written on top of the Core.)
The Massiv security model expects that server nodes can be trusted. However, the Massiv does not trust client nodes. For example, we can't expect that client nodes always reply to requests. Because of that reason, it's illegal to call synchronously methods of objects owned by client nodes.
The greatest advantage of synchronous RPC is obvious: its semantics is similar to that of standard local call and it's the easiest way to get call results. However, it's not recommended to use SRPC between servers too often in the Massiv because of the following reasons (most of them apply especially if you implement game-like application on top of the Massiv; you should have probably used another middleware if you wanted to implement different kind of application):
The Massiv distribution model allows large distances (and pings) between server nodes. Synchronous calls may block for quite a long time, which may harm real-time requirements of the simulation.
SRPC introduces cooperative threading into the simulation. While a stub waits for call results, other thread may (and probably will) run. The same object could be reentered by the other thread to service another request in the meantime. It's suprisingly easy to make serious and hard-to-debug errors in such environment, especially if you are not used to it. Make sure you know exactly in what state object performing a SRPC may be, and what may happen when someone wants to access the object or to call its method.
If you try to solve the problem above using a locking mechanism, be aware of potential deadlocks.
These reasons apply to client-to-server communication too. Most simulation-related requests from client should be asynchronous and the replication should be used to send presentation-related data from servers to clients.
However, there are some cases when the SRPC is really useful and should be used. One such example is the movement of entities in the Massiv Demo. When an entity wants to move to a different location, it must first make sure that it can move to the new location and then move there. All entities reserve area around them to make sure that they never collide, and both area around the old location and the new location must be reserved before the entity can actually change its position. If the movement fails, the entity remains at the old location. If the movement succeeds, the entity cancels reservations around the old location.
In the Massiv Demo we have chosen to divide the world into evenly-sized sectors. These sectors can be arbitrary distributed among the servers and entities can freely move between the sectors. This means that there are no “teleports when moving between different areas of the world” problems/hacks. However, it also makes the implementation of the entity movement a bit tricky. Even this simple collision system requires a modification of several data structures during the movement, and those data structures might be owned by different server nodes.
The implementation of do_move() method makes heavy use of the SRPC. During its exection, it does not modify entity data, and all modifications done to other data structures leave them in consistent state and are reversible. When an entity wants to move, and it's not currently moving, it simply calls do_move(). However, if it's already moving, it just remembers that it wants to move and where it wants to move to. If do_move() fails, it reverts modified data structures to their previous state. If it succeeds, it changes entity position. In both cases, if another movement was requested while do_move() had been moving the entity, it will try to move the entity again.
This implementation is optimal. If an entity is not near a boundary between sectors owned by different nodes, do_move() will finish immediately, because the local SRPC is optimized (see Section 8.5.2, “Synchronous RPC Optimizations”). Otherwise it may take a while, but the implementation keeps calls between different nodes at the minimal rate, and the movement does neither block nor interfere with execution of other entity methods.
For more details check src/demo/lib/server/entity.cpp, methods move_entity() and do_move().