This is an automated email from the ASF dual-hosted git repository.

daim pushed a commit to branch OAK-12121
in repository https://gitbox.apache.org/repos/asf/jackrabbit-oak.git

commit 705a360986c5d9065d0750649a7981a55c75abd1
Author: rishabhdaim <[email protected]>
AuthorDate: Mon Mar 2 22:34:09 2026 +0530

    OAK-12121 : add regression tests for offline compaction not persisting 
gc.log
---
 .../segment/file/OfflineCompactionGcLogTest.java   | 192 +++++++++++++++++++++
 1 file changed, 192 insertions(+)

diff --git 
a/oak-segment-tar/src/test/java/org/apache/jackrabbit/oak/segment/file/OfflineCompactionGcLogTest.java
 
b/oak-segment-tar/src/test/java/org/apache/jackrabbit/oak/segment/file/OfflineCompactionGcLogTest.java
new file mode 100644
index 0000000000..3f13bc11bc
--- /dev/null
+++ 
b/oak-segment-tar/src/test/java/org/apache/jackrabbit/oak/segment/file/OfflineCompactionGcLogTest.java
@@ -0,0 +1,192 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.jackrabbit.oak.segment.file;
+
+import org.apache.jackrabbit.oak.segment.SegmentNodeBuilder;
+import org.apache.jackrabbit.oak.segment.SegmentNodeState;
+import org.apache.jackrabbit.oak.segment.file.GCJournal.GCJournalEntry;
+import org.apache.jackrabbit.oak.segment.file.tar.TarPersistence;
+import org.apache.jackrabbit.oak.segment.spi.persistence.GCGeneration;
+import org.junit.Assert;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+import java.io.File;
+
+import static 
org.apache.jackrabbit.oak.segment.compaction.SegmentGCOptions.defaultGCOptions;
+import static 
org.apache.jackrabbit.oak.segment.file.FileStoreBuilder.fileStoreBuilder;
+
+/**
+ * Tests that offline compaction (separate {@code compactFull()} + {@code 
cleanup()} calls,
+ * as performed by oak-run compact command) correctly persists the compacted 
head to gc.log.
+ *
+ * <p>All tests in this class are true regression tests: they fail against the 
unfixed code
+ * (because gc.log is never written when compactFull and cleanup are called 
separately)
+ * and pass once the fix that retains the {@code CompactionResult} across the 
two calls
+ * is applied.
+ */
+public class OfflineCompactionGcLogTest {
+
+    @Rule
+    public TemporaryFolder folder = new TemporaryFolder(new File("target"));
+
+    /**
+     * Verifies that calling {@code compactFull()} followed by a separate 
{@code cleanup()}
+     * — which is exactly the sequence used by the oak-run compact command — 
writes an entry
+     * to gc.log with a non-null, non-empty root id and an incremented GC 
generation.
+     *
+     * <p>Prior to the fix, the {@code CompactionResult.succeeded(...)} 
produced by
+     * {@code compactFull()} was discarded before {@code cleanup()} ran. The 
cleanup used a
+     * synthetic {@code CompactionResult.skipped(...)} whose {@code 
requiresGCJournalEntry()}
+     * returns {@code false}, so {@code GCJournal.persist()} was never called.
+     */
+    @Test
+    public void testOfflineCompactionPersistsGcLog() throws Exception {
+        File storeDir = folder.getRoot();
+
+        // Step 1: populate the store with some content
+        try (FileStore store = fileStoreBuilder(storeDir)
+                .withGCOptions(defaultGCOptions().setOffline())
+                .build()) {
+            SegmentNodeState base = store.getHead();
+            SegmentNodeBuilder builder = base.builder();
+            builder.setProperty("key", "value");
+            store.getRevisions().setHead(base.getRecordId(), 
builder.getNodeState().getRecordId());
+            store.flush();
+        }
+
+        // Step 2: verify gc.log is empty before offline compaction
+        GCJournal gcJournalBefore = new GCJournal(new 
TarPersistence(storeDir).getGCJournalFile());
+        Assert.assertEquals("gc.log should be empty before offline compaction",
+                GCJournalEntry.EMPTY, gcJournalBefore.read());
+
+        // Step 3: perform offline compaction — compactFull() and cleanup() as 
separate calls
+        // (this is the exact sequence used by Compact.run() via oak-run 
compact)
+        try (FileStore store = fileStoreBuilder(storeDir)
+                .withGCOptions(defaultGCOptions().setOffline())
+                .build()) {
+            boolean compacted = store.compactFull();
+            Assert.assertTrue("compactFull() should succeed", compacted);
+            store.cleanup();
+        }
+
+        // Step 4: assert gc.log has an entry with the compacted head
+        GCJournal gcJournalAfter = new GCJournal(new 
TarPersistence(storeDir).getGCJournalFile());
+        GCJournalEntry entry = gcJournalAfter.read();
+
+        Assert.assertNotEquals("gc.log must have a non-empty entry after 
offline compaction",
+                GCJournalEntry.EMPTY, entry);
+        Assert.assertNotNull("gc.log entry root must not be null", 
entry.getRoot());
+        Assert.assertFalse("gc.log entry root must not be empty", 
entry.getRoot().isEmpty());
+
+        GCGeneration generation = entry.getGcGeneration();
+        Assert.assertTrue("gc.log entry must record a full generation >= 1",
+                generation.getFullGeneration() >= 1);
+    }
+
+    /**
+     * Scenario: cleanup → compact(ok) → cleanup
+     *
+     * <p>A pre-compaction cleanup (with no preceding {@code compactFull()}) 
must not write a
+     * gc.log entry. Only the cleanup that follows a successful compaction 
should write one.
+     *
+     * <p>Before the fix: gc.log is empty after the full sequence — neither 
cleanup writes
+     * because both fall back to the synthetic {@code 
CompactionResult.skipped(...)} path.
+     * After the fix: gc.log has a valid entry written by the second 
(post-compact) cleanup.
+     */
+    @Test
+    public void testCleanupFirstCompactOkCleanupGcLogWritten() throws 
Exception {
+        File storeDir = folder.getRoot();
+
+        // Step 1: populate the store
+        try (FileStore store = fileStoreBuilder(storeDir)
+                .withGCOptions(defaultGCOptions().setOffline())
+                .build()) {
+            SegmentNodeState base = store.getHead();
+            SegmentNodeBuilder builder = base.builder();
+            builder.setProperty("key", "value");
+            store.getRevisions().setHead(base.getRecordId(), 
builder.getNodeState().getRecordId());
+            store.flush();
+        }
+
+        // Step 2: run cleanup before any compaction, then compact + cleanup
+        try (FileStore store = fileStoreBuilder(storeDir)
+                .withGCOptions(defaultGCOptions().setOffline())
+                .build()) {
+            store.cleanup();                        // no preceding compact — 
must NOT write gc.log
+            boolean compacted = store.compactFull();
+            Assert.assertTrue("compactFull() should succeed", compacted);
+            store.cleanup();                        // succeeded result 
available — must write gc.log
+        }
+
+        // Step 3: assert gc.log has an entry (written by the second cleanup)
+        GCJournal gcJournal = new GCJournal(new 
TarPersistence(storeDir).getGCJournalFile());
+        GCJournalEntry entry = gcJournal.read();
+
+        Assert.assertNotEquals(
+                "gc.log must have a non-empty entry after cleanup → compact → 
cleanup",
+                GCJournalEntry.EMPTY, entry);
+        Assert.assertTrue("gc.log entry must record a full generation >= 1",
+                entry.getGcGeneration().getFullGeneration() >= 1);
+    }
+
+    /**
+     * Scenario: compact(ok) → cleanup → cleanup
+     *
+     * <p>The first cleanup after a successful compaction must write a gc.log 
entry and consume
+     * the stored compaction result. The second cleanup must not write a 
duplicate entry —
+     * it has no stored result and falls back to the no-journal path.
+     *
+     * <p>Before the fix: gc.log has 0 entries — neither cleanup ever writes.
+     * After the fix: gc.log has exactly 1 entry — only the first cleanup 
writes.
+     */
+    @Test
+    public void testCompactOkCleanupCleanupGcLogHasExactlyOneEntry() throws 
Exception {
+        File storeDir = folder.getRoot();
+
+        // Step 1: populate the store
+        try (FileStore store = fileStoreBuilder(storeDir)
+                .withGCOptions(defaultGCOptions().setOffline())
+                .build()) {
+            SegmentNodeState base = store.getHead();
+            SegmentNodeBuilder builder = base.builder();
+            builder.setProperty("key", "value");
+            store.getRevisions().setHead(base.getRecordId(), 
builder.getNodeState().getRecordId());
+            store.flush();
+        }
+
+        // Step 2: compact then call cleanup twice
+        try (FileStore store = fileStoreBuilder(storeDir)
+                .withGCOptions(defaultGCOptions().setOffline())
+                .build()) {
+            boolean compacted = store.compactFull();
+            Assert.assertTrue("compactFull() should succeed", compacted);
+            store.cleanup();    // 2-arg path: writes gc.log, clears stored 
compaction result
+            store.cleanup();    // 1-arg path: no stored result — must NOT 
write a second entry
+        }
+
+        // Step 3: assert gc.log has exactly one entry
+        GCJournal gcJournal = new GCJournal(new 
TarPersistence(storeDir).getGCJournalFile());
+        Assert.assertEquals(
+                "gc.log must have exactly one entry after compact → cleanup → 
cleanup",
+                1, gcJournal.readAll().size());
+    }
+
+}

Reply via email to