reftable/stack: handle outdated stacks when compacting

When we compact the reftable stack we first acquire the lock for the "tables.list" file and then reload the stack to check that it is still up-to-date. This is done by calling `stack_uptodate()`, which knows to return zero in case the stack is up-to-date, a positive value if it is not and a negative error code on unexpected conditions. We don't do proper error checking though, but instead we only check whether the returned error code is non-zero. If so, we simply bubble it up the calling stack, which means that callers may see an unexpected positive value. Fix this issue by translating to `REFTABLE_OUTDATED_ERROR` instead. Handle this situation in `reftable_addition_commit()`, where we perform a best-effort auto-compaction. All other callsites of `stack_uptodate()` know to handle a positive return value and thus don't need to be fixed. Signed-off-by: Patrick Steinhardt <ps@pks.im> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Patrick Steinhardt committed Aug 12, 2025 at 11:54 UTC 54d25de3ea93d42457bfdec43949683544d0031b
1 file changed +26 -6
reftable/stack.c
+26 -6
@@ -579,9 +579,11 @@ out:
579 return err;
580 }
581
582 -/* -1 = error
583 - 0 = up to date
584 - 1 = changed. */
582 +/*
583 + * Check whether the given stack is up-to-date with what we have in memory.
584 + * Returns 0 if so, 1 if the stack is out-of-date or a negative error code
585 + * otherwise.
586 + */
587 static int stack_uptodate(struct reftable_stack *st)
588 {
589 char **names = NULL;
@@ -850,10 +852,13 @@ int reftable_addition_commit(struct reftable_addition *add)
852 * control. It is possible that a concurrent writer is already
853 * trying to compact parts of the stack, which would lead to a
854 * `REFTABLE_LOCK_ERROR` because parts of the stack are locked
853 - * already. This is a benign error though, so we ignore it.
855 + * already. Similarly, the stack may have been rewritten by a
856 + * concurrent writer, which causes `REFTABLE_OUTDATED_ERROR`.
857 + * Both of these errors are benign, so we simply ignore them.
858 */
859 err = reftable_stack_auto_compact(add->stack);
856 - if (err < 0 && err != REFTABLE_LOCK_ERROR)
860 + if (err < 0 && err != REFTABLE_LOCK_ERROR &&
861 + err != REFTABLE_OUTDATED_ERROR)
862 goto done;
863 err = 0;
864 }
@@ -1215,9 +1220,24 @@ static int stack_compact_range(struct reftable_stack *st,
1220 goto done;
1221 }
1222
1223 + /*
1224 + * Check whether the stack is up-to-date. We unfortunately cannot
1225 + * handle the situation gracefully in case it's _not_ up-to-date
1226 + * because the range of tables that the user has requested us to
1227 + * compact may have been changed. So instead we abort.
1228 + *
1229 + * We could in theory improve the situation by having the caller not
1230 + * pass in a range, but instead the list of tables to compact. If so,
1231 + * we could check that relevant tables still exist. But for now it's
1232 + * good enough to just abort.
1233 + */
1234 err = stack_uptodate(st);
1219 - if (err)
1235 + if (err < 0)
1236 goto done;
1237 + if (err > 0) {
1238 + err = REFTABLE_OUTDATED_ERROR;
1239 + goto done;
1240 + }
1241
1242 /*
1243 * Lock all tables in the user-provided range. This is the slice of our