Pete Zaitcev ac36771
commit 1f4ec235cdfd8c868f2d6458532f9dc32c00b8ca
Pete Zaitcev ac36771
Author: Peter Portante <peter.portante@redhat.com>
Pete Zaitcev ac36771
Date:   Fri Jul 26 15:03:34 2013 -0400
Pete Zaitcev ac36771
Pete Zaitcev ac36771
    Fix handling of DELETE obj reqs with old timestamp
Pete Zaitcev ac36771
    
Pete Zaitcev ac36771
    The DELETE object REST API was creating tombstone files with old
Pete Zaitcev ac36771
    timestamps, potentially filling up the disk, as well as sending
Pete Zaitcev ac36771
    container updates.
Pete Zaitcev ac36771
    
Pete Zaitcev ac36771
    Here we now make DELETEs with a request timestamp return a 409 (HTTP
Pete Zaitcev ac36771
    Conflict) if a data file exists with a newer timestamp, only creating
Pete Zaitcev ac36771
    tombstones if they have a newer timestamp.
Pete Zaitcev ac36771
    
Pete Zaitcev ac36771
    The key fix is to actually read the timestamp metadata from an
Pete Zaitcev ac36771
    existing tombstone file (thanks to Pete Zaitcev for catching this),
Pete Zaitcev ac36771
    and then only create tombstone files with newer timestamps.
Pete Zaitcev ac36771
    
Pete Zaitcev ac36771
    We also prevent PUT and POST operations using old timestamps as well.
Pete Zaitcev ac36771
    
Pete Zaitcev ac36771
    Change-Id: I631957029d17c6578bca5779367df5144ba01fc9
Pete Zaitcev ac36771
    Signed-off-by: Peter Portante <peter.portante@redhat.com>
Pete Zaitcev ac36771
Pete Zaitcev ac36771
diff --git a/swift/obj/server.py b/swift/obj/server.py
Pete Zaitcev ac36771
index fc23ea2..f416162 100644
Pete Zaitcev ac36771
--- a/swift/obj/server.py
Pete Zaitcev ac36771
+++ b/swift/obj/server.py
Pete Zaitcev ac36771
@@ -46,7 +46,7 @@ from swift.common.swob import HTTPAccepted, HTTPBadRequest, HTTPCreated, \
Pete Zaitcev ac36771
     HTTPInternalServerError, HTTPNoContent, HTTPNotFound, HTTPNotModified, \
Pete Zaitcev ac36771
     HTTPPreconditionFailed, HTTPRequestTimeout, HTTPUnprocessableEntity, \
Pete Zaitcev ac36771
     HTTPClientDisconnect, HTTPMethodNotAllowed, Request, Response, UTC, \
Pete Zaitcev ac36771
-    HTTPInsufficientStorage, multi_range_iterator
Pete Zaitcev ac36771
+    HTTPInsufficientStorage, multi_range_iterator, HTTPConflict
Pete Zaitcev ac36771
 
Pete Zaitcev ac36771
 
Pete Zaitcev ac36771
 DATADIR = 'objects'
Pete Zaitcev ac36771
@@ -121,7 +121,6 @@ class DiskFile(object):
Pete Zaitcev ac36771
         self.tmppath = None
Pete Zaitcev ac36771
         self.logger = logger
Pete Zaitcev ac36771
         self.metadata = {}
Pete Zaitcev ac36771
-        self.meta_file = None
Pete Zaitcev ac36771
         self.data_file = None
Pete Zaitcev ac36771
         self.fp = None
Pete Zaitcev ac36771
         self.iter_etag = None
Pete Zaitcev ac36771
@@ -133,15 +132,18 @@ class DiskFile(object):
Pete Zaitcev ac36771
         if not os.path.exists(self.datadir):
Pete Zaitcev ac36771
             return
Pete Zaitcev ac36771
         files = sorted(os.listdir(self.datadir), reverse=True)
