Introduction
Once I came across the problem of uploading a file from a client’s application to the application server. File sizes can vary and the situation where the service could reject the file because it exceeds the maximum size of data may occur or cause a timeout. Client’s application was to remain responsive to other high priority events, such as processing events of the application server, rendering the user interface, etc., while carrying out a long-running upload file task. The task needed to be cancelled before completion to show the progress of the completion of the task. It was important for the transfer to continue from the place where an error occurred rather than from the very beginning.
Figure 1. An example of solving such problems in a well-known application.
Problem
A process needs to carry out a long-running task (a task, with an unpredictable duration) while remaining responsive to high priority events. The task should be split into smaller subtasks. The task at any time can be interrupted or suspended and then resumed.
Solution
Long-Running Active Object Pattern is the best solution for solving this one and other similar tasks.
Structure
Active objects are very similar to traditional (passive) objects. They have private fields and provide methods to operate on this data (figure 2).
Figure 2. Passive object.
The only vivid distinction lies in the fact that each active object runs in its own thread of control and the invoked methods do not block the Client (caller) but are executed asynchronously. Active objects have public interface and internal implementation. Public interface is often called proxy. It is available for the client and is responsible for accepting method calls and it has the ideally same interface as passive object. The proxy transforms the method calls into messages and puts them in a queue (message queue) and asynchronously returns possible results in the form of future objects. The message contains all the necessary information to perform the method, task priority and so on. A special object usually called Dispatcher or Scheduler dequeues and processes each incoming message from the queue according to a particular algorithm and then invokes the actual methods of the Servant. The Servant is the real method itself for implementation or in other words a passive object method that was used until the active objects pattern was applied. Active objects are inherently thread-safe by sequentially dequeuing and processing enqueued requests and always executing Servant’s methods in a single thread of control. The thread-safety of active objects largely removes the need for manual locking and mutual exclusion mechanisms (figure 3).
Figure 3. Active Object.
Client can to try to get the result of the future object at any moment. If the result has not yet been calculated, the client will be forced to suspend the execution and await the result of the execution of an active object. Alternatively there might be a case when the execution of the active object led to an error. In this case, the future object will enter the error state and will get the full information about occurred exception. The client may try to cancel the execution of the method. If this happens before the message has been processed, the dispatcher will cancel the Servant’s method and will set the state of the future object as cancelled. Possible states of the future object are shown in Figure 4.
Figure 4. Future object states.
Dynamics
There must be a dispatcher or scheduler created before proceeding any work with an active object. Dispatcher requires an algorithm (strategy) to obtain the next message from the queue. This algorithm can be set either before or during the launch dispatcher. Dispatcher, together with the proxy implements the Producer-Consumer pattern. Dispatcher shares with the proxy a common message queue and processes the incoming messages. Dispatcher can be suspended and then resumed. Figure 5 shows the activity diagram of the dispatcher.
Figure 5. Activity diagram of the dispatcher.
If one or more messages get in the queue, the dispatcher starts the working thread (workflow) to process them. If no messages are in the queue, the working thread stays in standby mode. Dispatcher, according to the selected algorithm, receives the first message from the queue and invokes its AllowInvoke()
method. This method can allow or deny the execution of the method of the Servant. Thus, the security mechanism (guard) is implemented for the safe invoke of the servant. If it is not possible to process the message at the moment, the dispatcher gets the next one from the queue. If the guard allows the execution, dispatcher invokes the method Invoke()
of the message. In case of error execution, other method Failed()
will be applied and Future object will be set to the HasFailed
state. In case of successful execution, the dispatcher sets the state of the future object as HasSuccessed
and the future objects will assign the result of execution of the method Invoke()
. It is also possible that the client can cancel the processing of the message (Cancelled
). Once the dispatcher is installed, we can perform the methods of the active objects.
Using the Code
The following classes have been developed to solve a particular problem, which is to upload a file of an indeterminate size to the application service in the background with the ability to cancel the operation:
ProcessFileByPiecesCommand
is a Servant, which implements the IServant
interface. The main objective of this class is to divide a file into equal parts and to read them continuously. The next message is created by Proxy and is added to the message queue automatically after the servant has been sent the previous piece of file to the application service (this implementation is omitted in this example). This process occurs until all parts of the file are fully read and sent or the process aborts due to the abolition of the user or when an error occurs.
ActiveObjectProxyServant
is a class-adapter to work with the objects that implement the interface IServant
, as with the active objects. Implementation of the proxy is absent in its pure form. We can use Dynamic Proxy by Castle Project or other frameworks to create proxies. This adapter can be invoked by proxy. The same adapter can be used to copy another file, the second, third and so on. Thus, the adapter has a list of future objects to hold all final and intermediate results for every method call.
ActiveScheduler
is a dispatcher to process messages. Dispatcher can execute other active objects’ messages of the system. For example, the task of uploading the file has a low priority, so if there is a new message with higher priority in the queue, it will be processed by the dispatcher on the first place. Thus the dispatcher will handle the critical tasks firstly and only then the low priority ones.
PriorityAndLIFO
is a class that implements interface IQueueSortStrategy
. It sorts the messages according to their priority and consecutive number in the queue.
Future
and its descendants are designed for storing the state and the results of the execution of the method of the active object.
ServantOperation
is a message (IOperation
, Operation
). It contains information about operation, its priority and the reference to the future object. Dynamics of the active object are shown in Figure 6:
Figure 6. Activity diagram of the active object to upload the file to the application server.
Points of Interest
There were other design patterns rather than Active Object
used during the implementation. They are: Strategy
, Factory method
, "Fail First"
(Assert, assertion), Command
, and Proxy
. During the development, the following .NET Framework features were used, in particular version 3.5: reflection, custom attributes, operator overloading, multithreading, locks (ReaderWriterLockSlim
, Monitor
), and of course, powerful object-oriented programming was present. Also the attributes of any good code were used, such as unit tests (NUnit) and code documentation.