You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cayenne.apache.org by nt...@apache.org on 2017/02/10 15:23:44 UTC

cayenne git commit: CAY-2225 New Cache Invalidation filter + module builder to include it into cayenne runtime

Repository: cayenne
Updated Branches:
  refs/heads/master 4e54f8e2e -> 871574bcc


CAY-2225 New Cache Invalidation filter + module builder to include it into cayenne runtime


Project: http://git-wip-us.apache.org/repos/asf/cayenne/repo
Commit: http://git-wip-us.apache.org/repos/asf/cayenne/commit/871574bc
Tree: http://git-wip-us.apache.org/repos/asf/cayenne/tree/871574bc
Diff: http://git-wip-us.apache.org/repos/asf/cayenne/diff/871574bc

Branch: refs/heads/master
Commit: 871574bcc0d2778c5421a677959eaedb8e19c58b
Parents: 4e54f8e
Author: Nikita Timofeev <st...@gmail.com>
Authored: Fri Feb 10 18:19:03 2017 +0300
Committer: Nikita Timofeev <st...@gmail.com>
Committed: Fri Feb 10 18:19:03 2017 +0300

----------------------------------------------------------------------
 .../lifecycle/cache/CacheGroupsHandler.java     |  59 +++++++++
 .../cache/CacheInvalidationFilter.java          | 124 ++++++++++++-------
 .../cache/CacheInvalidationModuleBuilder.java   |  84 +++++++++++++
 .../lifecycle/cache/InvalidationFunction.java   |  36 ++++++
 .../lifecycle/cache/InvalidationHandler.java    |  35 ++++++
 .../lifecycle/cache/CacheInvalidationIT.java    |  77 ++++++++++++
 .../lifecycle/unit/CacheInvalidationCase.java   |  83 +++++++++++++
 docs/doc/src/main/resources/RELEASE-NOTES.txt   |   1 +
 8 files changed, 455 insertions(+), 44 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/cayenne/blob/871574bc/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/cache/CacheGroupsHandler.java
