swift 2.23.2__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 (208) 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.2.data/scripts/swift-account-audit → swift/cli/account_audit.py +23 -13
  9. swift-2.23.2.data/scripts/swift-config → swift/cli/config.py +2 -2
  10. swift/cli/container_deleter.py +5 -11
  11. swift-2.23.2.data/scripts/swift-dispersion-populate → swift/cli/dispersion_populate.py +8 -7
  12. swift/cli/dispersion_report.py +10 -9
  13. swift-2.23.2.data/scripts/swift-drive-audit → swift/cli/drive_audit.py +63 -21
  14. swift/cli/form_signature.py +3 -7
  15. swift-2.23.2.data/scripts/swift-get-nodes → swift/cli/get_nodes.py +8 -2
  16. swift/cli/info.py +183 -29
  17. swift/cli/manage_shard_ranges.py +708 -37
  18. swift-2.23.2.data/scripts/swift-oldies → swift/cli/oldies.py +25 -14
  19. swift-2.23.2.data/scripts/swift-orphans → swift/cli/orphans.py +7 -3
  20. swift/cli/recon.py +196 -67
  21. swift-2.23.2.data/scripts/swift-recon-cron → swift/cli/recon_cron.py +17 -20
  22. swift-2.23.2.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 +198 -127
  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 +396 -147
  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 -81
  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 +52 -19
  102. swift/common/middleware/tempauth.py +76 -58
  103. swift/common/middleware/tempurl.py +192 -174
  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.2.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} +2191 -2762
  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 +555 -536
  130. swift/container/auditor.py +14 -100
  131. swift/container/backend.py +552 -227
  132. swift/container/reconciler.py +126 -37
  133. swift/container/replicator.py +96 -22
  134. swift/container/server.py +397 -176
  135. swift/container/sharder.py +1580 -639
  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 +213 -122
  146. swift/obj/ssync_receiver.py +145 -85
  147. swift/obj/ssync_sender.py +113 -54
  148. swift/obj/updater.py +653 -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 +452 -86
  154. swift/proxy/controllers/info.py +3 -2
  155. swift/proxy/controllers/obj.py +1009 -490
  156. swift/proxy/server.py +185 -112
  157. swift-2.35.0.dist-info/AUTHORS +501 -0
  158. swift-2.35.0.dist-info/LICENSE +202 -0
  159. {swift-2.23.2.dist-info → swift-2.35.0.dist-info}/METADATA +52 -61
  160. swift-2.35.0.dist-info/RECORD +201 -0
  161. {swift-2.23.2.dist-info → swift-2.35.0.dist-info}/WHEEL +1 -1
  162. {swift-2.23.2.dist-info → swift-2.35.0.dist-info}/entry_points.txt +43 -0
  163. swift-2.35.0.dist-info/pbr.json +1 -0
  164. swift/locale/de/LC_MESSAGES/swift.po +0 -1216
  165. swift/locale/en_GB/LC_MESSAGES/swift.po +0 -1207
  166. swift/locale/es/LC_MESSAGES/swift.po +0 -1085
  167. swift/locale/fr/LC_MESSAGES/swift.po +0 -909
  168. swift/locale/it/LC_MESSAGES/swift.po +0 -894
  169. swift/locale/ja/LC_MESSAGES/swift.po +0 -965
  170. swift/locale/ko_KR/LC_MESSAGES/swift.po +0 -964
  171. swift/locale/pt_BR/LC_MESSAGES/swift.po +0 -881
  172. swift/locale/ru/LC_MESSAGES/swift.po +0 -891
  173. swift/locale/tr_TR/LC_MESSAGES/swift.po +0 -832
  174. swift/locale/zh_CN/LC_MESSAGES/swift.po +0 -833
  175. swift/locale/zh_TW/LC_MESSAGES/swift.po +0 -838
  176. swift-2.23.2.data/scripts/swift-account-auditor +0 -23
  177. swift-2.23.2.data/scripts/swift-account-info +0 -51
  178. swift-2.23.2.data/scripts/swift-account-reaper +0 -23
  179. swift-2.23.2.data/scripts/swift-account-replicator +0 -34
  180. swift-2.23.2.data/scripts/swift-account-server +0 -23
  181. swift-2.23.2.data/scripts/swift-container-auditor +0 -23
  182. swift-2.23.2.data/scripts/swift-container-info +0 -51
  183. swift-2.23.2.data/scripts/swift-container-reconciler +0 -21
  184. swift-2.23.2.data/scripts/swift-container-replicator +0 -34
  185. swift-2.23.2.data/scripts/swift-container-sharder +0 -33
  186. swift-2.23.2.data/scripts/swift-container-sync +0 -23
  187. swift-2.23.2.data/scripts/swift-container-updater +0 -23
  188. swift-2.23.2.data/scripts/swift-dispersion-report +0 -24
  189. swift-2.23.2.data/scripts/swift-form-signature +0 -20
  190. swift-2.23.2.data/scripts/swift-init +0 -119
  191. swift-2.23.2.data/scripts/swift-object-auditor +0 -29
  192. swift-2.23.2.data/scripts/swift-object-expirer +0 -33
  193. swift-2.23.2.data/scripts/swift-object-info +0 -60
  194. swift-2.23.2.data/scripts/swift-object-reconstructor +0 -33
  195. swift-2.23.2.data/scripts/swift-object-relinker +0 -41
  196. swift-2.23.2.data/scripts/swift-object-replicator +0 -37
  197. swift-2.23.2.data/scripts/swift-object-server +0 -27
  198. swift-2.23.2.data/scripts/swift-object-updater +0 -23
  199. swift-2.23.2.data/scripts/swift-proxy-server +0 -23
  200. swift-2.23.2.data/scripts/swift-recon +0 -24
  201. swift-2.23.2.data/scripts/swift-ring-builder +0 -24
  202. swift-2.23.2.data/scripts/swift-ring-builder-analyzer +0 -22
  203. swift-2.23.2.data/scripts/swift-ring-composer +0 -22
  204. swift-2.23.2.dist-info/DESCRIPTION.rst +0 -166
  205. swift-2.23.2.dist-info/RECORD +0 -220
  206. swift-2.23.2.dist-info/metadata.json +0 -1
  207. swift-2.23.2.dist-info/pbr.json +0 -1
  208. {swift-2.23.2.dist-info → swift-2.35.0.dist-info}/top_level.txt +0 -0
@@ -24,8 +24,7 @@
24
24
  # These shenanigans are to ensure all related objects can be garbage
25
25
  # collected. We've seen objects hang around forever otherwise.
26
26
 
27
- from six.moves.urllib.parse import unquote
28
- from six.moves import zip
27
+ from urllib.parse import quote, unquote
29
28
 
30
29
  import collections
31
30
  import itertools
@@ -34,45 +33,49 @@ import mimetypes
34
33
  import time
35
34
  import math
36
35
  import random
37
- from hashlib import md5
38
- from swift import gettext_ as _
39
36
 
40
37
  from greenlet import GreenletExit
41
- from eventlet import GreenPile, sleep
42
- from eventlet.queue import Queue
38
+ from eventlet import GreenPile
39
+ from eventlet.queue import Queue, Empty
43
40
  from eventlet.timeout import Timeout
44
41
 
45
42
  from swift.common.utils import (
46
43
  clean_content_type, config_true_value, ContextPool, csv_append,
47
- GreenAsyncPile, GreenthreadSafeIterator, Timestamp,
48
- normalize_delete_at_timestamp, public, get_expirer_container,
44
+ GreenAsyncPile, GreenthreadSafeIterator, Timestamp, WatchdogTimeout,
45
+ normalize_delete_at_timestamp, public,
49
46
  document_iters_to_http_response_body, parse_content_range,
50
- quorum_size, reiterate, close_if_possible, safe_json_loads)
47
+ quorum_size, reiterate, close_if_possible, safe_json_loads, md5,
48
+ NamespaceBoundList, CooperativeIterator)
51
49
  from swift.common.bufferedhttp import http_connect
52
50
  from swift.common.constraints import check_metadata, check_object_creation
53
51
  from swift.common import constraints
54
52
  from swift.common.exceptions import ChunkReadTimeout, \
55
53
  ChunkWriteTimeout, ConnectionTimeout, ResponseTimeout, \
56
54
  InsufficientStorage, FooterNotSupported, MultiphasePUTNotSupported, \
57
- PutterConnectError, ChunkReadError
55
+ PutterConnectError, ChunkReadError, RangeAlreadyComplete, ShortReadError
58
56
  from swift.common.header_key_dict import HeaderKeyDict
59
57
  from swift.common.http import (
60
58
  is_informational, is_success, is_client_error, is_server_error,
61
59
  is_redirection, HTTP_CONTINUE, HTTP_INTERNAL_SERVER_ERROR,
62
60
  HTTP_SERVICE_UNAVAILABLE, HTTP_INSUFFICIENT_STORAGE,
63
61
  HTTP_PRECONDITION_FAILED, HTTP_CONFLICT, HTTP_UNPROCESSABLE_ENTITY,
64
- HTTP_REQUESTED_RANGE_NOT_SATISFIABLE)
62
+ HTTP_REQUESTED_RANGE_NOT_SATISFIABLE, HTTP_NOT_FOUND)
65
63
  from swift.common.storage_policy import (POLICIES, REPL_POLICY, EC_POLICY,
66
64
  ECDriverError, PolicyError)
67
65
  from swift.proxy.controllers.base import Controller, delay_denial, \
68
- cors_validation, ResumingGetter, update_headers
66
+ cors_validation, update_headers, bytes_to_skip, ByteCountEnforcer, \
67
+ record_cache_op_metrics, get_cache_key, GetterBase, GetterSource, \
68
+ is_good_source, NodeIter, get_namespaces_from_cache, \
69
+ set_namespaces_in_cache
69
70
  from swift.common.swob import HTTPAccepted, HTTPBadRequest, HTTPNotFound, \
70
71
  HTTPPreconditionFailed, HTTPRequestEntityTooLarge, HTTPRequestTimeout, \
71
72
  HTTPServerError, HTTPServiceUnavailable, HTTPClientDisconnect, \
72
73
  HTTPUnprocessableEntity, Response, HTTPException, \
73
- HTTPRequestedRangeNotSatisfiable, Range, HTTPInternalServerError
74
+ HTTPRequestedRangeNotSatisfiable, Range, HTTPInternalServerError, \
75
+ normalize_etag, str_to_wsgi
74
76
  from swift.common.request_helpers import update_etag_is_at_header, \
75
- resolve_etag_is_at_header
77
+ resolve_etag_is_at_header, validate_internal_obj, get_ip_port, \
78
+ is_open_expired, append_log_info
76
79
 
77
80
 
78
81
  def check_content_type(req):
@@ -168,8 +171,10 @@ class BaseObjectController(Controller):
168
171
  self.account_name = unquote(account_name)
169
172
  self.container_name = unquote(container_name)
170
173
  self.object_name = unquote(object_name)
174
+ validate_internal_obj(
175
+ self.account_name, self.container_name, self.object_name)
171
176
 
