Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FFDB-007: Implement a Persistable Filetable #14

Merged
merged 1 commit into from
Jan 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@
<version>${junit.jupiter.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.esotericsoftware</groupId>
<artifactId>kryo</artifactId>
<version>5.5.0</version>
</dependency>
</dependencies>

<build>
Expand Down
45 changes: 45 additions & 0 deletions src/main/java/com/sahilbondre/firefly/filetable/FilePointer.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package com.sahilbondre.firefly.filetable;

import java.util.Objects;

public class FilePointer {
private String fileName;
private long offset;

public FilePointer(String fileName, long offset) {
this.fileName = fileName;
this.offset = offset;
}

public FilePointer() {
}

public String getFileName() {
return fileName;
}

public void setFileName(String fileName) {
this.fileName = fileName;
}

public long getOffset() {
return offset;
}

public void setOffset(long offset) {
this.offset = offset;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
FilePointer that = (FilePointer) o;
return offset == that.offset && fileName.equals(that.fileName);
}

@Override
public int hashCode() {
return Objects.hash(fileName, offset);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.sahilbondre.firefly.filetable;

public class InvalidFileTableException extends RuntimeException {
public InvalidFileTableException(String message) {
super(message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.sahilbondre.firefly.filetable;

import java.io.FileNotFoundException;

public interface PersistableFileTable {
void put(byte[] key, FilePointer value);

FilePointer get(byte[] key);

void saveToDisk(String filePath) throws FileNotFoundException;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package com.sahilbondre.firefly.filetable;

import com.esotericsoftware.kryo.Kryo;
import com.esotericsoftware.kryo.KryoException;
import com.esotericsoftware.kryo.io.Input;
import com.esotericsoftware.kryo.io.Output;

import java.io.*;
import java.util.HashMap;
import java.util.Map;

public class SerializedPersistableFileTable implements PersistableFileTable, Serializable {

private static final Kryo kryo = new Kryo();

private final Map<String, FilePointer> table;

public SerializedPersistableFileTable() {
kryo.register(SerializedPersistableFileTable.class);
kryo.register(HashMap.class);
kryo.register(FilePointer.class);
this.table = new HashMap<>();
}

public static SerializedPersistableFileTable fromEmpty() {
return new SerializedPersistableFileTable();
}

public static SerializedPersistableFileTable fromFile(String filePath) throws FileNotFoundException, KryoException {
try (Input input = new Input(new FileInputStream(filePath))) {
return kryo.readObject(input, SerializedPersistableFileTable.class);
} catch (KryoException e) {
throw new InvalidFileTableException("Failed to load FileTable from disk: " + e.getMessage());
}
}

@Override
public void put(byte[] key, FilePointer value) {
if (key != null && value != null) {
table.put(new String(key), value);
}
}

@Override
public FilePointer get(byte[] key) {
if (key != null) {
return table.get(new String(key));
}
return null;
}

@Override
public void saveToDisk(String filePath) throws FileNotFoundException {
Output output = new Output(new FileOutputStream(filePath));
kryo.writeObject(output, this);
output.close();
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.sahilbondre.firefly.log;

import com.sahilbondre.firefly.filetable.FilePointer;

import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
Expand Down Expand Up @@ -28,10 +30,11 @@ public String getFilePath() {
}

@Override
public void append(byte[] message) throws IOException {
public FilePointer append(byte[] message) throws IOException {
fileChannel.position(fileChannel.size());
ByteBuffer buffer = ByteBuffer.wrap(message);
fileChannel.write(buffer);
return new FilePointer(filePath, fileChannel.size() - message.length);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
package com.sahilbondre.firefly.log;

import com.sahilbondre.firefly.filetable.FilePointer;

import java.io.IOException;

public interface RandomAccessLog {
long size() throws IOException;

String getFilePath();

void append(byte[] message) throws IOException;
FilePointer append(byte[] message) throws IOException;

byte[] read(long offset, long length) throws IOException, InvalidRangeException;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package com.sahilbondre.firefly.filetable;

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;

import static org.junit.jupiter.api.Assertions.*;

class SerializedPersistableFileTableTest {

private static final String TEST_FILE_PATH = "src/test/resources/map";
private SerializedPersistableFileTable fileTable;

@BeforeEach
void setUp() throws IOException {
Files.deleteIfExists(Paths.get(TEST_FILE_PATH));
fileTable = SerializedPersistableFileTable.fromEmpty();
}

@AfterEach
void tearDown() throws IOException {
Files.deleteIfExists(Paths.get(TEST_FILE_PATH));
}

@Test
void given_KeyValue_When_PuttingAndGet_Then_RetrievedValueMatches() {
// Given
byte[] key = "testKey".getBytes();
FilePointer expectedValue = new FilePointer("test.txt", 42);

// When
fileTable.put(key, new FilePointer("test.txt", 42));
FilePointer retrievedValue = fileTable.get(key);

// Then
assertEquals(expectedValue, retrievedValue);
}

@Test
void given_NullKey_When_PuttingAndGet_Then_RetrievedValueIsNull() {
// Given
FilePointer value = new FilePointer("test.txt", 42);

// When
fileTable.put(null, value);
FilePointer retrievedValue = fileTable.get(null);

// Then
assertNull(retrievedValue);
}

@Test
void given_NullValue_When_PuttingAndGet_Then_RetrievedValueIsNull() {
// Given
byte[] key = "testKey".getBytes();

// When
fileTable.put(key, null);
FilePointer retrievedValue = fileTable.get(key);

// Then
assertNull(retrievedValue);
}

@Test
void given_KeyValue_When_SavingToDiskAndLoadingFromFile_Then_RetrievedValueMatches() throws FileNotFoundException {
// Given
byte[] key = "testKey".getBytes();
FilePointer value = new FilePointer("test.txt", 42);

// When
fileTable.put(key, value);
fileTable.saveToDisk(TEST_FILE_PATH);
SerializedPersistableFileTable loadedFileTable = SerializedPersistableFileTable.fromFile(TEST_FILE_PATH);
FilePointer retrievedValue = loadedFileTable.get(key);

// Then
assertEquals(value, retrievedValue);
}

@Test
void given_NonexistentFile_When_LoadingFromFile_Then_FileNotFoundExceptionIsThrown() {
// When
// Then
assertThrows(FileNotFoundException.class,
() -> SerializedPersistableFileTable.fromFile(TEST_FILE_PATH));
}

@Test
void given_CorruptedFile_When_LoadingFromFile_Then_InvalidFileTableExceptionIsThrown() throws IOException {
// Given
// Create a corrupted file by writing invalid data
Path filePath = Paths.get(TEST_FILE_PATH);
Files.write(filePath, List.of("Invalid Data"));

// Then
assertThrows(InvalidFileTableException.class,
() -> SerializedPersistableFileTable.fromFile(TEST_FILE_PATH));
}
}