/swift/common/ring/ring.py

https://github.com/mdegerne/swift-2 · Python · 160 lines · 85 code · 14 blank · 61 comment · 11 complexity · c7345f2fa94d14d3f7cabbbbae51d4b3 MD5 · raw file

  1. # Copyright (c) 2010-2011 OpenStack, LLC.
  2. #
  3. # Licensed under the Apache License, Version 2.0 (the "License");
  4. # you may not use this file except in compliance with the License.
  5. # You may obtain a copy of the License at
  6. #
  7. # http://www.apache.org/licenses/LICENSE-2.0
  8. #
  9. # Unless required by applicable law or agreed to in writing, software
  10. # distributed under the License is distributed on an "AS IS" BASIS,
  11. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
  12. # implied.
  13. # See the License for the specific language governing permissions and
  14. # limitations under the License.
  15. import cPickle as pickle
  16. from gzip import GzipFile
  17. from os.path import getmtime
  18. from struct import unpack_from
  19. from time import time
  20. from swift.common.utils import hash_path, validate_configuration
  21. class RingData(object):
  22. """Partitioned consistent hashing ring data (used for serialization)."""
  23. def __init__(self, replica2part2dev_id, devs, part_shift):
  24. self.devs = devs
  25. self._replica2part2dev_id = replica2part2dev_id
  26. self._part_shift = part_shift
  27. def to_dict(self):
  28. return {'devs': self.devs,
  29. 'replica2part2dev_id': self._replica2part2dev_id,
  30. 'part_shift': self._part_shift}
  31. class Ring(object):
  32. """
  33. Partitioned consistent hashing ring.
  34. :param pickle_gz_path: path to ring file
  35. :param reload_time: time interval in seconds to check for a ring change
  36. """
  37. def __init__(self, pickle_gz_path, reload_time=15):
  38. # can't use the ring unless HASH_PATH_SUFFIX is set
  39. validate_configuration()
  40. self.pickle_gz_path = pickle_gz_path
  41. self.reload_time = reload_time
  42. self._reload(force=True)
  43. def _reload(self, force=False):
  44. self._rtime = time() + self.reload_time
  45. if force or self.has_changed():
  46. ring_data = pickle.load(GzipFile(self.pickle_gz_path, 'rb'))
  47. if not hasattr(ring_data, 'devs'):
  48. ring_data = RingData(ring_data['replica2part2dev_id'],
  49. ring_data['devs'], ring_data['part_shift'])
  50. self._mtime = getmtime(self.pickle_gz_path)
  51. self.devs = ring_data.devs
  52. self.zone2devs = {}
  53. for dev in self.devs:
  54. if not dev:
  55. continue
  56. if dev['zone'] in self.zone2devs:
  57. self.zone2devs[dev['zone']].append(dev)
  58. else:
  59. self.zone2devs[dev['zone']] = [dev]
  60. self._replica2part2dev_id = ring_data._replica2part2dev_id
  61. self._part_shift = ring_data._part_shift
  62. @property
  63. def replica_count(self):
  64. """Number of replicas used in the ring."""
  65. return len(self._replica2part2dev_id)
  66. @property
  67. def partition_count(self):
  68. """Number of partitions in the ring."""
  69. return len(self._replica2part2dev_id[0])
  70. def has_changed(self):
  71. """
  72. Check to see if the ring on disk is different than the current one in
  73. memory.
  74. :returns: True if the ring on disk has changed, False otherwise
  75. """
  76. return getmtime(self.pickle_gz_path) != self._mtime
  77. def get_part_nodes(self, part):
  78. """
  79. Get the nodes that are responsible for the partition.
  80. :param part: partition to get nodes for
  81. :returns: list of node dicts
  82. See :func:`get_nodes` for a description of the node dicts.
  83. """
  84. if time() > self._rtime:
  85. self._reload()
  86. return [self.devs[r[part]] for r in self._replica2part2dev_id]
  87. def get_nodes(self, account, container=None, obj=None):
  88. """
  89. Get the partition and nodes for an account/container/object.
  90. :param account: account name
  91. :param container: container name
  92. :param obj: object name
  93. :returns: a tuple of (partition, list of node dicts)
  94. Each node dict will have at least the following keys:
  95. ====== ===============================================================
  96. id unique integer identifier amongst devices
  97. weight a float of the relative weight of this device as compared to
  98. others; this indicates how many partitions the builder will try
  99. to assign to this device
  100. zone integer indicating which zone the device is in; a given
  101. partition will not be assigned to multiple devices within the
  102. same zone ip the ip address of the device
  103. port the tcp port of the device
  104. device the device's name on disk (sdb1, for example)
  105. meta general use 'extra' field; for example: the online date, the
  106. hardware description
  107. ====== ===============================================================
  108. """
  109. key = hash_path(account, container, obj, raw_digest=True)
  110. if time() > self._rtime:
  111. self._reload()
  112. part = unpack_from('>I', key)[0] >> self._part_shift
  113. return part, [self.devs[r[part]] for r in self._replica2part2dev_id]
  114. def get_more_nodes(self, part):
  115. """
  116. Generator to get extra nodes for a partition for hinted handoff.
  117. :param part: partition to get handoff nodes for
  118. :returns: generator of node dicts
  119. See :func:`get_nodes` for a description of the node dicts.
  120. """
  121. if time() > self._rtime:
  122. self._reload()
  123. zones = sorted(self.zone2devs.keys())
  124. for part2dev_id in self._replica2part2dev_id:
  125. zones.remove(self.devs[part2dev_id[part]]['zone'])
  126. while zones:
  127. zone = zones.pop(part % len(zones))
  128. weighted_node = None
  129. for i in xrange(len(self.zone2devs[zone])):
  130. node = self.zone2devs[zone][(part + i) %
  131. len(self.zone2devs[zone])]
  132. if node.get('weight'):
  133. weighted_node = node
  134. break
  135. if weighted_node:
  136. yield weighted_node