From f74c4cc83c0e6c3f28319589aadfbf95ffd97926 Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Wed, 28 Jul 2021 15:53:22 +0100 Subject: [PATCH] Add config options for a global bandwidth limit This uses Netty's global traffic shaping handlers to limit the rate at which packets can be sent and received. If the bandwidth limit is hit, we'll start dropping packets, which will mean remote servers send traffic to us at a much slower pace. This isn't perfect, as there is only a global limit, and not a per-computer one. As a result, its possible for one computer to use all/most bandwidth, and thus slow down other computers. This would be something to improve on in the future. However, I've spent a lot of time reading the netty source code and docs, and the implementation for that is significantly more complex, and one I'm not comfortable working on right now. For the time being, this satisfies the issues in #33 and hopefully alleviates server owner's concerns about the http API. Remaining problems can either be solved by moderation (with help of the //computercraft track` command) or future updates. Closes #33 --- .../dan200/computercraft/ComputerCraft.java | 2 ++ .../core/apis/http/NetworkUtils.java | 31 ++++++++++++++----- .../core/apis/http/request/HttpRequest.java | 1 + .../core/apis/http/websocket/Websocket.java | 1 + .../computercraft/shared/CommonHooks.java | 3 ++ .../dan200/computercraft/shared/Config.java | 20 ++++++++++++ 6 files changed, 51 insertions(+), 7 deletions(-) diff --git a/src/main/java/dan200/computercraft/ComputerCraft.java b/src/main/java/dan200/computercraft/ComputerCraft.java index 440b4e5cd..32ff07327 100644 --- a/src/main/java/dan200/computercraft/ComputerCraft.java +++ b/src/main/java/dan200/computercraft/ComputerCraft.java @@ -52,6 +52,8 @@ public final class ComputerCraft public static int httpMaxRequests = 16; public static int httpMaxWebsockets = 4; + public static int httpDownloadBandwidth = 32 * 1024 * 1024; + public static int httpUploadBandwidth = 32 * 1024 * 1024; public static boolean enableCommandBlock = false; public static int modemRange = 64; diff --git a/src/main/java/dan200/computercraft/core/apis/http/NetworkUtils.java b/src/main/java/dan200/computercraft/core/apis/http/NetworkUtils.java index f3804f847..d5b1b18f2 100644 --- a/src/main/java/dan200/computercraft/core/apis/http/NetworkUtils.java +++ b/src/main/java/dan200/computercraft/core/apis/http/NetworkUtils.java @@ -20,6 +20,8 @@ import io.netty.handler.ssl.SslContext; import io.netty.handler.ssl.SslContextBuilder; import io.netty.handler.timeout.ReadTimeoutException; +import io.netty.handler.traffic.AbstractTrafficShapingHandler; +import io.netty.handler.traffic.GlobalTrafficShapingHandler; import javax.annotation.Nonnull; import javax.net.ssl.SSLException; @@ -28,9 +30,7 @@ import java.net.InetSocketAddress; import java.net.URI; import java.security.KeyStore; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.SynchronousQueue; -import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.TimeUnit; /** @@ -38,10 +38,8 @@ */ public final class NetworkUtils { - public static final ExecutorService EXECUTOR = new ThreadPoolExecutor( - 4, Integer.MAX_VALUE, - 60L, TimeUnit.SECONDS, - new SynchronousQueue<>(), + public static final ScheduledThreadPoolExecutor EXECUTOR = new ScheduledThreadPoolExecutor( + 4, ThreadUtils.builder( "Network" ) .setPriority( Thread.MIN_PRIORITY + (Thread.NORM_PRIORITY - Thread.MIN_PRIORITY) / 2 ) .build() @@ -52,6 +50,15 @@ public final class NetworkUtils .build() ); + public static final AbstractTrafficShapingHandler SHAPING_HANDLER = new GlobalTrafficShapingHandler( + EXECUTOR, ComputerCraft.httpUploadBandwidth, ComputerCraft.httpDownloadBandwidth + ); + + static + { + EXECUTOR.setKeepAliveTime( 60, TimeUnit.SECONDS ); + } + private NetworkUtils() { } @@ -107,6 +114,16 @@ public static SslContext getSslContext() throws HTTPRequestException } } + public static void reloadConfig() + { + SHAPING_HANDLER.configure( ComputerCraft.httpUploadBandwidth, ComputerCraft.httpDownloadBandwidth ); + } + + public static void reset() + { + SHAPING_HANDLER.trafficCounter().resetCumulativeTime(); + } + /** * Create a {@link InetSocketAddress} from a {@link java.net.URI}. * diff --git a/src/main/java/dan200/computercraft/core/apis/http/request/HttpRequest.java b/src/main/java/dan200/computercraft/core/apis/http/request/HttpRequest.java index 2049575ff..74957b204 100644 --- a/src/main/java/dan200/computercraft/core/apis/http/request/HttpRequest.java +++ b/src/main/java/dan200/computercraft/core/apis/http/request/HttpRequest.java @@ -167,6 +167,7 @@ protected void initChannel( SocketChannel ch ) } ChannelPipeline p = ch.pipeline(); + p.addLast( NetworkUtils.SHAPING_HANDLER ); if( sslContext != null ) { p.addLast( sslContext.newHandler( ch.alloc(), uri.getHost(), socketAddress.getPort() ) ); diff --git a/src/main/java/dan200/computercraft/core/apis/http/websocket/Websocket.java b/src/main/java/dan200/computercraft/core/apis/http/websocket/Websocket.java index 155540560..046efe79d 100644 --- a/src/main/java/dan200/computercraft/core/apis/http/websocket/Websocket.java +++ b/src/main/java/dan200/computercraft/core/apis/http/websocket/Websocket.java @@ -145,6 +145,7 @@ private void doConnect() protected void initChannel( SocketChannel ch ) { ChannelPipeline p = ch.pipeline(); + p.addLast( NetworkUtils.SHAPING_HANDLER ); if( sslContext != null ) { p.addLast( sslContext.newHandler( ch.alloc(), uri.getHost(), socketAddress.getPort() ) ); diff --git a/src/main/java/dan200/computercraft/shared/CommonHooks.java b/src/main/java/dan200/computercraft/shared/CommonHooks.java index 75db335cf..9a30a763e 100644 --- a/src/main/java/dan200/computercraft/shared/CommonHooks.java +++ b/src/main/java/dan200/computercraft/shared/CommonHooks.java @@ -6,6 +6,7 @@ package dan200.computercraft.shared; import dan200.computercraft.ComputerCraft; +import dan200.computercraft.core.apis.http.NetworkUtils; import dan200.computercraft.core.computer.MainThread; import dan200.computercraft.core.tracking.Tracking; import dan200.computercraft.shared.command.CommandComputerCraft; @@ -83,6 +84,7 @@ public static void onServerStarted( FMLServerStartedEvent event ) ComputerCraft.serverComputerRegistry.reset(); WirelessNetwork.resetNetworks(); Tracking.reset(); + NetworkUtils.reset(); } @SubscribeEvent @@ -91,6 +93,7 @@ public static void onServerStopped( FMLServerStoppedEvent event ) ComputerCraft.serverComputerRegistry.reset(); WirelessNetwork.resetNetworks(); Tracking.reset(); + NetworkUtils.reset(); } public static final ResourceLocation LOOT_TREASURE_DISK = new ResourceLocation( ComputerCraft.MOD_ID, "treasure_disk" ); diff --git a/src/main/java/dan200/computercraft/shared/Config.java b/src/main/java/dan200/computercraft/shared/Config.java index 4fb4f2c7d..ab29dc4ec 100644 --- a/src/main/java/dan200/computercraft/shared/Config.java +++ b/src/main/java/dan200/computercraft/shared/Config.java @@ -12,6 +12,7 @@ import com.google.common.base.Converter; import dan200.computercraft.ComputerCraft; import dan200.computercraft.api.turtle.event.TurtleAction; +import dan200.computercraft.core.apis.http.NetworkUtils; import dan200.computercraft.core.apis.http.options.Action; import dan200.computercraft.core.apis.http.options.AddressRuleConfig; import dan200.computercraft.shared.peripheral.monitor.MonitorRenderer; @@ -56,6 +57,9 @@ public final class Config private static final ConfigValue httpMaxRequests; private static final ConfigValue httpMaxWebsockets; + private static final ConfigValue httpDownloadBandwidth; + private static final ConfigValue httpUploadBandwidth; + private static final ConfigValue commandBlockEnabled; private static final ConfigValue modemRange; private static final ConfigValue modemHighAltitudeRange; @@ -187,6 +191,20 @@ private Config() {} .comment( "The number of websockets a computer can have open at one time. Set to 0 for unlimited." ) .defineInRange( "max_websockets", ComputerCraft.httpMaxWebsockets, 1, Integer.MAX_VALUE ); + builder + .comment( "Limits bandwidth used by computers" ) + .push( "bandwidth" ); + + httpDownloadBandwidth = builder + .comment( "The number of bytes which can be downloaded in a second. This is shared across all computers. (bytes/s)" ) + .defineInRange( "global_download", ComputerCraft.httpDownloadBandwidth, 1, Integer.MAX_VALUE ); + + httpUploadBandwidth = builder + .comment( "The number of bytes which can be uploaded in a second. This is shared across all computers. (bytes/s)" ) + .defineInRange( "global_upload", ComputerCraft.httpUploadBandwidth, 1, Integer.MAX_VALUE ); + + builder.pop(); + builder.pop(); } @@ -329,6 +347,8 @@ public static void sync() ComputerCraft.httpMaxRequests = httpMaxRequests.get(); ComputerCraft.httpMaxWebsockets = httpMaxWebsockets.get(); + ComputerCraft.httpDownloadBandwidth = httpDownloadBandwidth.get(); + NetworkUtils.reloadConfig(); // Peripheral ComputerCraft.enableCommandBlock = commandBlockEnabled.get();