swift 2.23.3__py3-none-any.whl → 2.35.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (206) hide show
  1. swift/__init__.py +29 -50
  2. swift/account/auditor.py +21 -118
  3. swift/account/backend.py +33 -28
  4. swift/account/reaper.py +37 -28
  5. swift/account/replicator.py +22 -0
  6. swift/account/server.py +60 -26
  7. swift/account/utils.py +28 -11
  8. swift-2.23.3.data/scripts/swift-account-audit → swift/cli/account_audit.py +23 -13
  9. swift-2.23.3.data/scripts/swift-config → swift/cli/config.py +2 -2
  10. swift/cli/container_deleter.py +5 -11
  11. swift-2.23.3.data/scripts/swift-dispersion-populate → swift/cli/dispersion_populate.py +8 -7
  12. swift/cli/dispersion_report.py +10 -9
  13. swift-2.23.3.data/scripts/swift-drive-audit → swift/cli/drive_audit.py +63 -21
  14. swift/cli/form_signature.py +3 -7
  15. swift-2.23.3.data/scripts/swift-get-nodes → swift/cli/get_nodes.py +8 -2
  16. swift/cli/info.py +154 -14
  17. swift/cli/manage_shard_ranges.py +705 -37
  18. swift-2.23.3.data/scripts/swift-oldies → swift/cli/oldies.py +25 -14
  19. swift-2.23.3.data/scripts/swift-orphans → swift/cli/orphans.py +7 -3
  20. swift/cli/recon.py +196 -67
  21. swift-2.23.3.data/scripts/swift-recon-cron → swift/cli/recon_cron.py +17 -20
  22. swift-2.23.3.data/scripts/swift-reconciler-enqueue → swift/cli/reconciler_enqueue.py +2 -3
  23. swift/cli/relinker.py +807 -126
  24. swift/cli/reload.py +135 -0
  25. swift/cli/ringbuilder.py +217 -20
  26. swift/cli/ringcomposer.py +0 -1
  27. swift/cli/shard-info.py +4 -3
  28. swift/common/base_storage_server.py +9 -20
  29. swift/common/bufferedhttp.py +48 -74
  30. swift/common/constraints.py +20 -15
  31. swift/common/container_sync_realms.py +9 -11
  32. swift/common/daemon.py +25 -8
  33. swift/common/db.py +195 -128
  34. swift/common/db_auditor.py +168 -0
  35. swift/common/db_replicator.py +95 -55
  36. swift/common/digest.py +141 -0
  37. swift/common/direct_client.py +144 -33
  38. swift/common/error_limiter.py +93 -0
  39. swift/common/exceptions.py +25 -1
  40. swift/common/header_key_dict.py +2 -9
  41. swift/common/http_protocol.py +373 -0
  42. swift/common/internal_client.py +129 -59
  43. swift/common/linkat.py +3 -4
  44. swift/common/manager.py +284 -67
  45. swift/common/memcached.py +390 -145
  46. swift/common/middleware/__init__.py +4 -0
  47. swift/common/middleware/account_quotas.py +211 -46
  48. swift/common/middleware/acl.py +3 -8
  49. swift/common/middleware/backend_ratelimit.py +230 -0
  50. swift/common/middleware/bulk.py +22 -34
  51. swift/common/middleware/catch_errors.py +1 -3
  52. swift/common/middleware/cname_lookup.py +6 -11
  53. swift/common/middleware/container_quotas.py +1 -1
  54. swift/common/middleware/container_sync.py +39 -17
  55. swift/common/middleware/copy.py +12 -0
  56. swift/common/middleware/crossdomain.py +22 -9
  57. swift/common/middleware/crypto/__init__.py +2 -1
  58. swift/common/middleware/crypto/crypto_utils.py +11 -15
  59. swift/common/middleware/crypto/decrypter.py +28 -11
  60. swift/common/middleware/crypto/encrypter.py +12 -17
  61. swift/common/middleware/crypto/keymaster.py +8 -15
  62. swift/common/middleware/crypto/kms_keymaster.py +2 -1
  63. swift/common/middleware/dlo.py +15 -11
  64. swift/common/middleware/domain_remap.py +5 -4
  65. swift/common/middleware/etag_quoter.py +128 -0
  66. swift/common/middleware/formpost.py +73 -70
  67. swift/common/middleware/gatekeeper.py +8 -1
  68. swift/common/middleware/keystoneauth.py +33 -3
  69. swift/common/middleware/list_endpoints.py +4 -4
  70. swift/common/middleware/listing_formats.py +85 -49
  71. swift/common/middleware/memcache.py +4 -95
  72. swift/common/middleware/name_check.py +3 -2
  73. swift/common/middleware/proxy_logging.py +160 -92
  74. swift/common/middleware/ratelimit.py +17 -10
  75. swift/common/middleware/read_only.py +6 -4
  76. swift/common/middleware/recon.py +59 -22
  77. swift/common/middleware/s3api/acl_handlers.py +25 -3
  78. swift/common/middleware/s3api/acl_utils.py +6 -1
  79. swift/common/middleware/s3api/controllers/__init__.py +6 -0
  80. swift/common/middleware/s3api/controllers/acl.py +3 -2
  81. swift/common/middleware/s3api/controllers/bucket.py +242 -137
  82. swift/common/middleware/s3api/controllers/logging.py +2 -2
  83. swift/common/middleware/s3api/controllers/multi_delete.py +43 -20
  84. swift/common/middleware/s3api/controllers/multi_upload.py +219 -133
  85. swift/common/middleware/s3api/controllers/obj.py +112 -8
  86. swift/common/middleware/s3api/controllers/object_lock.py +44 -0
  87. swift/common/middleware/s3api/controllers/s3_acl.py +2 -2
  88. swift/common/middleware/s3api/controllers/tagging.py +57 -0
  89. swift/common/middleware/s3api/controllers/versioning.py +36 -7
  90. swift/common/middleware/s3api/etree.py +22 -9
  91. swift/common/middleware/s3api/exception.py +0 -4
  92. swift/common/middleware/s3api/s3api.py +113 -41
  93. swift/common/middleware/s3api/s3request.py +384 -218
  94. swift/common/middleware/s3api/s3response.py +126 -23
  95. swift/common/middleware/s3api/s3token.py +16 -17
  96. swift/common/middleware/s3api/schema/delete.rng +1 -1
  97. swift/common/middleware/s3api/subresource.py +7 -10
  98. swift/common/middleware/s3api/utils.py +27 -10
  99. swift/common/middleware/slo.py +665 -358
  100. swift/common/middleware/staticweb.py +64 -37
  101. swift/common/middleware/symlink.py +51 -18
  102. swift/common/middleware/tempauth.py +76 -58
  103. swift/common/middleware/tempurl.py +191 -173
  104. swift/common/middleware/versioned_writes/__init__.py +51 -0
  105. swift/common/middleware/{versioned_writes.py → versioned_writes/legacy.py} +27 -26
  106. swift/common/middleware/versioned_writes/object_versioning.py +1482 -0
  107. swift/common/middleware/x_profile/exceptions.py +1 -4
  108. swift/common/middleware/x_profile/html_viewer.py +18 -19
  109. swift/common/middleware/x_profile/profile_model.py +1 -2
  110. swift/common/middleware/xprofile.py +10 -10
  111. swift-2.23.3.data/scripts/swift-container-server → swift/common/recon.py +13 -8
  112. swift/common/registry.py +147 -0
  113. swift/common/request_helpers.py +324 -57
  114. swift/common/ring/builder.py +67 -25
  115. swift/common/ring/composite_builder.py +1 -1
  116. swift/common/ring/ring.py +177 -51
  117. swift/common/ring/utils.py +1 -1
  118. swift/common/splice.py +10 -6
  119. swift/common/statsd_client.py +205 -0
  120. swift/common/storage_policy.py +49 -44
  121. swift/common/swob.py +86 -102
  122. swift/common/{utils.py → utils/__init__.py} +2163 -2772
  123. swift/common/utils/base.py +131 -0
  124. swift/common/utils/config.py +433 -0
  125. swift/common/utils/ipaddrs.py +256 -0
  126. swift/common/utils/libc.py +345 -0
  127. swift/common/utils/logs.py +859 -0
  128. swift/common/utils/timestamp.py +412 -0
  129. swift/common/wsgi.py +553 -535
  130. swift/container/auditor.py +14 -100
  131. swift/container/backend.py +490 -231
  132. swift/container/reconciler.py +126 -37
  133. swift/container/replicator.py +96 -22
  134. swift/container/server.py +358 -165
  135. swift/container/sharder.py +1540 -684
  136. swift/container/sync.py +94 -88
  137. swift/container/updater.py +53 -32
  138. swift/obj/auditor.py +153 -35
  139. swift/obj/diskfile.py +466 -217
  140. swift/obj/expirer.py +406 -124
  141. swift/obj/mem_diskfile.py +7 -4
  142. swift/obj/mem_server.py +1 -0
  143. swift/obj/reconstructor.py +523 -262
  144. swift/obj/replicator.py +249 -188
  145. swift/obj/server.py +207 -122
  146. swift/obj/ssync_receiver.py +145 -85
  147. swift/obj/ssync_sender.py +113 -54
  148. swift/obj/updater.py +652 -139
  149. swift/obj/watchers/__init__.py +0 -0
  150. swift/obj/watchers/dark_data.py +213 -0
  151. swift/proxy/controllers/account.py +11 -11
  152. swift/proxy/controllers/base.py +848 -604
  153. swift/proxy/controllers/container.py +433 -92
  154. swift/proxy/controllers/info.py +3 -2
  155. swift/proxy/controllers/obj.py +1000 -489
  156. swift/proxy/server.py +185 -112
  157. {swift-2.23.3.dist-info → swift-2.35.0.dist-info}/AUTHORS +58 -11
  158. {swift-2.23.3.dist-info → swift-2.35.0.dist-info}/METADATA +51 -56
  159. swift-2.35.0.dist-info/RECORD +201 -0
  160. {swift-2.23.3.dist-info → swift-2.35.0.dist-info}/WHEEL +1 -1
  161. {swift-2.23.3.dist-info → swift-2.35.0.dist-info}/entry_points.txt +43 -0
  162. swift-2.35.0.dist-info/pbr.json +1 -0
  163. swift/locale/de/LC_MESSAGES/swift.po +0 -1216
  164. swift/locale/en_GB/LC_MESSAGES/swift.po +0 -1207
  165. swift/locale/es/LC_MESSAGES/swift.po +0 -1085
  166. swift/locale/fr/LC_MESSAGES/swift.po +0 -909
  167. swift/locale/it/LC_MESSAGES/swift.po +0 -894
  168. swift/locale/ja/LC_MESSAGES/swift.po +0 -965
  169. swift/locale/ko_KR/LC_MESSAGES/swift.po +0 -964
  170. swift/locale/pt_BR/LC_MESSAGES/swift.po +0 -881
  171. swift/locale/ru/LC_MESSAGES/swift.po +0 -891
  172. swift/locale/tr_TR/LC_MESSAGES/swift.po +0 -832
  173. swift/locale/zh_CN/LC_MESSAGES/swift.po +0 -833
  174. swift/locale/zh_TW/LC_MESSAGES/swift.po +0 -838
  175. swift-2.23.3.data/scripts/swift-account-auditor +0 -23
  176. swift-2.23.3.data/scripts/swift-account-info +0 -51
  177. swift-2.23.3.data/scripts/swift-account-reaper +0 -23
  178. swift-2.23.3.data/scripts/swift-account-replicator +0 -34
  179. swift-2.23.3.data/scripts/swift-account-server +0 -23
  180. swift-2.23.3.data/scripts/swift-container-auditor +0 -23
  181. swift-2.23.3.data/scripts/swift-container-info +0 -55
  182. swift-2.23.3.data/scripts/swift-container-reconciler +0 -21
  183. swift-2.23.3.data/scripts/swift-container-replicator +0 -34
  184. swift-2.23.3.data/scripts/swift-container-sharder +0 -37
  185. swift-2.23.3.data/scripts/swift-container-sync +0 -23
  186. swift-2.23.3.data/scripts/swift-container-updater +0 -23
  187. swift-2.23.3.data/scripts/swift-dispersion-report +0 -24
  188. swift-2.23.3.data/scripts/swift-form-signature +0 -20
  189. swift-2.23.3.data/scripts/swift-init +0 -119
  190. swift-2.23.3.data/scripts/swift-object-auditor +0 -29
  191. swift-2.23.3.data/scripts/swift-object-expirer +0 -33
  192. swift-2.23.3.data/scripts/swift-object-info +0 -60
  193. swift-2.23.3.data/scripts/swift-object-reconstructor +0 -33
  194. swift-2.23.3.data/scripts/swift-object-relinker +0 -41
  195. swift-2.23.3.data/scripts/swift-object-replicator +0 -37
  196. swift-2.23.3.data/scripts/swift-object-server +0 -27
  197. swift-2.23.3.data/scripts/swift-object-updater +0 -23
  198. swift-2.23.3.data/scripts/swift-proxy-server +0 -23
  199. swift-2.23.3.data/scripts/swift-recon +0 -24
  200. swift-2.23.3.data/scripts/swift-ring-builder +0 -24
  201. swift-2.23.3.data/scripts/swift-ring-builder-analyzer +0 -22
  202. swift-2.23.3.data/scripts/swift-ring-composer +0 -22
  203. swift-2.23.3.dist-info/RECORD +0 -220
  204. swift-2.23.3.dist-info/pbr.json +0 -1
  205. {swift-2.23.3.dist-info → swift-2.35.0.dist-info}/LICENSE +0 -0
  206. {swift-2.23.3.dist-info → swift-2.35.0.dist-info}/top_level.txt +0 -0
