[tor-commits] [stem/master] Using @lru_cache for exit policies

atagar at torproject.org atagar at torproject.org
Tue Oct 8 07:50:17 UTC 2013


commit 19cca3a04b9b02e94d22ebb877af3ca2ca9ad5e4
Author: Damian Johnson <atagar at torproject.org>
Date:   Mon Oct 7 21:14:00 2013 -0700

    Using @lru_cache for exit policies
    
    The ExitPolicy and ExitPolicyRule classes are a perfect fit for @lru_cache.
    They're read-only classes that already do a fair bit of caching. The annotation
    lets us avoid doing this ourselves.
---
 stem/exit_policy.py             |  250 +++++++++++++++++----------------------
 stem/util/lru_cache.py          |   10 +-
 test/unit/exit_policy/policy.py |   22 ----
 3 files changed, 114 insertions(+), 168 deletions(-)

diff --git a/stem/exit_policy.py b/stem/exit_policy.py
index 17f3508..930c8bb 100644
--- a/stem/exit_policy.py
+++ b/stem/exit_policy.py
@@ -56,9 +56,16 @@ exiting to a destination is permissible or not. For instance...
   ============ ===========
 """
 
+import stem.prereq
 import stem.util.connection
 import stem.util.enum
 
+try:
+  # added in python 3.2
+  from collections import lru_cache
+except ImportError:
+  from stem.util.lru_cache import lru_cache
+
 AddressType = stem.util.enum.Enum(("WILDCARD", "Wildcard"), ("IPv4", "IPv4"), ("IPv6", "IPv6"))
 
 # Addresses aliased by the 'private' policy. From the tor man page...
@@ -77,21 +84,6 @@ PRIVATE_ADDRESSES = (
 )
 
 
-# TODO: The ExitPolicyRule could easily be a mutable class if we did the
-# following...
-#
-# * Provided setter methods that acquired an RLock which also wrapped all of
-#   our current methods to provide thread safety.
-#
-# * Reset our derived attributes (self._addr_bin, self._mask_bin, and
-#   self._str_representation) when we changed something that it was based on.
-#
-# That said, I'm not sure if this is entirely desirable since for most use
-# cases we *want* the caller to have an immutable ExitPolicy (since it
-# reflects something they... well, can't modify). However, I can think of
-# some use cases where we might want to construct custom policies. Maybe make
-# it a CustomExitPolicyRule subclass?
-
 def get_config_policy(rules):
   """
   Converts an ExitPolicy found in a torrc to a proper exit pattern. This
@@ -155,12 +147,20 @@ class ExitPolicy(object):
       if not isinstance(rule, (bytes, unicode, ExitPolicyRule)):
         raise TypeError("Exit policy rules can only contain strings or ExitPolicyRules, got a %s (%s)" % (type(rule), rules))
 
-    self._rules = None          # lazily loaded series of ExitPolicyRule
-    self._input_rules = rules   # input rules, only kept until self._rules is set
+    # Unparsed representation of the rules we were constructed with. Our
+    # _get_rules() method consumes this to provide ExitPolicyRule instances.
+    # This is lazily evaluated so we don't need to actually parse the exit
+    # policy if it's never used.
+
+    self._input_rules = rules
+
+    # Result when no rules apply. According to the spec policies default to 'is
+    # allowed', but our microdescriptor policy subclass might want to change
+    # this.
+
     self._is_allowed_default = True
-    self._summary_representation = None
-    self._can_exit_to_cache = {}
 
+  @lru_cache()
   def can_exit_to(self, address = None, port = None, strict = False):
     """
     Checks if this policy allows exiting to a given destination or not. If the
@@ -175,18 +175,13 @@ class ExitPolicy(object):
     :returns: **True** if exiting to this destination is allowed, **False** otherwise
     """
 
-    if not (address, port, strict) in self._can_exit_to_cache:
-      result = self._is_allowed_default
-
-      for rule in self._get_rules():
-        if rule.is_match(address, port, strict):
-          result = rule.is_accept
-          break
-
-      self._can_exit_to_cache[(address, port, strict)] = result
+    for rule in self._get_rules():
+      if rule.is_match(address, port, strict):
+        return rule.is_accept
 
-    return self._can_exit_to_cache[(address, port, strict)]
+    return self._is_allowed_default
 
+  @lru_cache()
   def is_exiting_allowed(self):
     """
     Provides **True** if the policy allows exiting whatsoever, **False**
@@ -194,6 +189,7 @@ class ExitPolicy(object):
     """
 
     rejected_ports = set()
+
     for rule in self._get_rules():
       if rule.is_accept:
         for port in xrange(rule.min_port, rule.max_port + 1):
@@ -207,6 +203,7 @@ class ExitPolicy(object):
 
     return self._is_allowed_default
 
+  @lru_cache()
   def summary(self):
     """
     Provides a short description of our policy chain, similar to a
@@ -227,131 +224,110 @@ class ExitPolicy(object):
     :returns: **str** with a concise summary for our policy
     """
 
-    if self._summary_representation is None:
-      # determines if we're a white-list or blacklist
-      is_whitelist = not self._is_allowed_default
+    # determines if we're a white-list or blacklist
+    is_whitelist = not self._is_allowed_default
 
-      for rule in self._get_rules():
-        if rule.is_address_wildcard() and rule.is_port_wildcard():
-          is_whitelist = not rule.is_accept
-          break
+    for rule in self._get_rules():
+      if rule.is_address_wildcard() and rule.is_port_wildcard():
+        is_whitelist = not rule.is_accept
+        break
 
-      # Iterates over the policies and adds the the ports we'll return (ie,
-      # allows if a white-list and rejects if a blacklist). Regardless of a
-      # port's allow/reject policy, all further entries with that port are
-      # ignored since policies respect the first matching policy.
+    # Iterates over the policies and adds the the ports we'll return (ie,
+    # allows if a white-list and rejects if a blacklist). Regardless of a
+    # port's allow/reject policy, all further entries with that port are
+    # ignored since policies respect the first matching policy.
 
-      display_ports, skip_ports = [], set()
+    display_ports, skip_ports = [], set()
 
-      for rule in self._get_rules():
-        if not rule.is_address_wildcard():
-          continue
-        elif rule.is_port_wildcard():
-          break
+    for rule in self._get_rules():
+      if not rule.is_address_wildcard():
+        continue
+      elif rule.is_port_wildcard():
+        break
 
-        for port in xrange(rule.min_port, rule.max_port + 1):
-          if port in skip_ports:
-            continue
+      for port in xrange(rule.min_port, rule.max_port + 1):
+        if port in skip_ports:
+          continue
 
-          # if accept + white-list or reject + blacklist then add
-          if rule.is_accept == is_whitelist:
-            display_ports.append(port)
+        # if accept + white-list or reject + blacklist then add
+        if rule.is_accept == is_whitelist:
+          display_ports.append(port)
 
-          # all further entries with this port should be ignored
-          skip_ports.add(port)
+        # all further entries with this port should be ignored
+        skip_ports.add(port)
 
-      # convert port list to a list of ranges (ie, ['1-3'] rather than [1, 2, 3])
-      if display_ports:
-        display_ranges, temp_range = [], []
-        display_ports.sort()
-        display_ports.append(None)  # ending item to include last range in loop
+    # convert port list to a list of ranges (ie, ['1-3'] rather than [1, 2, 3])
+    if display_ports:
+      display_ranges, temp_range = [], []
+      display_ports.sort()
+      display_ports.append(None)  # ending item to include last range in loop
 
-        for port in display_ports:
-          if not temp_range or temp_range[-1] + 1 == port:
-            temp_range.append(port)
+      for port in display_ports:
+        if not temp_range or temp_range[-1] + 1 == port:
+          temp_range.append(port)
+        else:
+          if len(temp_range) > 1:
+            display_ranges.append("%i-%i" % (temp_range[0], temp_range[-1]))
           else:
-            if len(temp_range) > 1:
-              display_ranges.append("%i-%i" % (temp_range[0], temp_range[-1]))
-            else:
-              display_ranges.append(str(temp_range[0]))
-
-            temp_range = [port]
-      else:
-        # everything for the inverse
-        is_whitelist = not is_whitelist
-        display_ranges = ["1-65535"]
+            display_ranges.append(str(temp_range[0]))
 
