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.
Files changed (206) hide show
  1. swift/__init__.py +29 -50
  2. swift/account/auditor.py +21 -118
  3. swift/account/backend.py +33 -28
  4. swift/account/reaper.py +37 -28
  5. swift/account/replicator.py +22 -0
  6. swift/account/server.py +60 -26
  7. swift/account/utils.py +28 -11
  8. swift-2.23.3.data/scripts/swift-account-audit → swift/cli/account_audit.py +23 -13
  9. swift-2.23.3.data/scripts/swift-config → swift/cli/config.py +2 -2
  10. swift/cli/container_deleter.py +5 -11
  11. swift-2.23.3.data/scripts/swift-dispersion-populate → swift/cli/dispersion_populate.py +8 -7
  12. swift/cli/dispersion_report.py +10 -9
  13. swift-2.23.3.data/scripts/swift-drive-audit → swift/cli/drive_audit.py +63 -21
  14. swift/cli/form_signature.py +3 -7
  15. swift-2.23.3.data/scripts/swift-get-nodes → swift/cli/get_nodes.py +8 -2
  16. swift/cli/info.py +154 -14
  17. swift/cli/manage_shard_ranges.py +705 -37
  18. swift-2.23.3.data/scripts/swift-oldies → swift/cli/oldies.py +25 -14
  19. swift-2.23.3.data/scripts/swift-orphans → swift/cli/orphans.py +7 -3
  20. swift/cli/recon.py +196 -67
  21. swift-2.23.3.data/scripts/swift-recon-cron → swift/cli/recon_cron.py +17 -20
  22. swift-2.23.3.data/scripts/swift-reconciler-enqueue → swift/cli/reconciler_enqueue.py +2 -3
  23. swift/cli/relinker.py +807 -126
  24. swift/cli/reload.py +135 -0
  25. swift/cli/ringbuilder.py +217 -20
  26. swift/cli/ringcomposer.py +0 -1
  27. swift/cli/shard-info.py +4 -3
  28. swift/common/base_storage_server.py +9 -20
  29. swift/common/bufferedhttp.py +48 -74
  30. swift/common/constraints.py +20 -15
  31. swift/common/container_sync_realms.py +9 -11
  32. swift/common/daemon.py +25 -8
  33. swift/common/db.py +195 -128
  34. swift/common/db_auditor.py +168 -0
  35. swift/common/db_replicator.py +95 -55
  36. swift/common/digest.py +141 -0
  37. swift/common/direct_client.py +144 -33
  38. swift/common/error_limiter.py +93 -0
  39. swift/common/exceptions.py +25 -1
  40. swift/common/header_key_dict.py +2 -9
  41. swift/common/http_protocol.py +373 -0
  42. swift/common/internal_client.py +129 -59
  43. swift/common/linkat.py +3 -4
  44. swift/common/manager.py +284 -67
  45. swift/common/memcached.py +390 -145
  46. swift/common/middleware/__init__.py +4 -0
  47. swift/common/middleware/account_quotas.py +211 -46
  48. swift/common/middleware/acl.py +3 -8
  49. swift/common/middleware/backend_ratelimit.py +230 -0
  50. swift/common/middleware/bulk.py +22 -34
  51. swift/common/middleware/catch_errors.py +1 -3
  52. swift/common/middleware/cname_lookup.py +6 -11
  53. swift/common/middleware/container_quotas.py +1 -1
  54. swift/common/middleware/container_sync.py +39 -17
  55. swift/common/middleware/copy.py +12 -0
  56. swift/common/middleware/crossdomain.py +22 -9
  57. swift/common/middleware/crypto/__init__.py +2 -1
  58. swift/common/middleware/crypto/crypto_utils.py +11 -15
  59. swift/common/middleware/crypto/decrypter.py +28 -11
  60. swift/common/middleware/crypto/encrypter.py +12 -17
  61. swift/common/middleware/crypto/keymaster.py +8 -15
  62. swift/common/middleware/crypto/kms_keymaster.py +2 -1
  63. swift/common/middleware/dlo.py +15 -11
  64. swift/common/middleware/domain_remap.py +5 -4
  65. swift/common/middleware/etag_quoter.py +128 -0
  66. swift/common/middleware/formpost.py +73 -70
  67. swift/common/middleware/gatekeeper.py +8 -1
  68. swift/common/middleware/keystoneauth.py +33 -3
  69. swift/common/middleware/list_endpoints.py +4 -4
  70. swift/common/middleware/listing_formats.py +85 -49
  71. swift/common/middleware/memcache.py +4 -95
  72. swift/common/middleware/name_check.py +3 -2
  73. swift/common/middleware/proxy_logging.py +160 -92
  74. swift/common/middleware/ratelimit.py +17 -10
  75. swift/common/middleware/read_only.py +6 -4
  76. swift/common/middleware/recon.py +59 -22
  77. swift/common/middleware/s3api/acl_handlers.py +25 -3
  78. swift/common/middleware/s3api/acl_utils.py +6 -1
  79. swift/common/middleware/s3api/controllers/__init__.py +6 -0
  80. swift/common/middleware/s3api/controllers/acl.py +3 -2
  81. swift/common/middleware/s3api/controllers/bucket.py +242 -137
  82. swift/common/middleware/s3api/controllers/logging.py +2 -2
  83. swift/common/middleware/s3api/controllers/multi_delete.py +43 -20
  84. swift/common/middleware/s3api/controllers/multi_upload.py +219 -133
  85. swift/common/middleware/s3api/controllers/obj.py +112 -8
  86. swift/common/middleware/s3api/controllers/object_lock.py +44 -0
  87. swift/common/middleware/s3api/controllers/s3_acl.py +2 -2
  88. swift/common/middleware/s3api/controllers/tagging.py +57 -0
  89. swift/common/middleware/s3api/controllers/versioning.py +36 -7
  90. swift/common/middleware/s3api/etree.py +22 -9
  91. swift/common/middleware/s3api/exception.py +0 -4
  92. swift/common/middleware/s3api/s3api.py +113 -41
  93. swift/common/middleware/s3api/s3request.py +384 -218
  94. swift/common/middleware/s3api/s3response.py +126 -23
  95. swift/common/middleware/s3api/s3token.py +16 -17
  96. swift/common/middleware/s3api/schema/delete.rng +1 -1
  97. swift/common/middleware/s3api/subresource.py +7 -10
  98. swift/common/middleware/s3api/utils.py +27 -10
  99. swift/common/middleware/slo.py +665 -358
  100. swift/common/middleware/staticweb.py +64 -37
  101. swift/common/middleware/symlink.py +51 -18
  102. swift/common/middleware/tempauth.py +76 -58
  103. swift/common/middleware/tempurl.py +191 -173
  104. swift/common/middleware/versioned_writes/__init__.py +51 -0
  105. swift/common/middleware/{versioned_writes.py → versioned_writes/legacy.py} +27 -26
  106. swift/common/middleware/versioned_writes/object_versioning.py +1482 -0
  107. swift/common/middleware/x_profile/exceptions.py +1 -4
  108. swift/common/middleware/x_profile/html_viewer.py +18 -19
  109. swift/common/middleware/x_profile/profile_model.py +1 -2
  110. swift/common/middleware/xprofile.py +10 -10
  111. swift-2.23.3.data/scripts/swift-container-server → swift/common/recon.py +13 -8
  112. swift/common/registry.py +147 -0
  113. swift/common/request_helpers.py +324 -57
  114. swift/common/ring/builder.py +67 -25
  115. swift/common/ring/composite_builder.py +1 -1
  116. swift/common/ring/ring.py +177 -51
  117. swift/common/ring/utils.py +1 -1
  118. swift/common/splice.py +10 -6
  119. swift/common/statsd_client.py +205 -0
  120. swift/common/storage_policy.py +49 -44
  121. swift/common/swob.py +86 -102
  122. swift/common/{utils.py → utils/__init__.py} +2163 -2772
  123. swift/common/utils/base.py +131 -0
  124. swift/common/utils/config.py +433 -0
  125. swift/common/utils/ipaddrs.py +256 -0
  126. swift/common/utils/libc.py +345 -0
  127. swift/common/utils/logs.py +859 -0
  128. swift/common/utils/timestamp.py +412 -0
  129. swift/common/wsgi.py +553 -535
  130. swift/container/auditor.py +14 -100
  131. swift/container/backend.py +490 -231
  132. swift/container/reconciler.py +126 -37
  133. swift/container/replicator.py +96 -22
  134. swift/container/server.py +358 -165
  135. swift/container/sharder.py +1540 -684
  136. swift/container/sync.py +94 -88
  137. swift/container/updater.py +53 -32
  138. swift/obj/auditor.py +153 -35
  139. swift/obj/diskfile.py +466 -217
  140. swift/obj/expirer.py +406 -124
  141. swift/obj/mem_diskfile.py +7 -4
  142. swift/obj/mem_server.py +1 -0
  143. swift/obj/reconstructor.py +523 -262
  144. swift/obj/replicator.py +249 -188
  145. swift/obj/server.py +207 -122
  146. swift/obj/ssync_receiver.py +145 -85
  147. swift/obj/ssync_sender.py +113 -54
  148. swift/obj/updater.py +652 -139
  149. swift/obj/watchers/__init__.py +0 -0
  150. swift/obj/watchers/dark_data.py +213 -0
  151. swift/proxy/controllers/account.py +11 -11
  152. swift/proxy/controllers/base.py +848 -604
  153. swift/proxy/controllers/container.py +433 -92
  154. swift/proxy/controllers/info.py +3 -2
  155. swift/proxy/controllers/obj.py +1000 -489
  156. swift/proxy/server.py +185 -112
  157. {swift-2.23.3.dist-info → swift-2.35.0.dist-info}/AUTHORS +58 -11
  158. {swift-2.23.3.dist-info → swift-2.35.0.dist-info}/METADATA +51 -56
  159. swift-2.35.0.dist-info/RECORD +201 -0
  160. {swift-2.23.3.dist-info → swift-2.35.0.dist-info}/WHEEL +1 -1
  161. {swift-2.23.3.dist-info → swift-2.35.0.dist-info}/entry_points.txt +43 -0
  162. swift-2.35.0.dist-info/pbr.json +1 -0
  163. swift/locale/de/LC_MESSAGES/swift.po +0 -1216
  164. swift/locale/en_GB/LC_MESSAGES/swift.po +0 -1207
  165. swift/locale/es/LC_MESSAGES/swift.po +0 -1085
  166. swift/locale/fr/LC_MESSAGES/swift.po +0 -909
  167. swift/locale/it/LC_MESSAGES/swift.po +0 -894
  168. swift/locale/ja/LC_MESSAGES/swift.po +0 -965
  169. swift/locale/ko_KR/LC_MESSAGES/swift.po +0 -964
  170. swift/locale/pt_BR/LC_MESSAGES/swift.po +0 -881
  171. swift/locale/ru/LC_MESSAGES/swift.po +0 -891
  172. swift/locale/tr_TR/LC_MESSAGES/swift.po +0 -832
  173. swift/locale/zh_CN/LC_MESSAGES/swift.po +0 -833
  174. swift/locale/zh_TW/LC_MESSAGES/swift.po +0 -838
  175. swift-2.23.3.data/scripts/swift-account-auditor +0 -23
  176. swift-2.23.3.data/scripts/swift-account-info +0 -51
  177. swift-2.23.3.data/scripts/swift-account-reaper +0 -23
  178. swift-2.23.3.data/scripts/swift-account-replicator +0 -34
  179. swift-2.23.3.data/scripts/swift-account-server +0 -23
  180. swift-2.23.3.data/scripts/swift-container-auditor +0 -23
  181. swift-2.23.3.data/scripts/swift-container-info +0 -55
  182. swift-2.23.3.data/scripts/swift-container-reconciler +0 -21
  183. swift-2.23.3.data/scripts/swift-container-replicator +0 -34
  184. swift-2.23.3.data/scripts/swift-container-sharder +0 -37
  185. swift-2.23.3.data/scripts/swift-container-sync +0 -23
  186. swift-2.23.3.data/scripts/swift-container-updater +0 -23
  187. swift-2.23.3.data/scripts/swift-dispersion-report +0 -24
  188. swift-2.23.3.data/scripts/swift-form-signature +0 -20
  189. swift-2.23.3.data/scripts/swift-init +0 -119
  190. swift-2.23.3.data/scripts/swift-object-auditor +0 -29
  191. swift-2.23.3.data/scripts/swift-object-expirer +0 -33
  192. swift-2.23.3.data/scripts/swift-object-info +0 -60
  193. swift-2.23.3.data/scripts/swift-object-reconstructor +0 -33
  194. swift-2.23.3.data/scripts/swift-object-relinker +0 -41
  195. swift-2.23.3.data/scripts/swift-object-replicator +0 -37
  196. swift-2.23.3.data/scripts/swift-object-server +0 -27
  197. swift-2.23.3.data/scripts/swift-object-updater +0 -23
  198. swift-2.23.3.data/scripts/swift-proxy-server +0 -23
  199. swift-2.23.3.data/scripts/swift-recon +0 -24
  200. swift-2.23.3.data/scripts/swift-ring-builder +0 -24
  201. swift-2.23.3.data/scripts/swift-ring-builder-analyzer +0 -22
  202. swift-2.23.3.data/scripts/swift-ring-composer +0 -22
  203. swift-2.23.3.dist-info/RECORD +0 -220
  204. swift-2.23.3.dist-info/pbr.json +0 -1
  205. {swift-2.23.3.dist-info → swift-2.35.0.dist-info}/LICENSE +0 -0
  206. {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 six.moves.cPickle as pickle
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
- from random import shuffle
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
- if six.PY2:
157
- def encode_str(item):
158
- if isinstance(item, six.text_type):
159
- return item.encode('utf8')
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
- if six.PY2:
177
- def to_str(item):
178
- if isinstance(item, six.text_type):
179
- return item.encode('utf8')
180
- return item
181
- else:
182
- def to_str(item):
183
- if isinstance(item, six.binary_type):
184
- return item.decode('utf8', 'surrogateescape')
185
- return item
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 dict(((to_str(k), to_str(v)) for k, v in metadata.items()))
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).hexdigest().encode('ascii')
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).hexdigest().encode('ascii')
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
- if six.PY2:
243
- metadata = pickle.loads(metadata)
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 = md5(metastr).hexdigest().encode('ascii')
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
- invalidate_hash(dirname(from_dir))
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
- for suffix in hashes.keys():
366
- if not suffix.isalnum():
367
- return {'valid': False}
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 found_invalidation_entry:
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, check_existing=False):
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. Creates intermediate directories if required.
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 check_existing: if True, check whether the link is already present
451
- before attempting to create a new one
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
- if not os.path.isdir(new_target_dir):
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
- link_exists = False
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 != errno.ENOTDIR:
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 != errno.ENOTDIR:
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
- if six.PY3:
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(_('Cannot read %(auditor_status)s (%(err)s)') %
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(_('Loading JSON from %(auditor_status)s failed'
588
- ' (%(err)s)') %
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(_('Cannot write %(auditor_status)s (%(err)s)') %
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 = strip_self(invalidate_hash)
677
- consolidate_hashes = strip_self(consolidate_hashes)
678
- quarantine_renamer = strip_self(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
- assert self._verify_ondisk_files(
1039
- results, **kwargs), \
1040
- "On-disk file search algorithm contract is broken: %s" \
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
- if is_reclaimable(file_info['timestamp']):
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
- if six.PY2:
1116
- hashes = defaultdict(md5)
1117
- else:
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
- def update(self, s):
1123
- if isinstance(s, str):
1124
- self.md5.update(s.encode('utf-8'))
1125
- else:
1126
- self.md5.update(s)
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
- def hexdigest(self):
1129
- return self.md5.hexdigest()
1130
- hashes = defaultdict(shim)
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
- _('Quarantined %(hsh_path)s to %(quar_path)s because '
1157
- 'it is not a directory'), {'hsh_path': hsh_path,
1158
- 'quar_path': quar_path})
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(_('Error hashing suffix'))
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 device given, if
1337
- configured to do so.
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
- if self.replication_concurrency_per_device:
1346
- dev_path = self.get_dev_path(device)
1347
- part_path = os.path.join(dev_path, get_data_dir(policy),
1348
- str(partition))
1349
- limit_time = time.time() + self.replication_lock_timeout
1350
- with lock_path(
1351
- dev_path,
1352
- timeout=self.replication_lock_timeout,
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
- else:
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 get_diskfile_from_hash(self, device, partition, object_hash,
1444
- policy, **kwargs):
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. Just in case someone thinks of refactoring, be
1448
- sure DiskFileDeleted is *not* raised, but the DiskFile
1449
- instance representing the tombstoned object is returned
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
- _('Quarantined %(object_path)s to %(quar_path)s because '
1479
- 'it is not a directory'), {'object_path': object_path,
1480
- 'quar_path': quar_path})
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
- return self.diskfile_cls(self, dev_path,
1497
- partition, account, container, obj,
1498
- policy=policy, **kwargs)
1575
+ df = self.diskfile_cls(self, dev_path, partition, account, container,
1576
+ obj, policy=policy, **kwargs)
1577
+ return df, filenames
1499
1578
 
1500
- def get_hashes(self, device, partition, suffixes, policy):
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 not os.path.exists(partition_path):
1514
- mkdirs(partition_path)
1515
- _junk, hashes = tpool.execute(
1516
- self._get_hashes, device, partition, policy, recalculate=suffixes)
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
- key_preference = (
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
- results = self.cleanup_ondisk_files(
1718
+ diskfile_info = self.cleanup_ondisk_files(
1607
1719
  object_path, **kwargs)
1608
- if results['files']:
1720
+ if diskfile_info['files']:
1609
1721
  found_files = True
1610
- timestamps = {}
1611
- for ts_key, info_key, info_ts_key in key_preference:
1612
- if info_key not in results:
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
- timestamps[ts_key] = results[info_key][info_ts_key]
1615
- if 'ts_data' not in timestamps:
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 (object_hash, timestamps)
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.warning(msg)
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._size is not None and self._size > 0:
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(_('Problem cleaning up %s'), self._datadir)
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(self._finalize_put, metadata, target_path, cleanup)
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
- _('Problem cleaning up %s'), new_target_dir)
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
- _('Problem cleaning up %s'), old_target_dir)
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
- self._iter_etag = md5()
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
- chunk = self._fp.read(self._disk_chunk_size)
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.format_stack()),
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
- self._fp = self._construct_from_data_file(
2441
- current_time=current_time, modernize=modernize, **file_info)
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
- _('Client path %(client)s does not match '
2566
- 'path stored in object metadata %(meta)s'),
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 writer(self, size=None):
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.writer(size)
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, six.binary_type):
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
- _('Unexpected fragment data type (not quarantined) '
2989
- '%(datadir)s: %(type)s at offset 0x%(offset)x'),
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
- _('Problem checking EC fragment %(datadir)s: %(err)s'),
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, self.next_part_power)
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
- _('No space left on device for %(file)s (%(err)s)'),
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
- _('Problem cleaning up %(datadir)s (%(err)s)'),
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
- _('Problem making data file durable %(file)s (%(err)s)'),
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
- remove_file(os.path.join(self._datadir, purge_file))
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
- remove_file(os.path.join(self._datadir, purge_file))
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
- remove_file(os.path.join(self._datadir, purge_file))
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 which must be stripped off
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 durable
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