You are viewing a plain text version of this content. The canonical link for it is here.
Posted to oak-commits@jackrabbit.apache.org by da...@apache.org on 2026/03/02 17:04:26 UTC

(jackrabbit-oak) 01/01: OAK-12121 : add regression tests for offline compaction not persisting gc.log

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 <ri...@gmail.com>
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());
+    }
+
+}