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
@@ -155,31 +155,124 @@ All three steps may be performed with one sub-command::
155
155
  Run container-sharder on all nodes to shard the container.
156
156
 
157
157
  """
158
- from __future__ import print_function
159
158
  import argparse
160
159
  import json
161
160
  import os.path
162
161
  import sys
163
162
  import time
163
+ from contextlib import contextmanager
164
164
 
165
- from six.moves import input
166
-
167
- from swift.common.utils import Timestamp, get_logger, ShardRange
165
+ from swift.common.utils import Timestamp, get_logger, ShardRange, readconf, \
166
+ ShardRangeList, non_negative_int, config_positive_int_value
168
167
  from swift.container.backend import ContainerBroker, UNSHARDED
169
168
  from swift.container.sharder import make_shard_ranges, sharding_enabled, \
170
- CleavingContext
169
+ CleavingContext, process_compactible_shard_sequences, \
170
+ find_compactible_shard_sequences, find_overlapping_ranges, \
171
+ find_paths, rank_paths, finalize_shrinking, DEFAULT_SHARDER_CONF, \
172
+ ContainerSharderConf, find_paths_with_gaps, combine_shard_ranges, \
173
+ update_own_shard_range_stats
171
174
 
175
+ EXIT_SUCCESS = 0
176
+ EXIT_ERROR = 1
177
+ EXIT_INVALID_ARGS = 2 # consistent with argparse exit code for invalid args
178
+ EXIT_USER_QUIT = 3
172
179
 
173
- def _load_and_validate_shard_data(args):
174
- try:
180
+ MIN_SHARD_RANGE_AGE_FOR_REPAIR = 4 * 3600
181
+
182
+ # Some CLI options derive their default values from DEFAULT_SHARDER_CONF if
183
+ # they have not been set. It is therefore important that the CLI parser
184
+ # provides None as a default so that we can detect that no value was set on the
185
+ # command line. We use this alias to act as a reminder.
186
+ USE_SHARDER_DEFAULT = object()
187
+
188
+
189
+ class ManageShardRangesException(Exception):
190
+ pass
191
+
192
+
193
+ class GapsFoundException(ManageShardRangesException):
194
+ pass
195
+
196
+
197
+ class InvalidStateException(ManageShardRangesException):
198
+ pass
199
+
200
+
201
+ class InvalidSolutionException(ManageShardRangesException):
202
+ def __init__(self, msg, acceptor_path, overlapping_donors):
203
+ super(InvalidSolutionException, self).__init__(msg)
204
+ self.acceptor_path = acceptor_path
205
+ self.overlapping_donors = overlapping_donors
206
+
207
+
208
+ def wrap_for_argparse(func, msg=None):
209
+ """
210
+ Wrap the given ``func`` to catch any ``ValueError`` and raise an
211
+ ``argparse.ArgumentTypeError`` instead.
212
+
213
+ :param func: a function.
214
+ :param msg: an optional message to use with any exception that is used; if
215
+ not given then the string representation of the ValueError will be
216
+ used.
217
+ :return: a function wrapper.
218
+ """
219
+ def wrapped_func(*args, **kwargs):
220
+ try:
221
+ return func(*args, **kwargs)
222
+ except ValueError as err:
223
+ raise argparse.ArgumentTypeError(str(err) if msg is None else msg)
224
+ return wrapped_func
225
+
226
+
227
+ def _proceed(args):
228
+ if args.dry_run:
229
+ choice = 'no'
230
+ elif args.yes:
231
+ choice = 'yes'
232
+ else:
233
+ try:
234
+ choice = input('Do you want to apply these changes to the '
235
+ 'container DB? [yes/N]')
236
+ except (EOFError, KeyboardInterrupt):
237
+ choice = 'no'
238
+ if choice != 'yes':
239
+ print('No changes applied')
240
+
241
+ return choice == 'yes'
242
+
243
+
244
+ def _print_shard_range(sr, level=0):
245
+ indent = ' ' * level
246
+ print(indent + '%r' % sr.name)
247
+ print(indent + ' objects: %9d, tombstones: %9d, lower: %r'
248
+ % (sr.object_count, sr.tombstones, sr.lower_str))
249
+ print(indent + ' state: %9s, deleted: %d upper: %r'
250
+ % (sr.state_text, sr.deleted, sr.upper_str))
251
+
252
+
253
+ @contextmanager
254
+ def _open_input(args):
255
+ if args.input == '-':
256
+ args.input = '<STDIN>'
257
+ yield sys.stdin
258
+ else:
175
259
  with open(args.input, 'r') as fd:
260
+ yield fd
261
+
262
+
263
+ def _load_and_validate_shard_data(args, require_index=True):
264
+ required_keys = ['lower', 'upper', 'object_count']
265
+ if require_index:
266
+ required_keys.append('index')
267
+ try:
268
+ with _open_input(args) as fd:
176
269
  try:
177
270
  data = json.load(fd)
178
271
  if not isinstance(data, list):
179
272
  raise ValueError('Shard data must be a list of dicts')
180
- for k in ('lower', 'upper', 'index', 'object_count'):
273
+ for k in required_keys:
181
274
  for shard in data:
182
- shard[k]
275
+ shard[k] # trigger KeyError for missing required key
183
276
  return data
184
277
  except (TypeError, ValueError, KeyError) as err:
185
278
  print('Failed to load valid shard range data: %r' % err,
@@ -209,7 +302,7 @@ def _check_shard_ranges(own_shard_range, shard_ranges):
209
302
  if reasons:
210
303
  print('WARNING: invalid shard ranges: %s.' % reasons)
211
304
  print('Aborting.')
212
- exit(2)
305
+ exit(EXIT_ERROR)
213
306
 
214
307
 
215
308
  def _check_own_shard_range(broker, args):
@@ -229,7 +322,8 @@ def _find_ranges(broker, args, status_file=None):
229
322
  start = last_report = time.time()
230
323
  limit = 5 if status_file else -1
231
324
  shard_data, last_found = broker.find_shard_ranges(
232
- args.rows_per_shard, limit=limit)
325
+ args.rows_per_shard, limit=limit,
326
+ minimum_shard_size=args.minimum_shard_size)
233
327
  if shard_data:
234
328
  while not last_found:
235
329
  if last_report + 10 < time.time():
@@ -239,7 +333,8 @@ def _find_ranges(broker, args, status_file=None):
239
333
  # prefix doesn't matter since we aren't persisting it
240
334
  found_ranges = make_shard_ranges(broker, shard_data, '.shards_')
241
335
  more_shard_data, last_found = broker.find_shard_ranges(
242
- args.rows_per_shard, existing_ranges=found_ranges, limit=5)
336
+ args.rows_per_shard, existing_ranges=found_ranges, limit=5,
337
+ minimum_shard_size=args.minimum_shard_size)
243
338
  shard_data.extend(more_shard_data)
244
339
  return shard_data, time.time() - start
245
340
 
@@ -251,11 +346,12 @@ def find_ranges(broker, args):
251
346
  (len(shard_data), delta_t,
252
347
  sum(r['object_count'] for r in shard_data)),
253
348
  file=sys.stderr)
254
- return 0
349
+ return EXIT_SUCCESS
255
350
 
256
351
 
257
352
  def show_shard_ranges(broker, args):
258
353
  shard_ranges = broker.get_shard_ranges(
354
+ includes=getattr(args, 'includes', None),
259
355
  include_deleted=getattr(args, 'include_deleted', False))
260
356
  shard_data = [dict(sr, state=sr.state_text)
261
357
  for sr in shard_ranges]
@@ -269,7 +365,7 @@ def show_shard_ranges(broker, args):
269
365
  else:
270
366
  print("Existing shard ranges:", file=sys.stderr)
271
367
  print(json.dumps(shard_data, sort_keys=True, indent=2))
272
- return 0
368
+ return EXIT_SUCCESS
273
369
 
274
370
 
275
371
  def db_info(broker, args):
@@ -281,6 +377,9 @@ def db_info(broker, args):
281
377
  if own_sr else None))
282
378
  db_state = broker.get_db_state()
283
379
  print('db_state = %s' % db_state)
380
+ info = broker.get_info()
381
+ print('object_count = %d' % info['object_count'])
382
+ print('bytes_used = %d' % info['bytes_used'])
284
383
  if db_state == 'sharding':
285
384
  print('Retiring db id: %s' % broker.get_brokers()[0].get_info()['id'])
286
385
  print('Cleaving context: %s' %
@@ -289,13 +388,14 @@ def db_info(broker, args):
289
388
  print('Metadata:')
290
389
  for k, (v, t) in broker.metadata.items():
291
390
  print(' %s = %s' % (k, v))
391
+ return EXIT_SUCCESS
292
392
 
293
393
 
294
394
  def delete_shard_ranges(broker, args):
295
395
  shard_ranges = broker.get_shard_ranges()
296
396
  if not shard_ranges:
297
397
  print("No shard ranges found to delete.")
298
- return 0
398
+ return EXIT_SUCCESS
299
399
 
300
400
  while not args.force:
301
401
  print('This will delete existing %d shard ranges.' % len(shard_ranges))
@@ -307,14 +407,18 @@ def delete_shard_ranges(broker, args):
307
407
  print(' - %d existing shard ranges have started sharding' %
308
408
  [sr.state != ShardRange.FOUND
309
409
  for sr in shard_ranges].count(True))
310
- choice = input('Do you want to show the existing ranges [s], '
311
- 'delete the existing ranges [yes] '
312
- 'or quit without deleting [q]? ')
410
+ try:
411
+ choice = input('Do you want to show the existing ranges [s], '
412
+ 'delete the existing ranges [yes] '
413
+ 'or quit without deleting [q]? ')
414
+ except (EOFError, KeyboardInterrupt):
415
+ choice = 'q'
416
+
313
417
  if choice == 's':
314
418
  show_shard_ranges(broker, args)
315
419
  continue
316
420
  elif choice == 'q':
317
- return 1
421
+ return EXIT_USER_QUIT
318
422
  elif choice == 'yes':
319
423
  break
320
424
  else:
@@ -327,7 +431,41 @@ def delete_shard_ranges(broker, args):
327
431
  sr.timestamp = now
328
432
  broker.merge_shard_ranges(shard_ranges)
329
433
  print('Deleted %s existing shard ranges.' % len(shard_ranges))
330
- return 0
434
+ return EXIT_SUCCESS
435
+
436
+
437
+ def merge_shard_ranges(broker, args):
438
+ _check_own_shard_range(broker, args)
439
+ shard_data = _load_and_validate_shard_data(args, require_index=False)
440
+ new_shard_ranges = ShardRangeList([ShardRange.from_dict(sr)
441
+ for sr in shard_data])
442
+ new_shard_ranges.sort(key=ShardRange.sort_key)
443
+
444
+ # do some checks before merging...
445
+ existing_shard_ranges = ShardRangeList(
446
+ broker.get_shard_ranges(include_deleted=True))
447
+ outcome = combine_shard_ranges(new_shard_ranges, existing_shard_ranges)
448
+ if args.verbose:
449
+ print('This change will result in the following shard ranges in the '
450
+ 'affected namespace:')
451
+ print(json.dumps([dict(sr) for sr in outcome], indent=2))
452
+ overlaps = find_overlapping_ranges(outcome)
453
+ if overlaps:
454
+ print('WARNING: this change will result in shard ranges overlaps!')
455
+ paths_with_gaps = find_paths_with_gaps(outcome)
456
+ gaps = [gap for start_path, gap, end_path in paths_with_gaps
457
+ if existing_shard_ranges.includes(gap)]
458
+ if gaps:
459
+ print('WARNING: this change will result in shard ranges gaps!')
460
+
461
+ if not _proceed(args):
462
+ return EXIT_USER_QUIT
463
+
464
+ with broker.updated_timeout(args.replace_timeout):
465
+ broker.merge_shard_ranges(new_shard_ranges)
466
+ print('Injected %d shard ranges.' % len(new_shard_ranges))
467
+ print('Run container-replicator to replicate them to other nodes.')
468
+ return EXIT_SUCCESS
331
469
 
332
470
 
333
471
  def _replace_shard_ranges(broker, args, shard_data, timeout=0):
@@ -344,7 +482,7 @@ def _replace_shard_ranges(broker, args, shard_data, timeout=0):
344
482
  # Crank up the timeout in an effort to *make sure* this succeeds
345
483
  with broker.updated_timeout(max(timeout, args.replace_timeout)):
346
484
  delete_status = delete_shard_ranges(broker, args)
347
- if delete_status != 0:
485
+ if delete_status != EXIT_SUCCESS:
348
486
  return delete_status
349
487
  broker.merge_shard_ranges(shard_ranges)
350
488
 
@@ -354,7 +492,7 @@ def _replace_shard_ranges(broker, args, shard_data, timeout=0):
354
492
  return enable_sharding(broker, args)
355
493
  else:
356
494
  print('Use the enable sub-command to enable sharding.')
357
- return 0
495
+ return EXIT_SUCCESS
358
496
 
359
497
 
360
498
  def replace_shard_ranges(broker, args):
@@ -373,6 +511,8 @@ def _enable_sharding(broker, own_shard_range, args):
373
511
  if own_shard_range.update_state(ShardRange.SHARDING):
374
512
  own_shard_range.epoch = Timestamp.now()
375
513
  own_shard_range.state_timestamp = own_shard_range.epoch
514
+ # initialise own_shard_range with current broker object stats...
515
+ update_own_shard_range_stats(broker, own_shard_range)
376
516
 
377
517
  with broker.updated_timeout(args.enable_timeout):
378
518
  broker.merge_shard_ranges([own_shard_range])
@@ -404,20 +544,381 @@ def enable_sharding(broker, args):
404
544
  print('WARNING: container in state %s (should be active or sharding).'
405
545
  % own_shard_range.state_text)
406
546
  print('Aborting.')
407
- return 2
547
+ return EXIT_ERROR
408
548
 
409
549
  print('Run container-sharder on all nodes to shard the container.')
410
- return 0
550
+ return EXIT_SUCCESS
551
+
552
+
553
+ def compact_shard_ranges(broker, args):
554
+ if not broker.is_root_container():
555
+ print('WARNING: Shard containers cannot be compacted.')
556
+ print('This command should be used on a root container.')
557
+ return EXIT_ERROR
558
+
559
+ if not broker.is_sharded():
560
+ print('WARNING: Container is not yet sharded so cannot be compacted.')
561
+ return EXIT_ERROR
562
+
563
+ shard_ranges = broker.get_shard_ranges()
564
+ if find_overlapping_ranges([sr for sr in shard_ranges if
565
+ sr.state != ShardRange.SHRINKING]):
566
+ print('WARNING: Container has overlapping shard ranges so cannot be '
567
+ 'compacted.')
568
+ return EXIT_ERROR
569
+
570
+ compactible = find_compactible_shard_sequences(broker,
571
+ args.shrink_threshold,
572
+ args.expansion_limit,
573
+ args.max_shrinking,
574
+ args.max_expanding)
575
+ if not compactible:
576
+ print('No shards identified for compaction.')
577
+ return EXIT_SUCCESS
578
+
579
+ for sequence in compactible:
580
+ if sequence[-1].state not in (ShardRange.ACTIVE, ShardRange.SHARDED):
581
+ print('ERROR: acceptor not in correct state: %s' % sequence[-1],
582
+ file=sys.stderr)
583
+ return EXIT_ERROR
584
+
585
+ for sequence in compactible:
586
+ acceptor = sequence[-1]
587
+ donors = sequence[:-1]
588
+ print('Donor shard range(s) with total of %d rows:'
589
+ % donors.row_count)
590
+ for donor in donors:
591
+ _print_shard_range(donor, level=1)
592
+ print('can be compacted into acceptor shard range:')
593
+ _print_shard_range(acceptor, level=1)
594
+ print('Total of %d shard sequences identified for compaction.'
595
+ % len(compactible))
596
+ print('Once applied to the broker these changes will result in shard '
597
+ 'range compaction the next time the sharder runs.')
598
+
599
+ if not _proceed(args):
600
+ return EXIT_USER_QUIT
601
+
602
+ process_compactible_shard_sequences(broker, compactible)
603
+ print('Updated %s shard sequences for compaction.' % len(compactible))
604
+ print('Run container-replicator to replicate the changes to other '
605
+ 'nodes.')
606
+ print('Run container-sharder on all nodes to compact shards.')
607
+ return EXIT_SUCCESS
608
+
609
+
610
+ def _remove_illegal_overlapping_donors(
611
+ acceptor_path, overlapping_donors, args):
612
+ # Check parent-children relationship in overlaps between acceptors and
613
+ # donors, remove any overlapping parent or child shard range from donors.
614
+ # Note: we can use set() here, since shard range object is hashed by
615
+ # id and all shard ranges in overlapping_donors are unique already.
616
+ parent_child_donors = set()
617
+ for acceptor in acceptor_path:
618
+ parent_child_donors.update(
619
+ [donor for donor in overlapping_donors
620
+ if acceptor.is_child_of(donor) or donor.is_child_of(acceptor)])
621
+ if parent_child_donors:
622
+ overlapping_donors = ShardRangeList(
623
+ [sr for sr in overlapping_donors
624
+ if sr not in parent_child_donors])
625
+ print('%d donor shards ignored due to parent-child relationship '
626
+ 'checks' % len(parent_child_donors))
627
+
628
+ # Check minimum age requirement in overlaps between acceptors and donors.
629
+ if args.min_shard_age == 0:
630
+ return acceptor_path, overlapping_donors
631
+ ts_now = Timestamp.now()
632
+ # Remove overlapping donor shard ranges who were created recently within
633
+ # 'min_shard_age' age limit.
634
+ qualified_donors = ShardRangeList(
635
+ [sr for sr in overlapping_donors
636
+ if float(sr.timestamp) + args.min_shard_age < float(ts_now)])
637
+ young_donors = len(overlapping_donors) - len(qualified_donors)
638
+ if young_donors > 0:
639
+ print('%d overlapping donor shards ignored due to minimum age '
640
+ 'limit' % young_donors)
641
+ if not qualified_donors:
642
+ return acceptor_path, None
643
+ # Remove those overlapping donors whose overlapping acceptors were created
644
+ # within age limit.
645
+ donors_with_young_overlap_acceptor = set()
646
+ for acceptor_sr in acceptor_path:
647
+ if float(acceptor_sr.timestamp) + args.min_shard_age < float(ts_now):
648
+ continue
649
+ donors_with_young_overlap_acceptor.update(
650
+ [sr for sr in qualified_donors if acceptor_sr.overlaps(sr)])
651
+ if donors_with_young_overlap_acceptor:
652
+ qualified_donors = ShardRangeList(
653
+ [sr for sr in qualified_donors
654
+ if sr not in donors_with_young_overlap_acceptor])
655
+ print('%d donor shards ignored due to existence of overlapping young '
656
+ 'acceptors' % len(donors_with_young_overlap_acceptor))
657
+
658
+ return acceptor_path, qualified_donors
659
+
660
+
661
+ def _find_overlapping_donors(shard_ranges, own_sr, args):
662
+ shard_ranges = ShardRangeList(shard_ranges)
663
+ if ShardRange.SHARDING in shard_ranges.states:
664
+ # This may be over-cautious, but for now we'll avoid dealing with
665
+ # SHARDING shards (which by design will temporarily overlap with their
666
+ # sub-shards) and require repair to be re-tried once sharding has
667
+ # completed. Note that once a shard ranges moves from SHARDING to
668
+ # SHARDED state and is deleted, some replicas of the shard may still be
669
+ # in the process of sharding but we cannot detect that at the root.
670
+ raise InvalidStateException('Found shard ranges in sharding state')
671
+ if ShardRange.SHRINKING in shard_ranges.states:
672
+ # Also stop now if there are SHRINKING shard ranges: we would need to
673
+ # ensure that these were not chosen as acceptors, but for now it is
674
+ # simpler to require repair to be re-tried once shrinking has
675
+ # completes.
676
+ raise InvalidStateException('Found shard ranges in shrinking state')
677
+
678
+ paths = find_paths(shard_ranges)
679
+ ranked_paths = rank_paths(paths, own_sr)
680
+ if not (ranked_paths and ranked_paths[0].includes(own_sr)):
681
+ # individual paths do not have gaps within them; if no path spans the
682
+ # entire namespace then there must be a gap in the shard_ranges
683
+ raise GapsFoundException
684
+
685
+ # simple repair strategy: choose the highest ranked complete sequence and
686
+ # shrink all other shard ranges into it
687
+ acceptor_path = ranked_paths[0]
688
+ acceptor_names = set(sr.name for sr in acceptor_path)
689
+ overlapping_donors = ShardRangeList([sr for sr in shard_ranges
690
+ if sr.name not in acceptor_names])
691
+
692
+ # check that the solution makes sense: if the acceptor path has the most
693
+ # progressed continuous cleaving, which has reached cleaved_upper, then we
694
+ # don't expect any shard ranges beyond cleaved_upper to be in states
695
+ # CLEAVED or ACTIVE, otherwise there should have been a better acceptor
696
+ # path that reached them.
697
+ cleaved_states = {ShardRange.CLEAVED, ShardRange.ACTIVE}
698
+ cleaved_upper = acceptor_path.find_lower(
699
+ lambda sr: sr.state not in cleaved_states)
700
+ beyond_cleaved = acceptor_path.filter(marker=cleaved_upper)
701
+ if beyond_cleaved.states.intersection(cleaved_states):
702
+ raise InvalidSolutionException(
703
+ 'Isolated cleaved and/or active shard ranges in acceptor path',
704
+ acceptor_path, overlapping_donors)
705
+ beyond_cleaved = overlapping_donors.filter(marker=cleaved_upper)
706
+ if beyond_cleaved.states.intersection(cleaved_states):
707
+ raise InvalidSolutionException(
708
+ 'Isolated cleaved and/or active shard ranges in donor ranges',
709
+ acceptor_path, overlapping_donors)
710
+
711
+ return _remove_illegal_overlapping_donors(
712
+ acceptor_path, overlapping_donors, args)
713
+
714
+
715
+ def _fix_gaps(broker, args, paths_with_gaps):
716
+ timestamp = Timestamp.now()
717
+ solutions = []
718
+ print('Found %d gaps:' % len(paths_with_gaps))
719
+ for start_path, gap_range, end_path in paths_with_gaps:
720
+ if end_path[0].state == ShardRange.ACTIVE:
721
+ expanding_range = end_path[0]
722
+ solutions.append((gap_range, expanding_range))
723
+ elif start_path[-1].state == ShardRange.ACTIVE:
724
+ expanding_range = start_path[-1]
725
+ solutions.append((gap_range, expanding_range))
726
+ else:
727
+ expanding_range = None
728
+ print(' gap: %r - %r'
729
+ % (gap_range.lower, gap_range.upper))
730
+ print(' apparent gap contents:')
731
+ for sr in broker.get_shard_ranges(marker=gap_range.lower,
732
+ end_marker=gap_range.upper,
733
+ include_deleted=True):
734
+ _print_shard_range(sr, 3)
735
+ if expanding_range:
736
+ print(' gap can be fixed by expanding neighbor range:')
737
+ _print_shard_range(expanding_range, 3)
738
+ else:
739
+ print('Warning: cannot fix gap: non-ACTIVE neighbors')
740
+
741
+ if args.max_expanding >= 0:
742
+ solutions = solutions[:args.max_expanding]
743
+
744
+ # it's possible that an expanding range is used twice, expanding both down
745
+ # and up; if so, we only want one copy of it in our merged shard ranges
746
+ expanding_ranges = {}
747
+ for gap_range, expanding_range in solutions:
748
+ expanding_range.expand([gap_range])
749
+ expanding_range.timestamp = timestamp
750
+ expanding_ranges[expanding_range.name] = expanding_range
751
+
752
+ print('')
753
+ print('Repairs necessary to fill gaps.')
754
+ print('The following expanded shard range(s) will be applied to the DB:')
755
+ for expanding_range in sorted(expanding_ranges.values(),
756
+ key=lambda s: s.lower):
757
+ _print_shard_range(expanding_range, 2)
758
+ print('')
759
+ print(
760
+ 'It is recommended that no other concurrent changes are made to the \n'
761
+ 'shard ranges while fixing gaps. If necessary, abort this change \n'
762
+ 'and stop any auto-sharding processes before repeating this command.'
763
+ )
764
+ print('')
765
+
766
+ if not _proceed(args):
767
+ return EXIT_USER_QUIT
768
+
769
+ broker.merge_shard_ranges(list(expanding_ranges.values()))
770
+ print('Run container-replicator to replicate the changes to other nodes.')
771
+ print('Run container-sharder on all nodes to fill gaps.')
772
+ return EXIT_SUCCESS
773
+
774
+
775
+ def repair_gaps(broker, args):
776
+ shard_ranges = broker.get_shard_ranges()
777
+ paths_with_gaps = find_paths_with_gaps(shard_ranges)
778
+ if paths_with_gaps:
779
+ return _fix_gaps(broker, args, paths_with_gaps)
780
+ else:
781
+ print('Found one complete sequence of %d shard ranges with no gaps.'
782
+ % len(shard_ranges))
783
+ print('No repairs necessary.')
784
+ return EXIT_SUCCESS
785
+
786
+
787
+ def print_repair_solution(acceptor_path, overlapping_donors):
788
+ print('Donors:')
789
+ for donor in sorted(overlapping_donors):
790
+ _print_shard_range(donor, level=1)
791
+ print('Acceptors:')
792
+ for acceptor in acceptor_path:
793
+ _print_shard_range(acceptor, level=1)
794
+
795
+
796
+ def find_repair_solution(shard_ranges, own_sr, args):
797
+ try:
798
+ acceptor_path, overlapping_donors = _find_overlapping_donors(
799
+ shard_ranges, own_sr, args)
800
+ except GapsFoundException:
801
+ print('Found no complete sequence of shard ranges.')
802
+ print('Repairs necessary to fill gaps.')
803
+ print('Gap filling not supported by this tool. No repairs performed.')
804
+ raise
805
+ except InvalidStateException as exc:
806
+ print('WARNING: %s' % exc)
807
+ print('No repairs performed.')
808
+ raise
809
+ except InvalidSolutionException as exc:
810
+ print('ERROR: %s' % exc)
811
+ print_repair_solution(exc.acceptor_path, exc.overlapping_donors)
812
+ print('No repairs performed.')
813
+ raise
814
+
815
+ if not overlapping_donors:
816
+ print('Found one complete sequence of %d shard ranges and no '
817
+ 'overlapping shard ranges.' % len(acceptor_path))
818
+ print('No repairs necessary.')
819
+ return None, None
820
+
821
+ print('Repairs necessary to remove overlapping shard ranges.')
822
+ print('Chosen a complete sequence of %d shard ranges with current total '
823
+ 'of %d object records to accept object records from %d overlapping '
824
+ 'donor shard ranges.' %
825
+ (len(acceptor_path), acceptor_path.object_count,
826
+ len(overlapping_donors)))
827
+ if args.verbose:
828
+ print_repair_solution(acceptor_path, overlapping_donors)
829
+
830
+ print('Once applied to the broker these changes will result in:')
831
+ print(' %d shard ranges being removed.' % len(overlapping_donors))
832
+ print(' %d object records being moved to the chosen shard ranges.'
833
+ % overlapping_donors.object_count)
834
+
835
+ return acceptor_path, overlapping_donors
836
+
837
+
838
+ def repair_overlaps(broker, args):
839
+ shard_ranges = broker.get_shard_ranges()
840
+ if not shard_ranges:
841
+ print('No shards found, nothing to do.')
842
+ return EXIT_SUCCESS
843
+
844
+ own_sr = broker.get_own_shard_range()
845
+ try:
846
+ acceptor_path, overlapping_donors = find_repair_solution(
847
+ shard_ranges, own_sr, args)
848
+ except ManageShardRangesException:
849
+ return EXIT_ERROR
850
+
851
+ if not acceptor_path:
852
+ return EXIT_SUCCESS
853
+
854
+ if not _proceed(args):
855
+ return EXIT_USER_QUIT
856
+
857
+ # merge changes to the broker...
858
+ # note: acceptors do not need to be modified since they already span the
859
+ # complete range
860
+ ts_now = Timestamp.now()
861
+ finalize_shrinking(broker, [], overlapping_donors, ts_now)
862
+ print('Updated %s donor shard ranges.' % len(overlapping_donors))
863
+ print('Run container-replicator to replicate the changes to other nodes.')
864
+ print('Run container-sharder on all nodes to repair shards.')
865
+ return EXIT_SUCCESS
866
+
867
+
868
+ def repair_shard_ranges(broker, args):
869
+ if not broker.is_root_container():
870
+ print('WARNING: Shard containers cannot be repaired.')
871
+ print('This command should be used on a root container.')
872
+ return EXIT_ERROR
873
+ if args.gaps:
874
+ return repair_gaps(broker, args)
875
+ else:
876
+ return repair_overlaps(broker, args)
877
+
878
+
879
+ def analyze_shard_ranges(args):
880
+ shard_data = _load_and_validate_shard_data(args, require_index=False)
881
+ for data in shard_data:
882
+ # allow for incomplete shard range data that may have been scraped from
883
+ # swift-container-info output
884
+ data.setdefault('epoch', None)
885
+ shard_ranges = [ShardRange.from_dict(data) for data in shard_data]
886
+ whole_sr = ShardRange('whole/namespace', 0)
887
+ try:
888
+ find_repair_solution(shard_ranges, whole_sr, args)
889
+ except ManageShardRangesException:
890
+ return EXIT_ERROR
891
+ return EXIT_SUCCESS
411
892
 
412
893
 
413
894
  def _add_find_args(parser):
414
- parser.add_argument('rows_per_shard', nargs='?', type=int, default=500000)
895
+ parser.add_argument(
896
+ 'rows_per_shard', nargs='?', type=int, default=USE_SHARDER_DEFAULT,
897
+ help='Target number of rows for newly created shards. '
898
+ 'Default is half of the shard_container_threshold value if that is '
899
+ 'given in a conf file specified with --config, otherwise %s.'
900
+ % DEFAULT_SHARDER_CONF['rows_per_shard'])
901
+ parser.add_argument(
902
+ '--minimum-shard-size',
903
+ type=wrap_for_argparse(config_positive_int_value, 'must be > 0'),
904
+ default=USE_SHARDER_DEFAULT,
905
+ help='Minimum size of the final shard range. If this is greater than '
906
+ 'one then the final shard range may be extended to more than '
907
+ 'rows_per_shard in order to avoid a further shard range with less '
908
+ 'than minimum-shard-size rows.')
415
909
 
416
910
 
417
- def _add_replace_args(parser):
911
+ def _add_account_prefix_arg(parser):
418
912
  parser.add_argument(
419
913
  '--shards_account_prefix', metavar='shards_account_prefix', type=str,
420
- required=False, help='Prefix for shards account', default='.shards_')
914
+ required=False, default='.shards_',
915
+ help="Prefix for shards account. The default is '.shards_'. This "
916
+ "should only be changed if the auto_create_account_prefix option "
917
+ "has been similarly changed in swift.conf.")
918
+
919
+
920
+ def _add_replace_args(parser):
921
+ _add_account_prefix_arg(parser)
421
922
  parser.add_argument(
422
923
  '--replace-timeout', type=int, default=600,
423
924
  help='Minimum DB timeout to use when replacing shard ranges.')
@@ -435,11 +936,51 @@ def _add_enable_args(parser):
435
936
  help='DB timeout to use when enabling sharding.')
436
937
 
437
938
 
939
+ def _add_prompt_args(parser):
940
+ group = parser.add_mutually_exclusive_group()
941
+ group.add_argument(
942
+ '--yes', '-y', action='store_true', default=False,
943
+ help='Apply shard range changes to broker without prompting. '
944
+ 'Cannot be used with --dry-run option.')
945
+ group.add_argument(
946
+ '--dry-run', '-n', action='store_true', default=False,
947
+ help='Do not apply any shard range changes to broker. '
948
+ 'Cannot be used with --yes option.')
949
+
950
+
951
+ def _add_max_expanding_arg(parser):
952
+ parser.add_argument(
953
+ '--max-expanding', nargs='?',
954
+ type=wrap_for_argparse(config_positive_int_value, 'must be > 0'),
955
+ default=USE_SHARDER_DEFAULT,
956
+ help='Maximum number of shards that should be '
957
+ 'expanded. Defaults to unlimited.')
958
+
959
+
438
960
  def _make_parser():
439
961
  parser = argparse.ArgumentParser(description='Manage shard ranges')
440
- parser.add_argument('container_db')
962
+ parser.add_argument('path_to_file',
963
+ help='Path to a container DB file or, for the analyze '
964
+ 'subcommand, a shard data file.')
965
+ parser.add_argument('--config', dest='conf_file', required=False,
966
+ help='Path to config file with [container-sharder] '
967
+ 'section. The following subcommand options will '
968
+ 'be loaded from a config file if they are not '
969
+ 'given on the command line: '
970
+ 'rows_per_shard, '
971
+ 'max_shrinking, '
972
+ 'max_expanding, '
973
+ 'shrink_threshold, '
974
+ 'expansion_limit')
441
975
  parser.add_argument('--verbose', '-v', action='count', default=0,
442
976
  help='Increase output verbosity')
977
+ # this is useful for probe tests that shard containers with unrealistically
978
+ # low numbers of objects, of which a significant proportion may still be in
979
+ # the pending file
980
+ parser.add_argument(
981
+ '--force-commits', action='store_true', default=False,
982
+ help='Force broker to commit pending object updates before finding '
983
+ 'shard ranges. By default the broker will skip commits.')
443
984
  subparsers = parser.add_subparsers(
444
985
  dest='subcommand', help='Sub-command help', title='Sub-commands')
445
986
 
@@ -466,6 +1007,8 @@ def _make_parser():
466
1007
  show_parser.add_argument(
467
1008
  '--brief', '-b', action='store_true', default=False,
468
1009
  help='Show only shard range bounds in output.')
1010
+ show_parser.add_argument('--includes',
1011
+ help='limit shard ranges to include key')
469
1012
  show_parser.set_defaults(func=show_shard_ranges)
470
1013
 
471
1014
  # info
@@ -473,6 +1016,22 @@ def _make_parser():
473
1016
  'info', help='Print container db info')
474
1017
  info_parser.set_defaults(func=db_info)
475
1018
 
1019
+ # merge
1020
+ merge_parser = subparsers.add_parser(
1021
+ 'merge',
1022
+ help='Merge shard range(s) from file with existing shard ranges. This '
1023
+ 'subcommand should only be used if you are confident that you '
1024
+ 'know what you are doing. Shard ranges should not typically be '
1025
+ 'modified in this way.')
1026
+ merge_parser.add_argument('input', metavar='input_file',
1027
+ type=str, help='Name of file')
1028
+ merge_parser.add_argument(
1029
+ '--replace-timeout', type=int, default=600,
1030
+ help='Minimum DB timeout to use when merging shard ranges.')
1031
+ _add_account_prefix_arg(merge_parser)
1032
+ _add_prompt_args(merge_parser)
1033
+ merge_parser.set_defaults(func=merge_shard_ranges)
1034
+
476
1035
  # replace
477
1036
  replace_parser = subparsers.add_parser(
478
1037
  'replace',
@@ -500,12 +1059,89 @@ def _make_parser():
500
1059
  _add_enable_args(enable_parser)
501
1060
  enable_parser.set_defaults(func=enable_sharding)
502
1061
  _add_replace_args(enable_parser)
1062
+
1063
+ # compact
1064
+ compact_parser = subparsers.add_parser(
1065
+ 'compact',
1066
+ help='Compact shard ranges with less than the shrink-threshold number '
1067
+ 'of rows. This command only works on root containers.')
1068
+ _add_prompt_args(compact_parser)
1069
+ compact_parser.add_argument(
1070
+ '--shrink-threshold', nargs='?',
1071
+ type=wrap_for_argparse(config_positive_int_value, 'must be > 0'),
1072
+ default=USE_SHARDER_DEFAULT,
1073
+ help='The number of rows below which a shard can qualify for '
1074
+ 'shrinking. '
1075
+ 'Defaults to %d' % DEFAULT_SHARDER_CONF['shrink_threshold'])
1076
+ compact_parser.add_argument(
1077
+ '--expansion-limit', nargs='?',
1078
+ type=wrap_for_argparse(config_positive_int_value, 'must be > 0'),
1079
+ default=USE_SHARDER_DEFAULT,
1080
+ help='Maximum number of rows for an expanding shard to have after '
1081
+ 'compaction has completed. '
1082
+ 'Defaults to %d' % DEFAULT_SHARDER_CONF['expansion_limit'])
1083
+ # If just one donor shard is chosen to shrink to an acceptor then the
1084
+ # expanded acceptor will handle object listings as soon as the donor shard
1085
+ # has shrunk. If more than one donor shard are chosen to shrink to an
1086
+ # acceptor then the acceptor may not handle object listings for some donor
1087
+ # shards that have shrunk until *all* donors have shrunk, resulting in
1088
+ # temporary gap(s) in object listings where the shrunk donors are missing.
1089
+ compact_parser.add_argument(
1090
+ '--max-shrinking', nargs='?',
1091
+ type=wrap_for_argparse(config_positive_int_value, 'must be > 0'),
1092
+ default=USE_SHARDER_DEFAULT,
1093
+ help='Maximum number of shards that should be '
1094
+ 'shrunk into each expanding shard. '
1095
+ 'Defaults to 1. Using values greater '
1096
+ 'than 1 may result in temporary gaps in '
1097
+ 'object listings until all selected '
1098
+ 'shards have shrunk.')
1099
+ _add_max_expanding_arg(compact_parser)
1100
+ compact_parser.set_defaults(func=compact_shard_ranges)
1101
+
1102
+ # repair
1103
+ repair_parser = subparsers.add_parser(
1104
+ 'repair',
1105
+ help='Repair overlapping shard ranges. No action will be taken '
1106
+ 'without user confirmation unless the -y option is used.')
1107
+ _add_prompt_args(repair_parser)
1108
+ repair_parser.add_argument(
1109
+ '--min-shard-age', nargs='?',
1110
+ type=wrap_for_argparse(non_negative_int, 'must be >= 0'),
1111
+ default=MIN_SHARD_RANGE_AGE_FOR_REPAIR,
1112
+ help='Minimum age of a shard for it to be considered as an overlap '
1113
+ 'that is due for repair. Overlapping shards younger than this '
1114
+ 'age will be ignored. Value of 0 means no recent shards will be '
1115
+ 'ignored. Defaults to %d.' % MIN_SHARD_RANGE_AGE_FOR_REPAIR)
1116
+ # TODO: maybe this should be a separate subcommand given that it needs
1117
+ # some extra options vs repairing overlaps?
1118
+ repair_parser.add_argument(
1119
+ '--gaps', action='store_true', default=False,
1120
+ help='Repair gaps in shard ranges.')
1121
+ _add_max_expanding_arg(repair_parser)
1122
+ repair_parser.set_defaults(func=repair_shard_ranges)
1123
+
1124
+ # analyze
1125
+ analyze_parser = subparsers.add_parser(
1126
+ 'analyze',
1127
+ help='Analyze shard range json data read from file. Use -v to see '
1128
+ 'more detailed analysis.')
1129
+ analyze_parser.add_argument(
1130
+ '--min-shard-age', nargs='?',
1131
+ type=wrap_for_argparse(non_negative_int, 'must be >= 0'),
1132
+ default=0,
1133
+ help='Minimum age of a shard for it to be considered as an overlap '
1134
+ 'that is due for repair. Overlapping shards younger than this '
1135
+ 'age will be ignored. Value of 0 means no recent shards will be '
1136
+ 'ignored. Defaults to 0.')
1137
+ analyze_parser.set_defaults(func=analyze_shard_ranges)
1138
+
503
1139
  return parser
504
1140
 
505
1141
 
506
- def main(args=None):
1142
+ def main(cli_args=None):
507
1143
  parser = _make_parser()
508
- args = parser.parse_args(args)
1144
+ args = parser.parse_args(cli_args)
509
1145
  if not args.subcommand:
510
1146
  # On py2, subparsers are required; on py3 they are not; see
511
1147
  # https://bugs.python.org/issue9253. py37 added a `required` kwarg
@@ -513,18 +1149,50 @@ def main(args=None):
513
1149
  # the matter. So, check whether the destination was set and bomb
514
1150
  # out if not.
515
1151
  parser.print_help()
516
- print('\nA sub-command is required.')
517
- return 1
1152
+ print('\nA sub-command is required.', file=sys.stderr)
1153
+ return EXIT_INVALID_ARGS
1154
+
1155
+ try:
1156
+ conf = {}
1157
+ if args.conf_file:
1158
+ conf = readconf(args.conf_file, 'container-sharder')
1159
+ conf.update(dict((k, v) for k, v in vars(args).items()
1160
+ if v != USE_SHARDER_DEFAULT))
1161
+ conf_args = ContainerSharderConf(conf)
1162
+ except (OSError, IOError) as exc:
1163
+ print('Error opening config file %s: %s' % (args.conf_file, exc),
1164
+ file=sys.stderr)
1165
+ return EXIT_ERROR
1166
+ except (TypeError, ValueError) as exc:
1167
+ print('Error loading config: %s' % exc, file=sys.stderr)
1168
+ return EXIT_INVALID_ARGS
1169
+
1170
+ for k, v in vars(args).items():
1171
+ # set any un-set cli args from conf_args
1172
+ if v is USE_SHARDER_DEFAULT:
1173
+ setattr(args, k, getattr(conf_args, k))
1174
+
1175
+ try:
1176
+ ContainerSharderConf.validate_conf(args)
1177
+ except ValueError as err:
1178
+ print('Invalid config: %s' % err, file=sys.stderr)
1179
+ return EXIT_INVALID_ARGS
1180
+
1181
+ if args.func in (analyze_shard_ranges,):
1182
+ args.input = args.path_to_file
1183
+ return args.func(args) or 0
1184
+
518
1185
  logger = get_logger({}, name='ContainerBroker', log_to_console=True)
519
- broker = ContainerBroker(os.path.realpath(args.container_db),
520
- logger=logger, skip_commits=True)
1186
+ broker = ContainerBroker(os.path.realpath(args.path_to_file),
1187
+ logger=logger,
1188
+ skip_commits=not args.force_commits)
521
1189
  try:
522
1190
  broker.get_info()
523
1191
  except Exception as exc:
524
- print('Error opening container DB %s: %s' % (args.container_db, exc),
1192
+ print('Error opening container DB %s: %s' % (args.path_to_file, exc),
525
1193
  file=sys.stderr)
526
- return 2
527
- print('Loaded db broker for %s.' % broker.path, file=sys.stderr)
1194
+ return EXIT_ERROR
1195
+ print('Loaded db broker for %s' % broker.path, file=sys.stderr)
528
1196
  return args.func(broker, args)
529
1197
 
530
1198