-      # constructs the summary string
-      label_prefix = "accept " if is_whitelist else "reject "
-
-      self._summary_representation = (label_prefix + ", ".join(display_ranges)).strip()
-
-    return self._summary_representation
-
-  def _set_default_allowed(self, is_allowed_default):
-    """
-    Generally policies end with either an 'reject \*:\*' or 'accept \*:\*'
-    policy, but if it doesn't then is_allowed_default will determine the
-    default response for our :meth:`~stem.exit_policy.ExitPolicy.can_exit_to`
-    method.
-
-    Our default, and tor's, is **True**.
+          temp_range = [port]
+    else:
+      # everything for the inverse
+      is_whitelist = not is_whitelist
+      display_ranges = ["1-65535"]
 
-    :param bool is_allowed_default:
-      :meth:`~stem.exit_policy.ExitPolicy.can_exit_to` default when no rules
-      apply
-    """
+    # constructs the summary string
+    label_prefix = "accept " if is_whitelist else "reject "
 
-    self._is_allowed_default = is_allowed_default
-    self._can_exit_to_cache = {}
+    return (label_prefix + ", ".join(display_ranges)).strip()
 
+  @lru_cache()
   def _get_rules(self):
-    if self._rules is None:
-      rules = []
-      is_all_accept, is_all_reject = True, True
-
-      for rule in self._input_rules:
-        if isinstance(rule, (bytes, unicode)):
-          rule = ExitPolicyRule(rule.strip())
+    rules = []
+    is_all_accept, is_all_reject = True, True
 
-        if rule.is_accept:
-          is_all_reject = False
-        else:
-          is_all_accept = False
+    for rule in self._input_rules:
+      if isinstance(rule, (bytes, unicode)):
+        rule = ExitPolicyRule(rule.strip())
 
-        rules.append(rule)
+      if rule.is_accept:
+        is_all_reject = False
+      else:
+        is_all_accept = False
 
-        if rule.is_address_wildcard() and rule.is_port_wildcard():
-          break  # this is a catch-all, no reason to include more
+      rules.append(rule)
 
-      # If we only have one kind of entry *and* end with a wildcard then
-      # we might as well use the simpler version. For instance...
-      #
-      #   reject *:80, reject *:443, reject *:*
-      #
-      # ... could also be represented as simply...
-      #
-      #   reject *:*
-      #
-      # This mostly comes up with reject-all policies because the
-      # 'reject private:*' appends an extra seven rules that have no
-      # effect.
+      if rule.is_address_wildcard() and rule.is_port_wildcard():
+        break  # this is a catch-all, no reason to include more
 
-      if rules and (rules[-1].is_address_wildcard() and rules[-1].is_port_wildcard()):
-        if is_all_accept:
-          rules = [ExitPolicyRule("accept *:*")]
-        elif is_all_reject:
-          rules = [ExitPolicyRule("reject *:*")]
+    # If we only have one kind of entry *and* end with a wildcard then
+    # we might as well use the simpler version. For instance...
+    #
+    #   reject *:80, reject *:443, reject *:*
+    #
+    # ... could also be represented as simply...
+    #
+    #   reject *:*
+    #
+    # This mostly comes up with reject-all policies because the
+    # 'reject private:*' appends an extra seven rules that have no
+    # effect.
 
-      self._rules = rules
-      self._input_rules = None
+    if rules and (rules[-1].is_address_wildcard() and rules[-1].is_port_wildcard()):
+      if is_all_accept:
+        rules = [ExitPolicyRule("accept *:*")]
+      elif is_all_reject:
+        rules = [ExitPolicyRule("reject *:*")]
 
-    return self._rules
+    self._input_rules = None
+    return rules
 
   def __iter__(self):
     for rule in self._get_rules():
       yield rule
 
+  @lru_cache()
   def __str__(self):
     return ', '.join([str(rule) for rule in self._get_rules()])
 
@@ -430,7 +406,7 @@ class MicroExitPolicy(ExitPolicy):
       rules.append(MicroExitPolicyRule(self.is_accept, int(min_port), int(max_port)))
 
     super(MicroExitPolicy, self).__init__(*rules)
-    self._set_default_allowed(not self.is_accept)
+    self._is_allowed_default = not self.is_accept
 
   def __str__(self):
     return self._policy
@@ -505,12 +481,6 @@ class ExitPolicyRule(object):
     self._apply_addrspec(rule, addrspec)
     self._apply_portspec(rule, portspec)
 
-    # The integer representation of our mask and masked address. These are
-    # lazily loaded and used by our is_match() method to compare ourselves to
-    # other addresses.
-
-    self._mask_bin = self._addr_bin = None
-
     # Lazily loaded string representation of our policy.
 
     self._str_representation = None
