[Author Prev][Author Next][Thread Prev][Thread Next][Author Index][Thread Index]
[or-cvs] [bridgedb/master] Switch the storage backend to use sqlite3.
Author: Nick Mathewson <nickm@xxxxxxxxxxxxxx>
Date: Thu, 1 Oct 2009 15:01:57 -0400
Subject: Switch the storage backend to use sqlite3.
Commit: 59d34d897be6b5d02536945e621c4465c94279b2
Still unimplemented:
- cleaning out emailedbridges table
- rate-limiting by email addr
---
lib/bridgedb/Bridges.py | 109 +++++++---------------------------------
lib/bridgedb/Dist.py | 28 ++++++----
lib/bridgedb/Main.py | 23 ++------
lib/bridgedb/Storage.py | 129 ++++++++++++++++++++++++++++++++++++++++------
lib/bridgedb/Tests.py | 71 +++++++++++++++++++++++++-
lib/bridgedb/Time.py | 10 ++--
6 files changed, 227 insertions(+), 143 deletions(-)
diff --git a/lib/bridgedb/Bridges.py b/lib/bridgedb/Bridges.py
index 8fc5cb7..9d588da 100644
--- a/lib/bridgedb/Bridges.py
+++ b/lib/bridgedb/Bridges.py
@@ -16,6 +16,8 @@ import sha
import socket
import time
+import bridgedb.Storage
+
HEX_FP_LEN = 40
ID_LEN = 20
@@ -182,7 +184,6 @@ def parseDescFile(f, bridge_purpose='bridge'):
def parseStatusFile(f):
"""DOCDOC"""
- result = None
ID = None
for line in f:
line = line.strip()
@@ -347,64 +348,6 @@ class BridgeRing(BridgeHolder):
return self.bridgesByID.get(fp)
-class LogDB:
- """Wraps a database object and records all modifications to a
- human-readable logfile."""
- def __init__(self, kwd, db, logfile):
- if kwd:
- self._kwd = "%s: "%kwd
- else:
- self._kwd = ""
- self._db = db
- self._logfile = logfile
- def __delitem__(self, k):
- self._logfile.write("%s: del[%r]\n"%(self._kwd, k))
- del self._db[k]
- def __setitem__(self, k, v):
- self._logfile.write("%s: [%r] = [%r]\n"%(self._kwd, k, v))
- self._db[k] = v
- def setdefault(self, k, v):
- try:
- return self._db[k]
- except KeyError:
- self._logfile.write("%s[%r] = [%r]\n"%(self._kwd, k, v))
- self._db[k] = v
- return v
- def __len__(self):
- return len(self._db)
- def __getitem__(self, k):
- return self._db[k]
- def has_key(self, k):
- return self._db.has_key(k)
- def get(self, k, v=None):
- return self._db.get(k, v)
- def keys(self):
- return self._db.keys()
-
-
-class PrefixStore:
- """Wraps a database object and prefixes the keys in all requests with
- 'prefix'. This is used to multiplex several key->value mappings
- onto a single database."""
- def __init__(self, store, prefix):
- self._d = store
- self._p = prefix
- def __setitem__(self, k, v):
- self._d[self._p+k] = v
- def __delitem__(self, k):
- del self._d[self._p+k]
- def __getitem__(self, k):
- return self._d[self._p+k]
- def has_key(self, k):
- return self._d.has_key(self._p+k)
- def get(self, k, v=None):
- return self._d.get(self._p+k, v)
- def setdefault(self, k, v):
- return self._d.setdefault(self._p+k, v)
- def keys(self):
- n = len(self._p)
- return [ k[n:] for k in self._d.keys() if k.startswith(self._p) ]
-
class FixedBridgeSplitter(BridgeHolder):
"""A bridgeholder that splits bridges up based on an hmac and assigns
them to several sub-bridgeholders with equal probability.
@@ -446,31 +389,13 @@ class UnallocatedHolder(BridgeHolder):
def __len__(self):
return 0
-class BridgeTracker:
- """A stats tracker that records when we first saw and most recently
- saw each bridge.
- """
- def __init__(self, firstSeenStore, lastSeenStore):
- self.firstSeenStore = firstSeenStore
- self.lastSeenStore = lastSeenStore
-
- def insert(self, bridge):
- #XXXX is this really sane? Should we track minutes? hours?
- now = time.strftime("%Y-%m-%d %H:%M", time.gmtime())
- bridgeID = bridge.getID()
- # The last-seen time always gets updated
- self.lastSeenStore[bridgeID] = now
- # The first-seen time only gets updated if it wasn't already set.
- self.firstSeenStore.setdefault(bridgeID, now)
-
class BridgeSplitter(BridgeHolder):
"""A BridgeHolder that splits incoming bridges up based on an hmac,
and assigns them to sub-bridgeholders with different probabilities.
Bridge-to-bridgeholder associations are recorded in a store.
"""
- def __init__(self, key, store):
+ def __init__(self, key):
self.hmac = get_hmac_fn(key, hex=True)
- self.store = store
self.ringsByName = {}
self.totalP = 0
self.pValues = []
@@ -508,24 +433,26 @@ class BridgeSplitter(BridgeHolder):
def insert(self, bridge):
assert self.rings
+ db = bridgedb.Storage.getDB()
+
for s in self.statsHolders:
s.insert(bridge)
if bridge.running == False or bridge.running == None:
return
bridgeID = bridge.getID()
- ringname = self.store.get(bridgeID, "")
+
+ # Determine which ring to put this bridge in if we haven't seen it
+ # before.
+ pos = self.hmac(bridgeID)
+ n = int(pos[:8], 16) % self.totalP
+ pos = bisect.bisect_right(self.pValues, n) - 1
+ assert 0 <= pos < len(self.rings)
+ ringname = self.rings[pos]
+
+ ringname = db.insertBridgeAndGetRing(bridge, ringname, time.time())
+ db.commit()
+
ring = self.ringsByName.get(ringname)
- if ring is not None:
- ring.insert(bridge)
- else:
- pos = self.hmac(bridgeID)
- n = int(pos[:8], 16) % self.totalP
- pos = bisect.bisect_right(self.pValues, n) - 1
- assert 0 <= pos < len(self.rings)
- ringname = self.rings[pos]
- ring = self.ringsByName.get(ringname)
- if ring.assignmentsArePersistent():
- self.store[bridgeID] = ringname
- ring.insert(bridge)
+ ring.insert(bridge)
diff --git a/lib/bridgedb/Dist.py b/lib/bridgedb/Dist.py
index f00d918..701e82b 100644
--- a/lib/bridgedb/Dist.py
+++ b/lib/bridgedb/Dist.py
@@ -7,9 +7,11 @@ This module has functions to decide which bridges to hand out to whom.
"""
import bridgedb.Bridges
+import bridgedb.Storage
import logging
import re
+import time
def uniformMap(ip):
"""Map an IP to an arbitrary 'area' string, such that any two /24 addresses
@@ -205,7 +207,7 @@ class EmailBasedDistributor(bridgedb.Bridges.BridgeHolder):
## store -- a database object to remember what we've given to whom.
## domainmap -- a map from lowercase domains that we support mail from
## to their canonical forms.
- def __init__(self, key, store, domainmap, domainrules,
+ def __init__(self, key, domainmap, domainrules,
answerParameters=None):
key1 = bridgedb.Bridges.get_hmac(key, "Map-Addresses-To-Ring")
self.emailHmac = bridgedb.Bridges.get_hmac_fn(key1, hex=False)
@@ -214,7 +216,6 @@ class EmailBasedDistributor(bridgedb.Bridges.BridgeHolder):
self.ring = bridgedb.Bridges.BridgeRing(key2, answerParameters)
self.ring.name = "email ring"
# XXXX clear the store when the period rolls over!
- self.store = store
self.domainmap = domainmap
self.domainrules = domainrules
@@ -232,29 +233,34 @@ class EmailBasedDistributor(bridgedb.Bridges.BridgeHolder):
be any string, so long as it changes with every period.
N -- the number of bridges to try to give back.
"""
- try:
+ try:
emailaddress = normalizeEmail(emailaddress, self.domainmap,
self.domainrules)
except BadEmail:
return [] #XXXX log the exception
if emailaddress is None:
return [] #XXXX raise an exception.
- if self.store.has_key(emailaddress):
- result = []
- ids_str = self.store[emailaddress]
+
+ db = bridgedb.Storage.getDB()
+
+ ids = db.getEmailedBridges(emailaddress)
+
+ if ids:
logging.info("We've seen %r before. Sending the same bridges"
" as last time", emailaddress)
- for ident in bridgedb.Bridges.chopString(ids_str,
- bridgedb.Bridges.ID_LEN):
- b = self.ring.getBridgeByID(ident)
+ result = []
+ for fp in ids:
+ b = self.ring.getBridgeByID(bridgedb.Bridges.fromHex(fp))
if b != None:
result.append(b)
return result
pos = self.emailHmac("<%s>%s" % (epoch, emailaddress))
result = self.ring.getBridges(pos, N)
- memo = "".join(b.getID() for b in result)
- self.store[emailaddress] = memo
+
+ db.addEmailedBridges(emailaddress, time.time(),
+ [b.fingerprint for b in result])
+ db.commit()
return result
def __len__(self):
diff --git a/lib/bridgedb/Main.py b/lib/bridgedb/Main.py
index f7339cf..79f37ad 100644
--- a/lib/bridgedb/Main.py
+++ b/lib/bridgedb/Main.py
@@ -6,7 +6,6 @@
This module sets up a bridgedb and starts the servers running.
"""
-import anydbm
import os
import signal
import sys
@@ -18,6 +17,7 @@ import bridgedb.Bridges as Bridges
import bridgedb.Dist as Dist
import bridgedb.Time as Time
import bridgedb.Server as Server
+import bridgedb.Storage
class Conf:
"""A configuration object. Holds unvalidated attributes.
@@ -220,11 +220,9 @@ def startup(cfg):
key = getKey(cfg.MASTER_KEY_FILE)
# Initialize our DB file.
- dblogfile = None
- baseStore = store = anydbm.open(cfg.DB_FILE, "c", 0600)
- if cfg.DB_LOG_FILE:
- dblogfile = open(cfg.DB_LOG_FILE, "a+", 0)
- store = Bridges.LogDB(None, store, dblogfile)
+ db = bridgedb.Storage.Database(cfg.DB_FILE+".sqlite",
+ cfg.DB_FILE)
+ bridgedb.Storage.setGlobalDB(db)
# Get a proxy list.
proxyList = ProxyCategory()
@@ -232,8 +230,7 @@ def startup(cfg):
# Create a BridgeSplitter to assign the bridges to the different
# distributors.
- splitter = Bridges.BridgeSplitter(Bridges.get_hmac(key, "Splitter-Key"),
- Bridges.PrefixStore(store, "sp|"))
+ splitter = Bridges.BridgeSplitter(Bridges.get_hmac(key, "Splitter-Key"))
# Create ring parameters.
forcePorts = getattr(cfg, "FORCE_PORTS")
@@ -264,7 +261,6 @@ def startup(cfg):
cfg.EMAIL_DOMAIN_MAP[d] = d
emailDistributor = Dist.EmailBasedDistributor(
Bridges.get_hmac(key, "Email-Dist-Key"),
- Bridges.PrefixStore(store, "em|"),
cfg.EMAIL_DOMAIN_MAP.copy(),
cfg.EMAIL_DOMAIN_RULES.copy(),
answerParameters=ringParams)
@@ -277,11 +273,6 @@ def startup(cfg):
"unallocated",
cfg.RESERVED_SHARE)
- # Add a tracker to tell us how often we've seen various bridges.
- stats = Bridges.BridgeTracker(Bridges.PrefixStore(store, "fs|"),
- Bridges.PrefixStore(store, "ls|"))
- splitter.addTracker(stats)
-
# Make the parse-bridges function get re-called on SIGHUP.
def reload():
logging.info("Caught SIGHUP")
@@ -315,9 +306,7 @@ def startup(cfg):
logging.info("Starting reactors.")
Server.runServers()
finally:
- baseStore.close()
- if dblogfile is not None:
- dblogfile.close()
+ db.close()
if cfg.PIDFILE:
os.unlink(cfg.PIDFILE)
diff --git a/lib/bridgedb/Storage.py b/lib/bridgedb/Storage.py
index 07ec8ba..d3e0e22 100644
--- a/lib/bridgedb/Storage.py
+++ b/lib/bridgedb/Storage.py
@@ -6,13 +6,19 @@ import os
import logging
import bridgedb.Bridges
import binascii
+import sqlite3
+import time
toHex = binascii.b2a_hex
fromHex = binascii.a2b_hex
+HEX_ID_LEN = 40
def _escapeValue(v):
return "'%s'" % v.replace("'", "''")
+def timeToStr(t):
+ return time.strftime("%Y-%m-%d %H:%M", time.gmtime(t))
+
class SqliteDict:
"""
A SqliteDict wraps a SQLite table and makes it look like a
@@ -61,7 +67,7 @@ class SqliteDict:
raise KeyError(k)
else:
return val[0]
- def has_key(self):
+ def has_key(self, k):
self._cursor.execute(self._getStmt, (k,))
return self._cursor.rowcount != 0
def get(self, k, v=None):
@@ -86,6 +92,7 @@ class SqliteDict:
def rollback(self):
self._conn.rollback()
+
# The old DB system was just a key->value mapping DB, with special key
# prefixes to indicate which database they fell into.
#
@@ -108,8 +115,9 @@ SCHEMA1_SCRIPT = """
CREATE TABLE Bridges (
id INTEGER PRIMARY KEY NOT NULL,
- hex_key, -- index this.
+ hex_key,
address,
+ or_port,
distributor,
first_seen,
last_seen
@@ -126,23 +134,101 @@ SCHEMA1_SCRIPT = """
CREATE INDEX EmailedBridgesEmailIndex ON EmailedBridges ( email );
"""
-def openDatabase(sqlite_file, db_file):
+
+class Database:
+ def __init__(self, sqlite_fname, db_fname=None):
+ if db_fname is None:
+ self._conn = openDatabase(sqlite_fname)
+ else:
+ self._conn = openOrConvertDatabase(sqlite_fname, db_fname)
+ self._cur = self._conn.cursor()
+
+ def commit(self):
+ self._conn.commit()
+
+ def close(self):
+ self._cur.close()
+ self._conn.close()
+
+ def insertBridgeAndGetRing(self, bridge, setRing, seenAt):
+ '''updates info about bridge, setting ring to setRing if none was set.
+ Returns the bridge's ring.
+ '''
+ cur = self._cur
+
+ t = timeToStr(seenAt)
+ h = bridge.fingerprint
+ assert len(h) == HEX_ID_LEN
+
+ cur.execute("SELECT id, distributor "
+ "FROM Bridges WHERE hex_key = ?", (h,))
+ v = cur.fetchone()
+ if v is not None:
+ idx, ring = v
+ # Update last_seen and address.
+ cur.execute("UPDATE Bridges SET address = ?, or_port = ?, "
+ "last_seen = ? WHERE id = ?",
+ (bridge.ip, bridge.orport, timeToStr(seenAt), idx))
+ return ring
+ else:
+ # Insert it.
+ cur.execute("INSERT INTO Bridges (hex_key, address, or_port, "
+ "distributor, first_seen, last_seen) "
+ "VALUES (?, ?, ?, ?, ?, ?)",
+ (h, bridge.ip, bridge.orport, setRing, t, t))
+ return setRing
+
+ def cleanEmailedBridges(self, expireBefore):
+ cur = self._cur
+ t = timeToStr(expireBefore)
+
+ cur.execute("DELETE FROM Bridges WHERE when_mailed < ?", t);
+
+ def getEmailedBridges(self, addr):
+ cur = self._cur
+ cur.execute("SELECT hex_key FROM EmailedBridges, Bridges WHERE "
+ "email = ? AND Bridges.id = EmailedBridges.id", (addr,))
+ return [ hk for hk, in cur.fetchall() ]
+
+ def addEmailedBridges(self, addr, whenMailed, bridgeKeys):
+ cur = self._cur
+ t = timeToStr(whenMailed)
+ for k in bridgeKeys:
+ assert(len(k))==HEX_ID_LEN
+ cur.executemany("INSERT INTO EmailedBridges (email,when_mailed,id) "
+ "SELECT ?,?,id FROM Bridges WHERE hex_key = ?",
+ [(addr,t,k) for k in bridgeKeys])
+
+def openDatabase(sqlite_file):
+ conn = sqlite3.Connection(sqlite_file)
+ cur = conn.cursor()
+ try:
+ try:
+ cur.execute("SELECT value FROM Config WHERE key = 'schema-version'")
+ val, = cur.fetchone()
+ if val != 1:
+ logging.warn("Unknown schema version %s in database.", val)
+ except sqlite3.OperationalError:
+ logging.warn("No Config table found in DB; creating tables")
+ cur.executescript(SCHEMA1_SCRIPT)
+ conn.commit()
+ finally:
+ cur.close()
+ return conn
+
+
+def openOrConvertDatabase(sqlite_file, db_file):
"""Open a sqlite database, converting it from a db file if needed."""
if os.path.exists(sqlite_file):
- conn = sqlite3.Connection(sqlite_file)
- cur = conn.cursor()
- cur.execute("SELECT value FROM Config WHERE key = 'schema-version'")
- val, = cur.fetchone()
- if val != 1:
- logging.warn("Unknown schema version %s in database.", val)
- cur.close()
- return conn
+ return openDatabase(sqlite_file)
conn = sqlite3.Connection(sqlite_file)
cur = conn.cursor()
cur.executescript(SCHEMA1_SCRIPT)
conn.commit()
+ import anydbm
+
try:
db = anydbm.open(db_file, 'r')
except anydbm.error:
@@ -154,22 +240,22 @@ def openDatabase(sqlite_file, db_file):
if k.startswith("sp|"):
assert len(k) == 23
cur.execute("INSERT INTO Bridges ( hex_key, distributor ) "
- "VALUES (%s %s)", (toHex(k[3:]),v))
+ "VALUES (?, ?)", (toHex(k[3:]),v))
for k in db.keys():
v = db[k]
if k.startswith("fs|"):
assert len(k) == 23
- cur.execute("UPDATE Bridges SET first_seen = %s "
- "WHERE hex_key = %s", (v, k[3:]))
+ cur.execute("UPDATE Bridges SET first_seen = ? "
+ "WHERE hex_key = ?", (v, toHex(k[3:])))
elif k.startswith("ls|"):
assert len(k) == 23
- cur.execute("UPDATE Bridges SET last_seen = %s "
- "WHERE hex_key = %s", (v, toHex(k[3:])))
+ cur.execute("UPDATE Bridges SET last_seen = ? "
+ "WHERE hex_key = ?", (v, toHex(k[3:])))
elif k.startswith("em|"):
keys = list(toHex(i) for i in
bridgedb.Bridges.chopString(v, bridgedb.Bridges.ID_LEN))
cur.executemany("INSERT INTO EmailedBridges ( email, id ) "
- "SELECT %s, id FROM Bridges WHERE hex_key = %s",
+ "SELECT ?, id FROM Bridges WHERE hex_key = ?",
[(k[3:],i) for i in keys])
elif k.startswith("sp|"):
pass
@@ -183,3 +269,12 @@ def openDatabase(sqlite_file, db_file):
conn.commit()
return conn
+_THE_DB = None
+
+def setGlobalDB(db):
+ global _THE_DB
+ _THE_DB = db
+
+def getDB(db):
+ return _THE_DB
+
diff --git a/lib/bridgedb/Tests.py b/lib/bridgedb/Tests.py
index 865c91e..826b3c4 100644
--- a/lib/bridgedb/Tests.py
+++ b/lib/bridgedb/Tests.py
@@ -9,6 +9,7 @@ import sqlite3
import tempfile
import unittest
import warnings
+import time
import bridgedb.Bridges
import bridgedb.Main
@@ -82,7 +83,7 @@ class IPBridgeDistTests(unittest.TestCase):
self.assertEquals(len(fps), 5)
self.assertTrue(count >= 1)
-class StorageTests(unittest.TestCase):
+class DictStorageTests(unittest.TestCase):
def setUp(self):
self.fd, self.fname = tempfile.mkstemp()
self.conn = sqlite3.Connection(self.fname)
@@ -128,11 +129,77 @@ class StorageTests(unittest.TestCase):
self.assertEquals(d.setdefault("yo","bye"), "bye")
self.assertEquals(d['yo'], "bye")
+class SQLStorageTests(unittest.TestCase):
+ def setUp(self):
+ self.fd, self.fname = tempfile.mkstemp()
+ self.db = bridgedb.Storage.Database(self.fname)
+ self.cur = self.db._conn.cursor()
+
+ def tearDown(self):
+ self.db.close()
+ os.close(self.fd)
+ os.unlink(self.fname)
+
+ def testBridgeStorage(self):
+ db = self.db
+ B = bridgedb.Bridges.Bridge
+ t = time.time()
+ cur = self.cur
+
+ k1 = "aaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbb"
+ k2 = "abababababababababababababababababababab"
+ k3 = "cccccccccccccccccccccccccccccccccccccccc"
+ b1 = B("serv1", "1.2.3.4", 999, fingerprint=k1)
+ b1_v2 = B("serv1", "1.2.3.5", 9099, fingerprint=k1)
+ b2 = B("serv2", "2.3.4.5", 9990, fingerprint=k2)
+ b3 = B("serv3", "2.3.4.6", 9008, fingerprint=k3)
+
+ r = db.insertBridgeAndGetRing(b1, "ring1", t)
+ self.assertEquals(r, "ring1")
+ r = db.insertBridgeAndGetRing(b1, "ring10", t+500)
+ self.assertEquals(r, "ring1")
+
+ cur.execute("SELECT distributor, address, or_port, first_seen, "
+ "last_seen FROM Bridges WHERE hex_key = ?", (k1,))
+ v = cur.fetchone()
+ self.assertEquals(v,
+ ("ring1", "1.2.3.4", 999,
+ bridgedb.Storage.timeToStr(t),
+ bridgedb.Storage.timeToStr(t+500)))
+
+ r = db.insertBridgeAndGetRing(b1_v2, "ring99", t+800)
+ self.assertEquals(r, "ring1")
+ cur.execute("SELECT distributor, address, or_port, first_seen, "
+ "last_seen FROM Bridges WHERE hex_key = ?", (k1,))
+ v = cur.fetchone()
+ self.assertEquals(v,
+ ("ring1", "1.2.3.5", 9099,
+ bridgedb.Storage.timeToStr(t),
+ bridgedb.Storage.timeToStr(t+800)))
+
+ db.insertBridgeAndGetRing(b2, "ring2", t)
+ db.insertBridgeAndGetRing(b3, "ring3", t)
+
+ cur.execute("SELECT COUNT(distributor) FROM Bridges")
+ v = cur.fetchone()
+ self.assertEquals(v, (3,))
+
+ r = db.getEmailedBridges("abc@xxxxxxxxxxx")
+ self.assertEquals(r, [])
+ db.addEmailedBridges("abc@xxxxxxxxxxx", t, [k1,k2])
+ db.addEmailedBridges("def@xxxxxxxxxxx", t+1000, [k2,k3])
+ r = db.getEmailedBridges("abc@xxxxxxxxxxx")
+ self.assertEquals(sorted(r), sorted([k1,k2]))
+ r = db.getEmailedBridges("def@xxxxxxxxxxx")
+ self.assertEquals(sorted(r), sorted([k2,k3]))
+ r = db.getEmailedBridges("ghi@xxxxxxxxxxx")
+ self.assertEquals(r, [])
+
def testSuite():
suite = unittest.TestSuite()
loader = unittest.TestLoader()
- for klass in [ IPBridgeDistTests, StorageTests ]:
+ for klass in [ IPBridgeDistTests, DictStorageTests, SQLStorageTests ]:
suite.addTest(loader.loadTestsFromTestCase(klass))
for module in [ bridgedb.Bridges,
diff --git a/lib/bridgedb/Time.py b/lib/bridgedb/Time.py
index ffc73e0..6c263c7 100644
--- a/lib/bridgedb/Time.py
+++ b/lib/bridgedb/Time.py
@@ -35,7 +35,7 @@ class IntervalSchedule:
self.itype = it
self.count = count
- def _intervalStart(self, when):
+ def intervalStart(self, when):
"""Return the time (as an int) of the start of the interval containing
'when'."""
if self.itype == 'month':
@@ -72,11 +72,11 @@ class IntervalSchedule:
month = n%12 + 1
return "%04d-%02d" % (n // 12, month)
elif self.itype == 'day':
- when = self._intervalStart(when) + 7200 #slop
+ when = self.intervalStart(when) + 7200 #slop
tm = time.gmtime(when)
return "%04d-%02d-%02d" % (tm.tm_year, tm.tm_mon, tm.tm_mday)
elif self.itype == 'hour':
- when = self._intervalStart(when) + 120 #slop
+ when = self.intervalStart(when) + 120 #slop
tm = time.gmtime(when)
return "%04d-%02d-%02d %02d" % (tm.tm_year, tm.tm_mon, tm.tm_mday,
tm.tm_hour)
@@ -93,7 +93,7 @@ class IntervalSchedule:
tm = (n // 12, month+self.count, 1, 0,0,0)
return calendar.timegm(tm)
elif self.itype == 'day':
- return self._intervalStart(when) + 86400 * self.count
+ return self.intervalStart(when) + 86400 * self.count
elif self.itype == 'hour':
- return self._intervalStart(when) + 3600 * self.count
+ return self.intervalStart(when) + 3600 * self.count
--
1.5.6.5