/*
* 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.writer;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.codahale.metrics.Timer;
import com.github.rholder.retry.RetryerBuilder;
import com.google.common.base.Optional;
import com.google.common.base.Preconditions;
import gobblin.util.limiter.Limiter;
import gobblin.util.limiter.RateBasedLimiter;
import gobblin.configuration.ConfigurationKeys;
import gobblin.configuration.State;
import gobblin.instrumented.Instrumented;
import gobblin.metrics.GobblinMetrics;
import gobblin.util.FinalState;
/**
* Throttle writer follows decorator pattern that throttles inner writer by either QPS or by bytes.
* @param <D>
*/
public class ThrottleWriter<D> implements DataWriter<D>, FinalState, Retriable {
private static final Logger LOG = LoggerFactory.getLogger(ThrottleWriter.class);
public static final String WRITER_THROTTLE_TYPE_KEY = "gobblin.writer.throttle_type";
public static final String WRITER_LIMIT_RATE_LIMIT_KEY = "gobblin.writer.throttle_rate";
public static final String WRITES_THROTTLED_TIMER = "gobblin.writer.throttled_time";
public static final String THROTTLED_TIME_KEY = "ThrottledTime";
private static final String LOCAL_JOB_LAUNCHER_TYPE = "LOCAL";
public static enum ThrottleType {
QPS,
Bytes
}
private final State state;
private final DataWriter<D> writer;
private final Limiter limiter;
private final ThrottleType type;
private final Optional<Timer> throttledTimer;
private long throttledTime;
public ThrottleWriter(DataWriter<D> writer, State state) {
Preconditions.checkNotNull(writer, "DataWriter is required.");
Preconditions.checkNotNull(state, "State is required.");
this.state = state;
this.writer = writer;
this.type = ThrottleType.valueOf(state.getProp(WRITER_THROTTLE_TYPE_KEY));
int rateLimit = computeRateLimit(state);
LOG.info("Rate limit for each writer: " + rateLimit + " " + type);
this.limiter = new RateBasedLimiter(computeRateLimit(state));
if (GobblinMetrics.isEnabled(state)) {
throttledTimer = Optional.<Timer>of(Instrumented.getMetricContext(state, getClass()).timer(WRITES_THROTTLED_TIMER));
} else {
throttledTimer = Optional.absent();
}
}
/**
* Compute rate limit per executor.
* Rate limit = Total rate limit / # of parallelism
*
* # of parallelism:
* - if LOCAL job type Min(# of source partition, thread pool size)
* - else Min(# of source partition, # of max mappers)
*
* @param state
* @return
*/
private int computeRateLimit(State state) {
String jobLauncherType = state.getProp(ConfigurationKeys.JOB_LAUNCHER_TYPE_KEY, "LOCAL");
int parallelism = 1;
if (LOCAL_JOB_LAUNCHER_TYPE.equals(jobLauncherType)) {
parallelism = state.getPropAsInt(ConfigurationKeys.TASK_EXECUTOR_THREADPOOL_SIZE_KEY,
ConfigurationKeys.DEFAULT_TASK_EXECUTOR_THREADPOOL_SIZE);
} else {
parallelism = state.getPropAsInt(ConfigurationKeys.MR_JOB_MAX_MAPPERS_KEY,
ConfigurationKeys.DEFAULT_MR_JOB_MAX_MAPPERS);
}
parallelism = Math.min(parallelism, state.getPropAsInt(ConfigurationKeys.SOURCE_MAX_NUMBER_OF_PARTITIONS,
ConfigurationKeys.DEFAULT_MAX_NUMBER_OF_PARTITIONS));
parallelism = Math.max(parallelism, 1);
int rateLimit = state.getPropAsInt(WRITER_LIMIT_RATE_LIMIT_KEY) / parallelism;
rateLimit = Math.max(rateLimit, 1);
return rateLimit;
}
/**
* Calls inner writer with self throttling.
* If the throttle type is byte, it applies throttle after write happens.
* This is because it can figure out written bytes after it's written. It's not ideal but throttling after write should be sufficient for most cases.
* {@inheritDoc}
* @see gobblin.writer.DataWriter#write(java.lang.Object)
*/
@Override
public void write(D record) throws IOException {
try {
if (ThrottleType.QPS.equals(type)) {
acquirePermits(1L);
}
long beforeWrittenBytes = writer.bytesWritten();
writer.write(record);
if (ThrottleType.Bytes.equals(type)) {
long delta = writer.bytesWritten() - beforeWrittenBytes;
if (delta < 0) {
throw new UnsupportedOperationException("Cannot throttle on bytes because "
+ writer.getClass().getSimpleName() + " does not supports bytesWritten");
}
if (delta > 0) {
acquirePermits(delta);
}
}
} catch (InterruptedException e) {
throw new IOException("Failed while acquiring permits.",e);
}
}
/**
* Acquire permit along with emitting metrics if enabled.
* @param permits
* @throws InterruptedException
*/
private void acquirePermits(long permits) throws InterruptedException {
long startMs = System.currentTimeMillis(); //Measure in milliseconds. (Nanoseconds are more precise but expensive and not worth for this case)
limiter.acquirePermits(permits);
long permitAcquisitionTime = System.currentTimeMillis() - startMs;
if (throttledTimer.isPresent()) { // Metrics enabled
Instrumented.updateTimer(throttledTimer, permitAcquisitionTime, TimeUnit.MILLISECONDS);
}
this.throttledTime += permitAcquisitionTime;
}
@Override
public void close() throws IOException {
writer.close();
}
@Override
public void commit() throws IOException {
writer.commit();
}
@Override
public void cleanup() throws IOException {
writer.cleanup();
}
@Override
public long recordsWritten() {
return writer.recordsWritten();
}
@Override
public long bytesWritten() throws IOException {
return writer.bytesWritten();
}
@Override
public RetryerBuilder<Void> getRetryerBuilder() {
if (writer instanceof Retriable) {
return ((Retriable) writer).getRetryerBuilder();
}
return RetryWriter.createRetryBuilder(state);
}
@Override
public State getFinalState() {
State state = new State();
if (this.writer instanceof FinalState) {
state.addAll(((FinalState)this.writer).getFinalState());
} else {
LOG.warn("Wrapped writer does not implement FinalState: " + this.writer.getClass());
}
state.setProp(THROTTLED_TIME_KEY, this.throttledTime);
return state;
}
}