/*
* 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.instrumented.writer;
import java.io.Closeable;
import java.io.IOException;
import java.util.List;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import com.codahale.metrics.Meter;
import com.codahale.metrics.Timer;
import com.google.common.base.Optional;
import com.google.common.collect.Lists;
import com.google.common.io.Closer;
import lombok.extern.slf4j.Slf4j;
import gobblin.configuration.State;
import gobblin.instrumented.Instrumentable;
import gobblin.instrumented.Instrumented;
import gobblin.metrics.GobblinMetrics;
import gobblin.metrics.MetricContext;
import gobblin.metrics.MetricNames;
import gobblin.metrics.Tag;
import gobblin.util.ExecutorsUtils;
import gobblin.util.FinalState;
import gobblin.writer.DataWriter;
@Slf4j
/**
* Package-private implementation of instrumentation for {@link gobblin.writer.DataWriter}.
*
* @see gobblin.instrumented.writer.InstrumentedDataWriter for extensible class.
*/
abstract class InstrumentedDataWriterBase<D> implements DataWriter<D>, Instrumentable, Closeable, FinalState {
private final Optional<ScheduledThreadPoolExecutor> writerMetricsUpdater;
private final boolean instrumentationEnabled;
private MetricContext metricContext;
private Optional<Meter> recordsInMeter;
private Optional<Meter> successfulWritesMeter;
private Optional<Meter> failedWritesMeter;
private Optional<Timer> dataWriterTimer;
private Optional<Meter> recordsWrittenMeter;
private Optional<Meter> bytesWrittenMeter;
protected final Closer closer;
public static final String WRITER_METRICS_UPDATER_INTERVAL = "gobblin.writer.metrics.updater.interval";
public static final long DEFAULT_WRITER_METRICS_UPDATER_INTERVAL = 30000;
public InstrumentedDataWriterBase(State state) {
this(state, Optional.<Class<?>> absent());
}
protected InstrumentedDataWriterBase(State state, Optional<Class<?>> classTag) {
this.closer = Closer.create();
this.instrumentationEnabled = GobblinMetrics.isEnabled(state);
this.metricContext = this.closer.register(Instrumented.getMetricContext(state, classTag.or(this.getClass())));
if (this.instrumentationEnabled) {
this.writerMetricsUpdater = Optional.of(buildWriterMetricsUpdater());
scheduleWriterMetricsUpdater(this.writerMetricsUpdater.get(), getWriterMetricsUpdaterInterval(state));
} else {
this.writerMetricsUpdater = Optional.absent();
}
regenerateMetrics();
}
@Override
public void switchMetricContext(List<Tag<?>> tags) {
this.metricContext = this.closer
.register(Instrumented.newContextFromReferenceContext(this.metricContext, tags, Optional.<String> absent()));
regenerateMetrics();
}
@Override
public void switchMetricContext(MetricContext context) {
this.metricContext = context;
regenerateMetrics();
}
/**
* Generates metrics for the instrumentation of this class.
*/
protected void regenerateMetrics() {
if (isInstrumentationEnabled()) {
this.recordsInMeter = Optional.of(this.metricContext.meter(MetricNames.DataWriterMetrics.RECORDS_IN_METER));
this.successfulWritesMeter =
Optional.of(this.metricContext.meter(MetricNames.DataWriterMetrics.SUCCESSFUL_WRITES_METER));
this.failedWritesMeter = Optional.of(this.metricContext.meter(MetricNames.DataWriterMetrics.FAILED_WRITES_METER));
setRecordsWrittenMeter(isInstrumentationEnabled());
setBytesWrittenMeter(isInstrumentationEnabled());
this.dataWriterTimer = Optional.<Timer>of(this.metricContext.timer(MetricNames.DataWriterMetrics.WRITE_TIMER));
} else {
this.recordsInMeter = Optional.absent();
this.successfulWritesMeter = Optional.absent();
this.failedWritesMeter = Optional.absent();
setRecordsWrittenMeter(isInstrumentationEnabled());
setBytesWrittenMeter(isInstrumentationEnabled());
this.dataWriterTimer = Optional.absent();
}
}
private synchronized void setRecordsWrittenMeter(boolean isInstrumentationEnabled) {
if (isInstrumentationEnabled) {
this.recordsWrittenMeter =
Optional.of(this.metricContext.meter(MetricNames.DataWriterMetrics.RECORDS_WRITTEN_METER));
} else {
this.recordsWrittenMeter = Optional.absent();
}
}
private synchronized void setBytesWrittenMeter(boolean isInstrumentationEnabled) {
if (isInstrumentationEnabled) {
this.bytesWrittenMeter = Optional.of(this.metricContext.meter(MetricNames.DataWriterMetrics.BYTES_WRITTEN_METER));
} else {
this.bytesWrittenMeter = Optional.absent();
}
}
/** Default with no additional tags */
@Override
public List<Tag<?>> generateTags(State state) {
return Lists.newArrayList();
}
@Override
public boolean isInstrumentationEnabled() {
return this.instrumentationEnabled;
}
@Override
public void write(D record) throws IOException {
if (!isInstrumentationEnabled()) {
writeImpl(record);
return;
}
try {
long startTimeNanos = System.nanoTime();
beforeWrite(record);
writeImpl(record);
onSuccessfulWrite(startTimeNanos);
} catch (IOException exception) {
onException(exception);
throw exception;
}
}
/**
* Called beforeWriting a record.
* @param record record to write.
*/
public void beforeWrite(D record) {
Instrumented.markMeter(this.recordsInMeter);
}
/**
* Called after a successful write of a record.
* @param startTimeNanos time at which writing started.
*/
public void onSuccessfulWrite(long startTimeNanos) {
Instrumented.updateTimer(this.dataWriterTimer, System.nanoTime() - startTimeNanos, TimeUnit.NANOSECONDS);
Instrumented.markMeter(this.successfulWritesMeter);
}
/** Called after a failed writing of a record.
* @param exception exception thrown.
*/
public void onException(Exception exception) {
Instrumented.markMeter(this.failedWritesMeter);
}
/**
* Subclasses should implement this instead of {@link gobblin.writer.DataWriter#write}
*/
public abstract void writeImpl(D record) throws IOException;
/**
* Get final state for this object. By default this returns an empty {@link gobblin.configuration.State}, but
* concrete subclasses can add information that will be added to the task state.
* @return Empty {@link gobblin.configuration.State}.
*/
@Override
public State getFinalState() {
return new State();
}
@Override
public void close() throws IOException {
this.closer.close();
if (this.writerMetricsUpdater.isPresent()) {
ExecutorsUtils.shutdownExecutorService(this.writerMetricsUpdater.get(), Optional.of(log));
}
}
@Override
public MetricContext getMetricContext() {
return this.metricContext;
}
/**
* Update the {@link #recordsWrittenMeter} and {@link #bytesWrittenMeter}. This method should be invoked after the
* wrapped {@link DataWriter#commit()} is invoked. This ensures that the record-level and byte-level meters are
* updated at least once.
*/
@Override
public void commit() throws IOException {
updateRecordsWrittenMeter();
updateBytesWrittenMeter();
}
/**
* Update the {@link #recordsWrittenMeter} using the {@link DataWriter#recordsWritten()} method..
*/
private synchronized void updateRecordsWrittenMeter() {
if (this.recordsWrittenMeter.isPresent()) {
this.recordsWrittenMeter.get().mark(recordsWritten() - this.recordsWrittenMeter.get().getCount());
}
}
/**
* Update the {@link #bytesWrittenMeter} using the {@link DataWriter#bytesWritten()} method.
*/
private synchronized void updateBytesWrittenMeter() {
if (this.bytesWrittenMeter.isPresent()) {
try {
this.bytesWrittenMeter.get().mark(bytesWritten() - this.bytesWrittenMeter.get().getCount());
} catch (IOException e) {
log.error("Cannot get bytesWritten for DataWriter, will not update " + this.bytesWrittenMeter.get().toString(),
e);
}
}
}
/**
* Build a {@link ScheduledThreadPoolExecutor} that updates record-level and byte-level metrics.
*/
private static ScheduledThreadPoolExecutor buildWriterMetricsUpdater() {
return new ScheduledThreadPoolExecutor(1,
ExecutorsUtils.newThreadFactory(Optional.of(log), Optional.of("WriterMetricsUpdater-%d")));
}
/**
* Get the interval that the Writer Metrics Updater should be scheduled on.
*/
private static long getWriterMetricsUpdaterInterval(State state) {
return state.getPropAsLong(WRITER_METRICS_UPDATER_INTERVAL, DEFAULT_WRITER_METRICS_UPDATER_INTERVAL);
}
/**
* Schedule the given {@link ScheduledThreadPoolExecutor} to run at the given interval.
*/
private ScheduledFuture<?> scheduleWriterMetricsUpdater(ScheduledThreadPoolExecutor writerMetricsUpdater,
long scheduleInterval) {
return writerMetricsUpdater.scheduleAtFixedRate(new WriterMetricsUpdater(), scheduleInterval, scheduleInterval,
TimeUnit.MILLISECONDS);
}
/**
* An implementation of {@link Runnable} that updates record-level and byte-level metrics.
*/
private class WriterMetricsUpdater implements Runnable {
@Override
public void run() {
updateRecordsWrittenMeter();
updateBytesWrittenMeter();
}
}
}