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/diskfile.py
CHANGED
@@ -30,7 +30,7 @@ The remaining methods in this module are considered implementation specific and
|
|
30
30
|
are also not considered part of the backend API.
|
31
31
|
"""
|
32
32
|
|
33
|
-
import
|
33
|
+
import pickle # nosec: B403
|
34
34
|
import binascii
|
35
35
|
import copy
|
36
36
|
import errno
|
@@ -40,12 +40,11 @@ import os
|
|
40
40
|
import re
|
41
41
|
import time
|
42
42
|
import uuid
|
43
|
-
from hashlib import md5
|
44
43
|
import logging
|
45
44
|
import traceback
|
46
45
|
import xattr
|
47
46
|
from os.path import basename, dirname, exists, join, splitext
|
48
|
-
|
47
|
+
import random
|
49
48
|
from tempfile import mkstemp
|
50
49
|
from contextlib import contextmanager
|
51
50
|
from collections import defaultdict
|
@@ -53,11 +52,9 @@ from datetime import timedelta
|
|
53
52
|
|
54
53
|
from eventlet import Timeout, tpool
|
55
54
|
from eventlet.hubs import trampoline
|
56
|
-
import six
|
57
55
|
from pyeclib.ec_iface import ECDriverError, ECInvalidFragmentMetadata, \
|
58
56
|
ECBadFragmentChecksum, ECInvalidParameter
|
59
57
|
|
60
|
-
from swift import gettext_ as _
|
61
58
|
from swift.common.constraints import check_drive
|
62
59
|
from swift.common.request_helpers import is_sys_meta
|
63
60
|
from swift.common.utils import mkdirs, Timestamp, \
|
@@ -66,13 +63,15 @@ from swift.common.utils import mkdirs, Timestamp, \
|
|
66
63
|
config_true_value, listdir, split_path, remove_file, \
|
67
64
|
get_md5_socket, F_SETPIPE_SZ, decode_timestamps, encode_timestamps, \
|
68
65
|
MD5_OF_EMPTY_STRING, link_fd_to_path, \
|
69
|
-
O_TMPFILE, makedirs_count, replace_partition_in_path, remove_directory
|
66
|
+
O_TMPFILE, makedirs_count, replace_partition_in_path, remove_directory, \
|
67
|
+
md5, is_file_older, non_negative_float, config_fallocate_value, \
|
68
|
+
fs_has_free_space, CooperativeIterator, EUCLEAN
|
70
69
|
from swift.common.splice import splice, tee
|
71
70
|
from swift.common.exceptions import DiskFileQuarantined, DiskFileNotExist, \
|
72
71
|
DiskFileCollision, DiskFileNoSpace, DiskFileDeviceUnavailable, \
|
73
72
|
DiskFileDeleted, DiskFileError, DiskFileNotOpen, PathNotDir, \
|
74
73
|
ReplicationLockTimeout, DiskFileExpired, DiskFileXattrNotSupported, \
|
75
|
-
DiskFileBadMetadataChecksum
|
74
|
+
DiskFileBadMetadataChecksum, PartitionLockTimeout
|
76
75
|
from swift.common.swob import multi_range_iterator
|
77
76
|
from swift.common.storage_policy import (
|
78
77
|
get_policy_string, split_policy_string, PolicyError, POLICIES,
|
@@ -81,6 +80,7 @@ from swift.common.storage_policy import (
|
|
81
80
|
|
82
81
|
PICKLE_PROTOCOL = 2
|
83
82
|
DEFAULT_RECLAIM_AGE = timedelta(weeks=1).total_seconds()
|
83
|
+
DEFAULT_COMMIT_WINDOW = 60.0
|
84
84
|
HASH_FILE = 'hashes.pkl'
|
85
85
|
HASH_INVALIDATIONS_FILE = 'hashes.invalid'
|
86
86
|
METADATA_KEY = b'user.swift.metadata'
|
@@ -153,38 +153,33 @@ def _encode_metadata(metadata):
|
|
153
153
|
|
154
154
|
:param metadata: a dict
|
155
155
|
"""
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
return item
|
161
|
-
else:
|
162
|
-
def encode_str(item):
|
163
|
-
if isinstance(item, six.text_type):
|
164
|
-
return item.encode('utf8', 'surrogateescape')
|
165
|
-
return item
|
156
|
+
def encode_str(item):
|
157
|
+
if isinstance(item, str):
|
158
|
+
return item.encode('utf8', 'surrogateescape')
|
159
|
+
return item
|
166
160
|
|
167
161
|
return dict(((encode_str(k), encode_str(v)) for k, v in metadata.items()))
|
168
162
|
|
169
163
|
|
170
|
-
def _decode_metadata(metadata):
|
164
|
+
def _decode_metadata(metadata, metadata_written_by_py3):
|
171
165
|
"""
|
172
166
|
Given a metadata dict from disk, convert keys and values to native strings.
|
173
167
|
|
174
168
|
:param metadata: a dict
|
169
|
+
:param metadata_written_by_py3:
|
175
170
|
"""
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
171
|
+
def to_str(item, is_name=False):
|
172
|
+
# For years, py2 and py3 handled non-ascii metadata differently;
|
173
|
+
# see https://bugs.launchpad.net/swift/+bug/2012531
|
174
|
+
if not metadata_written_by_py3 and isinstance(item, bytes) \
|
175
|
+
and not is_name:
|
176
|
+
# do our best to read old py2 data
|
177
|
+
item = item.decode('latin1')
|
178
|
+
if isinstance(item, bytes):
|
179
|
+
return item.decode('utf8', 'surrogateescape')
|
180
|
+
return item
|
186
181
|
|
187
|
-
return
|
182
|
+
return {to_str(k): to_str(v, k == b'name') for k, v in metadata.items()}
|
188
183
|
|
189
184
|
|
190
185
|
def read_metadata(fd, add_missing_checksum=False):
|
@@ -222,28 +217,28 @@ def read_metadata(fd, add_missing_checksum=False):
|
|
222
217
|
# exist. This is fine; it just means that this object predates the
|
223
218
|
# introduction of metadata checksums.
|
224
219
|
if add_missing_checksum:
|
225
|
-
new_checksum = md5(metadata)
|
220
|
+
new_checksum = (md5(metadata, usedforsecurity=False)
|
221
|
+
.hexdigest().encode('ascii'))
|
226
222
|
try:
|
227
223
|
xattr.setxattr(fd, METADATA_CHECKSUM_KEY, new_checksum)
|
228
224
|
except (IOError, OSError) as e:
|
229
225
|
logging.error("Error adding metadata: %s" % e)
|
230
226
|
|
231
227
|
if metadata_checksum:
|
232
|
-
computed_checksum = md5(metadata)
|
228
|
+
computed_checksum = (md5(metadata, usedforsecurity=False)
|
229
|
+
.hexdigest().encode('ascii'))
|
233
230
|
if metadata_checksum != computed_checksum:
|
234
231
|
raise DiskFileBadMetadataChecksum(
|
235
232
|
"Metadata checksum mismatch for %s: "
|
236
233
|
"stored checksum='%s', computed='%s'" % (
|
237
234
|
fd, metadata_checksum, computed_checksum))
|
238
235
|
|
236
|
+
metadata_written_by_py3 = (b'_codecs\nencode' in metadata[:32])
|
239
237
|
# strings are utf-8 encoded when written, but have not always been
|
240
238
|
# (see https://bugs.launchpad.net/swift/+bug/1678018) so encode them again
|
241
239
|
# when read
|
242
|
-
|
243
|
-
|
244
|
-
else:
|
245
|
-
metadata = pickle.loads(metadata, encoding='bytes')
|
246
|
-
return _decode_metadata(metadata)
|
240
|
+
metadata = pickle.loads(metadata, encoding='bytes') # nosec: B301
|
241
|
+
return _decode_metadata(metadata, metadata_written_by_py3)
|
247
242
|
|
248
243
|
|
249
244
|
def write_metadata(fd, metadata, xattr_size=65536):
|
@@ -254,7 +249,8 @@ def write_metadata(fd, metadata, xattr_size=65536):
|
|
254
249
|
:param metadata: metadata to write
|
255
250
|
"""
|
256
251
|
metastr = pickle.dumps(_encode_metadata(metadata), PICKLE_PROTOCOL)
|
257
|
-
metastr_md5 =
|
252
|
+
metastr_md5 = (
|
253
|
+
md5(metastr, usedforsecurity=False).hexdigest().encode('ascii'))
|
258
254
|
key = 0
|
259
255
|
try:
|
260
256
|
while metastr:
|
@@ -327,7 +323,11 @@ def quarantine_renamer(device_path, corrupted_file_path):
|
|
327
323
|
to_dir = join(device_path, 'quarantined',
|
328
324
|
get_data_dir(policy),
|
329
325
|
basename(from_dir))
|
330
|
-
|
326
|
+
if len(basename(from_dir)) == 3:
|
327
|
+
# quarantining whole suffix
|
328
|
+
invalidate_hash(from_dir)
|
329
|
+
else:
|
330
|
+
invalidate_hash(dirname(from_dir))
|
331
331
|
try:
|
332
332
|
renamer(from_dir, to_dir, fsync=False)
|
333
333
|
except OSError as e:
|
@@ -338,6 +338,12 @@ def quarantine_renamer(device_path, corrupted_file_path):
|
|
338
338
|
return to_dir
|
339
339
|
|
340
340
|
|
341
|
+
def valid_suffix(value):
|
342
|
+
if not isinstance(value, str) or len(value) != 3:
|
343
|
+
return False
|
344
|
+
return all(c in '0123456789abcdef' for c in value)
|
345
|
+
|
346
|
+
|
341
347
|
def read_hashes(partition_dir):
|
342
348
|
"""
|
343
349
|
Read the existing hashes.pkl
|
@@ -354,7 +360,7 @@ def read_hashes(partition_dir):
|
|
354
360
|
pass
|
355
361
|
else:
|
356
362
|
try:
|
357
|
-
hashes = pickle.loads(pickled_hashes)
|
363
|
+
hashes = pickle.loads(pickled_hashes) # nosec: B301
|
358
364
|
except Exception:
|
359
365
|
# pickle.loads() can raise a wide variety of exceptions when
|
360
366
|
# given invalid input depending on the way in which the
|
@@ -362,9 +368,9 @@ def read_hashes(partition_dir):
|
|
362
368
|
pass
|
363
369
|
|
364
370
|
# Check for corrupted data that could break os.listdir()
|
365
|
-
|
366
|
-
|
367
|
-
|
371
|
+
if not all(valid_suffix(key) or key in ('valid', 'updated')
|
372
|
+
for key in hashes):
|
373
|
+
return {'valid': False}
|
368
374
|
|
369
375
|
# hashes.pkl w/o valid updated key is "valid" but "forever old"
|
370
376
|
hashes.setdefault('valid', True)
|
@@ -402,19 +408,23 @@ def consolidate_hashes(partition_dir):
|
|
402
408
|
with lock_path(partition_dir):
|
403
409
|
hashes = read_hashes(partition_dir)
|
404
410
|
|
405
|
-
found_invalidation_entry = False
|
411
|
+
found_invalidation_entry = hashes_updated = False
|
406
412
|
try:
|
407
413
|
with open(invalidations_file, 'r') as inv_fh:
|
408
414
|
for line in inv_fh:
|
409
415
|
found_invalidation_entry = True
|
410
416
|
suffix = line.strip()
|
417
|
+
if not valid_suffix(suffix):
|
418
|
+
continue
|
419
|
+
hashes_updated = True
|
411
420
|
hashes[suffix] = None
|
412
421
|
except (IOError, OSError) as e:
|
413
422
|
if e.errno != errno.ENOENT:
|
414
423
|
raise
|
415
424
|
|
416
|
-
if
|
425
|
+
if hashes_updated:
|
417
426
|
write_hashes(partition_dir, hashes)
|
427
|
+
if found_invalidation_entry:
|
418
428
|
# Now that all the invalidations are reflected in hashes.pkl, it's
|
419
429
|
# safe to clear out the invalidations file.
|
420
430
|
with open(invalidations_file, 'wb') as inv_fh:
|
@@ -440,35 +450,58 @@ def invalidate_hash(suffix_dir):
|
|
440
450
|
inv_fh.write(suffix + b"\n")
|
441
451
|
|
442
452
|
|
443
|
-
def relink_paths(target_path, new_target_path,
|
453
|
+
def relink_paths(target_path, new_target_path, ignore_missing=True):
|
444
454
|
"""
|
445
|
-
Hard-links a file located in target_path using the second path
|
446
|
-
new_target_path
|
455
|
+
Hard-links a file located in ``target_path`` using the second path
|
456
|
+
``new_target_path``. Creates intermediate directories if required.
|
447
457
|
|
448
458
|
:param target_path: current absolute filename
|
449
459
|
:param new_target_path: new absolute filename for the hardlink
|
450
|
-
:param
|
451
|
-
|
460
|
+
:param ignore_missing: if True then no exception is raised if the link
|
461
|
+
could not be made because ``target_path`` did not exist, otherwise an
|
462
|
+
OSError will be raised.
|
463
|
+
:raises: OSError if the hard link could not be created, unless the intended
|
464
|
+
hard link already exists or the ``target_path`` does not exist and
|
465
|
+
``must_exist`` if False.
|
466
|
+
:returns: True if the link was created by the call to this method, False
|
467
|
+
otherwise.
|
452
468
|
"""
|
453
|
-
|
469
|
+
link_created = False
|
454
470
|
if target_path != new_target_path:
|
455
|
-
logging.debug('Relinking %s to %s due to next_part_power set',
|
456
|
-
target_path, new_target_path)
|
457
471
|
new_target_dir = os.path.dirname(new_target_path)
|
458
|
-
|
472
|
+
try:
|
459
473
|
os.makedirs(new_target_dir)
|
474
|
+
except OSError as err:
|
475
|
+
if err.errno != errno.EEXIST:
|
476
|
+
raise
|
460
477
|
|
461
|
-
|
462
|
-
if check_existing:
|
463
|
-
try:
|
464
|
-
new_stat = os.stat(new_target_path)
|
465
|
-
orig_stat = os.stat(target_path)
|
466
|
-
link_exists = (new_stat.st_ino == orig_stat.st_ino)
|
467
|
-
except OSError:
|
468
|
-
pass # if anything goes wrong, try anyway
|
469
|
-
|
470
|
-
if not link_exists:
|
478
|
+
try:
|
471
479
|
os.link(target_path, new_target_path)
|
480
|
+
link_created = True
|
481
|
+
except OSError as err:
|
482
|
+
# there are some circumstances in which it may be ok that the
|
483
|
+
# attempted link failed
|
484
|
+
ok = False
|
485
|
+
if err.errno == errno.ENOENT:
|
486
|
+
# this is ok if the *target* path doesn't exist anymore
|
487
|
+
ok = not os.path.exists(target_path) and ignore_missing
|
488
|
+
if err.errno == errno.EEXIST:
|
489
|
+
# this is ok *if* the intended link has already been made
|
490
|
+
try:
|
491
|
+
orig_stat = os.stat(target_path)
|
492
|
+
except OSError as sub_err:
|
493
|
+
# this is ok: the *target* path doesn't exist anymore
|
494
|
+
ok = sub_err.errno == errno.ENOENT and ignore_missing
|
495
|
+
else:
|
496
|
+
try:
|
497
|
+
new_stat = os.stat(new_target_path)
|
498
|
+
ok = new_stat.st_ino == orig_stat.st_ino
|
499
|
+
except OSError:
|
500
|
+
# squash this exception; the original will be raised
|
501
|
+
pass
|
502
|
+
if not ok:
|
503
|
+
raise err
|
504
|
+
return link_created
|
472
505
|
|
473
506
|
|
474
507
|
def get_part_path(dev_path, policy, partition):
|
@@ -522,7 +555,7 @@ def object_audit_location_generator(devices, datadir, mount_check=True,
|
|
522
555
|
device_dirs = list(
|
523
556
|
set(listdir(devices)).intersection(set(device_dirs)))
|
524
557
|
# randomize devices in case of process restart before sweep completed
|
525
|
-
shuffle(device_dirs)
|
558
|
+
random.shuffle(device_dirs)
|
526
559
|
|
527
560
|
base, policy = split_policy_string(datadir)
|
528
561
|
for device in device_dirs:
|
@@ -546,7 +579,8 @@ def object_audit_location_generator(devices, datadir, mount_check=True,
|
|
546
579
|
try:
|
547
580
|
suffixes = listdir(part_path)
|
548
581
|
except OSError as e:
|
549
|
-
if e.errno
|
582
|
+
if e.errno not in (errno.ENOTDIR, errno.ENODATA,
|
583
|
+
EUCLEAN):
|
550
584
|
raise
|
551
585
|
continue
|
552
586
|
for asuffix in suffixes:
|
@@ -554,7 +588,8 @@ def object_audit_location_generator(devices, datadir, mount_check=True,
|
|
554
588
|
try:
|
555
589
|
hashes = listdir(suff_path)
|
556
590
|
except OSError as e:
|
557
|
-
if e.errno
|
591
|
+
if e.errno not in (errno.ENOTDIR, errno.ENODATA,
|
592
|
+
EUCLEAN):
|
558
593
|
raise
|
559
594
|
continue
|
560
595
|
for hsh in hashes:
|
@@ -570,31 +605,25 @@ def get_auditor_status(datadir_path, logger, auditor_type):
|
|
570
605
|
datadir_path, "auditor_status_%s.json" % auditor_type)
|
571
606
|
status = {}
|
572
607
|
try:
|
573
|
-
|
574
|
-
statusfile = open(auditor_status, encoding='utf8')
|
575
|
-
else:
|
576
|
-
statusfile = open(auditor_status, 'rb')
|
577
|
-
with statusfile:
|
608
|
+
with open(auditor_status, encoding='utf8') as statusfile:
|
578
609
|
status = statusfile.read()
|
579
610
|
except (OSError, IOError) as e:
|
580
611
|
if e.errno != errno.ENOENT and logger:
|
581
|
-
logger.warning(
|
612
|
+
logger.warning('Cannot read %(auditor_status)s (%(err)s)',
|
582
613
|
{'auditor_status': auditor_status, 'err': e})
|
583
614
|
return listdir(datadir_path)
|
584
615
|
try:
|
585
616
|
status = json.loads(status)
|
586
617
|
except ValueError as e:
|
587
|
-
logger.warning(
|
588
|
-
|
618
|
+
logger.warning('Loading JSON from %(auditor_status)s failed'
|
619
|
+
' (%(err)s)',
|
589
620
|
{'auditor_status': auditor_status, 'err': e})
|
590
621
|
return listdir(datadir_path)
|
591
622
|
return status['partitions']
|
592
623
|
|
593
624
|
|
594
625
|
def update_auditor_status(datadir_path, logger, partitions, auditor_type):
|
595
|
-
status = json.dumps({'partitions': partitions})
|
596
|
-
if six.PY3:
|
597
|
-
status = status.encode('utf8')
|
626
|
+
status = json.dumps({'partitions': partitions}).encode('utf8')
|
598
627
|
auditor_status = os.path.join(
|
599
628
|
datadir_path, "auditor_status_%s.json" % auditor_type)
|
600
629
|
try:
|
@@ -612,7 +641,7 @@ def update_auditor_status(datadir_path, logger, partitions, auditor_type):
|
|
612
641
|
statusfile.write(status)
|
613
642
|
except (OSError, IOError) as e:
|
614
643
|
if logger:
|
615
|
-
logger.warning(
|
644
|
+
logger.warning('Cannot write %(auditor_status)s (%(err)s)',
|
616
645
|
{'auditor_status': auditor_status, 'err': e})
|
617
646
|
|
618
647
|
|
@@ -625,15 +654,6 @@ def clear_auditor_status(devices, datadir, auditor_type="ALL"):
|
|
625
654
|
remove_file(auditor_status)
|
626
655
|
|
627
656
|
|
628
|
-
def strip_self(f):
|
629
|
-
"""
|
630
|
-
Wrapper to attach module level functions to base class.
|
631
|
-
"""
|
632
|
-
def wrapper(self, *args, **kwargs):
|
633
|
-
return f(*args, **kwargs)
|
634
|
-
return wrapper
|
635
|
-
|
636
|
-
|
637
657
|
class DiskFileRouter(object):
|
638
658
|
|
639
659
|
def __init__(self, *args, **kwargs):
|
@@ -673,9 +693,9 @@ class BaseDiskFileManager(object):
|
|
673
693
|
diskfile_cls = None # must be set by subclasses
|
674
694
|
policy = None # must be set by subclasses
|
675
695
|
|
676
|
-
invalidate_hash =
|
677
|
-
consolidate_hashes =
|
678
|
-
quarantine_renamer =
|
696
|
+
invalidate_hash = staticmethod(invalidate_hash)
|
697
|
+
consolidate_hashes = staticmethod(consolidate_hashes)
|
698
|
+
quarantine_renamer = staticmethod(quarantine_renamer)
|
679
699
|
|
680
700
|
def __init__(self, conf, logger):
|
681
701
|
self.logger = logger
|
@@ -685,6 +705,8 @@ class BaseDiskFileManager(object):
|
|
685
705
|
self.bytes_per_sync = int(conf.get('mb_per_sync', 512)) * 1024 * 1024
|
686
706
|
self.mount_check = config_true_value(conf.get('mount_check', 'true'))
|
687
707
|
self.reclaim_age = int(conf.get('reclaim_age', DEFAULT_RECLAIM_AGE))
|
708
|
+
self.commit_window = non_negative_float(conf.get(
|
709
|
+
'commit_window', DEFAULT_COMMIT_WINDOW))
|
688
710
|
replication_concurrency_per_device = conf.get(
|
689
711
|
'replication_concurrency_per_device')
|
690
712
|
replication_one_per_device = conf.get('replication_one_per_device')
|
@@ -709,6 +731,8 @@ class BaseDiskFileManager(object):
|
|
709
731
|
replication_concurrency_per_device)
|
710
732
|
self.replication_lock_timeout = int(conf.get(
|
711
733
|
'replication_lock_timeout', 15))
|
734
|
+
self.fallocate_reserve, self.fallocate_is_percent = \
|
735
|
+
config_fallocate_value(conf.get('fallocate_reserve', '1%'))
|
712
736
|
|
713
737
|
self.use_splice = False
|
714
738
|
self.pipe_size = None
|
@@ -913,7 +937,9 @@ class BaseDiskFileManager(object):
|
|
913
937
|
valid fileset, or None.
|
914
938
|
|
915
939
|
:param files: a list of file names.
|
916
|
-
:param datadir: directory name files are from
|
940
|
+
:param datadir: directory name files are from; this is used to
|
941
|
+
construct file paths in the results, but the datadir is
|
942
|
+
not modified by this method.
|
917
943
|
:param verify: if True verify that the ondisk file contract has not
|
918
944
|
been violated, otherwise do not verify.
|
919
945
|
:param policy: storage policy used to store the files. Used to
|
@@ -1034,11 +1060,10 @@ class BaseDiskFileManager(object):
|
|
1034
1060
|
key = info_key[:-5] + '_file'
|
1035
1061
|
results[key] = join(datadir, info['filename']) if info else None
|
1036
1062
|
|
1037
|
-
if verify:
|
1038
|
-
|
1039
|
-
|
1040
|
-
|
1041
|
-
% str(results)
|
1063
|
+
if verify and not self._verify_ondisk_files(results, **kwargs):
|
1064
|
+
raise RuntimeError(
|
1065
|
+
"On-disk file search algorithm contract is broken: %s"
|
1066
|
+
% str(results))
|
1042
1067
|
|
1043
1068
|
return results
|
1044
1069
|
|
@@ -1077,8 +1102,14 @@ class BaseDiskFileManager(object):
|
|
1077
1102
|
remove_file(join(hsh_path, results['ts_info']['filename']))
|
1078
1103
|
files.remove(results.pop('ts_info')['filename'])
|
1079
1104
|
for file_info in results.get('possible_reclaim', []):
|
1080
|
-
# stray files are not deleted until reclaim-age
|
1081
|
-
|
1105
|
+
# stray files are not deleted until reclaim-age; non-durable data
|
1106
|
+
# files are not deleted unless they were written before
|
1107
|
+
# commit_window
|
1108
|
+
filepath = join(hsh_path, file_info['filename'])
|
1109
|
+
if (is_reclaimable(file_info['timestamp']) and
|
1110
|
+
(file_info.get('durable', True) or
|
1111
|
+
self.commit_window <= 0 or
|
1112
|
+
is_file_older(filepath, self.commit_window))):
|
1082
1113
|
results.setdefault('obsolete', []).append(file_info)
|
1083
1114
|
for file_info in results.get('obsolete', []):
|
1084
1115
|
remove_file(join(hsh_path, file_info['filename']))
|
@@ -1112,22 +1143,19 @@ class BaseDiskFileManager(object):
|
|
1112
1143
|
:param path: full path to directory
|
1113
1144
|
:param policy: storage policy used
|
1114
1145
|
"""
|
1115
|
-
|
1116
|
-
|
1117
|
-
|
1118
|
-
class shim(object):
|
1119
|
-
def __init__(self):
|
1120
|
-
self.md5 = md5()
|
1146
|
+
class shim(object):
|
1147
|
+
def __init__(self):
|
1148
|
+
self.md5 = md5(usedforsecurity=False)
|
1121
1149
|
|
1122
|
-
|
1123
|
-
|
1124
|
-
|
1125
|
-
|
1126
|
-
|
1150
|
+
def update(self, s):
|
1151
|
+
if isinstance(s, str):
|
1152
|
+
self.md5.update(s.encode('utf-8'))
|
1153
|
+
else:
|
1154
|
+
self.md5.update(s)
|
1127
1155
|
|
1128
|
-
|
1129
|
-
|
1130
|
-
|
1156
|
+
def hexdigest(self):
|
1157
|
+
return self.md5.hexdigest()
|
1158
|
+
hashes = defaultdict(shim)
|
1131
1159
|
try:
|
1132
1160
|
path_contents = sorted(os.listdir(path))
|
1133
1161
|
except OSError as err:
|
@@ -1140,10 +1168,10 @@ class BaseDiskFileManager(object):
|
|
1140
1168
|
ondisk_info = self.cleanup_ondisk_files(
|
1141
1169
|
hsh_path, policy=policy)
|
1142
1170
|
except OSError as err:
|
1171
|
+
partition_path = dirname(path)
|
1172
|
+
objects_path = dirname(partition_path)
|
1173
|
+
device_path = dirname(objects_path)
|
1143
1174
|
if err.errno == errno.ENOTDIR:
|
1144
|
-
partition_path = dirname(path)
|
1145
|
-
objects_path = dirname(partition_path)
|
1146
|
-
device_path = dirname(objects_path)
|
1147
1175
|
# The made-up filename is so that the eventual dirpath()
|
1148
1176
|
# will result in this object directory that we care about.
|
1149
1177
|
# Some failures will result in an object directory
|
@@ -1153,9 +1181,27 @@ class BaseDiskFileManager(object):
|
|
1153
1181
|
join(hsh_path,
|
1154
1182
|
"made-up-filename"))
|
1155
1183
|
logging.exception(
|
1156
|
-
|
1157
|
-
|
1158
|
-
|
1184
|
+
'Quarantined %(hsh_path)s to %(quar_path)s because '
|
1185
|
+
'it is not a directory', {'hsh_path': hsh_path,
|
1186
|
+
'quar_path': quar_path})
|
1187
|
+
continue
|
1188
|
+
elif err.errno in (errno.ENODATA, EUCLEAN):
|
1189
|
+
try:
|
1190
|
+
# We've seen cases where bad sectors lead to ENODATA
|
1191
|
+
# here; use a similar hack as above
|
1192
|
+
quar_path = quarantine_renamer(
|
1193
|
+
device_path,
|
1194
|
+
join(hsh_path, "made-up-filename"))
|
1195
|
+
orig_path = hsh_path
|
1196
|
+
except (OSError, IOError):
|
1197
|
+
# We've *also* seen the bad sectors lead to us needing
|
1198
|
+
# to quarantine the whole suffix
|
1199
|
+
quar_path = quarantine_renamer(device_path, hsh_path)
|
1200
|
+
orig_path = path
|
1201
|
+
logging.exception(
|
1202
|
+
'Quarantined %(orig_path)s to %(quar_path)s because '
|
1203
|
+
'it could not be listed', {'orig_path': orig_path,
|
1204
|
+
'quar_path': quar_path})
|
1159
1205
|
continue
|
1160
1206
|
raise
|
1161
1207
|
if not ondisk_info['files']:
|
@@ -1277,6 +1323,8 @@ class BaseDiskFileManager(object):
|
|
1277
1323
|
self.logger.debug('Run listdir on %s', partition_path)
|
1278
1324
|
hashes.update((suffix, None) for suffix in recalculate)
|
1279
1325
|
for suffix, hash_ in list(hashes.items()):
|
1326
|
+
if suffix in ('valid', 'updated'):
|
1327
|
+
continue
|
1280
1328
|
if not hash_:
|
1281
1329
|
suffix_dir = join(partition_path, suffix)
|
1282
1330
|
try:
|
@@ -1286,7 +1334,7 @@ class BaseDiskFileManager(object):
|
|
1286
1334
|
except PathNotDir:
|
1287
1335
|
del hashes[suffix]
|
1288
1336
|
except OSError:
|
1289
|
-
logging.exception(
|
1337
|
+
logging.exception('Error hashing suffix')
|
1290
1338
|
modified = True
|
1291
1339
|
if modified:
|
1292
1340
|
with lock_path(partition_path):
|
@@ -1333,8 +1381,8 @@ class BaseDiskFileManager(object):
|
|
1333
1381
|
@contextmanager
|
1334
1382
|
def replication_lock(self, device, policy, partition):
|
1335
1383
|
"""
|
1336
|
-
A context manager that will lock on the
|
1337
|
-
|
1384
|
+
A context manager that will lock on the partition and, if configured
|
1385
|
+
to do so, on the device given.
|
1338
1386
|
|
1339
1387
|
:param device: name of target device
|
1340
1388
|
:param policy: policy targeted by the replication request
|
@@ -1342,24 +1390,36 @@ class BaseDiskFileManager(object):
|
|
1342
1390
|
:raises ReplicationLockTimeout: If the lock on the device
|
1343
1391
|
cannot be granted within the configured timeout.
|
1344
1392
|
"""
|
1345
|
-
|
1346
|
-
|
1347
|
-
|
1348
|
-
|
1349
|
-
|
1350
|
-
|
1351
|
-
|
1352
|
-
|
1353
|
-
timeout_class=ReplicationLockTimeout,
|
1354
|
-
limit=self.replication_concurrency_per_device):
|
1355
|
-
with lock_path(
|
1356
|
-
part_path,
|
1357
|
-
timeout=limit_time - time.time(),
|
1358
|
-
timeout_class=ReplicationLockTimeout,
|
1359
|
-
limit=1,
|
1360
|
-
name='replication'):
|
1393
|
+
limit_time = time.time() + self.replication_lock_timeout
|
1394
|
+
with self.partition_lock(device, policy, partition, name='replication',
|
1395
|
+
timeout=self.replication_lock_timeout):
|
1396
|
+
if self.replication_concurrency_per_device:
|
1397
|
+
with lock_path(self.get_dev_path(device),
|
1398
|
+
timeout=limit_time - time.time(),
|
1399
|
+
timeout_class=ReplicationLockTimeout,
|
1400
|
+
limit=self.replication_concurrency_per_device):
|
1361
1401
|
yield True
|
1362
|
-
|
1402
|
+
else:
|
1403
|
+
yield True
|
1404
|
+
|
1405
|
+
@contextmanager
|
1406
|
+
def partition_lock(self, device, policy, partition, name=None,
|
1407
|
+
timeout=None):
|
1408
|
+
"""
|
1409
|
+
A context manager that will lock on the partition given.
|
1410
|
+
|
1411
|
+
:param device: device targeted by the lock request
|
1412
|
+
:param policy: policy targeted by the lock request
|
1413
|
+
:param partition: partition targeted by the lock request
|
1414
|
+
:raises PartitionLockTimeout: If the lock on the partition
|
1415
|
+
cannot be granted within the configured timeout.
|
1416
|
+
"""
|
1417
|
+
if timeout is None:
|
1418
|
+
timeout = self.replication_lock_timeout
|
1419
|
+
part_path = os.path.join(self.get_dev_path(device),
|
1420
|
+
get_data_dir(policy), str(partition))
|
1421
|
+
with lock_path(part_path, timeout=timeout,
|
1422
|
+
timeout_class=PartitionLockTimeout, limit=1, name=name):
|
1363
1423
|
yield True
|
1364
1424
|
|
1365
1425
|
def pickle_async_update(self, device, account, container, obj, data,
|
@@ -1440,21 +1500,22 @@ class BaseDiskFileManager(object):
|
|
1440
1500
|
self, audit_location.path, dev_path,
|
1441
1501
|
audit_location.partition, policy=audit_location.policy)
|
1442
1502
|
|
1443
|
-
def
|
1444
|
-
|
1503
|
+
def get_diskfile_and_filenames_from_hash(self, device, partition,
|
1504
|
+
object_hash, policy, **kwargs):
|
1445
1505
|
"""
|
1446
|
-
Returns a DiskFile instance for an object at the given
|
1447
|
-
object_hash
|
1448
|
-
|
1449
|
-
instance representing the tombstoned
|
1450
|
-
instead.
|
1506
|
+
Returns a tuple of (a DiskFile instance for an object at the given
|
1507
|
+
object_hash, the basenames of the files in the object's hash dir).
|
1508
|
+
Just in case someone thinks of refactoring, be sure DiskFileDeleted is
|
1509
|
+
*not* raised, but the DiskFile instance representing the tombstoned
|
1510
|
+
object is returned instead.
|
1451
1511
|
|
1452
1512
|
:param device: name of target device
|
1453
1513
|
:param partition: partition on the device in which the object lives
|
1454
1514
|
:param object_hash: the hash of an object path
|
1455
1515
|
:param policy: the StoragePolicy instance
|
1456
1516
|
:raises DiskFileNotExist: if the object does not exist
|
1457
|
-
:returns: an instance of BaseDiskFile
|
1517
|
+
:returns: a tuple comprising (an instance of BaseDiskFile, a list of
|
1518
|
+
file basenames)
|
1458
1519
|
"""
|
1459
1520
|
dev_path = self.get_dev_path(device)
|
1460
1521
|
if not dev_path:
|
@@ -1475,9 +1536,27 @@ class BaseDiskFileManager(object):
|
|
1475
1536
|
join(object_path,
|
1476
1537
|
"made-up-filename"))
|
1477
1538
|
logging.exception(
|
1478
|
-
|
1479
|
-
|
1480
|
-
|
1539
|
+
'Quarantined %(object_path)s to %(quar_path)s because '
|
1540
|
+
'it is not a directory', {'object_path': object_path,
|
1541
|
+
'quar_path': quar_path})
|
1542
|
+
raise DiskFileNotExist()
|
1543
|
+
elif err.errno in (errno.ENODATA, EUCLEAN):
|
1544
|
+
try:
|
1545
|
+
# We've seen cases where bad sectors lead to ENODATA here;
|
1546
|
+
# use a similar hack as above
|
1547
|
+
quar_path = self.quarantine_renamer(
|
1548
|
+
dev_path,
|
1549
|
+
join(object_path, "made-up-filename"))
|
1550
|
+
orig_path = object_path
|
1551
|
+
except (OSError, IOError):
|
1552
|
+
# We've *also* seen the bad sectors lead to us needing to
|
1553
|
+
# quarantine the whole suffix, not just the hash dir
|
1554
|
+
quar_path = self.quarantine_renamer(dev_path, object_path)
|
1555
|
+
orig_path = os.path.dirname(object_path)
|
1556
|
+
logging.exception(
|
1557
|
+
'Quarantined %(orig_path)s to %(quar_path)s because '
|
1558
|
+
'it could not be listed', {'orig_path': orig_path,
|
1559
|
+
'quar_path': quar_path})
|
1481
1560
|
raise DiskFileNotExist()
|
1482
1561
|
if err.errno != errno.ENOENT:
|
1483
1562
|
raise
|
@@ -1493,27 +1572,55 @@ class BaseDiskFileManager(object):
|
|
1493
1572
|
metadata.get('name', ''), 3, 3, True)
|
1494
1573
|
except ValueError:
|
1495
1574
|
raise DiskFileNotExist()
|
1496
|
-
|
1497
|
-
|
1498
|
-
|
1575
|
+
df = self.diskfile_cls(self, dev_path, partition, account, container,
|
1576
|
+
obj, policy=policy, **kwargs)
|
1577
|
+
return df, filenames
|
1499
1578
|
|
1500
|
-
def
|
1579
|
+
def get_diskfile_from_hash(self, device, partition, object_hash, policy,
|
1580
|
+
**kwargs):
|
1581
|
+
"""
|
1582
|
+
Returns a DiskFile instance for an object at the given object_hash.
|
1583
|
+
Just in case someone thinks of refactoring, be sure DiskFileDeleted is
|
1584
|
+
*not* raised, but the DiskFile instance representing the tombstoned
|
1585
|
+
object is returned instead.
|
1586
|
+
|
1587
|
+
:param device: name of target device
|
1588
|
+
:param partition: partition on the device in which the object lives
|
1589
|
+
:param object_hash: the hash of an object path
|
1590
|
+
:param policy: the StoragePolicy instance
|
1591
|
+
:raises DiskFileNotExist: if the object does not exist
|
1592
|
+
:returns: an instance of BaseDiskFile
|
1593
|
+
"""
|
1594
|
+
return self.get_diskfile_and_filenames_from_hash(
|
1595
|
+
device, partition, object_hash, policy, **kwargs)[0]
|
1596
|
+
|
1597
|
+
def get_hashes(self, device, partition, suffixes, policy,
|
1598
|
+
skip_rehash=False):
|
1501
1599
|
"""
|
1502
1600
|
|
1503
1601
|
:param device: name of target device
|
1504
1602
|
:param partition: partition name
|
1505
1603
|
:param suffixes: a list of suffix directories to be recalculated
|
1506
1604
|
:param policy: the StoragePolicy instance
|
1605
|
+
:param skip_rehash: just mark the suffixes dirty; return None
|
1507
1606
|
:returns: a dictionary that maps suffix directories
|
1508
1607
|
"""
|
1509
1608
|
dev_path = self.get_dev_path(device)
|
1510
1609
|
if not dev_path:
|
1511
1610
|
raise DiskFileDeviceUnavailable()
|
1512
1611
|
partition_path = get_part_path(dev_path, policy, partition)
|
1513
|
-
if
|
1514
|
-
|
1515
|
-
|
1516
|
-
|
1612
|
+
suffixes = [suf for suf in suffixes or [] if valid_suffix(suf)]
|
1613
|
+
|
1614
|
+
if skip_rehash:
|
1615
|
+
for suffix in suffixes:
|
1616
|
+
self.invalidate_hash(os.path.join(partition_path, suffix))
|
1617
|
+
hashes = None
|
1618
|
+
elif not os.path.exists(partition_path):
|
1619
|
+
hashes = {}
|
1620
|
+
else:
|
1621
|
+
_junk, hashes = tpool.execute(
|
1622
|
+
self._get_hashes, device, partition, policy,
|
1623
|
+
recalculate=suffixes)
|
1517
1624
|
return hashes
|
1518
1625
|
|
1519
1626
|
def _listdir(self, path):
|
@@ -1567,6 +1674,7 @@ class BaseDiskFileManager(object):
|
|
1567
1674
|
- ts_meta -> timestamp of meta file, if one exists
|
1568
1675
|
- ts_ctype -> timestamp of meta file containing most recent
|
1569
1676
|
content-type value, if one exists
|
1677
|
+
- durable -> True if data file at ts_data is durable, False otherwise
|
1570
1678
|
|
1571
1679
|
where timestamps are instances of
|
1572
1680
|
:class:`~swift.common.utils.Timestamp`
|
@@ -1588,11 +1696,15 @@ class BaseDiskFileManager(object):
|
|
1588
1696
|
(os.path.join(partition_path, suffix), suffix)
|
1589
1697
|
for suffix in suffixes)
|
1590
1698
|
|
1591
|
-
|
1699
|
+
# define keys that we need to extract the result from the on disk info
|
1700
|
+
# data:
|
1701
|
+
# (x, y, z) -> result[x] should take the value of y[z]
|
1702
|
+
key_map = (
|
1592
1703
|
('ts_meta', 'meta_info', 'timestamp'),
|
1593
1704
|
('ts_data', 'data_info', 'timestamp'),
|
1594
1705
|
('ts_data', 'ts_info', 'timestamp'),
|
1595
1706
|
('ts_ctype', 'ctype_info', 'ctype_timestamp'),
|
1707
|
+
('durable', 'data_info', 'durable'),
|
1596
1708
|
)
|
1597
1709
|
|
1598
1710
|
# cleanup_ondisk_files() will remove empty hash dirs, and we'll
|
@@ -1603,21 +1715,24 @@ class BaseDiskFileManager(object):
|
|
1603
1715
|
for object_hash in self._listdir(suffix_path):
|
1604
1716
|
object_path = os.path.join(suffix_path, object_hash)
|
1605
1717
|
try:
|
1606
|
-
|
1718
|
+
diskfile_info = self.cleanup_ondisk_files(
|
1607
1719
|
object_path, **kwargs)
|
1608
|
-
if
|
1720
|
+
if diskfile_info['files']:
|
1609
1721
|
found_files = True
|
1610
|
-
|
1611
|
-
for
|
1612
|
-
if
|
1722
|
+
result = {}
|
1723
|
+
for result_key, diskfile_info_key, info_key in key_map:
|
1724
|
+
if diskfile_info_key not in diskfile_info:
|
1613
1725
|
continue
|
1614
|
-
|
1615
|
-
|
1726
|
+
info = diskfile_info[diskfile_info_key]
|
1727
|
+
if info_key in info:
|
1728
|
+
# durable key not returned from replicated Diskfile
|
1729
|
+
result[result_key] = info[info_key]
|
1730
|
+
if 'ts_data' not in result:
|
1616
1731
|
# file sets that do not include a .data or .ts
|
1617
1732
|
# file cannot be opened and therefore cannot
|
1618
1733
|
# be ssync'd
|
1619
1734
|
continue
|
1620
|
-
yield
|
1735
|
+
yield object_hash, result
|
1621
1736
|
except AssertionError as err:
|
1622
1737
|
self.logger.debug('Invalid file set in %s (%s)' % (
|
1623
1738
|
object_path, err))
|
@@ -1656,25 +1771,27 @@ class BaseDiskFileWriter(object):
|
|
1656
1771
|
:param bytes_per_sync: number bytes written between sync calls
|
1657
1772
|
:param diskfile: the diskfile creating this DiskFileWriter instance
|
1658
1773
|
:param next_part_power: the next partition power to be used
|
1774
|
+
:param extension: the file extension to be used; may be used internally
|
1775
|
+
to distinguish between PUT/POST/DELETE operations
|
1659
1776
|
"""
|
1660
1777
|
|
1661
1778
|
def __init__(self, name, datadir, size, bytes_per_sync, diskfile,
|
1662
|
-
next_part_power):
|
1779
|
+
next_part_power, extension='.data'):
|
1663
1780
|
# Parameter tracking
|
1664
1781
|
self._name = name
|
1665
1782
|
self._datadir = datadir
|
1666
1783
|
self._fd = None
|
1667
1784
|
self._tmppath = None
|
1668
1785
|
self._size = size
|
1669
|
-
self._chunks_etag = md5()
|
1786
|
+
self._chunks_etag = md5(usedforsecurity=False)
|
1670
1787
|
self._bytes_per_sync = bytes_per_sync
|
1671
1788
|
self._diskfile = diskfile
|
1672
1789
|
self.next_part_power = next_part_power
|
1790
|
+
self._extension = extension
|
1673
1791
|
|
1674
1792
|
# Internal attributes
|
1675
1793
|
self._upload_size = 0
|
1676
1794
|
self._last_sync = 0
|
1677
|
-
self._extension = '.data'
|
1678
1795
|
self._put_succeeded = False
|
1679
1796
|
|
1680
1797
|
@property
|
@@ -1696,7 +1813,7 @@ class BaseDiskFileWriter(object):
|
|
1696
1813
|
msg = 'open(%s, O_TMPFILE | O_WRONLY) failed: %s \
|
1697
1814
|
Falling back to using mkstemp()' \
|
1698
1815
|
% (self._datadir, os.strerror(err.errno))
|
1699
|
-
self.logger.
|
1816
|
+
self.logger.debug(msg)
|
1700
1817
|
self.manager.use_linkat = False
|
1701
1818
|
else:
|
1702
1819
|
raise
|
@@ -1719,13 +1836,26 @@ class BaseDiskFileWriter(object):
|
|
1719
1836
|
# No more inodes in filesystem
|
1720
1837
|
raise DiskFileNoSpace()
|
1721
1838
|
raise
|
1722
|
-
if self.
|
1839
|
+
if self._extension == '.ts':
|
1840
|
+
# DELETEs always bypass any free-space reserve checks
|
1841
|
+
pass
|
1842
|
+
elif self._size:
|
1723
1843
|
try:
|
1724
1844
|
fallocate(self._fd, self._size)
|
1725
1845
|
except OSError as err:
|
1726
1846
|
if err.errno in (errno.ENOSPC, errno.EDQUOT):
|
1727
1847
|
raise DiskFileNoSpace()
|
1728
1848
|
raise
|
1849
|
+
else:
|
1850
|
+
# If we don't know the size (i.e. self._size is None) or the size
|
1851
|
+
# is known to be zero, we still want to block writes once we're
|
1852
|
+
# past the reserve threshold.
|
1853
|
+
if not fs_has_free_space(
|
1854
|
+
self._fd,
|
1855
|
+
self.manager.fallocate_reserve,
|
1856
|
+
self.manager.fallocate_is_percent
|
1857
|
+
):
|
1858
|
+
raise DiskFileNoSpace()
|
1729
1859
|
return self
|
1730
1860
|
|
1731
1861
|
def close(self):
|
@@ -1780,7 +1910,10 @@ class BaseDiskFileWriter(object):
|
|
1780
1910
|
"""
|
1781
1911
|
return self._upload_size, self._chunks_etag.hexdigest()
|
1782
1912
|
|
1783
|
-
def _finalize_put(self, metadata, target_path, cleanup
|
1913
|
+
def _finalize_put(self, metadata, target_path, cleanup,
|
1914
|
+
logger_thread_locals):
|
1915
|
+
if logger_thread_locals is not None:
|
1916
|
+
self.logger.thread_locals = logger_thread_locals
|
1784
1917
|
# Write the metadata before calling fsync() so that both data and
|
1785
1918
|
# metadata are flushed to disk.
|
1786
1919
|
write_metadata(self._fd, metadata)
|
@@ -1807,10 +1940,13 @@ class BaseDiskFileWriter(object):
|
|
1807
1940
|
new_target_path = None
|
1808
1941
|
if self.next_part_power:
|
1809
1942
|
new_target_path = replace_partition_in_path(
|
1810
|
-
target_path, self.next_part_power)
|
1943
|
+
self.manager.devices, target_path, self.next_part_power)
|
1811
1944
|
if target_path != new_target_path:
|
1812
1945
|
try:
|
1813
1946
|
fsync_dir(os.path.dirname(target_path))
|
1947
|
+
self.manager.logger.debug(
|
1948
|
+
'Relinking %s to %s due to next_part_power set',
|
1949
|
+
target_path, new_target_path)
|
1814
1950
|
relink_paths(target_path, new_target_path)
|
1815
1951
|
except OSError as exc:
|
1816
1952
|
self.manager.logger.exception(
|
@@ -1825,7 +1961,7 @@ class BaseDiskFileWriter(object):
|
|
1825
1961
|
try:
|
1826
1962
|
self.manager.cleanup_ondisk_files(self._datadir)
|
1827
1963
|
except OSError:
|
1828
|
-
logging.exception(
|
1964
|
+
logging.exception('Problem cleaning up %s', self._datadir)
|
1829
1965
|
|
1830
1966
|
self._part_power_cleanup(target_path, new_target_path)
|
1831
1967
|
|
@@ -1854,7 +1990,9 @@ class BaseDiskFileWriter(object):
|
|
1854
1990
|
metadata['name'] = self._name
|
1855
1991
|
target_path = join(self._datadir, filename)
|
1856
1992
|
|
1857
|
-
tpool.execute(
|
1993
|
+
tpool.execute(
|
1994
|
+
self._finalize_put, metadata, target_path, cleanup,
|
1995
|
+
logger_thread_locals=getattr(self.logger, 'thread_locals', None))
|
1858
1996
|
|
1859
1997
|
def put(self, metadata):
|
1860
1998
|
"""
|
@@ -1898,19 +2036,19 @@ class BaseDiskFileWriter(object):
|
|
1898
2036
|
self.manager.cleanup_ondisk_files(new_target_dir)
|
1899
2037
|
except OSError:
|
1900
2038
|
logging.exception(
|
1901
|
-
|
2039
|
+
'Problem cleaning up %s', new_target_dir)
|
1902
2040
|
|
1903
2041
|
# Partition power has been increased, cleanup not yet finished
|
1904
2042
|
else:
|
1905
2043
|
prev_part_power = int(self.next_part_power) - 1
|
1906
2044
|
old_target_path = replace_partition_in_path(
|
1907
|
-
cur_path, prev_part_power)
|
2045
|
+
self.manager.devices, cur_path, prev_part_power)
|
1908
2046
|
old_target_dir = os.path.dirname(old_target_path)
|
1909
2047
|
try:
|
1910
2048
|
self.manager.cleanup_ondisk_files(old_target_dir)
|
1911
2049
|
except OSError:
|
1912
2050
|
logging.exception(
|
1913
|
-
|
2051
|
+
'Problem cleaning up %s', old_target_dir)
|
1914
2052
|
|
1915
2053
|
|
1916
2054
|
class BaseDiskFileReader(object):
|
@@ -1943,11 +2081,16 @@ class BaseDiskFileReader(object):
|
|
1943
2081
|
:param pipe_size: size of pipe buffer used in zero-copy operations
|
1944
2082
|
:param diskfile: the diskfile creating this DiskFileReader instance
|
1945
2083
|
:param keep_cache: should resulting reads be kept in the buffer cache
|
2084
|
+
:param cooperative_period: the period parameter when does cooperative
|
2085
|
+
yielding during file read
|
2086
|
+
:param etag_validate_frac: the probability that we should perform etag
|
2087
|
+
validation during a complete file read
|
1946
2088
|
"""
|
1947
2089
|
def __init__(self, fp, data_file, obj_size, etag,
|
1948
2090
|
disk_chunk_size, keep_cache_size, device_path, logger,
|
1949
2091
|
quarantine_hook, use_splice, pipe_size, diskfile,
|
1950
|
-
keep_cache=False
|
2092
|
+
keep_cache=False, cooperative_period=0,
|
2093
|
+
etag_validate_frac=1):
|
1951
2094
|
# Parameter tracking
|
1952
2095
|
self._fp = fp
|
1953
2096
|
self._data_file = data_file
|
@@ -1966,6 +2109,8 @@ class BaseDiskFileReader(object):
|
|
1966
2109
|
self._keep_cache = obj_size < keep_cache_size
|
1967
2110
|
else:
|
1968
2111
|
self._keep_cache = False
|
2112
|
+
self._cooperative_period = cooperative_period
|
2113
|
+
self._etag_validate_frac = etag_validate_frac
|
1969
2114
|
|
1970
2115
|
# Internal Attributes
|
1971
2116
|
self._iter_etag = None
|
@@ -1983,13 +2128,18 @@ class BaseDiskFileReader(object):
|
|
1983
2128
|
def _init_checks(self):
|
1984
2129
|
if self._fp.tell() == 0:
|
1985
2130
|
self._started_at_0 = True
|
1986
|
-
|
2131
|
+
if random.random() < self._etag_validate_frac:
|
2132
|
+
self._iter_etag = md5(usedforsecurity=False)
|
1987
2133
|
|
1988
2134
|
def _update_checks(self, chunk):
|
1989
2135
|
if self._iter_etag:
|
1990
2136
|
self._iter_etag.update(chunk)
|
1991
2137
|
|
1992
2138
|
def __iter__(self):
|
2139
|
+
return CooperativeIterator(
|
2140
|
+
self._inner_iter(), period=self._cooperative_period)
|
2141
|
+
|
2142
|
+
def _inner_iter(self):
|
1993
2143
|
"""Returns an iterator over the data file."""
|
1994
2144
|
try:
|
1995
2145
|
dropped_cache = 0
|
@@ -1998,7 +2148,15 @@ class BaseDiskFileReader(object):
|
|
1998
2148
|
self._read_to_eof = False
|
1999
2149
|
self._init_checks()
|
2000
2150
|
while True:
|
2001
|
-
|
2151
|
+
try:
|
2152
|
+
chunk = self._fp.read(self._disk_chunk_size)
|
2153
|
+
except IOError as e:
|
2154
|
+
if e.errno == errno.EIO:
|
2155
|
+
# Note that if there's no quarantine hook set up,
|
2156
|
+
# this won't raise any exception
|
2157
|
+
self._quarantine(str(e))
|
2158
|
+
# ... so it's significant that this is not in an else
|
2159
|
+
raise
|
2002
2160
|
if chunk:
|
2003
2161
|
self._update_checks(chunk)
|
2004
2162
|
self._bytes_read += len(chunk)
|
@@ -2214,10 +2372,10 @@ class BaseDiskFileReader(object):
|
|
2214
2372
|
except DiskFileQuarantined:
|
2215
2373
|
raise
|
2216
2374
|
except (Exception, Timeout) as e:
|
2217
|
-
self._logger.error(
|
2375
|
+
self._logger.error(
|
2218
2376
|
'ERROR DiskFile %(data_file)s'
|
2219
|
-
' close failure: %(exc)s : %(stack)s'
|
2220
|
-
{'exc': e, 'stack': ''.join(traceback.
|
2377
|
+
' close failure: %(exc)s : %(stack)s',
|
2378
|
+
{'exc': e, 'stack': ''.join(traceback.format_exc()),
|
2221
2379
|
'data_file': self._data_file})
|
2222
2380
|
finally:
|
2223
2381
|
fp, self._fp = self._fp, None
|
@@ -2311,6 +2469,9 @@ class BaseDiskFile(object):
|
|
2311
2469
|
device_path, storage_directory(get_data_dir(policy),
|
2312
2470
|
partition, name_hash))
|
2313
2471
|
|
2472
|
+
def __repr__(self):
|
2473
|
+
return '<%s datadir=%r>' % (self.__class__.__name__, self._datadir)
|
2474
|
+
|
2314
2475
|
@property
|
2315
2476
|
def manager(self):
|
2316
2477
|
return self._manager
|
@@ -2425,6 +2586,20 @@ class BaseDiskFile(object):
|
|
2425
2586
|
# want this one file and not its parent.
|
2426
2587
|
os.path.join(self._datadir, "made-up-filename"),
|
2427
2588
|
"Expected directory, found file at %s" % self._datadir)
|
2589
|
+
elif err.errno in (errno.ENODATA, EUCLEAN):
|
2590
|
+
try:
|
2591
|
+
# We've seen cases where bad sectors lead to ENODATA here
|
2592
|
+
raise self._quarantine(
|
2593
|
+
# similar hack to above
|
2594
|
+
os.path.join(self._datadir, "made-up-filename"),
|
2595
|
+
"Failed to list directory at %s" % self._datadir)
|
2596
|
+
except (OSError, IOError):
|
2597
|
+
# We've *also* seen the bad sectors lead to us needing to
|
2598
|
+
# quarantine the whole suffix, not just the hash dir
|
2599
|
+
raise self._quarantine(
|
2600
|
+
# skip the above hack to rename the suffix
|
2601
|
+
self._datadir,
|
2602
|
+
"Failed to list directory at %s" % self._datadir)
|
2428
2603
|
elif err.errno != errno.ENOENT:
|
2429
2604
|
raise DiskFileError(
|
2430
2605
|
"Error listing directory %s: %s" % (self._datadir, err))
|
@@ -2437,8 +2612,14 @@ class BaseDiskFile(object):
|
|
2437
2612
|
self._data_file = file_info.get('data_file')
|
2438
2613
|
if not self._data_file:
|
2439
2614
|
raise self._construct_exception_from_ts_file(**file_info)
|
2440
|
-
|
2441
|
-
|
2615
|
+
try:
|
2616
|
+
self._fp = self._construct_from_data_file(
|
2617
|
+
current_time=current_time, modernize=modernize, **file_info)
|
2618
|
+
except IOError as e:
|
2619
|
+
if e.errno in (errno.ENODATA, EUCLEAN):
|
2620
|
+
raise self._quarantine(
|
2621
|
+
file_info['data_file'],
|
2622
|
+
"Failed to open %s: %s" % (file_info['data_file'], e))
|
2442
2623
|
# This method must populate the internal _metadata attribute.
|
2443
2624
|
self._metadata = self._metadata or {}
|
2444
2625
|
return self
|
@@ -2526,6 +2707,9 @@ class BaseDiskFile(object):
|
|
2526
2707
|
exc = DiskFileDeleted(metadata=metadata)
|
2527
2708
|
return exc
|
2528
2709
|
|
2710
|
+
def validate_metadata(self):
|
2711
|
+
return ('Content-Length' in self._datafile_metadata)
|
2712
|
+
|
2529
2713
|
def _verify_name_matches_hash(self, data_file):
|
2530
2714
|
"""
|
2531
2715
|
|
@@ -2562,8 +2746,8 @@ class BaseDiskFile(object):
|
|
2562
2746
|
else:
|
2563
2747
|
if mname != self._name:
|
2564
2748
|
self._logger.error(
|
2565
|
-
|
2566
|
-
|
2749
|
+
'Client path %(client)s does not match '
|
2750
|
+
'path stored in object metadata %(meta)s',
|
2567
2751
|
{'client': self._name, 'meta': mname})
|
2568
2752
|
raise DiskFileCollision('Client path does not match path '
|
2569
2753
|
'stored in object metadata')
|
@@ -2774,7 +2958,8 @@ class BaseDiskFile(object):
|
|
2774
2958
|
with self.open(current_time=current_time):
|
2775
2959
|
return self.get_metadata()
|
2776
2960
|
|
2777
|
-
def reader(self, keep_cache=False,
|
2961
|
+
def reader(self, keep_cache=False, cooperative_period=0,
|
2962
|
+
etag_validate_frac=1,
|
2778
2963
|
_quarantine_hook=lambda m: None):
|
2779
2964
|
"""
|
2780
2965
|
Return a :class:`swift.common.swob.Response` class compatible
|
@@ -2786,6 +2971,10 @@ class BaseDiskFile(object):
|
|
2786
2971
|
|
2787
2972
|
:param keep_cache: caller's preference for keeping data read in the
|
2788
2973
|
OS buffer cache
|
2974
|
+
:param cooperative_period: the period parameter for cooperative
|
2975
|
+
yielding during file read
|
2976
|
+
:param etag_validate_frac: the probability that we should perform etag
|
2977
|
+
validation during a complete file read
|
2789
2978
|
:param _quarantine_hook: 1-arg callable called when obj quarantined;
|
2790
2979
|
the arg is the reason for quarantine.
|
2791
2980
|
Default is to ignore it.
|
@@ -2797,19 +2986,24 @@ class BaseDiskFile(object):
|
|
2797
2986
|
self._metadata['ETag'], self._disk_chunk_size,
|
2798
2987
|
self._manager.keep_cache_size, self._device_path, self._logger,
|
2799
2988
|
use_splice=self._use_splice, quarantine_hook=_quarantine_hook,
|
2800
|
-
pipe_size=self._pipe_size, diskfile=self, keep_cache=keep_cache
|
2989
|
+
pipe_size=self._pipe_size, diskfile=self, keep_cache=keep_cache,
|
2990
|
+
cooperative_period=cooperative_period,
|
2991
|
+
etag_validate_frac=etag_validate_frac)
|
2801
2992
|
# At this point the reader object is now responsible for closing
|
2802
2993
|
# the file pointer.
|
2803
2994
|
self._fp = None
|
2804
2995
|
return dr
|
2805
2996
|
|
2806
|
-
def
|
2997
|
+
def _writer(self, size, extension):
|
2807
2998
|
return self.writer_cls(self._name, self._datadir, size,
|
2808
2999
|
self._bytes_per_sync, self,
|
2809
|
-
self.next_part_power)
|
3000
|
+
self.next_part_power, extension=extension)
|
3001
|
+
|
3002
|
+
def writer(self, size=None):
|
3003
|
+
return self._writer(size, '.data')
|
2810
3004
|
|
2811
3005
|
@contextmanager
|
2812
|
-
def create(self, size=None):
|
3006
|
+
def create(self, size=None, extension='.data'):
|
2813
3007
|
"""
|
2814
3008
|
Context manager to create a file. We create a temporary file first, and
|
2815
3009
|
then return a DiskFileWriter object to encapsulate the state.
|
@@ -2822,9 +3016,11 @@ class BaseDiskFile(object):
|
|
2822
3016
|
|
2823
3017
|
:param size: optional initial size of file to explicitly allocate on
|
2824
3018
|
disk
|
3019
|
+
:param extension: file extension to use for the newly-created file;
|
3020
|
+
defaults to ``.data`` for the sake of tests
|
2825
3021
|
:raises DiskFileNoSpace: if a size is specified and allocation fails
|
2826
3022
|
"""
|
2827
|
-
dfw = self.
|
3023
|
+
dfw = self._writer(size, extension)
|
2828
3024
|
try:
|
2829
3025
|
yield dfw.open()
|
2830
3026
|
finally:
|
@@ -2840,8 +3036,7 @@ class BaseDiskFile(object):
|
|
2840
3036
|
:raises DiskFileError: this implementation will raise the same
|
2841
3037
|
errors as the `create()` method.
|
2842
3038
|
"""
|
2843
|
-
with self.create() as writer:
|
2844
|
-
writer._extension = '.meta'
|
3039
|
+
with self.create(extension='.meta') as writer:
|
2845
3040
|
writer.put(metadata)
|
2846
3041
|
|
2847
3042
|
def delete(self, timestamp):
|
@@ -2863,8 +3058,7 @@ class BaseDiskFile(object):
|
|
2863
3058
|
"""
|
2864
3059
|
# this is dumb, only tests send in strings
|
2865
3060
|
timestamp = Timestamp(timestamp)
|
2866
|
-
with self.create() as deleter:
|
2867
|
-
deleter._extension = '.ts'
|
3061
|
+
with self.create(extension='.ts') as deleter:
|
2868
3062
|
deleter.put({'X-Timestamp': timestamp.internal})
|
2869
3063
|
|
2870
3064
|
|
@@ -2957,11 +3151,13 @@ class ECDiskFileReader(BaseDiskFileReader):
|
|
2957
3151
|
def __init__(self, fp, data_file, obj_size, etag,
|
2958
3152
|
disk_chunk_size, keep_cache_size, device_path, logger,
|
2959
3153
|
quarantine_hook, use_splice, pipe_size, diskfile,
|
2960
|
-
keep_cache=False
|
3154
|
+
keep_cache=False, cooperative_period=0,
|
3155
|
+
etag_validate_frac=1):
|
2961
3156
|
super(ECDiskFileReader, self).__init__(
|
2962
3157
|
fp, data_file, obj_size, etag,
|
2963
3158
|
disk_chunk_size, keep_cache_size, device_path, logger,
|
2964
|
-
quarantine_hook, use_splice, pipe_size, diskfile, keep_cache
|
3159
|
+
quarantine_hook, use_splice, pipe_size, diskfile, keep_cache,
|
3160
|
+
cooperative_period, etag_validate_frac)
|
2965
3161
|
self.frag_buf = None
|
2966
3162
|
self.frag_offset = 0
|
2967
3163
|
self.frag_size = self._diskfile.policy.fragment_size
|
@@ -2980,13 +3176,13 @@ class ECDiskFileReader(BaseDiskFileReader):
|
|
2980
3176
|
def _check_frag(self, frag):
|
2981
3177
|
if not frag:
|
2982
3178
|
return
|
2983
|
-
if not isinstance(frag,
|
3179
|
+
if not isinstance(frag, bytes):
|
2984
3180
|
# ECInvalidParameter can be returned if the frag violates the input
|
2985
3181
|
# format so for safety, check the input chunk if it's binary to
|
2986
3182
|
# avoid quarantining a valid fragment archive.
|
2987
3183
|
self._diskfile._logger.warn(
|
2988
|
-
|
2989
|
-
|
3184
|
+
'Unexpected fragment data type (not quarantined) '
|
3185
|
+
'%(datadir)s: %(type)s at offset 0x%(offset)x',
|
2990
3186
|
{'datadir': self._diskfile._datadir,
|
2991
3187
|
'type': type(frag),
|
2992
3188
|
'offset': self.frag_offset})
|
@@ -3009,7 +3205,7 @@ class ECDiskFileReader(BaseDiskFileReader):
|
|
3009
3205
|
raise DiskFileQuarantined(msg)
|
3010
3206
|
except ECDriverError as err:
|
3011
3207
|
self._diskfile._logger.warn(
|
3012
|
-
|
3208
|
+
'Problem checking EC fragment %(datadir)s: %(err)s',
|
3013
3209
|
{'datadir': self._diskfile._datadir, 'err': err})
|
3014
3210
|
|
3015
3211
|
def _update_checks(self, chunk):
|
@@ -3031,14 +3227,16 @@ class ECDiskFileReader(BaseDiskFileReader):
|
|
3031
3227
|
|
3032
3228
|
class ECDiskFileWriter(BaseDiskFileWriter):
|
3033
3229
|
|
3034
|
-
def _finalize_durable(self, data_file_path, durable_data_file_path
|
3230
|
+
def _finalize_durable(self, data_file_path, durable_data_file_path,
|
3231
|
+
timestamp):
|
3035
3232
|
exc = None
|
3036
3233
|
new_data_file_path = new_durable_data_file_path = None
|
3037
3234
|
if self.next_part_power:
|
3038
3235
|
new_data_file_path = replace_partition_in_path(
|
3039
|
-
data_file_path, self.next_part_power)
|
3236
|
+
self.manager.devices, data_file_path, self.next_part_power)
|
3040
3237
|
new_durable_data_file_path = replace_partition_in_path(
|
3041
|
-
durable_data_file_path,
|
3238
|
+
self.manager.devices, durable_data_file_path,
|
3239
|
+
self.next_part_power)
|
3042
3240
|
try:
|
3043
3241
|
try:
|
3044
3242
|
os.rename(data_file_path, durable_data_file_path)
|
@@ -3055,12 +3253,27 @@ class ECDiskFileWriter(BaseDiskFileWriter):
|
|
3055
3253
|
exc)
|
3056
3254
|
|
3057
3255
|
except (OSError, IOError) as err:
|
3256
|
+
if err.errno == errno.ENOENT:
|
3257
|
+
files = os.listdir(self._datadir)
|
3258
|
+
results = self.manager.get_ondisk_files(
|
3259
|
+
files, self._datadir,
|
3260
|
+
frag_index=self._diskfile._frag_index,
|
3261
|
+
policy=self._diskfile.policy)
|
3262
|
+
# We "succeeded" if another writer cleaned up our data
|
3263
|
+
ts_info = results.get('ts_info')
|
3264
|
+
durables = results.get('durable_frag_set', [])
|
3265
|
+
if ts_info and ts_info['timestamp'] > timestamp:
|
3266
|
+
return
|
3267
|
+
elif any(frag['timestamp'] >= timestamp
|
3268
|
+
for frag in durables):
|
3269
|
+
return
|
3270
|
+
|
3058
3271
|
if err.errno not in (errno.ENOSPC, errno.EDQUOT):
|
3059
3272
|
# re-raise to catch all handler
|
3060
3273
|
raise
|
3061
3274
|
params = {'file': durable_data_file_path, 'err': err}
|
3062
3275
|
self.manager.logger.exception(
|
3063
|
-
|
3276
|
+
'No space left on device for %(file)s (%(err)s)',
|
3064
3277
|
params)
|
3065
3278
|
exc = DiskFileNoSpace(
|
3066
3279
|
'No space left on device for %(file)s (%(err)s)' % params)
|
@@ -3069,7 +3282,7 @@ class ECDiskFileWriter(BaseDiskFileWriter):
|
|
3069
3282
|
self.manager.cleanup_ondisk_files(self._datadir)
|
3070
3283
|
except OSError as os_err:
|
3071
3284
|
self.manager.logger.exception(
|
3072
|
-
|
3285
|
+
'Problem cleaning up %(datadir)s (%(err)s)',
|
3073
3286
|
{'datadir': self._datadir, 'err': os_err})
|
3074
3287
|
self._part_power_cleanup(
|
3075
3288
|
durable_data_file_path, new_durable_data_file_path)
|
@@ -3077,7 +3290,7 @@ class ECDiskFileWriter(BaseDiskFileWriter):
|
|
3077
3290
|
except Exception as err:
|
3078
3291
|
params = {'file': durable_data_file_path, 'err': err}
|
3079
3292
|
self.manager.logger.exception(
|
3080
|
-
|
3293
|
+
'Problem making data file durable %(file)s (%(err)s)',
|
3081
3294
|
params)
|
3082
3295
|
exc = DiskFileError(
|
3083
3296
|
'Problem making data file durable %(file)s (%(err)s)' % params)
|
@@ -3102,7 +3315,8 @@ class ECDiskFileWriter(BaseDiskFileWriter):
|
|
3102
3315
|
self._datadir, self.manager.make_on_disk_filename(
|
3103
3316
|
timestamp, '.data', self._diskfile._frag_index, durable=True))
|
3104
3317
|
tpool.execute(
|
3105
|
-
self._finalize_durable, data_file_path, durable_data_file_path
|
3318
|
+
self._finalize_durable, data_file_path, durable_data_file_path,
|
3319
|
+
timestamp)
|
3106
3320
|
|
3107
3321
|
def put(self, metadata):
|
3108
3322
|
"""
|
@@ -3176,6 +3390,17 @@ class ECDiskFile(BaseDiskFile):
|
|
3176
3390
|
raise DiskFileError(
|
3177
3391
|
'Bad frag_prefs: %r: %s' % (frag_prefs, e))
|
3178
3392
|
|
3393
|
+
def validate_metadata(self):
|
3394
|
+
required_metadata = [
|
3395
|
+
'Content-Length',
|
3396
|
+
'X-Object-Sysmeta-Ec-Frag-Index',
|
3397
|
+
'X-Object-Sysmeta-Ec-Etag',
|
3398
|
+
]
|
3399
|
+
for header in required_metadata:
|
3400
|
+
if not self._datafile_metadata.get(header):
|
3401
|
+
return False
|
3402
|
+
return True
|
3403
|
+
|
3179
3404
|
@property
|
3180
3405
|
def durable_timestamp(self):
|
3181
3406
|
"""
|
@@ -3222,7 +3447,8 @@ class ECDiskFile(BaseDiskFile):
|
|
3222
3447
|
frag_prefs=self._frag_prefs, policy=policy)
|
3223
3448
|
return self._ondisk_info
|
3224
3449
|
|
3225
|
-
def purge(self, timestamp, frag_index
|
3450
|
+
def purge(self, timestamp, frag_index, nondurable_purge_delay=0,
|
3451
|
+
meta_timestamp=None):
|
3226
3452
|
"""
|
3227
3453
|
Remove a tombstone file matching the specified timestamp or
|
3228
3454
|
datafile matching the specified timestamp and fragment index
|
@@ -3239,19 +3465,36 @@ class ECDiskFile(BaseDiskFile):
|
|
3239
3465
|
:class:`~swift.common.utils.Timestamp`
|
3240
3466
|
:param frag_index: fragment archive index, must be
|
3241
3467
|
a whole number or None.
|
3468
|
+
:param nondurable_purge_delay: only remove a non-durable data file if
|
3469
|
+
it's been on disk longer than this many seconds.
|
3470
|
+
:param meta_timestamp: if not None then remove any meta file with this
|
3471
|
+
timestamp
|
3242
3472
|
"""
|
3243
3473
|
purge_file = self.manager.make_on_disk_filename(
|
3244
3474
|
timestamp, ext='.ts')
|
3245
|
-
|
3475
|
+
purge_path = os.path.join(self._datadir, purge_file)
|
3476
|
+
remove_file(purge_path)
|
3477
|
+
|
3478
|
+
if meta_timestamp is not None:
|
3479
|
+
purge_file = self.manager.make_on_disk_filename(
|
3480
|
+
meta_timestamp, ext='.meta')
|
3481
|
+
purge_path = os.path.join(self._datadir, purge_file)
|
3482
|
+
remove_file(purge_path)
|
3483
|
+
|
3246
3484
|
if frag_index is not None:
|
3247
3485
|
# data file may or may not be durable so try removing both filename
|
3248
3486
|
# possibilities
|
3249
3487
|
purge_file = self.manager.make_on_disk_filename(
|
3250
3488
|
timestamp, ext='.data', frag_index=frag_index)
|
3251
|
-
|
3489
|
+
purge_path = os.path.join(self._datadir, purge_file)
|
3490
|
+
if is_file_older(purge_path, nondurable_purge_delay):
|
3491
|
+
remove_file(purge_path)
|
3492
|
+
|
3252
3493
|
purge_file = self.manager.make_on_disk_filename(
|
3253
3494
|
timestamp, ext='.data', frag_index=frag_index, durable=True)
|
3254
|
-
|
3495
|
+
purge_path = os.path.join(self._datadir, purge_file)
|
3496
|
+
remove_file(purge_path)
|
3497
|
+
|
3255
3498
|
remove_directory(self._datadir)
|
3256
3499
|
self.manager.invalidate_hash(dirname(self._datadir))
|
3257
3500
|
|
@@ -3317,7 +3560,7 @@ class ECDiskFileManager(BaseDiskFileManager):
|
|
3317
3560
|
"""
|
3318
3561
|
Returns timestamp(s) and other info extracted from a policy specific
|
3319
3562
|
file name. For EC policy the data file name includes a fragment index
|
3320
|
-
and possibly a durable marker, both of which
|
3563
|
+
and possibly a durable marker, both of which must be stripped off
|
3321
3564
|
to retrieve the timestamp.
|
3322
3565
|
|
3323
3566
|
:param filename: the file name including extension
|
@@ -3446,6 +3689,11 @@ class ECDiskFileManager(BaseDiskFileManager):
|
|
3446
3689
|
break
|
3447
3690
|
if durable_info and durable_info['timestamp'] == timestamp:
|
3448
3691
|
durable_frag_set = frag_set
|
3692
|
+
# a data frag filename may not have the #d part if durability
|
3693
|
+
# is defined by a legacy .durable, so always mark all data
|
3694
|
+
# frags as durable here
|
3695
|
+
for frag in frag_set:
|
3696
|
+
frag['durable'] = True
|
3449
3697
|
break # ignore frags that are older than durable timestamp
|
3450
3698
|
|
3451
3699
|
# Choose which frag set to use
|
@@ -3523,7 +3771,8 @@ class ECDiskFileManager(BaseDiskFileManager):
|
|
3523
3771
|
results.setdefault('obsolete', []).extend(exts['.durable'])
|
3524
3772
|
exts.pop('.durable')
|
3525
3773
|
|
3526
|
-
# Fragments *may* be ready for reclaim, unless they are
|
3774
|
+
# Fragments *may* be ready for reclaim, unless they are most recent
|
3775
|
+
# durable
|
3527
3776
|
for frag_set in frag_sets.values():
|
3528
3777
|
if frag_set in (durable_frag_set, chosen_frag_set):
|
3529
3778
|
continue
|