Merge branch 'jt/odb-keep-lockfile-fix'

The creation of '.keep' lockfiles during object database
transactions has been deferred to commit time, explicitly managing
the lockfiles within the files backend rather than relying on
'git-index-pack', to prevent race conditions that could lead to
concurrent lockfiles being erroneously deleted.

* jt/odb-keep-lockfile-fix:
  object-file: write ".keep" lockfiles during transaction commit
  object-file: explicitly write ".keep" lockfile
  pack-write: lift and expose index-pack report parsing
  pack-write: make index-pack output comment hash agnostic
This commit is contained in:
Junio C Hamano committed 2026-11-01 09:25:22 -08:00
commit cc3b09a046
4 files changed
+186 -49

No files matched your search

+100 -27
View File
@@ -491,6 +491,7 @@ struct odb_transaction_files {
struct transaction_packfile packfile;
const char *prefix;
struct strvec pack_names;
struct tempfile **pack_lockfiles;
size_t pack_lockfiles_nr;
size_t pack_lockfiles_alloc;
@@ -1253,6 +1254,82 @@ out:
return ret;
}
static int write_pack_lockfile(struct odb_source *source, const char *pack_name,
const char *keep_msg, struct tempfile **out)
{
struct strbuf lock_path = STRBUF_INIT;
struct strbuf contents = STRBUF_INIT;
int ret;
int fd;
strbuf_addf(&lock_path, "%s/pack/pack-%s.keep",
source->path, pack_name);
strbuf_addf(&contents, "%s\n", keep_msg);
fd = safe_create_file_with_leading_directories(source->odb->repo,
lock_path.buf);
if (fd < 0) {
ret = error_errno(_("unable to create '%s'"), lock_path.buf);
goto out;
}
if (write_in_full(fd, contents.buf, contents.len) < 0) {
ret = error_errno(_("unable to write '%s'"), lock_path.buf);
close(fd);
unlink(lock_path.buf);
goto out;
}
if (close(fd) < 0) {
ret = error_errno(_("unable to close '%s'"), lock_path.buf);
unlink(lock_path.buf);
goto out;
}
*out = register_tempfile(lock_path.buf);
ret = 0;
out:
strbuf_release(&lock_path);
strbuf_release(&contents);
return ret;
}
static int write_pack_lockfiles(struct odb_transaction_files *transaction)
{
struct strbuf keep_msg = STRBUF_INIT;
char hostname[HOST_NAME_MAX + 1];
int ret = 0;
if (!transaction->pack_names.nr)
return 0;
if (xgethostname(hostname, sizeof(hostname)))
xsnprintf(hostname, sizeof(hostname), "localhost");
strbuf_addf(&keep_msg, "receive-pack %"PRIuMAX" on %s",
(uintmax_t)getpid(), hostname);
for (size_t i = 0; i < transaction->pack_names.nr; i++) {
struct tempfile *lock;
ret = write_pack_lockfile(transaction->base.source,
transaction->pack_names.v[i],
keep_msg.buf, &lock);
if (ret < 0)
break;
ALLOC_GROW(transaction->pack_lockfiles,
transaction->pack_lockfiles_nr + 1,
transaction->pack_lockfiles_alloc);
transaction->pack_lockfiles[transaction->pack_lockfiles_nr++] = lock;
}
strbuf_release(&keep_msg);
return ret;
}
static int odb_transaction_files_commit(struct odb_transaction *base)
{
struct odb_transaction_files *transaction =
@@ -1281,6 +1358,9 @@ static int odb_transaction_files_commit(struct odb_transaction *base)
delete_tempfile(&temp);
strbuf_release(&temp_path);
if (write_pack_lockfiles(transaction))
return error(_("unable to lock packfiles"));
/*
* Make the object files visible in the primary ODB after their data is
* fully durable.
@@ -1345,9 +1425,11 @@ static int odb_transaction_files_write_pack(struct odb_transaction *base,
container_of(base, struct odb_transaction_files, base);
struct repository *repo = base->source->odb->repo;
struct child_process child = CHILD_PROCESS_INIT;
struct strbuf pack_name = STRBUF_INIT;
struct pack_header hdr;
const char *hdr_err;
int err_fd = opts->err_fd;
int ret = -1;
int status;
hdr_err = parse_pack_header(&hdr, pack_fd);
@@ -1386,19 +1468,9 @@ static int odb_transaction_files_write_pack(struct odb_transaction *base,
return -1;
}
} else {
char hostname[HOST_NAME_MAX + 1];
char *lockfile;
strvec_pushl(&child.args, "index-pack", "--stdin", NULL);
push_header_arg(&child.args, &hdr);
if (xgethostname(hostname, sizeof(hostname)))
xsnprintf(hostname, sizeof(hostname), "localhost");
strvec_pushf(&child.args,
"--keep=receive-pack %"PRIuMAX" on %s",
(uintmax_t)getpid(),
hostname);
if (!opts->quiet && err_fd)
strvec_push(&child.args, "--show-resolving-progress");
if (err_fd)
@@ -1421,33 +1493,32 @@ static int odb_transaction_files_write_pack(struct odb_transaction *base,
return -1;
}
/*
* The lockfile filepath is expected to be the final location of
* the ".keep" file after being migrated to the main ODB source.
* This ensures the lockfile can be found and removed later
* after the ODB transaction has been committed.
*/
lockfile = index_pack_lockfile(base->source, child.out, NULL);
if (lockfile) {
ALLOC_GROW(transaction->pack_lockfiles,
transaction->pack_lockfiles_nr + 1,
transaction->pack_lockfiles_alloc);
transaction->pack_lockfiles[transaction->pack_lockfiles_nr++] =
register_tempfile(lockfile);
free(lockfile);
}
ret = read_index_pack_report(repo, child.out, &pack_name, NULL);
close(child.out);
status = finish_command(&child);
if (status) {
strbuf_addstr(err_msg, "index-pack abnormal exit");
return -1;
goto out;
}
if (ret) {
strbuf_addstr(err_msg, "index-pack failed to report pack name");
goto out;
}
strvec_push(&transaction->pack_names, pack_name.buf);
odb_source_prepare(base->source, ODB_PREPARE_FLUSH_CACHES);
}
return 0;
ret = 0;
out:
strbuf_release(&pack_name);
return ret;
}
static int odb_transaction_files_finalize(struct odb_transaction *base)
@@ -1460,6 +1531,7 @@ static int odb_transaction_files_finalize(struct odb_transaction *base)
ret |= delete_tempfile(&transaction->pack_lockfiles[i]);
free(transaction->pack_lockfiles);
strvec_clear(&transaction->pack_names);
return ret;
}
@@ -1492,6 +1564,7 @@ int odb_transaction_files_begin(struct odb_source *source,
transaction->base.write_pack = odb_transaction_files_write_pack;
transaction->base.env = odb_transaction_files_env;
transaction->flags = flags;
strvec_init(&transaction->pack_names);
transaction->prefix = "bulk-fsync";
if (flags & ODB_TRANSACTION_RECEIVE) {
+49 -19
View File
@@ -469,33 +469,63 @@ void fixup_pack_header_footer(const struct git_hash_algo *hash_algo,
fsync_component_or_die(FSYNC_COMPONENT_PACK, pack_fd, pack_name);
}
int read_index_pack_report(struct repository *repo, int ip_out,
struct strbuf *pack_name, int *is_keep)
{
const int len = repo->hash_algo->hexsz + 6;
char packname[GIT_MAX_HEXSZ + 6];
int is_keep_ignored;
const char *name;
if (!is_keep)
is_keep = &is_keep_ignored;
/*
* The first thing we expect from index-pack's output is
* "pack\t<name>\n" or "keep\t<name>\n", where <name> is the hexadecimal
* name of the packfile it has just written. The line is therefore five
* bytes of prefix, `hexsz` bytes of name, and a trailing newline. A
* "keep" prefix tells us that index-pack has also created the
* corresponding ".keep" file.
*/
if (read_in_full(ip_out, packname, len) != len || packname[len - 1] != '\n')
return -1;
packname[len - 1] = 0;
if (skip_prefix(packname, "pack\t", &name))
*is_keep = 0;
else if (skip_prefix(packname, "keep\t", &name))
*is_keep = 1;
else
return -1;
strbuf_addstr(pack_name, name);
return 0;
}
char *index_pack_lockfile(struct odb_source *source, int ip_out,
int *is_well_formed)
{
char packname[GIT_MAX_HEXSZ + 6];
const int len = source->odb->repo->hash_algo->hexsz + 6;
/*
* The first thing we expect from index-pack's output
* is "pack\t%40s\n" or "keep\t%40s\n" (46 bytes) where
* %40s is the newly created pack SHA1 name. In the "keep"
* case, we need it to remove the corresponding .keep file
* later on. If we don't get that then tough luck with it.
*/
if (read_in_full(ip_out, packname, len) == len && packname[len-1] == '\n') {
const char *name;
struct strbuf pack_name = STRBUF_INIT;
char *lockfile = NULL;
int is_keep = 0;
if (read_index_pack_report(source->odb->repo, ip_out, &pack_name, &is_keep)) {
if (is_well_formed)
*is_well_formed = 1;
packname[len-1] = 0;
if (skip_prefix(packname, "keep\t", &name))
return xstrfmt("%s/pack/pack-%s.keep",
source->path, name);
*is_well_formed = 0;
return NULL;
}
if (is_well_formed)
*is_well_formed = 0;
return NULL;
*is_well_formed = 1;
/* If there is no ".keep" file, then there is no lockfile to return. */
if (is_keep)
lockfile = xstrfmt("%s/pack/pack-%s.keep", source->path, pack_name.buf);
strbuf_release(&pack_name);
return lockfile;
}
/*
+10
View File
@@ -106,6 +106,16 @@ off_t write_pack_header(struct hashfile *f, uint32_t);
void fixup_pack_header_footer(const struct git_hash_algo *, int,
unsigned char *, const char *, uint32_t,
unsigned char *, off_t);
/*
* Read the report that git-index-pack(1) writes to its standard output after
* the packfile is written. The hexadecimal name of the packfile is appended to
* `pack_name` and, if `is_keep` is non-NULL, it is set to indicate whether a
* ".keep" file was written. Return 0 on success and a negative error code on
* failure.
*/
int read_index_pack_report(struct repository *repo, int ip_out,
struct strbuf *pack_name, int *is_keep);
char *index_pack_lockfile(struct odb_source *source, int fd,
int *is_well_formed);
+27 -3
View File
@@ -77,10 +77,9 @@ test_expect_success '.keep file is removed after push' '
git -C keep.git config set receive.unpackLimit 0 &&
# While incoming objects are still quarantined, validate that the
# ".keep" lockfile is present in the quarantine directory.
# ".keep" lockfile is not present in the quarantine directory.
test_hook -C keep.git pre-receive <<-\EOF &&
keep="$(ls "$GIT_QUARANTINE_PATH"/pack/pack-*.keep)" &&
test -f "$keep"
test ! -e "$GIT_QUARANTINE_PATH"/pack/pack-*.keep
EOF
# After quarantined objects are migrated, validate that the ".keep"
@@ -101,4 +100,29 @@ test_expect_success '.keep file is removed after push' '
test_path_is_missing "$keep"
'
test_expect_success '.keep file created outside of process is not removed' '
test_when_finished rm -rf keep.git &&
git init --bare keep.git &&
git -C keep.git config set receive.unpackLimit 0 &&
# Create a ".keep" lockfile matching the packfile to simulate a
# concurrent git-receive-pack(1) that has already written its lockfile.
test_hook -C keep.git pre-receive <<-\EOF &&
pack="$(ls "$GIT_QUARANTINE_PATH"/pack/pack-*.pack)" &&
pack="${pack##*/}" &&
echo "baz" >"objects/pack/${pack%.pack}.keep"
EOF
test_commit bar &&
test_must_fail git push keep.git HEAD &&
# The ".keep" lockfile of the concurrent git-receive-pack(1) should be
# left in its place.
keep="$(ls keep.git/objects/pack/pack-*.keep)" &&
test_path_is_file "$keep" &&
test_grep "baz" "$keep" &&
test_path_is_missing "${keep%.keep}.pack"
'
test_done