client/dotnet: make LazyBrowseNode.ExpandAsync thread-safe
This commit is contained in:
@@ -142,6 +142,37 @@ public sealed class LazyBrowseNodeTests
|
|||||||
Assert.Equal("7:abc:2", transport.BrowseChildrenCalls[2].Request.PageToken);
|
Assert.Equal("7:abc:2", transport.BrowseChildrenCalls[2].Request.PageToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verifies that ten concurrent ExpandAsync calls issue exactly one RPC, not ten.
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public async Task Expand_CalledConcurrently_OnlyFiresOneRpc()
|
||||||
|
{
|
||||||
|
FakeGalaxyRepositoryTransport transport = CreateTransport();
|
||||||
|
transport.BrowseChildrenReplies.Enqueue(BuildReply(
|
||||||
|
children: [BuildObject(1, "Plant", isArea: true)],
|
||||||
|
childHasChildren: [true],
|
||||||
|
cacheSequence: 7));
|
||||||
|
transport.BrowseChildrenReplies.Enqueue(BuildReply(
|
||||||
|
children: [BuildObject(2, "Mixer_001")],
|
||||||
|
childHasChildren: [false],
|
||||||
|
cacheSequence: 7));
|
||||||
|
|
||||||
|
await using GalaxyRepositoryClient client = CreateClient(transport);
|
||||||
|
IReadOnlyList<LazyBrowseNode> roots = await client.BrowseAsync();
|
||||||
|
|
||||||
|
// Fire ten concurrent expands of the same node.
|
||||||
|
Task[] tasks = Enumerable.Range(0, 10)
|
||||||
|
.Select(_ => roots[0].ExpandAsync())
|
||||||
|
.ToArray();
|
||||||
|
await Task.WhenAll(tasks);
|
||||||
|
|
||||||
|
Assert.True(roots[0].IsExpanded);
|
||||||
|
Assert.Single(roots[0].Children);
|
||||||
|
// 1 roots fetch + exactly 1 expand fetch = 2 total
|
||||||
|
Assert.Equal(2, transport.BrowseChildrenCalls.Count);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Verifies that BrowseChildrenOptions filter fields are forwarded to the BrowseChildren request.
|
/// Verifies that BrowseChildrenOptions filter fields are forwarded to the BrowseChildren request.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ public sealed class LazyBrowseNode
|
|||||||
private readonly GalaxyRepositoryClient _client;
|
private readonly GalaxyRepositoryClient _client;
|
||||||
private readonly BrowseChildrenOptions _options;
|
private readonly BrowseChildrenOptions _options;
|
||||||
private readonly List<LazyBrowseNode> _children = [];
|
private readonly List<LazyBrowseNode> _children = [];
|
||||||
|
private readonly SemaphoreSlim _expandLock = new(1, 1);
|
||||||
private bool _isExpanded;
|
private bool _isExpanded;
|
||||||
|
|
||||||
internal LazyBrowseNode(
|
internal LazyBrowseNode(
|
||||||
@@ -43,6 +44,10 @@ public sealed class LazyBrowseNode
|
|||||||
/// Fetches direct children from the gateway and populates <see cref="Children"/>.
|
/// Fetches direct children from the gateway and populates <see cref="Children"/>.
|
||||||
/// Idempotent: subsequent calls are no-ops.
|
/// Idempotent: subsequent calls are no-ops.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Thread-safe: concurrent callers see exactly one fetch; subsequent callers
|
||||||
|
/// (after the first completes) return immediately.
|
||||||
|
/// </remarks>
|
||||||
/// <param name="cancellationToken">Token to observe for cancellation.</param>
|
/// <param name="cancellationToken">Token to observe for cancellation.</param>
|
||||||
public async Task ExpandAsync(CancellationToken cancellationToken = default)
|
public async Task ExpandAsync(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
@@ -51,33 +56,46 @@ public sealed class LazyBrowseNode
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
string pageToken = string.Empty;
|
await _expandLock.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||||
HashSet<string> seenPageTokens = new(StringComparer.Ordinal);
|
try
|
||||||
do
|
|
||||||
{
|
{
|
||||||
BrowseChildrenRequest request = GalaxyRepositoryClient.BuildBrowseChildrenRequest(_options);
|
if (_isExpanded)
|
||||||
request.ParentGobjectId = Object.GobjectId;
|
|
||||||
request.PageToken = pageToken;
|
|
||||||
|
|
||||||
BrowseChildrenReply reply = await _client
|
|
||||||
.BrowseChildrenRawAsync(request, cancellationToken)
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
|
|
||||||
for (int i = 0; i < reply.Children.Count; i++)
|
|
||||||
{
|
{
|
||||||
bool hint = i < reply.ChildHasChildren.Count && reply.ChildHasChildren[i];
|
return;
|
||||||
_children.Add(new LazyBrowseNode(_client, reply.Children[i], hint, _options));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pageToken = reply.NextPageToken;
|
string pageToken = string.Empty;
|
||||||
if (!string.IsNullOrWhiteSpace(pageToken) && !seenPageTokens.Add(pageToken))
|
HashSet<string> seenPageTokens = new(StringComparer.Ordinal);
|
||||||
|
do
|
||||||
{
|
{
|
||||||
throw new MxGatewayException(
|
BrowseChildrenRequest request = GalaxyRepositoryClient.BuildBrowseChildrenRequest(_options);
|
||||||
$"Galaxy BrowseChildren returned a repeated page token '{pageToken}'.");
|
request.ParentGobjectId = Object.GobjectId;
|
||||||
|
request.PageToken = pageToken;
|
||||||
|
|
||||||
|
BrowseChildrenReply reply = await _client
|
||||||
|
.BrowseChildrenRawAsync(request, cancellationToken)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
for (int i = 0; i < reply.Children.Count; i++)
|
||||||
|
{
|
||||||
|
bool hint = i < reply.ChildHasChildren.Count && reply.ChildHasChildren[i];
|
||||||
|
_children.Add(new LazyBrowseNode(_client, reply.Children[i], hint, _options));
|
||||||
|
}
|
||||||
|
|
||||||
|
pageToken = reply.NextPageToken;
|
||||||
|
if (!string.IsNullOrWhiteSpace(pageToken) && !seenPageTokens.Add(pageToken))
|
||||||
|
{
|
||||||
|
throw new MxGatewayException(
|
||||||
|
$"Galaxy BrowseChildren returned a repeated page token '{pageToken}'.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
while (!string.IsNullOrWhiteSpace(pageToken));
|
||||||
|
|
||||||
|
_isExpanded = true;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_expandLock.Release();
|
||||||
}
|
}
|
||||||
while (!string.IsNullOrWhiteSpace(pageToken));
|
|
||||||
|
|
||||||
_isExpanded = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user