Repository navigation
fix: time out idle HTTP connections and harden start.sh #7003
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: release_v4.8.3
Are you sure you want to change the base?
Changes from all commits
4a7acdc
e56b0d3
7b8ca88
bb50d9e
7dd470c
5dd60da
6025d61
57da1fd
1f8458d
a1b0c83
e68a2e5
0479e69
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1017,14 +1017,16 @@ private static void loadDnsPublishParameters(NodeConfig.DnsConfig dns, | |
| if (dns.getChangeThreshold() > 0) { | ||
| publishConfig.setChangeThreshold(dns.getChangeThreshold()); | ||
| } else if (Double.compare(dns.getChangeThreshold(), 0.0) != 0) { | ||
| logger.error("Check node.dns.changeThreshold, should be bigger than 0, default 0.1"); | ||
| throw new TronError("Check node.dns.changeThreshold, should be bigger than 0, default 0.1", | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [NIT] Fail-fast on invalid dns parameters also fires when node.dns.publish=false
The error message is clear and the fix is a one-line config edit, so this is not blocking — but the behavior-change surface is wider than "validation for the DNS publish feature" suggests. Suggestion: either (a) skip the validation when |
||
| TronError.ErrCode.PARAMETER_INIT); | ||
| } | ||
|
|
||
| int maxMergeSize = dns.getMaxMergeSize(); | ||
| if (maxMergeSize >= 1 && maxMergeSize <= 5) { | ||
| publishConfig.setMaxMergeSize(maxMergeSize); | ||
| } else if (maxMergeSize != 0) { | ||
| logger.error("Check node.dns.maxMergeSize, should be [1~5], default 5"); | ||
| throw new TronError("Check node.dns.maxMergeSize, should be [1~5], default 5", | ||
| TronError.ErrCode.PARAMETER_INIT); | ||
| } | ||
|
|
||
| if (StringUtils.isNotEmpty(dns.getDnsPrivate())) { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,148 @@ | ||
| package org.tron.common.jetty; | ||
|
|
||
| import java.io.BufferedReader; | ||
| import java.io.IOException; | ||
| import java.io.InputStreamReader; | ||
| import java.io.OutputStream; | ||
| import java.net.InetSocketAddress; | ||
| import java.net.Socket; | ||
| import java.net.SocketException; | ||
| import java.nio.charset.StandardCharsets; | ||
| import java.util.ArrayList; | ||
| import java.util.List; | ||
| import java.util.concurrent.TimeUnit; | ||
| import javax.servlet.http.HttpServlet; | ||
| import javax.servlet.http.HttpServletRequest; | ||
| import javax.servlet.http.HttpServletResponse; | ||
| import org.eclipse.jetty.server.AbstractConnector; | ||
| import org.eclipse.jetty.servlet.ServletContextHandler; | ||
| import org.eclipse.jetty.servlet.ServletHolder; | ||
| import org.junit.AfterClass; | ||
| import org.junit.Assert; | ||
| import org.junit.BeforeClass; | ||
| import org.junit.ClassRule; | ||
| import org.junit.Test; | ||
| import org.junit.rules.TemporaryFolder; | ||
| import org.tron.common.TestConstants; | ||
| import org.tron.common.application.HttpService; | ||
| import org.tron.common.utils.PublicMethod; | ||
| import org.tron.core.config.args.Args; | ||
|
|
||
| /** | ||
| * Tests the connection limit configured in {@link HttpService}: once connections that send | ||
| * nothing hold every slot, the server closes them after the limit's idle timeout and serves | ||
| * new clients, instead of waiting for the connector's 30-second idle timeout. | ||
| */ | ||
| public class ConnectionLimitTest { | ||
|
|
||
| private static final int MAX_CONNECTIONS = 2; | ||
|
|
||
| // Below the connector's default 30-second idle timeout, above the limit's 10-second one | ||
| // applied twice (Jetty half-closes an idle connection before closing it). | ||
| private static final int TIMEOUT_MS = 25_000; | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [NIT] ConnectionLimitTest takes ~20s and runs with a tight timing margin The test must really wait for the hardcoded Suggestion: make the idle timeout injectable (e.g. a |
||
|
|
||
| @ClassRule | ||
| public static final TemporaryFolder temporaryFolder = new TemporaryFolder(); | ||
|
|
||
| private static TestHttpService httpService; | ||
| private static int port; | ||
|
|
||
| public static class OkServlet extends HttpServlet { | ||
| @Override | ||
| protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { | ||
| resp.setStatus(HttpServletResponse.SC_OK); | ||
| resp.getWriter().print("ok"); | ||
| } | ||
| } | ||
|
|
||
| static class TestHttpService extends HttpService { | ||
| TestHttpService(int port) { | ||
| this.port = port; | ||
| this.contextPath = "/"; | ||
| } | ||
|
|
||
| @Override | ||
| protected void addServlet(ServletContextHandler context) { | ||
| context.addServlet(new ServletHolder(new OkServlet()), "/*"); | ||
| } | ||
|
|
||
| int connectedEndPoints() { | ||
| return ((AbstractConnector) apiServer.getConnectors()[0]).getConnectedEndPoints().size(); | ||
| } | ||
| } | ||
|
|
||
| @BeforeClass | ||
| public static void setup() throws Exception { | ||
| Args.setParam(new String[]{"-d", temporaryFolder.newFolder().toString()}, | ||
| TestConstants.TEST_CONF); | ||
| Args.getInstance().setMaxHttpConnectNumber(MAX_CONNECTIONS); | ||
| port = PublicMethod.chooseRandomPort(); | ||
| httpService = new TestHttpService(port); | ||
| httpService.start().get(10, TimeUnit.SECONDS); | ||
| } | ||
|
|
||
| @AfterClass | ||
| public static void teardown() throws Exception { | ||
| try { | ||
| if (httpService != null) { | ||
| httpService.stop(); | ||
| } | ||
| } finally { | ||
| Args.clearParam(); | ||
| } | ||
| } | ||
|
|
||
| @Test(timeout = 60_000) | ||
| public void testIdleConnectionsDoNotLockOutClients() throws Exception { | ||
| List<Socket> idleSockets = new ArrayList<>(); | ||
| try { | ||
| for (int i = 0; i < MAX_CONNECTIONS; i++) { | ||
| Socket socket = new Socket(); | ||
| socket.connect(new InetSocketAddress("localhost", port), TIMEOUT_MS); | ||
| idleSockets.add(socket); | ||
| awaitConnectedEndPoints(i + 1); | ||
| } | ||
|
|
||
| Assert.assertEquals("HTTP/1.1 200 OK", get()); | ||
| for (Socket socket : idleSockets) { | ||
| assertClosedByServer(socket); | ||
| } | ||
| } finally { | ||
| for (Socket socket : idleSockets) { | ||
| socket.close(); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private static void awaitConnectedEndPoints(int expected) throws InterruptedException { | ||
| long deadline = System.currentTimeMillis() + TIMEOUT_MS; | ||
| while (httpService.connectedEndPoints() < expected) { | ||
| Assert.assertTrue("server did not accept connection " + expected, | ||
| System.currentTimeMillis() < deadline); | ||
| Thread.sleep(10); | ||
| } | ||
| } | ||
|
|
||
| private static String get() throws IOException { | ||
| try (Socket socket = new Socket()) { | ||
| socket.connect(new InetSocketAddress("localhost", port), TIMEOUT_MS); | ||
| socket.setSoTimeout(TIMEOUT_MS); | ||
| OutputStream out = socket.getOutputStream(); | ||
| out.write("GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n" | ||
| .getBytes(StandardCharsets.US_ASCII)); | ||
| out.flush(); | ||
| BufferedReader in = new BufferedReader( | ||
| new InputStreamReader(socket.getInputStream(), StandardCharsets.US_ASCII)); | ||
| return in.readLine(); | ||
| } | ||
| } | ||
|
|
||
| private static void assertClosedByServer(Socket socket) throws IOException { | ||
| socket.setSoTimeout(TIMEOUT_MS); | ||
| try { | ||
| Assert.assertEquals(-1, socket.getInputStream().read()); | ||
| } catch (SocketException e) { | ||
| // a reset also means the server dropped the connection | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,6 +8,10 @@ If you already downloaded the `FullNode.jar`, you can use `start.sh` to run it, | |
|
|
||
| The script is available in the java-tron project at [github](https://github.com/tronprotocol/java-tron), or if you need a separate script: [start.sh](https://github.com/tronprotocol/java-tron/blob/develop/start.sh) | ||
|
|
||
| The script runs on x86_64 with JDK 8 and on ARM64 with JDK 17. It picks the JVM options for the Java version it finds, and downloads the release jars built for that architecture (`FullNode-aarch64.jar` on ARM64). | ||
|
|
||
| Downloaded release jars are verified against the GPG signature published with each release, made by the key listed under "Integrity Check" in the [README](./README.md), so `gpg` must be installed; a jar that fails verification is not used. The mainnet config is downloaded from java-tron and the Nile testnet config from [nile-testnet](https://github.com/tron-nile-testnet/nile-testnet). | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [NIT] Documentation & metadata backlog (2 items rolled up) Grouped as one comment since both are doc-or-metadata asks. This is a cross-cutting concern — the anchored line itself is fine.
Suggestion: one sentence in shell.md about the keyserver requirement (plus an optional README note), and a prefixed branch name next time. |
||
|
|
||
| *** | ||
|
|
||
| # Usage | ||
|
|
@@ -32,6 +36,8 @@ The script is available in the java-tron project at [github](https://github.com/ | |
| sh start.sh --stop | ||
| ``` | ||
|
|
||
| `--run` records the process id in `<jar name>.pid` (`FullNode.jar.pid` by default) next to `start.log`, and `--stop` reads it. Run `--stop` in the directory the node was started from, with the same `-j` name if one was given. After `--release` or `-cb` the node runs in `FullNode/`; `--stop` finds it there from the parent directory as well. A node started by an earlier version of the script (a `start.log` but no pid file) is still found by its jar name. | ||
|
|
||
| * Get the latest version of `FullNode.jar` and start it | ||
|
|
||
| ``` | ||
|
|
@@ -58,17 +64,21 @@ The script is available in the java-tron project at [github](https://github.com/ | |
|
|
||
| start the service | ||
|
|
||
| * `--stop` | ||
| * `--stop` or `-s` | ||
|
|
||
| stop the service started from the current directory | ||
|
|
||
| * `--` | ||
|
|
||
| stop the service | ||
| Everything after it is passed to `FullNode.jar` unchanged. Options the script does not know are passed on as well, so `--` is only needed when a value of such an option looks like a script option or a jar name. | ||
|
|
||
| * `-c` | ||
|
|
||
| Specify the configuration file, by default it will load the `config.conf` in the same directory as `FullNode.jar` | ||
| Specify the configuration file, by default it will load the `config.conf` in the current directory | ||
|
|
||
| * `-d` | ||
|
|
||
| Specify the database storage path, The default path is the same directory where `FullNode.jar` is located. | ||
| Specify the database storage path. The default is `output-directory` in the directory the script is run from (`FullNode/` after `--release` or `-cb`). | ||
|
|
||
| * `-j` | ||
|
|
||
|
|
@@ -79,7 +89,7 @@ The script is available in the java-tron project at [github](https://github.com/ | |
| Specify the maximum memory of the `FullNode.jar` service in`MB`, jvm's startup maximum memory will be adjusted according to this parameter. | ||
|
|
||
| * `--net` | ||
| Select test and private networks. | ||
| Select test (Nile) and private networks. | ||
|
|
||
| ### build project | ||
|
|
||
|
|
@@ -91,6 +101,14 @@ The script is available in the java-tron project at [github](https://github.com/ | |
|
|
||
| Get the latest released version of the `jar` package from github. | ||
|
|
||
| * `--upgrade` | ||
|
|
||
| Replace the local `jar` package with the latest release; the previous one is kept as `FullNode.jar_bak`. | ||
|
|
||
| * `--download` | ||
|
|
||
| Download the latest released `jar` package into the current directory without starting it. | ||
|
|
||
|
|
||
| ### rebuild the manifest | ||
|
|
||
|
|
@@ -100,7 +118,7 @@ The script is available in the java-tron project at [github](https://github.com/ | |
|
|
||
| * `-m` | ||
|
|
||
| specify the minimum required manifest file size ,unit:M,default:0 | ||
| specify the minimum required manifest file size ,unit:M,default:128 | ||
|
|
||
| * `-b` | ||
|
|
||
|
|
@@ -152,7 +170,7 @@ sh start.sh --stop | |
| Format: | ||
|
|
||
| ``` | ||
| sh start.sh <[--release | -cb]> <--run> [-m <manifest size>] | [-b <batch size>] | [-d <db database-directory> | [-dr | --disable-rewrite-manifes]] | ||
| sh start.sh <[--release | -cb]> <--run> [-m <manifest size>] | [-b <batch size>] | [-d <db database-directory> | [-dr | --disable-rewrite-manifest]] | ||
| ``` | ||
|
|
||
| Get the latest released version. | ||
|
|
@@ -162,13 +180,15 @@ Get the latest released version. | |
| sh start.sh --release --run | ||
| ``` | ||
|
|
||
| Following file structure will be generated after executing the above command and the `FullNode.jar` will be started. | ||
| Following file structure will be generated after executing the above command and the `FullNode.jar` will be started. The node runs from `FullNode/`, so later `--run` commands are executed there with the copied script; `--stop` works both there and from the parent directory. | ||
|
|
||
| ``` | ||
| ├── ... | ||
| ├── FullNode/ | ||
| ├── config.conf | ||
| ├── FullNode.jar | ||
| ├── FullNode.jar.pid | ||
| ├── start.log | ||
| ├── start.sh | ||
| ``` | ||
|
|
||
|
|
@@ -214,12 +234,14 @@ Following file structure will be created: | |
| ├── FullNode/ | ||
| |── config.conf | ||
| ├── FullNode.jar | ||
| ├── FullNode.jar.pid | ||
| ├── start.log | ||
| ├── start.sh | ||
| ``` | ||
|
|
||
| ### 3. rebuild manifest tool | ||
|
|
||
| This tool provides the ability to reformat the manifest based on current database, Enabled by default. | ||
| This tool provides the ability to reformat the manifest based on current database, Enabled by default. It applies to LevelDB only and is skipped on ARM64, which runs RocksDB. | ||
|
|
||
| 1.Local mode: | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[NIT] While saturated, the 10s idle timeout applies to all established connections, not just inactive ones
When
maxHttpConnectNumberis reached, Jetty'sConnectionLimit.limit()applies this idle timeout to every connected endpoint, not only to connections that have sent nothing (verified against jetty-server 9.4.58 sources). During saturation, a keep-alive client with more than 10s between two requests on the same connection will be disconnected and must reconnect. Request processing time does not count as idle, so normal API calls are unaffected.This is inherent to the upstream API and a reasonable price for freeing slots with a minimal change, but the PR description's "idle connections" phrasing reads narrower than the actual behavior.
Suggestion: add one sentence to the PR description or the http/shell documentation describing the saturated-state effect on keep-alive clients; introduce a config key for the timeout only if field evidence shows real clients being hurt.