-
Notifications
You must be signed in to change notification settings - Fork 2.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fix(linking): Dont integrity check build artifacts #5470
fix(linking): Dont integrity check build artifacts #5470
Conversation
If an install script modified a file that existed in the package, then yarn would view the file as stale on every command, mark the package as "fresh", and re-run the install scripts. With this change, known build artifacts from the integrity file are checked, and if this package file is a build atrifact, it's size and timestamp are not checked. yarnpkg#932
|
I think it is consistent with Yarn's promises. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for finding this, @rally25rs.
Lots of people will be happy
Are test failures related? |
The CircleCI test is related. I'll take a look at that. 🤔 |
@bestander @arcanis I've added some logging and verified that in the case of the failing test, it's actually taking the correct code branch for my fix. It seems to be failing for some reason that I haven't discovered yet. However the weird thing is that it consistently fails only on CircleCI Linux Node 8. All other environments pass. 😕 Do you know what linux type edit: I reproduced this in Ubuntu 16. It's wild... On Ubuntu it passes in Node v6 and fails in Node v8.10. On OSX it passes in Node 6 and 8. I guess I'll continue digging into this as I have time... |
@rally25rs - I think the problem stems from here: Lines 152 to 164 in 5ef113f
The So it is not the copying, it is the postinstall script running and modifying it. |
Hmm, interesting. Then why the difference between node versions? |
@BYK so get this... on linux with node 8, when files are copied from the cache to node_modules, the timestamps are updated. yarn decides the files are different and marks the reference fresh. Look at the stat timestamps on the copy in node_modules... all 3 timestamps are changed to the Change Time. This is the same commands run in node 6: This is going to cause a lot of churn in yarn on linux with node 8. It's going to re-copy every file every time. 😿 Edit: Oh, this is because Node v8.5 introduced I'll explore this option... |
sigh it looks like calling
how were we getting around this lack of |
🎉 tests pass. The one failing on CircleCI node6 is a random timeout and not related. It passed in a couple of the previous runs. It seems that on OSX and Linux, you can open a file with 'read' access and still edit the timestamps with |
I tried this and it caused a significant performance drop due to the extra file I/O. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm concerned about the performance impact with the additional file I/O here. Can you measure before and after on Node 8?
Would love to figure out a different way to do this.
Seems like this is a bug in Node itself and explains why I haven't seen the issue on Windows: nodejs/node#15793 (comment) (and you haven't seen it on Windows and OS X). Maybe we can limit the extra operation to Linux only which is the fastest anyway so the performance hit won't be too much of an issue? Alternatively, we may look at the creation time instead of modification time which seems to be consistent across platforms? (need to verify this last part before acting) |
I had a thought to just copy 1 file, and after that first copy check the timestamps. If they changed, set a flag to do the however, this is still broken is some weird way. Somehow when copying files to the cache, after the copy completes the file still doesn't exist. This seems to happen if you have a
This error comes from trying to Eventually I think we need to just stop using timestamps as part of the check, but I'm not sure what else to do. Getting a checksum or hash for every file is probably much slower. |
@BYK I did a bunch more work on this and fixed it up a bit. There are a lot of edge cases (read my code comments) so the code looks ugly, but... On the first file copy, check the timestamp to see if it's wrong. If it is already corect, then disable the calls to I discovered that my error above was because I was opening the file in write/append mode to call futimes, because you have to on Windows. However if the file is read-only (as is the case with .git objects files) then that fails. On OSX you can call futimes on a file that is open in read-mode, so I try to re-open the file in read mode. Performance hit... quantifiable but not bad. The first run (
Performance on Ubuntu with node 8, where
so noticeably slower, but subsequent adds won't re-copy files...
So you take a perf hit on initial install to save time on later adds. whatcha think? 👍 / 👎 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Makes sense. Left a few comments here and there but the PR looks good.
src/util/fs.js
Outdated
if (err) { | ||
reject(err); | ||
} else { | ||
fixTimes(0, dest, data).then(() => resolve(err)).catch(ex => reject(ex)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is the err
in resolve(err)
is a mistake?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe. I just preserved what the original code did:
new Promise((resolve, reject) => fs.copyFile(src, dest, flags, err => (err ? reject(err) : resolve(err))))
The code I was replacing would resolve(err)
too. I didn't want to make the assumption that it wasn't intentional. 🤷
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's ... weird. I think we can just fix this and check that everything is still green.
src/util/fs.js
Outdated
if (err) { | ||
reject(err); | ||
} else { | ||
fixTimes(0, dest, data).then(() => resolve(err)).catch(ex => reject(ex)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure I understand why the first parameter is 0
. The fd 0
is stdin, right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It was 0
just because it's falsy
. In the case of not using copyFile
we already have a file open that we can use, instead of having to re-open the file. If the fd
is falsey, then fixTimes
opens the file. I was going to pass undefined
but then Flow gets mad. I'll see if I can make it more understandable...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you should be able to use null
, then in the declaration of fixTime
just use fd: ?number
(instead of : number
). Flow should then accept === null
without complaining.
src/util/fs.js
Outdated
// On OSX you can open with read permissions and still call `fs.futimes`. | ||
// We first try to open the file in write mode because that works across all OSs. | ||
// However if the file is read-only (not even the owner has write perms) then that will fail. | ||
// We can try to reopen in read mode, which will work on OSX. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems a bit convoluted. Would it make sense to use a third-party package for this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is there a 3rd party package that will handle this? I agree that it's ugly. Maybe I can move it to it's own package.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's keep it there for now 👍
@arcanis I did a bit of refactoring and moved all the stuff in Hopefully this at least improves the situation... |
LGTM, I'll fix the conflict monday 👍 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These changes look great and I am looking forward to this being merged.
I am a bit concerned about some of the errors being caught and silenced in fs-normalized
though, some of the errors may be important for debugging strange cases down the line. If the errors are really not important, can a comment be made where they are silenced, justifying it so the reasoning is clear?
openfd = await open(dest, 'r', data.mode); | ||
} catch (err) { | ||
// We can't even open this file for reading. | ||
return; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is catching er
and err
without logging or throwing anything here.
Issues with this may be difficult to debug.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There shouldn't be any issues if it fails. We just won't correct the timestamps (because we can't) and yarn will behave the same way it does now. The comment below tries to explain this, but maybe I didn't do a great job of explaining it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is not critical, but if this code is not working on some machine it will look like a fairly big slowdown without a good way to debug it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@rally25rs, I agree with @james-rabbit, it makes sense to put verbose logs that open -x
failed for the future when we might be debugging why install on one OS is slower than the other.
Would you add it?
} | ||
} catch (er) { | ||
// If `futimes` throws an exception, we probably have a case of a read-only file on Windows. | ||
// In this case we can just return. The incorrect timestamp will just cause that file to be recopied |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we verify that the file is read-only and throw or log this er
if that is not the case?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is no logger passed down to the fs-normalized
module, so I didn't have any easy way of logging these issues. I was trying to avoid even bigger changes by passing the reporter down another level. I also didn't want fixTimes()
to ever cause a yarn install to fail. If we can't fix the time stamps, it will effect performance a bit, but won't really break anything. It'll just work like it does currently where sometimes modules might get rebuilt every time (#932 ) but that's better than failing the install. If I can bubble up an error without failing the install, I'll try to add a line to the verbose log. Otherwise I'd rather not restructure the code any more than I already have.
@BYK could you re-review so we can unblock merging? thanks! |
@rally25rs, could you fix the merge conflict? |
@bestander yep, working on it now... |
So we see 3% perf reduction for first installs but we get a significant win on consequent installs which I think is a more common case. |
@rally25rs, this is a huge win! Thank you. |
🎉 |
I just waited four times for already-installed native modules today, so ready for this PR ! |
fixes #932
Summary
If an install script modified a file that existed in the package, then yarn would view the file as stale on every command, mark the package as "fresh", and re-run the install scripts.
With this change, known build artifacts from the integrity file are checked, and if this package file is a
build artifact, it's size and timestamp are not checked.
In addition it was discovered that on Linux with node 8's
fs.copyFile()
timestamps are not preserved (they are on Win and OSX). This would cause yarn to always re-copy all files for all packages when doing ayarn add
oryarn remove
. Yarn will now detect this by checking the first file copy's timestamps, and if the first file is wrong then timestamps will be corrected usingfs.futimes()
for all file copies.Test plan
Added test in
__tests__/commands/add.js