View Javadoc
1   /*
2    *    Copyright 2009-2023 the original author or authors.
3    *
4    *    Licensed under the Apache License, Version 2.0 (the "License");
5    *    you may not use this file except in compliance with the License.
6    *    You may obtain a copy of the License at
7    *
8    *       https://www.apache.org/licenses/LICENSE-2.0
9    *
10   *    Unless required by applicable law or agreed to in writing, software
11   *    distributed under the License is distributed on an "AS IS" BASIS,
12   *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   *    See the License for the specific language governing permissions and
14   *    limitations under the License.
15   */
16  package org.apache.ibatis.cache.decorators;
17  
18  import java.util.concurrent.ConcurrentHashMap;
19  import java.util.concurrent.CountDownLatch;
20  import java.util.concurrent.TimeUnit;
21  
22  import org.apache.ibatis.cache.Cache;
23  import org.apache.ibatis.cache.CacheException;
24  
25  /**
26   * <p>
27   * Simple blocking decorator
28   * <p>
29   * Simple and inefficient version of EhCache's BlockingCache decorator. It sets a lock over a cache key when the element
30   * is not found in cache. This way, other threads will wait until this element is filled instead of hitting the
31   * database.
32   * <p>
33   * By its nature, this implementation can cause deadlock when used incorrectly.
34   *
35   * @author Eduardo Macarron
36   */
37  public class BlockingCache implements Cache {
38  
39    private long timeout;
40    private final Cache delegate;
41    private final ConcurrentHashMap<Object, CountDownLatch> locks;
42  
43    public BlockingCache(Cache delegate) {
44      this.delegate = delegate;
45      this.locks = new ConcurrentHashMap<>();
46    }
47  
48    @Override
49    public String getId() {
50      return delegate.getId();
51    }
52  
53    @Override
54    public int getSize() {
55      return delegate.getSize();
56    }
57  
58    @Override
59    public void putObject(Object key, Object value) {
60      try {
61        delegate.putObject(key, value);
62      } finally {
63        releaseLock(key);
64      }
65    }
66  
67    @Override
68    public Object getObject(Object key) {
69      acquireLock(key);
70      Object value = delegate.getObject(key);
71      if (value != null) {
72        releaseLock(key);
73      }
74      return value;
75    }
76  
77    @Override
78    public Object removeObject(Object key) {
79      // despite its name, this method is called only to release locks
80      releaseLock(key);
81      return null;
82    }
83  
84    @Override
85    public void clear() {
86      delegate.clear();
87    }
88  
89    private void acquireLock(Object key) {
90      CountDownLatch newLatch = new CountDownLatch(1);
91      while (true) {
92        CountDownLatch latch = locks.putIfAbsent(key, newLatch);
93        if (latch == null) {
94          break;
95        }
96        try {
97          if (timeout > 0) {
98            boolean acquired = latch.await(timeout, TimeUnit.MILLISECONDS);
99            if (!acquired) {
100             throw new CacheException(
101                 "Couldn't get a lock in " + timeout + " for the key " + key + " at the cache " + delegate.getId());
102           }
103         } else {
104           latch.await();
105         }
106       } catch (InterruptedException e) {
107         throw new CacheException("Got interrupted while trying to acquire lock for key " + key, e);
108       }
109     }
110   }
111 
112   private void releaseLock(Object key) {
113     CountDownLatch latch = locks.remove(key);
114     if (latch == null) {
115       throw new IllegalStateException("Detected an attempt at releasing unacquired lock. This should never happen.");
116     }
117     latch.countDown();
118   }
119 
120   public long getTimeout() {
121     return timeout;
122   }
123 
124   public void setTimeout(long timeout) {
125     this.timeout = timeout;
126   }
127 }