/*
* Copyright 2014-2017 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 java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.Semaphore;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.stream.Collectors;
/**
* Maps calls to zero or more sub-registries. If zero then it will act similar to the noop
* registry. Otherwise activity will be sent to all registries that are part of the composite.
*/
public final class CompositeRegistry implements Registry {
private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
private final ReentrantReadWriteLock.ReadLock rlock = lock.readLock();
private final ReentrantReadWriteLock.WriteLock wlock = lock.writeLock();
private final Clock clock;
private final List<Registry> registries;
private final ConcurrentHashMap<Id, SwapCounter> counters;
private final ConcurrentHashMap<Id, SwapDistributionSummary> distSummaries;
private final ConcurrentHashMap<Id, SwapTimer> timers;
private final ConcurrentHashMap<Id, SwapGauge> gauges;
private final ConcurrentHashMap<Id, AggrMeter> aggrGauges;
private final ConcurrentHashMap<Id, Object> state;
private final Semaphore pollSem = new Semaphore(1);
/** Creates a new instance. */
CompositeRegistry(Clock clock) {
this.clock = clock;
this.registries = new ArrayList<>();
this.counters = new ConcurrentHashMap<>();
this.distSummaries = new ConcurrentHashMap<>();
this.timers = new ConcurrentHashMap<>();
this.gauges = new ConcurrentHashMap<>();
this.aggrGauges = new ConcurrentHashMap<>();
this.state = new ConcurrentHashMap<>();
GaugePoller.schedule(
new WeakReference<>(this),
10000L,
CompositeRegistry::pollGauges);
}
private static void pollGauges(Registry r) {
((CompositeRegistry) r).pollGauges();
}
/** Poll the values from all registered gauges. */
@SuppressWarnings("PMD")
void pollGauges() {
if (pollSem.tryAcquire()) {
try {
for (Map.Entry<Id, AggrMeter> e : aggrGauges.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) {
aggrGauges.remove(id);
} 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.
aggrGauges.remove(id);
}
}
} finally {
pollSem.release();
}
}
}
/**
* Find the first registry in the composite that is an instance of {@code c}. If no match is
* found then null will be returned.
*/
@SuppressWarnings("unchecked")
<T extends Registry> T find(Class<T> c) {
for (Registry r : registries) {
if (c.isAssignableFrom(r.getClass())) {
return (T) r;
}
}
return null;
}
/** Add a registry to the composite. */
public void add(Registry registry) {
wlock.lock();
try {
registries.add(registry);
updateMeters();
} finally {
wlock.unlock();
}
}
/** Remove a registry from the composite. */
public void remove(Registry registry) {
wlock.lock();
try {
registries.remove(registry);
updateMeters();
} finally {
wlock.unlock();
}
}
/** Remove all registries from the composite. */
public void removeAll() {
wlock.lock();
try {
registries.clear();
updateMeters();
} finally {
wlock.unlock();
}
}
private void updateMeters() {
counters.forEach((id, c) -> c.setUnderlying(newCounter(id)));
distSummaries.forEach((id, d) -> d.setUnderlying(newDistributionSummary(id)));
timers.forEach((id, t) -> t.setUnderlying(newTimer(id)));
gauges.forEach((id, g) -> g.setUnderlying(newGauge(id)));
}
@Override public Clock clock() {
return clock;
}
@Override public Id createId(String name) {
return new DefaultId(name);
}
@Override public Id createId(String name, Iterable<Tag> tags) {
return new DefaultId(name, ArrayTagSet.create(tags));
}
@Override public void register(Meter meter) {
AggrMeter m = Utils.computeIfAbsent(aggrGauges, meter.id(), AggrMeter::new);
m.add(meter);
}
@Override public ConcurrentMap<Id, Object> state() {
return state;
}
private Counter newCounter(Id id) {
rlock.lock();
try {
Counter c;
switch (registries.size()) {
case 0:
c = NoopCounter.INSTANCE;
break;
case 1:
c = registries.get(0).counter(id);
break;
default:
List<Counter> cs = registries.stream()
.map(r -> r.counter(id))
.collect(Collectors.toList());
c = new CompositeCounter(id, cs);
break;
}
return c;
} finally {
rlock.unlock();
}
}
@Override public Counter counter(Id id) {
return Utils.computeIfAbsent(counters, id, i -> new SwapCounter(newCounter(i)));
}
private DistributionSummary newDistributionSummary(Id id) {
rlock.lock();
try {
DistributionSummary t;
switch (registries.size()) {
case 0:
t = NoopDistributionSummary.INSTANCE;
break;
case 1:
t = registries.get(0).distributionSummary(id);
break;
default:
List<DistributionSummary> ds = registries.stream()
.map(r -> r.distributionSummary(id))
.collect(Collectors.toList());
t = new CompositeDistributionSummary(id, ds);
break;
}
return t;
} finally {
rlock.unlock();
}
}
@Override public DistributionSummary distributionSummary(Id id) {
return Utils.computeIfAbsent(distSummaries, id, i -> new SwapDistributionSummary(newDistributionSummary(i)));
}
private Timer newTimer(Id id) {
rlock.lock();
try {
Timer t;
switch (registries.size()) {
case 0:
t = NoopTimer.INSTANCE;
break;
case 1:
t = registries.get(0).timer(id);
break;
default:
List<Timer> ts = registries.stream()
.map(r -> r.timer(id))
.collect(Collectors.toList());
t = new CompositeTimer(id, clock, ts);
break;
}
return t;
} finally {
rlock.unlock();
}
}
@Override public Timer timer(Id id) {
return Utils.computeIfAbsent(timers, id, i -> new SwapTimer(newTimer(i)));
}
private Gauge newGauge(Id id) {
rlock.lock();
try {
Gauge t;
switch (registries.size()) {
case 0:
t = NoopGauge.INSTANCE;
break;
case 1:
t = registries.get(0).gauge(id);
break;
default:
List<Gauge> gs = registries.stream()
.map(r -> r.gauge(id))
.collect(Collectors.toList());
t = new CompositeGauge(id, gs);
break;
}
return t;
} finally {
rlock.unlock();
}
}
@Override public Gauge gauge(Id id) {
return Utils.computeIfAbsent(gauges, id, i -> new SwapGauge(newGauge(i)));
}
@Override public Meter get(Id id) {
rlock.lock();
try {
for (Registry r : registries) {
Meter m = r.get(id);
if (m != null) {
if (m instanceof Counter) {
return counter(id);
} else if (m instanceof Timer) {
return timer(id);
} else if (m instanceof DistributionSummary) {
return distributionSummary(id);
} else if (m instanceof Gauge) {
return gauge(id);
} else {
return null;
}
}
}
return null;
} finally {
rlock.unlock();
}
}
@Override public Iterator<Meter> iterator() {
rlock.lock();
try {
if (registries.isEmpty()) {
return Collections.<Meter>emptyList().iterator();
} else {
final Set<Id> ids = new HashSet<>();
for (Registry r : registries) {
for (Meter m : r) ids.add(m.id());
}
return new Iterator<Meter>() {
private final Iterator<Id> idIter = ids.iterator();
@Override
public boolean hasNext() {
return idIter.hasNext();
}
@Override
public Meter next() {
return get(idIter.next());
}
@Override
public void remove() {
throw new UnsupportedOperationException();
}
};
}
} finally {
rlock.unlock();
}
}
}