@@ -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 quote, 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,28 +430,30 @@ 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
458
  headers[index]['X-Backend-Quoted-Container-Path'] = quote(
344
459
  container_path)
@@ -351,11 +466,12 @@ class BaseObjectController(Controller):
351
466
  # will eat the update and move it as a misplaced object.
352
467
 
353
468
  def set_delete_at_headers(index, delete_at_node):
469
+ ip, port = get_ip_port(delete_at_node, headers[index])
354
470
  headers[index]['X-Delete-At-Container'] = delete_at_container
355
471
  headers[index]['X-Delete-At-Partition'] = delete_at_partition
356
472
  headers[index]['X-Delete-At-Host'] = csv_append(
357
473
  headers[index].get('X-Delete-At-Host'),
358
- '%(ip)s:%(port)s' % delete_at_node)
474
+ '%(ip)s:%(port)s' % {'ip': ip, 'port': port})
359
475
  headers[index]['X-Delete-At-Device'] = csv_append(
360
476
  headers[index].get('X-Delete-At-Device'),
361
477
  delete_at_node['device'])
@@ -404,7 +520,7 @@ class BaseObjectController(Controller):
404
520
 
405
521
  def _get_conn_response(self, putter, path, logger_thread_locals,
406
522
  final_phase, **kwargs):
407
- self.app.logger.thread_locals = logger_thread_locals
523
+ self.logger.thread_locals = logger_thread_locals
408
524
  try:
409
525
  resp = putter.await_response(
410
526
  self.app.node_timeout, not final_phase)
@@ -415,8 +531,8 @@ class BaseObjectController(Controller):
415
531
  else:
416
532
  status_type = 'commit'
417
533
  self.app.exception_occurred(
418
- putter.node, _('Object'),
419
- _('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' %
420
536
  {'status_type': status_type, 'path': path})
421
537
  return (putter, resp)
422
538
 
@@ -461,7 +577,7 @@ class BaseObjectController(Controller):
461
577
  if putter.failed:
462
578
  continue
463
579
  pile.spawn(self._get_conn_response, putter, req.path,
464
- self.app.logger.thread_locals, final_phase=final_phase)
580
+ self.logger.thread_locals, final_phase=final_phase)
465
581
 
466
582
  def _handle_response(putter, response):
467
583
  statuses.append(response.status)
@@ -471,20 +587,11 @@ class BaseObjectController(Controller):
471
587
  else:
472
588
  body = b''
473
589
  bodies.append(body)
474
- if response.status == HTTP_INSUFFICIENT_STORAGE:
475
- putter.failed = True
476
- self.app.error_limit(putter.node,
477
- _('ERROR Insufficient Storage'))
478
- elif response.status >= HTTP_INTERNAL_SERVER_ERROR:
590
+ if not self.app.check_response(putter.node, 'Object', response,
591
+ req.method, req.path, body):
479
592
  putter.failed = True
480
- self.app.error_occurred(
481
- putter.node,
482
- _('ERROR %(status)d %(body)s From Object Server '
483
- 're: %(path)s') %
484
- {'status': response.status,
485
- 'body': body[:1024], 'path': req.path})
486
593
  elif is_success(response.status):
487
- etags.add(response.getheader('etag').strip('"'))
594
+ etags.add(normalize_etag(response.getheader('etag')))
488
595
 
489
596
  for (putter, response) in pile:
490
597
  if response:
@@ -517,19 +624,16 @@ class BaseObjectController(Controller):
517
624
  req = constraints.check_delete_headers(req)
518
625
 
519
626
  if 'x-delete-at' in req.headers:
