[z3-checkins] r24030 - in z3/sqlos/trunk/src/sqlos/file: . tests

jinty at codespeak.net jinty at codespeak.net
Mon Mar 6 19:11:20 CET 2006


Author: jinty
Date: Mon Mar  6 19:11:09 2006
New Revision: 24030

Added:
   z3/sqlos/trunk/src/sqlos/file/
   z3/sqlos/trunk/src/sqlos/file/README.txt   (contents, props changed)
   z3/sqlos/trunk/src/sqlos/file/__init__.py   (contents, props changed)
   z3/sqlos/trunk/src/sqlos/file/fsutility.py   (contents, props changed)
   z3/sqlos/trunk/src/sqlos/file/interfaces.py   (contents, props changed)
   z3/sqlos/trunk/src/sqlos/file/tests/
   z3/sqlos/trunk/src/sqlos/file/tests/__init__.py   (contents, props changed)
   z3/sqlos/trunk/src/sqlos/file/tests/test_fsutility.py   (contents, props changed)
Log:
introducing sqlos.file, a prototype of a transaction safe way of storing files on the filesystem and metadata in the RDB.

Added: z3/sqlos/trunk/src/sqlos/file/README.txt
==============================================================================
--- (empty file)
+++ z3/sqlos/trunk/src/sqlos/file/README.txt	Mon Mar  6 19:11:09 2006
@@ -0,0 +1,39 @@
+sqlos.file - Storing files on the filesystem and metadata in the database
+=========================================================================
+
+Objectives
+----------
+
+* Files to be stored on the filesystem with metadata a Relational or Object
+  database.
+* Files on the filesystem must be stored with their real name so that they
+  can be served by other servers (apache, ftp)
+* Must be transaction safe.
+
+Design decisions
+================
+
+sqlos.file.interfaces.IFileStorage
+----------------------------------
+
+This is a generic interface for a utility which can store files related to id's
+and filenames.
+
+    * Very primitive interface for simplicity.
+    * File ids are positive integers, this allows the conflation of these id's with
+      row ids in a relational database.
+    * Filenames can be passed to most methods to optimize the number of disk reads
+      if the files are stored on the filesystem. Methods must be robust if this name
+      turns out to be incorrect
+    * It only deals in file like objects and thus doesn't care about the encoding of
+      the contents.
+
+sqlos.file.fsutility.FileSystemFileStorage
+------------------------------------------
+
+    * Single instance represents one directory on the filesystem used for storage.
+    * Transactions don't conflict. At the moment of the commit the file is
+      either moved into place or deleted. It is up to other components of the
+      system to conflict during the prepare phase of the two phase commit.
+    * A tree structure is used to prevent having too many files in a single
+      directory.

Added: z3/sqlos/trunk/src/sqlos/file/__init__.py
==============================================================================