172
- def iter_nodes_local_first(self, ring, partition, policy=None,
177
+ def iter_nodes_local_first(self, ring, partition, request, policy=None,
173
178
  local_handoffs_first=False):
174
179
  """
175
180
  Yields nodes for a ring partition.
@@ -183,6 +188,8 @@ class BaseObjectController(Controller):
183
188
 
184
189
  :param ring: ring to get nodes from
185
190
  :param partition: ring partition to yield nodes for
191
+ :param request: nodes will be annotated with `use_replication` based on
192
+ the `request` headers
186
193
  :param policy: optional, an instance of
187
194
  :class:`~swift.common.storage_policy.BaseStoragePolicy`
188
195
  :param local_handoffs_first: optional, if True prefer primaries and
@@ -191,7 +198,9 @@ class BaseObjectController(Controller):
191
198
  policy_options = self.app.get_policy_options(policy)
192
199
  is_local = policy_options.write_affinity_is_local_fn
193
200
  if is_local is None:
194
- return self.app.iter_nodes(ring, partition, policy=policy)
201
+ return NodeIter(
202
+ 'object', self.app, ring, partition, self.logger, request,
203
+ policy=policy)
195
204
 
196
205
  primary_nodes = ring.get_part_nodes(partition)
197
206
  handoff_nodes = ring.get_more_nodes(partition)
@@ -224,8 +233,9 @@ class BaseObjectController(Controller):
224
233
  (node for node in all_nodes if node not in preferred_nodes)
225
234
  )
226
235
 
227
- return self.app.iter_nodes(ring, partition, node_iter=node_iter,
228
- policy=policy)
236
+ return NodeIter(
237
+ 'object', self.app, ring, partition, self.logger, request,
238
+ node_iter=node_iter, policy=policy)
229
239
 
230
240
  def GETorHEAD(self, req):
231
241
  """Handle HTTP GET or HEAD requests."""
@@ -238,13 +248,17 @@ class BaseObjectController(Controller):
238
248
  policy = POLICIES.get_by_index(policy_index)
239
249
  obj_ring = self.app.get_object_ring(policy_index)
240
250
  req.headers['X-Backend-Storage-Policy-Index'] = policy_index
251
+ if is_open_expired(self.app, req):
252
+ req.headers['X-Backend-Open-Expired'] = 'true'
241
253
  if 'swift.authorize' in req.environ:
242
254
  aresp = req.environ['swift.authorize'](req)
243
255
  if aresp:
244
256
  return aresp
245
257
  partition = obj_ring.get_part(
246
258
  self.account_name, self.container_name, self.object_name)
247
- node_iter = self.app.iter_nodes(obj_ring, partition, policy=policy)
259
+ node_iter = NodeIter(
260
+ 'object', self.app, obj_ring, partition, self.logger, req,
261
+ policy=policy)
248
262
 
249
263
  resp = self._get_or_head_response(req, node_iter, partition, policy)
250
264
 
@@ -267,18 +281,117 @@ class BaseObjectController(Controller):
267
281
  """Handler for HTTP HEAD requests."""
268
282
  return self.GETorHEAD(req)
269
283
 
284
+ def _get_updating_namespaces(
285
+ self, req, account, container, includes=None):
286
+ """
287
+ Fetch namespaces in 'updating' states from given `account/container`.
288
+ If `includes` is given then the shard range for that object name is
289
+ requested, otherwise all namespaces are requested.
290
+
291
+ :param req: original Request instance.
292
+ :param account: account from which namespaces should be fetched.
293
+ :param container: container from which namespaces should be fetched.
294
+ :param includes: (optional) restricts the list of fetched namespaces
295
+ to those which include the given name.
296
+ :return: a list of instances of :class:`swift.common.utils.Namespace`,
297
+ or None if there was a problem fetching the namespaces.
298
+ """
299
+ params = req.params.copy()
300
+ params.pop('limit', None)
301
+ params['format'] = 'json'
302
+ params['states'] = 'updating'
303
+ headers = {'X-Backend-Record-Type': 'shard',
304
+ 'X-Backend-Record-Shard-Format': 'namespace'}
305
+ if includes:
306
+ params['includes'] = str_to_wsgi(includes)
307
+ listing, response = self._get_container_listing(
308
+ req, account, container, headers=headers, params=params)
309
+ return self._parse_namespaces(req, listing, response), response
310
+
311
+ def _get_update_shard_caching_disabled(self, req, account, container, obj):
312
+ """
313
+ Fetch all updating shard ranges for the given root container when
314
+ all caching is disabled.
315
+
316
+ :param req: original Request instance.
317
+ :param account: account from which shard ranges should be fetched.
318
+ :param container: container from which shard ranges should be fetched.
319
+ :param obj: object getting updated.
320
+ :return: an instance of :class:`swift.common.utils.Namespace`,
321
+ or None if the update should go back to the root
322
+ """
323
+ # legacy behavior requests container server for includes=obj
324
+ namespaces, response = self._get_updating_namespaces(
325
+ req, account, container, includes=obj)
326
+ record_cache_op_metrics(
327
+ self.logger, self.server_type.lower(), 'shard_updating',
328
+ 'disabled', response)
329
+ # there will be only one Namespace in the list if any
330
+ return namespaces[0] if namespaces else None
331
+
332
+ def _get_update_shard(self, req, account, container, obj):
333
+ """
334
+ Find the appropriate shard range for an object update.
335
+
336
+ Note that this fetches and caches (in both the per-request infocache
337
+ and memcache, if available) all shard ranges for the given root
338
+ container so we won't have to contact the container DB for every write.
339
+
340
+ :param req: original Request instance.
341
+ :param account: account from which shard ranges should be fetched.
342
+ :param container: container from which shard ranges should be fetched.
343
+ :param obj: object getting updated.
344
+ :return: an instance of :class:`swift.common.utils.Namespace`,
345
+ or None if the update should go back to the root
346
+ """
347
+ if not self.app.recheck_updating_shard_ranges:
348
+ # caching is disabled
349
+ return self._get_update_shard_caching_disabled(
350
+ req, account, container, obj)
351
+
352
+ # caching is enabled, try to get from caches
353
+ response = None
354
+ cache_key = get_cache_key(account, container, shard='updating')
355
+ skip_chance = self.app.container_updating_shard_ranges_skip_cache
356
+ ns_bound_list, get_cache_state = get_namespaces_from_cache(
357
+ req, cache_key, skip_chance)
358
+ if not ns_bound_list:
359
+ # namespaces not found in either infocache or memcache so pull full
360
+ # set of updating shard ranges from backend
361
+ namespaces, response = self._get_updating_namespaces(
362
+ req, account, container)
363
+ if namespaces:
364
+ # only store the list of namespace lower bounds and names into
365
+ # infocache and memcache.
366
+ ns_bound_list = NamespaceBoundList.parse(namespaces)
367
+ set_cache_state = set_namespaces_in_cache(
368
+ req, cache_key, ns_bound_list,
369
+ self.app.recheck_updating_shard_ranges)
370
+ record_cache_op_metrics(
371
+ self.logger, self.server_type.lower(), 'shard_updating',
372
+ set_cache_state, None)
373
+ if set_cache_state == 'set':
374
+ self.logger.info(
375
+ 'Caching updating shards for %s (%d shards)',
376
+ cache_key, len(namespaces))
377
+ record_cache_op_metrics(
378
+ self.logger, self.server_type.lower(), 'shard_updating',
379
+ get_cache_state, response)
380
+ return ns_bound_list.get_namespace(obj) if ns_bound_list else None
381
+
270
382
  def _get_update_target(self, req, container_info):
271
383
  # find the sharded container to which we'll send the update
272
384
  db_state = container_info.get('sharding_state', 'unsharded')
273
385
  if db_state in ('sharded', 'sharding'):
274
- shard_range = self._get_update_shard(
386
+ update_shard_ns = self._get_update_shard(
275
387
  req, self.account_name, self.container_name, self.object_name)
276
- if shard_range:
388
+ if update_shard_ns:
277
389
  partition, nodes = self.app.container_ring.get_nodes(
278
- shard_range.account, shard_range.container)
279
- return partition, nodes, shard_range.name
390
+ update_shard_ns.account, update_shard_ns.container)
391
+ return partition, nodes, update_shard_ns.name, db_state
280
392
 
281
- return container_info['partition'], container_info['nodes'], None
393
+ return (container_info['partition'], container_info['nodes'], None,
394
+ db_state)
282
395
 
283
396
  @public
284
397
  @cors_validation
@@ -287,20 +400,20 @@ class BaseObjectController(Controller):
287
400
  """HTTP POST request handler."""
288
401
  container_info = self.container_info(
289
402
  self.account_name, self.container_name, req)
290
- container_partition, container_nodes, container_path = \
291
- self._get_update_target(req, container_info)
292
403
  req.acl = container_info['write_acl']
404
+ if is_open_expired(self.app, req):
405
+ req.headers['X-Backend-Open-Expired'] = 'true'
293
406
  if 'swift.authorize' in req.environ:
294
407
  aresp = req.environ['swift.authorize'](req)
295
408
  if aresp:
296
409
  return aresp
297
- if not container_nodes:
410
+ if not is_success(container_info.get('status')):
298
411
  return HTTPNotFound(request=req)
299
412
  error_response = check_metadata(req, 'object')
300
413
  if error_response:
301
414
  return error_response
302
415
 
303
- req.headers['X-Timestamp'] = Timestamp.now().internal
416
+ req.ensure_x_timestamp()
304
417
 
305
418
  req, delete_at_container, delete_at_part, \
306
419
  delete_at_nodes = self._config_obj_expiration(req)
@@ -317,37 +430,48 @@ class BaseObjectController(Controller):
317
430
  self.account_name, self.container_name, self.object_name)
318
431
 
319
432
  headers = self._backend_requests(
320
- req, len(nodes), container_partition, container_nodes,
321
- delete_at_container, delete_at_part, delete_at_nodes,
322
- container_path=container_path)
433
+ req, len(nodes), container_info, delete_at_container,
434
+ delete_at_part, delete_at_nodes)
323
435
  return self._post_object(req, obj_ring, partition, headers)
324
436
 
325
437
  def _backend_requests(self, req, n_outgoing,
326
- container_partition, containers,
327
- delete_at_container=None, delete_at_partition=None,
328
- delete_at_nodes=None, container_path=None):
438
+ container_info, delete_at_container=None,
439
+ delete_at_partition=None, delete_at_nodes=None):
329
440
  policy_index = req.headers['X-Backend-Storage-Policy-Index']
330
441
  policy = POLICIES.get_by_index(policy_index)
442
+ container_partition, containers, container_path, db_state = \
443
+ self._get_update_target(req, container_info)
331
444
  headers = [self.generate_request_headers(req, additional=req.headers)
332
445
  for _junk in range(n_outgoing)]
333
446
 
334
- def set_container_update(index, container):
447
+ def set_container_update(index, container_node):
448
+ ip, port = get_ip_port(container_node, headers[index])
335
449
  headers[index]['X-Container-Partition'] = container_partition
336
450
  headers[index]['X-Container-Host'] = csv_append(
337
451
  headers[index].get('X-Container-Host'),
338
- '%(ip)s:%(port)s' % container)
452
+ '%(ip)s:%(port)s' % {'ip': ip, 'port': port})
339
453
  headers[index]['X-Container-Device'] = csv_append(
340
454
  headers[index].get('X-Container-Device'),
341
- container['device'])
455
+ container_node['device'])
456
+ headers[index]['X-Container-Root-Db-State'] = db_state
342
457
  if container_path:
343
- headers[index]['X-Backend-Container-Path'] = container_path
458
+ headers[index]['X-Backend-Quoted-Container-Path'] = quote(
459
+ container_path)
460
+ # NB: we used to send
461
+ # 'X-Backend-Container-Path': container_path
462
+ # but that isn't safe for container names with nulls or
463
+ # newlines (or possibly some other characters). We consciously
464
+ # *don't* make any attempt to set the old meta; during an
465
+ # upgrade, old object-servers will talk to the root which
466
+ # will eat the update and move it as a misplaced object.
344
467
 
345
468
  def set_delete_at_headers(index, delete_at_node):
469
+ ip, port = get_ip_port(delete_at_node, headers[index])
346
470
  headers[index]['X-Delete-At-Container'] = delete_at_container
347
471
  headers[index]['X-Delete-At-Partition'] = delete_at_partition
348
472
  headers[index]['X-Delete-At-Host'] = csv_append(
349
473
  headers[index].get('X-Delete-At-Host'),
350
- '%(ip)s:%(port)s' % delete_at_node)
474
+ '%(ip)s:%(port)s' % {'ip': ip, 'port': port})
351
475
  headers[index]['X-Delete-At-Device'] = csv_append(
352
476
  headers[index].get('X-Delete-At-Device'),
353
477
  delete_at_node['device'])
@@ -396,7 +520,7 @@ class BaseObjectController(Controller):
396
520
 
397
521
  def _get_conn_response(self, putter, path, logger_thread_locals,
398
522
  final_phase, **kwargs):
399
- self.app.logger.thread_locals = logger_thread_locals
523
+ self.logger.thread_locals = logger_thread_locals
400
524
  try:
401
525
  resp = putter.await_response(
402
526
  self.app.node_timeout, not final_phase)
@@ -407,8 +531,8 @@ class BaseObjectController(Controller):
407
531
  else:
408
532
  status_type = 'commit'
409
533
  self.app.exception_occurred(
410
- putter.node, _('Object'),
411
- _('Trying to get %(status_type)s status of PUT to %(path)s') %
534
+ putter.node, 'Object',
535
+ 'Trying to get %(status_type)s status of PUT to %(path)s' %
412
536
  {'status_type': status_type, 'path': path})
413
537
  return (putter, resp)
414
538
 
@@ -453,7 +577,7 @@ class BaseObjectController(Controller):
453
577
  if putter.failed:
454
578
  continue
455
579
  pile.spawn(self._get_conn_response, putter, req.path,
456
- self.app.logger.thread_locals, final_phase=final_phase)
580
+ self.logger.thread_locals, final_phase=final_phase)
457
581
 
458
582
  def _handle_response(putter, response):
459
583
  statuses.append(response.status)
@@ -463,20 +587,11 @@ class BaseObjectController(Controller):
463
587
  else:
464
588
  body = b''
465
589
  bodies.append(body)
466
- if response.status == HTTP_INSUFFICIENT_STORAGE:
467
- putter.failed = True
468
- self.app.error_limit(putter.node,
469
- _('ERROR Insufficient Storage'))
470
- elif response.status >= HTTP_INTERNAL_SERVER_ERROR:
590
+ if not self.app.check_response(putter.node, 'Object', response,
591
+ req.method, req.path, body):
471
592
  putter.failed = True
472
- self.app.error_occurred(
473
- putter.node,
474
- _('ERROR %(status)d %(body)s From Object Server '
475
- 're: %(path)s') %
476
- {'status': response.status,
477
- 'body': body[:1024], 'path': req.path})
478
593
  elif is_success(response.status):
479
- etags.add(response.getheader('etag').strip('"'))
594
+ etags.add(normalize_etag(response.getheader('etag')))
480
595
 
481
596
  for (putter, response) in pile:
482
597
  if response:
@@ -509,19 +624,16 @@ class BaseObjectController(Controller):
509
624
  req = constraints.check_delete_headers(req)
510
625
 
511
626
  if 'x-delete-at' in req.headers:
512
- x_delete_at = int(normalize_delete_at_timestamp(
513
- int(req.headers['x-delete-at'])))
627
+ req.headers['x-delete-at'] = normalize_delete_at_timestamp(
628
+ int(req.headers['x-delete-at']))
629
+ x_delete_at = int(req.headers['x-delete-at'])
514
630
 
515
- req.environ.setdefault('swift.log_info', []).append(
516
- 'x-delete-at:%s' % x_delete_at)
631
+ append_log_info(req.environ, 'x-delete-at:%s' % x_delete_at)
517
632
 
518
- delete_at_container = get_expirer_container(
519
- x_delete_at, self.app.expiring_objects_container_divisor,
520
- self.account_name, self.container_name, self.object_name)
521
-
522
- delete_at_part, delete_at_nodes = \
523
- self.app.container_ring.get_nodes(
524
- self.app.expiring_objects_account, delete_at_container)
633
+ delete_at_part, delete_at_nodes, delete_at_container = \
634
+ self.app.expirer_config.get_delete_at_nodes(
635
+ x_delete_at, self.account_name, self.container_name,
636
+ self.object_name)
525
637
 
526
638
  return req, delete_at_container, delete_at_part, delete_at_nodes
527
639
 
@@ -536,23 +648,6 @@ class BaseObjectController(Controller):
536
648
  if detect_content_type:
537
649
  req.headers.pop('x-detect-content-type')
538
650
 
539
- def _update_x_timestamp(self, req):
540
- # The container sync feature includes an x-timestamp header with
541
- # requests. If present this is checked and preserved, otherwise a fresh
542
- # timestamp is added.
543
- if 'x-timestamp' in req.headers:
544
- try:
545
- req_timestamp = Timestamp(req.headers['X-Timestamp'])
546
- except ValueError:
547
- raise HTTPBadRequest(
548
- request=req, content_type='text/plain',
549
- body='X-Timestamp should be a UNIX timestamp float value; '
550
- 'was %r' % req.headers['x-timestamp'])
551
- req.headers['X-Timestamp'] = req_timestamp.internal
552
- else:
553
- req.headers['X-Timestamp'] = Timestamp.now().internal
554
- return None
555
-
556
651
  def _check_failure_put_connections(self, putters, req, min_conns):
557
652
  """
558
653
  Identify any failed connections and check minimum connection count.
@@ -566,8 +661,8 @@ class BaseObjectController(Controller):
566
661
  putter.resp.status for putter in putters if putter.resp]
567
662
  if HTTP_PRECONDITION_FAILED in statuses:
568
663
  # If we find any copy of the file, it shouldn't be uploaded
