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/obj/updater.py
CHANGED
@@ -12,14 +12,18 @@
|
|
12
12
|
# implied.
|
13
13
|
# See the License for the specific language governing permissions and
|
14
14
|
# limitations under the License.
|
15
|
+
import queue
|
15
16
|
|
16
|
-
import
|
17
|
+
import pickle # nosec: B403
|
18
|
+
import errno
|
17
19
|
import os
|
18
20
|
import signal
|
19
21
|
import sys
|
20
22
|
import time
|
21
|
-
|
22
|
-
from random import random
|
23
|
+
import uuid
|
24
|
+
from random import random, shuffle
|
25
|
+
from bisect import insort
|
26
|
+
from collections import deque
|
23
27
|
|
24
28
|
from eventlet import spawn, Timeout
|
25
29
|
|
@@ -29,31 +33,328 @@ from swift.common.exceptions import ConnectionTimeout
|
|
29
33
|
from swift.common.ring import Ring
|
30
34
|
from swift.common.utils import get_logger, renamer, write_pickle, \
|
31
35
|
dump_recon_cache, config_true_value, RateLimitedIterator, split_path, \
|
32
|
-
eventlet_monkey_patch, get_redirect_data, ContextPool
|
33
|
-
|
36
|
+
eventlet_monkey_patch, get_redirect_data, ContextPool, hash_path, \
|
37
|
+
non_negative_float, config_positive_int_value, non_negative_int, \
|
38
|
+
EventletRateLimiter, node_to_string, parse_options, load_recon_cache
|
39
|
+
from swift.common.daemon import Daemon, run_daemon
|
34
40
|
from swift.common.header_key_dict import HeaderKeyDict
|
35
41
|
from swift.common.storage_policy import split_policy_string, PolicyError
|
42
|
+
from swift.common.recon import RECON_OBJECT_FILE, DEFAULT_RECON_CACHE_PATH
|
36
43
|
from swift.obj.diskfile import get_tmp_dir, ASYNCDIR_BASE
|
37
44
|
from swift.common.http import is_success, HTTP_INTERNAL_SERVER_ERROR, \
|
38
45
|
HTTP_MOVED_PERMANENTLY
|
39
46
|
|
40
47
|
|
48
|
+
class RateLimiterBucket(EventletRateLimiter):
|
49
|
+
"""
|
50
|
+
Extends EventletRateLimiter to also maintain a deque of items that have
|
51
|
+
been deferred due to rate-limiting, and to provide a comparator for sorting
|
52
|
+
instanced by readiness.
|
53
|
+
"""
|
54
|
+
def __init__(self, max_updates_per_second):
|
55
|
+
super(RateLimiterBucket, self).__init__(max_updates_per_second,
|
56
|
+
rate_buffer=0)
|
57
|
+
self.deque = deque()
|
58
|
+
|
59
|
+
def __len__(self):
|
60
|
+
return len(self.deque)
|
61
|
+
|
62
|
+
def __bool__(self):
|
63
|
+
return bool(self.deque)
|
64
|
+
|
65
|
+
def __lt__(self, other):
|
66
|
+
# used to sort RateLimiterBuckets by readiness
|
67
|
+
if isinstance(other, RateLimiterBucket):
|
68
|
+
return self.running_time < other.running_time
|
69
|
+
return self.running_time < other
|
70
|
+
|
71
|
+
|
72
|
+
class BucketizedUpdateSkippingLimiter(object):
|
73
|
+
"""
|
74
|
+
Wrap an iterator to rate-limit updates on a per-bucket basis, where updates
|
75
|
+
are mapped to buckets by hashing their destination path. If an update is
|
76
|
+
rate-limited then it is placed on a deferral queue and may be sent later if
|
77
|
+
the wrapped iterator is exhausted before the ``drain_until`` time is
|
78
|
+
reached.
|
79
|
+
|
80
|
+
The deferral queue has constrained size and once the queue is full updates
|
81
|
+
are evicted using a first-in-first-out policy. This policy is used because
|
82
|
+
updates on the queue may have been made obsolete by newer updates written
|
83
|
+
to disk, and this is more likely for updates that have been on the queue
|
84
|
+
longest.
|
85
|
+
|
86
|
+
The iterator increments stats as follows:
|
87
|
+
|
88
|
+
* The `deferrals` stat is incremented for each update that is
|
89
|
+
rate-limited. Note that a individual update is rate-limited at most
|
90
|
+
once.
|
91
|
+
* The `skips` stat is incremented for each rate-limited update that is
|
92
|
+
not eventually yielded. This includes updates that are evicted from the
|
93
|
+
deferral queue and all updates that remain in the deferral queue when
|
94
|
+
``drain_until`` time is reached and the iterator terminates.
|
95
|
+
* The `drains` stat is incremented for each rate-limited update that is
|
96
|
+
eventually yielded.
|
97
|
+
|
98
|
+
Consequently, when this iterator terminates, the sum of `skips` and
|
99
|
+
`drains` is equal to the number of `deferrals`.
|
100
|
+
|
101
|
+
:param update_iterable: an async_pending update iterable
|
102
|
+
:param logger: a logger instance
|
103
|
+
:param stats: a SweepStats instance
|
104
|
+
:param num_buckets: number of buckets to divide container hashes into, the
|
105
|
+
more buckets total the less containers to a bucket
|
106
|
+
(once a busy container slows down a bucket the whole
|
107
|
+
bucket starts deferring)
|
108
|
+
:param max_elements_per_group_per_second: tunable, when deferring kicks in
|
109
|
+
:param max_deferred_elements: maximum number of deferred elements before
|
110
|
+
skipping starts. Each bucket may defer updates, but once the total
|
111
|
+
number of deferred updates summed across all buckets reaches this
|
112
|
+
value then all buckets will skip subsequent updates.
|
113
|
+
:param drain_until: time at which any remaining deferred elements must be
|
114
|
+
skipped and the iterator stops. Once the wrapped iterator has been
|
115
|
+
exhausted, this iterator will drain deferred elements from its buckets
|
116
|
+
until either all buckets have drained or this time is reached.
|
117
|
+
"""
|
118
|
+
|
119
|
+
def __init__(self, update_iterable, logger, stats, num_buckets=1000,
|
120
|
+
max_elements_per_group_per_second=50,
|
121
|
+
max_deferred_elements=0,
|
122
|
+
drain_until=0):
|
123
|
+
self.iterator = iter(update_iterable)
|
124
|
+
self.logger = logger
|
125
|
+
self.stats = stats
|
126
|
+
# if we want a smaller "blast radius" we could make this number bigger
|
127
|
+
self.num_buckets = max(num_buckets, 1)
|
128
|
+
self.max_deferred_elements = max_deferred_elements
|
129
|
+
self.deferred_buckets = deque()
|
130
|
+
self.drain_until = drain_until
|
131
|
+
self.salt = str(uuid.uuid4())
|
132
|
+
self.buckets = [RateLimiterBucket(max_elements_per_group_per_second)
|
133
|
+
for _ in range(self.num_buckets)]
|
134
|
+
self.buckets_ordered_by_readiness = None
|
135
|
+
|
136
|
+
def __iter__(self):
|
137
|
+
return self
|
138
|
+
|
139
|
+
def _bucket_key(self, update):
|
140
|
+
acct, cont = split_update_path(update)
|
141
|
+
return int(hash_path(acct, cont, self.salt), 16) % self.num_buckets
|
142
|
+
|
143
|
+
def _get_time(self):
|
144
|
+
return time.time()
|
145
|
+
|
146
|
+
def __next__(self):
|
147
|
+
# first iterate over the wrapped iterator...
|
148
|
+
for update_ctx in self.iterator:
|
149
|
+
bucket = self.buckets[self._bucket_key(update_ctx['update'])]
|
150
|
+
now = self._get_time()
|
151
|
+
if bucket.is_allowed(now=now):
|
152
|
+
# no need to ratelimit, just return next update
|
153
|
+
return update_ctx
|
154
|
+
|
155
|
+
self.stats.deferrals += 1
|
156
|
+
self.logger.increment("deferrals")
|
157
|
+
if self.max_deferred_elements > 0:
|
158
|
+
if len(self.deferred_buckets) >= self.max_deferred_elements:
|
159
|
+
# create space to defer this update by popping the least
|
160
|
+
# recent deferral from the least recently deferred bucket;
|
161
|
+
# updates read from disk recently are preferred over those
|
162
|
+
# read from disk less recently.
|
163
|
+
oldest_deferred_bucket = self.deferred_buckets.popleft()
|
164
|
+
oldest_deferred_bucket.deque.popleft()
|
165
|
+
self.stats.skips += 1
|
166
|
+
self.logger.increment("skips")
|
167
|
+
# append the update to the bucket's queue and append the bucket
|
168
|
+
# to the queue of deferred buckets
|
169
|
+
# note: buckets may have multiple entries in deferred_buckets,
|
170
|
+
# one for each deferred update in that particular bucket
|
171
|
+
bucket.deque.append(update_ctx)
|
172
|
+
self.deferred_buckets.append(bucket)
|
173
|
+
else:
|
174
|
+
self.stats.skips += 1
|
175
|
+
self.logger.increment("skips")
|
176
|
+
|
177
|
+
if self.buckets_ordered_by_readiness is None:
|
178
|
+
# initialise a queue of those buckets with deferred elements;
|
179
|
+
# buckets are queued in the chronological order in which they are
|
180
|
+
# ready to serve an element
|
181
|
+
self.buckets_ordered_by_readiness = queue.PriorityQueue()
|
182
|
+
for bucket in self.buckets:
|
183
|
+
if bucket:
|
184
|
+
self.buckets_ordered_by_readiness.put(bucket)
|
185
|
+
|
186
|
+
# now drain the buckets...
|
187
|
+
undrained_elements = []
|
188
|
+
while not self.buckets_ordered_by_readiness.empty():
|
189
|
+
now = self._get_time()
|
190
|
+
bucket = self.buckets_ordered_by_readiness.get_nowait()
|
191
|
+
if now < self.drain_until:
|
192
|
+
# wait for next element to be ready
|
193
|
+
bucket.wait(now=now)
|
194
|
+
# drain the most recently deferred element
|
195
|
+
item = bucket.deque.pop()
|
196
|
+
if bucket:
|
197
|
+
# bucket has more deferred elements, re-insert in queue in
|
198
|
+
# correct chronological position
|
199
|
+
self.buckets_ordered_by_readiness.put(bucket)
|
200
|
+
self.stats.drains += 1
|
201
|
+
self.logger.increment("drains")
|
202
|
+
return item
|
203
|
+
else:
|
204
|
+
# time to stop iterating: gather all un-drained elements
|
205
|
+
undrained_elements.extend(bucket.deque)
|
206
|
+
|
207
|
+
if undrained_elements:
|
208
|
+
# report final batch of skipped elements
|
209
|
+
self.stats.skips += len(undrained_elements)
|
210
|
+
self.logger.update_stats("skips", len(undrained_elements))
|
211
|
+
|
212
|
+
raise StopIteration()
|
213
|
+
|
214
|
+
|
215
|
+
class OldestAsyncPendingTracker:
|
216
|
+
"""
|
217
|
+
Manages the tracking of the oldest async pending updates for each
|
218
|
+
account-container pair using a sorted list for timestamps. Evicts the
|
219
|
+
newest pairs when t max_entries is reached. Supports retrieving the N
|
220
|
+
oldest async pending updates or calculating the age of the oldest pending
|
221
|
+
update.
|
222
|
+
"""
|
223
|
+
def __init__(
|
224
|
+
self,
|
225
|
+
max_entries,
|
226
|
+
):
|
227
|
+
self.max_entries = max_entries
|
228
|
+
self.sorted_entries = []
|
229
|
+
self.ac_to_timestamp = {}
|
230
|
+
|
231
|
+
def add_update(self, account, container, timestamp):
|
232
|
+
"""
|
233
|
+
Add or update a timestamp for a given account and container.
|
234
|
+
|
235
|
+
:param account: (str) The account name.
|
236
|
+
:param container: (str) The container name.
|
237
|
+
:param timestamp: (float) The timestamp to add or update.
|
238
|
+
"""
|
239
|
+
# Ensure the timestamp is a float
|
240
|
+
timestamp = float(timestamp)
|
241
|
+
|
242
|
+
ac = (account, container)
|
243
|
+
|
244
|
+
if ac in self.ac_to_timestamp:
|
245
|
+
old_timestamp = self.ac_to_timestamp[ac]
|
246
|
+
# Only replace the existing timestamp if the new one is older
|
247
|
+
if timestamp < old_timestamp:
|
248
|
+
# Remove the old (timestamp, ac) from the
|
249
|
+
# sorted list
|
250
|
+
self.sorted_entries.remove((old_timestamp, ac))
|
251
|
+
# Insert the new (timestamp, ac) in the sorted order
|
252
|
+
insort(self.sorted_entries, (timestamp, ac))
|
253
|
+
# Update the ac_to_timestamp dictionary
|
254
|
+
self.ac_to_timestamp[ac] = timestamp
|
255
|
+
else:
|
256
|
+
# Insert the new (timestamp, ac) in the sorted order
|
257
|
+
insort(self.sorted_entries, (timestamp, ac))
|
258
|
+
self.ac_to_timestamp[ac] = timestamp
|
259
|
+
|
260
|
+
# Check size and evict the newest ac(s) if necessary
|
261
|
+
if (len(self.ac_to_timestamp) > self.max_entries):
|
262
|
+
# Pop the newest entry (largest timestamp)
|
263
|
+
_, newest_ac = (self.sorted_entries.pop())
|
264
|
+
del self.ac_to_timestamp[newest_ac]
|
265
|
+
|
266
|
+
def get_n_oldest_timestamp_acs(self, n):
|
267
|
+
oldest_entries = self.sorted_entries[:n]
|
268
|
+
return {
|
269
|
+
'oldest_count': len(oldest_entries),
|
270
|
+
'oldest_entries': [
|
271
|
+
{
|
272
|
+
'timestamp': entry[0],
|
273
|
+
'account': entry[1][0],
|
274
|
+
'container': entry[1][1],
|
275
|
+
}
|
276
|
+
for entry in oldest_entries
|
277
|
+
],
|
278
|
+
}
|
279
|
+
|
280
|
+
def get_oldest_timestamp(self):
|
281
|
+
if self.sorted_entries:
|
282
|
+
return self.sorted_entries[0][0]
|
283
|
+
return None
|
284
|
+
|
285
|
+
def get_oldest_timestamp_age(self):
|
286
|
+
current_time = time.time()
|
287
|
+
oldest_timestamp = self.get_oldest_timestamp()
|
288
|
+
if oldest_timestamp is not None:
|
289
|
+
return current_time - oldest_timestamp
|
290
|
+
return None
|
291
|
+
|
292
|
+
def reset(self):
|
293
|
+
self.sorted_entries = []
|
294
|
+
self.ac_to_timestamp = {}
|
295
|
+
|
296
|
+
def get_memory_usage(self):
|
297
|
+
return self._get_size(self)
|
298
|
+
|
299
|
+
def _get_size(self, obj, seen=None):
|
300
|
+
if seen is None:
|
301
|
+
seen = set()
|
302
|
+
|
303
|
+
obj_id = id(obj)
|
304
|
+
if obj_id in seen:
|
305
|
+
return 0
|
306
|
+
seen.add(obj_id)
|
307
|
+
|
308
|
+
size = sys.getsizeof(obj)
|
309
|
+
|
310
|
+
if isinstance(obj, dict):
|
311
|
+
size += sum(
|
312
|
+
self._get_size(k, seen) + self._get_size(v, seen)
|
313
|
+
for k, v in obj.items()
|
314
|
+
)
|
315
|
+
elif hasattr(obj, '__dict__'):
|
316
|
+
size += self._get_size(obj.__dict__, seen)
|
317
|
+
elif (
|
318
|
+
hasattr(obj, '__iter__')
|
319
|
+
and not isinstance(obj, (str, bytes, bytearray))
|
320
|
+
):
|
321
|
+
size += sum(self._get_size(i, seen) for i in obj)
|
322
|
+
|
323
|
+
return size
|
324
|
+
|
325
|
+
|
41
326
|
class SweepStats(object):
|
42
327
|
"""
|
43
328
|
Stats bucket for an update sweep
|
329
|
+
|
330
|
+
A measure of the rate at which updates are being rate-limited is::
|
331
|
+
|
332
|
+
deferrals / (deferrals + successes + failures - drains)
|
333
|
+
|
334
|
+
A measure of the rate at which updates are not being sent during a sweep
|
335
|
+
is::
|
336
|
+
|
337
|
+
skips / (skips + successes + failures)
|
44
338
|
"""
|
45
339
|
def __init__(self, errors=0, failures=0, quarantines=0, successes=0,
|
46
|
-
unlinks=0, redirects=0
|
340
|
+
unlinks=0, outdated_unlinks=0, redirects=0, skips=0,
|
341
|
+
deferrals=0, drains=0):
|
47
342
|
self.errors = errors
|
48
343
|
self.failures = failures
|
49
344
|
self.quarantines = quarantines
|
50
345
|
self.successes = successes
|
51
346
|
self.unlinks = unlinks
|
347
|
+
self.outdated_unlinks = outdated_unlinks
|
52
348
|
self.redirects = redirects
|
349
|
+
self.skips = skips
|
350
|
+
self.deferrals = deferrals
|
351
|
+
self.drains = drains
|
53
352
|
|
54
353
|
def copy(self):
|
55
354
|
return type(self)(self.errors, self.failures, self.quarantines,
|
56
|
-
self.successes, self.unlinks
|
355
|
+
self.successes, self.unlinks, self.outdated_unlinks,
|
356
|
+
self.redirects, self.skips, self.deferrals,
|
357
|
+
self.drains)
|
57
358
|
|
58
359
|
def since(self, other):
|
59
360
|
return type(self)(self.errors - other.errors,
|
@@ -61,7 +362,11 @@ class SweepStats(object):
|
|
61
362
|
self.quarantines - other.quarantines,
|
62
363
|
self.successes - other.successes,
|
63
364
|
self.unlinks - other.unlinks,
|
64
|
-
self.
|
365
|
+
self.outdated_unlinks - other.outdated_unlinks,
|
366
|
+
self.redirects - other.redirects,
|
367
|
+
self.skips - other.skips,
|
368
|
+
self.deferrals - other.deferrals,
|
369
|
+
self.drains - other.drains)
|
65
370
|
|
66
371
|
def reset(self):
|
67
372
|
self.errors = 0
|
@@ -69,7 +374,11 @@ class SweepStats(object):
|
|
69
374
|
self.quarantines = 0
|
70
375
|
self.successes = 0
|
71
376
|
self.unlinks = 0
|
377
|
+
self.outdated_unlinks = 0
|
72
378
|
self.redirects = 0
|
379
|
+
self.skips = 0
|
380
|
+
self.deferrals = 0
|
381
|
+
self.drains = 0
|
73
382
|
|
74
383
|
def __str__(self):
|
75
384
|
keys = (
|
@@ -77,12 +386,31 @@ class SweepStats(object):
|
|
77
386
|
(self.failures, 'failures'),
|
78
387
|
(self.quarantines, 'quarantines'),
|
79
388
|
(self.unlinks, 'unlinks'),
|
389
|
+
(self.outdated_unlinks, 'outdated_unlinks'),
|
80
390
|
(self.errors, 'errors'),
|
81
391
|
(self.redirects, 'redirects'),
|
392
|
+
(self.skips, 'skips'),
|
393
|
+
(self.deferrals, 'deferrals'),
|
394
|
+
(self.drains, 'drains'),
|
82
395
|
)
|
83
396
|
return ', '.join('%d %s' % pair for pair in keys)
|
84
397
|
|
85
398
|
|
399
|
+
def split_update_path(update):
|
400
|
+
"""
|
401
|
+
Split the account and container parts out of the async update data.
|
402
|
+
|
403
|
+
N.B. updates to shards set the container_path key while the account and
|
404
|
+
container keys are always the root.
|
405
|
+
"""
|
406
|
+
container_path = update.get('container_path')
|
407
|
+
if container_path:
|
408
|
+
acct, cont = split_path('/' + container_path, minsegs=2)
|
409
|
+
else:
|
410
|
+
acct, cont = update['account'], update['container']
|
411
|
+
return acct, cont
|
412
|
+
|
413
|
+
|
86
414
|
class ObjectUpdater(Daemon):
|
87
415
|
"""Update object information in container listings."""
|
88
416
|
|
@@ -92,10 +420,11 @@ class ObjectUpdater(Daemon):
|
|
92
420
|
self.devices = conf.get('devices', '/srv/node')
|
93
421
|
self.mount_check = config_true_value(conf.get('mount_check', 'true'))
|
94
422
|
self.swift_dir = conf.get('swift_dir', '/etc/swift')
|
95
|
-
self.interval =
|
423
|
+
self.interval = float(conf.get('interval', 300))
|
96
424
|
self.container_ring = None
|
97
425
|
self.concurrency = int(conf.get('concurrency', 8))
|
98
|
-
self.updater_workers =
|
426
|
+
self.updater_workers = config_positive_int_value(
|
427
|
+
conf.get('updater_workers', 1))
|
99
428
|
if 'slowdown' in conf:
|
100
429
|
self.logger.warning(
|
101
430
|
'The slowdown option is deprecated in favor of '
|
@@ -109,13 +438,27 @@ class ObjectUpdater(Daemon):
|
|
109
438
|
self.max_objects_per_second = \
|
110
439
|
float(conf.get('objects_per_second',
|
111
440
|
objects_per_second))
|
441
|
+
self.max_objects_per_container_per_second = non_negative_float(
|
442
|
+
conf.get('max_objects_per_container_per_second', 0))
|
443
|
+
self.per_container_ratelimit_buckets = config_positive_int_value(
|
444
|
+
conf.get('per_container_ratelimit_buckets', 1000))
|
112
445
|
self.node_timeout = float(conf.get('node_timeout', 10))
|
113
446
|
self.conn_timeout = float(conf.get('conn_timeout', 0.5))
|
114
447
|
self.report_interval = float(conf.get('report_interval', 300))
|
115
448
|
self.recon_cache_path = conf.get('recon_cache_path',
|
116
|
-
|
117
|
-
self.rcache = os.path.join(self.recon_cache_path,
|
449
|
+
DEFAULT_RECON_CACHE_PATH)
|
450
|
+
self.rcache = os.path.join(self.recon_cache_path, RECON_OBJECT_FILE)
|
451
|
+
max_entries = config_positive_int_value(
|
452
|
+
conf.get('async_tracker_max_entries', 100)
|
453
|
+
)
|
454
|
+
self.dump_count = config_positive_int_value(
|
455
|
+
conf.get('async_tracker_dump_count', 5)
|
456
|
+
)
|
118
457
|
self.stats = SweepStats()
|
458
|
+
self.oldest_async_pendings = OldestAsyncPendingTracker(max_entries)
|
459
|
+
self.max_deferred_updates = non_negative_int(
|
460
|
+
conf.get('max_deferred_updates', 10000))
|
461
|
+
self.begin = time.time()
|
119
462
|
|
120
463
|
def _listdir(self, path):
|
121
464
|
try:
|
@@ -123,8 +466,8 @@ class ObjectUpdater(Daemon):
|
|
123
466
|
except OSError as e:
|
124
467
|
self.stats.errors += 1
|
125
468
|
self.logger.increment('errors')
|
126
|
-
self.logger.error(
|
127
|
-
|
469
|
+
self.logger.error('ERROR: Unable to access %(path)s: '
|
470
|
+
'%(error)s',
|
128
471
|
{'path': path, 'error': e})
|
129
472
|
return []
|
130
473
|
|
@@ -134,83 +477,207 @@ class ObjectUpdater(Daemon):
|
|
134
477
|
self.container_ring = Ring(self.swift_dir, ring_name='container')
|
135
478
|
return self.container_ring
|
136
479
|
|
480
|
+
def _process_device_in_child(self, dev_path, device):
|
481
|
+
"""Process a single device in a forked child process."""
|
482
|
+
signal.signal(signal.SIGTERM, signal.SIG_DFL)
|
483
|
+
os.environ.pop('NOTIFY_SOCKET', None)
|
484
|
+
eventlet_monkey_patch()
|
485
|
+
self.stats.reset()
|
486
|
+
self.oldest_async_pendings.reset()
|
487
|
+
forkbegin = time.time()
|
488
|
+
self.object_sweep(dev_path)
|
489
|
+
elapsed = time.time() - forkbegin
|
490
|
+
self.logger.info(
|
491
|
+
('Object update sweep of %(device)s '
|
492
|
+
'completed: %(elapsed).02fs, %(stats)s'),
|
493
|
+
{'device': device, 'elapsed': elapsed,
|
494
|
+
'stats': self.stats})
|
495
|
+
self.dump_device_recon(device)
|
496
|
+
|
497
|
+
def _process_devices(self, devices):
|
498
|
+
"""Process devices, handling both single and multi-threaded modes."""
|
499
|
+
pids = []
|
500
|
+
# read from container ring to ensure it's fresh
|
501
|
+
self.get_container_ring().get_nodes('')
|
502
|
+
for device in devices:
|
503
|
+
try:
|
504
|
+
dev_path = check_drive(self.devices, device,
|
505
|
+
self.mount_check)
|
506
|
+
except ValueError as err:
|
507
|
+
# We don't count this as an error. The occasional
|
508
|
+
# unmounted drive is part of normal cluster operations,
|
509
|
+
# so a simple warning is sufficient.
|
510
|
+
self.logger.warning('Skipping: %s', err)
|
511
|
+
continue
|
512
|
+
while len(pids) >= self.updater_workers:
|
513
|
+
pids.remove(os.wait()[0])
|
514
|
+
pid = os.fork()
|
515
|
+
if pid:
|
516
|
+
pids.append(pid)
|
517
|
+
else:
|
518
|
+
self._process_device_in_child(dev_path, device)
|
519
|
+
sys.exit()
|
520
|
+
|
521
|
+
while pids:
|
522
|
+
pids.remove(os.wait()[0])
|
523
|
+
|
137
524
|
def run_forever(self, *args, **kwargs):
|
138
525
|
"""Run the updater continuously."""
|
139
526
|
time.sleep(random() * self.interval)
|
140
527
|
while True:
|
141
|
-
self.
|
142
|
-
begin = time.time()
|
143
|
-
pids = []
|
144
|
-
# read from container ring to ensure it's fresh
|
145
|
-
self.get_container_ring().get_nodes('')
|
146
|
-
for device in self._listdir(self.devices):
|
147
|
-
try:
|
148
|
-
dev_path = check_drive(self.devices, device,
|
149
|
-
self.mount_check)
|
150
|
-
except ValueError as err:
|
151
|
-
# We don't count this as an error. The occasional
|
152
|
-
# unmounted drive is part of normal cluster operations,
|
153
|
-
# so a simple warning is sufficient.
|
154
|
-
self.logger.warning('Skipping: %s', err)
|
155
|
-
continue
|
156
|
-
while len(pids) >= self.updater_workers:
|
157
|
-
pids.remove(os.wait()[0])
|
158
|
-
pid = os.fork()
|
159
|
-
if pid:
|
160
|
-
pids.append(pid)
|
161
|
-
else:
|
162
|
-
signal.signal(signal.SIGTERM, signal.SIG_DFL)
|
163
|
-
eventlet_monkey_patch()
|
164
|
-
self.stats.reset()
|
165
|
-
forkbegin = time.time()
|
166
|
-
self.object_sweep(dev_path)
|
167
|
-
elapsed = time.time() - forkbegin
|
168
|
-
self.logger.info(
|
169
|
-
('Object update sweep of %(device)s '
|
170
|
-
'completed: %(elapsed).02fs, %(stats)s'),
|
171
|
-
{'device': device, 'elapsed': elapsed,
|
172
|
-
'stats': self.stats})
|
173
|
-
sys.exit()
|
174
|
-
while pids:
|
175
|
-
pids.remove(os.wait()[0])
|
176
|
-
elapsed = time.time() - begin
|
177
|
-
self.logger.info(_('Object update sweep completed: %.02fs'),
|
178
|
-
elapsed)
|
179
|
-
dump_recon_cache({'object_updater_sweep': elapsed},
|
180
|
-
self.rcache, self.logger)
|
528
|
+
elapsed = self.run_once(*args, **kwargs)
|
181
529
|
if elapsed < self.interval:
|
182
530
|
time.sleep(self.interval - elapsed)
|
183
531
|
|
184
532
|
def run_once(self, *args, **kwargs):
|
185
533
|
"""Run the updater once."""
|
186
|
-
self.logger.info(
|
187
|
-
begin = time.time()
|
188
|
-
self.
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
except ValueError as err:
|
193
|
-
# We don't count this as an error. The occasional unmounted
|
194
|
-
# drive is part of normal cluster operations, so a simple
|
195
|
-
# warning is sufficient.
|
196
|
-
self.logger.warning('Skipping: %s', err)
|
197
|
-
continue
|
198
|
-
self.object_sweep(dev_path)
|
199
|
-
elapsed = time.time() - begin
|
534
|
+
self.logger.info('Begin object update sweep of all devices')
|
535
|
+
self.begin = time.time()
|
536
|
+
devices = self._listdir(self.devices)
|
537
|
+
self._process_devices(devices)
|
538
|
+
now = time.time()
|
539
|
+
elapsed = now - self.begin
|
200
540
|
self.logger.info(
|
201
|
-
('Object update
|
202
|
-
'%(elapsed).02fs
|
203
|
-
{'elapsed': elapsed
|
204
|
-
|
205
|
-
|
541
|
+
('Object update sweep of all devices completed: '
|
542
|
+
'%(elapsed).02fs'),
|
543
|
+
{'elapsed': elapsed})
|
544
|
+
self.aggregate_and_dump_recon(devices, elapsed, now)
|
545
|
+
return elapsed
|
546
|
+
|
547
|
+
def _gather_recon_stats(self):
|
548
|
+
"""Gather stats for device recon dumps."""
|
549
|
+
stats = {
|
550
|
+
'failures_oldest_timestamp':
|
551
|
+
self.oldest_async_pendings.get_oldest_timestamp(),
|
552
|
+
'failures_oldest_timestamp_age':
|
553
|
+
self.oldest_async_pendings.get_oldest_timestamp_age(),
|
554
|
+
'failures_account_container_count':
|
555
|
+
len(self.oldest_async_pendings.ac_to_timestamp),
|
556
|
+
'failures_oldest_timestamp_account_containers':
|
557
|
+
self.oldest_async_pendings.get_n_oldest_timestamp_acs(
|
558
|
+
self.dump_count),
|
559
|
+
'tracker_memory_usage':
|
560
|
+
self.oldest_async_pendings.get_memory_usage(),
|
561
|
+
}
|
562
|
+
return stats
|
563
|
+
|
564
|
+
def dump_device_recon(self, device):
|
565
|
+
"""Dump recon stats for a single device."""
|
566
|
+
disk_recon_stats = self._gather_recon_stats()
|
567
|
+
dump_recon_cache(
|
568
|
+
{'object_updater_per_device': {device: disk_recon_stats}},
|
569
|
+
self.rcache,
|
570
|
+
self.logger,
|
571
|
+
)
|
572
|
+
|
573
|
+
def aggregate_and_dump_recon(self, devices, elapsed, now):
|
574
|
+
"""
|
575
|
+
Aggregate recon stats across devices and dump the result to the
|
576
|
+
recon cache.
|
577
|
+
"""
|
578
|
+
recon_cache = load_recon_cache(self.rcache)
|
579
|
+
device_stats = recon_cache.get('object_updater_per_device', {})
|
580
|
+
if not isinstance(device_stats, dict):
|
581
|
+
raise TypeError('object_updater_per_device must be a dict')
|
582
|
+
devices_to_remove = set(device_stats) - set(devices)
|
583
|
+
device_stats = {dev: device_stats.get(dev) or {}
|
584
|
+
for dev in devices}
|
585
|
+
|
586
|
+
aggregated_oldest_entries = []
|
587
|
+
|
588
|
+
for stats in device_stats.values():
|
589
|
+
container_data = stats.get(
|
590
|
+
'failures_oldest_timestamp_account_containers', {})
|
591
|
+
aggregated_oldest_entries.extend(container_data.get(
|
592
|
+
'oldest_entries', []))
|
593
|
+
aggregated_oldest_entries.sort(key=lambda x: x['timestamp'])
|
594
|
+
aggregated_oldest_entries = aggregated_oldest_entries[:self.dump_count]
|
595
|
+
aggregated_oldest_count = len(aggregated_oldest_entries)
|
596
|
+
|
597
|
+
aggregated_stats = {
|
598
|
+
'failures_account_container_count': max(
|
599
|
+
list(
|
600
|
+
stats.get('failures_account_container_count', 0)
|
601
|
+
for stats in device_stats.values()
|
602
|
+
)
|
603
|
+
or [0],
|
604
|
+
),
|
605
|
+
'tracker_memory_usage': (
|
606
|
+
float(sum(
|
607
|
+
stats.get('tracker_memory_usage', 0)
|
608
|
+
for stats in device_stats.values()
|
609
|
+
))
|
610
|
+
/ float(len(device_stats))
|
611
|
+
)
|
612
|
+
* max(1, min(self.updater_workers, len(device_stats)))
|
613
|
+
if device_stats
|
614
|
+
else 0,
|
615
|
+
'failures_oldest_timestamp': min(
|
616
|
+
list(filter(lambda x: x is not None,
|
617
|
+
[stats.get('failures_oldest_timestamp')
|
618
|
+
for stats in device_stats.values()]
|
619
|
+
)) or [None],
|
620
|
+
),
|
621
|
+
'failures_oldest_timestamp_age': max(
|
622
|
+
list(filter(lambda x: x is not None,
|
623
|
+
[stats.get('failures_oldest_timestamp_age')
|
624
|
+
for stats in device_stats.values()]
|
625
|
+
)) or [None],
|
626
|
+
),
|
627
|
+
'failures_oldest_timestamp_account_containers': {
|
628
|
+
'oldest_count': aggregated_oldest_count,
|
629
|
+
'oldest_entries': aggregated_oldest_entries,
|
630
|
+
},
|
631
|
+
}
|
632
|
+
|
633
|
+
recon_dump = {
|
634
|
+
'object_updater_sweep': elapsed,
|
635
|
+
'object_updater_stats': aggregated_stats,
|
636
|
+
'object_updater_last': now
|
637
|
+
}
|
638
|
+
if devices_to_remove:
|
639
|
+
recon_dump['object_updater_per_device'] = {
|
640
|
+
d: {} for d in devices_to_remove}
|
641
|
+
dump_recon_cache(
|
642
|
+
recon_dump,
|
643
|
+
self.rcache,
|
644
|
+
self.logger,
|
645
|
+
)
|
646
|
+
|
647
|
+
def _load_update(self, device, update_path):
|
648
|
+
try:
|
649
|
+
return pickle.load(open(update_path, 'rb')) # nosec: B301
|
650
|
+
except Exception as e:
|
651
|
+
if getattr(e, 'errno', None) == errno.ENOENT:
|
652
|
+
return
|
653
|
+
self.logger.exception(
|
654
|
+
'ERROR Pickle problem, quarantining %s', update_path)
|
655
|
+
self.stats.quarantines += 1
|
656
|
+
self.logger.increment('quarantines')
|
657
|
+
target_path = os.path.join(device, 'quarantined', 'objects',
|
658
|
+
os.path.basename(update_path))
|
659
|
+
renamer(update_path, target_path, fsync=False)
|
660
|
+
try:
|
661
|
+
# If this was the last async_pending in the directory,
|
662
|
+
# then this will succeed. Otherwise, it'll fail, and
|
663
|
+
# that's okay.
|
664
|
+
os.rmdir(os.path.dirname(update_path))
|
665
|
+
except OSError:
|
666
|
+
pass
|
667
|
+
return
|
206
668
|
|
207
669
|
def _iter_async_pendings(self, device):
|
208
670
|
"""
|
209
|
-
Locate and yield
|
210
|
-
|
211
|
-
|
671
|
+
Locate and yield an update context for all the async pending files on
|
672
|
+
the device. Each update context contains details of the async pending
|
673
|
+
file location, its timestamp and the un-pickled update data.
|
212
674
|
|
213
|
-
|
675
|
+
Async pending files that fail to load will be quarantined.
|
676
|
+
|
677
|
+
Only the most recent update for the same object is yielded; older
|
678
|
+
(stale) async pending files are unlinked as they are located.
|
679
|
+
|
680
|
+
The iterator tries to clean up empty directories as it goes.
|
214
681
|
"""
|
215
682
|
# loop through async pending dirs for all policies
|
216
683
|
for asyncdir in self._listdir(device):
|
@@ -226,28 +693,30 @@ class ObjectUpdater(Daemon):
|
|
226
693
|
except PolicyError as e:
|
227
694
|
# This isn't an error, but a misconfiguration. Logging a
|
228
695
|
# warning should be sufficient.
|
229
|
-
self.logger.warning(
|
230
|
-
|
231
|
-
|
696
|
+
self.logger.warning('Directory %(directory)r does not map '
|
697
|
+
'to a valid policy (%(error)s)', {
|
698
|
+
'directory': asyncdir, 'error': e})
|
232
699
|
continue
|
233
|
-
|
700
|
+
prefix_dirs = self._listdir(async_pending)
|
701
|
+
shuffle(prefix_dirs)
|
702
|
+
for prefix in prefix_dirs:
|
234
703
|
prefix_path = os.path.join(async_pending, prefix)
|
235
704
|
if not os.path.isdir(prefix_path):
|
236
705
|
continue
|
237
706
|
last_obj_hash = None
|
238
|
-
for
|
239
|
-
|
707
|
+
for update_file in sorted(self._listdir(prefix_path),
|
708
|
+
reverse=True):
|
709
|
+
update_path = os.path.join(prefix_path, update_file)
|
240
710
|
if not os.path.isfile(update_path):
|
241
711
|
continue
|
242
712
|
try:
|
243
|
-
obj_hash, timestamp =
|
713
|
+
obj_hash, timestamp = update_file.split('-')
|
244
714
|
except ValueError:
|
245
715
|
self.stats.errors += 1
|
246
716
|
self.logger.increment('errors')
|
247
717
|
self.logger.error(
|
248
|
-
|
249
|
-
|
250
|
-
% (update_path))
|
718
|
+
'ERROR async pending file with unexpected '
|
719
|
+
'name %s', update_path)
|
251
720
|
continue
|
252
721
|
# Async pendings are stored on disk like this:
|
253
722
|
#
|
@@ -269,14 +738,23 @@ class ObjectUpdater(Daemon):
|
|
269
738
|
#
|
270
739
|
# This way, our caller only gets useful async_pendings.
|
271
740
|
if obj_hash == last_obj_hash:
|
272
|
-
self.stats.
|
273
|
-
self.logger.increment('
|
274
|
-
|
741
|
+
self.stats.outdated_unlinks += 1
|
742
|
+
self.logger.increment('outdated_unlinks')
|
743
|
+
try:
|
744
|
+
os.unlink(update_path)
|
745
|
+
except OSError as e:
|
746
|
+
if e.errno != errno.ENOENT:
|
747
|
+
raise
|
275
748
|
else:
|
276
749
|
last_obj_hash = obj_hash
|
277
|
-
|
278
|
-
|
279
|
-
|
750
|
+
update = self._load_update(device, update_path)
|
751
|
+
if update is not None:
|
752
|
+
yield {'device': device,
|
753
|
+
'policy': policy,
|
754
|
+
'update_path': update_path,
|
755
|
+
'obj_hash': obj_hash,
|
756
|
+
'timestamp': timestamp,
|
757
|
+
'update': update}
|
280
758
|
|
281
759
|
def object_sweep(self, device):
|
282
760
|
"""
|
@@ -294,10 +772,15 @@ class ObjectUpdater(Daemon):
|
|
294
772
|
ap_iter = RateLimitedIterator(
|
295
773
|
self._iter_async_pendings(device),
|
296
774
|
elements_per_second=self.max_objects_per_second)
|
775
|
+
ap_iter = BucketizedUpdateSkippingLimiter(
|
776
|
+
ap_iter, self.logger, self.stats,
|
777
|
+
self.per_container_ratelimit_buckets,
|
778
|
+
self.max_objects_per_container_per_second,
|
779
|
+
max_deferred_elements=self.max_deferred_updates,
|
780
|
+
drain_until=self.begin + self.interval)
|
297
781
|
with ContextPool(self.concurrency) as pool:
|
298
|
-
for
|
299
|
-
pool.spawn(self.process_object_update,
|
300
|
-
update['path'], update['device'], update['policy'])
|
782
|
+
for update_ctx in ap_iter:
|
783
|
+
pool.spawn(self.process_object_update, **update_ctx)
|
301
784
|
now = time.time()
|
302
785
|
if now - last_status_update >= self.report_interval:
|
303
786
|
this_sweep = self.stats.since(start_stats)
|
@@ -318,8 +801,13 @@ class ObjectUpdater(Daemon):
|
|
318
801
|
'in %(elapsed).02fs seconds:, '
|
319
802
|
'%(successes)d successes, %(failures)d failures, '
|
320
803
|
'%(quarantines)d quarantines, '
|
321
|
-
'%(unlinks)d unlinks,
|
322
|
-
'%(
|
804
|
+
'%(unlinks)d unlinks, '
|
805
|
+
'%(outdated_unlinks)d outdated_unlinks, '
|
806
|
+
'%(errors)d errors, '
|
807
|
+
'%(redirects)d redirects, '
|
808
|
+
'%(skips)d skips, '
|
809
|
+
'%(deferrals)d deferrals, '
|
810
|
+
'%(drains)d drains '
|
323
811
|
'(pid: %(pid)d)'),
|
324
812
|
{'device': device,
|
325
813
|
'elapsed': time.time() - start_time,
|
@@ -328,28 +816,25 @@ class ObjectUpdater(Daemon):
|
|
328
816
|
'failures': sweep_totals.failures,
|
329
817
|
'quarantines': sweep_totals.quarantines,
|
330
818
|
'unlinks': sweep_totals.unlinks,
|
819
|
+
'outdated_unlinks': sweep_totals.outdated_unlinks,
|
331
820
|
'errors': sweep_totals.errors,
|
332
|
-
'redirects': sweep_totals.redirects
|
333
|
-
|
334
|
-
|
821
|
+
'redirects': sweep_totals.redirects,
|
822
|
+
'skips': sweep_totals.skips,
|
823
|
+
'deferrals': sweep_totals.deferrals,
|
824
|
+
'drains': sweep_totals.drains
|
825
|
+
})
|
826
|
+
|
827
|
+
def process_object_update(self, update_path, device, policy, update,
|
828
|
+
**kwargs):
|
335
829
|
"""
|
336
830
|
Process the object information to be updated and update.
|
337
831
|
|
338
832
|
:param update_path: path to pickled object update file
|
339
833
|
:param device: path to device
|
340
834
|
:param policy: storage policy of object update
|
835
|
+
:param update: the un-pickled update data
|
836
|
+
:param kwargs: un-used keys from update_ctx
|
341
837
|
"""
|
342
|
-
try:
|
343
|
-
update = pickle.load(open(update_path, 'rb'))
|
344
|
-
except Exception:
|
345
|
-
self.logger.exception(
|
346
|
-
_('ERROR Pickle problem, quarantining %s'), update_path)
|
347
|
-
self.stats.quarantines += 1
|
348
|
-
self.logger.increment('quarantines')
|
349
|
-
target_path = os.path.join(device, 'quarantined', 'objects',
|
350
|
-
os.path.basename(update_path))
|
351
|
-
renamer(update_path, target_path, fsync=False)
|
352
|
-
return
|
353
838
|
|
354
839
|
def do_update():
|
355
840
|
successes = update.get('successes', [])
|
@@ -358,15 +843,12 @@ class ObjectUpdater(Daemon):
|
|
358
843
|
headers_out.setdefault('X-Backend-Storage-Policy-Index',
|
359
844
|
str(int(policy)))
|
360
845
|
headers_out.setdefault('X-Backend-Accept-Redirect', 'true')
|
361
|
-
|
362
|
-
|
363
|
-
acct, cont = split_path('/' + container_path, minsegs=2)
|
364
|
-
else:
|
365
|
-
acct, cont = update['account'], update['container']
|
846
|
+
headers_out.setdefault('X-Backend-Accept-Quoted-Location', 'true')
|
847
|
+
acct, cont = split_update_path(update)
|
366
848
|
part, nodes = self.get_container_ring().get_nodes(acct, cont)
|
367
|
-
|
849
|
+
path = '/%s/%s/%s' % (acct, cont, update['obj'])
|
368
850
|
events = [spawn(self.object_update,
|
369
|
-
node, part, update['op'],
|
851
|
+
node, part, update['op'], path, headers_out)
|
370
852
|
for node in nodes if node['id'] not in successes]
|
371
853
|
success = True
|
372
854
|
new_successes = rewrite_pickle = False
|
@@ -385,8 +867,8 @@ class ObjectUpdater(Daemon):
|
|
385
867
|
if success:
|
386
868
|
self.stats.successes += 1
|
387
869
|
self.logger.increment('successes')
|
388
|
-
self.logger.debug('Update sent for %(
|
389
|
-
{'
|
870
|
+
self.logger.debug('Update sent for %(path)s %(update_path)s',
|
871
|
+
{'path': path, 'update_path': update_path})
|
390
872
|
self.stats.unlinks += 1
|
391
873
|
self.logger.increment('unlinks')
|
392
874
|
os.unlink(update_path)
|
@@ -412,15 +894,19 @@ class ObjectUpdater(Daemon):
|
|
412
894
|
self.stats.redirects += 1
|
413
895
|
self.logger.increment("redirects")
|
414
896
|
self.logger.debug(
|
415
|
-
'Update redirected for %(
|
416
|
-
|
897
|
+
'Update redirected for %(path)s %(update_path)s to '
|
898
|
+
'%(shard)s',
|
899
|
+
{'path': path, 'update_path': update_path,
|
417
900
|
'shard': update['container_path']})
|
418
901
|
rewrite_pickle = True
|
419
902
|
else:
|
420
903
|
self.stats.failures += 1
|
421
904
|
self.logger.increment('failures')
|
422
|
-
self.logger.debug('Update failed for %(
|
423
|
-
{'
|
905
|
+
self.logger.debug('Update failed for %(path)s %(update_path)s',
|
906
|
+
{'path': path, 'update_path': update_path})
|
907
|
+
self.oldest_async_pendings.add_update(
|
908
|
+
acct, cont, kwargs['timestamp']
|
909
|
+
)
|
424
910
|
if new_successes:
|
425
911
|
update['successes'] = successes
|
426
912
|
rewrite_pickle = True
|
@@ -435,14 +921,14 @@ class ObjectUpdater(Daemon):
|
|
435
921
|
write_pickle(update, update_path, os.path.join(
|
436
922
|
device, get_tmp_dir(policy)))
|
437
923
|
|
438
|
-
def object_update(self, node, part, op,
|
924
|
+
def object_update(self, node, part, op, path, headers_out):
|
439
925
|
"""
|
440
926
|
Perform the object update to the container
|
441
927
|
|
442
928
|
:param node: node dictionary from the container ring
|
443
929
|
:param part: partition that holds the container
|
444
930
|
:param op: operation performed (ex: 'PUT' or 'DELETE')
|
445
|
-
:param
|
931
|
+
:param path: /<acct>/<cont>/<obj> path being updated
|
446
932
|
:param headers_out: headers to send with the update
|
447
933
|
:return: a tuple of (``success``, ``node_id``, ``redirect``)
|
448
934
|
where ``success`` is True if the update succeeded, ``node_id`` is
|
@@ -450,31 +936,59 @@ class ObjectUpdater(Daemon):
|
|
450
936
|
tuple of (a path, a timestamp string).
|
451
937
|
"""
|
452
938
|
redirect = None
|
939
|
+
start = time.time()
|
940
|
+
# Assume an error until we hear otherwise
|
941
|
+
status = 500
|
453
942
|
try:
|
454
943
|
with ConnectionTimeout(self.conn_timeout):
|
455
|
-
conn = http_connect(
|
456
|
-
|
944
|
+
conn = http_connect(
|
945
|
+
node['replication_ip'], node['replication_port'],
|
946
|
+
node['device'], part, op, path, headers_out)
|
457
947
|
with Timeout(self.node_timeout):
|
458
948
|
resp = conn.getresponse()
|
459
949
|
resp.read()
|
950
|
+
status = resp.status
|
460
951
|
|
461
|
-
if
|
952
|
+
if status == HTTP_MOVED_PERMANENTLY:
|
462
953
|
try:
|
463
954
|
redirect = get_redirect_data(resp)
|
464
955
|
except ValueError as err:
|
465
956
|
self.logger.error(
|
466
957
|
'Container update failed for %r; problem with '
|
467
|
-
'redirect location: %s' % (
|
958
|
+
'redirect location: %s' % (path, err))
|
468
959
|
|
469
|
-
success = is_success(
|
960
|
+
success = is_success(status)
|
470
961
|
if not success:
|
471
962
|
self.logger.debug(
|
472
|
-
|
473
|
-
|
474
|
-
{'status': resp.status,
|
475
|
-
'
|
963
|
+
'Error code %(status)d is returned from remote '
|
964
|
+
'server %(node)s',
|
965
|
+
{'status': resp.status,
|
966
|
+
'node': node_to_string(node, replication=True)})
|
476
967
|
return success, node['id'], redirect
|
477
|
-
except
|
478
|
-
self.logger.exception(
|
479
|
-
|
968
|
+
except Exception:
|
969
|
+
self.logger.exception('ERROR with remote server %s',
|
970
|
+
node_to_string(node, replication=True))
|
971
|
+
except Timeout as exc:
|
972
|
+
action = 'connecting to'
|
973
|
+
if not isinstance(exc, ConnectionTimeout):
|
974
|
+
# i.e., we definitely made the request but gave up
|
975
|
+
# waiting for the response
|
976
|
+
status = 499
|
977
|
+
action = 'waiting on'
|
978
|
+
self.logger.info(
|
979
|
+
'Timeout %s remote server %s: %s',
|
980
|
+
action, node_to_string(node, replication=True), exc)
|
981
|
+
finally:
|
982
|
+
elapsed = time.time() - start
|
983
|
+
self.logger.timing('updater.timing.status.%s' % status,
|
984
|
+
elapsed * 1000)
|
480
985
|
return HTTP_INTERNAL_SERVER_ERROR, node['id'], redirect
|
986
|
+
|
987
|
+
|
988
|
+
def main():
|
989
|
+
conf_file, options = parse_options(once=True)
|
990
|
+
run_daemon(ObjectUpdater, conf_file, **options)
|
991
|
+
|
992
|
+
|
993
|
+
if __name__ == '__main__':
|
994
|
+
main()
|