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