From d44c7d2c1a5e9ec0e2d2f8c768d51efac0cf20f0 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Mon, 31 Jul 2017 14:14:06 -0700 Subject: [PATCH] server: increase RLIMIT_NOFILE to let us use more sockets Linux defaults to a soft limit of 1024, which limits us to 512 simultaneous non-transit-using connections. The transit relay runs in the same process, so long-running relayed transfers will compete for those sockets too. This raises the soft limit to equal the hard limit (if possible), or as much as we can manage, if the soft limit was less than 10k. If the resource.setrlimit calls aren't available (e.g. windows), or some other error happens, this will log a message and continue without changing the limits. closes #238 --- src/wormhole/server/server.py | 32 ++++++++++++++++ src/wormhole/test/test_server.py | 66 +++++++++++++++++++++++++++++++- 2 files changed, 97 insertions(+), 1 deletion(-) diff --git a/src/wormhole/server/server.py b/src/wormhole/server/server.py index aebd034..8340e98 100644 --- a/src/wormhole/server/server.py +++ b/src/wormhole/server/server.py @@ -2,6 +2,7 @@ # a str on Python 2 from __future__ import print_function import os, time, json +import resource from twisted.python import log from twisted.internet import reactor, endpoints from twisted.application import service, internet @@ -103,8 +104,39 @@ class RelayServer(service.MultiService): self._transit = transit self._transit_service = transit_service + def increase_rlimits(self): + try: + soft, hard = resource.getrlimit(resource.RLIMIT_NOFILE) + except AttributeError: + log.msg("AttributeError during getrlimit, leaving it alone") + return + if soft >= 10000: + log.msg("RLIMIT_NOFILE.soft was %d, leaving it alone" % soft) + return + # OS-X defaults to soft=7168, and reports a huge number for 'hard', + # but won't accept anything more than soft=10240, so we can't just + # set soft=hard. Linux returns (1024, 1048576) and is fine with + # soft=hard. Cygwin is reported to return (256,-1) and accepts up to + # soft=3200. So we try multiple values until something works. + for newlimit in [hard, 10000, 3200, 1024]: + log.msg("changing RLIMIT_NOFILE from (%s,%s) to (%s,%s)" % + (soft, hard, newlimit, hard)) + try: + resource.setrlimit(resource.RLIMIT_NOFILE, (newlimit, hard)) + log.msg("setrlimit successful") + return + except ValueError as e: + log.msg("error during setrlimit: %s" % e) + continue + except: + log.msg("other error during setrlimit, leaving it alone") + log.err() + return + log.msg("unable to change rlimit, leaving it alone") + def startService(self): service.MultiService.startService(self) + self.increase_rlimits() log.msg("websocket listening on /wormhole-relay/ws") log.msg("Wormhole relay server (Rendezvous and Transit) running") if self._blur_usage: diff --git a/src/wormhole/test/test_server.py b/src/wormhole/test/test_server.py index 343ffa4..9b21b69 100644 --- a/src/wormhole/test/test_server.py +++ b/src/wormhole/test/test_server.py @@ -3,7 +3,7 @@ import os, json, itertools, time import mock from twisted.trial import unittest from twisted.python import log -from twisted.internet import reactor, defer +from twisted.internet import reactor, defer, endpoints from twisted.internet.defer import inlineCallbacks, returnValue from autobahn.twisted import websocket from .common import ServerBase @@ -24,6 +24,70 @@ def easy_relay( **kwargs ) +class RLimits(unittest.TestCase): + def test_rlimit(self): + def patch_s(name, *args, **kwargs): + return mock.patch("wormhole.server.server." + name, *args, **kwargs) + # We never start this, so the endpoints can be fake. + # serverFromString() requires bytes on py2 and str on py3, so this + # is easier than just passing "tcp:0" + ep = endpoints.TCP4ServerEndpoint(None, 0) + with patch_s("endpoints.serverFromString", return_value=ep): + s = server.RelayServer("fake", None, None) + noattrs = object() + fakelog = [] + def checklog(*expected): + self.assertEqual(fakelog, list(expected)) + fakelog[:] = [] + NF = "NOFILE" + mock_NF = patch_s("resource.RLIMIT_NOFILE", NF) + + with patch_s("log.msg", fakelog.append): + with patch_s("resource", noattrs): + s.increase_rlimits() + checklog("AttributeError during getrlimit, leaving it alone") + + with mock_NF: + with patch_s("resource.getrlimit", + return_value=(20000, 30000)) as gr: + s.increase_rlimits() + self.assertEqual(gr.mock_calls, [mock.call(NF)]) + checklog("RLIMIT_NOFILE.soft was 20000, leaving it alone") + + with patch_s("resource.getrlimit", + return_value=(10, 30000)) as gr: + with patch_s("resource.setrlimit", + side_effect=TypeError("other")): + with patch_s("log.err") as err: + s.increase_rlimits() + self.assertEqual(err.mock_calls, [mock.call()]) + checklog("changing RLIMIT_NOFILE from (10,30000) to (30000,30000)", + "other error during setrlimit, leaving it alone") + + for maxlimit in [40000, 20000, 9000, 2000, 1000]: + def setrlimit(which, newlimit): + if newlimit[0] > maxlimit: + raise ValueError("nope") + return None + calls = [] + expected = [] + for tries in [30000, 10000, 3200, 1024]: + calls.append(mock.call(NF, (tries, 30000))) + expected.append("changing RLIMIT_NOFILE from (10,30000) to (%d,30000)" % tries) + if tries > maxlimit: + expected.append("error during setrlimit: nope") + else: + expected.append("setrlimit successful") + break + else: + expected.append("unable to change rlimit, leaving it alone") + + with patch_s("resource.setrlimit", + side_effect=setrlimit) as sr: + s.increase_rlimits() + self.assertEqual(sr.mock_calls, calls) + checklog(*expected) + class _Util: def _nameplate(self, app, name): np_row = app._db.execute("SELECT * FROM `nameplates`"