569
- self.app.logger.debug(
570
- _('Object PUT returning 412, %(statuses)r'),
664
+ self.logger.debug(
665
+ 'Object PUT returning 412, %(statuses)r',
571
666
  {'statuses': statuses})
572
667
  raise HTTPPreconditionFailed(request=req)
573
668
 
@@ -579,9 +674,9 @@ class BaseObjectController(Controller):
579
674
  putter.resp.getheaders()).get(
580
675
  'X-Backend-Timestamp', 'unknown')
581
676
  } for putter in putters if putter.resp]
582
- self.app.logger.debug(
583
- _('Object PUT returning 202 for 409: '
584
- '%(req_timestamp)s <= %(timestamps)r'),
677
+ self.logger.debug(
678
+ 'Object PUT returning 202 for 409: '
679
+ '%(req_timestamp)s <= %(timestamps)r',
585
680
  {'req_timestamp': req.timestamp.internal,
586
681
  'timestamps': ', '.join(status_times)})
587
682
  raise HTTPAccepted(request=req)
@@ -617,27 +712,26 @@ class BaseObjectController(Controller):
617
712
  :param req: a swob Request
618
713
  :param headers: request headers
619
714
  :param logger_thread_locals: The thread local values to be set on the
620
- self.app.logger to retain transaction
715
+ self.logger to retain transaction
621
716
  logging information.
622
717
  :return: an instance of a Putter
623
718
  """
624
- self.app.logger.thread_locals = logger_thread_locals
719
+ self.logger.thread_locals = logger_thread_locals
625
720
  for node in nodes:
626
721
  try:
627
722
  putter = self._make_putter(node, part, req, headers)
628
723
  self.app.set_node_timing(node, putter.connect_duration)
629
724
  return putter
630
725
  except InsufficientStorage:
631
- self.app.error_limit(node, _('ERROR Insufficient Storage'))
726
+ self.app.error_limit(node, 'ERROR Insufficient Storage')
632
727
  except PutterConnectError as e:
633
- self.app.error_occurred(
634
- node, _('ERROR %(status)d Expect: 100-continue '
635
- 'From Object Server') % {
636
- 'status': e.status})
728
+ msg = 'ERROR %d Expect: 100-continue From Object Server'
729
+ self.app.error_occurred(node, msg % e.status)
637
730
  except (Exception, Timeout):
638
731
  self.app.exception_occurred(
639
- node, _('Object'),
640
- _('Expect: 100-continue on %s') % req.swift_entity_path)
732
+ node, 'Object',
733
+ 'Expect: 100-continue on %s' %
734
+ quote(req.swift_entity_path))
641
735
 
642
736
  def _get_put_connections(self, req, nodes, partition, outgoing_headers,
643
737
  policy):
@@ -646,7 +740,8 @@ class BaseObjectController(Controller):
646
740
  """
647
741
  obj_ring = policy.object_ring
648
742
  node_iter = GreenthreadSafeIterator(
649
- self.iter_nodes_local_first(obj_ring, partition, policy=policy))
743
+ self.iter_nodes_local_first(obj_ring, partition, req,
744
+ policy=policy))
650
745
  pile = GreenPile(len(nodes))
651
746
 
652
747
  for nheaders in outgoing_headers:
@@ -657,19 +752,18 @@ class BaseObjectController(Controller):
657
752
  del nheaders['Content-Length']
658
753
  nheaders['Expect'] = '100-continue'
659
754
  pile.spawn(self._connect_put_node, node_iter, partition,
660
- req, nheaders, self.app.logger.thread_locals)
755
+ req, nheaders, self.logger.thread_locals)
661
756
 
662
757
  putters = [putter for putter in pile if putter]
663
758
 
664
759
  return putters
665
760
 
666
761
  def _check_min_conn(self, req, putters, min_conns, msg=None):
667
- msg = msg or _('Object PUT returning 503, %(conns)s/%(nodes)s '
668
- 'required connections')
762
+ msg = msg or ('Object PUT returning 503, %(conns)s/%(nodes)s '
763
+ 'required connections')
669
764
 
670
765
  if len(putters) < min_conns:
671
- self.app.logger.error((msg),
672
- {'conns': len(putters), 'nodes': min_conns})
766
+ self.logger.error(msg, {'conns': len(putters), 'nodes': min_conns})
673
767
  raise HTTPServiceUnavailable(request=req)
674
768
 
675
769
  def _get_footers(self, req):
@@ -748,8 +842,6 @@ class BaseObjectController(Controller):
748
842
  policy_index = req.headers.get('X-Backend-Storage-Policy-Index',
749
843
  container_info['storage_policy'])
750
844
  obj_ring = self.app.get_object_ring(policy_index)
751
- container_partition, container_nodes, container_path = \
752
- self._get_update_target(req, container_info)
753
845
  partition, nodes = obj_ring.get_nodes(
754
846
  self.account_name, self.container_name, self.object_name)
755
847
 
@@ -767,13 +859,13 @@ class BaseObjectController(Controller):
767
859
  if aresp:
768
860
  return aresp
769
861
 
770
- if not container_nodes:
862
+ if not is_success(container_info.get('status')):
771
863
  return HTTPNotFound(request=req)
772
864
 
773
865
  # update content type in case it is missing
774
866
  self._update_content_type(req)
775
867
 
776
- self._update_x_timestamp(req)
868
+ req.ensure_x_timestamp()
777
869
 
778
870
  # check constraints on object name and request headers
779
871
  error_response = check_object_creation(req, self.object_name) or \
@@ -795,9 +887,8 @@ class BaseObjectController(Controller):
795
887
 
796
888
  # add special headers to be handled by storage nodes
797
889
  outgoing_headers = self._backend_requests(
798
- req, len(nodes), container_partition, container_nodes,
799
- delete_at_container, delete_at_part, delete_at_nodes,
800
- container_path=container_path)
890
+ req, len(nodes), container_info,
891
+ delete_at_container, delete_at_part, delete_at_nodes)
801
892
 
802
893
  # send object to storage nodes
803
894
  resp = self._store_object(
@@ -820,20 +911,18 @@ class BaseObjectController(Controller):
820
911
  next_part_power = getattr(obj_ring, 'next_part_power', None)
821
912
  if next_part_power:
822
913
  req.headers['X-Backend-Next-Part-Power'] = next_part_power
823
- container_partition, container_nodes, container_path = \
824
- self._get_update_target(req, container_info)
825
914
  req.acl = container_info['write_acl']
826
915
  req.environ['swift_sync_key'] = container_info['sync_key']
827
916
  if 'swift.authorize' in req.environ:
828
917
  aresp = req.environ['swift.authorize'](req)
829
918
  if aresp:
830
919
  return aresp
831
- if not container_nodes:
920
+ if not is_success(container_info.get('status')):
832
921
  return HTTPNotFound(request=req)
833
922
  partition, nodes = obj_ring.get_nodes(
834
923
  self.account_name, self.container_name, self.object_name)
835
924
 
836
- self._update_x_timestamp(req)
925
+ req.ensure_x_timestamp()
837
926
 
838
927
  # Include local handoff nodes if write-affinity is enabled.
839
928
  node_count = len(nodes)
@@ -848,12 +937,10 @@ class BaseObjectController(Controller):
848
937
  local_handoffs = len(nodes) - len(local_primaries)
849
938
  node_count += local_handoffs
850
939
  node_iterator = self.iter_nodes_local_first(
851
- obj_ring, partition, policy=policy, local_handoffs_first=True
852
- )
940
+ obj_ring, partition, req, policy=policy,
941
+ local_handoffs_first=True)
853
942
 
854
- headers = self._backend_requests(
855
- req, node_count, container_partition, container_nodes,
856
- container_path=container_path)
943
+ headers = self._backend_requests(req, node_count, container_info)
857
944
  return self._delete_object(req, obj_ring, partition, headers,
858
945
  node_count=node_count,
859
946
  node_iterator=node_iterator)
@@ -864,27 +951,31 @@ class ReplicatedObjectController(BaseObjectController):
864
951
 
865
952
  def _get_or_head_response(self, req, node_iter, partition, policy):
866
953
  concurrency = self.app.get_object_ring(policy.idx).replica_count \
867
- if self.app.concurrent_gets else 1
954
+ if self.app.get_policy_options(policy).concurrent_gets else 1
868
955
  resp = self.GETorHEAD_base(
869
- req, _('Object'), node_iter, partition,
870
- req.swift_entity_path, concurrency)
956
+ req, 'Object', node_iter, partition,
957
+ req.swift_entity_path, concurrency, policy)
871
958
  return resp
872
959
 
873
960
  def _make_putter(self, node, part, req, headers):
874
961
  if req.environ.get('swift.callback.update_footers'):
875
962
  putter = MIMEPutter.connect(
876
- node, part, req.swift_entity_path, headers,
963
+ node, part, req.swift_entity_path, headers, self.app.watchdog,
877
964
  conn_timeout=self.app.conn_timeout,
878
965
  node_timeout=self.app.node_timeout,
879
- logger=self.app.logger,
966
+ write_timeout=self.app.node_timeout,
967
+ send_exception_handler=self.app.exception_occurred,
968
+ logger=self.logger,
880
969
  need_multiphase=False)
881
970
  else:
882
971
  te = ',' + headers.get('Transfer-Encoding', '')
883
972
  putter = Putter.connect(
884
- node, part, req.swift_entity_path, headers,
973
+ node, part, req.swift_entity_path, headers, self.app.watchdog,
885
974
  conn_timeout=self.app.conn_timeout,
886
975
  node_timeout=self.app.node_timeout,
887
- logger=self.app.logger,
976
+ write_timeout=self.app.node_timeout,
977
+ send_exception_handler=self.app.exception_occurred,
978
+ logger=self.logger,
888
979
  chunked=te.endswith(',chunked'))
889
980
  return putter
890
981
 
@@ -895,77 +986,72 @@ class ReplicatedObjectController(BaseObjectController):
895
986
  This method was added in the PUT method extraction change
