diff --git a/CHANGELOG.md b/CHANGELOG.md index 76e20e39e9aea4cd08be4a38849452398ec5d112..b842574af4c4b190614c0c75e5bf061b1d47171e 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 0a877fc83e2f680565181655ed92c3a61cda393b..cd323bd972b21a261c84bfa6c16c08ea54558fd0 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 edbefab78fdfb5846c59a058fdfb67b55a6707cb..da652cb3e9bed9b2412dae116bb104c034a01cfe 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"); + } }