Contact
CoCalc Logo Icon
StoreFeaturesDocsShareSupport News AboutSign UpSign In
| Download
Views: 39550
1
###############################################################################
2
#
3
# CoCalc: Collaborative Calculation in the Cloud
4
#
5
# Copyright (C) 2016, Sagemath Inc.
6
#
7
# This program is free software: you can redistribute it and/or modify
8
# it under the terms of the GNU General Public License as published by
9
# the Free Software Foundation, either version 3 of the License, or
10
# (at your option) any later version.
11
#
12
# This program is distributed in the hope that it will be useful,
13
# but WITHOUT ANY WARRANTY; without even the implied warranty of
14
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
# GNU General Public License for more details.
16
#
17
# You should have received a copy of the GNU General Public License
18
# along with this program. If not, see <http://www.gnu.org/licenses/>.
19
#
20
###############################################################################
21
22
23
"""
24
Based on https://pypi.python.org/pypi/hash_ring (copyright: 2008 by Amir Salihefendic; license: BSD)
25
26
I (William Stein) rewrote all the documentation, fixed some trivial bugs,
27
more compatible with node-hashring (e.g., vnode support), etc. BSD license.
28
29
Example:
30
31
import hashring
32
r = hashring.HashRing({'10.1.1.4':{'vnodes':256}, '10.1.2.4':{'vnodes':128}, '10.1.3.4':{'vnodes':128}})
33
r.range('foo')
34
['10.1.1.4', '10.1.3.4', '10.1.2.4']
35
"""
36
37
import math
38
import sys
39
from bisect import bisect
40
41
import hashlib
42
md5_constructor = hashlib.md5
43
44
class HashRing(object):
45
46
def __init__(self, nodes=None, weights=1, vnodes=40, replicas=4):
47
"""
48
`nodes` is a list of objects that have a proper __str__ representation.
49
`weights` is dictionary that sets weights to the nodes. The default
50
weight is that all nodes are equal.
51
"""
52
self.ring = dict()
53
self._sorted_keys = []
54
55
if isinstance(nodes, dict):
56
weights = dict([(n, nodes[n].get('weight',weights)) for n in nodes])
57
vnodes = dict([(n, nodes[n].get('vnodes',vnodes)) for n in nodes])
58
nodes = nodes.keys()
59
60
self.nodes = nodes
61
62
if not isinstance(weights, dict):
63
weights = dict([(n,weights) for n in nodes])
64
else:
65
for n in nodes:
66
if n not in weights:
67
weights[n] = 1
68
self.weights = weights
69
70
self.replicas = replicas
71
72
if not isinstance(vnodes, dict):
73
vnodes = dict([(n,vnodes) for n in nodes])
74
else:
75
for n in nodes:
76
if n not in vnodes:
77
vnodes[n] = 40
78
79
self.vnodes = vnodes
80
81
self._generate_circle()
82
83
def _generate_circle(self):
84
"""
85
Generates the circle.
86
"""
87
total_weight = 0
88
for node in self.nodes:
89
total_weight += self.weights.get(node, 1)
90
91
for node in self.nodes:
92
weight = self.weights[node]
93
factor = (self.vnodes[node]*len(self.nodes)*weight) // total_weight
94
for j in xrange(0, int(factor)):
95
b_key = self._hash_digest('%s-%s'%(node, j))
96
for i in xrange(0, self.replicas):
97
key = self._hash_val(b_key, lambda x: x+i*4)
98
self.ring[key] = node
99
self._sorted_keys.append(key)
100
101
self._sorted_keys.sort()
102
103
def get_node(self, string_key):
104
"""
105
Given a string key, return some corresponding node in the hash ring.
106
107
If the hash ring is empty, return `None`.
108
"""
109
pos = self.get_node_pos(string_key)
110
if pos is None:
111
return None
112
return self.ring[ self._sorted_keys[pos] ]
113
114
115
def range(self, key, size=None, distinct=True):
116
if size is None:
117
return list(self.iterate_nodes(key, distinct=distinct))
118
v = []
119
for k in self.iterate_nodes(key, distinct=distinct):
120
v.append(k)
121
if len(v) >= size:
122
return v
123
return v
124
125
def __getitem__(self, string_key):
126
"""
127
Given a string key, return node that hold the key.
128
"""
129
return self.range(string_key)
130
131
def get_node_pos(self, string_key):
132
"""
133
Given a string key, return a corresponding node in the hash ring
134
along with it's position in the ring.
135
136
If the hash ring is empty, `None` is returned.
137
"""
138
if not self.ring:
139
return None
140
141
key = self.gen_key(string_key)
142
143
nodes = self._sorted_keys
144
pos = bisect(nodes, key)
145
146
if pos == len(nodes):
147
return 0
148
else:
149
return pos
150
151
def iterate_nodes(self, string_key, distinct=True):
152
"""
153
Given a string key, return iterator over all nodes that hold the key.
154
155
The generator iterates one time through the ring
156
starting at the correct position.
157
158
if `distinct` is set, then the nodes returned will be unique,
159
i.e. no virtual copies will be returned.
160
"""
161
if not self.ring:
162
yield None, None
163
164
returned_values = set()
165
def distinct_filter(value):
166
if str(value) not in returned_values:
167
returned_values.add(str(value))
168
return value
169
170
pos = self.get_node_pos(string_key)
171
for key in self._sorted_keys[pos:]:
172
val = distinct_filter(self.ring[key])
173
if val:
174
yield val
175
176
# wrap around if necessary.
177
for i, key in enumerate(self._sorted_keys):
178
if i < pos:
179
val = distinct_filter(self.ring[key])
180
if val:
181
yield val
182
183
def gen_key(self, key):
184
"""
185
Given a string key return a long value, which
186
represents a place on the hash ring.
187
188
md5 is currently used because it mixes well.
189
"""
190
b_key = self._hash_digest(key)
191
return self._hash_val(b_key, lambda x: x)
192
193
def _hash_val(self, b_key, entry_fn):
194
return (( b_key[entry_fn(3)] << 24)
195
|(b_key[entry_fn(2)] << 16)
196
|(b_key[entry_fn(1)] << 8)
197
| b_key[entry_fn(0)] )
198
199
def _hash_digest(self, key):
200
m = md5_constructor()
201
m.update(key)
202
return map(ord, m.digest())
203