/*
* 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.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.codahale.metrics.Meter;
import com.github.rholder.retry.Attempt;
import com.github.rholder.retry.RetryException;
import com.github.rholder.retry.RetryListener;
import com.github.rholder.retry.Retryer;
import com.github.rholder.retry.RetryerBuilder;
import com.github.rholder.retry.StopStrategies;
import com.github.rholder.retry.WaitStrategies;
import com.google.common.base.Optional;
import com.google.common.base.Predicate;
import gobblin.commit.SpeculativeAttemptAwareConstruct;
import gobblin.configuration.State;
import gobblin.instrumented.Instrumented;
import gobblin.metrics.GobblinMetrics;
import gobblin.util.FinalState;
import gobblin.writer.exception.NonTransientException;
/**
* Retry writer follows decorator pattern that retries on inner writer's failure.
* @param <D>
*/
public class RetryWriter<D> extends WatermarkAwareWriterWrapper<D> implements DataWriter<D>, FinalState, SpeculativeAttemptAwareConstruct {
private static final Logger LOG = LoggerFactory.getLogger(RetryWriter.class);
public static final String RETRY_CONF_PREFIX = "gobblin.writer.retry.";
public static final String FAILED_RETRY_WRITES_METER = RETRY_CONF_PREFIX + "failed_writes";
public static final String RETRY_MULTIPLIER = RETRY_CONF_PREFIX + "multiplier";
public static final String RETRY_MAX_WAIT_MS_PER_INTERVAL = RETRY_CONF_PREFIX + "max_wait_ms_per_interval";
public static final String RETRY_MAX_ATTEMPTS = RETRY_CONF_PREFIX + "max_attempts";
public static final String FAILED_WRITES_KEY = "FailedWrites";
private final DataWriter<D> writer;
private final Retryer<Void> retryer;
private long failedWrites;
public RetryWriter(DataWriter<D> writer, State state) {
this.writer = writer;
this.retryer = buildRetryer(state);
if (this.writer instanceof WatermarkAwareWriter) {
setWatermarkAwareWriter((WatermarkAwareWriter) this.writer);
}
}
/**
* Build Retryer.
* - If Writer implements Retriable, it will use the RetryerBuilder from the writer.
* - Otherwise, it will use DEFAULT writer builder.
*
* - If Gobblin metrics is enabled, it will emit all failure count in to metrics.
*
* @param state
* @return
*/
private Retryer<Void> buildRetryer(State state) {
RetryerBuilder<Void> builder = null;
if (writer instanceof Retriable) {
builder = ((Retriable) writer).getRetryerBuilder();
} else {
builder = createRetryBuilder(state);
}
if (GobblinMetrics.isEnabled(state)) {
final Optional<Meter> retryMeter = Optional.of(Instrumented.getMetricContext(state, getClass()).meter(FAILED_RETRY_WRITES_METER));
builder.withRetryListener(new RetryListener() {
@Override
public <V> void onRetry(Attempt<V> attempt) {
if (attempt.hasException()) {
LOG.warn("Caught exception. This may be retried.", attempt.getExceptionCause());
Instrumented.markMeter(retryMeter);
failedWrites++;
}
}
});
}
return builder.build();
}
@Override
public void close() throws IOException {
writer.close();
}
@Override
public void write(final D record) throws IOException {
//Need a Callable interface to be wrapped by Retryer.
Callable<Void> writeCall = new Callable<Void>() {
@Override
public Void call() throws Exception {
writer.write(record);
return null;
}
};
callWithRetry(writeCall);
}
@Override
public void commit() throws IOException {
Callable<Void> commitCall = new Callable<Void>() {
@Override
public Void call() throws Exception {
writer.commit();
return null;
}
};
callWithRetry(commitCall);
}
private void callWithRetry(Callable<Void> callable) throws IOException {
try {
this.retryer.wrap(callable).call();
} catch (ExecutionException | RetryException e) {
throw new IOException(e);
}
}
@Override
public void cleanup() throws IOException {
writer.cleanup();
}
@Override
public long recordsWritten() {
return writer.recordsWritten();
}
@Override
public long bytesWritten() throws IOException {
return writer.bytesWritten();
}
/**
* @return RetryerBuilder that retries on all exceptions except NonTransientException with exponential back off
*/
public static RetryerBuilder<Void> createRetryBuilder(State state) {
Predicate<Throwable> transients = new Predicate<Throwable>() {
@Override
public boolean apply(Throwable t) {
return !(t instanceof NonTransientException);
}
};
long multiplier = state.getPropAsLong(RETRY_MULTIPLIER, 500L);
long maxWaitMsPerInterval = state.getPropAsLong(RETRY_MAX_WAIT_MS_PER_INTERVAL, 10000);
int maxAttempts = state.getPropAsInt(RETRY_MAX_ATTEMPTS, 5);
return RetryerBuilder.<Void> newBuilder()
.retryIfException(transients)
.withWaitStrategy(WaitStrategies.exponentialWait(multiplier, maxWaitMsPerInterval, TimeUnit.MILLISECONDS)) //1, 2, 4, 8, 16 seconds delay
.withStopStrategy(StopStrategies.stopAfterAttempt(maxAttempts)); //Total 5 attempts and fail.
}
@Override
public boolean isSpeculativeAttemptSafe() {
if (this.writer instanceof SpeculativeAttemptAwareConstruct) {
return ((SpeculativeAttemptAwareConstruct)this.writer).isSpeculativeAttemptSafe();
}
return false;
}
@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(FAILED_WRITES_KEY, this.failedWrites);
return state;
}
}