QuLab 2.10.10__cp313-cp313-macosx_10_13_universal2.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.
- qulab/__init__.py +33 -0
- qulab/__main__.py +4 -0
- qulab/cli/__init__.py +0 -0
- qulab/cli/commands.py +30 -0
- qulab/cli/config.py +170 -0
- qulab/cli/decorators.py +28 -0
- qulab/dicttree.py +523 -0
- qulab/executor/__init__.py +5 -0
- qulab/executor/analyze.py +188 -0
- qulab/executor/cli.py +434 -0
- qulab/executor/load.py +563 -0
- qulab/executor/registry.py +185 -0
- qulab/executor/schedule.py +543 -0
- qulab/executor/storage.py +615 -0
- qulab/executor/template.py +259 -0
- qulab/executor/utils.py +194 -0
- qulab/expression.py +827 -0
- qulab/fun.cpython-313-darwin.so +0 -0
- qulab/monitor/__init__.py +1 -0
- qulab/monitor/__main__.py +8 -0
- qulab/monitor/config.py +41 -0
- qulab/monitor/dataset.py +77 -0
- qulab/monitor/event_queue.py +54 -0
- qulab/monitor/mainwindow.py +234 -0
- qulab/monitor/monitor.py +115 -0
- qulab/monitor/ploter.py +123 -0
- qulab/monitor/qt_compat.py +16 -0
- qulab/monitor/toolbar.py +265 -0
- qulab/scan/__init__.py +2 -0
- qulab/scan/curd.py +221 -0
- qulab/scan/models.py +554 -0
- qulab/scan/optimize.py +76 -0
- qulab/scan/query.py +387 -0
- qulab/scan/record.py +603 -0
- qulab/scan/scan.py +1166 -0
- qulab/scan/server.py +450 -0
- qulab/scan/space.py +213 -0
- qulab/scan/utils.py +234 -0
- qulab/storage/__init__.py +0 -0
- qulab/storage/__main__.py +51 -0
- qulab/storage/backend/__init__.py +0 -0
- qulab/storage/backend/redis.py +204 -0
- qulab/storage/base_dataset.py +352 -0
- qulab/storage/chunk.py +60 -0
- qulab/storage/dataset.py +127 -0
- qulab/storage/file.py +273 -0
- qulab/storage/models/__init__.py +22 -0
- qulab/storage/models/base.py +4 -0
- qulab/storage/models/config.py +28 -0
- qulab/storage/models/file.py +89 -0
- qulab/storage/models/ipy.py +58 -0
- qulab/storage/models/models.py +88 -0
- qulab/storage/models/record.py +161 -0
- qulab/storage/models/report.py +22 -0
- qulab/storage/models/tag.py +93 -0
- qulab/storage/storage.py +95 -0
- qulab/sys/__init__.py +2 -0
- qulab/sys/chat.py +688 -0
- qulab/sys/device/__init__.py +3 -0
- qulab/sys/device/basedevice.py +255 -0
- qulab/sys/device/loader.py +86 -0
- qulab/sys/device/utils.py +79 -0
- qulab/sys/drivers/FakeInstrument.py +68 -0
- qulab/sys/drivers/__init__.py +0 -0
- qulab/sys/ipy_events.py +125 -0
- qulab/sys/net/__init__.py +0 -0
- qulab/sys/net/bencoder.py +205 -0
- qulab/sys/net/cli.py +169 -0
- qulab/sys/net/dhcp.py +543 -0
- qulab/sys/net/dhcpd.py +176 -0
- qulab/sys/net/kad.py +1142 -0
- qulab/sys/net/kcp.py +192 -0
- qulab/sys/net/nginx.py +194 -0
- qulab/sys/progress.py +190 -0
- qulab/sys/rpc/__init__.py +0 -0
- qulab/sys/rpc/client.py +0 -0
- qulab/sys/rpc/exceptions.py +96 -0
- qulab/sys/rpc/msgpack.py +1052 -0
- qulab/sys/rpc/msgpack.pyi +41 -0
- qulab/sys/rpc/router.py +35 -0
- qulab/sys/rpc/rpc.py +412 -0
- qulab/sys/rpc/serialize.py +139 -0
- qulab/sys/rpc/server.py +29 -0
- qulab/sys/rpc/socket.py +29 -0
- qulab/sys/rpc/utils.py +25 -0
- qulab/sys/rpc/worker.py +0 -0
- qulab/sys/rpc/zmq_socket.py +227 -0
- qulab/tools/__init__.py +0 -0
- qulab/tools/connection_helper.py +39 -0
- qulab/typing.py +2 -0
- qulab/utils.py +95 -0
- qulab/version.py +1 -0
- qulab/visualization/__init__.py +188 -0
- qulab/visualization/__main__.py +71 -0
- qulab/visualization/_autoplot.py +464 -0
- qulab/visualization/plot_circ.py +319 -0
- qulab/visualization/plot_layout.py +408 -0
- qulab/visualization/plot_seq.py +242 -0
- qulab/visualization/qdat.py +152 -0
- qulab/visualization/rot3d.py +23 -0
- qulab/visualization/widgets.py +86 -0
- qulab-2.10.10.dist-info/METADATA +110 -0
- qulab-2.10.10.dist-info/RECORD +107 -0
- qulab-2.10.10.dist-info/WHEEL +5 -0
- qulab-2.10.10.dist-info/entry_points.txt +2 -0
- qulab-2.10.10.dist-info/licenses/LICENSE +21 -0
- qulab-2.10.10.dist-info/top_level.txt +1 -0
qulab/sys/net/kad.py
ADDED
@@ -0,0 +1,1142 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import asyncio
|
4
|
+
import heapq
|
5
|
+
import logging
|
6
|
+
import os
|
7
|
+
import pickle
|
8
|
+
import random
|
9
|
+
import re
|
10
|
+
import secrets
|
11
|
+
import time
|
12
|
+
import uuid
|
13
|
+
from abc import ABC, abstractmethod
|
14
|
+
from base64 import b64encode
|
15
|
+
from collections import Counter, OrderedDict
|
16
|
+
from hashlib import sha1
|
17
|
+
from itertools import chain
|
18
|
+
from operator import itemgetter
|
19
|
+
from typing import Any, Coroutine, NamedTuple
|
20
|
+
|
21
|
+
import msgpack
|
22
|
+
|
23
|
+
log = logging.getLogger(__name__)
|
24
|
+
|
25
|
+
|
26
|
+
class Value(NamedTuple):
|
27
|
+
value: Any
|
28
|
+
ttl: float | None
|
29
|
+
ts: float
|
30
|
+
|
31
|
+
def outdated(self):
|
32
|
+
return self.ttl is not None and self.ttl < time.time() - self.ts
|
33
|
+
|
34
|
+
def __repr__(self):
|
35
|
+
fmt = "%a, %d %b %Y %H:%M:%S"
|
36
|
+
mt = time.strftime(fmt, time.localtime(self.ts))
|
37
|
+
return f"{self.value!r}, ttl={self.ttl}, last modified at {mt}"
|
38
|
+
|
39
|
+
|
40
|
+
class IStorage(ABC):
|
41
|
+
"""
|
42
|
+
Local storage for this node.
|
43
|
+
IStorage implementations of get must return the same type as put in by set
|
44
|
+
"""
|
45
|
+
|
46
|
+
@abstractmethod
|
47
|
+
def __setitem__(self, key: bytes, value):
|
48
|
+
"""
|
49
|
+
Set a key to the given value.
|
50
|
+
"""
|
51
|
+
|
52
|
+
@abstractmethod
|
53
|
+
def __getitem__(self, key: bytes):
|
54
|
+
"""
|
55
|
+
Get the given key. If item doesn't exist, return None.
|
56
|
+
"""
|
57
|
+
|
58
|
+
@abstractmethod
|
59
|
+
def get(self, key: bytes, default=None):
|
60
|
+
"""
|
61
|
+
Get given key. If not found, return default.
|
62
|
+
"""
|
63
|
+
|
64
|
+
@abstractmethod
|
65
|
+
def set(self, key: bytes, value, ttl: float = 0):
|
66
|
+
"""
|
67
|
+
Set a key to the given value.
|
68
|
+
"""
|
69
|
+
|
70
|
+
@abstractmethod
|
71
|
+
def iter_older_than(self, seconds_old: float):
|
72
|
+
"""
|
73
|
+
Return the an iterator over (key, value) tuples for items older
|
74
|
+
than the given secondsOld.
|
75
|
+
"""
|
76
|
+
|
77
|
+
@abstractmethod
|
78
|
+
def __iter__(self):
|
79
|
+
"""
|
80
|
+
Get the iterator for this storage, should yield tuple of (key, value)
|
81
|
+
"""
|
82
|
+
|
83
|
+
|
84
|
+
class ForgetfulStorage(IStorage):
|
85
|
+
|
86
|
+
def __init__(self, ttl: float = 604800):
|
87
|
+
self._storage: dict[bytes, Value] = {}
|
88
|
+
self.ttl: float = ttl
|
89
|
+
|
90
|
+
def get(self, key: bytes):
|
91
|
+
if key not in self._storage:
|
92
|
+
return None
|
93
|
+
value = self._storage[key]
|
94
|
+
if value.outdated():
|
95
|
+
del self._storage[key]
|
96
|
+
return None
|
97
|
+
else:
|
98
|
+
return value.value
|
99
|
+
|
100
|
+
def set(self, key: bytes, value, ttl: float | None = None):
|
101
|
+
if ttl is None:
|
102
|
+
ttl = self.ttl
|
103
|
+
self._storage[key] = Value(value, ttl, time.time())
|
104
|
+
|
105
|
+
def __iter(self):
|
106
|
+
for key, value in list(self._storage.items()):
|
107
|
+
if value.outdated():
|
108
|
+
del self._storage[key]
|
109
|
+
continue
|
110
|
+
yield key, value
|
111
|
+
|
112
|
+
def cull(self):
|
113
|
+
for key, value in self.__iter():
|
114
|
+
if value.outdated():
|
115
|
+
del self._storage[key]
|
116
|
+
continue
|
117
|
+
|
118
|
+
def __repr__(self):
|
119
|
+
self.cull()
|
120
|
+
return repr(self._storage)
|
121
|
+
|
122
|
+
def __contains__(self, key: bytes):
|
123
|
+
if key in self._storage:
|
124
|
+
value = self._storage[key]
|
125
|
+
if value.outdated():
|
126
|
+
del self._storage[key]
|
127
|
+
return False
|
128
|
+
else:
|
129
|
+
return True
|
130
|
+
else:
|
131
|
+
return False
|
132
|
+
|
133
|
+
def __setitem__(self, key: bytes, value):
|
134
|
+
self.set(key, value, ttl=self.ttl)
|
135
|
+
|
136
|
+
def __getitem__(self, key: bytes):
|
137
|
+
return self.get(key)
|
138
|
+
|
139
|
+
def __iter__(self):
|
140
|
+
for key, value in self.__iter():
|
141
|
+
yield key, value.value
|
142
|
+
|
143
|
+
def expire(self, key: bytes, ttl: float):
|
144
|
+
if key in self._storage:
|
145
|
+
value = self._storage[key]
|
146
|
+
self._storage[key] = Value(value.value, ttl, time.time())
|
147
|
+
|
148
|
+
def exists(self, key: bytes):
|
149
|
+
return self.__contains__(key)
|
150
|
+
|
151
|
+
def keys(self, pattern="*"):
|
152
|
+
return [key for key in self.scan_iter(pattern)]
|
153
|
+
|
154
|
+
def scan_iter(self, pattern="*"):
|
155
|
+
prog = re.compile(pattern.replace("*", ".*"))
|
156
|
+
for key, _ in self.__iter():
|
157
|
+
if prog.match(key):
|
158
|
+
yield key
|
159
|
+
|
160
|
+
def iter_older_than(self, t: float):
|
161
|
+
for key, value in self.__iter():
|
162
|
+
if time.time() - value.ts >= t:
|
163
|
+
yield key, value.value
|
164
|
+
|
165
|
+
|
166
|
+
async def gather_dict(dic: dict[bytes, Coroutine]):
|
167
|
+
cors = list(dic.values())
|
168
|
+
results = await asyncio.gather(*cors)
|
169
|
+
return dict(zip(dic.keys(), results))
|
170
|
+
|
171
|
+
|
172
|
+
def digest(string: str | bytes) -> bytes:
|
173
|
+
if not isinstance(string, bytes):
|
174
|
+
string = str(string).encode('utf8')
|
175
|
+
return sha1(string).digest()
|
176
|
+
|
177
|
+
|
178
|
+
def shared_prefix(args):
|
179
|
+
"""
|
180
|
+
Find the shared prefix between the strings.
|
181
|
+
For instance:
|
182
|
+
sharedPrefix(['blahblah', 'blahwhat'])
|
183
|
+
returns 'blah'.
|
184
|
+
"""
|
185
|
+
i = 0
|
186
|
+
while i < min(map(len, args)):
|
187
|
+
if len(set(map(itemgetter(i), args))) != 1:
|
188
|
+
break
|
189
|
+
i += 1
|
190
|
+
return args[0][:i]
|
191
|
+
|
192
|
+
|
193
|
+
def bytes_to_bit_string(bites):
|
194
|
+
bits = [bin(bite)[2:].rjust(8, '0') for bite in bites]
|
195
|
+
return "".join(bits)
|
196
|
+
|
197
|
+
|
198
|
+
class Node:
|
199
|
+
"""
|
200
|
+
Simple object to encapsulate the concept of a Node (minimally an ID, but
|
201
|
+
also possibly an IP and port if this represents a node on the network).
|
202
|
+
This class should generally not be instantiated directly, as it is a low
|
203
|
+
level construct mostly used by the router.
|
204
|
+
"""
|
205
|
+
__slots__ = ('id', 'ip', 'port', 'long_id')
|
206
|
+
|
207
|
+
def __init__(self, node_id: bytes, ip=None, port=None):
|
208
|
+
"""
|
209
|
+
Create a Node instance.
|
210
|
+
Args:
|
211
|
+
node_id (int): A value between 0 and 2^160
|
212
|
+
ip (string): Optional IP address where this Node lives
|
213
|
+
port (int): Optional port for this Node (set when IP is set)
|
214
|
+
"""
|
215
|
+
self.id = node_id # pylint: disable=invalid-name
|
216
|
+
self.ip = ip # pylint: disable=invalid-name
|
217
|
+
self.port = port
|
218
|
+
self.long_id = int(node_id.hex(), 16)
|
219
|
+
|
220
|
+
def same_home_as(self, node):
|
221
|
+
return self.ip == node.ip and self.port == node.port
|
222
|
+
|
223
|
+
def distance_to(self, node):
|
224
|
+
"""
|
225
|
+
Get the distance between this node and another.
|
226
|
+
"""
|
227
|
+
return self.long_id ^ node.long_id
|
228
|
+
|
229
|
+
def __iter__(self):
|
230
|
+
"""
|
231
|
+
Enables use of Node as a tuple - i.e., tuple(node) works.
|
232
|
+
"""
|
233
|
+
return iter([self.id, self.ip, self.port])
|
234
|
+
|
235
|
+
def __repr__(self):
|
236
|
+
return repr([self.long_id, self.ip, self.port])
|
237
|
+
|
238
|
+
def __str__(self):
|
239
|
+
return "%s:%s" % (self.ip, str(self.port))
|
240
|
+
|
241
|
+
|
242
|
+
class NodeHeap:
|
243
|
+
"""
|
244
|
+
A heap of nodes ordered by distance to a given node.
|
245
|
+
"""
|
246
|
+
|
247
|
+
def __init__(self, node: Node, maxsize: int):
|
248
|
+
"""
|
249
|
+
Constructor.
|
250
|
+
@param node: The node to measure all distnaces from.
|
251
|
+
@param maxsize: The maximum size that this heap can grow to.
|
252
|
+
"""
|
253
|
+
self.node = node
|
254
|
+
self.heap = []
|
255
|
+
self.contacted = set()
|
256
|
+
self.maxsize = maxsize
|
257
|
+
|
258
|
+
def remove(self, peers):
|
259
|
+
"""
|
260
|
+
Remove a list of peer ids from this heap. Note that while this
|
261
|
+
heap retains a constant visible size (based on the iterator), it's
|
262
|
+
actual size may be quite a bit larger than what's exposed. Therefore,
|
263
|
+
removal of nodes may not change the visible size as previously added
|
264
|
+
nodes suddenly become visible.
|
265
|
+
"""
|
266
|
+
peers = set(peers)
|
267
|
+
if not peers:
|
268
|
+
return
|
269
|
+
nheap = []
|
270
|
+
for distance, node in self.heap:
|
271
|
+
if node.id not in peers:
|
272
|
+
heapq.heappush(nheap, (distance, node))
|
273
|
+
self.heap = nheap
|
274
|
+
|
275
|
+
def get_node(self, node_id):
|
276
|
+
for _, node in self.heap:
|
277
|
+
if node.id == node_id:
|
278
|
+
return node
|
279
|
+
return None
|
280
|
+
|
281
|
+
def have_contacted_all(self):
|
282
|
+
return len(self.get_uncontacted()) == 0
|
283
|
+
|
284
|
+
def get_ids(self):
|
285
|
+
return [n.id for n in self]
|
286
|
+
|
287
|
+
def mark_contacted(self, node):
|
288
|
+
self.contacted.add(node.id)
|
289
|
+
|
290
|
+
def popleft(self):
|
291
|
+
return heapq.heappop(self.heap)[1] if self else None
|
292
|
+
|
293
|
+
def push(self, nodes):
|
294
|
+
"""
|
295
|
+
Push nodes onto heap.
|
296
|
+
@param nodes: This can be a single item or a C{list}.
|
297
|
+
"""
|
298
|
+
if not isinstance(nodes, list):
|
299
|
+
nodes = [nodes]
|
300
|
+
|
301
|
+
for node in nodes:
|
302
|
+
if node not in self:
|
303
|
+
distance = self.node.distance_to(node)
|
304
|
+
heapq.heappush(self.heap, (distance, node))
|
305
|
+
|
306
|
+
def __len__(self):
|
307
|
+
return min(len(self.heap), self.maxsize)
|
308
|
+
|
309
|
+
def __iter__(self):
|
310
|
+
nodes = heapq.nsmallest(self.maxsize, self.heap)
|
311
|
+
return iter(map(itemgetter(1), nodes))
|
312
|
+
|
313
|
+
def __contains__(self, node):
|
314
|
+
for _, other in self.heap:
|
315
|
+
if node.id == other.id:
|
316
|
+
return True
|
317
|
+
return False
|
318
|
+
|
319
|
+
def get_uncontacted(self):
|
320
|
+
return [n for n in self if n.id not in self.contacted]
|
321
|
+
|
322
|
+
|
323
|
+
class KBucket:
|
324
|
+
|
325
|
+
def __init__(self, rangeLower, rangeUpper, ksize, replacementNodeFactor=5):
|
326
|
+
self.range = (rangeLower, rangeUpper)
|
327
|
+
self.nodes = OrderedDict()
|
328
|
+
self.replacement_nodes = OrderedDict()
|
329
|
+
self.touch_last_updated()
|
330
|
+
self.ksize = ksize
|
331
|
+
self.max_replacement_nodes = self.ksize * replacementNodeFactor
|
332
|
+
|
333
|
+
def touch_last_updated(self):
|
334
|
+
self.last_updated = time.monotonic()
|
335
|
+
|
336
|
+
def get_nodes(self):
|
337
|
+
return list(self.nodes.values())
|
338
|
+
|
339
|
+
def split(self):
|
340
|
+
midpoint = (self.range[0] + self.range[1]) // 2
|
341
|
+
one = KBucket(self.range[0], midpoint, self.ksize)
|
342
|
+
two = KBucket(midpoint + 1, self.range[1], self.ksize)
|
343
|
+
nodes = chain(self.nodes.values(), self.replacement_nodes.values())
|
344
|
+
for node in nodes:
|
345
|
+
bucket = one if node.long_id <= midpoint else two
|
346
|
+
bucket.add_node(node)
|
347
|
+
|
348
|
+
return (one, two)
|
349
|
+
|
350
|
+
def remove_node(self, node):
|
351
|
+
if node.id in self.replacement_nodes:
|
352
|
+
del self.replacement_nodes[node.id]
|
353
|
+
|
354
|
+
if node.id in self.nodes:
|
355
|
+
del self.nodes[node.id]
|
356
|
+
|
357
|
+
if self.replacement_nodes:
|
358
|
+
newnode_id, newnode = self.replacement_nodes.popitem()
|
359
|
+
self.nodes[newnode_id] = newnode
|
360
|
+
|
361
|
+
def has_in_range(self, node):
|
362
|
+
return self.range[0] <= node.long_id <= self.range[1]
|
363
|
+
|
364
|
+
def is_new_node(self, node):
|
365
|
+
return node.id not in self.nodes
|
366
|
+
|
367
|
+
def add_node(self, node):
|
368
|
+
"""
|
369
|
+
Add a C{Node} to the C{KBucket}. Return True if successful,
|
370
|
+
False if the bucket is full.
|
371
|
+
If the bucket is full, keep track of node in a replacement list,
|
372
|
+
per section 4.1 of the paper.
|
373
|
+
"""
|
374
|
+
if node.id in self.nodes:
|
375
|
+
del self.nodes[node.id]
|
376
|
+
self.nodes[node.id] = node
|
377
|
+
elif len(self) < self.ksize:
|
378
|
+
self.nodes[node.id] = node
|
379
|
+
else:
|
380
|
+
if node.id in self.replacement_nodes:
|
381
|
+
del self.replacement_nodes[node.id]
|
382
|
+
self.replacement_nodes[node.id] = node
|
383
|
+
while len(self.replacement_nodes) > self.max_replacement_nodes:
|
384
|
+
self.replacement_nodes.popitem(last=False)
|
385
|
+
return False
|
386
|
+
return True
|
387
|
+
|
388
|
+
def depth(self):
|
389
|
+
vals = self.nodes.values()
|
390
|
+
sprefix = shared_prefix([bytes_to_bit_string(n.id) for n in vals])
|
391
|
+
return len(sprefix)
|
392
|
+
|
393
|
+
def head(self):
|
394
|
+
return list(self.nodes.values())[0]
|
395
|
+
|
396
|
+
def __getitem__(self, node_id):
|
397
|
+
return self.nodes.get(node_id, None)
|
398
|
+
|
399
|
+
def __len__(self):
|
400
|
+
return len(self.nodes)
|
401
|
+
|
402
|
+
|
403
|
+
class TableTraverser:
|
404
|
+
|
405
|
+
def __init__(self, table, startNode):
|
406
|
+
index = table.get_bucket_for(startNode)
|
407
|
+
table.buckets[index].touch_last_updated()
|
408
|
+
self.current_nodes = table.buckets[index].get_nodes()
|
409
|
+
self.left_buckets = table.buckets[:index]
|
410
|
+
self.right_buckets = table.buckets[(index + 1):]
|
411
|
+
self.left = True
|
412
|
+
|
413
|
+
def __iter__(self):
|
414
|
+
return self
|
415
|
+
|
416
|
+
def __next__(self):
|
417
|
+
"""
|
418
|
+
Pop an item from the left subtree, then right, then left, etc.
|
419
|
+
"""
|
420
|
+
if self.current_nodes:
|
421
|
+
return self.current_nodes.pop()
|
422
|
+
|
423
|
+
if self.left and self.left_buckets:
|
424
|
+
self.current_nodes = self.left_buckets.pop().get_nodes()
|
425
|
+
self.left = False
|
426
|
+
return next(self)
|
427
|
+
|
428
|
+
if self.right_buckets:
|
429
|
+
self.current_nodes = self.right_buckets.pop(0).get_nodes()
|
430
|
+
self.left = True
|
431
|
+
return next(self)
|
432
|
+
|
433
|
+
raise StopIteration
|
434
|
+
|
435
|
+
|
436
|
+
class RoutingTable:
|
437
|
+
|
438
|
+
def __init__(self, protocol, ksize, node):
|
439
|
+
"""
|
440
|
+
@param node: The node that represents this server. It won't
|
441
|
+
be added to the routing table, but will be needed later to
|
442
|
+
determine which buckets to split or not.
|
443
|
+
"""
|
444
|
+
self.node = node
|
445
|
+
self.protocol = protocol
|
446
|
+
self.ksize = ksize
|
447
|
+
self.flush()
|
448
|
+
|
449
|
+
def flush(self):
|
450
|
+
self.buckets: list[KBucket] = [KBucket(0, 2**160, self.ksize)]
|
451
|
+
|
452
|
+
def split_bucket(self, index: int):
|
453
|
+
one, two = self.buckets[index].split()
|
454
|
+
self.buckets[index] = one
|
455
|
+
self.buckets.insert(index + 1, two)
|
456
|
+
|
457
|
+
def lonely_buckets(self):
|
458
|
+
"""
|
459
|
+
Get all of the buckets that haven't been updated in over
|
460
|
+
an hour.
|
461
|
+
"""
|
462
|
+
hrago = time.monotonic() - 3600
|
463
|
+
return [b for b in self.buckets if b.last_updated < hrago]
|
464
|
+
|
465
|
+
def remove_contact(self, node):
|
466
|
+
index = self.get_bucket_for(node)
|
467
|
+
self.buckets[index].remove_node(node)
|
468
|
+
|
469
|
+
def is_new_node(self, node):
|
470
|
+
index = self.get_bucket_for(node)
|
471
|
+
return self.buckets[index].is_new_node(node)
|
472
|
+
|
473
|
+
def add_contact(self, node):
|
474
|
+
index = self.get_bucket_for(node)
|
475
|
+
bucket = self.buckets[index]
|
476
|
+
|
477
|
+
# this will succeed unless the bucket is full
|
478
|
+
if bucket.add_node(node):
|
479
|
+
return
|
480
|
+
|
481
|
+
# Per section 4.2 of paper, split if the bucket has the node
|
482
|
+
# in its range or if the depth is not congruent to 0 mod 5
|
483
|
+
if bucket.has_in_range(self.node) or bucket.depth() % 5 != 0:
|
484
|
+
self.split_bucket(index)
|
485
|
+
self.add_contact(node)
|
486
|
+
else:
|
487
|
+
asyncio.ensure_future(self.protocol.call_ping(bucket.head()))
|
488
|
+
|
489
|
+
def get_bucket_for(self, node):
|
490
|
+
"""
|
491
|
+
Get the index of the bucket that the given node would fall into.
|
492
|
+
"""
|
493
|
+
for index, bucket in enumerate(self.buckets):
|
494
|
+
if node.long_id < bucket.range[1]:
|
495
|
+
return index
|
496
|
+
# we should never be here, but make linter happy
|
497
|
+
return None
|
498
|
+
|
499
|
+
def find_neighbors(self, node, k=None, exclude=None):
|
500
|
+
k = k or self.ksize
|
501
|
+
nodes = []
|
502
|
+
for neighbor in TableTraverser(self, node):
|
503
|
+
notexcluded = exclude is None or not neighbor.same_home_as(exclude)
|
504
|
+
if neighbor.id != node.id and notexcluded:
|
505
|
+
heapq.heappush(nodes, (node.distance_to(neighbor), neighbor))
|
506
|
+
if len(nodes) == k:
|
507
|
+
break
|
508
|
+
|
509
|
+
return list(map(itemgetter(1), heapq.nsmallest(k, nodes)))
|
510
|
+
|
511
|
+
|
512
|
+
class SpiderCrawl:
|
513
|
+
"""
|
514
|
+
Crawl the network and look for given 160-bit keys.
|
515
|
+
"""
|
516
|
+
|
517
|
+
def __init__(self, protocol: KademliaProtocol, node, peers, ksize, alpha):
|
518
|
+
"""
|
519
|
+
Create a new C{SpiderCrawl}er.
|
520
|
+
Args:
|
521
|
+
protocol: A :class:`~kademlia.protocol.KademliaProtocol` instance.
|
522
|
+
node: A :class:`~kademlia.node.Node` representing the key we're
|
523
|
+
looking for
|
524
|
+
peers: A list of :class:`~kademlia.node.Node` instances that
|
525
|
+
provide the entry point for the network
|
526
|
+
ksize: The value for k based on the paper
|
527
|
+
alpha: The value for alpha based on the paper
|
528
|
+
"""
|
529
|
+
self.protocol = protocol
|
530
|
+
self.ksize = ksize
|
531
|
+
self.alpha = alpha
|
532
|
+
self.node = node
|
533
|
+
self.nearest = NodeHeap(self.node, self.ksize)
|
534
|
+
self.last_ids_crawled = []
|
535
|
+
log.info("creating spider with peers: %s", peers)
|
536
|
+
self.nearest.push(peers)
|
537
|
+
|
538
|
+
async def _find(self, rpcmethod):
|
539
|
+
"""
|
540
|
+
Get either a value or list of nodes.
|
541
|
+
Args:
|
542
|
+
rpcmethod: The protocol's callfindValue or call_find_node.
|
543
|
+
The process:
|
544
|
+
1. calls find_* to current ALPHA nearest not already queried nodes,
|
545
|
+
adding results to current nearest list of k nodes.
|
546
|
+
2. current nearest list needs to keep track of who has been queried
|
547
|
+
already sort by nearest, keep KSIZE
|
548
|
+
3. if list is same as last time, next call should be to everyone not
|
549
|
+
yet queried
|
550
|
+
4. repeat, unless nearest list has all been queried, then ur done
|
551
|
+
"""
|
552
|
+
log.info("crawling network with nearest: %s", str(tuple(self.nearest)))
|
553
|
+
count = self.alpha
|
554
|
+
if self.nearest.get_ids() == self.last_ids_crawled:
|
555
|
+
count = len(self.nearest)
|
556
|
+
self.last_ids_crawled = self.nearest.get_ids()
|
557
|
+
|
558
|
+
dicts = {}
|
559
|
+
for peer in self.nearest.get_uncontacted()[:count]:
|
560
|
+
dicts[peer.id] = rpcmethod(peer, self.node)
|
561
|
+
self.nearest.mark_contacted(peer)
|
562
|
+
found = await gather_dict(dicts)
|
563
|
+
return await self._nodes_found(found)
|
564
|
+
|
565
|
+
async def _nodes_found(self, responses):
|
566
|
+
raise NotImplementedError
|
567
|
+
|
568
|
+
|
569
|
+
class ValueSpiderCrawl(SpiderCrawl):
|
570
|
+
|
571
|
+
def __init__(self, protocol: KademliaProtocol, node, peers, ksize, alpha):
|
572
|
+
SpiderCrawl.__init__(self, protocol, node, peers, ksize, alpha)
|
573
|
+
# keep track of the single nearest node without value - per
|
574
|
+
# section 2.3 so we can set the key there if found
|
575
|
+
self.nearest_without_value = NodeHeap(self.node, 1)
|
576
|
+
|
577
|
+
async def find(self):
|
578
|
+
"""
|
579
|
+
Find either the closest nodes or the value requested.
|
580
|
+
"""
|
581
|
+
return await self._find(self.protocol.call_find_value)
|
582
|
+
|
583
|
+
async def _nodes_found(self, responses):
|
584
|
+
"""
|
585
|
+
Handle the result of an iteration in _find.
|
586
|
+
"""
|
587
|
+
toremove = []
|
588
|
+
found_values = []
|
589
|
+
for peerid, response in responses.items():
|
590
|
+
response = RPCFindResponse(response)
|
591
|
+
if not response.happened():
|
592
|
+
toremove.append(peerid)
|
593
|
+
elif response.has_value():
|
594
|
+
found_values.append(response.get_value())
|
595
|
+
else:
|
596
|
+
peer = self.nearest.get_node(peerid)
|
597
|
+
self.nearest_without_value.push(peer)
|
598
|
+
self.nearest.push(response.get_node_list())
|
599
|
+
self.nearest.remove(toremove)
|
600
|
+
|
601
|
+
if found_values:
|
602
|
+
return await self._handle_found_values(found_values)
|
603
|
+
if self.nearest.have_contacted_all():
|
604
|
+
# not found!
|
605
|
+
return None
|
606
|
+
return await self.find()
|
607
|
+
|
608
|
+
async def _handle_found_values(self, values):
|
609
|
+
"""
|
610
|
+
We got some values! Exciting. But let's make sure
|
611
|
+
they're all the same or freak out a little bit. Also,
|
612
|
+
make sure we tell the nearest node that *didn't* have
|
613
|
+
the value to store it.
|
614
|
+
"""
|
615
|
+
value_counts = Counter(values)
|
616
|
+
if len(value_counts) != 1:
|
617
|
+
log.warning("Got multiple values for key %i: %s",
|
618
|
+
self.node.long_id, str(values))
|
619
|
+
value = value_counts.most_common(1)[0][0]
|
620
|
+
|
621
|
+
peer = self.nearest_without_value.popleft()
|
622
|
+
if peer:
|
623
|
+
await self.protocol.call_store(peer, self.node.id, value)
|
624
|
+
return value
|
625
|
+
|
626
|
+
|
627
|
+
class NodeSpiderCrawl(SpiderCrawl):
|
628
|
+
|
629
|
+
async def find(self):
|
630
|
+
"""
|
631
|
+
Find the closest nodes.
|
632
|
+
"""
|
633
|
+
return await self._find(self.protocol.call_find_node)
|
634
|
+
|
635
|
+
async def _nodes_found(self, responses):
|
636
|
+
"""
|
637
|
+
Handle the result of an iteration in _find.
|
638
|
+
"""
|
639
|
+
toremove = []
|
640
|
+
for peerid, response in responses.items():
|
641
|
+
response = RPCFindResponse(response)
|
642
|
+
if not response.happened():
|
643
|
+
toremove.append(peerid)
|
644
|
+
else:
|
645
|
+
self.nearest.push(response.get_node_list())
|
646
|
+
self.nearest.remove(toremove)
|
647
|
+
|
648
|
+
if self.nearest.have_contacted_all():
|
649
|
+
return list(self.nearest)
|
650
|
+
return await self.find()
|
651
|
+
|
652
|
+
|
653
|
+
class RPCFindResponse:
|
654
|
+
|
655
|
+
def __init__(self, response):
|
656
|
+
"""
|
657
|
+
A wrapper for the result of a RPC find.
|
658
|
+
Args:
|
659
|
+
response: This will be a tuple of (<response received>, <value>)
|
660
|
+
where <value> will be a list of tuples if not found or
|
661
|
+
a dictionary of {'value': v} where v is the value desired
|
662
|
+
"""
|
663
|
+
self.response = response
|
664
|
+
|
665
|
+
def happened(self):
|
666
|
+
"""
|
667
|
+
Did the other host actually respond?
|
668
|
+
"""
|
669
|
+
return self.response[0]
|
670
|
+
|
671
|
+
def has_value(self):
|
672
|
+
return isinstance(self.response[1], dict)
|
673
|
+
|
674
|
+
def get_value(self):
|
675
|
+
return self.response[1]['value']
|
676
|
+
|
677
|
+
def get_node_list(self):
|
678
|
+
"""
|
679
|
+
Get the node list in the response. If there's no value, this should
|
680
|
+
be set.
|
681
|
+
"""
|
682
|
+
nodelist = self.response[1] or []
|
683
|
+
return [Node(*nodeple) for nodeple in nodelist]
|
684
|
+
|
685
|
+
|
686
|
+
class MalformedMessage(Exception):
|
687
|
+
"""
|
688
|
+
Message does not contain what is expected.
|
689
|
+
"""
|
690
|
+
|
691
|
+
|
692
|
+
class RPCProtocol(asyncio.DatagramProtocol):
|
693
|
+
|
694
|
+
def __init__(self, wait_timeout: float = 5):
|
695
|
+
self._timeout = wait_timeout
|
696
|
+
self._outstanding: dict[bytes, tuple[asyncio.Future,
|
697
|
+
asyncio.TimerHandle]] = {}
|
698
|
+
self.transport = None
|
699
|
+
|
700
|
+
def connection_made(self, transport):
|
701
|
+
self.transport = transport
|
702
|
+
|
703
|
+
def datagram_received(self, datagram, address):
|
704
|
+
log.debug("received datagram from %s", address)
|
705
|
+
if len(datagram) < 22:
|
706
|
+
log.warning("received datagram too small from %s,"
|
707
|
+
" ignoring", address)
|
708
|
+
return
|
709
|
+
|
710
|
+
msg_id = datagram[1:21]
|
711
|
+
data = msgpack.unpackb(datagram[21:])
|
712
|
+
|
713
|
+
if datagram[:1] == b'\x00':
|
714
|
+
# schedule accepting request and returning the result
|
715
|
+
self.handle_request(msg_id, data, address)
|
716
|
+
elif datagram[:1] == b'\x01':
|
717
|
+
self.handle_response(msg_id, data, address)
|
718
|
+
else:
|
719
|
+
# otherwise, don't know the format, don't do anything
|
720
|
+
log.debug("Received unknown message from %s, ignoring", address)
|
721
|
+
|
722
|
+
def handle_response(self, msg_id, data, address):
|
723
|
+
msgargs = (b64encode(msg_id), address)
|
724
|
+
if msg_id not in self._outstanding:
|
725
|
+
log.warning("received unknown message %s "
|
726
|
+
"from %s; ignoring", *msgargs)
|
727
|
+
return
|
728
|
+
log.debug("received response %s for message "
|
729
|
+
"id %s from %s", data, *msgargs)
|
730
|
+
future, timeout = self._outstanding[msg_id]
|
731
|
+
timeout.cancel()
|
732
|
+
future.set_result((True, data))
|
733
|
+
del self._outstanding[msg_id]
|
734
|
+
|
735
|
+
def handle_request(self, msg_id, data, address):
|
736
|
+
if not isinstance(data, list) or len(data) != 2:
|
737
|
+
raise MalformedMessage("Could not read packet: %s" % data)
|
738
|
+
funcname, args = data
|
739
|
+
|
740
|
+
try:
|
741
|
+
func = getattr(self, f"on_{funcname}")
|
742
|
+
except AttributeError:
|
743
|
+
msgargs = (self.__class__.__name__, funcname)
|
744
|
+
log.warning("%s has no callable method "
|
745
|
+
"on_%s; ignoring request", *msgargs)
|
746
|
+
return
|
747
|
+
|
748
|
+
try:
|
749
|
+
response = func(address, *args)
|
750
|
+
except Exception as e:
|
751
|
+
log.exception(e)
|
752
|
+
|
753
|
+
log.debug("sending response %s for msg id %s to %s", response,
|
754
|
+
b64encode(msg_id), address)
|
755
|
+
txdata = b'\x01' + msg_id + msgpack.packb(response)
|
756
|
+
self.transport.sendto(txdata, address)
|
757
|
+
|
758
|
+
def rpc_call(self, name, address, *args):
|
759
|
+
msg_id = sha1(os.urandom(32)).digest()
|
760
|
+
data = msgpack.packb([name, args])
|
761
|
+
if len(data) > 8192:
|
762
|
+
raise MalformedMessage("Total length of function "
|
763
|
+
"name and arguments cannot exceed 8K")
|
764
|
+
txdata = b'\x00' + msg_id + data
|
765
|
+
log.debug("calling remote function %s on %s (msgid %s)", name, address,
|
766
|
+
b64encode(msg_id))
|
767
|
+
self.transport.sendto(txdata, address)
|
768
|
+
|
769
|
+
loop = asyncio.get_event_loop()
|
770
|
+
if hasattr(loop, 'create_future'):
|
771
|
+
future = loop.create_future()
|
772
|
+
else:
|
773
|
+
future = asyncio.Future()
|
774
|
+
timeout = loop.call_later(self._timeout, self.rpc_cancel, msg_id)
|
775
|
+
self._outstanding[msg_id] = (future, timeout)
|
776
|
+
return future
|
777
|
+
|
778
|
+
def rpc_cancel(self, msg_id):
|
779
|
+
args = (b64encode(msg_id), self._timeout)
|
780
|
+
log.error("Did not receive reply for msg "
|
781
|
+
"id %s within %i seconds", *args)
|
782
|
+
self._outstanding[msg_id][0].set_result((False, None))
|
783
|
+
del self._outstanding[msg_id]
|
784
|
+
|
785
|
+
|
786
|
+
class KademliaProtocol(RPCProtocol):
|
787
|
+
|
788
|
+
def __init__(self, source_node, storage, ksize, wait_timeout=5):
|
789
|
+
super().__init__(wait_timeout)
|
790
|
+
self.router = RoutingTable(self, ksize, source_node)
|
791
|
+
self.storage = storage
|
792
|
+
self.source_node = source_node
|
793
|
+
|
794
|
+
def get_refresh_ids(self):
|
795
|
+
"""
|
796
|
+
Get ids to search for to keep old buckets up to date.
|
797
|
+
"""
|
798
|
+
ids = []
|
799
|
+
for bucket in self.router.lonely_buckets():
|
800
|
+
rid = random.randint(*bucket.range).to_bytes(20, byteorder='big')
|
801
|
+
ids.append(rid)
|
802
|
+
return ids
|
803
|
+
|
804
|
+
def on_ping(self, sender, nodeid):
|
805
|
+
source = Node(nodeid, sender[0], sender[1])
|
806
|
+
self.welcome_if_new(source)
|
807
|
+
return self.source_node.id
|
808
|
+
|
809
|
+
def on_store(self, sender, nodeid, key, value):
|
810
|
+
source = Node(nodeid, sender[0], sender[1])
|
811
|
+
self.welcome_if_new(source)
|
812
|
+
log.debug("got a store request from %s, storing '%s'='%s'", sender,
|
813
|
+
key.hex(), value)
|
814
|
+
self.storage[key] = value
|
815
|
+
return True
|
816
|
+
|
817
|
+
def on_find_node(self, sender, nodeid, key):
|
818
|
+
log.info("finding neighbors of %i in local table",
|
819
|
+
int(nodeid.hex(), 16))
|
820
|
+
source = Node(nodeid, sender[0], sender[1])
|
821
|
+
self.welcome_if_new(source)
|
822
|
+
node = Node(key)
|
823
|
+
neighbors = self.router.find_neighbors(node, exclude=source)
|
824
|
+
return list(map(tuple, neighbors))
|
825
|
+
|
826
|
+
def on_find_value(self, sender, nodeid, key):
|
827
|
+
source = Node(nodeid, sender[0], sender[1])
|
828
|
+
self.welcome_if_new(source)
|
829
|
+
value = self.storage.get(key)
|
830
|
+
if value is None:
|
831
|
+
return self.on_find_node(sender, nodeid, key)
|
832
|
+
return {'value': value}
|
833
|
+
|
834
|
+
async def ping(self, address, source_node_id):
|
835
|
+
return await self.rpc_call('ping', address, source_node_id)
|
836
|
+
|
837
|
+
async def store(self, address, source_node_id, key, value):
|
838
|
+
return await self.rpc_call('store', address, source_node_id, key,
|
839
|
+
value)
|
840
|
+
|
841
|
+
async def find_node(self, address, source_node_id, node_to_find_id):
|
842
|
+
return await self.rpc_call('find_node', address, source_node_id,
|
843
|
+
node_to_find_id)
|
844
|
+
|
845
|
+
async def find_value(self, address, source_node_id, node_to_find_id):
|
846
|
+
return await self.rpc_call('find_value', address, source_node_id,
|
847
|
+
node_to_find_id)
|
848
|
+
|
849
|
+
async def call_find_node(self, node_to_ask, node_to_find):
|
850
|
+
address = (node_to_ask.ip, node_to_ask.port)
|
851
|
+
result = await self.find_node(address, self.source_node.id,
|
852
|
+
node_to_find.id)
|
853
|
+
return self.handle_rpc_result(result, node_to_ask)
|
854
|
+
|
855
|
+
async def call_find_value(self, node_to_ask, node_to_find):
|
856
|
+
address = (node_to_ask.ip, node_to_ask.port)
|
857
|
+
result = await self.find_value(address, self.source_node.id,
|
858
|
+
node_to_find.id)
|
859
|
+
return self.handle_rpc_result(result, node_to_ask)
|
860
|
+
|
861
|
+
async def call_ping(self, node_to_ask):
|
862
|
+
address = (node_to_ask.ip, node_to_ask.port)
|
863
|
+
result = await self.ping(address, self.source_node.id)
|
864
|
+
return self.handle_rpc_result(result, node_to_ask)
|
865
|
+
|
866
|
+
async def call_store(self, node_to_ask, key, value):
|
867
|
+
address = (node_to_ask.ip, node_to_ask.port)
|
868
|
+
result = await self.store(address, self.source_node.id, key, value)
|
869
|
+
return self.handle_rpc_result(result, node_to_ask)
|
870
|
+
|
871
|
+
def welcome_if_new(self, node):
|
872
|
+
"""
|
873
|
+
Given a new node, send it all the keys/values it should be storing,
|
874
|
+
then add it to the routing table.
|
875
|
+
@param node: A new node that just joined (or that we just found out
|
876
|
+
about).
|
877
|
+
Process:
|
878
|
+
For each key in storage, get k closest nodes. If newnode is closer
|
879
|
+
than the furtherst in that list, and the node for this server
|
880
|
+
is closer than the closest in that list, then store the key/value
|
881
|
+
on the new node (per section 2.5 of the paper)
|
882
|
+
"""
|
883
|
+
if not self.router.is_new_node(node):
|
884
|
+
return
|
885
|
+
|
886
|
+
log.info("never seen %s before, adding to router", node)
|
887
|
+
for key, value in self.storage:
|
888
|
+
keynode = Node(digest(key))
|
889
|
+
neighbors = self.router.find_neighbors(keynode)
|
890
|
+
if neighbors:
|
891
|
+
last = neighbors[-1].distance_to(keynode)
|
892
|
+
new_node_close = node.distance_to(keynode) < last
|
893
|
+
first = neighbors[0].distance_to(keynode)
|
894
|
+
this_closest = self.source_node.distance_to(keynode) < first
|
895
|
+
if not neighbors or (new_node_close and this_closest):
|
896
|
+
asyncio.ensure_future(self.call_store(node, key, value))
|
897
|
+
self.router.add_contact(node)
|
898
|
+
|
899
|
+
def handle_rpc_result(self, result, node):
|
900
|
+
"""
|
901
|
+
If we get a response, add the node to the routing table. If
|
902
|
+
we get no response, make sure it's removed from the routing table.
|
903
|
+
"""
|
904
|
+
if not result[0]:
|
905
|
+
log.warning("no response from %s, removing from router", node)
|
906
|
+
self.router.remove_contact(node)
|
907
|
+
return result
|
908
|
+
|
909
|
+
log.info("got successful response from %s", node)
|
910
|
+
self.welcome_if_new(node)
|
911
|
+
return result
|
912
|
+
|
913
|
+
|
914
|
+
class Server:
|
915
|
+
"""
|
916
|
+
High level view of a node instance. This is the object that should be
|
917
|
+
created to start listening as an active node on the network.
|
918
|
+
"""
|
919
|
+
|
920
|
+
protocol_class = KademliaProtocol
|
921
|
+
|
922
|
+
def __init__(self, ksize=20, alpha=3, node_id=None, storage=None):
|
923
|
+
"""
|
924
|
+
Create a server instance. This will start listening on the given port.
|
925
|
+
Args:
|
926
|
+
ksize (int): The k parameter from the paper
|
927
|
+
alpha (int): The alpha parameter from the paper
|
928
|
+
node_id: The id for this node on the network.
|
929
|
+
storage: An instance that implements the interface
|
930
|
+
:class:`~kademlia.storage.IStorage`
|
931
|
+
"""
|
932
|
+
self.ksize = ksize
|
933
|
+
self.alpha = alpha
|
934
|
+
self.storage = storage or ForgetfulStorage()
|
935
|
+
self.node = Node(node_id
|
936
|
+
or uuid.uuid1().bytes[4:] + secrets.token_bytes(8))
|
937
|
+
self.transport = None
|
938
|
+
self.protocol = None
|
939
|
+
self.refresh_loop = None
|
940
|
+
self.save_state_loop = None
|
941
|
+
|
942
|
+
def stop(self):
|
943
|
+
if self.transport is not None:
|
944
|
+
self.transport.close()
|
945
|
+
|
946
|
+
if self.refresh_loop:
|
947
|
+
self.refresh_loop.cancel()
|
948
|
+
|
949
|
+
if self.save_state_loop:
|
950
|
+
self.save_state_loop.cancel()
|
951
|
+
|
952
|
+
def _create_protocol(self):
|
953
|
+
return self.protocol_class(self.node, self.storage, self.ksize)
|
954
|
+
|
955
|
+
async def listen(self, port, interface='0.0.0.0', interval=300):
|
956
|
+
"""
|
957
|
+
Start listening on the given port.
|
958
|
+
Provide interface="::" to accept ipv6 address
|
959
|
+
"""
|
960
|
+
loop = asyncio.get_event_loop()
|
961
|
+
listen = loop.create_datagram_endpoint(self._create_protocol,
|
962
|
+
local_addr=(interface, port))
|
963
|
+
log.info("Node %i listening on %s:%i", self.node.long_id, interface,
|
964
|
+
port)
|
965
|
+
self.transport, self.protocol = await listen
|
966
|
+
# finally, schedule refreshing table
|
967
|
+
self.refresh_table(interval)
|
968
|
+
|
969
|
+
def refresh_table(self, interval=300):
|
970
|
+
log.debug("Refreshing routing table")
|
971
|
+
asyncio.ensure_future(self._refresh_table(interval))
|
972
|
+
loop = asyncio.get_running_loop()
|
973
|
+
self.refresh_loop = loop.call_later(interval, self.refresh_table,
|
974
|
+
interval)
|
975
|
+
|
976
|
+
async def _refresh_table(self, interval=300):
|
977
|
+
"""
|
978
|
+
Refresh buckets that haven't had any lookups in the last hour
|
979
|
+
(per section 2.3 of the paper).
|
980
|
+
"""
|
981
|
+
results = []
|
982
|
+
for node_id in self.protocol.get_refresh_ids():
|
983
|
+
node = Node(node_id)
|
984
|
+
nearest = self.protocol.router.find_neighbors(node, self.alpha)
|
985
|
+
spider = NodeSpiderCrawl(self.protocol, node, nearest, self.ksize,
|
986
|
+
self.alpha)
|
987
|
+
results.append(spider.find())
|
988
|
+
|
989
|
+
# do our crawling
|
990
|
+
await asyncio.gather(*results)
|
991
|
+
|
992
|
+
# now republish keys older than one hour
|
993
|
+
for dkey, value in self.storage.iter_older_than(interval):
|
994
|
+
await self.set_digest(dkey, value)
|
995
|
+
|
996
|
+
def bootstrappable_neighbors(self):
|
997
|
+
"""
|
998
|
+
Get a :class:`list` of (ip, port) :class:`tuple` pairs suitable for
|
999
|
+
use as an argument to the bootstrap method.
|
1000
|
+
The server should have been bootstrapped
|
1001
|
+
already - this is just a utility for getting some neighbors and then
|
1002
|
+
storing them if this server is going down for a while. When it comes
|
1003
|
+
back up, the list of nodes can be used to bootstrap.
|
1004
|
+
"""
|
1005
|
+
neighbors = self.protocol.router.find_neighbors(self.node)
|
1006
|
+
return [tuple(n)[-2:] for n in neighbors]
|
1007
|
+
|
1008
|
+
async def bootstrap(self, addrs):
|
1009
|
+
"""
|
1010
|
+
Bootstrap the server by connecting to other known nodes in the network.
|
1011
|
+
Args:
|
1012
|
+
addrs: A `list` of (ip, port) `tuple` pairs. Note that only IP
|
1013
|
+
addresses are acceptable - hostnames will cause an error.
|
1014
|
+
"""
|
1015
|
+
log.debug("Attempting to bootstrap node with %i initial contacts",
|
1016
|
+
len(addrs))
|
1017
|
+
cos = list(map(self.bootstrap_node, addrs))
|
1018
|
+
gathered = await asyncio.gather(*cos)
|
1019
|
+
nodes = [node for node in gathered if node is not None]
|
1020
|
+
spider = NodeSpiderCrawl(self.protocol, self.node, nodes, self.ksize,
|
1021
|
+
self.alpha)
|
1022
|
+
return await spider.find()
|
1023
|
+
|
1024
|
+
async def bootstrap_node(self, addr):
|
1025
|
+
result = await self.protocol.ping(addr, self.node.id)
|
1026
|
+
return Node(result[1], addr[0], addr[1]) if result[0] else None
|
1027
|
+
|
1028
|
+
async def get(self, key):
|
1029
|
+
"""
|
1030
|
+
Get a key if the network has it.
|
1031
|
+
Returns:
|
1032
|
+
:class:`None` if not found, the value otherwise.
|
1033
|
+
"""
|
1034
|
+
log.info("Looking up key %s", key)
|
1035
|
+
dkey = digest(key)
|
1036
|
+
# if this node has it, return it
|
1037
|
+
if self.storage.get(dkey) is not None:
|
1038
|
+
return self.storage.get(dkey)
|
1039
|
+
node = Node(dkey)
|
1040
|
+
nearest = self.protocol.router.find_neighbors(node)
|
1041
|
+
if not nearest:
|
1042
|
+
log.warning("There are no known neighbors to get key %s", key)
|
1043
|
+
return None
|
1044
|
+
spider = ValueSpiderCrawl(self.protocol, node, nearest, self.ksize,
|
1045
|
+
self.alpha)
|
1046
|
+
return await spider.find()
|
1047
|
+
|
1048
|
+
async def set(self, key, value):
|
1049
|
+
"""
|
1050
|
+
Set the given string key to the given value in the network.
|
1051
|
+
"""
|
1052
|
+
if not check_dht_value_type(value):
|
1053
|
+
raise TypeError(
|
1054
|
+
"Value must be of type int, float, bool, str, or bytes")
|
1055
|
+
log.info("setting '%s' = '%s' on network", key, value)
|
1056
|
+
dkey = digest(key)
|
1057
|
+
return await self.set_digest(dkey, value)
|
1058
|
+
|
1059
|
+
async def set_digest(self, dkey, value):
|
1060
|
+
"""
|
1061
|
+
Set the given SHA1 digest key (bytes) to the given value in the
|
1062
|
+
network.
|
1063
|
+
"""
|
1064
|
+
node = Node(dkey)
|
1065
|
+
|
1066
|
+
nearest = self.protocol.router.find_neighbors(node)
|
1067
|
+
if not nearest:
|
1068
|
+
log.warning("There are no known neighbors to set key %s",
|
1069
|
+
dkey.hex())
|
1070
|
+
self.storage[dkey] = value
|
1071
|
+
return True
|
1072
|
+
|
1073
|
+
spider = NodeSpiderCrawl(self.protocol, node, nearest, self.ksize,
|
1074
|
+
self.alpha)
|
1075
|
+
nodes = await spider.find()
|
1076
|
+
log.info("setting '%s' on %s", dkey.hex(), list(map(str, nodes)))
|
1077
|
+
|
1078
|
+
# if this node is close too, then store here as well
|
1079
|
+
biggest = max([n.distance_to(node) for n in nodes])
|
1080
|
+
if self.node.distance_to(node) < biggest:
|
1081
|
+
self.storage[dkey] = value
|
1082
|
+
results = [self.protocol.call_store(n, dkey, value) for n in nodes]
|
1083
|
+
# return true only if at least one store call succeeded
|
1084
|
+
return any(await asyncio.gather(*results))
|
1085
|
+
|
1086
|
+
def save_state(self, fname):
|
1087
|
+
"""
|
1088
|
+
Save the state of this node (the alpha/ksize/id/immediate neighbors)
|
1089
|
+
to a cache file with the given fname.
|
1090
|
+
"""
|
1091
|
+
log.info("Saving state to %s", fname)
|
1092
|
+
data = {
|
1093
|
+
'ksize': self.ksize,
|
1094
|
+
'alpha': self.alpha,
|
1095
|
+
'id': self.node.id,
|
1096
|
+
'neighbors': self.bootstrappable_neighbors(),
|
1097
|
+
'storage': self.storage
|
1098
|
+
}
|
1099
|
+
if not data['neighbors']:
|
1100
|
+
log.warning("No known neighbors, so not writing to cache.")
|
1101
|
+
# return
|
1102
|
+
with open(fname, 'wb') as file:
|
1103
|
+
pickle.dump(data, file)
|
1104
|
+
|
1105
|
+
@classmethod
|
1106
|
+
async def load_state(cls, fname, port, interface='0.0.0.0', interval=300):
|
1107
|
+
"""
|
1108
|
+
Load the state of this node (the alpha/ksize/id/immediate neighbors)
|
1109
|
+
from a cache file with the given fname and then bootstrap the node
|
1110
|
+
(using the given port/interface to start listening/bootstrapping).
|
1111
|
+
"""
|
1112
|
+
log.info("Loading state from %s", fname)
|
1113
|
+
with open(fname, 'rb') as file:
|
1114
|
+
data = pickle.load(file)
|
1115
|
+
svr = cls(data['ksize'], data['alpha'], data['id'], data['storage'])
|
1116
|
+
await svr.listen(port, interface, interval)
|
1117
|
+
if data['neighbors']:
|
1118
|
+
await svr.bootstrap(data['neighbors'])
|
1119
|
+
return svr
|
1120
|
+
|
1121
|
+
def save_state_regularly(self, fname, frequency=300):
|
1122
|
+
"""
|
1123
|
+
Save the state of node with a given regularity to the given
|
1124
|
+
filename.
|
1125
|
+
Args:
|
1126
|
+
fname: File name to save retularly to
|
1127
|
+
frequency: Frequency in seconds that the state should be saved.
|
1128
|
+
By default, 10 minutes.
|
1129
|
+
"""
|
1130
|
+
self.save_state(fname)
|
1131
|
+
loop = asyncio.get_running_loop()
|
1132
|
+
self.save_state_loop = loop.call_later(frequency,
|
1133
|
+
self.save_state_regularly,
|
1134
|
+
fname, frequency)
|
1135
|
+
|
1136
|
+
|
1137
|
+
def check_dht_value_type(value):
|
1138
|
+
"""
|
1139
|
+
Checks to see if the type of the value is a valid type for
|
1140
|
+
placing in the dht.
|
1141
|
+
"""
|
1142
|
+
return isinstance(value, (int, float, bool, str, bytes))
|