diff options
author | bwarsaw <> | 2004-02-10 22:45:54 +0000 |
---|---|---|
committer | bwarsaw <> | 2004-02-10 22:45:54 +0000 |
commit | 5382db763a9404c83026aec486eb10ad2fbbbd7e (patch) | |
tree | 700e8a13be5f8d6f853adddcfc53034af6c4c985 /Mailman/Pending.py | |
parent | ea4a45b5cb19fbe5f026e47a837bc65e2422eb89 (diff) | |
download | mailman2-5382db763a9404c83026aec486eb10ad2fbbbd7e.tar.gz mailman2-5382db763a9404c83026aec486eb10ad2fbbbd7e.tar.xz mailman2-5382db763a9404c83026aec486eb10ad2fbbbd7e.zip |
Simplification and re-implementation as a per-list (and thus MailList mixin
class), pending database.
Diffstat (limited to '')
-rw-r--r-- | Mailman/Pending.py | 363 |
1 files changed, 132 insertions, 231 deletions
diff --git a/Mailman/Pending.py b/Mailman/Pending.py index 598863f9..6b6c2d14 100644 --- a/Mailman/Pending.py +++ b/Mailman/Pending.py @@ -1,4 +1,4 @@ -# Copyright (C) 1998-2003 by the Free Software Foundation, Inc. +# Copyright (C) 1998-2004 by the Free Software Foundation, Inc. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -14,28 +14,16 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. -""" Track pending confirmation of subscriptions. - -new(stuff...) places an item's data in the db, returning its cookie. - -confirmed(cookie) returns a tuple for the data, removing the item -from the db. It returns None if the cookie is not registered. -""" +"""Track pending actions which require confirmation.""" import os -import time import sha -import marshal -import cPickle -import random +import time import errno +import random +import cPickle from Mailman import mm_cfg -from Mailman import LockFile - -DBFILE = os.path.join(mm_cfg.DATA_DIR, 'pending.db') -PCKFILE = os.path.join(mm_cfg.DATA_DIR, 'pending.pck') -LOCKFILE = os.path.join(mm_cfg.LOCK_DIR, 'pending.lock') # Types of pending records SUBSCRIPTION = 'S' @@ -43,11 +31,12 @@ UNSUBSCRIPTION = 'U' CHANGE_OF_ADDRESS = 'C' HELD_MESSAGE = 'H' RE_ENABLE = 'E' +PROBE_BOUNCE = 'P' -_ALLKEYS = [(x,) for x in (SUBSCRIPTION, UNSUBSCRIPTION, - CHANGE_OF_ADDRESS, HELD_MESSAGE, - RE_ENABLE, - )] +_ALLKEYS = (SUBSCRIPTION, UNSUBSCRIPTION, + CHANGE_OF_ADDRESS, HELD_MESSAGE, + RE_ENABLE, PROBE_BOUNCE, + ) try: True, False @@ -56,228 +45,140 @@ except NameError: False = 0 - -def new(*content): - """Create a new entry in the pending database, returning cookie for it.""" - # It's a programming error if this assertion fails! We do it this way so - # the assert test won't fail if the sequence is empty. - assert content[:1] in _ALLKEYS - # Get a lock handle now, but only lock inside the loop. - lock = LockFile.LockFile( - LOCKFILE, withlogging=mm_cfg.PENDINGDB_LOCK_DEBUGGING) - # We try the main loop several times. If we get a lock error somewhere - # (for instance because someone broke the lock) we simply try again. - retries = mm_cfg.PENDINGDB_LOCK_ATTEMPTS - try: - while retries: - retries -= 1 - if not lock.locked(): - try: - lock.lock(timeout=mm_cfg.PENDINGDB_LOCK_TIMEOUT) - except LockFile.TimeOutError: - continue - # Load the current database - db = _load() - # Calculate a unique cookie. Algorithm vetted by the Timbot. - # time() has high resolution on Linux, clock() on Windows. random - # gives us about 45 bits in Python 2.2, 53 bits on Python 2.3. - # The time and clock values basically help obscure the random - # number generator, as does the hash calculation. The integral - # parts of the time values are discarded because they're the most - # predictable bits. - while True: - now = time.time() - x = random.random() + now % 1.0 + time.clock() % 1.0 - hashfood = repr(x) - cookie = sha.new(hashfood).hexdigest() - # We'll never get a duplicate, but we'll be anal about - # checking anyway. - if not db.has_key(cookie): - break - # Store the content, plus the time in the future when this entry - # will be evicted from the database, due to staleness. - db[cookie] = content - evictions = db.setdefault('evictions', {}) - evictions[cookie] = now + mm_cfg.PENDING_REQUEST_LIFE - try: - _save(db, lock) - except LockFile.NotLockedError: - continue - return cookie - else: - # We failed to get the lock or keep it long enough to save the - # data! - raise LockFile.TimeOutError - finally: - if lock.locked(): - lock.unlock() +_missing = [] -def confirm(cookie, expunge=True): - """Return data for cookie, or None if not found. - - If optional expunge is True (the default), the record is also removed from - the database. - """ - if not expunge: - db = _load() - missing = [] - content = db.get(cookie, missing) - if content is missing: - return None - return content - # Get a lock handle now, but only lock inside the loop. - lock = LockFile.LockFile(LOCKFILE, - withlogging=mm_cfg.PENDINGDB_LOCK_DEBUGGING) - # We try the main loop several times. If we get a lock error somewhere - # (for instance because someone broke the lock) we simply try again. - retries = mm_cfg.PENDINGDB_LOCK_ATTEMPTS - try: - while retries: - retries -= 1 - if not lock.locked(): - try: - lock.lock(timeout=mm_cfg.PENDINGDB_LOCK_TIMEOUT) - except LockFile.TimeOutError: - continue - # Load the database - db = _load() - missing = [] - content = db.get(cookie, missing) - if content is missing: - return None - del db[cookie] - del db['evictions'][cookie] - try: - _save(db, lock) - except LockFile.NotLockedError: - continue - return content - else: - # We failed to get the lock and keep it long enough to save the - # data! - raise LockFile.TimeOutError - finally: - if lock.locked(): - lock.unlock() - - - -def repend(cookie, data): - # Get a lock - lock = LockFile.LockFile( - LOCKFILE, withlogging=mm_cfg.PENDINGDB_LOCK_DEBUGGING) - lock.lock() - try: +class Pending: + def InitTempVars(self): + self.__pendfile = os.path.join(self.fullpath(), 'pending.pck') + + def pend_new(self, op, *content, **kws): + """Create a new entry in the pending database, returning cookie for it. + """ + assert op in _ALLKEYS, 'op: %s' % op + lifetime = kws.get('lifetime', mm_cfg.PENDING_REQUEST_LIFE) + # We try the main loop several times. If we get a lock error somewhere + # (for instance because someone broke the lock) we simply try again. + assert self.Locked() # Load the database - db = _load() - db[cookie] = data - db['evictions'][cookie] = time.time() + mm_cfg.PENDING_REQUEST_LIFE - _save(db, lock) - finally: - lock.unlock() - + db = self.__load() + # Calculate a unique cookie. Algorithm vetted by the Timbot. time() + # has high resolution on Linux, clock() on Windows. random gives us + # about 45 bits in Python 2.2, 53 bits on Python 2.3. The time and + # clock values basically help obscure the random number generator, as + # does the hash calculation. The integral parts of the time values + # are discarded because they're the most predictable bits. + while True: + now = time.time() + x = random.random() + now % 1.0 + time.clock() % 1.0 + cookie = sha.new(repr(x)).hexdigest() + # We'll never get a duplicate, but we'll be anal about checking + # anyway. + if not db.has_key(cookie): + break + # Store the content, plus the time in the future when this entry will + # be evicted from the database, due to staleness. + db[cookie] = (op,) + content + evictions = db.setdefault('evictions', {}) + evictions[cookie] = now + lifetime + self.__save(db) + return cookie - -def _load(): - # The list's lock must be acquired if you wish to alter data and save. - # - # First try to load the pickle file - fp = None - try: + def __load(self): try: - fp = open(PCKFILE) - return cPickle.load(fp) + fp = open(self.__pendfile) except IOError, e: if e.errno <> errno.ENOENT: raise - try: - # Try to load the old DBFILE - fp = open(DBFILE) - return marshal.load(fp) - except IOError, e: - if e.errno <> errno.ENOENT: raise - # Fresh pendings database - return {'evictions': {}} - finally: - if fp: + return {'evictions': {}} + try: + return cPickle.load(fp) + finally: fp.close() + def __save(self, db): + evictions = db['evictions'] + now = time.time() + for cookie, data in db.items(): + if cookie in ('evictions', 'version'): + continue + timestamp = evictions[cookie] + if now > timestamp: + # The entry is stale, so remove it. + del db[cookie] + del evictions[cookie] + # Clean out any bogus eviction entries. + for cookie in evictions.keys(): + if not db.has_key(cookie): + del evictions[cookie] + db['version'] = mm_cfg.PENDING_FILE_SCHEMA_VERSION + tmpfile = '%s.tmp.%d.%d' % (self.__pendfile, os.getpid(), now) + omask = os.umask(007) + try: + fp = open(tmpfile, 'w') + try: + cPickle.dump(db, fp) + fp.flush() + os.fsync(fp.fileno()) + finally: + fp.close() + os.rename(tmpfile, self.__pendfile) + finally: + os.umask(omask) + + def pend_confirm(self, cookie, expunge=True): + """Return data for cookie, or None if not found. + + If optional expunge is True (the default), the record is also removed + from the database. + """ + db = self.__load() + # If we're not expunging, the database is read-only. + if not expunge: + return db.get(cookie) + # Since we're going to modify the database, we must make sure the list + # is locked, since it's the list lock that protects pending.pck. + assert self.Locked() + content = db.get(cookie, _missing) + if content is _missing: + return None + # Do the expunge + del db[cookie] + del db['evictions'][cookie] + self.__save(db) + return content - -def _save(db, lock): - # Lock must be acquired before loading the data that is now being saved. - if not lock.locked(): - raise LockFile.NotLockedError - evictions = db['evictions'] - now = time.time() - for cookie, data in db.items(): - if cookie in ('evictions', 'version'): - continue - timestamp = evictions[cookie] - if now > timestamp: - # The entry is stale, so remove it. - del db[cookie] - del evictions[cookie] - # Clean out any bogus eviction entries. - for cookie in evictions.keys(): - if not db.has_key(cookie): - del evictions[cookie] - db['version'] = mm_cfg.PENDING_FILE_SCHEMA_VERSION - omask = os.umask(007) - # Always save this as a pickle (safely), and after that succeeds, blow - # away any old marshal file. - tmpfile = '%s.tmp.%d.%d' % (PCKFILE, os.getpid(), now) - fp = None - try: - fp = open(tmpfile, 'w') - cPickle.dump(db, fp) - fp.close() - fp = None - if not lock.locked(): - # Our lock was broken? - os.remove(tmpfile) - raise LockFile.NotLockedError - os.rename(tmpfile, PCKFILE) - if os.path.exists(DBFILE): - os.remove(DBFILE) - finally: - if fp: - fp.close() - os.umask(omask) + def pend_repend(self, cookie, data, lifetime=mm_cfg.PENDING_REQUEST_LIFE): + assert self.Locked() + db = self.__load() + db[cookie] = data + db['evictions'][cookie] = time.time() + lifetime + self.__save(db) def _update(olddb): - # Update an old pending_subscriptions.db database to the new format - lock = LockFile.LockFile(LOCKFILE, - withlogging=mm_cfg.PENDINGDB_LOCK_DEBUGGING) - lock.lock(timeout=mm_cfg.PENDINGDB_LOCK_TIMEOUT) - try: - # We don't need this entry anymore - if olddb.has_key('lastculltime'): - del olddb['lastculltime'] - db = _load() - evictions = db.setdefault('evictions', {}) - for cookie, data in olddb.items(): - # The cookies used to be kept as a 6 digit integer. We now keep - # the cookies as a string (sha in our case, but it doesn't matter - # for cookie matching). - cookie = str(cookie) - # The old format kept the content as a tuple and tacked the - # timestamp on as the last element of the tuple. We keep the - # timestamps separate, but require the prepending of a record type - # indicator. We know that the only things that were kept in the - # old format were subscription requests. Also, the old request - # format didn't have the subscription language. Best we can do - # here is use the server default. - db[cookie] = (SUBSCRIPTION,) + data[:-1] + \ - (mm_cfg.DEFAULT_SERVER_LANGUAGE,) - # The old database format kept the timestamp as the time the - # request was made. The new format keeps it as the time the - # request should be evicted. - evictions[cookie] = data[-1] + mm_cfg.PENDING_REQUEST_LIFE - _save(db, lock) - finally: - if lock.locked(): - lock.unlock() + db = {} + # We don't need this entry anymore + if olddb.has_key('lastculltime'): + del olddb['lastculltime'] + evictions = db.setdefault('evictions', {}) + for cookie, data in olddb.items(): + # The cookies used to be kept as a 6 digit integer. We now keep the + # cookies as a string (sha in our case, but it doesn't matter for + # cookie matching). + cookie = str(cookie) + # The old format kept the content as a tuple and tacked the timestamp + # on as the last element of the tuple. We keep the timestamps + # separate, but require the prepending of a record type indicator. We + # know that the only things that were kept in the old format were + # subscription requests. Also, the old request format didn't have the + # subscription language. Best we can do here is use the server + # default. + db[cookie] = (SUBSCRIPTION,) + data[:-1] + \ + (mm_cfg.DEFAULT_SERVER_LANGUAGE,) + # The old database format kept the timestamp as the time the request + # was made. The new format keeps it as the time the request should be + # evicted. + evictions[cookie] = data[-1] + mm_cfg.PENDING_REQUEST_LIFE + return db |