Added: z3/sqlos/trunk/src/sqlos/file/fsutility.py
==============================================================================
--- (empty file)
+++ z3/sqlos/trunk/src/sqlos/file/fsutility.py	Mon Mar  6 19:11:09 2006
@@ -0,0 +1,372 @@
+import os
+import shutil
+import threading
+
+import transaction
+from zope.interface import implements
+from transaction.interfaces import IDataManager
+from zope.thread import local
+
+from sqlos.file.interfaces import IFileStorage
+
+#
+# Globals
+#
+
+NOTFOUND = 'NOTFOUND'
+DELETED = 'DELETED'
+CHANGED = 'CHANGED'
+READ = 'READ'
+
+FS_USE_LOCK = threading.Lock() # One great big inefficient (but safe) lock for
+                               # whenever we do something not thread safe on
+                               # the filesystem
+
+class FileSystemFileStorage:
+    """Stores files keyed by integer ids on the filesystem.
+
+    First of all, setup the environment:
+
+        >>> import tempfile
+        >>> datadir = tempfile.mkdtemp()
+
+    Create a utility:
+
+        >>> fs = FileSystemFileStorage(datadir)
+        >>> from zope.interface.verify import verifyObject
+        >>> verifyObject(IFileStorage, fs)
+        True
+
+    and a file:
+
+        >>> from StringIO import StringIO
+        >>> teststring = "This is a test"
+        >>> testfile = StringIO(teststring)
+
+    we can store the test file in the utility:
+
+        >>> fs.putFile(5, 'testfile.txt', testfile)
+
+    and get it:
+
+        >>> fs.queryFile(1) is None
+        True
+        >>> name, file = fs.queryFile('5')
+        >>> (name, file.read()) == ('testfile.txt', teststring)
+        True
+        >>> name, file = fs.queryFile(5, filename='testfile.txt')
+        >>> (name, file.read()) == ('testfile.txt', teststring)
+        True
+        >>> name, file = fs.queryFile(5, filename='wrong')
+        >>> (name, file.read()) == ('testfile.txt', teststring)
+        True
+
+    Find out the size:
+
+        >>> fs.statFile(1) is None
+        True
+        >>> len(teststring) == fs.statFile('5').st_size
+        True
+        >>> len(teststring) == fs.statFile(5, filename='testfile.txt').st_size
+        True
+        >>> len(teststring) == fs.statFile(5, filename='wrong').st_size
+        True
+
+    Delete it:
+
+        >>> fs.deleteFile(1)
+        Traceback (most recent call last):
+            ...
+        KeyError: 1
+        >>> fs.deleteFile(5)
+        >>> fs.deleteFile(5)
+        Traceback (most recent call last):
+            ...
+        KeyError: 5
+        >>> fs.queryFile('5') is None
+        True
+
+    id's must be integers:
+
+        >>> fs.putFile('a', 'bbb', StringIO('a'))
+        Traceback (most recent call last):
+            ...
+        TypeError: a is not an integer
+        >>> fs.queryFile('a') == None
+        Traceback (most recent call last):
+            ...
+        TypeError: a is not an integer
+        >>> fs.statFile('a') == None
+        Traceback (most recent call last):
+            ...
+        TypeError: a is not an integer
+        >>> fs.deleteFile('a')
+        Traceback (most recent call last):
+            ...
+        TypeError: a is not an integer
+
+    id's must be greater or equal to 0:
+
+        >>> fs.putFile(-1, 'bbb', StringIO('a'))
+        Traceback (most recent call last):
+            ...
+        TypeError: -1 is less than 0
+        >>> fs.queryFile(-1) == None
+        Traceback (most recent call last):
+            ...
+        TypeError: -1 is less than 0
+        >>> fs.statFile(-1) == None
+        Traceback (most recent call last):
+            ...
+        TypeError: -1 is less than 0
+        >>> fs.deleteFile(-1)
+        Traceback (most recent call last):
+            ...
+        TypeError: -1 is less than 0
+
+        >>> fs.putFile(0, 'bbb', StringIO('a'))
+
+    Tear Down:
+
+        >>> transaction.get().commit()
+        >>> shutil.rmtree(datadir)
+    """
+
+    implements(IFileStorage)
+
+    def __init__(self, data_root):
+        self.data_root = data_root
+        self._tmpdir = os.path.join(self.data_root, 'tmp')
+        if os.path.exists(self._tmpdir):
+            # Remove stale temporary files dir in case of a non-clean shutdown
+            # XXX: are people going to try create more than on of this utility???
+            shutil.rmtree(self._tmpdir)
+        os.mkdir(self._tmpdir)
+        self._dmlocal = local()
+
+    def _getDataManager(self):
+        dm = getattr(self._dmlocal, 'dm', None)
+        if dm is None:
+            self._dmlocal.dm = FileSystemDataManager(self.data_root, self._tmpdir)
+            dm = self._dmlocal.dm
+        return dm
+
+    def _checkId(self, id):
+        try:
+            id = int(id) # parameter typecheck
+        except ValueError:
+            raise TypeError('%s is not an integer' % id)
+        if id < 0:
+            raise TypeError('%s is less than 0' % id)
+        return id
+
+    def statFile(self, id, filename=None):
+        id = self._checkId(id) # parameter typecheck
+        dm = self._getDataManager()
+        return dm.statFile(id, filename=filename)
+
+    def putFile(self, id, filename, file):
+        id = self._checkId(id) # parameter typecheck
+        dm = self._getDataManager()
+        return dm.putFile(id, filename, file)
+
+    def queryFile(self, id, filename=None):
+        id = self._checkId(id) # parameter typecheck
+        dm = self._getDataManager()
+        return dm.queryFile(id, filename=filename)
+
+    def deleteFile(self, id, filename=None):
+        id = self._checkId(id) # parameter typecheck
+        dm = self._getDataManager()
+        return dm.deleteFile(id, filename=filename)
+
+
+class FileSystemDataManager:
+
+    implements(IDataManager)
+
+    def __init__(self, data_root, data_tmp):
+        self._state = {}
+        self._filenames = {}
+        self._joined_txn = False
+        self.data_root = data_root
+        self.data_tmp = data_tmp
+
+    def _joinTransaction(self):
+        if not self._joined_txn:
+            txn = transaction.get()
+            txn.join(self)
+            self._joined_txn = True
+
+    def _calcFilePath(self, id):
+        """Calculate the file path for an id.
+
+        This chops the id up into a directory path. We need to put the file in
+        it's own directory as the file can have any filename. Also, with many
+        file names we want to create a tree structure so as not to have too
+        many entries in one directory. This is semi-randomly chosen as 100.
+
+        The entries are reversed so we can build it up as we go along
+
+        Like this:
+
+            >>> data_root = 'r'
+            >>> dm = FileSystemDataManager(data_root, '//b')
+            >>> dm._calcFilePath(00) == os.path.join(data_root, '00', 'data')
+            True
+            >>> dm._calcFilePath(23) == os.path.join(data_root, '23', 'data')
+            True
+            >>> dm._calcFilePath(100) == os.path.join(data_root, '00', '01', 'data')
+            True
+            >>> dm._calcFilePath(1234) == os.path.join(data_root, '34', '12', 'data')
+            True
+            >>> dm._calcFilePath(10001) == os.path.join(data_root, '01', '00', '01', 'data')
+            True
+            >>> dm._calcFilePath(123456) == os.path.join(data_root, '56', '34', '12', 'data')
+            True
+        """
+        # pad with a zero in the front if not even
+        id = str(id)
+        if len(id) % 2 == 1:
+            id = '0' + id
+        # divide into a list of groups of 2
+        tmp = None
+        path = []
+        for i in id:
+            if tmp is None:
+                tmp = i
+            else:
+                path.append(tmp + i)
+                tmp = None
+        # add on the non id parts and reverse
+        path.append(self.data_root)
+        path.reverse()
+        path.append('data')
+        return os.path.join(*path)
+
+    def _calcTmpFileName(self, id):
+        tid = threading.currentThread().getName()
+        filename = '%s-%s' % (tid, id)
+        return os.path.join(self.data_tmp, filename)
+
+    def _setTmpFile(self, id, filename):
+        assert id not in self._state
+        self._filenames[id] = filename
+        tmpfile = self._calcTmpFileName(id)
+        fullfilename = os.path.join(self._calcFilePath(id), filename)
+        # NOTE: Filesystem operation is thread safe as the thread id is a
+        # component of the filename
+        os.link(fullfilename, tmpfile)
+
+    def _getTmpFile(self, id):
+        assert id in self._state
+        tmpfile = self._calcTmpFileName(id)
+        return (self._filenames[id], open(tmpfile, 'r'))
+
+    def _delTmpFile(self, id):
+        tmpfile = self._calcTmpFileName(id)
+        # NOTE: Filesystem operation is thread safe as the thread id is a
+        # component of the filename
+        if os.path.exists(tmpfile):
+            os.remove(tmpfile)
+
+    def prepare(self, txn):
+        return True
+
+    def abort(self, txn):
+        for id in self._state:
+            self._delTmpFile(id)
+        self._state = {}
+        self._joined_txn = False
+
+    def commit(self, txn):
+        # XXX we need to thread lock those pieces dealing with the filesystem
+        for id, state in self._state.items():
+            if state == DELETED:
+                #delete if exists
+                dir = self._calcFilePath(id)
+                FS_USE_LOCK.acquire()
+                if os.path.exists(dir):
+                    shutil.rmtree(dir)
+                FS_USE_LOCK.release()
+            elif state == CHANGED:
+                filename = self._filenames[id]
+                tmpfile = self._calcTmpFileName(id)
+                filedir = self._calcFilePath(id)
+                fullfilename = os.path.join(filedir, filename)
+                FS_USE_LOCK.acquire()
+                if os.path.exists(filedir):
+                    shutil.rmtree(filedir)
+                os.makedirs(filedir)
+                os.link(tmpfile, fullfilename)
+                FS_USE_LOCK.release()
+            self._delTmpFile(id)
+        self._state = {}
+        self._joined_txn = False
+
+    def _queryFileName(self, id):
+        """Try to get a path to a file.
+
+        returns the path to the file, or None if not found.
+        """
+        filedir = self._calcFilePath(id)
+        try:
+            filenamelist = os.listdir(filedir)
+            assert len(filenamelist) == 1
+        except (AssertionError, OSError):
+            return None
+        return filenamelist[0]
+
+    def statFile(self, id, filename=None):
+        if id not in self._state:
+            # Never seen this file before
+            filename = self._queryFileName(id)
+            if filename is None:
+                self._state[id] = NOTFOUND
+                return None
+            self._setTmpFile(id, filename)
+            self._state[id] = READ
+        if self._state[id] in [NOTFOUND, DELETED]:
+            return None
+        tmpfile = self._calcTmpFileName(id)
+        return os.stat(tmpfile)
+
+    def queryFile(self, id, filename=None):
+        if id not in self._state:
+            # Never seen this file before
+            filename = self._queryFileName(id)
+            if filename is None:
+                self._state[id] = NOTFOUND
+                return None
+            self._setTmpFile(id, filename)
+            self._state[id] = READ
+        if self._state[id] in [NOTFOUND, DELETED]:
+            return None
+        return self._getTmpFile(id)
+
+    def deleteFile(self, id, filename=None):
+        if id not in self._state:
+            # Never seen this file before
+            filename = self._queryFileName(id)
+            if filename is None:
+                self._state[id] = NOTFOUND
+                raise KeyError(id)
+            self._joinTransaction()
+            self._state[id] = DELETED
+            return
+        if self._state[id] in [NOTFOUND, DELETED]:
+            raise KeyError(id)
+        self._joinTransaction()
+        self._state[id] = DELETED
+
+    def putFile(self, id, filename, file):
+        # XXX - this is really crappilly inefficient.
+        tmpfile = open(self._calcTmpFileName(id), 'w')
+        tmpfile.write(file.read())
+        file.close()
+        self._filenames[id] = filename
+        self._joinTransaction()
+        self._state[id] = CHANGED
+
+    def sortKey(self):
+        return str(id(self))

