changeset 1573:872748861ab1

Make executing prepared statements more resilient for WebStorage. Reviewed-by: vanaltj Review-thread: http://icedtea.classpath.org/pipermail/thermostat/2014-November/011796.html
author Severin Gehwolf <sgehwolf@redhat.com>
date Tue, 11 Nov 2014 16:02:59 +0100
parents c789ce56f99b
children 967ff19b0416
files storage/core/src/main/java/com/redhat/thermostat/storage/core/RetryableStatementExecutionException.java web/client/src/main/java/com/redhat/thermostat/web/client/internal/WebStorage.java web/client/src/test/java/com/redhat/thermostat/web/client/internal/WebStorageTest.java web/common/src/main/java/com/redhat/thermostat/web/common/PreparedStatementResponseCode.java web/common/src/main/java/com/redhat/thermostat/web/common/SharedStateId.java web/common/src/main/java/com/redhat/thermostat/web/common/WebPreparedStatement.java web/common/src/main/java/com/redhat/thermostat/web/common/WebPreparedStatementResponse.java web/common/src/main/java/com/redhat/thermostat/web/common/typeadapters/SharedStateIdTypeAdapter.java web/common/src/main/java/com/redhat/thermostat/web/common/typeadapters/SharedStateIdTypeAdapterFactory.java web/common/src/main/java/com/redhat/thermostat/web/common/typeadapters/WebPreparedStatementResponseTypeAdapter.java web/common/src/main/java/com/redhat/thermostat/web/common/typeadapters/WebPreparedStatementResponseTypeAdapterFactory.java web/common/src/main/java/com/redhat/thermostat/web/common/typeadapters/WebPreparedStatementTypeAdapter.java web/common/src/test/java/com/redhat/thermostat/web/common/SharedStateIdTest.java web/common/src/test/java/com/redhat/thermostat/web/common/typeadapters/JsonPerformanceTest.java web/common/src/test/java/com/redhat/thermostat/web/common/typeadapters/LegacyWebPreparedStatementSerializer.java web/common/src/test/java/com/redhat/thermostat/web/common/typeadapters/SharedStateIdTypeAdapterTest.java web/common/src/test/java/com/redhat/thermostat/web/common/typeadapters/WebPreparedStatementJSONPerformanceTest.java web/common/src/test/java/com/redhat/thermostat/web/common/typeadapters/WebPreparedStatementResponseJSONPerformanceTest.java web/common/src/test/java/com/redhat/thermostat/web/common/typeadapters/WebPreparedStatementResponseTypeAdapterTest.java web/common/src/test/java/com/redhat/thermostat/web/common/typeadapters/WebPreparedStatementTypeAdapterTest.java web/server/src/main/java/com/redhat/thermostat/web/server/PreparedStatementHolder.java web/server/src/main/java/com/redhat/thermostat/web/server/PreparedStatementManager.java web/server/src/main/java/com/redhat/thermostat/web/server/WebStorageEndPoint.java web/server/src/test/java/com/redhat/thermostat/web/server/PreparedStatementManagerTest.java web/server/src/test/java/com/redhat/thermostat/web/server/WebStorageEndpointTest.java
diffstat 25 files changed, 1147 insertions(+), 163 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/storage/core/src/main/java/com/redhat/thermostat/storage/core/RetryableStatementExecutionException.java	Tue Nov 11 16:02:59 2014 +0100
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2012-2014 Red Hat, Inc.
+ *
+ * This file is part of Thermostat.
+ *
+ * Thermostat is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published
+ * by the Free Software Foundation; either version 2, or (at your
+ * option) any later version.
+ *
+ * Thermostat is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Thermostat; see the file COPYING.  If not see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * Linking this code with other modules is making a combined work
+ * based on this code.  Thus, the terms and conditions of the GNU
+ * General Public License cover the whole combination.
+ *
+ * As a special exception, the copyright holders of this code give
+ * you permission to link this code with independent modules to
+ * produce an executable, regardless of the license terms of these
+ * independent modules, and to copy and distribute the resulting
+ * executable under terms of your choice, provided that you also
+ * meet, for each linked independent module, the terms and conditions
+ * of the license of that module.  An independent module is a module
+ * which is not derived from or based on this code.  If you modify
+ * this code, you may extend this exception to your version of the
+ * library, but you are not obligated to do so.  If you do not wish
+ * to do so, delete this exception statement from your version.
+ */
+
+package com.redhat.thermostat.storage.core;
+
+/**
+ * 
+ * Exception thrown if execution of a {@link PreparedStatement} failed, but
+ * may succeed if the same statement gets executed a second time.
+ *
+ */
+@SuppressWarnings("serial")
+public class RetryableStatementExecutionException extends
+        StatementExecutionException {
+
+    public RetryableStatementExecutionException(Throwable cause) {
+        super(cause);
+    }
+    
+}
--- a/web/client/src/main/java/com/redhat/thermostat/web/client/internal/WebStorage.java	Thu Nov 27 04:08:51 2014 -0700
+++ b/web/client/src/main/java/com/redhat/thermostat/web/client/internal/WebStorage.java	Tue Nov 11 16:02:59 2014 +0100
@@ -103,6 +103,7 @@
 import com.redhat.thermostat.storage.core.IllegalDescriptorException;
 import com.redhat.thermostat.storage.core.IllegalPatchException;
 import com.redhat.thermostat.storage.core.PreparedStatement;
+import com.redhat.thermostat.storage.core.RetryableStatementExecutionException;
 import com.redhat.thermostat.storage.core.SecureStorage;
 import com.redhat.thermostat.storage.core.StatementDescriptor;
 import com.redhat.thermostat.storage.core.StatementExecutionException;
@@ -111,12 +112,14 @@
 import com.redhat.thermostat.storage.core.StorageException;
 import com.redhat.thermostat.storage.model.Pojo;
 import com.redhat.thermostat.web.common.PreparedStatementResponseCode;
+import com.redhat.thermostat.web.common.SharedStateId;
 import com.redhat.thermostat.web.common.WebPreparedStatement;
 import com.redhat.thermostat.web.common.WebPreparedStatementResponse;
 import com.redhat.thermostat.web.common.WebQueryResponse;
 import com.redhat.thermostat.web.common.typeadapters.PojoTypeAdapterFactory;
 import com.redhat.thermostat.web.common.typeadapters.PreparedParameterTypeAdapterFactory;
 import com.redhat.thermostat.web.common.typeadapters.PreparedParametersTypeAdapterFactory;
+import com.redhat.thermostat.web.common.typeadapters.SharedStateIdTypeAdapterFactory;
 import com.redhat.thermostat.web.common.typeadapters.WebPreparedStatementResponseTypeAdapterFactory;
 import com.redhat.thermostat.web.common.typeadapters.WebPreparedStatementTypeAdapterFactory;
 import com.redhat.thermostat.web.common.typeadapters.WebQueryResponseTypeAdapterFactory;
@@ -325,7 +328,7 @@
         // statement execution
         private final transient Type parametrizedTypeToken;
         
-        public WebPreparedStatementImpl(Type parametrizedTypeToken, int numParams, int statementId) {
+        public WebPreparedStatementImpl(Type parametrizedTypeToken, int numParams, SharedStateId statementId) {
             super(numParams, statementId);
             this.parametrizedTypeToken = parametrizedTypeToken;
         }
@@ -380,6 +383,7 @@
         categoryIds = new HashMap<>();
         gson = new GsonBuilder()
                 .registerTypeAdapterFactory(new PojoTypeAdapterFactory())
+                .registerTypeAdapterFactory(new SharedStateIdTypeAdapterFactory())
                 .registerTypeAdapterFactory(new WebPreparedStatementResponseTypeAdapterFactory())
                 .registerTypeAdapterFactory(new WebQueryResponseTypeAdapterFactory())
                 .registerTypeAdapterFactory(new PreparedParameterTypeAdapterFactory())
@@ -575,24 +579,29 @@
         } catch (Exception e) {
             throw new StatementExecutionException(e);
         }
-        if (qResp.getResponseCode() == PreparedStatementResponseCode.QUERY_SUCCESS) {
-            // Return an empty cursor
+        switch(qResp.getResponseCode()) {
+        case PreparedStatementResponseCode.QUERY_SUCCESS:
             return new WebCursor<T>(this, qResp.getResultList(),
-                                    qResp.hasMoreBatches(),
-                                    qResp.getCursorId(), parametrizedTypeToken, stmt);
-        } else if (qResp.getResponseCode() == PreparedStatementResponseCode.ILLEGAL_PATCH) {
+                    qResp.hasMoreBatches(),
+                    qResp.getCursorId(), parametrizedTypeToken, stmt);
+        case PreparedStatementResponseCode.ILLEGAL_PATCH: {
             String msg = "Illegal statement argument. See server logs for details.";
             IllegalArgumentException iae = new IllegalArgumentException(msg);
             IllegalPatchException e = new IllegalPatchException(iae);
             throw new StatementExecutionException(e);
-        } else {
-            // We only handle success responses and illegal patches, like
-            // we do for other storages. This is just a defensive measure in
-            // order to fail early in case something unexpected comes back.
+        }
+        case PreparedStatementResponseCode.PREP_STMT_BAD_STOKEN: {
+            String msg = "Query failed to execute. Server changed token. Clearing prepared stmt cache!";
+            logger.log(Level.WARNING, msg);
+            clearPreparedStmtCache();
+            throw new RetryableStatementExecutionException(new RuntimeException(msg));
+        }
+        default: {
             String msg = "[query-execute] Unknown response from storage endpoint!";
             IllegalStateException ise = new IllegalStateException(msg);
             throw new StatementExecutionException(ise);
         }
+        }
     }
     
     /**
@@ -611,7 +620,8 @@
      * @return
      */
     <T extends Pojo> WebQueryResponse<T> getMore(int cursorId, Type parametrizedTypeToken, Integer batchSize, WebPreparedStatement<T> stmt) {
-        NameValuePair preparedStmtIdParam = new BasicNameValuePair("prepared-stmt-id", Integer.toString(stmt.getStatementId()));
+        String stmtId = gson.toJson(stmt.getStatementId());
+        NameValuePair preparedStmtIdParam = new BasicNameValuePair("prepared-stmt-id", stmtId);
         NameValuePair cursorIdParam = new BasicNameValuePair("cursor-id", Integer.toString(cursorId));
         NameValuePair batchSizeParam = new BasicNameValuePair("batch-size", batchSize.toString());
         
@@ -656,6 +666,11 @@
             IllegalArgumentException iae = new IllegalArgumentException(msg);
             IllegalPatchException e = new IllegalPatchException(iae);
             throw new StatementExecutionException(e);
+        } else if (responseCode == PreparedStatementResponseCode.PREP_STMT_BAD_STOKEN) {
+            String msg = "Write failed to execute. Server changed token. Clearing prepared stmt cache!";
+            logger.log(Level.WARNING, msg);
+            clearPreparedStmtCache();
+            throw new RetryableStatementExecutionException(new RuntimeException(msg));
         }
         return responseCode;
     }
@@ -754,6 +769,12 @@
     int getCategoryId(Category<?> category) {
         return categoryIds.get(category);
     }
+    
+    private void clearPreparedStmtCache() {
+        synchronized(this.stmtCache) {
+            stmtCache.clear();
+        }
+    }
 
     @Override
     public <T extends Pojo> PreparedStatement<T> prepareStatement(StatementDescriptor<T> desc)
@@ -799,13 +820,13 @@
             Reader reader = getContentAsReader(entity);
             WebPreparedStatementResponse result = gson.fromJson(reader, WebPreparedStatementResponse.class);
             int numParams = result.getNumFreeVariables();