Pete Zaitcev ac36771
-        for file in files:
Pete Zaitcev ac36771
-            if file.endswith('.ts'):
Pete Zaitcev ac36771
-                self.data_file = self.meta_file = None
Pete Zaitcev ac36771
-                self.metadata = {'deleted': True}
Pete Zaitcev ac36771
-                return
Pete Zaitcev ac36771
-            if file.endswith('.meta') and not self.meta_file:
Pete Zaitcev ac36771
-                self.meta_file = os.path.join(self.datadir, file)
Pete Zaitcev ac36771
-            if file.endswith('.data') and not self.data_file:
Pete Zaitcev ac36771
-                self.data_file = os.path.join(self.datadir, file)
Pete Zaitcev ac36771
+        meta_file = None
Pete Zaitcev ac36771
+        for afile in files:
Pete Zaitcev ac36771
+            if afile.endswith('.ts'):
Pete Zaitcev ac36771
+                self.data_file = None
Pete Zaitcev ac36771
+                with open(os.path.join(self.datadir, afile)) as mfp:
Pete Zaitcev ac36771
+                    self.metadata = read_metadata(mfp)
Pete Zaitcev ac36771
+                self.metadata['deleted'] = True
Pete Zaitcev ac36771
+                break
Pete Zaitcev ac36771
+            if afile.endswith('.meta') and not meta_file:
Pete Zaitcev ac36771
+                meta_file = os.path.join(self.datadir, afile)
Pete Zaitcev ac36771
+            if afile.endswith('.data') and not self.data_file:
Pete Zaitcev ac36771
+                self.data_file = os.path.join(self.datadir, afile)
Pete Zaitcev ac36771
                 break
Pete Zaitcev ac36771
         if not self.data_file:
Pete Zaitcev ac36771
             return
Pete Zaitcev ac36771
@@ -149,8 +151,8 @@ class DiskFile(object):
Pete Zaitcev ac36771
         self.metadata = read_metadata(self.fp)
Pete Zaitcev ac36771
         if not keep_data_fp:
Pete Zaitcev ac36771
             self.close(verify_file=False)
Pete Zaitcev ac36771
-        if self.meta_file:
Pete Zaitcev ac36771
-            with open(self.meta_file) as mfp:
Pete Zaitcev ac36771
+        if meta_file:
Pete Zaitcev ac36771
+            with open(meta_file) as mfp:
Pete Zaitcev ac36771
                 for key in self.metadata.keys():
Pete Zaitcev ac36771
                     if key.lower() not in DISALLOWED_HEADERS:
Pete Zaitcev ac36771
                         del self.metadata[key]
Pete Zaitcev ac36771
@@ -594,6 +596,9 @@ class ObjectController(object):
Pete Zaitcev ac36771
         except (DiskFileError, DiskFileNotExist):
Pete Zaitcev ac36771
             file.quarantine()
Pete Zaitcev ac36771
             return HTTPNotFound(request=request)
Pete Zaitcev ac36771
+        orig_timestamp = file.metadata.get('X-Timestamp', '0')
Pete Zaitcev ac36771
+        if orig_timestamp >= request.headers['x-timestamp']:
Pete Zaitcev ac36771
+            return HTTPConflict(request=request)
Pete Zaitcev ac36771
         metadata = {'X-Timestamp': request.headers['x-timestamp']}
Pete Zaitcev ac36771
         metadata.update(val for val in request.headers.iteritems()
Pete Zaitcev ac36771
                         if val[0].lower().startswith('x-object-meta-'))
Pete Zaitcev ac36771
@@ -639,6 +644,8 @@ class ObjectController(object):
Pete Zaitcev ac36771
         file = DiskFile(self.devices, device, partition, account, container,
Pete Zaitcev ac36771
                         obj, self.logger, disk_chunk_size=self.disk_chunk_size)
Pete Zaitcev ac36771
         orig_timestamp = file.metadata.get('X-Timestamp')
Pete Zaitcev ac36771
+        if orig_timestamp and orig_timestamp >= request.headers['x-timestamp']:
Pete Zaitcev ac36771
+            return HTTPConflict(request=request)
Pete Zaitcev ac36771
         upload_expiration = time.time() + self.max_upload_time
Pete Zaitcev ac36771
         etag = md5()
Pete Zaitcev ac36771
         upload_size = 0
Pete Zaitcev ac36771
@@ -863,23 +870,26 @@ class ObjectController(object):
Pete Zaitcev ac36771
             return HTTPPreconditionFailed(
Pete Zaitcev ac36771
                 request=request,
Pete Zaitcev ac36771
                 body='X-If-Delete-At and X-Delete-At do not match')
