using System.Threading.Channels;
using Microsoft.Data.Sqlite;
namespace MoShiro.Backend.Storage.Sqlite;
///
/// 连接与单写入者:所有写操作经过一个有界 Channel,由一个后台循环依次执行(每个操作一个事务);
/// 读操作使用独立的只读连接。WAL 模式下读不会阻塞写。
///
public sealed class SqliteDatabase : IDisposable, IAsyncDisposable
{
private const int WriteQueueCapacity = 1024;
private readonly Channel _writes = Channel.CreateBounded(new BoundedChannelOptions(WriteQueueCapacity)
{
SingleReader = true,
FullMode = BoundedChannelFullMode.Wait,
});
private readonly string _readConnectionString;
private readonly SqliteConnection _writer;
private readonly Task _loop;
public SqliteDatabase(string dataDirectory)
{
DataDirectory = Path.GetFullPath(dataDirectory);
RawDirectory = Path.Combine(DataDirectory, "raw");
Directory.CreateDirectory(RawDirectory);
string path = Path.Combine(DataDirectory, "moshiro.db");
_readConnectionString = new SqliteConnectionStringBuilder { DataSource = path, Mode = SqliteOpenMode.ReadOnly, Pooling = true }.ToString();
_writer = new SqliteConnection(new SqliteConnectionStringBuilder { DataSource = path, Mode = SqliteOpenMode.ReadWriteCreate, Pooling = false }.ToString());
try
{
_writer.Open();
Pragma(_writer, "journal_mode=WAL");
Pragma(_writer, "synchronous=NORMAL");
Pragma(_writer, "foreign_keys=ON");
Pragma(_writer, "busy_timeout=5000");
Schema.Migrate(_writer);
}
catch
{
// 构造失败(例如数据库来自更新的版本)时不能把写连接和文件句柄留在身后
_writer.Dispose();
SqliteConnection.ClearAllPools();
throw;
}
_loop = Task.Factory.StartNew(RunWriterAsync, CancellationToken.None, TaskCreationOptions.LongRunning, TaskScheduler.Default).Unwrap();
}
public string DataDirectory { get; }
public string RawDirectory { get; }
/// 在单写入者上执行一个写操作(一个事务)。 在写入线程上同步执行。
public async ValueTask WriteAsync(Func work, CancellationToken ct)
{
var completion = new TaskCompletionSource