diff --git a/oak-segment-azure/src/main/java/org/apache/jackrabbit/oak/segment/azure/AzureArchiveManager.java b/oak-segment-azure/src/main/java/org/apache/jackrabbit/oak/segment/azure/AzureArchiveManager.java index 132faa1d90..ed7b00939e 100644 --- a/oak-segment-azure/src/main/java/org/apache/jackrabbit/oak/segment/azure/AzureArchiveManager.java +++ b/oak-segment-azure/src/main/java/org/apache/jackrabbit/oak/segment/azure/AzureArchiveManager.java @@ -97,6 +97,12 @@ public class AzureArchiveManager implements SegmentArchiveManager { } } + @Override + public SegmentArchiveReader forceOpen(String archiveName) throws IOException { + CloudBlobDirectory archiveDirectory = getDirectory(archiveName); + return new AzureSegmentArchiveReader(archiveDirectory, ioMonitor); + } + @Override public SegmentArchiveWriter create(String archiveName) throws IOException { return new AzureSegmentArchiveWriter(getDirectory(archiveName), ioMonitor, monitor); diff --git a/oak-segment-tar/src/main/java/org/apache/jackrabbit/oak/segment/SegmentNodeStoreFactory.java b/oak-segment-tar/src/main/java/org/apache/jackrabbit/oak/segment/SegmentNodeStoreFactory.java index 6f2f350fdf..59f63b56d9 100644 --- a/oak-segment-tar/src/main/java/org/apache/jackrabbit/oak/segment/SegmentNodeStoreFactory.java +++ b/oak-segment-tar/src/main/java/org/apache/jackrabbit/oak/segment/SegmentNodeStoreFactory.java @@ -247,6 +247,12 @@ public class SegmentNodeStoreFactory { ) boolean customSegmentStore() default false; + @AttributeDefinition( + name = "Split persistence", + description = "Boolean value indicating that the writes should be done locally when using the custom segment store" + ) + boolean splitPersistence() default false; + @AttributeDefinition( name = "Backup directory", description = "Directory (relative to current working directory) for storing repository backups. " + @@ -466,6 +472,11 @@ public class SegmentNodeStoreFactory { return new File(getRepositoryHome(), appendRole("segmentstore")); } + @Override + public File getSplitPersistenceDirectory() { + return new File(getRepositoryHome(), appendRole("segmentstore-split")); + } + @Override public int getSegmentCacheSize() { return getCacheSize("segmentCache.size", configuration.segmentCache_size()); @@ -516,6 +527,11 @@ public class SegmentNodeStoreFactory { return configuration.customSegmentStore(); } + @Override + public boolean hasSplitPersistence() { + return configuration.splitPersistence(); + } + @Override public boolean registerDescriptors() { return configuration.registerDescriptors(); diff --git a/oak-segment-tar/src/main/java/org/apache/jackrabbit/oak/segment/SegmentNodeStoreRegistrar.java b/oak-segment-tar/src/main/java/org/apache/jackrabbit/oak/segment/SegmentNodeStoreRegistrar.java index 1b44102877..5ea98317c0 100644 --- a/oak-segment-tar/src/main/java/org/apache/jackrabbit/oak/segment/SegmentNodeStoreRegistrar.java +++ b/oak-segment-tar/src/main/java/org/apache/jackrabbit/oak/segment/SegmentNodeStoreRegistrar.java @@ -58,7 +58,9 @@ import org.apache.jackrabbit.oak.segment.file.FileStoreGCMonitor; import org.apache.jackrabbit.oak.segment.file.FileStoreStatsMBean; import org.apache.jackrabbit.oak.segment.file.InvalidFileStoreVersionException; import org.apache.jackrabbit.oak.segment.file.MetricsIOMonitor; +import org.apache.jackrabbit.oak.segment.file.tar.TarPersistence; import org.apache.jackrabbit.oak.segment.spi.persistence.SegmentNodeStorePersistence; +import org.apache.jackrabbit.oak.segment.split.SplitPersistence; import org.apache.jackrabbit.oak.spi.blob.BlobStore; import org.apache.jackrabbit.oak.spi.blob.GarbageCollectableBlobStore; import org.apache.jackrabbit.oak.spi.cluster.ClusterRepositoryInfo; @@ -116,6 +118,8 @@ class SegmentNodeStoreRegistrar { File getSegmentDirectory(); + File getSplitPersistenceDirectory(); + int getSegmentCacheSize(); int getStringCacheSize(); @@ -136,6 +140,8 @@ class SegmentNodeStoreRegistrar { boolean hasCustomSegmentStore(); + boolean hasSplitPersistence(); + boolean registerDescriptors(); String getRepositoryHome(); @@ -228,8 +234,17 @@ class SegmentNodeStoreRegistrar { } if (cfg.hasCustomSegmentStore() && cfg.getSegmentNodeStorePersistence() != null) { - cfg.getLogger().info("Initializing SegmentNodeStore with custom persistence [{}]", cfg.getSegmentNodeStorePersistence()); - builder.withCustomPersistence(cfg.getSegmentNodeStorePersistence()); + if (cfg.hasSplitPersistence()) { + cfg.getLogger().info("Initializing SegmentNodeStore with custom persistence [{}] and local writes", cfg.getSegmentNodeStorePersistence()); + cfg.getSplitPersistenceDirectory().mkdirs(); + SegmentNodeStorePersistence roPersistence = cfg.getSegmentNodeStorePersistence(); + SegmentNodeStorePersistence rwPersistence = new TarPersistence(cfg.getSplitPersistenceDirectory()); + SegmentNodeStorePersistence persistence = new SplitPersistence(roPersistence, rwPersistence); + builder.withCustomPersistence(persistence); + } else { + cfg.getLogger().info("Initializing SegmentNodeStore with custom persistence [{}]", cfg.getSegmentNodeStorePersistence()); + builder.withCustomPersistence(cfg.getSegmentNodeStorePersistence()); + } } if (cfg.isStandbyInstance()) { diff --git a/oak-segment-tar/src/main/java/org/apache/jackrabbit/oak/segment/SegmentNodeStoreService.java b/oak-segment-tar/src/main/java/org/apache/jackrabbit/oak/segment/SegmentNodeStoreService.java index e195de2869..4cc939e6de 100644 --- a/oak-segment-tar/src/main/java/org/apache/jackrabbit/oak/segment/SegmentNodeStoreService.java +++ b/oak-segment-tar/src/main/java/org/apache/jackrabbit/oak/segment/SegmentNodeStoreService.java @@ -245,6 +245,12 @@ public class SegmentNodeStoreService { ) boolean customSegmentStore() default false; + @AttributeDefinition( + name = "Split persistence", + description = "Boolean value indicating that the writes should be done locally when using the custom segment store" + ) + boolean splitPersistence() default false; + @AttributeDefinition( name = "Backup directory", description = "Directory (relative to current working directory) for storing repository backups. " + @@ -402,6 +408,11 @@ public class SegmentNodeStoreService { return new File(getRepositoryHome(), "segmentstore"); } + @Override + public File getSplitPersistenceDirectory() { + return new File(getRepositoryHome(), "segmentstore-split"); + } + @Override public int getSegmentCacheSize() { Integer size = Integer.getInteger("segmentCache.size"); @@ -472,6 +483,11 @@ public class SegmentNodeStoreService { return configuration.customSegmentStore(); } + @Override + public boolean hasSplitPersistence() { + return configuration.splitPersistence(); + } + @Override public boolean registerDescriptors() { return true; diff --git a/oak-segment-tar/src/main/java/org/apache/jackrabbit/oak/segment/file/tar/SegmentTarManager.java b/oak-segment-tar/src/main/java/org/apache/jackrabbit/oak/segment/file/tar/SegmentTarManager.java index d4db4e12b8..8e45065c4f 100644 --- a/oak-segment-tar/src/main/java/org/apache/jackrabbit/oak/segment/file/tar/SegmentTarManager.java +++ b/oak-segment-tar/src/main/java/org/apache/jackrabbit/oak/segment/file/tar/SegmentTarManager.java @@ -25,6 +25,7 @@ import org.apache.jackrabbit.oak.segment.spi.persistence.SegmentArchiveManager; import org.apache.jackrabbit.oak.segment.file.tar.index.Index; import org.apache.jackrabbit.oak.segment.spi.persistence.SegmentArchiveReader; import org.apache.jackrabbit.oak.segment.spi.persistence.SegmentArchiveWriter; +import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -115,6 +116,11 @@ public class SegmentTarManager implements SegmentArchiveManager { } } + @Override + public SegmentArchiveReader forceOpen(String archiveName) throws IOException { + return open(archiveName); + } + @Override public SegmentArchiveWriter create(String archiveName) { return new SegmentTarWriter(new File(segmentstoreDir, archiveName), fileStoreMonitor, ioMonitor); diff --git a/oak-segment-tar/src/main/java/org/apache/jackrabbit/oak/segment/spi/persistence/SegmentArchiveManager.java b/oak-segment-tar/src/main/java/org/apache/jackrabbit/oak/segment/spi/persistence/SegmentArchiveManager.java index 5f5700e66c..de09e6af07 100644 --- a/oak-segment-tar/src/main/java/org/apache/jackrabbit/oak/segment/spi/persistence/SegmentArchiveManager.java +++ b/oak-segment-tar/src/main/java/org/apache/jackrabbit/oak/segment/spi/persistence/SegmentArchiveManager.java @@ -47,11 +47,22 @@ public interface SegmentArchiveManager { * Opens a given archive for reading. * * @param archiveName - * @return the archive reader or null if the archive doesn't exist + * @return the archive reader or null if the archive doesn't exist or doesn't + * have a valid index */ @Nullable SegmentArchiveReader open(@NotNull String archiveName) throws IOException; + /** + * Opens an archive that wasn't closed correctly. + * + * @param archiveName + * @return the archive reader or null if the implementation doesn't support + * opening an unclosed archive + */ + @Nullable + SegmentArchiveReader forceOpen(String archiveName) throws IOException; + /** * Creates a new archive. * diff --git a/oak-segment-tar/src/main/java/org/apache/jackrabbit/oak/segment/split/SplitPersistence.java b/oak-segment-tar/src/main/java/org/apache/jackrabbit/oak/segment/split/SplitPersistence.java new file mode 100644 index 0000000000..fc2e94c337 --- /dev/null +++ b/oak-segment-tar/src/main/java/org/apache/jackrabbit/oak/segment/split/SplitPersistence.java @@ -0,0 +1,136 @@ +/* + * 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.split; + +import org.apache.jackrabbit.oak.segment.spi.monitor.FileStoreMonitor; +import org.apache.jackrabbit.oak.segment.spi.monitor.FileStoreMonitorAdapter; +import org.apache.jackrabbit.oak.segment.spi.monitor.IOMonitor; +import org.apache.jackrabbit.oak.segment.spi.monitor.IOMonitorAdapter; +import org.apache.jackrabbit.oak.segment.spi.persistence.GCJournalFile; +import org.apache.jackrabbit.oak.segment.spi.persistence.JournalFile; +import org.apache.jackrabbit.oak.segment.spi.persistence.JournalFileReader; +import org.apache.jackrabbit.oak.segment.spi.persistence.JournalFileWriter; +import org.apache.jackrabbit.oak.segment.spi.persistence.ManifestFile; +import org.apache.jackrabbit.oak.segment.spi.persistence.RepositoryLock; +import org.apache.jackrabbit.oak.segment.spi.persistence.SegmentArchiveManager; +import org.apache.jackrabbit.oak.segment.spi.persistence.SegmentNodeStorePersistence; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.Properties; + +public class SplitPersistence implements SegmentNodeStorePersistence { + + private final SegmentNodeStorePersistence roPersistence; + + private final SegmentNodeStorePersistence rwPersistence; + + private final Optional lastRoArchive; + + public SplitPersistence(SegmentNodeStorePersistence roPersistence, SegmentNodeStorePersistence rwPersistence) throws IOException { + this.roPersistence = roPersistence; + this.rwPersistence = rwPersistence; + + ManifestFile manifest = rwPersistence.getManifestFile(); + if (!manifest.exists()) { + initialize(); + } + Properties properties = manifest.load(); + lastRoArchive = Optional.ofNullable(properties.getProperty("split.lastRoArchive")); + } + + private void initialize() throws IOException { + Properties properties = roPersistence.getManifestFile().load(); + properties.setProperty("split.initialized", "true"); + Optional lastArchive = getLastArchive(); + lastArchive.ifPresent(a -> properties.setProperty("split.lastRoArchive", a)); + rwPersistence.getManifestFile().save(properties); + + GCJournalFile gcJournalFile = rwPersistence.getGCJournalFile(); + for (String line : roPersistence.getGCJournalFile().readLines()) { + gcJournalFile.writeLine(line); + } + + List journalLines = new ArrayList<>(); + try (JournalFileReader journalFileReader = roPersistence.getJournalFile().openJournalReader()) { + String journalLine; + while ((journalLine = journalFileReader.readLine()) != null) { + journalLines.add(journalLine); + } + } + + Collections.reverse(journalLines); + + try (JournalFileWriter journalFileWriter = rwPersistence.getJournalFile().openJournalWriter()) { + for (String line : journalLines) { + journalFileWriter.writeLine(line); + } + } + } + + private Optional getLastArchive() throws IOException { + SegmentArchiveManager manager = roPersistence.createArchiveManager(false, new IOMonitorAdapter(), new FileStoreMonitorAdapter()); + List archives = manager.listArchives(); + if (archives.isEmpty()) { + return Optional.empty(); + } else { + Collections.sort(archives); + return Optional.of(archives.get(archives.size() - 1)); + } + } + + @Override + public SegmentArchiveManager createArchiveManager(boolean memoryMapping, IOMonitor ioMonitor, FileStoreMonitor fileStoreMonitor) throws IOException { + if (lastRoArchive.isPresent()) { + return new SplitSegmentArchiveManager( + roPersistence.createArchiveManager(memoryMapping, ioMonitor, fileStoreMonitor), + rwPersistence.createArchiveManager(memoryMapping, ioMonitor, fileStoreMonitor), + lastRoArchive.get()); + } else { + return rwPersistence.createArchiveManager(memoryMapping, ioMonitor, fileStoreMonitor); + } + } + + @Override + public boolean segmentFilesExist() { + return lastRoArchive.isPresent() || rwPersistence.segmentFilesExist(); + } + + @Override + public JournalFile getJournalFile() { + return rwPersistence.getJournalFile(); + } + + @Override + public GCJournalFile getGCJournalFile() throws IOException { + return rwPersistence.getGCJournalFile(); + } + + @Override + public ManifestFile getManifestFile() throws IOException { + return rwPersistence.getManifestFile(); + } + + @Override + public RepositoryLock lockRepository() throws IOException { + return rwPersistence.lockRepository(); + } + +} diff --git a/oak-segment-tar/src/main/java/org/apache/jackrabbit/oak/segment/split/SplitSegmentArchiveManager.java b/oak-segment-tar/src/main/java/org/apache/jackrabbit/oak/segment/split/SplitSegmentArchiveManager.java new file mode 100644 index 0000000000..d4101de96f --- /dev/null +++ b/oak-segment-tar/src/main/java/org/apache/jackrabbit/oak/segment/split/SplitSegmentArchiveManager.java @@ -0,0 +1,133 @@ +/* + * 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.split; + +import org.apache.jackrabbit.oak.segment.spi.persistence.SegmentArchiveManager; +import org.apache.jackrabbit.oak.segment.spi.persistence.SegmentArchiveReader; +import org.apache.jackrabbit.oak.segment.spi.persistence.SegmentArchiveWriter; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.UUID; + +public class SplitSegmentArchiveManager implements SegmentArchiveManager { + + private final SegmentArchiveManager roArchiveManager; + + private final SegmentArchiveManager rwArchiveManager; + + private final List roArchiveList; + + public SplitSegmentArchiveManager(SegmentArchiveManager roArchiveManager, SegmentArchiveManager rwArchiveManager, String lastRoArchive) throws IOException { + this.roArchiveManager = roArchiveManager; + this.rwArchiveManager = rwArchiveManager; + this.roArchiveList = getRoArchives(lastRoArchive); + } + + private List getRoArchives(String lastRoArchive) throws IOException { + List archives = roArchiveManager.listArchives(); + Collections.sort(archives); + int index = archives.indexOf(lastRoArchive); + if (index == -1) { + throw new IllegalStateException("Can't find archive " + lastRoArchive + " in the read-only persistence"); + } + return new ArrayList<>(archives.subList(0, index + 1)); + } + + @Override + public @NotNull List listArchives() throws IOException { + List result = new ArrayList<>(); + result.addAll(roArchiveList); + result.addAll(rwArchiveManager.listArchives()); + return result; + } + + @Override + public @Nullable SegmentArchiveReader open(@NotNull String archiveName) throws IOException { + if (roArchiveList.contains(archiveName)) { + try { + return roArchiveManager.open(archiveName); + } catch (IOException e) { + return roArchiveManager.forceOpen(archiveName); + } + } else { + return rwArchiveManager.open(archiveName); + } + } + + @Override + public @Nullable SegmentArchiveReader forceOpen(String archiveName) throws IOException { + if (roArchiveList.contains(archiveName)) { + return roArchiveManager.forceOpen(archiveName); + } else { + return rwArchiveManager.forceOpen(archiveName); + } + } + + @Override + public @NotNull SegmentArchiveWriter create(@NotNull String archiveName) throws IOException { + return rwArchiveManager.create(archiveName); + } + + @Override + public boolean delete(@NotNull String archiveName) { + if (roArchiveList.contains(archiveName)) { + return false; + } else { + return rwArchiveManager.delete(archiveName); + } + } + + @Override + public boolean renameTo(@NotNull String from, @NotNull String to) { + if (roArchiveList.contains(from) || roArchiveList.contains(to)) { + return false; + } else { + return rwArchiveManager.renameTo(from, to); + } + } + + @Override + public void copyFile(@NotNull String from, @NotNull String to) throws IOException { + if (roArchiveList.contains(to)) { + throw new IOException("Can't overwrite the read-only " + to); + } else if (roArchiveList.contains(from)) { + throw new IOException("Can't copy the archive between persistances " + from + " -> " + to); + } else { + rwArchiveManager.copyFile(from, to); + } + } + + @Override + public boolean exists(@NotNull String archiveName) { + return roArchiveList.contains(archiveName) || rwArchiveManager.exists(archiveName); + } + + @Override + public void recoverEntries(@NotNull String archiveName, @NotNull LinkedHashMap entries) throws IOException { + if (roArchiveList.contains(archiveName)) { + roArchiveManager.recoverEntries(archiveName, entries); + } else { + rwArchiveManager.recoverEntries(archiveName, entries); + } + } +}