zipkin2.Call Maven / Gradle / Ivy
/*
* Copyright 2015-2024 The OpenZipkin Authors
*
* Licensed 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 zipkin2;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicReference;
/**
* This captures a (usually remote) request and can be used once, either {@link #execute()
* synchronously} or {@link #enqueue(Callback) asynchronously}. At any time, from any thread, you
* can call {@linkplain #cancel()}, which might stop an in-flight request or prevent one from
* occurring.
*
* Implementations should prepare a call such that there's little or no likelihood of late
* runtime exceptions. For example, if the call is to get a trace, the call to {@code listSpans}
* should propagate input errors vs delay them until a call to {@linkplain #execute()} or
* {@linkplain #enqueue(Callback)}.
*
*
Ex.
*
{@code
* // Any translation of an input request to remote parameters should happen here, and any related
* // errors should propagate here.
* Call>> listTraces = spanStore.listTraces(request);
* // When this executes, it should simply run the remote request.
* List trace = getTraceCall.execute();
* }
*
* An instance of call cannot be invoked more than once, but you can {@linkplain #clone()} an
* instance if you need to replay the call. There is no relationship between a call and a number of
* remote requests. For example, an implementation that stores spans may make hundreds of remote
* requests, possibly retrying on your behalf.
*
*
This type owes its design to {@code retrofit2.Call}, which is nearly the same, except limited
* to HTTP transports.
*
* @param the success type, typically not null except when {@code V} is {@linkplain Void}.
*/
public abstract class Call implements Cloneable {
/**
* Returns a completed call which has the supplied value. This is useful when input parameters
* imply there's no call needed. For example, an empty input might always result in an empty
* output.
*/
public static Call create(V v) {
return new Constant<>(v);
}
@SuppressWarnings("unchecked")
public static Call> emptyList() {
return Call.create(Collections.emptyList());
}
public interface Mapper {
V2 map(V1 input);
}
/**
* Maps the result of this call into a different shape, as defined by the {@code mapper} function.
* This is used to convert values from one type to another. For example, you could use this to
* convert between zipkin v1 and v2 span format.
*
* {@code
* getTracesV1Call = getTracesV2Call.map(traces -> v2TracesConverter);
* }
*
* This method intends to be used for chaining. That means "this" instance should be discarded
* in favor of the result of this method.
*/
public final Call map(Mapper mapper) {
return new Mapping<>(mapper, this);
}
public interface FlatMapper {
Call map(V1 input);
}
/**
* Maps the result of this call into another, as defined by the {@code flatMapper} function. This
* is used to chain two remote calls together. For example, you could use this to chain a list IDs
* call to a get by IDs call.
*
* {@code
* getTracesCall = getIdsCall.flatMap(ids -> getTraces(ids));
*
* // this would now invoke the chain
* traces = getTracesCall.enqueue(tracesCallback);
* }
*
* Cancelation propagates to the mapped call.
*
* This method intends to be used for chaining. That means "this" instance should be discarded
* in favor of the result of this method.
*/
public final Call flatMap(FlatMapper flatMapper) {
return new FlatMapping<>(flatMapper, this);
}
public interface ErrorHandler {
/** Attempts to resolve an error. The user must call the callback. */
void onErrorReturn(Throwable error, Callback callback);
}
/**
* Returns a call which can attempt to resolve an exception. This is useful when a remote call
* returns an error when a resource is not found.
*
* Here's an example of coercing 404 to empty:
*
{@code
* call.handleError((error, callback) -> {
* if (error instanceof HttpException && ((HttpException) error).code == 404) {
* callback.onSuccess(Collections.emptyList());
* } else {
* callback.onError(error);
* }
* });
* }
*/
public final Call handleError(ErrorHandler errorHandler) {
return new ErrorHandling<>(errorHandler, this);
}
// Taken from RxJava throwIfFatal, which was taken from scala
public static void propagateIfFatal(Throwable t) {
if (t instanceof VirtualMachineError) {
throw (VirtualMachineError) t;
} else if (t instanceof ThreadDeath) {
throw (ThreadDeath) t;
} else if (t instanceof LinkageError) {
throw (LinkageError) t;
}
}
/**
* Invokes a request, returning a success value or propagating an error to the caller. Invoking
* this more than once will result in an error. To repeat a call, make a copy with {@linkplain
* #clone()}.
*
* Eventhough this is a blocking call, implementations may honor calls to {@linkplain
* #cancel()} from a different thread.
*
* @return a success value. Null is unexpected, except when {@code V} is {@linkplain Void}.
*/
public abstract V execute() throws IOException;
/**
* Invokes a request asynchronously, signaling the {@code callback} when complete. Invoking this
* more than once will result in an error. To repeat a call, make a copy with {@linkplain
* #clone()}.
*/
public abstract void enqueue(Callback callback);
/**
* Requests to cancel this call, even if some implementations may not support it. For example, a
* blocking call is sometimes not cancelable.
*/
// Boolean isn't returned because some implementations may cancel asynchronously.
// Implementing might throw an IOException on execute or callback.onError(IOException)
public abstract void cancel();
/**
* Returns true if {@linkplain #cancel()} was called.
*
* Calls can fail before being canceled, so true does always mean cancelation caused a call to
* fail. That said, successful cancellation does result in a failure.
*/
// isCanceled exists while isExecuted does not because you do not need the latter to implement
// asynchronous bindings, such as rxjava2
public abstract boolean isCanceled();
/** Returns a copy of this object, so you can make an identical follow-up request. */
@Override public abstract Call clone();
static class Constant extends Base { // not final for mock testing
final V v;
Constant(V v) {
this.v = v;
}
@Override protected V doExecute() {
return v;
}
@Override protected void doEnqueue(Callback callback) {
callback.onSuccess(v);
}
@Override public Call clone() {
return new Constant<>(v);
}
@Override public String toString() {
return "ConstantCall{value=" + v + "}";
}
@Override public boolean equals(Object o) {
if (o == this) return true;
if (o instanceof Constant) {
Constant that = (Constant) o;
return Objects.equals(this.v, that.v);
}
return false;
}
@Override
public int hashCode() {
int h = 1;
h *= 1000003;
h ^= (v == null) ? 0 : v.hashCode();
return h;
}
}
static final class Mapping extends Base {
final Mapper mapper;
final Call delegate;
Mapping(Mapper mapper, Call delegate) {
this.mapper = mapper;
this.delegate = delegate;
}
@Override protected R doExecute() throws IOException {
return mapper.map(delegate.execute());
}
@Override protected void doEnqueue(final Callback callback) {
delegate.enqueue(new Callback() {
@Override public void onSuccess(V value) {
try {
callback.onSuccess(mapper.map(value));
} catch (Throwable t) {
callback.onError(t);
}
}
@Override public void onError(Throwable t) {
callback.onError(t);
}
});
}
@Override public String toString() {
return "Mapping{call=" + delegate + ", mapper=" + mapper + "}";
}
@Override public Call clone() {
return new Mapping<>(mapper, delegate.clone());
}
}
static final class FlatMapping extends Base {
final FlatMapper flatMapper;
final Call delegate;
volatile Call mapped;
FlatMapping(FlatMapper flatMapper, Call delegate) {
this.flatMapper = flatMapper;
this.delegate = delegate;
}
@Override protected R doExecute() throws IOException {
return (mapped = flatMapper.map(delegate.execute())).execute();
}
@Override protected void doEnqueue(final Callback callback) {
delegate.enqueue(new Callback() {
@Override public void onSuccess(V value) {
try {
(mapped = flatMapper.map(value)).enqueue(callback);
} catch (Throwable t) {
propagateIfFatal(t);
callback.onError(t);
}
}
@Override public void onError(Throwable t) {
callback.onError(t);
}
});
}
@Override protected void doCancel() {
delegate.cancel();
if (mapped != null) mapped.cancel();
}
@Override public String toString() {
return "FlatMapping{call=" + delegate + ", flatMapper=" + flatMapper + "}";
}
@Override public Call clone() {
return new FlatMapping<>(flatMapper, delegate.clone());
}
}
static final class ErrorHandling extends Base {
static final Object SENTINEL = new Object(); // to differentiate from null
final ErrorHandler errorHandler;
final Call delegate;
ErrorHandling(ErrorHandler errorHandler, Call delegate) {
this.errorHandler = errorHandler;
this.delegate = delegate;
}
@Override protected V doExecute() throws IOException {
try {
return delegate.execute();
} catch (IOException e) {
return handleError(e);
} catch (RuntimeException e) {
return handleError(e);
} catch (Error e) {
Call.propagateIfFatal(e);
return handleError(e);
}
}
V handleError(T e) throws T {
final AtomicReference ref = new AtomicReference(SENTINEL);
errorHandler.onErrorReturn(e, new Callback() {
@Override
public void onSuccess(V value) {
ref.set(value);
}
@Override
public void onError(Throwable t) {
}
});
Object result = ref.get();
if (SENTINEL == result) throw e;
return (V) result;
}
@Override protected void doEnqueue(final Callback callback) {
delegate.enqueue(new Callback() {
@Override public void onSuccess(V value) {
callback.onSuccess(value);
}
@Override public void onError(Throwable t) {
errorHandler.onErrorReturn(t, callback);
}
});
}
@Override protected void doCancel() {
delegate.cancel();
}
@Override public String toString() {
return "ErrorHandling{call=" + delegate + ", errorHandler=" + errorHandler + "}";
}
@Override public Call clone() {
return new ErrorHandling<>(errorHandler, delegate.clone());
}
}
public static abstract class Base extends Call {
volatile boolean canceled;
boolean executed;
protected Base() {
}
@Override public final V execute() throws IOException {
synchronized (this) {
if (this.executed) throw new IllegalStateException("Already Executed");
this.executed = true;
}
if (isCanceled()) {
throw new IOException("Canceled");
} else {
return this.doExecute();
}
}
protected abstract V doExecute() throws IOException;
@Override public final void enqueue(Callback callback) {
synchronized (this) {
if (this.executed) throw new IllegalStateException("Already Executed");
this.executed = true;
}
if (isCanceled()) {
callback.onError(new IOException("Canceled"));
} else {
this.doEnqueue(callback);
}
}
protected abstract void doEnqueue(Callback callback);
@Override public final void cancel() {
this.canceled = true;
doCancel();
}
protected void doCancel() {
}
@Override public final boolean isCanceled() {
return this.canceled || doIsCanceled();
}
protected boolean doIsCanceled() {
return false;
}
}
}