public inbox for git-commits@fedoraproject.org
help / color / mirror / Atom feed
From: Dusty Mabe <dusty@dustymabe.com>
To: git-commits@fedoraproject.org
Subject: [rpms/ostree] rawhide: Backport changes for ostree min-free-space accounting
Date: Mon, 27 Jul 2026 16:51:45 GMT [thread overview]
Message-ID: <178517110524.1.1339067946339889027.rpms-ostree-029cd0b3ef04@fedoraproject.org> (raw)
A new commit has been pushed.
Repo : rpms/ostree
Branch : rawhide
Commit : 029cd0b3ef04f81d4113ecf2fce04cd51e62b1f6
Author : Dusty Mabe <dusty@dustymabe.com>
Date : 2026-07-27T12:06:33-04:00
Stats : +385/-0 in 4 file(s)
URL : https://src.fedoraproject.org/rpms/ostree/c/029cd0b3ef04f81d4113ecf2fce04cd51e62b1f6?branch=rawhide
Log:
Backport changes for ostree min-free-space accounting
- https://github.com/ostreedev/ostree/pull/3614
- https://github.com/ostreedev/ostree/pull/3615
Assisted-By: <anthropic/claude-opus-4.6>
---
diff --git a/0001-lib-commit-Report-whether-committed-object-already-existed.patch b/0001-lib-commit-Report-whether-committed-object-already-existed.patch
new file mode 100644
index 0000000..788d8a4
--- /dev/null
+++ b/0001-lib-commit-Report-whether-committed-object-already-existed.patch
@@ -0,0 +1,152 @@
+From 974b5184cbd4b83e0423452acbb61b86d19ee160 Mon Sep 17 00:00:00 2001
+From: Jonathan Lebon <jlebon@redhat.com>
+Date: Wed, 22 Jul 2026 22:08:29 -0400
+Subject: [PATCH 1/3] lib/commit: Report whether committed object already
+ existed
+
+Add an `out_existed` output parameter to
+`_ostree_repo_commit_tmpf_final()` and `commit_loose_regfile_object()`.
+This requires switching from `GLNX_LINK_TMPFILE_NOREPLACE_IGNORE_EXIST`
+to `GLNX_LINK_TMPFILE_NOREPLACE` so we can detect `G_IO_ERROR_EXISTS`
+ourselves.
+
+No users for now. Prep for a follow-up patch using this.
+
+Assisted-by: AI
+---
+ src/libostree/ostree-repo-commit.c | 39 ++++++++++++++++++-----------
+ src/libostree/ostree-repo-private.h | 3 ++-
+ src/libostree/ostree-repo-pull.c | 4 +--
+ 3 files changed, 29 insertions(+), 17 deletions(-)
+
+diff --git a/src/libostree/ostree-repo-commit.c b/src/libostree/ostree-repo-commit.c
+index b7773e1db1..498376090a 100644
+--- a/src/libostree/ostree-repo-commit.c
++++ b/src/libostree/ostree-repo-commit.c
+@@ -181,7 +181,8 @@ ot_security_smack_reset_fd (int fd)
+ /* Given an O_TMPFILE regular file, link it into place. */
+ gboolean
+ _ostree_repo_commit_tmpf_final (OstreeRepo *self, const char *checksum, OstreeObjectType objtype,
+- GLnxTmpfile *tmpf, GCancellable *cancellable, GError **error)
++ GLnxTmpfile *tmpf, gboolean *out_existed, GCancellable *cancellable,
++ GError **error)
+ {
+ char tmpbuf[_OSTREE_LOOSE_PATH_MAX];
+ _ostree_loose_path (tmpbuf, checksum, objtype, self->mode);
+@@ -193,10 +194,19 @@ _ostree_repo_commit_tmpf_final (OstreeRepo *self, const char *checksum, OstreeOb
+ if (!_ostree_tmpf_fsverity (self, tmpf, NULL, error))
+ return FALSE;
+
+- if (!glnx_link_tmpfile_at (tmpf, GLNX_LINK_TMPFILE_NOREPLACE_IGNORE_EXIST, dest_dfd, tmpbuf,
+- error))
+- return FALSE;
+- /* We're done with the fd */
++ gboolean existed = FALSE;
++ g_autoptr (GError) local_error = NULL;
++ if (!glnx_link_tmpfile_at (tmpf, GLNX_LINK_TMPFILE_NOREPLACE, dest_dfd, tmpbuf, &local_error))
++ {
++ if (!g_error_matches (local_error, G_IO_ERROR, G_IO_ERROR_EXISTS))
++ {
++ g_propagate_error (error, g_steal_pointer (&local_error));
++ return FALSE;
++ }
++ existed = TRUE;
++ }
++ if (out_existed)
++ *out_existed = existed;
+ glnx_tmpfile_clear (tmpf);
+ return TRUE;
+ }
+@@ -237,8 +247,8 @@ commit_path_final (OstreeRepo *self, const char *checksum, OstreeObjectType objt
+ */
+ static gboolean
+ commit_loose_regfile_object (OstreeRepo *self, const char *checksum, GLnxTmpfile *tmpf, guint32 uid,
+- guint32 gid, guint32 mode, GVariant *xattrs, GCancellable *cancellable,
+- GError **error)
++ guint32 gid, guint32 mode, GVariant *xattrs, gboolean *out_existed,
++ GCancellable *cancellable, GError **error)
+ {
+ if (self->mode == OSTREE_REPO_MODE_BARE)
+ {
+@@ -308,8 +318,8 @@ commit_loose_regfile_object (OstreeRepo *self, const char *checksum, GLnxTmpfile
+ return glnx_throw_errno_prefix (error, "fsync");
+ }
+
+- if (!_ostree_repo_commit_tmpf_final (self, checksum, OSTREE_OBJECT_TYPE_FILE, tmpf, cancellable,
+- error))
++ if (!_ostree_repo_commit_tmpf_final (self, checksum, OSTREE_OBJECT_TYPE_FILE, tmpf, out_existed,
++ cancellable, error))
+ return FALSE;
+
+ return TRUE;
+@@ -569,7 +579,7 @@ _ostree_repo_bare_content_commit (OstreeRepo *self, OstreeRepoBareContent *barew
+ return FALSE;
+
+ if (!commit_loose_regfile_object (self, checksum_buf, &real->tmpf, real->uid, real->gid,
+- real->mode, real->xattrs, cancellable, error))
++ real->mode, real->xattrs, NULL, cancellable, error))
+ return FALSE;
+
+ /* Let's have a guarantee that after commit the object is cleaned up */
+@@ -1168,7 +1178,7 @@ write_content_object (OstreeRepo *self, const char *expected_checksum, GInputStr
+ return FALSE;
+
+ /* This path is for regular files */
+- if (!commit_loose_regfile_object (self, actual_checksum, &tmpf, uid, gid, mode, xattrs,
++ if (!commit_loose_regfile_object (self, actual_checksum, &tmpf, uid, gid, mode, xattrs, NULL,
+ cancellable, error))
+ return FALSE;
+
+@@ -1389,7 +1399,8 @@ write_metadata_object (OstreeRepo *self, OstreeObjectType objtype, const char *e
+ return FALSE;
+
+ /* And commit it into place */
+- if (!_ostree_repo_commit_tmpf_final (self, actual_checksum, objtype, &tmpf, cancellable, error))
++ if (!_ostree_repo_commit_tmpf_final (self, actual_checksum, objtype, &tmpf, NULL, cancellable,
++ error))
+ return FALSE;
+
+ if (objtype == OSTREE_OBJECT_TYPE_COMMIT)
+@@ -4466,8 +4477,8 @@ import_one_object_direct (OstreeRepo *dest_repo, OstreeRepo *src_repo, const cha
+ (void)futimens (tmp_dest.fd, ts);
+ }
+
+- if (!_ostree_repo_commit_tmpf_final (dest_repo, checksum, objtype, &tmp_dest, cancellable,
+- error))
++ if (!_ostree_repo_commit_tmpf_final (dest_repo, checksum, objtype, &tmp_dest, NULL,
++ cancellable, error))
+ return FALSE;
+ }
+
+diff --git a/src/libostree/ostree-repo-private.h b/src/libostree/ostree-repo-private.h
+index 77ea2a6bcc..ddaf5e7888 100644
+--- a/src/libostree/ostree-repo-private.h
++++ b/src/libostree/ostree-repo-private.h
+@@ -360,7 +360,8 @@ gboolean _ostree_repo_import_object (OstreeRepo *self, OstreeRepo *source, Ostre
+
+ gboolean _ostree_repo_commit_tmpf_final (OstreeRepo *self, const char *checksum,
+ OstreeObjectType objtype, GLnxTmpfile *tmpf,
+- GCancellable *cancellable, GError **error);
++ gboolean *out_existed, GCancellable *cancellable,
++ GError **error);
+
+ typedef struct
+ {
+diff --git a/src/libostree/ostree-repo-pull.c b/src/libostree/ostree-repo-pull.c
+index 9989415fa2..527ec2cb97 100644
+--- a/src/libostree/ostree-repo-pull.c
++++ b/src/libostree/ostree-repo-pull.c
+@@ -952,8 +952,8 @@ content_fetch_on_complete (GObject *object, GAsyncResult *result, gpointer user_
+ if (pull_data->trusted_http_direct)
+ {
+ g_assert (!verifying_bareuseronly);
+- if (!_ostree_repo_commit_tmpf_final (pull_data->repo, checksum, objtype, &tmpf, cancellable,
+- error))
++ if (!_ostree_repo_commit_tmpf_final (pull_data->repo, checksum, objtype, &tmpf, NULL,
++ cancellable, error))
+ goto out;
+ pull_data->n_fetched_content++;
+ }
+--
+2.49.0
+
diff --git a/0002-lib-commit-Fix-min-free-space-accounting-for-duplicate-content-objects.patch b/0002-lib-commit-Fix-min-free-space-accounting-for-duplicate-content-objects.patch
new file mode 100644
index 0000000..598c2b4
--- /dev/null
+++ b/0002-lib-commit-Fix-min-free-space-accounting-for-duplicate-content-objects.patch
@@ -0,0 +1,142 @@
+From 16e537cb8a9b69d8f254dfbf4abf4f0358251f56 Mon Sep 17 00:00:00 2001
+From: Jonathan Lebon <jlebon@redhat.com>
+Date: Wed, 22 Jul 2026 22:09:15 -0400
+Subject: [PATCH 2/3] lib/commit: Fix min-free-space accounting for duplicate
+ content objects
+
+First, in the _ostree_repo_bare_content_commit(), we never actually
+checked whether the file was already present in the repo before linking
+it in place. Do this so that we can no-op up front.
+
+Second, and the actual bug this patch is fixing: the min-free-space
+accounting in write_content_object() and
+_ostree_repo_bare_content_commit() optimistically reserves space from
+the running `txn.max_blocks` counter _before_ knowing whether we'll
+actually no-op or not based on the object already existing.
+
+Over many duplicate writes within a single transaction, the counter
+diverges from the actual free space and eventually hits zero, causing a
+spurious "min-free-space would be exceeded" error even with plenty of
+disk space available.
+
+This may be the source of the CI issue FCOS is hitting in
+https://github.com/coreos/fedora-coreos-config/pull/4265 due to the
+bootc SELinux relabeling done since
+https://github.com/bootc-dev/bootc/pull/2088.
+
+This patch saves the number of blocks reserved and credits them back in
+both code paths when a duplicate object is detected.
+
+This was heavily AI-guided, finding the bug, and then doing red-green
+testing towards the fix.
+
+Assisted-by: AI
+---
+ src/libostree/ostree-repo-commit.c | 41 ++++++++++++++++++++++--------
+ 1 file changed, 31 insertions(+), 10 deletions(-)
+
+diff --git a/src/libostree/ostree-repo-commit.c b/src/libostree/ostree-repo-commit.c
+index 498376090a..f0fdbd57aa 100644
+--- a/src/libostree/ostree-repo-commit.c
++++ b/src/libostree/ostree-repo-commit.c
+@@ -550,6 +550,7 @@ _ostree_repo_bare_content_commit (OstreeRepo *self, OstreeRepoBareContent *barew
+ OstreeRealRepoBareContent *real = (OstreeRealRepoBareContent *)barewrite;
+ g_assert (real->initialized);
+
++ fsblkcnt_t object_blocks_reserved = 0;
+ if ((self->min_free_space_percent > 0 || self->min_free_space_mb > 0) && self->in_transaction)
+ {
+ struct stat st_buf;
+@@ -559,15 +560,15 @@ _ostree_repo_bare_content_commit (OstreeRepo *self, OstreeRepoBareContent *barew
+ g_mutex_lock (&self->txn_lock);
+ g_assert_cmpint (self->txn.blocksize, >, 0);
+
+- const fsblkcnt_t object_blocks = (st_buf.st_size / self->txn.blocksize) + 1;
+- if (object_blocks > self->txn.max_blocks)
++ object_blocks_reserved = (st_buf.st_size / self->txn.blocksize) + 1;
++ if (object_blocks_reserved > self->txn.max_blocks)
+ {
+ self->cleanup_stagedir = TRUE;
+ g_mutex_unlock (&self->txn_lock);
+ return throw_min_free_space_error (self, st_buf.st_size, error);
+ }
+ /* This is the main bit that needs mutex protection */
+- self->txn.max_blocks -= object_blocks;
++ self->txn.max_blocks -= object_blocks_reserved;
+ g_mutex_unlock (&self->txn_lock);
+ }
+
+@@ -578,9 +579,19 @@ _ostree_repo_bare_content_commit (OstreeRepo *self, OstreeRepoBareContent *barew
+ checksum_buf, error))
+ return FALSE;
+
++ gboolean obj_existed;
+ if (!commit_loose_regfile_object (self, checksum_buf, &real->tmpf, real->uid, real->gid,
+- real->mode, real->xattrs, NULL, cancellable, error))
++ real->mode, real->xattrs, &obj_existed, cancellable, error))
+ return FALSE;
++ /* If the object already existed, credit back the space reservation we
++ * made above — no new disk space was consumed.
++ */
++ if (obj_existed)
++ {
++ g_mutex_lock (&self->txn_lock);
++ self->txn.max_blocks += object_blocks_reserved;
++ g_mutex_unlock (&self->txn_lock);
++ }
+
+ /* Let's have a guarantee that after commit the object is cleaned up */
+ _ostree_repo_bare_content_cleanup (barewrite);
+@@ -969,21 +980,28 @@ write_content_object (OstreeRepo *self, const char *expected_checksum, GInputStr
+
+ (void)file_input_owned; // Conditionally owned
+
+- /* Free space check; only applies during transactions */
++ /* Free space check; only applies during transactions.
++ *
++ * We optimistically reserve space here before we know the object's
++ * checksum. If we later discover the object already exists in the repo
++ * (i.e. it is a duplicate), we credit the reservation back — see the
++ * have_obj block below.
++ */
++ fsblkcnt_t object_blocks_reserved = 0;
+ if ((self->min_free_space_percent > 0 || self->min_free_space_mb > 0) && self->in_transaction)
+ {
+ g_mutex_lock (&self->txn_lock);
+ g_assert_cmpint (self->txn.blocksize, >, 0);
+- const fsblkcnt_t object_blocks = (size / self->txn.blocksize) + 1;
+- if (object_blocks > self->txn.max_blocks)
++ object_blocks_reserved = (size / self->txn.blocksize) + 1;
++ if (object_blocks_reserved > self->txn.max_blocks)
+ {
+- guint64 bytes_required = (guint64)object_blocks * self->txn.blocksize;
++ guint64 bytes_required = (guint64)object_blocks_reserved * self->txn.blocksize;
+ self->cleanup_stagedir = TRUE;
+ g_mutex_unlock (&self->txn_lock);
+ return throw_min_free_space_error (self, bytes_required, error);
+ }
+ /* This is the main bit that needs mutex protection */
+- self->txn.max_blocks -= object_blocks;
++ self->txn.max_blocks -= object_blocks_reserved;
+ g_mutex_unlock (&self->txn_lock);
+ }
+
+@@ -1110,11 +1128,14 @@ write_content_object (OstreeRepo *self, const char *expected_checksum, GInputStr
+ if (!_ostree_repo_has_loose_object (self, actual_checksum, OSTREE_OBJECT_TYPE_FILE, &have_obj,
+ cancellable, error))
+ return FALSE;
+- /* If we already have it, just update the stats. */
++ /* If we already have it, just update the stats. Also credit back the
++ * free-space reservation we made above — no new disk space was consumed.
++ */
+ if (have_obj)
+ {
+ g_mutex_lock (&self->txn_lock);
+ self->txn.stats.content_objects_total++;
++ self->txn.max_blocks += object_blocks_reserved;
+ g_mutex_unlock (&self->txn_lock);
+
+ if (!_create_payload_link (self, actual_checksum, actual_payload_checksum, file_info,
+--
+2.49.0
+
diff --git a/0003-lib-commit-Fix-min-free-space-accounting-for-reflinked-content-objects.patch b/0003-lib-commit-Fix-min-free-space-accounting-for-reflinked-content-objects.patch
new file mode 100644
index 0000000..d9ee113
--- /dev/null
+++ b/0003-lib-commit-Fix-min-free-space-accounting-for-reflinked-content-objects.patch
@@ -0,0 +1,85 @@
+From 07547d29e05a7e6c321a8cc7101d0d7fd361f5a6 Mon Sep 17 00:00:00 2001
+From: Jonathan Lebon <jlebon@redhat.com>
+Date: Fri, 24 Jul 2026 13:02:58 -0400
+Subject: [PATCH 3/3] lib/commit: Fix min-free-space accounting for reflinked
+ content objects
+
+When writing a content object,
+create_regular_tmpfile_linkable_with_content() opportunistically tries
+FICLONE to reflink data from the input fd. The reflinked file shares its
+data blocks via copy-on-write, so the actual disk cost is just metadata.
+
+However, the min-free-space accounting in write_content_object()
+reserves the full file size in max_blocks upfront and never credits it
+back after a successful reflink. Over many files this causes max_blocks
+to diverge significantly from actual free space.
+
+Propagate the reflink status from
+create_regular_tmpfile_linkable_with_content back to
+write_content_object so it can credit back the reserved blocks,
+following the same pattern used for duplicate objects in 16e537cb
+("lib/commit: Fix min-free-space accounting for duplicate content
+objects").
+
+This should fix the CI issues in
+https://github.com/coreos/fedora-coreos-config/pull/4265.
+
+Heavily AI-guided investigation and resolution and red-green testing.
+
+Assisted-by: AI
+---
+ src/libostree/ostree-repo-commit.c | 22 +++++++++++++++++++---
+ 1 file changed, 19 insertions(+), 3 deletions(-)
+
+diff --git a/src/libostree/ostree-repo-commit.c b/src/libostree/ostree-repo-commit.c
+index f0fdbd57aa..a634563905 100644
+--- a/src/libostree/ostree-repo-commit.c
++++ b/src/libostree/ostree-repo-commit.c
+@@ -617,8 +617,8 @@ _ostree_repo_bare_content_cleanup (OstreeRepoBareContent *regwrite)
+ static gboolean
+ create_regular_tmpfile_linkable_with_content (OstreeRepo *self, guint64 length,
+ GInputStream *original_input, GInputStream *input,
+- GLnxTmpfile *out_tmpf, GCancellable *cancellable,
+- GError **error)
++ GLnxTmpfile *out_tmpf, gboolean *out_was_reflinked,
++ GCancellable *cancellable, GError **error)
+ {
+ g_auto (GLnxTmpfile) tmpf = {
+ 0,
+@@ -670,6 +670,8 @@ create_regular_tmpfile_linkable_with_content (OstreeRepo *self, guint64 length,
+ if (!glnx_fchmod (tmpf.fd, 0644, error))
+ return FALSE;
+
++ if (out_was_reflinked)
++ *out_was_reflinked = did_clone;
+ *out_tmpf = tmpf;
+ tmpf.initialized = FALSE;
+ return TRUE;
+@@ -1032,9 +1034,23 @@ write_content_object (OstreeRepo *self, const char *expected_checksum, GInputStr
+ }
+ else if (repo_mode != OSTREE_REPO_MODE_ARCHIVE)
+ {
++ gboolean was_reflinked = FALSE;
+ if (!create_regular_tmpfile_linkable_with_content (self, size, input, file_input, &tmpf,
+- cancellable, error))
++ &was_reflinked, cancellable, error))
+ return FALSE;
++ /* We got a reflink, so we're not actually paying for data here. Credit back max_block
++ * reservation. In theory, we're still paying for inode metadata, but that's a variable cost
++ * depending on the actual filesystem. E.g. xfs allocates from a separate pool, not from data
++ * blocks, so it truly doesn't consume f_bavail. btrfs OTOH would but it wouldn't even cost a
++ * whole block. Trying to get this exactly right is probably not worth it since the actual
++ * amount is still trivial.
++ */
++ if (was_reflinked && object_blocks_reserved > 0)
++ {
++ g_mutex_lock (&self->txn_lock);
++ self->txn.max_blocks += object_blocks_reserved;
++ g_mutex_unlock (&self->txn_lock);
++ }
+ }
+ else
+ {
+--
+2.49.0
+
diff --git a/ostree.spec b/ostree.spec
index f2305a3..e8e7f10 100644
--- a/ostree.spec
+++ b/ostree.spec
@@ -19,6 +19,12 @@ URL: https://ostreedev.github.io/ostree/
ExcludeArch: %{ix86}
%endif
+# Backport https://github.com/ostreedev/ostree/pull/3614
+Patch0: 0001-lib-commit-Report-whether-committed-object-already-existed.patch
+Patch1: 0002-lib-commit-Fix-min-free-space-accounting-for-duplicate-content-objects.patch
+# Backport https://github.com/ostreedev/ostree/pull/3615
+Patch2: 0003-lib-commit-Fix-min-free-space-accounting-for-reflinked-content-objects.patch
+
BuildRequires: make
BuildRequires: git
# We always run autogen.sh
reply other threads:[~2026-07-27 16:51 UTC|newest]
Thread overview: [no followups] expand[flat|nested] mbox.gz Atom feed
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=178517110524.1.1339067946339889027.rpms-ostree-029cd0b3ef04@fedoraproject.org \
--to=dusty@dustymabe.com \
--cc=git-commits@fedoraproject.org \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox