swift 2.23.3__py3-none-any.whl → 2.35.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- swift/__init__.py +29 -50
- swift/account/auditor.py +21 -118
- swift/account/backend.py +33 -28
- swift/account/reaper.py +37 -28
- swift/account/replicator.py +22 -0
- swift/account/server.py +60 -26
- swift/account/utils.py +28 -11
- swift-2.23.3.data/scripts/swift-account-audit → swift/cli/account_audit.py +23 -13
- swift-2.23.3.data/scripts/swift-config → swift/cli/config.py +2 -2
- swift/cli/container_deleter.py +5 -11
- swift-2.23.3.data/scripts/swift-dispersion-populate → swift/cli/dispersion_populate.py +8 -7
- swift/cli/dispersion_report.py +10 -9
- swift-2.23.3.data/scripts/swift-drive-audit → swift/cli/drive_audit.py +63 -21
- swift/cli/form_signature.py +3 -7
- swift-2.23.3.data/scripts/swift-get-nodes → swift/cli/get_nodes.py +8 -2
- swift/cli/info.py +154 -14
- swift/cli/manage_shard_ranges.py +705 -37
- swift-2.23.3.data/scripts/swift-oldies → swift/cli/oldies.py +25 -14
- swift-2.23.3.data/scripts/swift-orphans → swift/cli/orphans.py +7 -3
- swift/cli/recon.py +196 -67
- swift-2.23.3.data/scripts/swift-recon-cron → swift/cli/recon_cron.py +17 -20
- swift-2.23.3.data/scripts/swift-reconciler-enqueue → swift/cli/reconciler_enqueue.py +2 -3
- swift/cli/relinker.py +807 -126
- swift/cli/reload.py +135 -0
- swift/cli/ringbuilder.py +217 -20
- swift/cli/ringcomposer.py +0 -1
- swift/cli/shard-info.py +4 -3
- swift/common/base_storage_server.py +9 -20
- swift/common/bufferedhttp.py +48 -74
- swift/common/constraints.py +20 -15
- swift/common/container_sync_realms.py +9 -11
- swift/common/daemon.py +25 -8
- swift/common/db.py +195 -128
- swift/common/db_auditor.py +168 -0
- swift/common/db_replicator.py +95 -55
- swift/common/digest.py +141 -0
- swift/common/direct_client.py +144 -33
- swift/common/error_limiter.py +93 -0
- swift/common/exceptions.py +25 -1
- swift/common/header_key_dict.py +2 -9
- swift/common/http_protocol.py +373 -0
- swift/common/internal_client.py +129 -59
- swift/common/linkat.py +3 -4
- swift/common/manager.py +284 -67
- swift/common/memcached.py +390 -145
- swift/common/middleware/__init__.py +4 -0
- swift/common/middleware/account_quotas.py +211 -46
- swift/common/middleware/acl.py +3 -8
- swift/common/middleware/backend_ratelimit.py +230 -0
- swift/common/middleware/bulk.py +22 -34
- swift/common/middleware/catch_errors.py +1 -3
- swift/common/middleware/cname_lookup.py +6 -11
- swift/common/middleware/container_quotas.py +1 -1
- swift/common/middleware/container_sync.py +39 -17
- swift/common/middleware/copy.py +12 -0
- swift/common/middleware/crossdomain.py +22 -9
- swift/common/middleware/crypto/__init__.py +2 -1
- swift/common/middleware/crypto/crypto_utils.py +11 -15
- swift/common/middleware/crypto/decrypter.py +28 -11
- swift/common/middleware/crypto/encrypter.py +12 -17
- swift/common/middleware/crypto/keymaster.py +8 -15
- swift/common/middleware/crypto/kms_keymaster.py +2 -1
- swift/common/middleware/dlo.py +15 -11
- swift/common/middleware/domain_remap.py +5 -4
- swift/common/middleware/etag_quoter.py +128 -0
- swift/common/middleware/formpost.py +73 -70
- swift/common/middleware/gatekeeper.py +8 -1
- swift/common/middleware/keystoneauth.py +33 -3
- swift/common/middleware/list_endpoints.py +4 -4
- swift/common/middleware/listing_formats.py +85 -49
- swift/common/middleware/memcache.py +4 -95
- swift/common/middleware/name_check.py +3 -2
- swift/common/middleware/proxy_logging.py +160 -92
- swift/common/middleware/ratelimit.py +17 -10
- swift/common/middleware/read_only.py +6 -4
- swift/common/middleware/recon.py +59 -22
- swift/common/middleware/s3api/acl_handlers.py +25 -3
- swift/common/middleware/s3api/acl_utils.py +6 -1
- swift/common/middleware/s3api/controllers/__init__.py +6 -0
- swift/common/middleware/s3api/controllers/acl.py +3 -2
- swift/common/middleware/s3api/controllers/bucket.py +242 -137
- swift/common/middleware/s3api/controllers/logging.py +2 -2
- swift/common/middleware/s3api/controllers/multi_delete.py +43 -20
- swift/common/middleware/s3api/controllers/multi_upload.py +219 -133
- swift/common/middleware/s3api/controllers/obj.py +112 -8
- swift/common/middleware/s3api/controllers/object_lock.py +44 -0
- swift/common/middleware/s3api/controllers/s3_acl.py +2 -2
- swift/common/middleware/s3api/controllers/tagging.py +57 -0
- swift/common/middleware/s3api/controllers/versioning.py +36 -7
- swift/common/middleware/s3api/etree.py +22 -9
- swift/common/middleware/s3api/exception.py +0 -4
- swift/common/middleware/s3api/s3api.py +113 -41
- swift/common/middleware/s3api/s3request.py +384 -218
- swift/common/middleware/s3api/s3response.py +126 -23
- swift/common/middleware/s3api/s3token.py +16 -17
- swift/common/middleware/s3api/schema/delete.rng +1 -1
- swift/common/middleware/s3api/subresource.py +7 -10
- swift/common/middleware/s3api/utils.py +27 -10
- swift/common/middleware/slo.py +665 -358
- swift/common/middleware/staticweb.py +64 -37
- swift/common/middleware/symlink.py +51 -18
- swift/common/middleware/tempauth.py +76 -58
- swift/common/middleware/tempurl.py +191 -173
- swift/common/middleware/versioned_writes/__init__.py +51 -0
- swift/common/middleware/{versioned_writes.py → versioned_writes/legacy.py} +27 -26
- swift/common/middleware/versioned_writes/object_versioning.py +1482 -0
- swift/common/middleware/x_profile/exceptions.py +1 -4
- swift/common/middleware/x_profile/html_viewer.py +18 -19
- swift/common/middleware/x_profile/profile_model.py +1 -2
- swift/common/middleware/xprofile.py +10 -10
- swift-2.23.3.data/scripts/swift-container-server → swift/common/recon.py +13 -8
- swift/common/registry.py +147 -0
- swift/common/request_helpers.py +324 -57
- swift/common/ring/builder.py +67 -25
- swift/common/ring/composite_builder.py +1 -1
- swift/common/ring/ring.py +177 -51
- swift/common/ring/utils.py +1 -1
- swift/common/splice.py +10 -6
- swift/common/statsd_client.py +205 -0
- swift/common/storage_policy.py +49 -44
- swift/common/swob.py +86 -102
- swift/common/{utils.py → utils/__init__.py} +2163 -2772
- swift/common/utils/base.py +131 -0
- swift/common/utils/config.py +433 -0
- swift/common/utils/ipaddrs.py +256 -0
- swift/common/utils/libc.py +345 -0
- swift/common/utils/logs.py +859 -0
- swift/common/utils/timestamp.py +412 -0
- swift/common/wsgi.py +553 -535
- swift/container/auditor.py +14 -100
- swift/container/backend.py +490 -231
- swift/container/reconciler.py +126 -37
- swift/container/replicator.py +96 -22
- swift/container/server.py +358 -165
- swift/container/sharder.py +1540 -684
- swift/container/sync.py +94 -88
- swift/container/updater.py +53 -32
- swift/obj/auditor.py +153 -35
- swift/obj/diskfile.py +466 -217
- swift/obj/expirer.py +406 -124
- swift/obj/mem_diskfile.py +7 -4
- swift/obj/mem_server.py +1 -0
- swift/obj/reconstructor.py +523 -262
- swift/obj/replicator.py +249 -188
- swift/obj/server.py +207 -122
- swift/obj/ssync_receiver.py +145 -85
- swift/obj/ssync_sender.py +113 -54
- swift/obj/updater.py +652 -139
- swift/obj/watchers/__init__.py +0 -0
- swift/obj/watchers/dark_data.py +213 -0
- swift/proxy/controllers/account.py +11 -11
- swift/proxy/controllers/base.py +848 -604
- swift/proxy/controllers/container.py +433 -92
- swift/proxy/controllers/info.py +3 -2
- swift/proxy/controllers/obj.py +1000 -489
- swift/proxy/server.py +185 -112
- {swift-2.23.3.dist-info → swift-2.35.0.dist-info}/AUTHORS +58 -11
- {swift-2.23.3.dist-info → swift-2.35.0.dist-info}/METADATA +51 -56
- swift-2.35.0.dist-info/RECORD +201 -0
- {swift-2.23.3.dist-info → swift-2.35.0.dist-info}/WHEEL +1 -1
- {swift-2.23.3.dist-info → swift-2.35.0.dist-info}/entry_points.txt +43 -0
- swift-2.35.0.dist-info/pbr.json +1 -0
- swift/locale/de/LC_MESSAGES/swift.po +0 -1216
- swift/locale/en_GB/LC_MESSAGES/swift.po +0 -1207
- swift/locale/es/LC_MESSAGES/swift.po +0 -1085
- swift/locale/fr/LC_MESSAGES/swift.po +0 -909
- swift/locale/it/LC_MESSAGES/swift.po +0 -894
- swift/locale/ja/LC_MESSAGES/swift.po +0 -965
- swift/locale/ko_KR/LC_MESSAGES/swift.po +0 -964
- swift/locale/pt_BR/LC_MESSAGES/swift.po +0 -881
- swift/locale/ru/LC_MESSAGES/swift.po +0 -891
- swift/locale/tr_TR/LC_MESSAGES/swift.po +0 -832
- swift/locale/zh_CN/LC_MESSAGES/swift.po +0 -833
- swift/locale/zh_TW/LC_MESSAGES/swift.po +0 -838
- swift-2.23.3.data/scripts/swift-account-auditor +0 -23
- swift-2.23.3.data/scripts/swift-account-info +0 -51
- swift-2.23.3.data/scripts/swift-account-reaper +0 -23
- swift-2.23.3.data/scripts/swift-account-replicator +0 -34
- swift-2.23.3.data/scripts/swift-account-server +0 -23
- swift-2.23.3.data/scripts/swift-container-auditor +0 -23
- swift-2.23.3.data/scripts/swift-container-info +0 -55
- swift-2.23.3.data/scripts/swift-container-reconciler +0 -21
- swift-2.23.3.data/scripts/swift-container-replicator +0 -34
- swift-2.23.3.data/scripts/swift-container-sharder +0 -37
- swift-2.23.3.data/scripts/swift-container-sync +0 -23
- swift-2.23.3.data/scripts/swift-container-updater +0 -23
- swift-2.23.3.data/scripts/swift-dispersion-report +0 -24
- swift-2.23.3.data/scripts/swift-form-signature +0 -20
- swift-2.23.3.data/scripts/swift-init +0 -119
- swift-2.23.3.data/scripts/swift-object-auditor +0 -29
- swift-2.23.3.data/scripts/swift-object-expirer +0 -33
- swift-2.23.3.data/scripts/swift-object-info +0 -60
- swift-2.23.3.data/scripts/swift-object-reconstructor +0 -33
- swift-2.23.3.data/scripts/swift-object-relinker +0 -41
- swift-2.23.3.data/scripts/swift-object-replicator +0 -37
- swift-2.23.3.data/scripts/swift-object-server +0 -27
- swift-2.23.3.data/scripts/swift-object-updater +0 -23
- swift-2.23.3.data/scripts/swift-proxy-server +0 -23
- swift-2.23.3.data/scripts/swift-recon +0 -24
- swift-2.23.3.data/scripts/swift-ring-builder +0 -24
- swift-2.23.3.data/scripts/swift-ring-builder-analyzer +0 -22
- swift-2.23.3.data/scripts/swift-ring-composer +0 -22
- swift-2.23.3.dist-info/RECORD +0 -220
- swift-2.23.3.dist-info/pbr.json +0 -1
- {swift-2.23.3.dist-info → swift-2.35.0.dist-info}/LICENSE +0 -0
- {swift-2.23.3.dist-info → swift-2.35.0.dist-info}/top_level.txt +0 -0
swift/proxy/controllers/obj.py
CHANGED
@@ -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
|
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
|
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,
|
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,
|
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
|
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
|
228
|
-
|
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 =
|
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
|
-
|
386
|
+
update_shard_ns = self._get_update_shard(
|
275
387
|
req, self.account_name, self.container_name, self.object_name)
|
276
|
-
if
|
388
|
+
if update_shard_ns:
|
277
389
|
partition, nodes = self.app.container_ring.get_nodes(
|
278
|
-
|
279
|
-
return partition, nodes,
|
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
|
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.
|
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),
|
321
|
-
|
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
|
-
|
327
|
-
|
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,
|
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' %
|
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
|
-
|
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' %
|
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.
|
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,
|
419
|
-
|
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.
|
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
|
475
|
-
|
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')
|
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
|
-
|
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
|
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 =
|
527
|
-
|
528
|
-
|
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.
|
578
|
-
|
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.
|
591
|
-
|
592
|
-
|
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.
|
715
|
+
self.logger to retain transaction
|
629
716
|
logging information.
|
630
717
|
:return: an instance of a Putter
|
631
718
|
"""
|
632
|
-
self.
|
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,
|
726
|
+
self.app.error_limit(node, 'ERROR Insufficient Storage')
|
640
727
|
except PutterConnectError as e:
|
641
|
-
|
642
|
-
|
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,
|
648
|
-
|
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,
|
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.
|
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
|
676
|
-
|
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.
|
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
|
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
|
-
|
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),
|
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
|
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
|
-
|
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,
|
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,
|
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
|
-
|
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
|
-
|
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=
|
917
|
-
|
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
|
-
|
922
|
-
|
923
|
-
|
924
|
-
|
925
|
-
|
926
|
-
|
927
|
-
|
928
|
-
|
929
|
-
|
930
|
-
|
931
|
-
|
932
|
-
|
933
|
-
|
934
|
-
|
935
|
-
|
936
|
-
|
937
|
-
|
938
|
-
|
939
|
-
|
940
|
-
|
941
|
-
|
942
|
-
|
943
|
-
|
944
|
-
|
945
|
-
|
946
|
-
|
947
|
-
|
948
|
-
|
949
|
-
|
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.
|
959
|
-
|
960
|
-
self.
|
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
|
-
|
966
|
-
|
967
|
-
|
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.
|
972
|
-
|
1049
|
+
self.logger.exception(
|
1050
|
+
'ERROR Exception causing client disconnect')
|
973
1051
|
raise HTTPClientDisconnect(request=req)
|
974
1052
|
except Exception:
|
975
|
-
self.
|
976
|
-
|
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.
|
1020
|
-
|
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
|
-
|
1025
|
-
resp.last_modified =
|
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
|
1073
|
-
#
|
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
|
1077
|
-
|
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
|
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
|
1412
|
-
self.logger.exception(
|
1413
|
-
|
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(
|
1416
|
-
|
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
|
-
|
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
|
-
|
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.
|
1445
|
-
|
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,
|
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.
|
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.
|
1772
|
+
self._send_chunk(b'')
|
1674
1773
|
self.state = DATA_SENT
|
1675
1774
|
|
1676
|
-
def
|
1677
|
-
|
1678
|
-
|
1679
|
-
|
1680
|
-
|
1681
|
-
|
1682
|
-
|
1683
|
-
|
1684
|
-
|
1685
|
-
|
1686
|
-
|
1687
|
-
|
1688
|
-
|
1689
|
-
|
1690
|
-
|
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(
|
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,
|
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,
|
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,
|
1764
|
-
logger, mime_boundary,
|
1765
|
-
|
1766
|
-
|
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.
|
1778
|
-
|
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(
|
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.
|
1908
|
+
self._send_chunk(b"".join(message_parts))
|
1812
1909
|
|
1813
|
-
self.
|
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.
|
1935
|
+
self._send_chunk(b"".join(message_parts))
|
1839
1936
|
|
1840
|
-
self.
|
1937
|
+
self._send_chunk(b'')
|
1841
1938
|
self.state = COMMIT_SENT
|
1842
1939
|
|
1843
1940
|
@classmethod
|
1844
|
-
def connect(cls, node, part,
|
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,
|
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,
|
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,
|
2098
|
+
def __init__(self, policy, timestamp):
|
2000
2099
|
"""
|
2001
2100
|
:param policy: an instance of ECStoragePolicy
|
2002
|
-
:param
|
2101
|
+
:param timestamp: a Timestamp, or None for a bucket of error responses
|
2003
2102
|
"""
|
2004
2103
|
self.policy = policy
|
2005
|
-
self.
|
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 =
|
2033
|
-
elif (
|
2034
|
-
|
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
|
2038
|
-
#
|
2039
|
-
#
|
2040
|
-
#
|
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 =
|
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
|
-
(
|
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
|
-
|
2072
|
-
|
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.
|
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
|
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,
|
2241
|
+
def _get_bucket(self, timestamp):
|
2110
2242
|
"""
|
2111
|
-
:param
|
2243
|
+
:param timestamp: a Timestamp
|
2112
2244
|
:return: ECGetResponseBucket for given timestamp
|
2113
2245
|
"""
|
2114
2246
|
return self.buckets.setdefault(
|
2115
|
-
|
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.
|
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
|
-
|
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
|
-
|
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.
|
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
|
-
|
2183
|
-
|
2184
|
-
|
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.
|
2393
|
+
if bucket.timestamp:
|
2192
2394
|
exclusions = [fi for fi in bucket.gets if fi is not None]
|
2193
|
-
prefs = {'timestamp': bucket.
|
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(
|
2255
|
-
|
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 =
|
2263
|
-
|
2264
|
-
|
2265
|
-
|
2266
|
-
|
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
|
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,
|
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
|
-
|
2345
|
-
|
2346
|
-
|
2347
|
-
|
2348
|
-
|
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
|
-
|
2355
|
-
|
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
|
-
|
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.
|
2395
|
-
|
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
|
-
|
2400
|
-
|
2401
|
-
|
2402
|
-
|
2403
|
-
|
2404
|
-
|
2405
|
-
|
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
|
-
|
2414
|
-
|
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
|
-
|
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
|
-
[
|
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.
|
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
|
-
|
2457
|
-
|
2458
|
-
|
2459
|
-
|
2460
|
-
|
2461
|
-
|
2462
|
-
|
2463
|
-
bad_resp_headers.
|
2464
|
-
|
2465
|
-
|
2466
|
-
|
2467
|
-
|
2468
|
-
|
2469
|
-
|
2470
|
-
|
2471
|
-
|
2472
|
-
|
2473
|
-
|
2474
|
-
|
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
|
-
|
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(
|
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=
|
2625
|
-
|
3146
|
+
msg='Object PUT exceptions during send, '
|
3147
|
+
'%(conns)s/%(nodes)s required connections')
|
2626
3148
|
|
2627
3149
|
try:
|
2628
|
-
|
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
|
-
|
2631
|
-
|
2632
|
-
|
2633
|
-
|
2634
|
-
|
2635
|
-
|
2636
|
-
|
2637
|
-
|
2638
|
-
|
2639
|
-
|
2640
|
-
|
2641
|
-
|
2642
|
-
|
2643
|
-
|
2644
|
-
|
2645
|
-
|
2646
|
-
|
2647
|
-
|
2648
|
-
|
2649
|
-
|
2650
|
-
|
2651
|
-
|
2652
|
-
|
2653
|
-
|
2654
|
-
|
2655
|
-
|
2656
|
-
|
2657
|
-
|
2658
|
-
|
2659
|
-
|
2660
|
-
|
2661
|
-
|
2662
|
-
|
2663
|
-
|
2664
|
-
|
2665
|
-
|
2666
|
-
|
2667
|
-
|
2668
|
-
|
2669
|
-
|
2670
|
-
|
2671
|
-
|
2672
|
-
|
2673
|
-
|
2674
|
-
|
2675
|
-
|
2676
|
-
|
2677
|
-
|
2678
|
-
|
2679
|
-
|
2680
|
-
|
2681
|
-
|
2682
|
-
|
2683
|
-
|
2684
|
-
|
2685
|
-
|
2686
|
-
|
2687
|
-
|
2688
|
-
|
2689
|
-
|
2690
|
-
|
2691
|
-
|
2692
|
-
|
2693
|
-
|
2694
|
-
|
2695
|
-
|
2696
|
-
|
2697
|
-
|
2698
|
-
|
2699
|
-
|
2700
|
-
|
2701
|
-
|
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
|
-
|
2705
|
-
|
2706
|
-
|
2707
|
-
|
2708
|
-
|
2709
|
-
|
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.
|
2730
|
-
|
2731
|
-
self.
|
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
|
-
|
2735
|
-
|
2736
|
-
|
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.
|
2743
|
-
|
3254
|
+
self.logger.exception(
|
3255
|
+
'ERROR Exception causing client disconnect')
|
2744
3256
|
raise HTTPClientDisconnect(request=req)
|
2745
3257
|
except Exception:
|
2746
|
-
self.
|
2747
|
-
|
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
|
-
|
3379
|
+
'Object PUT', etag=etag,
|
2868
3380
|
quorum_size=min_conns)
|
2869
|
-
resp.last_modified =
|
2870
|
-
float(Timestamp(req.headers['X-Timestamp'])))
|
3381
|
+
resp.last_modified = Timestamp(req.headers['X-Timestamp']).ceil()
|
2871
3382
|
return resp
|