520
- x_delete_at = int(normalize_delete_at_timestamp(
521
- 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'])
522
630
 
523
- req.environ.setdefault('swift.log_info', []).append(
524
- 'x-delete-at:%s' % x_delete_at)
631
+ append_log_info(req.environ, 'x-delete-at:%s' % x_delete_at)
525
632
 
526
- delete_at_container = get_expirer_container(
527
- x_delete_at, self.app.expiring_objects_container_divisor,
528
- self.account_name, self.container_name, self.object_name)
529
-
530
- delete_at_part, delete_at_nodes = \
531
- self.app.container_ring.get_nodes(
532
- 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)
533
637
 
534
638
  return req, delete_at_container, delete_at_part, delete_at_nodes
535
639
 
@@ -544,23 +648,6 @@ class BaseObjectController(Controller):
544
648
  if detect_content_type:
545
649
  req.headers.pop('x-detect-content-type')
546
650
 
547
- def _update_x_timestamp(self, req):
548
- # The container sync feature includes an x-timestamp header with
549
- # requests. If present this is checked and preserved, otherwise a fresh
550
- # timestamp is added.
551
- if 'x-timestamp' in req.headers:
552
- try:
553
- req_timestamp = Timestamp(req.headers['X-Timestamp'])
554
- except ValueError:
555
- raise HTTPBadRequest(
556
- request=req, content_type='text/plain',
557
- body='X-Timestamp should be a UNIX timestamp float value; '
558
- 'was %r' % req.headers['x-timestamp'])
559
- req.headers['X-Timestamp'] = req_timestamp.internal
560
- else:
561
- req.headers['X-Timestamp'] = Timestamp.now().internal
562
- return None
563
-
564
651
  def _check_failure_put_connections(self, putters, req, min_conns):
565
652
  """
566
653
  Identify any failed connections and check minimum connection count.
@@ -574,8 +661,8 @@ class BaseObjectController(Controller):
574
661
  putter.resp.status for putter in putters if putter.resp]
575
662
  if HTTP_PRECONDITION_FAILED in statuses:
576
663
  # If we find any copy of the file, it shouldn't be uploaded
577
- self.app.logger.debug(
578
- _('Object PUT returning 412, %(statuses)r'),
664
+ self.logger.debug(
665
+ 'Object PUT returning 412, %(statuses)r',
579
666
  {'statuses': statuses})
580
667
  raise HTTPPreconditionFailed(request=req)
581
668
 
@@ -587,9 +674,9 @@ class BaseObjectController(Controller):
587
674
  putter.resp.getheaders()).get(
588
675
  'X-Backend-Timestamp', 'unknown')
589
676
  } for putter in putters if putter.resp]
590
- self.app.logger.debug(
591
- _('Object PUT returning 202 for 409: '
592
- '%(req_timestamp)s <= %(timestamps)r'),
677
+ self.logger.debug(
678
+ 'Object PUT returning 202 for 409: '
679
+ '%(req_timestamp)s <= %(timestamps)r',
593
680
  {'req_timestamp': req.timestamp.internal,
594
681
  'timestamps': ', '.join(status_times)})
595
682
  raise HTTPAccepted(request=req)
@@ -625,27 +712,26 @@ class BaseObjectController(Controller):
625
712
  :param req: a swob Request
626
713
  :param headers: request headers
627
714
  :param logger_thread_locals: The thread local values to be set on the
628
- self.app.logger to retain transaction
715
+ self.logger to retain transaction
629
716
  logging information.
630
717
  :return: an instance of a Putter
631
718
  """
632
- self.app.logger.thread_locals = logger_thread_locals
719
+ self.logger.thread_locals = logger_thread_locals
633
720
  for node in nodes:
634
721
  try:
635
722
  putter = self._make_putter(node, part, req, headers)
636
723
  self.app.set_node_timing(node, putter.connect_duration)
637
724
  return putter
638
725
  except InsufficientStorage:
639
- self.app.error_limit(node, _('ERROR Insufficient Storage'))
726
+ self.app.error_limit(node, 'ERROR Insufficient Storage')
640
727
  except PutterConnectError as e:
641
- self.app.error_occurred(
642
- node, _('ERROR %(status)d Expect: 100-continue '
643
- 'From Object Server') % {
644
- 'status': e.status})
728
+ msg = 'ERROR %d Expect: 100-continue From Object Server'
729
+ self.app.error_occurred(node, msg % e.status)
645
730
  except (Exception, Timeout):
646
731
  self.app.exception_occurred(
647
- node, _('Object'),
648
- _('Expect: 100-continue on %s') % req.swift_entity_path)
732
+ node, 'Object',
733
+ 'Expect: 100-continue on %s' %
734
+ quote(req.swift_entity_path))
649
735
 
650
736
  def _get_put_connections(self, req, nodes, partition, outgoing_headers,
651
737
  policy):
@@ -654,7 +740,8 @@ class BaseObjectController(Controller):
654
740
  """
655
741
  obj_ring = policy.object_ring
656
742
  node_iter = GreenthreadSafeIterator(
657
- self.iter_nodes_local_first(obj_ring, partition, policy=policy))
743
+ self.iter_nodes_local_first(obj_ring, partition, req,
744
+ policy=policy))
658
745
  pile = GreenPile(len(nodes))
659
746
 
660
747
  for nheaders in outgoing_headers:
@@ -665,19 +752,18 @@ class BaseObjectController(Controller):
665
752
  del nheaders['Content-Length']
666
753
  nheaders['Expect'] = '100-continue'
667
754
  pile.spawn(self._connect_put_node, node_iter, partition,
668
- req, nheaders, self.app.logger.thread_locals)
755
+ req, nheaders, self.logger.thread_locals)
669
756
 
670
757
  putters = [putter for putter in pile if putter]
671
758
 
672
759
  return putters
673
760
 
674
761
  def _check_min_conn(self, req, putters, min_conns, msg=None):
675
- msg = msg or _('Object PUT returning 503, %(conns)s/%(nodes)s '
676
- 'required connections')
762
+ msg = msg or ('Object PUT returning 503, %(conns)s/%(nodes)s '
763
+ 'required connections')
677
764
 
678
765
  if len(putters) < min_conns:
679
- self.app.logger.error((msg),
680
- {'conns': len(putters), 'nodes': min_conns})
766
+ self.logger.error(msg, {'conns': len(putters), 'nodes': min_conns})
681
767
  raise HTTPServiceUnavailable(request=req)
682
768
 
683
769
  def _get_footers(self, req):
@@ -756,8 +842,6 @@ class BaseObjectController(Controller):
756
842
  policy_index = req.headers.get('X-Backend-Storage-Policy-Index',
757
843
  container_info['storage_policy'])
758
844
  obj_ring = self.app.get_object_ring(policy_index)
759
- container_partition, container_nodes, container_path = \
760
- self._get_update_target(req, container_info)
761
845
  partition, nodes = obj_ring.get_nodes(
762
846
  self.account_name, self.container_name, self.object_name)
763
847
 
@@ -775,13 +859,13 @@ class BaseObjectController(Controller):
775
859
  if aresp:
776
860
  return aresp
777
861
 
778
- if not container_nodes:
862
+ if not is_success(container_info.get('status')):
779
863
  return HTTPNotFound(request=req)
780
864
 
781
865
  # update content type in case it is missing
782
866
  self._update_content_type(req)
783
867
 
784
- self._update_x_timestamp(req)
868
+ req.ensure_x_timestamp()
785
869
 
786
870
  # check constraints on object name and request headers
787
871
  error_response = check_object_creation(req, self.object_name) or \
@@ -803,9 +887,8 @@ class BaseObjectController(Controller):
803
887
 
804
888
  # add special headers to be handled by storage nodes
805
889
  outgoing_headers = self._backend_requests(
806
- req, len(nodes), container_partition, container_nodes,
807
- delete_at_container, delete_at_part, delete_at_nodes,
808
- container_path=container_path)
890
+ req, len(nodes), container_info,
891
+ delete_at_container, delete_at_part, delete_at_nodes)
809
892
 
810
893
  # send object to storage nodes
811
894
  resp = self._store_object(
@@ -828,20 +911,18 @@ class BaseObjectController(Controller):
828
911
  next_part_power = getattr(obj_ring, 'next_part_power', None)
829
912
  if next_part_power:
830
913
  req.headers['X-Backend-Next-Part-Power'] = next_part_power
831
- container_partition, container_nodes, container_path = \
832
- self._get_update_target(req, container_info)
833
914
  req.acl = container_info['write_acl']
834
915
  req.environ['swift_sync_key'] = container_info['sync_key']
835
916
  if 'swift.authorize' in req.environ:
836
917
  aresp = req.environ['swift.authorize'](req)
837
918
  if aresp:
838
919
  return aresp
839
- if not container_nodes:
920
+ if not is_success(container_info.get('status')):
840
921
  return HTTPNotFound(request=req)
841
922
  partition, nodes = obj_ring.get_nodes(
842
923
  self.account_name, self.container_name, self.object_name)
843
924
 
844
- self._update_x_timestamp(req)
925
+ req.ensure_x_timestamp()
845
926
 
846
927
  # Include local handoff nodes if write-affinity is enabled.
847
928
  node_count = len(nodes)
@@ -856,12 +937,10 @@ class BaseObjectController(Controller):
856
937
  local_handoffs = len(nodes) - len(local_primaries)
857
938
  node_count += local_handoffs
858
939
  node_iterator = self.iter_nodes_local_first(
859
- obj_ring, partition, policy=policy, local_handoffs_first=True
860
- )
940
+ obj_ring, partition, req, policy=policy,
941
+ local_handoffs_first=True)
861
942
 
862
- headers = self._backend_requests(
863
- req, node_count, container_partition, container_nodes,
864
- container_path=container_path)
943
+ headers = self._backend_requests(req, node_count, container_info)
865
944
  return self._delete_object(req, obj_ring, partition, headers,
866
945
  node_count=node_count,
867
946
  node_iterator=node_iterator)
@@ -872,27 +951,31 @@ class ReplicatedObjectController(BaseObjectController):
872
951
 
873
952
  def _get_or_head_response(self, req, node_iter, partition, policy):
874
953
  concurrency = self.app.get_object_ring(policy.idx).replica_count \
875
- if self.app.concurrent_gets else 1
954
+ if self.app.get_policy_options(policy).concurrent_gets else 1
876
955
  resp = self.GETorHEAD_base(
877
- req, _('Object'), node_iter, partition,
878
- req.swift_entity_path, concurrency)
956
+ req, 'Object', node_iter, partition,
957
+ req.swift_entity_path, concurrency, policy)
879
958
  return resp
880
959
 
881
960
  def _make_putter(self, node, part, req, headers):
882
961
  if req.environ.get('swift.callback.update_footers'):
883
962
  putter = MIMEPutter.connect(
884
- node, part, req.swift_entity_path, headers,
963
+ node, part, req.swift_entity_path, headers, self.app.watchdog,
885
964
  conn_timeout=self.app.conn_timeout,
886
965
  node_timeout=self.app.node_timeout,
887
- logger=self.app.logger,
966
+ write_timeout=self.app.node_timeout,
967
+ send_exception_handler=self.app.exception_occurred,
968
+ logger=self.logger,
888
969
  need_multiphase=False)
889
970
  else:
890
971
  te = ',' + headers.get('Transfer-Encoding', '')
891
972
  putter = Putter.connect(
892
- node, part, req.swift_entity_path, headers,
973
+ node, part, req.swift_entity_path, headers, self.app.watchdog,
893
974
  conn_timeout=self.app.conn_timeout,
894
975
  node_timeout=self.app.node_timeout,
895
- logger=self.app.logger,
976
+ write_timeout=self.app.node_timeout,
977
+ send_exception_handler=self.app.exception_occurred,
978
+ logger=self.logger,
896
979
  chunked=te.endswith(',chunked'))
897
980
  return putter
898
981
 
@@ -903,77 +986,72 @@ class ReplicatedObjectController(BaseObjectController):
903
986
  This method was added in the PUT method extraction change
904
987
  """
905
988
  bytes_transferred = 0
989
+ data_source = CooperativeIterator(data_source)
906
990
 
907
991
  def send_chunk(chunk):
992
+ timeout_at = time.time() + self.app.node_timeout
908
993
  for putter in list(putters):
909
994
  if not putter.failed:
910
- putter.send_chunk(chunk)
995
+ putter.send_chunk(chunk, timeout_at=timeout_at)
911
996
  else:
912
997
  putter.close()
913
998
  putters.remove(putter)
914
999
  self._check_min_conn(
915
1000
  req, putters, min_conns,
916
- msg=_('Object PUT exceptions during send, '
917
- '%(conns)s/%(nodes)s required connections'))
1001
+ msg='Object PUT exceptions during send, '
1002
+ '%(conns)s/%(nodes)s required connections')
918
1003
 
