[tor-commits] [stem/master] Temporarily cache 'GETINFO address' responses

atagar at torproject.org atagar at torproject.org
Fri Oct 27 15:39:48 UTC 2017


commit d0922386bf73ab81e3db9f993adf9842c18f4d2a
Author: Damian Johnson <atagar at torproject.org>
Date:   Wed Oct 25 13:33:34 2017 -0700

    Temporarily cache 'GETINFO address' responses
    
    This is a mutable parameter, but it's both reasonably static and highly
    requested. Caching it for a second by default.
---
 stem/control.py                 | 23 +++++++++++++++++++++++
 test/unit/control/controller.py | 29 +++++++++++++++++++++++++++--
 2 files changed, 50 insertions(+), 2 deletions(-)

diff --git a/stem/control.py b/stem/control.py
index 6746441c..eebc24b6 100644
--- a/stem/control.py
+++ b/stem/control.py
@@ -372,6 +372,7 @@ MAPPED_CONFIG_KEYS = {
 # unchangeable GETINFO parameters
 
 CACHEABLE_GETINFO_PARAMS = (
+  'address',
   'version',
   'config-file',
   'exit-policy/default',
@@ -389,6 +390,12 @@ CACHEABLE_GETINFO_PARAMS_UNTIL_SETCONF = (
   'accounting/enabled',
 )
 
+# 'GETINFO address' isn't technically cachable, but it's also both highly
+# requested and highly static. As such fetching it at a fixed rate. If
+# you'd like you can set this global to zero to disable caching.
+
+CACHE_ADDRESS_FOR = 1.0
+
 # GETCONF parameters we shouldn't cache. This includes hidden service
 # perameters due to the funky way they're set and retrieved (for instance,
 # 'SETCONF HiddenServiceDir' effects 'GETCONF HiddenServiceOptions').
@@ -1048,6 +1055,9 @@ class Controller(BaseController):
     self._event_listeners_lock = threading.RLock()
     self._enabled_features = []
     self._is_geoip_unavailable = None
+
+    self._address_cached_at = 0
+    self._last_address_exc = None
     self._last_fingerprint_exc = None
 
     super(Controller, self).__init__(control_socket, is_authenticated)
@@ -1145,6 +1155,8 @@ class Controller(BaseController):
     for param in params:
       if param.startswith('ip-to-country/') and param != 'ip-to-country/0.0.0.0' and self.is_geoip_unavailable():
         raise stem.ProtocolError('Tor geoip database is unavailable')
+      elif param == 'address' and self._last_address_exc and (time.time() - self._address_cached_at) < CACHE_ADDRESS_FOR:
+        raise self._last_address_exc  # we already know we can't resolve an address
       elif param == 'fingerprint' and self._last_fingerprint_exc and self.get_conf('ORPort', None) is None:
         raise self._last_fingerprint_exc  # we already know we're not a relay
 
@@ -1153,6 +1165,9 @@ class Controller(BaseController):
     cached_results = self._get_cache_map(map(str.lower, params), 'getinfo')
 
     for key in cached_results:
+      if key == 'address' and (time.time() - self._address_cached_at) > CACHE_ADDRESS_FOR:
+        continue  # cached address is too old
+
       user_expected_key = _case_insensitive_lookup(params, key)
       reply[user_expected_key] = cached_results[key]
       params.remove(user_expected_key)
@@ -1192,6 +1207,10 @@ class Controller(BaseController):
 
         self._set_cache(to_cache, 'getinfo')
 
+      if 'address' in params:
+        self._address_cached_at = time.time()
+        self._last_address_exc = None
+
       if 'fingerprint' in params:
         self._last_fingerprint_exc = None
 
@@ -1202,6 +1221,10 @@ class Controller(BaseController):
       else:
         return list(reply.values())[0]
     except stem.ControllerError as exc:
+      if 'address' in params:
+        self._address_cached_at = time.time()
+        self._last_address_exc = exc
+
       if 'fingerprint' in params:
         self._last_fingerprint_exc = exc
 
diff --git a/test/unit/control/controller.py b/test/unit/control/controller.py
index c7e412c5..a9a7e73d 100644
--- a/test/unit/control/controller.py
+++ b/test/unit/control/controller.py
@@ -5,6 +5,7 @@ integ tests, but a few bits lend themselves to unit testing.
 
 import datetime
 import io
+import time
 import unittest
 
 import stem.descriptor.router_status_entry
@@ -46,13 +47,37 @@ class TestControl(unittest.TestCase):
       self.assertTrue(stem.control.event_description(event) is not None)
 
   @patch('stem.control.Controller.msg')
-  def test_get_get_info(self, msg_mock):
+  def test_get_info(self, msg_mock):
     msg_mock.return_value = ControlMessage.from_str('250-hello=hi right back!\r\n250 OK\r\n', 'GETINFO')
     self.assertEqual('hi right back!', self.controller.get_info('hello'))
 
   @patch('stem.control.Controller.msg')
+  def test_get_info_address_caching(self, msg_mock):
+    test_start = time.time()
+    msg_mock.return_value = ControlMessage.from_str('551 Address unknown\r\n')
+
+    self.assertEqual(0, self.controller._address_cached_at)
+    self.assertEqual(None, self.controller._last_address_exc)
+    self.assertRaisesRegexp(stem.ProtocolError, 'Address unknown', self.controller.get_info, 'address')
+    self.assertTrue(test_start <= self.controller._address_cached_at <= time.time())
+    self.assertEqual("GETINFO response didn't have an OK status:\nAddress unknown", str(self.controller._last_address_exc))
+    self.assertEqual(1, msg_mock.call_count)
+
+    # now that we have a cached failure we should provide that back
+
+    self.assertRaisesRegexp(stem.ProtocolError, 'Address unknown', self.controller.get_info, 'address')
+    self.assertEqual(1, msg_mock.call_count)
+
+    # pretend it's a minute later and try again, now with an address
+
+    msg_mock.return_value = ControlMessage.from_str('250-address=17.2.89.80\r\n250 OK\r\n', 'GETINFO')
+    self.assertRaisesRegexp(stem.ProtocolError, 'Address unknown', self.controller.get_info, 'address')
+    self.controller._address_cached_at -= 60
+    self.assertEqual('17.2.89.80', self.controller.get_info('address'))
+
+  @patch('stem.control.Controller.msg')
   @patch('stem.control.Controller.get_conf')
-  def test_get_get_info_without_fingerprint(self, get_conf_mock, msg_mock):
+  def test_get_info_without_fingerprint(self, get_conf_mock, msg_mock):
     msg_mock.return_value = ControlMessage.from_str('551 Not running in server mode\r\n')
     get_conf_mock.return_value = None
 



More information about the tor-commits mailing list