Skip to content

Commit 9d5764d

Browse files
author
Atakan Ük
committed
Avoid blocking SemaphoreSlim.Wait in BuildDatasyncOfflineOptions for browser-wasm (#574)
Use a Monitor lock for the synchronous lazy init, since SemaphoreSlim.Wait is unsupported on single-threaded browser-wasm. Adds regression tests.
1 parent 3b6fc78 commit 9d5764d

2 files changed

Lines changed: 44 additions & 9 deletions

File tree

‎src/CommunityToolkit.Datasync.Client/Offline/OfflineDbContext.cs‎

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,12 @@ public abstract partial class OfflineDbContext : DbContext
6060
/// A checker for the disposed state of this context.
6161
/// </summary>
6262
internal bool _disposedValue;
63-
internal DisposableLock _optionsLock = new();
63+
64+
/// <summary>
65+
/// Guards the lazy creation of <see cref="OfflineOptions"/>. A monitor is used instead of a
66+
/// <see cref="SemaphoreSlim"/> because blocking semaphore waits are not supported on single-threaded browser-wasm.
67+
/// </summary>
68+
private readonly object _optionsLock = new();
6469

6570
/// <summary>
6671
/// The operations queue. This is used to store pending requests to the datasync service.
@@ -144,15 +149,17 @@ protected OfflineDbContext(DbContextOptions options) : base(options)
144149
/// <returns>The offline options for the datasync operation.</returns>
145150
internal OfflineOptions BuildDatasyncOfflineOptions()
146151
{
147-
using IDisposable optionsLock = this._optionsLock.AcquireLock();
148-
if (OfflineOptions is null)
152+
lock (this._optionsLock)
149153
{
150-
DatasyncOfflineOptionsBuilder builder = new(QueueManager.GetSynchronizableEntityTypes());
151-
OnDatasyncInitialization(builder);
152-
OfflineOptions = builder.Build();
154+
if (OfflineOptions is null)
155+
{
156+
DatasyncOfflineOptionsBuilder builder = new(QueueManager.GetSynchronizableEntityTypes());
157+
OnDatasyncInitialization(builder);
158+
OfflineOptions = builder.Build();
159+
}
160+
161+
return OfflineOptions;
153162
}
154-
155-
return OfflineOptions;
156163
}
157164

158165
/// <summary>
@@ -604,7 +611,6 @@ protected virtual void Dispose(bool disposing)
604611
{
605612
if (disposing)
606613
{
607-
this._optionsLock.Dispose();
608614
base.Dispose();
609615
}
610616

‎tests/CommunityToolkit.Datasync.Client.Test/Offline/OfflineDbContext_Tests.cs‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
#pragma warning disable IDE0305 // Simplify collection initialization
66

77
using CommunityToolkit.Datasync.Client.Offline;
8+
using CommunityToolkit.Datasync.Client.Offline.Models;
89
using CommunityToolkit.Datasync.Client.Serialization;
910
using CommunityToolkit.Datasync.Client.Test.Offline.Helpers;
1011
using CommunityToolkit.Datasync.TestCommon;
@@ -41,6 +42,34 @@ public void Base_Ctor_CreatesInternalApi()
4142
}
4243
#endregion
4344

45+
#region BuildDatasyncOfflineOptions
46+
[Fact]
47+
public void BuildDatasyncOfflineOptions_ReturnsSameInstance()
48+
{
49+
int configured = 0;
50+
this.context.Configurator = _ => configured++;
51+
52+
OfflineOptions first = this.context.BuildDatasyncOfflineOptions();
53+
OfflineOptions second = this.context.BuildDatasyncOfflineOptions();
54+
55+
second.Should().BeSameAs(first);
56+
configured.Should().Be(1);
57+
}
58+
59+
[Fact]
60+
public async Task BuildDatasyncOfflineOptions_ConcurrentCalls_BuildOnce()
61+
{
62+
int configured = 0;
63+
this.context.Configurator = _ => Interlocked.Increment(ref configured);
64+
65+
OfflineOptions[] results = await Task.WhenAll(
66+
Enumerable.Range(0, 16).Select(_ => Task.Run(() => this.context.BuildDatasyncOfflineOptions())));
67+
68+
results.Should().OnlyContain(o => ReferenceEquals(o, results[0]));
69+
configured.Should().Be(1);
70+
}
71+
#endregion
72+
4473
#region PullAsync
4574
[Fact]
4675
public async Task PullAsync_List_Works_InitialSync()

0 commit comments

Comments
 (0)