At this point, you may still be wondering how CAS values are used to prevent clients from writing over values that were changed by another client. Here is the answer:
In essence, the CAS value exists so that that a client can 'hold' the CAS value for a item ID that it knows, and only update the item if the CAS has not changed. Hence, Compare And Swap (CAS). In a multi-client environment it's designed to prevent one client changing the value of an item when another client may have already updated it.
Unfortunately there's no way to lock items; individual operations (set, for example) are atomic, but multiple operations are not, and this is what CAS is designed to protect against. To stop you changing a value that has changed since the last GET.
In order to demonstrate this situation, add the bold lines to the processInput method to allow a way to perform a CAS operation and see what happens if two copies of the program are run at the same time:
} else if (input.startsWith("/who")) { System.out.println("Users connected: " + client.get("CurrentUsers")); } else if (input.startsWith("/cas")) { runCasTest(); } else {
Now create the runCasTest() method at the
bottom of the class:
private void runCasTest() { System.out.println("Testing a CAS operation."); CASValue<Object> cas = client.gets("CasTest"); if (cas == null) { // Must create it first System.out.println("Creating CasTest value."); client.set("CasTest", 120, "InitialValue"); return; } System.out.println("CAS for CasTest = "+cas.getCas()); System.out.println("Sleeping for 10 seconds."); try { Thread.sleep(10000); } catch (InterruptedException e) { } CASResponse response = client.cas("CasTest", cas.getCas(), "ReplacedValue"); if (response.equals(CASResponse.OK)) { System.out.println("OK response."); } else if (response.equals(CASResponse.EXISTS)) { System.out.println("EXISTS response."); } else if (response.equals(CASResponse.NOT_FOUND)) { System.out.println("NOT_FOUND response."); } cas = client.gets("CasTest"); System.err.println("CAS after = "+cas.getCas()); }
The first time the test is run (by typing "/cas" while the
application is running) the gets() method will
return null, so it will just set the CasTest key to "InitialValue"
and return. The second time the test is run it will get a
CASValue<Object> instance from the
gets() method, print out its value, and then
sleep for 10 seconds. Then after sleeping, the code performs a
client.cas() method call to replace the value.
If you run this in two different windows you may see output something like the following if you time things just right:
/cas Testing a CAS operation. Creating CasTest value. /cas Testing a CAS operation. CAS for CasTest = 74 Sleeping for 10 seconds. OK response. CAS after = 75 /cas Testing a CAS operation. CAS for CasTest = 75 Sleeping for 10 seconds. EXISTS response. CAS after = 79
In the second copy of the application, the output would look something like this:
/cas Testing a CAS operation. CAS for CasTest = 75 Sleeping for 10 seconds. OK response. CAS after = 79
What you see is that when the CAS is 75, the second client modifies the value before the first client is done sleeping. Instead of getting an OK response, that client gets an EXISTS response indicating that a change has been made before it had a chance to do so, so its client.cas() operation failed, and the code would have to handle this situation to rectify the situation.
Locking is not an option if you want to have an operational distributed database. Locking causes contention and complexity. Being able to quickly detect when a value has been overwritten without having to hold a lock is a simple solution to the problem. You will be able to write code to handle the situation either by returning an error to the user, or retrying the operation after obtaining another CAS value.