threads / discuss / 5059

Fix double "close()" in ce_compare_data

Subject: Fix double "close()" in ce_compare_data

## tl;dr

8 messages between Jul 31, 2006 and Aug 8, 2006.

replies: 7people: 3as markdown or json

Linus Torvalds· Jul 31, 2006, 16:55 UTC · lore

Doing an "strace" on "git diff" shows that we close() a file descriptor twice (getting EBADFD on the second one) when we end up in ce_compare_data if the index does not match the checked-out stat information.

The "index_fd()" function will already have closed the fd for us, so we should not close it again.

Signed-off-by: Linus Torvalds <torvalds@osdl.org>
---

The way I found this also showed a potential performance problem: if you do a "git reset --hard" (or similar) after you have changes in your tree, it will write the index file with the same timestamp as the checked out files that it re-wrote.

That will also then forever afterwards (well, until the next "git update-index --refresh") cause the "uncommon" timestamp case in ce_match_stat(), where we check the index-file timestamp against the timestamp of the stat data, to trigger.

Not very good. The "ce_modified_check_fs()" tests can be quite expensive if you have lots of those files because we end up then calling the "ce_compare_data()" function a lot. And suddenly "git diff" doesn't take a tenth of a second any more.

We should really try to have some way to re-generate the index automatically when this case triggers, so that we only need to do it _once_ rather than keep doing it forever while the index is "potentially stale".

Any ideas?
diff --git a/read-cache.c b/read-cache.c
index c0b0313..f92cdaa 100644
--- a/read-cache.c
+++ b/read-cache.c
@@ -61,7 +61,7 @@ static int ce_compare_data(struct cache_
 		unsigned char sha1[20];
 		if (!index_fd(sha1, fd, st, 0, NULL))
 			match = memcmp(sha1, ce->sha1, 20);
-		close(fd);
+		/* index_fd() closed the file descriptor already */
 	}
 	return match;
 }
Junio C Hamano· Jul 31, 2006, 19:05 UTC · re: Linus Torvalds · lore

Re: Fix double "close()" in ce_compare_data

Linus Torvalds <torvalds@osdl.org> writes:
Show 11 quoted lines
> The way I found this also showed a potential performance problem: if you 
> do a "git reset --hard" (or similar) after you have changes in your tree, 
> it will write the index file with the same timestamp as the checked out 
> files that it re-wrote.
>...
> We should really try to have some way to re-generate the index 
> automatically when this case triggers, so that we only need to do it 
> _once_ rather than keep doing it forever while the index is "potentially 
> stale".
>
> Any ideas?

Just before writing the index out, we could do a gettimeofday() and wait for 1 second (or two if you are on FAT) if we have many many paths that have the same timestamp. Ugly and would not work on NFS I suspect.

We could do gratuitous "update-index --refresh" but that is uglier especially if done at the core level; the caller does not expect the index file to be updated.

Or we could remove the check and tell the user not to touch the working tree after calling update-index (I am not seriously suggesting this).

Junio C Hamano· Aug 5, 2006, 11:20 UTC · re: Linus Torvalds · lore

[PATCH] Racy git: avoid having to be always too careful

Immediately after a bulk checkout, most of the paths in the working tree would have the same timestamp as the index file, and this would force ce_match_stat() to take slow path for all of them. When writing an index file out, if many of the paths have very new (read: the same timestamp as the index file being written out) timestamp, we are better off delaying the return from the command, to make sure that later command to touch the working tree files will leave newer timestamps than recorded in the index, thereby avoiding to take the slow path.

