This section deals with the advanced features the SQLSpaces have. In constrast to the operations described earlier these features are mostly not actually features that were contained in the first releases of Linda and therefore are not a common denominator of all Tuplespace implementations. However, some of these features were also added in some other implementations to increase the possibilities without changing the blackboard character of the Tuplespace idea.
In the standard model the Tuplespace server is a purely passive component of the system, which only acts on direct queries. This can sometimes be a bit annoying, e.g. if you want to get every tuple that is written into a space, but think a design that uses waitToTake is too inelegant. Of course you could use an infinite loop that takes away each tuple as soon as it arrives at the server. However, you often do not want to take away these tuples, but rather let them in the space. Indeed you could use waitToRead, but after one tuple is written into the space, the infinite loop would constantly fetch this tuple again, because after each loop it still matches the given template. Of course, this problem could be solved by using certain incrementing counters in the tuple fields that each new tuple should have, so after each loop you waitToRead for the next counter. However, this design obviously looks a bit complicated and there is another means called callbacks that satisfies such requirements.
A callback can be compared with a listener that is registered on server side. This registration contains a template tuple, a listener that should react in case of a matching operation and also a type of operation. This type is one of the following: write, delete, update, all. This listener needs to implement the interface Callback, that contains one method named call. To register a callback, you need to invoke the method eventRegister in the class TupleSpace. The return value of this method is an integer, that identifies this registration. It is called sequence number and it is needed if you want to cancel a registration (by use of the method eventDeRegister).
TupleSpace ts = new TupleSpace(); Tuple tmp = new Tuple(String.class, String.class); Callback cb = new MyCallback(); int seqNo = ts.eventRegister(Command.WRITE, tmp, cb, false);
The last parameter defines whether the client should fork a new thread to process this callback or not. If it is clear that the procession is quite fast, this should be set to false, otherwise new threads should be created in order to not block the receiving thread of the client for too long. The class MyCallback may look like this:
static class MyCallback implements Callback {
public void call(Command c, int seq, Tuple aT, Tuple bT) {
System.out.println("Action " + c.toString() + " occured!");
System.out.println("BeforeTuple:" + bT.toString());
System.out.println("AfterTuple:" + aT.toString());
}
}The assignment of the variables afterTuple and beforeTuple is depending on the operations:
| afterTuple | beforeTuple | |
| deletion (take) of tuple X insertion (write) of tuple X updating tuple X to Y | null X Y | X null X |
Normally only primitive datatypes are allowed in fields of tuples. The SQLSpaces support here all these primitive Java datatypes: boolean, byte, short, int, long, float, double, char and String (of course, String is not really a primitive datatype in Java, but despite many programming languages consider it as one ...). In addition to these SQLSpaces also support two complex datatypes: binary and xml.
Storing binary data in a field is done via byte arrays. These are then encoded into strings using the Base64 algorithm.
URL url = new URL("http://www.collide.info/logo.jpg");
InputStream stream = url.openStream();
byte[] imageBytes = new byte[stream.available()];
stream.read(imageBytes);
ts.write(new Tuple("Collide-Logo", imageBytes));XML data can be stored as org.w3c.dom.Document instances. At the moment this only is more comfortable to do the string conversion manually, but it is planned to implement special matching features for these xml fields like XPath or XQuery.
Often tuples are used as messages. However, in many cases such messages have a lifetime, after which they are invalid, outdated or somehow other worthless. In SQLSpaces tuples can also have a so-called expiration time, which defines when the server should delete them. This time is set in milliseconds which are counted from the moment of writing the tuple into the server. When a tuple expires it is deleted, which of course also triggers callbacks, that wait for matching delete events. By the use of expiration tuples can be used to facilitate regular "heartbeats" of all agents that participate in a system.
Tuple t = new Tuple("I am alive!", "temperature agent 1");
t.setExpiration(60 * 1000); // 60 seconds
ts.write(t);If you have very large amounts of tuples (over one million), the default operations will do what they should do, but maybe slower that necessary for your case. If you e.g. have many many tuples that should be fetched by one single readAll command, it is not unprobable that you get memory problems, on the server as well as on the client. To prevent this, there is a readAll method that takes additionally to the template a windowsize and returns a tuple iterator. This iterator won't fetch all tuples but only windowsize tuples at the same time. The developer won't notice this flow control mechanism, but it will use less memory and the first results will come in much quicker.
Although this readAll might uses several database queries, it is guaranteed that the tuples the iterator returns represent the state of the SQLSpaces server at the time the iterator was created. So for instance it won't return matching tuples, that were written in the space after the readAll call!
for (Tuple t : ts.readAll(templateTuple, 100)) {
ts.write(t);
}Furthermore, it maybe the case, that you don't need the whole response from the server, e.g. if you just want to delete tuples or if you want to know, how many matches there are. In these cases there are the responseless equivalents of take, takeAll and readAll that are called delete, deleteAll and count.
// delete everything, faster than taking everything ts.deleteAll(new Tuple());
The last point affects the operations, that return one tuple, i.e. read, take, waitToTake, waitToRead, delete. These operations internally use randomization to guarantee that when done iteratively they will hit eventually all matches. This randomization can be disabled so that you always get only the first hit. The methods for that have all a trailing "first" like readFirst, takeFirst, etc.
// only get the first tuple in the table ts.readFirst(new Tuple(String.class)); // later calls will all return the same tuple, if there are no changes in the space
Since SQLSpaces are based on a relational database management system, it is easy to support transactional operations. Therefore the class TupleSpace has the three methods beginTransaction(), commitTransaction() and abortTransaction(). After starting a transaction, all following operations of this connection will be handled transactionally. If then the transaction is aborted, all operations contained in it will be undone, and if it is commited all effects of the operation will be visible for other users.
TupleSpace ts = new TupleSpace();
ts.beginTransaction();
// do some stuff
if (errorOccured) {
ts.abortTransaction();
} else {
ts.commitTransaction();
}Note: Please be aware, that the exact semantics of the transaction are depending on the underlying database. Especially the isolation level has to be considered. In MySQL (i.e. InnoDB) the default transaction isolation level is REPEATABLE READ, whereas HSQL only uses READ UNCOMMITTED. If you are unsure what that means, please refer to the corresponding Wikipedia article.
The basic matching algorithm with templates of formal and actual fields is extended by some other features that are described in the following:
ts.write(new Tuple("temperature", 3));
ts.write(new Tuple("temperature", 6));
ts.write(new Tuple("temperature", 8));
ts.write(new Tuple("temperature", 12));
Field f1 = new Field("temperature");
Field f2 = new Field(Integer.class);
f2.setLowerBound(new Integer(5));
f2.setUpperBound(new Integer(10));
Tuple template = new Tuple(f1, f2);
Tuple[] tuples = ts.readAll(template);
for (Tuple t : tuples) {
System.out.println(t);
}An overall view of the matching facility is shown in this figure:

A SQLSpaces server consists of several spaces, which are disjoint tuple containers, and normally each client needs to have a logical connection to one of these. However, it is also possible to have a connection to several spaces. Such a connection is established by passing more than one space name to the constructor of TupleSpace. Queries over such a connection are interpreted as queries over the union of all corresponding spaces. In this case write and update commands need also the space name, so the server is able to assign the command to the correct space. If this space is ommitted, an exception is thrown.
TupleSpace ts = new TupleSpace("one space", "another space");
// this is executed on both spaces
Tuple t = ts.read(new Tuple(String.class));
// writes somethin in the second space
ts.write(new Tuple("Hello", "Adam"), "another space");
// no clear space assignment --> exception!!
ts.write(new Tuple("Hello", "Stefan"));In the delivery state the SQLSpaces have only a very basic user management. This means each connection call (i.e. call of the TupleSpace constructor) can have a username and password, which are interpreted like this: If a username is new, the credentials are simply stored, but if a username is already known, the password is checked. Once a user got a connection he has full access to the server.
It is also possible to constrict the rights of single users, if the rightsmanagement is enabled in the global configuration file (see chapter Configuration). Then the rights model consists of five different rights:
If a user tries to do an action, that he is not allowed to do, a TupleSpaceException is thrown. If a user has manage rights over a space, he is able to set space-based rights for "his" space for other users using the setUserRights method of the class TupleSpace. Therefore he needs to be connected to "his" space with this instance of TupleSpace.
A normal tuplespace has no memory of prior states of it, i.e. there is no history saved and no undo function is available. In order to save a certain state of a space it is possible to create version of this space. The versioning systems is closely related to the idea of the versioning system Subversion. By calling the method createNewVersion of a TupleSpace instance, a new version of the space the instance is currently connected to is created. All versions have a major and minor version number (starting with 1.0) and so a new version will have an incremented minor version by default.
For handling the versions, the class TupleSpace has several methods, that all should speak for themselves: createNewVersion, editSpaceVersion, getAllVersions, getAllUserVersions, getCurrentVersion, getCurrentUserVersion and switchToVersion. If there are several versions of a space, you always connect by default to the latest one. If you want to access a specific version, you first have to switch to this one.
Since the semantics of merging two versions is quite domain-dependant, there is no generic support for merging, so this has to be implemented by the programmer himself.
Often it is necessary to have a direct look inside the server. In this case it is not recommended to inspect the database tables itself, because the technical representation of the tuples and spaces are not really designed for maximum human readability. However, an SQLSpaces user can use a web-based UI for investigating the contents of the SQLSpaces. Therefore the built-in webserver has to be enabled (see chapter Configuration) and the investigator.war has to be deployed, i.e. it needs to be in the SQLSpaces folder during startup. By default this server runs on port 8080 and to access the so-called SQLSpaces Investigator the URL http://localhost:8080/investigator is used.
The first view of the investigator should be similar to the following:

If you move the cursor to the left side of the Investigator, a navigation bar appears with all combo boxes that are needed for browsing through the different spaces, versions and subspaces. A subspace in this context is defined by all tuples of one space and version, that have the same signature (i.e. same types of the fields). This division into subspaces was simply needed to display the contents in a tabular way. On the right side of the Investigator a list of all tuples of the selected subspace is shown, which then can be explored.

Another representation of the SQLSpaces server is called Vizard and can be accessed by clicking on the "Vizard" string in the upper head of the page. The Vizard is more a synchronous visualization that displays in real-time the communication between the connected clients in an animated way. Since it is based on JavaFX, you need a Java-Plugin installed in your browser. Note: The use of the Vizard is not recommended if you have many clients and really many tuples, since it easily gets slow and confusing. You should have less than 10 clients and less than 500 tuples as a rule of thumb.
The different types of the actions (write, take, update ...) are visualized by different colors. This Vizard is showing a client that currently writes a tuple:

And this Vizard is showing a client that takes a tuple:
