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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (206) hide show
  1. swift/__init__.py +29 -50
  2. swift/account/auditor.py +21 -118
  3. swift/account/backend.py +33 -28
  4. swift/account/reaper.py +37 -28
  5. swift/account/replicator.py +22 -0
  6. swift/account/server.py +60 -26
  7. swift/account/utils.py +28 -11
  8. swift-2.23.3.data/scripts/swift-account-audit → swift/cli/account_audit.py +23 -13
  9. swift-2.23.3.data/scripts/swift-config → swift/cli/config.py +2 -2
  10. swift/cli/container_deleter.py +5 -11
  11. swift-2.23.3.data/scripts/swift-dispersion-populate → swift/cli/dispersion_populate.py +8 -7
  12. swift/cli/dispersion_report.py +10 -9
  13. swift-2.23.3.data/scripts/swift-drive-audit → swift/cli/drive_audit.py +63 -21
  14. swift/cli/form_signature.py +3 -7
  15. swift-2.23.3.data/scripts/swift-get-nodes → swift/cli/get_nodes.py +8 -2
  16. swift/cli/info.py +154 -14
  17. swift/cli/manage_shard_ranges.py +705 -37
  18. swift-2.23.3.data/scripts/swift-oldies → swift/cli/oldies.py +25 -14
  19. swift-2.23.3.data/scripts/swift-orphans → swift/cli/orphans.py +7 -3
  20. swift/cli/recon.py +196 -67
  21. swift-2.23.3.data/scripts/swift-recon-cron → swift/cli/recon_cron.py +17 -20
  22. swift-2.23.3.data/scripts/swift-reconciler-enqueue → swift/cli/reconciler_enqueue.py +2 -3
  23. swift/cli/relinker.py +807 -126
  24. swift/cli/reload.py +135 -0
  25. swift/cli/ringbuilder.py +217 -20
  26. swift/cli/ringcomposer.py +0 -1
  27. swift/cli/shard-info.py +4 -3
  28. swift/common/base_storage_server.py +9 -20
  29. swift/common/bufferedhttp.py +48 -74
  30. swift/common/constraints.py +20 -15
  31. swift/common/container_sync_realms.py +9 -11
  32. swift/common/daemon.py +25 -8
  33. swift/common/db.py +195 -128
  34. swift/common/db_auditor.py +168 -0
  35. swift/common/db_replicator.py +95 -55
  36. swift/common/digest.py +141 -0
  37. swift/common/direct_client.py +144 -33
  38. swift/common/error_limiter.py +93 -0
  39. swift/common/exceptions.py +25 -1
  40. swift/common/header_key_dict.py +2 -9
  41. swift/common/http_protocol.py +373 -0
  42. swift/common/internal_client.py +129 -59
  43. swift/common/linkat.py +3 -4
  44. swift/common/manager.py +284 -67
  45. swift/common/memcached.py +390 -145
  46. swift/common/middleware/__init__.py +4 -0
  47. swift/common/middleware/account_quotas.py +211 -46
  48. swift/common/middleware/acl.py +3 -8
  49. swift/common/middleware/backend_ratelimit.py +230 -0
  50. swift/common/middleware/bulk.py +22 -34
  51. swift/common/middleware/catch_errors.py +1 -3
  52. swift/common/middleware/cname_lookup.py +6 -11
  53. swift/common/middleware/container_quotas.py +1 -1
  54. swift/common/middleware/container_sync.py +39 -17
  55. swift/common/middleware/copy.py +12 -0
  56. swift/common/middleware/crossdomain.py +22 -9
  57. swift/common/middleware/crypto/__init__.py +2 -1
  58. swift/common/middleware/crypto/crypto_utils.py +11 -15
  59. swift/common/middleware/crypto/decrypter.py +28 -11
  60. swift/common/middleware/crypto/encrypter.py +12 -17
  61. swift/common/middleware/crypto/keymaster.py +8 -15
  62. swift/common/middleware/crypto/kms_keymaster.py +2 -1
  63. swift/common/middleware/dlo.py +15 -11
  64. swift/common/middleware/domain_remap.py +5 -4
  65. swift/common/middleware/etag_quoter.py +128 -0
  66. swift/common/middleware/formpost.py +73 -70
  67. swift/common/middleware/gatekeeper.py +8 -1
  68. swift/common/middleware/keystoneauth.py +33 -3
  69. swift/common/middleware/list_endpoints.py +4 -4
  70. swift/common/middleware/listing_formats.py +85 -49
  71. swift/common/middleware/memcache.py +4 -95
  72. swift/common/middleware/name_check.py +3 -2
  73. swift/common/middleware/proxy_logging.py +160 -92
  74. swift/common/middleware/ratelimit.py +17 -10
  75. swift/common/middleware/read_only.py +6 -4
  76. swift/common/middleware/recon.py +59 -22
  77. swift/common/middleware/s3api/acl_handlers.py +25 -3
  78. swift/common/middleware/s3api/acl_utils.py +6 -1
  79. swift/common/middleware/s3api/controllers/__init__.py +6 -0
  80. swift/common/middleware/s3api/controllers/acl.py +3 -2
  81. swift/common/middleware/s3api/controllers/bucket.py +242 -137
  82. swift/common/middleware/s3api/controllers/logging.py +2 -2
  83. swift/common/middleware/s3api/controllers/multi_delete.py +43 -20
  84. swift/common/middleware/s3api/controllers/multi_upload.py +219 -133
  85. swift/common/middleware/s3api/controllers/obj.py +112 -8
  86. swift/common/middleware/s3api/controllers/object_lock.py +44 -0
  87. swift/common/middleware/s3api/controllers/s3_acl.py +2 -2
  88. swift/common/middleware/s3api/controllers/tagging.py +57 -0
  89. swift/common/middleware/s3api/controllers/versioning.py +36 -7
  90. swift/common/middleware/s3api/etree.py +22 -9
  91. swift/common/middleware/s3api/exception.py +0 -4
  92. swift/common/middleware/s3api/s3api.py +113 -41
  93. swift/common/middleware/s3api/s3request.py +384 -218
  94. swift/common/middleware/s3api/s3response.py +126 -23
  95. swift/common/middleware/s3api/s3token.py +16 -17
  96. swift/common/middleware/s3api/schema/delete.rng +1 -1
  97. swift/common/middleware/s3api/subresource.py +7 -10
  98. swift/common/middleware/s3api/utils.py +27 -10
  99. swift/common/middleware/slo.py +665 -358
  100. swift/common/middleware/staticweb.py +64 -37
  101. swift/common/middleware/symlink.py +51 -18
  102. swift/common/middleware/tempauth.py +76 -58
  103. swift/common/middleware/tempurl.py +191 -173
  104. swift/common/middleware/versioned_writes/__init__.py +51 -0
  105. swift/common/middleware/{versioned_writes.py → versioned_writes/legacy.py} +27 -26
  106. swift/common/middleware/versioned_writes/object_versioning.py +1482 -0
  107. swift/common/middleware/x_profile/exceptions.py +1 -4
  108. swift/common/middleware/x_profile/html_viewer.py +18 -19
  109. swift/common/middleware/x_profile/profile_model.py +1 -2
  110. swift/common/middleware/xprofile.py +10 -10
  111. swift-2.23.3.data/scripts/swift-container-server → swift/common/recon.py +13 -8
  112. swift/common/registry.py +147 -0
  113. swift/common/request_helpers.py +324 -57
  114. swift/common/ring/builder.py +67 -25
  115. swift/common/ring/composite_builder.py +1 -1
  116. swift/common/ring/ring.py +177 -51
  117. swift/common/ring/utils.py +1 -1
  118. swift/common/splice.py +10 -6
  119. swift/common/statsd_client.py +205 -0
  120. swift/common/storage_policy.py +49 -44
  121. swift/common/swob.py +86 -102
  122. swift/common/{utils.py → utils/__init__.py} +2163 -2772
  123. swift/common/utils/base.py +131 -0
  124. swift/common/utils/config.py +433 -0
  125. swift/common/utils/ipaddrs.py +256 -0
  126. swift/common/utils/libc.py +345 -0
  127. swift/common/utils/logs.py +859 -0
  128. swift/common/utils/timestamp.py +412 -0
  129. swift/common/wsgi.py +553 -535
  130. swift/container/auditor.py +14 -100
  131. swift/container/backend.py +490 -231
  132. swift/container/reconciler.py +126 -37
  133. swift/container/replicator.py +96 -22
  134. swift/container/server.py +358 -165
  135. swift/container/sharder.py +1540 -684
  136. swift/container/sync.py +94 -88
  137. swift/container/updater.py +53 -32
  138. swift/obj/auditor.py +153 -35
  139. swift/obj/diskfile.py +466 -217
  140. swift/obj/expirer.py +406 -124
  141. swift/obj/mem_diskfile.py +7 -4
  142. swift/obj/mem_server.py +1 -0
  143. swift/obj/reconstructor.py +523 -262
  144. swift/obj/replicator.py +249 -188
  145. swift/obj/server.py +207 -122
  146. swift/obj/ssync_receiver.py +145 -85
  147. swift/obj/ssync_sender.py +113 -54
  148. swift/obj/updater.py +652 -139
  149. swift/obj/watchers/__init__.py +0 -0
  150. swift/obj/watchers/dark_data.py +213 -0
  151. swift/proxy/controllers/account.py +11 -11
  152. swift/proxy/controllers/base.py +848 -604
  153. swift/proxy/controllers/container.py +433 -92
  154. swift/proxy/controllers/info.py +3 -2
  155. swift/proxy/controllers/obj.py +1000 -489
  156. swift/proxy/server.py +185 -112
  157. {swift-2.23.3.dist-info → swift-2.35.0.dist-info}/AUTHORS +58 -11
  158. {swift-2.23.3.dist-info → swift-2.35.0.dist-info}/METADATA +51 -56
  159. swift-2.35.0.dist-info/RECORD +201 -0
  160. {swift-2.23.3.dist-info → swift-2.35.0.dist-info}/WHEEL +1 -1
  161. {swift-2.23.3.dist-info → swift-2.35.0.dist-info}/entry_points.txt +43 -0
  162. swift-2.35.0.dist-info/pbr.json +1 -0
  163. swift/locale/de/LC_MESSAGES/swift.po +0 -1216
  164. swift/locale/en_GB/LC_MESSAGES/swift.po +0 -1207
  165. swift/locale/es/LC_MESSAGES/swift.po +0 -1085
  166. swift/locale/fr/LC_MESSAGES/swift.po +0 -909
  167. swift/locale/it/LC_MESSAGES/swift.po +0 -894
  168. swift/locale/ja/LC_MESSAGES/swift.po +0 -965
  169. swift/locale/ko_KR/LC_MESSAGES/swift.po +0 -964
  170. swift/locale/pt_BR/LC_MESSAGES/swift.po +0 -881
  171. swift/locale/ru/LC_MESSAGES/swift.po +0 -891
  172. swift/locale/tr_TR/LC_MESSAGES/swift.po +0 -832
  173. swift/locale/zh_CN/LC_MESSAGES/swift.po +0 -833
  174. swift/locale/zh_TW/LC_MESSAGES/swift.po +0 -838
  175. swift-2.23.3.data/scripts/swift-account-auditor +0 -23
  176. swift-2.23.3.data/scripts/swift-account-info +0 -51
  177. swift-2.23.3.data/scripts/swift-account-reaper +0 -23
  178. swift-2.23.3.data/scripts/swift-account-replicator +0 -34
  179. swift-2.23.3.data/scripts/swift-account-server +0 -23
  180. swift-2.23.3.data/scripts/swift-container-auditor +0 -23
  181. swift-2.23.3.data/scripts/swift-container-info +0 -55
  182. swift-2.23.3.data/scripts/swift-container-reconciler +0 -21
  183. swift-2.23.3.data/scripts/swift-container-replicator +0 -34
  184. swift-2.23.3.data/scripts/swift-container-sharder +0 -37
  185. swift-2.23.3.data/scripts/swift-container-sync +0 -23
  186. swift-2.23.3.data/scripts/swift-container-updater +0 -23
  187. swift-2.23.3.data/scripts/swift-dispersion-report +0 -24
  188. swift-2.23.3.data/scripts/swift-form-signature +0 -20
  189. swift-2.23.3.data/scripts/swift-init +0 -119
  190. swift-2.23.3.data/scripts/swift-object-auditor +0 -29
  191. swift-2.23.3.data/scripts/swift-object-expirer +0 -33
  192. swift-2.23.3.data/scripts/swift-object-info +0 -60
  193. swift-2.23.3.data/scripts/swift-object-reconstructor +0 -33
  194. swift-2.23.3.data/scripts/swift-object-relinker +0 -41
  195. swift-2.23.3.data/scripts/swift-object-replicator +0 -37
  196. swift-2.23.3.data/scripts/swift-object-server +0 -27
  197. swift-2.23.3.data/scripts/swift-object-updater +0 -23
  198. swift-2.23.3.data/scripts/swift-proxy-server +0 -23
  199. swift-2.23.3.data/scripts/swift-recon +0 -24
  200. swift-2.23.3.data/scripts/swift-ring-builder +0 -24
  201. swift-2.23.3.data/scripts/swift-ring-builder-analyzer +0 -22
  202. swift-2.23.3.data/scripts/swift-ring-composer +0 -22
  203. swift-2.23.3.dist-info/RECORD +0 -220
  204. swift-2.23.3.dist-info/pbr.json +0 -1
  205. {swift-2.23.3.dist-info → swift-2.35.0.dist-info}/LICENSE +0 -0
  206. {swift-2.23.3.dist-info → swift-2.35.0.dist-info}/top_level.txt +0 -0
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 six.moves.cPickle as pickle
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
- from swift import gettext_ as _
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
- from swift.common.daemon import Daemon
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.redirects - other.redirects)
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 = int(conf.get('interval', 300))
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 = int(conf.get('updater_workers', 1))
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
- '/var/cache/swift')
117
- self.rcache = os.path.join(self.recon_cache_path, 'object.recon')
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(_('ERROR: Unable to access %(path)s: '
127
- '%(error)s') %
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.logger.info(_('Begin object update sweep'))
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(_('Begin object update single threaded sweep'))
187
- begin = time.time()
188
- self.stats.reset()
189
- for device in self._listdir(self.devices):
190
- try:
191
- dev_path = check_drive(self.devices, device, self.mount_check)
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 single-threaded sweep completed: '
202
- '%(elapsed).02fs, %(stats)s'),
203
- {'elapsed': elapsed, 'stats': self.stats})
204
- dump_recon_cache({'object_updater_sweep': elapsed},
205
- self.rcache, self.logger)
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 all the async pendings on the device. Multiple updates
210
- for the same object will come out in reverse-chronological order
211
- (i.e. newest first) so that callers can skip stale async_pendings.
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
- Tries to clean up empty directories as it goes.
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(_('Directory %(directory)r does not map '
230
- 'to a valid policy (%(error)s)') % {
231
- 'directory': asyncdir, 'error': e})
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
- for prefix in self._listdir(async_pending):
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 update in sorted(self._listdir(prefix_path), reverse=True):
239
- update_path = os.path.join(prefix_path, update)
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 = update.split('-')
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
- _('ERROR async pending file with unexpected '
249
- 'name %s')
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.unlinks += 1
273
- self.logger.increment('unlinks')
274
- os.unlink(update_path)
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
- yield {'device': device, 'policy': policy,
278
- 'path': update_path,
279
- 'obj_hash': obj_hash, 'timestamp': timestamp}
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 update in ap_iter:
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, %(errors)d errors, '
322
- '%(redirects)d redirects '
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
- def process_object_update(self, update_path, device, policy):
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', [])
@@ -359,15 +844,11 @@ class ObjectUpdater(Daemon):
359
844
  str(int(policy)))