919
1004
  min_conns = quorum_size(len(nodes))
920
1005
  try:
921
- with ContextPool(len(nodes)) as pool:
922
- for putter in putters:
923
- putter.spawn_sender_greenthread(
924
- pool, self.app.put_queue_depth, self.app.node_timeout,
925
- self.app.exception_occurred)
926
- while True:
927
- with ChunkReadTimeout(self.app.client_timeout):
928
- try:
929
- chunk = next(data_source)
930
- except StopIteration:
931
- break
932
- bytes_transferred += len(chunk)
933
- if bytes_transferred > constraints.MAX_FILE_SIZE:
934
- raise HTTPRequestEntityTooLarge(request=req)
935
-
936
- send_chunk(chunk)
937
-
938
- ml = req.message_length()
939
- if ml and bytes_transferred < ml:
940
- req.client_disconnect = True
941
- self.app.logger.warning(
942
- _('Client disconnected without sending enough data'))
943
- self.app.logger.increment('client_disconnects')
944
- raise HTTPClientDisconnect(request=req)
945
-
946
- trail_md = self._get_footers(req)
947
- for putter in putters:
948
- # send any footers set by middleware
949
- putter.end_of_object_data(footer_metadata=trail_md)
950
-
951
- for putter in putters:
952
- putter.wait()
953
- self._check_min_conn(
954
- req, [p for p in putters if not p.failed], min_conns,
955
- msg=_('Object PUT exceptions after last send, '
956
- '%(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')
957
1036
  except ChunkReadTimeout as err:
958
- self.app.logger.warning(
959
- _('ERROR Client read timeout (%ss)'), err.seconds)
960
- 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')
961
1040
  raise HTTPRequestTimeout(request=req)
962
1041
  except HTTPException:
963
1042
  raise
964
1043
  except ChunkReadError:
965
- req.client_disconnect = True
966
- self.app.logger.warning(
967
- _('Client disconnected without sending last chunk'))
968
- self.app.logger.increment('client_disconnects')
1044
+ self.logger.warning(
1045
+ 'Client disconnected without sending last chunk')
1046
+ self.logger.increment('object.client_disconnects')
969
1047
  raise HTTPClientDisconnect(request=req)
970
1048
  except Timeout:
971
- self.app.logger.exception(
972
- _('ERROR Exception causing client disconnect'))
1049
+ self.logger.exception(
1050
+ 'ERROR Exception causing client disconnect')
973
1051
  raise HTTPClientDisconnect(request=req)
974
1052
  except Exception:
975
- self.app.logger.exception(
976
- _('ERROR Exception transferring data to object servers %s'),
1053
+ self.logger.exception(
1054
+ 'ERROR Exception transferring data to object servers %s',
977
1055
  {'path': req.path})
978
1056
  raise HTTPInternalServerError(request=req)
979
1057
 
@@ -1016,14 +1094,13 @@ class ReplicatedObjectController(BaseObjectController):
1016
1094
  putter.close()
1017
1095
 
1018
1096
  if len(etags) > 1:
1019
- self.app.logger.error(
1020
- _('Object servers returned %s mismatched etags'), len(etags))
1097
+ self.logger.error(
1098
+ 'Object servers returned %s mismatched etags', len(etags))
1021
1099
  return HTTPServerError(request=req)
1022
1100
  etag = etags.pop() if len(etags) else None
1023
1101
  resp = self.best_response(req, statuses, reasons, bodies,
1024
- _('Object PUT'), etag=etag)
1025
- resp.last_modified = math.ceil(
1026
- float(Timestamp(req.headers['X-Timestamp'])))
1102
+ 'Object PUT', etag=etag)
1103
+ resp.last_modified = Timestamp(req.headers['X-Timestamp']).ceil()
1027
1104
  return resp
1028
1105
 
1029
1106
 
@@ -1067,14 +1144,15 @@ class ECAppIter(object):
1067
1144
  self.mime_boundary = None
1068
1145
  self.learned_content_type = None
1069
1146
  self.stashed_iter = None
1147
+ self.pool = ContextPool(len(internal_parts_iters))
1070
1148
 
1071
1149
  def close(self):
1072
- # close down the stashed iter first so the ContextPool can
1073
- # 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
1074
1152
  # executing the internal_parts_iters.
1075
1153
  if self.stashed_iter:
1076
- self.stashed_iter.close()
1077
- sleep() # Give the per-frag threads a chance to clean up
1154
+ close_if_possible(self.stashed_iter)
1155
+ self.pool.close()
1078
1156
  for it in self.internal_parts_iters:
1079
1157
  close_if_possible(it)
1080
1158
 
@@ -1210,10 +1288,13 @@ class ECAppIter(object):
1210
1288
 
1211
1289
  def __iter__(self):
1212
1290
  if self.stashed_iter is not None:
1213
- return iter(self.stashed_iter)
1291
+ return self
1214
1292
  else:
1215
1293
  raise ValueError("Failed to call kickoff() before __iter__()")
1216
1294
 
1295
+ def __next__(self):
1296
+ return next(self.stashed_iter)
1297
+
1217
1298
  def _real_iter(self, req, resp_headers):
1218
1299
  if not self.range_specs:
1219
1300
  client_asked_for_range = False
@@ -1398,7 +1479,8 @@ class ECAppIter(object):
1398
1479
  # segment at a time.
1399
1480
  queues = [Queue(1) for _junk in range(len(fragment_iters))]
1400
1481
 
1401
- 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
1402
1484
  try:
1403
1485
  for fragment in frag_iter:
1404
1486
  if fragment.startswith(b' '):
@@ -1408,20 +1490,28 @@ class ECAppIter(object):
1408
1490
  # killed by contextpool
1409
1491
  pass
1410
1492
  except ChunkReadTimeout:
1411
- # unable to resume in GetOrHeadHandler
1412
- self.logger.exception(_("Timeout fetching fragments for %r"),
1413
- 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))
1414
1502
  except: # noqa
1415
- self.logger.exception(_("Exception fetching fragments for"
1416
- " %r"), self.path)
1503
+ self.logger.exception("Exception fetching fragments for %r",
1504
+ quote(self.path))
1417
1505
  finally:
1418
1506
  queue.resize(2) # ensure there's room
1419
1507
  queue.put(None)
1420
1508
  frag_iter.close()
1421
1509
 
1422
- with ContextPool(len(fragment_iters)) as pool:
1510
+ segments_decoded = 0
1511
+ with self.pool as pool:
1423
1512
  for frag_iter, queue in zip(fragment_iters, queues):
1424
- pool.spawn(put_fragments_in_queue, frag_iter, queue)
1513
+ pool.spawn(put_fragments_in_queue, frag_iter, queue,
1514
+ self.logger.thread_locals)
1425
1515
 
1426
1516
  while True:
1427
1517
  fragments = []
@@ -1436,15 +1526,27 @@ class ECAppIter(object):
1436
1526
  # with an un-reconstructible list of fragments - so we'll
1437
1527
  # break out of the iter so WSGI can tear down the broken
1438
1528
  # connection.
1439
- 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))
1440
1536
  break
1441
1537
  try:
1442
1538
  segment = self.policy.pyeclib_driver.decode(fragments)
