diff options
author | Clay Gerrard <clay.gerrard@gmail.com> | 2014-03-17 16:00:46 -0700 |
---|---|---|
committer | Clay Gerrard <clay.gerrard@gmail.com> | 2014-03-17 16:13:43 -0700 |
commit | 2e8bc44c9d0586813587a5711f8e857a31393326 (patch) | |
tree | a744b2466080dcc904a8c0e622ab657e011a48b5 | |
parent | 6cdf784e2f0e3b37fb040d24b57c9ed92166e6c7 (diff) | |
download | swift-2e8bc44c9d0586813587a5711f8e857a31393326.tar.gz |
Fix race on container recreate
There was a path on container recreate that would sometimes allow db to get
reinitialized without updating put_timestamp. Replication would of course fix
it up, but that node would think it's database was deleted till then desipite
just ok'ing a request with a newer X-Timestamp than the deleted_timestamp on
disk.
Change-Id: I8b98afb2aac2e433b6ecb5c421ba0d778cef42fa
Closes-Bug: #1292784
-rw-r--r-- | swift/container/server.py | 26 | ||||
-rw-r--r-- | test/unit/container/test_server.py | 88 |
2 files changed, 102 insertions, 12 deletions
diff --git a/swift/container/server.py b/swift/container/server.py index 7f6e30ad9..4242465bf 100644 --- a/swift/container/server.py +++ b/swift/container/server.py @@ -227,6 +227,20 @@ class ContainerController(object): return HTTPNoContent(request=req) return HTTPNotFound() + def _update_or_create(self, req, broker, timestamp): + if not os.path.exists(broker.db_file): + try: + broker.initialize(timestamp) + except DatabaseAlreadyExists: + pass + else: + return True # created + created = broker.is_deleted() + broker.update_put_timestamp(timestamp) + if broker.is_deleted(): + raise HTTPConflict(request=req) + return created + @public @timing_stats() def PUT(self, req): @@ -261,17 +275,7 @@ class ContainerController(object): req.headers['x-etag']) return HTTPCreated(request=req) else: # put container - if not os.path.exists(broker.db_file): - try: - broker.initialize(timestamp) - created = True - except DatabaseAlreadyExists: - created = False - else: - created = broker.is_deleted() - broker.update_put_timestamp(timestamp) - if broker.is_deleted(): - return HTTPConflict(request=req) + created = self._update_or_create(req, broker, timestamp) metadata = {} metadata.update( (key, (value, timestamp)) diff --git a/test/unit/container/test_server.py b/test/unit/container/test_server.py index ae6bc487b..1d711dcca 100644 --- a/test/unit/container/test_server.py +++ b/test/unit/container/test_server.py @@ -30,7 +30,8 @@ import simplejson from swift.common.swob import Request, HeaderKeyDict import swift.container from swift.container import server as container_server -from swift.common.utils import normalize_timestamp, mkdirs, public, replication +from swift.common.utils import (normalize_timestamp, mkdirs, public, + replication, lock_parent_directory) from test.unit import fake_http_connect from swift.common.request_helpers import get_sys_meta_prefix @@ -764,6 +765,91 @@ class TestContainerController(unittest.TestCase): resp = req.get_response(self.controller) self.assertEquals(resp.status_int, 404) + def test_DELETE_PUT_recreate(self): + path = '/sda1/p/a/c' + req = Request.blank(path, method='PUT', + headers={'X-Timestamp': '1'}) + resp = req.get_response(self.controller) + self.assertEquals(resp.status_int, 201) + req = Request.blank(path, method='DELETE', + headers={'X-Timestamp': '2'}) + resp = req.get_response(self.controller) + self.assertEquals(resp.status_int, 204) + req = Request.blank(path, method='GET') + resp = req.get_response(self.controller) + self.assertEquals(resp.status_int, 404) # sanity + db = self.controller._get_container_broker('sda1', 'p', 'a', 'c') + self.assertEqual(True, db.is_deleted()) + info = db.get_info() + self.assertEquals(info['put_timestamp'], normalize_timestamp('1')) + self.assertEquals(info['delete_timestamp'], normalize_timestamp('2')) + # recreate + req = Request.blank(path, method='PUT', + headers={'X-Timestamp': '4'}) + resp = req.get_response(self.controller) + self.assertEquals(resp.status_int, 201) + db = self.controller._get_container_broker('sda1', 'p', 'a', 'c') + self.assertEqual(False, db.is_deleted()) + info = db.get_info() + self.assertEquals(info['put_timestamp'], normalize_timestamp('4')) + self.assertEquals(info['delete_timestamp'], normalize_timestamp('2')) + + def test_DELETE_PUT_recreate_replication_race(self): + path = '/sda1/p/a/c' + # create a deleted db + req = Request.blank(path, method='PUT', + headers={'X-Timestamp': '1'}) + resp = req.get_response(self.controller) + self.assertEquals(resp.status_int, 201) + db = self.controller._get_container_broker('sda1', 'p', 'a', 'c') + req = Request.blank(path, method='DELETE', + headers={'X-Timestamp': '2'}) + resp = req.get_response(self.controller) + self.assertEquals(resp.status_int, 204) + req = Request.blank(path, method='GET') + resp = req.get_response(self.controller) + self.assertEquals(resp.status_int, 404) # sanity + self.assertEqual(True, db.is_deleted()) + # now save a copy of this db (and remove it from the "current node") + db = self.controller._get_container_broker('sda1', 'p', 'a', 'c') + db_path = db.db_file + other_path = os.path.join(self.testdir, 'othernode.db') + os.rename(db_path, other_path) + # that should make it missing on this node + req = Request.blank(path, method='GET') + resp = req.get_response(self.controller) + self.assertEquals(resp.status_int, 404) # sanity + + # setup the race in os.path.exists (first time no, then yes) + mock_called = [] + _real_exists = os.path.exists + + def mock_exists(db_path): + rv = _real_exists(db_path) + if not mock_called: + # be as careful as we might hope backend replication can be... + with lock_parent_directory(db_path, timeout=1): + os.rename(other_path, db_path) + mock_called.append((rv, db_path)) + return rv + + req = Request.blank(path, method='PUT', + headers={'X-Timestamp': '4'}) + with mock.patch.object(container_server.os.path, 'exists', + mock_exists): + resp = req.get_response(self.controller) + # db was successfully created + self.assertEqual(resp.status_int // 100, 2) + db = self.controller._get_container_broker('sda1', 'p', 'a', 'c') + self.assertEqual(False, db.is_deleted()) + # mock proves the race + self.assertEqual(mock_called[:2], + [(exists, db.db_file) for exists in (False, True)]) + # info was updated + info = db.get_info() + self.assertEquals(info['put_timestamp'], normalize_timestamp('4')) + self.assertEquals(info['delete_timestamp'], normalize_timestamp('2')) + def test_DELETE_not_found(self): # Even if the container wasn't previously heard of, the container # server will accept the delete and replicate it to where it belongs |