Fix Galaxy paging review findings
This commit is contained in:
@@ -814,9 +814,7 @@ public static class MxGatewayClientCli
|
|||||||
TextWriter output,
|
TextWriter output,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
DiscoverHierarchyReply reply = await client.GalaxyDiscoverHierarchyAsync(
|
DiscoverHierarchyReply reply = await DiscoverAllGalaxyHierarchyAsync(client, cancellationToken)
|
||||||
new DiscoverHierarchyRequest(),
|
|
||||||
cancellationToken)
|
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
if (arguments.HasFlag("json"))
|
if (arguments.HasFlag("json"))
|
||||||
@@ -834,6 +832,39 @@ public static class MxGatewayClientCli
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static async Task<DiscoverHierarchyReply> DiscoverAllGalaxyHierarchyAsync(
|
||||||
|
IMxGatewayCliClient client,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
DiscoverHierarchyReply aggregate = new();
|
||||||
|
HashSet<string> seenPageTokens = new(StringComparer.Ordinal);
|
||||||
|
string pageToken = string.Empty;
|
||||||
|
do
|
||||||
|
{
|
||||||
|
DiscoverHierarchyReply page = await client.GalaxyDiscoverHierarchyAsync(
|
||||||
|
new DiscoverHierarchyRequest
|
||||||
|
{
|
||||||
|
PageSize = 5000,
|
||||||
|
PageToken = pageToken,
|
||||||
|
},
|
||||||
|
cancellationToken)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
aggregate.Objects.Add(page.Objects);
|
||||||
|
aggregate.TotalObjectCount = page.TotalObjectCount;
|
||||||
|
pageToken = page.NextPageToken;
|
||||||
|
if (!string.IsNullOrWhiteSpace(pageToken)
|
||||||
|
&& !seenPageTokens.Add(pageToken))
|
||||||
|
{
|
||||||
|
throw new MxGatewayException(
|
||||||
|
$"Galaxy DiscoverHierarchy returned a repeated page token '{pageToken}'.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
while (!string.IsNullOrWhiteSpace(pageToken));
|
||||||
|
|
||||||
|
return aggregate;
|
||||||
|
}
|
||||||
|
|
||||||
private static async Task<int> GalaxyWatchAsync(
|
private static async Task<int> GalaxyWatchAsync(
|
||||||
CliArguments arguments,
|
CliArguments arguments,
|
||||||
IMxGatewayCliClient client,
|
IMxGatewayCliClient client,
|
||||||
|
|||||||
@@ -140,6 +140,26 @@ public sealed class GalaxyRepositoryClientTests
|
|||||||
Assert.False(call.CallOptions.CancellationToken.IsCancellationRequested);
|
Assert.False(call.CallOptions.CancellationToken.IsCancellationRequested);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task DiscoverHierarchyAsync_WithRepeatedPageToken_ThrowsProtocolError()
|
||||||
|
{
|
||||||
|
FakeGalaxyRepositoryTransport transport = CreateTransport();
|
||||||
|
transport.DiscoverHierarchyReplies.Enqueue(new DiscoverHierarchyReply
|
||||||
|
{
|
||||||
|
NextPageToken = "7:1",
|
||||||
|
});
|
||||||
|
transport.DiscoverHierarchyReplies.Enqueue(new DiscoverHierarchyReply
|
||||||
|
{
|
||||||
|
NextPageToken = "7:1",
|
||||||
|
});
|
||||||
|
await using GalaxyRepositoryClient client = CreateClient(transport);
|
||||||
|
|
||||||
|
MxGatewayException exception = await Assert.ThrowsAsync<MxGatewayException>(
|
||||||
|
async () => await client.DiscoverHierarchyAsync());
|
||||||
|
|
||||||
|
Assert.Contains("repeated page token", exception.Message, StringComparison.Ordinal);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task TestConnectionAsync_RetriesOnTransientGrpcFailure()
|
public async Task TestConnectionAsync_RetriesOnTransientGrpcFailure()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -207,8 +207,10 @@ public sealed class MxGatewayClientCliTests
|
|||||||
using var output = new StringWriter();
|
using var output = new StringWriter();
|
||||||
using var error = new StringWriter();
|
using var error = new StringWriter();
|
||||||
FakeCliClient fakeClient = new();
|
FakeCliClient fakeClient = new();
|
||||||
fakeClient.GalaxyDiscoverHierarchyReply = new DiscoverHierarchyReply
|
fakeClient.GalaxyDiscoverHierarchyReplies.Enqueue(new DiscoverHierarchyReply
|
||||||
{
|
{
|
||||||
|
NextPageToken = "7:1",
|
||||||
|
TotalObjectCount = 2,
|
||||||
Objects =
|
Objects =
|
||||||
{
|
{
|
||||||
new GalaxyObject
|
new GalaxyObject
|
||||||
@@ -227,7 +229,21 @@ public sealed class MxGatewayClientCliTests
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
});
|
||||||
|
fakeClient.GalaxyDiscoverHierarchyReplies.Enqueue(new DiscoverHierarchyReply
|
||||||
|
{
|
||||||
|
TotalObjectCount = 2,
|
||||||
|
Objects =
|
||||||
|
{
|
||||||
|
new GalaxyObject
|
||||||
|
{
|
||||||
|
GobjectId = 8,
|
||||||
|
TagName = "DelmiaReceiver_002",
|
||||||
|
ContainedName = "DelmiaReceiver",
|
||||||
|
ParentGobjectId = 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
int exitCode = await MxGatewayClientCli.RunAsync(
|
int exitCode = await MxGatewayClientCli.RunAsync(
|
||||||
[
|
[
|
||||||
@@ -242,10 +258,14 @@ public sealed class MxGatewayClientCliTests
|
|||||||
_ => fakeClient);
|
_ => fakeClient);
|
||||||
|
|
||||||
Assert.Equal(0, exitCode);
|
Assert.Equal(0, exitCode);
|
||||||
Assert.Single(fakeClient.GalaxyDiscoverHierarchyRequests);
|
Assert.Equal(2, fakeClient.GalaxyDiscoverHierarchyRequests.Count);
|
||||||
|
Assert.Equal(5000, fakeClient.GalaxyDiscoverHierarchyRequests[0].PageSize);
|
||||||
|
Assert.Equal("", fakeClient.GalaxyDiscoverHierarchyRequests[0].PageToken);
|
||||||
|
Assert.Equal("7:1", fakeClient.GalaxyDiscoverHierarchyRequests[1].PageToken);
|
||||||
string text = output.ToString();
|
string text = output.ToString();
|
||||||
Assert.Contains("objects=1", text);
|
Assert.Contains("objects=2", text);
|
||||||
Assert.Contains("DelmiaReceiver_001", text);
|
Assert.Contains("DelmiaReceiver_001", text);
|
||||||
|
Assert.Contains("DelmiaReceiver_002", text);
|
||||||
Assert.Contains("attributes=1", text);
|
Assert.Contains("attributes=1", text);
|
||||||
Assert.Equal(string.Empty, error.ToString());
|
Assert.Equal(string.Empty, error.ToString());
|
||||||
}
|
}
|
||||||
@@ -411,6 +431,8 @@ public sealed class MxGatewayClientCliTests
|
|||||||
|
|
||||||
public DiscoverHierarchyReply GalaxyDiscoverHierarchyReply { get; set; } = new();
|
public DiscoverHierarchyReply GalaxyDiscoverHierarchyReply { get; set; } = new();
|
||||||
|
|
||||||
|
public Queue<DiscoverHierarchyReply> GalaxyDiscoverHierarchyReplies { get; } = new();
|
||||||
|
|
||||||
public List<TestConnectionRequest> GalaxyTestConnectionRequests { get; } = [];
|
public List<TestConnectionRequest> GalaxyTestConnectionRequests { get; } = [];
|
||||||
|
|
||||||
public List<GetLastDeployTimeRequest> GalaxyGetLastDeployTimeRequests { get; } = [];
|
public List<GetLastDeployTimeRequest> GalaxyGetLastDeployTimeRequests { get; } = [];
|
||||||
@@ -438,7 +460,10 @@ public sealed class MxGatewayClientCliTests
|
|||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
GalaxyDiscoverHierarchyRequests.Add(request);
|
GalaxyDiscoverHierarchyRequests.Add(request);
|
||||||
return Task.FromResult(GalaxyDiscoverHierarchyReply);
|
return Task.FromResult(
|
||||||
|
GalaxyDiscoverHierarchyReplies.TryDequeue(out DiscoverHierarchyReply? reply)
|
||||||
|
? reply
|
||||||
|
: GalaxyDiscoverHierarchyReply);
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<WatchDeployEventsRequest> GalaxyWatchDeployEventsRequests { get; } = [];
|
public List<WatchDeployEventsRequest> GalaxyWatchDeployEventsRequests { get; } = [];
|
||||||
|
|||||||
@@ -146,6 +146,7 @@ public sealed class GalaxyRepositoryClient : IAsyncDisposable
|
|||||||
public async Task<IReadOnlyList<GalaxyObject>> DiscoverHierarchyAsync(CancellationToken cancellationToken = default)
|
public async Task<IReadOnlyList<GalaxyObject>> DiscoverHierarchyAsync(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
List<GalaxyObject> objects = [];
|
List<GalaxyObject> objects = [];
|
||||||
|
HashSet<string> seenPageTokens = new(StringComparer.Ordinal);
|
||||||
string pageToken = string.Empty;
|
string pageToken = string.Empty;
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
@@ -160,6 +161,12 @@ public sealed class GalaxyRepositoryClient : IAsyncDisposable
|
|||||||
|
|
||||||
objects.AddRange(reply.Objects);
|
objects.AddRange(reply.Objects);
|
||||||
pageToken = reply.NextPageToken;
|
pageToken = reply.NextPageToken;
|
||||||
|
if (!string.IsNullOrWhiteSpace(pageToken)
|
||||||
|
&& !seenPageTokens.Add(pageToken))
|
||||||
|
{
|
||||||
|
throw new MxGatewayException(
|
||||||
|
$"Galaxy DiscoverHierarchy returned a repeated page token '{pageToken}'.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
while (!string.IsNullOrWhiteSpace(pageToken));
|
while (!string.IsNullOrWhiteSpace(pageToken));
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package mxgateway
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -148,6 +149,7 @@ func (c *GalaxyClient) DiscoverHierarchy(ctx context.Context) ([]*GalaxyObject,
|
|||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
var objects []*GalaxyObject
|
var objects []*GalaxyObject
|
||||||
|
seenPageTokens := make(map[string]struct{})
|
||||||
pageToken := ""
|
pageToken := ""
|
||||||
for {
|
for {
|
||||||
reply, err := c.raw.DiscoverHierarchy(callCtx, &pb.DiscoverHierarchyRequest{
|
reply, err := c.raw.DiscoverHierarchy(callCtx, &pb.DiscoverHierarchyRequest{
|
||||||
@@ -162,6 +164,10 @@ func (c *GalaxyClient) DiscoverHierarchy(ctx context.Context) ([]*GalaxyObject,
|
|||||||
if pageToken == "" {
|
if pageToken == "" {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
if _, seen := seenPageTokens[pageToken]; seen {
|
||||||
|
return nil, fmt.Errorf("mxgateway: galaxy discover hierarchy returned repeated page token %q", pageToken)
|
||||||
|
}
|
||||||
|
seenPageTokens[pageToken] = struct{}{}
|
||||||
}
|
}
|
||||||
return objects, nil
|
return objects, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"net"
|
"net"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -159,6 +160,25 @@ func TestGalaxyDiscoverHierarchyReturnsObjects(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGalaxyDiscoverHierarchyRejectsRepeatedPageToken(t *testing.T) {
|
||||||
|
fake := &fakeGalaxyServer{
|
||||||
|
discoverReplies: []*pb.DiscoverHierarchyReply{
|
||||||
|
{NextPageToken: "7:1"},
|
||||||
|
{NextPageToken: "7:1"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
client, cleanup := newGalaxyBufconnClient(t, fake)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
_, err := client.DiscoverHierarchy(context.Background())
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("DiscoverHierarchy() error = nil, want repeated token error")
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "repeated page token") {
|
||||||
|
t.Fatalf("error = %v, want repeated page token", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestGalaxyDialReturnsGatewayErrorOnRpcFailure(t *testing.T) {
|
func TestGalaxyDialReturnsGatewayErrorOnRpcFailure(t *testing.T) {
|
||||||
fake := &fakeGalaxyServer{failTest: true}
|
fake := &fakeGalaxyServer{failTest: true}
|
||||||
client, cleanup := newGalaxyBufconnClient(t, fake)
|
client, cleanup := newGalaxyBufconnClient(t, fake)
|
||||||
|
|||||||
+14
-3
@@ -133,6 +133,7 @@ public final class GalaxyRepositoryClient implements AutoCloseable {
|
|||||||
public List<GalaxyObject> discoverHierarchy() {
|
public List<GalaxyObject> discoverHierarchy() {
|
||||||
try {
|
try {
|
||||||
java.util.ArrayList<GalaxyObject> objects = new java.util.ArrayList<>();
|
java.util.ArrayList<GalaxyObject> objects = new java.util.ArrayList<>();
|
||||||
|
java.util.HashSet<String> seenPageTokens = new java.util.HashSet<>();
|
||||||
String pageToken = "";
|
String pageToken = "";
|
||||||
do {
|
do {
|
||||||
DiscoverHierarchyReply reply = rawBlockingStub().discoverHierarchy(DiscoverHierarchyRequest.newBuilder()
|
DiscoverHierarchyReply reply = rawBlockingStub().discoverHierarchy(DiscoverHierarchyRequest.newBuilder()
|
||||||
@@ -141,6 +142,10 @@ public final class GalaxyRepositoryClient implements AutoCloseable {
|
|||||||
.build());
|
.build());
|
||||||
objects.addAll(reply.getObjectsList());
|
objects.addAll(reply.getObjectsList());
|
||||||
pageToken = reply.getNextPageToken();
|
pageToken = reply.getNextPageToken();
|
||||||
|
if (!pageToken.isBlank() && !seenPageTokens.add(pageToken)) {
|
||||||
|
throw new MxGatewayException(
|
||||||
|
"galaxy discover hierarchy returned repeated page token: " + pageToken);
|
||||||
|
}
|
||||||
} while (!pageToken.isBlank());
|
} while (!pageToken.isBlank());
|
||||||
return objects;
|
return objects;
|
||||||
} catch (RuntimeException error) {
|
} catch (RuntimeException error) {
|
||||||
@@ -152,7 +157,7 @@ public final class GalaxyRepositoryClient implements AutoCloseable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public CompletableFuture<List<GalaxyObject>> discoverHierarchyAsync() {
|
public CompletableFuture<List<GalaxyObject>> discoverHierarchyAsync() {
|
||||||
return discoverHierarchyPageAsync("", new java.util.ArrayList<>());
|
return discoverHierarchyPageAsync("", new java.util.ArrayList<>(), new java.util.HashSet<>());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -268,7 +273,7 @@ public final class GalaxyRepositoryClient implements AutoCloseable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private CompletableFuture<List<GalaxyObject>> discoverHierarchyPageAsync(
|
private CompletableFuture<List<GalaxyObject>> discoverHierarchyPageAsync(
|
||||||
String pageToken, java.util.ArrayList<GalaxyObject> objects) {
|
String pageToken, java.util.ArrayList<GalaxyObject> objects, java.util.HashSet<String> seenPageTokens) {
|
||||||
DiscoverHierarchyRequest request = DiscoverHierarchyRequest.newBuilder()
|
DiscoverHierarchyRequest request = DiscoverHierarchyRequest.newBuilder()
|
||||||
.setPageSize(DISCOVER_HIERARCHY_PAGE_SIZE)
|
.setPageSize(DISCOVER_HIERARCHY_PAGE_SIZE)
|
||||||
.setPageToken(pageToken)
|
.setPageToken(pageToken)
|
||||||
@@ -278,7 +283,13 @@ public final class GalaxyRepositoryClient implements AutoCloseable {
|
|||||||
if (reply.getNextPageToken().isBlank()) {
|
if (reply.getNextPageToken().isBlank()) {
|
||||||
return CompletableFuture.completedFuture(objects);
|
return CompletableFuture.completedFuture(objects);
|
||||||
}
|
}
|
||||||
return discoverHierarchyPageAsync(reply.getNextPageToken(), objects);
|
if (!seenPageTokens.add(reply.getNextPageToken())) {
|
||||||
|
CompletableFuture<List<GalaxyObject>> failed = new CompletableFuture<>();
|
||||||
|
failed.completeExceptionally(new MxGatewayException(
|
||||||
|
"galaxy discover hierarchy returned repeated page token: " + reply.getNextPageToken()));
|
||||||
|
return failed;
|
||||||
|
}
|
||||||
|
return discoverHierarchyPageAsync(reply.getNextPageToken(), objects, seenPageTokens);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+22
@@ -3,6 +3,7 @@ package com.dohertylan.mxgateway.client;
|
|||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
import com.google.protobuf.Timestamp;
|
import com.google.protobuf.Timestamp;
|
||||||
@@ -174,6 +175,27 @@ final class GalaxyRepositoryClientTests {
|
|||||||
assertFalse(stream.hasNext());
|
assertFalse(stream.hasNext());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void discoverHierarchyRejectsRepeatedPageToken() throws Exception {
|
||||||
|
TestService service = new TestService() {
|
||||||
|
@Override
|
||||||
|
public void discoverHierarchy(
|
||||||
|
DiscoverHierarchyRequest request, StreamObserver<DiscoverHierarchyReply> responseObserver) {
|
||||||
|
responseObserver.onNext(DiscoverHierarchyReply.newBuilder()
|
||||||
|
.setNextPageToken("7:1")
|
||||||
|
.build());
|
||||||
|
responseObserver.onCompleted();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
try (InProcessGalaxy g = InProcessGalaxy.start(service, new AtomicReference<>());
|
||||||
|
GalaxyRepositoryClient client = g.client("")) {
|
||||||
|
MxGatewayException error = assertThrows(MxGatewayException.class, client::discoverHierarchy);
|
||||||
|
|
||||||
|
assertTrue(error.getMessage().contains("repeated page token"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void watchDeployEventsReceivesEventsInOrder() throws Exception {
|
void watchDeployEventsReceivesEventsInOrder() throws Exception {
|
||||||
DeployEvent first = DeployEvent.newBuilder()
|
DeployEvent first = DeployEvent.newBuilder()
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import grpc
|
|||||||
from google.protobuf.timestamp_pb2 import Timestamp
|
from google.protobuf.timestamp_pb2 import Timestamp
|
||||||
|
|
||||||
from .auth import merge_metadata
|
from .auth import merge_metadata
|
||||||
from .errors import map_rpc_error
|
from .errors import MxGatewayError, map_rpc_error
|
||||||
from .generated import galaxy_repository_pb2 as galaxy_pb
|
from .generated import galaxy_repository_pb2 as galaxy_pb
|
||||||
from .generated import galaxy_repository_pb2_grpc as galaxy_pb_grpc
|
from .generated import galaxy_repository_pb2_grpc as galaxy_pb_grpc
|
||||||
from .options import ClientOptions, create_channel
|
from .options import ClientOptions, create_channel
|
||||||
@@ -115,6 +115,7 @@ class GalaxyRepositoryClient:
|
|||||||
"""Return the deployed Galaxy object hierarchy as raw proto messages."""
|
"""Return the deployed Galaxy object hierarchy as raw proto messages."""
|
||||||
|
|
||||||
objects: list[galaxy_pb.GalaxyObject] = []
|
objects: list[galaxy_pb.GalaxyObject] = []
|
||||||
|
seen_page_tokens: set[str] = set()
|
||||||
page_token = ""
|
page_token = ""
|
||||||
while True:
|
while True:
|
||||||
reply = await self._unary(
|
reply = await self._unary(
|
||||||
@@ -129,6 +130,11 @@ class GalaxyRepositoryClient:
|
|||||||
page_token = reply.next_page_token
|
page_token = reply.next_page_token
|
||||||
if not page_token:
|
if not page_token:
|
||||||
return objects
|
return objects
|
||||||
|
if page_token in seen_page_tokens:
|
||||||
|
raise MxGatewayError(
|
||||||
|
f"galaxy discover hierarchy returned repeated page token {page_token!r}"
|
||||||
|
)
|
||||||
|
seen_page_tokens.add(page_token)
|
||||||
|
|
||||||
def watch_deploy_events(
|
def watch_deploy_events(
|
||||||
self,
|
self,
|
||||||
|
|||||||
@@ -148,6 +148,22 @@ async def test_discover_hierarchy_returns_proto_objects() -> None:
|
|||||||
assert objects[1].attributes[0].full_tag_reference == "DelmiaReceiver_001.DownloadPath"
|
assert objects[1].attributes[0].full_tag_reference == "DelmiaReceiver_001.DownloadPath"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_discover_hierarchy_rejects_repeated_page_token() -> None:
|
||||||
|
stub = FakeGalaxyStub()
|
||||||
|
stub.discover_hierarchy.replies = [
|
||||||
|
galaxy_pb.DiscoverHierarchyReply(next_page_token="7:1"),
|
||||||
|
galaxy_pb.DiscoverHierarchyReply(next_page_token="7:1"),
|
||||||
|
]
|
||||||
|
client = await GalaxyRepositoryClient.connect(
|
||||||
|
ClientOptions(endpoint="fake", plaintext=True),
|
||||||
|
stub=stub,
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(Exception, match="repeated page token"):
|
||||||
|
await client.discover_hierarchy()
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_watch_deploy_events_yields_events_in_order() -> None:
|
async def test_watch_deploy_events_yields_events_in_order() -> None:
|
||||||
ts1 = Timestamp()
|
ts1 = Timestamp()
|
||||||
|
|||||||
@@ -144,6 +144,7 @@ impl GalaxyClient {
|
|||||||
/// the object's identifying names plus its dynamic attributes.
|
/// the object's identifying names plus its dynamic attributes.
|
||||||
pub async fn discover_hierarchy(&mut self) -> Result<Vec<GalaxyObject>, Error> {
|
pub async fn discover_hierarchy(&mut self) -> Result<Vec<GalaxyObject>, Error> {
|
||||||
let mut objects = Vec::new();
|
let mut objects = Vec::new();
|
||||||
|
let mut seen_page_tokens = std::collections::HashSet::new();
|
||||||
let mut page_token = String::new();
|
let mut page_token = String::new();
|
||||||
loop {
|
loop {
|
||||||
let response = self
|
let response = self
|
||||||
@@ -159,6 +160,14 @@ impl GalaxyClient {
|
|||||||
if page_token.is_empty() {
|
if page_token.is_empty() {
|
||||||
return Ok(objects);
|
return Ok(objects);
|
||||||
}
|
}
|
||||||
|
if !seen_page_tokens.insert(page_token.clone()) {
|
||||||
|
return Err(Error::InvalidArgument {
|
||||||
|
name: "page_token".to_owned(),
|
||||||
|
detail: format!(
|
||||||
|
"galaxy discover hierarchy returned repeated page token `{page_token}`"
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -517,6 +526,37 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn discover_hierarchy_rejects_repeated_page_token() {
|
||||||
|
let state = Arc::new(FakeState::default());
|
||||||
|
state
|
||||||
|
.discover_replies
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.push_back(DiscoverHierarchyReply {
|
||||||
|
objects: Vec::new(),
|
||||||
|
next_page_token: "7:1".to_owned(),
|
||||||
|
total_object_count: 1,
|
||||||
|
});
|
||||||
|
state
|
||||||
|
.discover_replies
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.push_back(DiscoverHierarchyReply {
|
||||||
|
objects: Vec::new(),
|
||||||
|
next_page_token: "7:1".to_owned(),
|
||||||
|
total_object_count: 1,
|
||||||
|
});
|
||||||
|
let endpoint = spawn_fake(state).await;
|
||||||
|
let mut client = GalaxyClient::connect(ClientOptions::new(endpoint))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let error = client.discover_hierarchy().await.unwrap_err();
|
||||||
|
|
||||||
|
assert!(error.to_string().contains("repeated page token"));
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn watch_deploy_events_yields_events_in_order() {
|
async fn watch_deploy_events_yields_events_in_order() {
|
||||||
let state = Arc::new(FakeState::default());
|
let state = Arc::new(FakeState::default());
|
||||||
|
|||||||
@@ -99,6 +99,14 @@ public sealed class GalaxyHierarchyCache : IGalaxyHierarchyCache
|
|||||||
LastQueriedAt = queriedAt,
|
LastQueriedAt = queriedAt,
|
||||||
LastSuccessAt = queriedAt,
|
LastSuccessAt = queriedAt,
|
||||||
LastError = null,
|
LastError = null,
|
||||||
|
DashboardSummary = previous.DashboardSummary with
|
||||||
|
{
|
||||||
|
Status = DashboardGalaxyStatus.Healthy,
|
||||||
|
LastQueriedAt = queriedAt,
|
||||||
|
LastSuccessAt = queriedAt,
|
||||||
|
LastDeployTime = deployTime,
|
||||||
|
LastError = null,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
Volatile.Write(ref _current, refreshed);
|
Volatile.Write(ref _current, refreshed);
|
||||||
_firstLoad.TrySetResult();
|
_firstLoad.TrySetResult();
|
||||||
|
|||||||
@@ -68,7 +68,8 @@ public sealed class GalaxyRepositoryGrpcService(
|
|||||||
ResolveUnavailableMessage(entry)));
|
ResolveUnavailableMessage(entry)));
|
||||||
}
|
}
|
||||||
|
|
||||||
int offset = ParsePageToken(request.PageToken);
|
PageToken pageToken = ParsePageToken(request.PageToken, entry.Sequence);
|
||||||
|
int offset = pageToken.Offset;
|
||||||
if (offset > entry.Objects.Count)
|
if (offset > entry.Objects.Count)
|
||||||
{
|
{
|
||||||
throw new RpcException(new Status(
|
throw new RpcException(new Status(
|
||||||
@@ -90,7 +91,7 @@ public sealed class GalaxyRepositoryGrpcService(
|
|||||||
int nextOffset = offset + take;
|
int nextOffset = offset + take;
|
||||||
if (nextOffset < entry.Objects.Count)
|
if (nextOffset < entry.Objects.Count)
|
||||||
{
|
{
|
||||||
reply.NextPageToken = nextOffset.ToString(System.Globalization.CultureInfo.InvariantCulture);
|
reply.NextPageToken = FormatPageToken(entry.Sequence, nextOffset);
|
||||||
}
|
}
|
||||||
|
|
||||||
return reply;
|
return reply;
|
||||||
@@ -182,15 +183,30 @@ public sealed class GalaxyRepositoryGrpcService(
|
|||||||
return Math.Min(pageSize, MaxDiscoverPageSize);
|
return Math.Min(pageSize, MaxDiscoverPageSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int ParsePageToken(string pageToken)
|
private static string FormatPageToken(long sequence, int offset)
|
||||||
|
{
|
||||||
|
return string.Concat(
|
||||||
|
sequence.ToString(System.Globalization.CultureInfo.InvariantCulture),
|
||||||
|
":",
|
||||||
|
offset.ToString(System.Globalization.CultureInfo.InvariantCulture));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PageToken ParsePageToken(string pageToken, long currentSequence)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(pageToken))
|
if (string.IsNullOrWhiteSpace(pageToken))
|
||||||
{
|
{
|
||||||
return 0;
|
return new PageToken(currentSequence, Offset: 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!int.TryParse(
|
string[] parts = pageToken.Split(':', count: 2);
|
||||||
pageToken,
|
if (parts.Length != 2
|
||||||
|
|| !long.TryParse(
|
||||||
|
parts[0],
|
||||||
|
System.Globalization.NumberStyles.None,
|
||||||
|
System.Globalization.CultureInfo.InvariantCulture,
|
||||||
|
out long sequence)
|
||||||
|
|| !int.TryParse(
|
||||||
|
parts[1],
|
||||||
System.Globalization.NumberStyles.None,
|
System.Globalization.NumberStyles.None,
|
||||||
System.Globalization.CultureInfo.InvariantCulture,
|
System.Globalization.CultureInfo.InvariantCulture,
|
||||||
out int offset)
|
out int offset)
|
||||||
@@ -201,9 +217,18 @@ public sealed class GalaxyRepositoryGrpcService(
|
|||||||
"DiscoverHierarchy page_token is invalid."));
|
"DiscoverHierarchy page_token is invalid."));
|
||||||
}
|
}
|
||||||
|
|
||||||
return offset;
|
if (sequence != currentSequence)
|
||||||
|
{
|
||||||
|
throw new RpcException(new Status(
|
||||||
|
StatusCode.InvalidArgument,
|
||||||
|
"DiscoverHierarchy page_token is stale."));
|
||||||
|
}
|
||||||
|
|
||||||
|
return new PageToken(sequence, offset);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private sealed record PageToken(long Sequence, int Offset);
|
||||||
|
|
||||||
[System.Diagnostics.CodeAnalysis.SuppressMessage(
|
[System.Diagnostics.CodeAnalysis.SuppressMessage(
|
||||||
"Style",
|
"Style",
|
||||||
"IDE0051:Remove unused private members",
|
"IDE0051:Remove unused private members",
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ public sealed class GalaxyRepositoryGrpcServiceTests
|
|||||||
Assert.Equal(2, reply.Objects.Count);
|
Assert.Equal(2, reply.Objects.Count);
|
||||||
Assert.Equal("Object_001", reply.Objects[0].TagName);
|
Assert.Equal("Object_001", reply.Objects[0].TagName);
|
||||||
Assert.Equal("Object_002", reply.Objects[1].TagName);
|
Assert.Equal("Object_002", reply.Objects[1].TagName);
|
||||||
Assert.Equal("2", reply.NextPageToken);
|
Assert.Equal("7:2", reply.NextPageToken);
|
||||||
Assert.Equal(3, reply.TotalObjectCount);
|
Assert.Equal(3, reply.TotalObjectCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,7 +37,7 @@ public sealed class GalaxyRepositoryGrpcServiceTests
|
|||||||
new DiscoverHierarchyRequest
|
new DiscoverHierarchyRequest
|
||||||
{
|
{
|
||||||
PageSize = 2,
|
PageSize = 2,
|
||||||
PageToken = "2",
|
PageToken = "7:2",
|
||||||
},
|
},
|
||||||
new TestServerCallContext());
|
new TestServerCallContext());
|
||||||
|
|
||||||
@@ -50,7 +50,8 @@ public sealed class GalaxyRepositoryGrpcServiceTests
|
|||||||
[Theory]
|
[Theory]
|
||||||
[InlineData("-1", 1)]
|
[InlineData("-1", 1)]
|
||||||
[InlineData("not-an-offset", 1)]
|
[InlineData("not-an-offset", 1)]
|
||||||
[InlineData("4", 1)]
|
[InlineData("7:4", 1)]
|
||||||
|
[InlineData("6:2", 1)]
|
||||||
[InlineData("", -1)]
|
[InlineData("", -1)]
|
||||||
public async Task DiscoverHierarchy_WithInvalidPagingArguments_ReturnsInvalidArgument(
|
public async Task DiscoverHierarchy_WithInvalidPagingArguments_ReturnsInvalidArgument(
|
||||||
string pageToken,
|
string pageToken,
|
||||||
@@ -88,6 +89,7 @@ public sealed class GalaxyRepositoryGrpcServiceTests
|
|||||||
return GalaxyHierarchyCacheEntry.Empty with
|
return GalaxyHierarchyCacheEntry.Empty with
|
||||||
{
|
{
|
||||||
Status = GalaxyCacheStatus.Healthy,
|
Status = GalaxyCacheStatus.Healthy,
|
||||||
|
Sequence = 7,
|
||||||
LastSuccessAt = DateTimeOffset.UtcNow,
|
LastSuccessAt = DateTimeOffset.UtcNow,
|
||||||
Objects = objects,
|
Objects = objects,
|
||||||
DashboardSummary = DashboardGalaxySummary.Unknown with
|
DashboardSummary = DashboardGalaxySummary.Unknown with
|
||||||
|
|||||||
Reference in New Issue
Block a user