-
Notifications
You must be signed in to change notification settings - Fork 3.8k
Better MapCache concurrency #1849
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -17,14 +17,21 @@ | |
|
|
||
| package io.druid.client.cache; | ||
|
|
||
| import com.fasterxml.jackson.annotation.JacksonInject; | ||
| import com.fasterxml.jackson.annotation.JsonProperty; | ||
| import com.google.common.collect.ImmutableMap; | ||
| import com.metamx.emitter.service.ServiceEmitter; | ||
| import io.druid.query.DruidProcessingConfig; | ||
|
|
||
| import javax.validation.constraints.Min; | ||
| import java.util.Map; | ||
|
|
||
| /** | ||
| */ | ||
| public class LocalCacheProvider implements CacheProvider | ||
| { | ||
| public static final NoopCache NOOP_CACHE = new NoopCache(); | ||
|
|
||
| @JsonProperty | ||
| @Min(0) | ||
| private long sizeInBytes = 0; | ||
|
|
@@ -33,13 +40,60 @@ public class LocalCacheProvider implements CacheProvider | |
| @Min(0) | ||
| private int initialSize = 500000; | ||
|
|
||
| @JsonProperty | ||
| @Min(0) | ||
| private int logEvictionCount = 0; | ||
| @JacksonInject | ||
| private DruidProcessingConfig config = null; | ||
|
|
||
| @Override | ||
| public Cache get() | ||
| { | ||
| return new MapCache(new ByteCountingLRUMap(initialSize, logEvictionCount, sizeInBytes)); | ||
| if(sizeInBytes > 0) { | ||
| return MapCache.create(sizeInBytes, initialSize, config.getNumThreads()); | ||
| } else { | ||
| return NOOP_CACHE; | ||
| } | ||
| } | ||
|
|
||
| private static class NoopCache implements Cache | ||
| { | ||
| @Override | ||
| public byte[] get(NamedKey key) | ||
| { | ||
| return null; | ||
| } | ||
|
|
||
| @Override | ||
| public void put(NamedKey key, byte[] value) | ||
| { | ||
| } | ||
|
|
||
| @Override | ||
| public Map<NamedKey, byte[]> getBulk(Iterable<NamedKey> keys) | ||
| { | ||
| return ImmutableMap.of(); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can this be a static ImmutableMap?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ImmutableMap.of() does return a static
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. oh nice |
||
| } | ||
|
|
||
| @Override | ||
| public void close(String namespace) | ||
| { | ||
|
|
||
| } | ||
|
|
||
| @Override | ||
| public CacheStats getStats() | ||
| { | ||
| return new CacheStats(0,0,0,0,0,0,0); | ||
| } | ||
|
|
||
| @Override | ||
| public boolean isLocal() | ||
| { | ||
| return true; | ||
| } | ||
|
|
||
| @Override | ||
| public void doMonitor(ServiceEmitter emitter) | ||
| { | ||
|
|
||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -17,59 +17,77 @@ | |
|
|
||
| package io.druid.client.cache; | ||
|
|
||
| import com.google.common.cache.CacheBuilder; | ||
| import com.google.common.cache.CacheLoader; | ||
| import com.google.common.cache.Weigher; | ||
| import com.google.common.collect.Lists; | ||
| import com.google.common.collect.Maps; | ||
| import com.google.common.primitives.Ints; | ||
| import com.metamx.emitter.service.ServiceEmitter; | ||
|
|
||
| import java.nio.ByteBuffer; | ||
| import java.util.Collections; | ||
| import java.util.Iterator; | ||
| import java.util.List; | ||
| import java.util.Map; | ||
| import java.util.concurrent.atomic.AtomicInteger; | ||
| import java.util.concurrent.atomic.AtomicLong; | ||
|
|
||
| /** | ||
| */ | ||
| public class MapCache implements Cache | ||
| { | ||
| public static Cache create(long sizeInBytes) | ||
| public static MapCache create(long sizeInBytes) { | ||
| return create(sizeInBytes, 0, 4); | ||
| } | ||
| public static MapCache create(long sizeInBytes, int initialCapacity, int concurrencyLevel) | ||
| { | ||
| return new MapCache(new ByteCountingLRUMap(sizeInBytes)); | ||
| return new MapCache(sizeInBytes, initialCapacity, concurrencyLevel); | ||
| } | ||
|
|
||
| private final Map<ByteBuffer, byte[]> baseMap; | ||
| private final ByteCountingLRUMap byteCountingLRUMap; | ||
|
|
||
| private final Map<String, byte[]> namespaceId; | ||
| private final AtomicInteger ids; | ||
|
|
||
| private final Object clearLock = new Object(); | ||
| private final com.google.common.cache.Cache<ByteBuffer, byte[]> baseCache; | ||
| private final com.google.common.cache.LoadingCache<String, Integer> namespaceId; | ||
| private final AtomicInteger ids = new AtomicInteger(); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since we're doing increments can this start at |
||
|
|
||
| private final AtomicLong hitCount = new AtomicLong(0); | ||
| private final AtomicLong missCount = new AtomicLong(0); | ||
|
|
||
| MapCache( | ||
| ByteCountingLRUMap byteCountingLRUMap | ||
| ) | ||
| private MapCache(long sizeInBytes, int initialCapacity, int concurrencyLevel) | ||
| { | ||
| this.byteCountingLRUMap = byteCountingLRUMap; | ||
| this.baseMap = Collections.synchronizedMap(byteCountingLRUMap); | ||
|
|
||
| namespaceId = Maps.newHashMap(); | ||
| ids = new AtomicInteger(); | ||
| this.baseCache = CacheBuilder | ||
| .newBuilder() | ||
| .initialCapacity(initialCapacity) | ||
| .maximumWeight(sizeInBytes) | ||
| .recordStats() | ||
| .concurrencyLevel(concurrencyLevel) | ||
| .weigher( | ||
| new Weigher<ByteBuffer, byte[]>() | ||
| { | ||
| @Override | ||
| public int weigh(ByteBuffer key, byte[] value) | ||
| { | ||
| return key.remaining() + value.length; | ||
| } | ||
| } | ||
| ) | ||
| .build(); | ||
|
|
||
| this.namespaceId = CacheBuilder | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. if my understanding is correct, there is no eviction on this cache, if so, i don't see why not using a simple concurrent hash map ? |
||
| .newBuilder() | ||
| .concurrencyLevel(concurrencyLevel) | ||
| .build(new CacheLoader<String, Integer>() | ||
| { | ||
| @Override | ||
| public Integer load(String namespace) throws Exception | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. if there is no checked exception, i think you don't need |
||
| { | ||
| return ids.getAndIncrement(); | ||
| } | ||
| }); | ||
| } | ||
|
|
||
| @Override | ||
| public CacheStats getStats() | ||
| { | ||
| final com.google.common.cache.CacheStats stats = baseCache.stats(); | ||
| return new CacheStats( | ||
| hitCount.get(), | ||
| missCount.get(), | ||
| byteCountingLRUMap.size(), | ||
| byteCountingLRUMap.getNumBytes(), | ||
| byteCountingLRUMap.getEvictionCount(), | ||
| stats.hitCount(), | ||
| stats.missCount(), | ||
| baseCache.size(), | ||
| -1, | ||
| stats.evictionCount(), | ||
| 0, | ||
| 0 | ||
| ); | ||
|
|
@@ -78,24 +96,13 @@ public CacheStats getStats() | |
| @Override | ||
| public byte[] get(NamedKey key) | ||
| { | ||
| final byte[] retVal; | ||
| synchronized (clearLock) { | ||
| retVal = baseMap.get(computeKey(getNamespaceId(key.namespace), key.key)); | ||
| } | ||
| if (retVal == null) { | ||
| missCount.incrementAndGet(); | ||
| } else { | ||
| hitCount.incrementAndGet(); | ||
| } | ||
| return retVal; | ||
| return baseCache.getIfPresent(computeKey(namespaceId.getUnchecked(key.namespace), key.key)); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there a race here between get and close? whereby two threads may each execute get xor close and leave namespaceId polluted with a namespace that is assumed closed? (it is not clear that is not present in the existing impl)
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. see comment above.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| } | ||
|
|
||
| @Override | ||
| public void put(NamedKey key, byte[] value) | ||
| { | ||
| synchronized (clearLock) { | ||
| baseMap.put(computeKey(getNamespaceId(key.namespace), key.key), value); | ||
| } | ||
| baseCache.put(computeKey(namespaceId.getUnchecked(key.namespace), key.key), value); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. To prevent race conditions on namespaceId, the namespaceId can be stored, then the asMap() representation of namespaceId can be used to check and see if the namespaceId after baseCache.put is the same as when get was first called. if it is, then things are safe, if it is not, then the cache just populated should be removed. |
||
| } | ||
|
|
||
| @Override | ||
|
|
@@ -114,51 +121,25 @@ public Map<NamedKey, byte[]> getBulk(Iterable<NamedKey> keys) | |
| @Override | ||
| public void close(String namespace) | ||
| { | ||
| byte[] idBytes; | ||
| synchronized (namespaceId) { | ||
| idBytes = getNamespaceId(namespace); | ||
| if (idBytes == null) { | ||
| return; | ||
| } | ||
|
|
||
| namespaceId.remove(namespace); | ||
| } | ||
| synchronized (clearLock) { | ||
| Iterator<ByteBuffer> iter = baseMap.keySet().iterator(); | ||
| final Integer id = namespaceId.asMap().remove(namespace); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Prior impl had a guard against races between put/get and close.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm thinking of just having things expire in the namespaceId cache as well.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That would have the potential to waste memory temporarily, since it would auto-recover after a time I agree its probably fine to just let the LRU get rid of it over time.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this call will return an entire map, i think you can use |
||
| if (id != null) { | ||
| List<ByteBuffer> toRemove = Lists.newLinkedList(); | ||
| while (iter.hasNext()) { | ||
| ByteBuffer next = iter.next(); | ||
|
|
||
| if (next.get(0) == idBytes[0] | ||
| && next.get(1) == idBytes[1] | ||
| && next.get(2) == idBytes[2] | ||
| && next.get(3) == idBytes[3]) { | ||
| final int unboxed = id; | ||
| for (ByteBuffer next : baseCache.asMap().keySet()) { | ||
| if (next.getInt(0) == unboxed) { | ||
| toRemove.add(next); | ||
| } | ||
| } | ||
| for (ByteBuffer key : toRemove) { | ||
| baseMap.remove(key); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private byte[] getNamespaceId(final String identifier) | ||
| { | ||
| synchronized (namespaceId) { | ||
| byte[] idBytes = namespaceId.get(identifier); | ||
| if (idBytes != null) { | ||
| return idBytes; | ||
| } | ||
|
|
||
| idBytes = Ints.toByteArray(ids.getAndIncrement()); | ||
| namespaceId.put(identifier, idBytes); | ||
| return idBytes; | ||
| baseCache.invalidateAll(toRemove); | ||
| } | ||
| } | ||
|
|
||
| private ByteBuffer computeKey(byte[] idBytes, byte[] key) | ||
| private ByteBuffer computeKey(int id, byte[] key) | ||
| { | ||
| final ByteBuffer retVal = ByteBuffer.allocate(key.length + 4).put(idBytes).put(key); | ||
| final ByteBuffer retVal = ByteBuffer | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since we're doing putInt we need to specify the byte order.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. it's big-endian by default
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Found the docs, sounds good |
||
| .allocate(key.length + 4) | ||
| .putInt(id) | ||
| .put(key); | ||
| retVal.rewind(); | ||
| return retVal; | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you set the concurrency to the number of threads, the likelihood of having a collision in the striped locking is pretty high if they happen to each request a random lock. Can we make the concurrency higher than the number of threads?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
From what I've read, for most use cases, setting a concurrency level to the number of threads should be enough, or do you have examples that show otherwise?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
as per #1849 (comment) only in java 7 and earlier.