Zsh Mailing List Archive
Messages sorted by: Reverse Date, Date, Thread, Author

Issues with fcntl() history file locking



Hi,

I've been using zsh with share_history for many years and never had any
real issues on several networks where my home directory is mounted over
NFS.  Recently, it's been giving me trouble, maybe when I bumped up my
history file size to 10k entries.

Terminal 1 on host1:                 Terminal 2 on host 2:
host1% echo 1 2 3<ENTER>             host2%
1 2 3
host1%
                                     <ENTER>
                                     host2% <UP>
                                     host2% unrelated command

I'd expect that on pressing UP on host2, my last host1 command would
show up.  It does most of the times, but not reliably enough to make me
completely happy :-)

I then discovered hist_fcntl_lock, which I had not ever set, and turned
it on.  It didn't improve anything.

After a bit of stracing, and subsequent reading of the code, I found
out that the the history file is opened and closed many times during
the history file manipulation, while the lock is maintained on one of
the open descriptors.
Unfortunately, POSIX states that the fcntl() lock will be released upon
the closing the first descriptor to the file.  Quoth 'man -s 3p fcntl':

   All locks associated with a file for a given process
   shall be removed when a file descriptor for that file is closed by
   that process or the process holding that file descriptor terminates.

The key word here is "a", in "a file descriptor ... is closed".
Don't you love standardese?

If you look at Src/hist.c, you'll see that locks are sprinkled
everywhere and both readhistfile() and writehistfile() open the history
file and are cross-recursive.
We can totally end up in a case where:
 * flockhistfile opens and puts a write lock on the history file.
 * writehistfile opens a new fd to the same file
 * history needs to be merged/trimmed or whatever else leading to a
   recursive call to...
 * readhistfile, which opens another fd to the same file, and closes
   it, at which point the lock is lost.
 * writehistfile writes the history file without lock
 * ...

Now I'm not sure if that's what's causing my mysterious shared history
lapses, but fixing that problem shouldn't hurt.

After contemplating Src/hist.c for a bit, it won't be a trivial fix.
I see two ways:  the right and hard way, and the easy messy way.

The right and hard way is to have the various calls to open() the
history file to actually use the flock_fd lock file descriptor (and not
close it when done with it, leaving that to unlockhistfile()).
I think we can open the descriptor in flockhistfile() with O_APPEND
since I haven't spotted any location where we do not write at the end
of the file.  O_APPEND can't hurt if we don't write in the middle of
the file.
That leaves the issue of truncating the file when needed.  We cannot
open(...O_TRUNC...) for the same reason:  we will need ftruncate(),
which we've avoided all these years :-) and probably an autoconf
feature test.
We may also have to keep track (or reset) the seek pointer depending on
the sequencing of the calls, I haven't fully investigated the need for
that.
The whole thing will certainly not improve the clarity of the code :-/

The easy messy way is to keep track of all the open descriptors to the
history file in a global variable, and delaying the actual close until
unlockhistfile() is called.
While it's conceptually fugly, it's arguably easier to maintain and
understand.

Any opinions?

Phil.

===
Full history options and variables
% echo $ZSH_VERSION 
   5.6.2
% setopt | grep hist
   noappendhistory       off
   nobanghist            on
   cshjunkiehistory      off
   extendedhistory       on
   histallowclobber      off
   nohistbeep            off
   histexpiredupsfirst   on
   histfcntllock         on
   histfindnodups        off
   histignorealldups     off
   histignoredups        on
   histignorespace       off
   histlexwords          off
   histnofunctions       off
   histnostore           off
   histreduceblanks      on
   nohistsavebycopy      off
   histsavenodups        off
   histsubstpattern      off
   histverify            off
   incappendhistory      off
   incappendhistorytime  off
   sharehistory          on
% set | grep -i hist
   HISTCHARS='!^#'
   HISTCMD=10264
   HISTFILE=/home/phil/.zhistory
   HISTSIZE=40960
   SAVEHIST=10240




Messages sorted by: Reverse Date, Date, Thread, Author