mirror of
https://github.com/bisq-network/bisq.git
synced 2024-11-19 01:41:11 +01:00
Implement AtomicFileWriter
First, the AtomicFileWriter writes data to a rolling file and before swapping the rolling file with the active file. This makes all file writes atomic and let's Bisq recover from crashes.
This commit is contained in:
parent
db5fb3b2fd
commit
7f06107f4d
@ -0,0 +1,24 @@
|
|||||||
|
/*
|
||||||
|
* This file is part of Bisq.
|
||||||
|
*
|
||||||
|
* Bisq is free software: you can redistribute it and/or modify it
|
||||||
|
* under the terms of the GNU Affero General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or (at
|
||||||
|
* your option) any later version.
|
||||||
|
*
|
||||||
|
* Bisq is distributed in the hope that it will be useful, but WITHOUT
|
||||||
|
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||||
|
* License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package bisq.persistence;
|
||||||
|
|
||||||
|
public class AtomicFileWriteFailedException extends RuntimeException {
|
||||||
|
public AtomicFileWriteFailedException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,64 @@
|
|||||||
|
/*
|
||||||
|
* This file is part of Bisq.
|
||||||
|
*
|
||||||
|
* Bisq is free software: you can redistribute it and/or modify it
|
||||||
|
* under the terms of the GNU Affero General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or (at
|
||||||
|
* your option) any later version.
|
||||||
|
*
|
||||||
|
* Bisq is distributed in the hope that it will be useful, but WITHOUT
|
||||||
|
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||||
|
* License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package bisq.persistence;
|
||||||
|
|
||||||
|
import java.nio.file.Path;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
|
||||||
|
import java.util.concurrent.CountDownLatch;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
public class AtomicFileWriter {
|
||||||
|
private final PersistenceFileWriter rollingFileWriter;
|
||||||
|
private File activeFile;
|
||||||
|
private File rollingFile;
|
||||||
|
|
||||||
|
|
||||||
|
public AtomicFileWriter(Path destinationPath,
|
||||||
|
PersistenceFileWriter rollingFileWriter) {
|
||||||
|
this.rollingFileWriter = rollingFileWriter;
|
||||||
|
activeFile = destinationPath.toFile();
|
||||||
|
rollingFile = rollingFileWriter.getFilePath().toFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void write(byte[] data) {
|
||||||
|
try {
|
||||||
|
CountDownLatch countDownLatch = rollingFileWriter.write(data);
|
||||||
|
boolean isSuccess = countDownLatch.await(45, TimeUnit.SECONDS);
|
||||||
|
if (!isSuccess) {
|
||||||
|
throw new AtomicFileWriteFailedException("Async atomic file write timeout triggered after 45 seconds.");
|
||||||
|
}
|
||||||
|
|
||||||
|
isSuccess = rollingFile.renameTo(activeFile);
|
||||||
|
if (!isSuccess) {
|
||||||
|
throw new AtomicFileWriteFailedException("Couldn't rename rolling file to active file.");
|
||||||
|
}
|
||||||
|
|
||||||
|
File tmpFile = activeFile;
|
||||||
|
activeFile = rollingFile;
|
||||||
|
rollingFile = tmpFile;
|
||||||
|
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
log.error("AtomicFileWriter got interrupted during write.", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,84 @@
|
|||||||
|
/*
|
||||||
|
* This file is part of Bisq.
|
||||||
|
*
|
||||||
|
* Bisq is free software: you can redistribute it and/or modify it
|
||||||
|
* under the terms of the GNU Affero General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or (at
|
||||||
|
* your option) any later version.
|
||||||
|
*
|
||||||
|
* Bisq is distributed in the hope that it will be useful, but WITHOUT
|
||||||
|
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||||
|
* License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package bisq.persistence;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
|
||||||
|
import java.util.concurrent.CountDownLatch;
|
||||||
|
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.junit.jupiter.api.io.TempDir;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyLong;
|
||||||
|
import static org.mockito.Mockito.doReturn;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
public class AtomicFileWriterTests {
|
||||||
|
private static final byte[] DATA = "Hello World!".getBytes(StandardCharsets.UTF_8);
|
||||||
|
@Mock
|
||||||
|
private PersistenceFileWriter persistenceFileWriter;
|
||||||
|
@Mock
|
||||||
|
private File rollingFile = mock(File.class);
|
||||||
|
@Mock
|
||||||
|
private CountDownLatch countDownLatch;
|
||||||
|
private AtomicFileWriter atomicFileWriter;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setup(@TempDir Path tempDir, @Mock Path rollingFilePath) {
|
||||||
|
doReturn(countDownLatch).when(persistenceFileWriter).write(any());
|
||||||
|
doReturn(rollingFile).when(rollingFilePath).toFile();
|
||||||
|
doReturn(rollingFilePath).when(persistenceFileWriter).getFilePath();
|
||||||
|
|
||||||
|
var file = tempDir.resolve("my_file");
|
||||||
|
atomicFileWriter = new AtomicFileWriter(file, persistenceFileWriter);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void triggerFileWriteTimeout() throws InterruptedException {
|
||||||
|
doReturn(false).when(countDownLatch).await(anyLong(), any());
|
||||||
|
assertThrows(AtomicFileWriteFailedException.class,
|
||||||
|
() -> atomicFileWriter.write(DATA));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void renameFailure() throws InterruptedException {
|
||||||
|
doReturn(true).when(countDownLatch).await(anyLong(), any());
|
||||||
|
doReturn(false).when(rollingFile).renameTo(any());
|
||||||
|
|
||||||
|
assertThrows(AtomicFileWriteFailedException.class,
|
||||||
|
() -> atomicFileWriter.write(DATA));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void write() throws InterruptedException {
|
||||||
|
doReturn(true).when(countDownLatch).await(anyLong(), any());
|
||||||
|
doReturn(true).when(rollingFile).renameTo(any());
|
||||||
|
atomicFileWriter.write(DATA);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user