From 3de302ab13d4a60c291cb5a415e8f10ad8b53f8f Mon Sep 17 00:00:00 2001 From: Alfredo Gutierrez Date: Wed, 27 Nov 2024 14:32:52 -0600 Subject: [PATCH] feat: server properties configurable (#365) Signed-off-by: Alfredo Gutierrez --- server/docs/configuration.md | 16 ++-- .../com/hedera/block/server/BlockNodeApp.java | 10 ++- .../com/hedera/block/server/ServerConfig.java | 84 +++++++++++++++++++ .../config/BlockNodeConfigExtension.java | 4 +- .../server/config/ConfigInjectionModule.java | 13 +++ .../ServerMappedConfigSourceInitializer.java | 20 ++--- .../hedera/block/server/BlockNodeAppTest.java | 7 +- .../hedera/block/server/ServerConfigTest.java | 69 +++++++++++++++ .../config/ConfigInjectionModuleTest.java | 23 +++-- ...rverMappedConfigSourceInitializerTest.java | 17 ++-- 10 files changed, 225 insertions(+), 38 deletions(-) create mode 100644 server/src/main/java/com/hedera/block/server/ServerConfig.java create mode 100644 server/src/test/java/com/hedera/block/server/ServerConfigTest.java diff --git a/server/docs/configuration.md b/server/docs/configuration.md index 50f2cd6d9..cda8487c1 100644 --- a/server/docs/configuration.md +++ b/server/docs/configuration.md @@ -9,10 +9,12 @@ The default configuration allows users to quickly get up and running without hav ease of use at the trade-off of some insecure default configuration. Most configuration settings have appropriate defaults and can be left unchanged. It is recommended to browse the properties below and adjust to your needs. -| Environment Variable | Description | Default Value | -|:---|:---|---:| -| PERSISTENCE_STORAGE_ROOT_PATH | The root path for the storage, if not provided will attempt to create a `data` on the working dir of the app. | | -| CONSUMER_TIMEOUT_THRESHOLD_MILLIS | Time to wait for subscribers before disconnecting in milliseconds | 1500 | -| SERVICE_DELAY_MILLIS | Service shutdown delay in milliseconds | 500 | -| MEDIATOR_RING_BUFFER_SIZE | Size of the ring buffer used by the mediator (must be a power of 2) | 67108864 | -| NOTIFIER_RING_BUFFER_SIZE | Size of the ring buffer used by the notifier (must be a power of 2) | 2048 | +| Environment Variable | Description | Default Value | +|:----------------------------------|:--------------------------------------------------------------------------------------------------------------|:--------------| +| PERSISTENCE_STORAGE_ROOT_PATH | The root path for the storage, if not provided will attempt to create a `data` on the working dir of the app. | | +| CONSUMER_TIMEOUT_THRESHOLD_MILLIS | Time to wait for subscribers before disconnecting in milliseconds | 1500 | +| SERVICE_DELAY_MILLIS | Service shutdown delay in milliseconds | 500 | +| MEDIATOR_RING_BUFFER_SIZE | Size of the ring buffer used by the mediator (must be a power of 2) | 67108864 | +| NOTIFIER_RING_BUFFER_SIZE | Size of the ring buffer used by the notifier (must be a power of 2) | 2048 | +| SERVER_PORT | The port the server will listen on | 8080 | +| SERVER_MAX_MESSAGE_SIZE_BYTES | The maximum size of a message frame in bytes | 1048576 | diff --git a/server/src/main/java/com/hedera/block/server/BlockNodeApp.java b/server/src/main/java/com/hedera/block/server/BlockNodeApp.java index e5d7723c4..91485e3bc 100644 --- a/server/src/main/java/com/hedera/block/server/BlockNodeApp.java +++ b/server/src/main/java/com/hedera/block/server/BlockNodeApp.java @@ -48,6 +48,7 @@ public class BlockNodeApp { private final WebServerConfig.Builder webServerBuilder; private final PbjBlockStreamService pbjBlockStreamService; private final PbjBlockAccessService pbjBlockAccessService; + private final ServerConfig serverConfig; /** * Constructs a new BlockNodeApp with the specified dependencies. @@ -57,6 +58,7 @@ public class BlockNodeApp { * @param pbjBlockStreamService defines the Block Stream services * @param pbjBlockAccessService defines the Block Access services * @param webServerBuilder used to build the web server and start it + * @param serverConfig has the server configuration */ @Inject public BlockNodeApp( @@ -64,12 +66,14 @@ public BlockNodeApp( @NonNull HealthService healthService, @NonNull PbjBlockStreamService pbjBlockStreamService, @NonNull PbjBlockAccessService pbjBlockAccessService, - @NonNull WebServerConfig.Builder webServerBuilder) { + @NonNull WebServerConfig.Builder webServerBuilder, + @NonNull ServerConfig serverConfig) { this.serviceStatus = serviceStatus; this.healthService = healthService; this.pbjBlockStreamService = pbjBlockStreamService; this.pbjBlockAccessService = pbjBlockAccessService; this.webServerBuilder = webServerBuilder; + this.serverConfig = serverConfig; } /** @@ -88,13 +92,13 @@ public void start() throws IOException { // Override the default message size final PbjConfig pbjConfig = PbjConfig.builder() .name(PBJ_PROTOCOL_PROVIDER_CONFIG_NAME) - .maxMessageSizeBytes(1024 * 4096) + .maxMessageSizeBytes(serverConfig.maxMessageSizeBytes()) .build(); // Build the web server // TODO: make port server a configurable value. final WebServer webServer = webServerBuilder - .port(8080) + .port(serverConfig.port()) .addProtocol(pbjConfig) .addRouting(pbjRouting) .addRouting(httpRouting) diff --git a/server/src/main/java/com/hedera/block/server/ServerConfig.java b/server/src/main/java/com/hedera/block/server/ServerConfig.java new file mode 100644 index 000000000..b10e8e6c4 --- /dev/null +++ b/server/src/main/java/com/hedera/block/server/ServerConfig.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hedera.block.server; + +import com.swirlds.config.api.ConfigData; +import com.swirlds.config.api.ConfigProperty; +import com.swirlds.config.api.validation.annotation.Max; +import com.swirlds.config.api.validation.annotation.Min; + +/** + * Use this configuration across the server features + * + *

ServerConfig will have settings for the server. + * + * @param maxMessageSizeBytes the http2 max message/frame size in bytes + * @param port the port the server will listen on + */ +@ConfigData("server") +public record ServerConfig( + @ConfigProperty(defaultValue = defaultMaxMessageSizeBytes) + @Min(minMaxMessageSizeBytes) + @Max(maxMaxMessageSizeBytes) + int maxMessageSizeBytes, + @ConfigProperty(defaultValue = defaultPort) @Min(minPort) @Max(maxPort) int port) { + private static final System.Logger LOGGER = System.getLogger(ServerConfig.class.getName()); + + // Constants for maxMessageSizeBytes property + private static final String defaultMaxMessageSizeBytes = "4_194_304"; + private static final long minMaxMessageSizeBytes = 10_240; + private static final long maxMaxMessageSizeBytes = 16_777_215; + + // Constants for port property + private static final String defaultPort = "8080"; + private static final int minPort = 1024; + private static final int maxPort = 65_535; + + /** + * Validate the configuration. + * + * @throws IllegalArgumentException if the configuration is invalid + */ + public ServerConfig { + + validateMaxMessageSizeBytes(maxMessageSizeBytes); + validatePort(port); + + LOGGER.log(System.Logger.Level.INFO, "Server configuration server.maxMessageSizeBytes: " + maxMessageSizeBytes); + LOGGER.log(System.Logger.Level.INFO, "Server configuration server.port: " + port); + } + + private void validatePort(int port) { + if (port < minPort) { + throw new IllegalArgumentException("port must be greater than " + minPort); + } + + if (port > maxPort) { + throw new IllegalArgumentException("port must be less than " + maxPort); + } + } + + private void validateMaxMessageSizeBytes(int maxMessageSizeBytes) { + if (maxMessageSizeBytes < minMaxMessageSizeBytes) { + throw new IllegalArgumentException("maxMessageSizeBytes must be greater than " + minMaxMessageSizeBytes); + } + + if (maxMessageSizeBytes > maxMaxMessageSizeBytes) { + throw new IllegalArgumentException("maxMessageSizeBytes must be less than " + maxMaxMessageSizeBytes); + } + } +} diff --git a/server/src/main/java/com/hedera/block/server/config/BlockNodeConfigExtension.java b/server/src/main/java/com/hedera/block/server/config/BlockNodeConfigExtension.java index 198a9ec0b..48b032784 100644 --- a/server/src/main/java/com/hedera/block/server/config/BlockNodeConfigExtension.java +++ b/server/src/main/java/com/hedera/block/server/config/BlockNodeConfigExtension.java @@ -17,6 +17,7 @@ package com.hedera.block.server.config; import com.google.auto.service.AutoService; +import com.hedera.block.server.ServerConfig; import com.hedera.block.server.consumer.ConsumerConfig; import com.hedera.block.server.mediator.MediatorConfig; import com.hedera.block.server.notifier.NotifierConfig; @@ -54,6 +55,7 @@ public Set> getConfigDataTypes() { PrometheusConfig.class, ProducerConfig.class, ConsumerConfig.class, - PersistenceStorageConfig.class); + PersistenceStorageConfig.class, + ServerConfig.class); } } diff --git a/server/src/main/java/com/hedera/block/server/config/ConfigInjectionModule.java b/server/src/main/java/com/hedera/block/server/config/ConfigInjectionModule.java index 342790d80..e8c2550cd 100644 --- a/server/src/main/java/com/hedera/block/server/config/ConfigInjectionModule.java +++ b/server/src/main/java/com/hedera/block/server/config/ConfigInjectionModule.java @@ -16,6 +16,7 @@ package com.hedera.block.server.config; +import com.hedera.block.server.ServerConfig; import com.hedera.block.server.consumer.ConsumerConfig; import com.hedera.block.server.mediator.MediatorConfig; import com.hedera.block.server.notifier.NotifierConfig; @@ -118,4 +119,16 @@ static NotifierConfig provideNotifierConfig(Configuration configuration) { static ProducerConfig provideProducerConfig(Configuration configuration) { return configuration.getConfigData(ProducerConfig.class); } + + /** + * Provides a server configuration singleton using the configuration. + * + * @param configuration is the configuration singleton + * @return a server configuration singleton + */ + @Singleton + @Provides + static ServerConfig provideServerConfig(Configuration configuration) { + return configuration.getConfigData(ServerConfig.class); + } } diff --git a/server/src/main/java/com/hedera/block/server/config/ServerMappedConfigSourceInitializer.java b/server/src/main/java/com/hedera/block/server/config/ServerMappedConfigSourceInitializer.java index ac49cf417..67b2d8aa0 100644 --- a/server/src/main/java/com/hedera/block/server/config/ServerMappedConfigSourceInitializer.java +++ b/server/src/main/java/com/hedera/block/server/config/ServerMappedConfigSourceInitializer.java @@ -26,15 +26,14 @@ * A class that extends {@link MappedConfigSource} ir order to have project-relevant initialization. */ public final class ServerMappedConfigSourceInitializer { - private static final List MAPPINGS = - List.of( - new ConfigMapping( - "consumer.timeoutThresholdMillis", "CONSUMER_TIMEOUT_THRESHOLD_MILLIS"), - new ConfigMapping( - "persistence.storage.rootPath", "PERSISTENCE_STORAGE_ROOT_PATH"), - new ConfigMapping("service.delayMillis", "SERVICE_DELAY_MILLIS"), - new ConfigMapping("mediator.ringBufferSize", "MEDIATOR_RING_BUFFER_SIZE"), - new ConfigMapping("notifier.ringBufferSize", "NOTIFIER_RING_BUFFER_SIZE")); + private static final List MAPPINGS = List.of( + new ConfigMapping("consumer.timeoutThresholdMillis", "CONSUMER_TIMEOUT_THRESHOLD_MILLIS"), + new ConfigMapping("persistence.storage.rootPath", "PERSISTENCE_STORAGE_ROOT_PATH"), + new ConfigMapping("service.delayMillis", "SERVICE_DELAY_MILLIS"), + new ConfigMapping("mediator.ringBufferSize", "MEDIATOR_RING_BUFFER_SIZE"), + new ConfigMapping("notifier.ringBufferSize", "NOTIFIER_RING_BUFFER_SIZE"), + new ConfigMapping("server.maxMessageSizeBytes", "SERVER_MAX_MESSAGE_SIZE_BYTES"), + new ConfigMapping("server.port", "SERVER_PORT")); private ServerMappedConfigSourceInitializer() {} @@ -47,8 +46,7 @@ private ServerMappedConfigSourceInitializer() {} */ @NonNull public static MappedConfigSource getMappedConfigSource() { - final MappedConfigSource config = - new MappedConfigSource(SystemEnvironmentConfigSource.getInstance()); + final MappedConfigSource config = new MappedConfigSource(SystemEnvironmentConfigSource.getInstance()); MAPPINGS.forEach(config::addMapping); return config; } diff --git a/server/src/test/java/com/hedera/block/server/BlockNodeAppTest.java b/server/src/test/java/com/hedera/block/server/BlockNodeAppTest.java index 40e067b50..001a0857b 100644 --- a/server/src/test/java/com/hedera/block/server/BlockNodeAppTest.java +++ b/server/src/test/java/com/hedera/block/server/BlockNodeAppTest.java @@ -74,18 +74,23 @@ class BlockNodeAppTest { @Mock private BlockNodeContext blockNodeContext; + ServerConfig serverConfig; + private BlockNodeApp blockNodeApp; @BeforeEach void setup() { + serverConfig = new ServerConfig(4_194_304, 8080); + blockNodeApp = new BlockNodeApp( serviceStatus, healthService, new PbjBlockStreamServiceProxy( liveStreamMediator, serviceStatus, blockNodeEventHandler, notifier, blockNodeContext), new PbjBlockAccessServiceProxy(serviceStatus, blockReader, blockNodeContext), - webServerBuilder); + webServerBuilder, + serverConfig); when(webServerBuilder.port(8080)).thenReturn(webServerBuilder); when(webServerBuilder.addProtocol(any(PbjConfig.class))).thenReturn(webServerBuilder); diff --git a/server/src/test/java/com/hedera/block/server/ServerConfigTest.java b/server/src/test/java/com/hedera/block/server/ServerConfigTest.java new file mode 100644 index 000000000..14ab2f077 --- /dev/null +++ b/server/src/test/java/com/hedera/block/server/ServerConfigTest.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hedera.block.server; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class ServerConfigTest { + + @BeforeEach + void setUp() {} + + @AfterEach + void tearDown() {} + + @Test + void testValidValues() { + + ServerConfig serverConfig = new ServerConfig(4_194_304, 8080); + + assertEquals(4_194_304, serverConfig.maxMessageSizeBytes()); + assertEquals(8080, serverConfig.port()); + } + + @Test + void testMessageSizeTooBig() { + assertThrows(IllegalArgumentException.class, () -> { + new ServerConfig(16_777_216, 8080); + }); + } + + @Test + void testMessageSizeTooSmall() { + assertThrows(IllegalArgumentException.class, () -> { + new ServerConfig(10_239, 8080); + }); + } + + @Test + void testPortValueTooBig() { + assertThrows(IllegalArgumentException.class, () -> { + new ServerConfig(4_194_304, 65_536); + }); + } + + @Test + void testPortValueTooSmall() { + assertThrows(IllegalArgumentException.class, () -> { + new ServerConfig(4_194_304, 1023); + }); + } +} diff --git a/server/src/test/java/com/hedera/block/server/config/ConfigInjectionModuleTest.java b/server/src/test/java/com/hedera/block/server/config/ConfigInjectionModuleTest.java index 898fe0b70..5d2f58824 100644 --- a/server/src/test/java/com/hedera/block/server/config/ConfigInjectionModuleTest.java +++ b/server/src/test/java/com/hedera/block/server/config/ConfigInjectionModuleTest.java @@ -18,6 +18,7 @@ import static org.junit.jupiter.api.Assertions.*; +import com.hedera.block.server.ServerConfig; import com.hedera.block.server.consumer.ConsumerConfig; import com.hedera.block.server.mediator.MediatorConfig; import com.hedera.block.server.notifier.NotifierConfig; @@ -36,12 +37,10 @@ void testProvidePersistenceStorageConfig() throws IOException { BlockNodeContext context = TestConfigUtil.getTestBlockNodeContext(); Configuration configuration = context.configuration(); - PersistenceStorageConfig persistenceStorageConfig = - configuration.getConfigData(PersistenceStorageConfig.class); + PersistenceStorageConfig persistenceStorageConfig = configuration.getConfigData(PersistenceStorageConfig.class); // Call the method under test - PersistenceStorageConfig providedConfig = - ConfigInjectionModule.providePersistenceStorageConfig(configuration); + PersistenceStorageConfig providedConfig = ConfigInjectionModule.providePersistenceStorageConfig(configuration); // Verify that the correct config data is returned assertNotNull(providedConfig); @@ -71,8 +70,7 @@ void testProvidePrometheusConfig() throws IOException { PrometheusConfig prometheusConfig = configuration.getConfigData(PrometheusConfig.class); // Call the method under test - PrometheusConfig providedConfig = - ConfigInjectionModule.providePrometheusConfig(configuration); + PrometheusConfig providedConfig = ConfigInjectionModule.providePrometheusConfig(configuration); // Verify that the correct config data is returned assertNotNull(providedConfig); @@ -119,4 +117,17 @@ void testProvideNotifierConfig() throws IOException { assertNotNull(providedConfig); assertSame(notifierConfig, providedConfig); } + + @Test + void testServerConfig() throws IOException { + BlockNodeContext context = TestConfigUtil.getTestBlockNodeContext(); + Configuration configuration = context.configuration(); + ServerConfig serverConfig = configuration.getConfigData(ServerConfig.class); + + ServerConfig providedConfig = ConfigInjectionModule.provideServerConfig(configuration); + + // Verify the config + assertNotNull(providedConfig); + assertSame(serverConfig, providedConfig); + } } diff --git a/server/src/test/java/com/hedera/block/server/config/ServerMappedConfigSourceInitializerTest.java b/server/src/test/java/com/hedera/block/server/config/ServerMappedConfigSourceInitializerTest.java index 863b8a3ea..5c2f03dde 100644 --- a/server/src/test/java/com/hedera/block/server/config/ServerMappedConfigSourceInitializerTest.java +++ b/server/src/test/java/com/hedera/block/server/config/ServerMappedConfigSourceInitializerTest.java @@ -33,7 +33,9 @@ class ServerMappedConfigSourceInitializerTest { new ConfigMapping("persistence.storage.rootPath", "PERSISTENCE_STORAGE_ROOT_PATH"), new ConfigMapping("service.delayMillis", "SERVICE_DELAY_MILLIS"), new ConfigMapping("mediator.ringBufferSize", "MEDIATOR_RING_BUFFER_SIZE"), - new ConfigMapping("notifier.ringBufferSize", "NOTIFIER_RING_BUFFER_SIZE") + new ConfigMapping("notifier.ringBufferSize", "NOTIFIER_RING_BUFFER_SIZE"), + new ConfigMapping("server.maxMessageSizeBytes", "SERVER_MAX_MESSAGE_SIZE_BYTES"), + new ConfigMapping("server.port", "SERVER_PORT"), }; private static MappedConfigSource toTest; @@ -59,19 +61,16 @@ void test_VerifyAllSupportedMappingsAreAddedToInstance() throws ReflectiveOperat for (final ConfigMapping current : SUPPORTED_MAPPINGS) { final Predicate predicate = - cm -> - current.mappedName().equals(cm.mappedName()) - && current.originalName().equals(cm.originalName()); + cm -> current.mappedName().equals(cm.mappedName()) + && current.originalName().equals(cm.originalName()); assertTrue( actual.stream().anyMatch(predicate), - () -> - "when testing for: [%s] it is not contained in mappings of the actual initialized object %s" - .formatted(current, actual)); + () -> "when testing for: [%s] it is not contained in mappings of the actual initialized object %s" + .formatted(current, actual)); } } - private static Queue extractConfigMappings() - throws ReflectiveOperationException { + private static Queue extractConfigMappings() throws ReflectiveOperationException { final Field configMappings = MappedConfigSource.class.getDeclaredField("configMappings"); try { configMappings.setAccessible(true);