diff --git a/agents/drivers/etcd/src/main/java/com/dbx/agent/etcd/EtcdAgent.java b/agents/drivers/etcd/src/main/java/com/dbx/agent/etcd/EtcdAgent.java index 9605ecd06..db57ada6b 100644 --- a/agents/drivers/etcd/src/main/java/com/dbx/agent/etcd/EtcdAgent.java +++ b/agents/drivers/etcd/src/main/java/com/dbx/agent/etcd/EtcdAgent.java @@ -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 CAPABILITIES = Collections.unmodifiableList(Arrays.asList( @@ -167,7 +171,8 @@ public final class EtcdAgent { List 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 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(); diff --git a/agents/drivers/etcd/src/test/java/com/dbx/agent/etcd/EtcdAgentTest.java b/agents/drivers/etcd/src/test/java/com/dbx/agent/etcd/EtcdAgentTest.java index d8cec636b..f74e0c7a6 100644 --- a/agents/drivers/etcd/src/test/java/com/dbx/agent/etcd/EtcdAgentTest.java +++ b/agents/drivers/etcd/src/test/java/com/dbx/agent/etcd/EtcdAgentTest.java @@ -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( diff --git a/apps/desktop/src/components/connection/ConnectionDialog.vue b/apps/desktop/src/components/connection/ConnectionDialog.vue index d20a27a2d..c161037e0 100644 --- a/apps/desktop/src/components/connection/ConnectionDialog.vue +++ b/apps/desktop/src/components/connection/ConnectionDialog.vue @@ -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) { +
+ +
+ +

{{ t("connection.etcdGrpcMaxInboundHint") }}

+
+
diff --git a/apps/desktop/src/i18n/locales/en.ts b/apps/desktop/src/i18n/locales/en.ts index 84faf6ec7..5c5467c86 100644 --- a/apps/desktop/src/i18n/locales/en.ts +++ b/apps/desktop/src/i18n/locales/en.ts @@ -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", diff --git a/apps/desktop/src/i18n/locales/es.ts b/apps/desktop/src/i18n/locales/es.ts index 49f009bee..5cdc6eb31 100644 --- a/apps/desktop/src/i18n/locales/es.ts +++ b/apps/desktop/src/i18n/locales/es.ts @@ -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", diff --git a/apps/desktop/src/i18n/locales/it.ts b/apps/desktop/src/i18n/locales/it.ts index 58b014460..4520534ad 100644 --- a/apps/desktop/src/i18n/locales/it.ts +++ b/apps/desktop/src/i18n/locales/it.ts @@ -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", diff --git a/apps/desktop/src/i18n/locales/ja.ts b/apps/desktop/src/i18n/locales/ja.ts index addef60a0..90dc6b404 100644 --- a/apps/desktop/src/i18n/locales/ja.ts +++ b/apps/desktop/src/i18n/locales/ja.ts @@ -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: "クライアント認証", diff --git a/apps/desktop/src/i18n/locales/ko.ts b/apps/desktop/src/i18n/locales/ko.ts index 7c4880aa9..6d470d624 100644 --- a/apps/desktop/src/i18n/locales/ko.ts +++ b/apps/desktop/src/i18n/locales/ko.ts @@ -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: "클라이언트 인증", diff --git a/apps/desktop/src/i18n/locales/pt-BR.ts b/apps/desktop/src/i18n/locales/pt-BR.ts index 9782437e2..ed6887aa3 100644 --- a/apps/desktop/src/i18n/locales/pt-BR.ts +++ b/apps/desktop/src/i18n/locales/pt-BR.ts @@ -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", diff --git a/apps/desktop/src/i18n/locales/zh-CN.ts b/apps/desktop/src/i18n/locales/zh-CN.ts index 5a810dc41..f83448d95 100644 --- a/apps/desktop/src/i18n/locales/zh-CN.ts +++ b/apps/desktop/src/i18n/locales/zh-CN.ts @@ -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: "客户端认证", diff --git a/apps/desktop/src/i18n/locales/zh-TW.ts b/apps/desktop/src/i18n/locales/zh-TW.ts index a6adde794..7e55e0b1a 100644 --- a/apps/desktop/src/i18n/locales/zh-TW.ts +++ b/apps/desktop/src/i18n/locales/zh-TW.ts @@ -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: "用戶端認證",