Pete Zaitcev ac36771
-        orig_timestamp = file.metadata.get('X-Timestamp')
Pete Zaitcev ac36771
-        if file.is_deleted() or file.is_expired():
Pete Zaitcev ac36771
-            response_class = HTTPNotFound
Pete Zaitcev ac36771
-        metadata = {
Pete Zaitcev ac36771
-            'X-Timestamp': request.headers['X-Timestamp'], 'deleted': True,
Pete Zaitcev ac36771
-        }
Pete Zaitcev ac36771
         old_delete_at = int(file.metadata.get('X-Delete-At') or 0)
Pete Zaitcev ac36771
         if old_delete_at:
Pete Zaitcev ac36771
             self.delete_at_update('DELETE', old_delete_at, account,
Pete Zaitcev ac36771
                                   container, obj, request.headers, device)
Pete Zaitcev ac36771
-        file.put_metadata(metadata, tombstone=True)
Pete Zaitcev ac36771
-        file.unlinkold(metadata['X-Timestamp'])
Pete Zaitcev ac36771
-        if not orig_timestamp or \
Pete Zaitcev ac36771
-                orig_timestamp < request.headers['x-timestamp']:
Pete Zaitcev ac36771
+        orig_timestamp = file.metadata.get('X-Timestamp', 0)
Pete Zaitcev ac36771
+        req_timestamp = request.headers['X-Timestamp']
Pete Zaitcev ac36771
+        if file.is_deleted() or file.is_expired():
Pete Zaitcev ac36771
+            response_class = HTTPNotFound
Pete Zaitcev ac36771
+        else:
Pete Zaitcev ac36771
+            if orig_timestamp < req_timestamp:
Pete Zaitcev ac36771
+                response_class = HTTPNoContent
Pete Zaitcev ac36771
+            else:
Pete Zaitcev ac36771
+                response_class = HTTPConflict
Pete Zaitcev ac36771
+        if orig_timestamp < req_timestamp:
Pete Zaitcev ac36771
+            file.put_metadata({'X-Timestamp': req_timestamp},
Pete Zaitcev ac36771
+                              tombstone=True)
Pete Zaitcev ac36771
+            file.unlinkold(req_timestamp)
Pete Zaitcev ac36771
             self.container_update(
Pete Zaitcev ac36771
                 'DELETE', account, container, obj, request.headers,
Pete Zaitcev ac36771
-                {'x-timestamp': metadata['X-Timestamp'],
Pete Zaitcev ac36771
+                {'x-timestamp': req_timestamp,
Pete Zaitcev ac36771
                  'x-trans-id': request.headers.get('x-trans-id', '-')},
Pete Zaitcev ac36771
                 device)
Pete Zaitcev ac36771
         resp = response_class(request=request)
Pete Zaitcev ac36771
diff --git a/test/unit/obj/test_server.py b/test/unit/obj/test_server.py
Pete Zaitcev ac36771
index 8ee266b..b354b97 100755
Pete Zaitcev ac36771
--- a/test/unit/obj/test_server.py
Pete Zaitcev ac36771
+++ b/test/unit/obj/test_server.py
Pete Zaitcev ac36771
@@ -509,6 +509,41 @@ class TestObjectController(unittest.TestCase):
Pete Zaitcev ac36771
                      "X-Object-Meta-3" in resp.headers)
Pete Zaitcev ac36771
         self.assertEquals(resp.headers['Content-Type'], 'application/x-test')
Pete Zaitcev ac36771
 
