Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion checkstyle/suppressions.xml
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@
files="RequestResponseTest.java"/>

<suppress checks="NPathComplexity"
files="MemoryRecordsTest.java"/>
files="MemoryRecordsTest|MetricsTest"/>

<!-- Connect -->
<suppress checks="ClassFanOutComplexity"
Expand Down
38 changes: 29 additions & 9 deletions clients/src/main/java/org/apache/kafka/common/metrics/Sensor.java
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ public final class Sensor {
private final Time time;
private volatile long lastRecordTime;
private final long inactiveSensorExpirationTimeMs;
private final Object metricLock;

public enum RecordingLevel {
INFO(0, "INFO"), DEBUG(1, "DEBUG");
Expand Down Expand Up @@ -113,6 +114,7 @@ public boolean shouldRecord(final int configId) {
this.inactiveSensorExpirationTimeMs = TimeUnit.MILLISECONDS.convert(inactiveSensorExpirationTimeSeconds, TimeUnit.SECONDS);
this.lastRecordTime = time.milliseconds();
this.recordingLevel = recordingLevel;
this.metricLock = new Object();
checkForest(new HashSet<Sensor>());
}

Expand Down Expand Up @@ -174,9 +176,11 @@ public void record(double value, long timeMs, boolean checkQuotas) {
if (shouldRecord()) {
this.lastRecordTime = timeMs;
synchronized (this) {
// increment all the stats
for (Stat stat : this.stats)
stat.record(config, value, timeMs);
synchronized (metricLock()) {
// increment all the stats
for (Stat stat : this.stats)
stat.record(config, value, timeMs);
}
if (checkQuotas)
checkQuotas(timeMs);
}
Expand Down Expand Up @@ -229,7 +233,7 @@ public synchronized boolean add(CompoundStat stat, MetricConfig config) {
return false;

this.stats.add(Utils.notNull(stat));
Object lock = metricLock(stat);
Object lock = metricLock();
for (NamedMeasurable m : stat.stats()) {
final KafkaMetric metric = new KafkaMetric(lock, m.name(), m.stat(), config == null ? this.config : config, time);
if (!metrics.containsKey(metric.metricName())) {
Expand Down Expand Up @@ -265,7 +269,7 @@ public synchronized boolean add(final MetricName metricName, final MeasurableSta
return true;
} else {
final KafkaMetric metric = new KafkaMetric(
metricLock(stat),
metricLock(),
Utils.notNull(metricName),
Utils.notNull(stat),
config == null ? this.config : config,
Expand All @@ -291,10 +295,26 @@ synchronized List<KafkaMetric> metrics() {
}

/**
* KafkaMetrics of sensors which use SampledStat should be synchronized on the Sensor object
* to allow concurrent reads and updates. For simplicity, all sensors are synchronized on Sensor.
* KafkaMetrics of sensors which use SampledStat should be synchronized on the same lock
* for sensor record and metric value read to allow concurrent reads and updates. For simplicity,
* all sensors are synchronized on this object.
* <p>
* Sensor object is not used as a lock for reading metric value since metrics reporter is
* invoked while holding Sensor and Metrics locks to report addition and removal of metrics
* and synchronized reporters may deadlock if Sensor lock is used for reading metrics values.
* Note that Sensor object itself is used as a lock to protect the access to stats and metrics
* while recording metric values, adding and deleting sensors.
* </p><p>
* Locking order (assume all MetricsReporter methods may be synchronized):
* <ul>
* <li>Sensor#add: Sensor -> Metrics -> MetricsReporter</li>
* <li>Metrics#removeSensor: Sensor -> Metrics -> MetricsReporter</li>
* <li>KafkaMetric#metricValue: MetricsReporter -> Sensor#metricLock</li>
* <li>Sensor#record: Sensor -> Sensor#metricLock</li>
* </ul>
* </p>
*/
private Object metricLock(Stat stat) {
return this;
private Object metricLock() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: do we still need this function? Could we just reference the metricLock object directly? Since it is private my understanding is that it was not intended to be used outside this class.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@guozhangwang Thanks for the review. I left the method in since it gives a good place to document :-) I think the method would get optimized away at runtime anyway. I don't mind changing it if you think it may be confusing.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah I see. MVN then :)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can document the field too, right? If there's no reason to have a method, I'd remove it.

return metricLock;
}
}
117 changes: 105 additions & 12 deletions clients/src/test/java/org/apache/kafka/common/metrics/MetricsTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,16 @@
import java.util.Arrays;
import java.util.Collections;
import java.util.Deque;
import java.util.List;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;

import java.util.concurrent.atomic.AtomicBoolean;

import org.apache.kafka.common.Metric;
Expand All @@ -54,9 +57,12 @@
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@SuppressWarnings("deprecation")
public class MetricsTest {
private static final Logger log = LoggerFactory.getLogger(MetricsTest.class);

private static final double EPS = 0.000001;
private MockTime time = new MockTime();
Expand Down Expand Up @@ -604,25 +610,21 @@ public void testMetricInstances() {
}
}

/**
* Verifies that concurrent sensor add, remove, updates and read don't result
* in errors or deadlock.
*/
@Test
public void testConcurrentAccess() throws Exception {
public void testConcurrentReadUpdate() throws Exception {
final Random random = new Random();
final Deque<Sensor> sensors = new ConcurrentLinkedDeque<>();
metrics = new Metrics(new MockTime(10));
SensorCreator sensorCreator = new SensorCreator(metrics);

final AtomicBoolean alive = new AtomicBoolean(true);
executorService = Executors.newSingleThreadExecutor();
executorService.submit(new Runnable() {
@Override
public void run() {
while (alive.get()) {
for (Sensor sensor : sensors) {
sensor.record(random.nextInt(10000));
}
}
}
});
executorService.submit(new ConcurrentMetricOperation(alive, "record",
() -> sensors.forEach(sensor -> sensor.record(random.nextInt(10000)))));

for (int i = 0; i < 10000; i++) {
if (sensors.size() > 5) {
Expand All @@ -640,6 +642,97 @@ public void run() {
alive.set(false);
}

/**
* Verifies that concurrent sensor add, remove, updates and read with a metrics reporter
* that synchronizes on every reporter method doesn't result in errors or deadlock.
*/
@Test
public void testConcurrentReadUpdateReport() throws Exception {

class LockingReporter implements MetricsReporter {
Map<MetricName, KafkaMetric> activeMetrics = new HashMap<>();
@Override
public synchronized void init(List<KafkaMetric> metrics) {
}

@Override
public synchronized void metricChange(KafkaMetric metric) {
activeMetrics.put(metric.metricName(), metric);
}

@Override
public synchronized void metricRemoval(KafkaMetric metric) {
activeMetrics.remove(metric.metricName(), metric);
}

@Override
public synchronized void close() {
}

@Override
public void configure(Map<String, ?> configs) {
}

synchronized void processMetrics() {
for (KafkaMetric metric : activeMetrics.values()) {
assertNotNull("Invalid metric value", metric.metricValue());
}
}
}

final LockingReporter reporter = new LockingReporter();
this.metrics.close();
this.metrics = new Metrics(config, Arrays.asList((MetricsReporter) reporter), new MockTime(10), true);
final Deque<Sensor> sensors = new ConcurrentLinkedDeque<>();
SensorCreator sensorCreator = new SensorCreator(metrics);

final Random random = new Random();
final AtomicBoolean alive = new AtomicBoolean(true);
executorService = Executors.newFixedThreadPool(3);

Future<?> writeFuture = executorService.submit(new ConcurrentMetricOperation(alive, "record",
() -> sensors.forEach(sensor -> sensor.record(random.nextInt(10000)))));
Future<?> readFuture = executorService.submit(new ConcurrentMetricOperation(alive, "read",
() -> sensors.forEach(sensor -> sensor.metrics().forEach(metric ->
assertNotNull("Invalid metric value", metric.metricValue())))));
Future<?> reportFuture = executorService.submit(new ConcurrentMetricOperation(alive, "report",
() -> reporter.processMetrics()));

for (int i = 0; i < 10000; i++) {
if (sensors.size() > 10) {
Sensor sensor = random.nextBoolean() ? sensors.removeFirst() : sensors.removeLast();
metrics.removeSensor(sensor.name());
}
StatType statType = StatType.forId(random.nextInt(StatType.values().length));
sensors.add(sensorCreator.createSensor(statType, i));
}
assertFalse("Read failed", readFuture.isDone());
assertFalse("Write failed", writeFuture.isDone());
assertFalse("Report failed", reportFuture.isDone());

alive.set(false);
}

private class ConcurrentMetricOperation implements Runnable {
private final AtomicBoolean alive;
private final String opName;
private final Runnable op;
ConcurrentMetricOperation(AtomicBoolean alive, String opName, Runnable op) {
this.alive = alive;
this.opName = opName;
this.op = op;
}
public void run() {
try {
while (alive.get()) {
op.run();
}
} catch (Throwable t) {
log.error("Metric {} failed with exception", opName, t);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we get an exception, should we fail the test?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test fails if there is an exception since it verifies that the task is still running.

}
}
}

enum StatType {
AVG(0),
TOTAL(1),
Expand Down Expand Up @@ -676,7 +769,7 @@ private static class SensorCreator {
}

private Sensor createSensor(StatType statType, int index) {
Sensor sensor = metrics.sensor("kafka.requests");
Sensor sensor = metrics.sensor("kafka.requests." + index);
Map<String, String> tags = Collections.singletonMap("tag", "tag" + index);
switch (statType) {
case AVG:
Expand Down