-            int statementId = result.getStatementId();
-            if (statementId == WebPreparedStatementResponse.ILLEGAL_STATEMENT) {
+            SharedStateId statementId = result.getStatementId();
+            if (statementId.getId() == WebPreparedStatementResponse.ILLEGAL_STATEMENT) {
                 // we've got a descriptor the endpoint doesn't know about or
                 // refuses to accept for security reasons.
                 String msg = "Unknown query descriptor which endpoint of " + WebStorage.class.getName() + " refused to accept!";
                 throw new IllegalDescriptorException(msg, desc.getDescriptor());
-            } else if (statementId == WebPreparedStatementResponse.DESCRIPTOR_PARSE_FAILED) {
+            } else if (statementId.getId() == WebPreparedStatementResponse.DESCRIPTOR_PARSE_FAILED) {
                 String msg = "Statement descriptor failed to parse. " +
                              "Please check server logs for details!";
                 throw new DescriptorParsingException(msg);
@@ -826,9 +847,9 @@
         
         private final Type typeToken;
         private final int numParams;
-        private final int statementId;
+        private final SharedStateId statementId;
         
-        WebPreparedStatementHolder(Type typeToken, int numParams, int statementId) {
+        WebPreparedStatementHolder(Type typeToken, int numParams, SharedStateId statementId) {
             this.typeToken = typeToken;
             this.numParams = numParams;
             this.statementId = statementId;
--- a/web/client/src/test/java/com/redhat/thermostat/web/client/internal/WebStorageTest.java	Thu Nov 27 04:08:51 2014 -0700
+++ b/web/client/src/test/java/com/redhat/thermostat/web/client/internal/WebStorageTest.java	Tue Nov 11 16:02:59 2014 +0100
@@ -62,6 +62,7 @@
 import java.util.Enumeration;
 import java.util.HashMap;
 import java.util.Map;
+import java.util.UUID;
 import java.util.concurrent.CountDownLatch;
 
 import javax.servlet.ServletException;
@@ -106,11 +107,13 @@
 import com.redhat.thermostat.test.FreePortFinder;
 import com.redhat.thermostat.test.FreePortFinder.TryPort;
 import com.redhat.thermostat.web.common.PreparedStatementResponseCode;
+import com.redhat.thermostat.web.common.SharedStateId;
 import com.redhat.thermostat.web.common.WebPreparedStatement;
 import com.redhat.thermostat.web.common.WebPreparedStatementResponse;
 import com.redhat.thermostat.web.common.WebQueryResponse;
 import com.redhat.thermostat.web.common.typeadapters.PojoTypeAdapterFactory;
 import com.redhat.thermostat.web.common.typeadapters.PreparedParameterTypeAdapterFactory;
+import com.redhat.thermostat.web.common.typeadapters.SharedStateIdTypeAdapterFactory;
 import com.redhat.thermostat.web.common.typeadapters.WebPreparedStatementResponseTypeAdapterFactory;
 import com.redhat.thermostat.web.common.typeadapters.WebPreparedStatementTypeAdapterFactory;
 import com.redhat.thermostat.web.common.typeadapters.WebQueryResponseTypeAdapterFactory;
@@ -264,6 +267,7 @@
     public void preparingFaultyDescriptorThrowsException() throws UnsupportedEncodingException, IOException {
         Gson gson = new GsonBuilder()
                 .registerTypeAdapterFactory(new WebPreparedStatementTypeAdapterFactory())
+                .registerTypeAdapterFactory(new SharedStateIdTypeAdapterFactory())
                 .registerTypeAdapterFactory(new WebPreparedStatementResponseTypeAdapterFactory())
                 .create();
 
@@ -272,7 +276,8 @@
         StatementDescriptor<TestObj> desc = new StatementDescriptor<>(category, strDesc);
         
         WebPreparedStatementResponse fakeResponse = new WebPreparedStatementResponse();
-        fakeResponse.setStatementId(WebPreparedStatementResponse.DESCRIPTOR_PARSE_FAILED);
+        SharedStateId id = new SharedStateId(WebPreparedStatementResponse.DESCRIPTOR_PARSE_FAILED, UUID.randomUUID());
+        fakeResponse.setStatementId(id);
         prepareServer(gson.toJson(fakeResponse));
         try {
             storage.prepareStatement(desc);
@@ -289,6 +294,7 @@
     public void preparingUnknownDescriptorThrowsException() throws UnsupportedEncodingException, IOException {
         Gson gson = new GsonBuilder()
                 .registerTypeAdapterFactory(new WebPreparedStatementTypeAdapterFactory())
+                .registerTypeAdapterFactory(new SharedStateIdTypeAdapterFactory())
                 .registerTypeAdapterFactory(new WebPreparedStatementResponseTypeAdapterFactory())
                 .create();
 
@@ -296,7 +302,8 @@
         StatementDescriptor<TestObj> desc = new StatementDescriptor<>(category, strDesc);
         
         WebPreparedStatementResponse fakeResponse = new WebPreparedStatementResponse();
-        fakeResponse.setStatementId(WebPreparedStatementResponse.ILLEGAL_STATEMENT);
+        SharedStateId id = new SharedStateId(WebPreparedStatementResponse.ILLEGAL_STATEMENT, UUID.randomUUID());
+        fakeResponse.setStatementId(id);
         prepareServer(gson.toJson(fakeResponse));
         try {
             storage.prepareStatement(desc);
@@ -314,6 +321,7 @@
     public void forbiddenExecuteQueryThrowsConsumingExcptn() throws UnsupportedEncodingException, IOException {
         Gson gson = new GsonBuilder()
                             .registerTypeAdapterFactory(new PojoTypeAdapterFactory())
+                            .registerTypeAdapterFactory(new SharedStateIdTypeAdapterFactory())
                             .registerTypeAdapterFactory(new WebPreparedStatementResponseTypeAdapterFactory())
                             .registerTypeAdapterFactory(new WebQueryResponseTypeAdapterFactory())
                             .registerTypeAdapterFactory(new PreparedParameterTypeAdapterFactory())
@@ -325,9 +333,10 @@
         PreparedStatement<TestObj> stmt = null;
         
         int fakePrepStmtId = 5;
+        SharedStateId id = new SharedStateId(fakePrepStmtId, UUID.randomUUID());
         WebPreparedStatementResponse fakeResponse = new WebPreparedStatementResponse();
         fakeResponse.setNumFreeVariables(1);
-        fakeResponse.setStatementId(fakePrepStmtId);
+        fakeResponse.setStatementId(id);
         prepareServer(gson.toJson(fakeResponse));
         try {
             stmt = storage.prepareStatement(desc);
@@ -337,7 +346,7 @@
         }
         assertTrue(stmt instanceof WebPreparedStatement);
         WebPreparedStatement<TestObj> webStmt = (WebPreparedStatement<TestObj>)stmt;
-        assertEquals(fakePrepStmtId, webStmt.getStatementId());
+        assertEquals(id, webStmt.getStatementId());
         PreparedParameters params = webStmt.getParams();
         assertEquals(1, params.getParams().length);
         assertNull(params.getParams()[0]);
@@ -367,9 +376,9 @@
         // should fill the cache
         WebPreparedStatement<TestObj> stmt = (WebPreparedStatement<TestObj>)testStorage.prepareStatement(desc);
         int numParams = stmt.getParams().getParams().length;
-        int stmtId = stmt.getStatementId();
+        SharedStateId stmtId = stmt.getStatementId();
         assertEquals(0, numParams);
-        assertEquals(1, stmtId);
+        assertEquals(1, stmtId.getId());
         // this one should be cached, same stmtId and numParams as previous
         // one.
         stmt = (WebPreparedStatement<TestObj>)testStorage.prepareStatement(desc);
@@ -379,7 +388,7 @@
                      " if it wasn't cached. Was it 2? Bad!",
                      0, numParams);
         assertEquals("PreparedStatementWebStorge increments a counter" +
-                     " if it wasn't cached. Was it 3? Bad!", 1, stmtId);
+                     " if it wasn't cached. Was it 3? Bad!", 1, stmtId.getId());
         // preparing a different descriptor should not be cached.
         strDesc = "QUERY test WHERE 'foo' = ?l";
         desc = new StatementDescriptor<>(category, strDesc);
@@ -391,7 +400,7 @@
                      2, numParams);
         assertEquals("PreparedStatementWebStorge increments a counter" +
                      " if it wasn't cached. Triggers e.g. if it was erronously cached!",
-                     3, stmtId);
+                     3, stmtId.getId());
     }
     
     /**
@@ -423,11 +432,13 @@
      * 
      * By setting hasMoreBatches to true in WebQueryResponse we signal that
      * there are more batches available via getMore().
+     * @throws IOException 
+     * @throws UnsupportedEncodingException 
      * 
      * @see {@link #canPrepareAndExecuteQueryMultiBatchFailure()}
      */
     @Test
-    public void canPrepareAndExecuteQueryMultiBatchSuccess() {
+    public void canPrepareAndExecuteQueryMultiBatchSuccess() throws UnsupportedEncodingException, IOException {
         WebQueryResponse<TestObj> fakeQueryResponse = new WebQueryResponse<>();
         fakeQueryResponse.setResponseCode(PreparedStatementResponseCode.QUERY_SUCCESS);
         fakeQueryResponse.setResultList(getTwoTestObjects());
@@ -459,7 +470,10 @@
         path = requestURI.substring(requestURI.lastIndexOf('/'));
         assertEquals("/get-more", path);
         // Verify correctly passed parameters
-        String[] requestParams = requestBody.split("&");
+        StringReader reader = new StringReader(requestBody);
+        BufferedReader bufRead = new BufferedReader(reader);
+        String line = URLDecoder.decode(bufRead.readLine(), "UTF-8");
+        String[] requestParams = line.split("&");
         String prepStmtIdParam = requestParams[0];
         String cursorIdParam = requestParams[1];
         String batchSizeParam = requestParams[2];
@@ -467,7 +481,8 @@
         String[] cursorIdArray = cursorIdParam.split("=");
         String[] batchSizeArray = batchSizeParam.split("=");
         assertEquals("prepared-stmt-id", prStmtArray[0]);
-        assertEquals("5", prStmtArray[1]);
+        SharedStateId prStmtId = gson.fromJson(prStmtArray[1], SharedStateId.class);
+        assertEquals(5, prStmtId.getId());
         assertEquals("cursor-id", cursorIdArray[0]);
         assertEquals("444", cursorIdArray[1]);
         assertEquals("batch-size", batchSizeArray[0]);
@@ -553,7 +568,8 @@
         int fakePrepStmtId = 5;
         WebPreparedStatementResponse fakeResponse = new WebPreparedStatementResponse();
         fakeResponse.setNumFreeVariables(1);
-        fakeResponse.setStatementId(fakePrepStmtId);
+        SharedStateId id = new SharedStateId(fakePrepStmtId, UUID.randomUUID());
+        fakeResponse.setStatementId(id);
         prepareServer(gson.toJson(fakeResponse));
         try {
             stmt = storage.prepareStatement(desc);
@@ -563,7 +579,7 @@
         }
         assertTrue(stmt instanceof WebPreparedStatement);
         WebPreparedStatement<TestObj> webStmt = (WebPreparedStatement<TestObj>)stmt;
-        assertEquals(fakePrepStmtId, webStmt.getStatementId());
+        assertEquals(fakePrepStmtId, webStmt.getStatementId().getId());
         PreparedParameters params = webStmt.getParams();
         assertEquals(1, params.getParams().length);
         assertNull(params.getParams()[0]);
@@ -595,6 +611,7 @@
     private Gson getQueryGson() {
         return new GsonBuilder()
                     .registerTypeAdapterFactory(new PojoTypeAdapterFactory())
+                    .registerTypeAdapterFactory(new SharedStateIdTypeAdapterFactory())
                     .registerTypeAdapterFactory(new WebPreparedStatementResponseTypeAdapterFactory())
                     .registerTypeAdapterFactory(new WebQueryResponseTypeAdapterFactory())
                     .registerTypeAdapterFactory(new PreparedParameterTypeAdapterFactory())
@@ -610,6 +627,7 @@
         obj2.setProperty1("fluffor2");
         Gson gson = new GsonBuilder()
                             .registerTypeAdapterFactory(new PojoTypeAdapterFactory())
+                            .registerTypeAdapterFactory(new SharedStateIdTypeAdapterFactory())
                             .registerTypeAdapterFactory(new WebPreparedStatementResponseTypeAdapterFactory())
                             .registerTypeAdapterFactory(new WebQueryResponseTypeAdapterFactory())
                             .registerTypeAdapterFactory(new PreparedParameterTypeAdapterFactory())
@@ -623,7 +641,8 @@
         int fakePrepStmtId = 3;
         WebPreparedStatementResponse fakeResponse = new WebPreparedStatementResponse();
         fakeResponse.setNumFreeVariables(1);
-        fakeResponse.setStatementId(fakePrepStmtId);
+        SharedStateId id = new SharedStateId(fakePrepStmtId, UUID.randomUUID());
+        fakeResponse.setStatementId(id);
         prepareServer(gson.toJson(fakeResponse));
         try {
             stmt = storage.prepareStatement(desc);
@@ -633,7 +652,7 @@
         }
         assertTrue(stmt instanceof WebPreparedStatement);
         WebPreparedStatement<TestObj> webStmt = (WebPreparedStatement<TestObj>)stmt;
-        assertEquals(fakePrepStmtId, webStmt.getStatementId());
+        assertEquals(fakePrepStmtId, webStmt.getStatementId().getId());
         PreparedParameters params = webStmt.getParams();
         assertEquals(1, params.getParams().length);
         assertNull(params.getParams()[0]);
@@ -661,6 +680,7 @@
     public void forbiddenExecuteWriteReturnsGenericWriteFailure() {
         Gson gson = new GsonBuilder()
                             .registerTypeAdapterFactory(new PojoTypeAdapterFactory())
+                            .registerTypeAdapterFactory(new SharedStateIdTypeAdapterFactory())
                             .registerTypeAdapterFactory(new WebPreparedStatementResponseTypeAdapterFactory())
                             .registerTypeAdapterFactory(new WebQueryResponseTypeAdapterFactory())
                             .registerTypeAdapterFactory(new PreparedParameterTypeAdapterFactory())
@@ -674,7 +694,8 @@
         int fakePrepStmtId = 3;
         WebPreparedStatementResponse fakeResponse = new WebPreparedStatementResponse();
         fakeResponse.setNumFreeVariables(1);
-        fakeResponse.setStatementId(fakePrepStmtId);
+        SharedStateId id = new SharedStateId(fakePrepStmtId, UUID.randomUUID());
+        fakeResponse.setStatementId(id);
         prepareServer(gson.toJson(fakeResponse));
         try {
             stmt = storage.prepareStatement(desc);
@@ -684,7 +705,7 @@
         }
         assertTrue(stmt instanceof WebPreparedStatement);
         WebPreparedStatement<TestObj> webStmt = (WebPreparedStatement<TestObj>)stmt;
-        assertEquals(fakePrepStmtId, webStmt.getStatementId());
+        assertEquals(fakePrepStmtId, webStmt.getStatementId().getId());
         PreparedParameters params = webStmt.getParams();
         assertEquals(1, params.getParams().length);
         assertNull(params.getParams()[0]);
@@ -980,7 +1001,10 @@
         @Override
         <T extends Pojo> WebPreparedStatementHolder sendPrepareStmtRequest(StatementDescriptor<T> desc)
                 throws DescriptorParsingException {
-            return new WebPreparedStatementHolder(TestObj.class, counter++, counter++); 
+            int numParams = counter++;
+            int stmtId = counter++;
+            SharedStateId id = new SharedStateId(stmtId, UUID.randomUUID());
+            return new WebPreparedStatementHolder(TestObj.class, numParams, id); 
         }
     }
 }
--- a/web/common/src/main/java/com/redhat/thermostat/web/common/PreparedStatementResponseCode.java	Thu Nov 27 04:08:51 2014 -0700
+++ b/web/common/src/main/java/com/redhat/thermostat/web/common/PreparedStatementResponseCode.java	Tue Nov 11 16:02:59 2014 +0100
@@ -55,6 +55,7 @@
      */
     public static final int QUERY_FAILURE = -100;
     
+    
     /**
      * Failure code for expired cursors. Usually returned if
      * get-more requests failed because the underlying cursor
@@ -74,6 +75,14 @@
     public static final int ILLEGAL_PATCH = -1;
     
     /**
+     * Failure code for mismatching server tokens. This is usually happening if
+     * client and server get out of sync due to re-deployment or the like.
+     * Client should recover from this automatically by clearing the client
+     * cache and preparing statements again.
+     */
+    public static final int PREP_STMT_BAD_STOKEN = -2;
+    
+    /**
      * Failure to execute a prepared write statement for some unknown reason.
      */
     public static final int WRITE_GENERIC_FAILURE = -200;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/common/src/main/java/com/redhat/thermostat/web/common/SharedStateId.java	Tue Nov 11 16:02:59 2014 +0100
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2012-2014 Red Hat, Inc.
+ *
+ * This file is part of Thermostat.
+ *
+ * Thermostat is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published
+ * by the Free Software Foundation; either version 2, or (at your
+ * option) any later version.
+ *
+ * Thermostat is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Thermostat; see the file COPYING.  If not see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * Linking this code with other modules is making a combined work
+ * based on this code.  Thus, the terms and conditions of the GNU
+ * General Public License cover the whole combination.
+ *
+ * As a special exception, the copyright holders of this code give
+ * you permission to link this code with independent modules to
+ * produce an executable, regardless of the license terms of these
+ * independent modules, and to copy and distribute the resulting
+ * executable under terms of your choice, provided that you also
+ * meet, for each linked independent module, the terms and conditions
+ * of the license of that module.  An independent module is a module
+ * which is not derived from or based on this code.  If you modify
+ * this code, you may extend this exception to your version of the
+ * library, but you are not obligated to do so.  If you do not wish
+ * to do so, delete this exception statement from your version.
+ */
+
+package com.redhat.thermostat.web.common;
+
+import java.util.Objects;
+import java.util.UUID;
+
+/**
+ * Simple data structure which uniquely identifies shared state between
+ * client (WebStorage) and server (WebStorageEndPoint).
+ *
+ */
+public class SharedStateId {
+
+    // The id of the statement
+    private final int id;
+    // A unique token only used once per webapp deployment.
+    private final UUID serverToken;
+    
+    public SharedStateId(int id, UUID serverToken) {
+        this.id = id;
+        this.serverToken = serverToken;
+    }
+    
+    public int getId() {
+        return id;
+    }
+
+    public UUID getServerToken() {
+        return serverToken;
+    }
+
+    @Override
+    public boolean equals(Object other) {
+        if (other == null || other.getClass() != SharedStateId.class) {
+            return false;
+        }
+        SharedStateId o = (SharedStateId)other;
+        return id == o.id && serverToken.equals(o.serverToken);
+    }
+    
+    @Override
+    public int hashCode() {
+        return Objects.hash(id, serverToken);
+    }
+}
--- a/web/common/src/main/java/com/redhat/thermostat/web/common/WebPreparedStatement.java	Thu Nov 27 04:08:51 2014 -0700
+++ b/web/common/src/main/java/com/redhat/thermostat/web/common/WebPreparedStatement.java	Tue Nov 11 16:02:59 2014 +0100
@@ -47,9 +47,9 @@
         PreparedStatement<T> {
     
     private PreparedParameters params;
-    private int statementId;
+    private SharedStateId statementId;
     
-    public WebPreparedStatement(int numParams, int statementId) {
+    public WebPreparedStatement(int numParams, SharedStateId statementId) {
         this.params = new PreparedParameters(numParams);
         this.statementId = statementId;
     }
@@ -58,11 +58,11 @@
         // nothing. used for serialization
     }
 
-    public int getStatementId() {
+    public SharedStateId getStatementId() {
         return statementId;
     }
 
-    public void setStatementId(int statementId) {
+    public void setStatementId(SharedStateId statementId) {
         this.statementId = statementId;
     }
 
--- a/web/common/src/main/java/com/redhat/thermostat/web/common/WebPreparedStatementResponse.java	Thu Nov 27 04:08:51 2014 -0700
+++ b/web/common/src/main/java/com/redhat/thermostat/web/common/WebPreparedStatementResponse.java	Tue Nov 11 16:02:59 2014 +0100
@@ -60,13 +60,13 @@
     }
     
     private int numFreeVariables;
-    private int statementId;
+    private SharedStateId statementId;
     
-    public int getStatementId() {
+    public SharedStateId getStatementId() {
         return statementId;
     }
 
-    public void setStatementId(int statementId) {
+    public void setStatementId(SharedStateId statementId) {
         this.statementId = statementId;
     }
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/common/src/main/java/com/redhat/thermostat/web/common/typeadapters/SharedStateIdTypeAdapter.java	Tue Nov 11 16:02:59 2014 +0100
@@ -0,0 +1,113 @@
+/*
+ * Copyright 2012-2014 Red Hat, Inc.
+ *
+ * This file is part of Thermostat.
+ *
+ * Thermostat is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published
+ * by the Free Software Foundation; either version 2, or (at your
+ * option) any later version.
+ *
+ * Thermostat is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Thermostat; see the file COPYING.  If not see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * Linking this code with other modules is making a combined work
+ * based on this code.  Thus, the terms and conditions of the GNU
+ * General Public License cover the whole combination.
+ *
+ * As a special exception, the copyright holders of this code give
+ * you permission to link this code with independent modules to
+ * produce an executable, regardless of the license terms of these
+ * independent modules, and to copy and distribute the resulting
+ * executable under terms of your choice, provided that you also
+ * meet, for each linked independent module, the terms and conditions
+ * of the license of that module.  An independent module is a module
+ * which is not derived from or based on this code.  If you modify
+ * this code, you may extend this exception to your version of the
+ * library, but you are not obligated to do so.  If you do not wish
+ * to do so, delete this exception statement from your version.
+ */
+
+package com.redhat.thermostat.web.common.typeadapters;
+
+import java.io.IOException;
+import java.util.UUID;
+
+import com.google.gson.TypeAdapter;
+import com.google.gson.stream.JsonReader;
+import com.google.gson.stream.JsonToken;
+import com.google.gson.stream.JsonWriter;
+import com.redhat.thermostat.web.common.SharedStateId;
+
+class SharedStateIdTypeAdapter extends TypeAdapter<SharedStateId> {
+
+    private static final String PROP_STMT_ID = "sid";
+    private static final String PROP_SERVER_TOKEN = "stok";
+    
+    @Override
+    public void write(JsonWriter out, SharedStateId value) throws IOException {
+        // handle null
+        if (value == null) {
+            out.nullValue();
+            return;
+        }
+        
+        out.beginObject();
+        
+        // statement id
+        out.name(PROP_STMT_ID);
+        out.value(value.getId());
+        
+        // server token, may be null
+        if (value.getServerToken() != null) {
+            out.name(PROP_SERVER_TOKEN);
+            out.value(value.getServerToken().toString());
+        } else {
+            out.name(PROP_SERVER_TOKEN);
+            out.nullValue();
+        }
+        
+        out.endObject();
+        
+    }
+
+    @Override
+    public SharedStateId read(JsonReader in) throws IOException {
+        // handle null
+        if (in.peek() == JsonToken.NULL) {
+            in.nextNull();
+            return null;
+        }
+        
+        in.beginObject();
+        
+        
+        // statement id
+        String name = in.nextName();
+        if (!name.equals(PROP_STMT_ID)) {
+            throw new IllegalStateException("Expected name " + PROP_STMT_ID + " but was " + name);
+        }
+        int stmtId = in.nextInt();
+        
+        UUID serverToken = null;
+        if (in.peek() == JsonToken.NAME) {
+            name = in.nextName();
+            if (!name.equals(PROP_SERVER_TOKEN)) {
+                throw new IllegalStateException("Expected name " + PROP_SERVER_TOKEN + " but was " + name);
+            }
+            String sToken = in.nextString();
+            serverToken = UUID.fromString(sToken);
+        }
+        in.endObject();
+        
+        return new SharedStateId(stmtId, serverToken);
+    }
+
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/common/src/main/java/com/redhat/thermostat/web/common/typeadapters/SharedStateIdTypeAdapterFactory.java	Tue Nov 11 16:02:59 2014 +0100
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2012-2014 Red Hat, Inc.
+ *
+ * This file is part of Thermostat.
+ *
+ * Thermostat is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published
+ * by the Free Software Foundation; either version 2, or (at your
+ * option) any later version.
+ *
+ * Thermostat is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Thermostat; see the file COPYING.  If not see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * Linking this code with other modules is making a combined work
+ * based on this code.  Thus, the terms and conditions of the GNU
+ * General Public License cover the whole combination.
+ *
+ * As a special exception, the copyright holders of this code give
+ * you permission to link this code with independent modules to
+ * produce an executable, regardless of the license terms of these
+ * independent modules, and to copy and distribute the resulting
+ * executable under terms of your choice, provided that you also
+ * meet, for each linked independent module, the terms and conditions
+ * of the license of that module.  An independent module is a module
+ * which is not derived from or based on this code.  If you modify
+ * this code, you may extend this exception to your version of the
+ * library, but you are not obligated to do so.  If you do not wish
+ * to do so, delete this exception statement from your version.
+ */
+
+package com.redhat.thermostat.web.common.typeadapters;
+
+import com.google.gson.Gson;
+import com.google.gson.TypeAdapter;
+import com.google.gson.TypeAdapterFactory;
+import com.google.gson.reflect.TypeToken;
+import com.redhat.thermostat.web.common.SharedStateId;
+
+public class SharedStateIdTypeAdapterFactory implements TypeAdapterFactory {
+
+    @Override
+    public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
+        Class<?> rawType = type.getRawType();
+        if (rawType == SharedStateId.class) {
+            @SuppressWarnings("unchecked")
+            TypeAdapter<T> ta = (TypeAdapter<T>)new SharedStateIdTypeAdapter();
+            return ta;
+        }
+        return null;
+    }
+
+}
--- a/web/common/src/main/java/com/redhat/thermostat/web/common/typeadapters/WebPreparedStatementResponseTypeAdapter.java	Thu Nov 27 04:08:51 2014 -0700
+++ b/web/common/src/main/java/com/redhat/thermostat/web/common/typeadapters/WebPreparedStatementResponseTypeAdapter.java	Tue Nov 11 16:02:59 2014 +0100
@@ -42,6 +42,7 @@
 import com.google.gson.stream.JsonReader;
 import com.google.gson.stream.JsonToken;
 import com.google.gson.stream.JsonWriter;
+import com.redhat.thermostat.web.common.SharedStateId;
 import com.redhat.thermostat.web.common.WebPreparedStatementResponse;
 
 class WebPreparedStatementResponseTypeAdapter extends
@@ -49,9 +50,10 @@
     
     private static final String NUM_FREE_VARS_NAME = "numFreeVars";
     private static final String STMT_ID_NAME = "stmtId";
-
-    WebPreparedStatementResponseTypeAdapter() {
-        // package-private no-arg constructor
+    private final TypeAdapter<SharedStateId> sharedStateTa;
+    
+    WebPreparedStatementResponseTypeAdapter(TypeAdapter<SharedStateId> sharedStateTa) {
+        this.sharedStateTa = sharedStateTa;
     }
     
     @Override
@@ -71,7 +73,8 @@
         }
         name = reader.nextName();
         if (name.equals(STMT_ID_NAME)) {
-            response.setStatementId(reader.nextInt());
+            SharedStateId id = sharedStateTa.read(reader);
+            response.setStatementId(id);
         }
         reader.endObject();
         
@@ -87,7 +90,7 @@
         }
         
         int freeVars = value.getNumFreeVariables();
-        int stmtId = value.getStatementId();
+        SharedStateId stmtId = value.getStatementId();
         
         writer.beginObject();
         
@@ -96,7 +99,7 @@
         writer.value(freeVars);
         // stmt id
         writer.name(STMT_ID_NAME);
-        writer.value(stmtId);
+        sharedStateTa.write(writer, stmtId);
 
         writer.endObject();
     }
--- a/web/common/src/main/java/com/redhat/thermostat/web/common/typeadapters/WebPreparedStatementResponseTypeAdapterFactory.java	Thu Nov 27 04:08:51 2014 -0700
+++ b/web/common/src/main/java/com/redhat/thermostat/web/common/typeadapters/WebPreparedStatementResponseTypeAdapterFactory.java	Tue Nov 11 16:02:59 2014 +0100
@@ -40,6 +40,7 @@
 import com.google.gson.TypeAdapter;
 import com.google.gson.TypeAdapterFactory;
 import com.google.gson.reflect.TypeToken;
+import com.redhat.thermostat.web.common.SharedStateId;
 import com.redhat.thermostat.web.common.WebPreparedStatementResponse;
 
 public class WebPreparedStatementResponseTypeAdapterFactory implements
@@ -49,8 +50,9 @@
     public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
         Class<?> rawType = type.getRawType();
         if (rawType == WebPreparedStatementResponse.class) {
+            TypeAdapter<SharedStateId> sharedStateIdTa = gson.getAdapter(SharedStateId.class);
             @SuppressWarnings("unchecked")
-            TypeAdapter<T> ta = (TypeAdapter<T>)new WebPreparedStatementResponseTypeAdapter();
+            TypeAdapter<T> ta = (TypeAdapter<T>)new WebPreparedStatementResponseTypeAdapter(sharedStateIdTa);
             return ta;
         }
         return null;
--- a/web/common/src/main/java/com/redhat/thermostat/web/common/typeadapters/WebPreparedStatementTypeAdapter.java	Thu Nov 27 04:08:51 2014 -0700
+++ b/web/common/src/main/java/com/redhat/thermostat/web/common/typeadapters/WebPreparedStatementTypeAdapter.java	Tue Nov 11 16:02:59 2014 +0100
@@ -44,6 +44,7 @@
 import com.google.gson.stream.JsonToken;
 import com.google.gson.stream.JsonWriter;
 import com.redhat.thermostat.storage.core.PreparedParameters;
+import com.redhat.thermostat.web.common.SharedStateId;
 import com.redhat.thermostat.web.common.WebPreparedStatement;
 
 @SuppressWarnings("rawtypes")
@@ -52,10 +53,12 @@
     private static final String PROP_PARAMS = "p";
     private static final String PROP_STMT_ID = "sid";
     
-    private final Gson gson;
+    private final TypeAdapter<SharedStateId> sharedStateTa;
+    private final TypeAdapter<PreparedParameters> prepParamsTa;
     
     WebPreparedStatementTypeAdapter(Gson gson) {
-        this.gson = gson;
+        this.sharedStateTa = gson.getAdapter(SharedStateId.class);
+        this.prepParamsTa = gson.getAdapter(PreparedParameters.class);
     }
     
     @Override
@@ -71,12 +74,11 @@
         
         // statement id
         out.name(PROP_STMT_ID);
-        out.value(value.getStatementId());
+        sharedStateTa.write(out, value.getStatementId());
 
         // prepared parameters
         out.name(PROP_PARAMS);
-        TypeAdapter<PreparedParameters> ta = gson.getAdapter(PreparedParameters.class);
-        ta.write(out, value.getParams());
+        prepParamsTa.write(out, value.getParams());
         
         out.endObject();        
     }
@@ -97,8 +99,8 @@
         if (!name.equals(PROP_STMT_ID)) {
             throw new IllegalStateException("Expected name " + PROP_STMT_ID + " but was " + name);
         }
-        int stmtId = in.nextInt();
-
+        SharedStateId id = sharedStateTa.read(in);
+        
         // params
         PreparedParameters params = null;
         // params value might be null and missing.
@@ -107,15 +109,14 @@
             if (!name.equals(PROP_PARAMS)) {
                 throw new IllegalStateException("Expected name " + PROP_PARAMS + " but was " + name);
             }
-            TypeAdapter<PreparedParameters> ta = gson.getAdapter(PreparedParameters.class);
-            params = ta.read(in);
+            params = prepParamsTa.read(in);
         }
         
         in.endObject();
         
         WebPreparedStatement<?> stmt = new WebPreparedStatement<>();
         stmt.setParams(params);
-        stmt.setStatementId(stmtId);
+        stmt.setStatementId(id);
         return stmt;
     }
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/common/src/test/java/com/redhat/thermostat/web/common/SharedStateIdTest.java	Tue Nov 11 16:02:59 2014 +0100
@@ -0,0 +1,87 @@
+/*
+ * Copyright 2012-2014 Red Hat, Inc.
+ *
+ * This file is part of Thermostat.
+ *
+ * Thermostat is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published
+ * by the Free Software Foundation; either version 2, or (at your
+ * option) any later version.
+ *
+ * Thermostat is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Thermostat; see the file COPYING.  If not see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * Linking this code with other modules is making a combined work
+ * based on this code.  Thus, the terms and conditions of the GNU
+ * General Public License cover the whole combination.
+ *
+ * As a special exception, the copyright holders of this code give
+ * you permission to link this code with independent modules to
+ * produce an executable, regardless of the license terms of these
+ * independent modules, and to copy and distribute the resulting
+ * executable under terms of your choice, provided that you also
+ * meet, for each linked independent module, the terms and conditions
+ * of the license of that module.  An independent module is a module
+ * which is not derived from or based on this code.  If you modify
+ * this code, you may extend this exception to your version of the
+ * library, but you are not obligated to do so.  If you do not wish
+ * to do so, delete this exception statement from your version.
+ */
+
+package com.redhat.thermostat.web.common;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import java.util.UUID;
+
+import org.junit.Test;
+
+public class SharedStateIdTest {
+    
+    @Test
+    public void testEquals() {
+        UUID uuid = UUID.randomUUID();
+        SharedStateId id = new SharedStateId(3200, uuid);
+        SharedStateId id2 = new SharedStateId(3200, UUID.randomUUID());
+        SharedStateId id3 = new SharedStateId(300, uuid);
+        SharedStateId equals = new SharedStateId(3200, uuid);
+        
+        assertFalse(id.equals(null));
+        assertTrue(id.equals(equals));
+        assertFalse("Different uuid", id.equals(id2));
+        assertFalse("Different id val", id.equals(id3));
+        assertFalse("UUID and id val different", id3.equals(id2));
+        assertTrue(id.equals(id));
+    }
+
+    @Test
+    public void testHashCode() {
+        UUID uuid = UUID.randomUUID();
+        SharedStateId id = new SharedStateId(3200, uuid);
+        SharedStateId id2 = new SharedStateId(3200, UUID.randomUUID());
+        SharedStateId id3 = new SharedStateId(300, uuid);
+        SharedStateId equals = new SharedStateId(3200, uuid);
+        
+        assertTrue(id.hashCode() == equals.hashCode());
+        assertTrue("Different uuid", id.hashCode() != id2.hashCode());
+        assertTrue("Different id val", id.hashCode() != id3.hashCode());
+        assertTrue("UUID and id val different", id3.hashCode() != id2.hashCode());
+        assertTrue(id.hashCode() == id.hashCode());
+    }
+    
+    @Test
+    public void testBasic() {
+        UUID uuid = UUID.randomUUID();
+        SharedStateId id = new SharedStateId(3200, uuid);
+        assertEquals(uuid, id.getServerToken());
+        assertEquals(3200, id.getId());
+    }
+}
--- a/web/common/src/test/java/com/redhat/thermostat/web/common/typeadapters/JsonPerformanceTest.java	Thu Nov 27 04:08:51 2014 -0700
+++ b/web/common/src/test/java/com/redhat/thermostat/web/common/typeadapters/JsonPerformanceTest.java	Tue Nov 11 16:02:59 2014 +0100
@@ -72,10 +72,14 @@
     
     private final boolean debug;
     private final String testClass;
+    private final Gson oldGson;
+    private final Gson newGson;
     
     protected JsonPerformanceTest(boolean debug, String testClass) {
         this.debug = debug;
         this.testClass = testClass;
+        this.oldGson = getSlowGson();
+        this.newGson = getFasterGson();
     }
     
     /**
@@ -237,9 +241,7 @@
     }
     
     private double measureSerializationSpeed(final int iterations) {
-        final Gson oldGson = getSlowGson();
-        final Gson newGson = getFasterGson();
-        PerfTestResult result = runSerializationPerformanceTest(iterations, oldGson, newGson);
+        PerfTestResult result = runSerializationPerformanceTest(iterations);
         return result.getSpeedup();
     }
     
@@ -256,13 +258,11 @@
     }
     
     private double measureDeserializationSpeed(final int iterations) {
-        final Gson oldGson = getSlowGson();
-        final Gson newGson = getFasterGson();
-        PerfTestResult result = runDeserializationPerformanceTest(iterations, oldGson, newGson);
+        PerfTestResult result = runDeserializationPerformanceTest(iterations);
         return result.getSpeedup();
     }
     
-    private PerfTestResult runSerializationPerformanceTest(final int iterations, final Gson oldGson, final Gson newGson) {
+    private PerfTestResult runSerializationPerformanceTest(final int iterations) {
         List<String> list = new ArrayList<>();
         double oldSum = 0;
         double newSum = 0;
@@ -294,7 +294,7 @@
         return res;
     }
     
-    private PerfTestResult runDeserializationPerformanceTest(final int iterations, final Gson oldGson, final Gson newGson) {
+    private PerfTestResult runDeserializationPerformanceTest(final int iterations) {
         List<T> list = new ArrayList<>();
 
         double oldSum = 0;
--- a/web/common/src/test/java/com/redhat/thermostat/web/common/typeadapters/LegacyWebPreparedStatementSerializer.java	Thu Nov 27 04:08:51 2014 -0700
+++ b/web/common/src/test/java/com/redhat/thermostat/web/common/typeadapters/LegacyWebPreparedStatementSerializer.java	Tue Nov 11 16:02:59 2014 +0100
@@ -47,6 +47,7 @@
 import com.google.gson.JsonSerializationContext;
 import com.google.gson.JsonSerializer;
 import com.redhat.thermostat.storage.core.PreparedParameters;
+import com.redhat.thermostat.web.common.SharedStateId;
 import com.redhat.thermostat.web.common.WebPreparedStatement;
 
 /**
@@ -66,7 +67,7 @@
         JsonObject result = new JsonObject();
         JsonElement parameters = ctxt.serialize(stmt.getParams(), PreparedParameters.class);
         result.add(PROP_PARAMS, parameters);
-        JsonPrimitive stmtIdElem = new JsonPrimitive(stmt.getStatementId());
+        JsonElement stmtIdElem = ctxt.serialize(stmt.getStatementId(), SharedStateId.class);
         result.add(PROP_STMT_ID, stmtIdElem);
         return result;
     }
@@ -77,7 +78,7 @@
         JsonElement paramsElem = jsonElem.getAsJsonObject().get(PROP_PARAMS);
         JsonElement stmtIdElem = jsonElem.getAsJsonObject().get(PROP_STMT_ID);
         PreparedParameters params = ctxt.deserialize(paramsElem, PreparedParameters.class);
-        int stmtId = ctxt.deserialize(stmtIdElem, int.class);
+        SharedStateId stmtId = ctxt.deserialize(stmtIdElem, SharedStateId.class);
         WebPreparedStatement<?> stmt = new WebPreparedStatement<>();
         stmt.setStatementId(stmtId);
         stmt.setParams(params);
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/common/src/test/java/com/redhat/thermostat/web/common/typeadapters/SharedStateIdTypeAdapterTest.java	Tue Nov 11 16:02:59 2014 +0100
@@ -0,0 +1,101 @@
+/*
+ * Copyright 2012-2014 Red Hat, Inc.
+ *
+ * This file is part of Thermostat.
+ *
+ * Thermostat is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published
+ * by the Free Software Foundation; either version 2, or (at your
+ * option) any later version.
+ *
+ * Thermostat is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Thermostat; see the file COPYING.  If not see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * Linking this code with other modules is making a combined work
+ * based on this code.  Thus, the terms and conditions of the GNU
+ * General Public License cover the whole combination.
+ *
+ * As a special exception, the copyright holders of this code give
+ * you permission to link this code with independent modules to
+ * produce an executable, regardless of the license terms of these
+ * independent modules, and to copy and distribute the resulting
+ * executable under terms of your choice, provided that you also
+ * meet, for each linked independent module, the terms and conditions
+ * of the license of that module.  An independent module is a module
+ * which is not derived from or based on this code.  If you modify
+ * this code, you may extend this exception to your version of the
+ * library, but you are not obligated to do so.  If you do not wish
+ * to do so, delete this exception statement from your version.
+ */
+
+package com.redhat.thermostat.web.common.typeadapters;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+
+import java.util.UUID;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.redhat.thermostat.web.common.SharedStateId;
+
+public class SharedStateIdTypeAdapterTest {
+
+private Gson gson;
+    
+    @Before
+    public void setup() {
+        gson = new GsonBuilder()
+                    .registerTypeAdapter(SharedStateId.class, new SharedStateIdTypeAdapter())
+                    .create();
+    }
+    
+    @Test
+    public void canSerializeBasic() {
+        UUID uuid = UUID.randomUUID();
+        SharedStateId id = new SharedStateId(3, uuid);
+        String gsonStr = gson.toJson(id);
+        String expectedJSON = "{\"sid\":3,\"stok\":\"" + uuid.toString() + "\"}";
+        assertEquals(expectedJSON, gsonStr);
+    }
+    
+    @Test
+    public void canSerializeNull() {
+        SharedStateId id = new SharedStateId(3000, null);
+        String gsonStr = gson.toJson(id);
+        String expectedJSON = "{\"sid\":3000}";
+        assertEquals(expectedJSON, gsonStr);
+    }
+    
+    @Test
+    public void canDeserializeBasic() {
+        UUID uuid = UUID.randomUUID();
+        SharedStateId id = new SharedStateId(3, uuid);
+        String json = "{\"sid\":3,\"stok\":\"" + uuid.toString() + "\"}";
+        SharedStateId deserialized = gson.fromJson(json, SharedStateId.class);
+        assertEquals(3, deserialized.getId());
+        assertEquals(uuid, deserialized.getServerToken());
+        assertEquals(id, deserialized);
+    }
+    
+    /**
+     * UUID string value might be null. Make sure deserialization works for
+     * those.
+     */
+    @Test
+    public void canDeserializeNullServerNonce() {
+        String json = "{\"sid\":3}";
+        SharedStateId deserialized = gson.fromJson(json, SharedStateId.class);
+        assertEquals(3, deserialized.getId());
+        assertNull(deserialized.getServerToken());
+    }
+}
--- a/web/common/src/test/java/com/redhat/thermostat/web/common/typeadapters/WebPreparedStatementJSONPerformanceTest.java	Thu Nov 27 04:08:51 2014 -0700
+++ b/web/common/src/test/java/com/redhat/thermostat/web/common/typeadapters/WebPreparedStatementJSONPerformanceTest.java	Tue Nov 11 16:02:59 2014 +0100
@@ -36,6 +36,8 @@
 
 package com.redhat.thermostat.web.common.typeadapters;
 
+import java.util.UUID;
+
 import org.junit.experimental.categories.Category;
 
 import com.google.gson.Gson;
@@ -43,6 +45,7 @@
 import com.google.gson.reflect.TypeToken;
 import com.redhat.thermostat.storage.model.Pojo;
 import com.redhat.thermostat.testutils.PerformanceTest;
+import com.redhat.thermostat.web.common.SharedStateId;
 import com.redhat.thermostat.web.common.WebPreparedStatement;
 import com.redhat.thermostat.web.common.typeadapters.PojoTypeAdapterFactory;
 import com.redhat.thermostat.web.common.typeadapters.WebPreparedStatementTypeAdapterFactory;
@@ -66,6 +69,7 @@
     protected Gson getSlowGson() {
         return new GsonBuilder()
                     .registerTypeHierarchyAdapter(Pojo.class, new LegacyGSONConverter())
+                    .registerTypeAdapterFactory(new SharedStateIdTypeAdapterFactory())
                     .registerTypeAdapter(WebPreparedStatement.class, new LegacyWebPreparedStatementSerializer())
                     .create();
     }
@@ -74,6 +78,7 @@
     protected Gson getFasterGson() {
         return new GsonBuilder()
                     .registerTypeAdapterFactory(new PojoTypeAdapterFactory())
+                    .registerTypeAdapterFactory(new SharedStateIdTypeAdapterFactory())
                     .registerTypeAdapterFactory(new WebPreparedStatementTypeAdapterFactory())
                     .create();
     }
@@ -95,13 +100,13 @@
 
     @Override
     protected String mutateJsonString(GsonContext ctx, int mutator) {
-        return String.format("{\"sid\":%d,\"p\":{\"params\":[]}}", mutator);
+        return String.format("{\"sid\":{\"sid\":%d,\"stok\":\"" + UUID.randomUUID() + "\"},\"p\":{\"params\":[]}}", mutator);
     }
 
     @Override
     protected WebPreparedStatement mutateToBeSerializedInstance(int mutator) {
-        WebPreparedStatement retval = new WebPreparedStatement<>(0, 555);
-        retval.setStatementId(mutator);
+        SharedStateId id = new SharedStateId(mutator, UUID.randomUUID());
+        WebPreparedStatement retval = new WebPreparedStatement<>(0, id);
         return retval;
     }
 
--- a/web/common/src/test/java/com/redhat/thermostat/web/common/typeadapters/WebPreparedStatementResponseJSONPerformanceTest.java	Thu Nov 27 04:08:51 2014 -0700
+++ b/web/common/src/test/java/com/redhat/thermostat/web/common/typeadapters/WebPreparedStatementResponseJSONPerformanceTest.java	Tue Nov 11 16:02:59 2014 +0100
@@ -36,12 +36,15 @@
 
 package com.redhat.thermostat.web.common.typeadapters;
 
+import java.util.UUID;
+
 import org.junit.experimental.categories.Category;
 
 import com.google.gson.Gson;
 import com.google.gson.GsonBuilder;
 import com.google.gson.reflect.TypeToken;
 import com.redhat.thermostat.testutils.PerformanceTest;
+import com.redhat.thermostat.web.common.SharedStateId;
 import com.redhat.thermostat.web.common.WebPreparedStatementResponse;
 import com.redhat.thermostat.web.common.typeadapters.WebPreparedStatementResponseTypeAdapterFactory;
 
@@ -62,12 +65,15 @@
     
     @Override
     protected Gson getSlowGson() {
-        return new GsonBuilder().create();
+        return new GsonBuilder()
+                .registerTypeAdapterFactory(new SharedStateIdTypeAdapterFactory())
+                .create();
     }
 
     @Override
     protected Gson getFasterGson() {
         return new GsonBuilder()
+                    .registerTypeAdapterFactory(new SharedStateIdTypeAdapterFactory())
                     .registerTypeAdapterFactory(new WebPreparedStatementResponseTypeAdapterFactory())
                     .create();
     }
@@ -95,14 +101,15 @@
     protected String mutateJsonString(GsonContext ctx, int mutator) {
         int stmtId = (mutator + 1) * 20;
         int numFreeVars = mutator;
-        return String.format("{\"numFreeVars\":%d,\"stmtId\":%d}", numFreeVars, stmtId);
+        return String.format("{\"numFreeVars\":%d,\"stmtId\":{\"sid\":%d,\"stok\":\"%s\"}}", numFreeVars, stmtId, UUID.randomUUID().toString());
     }
 
     @Override
     protected WebPreparedStatementResponse mutateToBeSerializedInstance(int mutator) {
         WebPreparedStatementResponse response = new WebPreparedStatementResponse();
         response.setNumFreeVariables(3);
-        response.setStatementId(mutator);
+        SharedStateId id = new SharedStateId(mutator, UUID.randomUUID());
+        response.setStatementId(id);
         return response;
     }
 
@@ -128,7 +135,7 @@
 
     @Override
     protected int getWarmDeserializationIterations() {
-        return 5000;
+        return 2000;
     }
 
     @Override
@@ -138,7 +145,7 @@
 
     @Override
     protected double getSelfDeserializationDelta() {
-        return 0.3;
+        return 0;
     }
 
 }
--- a/web/common/src/test/java/com/redhat/thermostat/web/common/typeadapters/WebPreparedStatementResponseTypeAdapterTest.java	Thu Nov 27 04:08:51 2014 -0700
+++ b/web/common/src/test/java/com/redhat/thermostat/web/common/typeadapters/WebPreparedStatementResponseTypeAdapterTest.java	Tue Nov 11 16:02:59 2014 +0100
@@ -39,12 +39,15 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNull;
 
+import java.util.UUID;
+
 import org.junit.Before;
 import org.junit.Test;
 
 import com.google.gson.Gson;
 import com.google.gson.GsonBuilder;
 import com.google.gson.JsonSyntaxException;
+import com.redhat.thermostat.web.common.SharedStateId;
 import com.redhat.thermostat.web.common.WebPreparedStatementResponse;
 
 public class WebPreparedStatementResponseTypeAdapterTest {
@@ -54,6 +57,7 @@
     @Before
     public void setup() {
         gson = new GsonBuilder()
+                .registerTypeAdapterFactory(new SharedStateIdTypeAdapterFactory())
                 .registerTypeAdapterFactory(new WebPreparedStatementResponseTypeAdapterFactory())
                 .create();
     }
@@ -62,29 +66,35 @@
     public void testSerializationDeserializationBasic() {
         WebPreparedStatementResponse response = new WebPreparedStatementResponse();
         response.setNumFreeVariables(6);
-        response.setStatementId(WebPreparedStatementResponse.ILLEGAL_STATEMENT);
+        
+        UUID uuid = UUID.randomUUID();
+        SharedStateId stmtId = new SharedStateId(WebPreparedStatementResponse.ILLEGAL_STATEMENT, uuid);
+        response.setStatementId(stmtId);
         
         String jsonStr = gson.toJson(response, WebPreparedStatementResponse.class);
-        String expectedString = "{\"numFreeVars\":6,\"stmtId\":-1}";
+        String expectedString = "{\"numFreeVars\":6,\"stmtId\":{\"sid\":-1,\"stok\":\""+ uuid.toString() + "\"}}";
         assertEquals(expectedString, jsonStr);
         
         WebPreparedStatementResponse actual = gson.fromJson(jsonStr, WebPreparedStatementResponse.class);
         
         assertEquals(6, actual.getNumFreeVariables());
-        assertEquals(WebPreparedStatementResponse.ILLEGAL_STATEMENT, actual.getStatementId());
+        assertEquals(stmtId, actual.getStatementId());
     }
     
-    @Test(expected=JsonSyntaxException.class)
-    public void failDeserializePrimitiveSetNull() {
-        String jsonString = "{\"numFreeVars\":11,\"stmtId\":null}";
-        gson.fromJson(jsonString, WebPreparedStatementResponse.class);
+    public void deserializeNull() {
+        String jsonString = "{\"numFreeVars\":11}";
+        WebPreparedStatementResponse resp = gson.fromJson(jsonString, WebPreparedStatementResponse.class);
+        assertNull(resp.getStatementId());
+        assertEquals(11, resp.getNumFreeVariables());
     }
     
     @Test
     public void canDeserializeBasic() {
-        String jsonString = "{\"numFreeVars\":11,\"stmtId\":6}";
+        UUID uuid = UUID.randomUUID();
+        SharedStateId stmtId = new SharedStateId(6, uuid);
+        String jsonString = "{\"numFreeVars\":11,\"stmtId\":{\"sid\":6,\"stok\":\""+ uuid.toString() + "\"}}";
         WebPreparedStatementResponse actual = gson.fromJson(jsonString, WebPreparedStatementResponse.class);
-        assertEquals(6, actual.getStatementId());
+        assertEquals(stmtId, actual.getStatementId());
         assertEquals(11, actual.getNumFreeVariables());
     }
     
--- a/web/common/src/test/java/com/redhat/thermostat/web/common/typeadapters/WebPreparedStatementTypeAdapterTest.java	Thu Nov 27 04:08:51 2014 -0700
+++ b/web/common/src/test/java/com/redhat/thermostat/web/common/typeadapters/WebPreparedStatementTypeAdapterTest.java	Tue Nov 11 16:02:59 2014 +0100
@@ -40,6 +40,8 @@
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 
+import java.util.UUID;
+
 import org.junit.Before;
 import org.junit.Test;
 
@@ -48,6 +50,7 @@
 import com.redhat.thermostat.storage.core.PreparedParameter;
 import com.redhat.thermostat.storage.core.PreparedParameters;
 import com.redhat.thermostat.storage.model.AgentInformation;
+import com.redhat.thermostat.web.common.SharedStateId;
 import com.redhat.thermostat.web.common.WebPreparedStatement;
 import com.redhat.thermostat.web.common.WebPreparedStatementResponse;
 
@@ -59,6 +62,7 @@
     public void setup() {
         gson = new GsonBuilder()
                 .registerTypeAdapterFactory(new WebPreparedStatementTypeAdapterFactory())
+                .registerTypeAdapterFactory(new SharedStateIdTypeAdapterFactory())
                 .registerTypeAdapterFactory(new PreparedParameterTypeAdapterFactory())
                 .registerTypeAdapterFactory(new PojoTypeAdapterFactory())
                 .create();
@@ -67,10 +71,11 @@
     @Test
     public void canSerializeNullParams() {
         WebPreparedStatement<?> stmt = new WebPreparedStatement<>();
-        stmt.setStatementId(500);
+        UUID uuid = UUID.randomUUID();
+        stmt.setStatementId(new SharedStateId(500, uuid));
         stmt.setParams(null);
         
-        String expected = "{\"sid\":500}";
+        String expected = "{\"sid\":{\"sid\":500,\"stok\":\"" + uuid.toString() + "\"}}";
         
         String actual = gson.toJson(stmt);
         assertEquals(expected, actual);
@@ -78,28 +83,33 @@
     
     @Test
     public void canDeserializeNullParams() {
-        String json = "{\"sid\": 500}";
+        UUID uuid = UUID.randomUUID();
+        String json = "{\"sid\":{\"sid\":500,\"stok\":\"" + uuid.toString() + "\"}}";
         
         WebPreparedStatement<?> stmt = gson.fromJson(json, WebPreparedStatement.class);
-        assertEquals(500, stmt.getStatementId());
+        assertEquals(new SharedStateId(500, uuid), stmt.getStatementId());
         assertNull(stmt.getParams());
     }
     
     @Test
     public void canSerializeEmptyParams() {
-        WebPreparedStatement<?> stmt = new WebPreparedStatement<>(0, 555);
+        UUID uuid = UUID.randomUUID();
+        SharedStateId id = new SharedStateId(555, uuid);
+        WebPreparedStatement<?> stmt = new WebPreparedStatement<>(0, id);
         
-        String expected = "{\"sid\":555,\"p\":{\"params\":[]}}";
+        String expected = "{\"sid\":{\"sid\":555,\"stok\":\"" + uuid.toString() + "\"},\"p\":{\"params\":[]}}";
         String actual = gson.toJson(stmt);
         assertEquals(expected, actual);
     }
     
     @Test
     public void canDeserializeEmptyParams() {
-        String json = "{\"sid\":555,\"p\":{\"params\":[]}}";
+        UUID uuid = UUID.randomUUID();
+        SharedStateId id = new SharedStateId(555, uuid);
+        String json = "{\"sid\":{\"sid\":555,\"stok\":\"" + uuid.toString() + "\"},\"p\":{\"params\":[]}}";
         
         WebPreparedStatement<?> stmt = gson.fromJson(json, WebPreparedStatement.class);
-        assertEquals(555, stmt.getStatementId());
+        assertEquals(id, stmt.getStatementId());
         assertNotNull(stmt.getParams());
         assertEquals(0, stmt.getParams().getParams().length);
     }
@@ -114,7 +124,9 @@
         params.setBoolean(4, true);
         WebPreparedStatement<?> stmt = new WebPreparedStatement<>();
         stmt.setParams(params);
-        stmt.setStatementId(WebPreparedStatementResponse.DESCRIPTOR_PARSE_FAILED);
+        UUID uuid = UUID.randomUUID();
+        SharedStateId id = new SharedStateId(WebPreparedStatementResponse.DESCRIPTOR_PARSE_FAILED, uuid);
+        stmt.setStatementId(id);
         String jsonString = gson.toJson(stmt, WebPreparedStatement.class);
         WebPreparedStatement<?> newStmt = gson.fromJson(jsonString, WebPreparedStatement.class);
         assertNotNull(newStmt);
@@ -134,7 +146,7 @@
         assertEquals(String[].class, parameters[3].getType());
         assertEquals(true, parameters[4].getValue());
         assertEquals(boolean.class, parameters[4].getType());
-        assertEquals(WebPreparedStatementResponse.DESCRIPTOR_PARSE_FAILED, newStmt.getStatementId());
+        assertEquals(id, newStmt.getStatementId());
     }
     
     /*
@@ -153,13 +165,15 @@
         
         WebPreparedStatement<?> stmt = new WebPreparedStatement<>();
         stmt.setParams(params);
-        stmt.setStatementId(WebPreparedStatementResponse.DESCRIPTOR_PARSE_FAILED);
+        UUID uuid = UUID.randomUUID();
+        SharedStateId id = new SharedStateId(WebPreparedStatementResponse.DESCRIPTOR_PARSE_FAILED, uuid);
+        stmt.setStatementId(id);
         
         String jsonString = gson.toJson(stmt, WebPreparedStatement.class);
         assertNotNull(jsonString);
         
         WebPreparedStatement<?> result = gson.fromJson(jsonString, WebPreparedStatement.class);
-        assertEquals(WebPreparedStatementResponse.DESCRIPTOR_PARSE_FAILED, result.getStatementId());
+        assertEquals(id, result.getStatementId());
         assertNotNull(result.getParams());
     }
 }
--- a/web/server/src/main/java/com/redhat/thermostat/web/server/PreparedStatementHolder.java	Thu Nov 27 04:08:51 2014 -0700
+++ b/web/server/src/main/java/com/redhat/thermostat/web/server/PreparedStatementHolder.java	Tue Nov 11 16:02:59 2014 +0100
@@ -39,23 +39,24 @@
 import com.redhat.thermostat.storage.core.PreparedStatement;
 import com.redhat.thermostat.storage.core.StatementDescriptor;
 import com.redhat.thermostat.storage.model.Pojo;
+import com.redhat.thermostat.web.common.SharedStateId;
 
 class PreparedStatementHolder<T extends Pojo> {
 
-    private final int id;
+    private final SharedStateId stmtId;
     private final PreparedStatement<T> stmt;
     private final Class<T> dataClass;
     private final StatementDescriptor<T> desc;
     
-    PreparedStatementHolder(int id, PreparedStatement<T> stmt, Class<T> dataClass, StatementDescriptor<T> desc) {
-        this.id = id;
+    PreparedStatementHolder(SharedStateId stmtId, PreparedStatement<T> stmt, Class<T> dataClass, StatementDescriptor<T> desc) {
+        this.stmtId = stmtId;
         this.stmt = stmt;
         this.dataClass = dataClass;
         this.desc = desc;
     }
 
-    int getId() {
-        return id;
+    SharedStateId getId() {
+        return stmtId;
     }
 
     PreparedStatement<T> getStmt() {
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/server/src/main/java/com/redhat/thermostat/web/server/PreparedStatementManager.java	Tue Nov 11 16:02:59 2014 +0100
@@ -0,0 +1,132 @@
+/*
+ * Copyright 2012-2014 Red Hat, Inc.
+ *
+ * This file is part of Thermostat.
+ *
+ * Thermostat is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published
+ * by the Free Software Foundation; either version 2, or (at your
+ * option) any later version.
+ *
+ * Thermostat is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Thermostat; see the file COPYING.  If not see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * Linking this code with other modules is making a combined work
+ * based on this code.  Thus, the terms and conditions of the GNU
+ * General Public License cover the whole combination.
+ *
+ * As a special exception, the copyright holders of this code give
+ * you permission to link this code with independent modules to
+ * produce an executable, regardless of the license terms of these
+ * independent modules, and to copy and distribute the resulting
+ * executable under terms of your choice, provided that you also
+ * meet, for each linked independent module, the terms and conditions
+ * of the license of that module.  An independent module is a module
+ * which is not derived from or based on this code.  If you modify
+ * this code, you may extend this exception to your version of the
+ * library, but you are not obligated to do so.  If you do not wish
+ * to do so, delete this exception statement from your version.
+ */
+
+package com.redhat.thermostat.web.server;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+import java.util.UUID;
+
+import com.redhat.thermostat.storage.core.PreparedStatement;
+import com.redhat.thermostat.storage.core.StatementDescriptor;
+import com.redhat.thermostat.storage.model.Pojo;
+import com.redhat.thermostat.web.common.SharedStateId;
+
+/**
+ * Manager for {@link PreparedStatement}s which get prepared/executed via
+ * {@link WebStorage}.
+ *
+ */
+class PreparedStatementManager {
+
+    // We have one map per server token.
+    private final Map<SharedStateId, PreparedStatementHolder<?>> preparedStatementIds;
+    private final Map<StatementDescriptor<?>, PreparedStatementHolder<?>> preparedStmts;
+    private int currentPreparedStmtId = 0;
+    
+    PreparedStatementManager() {
+        preparedStatementIds = new HashMap<>();
+        preparedStmts = new HashMap<>();
+    }
+    
+    // Test only constructor
+    PreparedStatementManager(int initialValue) {
+        this();
+        currentPreparedStmtId = initialValue;
+    }
+    
+    @SuppressWarnings("unchecked") // we are the only ones adding them
+    synchronized <T extends Pojo> PreparedStatementHolder<T> getStatementHolder(SharedStateId id) {
+        return (PreparedStatementHolder<T>)preparedStatementIds.get(Objects.requireNonNull(id));
+    }
+    
+    @SuppressWarnings("unchecked") // we are the only ones adding them
+    synchronized <T extends Pojo> PreparedStatementHolder<T> getStatementHolder(StatementDescriptor<T> desc) {
+        return (PreparedStatementHolder<T>)preparedStmts.get(Objects.requireNonNull(desc));
+    }
+    
+    /**
+     * Adds a new {@link PreparedStatementHolder} into this
+     * {@link PreparedStatementManager}. Adding an equal {@code targetDesc}
+     * statement twice will yield the same returned id.
+     * 
+     * @param serverToken
+     *            A server token used for creating a new {@link SharedStateId}
+     *            if the target statement is not already tracked.
+     * @param targetStmt
+     *            The target statement to keep track of.
+     * @param dataClass
+     *            The data class of the target statement.
+     * @param targetDesc
+     *            The {@link StatementDescriptor} which was used for creating
+     *            the target statement {@code targetStmt}
+     * @return A unique ID identifying this statement. It's suitable to be
+     *         shared between server and client.
+     */
+    synchronized <T extends Pojo> SharedStateId createAndPutHolder(
+                                                             UUID serverToken,
+                                                             PreparedStatement<T> targetStmt,
+                                                             Class<T> dataClass,
+                                                             StatementDescriptor<T> targetDesc) {
+        // check if we have this descriptor already added
+        @SuppressWarnings("unchecked")
+        PreparedStatementHolder<T> holder = (PreparedStatementHolder<T>)preparedStmts.get(Objects.requireNonNull(targetDesc));
+        if (holder != null) {
+            // nothing to do
+            assert( preparedStatementIds.get(holder.getId()) != null );
+            return holder.getId();
+        }
+        // OK, must be a new statement we don't yet track
+        SharedStateId id = new SharedStateId(currentPreparedStmtId, Objects.requireNonNull(serverToken));
+        currentPreparedStmtId++;
+        // There is nothing we can do other than using a long rather than an int
+        // for the ID. That being said, having more than 2 billion *different* queries
+        // seems more than unlikely. It may very well be a bug. Either way it
+        // seems like a good idea to fail hard in order to be in the know about
+        // this situation.
+        if (currentPreparedStmtId == Integer.MAX_VALUE) {
+            throw new IllegalStateException("Too many different statements!");
+        }
+        holder = new PreparedStatementHolder<>(id,
+                                               Objects.requireNonNull(targetStmt),
+                                               Objects.requireNonNull(dataClass),
+                                               targetDesc);
+        preparedStmts.put(targetDesc, holder);
+        preparedStatementIds.put(id, holder);
+        return id;
+    }
+}
--- a/web/server/src/main/java/com/redhat/thermostat/web/server/WebStorageEndPoint.java	Thu Nov 27 04:08:51 2014 -0700
+++ b/web/server/src/main/java/com/redhat/thermostat/web/server/WebStorageEndPoint.java	Tue Nov 11 16:02:59 2014 +0100
@@ -43,6 +43,7 @@
 import java.io.PrintWriter;
 import java.io.Writer;
 import java.lang.reflect.Array;
+import java.net.URLDecoder;
 import java.security.Principal;
 import java.util.ArrayList;
 import java.util.Collections;
@@ -51,6 +52,7 @@
 import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
+import java.util.UUID;
 import java.util.logging.Level;
 import java.util.logging.Logger;
 
@@ -100,12 +102,14 @@
 import com.redhat.thermostat.storage.query.BinaryLogicalOperator;
 import com.redhat.thermostat.storage.query.Expression;
 import com.redhat.thermostat.web.common.PreparedStatementResponseCode;
+import com.redhat.thermostat.web.common.SharedStateId;
 import com.redhat.thermostat.web.common.WebPreparedStatement;
 import com.redhat.thermostat.web.common.WebPreparedStatementResponse;
 import com.redhat.thermostat.web.common.WebQueryResponse;
 import com.redhat.thermostat.web.common.typeadapters.PojoTypeAdapterFactory;
 import com.redhat.thermostat.web.common.typeadapters.PreparedParameterTypeAdapterFactory;
 import com.redhat.thermostat.web.common.typeadapters.PreparedParametersTypeAdapterFactory;
+import com.redhat.thermostat.web.common.typeadapters.SharedStateIdTypeAdapterFactory;
 import com.redhat.thermostat.web.common.typeadapters.WebPreparedStatementResponseTypeAdapterFactory;
 import com.redhat.thermostat.web.common.typeadapters.WebPreparedStatementTypeAdapterFactory;
 import com.redhat.thermostat.web.common.typeadapters.WebQueryResponseTypeAdapterFactory;
@@ -131,6 +135,7 @@
     private static final String TOKEN_MANAGER_KEY = "token-manager";
     private static final String USER_PRINCIPAL_CALLBACK_KEY = "user-principal-callback";
     private static final String CURSOR_MANAGER_KEY = "cursor-manager";
+    private static final String PREPARED_STMT_MANAGER_KEY = "prepared-stmt-manager";
     private static final int UNKNOWN_CURSOR_ID = -0xdeadbeef;
     private static final String CATEGORY_KEY_FORMAT = "%s|%s";
 
@@ -153,12 +158,9 @@
 
     private Map<String, Integer> categoryIds;
     private Map<Integer, Category<?>> categories;
-    
-    private Map<StatementDescriptor<?>, PreparedStatementHolder<?>> preparedStmts;
-    private Map<Integer, PreparedStatementHolder<?>> preparedStatementIds;
-    // Lock to be held for setting/getting prepared queries in the above maps
-    private Object preparedStmtLock = new Object();
-    private int currentPreparedStmtId;
+    // A unique server token, which gets reset on every Servlet.init() call.
+    // I.e. every reload/redeployment.
+    private UUID serverToken;
     
     // read-only set of all known statement descriptors we trust and allow
     private Set<String> knownStatementDescriptors;
@@ -176,6 +178,7 @@
         
         gson = new GsonBuilder()
                 .registerTypeAdapterFactory(new PojoTypeAdapterFactory())
+                .registerTypeAdapterFactory(new SharedStateIdTypeAdapterFactory())
                 .registerTypeAdapterFactory(new WebPreparedStatementResponseTypeAdapterFactory())
                 .registerTypeAdapterFactory(new WebQueryResponseTypeAdapterFactory())
                 .registerTypeAdapterFactory(new PreparedParameterTypeAdapterFactory())
@@ -184,8 +187,6 @@
                 .create();
         categoryIds = new HashMap<>();
         categories = new HashMap<>();
-        preparedStatementIds = new HashMap<>();
-        preparedStmts = new HashMap<>();
         TokenManager tokenManager = new TokenManager();
         String timeoutParam = getInitParameter(TOKEN_MANAGER_TIMEOUT_PARAM);
         if (timeoutParam != null) {
@@ -208,6 +209,10 @@
         PrincipalCallbackFactory cbFactory = new PrincipalCallbackFactory(info);
         PrincipalCallback callback = Objects.requireNonNull(cbFactory.getCallback());
         servletContext.setAttribute(USER_PRINCIPAL_CALLBACK_KEY, callback);
+        synchronized(servletContext) {
+            servletContext.setAttribute(PREPARED_STMT_MANAGER_KEY, new PreparedStatementManager());
+        }
+        serverToken = UUID.randomUUID();
     }
     
     @Override
@@ -350,7 +355,8 @@
         if (cat == null) {
             // bad category? we refuse to accept this
             logger.log(Level.WARNING, "Attepted to prepare a statement with an illegal category id");
-            response.setStatementId(WebPreparedStatementResponse.ILLEGAL_STATEMENT);
+            SharedStateId id = new SharedStateId(WebPreparedStatementResponse.ILLEGAL_STATEMENT, serverToken);
+            response.setStatementId(id);
             writeResponse(resp, response, WebPreparedStatementResponse.class);
             return;
         }
@@ -360,50 +366,46 @@
             String msg = "Attempted to prepare a statement descriptor which we " +
             		"don't trust! Descriptor was: ->" + desc.getDescriptor() + "<-";
             logger.log(Level.WARNING, msg);
-            response.setStatementId(WebPreparedStatementResponse.ILLEGAL_STATEMENT);
+            SharedStateId id = new SharedStateId(WebPreparedStatementResponse.ILLEGAL_STATEMENT, serverToken);
+            response.setStatementId(id);
             writeResponse(resp, response, WebPreparedStatementResponse.class);
             return;
         }
         
-        synchronized (preparedStmtLock) {
-            // see if we've prepared this query already
-            if (preparedStmts.containsKey(desc)) {
-                PreparedStatementHolder<T> holder = (PreparedStatementHolder<T>) preparedStmts
-                        .get(desc);
-                ParsedStatement<T> parsed = holder.getStmt()
-                        .getParsedStatement();
-                int freeVars = parsed.getNumParams();
-                response.setNumFreeVariables(freeVars);
-                response.setStatementId(holder.getId());
-                writeResponse(resp, response,
-                        WebPreparedStatementResponse.class);
-                return;
-            }
-            
-            // Prepare the target statement and put it into our prepared statement
-            // maps.
+        PreparedStatementManager prepStmtManager = getPreparedStmtManager();
+        // see if we've prepared this query already
+        PreparedStatementHolder<T> holder = prepStmtManager.getStatementHolder(desc);
+        if (holder != null) {
+            ParsedStatement<T> parsed = holder.getStmt().getParsedStatement();
+            int freeVars = parsed.getNumParams();
+            response.setNumFreeVariables(freeVars);
+            SharedStateId id = new SharedStateId(holder.getId().getId(), serverToken);
+            response.setStatementId(id);
+            writeResponse(resp, response, WebPreparedStatementResponse.class);
+            return;
+        } else {
+            // Prepare the target statement and track it via
+            // PreparedStatementManager
             PreparedStatement<T> targetPreparedStatement;
             try {
                 targetPreparedStatement = (PreparedStatement<T>) storage
                         .prepareStatement(desc);
             } catch (DescriptorParsingException e) {
                 logger.log(Level.WARNING, "Descriptor parse error!", e);
-                response.setStatementId(WebPreparedStatementResponse.DESCRIPTOR_PARSE_FAILED);
+                SharedStateId id = new SharedStateId(WebPreparedStatementResponse.DESCRIPTOR_PARSE_FAILED, serverToken);
+                response.setStatementId(id);
                 writeResponse(resp, response,
                         WebPreparedStatementResponse.class);
                 return;
             }
-            PreparedStatementHolder<T> holder = new PreparedStatementHolder<T>(
-                    currentPreparedStmtId, targetPreparedStatement,
-                    (Class<T>) cat.getDataClass(), desc);
-            preparedStmts.put(desc, holder);
-            preparedStatementIds.put(currentPreparedStmtId, holder);
+            SharedStateId stmtId = prepStmtManager.createAndPutHolder(
+                    serverToken, targetPreparedStatement, cat.getDataClass(),
+                    desc);
             ParsedStatement<?> parsed = targetPreparedStatement
                     .getParsedStatement();
             response.setNumFreeVariables(parsed.getNumParams());
-            response.setStatementId(currentPreparedStmtId);
+            response.setStatementId(stmtId);
             writeResponse(resp, response, WebPreparedStatementResponse.class);
-            currentPreparedStmtId++;
         }
     }
 
@@ -607,9 +609,22 @@
         @SuppressWarnings("unchecked")
         WebPreparedStatement<T> stmt = gson.fromJson(queryParam, WebPreparedStatement.class);
         
+        // Check if the server token the client knows about still matches.
+        // Bail out early otherwise.
+        SharedStateId stmtId = stmt.getStatementId();
+        if (!serverToken.equals(stmtId.getServerToken())) {
+            logger.log(Level.INFO, "Server token: '" + serverToken +
+                                   "' and client token '" + stmtId.getServerToken() +
+                                   "' out of sync.");
+            WebQueryResponse<T> response = new WebQueryResponse<>();
+            response.setResponseCode(PreparedStatementResponseCode.PREP_STMT_BAD_STOKEN);
+            writeResponse(resp, response, WebQueryResponse.class);
+            return;
+        }
         PreparedParameters p = stmt.getParams();
         PreparedParameter[] params = p.getParams();
-        PreparedStatementHolder<T> targetStmtHolder = getStatementHolderFromId(stmt.getStatementId());
+        PreparedStatementManager prepStmtManager = getPreparedStmtManager();
+        PreparedStatementHolder<T> targetStmtHolder = prepStmtManager.getStatementHolder(stmtId);
         PreparedStatement<T> targetStmt = targetStmtHolder.getStmt();
         ParsedStatement<T> parsed = targetStmt.getParsedStatement();
         Query<T> targetQuery = null;
@@ -665,6 +680,16 @@
         }
         writeQueryResponse(resp, response, resultsList, targetStmtHolder);
     }
+
+    private PreparedStatementManager getPreparedStmtManager() {
+        ServletContext servletContext = getServletContext();
+        PreparedStatementManager prepStmtManager = null;
+        synchronized(servletContext) {
+            prepStmtManager = (PreparedStatementManager)servletContext.getAttribute(PREPARED_STMT_MANAGER_KEY);
+        }
+        // If this throws a NPE this is certainly a bug.
+        return Objects.requireNonNull(prepStmtManager);
+    }
     
     private <T extends Pojo> void writeQueryResponse(HttpServletResponse resp, WebQueryResponse<T> response, List<T> resultsList, PreparedStatementHolder<T> targetStmtHolder) throws IOException {
         @SuppressWarnings("unchecked")
@@ -694,7 +719,8 @@
         String cursorIdParam = req.getParameter("cursor-id");
         String batchSizeParam = req.getParameter("batch-size");
         
-        int stmtId = Integer.parseInt(stmtIdParam);
+        // Statement Id is JSON encoded.
+        SharedStateId id = gson.fromJson(URLDecoder.decode(stmtIdParam, "UTF-8"), SharedStateId.class);
         int cursorId = Integer.parseInt(cursorIdParam);
         int batchSize = Integer.parseInt(batchSizeParam);
         
@@ -712,7 +738,8 @@
         @SuppressWarnings("unchecked")
         BatchCursor<T> batchCursor = (BatchCursor<T>)cursorManager.get(cursorId);
         
-        PreparedStatementHolder<T> targetStmtHolder = getStatementHolderFromId(stmtId);
+        PreparedStatementManager prepStmtManager = getPreparedStmtManager();
+        PreparedStatementHolder<T> targetStmtHolder = prepStmtManager.getStatementHolder(id);
         if (batchCursor == null) {
             // This either means:
             // 1. The underlying (backing-storage) cursor didn't have
@@ -790,9 +817,20 @@
         String queryParam = req.getParameter("prepared-stmt");
         WebPreparedStatement<T> stmt = gson.fromJson(queryParam, WebPreparedStatement.class);
         
+        // Check if the server token the client knows about still matches.
+        // Bail out early otherwise.
+        SharedStateId stmtId = stmt.getStatementId();
+        if (!serverToken.equals(stmtId.getServerToken())) {
+            logger.log(Level.INFO, "Server token: '" + serverToken +
+                                   "' and client token '" + stmtId.getServerToken() +
+                                   "' out of sync.");
+            writeResponse(resp, PreparedStatementResponseCode.PREP_STMT_BAD_STOKEN, int.class);
+            return;
+        }
         PreparedParameters p = stmt.getParams();
         PreparedParameter[] params = p.getParams();
-        PreparedStatementHolder<T> targetStmtHolder = getStatementHolderFromId(stmt.getStatementId());
+        PreparedStatementManager prepStmtManager = getPreparedStmtManager();
+        PreparedStatementHolder<T> targetStmtHolder = prepStmtManager.getStatementHolder(stmt.getStatementId());
         PreparedStatement<T> targetStmt = targetStmtHolder.getStmt();
         ParsedStatement<T> parsed = targetStmt.getParsedStatement();
         
@@ -860,12 +898,6 @@
         return patchedQuery;
     }
 
-    private <T extends Pojo> PreparedStatementHolder<T> getStatementHolderFromId(int statementId) {
-        @SuppressWarnings("unchecked") // we are the only ones adding them
-        PreparedStatementHolder<T> holder = (PreparedStatementHolder<T>)preparedStatementIds.get(statementId);
-        return holder;
-    }
-
     private Category<?> getCategoryFromId(int categoryId) {
         Category<?> category = categories.get(categoryId);
         return category;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/server/src/test/java/com/redhat/thermostat/web/server/PreparedStatementManagerTest.java	Tue Nov 11 16:02:59 2014 +0100
@@ -0,0 +1,221 @@
+/*
+ * Copyright 2012-2014 Red Hat, Inc.
+ *
+ * This file is part of Thermostat.
+ *
+ * Thermostat is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published
+ * by the Free Software Foundation; either version 2, or (at your
+ * option) any later version.
+ *
+ * Thermostat is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Thermostat; see the file COPYING.  If not see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * Linking this code with other modules is making a combined work
+ * based on this code.  Thus, the terms and conditions of the GNU
+ * General Public License cover the whole combination.
+ *
+ * As a special exception, the copyright holders of this code give
+ * you permission to link this code with independent modules to
+ * produce an executable, regardless of the license terms of these
+ * independent modules, and to copy and distribute the resulting
+ * executable under terms of your choice, provided that you also
+ * meet, for each linked independent module, the terms and conditions
+ * of the license of that module.  An independent module is a module
+ * which is not derived from or based on this code.  If you modify
+ * this code, you may extend this exception to your version of the
+ * library, but you are not obligated to do so.  If you do not wish
+ * to do so, delete this exception statement from your version.
+ */
+
+package com.redhat.thermostat.web.server;
+
+import java.util.UUID;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNotSame;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.mockito.Mockito.mock;
+
+import org.junit.Test;
+
+import com.redhat.thermostat.storage.core.Category;
+import com.redhat.thermostat.storage.core.PreparedStatement;
+import com.redhat.thermostat.storage.core.StatementDescriptor;
+import com.redhat.thermostat.storage.model.Pojo;
+import com.redhat.thermostat.web.common.SharedStateId;
+
+public class PreparedStatementManagerTest {
+    
+    /**
+     * Verify that every call to put creates a new ID entry only if
+     * statements (id'ed by descriptor) are different.
+     */
+    @Test
+    public void canCreateAndPutStmt() {
+        PreparedStatementManager manager = new PreparedStatementManager();
+        UUID serverToken = UUID.randomUUID();
+        @SuppressWarnings("unchecked")
+        PreparedStatement<TestPojo> targetStmt = mock(PreparedStatement.class);
+        @SuppressWarnings({ "unchecked" })
+        StatementDescriptor<TestPojo> testDesc = new StatementDescriptor<>(mock(Category.class), "unused");
+        Class<TestPojo> testClass = TestPojo.class;
+        SharedStateId id = manager.createAndPutHolder(serverToken, targetStmt, testClass, testDesc);
+        assertNotNull(id);
+        assertEquals(serverToken, id.getServerToken());
+        assertEquals("counter starts with 0", 0, id.getId());
+        // do it again with a different statement
+        @SuppressWarnings("unchecked")
+        PreparedStatement<TestPojo> otherTargetStmt = mock(PreparedStatement.class);
+        @SuppressWarnings({ "unchecked" })
+        StatementDescriptor<TestPojo> otherTestDesc = new StatementDescriptor<>(mock(Category.class), "other");
+        SharedStateId otherId = manager.createAndPutHolder(serverToken, otherTargetStmt, testClass, otherTestDesc);
+        assertNotNull(otherId);
+        assertFalse(id.equals(otherId));
+        assertEquals(1, otherId.getId());
+        assertEquals(serverToken, otherId.getServerToken());
+        // do it once more with same token, which should yield the same id than
+        // we used to get for the first createAndPutHolder() call.
+        SharedStateId thirdId = manager.createAndPutHolder(serverToken, targetStmt, testClass, testDesc);
+        assertEquals(id, thirdId);
+        assertSame(id, thirdId);
+    }
+    
+    /**
+     * Verify that adding a new value when the int ID would overflow throws an
+     * exception.
+     */
+    @Test
+    public void intIdOverflowThrowsException() {
+        PreparedStatementManager manager = new PreparedStatementManager(Integer.MAX_VALUE - 1);
+        UUID serverNonce = UUID.randomUUID();
+        @SuppressWarnings("unchecked")
+        PreparedStatement<TestPojo> targetStmt = mock(PreparedStatement.class);
+        @SuppressWarnings({ "unchecked" })
+        StatementDescriptor<TestPojo> testDesc = new StatementDescriptor<>(mock(Category.class), "unused");
+        Class<TestPojo> testClass = TestPojo.class;
+        try {
+            manager.createAndPutHolder(serverNonce, targetStmt, testClass, testDesc);
+            fail("Should have thrown ISE due to int id overflow");
+        } catch (IllegalStateException e) {
+            assertEquals("Too many different statements!", e.getMessage());
+        }
+    }
+    
+    /**
+     * Basic parameters must not be null.
+     */
+    @Test
+    public void rejectsNullValuesAsParameters() {
+        PreparedStatementManager manager = new PreparedStatementManager();
+        @SuppressWarnings("unchecked")
+        PreparedStatement<TestPojo> targetStmt = mock(PreparedStatement.class);
+        @SuppressWarnings({ "unchecked" })
+        StatementDescriptor<TestPojo> testDesc = new StatementDescriptor<>(mock(Category.class), "unused");
+        Class<TestPojo> testClass = TestPojo.class;
+        try {
+            manager.createAndPutHolder(null, targetStmt, testClass, testDesc);
+            fail("Expected NPE due to null server token");
+        } catch (NullPointerException e) {
+            // pass
+        }
+        UUID serverNonce = UUID.randomUUID();
+        try {
+            manager.createAndPutHolder(serverNonce, null, testClass, testDesc);
+            fail("Expected NPE due to null target stmt");
+        } catch (NullPointerException e) {
+            // pass
+        }
+        try {
+            manager.createAndPutHolder(serverNonce, targetStmt, null, testDesc);
+            fail("Expected NPE due to null data class");
+        } catch (NullPointerException e) {
+            // pass
+        }
+        try {
+            manager.createAndPutHolder(serverNonce, targetStmt, testClass, null);
+            fail("Expected NPE due to null stmt descriptor");
+        } catch (NullPointerException e) {
+            // pass
+        }
+    }
+    
+    @Test
+    public void canGetAddedHolderItemViaID() {
+        PreparedStatementManager manager = new PreparedStatementManager();
+        UUID serverNonce = UUID.randomUUID();
+        @SuppressWarnings("unchecked")
+        PreparedStatement<TestPojo> targetStmt = mock(PreparedStatement.class);
+        @SuppressWarnings({ "unchecked" })
+        StatementDescriptor<TestPojo> testDesc = new StatementDescriptor<>(mock(Category.class), "unused");
+        Class<TestPojo> testClass = TestPojo.class;
+        SharedStateId id = manager.createAndPutHolder(serverNonce, targetStmt, testClass, testDesc);
+        PreparedStatementHolder<TestPojo> holder = manager.getStatementHolder(id);
+        assertNotNull(holder);
+        assertSame(serverNonce, holder.getId().getServerToken());
+        assertSame(id, holder.getId());
+        assertSame(targetStmt, holder.getStmt());
+        assertSame(testClass, holder.getDataClass());
+        assertSame(testDesc, holder.getStatementDescriptor());
+        // do it again with an equal ID
+        SharedStateId retrievalId = new SharedStateId(0, serverNonce);
+        assertTrue(retrievalId.equals(id));
+        holder = manager.getStatementHolder(retrievalId);
+        assertEquals(id, holder.getId());
+        assertNotSame(retrievalId, holder.getId());
+        assertSame(id, holder.getId());
+        assertSame(targetStmt, holder.getStmt());
+        assertSame(testClass, holder.getDataClass());
+        assertSame(testDesc, holder.getStatementDescriptor());
+        
+        // unknown server token should return null
+        assertNull(manager.getStatementHolder(new SharedStateId(0, UUID.randomUUID())));
+    }
+    
+    @Test
+    public void canGetAddedHolderItemViaDescriptor() {
+        PreparedStatementManager manager = new PreparedStatementManager();
+        UUID serverNonce = UUID.randomUUID();
+        @SuppressWarnings("unchecked")
+        PreparedStatement<TestPojo> targetStmt = mock(PreparedStatement.class);
+        @SuppressWarnings({ "unchecked" })
+        Category<TestPojo> cat = mock(Category.class);
+        StatementDescriptor<TestPojo> testDesc = new StatementDescriptor<>(cat, "no-matter");
+        Class<TestPojo> testClass = TestPojo.class;
+        SharedStateId id = manager.createAndPutHolder(serverNonce, targetStmt, testClass, testDesc);
+        PreparedStatementHolder<TestPojo> holder = manager.getStatementHolder(testDesc);
+        assertNotNull(holder);
+        assertSame(serverNonce, holder.getId().getServerToken());
+        assertSame(id, holder.getId());
+        assertSame(targetStmt, holder.getStmt());
+        assertSame(testClass, holder.getDataClass());
+        assertSame(testDesc, holder.getStatementDescriptor());
+        
+        // do it again with an equal descriptor
+        StatementDescriptor<TestPojo> equalDesc = new StatementDescriptor<>(cat, "no-matter");
+        assertNotSame(testDesc, equalDesc);
+        assertEquals(testDesc, equalDesc);
+        holder = manager.getStatementHolder(equalDesc);
+        assertNotNull(holder);
+        assertSame(serverNonce, holder.getId().getServerToken());
+        assertSame(id, holder.getId());
+        assertSame(targetStmt, holder.getStmt());
+        assertSame(testClass, holder.getDataClass());
+        assertSame(testDesc, holder.getStatementDescriptor());
+    }
+
+    private static class TestPojo implements Pojo {
+        // nothing
+    }
+}
--- a/web/server/src/test/java/com/redhat/thermostat/web/server/WebStorageEndpointTest.java	Thu Nov 27 04:08:51 2014 -0700
+++ b/web/server/src/test/java/com/redhat/thermostat/web/server/WebStorageEndpointTest.java	Tue Nov 11 16:02:59 2014 +0100
@@ -132,12 +132,14 @@
 import com.redhat.thermostat.test.FreePortFinder;
 import com.redhat.thermostat.test.FreePortFinder.TryPort;
 import com.redhat.thermostat.web.common.PreparedStatementResponseCode;
+import com.redhat.thermostat.web.common.SharedStateId;
 import com.redhat.thermostat.web.common.WebPreparedStatement;
 import com.redhat.thermostat.web.common.WebPreparedStatementResponse;
 import com.redhat.thermostat.web.common.WebQueryResponse;
 import com.redhat.thermostat.web.common.typeadapters.PojoTypeAdapterFactory;
 import com.redhat.thermostat.web.common.typeadapters.PreparedParameterTypeAdapterFactory;
 import com.redhat.thermostat.web.common.typeadapters.PreparedParametersTypeAdapterFactory;
+import com.redhat.thermostat.web.common.typeadapters.SharedStateIdTypeAdapterFactory;
 import com.redhat.thermostat.web.common.typeadapters.WebPreparedStatementResponseTypeAdapterFactory;
 import com.redhat.thermostat.web.common.typeadapters.WebPreparedStatementTypeAdapterFactory;
 import com.redhat.thermostat.web.common.typeadapters.WebQueryResponseTypeAdapterFactory;
@@ -335,6 +337,7 @@
         conn.setDoOutput(true);
         Gson gson = new GsonBuilder()
                         .registerTypeAdapterFactory(new PojoTypeAdapterFactory())
+                        .registerTypeAdapterFactory(new SharedStateIdTypeAdapterFactory())
                         .registerTypeAdapterFactory(new WebPreparedStatementResponseTypeAdapterFactory())
                         .registerTypeAdapterFactory(new WebQueryResponseTypeAdapterFactory())
                         .registerTypeAdapterFactory(new PreparedParameterTypeAdapterFactory())
@@ -348,7 +351,7 @@
         Reader in = new InputStreamReader(conn.getInputStream());
         WebPreparedStatementResponse response = gson.fromJson(in, WebPreparedStatementResponse.class);
         assertEquals("descriptor not trusted, so expected number should be negative!", -1, response.getNumFreeVariables());
-        assertEquals(WebPreparedStatementResponse.ILLEGAL_STATEMENT, response.getStatementId());
+        assertEquals(WebPreparedStatementResponse.ILLEGAL_STATEMENT, response.getStatementId().getId());
         assertEquals("application/json; charset=UTF-8", conn.getContentType());
     }
 
@@ -371,8 +374,8 @@
         TrustedPreparedQueryTestResult prepareQueryResult = prepareQuery(strDescriptor, moreBatches);
         
         Type typeToken = new TypeToken<WebQueryResponse<TestClass>>(){}.getType();
-        // now execute the query we've just prepared
-        WebPreparedStatement<TestClass> stmt = new WebPreparedStatement<>(1, 0);
+
+        WebPreparedStatement<TestClass> stmt = new WebPreparedStatement<>(1, prepareQueryResult.stmtId);
         stmt.setString(0, "fluff");
         
         // Execute the query, preserver the cookie
@@ -407,7 +410,7 @@
         getMoreConn.setDoOutput(true);
         
         OutputStreamWriter out = new OutputStreamWriter(getMoreConn.getOutputStream());
-        String body = "prepared-stmt-id=" + stmt.getStatementId() + "&";
+        String body = "prepared-stmt-id=" + URLEncoder.encode(prepareQueryResult.gson.toJson(stmt.getStatementId()), "UTF-8") + "&";
         body += "cursor-id=" + cursorId + "&";
         body += "batch-size=" + batchSize;
         out.write(body);
@@ -442,8 +445,8 @@
         TrustedPreparedQueryTestResult prepareQueryResult = prepareQuery(strDescriptor, moreBatches);
         
         Type typeToken = new TypeToken<WebQueryResponse<TestClass>>(){}.getType();
-        // now execute the query we've just prepared
-        WebPreparedStatement<TestClass> stmt = new WebPreparedStatement<>(1, 0);
+
+        WebPreparedStatement<TestClass> stmt = new WebPreparedStatement<>(1, prepareQueryResult.stmtId);
         stmt.setString(0, "fluff");
         
         // Execute the query, preserver the cookie
@@ -478,7 +481,7 @@
         getMoreConn.setDoOutput(true);
         
         OutputStreamWriter out = new OutputStreamWriter(getMoreConn.getOutputStream());
-        String body = "prepared-stmt-id=" + stmt.getStatementId() + "&";
+        String body = "prepared-stmt-id=" + URLEncoder.encode(prepareQueryResult.gson.toJson(stmt.getStatementId()), "UTF-8") + "&";
         body += "cursor-id=" + cursorId + "&";
         body += "batch-size=" + batchSize;
         out.write(body);
@@ -533,11 +536,13 @@
         private final Gson gson;
         private final Query<TestClass> mockMongoQuery;
         private final BatchCursor<TestClass> cursor;
+        private final SharedStateId stmtId;
         
-        private TrustedPreparedQueryTestResult(Gson gson, Query<TestClass> mockMongoQuery, BatchCursor<TestClass> cursor) {
+        private TrustedPreparedQueryTestResult(Gson gson, Query<TestClass> mockMongoQuery, BatchCursor<TestClass> cursor, SharedStateId stmtId) {
             this.cursor = cursor;
             this.gson = gson;
             this.mockMongoQuery = mockMongoQuery;
+            this.stmtId = stmtId;
         }
     }
     
@@ -585,6 +590,7 @@
         conn.setDoOutput(true);
         Gson gson = new GsonBuilder()
                             .registerTypeAdapterFactory(new PojoTypeAdapterFactory())
+                            .registerTypeAdapterFactory(new SharedStateIdTypeAdapterFactory())
                             .registerTypeAdapterFactory(new WebPreparedStatementResponseTypeAdapterFactory())
                             .registerTypeAdapterFactory(new WebQueryResponseTypeAdapterFactory())
                             .registerTypeAdapterFactory(new PreparedParameterTypeAdapterFactory())
@@ -599,10 +605,10 @@
         Reader in = new InputStreamReader(conn.getInputStream());
         WebPreparedStatementResponse response = gson.fromJson(in, WebPreparedStatementResponse.class);
         assertEquals(1, response.getNumFreeVariables());
-        assertEquals(0, response.getStatementId());
+        assertEquals(0, response.getStatementId().getId());
         assertEquals("application/json; charset=UTF-8", conn.getContentType());
         
-        return new TrustedPreparedQueryTestResult(gson, mockMongoQuery, cursor);
+        return new TrustedPreparedQueryTestResult(gson, mockMongoQuery, cursor, response.getStatementId());
     }
 
     private String setupPreparedQueryWithTrustedDescriptor() throws Exception {
@@ -715,6 +721,7 @@
             conn.setDoOutput(true);
             Gson gson = new GsonBuilder()
                                 .registerTypeAdapterFactory(new PojoTypeAdapterFactory())
+                                .registerTypeAdapterFactory(new SharedStateIdTypeAdapterFactory())
                                 .registerTypeAdapterFactory(new WebPreparedStatementResponseTypeAdapterFactory())
                                 .registerTypeAdapterFactory(new WebQueryResponseTypeAdapterFactory())
                                 .registerTypeAdapterFactory(new PreparedParameterTypeAdapterFactory())
@@ -729,13 +736,13 @@
             Reader in = new InputStreamReader(conn.getInputStream());
             WebPreparedStatementResponse response = gson.fromJson(in, WebPreparedStatementResponse.class);
             assertEquals(1, response.getNumFreeVariables());
-            assertEquals(0, response.getStatementId());
+            assertEquals(0, response.getStatementId().getId());
             assertEquals("application/json; charset=UTF-8", conn.getContentType());
             
             
             
             // now execute the query we've just prepared
-            WebPreparedStatement<TestClass> stmt = new WebPreparedStatement<>(1, 0);
+            WebPreparedStatement<TestClass> stmt = new WebPreparedStatement<>(1, response.getStatementId());
             stmt.setString(0, "fluff");
             
             url = new URL(endpoint + "/query-execute");
@@ -837,6 +844,7 @@
         conn.setDoOutput(true);
         Gson gson = new GsonBuilder()
                             .registerTypeAdapterFactory(new PojoTypeAdapterFactory())
+                            .registerTypeAdapterFactory(new SharedStateIdTypeAdapterFactory())
                             .registerTypeAdapterFactory(new WebPreparedStatementResponseTypeAdapterFactory())
                             .registerTypeAdapterFactory(new WebQueryResponseTypeAdapterFactory())
                             .registerTypeAdapterFactory(new PreparedParameterTypeAdapterFactory())
@@ -851,13 +859,13 @@
         Reader in = new InputStreamReader(conn.getInputStream());
         WebPreparedStatementResponse response = gson.fromJson(in, WebPreparedStatementResponse.class);
         assertEquals(0, response.getNumFreeVariables());
-        assertEquals(0, response.getStatementId());
+        assertEquals(0, response.getStatementId().getId());
         assertEquals("application/json; charset=UTF-8", conn.getContentType());
         
         
         
         // now execute the query we've just prepared
-        WebPreparedStatement<AggregateCount> stmt = new WebPreparedStatement<>(0, 0);
+        WebPreparedStatement<AggregateCount> stmt = new WebPreparedStatement<>(0, response.getStatementId());
         
         url = new URL(endpoint + "/query-execute");
         HttpURLConnection conn2 = (HttpURLConnection) url.openConnection();
@@ -968,6 +976,7 @@
             conn.setDoOutput(true);
             Gson gson = new GsonBuilder()
                 .registerTypeAdapterFactory(new PojoTypeAdapterFactory())
+                .registerTypeAdapterFactory(new SharedStateIdTypeAdapterFactory())
                 .registerTypeAdapterFactory(new PreparedParameterTypeAdapterFactory())
                 .registerTypeAdapterFactory(new WebPreparedStatementTypeAdapterFactory())
                 .registerTypeAdapterFactory(new WebPreparedStatementResponseTypeAdapterFactory())
@@ -981,13 +990,13 @@
             Reader in = new InputStreamReader(conn.getInputStream());
             WebPreparedStatementResponse response = gson.fromJson(in, WebPreparedStatementResponse.class);
             assertEquals(2, response.getNumFreeVariables());
-            assertEquals(0, response.getStatementId());
+            assertEquals(0, response.getStatementId().getId());
             assertEquals("application/json; charset=UTF-8", conn.getContentType());
             
             
             
             // now execute the ADD we've just prepared
-            WebPreparedStatement<TestClass> stmt = new WebPreparedStatement<>(2, 0);
+            WebPreparedStatement<TestClass> stmt = new WebPreparedStatement<>(2, response.getStatementId());
             stmt.setString(0, "fluff");
             stmt.setString(1, "test2");