896
987
  """
897
988
  bytes_transferred = 0
989
+ data_source = CooperativeIterator(data_source)
898
990
 
899
991
  def send_chunk(chunk):
992
+ timeout_at = time.time() + self.app.node_timeout
900
993
  for putter in list(putters):
901
994
  if not putter.failed:
902
- putter.send_chunk(chunk)
995
+ putter.send_chunk(chunk, timeout_at=timeout_at)
903
996
  else:
904
997
  putter.close()
905
998
  putters.remove(putter)
906
999
  self._check_min_conn(
907
1000
  req, putters, min_conns,
908
- msg=_('Object PUT exceptions during send, '
909
- '%(conns)s/%(nodes)s required connections'))
1001
+ msg='Object PUT exceptions during send, '
1002
+ '%(conns)s/%(nodes)s required connections')
910
1003
 
911
1004
  min_conns = quorum_size(len(nodes))
912
1005
  try:
913
- with ContextPool(len(nodes)) as pool:
914
- for putter in putters:
915
- putter.spawn_sender_greenthread(
916
- pool, self.app.put_queue_depth, self.app.node_timeout,
917
- self.app.exception_occurred)
918
- while True:
919
- with ChunkReadTimeout(self.app.client_timeout):
920
- try:
921
- chunk = next(data_source)
922
- except StopIteration:
923
- break
924
- bytes_transferred += len(chunk)
925
- if bytes_transferred > constraints.MAX_FILE_SIZE:
926
- raise HTTPRequestEntityTooLarge(request=req)
927
-
928
- send_chunk(chunk)
929
-
930
- ml = req.message_length()
931
- if ml and bytes_transferred < ml:
932
- req.client_disconnect = True
933
- self.app.logger.warning(
934
- _('Client disconnected without sending enough data'))
935
- self.app.logger.increment('client_disconnects')
936
- raise HTTPClientDisconnect(request=req)
937
-
938
- trail_md = self._get_footers(req)
939
- for putter in putters:
940
- # send any footers set by middleware
941
- putter.end_of_object_data(footer_metadata=trail_md)
942
-
943
- for putter in putters:
944
- putter.wait()
945
- self._check_min_conn(
946
- req, [p for p in putters if not p.failed], min_conns,
947
- msg=_('Object PUT exceptions after last send, '
948
- '%(conns)s/%(nodes)s required connections'))
1006
+ while True:
1007
+ with WatchdogTimeout(self.app.watchdog,
1008
+ self.app.client_timeout,
1009
+ ChunkReadTimeout):
1010
+ try:
1011
+ chunk = next(data_source)
1012
+ except StopIteration:
1013
+ break
1014
+ bytes_transferred += len(chunk)
1015
+ if bytes_transferred > constraints.MAX_FILE_SIZE:
1016
+ raise HTTPRequestEntityTooLarge(request=req)
1017
+
1018
+ send_chunk(chunk)
1019
+
1020
+ ml = req.message_length()
1021
+ if ml and bytes_transferred < ml:
1022
+ self.logger.warning(
1023
+ 'Client disconnected without sending enough data')
1024
+ self.logger.increment('object.client_disconnects')
1025
+ raise HTTPClientDisconnect(request=req)
1026
+
1027
+ trail_md = self._get_footers(req)
1028
+ for putter in putters:
1029
+ # send any footers set by middleware
1030
+ putter.end_of_object_data(footer_metadata=trail_md)
1031
+
1032
+ self._check_min_conn(
1033
+ req, [p for p in putters if not p.failed], min_conns,
1034
+ msg='Object PUT exceptions after last send, '
1035
+ '%(conns)s/%(nodes)s required connections')
949
1036
  except ChunkReadTimeout as err:
950
- self.app.logger.warning(
951
- _('ERROR Client read timeout (%ss)'), err.seconds)
952
- self.app.logger.increment('client_timeouts')
1037
+ self.logger.warning(
1038
+ 'ERROR Client read timeout (%ss)', err.seconds)
1039
+ self.logger.increment('object.client_timeouts')
953
1040
  raise HTTPRequestTimeout(request=req)
954
1041
  except HTTPException:
955
1042
  raise
956
1043
  except ChunkReadError:
957
- req.client_disconnect = True
958
- self.app.logger.warning(
959
- _('Client disconnected without sending last chunk'))
960
- self.app.logger.increment('client_disconnects')
1044
+ self.logger.warning(
1045
+ 'Client disconnected without sending last chunk')
1046
+ self.logger.increment('object.client_disconnects')
961
1047
  raise HTTPClientDisconnect(request=req)
962
1048
  except Timeout:
963
- self.app.logger.exception(
964
- _('ERROR Exception causing client disconnect'))
1049
+ self.logger.exception(
1050
+ 'ERROR Exception causing client disconnect')
965
1051
  raise HTTPClientDisconnect(request=req)
966
1052
  except Exception:
967
- self.app.logger.exception(
968
- _('ERROR Exception transferring data to object servers %s'),
1053
+ self.logger.exception(
1054
+ 'ERROR Exception transferring data to object servers %s',
969
1055
  {'path': req.path})
970
1056
  raise HTTPInternalServerError(request=req)
971
1057
 
@@ -1008,14 +1094,13 @@ class ReplicatedObjectController(BaseObjectController):
1008
1094
  putter.close()
1009
1095
 
1010
1096
  if len(etags) > 1:
1011
- self.app.logger.error(
1012
- _('Object servers returned %s mismatched etags'), len(etags))
1097
+ self.logger.error(
1098
+ 'Object servers returned %s mismatched etags', len(etags))
1013
1099
  return HTTPServerError(request=req)
1014
1100
  etag = etags.pop() if len(etags) else None
1015
1101
  resp = self.best_response(req, statuses, reasons, bodies,
1016
- _('Object PUT'), etag=etag)
1017
- resp.last_modified = math.ceil(
1018
- float(Timestamp(req.headers['X-Timestamp'])))
1102
+ 'Object PUT', etag=etag)
1103
+ resp.last_modified = Timestamp(req.headers['X-Timestamp']).ceil()
1019
1104
  return resp
1020
1105
 
1021
1106
 
@@ -1059,14 +1144,15 @@ class ECAppIter(object):
1059
1144
  self.mime_boundary = None
1060
1145
  self.learned_content_type = None
1061
1146
  self.stashed_iter = None
1147
+ self.pool = ContextPool(len(internal_parts_iters))
1062
1148
 
1063
1149
  def close(self):
1064
- # close down the stashed iter first so the ContextPool can
1065
- # cleanup the frag queue feeding coros that may be currently
1150
+ # close down the stashed iter and shutdown the context pool to
1151
+ # clean up the frag queue feeding coroutines that may be currently
1066
1152
  # executing the internal_parts_iters.
1067
1153
  if self.stashed_iter:
1068
- self.stashed_iter.close()
1069
- sleep() # Give the per-frag threads a chance to clean up
1154
+ close_if_possible(self.stashed_iter)
1155
+ self.pool.close()
1070
1156
  for it in self.internal_parts_iters:
1071
1157
  close_if_possible(it)
1072
1158
 
@@ -1202,10 +1288,13 @@ class ECAppIter(object):
1202
1288
 
1203
1289
  def __iter__(self):
1204
1290
  if self.stashed_iter is not None:
1205
- return iter(self.stashed_iter)
1291
+ return self
1206
1292
  else:
1207
1293
  raise ValueError("Failed to call kickoff() before __iter__()")
1208
1294
 
1295
+ def __next__(self):
1296
+ return next(self.stashed_iter)
1297
+
1209
1298
  def _real_iter(self, req, resp_headers):
1210
1299
  if not self.range_specs:
1211
1300
  client_asked_for_range = False
@@ -1390,7 +1479,8 @@ class ECAppIter(object):
1390
1479
  # segment at a time.
1391
1480
  queues = [Queue(1) for _junk in range(len(fragment_iters))]
1392
1481
 
1393
- def put_fragments_in_queue(frag_iter, queue):
1482
+ def put_fragments_in_queue(frag_iter, queue, logger_thread_locals):
1483
+ self.logger.thread_locals = logger_thread_locals
1394
1484
  try:
1395
1485
  for fragment in frag_iter:
1396
1486
  if fragment.startswith(b' '):
@@ -1400,20 +1490,28 @@ class ECAppIter(object):
1400
1490
  # killed by contextpool
1401
1491
  pass
1402
1492
  except ChunkReadTimeout:
1403
- # unable to resume in GetOrHeadHandler
1404
- self.logger.exception(_("Timeout fetching fragments for %r"),
1405
- self.path)
1493
+ # unable to resume in ECFragGetter
1494
+ self.logger.exception(
1495
+ "ChunkReadTimeout fetching fragments for %r",
1496
+ quote(self.path))
1497
+ except ChunkWriteTimeout:
1498
+ # slow client disconnect
1499
+ self.logger.exception(
1500
+ "ChunkWriteTimeout feeding fragments for %r",
1501
+ quote(self.path))
1406
1502
  except: # noqa
1407
- self.logger.exception(_("Exception fetching fragments for"
1408
- " %r"), self.path)
1503
+ self.logger.exception("Exception fetching fragments for %r",
1504
+ quote(self.path))
1409
1505
  finally:
1410
1506
  queue.resize(2) # ensure there's room
1411
1507
  queue.put(None)
1412
1508
  frag_iter.close()
1413
1509
 
1414
- with ContextPool(len(fragment_iters)) as pool:
1510
+ segments_decoded = 0
1511
+ with self.pool as pool:
1415
1512
  for frag_iter, queue in zip(fragment_iters, queues):
1416
- pool.spawn(put_fragments_in_queue, frag_iter, queue)
1513
+ pool.spawn(put_fragments_in_queue, frag_iter, queue,
1514
+ self.logger.thread_locals)
1417
1515
 
1418
1516
  while True:
1419
1517
  fragments = []
@@ -1428,15 +1526,27 @@ class ECAppIter(object):
1428
1526
  # with an un-reconstructible list of fragments - so we'll
1429
1527
  # break out of the iter so WSGI can tear down the broken
1430
1528
  # connection.
1431
- if not all(fragments):
1529
+ frags_with_data = sum([1 for f in fragments if f])
1530
+ if frags_with_data < len(fragments):
1531
+ if frags_with_data > 0:
1532
+ self.logger.warning(
1533
+ 'Un-recoverable fragment rebuild. Only received '
1534
+ '%d/%d fragments for %r', frags_with_data,
1535
+ len(fragments), quote(self.path))
1432
1536
  break
1433
1537
  try:
1434
1538
  segment = self.policy.pyeclib_driver.decode(fragments)
1435
- except ECDriverError:
1436
- self.logger.exception(_("Error decoding fragments for"
1437
- " %r"), self.path)
1539
+ except ECDriverError as err:
1540
+ self.logger.error(
1541
+ "Error decoding fragments for %r. "
1542
+ "Segments decoded: %d, "
1543
+ "Lengths: [%s]: %s" % (
1544
+ quote(self.path), segments_decoded,
1545
+ ', '.join(map(str, map(len, fragments))),
1546
+ str(err)))
1438
1547
  raise
1439
1548
 
1549
+ segments_decoded += 1
1440
1550
  yield segment
1441
1551
 
1442
1552
  def app_iter_range(self, start, end):
@@ -1576,10 +1686,15 @@ class Putter(object):
1576
1686
  :param resp: an HTTPResponse instance if connect() received final response
1577
1687
  :param path: the object path to send to the storage node
1578
1688
  :param connect_duration: time taken to initiate the HTTPConnection
1689
+ :param watchdog: a spawned Watchdog instance that will enforce timeouts
1690
+ :param write_timeout: time limit to write a chunk to the connection socket
1691
+ :param send_exception_handler: callback called when an exception occured
1692
+ writing to the connection socket
1579
1693
  :param logger: a Logger instance
1580
1694
  :param chunked: boolean indicating if the request encoding is chunked
1581
1695
  """
1582
- def __init__(self, conn, node, resp, path, connect_duration, logger,
1696
+ def __init__(self, conn, node, resp, path, connect_duration, watchdog,
1697
+ write_timeout, send_exception_handler, logger,
1583
1698
  chunked=False):
1584
1699
  # Note: you probably want to call Putter.connect() instead of
1585
1700
  # instantiating one of these directly.
@@ -1588,11 +1703,13 @@ class Putter(object):
1588
1703
  self.resp = self.final_resp = resp
1589
1704
  self.path = path
1590
1705
  self.connect_duration = connect_duration
1706
+ self.watchdog = watchdog
1707
+ self.write_timeout = write_timeout
1708
+ self.send_exception_handler = send_exception_handler
1591
1709
  # for handoff nodes node_index is None
1592
1710
  self.node_index = node.get('index')
1593
1711
 
1594
1712
  self.failed = False
1595
- self.queue = None
1596
1713
  self.state = NO_DATA_SENT
1597
1714
  self.chunked = chunked
1598
1715
  self.logger = logger
@@ -1624,22 +1741,12 @@ class Putter(object):
1624
1741
  self.resp = self.conn.getresponse()
1625
1742
  return self.resp
1626
1743
 
1627
- def spawn_sender_greenthread(self, pool, queue_depth, write_timeout,
1628
- exception_handler):
1629
- """Call before sending the first chunk of request body"""
1630
- self.queue = Queue(queue_depth)
1631
- pool.spawn(self._send_file, write_timeout, exception_handler)
1632
-
1633
- def wait(self):
1634
- if self.queue.unfinished_tasks:
1635
- self.queue.join()
1636
-
1637
1744
  def _start_object_data(self):
1638
1745
  # Called immediately before the first chunk of object data is sent.
1639
1746
  # Subclasses may implement custom behaviour
1640
1747
  pass
1641
1748
 
1642
- def send_chunk(self, chunk):
1749
+ def send_chunk(self, chunk, timeout_at=None):
1643
1750
  if not chunk:
1644
1751
  # If we're not using chunked transfer-encoding, sending a 0-byte
1645
1752
  # chunk is just wasteful. If we *are* using chunked
@@ -1653,7 +1760,7 @@ class Putter(object):
1653
1760
  self._start_object_data()
1654
1761
  self.state = SENDING_DATA
1655
1762
 
1656
- self.queue.put(chunk)
1763
+ self._send_chunk(chunk, timeout_at=timeout_at)
1657
1764
 
1658
1765
  def end_of_object_data(self, **kwargs):
1659
1766
  """
@@ -1662,33 +1769,24 @@ class Putter(object):
1662
1769
  if self.state == DATA_SENT:
1663
1770
  raise ValueError("called end_of_object_data twice")
1664
1771
 
1665
- self.queue.put(b'')
1772
+ self._send_chunk(b'')
1666
1773
  self.state = DATA_SENT
1667
1774
 
1668
- def _send_file(self, write_timeout, exception_handler):
1669
- """
1670
- Method for a file PUT coroutine. Takes chunks from a queue and sends
1671
- them down a socket.
1672
-
1673
- If something goes wrong, the "failed" attribute will be set to true
1674
- and the exception handler will be called.
1675
- """
1676
- while True:
1677
- chunk = self.queue.get()
1678
- if not self.failed:
1679
- if self.chunked:
1680
- to_send = b"%x\r\n%s\r\n" % (len(chunk), chunk)
1681
- else:
1682
- to_send = chunk
1683
- try:
1684
- with ChunkWriteTimeout(write_timeout):
1685
- self.conn.send(to_send)
1686
- except (Exception, ChunkWriteTimeout):
1687
- self.failed = True
1688
- exception_handler(self.node, _('Object'),
1689
- _('Trying to write to %s') % self.path)
1690
-
1691
- self.queue.task_done()
1775
+ def _send_chunk(self, chunk, timeout_at=None):
1776
+ if not self.failed:
1777
+ if self.chunked:
1778
+ to_send = b"%x\r\n%s\r\n" % (len(chunk), chunk)
1779
+ else:
1780
+ to_send = chunk
1781
+ try:
1782
+ with WatchdogTimeout(self.watchdog, self.write_timeout,
1783
+ ChunkWriteTimeout, timeout_at=timeout_at):
1784
+ self.conn.send(to_send)
1785
+ except (Exception, ChunkWriteTimeout):
1786
+ self.failed = True
1787
+ self.send_exception_handler(self.node, 'Object',
1788
+ 'Trying to write to %s'
1789
+ % quote(self.path))
1692
1790
 
1693
1791
  def close(self):
1694
1792
  # release reference to response to ensure connection really does close,
@@ -1699,9 +1797,10 @@ class Putter(object):
1699
1797
  @classmethod
1700
1798
  def _make_connection(cls, node, part, path, headers, conn_timeout,
1701
1799
  node_timeout):
1800
+ ip, port = get_ip_port(node, headers)
1702
1801
  start_time = time.time()
1703
1802
  with ConnectionTimeout(conn_timeout):
1704
- conn = http_connect(node['ip'], node['port'], node['device'],
1803
+ conn = http_connect(ip, port, node['device'],
1705
1804
  part, 'PUT', path, headers)
1706
1805
  connect_duration = time.time() - start_time
1707
1806
 
@@ -1724,7 +1823,8 @@ class Putter(object):
1724
1823
  return conn, resp, final_resp, connect_duration
1725
1824
 
1726
1825
  @classmethod
1727
- def connect(cls, node, part, path, headers, conn_timeout, node_timeout,
1826
+ def connect(cls, node, part, path, headers, watchdog, conn_timeout,
1827
+ node_timeout, write_timeout, send_exception_handler,
1728
1828
  logger=None, chunked=False, **kwargs):
1729
1829
  """
1730
1830
  Connect to a backend node and send the headers.
@@ -1738,7 +1838,8 @@ class Putter(object):
1738
1838
  """
1739
1839
  conn, expect_resp, final_resp, connect_duration = cls._make_connection(
1740
1840
  node, part, path, headers, conn_timeout, node_timeout)
1741
- return cls(conn, node, final_resp, path, connect_duration, logger,
1841
+ return cls(conn, node, final_resp, path, connect_duration, watchdog,
1842
+ write_timeout, send_exception_handler, logger,
1742
1843
  chunked=chunked)
1743
1844
 
1744
1845
 
@@ -1752,10 +1853,13 @@ class MIMEPutter(Putter):
1752
1853
 
1753
1854
  An HTTP PUT request that supports streaming.
1754
1855
  """
1755
- def __init__(self, conn, node, resp, req, connect_duration,
1756
- logger, mime_boundary, multiphase=False):
1757
- super(MIMEPutter, self).__init__(conn, node, resp, req,
1758
- connect_duration, logger)
1856
+ def __init__(self, conn, node, resp, path, connect_duration, watchdog,
1857
+ write_timeout, send_exception_handler, logger, mime_boundary,
1858
+ multiphase=False):
1859
+ super(MIMEPutter, self).__init__(conn, node, resp, path,
1860
+ connect_duration, watchdog,
1861
+ write_timeout, send_exception_handler,
1862
+ logger)
1759
1863
  # Note: you probably want to call MimePutter.connect() instead of
1760
1864
  # instantiating one of these directly.
1761
1865
  self.chunked = True # MIME requests always send chunked body
@@ -1766,8 +1870,8 @@ class MIMEPutter(Putter):
1766
1870
  # We're sending the object plus other stuff in the same request
1767
1871
  # body, all wrapped up in multipart MIME, so we'd better start
1768
1872
  # off the MIME document before sending any object data.
1769
- self.queue.put(b"--%s\r\nX-Document: object body\r\n\r\n" %
1770
- (self.mime_boundary,))
1873
+ self._send_chunk(b"--%s\r\nX-Document: object body\r\n\r\n" %
1874
+ (self.mime_boundary,))
1771
1875
 
1772
1876
  def end_of_object_data(self, footer_metadata=None):
1773
1877
  """
@@ -1785,7 +1889,8 @@ class MIMEPutter(Putter):
1785
1889
  self._start_object_data()
1786
1890
 
1787
1891
  footer_body = json.dumps(footer_metadata).encode('ascii')
1788
- footer_md5 = md5(footer_body).hexdigest().encode('ascii')
1892
+ footer_md5 = md5(
1893
+ footer_body, usedforsecurity=False).hexdigest().encode('ascii')
1789
1894
 
1790
1895
  tail_boundary = (b"--%s" % (self.mime_boundary,))
1791
1896
  if not self.multiphase:
@@ -1800,9 +1905,9 @@ class MIMEPutter(Putter):
1800
1905
  footer_body, b"\r\n",
1801
1906
  tail_boundary, b"\r\n",
1802
1907
  ]
1803
- self.queue.put(b"".join(message_parts))
1908
+ self._send_chunk(b"".join(message_parts))
1804
1909
 
1805
- self.queue.put(b'')
1910
+ self._send_chunk(b'')
1806
1911
  self.state = DATA_SENT
1807
1912
 
1808
1913
  def send_commit_confirmation(self):
@@ -1827,13 +1932,14 @@ class MIMEPutter(Putter):
1827
1932
  body, b"\r\n",
1828
1933
  tail_boundary,
1829
1934
  ]
1830
- self.queue.put(b"".join(message_parts))
1935
+ self._send_chunk(b"".join(message_parts))
1831
1936
 
1832
- self.queue.put(b'')
1937
+ self._send_chunk(b'')
1833
1938
  self.state = COMMIT_SENT
1834
1939
 
1835
1940
  @classmethod
1836
- def connect(cls, node, part, req, headers, conn_timeout, node_timeout,
1941
+ def connect(cls, node, part, path, headers, watchdog, conn_timeout,
1942
+ node_timeout, write_timeout, send_exception_handler,
1837
1943
  logger=None, need_multiphase=True, **kwargs):
1838
1944
  """
1839
1945
  Connect to a backend node and send the headers.
@@ -1871,7 +1977,7 @@ class MIMEPutter(Putter):
1871
1977
  headers['X-Backend-Obj-Multiphase-Commit'] = 'yes'
1872
1978
 
1873
1979
  conn, expect_resp, final_resp, connect_duration = cls._make_connection(
1874
- node, part, req, headers, conn_timeout, node_timeout)
1980
+ node, part, path, headers, conn_timeout, node_timeout)
1875
1981
 
