fix(etcd): make gRPC max inbound message size configurable

This commit is contained in:
tyloryang 2026-08-09 23:28:02 +08:00 committed by GitHub
parent 5966aea43b
commit f1c998c9cc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 113 additions and 1 deletions

View File

@ -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();

View File

@ -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(

View File

@ -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">

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -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: "クライアント認証",

View File

@ -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: "클라이언트 인증",

View File

@ -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",

View File

@ -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: "客户端认证",

View File

@ -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: "用戶端認證",