broken assert in upload.py #1118
Labels
No Label
0.2.0
0.3.0
0.4.0
0.5.0
0.5.1
0.6.0
0.6.1
0.7.0
0.8.0
0.9.0
1.0.0
1.1.0
1.10.0
1.10.1
1.10.2
1.10a2
1.11.0
1.12.0
1.12.1
1.13.0
1.14.0
1.15.0
1.15.1
1.2.0
1.3.0
1.4.1
1.5.0
1.6.0
1.6.1
1.7.0
1.7.1
1.7β
1.8.0
1.8.1
1.8.2
1.8.3
1.8β
1.9.0
1.9.0-s3branch
1.9.0a1
1.9.0a2
1.9.0b1
1.9.1
1.9.2
1.9.2a1
LeastAuthority.com automation
blocker
cannot reproduce
cloud-branch
code
code-dirnodes
code-encoding
code-frontend
code-frontend-cli
code-frontend-ftp-sftp
code-frontend-magic-folder
code-frontend-web
code-mutable
code-network
code-nodeadmin
code-peerselection
code-storage
contrib
critical
defect
dev-infrastructure
documentation
duplicate
enhancement
fixed
invalid
major
minor
n/a
normal
operational
packaging
somebody else's problem
supercritical
task
trivial
unknown
was already fixed
website
wontfix
worksforme
No Milestone
No Assignees
3 Participants
Notifications
Due Date
No due date set.
Reference: tahoe-lafs/trac-2024-07-25#1118
Loading…
Reference in New Issue
No description provided.
Delete Branch "%!s(<nil>)"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
from http://tahoe-lafs.org/pipermail/tahoe-dev/2010-July/004656.html:
Kevan: could this be a regression from v1.6.0? Looking at the annotation of that file, the assertion and the calculation of buckets dates from way back (patch changeset:17299fc96e6fb855!), but of course the server-selection was changed in v1.7.0 thanks to your work, so maybe it is a regression.
(caveat: I haven't thoroughly investigated this issue yet, so what I say below might not be the case)
I think that the old peer selection technique would never end up allocating a remote bucket for a particular share more than once. If the new peer selection technique redistributes shares, it can end up doing that if it ends up allocating a share to a different peer in the redistribution process than it did in the initial process. The assert, as written, works fine for the former case, because there will never be more buckets than are in the dictionary. But in the servers-of-happiness case, there can be. Wait, I guess there shouldn't be -- there is no reason for
used_peers
to contain duplicate buckets, since we should be aborting and removing the ones that we reassign. So in this case, the assert is right, and the code is wrong. I think. I'll look at this tomorrow when I'm less sleepy.In summary, I think this is a regression from 1.6.0.
Attachment stuff.flog.bz2 (9028 bytes) added
flogtool log
I've just attached stuff.flog.bz2 containing one instance of each problem I reported in the original e-mail to tahoe-dev. This log is from the node performing the backup. (I couldn't get flogtool running on Windows, and the three other nodes in the grid are all different flavors of Windows.)
First is the assert error:
Second, I see that if I immediately retry the backup operation, I will immediately get the second problem of being unable to place shares even though no storage nodes are anywhere near full:
I think it's important to note that as I was using my grid I originally saw the UploadUnhappinessError a couple times, nonreproducibly. Only after encountering the (reproducible) assert error do I now have an easily reproducible UploadUnhappinessError.
The persistent
UploadUnhappinessError
is probably explained by the failure to abort the allocated-but-not-written shares, covered in #1117. If the number of available servers in the grid is not much bigger than N, it wouldn't take more than one or two of theseassert
failures to get your client into a state where it would think that the servers were all full. Restarting the client (specifically dropping and reestablishing the TCP connections to the servers) should cause those not-yet-written shares to be abandoned, clearing the problem.I tried to write a unit test for this case, but my server 0 is constructed by the test code as having all shares. I want server 0 to have no shares. Can someone else tell me how to write a unit test that creates a server with no shares? Here's the relevant part of the test.log:
I will attach the test code as a darcs patch.
When you do
you cause server 0 to be created, and to receive all of the initial shares of the upload. The server 0 that you create later shares a storage directory with the first one, so from the perspective of the second upload, it does have all the shares. You can fix this by emptying the
storedir
of server 0 after you've finished copying shares, using something like this:placed after the
self._copy_share_to_server
call. On my machine, this modification raises theAssertionError
.Brian suggested on IRC last night that maybe the assertion should just be removed.
Here's a better patch for adding debug diagnostics. I don't intend to commit this to trunk before 1.7.1, but it might be useful for people who are debugging.
Attachment debugging-1118-2.dpatch.txt (19274 bytes) added
Here is a unit test which uses Kevan's suggestion to reproduce the assertion failure. I'll attach it as a darcs patch.
Attachment test-1118.dpatch.txt (12858 bytes) added
Okay, I've figured this out.
CHKUploader decides what to upload, including potentially deciding to upload one share to multiple servers. But
encoder.set_shareholders()
takes only a dict mapping from shnum to IStorageBucketWriter, so encoder will not upload a shnum to more than one server.The assertion is showing this mismatch.
Now what to do about it? We could simply comment-out the assertion, but then for each shnum that CHKUploader wants to upload to multiple servers, it will get uploaded to only one (random) server. Interesting fact: in this case, as reported by Kyle Markley and implemented in attachment:test-1118.dpatch.txt, it wouldn't matter! CHKUploader is trying to upload sh0 to two different servers, but as far as I can tell if it uploads to either one of the servers it will still achieve happiness level 2.
(Question: why then is CHKUploader overdoing it? Kevan: do you know?)
Next step is to write a unit test for a situation in which you need to actually successfully upload a share to more than one server in order to achieve the desired happiness level. The current upload code would get this assertion failure under that test. Even if we commented out [that assertion]source:src/allmydata/immutable/upload.py@4562#L919, then the code would think that it had successfully uploaded but would in fact leave an insufficiently happy share distribution.
I'm not sure if we should hold up v1.7.1 for this issue.
I think the solution is actually fairly straightforward to extend encoder to accept a dict from shnum to set of IStorageBucketWriter instead of to a single IStorageBucketWriter. We should of course write the aforementioned unit test first.
I'm thinking about it, but I'm having trouble coming up with a case where uploading one share to multiple servers would increase the size of a maximum matching in the graph that happiness is. I think that the fact that
CHKUploader
does this is a bug. I think that the only time that it would open multiple bucketwriters for one share is the case where it redistributes a share to another server to try to increase happiness. When it does this, it should look in its collection of remote proxies and check to see if it has one for the share that it redistributes -- if it does, that should beabort()
ed and removed. Extending the encoder to accept a dict ofshnum => set(IStorageBucketWriter)
would also solve the issue, but, if I'm right about multiply-uploaded shares not increasing happiness, at an efficiency cost, in that an upload would need to upload1/k
more data without any increase in happiness. I favor the first approach, and favor leaving theassert
for that reason.David-Sarah asked on IRC: doesn't uploader check whether it has actually successfully uploaded to happiness level
H
? I thought so, but I looked in the source code to remind myself. Bad news: [Encoder._remove_shareholder()]source:src/allmydata/immutable/encode.py@4308#L483 checks by using its.servermap
attribute, which was initialized with the intended upload plan of CHKUploader, so it contains the plan to upload the same shnum to multiple servers, so Encoder might be falsely thinking that the plan is still capable of achieving happiness when it is not.We want to have some unit tests which test whether the actual share distribution that ends up on the servers satisfied, not just whether the CHKUploader and Encoder were satisfied with their own progress. Let's see...
Hm, no most of the tests in source:src/allmydata/test/test_upload.py are just relying on the uploader's self-evaluation. I see there is one test that looks at
result.pushed_shares
. That could be a useful thing to check too. But in general we should probably add to some or all of the tests which try an upload that afterward we inspect the share distribution on the servers and fail the test if that distribution isn't actually happy.Now another tweak that we could do right away is to change the calculation of
servermap
in CHKUploader so that it doesn't include a plan to upload the same shnum to multiple servers. That would be in [CHKUploader.set_shareholders()]source:src/allmydata/immutable/upload.py@4562#L899 and is an easy change to make.Replying to kevan:
That does sound right! So, we should still extend the test_upload.py unit tests to inspect the actual share distribution, and we should leave the assertion in place, and we should figure out ...
Oh wait, see the example in attachment:test-1118.dpatch.txt. It is choosing to upload sh0 to two different servers, neither of which is the server that already had sh0! So I agree that this is, in this particular instance at least, a bug in CHKUploader, but it isn't the bug you thought of just now.
I'm working on extending the tests in source:src/allmydata/test/test_upload.py to inspect the actual share distribution's happiness level.
Attachment diff.txt (967 bytes) added
This diff changes the
abort
function on the PeerTracker class to take a list of sharenumbers that should be aborted, rather than simply aborting all of them. It also aborts all of the homeless shares after the reassignment algorithm runs. AFAICT, it makes the test in test-1118.dpatch.txt pass.Attachment fix-1118.dpatch (12881 bytes) added
Please review this patch which extends the unit tests to make sure that the shares that land on the storage servers really do form a happy distribution. It would be okay to leave this patch out of 1.7.1 since there is no known bug that these tests would have caught, but it might be good to apply it too.
Attachment happy_share_distribution.dpatch.txt (38344 bytes) added
It is a bit confusing to attempt aborting on all of
homeless_shares
, since that gets populated with all shares at the beginning and then gradually whittled down and sometimes repopulated. It would make more sense to me to just attempt aborting on the shares that are getting aborted right now -- theshare
variable inside thewhile
loop. (Also of course this would waste a tad fewer CPU cycles.)Replying to zooko:
The reason I didn't do this was that I couldn't think of an obvious way to know which of the
PeerTracker
s, if any, was the appropriate one for a particular share. So, to abort within the loop, we'd have to loop through the list of peer trackers once for each share that we wanted to abort. By aborting all of the homeless shares at once, we only have to loop through that list once. To avoid wasting CPU cycles on homeless shares that aren't homeless as a result of redistribution, we could store those in a separate list, and then just abort that list at the end, if you think that addresses your comment (or I could be missing something really obvious which may also address your comment).reviewed! +1
Attachment fix-1118-v2.dpatch (13226 bytes) added
upload.py: abort homeless buckets when reassignment runs. This makes a previously failing assert correct. This versi on refactors 'abort' into two methods, rather than using a default argument. fixes #1118
Attachment improved-logging.dpatch.txt (52523 bytes) added
The problem with hitting an assertion failure is fixed by changeset:461f9464579f2442. The other problem that it would sometimes upload an unhappy share distribution while thinking it was happy is fixed by changeset:13b5e44fbc2effd0 -- see also tests in changeset:2e83f243c2c2492b and changeset:1dbfcf753de46e06. The remaining problem, that it gives up with unhappiness when it could more cleverly find a happy way to upload in this situation, is moved to ticket #1128.
Thanks Kyle, Kevan, David-Sarah, and Brian!
In changeset:461f9464579f2442:
Fixing this problem uncovered another one, due to a bug in [merge_peers in happinessutil.py]source:src/allmydata/util/happinessutil.py@4308#L55, which sometimes resulted in an upload succeeding even though the share distribution should not have been considered happy.
merge_peers
was only shallow-copying itsservermap
argument, rather than deep-copying it. This resulted in the sets of servers being aliased between themerged
result, and theself.preexisting_shares
attribute of [source:src/allmydata/immutable/upload.py@4575#L155 Tahoe2PeerSelector in immutable/upload.py]. Somerge_peers
was accidentally adding servers to the sets inpreexisting_shares
. This bug was fixed in [changeset:13b5e44fbc2effd0].The current code deep-copies the
servermap
even whenmerge_peers
is not going to mutate it. This isn't strictly necessary, but returning a result in which the sets are aliased would be correct only because the call sites ofmerge_peers
in source:src/allmydata/immutable/upload.py have finished looking at the result before modifying any of those sets. Relying on that would be fragile under maintenance.