1876
1982
  if is_informational(expect_resp.status):
1877
1983
  continue_headers = HeaderKeyDict(expect_resp.getheaders())
@@ -1886,7 +1992,8 @@ class MIMEPutter(Putter):
1886
1992
  if need_multiphase and not can_handle_multiphase_put:
1887
1993
  raise MultiphasePUTNotSupported()
1888
1994
 
1889
- return cls(conn, node, final_resp, req, connect_duration, logger,
1995
+ return cls(conn, node, final_resp, path, connect_duration, watchdog,
1996
+ write_timeout, send_exception_handler, logger,
1890
1997
  mime_boundary, multiphase=need_multiphase)
1891
1998
 
1892
1999
 
@@ -1988,13 +2095,16 @@ class ECGetResponseBucket(object):
1988
2095
  A helper class to encapsulate the properties of buckets in which fragment
1989
2096
  getters and alternate nodes are collected.
1990
2097
  """
1991
- def __init__(self, policy, timestamp_str):
2098
+ def __init__(self, policy, timestamp):
1992
2099
  """
1993
2100
  :param policy: an instance of ECStoragePolicy
1994
- :param timestamp_str: a string representation of a timestamp
2101
+ :param timestamp: a Timestamp, or None for a bucket of error responses
1995
2102
  """
1996
2103
  self.policy = policy
1997
- self.timestamp_str = timestamp_str
2104
+ self.timestamp = timestamp
2105
+ # if no timestamp when init'd then the bucket will update its timestamp
2106
+ # as responses are added
2107
+ self.update_timestamp = timestamp is None
1998
2108
  self.gets = collections.defaultdict(list)
1999
2109
  self.alt_nodes = collections.defaultdict(list)
2000
2110
  self._durable = False
@@ -2008,10 +2118,20 @@ class ECGetResponseBucket(object):
2008
2118
  return self._durable
2009
2119
 
2010
2120
  def add_response(self, getter, parts_iter):
2121
+ """
2122
+ Add another response to this bucket. Response buckets can be for
2123
+ fragments with the same timestamp, or for errors with the same status.
2124
+ """
2125
+ headers = getter.last_headers
2126
+ timestamp_str = headers.get('X-Backend-Timestamp',
2127
+ headers.get('X-Timestamp'))
2128
+ if timestamp_str and self.update_timestamp:
2129
+ # 404s will keep the most recent timestamp
2130
+ self.timestamp = max(Timestamp(timestamp_str), self.timestamp)
2011
2131
  if not self.gets:
2012
- self.status = getter.last_status
2013
2132
  # stash first set of backend headers, which will be used to
2014
2133
  # populate a client response
2134
+ self.status = getter.last_status
2015
2135
  # TODO: each bucket is for a single *data* timestamp, but sources
2016
2136
  # in the same bucket may have different *metadata* timestamps if
2017
2137
  # some backends have more recent .meta files than others. Currently
@@ -2021,18 +2141,17 @@ class ECGetResponseBucket(object):
2021
2141
  # recent metadata. We could alternatively choose to the *newest*
2022
2142
  # metadata headers for self.headers by selecting the source with
2023
2143
  # the latest X-Timestamp.
2024
- self.headers = getter.last_headers
2025
- elif (self.timestamp_str is not None and # ie, not bad_bucket
2026
- getter.last_headers.get('X-Object-Sysmeta-Ec-Etag') !=
2027
- self.headers.get('X-Object-Sysmeta-Ec-Etag')):
2144
+ self.headers = headers
2145
+ elif headers.get('X-Object-Sysmeta-Ec-Etag') != \
2146
+ self.headers.get('X-Object-Sysmeta-Ec-Etag'):
2028
2147
  # Fragments at the same timestamp with different etags are never
2029
- # expected. If somehow it happens then ignore those fragments
2030
- # to avoid mixing fragments that will not reconstruct otherwise
2031
- # an exception from pyeclib is almost certain. This strategy leaves
2032
- # a possibility that a set of consistent frags will be gathered.
2148
+ # expected and error buckets shouldn't have this header. If somehow
2149
+ # this happens then ignore those responses to avoid mixing
2150
+ # fragments that will not reconstruct otherwise an exception from
2151
+ # pyeclib is almost certain.
2033
2152
  raise ValueError("ETag mismatch")
2034
2153
 
2035
- frag_index = getter.last_headers.get('X-Object-Sysmeta-Ec-Frag-Index')
2154
+ frag_index = headers.get('X-Object-Sysmeta-Ec-Frag-Index')
2036
2155
  frag_index = int(frag_index) if frag_index is not None else None
2037
2156
  self.gets[frag_index].append((getter, parts_iter))
2038
2157
 
@@ -2042,7 +2161,7 @@ class ECGetResponseBucket(object):
2042
2161
  associated with the same frag_index then only one is included.
2043
2162
 
2044
2163
  :return: a list of sources, each source being a tuple of form
2045
- (ResumingGetter, iter)
2164
+ (ECFragGetter, iter)
2046
2165
  """
2047
2166
  all_sources = []
2048
2167
  for frag_index, sources in self.gets.items():
@@ -2060,8 +2179,19 @@ class ECGetResponseBucket(object):
2060
2179
 
2061
2180
  @property
2062
2181
  def shortfall(self):
2063
- result = self.policy.ec_ndata - len(self.get_responses())
2064
- return max(result, 0)
2182
+ """
2183
+ The number of additional responses needed to complete this bucket;
2184
+ typically (ndata - resp_count).
2185
+
2186
+ If the bucket has no durable responses, shortfall is extended out to
2187
+ replica count to ensure the proxy makes additional primary requests.
2188
+ """
2189
+ resp_count = len(self.get_responses())
2190
+ if self.durable or self.status == HTTP_REQUESTED_RANGE_NOT_SATISFIABLE:
2191
+ return max(self.policy.ec_ndata - resp_count, 0)
2192
+ alt_count = min(self.policy.object_ring.replica_count - resp_count,
2193
+ self.policy.ec_nparity)
2194
+ return max([1, self.policy.ec_ndata - resp_count, alt_count])
2065
2195
 
2066
2196
  @property
2067
2197
  def shortfall_with_alts(self):
@@ -2071,16 +2201,24 @@ class ECGetResponseBucket(object):
2071
2201
  result = self.policy.ec_ndata - (len(self.get_responses()) + len(alts))
2072
2202
  return max(result, 0)
2073
2203
 
2204
+ def close_conns(self):
2205
+ """
2206
+ Close bucket's responses; they won't be used for a client response.
2207
+ """
2208
+ for getter, frag_iter in self.get_responses():
2209
+ if getter.source:
2210
+ getter.source.close()
2211
+
2074
2212
  def __str__(self):
2075
2213
  # return a string summarising bucket state, useful for debugging.
2076
2214
  return '<%s, %s, %s, %s(%s), %s>' \
2077
- % (self.timestamp_str, self.status, self._durable,
2215
+ % (self.timestamp.internal, self.status, self._durable,
2078
2216
  self.shortfall, self.shortfall_with_alts, len(self.gets))
2079
2217
 
2080
2218
 
2081
2219
  class ECGetResponseCollection(object):
2082
2220
  """
2083
- Manages all successful EC GET responses gathered by ResumingGetters.
2221
+ Manages all successful EC GET responses gathered by ECFragGetters.
2084
2222
 
2085
2223
  A response comprises a tuple of (<getter instance>, <parts iterator>). All
2086
2224
  responses having the same data timestamp are placed in an
@@ -2096,33 +2234,61 @@ class ECGetResponseCollection(object):
2096
2234
  """
2097
2235
  self.policy = policy
2098
2236
  self.buckets = {}
2237
+ self.default_bad_bucket = ECGetResponseBucket(self.policy, None)
2238
+ self.bad_buckets = {}
2099
2239
  self.node_iter_count = 0
2100
2240
 
2101
- def _get_bucket(self, timestamp_str):
2241
+ def _get_bucket(self, timestamp):
2102
2242
  """
2103
- :param timestamp_str: a string representation of a timestamp
2243
+ :param timestamp: a Timestamp
2104
2244
  :return: ECGetResponseBucket for given timestamp
2105
2245
  """
2106
2246
  return self.buckets.setdefault(
2107
- timestamp_str, ECGetResponseBucket(self.policy, timestamp_str))
2247
+ timestamp, ECGetResponseBucket(self.policy, timestamp))
2248
+
2249
+ def _get_bad_bucket(self, status):
2250
+ """
2251
+ :param status: a representation of status
2252
+ :return: ECGetResponseBucket for given status
2253
+ """
2254
+ return self.bad_buckets.setdefault(
2255
+ status, ECGetResponseBucket(self.policy, None))
2108
2256
 
2109
2257
  def add_response(self, get, parts_iter):
2110
2258
  """
2111
2259
  Add a response to the collection.
2112
2260
 
2113
2261
  :param get: An instance of
2114
- :class:`~swift.proxy.controllers.base.ResumingGetter`
2262
+ :class:`~swift.proxy.controllers.obj.ECFragGetter`
2115
2263
  :param parts_iter: An iterator over response body parts
2116
2264
  :raises ValueError: if the response etag or status code values do not
2117
2265
  match any values previously received for the same timestamp
2118
2266
  """
2267
+ if is_success(get.last_status):
2268
+ self.add_good_response(get, parts_iter)
2269
+ else:
2270
+ self.add_bad_resp(get, parts_iter)
2271
+
2272
+ def add_bad_resp(self, get, parts_iter):
2273
+ bad_bucket = self._get_bad_bucket(get.last_status)
2274
+ bad_bucket.add_response(get, parts_iter)
2275
+
2276
+ def add_good_response(self, get, parts_iter):
2119
2277
  headers = get.last_headers
2120
2278
  # Add the response to the appropriate bucket keyed by data file
2121
2279
  # timestamp. Fall back to using X-Backend-Timestamp as key for object
2122
2280
  # servers that have not been upgraded.
2123
2281
  t_data_file = headers.get('X-Backend-Data-Timestamp')
2124
2282
  t_obj = headers.get('X-Backend-Timestamp', headers.get('X-Timestamp'))
2125
- self._get_bucket(t_data_file or t_obj).add_response(get, parts_iter)
2283
+ if t_data_file:
2284
+ timestamp = Timestamp(t_data_file)
2285
+ elif t_obj:
2286
+ timestamp = Timestamp(t_obj)
2287
+ else:
2288
+ # Don't think this should ever come up in practice,
2289
+ # but tests cover it
2290
+ timestamp = None
2291
+ self._get_bucket(timestamp).add_response(get, parts_iter)
2126
2292
 
2127
2293
  # The node may also have alternate fragments indexes (possibly at
2128
2294
  # different timestamps). For each list of alternate fragments indexes,
@@ -2130,7 +2296,9 @@ class ECGetResponseCollection(object):
2130
2296
  # list to that bucket's alternate nodes.
2131
2297
  frag_sets = safe_json_loads(headers.get('X-Backend-Fragments')) or {}
2132
2298
  for t_frag, frag_set in frag_sets.items():
2133
- self._get_bucket(t_frag).add_alternate_nodes(get.node, frag_set)
2299
+ t_frag = Timestamp(t_frag)
2300
+ self._get_bucket(t_frag).add_alternate_nodes(
2301
+ get.source.node, frag_set)
2134
2302
  # If the response includes a durable timestamp then mark that bucket as
2135
2303
  # durable. Note that this may be a different bucket than the one this
2136
2304
  # response got added to, and that we may never go and get a durable
@@ -2141,7 +2309,7 @@ class ECGetResponseCollection(object):
2141
2309
  # obj server not upgraded so assume this response's frag is durable
2142
2310
  t_durable = t_obj
2143
2311
  if t_durable:
2144
- self._get_bucket(t_durable).set_durable()
2312
+ self._get_bucket(Timestamp(t_durable)).set_durable()
2145
2313
 
2146
2314
  def _sort_buckets(self):
2147
2315
  def key_fn(bucket):
@@ -2154,35 +2322,77 @@ class ECGetResponseCollection(object):
2154
2322
  return (bucket.durable,
2155
2323
  bucket.shortfall <= 0,
2156
2324
  -1 * bucket.shortfall_with_alts,
2157
- bucket.timestamp_str)
2325
+ bucket.timestamp)
2158
2326
 