Added: z3/sqlos/trunk/src/sqlos/file/interfaces.py
==============================================================================
--- (empty file)
+++ z3/sqlos/trunk/src/sqlos/file/interfaces.py	Mon Mar  6 19:11:09 2006
@@ -0,0 +1,51 @@
+from zope.interface import Interface, Attribute
+from zope.schema.interfaces import IField
+from zope.schema import TextLine, Bytes
+
+class IFileStorage(Interface):
+
+    def statFile(id, filename=None):
+        """Return the results of calling os.stat on the file or None.
+
+        raises TypeError if the id is not a positive integer (i.e. => 0)
+
+        id          - positive integer.
+        filename    - a hint used for optimization or None for no hint,
+                      the hint is not trusted.
+
+        returns None if no file exists at that id.
+
+        Probably this is only useful for finding the size of the file.
+        """
+
+    def queryFile(id, filename=None):
+        """Return a tuple of (filename, file) or None.
+
+        raises TypeError if the id is not a positive integer (i.e. => 0)
+
+        id          - positive integer.
+        filename    - a hint used for optimization or None for no hint,
+                      the hint is not trusted.
+        """
+
+    def putFile(id, filename, file):
+        """Store a file, overwriting any existing file.
+
+        raises TypeError if the id is not a positive integer (i.e. => 0)
+
+        id          - positive integer
+        filename    - string.
+        file        - file like object, seek may or may not be called,
+                      so make sure that the current position is 0.
+        """
+
+    def deleteFile(id, filename=None):
+        """Delete the stored file.
+
+        raises a KeyError if no file by that id exists.
+        raises TypeError if the id is not a positive integer (i.e. => 0)
+
+        id          - positive integer.
+        filename    - a hint used for optimization or None for no hint,
+                      the hint is not trusted.
+        """

