api: Fix CallOptions to be properly `@Immutable` (#9689)

Although CallOptions is annotated by @Immutable, its fields are not
final. So it's not truly immutable, namely not safe for unsynchronized
publication.

This commit adds final to all fields of CallOptions. Using internal
builder class to keep flexibility of constructing CallOptions.

Fixes #9658
This commit is contained in:
pandaapo 2022-11-18 05:11:27 +08:00 committed by GitHub
parent 1241946c15
commit 1b94f48dc8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
1 changed files with 95 additions and 71 deletions

View File

@ -41,42 +41,68 @@ public final class CallOptions {
/**
* A blank {@code CallOptions} that all fields are not set.
*/
public static final CallOptions DEFAULT = new CallOptions();
// Although {@code CallOptions} is immutable, its fields are not final, so that we can initialize
// them outside of constructor. Otherwise the constructor will have a potentially long list of
// unnamed arguments, which is undesirable.
@Nullable
private Deadline deadline;
@Nullable
private Executor executor;
public static final CallOptions DEFAULT = new Builder().build();
@Nullable
private String authority;
private final Deadline deadline;
@Nullable
private CallCredentials credentials;
private final Executor executor;
@Nullable
private String compressorName;
private final String authority;
private Object[][] customOptions;
@Nullable
private final CallCredentials credentials;
// Unmodifiable list
private List<ClientStreamTracer.Factory> streamTracerFactories = Collections.emptyList();
@Nullable
private final String compressorName;
private final Object[][] customOptions;
private final List<ClientStreamTracer.Factory> streamTracerFactories;
/**
* Opposite to fail fast.
*/
@Nullable
private Boolean waitForReady;
private final Boolean waitForReady;
@Nullable
private Integer maxInboundMessageSize;
private final Integer maxInboundMessageSize;
@Nullable
private Integer maxOutboundMessageSize;
private final Integer maxOutboundMessageSize;
private CallOptions(Builder builder) {
this.deadline = builder.deadline;
this.executor = builder.executor;
this.authority = builder.authority;
this.credentials = builder.credentials;
this.compressorName = builder.compressorName;
this.customOptions = builder.customOptions;
this.streamTracerFactories = builder.streamTracerFactories;
this.waitForReady = builder.waitForReady;
this.maxInboundMessageSize = builder.maxInboundMessageSize;
this.maxOutboundMessageSize = builder.maxOutboundMessageSize;
}
static class Builder {
Deadline deadline;
Executor executor;
String authority;
CallCredentials credentials;
String compressorName;
Object[][] customOptions = new Object[0][2];
// Unmodifiable list
List<ClientStreamTracer.Factory> streamTracerFactories = Collections.emptyList();
Boolean waitForReady;
Integer maxInboundMessageSize;
Integer maxOutboundMessageSize;
private CallOptions build() {
return new CallOptions(this);
}
}
/**
* Override the HTTP/2 authority the channel claims to be connecting to. <em>This is not
@ -89,18 +115,18 @@ public final class CallOptions {
*/
@ExperimentalApi("https://github.com/grpc/grpc-java/issues/1767")
public CallOptions withAuthority(@Nullable String authority) {
CallOptions newOptions = new CallOptions(this);
newOptions.authority = authority;
return newOptions;
Builder builder = toBuilder(this);
builder.authority = authority;
return builder.build();
}
/**
* Returns a new {@code CallOptions} with the given call credentials.
*/
public CallOptions withCallCredentials(@Nullable CallCredentials credentials) {
CallOptions newOptions = new CallOptions(this);
newOptions.credentials = credentials;
return newOptions;
Builder builder = toBuilder(this);
builder.credentials = credentials;
return builder.build();
}
/**
@ -113,9 +139,9 @@ public final class CallOptions {
*/
@ExperimentalApi("https://github.com/grpc/grpc-java/issues/1704")
public CallOptions withCompression(@Nullable String compressorName) {
CallOptions newOptions = new CallOptions(this);
newOptions.compressorName = compressorName;
return newOptions;
Builder builder = toBuilder(this);
builder.compressorName = compressorName;
return builder.build();
}
/**
@ -127,9 +153,9 @@ public final class CallOptions {
* @param deadline the deadline or {@code null} for unsetting the deadline.
*/
public CallOptions withDeadline(@Nullable Deadline deadline) {
CallOptions newOptions = new CallOptions(this);
newOptions.deadline = deadline;
return newOptions;
Builder builder = toBuilder(this);
builder.deadline = deadline;
return builder.build();
}
/**
@ -156,9 +182,9 @@ public final class CallOptions {
* fails RPCs without sending them if unable to connect.
*/
public CallOptions withWaitForReady() {
CallOptions newOptions = new CallOptions(this);
newOptions.waitForReady = Boolean.TRUE;
return newOptions;
Builder builder = toBuilder(this);
builder.waitForReady = Boolean.TRUE;
return builder.build();
}
/**
@ -166,9 +192,9 @@ public final class CallOptions {
* This method should be rarely used because the default is without 'wait for ready'.
*/
public CallOptions withoutWaitForReady() {
CallOptions newOptions = new CallOptions(this);
newOptions.waitForReady = Boolean.FALSE;
return newOptions;
Builder builder = toBuilder(this);
builder.waitForReady = Boolean.FALSE;
return builder.build();
}
/**
@ -208,9 +234,9 @@ public final class CallOptions {
* executor specified with {@link ManagedChannelBuilder#executor}.
*/
public CallOptions withExecutor(@Nullable Executor executor) {
CallOptions newOptions = new CallOptions(this);
newOptions.executor = executor;
return newOptions;
Builder builder = toBuilder(this);
builder.executor = executor;
return builder.build();
}
/**
@ -221,13 +247,13 @@ public final class CallOptions {
*/
@ExperimentalApi("https://github.com/grpc/grpc-java/issues/2861")
public CallOptions withStreamTracerFactory(ClientStreamTracer.Factory factory) {
CallOptions newOptions = new CallOptions(this);
ArrayList<ClientStreamTracer.Factory> newList =
new ArrayList<>(streamTracerFactories.size() + 1);
newList.addAll(streamTracerFactories);
newList.add(factory);
newOptions.streamTracerFactories = Collections.unmodifiableList(newList);
return newOptions;
Builder builder = toBuilder(this);
builder.streamTracerFactories = Collections.unmodifiableList(newList);
return builder.build();
}
/**
@ -319,7 +345,7 @@ public final class CallOptions {
Preconditions.checkNotNull(key, "key");
Preconditions.checkNotNull(value, "value");
CallOptions newOptions = new CallOptions(this);
Builder builder = toBuilder(this);
int existingIdx = -1;
for (int i = 0; i < customOptions.length; i++) {
if (key.equals(customOptions[i][0])) {
@ -328,18 +354,18 @@ public final class CallOptions {
}
}
newOptions.customOptions = new Object[customOptions.length + (existingIdx == -1 ? 1 : 0)][2];
System.arraycopy(customOptions, 0, newOptions.customOptions, 0, customOptions.length);
builder.customOptions = new Object[customOptions.length + (existingIdx == -1 ? 1 : 0)][2];
System.arraycopy(customOptions, 0, builder.customOptions, 0, customOptions.length);
if (existingIdx == -1) {
// Add a new option
newOptions.customOptions[customOptions.length] = new Object[] {key, value};
builder.customOptions[customOptions.length] = new Object[] {key, value};
} else {
// Replace an existing option
newOptions.customOptions[existingIdx] = new Object[] {key, value};
builder.customOptions[existingIdx] = new Object[] {key, value};
}
return newOptions;
return builder.build();
}
/**
@ -368,10 +394,6 @@ public final class CallOptions {
return executor;
}
private CallOptions() {
customOptions = new Object[0][2];
}
/**
* Returns whether <a href="https://github.com/grpc/grpc/blob/master/doc/wait-for-ready.md">
* 'wait for ready'</a> option is enabled for the call. 'Fail fast' is the default option for gRPC
@ -392,9 +414,9 @@ public final class CallOptions {
@ExperimentalApi("https://github.com/grpc/grpc-java/issues/2563")
public CallOptions withMaxInboundMessageSize(int maxSize) {
checkArgument(maxSize >= 0, "invalid maxsize %s", maxSize);
CallOptions newOptions = new CallOptions(this);
newOptions.maxInboundMessageSize = maxSize;
return newOptions;
Builder builder = toBuilder(this);
builder.maxInboundMessageSize = maxSize;
return builder.build();
}
/**
@ -403,9 +425,9 @@ public final class CallOptions {
@ExperimentalApi("https://github.com/grpc/grpc-java/issues/2563")
public CallOptions withMaxOutboundMessageSize(int maxSize) {
checkArgument(maxSize >= 0, "invalid maxsize %s", maxSize);
CallOptions newOptions = new CallOptions(this);
newOptions.maxOutboundMessageSize = maxSize;
return newOptions;
Builder builder = toBuilder(this);
builder.maxOutboundMessageSize = maxSize;
return builder.build();
}
/**
@ -427,19 +449,21 @@ public final class CallOptions {
}
/**
* Copy constructor.
* Copy CallOptions.
*/
private CallOptions(CallOptions other) {
deadline = other.deadline;
authority = other.authority;
credentials = other.credentials;
executor = other.executor;
compressorName = other.compressorName;
customOptions = other.customOptions;
waitForReady = other.waitForReady;
maxInboundMessageSize = other.maxInboundMessageSize;
maxOutboundMessageSize = other.maxOutboundMessageSize;
streamTracerFactories = other.streamTracerFactories;
private static Builder toBuilder(CallOptions other) {
Builder builder = new Builder();
builder.deadline = other.deadline;
builder.executor = other.executor;
builder.authority = other.authority;
builder.credentials = other.credentials;
builder.compressorName = other.compressorName;
builder.customOptions = other.customOptions;
builder.streamTracerFactories = other.streamTracerFactories;
builder.waitForReady = other.waitForReady;
builder.maxInboundMessageSize = other.maxInboundMessageSize;
builder.maxOutboundMessageSize = other.maxOutboundMessageSize;
return builder;
}
@Override