Signed-off-by: Junio C Hamano <junkio@cox.net>
---
Linus Torvalds <torvalds@osdl.org> writes:
Show 31 quoted lines
> Doing an "strace" on "git diff" shows that we close() a file descriptor 
> twice (getting EBADFD on the second one) when we end up in ce_compare_data 
> if the index does not match the checked-out stat information.
>
> The "index_fd()" function will already have closed the fd for us, so we 
> should not close it again.
>
> Signed-off-by: Linus Torvalds <torvalds@osdl.org>
> ---
>
> The way I found this also showed a potential performance problem: if you 
> do a "git reset --hard" (or similar) after you have changes in your tree, 
> it will write the index file with the same timestamp as the checked out 
> files that it re-wrote.
>
> That will also then forever afterwards (well, until the next "git 
> update-index --refresh") cause the "uncommon" timestamp case in 
> ce_match_stat(), where we check the index-file timestamp against the 
> timestamp of the stat data, to trigger.
>
> Not very good. The "ce_modified_check_fs()" tests can be quite expensive 
> if you have lots of those files because we end up then calling the 
> "ce_compare_data()" function a lot. And suddenly "git diff" doesn't take a 
> tenth of a second any more.
>
> We should really try to have some way to re-generate the index 
> automatically when this case triggers, so that we only need to do it 
> _once_ rather than keep doing it forever while the index is "potentially 
> stale".
>
> Any ideas?

Here is what I came up with. I am not very happy with its magic, but should be better than nothing.

 read-cache.c |   44 ++++++++++++++++++++++++++++++++++++++++----
 1 files changed, 40 insertions(+), 4 deletions(-)
diff --git a/read-cache.c b/read-cache.c
index f92cdaa..ce76c20 100644
--- a/read-cache.c
+++ b/read-cache.c
@@ -5,6 +5,7 @@
  */
 #include "cache.h"
 #include "cache-tree.h"
+#include <time.h>
 
 /* Index extensions.
  *
@@ -923,7 +924,7 @@ static void ce_smudge_racily_clean_entry
 		 * $ echo filfre >nitfol
 		 * $ git-update-index --add nitfol
 		 *
-		 * but it does not.  Whe the second update-index runs,
+		 * but it does not.  When the second update-index runs,
 		 * it notices that the entry "frotz" has the same timestamp
 		 * as index, and if we were to smudge it by resetting its
 		 * size to zero here, then the object name recorded
@@ -945,7 +946,9 @@ int write_cache(int newfd, struct cache_
 {
 	SHA_CTX c;
 	struct cache_header hdr;
-	int i, removed;
+	int i, removed, recent;
+	struct stat st;
+	time_t now;
 
 	for (i = removed = 0; i < entries; i++)
 		if (!cache[i]->ce_mode)
@@ -959,15 +962,19 @@ int write_cache(int newfd, struct cache_
 	if (ce_write(&c, newfd, &hdr, sizeof(hdr)) < 0)
 		return -1;
 
+	now = fstat(newfd, &st) ? 0 : st.st_mtime;
+	recent = 0;
 	for (i = 0; i < entries; i++) {
 		struct cache_entry *ce = cache[i];
+		time_t entry_time = (time_t) ntohl(ce->ce_mtime.sec);
 		if (!ce->ce_mode)
 			continue;
-		if (index_file_timestamp &&
-		    index_file_timestamp <= ntohl(ce->ce_mtime.sec))
+		if (index_file_timestamp && index_file_timestamp <= entry_time)
 			ce_smudge_racily_clean_entry(ce);
 		if (ce_write(&c, newfd, ce, ce_size(ce)) < 0)
 			return -1;
+		if (now && now <= entry_time)
+			recent++;
 	}
 
 	/* Write extension data here */
@@ -983,5 +990,34 @@ int write_cache(int newfd, struct cache_
 			return -1;
 		}
 	}
+
+	/*
+	 * To prevent later ce_match_stat() from always falling into
+	 * check_fs(), if we have too many entries that can trigger
+	 * racily clean check, we are better off delaying the return.
+	 * We arbitrarily say if more than 20 paths or 25% of total
+	 * paths are very new, we delay the return until the index
+	 * file gets a new timestamp.
+	 *
+	 * NOTE! NOTE! NOTE!
+	 *
+	 * This assumes that nobody is touching the working tree while
+	 * we are updating the index.
+	 */
+	if (20 < recent || entries <= recent * 4) {
+		now = fstat(newfd, &st) ? 0 : st.st_mtime;
+		while (now && !fstat(newfd, &st) && st.st_mtime <= now) {
+			struct timespec rq, rm;
+			off_t where = lseek(newfd, 0, SEEK_CUR);
+			rq.tv_sec = 0;
+			rq.tv_nsec = 250000000;
+			nanosleep(&rq, &rm);
+			if ((where == (off_t) -1) ||
+			    (write(newfd, "", 1) != 1) ||
+			    (lseek(newfd, -1, SEEK_CUR) != where) ||
+			    ftruncate(newfd, where))
+				break;
+		}
+	}
 	return ce_flush(&c, newfd);
 }
