mirror of
https://github.com/streetwriters/notesnook-sync-server.git
synced 2026-11-03 14:47:14 +01:00
* sync: keep pushed ciphers as UTF-8 from the hub to MongoDB Pushed ciphers were parsed into strings, then encoded back to UTF-8 by the driver into a new buffer for each large one: the two largest sources of large object heap allocations on prod. PushItems now reads each cipher straight into a pooled UTF-8 buffer and UpsertMany writes the item's BSON itself, from that buffer, into another pooled one. The stored documents are unchanged. Validation moves to SyncItemValidator, and the base64 check is vectorized. * sync: hand back pushed buffers when a push is rejected The hub disposed the pushed items only after its own checks, and the authorization filter rejects a call after SignalR has bound it, so those rejections left their pooled buffers to the GC. A push with null items also made the cleanup throw over the real error.
87 lines
3.4 KiB
C#
87 lines
3.4 KiB
C#
using MongoDB.Bson;
|
|
using MongoDB.Driver;
|
|
using Notesnook.API.Models;
|
|
|
|
namespace Notesnook.API.Tests;
|
|
|
|
public class UnitOfWorkTests(TestDatabase db) : IClassFixture<TestDatabase>
|
|
{
|
|
private static string NewUserId() => Guid.NewGuid().ToString("N");
|
|
|
|
// #10: an empty batch (e.g. an empty push) is a successful no-op
|
|
[Fact]
|
|
public async Task Empty_commit_succeeds()
|
|
{
|
|
var scope = db.CreateScope();
|
|
scope.Repositories.Notes.UpsertMany([], NewUserId());
|
|
|
|
Assert.True(await scope.Unit.Commit());
|
|
}
|
|
|
|
// #10: committing twice must not replay the first batch
|
|
[Fact]
|
|
public async Task Commands_are_cleared_after_commit()
|
|
{
|
|
var scope = db.CreateScope();
|
|
var device = new SyncDevice { Id = ObjectId.GenerateNewId(), UserId = NewUserId(), DeviceId = "A", LastAccessTime = 0, IsSyncReset = false };
|
|
scope.Repositories.SyncDevices.Insert(device);
|
|
|
|
Assert.True(await scope.Unit.Commit());
|
|
// a replayed insert would fail with a duplicate key error
|
|
Assert.True(await scope.Unit.Commit());
|
|
Assert.Equal(1, await scope.Repositories.SyncDevices.Collection.CountDocumentsAsync(d => d.UserId == device.UserId));
|
|
}
|
|
|
|
// #10: errors reach the caller instead of being swallowed
|
|
[Fact]
|
|
public async Task Commit_throws_on_failure()
|
|
{
|
|
var scope = db.CreateScope();
|
|
scope.DbContext.AddCommand((_, _) => throw new InvalidOperationException("boom"));
|
|
|
|
await Assert.ThrowsAsync<InvalidOperationException>(() => scope.Unit.Commit());
|
|
}
|
|
|
|
// #10: commands share one session, so they must run one after another, in order
|
|
[Fact]
|
|
public async Task Commands_run_sequentially_in_order()
|
|
{
|
|
var userId = NewUserId();
|
|
var scope = db.CreateScope();
|
|
var collection = scope.Repositories.Notes.Collection;
|
|
var running = 0;
|
|
var overlapped = false;
|
|
var seen = new List<long>();
|
|
|
|
for (var i = 0; i < 20; i++)
|
|
{
|
|
var expectedBefore = i;
|
|
scope.DbContext.AddCommand(async (session, ct) =>
|
|
{
|
|
if (Interlocked.Increment(ref running) > 1) overlapped = true;
|
|
// every earlier command's write must already be visible in this session
|
|
seen.Add(await collection.CountDocumentsAsync(session, Builders<SyncItem>.Filter.Eq(x => x.UserId, userId), cancellationToken: ct));
|
|
await collection.InsertOneAsync(session, new SyncItem { UserId = userId, ItemId = $"{expectedBefore}" }, cancellationToken: ct);
|
|
Interlocked.Decrement(ref running);
|
|
});
|
|
}
|
|
|
|
Assert.True(await scope.Unit.Commit());
|
|
Assert.False(overlapped);
|
|
Assert.Equal(Enumerable.Range(0, 20).Select(i => (long)i), seen);
|
|
}
|
|
|
|
// #10: all commands commit or roll back together
|
|
[Fact]
|
|
public async Task Failed_commit_rolls_back_everything()
|
|
{
|
|
var userId = NewUserId();
|
|
var scope = db.CreateScope();
|
|
scope.Repositories.Notes.UpsertMany([TestItems.PushedNote("1"), TestItems.PushedNote("2")], userId);
|
|
scope.DbContext.AddCommand((_, _) => throw new InvalidOperationException("boom"));
|
|
|
|
await Assert.ThrowsAsync<InvalidOperationException>(() => scope.Unit.Commit());
|
|
Assert.Equal(0, await scope.Repositories.Notes.Collection.CountDocumentsAsync(i => i.UserId == userId));
|
|
}
|
|
}
|