com.spotify.metrics.core.MetricIdCache Maven / Gradle / Ivy
Show all versions of semantic-metrics-api Show documentation
/*
* Copyright (c) 2016 Spotify AB.
*
* 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 com.spotify.metrics.core;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ExecutionException;
// @formatter:off
/**
* A utility class that simplifies mutating and storing a MetricId.
*
* Example
*
*
* // example using Guava.
* final MetricIdCache.CacheBuilder cacheBuilder = new MetricIdCache.CacheBuilder() {
* public ConcurrentMap build() {
* return CacheBuilder.newBuilder()
* .expireAfterWrite(12, TimeUnit.HOURS).build().asMap()
* }
* };
*
* final MetricIdCache.Builder builder = MetricIdCache.builder().cacheBuilder(cacheBuilder);
*
* final MetricIdCache cache = builder.metricId(MetricId.build().tagged("foo", "bar"))
* .loader(new MetricIdCache.Loader() {
* public MetricId load(MetricId base, String key) {
* base.tagged("key", key);
* }
* }).build();
*
* cache.get("hello"); // MetricId(foo=bar, key=hello)
* cache.get("world"); // MetricId(foo=bar, key=world)
*
* // subsequent calls with cache.get("hello"), or cache.get("world") will return the same
* MetricId instance.
*
*
* @author udoprog
* @param The key type for loading the metric id.
*/
// @formatter:on
public class MetricIdCache {
private final Cache cache;
private final MetricId metricId;
private MetricIdCache(Cache cache, MetricId metricId) {
this.cache = cache;
this.metricId = metricId;
}
public static Any builder() {
return new Any();
}
/**
* Get the value for the specified key.
*
* This method is guaranteed to return the same instance of the associated MetricId, regardless
* of how many threads invoke it.
*
* The configured Loader could be called multiple times, and should be idempotent to that.
*
* @param key The given key to get MetricId for.
* @return A metric id for the given key.
* @throws IllegalStateException if loader returns {@code null}.
*/
public MetricId get(T key) {
try {
return cache.get(metricId, key);
} catch (ExecutionException e) {
throw new RuntimeException("failed to get cache value", e);
}
}
public void invalidate(T key) {
cache.invalidate(key);
}
public void invalidateAll() {
cache.invalidateAll();
}
public static interface Loader {
public MetricId load(MetricId id, T key);
}
public static interface Cache {
public MetricId get(MetricId metricId, T key) throws ExecutionException;
public void invalidate(T key);
public void invalidateAll();
}
public static interface MapBuilder {
public ConcurrentMap build();
}
public static interface CacheBuilder {
public Cache build(Loader loader);
}
private static final class ConcurrentMapCache implements Cache {
private final ConcurrentMap cache;
private final Loader loader;
private ConcurrentMapCache(ConcurrentMap cache, Loader loader) {
this.cache = cache;
this.loader = loader;
}
@Override
public MetricId get(MetricId metricId, T key) {
final MetricId candidate = cache.get(key);
if (candidate != null) {
return candidate;
}
final MetricId addition = loader.load(metricId, key);
if (addition == null) {
throw new IllegalStateException("loader returned null value");
}
final MetricId put;
if ((put = cache.putIfAbsent(key, addition)) != null) {
return put;
}
return addition;
}
@Override
public void invalidate(T key) {
cache.remove(key);
}
@Override
public void invalidateAll() {
cache.clear();
}
}
private interface TypedCacheBuilder {
Cache build(Loader loader);
}
/**
* A builder for which the metric id cache has a given type.
*
* @param The type of the metric id cache.
*/
public static class Typed {
private final Any any;
private final Cache cache;
private final Loader loader;
/* internal */
private final TypedCacheBuilder typedCacheBuilder;
private Typed(
Any any, Cache cache, Loader loader, TypedCacheBuilder typedCacheBuilder
) {
this.any = any;
this.cache = cache;
this.loader = loader;
this.typedCacheBuilder = typedCacheBuilder;
}
/**
* Configure a base metric id.
*
* @see Any#metricId(MetricId)
*/
public Typed metricId(MetricId base) {
return any(any.metricId(base));
}
/**
* Use a shared cache.
*/
public Typed cache(Cache cache) {
if (cache == null) {
throw new IllegalArgumentException("'cache' must not be null");
}
return new Typed(any, cache, loader, typedCacheBuilder);
}
/**
* Allows an unbounded cache.
*
* This could leak memory, since the provided cache implementation will never expire
* entries.
*
* @see Any#unbounded(Loader)
*/
public Typed unbounded(Loader loader) {
return cache(new ConcurrentHashMap(), loader);
}
/**
* Configure a specific cache instance, use this if you wish for the consequent builders to
* re-use the same cache instance.
*/
public Typed cache(final ConcurrentMap map, Loader loader) {
if (map == null) {
throw new IllegalArgumentException("'cache' must not be null");
}
return loader(loader).typedCacheBuilder(new TypedCacheBuilder() {
@Override
public Cache build(Loader loader) {
return new ConcurrentMapCache(map, loader);
}
});
}
public Typed cacheBuilder(CacheBuilder cacheBuilder) {
return any(any.cacheBuilder(cacheBuilder));
}
public Typed mapBuilder(MapBuilder mapBuilder, Loader loader) {
return loader(loader).any(any.mapBuilder(mapBuilder));
}
public Typed mapBuilder(MapBuilder mapBuilder) {
return any(any.mapBuilder(mapBuilder));
}
public Typed loader(Loader loader) {
if (cache != null) {
throw new IllegalStateException("shared cache is already set");
}
return new Typed(any, cache, loader, typedCacheBuilder);
}
/**
* Build a cache with the given MetricId base.
*/
public MetricIdCache build() {
final Cache cache = any.buildCache(this.cache, this.loader, this.typedCacheBuilder);
return new MetricIdCache(cache, any.buildMetricId());
}
private Typed typedCacheBuilder(TypedCacheBuilder typedCacheBuilder) {
return new Typed(any, cache, loader, typedCacheBuilder);
}
private Typed any(Any any) {
return new Typed<>(any, cache, loader, typedCacheBuilder);
}
}
/**
* A metric id cache builder that can take any type.
*
* Calling one of the type-specific configuration options will returned a regular, typed
* Builder.
*
* @author udoprog
*/
public static class Any {
private final MetricId metricId;
private final CacheBuilder cacheBuilder;
private Any(MetricId base, CacheBuilder cacheBuilder) {
this.metricId = base;
this.cacheBuilder = cacheBuilder;
}
private Any() {
this(null, null);
}
/**
* Set the given base metric id for the cache.
*/
public Any metricId(MetricId metricId) {
if (metricId == null) {
throw new IllegalArgumentException("'metricId' must not be null");
}
return new Any(metricId, cacheBuilder);
}
public Typed unbounded(Loader loader) {
return new Typed(this, null, null, null).unbounded(loader);
}
public Typed cache(Cache cache) {
return new Typed(this, null, null, null).cache(cache);
}
public Typed cache(ConcurrentMap cache, Loader loader) {
return new Typed(this, null, null, null).cache(cache, loader);
}
public Typed loader(Loader loader) {
return new Typed(this, null, null, null).loader(loader);
}
public Typed mapBuilder(MapBuilder mapBuilder, Loader loader) {
return mapBuilder(mapBuilder).loader(loader);
}
public Any cacheBuilder(CacheBuilder cacheBuilder) {
if (cacheBuilder == null) {
throw new IllegalArgumentException("'cacheBuilder' must not be null");
}
return new Any(metricId, cacheBuilder);
}
public Any mapBuilder(final MapBuilder mapBuilder) {
if (mapBuilder == null) {
throw new IllegalArgumentException("'mapBuilder' must not be null");
}
return new Any(metricId, new CacheBuilder() {
@Override
public Cache build(Loader loader) {
final ConcurrentMap map = mapBuilder.build();
if (map == null) {
throw new IllegalStateException("'mapBuilder' must not return null");
}
return new ConcurrentMapCache(map, loader);
}
});
}
private Cache buildCache(
Cache cache, Loader loader, TypedCacheBuilder typedCacheBuilder
) {
if (cache != null) {
return cache;
}
if (loader == null) {
throw new IllegalStateException("'loader' must be set");
}
if (cacheBuilder != null) {
final Cache c = cacheBuilder.build(loader);
if (c == null) {
throw new IllegalStateException("'cacheBuilder' must return non-null values");
}
return c;
}
if (typedCacheBuilder != null) {
return typedCacheBuilder.build(loader);
}
throw new IllegalStateException("No cache implementation is configured");
}
private MetricId buildMetricId() {
if (metricId == null) {
return MetricId.EMPTY;
}
return metricId;
}
}
}