swift 2.23.3__py3-none-any.whl → 2.35.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- swift/__init__.py +29 -50
- swift/account/auditor.py +21 -118
- swift/account/backend.py +33 -28
- swift/account/reaper.py +37 -28
- swift/account/replicator.py +22 -0
- swift/account/server.py +60 -26
- swift/account/utils.py +28 -11
- swift-2.23.3.data/scripts/swift-account-audit → swift/cli/account_audit.py +23 -13
- swift-2.23.3.data/scripts/swift-config → swift/cli/config.py +2 -2
- swift/cli/container_deleter.py +5 -11
- swift-2.23.3.data/scripts/swift-dispersion-populate → swift/cli/dispersion_populate.py +8 -7
- swift/cli/dispersion_report.py +10 -9
- swift-2.23.3.data/scripts/swift-drive-audit → swift/cli/drive_audit.py +63 -21
- swift/cli/form_signature.py +3 -7
- swift-2.23.3.data/scripts/swift-get-nodes → swift/cli/get_nodes.py +8 -2
- swift/cli/info.py +154 -14
- swift/cli/manage_shard_ranges.py +705 -37
- swift-2.23.3.data/scripts/swift-oldies → swift/cli/oldies.py +25 -14
- swift-2.23.3.data/scripts/swift-orphans → swift/cli/orphans.py +7 -3
- swift/cli/recon.py +196 -67
- swift-2.23.3.data/scripts/swift-recon-cron → swift/cli/recon_cron.py +17 -20
- swift-2.23.3.data/scripts/swift-reconciler-enqueue → swift/cli/reconciler_enqueue.py +2 -3
- swift/cli/relinker.py +807 -126
- swift/cli/reload.py +135 -0
- swift/cli/ringbuilder.py +217 -20
- swift/cli/ringcomposer.py +0 -1
- swift/cli/shard-info.py +4 -3
- swift/common/base_storage_server.py +9 -20
- swift/common/bufferedhttp.py +48 -74
- swift/common/constraints.py +20 -15
- swift/common/container_sync_realms.py +9 -11
- swift/common/daemon.py +25 -8
- swift/common/db.py +195 -128
- swift/common/db_auditor.py +168 -0
- swift/common/db_replicator.py +95 -55
- swift/common/digest.py +141 -0
- swift/common/direct_client.py +144 -33
- swift/common/error_limiter.py +93 -0
- swift/common/exceptions.py +25 -1
- swift/common/header_key_dict.py +2 -9
- swift/common/http_protocol.py +373 -0
- swift/common/internal_client.py +129 -59
- swift/common/linkat.py +3 -4
- swift/common/manager.py +284 -67
- swift/common/memcached.py +390 -145
- swift/common/middleware/__init__.py +4 -0
- swift/common/middleware/account_quotas.py +211 -46
- swift/common/middleware/acl.py +3 -8
- swift/common/middleware/backend_ratelimit.py +230 -0
- swift/common/middleware/bulk.py +22 -34
- swift/common/middleware/catch_errors.py +1 -3
- swift/common/middleware/cname_lookup.py +6 -11
- swift/common/middleware/container_quotas.py +1 -1
- swift/common/middleware/container_sync.py +39 -17
- swift/common/middleware/copy.py +12 -0
- swift/common/middleware/crossdomain.py +22 -9
- swift/common/middleware/crypto/__init__.py +2 -1
- swift/common/middleware/crypto/crypto_utils.py +11 -15
- swift/common/middleware/crypto/decrypter.py +28 -11
- swift/common/middleware/crypto/encrypter.py +12 -17
- swift/common/middleware/crypto/keymaster.py +8 -15
- swift/common/middleware/crypto/kms_keymaster.py +2 -1
- swift/common/middleware/dlo.py +15 -11
- swift/common/middleware/domain_remap.py +5 -4
- swift/common/middleware/etag_quoter.py +128 -0
- swift/common/middleware/formpost.py +73 -70
- swift/common/middleware/gatekeeper.py +8 -1
- swift/common/middleware/keystoneauth.py +33 -3
- swift/common/middleware/list_endpoints.py +4 -4
- swift/common/middleware/listing_formats.py +85 -49
- swift/common/middleware/memcache.py +4 -95
- swift/common/middleware/name_check.py +3 -2
- swift/common/middleware/proxy_logging.py +160 -92
- swift/common/middleware/ratelimit.py +17 -10
- swift/common/middleware/read_only.py +6 -4
- swift/common/middleware/recon.py +59 -22
- swift/common/middleware/s3api/acl_handlers.py +25 -3
- swift/common/middleware/s3api/acl_utils.py +6 -1
- swift/common/middleware/s3api/controllers/__init__.py +6 -0
- swift/common/middleware/s3api/controllers/acl.py +3 -2
- swift/common/middleware/s3api/controllers/bucket.py +242 -137
- swift/common/middleware/s3api/controllers/logging.py +2 -2
- swift/common/middleware/s3api/controllers/multi_delete.py +43 -20
- swift/common/middleware/s3api/controllers/multi_upload.py +219 -133
- swift/common/middleware/s3api/controllers/obj.py +112 -8
- swift/common/middleware/s3api/controllers/object_lock.py +44 -0
- swift/common/middleware/s3api/controllers/s3_acl.py +2 -2
- swift/common/middleware/s3api/controllers/tagging.py +57 -0
- swift/common/middleware/s3api/controllers/versioning.py +36 -7
- swift/common/middleware/s3api/etree.py +22 -9
- swift/common/middleware/s3api/exception.py +0 -4
- swift/common/middleware/s3api/s3api.py +113 -41
- swift/common/middleware/s3api/s3request.py +384 -218
- swift/common/middleware/s3api/s3response.py +126 -23
- swift/common/middleware/s3api/s3token.py +16 -17
- swift/common/middleware/s3api/schema/delete.rng +1 -1
- swift/common/middleware/s3api/subresource.py +7 -10
- swift/common/middleware/s3api/utils.py +27 -10
- swift/common/middleware/slo.py +665 -358
- swift/common/middleware/staticweb.py +64 -37
- swift/common/middleware/symlink.py +51 -18
- swift/common/middleware/tempauth.py +76 -58
- swift/common/middleware/tempurl.py +191 -173
- swift/common/middleware/versioned_writes/__init__.py +51 -0
- swift/common/middleware/{versioned_writes.py → versioned_writes/legacy.py} +27 -26
- swift/common/middleware/versioned_writes/object_versioning.py +1482 -0
- swift/common/middleware/x_profile/exceptions.py +1 -4
- swift/common/middleware/x_profile/html_viewer.py +18 -19
- swift/common/middleware/x_profile/profile_model.py +1 -2
- swift/common/middleware/xprofile.py +10 -10
- swift-2.23.3.data/scripts/swift-container-server → swift/common/recon.py +13 -8
- swift/common/registry.py +147 -0
- swift/common/request_helpers.py +324 -57
- swift/common/ring/builder.py +67 -25
- swift/common/ring/composite_builder.py +1 -1
- swift/common/ring/ring.py +177 -51
- swift/common/ring/utils.py +1 -1
- swift/common/splice.py +10 -6
- swift/common/statsd_client.py +205 -0
- swift/common/storage_policy.py +49 -44
- swift/common/swob.py +86 -102
- swift/common/{utils.py → utils/__init__.py} +2163 -2772
- swift/common/utils/base.py +131 -0
- swift/common/utils/config.py +433 -0
- swift/common/utils/ipaddrs.py +256 -0
- swift/common/utils/libc.py +345 -0
- swift/common/utils/logs.py +859 -0
- swift/common/utils/timestamp.py +412 -0
- swift/common/wsgi.py +553 -535
- swift/container/auditor.py +14 -100
- swift/container/backend.py +490 -231
- swift/container/reconciler.py +126 -37
- swift/container/replicator.py +96 -22
- swift/container/server.py +358 -165
- swift/container/sharder.py +1540 -684
- swift/container/sync.py +94 -88
- swift/container/updater.py +53 -32
- swift/obj/auditor.py +153 -35
- swift/obj/diskfile.py +466 -217
- swift/obj/expirer.py +406 -124
- swift/obj/mem_diskfile.py +7 -4
- swift/obj/mem_server.py +1 -0
- swift/obj/reconstructor.py +523 -262
- swift/obj/replicator.py +249 -188
- swift/obj/server.py +207 -122
- swift/obj/ssync_receiver.py +145 -85
- swift/obj/ssync_sender.py +113 -54
- swift/obj/updater.py +652 -139
- swift/obj/watchers/__init__.py +0 -0
- swift/obj/watchers/dark_data.py +213 -0
- swift/proxy/controllers/account.py +11 -11
- swift/proxy/controllers/base.py +848 -604
- swift/proxy/controllers/container.py +433 -92
- swift/proxy/controllers/info.py +3 -2
- swift/proxy/controllers/obj.py +1000 -489
- swift/proxy/server.py +185 -112
- {swift-2.23.3.dist-info → swift-2.35.0.dist-info}/AUTHORS +58 -11
- {swift-2.23.3.dist-info → swift-2.35.0.dist-info}/METADATA +51 -56
- swift-2.35.0.dist-info/RECORD +201 -0
- {swift-2.23.3.dist-info → swift-2.35.0.dist-info}/WHEEL +1 -1
- {swift-2.23.3.dist-info → swift-2.35.0.dist-info}/entry_points.txt +43 -0
- swift-2.35.0.dist-info/pbr.json +1 -0
- swift/locale/de/LC_MESSAGES/swift.po +0 -1216
- swift/locale/en_GB/LC_MESSAGES/swift.po +0 -1207
- swift/locale/es/LC_MESSAGES/swift.po +0 -1085
- swift/locale/fr/LC_MESSAGES/swift.po +0 -909
- swift/locale/it/LC_MESSAGES/swift.po +0 -894
- swift/locale/ja/LC_MESSAGES/swift.po +0 -965
- swift/locale/ko_KR/LC_MESSAGES/swift.po +0 -964
- swift/locale/pt_BR/LC_MESSAGES/swift.po +0 -881
- swift/locale/ru/LC_MESSAGES/swift.po +0 -891
- swift/locale/tr_TR/LC_MESSAGES/swift.po +0 -832
- swift/locale/zh_CN/LC_MESSAGES/swift.po +0 -833
- swift/locale/zh_TW/LC_MESSAGES/swift.po +0 -838
- swift-2.23.3.data/scripts/swift-account-auditor +0 -23
- swift-2.23.3.data/scripts/swift-account-info +0 -51
- swift-2.23.3.data/scripts/swift-account-reaper +0 -23
- swift-2.23.3.data/scripts/swift-account-replicator +0 -34
- swift-2.23.3.data/scripts/swift-account-server +0 -23
- swift-2.23.3.data/scripts/swift-container-auditor +0 -23
- swift-2.23.3.data/scripts/swift-container-info +0 -55
- swift-2.23.3.data/scripts/swift-container-reconciler +0 -21
- swift-2.23.3.data/scripts/swift-container-replicator +0 -34
- swift-2.23.3.data/scripts/swift-container-sharder +0 -37
- swift-2.23.3.data/scripts/swift-container-sync +0 -23
- swift-2.23.3.data/scripts/swift-container-updater +0 -23
- swift-2.23.3.data/scripts/swift-dispersion-report +0 -24
- swift-2.23.3.data/scripts/swift-form-signature +0 -20
- swift-2.23.3.data/scripts/swift-init +0 -119
- swift-2.23.3.data/scripts/swift-object-auditor +0 -29
- swift-2.23.3.data/scripts/swift-object-expirer +0 -33
- swift-2.23.3.data/scripts/swift-object-info +0 -60
- swift-2.23.3.data/scripts/swift-object-reconstructor +0 -33
- swift-2.23.3.data/scripts/swift-object-relinker +0 -41
- swift-2.23.3.data/scripts/swift-object-replicator +0 -37
- swift-2.23.3.data/scripts/swift-object-server +0 -27
- swift-2.23.3.data/scripts/swift-object-updater +0 -23
- swift-2.23.3.data/scripts/swift-proxy-server +0 -23
- swift-2.23.3.data/scripts/swift-recon +0 -24
- swift-2.23.3.data/scripts/swift-ring-builder +0 -24
- swift-2.23.3.data/scripts/swift-ring-builder-analyzer +0 -22
- swift-2.23.3.data/scripts/swift-ring-composer +0 -22
- swift-2.23.3.dist-info/RECORD +0 -220
- swift-2.23.3.dist-info/pbr.json +0 -1
- {swift-2.23.3.dist-info → swift-2.35.0.dist-info}/LICENSE +0 -0
- {swift-2.23.3.dist-info → swift-2.35.0.dist-info}/top_level.txt +0 -0
swift/obj/expirer.py
CHANGED
@@ -13,41 +13,156 @@
|
|
13
13
|
# See the License for the specific language governing permissions and
|
14
14
|
# limitations under the License.
|
15
15
|
|
16
|
-
import
|
16
|
+
import urllib
|
17
17
|
|
18
18
|
from random import random
|
19
19
|
from time import time
|
20
|
+
from optparse import OptionParser
|
20
21
|
from os.path import join
|
21
|
-
from swift import gettext_ as _
|
22
22
|
from collections import defaultdict, deque
|
23
|
-
import hashlib
|
24
23
|
|
25
24
|
from eventlet import sleep, Timeout
|
26
25
|
from eventlet.greenpool import GreenPool
|
27
26
|
|
28
|
-
from swift.common.
|
27
|
+
from swift.common.constraints import AUTO_CREATE_ACCOUNT_PREFIX
|
28
|
+
from swift.common.daemon import Daemon, run_daemon
|
29
29
|
from swift.common.internal_client import InternalClient, UnexpectedResponse
|
30
|
+
from swift.common import utils
|
30
31
|
from swift.common.utils import get_logger, dump_recon_cache, split_path, \
|
31
|
-
Timestamp, config_true_value, normalize_delete_at_timestamp
|
32
|
+
Timestamp, config_true_value, normalize_delete_at_timestamp, \
|
33
|
+
RateLimitedIterator, md5, non_negative_float, non_negative_int, \
|
34
|
+
parse_content_type, parse_options, config_positive_int_value
|
32
35
|
from swift.common.http import HTTP_NOT_FOUND, HTTP_CONFLICT, \
|
33
36
|
HTTP_PRECONDITION_FAILED
|
34
|
-
from swift.common.
|
37
|
+
from swift.common.recon import RECON_OBJECT_FILE, DEFAULT_RECON_CACHE_PATH
|
35
38
|
|
36
39
|
from swift.container.reconciler import direct_delete_container_entry
|
37
40
|
|
38
41
|
MAX_OBJECTS_TO_CACHE = 100000
|
42
|
+
X_DELETE_TYPE = 'text/plain'
|
39
43
|
ASYNC_DELETE_TYPE = 'application/async-deleted'
|
40
44
|
|
45
|
+
# expiring_objects_account_name used to be a supported configuration across
|
46
|
+
# proxy/expirer configs, but AUTO_CREATE_ACCOUNT_PREFIX is configured in
|
47
|
+
# swift.conf constraints; neither should be changed
|
48
|
+
EXPIRER_ACCOUNT_NAME = AUTO_CREATE_ACCOUNT_PREFIX + 'expiring_objects'
|
49
|
+
# Most clusters use the default "expiring_objects_container_divisor" of 86400
|
50
|
+
EXPIRER_CONTAINER_DIVISOR = 86400
|
51
|
+
EXPIRER_CONTAINER_PER_DIVISOR = 100
|
52
|
+
|
53
|
+
|
54
|
+
class ExpirerConfig(object):
|
55
|
+
|
56
|
+
def __init__(self, conf, container_ring=None, logger=None):
|
57
|
+
"""
|
58
|
+
Read the configurable object-expirer values consistently and issue
|
59
|
+
warnings appropriately when we encounter deprecated options.
|
60
|
+
|
61
|
+
This class is used in multiple contexts on proxy and object servers.
|
62
|
+
|
63
|
+
:param conf: a config dictionary
|
64
|
+
:param container_ring: optional, required in proxy context to lookup
|
65
|
+
task container (part, nodes)
|
66
|
+
:param logger: optional, will create one from the conf if not given
|
67
|
+
"""
|
68
|
+
logger = logger or get_logger(conf)
|
69
|
+
if 'expiring_objects_container_divisor' in conf:
|
70
|
+
logger.warning(
|
71
|
+
'expiring_objects_container_divisor is deprecated')
|
72
|
+
expirer_divisor = config_positive_int_value(
|
73
|
+
conf['expiring_objects_container_divisor'])
|
74
|
+
else:
|
75
|
+
expirer_divisor = EXPIRER_CONTAINER_DIVISOR
|
76
|
+
|
77
|
+
if 'expiring_objects_account_name' in conf:
|
78
|
+
logger.warning(
|
79
|
+
'expiring_objects_account_name is deprecated; you need '
|
80
|
+
'to migrate to the standard .expiring_objects account')
|
81
|
+
account_name = (AUTO_CREATE_ACCOUNT_PREFIX +
|
82
|
+
conf['expiring_objects_account_name'])
|
83
|
+
else:
|
84
|
+
account_name = EXPIRER_ACCOUNT_NAME
|
85
|
+
self.account_name = account_name
|
86
|
+
self.expirer_divisor = expirer_divisor
|
87
|
+
self.task_container_per_day = EXPIRER_CONTAINER_PER_DIVISOR
|
88
|
+
if self.task_container_per_day >= self.expirer_divisor:
|
89
|
+
msg = 'expiring_objects_container_divisor MUST be greater than 100'
|
90
|
+
if self.expirer_divisor != 86400:
|
91
|
+
msg += '; expiring_objects_container_divisor (%s) SHOULD be ' \
|
92
|
+
'default value of %d' \
|
93
|
+
% (self.expirer_divisor, EXPIRER_CONTAINER_DIVISOR)
|
94
|
+
raise ValueError(msg)
|
95
|
+
self.container_ring = container_ring
|
96
|
+
|
97
|
+
def get_expirer_container(self, x_delete_at, acc, cont, obj):
|
98
|
+
"""
|
99
|
+
Returns an expiring object task container name for given X-Delete-At
|
100
|
+
and (native string) a/c/o.
|
101
|
+
"""
|
102
|
+
# offset backwards from the expected day is a hash of size "per day"
|
103
|
+
shard_int = (int(utils.hash_path(acc, cont, obj), 16) %
|
104
|
+
self.task_container_per_day)
|
105
|
+
# even though the attr is named "task_container_per_day" it's actually
|
106
|
+
# "task_container_per_divisor" if for some reason the deprecated config
|
107
|
+
# "expirer_divisor" option doesn't have the default value of 86400
|
108
|
+
return normalize_delete_at_timestamp(
|
109
|
+
int(x_delete_at) // self.expirer_divisor *
|
110
|
+
self.expirer_divisor - shard_int)
|
111
|
+
|
112
|
+
def get_expirer_account_and_container(self, x_delete_at, acc, cont, obj):
|
113
|
+
"""
|
114
|
+
Calculates the expected expirer account and container for the target
|
115
|
+
given the current configuration.
|
116
|
+
|
117
|
+
:returns: a tuple, (account_name, task_container)
|
118
|
+
"""
|
119
|
+
task_container = self.get_expirer_container(
|
120
|
+
x_delete_at, acc, cont, obj)
|
121
|
+
return self.account_name, task_container
|
122
|
+
|
123
|
+
def is_expected_task_container(self, task_container_int):
|
124
|
+
"""
|
125
|
+
Validate the task_container timestamp as an expected value given the
|
126
|
+
current configuration. Changing the expirer configuration will lead to
|
127
|
+
orphaned x-delete-at task objects on overwrite, which may stick around
|
128
|
+
a whole reclaim age.
|
129
|
+
|
130
|
+
:params task_container_int: an int, all task_containers are expected
|
131
|
+
to be integer timestamps
|
132
|
+
|
133
|
+
:returns: a boolean, True if name fits with the given config
|
134
|
+
"""
|
135
|
+
# calculate seconds offset into previous divisor window
|
136
|
+
r = (task_container_int - 1) % self.expirer_divisor
|
137
|
+
# seconds offset should be no more than task_container_per_day i.e.
|
138
|
+
# given % 86400, r==86359 is ok (because 41 is less than 100), but
|
139
|
+
# 49768 would be unexpected
|
140
|
+
return self.expirer_divisor - r <= self.task_container_per_day
|
141
|
+
|
142
|
+
def get_delete_at_nodes(self, x_delete_at, acc, cont, obj):
|
143
|
+
"""
|
144
|
+
Get the task_container part, nodes, and name.
|
145
|
+
|
146
|
+
:returns: a tuple, (part, nodes, task_container_name)
|
147
|
+
"""
|
148
|
+
if not self.container_ring:
|
149
|
+
raise RuntimeError('%s was not created with container_ring' % self)
|
150
|
+
account_name, task_container = self.get_expirer_account_and_container(
|
151
|
+
x_delete_at, acc, cont, obj)
|
152
|
+
part, nodes = self.container_ring.get_nodes(
|
153
|
+
account_name, task_container)
|
154
|
+
return part, nodes, task_container
|
155
|
+
|
41
156
|
|
42
157
|
def build_task_obj(timestamp, target_account, target_container,
|
43
|
-
target_obj):
|
158
|
+
target_obj, high_precision=False):
|
44
159
|
"""
|
45
160
|
:return: a task object name in format of
|
46
161
|
"<timestamp>-<target_account>/<target_container>/<target_obj>"
|
47
162
|
"""
|
48
163
|
timestamp = Timestamp(timestamp)
|
49
164
|
return '%s-%s/%s/%s' % (
|
50
|
-
normalize_delete_at_timestamp(timestamp),
|
165
|
+
normalize_delete_at_timestamp(timestamp, high_precision),
|
51
166
|
target_account, target_container, target_obj)
|
52
167
|
|
53
168
|
|
@@ -66,6 +181,80 @@ def parse_task_obj(task_obj):
|
|
66
181
|
return timestamp, target_account, target_container, target_obj
|
67
182
|
|
68
183
|
|
184
|
+
def extract_expirer_bytes_from_ctype(content_type):
|
185
|
+
"""
|
186
|
+
Parse a content-type and return the number of bytes.
|
187
|
+
|
188
|
+
:param content_type: a content-type string
|
189
|
+
:return: int or None
|
190
|
+
"""
|
191
|
+
content_type, params = parse_content_type(content_type)
|
192
|
+
bytes_size = None
|
193
|
+
for k, v in params:
|
194
|
+
if k == 'swift_expirer_bytes':
|
195
|
+
bytes_size = int(v)
|
196
|
+
return bytes_size
|
197
|
+
|
198
|
+
|
199
|
+
def embed_expirer_bytes_in_ctype(content_type, metadata):
|
200
|
+
"""
|
201
|
+
Embed number of bytes into content-type. The bytes should come from
|
202
|
+
content-length on regular objects, but future extensions to "bytes in
|
203
|
+
expirer queue" monitoring may want to more closely consider expiration of
|
204
|
+
large multipart object manifests.
|
205
|
+
|
206
|
+
:param content_type: a content-type string
|
207
|
+
:param metadata: a dict, from Diskfile metadata
|
208
|
+
:return: str
|
209
|
+
"""
|
210
|
+
# as best I can tell this key is required by df.open
|
211
|
+
report_bytes = metadata['Content-Length']
|
212
|
+
return "%s;swift_expirer_bytes=%d" % (content_type, int(report_bytes))
|
213
|
+
|
214
|
+
|
215
|
+
def read_conf_for_delay_reaping_times(conf):
|
216
|
+
delay_reaping_times = {}
|
217
|
+
for conf_key in conf:
|
218
|
+
delay_reaping_prefix = "delay_reaping_"
|
219
|
+
if not conf_key.startswith(delay_reaping_prefix):
|
220
|
+
continue
|
221
|
+
delay_reaping_key = urllib.parse.unquote(
|
222
|
+
conf_key[len(delay_reaping_prefix):])
|
223
|
+
if delay_reaping_key.strip('/') != delay_reaping_key:
|
224
|
+
raise ValueError(
|
225
|
+
'%s '
|
226
|
+
'should be in the form delay_reaping_<account> '
|
227
|
+
'or delay_reaping_<account>/<container> '
|
228
|
+
'(leading or trailing "/" is not allowed)' % conf_key)
|
229
|
+
try:
|
230
|
+
# If split_path fails, have multiple '/' or
|
231
|
+
# account name is invalid
|
232
|
+
account, container = split_path(
|
233
|
+
'/' + delay_reaping_key, 1, 2
|
234
|
+
)
|
235
|
+
except ValueError:
|
236
|
+
raise ValueError(
|
237
|
+
'%s '
|
238
|
+
'should be in the form delay_reaping_<account> '
|
239
|
+
'or delay_reaping_<account>/<container> '
|
240
|
+
'(at most one "/" is allowed)' % conf_key)
|
241
|
+
try:
|
242
|
+
delay_reaping_times[(account, container)] = non_negative_float(
|
243
|
+
conf.get(conf_key)
|
244
|
+
)
|
245
|
+
except ValueError:
|
246
|
+
raise ValueError(
|
247
|
+
'%s must be a float '
|
248
|
+
'greater than or equal to 0' % conf_key)
|
249
|
+
return delay_reaping_times
|
250
|
+
|
251
|
+
|
252
|
+
def get_delay_reaping(delay_reaping_times, target_account, target_container):
|
253
|
+
return delay_reaping_times.get(
|
254
|
+
(target_account, target_container),
|
255
|
+
delay_reaping_times.get((target_account, None), 0.0))
|
256
|
+
|
257
|
+
|
69
258
|
class ObjectExpirer(Daemon):
|
70
259
|
"""
|
71
260
|
Daemon that queries the internal hidden task accounts to discover objects
|
@@ -73,11 +262,14 @@ class ObjectExpirer(Daemon):
|
|
73
262
|
|
74
263
|
:param conf: The daemon configuration.
|
75
264
|
"""
|
265
|
+
log_route = 'object-expirer'
|
76
266
|
|
77
267
|
def __init__(self, conf, logger=None, swift=None):
|
78
268
|
self.conf = conf
|
79
|
-
self.logger = logger or get_logger(conf, log_route=
|
80
|
-
self.interval =
|
269
|
+
self.logger = logger or get_logger(conf, log_route=self.log_route)
|
270
|
+
self.interval = float(conf.get('interval') or 300)
|
271
|
+
self.tasks_per_second = float(conf.get('tasks_per_second', 50.0))
|
272
|
+
self.expirer_config = ExpirerConfig(conf, logger=self.logger)
|
81
273
|
|
82
274
|
self.conf_path = \
|
83
275
|
self.conf.get('__file__') or '/etc/swift/object-expirer.conf'
|
@@ -87,22 +279,14 @@ class ObjectExpirer(Daemon):
|
|
87
279
|
self.dequeue_from_legacy = \
|
88
280
|
True if is_legacy_conf else \
|
89
281
|
config_true_value(conf.get('dequeue_from_legacy', 'false'))
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
else:
|
94
|
-
self.ic_conf_path = \
|
95
|
-
self.conf.get('internal_client_conf_path') or \
|
96
|
-
'/etc/swift/internal-client.conf'
|
97
|
-
|
98
|
-
self.read_conf_for_queue_access(swift)
|
99
|
-
|
100
|
-
self.report_interval = int(conf.get('report_interval') or 300)
|
282
|
+
self.swift = swift or self._make_internal_client(is_legacy_conf)
|
283
|
+
self.read_conf_for_queue_access()
|
284
|
+
self.report_interval = float(conf.get('report_interval') or 300)
|
101
285
|
self.report_first_time = self.report_last_time = time()
|
102
286
|
self.report_objects = 0
|
103
287
|
self.recon_cache_path = conf.get('recon_cache_path',
|
104
|
-
|
105
|
-
self.rcache = join(self.recon_cache_path,
|
288
|
+
DEFAULT_RECON_CACHE_PATH)
|
289
|
+
self.rcache = join(self.recon_cache_path, RECON_OBJECT_FILE)
|
106
290
|
self.concurrency = int(conf.get('concurrency', 1))
|
107
291
|
if self.concurrency < 1:
|
108
292
|
raise ValueError("concurrency must be set to at least 1")
|
@@ -111,21 +295,33 @@ class ObjectExpirer(Daemon):
|
|
111
295
|
# with the tombstone reclaim age in the consistency engine.
|
112
296
|
self.reclaim_age = int(conf.get('reclaim_age', 604800))
|
113
297
|
|
114
|
-
|
115
|
-
self.
|
116
|
-
|
298
|
+
self.delay_reaping_times = read_conf_for_delay_reaping_times(conf)
|
299
|
+
self.round_robin_task_cache_size = int(
|
300
|
+
conf.get('round_robin_task_cache_size', MAX_OBJECTS_TO_CACHE))
|
301
|
+
|
302
|
+
def _make_internal_client(self, is_legacy_conf):
|
303
|
+
default_ic_conf_path = '/etc/swift/internal-client.conf'
|
304
|
+
if is_legacy_conf:
|
305
|
+
default_ic_conf_path = self.conf_path
|
306
|
+
ic_conf_path = self.conf.get(
|
307
|
+
'internal_client_conf_path', default_ic_conf_path)
|
308
|
+
request_tries = int(self.conf.get('request_tries') or 3)
|
309
|
+
return InternalClient(
|
310
|
+
ic_conf_path, 'Swift Object Expirer', request_tries,
|
311
|
+
use_replication_network=True,
|
312
|
+
global_conf={'log_name': '%s-ic' % self.conf.get(
|
313
|
+
'log_name', self.log_route)})
|
314
|
+
|
315
|
+
def read_conf_for_queue_access(self):
|
316
|
+
self.expiring_objects_account = AUTO_CREATE_ACCOUNT_PREFIX + \
|
117
317
|
(self.conf.get('expiring_objects_account_name') or
|
118
318
|
'expiring_objects')
|
119
319
|
|
120
320
|
# This is for common parameter with general task queue in future
|
121
321
|
self.task_container_prefix = ''
|
122
|
-
|
123
|
-
|
124
|
-
self.
|
125
|
-
self.ic_conf_path, 'Swift Object Expirer', request_tries)
|
126
|
-
|
127
|
-
self.processes = int(self.conf.get('processes', 0))
|
128
|
-
self.process = int(self.conf.get('process', 0))
|
322
|
+
self.processes = non_negative_int(self.conf.get('processes', 0))
|
323
|
+
self.process = non_negative_int(self.conf.get('process', 0))
|
324
|
+
self._validate_processes_config()
|
129
325
|
|
130
326
|
def report(self, final=False):
|
131
327
|
"""
|
@@ -137,17 +333,17 @@ class ObjectExpirer(Daemon):
|
|
137
333
|
"""
|
138
334
|
if final:
|
139
335
|
elapsed = time() - self.report_first_time
|
140
|
-
self.logger.info(
|
141
|
-
|
142
|
-
|
336
|
+
self.logger.info(
|
337
|
+
'Pass completed in %(time)ds; %(objects)d objects expired', {
|
338
|
+
'time': elapsed, 'objects': self.report_objects})
|
143
339
|
dump_recon_cache({'object_expiration_pass': elapsed,
|
144
340
|
'expired_last_pass': self.report_objects},
|
145
341
|
self.rcache, self.logger)
|
146
342
|
elif time() - self.report_last_time >= self.report_interval:
|
147
343
|
elapsed = time() - self.report_first_time
|
148
|
-
self.logger.info(
|
149
|
-
|
150
|
-
|
344
|
+
self.logger.info(
|
345
|
+
'Pass so far %(time)ds; %(objects)d objects expired', {
|
346
|
+
'time': elapsed, 'objects': self.report_objects})
|
151
347
|
self.report_last_time = time()
|
152
348
|
|
153
349
|
def parse_task_obj(self, task_obj):
|
@@ -186,7 +382,7 @@ class ObjectExpirer(Daemon):
|
|
186
382
|
obj_cache[cache_key].append(delete_task)
|
187
383
|
cnt += 1
|
188
384
|
|
189
|
-
if cnt >
|
385
|
+
if cnt > self.round_robin_task_cache_size:
|
190
386
|
for task in dump_obj_cache_in_round_robin():
|
191
387
|
yield task
|
192
388
|
cnt = 0
|
@@ -203,7 +399,8 @@ class ObjectExpirer(Daemon):
|
|
203
399
|
if not isinstance(name, bytes):
|
204
400
|
name = name.encode('utf8')
|
205
401
|
# md5 is only used for shuffling mod
|
206
|
-
return int(
|
402
|
+
return int(md5(
|
403
|
+
name, usedforsecurity=False).hexdigest(), 16) % divisor
|
207
404
|
|
208
405
|
def iter_task_accounts_to_expire(self):
|
209
406
|
"""
|
@@ -217,29 +414,112 @@ class ObjectExpirer(Daemon):
|
|
217
414
|
only one expirer.
|
218
415
|
"""
|
219
416
|
if self.processes > 0:
|
220
|
-
yield self.
|
417
|
+
yield (self.expirer_config.account_name,
|
418
|
+
self.process, self.processes)
|
221
419
|
else:
|
222
|
-
yield self.
|
223
|
-
|
224
|
-
def delete_at_time_of_task_container(self, task_container):
|
225
|
-
"""
|
226
|
-
get delete_at timestamp from task_container name
|
227
|
-
"""
|
228
|
-
# task_container name is timestamp
|
229
|
-
return Timestamp(task_container)
|
420
|
+
yield self.expirer_config.account_name, 0, 1
|
230
421
|
|
231
|
-
def
|
422
|
+
def get_task_containers_to_expire(self, task_account):
|
232
423
|
"""
|
233
|
-
|
424
|
+
Collects task_container names under the task_account if the delete at
|
234
425
|
timestamp of task_container is past.
|
235
426
|
"""
|
427
|
+
container_list = []
|
428
|
+
unexpected_task_containers = {
|
429
|
+
'examples': [],
|
430
|
+
'count': 0,
|
431
|
+
}
|
236
432
|
for c in self.swift.iter_containers(task_account,
|
237
433
|
prefix=self.task_container_prefix):
|
238
|
-
|
239
|
-
|
240
|
-
|
434
|
+
try:
|
435
|
+
task_container_int = int(Timestamp(c['name']))
|
436
|
+
except ValueError:
|
437
|
+
self.logger.error('skipping invalid task container: %s/%s',
|
438
|
+
task_account, c['name'])
|
439
|
+
continue
|
440
|
+
if not self.expirer_config.is_expected_task_container(
|
441
|
+
task_container_int):
|
442
|
+
unexpected_task_containers['count'] += 1
|
443
|
+
if unexpected_task_containers['count'] < 5:
|
444
|
+
unexpected_task_containers['examples'].append(c['name'])
|
445
|
+
if task_container_int > Timestamp.now():
|
446
|
+
break
|
447
|
+
container_list.append(str(task_container_int))
|
448
|
+
|
449
|
+
if unexpected_task_containers['count']:
|
450
|
+
self.logger.info(
|
451
|
+
'processing %s unexpected task containers (e.g. %s)',
|
452
|
+
unexpected_task_containers['count'],
|
453
|
+
' '.join(unexpected_task_containers['examples']))
|
454
|
+
return container_list
|
455
|
+
|
456
|
+
def get_delay_reaping(self, target_account, target_container):
|
457
|
+
return get_delay_reaping(self.delay_reaping_times, target_account,
|
458
|
+
target_container)
|
459
|
+
|
460
|
+
def _iter_task_container(self, task_account, task_container,
|
461
|
+
my_index, divisor):
|
462
|
+
"""
|
463
|
+
Iterates the input task container, yields a task expire info dict for
|
464
|
+
each delete task if it is assigned to this expirer process.
|
465
|
+
|
466
|
+
:raises UnexpectedResponse: if the task container listing is not
|
467
|
+
successful.
|
468
|
+
"""
|
469
|
+
container_empty = True
|
470
|
+
for o in self.swift.iter_objects(task_account,
|
471
|
+
task_container,
|
472
|
+
acceptable_statuses=[2]):
|
473
|
+
container_empty = False
|
474
|
+
task_object = o['name']
|
475
|
+
try:
|
476
|
+
delete_timestamp, target_account, target_container, \
|
477
|
+
target_object = parse_task_obj(task_object)
|
478
|
+
except ValueError:
|
479
|
+
self.logger.exception('Unexcepted error handling task %r' %
|
480
|
+
task_object)
|
481
|
+
self.logger.increment('tasks.parse_errors')
|
482
|
+
continue
|
483
|
+
is_async = o.get('content_type') == ASYNC_DELETE_TYPE
|
484
|
+
delay_reaping = self.get_delay_reaping(target_account,
|
485
|
+
target_container)
|
486
|
+
|
487
|
+
if delete_timestamp > Timestamp.now():
|
488
|
+
# we shouldn't yield ANY more objects that can't reach
|
489
|
+
# the expiration date yet.
|
241
490
|
break
|
242
|
-
|
491
|
+
|
492
|
+
# Only one expirer daemon assigned for each task
|
493
|
+
if self.hash_mod('%s/%s' % (task_container, task_object),
|
494
|
+
divisor) != my_index:
|
495
|
+
self.logger.increment('tasks.skipped')
|
496
|
+
continue
|
497
|
+
|
498
|
+
if delete_timestamp > Timestamp(time() - delay_reaping) \
|
499
|
+
and not is_async:
|
500
|
+
# we shouldn't yield the object during the delay
|
501
|
+
self.logger.increment('tasks.delayed')
|
502
|
+
continue
|
503
|
+
|
504
|
+
self.logger.increment('tasks.assigned')
|
505
|
+
yield {'task_account': task_account,
|
506
|
+
'task_container': task_container,
|
507
|
+
'task_object': task_object,
|
508
|
+
'target_path': '/'.join([
|
509
|
+
target_account, target_container, target_object]),
|
510
|
+
'delete_timestamp': delete_timestamp,
|
511
|
+
'is_async_delete': is_async}
|
512
|
+
if container_empty:
|
513
|
+
try:
|
514
|
+
self.swift.delete_container(
|
515
|
+
task_account, task_container,
|
516
|
+
acceptable_statuses=(2, HTTP_NOT_FOUND, HTTP_CONFLICT))
|
517
|
+
except (Exception, Timeout) as err:
|
518
|
+
self.logger.exception(
|
519
|
+
'Exception while deleting container %(account)s '
|
520
|
+
'%(container)s %(err)s', {
|
521
|
+
'account': task_account,
|
522
|
+
'container': task_container, 'err': str(err)})
|
243
523
|
|
244
524
|
def iter_task_to_expire(self, task_account_container_list,
|
245
525
|
my_index, divisor):
|
@@ -248,36 +528,27 @@ class ObjectExpirer(Daemon):
|
|
248
528
|
task_container, task_object, timestamp_to_delete, and target_path
|
249
529
|
"""
|
250
530
|
for task_account, task_container in task_account_container_list:
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
is_async = o.get('content_type') == ASYNC_DELETE_TYPE
|
274
|
-
yield {'task_account': task_account,
|
275
|
-
'task_container': task_container,
|
276
|
-
'task_object': task_object,
|
277
|
-
'target_path': '/'.join([
|
278
|
-
target_account, target_container, target_object]),
|
279
|
-
'delete_timestamp': delete_timestamp,
|
280
|
-
'is_async_delete': is_async}
|
531
|
+
try:
|
532
|
+
for item in self._iter_task_container(
|
533
|
+
task_account, task_container, my_index, divisor):
|
534
|
+
yield item
|
535
|
+
except UnexpectedResponse as err:
|
536
|
+
if err.resp.status_int != 404:
|
537
|
+
self.logger.error(
|
538
|
+
'Unexpected response while listing objects in '
|
539
|
+
'container %(account)s %(container)s: %(err)s', {
|
540
|
+
'account': task_account,
|
541
|
+
'container': task_container,
|
542
|
+
'err': str(err)
|
543
|
+
})
|
544
|
+
except (Exception, Timeout) as err:
|
545
|
+
self.logger.error(
|
546
|
+
'Exception while listing objects in container %(account)s '
|
547
|
+
'%(container)s: %(err)s', {
|
548
|
+
'account': task_account,
|
549
|
+
'container': task_container,
|
550
|
+
'err': str(err)
|
551
|
+
})
|
281
552
|
|
282
553
|
def run_once(self, *args, **kwargs):
|
283
554
|
"""
|
@@ -290,6 +561,9 @@ class ObjectExpirer(Daemon):
|
|
290
561
|
These will override the values from the config file if
|
291
562
|
provided.
|
292
563
|
"""
|
564
|
+
# these config values are available to override at the command line,
|
565
|
+
# blow-up now if they're wrong
|
566
|
+
self.override_proceses_config_from_command_line(**kwargs)
|
293
567
|
# This if-clause will be removed when general task queue feature is
|
294
568
|
# implemented.
|
295
569
|
if not self.dequeue_from_legacy:
|
@@ -300,13 +574,11 @@ class ObjectExpirer(Daemon):
|
|
300
574
|
'with dequeue_from_legacy == true.')
|
301
575
|
return
|
302
576
|
|
303
|
-
self.get_process_values(kwargs)
|
304
577
|
pool = GreenPool(self.concurrency)
|
305
578
|
self.report_first_time = self.report_last_time = time()
|
306
579
|
self.report_objects = 0
|
307
580
|
try:
|
308
581
|
self.logger.debug('Run begin')
|
309
|
-
task_account_container_list_to_delete = list()
|
310
582
|
for task_account, my_index, divisor in \
|
311
583
|
self.iter_task_accounts_to_expire():
|
312
584
|
container_count, obj_count = \
|
@@ -316,20 +588,17 @@ class ObjectExpirer(Daemon):
|
|
316
588
|
if not container_count:
|
317
589
|
continue
|
318
590
|
|
319
|
-
self.logger.info(
|
591
|
+
self.logger.info(
|
320
592
|
'Pass beginning for task account %(account)s; '
|
321
593
|
'%(container_count)s possible containers; '
|
322
|
-
'%(obj_count)s possible objects'
|
323
|
-
|
324
|
-
|
325
|
-
|
594
|
+
'%(obj_count)s possible objects', {
|
595
|
+
'account': task_account,
|
596
|
+
'container_count': container_count,
|
597
|
+
'obj_count': obj_count})
|
326
598
|
|
327
599
|
task_account_container_list = \
|
328
600
|
[(task_account, task_container) for task_container in
|
329
|
-
self.
|
330
|
-
|
331
|
-
task_account_container_list_to_delete.extend(
|
332
|
-
task_account_container_list)
|
601
|
+
self.get_task_containers_to_expire(task_account)]
|
333
602
|
|
334
603
|
# delete_task_iter is a generator to yield a dict of
|
335
604
|
# task_account, task_container, task_object, delete_timestamp,
|
@@ -338,27 +607,17 @@ class ObjectExpirer(Daemon):
|
|
338
607
|
delete_task_iter = \
|
339
608
|
self.round_robin_order(self.iter_task_to_expire(
|
340
609
|
task_account_container_list, my_index, divisor))
|
341
|
-
|
342
|
-
|
610
|
+
rate_limited_iter = RateLimitedIterator(
|
611
|
+
delete_task_iter,
|
612
|
+
elements_per_second=self.tasks_per_second)
|
613
|
+
for delete_task in rate_limited_iter:
|
343
614
|
pool.spawn_n(self.delete_object, **delete_task)
|
344
615
|
|
345
616
|
pool.waitall()
|
346
|
-
for task_account, task_container in \
|
347
|
-
task_account_container_list_to_delete:
|
348
|
-
try:
|
349
|
-
self.swift.delete_container(
|
350
|
-
task_account, task_container,
|
351
|
-
acceptable_statuses=(2, HTTP_NOT_FOUND, HTTP_CONFLICT))
|
352
|
-
except (Exception, Timeout) as err:
|
353
|
-
self.logger.exception(
|
354
|
-
_('Exception while deleting container %(account)s '
|
355
|
-
'%(container)s %(err)s') % {
|
356
|
-
'account': task_account,
|
357
|
-
'container': task_container, 'err': str(err)})
|
358
617
|
self.logger.debug('Run end')
|
359
618
|
self.report(final=True)
|
360
619
|
except (Exception, Timeout):
|
361
|
-
self.logger.exception(
|
620
|
+
self.logger.exception('Unhandled exception')
|
362
621
|
|
363
622
|
def run_forever(self, *args, **kwargs):
|
364
623
|
"""
|
@@ -369,18 +628,21 @@ class ObjectExpirer(Daemon):
|
|
369
628
|
:param kwargs: Extra keyword args to fulfill the Daemon interface; this
|
370
629
|
daemon has no additional keyword args.
|
371
630
|
"""
|
631
|
+
# these config values are available to override at the command line
|
632
|
+
# blow-up now if they're wrong
|
633
|
+
self.override_proceses_config_from_command_line(**kwargs)
|
372
634
|
sleep(random() * self.interval)
|
373
635
|
while True:
|
374
636
|
begin = time()
|
375
637
|
try:
|
376
638
|
self.run_once(*args, **kwargs)
|
377
639
|
except (Exception, Timeout):
|
378
|
-
self.logger.exception(
|
640
|
+
self.logger.exception('Unhandled exception')
|
379
641
|
elapsed = time() - begin
|
380
642
|
if elapsed < self.interval:
|
381
643
|
sleep(random() * (self.interval - elapsed))
|
382
644
|
|
383
|
-
def
|
645
|
+
def override_proceses_config_from_command_line(self, **kwargs):
|
384
646
|
"""
|
385
647
|
Sets self.processes and self.process from the kwargs if those
|
386
648
|
values exist, otherwise, leaves those values as they were set in
|
@@ -391,19 +653,20 @@ class ObjectExpirer(Daemon):
|
|
391
653
|
line when the daemon is run.
|
392
654
|
"""
|
393
655
|
if kwargs.get('processes') is not None:
|
394
|
-
self.processes =
|
656
|
+
self.processes = non_negative_int(kwargs['processes'])
|
395
657
|
|
396
658
|
if kwargs.get('process') is not None:
|
397
|
-
self.process =
|
659
|
+
self.process = non_negative_int(kwargs['process'])
|
398
660
|
|
399
|
-
|
400
|
-
raise ValueError(
|
401
|
-
'process must be an integer greater than or equal to 0')
|
661
|
+
self._validate_processes_config()
|
402
662
|
|
403
|
-
|
404
|
-
|
405
|
-
|
663
|
+
def _validate_processes_config(self):
|
664
|
+
"""
|
665
|
+
Used in constructor and in override_proceses_config_from_command_line
|
666
|
+
to validate the processes configuration requirements.
|
406
667
|
|
668
|
+
:raiess: ValueError if processes config is invalid
|
669
|
+
"""
|
407
670
|
if self.processes and self.process >= self.processes:
|
408
671
|
raise ValueError(
|
409
672
|
'process must be less than processes')
|
@@ -469,7 +732,6 @@ class ObjectExpirer(Daemon):
|
|
469
732
|
:raises UnexpectedResponse: if the delete was unsuccessful and
|
470
733
|
should be retried later
|
471
734
|
"""
|
472
|
-
path = '/v1/' + wsgi_quote(str_to_wsgi(actual_obj.lstrip('/')))
|
473
735
|
if is_async_delete:
|
474
736
|
headers = {'X-Timestamp': timestamp.normal}
|
475
737
|
acceptable_statuses = (2, HTTP_CONFLICT, HTTP_NOT_FOUND)
|
@@ -478,4 +740,24 @@ class ObjectExpirer(Daemon):
|
|
478
740
|
'X-If-Delete-At': timestamp.normal,
|
479
741
|
'X-Backend-Clean-Expiring-Object-Queue': 'no'}
|
480
742
|
acceptable_statuses = (2, HTTP_CONFLICT)
|
481
|
-
self.swift.
|
743
|
+
self.swift.delete_object(*split_path('/' + actual_obj, 3, 3, True),
|
744
|
+
headers=headers,
|
745
|
+
acceptable_statuses=acceptable_statuses)
|
746
|
+
|
747
|
+
|
748
|
+
def main():
|
749
|
+
parser = OptionParser("%prog CONFIG [options]")
|
750
|
+
parser.add_option('--processes', dest='processes',
|
751
|
+
help="Number of processes to use to do the work, don't "
|
752
|
+
"use this option to do all the work in one process")
|
753
|
+
parser.add_option('--process', dest='process',
|
754
|
+
help="Process number for this process, don't use "
|
755
|
+
"this option to do all the work in one process, this "
|
756
|
+
"is used to determine which part of the work this "
|
757
|
+
"process should do")
|
758
|
+
conf_file, options = parse_options(parser=parser, once=True)
|
759
|
+
run_daemon(ObjectExpirer, conf_file, **options)
|
760
|
+
|
761
|
+
|
762
|
+
if __name__ == '__main__':
|
763
|
+
main()
|