2159
2327
  return sorted(self.buckets.values(), key=key_fn, reverse=True)
2160
2328
 
2161
2329
  @property
2162
2330
  def best_bucket(self):
2163
2331
  """
2164
- Return the best bucket in the collection.
2332
+ Return the "best" bucket in the collection.
2165
2333
 
2166
2334
  The "best" bucket is the newest timestamp with sufficient getters, or
2167
2335
  the closest to having sufficient getters, unless it is bettered by a
2168
2336
  bucket with potential alternate nodes.
2169
2337
 
2338
+ If there are no good buckets we return the "least_bad" bucket.
2339
+
2170
2340
  :return: An instance of :class:`~ECGetResponseBucket` or None if there
2171
2341
  are no buckets in the collection.
2172
2342
  """
2173
2343
  sorted_buckets = self._sort_buckets()
2174
- if sorted_buckets:
2175
- return sorted_buckets[0]
2176
- return None
2344
+ for bucket in sorted_buckets:
2345
+ # tombstones will set bad_bucket.timestamp
2346
+ not_found_bucket = self.bad_buckets.get(404)
2347
+ if not_found_bucket and not_found_bucket.timestamp and \
2348
+ bucket.timestamp < not_found_bucket.timestamp:
2349
+ # "good bucket" is trumped by newer tombstone
2350
+ continue
2351
+ return bucket
2352
+ return self.least_bad_bucket
2353
+
2354
+ def choose_best_bucket(self):
2355
+ best_bucket = self.best_bucket
2356
+ # it's now or never -- close down any other requests
2357
+ for bucket in self.buckets.values():
2358
+ if bucket is best_bucket:
2359
+ continue
2360
+ bucket.close_conns()
2361
+ return best_bucket
2362
+
2363
+ @property
2364
+ def least_bad_bucket(self):
2365
+ """
2366
+ Return the bad_bucket with the smallest shortfall
2367
+ """
2368
+ if all(status == 404 for status in self.bad_buckets):
2369
+ # NB: also covers an empty self.bad_buckets
2370
+ return self.default_bad_bucket
2371
+ # we want "enough" 416s to prevent "extra" requests - but we keep
2372
+ # digging on 404s
2373
+ short, status = min((bucket.shortfall, status)
2374
+ for status, bucket in self.bad_buckets.items()
2375
+ if status != 404)
2376
+ return self.bad_buckets[status]
2377
+
2378
+ @property
2379
+ def shortfall(self):
2380
+ best_bucket = self.best_bucket
2381
+ shortfall = best_bucket.shortfall
2382
+ return min(shortfall, self.least_bad_bucket.shortfall)
2383
+
2384
+ @property
2385
+ def durable(self):
2386
+ return self.best_bucket.durable
2177
2387
 
2178
2388
  def _get_frag_prefs(self):
2179
2389
  # Construct the current frag_prefs list, with best_bucket prefs first.
2180
2390
  frag_prefs = []
2181
2391
 
2182
2392
  for bucket in self._sort_buckets():
2183
- if bucket.timestamp_str:
2393
+ if bucket.timestamp:
2184
2394
  exclusions = [fi for fi in bucket.gets if fi is not None]
