Skip to content
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
28 changes: 28 additions & 0 deletions src/ZoneTree.UnitTests/InMemoryFileStreamProviderTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using Tenray.ZoneTree.AbstractFileStream;
using Tenray.ZoneTree.Logger;
using Tenray.ZoneTree.Serializers;
using Tenray.ZoneTree.WAL;

namespace Tenray.ZoneTree.UnitTests;

public sealed class InMemoryFileStreamProviderTests
{
[Test]
public void WalWithInMemoryProvider()
{
var serializer = new UnicodeStringSerializer();
var provider = new InMemoryFileStreamProvider();
var wal = new SyncFileSystemWriteAheadLog<string, string>(
new ConsoleLogger(),
provider,
serializer,
serializer,
"test.wal");
wal.Append("hello", "world", 0);
var result = wal.ReadLogEntries(false, false, true);
Assert.That(result.Success, Is.True);
Assert.That(result.Keys[0], Is.EqualTo("hello"));
Assert.That(result.Values[0], Is.EqualTo("world"));
wal.Drop();
}
}
52 changes: 52 additions & 0 deletions src/ZoneTree/AbstractFileStream/InMemoryFileStream.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
using System;
using System.IO;

namespace Tenray.ZoneTree.AbstractFileStream;

public sealed class InMemoryFileStream : MemoryStream, IFileStream
{
readonly InMemoryFileStreamProvider Provider;

public string FilePath { get; }

public InMemoryFileStream(
InMemoryFileStreamProvider provider,
string path,
byte[] buffer)
{
Provider = provider;
FilePath = path;
if (buffer.Length > 0)
Write(buffer, 0, buffer.Length);
Position = 0;
}

public void Flush(bool flushToDisk)
{
Flush();
}

public int ReadFaster(byte[] buffer, int offset, int count)
{
int totalRead = 0;
while (totalRead < count)
{
int read = Read(buffer, offset + totalRead, count - totalRead);
if (read == 0)
throw new EndOfStreamException();
totalRead += read;
}
return totalRead;
}

public Stream ToStream() => this;

protected override void Dispose(bool disposing)
{
if (disposing)
{
Provider.UpdateFile(FilePath, ToArray());
}
base.Dispose(disposing);
}
}
137 changes: 137 additions & 0 deletions src/ZoneTree/AbstractFileStream/InMemoryFileStreamProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
using System.Text;

namespace Tenray.ZoneTree.AbstractFileStream;

public sealed class InMemoryFileStreamProvider : IFileStreamProvider
{
readonly Dictionary<string, byte[]> Files = new();
readonly HashSet<string> Directories = new();

public IFileStream CreateFileStream(
string path,
FileMode mode,
FileAccess access,
FileShare share,
int bufferSize = 4096,
FileOptions options = FileOptions.None)
{
lock (this)
{
if (!Files.ContainsKey(path))
{
if (mode == FileMode.Open)
throw new FileNotFoundException(path);
Files[path] = Array.Empty<byte>();
}
else if (mode == FileMode.CreateNew)
{
throw new IOException($"File {path} already exists.");
}
else if (mode == FileMode.Create)
{
Files[path] = Array.Empty<byte>();
}
else if (mode == FileMode.Truncate)
{
Files[path] = Array.Empty<byte>();
}
var bytes = Files[path];
var stream = new InMemoryFileStream(this, path, bytes);
if (mode == FileMode.Append)
stream.Seek(0, SeekOrigin.End);
return stream;
}
}

public bool FileExists(string path)
{
lock (this) return Files.ContainsKey(path);
}

public bool DirectoryExists(string path)
{
lock (this) return Directories.Contains(path);
}

public void CreateDirectory(string path)
{
lock (this) Directories.Add(path);
}

public void DeleteFile(string path)
{
lock (this) Files.Remove(path);
}

public void DeleteDirectory(string path, bool recursive)
{
lock (this)
{
Directories.Remove(path);
if (recursive)
{
var toRemove = Files.Keys.Where(x => x.StartsWith(path)).ToList();
foreach (var f in toRemove)
Files.Remove(f);
}
}
}

public string ReadAllText(string path)
{
lock (this) return Encoding.UTF8.GetString(Files[path]);
}

public byte[] ReadAllBytes(string path)
{
lock (this)
{
var b = Files[path];
var copy = new byte[b.Length];
Buffer.BlockCopy(b, 0, copy, 0, b.Length);
return copy;
}
}

public void Replace(
string sourceFileName,
string destinationFileName,
string destinationBackupFileName)
{
lock (this)
{
if (destinationBackupFileName != null && Files.ContainsKey(destinationFileName))
{
Files[destinationBackupFileName] = Files[destinationFileName];
}
Files[destinationFileName] = Files.ContainsKey(sourceFileName) ? Files[sourceFileName] : Array.Empty<byte>();
Files.Remove(sourceFileName);
}
}

public DurableFileWriter GetDurableFileWriter()
{
return new DurableFileWriter(this);
}

public IReadOnlyList<string> GetDirectories(string path)
{
lock (this)
{
return Directories.Where(x => x.StartsWith(path)).ToArray();
}
}

public string CombinePaths(string path1, string path2)
{
return Path.Combine(path1, path2);
}

internal void UpdateFile(string path, byte[] data)
{
lock (this)
{
Files[path] = data;
}
}
}
12 changes: 11 additions & 1 deletion src/ZoneTree/docs/ZoneTree/guide/quick-start.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,4 +162,14 @@ while(iterator.Next()) {
var key = iterator.CurrentKey;
var value = iterator.CurrentValue;
}
```
```
## Using the InMemoryFileStreamProvider

The `InMemoryFileStreamProvider` keeps all files entirely in memory. It is useful for unit testing or scenarios that require fast, temporary storage without touching the disk.

```csharp
var provider = new InMemoryFileStreamProvider();
using var zoneTree = new ZoneTreeFactory<int, string>(provider)
.OpenOrCreate();
zoneTree.Upsert(1, "value");
```
Loading