/**
* Copyright 2015 Netflix, Inc.
*
* 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.
*/
package com.netflix.spectator.api;
import com.netflix.spectator.impl.Config;
import com.netflix.spectator.impl.Preconditions;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.lang.ref.WeakReference;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.Semaphore;
/**
* Base class to make it easier to implement a simple registry that only needs to customise the
* types returned for Counter, DistributionSummary, and Timer calls.
*/
public abstract class AbstractRegistry implements Registry {
/** Logger instance for the class. */
protected final Logger logger;
private final Clock clock;
private final RegistryConfig config;
private final ConcurrentHashMap<Id, Meter> meters;
private final ConcurrentHashMap<Id, Meter> gauges;
private final ConcurrentHashMap<Id, Object> state;
private final Semaphore pollSem = new Semaphore(1);
/**
* Create a new instance.
*
* @param clock
* Clock used for performing all timing measurements.
*/
public AbstractRegistry(Clock clock) {
this(clock, Config.defaultConfig());
}
/**
* Create a new instance.
*
* @param clock
* Clock used for performing all timing measurements.
* @param config
* Configuration settings for the registry.
*/
public AbstractRegistry(Clock clock, RegistryConfig config) {
this.logger = LoggerFactory.getLogger(getClass());
this.clock = clock;
this.config = config;
this.meters = new ConcurrentHashMap<>();
this.gauges = new ConcurrentHashMap<>();
this.state = new ConcurrentHashMap<>();
GaugePoller.schedule(
new WeakReference<>(this),
config.gaugePollingFrequency().toMillis(),
AbstractRegistry::pollGauges);
}
/**
* Create a new counter instance for a given id.
*
* @param id
* Identifier used to lookup this meter in the registry.
* @return
* New counter instance.
*/
protected abstract Counter newCounter(Id id);
/**
* Create a new distribution summary instance for a given id.
*
* @param id
* Identifier used to lookup this meter in the registry.
* @return
* New distribution summary instance.
*/
protected abstract DistributionSummary newDistributionSummary(Id id);
/**
* Create a new timer instance for a given id.
*
* @param id
* Identifier used to lookup this meter in the registry.
* @return
* New timer instance.
*/
protected abstract Timer newTimer(Id id);
/**
* Create a new gauge instance for a given id.
*
* @param id
* Identifier used to lookup this meter in the registry.
* @return
* New gauge instance.
*/
protected abstract Gauge newGauge(Id id);
@Override public final Clock clock() {
return clock;
}
@Override public final RegistryConfig config() {
return config;
}
@Override public final Id createId(String name) {
return new DefaultId(name);
}
@Override public final Id createId(String name, Iterable<Tag> tags) {
return new DefaultId(name, ArrayTagSet.create(tags));
}
private void logTypeError(Id id, Class<?> desired, Class<?> found) {
final String dtype = desired.getName();
final String ftype = found.getName();
final String msg = String.format("cannot access '%s' as a %s, it already exists as a %s",
id, dtype, ftype);
propagate(new IllegalStateException(msg));
}
private void addToAggr(Meter aggr, Meter meter) {
if (aggr instanceof AggrMeter) {
((AggrMeter) aggr).add(meter);
} else {
logTypeError(meter.id(), meter.getClass(), aggr.getClass());
}
}
private Meter compute(Meter m, Meter fallback) {
return (meters.size() >= config.maxNumberOfMeters()) ? fallback : m;
}
private void handleGaugeException(Id id, Throwable t) {
logger.warn("dropping gauge [{}], exception occurred when polling value", id, t);
gauges.remove(id);
}
/**
* Lambda passed to {@link GaugePoller} to avoid having a strong reference to this
* registry that would prevent garbage collection.
*/
private static void pollGauges(Registry r) {
((AbstractRegistry) r).pollGauges();
}
/** Poll the values from all registered gauges. */
@SuppressWarnings("PMD")
void pollGauges() {
if (pollSem.tryAcquire()) {
try {
for (Map.Entry<Id, Meter> e : gauges.entrySet()) {
Id id = e.getKey();
Meter meter = e.getValue();
try {
if (!meter.hasExpired()) {
for (Measurement m : meter.measure()) {
gauge(m.id()).set(m.value());
}
}
} catch (StackOverflowError t) {
handleGaugeException(id, t);
} catch (VirtualMachineError | ThreadDeath t) {
// Avoid catching OutOfMemoryError and other serious problems in the next
// catch block.
throw t;
} catch (Throwable t) {
// The sampling is calling user functions and therefore we cannot
// make any guarantees they are well-behaved. We catch most Throwables with
// the exception of some VM errors and drop the gauge.
handleGaugeException(id, t);
}
}
} finally {
pollSem.release();
}
}
}
@Override public void register(Meter meter) {
Meter aggr = (gauges.size() >= config.maxNumberOfMeters())
? meters.get(meter.id())
: Utils.computeIfAbsent(gauges, meter.id(), AggrMeter::new);
if (aggr != null) {
addToAggr(aggr, meter);
}
}
@Override public ConcurrentMap<Id, Object> state() {
return state;
}
@Override public final Counter counter(Id id) {
try {
Preconditions.checkNotNull(id, "id");
Meter m = Utils.computeIfAbsent(meters, id, i -> compute(newCounter(i), NoopCounter.INSTANCE));
if (!(m instanceof Counter)) {
logTypeError(id, Counter.class, m.getClass());
m = NoopCounter.INSTANCE;
}
return (Counter) m;
} catch (Exception e) {
propagate(e);
return NoopCounter.INSTANCE;
}
}
@Override public final DistributionSummary distributionSummary(Id id) {
try {
Preconditions.checkNotNull(id, "id");
Meter m = Utils.computeIfAbsent(meters, id, i ->
compute(newDistributionSummary(i), NoopDistributionSummary.INSTANCE));
if (!(m instanceof DistributionSummary)) {
logTypeError(id, DistributionSummary.class, m.getClass());
m = NoopDistributionSummary.INSTANCE;
}
return (DistributionSummary) m;
} catch (Exception e) {
propagate(e);
return NoopDistributionSummary.INSTANCE;
}
}
@Override public final Timer timer(Id id) {
try {
Meter m = Utils.computeIfAbsent(meters, id, i -> compute(newTimer(i), NoopTimer.INSTANCE));
if (!(m instanceof Timer)) {
logTypeError(id, Timer.class, m.getClass());
m = NoopTimer.INSTANCE;
}
return (Timer) m;
} catch (Exception e) {
propagate(e);
return NoopTimer.INSTANCE;
}
}
@Override public final Gauge gauge(Id id) {
try {
Meter m = Utils.computeIfAbsent(meters, id, i -> compute(newGauge(i), NoopGauge.INSTANCE));
if (!(m instanceof Gauge)) {
logTypeError(id, Gauge.class, m.getClass());
m = NoopGauge.INSTANCE;
}
return (Gauge) m;
} catch (Exception e) {
propagate(e);
return NoopGauge.INSTANCE;
}
}
@Override public final Meter get(Id id) {
return meters.get(id);
}
@Override public final Iterator<Meter> iterator() {
// Force update of gauges before traversing values
pollGauges();
return meters.values().iterator();
}
}