[tor-commits] [nyx/master] Implement relay cache

atagar at torproject.org atagar at torproject.org
Sun Sep 3 01:24:13 UTC 2017


commit 8ea32382cc467202707c0161d05b271846978669
Author: Damian Johnson <atagar at torproject.org>
Date:   Fri Sep 1 11:39:06 2017 -0700

    Implement relay cache
    
    There, that's more like what I was after. Adding methods to read and write
    relay data to the cache along with tests.
---
 nyx/__init__.py | 72 +++++++++++++++++++++++++++++++++++++++++++++--
 test/cache.py   | 87 ++++++++++++++++++++++++++++++++++++++++++++++++---------
 2 files changed, 144 insertions(+), 15 deletions(-)

diff --git a/nyx/__init__.py b/nyx/__init__.py
index 3c3c939..39c1e5d 100644
--- a/nyx/__init__.py
+++ b/nyx/__init__.py
@@ -31,6 +31,7 @@ Tor curses monitoring application.
     +- halt - stops daemon panels
 """
 
+import contextlib
 import distutils.spawn
 import os
 import sqlite3
@@ -42,8 +43,10 @@ import stem
 import stem.connection
 import stem.control
 import stem.util.conf
+import stem.util.connection
 import stem.util.log
 import stem.util.system
+import stem.util.tor_tools
 
 __version__ = '1.4.6-dev'
 __release_date__ = 'April 28, 2011'
@@ -339,7 +342,8 @@ def join(entries, joiner = ' ', size = None):
 
 class Cache(object):
   """
-  Cache for frequently used information.
+  Cache for frequently needed information. This persists to disk if we can, and
+  otherwise is an in-memory cache.
   """
 
   def __init__(self):
@@ -374,7 +378,43 @@ class Cache(object):
       for cmd in SCHEMA:
         self._conn.execute(cmd)
 
-  def query(self, query, *param):
+  @contextlib.contextmanager
+  def write(self):
+    """
+    Provides a context in which we can modify the cache.
+
+    :returns: :class:`~nyx.CacheWriter` that can modify the cache
+    """
+
+    with self._conn:
+      yield CacheWriter(self)
+
+  def relay_nickname(self, fingerprint):
+    """
+    Provides the nickname associated with the given relay.
+
+    :param str fingerprint: relay to look up
+
+    :returns: **str** with the nickname ("Unnamed" if unset), and **None** if
+      no such relay exists
+    """
+
+    result = self._query('SELECT nickname FROM relays WHERE fingerprint=?', fingerprint).fetchone()
+    return result[0] if result else None
+
+  def relay_address(self, fingerprint):
+    """
+    Provides the (address, port) tuple where a relay is running.
+
+    :param str fingerprint: fingerprint to be checked
+
+    :returns: **tuple** with a **str** address and **int** port
+    """
+
+    result = self._query('SELECT address, or_port FROM relays WHERE fingerprint=?', fingerprint).fetchone()
+    return result if result else None  # TODO: does this raise if fingerprint doesn't exist?
+
+  def _query(self, query, *param):
     """
     Performs a query on our cache.
     """
@@ -383,6 +423,34 @@ class Cache(object):
       return self._conn.execute(query, param)
 
 
+class CacheWriter(object):
+  def __init__(self, cache):
+    self._cache = cache
+
+  def record_relay(self, fingerprint, address, or_port, nickname):
+    """
+    Records relay metadata.
+
+    :param str fingerprint: relay fingerprint
+    :param str address: ipv4 or ipv6 address
+    :param int or_port: ORPort of the relay
+    :param str nickname: relay nickname
+
+    :raises: **ValueError** if provided data is malformed
+    """
+
+    if not stem.util.tor_tools.is_valid_fingerprint(fingerprint):
+      raise ValueError("'%s' isn't a valid fingerprint" % fingerprint)
+    elif not stem.util.tor_tools.is_valid_nickname(nickname):
+      raise ValueError("'%s' isn't a valid nickname" % nickname)
+    elif not stem.util.connection.is_valid_ipv4_address(address) and not stem.util.connection.is_valid_ipv6_address(address):
+      raise ValueError("'%s' isn't a valid address" % address)
+    elif not stem.util.connection.is_valid_port(or_port):
+      raise ValueError("'%s' isn't a valid port" % or_port)
+
+    self._cache._query('INSERT OR REPLACE INTO relays(fingerprint, address, or_port, nickname) VALUES (?,?,?,?)', fingerprint, address, or_port, nickname)
+
+
 class Interface(object):
   """
   Overall state of the nyx interface.
diff --git a/test/cache.py b/test/cache.py
index 91e9ab8..dd76c00 100644
--- a/test/cache.py
+++ b/test/cache.py
@@ -2,6 +2,7 @@
 Unit tests for nyx.cache.
 """
 
+import re
 import tempfile
 import unittest
 
@@ -9,11 +10,6 @@ import nyx
 
 from mock import Mock, patch
 
-FINGERPRINT = '3EA8E960F6B94CE30062AA8EF02894C00F8D1E66'
-ADDRESS = '208.113.165.162'
-PORT = 1443
-NICKNAME = 'caersidi'
-
 
 class TestCache(unittest.TestCase):
   def setUp(self):
@@ -26,10 +22,12 @@ class TestCache(unittest.TestCase):
     """
 
     cache = nyx.cache()
+    self.assertEqual((0, 'main', ''), cache._query('PRAGMA database_list').fetchone())
+
+    with cache.write() as writer:
+      writer.record_relay('3EA8E960F6B94CE30062AA8EF02894C00F8D1E66', '208.113.165.162', 1443, 'caersidi')
 
-    self.assertEqual((0, 'main', ''), cache.query('PRAGMA database_list').fetchone())
-    cache.query('INSERT INTO relays(fingerprint, address, or_port, nickname) VALUES (?,?,?,?)', FINGERPRINT, ADDRESS, PORT, NICKNAME)
-    self.assertEqual(NICKNAME, cache.query('SELECT nickname FROM relays WHERE fingerprint=?', FINGERPRINT).fetchone()[0])
+    self.assertEqual('caersidi', cache.relay_nickname('3EA8E960F6B94CE30062AA8EF02894C00F8D1E66'))
 
   def test_file_cache(self):
     """
@@ -39,12 +37,75 @@ class TestCache(unittest.TestCase):
     with tempfile.NamedTemporaryFile(suffix = '.sqlite') as tmp:
       with patch('nyx.data_directory', Mock(return_value = tmp.name)):
         cache = nyx.cache()
+        self.assertEqual((0, 'main', tmp.name), cache._query('PRAGMA database_list').fetchone())
 
-        self.assertEqual((0, 'main', tmp.name), cache.query('PRAGMA database_list').fetchone())
-        cache.query('INSERT INTO relays(fingerprint, address, or_port, nickname) VALUES (?,?,?,?)', FINGERPRINT, ADDRESS, PORT, NICKNAME)
-        cache._conn.commit()
+        with cache.write() as writer:
+          writer.record_relay('3EA8E960F6B94CE30062AA8EF02894C00F8D1E66', '208.113.165.162', 1443, 'caersidi')
 
         nyx.CACHE = None
-
         cache = nyx.cache()
-        self.assertEqual(NICKNAME, cache.query('SELECT nickname FROM relays WHERE fingerprint=?', FINGERPRINT).fetchone()[0])
+        self.assertEqual('caersidi', cache.relay_nickname('3EA8E960F6B94CE30062AA8EF02894C00F8D1E66'))
+
+  @patch('nyx.data_directory', Mock(return_value = None))
+  def test_relay_nickname(self):
+    """
+    Basic checks for registering and fetching nicknames.
+    """
+
+    cache = nyx.cache()
+
+    with cache.write() as writer:
+      writer.record_relay('3EA8E960F6B94CE30062AA8EF02894C00F8D1E66', '208.113.165.162', 1443, 'caersidi')
+      writer.record_relay('9695DFC35FFEB861329B9F1AB04C46397020CE31', '128.31.0.34', 9101, 'moria1')
+      writer.record_relay('74A910646BCEEFBCD2E874FC1DC997430F968145', '199.254.238.53', 443, 'longclaw')
+
+    self.assertEqual('moria1', cache.relay_nickname('9695DFC35FFEB861329B9F1AB04C46397020CE31'))
+    self.assertEqual('longclaw', cache.relay_nickname('74A910646BCEEFBCD2E874FC1DC997430F968145'))
+    self.assertEqual('caersidi', cache.relay_nickname('3EA8E960F6B94CE30062AA8EF02894C00F8D1E66'))
+
+    self.assertEqual(None, cache.relay_nickname('66E1D8F00C49820FE8AA26003EC49B6F069E8AE3'))
+
+  @patch('nyx.data_directory', Mock(return_value = None))
+  def test_relay_address(self):
+    """
+    Basic checks for registering and fetching nicknames.
+    """
+
+    cache = nyx.cache()
+
+    with cache.write() as writer:
+      writer.record_relay('3EA8E960F6B94CE30062AA8EF02894C00F8D1E66', '208.113.165.162', 1443, 'caersidi')
+      writer.record_relay('9695DFC35FFEB861329B9F1AB04C46397020CE31', '128.31.0.34', 9101, 'moria1')
+      writer.record_relay('74A910646BCEEFBCD2E874FC1DC997430F968145', '199.254.238.53', 443, 'longclaw')
+
+    self.assertEqual(('128.31.0.34', 9101), cache.relay_address('9695DFC35FFEB861329B9F1AB04C46397020CE31'))
+    self.assertEqual(('199.254.238.53', 443), cache.relay_address('74A910646BCEEFBCD2E874FC1DC997430F968145'))
+    self.assertEqual(('208.113.165.162', 1443), cache.relay_address('3EA8E960F6B94CE30062AA8EF02894C00F8D1E66'))
+
+    self.assertEqual(None, cache.relay_address('66E1D8F00C49820FE8AA26003EC49B6F069E8AE3'))
+
+  @patch('nyx.data_directory', Mock(return_value = None))
+  def test_record_relay_when_updating(self):
+    cache = nyx.cache()
+
+    with cache.write() as writer:
+      writer.record_relay('3EA8E960F6B94CE30062AA8EF02894C00F8D1E66', '208.113.165.162', 1443, 'caersidi')
+
+    self.assertEqual('caersidi', cache.relay_nickname('3EA8E960F6B94CE30062AA8EF02894C00F8D1E66'))
+
+    with cache.write() as writer:
+      writer.record_relay('3EA8E960F6B94CE30062AA8EF02894C00F8D1E66', '128.31.0.34', 9101, 'moria1')
+
+    self.assertEqual('moria1', cache.relay_nickname('3EA8E960F6B94CE30062AA8EF02894C00F8D1E66'))
+
+  @patch('nyx.data_directory', Mock(return_value = None))
+  def test_record_relay_when_invalid(self):
+    """
+    Provide malformed information to record_relay.
+    """
+
+    with nyx.cache().write() as writer:
+      self.assertRaisesRegexp(ValueError, re.escape("'blarg' isn't a valid fingerprint"), writer.record_relay, 'blarg', '208.113.165.162', 1443, 'caersidi')
+      self.assertRaisesRegexp(ValueError, re.escape("'blarg' isn't a valid address"), writer.record_relay, '3EA8E960F6B94CE30062AA8EF02894C00F8D1E66', 'blarg', 1443, 'caersidi')
+      self.assertRaisesRegexp(ValueError, re.escape("'blarg' isn't a valid port"), writer.record_relay, '3EA8E960F6B94CE30062AA8EF02894C00F8D1E66', '208.113.165.162', 'blarg', 'caersidi')
+      self.assertRaisesRegexp(ValueError, re.escape("'~blarg' isn't a valid nickname"), writer.record_relay, '3EA8E960F6B94CE30062AA8EF02894C00F8D1E66', '208.113.165.162', 1443, '~blarg')





More information about the tor-commits mailing list