-- 
1.4.2.rc3.g19a8a
Johannes Schindelin· Aug 8, 2006, 15:43 UTC · re: Junio C Hamano · lore

Re: [PATCH] Racy git: avoid having to be always too careful

Hi,
On Sat, 5 Aug 2006, Junio C Hamano wrote:
Show 9 quoted lines
> Immediately after a bulk checkout, most of the paths in the
> working tree would have the same timestamp as the index file,
> and this would force ce_match_stat() to take slow path for all
> of them.  When writing an index file out, if many of the paths
> have very new (read: the same timestamp as the index file being
> written out) timestamp, we are better off delaying the return
> from the command, to make sure that later command to touch the
> working tree files will leave newer timestamps than recorded in
> the index, thereby avoiding to take the slow path.

This makes "make test" dog slow. How about making this overrideable? (It is just a guess, but I _think_ that the sleeping is worse than having to check the files with the same time stamp again and again -- a "git status" will help that).

"make test" without this patch:
	real    9m59.314s
	user    0m36.580s
	sys     0m39.290s
"make test" with this patch:
	real    1m36.429s
	user    0m37.410s
	sys     0m40.460s

-- 8< -- [PATCH] read-cache: optionally disable being cautious with racy caches

By setting the environment variable GIT_RISK_RACY_CACHE or the config variable core.riskRacyCache, the sleeping (to avoid a racy cache) is disabled. This is also used for "make test", drastically reducing the needed time.

Signed-off-by: Johannes Schindelin <Johannes.Schindelin@gmx.de>
---
 Documentation/config.txt |    4 ++++
 Documentation/git.txt    |    5 +++++
 cache.h                  |    1 +
 config.c                 |    4 ++++
 environment.c            |    1 +
 read-cache.c             |    3 ++-
 t/test-lib.sh            |    2 ++
 7 files changed, 19 insertions(+), 1 deletions(-)
diff --git a/Documentation/config.txt b/Documentation/config.txt
index d89916b..b61fd91 100644
--- a/Documentation/config.txt
+++ b/Documentation/config.txt
@@ -103,6 +103,10 @@ core.legacyheaders::
 	database directly (where the "http://" and "rsync://" protocols
 	count as direct access).
 
+core.riskRacyCache::
+	A boolean which allows to disable the 1-second sleeping to avoid
+	a racy cache.
+
 alias.*::
 	Command aliases for the gitlink:git[1] command wrapper - e.g.
 	after defining "alias.last = cat-file commit HEAD", the invocation
diff --git a/Documentation/git.txt b/Documentation/git.txt
index ba525d3..ff464e8 100644
--- a/Documentation/git.txt
+++ b/Documentation/git.txt
@@ -649,6 +649,11 @@ other
 	stderr telling about alias expansion, built-in command
 	execution and external command execution.
 
+'GIT_RISK_RACY_CACHE'::
+	If this variable is set, gitlink:git-read-cache[1] will never
+	sleep to avoid a racy cache by making the timestamps of the index
+	file newer than the files referenced in the index.
+
 Discussion[[Discussion]]
 ------------------------
 include::README[]
diff --git a/cache.h b/cache.h
index 19fdef6..e8bbe6d 100644
--- a/cache.h
+++ b/cache.h
@@ -174,6 +174,7 @@ #define REFRESH_UNMERGED	0x0002	/* allow
 #define REFRESH_QUIET		0x0004	/* be quiet about it */
 #define REFRESH_IGNORE_MISSING	0x0008	/* ignore non-existent */
 extern int refresh_cache(unsigned int flags);
