Skip to content

Commit b9515a6

Browse files
committed
test: add regression tests for streaming sync retry backoff
1 parent f41ec08 commit b9515a6

1 file changed

Lines changed: 106 additions & 0 deletions

File tree

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
namespace PowerSync.Common.Tests.Client.Sync;
2+
3+
using System.Collections.Concurrent;
4+
5+
using PowerSync.Common.Client;
6+
using PowerSync.Common.Client.Connection;
7+
using PowerSync.Common.Client.Sync.Stream;
8+
using PowerSync.Common.Tests.Utils;
9+
using PowerSync.Common.Tests.Utils.Sync;
10+
11+
/// <summary>
12+
/// dotnet test -v n --framework net8.0 --filter "StreamingSyncRetryTests"
13+
/// </summary>
14+
public class StreamingSyncRetryTests
15+
{
16+
[Fact(Timeout = 15000)]
17+
public async Task RetryLoop_AppliesDelayBetweenFailedAttempts()
18+
{
19+
const int retryDelayMs = 200;
20+
const double tolerance = 0.75;
21+
22+
var attemptTimes = new ConcurrentQueue<DateTime>();
23+
var attemptSignal = new SemaphoreSlim(0);
24+
25+
var dbFilename = $"sync-retry-{Guid.NewGuid():N}.db";
26+
var throwing = new ThrowingRemote(new TestConnector(), attemptTimes, attemptSignal);
27+
28+
var db = new PowerSyncDatabase(new PowerSyncDatabaseOptions
29+
{
30+
Database = new SQLOpenOptions { DbFilename = dbFilename },
31+
Schema = TestSchemaTodoList.AppSchema,
32+
RemoteFactory = _ => throwing
33+
});
34+
35+
try
36+
{
37+
await db.Init();
38+
39+
// Fire-and-forget: Connect() awaits Connected=true, which never fires
40+
// because every iteration throws. The retry loop runs in the background.
41+
_ = db.Connect(
42+
new TestConnector(),
43+
new PowerSyncConnectionOptions { RetryDelayMs = retryDelayMs }
44+
);
45+
46+
for (int i = 0; i < 4; i++)
47+
{
48+
Assert.True(
49+
await attemptSignal.WaitAsync(TimeSpan.FromSeconds(5)),
50+
$"Did not observe attempt #{i + 1} within timeout — retry loop is not running"
51+
);
52+
}
53+
54+
var timestamps = attemptTimes.ToArray();
55+
Assert.True(timestamps.Length >= 4);
56+
57+
for (int i = 1; i < timestamps.Length; i++)
58+
{
59+
var deltaMs = (timestamps[i] - timestamps[i - 1]).TotalMilliseconds;
60+
Assert.True(
61+
deltaMs >= retryDelayMs * tolerance,
62+
$"Retry gap #{i} was {deltaMs:F0}ms, expected >= {retryDelayMs * tolerance:F0}ms (RetryDelayMs={retryDelayMs})"
63+
);
64+
}
65+
}
66+
finally
67+
{
68+
await db.Disconnect();
69+
await db.Close();
70+
DatabaseUtils.CleanDb(dbFilename);
71+
}
72+
}
73+
}
74+
75+
internal sealed class ThrowingRemote : Remote
76+
{
77+
private readonly ConcurrentQueue<DateTime> timestamps;
78+
private readonly SemaphoreSlim signal;
79+
80+
public ThrowingRemote(
81+
IPowerSyncBackendConnector connector,
82+
ConcurrentQueue<DateTime> timestamps,
83+
SemaphoreSlim signal
84+
) : base(connector)
85+
{
86+
this.timestamps = timestamps;
87+
this.signal = signal;
88+
}
89+
90+
public override Task<System.IO.Stream> PostStreamRaw(SyncStreamOptions options)
91+
{
92+
timestamps.Enqueue(DateTime.UtcNow);
93+
signal.Release();
94+
throw new HttpRequestException(
95+
"HTTP InternalServerError: simulated [PSYNC_S2305] from ThrowingRemote"
96+
);
97+
}
98+
99+
public override Task<T> Get<T>(string path, Dictionary<string, string>? headers = null)
100+
{
101+
var response = new StreamingSyncImplementation.ApiResponse(
102+
new StreamingSyncImplementation.ResponseData("1")
103+
);
104+
return Task.FromResult((T)(object)response);
105+
}
106+
}

0 commit comments

Comments
 (0)