Skip to content

Commit

Permalink
Fix for mybatis#116. Default implementation for a blocking cache and …
Browse files Browse the repository at this point in the history
…support

for EhCache's impl provided by Iwao
  • Loading branch information
emacarron committed Sep 28, 2014
1 parent ea41358 commit 8638f0e
Show file tree
Hide file tree
Showing 14 changed files with 451 additions and 49 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,6 @@

boolean readWrite() default true;

boolean blocking() default false;

}
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ public Cache useNewCache(Class<? extends Cache> typeClass,
Long flushInterval,
Integer size,
boolean readWrite,
boolean blocking,
Properties props) {
typeClass = valueOrDefault(typeClass, PerpetualCache.class);
evictionClass = valueOrDefault(evictionClass, LruCache.class);
Expand All @@ -135,6 +136,7 @@ public Cache useNewCache(Class<? extends Cache> typeClass,
.clearInterval(flushInterval)
.size(size)
.readWrite(readWrite)
.blocking(blocking)
.properties(props)
.build();
configuration.addCache(cache);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ private void parseCache() {
if (cacheDomain != null) {
Integer size = cacheDomain.size() == 0 ? null : cacheDomain.size();
Long flushInterval = cacheDomain.flushInterval() == 0 ? null : cacheDomain.flushInterval();
assistant.useNewCache(cacheDomain.implementation(), cacheDomain.eviction(), flushInterval, size, cacheDomain.readWrite(), null);
assistant.useNewCache(cacheDomain.implementation(), cacheDomain.eviction(), flushInterval, size, cacheDomain.readWrite(), cacheDomain.blocking(), null);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -205,8 +205,9 @@ private void cacheElement(XNode context) throws Exception {
Long flushInterval = context.getLongAttribute("flushInterval");
Integer size = context.getIntAttribute("size");
boolean readWrite = !context.getBooleanAttribute("readOnly", false);
boolean blocking = context.getBooleanAttribute("blocking", false);
Properties props = context.getChildrenAsProperties();
builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, props);
builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ eviction CDATA #IMPLIED
flushInterval CDATA #IMPLIED
size CDATA #IMPLIED
readOnly CDATA #IMPLIED
blocking CDATA #IMPLIED
>

<!ELEMENT parameterMap (parameter+)?>
Expand Down
128 changes: 128 additions & 0 deletions src/main/java/org/apache/ibatis/cache/decorators/BlockingCache.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
package org.apache.ibatis.cache.decorators;
/*
* Copyright 2009-2014 the original author or authors.
*
* Licensed 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.
*/

import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantLock;

import org.apache.ibatis.cache.Cache;
import org.apache.ibatis.cache.CacheException;

/**
* Simple blocking decorator
*
* Sipmle and inefficient version of EhCache's BlockingCache decorator.
* It sets a lock over a cache key when the element is not found in cache.
* This way, other threads will wait until this element is filled instead of hitting the database.
*
* @author Eduardo Macarron
*
*/
public class BlockingCache implements Cache {

private long timeout;
private final Cache delegate;
private final ConcurrentHashMap<Object, ReentrantLock> locks;

public BlockingCache(Cache delegate) {
this.delegate = delegate;
this.locks = new ConcurrentHashMap<Object, ReentrantLock>();
}

@Override
public String getId() {
return delegate.getId();
}

@Override
public int getSize() {
return delegate.getSize();
}

@Override
public void putObject(Object key, Object value) {
try {
delegate.putObject(key, value);
} finally {
releaseLock(key);
}
}

@Override
public Object getObject(Object key) {
acquireLock(key);
Object value = delegate.getObject(key);
if (value != null) {
releaseLock(key);
}
return value;
}

@Override
public Object removeObject(Object key) {
return delegate.removeObject(key);
}

@Override
public void clear() {
delegate.clear();
}

@Override
public ReadWriteLock getReadWriteLock() {
return null;
}

private ReentrantLock getLockForKey(Object key) {
ReentrantLock lock = new ReentrantLock();
ReentrantLock previous = locks.putIfAbsent(key, lock);
return previous == null ? lock : previous;
}

private void acquireLock(Object key) {
Lock lock = getLockForKey(key);
if (timeout > 0) {
try {
boolean acquired = lock.tryLock(timeout, TimeUnit.MILLISECONDS);
if (!acquired) {
throw new CacheException("Couldn't get a lock in " + timeout + " for the key " + key + " at the cache " + delegate.getId());
}
} catch (InterruptedException e) {
throw new CacheException("Got interrupted while trying to acquire lock for key " + key, e);
}
} else {
lock.lock();
}
}

private void releaseLock(Object key) {
ReentrantLock lock = locks.get(key);
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}

public long getTimeout() {
return timeout;
}

public void setTimeout(long timeout) {
this.timeout = timeout;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,26 +16,36 @@
package org.apache.ibatis.cache.decorators;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.locks.ReadWriteLock;

import org.apache.ibatis.cache.Cache;

/**
* The 2nd level cache transactional buffer.
*
* This class holds all cache entries that are to be added to the 2nd level cache during a Session.
* Entries are sent to the cache when commit is called or discarded if the Session is rolled back.
* Blocking cache support has been added. Therefore any get() that returns a cache miss
* will be followed by a put() so any lock associated with the key can be released.
*
* @author Clinton Begin
* @author Eduardo Macarron
*/
public class TransactionalCache implements Cache {

private Cache delegate;
private boolean clearOnCommit;
private Map<Object, AddEntry> entriesToAddOnCommit;
private Map<Object, RemoveEntry> entriesToRemoveOnCommit;
private Map<Object, Object> entriesToAddOnCommit;
private Set<Object> entriesMissedInCache;

public TransactionalCache(Cache delegate) {
this.delegate = delegate;
this.clearOnCommit = false;
this.entriesToAddOnCommit = new HashMap<Object, AddEntry>();
this.entriesToRemoveOnCommit = new HashMap<Object, RemoveEntry>();
this.entriesToAddOnCommit = new HashMap<Object, Object>();
this.entriesMissedInCache = new HashSet<Object>();
}

@Override
Expand All @@ -50,8 +60,17 @@ public int getSize() {

@Override
public Object getObject(Object key) {
// issue #116
Object object = delegate.getObject(key);
if (object == null) {
entriesMissedInCache.add(key);
}
// issue #146
return clearOnCommit ? null : delegate.getObject(key);
if (clearOnCommit) {
return null;
} else {
return object;
}
}

@Override
Expand All @@ -61,74 +80,53 @@ public ReadWriteLock getReadWriteLock() {

@Override
public void putObject(Object key, Object object) {
entriesToRemoveOnCommit.remove(key);
entriesToAddOnCommit.put(key, new AddEntry(delegate, key, object));
entriesToAddOnCommit.put(key, object);
}

@Override
public Object removeObject(Object key) {
entriesToAddOnCommit.remove(key);
entriesToRemoveOnCommit.put(key, new RemoveEntry(delegate, key));
return delegate.getObject(key);
return null;
}

@Override
public void clear() {
reset();
clearOnCommit = true;
entriesToAddOnCommit.clear();
}

public void commit() {
if (clearOnCommit) {
delegate.clear();
} else {
for (RemoveEntry entry : entriesToRemoveOnCommit.values()) {
entry.commit();
}
}
for (AddEntry entry : entriesToAddOnCommit.values()) {
entry.commit();
}
flushPendingEntries();
reset();
}

public void rollback() {
unlockMissedEntries();
reset();
}

private void reset() {
clearOnCommit = false;
entriesToRemoveOnCommit.clear();
entriesToAddOnCommit.clear();
entriesMissedInCache.clear();
}

private static class AddEntry {
private Cache cache;
private Object key;
private Object value;

public AddEntry(Cache cache, Object key, Object value) {
this.cache = cache;
this.key = key;
this.value = value;
private void flushPendingEntries() {
for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
delegate.putObject(entry.getKey(), entry.getValue());
}

public void commit() {
cache.putObject(key, value);
for (Object entry : entriesMissedInCache) {
if (!entriesToAddOnCommit.containsKey(entry)) {
delegate.putObject(entry, null);
}
}
}

private static class RemoveEntry {
private Cache cache;
private Object key;

public RemoveEntry(Cache cache, Object key) {
this.cache = cache;
this.key = key;
}

public void commit() {
cache.removeObject(key);
private void unlockMissedEntries() {
for (Object entry : entriesMissedInCache) {
delegate.putObject(entry, null);
}
}

Expand Down
7 changes: 2 additions & 5 deletions src/main/java/org/apache/ibatis/executor/CachingExecutor.java
Original file line number Diff line number Diff line change
Expand Up @@ -93,11 +93,8 @@ public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds r
@SuppressWarnings("unchecked")
List<E> list = (List<E>) tcm.getObject(cache, key);
if (list == null) {
try {
list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
} finally {
tcm.putObject(cache, key, list); // issue #578 and #116
}
list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
tcm.putObject(cache, key, list); // issue #578 and #116
}
return list;
}
Expand Down
10 changes: 10 additions & 0 deletions src/main/java/org/apache/ibatis/mapping/CacheBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@

import org.apache.ibatis.cache.Cache;
import org.apache.ibatis.cache.CacheException;
import org.apache.ibatis.cache.decorators.BlockingCache;
import org.apache.ibatis.cache.decorators.LoggingCache;
import org.apache.ibatis.cache.decorators.LruCache;
import org.apache.ibatis.cache.decorators.ScheduledCache;
Expand All @@ -43,6 +44,7 @@ public class CacheBuilder {
private Long clearInterval;
private boolean readWrite;
private Properties properties;
private boolean blocking;

public CacheBuilder(String id) {
this.id = id;
Expand Down Expand Up @@ -76,6 +78,11 @@ public CacheBuilder readWrite(boolean readWrite) {
return this;
}

public CacheBuilder blocking(boolean blocking) {
this.blocking = blocking;
return this;
}

public CacheBuilder properties(Properties properties) {
this.properties = properties;
return this;
Expand Down Expand Up @@ -122,6 +129,9 @@ private Cache setStandardDecorators(Cache cache) {
}
cache = new LoggingCache(cache);
cache = new SynchronizedCache(cache);
if (blocking) {
cache = new BlockingCache(cache);
}
return cache;
} catch (Exception e) {
throw new CacheException("Error building standard cache decorators. Cause: " + e, e);
Expand Down
Loading

0 comments on commit 8638f0e

Please sign in to comment.