1443
- except ECDriverError:
1444
- self.logger.exception(_("Error decoding fragments for"
1445
- " %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)))
1446
1547
  raise
1447
1548
 
1549
+ segments_decoded += 1
1448
1550
  yield segment
1449
1551
 
1450
1552
  def app_iter_range(self, start, end):
@@ -1584,10 +1686,15 @@ class Putter(object):
1584
1686
  :param resp: an HTTPResponse instance if connect() received final response
1585
1687
  :param path: the object path to send to the storage node
1586
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
1587
1693
  :param logger: a Logger instance
1588
1694
  :param chunked: boolean indicating if the request encoding is chunked
1589
1695
  """
1590
- 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,
1591
1698
  chunked=False):
1592
1699
  # Note: you probably want to call Putter.connect() instead of
1593
1700
  # instantiating one of these directly.
@@ -1596,11 +1703,13 @@ class Putter(object):
1596
1703
  self.resp = self.final_resp = resp
1597
1704
  self.path = path
1598
1705
  self.connect_duration = connect_duration
1706
+ self.watchdog = watchdog
1707
+ self.write_timeout = write_timeout
1708
+ self.send_exception_handler = send_exception_handler
1599
1709
  # for handoff nodes node_index is None
1600
1710
  self.node_index = node.get('index')
1601
1711
 
1602
1712
  self.failed = False
1603
- self.queue = None
1604
1713
  self.state = NO_DATA_SENT
1605
1714
  self.chunked = chunked
1606
1715
  self.logger = logger
@@ -1632,22 +1741,12 @@ class Putter(object):
1632
1741
  self.resp = self.conn.getresponse()
1633
1742
  return self.resp
1634
1743
 
1635
- def spawn_sender_greenthread(self, pool, queue_depth, write_timeout,
1636
- exception_handler):
1637
- """Call before sending the first chunk of request body"""
1638
- self.queue = Queue(queue_depth)
1639
- pool.spawn(self._send_file, write_timeout, exception_handler)
1640
-
1641
- def wait(self):
1642
- if self.queue.unfinished_tasks:
1643
- self.queue.join()
1644
-
1645
1744
  def _start_object_data(self):
1646
1745
  # Called immediately before the first chunk of object data is sent.
1647
1746
  # Subclasses may implement custom behaviour
1648
1747
  pass
1649
1748
 
1650
- def send_chunk(self, chunk):
1749
+ def send_chunk(self, chunk, timeout_at=None):
1651
1750
  if not chunk:
1652
1751
  # If we're not using chunked transfer-encoding, sending a 0-byte
1653
1752
  # chunk is just wasteful. If we *are* using chunked
@@ -1661,7 +1760,7 @@ class Putter(object):
1661
1760
  self._start_object_data()
1662
1761
  self.state = SENDING_DATA
1663
1762
 
1664
- self.queue.put(chunk)
1763
+ self._send_chunk(chunk, timeout_at=timeout_at)
1665
1764
 
1666
1765
  def end_of_object_data(self, **kwargs):
1667
1766
  """
@@ -1670,33 +1769,24 @@ class Putter(object):
1670
1769
  if self.state == DATA_SENT:
1671
1770
  raise ValueError("called end_of_object_data twice")
1672
1771
 
1673
- self.queue.put(b'')
1772
+ self._send_chunk(b'')
1674
1773
  self.state = DATA_SENT
1675
1774
 
1676
- def _send_file(self, write_timeout, exception_handler):
1677
- """
1678
- Method for a file PUT coroutine. Takes chunks from a queue and sends
1679
- them down a socket.
1680
-
1681
- If something goes wrong, the "failed" attribute will be set to true
1682
- and the exception handler will be called.
1683
- """
1684
- while True:
1685
- chunk = self.queue.get()
1686
- if not self.failed:
1687
- if self.chunked:
1688
- to_send = b"%x\r\n%s\r\n" % (len(chunk), chunk)
1689
- else:
1690
- to_send = chunk
1691
- try:
1692
- with ChunkWriteTimeout(write_timeout):
1693
- self.conn.send(to_send)
1694
- except (Exception, ChunkWriteTimeout):
1695
- self.failed = True
1696
- exception_handler(self.node, _('Object'),
1697
- _('Trying to write to %s') % self.path)
1698
-
1699
- 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))
1700
1790
 
1701
1791
  def close(self):
1702
1792
  # release reference to response to ensure connection really does close,
@@ -1707,9 +1797,10 @@ class Putter(object):
1707
1797
  @classmethod
1708
1798
  def _make_connection(cls, node, part, path, headers, conn_timeout,
1709
1799
  node_timeout):
1800
+ ip, port = get_ip_port(node, headers)
1710
1801
  start_time = time.time()
1711
1802
  with ConnectionTimeout(conn_timeout):
1712
- conn = http_connect(node['ip'], node['port'], node['device'],
1803
+ conn = http_connect(ip, port, node['device'],
1713
1804
  part, 'PUT', path, headers)
1714
1805
  connect_duration = time.time() - start_time
1715
1806
 
@@ -1732,7 +1823,8 @@ class Putter(object):
1732
1823
  return conn, resp, final_resp, connect_duration
1733
1824
 
1734
1825
  @classmethod
1735
- 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,
1736
1828
  logger=None, chunked=False, **kwargs):
1737
1829
  """
1738
1830
  Connect to a backend node and send the headers.
@@ -1746,7 +1838,8 @@ class Putter(object):
1746
1838
  """
1747
1839
  conn, expect_resp, final_resp, connect_duration = cls._make_connection(
1748
1840
  node, part, path, headers, conn_timeout, node_timeout)
1749
- 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,
1750
1843
  chunked=chunked)
1751
1844
 
1752
1845
 
@@ -1760,10 +1853,13 @@ class MIMEPutter(Putter):
1760
1853
 
1761
1854
  An HTTP PUT request that supports streaming.
1762
1855
  """
1763
- def __init__(self, conn, node, resp, req, connect_duration,
1764
- logger, mime_boundary, multiphase=False):
1765
- super(MIMEPutter, self).__init__(conn, node, resp, req,
1766
- 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)
1767
1863
  # Note: you probably want to call MimePutter.connect() instead of
1768
1864
  # instantiating one of these directly.
1769
1865
  self.chunked = True # MIME requests always send chunked body
@@ -1774,8 +1870,8 @@ class MIMEPutter(Putter):
1774
1870
  # We're sending the object plus other stuff in the same request
1775
1871
  # body, all wrapped up in multipart MIME, so we'd better start
1776
1872
  # off the MIME document before sending any object data.
1777
- self.queue.put(b"--%s\r\nX-Document: object body\r\n\r\n" %
1778
- (self.mime_boundary,))
1873
+ self._send_chunk(b"--%s\r\nX-Document: object body\r\n\r\n" %
1874
+ (self.mime_boundary,))
1779
1875
 
1780
1876
  def end_of_object_data(self, footer_metadata=None):
1781
1877
  """
@@ -1793,7 +1889,8 @@ class MIMEPutter(Putter):
1793
1889
  self._start_object_data()
1794
1890
 
1795
1891
  footer_body = json.dumps(footer_metadata).encode('ascii')
1796
- footer_md5 = md5(footer_body).hexdigest().encode('ascii')
1892
+ footer_md5 = md5(
1893
+ footer_body, usedforsecurity=False).hexdigest().encode('ascii')
1797
1894
 
1798
1895
  tail_boundary = (b"--%s" % (self.mime_boundary,))
1799
1896
  if not self.multiphase:
@@ -1808,9 +1905,9 @@ class MIMEPutter(Putter):
1808
1905
  footer_body, b"\r\n",
1809
1906
  tail_boundary, b"\r\n",
1810
1907
  ]
1811
- self.queue.put(b"".join(message_parts))
1908
+ self._send_chunk(b"".join(message_parts))
1812
1909
 
1813
- self.queue.put(b'')
1910
+ self._send_chunk(b'')
1814
1911
  self.state = DATA_SENT
1815
1912
 
1816
1913
  def send_commit_confirmation(self):
@@ -1835,13 +1932,14 @@ class MIMEPutter(Putter):
1835
1932
  body, b"\r\n",
1836
1933
  tail_boundary,
1837
1934
  ]
1838
- self.queue.put(b"".join(message_parts))
1935
+ self._send_chunk(b"".join(message_parts))
1839
1936
 
1840
- self.queue.put(b'')
1937
+ self._send_chunk(b'')
1841
1938
  self.state = COMMIT_SENT
1842
1939
 
1843
1940
  @classmethod
1844
- 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,
1845
1943
  logger=None, need_multiphase=True, **kwargs):
1846
1944
  """
1847
1945
  Connect to a backend node and send the headers.
@@ -1879,7 +1977,7 @@ class MIMEPutter(Putter):
1879
1977
  headers['X-Backend-Obj-Multiphase-Commit'] = 'yes'
1880
1978
 
1881
1979
  conn, expect_resp, final_resp, connect_duration = cls._make_connection(
1882
- node, part, req, headers, conn_timeout, node_timeout)
1980
+ node, part, path, headers, conn_timeout, node_timeout)
1883
1981
 
1884
1982
  if is_informational(expect_resp.status):
1885
1983
  continue_headers = HeaderKeyDict(expect_resp.getheaders())
@@ -1894,7 +1992,8 @@ class MIMEPutter(Putter):
1894
1992
  if need_multiphase and not can_handle_multiphase_put:
1895
1993
  raise MultiphasePUTNotSupported()
1896
1994
 
1897
- 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,
1898
1997
  mime_boundary, multiphase=need_multiphase)
1899
1998
 
1900
1999
 
@@ -1996,13 +2095,16 @@ class ECGetResponseBucket(object):
1996
2095
  A helper class to encapsulate the properties of buckets in which fragment
1997
2096
  getters and alternate nodes are collected.
1998
2097
  """
