laforge49 270005CXQG 2,808 Views
I see two kinds of actor models, the traditional actor model used by virtually all actor frameworks and the robust actor model used by JActor2 and Microsoft's Orleans project. But before we get into that, we need to talk just a little bit about systems like Python Twisted and Node.js. Both of these systems are event-based application platforms, where the events are processed by a single thread but with many services operating on separate threads. Both Twisted and Node.js deliver high-performance and, being largely single-threaded, provide implicit thread safety without the need for locks.
The key differentiator between actors and twisted/node.js is the extensive use of multi-threading in actor frameworks. All these frameworks work by message/event passing. But the use of events in a single threaded environment is proven technology. With actors, we do have the potential of making better use of today's multi-core processors, but single-threaded processes typically run faster than all but the most well-designed multi-threaded processes. And there is a significant down-side in the use of actors in terms of coupling arising from multiple event queues (one per actor) and out-of-order event processing.
The issue arises in part from the obligation of an actor not to block its thread, as actor platforms often run on a minimum number of threads and such blocking could easily lead to thread starvation of other actors and even deadlocks. Actors do not block when passing events to other actors, except for 2-way messages (request/response). The common actor model only supports replies, if it does so at all, by either blocking the actor's thread or by event filters.
What happens is that when one actor receives a request that requires consultation with other actors, it will temporarily select only events from the other actors it is consulting with until it receives the responses it needs to complete that initial request. Event processing then is not always in the order the events are received, the events being selected being based on the actor's state. This means that actors will often be coupled to other actors, with expectations as to how those other actors will behave. And coupling is fine, if it is well documented, isolated to small sub-systems, and the project is reasonably short-lived and/or reasonably stable.
The actor model used by both JActor and Orleans is a bit different. One of the key differences is that non-blocking 2-way messaging (request/response) is fully supported. Events are still supported, but are used mostly for things like notifications. And for 2-way messages, the actor's expectation is only that a response will be received within a reasonable time. And if an error occurs, then that error serves as the response.
With JActor/Orleans, responses are dispatched quite differently than events and requests. In JActor, a callback is provided when sending a request and that callback will be executed on the actor's thread. (Keeping in mind that the thread of an actor may change over time, but it only ever has one at most.) In contrast, any distinction in the common actor model between a request and a response must be done by the application itself. And to keep the application complexity reasonable, this generally means event filtering.
This is a really wonderful talk by someone who as worked with actors for a very long time. He makes a strong case for why message filtering can not reasonably be avoided when using the common actor model.
Here I explain the common actor model by way of an analog implementation that uses locks. My reason for doing this is to get people in a strong enough position that they can reason about actors. Actors are too often explained axiomatically, which leaves people with only a shallow understanding of actors.
For high-performance, array-backed data structures are generally recommended. On the other hand, inserting into a large array isn't the fastest thing. BListJid uses small arrays (max size is 27) in a balanced tree structure to support fast updates and super fast incremental deserialization/reserialization.
As with other Jid classes, a registered factory object is mandatory. The JidFactories class registers 8 different types of BListJid, one of which is a list of integers.
The iAdd method creates an inserts a new Jid object at a given location, where 0 is the first location, 1 is the second, -1 is the last location, -2 is the next-to-last location, etc.
BListJid supports only homogenous lists, where all the entries are of the same class. The advantage is that the serialized data is smaller than it would otherwise be and performance is a bit better as well. But sometimes we need a list of homogenous objects. This can be achieved with ActorJid, which is a superclass of RootJid and which can hold any object that subclasses Jid.
Being able to constrain the types used in a data structure can be important, and this is one of the advantages of using UnionJid instead of ActorJid. It also results in serialized data that is a bit more compact and a bit faster to deserialize. It also supports recursive types, which is what we will be looking at in this next example.
In the above code we define the type union1, which can hold either a StringJid or a Jid of type unions. And the type unions is defined as a BListJid whose elements are of type union1. We then proceed to create a list whose first element is a StringJid with a value of "a" and whose second element is a list whose first element is a StringJid with a value of "b".
To run JASocket you will need the JAR files for compatible versions of JActor, JID, JASocket and JFile. Download these projects from here and extract the jar files to a common directory, in our case c:\jaconfig. You will also need jar files from slf4j, sshd, joda-time and jline.
Next you need a few shell scripts. Here are some which work with windows:
Your directory should now look something like this:
The basic configuration of JAConfig is handled by JACNode. If you are familiar with JASocket, this code should be largely self explanatory:
Open a command window, go to the directory containing the jar files (you will need to be able to write to this directory) and enter the node command:
Now log in with the name admin and password admin.
Enter help to see the list of commands. There is nothing new here, as the commands are all implemented in JASocket:
Now we can use the localServers to see what is running on the local node:
We see that the config, kingmaker, quorum and ranker servers were started by node (class JACNode), while hostManager was started by kingmaker.
Authentication of operator passwords is handled by the config server, so a good starting point is to see what commands this server supports:
The very first thing we should do is to change the password for admin, and then test it.
The config database now holds the new admin password.
Accounts for operators are created just by assigning a password. Any operator can do this, but you will need the admin password. Note however that there is no difference between creating an operator account and changing another operator's password.
A quorum requires the participation of N/2+1 hosts. So with one host, you need one host to achieve a quorum. With two hosts you need both hosts running to achieve. But with three hosts, only two hosts are needed. Fault tolerance then is only achieved when running with 3 or more hosts.
The quorum server tracks the number of hosts that are available and gets the total host count from the config database--which must be manually assigned.
As soon as the total host count is set to 1 we achieve a quorum. The kingmaker, which was listening to the quorum server for a quorum notification, immediately starts up the cluster manager server.
Setting the totalHostCount back to 2 means we loose quorum and clusterManager stops running:
JAConfig has an alternate implementation of SSH server that subclasses HostServer. This allows us to use the host manager to run one copy of the SSH server on every host.
The port number used by the SSH server, 8889, is specified in the assignment to hostManager.ssh.
Multiple Nodes per Host
Starting a second node on the same host, we need an unused port. But when specifying the port, a console is not opened.
When more than one node runs on the same host, only one instance of the host manager is run, as well as only one instance of each host server.
Running a node on another host establishes quorum. In this case we will explicitly specify a port of 8880 to prevent the opening of another console:
The new host runs an instance of host manager, and an instance of each of the host servers. A single instance of the cluster manager is now running in the cluster.
As we define new cluster servers, one instance of each is run on the cluster. The nodes chosen to run the new servers are those least loaded.
Taking down the second node on the first host, the load has been rebalanced between the two remaining nodes.
laforge49 270005CXQG 2,347 Views
Super fast persistent data structures composed with pre-build classes is a step in the right direction, but is still a step backwards from what we are used to being able to do. Being able to easily write custom classes that are just as fast is also important.
Custom JID classes are subclasses of AppJid, which provides a persistent tuple with any number of entries of different types. To illustrate this we will look at a simple User class which has two persistent values, a name and an age.
User accesses the Jid objects in its persistent tuple using the protected method _iGet(int). But this tuple must have a StringJid as its first element and an IntegerJid as its second element. This requirement is met by using a factory object to create and initialize User objects.
The constructor UserFactory defines the Jid types of the elements of the persistent tuple, as well as specifying "User" as the type of Jid.
To test this we will create a User object, initialize its tuple entries, serialize it and then deserialize it.
Our test begins the same way as the test we wrote before, with the creation and initialization of a JAFactory object and then the creation and registration of a number of useful Jid factories.
We need to register the UserFactory with our JAFactory object. It is registered with the type name of "User".
Finally, we create a new RootJid, load it with the data in serializedData0 and then validate the name and age.
laforge49 270005CXQG 2,328 Views
"An Object Oriented Model for Robust Multi-threaded Programming", which introduces JActo2, has been published by Java Magazine: http://javamag.org/developing-java-applications-issue-on-java-is-out/
laforge49 270005CXQG 2,291 Views
All Jid objects have the same superclass, Jid, which in turn is a subclass of JLPCActor, which means that all Jid objects are actors.
So far, we have not given any examples of a Jid object initialized with a Mailbox, which means that none of the Jid objects shown are able to send or process messages. But initializing a Jid object with a Mailbox is easy to do and most of the methods in the JID API have corresponding Request classes. Also, the Jid objects in a Jid tree structure will always share the same mailbox, so an application Jid never needs to send Requests to the Jid objects in its tuple--it can just call their methods directly.
In the code below we create a RootJid with a JidString set to "Hello world!", serialize it and then deserialize it. Many of the method calls shown earlier have been replaced with request messages to illustrate their use. However, the serialization and deserialization logic still uses method calls, which means that thread safety is the responsibility of the application developer for these operations. (Thread safety can always be achieved by performing these operations within an actor which uses the same mailbox as the Jid Actor.)
laforge49 270005CXQG 2,165 Views
BMapJid is the base class for balanced tree maps which, like bListJid, provide for super-fast incremental deserialization and reserialization. BMapJid has 3 subclasses, IntegerBMapJid, LongBMapJid and StringBMapJid, which support Integer, Long and String keys respectively.
BMapJid<KEY_TYPE, VALUE_TYPE> is a collection of MapEntry objects, where MapEntry holds a key/value pair. BMapJid is effectively a sorted list of MapEntry objects, with fast indexing supporting the same methods as BListJid exception only the iAdd and iAddBytes methods are not supported. But access by key is also supported. These additional methods include
BMapJid objects are created using a registered factory object. As a convenience, JidFactories registers 24 such factory objects, though it is easy enough to define register additional factory objects using the IntegerBMapJidFactory, LongBMapJidFactory and StringBMapJidFactory classes.
laforge49 270005CXQG 2,139 Views
laforge49 270005CXQG 2,035 Views
Recently posted on Code Plex: Microsoft's Orleans, now available as a preview. http://lnkd.in/dh4G-zq. Orleans is a validation of the actor model used by JActor2. For example, Orleans actors, like JActor actors, lack the failure modes of traditional actors and consequently do not have monitors. And really there is no good reason for actors to fail except that the traditional actor model makes it difficult to maintain systems of actors that do not experience deadlocks. More on this later.
laforge49 270005CXQG 2,018 Views
When deserialization and reserialization are reasonably fast, their use to make deep copies of data structures becomes a reasonable approach. And Jid provides the CopyJid request, which is supported by all Jid actors. The GetSerializedBytes request is similar, in that it returns a byte array holding the serialized data of a Jid actor, and it too is supported by all Jid actors.
Making copies of data structures is important in a multithreaded application when it can be used to reduce the number of messages sent between threads. Conversely, being able to add a copy of a Jid actor to a collection may be even more useful. This is done by first getting the byte array of a Jid's serialized data and passing it in one of several requests which then create a copy of that Jid and add it to their collection. These requests include SetActorBytes for RootJid, ActorJid and UnionJid, IAddBytes for BListJid and KMakeBytes for BMapJid.