diff --git a/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/server/DefaultServerFactoryPropertyMapper.java b/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/server/DefaultServerFactoryPropertyMapper.java index 34b4fef..2939137 100644 --- a/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/server/DefaultServerFactoryPropertyMapper.java +++ b/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/server/DefaultServerFactoryPropertyMapper.java @@ -25,6 +25,7 @@ import io.grpc.ServerBuilder; import org.springframework.boot.context.properties.PropertyMapper; import org.springframework.grpc.server.DefaultGrpcServerFactory; +import org.springframework.util.unit.DataSize; /** * Helper class used to map {@link GrpcServerProperties} to @@ -46,10 +47,39 @@ class DefaultServerFactoryPropertyMapper> { * @param serverBuilder the builder */ void customizeServerBuilder(T serverBuilder) { - PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); - GrpcServerProperties.KeepAlive keepAlive = this.properties.getKeepAlive(); - map.from(keepAlive.getTime()).to(durationProperty(serverBuilder::keepAliveTime)); - map.from(keepAlive.getTimeout()).to(durationProperty(serverBuilder::keepAliveTimeout)); + PropertyMapper mapper = PropertyMapper.get().alwaysApplyingWhenNonNull(); + customizeKeepAlive(serverBuilder, mapper); + customizeInboundLimits(serverBuilder, mapper); + } + + /** + * Map the keep-alive properties to the server factory's server builder. + * @param serverBuilder the builder + * @param mapper the property mapper + */ + void customizeKeepAlive(T serverBuilder, PropertyMapper mapper) { + GrpcServerProperties.KeepAlive keepAliveProps = this.properties.getKeepAlive(); + mapper.from(keepAliveProps.getTime()).to(durationProperty(serverBuilder::keepAliveTime)); + mapper.from(keepAliveProps.getTimeout()).to(durationProperty(serverBuilder::keepAliveTimeout)); + mapper.from(keepAliveProps.getMaxIdle()).to(durationProperty(serverBuilder::maxConnectionIdle)); + mapper.from(keepAliveProps.getMaxAge()).to(durationProperty(serverBuilder::maxConnectionAge)); + mapper.from(keepAliveProps.getMaxAgeGrace()).to(durationProperty(serverBuilder::maxConnectionAgeGrace)); + mapper.from(keepAliveProps.getPermitTime()).to(durationProperty(serverBuilder::permitKeepAliveTime)); + mapper.from(keepAliveProps.isPermitWithoutCalls()).to(serverBuilder::permitKeepAliveWithoutCalls); + } + + /** + * Map the inbound limits properties to the server factory's server builder. + * @param serverBuilder the builder + * @param mapper the property mapper + */ + void customizeInboundLimits(T serverBuilder, PropertyMapper mapper) { + mapper.from(properties.getMaxInboundMessageSize()) + .asInt(DataSize::toBytes) + .to(serverBuilder::maxInboundMessageSize); + mapper.from(properties.getMaxInboundMetadataSize()) + .asInt(DataSize::toBytes) + .to(serverBuilder::maxInboundMetadataSize); } Consumer durationProperty(BiConsumer setter) { diff --git a/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/server/GrpcServerProperties.java b/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/server/GrpcServerProperties.java index cbf016b..95b9987 100644 --- a/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/server/GrpcServerProperties.java +++ b/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/server/GrpcServerProperties.java @@ -19,7 +19,10 @@ import java.time.Duration; import java.time.temporal.ChronoUnit; import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.convert.DataSizeUnit; import org.springframework.boot.convert.DurationUnit; +import org.springframework.util.unit.DataSize; +import org.springframework.util.unit.DataUnit; @ConfigurationProperties(prefix = "spring.grpc.server") public class GrpcServerProperties { @@ -29,16 +32,6 @@ public class GrpcServerProperties { */ public static final String ANY_IP_ADDRESS = "*"; - /** - * Server should listen to any IPv4 address. - */ - public static final String ANY_IPv4_ADDRESS = "0.0.0.0"; - - /** - * Server should listen to any IPv6 address. - */ - public static final String ANY_IPv6_ADDRESS = "::"; - /** * Server address to bind to. The default is any IP address ('*'). */ @@ -46,6 +39,7 @@ public class GrpcServerProperties { /** * Server port to listen on. When the value is 0, a random available port is selected. + * The default is 9090. */ private int port = 9090; @@ -57,10 +51,22 @@ public class GrpcServerProperties { @DurationUnit(ChronoUnit.SECONDS) private Duration shutdownGracePeriod = Duration.of(30, ChronoUnit.SECONDS); + /** + * Maximum message size allowed to be received by the server (default 4MiB). + */ + @DataSizeUnit(DataUnit.BYTES) + private DataSize maxInboundMessageSize = DataSize.ofBytes(4 * 1024 * 1024); + + /** + * Maximum metadata size allowed to be received by the server (default 8KiB). + */ + @DataSizeUnit(DataUnit.BYTES) + private DataSize maxInboundMetadataSize = DataSize.ofBytes(8192); + private final KeepAlive keepAlive = new KeepAlive(); public String getAddress() { - return address; + return this.address; } public void setAddress(String address) { @@ -68,7 +74,7 @@ public class GrpcServerProperties { } public int getPort() { - return port; + return this.port; } public void setPort(int port) { @@ -76,13 +82,29 @@ public class GrpcServerProperties { } public Duration getShutdownGracePeriod() { - return shutdownGracePeriod; + return this.shutdownGracePeriod; } public void setShutdownGracePeriod(Duration shutdownGracePeriod) { this.shutdownGracePeriod = shutdownGracePeriod; } + public DataSize getMaxInboundMessageSize() { + return this.maxInboundMessageSize; + } + + public void setMaxInboundMessageSize(DataSize maxInboundMessageSize) { + this.maxInboundMessageSize = maxInboundMessageSize; + } + + public DataSize getMaxInboundMetadataSize() { + return this.maxInboundMetadataSize; + } + + public void setMaxInboundMetadataSize(DataSize maxInboundMetadataSize) { + this.maxInboundMetadataSize = maxInboundMetadataSize; + } + public KeepAlive getKeepAlive() { return this.keepAlive; } @@ -103,8 +125,40 @@ public class GrpcServerProperties { @DurationUnit(ChronoUnit.SECONDS) private Duration timeout = Duration.of(20, ChronoUnit.SECONDS); + /** + * Maximum time a connection can remain idle before being gracefully terminated + * (default infinite). + */ + @DurationUnit(ChronoUnit.SECONDS) + private Duration maxIdle = null; + + /** + * Maximum time a connection may exist before being gracefully terminated (default + * infinite). + */ + @DurationUnit(ChronoUnit.SECONDS) + private Duration maxAge = null; + + /** + * Maximum time for graceful connection termination (default infinite). + */ + @DurationUnit(ChronoUnit.SECONDS) + private Duration maxAgeGrace = null; + + /** + * Maximum keep-alive time clients are permitted to configure (default 5m). + */ + @DurationUnit(ChronoUnit.SECONDS) + private Duration permitTime = Duration.of(5, ChronoUnit.MINUTES); + + /** + * Whether clients are permitted to send keep alive pings when there are no + * outstanding RPCs on the connection (default false). + */ + private boolean permitWithoutCalls = false; + public Duration getTime() { - return time; + return this.time; } public void setTime(Duration time) { @@ -112,13 +166,53 @@ public class GrpcServerProperties { } public Duration getTimeout() { - return timeout; + return this.timeout; } public void setTimeout(Duration timeout) { this.timeout = timeout; } + public Duration getMaxIdle() { + return this.maxIdle; + } + + public void setMaxIdle(Duration maxIdle) { + this.maxIdle = maxIdle; + } + + public Duration getMaxAge() { + return this.maxAge; + } + + public void setMaxAge(Duration maxAge) { + this.maxAge = maxAge; + } + + public Duration getMaxAgeGrace() { + return this.maxAgeGrace; + } + + public void setMaxAgeGrace(Duration maxAgeGrace) { + this.maxAgeGrace = maxAgeGrace; + } + + public Duration getPermitTime() { + return this.permitTime; + } + + public void setPermitTime(Duration permitTime) { + this.permitTime = permitTime; + } + + public boolean isPermitWithoutCalls() { + return this.permitWithoutCalls; + } + + public void setPermitWithoutCalls(boolean permitWithoutCalls) { + this.permitWithoutCalls = permitWithoutCalls; + } + } } diff --git a/spring-grpc-spring-boot-autoconfigure/src/test/java/org/springframework/grpc/autoconfigure/server/GrpcServerPropertiesTests.java b/spring-grpc-spring-boot-autoconfigure/src/test/java/org/springframework/grpc/autoconfigure/server/GrpcServerPropertiesTests.java index ab54922..38366f1 100644 --- a/spring-grpc-spring-boot-autoconfigure/src/test/java/org/springframework/grpc/autoconfigure/server/GrpcServerPropertiesTests.java +++ b/spring-grpc-spring-boot-autoconfigure/src/test/java/org/springframework/grpc/autoconfigure/server/GrpcServerPropertiesTests.java @@ -25,6 +25,7 @@ import org.junit.jupiter.api.Test; import org.springframework.boot.context.properties.bind.Binder; import org.springframework.boot.context.properties.source.MapConfigurationPropertySource; +import org.springframework.util.unit.DataSize; import static org.assertj.core.api.Assertions.assertThat; @@ -49,10 +50,11 @@ class GrpcServerPropertiesTests { Map map = new HashMap<>(); map.put("spring.grpc.server.address", "my-server-ip"); map.put("spring.grpc.server.port", "3130"); - map.put("spring.grpc.server.shutdown-grace-period", "15s"); + map.put("spring.grpc.server.shutdown-grace-period", "15"); GrpcServerProperties properties = bindProperties(map); assertThat(properties.getAddress()).isEqualTo("my-server-ip"); assertThat(properties.getPort()).isEqualTo(3130); + assertThat(properties.getShutdownGracePeriod()).isEqualTo(Duration.ofSeconds(15)); } } @@ -65,9 +67,62 @@ class GrpcServerPropertiesTests { Map map = new HashMap<>(); map.put("spring.grpc.server.keep-alive.time", "45m"); map.put("spring.grpc.server.keep-alive.timeout", "40s"); + map.put("spring.grpc.server.keep-alive.max-idle", "1h"); + map.put("spring.grpc.server.keep-alive.max-age", "3h"); + map.put("spring.grpc.server.keep-alive.max-age-grace", "21s"); + map.put("spring.grpc.server.keep-alive.permit-time", "33s"); + map.put("spring.grpc.server.keep-alive.permit-without-calls", "true"); GrpcServerProperties.KeepAlive properties = bindProperties(map).getKeepAlive(); + assertThatPropertiesSetAsExpected(properties); + } + + @Test + void bindWithoutUnitsSpecified() { + Map map = new HashMap<>(); + map.put("spring.grpc.server.keep-alive.time", "2700"); + map.put("spring.grpc.server.keep-alive.timeout", "40"); + map.put("spring.grpc.server.keep-alive.max-idle", "3600"); + map.put("spring.grpc.server.keep-alive.max-age", "10800"); + map.put("spring.grpc.server.keep-alive.max-age-grace", "21"); + map.put("spring.grpc.server.keep-alive.permit-time", "33"); + map.put("spring.grpc.server.keep-alive.permit-without-calls", "true"); + GrpcServerProperties.KeepAlive properties = bindProperties(map).getKeepAlive(); + assertThatPropertiesSetAsExpected(properties); + } + + private void assertThatPropertiesSetAsExpected(GrpcServerProperties.KeepAlive properties) { assertThat(properties.getTime()).isEqualTo(Duration.ofMinutes(45)); assertThat(properties.getTimeout()).isEqualTo(Duration.ofSeconds(40)); + assertThat(properties.getMaxIdle()).isEqualTo(Duration.ofHours(1)); + assertThat(properties.getMaxAge()).isEqualTo(Duration.ofHours(3)); + assertThat(properties.getMaxAgeGrace()).isEqualTo(Duration.ofSeconds(21)); + assertThat(properties.getPermitTime()).isEqualTo(Duration.ofSeconds(33)); + assertThat(properties.isPermitWithoutCalls()).isTrue(); + } + + } + + @Nested + class InboundLimitsProperties { + + @Test + void bind() { + Map map = new HashMap<>(); + map.put("spring.grpc.server.max-inbound-message-size", "20MB"); + map.put("spring.grpc.server.max-inbound-metadata-size", "1MB"); + GrpcServerProperties properties = bindProperties(map); + assertThat(properties.getMaxInboundMessageSize()).isEqualTo(DataSize.ofMegabytes(20)); + assertThat(properties.getMaxInboundMetadataSize()).isEqualTo(DataSize.ofMegabytes(1)); + } + + @Test + void bindWithoutUnits() { + Map map = new HashMap<>(); + map.put("spring.grpc.server.max-inbound-message-size", "1048576"); + map.put("spring.grpc.server.max-inbound-metadata-size", "1024"); + GrpcServerProperties properties = bindProperties(map); + assertThat(properties.getMaxInboundMessageSize()).isEqualTo(DataSize.ofMegabytes(1)); + assertThat(properties.getMaxInboundMetadataSize()).isEqualTo(DataSize.ofKilobytes(1)); } } diff --git a/spring-grpc-spring-boot-autoconfigure/src/test/java/org/springframework/grpc/autoconfigure/server/ServerFactoryPropertyMappersTests.java b/spring-grpc-spring-boot-autoconfigure/src/test/java/org/springframework/grpc/autoconfigure/server/ServerFactoryPropertyMappersTests.java index 343b4c5..2d78d47 100644 --- a/spring-grpc-spring-boot-autoconfigure/src/test/java/org/springframework/grpc/autoconfigure/server/ServerFactoryPropertyMappersTests.java +++ b/spring-grpc-spring-boot-autoconfigure/src/test/java/org/springframework/grpc/autoconfigure/server/ServerFactoryPropertyMappersTests.java @@ -24,6 +24,8 @@ import java.util.function.Supplier; import io.grpc.ServerBuilder; import org.junit.jupiter.api.Test; +import org.springframework.util.unit.DataSize; + import static org.mockito.BDDMockito.then; import static org.mockito.Mockito.mock; @@ -59,11 +61,25 @@ class ServerFactoryPropertyMappersTests { GrpcServerProperties properties = new GrpcServerProperties(); properties.getKeepAlive().setTime(Duration.ofHours(1)); properties.getKeepAlive().setTimeout(Duration.ofSeconds(10)); + properties.getKeepAlive().setMaxIdle(Duration.ofHours(2)); + properties.getKeepAlive().setMaxAge(Duration.ofHours(3)); + properties.getKeepAlive().setMaxAgeGrace(Duration.ofSeconds(45)); + properties.getKeepAlive().setPermitTime(Duration.ofMinutes(7)); + properties.getKeepAlive().setPermitWithoutCalls(true); + properties.setMaxInboundMessageSize(DataSize.ofMegabytes(333)); + properties.setMaxInboundMetadataSize(DataSize.ofKilobytes(111)); X mapper = mapperFactory.apply(properties); T builder = mockBuilderToCustomize.get(); mapper.customizeServerBuilder(builder); then(builder).should().keepAliveTime(Duration.ofHours(1).toNanos(), TimeUnit.NANOSECONDS); then(builder).should().keepAliveTimeout(Duration.ofSeconds(10).toNanos(), TimeUnit.NANOSECONDS); + then(builder).should().maxConnectionIdle(Duration.ofHours(2).toNanos(), TimeUnit.NANOSECONDS); + then(builder).should().maxConnectionAge(Duration.ofHours(3).toNanos(), TimeUnit.NANOSECONDS); + then(builder).should().maxConnectionAgeGrace(Duration.ofSeconds(45).toNanos(), TimeUnit.NANOSECONDS); + then(builder).should().permitKeepAliveTime(Duration.ofMinutes(7).toNanos(), TimeUnit.NANOSECONDS); + then(builder).should().permitKeepAliveWithoutCalls(true); + then(builder).should().maxInboundMessageSize(Math.toIntExact(DataSize.ofMegabytes(333).toBytes())); + then(builder).should().maxInboundMetadataSize(Math.toIntExact(DataSize.ofKilobytes(111).toBytes())); } }