From 3cad20570c79ec4b817b6998dc2e63bed1ea8c1d Mon Sep 17 00:00:00 2001 From: Clay Gerrard Date: Wed, 25 Jun 2014 20:34:39 -0700 Subject: [PATCH] Add X-Backend-Timestamp on more object server responses It's particularly interesting on writes (PUT, POST, DELETE) where the current on-disk timestamp would prevent the object server from serving the incoming request and returns 409 Conflict. The FakeConn has also been updated to respond in kind for 409's on expect and all responses generaly just cause it's good to keep fakes in line with the reals - not that I expected any existing tests to break because of the new headers. Change-Id: Iac6fbd2f872a9521bb2db84a333365b69f54fb6c --- swift/obj/server.py | 15 +++-- test/unit/__init__.py | 6 +- test/unit/obj/test_server.py | 105 +++++++++++++++++++++++++++++++---- 3 files changed, 111 insertions(+), 15 deletions(-) diff --git a/swift/obj/server.py b/swift/obj/server.py index 4a3b9926ff..62e327d7f9 100644 --- a/swift/obj/server.py +++ b/swift/obj/server.py @@ -342,7 +342,9 @@ class ObjectController(object): return HTTPNotFound(request=request) orig_timestamp = Timestamp(orig_metadata.get('X-Timestamp', 0)) if orig_timestamp >= req_timestamp: - return HTTPConflict(request=request) + return HTTPConflict( + request=request, + headers={'X-Backend-Timestamp': orig_timestamp.internal}) metadata = {'X-Timestamp': req_timestamp.internal} metadata.update(val for val in request.headers.iteritems() if is_user_meta('object', val[0])) @@ -402,8 +404,10 @@ class ObjectController(object): return HTTPPreconditionFailed(request=request) orig_timestamp = Timestamp(orig_metadata.get('X-Timestamp', 0)) - if orig_timestamp and orig_timestamp >= req_timestamp: - return HTTPConflict(request=request) + if orig_timestamp >= req_timestamp: + return HTTPConflict( + request=request, + headers={'X-Backend-Timestamp': orig_timestamp.internal}) orig_delete_at = int(orig_metadata.get('X-Delete-At') or 0) upload_expiration = time.time() + self.max_upload_time etag = md5() @@ -598,6 +602,7 @@ class ObjectController(object): response_class = HTTPNoContent else: response_class = HTTPConflict + response_timestamp = max(orig_timestamp, req_timestamp) orig_delete_at = int(orig_metadata.get('X-Delete-At') or 0) try: req_if_delete_at_val = request.headers['x-if-delete-at'] @@ -631,7 +636,9 @@ class ObjectController(object): 'DELETE', account, container, obj, request, HeaderKeyDict({'x-timestamp': req_timestamp.internal}), device, policy_idx) - return response_class(request=request) + return response_class( + request=request, + headers={'X-Backend-Timestamp': response_timestamp.internal}) @public @replication diff --git a/test/unit/__init__.py b/test/unit/__init__.py index a508106017..0ab8194f28 100644 --- a/test/unit/__init__.py +++ b/test/unit/__init__.py @@ -655,7 +655,10 @@ def fake_http_connect(*code_iter, **kwargs): def getexpect(self): if isinstance(self.expect_status, Exception): raise self.expect_status - return FakeConn(self.expect_status) + headers = {} + if self.expect_status == 409: + headers['X-Backend-Timestamp'] = self.timestamp + return FakeConn(self.expect_status, headers=headers) def getheaders(self): etag = self.etag @@ -668,6 +671,7 @@ def fake_http_connect(*code_iter, **kwargs): headers = {'content-length': len(self.body), 'content-type': 'x-application/test', 'x-timestamp': self.timestamp, + 'x-backend-timestamp': self.timestamp, 'last-modified': self.timestamp, 'x-object-meta-test': 'testing', 'x-delete-at': '9876543210', diff --git a/test/unit/obj/test_server.py b/test/unit/obj/test_server.py index 67c24dd743..f215e3458d 100755 --- a/test/unit/obj/test_server.py +++ b/test/unit/obj/test_server.py @@ -30,6 +30,7 @@ from time import gmtime, strftime, time, struct_time from tempfile import mkdtemp from hashlib import md5 import itertools +import tempfile from eventlet import sleep, spawn, wsgi, listen, Timeout, tpool @@ -39,7 +40,7 @@ from test.unit import FakeLogger, debug_logger, mocked_http_conn from test.unit import connect_tcp, readuntil2crlfs, patch_policies from swift.obj import server as object_server from swift.obj import diskfile -from swift.common import utils, storage_policy +from swift.common import utils, storage_policy, bufferedhttp from swift.common.utils import hash_path, mkdirs, normalize_timestamp, \ NullLogger, storage_directory, public, replication from swift.common import constraints @@ -254,9 +255,9 @@ class TestObjectController(unittest.TestCase): def test_POST_old_timestamp(self): ts = time() - timestamp = normalize_timestamp(ts) + orig_timestamp = utils.Timestamp(ts).internal req = Request.blank('/sda1/p/a/c/o', environ={'REQUEST_METHOD': 'PUT'}, - headers={'X-Timestamp': timestamp, + headers={'X-Timestamp': orig_timestamp, 'Content-Type': 'application/x-test', 'X-Object-Meta-1': 'One', 'X-Object-Meta-Two': 'Two'}) @@ -267,13 +268,14 @@ class TestObjectController(unittest.TestCase): # Same timestamp should result in 409 req = Request.blank('/sda1/p/a/c/o', environ={'REQUEST_METHOD': 'POST'}, - headers={'X-Timestamp': timestamp, + headers={'X-Timestamp': orig_timestamp, 'X-Object-Meta-3': 'Three', 'X-Object-Meta-4': 'Four', 'Content-Encoding': 'gzip', 'Content-Type': 'application/x-test'}) resp = req.get_response(self.object_controller) self.assertEquals(resp.status_int, 409) + self.assertEqual(resp.headers['X-Backend-Timestamp'], orig_timestamp) # Earlier timestamp should result in 409 timestamp = normalize_timestamp(ts - 1) @@ -286,6 +288,7 @@ class TestObjectController(unittest.TestCase): 'Content-Type': 'application/x-test'}) resp = req.get_response(self.object_controller) self.assertEquals(resp.status_int, 409) + self.assertEqual(resp.headers['X-Backend-Timestamp'], orig_timestamp) def test_POST_not_exist(self): timestamp = normalize_timestamp(time()) @@ -635,9 +638,10 @@ class TestObjectController(unittest.TestCase): def test_PUT_old_timestamp(self): ts = time() + orig_timestamp = utils.Timestamp(ts).internal req = Request.blank( '/sda1/p/a/c/o', environ={'REQUEST_METHOD': 'PUT'}, - headers={'X-Timestamp': normalize_timestamp(ts), + headers={'X-Timestamp': orig_timestamp, 'Content-Length': '6', 'Content-Type': 'application/octet-stream'}) req.body = 'VERIFY' @@ -651,6 +655,7 @@ class TestObjectController(unittest.TestCase): req.body = 'VERIFY TWO' resp = req.get_response(self.object_controller) self.assertEquals(resp.status_int, 409) + self.assertEqual(resp.headers['X-Backend-Timestamp'], orig_timestamp) req = Request.blank('/sda1/p/a/c/o', environ={'REQUEST_METHOD': 'PUT'}, headers={ @@ -660,6 +665,7 @@ class TestObjectController(unittest.TestCase): req.body = 'VERIFY THREE' resp = req.get_response(self.object_controller) self.assertEquals(resp.status_int, 409) + self.assertEqual(resp.headers['X-Backend-Timestamp'], orig_timestamp) def test_PUT_no_etag(self): req = Request.blank( @@ -1604,10 +1610,10 @@ class TestObjectController(unittest.TestCase): self.assertTrue(os.path.isfile(ts_1000_file)) self.assertEquals(len(os.listdir(os.path.dirname(ts_1000_file))), 1) - timestamp = normalize_timestamp(1002) + orig_timestamp = utils.Timestamp(1002).internal req = Request.blank('/sda1/p/a/c/o', environ={'REQUEST_METHOD': 'PUT'}, headers={ - 'X-Timestamp': timestamp, + 'X-Timestamp': orig_timestamp, 'Content-Type': 'application/octet-stream', 'Content-Length': '4', }) @@ -1619,7 +1625,7 @@ class TestObjectController(unittest.TestCase): self.testdir, 'sda1', storage_directory(diskfile.get_data_dir(0), 'p', hash_path('a', 'c', 'o')), - utils.Timestamp(timestamp).internal + '.data') + orig_timestamp + '.data') self.assertTrue(os.path.isfile(data_1002_file)) self.assertEquals(len(os.listdir(os.path.dirname(data_1002_file))), 1) @@ -1630,6 +1636,7 @@ class TestObjectController(unittest.TestCase): headers={'X-Timestamp': timestamp}) resp = req.get_response(self.object_controller) self.assertEquals(resp.status_int, 409) + self.assertEqual(resp.headers['X-Backend-Timestamp'], orig_timestamp) ts_1001_file = os.path.join( self.testdir, 'sda1', storage_directory(diskfile.get_data_dir(0), 'p', @@ -1658,10 +1665,10 @@ class TestObjectController(unittest.TestCase): # updates, making sure container update is called in the correct # state. start = time() - timestamp = utils.Timestamp(start) + orig_timestamp = utils.Timestamp(start) req = Request.blank('/sda1/p/a/c/o', environ={'REQUEST_METHOD': 'PUT'}, headers={ - 'X-Timestamp': timestamp.internal, + 'X-Timestamp': orig_timestamp.internal, 'Content-Type': 'application/octet-stream', 'Content-Length': '4', }) @@ -1685,6 +1692,8 @@ class TestObjectController(unittest.TestCase): headers={'X-Timestamp': timestamp.internal}) resp = req.get_response(self.object_controller) self.assertEquals(resp.status_int, 409) + self.assertEqual(resp.headers['x-backend-timestamp'], + orig_timestamp.internal) objfile = os.path.join( self.testdir, 'sda1', storage_directory(diskfile.get_data_dir(0), 'p', @@ -4101,5 +4110,81 @@ class TestObjectController(unittest.TestCase): self.assertEquals(resp.status_int, 201) self.assertTrue(os.path.isdir(object_dir)) + +class TestObjectServer(unittest.TestCase): + + def setUp(self): + # dirs + self.tempdir = os.path.join(tempfile.mkdtemp(), 'tmp_test_obj_server') + + self.devices = os.path.join(self.tempdir, 'srv/node') + for device in ('sda1', 'sdb1'): + os.makedirs(os.path.join(self.devices, device)) + + conf = { + 'devices': self.devices, + 'swift_dir': self.tempdir, + 'mount_check': 'false', + } + self.logger = debug_logger('test-object-server') + app = object_server.ObjectController(conf, logger=self.logger) + sock = listen(('127.0.0.1', 0)) + self.server = spawn(wsgi.server, sock, app, utils.NullLogger()) + self.port = sock.getsockname()[1] + + def test_not_found(self): + conn = bufferedhttp.http_connect('127.0.0.1', self.port, 'sda1', '0', + 'GET', '/a/c/o') + resp = conn.getresponse() + self.assertEqual(resp.status, 404) + resp.read() + resp.close() + + def test_expect_on_put(self): + test_body = 'test' + headers = { + 'Expect': '100-continue', + 'Content-Length': len(test_body), + 'X-Timestamp': utils.Timestamp(time()).internal, + } + conn = bufferedhttp.http_connect('127.0.0.1', self.port, 'sda1', '0', + 'PUT', '/a/c/o', headers=headers) + resp = conn.getexpect() + self.assertEqual(resp.status, 100) + conn.send(test_body) + resp = conn.getresponse() + self.assertEqual(resp.status, 201) + resp.read() + resp.close() + + def test_expect_on_put_conflict(self): + test_body = 'test' + put_timestamp = utils.Timestamp(time()) + headers = { + 'Expect': '100-continue', + 'Content-Length': len(test_body), + 'X-Timestamp': put_timestamp.internal, + } + conn = bufferedhttp.http_connect('127.0.0.1', self.port, 'sda1', '0', + 'PUT', '/a/c/o', headers=headers) + resp = conn.getexpect() + self.assertEqual(resp.status, 100) + conn.send(test_body) + resp = conn.getresponse() + self.assertEqual(resp.status, 201) + resp.read() + resp.close() + + # and again with same timestamp + conn = bufferedhttp.http_connect('127.0.0.1', self.port, 'sda1', '0', + 'PUT', '/a/c/o', headers=headers) + resp = conn.getexpect() + self.assertEqual(resp.status, 409) + headers = HeaderKeyDict(resp.getheaders()) + self.assertEqual(headers['X-Backend-Timestamp'], put_timestamp) + resp.read() + resp.close() + + if __name__ == '__main__': unittest.main()