From 16fbd6db039205c8cb45116a3ccce553dcf5bbef Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Henrik=20tom=20W=C3=B6rden?= <h.tomwoerden@indiscale.com>
Date: Tue, 2 Mar 2021 10:58:32 +0000
Subject: [PATCH] EHN: ETag property for the Query

---
 CHANGELOG.md                                  |  5 +++
 .../java/org/caosdb/server/query/Query.java   | 33 +++++++++++++--
 .../org/caosdb/server/query/QueryTest.java    | 41 +++++++++++++++++++
 3 files changed, 75 insertions(+), 4 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 76e20e39..b842574a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 ### Added
 
+* `ETag` property for the query. The `ETag` is assigned to the query cache
+  each time the cache is cleared (currently whenever the server state is being
+  updated, i.e. the stored entities change).
+  This can be used to debug the query cache and also allows a client
+  to determine whether the server's state has changed between queries.
 * Basic caching for queries. The caching is enabled by default and can be
   controlled by the usual "cache" flag.
 
diff --git a/src/main/java/org/caosdb/server/query/Query.java b/src/main/java/org/caosdb/server/query/Query.java
index 0a877fc8..cd323bd9 100644
--- a/src/main/java/org/caosdb/server/query/Query.java
+++ b/src/main/java/org/caosdb/server/query/Query.java
@@ -39,6 +39,7 @@ import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
+import java.util.UUID;
 import org.antlr.v4.runtime.CharStreams;
 import org.antlr.v4.runtime.CommonTokenStream;
 import org.apache.commons.jcs.access.behavior.ICacheAccess;
@@ -238,6 +239,14 @@ public class Query implements QueryInterface, ToElementable, TransactionInterfac
    */
   private boolean cachable = true;
 
+  /**
+   * Tags the query cache and is renewed each time the cache is being cleared, i.e. each time the
+   * database is being updated.
+   *
+   * <p>As the name suggests, the idea is similar to the ETag header of the HTTP protocol.
+   */
+  private static String cacheETag = UUID.randomUUID().toString();
+
   public Type getType() {
     return this.type;
   }
@@ -668,6 +677,7 @@ public class Query implements QueryInterface, ToElementable, TransactionInterfac
 
   /** Remove all cached queries from the cache. */
   public static void clearCache() {
+    cacheETag = UUID.randomUUID().toString();
     cache.clear();
   }
 
@@ -678,10 +688,12 @@ public class Query implements QueryInterface, ToElementable, TransactionInterfac
    * @param resultSet
    */
   private void setCache(String key, List<IdVersionPair> resultSet) {
-    if (resultSet instanceof Serializable) {
-      cache.put(key, (Serializable) resultSet);
-    } else {
-      cache.put(key, new ArrayList<>(resultSet));
+    synchronized (cache) {
+      if (resultSet instanceof Serializable) {
+        cache.put(key, (Serializable) resultSet);
+      } else {
+        cache.put(key, new ArrayList<>(resultSet));
+      }
     }
   }
 
@@ -859,6 +871,7 @@ public class Query implements QueryInterface, ToElementable, TransactionInterfac
       ret.setAttribute("results", "0");
     }
     ret.setAttribute("cached", Boolean.toString(this.cached));
+    ret.setAttribute("etag", cacheETag);
 
     final Element parseTreeElem = new Element("ParseTree");
     if (this.el.hasErrors()) {
@@ -972,4 +985,16 @@ public class Query implements QueryInterface, ToElementable, TransactionInterfac
   public Role getRole() {
     return this.role;
   }
+
+  /**
+   * Return the ETag.
+   *
+   * <p>The ETag tags the query cache and is renewed each time the cache is being cleared, i.e. each
+   * time the database is being updated.
+   *
+   * @return The ETag
+   */
+  public static String getETag() {
+    return cacheETag;
+  }
 }
diff --git a/src/test/java/org/caosdb/server/query/QueryTest.java b/src/test/java/org/caosdb/server/query/QueryTest.java
index edbefab7..da652cb3 100644
--- a/src/test/java/org/caosdb/server/query/QueryTest.java
+++ b/src/test/java/org/caosdb/server/query/QueryTest.java
@@ -1,10 +1,15 @@
 package org.caosdb.server.query;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
+import static org.junit.Assert.fail;
 
 import java.io.IOException;
 import org.caosdb.server.CaosDBServer;
+import org.caosdb.server.database.access.InitAccess;
+import org.caosdb.server.transaction.WriteTransaction;
 import org.junit.BeforeClass;
 import org.junit.Test;
 
@@ -61,4 +66,40 @@ public class QueryTest {
     assertNull(q.getEntity());
     assertEquals(Query.Role.ENTITY, q.getRole());
   }
+
+  /**
+   * Assure that {@link WriteTransaction#commit()} calls {@link Query#clearCache()}.
+   *
+   * Since currently the cache shall be cleared whenever there is a commit.
+   * */
+  @Test
+  public void testEtagChangesAfterWrite() {
+    String old = Query.getETag();
+    assertNotNull(old);
+
+    WriteTransaction w =
+        new WriteTransaction(null) {
+
+          @Override
+          public boolean useCache() {
+            // this function is being overriden purely for the purpose of calling
+            // commit() (which is protected)
+            try {
+              // otherwise the test fails because getAccess() return null;
+              setAccess(new InitAccess(null));
+
+              commit();
+            } catch (Exception e) {
+              fail("this should not happen");
+            }
+            return false;
+          }
+        };
+
+    // trigger commit();
+    w.useCache();
+
+    String neu = Query.getETag();
+    assertNotEquals(old, neu, "old and new tag should not be equal");
+  }
 }
-- 
GitLab