From 0c679e74ce509a255e21146053fe76a4999feff4 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Mon, 31 Jul 2017 14:01:54 -0700 Subject: [PATCH 1/3] server.py: move twisted.web.resource out of the way --- src/wormhole/server/server.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/wormhole/server/server.py b/src/wormhole/server/server.py index ee2ba48..aebd034 100644 --- a/src/wormhole/server/server.py +++ b/src/wormhole/server/server.py @@ -5,7 +5,8 @@ import os, time, json from twisted.python import log from twisted.internet import reactor, endpoints from twisted.application import service, internet -from twisted.web import server, static, resource +from twisted.web import server, static +from twisted.web.resource import Resource from autobahn.twisted.resource import WebSocketResource from .database import get_db from .rendezvous import Rendezvous @@ -18,10 +19,10 @@ MINUTE = 60*SECONDS CHANNEL_EXPIRATION_TIME = 11*MINUTE EXPIRATION_CHECK_PERIOD = 10*MINUTE -class Root(resource.Resource): +class Root(Resource): # child_FOO is a nevow thing, not a twisted.web.resource thing def __init__(self): - resource.Resource.__init__(self) + Resource.__init__(self) self.putChild(b"", static.Data(b"Wormhole Relay\n", "text/plain")) class PrivacyEnhancedSite(server.Site): From d44c7d2c1a5e9ec0e2d2f8c768d51efac0cf20f0 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Mon, 31 Jul 2017 14:14:06 -0700 Subject: [PATCH 2/3] 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`" From b9d1d11b03ee77cb3a10bf986deb49a6d88ea147 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Mon, 31 Jul 2017 15:42:25 -0700 Subject: [PATCH 3/3] server.py: windows doesn't even have the 'resource' module I thought it might exist, but be empty. But it doesn't even exist on windows, so we must guard against an ImportError. --- src/wormhole/server/server.py | 15 +++++++++------ src/wormhole/test/test_server.py | 19 +++++++------------ 2 files changed, 16 insertions(+), 18 deletions(-) diff --git a/src/wormhole/server/server.py b/src/wormhole/server/server.py index 8340e98..0174eca 100644 --- a/src/wormhole/server/server.py +++ b/src/wormhole/server/server.py @@ -2,7 +2,11 @@ # a str on Python 2 from __future__ import print_function import os, time, json -import resource +try: + # 'resource' is unix-only + from resource import getrlimit, setrlimit, RLIMIT_NOFILE +except ImportError: # pragma: nocover + getrlimit, setrlimit, RLIMIT_NOFILE = None, None, None # pragma: nocover from twisted.python import log from twisted.internet import reactor, endpoints from twisted.application import service, internet @@ -105,11 +109,10 @@ class RelayServer(service.MultiService): 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") + if getrlimit is None: + log.msg("unable to import 'resource', leaving rlimit alone") return + soft, hard = getrlimit(RLIMIT_NOFILE) if soft >= 10000: log.msg("RLIMIT_NOFILE.soft was %d, leaving it alone" % soft) return @@ -122,7 +125,7 @@ class RelayServer(service.MultiService): log.msg("changing RLIMIT_NOFILE from (%s,%s) to (%s,%s)" % (soft, hard, newlimit, hard)) try: - resource.setrlimit(resource.RLIMIT_NOFILE, (newlimit, hard)) + setrlimit(RLIMIT_NOFILE, (newlimit, hard)) log.msg("setrlimit successful") return except ValueError as e: diff --git a/src/wormhole/test/test_server.py b/src/wormhole/test/test_server.py index 9b21b69..8f55919 100644 --- a/src/wormhole/test/test_server.py +++ b/src/wormhole/test/test_server.py @@ -34,30 +34,26 @@ class RLimits(unittest.TestCase): 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) + mock_NF = patch_s("RLIMIT_NOFILE", NF) with patch_s("log.msg", fakelog.append): - with patch_s("resource", noattrs): + with patch_s("getrlimit", None): s.increase_rlimits() - checklog("AttributeError during getrlimit, leaving it alone") + checklog("unable to import 'resource', leaving rlimit alone") with mock_NF: - with patch_s("resource.getrlimit", - return_value=(20000, 30000)) as gr: + with patch_s("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("getrlimit", return_value=(10, 30000)) as gr: + with patch_s("setrlimit", side_effect=TypeError("other")): with patch_s("log.err") as err: s.increase_rlimits() self.assertEqual(err.mock_calls, [mock.call()]) @@ -82,8 +78,7 @@ class RLimits(unittest.TestCase): else: expected.append("unable to change rlimit, leaving it alone") - with patch_s("resource.setrlimit", - side_effect=setrlimit) as sr: + with patch_s("setrlimit", side_effect=setrlimit) as sr: s.increase_rlimits() self.assertEqual(sr.mock_calls, calls) checklog(*expected)