-
Notifications
You must be signed in to change notification settings - Fork 30.6k
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
repl: History file locking and other improvements #7005
Changes from 1 commit
30e2048
be19adf
ffc9cb2
a74ca07
f5302b4
3741889
45d7504
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -127,7 +127,8 @@ function setupHistory(repl, historyPath, oldHistoryPath, ready) { | |
} | ||
|
||
if (data) { | ||
repl.history = data.split(/[\n\r]+/, repl.historySize); | ||
repl.history = data.trim().split(/[\n\r]+/) | ||
.reverse().slice(0, repl.historySize); | ||
} else if (oldHistoryPath === historyPath) { | ||
// If pre-v3.0, the user had set NODE_REPL_HISTORY_FILE to | ||
// ~/.node_repl_history, warn the user about it and proceed. | ||
|
@@ -163,7 +164,7 @@ function setupHistory(repl, historyPath, oldHistoryPath, ready) { | |
} | ||
} | ||
|
||
fs.open(historyPath, 'w', onhandle); | ||
fs.open(historyPath, 'a', onhandle); | ||
} | ||
|
||
function onhandle(err, hnd) { | ||
|
@@ -172,13 +173,8 @@ function setupHistory(repl, historyPath, oldHistoryPath, ready) { | |
} | ||
repl._historyHandle = hnd; | ||
repl.on('line', online); | ||
|
||
// reading the file data out erases it | ||
repl.once('flushHistory', function() { | ||
repl.resume(); | ||
ready(null, repl); | ||
}); | ||
flushHistory(); | ||
repl.resume(); | ||
ready(null, repl); | ||
} | ||
|
||
// ------ history listeners ------ | ||
|
@@ -192,31 +188,49 @@ function setupHistory(repl, historyPath, oldHistoryPath, ready) { | |
timer = setTimeout(flushHistory, kDebounceHistoryMS); | ||
} | ||
|
||
const historyLockFile = `${historyPath}.LCK`; | ||
function flushHistory() { | ||
timer = null; | ||
if (writing) { | ||
pending = true; | ||
return; | ||
} | ||
writing = true; | ||
const historyData = repl.history.join(os.EOL); | ||
fs.write(repl._historyHandle, historyData, 0, 'utf8', onwritten); | ||
} | ||
|
||
function onwritten(err, data) { | ||
writing = false; | ||
if (pending) { | ||
pending = false; | ||
online(); | ||
} else { | ||
repl._flushing = Boolean(timer); | ||
if (!repl._flushing) { | ||
repl.emit('flushHistory'); | ||
// Ref: https://github.com/nodejs/node/issues/1634 | ||
// opening a file for exclusive write should be atomic | ||
// on non-networked POSIX systems. | ||
fs.open(historyLockFile, 'wx', (err, fd) => { | ||
if (err && err.code === 'EEXIST') { | ||
pending = true; | ||
repl.resume(); | ||
return; | ||
} | ||
} | ||
writing = true; | ||
const lastLine = repl.history[0] + '\n'; | ||
fs.write(repl._historyHandle, lastLine, NaN, 'utf8', onwritten(fd)); | ||
}); | ||
} | ||
} | ||
|
||
function onwritten(fd) { | ||
return (err, data) => { | ||
writing = false; | ||
if (pending) { | ||
pending = false; | ||
online(); | ||
} else { | ||
repl._flushing = Boolean(timer); | ||
if (!repl._flushing) { | ||
repl.emit('flushHistory'); | ||
} | ||
} | ||
unlockHistoryFile(fd); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Figured out where my (EDIT: Which may also be a good reason to do everything inside of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. OK - I will add a fix and a test for this. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm - I can only reproduce this when there is already a stray I can add a check for the existence of the
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Well, yeah, this is how I can reproduce it: $ ./node
> .exit
$ ./node
> .exit
[hangs] The first one here leaves the stray lock file lying around. This is just an idea, but how about this approach when saving the history:
That seems to be free of race conditions and does not require a lock file that could keep lying around if the process crashes (for whatever reason, it doesn’t have to be REPL-related)… what do you think? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. And btw, this is how bash/readline does it:
This approach obviously racy but apparently that’s sufficiently irrelevant to just not care. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @addaleax I've been playing with an implementation of your algorithm. It works, but I'm concerned about removing/recreating the history file. We would want to recreate it with the same file modes as the original. If I use Edit: It does work on OSX. Not sure about Windows - specifically the hidden bit. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @lance Valid point, and I don’t know if it works either (but I’d actually guess that it does)… if you have the code for that lying around somewhere, I can try and test it manually. |
||
}; | ||
} | ||
|
||
function unlockHistoryFile(fd) { | ||
fs.close(fd); | ||
fs.unlink(historyLockFile); | ||
} | ||
} | ||
|
||
function _replHistoryMessage() { | ||
if (this.history.length === 0) { | ||
|
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.
Does this PR change the ordering in the history file? if so, it is probably
semver-major
.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.
Umm… I think the intent of
repl.historySize
is to limit the file size, not the length of the history that’s being used?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's provided by readline to limit the history in memory. But we use it for both.
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.
@Fishrock123 Thanks for the clarification… It still seems to me (both from looking at the code and from experimenting) the file grows without bounds with this PR in its current state.
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.
@addaleax @Fishrock123 historically,
historySize
has limited both the file size, and the maximum number of lines loaded into memory at start up. But this almost seems like a side effect ofreadline
usage, and is not documented anywhere that I could find in the context of the REPL.This PR changes it so that so that the number of lines stored in memory from the history file at start up is still limited, but the history file size itself is unbounded.
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 any particular advantage to limiting the number of lines stored in memory if they are all going to be loaded (and saved?) anyway?