360
845
  headers_out.setdefault('X-Backend-Accept-Redirect', 'true')
361
846
  headers_out.setdefault('X-Backend-Accept-Quoted-Location', 'true')
362
- container_path = update.get('container_path')
363
- if container_path:
364
- acct, cont = split_path('/' + container_path, minsegs=2)
365
- else:
366
- acct, cont = update['account'], update['container']
847
+ acct, cont = split_update_path(update)
367
848
  part, nodes = self.get_container_ring().get_nodes(acct, cont)
368
- obj = '/%s/%s/%s' % (acct, cont, update['obj'])
849
+ path = '/%s/%s/%s' % (acct, cont, update['obj'])
369
850
  events = [spawn(self.object_update,
370
- node, part, update['op'], obj, headers_out)
851
+ node, part, update['op'], path, headers_out)
371
852
  for node in nodes if node['id'] not in successes]
372
853
  success = True
373
854
  new_successes = rewrite_pickle = False
@@ -386,8 +867,8 @@ class ObjectUpdater(Daemon):
386
867
  if success:
387
868
  self.stats.successes += 1
388
869
  self.logger.increment('successes')
389
- self.logger.debug('Update sent for %(obj)s %(path)s',
390
- {'obj': obj, 'path': update_path})
870
+ self.logger.debug('Update sent for %(path)s %(update_path)s',
871
+ {'path': path, 'update_path': update_path})
391
872
  self.stats.unlinks += 1
392
873
  self.logger.increment('unlinks')
393
874
  os.unlink(update_path)
@@ -413,15 +894,19 @@ class ObjectUpdater(Daemon):
413
894
  self.stats.redirects += 1
414
895
  self.logger.increment("redirects")
415
896
  self.logger.debug(
416
- 'Update redirected for %(obj)s %(path)s to %(shard)s',
417
- {'obj': obj, 'path': update_path,
897
+ 'Update redirected for %(path)s %(update_path)s to '
898
+ '%(shard)s',
899
+ {'path': path, 'update_path': update_path,
418
900
  'shard': update['container_path']})
419
901
  rewrite_pickle = True
420
902
  else:
421
903
  self.stats.failures += 1
422
904
  self.logger.increment('failures')
423
- self.logger.debug('Update failed for %(obj)s %(path)s',
424
- {'obj': obj, 'path': update_path})
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
+ )
425
910
  if new_successes:
426
911
  update['successes'] = successes
427
912
  rewrite_pickle = True
@@ -436,14 +921,14 @@ class ObjectUpdater(Daemon):
436
921
  write_pickle(update, update_path, os.path.join(
437
922
  device, get_tmp_dir(policy)))
438
923
 
439
- def object_update(self, node, part, op, obj, headers_out):
924
+ def object_update(self, node, part, op, path, headers_out):
440
925
  """
441
926
  Perform the object update to the container
442
927
 
443
928
  :param node: node dictionary from the container ring
444
929
  :param part: partition that holds the container
445
930
  :param op: operation performed (ex: 'PUT' or 'DELETE')
446
- :param obj: object name being updated
931
+ :param path: /<acct>/<cont>/<obj> path being updated
447
932
  :param headers_out: headers to send with the update
448
933
  :return: a tuple of (``success``, ``node_id``, ``redirect``)
449
934
  where ``success`` is True if the update succeeded, ``node_id`` is
@@ -451,31 +936,59 @@ class ObjectUpdater(Daemon):
451
936
  tuple of (a path, a timestamp string).
452
937
  """
453
938
  redirect = None
939
+ start = time.time()
940
+ # Assume an error until we hear otherwise
941
+ status = 500
454
942
  try:
455
943
  with ConnectionTimeout(self.conn_timeout):
456
- conn = http_connect(node['ip'], node['port'], node['device'],
457
- part, op, obj, headers_out)
944
+ conn = http_connect(
945
+ node['replication_ip'], node['replication_port'],
946
+ node['device'], part, op, path, headers_out)
458
947
  with Timeout(self.node_timeout):
459
948
  resp = conn.getresponse()
460
949
  resp.read()
950
+ status = resp.status
461
951
 
462
- if resp.status == HTTP_MOVED_PERMANENTLY:
952
+ if status == HTTP_MOVED_PERMANENTLY:
463
953
  try:
464
954
  redirect = get_redirect_data(resp)
465
955
  except ValueError as err:
466
956
  self.logger.error(
467
957
  'Container update failed for %r; problem with '
468
- 'redirect location: %s' % (obj, err))
958
+ 'redirect location: %s' % (path, err))
469
959
 
470
- success = is_success(resp.status)
960
+ success = is_success(status)
471
961
  if not success:
472
962
  self.logger.debug(
473
- _('Error code %(status)d is returned from remote '
474
- 'server %(ip)s: %(port)s / %(device)s'),
475
- {'status': resp.status, 'ip': node['ip'],
476
- 'port': node['port'], 'device': node['device']})
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)})
477
967
  return success, node['id'], redirect
478
- except (Exception, Timeout):
479
- self.logger.exception(_('ERROR with remote server '
480
- '%(ip)s:%(port)s/%(device)s'), node)
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)
481
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()