This is an automated email from the ASF dual-hosted git repository.
otto pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/nifi.git
The following commit(s) were added to refs/heads/main by this push:
new c06bb97e85 NIFI-11833 - remove deprecated classes in nifi-commons
(#7503)
c06bb97e85 is described below
commit c06bb97e85f1a592a85c529116739d9a86540dcd
Author: Pierre Villard <[email protected]>
AuthorDate: Thu Jul 20 17:58:51 2023 +0200
NIFI-11833 - remove deprecated classes in nifi-commons (#7503)
Signed-off-by: Otto Fowler <[email protected]>
---
.../apache/nifi/stream/io/BufferedInputStream.java | 34 -
.../nifi/stream/io/BufferedOutputStream.java | 34 -
.../nifi/stream/io/ByteArrayInputStream.java | 32 -
.../nifi/stream/io/ByteArrayOutputStream.java | 31 -
.../apache/nifi/stream/io/DataOutputStream.java | 30 -
.../java/org/wali/MinimalLockingWriteAheadLog.java | 1192 --------------------
.../org/wali/TestMinimalLockingWriteAheadLog.java | 977 ----------------
.../repository/WriteAheadFlowFileRepository.java | 154 +--
.../local/WriteAheadLocalStateProvider.java | 36 +-
.../TestWriteAheadFlowFileRepository.java | 4 +-
.../http/StandardHttpFlowFileServerProtocol.java | 4 +-
.../cache/server/map/PersistentMapCache.java | 36 +-
.../cache/server/set/PersistentSetCache.java | 35 +-
.../cache/server/map/TestPersistentMapCache.java | 41 -
14 files changed, 108 insertions(+), 2532 deletions(-)
diff --git
a/nifi-commons/nifi-utils/src/main/java/org/apache/nifi/stream/io/BufferedInputStream.java
b/nifi-commons/nifi-utils/src/main/java/org/apache/nifi/stream/io/BufferedInputStream.java
deleted file mode 100644
index 8dba8999e1..0000000000
---
a/nifi-commons/nifi-utils/src/main/java/org/apache/nifi/stream/io/BufferedInputStream.java
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
- * 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.nifi.stream.io;
-
-import java.io.InputStream;
-
-/**
- * @deprecated use java.io.BufferedInputStream instead
- */
-@Deprecated
-public class BufferedInputStream extends java.io.BufferedInputStream {
-
- public BufferedInputStream(InputStream in) {
- super(in);
- }
-
- public BufferedInputStream(InputStream in, int bufferSize) {
- super(in, bufferSize);
- }
-}
diff --git
a/nifi-commons/nifi-utils/src/main/java/org/apache/nifi/stream/io/BufferedOutputStream.java
b/nifi-commons/nifi-utils/src/main/java/org/apache/nifi/stream/io/BufferedOutputStream.java
deleted file mode 100644
index 8c28eca682..0000000000
---
a/nifi-commons/nifi-utils/src/main/java/org/apache/nifi/stream/io/BufferedOutputStream.java
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
- * 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.nifi.stream.io;
-
-import java.io.OutputStream;
-
-/**
- * @deprecated use java.io.BufferedOutputStream instead
- */
-@Deprecated
-public class BufferedOutputStream extends java.io.BufferedOutputStream {
-
- public BufferedOutputStream(OutputStream out) {
- super(out);
- }
-
- public BufferedOutputStream(OutputStream out, int bufferSize) {
- super(out, bufferSize);
- }
-}
diff --git
a/nifi-commons/nifi-utils/src/main/java/org/apache/nifi/stream/io/ByteArrayInputStream.java
b/nifi-commons/nifi-utils/src/main/java/org/apache/nifi/stream/io/ByteArrayInputStream.java
deleted file mode 100644
index fca5894819..0000000000
---
a/nifi-commons/nifi-utils/src/main/java/org/apache/nifi/stream/io/ByteArrayInputStream.java
+++ /dev/null
@@ -1,32 +0,0 @@
-/*
- * 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.nifi.stream.io;
-
-/**
- * @deprecated use java.io.ByteArrayInputStream instead
- */
-@Deprecated
-public class ByteArrayInputStream extends java.io.ByteArrayInputStream {
-
- public ByteArrayInputStream(byte[] buffer) {
- super(buffer);
- }
-
- public ByteArrayInputStream(byte[] buffer, int offset, int length) {
- super(buffer, offset, length);
- }
-}
diff --git
a/nifi-commons/nifi-utils/src/main/java/org/apache/nifi/stream/io/ByteArrayOutputStream.java
b/nifi-commons/nifi-utils/src/main/java/org/apache/nifi/stream/io/ByteArrayOutputStream.java
deleted file mode 100644
index bdc61aba9a..0000000000
---
a/nifi-commons/nifi-utils/src/main/java/org/apache/nifi/stream/io/ByteArrayOutputStream.java
+++ /dev/null
@@ -1,31 +0,0 @@
-/*
- * 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.nifi.stream.io;
-
-/**
- * @deprecated use java.io.ByteArrayOutputStream instead
- */
-@Deprecated
-public class ByteArrayOutputStream extends java.io.ByteArrayOutputStream {
- public ByteArrayOutputStream() {
- super();
- }
-
- public ByteArrayOutputStream(int initialBufferSize) {
- super(initialBufferSize);
- }
-}
diff --git
a/nifi-commons/nifi-utils/src/main/java/org/apache/nifi/stream/io/DataOutputStream.java
b/nifi-commons/nifi-utils/src/main/java/org/apache/nifi/stream/io/DataOutputStream.java
deleted file mode 100644
index 02b8f1ebcf..0000000000
---
a/nifi-commons/nifi-utils/src/main/java/org/apache/nifi/stream/io/DataOutputStream.java
+++ /dev/null
@@ -1,30 +0,0 @@
-/*
- * 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.nifi.stream.io;
-
-import java.io.OutputStream;
-
-/**
- * @deprecated use java.io.DataOutputStream instead
- */
-@Deprecated
-public class DataOutputStream extends java.io.DataOutputStream {
-
- public DataOutputStream(OutputStream out) {
- super(out);
- }
-}
diff --git
a/nifi-commons/nifi-write-ahead-log/src/main/java/org/wali/MinimalLockingWriteAheadLog.java
b/nifi-commons/nifi-write-ahead-log/src/main/java/org/wali/MinimalLockingWriteAheadLog.java
deleted file mode 100644
index 8db2d87885..0000000000
---
a/nifi-commons/nifi-write-ahead-log/src/main/java/org/wali/MinimalLockingWriteAheadLog.java
+++ /dev/null
@@ -1,1192 +0,0 @@
-/*
- * 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.wali;
-
-import static java.util.Objects.requireNonNull;
-
-import java.io.BufferedInputStream;
-import java.io.BufferedOutputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.DataInputStream;
-import java.io.DataOutputStream;
-import java.io.EOFException;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.nio.channels.FileChannel;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.StandardOpenOption;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Map;
-import java.util.Queue;
-import java.util.Set;
-import java.util.SortedMap;
-import java.util.SortedSet;
-import java.util.TreeMap;
-import java.util.TreeSet;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.ConcurrentMap;
-import java.util.concurrent.CopyOnWriteArraySet;
-import java.util.concurrent.LinkedBlockingQueue;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicInteger;
-import java.util.concurrent.atomic.AtomicLong;
-import java.util.concurrent.locks.Lock;
-import java.util.concurrent.locks.ReadWriteLock;
-import java.util.concurrent.locks.ReentrantReadWriteLock;
-import java.util.regex.Pattern;
-
-import org.apache.nifi.wali.SequentialAccessWriteAheadLog;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-/**
- * <p>
- * This implementation provides as little Locking as possible in order to
- * provide the highest throughput possible. However, this implementation is
ONLY
- * appropriate if it can be guaranteed that only a single thread will ever
issue
- * updates for a given Record at any one time.
- * </p>
- *
- * @param <T> type of record this WAL is for
- *
- * @deprecated This implementation is now deprecated in favor of {@link
SequentialAccessWriteAheadLog}.
- * This implementation, when given more than 1 partition, can have
issues recovering after a sudden loss
- * of power or an operating system crash.
- */
-@Deprecated
-public final class MinimalLockingWriteAheadLog<T> implements
WriteAheadRepository<T> {
-
- private final Path basePath;
- private final Path partialPath;
- private final Path snapshotPath;
-
- private final SerDeFactory<T> serdeFactory;
- private final SyncListener syncListener;
- private final FileChannel lockChannel;
- private final AtomicLong transactionIdGenerator = new AtomicLong(0L);
-
- private final Partition<T>[] partitions;
- private final AtomicLong partitionIndex = new AtomicLong(0L);
- private final ConcurrentMap<Object, T> recordMap = new
ConcurrentHashMap<>();
- private final Map<Object, T> unmodifiableRecordMap =
Collections.unmodifiableMap(recordMap);
- private final Set<String> externalLocations = new CopyOnWriteArraySet<>();
-
- private final Set<String> recoveredExternalLocations = new
CopyOnWriteArraySet<>();
-
- private final AtomicInteger numberBlackListedPartitions = new
AtomicInteger(0);
-
- private static final Logger logger =
LoggerFactory.getLogger(MinimalLockingWriteAheadLog.class);
-
- private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
- private final Lock readLock = rwLock.readLock(); // required to update a
partition
- private final Lock writeLock = rwLock.writeLock(); // required for
checkpoint
-
- private volatile boolean updated = false;
- private volatile boolean recovered = false;
-
- public MinimalLockingWriteAheadLog(final Path path, final int
partitionCount, final SerDe<T> serde, final SyncListener syncListener) throws
IOException {
- this(new TreeSet<>(Collections.singleton(path)), partitionCount, new
SingletonSerDeFactory<>(serde), syncListener);
- }
-
- public MinimalLockingWriteAheadLog(final Path path, final int
partitionCount, final SerDeFactory<T> serdeFactory, final SyncListener
syncListener) throws IOException {
- this(new TreeSet<>(Collections.singleton(path)), partitionCount,
serdeFactory, syncListener);
- }
-
- public MinimalLockingWriteAheadLog(final SortedSet<Path> paths, final int
partitionCount, final SerDe<T> serde, final SyncListener syncListener) throws
IOException {
- this(paths, partitionCount, new SingletonSerDeFactory<>(serde),
syncListener);
- }
-
- /**
- *
- * @param paths a sorted set of Paths to use for the partitions/journals
and
- * the snapshot. The snapshot will always be written to the first path
- * specified.
- * @param partitionCount the number of partitions/journals to use. For best
- * performance, this should be close to the number of threads that are
- * expected to update the repository simultaneously
- * @param serdeFactory the factory for the serializer/deserializer for
records
- * @param syncListener the listener
- * @throws IOException if unable to initialize due to IO issue
- */
- @SuppressWarnings("unchecked")
- public MinimalLockingWriteAheadLog(final SortedSet<Path> paths, final int
partitionCount, final SerDeFactory<T> serdeFactory, final SyncListener
syncListener) throws IOException {
- this.syncListener = syncListener;
-
- requireNonNull(paths);
- requireNonNull(serdeFactory);
-
- if (paths.isEmpty()) {
- throw new IllegalArgumentException("Paths must be non-empty");
- }
-
- int resolvedPartitionCount = partitionCount;
- int existingPartitions = 0;
- for (final Path path : paths) {
- if (!Files.exists(path)) {
- Files.createDirectories(path);
- }
-
- final File file = path.toFile();
- if (!file.isDirectory()) {
- throw new IOException("Path given [" + path + "] is not a
directory");
- }
- if (!file.canWrite()) {
- throw new IOException("Path given [" + path + "] is not
writable");
- }
- if (!file.canRead()) {
- throw new IOException("Path given [" + path + "] is not
readable");
- }
- if (!file.canExecute()) {
- throw new IOException("Path given [" + path + "] is not
executable");
- }
-
- final File[] children = file.listFiles();
- if (children != null) {
- for (final File child : children) {
- if (child.isDirectory() &&
child.getName().startsWith("partition-")) {
- existingPartitions++;
- }
- }
-
- if (existingPartitions != 0 && existingPartitions !=
partitionCount) {
- logger.warn("Constructing MinimalLockingWriteAheadLog with
partitionCount={}, but the repository currently has "
- + "{} partitions; ignoring argument and proceeding
with {} partitions",
- new Object[]{partitionCount, existingPartitions,
existingPartitions});
- resolvedPartitionCount = existingPartitions;
- }
- }
- }
-
- this.basePath = paths.iterator().next();
- this.partialPath = basePath.resolve("snapshot.partial");
- this.snapshotPath = basePath.resolve("snapshot");
- this.serdeFactory = serdeFactory;
-
- final Path lockPath = basePath.resolve("wali.lock");
- lockChannel = new FileOutputStream(lockPath.toFile()).getChannel();
- lockChannel.lock();
-
- partitions = new Partition[resolvedPartitionCount];
-
- Iterator<Path> pathIterator = paths.iterator();
- for (int i = 0; i < resolvedPartitionCount; i++) {
- // If we're out of paths, create a new iterator to start over.
- if (!pathIterator.hasNext()) {
- pathIterator = paths.iterator();
- }
-
- final Path partitionBasePath = pathIterator.next();
-
- partitions[i] = new
Partition<>(partitionBasePath.resolve("partition-" + i), serdeFactory, i,
getVersion());
- }
- }
-
- @Override
- public int update(final Collection<T> records, final boolean forceSync)
throws IOException {
- if (!recovered) {
- throw new IllegalStateException("Cannot update repository until
record recovery has been performed");
- }
-
- if (records.isEmpty()) {
- return -1;
- }
-
- updated = true;
- readLock.lock();
- try {
- while (true) {
- final int numBlackListed = numberBlackListedPartitions.get();
- if (numBlackListed >= partitions.length) {
- throw new IOException("All Partitions have been
blacklisted due to "
- + "failures when attempting to update. If the
Write-Ahead Log is able to perform a checkpoint, "
- + "this issue may resolve itself. Otherwise,
manual intervention will be required.");
- }
-
- final long partitionIdx = partitionIndex.getAndIncrement();
- final int resolvedIdx = (int) (partitionIdx %
partitions.length);
- final Partition<T> partition = partitions[resolvedIdx];
- if (partition.tryClaim()) {
- try {
- final long transactionId =
transactionIdGenerator.getAndIncrement();
- if (logger.isTraceEnabled()) {
- for (final T record : records) {
- logger.trace("Partition {} performing
Transaction {}: {}", new Object[] {partition, transactionId, record});
- }
- }
-
- try {
- partition.update(records, transactionId,
unmodifiableRecordMap, forceSync);
- } catch (final Throwable t) {
- partition.blackList();
- numberBlackListedPartitions.incrementAndGet();
- throw t;
- }
-
- if (forceSync && syncListener != null) {
- syncListener.onSync(resolvedIdx);
- }
- } finally {
- partition.releaseClaim();
- }
-
- for (final T record : records) {
- final UpdateType updateType =
serdeFactory.getUpdateType(record);
- final Object recordIdentifier =
serdeFactory.getRecordIdentifier(record);
-
- if (updateType == UpdateType.DELETE) {
- recordMap.remove(recordIdentifier);
- } else if (updateType == UpdateType.SWAP_OUT) {
- final String newLocation =
serdeFactory.getLocation(record);
- if (newLocation == null) {
- logger.error("Received Record (ID=" +
recordIdentifier + ") with UpdateType of SWAP_OUT but "
- + "no indicator of where the Record is
to be Swapped Out to; these records may be "
- + "lost when the repository is
restored!");
- } else {
- recordMap.remove(recordIdentifier);
- this.externalLocations.add(newLocation);
- }
- } else if (updateType == UpdateType.SWAP_IN) {
- final String newLocation =
serdeFactory.getLocation(record);
- if (newLocation == null) {
- logger.error("Received Record (ID=" +
recordIdentifier + ") with UpdateType of SWAP_IN but no "
- + "indicator of where the Record is to
be Swapped In from; these records may be duplicated "
- + "when the repository is restored!");
- } else {
- externalLocations.remove(newLocation);
- }
- recordMap.put(recordIdentifier, record);
- } else {
- recordMap.put(recordIdentifier, record);
- }
- }
-
- return resolvedIdx;
- }
- }
- } finally {
- readLock.unlock();
- }
- }
-
- @Override
- public Collection<T> recoverRecords() throws IOException {
- if (updated) {
- throw new IllegalStateException("Cannot recover records after
updating the repository; must call recoverRecords first");
- }
-
- final long recoverStart = System.nanoTime();
- writeLock.lock();
- try {
- Long maxTransactionId = recoverFromSnapshot(recordMap);
- recoverFromEdits(recordMap, maxTransactionId);
-
- for (final Partition<T> partition : partitions) {
- final long transId = partition.getMaxRecoveredTransactionId();
- if (maxTransactionId == null || transId > maxTransactionId) {
- maxTransactionId = transId;
- }
- }
-
- this.transactionIdGenerator.set(maxTransactionId + 1);
- this.externalLocations.addAll(recoveredExternalLocations);
- logger.info("{} finished recovering records. Performing Checkpoint
to ensure proper state of Partitions before updates", this);
- } finally {
- writeLock.unlock();
- }
- final long recoverNanos = System.nanoTime() - recoverStart;
- final long recoveryMillis =
TimeUnit.MILLISECONDS.convert(recoverNanos, TimeUnit.NANOSECONDS);
- logger.info("Successfully recovered {} records in {} milliseconds",
recordMap.size(), recoveryMillis);
- checkpoint();
-
- recovered = true;
- return recordMap.values();
- }
-
- @Override
- public Set<String> getRecoveredSwapLocations() throws IOException {
- return recoveredExternalLocations;
- }
-
- private Long recoverFromSnapshot(final Map<Object, T> recordMap) throws
IOException {
- final boolean partialExists = Files.exists(partialPath);
- final boolean snapshotExists = Files.exists(snapshotPath);
-
- if (!partialExists && !snapshotExists) {
- return null;
- }
-
- if (partialExists && snapshotExists) {
- // both files exist -- assume we failed while checkpointing. Delete
- // the partial file
- Files.delete(partialPath);
- } else if (partialExists) {
- // partial exists but snapshot does not -- we must have completed
- // creating the partial, deleted the snapshot
- // but crashed before renaming the partial to the snapshot. Just
- // rename partial to snapshot
- Files.move(partialPath, snapshotPath);
- }
-
- if (Files.size(snapshotPath) == 0) {
- logger.warn("{} Found 0-byte Snapshot file; skipping Snapshot file
in recovery", this);
- return null;
- }
-
- // at this point, we know the snapshotPath exists because if it
didn't, then we either returned null
- // or we renamed partialPath to snapshotPath. So just Recover from
snapshotPath.
- try (final DataInputStream dataIn = new DataInputStream(new
BufferedInputStream(Files.newInputStream(snapshotPath,
StandardOpenOption.READ)))) {
- final String waliImplementationClass = dataIn.readUTF();
- final int waliImplementationVersion = dataIn.readInt();
-
- if
(!waliImplementationClass.equals(MinimalLockingWriteAheadLog.class.getName())) {
- throw new IOException("Write-Ahead Log located at " +
snapshotPath + " was written using the "
- + waliImplementationClass + " class; cannot restore
using " + getClass().getName());
- }
-
- if (waliImplementationVersion > getVersion()) {
- throw new IOException("Write-Ahead Log located at " +
snapshotPath + " was written using version "
- + waliImplementationVersion + " of the " +
waliImplementationClass + " class; cannot restore using Version " +
getVersion());
- }
-
- final String serdeEncoding = dataIn.readUTF(); // ignore serde
class name for now
- final int serdeVersion = dataIn.readInt();
- final long maxTransactionId = dataIn.readLong();
- final int numRecords = dataIn.readInt();
-
- final SerDe<T> serde = serdeFactory.createSerDe(serdeEncoding);
- serde.readHeader(dataIn);
-
- for (int i = 0; i < numRecords; i++) {
- final T record = serde.deserializeRecord(dataIn, serdeVersion);
- if (record == null) {
- throw new EOFException();
- }
-
- final UpdateType updateType = serde.getUpdateType(record);
- if (updateType == UpdateType.DELETE) {
- logger.warn("While recovering from snapshot, found record
with type 'DELETE'; this record will not be restored");
- continue;
- }
-
- logger.trace("Recovered from snapshot: {}", record);
- recordMap.put(serde.getRecordIdentifier(record), record);
- }
-
- final int numSwapRecords = dataIn.readInt();
- final Set<String> swapLocations = new HashSet<>();
- for (int i = 0; i < numSwapRecords; i++) {
- swapLocations.add(dataIn.readUTF());
- }
- this.recoveredExternalLocations.addAll(swapLocations);
-
- logger.debug("{} restored {} Records and {} Swap Files from
Snapshot, ending with Transaction ID {}",
- new Object[]{this, numRecords,
recoveredExternalLocations.size(), maxTransactionId});
- return maxTransactionId;
- }
- }
-
- /**
- * Recovers records from the edit logs via the Partitions. Returns a
boolean
- * if recovery of a Partition requires the Write-Ahead Log be checkpointed
- * before modification.
- *
- * @param modifiableRecordMap map
- * @param maxTransactionIdRestored index of max restored transaction
- * @throws IOException if unable to recover from edits
- */
- private void recoverFromEdits(final Map<Object, T> modifiableRecordMap,
final Long maxTransactionIdRestored) throws IOException {
- final Map<Object, T> updateMap = new HashMap<>();
- final Map<Object, T> unmodifiableRecordMap =
Collections.unmodifiableMap(modifiableRecordMap);
- final Map<Object, T> ignorableMap = new HashMap<>();
- final Set<String> ignorableSwapLocations = new HashSet<>();
-
- // populate a map of the next transaction id for each partition to the
- // partition that has that next transaction id.
- final SortedMap<Long, Partition<T>> transactionMap = new TreeMap<>();
- for (final Partition<T> partition : partitions) {
- Long transactionId;
- boolean keepTransaction;
- do {
- transactionId = partition.getNextRecoverableTransactionId();
-
- keepTransaction = transactionId == null ||
maxTransactionIdRestored == null || transactionId > maxTransactionIdRestored;
- if (keepTransaction && transactionId != null) {
- // map this transaction id to its partition so that we can
- // start restoring transactions from this partition,
- // starting at 'transactionId'
- transactionMap.put(transactionId, partition);
- } else if (transactionId != null) {
- // skip the next transaction, because our snapshot already
- // contained this transaction.
- try {
- partition.recoverNextTransaction(ignorableMap,
updateMap, ignorableSwapLocations);
- } catch (final EOFException e) {
- logger.error("{} unexpectedly reached End of File
while reading from {} for Transaction {}; "
- + "assuming crash and ignoring this
transaction.",
- new Object[]{this, partition, transactionId});
- }
- }
- } while (!keepTransaction);
- }
-
- while (!transactionMap.isEmpty()) {
- final Map.Entry<Long, Partition<T>> firstEntry =
transactionMap.entrySet().iterator().next();
- final Long firstTransactionId = firstEntry.getKey();
- final Partition<T> nextPartition = firstEntry.getValue();
-
- try {
- updateMap.clear();
- final Set<Object> idsRemoved =
nextPartition.recoverNextTransaction(unmodifiableRecordMap, updateMap,
recoveredExternalLocations);
- modifiableRecordMap.putAll(updateMap);
- for (final Object id : idsRemoved) {
- modifiableRecordMap.remove(id);
- }
- } catch (final EOFException e) {
- logger.error("{} unexpectedly reached End-of-File when reading
from {} for Transaction ID {}; "
- + "assuming crash and ignoring this transaction",
- new Object[]{this, nextPartition, firstTransactionId});
- }
-
- transactionMap.remove(firstTransactionId);
-
- Long subsequentTransactionId = null;
- try {
- subsequentTransactionId =
nextPartition.getNextRecoverableTransactionId();
- } catch (final IOException e) {
- logger.error("{} unexpectedly found End-of-File when reading
from {} for Transaction ID {}; "
- + "assuming crash and ignoring this transaction",
- new Object[]{this, nextPartition, firstTransactionId});
- }
-
- if (subsequentTransactionId != null) {
- transactionMap.put(subsequentTransactionId, nextPartition);
- }
- }
-
- for (final Partition<T> partition : partitions) {
- partition.endRecovery();
- }
- }
-
- @Override
- public synchronized int checkpoint() throws IOException {
- final Set<T> records;
- final Set<String> swapLocations;
- final long maxTransactionId;
-
- final long startNanos = System.nanoTime();
-
- FileOutputStream fileOut = null;
- DataOutputStream dataOut = null;
-
- long stopTheWorldNanos = -1L;
- long stopTheWorldStart = -1L;
- try {
- final List<OutputStream> partitionStreams = new ArrayList<>();
-
- writeLock.lock();
- try {
- stopTheWorldStart = System.nanoTime();
- // stop the world while we make a copy of the records that must
- // be checkpointed and rollover the partitions.
- // We copy the records because serializing them is potentially
- // very expensive, especially when we have hundreds
- // of thousands or even millions of them. We don't want to
- // prevent WALI from being used during this time.
-
- // So the design is to copy all of the records, determine the
- // last transaction ID that the records represent,
- // and roll over the partitions to new write-ahead logs.
- // Then, outside of the write lock, we will serialize the data
- // to disk, and then remove the old Partition data.
- records = new HashSet<>(recordMap.values());
- maxTransactionId = transactionIdGenerator.get() - 1;
-
- swapLocations = new HashSet<>(externalLocations);
- for (final Partition<T> partition : partitions) {
- try {
- partitionStreams.add(partition.rollover());
- } catch (final Throwable t) {
- partition.blackList();
- numberBlackListedPartitions.getAndIncrement();
- throw t;
- }
- }
- } finally {
- writeLock.unlock();
- }
-
- stopTheWorldNanos = System.nanoTime() - stopTheWorldStart;
-
- // Close all of the Partitions' Output Streams. We do this here,
instead of in Partition.rollover()
- // because we want to do this outside of the write lock. Because
calling close() on FileOutputStream can
- // be very expensive, as it has to flush the data to disk, we
don't want to prevent other Process Sessions
- // from getting committed. Since rollover() transitions the
partition to write to a new file already, there
- // is no reason that we need to close this FileOutputStream before
releasing the write lock. Also, if any Exception
- // does get thrown when calling close(), we don't need to
blacklist the partition, as the stream that was getting
- // closed is not the stream being written to for the partition
anyway. We also catch any IOException and wait until
- // after we've attempted to close all streams before we throw an
Exception, to avoid resource leaks if one of them
- // is unable to be closed (due to out of storage space, for
instance).
- IOException failure = null;
- for (final OutputStream partitionStream : partitionStreams) {
- try {
- partitionStream.close();
- } catch (final IOException e) {
- failure = e;
- }
- }
- if (failure != null) {
- throw failure;
- }
-
- // notify global sync with the write lock held. We do this because
we don't want the repository to get updated
- // while the listener is performing its necessary tasks
- if (syncListener != null) {
- syncListener.onGlobalSync();
- }
-
- final SerDe<T> serde = serdeFactory.createSerDe(null);
-
- // perform checkpoint, writing to .partial file
- fileOut = new FileOutputStream(partialPath.toFile());
- dataOut = new DataOutputStream(new BufferedOutputStream(fileOut));
- dataOut.writeUTF(MinimalLockingWriteAheadLog.class.getName());
- dataOut.writeInt(getVersion());
- dataOut.writeUTF(serde.getClass().getName());
- dataOut.writeInt(serde.getVersion());
- dataOut.writeLong(maxTransactionId);
- dataOut.writeInt(records.size());
- serde.writeHeader(dataOut);
-
- for (final T record : records) {
- logger.trace("Checkpointing {}", record);
- serde.serializeRecord(record, dataOut);
- }
-
- dataOut.writeInt(swapLocations.size());
- for (final String swapLocation : swapLocations) {
- dataOut.writeUTF(swapLocation);
- }
- } finally {
- if (dataOut != null) {
- try {
- try {
- dataOut.flush();
- fileOut.getFD().sync();
- } finally {
- dataOut.close();
- }
- } catch (final IOException e) {
- logger.warn("Failed to close Data Stream due to {}",
e.toString(), e);
- }
- }
- }
-
- // delete the snapshot, if it exists, and rename the .partial to
- // snapshot
- Files.deleteIfExists(snapshotPath);
- Files.move(partialPath, snapshotPath);
-
- // clear all of the edit logs
- final long partitionStart = System.nanoTime();
- for (final Partition<T> partition : partitions) {
- // we can call clearOld without claiming the partition because it
- // does not change the partition's state
- // and the only member variable it touches cannot be modified,
other
- // than when #rollover() is called.
- // And since this method is the only one that calls #rollover() and
- // this method is synchronized,
- // the value of that member variable will not change. And it's
- // volatile, so we will get the correct value.
- partition.clearOld();
- }
- final long partitionEnd = System.nanoTime();
- numberBlackListedPartitions.set(0);
-
- final long endNanos = System.nanoTime();
- final long millis = TimeUnit.MILLISECONDS.convert(endNanos -
startNanos, TimeUnit.NANOSECONDS);
- final long partitionMillis =
TimeUnit.MILLISECONDS.convert(partitionEnd - partitionStart,
TimeUnit.NANOSECONDS);
- final long stopTheWorldMillis =
TimeUnit.NANOSECONDS.toMillis(stopTheWorldNanos);
-
- logger.info("{} checkpointed with {} Records and {} Swap Files in {}
milliseconds (Stop-the-world "
- + "time = {} milliseconds, Clear Edit Logs time = {} millis),
max Transaction ID {}",
- new Object[]{this, records.size(), swapLocations.size(),
millis, stopTheWorldMillis, partitionMillis, maxTransactionId});
-
- return records.size();
- }
-
- @Override
- public void shutdown() throws IOException {
- writeLock.lock();
- try {
- for (final Partition<T> partition : partitions) {
- partition.close();
- }
- } finally {
- writeLock.unlock();
- lockChannel.close();
-
- final File lockFile = new File(basePath.toFile(), "wali.lock");
- lockFile.delete();
- }
- }
-
- public int getVersion() {
- return 1;
- }
-
- /**
- * Represents a partition of this repository, which maps directly to a
- * .journal file.
- *
- * All methods with the exceptions of {@link #claim()}, {@link
#tryClaim()},
- * and {@link #releaseClaim()} in this Partition MUST be called while
- * holding the claim (via {@link #claim} or {@link #tryClaim()}).
- *
- * @param <S> type of record held in the partitions
- */
- private static class Partition<S> {
- public static final String JOURNAL_EXTENSION = ".journal";
- private static final int NUL_BYTE = 0;
- private static final Pattern JOURNAL_FILENAME_PATTERN =
Pattern.compile("\\d+\\.journal");
-
- private final SerDeFactory<S> serdeFactory;
- private SerDe<S> serde;
-
- private final Path editDirectory;
- private final int writeAheadLogVersion;
-
- private DataOutputStream dataOut = null;
- private FileOutputStream fileOut = null;
- private volatile boolean blackListed = false;
- private volatile boolean closed = false;
- private DataInputStream recoveryIn;
- private int recoveryVersion;
- private String currentJournalFilename = "";
-
- private static final byte TRANSACTION_CONTINUE = 1;
- private static final byte TRANSACTION_COMMIT = 2;
-
- private final String description;
- private final AtomicLong maxTransactionId = new AtomicLong(-1L);
- private final Logger logger =
LoggerFactory.getLogger(MinimalLockingWriteAheadLog.class);
-
- private final Queue<Path> recoveryFiles;
-
- public Partition(final Path path, final SerDeFactory<S> serdeFactory,
final int partitionIndex, final int writeAheadLogVersion) throws IOException {
- this.editDirectory = path;
- this.serdeFactory = serdeFactory;
-
- final File file = path.toFile();
- if (!file.exists() && !file.mkdirs()) {
- throw new IOException("Could not create directory " +
file.getAbsolutePath());
- }
-
- this.recoveryFiles = new LinkedBlockingQueue<>();
- for (final Path recoveryPath : getRecoveryPaths()) {
- recoveryFiles.add(recoveryPath);
- }
-
- this.description = "Partition-" + partitionIndex;
- this.writeAheadLogVersion = writeAheadLogVersion;
- }
-
- public boolean tryClaim() {
- return !blackListed;
- }
-
- public void releaseClaim() {
- }
-
- public void close() {
- this.closed = true;
-
- // Note that here we are closing fileOut and NOT dataOut.
- // This is very much intentional, not an oversight. This is done
because of
- // the way that the OutputStreams are structured. dataOut wraps a
BufferedOutputStream,
- // which then wraps the FileOutputStream. If we close 'dataOut',
then this will call
- // the flush() method of BufferedOutputStream. Under normal
conditions, this is fine.
- // However, there is a very important corner case to consider:
- //
- // If we are writing to the DataOutputStream in the update()
method and that
- // call to write() then results in the BufferedOutputStream
calling flushBuffer() -
- // or if we finish the call to update() and call flush()
ourselves - it is possible
- // that the internal buffer of the BufferedOutputStream can
get partially written to
- // to the FileOutputStream and then an IOException occurs. If
this occurs, we have
- // written a partial record to disk. This still is okay, as
we have logic to handle
- // the condition where we have a partial record and then an
unexpected End-of-File.
- // But if we then call close() on 'dataOut', this will call
the flush() method of the
- // underlying BufferedOutputStream. As a result, we will end
up again writing the internal
- // buffer of the BufferedOutputStream to the underlying file.
At this point, we are left
- // not with an unexpected/premature End-of-File but instead a
bunch of seemingly random
- // bytes that happened to be residing in that internal
buffer, and this will result in
- // a corrupt and unrecoverable Write-Ahead Log.
- //
- // Additionally, we are okay not ever calling close on the
wrapping BufferedOutputStream and
- // DataOutputStream because they don't actually hold any resources
that need to be reclaimed,
- // and after each update to the Write-Ahead Log, we call flush()
ourselves to ensure that we don't
- // leave arbitrary data in the BufferedOutputStream that hasn't
been flushed to the underlying
- // FileOutputStream.
- final OutputStream out = fileOut;
- if (out != null) {
- try {
- out.close();
- } catch (final Exception e) {
- }
- }
-
- this.dataOut = null;
- this.fileOut = null;
- }
-
- public void blackList() {
- blackListed = true;
- logger.debug("Blacklisted {}", this);
- }
-
- /**
- * Closes resources pointing to the current journal and begins writing
- * to a new one
- *
- * @throws IOException if failure to rollover
- */
- public OutputStream rollover() throws IOException {
- // Note that here we are closing fileOut and NOT dataOut. See the
note in the close()
- // method to understand the logic behind this.
- final OutputStream oldOutputStream = fileOut;
- dataOut = null;
- fileOut = null;
-
- this.serde = serdeFactory.createSerDe(null);
- final Path editPath = getNewEditPath();
- final FileOutputStream fos = new
FileOutputStream(editPath.toFile());
- try {
- final DataOutputStream outStream = new DataOutputStream(new
BufferedOutputStream(fos));
-
outStream.writeUTF(MinimalLockingWriteAheadLog.class.getName());
- outStream.writeInt(writeAheadLogVersion);
- outStream.writeUTF(serde.getClass().getName());
- outStream.writeInt(serde.getVersion());
- serde.writeHeader(outStream);
-
- outStream.flush();
- dataOut = outStream;
- fileOut = fos;
- } catch (final IOException ioe) {
- try {
- oldOutputStream.close();
- } catch (final IOException ioe2) {
- ioe.addSuppressed(ioe2);
- }
-
- logger.error("Failed to create new journal for {} due to {}",
new Object[] {this, ioe.toString()}, ioe);
- try {
- fos.close();
- } catch (final IOException innerIOE) {
- }
-
- dataOut = null;
- fileOut = null;
- blackList();
-
- throw ioe;
- }
-
- currentJournalFilename = editPath.toFile().getName();
-
- blackListed = false;
- return oldOutputStream;
- }
-
- private long getJournalIndex(final File file) {
- final String filename = file.getName();
- final int dotIndex = filename.indexOf(".");
- final String number = filename.substring(0, dotIndex);
- return Long.parseLong(number);
- }
-
- private Path getNewEditPath() {
- final List<Path> recoveryPaths = getRecoveryPaths();
- final long newIndex;
- if (recoveryPaths == null || recoveryPaths.isEmpty()) {
- newIndex = 1;
- } else {
- final long lastFileIndex =
getJournalIndex(recoveryPaths.get(recoveryPaths.size() - 1).toFile());
- newIndex = lastFileIndex + 1;
- }
-
- return editDirectory.resolve(newIndex + JOURNAL_EXTENSION);
- }
-
- private List<Path> getRecoveryPaths() {
- final List<Path> paths = new ArrayList<>();
-
- final File directory = editDirectory.toFile();
- final File[] partitionFiles = directory.listFiles();
- if (partitionFiles == null) {
- return paths;
- }
-
- for (final File file : partitionFiles) {
- // if file is a journal file but no data has yet been
persisted, it may
- // very well be a 0-byte file (the journal is not SYNC'ed to
disk after
- // a header is written out, so it may be lost). In this case,
the journal
- // is empty, so we can just skip it.
- if (file.isDirectory() || file.length() == 0L) {
- continue;
- }
-
- if
(!JOURNAL_FILENAME_PATTERN.matcher(file.getName()).matches()) {
- continue;
- }
-
- if (isJournalFile(file)) {
- paths.add(file.toPath());
- } else {
- logger.warn("Found file {}, but could not access it, or it
was not in the expected format; "
- + "will ignore this file", file.getAbsolutePath());
- }
- }
-
- // Sort journal files by the numeric portion of the filename
- Collections.sort(paths, new Comparator<Path>() {
- @Override
- public int compare(final Path o1, final Path o2) {
- if (o1 == null && o2 == null) {
- return 0;
- }
- if (o1 == null) {
- return 1;
- }
- if (o2 == null) {
- return -1;
- }
-
- final long index1 = getJournalIndex(o1.toFile());
- final long index2 = getJournalIndex(o2.toFile());
- return Long.compare(index1, index2);
- }
- });
-
- return paths;
- }
-
- void clearOld() {
- final List<Path> oldRecoveryFiles = getRecoveryPaths();
-
- for (final Path path : oldRecoveryFiles) {
- final File file = path.toFile();
- if (file.getName().equals(currentJournalFilename)) {
- continue;
- }
- if (file.exists()) {
- file.delete();
- }
- }
- }
-
- private boolean isJournalFile(final File file) {
- final String expectedStartsWith =
MinimalLockingWriteAheadLog.class.getName();
- try {
- try (final FileInputStream fis = new FileInputStream(file);
- final InputStream bufferedIn = new
BufferedInputStream(fis);
- final DataInputStream in = new
DataInputStream(bufferedIn)) {
- final String waliImplClassName = in.readUTF();
- if (!expectedStartsWith.equals(waliImplClassName)) {
- return false;
- }
- }
- } catch (final IOException e) {
- return false;
- }
-
- return true;
- }
-
- public void update(final Collection<S> records, final long
transactionId, final Map<Object, S> recordMap, final boolean forceSync) throws
IOException {
- try (final ByteArrayOutputStream baos = new
ByteArrayOutputStream(256);
- final DataOutputStream out = new DataOutputStream(baos)) {
-
- out.writeLong(transactionId);
- final int numEditsToSerialize = records.size();
- int editsSerialized = 0;
- for (final S record : records) {
- final Object recordId = serde.getRecordIdentifier(record);
- final S previousVersion = recordMap.get(recordId);
-
- serde.serializeEdit(previousVersion, record, out);
- if (++editsSerialized < numEditsToSerialize) {
- out.write(TRANSACTION_CONTINUE);
- } else {
- out.write(TRANSACTION_COMMIT);
- }
- }
-
- out.flush();
-
- if (this.closed) {
- throw new IllegalStateException("Partition is closed");
- }
-
- baos.writeTo(dataOut);
- dataOut.flush();
-
- if (forceSync) {
- synchronized (fileOut) {
- fileOut.getFD().sync();
- }
- }
- }
- }
-
- private DataInputStream createDataInputStream(final Path path) throws
IOException {
- return new DataInputStream(new
BufferedInputStream(Files.newInputStream(path)));
- }
-
- private DataInputStream getRecoveryStream() throws IOException {
- if (recoveryIn != null && hasMoreData(recoveryIn)) {
- return recoveryIn;
- }
-
- while (true) {
- final Path nextRecoveryPath = recoveryFiles.poll();
- if (nextRecoveryPath == null) {
- return null;
- }
-
- logger.debug("{} recovering from {}", this, nextRecoveryPath);
- recoveryIn = createDataInputStream(nextRecoveryPath);
- if (hasMoreData(recoveryIn)) {
- try {
- final String waliImplementationClass =
recoveryIn.readUTF();
- if
(!MinimalLockingWriteAheadLog.class.getName().equals(waliImplementationClass)) {
- continue;
- }
-
- final long waliVersion = recoveryIn.readInt();
- if (waliVersion > writeAheadLogVersion) {
- throw new IOException("Cannot recovery from file "
+ nextRecoveryPath + " because it was written using "
- + "WALI version " + waliVersion + ", but the
version used to restore it is only " + writeAheadLogVersion);
- }
-
- final String serdeEncoding = recoveryIn.readUTF();
- this.recoveryVersion = recoveryIn.readInt();
- serde = serdeFactory.createSerDe(serdeEncoding);
-
- serde.readHeader(recoveryIn);
- break;
- } catch (final Exception e) {
- logger.warn("Failed to recover data from Write-Ahead
Log for {} because the header information could not be read properly. "
- + "This often is the result of the file not being
fully written out before the application is restarted. This file will be
ignored.", nextRecoveryPath);
- }
- }
- }
-
- return recoveryIn;
- }
-
- public Long getNextRecoverableTransactionId() throws IOException {
- while (true) {
- DataInputStream recoveryStream = getRecoveryStream();
- if (recoveryStream == null) {
- return null;
- }
-
- final long transactionId;
- try {
- transactionId = recoveryIn.readLong();
- } catch (final EOFException e) {
- continue;
- } catch (final Exception e) {
- // If the stream consists solely of NUL bytes, then we
want to treat it
- // the same as an EOF because we see this happen when we
suddenly lose power
- // while writing to a file.
- if (remainingBytesAllNul(recoveryIn)) {
- logger.warn("Failed to recover data from Write-Ahead
Log Partition because encountered trailing NUL bytes. "
- + "This will sometimes happen after a sudden power
loss. The rest of this journal file will be skipped for recovery purposes.");
- continue;
- } else {
- throw e;
- }
- }
-
- this.maxTransactionId.set(transactionId);
- return transactionId;
- }
- }
-
- /**
- * In the case of a sudden power loss, it is common - at least in a
Linux journaling File System -
- * that the partition file that is being written to will have many
trailing "NUL bytes" (0's).
- * If this happens, then on restart we want to treat this as an
incomplete transaction, so we detect
- * this case explicitly.
- *
- * @param in the input stream to scan
- * @return <code>true</code> if the InputStream contains no data or
contains only NUL bytes
- * @throws IOException if unable to read from the given InputStream
- */
- private boolean remainingBytesAllNul(final InputStream in) throws
IOException {
- int nextByte;
- while ((nextByte = in.read()) != -1) {
- if (nextByte != NUL_BYTE) {
- return false;
- }
- }
-
- return true;
- }
-
- private boolean hasMoreData(final InputStream in) throws IOException {
- in.mark(1);
- final int nextByte = in.read();
- in.reset();
- return nextByte >= 0;
- }
-
- public void endRecovery() throws IOException {
- if (recoveryIn != null) {
- recoveryIn.close();
- }
-
- final Path nextRecoveryPath = this.recoveryFiles.poll();
- if (nextRecoveryPath != null) {
- throw new IllegalStateException("Signaled to end recovery, but
there are more recovery files for Partition "
- + "in directory " + editDirectory);
- }
-
- final Path newEditPath = getNewEditPath();
-
- this.serde = serdeFactory.createSerDe(null);
- final FileOutputStream fos = new
FileOutputStream(newEditPath.toFile());
- final DataOutputStream outStream = new DataOutputStream(new
BufferedOutputStream(fos));
- outStream.writeUTF(MinimalLockingWriteAheadLog.class.getName());
- outStream.writeInt(writeAheadLogVersion);
- outStream.writeUTF(serde.getClass().getName());
- outStream.writeInt(serde.getVersion());
- serde.writeHeader(outStream);
-
- outStream.flush();
- dataOut = outStream;
- fileOut = fos;
- }
-
- public Set<Object> recoverNextTransaction(final Map<Object, S>
currentRecordMap, final Map<Object, S> updatedRecordMap, final Set<String>
swapLocations) throws IOException {
- final Set<Object> idsRemoved = new HashSet<>();
-
- int transactionFlag;
- do {
- final S record;
- try {
- record = serde.deserializeEdit(recoveryIn,
currentRecordMap, recoveryVersion);
- if (record == null) {
- throw new EOFException();
- }
- } catch (final EOFException eof) {
- throw eof;
- } catch (final Exception e) {
- // If the stream consists solely of NUL bytes, then we
want to treat it
- // the same as an EOF because we see this happen when we
suddenly lose power
- // while writing to a file. We also have logic already in
the caller of this
- // method to properly handle EOFException's, so we will
simply throw an EOFException
- // ourselves. However, if that is not the case, then
something else has gone wrong.
- // In such a case, there is not much that we can do. If we
simply skip over the transaction,
- // then the transaction may be indicating that a new
attribute was added or changed. Or the
- // content of the FlowFile changed. A subsequent
transaction for the same FlowFile may then
- // update the connection that is holding the FlowFile. In
this case, if we simply skip over
- // the transaction, we end up with a FlowFile in a queue
that has the wrong attributes or
- // content, and that can result in some very bad behavior
- even security vulnerabilities if
- // a Route processor, for instance, routes incorrectly due
to a missing attribute or content
- // is pointing to a previous claim where sensitive values
have not been removed, etc. So
- // instead of attempting to skip the transaction and move
on, we instead just throw the Exception
- // indicating that the write-ahead log is corrupt and
allow the user to handle it as he/she sees
- // fit (likely this will result in deleting the repo, but
it's possible that it could be repaired
- // manually or through some sort of script).
- if (remainingBytesAllNul(recoveryIn)) {
- final EOFException eof = new EOFException("Failed to
recover data from Write-Ahead Log Partition because encountered trailing NUL
bytes. "
- + "This will sometimes happen after a sudden power
loss. The rest of this journal file will be skipped for recovery purposes.");
- eof.addSuppressed(e);
- throw eof;
- } else {
- throw e;
- }
- }
-
-
- if (logger.isDebugEnabled()) {
- logger.debug("{} Recovering Transaction {}: {}", new
Object[] { this, maxTransactionId.get(), record });
- }
-
- final Object recordId = serde.getRecordIdentifier(record);
- final UpdateType updateType = serde.getUpdateType(record);
- if (updateType == UpdateType.DELETE) {
- updatedRecordMap.remove(recordId);
- idsRemoved.add(recordId);
- } else if (updateType == UpdateType.SWAP_IN) {
- final String location = serde.getLocation(record);
- if (location == null) {
- logger.error("Recovered SWAP_IN record from edit log,
but it did not contain a Location; skipping record");
- } else {
- swapLocations.remove(location);
- updatedRecordMap.put(recordId, record);
- idsRemoved.remove(recordId);
- }
- } else if (updateType == UpdateType.SWAP_OUT) {
- final String location = serde.getLocation(record);
- if (location == null) {
- logger.error("Recovered SWAP_OUT record from edit log,
but it did not contain a Location; skipping record");
- } else {
- swapLocations.add(location);
- updatedRecordMap.remove(recordId);
- idsRemoved.add(recordId);
- }
- } else {
- updatedRecordMap.put(recordId, record);
- idsRemoved.remove(recordId);
- }
-
- transactionFlag = recoveryIn.read();
- } while (transactionFlag != TRANSACTION_COMMIT);
-
- return idsRemoved;
- }
-
- /**
- * Must be called after recovery has finished
- *
- * @return max recovered transaction id
- */
- public long getMaxRecoveredTransactionId() {
- return maxTransactionId.get();
- }
-
- @Override
- public String toString() {
- return description;
- }
- }
-}
diff --git
a/nifi-commons/nifi-write-ahead-log/src/test/java/org/wali/TestMinimalLockingWriteAheadLog.java
b/nifi-commons/nifi-write-ahead-log/src/test/java/org/wali/TestMinimalLockingWriteAheadLog.java
deleted file mode 100644
index 7954553110..0000000000
---
a/nifi-commons/nifi-write-ahead-log/src/test/java/org/wali/TestMinimalLockingWriteAheadLog.java
+++ /dev/null
@@ -1,977 +0,0 @@
-/*
- * 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.wali;
-
-import org.junit.jupiter.api.Disabled;
-import org.junit.jupiter.api.Test;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.io.BufferedInputStream;
-import java.io.DataInputStream;
-import java.io.DataOutputStream;
-import java.io.EOFException;
-import java.io.File;
-import java.io.FileFilter;
-import java.io.FileInputStream;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.text.NumberFormat;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
-import java.util.Random;
-import java.util.SortedSet;
-import java.util.TreeSet;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.concurrent.atomic.AtomicInteger;
-import java.util.concurrent.atomic.AtomicReference;
-
-import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertFalse;
-import static org.junit.jupiter.api.Assertions.assertThrows;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
-@SuppressWarnings("deprecation")
-public class TestMinimalLockingWriteAheadLog {
- private static final Logger logger =
LoggerFactory.getLogger(TestMinimalLockingWriteAheadLog.class);
-
-
- @Test
- public void testTruncatedPartitionHeader() throws IOException {
- final int numPartitions = 4;
-
- final Path path = Paths.get("target/testTruncatedPartitionHeader");
- deleteRecursively(path.toFile());
- assertTrue(path.toFile().mkdirs());
-
- final AtomicInteger counter = new AtomicInteger(0);
- final SerDe<Object> serde = new SerDe<Object>() {
- @Override
- public void readHeader(DataInputStream in) throws IOException {
- if (counter.getAndIncrement() == 1) {
- throw new EOFException("Intentionally thrown for unit
test");
- }
- }
-
- @Override
- public void serializeEdit(Object previousRecordState, Object
newRecordState, DataOutputStream out) throws IOException {
- out.write(1);
- }
-
- @Override
- public void serializeRecord(Object record, DataOutputStream out)
throws IOException {
- out.write(1);
- }
-
- @Override
- public Object deserializeEdit(DataInputStream in, Map<Object,
Object> currentRecordStates, int version) throws IOException {
- final int val = in.read();
- return (val == 1) ? new Object() : null;
- }
-
- @Override
- public Object deserializeRecord(DataInputStream in, int version)
throws IOException {
- final int val = in.read();
- return (val == 1) ? new Object() : null;
- }
-
- @Override
- public Object getRecordIdentifier(Object record) {
- return 1;
- }
-
- @Override
- public UpdateType getUpdateType(Object record) {
- return UpdateType.CREATE;
- }
-
- @Override
- public String getLocation(Object record) {
- return null;
- }
-
- @Override
- public int getVersion() {
- return 0;
- }
- };
-
- final WriteAheadRepository<Object> repo = new
MinimalLockingWriteAheadLog<>(path, numPartitions, serde, null);
- try {
- final Collection<Object> initialRecs = repo.recoverRecords();
- assertTrue(initialRecs.isEmpty());
-
- repo.update(Collections.singletonList(new Object()), false);
- repo.update(Collections.singletonList(new Object()), false);
- repo.update(Collections.singletonList(new Object()), false);
- } finally {
- repo.shutdown();
- }
-
- final WriteAheadRepository<Object> secondRepo = new
MinimalLockingWriteAheadLog<>(path, numPartitions, serde, null);
- try {
- secondRepo.recoverRecords();
- } finally {
- secondRepo.shutdown();
- }
- }
-
- @Test
- @Disabled("For manual performance testing")
- public void testUpdatePerformance() throws IOException,
InterruptedException {
- final int numPartitions = 16;
-
- final Path path = Paths.get("target/minimal-locking-repo");
- deleteRecursively(path.toFile());
- assertTrue(path.toFile().mkdirs());
-
- final DummyRecordSerde serde = new DummyRecordSerde();
- final WriteAheadRepository<DummyRecord> repo = new
MinimalLockingWriteAheadLog<>(path, numPartitions, serde, null);
- final Collection<DummyRecord> initialRecs = repo.recoverRecords();
- assertTrue(initialRecs.isEmpty());
-
- final long updateCountPerThread = 1_000_000;
- final int numThreads = 4;
-
- final Thread[] threads = new Thread[numThreads];
-
- final int batchSize = 1;
-
- long previousBytes = 0;
-
- for (int j = 0; j < 2; j++) {
- for (int i = 0; i < numThreads; i++) {
- final Thread t = new Thread(() -> {
- final List<DummyRecord> batch = new ArrayList<>();
-
- for (int i1 = 0; i1 < updateCountPerThread / batchSize;
i1++) {
- batch.clear();
- for (int j1 = 0; j1 < batchSize; j1++) {
- final DummyRecord record = new
DummyRecord(String.valueOf(i1), UpdateType.CREATE);
- batch.add(record);
- }
-
- assertDoesNotThrow(() -> repo.update(batch, false));
- }
- });
-
- threads[i] = t;
- }
-
- final long start = System.nanoTime();
- for (final Thread t : threads) {
- t.start();
- }
- for (final Thread t : threads) {
- t.join();
- }
-
- long bytes = 0L;
- for (final File file : path.toFile().listFiles()) {
- if (file.getName().startsWith("partition-")) {
- for (final File journalFile : file.listFiles()) {
- bytes += journalFile.length();
- }
- }
- }
-
- bytes -= previousBytes;
- previousBytes = bytes;
-
- final long millis =
TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start);
- final long eventsPerSecond = (updateCountPerThread * numThreads *
1000) / millis;
- final String eps =
NumberFormat.getInstance().format(eventsPerSecond);
- final long bytesPerSecond = bytes * 1000 / millis;
- final String bps =
NumberFormat.getInstance().format(bytesPerSecond);
-
- if (j == 0) {
- System.out.println(millis + " ms to insert " +
updateCountPerThread * numThreads + " updates using " + numThreads + " threads,
*as a warmup!* "
- + eps + " events per second, " + bps + " bytes per
second");
- } else {
- System.out.println(millis + " ms to insert " +
updateCountPerThread * numThreads + " updates using " + numThreads + " threads,
"
- + eps + " events per second, " + bps + " bytes per
second");
- }
- }
- }
-
-
-
- @Test
- public void testRepoDoesntContinuallyGrowOnOutOfMemoryError() throws
IOException, InterruptedException {
- final int numPartitions = 8;
-
- final Path path = Paths.get("target/minimal-locking-repo");
- deleteRecursively(path.toFile());
- assertTrue(path.toFile().mkdirs());
-
- final DummyRecordSerde serde = new DummyRecordSerde();
- final WriteAheadRepository<DummyRecord> repo = new
MinimalLockingWriteAheadLog<>(path, numPartitions, serde, null);
- try {
- final Collection<DummyRecord> initialRecs = repo.recoverRecords();
- assertTrue(initialRecs.isEmpty());
-
- serde.setThrowOOMEAfterNSerializeEdits(100);
- for (int i = 0; i < 108; i++) {
- try {
- final DummyRecord record = new
DummyRecord(String.valueOf(i), UpdateType.CREATE);
- repo.update(Collections.singleton(record), false);
- } catch (final OutOfMemoryError oome) {
- logger.info("Received OOME on record " + i);
- }
- }
-
- long expectedSize = sizeOf(path.toFile());
- for (int i = 0; i < 1000; i++) {
- final DummyRecord record = new DummyRecord(String.valueOf(i),
UpdateType.CREATE);
- assertThrows(IOException.class, () ->
repo.update(Collections.singleton(record), false));
- }
-
- long newSize = sizeOf(path.toFile());
- assertEquals(expectedSize, newSize);
-
- assertThrows(OutOfMemoryError.class, () -> repo.checkpoint());
-
- expectedSize = sizeOf(path.toFile());
-
- for (int i = 0; i < 100000; i++) {
- final DummyRecord record = new DummyRecord(String.valueOf(i),
UpdateType.CREATE);
- assertThrows(IOException.class, () ->
repo.update(Collections.singleton(record), false));
- }
-
- newSize = sizeOf(path.toFile());
- assertEquals(expectedSize, newSize);
- } finally {
- repo.shutdown();
- }
- }
-
- /**
- * This test is intended to continually update the Write-ahead log using
many threads, then
- * stop and restore the repository to check for any corruption. There were
reports of potential threading
- * issues leading to repository corruption. This was an attempt to
replicate. It should not be run as a
- * unit test, really, but will be left, as it can be valuable to exercise
the implementation
- *
- * @throws IOException if unable to read from/write to the write-ahead log
- * @throws InterruptedException if a thread is interrupted
- */
- @Test
- @Disabled
- public void tryToCauseThreadingIssue() throws IOException,
InterruptedException {
- System.setProperty("org.slf4j.simpleLogger.log.org.wali", "INFO");
-
- final int numThreads = 12;
- final long iterationsPerThread = 1000000;
- final int numAttempts = 1000;
-
- final Path path = Paths.get("D:/dummy/minimal-locking-repo");
- path.toFile().mkdirs();
-
- final AtomicReference<WriteAheadRepository<DummyRecord>> writeRepoRef
= new AtomicReference<>();
- final AtomicBoolean checkpointing = new AtomicBoolean(false);
-
- final Thread bgThread = new Thread(() -> {
- while (true) {
- checkpointing.set(true);
-
- final WriteAheadRepository<DummyRecord> repo =
writeRepoRef.get();
- if (repo != null) {
- assertDoesNotThrow(() -> repo.checkpoint());
- }
-
- checkpointing.set(false);
-
- try {
- TimeUnit.SECONDS.sleep(5);
- } catch (InterruptedException e) {
- }
- }
- });
- bgThread.setDaemon(true);
- bgThread.start();
-
- for (int x = 0; x < numAttempts; x++) {
- final DummyRecordSerde serde = new DummyRecordSerde();
- final WriteAheadRepository<DummyRecord> writeRepo = new
MinimalLockingWriteAheadLog<>(path, 256, serde, null);
- final Collection<DummyRecord> writeRecords =
writeRepo.recoverRecords();
- for (final DummyRecord record : writeRecords) {
- assertEquals("B", record.getProperty("A"));
- }
-
- writeRepoRef.set(writeRepo);
-
- final Thread[] threads = new Thread[numThreads];
- for (int i = 0; i < numThreads; i++) {
- final Thread t = new
InlineCreationInsertThread(iterationsPerThread, writeRepo);
- t.start();
- threads[i] = t;
- }
-
- for (final Thread t : threads) {
- t.join();
- }
-
- writeRepoRef.set(null);
- writeRepo.shutdown();
-
- boolean cp = checkpointing.get();
- while (cp) {
- Thread.sleep(100L);
- cp = checkpointing.get();
- }
-
- final WriteAheadRepository<DummyRecord> readRepo = new
MinimalLockingWriteAheadLog<>(path, 256, serde, null);
- // ensure that we are able to recover the records properly
- final Collection<DummyRecord> readRecords =
readRepo.recoverRecords();
- for (final DummyRecord record : readRecords) {
- assertEquals("B", record.getProperty("A"));
- }
- readRepo.shutdown();
- }
- }
-
- @Test
- public void testWrite() throws IOException, InterruptedException {
- final int numPartitions = 8;
-
- final Path path = Paths.get("target/minimal-locking-repo");
- deleteRecursively(path.toFile());
- assertTrue(path.toFile().mkdirs());
-
- final DummyRecordSerde serde = new DummyRecordSerde();
- final WriteAheadRepository<DummyRecord> repo = new
MinimalLockingWriteAheadLog<>(path, numPartitions, serde, null);
- final Collection<DummyRecord> initialRecs = repo.recoverRecords();
- assertTrue(initialRecs.isEmpty());
-
- final List<InsertThread> threads = new ArrayList<>();
- for (int i = 0; i < 10; i++) {
- threads.add(new InsertThread(10000, 1000000 * i, repo));
- }
-
- final long start = System.nanoTime();
- for (final InsertThread thread : threads) {
- thread.start();
- }
- for (final InsertThread thread : threads) {
- thread.join();
- }
- final long nanos = System.nanoTime() - start;
- final long millis = TimeUnit.MILLISECONDS.convert(nanos,
TimeUnit.NANOSECONDS);
- System.out.println("Took " + millis + " millis to insert 1,000,000
records each in its own transaction");
- repo.shutdown();
-
- final WriteAheadRepository<DummyRecord> recoverRepo = new
MinimalLockingWriteAheadLog<>(path, numPartitions, serde, null);
- final Collection<DummyRecord> recoveredRecords =
recoverRepo.recoverRecords();
- assertFalse(recoveredRecords.isEmpty());
- assertEquals(100000, recoveredRecords.size());
- for (final DummyRecord record : recoveredRecords) {
- final Map<String, String> recoveredProps = record.getProperties();
- assertEquals(1, recoveredProps.size());
- assertEquals("B", recoveredProps.get("A"));
- }
- }
-
- @Test
- public void testRecoverAfterIOException() throws IOException {
- final int numPartitions = 5;
- final Path path =
Paths.get("target/minimal-locking-repo-test-recover-after-ioe");
- deleteRecursively(path.toFile());
- Files.createDirectories(path);
-
- final DummyRecordSerde serde = new DummyRecordSerde();
- final WriteAheadRepository<DummyRecord> repo = new
MinimalLockingWriteAheadLog<>(path, numPartitions, serde, null);
- final Collection<DummyRecord> initialRecs = repo.recoverRecords();
- assertTrue(initialRecs.isEmpty());
-
- serde.setThrowIOEAfterNSerializeEdits(7); // serialize the 2
transactions, then the first edit of the third transaction; then throw
IOException
-
- final List<DummyRecord> firstTransaction = new ArrayList<>();
- firstTransaction.add(new DummyRecord("1", UpdateType.CREATE));
- firstTransaction.add(new DummyRecord("2", UpdateType.CREATE));
- firstTransaction.add(new DummyRecord("3", UpdateType.CREATE));
-
- final List<DummyRecord> secondTransaction = new ArrayList<>();
- secondTransaction.add(new DummyRecord("1",
UpdateType.UPDATE).setProperty("abc", "123"));
- secondTransaction.add(new DummyRecord("2",
UpdateType.UPDATE).setProperty("cba", "123"));
- secondTransaction.add(new DummyRecord("3",
UpdateType.UPDATE).setProperty("aaa", "123"));
-
- final List<DummyRecord> thirdTransaction = new ArrayList<>();
- thirdTransaction.add(new DummyRecord("1", UpdateType.DELETE));
- thirdTransaction.add(new DummyRecord("2", UpdateType.DELETE));
-
- repo.update(firstTransaction, true);
- repo.update(secondTransaction, true);
- assertThrows(IOException.class, () -> repo.update(thirdTransaction,
true));
-
- repo.shutdown();
-
- serde.setThrowIOEAfterNSerializeEdits(-1);
- final WriteAheadRepository<DummyRecord> recoverRepo = new
MinimalLockingWriteAheadLog<>(path, numPartitions, serde, null);
- final Collection<DummyRecord> recoveredRecords =
recoverRepo.recoverRecords();
- assertFalse(recoveredRecords.isEmpty());
- assertEquals(3, recoveredRecords.size());
-
- boolean record1 = false, record2 = false, record3 = false;
- for (final DummyRecord record : recoveredRecords) {
- switch (record.getId()) {
- case "1":
- record1 = true;
- assertEquals("123", record.getProperty("abc"));
- break;
- case "2":
- record2 = true;
- assertEquals("123", record.getProperty("cba"));
- break;
- case "3":
- record3 = true;
- assertEquals("123", record.getProperty("aaa"));
- break;
- }
- }
-
- assertTrue(record1);
- assertTrue(record2);
- assertTrue(record3);
- }
-
-
- @Test
- public void testRecoverFileThatHasTrailingNULBytesAndTruncation() throws
IOException {
- final int numPartitions = 5;
- final Path path =
Paths.get("target/testRecoverFileThatHasTrailingNULBytesAndTruncation");
- deleteRecursively(path.toFile());
- Files.createDirectories(path);
-
- final DummyRecordSerde serde = new DummyRecordSerde();
- final WriteAheadRepository<DummyRecord> repo = new
MinimalLockingWriteAheadLog<>(path, numPartitions, serde, null);
- final Collection<DummyRecord> initialRecs = repo.recoverRecords();
- assertTrue(initialRecs.isEmpty());
-
- final List<DummyRecord> firstTransaction = new ArrayList<>();
- firstTransaction.add(new DummyRecord("1", UpdateType.CREATE));
- firstTransaction.add(new DummyRecord("2", UpdateType.CREATE));
- firstTransaction.add(new DummyRecord("3", UpdateType.CREATE));
-
- final List<DummyRecord> secondTransaction = new ArrayList<>();
- secondTransaction.add(new DummyRecord("1",
UpdateType.UPDATE).setProperty("abc", "123"));
- secondTransaction.add(new DummyRecord("2",
UpdateType.UPDATE).setProperty("cba", "123"));
- secondTransaction.add(new DummyRecord("3",
UpdateType.UPDATE).setProperty("aaa", "123"));
-
- final List<DummyRecord> thirdTransaction = new ArrayList<>();
- thirdTransaction.add(new DummyRecord("1", UpdateType.DELETE));
- thirdTransaction.add(new DummyRecord("2", UpdateType.DELETE));
-
- repo.update(firstTransaction, true);
- repo.update(secondTransaction, true);
- repo.update(thirdTransaction, true);
-
- repo.shutdown();
-
- final File partition3Dir = path.resolve("partition-2").toFile();
- final File journalFile = partition3Dir.listFiles()[0];
- final byte[] contents = Files.readAllBytes(journalFile.toPath());
-
- // Truncate the contents of the journal file by 8 bytes. Then replace
with 28 trailing NUL bytes,
- // as this is what we often see when we have a sudden power loss.
- final byte[] truncated = Arrays.copyOfRange(contents, 0,
contents.length - 8);
- final byte[] withNuls = new byte[truncated.length + 28];
- System.arraycopy(truncated, 0, withNuls, 0, truncated.length);
-
- try (final OutputStream fos = new FileOutputStream(journalFile)) {
- fos.write(withNuls);
- }
-
- final WriteAheadRepository<DummyRecord> recoverRepo = new
MinimalLockingWriteAheadLog<>(path, numPartitions, serde, null);
- final Collection<DummyRecord> recoveredRecords =
recoverRepo.recoverRecords();
- assertFalse(recoveredRecords.isEmpty());
- assertEquals(3, recoveredRecords.size());
-
- boolean record1 = false, record2 = false, record3 = false;
- for (final DummyRecord record : recoveredRecords) {
- switch (record.getId()) {
- case "1":
- record1 = true;
- assertEquals("123", record.getProperty("abc"));
- break;
- case "2":
- record2 = true;
- assertEquals("123", record.getProperty("cba"));
- break;
- case "3":
- record3 = true;
- assertEquals("123", record.getProperty("aaa"));
- break;
- }
- }
-
- assertTrue(record1);
- assertTrue(record2);
- assertTrue(record3);
- }
-
- @Test
- public void testRecoverFileThatHasTrailingNULBytesNoTruncation() throws
IOException {
- final int numPartitions = 5;
- final Path path =
Paths.get("target/testRecoverFileThatHasTrailingNULBytesNoTruncation");
- deleteRecursively(path.toFile());
- Files.createDirectories(path);
-
- final DummyRecordSerde serde = new DummyRecordSerde();
- final WriteAheadRepository<DummyRecord> repo = new
MinimalLockingWriteAheadLog<>(path, numPartitions, serde, null);
- final Collection<DummyRecord> initialRecs = repo.recoverRecords();
- assertTrue(initialRecs.isEmpty());
-
- final List<DummyRecord> firstTransaction = new ArrayList<>();
- firstTransaction.add(new DummyRecord("1", UpdateType.CREATE));
- firstTransaction.add(new DummyRecord("2", UpdateType.CREATE));
- firstTransaction.add(new DummyRecord("3", UpdateType.CREATE));
-
- final List<DummyRecord> secondTransaction = new ArrayList<>();
- secondTransaction.add(new DummyRecord("1",
UpdateType.UPDATE).setProperty("abc", "123"));
- secondTransaction.add(new DummyRecord("2",
UpdateType.UPDATE).setProperty("cba", "123"));
- secondTransaction.add(new DummyRecord("3",
UpdateType.UPDATE).setProperty("aaa", "123"));
-
- final List<DummyRecord> thirdTransaction = new ArrayList<>();
- thirdTransaction.add(new DummyRecord("1", UpdateType.DELETE));
- thirdTransaction.add(new DummyRecord("2", UpdateType.DELETE));
-
- repo.update(firstTransaction, true);
- repo.update(secondTransaction, true);
- repo.update(thirdTransaction, true);
-
- repo.shutdown();
-
- final File partition3Dir = path.resolve("partition-2").toFile();
- final File journalFile = partition3Dir.listFiles()[0];
-
- // Truncate the contents of the journal file by 8 bytes. Then replace
with 28 trailing NUL bytes,
- // as this is what we often see when we have a sudden power loss.
- final byte[] withNuls = new byte[28];
-
- try (final OutputStream fos = new FileOutputStream(journalFile, true))
{
- fos.write(withNuls);
- }
-
- final WriteAheadRepository<DummyRecord> recoverRepo = new
MinimalLockingWriteAheadLog<>(path, numPartitions, serde, null);
- final Collection<DummyRecord> recoveredRecords =
recoverRepo.recoverRecords();
- assertFalse(recoveredRecords.isEmpty());
- assertEquals(1, recoveredRecords.size());
-
- boolean record1 = false, record2 = false, record3 = false;
- for (final DummyRecord record : recoveredRecords) {
- switch (record.getId()) {
- case "1":
- record1 = record.getUpdateType() != UpdateType.DELETE;
- assertEquals("123", record.getProperty("abc"));
- break;
- case "2":
- record2 = record.getUpdateType() != UpdateType.DELETE;
- assertEquals("123", record.getProperty("cba"));
- break;
- case "3":
- record3 = true;
- assertEquals("123", record.getProperty("aaa"));
- break;
- }
- }
-
- assertFalse(record1);
- assertFalse(record2);
- assertTrue(record3);
- }
-
- @Test
- public void testCannotModifyLogAfterAllAreBlackListed() throws IOException
{
- final int numPartitions = 5;
- final Path path =
Paths.get("target/minimal-locking-repo-test-cannot-modify-after-all-blacklisted");
- deleteRecursively(path.toFile());
- Files.createDirectories(path);
-
- final DummyRecordSerde serde = new DummyRecordSerde();
- final WriteAheadRepository<DummyRecord> repo = new
MinimalLockingWriteAheadLog<>(path, numPartitions, serde, null);
- final Collection<DummyRecord> initialRecs = repo.recoverRecords();
- assertTrue(initialRecs.isEmpty());
-
- serde.setThrowIOEAfterNSerializeEdits(3); // serialize the first
transaction, then fail on all subsequent transactions
-
- final List<DummyRecord> firstTransaction = new ArrayList<>();
- firstTransaction.add(new DummyRecord("1", UpdateType.CREATE));
- firstTransaction.add(new DummyRecord("2", UpdateType.CREATE));
- firstTransaction.add(new DummyRecord("3", UpdateType.CREATE));
-
- final List<DummyRecord> secondTransaction = new ArrayList<>();
- secondTransaction.add(new DummyRecord("1",
UpdateType.UPDATE).setProperty("abc", "123"));
- secondTransaction.add(new DummyRecord("2",
UpdateType.UPDATE).setProperty("cba", "123"));
- secondTransaction.add(new DummyRecord("3",
UpdateType.UPDATE).setProperty("aaa", "123"));
-
- final List<DummyRecord> thirdTransaction = new ArrayList<>();
- thirdTransaction.add(new DummyRecord("1", UpdateType.DELETE));
- thirdTransaction.add(new DummyRecord("2", UpdateType.DELETE));
-
- repo.update(firstTransaction, true);
-
- assertThrows(IOException.class, () -> repo.update(secondTransaction,
true));
-
- for (int i = 0; i < 4; i++) {
- assertThrows(IOException.class, () ->
repo.update(thirdTransaction, true));
- }
-
- serde.setThrowIOEAfterNSerializeEdits(-1);
- final List<DummyRecord> fourthTransaction = new ArrayList<>();
- fourthTransaction.add(new DummyRecord("1", UpdateType.DELETE));
-
- IOException e = assertThrows(IOException.class, () ->
repo.update(fourthTransaction, true));
- assertTrue(e.getMessage().contains("All Partitions have been
blacklisted"));
-
- repo.shutdown();
- serde.setThrowIOEAfterNSerializeEdits(-1);
-
- final WriteAheadRepository<DummyRecord> recoverRepo = new
MinimalLockingWriteAheadLog<>(path, numPartitions, serde, null);
- final Collection<DummyRecord> recoveredRecords =
recoverRepo.recoverRecords();
- assertFalse(recoveredRecords.isEmpty());
- assertEquals(3, recoveredRecords.size());
- }
-
- @Test
- public void testStriping() throws IOException {
- final int numPartitions = 6;
- final Path path = Paths.get("target/minimal-locking-repo-striped");
- deleteRecursively(path.toFile());
- Files.createDirectories(path);
-
- final SortedSet<Path> paths = new TreeSet<>();
- paths.add(path.resolve("stripe-1"));
- paths.add(path.resolve("stripe-2"));
-
- final DummyRecordSerde serde = new DummyRecordSerde();
- final WriteAheadRepository<DummyRecord> repo = new
MinimalLockingWriteAheadLog<>(paths, numPartitions, serde, null);
- final Collection<DummyRecord> initialRecs = repo.recoverRecords();
- assertTrue(initialRecs.isEmpty());
-
- final InsertThread inserter = new InsertThread(100000, 0, repo);
- inserter.run();
-
- for (final Path partitionPath : paths) {
- final File[] files = partitionPath.toFile().listFiles(new
FileFilter() {
- @Override
- public boolean accept(File pathname) {
- return pathname.getName().startsWith("partition");
- }
- });
- assertEquals(3, files.length);
-
- for (final File file : files) {
- final File[] journalFiles = file.listFiles();
- assertEquals(1, journalFiles.length);
- }
- }
-
- repo.checkpoint();
-
- }
-
- @Test
- public void testShutdownWhileBlacklisted() throws IOException {
- final Path path =
Paths.get("target/minimal-locking-repo-shutdown-blacklisted");
- deleteRecursively(path.toFile());
- Files.createDirectories(path);
-
- final SerDe<SimpleRecord> failOnThirdWriteSerde = new
SerDe<SimpleRecord>() {
- private int writes = 0;
-
- @Override
- public void serializeEdit(SimpleRecord previousRecordState,
SimpleRecord newRecordState, DataOutputStream out) throws IOException {
- serializeRecord(newRecordState, out);
- }
-
- @Override
- public void serializeRecord(SimpleRecord record, DataOutputStream
out) throws IOException {
- int size = (int) record.getSize();
- out.writeLong(record.getSize());
-
- for (int i = 0; i < size; i++) {
- out.write('A');
- }
-
- if (++writes == 3) {
- throw new IOException("Intentional Exception for Unit
Testing");
- }
-
- out.writeLong(record.getId());
- }
-
- @Override
- public SimpleRecord deserializeEdit(DataInputStream in,
Map<Object, SimpleRecord> currentRecordStates, int version) throws IOException {
- return deserializeRecord(in, version);
- }
-
- @Override
- public SimpleRecord deserializeRecord(DataInputStream in, int
version) throws IOException {
- long size = in.readLong();
-
- for (int i = 0; i < (int) size; i++) {
- in.read();
- }
-
- long id = in.readLong();
- return new SimpleRecord(id, size);
- }
-
- @Override
- public Object getRecordIdentifier(SimpleRecord record) {
- return record.getId();
- }
-
- @Override
- public UpdateType getUpdateType(SimpleRecord record) {
- return UpdateType.CREATE;
- }
-
- @Override
- public String getLocation(SimpleRecord record) {
- return null;
- }
-
- @Override
- public int getVersion() {
- return 0;
- }
- };
-
- final WriteAheadRepository<SimpleRecord> writeRepo = new
MinimalLockingWriteAheadLog<>(path, 1, failOnThirdWriteSerde, null);
- final Collection<SimpleRecord> initialRecs =
writeRepo.recoverRecords();
- assertTrue(initialRecs.isEmpty());
-
-
- writeRepo.update(Collections.singleton(new SimpleRecord(1L, 1L)),
false);
- writeRepo.update(Collections.singleton(new SimpleRecord(2L, 2L)),
false);
- // Use a size of 8194 because the BufferedOutputStream has a buffer
size of 8192 and we want
- // to exceed this for testing purposes.
- assertThrows(IOException.class,
- () -> writeRepo.update(Collections.singleton(new
SimpleRecord(3L, 8194L)), false));
-
- final Path partitionDir = path.resolve("partition-0");
- final File journalFile = partitionDir.toFile().listFiles()[0];
- final long journalFileSize = journalFile.length();
- verifyBlacklistedJournalContents(journalFile, failOnThirdWriteSerde);
-
- writeRepo.shutdown();
-
- // Ensure that calling shutdown() didn't write anything to the journal
file
- final long newJournalSize = journalFile.length();
- assertEquals(newJournalSize, journalFile.length(), "Calling Shutdown
wrote " + (newJournalSize - journalFileSize) + " bytes to the journal file");
- }
-
- private void verifyBlacklistedJournalContents(final File journalFile,
final SerDe<?> serde) throws IOException {
- try (final FileInputStream fis = new FileInputStream(journalFile);
- final InputStream bis = new BufferedInputStream(fis);
- final DataInputStream in = new DataInputStream(bis)) {
-
- // Verify header info.
- final String waliClassName = in.readUTF();
- assertEquals(MinimalLockingWriteAheadLog.class.getName(),
waliClassName);
-
- final int waliVersion = in.readInt();
- assertTrue(waliVersion > 0);
-
- final String serdeClassName = in.readUTF();
- assertEquals(serde.getClass().getName(), serdeClassName);
-
- final int serdeVersion = in.readInt();
- assertEquals(serde.getVersion(), serdeVersion);
-
- for (int i = 0; i < 2; i++) {
- long transactionId = in.readLong();
- assertEquals(i, transactionId);
-
- // read what serde wrote
- long size = in.readLong();
-
- assertEquals((i + 1), size);
-
- for (int j = 0; j < (int) size; j++) {
- final int c = in.read();
- assertEquals('A', c);
- }
-
- long id = in.readLong();
- assertEquals((i + 1), id);
-
- int transactionIndicator = in.read();
- assertEquals(2, transactionIndicator);
- }
-
- // In previous implementations, we would still have a partial
record written out.
- // In the current version, however, the serde above would result
in the data serialization
- // failing and as a result no data would be written to the stream,
so the stream should
- // now be out of data
- final int nextByte = in.read();
- assertEquals(-1, nextByte);
- }
- }
-
-
-
- @Test
- public void testDecreaseNumberOfPartitions() throws IOException {
- final Path path =
Paths.get("target/minimal-locking-repo-decrease-partitions");
- deleteRecursively(path.toFile());
- Files.createDirectories(path);
-
- final DummyRecordSerde serde = new DummyRecordSerde();
- final WriteAheadRepository<DummyRecord> writeRepo = new
MinimalLockingWriteAheadLog<>(path, 256, serde, null);
- final Collection<DummyRecord> initialRecs = writeRepo.recoverRecords();
- assertTrue(initialRecs.isEmpty());
-
- final DummyRecord record1 = new DummyRecord("1", UpdateType.CREATE);
- writeRepo.update(Collections.singleton(record1), false);
-
- for (int i=0; i < 8; i++) {
- final DummyRecord r = new DummyRecord("1", UpdateType.UPDATE);
- r.setProperty("i", String.valueOf(i));
- writeRepo.update(Collections.singleton(r), false);
- }
-
- writeRepo.shutdown();
-
- final WriteAheadRepository<DummyRecord> recoverRepo = new
MinimalLockingWriteAheadLog<>(path, 6, serde, null);
- final Collection<DummyRecord> records = recoverRepo.recoverRecords();
- final List<DummyRecord> list = new ArrayList<>(records);
- assertEquals(1, list.size());
-
- final DummyRecord recoveredRecord = list.get(0);
- assertEquals("1", recoveredRecord.getId());
- assertEquals("7",recoveredRecord.getProperty("i"));
- }
-
-
- private static class InsertThread extends Thread {
-
- private final List<List<DummyRecord>> records;
- private final WriteAheadRepository<DummyRecord> repo;
-
- public InsertThread(final int numInsertions, final int startIndex,
final WriteAheadRepository<DummyRecord> repo) {
- records = new ArrayList<>();
- for (int i = 0; i < numInsertions; i++) {
- final DummyRecord record = new DummyRecord(String.valueOf(i +
startIndex), UpdateType.CREATE);
- record.setProperty("A", "B");
- final List<DummyRecord> list = new ArrayList<>();
- list.add(record);
- records.add(list);
- }
- this.repo = repo;
- }
-
- @Override
- public void run() {
- int counter = 0;
- for (final List<DummyRecord> list : records) {
- final boolean forceSync = (++counter == records.size());
- assertDoesNotThrow(() -> repo.update(list, forceSync));
- }
- }
- }
-
-
- private static class InlineCreationInsertThread extends Thread {
- private final long iterations;
- private final WriteAheadRepository<DummyRecord> repo;
-
- public InlineCreationInsertThread(final long numInsertions, final
WriteAheadRepository<DummyRecord> repo) {
- this.iterations = numInsertions;
- this.repo = repo;
- }
-
- @Override
- public void run() {
- final List<DummyRecord> list = new ArrayList<>(1);
- list.add(null);
- final UpdateType[] updateTypes = new UpdateType[] {
UpdateType.CREATE, UpdateType.DELETE, UpdateType.UPDATE };
- final Random random = new Random();
-
- for (long i = 0; i < iterations; i++) {
- final int updateTypeIndex = random.nextInt(updateTypes.length);
- final UpdateType updateType = updateTypes[updateTypeIndex];
-
- final DummyRecord record = new DummyRecord(String.valueOf(i),
updateType);
- record.setProperty("A", "B");
- list.set(0, record);
-
- try {
- repo.update(list, false);
- } catch (final Throwable t) {
- t.printStackTrace();
- }
- }
- }
- }
-
- private void deleteRecursively(final File file) {
- final File[] children = file.listFiles();
- if (children != null) {
- for (final File child : children) {
- deleteRecursively(child);
- }
- }
-
- file.delete();
- }
-
- private long sizeOf(final File file) {
- long size = 0L;
- if (file.isDirectory()) {
- final File[] children = file.listFiles();
- if (children != null) {
- for (final File child : children) {
- size += sizeOf(child);
- }
- }
- }
-
- size += file.length();
-
- return size;
- }
-
- static class SimpleRecord {
- private long id;
- private long size;
-
- public SimpleRecord(final long id, final long size) {
- this.id = id;
- this.size = size;
- }
-
- public long getId() {
- return id;
- }
-
- public long getSize() {
- return size;
- }
- }
-}
diff --git
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/repository/WriteAheadFlowFileRepository.java
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/repository/WriteAheadFlowFileRepository.java
index e1b78a0c82..146d990b6e 100644
---
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/repository/WriteAheadFlowFileRepository.java
+++
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/repository/WriteAheadFlowFileRepository.java
@@ -31,7 +31,6 @@ import org.apache.nifi.wali.SequentialAccessWriteAheadLog;
import org.apache.nifi.wali.SnapshotCapture;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import org.wali.MinimalLockingWriteAheadLog;
import org.wali.SyncListener;
import org.wali.WriteAheadRepository;
@@ -47,10 +46,7 @@ import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
-import java.util.Optional;
import java.util.Set;
-import java.util.SortedSet;
-import java.util.TreeSet;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
@@ -94,7 +90,6 @@ public class WriteAheadFlowFileRepository implements
FlowFileRepository, SyncLis
static final String SEQUENTIAL_ACCESS_WAL =
"org.apache.nifi.wali.SequentialAccessWriteAheadLog";
static final String ENCRYPTED_SEQUENTIAL_ACCESS_WAL =
"org.apache.nifi.wali.EncryptedSequentialAccessWriteAheadLog";
- private static final String MINIMAL_LOCKING_WALI =
"org.wali.MinimalLockingWriteAheadLog";
private static final String DEFAULT_WAL_IMPLEMENTATION =
SEQUENTIAL_ACCESS_WAL;
private static final int DEFAULT_CACHE_SIZE = 10_000_000;
@@ -110,7 +105,6 @@ public class WriteAheadFlowFileRepository implements
FlowFileRepository, SyncLis
private final long checkpointDelayMillis;
private final List<File> flowFileRepositoryPaths = new ArrayList<>();
- private final List<File> recoveryFiles = new ArrayList<>();
private final ScheduledExecutorService checkpointExecutor;
private final int maxCharactersToCache;
@@ -176,24 +170,8 @@ public class WriteAheadFlowFileRepository implements
FlowFileRepository, SyncLis
this.walImplementation = writeAheadLogImpl;
this.maxCharactersToCache =
nifiProperties.getIntegerProperty(FLOWFILE_REPO_CACHE_SIZE, DEFAULT_CACHE_SIZE);
- // We used to use one implementation (minimal locking) of the
write-ahead log, but we now want to use the other
- // (sequential access), we must address this. Since the
MinimalLockingWriteAheadLog supports multiple partitions,
- // we need to ensure that we recover records from all partitions, so
we build up a List of Files for the
- // recovery files.
- for (final String propertyName : nifiProperties.getPropertyKeys()) {
- if (propertyName.startsWith(FLOWFILE_REPOSITORY_DIRECTORY_PREFIX))
{
- final String dirName =
nifiProperties.getProperty(propertyName);
- recoveryFiles.add(new File(dirName));
- }
- }
-
- if (isSequentialAccessWAL(walImplementation)) {
- final String directoryName =
nifiProperties.getProperty(FLOWFILE_REPOSITORY_DIRECTORY_PREFIX);
- flowFileRepositoryPaths.add(new File(directoryName));
- } else {
- flowFileRepositoryPaths.addAll(recoveryFiles);
- }
-
+ final String directoryName =
nifiProperties.getProperty(FLOWFILE_REPOSITORY_DIRECTORY_PREFIX);
+ flowFileRepositoryPaths.add(new File(directoryName));
checkpointDelayMillis =
FormatUtils.getTimeDuration(nifiProperties.getFlowFileRepositoryCheckpointInterval(),
TimeUnit.MILLISECONDS);
@@ -207,16 +185,6 @@ public class WriteAheadFlowFileRepository implements
FlowFileRepository, SyncLis
});
}
- /**
- * Returns true if the provided implementation is a sequential access
write ahead log (plaintext or encrypted).
- *
- * @param walImplementation the implementation to check
- * @return true if this implementation is sequential access
- */
- private static boolean isSequentialAccessWAL(String walImplementation) {
- return walImplementation.equals(SEQUENTIAL_ACCESS_WAL) ||
walImplementation.equals(ENCRYPTED_SEQUENTIAL_ACCESS_WAL);
- }
-
@Override
public void initialize(final ResourceClaimManager claimManager) throws
IOException {
final FieldCache fieldCache = new
CaffeineFieldCache(maxCharactersToCache);
@@ -251,15 +219,9 @@ public class WriteAheadFlowFileRepository implements
FlowFileRepository, SyncLis
this.serdeFactory = serdeFactory;
// The specified implementation can be plaintext or encrypted; the
only difference is the serde factory
- if (isSequentialAccessWAL(walImplementation)) {
+ if (walImplementation.equals(SEQUENTIAL_ACCESS_WAL) ||
walImplementation.equals(ENCRYPTED_SEQUENTIAL_ACCESS_WAL)) {
// TODO: May need to instantiate ESAWAL for clarity?
wal = new
SequentialAccessWriteAheadLog<>(flowFileRepositoryPaths.get(0), serdeFactory,
this);
- } else if (walImplementation.equals(MINIMAL_LOCKING_WALI)) {
- final SortedSet<Path> paths = flowFileRepositoryPaths.stream()
- .map(File::toPath)
- .collect(Collectors.toCollection(TreeSet::new));
-
- wal = new MinimalLockingWriteAheadLog<>(paths, 1, serdeFactory,
this);
} else {
throw new IllegalStateException("Cannot create Write-Ahead Log
because the configured property '" + WRITE_AHEAD_LOG_IMPL + "' has an invalid
value of '" + walImplementation
+ "'. Please update nifi.properties to indicate a valid
value for this property.");
@@ -285,10 +247,6 @@ public class WriteAheadFlowFileRepository implements
FlowFileRepository, SyncLis
@Override
public Map<ResourceClaim, Set<ResourceClaimReference>>
findResourceClaimReferences(final Set<ResourceClaim> resourceClaims, final
FlowFileSwapManager swapManager) {
- if (!(isSequentialAccessWAL(walImplementation))) {
- return null;
- }
-
final Map<ResourceClaim, Set<ResourceClaimReference>> references = new
HashMap<>();
final SnapshotCapture<SerializedRepositoryRecord> snapshot =
((SequentialAccessWriteAheadLog<SerializedRepositoryRecord>)
wal).captureSnapshot();
@@ -747,95 +705,6 @@ public class WriteAheadFlowFileRepository implements
FlowFileRepository, SyncLis
}
}
- private Optional<Collection<SerializedRepositoryRecord>>
migrateFromSequentialAccessLog(final
WriteAheadRepository<SerializedRepositoryRecord> toUpdate) throws IOException {
- final String recoveryDirName =
nifiProperties.getProperty(FLOWFILE_REPOSITORY_DIRECTORY_PREFIX);
- final File recoveryDir = new File(recoveryDirName);
- if (!recoveryDir.exists()) {
- return Optional.empty();
- }
-
- final WriteAheadRepository<SerializedRepositoryRecord> recoveryWal =
new SequentialAccessWriteAheadLog<>(recoveryDir, serdeFactory, this);
- logger.info("Encountered FlowFile Repository that was written using
the Sequential Access Write Ahead Log. Will recover from this version.");
-
- final Collection<SerializedRepositoryRecord> recordList;
- try {
- recordList = recoveryWal.recoverRecords();
- } finally {
- recoveryWal.shutdown();
- }
-
- toUpdate.update(recordList, true);
-
- logger.info("Successfully recovered files from existing Write-Ahead
Log and transitioned to new Write-Ahead Log. Will not delete old files.");
-
- final File journalsDir = new File(recoveryDir, "journals");
- deleteRecursively(journalsDir);
-
- final File checkpointFile = new File(recoveryDir, "checkpoint");
- if (!checkpointFile.delete() && checkpointFile.exists()) {
- logger.warn("Failed to delete old file {}; this file should be
cleaned up manually", checkpointFile);
- }
-
- final File partialFile = new File(recoveryDir, "checkpoint.partial");
- if (!partialFile.delete() && partialFile.exists()) {
- logger.warn("Failed to delete old file {}; this file should be
cleaned up manually", partialFile);
- }
-
- return Optional.of(recordList);
- }
-
- @SuppressWarnings("deprecation")
- private Optional<Collection<SerializedRepositoryRecord>>
migrateFromMinimalLockingLog(final
WriteAheadRepository<SerializedRepositoryRecord> toUpdate) throws IOException {
- final List<File> partitionDirs = new ArrayList<>();
- for (final File recoveryFile : recoveryFiles) {
- final File[] partitions = recoveryFile.listFiles(file ->
file.getName().startsWith("partition-"));
- for (final File partition : partitions) {
- partitionDirs.add(partition);
- }
- }
-
- if (partitionDirs == null || partitionDirs.isEmpty()) {
- return Optional.empty();
- }
-
- logger.info("Encountered FlowFile Repository that was written using
the 'Minimal Locking Write-Ahead Log'. "
- + "Will recover from this version and re-write the repository
using the new version of the Write-Ahead Log.");
-
- final SortedSet<Path> paths = recoveryFiles.stream()
- .map(File::toPath)
- .collect(Collectors.toCollection(TreeSet::new));
-
- final Collection<SerializedRepositoryRecord> recordList;
- final MinimalLockingWriteAheadLog<SerializedRepositoryRecord>
minimalLockingWal = new MinimalLockingWriteAheadLog<>(paths,
partitionDirs.size(), serdeFactory, null);
- try {
- recordList = minimalLockingWal.recoverRecords();
- } finally {
- minimalLockingWal.shutdown();
- }
-
- toUpdate.update(recordList, true);
-
- // Delete the old repository
- logger.info("Successfully recovered files from existing Write-Ahead
Log and transitioned to new implementation. Will now delete old files.");
- for (final File partitionDir : partitionDirs) {
- deleteRecursively(partitionDir);
- }
-
- for (final File recoveryFile : recoveryFiles) {
- final File snapshotFile = new File(recoveryFile, "snapshot");
- if (!snapshotFile.delete() && snapshotFile.exists()) {
- logger.warn("Failed to delete old file {}; this file should be
cleaned up manually", snapshotFile);
- }
-
- final File partialFile = new File(recoveryFile,
"snapshot.partial");
- if (!partialFile.delete() && partialFile.exists()) {
- logger.warn("Failed to delete old file {}; this file should be
cleaned up manually", partialFile);
- }
- }
-
- return Optional.of(recordList);
- }
-
@Override
public Set<String> findQueuesWithFlowFiles(final FlowFileSwapManager
swapManager) throws IOException {
if (recoveredRecords == null) {
@@ -885,23 +754,6 @@ public class WriteAheadFlowFileRepository implements
FlowFileRepository, SyncLis
logger.debug("Recovered {} Swap Files: {}",
swapLocationSuffixes.size(), swapLocationSuffixes);
}
- // If we didn't recover any records from our write-ahead log, attempt
to recover records from the other implementation
- // of the write-ahead log. We do this in case the user changed the
"nifi.flowfile.repository.wal.impl" property.
- // In such a case, we still want to recover the records from the
previous FlowFile Repository and write them into the new one.
- // Since these implementations do not write to the same files, they
will not interfere with one another. If we do recover records,
- // then we will update the new WAL (with fsync()) and delete the old
repository so that we won't recover it again.
- if (recordList == null || recordList.isEmpty()) {
- if (isSequentialAccessWAL(walImplementation)) {
- // Configured to use Sequential Access WAL but it has no
records. Check if there are records in
- // a MinimalLockingWriteAheadLog that we can recover.
- recordList = migrateFromMinimalLockingLog(wal).orElse(new
ArrayList<>());
- } else {
- // Configured to use Minimal Locking WAL but it has no
records. Check if there are records in
- // a SequentialAccess Log that we can recover.
- recordList = migrateFromSequentialAccessLog(wal).orElse(new
ArrayList<>());
- }
- }
-
fieldCache.clear();
final Map<String, FlowFileQueue> queueMap = new HashMap<>();
diff --git
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/state/providers/local/WriteAheadLocalStateProvider.java
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/state/providers/local/WriteAheadLocalStateProvider.java
index 682fc5a2d7..69276abbd4 100644
---
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/state/providers/local/WriteAheadLocalStateProvider.java
+++
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/state/providers/local/WriteAheadLocalStateProvider.java
@@ -43,9 +43,11 @@ import org.apache.nifi.controller.state.StateMapSerDe;
import org.apache.nifi.controller.state.StateMapUpdate;
import org.apache.nifi.controller.state.providers.AbstractStateProvider;
import org.apache.nifi.processor.util.StandardValidators;
+import org.apache.nifi.wali.SequentialAccessWriteAheadLog;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import org.wali.MinimalLockingWriteAheadLog;
+import org.wali.SerDe;
+import org.wali.SerDeFactory;
import org.wali.UpdateType;
import org.wali.WriteAheadRepository;
@@ -131,7 +133,7 @@ public class WriteAheadLocalStateProvider extends
AbstractStateProvider {
}
versionGenerator = new AtomicLong(-1L);
- writeAheadLog = new MinimalLockingWriteAheadLog<>(basePath.toPath(),
numPartitions, serde, null);
+ writeAheadLog = new SequentialAccessWriteAheadLog<>(basePath, new
SerdeFactory(serde));
final Collection<StateMapUpdate> updates =
writeAheadLog.recoverRecords();
long maxRecordVersion = EMPTY_VERSION;
@@ -319,4 +321,34 @@ public class WriteAheadLocalStateProvider extends
AbstractStateProvider {
return t;
}
}
+
+ private static class SerdeFactory implements SerDeFactory<StateMapUpdate> {
+
+ private StateMapSerDe serde;
+
+ public SerdeFactory(StateMapSerDe serde) {
+ this.serde = serde;
+ }
+
+ @Override
+ public SerDe<StateMapUpdate> createSerDe(String encodingName) {
+ return this.serde;
+ }
+
+ @Override
+ public Object getRecordIdentifier(StateMapUpdate record) {
+ return this.serde.getRecordIdentifier(record);
+ }
+
+ @Override
+ public UpdateType getUpdateType(StateMapUpdate record) {
+ return this.serde.getUpdateType(record);
+ }
+
+ @Override
+ public String getLocation(StateMapUpdate record) {
+ return this.serde.getLocation(record);
+ }
+
+ }
}
diff --git
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/repository/TestWriteAheadFlowFileRepository.java
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/repository/TestWriteAheadFlowFileRepository.java
index c4f17b8962..3c7fc6b2fd 100644
---
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/repository/TestWriteAheadFlowFileRepository.java
+++
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/repository/TestWriteAheadFlowFileRepository.java
@@ -43,13 +43,13 @@ import org.apache.nifi.processor.FlowFileFilter;
import org.apache.nifi.util.MockFlowFile;
import org.apache.nifi.util.NiFiProperties;
import org.apache.nifi.util.file.FileUtils;
+import org.apache.nifi.wali.SequentialAccessWriteAheadLog;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.mockito.stubbing.Answer;
-import org.wali.MinimalLockingWriteAheadLog;
import org.wali.WriteAheadRepository;
import java.io.File;
@@ -359,7 +359,7 @@ public class TestWriteAheadFlowFileRepository {
final ResourceClaimManager claimManager = new
StandardResourceClaimManager();
final StandardRepositoryRecordSerdeFactory serdeFactory = new
StandardRepositoryRecordSerdeFactory(claimManager);
- final WriteAheadRepository<SerializedRepositoryRecord> repo = new
MinimalLockingWriteAheadLog<>(path, numPartitions, serdeFactory, null);
+ final WriteAheadRepository<SerializedRepositoryRecord> repo = new
SequentialAccessWriteAheadLog<>(path.toFile(), serdeFactory);
final Collection<SerializedRepositoryRecord> initialRecs =
repo.recoverRecords();
assertTrue(initialRecs.isEmpty());
diff --git
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-site-to-site/src/main/java/org/apache/nifi/remote/protocol/http/StandardHttpFlowFileServerProtocol.java
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-site-to-site/src/main/java/org/apache/nifi/remote/protocol/http/StandardHttpFlowFileServerProtocol.java
index f36cfaa7c7..64651d069b 100644
---
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-site-to-site/src/main/java/org/apache/nifi/remote/protocol/http/StandardHttpFlowFileServerProtocol.java
+++
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-site-to-site/src/main/java/org/apache/nifi/remote/protocol/http/StandardHttpFlowFileServerProtocol.java
@@ -33,10 +33,10 @@ import org.apache.nifi.remote.protocol.HandshakeProperties;
import org.apache.nifi.remote.protocol.RequestType;
import org.apache.nifi.remote.protocol.Response;
import org.apache.nifi.remote.protocol.ResponseCode;
-import org.apache.nifi.stream.io.ByteArrayInputStream;
-import org.apache.nifi.stream.io.ByteArrayOutputStream;
import org.apache.nifi.util.StringUtils;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
diff --git
a/nifi-nar-bundles/nifi-standard-services/nifi-distributed-cache-services-bundle/nifi-distributed-cache-server/src/main/java/org/apache/nifi/distributed/cache/server/map/PersistentMapCache.java
b/nifi-nar-bundles/nifi-standard-services/nifi-distributed-cache-services-bundle/nifi-distributed-cache-server/src/main/java/org/apache/nifi/distributed/cache/server/map/PersistentMapCache.java
index c1eebd61ee..03ed1deaf9 100644
---
a/nifi-nar-bundles/nifi-standard-services/nifi-distributed-cache-services-bundle/nifi-distributed-cache-server/src/main/java/org/apache/nifi/distributed/cache/server/map/PersistentMapCache.java
+++
b/nifi-nar-bundles/nifi-standard-services/nifi-distributed-cache-services-bundle/nifi-distributed-cache-server/src/main/java/org/apache/nifi/distributed/cache/server/map/PersistentMapCache.java
@@ -29,11 +29,13 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicLong;
+
+import org.apache.nifi.wali.SequentialAccessWriteAheadLog;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import org.wali.MinimalLockingWriteAheadLog;
import org.wali.SerDe;
+import org.wali.SerDeFactory;
import org.wali.UpdateType;
import org.wali.WriteAheadRepository;
@@ -48,7 +50,7 @@ public class PersistentMapCache implements MapCache {
public PersistentMapCache(final String serviceIdentifier, final File
persistencePath, final MapCache cacheToWrap) throws IOException {
try {
- wali = new MinimalLockingWriteAheadLog<>(persistencePath.toPath(),
1, new Serde(), null);
+ wali = new SequentialAccessWriteAheadLog<>(persistencePath, new
SerdeFactory());
} catch (OverlappingFileLockException ex) {
logger.error("OverlappingFileLockException thrown: Check lock
location - possible duplicate persistencePath conflict in PersistentMapCache.");
// Propagate the exception
@@ -276,4 +278,34 @@ public class PersistentMapCache implements MapCache {
return 1;
}
}
+
+ private static class SerdeFactory implements SerDeFactory<MapWaliRecord> {
+
+ private Serde serde;
+
+ public SerdeFactory() {
+ this.serde = new Serde();
+ }
+
+ @Override
+ public SerDe<MapWaliRecord> createSerDe(String encodingName) {
+ return this.serde;
+ }
+
+ @Override
+ public Object getRecordIdentifier(MapWaliRecord record) {
+ return this.serde.getRecordIdentifier(record);
+ }
+
+ @Override
+ public UpdateType getUpdateType(MapWaliRecord record) {
+ return this.serde.getUpdateType(record);
+ }
+
+ @Override
+ public String getLocation(MapWaliRecord record) {
+ return this.serde.getLocation(record);
+ }
+
+ }
}
diff --git
a/nifi-nar-bundles/nifi-standard-services/nifi-distributed-cache-services-bundle/nifi-distributed-cache-server/src/main/java/org/apache/nifi/distributed/cache/server/set/PersistentSetCache.java
b/nifi-nar-bundles/nifi-standard-services/nifi-distributed-cache-services-bundle/nifi-distributed-cache-server/src/main/java/org/apache/nifi/distributed/cache/server/set/PersistentSetCache.java
index c2c3a4112a..8b6cede7f3 100644
---
a/nifi-nar-bundles/nifi-standard-services/nifi-distributed-cache-services-bundle/nifi-distributed-cache-server/src/main/java/org/apache/nifi/distributed/cache/server/set/PersistentSetCache.java
+++
b/nifi-nar-bundles/nifi-standard-services/nifi-distributed-cache-services-bundle/nifi-distributed-cache-server/src/main/java/org/apache/nifi/distributed/cache/server/set/PersistentSetCache.java
@@ -29,8 +29,9 @@ import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicLong;
-import org.wali.MinimalLockingWriteAheadLog;
+import org.apache.nifi.wali.SequentialAccessWriteAheadLog;
import org.wali.SerDe;
+import org.wali.SerDeFactory;
import org.wali.UpdateType;
import org.wali.WriteAheadRepository;
@@ -42,7 +43,7 @@ public class PersistentSetCache implements SetCache {
private final AtomicLong modifications = new AtomicLong(0L);
public PersistentSetCache(final String serviceIdentifier, final File
persistencePath, final SetCache cacheToWrap) throws IOException {
- wali = new MinimalLockingWriteAheadLog<>(persistencePath.toPath(), 1,
new Serde(), null);
+ wali = new SequentialAccessWriteAheadLog<>(persistencePath, new
SerdeFactory());
wrapped = cacheToWrap;
}
@@ -192,4 +193,34 @@ public class PersistentSetCache implements SetCache {
return 1;
}
}
+
+ private static class SerdeFactory implements SerDeFactory<SetRecord> {
+
+ private Serde serde;
+
+ public SerdeFactory() {
+ this.serde = new Serde();
+ }
+
+ @Override
+ public SerDe<SetRecord> createSerDe(String encodingName) {
+ return this.serde;
+ }
+
+ @Override
+ public Object getRecordIdentifier(SetRecord record) {
+ return this.serde.getRecordIdentifier(record);
+ }
+
+ @Override
+ public UpdateType getUpdateType(SetRecord record) {
+ return this.serde.getUpdateType(record);
+ }
+
+ @Override
+ public String getLocation(SetRecord record) {
+ return this.serde.getLocation(record);
+ }
+
+ }
}
diff --git
a/nifi-nar-bundles/nifi-standard-services/nifi-distributed-cache-services-bundle/nifi-distributed-cache-server/src/test/java/org/apache/nifi/distributed/cache/server/map/TestPersistentMapCache.java
b/nifi-nar-bundles/nifi-standard-services/nifi-distributed-cache-services-bundle/nifi-distributed-cache-server/src/test/java/org/apache/nifi/distributed/cache/server/map/TestPersistentMapCache.java
deleted file mode 100644
index 5371e42977..0000000000
---
a/nifi-nar-bundles/nifi-standard-services/nifi-distributed-cache-services-bundle/nifi-distributed-cache-server/src/test/java/org/apache/nifi/distributed/cache/server/map/TestPersistentMapCache.java
+++ /dev/null
@@ -1,41 +0,0 @@
-/*
- * 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.nifi.distributed.cache.server.map;
-
-import org.apache.nifi.distributed.cache.server.EvictionPolicy;
-import org.junit.jupiter.api.Test;
-
-import java.io.File;
-import java.nio.channels.OverlappingFileLockException;
-
-import static org.junit.jupiter.api.Assertions.assertThrows;
-
-public class TestPersistentMapCache {
-
- /**
- * Test OverlappingFileLockException is caught when persistent path is
duplicated.
- */
- @Test
- public void testDuplicatePersistenceDirectory() {
- assertThrows(OverlappingFileLockException.class, () -> {
- File duplicatedFilePath = new File("/tmp/path1");
- final MapCache cache = new SimpleMapCache("simpleCache", 2,
EvictionPolicy.FIFO);
- PersistentMapCache pmc1 = new PersistentMapCache("id1",
duplicatedFilePath, cache);
- PersistentMapCache pmc2 = new PersistentMapCache("id2",
duplicatedFilePath, cache);
- });
- }
-}
\ No newline at end of file