swift 2.32.0__py2.py3-none-any.whl → 2.34.0__py2.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 (127) hide show
  1. swift/account/auditor.py +11 -0
  2. swift/account/reaper.py +11 -1
  3. swift/account/replicator.py +22 -0
  4. swift/account/server.py +13 -12
  5. swift-2.32.0.data/scripts/swift-account-audit → swift/cli/account_audit.py +6 -2
  6. swift-2.32.0.data/scripts/swift-config → swift/cli/config.py +1 -1
  7. swift-2.32.0.data/scripts/swift-dispersion-populate → swift/cli/dispersion_populate.py +6 -2
  8. swift-2.32.0.data/scripts/swift-drive-audit → swift/cli/drive_audit.py +12 -3
  9. swift-2.32.0.data/scripts/swift-get-nodes → swift/cli/get_nodes.py +6 -2
  10. swift/cli/info.py +131 -3
  11. swift-2.32.0.data/scripts/swift-oldies → swift/cli/oldies.py +6 -3
  12. swift-2.32.0.data/scripts/swift-orphans → swift/cli/orphans.py +7 -2
  13. swift-2.32.0.data/scripts/swift-recon-cron → swift/cli/recon_cron.py +9 -18
  14. swift-2.32.0.data/scripts/swift-reconciler-enqueue → swift/cli/reconciler_enqueue.py +2 -3
  15. swift/cli/relinker.py +1 -1
  16. swift/cli/reload.py +141 -0
  17. swift/cli/ringbuilder.py +24 -0
  18. swift/common/daemon.py +12 -2
  19. swift/common/db.py +14 -9
  20. swift/common/db_auditor.py +2 -2
  21. swift/common/db_replicator.py +6 -0
  22. swift/common/exceptions.py +12 -0
  23. swift/common/http_protocol.py +76 -3
  24. swift/common/manager.py +120 -5
  25. swift/common/memcached.py +24 -25
  26. swift/common/middleware/account_quotas.py +144 -43
  27. swift/common/middleware/backend_ratelimit.py +166 -24
  28. swift/common/middleware/catch_errors.py +1 -3
  29. swift/common/middleware/cname_lookup.py +3 -5
  30. swift/common/middleware/container_sync.py +6 -10
  31. swift/common/middleware/crypto/crypto_utils.py +4 -5
  32. swift/common/middleware/crypto/decrypter.py +4 -5
  33. swift/common/middleware/crypto/kms_keymaster.py +2 -1
  34. swift/common/middleware/proxy_logging.py +57 -43
  35. swift/common/middleware/ratelimit.py +6 -7
  36. swift/common/middleware/recon.py +6 -7
  37. swift/common/middleware/s3api/acl_handlers.py +10 -1
  38. swift/common/middleware/s3api/controllers/__init__.py +3 -0
  39. swift/common/middleware/s3api/controllers/acl.py +3 -2
  40. swift/common/middleware/s3api/controllers/logging.py +2 -2
  41. swift/common/middleware/s3api/controllers/multi_upload.py +31 -15
  42. swift/common/middleware/s3api/controllers/obj.py +20 -1
  43. swift/common/middleware/s3api/controllers/object_lock.py +44 -0
  44. swift/common/middleware/s3api/s3api.py +6 -0
  45. swift/common/middleware/s3api/s3request.py +190 -74
  46. swift/common/middleware/s3api/s3response.py +48 -8
  47. swift/common/middleware/s3api/s3token.py +2 -2
  48. swift/common/middleware/s3api/utils.py +2 -1
  49. swift/common/middleware/slo.py +508 -310
  50. swift/common/middleware/staticweb.py +45 -14
  51. swift/common/middleware/tempauth.py +6 -4
  52. swift/common/middleware/tempurl.py +134 -93
  53. swift/common/middleware/x_profile/exceptions.py +1 -4
  54. swift/common/middleware/x_profile/html_viewer.py +9 -10
  55. swift/common/middleware/x_profile/profile_model.py +1 -2
  56. swift/common/middleware/xprofile.py +1 -2
  57. swift/common/request_helpers.py +101 -8
  58. swift/common/statsd_client.py +207 -0
  59. swift/common/storage_policy.py +1 -1
  60. swift/common/swob.py +5 -2
  61. swift/common/utils/__init__.py +331 -1774
  62. swift/common/utils/base.py +138 -0
  63. swift/common/utils/config.py +443 -0
  64. swift/common/utils/logs.py +999 -0
  65. swift/common/utils/timestamp.py +23 -2
  66. swift/common/wsgi.py +19 -3
  67. swift/container/auditor.py +11 -0
  68. swift/container/backend.py +136 -31
  69. swift/container/reconciler.py +11 -2
  70. swift/container/replicator.py +64 -7
  71. swift/container/server.py +276 -146
  72. swift/container/sharder.py +86 -42
  73. swift/container/sync.py +11 -1
  74. swift/container/updater.py +12 -2
  75. swift/obj/auditor.py +20 -3
  76. swift/obj/diskfile.py +63 -25
  77. swift/obj/expirer.py +154 -47
  78. swift/obj/mem_diskfile.py +2 -1
  79. swift/obj/mem_server.py +1 -0
  80. swift/obj/reconstructor.py +28 -4
  81. swift/obj/replicator.py +63 -24
  82. swift/obj/server.py +76 -59
  83. swift/obj/updater.py +12 -2
  84. swift/obj/watchers/dark_data.py +72 -34
  85. swift/proxy/controllers/account.py +3 -2
  86. swift/proxy/controllers/base.py +254 -148
  87. swift/proxy/controllers/container.py +274 -289
  88. swift/proxy/controllers/obj.py +120 -166
  89. swift/proxy/server.py +17 -13
  90. {swift-2.32.0.dist-info → swift-2.34.0.dist-info}/AUTHORS +14 -4
  91. {swift-2.32.0.dist-info → swift-2.34.0.dist-info}/METADATA +9 -7
  92. {swift-2.32.0.dist-info → swift-2.34.0.dist-info}/RECORD +97 -120
  93. {swift-2.32.0.dist-info → swift-2.34.0.dist-info}/entry_points.txt +39 -0
  94. swift-2.34.0.dist-info/pbr.json +1 -0
  95. swift-2.32.0.data/scripts/swift-account-auditor +0 -23
  96. swift-2.32.0.data/scripts/swift-account-info +0 -52
  97. swift-2.32.0.data/scripts/swift-account-reaper +0 -23
  98. swift-2.32.0.data/scripts/swift-account-replicator +0 -34
  99. swift-2.32.0.data/scripts/swift-account-server +0 -23
  100. swift-2.32.0.data/scripts/swift-container-auditor +0 -23
  101. swift-2.32.0.data/scripts/swift-container-info +0 -56
  102. swift-2.32.0.data/scripts/swift-container-reconciler +0 -21
  103. swift-2.32.0.data/scripts/swift-container-replicator +0 -34
  104. swift-2.32.0.data/scripts/swift-container-server +0 -23
  105. swift-2.32.0.data/scripts/swift-container-sharder +0 -37
  106. swift-2.32.0.data/scripts/swift-container-sync +0 -23
  107. swift-2.32.0.data/scripts/swift-container-updater +0 -23
  108. swift-2.32.0.data/scripts/swift-dispersion-report +0 -24
  109. swift-2.32.0.data/scripts/swift-form-signature +0 -20
  110. swift-2.32.0.data/scripts/swift-init +0 -119
  111. swift-2.32.0.data/scripts/swift-object-auditor +0 -29
  112. swift-2.32.0.data/scripts/swift-object-expirer +0 -33
  113. swift-2.32.0.data/scripts/swift-object-info +0 -60
  114. swift-2.32.0.data/scripts/swift-object-reconstructor +0 -33
  115. swift-2.32.0.data/scripts/swift-object-relinker +0 -23
  116. swift-2.32.0.data/scripts/swift-object-replicator +0 -37
  117. swift-2.32.0.data/scripts/swift-object-server +0 -27
  118. swift-2.32.0.data/scripts/swift-object-updater +0 -23
  119. swift-2.32.0.data/scripts/swift-proxy-server +0 -23
  120. swift-2.32.0.data/scripts/swift-recon +0 -24
  121. swift-2.32.0.data/scripts/swift-ring-builder +0 -37
  122. swift-2.32.0.data/scripts/swift-ring-builder-analyzer +0 -22
  123. swift-2.32.0.data/scripts/swift-ring-composer +0 -22
  124. swift-2.32.0.dist-info/pbr.json +0 -1
  125. {swift-2.32.0.dist-info → swift-2.34.0.dist-info}/LICENSE +0 -0
  126. {swift-2.32.0.dist-info → swift-2.34.0.dist-info}/WHEEL +0 -0
  127. {swift-2.32.0.dist-info → swift-2.34.0.dist-info}/top_level.txt +0 -0