Pete Zaitcev ac36771
+    def test_POST_old_timestamp(self):
Pete Zaitcev ac36771
+        ts = time()
Pete Zaitcev ac36771
+        timestamp = normalize_timestamp(ts)
Pete Zaitcev ac36771
+        req = Request.blank('/sda1/p/a/c/o', environ={'REQUEST_METHOD': 'PUT'},
Pete Zaitcev ac36771
+                            headers={'X-Timestamp': timestamp,
Pete Zaitcev ac36771
+                                     'Content-Type': 'application/x-test',
Pete Zaitcev ac36771
+                                     'X-Object-Meta-1': 'One',
Pete Zaitcev ac36771
+                                     'X-Object-Meta-Two': 'Two'})
Pete Zaitcev ac36771
+        req.body = 'VERIFY'
Pete Zaitcev ac36771
+        resp = self.object_controller.PUT(req)
Pete Zaitcev ac36771
+        self.assertEquals(resp.status_int, 201)
Pete Zaitcev ac36771
+
Pete Zaitcev ac36771
+        # Same timestamp should result in 409
Pete Zaitcev ac36771
+        req = Request.blank('/sda1/p/a/c/o',
Pete Zaitcev ac36771
+                            environ={'REQUEST_METHOD': 'POST'},
Pete Zaitcev ac36771
+                            headers={'X-Timestamp': timestamp,
Pete Zaitcev ac36771
+                                     'X-Object-Meta-3': 'Three',
Pete Zaitcev ac36771
+                                     'X-Object-Meta-4': 'Four',
Pete Zaitcev ac36771
+                                     'Content-Encoding': 'gzip',
Pete Zaitcev ac36771
+                                     'Content-Type': 'application/x-test'})
Pete Zaitcev ac36771
+        resp = self.object_controller.POST(req)
Pete Zaitcev ac36771
+        self.assertEquals(resp.status_int, 409)
Pete Zaitcev ac36771
+
Pete Zaitcev ac36771
+        # Earlier timestamp should result in 409
Pete Zaitcev ac36771
+        timestamp = normalize_timestamp(ts - 1)
Pete Zaitcev ac36771
+        req = Request.blank('/sda1/p/a/c/o',
Pete Zaitcev ac36771
+                            environ={'REQUEST_METHOD': 'POST'},
Pete Zaitcev ac36771
+                            headers={'X-Timestamp': timestamp,
Pete Zaitcev ac36771
+                                     'X-Object-Meta-5': 'Five',
Pete Zaitcev ac36771
+                                     'X-Object-Meta-6': 'Six',
Pete Zaitcev ac36771
+                                     'Content-Encoding': 'gzip',
Pete Zaitcev ac36771
+                                     'Content-Type': 'application/x-test'})
Pete Zaitcev ac36771
+        resp = self.object_controller.POST(req)
Pete Zaitcev ac36771
+        self.assertEquals(resp.status_int, 409)
Pete Zaitcev ac36771
+
Pete Zaitcev ac36771
     def test_POST_not_exist(self):
Pete Zaitcev ac36771
         timestamp = normalize_timestamp(time())
