Merge origin/main with local pending work and update AGENTS.md references
- Resolve 14 conflicts from popping local stash on top of origin'seed1e88+8d3352fdoc-comment additions (11 mechanical, plus version.rs, DashboardAuthenticatorTests.cs, DashboardGalaxyProjector.cs) - Fix 4 test files that used AGENTS.md as the repo-root sentinel (now use CLAUDE.md, since AGENTS.md was removed in4731ab5) - Redirect 10 doc citations from AGENTS.md to the matching gateway.md sections (Value Model, Status Model, Security, STA Worker Thread Model, gRPC Layer rule, cancellation rule) Verified: solution build clean, x86 worker build clean, 266/266 gateway tests passing, 121/121 worker tests passing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -824,9 +824,7 @@ public static class MxGatewayClientCli
|
||||
TextWriter output,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
DiscoverHierarchyReply reply = await client.GalaxyDiscoverHierarchyAsync(
|
||||
new DiscoverHierarchyRequest(),
|
||||
cancellationToken)
|
||||
DiscoverHierarchyReply reply = await DiscoverAllGalaxyHierarchyAsync(client, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (arguments.HasFlag("json"))
|
||||
@@ -844,6 +842,39 @@ public static class MxGatewayClientCli
|
||||
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(
|
||||
CliArguments arguments,
|
||||
IMxGatewayCliClient client,
|
||||
|
||||
@@ -48,6 +48,8 @@ internal sealed class FakeGalaxyRepositoryTransport(MxGatewayClientOptions optio
|
||||
/// </summary>
|
||||
public DiscoverHierarchyReply DiscoverHierarchyReply { get; set; } = new();
|
||||
|
||||
public Queue<DiscoverHierarchyReply> DiscoverHierarchyReplies { get; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the queue of exceptions to throw from TestConnection; dequeued in FIFO order.
|
||||
/// </summary>
|
||||
@@ -114,7 +116,10 @@ internal sealed class FakeGalaxyRepositoryTransport(MxGatewayClientOptions optio
|
||||
throw exception;
|
||||
}
|
||||
|
||||
return Task.FromResult(DiscoverHierarchyReply);
|
||||
return Task.FromResult(
|
||||
DiscoverHierarchyReplies.TryDequeue(out DiscoverHierarchyReply? reply)
|
||||
? reply
|
||||
: DiscoverHierarchyReply);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -83,8 +83,10 @@ public sealed class GalaxyRepositoryClientTests
|
||||
public async Task DiscoverHierarchyAsync_ReturnsObjectsFromReply()
|
||||
{
|
||||
FakeGalaxyRepositoryTransport transport = CreateTransport();
|
||||
transport.DiscoverHierarchyReply = new DiscoverHierarchyReply
|
||||
transport.DiscoverHierarchyReplies.Enqueue(new DiscoverHierarchyReply
|
||||
{
|
||||
NextPageToken = "page-2",
|
||||
TotalObjectCount = 2,
|
||||
Objects =
|
||||
{
|
||||
new GalaxyObject
|
||||
@@ -106,12 +108,29 @@ public sealed class GalaxyRepositoryClientTests
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
transport.DiscoverHierarchyReplies.Enqueue(new DiscoverHierarchyReply
|
||||
{
|
||||
TotalObjectCount = 2,
|
||||
Objects =
|
||||
{
|
||||
new GalaxyObject
|
||||
{
|
||||
GobjectId = 13,
|
||||
TagName = "DelmiaReceiver_002",
|
||||
},
|
||||
},
|
||||
});
|
||||
await using GalaxyRepositoryClient client = CreateClient(transport);
|
||||
|
||||
IReadOnlyList<GalaxyObject> objects = await client.DiscoverHierarchyAsync();
|
||||
|
||||
GalaxyObject obj = Assert.Single(objects);
|
||||
Assert.Equal(2, objects.Count);
|
||||
Assert.Equal(2, transport.DiscoverHierarchyCalls.Count);
|
||||
Assert.Equal(5000, transport.DiscoverHierarchyCalls[0].Request.PageSize);
|
||||
Assert.Equal("", transport.DiscoverHierarchyCalls[0].Request.PageToken);
|
||||
Assert.Equal("page-2", transport.DiscoverHierarchyCalls[1].Request.PageToken);
|
||||
GalaxyObject obj = objects[0];
|
||||
Assert.Equal(12, obj.GobjectId);
|
||||
Assert.Equal("DelmiaReceiver_001", obj.TagName);
|
||||
GalaxyAttribute attribute = Assert.Single(obj.Attributes);
|
||||
@@ -142,6 +161,57 @@ public sealed class GalaxyRepositoryClientTests
|
||||
/// <summary>
|
||||
/// Verifies that TestConnectionAsync retries on transient gRPC failures.
|
||||
/// </summary>
|
||||
[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]
|
||||
public async Task DiscoverHierarchyAsync_WithOptions_MapsTypedFilters()
|
||||
{
|
||||
FakeGalaxyRepositoryTransport transport = CreateTransport();
|
||||
await using GalaxyRepositoryClient client = CreateClient(transport);
|
||||
|
||||
await client.DiscoverHierarchyAsync(new DiscoverHierarchyOptions
|
||||
{
|
||||
RootContainedPath = "Area1/Line3",
|
||||
MaxDepth = 2,
|
||||
CategoryIds = [10, 13],
|
||||
TemplateChainContains = ["Pump"],
|
||||
TagNameGlob = "Pump_*",
|
||||
IncludeAttributes = false,
|
||||
AlarmBearingOnly = true,
|
||||
HistorizedOnly = true,
|
||||
});
|
||||
|
||||
DiscoverHierarchyRequest request = Assert.Single(transport.DiscoverHierarchyCalls).Request;
|
||||
Assert.Equal(DiscoverHierarchyRequest.RootOneofCase.RootContainedPath, request.RootCase);
|
||||
Assert.Equal("Area1/Line3", request.RootContainedPath);
|
||||
Assert.Equal(2, request.MaxDepth);
|
||||
Assert.Equal([10, 13], request.CategoryIds);
|
||||
Assert.Equal(["Pump"], request.TemplateChainContains);
|
||||
Assert.Equal("Pump_*", request.TagNameGlob);
|
||||
Assert.True(request.HasIncludeAttributes);
|
||||
Assert.False(request.IncludeAttributes);
|
||||
Assert.True(request.AlarmBearingOnly);
|
||||
Assert.True(request.HistorizedOnly);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TestConnectionAsync_RetriesOnTransientGrpcFailure()
|
||||
{
|
||||
|
||||
@@ -18,7 +18,7 @@ public sealed class MxGatewayClientCliTests
|
||||
var exitCode = MxGatewayClientCli.Run(["version"], output, error);
|
||||
|
||||
Assert.Equal(0, exitCode);
|
||||
Assert.Contains("gateway-protocol=1", output.ToString());
|
||||
Assert.Contains("gateway-protocol=2", output.ToString());
|
||||
Assert.Contains("worker-protocol=1", output.ToString());
|
||||
Assert.Equal(string.Empty, error.ToString());
|
||||
}
|
||||
@@ -33,7 +33,7 @@ public sealed class MxGatewayClientCliTests
|
||||
int exitCode = await MxGatewayClientCli.RunAsync(["version", "--json"], output, error);
|
||||
|
||||
Assert.Equal(0, exitCode);
|
||||
Assert.Contains("\"gatewayProtocolVersion\":1", output.ToString());
|
||||
Assert.Contains("\"gatewayProtocolVersion\":2", output.ToString());
|
||||
Assert.Equal(string.Empty, error.ToString());
|
||||
}
|
||||
|
||||
@@ -216,8 +216,10 @@ public sealed class MxGatewayClientCliTests
|
||||
using var output = new StringWriter();
|
||||
using var error = new StringWriter();
|
||||
FakeCliClient fakeClient = new();
|
||||
fakeClient.GalaxyDiscoverHierarchyReply = new DiscoverHierarchyReply
|
||||
fakeClient.GalaxyDiscoverHierarchyReplies.Enqueue(new DiscoverHierarchyReply
|
||||
{
|
||||
NextPageToken = "7:1",
|
||||
TotalObjectCount = 2,
|
||||
Objects =
|
||||
{
|
||||
new GalaxyObject
|
||||
@@ -236,7 +238,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(
|
||||
[
|
||||
@@ -251,10 +267,14 @@ public sealed class MxGatewayClientCliTests
|
||||
_ => fakeClient);
|
||||
|
||||
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();
|
||||
Assert.Contains("objects=1", text);
|
||||
Assert.Contains("objects=2", text);
|
||||
Assert.Contains("DelmiaReceiver_001", text);
|
||||
Assert.Contains("DelmiaReceiver_002", text);
|
||||
Assert.Contains("attributes=1", text);
|
||||
Assert.Equal(string.Empty, error.ToString());
|
||||
}
|
||||
@@ -436,6 +456,8 @@ public sealed class MxGatewayClientCliTests
|
||||
/// <summary>Galaxy discover hierarchy reply to return.</summary>
|
||||
public DiscoverHierarchyReply GalaxyDiscoverHierarchyReply { get; set; } = new();
|
||||
|
||||
public Queue<DiscoverHierarchyReply> GalaxyDiscoverHierarchyReplies { get; } = new();
|
||||
|
||||
/// <summary>List of received galaxy test connection requests.</summary>
|
||||
public List<TestConnectionRequest> GalaxyTestConnectionRequests { get; } = [];
|
||||
|
||||
@@ -469,7 +491,10 @@ public sealed class MxGatewayClientCliTests
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
GalaxyDiscoverHierarchyRequests.Add(request);
|
||||
return Task.FromResult(GalaxyDiscoverHierarchyReply);
|
||||
return Task.FromResult(
|
||||
GalaxyDiscoverHierarchyReplies.TryDequeue(out DiscoverHierarchyReply? reply)
|
||||
? reply
|
||||
: GalaxyDiscoverHierarchyReply);
|
||||
}
|
||||
|
||||
/// <summary>List of received galaxy watch deploy events requests.</summary>
|
||||
|
||||
@@ -18,6 +18,8 @@ namespace MxGateway.Client;
|
||||
/// </summary>
|
||||
public sealed class GalaxyRepositoryClient : IAsyncDisposable
|
||||
{
|
||||
private const int DiscoverHierarchyPageSize = 5000;
|
||||
|
||||
private readonly GrpcChannel? _channel;
|
||||
private readonly IGalaxyRepositoryClientTransport _transport;
|
||||
private readonly ResiliencePipeline _safeUnaryRetryPipeline;
|
||||
@@ -84,6 +86,8 @@ public sealed class GalaxyRepositoryClient : IAsyncDisposable
|
||||
{
|
||||
HttpHandler = handler,
|
||||
LoggerFactory = options.LoggerFactory,
|
||||
MaxReceiveMessageSize = options.MaxGrpcMessageBytes,
|
||||
MaxSendMessageSize = options.MaxGrpcMessageBytes,
|
||||
});
|
||||
|
||||
return new GalaxyRepositoryClient(
|
||||
@@ -175,12 +179,81 @@ public sealed class GalaxyRepositoryClient : IAsyncDisposable
|
||||
/// <returns>The collection of Galaxy objects in the hierarchy.</returns>
|
||||
public async Task<IReadOnlyList<GalaxyObject>> DiscoverHierarchyAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
DiscoverHierarchyReply reply = await DiscoverHierarchyRawAsync(
|
||||
new DiscoverHierarchyRequest(),
|
||||
cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
return await DiscoverHierarchyAsync(new DiscoverHierarchyOptions(), cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return reply.Objects;
|
||||
public async Task<IReadOnlyList<GalaxyObject>> DiscoverHierarchyAsync(
|
||||
DiscoverHierarchyOptions options,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
List<GalaxyObject> objects = [];
|
||||
HashSet<string> seenPageTokens = new(StringComparer.Ordinal);
|
||||
string pageToken = string.Empty;
|
||||
do
|
||||
{
|
||||
DiscoverHierarchyRequest request = CreateDiscoverHierarchyRequest(options);
|
||||
request.PageSize = DiscoverHierarchyPageSize;
|
||||
request.PageToken = pageToken;
|
||||
DiscoverHierarchyReply reply = await DiscoverHierarchyRawAsync(
|
||||
request,
|
||||
cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
objects.AddRange(reply.Objects);
|
||||
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));
|
||||
|
||||
return objects;
|
||||
}
|
||||
|
||||
private static DiscoverHierarchyRequest CreateDiscoverHierarchyRequest(DiscoverHierarchyOptions options)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(options);
|
||||
|
||||
DiscoverHierarchyRequest request = new()
|
||||
{
|
||||
AlarmBearingOnly = options.AlarmBearingOnly,
|
||||
HistorizedOnly = options.HistorizedOnly,
|
||||
};
|
||||
|
||||
if (options.RootGobjectId.HasValue)
|
||||
{
|
||||
request.RootGobjectId = options.RootGobjectId.Value;
|
||||
}
|
||||
else if (!string.IsNullOrWhiteSpace(options.RootTagName))
|
||||
{
|
||||
request.RootTagName = options.RootTagName;
|
||||
}
|
||||
else if (!string.IsNullOrWhiteSpace(options.RootContainedPath))
|
||||
{
|
||||
request.RootContainedPath = options.RootContainedPath;
|
||||
}
|
||||
|
||||
if (options.MaxDepth.HasValue)
|
||||
{
|
||||
request.MaxDepth = options.MaxDepth.Value;
|
||||
}
|
||||
|
||||
request.CategoryIds.Add(options.CategoryIds);
|
||||
request.TemplateChainContains.Add(options.TemplateChainContains);
|
||||
if (!string.IsNullOrWhiteSpace(options.TagNameGlob))
|
||||
{
|
||||
request.TagNameGlob = options.TagNameGlob;
|
||||
}
|
||||
|
||||
if (options.IncludeAttributes.HasValue)
|
||||
{
|
||||
request.IncludeAttributes = options.IncludeAttributes.Value;
|
||||
}
|
||||
|
||||
return request;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -80,6 +80,8 @@ public sealed class MxGatewayClient : IAsyncDisposable
|
||||
{
|
||||
HttpHandler = handler,
|
||||
LoggerFactory = options.LoggerFactory,
|
||||
MaxReceiveMessageSize = options.MaxGrpcMessageBytes,
|
||||
MaxSendMessageSize = options.MaxGrpcMessageBytes,
|
||||
});
|
||||
|
||||
return new MxGatewayClient(
|
||||
|
||||
@@ -47,6 +47,8 @@ public sealed class MxGatewayClientOptions
|
||||
/// </summary>
|
||||
public TimeSpan? StreamTimeout { get; init; }
|
||||
|
||||
public int MaxGrpcMessageBytes { get; init; } = 16 * 1024 * 1024;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the retry configuration for safe unary calls.
|
||||
/// </summary>
|
||||
@@ -102,6 +104,13 @@ public sealed class MxGatewayClientOptions
|
||||
"The stream timeout must be greater than zero when configured.");
|
||||
}
|
||||
|
||||
if (MaxGrpcMessageBytes <= 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(
|
||||
nameof(MaxGrpcMessageBytes),
|
||||
"The maximum gRPC message size must be greater than zero.");
|
||||
}
|
||||
|
||||
if (UseTls && Endpoint.Scheme != Uri.UriSchemeHttps)
|
||||
{
|
||||
throw new ArgumentException(
|
||||
|
||||
+7
-3
@@ -11,6 +11,7 @@ import java.util.NoSuchElementException;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.ArrayBlockingQueue;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
/**
|
||||
* Iterator-style adaptor over the {@code WatchDeployEvents} server-streaming
|
||||
@@ -22,8 +23,8 @@ public final class DeployEventStream implements Iterator<DeployEvent>, AutoClose
|
||||
private static final Object END = new Object();
|
||||
|
||||
private final BlockingQueue<Object> queue;
|
||||
private final AtomicBoolean closed = new AtomicBoolean();
|
||||
private volatile ClientCallStreamObserver<WatchDeployEventsRequest> requestStream;
|
||||
private volatile boolean closed;
|
||||
private Object next;
|
||||
|
||||
DeployEventStream(int capacity) {
|
||||
@@ -35,6 +36,9 @@ public final class DeployEventStream implements Iterator<DeployEvent>, AutoClose
|
||||
@Override
|
||||
public void beforeStart(ClientCallStreamObserver<WatchDeployEventsRequest> requestStream) {
|
||||
DeployEventStream.this.requestStream = requestStream;
|
||||
if (closed.get()) {
|
||||
requestStream.cancel("client cancelled deploy event stream", null);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -44,7 +48,7 @@ public final class DeployEventStream implements Iterator<DeployEvent>, AutoClose
|
||||
|
||||
@Override
|
||||
public void onError(Throwable error) {
|
||||
if (Status.fromThrowable(error).getCode() == Status.Code.CANCELLED && closed) {
|
||||
if (Status.fromThrowable(error).getCode() == Status.Code.CANCELLED && closed.get()) {
|
||||
offer(END);
|
||||
return;
|
||||
}
|
||||
@@ -90,7 +94,7 @@ public final class DeployEventStream implements Iterator<DeployEvent>, AutoClose
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
closed = true;
|
||||
closed.set(true);
|
||||
ClientCallStreamObserver<WatchDeployEventsRequest> stream = requestStream;
|
||||
if (stream != null) {
|
||||
stream.cancel("client cancelled deploy event stream", null);
|
||||
|
||||
+41
-6
@@ -36,6 +36,8 @@ import javax.net.ssl.SSLException;
|
||||
* {@link MxGatewayClient}.
|
||||
*/
|
||||
public final class GalaxyRepositoryClient implements AutoCloseable {
|
||||
private static final int DISCOVER_HIERARCHY_PAGE_SIZE = 5000;
|
||||
|
||||
private final ManagedChannel ownedChannel;
|
||||
private final MxGatewayClientOptions options;
|
||||
private final GalaxyRepositoryGrpc.GalaxyRepositoryBlockingStub blockingStub;
|
||||
@@ -177,9 +179,22 @@ public final class GalaxyRepositoryClient implements AutoCloseable {
|
||||
*/
|
||||
public List<GalaxyObject> discoverHierarchy() {
|
||||
try {
|
||||
DiscoverHierarchyReply reply =
|
||||
rawBlockingStub().discoverHierarchy(DiscoverHierarchyRequest.getDefaultInstance());
|
||||
return reply.getObjectsList();
|
||||
java.util.ArrayList<GalaxyObject> objects = new java.util.ArrayList<>();
|
||||
java.util.HashSet<String> seenPageTokens = new java.util.HashSet<>();
|
||||
String pageToken = "";
|
||||
do {
|
||||
DiscoverHierarchyReply reply = rawBlockingStub().discoverHierarchy(DiscoverHierarchyRequest.newBuilder()
|
||||
.setPageSize(DISCOVER_HIERARCHY_PAGE_SIZE)
|
||||
.setPageToken(pageToken)
|
||||
.build());
|
||||
objects.addAll(reply.getObjectsList());
|
||||
pageToken = reply.getNextPageToken();
|
||||
if (!pageToken.isBlank() && !seenPageTokens.add(pageToken)) {
|
||||
throw new MxGatewayException(
|
||||
"galaxy discover hierarchy returned repeated page token: " + pageToken);
|
||||
}
|
||||
} while (!pageToken.isBlank());
|
||||
return objects;
|
||||
} catch (RuntimeException error) {
|
||||
if (error instanceof MxGatewayException) {
|
||||
throw error;
|
||||
@@ -195,8 +210,7 @@ public final class GalaxyRepositoryClient implements AutoCloseable {
|
||||
* exceptionally with {@link MxGatewayException} on failure
|
||||
*/
|
||||
public CompletableFuture<List<GalaxyObject>> discoverHierarchyAsync() {
|
||||
return toCompletable(rawFutureStub().discoverHierarchy(DiscoverHierarchyRequest.getDefaultInstance()))
|
||||
.thenApply(DiscoverHierarchyReply::getObjectsList);
|
||||
return discoverHierarchyPageAsync("", new java.util.ArrayList<>(), new java.util.HashSet<>());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -295,7 +309,7 @@ public final class GalaxyRepositoryClient implements AutoCloseable {
|
||||
|
||||
private static ManagedChannel createChannel(MxGatewayClientOptions options) {
|
||||
NettyChannelBuilder builder = NettyChannelBuilder.forTarget(options.endpoint())
|
||||
.maxInboundMessageSize(16 * 1024 * 1024);
|
||||
.maxInboundMessageSize(options.maxGrpcMessageBytes());
|
||||
if (!options.connectTimeout().isNegative()) {
|
||||
builder.withOption(
|
||||
io.grpc.netty.shaded.io.netty.channel.ChannelOption.CONNECT_TIMEOUT_MILLIS,
|
||||
@@ -327,6 +341,27 @@ public final class GalaxyRepositoryClient implements AutoCloseable {
|
||||
return stub.withDeadlineAfter(options.callTimeout().toNanos(), TimeUnit.NANOSECONDS);
|
||||
}
|
||||
|
||||
private CompletableFuture<List<GalaxyObject>> discoverHierarchyPageAsync(
|
||||
String pageToken, java.util.ArrayList<GalaxyObject> objects, java.util.HashSet<String> seenPageTokens) {
|
||||
DiscoverHierarchyRequest request = DiscoverHierarchyRequest.newBuilder()
|
||||
.setPageSize(DISCOVER_HIERARCHY_PAGE_SIZE)
|
||||
.setPageToken(pageToken)
|
||||
.build();
|
||||
return toCompletable(rawFutureStub().discoverHierarchy(request)).thenCompose(reply -> {
|
||||
objects.addAll(reply.getObjectsList());
|
||||
if (reply.getNextPageToken().isBlank()) {
|
||||
return CompletableFuture.completedFuture(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);
|
||||
});
|
||||
}
|
||||
|
||||
private static <T> CompletableFuture<T> toCompletable(com.google.common.util.concurrent.ListenableFuture<T> source) {
|
||||
CompletableFuture<T> target = new CompletableFuture<>();
|
||||
Futures.addCallback(
|
||||
|
||||
+1
-1
@@ -284,7 +284,7 @@ public final class MxGatewayClient implements AutoCloseable {
|
||||
|
||||
private static ManagedChannel createChannel(MxGatewayClientOptions options) {
|
||||
NettyChannelBuilder builder = NettyChannelBuilder.forTarget(options.endpoint())
|
||||
.maxInboundMessageSize(16 * 1024 * 1024);
|
||||
.maxInboundMessageSize(options.maxGrpcMessageBytes());
|
||||
if (!options.connectTimeout().isNegative()) {
|
||||
builder.withOption(
|
||||
io.grpc.netty.shaded.io.netty.channel.ChannelOption.CONNECT_TIMEOUT_MILLIS,
|
||||
|
||||
+17
@@ -14,6 +14,7 @@ import java.util.Objects;
|
||||
public final class MxGatewayClientOptions {
|
||||
private static final Duration DEFAULT_CONNECT_TIMEOUT = Duration.ofSeconds(10);
|
||||
private static final Duration DEFAULT_CALL_TIMEOUT = Duration.ofSeconds(30);
|
||||
private static final int DEFAULT_MAX_GRPC_MESSAGE_BYTES = 16 * 1024 * 1024;
|
||||
|
||||
private final String endpoint;
|
||||
private final String apiKey;
|
||||
@@ -23,6 +24,7 @@ public final class MxGatewayClientOptions {
|
||||
private final Duration connectTimeout;
|
||||
private final Duration callTimeout;
|
||||
private final Duration streamTimeout;
|
||||
private final int maxGrpcMessageBytes;
|
||||
|
||||
private MxGatewayClientOptions(Builder builder) {
|
||||
endpoint = requireText(builder.endpoint, "endpoint");
|
||||
@@ -33,6 +35,9 @@ public final class MxGatewayClientOptions {
|
||||
connectTimeout = builder.connectTimeout == null ? DEFAULT_CONNECT_TIMEOUT : builder.connectTimeout;
|
||||
callTimeout = builder.callTimeout == null ? DEFAULT_CALL_TIMEOUT : builder.callTimeout;
|
||||
streamTimeout = builder.streamTimeout;
|
||||
maxGrpcMessageBytes = builder.maxGrpcMessageBytes <= 0
|
||||
? DEFAULT_MAX_GRPC_MESSAGE_BYTES
|
||||
: builder.maxGrpcMessageBytes;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -126,6 +131,10 @@ public final class MxGatewayClientOptions {
|
||||
return streamTimeout;
|
||||
}
|
||||
|
||||
public int maxGrpcMessageBytes() {
|
||||
return maxGrpcMessageBytes;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "MxGatewayClientOptions{"
|
||||
@@ -148,6 +157,8 @@ public final class MxGatewayClientOptions {
|
||||
+ callTimeout
|
||||
+ ", streamTimeout="
|
||||
+ streamTimeout
|
||||
+ ", maxGrpcMessageBytes="
|
||||
+ maxGrpcMessageBytes
|
||||
+ '}';
|
||||
}
|
||||
|
||||
@@ -170,6 +181,7 @@ public final class MxGatewayClientOptions {
|
||||
private Duration connectTimeout;
|
||||
private Duration callTimeout;
|
||||
private Duration streamTimeout;
|
||||
private int maxGrpcMessageBytes;
|
||||
|
||||
private Builder() {
|
||||
}
|
||||
@@ -265,6 +277,11 @@ public final class MxGatewayClientOptions {
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder maxGrpcMessageBytes(int value) {
|
||||
maxGrpcMessageBytes = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds an immutable {@link MxGatewayClientOptions} from the current state.
|
||||
*
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@ package com.dohertylan.mxgateway.client;
|
||||
* worker speak the same protocol version as the client.
|
||||
*/
|
||||
public final class MxGatewayClientVersion {
|
||||
private static final int GATEWAY_PROTOCOL_VERSION = 1;
|
||||
private static final int GATEWAY_PROTOCOL_VERSION = 2;
|
||||
private static final int WORKER_PROTOCOL_VERSION = 1;
|
||||
private static final String CLIENT_VERSION = "0.1.0";
|
||||
|
||||
|
||||
+121
-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.assertFalse;
|
||||
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 com.google.protobuf.Timestamp;
|
||||
@@ -25,6 +26,8 @@ import io.grpc.ServerCallHandler;
|
||||
import io.grpc.ServerInterceptor;
|
||||
import io.grpc.inprocess.InProcessChannelBuilder;
|
||||
import io.grpc.inprocess.InProcessServerBuilder;
|
||||
import io.grpc.stub.ClientCallStreamObserver;
|
||||
import io.grpc.stub.ClientResponseObserver;
|
||||
import io.grpc.stub.StreamObserver;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
@@ -100,31 +103,44 @@ final class GalaxyRepositoryClientTests {
|
||||
|
||||
@Test
|
||||
void discoverHierarchyReturnsObjectsAndAttributes() throws Exception {
|
||||
AtomicReference<DiscoverHierarchyRequest> seenRequest = new AtomicReference<>();
|
||||
AtomicReference<DiscoverHierarchyRequest> firstRequest = new AtomicReference<>();
|
||||
AtomicReference<DiscoverHierarchyRequest> secondRequest = new AtomicReference<>();
|
||||
TestService service = new TestService() {
|
||||
@Override
|
||||
public void discoverHierarchy(
|
||||
DiscoverHierarchyRequest request, StreamObserver<DiscoverHierarchyReply> responseObserver) {
|
||||
seenRequest.set(request);
|
||||
responseObserver.onNext(DiscoverHierarchyReply.newBuilder()
|
||||
.addObjects(GalaxyObject.newBuilder()
|
||||
.setGobjectId(7)
|
||||
.setTagName("Pump_001")
|
||||
.setContainedName("Pump")
|
||||
.setBrowseName("Pump")
|
||||
.setParentGobjectId(1)
|
||||
.setIsArea(false)
|
||||
.setCategoryId(3)
|
||||
.setHostedByGobjectId(0)
|
||||
.addTemplateChain("$Pump")
|
||||
.addAttributes(GalaxyAttribute.newBuilder()
|
||||
.setAttributeName("Speed")
|
||||
.setFullTagReference("Pump_001.Speed")
|
||||
.setMxDataType(5)
|
||||
.setDataTypeName("MxFloat")
|
||||
.setIsArray(false)
|
||||
.setIsHistorized(true)))
|
||||
.build());
|
||||
if (request.getPageToken().isEmpty()) {
|
||||
firstRequest.set(request);
|
||||
responseObserver.onNext(DiscoverHierarchyReply.newBuilder()
|
||||
.setNextPageToken("page-2")
|
||||
.setTotalObjectCount(2)
|
||||
.addObjects(GalaxyObject.newBuilder()
|
||||
.setGobjectId(7)
|
||||
.setTagName("Pump_001")
|
||||
.setContainedName("Pump")
|
||||
.setBrowseName("Pump")
|
||||
.setParentGobjectId(1)
|
||||
.setIsArea(false)
|
||||
.setCategoryId(3)
|
||||
.setHostedByGobjectId(0)
|
||||
.addTemplateChain("$Pump")
|
||||
.addAttributes(GalaxyAttribute.newBuilder()
|
||||
.setAttributeName("Speed")
|
||||
.setFullTagReference("Pump_001.Speed")
|
||||
.setMxDataType(5)
|
||||
.setDataTypeName("MxFloat")
|
||||
.setIsArray(false)
|
||||
.setIsHistorized(true)))
|
||||
.build());
|
||||
} else {
|
||||
secondRequest.set(request);
|
||||
responseObserver.onNext(DiscoverHierarchyReply.newBuilder()
|
||||
.setTotalObjectCount(2)
|
||||
.addObjects(GalaxyObject.newBuilder()
|
||||
.setGobjectId(8)
|
||||
.setTagName("Pump_002"))
|
||||
.build());
|
||||
}
|
||||
responseObserver.onCompleted();
|
||||
}
|
||||
};
|
||||
@@ -132,7 +148,10 @@ final class GalaxyRepositoryClientTests {
|
||||
try (InProcessGalaxy g = InProcessGalaxy.start(service, new AtomicReference<>());
|
||||
GalaxyRepositoryClient client = g.client("")) {
|
||||
List<GalaxyObject> objects = client.discoverHierarchy();
|
||||
assertEquals(1, objects.size());
|
||||
assertEquals(2, objects.size());
|
||||
assertEquals(5000, firstRequest.get().getPageSize());
|
||||
assertEquals("", firstRequest.get().getPageToken());
|
||||
assertEquals("page-2", secondRequest.get().getPageToken());
|
||||
GalaxyObject only = objects.get(0);
|
||||
assertEquals(7, only.getGobjectId());
|
||||
assertEquals("Pump_001", only.getTagName());
|
||||
@@ -142,6 +161,41 @@ final class GalaxyRepositoryClientTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void deployEventStreamCloseBeforeBeforeStartCancelsStream() {
|
||||
DeployEventStream stream = new DeployEventStream(4);
|
||||
ClientResponseObserver<WatchDeployEventsRequest, DeployEvent> observer = stream.observer();
|
||||
RecordingClientCallStreamObserver requestStream = new RecordingClientCallStreamObserver();
|
||||
|
||||
stream.close();
|
||||
observer.beforeStart(requestStream);
|
||||
|
||||
assertTrue(requestStream.cancelled);
|
||||
assertEquals("client cancelled deploy event stream", requestStream.cancelMessage);
|
||||
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
|
||||
void watchDeployEventsReceivesEventsInOrder() throws Exception {
|
||||
DeployEvent first = DeployEvent.newBuilder()
|
||||
@@ -281,6 +335,51 @@ final class GalaxyRepositoryClientTests {
|
||||
}
|
||||
}
|
||||
|
||||
private static final class RecordingClientCallStreamObserver
|
||||
extends ClientCallStreamObserver<WatchDeployEventsRequest> {
|
||||
private boolean cancelled;
|
||||
private String cancelMessage;
|
||||
|
||||
@Override
|
||||
public boolean isReady() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setOnReadyHandler(Runnable onReadyHandler) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void disableAutoInboundFlowControl() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void request(int count) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setMessageCompression(boolean enable) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cancel(String message, Throwable cause) {
|
||||
cancelled = true;
|
||||
cancelMessage = message;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNext(WatchDeployEventsRequest value) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Throwable error) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCompleted() {
|
||||
}
|
||||
}
|
||||
|
||||
private record InProcessGalaxy(Server server, ManagedChannel channel) implements AutoCloseable {
|
||||
static InProcessGalaxy start(
|
||||
GalaxyRepositoryGrpc.GalaxyRepositoryImplBase service, AtomicReference<String> authorization)
|
||||
|
||||
+2540
-93
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@@ -2,7 +2,7 @@
|
||||
"schemaVersion": 1,
|
||||
"fixtureSet": "mxaccess-gateway-client-behavior",
|
||||
"contractName": "mxaccess-gateway",
|
||||
"gatewayProtocolVersion": 1,
|
||||
"gatewayProtocolVersion": 2,
|
||||
"workerProtocolVersion": 1,
|
||||
"protoInputManifest": "clients/proto/proto-inputs.json",
|
||||
"fixtures": [
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"backendName": "mxaccess-worker",
|
||||
"workerProcessId": 1234,
|
||||
"workerProtocolVersion": 1,
|
||||
"gatewayProtocolVersion": 1,
|
||||
"gatewayProtocolVersion": 2,
|
||||
"capabilities": [
|
||||
"unary-open-session",
|
||||
"unary-close-session",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"schemaVersion": 1,
|
||||
"fixtureSet": "mxaccess-gateway-parity-fixture-matrix",
|
||||
"contractName": "mxaccess-gateway",
|
||||
"gatewayProtocolVersion": 1,
|
||||
"gatewayProtocolVersion": 2,
|
||||
"workerProtocolVersion": 1,
|
||||
"sourceCaptureRoot": "C:/Users/dohertj2/Desktop/mxaccess/captures",
|
||||
"sourceDocs": [
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"contractName": "mxaccess-gateway",
|
||||
"gatewayProtocolVersion": 1,
|
||||
"gatewayProtocolVersion": 2,
|
||||
"workerProtocolVersion": 1,
|
||||
"protoRoot": "src/MxGateway.Contracts/Protos",
|
||||
"sourceFiles": [
|
||||
|
||||
@@ -18,11 +18,13 @@ import grpc
|
||||
from google.protobuf.timestamp_pb2 import Timestamp
|
||||
|
||||
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_grpc as galaxy_pb_grpc
|
||||
from .options import ClientOptions, create_channel
|
||||
|
||||
_DISCOVER_HIERARCHY_PAGE_SIZE = 5000
|
||||
|
||||
|
||||
class GalaxyRepositoryClient:
|
||||
"""Async client for the Galaxy Repository gRPC service."""
|
||||
@@ -115,12 +117,27 @@ class GalaxyRepositoryClient:
|
||||
async def discover_hierarchy(self) -> list[galaxy_pb.GalaxyObject]:
|
||||
"""Return the deployed Galaxy object hierarchy as raw proto messages."""
|
||||
|
||||
reply = await self._unary(
|
||||
"discover hierarchy",
|
||||
self.raw_stub.DiscoverHierarchy,
|
||||
galaxy_pb.DiscoverHierarchyRequest(),
|
||||
)
|
||||
return list(reply.objects)
|
||||
objects: list[galaxy_pb.GalaxyObject] = []
|
||||
seen_page_tokens: set[str] = set()
|
||||
page_token = ""
|
||||
while True:
|
||||
reply = await self._unary(
|
||||
"discover hierarchy",
|
||||
self.raw_stub.DiscoverHierarchy,
|
||||
galaxy_pb.DiscoverHierarchyRequest(
|
||||
page_size=_DISCOVER_HIERARCHY_PAGE_SIZE,
|
||||
page_token=page_token,
|
||||
),
|
||||
)
|
||||
objects.extend(reply.objects)
|
||||
page_token = reply.next_page_token
|
||||
if not page_token:
|
||||
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(
|
||||
self,
|
||||
|
||||
@@ -23,9 +23,10 @@ _sym_db = _symbol_database.Default()
|
||||
|
||||
|
||||
from google.protobuf import timestamp_pb2 as google_dot_protobuf_dot_timestamp__pb2
|
||||
from google.protobuf import wrappers_pb2 as google_dot_protobuf_dot_wrappers__pb2
|
||||
|
||||
|
||||
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x17galaxy_repository.proto\x12\x14galaxy_repository.v1\x1a\x1fgoogle/protobuf/timestamp.proto\"\x17\n\x15TestConnectionRequest\"!\n\x13TestConnectionReply\x12\n\n\x02ok\x18\x01 \x01(\x08\"\x1a\n\x18GetLastDeployTimeRequest\"b\n\x16GetLastDeployTimeReply\x12\x0f\n\x07present\x18\x01 \x01(\x08\x12\x37\n\x13time_of_last_deploy\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\"\x1a\n\x18\x44iscoverHierarchyRequest\"M\n\x16\x44iscoverHierarchyReply\x12\x33\n\x07objects\x18\x01 \x03(\x0b\x32\".galaxy_repository.v1.GalaxyObject\"U\n\x18WatchDeployEventsRequest\x12\x39\n\x15last_seen_deploy_time\x18\x01 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\"\xdd\x01\n\x0b\x44\x65ployEvent\x12\x10\n\x08sequence\x18\x01 \x01(\x04\x12/\n\x0bobserved_at\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x37\n\x13time_of_last_deploy\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12#\n\x1btime_of_last_deploy_present\x18\x04 \x01(\x08\x12\x14\n\x0cobject_count\x18\x05 \x01(\x05\x12\x17\n\x0f\x61ttribute_count\x18\x06 \x01(\x05\"\x93\x02\n\x0cGalaxyObject\x12\x12\n\ngobject_id\x18\x01 \x01(\x05\x12\x10\n\x08tag_name\x18\x02 \x01(\t\x12\x16\n\x0e\x63ontained_name\x18\x03 \x01(\t\x12\x13\n\x0b\x62rowse_name\x18\x04 \x01(\t\x12\x19\n\x11parent_gobject_id\x18\x05 \x01(\x05\x12\x0f\n\x07is_area\x18\x06 \x01(\x08\x12\x13\n\x0b\x63\x61tegory_id\x18\x07 \x01(\x05\x12\x1c\n\x14hosted_by_gobject_id\x18\x08 \x01(\x05\x12\x16\n\x0etemplate_chain\x18\t \x03(\t\x12\x39\n\nattributes\x18\n \x03(\x0b\x32%.galaxy_repository.v1.GalaxyAttribute\"\xa8\x02\n\x0fGalaxyAttribute\x12\x16\n\x0e\x61ttribute_name\x18\x01 \x01(\t\x12\x1a\n\x12\x66ull_tag_reference\x18\x02 \x01(\t\x12\x14\n\x0cmx_data_type\x18\x03 \x01(\x05\x12\x16\n\x0e\x64\x61ta_type_name\x18\x04 \x01(\t\x12\x10\n\x08is_array\x18\x05 \x01(\x08\x12\x17\n\x0f\x61rray_dimension\x18\x06 \x01(\x05\x12\x1f\n\x17\x61rray_dimension_present\x18\x07 \x01(\x08\x12\x1d\n\x15mx_attribute_category\x18\x08 \x01(\x05\x12\x1f\n\x17security_classification\x18\t \x01(\x05\x12\x15\n\ris_historized\x18\n \x01(\x08\x12\x10\n\x08is_alarm\x18\x0b \x01(\x08\x32\xcc\x03\n\x10GalaxyRepository\x12h\n\x0eTestConnection\x12+.galaxy_repository.v1.TestConnectionRequest\x1a).galaxy_repository.v1.TestConnectionReply\x12q\n\x11GetLastDeployTime\x12..galaxy_repository.v1.GetLastDeployTimeRequest\x1a,.galaxy_repository.v1.GetLastDeployTimeReply\x12q\n\x11\x44iscoverHierarchy\x12..galaxy_repository.v1.DiscoverHierarchyRequest\x1a,.galaxy_repository.v1.DiscoverHierarchyReply\x12h\n\x11WatchDeployEvents\x12..galaxy_repository.v1.WatchDeployEventsRequest\x1a!.galaxy_repository.v1.DeployEvent0\x01\x42#\xaa\x02 MxGateway.Contracts.Proto.Galaxyb\x06proto3')
|
||||
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x17galaxy_repository.proto\x12\x14galaxy_repository.v1\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x1egoogle/protobuf/wrappers.proto\"\x17\n\x15TestConnectionRequest\"!\n\x13TestConnectionReply\x12\n\n\x02ok\x18\x01 \x01(\x08\"\x1a\n\x18GetLastDeployTimeRequest\"b\n\x16GetLastDeployTimeReply\x12\x0f\n\x07present\x18\x01 \x01(\x08\x12\x37\n\x13time_of_last_deploy\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\"\x87\x03\n\x18\x44iscoverHierarchyRequest\x12\x11\n\tpage_size\x18\x01 \x01(\x05\x12\x12\n\npage_token\x18\x02 \x01(\t\x12\x19\n\x0froot_gobject_id\x18\x03 \x01(\x05H\x00\x12\x17\n\rroot_tag_name\x18\x04 \x01(\tH\x00\x12\x1d\n\x13root_contained_path\x18\x05 \x01(\tH\x00\x12.\n\tmax_depth\x18\x06 \x01(\x0b\x32\x1b.google.protobuf.Int32Value\x12\x14\n\x0c\x63\x61tegory_ids\x18\x07 \x03(\x05\x12\x1f\n\x17template_chain_contains\x18\x08 \x03(\t\x12\x15\n\rtag_name_glob\x18\t \x01(\t\x12\x1f\n\x12include_attributes\x18\n \x01(\x08H\x01\x88\x01\x01\x12\x1a\n\x12\x61larm_bearing_only\x18\x0b \x01(\x08\x12\x17\n\x0fhistorized_only\x18\x0c \x01(\x08\x42\x06\n\x04rootB\x15\n\x13_include_attributes\"\x82\x01\n\x16\x44iscoverHierarchyReply\x12\x33\n\x07objects\x18\x01 \x03(\x0b\x32\".galaxy_repository.v1.GalaxyObject\x12\x17\n\x0fnext_page_token\x18\x02 \x01(\t\x12\x1a\n\x12total_object_count\x18\x03 \x01(\x05\"U\n\x18WatchDeployEventsRequest\x12\x39\n\x15last_seen_deploy_time\x18\x01 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\"\xdd\x01\n\x0b\x44\x65ployEvent\x12\x10\n\x08sequence\x18\x01 \x01(\x04\x12/\n\x0bobserved_at\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x37\n\x13time_of_last_deploy\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12#\n\x1btime_of_last_deploy_present\x18\x04 \x01(\x08\x12\x14\n\x0cobject_count\x18\x05 \x01(\x05\x12\x17\n\x0f\x61ttribute_count\x18\x06 \x01(\x05\"\x93\x02\n\x0cGalaxyObject\x12\x12\n\ngobject_id\x18\x01 \x01(\x05\x12\x10\n\x08tag_name\x18\x02 \x01(\t\x12\x16\n\x0e\x63ontained_name\x18\x03 \x01(\t\x12\x13\n\x0b\x62rowse_name\x18\x04 \x01(\t\x12\x19\n\x11parent_gobject_id\x18\x05 \x01(\x05\x12\x0f\n\x07is_area\x18\x06 \x01(\x08\x12\x13\n\x0b\x63\x61tegory_id\x18\x07 \x01(\x05\x12\x1c\n\x14hosted_by_gobject_id\x18\x08 \x01(\x05\x12\x16\n\x0etemplate_chain\x18\t \x03(\t\x12\x39\n\nattributes\x18\n \x03(\x0b\x32%.galaxy_repository.v1.GalaxyAttribute\"\xa8\x02\n\x0fGalaxyAttribute\x12\x16\n\x0e\x61ttribute_name\x18\x01 \x01(\t\x12\x1a\n\x12\x66ull_tag_reference\x18\x02 \x01(\t\x12\x14\n\x0cmx_data_type\x18\x03 \x01(\x05\x12\x16\n\x0e\x64\x61ta_type_name\x18\x04 \x01(\t\x12\x10\n\x08is_array\x18\x05 \x01(\x08\x12\x17\n\x0f\x61rray_dimension\x18\x06 \x01(\x05\x12\x1f\n\x17\x61rray_dimension_present\x18\x07 \x01(\x08\x12\x1d\n\x15mx_attribute_category\x18\x08 \x01(\x05\x12\x1f\n\x17security_classification\x18\t \x01(\x05\x12\x15\n\ris_historized\x18\n \x01(\x08\x12\x10\n\x08is_alarm\x18\x0b \x01(\x08\x32\xcc\x03\n\x10GalaxyRepository\x12h\n\x0eTestConnection\x12+.galaxy_repository.v1.TestConnectionRequest\x1a).galaxy_repository.v1.TestConnectionReply\x12q\n\x11GetLastDeployTime\x12..galaxy_repository.v1.GetLastDeployTimeRequest\x1a,.galaxy_repository.v1.GetLastDeployTimeReply\x12q\n\x11\x44iscoverHierarchy\x12..galaxy_repository.v1.DiscoverHierarchyRequest\x1a,.galaxy_repository.v1.DiscoverHierarchyReply\x12h\n\x11WatchDeployEvents\x12..galaxy_repository.v1.WatchDeployEventsRequest\x1a!.galaxy_repository.v1.DeployEvent0\x01\x42#\xaa\x02 MxGateway.Contracts.Proto.Galaxyb\x06proto3')
|
||||
|
||||
_globals = globals()
|
||||
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
|
||||
@@ -33,26 +34,26 @@ _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'galaxy_repository_pb2', _gl
|
||||
if not _descriptor._USE_C_DESCRIPTORS:
|
||||
_globals['DESCRIPTOR']._loaded_options = None
|
||||
_globals['DESCRIPTOR']._serialized_options = b'\252\002 MxGateway.Contracts.Proto.Galaxy'
|
||||
_globals['_TESTCONNECTIONREQUEST']._serialized_start=82
|
||||
_globals['_TESTCONNECTIONREQUEST']._serialized_end=105
|
||||
_globals['_TESTCONNECTIONREPLY']._serialized_start=107
|
||||
_globals['_TESTCONNECTIONREPLY']._serialized_end=140
|
||||
_globals['_GETLASTDEPLOYTIMEREQUEST']._serialized_start=142
|
||||
_globals['_GETLASTDEPLOYTIMEREQUEST']._serialized_end=168
|
||||
_globals['_GETLASTDEPLOYTIMEREPLY']._serialized_start=170
|
||||
_globals['_GETLASTDEPLOYTIMEREPLY']._serialized_end=268
|
||||
_globals['_DISCOVERHIERARCHYREQUEST']._serialized_start=270
|
||||
_globals['_DISCOVERHIERARCHYREQUEST']._serialized_end=296
|
||||
_globals['_DISCOVERHIERARCHYREPLY']._serialized_start=298
|
||||
_globals['_DISCOVERHIERARCHYREPLY']._serialized_end=375
|
||||
_globals['_WATCHDEPLOYEVENTSREQUEST']._serialized_start=377
|
||||
_globals['_WATCHDEPLOYEVENTSREQUEST']._serialized_end=462
|
||||
_globals['_DEPLOYEVENT']._serialized_start=465
|
||||
_globals['_DEPLOYEVENT']._serialized_end=686
|
||||
_globals['_GALAXYOBJECT']._serialized_start=689
|
||||
_globals['_GALAXYOBJECT']._serialized_end=964
|
||||
_globals['_GALAXYATTRIBUTE']._serialized_start=967
|
||||
_globals['_GALAXYATTRIBUTE']._serialized_end=1263
|
||||
_globals['_GALAXYREPOSITORY']._serialized_start=1266
|
||||
_globals['_GALAXYREPOSITORY']._serialized_end=1726
|
||||
_globals['_TESTCONNECTIONREQUEST']._serialized_start=114
|
||||
_globals['_TESTCONNECTIONREQUEST']._serialized_end=137
|
||||
_globals['_TESTCONNECTIONREPLY']._serialized_start=139
|
||||
_globals['_TESTCONNECTIONREPLY']._serialized_end=172
|
||||
_globals['_GETLASTDEPLOYTIMEREQUEST']._serialized_start=174
|
||||
_globals['_GETLASTDEPLOYTIMEREQUEST']._serialized_end=200
|
||||
_globals['_GETLASTDEPLOYTIMEREPLY']._serialized_start=202
|
||||
_globals['_GETLASTDEPLOYTIMEREPLY']._serialized_end=300
|
||||
_globals['_DISCOVERHIERARCHYREQUEST']._serialized_start=303
|
||||
_globals['_DISCOVERHIERARCHYREQUEST']._serialized_end=694
|
||||
_globals['_DISCOVERHIERARCHYREPLY']._serialized_start=697
|
||||
_globals['_DISCOVERHIERARCHYREPLY']._serialized_end=827
|
||||
_globals['_WATCHDEPLOYEVENTSREQUEST']._serialized_start=829
|
||||
_globals['_WATCHDEPLOYEVENTSREQUEST']._serialized_end=914
|
||||
_globals['_DEPLOYEVENT']._serialized_start=917
|
||||
_globals['_DEPLOYEVENT']._serialized_end=1138
|
||||
_globals['_GALAXYOBJECT']._serialized_start=1141
|
||||
_globals['_GALAXYOBJECT']._serialized_end=1416
|
||||
_globals['_GALAXYATTRIBUTE']._serialized_start=1419
|
||||
_globals['_GALAXYATTRIBUTE']._serialized_end=1715
|
||||
_globals['_GALAXYREPOSITORY']._serialized_start=1718
|
||||
_globals['_GALAXYREPOSITORY']._serialized_end=2178
|
||||
# @@protoc_insertion_point(module_scope)
|
||||
|
||||
@@ -21,6 +21,7 @@ class ClientOptions:
|
||||
server_name_override: str | None = None
|
||||
call_timeout: float | None = 30.0
|
||||
stream_timeout: float | None = None
|
||||
max_grpc_message_bytes: int = 16 * 1024 * 1024
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
"""Validate options; raise `ValueError` for invalid combinations."""
|
||||
@@ -33,6 +34,8 @@ class ClientOptions:
|
||||
raise ValueError("call_timeout must be greater than zero")
|
||||
if self.stream_timeout is not None and self.stream_timeout <= 0:
|
||||
raise ValueError("stream_timeout must be greater than zero")
|
||||
if self.max_grpc_message_bytes <= 0:
|
||||
raise ValueError("max_grpc_message_bytes must be greater than zero")
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""Return a repr that redacts the API key value."""
|
||||
@@ -43,14 +46,18 @@ class ClientOptions:
|
||||
f"ca_file={self.ca_file!r}, "
|
||||
f"server_name_override={self.server_name_override!r}, "
|
||||
f"call_timeout={self.call_timeout!r}, "
|
||||
f"stream_timeout={self.stream_timeout!r})"
|
||||
f"stream_timeout={self.stream_timeout!r}, "
|
||||
f"max_grpc_message_bytes={self.max_grpc_message_bytes!r})"
|
||||
)
|
||||
|
||||
|
||||
def create_channel(options: ClientOptions) -> grpc.aio.Channel:
|
||||
"""Create a plaintext or TLS `grpc.aio` channel from client options."""
|
||||
|
||||
channel_options: list[tuple[str, str]] = []
|
||||
channel_options: list[tuple[str, str | int]] = [
|
||||
("grpc.max_receive_message_length", options.max_grpc_message_bytes),
|
||||
("grpc.max_send_message_length", options.max_grpc_message_bytes),
|
||||
]
|
||||
if options.server_name_override:
|
||||
channel_options.append(("grpc.ssl_target_name_override", options.server_name_override))
|
||||
|
||||
|
||||
@@ -61,7 +61,15 @@ def test_create_channel_uses_plaintext_channel(monkeypatch: pytest.MonkeyPatch)
|
||||
channel = create_channel(ClientOptions(endpoint="localhost:5000", plaintext=True))
|
||||
|
||||
assert channel == "plain-channel"
|
||||
assert calls == [("localhost:5000", [])]
|
||||
assert calls == [
|
||||
(
|
||||
"localhost:5000",
|
||||
[
|
||||
("grpc.max_receive_message_length", 16 * 1024 * 1024),
|
||||
("grpc.max_send_message_length", 16 * 1024 * 1024),
|
||||
],
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
def test_create_channel_uses_tls_channel(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
@@ -95,9 +103,13 @@ def test_create_channel_uses_tls_channel(monkeypatch: pytest.MonkeyPatch) -> Non
|
||||
|
||||
assert channel == "tls-channel"
|
||||
assert calls == [
|
||||
(
|
||||
"gateway.example:5001",
|
||||
"creds",
|
||||
[("grpc.ssl_target_name_override", "gateway.test")],
|
||||
),
|
||||
]
|
||||
(
|
||||
"gateway.example:5001",
|
||||
"creds",
|
||||
[
|
||||
("grpc.max_receive_message_length", 16 * 1024 * 1024),
|
||||
("grpc.max_send_message_length", 16 * 1024 * 1024),
|
||||
("grpc.ssl_target_name_override", "gateway.test"),
|
||||
],
|
||||
),
|
||||
]
|
||||
|
||||
@@ -98,6 +98,8 @@ async def test_discover_hierarchy_returns_proto_objects() -> None:
|
||||
stub = FakeGalaxyStub()
|
||||
stub.discover_hierarchy.replies = [
|
||||
galaxy_pb.DiscoverHierarchyReply(
|
||||
next_page_token="page-2",
|
||||
total_object_count=2,
|
||||
objects=[
|
||||
galaxy_pb.GalaxyObject(
|
||||
gobject_id=1,
|
||||
@@ -106,6 +108,11 @@ async def test_discover_hierarchy_returns_proto_objects() -> None:
|
||||
browse_name="TestMachine_001",
|
||||
is_area=True,
|
||||
),
|
||||
],
|
||||
),
|
||||
galaxy_pb.DiscoverHierarchyReply(
|
||||
total_object_count=2,
|
||||
objects=[
|
||||
galaxy_pb.GalaxyObject(
|
||||
gobject_id=2,
|
||||
tag_name="DelmiaReceiver_001",
|
||||
@@ -133,10 +140,30 @@ async def test_discover_hierarchy_returns_proto_objects() -> None:
|
||||
|
||||
assert isinstance(objects, list)
|
||||
assert len(objects) == 2
|
||||
assert len(stub.discover_hierarchy.requests) == 2
|
||||
assert stub.discover_hierarchy.requests[0].page_size == 5000
|
||||
assert stub.discover_hierarchy.requests[0].page_token == ""
|
||||
assert stub.discover_hierarchy.requests[1].page_token == "page-2"
|
||||
assert objects[0].tag_name == "TestMachine_001"
|
||||
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
|
||||
async def test_watch_deploy_events_yields_events_in_order() -> None:
|
||||
ts1 = Timestamp()
|
||||
|
||||
@@ -1048,7 +1048,7 @@ mod tests {
|
||||
fn version_json_output_has_protocol_versions() {
|
||||
let value = super::version_json();
|
||||
|
||||
assert_eq!(value["gatewayProtocolVersion"], 1);
|
||||
assert_eq!(value["gatewayProtocolVersion"], 2);
|
||||
assert_eq!(value["workerProtocolVersion"], 1);
|
||||
}
|
||||
|
||||
|
||||
@@ -79,9 +79,12 @@ impl GatewayClient {
|
||||
|
||||
let channel = endpoint.connect().await?;
|
||||
let interceptor = AuthInterceptor::new(options.api_key().cloned());
|
||||
let max_grpc_message_bytes = options.max_grpc_message_bytes();
|
||||
|
||||
Ok(Self {
|
||||
inner: MxAccessGatewayClient::with_interceptor(channel, interceptor),
|
||||
inner: MxAccessGatewayClient::with_interceptor(channel, interceptor)
|
||||
.max_decoding_message_size(max_grpc_message_bytes)
|
||||
.max_encoding_message_size(max_grpc_message_bytes),
|
||||
call_timeout: options.call_timeout(),
|
||||
stream_timeout: options.stream_timeout(),
|
||||
})
|
||||
|
||||
+140
-33
@@ -21,6 +21,8 @@ use crate::generated::galaxy_repository::v1::{
|
||||
};
|
||||
use crate::options::ClientOptions;
|
||||
|
||||
const DISCOVER_HIERARCHY_PAGE_SIZE: i32 = 5000;
|
||||
|
||||
/// Convenience alias for the generated Galaxy client wrapped in the
|
||||
/// authentication interceptor.
|
||||
pub type RawGalaxyClient = GalaxyRepositoryClient<InterceptedService<Channel, AuthInterceptor>>;
|
||||
@@ -77,9 +79,12 @@ impl GalaxyClient {
|
||||
|
||||
let channel = endpoint.connect().await?;
|
||||
let interceptor = AuthInterceptor::new(options.api_key().cloned());
|
||||
let max_grpc_message_bytes = options.max_grpc_message_bytes();
|
||||
|
||||
Ok(Self {
|
||||
inner: GalaxyRepositoryClient::with_interceptor(channel, interceptor),
|
||||
inner: GalaxyRepositoryClient::with_interceptor(channel, interceptor)
|
||||
.max_decoding_message_size(max_grpc_message_bytes)
|
||||
.max_encoding_message_size(max_grpc_message_bytes),
|
||||
call_timeout: options.call_timeout(),
|
||||
stream_timeout: options.stream_timeout(),
|
||||
})
|
||||
@@ -89,8 +94,11 @@ impl GalaxyClient {
|
||||
/// channel. Tests use this to wire up an in-memory transport.
|
||||
pub fn from_channel(channel: Channel, options: &ClientOptions) -> Self {
|
||||
let interceptor = AuthInterceptor::new(options.api_key().cloned());
|
||||
let max_grpc_message_bytes = options.max_grpc_message_bytes();
|
||||
Self {
|
||||
inner: GalaxyRepositoryClient::with_interceptor(channel, interceptor),
|
||||
inner: GalaxyRepositoryClient::with_interceptor(channel, interceptor)
|
||||
.max_decoding_message_size(max_grpc_message_bytes)
|
||||
.max_encoding_message_size(max_grpc_message_bytes),
|
||||
call_timeout: options.call_timeout(),
|
||||
stream_timeout: options.stream_timeout(),
|
||||
}
|
||||
@@ -135,11 +143,33 @@ impl GalaxyClient {
|
||||
/// Walk the deployed object hierarchy. Each [`GalaxyObject`] contains
|
||||
/// the object's identifying names plus its dynamic attributes.
|
||||
pub async fn discover_hierarchy(&mut self) -> Result<Vec<GalaxyObject>, Error> {
|
||||
let response = self
|
||||
.inner
|
||||
.discover_hierarchy(self.unary_request(DiscoverHierarchyRequest {}))
|
||||
.await?;
|
||||
Ok(response.into_inner().objects)
|
||||
let mut objects = Vec::new();
|
||||
let mut seen_page_tokens = std::collections::HashSet::new();
|
||||
let mut page_token = String::new();
|
||||
loop {
|
||||
let response = self
|
||||
.inner
|
||||
.discover_hierarchy(self.unary_request(DiscoverHierarchyRequest {
|
||||
page_size: DISCOVER_HIERARCHY_PAGE_SIZE,
|
||||
page_token,
|
||||
..Default::default()
|
||||
}))
|
||||
.await?;
|
||||
let reply = response.into_inner();
|
||||
objects.extend(reply.objects);
|
||||
page_token = reply.next_page_token;
|
||||
if page_token.is_empty() {
|
||||
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}`"
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Subscribe to the server-streamed deploy-event feed.
|
||||
@@ -217,6 +247,8 @@ mod tests {
|
||||
present: Mutex<bool>,
|
||||
last_deploy: Mutex<Option<Timestamp>>,
|
||||
objects: Mutex<Vec<GalaxyObject>>,
|
||||
discover_requests: Mutex<Vec<DiscoverHierarchyRequest>>,
|
||||
discover_replies: Mutex<std::collections::VecDeque<DiscoverHierarchyReply>>,
|
||||
watch_requests: Mutex<Vec<WatchDeployEventsRequest>>,
|
||||
watch_events: Mutex<Vec<DeployEvent>>,
|
||||
watch_senders: Mutex<Vec<DeployEventTx>>,
|
||||
@@ -256,10 +288,21 @@ mod tests {
|
||||
|
||||
async fn discover_hierarchy(
|
||||
&self,
|
||||
_request: Request<DiscoverHierarchyRequest>,
|
||||
request: Request<DiscoverHierarchyRequest>,
|
||||
) -> Result<Response<DiscoverHierarchyReply>, Status> {
|
||||
self.state
|
||||
.discover_requests
|
||||
.lock()
|
||||
.unwrap()
|
||||
.push(request.into_inner());
|
||||
if let Some(reply) = self.state.discover_replies.lock().unwrap().pop_front() {
|
||||
return Ok(Response::new(reply));
|
||||
}
|
||||
|
||||
Ok(Response::new(DiscoverHierarchyReply {
|
||||
objects: self.state.objects.lock().unwrap().clone(),
|
||||
next_page_token: String::new(),
|
||||
total_object_count: self.state.objects.lock().unwrap().len() as i32,
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -409,30 +452,58 @@ mod tests {
|
||||
#[tokio::test]
|
||||
async fn discover_hierarchy_returns_objects_with_attributes() {
|
||||
let state = Arc::new(FakeState::default());
|
||||
*state.objects.lock().unwrap() = vec![GalaxyObject {
|
||||
gobject_id: 42,
|
||||
tag_name: "DelmiaReceiver_001".to_owned(),
|
||||
contained_name: "DelmiaReceiver".to_owned(),
|
||||
browse_name: "TestMachine_001/DelmiaReceiver".to_owned(),
|
||||
parent_gobject_id: 7,
|
||||
is_area: false,
|
||||
category_id: 3,
|
||||
hosted_by_gobject_id: 1,
|
||||
template_chain: vec!["$UserDefined".to_owned(), "$DelmiaReceiver".to_owned()],
|
||||
attributes: vec![GalaxyAttribute {
|
||||
attribute_name: "DownloadPath".to_owned(),
|
||||
full_tag_reference: "DelmiaReceiver_001.DownloadPath".to_owned(),
|
||||
mx_data_type: 8,
|
||||
data_type_name: "MxString".to_owned(),
|
||||
is_array: false,
|
||||
array_dimension: 0,
|
||||
array_dimension_present: false,
|
||||
mx_attribute_category: 2,
|
||||
security_classification: 1,
|
||||
is_historized: false,
|
||||
is_alarm: false,
|
||||
}],
|
||||
}];
|
||||
state
|
||||
.discover_replies
|
||||
.lock()
|
||||
.unwrap()
|
||||
.push_back(DiscoverHierarchyReply {
|
||||
objects: vec![GalaxyObject {
|
||||
gobject_id: 42,
|
||||
tag_name: "DelmiaReceiver_001".to_owned(),
|
||||
contained_name: "DelmiaReceiver".to_owned(),
|
||||
browse_name: "TestMachine_001/DelmiaReceiver".to_owned(),
|
||||
parent_gobject_id: 7,
|
||||
is_area: false,
|
||||
category_id: 3,
|
||||
hosted_by_gobject_id: 1,
|
||||
template_chain: vec!["$UserDefined".to_owned(), "$DelmiaReceiver".to_owned()],
|
||||
attributes: vec![GalaxyAttribute {
|
||||
attribute_name: "DownloadPath".to_owned(),
|
||||
full_tag_reference: "DelmiaReceiver_001.DownloadPath".to_owned(),
|
||||
mx_data_type: 8,
|
||||
data_type_name: "MxString".to_owned(),
|
||||
is_array: false,
|
||||
array_dimension: 0,
|
||||
array_dimension_present: false,
|
||||
mx_attribute_category: 2,
|
||||
security_classification: 1,
|
||||
is_historized: false,
|
||||
is_alarm: false,
|
||||
}],
|
||||
}],
|
||||
next_page_token: "page-2".to_owned(),
|
||||
total_object_count: 2,
|
||||
});
|
||||
state
|
||||
.discover_replies
|
||||
.lock()
|
||||
.unwrap()
|
||||
.push_back(DiscoverHierarchyReply {
|
||||
objects: vec![GalaxyObject {
|
||||
gobject_id: 43,
|
||||
tag_name: "DelmiaReceiver_002".to_owned(),
|
||||
contained_name: String::new(),
|
||||
browse_name: String::new(),
|
||||
parent_gobject_id: 0,
|
||||
is_area: false,
|
||||
category_id: 0,
|
||||
hosted_by_gobject_id: 0,
|
||||
template_chain: Vec::new(),
|
||||
attributes: Vec::new(),
|
||||
}],
|
||||
next_page_token: String::new(),
|
||||
total_object_count: 2,
|
||||
});
|
||||
let endpoint = spawn_fake(state.clone()).await;
|
||||
|
||||
let mut client = GalaxyClient::connect(ClientOptions::new(endpoint))
|
||||
@@ -441,7 +512,12 @@ mod tests {
|
||||
|
||||
let objects = client.discover_hierarchy().await.unwrap();
|
||||
|
||||
assert_eq!(objects.len(), 1);
|
||||
assert_eq!(objects.len(), 2);
|
||||
let requests = state.discover_requests.lock().unwrap();
|
||||
assert_eq!(requests.len(), 2);
|
||||
assert_eq!(requests[0].page_size, 5000);
|
||||
assert_eq!(requests[0].page_token, "");
|
||||
assert_eq!(requests[1].page_token, "page-2");
|
||||
assert_eq!(objects[0].tag_name, "DelmiaReceiver_001");
|
||||
assert_eq!(objects[0].attributes.len(), 1);
|
||||
assert_eq!(objects[0].attributes[0].attribute_name, "DownloadPath");
|
||||
@@ -451,6 +527,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]
|
||||
async fn watch_deploy_events_yields_events_in_order() {
|
||||
let state = Arc::new(FakeState::default());
|
||||
|
||||
@@ -8,6 +8,8 @@ use std::time::Duration;
|
||||
|
||||
use crate::auth::ApiKey;
|
||||
|
||||
const DEFAULT_MAX_GRPC_MESSAGE_BYTES: usize = 16 * 1024 * 1024;
|
||||
|
||||
/// Configuration for connecting to a gateway endpoint.
|
||||
///
|
||||
/// Defaults are 10s connect timeout, 30s call timeout, no streaming timeout,
|
||||
@@ -24,6 +26,7 @@ pub struct ClientOptions {
|
||||
connect_timeout: Duration,
|
||||
call_timeout: Duration,
|
||||
stream_timeout: Option<Duration>,
|
||||
max_grpc_message_bytes: usize,
|
||||
}
|
||||
|
||||
impl ClientOptions {
|
||||
@@ -39,6 +42,7 @@ impl ClientOptions {
|
||||
connect_timeout: Duration::from_secs(10),
|
||||
call_timeout: Duration::from_secs(30),
|
||||
stream_timeout: None,
|
||||
max_grpc_message_bytes: DEFAULT_MAX_GRPC_MESSAGE_BYTES,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,6 +95,11 @@ impl ClientOptions {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_max_grpc_message_bytes(mut self, max_grpc_message_bytes: usize) -> Self {
|
||||
self.max_grpc_message_bytes = max_grpc_message_bytes;
|
||||
self
|
||||
}
|
||||
|
||||
/// Configured endpoint URL.
|
||||
pub fn endpoint(&self) -> &str {
|
||||
&self.endpoint
|
||||
@@ -130,6 +139,10 @@ impl ClientOptions {
|
||||
pub fn stream_timeout(&self) -> Option<Duration> {
|
||||
self.stream_timeout
|
||||
}
|
||||
|
||||
pub fn max_grpc_message_bytes(&self) -> usize {
|
||||
self.max_grpc_message_bytes
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ClientOptions {
|
||||
@@ -150,6 +163,7 @@ impl fmt::Debug for ClientOptions {
|
||||
.field("connect_timeout", &self.connect_timeout)
|
||||
.field("call_timeout", &self.call_timeout)
|
||||
.field("stream_timeout", &self.stream_timeout)
|
||||
.field("max_grpc_message_bytes", &self.max_grpc_message_bytes)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
pub const CLIENT_VERSION: &str = "0.1.0-dev";
|
||||
|
||||
/// Public gateway gRPC protocol version this client targets.
|
||||
pub const GATEWAY_PROTOCOL_VERSION: u32 = 1;
|
||||
pub const GATEWAY_PROTOCOL_VERSION: u32 = 2;
|
||||
|
||||
/// Internal worker IPC protocol version this client expects sessions to use.
|
||||
pub const WORKER_PROTOCOL_VERSION: u32 = 1;
|
||||
|
||||
Reference in New Issue
Block a user