/*
* 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 gobblin.runtime;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;
import com.codahale.metrics.Gauge;
import com.codahale.metrics.Meter;
import com.codahale.metrics.MetricRegistry;
import com.google.common.base.Optional;
import com.google.common.base.Preconditions;
import com.google.common.collect.Queues;
import gobblin.configuration.ConfigurationKeys;
/**
* A class implementing a bounded blocking queue with timeout for buffering records between a producer and a consumer.
*
* <p>
* In addition to the normal queue operations, this class also keeps track of the following statistics:
*
* <ul>
* <li>Queue size.</li>
* <li>Queue fill ratio (queue size/queue capacity).</li>
* <li>Put attempt count.</li>
* <li>Mean rate of put attempts (puts/sec).</li>
* <li>Get attempt count.</li>
* <li>Mean rate of get attempts (gets/sec).</li>
* </ul>
* </p>
*
* @author Yinan Li
*/
public class BoundedBlockingRecordQueue<T> {
private final int capacity;
private final long timeout;
private final TimeUnit timeoutTimeUnit;
private final BlockingQueue<T> blockingQueue;
private final Optional<QueueStats> queueStats;
private BoundedBlockingRecordQueue(Builder<T> builder) {
Preconditions.checkArgument(builder.capacity > 0, "Invalid queue capacity");
Preconditions.checkArgument(builder.timeout > 0, "Invalid timeout time");
this.capacity = builder.capacity;
this.timeout = builder.timeout;
this.timeoutTimeUnit = builder.timeoutTimeUnit;
this.blockingQueue = Queues.newArrayBlockingQueue(builder.capacity);
this.queueStats = builder.ifCollectStats ? Optional.of(new QueueStats()) : Optional.<QueueStats> absent();
}
/**
* Put a record to the tail of the queue, waiting (up to the configured timeout time)
* for an empty space to become available.
*
* @param record the record to put to the tail of the queue
* @return whether the record has been successfully put into the queue
* @throws InterruptedException if interrupted while waiting
*/
public boolean put(T record) throws InterruptedException {
boolean offered = this.blockingQueue.offer(record, this.timeout, this.timeoutTimeUnit);
if (this.queueStats.isPresent()) {
this.queueStats.get().putsRateMeter.mark();
}
return offered;
}
/**
* Get a record from the head of the queue, waiting (up to the configured timeout time)
* for a record to become available.
*
* @return the record at the head of the queue, or <code>null</code> if no record is available
* @throws InterruptedException if interrupted while waiting
*/
public T get() throws InterruptedException {
T record = this.blockingQueue.poll(this.timeout, this.timeoutTimeUnit);
if (this.queueStats.isPresent()) {
this.queueStats.get().getsRateMeter.mark();
}
return record;
}
/**
* Get a {@link QueueStats} object representing queue statistics of this {@link BoundedBlockingRecordQueue}.
*
* @return a {@link QueueStats} object wrapped in an {@link com.google.common.base.Optional},
* which means it may be absent if collecting of queue statistics is not enabled.
*/
public Optional<QueueStats> stats() {
return this.queueStats;
}
/**
* Clear the queue.
*/
public void clear() {
this.blockingQueue.clear();
}
/**
* Get a new {@link BoundedBlockingRecordQueue.Builder}.
*
* @param <T> record type
* @return a new {@link BoundedBlockingRecordQueue.Builder}
*/
public static <T> Builder<T> newBuilder() {
return new Builder<>();
}
/**
* A builder class for {@link BoundedBlockingRecordQueue}.
*
* @param <T> record type
*/
public static class Builder<T> {
private int capacity = ConfigurationKeys.DEFAULT_FORK_RECORD_QUEUE_CAPACITY;
private long timeout = ConfigurationKeys.DEFAULT_FORK_RECORD_QUEUE_TIMEOUT;
private TimeUnit timeoutTimeUnit = TimeUnit.MILLISECONDS;
private boolean ifCollectStats = false;
/**
* Configure the capacity of the queue.
*
* @param capacity the capacity of the queue
* @return this {@link Builder} instance
*/
public Builder<T> hasCapacity(int capacity) {
this.capacity = capacity;
return this;
}
/**
* Configure the timeout time of queue operations.
*
* @param timeout the time timeout time
* @return this {@link Builder} instance
*/
public Builder<T> useTimeout(long timeout) {
this.timeout = timeout;
return this;
}
/**
* Configure the timeout time unit of queue operations.
*
* @param timeoutTimeUnit the time timeout time unit
* @return this {@link Builder} instance
*/
public Builder<T> useTimeoutTimeUnit(TimeUnit timeoutTimeUnit) {
this.timeoutTimeUnit = timeoutTimeUnit;
return this;
}
/**
* Configure whether to collect queue statistics.
*
* @return this {@link Builder} instance
*/
public Builder<T> collectStats() {
this.ifCollectStats = true;
return this;
}
/**
* Build a new {@link BoundedBlockingRecordQueue}.
*
* @return the newly built {@link BoundedBlockingRecordQueue}
*/
public BoundedBlockingRecordQueue<T> build() {
return new BoundedBlockingRecordQueue<>(this);
}
}
/**
* A class for collecting queue statistics.
*
* <p>
* All statistics will have zero values if collecting of statistics is not enabled.
* </p>
*/
public class QueueStats {
public static final String QUEUE_SIZE = "queueSize";
public static final String FILL_RATIO = "fillRatio";
public static final String PUT_ATTEMPT_RATE = "putAttemptRate";
public static final String GET_ATTEMPT_RATE = "getAttemptRate";
public static final String PUT_ATTEMPT_COUNT = "putAttemptCount";
public static final String GET_ATTEMPT_COUNT = "getAttemptCount";
private final Gauge<Integer> queueSizeGauge;
private final Gauge<Double> fillRatioGauge;
private final Meter putsRateMeter;
private final Meter getsRateMeter;
public QueueStats() {
this.queueSizeGauge = new Gauge<Integer>() {
@Override
public Integer getValue() {
return BoundedBlockingRecordQueue.this.blockingQueue.size();
}
};
this.fillRatioGauge = new Gauge<Double>() {
@Override
public Double getValue() {
return (double) BoundedBlockingRecordQueue.this.blockingQueue.size()
/ BoundedBlockingRecordQueue.this.capacity;
}
};
this.putsRateMeter = new Meter();
this.getsRateMeter = new Meter();
}
/**
* Return the queue size.
*
* @return the queue size
*/
public int queueSize() {
return this.queueSizeGauge.getValue();
}
/**
* Return the queue fill ratio.
*
* @return the queue fill ratio
*/
public double fillRatio() {
return this.fillRatioGauge.getValue();
}
/**
* Return the rate of put attempts.
*
* @return the rate of put attempts
*/
public double putAttemptRate() {
return this.putsRateMeter.getMeanRate();
}
/**
* Return the total count of put attempts.
*
* @return the total count of put attempts
*/
public long putAttemptCount() {
return this.putsRateMeter.getCount();
}
/**
* Return the rate of get attempts.
*
* @return the rate of get attempts
*/
public double getAttemptRate() {
return this.getsRateMeter.getMeanRate();
}
/**
* Return the total count of get attempts.
*
* @return the total count of get attempts
*/
public long getAttemptCount() {
return this.getsRateMeter.getCount();
}
/**
* Register all statistics as {@link com.codahale.metrics.Metric}s with a
* {@link com.codahale.metrics.MetricRegistry}.
*
* @param metricRegistry the {@link com.codahale.metrics.MetricRegistry} to register with
* @param prefix metric name prefix
*/
public void registerAll(MetricRegistry metricRegistry, String prefix) {
metricRegistry.register(MetricRegistry.name(prefix, QUEUE_SIZE), this.queueSizeGauge);
metricRegistry.register(MetricRegistry.name(prefix, FILL_RATIO), this.fillRatioGauge);
metricRegistry.register(MetricRegistry.name(prefix, PUT_ATTEMPT_RATE), this.putsRateMeter);
metricRegistry.register(MetricRegistry.name(prefix, GET_ATTEMPT_RATE), this.getsRateMeter);
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder("[");
sb.append(QUEUE_SIZE).append("=").append(queueSize()).append(", ");
sb.append(FILL_RATIO).append("=").append(fillRatio()).append(", ");
sb.append(PUT_ATTEMPT_RATE).append("=").append(putAttemptRate()).append(", ");
sb.append(PUT_ATTEMPT_COUNT).append("=").append(putAttemptCount()).append(", ");
sb.append(GET_ATTEMPT_RATE).append("=").append(getAttemptRate()).append(", ");
sb.append(GET_ATTEMPT_COUNT).append("=").append(getAttemptCount()).append("]");
return sb.toString();
}
}
}