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`"