Skip to content
Open
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
1 change: 1 addition & 0 deletions PowerSync/PowerSync.Common/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
- Add support for .NET 9.0. Supported targets now also include `net9.0`, `net9.0-android`, `net9.0-ios`, and `net9.0-maccatalyst`.
- Update the PowerSync SQLite core extension to 0.4.13.
- Add support for offline-first file attachments via `AttachmentQueue`. See `Attachments/README.md`.
- Fix streaming sync retry loop reconnecting with no delay after an exception, ignoring `RetryDelayMs`.

## 0.1.1

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,11 @@ protected async Task StreamingSync(CancellationToken? signal, PowerSyncConnectio
break;
}
iterationResult = await StreamingSyncIteration(nestedCts.Token, options);

if (iterationResult.ImmediateRestart == true || iterationResult.LegacyRetry == true)
{
shouldDelayRetry = false;
}
}
catch (Exception ex)
{
Expand Down Expand Up @@ -413,20 +418,15 @@ protected async Task StreamingSync(CancellationToken? signal, PowerSyncConnectio
nestedCts = new CancellationTokenSource();
}

if (iterationResult != null && (iterationResult.ImmediateRestart != true && iterationResult.LegacyRetry != true))
if (shouldDelayRetry)
{

UpdateSyncStatus(new SyncStatusOptions
{
Connected = false,
Connecting = true
});

// On error, wait a little before retrying
if (shouldDelayRetry)
{
await DelayRetry();
}
await DelayRetry();
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
namespace PowerSync.Common.Tests.Client.Sync;

using System.Collections.Concurrent;

using PowerSync.Common.Client;
using PowerSync.Common.Client.Connection;
using PowerSync.Common.Client.Sync.Stream;
using PowerSync.Common.Tests.Utils;
using PowerSync.Common.Tests.Utils.Sync;

/// <summary>
/// dotnet test -v n --framework net8.0 --filter "StreamingSyncRetryTests"
/// </summary>
public class StreamingSyncRetryTests
{
[Fact(Timeout = 15000)]
public async Task RetryLoop_AppliesDelayBetweenFailedAttempts()
{
const int retryDelayMs = 200;
const double tolerance = 0.75;

var attemptTimes = new ConcurrentQueue<DateTime>();
var attemptSignal = new SemaphoreSlim(0);

var dbFilename = $"sync-retry-{Guid.NewGuid():N}.db";
var throwing = new ThrowingRemote(new TestConnector(), attemptTimes, attemptSignal);

var db = new PowerSyncDatabase(new PowerSyncDatabaseOptions
{
Database = new SQLOpenOptions { DbFilename = dbFilename },
Schema = TestSchemaTodoList.AppSchema,
RemoteFactory = _ => throwing
});

try
{
await db.Init();

// Fire-and-forget: Connect() awaits Connected=true, which never fires
// because every iteration throws. The retry loop runs in the background.
_ = db.Connect(
new TestConnector(),
new PowerSyncConnectionOptions { RetryDelayMs = retryDelayMs }
);

for (int i = 0; i < 4; i++)
{
Assert.True(
await attemptSignal.WaitAsync(TimeSpan.FromSeconds(5)),
$"Did not observe attempt #{i + 1} within timeout — retry loop is not running"
);
}

var timestamps = attemptTimes.ToArray();
Assert.True(timestamps.Length >= 4);

for (int i = 1; i < timestamps.Length; i++)
{
var deltaMs = (timestamps[i] - timestamps[i - 1]).TotalMilliseconds;
Assert.True(
deltaMs >= retryDelayMs * tolerance,
$"Retry gap #{i} was {deltaMs:F0}ms, expected >= {retryDelayMs * tolerance:F0}ms (RetryDelayMs={retryDelayMs})"
);
}
}
finally
{
await db.Disconnect();
await db.Close();
DatabaseUtils.CleanDb(dbFilename);
}
}
}

internal sealed class ThrowingRemote : Remote
{
private readonly ConcurrentQueue<DateTime> timestamps;
private readonly SemaphoreSlim signal;

public ThrowingRemote(
IPowerSyncBackendConnector connector,
ConcurrentQueue<DateTime> timestamps,
SemaphoreSlim signal
) : base(connector)
{
this.timestamps = timestamps;
this.signal = signal;
}

public override Task<System.IO.Stream> PostStreamRaw(SyncStreamOptions options)
{
timestamps.Enqueue(DateTime.UtcNow);
signal.Release();
throw new HttpRequestException(
"HTTP InternalServerError: simulated [PSYNC_S2305] from ThrowingRemote"
);
}

public override Task<T> Get<T>(string path, Dictionary<string, string>? headers = null)
{
var response = new StreamingSyncImplementation.ApiResponse(
new StreamingSyncImplementation.ResponseData("1")
);
return Task.FromResult((T)(object)response);
}
}
Loading