----------------------------------------------------------------------
diff --git a/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/cache/CacheGroupsHandler.java b/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/cache/CacheGroupsHandler.java
new file mode 100644
index 0000000..330a30c
--- /dev/null
+++ b/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/cache/CacheGroupsHandler.java
@@ -0,0 +1,59 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ ****************************************************************/
+
+package org.apache.cayenne.lifecycle.cache;
+
+import java.util.Collection;
+
+import org.apache.cayenne.Persistent;
+
+import static java.util.Arrays.asList;
+
+/**
+ * @since 4.0
+ */
+public class CacheGroupsHandler implements InvalidationHandler {
+
+    /**
+     * Return invalidation function that returns values
+     * of {@link CacheGroups} annotations for the given type.
+     */
+    @Override
+    public InvalidationFunction canHandle(Class<? extends Persistent> type) {
+
+        CacheGroups a = type.getAnnotation(CacheGroups.class);
+        if (a == null) {
+            return null;
+        }
+
+        String[] groups = a.value();
+        if (groups.length == 0) {
+            return null;
+        }
+
+        final Collection<String> groupsList = asList(groups);
+        return new InvalidationFunction() {
+            @Override
+            public Collection<String> apply(Persistent persistent) {
+                return groupsList;
+            }
+        };
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/871574bc/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/cache/CacheInvalidationFilter.java
----------------------------------------------------------------------
diff --git a/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/cache/CacheInvalidationFilter.java b/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/cache/CacheInvalidationFilter.java
index aaf66d1..f18a668 100644
--- a/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/cache/CacheInvalidationFilter.java
+++ b/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/cache/CacheInvalidationFilter.java
@@ -16,71 +16,98 @@
  *  specific language governing permissions and limitations
  *  under the License.
  ****************************************************************/
+
 package org.apache.cayenne.lifecycle.cache;
 
 import java.util.Collection;
+import java.util.Collections;
 import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
 import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
 
 import org.apache.cayenne.DataChannel;
 import org.apache.cayenne.DataChannelFilter;
 import org.apache.cayenne.DataChannelFilterChain;
 import org.apache.cayenne.ObjectContext;
+import org.apache.cayenne.Persistent;
 import org.apache.cayenne.QueryResponse;
-import org.apache.cayenne.access.DataContext;
 import org.apache.cayenne.annotation.PrePersist;
 import org.apache.cayenne.annotation.PreRemove;
 import org.apache.cayenne.annotation.PreUpdate;
 import org.apache.cayenne.cache.QueryCache;
+import org.apache.cayenne.di.Inject;
+import org.apache.cayenne.di.Provider;
 import org.apache.cayenne.graph.GraphDiff;
 import org.apache.cayenne.query.Query;
 
 /**
- * A {@link DataChannelFilter} that invalidates cache groups defined for mapped entities
- * via {@link CacheGroups} annotations.
- * 
+ * <p>
+ * A {@link DataChannelFilter} that invalidates cache groups.
+ * Use custom rules for invalidation provided via DI.
+ * </p>
+ * <p>
+ * Default rule is based on entities' {@link CacheGroups} annotation.
+ * </p>
+ * <p>
+ *     To add default filter: <pre>
+ *         ServerRuntime.builder("cayenne-project.xml")
+ *              .addModule(CacheInvalidationModuleBuilder.builder().build());
+ *     </pre>
+ * </p>
+ *
  * @since 3.1
+ * @see InvalidationHandler
+ * @see CacheInvalidationModuleBuilder
  */
 public class CacheInvalidationFilter implements DataChannelFilter {
 
-    private final ThreadLocal<Set<String>> groups = new ThreadLocal<Set<String>>();
+    @Inject
+    private Provider<QueryCache> cacheProvider;
+
+    @Inject(CacheInvalidationModuleBuilder.INVALIDATION_HANDLERS_LIST)
+    private List<InvalidationHandler> handlers;
+
+    private final Map<Class<? extends Persistent>, InvalidationFunction> mappedHandlers;
+
+    private final InvalidationFunction skipHandler;
+
+    private final ThreadLocal<Set<String>> groups;
+
+    public CacheInvalidationFilter() {
+        mappedHandlers = new ConcurrentHashMap<>();
+        skipHandler = new InvalidationFunction() {
+            @Override
+            public Collection<String> apply(Persistent p) {
+                return Collections.emptyList();
+            }
+        };
+        groups = new ThreadLocal<>();
+    }
 
     public void init(DataChannel channel) {
         // noop
     }
 
-    public QueryResponse onQuery(
-            ObjectContext originatingContext,
-            Query query,
-            DataChannelFilterChain filterChain) {
+    public QueryResponse onQuery(ObjectContext originatingContext, Query query, DataChannelFilterChain filterChain) {
         return filterChain.onQuery(originatingContext, query);
     }
 
-    public GraphDiff onSync(
-            ObjectContext originatingContext,
-            GraphDiff changes,
-            int syncType,
-            DataChannelFilterChain filterChain) {
-
+    public GraphDiff onSync(ObjectContext originatingContext, GraphDiff changes,
+                            int syncType, DataChannelFilterChain filterChain) {
         try {
             GraphDiff result = filterChain.onSync(originatingContext, changes, syncType);
-
             // no exceptions, flush...
-
             Collection<String> groupSet = groups.get();
             if (groupSet != null && !groupSet.isEmpty()) {
-
-                // TODO: replace this with QueryCache injection once CAY-1445 is done
-                QueryCache cache = ((DataContext) originatingContext).getQueryCache();
-
+                QueryCache cache = cacheProvider.get();
                 for (String group : groupSet) {
                     cache.removeGroup(group);
                 }
             }
-
             return result;
-        }
-        finally {
+        } finally {
             groups.set(null);
         }
     }
@@ -88,31 +115,40 @@ public class CacheInvalidationFilter implements DataChannelFilter {
     /**
      * A callback method that records cache group to flush at the end of the commit.
      */
-    @PrePersist(entityAnnotations = CacheGroups.class)
-    @PreRemove(entityAnnotations = CacheGroups.class)
-    @PreUpdate(entityAnnotations = CacheGroups.class)
+    @PrePersist
+    @PreRemove
+    @PreUpdate
     protected void preCommit(Object object) {
-
-        Set<String> groupSet = groups.get();
-        if (groupSet == null) {
-            groupSet = new HashSet<String>();
-            groups.set(groupSet);
+        // TODO: for some reason we can't use Persistent as the argument type... (is it fixed in Cayenne 4.0.M4?)
+        Persistent p = (Persistent) object;
+
+        InvalidationFunction invalidationFunction = mappedHandlers.get(p.getClass());
+        if(invalidationFunction == null) {
+            invalidationFunction = skipHandler;
+            for (InvalidationHandler handler : handlers) {
+                InvalidationFunction function = handler.canHandle(p.getClass());
+                if (function != null) {
+                    invalidationFunction = function;
+                    break;
+                }
+            }
+            mappedHandlers.put(p.getClass(), invalidationFunction);
         }
 
-        addCacheGroups(groupSet, object);
+        Collection<String> objectGroups = invalidationFunction.apply(p);
+        if (!objectGroups.isEmpty()) {
+            getOrCreateTxGroups().addAll(objectGroups);
+        }
     }
 
-    /**
-     * A method that builds a list of cache groups for a given object and adds them to the
-     * invalidation group set. This implementation adds all groups defined via
-     * {@link CacheGroups} annotation for a given class. Subclasses may override this
-     * method to provide more fine-grained filtering of cache groups to invalidate, based
-     * on the state of the object.
-     */
-    protected void addCacheGroups(Set<String> groupSet, Object object) {
-        CacheGroups a = object.getClass().getAnnotation(CacheGroups.class);
-        for (String group : a.value()) {
-            groupSet.add(group);
+
+    protected Set<String> getOrCreateTxGroups() {
+        Set<String> txGroups = groups.get();
+        if (txGroups == null) {
+            txGroups = new HashSet<>();
+            groups.set(txGroups);
         }
+
+        return txGroups;
     }
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/871574bc/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/cache/CacheInvalidationModuleBuilder.java
----------------------------------------------------------------------
diff --git a/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/cache/CacheInvalidationModuleBuilder.java b/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/cache/CacheInvalidationModuleBuilder.java
new file mode 100644
index 0000000..d6ce504
--- /dev/null
+++ b/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/cache/CacheInvalidationModuleBuilder.java
@@ -0,0 +1,84 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ ****************************************************************/
+
+package org.apache.cayenne.lifecycle.cache;
+
+import java.util.Collection;
+import java.util.HashSet;
+
+import org.apache.cayenne.configuration.Constants;
+import org.apache.cayenne.di.Binder;
+import org.apache.cayenne.di.ListBuilder;
+import org.apache.cayenne.di.Module;
+import org.apache.cayenne.tx.TransactionFilter;
+
+/**
+ * @since 4.0
+ */
+public class CacheInvalidationModuleBuilder {
+
+    public static final String INVALIDATION_HANDLERS_LIST = "cayenne.querycache.invalidation_handlers";
+
+    private Collection<Class<? extends InvalidationHandler>> handlerTypes;
+
+    private Collection<InvalidationHandler> handlerInstances;
+
+    public static CacheInvalidationModuleBuilder builder() {
+        return new CacheInvalidationModuleBuilder();
+    }
+
+    private static ListBuilder<InvalidationHandler> contributeInvalidationHandler(Binder binder) {
+        return binder.bindList(INVALIDATION_HANDLERS_LIST);
+    }
+
+    CacheInvalidationModuleBuilder() {
+        this.handlerTypes = new HashSet<>();
+        this.handlerInstances = new HashSet<>();
+    }
+
+    public CacheInvalidationModuleBuilder invalidationHandler(Class<? extends InvalidationHandler> handlerType) {
+        handlerTypes.add(handlerType);
+        return this;
+    }
+
+    public CacheInvalidationModuleBuilder invalidationHandler(InvalidationHandler handlerInstance) {
+        handlerInstances.add(handlerInstance);
+        return this;
+    }
+
+    public Module build() {
+        return new Module() {
+            @Override
+            public void configure(Binder binder) {
+                ListBuilder<InvalidationHandler> handlers = contributeInvalidationHandler(binder);
+
+                handlers.add(CacheGroupsHandler.class);
+                handlers.addAll(handlerInstances);
+
+                for(Class<? extends InvalidationHandler> handlerType : handlerTypes) {
+                    handlers.add(handlerType);
+                }
+
+                // want the filter to be INSIDE transaction
+                binder.bindList(Constants.SERVER_DOMAIN_FILTERS_LIST)
+                        .add(CacheInvalidationFilter.class).before(TransactionFilter.class);
+            }
+        };
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/871574bc/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/cache/InvalidationFunction.java
----------------------------------------------------------------------
diff --git a/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/cache/InvalidationFunction.java b/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/cache/InvalidationFunction.java
new file mode 100644
index 0000000..8ae16db
--- /dev/null
+++ b/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/cache/InvalidationFunction.java
@@ -0,0 +1,36 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ ****************************************************************/
+
+package org.apache.cayenne.lifecycle.cache;
+
+import java.util.Collection;
+
+import org.apache.cayenne.Persistent;
+
+/**
+ * @since 4.0
+ */
+public interface InvalidationFunction {
+
+    /**
+     * @return collection of cache groups to invalidate for given object
+     */
+    Collection<String> apply(Persistent persistent);
+
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/871574bc/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/cache/InvalidationHandler.java
----------------------------------------------------------------------
diff --git a/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/cache/InvalidationHandler.java b/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/cache/InvalidationHandler.java
new file mode 100644
index 0000000..a667ead
--- /dev/null
+++ b/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/cache/InvalidationHandler.java
@@ -0,0 +1,35 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ ****************************************************************/
+
+package org.apache.cayenne.lifecycle.cache;
+
+import org.apache.cayenne.Persistent;
+
+/**
+ * A pluggable handler to invalidate cache groups on changes in certain objects.
+ * @since 4.0
+ */
+public interface InvalidationHandler {
+
+    /**
+     * @return invalidation function or null if there is nothing to invalidate
+     */
+    InvalidationFunction canHandle(Class<? extends Persistent> type);
+
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/871574bc/cayenne-lifecycle/src/test/java/org/apache/cayenne/lifecycle/cache/CacheInvalidationIT.java
----------------------------------------------------------------------
diff --git a/cayenne-lifecycle/src/test/java/org/apache/cayenne/lifecycle/cache/CacheInvalidationIT.java b/cayenne-lifecycle/src/test/java/org/apache/cayenne/lifecycle/cache/CacheInvalidationIT.java
new file mode 100644
index 0000000..ff7a475
--- /dev/null
+++ b/cayenne-lifecycle/src/test/java/org/apache/cayenne/lifecycle/cache/CacheInvalidationIT.java
@@ -0,0 +1,77 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ ****************************************************************/
+
+package org.apache.cayenne.lifecycle.cache;
+
+import org.apache.cayenne.ObjectContext;
+import org.apache.cayenne.lifecycle.db.E1;
+import org.apache.cayenne.lifecycle.unit.CacheInvalidationCase;
+import org.apache.cayenne.query.ObjectSelect;
+
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * @since 4.0
+ */
+public class CacheInvalidationIT extends CacheInvalidationCase {
+
+    @Test
+    public void testInvalidate_Custom() throws Exception {
+        ObjectContext context = runtime.newContext();
+
+        // no explicit cache group must still work - it lands inside default cache called 'cayenne.default.cache'
+        ObjectSelect<E1> g0 = ObjectSelect.query(E1.class).localCache();
+        g0.setName("q0");
+        ObjectSelect<E1> g1 = ObjectSelect.query(E1.class).localCache("g1");
+        g1.setName("q1");
+        ObjectSelect<E1> g2 = ObjectSelect.query(E1.class).localCache("g2");
+        g2.setName("q2");
+
+        assertEquals(0, g0.selectCount(context));
+        assertEquals(0, g1.selectCount(context));
+        assertEquals(0, g2.selectCount(context));
+
+        e1.insert(1).insert(2);
+
+        // inserted via SQL... query results are still cached...
+        assertEquals(0, g0.selectCount(context));
+        assertEquals(0, g1.selectCount(context));
+        assertEquals(0, g2.selectCount(context));
+
+
+        E1 e1 = context.newObject(E1.class);
+        context.commitChanges();
+        runtime.getDataDomain().getQueryCache().removeGroup("g1");
+
+        // inserted via Cayenne... "g1" should get auto refreshed...
+        assertEquals(0, g0.selectCount(context));
+        assertEquals(3, g1.selectCount(context));
+        assertEquals(0, g2.selectCount(context));
+
+        context.deleteObject(e1);
+        context.commitChanges();
+
+        // deleted via Cayenne... "g1" should get auto refreshed
+        assertEquals(0, g0.selectCount(context));
+        assertEquals(2, g1.selectCount(context));
+        assertEquals(0, g2.selectCount(context));
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/871574bc/cayenne-lifecycle/src/test/java/org/apache/cayenne/lifecycle/unit/CacheInvalidationCase.java
----------------------------------------------------------------------
diff --git a/cayenne-lifecycle/src/test/java/org/apache/cayenne/lifecycle/unit/CacheInvalidationCase.java b/cayenne-lifecycle/src/test/java/org/apache/cayenne/lifecycle/unit/CacheInvalidationCase.java
new file mode 100644
index 0000000..2d17220
--- /dev/null
+++ b/cayenne-lifecycle/src/test/java/org/apache/cayenne/lifecycle/unit/CacheInvalidationCase.java
@@ -0,0 +1,83 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ ****************************************************************/
+package org.apache.cayenne.lifecycle.unit;
+
+import java.util.Collection;
+import java.util.Collections;
+
+import org.apache.cayenne.Persistent;
+import org.apache.cayenne.configuration.server.ServerRuntime;
+import org.apache.cayenne.configuration.server.ServerRuntimeBuilder;
+import org.apache.cayenne.di.Module;
+import org.apache.cayenne.lifecycle.cache.CacheGroups;
+import org.apache.cayenne.lifecycle.cache.CacheInvalidationModuleBuilder;
+import org.apache.cayenne.lifecycle.cache.InvalidationFunction;
+import org.apache.cayenne.lifecycle.cache.InvalidationHandler;
+import org.apache.cayenne.test.jdbc.DBHelper;
+import org.apache.cayenne.test.jdbc.TableHelper;
+import org.junit.After;
+import org.junit.Before;
+
+public class CacheInvalidationCase {
+
+	protected ServerRuntime runtime;
+
+	protected TableHelper e1;
+
+	@Before
+	public void startCayenne() throws Exception {
+		this.runtime = configureCayenne().build();
+
+		DBHelper dbHelper = new DBHelper(runtime.getDataSource());
+
+		this.e1 = new TableHelper(dbHelper, "E1").setColumns("ID");
+		this.e1.deleteAll();
+	}
+
+	protected ServerRuntimeBuilder configureCayenne() {
+		Module cacheInvalidationModule = CacheInvalidationModuleBuilder
+				.builder()
+				.invalidationHandler(G1InvalidationHandler.class)
+				.build();
+
+		return ServerRuntime.builder()
+				.addModule(cacheInvalidationModule)
+				.addConfig("cayenne-lifecycle.xml");
+	}
+
+	@After
+	public void shutdownCayenne() {
+		if (runtime != null) {
+			runtime.shutdown();
+		}
+	}
+
+	public static class G1InvalidationHandler implements InvalidationHandler {
+		@Override
+		public InvalidationFunction canHandle(Class<? extends Persistent> type) {
+			return new InvalidationFunction() {
+				@Override
+				public Collection<String> apply(Persistent persistent) {
+					return Collections.singleton("g1");
+				}
+			};
+		}
+	}
+
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/871574bc/docs/doc/src/main/resources/RELEASE-NOTES.txt
----------------------------------------------------------------------
diff --git a/docs/doc/src/main/resources/RELEASE-NOTES.txt b/docs/doc/src/main/resources/RELEASE-NOTES.txt
index b94f929..7ea7ac7 100644
--- a/docs/doc/src/main/resources/RELEASE-NOTES.txt
+++ b/docs/doc/src/main/resources/RELEASE-NOTES.txt
@@ -29,6 +29,7 @@ CAY-2187 Support for the scalar and aggregate SQL functions in ObjectSelect API
 CAY-2197 Update sqlite version and enable in-memory default config
 CAY-2212 cdbimport cleanup and configuration schema refactoring
 CAY-2223 JCacheQueryCache - a query cache provider to plug in JCache implementers
+CAY-2225 Extensible CacheInvalidationFilter logic
 
 Bug Fixes: