mirror of
https://github.com/tahoe-lafs/tahoe-lafs.git
synced 2025-04-13 22:03:04 +00:00
Merge PR416: immutable upload now uses happiness-metric algorithm
Closes ticket:1382
This commit is contained in:
commit
53130c69c0
@ -90,3 +90,89 @@ issues.
|
||||
|
||||
We don't use servers-of-happiness for mutable files yet; this fix will
|
||||
likely come in Tahoe-LAFS version 1.13.
|
||||
|
||||
|
||||
============================
|
||||
Upload Strategy of Happiness
|
||||
============================
|
||||
|
||||
As mentioned above, the uploader is good at detecting instances which
|
||||
do not pass the servers-of-happiness test, but the share distribution algorithm
|
||||
is not always successful in instances where happiness can be achieved. A new
|
||||
placement algorithm designed to pass the servers-of-happiness test, titled
|
||||
'Upload Strategy of Happiness', is meant to fix these instances where the uploader
|
||||
is unable to achieve happiness.
|
||||
|
||||
Calculating Share Placements
|
||||
============================
|
||||
|
||||
We calculate share placement like so:
|
||||
|
||||
0. Start with an ordered list of servers. Maybe *2N* of them.
|
||||
|
||||
1. Query all servers for existing shares.
|
||||
|
||||
1a. Query remaining space from all servers. Every server that has
|
||||
enough free space is considered "readwrite" and every server with too
|
||||
little space is "readonly".
|
||||
|
||||
2. Construct a bipartite graph G1 of *readonly* servers to pre-existing
|
||||
shares, where an edge exists between an arbitrary readonly server S and an
|
||||
arbitrary share T if and only if S currently holds T.
|
||||
|
||||
3. Calculate a maximum matching graph of G1 (a set of S->T edges that has or
|
||||
is-tied-for the highest "happiness score"). There is a clever efficient
|
||||
algorithm for this, named "Ford-Fulkerson". There may be more than one
|
||||
maximum matching for this graph; we choose one of them arbitrarily, but
|
||||
prefer earlier servers. Call this particular placement M1. The placement
|
||||
maps shares to servers, where each share appears at most once, and each
|
||||
server appears at most once.
|
||||
|
||||
4. Construct a bipartite graph G2 of readwrite servers to pre-existing
|
||||
shares. Then remove any edge (from G2) that uses a server or a share found
|
||||
in M1. Let an edge exist between server S and share T if and only if S
|
||||
already holds T.
|
||||
|
||||
5. Calculate a maximum matching graph of G2, call this M2, again preferring
|
||||
earlier servers.
|
||||
|
||||
6. Construct a bipartite graph G3 of (only readwrite) servers to
|
||||
shares (some shares may already exist on a server). Then remove
|
||||
(from G3) any servers and shares used in M1 or M2 (note that we
|
||||
retain servers/shares that were in G1/G2 but *not* in the M1/M2
|
||||
subsets)
|
||||
|
||||
7. Calculate a maximum matching graph of G3, call this M3, preferring earlier
|
||||
servers. The final placement table is the union of M1+M2+M3.
|
||||
|
||||
8. Renew the shares on their respective servers from M1 and M2.
|
||||
|
||||
9. Upload share T to server S if an edge exists between S and T in M3.
|
||||
|
||||
10. If any placements from step 9 fail, mark the server as read-only. Go back
|
||||
to step 2 (since we may discover a server is/has-become read-only, or has
|
||||
failed, during step 9).
|
||||
|
||||
Rationale (Step 4): when we see pre-existing shares on read-only servers, we
|
||||
prefer to rely upon those (rather than the ones on read-write servers), so we
|
||||
can maybe use the read-write servers for new shares. If we picked the
|
||||
read-write server's share, then we couldn't re-use that server for new ones
|
||||
(we only rely upon each server for one share, more or less).
|
||||
|
||||
Properties of Upload Strategy of Happiness
|
||||
==========================================
|
||||
|
||||
The size of the maximum bipartite matching is bounded by the size of the smaller
|
||||
set of vertices. Therefore in a situation where the set of servers is smaller
|
||||
than the set of shares, placement is not generated for a subset of shares. In
|
||||
this case the remaining shares are distributed as evenly as possible across the
|
||||
set of writable servers.
|
||||
|
||||
If the servers-of-happiness criteria can be met, the upload strategy of
|
||||
happiness guarantees that H shares will be placed on the network. During file
|
||||
repair, if the set of servers is larger than N, the algorithm will only attempt
|
||||
to spread shares over N distinct servers. For both initial file upload and file
|
||||
repair, N should be viewed as the maximum number of distinct servers shares
|
||||
can be placed on, and H as the minimum amount. The uploader will fail if
|
||||
the number of distinct servers is less than H, and it will never attempt to
|
||||
exceed N.
|
||||
|
53
integration/test_servers_of_happiness.py
Normal file
53
integration/test_servers_of_happiness.py
Normal file
@ -0,0 +1,53 @@
|
||||
import sys
|
||||
import time
|
||||
import shutil
|
||||
from os import mkdir, unlink, listdir
|
||||
from os.path import join, exists
|
||||
|
||||
from twisted.internet import defer, reactor, task
|
||||
from twisted.internet.error import ProcessTerminated
|
||||
|
||||
import util
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.inlineCallbacks
|
||||
def test_upload_immutable(reactor, temp_dir, introducer_furl, flog_gatherer, storage_nodes, request):
|
||||
|
||||
# hmm, for some reason this still gets storage enabled ...
|
||||
process = yield util._create_node(
|
||||
reactor, request, temp_dir, introducer_furl, flog_gatherer, "edna",
|
||||
web_port="tcp:9983:interface=localhost",
|
||||
storage=False,
|
||||
needed=3,
|
||||
happy=10,
|
||||
total=10,
|
||||
)
|
||||
|
||||
|
||||
node_dir = join(temp_dir, 'edna')
|
||||
|
||||
print("waiting 5 seconds unil we're maybe ready")
|
||||
yield task.deferLater(reactor, 5, lambda: None)
|
||||
|
||||
# upload a file, which should fail because we have don't have 7
|
||||
# storage servers (but happiness is set to 7)
|
||||
proto = util._CollectOutputProtocol()
|
||||
transport = reactor.spawnProcess(
|
||||
proto,
|
||||
sys.executable,
|
||||
[
|
||||
sys.executable, '-m', 'allmydata.scripts.runner',
|
||||
'-d', node_dir,
|
||||
'put', __file__,
|
||||
]
|
||||
)
|
||||
try:
|
||||
yield proto.done
|
||||
assert False, "should raise exception"
|
||||
except Exception as e:
|
||||
assert isinstance(e, ProcessTerminated)
|
||||
|
||||
output = proto.output.getvalue()
|
||||
assert "shares could be placed on only" in output
|
@ -132,7 +132,12 @@ def _run_node(reactor, node_dir, request, magic_text):
|
||||
return protocol.magic_seen
|
||||
|
||||
|
||||
def _create_node(reactor, request, temp_dir, introducer_furl, flog_gatherer, name, web_port, storage=True, magic_text=None):
|
||||
def _create_node(reactor, request, temp_dir, introducer_furl, flog_gatherer, name, web_port,
|
||||
storage=True,
|
||||
magic_text=None,
|
||||
needed=2,
|
||||
happy=3,
|
||||
total=4):
|
||||
"""
|
||||
Helper to create a single node, run it and return the instance
|
||||
spawnProcess returned (ITransport)
|
||||
@ -161,10 +166,11 @@ def _create_node(reactor, request, temp_dir, introducer_furl, flog_gatherer, nam
|
||||
sys.executable,
|
||||
args,
|
||||
)
|
||||
pytest.blockon(done_proto.done)
|
||||
created_d = done_proto.done
|
||||
|
||||
with open(join(node_dir, 'tahoe.cfg'), 'w') as f:
|
||||
f.write('''
|
||||
def created(_):
|
||||
with open(join(node_dir, 'tahoe.cfg'), 'w') as f:
|
||||
f.write('''
|
||||
[node]
|
||||
nickname = %(name)s
|
||||
web.port = %(web_port)s
|
||||
@ -174,18 +180,28 @@ log_gatherer.furl = %(log_furl)s
|
||||
[client]
|
||||
# Which services should this client connect to?
|
||||
introducer.furl = %(furl)s
|
||||
shares.needed = 2
|
||||
shares.happy = 3
|
||||
shares.total = 4
|
||||
shares.needed = %(needed)d
|
||||
shares.happy = %(happy)d
|
||||
shares.total = %(total)d
|
||||
|
||||
''' % {
|
||||
'name': name,
|
||||
'furl': introducer_furl,
|
||||
'web_port': web_port,
|
||||
'log_furl': flog_gatherer,
|
||||
'needed': needed,
|
||||
'happy': happy,
|
||||
'total': total,
|
||||
})
|
||||
created_d.addCallback(created)
|
||||
else:
|
||||
created_d = defer.succeed(None)
|
||||
|
||||
return _run_node(reactor, node_dir, request, magic_text)
|
||||
d = Deferred()
|
||||
d.callback(None)
|
||||
d.addCallback(lambda _: created_d)
|
||||
d.addCallback(lambda _: _run_node(reactor, node_dir, request, magic_text))
|
||||
return d
|
||||
|
||||
|
||||
def await_file_contents(path, contents, timeout=15):
|
||||
|
1
setup.py
1
setup.py
@ -284,6 +284,7 @@ setup(name="tahoe-lafs", # also set in __init__.py
|
||||
"txi2p >= 0.3.2", # in case pip's resolver doesn't work
|
||||
"pytest",
|
||||
"pytest-twisted",
|
||||
"hypothesis >= 3.6.1",
|
||||
],
|
||||
"tor": [
|
||||
"foolscap[tor] >= 0.12.5",
|
||||
|
@ -655,6 +655,6 @@ class Client(node.Node, pollmixin.PollMixin):
|
||||
return self.nodemaker.create_mutable_file(contents, keysize,
|
||||
version=version)
|
||||
|
||||
def upload(self, uploadable):
|
||||
def upload(self, uploadable, reactor=None):
|
||||
uploader = self.getServiceNamed("uploader")
|
||||
return uploader.upload(uploadable)
|
||||
return uploader.upload(uploadable, reactor=reactor)
|
||||
|
@ -72,6 +72,7 @@ class ControlServer(Referenceable, service.Service):
|
||||
f.close()
|
||||
uploader = self.parent.getServiceNamed("uploader")
|
||||
u = upload.FileName(filename, convergence=convergence)
|
||||
# XXX should pass reactor arg
|
||||
d = uploader.upload(u)
|
||||
d.addCallback(lambda results: results.get_uri())
|
||||
def _done(uri):
|
||||
|
@ -599,6 +599,7 @@ class DirectoryNode(object):
|
||||
name = normalize(namex)
|
||||
if self.is_readonly():
|
||||
return defer.fail(NotWriteableError())
|
||||
# XXX should pass reactor arg
|
||||
d = self._uploader.upload(uploadable, progress=progress)
|
||||
d.addCallback(lambda results:
|
||||
self._create_and_validate_node(results.get_uri(), None,
|
||||
|
@ -724,12 +724,16 @@ class Checker(log.PrefixingLogMixin):
|
||||
|
||||
def _check_server_shares(self, s):
|
||||
"""Return a deferred which eventually fires with a tuple of
|
||||
(set(sharenum), server, set(), set(), responded) showing all the
|
||||
shares claimed to be served by this server. In case the server is
|
||||
disconnected then it fires with (set(), server, set(), set(), False)
|
||||
(a server disconnecting when we ask it for buckets is the same, for
|
||||
our purposes, as a server that says it has none, except that we want
|
||||
to track and report whether or not each server responded.)"""
|
||||
(set(sharenum), server, set(corrupt), set(incompatible),
|
||||
responded) showing all the shares claimed to be served by this
|
||||
server. In case the server is disconnected then it fires with
|
||||
(set(), server, set(), set(), False) (a server disconnecting
|
||||
when we ask it for buckets is the same, for our purposes, as a
|
||||
server that says it has none, except that we want to track and
|
||||
report whether or not each server responded.)
|
||||
|
||||
see also _verify_server_shares()
|
||||
"""
|
||||
def _curry_empty_corrupted(res):
|
||||
buckets, responded = res
|
||||
return (set(buckets), s, set(), set(), responded)
|
||||
|
@ -18,9 +18,12 @@ from common import COMPLETE, CORRUPT, DEAD, BADSEGNUM
|
||||
|
||||
class LayoutInvalid(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class DataUnavailable(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class Share:
|
||||
"""I represent a single instance of a single share (e.g. I reference the
|
||||
shnum2 for share SI=abcde on server xy12t, not the one on server ab45q).
|
||||
|
@ -122,7 +122,7 @@ class Encoder(object):
|
||||
assert not self._codec
|
||||
k, happy, n, segsize = params
|
||||
self.required_shares = k
|
||||
self.servers_of_happiness = happy
|
||||
self.min_happiness = happy
|
||||
self.num_shares = n
|
||||
self.segment_size = segsize
|
||||
self.log("got encoding parameters: %d/%d/%d %d" % (k,happy,n, segsize))
|
||||
@ -180,7 +180,7 @@ class Encoder(object):
|
||||
if name == "storage_index":
|
||||
return self._storage_index
|
||||
elif name == "share_counts":
|
||||
return (self.required_shares, self.servers_of_happiness,
|
||||
return (self.required_shares, self.min_happiness,
|
||||
self.num_shares)
|
||||
elif name == "num_segments":
|
||||
return self.num_segments
|
||||
@ -503,17 +503,17 @@ class Encoder(object):
|
||||
self.log("they weren't in our list of landlords", parent=ln,
|
||||
level=log.WEIRD, umid="TQGFRw")
|
||||
happiness = happinessutil.servers_of_happiness(self.servermap)
|
||||
if happiness < self.servers_of_happiness:
|
||||
if happiness < self.min_happiness:
|
||||
peerids = set(happinessutil.shares_by_server(self.servermap).keys())
|
||||
msg = happinessutil.failure_message(len(peerids),
|
||||
self.required_shares,
|
||||
self.servers_of_happiness,
|
||||
self.min_happiness,
|
||||
happiness)
|
||||
msg = "%s: %s" % (msg, why)
|
||||
raise UploadUnhappinessError(msg)
|
||||
self.log("but we can still continue with %s shares, we'll be happy "
|
||||
"with at least %s" % (happiness,
|
||||
self.servers_of_happiness),
|
||||
self.min_happiness),
|
||||
parent=ln)
|
||||
|
||||
def _gather_responses(self, dl):
|
||||
|
409
src/allmydata/immutable/happiness_upload.py
Normal file
409
src/allmydata/immutable/happiness_upload.py
Normal file
@ -0,0 +1,409 @@
|
||||
|
||||
from Queue import PriorityQueue
|
||||
|
||||
|
||||
def augmenting_path_for(graph):
|
||||
"""
|
||||
I return an augmenting path, if there is one, from the source node
|
||||
to the sink node in the flow network represented by my graph argument.
|
||||
If there is no augmenting path, I return False. I assume that the
|
||||
source node is at index 0 of graph, and the sink node is at the last
|
||||
index. I also assume that graph is a flow network in adjacency list
|
||||
form.
|
||||
"""
|
||||
bfs_tree = bfs(graph, 0)
|
||||
if bfs_tree[len(graph) - 1]:
|
||||
n = len(graph) - 1
|
||||
path = [] # [(u, v)], where u and v are vertices in the graph
|
||||
while n != 0:
|
||||
path.insert(0, (bfs_tree[n], n))
|
||||
n = bfs_tree[n]
|
||||
return path
|
||||
return False
|
||||
|
||||
def bfs(graph, s):
|
||||
"""
|
||||
Perform a BFS on graph starting at s, where graph is a graph in
|
||||
adjacency list form, and s is a node in graph. I return the
|
||||
predecessor table that the BFS generates.
|
||||
"""
|
||||
# This is an adaptation of the BFS described in "Introduction to
|
||||
# Algorithms", Cormen et al, 2nd ed., p. 532.
|
||||
# WHITE vertices are those that we haven't seen or explored yet.
|
||||
WHITE = 0
|
||||
# GRAY vertices are those we have seen, but haven't explored yet
|
||||
GRAY = 1
|
||||
# BLACK vertices are those we have seen and explored
|
||||
BLACK = 2
|
||||
color = [WHITE for i in xrange(len(graph))]
|
||||
predecessor = [None for i in xrange(len(graph))]
|
||||
distance = [-1 for i in xrange(len(graph))]
|
||||
queue = [s] # vertices that we haven't explored yet.
|
||||
color[s] = GRAY
|
||||
distance[s] = 0
|
||||
while queue:
|
||||
n = queue.pop(0)
|
||||
for v in graph[n]:
|
||||
if color[v] == WHITE:
|
||||
color[v] = GRAY
|
||||
distance[v] = distance[n] + 1
|
||||
predecessor[v] = n
|
||||
queue.append(v)
|
||||
color[n] = BLACK
|
||||
return predecessor
|
||||
|
||||
def residual_network(graph, f):
|
||||
"""
|
||||
I return the residual network and residual capacity function of the
|
||||
flow network represented by my graph and f arguments. graph is a
|
||||
flow network in adjacency-list form, and f is a flow in graph.
|
||||
"""
|
||||
new_graph = [[] for i in xrange(len(graph))]
|
||||
cf = [[0 for s in xrange(len(graph))] for sh in xrange(len(graph))]
|
||||
for i in xrange(len(graph)):
|
||||
for v in graph[i]:
|
||||
if f[i][v] == 1:
|
||||
# We add an edge (v, i) with cf[v,i] = 1. This means
|
||||
# that we can remove 1 unit of flow from the edge (i, v)
|
||||
new_graph[v].append(i)
|
||||
cf[v][i] = 1
|
||||
cf[i][v] = -1
|
||||
else:
|
||||
# We add the edge (i, v), since we're not using it right
|
||||
# now.
|
||||
new_graph[i].append(v)
|
||||
cf[i][v] = 1
|
||||
cf[v][i] = -1
|
||||
return (new_graph, cf)
|
||||
|
||||
|
||||
def calculate_happiness(mappings):
|
||||
"""
|
||||
:param mappings: a dict mapping 'share' -> 'peer'
|
||||
|
||||
:returns: the happiness, which is the number of unique peers we've
|
||||
placed shares on.
|
||||
"""
|
||||
unique_peers = set(mappings.values())
|
||||
assert None not in unique_peers
|
||||
return len(unique_peers)
|
||||
|
||||
|
||||
def _calculate_mappings(peers, shares, servermap=None):
|
||||
"""
|
||||
Given a set of peers, a set of shares, and a dictionary of server ->
|
||||
set(shares), determine how the uploader should allocate shares. If a
|
||||
servermap is supplied, determine which existing allocations should be
|
||||
preserved. If servermap is None, calculate the maximum matching of the
|
||||
bipartite graph (U, V, E) such that:
|
||||
|
||||
U = peers
|
||||
V = shares
|
||||
E = peers x shares
|
||||
|
||||
Returns a dictionary {share -> set(peer)}, indicating that the share
|
||||
should be placed on each peer in the set. If a share's corresponding
|
||||
value is None, the share can be placed on any server. Note that the set
|
||||
of peers should only be one peer when returned, but it is possible to
|
||||
duplicate shares by adding additional servers to the set.
|
||||
"""
|
||||
peer_to_index, index_to_peer = _reindex(peers, 1)
|
||||
share_to_index, index_to_share = _reindex(shares, len(peers) + 1)
|
||||
shareIndices = [share_to_index[s] for s in shares]
|
||||
if servermap:
|
||||
graph = _servermap_flow_graph(peers, shares, servermap)
|
||||
else:
|
||||
peerIndices = [peer_to_index[peer] for peer in peers]
|
||||
graph = _flow_network(peerIndices, shareIndices)
|
||||
max_graph = _compute_maximum_graph(graph, shareIndices)
|
||||
return _convert_mappings(index_to_peer, index_to_share, max_graph)
|
||||
|
||||
|
||||
def _compute_maximum_graph(graph, shareIndices):
|
||||
"""
|
||||
This is an implementation of the Ford-Fulkerson method for finding
|
||||
a maximum flow in a flow network applied to a bipartite graph.
|
||||
Specifically, it is the Edmonds-Karp algorithm, since it uses a
|
||||
BFS to find the shortest augmenting path at each iteration, if one
|
||||
exists.
|
||||
|
||||
The implementation here is an adapation of an algorithm described in
|
||||
"Introduction to Algorithms", Cormen et al, 2nd ed., pp 658-662.
|
||||
"""
|
||||
|
||||
if graph == []:
|
||||
return {}
|
||||
|
||||
dim = len(graph)
|
||||
flow_function = [[0 for sh in xrange(dim)] for s in xrange(dim)]
|
||||
residual_graph, residual_function = residual_network(graph, flow_function)
|
||||
|
||||
while augmenting_path_for(residual_graph):
|
||||
path = augmenting_path_for(residual_graph)
|
||||
# Delta is the largest amount that we can increase flow across
|
||||
# all of the edges in path. Because of the way that the residual
|
||||
# function is constructed, f[u][v] for a particular edge (u, v)
|
||||
# is the amount of unused capacity on that edge. Taking the
|
||||
# minimum of a list of those values for each edge in the
|
||||
# augmenting path gives us our delta.
|
||||
delta = min(map(lambda (u, v), rf=residual_function: rf[u][v],
|
||||
path))
|
||||
for (u, v) in path:
|
||||
flow_function[u][v] += delta
|
||||
flow_function[v][u] -= delta
|
||||
residual_graph, residual_function = residual_network(graph,flow_function)
|
||||
|
||||
new_mappings = {}
|
||||
for shareIndex in shareIndices:
|
||||
peer = residual_graph[shareIndex]
|
||||
if peer == [dim - 1]:
|
||||
new_mappings.setdefault(shareIndex, None)
|
||||
else:
|
||||
new_mappings.setdefault(shareIndex, peer[0])
|
||||
|
||||
return new_mappings
|
||||
|
||||
|
||||
def _extract_ids(mappings):
|
||||
shares = set()
|
||||
peers = set()
|
||||
for share in mappings:
|
||||
if mappings[share] == None:
|
||||
pass
|
||||
else:
|
||||
shares.add(share)
|
||||
for item in mappings[share]:
|
||||
peers.add(item)
|
||||
return (peers, shares)
|
||||
|
||||
def _distribute_homeless_shares(mappings, homeless_shares, peers_to_shares):
|
||||
"""
|
||||
Shares which are not mapped to a peer in the maximum spanning graph
|
||||
still need to be placed on a server. This function attempts to
|
||||
distribute those homeless shares as evenly as possible over the
|
||||
available peers. If possible a share will be placed on the server it was
|
||||
originally on, signifying the lease should be renewed instead.
|
||||
"""
|
||||
#print "mappings, homeless_shares, peers_to_shares %s %s %s" % (mappings, homeless_shares, peers_to_shares)
|
||||
servermap_peerids = set([key for key in peers_to_shares])
|
||||
servermap_shareids = set()
|
||||
for key in sorted(peers_to_shares.keys()):
|
||||
# XXX maybe sort?
|
||||
for share in peers_to_shares[key]:
|
||||
servermap_shareids.add(share)
|
||||
|
||||
# First check to see if the leases can be renewed.
|
||||
to_distribute = set()
|
||||
for share in homeless_shares:
|
||||
if share in servermap_shareids:
|
||||
for peerid in peers_to_shares:
|
||||
if share in peers_to_shares[peerid]:
|
||||
mappings[share] = set([peerid])
|
||||
break
|
||||
else:
|
||||
to_distribute.add(share)
|
||||
# This builds a priority queue of peers with the number of shares
|
||||
# each peer holds as the priority.
|
||||
priority = {}
|
||||
pQueue = PriorityQueue()
|
||||
for peerid in servermap_peerids:
|
||||
priority.setdefault(peerid, 0)
|
||||
for share in mappings:
|
||||
if mappings[share] is not None:
|
||||
for peer in mappings[share]:
|
||||
if peer in servermap_peerids:
|
||||
priority[peer] += 1
|
||||
if priority == {}:
|
||||
return
|
||||
for peerid in priority:
|
||||
pQueue.put((priority[peerid], peerid))
|
||||
# Distribute the shares to peers with the lowest priority.
|
||||
for share in to_distribute:
|
||||
peer = pQueue.get()
|
||||
mappings[share] = set([peer[1]])
|
||||
pQueue.put((peer[0]+1, peer[1]))
|
||||
|
||||
def _convert_mappings(index_to_peer, index_to_share, maximum_graph):
|
||||
"""
|
||||
Now that a maximum spanning graph has been found, convert the indexes
|
||||
back to their original ids so that the client can pass them to the
|
||||
uploader.
|
||||
"""
|
||||
|
||||
converted_mappings = {}
|
||||
for share in maximum_graph:
|
||||
peer = maximum_graph[share]
|
||||
if peer == None:
|
||||
converted_mappings.setdefault(index_to_share[share], None)
|
||||
else:
|
||||
converted_mappings.setdefault(index_to_share[share], set([index_to_peer[peer]]))
|
||||
return converted_mappings
|
||||
|
||||
|
||||
def _servermap_flow_graph(peers, shares, servermap):
|
||||
"""
|
||||
Generates a flow network of peerIndices to shareIndices from a server map
|
||||
of 'peer' -> ['shares']. According to Wikipedia, "a flow network is a
|
||||
directed graph where each edge has a capacity and each edge receives a flow.
|
||||
The amount of flow on an edge cannot exceed the capacity of the edge." This
|
||||
is necessary because in order to find the maximum spanning, the Edmonds-Karp algorithm
|
||||
converts the problem into a maximum flow problem.
|
||||
"""
|
||||
if servermap == {}:
|
||||
return []
|
||||
|
||||
peer_to_index, index_to_peer = _reindex(peers, 1)
|
||||
share_to_index, index_to_share = _reindex(shares, len(peers) + 1)
|
||||
graph = []
|
||||
indexedShares = []
|
||||
sink_num = len(peers) + len(shares) + 1
|
||||
graph.append([peer_to_index[peer] for peer in peers])
|
||||
#print "share_to_index %s" % share_to_index
|
||||
#print "servermap %s" % servermap
|
||||
for peer in peers:
|
||||
if servermap.has_key(peer):
|
||||
for s in servermap[peer]:
|
||||
if share_to_index.has_key(s):
|
||||
indexedShares.append(share_to_index[s])
|
||||
graph.insert(peer_to_index[peer], indexedShares)
|
||||
for share in shares:
|
||||
graph.insert(share_to_index[share], [sink_num])
|
||||
graph.append([])
|
||||
return graph
|
||||
|
||||
|
||||
def _reindex(items, base):
|
||||
"""
|
||||
I take an iteratble of items and give each item an index to be used in
|
||||
the construction of a flow network. Indices for these items start at base
|
||||
and continue to base + len(items) - 1.
|
||||
|
||||
I return two dictionaries: ({item: index}, {index: item})
|
||||
"""
|
||||
item_to_index = {}
|
||||
index_to_item = {}
|
||||
for item in items:
|
||||
item_to_index.setdefault(item, base)
|
||||
index_to_item.setdefault(base, item)
|
||||
base += 1
|
||||
return (item_to_index, index_to_item)
|
||||
|
||||
|
||||
def _flow_network(peerIndices, shareIndices):
|
||||
"""
|
||||
Given set of peerIndices and a set of shareIndices, I create a flow network
|
||||
to be used by _compute_maximum_graph. The return value is a two
|
||||
dimensional list in the form of a flow network, where each index represents
|
||||
a node, and the corresponding list represents all of the nodes it is connected
|
||||
to.
|
||||
|
||||
This function is similar to allmydata.util.happinessutil.flow_network_for, but
|
||||
we connect every peer with all shares instead of reflecting a supplied servermap.
|
||||
"""
|
||||
graph = []
|
||||
# The first entry in our flow network is the source.
|
||||
# Connect the source to every server.
|
||||
graph.append(peerIndices)
|
||||
sink_num = len(peerIndices + shareIndices) + 1
|
||||
# Connect every server with every share it can possibly store.
|
||||
for peerIndex in peerIndices:
|
||||
graph.insert(peerIndex, shareIndices)
|
||||
# Connect every share with the sink.
|
||||
for shareIndex in shareIndices:
|
||||
graph.insert(shareIndex, [sink_num])
|
||||
# Add an empty entry for the sink.
|
||||
graph.append([])
|
||||
return graph
|
||||
|
||||
def share_placement(peers, readonly_peers, shares, peers_to_shares):
|
||||
"""
|
||||
Generates the allocations the upload should based on the given
|
||||
information. We construct a dictionary of 'share_num' ->
|
||||
'server_id' and return it to the caller. Existing allocations
|
||||
appear as placements because attempting to place an existing
|
||||
allocation will renew the share.
|
||||
|
||||
For more information on the algorithm this class implements, refer to
|
||||
docs/specifications/servers-of-happiness.rst
|
||||
"""
|
||||
if not peers:
|
||||
return dict()
|
||||
|
||||
homeless_shares = set()
|
||||
|
||||
# First calculate share placement for the readonly servers.
|
||||
readonly_peers = readonly_peers
|
||||
readonly_shares = set()
|
||||
readonly_map = {}
|
||||
for peer in sorted(peers_to_shares.keys()):
|
||||
if peer in readonly_peers:
|
||||
readonly_map.setdefault(peer, peers_to_shares[peer])
|
||||
for share in peers_to_shares[peer]:
|
||||
readonly_shares.add(share)
|
||||
|
||||
readonly_mappings = _calculate_mappings(readonly_peers, readonly_shares, readonly_map)
|
||||
used_peers, used_shares = _extract_ids(readonly_mappings)
|
||||
|
||||
# Calculate share placement for the remaining existing allocations
|
||||
new_peers = set(peers) - used_peers
|
||||
# Squash a list of sets into one set
|
||||
new_shares = shares - used_shares
|
||||
|
||||
servermap = peers_to_shares.copy()
|
||||
for peer in sorted(peers_to_shares.keys()):
|
||||
if peer in used_peers:
|
||||
servermap.pop(peer, None)
|
||||
else:
|
||||
servermap[peer] = set(servermap[peer]) - used_shares
|
||||
if servermap[peer] == set():
|
||||
servermap.pop(peer, None)
|
||||
# allmydata.test.test_upload.EncodingParameters.test_exception_messages_during_server_selection
|
||||
# allmydata.test.test_upload.EncodingParameters.test_problem_layout_comment_52
|
||||
# both ^^ trigger a "keyerror" here .. just ignoring is right? (fixes the tests, but ...)
|
||||
try:
|
||||
new_peers.remove(peer)
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
existing_mappings = _calculate_mappings(new_peers, new_shares, servermap)
|
||||
existing_peers, existing_shares = _extract_ids(existing_mappings)
|
||||
|
||||
# Calculate share placement for the remaining peers and shares which
|
||||
# won't be preserved by existing allocations.
|
||||
new_peers = new_peers - existing_peers - used_peers
|
||||
|
||||
|
||||
new_shares = new_shares - existing_shares - used_shares
|
||||
new_mappings = _calculate_mappings(new_peers, new_shares)
|
||||
#print "new_peers %s" % new_peers
|
||||
#print "new_mappings %s" % new_mappings
|
||||
mappings = dict(readonly_mappings.items() + existing_mappings.items() + new_mappings.items())
|
||||
homeless_shares = set()
|
||||
for share in mappings:
|
||||
if mappings[share] is None:
|
||||
homeless_shares.add(share)
|
||||
if len(homeless_shares) != 0:
|
||||
# 'servermap' should contain only read/write peers
|
||||
_distribute_homeless_shares(
|
||||
mappings, homeless_shares,
|
||||
{
|
||||
k: v
|
||||
for k, v in peers_to_shares.items()
|
||||
if k not in readonly_peers
|
||||
}
|
||||
)
|
||||
|
||||
# now, if any share is *still* mapped to None that means "don't
|
||||
# care which server it goes on", so we place it on a round-robin
|
||||
# of read-write servers
|
||||
|
||||
def round_robin(peers):
|
||||
while True:
|
||||
for peer in peers:
|
||||
yield peer
|
||||
peer_iter = round_robin(peers - readonly_peers)
|
||||
|
||||
return {
|
||||
k: v.pop() if v else next(peer_iter)
|
||||
for k, v in mappings.items()
|
||||
}
|
@ -61,6 +61,7 @@ class Repairer(log.PrefixingLogMixin):
|
||||
# (http://tahoe-lafs.org/trac/tahoe-lafs/ticket/1212)
|
||||
happy = 0
|
||||
self._encodingparams = (k, happy, N, segsize)
|
||||
# XXX should pass a reactor to this
|
||||
ul = upload.CHKUploader(self._storage_broker, self._secret_holder)
|
||||
return ul.start(self) # I am the IEncryptedUploadable
|
||||
d.addCallback(_got_segsize)
|
||||
|
@ -9,23 +9,24 @@ from allmydata.util.hashutil import file_renewal_secret_hash, \
|
||||
file_cancel_secret_hash, bucket_renewal_secret_hash, \
|
||||
bucket_cancel_secret_hash, plaintext_hasher, \
|
||||
storage_index_hash, plaintext_segment_hasher, convergence_hasher
|
||||
from allmydata.util.deferredutil import timeout_call
|
||||
from allmydata import hashtree, uri
|
||||
from allmydata.storage.server import si_b2a
|
||||
from allmydata.immutable import encode
|
||||
from allmydata.util import base32, dictutil, idlib, log, mathutil
|
||||
from allmydata.util.happinessutil import servers_of_happiness, \
|
||||
shares_by_server, merge_servers, \
|
||||
failure_message
|
||||
merge_servers, failure_message
|
||||
from allmydata.util.assertutil import precondition, _assert
|
||||
from allmydata.util.rrefutil import add_version_to_remote_reference
|
||||
from allmydata.interfaces import IUploadable, IUploader, IUploadResults, \
|
||||
IEncryptedUploadable, RIEncryptedUploadable, IUploadStatus, \
|
||||
NoServersError, InsufficientVersionError, UploadUnhappinessError, \
|
||||
DEFAULT_MAX_SEGMENT_SIZE, IProgress
|
||||
DEFAULT_MAX_SEGMENT_SIZE, IProgress, IPeerSelector
|
||||
from allmydata.immutable import layout
|
||||
from pycryptopp.cipher.aes import AES
|
||||
|
||||
from cStringIO import StringIO
|
||||
from happiness_upload import share_placement, calculate_happiness
|
||||
|
||||
|
||||
# this wants to live in storage, not here
|
||||
@ -117,7 +118,7 @@ EXTENSION_SIZE = 1000
|
||||
def pretty_print_shnum_to_servers(s):
|
||||
return ', '.join([ "sh%s: %s" % (k, '+'.join([idlib.shortnodeid_b2a(x) for x in v])) for k, v in s.iteritems() ])
|
||||
|
||||
class ServerTracker:
|
||||
class ServerTracker(object):
|
||||
def __init__(self, server,
|
||||
sharesize, blocksize, num_segments, num_share_hashes,
|
||||
storage_index,
|
||||
@ -160,14 +161,14 @@ class ServerTracker:
|
||||
sharenums,
|
||||
self.allocated_size,
|
||||
canary=Referenceable())
|
||||
d.addCallback(self._got_reply)
|
||||
d.addCallback(self._buckets_allocated)
|
||||
return d
|
||||
|
||||
def ask_about_existing_shares(self):
|
||||
rref = self._server.get_rref()
|
||||
return rref.callRemote("get_buckets", self.storage_index)
|
||||
|
||||
def _got_reply(self, (alreadygot, buckets)):
|
||||
def _buckets_allocated(self, (alreadygot, buckets)):
|
||||
#log.msg("%s._got_reply(%s)" % (self, (alreadygot, buckets)))
|
||||
b = {}
|
||||
for sharenum, rref in buckets.iteritems():
|
||||
@ -201,27 +202,143 @@ class ServerTracker:
|
||||
def str_shareloc(shnum, bucketwriter):
|
||||
return "%s: %s" % (shnum, bucketwriter.get_servername(),)
|
||||
|
||||
|
||||
@implementer(IPeerSelector)
|
||||
class PeerSelector(object):
|
||||
|
||||
def __init__(self, num_segments, total_shares, needed_shares, min_happiness):
|
||||
self.num_segments = num_segments
|
||||
self.total_shares = total_shares
|
||||
self.needed_shares = needed_shares
|
||||
self.min_happiness = min_happiness
|
||||
|
||||
self.existing_shares = {}
|
||||
self.peers = set()
|
||||
self.readonly_peers = set()
|
||||
self.bad_peers = set()
|
||||
|
||||
def add_peer_with_share(self, peerid, shnum):
|
||||
try:
|
||||
self.existing_shares[peerid].add(shnum)
|
||||
except KeyError:
|
||||
self.existing_shares[peerid] = set([shnum])
|
||||
|
||||
def add_peer(self, peerid):
|
||||
self.peers.add(peerid)
|
||||
|
||||
def mark_readonly_peer(self, peerid):
|
||||
self.readonly_peers.add(peerid)
|
||||
self.peers.remove(peerid)
|
||||
|
||||
def mark_bad_peer(self, peerid):
|
||||
if peerid in self.peers:
|
||||
self.peers.remove(peerid)
|
||||
self.bad_peers.add(peerid)
|
||||
elif peerid in self.readonly_peers:
|
||||
self.readonly_peers.remove(peerid)
|
||||
self.bad_peers.add(peerid)
|
||||
|
||||
def get_sharemap_of_preexisting_shares(self):
|
||||
preexisting = dictutil.DictOfSets()
|
||||
for server, shares in self.existing_shares.iteritems():
|
||||
for share in shares:
|
||||
preexisting.add(share, server)
|
||||
return preexisting
|
||||
|
||||
def get_share_placements(self):
|
||||
shares = set(range(self.total_shares))
|
||||
self.happiness_mappings = share_placement(self.peers, self.readonly_peers, shares, self.existing_shares)
|
||||
self.happiness = calculate_happiness(self.happiness_mappings)
|
||||
return self.happiness_mappings
|
||||
|
||||
|
||||
class _QueryStatistics(object):
|
||||
|
||||
def __init__(self):
|
||||
self.total = 0
|
||||
self.good = 0
|
||||
self.bad = 0
|
||||
self.full = 0
|
||||
self.error = 0
|
||||
self.contacted = 0
|
||||
|
||||
def __str__(self):
|
||||
return "QueryStatistics(total={} good={} bad={} full={} " \
|
||||
"error={} contacted={})".format(
|
||||
self.total,
|
||||
self.good,
|
||||
self.bad,
|
||||
self.full,
|
||||
self.error,
|
||||
self.contacted,
|
||||
)
|
||||
|
||||
|
||||
class Tahoe2ServerSelector(log.PrefixingLogMixin):
|
||||
|
||||
def __init__(self, upload_id, logparent=None, upload_status=None):
|
||||
def __init__(self, upload_id, logparent=None, upload_status=None, reactor=None):
|
||||
self.upload_id = upload_id
|
||||
self.query_count, self.good_query_count, self.bad_query_count = 0,0,0
|
||||
# Servers that are working normally, but full.
|
||||
self.full_count = 0
|
||||
self.error_count = 0
|
||||
self.num_servers_contacted = 0
|
||||
self._query_stats = _QueryStatistics()
|
||||
self.last_failure_msg = None
|
||||
self._status = IUploadStatus(upload_status)
|
||||
log.PrefixingLogMixin.__init__(self, 'tahoe.immutable.upload', logparent, prefix=upload_id)
|
||||
self.log("starting", level=log.OPERATIONAL)
|
||||
if reactor is None:
|
||||
from twisted.internet import reactor
|
||||
self._reactor = reactor
|
||||
|
||||
def __repr__(self):
|
||||
return "<Tahoe2ServerSelector for upload %s>" % self.upload_id
|
||||
|
||||
def _create_trackers(self, candidate_servers, allocated_size,
|
||||
file_renewal_secret, file_cancel_secret, create_server_tracker):
|
||||
|
||||
# filter the list of servers according to which ones can accomodate
|
||||
# this request. This excludes older servers (which used a 4-byte size
|
||||
# field) from getting large shares (for files larger than about
|
||||
# 12GiB). See #439 for details.
|
||||
def _get_maxsize(server):
|
||||
v0 = server.get_rref().version
|
||||
v1 = v0["http://allmydata.org/tahoe/protocols/storage/v1"]
|
||||
return v1["maximum-immutable-share-size"]
|
||||
|
||||
for server in candidate_servers:
|
||||
self.peer_selector.add_peer(server.get_serverid())
|
||||
writeable_servers = [
|
||||
server for server in candidate_servers
|
||||
if _get_maxsize(server) >= allocated_size
|
||||
]
|
||||
readonly_servers = set(candidate_servers) - set(writeable_servers)
|
||||
|
||||
for server in readonly_servers:
|
||||
self.peer_selector.mark_readonly_peer(server.get_serverid())
|
||||
|
||||
def _make_trackers(servers):
|
||||
trackers = []
|
||||
for s in servers:
|
||||
seed = s.get_lease_seed()
|
||||
renew = bucket_renewal_secret_hash(file_renewal_secret, seed)
|
||||
cancel = bucket_cancel_secret_hash(file_cancel_secret, seed)
|
||||
st = create_server_tracker(s, renew, cancel)
|
||||
trackers.append(st)
|
||||
return trackers
|
||||
|
||||
write_trackers = _make_trackers(writeable_servers)
|
||||
|
||||
# We don't try to allocate shares to these servers, since they've
|
||||
# said that they're incapable of storing shares of the size that we'd
|
||||
# want to store. We ask them about existing shares for this storage
|
||||
# index, which we want to know about for accurate
|
||||
# servers_of_happiness accounting, then we forget about them.
|
||||
readonly_trackers = _make_trackers(readonly_servers)
|
||||
|
||||
return readonly_trackers, write_trackers
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def get_shareholders(self, storage_broker, secret_holder,
|
||||
storage_index, share_size, block_size,
|
||||
num_segments, total_shares, needed_shares,
|
||||
servers_of_happiness):
|
||||
min_happiness):
|
||||
"""
|
||||
@return: (upload_trackers, already_serverids), where upload_trackers
|
||||
is a set of ServerTracker instances that have agreed to hold
|
||||
@ -231,11 +348,17 @@ class Tahoe2ServerSelector(log.PrefixingLogMixin):
|
||||
already have the share.
|
||||
"""
|
||||
|
||||
# re-initialize statistics
|
||||
self._query_status = _QueryStatistics()
|
||||
|
||||
if self._status:
|
||||
self._status.set_status("Contacting Servers..")
|
||||
|
||||
self.peer_selector = PeerSelector(num_segments, total_shares,
|
||||
needed_shares, min_happiness)
|
||||
|
||||
self.total_shares = total_shares
|
||||
self.servers_of_happiness = servers_of_happiness
|
||||
self.min_happiness = min_happiness
|
||||
self.needed_shares = needed_shares
|
||||
|
||||
self.homeless_shares = set(range(total_shares))
|
||||
@ -259,100 +382,217 @@ class Tahoe2ServerSelector(log.PrefixingLogMixin):
|
||||
share_size, 0, num_segments,
|
||||
num_share_hashes, EXTENSION_SIZE)
|
||||
allocated_size = wbp.get_allocated_size()
|
||||
|
||||
# decide upon the renewal/cancel secrets, to include them in the
|
||||
# allocate_buckets query.
|
||||
file_renewal_secret = file_renewal_secret_hash(
|
||||
secret_holder.get_renewal_secret(),
|
||||
storage_index,
|
||||
)
|
||||
file_cancel_secret = file_cancel_secret_hash(
|
||||
secret_holder.get_cancel_secret(),
|
||||
storage_index,
|
||||
)
|
||||
|
||||
# see docs/specifications/servers-of-happiness.rst
|
||||
# 0. Start with an ordered list of servers. Maybe *2N* of them.
|
||||
#
|
||||
|
||||
all_servers = storage_broker.get_servers_for_psi(storage_index)
|
||||
if not all_servers:
|
||||
raise NoServersError("client gave us zero servers")
|
||||
|
||||
# filter the list of servers according to which ones can accomodate
|
||||
# this request. This excludes older servers (which used a 4-byte size
|
||||
# field) from getting large shares (for files larger than about
|
||||
# 12GiB). See #439 for details.
|
||||
def _get_maxsize(server):
|
||||
v0 = server.get_rref().version
|
||||
v1 = v0["http://allmydata.org/tahoe/protocols/storage/v1"]
|
||||
return v1["maximum-immutable-share-size"]
|
||||
writeable_servers = [server for server in all_servers
|
||||
if _get_maxsize(server) >= allocated_size]
|
||||
readonly_servers = set(all_servers[:2*total_shares]) - set(writeable_servers)
|
||||
def _create_server_tracker(server, renew, cancel):
|
||||
return ServerTracker(
|
||||
server, share_size, block_size, num_segments, num_share_hashes,
|
||||
storage_index, renew, cancel,
|
||||
)
|
||||
|
||||
# decide upon the renewal/cancel secrets, to include them in the
|
||||
# allocate_buckets query.
|
||||
client_renewal_secret = secret_holder.get_renewal_secret()
|
||||
client_cancel_secret = secret_holder.get_cancel_secret()
|
||||
readonly_trackers, write_trackers = self._create_trackers(
|
||||
all_servers[:(2 * total_shares)],
|
||||
allocated_size,
|
||||
file_renewal_secret,
|
||||
file_cancel_secret,
|
||||
_create_server_tracker,
|
||||
)
|
||||
|
||||
file_renewal_secret = file_renewal_secret_hash(client_renewal_secret,
|
||||
storage_index)
|
||||
file_cancel_secret = file_cancel_secret_hash(client_cancel_secret,
|
||||
storage_index)
|
||||
def _make_trackers(servers):
|
||||
trackers = []
|
||||
for s in servers:
|
||||
seed = s.get_lease_seed()
|
||||
renew = bucket_renewal_secret_hash(file_renewal_secret, seed)
|
||||
cancel = bucket_cancel_secret_hash(file_cancel_secret, seed)
|
||||
st = ServerTracker(s,
|
||||
share_size, block_size,
|
||||
num_segments, num_share_hashes,
|
||||
storage_index,
|
||||
renew, cancel)
|
||||
trackers.append(st)
|
||||
return trackers
|
||||
# see docs/specifications/servers-of-happiness.rst
|
||||
# 1. Query all servers for existing shares.
|
||||
#
|
||||
# The spec doesn't say what to do for timeouts/errors. This
|
||||
# adds a timeout to each request, and rejects any that reply
|
||||
# with error (i.e. just removed from the list)
|
||||
|
||||
# We assign each servers/trackers into one three lists. They all
|
||||
# start in the "first pass" list. During the first pass, as we ask
|
||||
# each one to hold a share, we move their tracker to the "second
|
||||
# pass" list, until the first-pass list is empty. Then during the
|
||||
# second pass, as we ask each to hold more shares, we move their
|
||||
# tracker to the "next pass" list, until the second-pass list is
|
||||
# empty. Then we move everybody from the next-pass list back to the
|
||||
# second-pass list and repeat the "second" pass (really the third,
|
||||
# fourth, etc pass), until all shares are assigned, or we've run out
|
||||
# of potential servers.
|
||||
self.first_pass_trackers = _make_trackers(writeable_servers)
|
||||
self.second_pass_trackers = [] # servers worth asking again
|
||||
self.next_pass_trackers = [] # servers that we have asked again
|
||||
self._started_second_pass = False
|
||||
|
||||
# We don't try to allocate shares to these servers, since they've
|
||||
# said that they're incapable of storing shares of the size that we'd
|
||||
# want to store. We ask them about existing shares for this storage
|
||||
# index, which we want to know about for accurate
|
||||
# servers_of_happiness accounting, then we forget about them.
|
||||
readonly_trackers = _make_trackers(readonly_servers)
|
||||
|
||||
# We now ask servers that can't hold any new shares about existing
|
||||
# shares that they might have for our SI. Once this is done, we
|
||||
# start placing the shares that we haven't already accounted
|
||||
# for.
|
||||
ds = []
|
||||
if self._status and readonly_trackers:
|
||||
self._status.set_status("Contacting readonly servers to find "
|
||||
"any existing shares")
|
||||
self._status.set_status(
|
||||
"Contacting readonly servers to find any existing shares"
|
||||
)
|
||||
|
||||
# in the "pre servers-of-happiness" code, it was a little
|
||||
# ambigious whether "merely asking" counted as a "query" or
|
||||
# not, because "allocate_buckets" with nothing to allocate was
|
||||
# used to "ask" a write-able server what it held. Now we count
|
||||
# "actual allocation queries" only, because those are the only
|
||||
# things that actually affect what the server does.
|
||||
|
||||
for tracker in readonly_trackers:
|
||||
assert isinstance(tracker, ServerTracker)
|
||||
d = tracker.ask_about_existing_shares()
|
||||
d = timeout_call(self._reactor, tracker.ask_about_existing_shares(), 15)
|
||||
d.addBoth(self._handle_existing_response, tracker)
|
||||
ds.append(d)
|
||||
self.num_servers_contacted += 1
|
||||
self.query_count += 1
|
||||
self.log("asking server %s for any existing shares" %
|
||||
(tracker.get_name(),), level=log.NOISY)
|
||||
dl = defer.DeferredList(ds)
|
||||
dl.addCallback(lambda ign: self._loop())
|
||||
return dl
|
||||
|
||||
for tracker in write_trackers:
|
||||
assert isinstance(tracker, ServerTracker)
|
||||
d = timeout_call(self._reactor, tracker.ask_about_existing_shares(), 15)
|
||||
|
||||
def timed_out(f, tracker):
|
||||
# print("TIMEOUT {}: {}".format(tracker, f))
|
||||
write_trackers.remove(tracker)
|
||||
readonly_trackers.append(tracker)
|
||||
return f
|
||||
d.addErrback(timed_out, tracker)
|
||||
d.addBoth(self._handle_existing_write_response, tracker, set())
|
||||
ds.append(d)
|
||||
self.log("asking server %s for any existing shares" %
|
||||
(tracker.get_name(),), level=log.NOISY)
|
||||
|
||||
trackers = set(write_trackers) | set(readonly_trackers)
|
||||
|
||||
# these will always be (True, None) because errors are handled
|
||||
# in the _handle_existing_write_response etc callbacks
|
||||
yield defer.DeferredList(ds)
|
||||
|
||||
# okay, we've queried the 2N servers, time to get the share
|
||||
# placements and attempt to actually place the shares (or
|
||||
# renew them on read-only servers). We want to run the loop
|
||||
# below *at least once* because even read-only servers won't
|
||||
# renew their shares until "allocate_buckets" is called (via
|
||||
# tracker.query())
|
||||
|
||||
# https://tahoe-lafs.org/trac/tahoe-lafs/ticket/778#comment:48
|
||||
# min_happiness will be 0 for the repairer, so we set current
|
||||
# effective_happiness to less than zero so this loop runs at
|
||||
# least once for the repairer...
|
||||
|
||||
def _bad_server(fail, tracker):
|
||||
self.last_failure_msg = fail
|
||||
return False # will mark it readonly
|
||||
|
||||
def _make_readonly(tracker):
|
||||
# print("making {} read-only".format(tracker.get_serverid()))
|
||||
try:
|
||||
write_trackers.remove(tracker)
|
||||
except ValueError:
|
||||
pass
|
||||
# XXX can we just use a set() or does order matter?
|
||||
if tracker not in readonly_trackers:
|
||||
readonly_trackers.append(tracker)
|
||||
return None
|
||||
|
||||
# so we *always* want to run this loop at least once, even if
|
||||
# we only have read-only servers -- because asking them to
|
||||
# allocate buckets renews those shares they already have. For
|
||||
# subsequent loops, we give up if we've achieved happiness OR
|
||||
# if we have zero writable servers left
|
||||
|
||||
last_happiness = None
|
||||
effective_happiness = -1
|
||||
while effective_happiness < min_happiness and \
|
||||
(last_happiness is None or len(write_trackers)):
|
||||
errors_before = self._query_stats.bad
|
||||
self._share_placements = self.peer_selector.get_share_placements()
|
||||
|
||||
placements = []
|
||||
for tracker in trackers:
|
||||
shares_to_ask = self._allocation_for(tracker)
|
||||
|
||||
# if we already tried to upload share X to this very
|
||||
# same server in a previous iteration, we should *not*
|
||||
# ask again. If we *do* ask, there's no real harm, but
|
||||
# the server will respond with an empty dict and that
|
||||
# confuses our statistics. However, if the server is a
|
||||
# readonly sever, we *do* want to ask so it refreshes
|
||||
# the share.
|
||||
if shares_to_ask != set(tracker.buckets.keys()) or tracker in readonly_trackers:
|
||||
self._query_stats.total += 1
|
||||
self._query_stats.contacted += 1
|
||||
d = timeout_call(self._reactor, tracker.query(shares_to_ask), 15)
|
||||
d.addBoth(self._buckets_allocated, tracker, shares_to_ask)
|
||||
d.addErrback(lambda f, tr: _bad_server(f, tr), tracker)
|
||||
d.addCallback(lambda x, tr: _make_readonly(tr) if not x else x, tracker)
|
||||
placements.append(d)
|
||||
|
||||
yield defer.DeferredList(placements)
|
||||
merged = merge_servers(self.peer_selector.get_sharemap_of_preexisting_shares(), self.use_trackers)
|
||||
effective_happiness = servers_of_happiness(merged)
|
||||
if effective_happiness == last_happiness:
|
||||
# print("effective happiness still {}".format(last_happiness))
|
||||
# we haven't improved over the last iteration; give up
|
||||
break;
|
||||
if errors_before == self._query_stats.bad:
|
||||
if False: print("no more errors; break")
|
||||
break;
|
||||
last_happiness = effective_happiness
|
||||
# print("write trackers left: {}".format(len(write_trackers)))
|
||||
|
||||
# note: peer_selector.get_allocations() only maps "things we
|
||||
# uploaded in the above loop" and specificaly does *not*
|
||||
# include any pre-existing shares on read-only servers .. but
|
||||
# we *do* want to count those shares towards total happiness.
|
||||
|
||||
# no more servers. If we haven't placed enough shares, we fail.
|
||||
# XXX note sometimes we're not running the loop at least once,
|
||||
# and so 'merged' must be (re-)computed here.
|
||||
merged = merge_servers(self.peer_selector.get_sharemap_of_preexisting_shares(), self.use_trackers)
|
||||
effective_happiness = servers_of_happiness(merged)
|
||||
|
||||
# print("placements completed {} vs {}".format(effective_happiness, min_happiness))
|
||||
# for k, v in merged.items():
|
||||
# print(" {} -> {}".format(k, v))
|
||||
|
||||
if effective_happiness < min_happiness:
|
||||
msg = failure_message(
|
||||
peer_count=len(self.serverids_with_shares),
|
||||
k=self.needed_shares,
|
||||
happy=min_happiness,
|
||||
effective_happy=effective_happiness,
|
||||
)
|
||||
msg = ("server selection failed for %s: %s (%s), merged=%s" %
|
||||
(self, msg, self._get_progress_message(),
|
||||
pretty_print_shnum_to_servers(merged)))
|
||||
if self.last_failure_msg:
|
||||
msg += " (%s)" % (self.last_failure_msg,)
|
||||
self.log(msg, level=log.UNUSUAL)
|
||||
self._failed(msg) # raises UploadUnhappinessError
|
||||
return
|
||||
|
||||
# we placed (or already had) enough to be happy, so we're done
|
||||
if self._status:
|
||||
self._status.set_status("Placed all shares")
|
||||
msg = ("server selection successful for %s: %s: pretty_print_merged: %s, "
|
||||
"self.use_trackers: %s, self.preexisting_shares: %s") \
|
||||
% (self, self._get_progress_message(),
|
||||
pretty_print_shnum_to_servers(merged),
|
||||
[', '.join([str_shareloc(k,v)
|
||||
for k,v in st.buckets.iteritems()])
|
||||
for st in self.use_trackers],
|
||||
pretty_print_shnum_to_servers(self.preexisting_shares))
|
||||
self.log(msg, level=log.OPERATIONAL)
|
||||
defer.returnValue((self.use_trackers, self.peer_selector.get_sharemap_of_preexisting_shares()))
|
||||
|
||||
def _handle_existing_response(self, res, tracker):
|
||||
"""
|
||||
I handle responses to the queries sent by
|
||||
Tahoe2ServerSelector._existing_shares.
|
||||
Tahoe2ServerSelector.get_shareholders.
|
||||
"""
|
||||
serverid = tracker.get_serverid()
|
||||
if isinstance(res, failure.Failure):
|
||||
self.log("%s got error during existing shares check: %s"
|
||||
% (tracker.get_name(), res), level=log.UNUSUAL)
|
||||
self.error_count += 1
|
||||
self.bad_query_count += 1
|
||||
self.peer_selector.mark_bad_peer(serverid)
|
||||
else:
|
||||
buckets = res
|
||||
if buckets:
|
||||
@ -361,11 +601,25 @@ class Tahoe2ServerSelector(log.PrefixingLogMixin):
|
||||
% (tracker.get_name(), tuple(sorted(buckets))),
|
||||
level=log.NOISY)
|
||||
for bucket in buckets:
|
||||
self.peer_selector.add_peer_with_share(serverid, bucket)
|
||||
self.preexisting_shares.setdefault(bucket, set()).add(serverid)
|
||||
self.homeless_shares.discard(bucket)
|
||||
self.full_count += 1
|
||||
self.bad_query_count += 1
|
||||
|
||||
def _handle_existing_write_response(self, res, tracker, shares_to_ask):
|
||||
"""
|
||||
Function handles the response from the write servers
|
||||
when inquiring about what shares each server already has.
|
||||
"""
|
||||
if isinstance(res, failure.Failure):
|
||||
self.peer_selector.mark_bad_peer(tracker.get_serverid())
|
||||
self.log("%s got error during server selection: %s" % (tracker, res),
|
||||
level=log.UNUSUAL)
|
||||
self.homeless_shares |= shares_to_ask
|
||||
msg = ("last failure (from %s) was: %s" % (tracker, res))
|
||||
self.last_failure_msg = msg
|
||||
else:
|
||||
for share in res.keys():
|
||||
self.peer_selector.add_peer_with_share(tracker.get_serverid(), share)
|
||||
|
||||
def _get_progress_message(self):
|
||||
if not self.homeless_shares:
|
||||
@ -375,176 +629,68 @@ class Tahoe2ServerSelector(log.PrefixingLogMixin):
|
||||
(self.total_shares - len(self.homeless_shares),
|
||||
self.total_shares,
|
||||
len(self.homeless_shares)))
|
||||
return (msg + "want to place shares on at least %d servers such that "
|
||||
"any %d of them have enough shares to recover the file, "
|
||||
"sent %d queries to %d servers, "
|
||||
"%d queries placed some shares, %d placed none "
|
||||
"(of which %d placed none due to the server being"
|
||||
" full and %d placed none due to an error)" %
|
||||
(self.servers_of_happiness, self.needed_shares,
|
||||
self.query_count, self.num_servers_contacted,
|
||||
self.good_query_count, self.bad_query_count,
|
||||
self.full_count, self.error_count))
|
||||
assert self._query_stats.bad == (self._query_stats.full + self._query_stats.error)
|
||||
return (
|
||||
msg + "want to place shares on at least {happy} servers such that "
|
||||
"any {needed} of them have enough shares to recover the file, "
|
||||
"sent {queries} queries to {servers} servers, "
|
||||
"{good} queries placed some shares, {bad} placed none "
|
||||
"(of which {full} placed none due to the server being"
|
||||
" full and {error} placed none due to an error)".format(
|
||||
happy=self.min_happiness,
|
||||
needed=self.needed_shares,
|
||||
queries=self._query_stats.total,
|
||||
servers=self._query_stats.contacted,
|
||||
good=self._query_stats.good,
|
||||
bad=self._query_stats.bad,
|
||||
full=self._query_stats.full,
|
||||
error=self._query_stats.error
|
||||
)
|
||||
)
|
||||
|
||||
def _allocation_for(self, tracker):
|
||||
"""
|
||||
Given a ServerTracker, return a list of shares that we should
|
||||
store on that server.
|
||||
"""
|
||||
assert isinstance(tracker, ServerTracker)
|
||||
|
||||
def _loop(self):
|
||||
if not self.homeless_shares:
|
||||
merged = merge_servers(self.preexisting_shares, self.use_trackers)
|
||||
effective_happiness = servers_of_happiness(merged)
|
||||
if self.servers_of_happiness <= effective_happiness:
|
||||
msg = ("server selection successful for %s: %s: pretty_print_merged: %s, "
|
||||
"self.use_trackers: %s, self.preexisting_shares: %s") \
|
||||
% (self, self._get_progress_message(),
|
||||
pretty_print_shnum_to_servers(merged),
|
||||
[', '.join([str_shareloc(k,v)
|
||||
for k,v in st.buckets.iteritems()])
|
||||
for st in self.use_trackers],
|
||||
pretty_print_shnum_to_servers(self.preexisting_shares))
|
||||
self.log(msg, level=log.OPERATIONAL)
|
||||
return (self.use_trackers, self.preexisting_shares)
|
||||
else:
|
||||
# We're not okay right now, but maybe we can fix it by
|
||||
# redistributing some shares. In cases where one or two
|
||||
# servers has, before the upload, all or most of the
|
||||
# shares for a given SI, this can work by allowing _loop
|
||||
# a chance to spread those out over the other servers,
|
||||
delta = self.servers_of_happiness - effective_happiness
|
||||
shares = shares_by_server(self.preexisting_shares)
|
||||
# Each server in shares maps to a set of shares stored on it.
|
||||
# Since we want to keep at least one share on each server
|
||||
# that has one (otherwise we'd only be making
|
||||
# the situation worse by removing distinct servers),
|
||||
# each server has len(its shares) - 1 to spread around.
|
||||
shares_to_spread = sum([len(list(sharelist)) - 1
|
||||
for (server, sharelist)
|
||||
in shares.items()])
|
||||
if delta <= len(self.first_pass_trackers) and \
|
||||
shares_to_spread >= delta:
|
||||
items = shares.items()
|
||||
while len(self.homeless_shares) < delta:
|
||||
# Loop through the allocated shares, removing
|
||||
# one from each server that has more than one
|
||||
# and putting it back into self.homeless_shares
|
||||
# until we've done this delta times.
|
||||
server, sharelist = items.pop()
|
||||
if len(sharelist) > 1:
|
||||
share = sharelist.pop()
|
||||
self.homeless_shares.add(share)
|
||||
self.preexisting_shares[share].remove(server)
|
||||
if not self.preexisting_shares[share]:
|
||||
del self.preexisting_shares[share]
|
||||
items.append((server, sharelist))
|
||||
for writer in self.use_trackers:
|
||||
writer.abort_some_buckets(self.homeless_shares)
|
||||
return self._loop()
|
||||
else:
|
||||
# Redistribution won't help us; fail.
|
||||
server_count = len(self.serverids_with_shares)
|
||||
failmsg = failure_message(server_count,
|
||||
self.needed_shares,
|
||||
self.servers_of_happiness,
|
||||
effective_happiness)
|
||||
servmsgtempl = "server selection unsuccessful for %r: %s (%s), merged=%s"
|
||||
servmsg = servmsgtempl % (
|
||||
self,
|
||||
failmsg,
|
||||
self._get_progress_message(),
|
||||
pretty_print_shnum_to_servers(merged)
|
||||
)
|
||||
self.log(servmsg, level=log.INFREQUENT)
|
||||
return self._failed("%s (%s)" % (failmsg, self._get_progress_message()))
|
||||
shares_to_ask = set()
|
||||
servermap = self._share_placements
|
||||
for shnum, tracker_id in servermap.items():
|
||||
if tracker_id == None:
|
||||
continue
|
||||
if tracker.get_serverid() == tracker_id:
|
||||
shares_to_ask.add(shnum)
|
||||
if shnum in self.homeless_shares:
|
||||
self.homeless_shares.remove(shnum)
|
||||
|
||||
if self.first_pass_trackers:
|
||||
tracker = self.first_pass_trackers.pop(0)
|
||||
# TODO: don't pre-convert all serverids to ServerTrackers
|
||||
assert isinstance(tracker, ServerTracker)
|
||||
if self._status:
|
||||
self._status.set_status("Contacting Servers [%s] (first query),"
|
||||
" %d shares left.."
|
||||
% (tracker.get_name(),
|
||||
len(self.homeless_shares)))
|
||||
return shares_to_ask
|
||||
|
||||
shares_to_ask = set(sorted(self.homeless_shares)[:1])
|
||||
self.homeless_shares -= shares_to_ask
|
||||
self.query_count += 1
|
||||
self.num_servers_contacted += 1
|
||||
if self._status:
|
||||
self._status.set_status("Contacting Servers [%s] (first query),"
|
||||
" %d shares left.."
|
||||
% (tracker.get_name(),
|
||||
len(self.homeless_shares)))
|
||||
d = tracker.query(shares_to_ask)
|
||||
d.addBoth(self._got_response, tracker, shares_to_ask,
|
||||
self.second_pass_trackers)
|
||||
return d
|
||||
elif self.second_pass_trackers:
|
||||
# ask a server that we've already asked.
|
||||
if not self._started_second_pass:
|
||||
self.log("starting second pass",
|
||||
level=log.NOISY)
|
||||
self._started_second_pass = True
|
||||
num_shares = mathutil.div_ceil(len(self.homeless_shares),
|
||||
len(self.second_pass_trackers))
|
||||
tracker = self.second_pass_trackers.pop(0)
|
||||
shares_to_ask = set(sorted(self.homeless_shares)[:num_shares])
|
||||
self.homeless_shares -= shares_to_ask
|
||||
self.query_count += 1
|
||||
if self._status:
|
||||
self._status.set_status("Contacting Servers [%s] (second query),"
|
||||
" %d shares left.."
|
||||
% (tracker.get_name(),
|
||||
len(self.homeless_shares)))
|
||||
d = tracker.query(shares_to_ask)
|
||||
d.addBoth(self._got_response, tracker, shares_to_ask,
|
||||
self.next_pass_trackers)
|
||||
return d
|
||||
elif self.next_pass_trackers:
|
||||
# we've finished the second-or-later pass. Move all the remaining
|
||||
# servers back into self.second_pass_trackers for the next pass.
|
||||
self.second_pass_trackers.extend(self.next_pass_trackers)
|
||||
self.next_pass_trackers[:] = []
|
||||
return self._loop()
|
||||
else:
|
||||
# no more servers. If we haven't placed enough shares, we fail.
|
||||
merged = merge_servers(self.preexisting_shares, self.use_trackers)
|
||||
effective_happiness = servers_of_happiness(merged)
|
||||
if effective_happiness < self.servers_of_happiness:
|
||||
msg = failure_message(len(self.serverids_with_shares),
|
||||
self.needed_shares,
|
||||
self.servers_of_happiness,
|
||||
effective_happiness)
|
||||
msg = ("server selection failed for %s: %s (%s)" %
|
||||
(self, msg, self._get_progress_message()))
|
||||
if self.last_failure_msg:
|
||||
msg += " (%s)" % (self.last_failure_msg,)
|
||||
self.log(msg, level=log.UNUSUAL)
|
||||
return self._failed(msg)
|
||||
else:
|
||||
# we placed enough to be happy, so we're done
|
||||
if self._status:
|
||||
self._status.set_status("Placed all shares")
|
||||
msg = ("server selection successful (no more servers) for %s: %s: %s" % (self,
|
||||
self._get_progress_message(), pretty_print_shnum_to_servers(merged)))
|
||||
self.log(msg, level=log.OPERATIONAL)
|
||||
return (self.use_trackers, self.preexisting_shares)
|
||||
|
||||
def _got_response(self, res, tracker, shares_to_ask, put_tracker_here):
|
||||
def _buckets_allocated(self, res, tracker, shares_to_ask):
|
||||
"""
|
||||
Internal helper. If this returns an error or False, the server
|
||||
will be considered read-only for any future iterations.
|
||||
"""
|
||||
if isinstance(res, failure.Failure):
|
||||
# This is unusual, and probably indicates a bug or a network
|
||||
# problem.
|
||||
self.log("%s got error during server selection: %s" % (tracker, res),
|
||||
level=log.UNUSUAL)
|
||||
self.error_count += 1
|
||||
self.bad_query_count += 1
|
||||
self._query_stats.error += 1
|
||||
self._query_stats.bad += 1
|
||||
self.homeless_shares |= shares_to_ask
|
||||
if (self.first_pass_trackers
|
||||
or self.second_pass_trackers
|
||||
or self.next_pass_trackers):
|
||||
# there is still hope, so just loop
|
||||
try:
|
||||
self.peer_selector.mark_readonly_peer(tracker.get_serverid())
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
# No more servers, so this upload might fail (it depends upon
|
||||
# whether we've hit servers_of_happiness or not). Log the last
|
||||
# failure we got: if a coding error causes all servers to fail
|
||||
# in the same way, this allows the common failure to be seen
|
||||
# by the uploader and should help with debugging
|
||||
msg = ("last failure (from %s) was: %s" % (tracker, res))
|
||||
self.last_failure_msg = msg
|
||||
return res
|
||||
|
||||
else:
|
||||
(alreadygot, allocated) = res
|
||||
self.log("response to allocate_buckets() from server %s: alreadygot=%s, allocated=%s"
|
||||
@ -572,16 +718,6 @@ class Tahoe2ServerSelector(log.PrefixingLogMixin):
|
||||
not_yet_present = set(shares_to_ask) - set(alreadygot)
|
||||
still_homeless = not_yet_present - set(allocated)
|
||||
|
||||
if progress:
|
||||
# They accepted at least one of the shares that we asked
|
||||
# them to accept, or they had a share that we didn't ask
|
||||
# them to accept but that we hadn't placed yet, so this
|
||||
# was a productive query
|
||||
self.good_query_count += 1
|
||||
else:
|
||||
self.bad_query_count += 1
|
||||
self.full_count += 1
|
||||
|
||||
if still_homeless:
|
||||
# In networks with lots of space, this is very unusual and
|
||||
# probably indicates an error. In networks with servers that
|
||||
@ -595,14 +731,20 @@ class Tahoe2ServerSelector(log.PrefixingLogMixin):
|
||||
self.homeless_shares |= still_homeless
|
||||
# Since they were unable to accept all of our requests, so it
|
||||
# is safe to assume that asking them again won't help.
|
||||
|
||||
if progress:
|
||||
# They accepted at least one of the shares that we asked
|
||||
# them to accept, or they had a share that we didn't ask
|
||||
# them to accept but that we hadn't placed yet, so this
|
||||
# was a productive query
|
||||
self._query_stats.good += 1
|
||||
else:
|
||||
# if they *were* able to accept everything, they might be
|
||||
# willing to accept even more.
|
||||
put_tracker_here.append(tracker)
|
||||
|
||||
# now loop
|
||||
return self._loop()
|
||||
|
||||
# if we asked for some allocations, but the server
|
||||
# didn't return any at all (i.e. empty dict) it must
|
||||
# be full
|
||||
self._query_stats.full += 1
|
||||
self._query_stats.bad += 1
|
||||
return progress
|
||||
|
||||
def _failed(self, msg):
|
||||
"""
|
||||
@ -894,10 +1036,9 @@ class UploadStatus(object):
|
||||
def set_results(self, value):
|
||||
self.results = value
|
||||
|
||||
class CHKUploader:
|
||||
server_selector_class = Tahoe2ServerSelector
|
||||
class CHKUploader(object):
|
||||
|
||||
def __init__(self, storage_broker, secret_holder, progress=None):
|
||||
def __init__(self, storage_broker, secret_holder, progress=None, reactor=None):
|
||||
# server_selector needs storage_broker and secret_holder
|
||||
self._storage_broker = storage_broker
|
||||
self._secret_holder = secret_holder
|
||||
@ -908,6 +1049,7 @@ class CHKUploader:
|
||||
self._upload_status.set_helper(False)
|
||||
self._upload_status.set_active(True)
|
||||
self._progress = progress
|
||||
self._reactor = reactor
|
||||
|
||||
# locate_all_shareholders() will create the following attribute:
|
||||
# self._server_trackers = {} # k: shnum, v: instance of ServerTracker
|
||||
@ -946,22 +1088,28 @@ class CHKUploader:
|
||||
return defer.succeed(None)
|
||||
return self._encoder.abort()
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def start_encrypted(self, encrypted):
|
||||
""" Returns a Deferred that will fire with the UploadResults instance. """
|
||||
"""
|
||||
Returns a Deferred that will fire with the UploadResults instance.
|
||||
"""
|
||||
eu = IEncryptedUploadable(encrypted)
|
||||
|
||||
started = time.time()
|
||||
self._encoder = e = encode.Encoder(
|
||||
# would be Really Nice to make Encoder just a local; only
|
||||
# abort() really needs self._encoder ...
|
||||
self._encoder = encode.Encoder(
|
||||
self._log_number,
|
||||
self._upload_status,
|
||||
progress=self._progress,
|
||||
)
|
||||
d = e.set_encrypted_uploadable(eu)
|
||||
d.addCallback(self.locate_all_shareholders, started)
|
||||
d.addCallback(self.set_shareholders, e)
|
||||
d.addCallback(lambda res: e.start())
|
||||
d.addCallback(self._encrypted_done)
|
||||
return d
|
||||
# this just returns itself
|
||||
yield self._encoder.set_encrypted_uploadable(eu)
|
||||
(upload_trackers, already_serverids) = yield self.locate_all_shareholders(self._encoder, started)
|
||||
yield self.set_shareholders(upload_trackers, already_serverids, self._encoder)
|
||||
verifycap = yield self._encoder.start()
|
||||
results = yield self._encrypted_done(verifycap)
|
||||
defer.returnValue(results)
|
||||
|
||||
def locate_all_shareholders(self, encoder, started):
|
||||
server_selection_started = now = time.time()
|
||||
@ -972,14 +1120,17 @@ class CHKUploader:
|
||||
self._storage_index = storage_index
|
||||
upload_id = si_b2a(storage_index)[:5]
|
||||
self.log("using storage index %s" % upload_id)
|
||||
server_selector = self.server_selector_class(upload_id,
|
||||
self._log_number,
|
||||
self._upload_status)
|
||||
server_selector = Tahoe2ServerSelector(
|
||||
upload_id,
|
||||
self._log_number,
|
||||
self._upload_status,
|
||||
reactor=self._reactor,
|
||||
)
|
||||
|
||||
share_size = encoder.get_param("share_size")
|
||||
block_size = encoder.get_param("block_size")
|
||||
num_segments = encoder.get_param("num_segments")
|
||||
k,desired,n = encoder.get_param("share_counts")
|
||||
k, desired, n = encoder.get_param("share_counts")
|
||||
|
||||
self._server_selection_started = time.time()
|
||||
d = server_selector.get_shareholders(storage_broker, secret_holder,
|
||||
@ -992,13 +1143,13 @@ class CHKUploader:
|
||||
d.addCallback(_done)
|
||||
return d
|
||||
|
||||
def set_shareholders(self, (upload_trackers, already_serverids), encoder):
|
||||
def set_shareholders(self, upload_trackers, already_serverids, encoder):
|
||||
"""
|
||||
@param upload_trackers: a sequence of ServerTracker objects that
|
||||
:param upload_trackers: a sequence of ServerTracker objects that
|
||||
have agreed to hold some shares for us (the
|
||||
shareids are stashed inside the ServerTracker)
|
||||
|
||||
@paran already_serverids: a dict mapping sharenum to a set of
|
||||
:param already_serverids: a dict mapping sharenum to a set of
|
||||
serverids for servers that claim to already
|
||||
have this share
|
||||
"""
|
||||
@ -1558,7 +1709,7 @@ class Uploader(service.MultiService, log.PrefixingLogMixin):
|
||||
return (self._helper_furl, bool(self._helper))
|
||||
|
||||
|
||||
def upload(self, uploadable, progress=None):
|
||||
def upload(self, uploadable, progress=None, reactor=None):
|
||||
"""
|
||||
Returns a Deferred that will fire with the UploadResults instance.
|
||||
"""
|
||||
@ -1594,7 +1745,7 @@ class Uploader(service.MultiService, log.PrefixingLogMixin):
|
||||
else:
|
||||
storage_broker = self.parent.get_storage_broker()
|
||||
secret_holder = self.parent._secret_holder
|
||||
uploader = CHKUploader(storage_broker, secret_holder, progress=progress)
|
||||
uploader = CHKUploader(storage_broker, secret_holder, progress=progress, reactor=reactor)
|
||||
d2.addCallback(lambda x: uploader.start(eu))
|
||||
|
||||
self._all_uploads[uploader] = None
|
||||
|
@ -730,6 +730,59 @@ class IReadable(Interface):
|
||||
download-to-memory consumer.
|
||||
"""
|
||||
|
||||
class IPeerSelector(Interface):
|
||||
"""
|
||||
I select peers for an upload, maximizing some measure of health.
|
||||
|
||||
I keep track of the state of a grid relative to a file. This means
|
||||
that I know about all of the peers that parts of that file could be
|
||||
placed on, and about shares that have been placed on those peers.
|
||||
Given this, I assign shares to peers in a way that maximizes the
|
||||
file's health according to whichever definition of health I am
|
||||
programmed with. I tell the uploader whether or not my assignment is
|
||||
healthy. I keep track of failures during the process and update my
|
||||
conclusions appropriately.
|
||||
"""
|
||||
def add_peer_with_share(peerid, shnum):
|
||||
"""
|
||||
Update my internal state to reflect the fact that peer peerid
|
||||
holds share shnum. Called for shares that are detected before
|
||||
peer selection begins.
|
||||
"""
|
||||
|
||||
def confirm_share_allocation(peerid, shnum):
|
||||
"""
|
||||
Confirm that an allocated peer=>share pairing has been
|
||||
successfully established.
|
||||
"""
|
||||
|
||||
def add_peers(peerids=set):
|
||||
"""
|
||||
Update my internal state to include the peers in peerids as
|
||||
potential candidates for storing a file.
|
||||
"""
|
||||
|
||||
def mark_readonly_peer(peerid):
|
||||
"""
|
||||
Mark the peer peerid as full. This means that any
|
||||
peer-with-share relationships I know about for peerid remain
|
||||
valid, but that peerid will not be assigned any new shares.
|
||||
"""
|
||||
|
||||
def mark_bad_peer(peerid):
|
||||
"""
|
||||
Mark the peer peerid as bad. This is typically called when an
|
||||
error is encountered when communicating with a peer. I will
|
||||
disregard any existing peer => share relationships associated
|
||||
with peerid, and will not attempt to assign it any more shares.
|
||||
"""
|
||||
|
||||
def get_share_placements():
|
||||
"""
|
||||
Return the share-placement map (a dict) which maps shares to
|
||||
server-ids
|
||||
"""
|
||||
|
||||
|
||||
class IWriteable(Interface):
|
||||
"""
|
||||
|
@ -142,6 +142,7 @@ class NodeMaker(object):
|
||||
convergence = self.secret_holder.get_convergence_secret()
|
||||
packed = pack_children(children, None, deep_immutable=True)
|
||||
uploadable = Data(packed, convergence)
|
||||
# XXX should pass reactor arg
|
||||
d = self.uploader.upload(uploadable)
|
||||
d.addCallback(lambda results:
|
||||
self.create_from_cap(None, results.get_uri()))
|
||||
|
@ -2,6 +2,7 @@
|
||||
import os.path
|
||||
from cStringIO import StringIO
|
||||
import urllib, sys
|
||||
import re
|
||||
|
||||
from twisted.trial import unittest
|
||||
from twisted.python.monkey import MonkeyPatcher
|
||||
@ -769,15 +770,14 @@ class Errors(GridTestMixin, CLITestMixin, unittest.TestCase):
|
||||
# enough shares. The one remaining share might be in either the
|
||||
# COMPLETE or the PENDING state.
|
||||
in_complete_msg = "ran out of shares: complete=sh0 pending= overdue= unused= need 3"
|
||||
in_pending_msg = "ran out of shares: complete= pending=Share(sh0-on-fob7vqgd) overdue= unused= need 3"
|
||||
in_pending_msg_regex = "ran out of shares: complete= pending=Share\(.+\) overdue= unused= need 3"
|
||||
|
||||
d.addCallback(lambda ign: self.do_cli("get", self.uri_1share))
|
||||
def _check1((rc, out, err)):
|
||||
self.failIfEqual(rc, 0)
|
||||
self.failUnless("410 Gone" in err, err)
|
||||
self.failUnlessIn("NotEnoughSharesError: ", err)
|
||||
self.failUnless(in_complete_msg in err or in_pending_msg in err,
|
||||
err)
|
||||
self.failUnless(in_complete_msg in err or re.search(in_pending_msg_regex, err))
|
||||
d.addCallback(_check1)
|
||||
|
||||
targetf = os.path.join(self.basedir, "output")
|
||||
@ -786,8 +786,7 @@ class Errors(GridTestMixin, CLITestMixin, unittest.TestCase):
|
||||
self.failIfEqual(rc, 0)
|
||||
self.failUnless("410 Gone" in err, err)
|
||||
self.failUnlessIn("NotEnoughSharesError: ", err)
|
||||
self.failUnless(in_complete_msg in err or in_pending_msg in err,
|
||||
err)
|
||||
self.failUnless(in_complete_msg in err or re.search(in_pending_msg_regex, err))
|
||||
self.failIf(os.path.exists(targetf))
|
||||
d.addCallback(_check2)
|
||||
|
||||
|
@ -473,9 +473,11 @@ class GridTestMixin:
|
||||
|
||||
def corrupt_all_shares(self, uri, corruptor, debug=False):
|
||||
for (i_shnum, i_serverid, i_sharefile) in self.find_uri_shares(uri):
|
||||
sharedata = open(i_sharefile, "rb").read()
|
||||
with open(i_sharefile, "rb") as f:
|
||||
sharedata = f.read()
|
||||
corruptdata = corruptor(sharedata, debug=debug)
|
||||
open(i_sharefile, "wb").write(corruptdata)
|
||||
with open(i_sharefile, "wb") as f:
|
||||
f.write(corruptdata)
|
||||
|
||||
def GET(self, urlpath, followRedirect=False, return_response=False,
|
||||
method="GET", clientnum=0, **kwargs):
|
||||
|
@ -391,7 +391,6 @@ class BalancingAct(GridTestMixin, unittest.TestCase):
|
||||
return self.imm.check_and_repair(Monitor())
|
||||
def _check_counts(crr, shares_good, good_share_hosts):
|
||||
prr = crr.get_post_repair_results()
|
||||
#print self._pretty_shares_chart(self.uri)
|
||||
self.failUnlessEqual(prr.get_share_counter_good(), shares_good)
|
||||
self.failUnlessEqual(prr.get_host_counter_good_shares(),
|
||||
good_share_hosts)
|
||||
@ -401,16 +400,26 @@ class BalancingAct(GridTestMixin, unittest.TestCase):
|
||||
0:[A] 1:[A] 2:[A] 3:[A,B,C,D,E]
|
||||
4 good shares, but 5 good hosts
|
||||
After deleting all instances of share #3 and repairing:
|
||||
0:[A,B], 1:[A,C], 2:[A,D], 3:[E]
|
||||
Still 4 good shares and 5 good hosts
|
||||
0:[A], 1:[A,B], 2:[C,A], 3:[E]
|
||||
# actually: {0: ['E', 'A'], 1: ['C', 'A'], 2: ['A', 'B'], 3: ['D']}
|
||||
Still 4 good shares but now 4 good hosts
|
||||
"""
|
||||
d.addCallback(_check_and_repair)
|
||||
d.addCallback(_check_counts, 4, 5)
|
||||
d.addCallback(lambda _: self.delete_shares_numbered(self.uri, [3]))
|
||||
d.addCallback(_check_and_repair)
|
||||
d.addCallback(_check_counts, 4, 5)
|
||||
d.addCallback(lambda _: [self.g.break_server(sid)
|
||||
for sid in self.g.get_all_serverids()])
|
||||
|
||||
# it can happen that our uploader will choose, e.g., to upload
|
||||
# to servers B, C, D, E .. which will mean that all 5 serves
|
||||
# now contain our shares (and thus "respond").
|
||||
|
||||
def _check_happy(crr):
|
||||
prr = crr.get_post_repair_results()
|
||||
self.assertTrue(prr.get_host_counter_good_shares() >= 4)
|
||||
return crr
|
||||
d.addCallback(_check_happy)
|
||||
d.addCallback(lambda _: all([self.g.break_server(sid)
|
||||
for sid in self.g.get_all_serverids()]))
|
||||
d.addCallback(_check_and_repair)
|
||||
d.addCallback(_check_counts, 0, 0)
|
||||
return d
|
||||
|
@ -294,8 +294,7 @@ class DownloadTest(_Base, unittest.TestCase):
|
||||
def _kill_some_shares():
|
||||
# find the shares that were used and delete them
|
||||
shares = self.n._cnode._node._shares
|
||||
shnums = sorted([s._shnum for s in shares])
|
||||
self.failUnlessEqual(shnums, [0,1,2,3])
|
||||
self.killed_share_nums = sorted([s._shnum for s in shares])
|
||||
|
||||
# break the RIBucketReader references
|
||||
# (we don't break the RIStorageServer references, because that
|
||||
@ -312,7 +311,7 @@ class DownloadTest(_Base, unittest.TestCase):
|
||||
self.failUnlessEqual("".join(c.chunks), plaintext)
|
||||
shares = self.n._cnode._node._shares
|
||||
shnums = sorted([s._shnum for s in shares])
|
||||
self.failIfEqual(shnums, [0,1,2,3])
|
||||
self.failIfEqual(shnums, self.killed_share_nums)
|
||||
d.addCallback(_check_failover)
|
||||
return d
|
||||
|
||||
@ -934,13 +933,13 @@ class Corruption(_Base, unittest.TestCase):
|
||||
log.msg("corrupt %d" % which)
|
||||
def _corruptor(s, debug=False):
|
||||
return s[:which] + chr(ord(s[which])^0x01) + s[which+1:]
|
||||
self.corrupt_shares_numbered(imm_uri, [0], _corruptor)
|
||||
self.corrupt_shares_numbered(imm_uri, [2], _corruptor)
|
||||
|
||||
def _corrupt_set(self, ign, imm_uri, which, newvalue):
|
||||
log.msg("corrupt %d" % which)
|
||||
def _corruptor(s, debug=False):
|
||||
return s[:which] + chr(newvalue) + s[which+1:]
|
||||
self.corrupt_shares_numbered(imm_uri, [0], _corruptor)
|
||||
self.corrupt_shares_numbered(imm_uri, [2], _corruptor)
|
||||
|
||||
def test_each_byte(self):
|
||||
# Setting catalog_detection=True performs an exhaustive test of the
|
||||
@ -976,25 +975,27 @@ class Corruption(_Base, unittest.TestCase):
|
||||
def _got_data(data):
|
||||
self.failUnlessEqual(data, plaintext)
|
||||
shnums = sorted([s._shnum for s in n._cnode._node._shares])
|
||||
no_sh0 = bool(0 not in shnums)
|
||||
sh0 = [s for s in n._cnode._node._shares if s._shnum == 0]
|
||||
sh0_had_corruption = False
|
||||
if sh0 and sh0[0].had_corruption:
|
||||
sh0_had_corruption = True
|
||||
no_sh2 = bool(2 not in shnums)
|
||||
sh2 = [s for s in n._cnode._node._shares if s._shnum == 2]
|
||||
sh2_had_corruption = False
|
||||
if sh2 and sh2[0].had_corruption:
|
||||
sh2_had_corruption = True
|
||||
num_needed = len(n._cnode._node._shares)
|
||||
if self.catalog_detection:
|
||||
detected = no_sh0 or sh0_had_corruption or (num_needed!=3)
|
||||
detected = no_sh2 or sh2_had_corruption or (num_needed!=3)
|
||||
if not detected:
|
||||
undetected.add(which, 1)
|
||||
if expected == "no-sh0":
|
||||
self.failIfIn(0, shnums)
|
||||
elif expected == "0bad-need-3":
|
||||
self.failIf(no_sh0)
|
||||
self.failUnless(sh0[0].had_corruption)
|
||||
if expected == "no-sh2":
|
||||
self.failIfIn(2, shnums)
|
||||
elif expected == "2bad-need-3":
|
||||
self.failIf(no_sh2)
|
||||
self.failUnless(sh2[0].had_corruption)
|
||||
self.failUnlessEqual(num_needed, 3)
|
||||
elif expected == "need-4th":
|
||||
self.failIf(no_sh0)
|
||||
self.failUnless(sh0[0].had_corruption)
|
||||
# XXX check with warner; what relevance does this
|
||||
# have for the "need-4th" stuff?
|
||||
#self.failIf(no_sh2)
|
||||
#self.failUnless(sh2[0].had_corruption)
|
||||
self.failIfEqual(num_needed, 3)
|
||||
d.addCallback(_got_data)
|
||||
return d
|
||||
@ -1012,23 +1013,20 @@ class Corruption(_Base, unittest.TestCase):
|
||||
# data-block-offset, and offset=48 is the first byte of the first
|
||||
# data-block). Each one also specifies what sort of corruption
|
||||
# we're expecting to see.
|
||||
no_sh0_victims = [0,1,2,3] # container version
|
||||
no_sh2_victims = [0,1,2,3] # container version
|
||||
need3_victims = [ ] # none currently in this category
|
||||
# when the offsets are corrupted, the Share will be unable to
|
||||
# retrieve the data it wants (because it thinks that data lives
|
||||
# off in the weeds somewhere), and Share treats DataUnavailable
|
||||
# as abandon-this-share, so in general we'll be forced to look
|
||||
# for a 4th share.
|
||||
need_4th_victims = [12,13,14,15, # share version
|
||||
24,25,26,27, # offset[data]
|
||||
32,33,34,35, # offset[crypttext_hash_tree]
|
||||
36,37,38,39, # offset[block_hashes]
|
||||
44,45,46,47, # offset[UEB]
|
||||
need_4th_victims = [12,13,14,15, # offset[data]
|
||||
24,25,26,27, # offset[block_hashes]
|
||||
]
|
||||
need_4th_victims.append(48) # block data
|
||||
need_4th_victims.append(36) # block data
|
||||
# when corrupting hash trees, we must corrupt a value that isn't
|
||||
# directly set from somewhere else. Since we download data from
|
||||
# seg0, corrupt something on its hash chain, like [2] (the
|
||||
# seg2, corrupt something on its hash chain, like [2] (the
|
||||
# right-hand child of the root)
|
||||
need_4th_victims.append(600+2*32) # block_hashes[2]
|
||||
# Share.loop is pretty conservative: it abandons the share at the
|
||||
@ -1039,15 +1037,15 @@ class Corruption(_Base, unittest.TestCase):
|
||||
# the following fields (which are present in multiple shares)
|
||||
# should fall into the "need3_victims" case instead of the
|
||||
# "need_4th_victims" case.
|
||||
need_4th_victims.append(376+2*32) # crypttext_hash_tree[2]
|
||||
need_4th_victims.append(824) # share_hashes
|
||||
need_4th_victims.append(994) # UEB length
|
||||
need_4th_victims.append(998) # UEB
|
||||
corrupt_me = ([(i,"no-sh0") for i in no_sh0_victims] +
|
||||
[(i, "0bad-need-3") for i in need3_victims] +
|
||||
corrupt_me = ([(i,"no-sh2") for i in no_sh2_victims] +
|
||||
[(i, "2bad-need-3") for i in need3_victims] +
|
||||
[(i, "need-4th") for i in need_4th_victims])
|
||||
if self.catalog_detection:
|
||||
corrupt_me = [(i, "") for i in range(len(self.sh0_orig))]
|
||||
share_len = len(self.shares.values()[0])
|
||||
corrupt_me = [(i, "") for i in range(share_len)]
|
||||
# This is a work around for ticket #2024.
|
||||
corrupt_me = corrupt_me[0:8]+corrupt_me[12:]
|
||||
for i,expected in corrupt_me:
|
||||
# All these tests result in a successful download. What we're
|
||||
# measuring is how many shares the downloader had to use.
|
||||
@ -1055,7 +1053,7 @@ class Corruption(_Base, unittest.TestCase):
|
||||
d.addCallback(_download, imm_uri, i, expected)
|
||||
d.addCallback(lambda ign: self.restore_all_shares(self.shares))
|
||||
d.addCallback(fireEventually)
|
||||
corrupt_values = [(3, 2, "no-sh0"),
|
||||
corrupt_values = [(3, 2, "no-sh2"),
|
||||
(15, 2, "need-4th"), # share looks v2
|
||||
]
|
||||
for i,newvalue,expected in corrupt_values:
|
||||
@ -1066,9 +1064,10 @@ class Corruption(_Base, unittest.TestCase):
|
||||
return d
|
||||
d.addCallback(_uploaded)
|
||||
def _show_results(ign):
|
||||
share_len = len(self.shares.values()[0])
|
||||
print
|
||||
print ("of [0:%d], corruption ignored in %s" %
|
||||
(len(self.sh0_orig), undetected.dump()))
|
||||
(share_len, undetected.dump()))
|
||||
if self.catalog_detection:
|
||||
d.addCallback(_show_results)
|
||||
# of [0:2070], corruption ignored in len=1133:
|
||||
|
271
src/allmydata/test/test_happiness.py
Normal file
271
src/allmydata/test/test_happiness.py
Normal file
@ -0,0 +1,271 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from twisted.trial import unittest
|
||||
from hypothesis import given
|
||||
from hypothesis.strategies import text, sets
|
||||
from allmydata.immutable import happiness_upload
|
||||
|
||||
|
||||
class HappinessUtils(unittest.TestCase):
|
||||
"""
|
||||
test-cases for utility functions augmenting_path_for and residual_network
|
||||
"""
|
||||
|
||||
def test_residual_0(self):
|
||||
graph = happiness_upload._servermap_flow_graph(
|
||||
['peer0'],
|
||||
['share0'],
|
||||
servermap={
|
||||
'peer0': ['share0'],
|
||||
}
|
||||
)
|
||||
flow = [[0 for _ in graph] for _ in graph]
|
||||
|
||||
residual, capacity = happiness_upload.residual_network(graph, flow)
|
||||
|
||||
# XXX no idea if these are right; hand-verify
|
||||
self.assertEqual(residual, [[1], [2], [3], []])
|
||||
self.assertEqual(capacity, [[0, 1, 0, 0], [-1, 0, 1, 0], [0, -1, 0, 1], [0, 0, -1, 0]])
|
||||
|
||||
def test_trivial_maximum_graph(self):
|
||||
self.assertEqual(
|
||||
{},
|
||||
happiness_upload._compute_maximum_graph([], {})
|
||||
)
|
||||
|
||||
def test_trivial_flow_graph(self):
|
||||
self.assertEqual(
|
||||
[],
|
||||
happiness_upload._servermap_flow_graph(set(), set(), {})
|
||||
)
|
||||
|
||||
|
||||
class Happiness(unittest.TestCase):
|
||||
|
||||
def test_placement_simple(self):
|
||||
|
||||
shares = {'share0', 'share1', 'share2'}
|
||||
peers = {'peer0', 'peer1'}
|
||||
readonly_peers = {'peer0'}
|
||||
peers_to_shares = {
|
||||
'peer0': {'share2'},
|
||||
'peer1': [],
|
||||
}
|
||||
|
||||
places = happiness_upload.share_placement(peers, readonly_peers, shares, peers_to_shares)
|
||||
|
||||
self.assertEqual(
|
||||
places,
|
||||
{
|
||||
'share0': 'peer1',
|
||||
'share1': 'peer1',
|
||||
'share2': 'peer0',
|
||||
}
|
||||
)
|
||||
|
||||
def test_placement_1(self):
|
||||
|
||||
shares = {
|
||||
'share0', 'share1', 'share2',
|
||||
'share3', 'share4', 'share5',
|
||||
'share6', 'share7', 'share8',
|
||||
'share9',
|
||||
}
|
||||
peers = {
|
||||
'peer0', 'peer1', 'peer2', 'peer3',
|
||||
'peer4', 'peer5', 'peer6', 'peer7',
|
||||
'peer8', 'peer9', 'peerA', 'peerB',
|
||||
}
|
||||
readonly_peers = {'peer0', 'peer1', 'peer2', 'peer3'}
|
||||
peers_to_shares = {
|
||||
'peer0': {'share0'},
|
||||
'peer1': {'share1'},
|
||||
'peer2': {'share2'},
|
||||
'peer3': {'share3'},
|
||||
'peer4': {'share4'},
|
||||
'peer5': {'share5'},
|
||||
'peer6': {'share6'},
|
||||
'peer7': {'share7'},
|
||||
'peer8': {'share8'},
|
||||
'peer9': {'share9'},
|
||||
'peerA': set(),
|
||||
'peerB': set(),
|
||||
}
|
||||
|
||||
places = happiness_upload.share_placement(peers, readonly_peers, shares, peers_to_shares)
|
||||
|
||||
# actually many valid answers for this, so long as peer's 0,
|
||||
# 1, 2, 3 all have share 0, 1, 2 3.
|
||||
|
||||
# share N maps to peer N
|
||||
# i.e. this says that share0 should be on peer0, share1 should
|
||||
# be on peer1, etc.
|
||||
expected = {
|
||||
'share{}'.format(i): 'peer{}'.format(i)
|
||||
for i in range(10)
|
||||
}
|
||||
self.assertEqual(expected, places)
|
||||
|
||||
def test_unhappy(self):
|
||||
shares = {
|
||||
'share1', 'share2', 'share3', 'share4', 'share5',
|
||||
}
|
||||
peers = {
|
||||
'peer1', 'peer2', 'peer3', 'peer4',
|
||||
}
|
||||
readonly_peers = set()
|
||||
peers_to_shares = {}
|
||||
places = happiness_upload.share_placement(peers, readonly_peers, shares, peers_to_shares)
|
||||
happiness = happiness_upload.calculate_happiness(places)
|
||||
self.assertEqual(4, happiness)
|
||||
|
||||
def test_hypothesis0(self):
|
||||
peers={u'0', u'00'}
|
||||
shares={u'0', u'1'}
|
||||
readonly_peers = set()
|
||||
peers_to_shares = dict()
|
||||
|
||||
#h = happiness_upload.HappinessUpload(peers, readonly_peers, shares, peers_to_shares)
|
||||
#places = h.generate_mappings()
|
||||
#happiness = h.happiness()
|
||||
|
||||
places = happiness_upload.share_placement(peers, readonly_peers, shares, peers_to_shares)
|
||||
happiness = happiness_upload.calculate_happiness(places)
|
||||
|
||||
self.assertEqual(2, happiness)
|
||||
|
||||
def test_100(self):
|
||||
peers = set(['peer{}'.format(x) for x in range(100)])
|
||||
shares = set(['share{}'.format(x) for x in range(100)])
|
||||
readonly_peers = set()
|
||||
peers_to_shares = dict()
|
||||
|
||||
places = happiness_upload.share_placement(peers, readonly_peers, shares, peers_to_shares)
|
||||
happiness = happiness_upload.calculate_happiness(places)
|
||||
|
||||
self.assertEqual(100, happiness)
|
||||
|
||||
def test_redistribute(self):
|
||||
"""
|
||||
with existing shares 0, 3 on a single servers we can achieve
|
||||
higher happiness by moving one of those shares to a new server
|
||||
"""
|
||||
peers = {'a', 'b', 'c', 'd'}
|
||||
shares = {'0', '1', '2', '3'}
|
||||
readonly_peers = set()
|
||||
peers_to_shares = {
|
||||
'a': set(['0']),
|
||||
'b': set(['1']),
|
||||
'c': set(['2', '3']),
|
||||
}
|
||||
# we can achieve more happiness by moving "2" or "3" to server "d"
|
||||
|
||||
places = happiness_upload.share_placement(peers, readonly_peers, shares, peers_to_shares)
|
||||
#print "places %s" % places
|
||||
#places = happiness_upload.slow_share_placement(peers, readonly_peers, shares, peers_to_shares)
|
||||
#print "places %s" % places
|
||||
|
||||
happiness = happiness_upload.calculate_happiness(places)
|
||||
self.assertEqual(4, happiness)
|
||||
|
||||
def test_calc_happy(self):
|
||||
# share -> server
|
||||
share_placements = {
|
||||
0: "\x0e\xd6\xb3>\xd6\x85\x9d\x94')'\xf03:R\x88\xf1\x04\x1b\xa4",
|
||||
1: '\xb9\xa3N\x80u\x9c_\xf7\x97FSS\xa7\xbd\x02\xf9f$:\t',
|
||||
2: '\xb9\xa3N\x80u\x9c_\xf7\x97FSS\xa7\xbd\x02\xf9f$:\t',
|
||||
3: '\xb9\xa3N\x80u\x9c_\xf7\x97FSS\xa7\xbd\x02\xf9f$:\t',
|
||||
4: '\xb9\xa3N\x80u\x9c_\xf7\x97FSS\xa7\xbd\x02\xf9f$:\t',
|
||||
5: '\xb9\xa3N\x80u\x9c_\xf7\x97FSS\xa7\xbd\x02\xf9f$:\t',
|
||||
6: '\xb9\xa3N\x80u\x9c_\xf7\x97FSS\xa7\xbd\x02\xf9f$:\t',
|
||||
7: '\xb9\xa3N\x80u\x9c_\xf7\x97FSS\xa7\xbd\x02\xf9f$:\t',
|
||||
8: '\xb9\xa3N\x80u\x9c_\xf7\x97FSS\xa7\xbd\x02\xf9f$:\t',
|
||||
9: '\xb9\xa3N\x80u\x9c_\xf7\x97FSS\xa7\xbd\x02\xf9f$:\t',
|
||||
}
|
||||
happy = happiness_upload.calculate_happiness(share_placements)
|
||||
self.assertEqual(2, happy)
|
||||
|
||||
def test_hypothesis_0(self):
|
||||
"""
|
||||
an error-case Hypothesis found
|
||||
"""
|
||||
peers={u'0'}
|
||||
shares={u'0', u'1'}
|
||||
|
||||
places = happiness_upload.share_placement(peers, set(), shares, {})
|
||||
happiness = happiness_upload.calculate_happiness(places)
|
||||
|
||||
assert set(places.values()).issubset(peers)
|
||||
assert happiness == min(len(peers), len(shares))
|
||||
|
||||
def test_hypothesis_1(self):
|
||||
"""
|
||||
an error-case Hypothesis found
|
||||
"""
|
||||
peers = {u'0', u'1', u'2', u'3'}
|
||||
shares = {u'0', u'1', u'2', u'3', u'4', u'5', u'6', u'7', u'8'}
|
||||
|
||||
places = happiness_upload.share_placement(peers, set(), shares, {})
|
||||
happiness = happiness_upload.calculate_happiness(places)
|
||||
|
||||
assert set(places.values()).issubset(peers)
|
||||
assert happiness == min(len(peers), len(shares))
|
||||
|
||||
def test_everything_broken(self):
|
||||
peers = set()
|
||||
shares = {u'0', u'1', u'2', u'3'}
|
||||
|
||||
places = happiness_upload.share_placement(peers, set(), shares, {})
|
||||
self.assertEqual(places, dict())
|
||||
|
||||
|
||||
class PlacementTests(unittest.TestCase):
|
||||
|
||||
@given(
|
||||
sets(elements=text(min_size=1), min_size=4, max_size=4),
|
||||
sets(elements=text(min_size=1), min_size=4),
|
||||
)
|
||||
def test_hypothesis_unhappy(self, peers, shares):
|
||||
"""
|
||||
similar to test_unhappy we test that the resulting happiness is
|
||||
always 4 since the size of peers is 4.
|
||||
"""
|
||||
# https://hypothesis.readthedocs.io/en/latest/data.html#hypothesis.strategies.sets
|
||||
# hypothesis.strategies.sets(elements=None, min_size=None, average_size=None, max_size=None)[source]
|
||||
readonly_peers = set()
|
||||
peers_to_shares = {}
|
||||
places = happiness_upload.share_placement(peers, readonly_peers, shares, peers_to_shares)
|
||||
happiness = happiness_upload.calculate_happiness(places)
|
||||
assert set(places.keys()) == shares
|
||||
assert happiness == 4
|
||||
|
||||
@given(
|
||||
sets(elements=text(min_size=1), min_size=1, max_size=10),
|
||||
# can we make a readonly_peers that's a subset of ^
|
||||
sets(elements=text(min_size=1), min_size=1, max_size=20),
|
||||
)
|
||||
def test_more_hypothesis(self, peers, shares):
|
||||
"""
|
||||
similar to test_unhappy we test that the resulting happiness is
|
||||
always either the number of peers or the number of shares
|
||||
whichever is smaller.
|
||||
"""
|
||||
# https://hypothesis.readthedocs.io/en/latest/data.html#hypothesis.strategies.sets
|
||||
# hypothesis.strategies.sets(elements=None, min_size=None, average_size=None, max_size=None)[source]
|
||||
# XXX would be nice to paramaterize these by hypothesis too
|
||||
readonly_peers = set()
|
||||
peers_to_shares = {}
|
||||
|
||||
places = happiness_upload.share_placement(peers, readonly_peers, set(list(shares)), peers_to_shares)
|
||||
happiness = happiness_upload.calculate_happiness(places)
|
||||
|
||||
# every share should get placed
|
||||
assert set(places.keys()) == shares
|
||||
|
||||
# we should only use peers that exist
|
||||
assert set(places.values()).issubset(peers)
|
||||
|
||||
# if we have more shares than peers, happiness is at most # of
|
||||
# peers; if we have fewer shares than peers happiness is capped at
|
||||
# # of peers.
|
||||
assert happiness == min(len(peers), len(shares))
|
@ -233,7 +233,12 @@ class HungServerDownloadTest(GridTestMixin, ShouldFailMixin, PollMixin,
|
||||
done = []
|
||||
d = self._set_up(False, "test_5_overdue_immutable")
|
||||
def _reduce_max_outstanding_requests_and_download(ign):
|
||||
self._hang_shares(range(5))
|
||||
# we need to hang the first 5 servers, so we have to
|
||||
# figure out where the shares were placed.
|
||||
si = uri.from_string(self.uri).get_storage_index()
|
||||
placed = self.c0.storage_broker.get_servers_for_psi(si)
|
||||
self._hang([(s.get_serverid(), s) for s in placed[:5]])
|
||||
|
||||
n = self.c0.create_node_from_uri(self.uri)
|
||||
n._cnode._maybe_create_download_node()
|
||||
self._sf = n._cnode._node._sharefinder
|
||||
|
@ -706,8 +706,10 @@ class Repairer(GridTestMixin, unittest.TestCase, RepairTestMixin,
|
||||
# filecheck, but then *do* respond to the post-repair filecheck
|
||||
def _then(ign):
|
||||
ss = self.g.servers_by_number[0]
|
||||
self.g.break_server(ss.my_nodeid, count=1)
|
||||
self.delete_shares_numbered(self.uri, [9])
|
||||
# we want to delete the share corresponding to the server
|
||||
# we're making not-respond
|
||||
share = next(ss._get_bucket_shares(self.c0_filenode.get_storage_index()))[0]
|
||||
self.delete_shares_numbered(self.uri, [share])
|
||||
return self.c0_filenode.check_and_repair(Monitor())
|
||||
d.addCallback(_then)
|
||||
def _check(rr):
|
||||
|
@ -4,7 +4,7 @@ import os, shutil
|
||||
from cStringIO import StringIO
|
||||
from twisted.trial import unittest
|
||||
from twisted.python.failure import Failure
|
||||
from twisted.internet import defer
|
||||
from twisted.internet import defer, task
|
||||
from foolscap.api import fireEventually
|
||||
|
||||
import allmydata # for __full_version__
|
||||
@ -17,7 +17,7 @@ from allmydata.util.deferredutil import DeferredListShouldSucceed
|
||||
from allmydata.test.no_network import GridTestMixin
|
||||
from allmydata.test.common_util import ShouldFailMixin
|
||||
from allmydata.util.happinessutil import servers_of_happiness, \
|
||||
shares_by_server, merge_servers
|
||||
shares_by_server, merge_servers
|
||||
from allmydata.storage_client import StorageFarmBroker
|
||||
from allmydata.storage.server import storage_index_to_dir
|
||||
from allmydata.client import Client
|
||||
@ -101,19 +101,26 @@ class SetDEPMixin:
|
||||
self.node.encoding_params = p
|
||||
|
||||
class FakeStorageServer:
|
||||
def __init__(self, mode):
|
||||
def __init__(self, mode, reactor=None):
|
||||
self.mode = mode
|
||||
self.allocated = []
|
||||
self.queries = 0
|
||||
self.version = { "http://allmydata.org/tahoe/protocols/storage/v1" :
|
||||
{ "maximum-immutable-share-size": 2**32 - 1 },
|
||||
"application-version": str(allmydata.__full_version__),
|
||||
}
|
||||
self._alloc_queries = 0
|
||||
self._get_queries = 0
|
||||
self.version = {
|
||||
"http://allmydata.org/tahoe/protocols/storage/v1" :
|
||||
{
|
||||
"maximum-immutable-share-size": 2**32 - 1,
|
||||
},
|
||||
"application-version": str(allmydata.__full_version__),
|
||||
}
|
||||
if mode == "small":
|
||||
self.version = { "http://allmydata.org/tahoe/protocols/storage/v1" :
|
||||
{ "maximum-immutable-share-size": 10 },
|
||||
"application-version": str(allmydata.__full_version__),
|
||||
}
|
||||
self.version = {
|
||||
"http://allmydata.org/tahoe/protocols/storage/v1" :
|
||||
{
|
||||
"maximum-immutable-share-size": 10,
|
||||
},
|
||||
"application-version": str(allmydata.__full_version__),
|
||||
}
|
||||
|
||||
|
||||
def callRemote(self, methname, *args, **kwargs):
|
||||
@ -126,14 +133,16 @@ class FakeStorageServer:
|
||||
|
||||
def allocate_buckets(self, storage_index, renew_secret, cancel_secret,
|
||||
sharenums, share_size, canary):
|
||||
#print "FakeStorageServer.allocate_buckets(num=%d, size=%d)" % (len(sharenums), share_size)
|
||||
# print "FakeStorageServer.allocate_buckets(num=%d, size=%d, mode=%s, queries=%d)" % (len(sharenums), share_size, self.mode, self._alloc_queries)
|
||||
if self.mode == "timeout":
|
||||
return defer.Deferred()
|
||||
if self.mode == "first-fail":
|
||||
if self.queries == 0:
|
||||
if self._alloc_queries == 0:
|
||||
raise ServerError
|
||||
if self.mode == "second-fail":
|
||||
if self.queries == 1:
|
||||
if self._alloc_queries == 1:
|
||||
raise ServerError
|
||||
self.queries += 1
|
||||
self._alloc_queries += 1
|
||||
if self.mode == "full":
|
||||
return (set(), {},)
|
||||
elif self.mode == "already got them":
|
||||
@ -146,6 +155,18 @@ class FakeStorageServer:
|
||||
for shnum in sharenums]),
|
||||
)
|
||||
|
||||
def get_buckets(self, storage_index, **kw):
|
||||
# this should map shnum to a BucketReader but there isn't a
|
||||
# handy FakeBucketReader and we don't actually read the shares
|
||||
# back anyway (just the keys)
|
||||
return {
|
||||
shnum: None
|
||||
for (si, shnum) in self.allocated
|
||||
if si == storage_index
|
||||
}
|
||||
|
||||
|
||||
|
||||
class FakeBucketWriter:
|
||||
# a diagnostic version of storageserver.BucketWriter
|
||||
def __init__(self, size):
|
||||
@ -184,20 +205,23 @@ class FakeBucketWriter:
|
||||
def remote_abort(self):
|
||||
pass
|
||||
|
||||
class FakeClient:
|
||||
DEFAULT_ENCODING_PARAMETERS = {"k":25,
|
||||
"happy": 25,
|
||||
"n": 100,
|
||||
"max_segment_size": 1*MiB,
|
||||
}
|
||||
class FakeClient(object):
|
||||
DEFAULT_ENCODING_PARAMETERS = {
|
||||
"k":25,
|
||||
"happy": 25,
|
||||
"n": 100,
|
||||
"max_segment_size": 1 * MiB,
|
||||
}
|
||||
|
||||
def __init__(self, mode="good", num_servers=50):
|
||||
def __init__(self, mode="good", num_servers=50, reactor=None):
|
||||
self.num_servers = num_servers
|
||||
self.encoding_params = self.DEFAULT_ENCODING_PARAMETERS.copy()
|
||||
if type(mode) is str:
|
||||
mode = dict([i,mode] for i in range(num_servers))
|
||||
servers = [ ("%20d"%fakeid, FakeStorageServer(mode[fakeid]))
|
||||
for fakeid in range(self.num_servers) ]
|
||||
servers = [
|
||||
("%20d" % fakeid, FakeStorageServer(mode[fakeid], reactor=reactor))
|
||||
for fakeid in range(self.num_servers)
|
||||
]
|
||||
self.storage_broker = StorageFarmBroker(permute_peers=True, tub_maker=None)
|
||||
for (serverid, rref) in servers:
|
||||
ann = {"anonymous-storage-FURL": "pb://%s@nowhere/fake" % base32.b2a(serverid),
|
||||
@ -248,15 +272,21 @@ SIZE_ZERO = 0
|
||||
SIZE_SMALL = 16
|
||||
SIZE_LARGE = len(DATA)
|
||||
|
||||
def upload_data(uploader, data):
|
||||
|
||||
def upload_data(uploader, data, reactor=None):
|
||||
u = upload.Data(data, convergence=None)
|
||||
return uploader.upload(u)
|
||||
def upload_filename(uploader, filename):
|
||||
return uploader.upload(u, reactor=reactor)
|
||||
|
||||
|
||||
def upload_filename(uploader, filename, reactor=None):
|
||||
u = upload.FileName(filename, convergence=None)
|
||||
return uploader.upload(u)
|
||||
def upload_filehandle(uploader, fh):
|
||||
return uploader.upload(u, reactor=reactor)
|
||||
|
||||
|
||||
def upload_filehandle(uploader, fh, reactor=None):
|
||||
u = upload.FileHandle(fh, convergence=None)
|
||||
return uploader.upload(u)
|
||||
return uploader.upload(u, reactor=reactor)
|
||||
|
||||
|
||||
class GoodServer(unittest.TestCase, ShouldFailMixin, SetDEPMixin):
|
||||
def setUp(self):
|
||||
@ -425,38 +455,109 @@ class ServerErrors(unittest.TestCase, ShouldFailMixin, SetDEPMixin):
|
||||
d.addCallback(_check)
|
||||
return d
|
||||
|
||||
def test_second_error(self):
|
||||
# we want to make sure we make it to a third pass. This means that
|
||||
# the first pass was insufficient to place all shares, and at least
|
||||
# one of second pass servers (other than the last one) accepted a
|
||||
# share (so we'll believe that a third pass will be useful). (if
|
||||
# everyone but the last server throws an error, then we'll send all
|
||||
# the remaining shares to the last server at the end of the second
|
||||
# pass, and if that succeeds, we won't make it to a third pass).
|
||||
#
|
||||
# we can achieve this 97.5% of the time by using 40 servers, having
|
||||
# 39 of them fail on the second request, leaving only one to succeed
|
||||
# on the second request. (we need to keep the number of servers low
|
||||
# enough to ensure a second pass with 100 shares).
|
||||
mode = dict([(0,"good")] + [(i,"second-fail") for i in range(1,40)])
|
||||
self.make_node(mode, 40)
|
||||
d = upload_data(self.u, DATA)
|
||||
d.addCallback(extract_uri)
|
||||
d.addCallback(self._check_large, SIZE_LARGE)
|
||||
return d
|
||||
|
||||
def test_second_error_all(self):
|
||||
self.make_node("second-fail")
|
||||
d = self.shouldFail(UploadUnhappinessError, "second_error_all",
|
||||
"server selection failed",
|
||||
upload_data, self.u, DATA)
|
||||
def _check((f,)):
|
||||
self.failUnlessIn("placed 10 shares out of 100 total", str(f.value))
|
||||
# there should also be a 'last failure was' message
|
||||
self.failUnlessIn("ServerError", str(f.value))
|
||||
self.failUnlessIn("shares could be placed or found on only 10 server(s)", str(f.value))
|
||||
d.addCallback(_check)
|
||||
return d
|
||||
|
||||
def test_allocation_error_some(self):
|
||||
self.make_node({
|
||||
0: "good",
|
||||
1: "good",
|
||||
2: "good",
|
||||
3: "good",
|
||||
4: "good",
|
||||
5: "first-fail",
|
||||
6: "first-fail",
|
||||
7: "first-fail",
|
||||
8: "first-fail",
|
||||
9: "first-fail",
|
||||
})
|
||||
self.set_encoding_parameters(3, 7, 10)
|
||||
d = self.shouldFail(UploadUnhappinessError, "second_error_some",
|
||||
"server selection failed",
|
||||
upload_data, self.u, DATA)
|
||||
def _check((f,)):
|
||||
self.failUnlessIn("shares could be placed on only 5 server(s)", str(f.value))
|
||||
d.addCallback(_check)
|
||||
return d
|
||||
|
||||
def test_allocation_error_recovery(self):
|
||||
self.make_node({
|
||||
0: "good",
|
||||
1: "good",
|
||||
2: "good",
|
||||
3: "good",
|
||||
4: "second-fail",
|
||||
5: "second-fail",
|
||||
6: "first-fail",
|
||||
7: "first-fail",
|
||||
8: "first-fail",
|
||||
9: "first-fail",
|
||||
})
|
||||
self.set_encoding_parameters(3, 7, 10)
|
||||
# we placed shares on 0 through 5, which wasn't enough. so
|
||||
# then we looped and only placed on 0-3 (because now 4-9 have
|
||||
# all failed) ... so the error message should say we only
|
||||
# placed on 6 servers (not 4) because those two shares *did*
|
||||
# at some point succeed.
|
||||
d = self.shouldFail(UploadUnhappinessError, "second_error_some",
|
||||
"server selection failed",
|
||||
upload_data, self.u, DATA)
|
||||
def _check((f,)):
|
||||
self.failUnlessIn("shares could be placed on only 6 server(s)", str(f.value))
|
||||
d.addCallback(_check)
|
||||
return d
|
||||
|
||||
def test_good_servers_stay_writable(self):
|
||||
self.make_node({
|
||||
0: "good",
|
||||
1: "good",
|
||||
2: "second-fail",
|
||||
3: "second-fail",
|
||||
4: "second-fail",
|
||||
5: "first-fail",
|
||||
6: "first-fail",
|
||||
7: "first-fail",
|
||||
8: "first-fail",
|
||||
9: "first-fail",
|
||||
})
|
||||
self.set_encoding_parameters(3, 7, 10)
|
||||
# we placed shares on 0 through 5, which wasn't enough. so
|
||||
# then we looped and only placed on 0-3 (because now 4-9 have
|
||||
# all failed) ... so the error message should say we only
|
||||
# placed on 6 servers (not 4) because those two shares *did*
|
||||
# at some point succeed.
|
||||
d = self.shouldFail(UploadUnhappinessError, "good_servers_stay_writable",
|
||||
"server selection failed",
|
||||
upload_data, self.u, DATA)
|
||||
def _check((f,)):
|
||||
self.failUnlessIn("shares could be placed on only 5 server(s)", str(f.value))
|
||||
d.addCallback(_check)
|
||||
return d
|
||||
|
||||
def test_timeout(self):
|
||||
clock = task.Clock()
|
||||
self.make_node("timeout")
|
||||
self.set_encoding_parameters(k=25, happy=1, n=50)
|
||||
d = self.shouldFail(
|
||||
UploadUnhappinessError, __name__,
|
||||
"server selection failed",
|
||||
upload_data, self.u, DATA, reactor=clock,
|
||||
)
|
||||
# XXX double-check; it's doing 3 iterations?
|
||||
# XXX should only do 1!
|
||||
clock.advance(15)
|
||||
clock.advance(15)
|
||||
return d
|
||||
|
||||
|
||||
|
||||
class FullServer(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.node = FakeClient(mode="full")
|
||||
@ -515,7 +616,7 @@ class ServerSelection(unittest.TestCase):
|
||||
for s in self.node.last_servers:
|
||||
allocated = s.allocated
|
||||
self.failUnlessEqual(len(allocated), 1)
|
||||
self.failUnlessEqual(s.queries, 1)
|
||||
self.failUnlessEqual(s._alloc_queries, 1)
|
||||
d.addCallback(_check)
|
||||
return d
|
||||
|
||||
@ -534,7 +635,7 @@ class ServerSelection(unittest.TestCase):
|
||||
for s in self.node.last_servers:
|
||||
allocated = s.allocated
|
||||
self.failUnlessEqual(len(allocated), 2)
|
||||
self.failUnlessEqual(s.queries, 2)
|
||||
self.failUnlessEqual(s._alloc_queries, 1)
|
||||
d.addCallback(_check)
|
||||
return d
|
||||
|
||||
@ -555,10 +656,10 @@ class ServerSelection(unittest.TestCase):
|
||||
allocated = s.allocated
|
||||
self.failUnless(len(allocated) in (1,2), len(allocated))
|
||||
if len(allocated) == 1:
|
||||
self.failUnlessEqual(s.queries, 1)
|
||||
self.failUnlessEqual(s._alloc_queries, 1)
|
||||
got_one.append(s)
|
||||
else:
|
||||
self.failUnlessEqual(s.queries, 2)
|
||||
self.failUnlessEqual(s._alloc_queries, 1)
|
||||
got_two.append(s)
|
||||
self.failUnlessEqual(len(got_one), 49)
|
||||
self.failUnlessEqual(len(got_two), 1)
|
||||
@ -582,7 +683,7 @@ class ServerSelection(unittest.TestCase):
|
||||
for s in self.node.last_servers:
|
||||
allocated = s.allocated
|
||||
self.failUnlessEqual(len(allocated), 4)
|
||||
self.failUnlessEqual(s.queries, 2)
|
||||
self.failUnlessEqual(s._alloc_queries, 1)
|
||||
d.addCallback(_check)
|
||||
return d
|
||||
|
||||
@ -634,6 +735,21 @@ class ServerSelection(unittest.TestCase):
|
||||
d.addCallback(_check)
|
||||
return d
|
||||
|
||||
def test_number_of_servers_contacted(self):
|
||||
# This tests ensures that Tahoe only contacts 2n servers
|
||||
# during peer selection
|
||||
self.make_client(40)
|
||||
self.set_encoding_parameters(3, 7, 10)
|
||||
data = self.get_data(SIZE_LARGE)
|
||||
d = upload_data(self.u, data)
|
||||
def _check(res):
|
||||
servers_contacted = []
|
||||
for s in self.node.last_servers:
|
||||
if(s._alloc_queries != 0):
|
||||
servers_contacted.append(s)
|
||||
self.failUnless(len(servers_contacted), 20)
|
||||
d.addCallback(_check)
|
||||
return d
|
||||
|
||||
class StorageIndex(unittest.TestCase):
|
||||
def test_params_must_matter(self):
|
||||
@ -728,16 +844,11 @@ def is_happy_enough(servertoshnums, h, k):
|
||||
""" I calculate whether servertoshnums achieves happiness level h. I do this with a naïve "brute force search" approach. (See src/allmydata/util/happinessutil.py for a better algorithm.) """
|
||||
if len(servertoshnums) < h:
|
||||
return False
|
||||
# print "servertoshnums: ", servertoshnums, h, k
|
||||
for happysetcombo in combinations(servertoshnums.iterkeys(), h):
|
||||
# print "happysetcombo: ", happysetcombo
|
||||
for subsetcombo in combinations(happysetcombo, k):
|
||||
shnums = reduce(set.union, [ servertoshnums[s] for s in subsetcombo ])
|
||||
# print "subsetcombo: ", subsetcombo, ", shnums: ", shnums
|
||||
if len(shnums) < k:
|
||||
# print "NOT HAAPP{Y", shnums, k
|
||||
return False
|
||||
# print "HAAPP{Y"
|
||||
return True
|
||||
|
||||
class FakeServerTracker:
|
||||
@ -822,6 +933,7 @@ class EncodingParameters(GridTestMixin, unittest.TestCase, SetDEPMixin,
|
||||
ss = self.g.make_server(server_number, readonly)
|
||||
log.msg("just created a server, number: %s => %s" % (server_number, ss,))
|
||||
self.g.add_server(server_number, ss)
|
||||
self.g.rebuild_serverlist()
|
||||
|
||||
def _add_server_with_share(self, server_number, share_number=None,
|
||||
readonly=False):
|
||||
@ -945,7 +1057,7 @@ class EncodingParameters(GridTestMixin, unittest.TestCase, SetDEPMixin,
|
||||
self.basedir = "upload/EncodingParameters/aborted_shares"
|
||||
self.set_up_grid(num_servers=4)
|
||||
c = self.g.clients[0]
|
||||
DATA = upload.Data(100* "kittens", convergence="")
|
||||
DATA = upload.Data(100 * "kittens", convergence="")
|
||||
# These parameters are unsatisfiable with only 4 servers, but should
|
||||
# work with 5, as long as the original 4 are not stuck in the open
|
||||
# BucketWriter state (open() but not
|
||||
@ -1202,7 +1314,6 @@ class EncodingParameters(GridTestMixin, unittest.TestCase, SetDEPMixin,
|
||||
d.addCallback(lambda ign:
|
||||
self.failUnless(self._has_happy_share_distribution()))
|
||||
return d
|
||||
test_problem_layout_ticket_1124.todo = "Fix this after 1.7.1 release."
|
||||
|
||||
def test_happiness_with_some_readonly_servers(self):
|
||||
# Try the following layout
|
||||
@ -1597,7 +1708,7 @@ class EncodingParameters(GridTestMixin, unittest.TestCase, SetDEPMixin,
|
||||
d.addCallback(_setup)
|
||||
d.addCallback(lambda c:
|
||||
self.shouldFail(UploadUnhappinessError, "test_query_counting",
|
||||
"10 queries placed some shares",
|
||||
"0 queries placed some shares",
|
||||
c.upload, upload.Data("data" * 10000,
|
||||
convergence="")))
|
||||
# Now try with some readonly servers. We want to make sure that
|
||||
@ -1620,7 +1731,7 @@ class EncodingParameters(GridTestMixin, unittest.TestCase, SetDEPMixin,
|
||||
d.addCallback(_then)
|
||||
d.addCallback(lambda c:
|
||||
self.shouldFail(UploadUnhappinessError, "test_query_counting",
|
||||
"2 placed none (of which 2 placed none due to "
|
||||
"4 placed none (of which 4 placed none due to "
|
||||
"the server being full",
|
||||
c.upload, upload.Data("data" * 10000,
|
||||
convergence="")))
|
||||
@ -1650,7 +1761,7 @@ class EncodingParameters(GridTestMixin, unittest.TestCase, SetDEPMixin,
|
||||
d.addCallback(_next)
|
||||
d.addCallback(lambda c:
|
||||
self.shouldFail(UploadUnhappinessError, "test_query_counting",
|
||||
"1 queries placed some shares",
|
||||
"0 queries placed some shares",
|
||||
c.upload, upload.Data("data" * 10000,
|
||||
convergence="")))
|
||||
return d
|
||||
@ -1867,11 +1978,11 @@ class EncodingParameters(GridTestMixin, unittest.TestCase, SetDEPMixin,
|
||||
d.addCallback(lambda ign:
|
||||
self.failUnless(self._has_happy_share_distribution()))
|
||||
return d
|
||||
test_problem_layout_comment_187.todo = "this isn't fixed yet"
|
||||
|
||||
def test_problem_layout_ticket_1118(self):
|
||||
# #1118 includes a report from a user who hit an assertion in
|
||||
# the upload code with this layout.
|
||||
# Note that 'servers of happiness' lets this test work now
|
||||
self.basedir = self.mktemp()
|
||||
d = self._setup_and_upload(k=2, n=4)
|
||||
|
||||
@ -1893,17 +2004,6 @@ class EncodingParameters(GridTestMixin, unittest.TestCase, SetDEPMixin,
|
||||
return client
|
||||
|
||||
d.addCallback(_setup)
|
||||
# Note: actually it should succeed! See
|
||||
# test_problem_layout_ticket_1128. But ticket 1118 is just to
|
||||
# make it realize that it has failed, so if it raises
|
||||
# UploadUnhappinessError then we'll give it the green light
|
||||
# for now.
|
||||
d.addCallback(lambda ignored:
|
||||
self.shouldFail(UploadUnhappinessError,
|
||||
"test_problem_layout_ticket_1118",
|
||||
"",
|
||||
self.g.clients[0].upload, upload.Data("data" * 10000,
|
||||
convergence="")))
|
||||
return d
|
||||
|
||||
def test_problem_layout_ticket_1128(self):
|
||||
@ -1936,7 +2036,6 @@ class EncodingParameters(GridTestMixin, unittest.TestCase, SetDEPMixin,
|
||||
d.addCallback(lambda ign:
|
||||
self.failUnless(self._has_happy_share_distribution()))
|
||||
return d
|
||||
test_problem_layout_ticket_1128.todo = "Invent a smarter uploader that uploads successfully in this case."
|
||||
|
||||
def test_upload_succeeds_with_some_homeless_shares(self):
|
||||
# If the upload is forced to stop trying to place shares before
|
||||
|
@ -1094,7 +1094,7 @@ class Grid(GridTestMixin, WebErrorMixin, ShouldFailMixin, testutil.ReallyEqualMi
|
||||
" overdue= unused= need 3. Last failure: None")
|
||||
msg2 = msgbase + (" ran out of shares:"
|
||||
" complete="
|
||||
" pending=Share(sh0-on-xgru5)"
|
||||
" pending=Share(sh0-on-ysbz4st7)"
|
||||
" overdue= unused= need 3. Last failure: None")
|
||||
self.failUnless(body == msg1 or body == msg2, body)
|
||||
d.addCallback(_check_one_share)
|
||||
|
@ -2,13 +2,42 @@
|
||||
import time
|
||||
|
||||
from foolscap.api import eventually, fireEventually
|
||||
from twisted.internet import defer, reactor
|
||||
from twisted.internet import defer, reactor, error
|
||||
from twisted.python.failure import Failure
|
||||
|
||||
from allmydata.util import log
|
||||
from allmydata.util.assertutil import _assert
|
||||
from allmydata.util.pollmixin import PollMixin
|
||||
|
||||
|
||||
class TimeoutError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def timeout_call(reactor, d, timeout):
|
||||
"""
|
||||
This returns the result of 'd', unless 'timeout' expires before
|
||||
'd' is completed in which case a TimeoutError is raised.
|
||||
"""
|
||||
timer_d = defer.Deferred()
|
||||
|
||||
def _timed_out():
|
||||
timer_d.errback(Failure(TimeoutError()))
|
||||
|
||||
def _got_result(x):
|
||||
try:
|
||||
timer.cancel()
|
||||
timer_d.callback(x)
|
||||
except error.AlreadyCalled, defer.AlreadyCalledError:
|
||||
pass
|
||||
return None
|
||||
|
||||
timer = reactor.callLater(timeout, _timed_out)
|
||||
d.addBoth(_got_result)
|
||||
return timer_d
|
||||
|
||||
|
||||
|
||||
# utility wrapper for DeferredList
|
||||
def _check_deferred_list(results):
|
||||
# if any of the component Deferreds failed, return the first failure such
|
||||
|
@ -4,6 +4,9 @@ reporting it in messages
|
||||
"""
|
||||
|
||||
from copy import deepcopy
|
||||
from allmydata.immutable.happiness_upload import residual_network
|
||||
from allmydata.immutable.happiness_upload import augmenting_path_for
|
||||
|
||||
|
||||
def failure_message(peer_count, k, happy, effective_happy):
|
||||
# If peer_count < needed_shares, this error message makes more
|
||||
@ -77,6 +80,7 @@ def merge_servers(servermap, upload_trackers=None):
|
||||
servermap.setdefault(shnum, set()).add(tracker.get_serverid())
|
||||
return servermap
|
||||
|
||||
|
||||
def servers_of_happiness(sharemap):
|
||||
"""
|
||||
I accept 'sharemap', a dict of shareid -> set(peerid) mappings. I
|
||||
@ -126,8 +130,13 @@ def servers_of_happiness(sharemap):
|
||||
"""
|
||||
if sharemap == {}:
|
||||
return 0
|
||||
sharemap = shares_by_server(sharemap)
|
||||
graph = flow_network_for(sharemap)
|
||||
servermap = shares_by_server(sharemap)
|
||||
graph = _flow_network_for(servermap)
|
||||
|
||||
# XXX this core stuff is identical to
|
||||
# happiness_upload._compute_maximum_graph and we should find a way
|
||||
# to share the code.
|
||||
|
||||
# This is an implementation of the Ford-Fulkerson method for finding
|
||||
# a maximum flow in a flow network applied to a bipartite graph.
|
||||
# Specifically, it is the Edmonds-Karp algorithm, since it uses a
|
||||
@ -154,7 +163,7 @@ def servers_of_happiness(sharemap):
|
||||
flow_function[v][u] -= delta
|
||||
residual_graph, residual_function = residual_network(graph,
|
||||
flow_function)
|
||||
num_servers = len(sharemap)
|
||||
num_servers = len(servermap)
|
||||
# The value of a flow is the total flow out of the source vertex
|
||||
# (vertex 0, in our graph). We could just as well sum across all of
|
||||
# f[0], but we know that vertex 0 only has edges to the servers in
|
||||
@ -163,14 +172,14 @@ def servers_of_happiness(sharemap):
|
||||
# matching on the bipartite graph described above.
|
||||
return sum([flow_function[0][v] for v in xrange(1, num_servers+1)])
|
||||
|
||||
def flow_network_for(sharemap):
|
||||
def _flow_network_for(servermap):
|
||||
"""
|
||||
I take my argument, a dict of peerid -> set(shareid) mappings, and
|
||||
turn it into a flow network suitable for use with Edmonds-Karp. I
|
||||
then return the adjacency list representation of that network.
|
||||
|
||||
Specifically, I build G = (V, E), where:
|
||||
V = { peerid in sharemap } U { shareid in sharemap } U {s, t}
|
||||
V = { peerid in servermap } U { shareid in servermap } U {s, t}
|
||||
E = {(s, peerid) for each peerid}
|
||||
U {(peerid, shareid) if peerid is to store shareid }
|
||||
U {(shareid, t) for each shareid}
|
||||
@ -185,16 +194,16 @@ def flow_network_for(sharemap):
|
||||
# we re-index so that all of our vertices have integral indices, and
|
||||
# that there aren't any holes. We start indexing at 1, so that we
|
||||
# can add a source node at index 0.
|
||||
sharemap, num_shares = reindex(sharemap, base_index=1)
|
||||
num_servers = len(sharemap)
|
||||
servermap, num_shares = _reindex(servermap, base_index=1)
|
||||
num_servers = len(servermap)
|
||||
graph = [] # index -> [index], an adjacency list
|
||||
# Add an entry at the top (index 0) that has an edge to every server
|
||||
# in sharemap
|
||||
graph.append(sharemap.keys())
|
||||
# in servermap
|
||||
graph.append(servermap.keys())
|
||||
# For each server, add an entry that has an edge to every share that it
|
||||
# contains (or will contain).
|
||||
for k in sharemap:
|
||||
graph.append(sharemap[k])
|
||||
for k in servermap:
|
||||
graph.append(servermap[k])
|
||||
# For each share, add an entry that has an edge to the sink.
|
||||
sink_num = num_servers + num_shares + 1
|
||||
for i in xrange(num_shares):
|
||||
@ -203,20 +212,22 @@ def flow_network_for(sharemap):
|
||||
graph.append([])
|
||||
return graph
|
||||
|
||||
def reindex(sharemap, base_index):
|
||||
|
||||
# XXX warning: this is different from happiness_upload's _reindex!
|
||||
def _reindex(servermap, base_index):
|
||||
"""
|
||||
Given sharemap, I map peerids and shareids to integers that don't
|
||||
Given servermap, I map peerids and shareids to integers that don't
|
||||
conflict with each other, so they're useful as indices in a graph. I
|
||||
return a sharemap that is reindexed appropriately, and also the
|
||||
number of distinct shares in the resulting sharemap as a convenience
|
||||
return a servermap that is reindexed appropriately, and also the
|
||||
number of distinct shares in the resulting servermap as a convenience
|
||||
for my caller. base_index tells me where to start indexing.
|
||||
"""
|
||||
shares = {} # shareid -> vertex index
|
||||
num = base_index
|
||||
ret = {} # peerid -> [shareid], a reindexed sharemap.
|
||||
ret = {} # peerid -> [shareid], a reindexed servermap.
|
||||
# Number the servers first
|
||||
for k in sharemap:
|
||||
ret[num] = sharemap[k]
|
||||
for k in servermap:
|
||||
ret[num] = servermap[k]
|
||||
num += 1
|
||||
# Number the shares
|
||||
for k in ret:
|
||||
@ -226,77 +237,3 @@ def reindex(sharemap, base_index):
|
||||
num += 1
|
||||
ret[k] = map(lambda x: shares[x], ret[k])
|
||||
return (ret, len(shares))
|
||||
|
||||
def residual_network(graph, f):
|
||||
"""
|
||||
I return the residual network and residual capacity function of the
|
||||
flow network represented by my graph and f arguments. graph is a
|
||||
flow network in adjacency-list form, and f is a flow in graph.
|
||||
"""
|
||||
new_graph = [[] for i in xrange(len(graph))]
|
||||
cf = [[0 for s in xrange(len(graph))] for sh in xrange(len(graph))]
|
||||
for i in xrange(len(graph)):
|
||||
for v in graph[i]:
|
||||
if f[i][v] == 1:
|
||||
# We add an edge (v, i) with cf[v,i] = 1. This means
|
||||
# that we can remove 1 unit of flow from the edge (i, v)
|
||||
new_graph[v].append(i)
|
||||
cf[v][i] = 1
|
||||
cf[i][v] = -1
|
||||
else:
|
||||
# We add the edge (i, v), since we're not using it right
|
||||
# now.
|
||||
new_graph[i].append(v)
|
||||
cf[i][v] = 1
|
||||
cf[v][i] = -1
|
||||
return (new_graph, cf)
|
||||
|
||||
def augmenting_path_for(graph):
|
||||
"""
|
||||
I return an augmenting path, if there is one, from the source node
|
||||
to the sink node in the flow network represented by my graph argument.
|
||||
If there is no augmenting path, I return False. I assume that the
|
||||
source node is at index 0 of graph, and the sink node is at the last
|
||||
index. I also assume that graph is a flow network in adjacency list
|
||||
form.
|
||||
"""
|
||||
bfs_tree = bfs(graph, 0)
|
||||
if bfs_tree[len(graph) - 1]:
|
||||
n = len(graph) - 1
|
||||
path = [] # [(u, v)], where u and v are vertices in the graph
|
||||
while n != 0:
|
||||
path.insert(0, (bfs_tree[n], n))
|
||||
n = bfs_tree[n]
|
||||
return path
|
||||
return False
|
||||
|
||||
def bfs(graph, s):
|
||||
"""
|
||||
Perform a BFS on graph starting at s, where graph is a graph in
|
||||
adjacency list form, and s is a node in graph. I return the
|
||||
predecessor table that the BFS generates.
|
||||
"""
|
||||
# This is an adaptation of the BFS described in "Introduction to
|
||||
# Algorithms", Cormen et al, 2nd ed., p. 532.
|
||||
# WHITE vertices are those that we haven't seen or explored yet.
|
||||
WHITE = 0
|
||||
# GRAY vertices are those we have seen, but haven't explored yet
|
||||
GRAY = 1
|
||||
# BLACK vertices are those we have seen and explored
|
||||
BLACK = 2
|
||||
color = [WHITE for i in xrange(len(graph))]
|
||||
predecessor = [None for i in xrange(len(graph))]
|
||||
distance = [-1 for i in xrange(len(graph))]
|
||||
queue = [s] # vertices that we haven't explored yet.
|
||||
color[s] = GRAY
|
||||
distance[s] = 0
|
||||
while queue:
|
||||
n = queue.pop(0)
|
||||
for v in graph[n]:
|
||||
if color[v] == WHITE:
|
||||
color[v] = GRAY
|
||||
distance[v] = distance[n] + 1
|
||||
predecessor[v] = n
|
||||
queue.append(v)
|
||||
color[n] = BLACK
|
||||
return predecessor
|
||||
|
Loading…
x
Reference in New Issue
Block a user