2185
- prefs = {'timestamp': bucket.timestamp_str,
2395
+ prefs = {'timestamp': bucket.timestamp.internal,
2186
2396
  'exclude': exclusions}
2187
2397
  frag_prefs.append(prefs)
2188
2398
 
@@ -2241,22 +2451,292 @@ class ECGetResponseCollection(object):
2241
2451
  return nodes.pop(0).copy()
2242
2452
 
2243
2453
 
2454
+ class ECFragGetter(GetterBase):
2455
+
2456
+ def __init__(self, app, req, node_iter, partition, policy, path,
2457
+ backend_headers, header_provider, logger_thread_locals,
2458
+ logger):
2459
+ super(ECFragGetter, self).__init__(
2460
+ app=app, req=req, node_iter=node_iter, partition=partition,
2461
+ policy=policy, path=path, backend_headers=backend_headers,
2462
+ node_timeout=app.recoverable_node_timeout,
2463
+ resource_type='EC fragment', logger=logger)
2464
+ self.header_provider = header_provider
2465
+ self.fragment_size = policy.fragment_size
2466
+ self.skip_bytes = 0
2467
+ self.logger_thread_locals = logger_thread_locals
2468
+ self.status = self.reason = self.body = self.source_headers = None
2469
+ self._source_iter = None
2470
+
2471
+ def _iter_bytes_from_response_part(self, part_file, nbytes):
2472
+ buf = b''
2473
+ part_file = ByteCountEnforcer(part_file, nbytes)
2474
+ while True:
2475
+ try:
2476
+ with WatchdogTimeout(self.app.watchdog,
2477
+ self.node_timeout,
2478
+ ChunkReadTimeout):
2479
+ chunk = part_file.read(self.app.object_chunk_size)
2480
+ # NB: this append must be *inside* the context
2481
+ # manager for test.unit.SlowBody to do its thing
2482
+ buf += chunk
2483
+ if nbytes is not None:
2484
+ nbytes -= len(chunk)
2485
+ except (ChunkReadTimeout, ShortReadError) as e:
2486
+ try:
2487
+ self.fast_forward(self.bytes_used_from_backend)
2488
+ except (HTTPException, ValueError):
2489
+ self.logger.exception('Unable to fast forward')
2490
+ raise e
2491
+ except RangeAlreadyComplete:
2492
+ break
2493
+ buf = b''
2494
+ if self._replace_source(
2495
+ 'Trying to read EC fragment during GET (retrying)'):
2496
+ try:
2497
+ _junk, _junk, _junk, _junk, part_file = \
2498
+ self._get_next_response_part()
2499
+ except StopIteration:
2500
+ # it's not clear to me how to make
2501
+ # _get_next_response_part raise StopIteration for the
2502
+ # first doc part of a new request
2503
+ raise e
2504
+ part_file = ByteCountEnforcer(part_file, nbytes)
2505
+ else:
2506
+ raise e
2507
+ else:
2508
+ if buf and self.skip_bytes:
2509
+ if self.skip_bytes < len(buf):
2510
+ buf = buf[self.skip_bytes:]
2511
+ self.bytes_used_from_backend += self.skip_bytes
2512
+ self.skip_bytes = 0
2513
+ else:
2514
+ self.skip_bytes -= len(buf)
2515
+ self.bytes_used_from_backend += len(buf)
2516
+ buf = b''
2517
+
2518
+ while buf and (len(buf) >= self.fragment_size or not chunk):
2519
+ client_chunk = buf[:self.fragment_size]
2520
+ buf = buf[self.fragment_size:]
2521
+ with WatchdogTimeout(self.app.watchdog,
2522
+ self.app.client_timeout,
2523
+ ChunkWriteTimeout):
2524
+ self.bytes_used_from_backend += len(client_chunk)
2525
+ yield client_chunk
2526
+
2527
+ if not chunk:
2528
+ break
2529
+
2530
+ def _iter_parts_from_response(self):
2531
+ try:
2532
+ part_iter = None
2533
+ try:
2534
+ while True:
2535
+ try:
2536
+ start_byte, end_byte, length, headers, part = \
2537
+ self._get_next_response_part()
2538
+ except StopIteration:
2539
+ # it seems this is the only way out of the loop; not
2540
+ # sure why the req.environ update is always needed
2541
+ self.req.environ['swift.non_client_disconnect'] = True
2542
+ break
2543
+ # skip_bytes compensates for the backend request range
2544
+ # expansion done in _convert_range
2545
+ self.skip_bytes = bytes_to_skip(
2546
+ self.fragment_size, start_byte)
2547
+ self.learn_size_from_content_range(
2548
+ start_byte, end_byte, length)
2549
+ self.bytes_used_from_backend = 0
2550
+ # not length; that refers to the whole object, so is the
2551
+ # wrong value to use for GET-range responses
2552
+ byte_count = ((end_byte - start_byte + 1) - self.skip_bytes
2553
+ if (end_byte is not None
2554
+ and start_byte is not None)
2555
+ else None)
2556
+ part_iter = CooperativeIterator(
2557
+ self._iter_bytes_from_response_part(part, byte_count))
2558
+ yield {'start_byte': start_byte, 'end_byte': end_byte,
2559
+ 'entity_length': length, 'headers': headers,
2560
+ 'part_iter': part_iter}
2561
+ self.pop_range()
2562
+ finally:
2563
+ if part_iter:
2564
+ part_iter.close()
2565
+
2566
+ except ChunkReadTimeout:
2567
+ self.app.exception_occurred(self.source.node, 'Object',
2568
+ 'Trying to read during GET')
2569
+ raise
2570
+ except ChunkWriteTimeout:
2571
+ self.logger.warning(
2572
+ 'Client did not read from proxy within %ss' %
2573
+ self.app.client_timeout)
2574
+ self.logger.increment('object.client_timeouts')
2575
+ except GeneratorExit:
2576
+ warn = True
2577
+ req_range = self.backend_headers['Range']
2578
+ if req_range:
2579
+ req_range = Range(req_range)
2580
+ if len(req_range.ranges) == 1:
2581
+ begin, end = req_range.ranges[0]
2582
+ if end is not None and begin is not None:
2583
+ if end - begin + 1 == self.bytes_used_from_backend:
2584
+ warn = False
2585
+ if (warn and
2586
+ not self.req.environ.get('swift.non_client_disconnect')):
2587
+ self.logger.warning(
2588
+ 'Client disconnected on read of EC frag %r', self.path)
2589
+ raise
2590
+ except Exception:
2591
+ self.logger.exception('Trying to send to client')
2592
+ raise
2593
+ finally:
2594
+ self.source.close()
2595
+
2596
+ @property
2597
+ def last_status(self):
2598
+ return self.status or HTTP_INTERNAL_SERVER_ERROR
2599
+
2600
+ @property
2601
+ def last_headers(self):
2602
+ if self.source_headers:
2603
+ return HeaderKeyDict(self.source_headers)
2604
+ else:
2605
+ return HeaderKeyDict()
2606
+
2607
+ def _make_node_request(self, node):
2608
+ # make a backend request; return a response if it has an acceptable
2609
+ # status code, otherwise None
2610
+ self.logger.thread_locals = self.logger_thread_locals
2611
+ req_headers = dict(self.backend_headers)
2612
+ ip, port = get_ip_port(node, req_headers)
2613
+ req_headers.update(self.header_provider())
2614
+ start_node_timing = time.time()
2615
+ try:
2616
+ with ConnectionTimeout(self.app.conn_timeout):
2617
+ conn = http_connect(
2618
+ ip, port, node['device'],
2619
+ self.partition, 'GET', self.path,
2620
+ headers=req_headers,
2621
+ query_string=self.req.query_string)
2622
+ self.app.set_node_timing(node, time.time() - start_node_timing)
2623
+
2624
+ with Timeout(self.node_timeout):
2625
+ possible_source = conn.getresponse()
2626
+ # See NOTE: swift_conn at top of file about this.
2627
+ possible_source.swift_conn = conn
2628
+ except (Exception, Timeout):
2629
+ self.app.exception_occurred(
2630
+ node, 'Object',
2631
+ 'Trying to %(method)s %(path)s' %
2632
+ {'method': self.req.method, 'path': self.req.path})
2633
+ return None
2634
+
2635
+ src_headers = dict(
2636
+ (k.lower(), v) for k, v in
2637
+ possible_source.getheaders())
2638
+
2639
+ if 'handoff_index' in node and \
2640
+ (is_server_error(possible_source.status) or
2641
+ possible_source.status == HTTP_NOT_FOUND) and \
2642
+ not Timestamp(src_headers.get('x-backend-timestamp', 0)):
2643
+ # throw out 5XX and 404s from handoff nodes unless the data is
2644
+ # really on disk and had been DELETEd
2645
+ self.logger.debug('Ignoring %s from handoff' %
2646
+ possible_source.status)
2647
+ conn.close()
2648
+ return None
2649
+
2650
+ self.status = possible_source.status
2651
+ self.reason = possible_source.reason
2652
+ self.source_headers = possible_source.getheaders()
2653
+ if is_good_source(possible_source.status, server_type='Object'):
2654
+ self.body = None
2655
+ return possible_source
2656
+ else:
2657
+ self.body = possible_source.read()
2658
+ conn.close()
2659
+
2660
+ if self.app.check_response(node, 'Object', possible_source, 'GET',
2661
+ self.path):
2662
+ self.logger.debug(
2663
+ 'Ignoring %s from primary' % possible_source.status)
2664
+
2665
+ return None
2666
+
2667
+ @property
2668
+ def source_iter(self):
2669
+ """
2670
+ An iterator over responses to backend fragment GETs. Yields an
2671
+ instance of ``GetterSource`` if a response is good, otherwise ``None``.
2672
+ """
2673
+ if self._source_iter is None:
2674
+ self._source_iter = self._source_gen()
2675
+ return self._source_iter
2676
+
2677
+ def _source_gen(self):
2678
+ self.status = self.reason = self.body = self.source_headers = None
2679
+ for node in self.node_iter:
2680
+ source = self._make_node_request(node)
2681
+ if source:
2682
+ yield GetterSource(self.app, source, node)
2683
+ else:
2684
+ yield None
2685
+ self.status = self.reason = self.body = self.source_headers = None
2686
+
2687
+ def _find_source(self):
2688
+ # capture last used etag before continuation
2689
+ used_etag = self.last_headers.get('X-Object-Sysmeta-EC-ETag')
2690
+ for source in self.source_iter:
2691
+ if not source:
2692
+ # _make_node_request only returns good sources
2693
+ continue
2694
+ if source.resp.getheader('X-Object-Sysmeta-EC-ETag') != used_etag:
2695
+ self.logger.warning(
2696
+ 'Skipping source (etag mismatch: got %s, expected %s)',
2697
+ source.resp.getheader('X-Object-Sysmeta-EC-ETag'),
2698
+ used_etag)
2699
+ else:
2700
+ self.source = source
2701
+ return True
2702
+ return False
2703
+
2704
+ def response_parts_iter(self):
2705
+ """
2706
+ Create an iterator over a single fragment response body.
2707
+
2708
+ :return: an interator that yields chunks of bytes from a fragment
2709
+ response body.
2710
+ """
2711
+ it = None
2712
+ try:
2713
+ source = next(self.source_iter)
2714
+ except StopIteration:
2715
+ pass
2716
+ else:
2717
+ if source:
2718
+ self.source = source
2719
+ it = self._iter_parts_from_response()
2720
+ return it
2721
+
2722
+
2244
2723
  @ObjectControllerRouter.register(EC_POLICY)
2245
2724
  class ECObjectController(BaseObjectController):
2246
- def _fragment_GET_request(self, req, node_iter, partition, policy,
2247
- header_provider=None):
2725
+ def _fragment_GET_request(
2726
+ self, req, node_iter, partition, policy,
2727
+ header_provider, logger_thread_locals):
2248
2728
  """
2249
2729
  Makes a GET request for a fragment.
2250
2730
  """
2731
+ self.logger.thread_locals = logger_thread_locals
2251
2732
  backend_headers = self.generate_request_headers(
2252
2733
  req, additional=req.headers)
2253
2734
 
2254
- getter = ResumingGetter(self.app, req, 'Object', node_iter,
2255
- partition, req.swift_entity_path,
2256
- backend_headers,
2257
- client_chunk_size=policy.fragment_size,
2258
- newest=False, header_provider=header_provider)
2259
- return (getter, getter.response_parts_iter(req))
2735
+ getter = ECFragGetter(self.app, req, node_iter, partition,
2736
+ policy, req.swift_entity_path, backend_headers,
2737
+ header_provider, logger_thread_locals,
2738
+ self.logger)
2739
+ return getter, getter.response_parts_iter()
2260
2740
 
2261
2741
  def _convert_range(self, req, policy):
2262
2742
  """
@@ -2310,6 +2790,27 @@ class ECObjectController(BaseObjectController):
2310
2790
  for s, e in new_ranges)
2311
2791
  return range_specs
2312
2792
 
2793
+ def feed_remaining_primaries(self, safe_iter, pile, req, partition, policy,
2794
+ buckets, feeder_q, logger_thread_locals):
2795
+ timeout = self.app.get_policy_options(policy).concurrency_timeout
2796
+ while True:
2797
+ try:
2798
+ feeder_q.get(timeout=timeout)
2799
+ except Empty:
2800
+ if safe_iter.unsafe_iter.primaries_left:
2801
+ # this will run async, if it ends up taking the last
2802
+ # primary we won't find out until the next pass
2803
+ pile.spawn(self._fragment_GET_request,
2804
+ req, safe_iter, partition,
2805
+ policy, buckets.get_extra_headers,
2806
+ logger_thread_locals)
2807
+ else:
2808
+ # ran out of primaries
2809
+ break
2810
+ else:
2811
+ # got a stop
2812
+ break
2813
+
2313
2814
  def _get_or_head_response(self, req, node_iter, partition, policy):
2314
2815
  update_etag_is_at_header(req, "X-Object-Sysmeta-Ec-Etag")
2315
2816
 
@@ -2317,10 +2818,11 @@ class ECObjectController(BaseObjectController):
2317
2818
  # no fancy EC decoding here, just one plain old HEAD request to
2318
2819
  # one object server because all fragments hold all metadata
2319
2820
  # information about the object.
2320
- concurrency = policy.ec_ndata if self.app.concurrent_gets else 1
2821
+ concurrency = policy.ec_ndata \
2822
+ if self.app.get_policy_options(policy).concurrent_gets else 1
2321
2823
  resp = self.GETorHEAD_base(
2322
- req, _('Object'), node_iter, partition,
2323
- req.swift_entity_path, concurrency)
2824
+ req, 'Object', node_iter, partition,
2825
+ req.swift_entity_path, concurrency, policy)
2324
2826
  self._fix_response(req, resp)
2325
2827
  return resp
2326
2828
 
@@ -2333,27 +2835,28 @@ class ECObjectController(BaseObjectController):
2333
2835
 
2334
2836
  safe_iter = GreenthreadSafeIterator(node_iter)
2335
2837
 
2336
- # Sending the request concurrently to all nodes, and responding
2337
- # with the first response isn't something useful for EC as all
2338
- # nodes contain different fragments. Also EC has implemented it's
2339
- # own specific implementation of concurrent gets to ec_ndata nodes.
2340
- # So we don't need to worry about plumbing and sending a
2341
- # concurrency value to ResumingGetter.
2342
- with ContextPool(policy.ec_ndata) as pool:
2838
+ policy_options = self.app.get_policy_options(policy)
2839
+ ec_request_count = policy.ec_ndata
2840
+ if policy_options.concurrent_gets:
2841
+ ec_request_count += policy_options.concurrent_ec_extra_requests
2842
+ with ContextPool(policy.ec_n_unique_fragments) as pool:
2343
2843
  pile = GreenAsyncPile(pool)
2344
2844
  buckets = ECGetResponseCollection(policy)
2345
2845
  node_iter.set_node_provider(buckets.provide_alternate_node)
2346
- # include what may well be an empty X-Backend-Fragment-Preferences
2347
- # header from the buckets.get_extra_headers to let the object
2348
- # server know that it is ok to return non-durable fragments
2349
- for _junk in range(policy.ec_ndata):
2846
+
2847
+ for node_count in range(ec_request_count):
2350
2848
  pile.spawn(self._fragment_GET_request,
2351
2849
  req, safe_iter, partition,
2352
- policy, buckets.get_extra_headers)
2850
+ policy, buckets.get_extra_headers,
2851
+ self.logger.thread_locals)
2852
+
2853
+ feeder_q = None
2854
+ if policy_options.concurrent_gets:
2855
+ feeder_q = Queue()
2856
+ pool.spawn(self.feed_remaining_primaries, safe_iter, pile, req,
2857
+ partition, policy, buckets, feeder_q,
2858
+ self.logger.thread_locals)
2353
2859
 
2354
- bad_bucket = ECGetResponseBucket(policy, None)
2355
- bad_bucket.set_durable()
2356
- best_bucket = None
2357
2860
  extra_requests = 0
2358
2861
  # max_extra_requests is an arbitrary hard limit for spawning extra
2359
2862
  # getters in case some unforeseen scenario, or a misbehaving object
@@ -2363,50 +2866,33 @@ class ECObjectController(BaseObjectController):
2363
2866
  # be limit at most 2 * replicas.
2364
2867
  max_extra_requests = (
2365
2868
  (policy.object_ring.replica_count * 2) - policy.ec_ndata)
2366
-
2367
2869
  for get, parts_iter in pile:
2368
- if get.last_status is None:
2369
- # We may have spawned getters that find the node iterator
2370
- # has been exhausted. Ignore them.
2371
- # TODO: turns out that node_iter.nodes_left can bottom
2372
- # out at >0 when number of devs in ring is < 2* replicas,
2373
- # which definitely happens in tests and results in status
2374
- # of None. We should fix that but keep this guard because
2375
- # there is also a race between testing nodes_left/spawning
2376
- # a getter and an existing getter calling next(node_iter).
2377
- continue
2378
2870
  try:
2379
- if is_success(get.last_status):
2380
- # 2xx responses are managed by a response collection
2381
- buckets.add_response(get, parts_iter)
2382
- else:
2383
- # all other responses are lumped into a single bucket
2384
- bad_bucket.add_response(get, parts_iter)
2871
+ buckets.add_response(get, parts_iter)
2385
2872
  except ValueError as err:
2386
- self.app.logger.error(
2387
- _("Problem with fragment response: %s"), err)
2388
- shortfall = bad_bucket.shortfall
2873
+ self.logger.error(
2874
+ "Problem with fragment response: %s", err)
2389
2875
  best_bucket = buckets.best_bucket
2390
- if best_bucket:
2391
- shortfall = best_bucket.shortfall
2392
- if not best_bucket.durable and shortfall <= 0:
2393
- # be willing to go a *little* deeper, slowly
2394
- shortfall = 1
2395
- shortfall = min(shortfall, bad_bucket.shortfall)
2396
- if (extra_requests < max_extra_requests and
2397
- shortfall > pile._pending and
2398
- (node_iter.nodes_left > 0 or
2399
- buckets.has_alternate_node())):
2400
- # we need more matching responses to reach ec_ndata
2401
- # than we have pending gets, as long as we still have
2402
- # nodes in node_iter we can spawn another
2876
+ if best_bucket.durable and best_bucket.shortfall <= 0:
2877
+ # good enough!
2878
+ break
2879
+ requests_available = extra_requests < max_extra_requests and (
2880
+ node_iter.nodes_left > 0 or buckets.has_alternate_node())
2881
+ if requests_available and (
2882
+ buckets.shortfall > pile._pending or
2883
+ not is_good_source(get.last_status, self.server_type)):
2403
2884
  extra_requests += 1
2404
- pile.spawn(self._fragment_GET_request, req,
2405
- safe_iter, partition, policy,
2406
- buckets.get_extra_headers)
2407
-
2885
+ pile.spawn(self._fragment_GET_request, req, safe_iter,
2886
+ partition, policy, buckets.get_extra_headers,
2887
+ self.logger.thread_locals)
2888
+ if feeder_q:
2889
+ feeder_q.put('stop')
2890
+
2891
+ # Put this back, since we *may* need it for kickoff()/_fix_response()
2892
+ # (but note that _fix_ranges() may also pop it back off before then)
2408
2893
  req.range = orig_range
2409
- if best_bucket and best_bucket.shortfall <= 0 and best_bucket.durable:
2894
+ best_bucket = buckets.choose_best_bucket()
2895
+ if best_bucket.shortfall <= 0 and best_bucket.durable:
2410
2896
  # headers can come from any of the getters
2411
2897
  resp_headers = best_bucket.headers
2412
2898
  resp_headers.pop('Content-Range', None)
@@ -2419,15 +2905,15 @@ class ECObjectController(BaseObjectController):
2419
2905
  app_iter = ECAppIter(
2420
2906
  req.swift_entity_path,
2421
2907
  policy,
2422
- [parts_iter for
2423
- _getter, parts_iter in best_bucket.get_responses()],
2908
+ [p_iter for _getter, p_iter in best_bucket.get_responses()],
2424
2909
  range_specs, fa_length, obj_length,
2425
- self.app.logger)
2910
+ self.logger)
2426
2911
  resp = Response(
2427
2912
  request=req,
2428
2913
  conditional_response=True,
2429
2914
  app_iter=app_iter)
2430
2915
  update_headers(resp, resp_headers)
2916
+ self._fix_ranges(req, resp)
2431
2917
  try:
2432
2918
  app_iter.kickoff(req, resp)
2433
2919
  except HTTPException as err_resp:
@@ -2445,25 +2931,40 @@ class ECObjectController(BaseObjectController):
2445
2931
  reasons = []
2446
2932
  bodies = []
2447
2933
  headers = []
2448
- for getter, _parts_iter in bad_bucket.get_responses():
2449
- if best_bucket and best_bucket.durable:
2450
- bad_resp_headers = HeaderKeyDict(getter.last_headers)
2451
- t_data_file = bad_resp_headers.get(
2452
- 'X-Backend-Data-Timestamp')
2453
- t_obj = bad_resp_headers.get(
2454
- 'X-Backend-Timestamp',
2455
- bad_resp_headers.get('X-Timestamp'))
2456
- bad_ts = Timestamp(t_data_file or t_obj or '0')
2457
- if bad_ts <= Timestamp(best_bucket.timestamp_str):
2458
- # We have reason to believe there's still good data
2459
- # out there, it's just currently unavailable
2460
- continue
2461
- statuses.extend(getter.statuses)
2462
- reasons.extend(getter.reasons)
2463
- bodies.extend(getter.bodies)
2464
- headers.extend(getter.source_headers)
2465
-
2466
- if not statuses and best_bucket and not best_bucket.durable:
2934
+ best_bucket.close_conns()
2935
+ rebalance_missing_suppression_count = min(
2936
+ policy_options.rebalance_missing_suppression_count,
2937
+ node_iter.num_primary_nodes - 1)
2938
+ for status, bad_bucket in buckets.bad_buckets.items():
2939
+ for getter, _parts_iter in bad_bucket.get_responses():
2940
+ if best_bucket.durable:
2941
+ bad_resp_headers = getter.last_headers
2942
+ t_data_file = bad_resp_headers.get(
2943
+ 'X-Backend-Data-Timestamp')
2944
+ t_obj = bad_resp_headers.get(
2945
+ 'X-Backend-Timestamp',
2946
+ bad_resp_headers.get('X-Timestamp'))
2947
+ bad_ts = Timestamp(t_data_file or t_obj or '0')
2948
+ if bad_ts <= best_bucket.timestamp:
2949
+ # We have reason to believe there's still good data
2950
+ # out there, it's just currently unavailable
2951
+ continue
2952
+ if getter.status:
2953
+ timestamp = Timestamp(getter.last_headers.get(
2954
+ 'X-Backend-Timestamp',
2955
+ getter.last_headers.get('X-Timestamp', 0)))
2956
+ if (rebalance_missing_suppression_count > 0 and
2957
+ getter.status == HTTP_NOT_FOUND and
2958
+ not timestamp):
2959
+ rebalance_missing_suppression_count -= 1
2960
+ continue
2961
+ statuses.append(getter.status)
2962
+ reasons.append(getter.reason)
2963
+ bodies.append(getter.body)
2964
+ headers.append(getter.source_headers)
2965
+
2966
+ if not statuses and is_success(best_bucket.status) and \
2967
+ not best_bucket.durable:
2467
2968
  # pretend that non-durable bucket was 404s
2468
2969
  statuses.append(404)
2469
2970
  reasons.append('404 Not Found')
@@ -2474,6 +2975,10 @@ class ECObjectController(BaseObjectController):
2474
2975
  req, statuses, reasons, bodies, 'Object',
2475
2976
  headers=headers)
2476
2977
  self._fix_response(req, resp)
2978
+
2979
+ # For sure put this back before actually returning the response
2980
+ # to the rest of the pipeline, so we don't modify the client headers
2981
+ req.range = orig_range
2477
2982
  return resp
2478
2983
 
2479
2984
  def _fix_response(self, req, resp):
@@ -2495,13 +3000,36 @@ class ECObjectController(BaseObjectController):
2495
3000
  if resp.status_int == HTTP_REQUESTED_RANGE_NOT_SATISFIABLE:
2496
3001
  resp.headers['Content-Range'] = 'bytes */%s' % resp.headers[
2497
3002
  'X-Object-Sysmeta-Ec-Content-Length']
3003
+ ec_headers = [header for header in resp.headers
3004
+ if header.lower().startswith('x-object-sysmeta-ec-')]
3005
+ for header in ec_headers:
3006
+ # clients (including middlewares) shouldn't need to care about
3007
+ # this implementation detail
3008
+ del resp.headers[header]
3009
+
3010
+ def _fix_ranges(self, req, resp):
3011
+ # Has to be called *before* kickoff()!
3012
+ if is_success(resp.status_int):
3013
+ ignore_range_headers = set(
3014
+ h.strip().lower()
3015
+ for h in req.headers.get(
3016
+ 'X-Backend-Ignore-Range-If-Metadata-Present',
3017
+ '').split(','))
3018
+ if ignore_range_headers.intersection(
3019
+ h.lower() for h in resp.headers):
3020
+ # If we leave the Range header around, swob (or somebody) will
3021
+ # try to "fix" things for us when we kickoff() the app_iter.
3022
+ req.headers.pop('Range', None)
3023
+ resp.app_iter.range_specs = []
2498
3024
 
2499
3025
  def _make_putter(self, node, part, req, headers):
2500
3026
  return MIMEPutter.connect(
2501
- node, part, req.swift_entity_path, headers,
3027
+ node, part, req.swift_entity_path, headers, self.app.watchdog,
2502
3028
  conn_timeout=self.app.conn_timeout,
2503
3029
  node_timeout=self.app.node_timeout,
2504
- logger=self.app.logger,
3030
+ write_timeout=self.app.node_timeout,
3031
+ send_exception_handler=self.app.exception_occurred,
3032
+ logger=self.logger,
2505
3033
  need_multiphase=True)
2506
3034
 
2507
3035
  def _determine_chunk_destinations(self, putters, policy):
@@ -2578,7 +3106,8 @@ class ECObjectController(BaseObjectController):
2578
3106
  bytes_transferred = 0
2579
3107
  chunk_transform = chunk_transformer(policy)
2580
3108
  chunk_transform.send(None)
2581
- frag_hashers = collections.defaultdict(md5)
3109
+ frag_hashers = collections.defaultdict(
3110
+ lambda: md5(usedforsecurity=False))
2582
3111
 
2583
3112
  def send_chunk(chunk):
2584
3113
  # Note: there's two different hashers in here. etag_hasher is
@@ -2597,6 +3126,7 @@ class ECObjectController(BaseObjectController):
2597
3126
  return
2598
3127
 
2599
3128
  updated_frag_indexes = set()
3129
+ timeout_at = time.time() + self.app.node_timeout
2600
3130
  for putter in list(putters):
2601
3131
  frag_index = putter_to_frag_index[putter]
2602
3132
  backend_chunk = backend_chunks[frag_index]
@@ -2607,136 +3137,126 @@ class ECObjectController(BaseObjectController):
2607
3137
  if frag_index not in updated_frag_indexes:
2608
3138
  frag_hashers[frag_index].update(backend_chunk)
2609
3139
  updated_frag_indexes.add(frag_index)
2610
- putter.send_chunk(backend_chunk)
3140
+ putter.send_chunk(backend_chunk, timeout_at=timeout_at)
2611
3141
  else:
2612
3142
  putter.close()
2613
3143
  putters.remove(putter)
2614
3144
  self._check_min_conn(
2615
3145
  req, putters, min_conns,
2616
- msg=_('Object PUT exceptions during send, '
2617
- '%(conns)s/%(nodes)s required connections'))
3146
+ msg='Object PUT exceptions during send, '
3147
+ '%(conns)s/%(nodes)s required connections')
2618
3148
 
2619
3149
  try:
2620
- with ContextPool(len(putters)) as pool:
3150
+ # build our putter_to_frag_index dict to place handoffs in the
3151
+ # same part nodes index as the primaries they are covering
3152
+ putter_to_frag_index = self._determine_chunk_destinations(
3153
+ putters, policy)
3154
+ data_source = CooperativeIterator(data_source)
2621
3155
 
2622
- # build our putter_to_frag_index dict to place handoffs in the
2623
- # same part nodes index as the primaries they are covering
2624
- putter_to_frag_index = self._determine_chunk_destinations(
2625
- putters, policy)
2626
-
2627
- for putter in putters:
2628
- putter.spawn_sender_greenthread(
2629
- pool, self.app.put_queue_depth, self.app.node_timeout,
2630
- self.app.exception_occurred)
2631
- while True:
2632
- with ChunkReadTimeout(self.app.client_timeout):
2633
- try:
2634
- chunk = next(data_source)
2635
- except StopIteration:
2636
- break
2637
- bytes_transferred += len(chunk)
2638
- if bytes_transferred > constraints.MAX_FILE_SIZE:
2639
- raise HTTPRequestEntityTooLarge(request=req)
2640
-
2641
- send_chunk(chunk)
2642
-
2643
- ml = req.message_length()
2644
- if ml and bytes_transferred < ml:
2645
- req.client_disconnect = True
2646
- self.app.logger.warning(
2647
- _('Client disconnected without sending enough data'))
2648
- self.app.logger.increment('client_disconnects')
2649
- raise HTTPClientDisconnect(request=req)
2650
-
2651
- send_chunk(b'') # flush out any buffered data
2652
-
2653
- computed_etag = (etag_hasher.hexdigest()
2654
- if etag_hasher else None)
2655
- footers = self._get_footers(req)
2656
- received_etag = footers.get('etag', req.headers.get(
2657
- 'etag', '')).strip('"')
2658
- if (computed_etag and received_etag and
2659
- computed_etag != received_etag):
2660
- raise HTTPUnprocessableEntity(request=req)
2661
-
2662
- # Remove any EC reserved metadata names from footers
2663
- footers = {(k, v) for k, v in footers.items()
2664
- if not k.lower().startswith('x-object-sysmeta-ec-')}
2665
- for putter in putters:
2666
- frag_index = putter_to_frag_index[putter]
2667
- # Update any footers set by middleware with EC footers
2668
- trail_md = trailing_metadata(
2669
- policy, etag_hasher,
2670
- bytes_transferred, frag_index)
2671
- trail_md.update(footers)
2672
- # Etag footer must always be hash of what we sent
2673
- trail_md['Etag'] = frag_hashers[frag_index].hexdigest()
2674
- putter.end_of_object_data(footer_metadata=trail_md)
2675
-
2676
- for putter in putters:
2677
- putter.wait()
2678
-
2679
- # for storage policies requiring 2-phase commit (e.g.
2680
- # erasure coding), enforce >= 'quorum' number of
2681
- # 100-continue responses - this indicates successful
2682
- # object data and metadata commit and is a necessary
2683
- # condition to be met before starting 2nd PUT phase
2684
- final_phase = False
2685
- statuses, reasons, bodies, _junk = \
2686
- self._get_put_responses(
2687
- req, putters, len(nodes), final_phase=final_phase,
2688
- min_responses=min_conns)
2689
- if not self.have_quorum(
2690
- statuses, len(nodes), quorum=min_conns):
2691
- self.app.logger.error(
2692
- _('Not enough object servers ack\'ed (got %d)'),
2693
- statuses.count(HTTP_CONTINUE))
3156
+ while True:
3157
+ with WatchdogTimeout(self.app.watchdog,
3158
+ self.app.client_timeout,
3159
+ ChunkReadTimeout):
3160
+ try:
3161
+ chunk = next(data_source)
3162
+ except StopIteration:
3163
+ break
3164
+ bytes_transferred += len(chunk)
3165
+ if bytes_transferred > constraints.MAX_FILE_SIZE:
3166
+ raise HTTPRequestEntityTooLarge(request=req)
3167
+
3168
+ send_chunk(chunk)
3169
+
3170
+ ml = req.message_length()
3171
+ if ml and bytes_transferred < ml:
3172
+ self.logger.warning(
3173
+ 'Client disconnected without sending enough data')
3174
+ self.logger.increment('object.client_disconnects')
3175
+ raise HTTPClientDisconnect(request=req)
3176
+
3177
+ send_chunk(b'') # flush out any buffered data
3178
+
3179
+ computed_etag = (etag_hasher.hexdigest()
3180
+ if etag_hasher else None)
3181
+ footers = self._get_footers(req)
3182
+ received_etag = normalize_etag(footers.get(
3183
+ 'etag', req.headers.get('etag', '')))
3184
+ if (computed_etag and received_etag and
3185
+ computed_etag != received_etag):
3186
+ raise HTTPUnprocessableEntity(request=req)
3187
+
3188
+ # Remove any EC reserved metadata names from footers
3189
+ footers = {(k, v) for k, v in footers.items()
3190
+ if not k.lower().startswith('x-object-sysmeta-ec-')}
3191
+ for putter in putters:
3192
+ frag_index = putter_to_frag_index[putter]
3193
+ # Update any footers set by middleware with EC footers
3194
+ trail_md = trailing_metadata(
3195
+ policy, etag_hasher,
3196
+ bytes_transferred, frag_index)
3197
+ trail_md.update(footers)
3198
+ # Etag footer must always be hash of what we sent
3199
+ trail_md['Etag'] = frag_hashers[frag_index].hexdigest()
3200
+ putter.end_of_object_data(footer_metadata=trail_md)
3201
+
3202
+ # for storage policies requiring 2-phase commit (e.g.
3203
+ # erasure coding), enforce >= 'quorum' number of
3204
+ # 100-continue responses - this indicates successful
3205
+ # object data and metadata commit and is a necessary
3206
+ # condition to be met before starting 2nd PUT phase
3207
+ final_phase = False
3208
+ statuses, reasons, bodies, _junk = \
3209
+ self._get_put_responses(
3210
+ req, putters, len(nodes), final_phase=final_phase,
3211
+ min_responses=min_conns)
3212
+ if not self.have_quorum(
3213
+ statuses, len(nodes), quorum=min_conns):
3214
+ self.logger.error(
3215
+ 'Not enough object servers ack\'ed (got %d)',
3216
+ statuses.count(HTTP_CONTINUE))
3217
+ raise HTTPServiceUnavailable(request=req)
3218
+
3219
+ elif not self._have_adequate_informational(
3220
+ statuses, min_conns):
3221
+ resp = self.best_response(req, statuses, reasons, bodies,
3222
+ 'Object PUT',
3223
+ quorum_size=min_conns)
3224
+ if is_client_error(resp.status_int):
3225
+ # if 4xx occurred in this state it is absolutely
3226
+ # a bad conversation between proxy-server and
3227
+ # object-server (even if it's
3228
+ # HTTP_UNPROCESSABLE_ENTITY) so we should regard this
3229
+ # as HTTPServiceUnavailable.
2694
3230
  raise HTTPServiceUnavailable(request=req)
3231
+ else:
3232
+ # Other errors should use raw best_response
3233
+ raise resp
2695
3234
 
2696
- elif not self._have_adequate_informational(
2697
- statuses, min_conns):
2698
- resp = self.best_response(req, statuses, reasons, bodies,
2699
- _('Object PUT'),
2700
- quorum_size=min_conns)
2701
- if is_client_error(resp.status_int):
2702
- # if 4xx occurred in this state it is absolutely
2703
- # a bad conversation between proxy-server and
2704
- # object-server (even if it's
2705
- # HTTP_UNPROCESSABLE_ENTITY) so we should regard this
2706
- # as HTTPServiceUnavailable.
2707
- raise HTTPServiceUnavailable(request=req)
2708
- else:
2709
- # Other errors should use raw best_response
2710
- raise resp
2711
-
2712
- # quorum achieved, start 2nd phase - send commit
2713
- # confirmation to participating object servers
2714
- # so they write a .durable state file indicating
2715
- # a successful PUT
2716
- for putter in putters:
2717
- putter.send_commit_confirmation()
2718
- for putter in putters:
2719
- putter.wait()
3235
+ # quorum achieved, start 2nd phase - send commit
3236
+ # confirmation to participating object servers
3237
+ # so they write a .durable state file indicating
3238
+ # a successful PUT
3239
+ for putter in putters:
3240
+ putter.send_commit_confirmation()
2720
3241
  except ChunkReadTimeout as err:
2721
- self.app.logger.warning(
2722
- _('ERROR Client read timeout (%ss)'), err.seconds)
2723
- self.app.logger.increment('client_timeouts')
3242
+ self.logger.warning(
3243
+ 'ERROR Client read timeout (%ss)', err.seconds)
3244
+ self.logger.increment('object.client_timeouts')
2724
3245
  raise HTTPRequestTimeout(request=req)
2725
3246
  except ChunkReadError:
2726
- req.client_disconnect = True
2727
- self.app.logger.warning(
2728
- _('Client disconnected without sending last chunk'))
2729
- self.app.logger.increment('client_disconnects')
3247
+ self.logger.warning(
3248
+ 'Client disconnected without sending last chunk')
3249
+ self.logger.increment('object.client_disconnects')
2730
3250
  raise HTTPClientDisconnect(request=req)
2731
3251
  except HTTPException:
2732
3252
  raise
2733
3253
  except Timeout:
2734
- self.app.logger.exception(
2735
- _('ERROR Exception causing client disconnect'))
3254
+ self.logger.exception(
3255
+ 'ERROR Exception causing client disconnect')
2736
3256
  raise HTTPClientDisconnect(request=req)
2737
3257
  except Exception:
2738
- self.app.logger.exception(
2739
- _('ERROR Exception transferring data to object servers %s'),
3258
+ self.logger.exception(
3259
+ 'ERROR Exception transferring data to object servers %s',
2740
3260
  {'path': req.path})
2741
3261
  raise HTTPInternalServerError(request=req)
2742
3262
 
@@ -2821,7 +3341,7 @@ class ECObjectController(BaseObjectController):
2821
3341
  # the same as the request body sent proxy -> object, we
2822
3342
  # can't rely on the object-server to do the etag checking -
2823
3343
  # so we have to do it here.
2824
- etag_hasher = md5()
3344
+ etag_hasher = md5(usedforsecurity=False)
2825
3345
 
2826
3346
  min_conns = policy.quorum
2827
3347
  putters = self._get_put_connections(
@@ -2856,8 +3376,7 @@ class ECObjectController(BaseObjectController):
2856
3376
 
2857
3377
  etag = etag_hasher.hexdigest()
2858
3378
  resp = self.best_response(req, statuses, reasons, bodies,
2859
- _('Object PUT'), etag=etag,
3379
+ 'Object PUT', etag=etag,
2860
3380
  quorum_size=min_conns)
2861
- resp.last_modified = math.ceil(
2862
- float(Timestamp(req.headers['X-Timestamp'])))
3381
+ resp.last_modified = Timestamp(req.headers['X-Timestamp']).ceil()
2863
3382
  return resp