diff --git a/sdk/cosmos/azure-cosmos-tests/src/test/java/com/azure/cosmos/implementation/ConfigsTests.java b/sdk/cosmos/azure-cosmos-tests/src/test/java/com/azure/cosmos/implementation/ConfigsTests.java index a04a474faccf..c814714ce2ee 100644 --- a/sdk/cosmos/azure-cosmos-tests/src/test/java/com/azure/cosmos/implementation/ConfigsTests.java +++ b/sdk/cosmos/azure-cosmos-tests/src/test/java/com/azure/cosmos/implementation/ConfigsTests.java @@ -169,6 +169,67 @@ public void http2MaxConcurrentStreams() { } } + @Test(groups = { "unit" }) + public void http2MaxFrameSizeInBytes() { + final String propName = "COSMOS.HTTP2_MAX_FRAME_SIZE_IN_KB"; + final int defaultBytes = 64 * 1024; + final int minBytes = 64 * 1024; + final int maxBytes = 16_383 * 1024; + + System.clearProperty(propName); + + // Default (64 KB) + assertThat(Configs.getHttp2MaxFrameSizeInBytes()).isEqualTo(defaultBytes); + + // Valid value at lower bound + System.setProperty(propName, "64"); + try { + assertThat(Configs.getHttp2MaxFrameSizeInBytes()).isEqualTo(minBytes); + } finally { + System.clearProperty(propName); + } + + // Valid value at upper bound (16383 KB) + System.setProperty(propName, "16383"); + try { + assertThat(Configs.getHttp2MaxFrameSizeInBytes()).isEqualTo(maxBytes); + } finally { + System.clearProperty(propName); + } + + // Valid value within range (1 MB) + System.setProperty(propName, "1024"); + try { + assertThat(Configs.getHttp2MaxFrameSizeInBytes()).isEqualTo(1024 * 1024); + } finally { + System.clearProperty(propName); + } + + // Below lower bound -> clamped up to 64 KB + System.setProperty(propName, "32"); + try { + assertThat(Configs.getHttp2MaxFrameSizeInBytes()).isEqualTo(minBytes); + } finally { + System.clearProperty(propName); + } + + // Above upper bound -> clamped down to 16383 KB + System.setProperty(propName, String.valueOf(32 * 1024)); + try { + assertThat(Configs.getHttp2MaxFrameSizeInBytes()).isEqualTo(maxBytes); + } finally { + System.clearProperty(propName); + } + + // Unparseable -> falls back to default + System.setProperty(propName, "not-a-number"); + try { + assertThat(Configs.getHttp2MaxFrameSizeInBytes()).isEqualTo(defaultBytes); + } finally { + System.clearProperty(propName); + } + } + @Test(groups = { "emulator" }) public void thinClientEnabledTest() { assertThat(isThinClientEnabled()).isFalse(); diff --git a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/Configs.java b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/Configs.java index c1bab22d8cad..cf1a812e10f2 100644 --- a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/Configs.java +++ b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/Configs.java @@ -426,6 +426,16 @@ public class Configs { private static final String HTTP2_MAX_CONCURRENT_STREAMS = "COSMOS.HTTP2_MAX_CONCURRENT_STREAMS"; private static final String HTTP2_MAX_CONCURRENT_STREAMS_VARIABLE = "COSMOS_HTTP2_MAX_CONCURRENT_STREAMS"; + // Config to indicate the SETTINGS_MAX_FRAME_SIZE advertised by the HTTP/2 client to the remote peer. + // The value is expressed in kilobytes (KB) and is clamped to [64 KB, 16383 KB] — the lower bound matches + // the SDK's historical default so users can only grow the frame size, and the upper bound is the + // largest whole-KB value below the HTTP/2 spec max (RFC 7540: 2^24 - 1 bytes). + private static final int MIN_HTTP2_MAX_FRAME_SIZE_IN_KB = 64; // 64 KB + private static final int MAX_HTTP2_MAX_FRAME_SIZE_IN_KB = 16_383; // 16383 KB + private static final int DEFAULT_HTTP2_MAX_FRAME_SIZE_IN_KB = 64; // 64 KB + private static final String HTTP2_MAX_FRAME_SIZE_IN_KB = "COSMOS.HTTP2_MAX_FRAME_SIZE_IN_KB"; + private static final String HTTP2_MAX_FRAME_SIZE_IN_KB_VARIABLE = "COSMOS_HTTP2_MAX_FRAME_SIZE_IN_KB"; + private static final boolean DEFAULT_IS_NON_PARSEABLE_DOCUMENT_LOGGING_ENABLED = false; private static final String IS_NON_PARSEABLE_DOCUMENT_LOGGING_ENABLED = "COSMOS.IS_NON_PARSEABLE_DOCUMENT_LOGGING_ENABLED"; private static final String IS_NON_PARSEABLE_DOCUMENT_LOGGING_ENABLED_VARIABLE = "COSMOS_IS_NON_PARSEABLE_DOCUMENT_LOGGING_ENABLED"; @@ -1485,6 +1495,54 @@ public static int getHttp2MaxConcurrentStreams() { return Integer.parseInt(http2MaxConcurrentStreams); } + /** + * Returns the HTTP/2 SETTINGS_MAX_FRAME_SIZE to advertise to the remote peer, in bytes. + * + * The customer-facing configuration is expressed in kilobytes (KB) via system property + * {@code COSMOS.HTTP2_MAX_FRAME_SIZE_IN_KB} or environment variable + * {@code COSMOS_HTTP2_MAX_FRAME_SIZE_IN_KB}. Resolution order is system property, then environment + * variable, then the SDK default of 64 KB. The configured value is clamped to the inclusive range + * [64 KB, 16383 KB]; an unparseable or out-of-range value falls back to the default (or is clamped) + * and a warning is logged. The returned value is converted to bytes for downstream Netty + * consumption. + */ + public static int getHttp2MaxFrameSizeInBytes() { + String configuredValue = System.getProperty( + HTTP2_MAX_FRAME_SIZE_IN_KB, + firstNonNull( + emptyToNull(System.getenv().get(HTTP2_MAX_FRAME_SIZE_IN_KB_VARIABLE)), + String.valueOf(DEFAULT_HTTP2_MAX_FRAME_SIZE_IN_KB))); + + int parsedInKb; + try { + parsedInKb = Integer.parseInt(configuredValue); + } catch (NumberFormatException ex) { + logger.warn( + "Invalid value '{}' for {} / {}; falling back to default {} KB.", + configuredValue, + HTTP2_MAX_FRAME_SIZE_IN_KB, + HTTP2_MAX_FRAME_SIZE_IN_KB_VARIABLE, + DEFAULT_HTTP2_MAX_FRAME_SIZE_IN_KB); + return DEFAULT_HTTP2_MAX_FRAME_SIZE_IN_KB * 1024; + } + + if (parsedInKb < MIN_HTTP2_MAX_FRAME_SIZE_IN_KB || parsedInKb > MAX_HTTP2_MAX_FRAME_SIZE_IN_KB) { + int clampedInKb = Math.min( + MAX_HTTP2_MAX_FRAME_SIZE_IN_KB, + Math.max(MIN_HTTP2_MAX_FRAME_SIZE_IN_KB, parsedInKb)); + logger.warn( + "Configured HTTP/2 max frame size {} KB is outside the allowed range [{}, {}] KB; " + + "clamping to {} KB.", + parsedInKb, + MIN_HTTP2_MAX_FRAME_SIZE_IN_KB, + MAX_HTTP2_MAX_FRAME_SIZE_IN_KB, + clampedInKb); + return clampedInKb * 1024; + } + + return parsedInKb * 1024; + } + public static boolean isEmulatorServerCertValidationDisabled() { String certVerificationDisabledConfig = System.getProperty( EMULATOR_SERVER_CERTIFICATE_VALIDATION_DISABLED, diff --git a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/http/ReactorNettyClient.java b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/http/ReactorNettyClient.java index bdd6e68453b2..f62082edaacc 100644 --- a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/http/ReactorNettyClient.java +++ b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/http/ReactorNettyClient.java @@ -184,7 +184,7 @@ private void configureChannelPipelineHandlers() { .protocol(HttpProtocol.H2, HttpProtocol.HTTP11) .http2Settings(settings -> settings .initialWindowSize(1024 * 1024) // 1MB initial window size - .maxFrameSize(64 * 1024) // 64KB max frame size + .maxFrameSize(Configs.getHttp2MaxFrameSizeInBytes()) // 64KB default; overridable via COSMOS.HTTP2_MAX_FRAME_SIZE_IN_KB / COSMOS_HTTP2_MAX_FRAME_SIZE_IN_KB (clamped to [64KB, 16383KB]) .maxConcurrentStreams(http2CfgAccessor().getEffectiveMaxConcurrentStreams(http2Cfg)) // Increased from default 30 ) .doOnConnected((connection -> {