Pete Zaitcev ac36771
         req = Request.blank('/sda1/p/a/c/fail',
Pete Zaitcev ac36771
@@ -555,11 +590,15 @@ class TestObjectController(unittest.TestCase):
Pete Zaitcev ac36771
 
Pete Zaitcev ac36771
         old_http_connect = object_server.http_connect
Pete Zaitcev ac36771
         try:
Pete Zaitcev ac36771
-            timestamp = normalize_timestamp(time())
Pete Zaitcev ac36771
+            ts = time()
Pete Zaitcev ac36771
+            timestamp = normalize_timestamp(ts)
Pete Zaitcev ac36771
             req = Request.blank('/sda1/p/a/c/o', environ={'REQUEST_METHOD':
Pete Zaitcev ac36771
                 'POST'}, headers={'X-Timestamp': timestamp, 'Content-Type':
Pete Zaitcev ac36771
                 'text/plain', 'Content-Length': '0'})
Pete Zaitcev ac36771
             resp = self.object_controller.PUT(req)
Pete Zaitcev ac36771
+            self.assertEquals(resp.status_int, 201)
Pete Zaitcev ac36771
+
Pete Zaitcev ac36771
+            timestamp = normalize_timestamp(ts + 1)
Pete Zaitcev ac36771
             req = Request.blank('/sda1/p/a/c/o',
Pete Zaitcev ac36771
                     environ={'REQUEST_METHOD': 'POST'},
Pete Zaitcev ac36771
                     headers={'X-Timestamp': timestamp,
Pete Zaitcev ac36771
@@ -571,6 +610,8 @@ class TestObjectController(unittest.TestCase):
Pete Zaitcev ac36771
             object_server.http_connect = mock_http_connect(202)
Pete Zaitcev ac36771
             resp = self.object_controller.POST(req)
Pete Zaitcev ac36771
             self.assertEquals(resp.status_int, 202)
Pete Zaitcev ac36771
+
Pete Zaitcev ac36771
+            timestamp = normalize_timestamp(ts + 2)
Pete Zaitcev ac36771
             req = Request.blank('/sda1/p/a/c/o',
Pete Zaitcev ac36771
                     environ={'REQUEST_METHOD': 'POST'},
Pete Zaitcev ac36771
                     headers={'X-Timestamp': timestamp,
Pete Zaitcev ac36771
@@ -582,6 +623,8 @@ class TestObjectController(unittest.TestCase):
Pete Zaitcev ac36771
             object_server.http_connect = mock_http_connect(202, with_exc=True)
Pete Zaitcev ac36771
             resp = self.object_controller.POST(req)
Pete Zaitcev ac36771
             self.assertEquals(resp.status_int, 202)
Pete Zaitcev ac36771
+
Pete Zaitcev ac36771
+            timestamp = normalize_timestamp(ts + 3)
Pete Zaitcev ac36771
             req = Request.blank('/sda1/p/a/c/o',
Pete Zaitcev ac36771
                     environ={'REQUEST_METHOD': 'POST'},
Pete Zaitcev ac36771
                     headers={'X-Timestamp': timestamp,
Pete Zaitcev ac36771
@@ -718,6 +761,32 @@ class TestObjectController(unittest.TestCase):
Pete Zaitcev ac36771
                            'name': '/a/c/o',
Pete Zaitcev ac36771
                            'Content-Encoding': 'gzip'})
Pete Zaitcev ac36771
 
Pete Zaitcev ac36771
+    def test_PUT_old_timestamp(self):
Pete Zaitcev ac36771
+        ts = time()
Pete Zaitcev ac36771
+        req = Request.blank('/sda1/p/a/c/o', environ={'REQUEST_METHOD': 'PUT'},
Pete Zaitcev ac36771
+                headers={'X-Timestamp': normalize_timestamp(ts),
Pete Zaitcev ac36771
+                         'Content-Length': '6',
Pete Zaitcev ac36771
+                         'Content-Type': 'application/octet-stream'})
Pete Zaitcev ac36771
+        req.body = 'VERIFY'
Pete Zaitcev ac36771
+        resp = self.object_controller.PUT(req)
Pete Zaitcev ac36771
+        self.assertEquals(resp.status_int, 201)
Pete Zaitcev ac36771
+
Pete Zaitcev ac36771
+        req = Request.blank('/sda1/p/a/c/o', environ={'REQUEST_METHOD': 'PUT'},
Pete Zaitcev ac36771
+                            headers={'X-Timestamp': normalize_timestamp(ts),
Pete Zaitcev ac36771
+                                     'Content-Type': 'text/plain',
Pete Zaitcev ac36771
+                                     'Content-Encoding': 'gzip'})
Pete Zaitcev ac36771
+        req.body = 'VERIFY TWO'
Pete Zaitcev ac36771
+        resp = self.object_controller.PUT(req)
Pete Zaitcev ac36771
+        self.assertEquals(resp.status_int, 409)
Pete Zaitcev ac36771
+
Pete Zaitcev ac36771
+        req = Request.blank('/sda1/p/a/c/o', environ={'REQUEST_METHOD': 'PUT'},
Pete Zaitcev ac36771
+                            headers={'X-Timestamp': normalize_timestamp(ts - 1),
Pete Zaitcev ac36771
+                                     'Content-Type': 'text/plain',
Pete Zaitcev ac36771
+                                     'Content-Encoding': 'gzip'})
Pete Zaitcev ac36771
+        req.body = 'VERIFY THREE'
Pete Zaitcev ac36771
+        resp = self.object_controller.PUT(req)
Pete Zaitcev ac36771
+        self.assertEquals(resp.status_int, 409)
Pete Zaitcev ac36771
+
Pete Zaitcev ac36771
     def test_PUT_no_etag(self):
Pete Zaitcev ac36771
         req = Request.blank('/sda1/p/a/c/o', environ={'REQUEST_METHOD': 'PUT'},
Pete Zaitcev ac36771
                            headers={'X-Timestamp': normalize_timestamp(time()),
Pete Zaitcev ac36771
@@ -1306,12 +1375,32 @@ class TestObjectController(unittest.TestCase):
Pete Zaitcev ac36771
         self.assertEquals(resp.status_int, 400)
Pete Zaitcev ac36771
         # self.assertRaises(KeyError, self.object_controller.DELETE, req)
Pete Zaitcev ac36771
 
Pete Zaitcev ac36771
+        # The following should have created a tombstone file
Pete Zaitcev ac36771
         timestamp = normalize_timestamp(time())
Pete Zaitcev ac36771
         req = Request.blank('/sda1/p/a/c/o',
Pete Zaitcev ac36771
                             environ={'REQUEST_METHOD': 'DELETE'},
Pete Zaitcev ac36771
                             headers={'X-Timestamp': timestamp})
Pete Zaitcev ac36771
         resp = self.object_controller.DELETE(req)
Pete Zaitcev ac36771
         self.assertEquals(resp.status_int, 404)
Pete Zaitcev ac36771
+        objfile = os.path.join(self.testdir, 'sda1',
Pete Zaitcev ac36771
+            storage_directory(object_server.DATADIR, 'p',
Pete Zaitcev ac36771
+                              hash_path('a', 'c', 'o')),
Pete Zaitcev ac36771
+            timestamp + '.ts')
Pete Zaitcev ac36771
+        self.assert_(os.path.isfile(objfile))
Pete Zaitcev ac36771
+
Pete Zaitcev ac36771
+        # The following should *not* have created a tombstone file.
Pete Zaitcev ac36771
+        timestamp = normalize_timestamp(float(timestamp) - 1)
Pete Zaitcev ac36771
+        req = Request.blank('/sda1/p/a/c/o',
Pete Zaitcev ac36771
+                            environ={'REQUEST_METHOD': 'DELETE'},
Pete Zaitcev ac36771
+                            headers={'X-Timestamp': timestamp})
Pete Zaitcev ac36771
+        resp = self.object_controller.DELETE(req)
Pete Zaitcev ac36771
+        self.assertEquals(resp.status_int, 404)
Pete Zaitcev ac36771
+        objfile = os.path.join(self.testdir, 'sda1',
Pete Zaitcev ac36771
+            storage_directory(object_server.DATADIR, 'p',
Pete Zaitcev ac36771
+                              hash_path('a', 'c', 'o')),
Pete Zaitcev ac36771
+            timestamp + '.ts')
Pete Zaitcev ac36771
+        self.assertFalse(os.path.exists(objfile))
Pete Zaitcev ac36771
+        self.assertEquals(len(os.listdir(os.path.dirname(objfile))), 1)
Pete Zaitcev ac36771
 
Pete Zaitcev ac36771
         sleep(.00001)
Pete Zaitcev ac36771
         timestamp = normalize_timestamp(time())
Pete Zaitcev ac36771
@@ -1325,17 +1414,19 @@ class TestObjectController(unittest.TestCase):
Pete Zaitcev ac36771
         resp = self.object_controller.PUT(req)
Pete Zaitcev ac36771
         self.assertEquals(resp.status_int, 201)
Pete Zaitcev ac36771
 
Pete Zaitcev ac36771
+        # The following should *not* have created a tombstone file.
Pete Zaitcev ac36771
         timestamp = normalize_timestamp(float(timestamp) - 1)
Pete Zaitcev ac36771
         req = Request.blank('/sda1/p/a/c/o',
Pete Zaitcev ac36771
                             environ={'REQUEST_METHOD': 'DELETE'},
Pete Zaitcev ac36771
                             headers={'X-Timestamp': timestamp})
Pete Zaitcev ac36771
         resp = self.object_controller.DELETE(req)
Pete Zaitcev ac36771
-        self.assertEquals(resp.status_int, 204)
Pete Zaitcev ac36771
+        self.assertEquals(resp.status_int, 409)
Pete Zaitcev ac36771
         objfile = os.path.join(self.testdir, 'sda1',
Pete Zaitcev ac36771
             storage_directory(object_server.DATADIR, 'p',
Pete Zaitcev ac36771
                               hash_path('a', 'c', 'o')),
Pete Zaitcev ac36771
             timestamp + '.ts')
Pete Zaitcev ac36771
-        self.assert_(os.path.isfile(objfile))
Pete Zaitcev ac36771
+        self.assertFalse(os.path.exists(objfile))
Pete Zaitcev ac36771
+        self.assertEquals(len(os.listdir(os.path.dirname(objfile))), 1)
Pete Zaitcev ac36771
 
Pete Zaitcev ac36771
         sleep(.00001)
Pete Zaitcev ac36771
         timestamp = normalize_timestamp(time())
Pete Zaitcev ac36771
@@ -1350,6 +1441,103 @@ class TestObjectController(unittest.TestCase):
Pete Zaitcev ac36771
             timestamp + '.ts')
Pete Zaitcev ac36771
         self.assert_(os.path.isfile(objfile))
Pete Zaitcev ac36771
 
Pete Zaitcev ac36771
+    def test_DELETE_container_updates(self):
Pete Zaitcev ac36771
+        # Test swift.object_server.ObjectController.DELETE and container
Pete Zaitcev ac36771
+        # updates, making sure container update is called in the correct
Pete Zaitcev ac36771
+        # state.
Pete Zaitcev ac36771
+        timestamp = normalize_timestamp(time())
Pete Zaitcev ac36771
+        req = Request.blank('/sda1/p/a/c/o', environ={'REQUEST_METHOD': 'PUT'},
Pete Zaitcev ac36771
+                            headers={
Pete Zaitcev ac36771
+                                'X-Timestamp': timestamp,
Pete Zaitcev ac36771
+                                'Content-Type': 'application/octet-stream',
Pete Zaitcev ac36771
+                                'Content-Length': '4',
Pete Zaitcev ac36771
+                                })
Pete Zaitcev ac36771
+        req.body = 'test'
Pete Zaitcev ac36771
+        resp = self.object_controller.PUT(req)
Pete Zaitcev ac36771
+        self.assertEquals(resp.status_int, 201)
Pete Zaitcev ac36771
+
Pete Zaitcev ac36771
+        calls_made = [0]
Pete Zaitcev ac36771
+
Pete Zaitcev ac36771
+        def our_container_update(*args, **kwargs):
Pete Zaitcev ac36771
+            calls_made[0] += 1
Pete Zaitcev ac36771
+
Pete Zaitcev ac36771
+        orig_cu = self.object_controller.container_update
Pete Zaitcev ac36771
+        self.object_controller.container_update = our_container_update
Pete Zaitcev ac36771
+        try:
Pete Zaitcev ac36771
+            # The following request should return 409 (HTTP Conflict). A
Pete Zaitcev ac36771
+            # tombstone file should not have been created with this timestamp.
Pete Zaitcev ac36771
+            timestamp = normalize_timestamp(float(timestamp) - 1)
Pete Zaitcev ac36771
+            req = Request.blank('/sda1/p/a/c/o',
Pete Zaitcev ac36771
+                                environ={'REQUEST_METHOD': 'DELETE'},
Pete Zaitcev ac36771
+                                headers={'X-Timestamp': timestamp})
Pete Zaitcev ac36771
+            resp = self.object_controller.DELETE(req)
Pete Zaitcev ac36771
+            self.assertEquals(resp.status_int, 409)
Pete Zaitcev ac36771
+            objfile = os.path.join(self.testdir, 'sda1',
Pete Zaitcev ac36771
+                storage_directory(object_server.DATADIR, 'p',
Pete Zaitcev ac36771
+                                  hash_path('a', 'c', 'o')),
Pete Zaitcev ac36771
+                timestamp + '.ts')
Pete Zaitcev ac36771
+            self.assertFalse(os.path.isfile(objfile))
Pete Zaitcev ac36771
+            self.assertEquals(len(os.listdir(os.path.dirname(objfile))), 1)
Pete Zaitcev ac36771
+            self.assertEquals(0, calls_made[0])
Pete Zaitcev ac36771
+
Pete Zaitcev ac36771
+            # The following request should return 204, and the object should
Pete Zaitcev ac36771
+            # be truly deleted (container update is performed) because this
Pete Zaitcev ac36771
+            # timestamp is newer. A tombstone file should have been created
Pete Zaitcev ac36771
+            # with this timestamp.
Pete Zaitcev ac36771
+            sleep(.00001)
Pete Zaitcev ac36771
+            timestamp = normalize_timestamp(time())
Pete Zaitcev ac36771
+            req = Request.blank('/sda1/p/a/c/o',
Pete Zaitcev ac36771
+                                environ={'REQUEST_METHOD': 'DELETE'},
Pete Zaitcev ac36771
+                                headers={'X-Timestamp': timestamp})
Pete Zaitcev ac36771
+            resp = self.object_controller.DELETE(req)
Pete Zaitcev ac36771
+            self.assertEquals(resp.status_int, 204)
Pete Zaitcev ac36771
+            objfile = os.path.join(self.testdir, 'sda1',
Pete Zaitcev ac36771
+                storage_directory(object_server.DATADIR, 'p',
Pete Zaitcev ac36771
+                                  hash_path('a', 'c', 'o')),
Pete Zaitcev ac36771
+                timestamp + '.ts')
Pete Zaitcev ac36771
+            self.assert_(os.path.isfile(objfile))
Pete Zaitcev ac36771
+            self.assertEquals(1, calls_made[0])
Pete Zaitcev ac36771
+            self.assertEquals(len(os.listdir(os.path.dirname(objfile))), 1)
Pete Zaitcev ac36771
+
Pete Zaitcev ac36771
+            # The following request should return a 404, as the object should
Pete Zaitcev ac36771
+            # already have been deleted, but it should have also performed a
Pete Zaitcev ac36771
+            # container update because the timestamp is newer, and a tombstone
Pete Zaitcev ac36771
+            # file should also exist with this timestamp.
Pete Zaitcev ac36771
+            sleep(.00001)
Pete Zaitcev ac36771
+            timestamp = normalize_timestamp(time())
Pete Zaitcev ac36771
+            req = Request.blank('/sda1/p/a/c/o',
Pete Zaitcev ac36771
+                                environ={'REQUEST_METHOD': 'DELETE'},
Pete Zaitcev ac36771
+                                headers={'X-Timestamp': timestamp})
Pete Zaitcev ac36771
+            resp = self.object_controller.DELETE(req)
Pete Zaitcev ac36771
+            self.assertEquals(resp.status_int, 404)
Pete Zaitcev ac36771
+            objfile = os.path.join(self.testdir, 'sda1',
Pete Zaitcev ac36771
+                storage_directory(object_server.DATADIR, 'p',
Pete Zaitcev ac36771
+                                  hash_path('a', 'c', 'o')),
Pete Zaitcev ac36771
+                timestamp + '.ts')
Pete Zaitcev ac36771
+            self.assert_(os.path.isfile(objfile))
Pete Zaitcev ac36771
+            self.assertEquals(2, calls_made[0])
Pete Zaitcev ac36771
+            self.assertEquals(len(os.listdir(os.path.dirname(objfile))), 1)
Pete Zaitcev ac36771
+
Pete Zaitcev ac36771
+            # The following request should return a 404, as the object should
Pete Zaitcev ac36771
+            # already have been deleted, and it should not have performed a
Pete Zaitcev ac36771
+            # container update because the timestamp is older, or created a
Pete Zaitcev ac36771
+            # tombstone file with this timestamp.
Pete Zaitcev ac36771
+            timestamp = normalize_timestamp(float(timestamp) - 1)
Pete Zaitcev ac36771
+            req = Request.blank('/sda1/p/a/c/o',
Pete Zaitcev ac36771
+                                environ={'REQUEST_METHOD': 'DELETE'},
Pete Zaitcev ac36771
+                                headers={'X-Timestamp': timestamp})
Pete Zaitcev ac36771
+            resp = self.object_controller.DELETE(req)
Pete Zaitcev ac36771
+            self.assertEquals(resp.status_int, 404)
Pete Zaitcev ac36771
+            objfile = os.path.join(self.testdir, 'sda1',
Pete Zaitcev ac36771
+                storage_directory(object_server.DATADIR, 'p',
Pete Zaitcev ac36771
+                                  hash_path('a', 'c', 'o')),
Pete Zaitcev ac36771
+                timestamp + '.ts')
Pete Zaitcev ac36771
+            self.assertFalse(os.path.isfile(objfile))
Pete Zaitcev ac36771
+            self.assertEquals(2, calls_made[0])
Pete Zaitcev ac36771
+            self.assertEquals(len(os.listdir(os.path.dirname(objfile))), 1)
Pete Zaitcev ac36771
+        finally:
Pete Zaitcev ac36771
+            self.object_controller.container_update = orig_cu
Pete Zaitcev ac36771
+
Pete Zaitcev ac36771
     def test_call(self):
Pete Zaitcev ac36771
         """ Test swift.object_server.ObjectController.__call__ """
Pete Zaitcev ac36771
         inbuf = StringIO()