swift/obj/diskfile.py CHANGED
@@ -65,7 +65,8 @@ from swift.common.utils import mkdirs, Timestamp, \
65
65
  get_md5_socket, F_SETPIPE_SZ, decode_timestamps, encode_timestamps, \
66
66
  MD5_OF_EMPTY_STRING, link_fd_to_path, \
67
67
  O_TMPFILE, makedirs_count, replace_partition_in_path, remove_directory, \
68
- md5, is_file_older, non_negative_float
68
+ md5, is_file_older, non_negative_float, config_fallocate_value, \
69
+ fs_has_free_space, CooperativeIterator
69
70
  from swift.common.splice import splice, tee
70
71
  from swift.common.exceptions import DiskFileQuarantined, DiskFileNotExist, \
71
72
  DiskFileCollision, DiskFileNoSpace, DiskFileDeviceUnavailable, \
@@ -428,19 +429,23 @@ def consolidate_hashes(partition_dir):
428
429
  with lock_path(partition_dir):
429
430
  hashes = read_hashes(partition_dir)
430
431
 
431
- found_invalidation_entry = False
432
+ found_invalidation_entry = hashes_updated = False
432
433
  try:
433
434
  with open(invalidations_file, 'r') as inv_fh:
434
435
  for line in inv_fh:
