I had a debate over a beer with a friend about volatile arrays. I forgot who was trying to prove what but I did come across an interesting quirk.
We were talking about how adding the volatile keyword to a reference means that threads will always see the same thing once the reference had been set. But what if that reference were an array? Would the threads agree on the elements of the array? (The answer is "not necessarily" as pointed out in my blog last year).
But I had trouble demonstrating this with some simple, multi-threaded code. Annoyingly, my threads kept agreeing on the elements of the array. Maybe it was the chip architecture, I lazily thought.
Then it occurred to me. I was putting some System.out.println statements in my code to help me try to solve the problem. This, of course, was the problem. The reason why is that by introducing this logging, the threads are synchronizing on the same object - and as I mentioned in another blog post, this ensures "all threads see the most up-to-date values of shared mutable variables [when] the reading and writing threads ... synchronize on a common lock" (Java Concurrency in Practice, p37).
So, what was introducing this mysterious common lock? Why, System.out.println itself! The println method is synchronized so both threads synchronize on the common lock of the System.out object.
This reminded me of a time when a colleague was wondering why adding System.out.println appeared to be altering the behaviour of a piece of code. Stumped, I just thought it might have introduced a timing issue to his multi-threaded code. Now I know better.
But it begs the question: how much code is out there that is not properly multi-threaded with only some ad hoc logging holding it together?
Tuesday, October 4, 2011
Tuesday, August 23, 2011
Optimising Collection Access
I had an interview question recently that asked me to write code to access as efficiently as possible a collection while multiple threads intensively read and write to it.
Giving the caveat that everything should be subject to stress tests anyway, I went ahead and used a java.util.ConcurrentHashMap. I knew this is a very efficient piece of code and I wouldn't have to worry about synchronization.
When I got home, I wondered about my answer and started to put together JMeter tests to establish some empirical data. JMeter provides a lot of the plumbing for stress tests that makes it superior to just writing your own class.
[Incidentally, if you do write your own executable classes for stress tests, don't forget to pause for 4 seconds before running your code. Any object created in the first 4s of the JVM's life is not eligible for an optimization called Biased Locking (see this email from a Sun engineer). Biased Locking is an optimization for the use case where a lock is mostly sought by just one thread. In this case, the favoured thread finds attaining the lock is cheap.]
First, if you wish to plug your code into JMeter, it's helpful to have a superclass that implements the JMeter interface:
To make JMeter pick up my classes, I put this in $JMETER_HOME/bin/jmeter.properties:
search_paths=/Users/henryp/Documents/workspace/TestMaven/target/TestMaven-0.0.1-SNAPSHOT.jar
This way, I only had to execute Maven to compile my code and rebuild the JAR. It would be nice to hot-deploy the code but I don't know if JMeter supports this. So, instead I have to restart JMeter each time I change the code :-(
My code that iterates over the entry set of a ConcurrentHashMap was producing a throughput of about 25 000/s when using 100 read-threads while another 10 write-threads added an element to the collection with a constant throughput of 100/s (I've not shown the code for this).
Not bad. But could it be improved easily? Let's try using a java.util.List (perhaps surprisingly, ArrayList and LinkedList gave me similar figures) and use read/write locks to synchronize access. So, the read-code looks something like this:
and the write-code looks something like this:
The subsequent JMeter test (100 read thread, 10 write threads with a throughput of 100/s) indicated that this was about twice as fast as using ConcurrentHashMap.
What's more, it seemed to suffer a far smaller degradation in performance when I increased the throughput of write-threads to 1000/s.
Conclusion: they're both pretty nifty and perform at the same order of magnitude. Your mileage may vary but it seems java.util.concurrent.locks.ReadWriteLock is a more efficient approach.
Giving the caveat that everything should be subject to stress tests anyway, I went ahead and used a java.util.ConcurrentHashMap. I knew this is a very efficient piece of code and I wouldn't have to worry about synchronization.
When I got home, I wondered about my answer and started to put together JMeter tests to establish some empirical data. JMeter provides a lot of the plumbing for stress tests that makes it superior to just writing your own class.
[Incidentally, if you do write your own executable classes for stress tests, don't forget to pause for 4 seconds before running your code. Any object created in the first 4s of the JVM's life is not eligible for an optimization called Biased Locking (see this email from a Sun engineer). Biased Locking is an optimization for the use case where a lock is mostly sought by just one thread. In this case, the favoured thread finds attaining the lock is cheap.]
First, if you wish to plug your code into JMeter, it's helpful to have a superclass that implements the JMeter interface:
package com.henryp.stress;Then, subclass it. For my ConcurrentHashMap, it looks something like this:
import org.apache.jmeter.protocol.java.sampler.JavaSamplerClient;
import org.apache.jmeter.protocol.java.sampler.JavaSamplerContext;
import org.apache.jmeter.samplers.SampleResult;
public abstract class AbstractSamplerClient implements JavaSamplerClient {
public SampleResult runTest(JavaSamplerContext javaSamplerContext) {
SampleResult result = new SampleResult();
result.sampleStart();
doSample();
result.sampleEnd();
result.setSuccessful(true);
result.setResponseCodeOK();
result.setResponseMessageOK();
return result;
}
protected abstract void doSample();
}
package com.henryp.stress;The MapPopulator is just a utility that instantiates a Map of a user-defined type and populates it with a user-defined number of elements. Setting the default parameters in that class looks something like:
import java.util.Map;
import org.apache.jmeter.config.Arguments;
public abstract class AbstractMapSamplerClient extends AbstractSamplerClient {
protected static MapconcurrentHashMap;
protected final MapPopulator mapPopulator = new MapPopulator();
@Override
public Arguments getDefaultParameters() {
return mapPopulator.getDefaultParameters();
}
@Override
protected void doSample() {concurrentHashMap
for (Map.Entryentry : .entrySet()) {
// NoOp while I think of something to do here
}
}
@Override
public synchronized void setupTest(JavaSamplerContext javaSamplerContext) {
if (concurrentHashMap == null) {
System.out.println("setupTest: " + javaSamplerContext);
concurrentHashMap = mapPopulator.makePopulatedMap(javaSamplerContext);
}
}
@Override
public void teardownTest(JavaSamplerContext javaSamplerContext) {
concurrentHashMap = null;
}
}
.
.
.
protected static final int DEFAULT_COLLECTION_SIZE = 100;
protected static final String COLLECTION_SIZE_KEY = "COLLECTION_SIZE_KEY";
public Arguments getDefaultParameters() {
System.out.println("getDefaultParameters");
Arguments arguments = new Arguments();
Argument argument = new Argument();
argument.setName(COLLECTION_SIZE_KEY);
argument.setValue("" + DEFAULT_COLLECTION_SIZE);
arguments.addArgument(argument );
return arguments;
}
.
.
.
To make JMeter pick up my classes, I put this in $JMETER_HOME/bin/jmeter.properties:
search_paths=/Users/henryp/Documents/workspace/TestMaven/target/TestMaven-0.0.1-SNAPSHOT.jar
This way, I only had to execute Maven to compile my code and rebuild the JAR. It would be nice to hot-deploy the code but I don't know if JMeter supports this. So, instead I have to restart JMeter each time I change the code :-(
My code that iterates over the entry set of a ConcurrentHashMap was producing a throughput of about 25 000/s when using 100 read-threads while another 10 write-threads added an element to the collection with a constant throughput of 100/s (I've not shown the code for this).
Not bad. But could it be improved easily? Let's try using a java.util.List (perhaps surprisingly, ArrayList and LinkedList gave me similar figures) and use read/write locks to synchronize access. So, the read-code looks something like this:
static ReadWriteLock readWriteLock;
@Override
protected void doSample() {
Lock readLock = readWriteLock.readLock();
try {
readLock.lock();
for (String aValue : list) {
// no-op
}
} finally {
readLock.unlock();
}
}
and the write-code looks something like this:
private final AtomicInteger callId = new AtomicInteger();
.
.
.
@Override
protected void doSample() {
Lock writeLock = readWriteLock.writeLock();
try {
writeLock.lock();
String aValue = this.toString() + callId.getAndIncrement();
SingleListReaderWithRWLocksSamplerClient.list.add(aValue);
} finally {
writeLock.unlock();
}
}
The subsequent JMeter test (100 read thread, 10 write threads with a throughput of 100/s) indicated that this was about twice as fast as using ConcurrentHashMap.
What's more, it seemed to suffer a far smaller degradation in performance when I increased the throughput of write-threads to 1000/s.
Conclusion: they're both pretty nifty and perform at the same order of magnitude. Your mileage may vary but it seems java.util.concurrent.locks.ReadWriteLock is a more efficient approach.
Friday, August 12, 2011
Tomcat/Maven plugin
My consultancy contract has come to an end so now I have time to attend my much neglected blog.
In the last few months, I have been Maven-izing and modularizing a legacy application. (Architectural tip: err on the side of being too modularized. It's much easier to fuse modules together than to split them.)
Anyway, one of the jobs was to improve the developer experience by having a web app running via Maven. The advantages include the project working straight out of the box with no awkward manual configuration. But it was only partially working with the Tomcat Maven Plugin. Some parts of the page were not loading because of NullPointerExceptions in our code. And yet, when I pointed a standalone version of Tomcat at the same exploded WAR, everything worked.
Using Firebug's debugger and single-stepping through Javascript code revealed nothing amiss. But stepping through code of the Tomcat embedded in the Maven plugin, I found this in org.apache.catalina.authenticator.AuthenticatorBase:
Further investigation shows that the Maven Tomcat Plugin uses Tomcat 6.0.29 (you can tell this by looking at the plugin's pom.xml). Our non-Maven Tomcat was 6.0.14.
What could be different? The change of a patch version number suggests bug-fixes and not new functionality. And what is this changeSessionIdOnAuthentication field?
It happens that we had a bespoke security plugin which extends Tomcat's AuthenticatorBase and authenticates on every HTTP call. With the default value of changeSessionIdOnAuthentication being true, this was causing the session to be discarded on the 6.0.29 Tomcat instance and so all fields in the session object to be uninitialized, ergo the NullPointerExceptions.
The changeSessionIdOnAuthentication field was added some time between releases 6.0.14 and 6.0.29. According to the Tomcat documentation, it is used "to prevent session fixation attacks".
These attacks are when somebody sends you a link to a website for which you have an account. The link includes the JSESSIONID that the attacker has been given by the website. You enter your credentials and the HTTP session associated with this JSESSIONID now carries some flag to say you have logged in. After this, the person who sent you the link can go to the website and as long as he is using the same JSESSIONID as you, he too can see your account since the associated HTTP sessions says you are authenticated.
The changeSessionIdOnAuthentication field being set to true means that as soon as you log in, you get a new JSESSIONID and the attacker is foiled.
Since our developers don't need to worry about this, we turned the functionality off in the server.xml file that the Maven plugin is told to use with:
<host name="localhost"... >
.
.
.
<valve classname="our.implementation.of.AuthenticatorBase" changeSessionIdOnAuthentication="false"...
In the last few months, I have been Maven-izing and modularizing a legacy application. (Architectural tip: err on the side of being too modularized. It's much easier to fuse modules together than to split them.)
Anyway, one of the jobs was to improve the developer experience by having a web app running via Maven. The advantages include the project working straight out of the box with no awkward manual configuration. But it was only partially working with the Tomcat Maven Plugin. Some parts of the page were not loading because of NullPointerExceptions in our code. And yet, when I pointed a standalone version of Tomcat at the same exploded WAR, everything worked.
Using Firebug's debugger and single-stepping through Javascript code revealed nothing amiss. But stepping through code of the Tomcat embedded in the Maven plugin, I found this in org.apache.catalina.authenticator.AuthenticatorBase:
if (session != null && changeSessionIdOnAuthentication) {
Manager manager = request.getContext().getManager();
manager.changeSessionId(session);
request.changeSessionId(session.getId());
}
So, session IDs were changing on each call. Indeed, Firebug did show that each AJAX call had a different JSESSIONID.Further investigation shows that the Maven Tomcat Plugin uses Tomcat 6.0.29 (you can tell this by looking at the plugin's pom.xml). Our non-Maven Tomcat was 6.0.14.
What could be different? The change of a patch version number suggests bug-fixes and not new functionality. And what is this changeSessionIdOnAuthentication field?
It happens that we had a bespoke security plugin which extends Tomcat's AuthenticatorBase and authenticates on every HTTP call. With the default value of changeSessionIdOnAuthentication being true, this was causing the session to be discarded on the 6.0.29 Tomcat instance and so all fields in the session object to be uninitialized, ergo the NullPointerExceptions.
The changeSessionIdOnAuthentication field was added some time between releases 6.0.14 and 6.0.29. According to the Tomcat documentation, it is used "to prevent session fixation attacks".
These attacks are when somebody sends you a link to a website for which you have an account. The link includes the JSESSIONID that the attacker has been given by the website. You enter your credentials and the HTTP session associated with this JSESSIONID now carries some flag to say you have logged in. After this, the person who sent you the link can go to the website and as long as he is using the same JSESSIONID as you, he too can see your account since the associated HTTP sessions says you are authenticated.
The changeSessionIdOnAuthentication field being set to true means that as soon as you log in, you get a new JSESSIONID and the attacker is foiled.
Since our developers don't need to worry about this, we turned the functionality off in the server.xml file that the Maven plugin is told to use with:
<host name="localhost"... >
.
.
.
<valve classname="our.implementation.of.AuthenticatorBase" changeSessionIdOnAuthentication="false"...
Thursday, August 4, 2011
In praise of checked exceptions
I am swimming against the tide but I quite like checked exceptions - when done properly. I find them self-documenting and they force engineers to consider the possibility that their request cannot be serviced.
My partner, who is a lawyer but a wannabe geek, laughs when I tell her that lots of the problems I encounter at work are because somebody did not consider a certain set of circumstances. She tells me that in law, it is a very poor contract that does not cover all eventualities.
When I discuss my unpopular view with other engineers, I often hear that if you can't do anything then you have no choice but to re-throw an Exception. This is true but people often underestimate the number of options still open to them upon catching one.
For example, in the event of a database deadlock, one of the threads will immediately be rolled back. The JDBC driver will throw an SQLException but then that thread can try again.
To be fair, Java's JDBC API has left something to be desired. For most JDBC drivers, an update that violates a constraint and is never going to succeed throws the same exception as one that was deadlocked and might work upon a retry. (A class was added to the SQLException hierarchy to address this - java.sql.SQLRecoverableException - but it only appeared as late as JDK 1.6).
I am content to adopt the house-style. If my client prefers RuntimeExceptions then so be it. But an area where I feel checked exceptions should be mandatory is when one team creates an API for another.
A problem I saw recently was when the core team wrote a Java API and published it via Java RMI. A second team called that API but upon encountering a non-deterministic deadlock was surprised when the JVM barfed with an Error. It transpired that DeadlockLoserDataAccessException was not in their classpath - a Spring class that was thrown when Spring converted the SQLException to something a little more descriptive.
With checked exceptions, APIs are not only somewhat self-documenting but better define what the client needs in its classpath.
This proposal is not a panacea - objects passed over the wire might reference non-standard implementations. For instance, objects re-hydrated with Hibernate might have Hibernate implementations of standard Java collection interfaces (you might want to look at Gilead to clean up the object graph in this case).
[Interestingly, the proposal to use Gilead on this particular project also prompted a discussion on what was reasonable tech debt. Not all debt is bad - a mortgage allowed me to buy my house. Only when it becomes unmanageable is it a problem. When the team who wrote the API that passed Hibernate re-hydrated objects across the wire first started coding, they were the only clients of it. Therefore, they considered the use of Gilead as unnecessary. "Do the simplest thing that works," they said and put Hibernate in their client's classpath. The problem became a little more intractable when a second team used the API since they used a different version of Hibernate.]
One last point: if you publish APIs and both you and your clients use Maven, you can have the pom.xml of the API module to pull in any necessary third-party libraries as transitive dependencies. However, do remember that not all teams use Maven. What's more, not all APIs use just Java.
My partner, who is a lawyer but a wannabe geek, laughs when I tell her that lots of the problems I encounter at work are because somebody did not consider a certain set of circumstances. She tells me that in law, it is a very poor contract that does not cover all eventualities.
When I discuss my unpopular view with other engineers, I often hear that if you can't do anything then you have no choice but to re-throw an Exception. This is true but people often underestimate the number of options still open to them upon catching one.
For example, in the event of a database deadlock, one of the threads will immediately be rolled back. The JDBC driver will throw an SQLException but then that thread can try again.
To be fair, Java's JDBC API has left something to be desired. For most JDBC drivers, an update that violates a constraint and is never going to succeed throws the same exception as one that was deadlocked and might work upon a retry. (A class was added to the SQLException hierarchy to address this - java.sql.SQLRecoverableException - but it only appeared as late as JDK 1.6).
I am content to adopt the house-style. If my client prefers RuntimeExceptions then so be it. But an area where I feel checked exceptions should be mandatory is when one team creates an API for another.
A problem I saw recently was when the core team wrote a Java API and published it via Java RMI. A second team called that API but upon encountering a non-deterministic deadlock was surprised when the JVM barfed with an Error. It transpired that DeadlockLoserDataAccessException was not in their classpath - a Spring class that was thrown when Spring converted the SQLException to something a little more descriptive.
With checked exceptions, APIs are not only somewhat self-documenting but better define what the client needs in its classpath.
This proposal is not a panacea - objects passed over the wire might reference non-standard implementations. For instance, objects re-hydrated with Hibernate might have Hibernate implementations of standard Java collection interfaces (you might want to look at Gilead to clean up the object graph in this case).
[Interestingly, the proposal to use Gilead on this particular project also prompted a discussion on what was reasonable tech debt. Not all debt is bad - a mortgage allowed me to buy my house. Only when it becomes unmanageable is it a problem. When the team who wrote the API that passed Hibernate re-hydrated objects across the wire first started coding, they were the only clients of it. Therefore, they considered the use of Gilead as unnecessary. "Do the simplest thing that works," they said and put Hibernate in their client's classpath. The problem became a little more intractable when a second team used the API since they used a different version of Hibernate.]
One last point: if you publish APIs and both you and your clients use Maven, you can have the pom.xml of the API module to pull in any necessary third-party libraries as transitive dependencies. However, do remember that not all teams use Maven. What's more, not all APIs use just Java.
Sunday, April 24, 2011
To Infinispan and Beyond!
I've been playing for the last few weeks with JBoss's Infinispan - a distributed cache and the successor to JBossCache.
It's a nice piece of technology. But for our needs, it may be inappropriate.
It may be OK for storing users' HTTP sessions and the like. But we were thinking about storing data for which integrity is absolutely essential (bond orders that run typically into the billions of dollars). Let me run through a small number of arguments that made me think Infinispan is not for us.
The Infinispan FAQ says it uses MVCC to optimize performance by having write locks not block read locks. However, dig deeper and you see this is not MVCC as most people who are familiar with Oracle and Postgres would know it. In these database implementations, a reading thread works with a snapshot of data. If a writing thread updates that row in the database, then that's OK. The reading thread continues with its snapshot. This is great since nobody is blocking anybody and it can improve performance.
However, with Infinispan, this is not quite what is happening. It's true that write threads do not block read threads. But the data in question may be mutable.
In the Infinispan mailing lists, we read:
"[I]n this case you may well see the change before [a write transaction] commits, we don't explicitly clone or copy mutable objects ... I suppose though we could add the ability to defensively copy mutable objects, but we'd need a way of knowing which are immutable, etc. Also, this would be more expensive, depending on the size of the atomic map."
A quick-and-dirty test I wrote demonstrates this:
The line in bold is where the test fails. The snapshot the read-thread has is the same as the write-thread, even though it read the object from the cache first. This is simply due to the fact that both read- and write-threads have the same object reference.
Note, if the write operations were rolled back, the read-thread would still see the change it had made. In fact, the whole cache from this point on would hold incorrect data.
This is not dissimilar to Hibernate updating an object (eg, assigning a primary key or a timestamp) even when the DB transaction it was in was rolled back. I've seen this happen when there were DB deadlocks and my transaction was chosen as the victim.
There are many other reasons why I think business critical data should not be put into Infinispan. I hope to present them in upcoming blog entries.
It's a nice piece of technology. But for our needs, it may be inappropriate.
It may be OK for storing users' HTTP sessions and the like. But we were thinking about storing data for which integrity is absolutely essential (bond orders that run typically into the billions of dollars). Let me run through a small number of arguments that made me think Infinispan is not for us.
The Infinispan FAQ says it uses MVCC to optimize performance by having write locks not block read locks. However, dig deeper and you see this is not MVCC as most people who are familiar with Oracle and Postgres would know it. In these database implementations, a reading thread works with a snapshot of data. If a writing thread updates that row in the database, then that's OK. The reading thread continues with its snapshot. This is great since nobody is blocking anybody and it can improve performance.
However, with Infinispan, this is not quite what is happening. It's true that write threads do not block read threads. But the data in question may be mutable.
In the Infinispan mailing lists, we read:
"[I]n this case you may well see the change before [a write transaction] commits, we don't explicitly clone or copy mutable objects ... I suppose though we could add the ability to defensively copy mutable objects, but we'd need a way of knowing which are immutable, etc. Also, this would be more expensive, depending on the size of the atomic map."
A quick-and-dirty test I wrote demonstrates this:
package org.infinispan.replication;
import static org.testng.AssertJUnit.assertNull;
import java.util.concurrent.CountDownLatch;
import javax.transaction.HeuristicMixedException;
import javax.transaction.HeuristicRollbackException;
import javax.transaction.NotSupportedException;
import javax.transaction.RollbackException;
import javax.transaction.SystemException;
import javax.transaction.TransactionManager;
import org.infinispan.Cache;
import org.infinispan.config.Configuration;
import org.infinispan.test.MultipleCacheManagersTest;
import org.infinispan.test.TestingUtil;
import org.infinispan.test.AbstractCacheTest.CleanupPhase;
import org.testng.AssertJUnit;
import org.testng.annotations.Test;
@Test(groups = "functional", testName = "replication.SyncReplLockingAtomicityPHTest")
public class SyncReplLockingAtomicityPHTest extends MultipleCacheManagersTest {
private static final String CACHE_NAME = "testcache";
private static final String k = "key", v = "value";
public SyncReplLockingAtomicityPHTest() {
cleanup = CleanupPhase.AFTER_METHOD;
}
protected Configuration.CacheMode getCacheMode() {
return Configuration.CacheMode.REPL_SYNC;
}
protected void createCacheManagers() throws Throwable {
Configuration cfg = getDefaultClusteredConfig(getCacheMode(), true);
cfg.setLockAcquisitionTimeout(500);
createClusteredCaches(2, CACHE_NAME, cfg);
}
public void testUpdateWhileReadLock() throws Exception {
final Cache cache = cache(0, CACHE_NAME);
final CountDownLatch latchAfterRead = new CountDownLatch(1);
final CountDownLatch latchBeforeCommit = new CountDownLatch(1);
final ReaderThread readerRunnable = new ReaderThread(k, cache, latchAfterRead, latchBeforeCommit);
final UpdateThread updateRunnable = new UpdateThread(k, cache);
updateWhileReadTX(latchAfterRead, latchBeforeCommit, readerRunnable, updateRunnable);
}
private void updateWhileReadTX(
CountDownLatch latchAfterRead,
CountDownLatch latchBeforeReadCommit,
ReaderThread readerRunnable,
UpdateThread updateRunnable) throws SecurityException, IllegalStateException, RollbackException, HeuristicMixedException, HeuristicRollbackException, SystemException, NotSupportedException, InterruptedException {
Cache cache1 = cache(0, "testcache");
Cache cache2 = cache(1, "testcache");
assertClusterSize("Should only be 2 caches in the cluster!!!", 2);
assertNull("Should be null", cache1.get(k));
assertNull("Should be null", cache2.get(k));
final StringBuffer initialObj = populateCache(cache1);
final Thread readThread = new Thread(readerRunnable);
readThread.start();
latchAfterRead.await();
final StringBuffer readObject = readerRunnable.getFromCache();
final Thread updateThread = new Thread(updateRunnable);
updateThread.start();
updateThread.join();
final StringBuffer writeObject = updateRunnable.getValue();
final String writeSnapshot = writeObject.toString();
final String readSnapshot = readObject.toString();
latchBeforeReadCommit.countDown();
AssertJUnit.assertSame(readObject, writeObject);
AssertJUnit.assertFalse(readSnapshot.equals(writeSnapshot));
}
private StringBuffer populateCache(Cache cache) throws SecurityException, IllegalStateException, RollbackException, HeuristicMixedException, HeuristicRollbackException, SystemException, NotSupportedException {
StringBuffer mutableObject = new StringBuffer("test");
TransactionManager mgr = TestingUtil.getTransactionManager(cache);
mgr.begin();
cache.getAdvancedCache().lock(k);
cache.put(k, mutableObject);
mgr.commit();
return mutableObject;
}
private class UpdateThread implements Runnable {
protected final String key;
protected final Cache cache;
protected final TransactionManager mgr;
private StringBuffer fromCache;
public UpdateThread(String k, Cache cache) {
this.key = k;
this.cache = cache;
mgr = TestingUtil.getTransactionManager(cache);
}
@Override
public void run() {
try {
mgr.begin();
getValueFromCache();
fromCache.append(System.currentTimeMillis());
cache.put(k, fromCache);
} catch (Exception x) {
x.printStackTrace();
} finally {
finishTx();
}
}
private void finishTx() {
try {
mgr.commit();
} catch (Exception x) {
x.printStackTrace();
}
}
private void getValueFromCache() {
fromCache = (StringBuffer) cache.get(key);
}
public StringBuffer getValue() {
return fromCache;
}
}
private class ReaderThread implements Runnable {
private final String key;
private final Cache cache;
private final CountDownLatch latchAfterRead, latchBeforeCommit;
private StringBuffer fromCache;
public ReaderThread(String k, Cache cache,
CountDownLatch latchAfterRead, CountDownLatch latchBeforeCommit) {
this.key = k;
this.cache = cache;
this.latchAfterRead = latchAfterRead;
this.latchBeforeCommit = latchBeforeCommit;
}
public StringBuffer getFromCache() {
return fromCache;
}
@Override
public void run() {
TransactionManager mgr = TestingUtil.getTransactionManager(cache);
try {
mgr.begin();
fromCache = (StringBuffer) cache.get(key);
latchAfterRead.countDown();
} catch (Exception x) {
x.printStackTrace();
} finally {
try {
latchBeforeCommit.await();
mgr.commit();
} catch (Exception x) {
x.printStackTrace();
}
}
}
}
}
The line in bold is where the test fails. The snapshot the read-thread has is the same as the write-thread, even though it read the object from the cache first. This is simply due to the fact that both read- and write-threads have the same object reference.
Note, if the write operations were rolled back, the read-thread would still see the change it had made. In fact, the whole cache from this point on would hold incorrect data.
This is not dissimilar to Hibernate updating an object (eg, assigning a primary key or a timestamp) even when the DB transaction it was in was rolled back. I've seen this happen when there were DB deadlocks and my transaction was chosen as the victim.
There are many other reasons why I think business critical data should not be put into Infinispan. I hope to present them in upcoming blog entries.
Useful SQL
Imagine an audit table of deal histories where every change to the deal means one more row in the table for a given deal ID.
How do you find the deals with the first audit time within a given time window?
[I'm posting this on my blog because I come across this problem quite often but never seem to remember the solution. By putting it here, I can copy-and-paste it in the future :) ]
So, imagine the audit table for deal_history looks a little like this:
+----+----------------+----------------+
|name|last_update_time|last_update_user|...
+----+----------------+----------------+
Such that the data looks like:
+-------+-------------------+----+--
|deal #1|03/14/2011 14:00:00|Tom |...
+-------+-------------------+----+--
|deal #1|03/14/2011 15:33:33|Dick|...
+-------+-------------------+----+--
|deal #2|03/14/2011 16:22:22|Tom |...
+-------+-------------------+----+--
| ... ... ..
+-------+-------------------+----+--
The trick is to select over the same table twice thus:
select d1.last_update_time, d1.name, d1.last_update_user
from deal_history d1 , deal_history d2
where d1.last_update_time >= '03/14/2011 14:00:00'
and d1.last_update_time <= '03/21/2011 15:00:00'
and d1.deal_id = d2.deal_id
group by d1.last_update_time, d1.name , d1.last_update_user
having d1.last_update_time = min(d2.last_update_time)
Making sure the unique identifier for a give deal is equal in both sets of data (note: in our example, this is not a PK since there are many rows per deal ID).
Also, note that the group by clause has fields in the same order and that the having clause must follow the group by.
How do you find the deals with the first audit time within a given time window?
[I'm posting this on my blog because I come across this problem quite often but never seem to remember the solution. By putting it here, I can copy-and-paste it in the future :) ]
So, imagine the audit table for deal_history looks a little like this:
+----+----------------+----------------+
|name|last_update_time|last_update_user|...
+----+----------------+----------------+
Such that the data looks like:
+-------+-------------------+----+--
|deal #1|03/14/2011 14:00:00|Tom |...
+-------+-------------------+----+--
|deal #1|03/14/2011 15:33:33|Dick|...
+-------+-------------------+----+--
|deal #2|03/14/2011 16:22:22|Tom |...
+-------+-------------------+----+--
| ... ... ..
+-------+-------------------+----+--
The trick is to select over the same table twice thus:
select d1.last_update_time, d1.name, d1.last_update_user
from deal_history d1 , deal_history d2
where d1.last_update_time >= '03/14/2011 14:00:00'
and d1.last_update_time <= '03/21/2011 15:00:00'
and d1.deal_id = d2.deal_id
group by d1.last_update_time, d1.name , d1.last_update_user
having d1.last_update_time = min(d2.last_update_time)
Making sure the unique identifier for a give deal is equal in both sets of data (note: in our example, this is not a PK since there are many rows per deal ID).
Also, note that the group by clause has fields in the same order and that the having clause must follow the group by.
Three reasons to avoid Subversion
I've been using CVS for most of the last 15 years. I came to the Subversion party late. Despite all my friends saying how wonderful it was, I didn't use it professionally until 2009.
Initially, I was reasonably happy with it but over time I have come to see the wisdom of Linus Torvalds statement that Subversiom is "the most pointless project ever started."
Reason #1
The integration with Eclipse is patchy. No matter how great the tool, if the integration with your IDE of choice is buggy then avoid it unless absolutely necessary.
Eclipse sometimes says I have checked everything in when I haven't. It says I have sometimes checked everything out when I haven't. It can get confused during merges - especially if you have been moving files around. And this seems to be the case over a number of versions of Eclipse I have used in the last 2 years (most recently Helios) using both Subversive and Subclipse plugins. A few members of my team and I have been forced to avoid Eclipse altogether and use the Tortoise Subversion plugin.
When Subversion gets its knickers in a twist, it's harder to manually edit the metadata files in the .svn directory since they appear to be binaries. With CVS, they were plain text files. Of course, it's always nasty to do this. But sometimes, you just gotta.
Reason #2
Subversion can have old JARs stored in the .svn directories. As an example, I picked a random project from my hard drive (Gilead) and looked in the .svn directory.
phillip:1.3.1-SNAPSHOT henryp$ pwd
/Users/henryp/Documents/Code/gilead/gilead/maven-snapshots-repo/net/sf/gilead/comet4gwt/1.3.1-SNAPSHOT
phillip:1.3.1-SNAPSHOT henryp$ jar -tf .svn/text-base/comet4gwt-1.3.1-20100205.214253-2.jar.svn-base | more
META-INF/
META-INF/MANIFEST.MF
net/
net/sf/
net/sf/gilead/
net/sf/gilead/annotations/
net/sf/gilead/comet/
net/sf/gilead/exception/
net/sf/gilead/annotations/Comet.class
.
.
.
So, a living, breathing JAR in the metadata directory!
You might ask: "so what?". Well, this caused a lot of confusion on a project that loaded all its JARs into the classpath using a shell script that used a wild card. (Yes, I know that is bad practise in these days of OSGi et al but bad practices are unfortunately part of life and probably will continue to be until we are a regulated profession...) For a few hours, we were wondering why classes from a library that had long since been removed were showing up in our code.
Reason #3
When a friend breathlessly told me about Subversion, he said you could move files around and Subversion would understand. Poor CVS simply saw it as one file being deleted and another being created.
Well, this is not the entire story. If you move a file, the change history can move with it and that's about it.
“A lesser-known fact about Subversion is that it lacks “true renames”—the svn move command is nothing more than an aggregation of svn copy and svn delete.”
Version Control with Subversion (For Subversion 1.5, r3305) p106
This may be tolerable for just one or two classes. But, let's say you need to do a major refactoring. I'm sure you've been here before:
"Files and directories are shuffled around and renamed, often causing great disruption to everyone on the project. Sound like the perfect case for a branch, doesn't it? Just create a branch, shuffle things around, then merge the branch back into trunk, right?
"Alas, this scenario doesn't work so well right now and is considered one of Subversion's current weak spots."
Version Control with Subversion
“The moral of this story is that until Subversion improves, be very careful about merging copies and renames from one branch to another.”
Version Control with Subversion (For Subversion 1.5, r3305), p107
I found during a recent mass-refactor that if I moved a file then moved the file again (having not checked in until I was satisfied), Subversion would simply panic.
Conclusion
Subversion will not fulfill the dream of having many branches where you can refactor to your heart's content and merge later. If you want that, try another tool (I'm starting to get interested in Git).
The bottom line is that Subversion is only useful if you have one branch that's in production and one development branch. This is OK for many projects. But if you are working on a project that has teams from New York to London to Bangalore working on different product lines as I am currently, I'd strongly recommend that you avoid it.
Initially, I was reasonably happy with it but over time I have come to see the wisdom of Linus Torvalds statement that Subversiom is "the most pointless project ever started."
Reason #1
The integration with Eclipse is patchy. No matter how great the tool, if the integration with your IDE of choice is buggy then avoid it unless absolutely necessary.
Eclipse sometimes says I have checked everything in when I haven't. It says I have sometimes checked everything out when I haven't. It can get confused during merges - especially if you have been moving files around. And this seems to be the case over a number of versions of Eclipse I have used in the last 2 years (most recently Helios) using both Subversive and Subclipse plugins. A few members of my team and I have been forced to avoid Eclipse altogether and use the Tortoise Subversion plugin.
When Subversion gets its knickers in a twist, it's harder to manually edit the metadata files in the .svn directory since they appear to be binaries. With CVS, they were plain text files. Of course, it's always nasty to do this. But sometimes, you just gotta.
Reason #2
Subversion can have old JARs stored in the .svn directories. As an example, I picked a random project from my hard drive (Gilead) and looked in the .svn directory.
phillip:1.3.1-SNAPSHOT henryp$ pwd
/Users/henryp/Documents/Code/gilead/gilead/maven-snapshots-repo/net/sf/gilead/comet4gwt/1.3.1-SNAPSHOT
phillip:1.3.1-SNAPSHOT henryp$ jar -tf .svn/text-base/comet4gwt-1.3.1-20100205.214253-2.jar.svn-base | more
META-INF/
META-INF/MANIFEST.MF
net/
net/sf/
net/sf/gilead/
net/sf/gilead/annotations/
net/sf/gilead/comet/
net/sf/gilead/exception/
net/sf/gilead/annotations/Comet.class
.
.
.
So, a living, breathing JAR in the metadata directory!
You might ask: "so what?". Well, this caused a lot of confusion on a project that loaded all its JARs into the classpath using a shell script that used a wild card. (Yes, I know that is bad practise in these days of OSGi et al but bad practices are unfortunately part of life and probably will continue to be until we are a regulated profession...) For a few hours, we were wondering why classes from a library that had long since been removed were showing up in our code.
Reason #3
When a friend breathlessly told me about Subversion, he said you could move files around and Subversion would understand. Poor CVS simply saw it as one file being deleted and another being created.
Well, this is not the entire story. If you move a file, the change history can move with it and that's about it.
“A lesser-known fact about Subversion is that it lacks “true renames”—the svn move command is nothing more than an aggregation of svn copy and svn delete.”
Version Control with Subversion (For Subversion 1.5, r3305) p106
This may be tolerable for just one or two classes. But, let's say you need to do a major refactoring. I'm sure you've been here before:
"Files and directories are shuffled around and renamed, often causing great disruption to everyone on the project. Sound like the perfect case for a branch, doesn't it? Just create a branch, shuffle things around, then merge the branch back into trunk, right?
"Alas, this scenario doesn't work so well right now and is considered one of Subversion's current weak spots."
Version Control with Subversion
“The moral of this story is that until Subversion improves, be very careful about merging copies and renames from one branch to another.”
Version Control with Subversion (For Subversion 1.5, r3305), p107
I found during a recent mass-refactor that if I moved a file then moved the file again (having not checked in until I was satisfied), Subversion would simply panic.
Conclusion
Subversion will not fulfill the dream of having many branches where you can refactor to your heart's content and merge later. If you want that, try another tool (I'm starting to get interested in Git).
The bottom line is that Subversion is only useful if you have one branch that's in production and one development branch. This is OK for many projects. But if you are working on a project that has teams from New York to London to Bangalore working on different product lines as I am currently, I'd strongly recommend that you avoid it.
Subscribe to:
Posts (Atom)