Added: z3/sqlos/trunk/src/sqlos/file/tests/__init__.py
==============================================================================

Added: z3/sqlos/trunk/src/sqlos/file/tests/test_fsutility.py
==============================================================================
--- (empty file)
+++ z3/sqlos/trunk/src/sqlos/file/tests/test_fsutility.py	Mon Mar  6 19:11:09 2006
@@ -0,0 +1,357 @@
+"""
+$Id: test_doctests.py 20906 2005-12-08 20:56:12Z jinty $
+"""
+
+import unittest
+import doctest
+import tempfile
+import shutil
+import threading
+from StringIO import StringIO
+
+import transaction
+from zope.testing.doctestunit import DocTestSuite
+from zope.app.testing.placelesssetup import setUp, tearDown
+from zope.app.testing import ztapi
+from zope import component
+
+from sqlos.file.interfaces import IFileStorage
+from sqlos.file.fsutility import FileSystemFileStorage
+
+class FileSystemFileStorageSetUp(object):
+
+    datadir = None
+
+    @classmethod
+    def setUp(cls):
+        assert cls.datadir is None
+        cls.datadir = tempfile.mkdtemp()
+        fs = FileSystemFileStorage(cls.datadir)
+        ztapi.provideUtility(IFileStorage, fs)
+
+    @classmethod
+    def tearDown(cls):
+        assert cls.datadir is not None
+        shutil.rmtree(cls.datadir)
+        cls.datadir = None
+        ztapi.unprovideUtility(IFileStorage)
+
+def test_transaction_read_isolation():
+    r"""
+    Set Up:
+
+        >>> setUp()
+        >>> FileSystemFileStorageSetUp.setUp()
+        >>> fileid = 5
+
+    Begin a transaction:
+
+        >>> transaction.get().commit()
+        >>> txn = transaction.begin()
+
+    Create a function to query the file in a different thread:
+
+        >>> log = []
+        >>> def queryFile():
+        ...     transaction.begin()
+        ...     fs = component.getUtility(IFileStorage)
+        ...     file = fs.queryFile(fileid)
+        ...     if file is not None:
+        ...         name = file[0]
+        ...         content = file[1].read()
+        ...         log.append('FILENAME:\n%s\nCONTENTS:\n%s' % (name, content))
+        ...     else:
+        ...         log.append(file)
+        ...     transaction.get().commit()
+
+        >>> def runInThread(target):
+        ...     thread = threading.Thread(target=target)
+        ...     thread.start()
+        ...     thread.join()
+        ...     print log.pop()
+
+    Try to get the file in a different thread:
+
+        >>> runInThread(queryFile)
+        None
+
+    Create a file:
+
+        >>> f = StringIO('File')
+        >>> fs = component.getUtility(IFileStorage)
+        >>> fs.putFile(fileid, 'file.txt', f)
+
+    Try to get the file in a different thread before comitting:
+
+        >>> runInThread(queryFile)
+        None
+
+    Commit the transaction
+
+        >>> transaction.get().commit()
+
+    Try to get the file in a different thread:
+
+        >>> runInThread(queryFile)
+        FILENAME:
+        file.txt
+        CONTENTS:
+        File
+
+    TearDown:
+
+        >>> FileSystemFileStorageSetUp.tearDown()
+        >>> tearDown()
+    """
+
+def test_transaction_conflict_write():
+    r"""Test what should happen when we get competing write/deletes.
+
+    The strategy here is an overly optimistic one. Writes always succeed.
+    Files are moved into place on transaction commit.
+
+    Set Up:
+
+        >>> setUp()
+        >>> FileSystemFileStorageSetUp.setUp()
+        >>> fileid = 5
+
+    Begin a transaction:
+
+        >>> transaction.get().commit()
+        >>> txn = transaction.begin()
+
+    Create a function to put or delete the file in a different thread:
+
+        >>> log = []
+        >>> def queryFile():
+        ...     transaction.begin()
+        ...     fs = component.getUtility(IFileStorage)
+        ...     file = fs.queryFile(fileid)
+        ...     if file is not None:
+        ...         name = file[0]
+        ...         content = file[1].read()
+        ...         log.append('FILENAME:\n%s\nCONTENTS:\n%s' % (name, content))
+        ...     else:
+        ...         log.append(file)
+        ...     transaction.get().commit()
+        >>> def putFile():
+        ...     transaction.begin()
+        ...     f = StringIO('eliF')
+        ...     fs = component.getUtility(IFileStorage)
+        ...     try:
+        ...         fs.putFile(fileid, 'txt.file', f)
+        ...         log.append('SUCCESS')
+        ...     except WriteConflictError:
+        ...         log.append('CONFLICT')
+        ...     transaction.get().commit()
+        >>> def deleteFile():
+        ...     transaction.begin()
+        ...     f = StringIO('File')
+        ...     fs = component.getUtility(IFileStorage)
+        ...     try:
+        ...         fs.deleteFile(fileid)
+        ...         log.append('SUCCESS')
+        ...     except KeyError:
+        ...         log.append('NOT FOUND')
+        ...     except WriteConflictError:
+        ...         log.append('CONFLICT')
+        ...     transaction.get().commit()
+
+        >>> def runInThread(target):
+        ...     thread = threading.Thread(target=target)
+        ...     thread.start()
+        ...     thread.join()
+        ...     print log.pop()
+
+    Try to put and delete in a different thread:
+
+        >>> runInThread(putFile)
+        SUCCESS
+        >>> runInThread(deleteFile)
+        SUCCESS
+
+    Create a file:
+
+        >>> f = StringIO('File')
+        >>> fs = component.getUtility(IFileStorage)
+        >>> fs.putFile(fileid, 'file.txt', f)
+
+    Try _really_ hard to break the transaction isolation before committing:
+
+        >>> runInThread(deleteFile)
+        NOT FOUND
+
+        >>> runInThread(queryFile)
+        None
+
+        >>> (name, file) = fs.queryFile(fileid)
+        >>> print 'FILENAME:\n%s\nCONTENTS:\n%s' % (name, file.read())
+        FILENAME:
+        file.txt
+        CONTENTS:
+        File
+
+        >>> runInThread(putFile)
+        SUCCESS
+
+        >>> (name, file) = fs.queryFile(fileid)
+        >>> print 'FILENAME:\n%s\nCONTENTS:\n%s' % (name, file.read())
+        FILENAME:
+        file.txt
+        CONTENTS:
+        File
+
+        >>> runInThread(queryFile)
+        FILENAME:
+        txt.file
+        CONTENTS:
+        eliF
+
+        >>> runInThread(deleteFile)
+        SUCCESS
+
+        >>> runInThread(queryFile)
+        None
+
+        >>> (name, file) = fs.queryFile(fileid)
+        >>> print 'FILENAME:\n%s\nCONTENTS:\n%s' % (name, file.read())
+        FILENAME:
+        file.txt
+        CONTENTS:
+        File
+
+    Commit the transaction
+
+        >>> transaction.get().commit()
+
+    Check that the file was created, overwriting the previous file:
+
+        >>> runInThread(queryFile)
+        FILENAME:
+        file.txt
+        CONTENTS:
+        File
+
+    TearDown:
+
+        >>> FileSystemFileStorageSetUp.tearDown()
+        >>> tearDown()
+    """
+
+def test_transaction_conflict_delete():
+    r"""Test what should happen when we get competing write/deletes.
+
+    The strategy here is an overly optimistic one. Writes always succeed.
+    Files are moved into place on transaction commit.
+
+    Set Up:
+
+        >>> setUp()
+        >>> FileSystemFileStorageSetUp.setUp()
+        >>> fileid = 6
+
+    Begin a transaction:
+
+        >>> transaction.get().commit()
+        >>> txn = transaction.begin()
+        >>> f = StringIO('File')
+        >>> fs = component.getUtility(IFileStorage)
+        >>> fs.putFile(fileid, 'file.txt', f)
+        >>> transaction.get().commit()
+        >>> txn = transaction.begin()
+
+    Create a function to put or delete the file in a different thread:
+
+        >>> log = []
+        >>> def queryFile():
+        ...     transaction.begin()
+        ...     fs = component.getUtility(IFileStorage)
+        ...     file = fs.queryFile(fileid)
+        ...     if file is not None:
+        ...         name = file[0]
+        ...         content = file[1].read()
+        ...         log.append('FILENAME:\n%s\nCONTENTS:\n%s' % (name, content))
+        ...     else:
+        ...         log.append(file)
+        ...     transaction.get().commit()
+        >>> def putFile():
+        ...     transaction.begin()
+        ...     f = StringIO('eliF')
+        ...     fs = component.getUtility(IFileStorage)
+        ...     fs.putFile(fileid, 'txt.file', f)
+        ...     log.append('SUCCESS')
+        ...     transaction.get().commit()
+        >>> def deleteFile():
+        ...     transaction.begin()
+        ...     fs = component.getUtility(IFileStorage)
+        ...     try:
+        ...         fs.deleteFile(fileid)
+        ...         log.append('SUCCESS')
+        ...     except KeyError:
+        ...         log.append('NOT FOUND')
+        ...     transaction.get().commit()
+
+        >>> def runInThread(target):
+        ...     thread = threading.Thread(target=target)
+        ...     thread.start()
+        ...     thread.join()
+        ...     print log.pop()
+
+    Try to put and delete in a different thread:
+
+        >>> runInThread(putFile) #a
+        SUCCESS
+        >>> runInThread(deleteFile) #b
+        SUCCESS
+        >>> runInThread(putFile) #c
+        SUCCESS
+
+    Delete the file:
+
+        >>> fs = component.getUtility(IFileStorage)
+        >>> fs.deleteFile(fileid)
+
+    Try _really_ hard to break the transaction isolation before committing:
+
+        >>> runInThread(deleteFile) #e
+        SUCCESS
+
+        >>> runInThread(queryFile) #f
+        None
+
+        >>> fs.queryFile(fileid) == None
+        True
+
+        >>> runInThread(putFile) #g
+        SUCCESS
+
+        >>> fs.queryFile(fileid) == None
+        True
+
+        >>> runInThread(queryFile) #h
+        FILENAME:
+        txt.file
+        CONTENTS:
+        eliF
+
+    Commit the transaction
+
+        >>> transaction.get().commit()
+
+    Check that the file was deleted, removing the previous file:
+
+        >>> runInThread(queryFile)
+        None
+
+    TearDown:
+
+        >>> FileSystemFileStorageSetUp.tearDown()
+        >>> tearDown()
+    """
+
+def test_suite():
+    return unittest.TestSuite([
+            DocTestSuite('sqlos.file.fsutility'),
+            DocTestSuite()
+            ])


More information about the z3-checkins mailing list