435
436
  found_invalidation_entry = True
436
437
  suffix = line.strip()
438
+ if not valid_suffix(suffix):
439
+ continue
440
+ hashes_updated = True
437
441
  hashes[suffix] = None
438
442
  except (IOError, OSError) as e:
439
443
  if e.errno != errno.ENOENT:
440
444
  raise
441
445
 
442
- if found_invalidation_entry:
446
+ if hashes_updated:
443
447
  write_hashes(partition_dir, hashes)
448
+ if found_invalidation_entry:
444
449
  # Now that all the invalidations are reflected in hashes.pkl, it's
445
450
  # safe to clear out the invalidations file.
446
451
  with open(invalidations_file, 'wb') as inv_fh:
@@ -595,7 +600,8 @@ def object_audit_location_generator(devices, datadir, mount_check=True,
595
600
  try:
596
601
  suffixes = listdir(part_path)
597
602
  except OSError as e:
598
- if e.errno not in (errno.ENOTDIR, errno.ENODATA):
603
+ if e.errno not in (errno.ENOTDIR, errno.ENODATA,
604
+ errno.EUCLEAN):
599
605
  raise
600
606
  continue
601
607
  for asuffix in suffixes:
@@ -603,7 +609,8 @@ def object_audit_location_generator(devices, datadir, mount_check=True,
603
609
  try:
604
610
  hashes = listdir(suff_path)
605
611
  except OSError as e:
606
- if e.errno not in (errno.ENOTDIR, errno.ENODATA):
612
+ if e.errno not in (errno.ENOTDIR, errno.ENODATA,
613
+ errno.EUCLEAN):
607
614
  raise
608
615
  continue
609
616
  for hsh in hashes:
@@ -751,6 +758,8 @@ class BaseDiskFileManager(object):
751
758
  replication_concurrency_per_device)
752
759
  self.replication_lock_timeout = int(conf.get(
753
760
  'replication_lock_timeout', 15))
761
+ self.fallocate_reserve, self.fallocate_is_percent = \
762
+ config_fallocate_value(conf.get('fallocate_reserve', '1%'))
754
763
 
755
764
  self.use_splice = False
756
765
  self.pipe_size = None
@@ -1207,7 +1216,7 @@ class BaseDiskFileManager(object):
1207
1216
  'it is not a directory', {'hsh_path': hsh_path,
1208
1217
  'quar_path': quar_path})
1209
1218
  continue
1210
- elif err.errno == errno.ENODATA:
1219
+ elif err.errno in (errno.ENODATA, errno.EUCLEAN):
1211
1220
  try:
1212
1221
  # We've seen cases where bad sectors lead to ENODATA
1213
1222
  # here; use a similar hack as above
@@ -1562,7 +1571,7 @@ class BaseDiskFileManager(object):
1562
1571
  'it is not a directory', {'object_path': object_path,
1563
1572
  'quar_path': quar_path})
1564
1573
  raise DiskFileNotExist()
1565
- elif err.errno == errno.ENODATA:
1574
+ elif err.errno in (errno.ENODATA, errno.EUCLEAN):
1566
1575
  try:
1567
1576
  # We've seen cases where bad sectors lead to ENODATA here;
1568
1577
  # use a similar hack as above
@@ -1793,10 +1802,12 @@ class BaseDiskFileWriter(object):
1793
1802
  :param bytes_per_sync: number bytes written between sync calls
1794
1803
  :param diskfile: the diskfile creating this DiskFileWriter instance
1795
1804
  :param next_part_power: the next partition power to be used
1805
+ :param extension: the file extension to be used; may be used internally
1806
+ to distinguish between PUT/POST/DELETE operations
1796
1807
  """
1797
1808
 
1798
1809
  def __init__(self, name, datadir, size, bytes_per_sync, diskfile,
1799
- next_part_power):
1810
+ next_part_power, extension='.data'):
1800
1811
  # Parameter tracking
1801
1812
  self._name = name
1802
1813
  self._datadir = datadir
@@ -1807,11 +1818,11 @@ class BaseDiskFileWriter(object):
1807
1818
  self._bytes_per_sync = bytes_per_sync
1808
1819
  self._diskfile = diskfile
1809
1820
  self.next_part_power = next_part_power
1821
+ self._extension = extension
1810
1822
 
1811
1823
  # Internal attributes
1812
1824
  self._upload_size = 0
1813
1825
  self._last_sync = 0
1814
- self._extension = '.data'
1815
1826
  self._put_succeeded = False
1816
1827
 
1817
1828
  @property
@@ -1856,13 +1867,26 @@ class BaseDiskFileWriter(object):
1856
1867
  # No more inodes in filesystem
1857
1868
  raise DiskFileNoSpace()
1858
1869
  raise
1859
- if self._size is not None and self._size > 0:
1870
+ if self._extension == '.ts':
1871
+ # DELETEs always bypass any free-space reserve checks
1872
+ pass
1873
+ elif self._size:
1860
1874
  try:
1861
1875
  fallocate(self._fd, self._size)
1862
1876
  except OSError as err:
1863
1877
  if err.errno in (errno.ENOSPC, errno.EDQUOT):
1864
1878
  raise DiskFileNoSpace()
1865
1879
  raise
1880
+ else:
1881
+ # If we don't know the size (i.e. self._size is None) or the size
1882
+ # is known to be zero, we still want to block writes once we're
1883
+ # past the reserve threshold.
1884
+ if not fs_has_free_space(
1885
+ self._fd,
1886
+ self.manager.fallocate_reserve,
1887
+ self.manager.fallocate_is_percent
1888
+ ):
1889
+ raise DiskFileNoSpace()
1866
1890
  return self
1867
1891
 
1868
1892
  def close(self):
@@ -2088,11 +2112,13 @@ class BaseDiskFileReader(object):
2088
2112
  :param pipe_size: size of pipe buffer used in zero-copy operations
2089
2113
  :param diskfile: the diskfile creating this DiskFileReader instance
2090
2114
  :param keep_cache: should resulting reads be kept in the buffer cache
2115
+ :param cooperative_period: the period parameter when does cooperative
2116
+ yielding during file read
2091
2117
  """
2092
2118
  def __init__(self, fp, data_file, obj_size, etag,
2093
2119
  disk_chunk_size, keep_cache_size, device_path, logger,
2094
2120
  quarantine_hook, use_splice, pipe_size, diskfile,
2095
- keep_cache=False):
2121
+ keep_cache=False, cooperative_period=0):
2096
2122
  # Parameter tracking
2097
2123
  self._fp = fp
2098
2124
  self._data_file = data_file
@@ -2111,6 +2137,7 @@ class BaseDiskFileReader(object):
2111
2137
  self._keep_cache = obj_size < keep_cache_size
2112
2138
  else:
2113
2139
  self._keep_cache = False
2140
+ self._cooperative_period = cooperative_period
2114
2141
 
2115
2142
  # Internal Attributes
2116
2143
  self._iter_etag = None
@@ -2135,6 +2162,10 @@ class BaseDiskFileReader(object):
2135
2162
  self._iter_etag.update(chunk)
2136
2163
 
2137
2164
  def __iter__(self):
2165
+ return CooperativeIterator(
2166
+ self._inner_iter(), period=self._cooperative_period)
2167
+
2168
+ def _inner_iter(self):
2138
2169
  """Returns an iterator over the data file."""
2139
2170
  try:
2140
2171
  dropped_cache = 0
@@ -2581,7 +2612,7 @@ class BaseDiskFile(object):
2581
2612
  # want this one file and not its parent.
2582
2613
  os.path.join(self._datadir, "made-up-filename"),
2583
2614
  "Expected directory, found file at %s" % self._datadir)
2584
- elif err.errno == errno.ENODATA:
2615
+ elif err.errno in (errno.ENODATA, errno.EUCLEAN):
2585
2616
  try:
2586
2617
  # We've seen cases where bad sectors lead to ENODATA here
2587
2618
  raise self._quarantine(
@@ -2611,7 +2642,7 @@ class BaseDiskFile(object):
2611
2642
  self._fp = self._construct_from_data_file(
2612
2643
  current_time=current_time, modernize=modernize, **file_info)
2613
2644
  except IOError as e:
2614
- if e.errno == errno.ENODATA:
2645
+ if e.errno in (errno.ENODATA, errno.EUCLEAN):
2615
2646
  raise self._quarantine(
2616
2647
  file_info['data_file'],
2617
2648
  "Failed to open %s: %s" % (file_info['data_file'], e))
@@ -2953,7 +2984,7 @@ class BaseDiskFile(object):
2953
2984
  with self.open(current_time=current_time):
2954
2985
  return self.get_metadata()
2955
2986
 
2956
- def reader(self, keep_cache=False,
2987
+ def reader(self, keep_cache=False, cooperative_period=0,
2957
2988
  _quarantine_hook=lambda m: None):
2958
2989
  """
2959
2990
  Return a :class:`swift.common.swob.Response` class compatible
@@ -2965,6 +2996,8 @@ class BaseDiskFile(object):
2965
2996
 
2966
2997
  :param keep_cache: caller's preference for keeping data read in the
2967
2998
  OS buffer cache
2999
+ :param cooperative_period: the period parameter for cooperative
3000
+ yielding during file read
2968
3001
  :param _quarantine_hook: 1-arg callable called when obj quarantined;
2969
3002
  the arg is the reason for quarantine.
2970
3003
  Default is to ignore it.
@@ -2976,19 +3009,23 @@ class BaseDiskFile(object):
2976
3009
  self._metadata['ETag'], self._disk_chunk_size,
2977
3010
  self._manager.keep_cache_size, self._device_path, self._logger,
2978
3011
  use_splice=self._use_splice, quarantine_hook=_quarantine_hook,
2979
- pipe_size=self._pipe_size, diskfile=self, keep_cache=keep_cache)
3012
+ pipe_size=self._pipe_size, diskfile=self, keep_cache=keep_cache,
3013
+ cooperative_period=cooperative_period)
2980
3014
  # At this point the reader object is now responsible for closing
2981
3015
  # the file pointer.
2982
3016
  self._fp = None
2983
3017
  return dr
2984
3018
 
2985
- def writer(self, size=None):
3019
+ def _writer(self, size, extension):
2986
3020
  return self.writer_cls(self._name, self._datadir, size,
2987
3021
  self._bytes_per_sync, self,
2988
- self.next_part_power)
3022
+ self.next_part_power, extension=extension)
3023
+
3024
+ def writer(self, size=None):
3025
+ return self._writer(size, '.data')
2989
3026
 
2990
3027
  @contextmanager
2991
- def create(self, size=None):
3028
+ def create(self, size=None, extension='.data'):
2992
3029
  """
2993
3030
  Context manager to create a file. We create a temporary file first, and
2994
3031
  then return a DiskFileWriter object to encapsulate the state.
@@ -3001,9 +3038,11 @@ class BaseDiskFile(object):
3001
3038
 
3002
3039
  :param size: optional initial size of file to explicitly allocate on
3003
3040
  disk
3041
+ :param extension: file extension to use for the newly-created file;
3042
+ defaults to ``.data`` for the sake of tests
3004
3043
  :raises DiskFileNoSpace: if a size is specified and allocation fails
3005
3044
  """
3006
- dfw = self.writer(size)
3045
+ dfw = self._writer(size, extension)
3007
3046
  try:
3008
3047
  yield dfw.open()
3009
3048
  finally:
@@ -3019,8 +3058,7 @@ class BaseDiskFile(object):
3019
3058
  :raises DiskFileError: this implementation will raise the same
3020
3059
  errors as the `create()` method.
3021
3060
  """
3022
- with self.create() as writer:
3023
- writer._extension = '.meta'
3061
+ with self.create(extension='.meta') as writer:
3024
3062
  writer.put(metadata)
3025
3063
 
3026
3064
  def delete(self, timestamp):
@@ -3042,8 +3080,7 @@ class BaseDiskFile(object):
3042
3080
  """
3043
3081
  # this is dumb, only tests send in strings
3044
3082
  timestamp = Timestamp(timestamp)
3045
- with self.create() as deleter:
3046
- deleter._extension = '.ts'
3083
+ with self.create(extension='.ts') as deleter:
3047
3084
  deleter.put({'X-Timestamp': timestamp.internal})
3048
3085
 
3049
3086
 
@@ -3136,11 +3173,12 @@ class ECDiskFileReader(BaseDiskFileReader):
3136
3173
  def __init__(self, fp, data_file, obj_size, etag,
3137
3174
  disk_chunk_size, keep_cache_size, device_path, logger,
3138
3175
  quarantine_hook, use_splice, pipe_size, diskfile,
3139
- keep_cache=False):
3176
+ keep_cache=False, cooperative_period=0):
3140
3177
  super(ECDiskFileReader, self).__init__(
3141
3178
  fp, data_file, obj_size, etag,
3142
3179
  disk_chunk_size, keep_cache_size, device_path, logger,
3143
- quarantine_hook, use_splice, pipe_size, diskfile, keep_cache)
3180
+ quarantine_hook, use_splice, pipe_size, diskfile, keep_cache,
3181
+ cooperative_period)
3144
3182
  self.frag_buf = None
3145
3183
  self.frag_offset = 0
3146
3184
  self.frag_size = self._diskfile.policy.fragment_size
swift/obj/expirer.py CHANGED
@@ -14,9 +14,11 @@
14
14
  # limitations under the License.
15
15
 
16
16
  import six
17
+ from six.moves import urllib
17
18
 
18
19
  from random import random
19
20
  from time import time
21
+ from optparse import OptionParser
20
22
  from os.path import join
21
23
  from collections import defaultdict, deque
22
24
 
@@ -24,11 +26,12 @@ from eventlet import sleep, Timeout
24
26
  from eventlet.greenpool import GreenPool
25
27
 
26
28
  from swift.common.constraints import AUTO_CREATE_ACCOUNT_PREFIX
27
- from swift.common.daemon import Daemon
29
+ from swift.common.daemon import Daemon, run_daemon
28
30
  from swift.common.internal_client import InternalClient, UnexpectedResponse
29
31
  from swift.common.utils import get_logger, dump_recon_cache, split_path, \
30
32
  Timestamp, config_true_value, normalize_delete_at_timestamp, \
31
- RateLimitedIterator, md5
33
+ RateLimitedIterator, md5, non_negative_float, non_negative_int, \
34
+ parse_content_type, parse_options
32
35
  from swift.common.http import HTTP_NOT_FOUND, HTTP_CONFLICT, \
33
36
  HTTP_PRECONDITION_FAILED
34
37
  from swift.common.recon import RECON_OBJECT_FILE, DEFAULT_RECON_CACHE_PATH
@@ -36,6 +39,7 @@ from swift.common.recon import RECON_OBJECT_FILE, DEFAULT_RECON_CACHE_PATH
36
39
  from swift.container.reconciler import direct_delete_container_entry
37
40
 
38
41
  MAX_OBJECTS_TO_CACHE = 100000
42
+ X_DELETE_TYPE = 'text/plain'
39
43
  ASYNC_DELETE_TYPE = 'application/async-deleted'
40
44
 
41
45
 
@@ -66,6 +70,80 @@ def parse_task_obj(task_obj):
66
70
  return timestamp, target_account, target_container, target_obj
67
71
 
68
72
 
73
+ def extract_expirer_bytes_from_ctype(content_type):
74
+ """
75
+ Parse a content-type and return the number of bytes.
76
+
77
+ :param content_type: a content-type string
78
+ :return: int or None
79
+ """
80
+ content_type, params = parse_content_type(content_type)
81
+ bytes_size = None
82
+ for k, v in params:
83
+ if k == 'swift_expirer_bytes':
84
+ bytes_size = int(v)
85
+ return bytes_size
86
+
87
+
88
+ def embed_expirer_bytes_in_ctype(content_type, metadata):
89
+ """
90
+ Embed number of bytes into content-type. The bytes should come from
91
+ content-length on regular objects, but future extensions to "bytes in
92
+ expirer queue" monitoring may want to more closely consider expiration of
93
+ large multipart object manifests.
94
+
95
+ :param content_type: a content-type string
96
+ :param metadata: a dict, from Diskfile metadata
97
+ :return: str
98
+ """
99
+ # as best I can tell this key is required by df.open
100
+ report_bytes = metadata['Content-Length']
101
+ return "%s;swift_expirer_bytes=%d" % (content_type, int(report_bytes))
102
+
103
+
104
+ def read_conf_for_delay_reaping_times(conf):
105
+ delay_reaping_times = {}
106
+ for conf_key in conf:
107
+ delay_reaping_prefix = "delay_reaping_"
108
+ if not conf_key.startswith(delay_reaping_prefix):
109
+ continue
110
+ delay_reaping_key = urllib.parse.unquote(
111
+ conf_key[len(delay_reaping_prefix):])
112
+ if delay_reaping_key.strip('/') != delay_reaping_key:
113
+ raise ValueError(
114
+ '%s '
115
+ 'should be in the form delay_reaping_<account> '
116
+ 'or delay_reaping_<account>/<container> '
117
+ '(leading or trailing "/" is not allowed)' % conf_key)
118
+ try:
119
+ # If split_path fails, have multiple '/' or
120
+ # account name is invalid
121
+ account, container = split_path(
122
+ '/' + delay_reaping_key, 1, 2
123
+ )
124
+ except ValueError:
125
+ raise ValueError(
126
+ '%s '
127
+ 'should be in the form delay_reaping_<account> '
128
+ 'or delay_reaping_<account>/<container> '
129
+ '(at most one "/" is allowed)' % conf_key)
130
+ try:
131
+ delay_reaping_times[(account, container)] = non_negative_float(
132
+ conf.get(conf_key)
133
+ )
134
+ except ValueError:
135
+ raise ValueError(
136
+ '%s must be a float '
137
+ 'greater than or equal to 0' % conf_key)
138
+ return delay_reaping_times
139
+
140
+
141
+ def get_delay_reaping(delay_reaping_times, target_account, target_container):
142
+ return delay_reaping_times.get(
143
+ (target_account, target_container),
144
+ delay_reaping_times.get((target_account, None), 0.0))
145
+
146
+
69
147
  class ObjectExpirer(Daemon):
70
148
  """
71
149
  Daemon that queries the internal hidden task accounts to discover objects
@@ -89,16 +167,8 @@ class ObjectExpirer(Daemon):
89
167
  self.dequeue_from_legacy = \
90
168
  True if is_legacy_conf else \
91
169
  config_true_value(conf.get('dequeue_from_legacy', 'false'))
92
-
93
- if is_legacy_conf:
94
- self.ic_conf_path = self.conf_path
95
- else:
96
- self.ic_conf_path = \
97
- self.conf.get('internal_client_conf_path') or \
98
- '/etc/swift/internal-client.conf'
99
-
100
- self.read_conf_for_queue_access(swift)
101
-
170
+ self.swift = swift or self._make_internal_client(is_legacy_conf)
171
+ self.read_conf_for_queue_access()
102
172
  self.report_interval = float(conf.get('report_interval') or 300)
103
173
  self.report_first_time = self.report_last_time = time()
104
174
  self.report_objects = 0
@@ -113,35 +183,32 @@ class ObjectExpirer(Daemon):
113
183
  # with the tombstone reclaim age in the consistency engine.
114
184
  self.reclaim_age = int(conf.get('reclaim_age', 604800))
115
185
 
116
- def read_conf_for_queue_access(self, swift):
117
- if self.conf.get('auto_create_account_prefix'):
118
- self.logger.warning('Option auto_create_account_prefix is '
119
- 'deprecated. Configure '
120
- 'auto_create_account_prefix under the '
121
- 'swift-constraints section of '
122
- 'swift.conf. This option will '
123
- 'be ignored in a future release.')
124
- auto_create_account_prefix = \
125
- self.conf['auto_create_account_prefix']
186
+ self.delay_reaping_times = read_conf_for_delay_reaping_times(conf)
187
+
188
+ def _make_internal_client(self, is_legacy_conf):
189
+ if is_legacy_conf:
190
+ ic_conf_path = self.conf_path
126
191
  else:
127
- auto_create_account_prefix = AUTO_CREATE_ACCOUNT_PREFIX
192
+ ic_conf_path = \
193
+ self.conf.get('internal_client_conf_path') or \
194
+ '/etc/swift/internal-client.conf'
195
+ request_tries = int(self.conf.get('request_tries') or 3)
196
+ return InternalClient(
197
+ ic_conf_path, 'Swift Object Expirer', request_tries,
198
+ use_replication_network=True,
199
+ global_conf={'log_name': '%s-ic' % self.conf.get(
200
+ 'log_name', self.log_route)})
128
201
 
129
- self.expiring_objects_account = auto_create_account_prefix + \
202
+ def read_conf_for_queue_access(self):
203
+ self.expiring_objects_account = AUTO_CREATE_ACCOUNT_PREFIX + \
130
204
  (self.conf.get('expiring_objects_account_name') or
131
205
  'expiring_objects')
132
206
 
133
207
  # This is for common parameter with general task queue in future
134
208
  self.task_container_prefix = ''
135
-
136
- request_tries = int(self.conf.get('request_tries') or 3)
137
- self.swift = swift or InternalClient(
138
- self.ic_conf_path, 'Swift Object Expirer', request_tries,
139
- use_replication_network=True,
140
- global_conf={'log_name': '%s-ic' % self.conf.get(
141
- 'log_name', self.log_route)})
142
-
143
- self.processes = int(self.conf.get('processes', 0))
144
- self.process = int(self.conf.get('process', 0))
209
+ self.processes = non_negative_int(self.conf.get('processes', 0))
210
+ self.process = non_negative_int(self.conf.get('process', 0))
211
+ self._validate_processes_config()
145
212
 
146
213
  def report(self, final=False):
147
214
  """
@@ -258,6 +325,10 @@ class ObjectExpirer(Daemon):
258
325
  break
259
326
  yield task_container
260
327
 
328
+ def get_delay_reaping(self, target_account, target_container):
329
+ return get_delay_reaping(self.delay_reaping_times, target_account,
330
+ target_container)
331
+
261
332
  def iter_task_to_expire(self, task_account_container_list,
262
333
  my_index, divisor):
263
334
  """
@@ -278,18 +349,30 @@ class ObjectExpirer(Daemon):
278
349
  except ValueError:
279
350
  self.logger.exception('Unexcepted error handling task %r' %
280
351
  task_object)
352
+ self.logger.increment('tasks.parse_errors')
281
353
  continue
354
+ is_async = o.get('content_type') == ASYNC_DELETE_TYPE
355
+ delay_reaping = self.get_delay_reaping(target_account,
356
+ target_container)
357
+
282
358
  if delete_timestamp > Timestamp.now():
283
- # we shouldn't yield the object that doesn't reach
359
+ # we shouldn't yield ANY more objects that can't reach
284
360
  # the expiration date yet.
285
361
  break
286
362
 
287
- # Only one expirer daemon assigned for one task
363
+ # Only one expirer daemon assigned for each task
288
364
  if self.hash_mod('%s/%s' % (task_container, task_object),
289
365
  divisor) != my_index:
366
+ self.logger.increment('tasks.skipped')
290
367
  continue
291
368
 
292
- is_async = o.get('content_type') == ASYNC_DELETE_TYPE
369
+ if delete_timestamp > Timestamp(time() - delay_reaping) \
370
+ and not is_async:
371
+ # we shouldn't yield the object during the delay
372
+ self.logger.increment('tasks.delayed')
373
+ continue
374
+
375
+ self.logger.increment('tasks.assigned')
293
376
  yield {'task_account': task_account,
294
377
  'task_container': task_container,
295
378
  'task_object': task_object,
@@ -320,6 +403,9 @@ class ObjectExpirer(Daemon):
320
403
  These will override the values from the config file if
321
404
  provided.
322
405
  """
406
+ # these config values are available to override at the command line,
407
+ # blow-up now if they're wrong
408
+ self.override_proceses_config_from_command_line(**kwargs)
323
409
  # This if-clause will be removed when general task queue feature is
324
410
  # implemented.
325
411
  if not self.dequeue_from_legacy:
@@ -330,7 +416,6 @@ class ObjectExpirer(Daemon):
330
416
  'with dequeue_from_legacy == true.')
331
417
  return
332
418
 
333
- self.get_process_values(kwargs)
334
419
  pool = GreenPool(self.concurrency)
335
420
  self.report_first_time = self.report_last_time = time()
336
421
  self.report_objects = 0
@@ -385,6 +470,9 @@ class ObjectExpirer(Daemon):
385
470
  :param kwargs: Extra keyword args to fulfill the Daemon interface; this
386
471
  daemon has no additional keyword args.
387
472
  """
473
+ # these config values are available to override at the command line
474
+ # blow-up now if they're wrong
475
+ self.override_proceses_config_from_command_line(**kwargs)
388
476
  sleep(random() * self.interval)
389
477
  while True:
390
478
  begin = time()
@@ -396,7 +484,7 @@ class ObjectExpirer(Daemon):
396
484
  if elapsed < self.interval:
397
485
  sleep(random() * (self.interval - elapsed))
398
486
 
399
- def get_process_values(self, kwargs):
487
+ def override_proceses_config_from_command_line(self, **kwargs):
400
488
  """
401
489
  Sets self.processes and self.process from the kwargs if those
402
490
  values exist, otherwise, leaves those values as they were set in
@@ -407,19 +495,20 @@ class ObjectExpirer(Daemon):
407
495
  line when the daemon is run.
408
496
  """
409
497
  if kwargs.get('processes') is not None:
410
- self.processes = int(kwargs['processes'])
498
+ self.processes = non_negative_int(kwargs['processes'])
411
499
 
412
500
  if kwargs.get('process') is not None:
413
- self.process = int(kwargs['process'])
501
+ self.process = non_negative_int(kwargs['process'])
414
502
 
415
- if self.process < 0:
416
- raise ValueError(
417
- 'process must be an integer greater than or equal to 0')
503
+ self._validate_processes_config()
418
504
 
419
- if self.processes < 0:
420
- raise ValueError(
421
- 'processes must be an integer greater than or equal to 0')
505
+ def _validate_processes_config(self):
506
+ """
507
+ Used in constructor and in override_proceses_config_from_command_line
508
+ to validate the processes configuration requirements.
422
509
 
510
+ :raiess: ValueError if processes config is invalid
511
+ """
423
512
  if self.processes and self.process >= self.processes:
424
513
  raise ValueError(
425
514
  'process must be less than processes')
@@ -496,3 +585,21 @@ class ObjectExpirer(Daemon):
496
585
  self.swift.delete_object(*split_path('/' + actual_obj, 3, 3, True),
497
586
  headers=headers,
498
587
  acceptable_statuses=acceptable_statuses)
588
+
589
+
590
+ def main():
591
+ parser = OptionParser("%prog CONFIG [options]")
592
+ parser.add_option('--processes', dest='processes',
593
+ help="Number of processes to use to do the work, don't "
594
+ "use this option to do all the work in one process")
595
+ parser.add_option('--process', dest='process',
596
+ help="Process number for this process, don't use "
597
+ "this option to do all the work in one process, this "
598
+ "is used to determine which part of the work this "
599
+ "process should do")
600
+ conf_file, options = parse_options(parser=parser, once=True)
601
+ run_daemon(ObjectExpirer, conf_file, **options)
602
+
603
+
604
+ if __name__ == '__main__':
605
+ main()
swift/obj/mem_diskfile.py CHANGED
@@ -426,13 +426,14 @@ class DiskFile(object):
426
426
  with self.open(current_time=current_time):
427
427
  return self.get_metadata()
428
428
 
429
- def reader(self, keep_cache=False):
429
+ def reader(self, keep_cache=False, cooperative_period=0):
430
430
  """
431
431
  Return a swift.common.swob.Response class compatible "app_iter"
432
432
  object. The responsibility of closing the open file is passed to the
433
433
  DiskFileReader object.
434
434
 
435
435
  :param keep_cache:
436
+ :param cooperative_period:
436
437
  """
437
438
  dr = DiskFileReader(self._name, self._fp,
438
439
  int(self._metadata['Content-Length']),
swift/obj/mem_server.py CHANGED
@@ -32,6 +32,7 @@ class ObjectController(server.ObjectController):
32
32
  :param conf: WSGI configuration parameter
33
33
  """
34
34
  self._filesystem = InMemoryFileSystem()
35
+ self.fallocate_reserve = 0
35
36
 
36
37
  def get_diskfile(self, device, partition, account, container, obj,
37
38
  **kwargs):