+extern int risk_racy_cache;
 
 struct lock_file {
 	struct lock_file *next;
diff --git a/config.c b/config.c
index c6e6f6a..4ab9329 100644
--- a/config.c
+++ b/config.c
@@ -291,6 +291,10 @@ int git_default_config(const char *var, 
 		return 0;
 	}
 
+	if (!strcmp(var, "core.riskracycache")) {
+		risk_racy_cache = git_config_bool(var, value);
+	}
+
 	if (!strcmp(var, "user.name")) {
 		strlcpy(git_default_name, value, sizeof(git_default_name));
 		return 0;
diff --git a/environment.c b/environment.c
index 1ce3411..a481fc4 100644
--- a/environment.c
+++ b/environment.c
@@ -24,6 +24,7 @@ const char *apply_default_whitespace = N
 int zlib_compression_level = Z_DEFAULT_COMPRESSION;
 int pager_in_use;
 int pager_use_color = 1;
+int risk_racy_cache = 0;
 
 static int dyn_git_object_dir, dyn_git_index_file, dyn_git_graft_file;
 static char *git_dir, *git_object_dir, *git_index_file, *git_refs_dir,
diff --git a/read-cache.c b/read-cache.c
index d64b503..f20dac5 100644
--- a/read-cache.c
+++ b/read-cache.c
@@ -1021,7 +1021,8 @@ int write_cache(int newfd, struct cache_
 	 * This assumes that nobody is touching the working tree while
 	 * we are updating the index.
 	 */
-	if (20 < recent || entries <= recent * 4) {
+	if (!risk_racy_cache && !getenv("GIT_RISK_RACY_CACHE") &&
+			(20 < recent || entries <= recent * 4)) {
 		now = fstat(newfd, &st) ? 0 : st.st_mtime;
 		while (now && !fstat(newfd, &st) && st.st_mtime <= now) {
 			struct timespec rq, rm;
diff --git a/t/test-lib.sh b/t/test-lib.sh
index b6d119a..3b6882d 100755
--- a/t/test-lib.sh
+++ b/t/test-lib.sh
@@ -197,6 +197,8 @@ # t/ subdirectory and are run in trash s
 PATH=$(pwd)/..:$PATH
 GIT_EXEC_PATH=$(pwd)/..
 export PATH GIT_EXEC_PATH
+GIT_RISK_RACY_CACHE=1
+export GIT_RISK_RACY_CACHE
 
 # Similarly use ../compat/subprocess.py if our python does not
 # have subprocess.py on its own.
-- 
1.4.2.rc3.g6b27
Junio C Hamano· Aug 8, 2006, 17:25 UTC · re: Johannes Schindelin · lore

Re: [PATCH] Racy git: avoid having to be always too careful

Johannes Schindelin <Johannes.Schindelin@gmx.de> writes:
Show 12 quoted lines
> This makes "make test" dog slow. How about making this overrideable?
> (It is just a guess, but I _think_ that the sleeping is worse than having 
> to check the files with the same time stamp again and again -- a "git 
> status" will help that).
>
>
> -- 8< --
> [PATCH] read-cache: optionally disable being cautious with racy caches
>
> By setting the environment variable GIT_RISK_RACY_CACHE or the config
> variable core.riskRacyCache, the sleeping (to avoid a racy cache) is
> disabled.

"RISK" is a misnomer here -- the original code without patch is being cautious and taking a runtime hit but not risky. The "delay while writing the cache out for the first time" is trying to avoid the runtime hit by taking a hit at cache generation time. In either case you never risk racy cache.

Perhaps throwing away the whole "delay" thing might be simpler and more worthwhile.

Another possibility is to tweak the heuristics -- currently we say 20 paths or 1/4 of whole paths is too many and would cause too much runtime hit but that was done without any measurement. We could raise the threashold which would solve the case for the testsuite whose trees are all small.

Johannes Schindelin· Aug 8, 2006, 22:30 UTC · re: Junio C Hamano · lore

Re: [PATCH] Racy git: avoid having to be always too careful

Hi,
On Tue, 8 Aug 2006, Junio C Hamano wrote:
Show 5 quoted lines
> Another possibility is to tweak the heuristics -- currently we
> say 20 paths or 1/4 of whole paths is too many and would cause
> too much runtime hit but that was done without any measurement.
> We could raise the threashold which would solve the case for the
> testsuite whose trees are all small.
Okay, how about 100 paths? And make _this_ a config variable?

Ciao, Dscho

Junio C Hamano· Aug 8, 2006, 23:07 UTC · re: Johannes Schindelin · lore

Re: [PATCH] Racy git: avoid having to be always too careful

Johannes Schindelin <Johannes.Schindelin@gmx.de> writes:
> Okay, how about 100 paths? And make _this_ a config variable?

Actually, I have a better patch I'll be pushing out in "next" this evening.

Hint: ce_write() batches and buffers things, so "now" obtained
in the current code is unreliable.
Johannes Schindelin· Aug 8, 2006, 23:44 UTC · re: Junio C Hamano · lore

Re: [PATCH] Racy git: avoid having to be always too careful

Hi,
On Tue, 8 Aug 2006, Junio C Hamano wrote:
Show 6 quoted lines
> Johannes Schindelin <Johannes.Schindelin@gmx.de> writes:
> 
> > Okay, how about 100 paths? And make _this_ a config variable?
> 
> Actually, I have a better patch I'll be pushing out in "next"
> this evening.
Pity. I just whipped up this patch...

-- 8< -- [PATCH] read-cache: introduce core.racyThreshold

The expensive sleep is done only when there are more than 100 updated files now. This number is overrideable by the core.racyThreshold variable.

Signed-off-by: Johannes Schindelin <Johannes.Schindelin@gmx.de>
---
 Documentation/config.txt |    5 +++++
 cache.h                  |    1 +
 config.c                 |    5 +++++
 environment.c            |    1 +
 read-cache.c             |    2 +-
 5 files changed, 13 insertions(+), 1 deletions(-)
diff --git a/Documentation/config.txt b/Documentation/config.txt
index d89916b..f428fb5 100644
--- a/Documentation/config.txt
+++ b/Documentation/config.txt
@@ -103,6 +103,11 @@ core.legacyheaders::
 	database directly (where the "http://" and "rsync://" protocols
 	count as direct access).
 
+core.racyThreshold::
+	The number of files which need to be updated at least, before
+	read-cache sleeps to make the timestamps of these files and the
+	timestamp of the index file distinct.
+
 alias.*::
 	Command aliases for the gitlink:git[1] command wrapper - e.g.
 	after defining "alias.last = cat-file commit HEAD", the invocation
diff --git a/cache.h b/cache.h
index 19fdef6..8c0a830 100644
--- a/cache.h
+++ b/cache.h
@@ -193,6 +193,7 @@ extern int warn_ambiguous_refs;
 extern int shared_repository;
 extern const char *apply_default_whitespace;
 extern int zlib_compression_level;
+extern int racy_threshold;
 
 #define GIT_REPO_VERSION 0
 extern int repository_format_version;
diff --git a/config.c b/config.c
index c6e6f6a..01b8c23 100644
--- a/config.c
+++ b/config.c
@@ -291,6 +291,11 @@ int git_default_config(const char *var, 
 		return 0;
 	}
 
+	if (!strcmp(var, "core.racythreshold")) {
+		racy_threshold = git_config_int(var, value);
+		return 0;
+	}
+
 	if (!strcmp(var, "user.name")) {
 		strlcpy(git_default_name, value, sizeof(git_default_name));
 		return 0;
diff --git a/environment.c b/environment.c
index 1ce3411..3656f36 100644
--- a/environment.c
+++ b/environment.c
@@ -24,6 +24,7 @@ const char *apply_default_whitespace = N
 int zlib_compression_level = Z_DEFAULT_COMPRESSION;
 int pager_in_use;
 int pager_use_color = 1;
+int racy_threshold = 100;
 
 static int dyn_git_object_dir, dyn_git_index_file, dyn_git_graft_file;
 static char *git_dir, *git_object_dir, *git_index_file, *git_refs_dir,
diff --git a/read-cache.c b/read-cache.c
index d64b503..42a3f78 100644
--- a/read-cache.c
+++ b/read-cache.c
@@ -1021,7 +1021,7 @@ int write_cache(int newfd, struct cache_
 	 * This assumes that nobody is touching the working tree while
 	 * we are updating the index.
 	 */
-	if (20 < recent || entries <= recent * 4) {
+	if (racy_threshold < recent) {
 		now = fstat(newfd, &st) ? 0 : st.st_mtime;
 		while (now && !fstat(newfd, &st) && st.st_mtime <= now) {
 			struct timespec rq, rm;
-- 
1.4.2.rc3.gfa72-dirty

← back to recent threads