@@ -700,21 +670,17 @@ class ExitPolicyRule(object):
 
     return self._str_representation
 
+  @lru_cache()
   def _get_mask_bin(self):
     # provides an integer representation of our mask
 
-    if self._mask_bin is None:
-      self._mask_bin = int(stem.util.connection._get_address_binary(self.get_mask(False)), 2)
-
-    return self._mask_bin
+    return int(stem.util.connection._get_address_binary(self.get_mask(False)), 2)
 
+  @lru_cache()
   def _get_address_bin(self):
     # provides an integer representation of our address
 
-    if self._addr_bin is None:
-      self._addr_bin = int(stem.util.connection._get_address_binary(self.address), 2) & self._mask_bin
-
-    return self._addr_bin
+    return int(stem.util.connection._get_address_binary(self.address), 2) & self._get_mask_bin()
 
   def _apply_addrspec(self, rule, addrspec):
     # Parses the addrspec...
diff --git a/stem/util/lru_cache.py b/stem/util/lru_cache.py
index 5591862..a0b8f1b 100644
--- a/stem/util/lru_cache.py
+++ b/stem/util/lru_cache.py
@@ -20,6 +20,7 @@ from threading import RLock
 
 _CacheInfo = namedtuple("CacheInfo", ["hits", "misses", "maxsize", "currsize"])
 
+
 class _HashedSeq(list):
     __slots__ = 'hashvalue'
 
@@ -30,10 +31,11 @@ class _HashedSeq(list):
     def __hash__(self):
         return self.hashvalue
 
+
 def _make_key(args, kwds, typed,
-             kwd_mark = (object(),),
-             fasttypes = {int, str, frozenset, type(None)},
-             sorted=sorted, tuple=tuple, type=type, len=len):
+              kwd_mark = (object(),),
+              fasttypes = {int, str, frozenset, type(None)},
+              sorted=sorted, tuple=tuple, type=type, len=len):
     'Make a cache key from optionally typed positional and keyword arguments'
     key = args
     if kwds:
@@ -49,6 +51,7 @@ def _make_key(args, kwds, typed,
         return key[0]
     return _HashedSeq(key)
 
+
 def lru_cache(maxsize=100, typed=False):
     """Least-recently-used cache decorator.
 
@@ -146,7 +149,6 @@ def lru_cache(maxsize=100, typed=False):
                         # empty the oldest link and make it the new root
                         root = nonlocal_root[0] = oldroot[NEXT]
                         oldkey = root[KEY]
-                        oldvalue = root[RESULT]
                         root[KEY] = root[RESULT] = None
                         # now update the cache dictionary for the new links
                         del cache[oldkey]
diff --git a/test/unit/exit_policy/policy.py b/test/unit/exit_policy/policy.py
index 8659b68..161bae0 100644
--- a/test/unit/exit_policy/policy.py
+++ b/test/unit/exit_policy/policy.py
@@ -47,28 +47,6 @@ class TestExitPolicy(unittest.TestCase):
     policy = ExitPolicy(*"reject *:80, reject *:443, reject *:*".split(","))
     self.assertEquals(ExitPolicy("reject *:*"), policy)
 
-  def test_set_default_allowed(self):
-    policy = ExitPolicy('reject *:80', 'accept *:443')
-
-    # our default for being allowed defaults to True
-    self.assertFalse(policy.can_exit_to("75.119.206.243", 80))
-    self.assertTrue(policy.can_exit_to("75.119.206.243", 443))
-    self.assertTrue(policy.can_exit_to("75.119.206.243", 999))
-
-    policy._set_default_allowed(False)
-    self.assertFalse(policy.can_exit_to("75.119.206.243", 80))
-    self.assertTrue(policy.can_exit_to("75.119.206.243", 443))
-    self.assertFalse(policy.can_exit_to("75.119.206.243", 999))
-
-    # Our is_exiting_allowed() is also influcenced by this flag if we lack any
-    # 'accept' rules.
-
-    policy = ExitPolicy()
-    self.assertTrue(policy.is_exiting_allowed())
-
-    policy._set_default_allowed(False)
-    self.assertFalse(policy.is_exiting_allowed())
-
   def test_can_exit_to(self):
     # Basic sanity test for our can_exit_to() method. Most of the interesting
     # use cases (ip masks, wildcards, etc) are covered by the ExitPolicyRule





More information about the tor-commits mailing list