1999
- def __init__(self, policy, timestamp_str):
2098
+ def __init__(self, policy, timestamp):
2000
2099
  """
2001
2100
  :param policy: an instance of ECStoragePolicy
2002
- :param timestamp_str: a string representation of a timestamp
2101
+ :param timestamp: a Timestamp, or None for a bucket of error responses
2003
2102
  """
2004
2103
  self.policy = policy
2005
- 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
2006
2108
  self.gets = collections.defaultdict(list)
2007
2109
  self.alt_nodes = collections.defaultdict(list)
2008
2110
  self._durable = False
@@ -2016,10 +2118,20 @@ class ECGetResponseBucket(object):
2016
2118
  return self._durable
2017
2119
 
2018
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)
2019
2131
  if not self.gets:
2020
- self.status = getter.last_status
2021
2132
  # stash first set of backend headers, which will be used to
2022
2133
  # populate a client response
2134
+ self.status = getter.last_status
2023
2135
  # TODO: each bucket is for a single *data* timestamp, but sources
2024
2136
  # in the same bucket may have different *metadata* timestamps if
2025
2137
  # some backends have more recent .meta files than others. Currently
@@ -2029,18 +2141,17 @@ class ECGetResponseBucket(object):
2029
2141
  # recent metadata. We could alternatively choose to the *newest*
2030
2142
  # metadata headers for self.headers by selecting the source with
2031
2143
  # the latest X-Timestamp.
2032
- self.headers = getter.last_headers
2033
- elif (self.timestamp_str is not None and # ie, not bad_bucket
2034
- getter.last_headers.get('X-Object-Sysmeta-Ec-Etag') !=
2035
- 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'):
2036
2147
  # Fragments at the same timestamp with different etags are never
2037
- # expected. If somehow it happens then ignore those fragments
2038
- # to avoid mixing fragments that will not reconstruct otherwise
2039
- # an exception from pyeclib is almost certain. This strategy leaves
2040
- # 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.
2041
2152
  raise ValueError("ETag mismatch")
2042
2153
 
2043
- frag_index = getter.last_headers.get('X-Object-Sysmeta-Ec-Frag-Index')
2154
+ frag_index = headers.get('X-Object-Sysmeta-Ec-Frag-Index')
2044
2155
  frag_index = int(frag_index) if frag_index is not None else None
2045
2156
  self.gets[frag_index].append((getter, parts_iter))
2046
2157
 
@@ -2050,7 +2161,7 @@ class ECGetResponseBucket(object):
2050
2161
  associated with the same frag_index then only one is included.
2051
2162
 
2052
2163
  :return: a list of sources, each source being a tuple of form
2053
- (ResumingGetter, iter)
2164
+ (ECFragGetter, iter)
2054
2165
  """
2055
2166
  all_sources = []
2056
2167
  for frag_index, sources in self.gets.items():
@@ -2068,8 +2179,19 @@ class ECGetResponseBucket(object):
2068
2179
 
2069
2180
  @property
2070
2181
  def shortfall(self):
2071
- result = self.policy.ec_ndata - len(self.get_responses())
2072
- 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])
2073
2195
 
2074
2196
  @property
2075
2197
  def shortfall_with_alts(self):
@@ -2079,16 +2201,24 @@ class ECGetResponseBucket(object):
2079
2201
  result = self.policy.ec_ndata - (len(self.get_responses()) + len(alts))
2080
2202
  return max(result, 0)
2081
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
+
2082
2212
  def __str__(self):
2083
2213
  # return a string summarising bucket state, useful for debugging.
2084
2214
  return '<%s, %s, %s, %s(%s), %s>' \
2085
- % (self.timestamp_str, self.status, self._durable,
2215
+ % (self.timestamp.internal, self.status, self._durable,
2086
2216
  self.shortfall, self.shortfall_with_alts, len(self.gets))
2087
2217
 
2088
2218
 
2089
2219
  class ECGetResponseCollection(object):
2090
2220
  """
2091
- Manages all successful EC GET responses gathered by ResumingGetters.
2221
+ Manages all successful EC GET responses gathered by ECFragGetters.
2092
2222
 
2093
2223
  A response comprises a tuple of (<getter instance>, <parts iterator>). All
2094
2224
  responses having the same data timestamp are placed in an
@@ -2104,33 +2234,61 @@ class ECGetResponseCollection(object):
2104
2234
  """
2105
2235
  self.policy = policy
2106
2236
  self.buckets = {}
2237
+ self.default_bad_bucket = ECGetResponseBucket(self.policy, None)
2238
+ self.bad_buckets = {}
2107
2239
  self.node_iter_count = 0
2108
2240
 
2109
- def _get_bucket(self, timestamp_str):
2241
+ def _get_bucket(self, timestamp):
2110
2242
  """
2111
- :param timestamp_str: a string representation of a timestamp
2243
+ :param timestamp: a Timestamp
2112
2244
  :return: ECGetResponseBucket for given timestamp
2113
2245
  """
2114
2246
  return self.buckets.setdefault(
2115
- 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))
2116
2256
 
2117
2257
  def add_response(self, get, parts_iter):
2118
2258
  """
2119
2259
  Add a response to the collection.
2120
2260
 
2121
2261
  :param get: An instance of
2122
- :class:`~swift.proxy.controllers.base.ResumingGetter`
2262
+ :class:`~swift.proxy.controllers.obj.ECFragGetter`
2123
2263
  :param parts_iter: An iterator over response body parts
2124
2264
  :raises ValueError: if the response etag or status code values do not
2125
2265
  match any values previously received for the same timestamp
2126
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):
2127
2277
  headers = get.last_headers
2128
2278
  # Add the response to the appropriate bucket keyed by data file
2129
2279
  # timestamp. Fall back to using X-Backend-Timestamp as key for object
2130
2280
  # servers that have not been upgraded.
2131
2281
  t_data_file = headers.get('X-Backend-Data-Timestamp')
2132
2282
  t_obj = headers.get('X-Backend-Timestamp', headers.get('X-Timestamp'))
2133
- 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)
2134
2292
 
2135
2293
  # The node may also have alternate fragments indexes (possibly at
2136
2294
  # different timestamps). For each list of alternate fragments indexes,
@@ -2138,7 +2296,9 @@ class ECGetResponseCollection(object):
2138
2296
  # list to that bucket's alternate nodes.
2139
2297
  frag_sets = safe_json_loads(headers.get('X-Backend-Fragments')) or {}
2140
2298
  for t_frag, frag_set in frag_sets.items():
2141
- 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)
2142
2302
  # If the response includes a durable timestamp then mark that bucket as
2143
2303
  # durable. Note that this may be a different bucket than the one this
2144
2304
  # response got added to, and that we may never go and get a durable
@@ -2149,7 +2309,7 @@ class ECGetResponseCollection(object):
2149
2309
  # obj server not upgraded so assume this response's frag is durable
2150
2310
  t_durable = t_obj
2151
2311
  if t_durable:
2152
- self._get_bucket(t_durable).set_durable()
2312
+ self._get_bucket(Timestamp(t_durable)).set_durable()
2153
2313
 
2154
2314
  def _sort_buckets(self):
2155
2315
  def key_fn(bucket):
@@ -2162,35 +2322,77 @@ class ECGetResponseCollection(object):
2162
2322
  return (bucket.durable,
2163
2323
  bucket.shortfall <= 0,
2164
2324
  -1 * bucket.shortfall_with_alts,
2165
- bucket.timestamp_str)
2325
+ bucket.timestamp)
2166
2326
 
2167
2327
  return sorted(self.buckets.values(), key=key_fn, reverse=True)
2168
2328
 
2169
2329
  @property
2170
2330
  def best_bucket(self):
2171
2331
  """
2172
- Return the best bucket in the collection.
2332
+ Return the "best" bucket in the collection.
2173
2333
 
2174
2334
  The "best" bucket is the newest timestamp with sufficient getters, or
2175
2335
  the closest to having sufficient getters, unless it is bettered by a
2176
2336
  bucket with potential alternate nodes.
2177
2337
 
2338
+ If there are no good buckets we return the "least_bad" bucket.
2339
+
2178
2340
  :return: An instance of :class:`~ECGetResponseBucket` or None if there
2179
2341
  are no buckets in the collection.
2180
2342
  """
2181
2343
  sorted_buckets = self._sort_buckets()
2182
- if sorted_buckets:
2183
- return sorted_buckets[0]
2184
- 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
2185
2387
 
2186
2388
  def _get_frag_prefs(self):
2187
2389
  # Construct the current frag_prefs list, with best_bucket prefs first.
2188
2390
  frag_prefs = []
2189
2391
 
2190
2392
  for bucket in self._sort_buckets():
2191
- if bucket.timestamp_str:
2393
+ if bucket.timestamp:
2192
2394
  exclusions = [fi for fi in bucket.gets if fi is not None]
