fix(etcd): make gRPC max inbound message size configurable
This commit is contained in:
parent
5966aea43b
commit
f1c998c9cc
|
|
@ -87,6 +87,10 @@ public final class EtcdAgent {
|
|||
private static final Gson GSON = new Gson();
|
||||
private static final int DEFAULT_LIMIT = 100;
|
||||
private static final int RPC_TIMEOUT_SECONDS = 30;
|
||||
static final int DEFAULT_GRPC_MAX_INBOUND_MESSAGE_SIZE = 32 * 1024 * 1024;
|
||||
static final int MIN_GRPC_MAX_INBOUND_MESSAGE_SIZE = 1024 * 1024;
|
||||
static final int MAX_GRPC_MAX_INBOUND_MESSAGE_SIZE = 256 * 1024 * 1024;
|
||||
private static final String GRPC_MAX_INBOUND_MESSAGE_SIZE_KEY = "grpc_max_inbound_message_size";
|
||||
private static final int PRESERVE_LEASE_MAX_ATTEMPTS = 3;
|
||||
private static final long HISTORY_DEFAULT_REVISION_WINDOW = 10_000L;
|
||||
private static final List<String> CAPABILITIES = Collections.unmodifiableList(Arrays.asList(
|
||||
|
|
@ -167,7 +171,8 @@ public final class EtcdAgent {
|
|||
List<String> endpoints = endpoints(connection);
|
||||
ClientBuilder builder = Client.builder()
|
||||
.endpoints(endpoints.toArray(String[]::new))
|
||||
.connectTimeout(Duration.ofSeconds(connectTimeoutSeconds(connection)));
|
||||
.connectTimeout(Duration.ofSeconds(connectTimeoutSeconds(connection)))
|
||||
.maxInboundMessageSize(grpcMaxInboundMessageSize(connection));
|
||||
String username = stringOrEmpty(connection, "username");
|
||||
String password = stringOrEmpty(connection, "password");
|
||||
if (!username.isBlank()) {
|
||||
|
|
@ -184,6 +189,19 @@ public final class EtcdAgent {
|
|||
return Math.min(300, Math.max(1, intOrDefault(connection, "connect_timeout_secs", RPC_TIMEOUT_SECONDS)));
|
||||
}
|
||||
|
||||
static int grpcMaxInboundMessageSize(JsonObject connection) {
|
||||
int configured = intOrDefault(
|
||||
connection,
|
||||
GRPC_MAX_INBOUND_MESSAGE_SIZE_KEY,
|
||||
intUrlParamOrDefault(
|
||||
stringOrEmpty(connection, "url_params"),
|
||||
GRPC_MAX_INBOUND_MESSAGE_SIZE_KEY,
|
||||
DEFAULT_GRPC_MAX_INBOUND_MESSAGE_SIZE
|
||||
)
|
||||
);
|
||||
return Math.min(MAX_GRPC_MAX_INBOUND_MESSAGE_SIZE, Math.max(MIN_GRPC_MAX_INBOUND_MESSAGE_SIZE, configured));
|
||||
}
|
||||
|
||||
private static Map<String, Object> validateConnectedClient() throws Exception {
|
||||
EtcdSessionState state = sessionState();
|
||||
Client active = requireClient();
|
||||
|
|
@ -1630,6 +1648,26 @@ public final class EtcdAgent {
|
|||
return element == null || element.isJsonNull() ? fallback : element.getAsInt();
|
||||
}
|
||||
|
||||
private static int intUrlParamOrDefault(String params, String key, int fallback) {
|
||||
if (params == null || params.isBlank()) {
|
||||
return fallback;
|
||||
}
|
||||
for (String entry : params.replaceFirst("^\\?", "").split("&")) {
|
||||
int separator = entry.indexOf('=');
|
||||
String entryKey = separator < 0 ? entry : entry.substring(0, separator);
|
||||
if (!key.equals(entryKey)) {
|
||||
continue;
|
||||
}
|
||||
String value = separator < 0 ? "" : entry.substring(separator + 1);
|
||||
try {
|
||||
return Integer.parseInt(value);
|
||||
} catch (NumberFormatException ignored) {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
private static boolean boolOrDefault(JsonObject object, String key, boolean fallback) {
|
||||
JsonElement element = object.get(key);
|
||||
return element == null || element.isJsonNull() ? fallback : element.getAsBoolean();
|
||||
|
|
|
|||
|
|
@ -89,6 +89,41 @@ final class EtcdAgentTest {
|
|||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void grpcInboundLimitDefaultsTo32MiBAndUsesSafeBounds() {
|
||||
Assertions.assertEquals(
|
||||
32 * 1024 * 1024,
|
||||
EtcdAgent.grpcMaxInboundMessageSize(new JsonObject())
|
||||
);
|
||||
Assertions.assertEquals(
|
||||
64 * 1024 * 1024,
|
||||
EtcdAgent.grpcMaxInboundMessageSize(
|
||||
JsonParser.parseString("{\"grpc_max_inbound_message_size\":67108864}").getAsJsonObject()
|
||||
)
|
||||
);
|
||||
Assertions.assertEquals(
|
||||
1024 * 1024,
|
||||
EtcdAgent.grpcMaxInboundMessageSize(
|
||||
JsonParser.parseString("{\"grpc_max_inbound_message_size\":0}").getAsJsonObject()
|
||||
)
|
||||
);
|
||||
Assertions.assertEquals(
|
||||
256 * 1024 * 1024,
|
||||
EtcdAgent.grpcMaxInboundMessageSize(
|
||||
JsonParser.parseString("{\"grpc_max_inbound_message_size\":536870912}").getAsJsonObject()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void grpcInboundLimitCanBeConfiguredThroughConnectionUrlParams() {
|
||||
JsonObject connection = JsonParser.parseString(
|
||||
"{\"url_params\":\"foo=bar&grpc_max_inbound_message_size=50331648\"}"
|
||||
).getAsJsonObject();
|
||||
|
||||
Assertions.assertEquals(48 * 1024 * 1024, EtcdAgent.grpcMaxInboundMessageSize(connection));
|
||||
}
|
||||
|
||||
@Test
|
||||
void validateConnectionRequiresAnActiveSession() {
|
||||
String response = EtcdAgent.handleRequest(
|
||||
|
|
|
|||
|
|
@ -153,6 +153,10 @@ const DREMIO_ARROW_FLIGHT_SQL_JDBC_DRIVER_CLASS = "org.apache.arrow.driver.jdbc.
|
|||
const DREMIO_LEGACY_JDBC_URL = "jdbc:dremio:direct=127.0.0.1:31010";
|
||||
const DREMIO_LEGACY_JDBC_DRIVER_CLASS = "com.dremio.jdbc.Driver";
|
||||
const DEFAULT_SSH_USER = "root";
|
||||
const ETCD_GRPC_MAX_INBOUND_DEFAULT_MIB = 32;
|
||||
const ETCD_GRPC_MAX_INBOUND_MIN_MIB = 1;
|
||||
const ETCD_GRPC_MAX_INBOUND_MAX_MIB = 256;
|
||||
const ETCD_GRPC_MAX_INBOUND_PARAM = "grpc_max_inbound_message_size";
|
||||
const NACOS_CONNECTION_PROFILES: ReadonlyArray<{ value: NacosConnectionProfile; title: string }> = [
|
||||
{ value: "v2", title: "Nacos 2.x" },
|
||||
{ value: "v3", title: "Nacos 3.x" },
|
||||
|
|
@ -3028,6 +3032,18 @@ const etcdEndpointsLines = computed({
|
|||
form.value.etcd_endpoints = normalizeEndpointLines(value);
|
||||
},
|
||||
});
|
||||
const etcdGrpcMaxInboundMessageSizeMiB = computed({
|
||||
get: () => {
|
||||
const configuredBytes = Number(getUrlParam(form.value.url_params, ETCD_GRPC_MAX_INBOUND_PARAM));
|
||||
if (!Number.isFinite(configuredBytes) || configuredBytes <= 0) return ETCD_GRPC_MAX_INBOUND_DEFAULT_MIB;
|
||||
return Math.min(ETCD_GRPC_MAX_INBOUND_MAX_MIB, Math.max(ETCD_GRPC_MAX_INBOUND_MIN_MIB, Math.round(configuredBytes / (1024 * 1024))));
|
||||
},
|
||||
set: (value: number) => {
|
||||
const configuredMiB = Number(value);
|
||||
const normalizedMiB = Number.isFinite(configuredMiB) ? Math.min(ETCD_GRPC_MAX_INBOUND_MAX_MIB, Math.max(ETCD_GRPC_MAX_INBOUND_MIN_MIB, Math.round(configuredMiB))) : ETCD_GRPC_MAX_INBOUND_DEFAULT_MIB;
|
||||
form.value.url_params = setUrlParam(form.value.url_params, ETCD_GRPC_MAX_INBOUND_PARAM, String(normalizedMiB * 1024 * 1024));
|
||||
},
|
||||
});
|
||||
const zookeeperConnectString = computed({
|
||||
get: () => form.value.connection_string || "",
|
||||
set: (value: string) => {
|
||||
|
|
@ -7457,6 +7473,13 @@ function openExternalUrl(url: string) {
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="form.db_type === 'etcd'" class="grid grid-cols-4 items-start gap-4">
|
||||
<Label :class="connectionLabelSmallPaddedClass">{{ t("connection.etcdGrpcMaxInbound") }}</Label>
|
||||
<div class="col-span-3 space-y-1">
|
||||
<Input v-model.number="etcdGrpcMaxInboundMessageSizeMiB" type="number" :min="ETCD_GRPC_MAX_INBOUND_MIN_MIB" :max="ETCD_GRPC_MAX_INBOUND_MAX_MIB" step="1" />
|
||||
<p class="text-xs leading-5 text-muted-foreground">{{ t("connection.etcdGrpcMaxInboundHint") }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label :class="connectionLabelSmallClass">{{ t("connection.queryTimeout") }}</Label>
|
||||
<div class="col-span-3 grid grid-cols-2 gap-2">
|
||||
|
|
|
|||
|
|
@ -447,6 +447,8 @@ export default {
|
|||
redisKeySeparator: "Key Namespace Separator",
|
||||
etcdEndpoints: "Endpoints",
|
||||
etcdEndpointsHint: "One endpoint per line. Leave blank to use the host and port above.",
|
||||
etcdGrpcMaxInbound: "gRPC receive limit (MiB)",
|
||||
etcdGrpcMaxInboundHint: "Maximum size of one etcd response. Defaults to 32 MiB and is capped at 256 MiB.",
|
||||
etcdCaCertPlaceholder: "/path/to/ca.crt",
|
||||
etcdCaCertBrowse: "Choose CA certificate",
|
||||
etcdClientAuth: "Client Auth",
|
||||
|
|
|
|||
|
|
@ -427,6 +427,8 @@ export default withEnglishFallback({
|
|||
redisKeySeparator: "Separador de espacio de nombres de clave",
|
||||
etcdEndpoints: "Endpoints",
|
||||
etcdEndpointsHint: "Una entrada por línea. Déjalo en blanco para usar el host y puerto de arriba.",
|
||||
etcdGrpcMaxInbound: "Límite de recepción gRPC (MiB)",
|
||||
etcdGrpcMaxInboundHint: "Tamaño máximo de una respuesta de etcd. El valor predeterminado es 32 MiB y el máximo es 256 MiB.",
|
||||
etcdCaCertPlaceholder: "/ruta/a/ca.crt",
|
||||
etcdCaCertBrowse: "Elegir certificado CA",
|
||||
etcdClientAuth: "Autenticación de cliente",
|
||||
|
|
|
|||
|
|
@ -426,6 +426,8 @@ export default withEnglishFallback({
|
|||
redisKeySeparator: "Separatore namespace chiavi",
|
||||
etcdEndpoints: "Endpoint",
|
||||
etcdEndpointsHint: "Un endpoint per riga. Lascia vuoto per usare l'host e la porta sopra.",
|
||||
etcdGrpcMaxInbound: "Limite ricezione gRPC (MiB)",
|
||||
etcdGrpcMaxInboundHint: "Dimensione massima di una risposta etcd. Il valore predefinito è 32 MiB e il limite è 256 MiB.",
|
||||
etcdCaCertPlaceholder: "/percorso/per/ca.crt",
|
||||
etcdCaCertBrowse: "Scegli certificato CA",
|
||||
etcdClientAuth: "Autenticazione Client",
|
||||
|
|
|
|||
|
|
@ -426,6 +426,8 @@ export default withEnglishFallback({
|
|||
redisKeySeparator: "キー名前空間セパレーター",
|
||||
etcdEndpoints: "エンドポイント",
|
||||
etcdEndpointsHint: "1行に1つのエンドポイント。空白の場合、上のホストとポートを使用します。",
|
||||
etcdGrpcMaxInbound: "gRPC 受信上限(MiB)",
|
||||
etcdGrpcMaxInboundHint: "etcd 応答1件の最大サイズです。既定値は32 MiB、上限は256 MiBです。",
|
||||
etcdCaCertPlaceholder: "/path/to/ca.crt",
|
||||
etcdCaCertBrowse: "CA証明書を選択",
|
||||
etcdClientAuth: "クライアント認証",
|
||||
|
|
|
|||
|
|
@ -441,6 +441,8 @@ export default withEnglishFallback({
|
|||
redisKeySeparator: "키 네임스페이스 구분자",
|
||||
etcdEndpoints: "엔드포인트",
|
||||
etcdEndpointsHint: "한 줄에 하나의 엔드포인트. 비워두면 위의 호스트와 포트를 사용합니다.",
|
||||
etcdGrpcMaxInbound: "gRPC 수신 제한 (MiB)",
|
||||
etcdGrpcMaxInboundHint: "단일 etcd 응답의 최대 크기입니다. 기본값은 32 MiB이며 최대 256 MiB입니다.",
|
||||
etcdCaCertPlaceholder: "/path/to/ca.crt",
|
||||
etcdCaCertBrowse: "CA 인증서 선택",
|
||||
etcdClientAuth: "클라이언트 인증",
|
||||
|
|
|
|||
|
|
@ -427,6 +427,8 @@ export default withEnglishFallback({
|
|||
redisKeySeparator: "Separador de namespace de chave",
|
||||
etcdEndpoints: "Endpoints",
|
||||
etcdEndpointsHint: "Um endpoint por linha. Deixe em branco para usar o host e porta acima.",
|
||||
etcdGrpcMaxInbound: "Limite de recebimento gRPC (MiB)",
|
||||
etcdGrpcMaxInboundHint: "Tamanho máximo de uma resposta do etcd. O padrão é 32 MiB e o limite é 256 MiB.",
|
||||
etcdCaCertPlaceholder: "/caminho/para/ca.crt",
|
||||
etcdCaCertBrowse: "Escolher certificado CA",
|
||||
etcdClientAuth: "Autenticação do Cliente",
|
||||
|
|
|
|||
|
|
@ -448,6 +448,8 @@ export default withEnglishFallback({
|
|||
redisKeySeparator: "键名分隔符(可选)",
|
||||
etcdEndpoints: "Endpoints",
|
||||
etcdEndpointsHint: "每行一个 endpoint。留空时使用上面的 host 和端口。",
|
||||
etcdGrpcMaxInbound: "gRPC 接收上限(MiB)",
|
||||
etcdGrpcMaxInboundHint: "单条 etcd 响应的最大大小。默认 32 MiB,最高 256 MiB。",
|
||||
etcdCaCertPlaceholder: "/path/to/ca.crt",
|
||||
etcdCaCertBrowse: "选择 CA 证书",
|
||||
etcdClientAuth: "客户端认证",
|
||||
|
|
|
|||
|
|
@ -427,6 +427,8 @@ export default withEnglishFallback({
|
|||
redisKeySeparator: "鍵名分隔符(可選)",
|
||||
etcdEndpoints: "端點",
|
||||
etcdEndpointsHint: "每行一個端點。留空則使用上方的主機和連接埠。",
|
||||
etcdGrpcMaxInbound: "gRPC 接收上限(MiB)",
|
||||
etcdGrpcMaxInboundHint: "單筆 etcd 回應的最大大小。預設 32 MiB,最高 256 MiB。",
|
||||
etcdCaCertPlaceholder: "/path/to/ca.crt",
|
||||
etcdCaCertBrowse: "選擇 CA 憑證",
|
||||
etcdClientAuth: "用戶端認證",
|
||||
|
|
|
|||
Loading…
Reference in New Issue