diff --git a/doc/manpages/object-expirer.conf.5 b/doc/manpages/object-expirer.conf.5 index 24a4d0ddf2..42ca4e0756 100644 --- a/doc/manpages/object-expirer.conf.5 +++ b/doc/manpages/object-expirer.conf.5 @@ -211,8 +211,6 @@ This is normally \fBegg:swift#proxy_logging\fR. See proxy-server.conf-sample for .RS 3 .IP \fBinterval\fR Replaces run_pause with the more standard "interval", which means the replicator won't pause unless it takes less than the interval set. The default is 300. -.IP \fBexpiring_objects_account_name\fR -The default is 'expiring_objects'. .IP \fBreport_interval\fR The default is 300 seconds. .IP \fBrequest_tries\fR diff --git a/doc/manpages/object-server.conf.5 b/doc/manpages/object-server.conf.5 index f193304d84..a8c743e6d4 100644 --- a/doc/manpages/object-server.conf.5 +++ b/doc/manpages/object-server.conf.5 @@ -86,11 +86,6 @@ Whether or not check if the devices are mounted to prevent accidentally writing the root device. The default is set to true. .IP \fBdisable_fallocate\fR Disable pre-allocate disk space for a file. The default is false. -.IP \fBexpiring_objects_container_divisor\fR -The default is 86400. -.IP \fBexpiring_objects_account_name\fR -Account name used for legacy style expirer task queue. -The default is 'expiring_objects'. .IP \fBservers_per_port\fR Make object-server run this many worker processes per unique port of "local" ring devices across all storage policies. The default value of 0 disables this diff --git a/doc/manpages/proxy-server.conf.5 b/doc/manpages/proxy-server.conf.5 index 621756a825..6fe7c93163 100644 --- a/doc/manpages/proxy-server.conf.5 +++ b/doc/manpages/proxy-server.conf.5 @@ -96,10 +96,6 @@ disabled by default. .IP \fBkey_file\fR Location of the SSL certificate key file. The default path is /etc/swift/proxy.key. This is disabled by default. -.IP \fBexpiring_objects_container_divisor\fR -The default is 86400. -.IP \fBexpiring_objects_account_name\fR -The default is 'expiring_objects'. .IP \fBlog_name\fR Label used when logging. The default is swift. .IP \fBlog_facility\fR diff --git a/doc/source/config/object_server_config.rst b/doc/source/config/object_server_config.rst index b9d7cadd1e..f85ee5df7e 100644 --- a/doc/source/config/object_server_config.rst +++ b/doc/source/config/object_server_config.rst @@ -703,8 +703,8 @@ interval 300 Time in seconds to report_interval 300 Frequency of status logs in seconds. concurrency 1 Level of concurrency to use to do the work, this value must be set to at least 1 -expiring_objects_account_name expiring_objects name for legacy expirer task queue -dequeue_from_legacy False This service will look for jobs on the legacy expirer task queue. +dequeue_from_legacy False This service will look for jobs on the + legacy expirer task queue. round_robin_task_cache_size 100000 Number of tasks objects to cache before processing. processes 0 How many parts to divide the legacy work into, one part per process that will be doing the work. diff --git a/doc/source/config/proxy_server_config.rst b/doc/source/config/proxy_server_config.rst index d3d9d70c9c..877cd32559 100644 --- a/doc/source/config/proxy_server_config.rst +++ b/doc/source/config/proxy_server_config.rst @@ -124,8 +124,6 @@ disallowed_sections swift.valid_api_versions Allows the abili public calls to /info. You can withhold subsections by separating the dict level with a ".". -expiring_objects_container_divisor 86400 -expiring_objects_account_name expiring_objects nice_priority None Scheduling priority of server processes. Niceness values range from -20 (most diff --git a/etc/object-expirer.conf-sample b/etc/object-expirer.conf-sample index 16dc004039..109c0b5093 100644 --- a/etc/object-expirer.conf-sample +++ b/etc/object-expirer.conf-sample @@ -39,7 +39,6 @@ [object-expirer] # interval = 300.0 -# expiring_objects_account_name = expiring_objects # report_interval = 300.0 # # request_tries is the number of times the expirer's internal client will diff --git a/etc/object-server.conf-sample b/etc/object-server.conf-sample index f7275f6ee0..10ed72be01 100644 --- a/etc/object-server.conf-sample +++ b/etc/object-server.conf-sample @@ -9,8 +9,6 @@ bind_port = 6200 # devices = /srv/node # mount_check = true # disable_fallocate = false -# expiring_objects_container_divisor = 86400 -# expiring_objects_account_name = expiring_objects # # Use an integer to override the number of pre-forked processes that will # accept connections. NOTE: if servers_per_port is set, this setting is diff --git a/etc/proxy-server.conf-sample b/etc/proxy-server.conf-sample index f2cd114309..cbf107762c 100644 --- a/etc/proxy-server.conf-sample +++ b/etc/proxy-server.conf-sample @@ -37,8 +37,6 @@ bind_port = 8080 # cert_file = /etc/swift/proxy.crt # key_file = /etc/swift/proxy.key # -# expiring_objects_container_divisor = 86400 -# expiring_objects_account_name = expiring_objects # # You can specify default log routing here if you want: # log_name = swift diff --git a/swift/common/middleware/slo.py b/swift/common/middleware/slo.py index 8bbb77c5ab..833b5a1bde 100644 --- a/swift/common/middleware/slo.py +++ b/swift/common/middleware/slo.py @@ -362,18 +362,19 @@ from swift.common.utils import get_logger, config_true_value, \ override_bytes_from_content_type, split_path, \ RateLimitedIterator, quote, closing_if_possible, \ LRUCache, StreamingPile, strict_b64decode, Timestamp, friendly_close, \ - get_expirer_container, md5 + md5 from swift.common.registry import register_swift_info from swift.common.request_helpers import SegmentedIterable, \ get_sys_meta_prefix, update_etag_is_at_header, resolve_etag_is_at_header, \ get_container_update_override_key, update_ignore_range_header, \ get_param, get_valid_part_num -from swift.common.constraints import check_utf8, AUTO_CREATE_ACCOUNT_PREFIX +from swift.common.constraints import check_utf8 from swift.common.http import HTTP_NOT_FOUND, HTTP_UNAUTHORIZED from swift.common.wsgi import WSGIContext, make_subrequest, make_env, \ make_pre_authed_request from swift.common.middleware.bulk import get_response_body, \ ACCEPTABLE_FORMATS, Bulk +from swift.obj import expirer from swift.proxy.controllers.base import get_container_info @@ -1322,11 +1323,7 @@ class StaticLargeObject(object): delete_concurrency=delete_concurrency, logger=self.logger) - prefix = AUTO_CREATE_ACCOUNT_PREFIX - self.expiring_objects_account = prefix + ( - conf.get('expiring_objects_account_name') or 'expiring_objects') - self.expiring_objects_container_divisor = int( - conf.get('expiring_objects_container_divisor', 86400)) + self.expirer_config = expirer.ExpirerConfig(conf, logger=self.logger) def handle_multipart_get_or_head(self, req, start_response): """ @@ -1774,13 +1771,14 @@ class StaticLargeObject(object): ts = req.ensure_x_timestamp() expirer_jobs = make_delete_jobs( wsgi_to_str(account), segment_container, segment_objects, ts) - expirer_cont = get_expirer_container( - ts, self.expiring_objects_container_divisor, - wsgi_to_str(account), wsgi_to_str(container), wsgi_to_str(obj)) + expiring_objects_account, expirer_cont = \ + self.expirer_config.get_expirer_account_and_container( + ts, wsgi_to_str(account), wsgi_to_str(container), + wsgi_to_str(obj)) enqueue_req = make_pre_authed_request( req.environ, method='UPDATE', - path="/v1/%s/%s" % (self.expiring_objects_account, expirer_cont), + path="/v1/%s/%s" % (expiring_objects_account, expirer_cont), body=json.dumps(expirer_jobs), headers={'Content-Type': 'application/json', 'X-Backend-Storage-Policy-Index': '0', diff --git a/swift/common/utils/__init__.py b/swift/common/utils/__init__.py index b5639f0cfe..8e0b54b6fe 100644 --- a/swift/common/utils/__init__.py +++ b/swift/common/utils/__init__.py @@ -2928,16 +2928,6 @@ def clean_content_type(value): return value -def get_expirer_container(x_delete_at, expirer_divisor, acc, cont, obj): - """ - Returns an expiring object container name for given X-Delete-At and - (native string) a/c/o. - """ - shard_int = int(hash_path(acc, cont, obj), 16) % 100 - return normalize_delete_at_timestamp( - int(x_delete_at) // expirer_divisor * expirer_divisor - shard_int) - - class _MultipartMimeFileLikeObject(object): def __init__(self, wsgi_input, boundary, input_buffer, read_chunk_size): diff --git a/swift/obj/expirer.py b/swift/obj/expirer.py index b5033ba0c8..3d006f8f74 100644 --- a/swift/obj/expirer.py +++ b/swift/obj/expirer.py @@ -27,10 +27,11 @@ from eventlet.greenpool import GreenPool from swift.common.constraints import AUTO_CREATE_ACCOUNT_PREFIX from swift.common.daemon import Daemon, run_daemon from swift.common.internal_client import InternalClient, UnexpectedResponse +from swift.common import utils from swift.common.utils import get_logger, dump_recon_cache, split_path, \ Timestamp, config_true_value, normalize_delete_at_timestamp, \ RateLimitedIterator, md5, non_negative_float, non_negative_int, \ - parse_content_type, parse_options + parse_content_type, parse_options, config_positive_int_value from swift.common.http import HTTP_NOT_FOUND, HTTP_CONFLICT, \ HTTP_PRECONDITION_FAILED from swift.common.recon import RECON_OBJECT_FILE, DEFAULT_RECON_CACHE_PATH @@ -41,6 +42,117 @@ MAX_OBJECTS_TO_CACHE = 100000 X_DELETE_TYPE = 'text/plain' ASYNC_DELETE_TYPE = 'application/async-deleted' +# expiring_objects_account_name used to be a supported configuration across +# proxy/expirer configs, but AUTO_CREATE_ACCOUNT_PREFIX is configured in +# swift.conf constraints; neither should be changed +EXPIRER_ACCOUNT_NAME = AUTO_CREATE_ACCOUNT_PREFIX + 'expiring_objects' +# Most clusters use the default "expiring_objects_container_divisor" of 86400 +EXPIRER_CONTAINER_DIVISOR = 86400 +EXPIRER_CONTAINER_PER_DIVISOR = 100 + + +class ExpirerConfig(object): + + def __init__(self, conf, container_ring=None, logger=None): + """ + Read the configurable object-expirer values consistently and issue + warnings appropriately when we encounter deprecated options. + + This class is used in multiple contexts on proxy and object servers. + + :param conf: a config dictionary + :param container_ring: optional, required in proxy context to lookup + task container (part, nodes) + :param logger: optional, will create one from the conf if not given + """ + logger = logger or get_logger(conf) + if 'expiring_objects_container_divisor' in conf: + logger.warning( + 'expiring_objects_container_divisor is deprecated') + expirer_divisor = config_positive_int_value( + conf['expiring_objects_container_divisor']) + else: + expirer_divisor = EXPIRER_CONTAINER_DIVISOR + + if 'expiring_objects_account_name' in conf: + logger.warning( + 'expiring_objects_account_name is deprecated; you need ' + 'to migrate to the standard .expiring_objects account') + account_name = (AUTO_CREATE_ACCOUNT_PREFIX + + conf['expiring_objects_account_name']) + else: + account_name = EXPIRER_ACCOUNT_NAME + self.account_name = account_name + self.expirer_divisor = expirer_divisor + self.task_container_per_day = EXPIRER_CONTAINER_PER_DIVISOR + if self.task_container_per_day >= self.expirer_divisor: + msg = 'expiring_objects_container_divisor MUST be greater than 100' + if self.expirer_divisor != 86400: + msg += '; expiring_objects_container_divisor (%s) SHOULD be ' \ + 'default value of %d' \ + % (self.expirer_divisor, EXPIRER_CONTAINER_DIVISOR) + raise ValueError(msg) + self.container_ring = container_ring + + def get_expirer_container(self, x_delete_at, acc, cont, obj): + """ + Returns an expiring object task container name for given X-Delete-At + and (native string) a/c/o. + """ + # offset backwards from the expected day is a hash of size "per day" + shard_int = (int(utils.hash_path(acc, cont, obj), 16) % + self.task_container_per_day) + # even though the attr is named "task_container_per_day" it's actually + # "task_container_per_divisor" if for some reason the deprecated config + # "expirer_divisor" option doesn't have the default value of 86400 + return normalize_delete_at_timestamp( + int(x_delete_at) // self.expirer_divisor * + self.expirer_divisor - shard_int) + + def get_expirer_account_and_container(self, x_delete_at, acc, cont, obj): + """ + Calculates the expected expirer account and container for the target + given the current configuration. + + :returns: a tuple, (account_name, task_container) + """ + task_container = self.get_expirer_container( + x_delete_at, acc, cont, obj) + return self.account_name, task_container + + def is_expected_task_container(self, task_container_int): + """ + Validate the task_container timestamp as an expected value given the + current configuration. Changing the expirer configuration will lead to + orphaned x-delete-at task objects on overwrite, which may stick around + a whole reclaim age. + + :params task_container_int: an int, all task_containers are expected + to be integer timestamps + + :returns: a boolean, True if name fits with the given config + """ + # calculate seconds offset into previous divisor window + r = (task_container_int - 1) % self.expirer_divisor + # seconds offset should be no more than task_container_per_day i.e. + # given % 86400, r==86359 is ok (because 41 is less than 100), but + # 49768 would be unexpected + return self.expirer_divisor - r <= self.task_container_per_day + + def get_delete_at_nodes(self, x_delete_at, acc, cont, obj): + """ + Get the task_container part, nodes, and name. + + :returns: a tuple, (part, nodes, task_container_name) + """ + if not self.container_ring: + raise RuntimeError('%s was not created with container_ring' % self) + account_name, task_container = self.get_expirer_account_and_container( + x_delete_at, acc, cont, obj) + part, nodes = self.container_ring.get_nodes( + account_name, task_container) + return part, nodes, task_container + def build_task_obj(timestamp, target_account, target_container, target_obj, high_precision=False): @@ -157,6 +269,7 @@ class ObjectExpirer(Daemon): self.logger = logger or get_logger(conf, log_route=self.log_route) self.interval = float(conf.get('interval') or 300) self.tasks_per_second = float(conf.get('tasks_per_second', 50.0)) + self.expirer_config = ExpirerConfig(conf, logger=self.logger) self.conf_path = \ self.conf.get('__file__') or '/etc/swift/object-expirer.conf' @@ -301,29 +414,44 @@ class ObjectExpirer(Daemon): only one expirer. """ if self.processes > 0: - yield self.expiring_objects_account, self.process, self.processes + yield (self.expirer_config.account_name, + self.process, self.processes) else: - yield self.expiring_objects_account, 0, 1 + yield self.expirer_config.account_name, 0, 1 - def delete_at_time_of_task_container(self, task_container): + def get_task_containers_to_expire(self, task_account): """ - get delete_at timestamp from task_container name - """ - # task_container name is timestamp - return Timestamp(task_container) - - def iter_task_containers_to_expire(self, task_account): - """ - Yields task_container names under the task_account if the delete at + Collects task_container names under the task_account if the delete at timestamp of task_container is past. """ + container_list = [] + unexpected_task_containers = { + 'examples': [], + 'count': 0, + } for c in self.swift.iter_containers(task_account, prefix=self.task_container_prefix): - task_container = str(c['name']) - timestamp = self.delete_at_time_of_task_container(task_container) - if timestamp > Timestamp.now(): + try: + task_container_int = int(Timestamp(c['name'])) + except ValueError: + self.logger.error('skipping invalid task container: %s/%s', + task_account, c['name']) + continue + if not self.expirer_config.is_expected_task_container( + task_container_int): + unexpected_task_containers['count'] += 1 + if unexpected_task_containers['count'] < 5: + unexpected_task_containers['examples'].append(c['name']) + if task_container_int > Timestamp.now(): break - yield task_container + container_list.append(str(task_container_int)) + + if unexpected_task_containers['count']: + self.logger.info( + 'processing %s unexpected task containers (e.g. %s)', + unexpected_task_containers['count'], + ' '.join(unexpected_task_containers['examples'])) + return container_list def get_delay_reaping(self, target_account, target_container): return get_delay_reaping(self.delay_reaping_times, target_account, @@ -470,7 +598,7 @@ class ObjectExpirer(Daemon): task_account_container_list = \ [(task_account, task_container) for task_container in - self.iter_task_containers_to_expire(task_account)] + self.get_task_containers_to_expire(task_account)] # delete_task_iter is a generator to yield a dict of # task_account, task_container, task_object, delete_timestamp, diff --git a/swift/obj/server.py b/swift/obj/server.py index b4bf640f74..b53f419a0d 100644 --- a/swift/obj/server.py +++ b/swift/obj/server.py @@ -31,7 +31,7 @@ from eventlet.greenthread import spawn from swift.common.utils import public, get_logger, \ config_true_value, config_percent_value, timing_stats, replication, \ normalize_delete_at_timestamp, get_log_line, Timestamp, \ - get_expirer_container, parse_mime_headers, \ + parse_mime_headers, \ iter_multipart_mime_documents, extract_swift_bytes, safe_json_loads, \ config_auto_int_value, split_path, get_redirect_data, \ normalize_timestamp, md5, parse_options, CooperativeIterator @@ -44,7 +44,7 @@ from swift.common.exceptions import ConnectionTimeout, DiskFileQuarantined, \ ChunkReadError, DiskFileXattrNotSupported from swift.common.request_helpers import resolve_ignore_range_header, \ OBJECT_SYSMETA_CONTAINER_UPDATE_OVERRIDE_PREFIX -from swift.obj import ssync_receiver +from swift.obj import ssync_receiver, expirer from swift.common.http import is_success, HTTP_MOVED_PERMANENTLY from swift.common.base_storage_server import BaseStorageServer from swift.common.header_key_dict import HeaderKeyDict @@ -181,10 +181,7 @@ class ObjectController(BaseStorageServer): self.allowed_headers.add(header) self.auto_create_account_prefix = AUTO_CREATE_ACCOUNT_PREFIX - self.expiring_objects_account = self.auto_create_account_prefix + \ - (conf.get('expiring_objects_account_name') or 'expiring_objects') - self.expiring_objects_container_divisor = \ - int(conf.get('expiring_objects_container_divisor') or 86400) + self.expirer_config = expirer.ExpirerConfig(conf, logger=self.logger) # Initialization was successful, so now apply the network chunk size # parameter as the default read / write buffer size for the network # sockets. @@ -462,11 +459,9 @@ class ObjectController(BaseStorageServer): if config_true_value( request.headers.get('x-backend-replication', 'f')): return - delete_at = normalize_delete_at_timestamp(delete_at) - updates = [(None, None)] - partition = None - hosts = contdevices = [None] + delete_at = normalize_delete_at_timestamp(delete_at) + headers_in = request.headers headers_out = HeaderKeyDict({ # system accounts are always Policy-0 @@ -474,26 +469,42 @@ class ObjectController(BaseStorageServer): 'x-timestamp': request.timestamp.internal, 'x-trans-id': headers_in.get('x-trans-id', '-'), 'referer': request.as_referer()}) + + expiring_objects_account_name, delete_at_container = \ + self.expirer_config.get_expirer_account_and_container( + delete_at, account, container, obj) if op != 'DELETE': hosts = headers_in.get('X-Delete-At-Host', None) if hosts is None: # If header is missing, no update needed as sufficient other # object servers should perform the required update. return - delete_at_container = headers_in.get('X-Delete-At-Container', None) - if not delete_at_container: - # older proxy servers did not send X-Delete-At-Container so for - # backwards compatibility calculate the value here, but also - # log a warning because this is prone to inconsistent - # expiring_objects_container_divisor configurations. - # See https://bugs.launchpad.net/swift/+bug/1187200 - self.logger.warning( - 'X-Delete-At-Container header must be specified for ' - 'expiring objects background %s to work properly. Making ' - 'best guess as to the container name for now.' % op) - delete_at_container = get_expirer_container( - delete_at, self.expiring_objects_container_divisor, - account, container, obj) + + proxy_delete_at_container = headers_in.get( + 'X-Delete-At-Container', None) + if delete_at_container != proxy_delete_at_container: + if not proxy_delete_at_container: + # We carry this warning around for pre-2013 proxies + self.logger.warning( + 'X-Delete-At-Container header must be specified for ' + 'expiring objects background %s to work properly. ' + 'Making best guess as to the container name ' + 'for now.', op) + proxy_delete_at_container = delete_at_container + else: + # Inconsistent configuration may lead to orphaned expirer + # task queue objects when X-Delete-At is updated, which can + # stick around for a whole reclaim age. + self.logger.debug( + 'Proxy X-Delete-At-Container %r does not match ' + 'expected %r for current expirer_config.', + proxy_delete_at_container, delete_at_container) + # it's not possible to say which is "more correct", this will + # at least match the host/part/device + delete_at_container = normalize_delete_at_timestamp( + proxy_delete_at_container) + + # new updates need to enqueue new x-delete-at partition = headers_in.get('X-Delete-At-Partition', None) contdevices = headers_in.get('X-Delete-At-Device', '') updates = [upd for upd in @@ -512,23 +523,13 @@ class ObjectController(BaseStorageServer): request.headers.get( 'X-Backend-Clean-Expiring-Object-Queue', 't')): return - - # DELETEs of old expiration data have no way of knowing what the - # old X-Delete-At-Container was at the time of the initial setting - # of the data, so a best guess is made here. - # Worst case is a DELETE is issued now for something that doesn't - # exist there and the original data is left where it is, where - # it will be ignored when the expirer eventually tries to issue the - # object DELETE later since the X-Delete-At value won't match up. - delete_at_container = get_expirer_container( - delete_at, self.expiring_objects_container_divisor, - account, container, obj) - delete_at_container = normalize_delete_at_timestamp( - delete_at_container) + # DELETE op always go directly to async_pending + partition = None + updates = [(None, None)] for host, contdevice in updates: self.async_update( - op, self.expiring_objects_account, delete_at_container, + op, expiring_objects_account_name, delete_at_container, build_task_obj(delete_at, account, container, obj), host, partition, contdevice, headers_out, objdevice, policy) diff --git a/swift/proxy/controllers/obj.py b/swift/proxy/controllers/obj.py index 61523fa814..7ac2d70564 100644 --- a/swift/proxy/controllers/obj.py +++ b/swift/proxy/controllers/obj.py @@ -42,7 +42,7 @@ from eventlet.timeout import Timeout from swift.common.utils import ( clean_content_type, config_true_value, ContextPool, csv_append, GreenAsyncPile, GreenthreadSafeIterator, Timestamp, WatchdogTimeout, - normalize_delete_at_timestamp, public, get_expirer_container, + normalize_delete_at_timestamp, public, document_iters_to_http_response_body, parse_content_range, quorum_size, reiterate, close_if_possible, safe_json_loads, md5, NamespaceBoundList, CooperativeIterator) @@ -630,13 +630,10 @@ class BaseObjectController(Controller): append_log_info(req.environ, 'x-delete-at:%s' % x_delete_at) - delete_at_container = get_expirer_container( - x_delete_at, self.app.expiring_objects_container_divisor, - self.account_name, self.container_name, self.object_name) - - delete_at_part, delete_at_nodes = \ - self.app.container_ring.get_nodes( - self.app.expiring_objects_account, delete_at_container) + delete_at_part, delete_at_nodes, delete_at_container = \ + self.app.expirer_config.get_delete_at_nodes( + x_delete_at, self.account_name, self.container_name, + self.object_name) return req, delete_at_container, delete_at_part, delete_at_nodes diff --git a/swift/proxy/server.py b/swift/proxy/server.py index 732c5823a4..8646cc5bfb 100644 --- a/swift/proxy/server.py +++ b/swift/proxy/server.py @@ -51,6 +51,7 @@ from swift.common.swob import HTTPBadRequest, HTTPForbidden, \ wsgi_to_str from swift.common.exceptions import APIVersionError from swift.common.wsgi import run_wsgi +from swift.obj import expirer # List of entry points for mandatory middlewares. @@ -264,10 +265,8 @@ class Application(object): config_true_value(conf.get('account_autocreate', 'no')) self.auto_create_account_prefix = \ constraints.AUTO_CREATE_ACCOUNT_PREFIX - self.expiring_objects_account = self.auto_create_account_prefix + \ - (conf.get('expiring_objects_account_name') or 'expiring_objects') - self.expiring_objects_container_divisor = \ - int(conf.get('expiring_objects_container_divisor') or 86400) + self.expirer_config = expirer.ExpirerConfig( + conf, container_ring=self.container_ring, logger=self.logger) self.max_containers_per_account = \ int(conf.get('max_containers_per_account') or 0) self.max_containers_whitelist = [ diff --git a/test/unit/common/middleware/test_slo.py b/test/unit/common/middleware/test_slo.py index 89b49b9044..a7fbe0b64c 100644 --- a/test/unit/common/middleware/test_slo.py +++ b/test/unit/common/middleware/test_slo.py @@ -33,7 +33,7 @@ from swift.common.swob import Request, HTTPException, str_to_wsgi, \ bytes_to_wsgi from swift.common.utils import quote, closing_if_possible, close_if_possible, \ parse_content_type, iter_multipart_mime_documents, parse_mime_headers, \ - Timestamp, get_expirer_container, md5 + Timestamp, md5 from test.unit.common.middleware.helpers import FakeSwift @@ -1609,8 +1609,8 @@ class TestSloDeleteManifest(SloTestCase): def test_handle_async_delete_whole(self): self.slo.allow_async_delete = True now = Timestamp(time.time()) - exp_obj_cont = get_expirer_container( - int(now), 86400, 'AUTH_test', 'deltest', 'man-all-there') + exp_obj_cont = self.slo.expirer_config.get_expirer_container( + int(now), 'AUTH_test', 'deltest', 'man-all-there') self.app.register( 'UPDATE', '/v1/.expiring_objects/%s' % exp_obj_cont, swob.HTTPNoContent, {}, None) @@ -1663,8 +1663,8 @@ class TestSloDeleteManifest(SloTestCase): unicode_acct = u'AUTH_test-un\u00efcode' wsgi_acct = bytes_to_wsgi(unicode_acct.encode('utf-8')) now = Timestamp(time.time()) - exp_obj_cont = get_expirer_container( - int(now), 86400, unicode_acct, 'deltest', 'man-all-there') + exp_obj_cont = self.slo.expirer_config.get_expirer_container( + int(now), unicode_acct, 'deltest', 'man-all-there') self.app.register( 'UPDATE', '/v1/.expiring_objects/%s' % exp_obj_cont, swob.HTTPNoContent, {}, None) @@ -1737,8 +1737,8 @@ class TestSloDeleteManifest(SloTestCase): unicode_acct = u'AUTH_test-un\u00efcode' wsgi_acct = bytes_to_wsgi(unicode_acct.encode('utf-8')) now = Timestamp(time.time()) - exp_obj_cont = get_expirer_container( - int(now), 86400, unicode_acct, u'\N{SNOWMAN}', 'same-container') + exp_obj_cont = self.slo.expirer_config.get_expirer_container( + int(now), unicode_acct, u'\N{SNOWMAN}', 'same-container') self.app.register( 'UPDATE', '/v1/.expiring_objects/%s' % exp_obj_cont, swob.HTTPNoContent, {}, None) @@ -1802,6 +1802,32 @@ class TestSloDeleteManifest(SloTestCase): 'storage_policy_index': 0}, ]) + def test_handle_async_delete_alternative_expirer_config(self): + # Test that SLO async delete operation will send UPDATE requests to the + # alternative expirer container when using a non-default account name + # and container divisor. + slo_conf = { + 'expiring_objects_account_name': 'exp', + 'expiring_objects_container_divisor': '5400', + } + self.slo = slo.filter_factory(slo_conf)(self.app) + now = Timestamp(time.time()) + exp_obj_cont = self.slo.expirer_config.get_expirer_container( + int(now), 'AUTH_test', 'deltest', 'man-all-there') + self.app.register( + 'UPDATE', '/v1/.exp/%s' % exp_obj_cont, + swob.HTTPNoContent, {}, None) + req = Request.blank( + '/v1/AUTH_test/deltest/man-all-there', + method='DELETE') + with patch('swift.common.utils.Timestamp.now', return_value=now): + self.slo.handle_async_delete(req) + self.assertEqual([ + ('GET', '/v1/AUTH_test/deltest/man-all-there' + '?multipart-manifest=get'), + ('UPDATE', '/v1/.exp/%s' % exp_obj_cont), + ], self.app.calls) + def test_handle_async_delete_nested(self): self.slo.allow_async_delete = True req = Request.blank( diff --git a/test/unit/common/test_utils.py b/test/unit/common/test_utils.py index 39b2eae43e..5bf0cb762b 100644 --- a/test/unit/common/test_utils.py +++ b/test/unit/common/test_utils.py @@ -4213,16 +4213,6 @@ class TestParseContentDisposition(unittest.TestCase): self.assertEqual(attrs, {'name': 'somefile', 'filename': 'test.html'}) -class TestGetExpirerContainer(unittest.TestCase): - - @mock.patch.object(utils, 'hash_path', return_value=hex(101)[2:]) - def test_get_expirer_container(self, mock_hash_path): - container = utils.get_expirer_container(1234, 20, 'a', 'c', 'o') - self.assertEqual(container, '0000001219') - container = utils.get_expirer_container(1234, 200, 'a', 'c', 'o') - self.assertEqual(container, '0000001199') - - class TestIterMultipartMimeDocuments(unittest.TestCase): def test_bad_start(self): diff --git a/test/unit/obj/test_expirer.py b/test/unit/obj/test_expirer.py index 60867b2c2a..0d86fa2660 100644 --- a/test/unit/obj/test_expirer.py +++ b/test/unit/obj/test_expirer.py @@ -32,6 +32,7 @@ from swift.common import internal_client, utils, swob from swift.common.utils import Timestamp from swift.common.swob import Response from swift.obj import expirer, diskfile +from swift.obj.expirer import ExpirerConfig def not_random(): @@ -114,6 +115,166 @@ class FakeInternalClient(object): ) +class TestExpirerConfig(TestCase): + def setUp(self): + self.logger = debug_logger() + + @mock.patch('swift.obj.expirer.utils.hash_path', return_value=hex(101)[2:]) + def test_get_expirer_container(self, mock_hash_path): + expirer_config = ExpirerConfig( + {'expiring_objects_container_divisor': 200}, logger=self.logger) + container = expirer_config.get_expirer_container( + 12340, 'a', 'c', 'o') + self.assertEqual(container, '0000012199') + expirer_config = ExpirerConfig( + {'expiring_objects_container_divisor': 2000}, logger=self.logger) + container = expirer_config.get_expirer_container( + 12340, 'a', 'c', 'o') + self.assertEqual(container, '0000011999') + + def test_is_expected_task_container(self): + expirer_config = ExpirerConfig({}, logger=self.logger) + self.assertEqual('.expiring_objects', expirer_config.account_name) + self.assertEqual(86400, expirer_config.expirer_divisor) + self.assertEqual(100, expirer_config.task_container_per_day) + self.assertFalse(expirer_config.is_expected_task_container(172801)) + self.assertTrue(expirer_config.is_expected_task_container(172800)) + self.assertTrue(expirer_config.is_expected_task_container(172799)) + self.assertTrue(expirer_config.is_expected_task_container(172701)) + self.assertFalse(expirer_config.is_expected_task_container(172700)) + self.assertFalse(expirer_config.is_expected_task_container(86401)) + self.assertTrue(expirer_config.is_expected_task_container(86400)) + self.assertTrue(expirer_config.is_expected_task_container(86399)) + self.assertTrue(expirer_config.is_expected_task_container(86301)) + self.assertFalse(expirer_config.is_expected_task_container(86300)) + + expirer_config = ExpirerConfig({ + 'expiring_objects_container_divisor': 1000, + }, logger=self.logger) + self.assertEqual('.expiring_objects', expirer_config.account_name) + self.assertEqual(1000, expirer_config.expirer_divisor) + self.assertEqual(100, expirer_config.task_container_per_day) + self.assertFalse(expirer_config.is_expected_task_container(2001)) + self.assertTrue(expirer_config.is_expected_task_container(2000)) + self.assertTrue(expirer_config.is_expected_task_container(1999)) + self.assertTrue(expirer_config.is_expected_task_container(1901)) + self.assertFalse(expirer_config.is_expected_task_container(1900)) + self.assertFalse(expirer_config.is_expected_task_container(1001)) + self.assertTrue(expirer_config.is_expected_task_container(1000)) + self.assertTrue(expirer_config.is_expected_task_container(999)) + self.assertTrue(expirer_config.is_expected_task_container(901)) + self.assertFalse(expirer_config.is_expected_task_container(900)) + + def test_get_expirer_container_legacy_config(self): + per_divisor = 100 + expirer_config = ExpirerConfig({ + 'expiring_objects_container_divisor': 86400 * 2, + }, logger=self.logger) + delete_at = time() + found = set() + for i in range(per_divisor * 10): + c = expirer_config.get_expirer_container( + delete_at, 'a', 'c', 'obj%s' % i) + found.add(c) + self.assertEqual(per_divisor, len(found)) + + def test_get_expirer_config_default(self): + conf = {} + config = ExpirerConfig(conf, logger=self.logger) + self.assertEqual('.expiring_objects', config.account_name) + self.assertEqual(86400, config.expirer_divisor) + self.assertEqual(100, config.task_container_per_day) + self.assertFalse(self.logger.all_log_lines()) + + def test_get_expirer_config_legacy(self): + conf = { + 'expiring_objects_account_name': 'exp', + 'expiring_objects_container_divisor': '1000', + } + config = ExpirerConfig(conf, logger=self.logger) + self.assertEqual('.exp', config.account_name) + self.assertEqual(1000, config.expirer_divisor) + self.assertEqual(100, config.task_container_per_day) + self.assertEqual([ + 'expiring_objects_container_divisor is deprecated', + 'expiring_objects_account_name is deprecated; you need to ' + 'migrate to the standard .expiring_objects account', + ], self.logger.get_lines_for_level('warning')) + + def test_get_expirer_config_legacy_no_logger_given(self): + # verify that a logger is constructed from conf if not given + conf = { + 'expiring_objects_account_name': 'exp', + 'expiring_objects_container_divisor': '1000', + 'log_route': 'test', + } + + with mock.patch( + 'swift.obj.expirer.get_logger', return_value=self.logger + ) as mock_get_logger: + config = ExpirerConfig(conf, logger=None) + self.assertEqual('.exp', config.account_name) + self.assertEqual(1000, config.expirer_divisor) + self.assertEqual(100, config.task_container_per_day) + self.assertEqual([ + 'expiring_objects_container_divisor is deprecated', + 'expiring_objects_account_name is deprecated; you need to ' + 'migrate to the standard .expiring_objects account', + ], self.logger.get_lines_for_level('warning')) + self.assertEqual([mock.call(conf)], mock_get_logger.call_args_list) + + def test_get_expirer_account_and_container_default(self): + expirer_config = ExpirerConfig({}, logger=self.logger) + delete_at = time() + account, container = \ + expirer_config.get_expirer_account_and_container( + delete_at, 'a', 'c', 'o') + self.assertEqual('.expiring_objects', account) + self.assertTrue(expirer_config.is_expected_task_container( + int(container))) + + def test_get_expirer_account_and_container_legacy(self): + expirer_config = ExpirerConfig({ + 'expiring_objects_account_name': 'exp', + 'expiring_objects_container_divisor': 1000, + }, logger=self.logger) + delete_at = time() + account, container = expirer_config.get_expirer_account_and_container( + delete_at, 'a', 'c', 'o') + self.assertEqual('.exp', account) + self.assertEqual(1000, expirer_config.expirer_divisor) + self.assertEqual(100, expirer_config.task_container_per_day) + self.assertTrue(expirer_config.is_expected_task_container( + int(container))) + + def test_get_delete_at_nodes(self): + container_ring = FakeRing() + # it seems default FakeRing is very predictable + self.assertEqual(32, container_ring._part_shift) + self.assertEqual(3, container_ring.replicas) + self.assertEqual(3, len(container_ring.devs)) + expirer_config = ExpirerConfig( + {}, logger=self.logger, container_ring=container_ring) + delete_at = time() + part, nodes, task_container = expirer_config.get_delete_at_nodes( + delete_at, 'a', 'c', 'o2') + self.assertEqual(0, part) # only one part + self.assertEqual([ + dict(n, index=i) for i, n in enumerate(container_ring.devs) + ], nodes) # assigned to all ring devices + self.assertTrue(expirer_config.is_expected_task_container( + int(task_container))) + + def test_get_delete_at_nodes_no_ring(self): + expirer_config = ExpirerConfig({}, logger=self.logger) + delete_at = time() + with self.assertRaises(RuntimeError) as ctx: + expirer_config.get_delete_at_nodes( + delete_at, 'a', 'c', 'o2') + self.assertIn('ExpirerConfig', str(ctx.exception)) + self.assertIn('container_ring', str(ctx.exception)) + + class TestExpirerHelpers(TestCase): def test_add_expirer_bytes_to_ctype(self): @@ -225,14 +386,15 @@ class TestObjectExpirer(TestCase): internal_client = None def get_expirer_container(self, delete_at, target_account='a', - target_container='c', target_object='o', - expirer_divisor=86400): + target_container='c', target_object='o'): # the actual target a/c/o used only matters for consistent # distribution, tests typically only create one task container per-day, # but we want the task container names to be realistic - return utils.get_expirer_container( - delete_at, expirer_divisor, - target_account, target_container, target_object) + expirer = getattr(self, 'expirer', None) + expirer_config = expirer.expirer_config if expirer else \ + ExpirerConfig(self.conf, self.logger) + return expirer_config.get_expirer_container( + delete_at, target_account, target_container, target_object) def setUp(self): global not_sleep @@ -250,9 +412,11 @@ class TestObjectExpirer(TestCase): self.now = now = int(time()) self.empty_time = str(now - 864000) - self.empty_time_container = self.get_expirer_container(self.empty_time) + self.empty_time_container = self.get_expirer_container( + self.empty_time) self.past_time = str(now - 86400) - self.past_time_container = self.get_expirer_container(self.past_time) + self.past_time_container = self.get_expirer_container( + self.past_time) self.just_past_time = str(now - 1) self.just_past_time_container = self.get_expirer_container( self.just_past_time) @@ -260,7 +424,7 @@ class TestObjectExpirer(TestCase): self.future_time_container = self.get_expirer_container( self.future_time) # Dummy task queue for test - self.fake_swift = FakeInternalClient({ + self._setup_fake_swift({ '.expiring_objects': { # this task container will be checked self.empty_time_container: [], @@ -285,8 +449,6 @@ class TestObjectExpirer(TestCase): self.future_time_container: [ self.future_time + '-a11/c11/o11']} }) - self.expirer = expirer.ObjectExpirer(self.conf, logger=self.logger, - swift=self.fake_swift) # map of times to target object paths which should be expirerd now self.expired_target_paths = { @@ -303,6 +465,12 @@ class TestObjectExpirer(TestCase): ], } + def _setup_fake_swift(self, aco_dict): + self.fake_swift = FakeInternalClient(aco_dict) + self.expirer = expirer.ObjectExpirer(self.conf, logger=self.logger, + swift=self.fake_swift) + self.expirer_config = self.expirer.expirer_config + def make_fake_ic(self, app): app._pipeline_final_app = mock.MagicMock() return internal_client.InternalClient(None, 'fake-ic', 1, app=app) @@ -320,7 +488,7 @@ class TestObjectExpirer(TestCase): use_replication_network=True, global_conf={'log_name': 'object-expirer-ic'})]) self.assertEqual(self.logger.get_lines_for_level('warning'), []) - self.assertEqual(x.expiring_objects_account, '.expiring_objects') + self.assertEqual(x.expirer_config.account_name, '.expiring_objects') self.assertIs(x.swift, self.fake_swift) def test_init_default_round_robin_cache_default(self): @@ -1054,13 +1222,6 @@ class TestObjectExpirer(TestCase): results = [_ for _ in x.iter_task_accounts_to_expire()] self.assertEqual(results, [('.expiring_objects', 1, 2)]) - def test_delete_at_time_of_task_container(self): - x = expirer.ObjectExpirer(self.conf, logger=self.logger, - swift=self.fake_swift) - self.assertEqual(x.delete_at_time_of_task_container('0000'), 0) - self.assertEqual(x.delete_at_time_of_task_container('0001'), 1) - self.assertEqual(x.delete_at_time_of_task_container('1000'), 1000) - def test_run_once_nothing_to_do(self): x = expirer.ObjectExpirer(self.conf, logger=self.logger, swift=self.fake_swift) @@ -1123,6 +1284,102 @@ class TestObjectExpirer(TestCase): 'Pass completed in 0s; 0 objects expired', ]) + def test_get_task_containers_unexpected_container(self): + expected = self.get_expirer_container(time()) + unexpected = str(int(expected) - 200) + for name in (expected, unexpected): + self.assertTrue(name.isdigit()) # sanity + + container_list = [{'name': unexpected}, {'name': expected}] + with mock.patch.object(self.expirer.swift, 'iter_containers', + return_value=container_list): + self.assertEqual( + self.expirer.get_task_containers_to_expire('task_account'), + [unexpected, expected]) + self.assertEqual(self.expirer.logger.all_log_lines(), {'info': [ + 'processing 1 unexpected task containers (e.g. %s)' % unexpected, + ]}) + + def test_get_task_containers_invalid_container(self): + ok_names = ['86301', '86400'] + bad_names = ['-1', 'rogue'] + unexpected = ['86300', '86401'] + + container_list = [{'name': name} for name in bad_names] + \ + [{'name': name} for name in ok_names] + \ + [{'name': name} for name in unexpected] + with mock.patch.object(self.expirer.swift, 'iter_containers', + return_value=container_list): + self.assertEqual( + self.expirer.get_task_containers_to_expire('task_account'), + ok_names + unexpected) + lines = self.expirer.logger.get_lines_for_level('error') + self.assertEqual(lines, [ + 'skipping invalid task container: task_account/-1', + 'skipping invalid task container: task_account/rogue', + ]) + lines = self.expirer.logger.get_lines_for_level('info') + self.assertEqual(lines, [ + 'processing 2 unexpected task containers (e.g. 86300 86401)' + ]) + + def _expirer_run_once_with_mocks(self, now=None, stub_pop_queue=None): + """ + call self.expirer.run_once() with some things (optionally) stubbed out + """ + now = now or time() + # IME abuse of MagicMock's call tracking will pop OOM + memory_efficient_noop = lambda *args, **kwargs: None + stub_pop_queue = stub_pop_queue or memory_efficient_noop + memory_efficient_time = lambda: now + with mock.patch.object(self.expirer, 'pop_queue', stub_pop_queue), \ + mock.patch('eventlet.sleep', memory_efficient_noop), \ + mock.patch('swift.common.utils.timestamp.time.time', + memory_efficient_time), \ + mock.patch('swift.obj.expirer.time', memory_efficient_time): + self.expirer.run_once() + + def test_run_once_with_invalid_container(self): + now = time() + t0 = Timestamp(now - 100000) + t1 = Timestamp(now - 10000) + normal_task_container = self.get_expirer_container(t0) + self.assertTrue(normal_task_container.isdigit()) + next_task_container = self.get_expirer_container(t1) + for name in (normal_task_container, next_task_container): + self.assertTrue(name.isdigit()) # sanity + + strange_task_container = normal_task_container + '-crazy' + self.assertFalse(strange_task_container.isdigit()) + + task_per_container = 3 + self._setup_fake_swift({ + '.expiring_objects': { + normal_task_container: [ + expirer.build_task_obj(t0, 'a', 'c1', 'o%s' % i) + for i in range(task_per_container) + ], + strange_task_container: [ + expirer.build_task_obj(t0, 'a', 'c2', 'o%s' % i) + for i in range(task_per_container) + ], + next_task_container: [ + expirer.build_task_obj(t1, 'a', 'c3', 'o%s' % i) + for i in range(task_per_container) + ], + } + }) + # sanity + self.assertEqual( + sorted(self.expirer.swift.aco_dict['.expiring_objects'].keys()), [ + normal_task_container, + strange_task_container, + next_task_container, + ]) + self._expirer_run_once_with_mocks(now=now) + # we processed all tasks in all valid containers + self.assertEqual(task_per_container * 2, self.expirer.report_objects) + def test_iter_task_to_expire(self): # In this test, all tasks are assigned to the tested expirer my_index = 0 diff --git a/test/unit/obj/test_server.py b/test/unit/obj/test_server.py index 3d8c745f9e..754ebf0c9a 100644 --- a/test/unit/obj/test_server.py +++ b/test/unit/obj/test_server.py @@ -39,6 +39,7 @@ from eventlet.green.http import client as http_client from swift import __version__ as swift_version from swift.common.http import is_success +from swift.obj.expirer import ExpirerConfig from test import listen_zero, BaseTestCase from test.debug_logger import debug_logger from test.unit import mocked_http_conn, \ @@ -1388,10 +1389,10 @@ class TestObjectController(BaseTestCase): def _update_delete_at_headers(self, headers, a='a', c='c', o='o', node_count=1): delete_at = headers['X-Delete-At'] - delete_at_container = utils.get_expirer_container(delete_at, 84600, - a, c, o) - part, nodes = self.container_ring.get_nodes( - '.expiring_objects', delete_at_container) + expirer_config = ExpirerConfig( + self.conf, logger=self.logger, container_ring=self.container_ring) + part, nodes, delete_at_container = expirer_config.get_delete_at_nodes( + delete_at, a, c, o) # proxy assigns each replica a node, index 0 for test stability nodes = nodes[:node_count] headers.update({ @@ -5769,7 +5770,7 @@ class TestObjectController(BaseTestCase): self.object_controller._diskfile_router = diskfile.DiskFileRouter( self.conf, self.object_controller.logger) policy = random.choice(list(POLICIES)) - self.object_controller.expiring_objects_account = 'exp' + self.object_controller.expirer_config.account_name = 'exp' http_connect_args = [] @@ -6806,9 +6807,10 @@ class TestObjectController(BaseTestCase): self.object_controller.delete_at_update( 'DELETE', 12345678901, 'a', 'c', 'o', req, 'sda1', policy) expiring_obj_container = given_args.pop(2) - expected_exp_cont = utils.get_expirer_container( - utils.normalize_delete_at_timestamp(12345678901), - 86400, 'a', 'c', 'o') + expected_exp_cont = \ + self.object_controller.expirer_config.get_expirer_container( + utils.normalize_delete_at_timestamp(12345678901), + 'a', 'c', 'o') self.assertEqual(expiring_obj_container, expected_exp_cont) self.assertEqual(given_args, [ @@ -6886,6 +6888,9 @@ class TestObjectController(BaseTestCase): 'X-Backend-Storage-Policy-Index': int(policy)}) self.object_controller.delete_at_update('PUT', 2, 'a', 'c', 'o', req, 'sda1', policy) + # proxy servers started sending the x-delete-at-container along with + # host/part/device in 2013 Ia0081693f01631d3f2a59612308683e939ced76a + # it may be no longer necessary to say "warning: upgrade faster" self.assertEqual( self.logger.get_lines_for_level('warning'), ['X-Delete-At-Container header must be specified for expiring ' @@ -6906,6 +6911,60 @@ class TestObjectController(BaseTestCase): 'referer': 'PUT http://localhost/v1/a/c/o'}), 'sda1', policy]) + def test_delete_at_update_put_with_info_but_wrong_container(self): + # Same as test_delete_at_update_put_with_info, but the + # X-Delete-At-Container is "wrong" + policy = random.choice(list(POLICIES)) + given_args = [] + + def fake_async_update(*args): + given_args.extend(args) + + self.object_controller.async_update = fake_async_update + self.object_controller.logger = self.logger + delete_at = time() + req_headers = { + 'X-Timestamp': 1, + 'X-Trans-Id': '1234', + 'X-Delete-At': delete_at, + 'X-Backend-Storage-Policy-Index': int(policy), + } + self._update_delete_at_headers(req_headers) + delete_at = str(int(time() + 30)) + expected_container = \ + self.object_controller.expirer_config.get_expirer_container( + delete_at, 'a', 'c', 'o') + unexpected_container = str(int(delete_at) + 100) + req_headers['X-Delete-At-Container'] = unexpected_container + req = Request.blank( + '/v1/a/c/o', + environ={'REQUEST_METHOD': 'PUT'}, + headers=req_headers) + self.object_controller.delete_at_update('PUT', delete_at, + 'a', 'c', 'o', + req, 'sda1', policy) + self.assertEqual({'debug': [ + "Proxy X-Delete-At-Container '%s' does not match expected " + "'%s' for current expirer_config." % (unexpected_container, + expected_container) + ]}, self.logger.all_log_lines()) + self.assertEqual( + given_args, [ + 'PUT', '.expiring_objects', unexpected_container, + '%s-a/c/o' % delete_at, + req_headers['X-Delete-At-Host'], + req_headers['X-Delete-At-Partition'], + req_headers['X-Delete-At-Device'], HeaderKeyDict({ + # the .expiring_objects account is always policy-0 + 'X-Backend-Storage-Policy-Index': 0, + 'x-size': '0', + 'x-etag': 'd41d8cd98f00b204e9800998ecf8427e', + 'x-content-type': 'text/plain', + 'x-timestamp': utils.Timestamp('1').internal, + 'x-trans-id': '1234', + 'referer': 'PUT http://localhost/v1/a/c/o'}), + 'sda1', policy]) + def test_delete_at_update_put_with_info_but_missing_host(self): # Same as test_delete_at_update_put_with_info, but just # missing the X-Delete-At-Host header. @@ -6948,8 +7007,9 @@ class TestObjectController(BaseTestCase): self.object_controller.async_update = fake_async_update self.object_controller.logger = self.logger - delete_at_container = utils.get_expirer_container( - '1', 84600, 'a', 'c', 'o') + delete_at_container = \ + self.object_controller.expirer_config.get_expirer_container( + '1', 'a', 'c', 'o') req = Request.blank( '/v1/a/c/o', environ={'REQUEST_METHOD': 'PUT'}, @@ -7773,10 +7833,9 @@ class TestObjectController(BaseTestCase): def test_extra_headers_contain_object_bytes(self): timestamp1 = next(self.ts).normal delete_at_timestamp1 = int(time() + 1000) - delete_at_container1 = str( - delete_at_timestamp1 / - self.object_controller.expiring_objects_container_divisor * - self.object_controller.expiring_objects_container_divisor) + delete_at_container1 = \ + self.object_controller.expirer_config.get_expirer_container( + delete_at_timestamp1, 'a', 'c', 'o') req = Request.blank( '/sda1/p/a/c/o', environ={'REQUEST_METHOD': 'PUT'}, headers={'X-Timestamp': timestamp1, @@ -7861,8 +7920,9 @@ class TestObjectController(BaseTestCase): policy = random.choice(list(POLICIES)) delete_at = int(next(self.ts)) + 30 - delete_at_container = utils.get_expirer_container(delete_at, 86400, - 'a', 'c', 'o') + delete_at_container = \ + self.object_controller.expirer_config.get_expirer_container( + delete_at, 'a', 'c', 'o') base_headers = { 'X-Backend-Storage-Policy-Index': int(policy), 'Content-Type': 'application/octet-stream', @@ -7957,8 +8017,9 @@ class TestObjectController(BaseTestCase): put_ts = next(self.ts) put_size = 1548 put_delete_at = int(next(self.ts)) + 30 - put_delete_at_container = utils.get_expirer_container( - put_delete_at, 86400, 'a', 'c', 'o') + put_delete_at_container = \ + self.object_controller.expirer_config.get_expirer_container( + put_delete_at, 'a', 'c', 'o') put_req = Request.blank( '/sda1/p/a/c/o', method='PUT', body='\x01' * put_size, headers={ @@ -8008,8 +8069,9 @@ class TestObjectController(BaseTestCase): delete_at = int(next(self.ts)) + 100 self.assertNotEqual(delete_at, put_delete_at) # sanity - delete_at_container = utils.get_expirer_container( - delete_at, 86400, 'a', 'c', 'o') + delete_at_container = \ + self.object_controller.expirer_config.get_expirer_container( + delete_at, 'a', 'c', 'o') base_headers = { 'X-Backend-Storage-Policy-Index': int(policy), @@ -8119,10 +8181,9 @@ class TestObjectController(BaseTestCase): self.object_controller.delete_at_update = fake_delete_at_update timestamp1 = normalize_timestamp(time()) delete_at_timestamp1 = int(time() + 1000) - delete_at_container1 = str( - delete_at_timestamp1 / - self.object_controller.expiring_objects_container_divisor * - self.object_controller.expiring_objects_container_divisor) + delete_at_container1 = \ + self.object_controller.expirer_config.get_expirer_container( + delete_at_timestamp1, 'a', 'c', 'o') req = Request.blank( '/sda1/p/a/c/o', environ={'REQUEST_METHOD': 'PUT'}, headers={'X-Timestamp': timestamp1, diff --git a/test/unit/proxy/controllers/test_obj.py b/test/unit/proxy/controllers/test_obj.py index f434a96c3a..dbb3af2be0 100644 --- a/test/unit/proxy/controllers/test_obj.py +++ b/test/unit/proxy/controllers/test_obj.py @@ -191,15 +191,8 @@ class BaseObjectControllerMixin(object): self.logger = debug_logger('proxy-server') self.logger.thread_locals = ('txn1', '127.0.0.2') # increase connection timeout to avoid intermittent failures - conf = {'conn_timeout': 1.0} - self.app = PatchedObjControllerApp( - conf, account_ring=FakeRing(), - container_ring=FakeRing(), logger=self.logger) - self.logger.clear() # startup/loading debug msgs not helpful - - # you can over-ride the container_info just by setting it on the app - # (see PatchedObjControllerApp for details) - self.app.container_info = dict(self.fake_container_info()) + self.conf = {'conn_timeout': 1.0} + self._make_app() # default policy and ring references self.policy = POLICIES.default @@ -207,6 +200,16 @@ class BaseObjectControllerMixin(object): self._ts_iter = (utils.Timestamp(t) for t in itertools.count(int(time.time()))) + def _make_app(self): + self.app = PatchedObjControllerApp( + self.conf, account_ring=FakeRing(), + container_ring=FakeRing(), logger=self.logger) + self.logger.clear() # startup/loading debug msgs not helpful + + # you can over-ride the container_info just by setting it on the app + # (see PatchedObjControllerApp for details) + self.app.container_info = dict(self.fake_container_info()) + def ts(self): return next(self._ts_iter) @@ -2587,6 +2590,8 @@ class TestReplicatedObjController(CommonObjectControllerMixin, def test_PUT_delete_at(self): t = str(int(time.time() + 100)) + expected_part, expected_nodes, expected_delete_at_container = \ + self.app.expirer_config.get_delete_at_nodes(t, 'a', 'c', 'o') req = swob.Request.blank('/v1/a/c/o', method='PUT', body=b'', headers={'Content-Type': 'foo/bar', 'X-Delete-At': t}) @@ -2600,12 +2605,52 @@ class TestReplicatedObjController(CommonObjectControllerMixin, with set_http_connect(*codes, give_connect=capture_headers): resp = req.get_response(self.app) self.assertEqual(resp.status_int, 201) + found_host_device = set() for given_headers in put_headers: + found_host_device.add('%s/%s' % ( + given_headers['X-Delete-At-Host'], + given_headers['X-Delete-At-Device'])) self.assertEqual(given_headers.get('X-Delete-At'), t) - self.assertIn('X-Delete-At-Host', given_headers) - self.assertIn('X-Delete-At-Device', given_headers) - self.assertIn('X-Delete-At-Partition', given_headers) - self.assertIn('X-Delete-At-Container', given_headers) + self.assertEqual(str(expected_part), + given_headers['X-Delete-At-Partition']) + self.assertEqual(expected_delete_at_container, + given_headers['X-Delete-At-Container']) + self.assertEqual({'%(ip)s:%(port)s/%(device)s' % n + for n in expected_nodes}, + found_host_device) + + def test_POST_delete_at_configure_task_container_per_day(self): + self.assertEqual(100, self.app.expirer_config.task_container_per_day) + t = str(int(time.time() + 100)) + expected_part, expected_nodes, expected_delete_at_container = \ + self.app.expirer_config.get_delete_at_nodes(t, 'a', 'c', 'o') + req = swob.Request.blank('/v1/a/c/o', method='POST', body=b'', + headers={'Content-Type': 'foo/bar', + 'X-Delete-At': t}) + post_headers = [] + + def capture_headers(ip, port, device, part, method, path, headers, + **kwargs): + if method == 'POST': + post_headers.append(headers) + codes = [201] * self.obj_ring.replicas + + with set_http_connect(*codes, give_connect=capture_headers): + resp = req.get_response(self.app) + self.assertEqual(resp.status_int, 201) + found_host_device = set() + for given_headers in post_headers: + found_host_device.add('%s/%s' % ( + given_headers['X-Delete-At-Host'], + given_headers['X-Delete-At-Device'])) + self.assertEqual(given_headers.get('X-Delete-At'), t) + self.assertEqual(str(expected_part), + given_headers['X-Delete-At-Partition']) + self.assertEqual(expected_delete_at_container, + given_headers['X-Delete-At-Container']) + self.assertEqual({'%(ip)s:%(port)s/%(device)s' % n + for n in expected_nodes}, + found_host_device) def test_POST_delete_at_with_x_open_expired(self): t_delete = str(int(time.time() + 30)) @@ -2635,11 +2680,8 @@ class TestReplicatedObjController(CommonObjectControllerMixin, self.assertIn('X-Delete-At-Container', given_headers) # Check when allow_open_expired config is set to true - conf = {'allow_open_expired': 'true'} - self.app = PatchedObjControllerApp( - conf, account_ring=FakeRing(), - container_ring=FakeRing(), logger=None) - self.app.container_info = dict(self.fake_container_info()) + self.conf['allow_open_expired'] = 'true' + self._make_app() self.obj_ring = self.app.get_object_ring(int(self.policy)) post_headers = [] diff --git a/test/unit/proxy/test_server.py b/test/unit/proxy/test_server.py index fadc735d65..f1b53423f4 100644 --- a/test/unit/proxy/test_server.py +++ b/test/unit/proxy/test_server.py @@ -7575,9 +7575,8 @@ class TestReplicatedObjectController( self.app.container_ring.set_replicas(2) delete_at_timestamp = int(time.time()) + 100000 - delete_at_container = utils.get_expirer_container( - delete_at_timestamp, self.app.expiring_objects_container_divisor, - 'a', 'c', 'o') + delete_at_container = self.app.expirer_config.get_expirer_container( + delete_at_timestamp, 'a', 'c', 'o') req = Request.blank('/v1/a/c/o', environ={'REQUEST_METHOD': 'PUT'}, headers={'Content-Type': 'application/stuff', 'Content-Length': '0', @@ -7608,13 +7607,22 @@ class TestReplicatedObjectController( @mock.patch('time.time', new=lambda: STATIC_TIME) def test_PUT_x_delete_at_with_more_container_replicas(self): self.app.container_ring.set_replicas(4) - self.app.expiring_objects_account = 'expires' - self.app.expiring_objects_container_divisor = 60 + self.app.expirer_config = proxy_server.expirer.ExpirerConfig( + { + 'expiring_objects_account_name': 'expires', + 'expiring_objects_container_divisor': 600, + }, logger=self.logger, container_ring=self.app.container_ring) + self.assertEqual([ + 'expiring_objects_container_divisor is deprecated', + 'expiring_objects_account_name is deprecated; you need to migrate ' + 'to the standard .expiring_objects account', + ], self.logger.get_lines_for_level('warning')) + self.assertIs(self.app.container_ring, + self.app.expirer_config.container_ring) delete_at_timestamp = int(time.time()) + 100000 - delete_at_container = utils.get_expirer_container( - delete_at_timestamp, self.app.expiring_objects_container_divisor, - 'a', 'c', 'o') + delete_at_container = self.app.expirer_config.get_expirer_container( + delete_at_timestamp, 'a', 'c', 'o') req = Request.blank('/v1/a/c/o', environ={'REQUEST_METHOD': 'PUT'}, headers={'Content-Type': 'application/stuff', 'Content-Length': 0,