2193
- prefs = {'timestamp': bucket.timestamp_str,
2395
+ prefs = {'timestamp': bucket.timestamp.internal,
2194
2396
  'exclude': exclusions}
2195
2397
  frag_prefs.append(prefs)
2196
2398
 
@@ -2249,22 +2451,292 @@ class ECGetResponseCollection(object):
2249
2451
  return nodes.pop(0).copy()
2250
2452
 
2251
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
+
2252
2723
  @ObjectControllerRouter.register(EC_POLICY)
2253
2724
  class ECObjectController(BaseObjectController):
2254
- def _fragment_GET_request(self, req, node_iter, partition, policy,
2255
- header_provider=None):
2725
+ def _fragment_GET_request(
2726
+ self, req, node_iter, partition, policy,
2727
+ header_provider, logger_thread_locals):
2256
2728
  """
2257
2729
  Makes a GET request for a fragment.
2258
2730
  """
2731
+ self.logger.thread_locals = logger_thread_locals
2259
2732
  backend_headers = self.generate_request_headers(
2260
2733
  req, additional=req.headers)
2261
2734
 
2262
- getter = ResumingGetter(self.app, req, 'Object', node_iter,
2263
- partition, req.swift_entity_path,
2264
- backend_headers,
2265
- client_chunk_size=policy.fragment_size,
2266
- newest=False, header_provider=header_provider)
2267
- 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()
2268
2740
 
2269
2741
  def _convert_range(self, req, policy):
2270
2742
  """
@@ -2318,6 +2790,27 @@ class ECObjectController(BaseObjectController):
2318
2790
  for s, e in new_ranges)
2319
2791
  return range_specs
2320
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
+
2321
2814
  def _get_or_head_response(self, req, node_iter, partition, policy):
2322
2815
  update_etag_is_at_header(req, "X-Object-Sysmeta-Ec-Etag")
2323
2816
 
@@ -2325,10 +2818,11 @@ class ECObjectController(BaseObjectController):
2325
2818
  # no fancy EC decoding here, just one plain old HEAD request to
2326
2819
  # one object server because all fragments hold all metadata
2327
2820
  # information about the object.
2328
- 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
2329
2823
  resp = self.GETorHEAD_base(
2330
- req, _('Object'), node_iter, partition,
2331
- req.swift_entity_path, concurrency)
2824
+ req, 'Object', node_iter, partition,
2825
+ req.swift_entity_path, concurrency, policy)
2332
2826
  self._fix_response(req, resp)
2333
2827
  return resp
2334
2828
 
@@ -2341,27 +2835,28 @@ class ECObjectController(BaseObjectController):
2341
2835
 
2342
2836
  safe_iter = GreenthreadSafeIterator(node_iter)
2343
2837
 
2344
- # Sending the request concurrently to all nodes, and responding
2345
- # with the first response isn't something useful for EC as all
2346
- # nodes contain different fragments. Also EC has implemented it's
2347
- # own specific implementation of concurrent gets to ec_ndata nodes.
2348
- # So we don't need to worry about plumbing and sending a
2349
- # concurrency value to ResumingGetter.
2350
- 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:
2351
2843
  pile = GreenAsyncPile(pool)
2352
2844
  buckets = ECGetResponseCollection(policy)
2353
2845
  node_iter.set_node_provider(buckets.provide_alternate_node)
2354
- # include what may well be an empty X-Backend-Fragment-Preferences
2355
- # header from the buckets.get_extra_headers to let the object
2356
- # server know that it is ok to return non-durable fragments
2357
- for _junk in range(policy.ec_ndata):
2846
+
2847
+ for node_count in range(ec_request_count):
2358
2848
  pile.spawn(self._fragment_GET_request,
2359
2849
  req, safe_iter, partition,
2360
- 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)
2361
2859
 
2362
- bad_bucket = ECGetResponseBucket(policy, None)
2363
- bad_bucket.set_durable()
2364
- best_bucket = None
2365
2860
  extra_requests = 0
2366
2861
  # max_extra_requests is an arbitrary hard limit for spawning extra
2367
2862
  # getters in case some unforeseen scenario, or a misbehaving object
@@ -2371,50 +2866,33 @@ class ECObjectController(BaseObjectController):
2371
2866
  # be limit at most 2 * replicas.
2372
2867
  max_extra_requests = (
2373
2868
  (policy.object_ring.replica_count * 2) - policy.ec_ndata)
2374
-
2375
2869
  for get, parts_iter in pile:
2376
- if get.last_status is None:
2377
- # We may have spawned getters that find the node iterator
2378
- # has been exhausted. Ignore them.
2379
- # TODO: turns out that node_iter.nodes_left can bottom
2380
- # out at >0 when number of devs in ring is < 2* replicas,
2381
- # which definitely happens in tests and results in status
2382
- # of None. We should fix that but keep this guard because
2383
- # there is also a race between testing nodes_left/spawning
2384
- # a getter and an existing getter calling next(node_iter).
2385
- continue
2386
2870
  try:
2387
- if is_success(get.last_status):
2388
- # 2xx responses are managed by a response collection
2389
- buckets.add_response(get, parts_iter)
2390
- else:
2391
- # all other responses are lumped into a single bucket
2392
- bad_bucket.add_response(get, parts_iter)
2871
+ buckets.add_response(get, parts_iter)
2393
2872
  except ValueError as err:
2394
- self.app.logger.error(
2395
- _("Problem with fragment response: %s"), err)
2396
- shortfall = bad_bucket.shortfall
2873
+ self.logger.error(
2874
+ "Problem with fragment response: %s", err)
2397
2875
  best_bucket = buckets.best_bucket
2398
- if best_bucket:
2399
- shortfall = best_bucket.shortfall
2400
- if not best_bucket.durable and shortfall <= 0:
2401
- # be willing to go a *little* deeper, slowly
2402
- shortfall = 1
2403
- shortfall = min(shortfall, bad_bucket.shortfall)
2404
- if (extra_requests < max_extra_requests and
2405
- shortfall > pile._pending and
2406
- (node_iter.nodes_left > 0 or
2407
- buckets.has_alternate_node())):
2408
- # we need more matching responses to reach ec_ndata
2409
- # than we have pending gets, as long as we still have
2410
- # 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)):
2411
2884
  extra_requests += 1
2412
- pile.spawn(self._fragment_GET_request, req,
2413
- safe_iter, partition, policy,
2414
- buckets.get_extra_headers)
2415
-
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)
2416
2893
  req.range = orig_range
2417
- 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:
2418
2896
  # headers can come from any of the getters
2419
2897
  resp_headers = best_bucket.headers
2420
2898
  resp_headers.pop('Content-Range', None)
@@ -2427,15 +2905,15 @@ class ECObjectController(BaseObjectController):
2427
2905
  app_iter = ECAppIter(
2428
2906
  req.swift_entity_path,
2429
2907
  policy,
2430
- [parts_iter for
2431
- _getter, parts_iter in best_bucket.get_responses()],
2908
+ [p_iter for _getter, p_iter in best_bucket.get_responses()],
2432
2909
  range_specs, fa_length, obj_length,
2433
- self.app.logger)
2910
+ self.logger)
2434
2911
  resp = Response(
2435
2912
  request=req,
2436
2913
  conditional_response=True,
2437
2914
  app_iter=app_iter)
2438
2915
  update_headers(resp, resp_headers)
2916
+ self._fix_ranges(req, resp)
2439
2917
  try:
2440
2918
  app_iter.kickoff(req, resp)
2441
2919
  except HTTPException as err_resp:
@@ -2453,25 +2931,40 @@ class ECObjectController(BaseObjectController):
2453
2931
  reasons = []
2454
2932
  bodies = []
2455
2933
  headers = []
2456
- for getter, _parts_iter in bad_bucket.get_responses():
2457
- if best_bucket and best_bucket.durable:
2458
- bad_resp_headers = HeaderKeyDict(getter.last_headers)
2459
- t_data_file = bad_resp_headers.get(
2460
- 'X-Backend-Data-Timestamp')
2461
- t_obj = bad_resp_headers.get(
2462
- 'X-Backend-Timestamp',
2463
- bad_resp_headers.get('X-Timestamp'))
2464
- bad_ts = Timestamp(t_data_file or t_obj or '0')
2465
- if bad_ts <= Timestamp(best_bucket.timestamp_str):
2466
- # We have reason to believe there's still good data
2467
- # out there, it's just currently unavailable
2468
- continue
2469
- statuses.extend(getter.statuses)
2470
- reasons.extend(getter.reasons)
2471
- bodies.extend(getter.bodies)
2472
- headers.extend(getter.source_headers)
2473
-
2474
- 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:
2475
2968
  # pretend that non-durable bucket was 404s
2476
2969
  statuses.append(404)
2477
2970
  reasons.append('404 Not Found')
@@ -2482,6 +2975,10 @@ class ECObjectController(BaseObjectController):
2482
2975
  req, statuses, reasons, bodies, 'Object',
2483
2976
  headers=headers)
2484
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
2485
2982
  return resp
2486
2983
 
2487
2984
  def _fix_response(self, req, resp):
@@ -2503,13 +3000,36 @@ class ECObjectController(BaseObjectController):
2503
3000
  if resp.status_int == HTTP_REQUESTED_RANGE_NOT_SATISFIABLE:
2504
3001
  resp.headers['Content-Range'] = 'bytes */%s' % resp.headers[
2505
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 = []
2506
3024
 
2507
3025
  def _make_putter(self, node, part, req, headers):
2508
3026
  return MIMEPutter.connect(
2509
- node, part, req.swift_entity_path, headers,
3027
+ node, part, req.swift_entity_path, headers, self.app.watchdog,
2510
3028
  conn_timeout=self.app.conn_timeout,
2511
3029
  node_timeout=self.app.node_timeout,
2512
- logger=self.app.logger,
3030
+ write_timeout=self.app.node_timeout,
3031
+ send_exception_handler=self.app.exception_occurred,
3032
+ logger=self.logger,
2513
3033
  need_multiphase=True)
2514
3034
 
2515
3035
  def _determine_chunk_destinations(self, putters, policy):
@@ -2586,7 +3106,8 @@ class ECObjectController(BaseObjectController):
2586
3106
  bytes_transferred = 0
2587
3107
  chunk_transform = chunk_transformer(policy)
2588
3108
  chunk_transform.send(None)
2589
- frag_hashers = collections.defaultdict(md5)
3109
+ frag_hashers = collections.defaultdict(
3110
+ lambda: md5(usedforsecurity=False))
2590
3111
 
2591
3112
  def send_chunk(chunk):
2592
3113
  # Note: there's two different hashers in here. etag_hasher is
@@ -2605,6 +3126,7 @@ class ECObjectController(BaseObjectController):
2605
3126
  return
2606
3127
 
2607
3128
  updated_frag_indexes = set()
3129
+ timeout_at = time.time() + self.app.node_timeout
2608
3130
  for putter in list(putters):
2609
3131
  frag_index = putter_to_frag_index[putter]
2610
3132
  backend_chunk = backend_chunks[frag_index]
@@ -2615,136 +3137,126 @@ class ECObjectController(BaseObjectController):
2615
3137
  if frag_index not in updated_frag_indexes:
2616
3138
  frag_hashers[frag_index].update(backend_chunk)
2617
3139
  updated_frag_indexes.add(frag_index)
2618
- putter.send_chunk(backend_chunk)
3140
+ putter.send_chunk(backend_chunk, timeout_at=timeout_at)
2619
3141
  else:
2620
3142
  putter.close()
2621
3143
  putters.remove(putter)
2622
3144
  self._check_min_conn(
2623
3145
  req, putters, min_conns,
2624
- msg=_('Object PUT exceptions during send, '
2625
- '%(conns)s/%(nodes)s required connections'))
3146
+ msg='Object PUT exceptions during send, '
3147
+ '%(conns)s/%(nodes)s required connections')
2626
3148
 
2627
3149
  try:
2628
- 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)
2629
3155
 
2630
- # build our putter_to_frag_index dict to place handoffs in the
2631
- # same part nodes index as the primaries they are covering
2632
- putter_to_frag_index = self._determine_chunk_destinations(
2633
- putters, policy)
2634
-
2635
- for putter in putters:
2636
- putter.spawn_sender_greenthread(
2637
- pool, self.app.put_queue_depth, self.app.node_timeout,
2638
- self.app.exception_occurred)
2639
- while True:
2640
- with ChunkReadTimeout(self.app.client_timeout):
2641
- try:
2642
- chunk = next(data_source)
2643
- except StopIteration:
2644
- break
2645
- bytes_transferred += len(chunk)
2646
- if bytes_transferred > constraints.MAX_FILE_SIZE:
2647
- raise HTTPRequestEntityTooLarge(request=req)
2648
-
2649
- send_chunk(chunk)
2650
-
2651
- ml = req.message_length()
2652
- if ml and bytes_transferred < ml:
2653
- req.client_disconnect = True
2654
- self.app.logger.warning(
2655
- _('Client disconnected without sending enough data'))
2656
- self.app.logger.increment('client_disconnects')
2657
- raise HTTPClientDisconnect(request=req)
2658
-
2659
- send_chunk(b'') # flush out any buffered data
2660
-
2661
- computed_etag = (etag_hasher.hexdigest()
2662
- if etag_hasher else None)
2663
- footers = self._get_footers(req)
2664
- received_etag = footers.get('etag', req.headers.get(
2665
- 'etag', '')).strip('"')
2666
- if (computed_etag and received_etag and
2667
- computed_etag != received_etag):
2668
- raise HTTPUnprocessableEntity(request=req)
2669
-
2670
- # Remove any EC reserved metadata names from footers
2671
- footers = {(k, v) for k, v in footers.items()
2672
- if not k.lower().startswith('x-object-sysmeta-ec-')}
2673
- for putter in putters:
2674
- frag_index = putter_to_frag_index[putter]
2675
- # Update any footers set by middleware with EC footers
2676
- trail_md = trailing_metadata(
2677
- policy, etag_hasher,
2678
- bytes_transferred, frag_index)
2679
- trail_md.update(footers)
2680
- # Etag footer must always be hash of what we sent
2681
- trail_md['Etag'] = frag_hashers[frag_index].hexdigest()
2682
- putter.end_of_object_data(footer_metadata=trail_md)
2683
-
2684
- for putter in putters:
2685
- putter.wait()
2686
-
2687
- # for storage policies requiring 2-phase commit (e.g.
2688
- # erasure coding), enforce >= 'quorum' number of
2689
- # 100-continue responses - this indicates successful
2690
- # object data and metadata commit and is a necessary
2691
- # condition to be met before starting 2nd PUT phase
2692
- final_phase = False
2693
- statuses, reasons, bodies, _junk = \
2694
- self._get_put_responses(
2695
- req, putters, len(nodes), final_phase=final_phase,
2696
- min_responses=min_conns)
2697
- if not self.have_quorum(
2698
- statuses, len(nodes), quorum=min_conns):
2699
- self.app.logger.error(
2700
- _('Not enough object servers ack\'ed (got %d)'),
2701
- 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.
2702
3230
  raise HTTPServiceUnavailable(request=req)
3231
+ else:
3232
+ # Other errors should use raw best_response
3233
+ raise resp
2703
3234
 
2704
- elif not self._have_adequate_informational(
2705
- statuses, min_conns):
2706
- resp = self.best_response(req, statuses, reasons, bodies,
2707
- _('Object PUT'),
2708
- quorum_size=min_conns)
2709
- if is_client_error(resp.status_int):
2710
- # if 4xx occurred in this state it is absolutely
2711
- # a bad conversation between proxy-server and
2712
- # object-server (even if it's
2713
- # HTTP_UNPROCESSABLE_ENTITY) so we should regard this
2714
- # as HTTPServiceUnavailable.
2715
- raise HTTPServiceUnavailable(request=req)
2716
- else:
2717
- # Other errors should use raw best_response
2718
- raise resp
2719
-
2720
- # quorum achieved, start 2nd phase - send commit
2721
- # confirmation to participating object servers
2722
- # so they write a .durable state file indicating
2723
- # a successful PUT
2724
- for putter in putters:
2725
- putter.send_commit_confirmation()
2726
- for putter in putters:
2727
- 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()
2728
3241
  except ChunkReadTimeout as err:
2729
- self.app.logger.warning(
2730
- _('ERROR Client read timeout (%ss)'), err.seconds)
2731
- 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')
2732
3245
  raise HTTPRequestTimeout(request=req)
2733
3246
  except ChunkReadError:
2734
- req.client_disconnect = True
2735
- self.app.logger.warning(
2736
- _('Client disconnected without sending last chunk'))
2737
- self.app.logger.increment('client_disconnects')
3247
+ self.logger.warning(
3248
+ 'Client disconnected without sending last chunk')
3249
+ self.logger.increment('object.client_disconnects')
2738
3250
  raise HTTPClientDisconnect(request=req)
2739
3251
  except HTTPException:
2740
3252
  raise
2741
3253
  except Timeout:
2742
- self.app.logger.exception(
2743
- _('ERROR Exception causing client disconnect'))
3254
+ self.logger.exception(
3255
+ 'ERROR Exception causing client disconnect')
2744
3256
  raise HTTPClientDisconnect(request=req)
2745
3257
  except Exception:
2746
- self.app.logger.exception(
2747
- _('ERROR Exception transferring data to object servers %s'),
3258
+ self.logger.exception(
3259
+ 'ERROR Exception transferring data to object servers %s',
2748
3260
  {'path': req.path})
2749
3261
  raise HTTPInternalServerError(request=req)
2750
3262
 
@@ -2829,7 +3341,7 @@ class ECObjectController(BaseObjectController):
2829
3341
  # the same as the request body sent proxy -> object, we
2830
3342
  # can't rely on the object-server to do the etag checking -
2831
3343
  # so we have to do it here.
2832
- etag_hasher = md5()
3344
+ etag_hasher = md5(usedforsecurity=False)
2833
3345
 
2834
3346
  min_conns = policy.quorum
2835
3347
  putters = self._get_put_connections(
@@ -2864,8 +3376,7 @@ class ECObjectController(BaseObjectController):
2864
3376
 
2865
3377
  etag = etag_hasher.hexdigest()
2866
3378
  resp = self.best_response(req, statuses, reasons, bodies,
2867
- _('Object PUT'), etag=etag,
3379
+ 'Object PUT', etag=etag,
2868
3380
  quorum_size=min_conns)
2869
- resp.last_modified = math.ceil(
2870
- float(Timestamp(req.headers['X-Timestamp'])))
3381
+ resp.last_modified = Timestamp(req.headers['X-Timestamp']).ceil()
2871
3382
  return resp