Fix runtime review findings
This commit is contained in:
@@ -21,6 +21,8 @@ internal sealed class FakeGalaxyRepositoryTransport(MxGatewayClientOptions optio
|
|||||||
|
|
||||||
public DiscoverHierarchyReply DiscoverHierarchyReply { get; set; } = new();
|
public DiscoverHierarchyReply DiscoverHierarchyReply { get; set; } = new();
|
||||||
|
|
||||||
|
public Queue<DiscoverHierarchyReply> DiscoverHierarchyReplies { get; } = new();
|
||||||
|
|
||||||
public Queue<Exception> TestConnectionExceptions { get; } = new();
|
public Queue<Exception> TestConnectionExceptions { get; } = new();
|
||||||
|
|
||||||
public Queue<Exception> GetLastDeployTimeExceptions { get; } = new();
|
public Queue<Exception> GetLastDeployTimeExceptions { get; } = new();
|
||||||
@@ -63,7 +65,10 @@ internal sealed class FakeGalaxyRepositoryTransport(MxGatewayClientOptions optio
|
|||||||
throw exception;
|
throw exception;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Task.FromResult(DiscoverHierarchyReply);
|
return Task.FromResult(
|
||||||
|
DiscoverHierarchyReplies.TryDequeue(out DiscoverHierarchyReply? reply)
|
||||||
|
? reply
|
||||||
|
: DiscoverHierarchyReply);
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<(WatchDeployEventsRequest Request, CallOptions CallOptions)> WatchDeployEventsCalls { get; } = [];
|
public List<(WatchDeployEventsRequest Request, CallOptions CallOptions)> WatchDeployEventsCalls { get; } = [];
|
||||||
|
|||||||
@@ -68,8 +68,10 @@ public sealed class GalaxyRepositoryClientTests
|
|||||||
public async Task DiscoverHierarchyAsync_ReturnsObjectsFromReply()
|
public async Task DiscoverHierarchyAsync_ReturnsObjectsFromReply()
|
||||||
{
|
{
|
||||||
FakeGalaxyRepositoryTransport transport = CreateTransport();
|
FakeGalaxyRepositoryTransport transport = CreateTransport();
|
||||||
transport.DiscoverHierarchyReply = new DiscoverHierarchyReply
|
transport.DiscoverHierarchyReplies.Enqueue(new DiscoverHierarchyReply
|
||||||
{
|
{
|
||||||
|
NextPageToken = "page-2",
|
||||||
|
TotalObjectCount = 2,
|
||||||
Objects =
|
Objects =
|
||||||
{
|
{
|
||||||
new GalaxyObject
|
new GalaxyObject
|
||||||
@@ -91,12 +93,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);
|
await using GalaxyRepositoryClient client = CreateClient(transport);
|
||||||
|
|
||||||
IReadOnlyList<GalaxyObject> objects = await client.DiscoverHierarchyAsync();
|
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(12, obj.GobjectId);
|
||||||
Assert.Equal("DelmiaReceiver_001", obj.TagName);
|
Assert.Equal("DelmiaReceiver_001", obj.TagName);
|
||||||
GalaxyAttribute attribute = Assert.Single(obj.Attributes);
|
GalaxyAttribute attribute = Assert.Single(obj.Attributes);
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ public sealed class MxGatewayClientCliTests
|
|||||||
var exitCode = MxGatewayClientCli.Run(["version"], output, error);
|
var exitCode = MxGatewayClientCli.Run(["version"], output, error);
|
||||||
|
|
||||||
Assert.Equal(0, exitCode);
|
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.Contains("worker-protocol=1", output.ToString());
|
||||||
Assert.Equal(string.Empty, error.ToString());
|
Assert.Equal(string.Empty, error.ToString());
|
||||||
}
|
}
|
||||||
@@ -30,7 +30,7 @@ public sealed class MxGatewayClientCliTests
|
|||||||
int exitCode = await MxGatewayClientCli.RunAsync(["version", "--json"], output, error);
|
int exitCode = await MxGatewayClientCli.RunAsync(["version", "--json"], output, error);
|
||||||
|
|
||||||
Assert.Equal(0, exitCode);
|
Assert.Equal(0, exitCode);
|
||||||
Assert.Contains("\"gatewayProtocolVersion\":1", output.ToString());
|
Assert.Contains("\"gatewayProtocolVersion\":2", output.ToString());
|
||||||
Assert.Equal(string.Empty, error.ToString());
|
Assert.Equal(string.Empty, error.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ namespace MxGateway.Client;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class GalaxyRepositoryClient : IAsyncDisposable
|
public sealed class GalaxyRepositoryClient : IAsyncDisposable
|
||||||
{
|
{
|
||||||
|
private const int DiscoverHierarchyPageSize = 5000;
|
||||||
|
|
||||||
private readonly GrpcChannel? _channel;
|
private readonly GrpcChannel? _channel;
|
||||||
private readonly IGalaxyRepositoryClientTransport _transport;
|
private readonly IGalaxyRepositoryClientTransport _transport;
|
||||||
private readonly ResiliencePipeline _safeUnaryRetryPipeline;
|
private readonly ResiliencePipeline _safeUnaryRetryPipeline;
|
||||||
@@ -68,6 +70,8 @@ public sealed class GalaxyRepositoryClient : IAsyncDisposable
|
|||||||
{
|
{
|
||||||
HttpHandler = handler,
|
HttpHandler = handler,
|
||||||
LoggerFactory = options.LoggerFactory,
|
LoggerFactory = options.LoggerFactory,
|
||||||
|
MaxReceiveMessageSize = options.MaxGrpcMessageBytes,
|
||||||
|
MaxSendMessageSize = options.MaxGrpcMessageBytes,
|
||||||
});
|
});
|
||||||
|
|
||||||
return new GalaxyRepositoryClient(
|
return new GalaxyRepositoryClient(
|
||||||
@@ -141,12 +145,25 @@ public sealed class GalaxyRepositoryClient : IAsyncDisposable
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public async Task<IReadOnlyList<GalaxyObject>> DiscoverHierarchyAsync(CancellationToken cancellationToken = default)
|
public async Task<IReadOnlyList<GalaxyObject>> DiscoverHierarchyAsync(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
DiscoverHierarchyReply reply = await DiscoverHierarchyRawAsync(
|
List<GalaxyObject> objects = [];
|
||||||
new DiscoverHierarchyRequest(),
|
string pageToken = string.Empty;
|
||||||
cancellationToken)
|
do
|
||||||
.ConfigureAwait(false);
|
{
|
||||||
|
DiscoverHierarchyReply reply = await DiscoverHierarchyRawAsync(
|
||||||
|
new DiscoverHierarchyRequest
|
||||||
|
{
|
||||||
|
PageSize = DiscoverHierarchyPageSize,
|
||||||
|
PageToken = pageToken,
|
||||||
|
},
|
||||||
|
cancellationToken)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
return reply.Objects;
|
objects.AddRange(reply.Objects);
|
||||||
|
pageToken = reply.NextPageToken;
|
||||||
|
}
|
||||||
|
while (!string.IsNullOrWhiteSpace(pageToken));
|
||||||
|
|
||||||
|
return objects;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<DiscoverHierarchyReply> DiscoverHierarchyRawAsync(
|
public Task<DiscoverHierarchyReply> DiscoverHierarchyRawAsync(
|
||||||
|
|||||||
@@ -64,6 +64,8 @@ public sealed class MxGatewayClient : IAsyncDisposable
|
|||||||
{
|
{
|
||||||
HttpHandler = handler,
|
HttpHandler = handler,
|
||||||
LoggerFactory = options.LoggerFactory,
|
LoggerFactory = options.LoggerFactory,
|
||||||
|
MaxReceiveMessageSize = options.MaxGrpcMessageBytes,
|
||||||
|
MaxSendMessageSize = options.MaxGrpcMessageBytes,
|
||||||
});
|
});
|
||||||
|
|
||||||
return new MxGatewayClient(
|
return new MxGatewayClient(
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ public sealed class MxGatewayClientOptions
|
|||||||
|
|
||||||
public TimeSpan? StreamTimeout { get; init; }
|
public TimeSpan? StreamTimeout { get; init; }
|
||||||
|
|
||||||
|
public int MaxGrpcMessageBytes { get; init; } = 16 * 1024 * 1024;
|
||||||
|
|
||||||
public MxGatewayClientRetryOptions Retry { get; init; } = new();
|
public MxGatewayClientRetryOptions Retry { get; init; } = new();
|
||||||
|
|
||||||
public ILoggerFactory? LoggerFactory { get; init; }
|
public ILoggerFactory? LoggerFactory { get; init; }
|
||||||
@@ -66,6 +68,13 @@ public sealed class MxGatewayClientOptions
|
|||||||
"The stream timeout must be greater than zero when configured.");
|
"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)
|
if (UseTls && Endpoint.Scheme != Uri.UriSchemeHttps)
|
||||||
{
|
{
|
||||||
throw new ArgumentException(
|
throw new ArgumentException(
|
||||||
|
|||||||
@@ -191,7 +191,12 @@ func (x *GetLastDeployTimeReply) GetTimeOfLastDeploy() *timestamppb.Timestamp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type DiscoverHierarchyRequest struct {
|
type DiscoverHierarchyRequest struct {
|
||||||
state protoimpl.MessageState `protogen:"open.v1"`
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
|
// Maximum number of objects to return. The server applies its default when
|
||||||
|
// unset and rejects non-positive values.
|
||||||
|
PageSize int32 `protobuf:"varint,1,opt,name=page_size,json=pageSize,proto3" json:"page_size,omitempty"`
|
||||||
|
// Opaque token returned by a previous DiscoverHierarchy response.
|
||||||
|
PageToken string `protobuf:"bytes,2,opt,name=page_token,json=pageToken,proto3" json:"page_token,omitempty"`
|
||||||
unknownFields protoimpl.UnknownFields
|
unknownFields protoimpl.UnknownFields
|
||||||
sizeCache protoimpl.SizeCache
|
sizeCache protoimpl.SizeCache
|
||||||
}
|
}
|
||||||
@@ -226,11 +231,29 @@ func (*DiscoverHierarchyRequest) Descriptor() ([]byte, []int) {
|
|||||||
return file_galaxy_repository_proto_rawDescGZIP(), []int{4}
|
return file_galaxy_repository_proto_rawDescGZIP(), []int{4}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (x *DiscoverHierarchyRequest) GetPageSize() int32 {
|
||||||
|
if x != nil {
|
||||||
|
return x.PageSize
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *DiscoverHierarchyRequest) GetPageToken() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.PageToken
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
type DiscoverHierarchyReply struct {
|
type DiscoverHierarchyReply struct {
|
||||||
state protoimpl.MessageState `protogen:"open.v1"`
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
Objects []*GalaxyObject `protobuf:"bytes,1,rep,name=objects,proto3" json:"objects,omitempty"`
|
Objects []*GalaxyObject `protobuf:"bytes,1,rep,name=objects,proto3" json:"objects,omitempty"`
|
||||||
unknownFields protoimpl.UnknownFields
|
// Non-empty when another page is available.
|
||||||
sizeCache protoimpl.SizeCache
|
NextPageToken string `protobuf:"bytes,2,opt,name=next_page_token,json=nextPageToken,proto3" json:"next_page_token,omitempty"`
|
||||||
|
// Total number of objects in the cached hierarchy at the time of the call.
|
||||||
|
TotalObjectCount int32 `protobuf:"varint,3,opt,name=total_object_count,json=totalObjectCount,proto3" json:"total_object_count,omitempty"`
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *DiscoverHierarchyReply) Reset() {
|
func (x *DiscoverHierarchyReply) Reset() {
|
||||||
@@ -270,6 +293,20 @@ func (x *DiscoverHierarchyReply) GetObjects() []*GalaxyObject {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (x *DiscoverHierarchyReply) GetNextPageToken() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.NextPageToken
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *DiscoverHierarchyReply) GetTotalObjectCount() int32 {
|
||||||
|
if x != nil {
|
||||||
|
return x.TotalObjectCount
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
type WatchDeployEventsRequest struct {
|
type WatchDeployEventsRequest struct {
|
||||||
state protoimpl.MessageState `protogen:"open.v1"`
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
// Optional. When set, the bootstrap event is suppressed if the cached deploy
|
// Optional. When set, the bootstrap event is suppressed if the cached deploy
|
||||||
@@ -654,10 +691,15 @@ const file_galaxy_repository_proto_rawDesc = "" +
|
|||||||
"\x18GetLastDeployTimeRequest\"}\n" +
|
"\x18GetLastDeployTimeRequest\"}\n" +
|
||||||
"\x16GetLastDeployTimeReply\x12\x18\n" +
|
"\x16GetLastDeployTimeReply\x12\x18\n" +
|
||||||
"\apresent\x18\x01 \x01(\bR\apresent\x12I\n" +
|
"\apresent\x18\x01 \x01(\bR\apresent\x12I\n" +
|
||||||
"\x13time_of_last_deploy\x18\x02 \x01(\v2\x1a.google.protobuf.TimestampR\x10timeOfLastDeploy\"\x1a\n" +
|
"\x13time_of_last_deploy\x18\x02 \x01(\v2\x1a.google.protobuf.TimestampR\x10timeOfLastDeploy\"V\n" +
|
||||||
"\x18DiscoverHierarchyRequest\"V\n" +
|
"\x18DiscoverHierarchyRequest\x12\x1b\n" +
|
||||||
|
"\tpage_size\x18\x01 \x01(\x05R\bpageSize\x12\x1d\n" +
|
||||||
|
"\n" +
|
||||||
|
"page_token\x18\x02 \x01(\tR\tpageToken\"\xac\x01\n" +
|
||||||
"\x16DiscoverHierarchyReply\x12<\n" +
|
"\x16DiscoverHierarchyReply\x12<\n" +
|
||||||
"\aobjects\x18\x01 \x03(\v2\".galaxy_repository.v1.GalaxyObjectR\aobjects\"i\n" +
|
"\aobjects\x18\x01 \x03(\v2\".galaxy_repository.v1.GalaxyObjectR\aobjects\x12&\n" +
|
||||||
|
"\x0fnext_page_token\x18\x02 \x01(\tR\rnextPageToken\x12,\n" +
|
||||||
|
"\x12total_object_count\x18\x03 \x01(\x05R\x10totalObjectCount\"i\n" +
|
||||||
"\x18WatchDeployEventsRequest\x12M\n" +
|
"\x18WatchDeployEventsRequest\x12M\n" +
|
||||||
"\x15last_seen_deploy_time\x18\x01 \x01(\v2\x1a.google.protobuf.TimestampR\x12lastSeenDeployTime\"\xbb\x02\n" +
|
"\x15last_seen_deploy_time\x18\x01 \x01(\v2\x1a.google.protobuf.TimestampR\x12lastSeenDeployTime\"\xbb\x02\n" +
|
||||||
"\vDeployEvent\x12\x1a\n" +
|
"\vDeployEvent\x12\x1a\n" +
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import (
|
|||||||
const (
|
const (
|
||||||
defaultDialTimeout = 10 * time.Second
|
defaultDialTimeout = 10 * time.Second
|
||||||
defaultCallTimeout = 30 * time.Second
|
defaultCallTimeout = 30 * time.Second
|
||||||
|
defaultMaxGrpcMessageBytes = 16 * 1024 * 1024
|
||||||
)
|
)
|
||||||
|
|
||||||
// Client owns a gateway gRPC connection and exposes session-oriented helpers.
|
// Client owns a gateway gRPC connection and exposes session-oriented helpers.
|
||||||
@@ -50,6 +51,10 @@ func Dial(ctx context.Context, opts Options) (*Client, error) {
|
|||||||
grpc.WithTransportCredentials(transportCredentials),
|
grpc.WithTransportCredentials(transportCredentials),
|
||||||
grpc.WithUnaryInterceptor(unaryAuthInterceptor(opts.APIKey)),
|
grpc.WithUnaryInterceptor(unaryAuthInterceptor(opts.APIKey)),
|
||||||
grpc.WithStreamInterceptor(streamAuthInterceptor(opts.APIKey)),
|
grpc.WithStreamInterceptor(streamAuthInterceptor(opts.APIKey)),
|
||||||
|
grpc.WithDefaultCallOptions(
|
||||||
|
grpc.MaxCallRecvMsgSize(resolveMaxGrpcMessageBytes(opts)),
|
||||||
|
grpc.MaxCallSendMsgSize(resolveMaxGrpcMessageBytes(opts)),
|
||||||
|
),
|
||||||
grpc.WithBlock(),
|
grpc.WithBlock(),
|
||||||
}
|
}
|
||||||
dialOptions = append(dialOptions, opts.DialOptions...)
|
dialOptions = append(dialOptions, opts.DialOptions...)
|
||||||
@@ -62,6 +67,13 @@ func Dial(ctx context.Context, opts Options) (*Client, error) {
|
|||||||
return NewClient(conn, opts), nil
|
return NewClient(conn, opts), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func resolveMaxGrpcMessageBytes(opts Options) int {
|
||||||
|
if opts.MaxGrpcMessageBytes > 0 {
|
||||||
|
return opts.MaxGrpcMessageBytes
|
||||||
|
}
|
||||||
|
return defaultMaxGrpcMessageBytes
|
||||||
|
}
|
||||||
|
|
||||||
// NewClient wraps an existing gRPC connection. The caller owns closing conn
|
// NewClient wraps an existing gRPC connection. The caller owns closing conn
|
||||||
// unless it calls Close on the returned Client.
|
// unless it calls Close on the returned Client.
|
||||||
func NewClient(conn *grpc.ClientConn, opts Options) *Client {
|
func NewClient(conn *grpc.ClientConn, opts Options) *Client {
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ import (
|
|||||||
"google.golang.org/protobuf/types/known/timestamppb"
|
"google.golang.org/protobuf/types/known/timestamppb"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const discoverHierarchyPageSize = 5000
|
||||||
|
|
||||||
// RawGalaxyRepositoryClient is the generated gRPC client interface for the
|
// RawGalaxyRepositoryClient is the generated gRPC client interface for the
|
||||||
// Galaxy Repository service exposed for callers that need direct contract
|
// Galaxy Repository service exposed for callers that need direct contract
|
||||||
// access.
|
// access.
|
||||||
@@ -70,6 +72,10 @@ func DialGalaxy(ctx context.Context, opts Options) (*GalaxyClient, error) {
|
|||||||
grpc.WithTransportCredentials(transportCredentials),
|
grpc.WithTransportCredentials(transportCredentials),
|
||||||
grpc.WithUnaryInterceptor(unaryAuthInterceptor(opts.APIKey)),
|
grpc.WithUnaryInterceptor(unaryAuthInterceptor(opts.APIKey)),
|
||||||
grpc.WithStreamInterceptor(streamAuthInterceptor(opts.APIKey)),
|
grpc.WithStreamInterceptor(streamAuthInterceptor(opts.APIKey)),
|
||||||
|
grpc.WithDefaultCallOptions(
|
||||||
|
grpc.MaxCallRecvMsgSize(resolveMaxGrpcMessageBytes(opts)),
|
||||||
|
grpc.MaxCallSendMsgSize(resolveMaxGrpcMessageBytes(opts)),
|
||||||
|
),
|
||||||
grpc.WithBlock(),
|
grpc.WithBlock(),
|
||||||
}
|
}
|
||||||
dialOptions = append(dialOptions, opts.DialOptions...)
|
dialOptions = append(dialOptions, opts.DialOptions...)
|
||||||
@@ -141,11 +147,23 @@ func (c *GalaxyClient) DiscoverHierarchy(ctx context.Context) ([]*GalaxyObject,
|
|||||||
callCtx, cancel := c.callContext(ctx)
|
callCtx, cancel := c.callContext(ctx)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
reply, err := c.raw.DiscoverHierarchy(callCtx, &pb.DiscoverHierarchyRequest{})
|
var objects []*GalaxyObject
|
||||||
if err != nil {
|
pageToken := ""
|
||||||
return nil, &GatewayError{Op: "galaxy discover hierarchy", Err: err}
|
for {
|
||||||
|
reply, err := c.raw.DiscoverHierarchy(callCtx, &pb.DiscoverHierarchyRequest{
|
||||||
|
PageSize: discoverHierarchyPageSize,
|
||||||
|
PageToken: pageToken,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, &GatewayError{Op: "galaxy discover hierarchy", Err: err}
|
||||||
|
}
|
||||||
|
objects = append(objects, reply.GetObjects()...)
|
||||||
|
pageToken = reply.GetNextPageToken()
|
||||||
|
if pageToken == "" {
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return reply.GetObjects(), nil
|
return objects, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// WatchDeployEventsRaw starts the generated WatchDeployEvents stream for callers
|
// WatchDeployEventsRaw starts the generated WatchDeployEvents stream for callers
|
||||||
|
|||||||
@@ -95,7 +95,9 @@ func TestGalaxyGetLastDeployTimeReturnsAbsentWhenTimestampNil(t *testing.T) {
|
|||||||
|
|
||||||
func TestGalaxyDiscoverHierarchyReturnsObjects(t *testing.T) {
|
func TestGalaxyDiscoverHierarchyReturnsObjects(t *testing.T) {
|
||||||
fake := &fakeGalaxyServer{
|
fake := &fakeGalaxyServer{
|
||||||
discoverReply: &pb.DiscoverHierarchyReply{
|
discoverReplies: []*pb.DiscoverHierarchyReply{{
|
||||||
|
NextPageToken: "page-2",
|
||||||
|
TotalObjectCount: 2,
|
||||||
Objects: []*pb.GalaxyObject{
|
Objects: []*pb.GalaxyObject{
|
||||||
{
|
{
|
||||||
GobjectId: 1,
|
GobjectId: 1,
|
||||||
@@ -114,6 +116,10 @@ func TestGalaxyDiscoverHierarchyReturnsObjects(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
TotalObjectCount: 2,
|
||||||
|
Objects: []*pb.GalaxyObject{
|
||||||
{
|
{
|
||||||
GobjectId: 2,
|
GobjectId: 2,
|
||||||
TagName: "TestMachine_002",
|
TagName: "TestMachine_002",
|
||||||
@@ -121,7 +127,7 @@ func TestGalaxyDiscoverHierarchyReturnsObjects(t *testing.T) {
|
|||||||
ParentGobjectId: 1,
|
ParentGobjectId: 1,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
}},
|
||||||
}
|
}
|
||||||
client, cleanup := newGalaxyBufconnClient(t, fake)
|
client, cleanup := newGalaxyBufconnClient(t, fake)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
@@ -133,6 +139,15 @@ func TestGalaxyDiscoverHierarchyReturnsObjects(t *testing.T) {
|
|||||||
if len(objects) != 2 {
|
if len(objects) != 2 {
|
||||||
t.Fatalf("len(objects) = %d, want 2", len(objects))
|
t.Fatalf("len(objects) = %d, want 2", len(objects))
|
||||||
}
|
}
|
||||||
|
if len(fake.discoverRequests) != 2 {
|
||||||
|
t.Fatalf("len(discoverRequests) = %d, want 2", len(fake.discoverRequests))
|
||||||
|
}
|
||||||
|
if fake.discoverRequests[0].GetPageSize() != 5000 || fake.discoverRequests[0].GetPageToken() != "" {
|
||||||
|
t.Fatalf("first request = %+v", fake.discoverRequests[0])
|
||||||
|
}
|
||||||
|
if fake.discoverRequests[1].GetPageToken() != "page-2" {
|
||||||
|
t.Fatalf("second page_token = %q, want page-2", fake.discoverRequests[1].GetPageToken())
|
||||||
|
}
|
||||||
if objects[0].GetTagName() != "TestMachine_001" {
|
if objects[0].GetTagName() != "TestMachine_001" {
|
||||||
t.Fatalf("objects[0].TagName = %q", objects[0].GetTagName())
|
t.Fatalf("objects[0].TagName = %q", objects[0].GetTagName())
|
||||||
}
|
}
|
||||||
@@ -375,6 +390,8 @@ type fakeGalaxyServer struct {
|
|||||||
failTest bool
|
failTest bool
|
||||||
deployReply *pb.GetLastDeployTimeReply
|
deployReply *pb.GetLastDeployTimeReply
|
||||||
discoverReply *pb.DiscoverHierarchyReply
|
discoverReply *pb.DiscoverHierarchyReply
|
||||||
|
discoverReplies []*pb.DiscoverHierarchyReply
|
||||||
|
discoverRequests []*pb.DiscoverHierarchyRequest
|
||||||
watchEvents []*pb.DeployEvent
|
watchEvents []*pb.DeployEvent
|
||||||
watchRequest *pb.WatchDeployEventsRequest
|
watchRequest *pb.WatchDeployEventsRequest
|
||||||
watchSendInterval time.Duration
|
watchSendInterval time.Duration
|
||||||
@@ -400,6 +417,12 @@ func (s *fakeGalaxyServer) GetLastDeployTime(ctx context.Context, req *pb.GetLas
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *fakeGalaxyServer) DiscoverHierarchy(ctx context.Context, req *pb.DiscoverHierarchyRequest) (*pb.DiscoverHierarchyReply, error) {
|
func (s *fakeGalaxyServer) DiscoverHierarchy(ctx context.Context, req *pb.DiscoverHierarchyRequest) (*pb.DiscoverHierarchyReply, error) {
|
||||||
|
s.discoverRequests = append(s.discoverRequests, req)
|
||||||
|
if len(s.discoverReplies) > 0 {
|
||||||
|
reply := s.discoverReplies[0]
|
||||||
|
s.discoverReplies = s.discoverReplies[1:]
|
||||||
|
return reply, nil
|
||||||
|
}
|
||||||
if s.discoverReply != nil {
|
if s.discoverReply != nil {
|
||||||
return s.discoverReply, nil
|
return s.discoverReply, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ type Options struct {
|
|||||||
ServerNameOverride string
|
ServerNameOverride string
|
||||||
DialTimeout time.Duration
|
DialTimeout time.Duration
|
||||||
CallTimeout time.Duration
|
CallTimeout time.Duration
|
||||||
|
MaxGrpcMessageBytes int
|
||||||
TLSConfig *tls.Config
|
TLSConfig *tls.Config
|
||||||
TransportCredentials credentials.TransportCredentials
|
TransportCredentials credentials.TransportCredentials
|
||||||
DialOptions []grpc.DialOption
|
DialOptions []grpc.DialOption
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ const (
|
|||||||
|
|
||||||
// GatewayProtocolVersion matches GatewayContractInfo.GatewayProtocolVersion
|
// GatewayProtocolVersion matches GatewayContractInfo.GatewayProtocolVersion
|
||||||
// in the shared .NET contracts.
|
// in the shared .NET contracts.
|
||||||
GatewayProtocolVersion uint32 = 1
|
GatewayProtocolVersion uint32 = 2
|
||||||
|
|
||||||
// WorkerProtocolVersion matches GatewayContractInfo.WorkerProtocolVersion
|
// WorkerProtocolVersion matches GatewayContractInfo.WorkerProtocolVersion
|
||||||
// and is exposed for fake-worker and parity tests.
|
// and is exposed for fake-worker and parity tests.
|
||||||
|
|||||||
+2
-2
@@ -32,7 +32,7 @@ final class MxGatewayCliTests {
|
|||||||
assertEquals(0, run.exitCode());
|
assertEquals(0, run.exitCode());
|
||||||
assertEquals("", run.errors());
|
assertEquals("", run.errors());
|
||||||
assertTrue(run.output().contains("mxgateway-java 0.1.0"));
|
assertTrue(run.output().contains("mxgateway-java 0.1.0"));
|
||||||
assertTrue(run.output().contains("gatewayProtocolVersion=1"));
|
assertTrue(run.output().contains("gatewayProtocolVersion=2"));
|
||||||
assertTrue(run.output().contains("workerProtocolVersion=1"));
|
assertTrue(run.output().contains("workerProtocolVersion=1"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,7 +42,7 @@ final class MxGatewayCliTests {
|
|||||||
|
|
||||||
assertEquals(0, run.exitCode());
|
assertEquals(0, run.exitCode());
|
||||||
assertTrue(run.output().contains("\"clientVersion\":\"0.1.0\""));
|
assertTrue(run.output().contains("\"clientVersion\":\"0.1.0\""));
|
||||||
assertTrue(run.output().contains("\"gatewayProtocolVersion\":1"));
|
assertTrue(run.output().contains("\"gatewayProtocolVersion\":2"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
+7
-3
@@ -11,6 +11,7 @@ import java.util.NoSuchElementException;
|
|||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.concurrent.ArrayBlockingQueue;
|
import java.util.concurrent.ArrayBlockingQueue;
|
||||||
import java.util.concurrent.BlockingQueue;
|
import java.util.concurrent.BlockingQueue;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Iterator-style adaptor over the {@code WatchDeployEvents} server-streaming
|
* 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 static final Object END = new Object();
|
||||||
|
|
||||||
private final BlockingQueue<Object> queue;
|
private final BlockingQueue<Object> queue;
|
||||||
|
private final AtomicBoolean closed = new AtomicBoolean();
|
||||||
private volatile ClientCallStreamObserver<WatchDeployEventsRequest> requestStream;
|
private volatile ClientCallStreamObserver<WatchDeployEventsRequest> requestStream;
|
||||||
private volatile boolean closed;
|
|
||||||
private Object next;
|
private Object next;
|
||||||
|
|
||||||
DeployEventStream(int capacity) {
|
DeployEventStream(int capacity) {
|
||||||
@@ -35,6 +36,9 @@ public final class DeployEventStream implements Iterator<DeployEvent>, AutoClose
|
|||||||
@Override
|
@Override
|
||||||
public void beforeStart(ClientCallStreamObserver<WatchDeployEventsRequest> requestStream) {
|
public void beforeStart(ClientCallStreamObserver<WatchDeployEventsRequest> requestStream) {
|
||||||
DeployEventStream.this.requestStream = requestStream;
|
DeployEventStream.this.requestStream = requestStream;
|
||||||
|
if (closed.get()) {
|
||||||
|
requestStream.cancel("client cancelled deploy event stream", null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -44,7 +48,7 @@ public final class DeployEventStream implements Iterator<DeployEvent>, AutoClose
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onError(Throwable error) {
|
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);
|
offer(END);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -90,7 +94,7 @@ public final class DeployEventStream implements Iterator<DeployEvent>, AutoClose
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void close() {
|
public void close() {
|
||||||
closed = true;
|
closed.set(true);
|
||||||
ClientCallStreamObserver<WatchDeployEventsRequest> stream = requestStream;
|
ClientCallStreamObserver<WatchDeployEventsRequest> stream = requestStream;
|
||||||
if (stream != null) {
|
if (stream != null) {
|
||||||
stream.cancel("client cancelled deploy event stream", null);
|
stream.cancel("client cancelled deploy event stream", null);
|
||||||
|
|||||||
+30
-6
@@ -36,6 +36,8 @@ import javax.net.ssl.SSLException;
|
|||||||
* {@link MxGatewayClient}.
|
* {@link MxGatewayClient}.
|
||||||
*/
|
*/
|
||||||
public final class GalaxyRepositoryClient implements AutoCloseable {
|
public final class GalaxyRepositoryClient implements AutoCloseable {
|
||||||
|
private static final int DISCOVER_HIERARCHY_PAGE_SIZE = 5000;
|
||||||
|
|
||||||
private final ManagedChannel ownedChannel;
|
private final ManagedChannel ownedChannel;
|
||||||
private final MxGatewayClientOptions options;
|
private final MxGatewayClientOptions options;
|
||||||
private final GalaxyRepositoryGrpc.GalaxyRepositoryBlockingStub blockingStub;
|
private final GalaxyRepositoryGrpc.GalaxyRepositoryBlockingStub blockingStub;
|
||||||
@@ -130,9 +132,17 @@ public final class GalaxyRepositoryClient implements AutoCloseable {
|
|||||||
*/
|
*/
|
||||||
public List<GalaxyObject> discoverHierarchy() {
|
public List<GalaxyObject> discoverHierarchy() {
|
||||||
try {
|
try {
|
||||||
DiscoverHierarchyReply reply =
|
java.util.ArrayList<GalaxyObject> objects = new java.util.ArrayList<>();
|
||||||
rawBlockingStub().discoverHierarchy(DiscoverHierarchyRequest.getDefaultInstance());
|
String pageToken = "";
|
||||||
return reply.getObjectsList();
|
do {
|
||||||
|
DiscoverHierarchyReply reply = rawBlockingStub().discoverHierarchy(DiscoverHierarchyRequest.newBuilder()
|
||||||
|
.setPageSize(DISCOVER_HIERARCHY_PAGE_SIZE)
|
||||||
|
.setPageToken(pageToken)
|
||||||
|
.build());
|
||||||
|
objects.addAll(reply.getObjectsList());
|
||||||
|
pageToken = reply.getNextPageToken();
|
||||||
|
} while (!pageToken.isBlank());
|
||||||
|
return objects;
|
||||||
} catch (RuntimeException error) {
|
} catch (RuntimeException error) {
|
||||||
if (error instanceof MxGatewayException) {
|
if (error instanceof MxGatewayException) {
|
||||||
throw error;
|
throw error;
|
||||||
@@ -142,8 +152,7 @@ public final class GalaxyRepositoryClient implements AutoCloseable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public CompletableFuture<List<GalaxyObject>> discoverHierarchyAsync() {
|
public CompletableFuture<List<GalaxyObject>> discoverHierarchyAsync() {
|
||||||
return toCompletable(rawFutureStub().discoverHierarchy(DiscoverHierarchyRequest.getDefaultInstance()))
|
return discoverHierarchyPageAsync("", new java.util.ArrayList<>());
|
||||||
.thenApply(DiscoverHierarchyReply::getObjectsList);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -226,7 +235,7 @@ public final class GalaxyRepositoryClient implements AutoCloseable {
|
|||||||
|
|
||||||
private static ManagedChannel createChannel(MxGatewayClientOptions options) {
|
private static ManagedChannel createChannel(MxGatewayClientOptions options) {
|
||||||
NettyChannelBuilder builder = NettyChannelBuilder.forTarget(options.endpoint())
|
NettyChannelBuilder builder = NettyChannelBuilder.forTarget(options.endpoint())
|
||||||
.maxInboundMessageSize(16 * 1024 * 1024);
|
.maxInboundMessageSize(options.maxGrpcMessageBytes());
|
||||||
if (!options.connectTimeout().isNegative()) {
|
if (!options.connectTimeout().isNegative()) {
|
||||||
builder.withOption(
|
builder.withOption(
|
||||||
io.grpc.netty.shaded.io.netty.channel.ChannelOption.CONNECT_TIMEOUT_MILLIS,
|
io.grpc.netty.shaded.io.netty.channel.ChannelOption.CONNECT_TIMEOUT_MILLIS,
|
||||||
@@ -258,6 +267,21 @@ public final class GalaxyRepositoryClient implements AutoCloseable {
|
|||||||
return stub.withDeadlineAfter(options.callTimeout().toNanos(), TimeUnit.NANOSECONDS);
|
return stub.withDeadlineAfter(options.callTimeout().toNanos(), TimeUnit.NANOSECONDS);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private CompletableFuture<List<GalaxyObject>> discoverHierarchyPageAsync(
|
||||||
|
String pageToken, java.util.ArrayList<GalaxyObject> objects) {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
return discoverHierarchyPageAsync(reply.getNextPageToken(), objects);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private static <T> CompletableFuture<T> toCompletable(com.google.common.util.concurrent.ListenableFuture<T> source) {
|
private static <T> CompletableFuture<T> toCompletable(com.google.common.util.concurrent.ListenableFuture<T> source) {
|
||||||
CompletableFuture<T> target = new CompletableFuture<>();
|
CompletableFuture<T> target = new CompletableFuture<>();
|
||||||
Futures.addCallback(
|
Futures.addCallback(
|
||||||
|
|||||||
+1
-1
@@ -169,7 +169,7 @@ public final class MxGatewayClient implements AutoCloseable {
|
|||||||
|
|
||||||
private static ManagedChannel createChannel(MxGatewayClientOptions options) {
|
private static ManagedChannel createChannel(MxGatewayClientOptions options) {
|
||||||
NettyChannelBuilder builder = NettyChannelBuilder.forTarget(options.endpoint())
|
NettyChannelBuilder builder = NettyChannelBuilder.forTarget(options.endpoint())
|
||||||
.maxInboundMessageSize(16 * 1024 * 1024);
|
.maxInboundMessageSize(options.maxGrpcMessageBytes());
|
||||||
if (!options.connectTimeout().isNegative()) {
|
if (!options.connectTimeout().isNegative()) {
|
||||||
builder.withOption(
|
builder.withOption(
|
||||||
io.grpc.netty.shaded.io.netty.channel.ChannelOption.CONNECT_TIMEOUT_MILLIS,
|
io.grpc.netty.shaded.io.netty.channel.ChannelOption.CONNECT_TIMEOUT_MILLIS,
|
||||||
|
|||||||
+17
@@ -7,6 +7,7 @@ import java.util.Objects;
|
|||||||
public final class MxGatewayClientOptions {
|
public final class MxGatewayClientOptions {
|
||||||
private static final Duration DEFAULT_CONNECT_TIMEOUT = Duration.ofSeconds(10);
|
private static final Duration DEFAULT_CONNECT_TIMEOUT = Duration.ofSeconds(10);
|
||||||
private static final Duration DEFAULT_CALL_TIMEOUT = Duration.ofSeconds(30);
|
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 endpoint;
|
||||||
private final String apiKey;
|
private final String apiKey;
|
||||||
@@ -16,6 +17,7 @@ public final class MxGatewayClientOptions {
|
|||||||
private final Duration connectTimeout;
|
private final Duration connectTimeout;
|
||||||
private final Duration callTimeout;
|
private final Duration callTimeout;
|
||||||
private final Duration streamTimeout;
|
private final Duration streamTimeout;
|
||||||
|
private final int maxGrpcMessageBytes;
|
||||||
|
|
||||||
private MxGatewayClientOptions(Builder builder) {
|
private MxGatewayClientOptions(Builder builder) {
|
||||||
endpoint = requireText(builder.endpoint, "endpoint");
|
endpoint = requireText(builder.endpoint, "endpoint");
|
||||||
@@ -26,6 +28,9 @@ public final class MxGatewayClientOptions {
|
|||||||
connectTimeout = builder.connectTimeout == null ? DEFAULT_CONNECT_TIMEOUT : builder.connectTimeout;
|
connectTimeout = builder.connectTimeout == null ? DEFAULT_CONNECT_TIMEOUT : builder.connectTimeout;
|
||||||
callTimeout = builder.callTimeout == null ? DEFAULT_CALL_TIMEOUT : builder.callTimeout;
|
callTimeout = builder.callTimeout == null ? DEFAULT_CALL_TIMEOUT : builder.callTimeout;
|
||||||
streamTimeout = builder.streamTimeout;
|
streamTimeout = builder.streamTimeout;
|
||||||
|
maxGrpcMessageBytes = builder.maxGrpcMessageBytes <= 0
|
||||||
|
? DEFAULT_MAX_GRPC_MESSAGE_BYTES
|
||||||
|
: builder.maxGrpcMessageBytes;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Builder builder() {
|
public static Builder builder() {
|
||||||
@@ -68,6 +73,10 @@ public final class MxGatewayClientOptions {
|
|||||||
return streamTimeout;
|
return streamTimeout;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int maxGrpcMessageBytes() {
|
||||||
|
return maxGrpcMessageBytes;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return "MxGatewayClientOptions{"
|
return "MxGatewayClientOptions{"
|
||||||
@@ -90,6 +99,8 @@ public final class MxGatewayClientOptions {
|
|||||||
+ callTimeout
|
+ callTimeout
|
||||||
+ ", streamTimeout="
|
+ ", streamTimeout="
|
||||||
+ streamTimeout
|
+ streamTimeout
|
||||||
|
+ ", maxGrpcMessageBytes="
|
||||||
|
+ maxGrpcMessageBytes
|
||||||
+ '}';
|
+ '}';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,6 +120,7 @@ public final class MxGatewayClientOptions {
|
|||||||
private Duration connectTimeout;
|
private Duration connectTimeout;
|
||||||
private Duration callTimeout;
|
private Duration callTimeout;
|
||||||
private Duration streamTimeout;
|
private Duration streamTimeout;
|
||||||
|
private int maxGrpcMessageBytes;
|
||||||
|
|
||||||
private Builder() {
|
private Builder() {
|
||||||
}
|
}
|
||||||
@@ -153,6 +165,11 @@ public final class MxGatewayClientOptions {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Builder maxGrpcMessageBytes(int value) {
|
||||||
|
maxGrpcMessageBytes = value;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public MxGatewayClientOptions build() {
|
public MxGatewayClientOptions build() {
|
||||||
return new MxGatewayClientOptions(this);
|
return new MxGatewayClientOptions(this);
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
package com.dohertylan.mxgateway.client;
|
package com.dohertylan.mxgateway.client;
|
||||||
|
|
||||||
public final class MxGatewayClientVersion {
|
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 int WORKER_PROTOCOL_VERSION = 1;
|
||||||
private static final String CLIENT_VERSION = "0.1.0";
|
private static final String CLIENT_VERSION = "0.1.0";
|
||||||
|
|
||||||
|
|||||||
+99
-22
@@ -25,6 +25,8 @@ import io.grpc.ServerCallHandler;
|
|||||||
import io.grpc.ServerInterceptor;
|
import io.grpc.ServerInterceptor;
|
||||||
import io.grpc.inprocess.InProcessChannelBuilder;
|
import io.grpc.inprocess.InProcessChannelBuilder;
|
||||||
import io.grpc.inprocess.InProcessServerBuilder;
|
import io.grpc.inprocess.InProcessServerBuilder;
|
||||||
|
import io.grpc.stub.ClientCallStreamObserver;
|
||||||
|
import io.grpc.stub.ClientResponseObserver;
|
||||||
import io.grpc.stub.StreamObserver;
|
import io.grpc.stub.StreamObserver;
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
@@ -100,31 +102,44 @@ final class GalaxyRepositoryClientTests {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void discoverHierarchyReturnsObjectsAndAttributes() throws Exception {
|
void discoverHierarchyReturnsObjectsAndAttributes() throws Exception {
|
||||||
AtomicReference<DiscoverHierarchyRequest> seenRequest = new AtomicReference<>();
|
AtomicReference<DiscoverHierarchyRequest> firstRequest = new AtomicReference<>();
|
||||||
|
AtomicReference<DiscoverHierarchyRequest> secondRequest = new AtomicReference<>();
|
||||||
TestService service = new TestService() {
|
TestService service = new TestService() {
|
||||||
@Override
|
@Override
|
||||||
public void discoverHierarchy(
|
public void discoverHierarchy(
|
||||||
DiscoverHierarchyRequest request, StreamObserver<DiscoverHierarchyReply> responseObserver) {
|
DiscoverHierarchyRequest request, StreamObserver<DiscoverHierarchyReply> responseObserver) {
|
||||||
seenRequest.set(request);
|
if (request.getPageToken().isEmpty()) {
|
||||||
responseObserver.onNext(DiscoverHierarchyReply.newBuilder()
|
firstRequest.set(request);
|
||||||
.addObjects(GalaxyObject.newBuilder()
|
responseObserver.onNext(DiscoverHierarchyReply.newBuilder()
|
||||||
.setGobjectId(7)
|
.setNextPageToken("page-2")
|
||||||
.setTagName("Pump_001")
|
.setTotalObjectCount(2)
|
||||||
.setContainedName("Pump")
|
.addObjects(GalaxyObject.newBuilder()
|
||||||
.setBrowseName("Pump")
|
.setGobjectId(7)
|
||||||
.setParentGobjectId(1)
|
.setTagName("Pump_001")
|
||||||
.setIsArea(false)
|
.setContainedName("Pump")
|
||||||
.setCategoryId(3)
|
.setBrowseName("Pump")
|
||||||
.setHostedByGobjectId(0)
|
.setParentGobjectId(1)
|
||||||
.addTemplateChain("$Pump")
|
.setIsArea(false)
|
||||||
.addAttributes(GalaxyAttribute.newBuilder()
|
.setCategoryId(3)
|
||||||
.setAttributeName("Speed")
|
.setHostedByGobjectId(0)
|
||||||
.setFullTagReference("Pump_001.Speed")
|
.addTemplateChain("$Pump")
|
||||||
.setMxDataType(5)
|
.addAttributes(GalaxyAttribute.newBuilder()
|
||||||
.setDataTypeName("MxFloat")
|
.setAttributeName("Speed")
|
||||||
.setIsArray(false)
|
.setFullTagReference("Pump_001.Speed")
|
||||||
.setIsHistorized(true)))
|
.setMxDataType(5)
|
||||||
.build());
|
.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();
|
responseObserver.onCompleted();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -132,7 +147,10 @@ final class GalaxyRepositoryClientTests {
|
|||||||
try (InProcessGalaxy g = InProcessGalaxy.start(service, new AtomicReference<>());
|
try (InProcessGalaxy g = InProcessGalaxy.start(service, new AtomicReference<>());
|
||||||
GalaxyRepositoryClient client = g.client("")) {
|
GalaxyRepositoryClient client = g.client("")) {
|
||||||
List<GalaxyObject> objects = client.discoverHierarchy();
|
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);
|
GalaxyObject only = objects.get(0);
|
||||||
assertEquals(7, only.getGobjectId());
|
assertEquals(7, only.getGobjectId());
|
||||||
assertEquals("Pump_001", only.getTagName());
|
assertEquals("Pump_001", only.getTagName());
|
||||||
@@ -142,6 +160,20 @@ 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
|
@Test
|
||||||
void watchDeployEventsReceivesEventsInOrder() throws Exception {
|
void watchDeployEventsReceivesEventsInOrder() throws Exception {
|
||||||
DeployEvent first = DeployEvent.newBuilder()
|
DeployEvent first = DeployEvent.newBuilder()
|
||||||
@@ -281,6 +313,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 {
|
private record InProcessGalaxy(Server server, ManagedChannel channel) implements AutoCloseable {
|
||||||
static InProcessGalaxy start(
|
static InProcessGalaxy start(
|
||||||
GalaxyRepositoryGrpc.GalaxyRepositoryImplBase service, AtomicReference<String> authorization)
|
GalaxyRepositoryGrpc.GalaxyRepositoryImplBase service, AtomicReference<String> authorization)
|
||||||
|
|||||||
+652
-86
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@@ -2,7 +2,7 @@
|
|||||||
"schemaVersion": 1,
|
"schemaVersion": 1,
|
||||||
"fixtureSet": "mxaccess-gateway-client-behavior",
|
"fixtureSet": "mxaccess-gateway-client-behavior",
|
||||||
"contractName": "mxaccess-gateway",
|
"contractName": "mxaccess-gateway",
|
||||||
"gatewayProtocolVersion": 1,
|
"gatewayProtocolVersion": 2,
|
||||||
"workerProtocolVersion": 1,
|
"workerProtocolVersion": 1,
|
||||||
"protoInputManifest": "clients/proto/proto-inputs.json",
|
"protoInputManifest": "clients/proto/proto-inputs.json",
|
||||||
"fixtures": [
|
"fixtures": [
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
"backendName": "mxaccess-worker",
|
"backendName": "mxaccess-worker",
|
||||||
"workerProcessId": 1234,
|
"workerProcessId": 1234,
|
||||||
"workerProtocolVersion": 1,
|
"workerProtocolVersion": 1,
|
||||||
"gatewayProtocolVersion": 1,
|
"gatewayProtocolVersion": 2,
|
||||||
"capabilities": [
|
"capabilities": [
|
||||||
"unary-open-session",
|
"unary-open-session",
|
||||||
"unary-close-session",
|
"unary-close-session",
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"schemaVersion": 1,
|
"schemaVersion": 1,
|
||||||
"fixtureSet": "mxaccess-gateway-parity-fixture-matrix",
|
"fixtureSet": "mxaccess-gateway-parity-fixture-matrix",
|
||||||
"contractName": "mxaccess-gateway",
|
"contractName": "mxaccess-gateway",
|
||||||
"gatewayProtocolVersion": 1,
|
"gatewayProtocolVersion": 2,
|
||||||
"workerProtocolVersion": 1,
|
"workerProtocolVersion": 1,
|
||||||
"sourceCaptureRoot": "C:/Users/dohertj2/Desktop/mxaccess/captures",
|
"sourceCaptureRoot": "C:/Users/dohertj2/Desktop/mxaccess/captures",
|
||||||
"sourceDocs": [
|
"sourceDocs": [
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"schemaVersion": 1,
|
"schemaVersion": 1,
|
||||||
"contractName": "mxaccess-gateway",
|
"contractName": "mxaccess-gateway",
|
||||||
"gatewayProtocolVersion": 1,
|
"gatewayProtocolVersion": 2,
|
||||||
"workerProtocolVersion": 1,
|
"workerProtocolVersion": 1,
|
||||||
"protoRoot": "src/MxGateway.Contracts/Protos",
|
"protoRoot": "src/MxGateway.Contracts/Protos",
|
||||||
"sourceFiles": [
|
"sourceFiles": [
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ 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
|
||||||
|
|
||||||
|
_DISCOVER_HIERARCHY_PAGE_SIZE = 5000
|
||||||
|
|
||||||
|
|
||||||
class GalaxyRepositoryClient:
|
class GalaxyRepositoryClient:
|
||||||
"""Async client for the Galaxy Repository gRPC service."""
|
"""Async client for the Galaxy Repository gRPC service."""
|
||||||
@@ -112,12 +114,21 @@ class GalaxyRepositoryClient:
|
|||||||
async def discover_hierarchy(self) -> list[galaxy_pb.GalaxyObject]:
|
async def discover_hierarchy(self) -> list[galaxy_pb.GalaxyObject]:
|
||||||
"""Return the deployed Galaxy object hierarchy as raw proto messages."""
|
"""Return the deployed Galaxy object hierarchy as raw proto messages."""
|
||||||
|
|
||||||
reply = await self._unary(
|
objects: list[galaxy_pb.GalaxyObject] = []
|
||||||
"discover hierarchy",
|
page_token = ""
|
||||||
self.raw_stub.DiscoverHierarchy,
|
while True:
|
||||||
galaxy_pb.DiscoverHierarchyRequest(),
|
reply = await self._unary(
|
||||||
)
|
"discover hierarchy",
|
||||||
return list(reply.objects)
|
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
|
||||||
|
|
||||||
def watch_deploy_events(
|
def watch_deploy_events(
|
||||||
self,
|
self,
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ _sym_db = _symbol_database.Default()
|
|||||||
from google.protobuf import timestamp_pb2 as google_dot_protobuf_dot_timestamp__pb2
|
from google.protobuf import timestamp_pb2 as google_dot_protobuf_dot_timestamp__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\"\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\"A\n\x18\x44iscoverHierarchyRequest\x12\x11\n\tpage_size\x18\x01 \x01(\x05\x12\x12\n\npage_token\x18\x02 \x01(\t\"\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()
|
_globals = globals()
|
||||||
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
|
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
|
||||||
@@ -42,17 +42,17 @@ if not _descriptor._USE_C_DESCRIPTORS:
|
|||||||
_globals['_GETLASTDEPLOYTIMEREPLY']._serialized_start=170
|
_globals['_GETLASTDEPLOYTIMEREPLY']._serialized_start=170
|
||||||
_globals['_GETLASTDEPLOYTIMEREPLY']._serialized_end=268
|
_globals['_GETLASTDEPLOYTIMEREPLY']._serialized_end=268
|
||||||
_globals['_DISCOVERHIERARCHYREQUEST']._serialized_start=270
|
_globals['_DISCOVERHIERARCHYREQUEST']._serialized_start=270
|
||||||
_globals['_DISCOVERHIERARCHYREQUEST']._serialized_end=296
|
_globals['_DISCOVERHIERARCHYREQUEST']._serialized_end=335
|
||||||
_globals['_DISCOVERHIERARCHYREPLY']._serialized_start=298
|
_globals['_DISCOVERHIERARCHYREPLY']._serialized_start=338
|
||||||
_globals['_DISCOVERHIERARCHYREPLY']._serialized_end=375
|
_globals['_DISCOVERHIERARCHYREPLY']._serialized_end=468
|
||||||
_globals['_WATCHDEPLOYEVENTSREQUEST']._serialized_start=377
|
_globals['_WATCHDEPLOYEVENTSREQUEST']._serialized_start=470
|
||||||
_globals['_WATCHDEPLOYEVENTSREQUEST']._serialized_end=462
|
_globals['_WATCHDEPLOYEVENTSREQUEST']._serialized_end=555
|
||||||
_globals['_DEPLOYEVENT']._serialized_start=465
|
_globals['_DEPLOYEVENT']._serialized_start=558
|
||||||
_globals['_DEPLOYEVENT']._serialized_end=686
|
_globals['_DEPLOYEVENT']._serialized_end=779
|
||||||
_globals['_GALAXYOBJECT']._serialized_start=689
|
_globals['_GALAXYOBJECT']._serialized_start=782
|
||||||
_globals['_GALAXYOBJECT']._serialized_end=964
|
_globals['_GALAXYOBJECT']._serialized_end=1057
|
||||||
_globals['_GALAXYATTRIBUTE']._serialized_start=967
|
_globals['_GALAXYATTRIBUTE']._serialized_start=1060
|
||||||
_globals['_GALAXYATTRIBUTE']._serialized_end=1263
|
_globals['_GALAXYATTRIBUTE']._serialized_end=1356
|
||||||
_globals['_GALAXYREPOSITORY']._serialized_start=1266
|
_globals['_GALAXYREPOSITORY']._serialized_start=1359
|
||||||
_globals['_GALAXYREPOSITORY']._serialized_end=1726
|
_globals['_GALAXYREPOSITORY']._serialized_end=1819
|
||||||
# @@protoc_insertion_point(module_scope)
|
# @@protoc_insertion_point(module_scope)
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ class ClientOptions:
|
|||||||
server_name_override: str | None = None
|
server_name_override: str | None = None
|
||||||
call_timeout: float | None = 30.0
|
call_timeout: float | None = 30.0
|
||||||
stream_timeout: float | None = None
|
stream_timeout: float | None = None
|
||||||
|
max_grpc_message_bytes: int = 16 * 1024 * 1024
|
||||||
|
|
||||||
def __post_init__(self) -> None:
|
def __post_init__(self) -> None:
|
||||||
if not self.endpoint:
|
if not self.endpoint:
|
||||||
@@ -32,6 +33,8 @@ class ClientOptions:
|
|||||||
raise ValueError("call_timeout must be greater than zero")
|
raise ValueError("call_timeout must be greater than zero")
|
||||||
if self.stream_timeout is not None and self.stream_timeout <= 0:
|
if self.stream_timeout is not None and self.stream_timeout <= 0:
|
||||||
raise ValueError("stream_timeout must be greater than zero")
|
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:
|
def __repr__(self) -> str:
|
||||||
api_key = REDACTED if self.api_key else None
|
api_key = REDACTED if self.api_key else None
|
||||||
@@ -41,14 +44,18 @@ class ClientOptions:
|
|||||||
f"ca_file={self.ca_file!r}, "
|
f"ca_file={self.ca_file!r}, "
|
||||||
f"server_name_override={self.server_name_override!r}, "
|
f"server_name_override={self.server_name_override!r}, "
|
||||||
f"call_timeout={self.call_timeout!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:
|
def create_channel(options: ClientOptions) -> grpc.aio.Channel:
|
||||||
"""Create a plaintext or TLS `grpc.aio` channel from client options."""
|
"""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:
|
if options.server_name_override:
|
||||||
channel_options.append(("grpc.ssl_target_name_override", 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))
|
channel = create_channel(ClientOptions(endpoint="localhost:5000", plaintext=True))
|
||||||
|
|
||||||
assert channel == "plain-channel"
|
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:
|
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 channel == "tls-channel"
|
||||||
assert calls == [
|
assert calls == [
|
||||||
(
|
(
|
||||||
"gateway.example:5001",
|
"gateway.example:5001",
|
||||||
"creds",
|
"creds",
|
||||||
[("grpc.ssl_target_name_override", "gateway.test")],
|
[
|
||||||
),
|
("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 = FakeGalaxyStub()
|
||||||
stub.discover_hierarchy.replies = [
|
stub.discover_hierarchy.replies = [
|
||||||
galaxy_pb.DiscoverHierarchyReply(
|
galaxy_pb.DiscoverHierarchyReply(
|
||||||
|
next_page_token="page-2",
|
||||||
|
total_object_count=2,
|
||||||
objects=[
|
objects=[
|
||||||
galaxy_pb.GalaxyObject(
|
galaxy_pb.GalaxyObject(
|
||||||
gobject_id=1,
|
gobject_id=1,
|
||||||
@@ -106,6 +108,11 @@ async def test_discover_hierarchy_returns_proto_objects() -> None:
|
|||||||
browse_name="TestMachine_001",
|
browse_name="TestMachine_001",
|
||||||
is_area=True,
|
is_area=True,
|
||||||
),
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
galaxy_pb.DiscoverHierarchyReply(
|
||||||
|
total_object_count=2,
|
||||||
|
objects=[
|
||||||
galaxy_pb.GalaxyObject(
|
galaxy_pb.GalaxyObject(
|
||||||
gobject_id=2,
|
gobject_id=2,
|
||||||
tag_name="DelmiaReceiver_001",
|
tag_name="DelmiaReceiver_001",
|
||||||
@@ -133,6 +140,10 @@ async def test_discover_hierarchy_returns_proto_objects() -> None:
|
|||||||
|
|
||||||
assert isinstance(objects, list)
|
assert isinstance(objects, list)
|
||||||
assert len(objects) == 2
|
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[0].tag_name == "TestMachine_001"
|
||||||
assert objects[1].attributes[0].full_tag_reference == "DelmiaReceiver_001.DownloadPath"
|
assert objects[1].attributes[0].full_tag_reference == "DelmiaReceiver_001.DownloadPath"
|
||||||
|
|
||||||
|
|||||||
@@ -1038,7 +1038,7 @@ mod tests {
|
|||||||
fn version_json_output_has_protocol_versions() {
|
fn version_json_output_has_protocol_versions() {
|
||||||
let value = super::version_json();
|
let value = super::version_json();
|
||||||
|
|
||||||
assert_eq!(value["gatewayProtocolVersion"], 1);
|
assert_eq!(value["gatewayProtocolVersion"], 2);
|
||||||
assert_eq!(value["workerProtocolVersion"], 1);
|
assert_eq!(value["workerProtocolVersion"], 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -54,9 +54,12 @@ impl GatewayClient {
|
|||||||
|
|
||||||
let channel = endpoint.connect().await?;
|
let channel = endpoint.connect().await?;
|
||||||
let interceptor = AuthInterceptor::new(options.api_key().cloned());
|
let interceptor = AuthInterceptor::new(options.api_key().cloned());
|
||||||
|
let max_grpc_message_bytes = options.max_grpc_message_bytes();
|
||||||
|
|
||||||
Ok(Self {
|
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(),
|
call_timeout: options.call_timeout(),
|
||||||
stream_timeout: options.stream_timeout(),
|
stream_timeout: options.stream_timeout(),
|
||||||
})
|
})
|
||||||
|
|||||||
+99
-33
@@ -21,6 +21,8 @@ use crate::generated::galaxy_repository::v1::{
|
|||||||
};
|
};
|
||||||
use crate::options::ClientOptions;
|
use crate::options::ClientOptions;
|
||||||
|
|
||||||
|
const DISCOVER_HIERARCHY_PAGE_SIZE: i32 = 5000;
|
||||||
|
|
||||||
/// Convenience alias for the generated Galaxy client wrapped in the
|
/// Convenience alias for the generated Galaxy client wrapped in the
|
||||||
/// authentication interceptor.
|
/// authentication interceptor.
|
||||||
pub type RawGalaxyClient = GalaxyRepositoryClient<InterceptedService<Channel, AuthInterceptor>>;
|
pub type RawGalaxyClient = GalaxyRepositoryClient<InterceptedService<Channel, AuthInterceptor>>;
|
||||||
@@ -77,9 +79,12 @@ impl GalaxyClient {
|
|||||||
|
|
||||||
let channel = endpoint.connect().await?;
|
let channel = endpoint.connect().await?;
|
||||||
let interceptor = AuthInterceptor::new(options.api_key().cloned());
|
let interceptor = AuthInterceptor::new(options.api_key().cloned());
|
||||||
|
let max_grpc_message_bytes = options.max_grpc_message_bytes();
|
||||||
|
|
||||||
Ok(Self {
|
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(),
|
call_timeout: options.call_timeout(),
|
||||||
stream_timeout: options.stream_timeout(),
|
stream_timeout: options.stream_timeout(),
|
||||||
})
|
})
|
||||||
@@ -89,8 +94,11 @@ impl GalaxyClient {
|
|||||||
/// channel. Tests use this to wire up an in-memory transport.
|
/// channel. Tests use this to wire up an in-memory transport.
|
||||||
pub fn from_channel(channel: Channel, options: &ClientOptions) -> Self {
|
pub fn from_channel(channel: Channel, options: &ClientOptions) -> Self {
|
||||||
let interceptor = AuthInterceptor::new(options.api_key().cloned());
|
let interceptor = AuthInterceptor::new(options.api_key().cloned());
|
||||||
|
let max_grpc_message_bytes = options.max_grpc_message_bytes();
|
||||||
Self {
|
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(),
|
call_timeout: options.call_timeout(),
|
||||||
stream_timeout: options.stream_timeout(),
|
stream_timeout: options.stream_timeout(),
|
||||||
}
|
}
|
||||||
@@ -135,11 +143,23 @@ impl GalaxyClient {
|
|||||||
/// Walk the deployed object hierarchy. Each [`GalaxyObject`] contains
|
/// Walk the deployed object hierarchy. Each [`GalaxyObject`] contains
|
||||||
/// 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 response = self
|
let mut objects = Vec::new();
|
||||||
.inner
|
let mut page_token = String::new();
|
||||||
.discover_hierarchy(self.unary_request(DiscoverHierarchyRequest {}))
|
loop {
|
||||||
.await?;
|
let response = self
|
||||||
Ok(response.into_inner().objects)
|
.inner
|
||||||
|
.discover_hierarchy(self.unary_request(DiscoverHierarchyRequest {
|
||||||
|
page_size: DISCOVER_HIERARCHY_PAGE_SIZE,
|
||||||
|
page_token,
|
||||||
|
}))
|
||||||
|
.await?;
|
||||||
|
let reply = response.into_inner();
|
||||||
|
objects.extend(reply.objects);
|
||||||
|
page_token = reply.next_page_token;
|
||||||
|
if page_token.is_empty() {
|
||||||
|
return Ok(objects);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Subscribe to the server-streamed deploy-event feed.
|
/// Subscribe to the server-streamed deploy-event feed.
|
||||||
@@ -217,6 +237,8 @@ mod tests {
|
|||||||
present: Mutex<bool>,
|
present: Mutex<bool>,
|
||||||
last_deploy: Mutex<Option<Timestamp>>,
|
last_deploy: Mutex<Option<Timestamp>>,
|
||||||
objects: Mutex<Vec<GalaxyObject>>,
|
objects: Mutex<Vec<GalaxyObject>>,
|
||||||
|
discover_requests: Mutex<Vec<DiscoverHierarchyRequest>>,
|
||||||
|
discover_replies: Mutex<std::collections::VecDeque<DiscoverHierarchyReply>>,
|
||||||
watch_requests: Mutex<Vec<WatchDeployEventsRequest>>,
|
watch_requests: Mutex<Vec<WatchDeployEventsRequest>>,
|
||||||
watch_events: Mutex<Vec<DeployEvent>>,
|
watch_events: Mutex<Vec<DeployEvent>>,
|
||||||
watch_senders: Mutex<Vec<DeployEventTx>>,
|
watch_senders: Mutex<Vec<DeployEventTx>>,
|
||||||
@@ -256,10 +278,21 @@ mod tests {
|
|||||||
|
|
||||||
async fn discover_hierarchy(
|
async fn discover_hierarchy(
|
||||||
&self,
|
&self,
|
||||||
_request: Request<DiscoverHierarchyRequest>,
|
request: Request<DiscoverHierarchyRequest>,
|
||||||
) -> Result<Response<DiscoverHierarchyReply>, Status> {
|
) -> 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 {
|
Ok(Response::new(DiscoverHierarchyReply {
|
||||||
objects: self.state.objects.lock().unwrap().clone(),
|
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 +442,58 @@ mod tests {
|
|||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn discover_hierarchy_returns_objects_with_attributes() {
|
async fn discover_hierarchy_returns_objects_with_attributes() {
|
||||||
let state = Arc::new(FakeState::default());
|
let state = Arc::new(FakeState::default());
|
||||||
*state.objects.lock().unwrap() = vec![GalaxyObject {
|
state
|
||||||
gobject_id: 42,
|
.discover_replies
|
||||||
tag_name: "DelmiaReceiver_001".to_owned(),
|
.lock()
|
||||||
contained_name: "DelmiaReceiver".to_owned(),
|
.unwrap()
|
||||||
browse_name: "TestMachine_001/DelmiaReceiver".to_owned(),
|
.push_back(DiscoverHierarchyReply {
|
||||||
parent_gobject_id: 7,
|
objects: vec![GalaxyObject {
|
||||||
is_area: false,
|
gobject_id: 42,
|
||||||
category_id: 3,
|
tag_name: "DelmiaReceiver_001".to_owned(),
|
||||||
hosted_by_gobject_id: 1,
|
contained_name: "DelmiaReceiver".to_owned(),
|
||||||
template_chain: vec!["$UserDefined".to_owned(), "$DelmiaReceiver".to_owned()],
|
browse_name: "TestMachine_001/DelmiaReceiver".to_owned(),
|
||||||
attributes: vec![GalaxyAttribute {
|
parent_gobject_id: 7,
|
||||||
attribute_name: "DownloadPath".to_owned(),
|
is_area: false,
|
||||||
full_tag_reference: "DelmiaReceiver_001.DownloadPath".to_owned(),
|
category_id: 3,
|
||||||
mx_data_type: 8,
|
hosted_by_gobject_id: 1,
|
||||||
data_type_name: "MxString".to_owned(),
|
template_chain: vec!["$UserDefined".to_owned(), "$DelmiaReceiver".to_owned()],
|
||||||
is_array: false,
|
attributes: vec![GalaxyAttribute {
|
||||||
array_dimension: 0,
|
attribute_name: "DownloadPath".to_owned(),
|
||||||
array_dimension_present: false,
|
full_tag_reference: "DelmiaReceiver_001.DownloadPath".to_owned(),
|
||||||
mx_attribute_category: 2,
|
mx_data_type: 8,
|
||||||
security_classification: 1,
|
data_type_name: "MxString".to_owned(),
|
||||||
is_historized: false,
|
is_array: false,
|
||||||
is_alarm: 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 endpoint = spawn_fake(state.clone()).await;
|
||||||
|
|
||||||
let mut client = GalaxyClient::connect(ClientOptions::new(endpoint))
|
let mut client = GalaxyClient::connect(ClientOptions::new(endpoint))
|
||||||
@@ -441,7 +502,12 @@ mod tests {
|
|||||||
|
|
||||||
let objects = client.discover_hierarchy().await.unwrap();
|
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].tag_name, "DelmiaReceiver_001");
|
||||||
assert_eq!(objects[0].attributes.len(), 1);
|
assert_eq!(objects[0].attributes.len(), 1);
|
||||||
assert_eq!(objects[0].attributes[0].attribute_name, "DownloadPath");
|
assert_eq!(objects[0].attributes[0].attribute_name, "DownloadPath");
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ use std::time::Duration;
|
|||||||
|
|
||||||
use crate::auth::ApiKey;
|
use crate::auth::ApiKey;
|
||||||
|
|
||||||
|
const DEFAULT_MAX_GRPC_MESSAGE_BYTES: usize = 16 * 1024 * 1024;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct ClientOptions {
|
pub struct ClientOptions {
|
||||||
endpoint: String,
|
endpoint: String,
|
||||||
@@ -14,6 +16,7 @@ pub struct ClientOptions {
|
|||||||
connect_timeout: Duration,
|
connect_timeout: Duration,
|
||||||
call_timeout: Duration,
|
call_timeout: Duration,
|
||||||
stream_timeout: Option<Duration>,
|
stream_timeout: Option<Duration>,
|
||||||
|
max_grpc_message_bytes: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ClientOptions {
|
impl ClientOptions {
|
||||||
@@ -27,6 +30,7 @@ impl ClientOptions {
|
|||||||
connect_timeout: Duration::from_secs(10),
|
connect_timeout: Duration::from_secs(10),
|
||||||
call_timeout: Duration::from_secs(30),
|
call_timeout: Duration::from_secs(30),
|
||||||
stream_timeout: None,
|
stream_timeout: None,
|
||||||
|
max_grpc_message_bytes: DEFAULT_MAX_GRPC_MESSAGE_BYTES,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,6 +69,11 @@ impl ClientOptions {
|
|||||||
self
|
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
|
||||||
|
}
|
||||||
|
|
||||||
pub fn endpoint(&self) -> &str {
|
pub fn endpoint(&self) -> &str {
|
||||||
&self.endpoint
|
&self.endpoint
|
||||||
}
|
}
|
||||||
@@ -96,6 +105,10 @@ impl ClientOptions {
|
|||||||
pub fn stream_timeout(&self) -> Option<Duration> {
|
pub fn stream_timeout(&self) -> Option<Duration> {
|
||||||
self.stream_timeout
|
self.stream_timeout
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn max_grpc_message_bytes(&self) -> usize {
|
||||||
|
self.max_grpc_message_bytes
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for ClientOptions {
|
impl Default for ClientOptions {
|
||||||
@@ -116,6 +129,7 @@ impl fmt::Debug for ClientOptions {
|
|||||||
.field("connect_timeout", &self.connect_timeout)
|
.field("connect_timeout", &self.connect_timeout)
|
||||||
.field("call_timeout", &self.call_timeout)
|
.field("call_timeout", &self.call_timeout)
|
||||||
.field("stream_timeout", &self.stream_timeout)
|
.field("stream_timeout", &self.stream_timeout)
|
||||||
|
.field("max_grpc_message_bytes", &self.max_grpc_message_bytes)
|
||||||
.finish()
|
.finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
pub const CLIENT_VERSION: &str = "0.1.0-dev";
|
pub const CLIENT_VERSION: &str = "0.1.0-dev";
|
||||||
pub const GATEWAY_PROTOCOL_VERSION: u32 = 1;
|
pub const GATEWAY_PROTOCOL_VERSION: u32 = 2;
|
||||||
pub const WORKER_PROTOCOL_VERSION: u32 = 1;
|
pub const WORKER_PROTOCOL_VERSION: u32 = 1;
|
||||||
|
|||||||
+34
-13
@@ -32,13 +32,14 @@ The service is defined in
|
|||||||
|-----|---------|
|
|-----|---------|
|
||||||
| `TestConnection` | Connectivity probe. Returns `{ ok: bool }` after a `SELECT 1`. Does not throw on SQL failure — returns `ok = false`. Always hits SQL directly so it remains a true health check. |
|
| `TestConnection` | Connectivity probe. Returns `{ ok: bool }` after a `SELECT 1`. Does not throw on SQL failure — returns `ok = false`. Always hits SQL directly so it remains a true health check. |
|
||||||
| `GetLastDeployTime` | Returns the cached `galaxy.time_of_last_deploy`. Served from the shared hierarchy cache; refreshed in the background. |
|
| `GetLastDeployTime` | Returns the cached `galaxy.time_of_last_deploy`. Served from the shared hierarchy cache; refreshed in the background. |
|
||||||
| `DiscoverHierarchy` | Returns the full deployed hierarchy plus every object's dynamic attributes. **Served from cache** — see [Hierarchy Cache](#hierarchy-cache). |
|
| `DiscoverHierarchy` | Returns one page of the deployed hierarchy plus each returned object's dynamic attributes. **Served from cache** — see [Hierarchy Cache](#hierarchy-cache). |
|
||||||
| `WatchDeployEvents` | **Server-streaming.** The server emits the current state immediately on subscribe (so clients can bootstrap without waiting), then emits one event per detected deploy change. See [Deploy Notifications](#deploy-notifications). |
|
| `WatchDeployEvents` | **Server-streaming.** The server emits the current state immediately on subscribe (so clients can bootstrap without waiting), then emits one event per detected deploy change. See [Deploy Notifications](#deploy-notifications). |
|
||||||
|
|
||||||
`DiscoverHierarchy` is intentionally a single unary RPC rather than a stream:
|
`DiscoverHierarchy` is a paged unary RPC. The raw request accepts `page_size`
|
||||||
the row set is small (thousands of objects, low tens-of-thousands of
|
and `page_token`; the server defaults omitted page size to 1000 objects and
|
||||||
attributes for typical Galaxies) and clients almost always want the whole tree
|
caps every page at 5000 objects. Invalid page tokens and negative page sizes
|
||||||
at once.
|
return `InvalidArgument`. Official high-level clients preserve the older
|
||||||
|
"return the full hierarchy" behavior by looping pages internally.
|
||||||
|
|
||||||
## Hierarchy Cache
|
## Hierarchy Cache
|
||||||
|
|
||||||
@@ -56,12 +57,14 @@ Refresh strategy is **deploy-time gated**:
|
|||||||
3. If the deploy timestamp is unchanged, the heavy hierarchy + attributes
|
3. If the deploy timestamp is unchanged, the heavy hierarchy + attributes
|
||||||
queries are **skipped**. The cache simply marks `LastSuccessAt`.
|
queries are **skipped**. The cache simply marks `LastSuccessAt`.
|
||||||
4. If the deploy timestamp changed (or no data has loaded yet), the cache
|
4. If the deploy timestamp changed (or no data has loaded yet), the cache
|
||||||
pulls hierarchy + attributes, materializes a `DiscoverHierarchyReply`
|
pulls hierarchy + attributes, materializes a Galaxy object list plus a
|
||||||
once, replaces the entry atomically, and publishes a deploy event.
|
dashboard summary once, replaces the entry atomically, and publishes a
|
||||||
|
deploy event.
|
||||||
|
|
||||||
Materializing the reply at refresh time means subsequent `DiscoverHierarchy`
|
Materializing objects and dashboard summaries at refresh time means subsequent
|
||||||
calls return a pre-built proto message — no per-request projection, no
|
`DiscoverHierarchy` calls page over an immutable object list. The dashboard
|
||||||
per-request allocations beyond the gRPC serializer's frame.
|
uses the precomputed summary and does not rescan raw SQL rowsets on each
|
||||||
|
snapshot.
|
||||||
|
|
||||||
When SQL is unreachable, the cache retains the previous data and flips
|
When SQL is unreachable, the cache retains the previous data and flips
|
||||||
`Status` to `Stale` (or `Unavailable` if no data was ever loaded). A
|
`Status` to `Stale` (or `Unavailable` if no data was ever loaded). A
|
||||||
@@ -139,6 +142,17 @@ message GalaxyAttribute {
|
|||||||
bool is_historized = 10;
|
bool is_historized = 10;
|
||||||
bool is_alarm = 11;
|
bool is_alarm = 11;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message DiscoverHierarchyRequest {
|
||||||
|
int32 page_size = 1; // omitted/0 uses the server default of 1000
|
||||||
|
string page_token = 2; // opaque offset token returned by the previous page
|
||||||
|
}
|
||||||
|
|
||||||
|
message DiscoverHierarchyReply {
|
||||||
|
repeated GalaxyObject objects = 1;
|
||||||
|
string next_page_token = 2;
|
||||||
|
int32 total_object_count = 3;
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Contained Name vs Tag Name
|
### Contained Name vs Tag Name
|
||||||
@@ -176,7 +190,8 @@ GalaxyHierarchyRefreshService (BackgroundService)
|
|||||||
-> GalaxyRepository.GetLastDeployTimeAsync (cheap, every tick)
|
-> GalaxyRepository.GetLastDeployTimeAsync (cheap, every tick)
|
||||||
-> GalaxyRepository.GetHierarchyAsync (only on deploy change)
|
-> GalaxyRepository.GetHierarchyAsync (only on deploy change)
|
||||||
-> GalaxyRepository.GetAttributesAsync (only on deploy change)
|
-> GalaxyRepository.GetAttributesAsync (only on deploy change)
|
||||||
-> GalaxyProtoMapper.MapObject (materialize DiscoverHierarchyReply once)
|
-> GalaxyProtoMapper.MapObject (materialize GalaxyObject list once)
|
||||||
|
-> DashboardGalaxySummary (precompute dashboard counts once)
|
||||||
-> IGalaxyDeployNotifier.Publish (only on deploy change)
|
-> IGalaxyDeployNotifier.Publish (only on deploy change)
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -189,8 +204,9 @@ Component breakdown:
|
|||||||
recursive CTEs and pick the most-derived attribute override per object.
|
recursive CTEs and pick the most-derived attribute override per object.
|
||||||
- `GalaxyHierarchyCache`
|
- `GalaxyHierarchyCache`
|
||||||
(`src/MxGateway.Server/Galaxy/GalaxyHierarchyCache.cs`) holds the most
|
(`src/MxGateway.Server/Galaxy/GalaxyHierarchyCache.cs`) holds the most
|
||||||
recent immutable `GalaxyHierarchyCacheEntry` (rows + materialized proto
|
recent immutable `GalaxyHierarchyCacheEntry` (materialized objects +
|
||||||
reply + counts + status). All gRPC clients share the same entry.
|
precomputed dashboard summary + counts + status). All gRPC clients share the
|
||||||
|
same entry.
|
||||||
- `GalaxyHierarchyRefreshService`
|
- `GalaxyHierarchyRefreshService`
|
||||||
(`src/MxGateway.Server/Galaxy/GalaxyHierarchyRefreshService.cs`) is a
|
(`src/MxGateway.Server/Galaxy/GalaxyHierarchyRefreshService.cs`) is a
|
||||||
hosted `BackgroundService` that drives `RefreshAsync` on the configured
|
hosted `BackgroundService` that drives `RefreshAsync` on the configured
|
||||||
@@ -220,6 +236,11 @@ Security`), but production deployments that use SQL authentication should set
|
|||||||
the override via environment variable rather than committing credentials to
|
the override via environment variable rather than committing credentials to
|
||||||
`appsettings.json`.
|
`appsettings.json`.
|
||||||
|
|
||||||
|
The dashboard parses this connection string and displays only non-secret
|
||||||
|
fields: server, database, integrated security, encrypt, and trust-server-
|
||||||
|
certificate. It never displays user id, password, access token, or arbitrary
|
||||||
|
unparsed connection string text.
|
||||||
|
|
||||||
## Authorization
|
## Authorization
|
||||||
|
|
||||||
All four Galaxy RPCs (including `WatchDeployEvents`) require the
|
All four Galaxy RPCs (including `WatchDeployEvents`) require the
|
||||||
|
|||||||
@@ -35,6 +35,8 @@ paths, timeouts, queue sizes, enum values, or protocol values are invalid.
|
|||||||
"DefaultCommandTimeoutSeconds": 30,
|
"DefaultCommandTimeoutSeconds": 30,
|
||||||
"MaxSessions": 64,
|
"MaxSessions": 64,
|
||||||
"MaxPendingCommandsPerSession": 128,
|
"MaxPendingCommandsPerSession": 128,
|
||||||
|
"DefaultLeaseSeconds": 1800,
|
||||||
|
"LeaseSweepIntervalSeconds": 30,
|
||||||
"AllowMultipleEventSubscribers": false
|
"AllowMultipleEventSubscribers": false
|
||||||
},
|
},
|
||||||
"Events": {
|
"Events": {
|
||||||
@@ -52,7 +54,8 @@ paths, timeouts, queue sizes, enum values, or protocol values are invalid.
|
|||||||
"ShowTagValues": false
|
"ShowTagValues": false
|
||||||
},
|
},
|
||||||
"Protocol": {
|
"Protocol": {
|
||||||
"WorkerProtocolVersion": 1
|
"WorkerProtocolVersion": 1,
|
||||||
|
"MaxGrpcMessageBytes": 16777216
|
||||||
},
|
},
|
||||||
"Galaxy": {
|
"Galaxy": {
|
||||||
"ConnectionString": "Server=localhost;Database=ZB;Integrated Security=True;TrustServerCertificate=True;Encrypt=False;",
|
"ConnectionString": "Server=localhost;Database=ZB;Integrated Security=True;TrustServerCertificate=True;Encrypt=False;",
|
||||||
@@ -107,6 +110,8 @@ to avoid accidental large allocations from malformed or oversized frames.
|
|||||||
| `MxGateway:Sessions:DefaultCommandTimeoutSeconds` | `30` | Default timeout used while the gateway waits for a worker command reply when an open-session request does not provide a positive command timeout. |
|
| `MxGateway:Sessions:DefaultCommandTimeoutSeconds` | `30` | Default timeout used while the gateway waits for a worker command reply when an open-session request does not provide a positive command timeout. |
|
||||||
| `MxGateway:Sessions:MaxSessions` | `64` | Maximum number of concurrently open gateway sessions. Session opens reserve a slot atomically before worker creation. |
|
| `MxGateway:Sessions:MaxSessions` | `64` | Maximum number of concurrently open gateway sessions. Session opens reserve a slot atomically before worker creation. |
|
||||||
| `MxGateway:Sessions:MaxPendingCommandsPerSession` | `128` | Maximum number of pending worker commands for one session. Excess commands fail fast instead of queueing indefinitely. |
|
| `MxGateway:Sessions:MaxPendingCommandsPerSession` | `128` | Maximum number of pending worker commands for one session. Excess commands fail fast instead of queueing indefinitely. |
|
||||||
|
| `MxGateway:Sessions:DefaultLeaseSeconds` | `1800` | Initial session lease and refresh duration. Unary client activity extends the lease by this duration. |
|
||||||
|
| `MxGateway:Sessions:LeaseSweepIntervalSeconds` | `30` | Hosted monitor interval for closing expired leases. Active event-stream subscribers keep a session from expiring while the stream remains attached. |
|
||||||
| `MxGateway:Sessions:AllowMultipleEventSubscribers` | `false` | Controls whether multiple `StreamEvents` subscribers may attach to one session. `true` is rejected until event fan-out is implemented. |
|
| `MxGateway:Sessions:AllowMultipleEventSubscribers` | `false` | Controls whether multiple `StreamEvents` subscribers may attach to one session. `true` is rejected until event fan-out is implemented. |
|
||||||
|
|
||||||
All numeric session options must be greater than zero. The current event stream
|
All numeric session options must be greater than zero. The current event stream
|
||||||
@@ -146,6 +151,7 @@ and `RecentSessionLimit` must be greater than or equal to zero.
|
|||||||
| Option | Default | Description |
|
| Option | Default | Description |
|
||||||
|--------|---------|-------------|
|
|--------|---------|-------------|
|
||||||
| `MxGateway:Protocol:WorkerProtocolVersion` | `1` | Worker IPC protocol version expected by the gateway and worker. This must match `GatewayContractInfo.WorkerProtocolVersion`. |
|
| `MxGateway:Protocol:WorkerProtocolVersion` | `1` | Worker IPC protocol version expected by the gateway and worker. This must match `GatewayContractInfo.WorkerProtocolVersion`. |
|
||||||
|
| `MxGateway:Protocol:MaxGrpcMessageBytes` | `16777216` | Public gRPC max send and receive message size in bytes. The same default is used by official clients. The validator allows values from `1024` through `268435456`. |
|
||||||
|
|
||||||
The protocol option is exposed for diagnostics and explicit deployment
|
The protocol option is exposed for diagnostics and explicit deployment
|
||||||
configuration, not for compatibility negotiation. A mismatch fails validation
|
configuration, not for compatibility negotiation. A mismatch fails validation
|
||||||
|
|||||||
@@ -31,6 +31,11 @@ A second gRPC service, `GalaxyRepositoryGrpcService`, is mapped alongside it. It
|
|||||||
|
|
||||||
`MxAccessGatewayService` derives from the generated `MxAccessGateway.MxAccessGatewayBase` and implements every RPC declared in `mxaccess_gateway.proto`. The proto contract itself is documented in [Contracts](./Contracts.md); this section covers only what the server-side handler does on top of that contract.
|
`MxAccessGatewayService` derives from the generated `MxAccessGateway.MxAccessGatewayBase` and implements every RPC declared in `mxaccess_gateway.proto`. The proto contract itself is documented in [Contracts](./Contracts.md); this section covers only what the server-side handler does on top of that contract.
|
||||||
|
|
||||||
|
Public gRPC send and receive message sizes are configured from
|
||||||
|
`MxGateway:Protocol:MaxGrpcMessageBytes` (default 16 MiB). Official clients use
|
||||||
|
the same default so paged Galaxy browse replies and larger MXAccess payloads
|
||||||
|
fail consistently instead of depending on language-specific gRPC defaults.
|
||||||
|
|
||||||
### `OpenSession`
|
### `OpenSession`
|
||||||
|
|
||||||
`OpenSession` validates the request, asks `ISessionManager` to open a session under the caller's identity, and returns a reply that advertises both protocol versions and the capabilities the gateway supports. Capability strings are static because the gateway has a fixed feature set per build; clients use them as a forward-compatibility hint rather than runtime negotiation.
|
`OpenSession` validates the request, asks `ISessionManager` to open a session under the caller's identity, and returns a reply that advertises both protocol versions and the capabilities the gateway supports. Capability strings are static because the gateway has a fixed feature set per build; clients use them as a forward-compatibility hint rather than runtime negotiation.
|
||||||
|
|||||||
+2
-2
@@ -178,9 +178,9 @@ The order — fault, deregister, dispose, release slot, record metric, log, reth
|
|||||||
|
|
||||||
While `Ready`, callers reach the worker through `SessionManager.InvokeAsync` or `ReadEventsAsync`. Both delegate to `GatewaySession`, which checks the state under lock and updates `LastClientActivityAt` on every invocation. `GatewaySession` also exposes typed bulk helpers (`AddItemBulkAsync`, `SubscribeBulkAsync`, etc.) that wrap `WorkerCommand` round-trips and translate non-`Ok` `ProtocolStatus` replies into `SessionManagerException` with `SessionNotReady`.
|
While `Ready`, callers reach the worker through `SessionManager.InvokeAsync` or `ReadEventsAsync`. Both delegate to `GatewaySession`, which checks the state under lock and updates `LastClientActivityAt` on every invocation. `GatewaySession` also exposes typed bulk helpers (`AddItemBulkAsync`, `SubscribeBulkAsync`, etc.) that wrap `WorkerCommand` round-trips and translate non-`Ok` `ProtocolStatus` replies into `SessionManagerException` with `SessionNotReady`.
|
||||||
|
|
||||||
Event streaming uses `AttachEventSubscriber` which returns a disposable lease. When `allowMultipleSubscribers` is false the second attach throws `EventSubscriberAlreadyActive`; this prevents two gRPC streams from racing on the same worker event channel.
|
Event streaming uses `AttachEventSubscriber` which returns a disposable lease. When `allowMultipleSubscribers` is false the second attach throws `EventSubscriberAlreadyActive`; this prevents two gRPC streams from racing on the same worker event channel. Active event subscribers keep the session lease from expiring until the stream is disposed.
|
||||||
|
|
||||||
`ExtendLease` and `IsLeaseExpired` cooperate with `SessionManager.CloseExpiredLeasesAsync`, which iterates a registry snapshot and closes any session whose lease has expired with `LeaseExpiredReason`.
|
Sessions open with `MxGateway:Sessions:DefaultLeaseSeconds` (default 1800) added to the open timestamp. Unary client activity refreshes the lease by the same duration. `ExtendLease` and `IsLeaseExpired` cooperate with `SessionManager.CloseExpiredLeasesAsync`, which iterates a registry snapshot and closes any session whose lease has expired with `LeaseExpiredReason`. `SessionLeaseMonitorHostedService` runs that sweep every `MxGateway:Sessions:LeaseSweepIntervalSeconds` seconds (default 30).
|
||||||
|
|
||||||
### Close
|
### Close
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ namespace MxGateway.Contracts;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static class GatewayContractInfo
|
public static class GatewayContractInfo
|
||||||
{
|
{
|
||||||
public const uint GatewayProtocolVersion = 1;
|
public const uint GatewayProtocolVersion = 2;
|
||||||
|
|
||||||
public const uint WorkerProtocolVersion = 1;
|
public const uint WorkerProtocolVersion = 1;
|
||||||
|
|
||||||
|
|||||||
@@ -29,41 +29,43 @@ namespace MxGateway.Contracts.Proto.Galaxy {
|
|||||||
"bm5lY3Rpb25SZXF1ZXN0IiEKE1Rlc3RDb25uZWN0aW9uUmVwbHkSCgoCb2sY",
|
"bm5lY3Rpb25SZXF1ZXN0IiEKE1Rlc3RDb25uZWN0aW9uUmVwbHkSCgoCb2sY",
|
||||||
"ASABKAgiGgoYR2V0TGFzdERlcGxveVRpbWVSZXF1ZXN0ImIKFkdldExhc3RE",
|
"ASABKAgiGgoYR2V0TGFzdERlcGxveVRpbWVSZXF1ZXN0ImIKFkdldExhc3RE",
|
||||||
"ZXBsb3lUaW1lUmVwbHkSDwoHcHJlc2VudBgBIAEoCBI3ChN0aW1lX29mX2xh",
|
"ZXBsb3lUaW1lUmVwbHkSDwoHcHJlc2VudBgBIAEoCBI3ChN0aW1lX29mX2xh",
|
||||||
"c3RfZGVwbG95GAIgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcCIa",
|
"c3RfZGVwbG95GAIgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcCJB",
|
||||||
"ChhEaXNjb3ZlckhpZXJhcmNoeVJlcXVlc3QiTQoWRGlzY292ZXJIaWVyYXJj",
|
"ChhEaXNjb3ZlckhpZXJhcmNoeVJlcXVlc3QSEQoJcGFnZV9zaXplGAEgASgF",
|
||||||
"aHlSZXBseRIzCgdvYmplY3RzGAEgAygLMiIuZ2FsYXh5X3JlcG9zaXRvcnku",
|
"EhIKCnBhZ2VfdG9rZW4YAiABKAkiggEKFkRpc2NvdmVySGllcmFyY2h5UmVw",
|
||||||
"djEuR2FsYXh5T2JqZWN0IlUKGFdhdGNoRGVwbG95RXZlbnRzUmVxdWVzdBI5",
|
"bHkSMwoHb2JqZWN0cxgBIAMoCzIiLmdhbGF4eV9yZXBvc2l0b3J5LnYxLkdh",
|
||||||
"ChVsYXN0X3NlZW5fZGVwbG95X3RpbWUYASABKAsyGi5nb29nbGUucHJvdG9i",
|
"bGF4eU9iamVjdBIXCg9uZXh0X3BhZ2VfdG9rZW4YAiABKAkSGgoSdG90YWxf",
|
||||||
"dWYuVGltZXN0YW1wIt0BCgtEZXBsb3lFdmVudBIQCghzZXF1ZW5jZRgBIAEo",
|
"b2JqZWN0X2NvdW50GAMgASgFIlUKGFdhdGNoRGVwbG95RXZlbnRzUmVxdWVz",
|
||||||
"BBIvCgtvYnNlcnZlZF9hdBgCIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1l",
|
"dBI5ChVsYXN0X3NlZW5fZGVwbG95X3RpbWUYASABKAsyGi5nb29nbGUucHJv",
|
||||||
"c3RhbXASNwoTdGltZV9vZl9sYXN0X2RlcGxveRgDIAEoCzIaLmdvb2dsZS5w",
|
"dG9idWYuVGltZXN0YW1wIt0BCgtEZXBsb3lFdmVudBIQCghzZXF1ZW5jZRgB",
|
||||||
"cm90b2J1Zi5UaW1lc3RhbXASIwobdGltZV9vZl9sYXN0X2RlcGxveV9wcmVz",
|
"IAEoBBIvCgtvYnNlcnZlZF9hdBgCIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5U",
|
||||||
"ZW50GAQgASgIEhQKDG9iamVjdF9jb3VudBgFIAEoBRIXCg9hdHRyaWJ1dGVf",
|
"aW1lc3RhbXASNwoTdGltZV9vZl9sYXN0X2RlcGxveRgDIAEoCzIaLmdvb2ds",
|
||||||
"Y291bnQYBiABKAUikwIKDEdhbGF4eU9iamVjdBISCgpnb2JqZWN0X2lkGAEg",
|
"ZS5wcm90b2J1Zi5UaW1lc3RhbXASIwobdGltZV9vZl9sYXN0X2RlcGxveV9w",
|
||||||
"ASgFEhAKCHRhZ19uYW1lGAIgASgJEhYKDmNvbnRhaW5lZF9uYW1lGAMgASgJ",
|
"cmVzZW50GAQgASgIEhQKDG9iamVjdF9jb3VudBgFIAEoBRIXCg9hdHRyaWJ1",
|
||||||
"EhMKC2Jyb3dzZV9uYW1lGAQgASgJEhkKEXBhcmVudF9nb2JqZWN0X2lkGAUg",
|
"dGVfY291bnQYBiABKAUikwIKDEdhbGF4eU9iamVjdBISCgpnb2JqZWN0X2lk",
|
||||||
"ASgFEg8KB2lzX2FyZWEYBiABKAgSEwoLY2F0ZWdvcnlfaWQYByABKAUSHAoU",
|
"GAEgASgFEhAKCHRhZ19uYW1lGAIgASgJEhYKDmNvbnRhaW5lZF9uYW1lGAMg",
|
||||||
"aG9zdGVkX2J5X2dvYmplY3RfaWQYCCABKAUSFgoOdGVtcGxhdGVfY2hhaW4Y",
|
"ASgJEhMKC2Jyb3dzZV9uYW1lGAQgASgJEhkKEXBhcmVudF9nb2JqZWN0X2lk",
|
||||||
"CSADKAkSOQoKYXR0cmlidXRlcxgKIAMoCzIlLmdhbGF4eV9yZXBvc2l0b3J5",
|
"GAUgASgFEg8KB2lzX2FyZWEYBiABKAgSEwoLY2F0ZWdvcnlfaWQYByABKAUS",
|
||||||
"LnYxLkdhbGF4eUF0dHJpYnV0ZSKoAgoPR2FsYXh5QXR0cmlidXRlEhYKDmF0",
|
"HAoUaG9zdGVkX2J5X2dvYmplY3RfaWQYCCABKAUSFgoOdGVtcGxhdGVfY2hh",
|
||||||
"dHJpYnV0ZV9uYW1lGAEgASgJEhoKEmZ1bGxfdGFnX3JlZmVyZW5jZRgCIAEo",
|
"aW4YCSADKAkSOQoKYXR0cmlidXRlcxgKIAMoCzIlLmdhbGF4eV9yZXBvc2l0",
|
||||||
"CRIUCgxteF9kYXRhX3R5cGUYAyABKAUSFgoOZGF0YV90eXBlX25hbWUYBCAB",
|
"b3J5LnYxLkdhbGF4eUF0dHJpYnV0ZSKoAgoPR2FsYXh5QXR0cmlidXRlEhYK",
|
||||||
"KAkSEAoIaXNfYXJyYXkYBSABKAgSFwoPYXJyYXlfZGltZW5zaW9uGAYgASgF",
|
"DmF0dHJpYnV0ZV9uYW1lGAEgASgJEhoKEmZ1bGxfdGFnX3JlZmVyZW5jZRgC",
|
||||||
"Eh8KF2FycmF5X2RpbWVuc2lvbl9wcmVzZW50GAcgASgIEh0KFW14X2F0dHJp",
|
"IAEoCRIUCgxteF9kYXRhX3R5cGUYAyABKAUSFgoOZGF0YV90eXBlX25hbWUY",
|
||||||
"YnV0ZV9jYXRlZ29yeRgIIAEoBRIfChdzZWN1cml0eV9jbGFzc2lmaWNhdGlv",
|
"BCABKAkSEAoIaXNfYXJyYXkYBSABKAgSFwoPYXJyYXlfZGltZW5zaW9uGAYg",
|
||||||
"bhgJIAEoBRIVCg1pc19oaXN0b3JpemVkGAogASgIEhAKCGlzX2FsYXJtGAsg",
|
"ASgFEh8KF2FycmF5X2RpbWVuc2lvbl9wcmVzZW50GAcgASgIEh0KFW14X2F0",
|
||||||
"ASgIMswDChBHYWxheHlSZXBvc2l0b3J5EmgKDlRlc3RDb25uZWN0aW9uEisu",
|
"dHJpYnV0ZV9jYXRlZ29yeRgIIAEoBRIfChdzZWN1cml0eV9jbGFzc2lmaWNh",
|
||||||
"Z2FsYXh5X3JlcG9zaXRvcnkudjEuVGVzdENvbm5lY3Rpb25SZXF1ZXN0Giku",
|
"dGlvbhgJIAEoBRIVCg1pc19oaXN0b3JpemVkGAogASgIEhAKCGlzX2FsYXJt",
|
||||||
"Z2FsYXh5X3JlcG9zaXRvcnkudjEuVGVzdENvbm5lY3Rpb25SZXBseRJxChFH",
|
"GAsgASgIMswDChBHYWxheHlSZXBvc2l0b3J5EmgKDlRlc3RDb25uZWN0aW9u",
|
||||||
"ZXRMYXN0RGVwbG95VGltZRIuLmdhbGF4eV9yZXBvc2l0b3J5LnYxLkdldExh",
|
"EisuZ2FsYXh5X3JlcG9zaXRvcnkudjEuVGVzdENvbm5lY3Rpb25SZXF1ZXN0",
|
||||||
"c3REZXBsb3lUaW1lUmVxdWVzdBosLmdhbGF4eV9yZXBvc2l0b3J5LnYxLkdl",
|
"GikuZ2FsYXh5X3JlcG9zaXRvcnkudjEuVGVzdENvbm5lY3Rpb25SZXBseRJx",
|
||||||
"dExhc3REZXBsb3lUaW1lUmVwbHkScQoRRGlzY292ZXJIaWVyYXJjaHkSLi5n",
|
"ChFHZXRMYXN0RGVwbG95VGltZRIuLmdhbGF4eV9yZXBvc2l0b3J5LnYxLkdl",
|
||||||
"YWxheHlfcmVwb3NpdG9yeS52MS5EaXNjb3ZlckhpZXJhcmNoeVJlcXVlc3Qa",
|
"dExhc3REZXBsb3lUaW1lUmVxdWVzdBosLmdhbGF4eV9yZXBvc2l0b3J5LnYx",
|
||||||
"LC5nYWxheHlfcmVwb3NpdG9yeS52MS5EaXNjb3ZlckhpZXJhcmNoeVJlcGx5",
|
"LkdldExhc3REZXBsb3lUaW1lUmVwbHkScQoRRGlzY292ZXJIaWVyYXJjaHkS",
|
||||||
"EmgKEVdhdGNoRGVwbG95RXZlbnRzEi4uZ2FsYXh5X3JlcG9zaXRvcnkudjEu",
|
"Li5nYWxheHlfcmVwb3NpdG9yeS52MS5EaXNjb3ZlckhpZXJhcmNoeVJlcXVl",
|
||||||
"V2F0Y2hEZXBsb3lFdmVudHNSZXF1ZXN0GiEuZ2FsYXh5X3JlcG9zaXRvcnku",
|
"c3QaLC5nYWxheHlfcmVwb3NpdG9yeS52MS5EaXNjb3ZlckhpZXJhcmNoeVJl",
|
||||||
"djEuRGVwbG95RXZlbnQwAUIjqgIgTXhHYXRld2F5LkNvbnRyYWN0cy5Qcm90",
|
"cGx5EmgKEVdhdGNoRGVwbG95RXZlbnRzEi4uZ2FsYXh5X3JlcG9zaXRvcnku",
|
||||||
"by5HYWxheHliBnByb3RvMw=="));
|
"djEuV2F0Y2hEZXBsb3lFdmVudHNSZXF1ZXN0GiEuZ2FsYXh5X3JlcG9zaXRv",
|
||||||
|
"cnkudjEuRGVwbG95RXZlbnQwAUIjqgIgTXhHYXRld2F5LkNvbnRyYWN0cy5Q",
|
||||||
|
"cm90by5HYWxheHliBnByb3RvMw=="));
|
||||||
descriptor = pbr::FileDescriptor.FromGeneratedCode(descriptorData,
|
descriptor = pbr::FileDescriptor.FromGeneratedCode(descriptorData,
|
||||||
new pbr::FileDescriptor[] { global::Google.Protobuf.WellKnownTypes.TimestampReflection.Descriptor, },
|
new pbr::FileDescriptor[] { global::Google.Protobuf.WellKnownTypes.TimestampReflection.Descriptor, },
|
||||||
new pbr::GeneratedClrTypeInfo(null, null, new pbr::GeneratedClrTypeInfo[] {
|
new pbr::GeneratedClrTypeInfo(null, null, new pbr::GeneratedClrTypeInfo[] {
|
||||||
@@ -71,8 +73,8 @@ namespace MxGateway.Contracts.Proto.Galaxy {
|
|||||||
new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.Galaxy.TestConnectionReply), global::MxGateway.Contracts.Proto.Galaxy.TestConnectionReply.Parser, new[]{ "Ok" }, null, null, null, null),
|
new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.Galaxy.TestConnectionReply), global::MxGateway.Contracts.Proto.Galaxy.TestConnectionReply.Parser, new[]{ "Ok" }, null, null, null, null),
|
||||||
new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.Galaxy.GetLastDeployTimeRequest), global::MxGateway.Contracts.Proto.Galaxy.GetLastDeployTimeRequest.Parser, null, null, null, null, null),
|
new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.Galaxy.GetLastDeployTimeRequest), global::MxGateway.Contracts.Proto.Galaxy.GetLastDeployTimeRequest.Parser, null, null, null, null, null),
|
||||||
new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.Galaxy.GetLastDeployTimeReply), global::MxGateway.Contracts.Proto.Galaxy.GetLastDeployTimeReply.Parser, new[]{ "Present", "TimeOfLastDeploy" }, null, null, null, null),
|
new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.Galaxy.GetLastDeployTimeReply), global::MxGateway.Contracts.Proto.Galaxy.GetLastDeployTimeReply.Parser, new[]{ "Present", "TimeOfLastDeploy" }, null, null, null, null),
|
||||||
new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.Galaxy.DiscoverHierarchyRequest), global::MxGateway.Contracts.Proto.Galaxy.DiscoverHierarchyRequest.Parser, null, null, null, null, null),
|
new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.Galaxy.DiscoverHierarchyRequest), global::MxGateway.Contracts.Proto.Galaxy.DiscoverHierarchyRequest.Parser, new[]{ "PageSize", "PageToken" }, null, null, null, null),
|
||||||
new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.Galaxy.DiscoverHierarchyReply), global::MxGateway.Contracts.Proto.Galaxy.DiscoverHierarchyReply.Parser, new[]{ "Objects" }, null, null, null, null),
|
new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.Galaxy.DiscoverHierarchyReply), global::MxGateway.Contracts.Proto.Galaxy.DiscoverHierarchyReply.Parser, new[]{ "Objects", "NextPageToken", "TotalObjectCount" }, null, null, null, null),
|
||||||
new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.Galaxy.WatchDeployEventsRequest), global::MxGateway.Contracts.Proto.Galaxy.WatchDeployEventsRequest.Parser, new[]{ "LastSeenDeployTime" }, null, null, null, null),
|
new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.Galaxy.WatchDeployEventsRequest), global::MxGateway.Contracts.Proto.Galaxy.WatchDeployEventsRequest.Parser, new[]{ "LastSeenDeployTime" }, null, null, null, null),
|
||||||
new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.Galaxy.DeployEvent), global::MxGateway.Contracts.Proto.Galaxy.DeployEvent.Parser, new[]{ "Sequence", "ObservedAt", "TimeOfLastDeploy", "TimeOfLastDeployPresent", "ObjectCount", "AttributeCount" }, null, null, null, null),
|
new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.Galaxy.DeployEvent), global::MxGateway.Contracts.Proto.Galaxy.DeployEvent.Parser, new[]{ "Sequence", "ObservedAt", "TimeOfLastDeploy", "TimeOfLastDeployPresent", "ObjectCount", "AttributeCount" }, null, null, null, null),
|
||||||
new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.Galaxy.GalaxyObject), global::MxGateway.Contracts.Proto.Galaxy.GalaxyObject.Parser, new[]{ "GobjectId", "TagName", "ContainedName", "BrowseName", "ParentGobjectId", "IsArea", "CategoryId", "HostedByGobjectId", "TemplateChain", "Attributes" }, null, null, null, null),
|
new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.Galaxy.GalaxyObject), global::MxGateway.Contracts.Proto.Galaxy.GalaxyObject.Parser, new[]{ "GobjectId", "TagName", "ContainedName", "BrowseName", "ParentGobjectId", "IsArea", "CategoryId", "HostedByGobjectId", "TemplateChain", "Attributes" }, null, null, null, null),
|
||||||
@@ -882,6 +884,8 @@ namespace MxGateway.Contracts.Proto.Galaxy {
|
|||||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
|
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
|
||||||
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
|
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
|
||||||
public DiscoverHierarchyRequest(DiscoverHierarchyRequest other) : this() {
|
public DiscoverHierarchyRequest(DiscoverHierarchyRequest other) : this() {
|
||||||
|
pageSize_ = other.pageSize_;
|
||||||
|
pageToken_ = other.pageToken_;
|
||||||
_unknownFields = pb::UnknownFieldSet.Clone(other._unknownFields);
|
_unknownFields = pb::UnknownFieldSet.Clone(other._unknownFields);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -891,6 +895,37 @@ namespace MxGateway.Contracts.Proto.Galaxy {
|
|||||||
return new DiscoverHierarchyRequest(this);
|
return new DiscoverHierarchyRequest(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Field number for the "page_size" field.</summary>
|
||||||
|
public const int PageSizeFieldNumber = 1;
|
||||||
|
private int pageSize_;
|
||||||
|
/// <summary>
|
||||||
|
/// Maximum number of objects to return. The server applies its default when
|
||||||
|
/// unset and rejects non-positive values.
|
||||||
|
/// </summary>
|
||||||
|
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
|
||||||
|
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
|
||||||
|
public int PageSize {
|
||||||
|
get { return pageSize_; }
|
||||||
|
set {
|
||||||
|
pageSize_ = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Field number for the "page_token" field.</summary>
|
||||||
|
public const int PageTokenFieldNumber = 2;
|
||||||
|
private string pageToken_ = "";
|
||||||
|
/// <summary>
|
||||||
|
/// Opaque token returned by a previous DiscoverHierarchy response.
|
||||||
|
/// </summary>
|
||||||
|
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
|
||||||
|
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
|
||||||
|
public string PageToken {
|
||||||
|
get { return pageToken_; }
|
||||||
|
set {
|
||||||
|
pageToken_ = pb::ProtoPreconditions.CheckNotNull(value, "value");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
|
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
|
||||||
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
|
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
|
||||||
public override bool Equals(object other) {
|
public override bool Equals(object other) {
|
||||||
@@ -906,6 +941,8 @@ namespace MxGateway.Contracts.Proto.Galaxy {
|
|||||||
if (ReferenceEquals(other, this)) {
|
if (ReferenceEquals(other, this)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
if (PageSize != other.PageSize) return false;
|
||||||
|
if (PageToken != other.PageToken) return false;
|
||||||
return Equals(_unknownFields, other._unknownFields);
|
return Equals(_unknownFields, other._unknownFields);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -913,6 +950,8 @@ namespace MxGateway.Contracts.Proto.Galaxy {
|
|||||||
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
|
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
|
||||||
public override int GetHashCode() {
|
public override int GetHashCode() {
|
||||||
int hash = 1;
|
int hash = 1;
|
||||||
|
if (PageSize != 0) hash ^= PageSize.GetHashCode();
|
||||||
|
if (PageToken.Length != 0) hash ^= PageToken.GetHashCode();
|
||||||
if (_unknownFields != null) {
|
if (_unknownFields != null) {
|
||||||
hash ^= _unknownFields.GetHashCode();
|
hash ^= _unknownFields.GetHashCode();
|
||||||
}
|
}
|
||||||
@@ -931,6 +970,14 @@ namespace MxGateway.Contracts.Proto.Galaxy {
|
|||||||
#if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE
|
#if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE
|
||||||
output.WriteRawMessage(this);
|
output.WriteRawMessage(this);
|
||||||
#else
|
#else
|
||||||
|
if (PageSize != 0) {
|
||||||
|
output.WriteRawTag(8);
|
||||||
|
output.WriteInt32(PageSize);
|
||||||
|
}
|
||||||
|
if (PageToken.Length != 0) {
|
||||||
|
output.WriteRawTag(18);
|
||||||
|
output.WriteString(PageToken);
|
||||||
|
}
|
||||||
if (_unknownFields != null) {
|
if (_unknownFields != null) {
|
||||||
_unknownFields.WriteTo(output);
|
_unknownFields.WriteTo(output);
|
||||||
}
|
}
|
||||||
@@ -941,6 +988,14 @@ namespace MxGateway.Contracts.Proto.Galaxy {
|
|||||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
|
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
|
||||||
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
|
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
|
||||||
void pb::IBufferMessage.InternalWriteTo(ref pb::WriteContext output) {
|
void pb::IBufferMessage.InternalWriteTo(ref pb::WriteContext output) {
|
||||||
|
if (PageSize != 0) {
|
||||||
|
output.WriteRawTag(8);
|
||||||
|
output.WriteInt32(PageSize);
|
||||||
|
}
|
||||||
|
if (PageToken.Length != 0) {
|
||||||
|
output.WriteRawTag(18);
|
||||||
|
output.WriteString(PageToken);
|
||||||
|
}
|
||||||
if (_unknownFields != null) {
|
if (_unknownFields != null) {
|
||||||
_unknownFields.WriteTo(ref output);
|
_unknownFields.WriteTo(ref output);
|
||||||
}
|
}
|
||||||
@@ -951,6 +1006,12 @@ namespace MxGateway.Contracts.Proto.Galaxy {
|
|||||||
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
|
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
|
||||||
public int CalculateSize() {
|
public int CalculateSize() {
|
||||||
int size = 0;
|
int size = 0;
|
||||||
|
if (PageSize != 0) {
|
||||||
|
size += 1 + pb::CodedOutputStream.ComputeInt32Size(PageSize);
|
||||||
|
}
|
||||||
|
if (PageToken.Length != 0) {
|
||||||
|
size += 1 + pb::CodedOutputStream.ComputeStringSize(PageToken);
|
||||||
|
}
|
||||||
if (_unknownFields != null) {
|
if (_unknownFields != null) {
|
||||||
size += _unknownFields.CalculateSize();
|
size += _unknownFields.CalculateSize();
|
||||||
}
|
}
|
||||||
@@ -963,6 +1024,12 @@ namespace MxGateway.Contracts.Proto.Galaxy {
|
|||||||
if (other == null) {
|
if (other == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (other.PageSize != 0) {
|
||||||
|
PageSize = other.PageSize;
|
||||||
|
}
|
||||||
|
if (other.PageToken.Length != 0) {
|
||||||
|
PageToken = other.PageToken;
|
||||||
|
}
|
||||||
_unknownFields = pb::UnknownFieldSet.MergeFrom(_unknownFields, other._unknownFields);
|
_unknownFields = pb::UnknownFieldSet.MergeFrom(_unknownFields, other._unknownFields);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -982,6 +1049,14 @@ namespace MxGateway.Contracts.Proto.Galaxy {
|
|||||||
default:
|
default:
|
||||||
_unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, input);
|
_unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, input);
|
||||||
break;
|
break;
|
||||||
|
case 8: {
|
||||||
|
PageSize = input.ReadInt32();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 18: {
|
||||||
|
PageToken = input.ReadString();
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
@@ -1001,6 +1076,14 @@ namespace MxGateway.Contracts.Proto.Galaxy {
|
|||||||
default:
|
default:
|
||||||
_unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, ref input);
|
_unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, ref input);
|
||||||
break;
|
break;
|
||||||
|
case 8: {
|
||||||
|
PageSize = input.ReadInt32();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 18: {
|
||||||
|
PageToken = input.ReadString();
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1044,6 +1127,8 @@ namespace MxGateway.Contracts.Proto.Galaxy {
|
|||||||
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
|
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
|
||||||
public DiscoverHierarchyReply(DiscoverHierarchyReply other) : this() {
|
public DiscoverHierarchyReply(DiscoverHierarchyReply other) : this() {
|
||||||
objects_ = other.objects_.Clone();
|
objects_ = other.objects_.Clone();
|
||||||
|
nextPageToken_ = other.nextPageToken_;
|
||||||
|
totalObjectCount_ = other.totalObjectCount_;
|
||||||
_unknownFields = pb::UnknownFieldSet.Clone(other._unknownFields);
|
_unknownFields = pb::UnknownFieldSet.Clone(other._unknownFields);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1064,6 +1149,36 @@ namespace MxGateway.Contracts.Proto.Galaxy {
|
|||||||
get { return objects_; }
|
get { return objects_; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Field number for the "next_page_token" field.</summary>
|
||||||
|
public const int NextPageTokenFieldNumber = 2;
|
||||||
|
private string nextPageToken_ = "";
|
||||||
|
/// <summary>
|
||||||
|
/// Non-empty when another page is available.
|
||||||
|
/// </summary>
|
||||||
|
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
|
||||||
|
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
|
||||||
|
public string NextPageToken {
|
||||||
|
get { return nextPageToken_; }
|
||||||
|
set {
|
||||||
|
nextPageToken_ = pb::ProtoPreconditions.CheckNotNull(value, "value");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Field number for the "total_object_count" field.</summary>
|
||||||
|
public const int TotalObjectCountFieldNumber = 3;
|
||||||
|
private int totalObjectCount_;
|
||||||
|
/// <summary>
|
||||||
|
/// Total number of objects in the cached hierarchy at the time of the call.
|
||||||
|
/// </summary>
|
||||||
|
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
|
||||||
|
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
|
||||||
|
public int TotalObjectCount {
|
||||||
|
get { return totalObjectCount_; }
|
||||||
|
set {
|
||||||
|
totalObjectCount_ = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
|
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
|
||||||
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
|
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
|
||||||
public override bool Equals(object other) {
|
public override bool Equals(object other) {
|
||||||
@@ -1080,6 +1195,8 @@ namespace MxGateway.Contracts.Proto.Galaxy {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if(!objects_.Equals(other.objects_)) return false;
|
if(!objects_.Equals(other.objects_)) return false;
|
||||||
|
if (NextPageToken != other.NextPageToken) return false;
|
||||||
|
if (TotalObjectCount != other.TotalObjectCount) return false;
|
||||||
return Equals(_unknownFields, other._unknownFields);
|
return Equals(_unknownFields, other._unknownFields);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1088,6 +1205,8 @@ namespace MxGateway.Contracts.Proto.Galaxy {
|
|||||||
public override int GetHashCode() {
|
public override int GetHashCode() {
|
||||||
int hash = 1;
|
int hash = 1;
|
||||||
hash ^= objects_.GetHashCode();
|
hash ^= objects_.GetHashCode();
|
||||||
|
if (NextPageToken.Length != 0) hash ^= NextPageToken.GetHashCode();
|
||||||
|
if (TotalObjectCount != 0) hash ^= TotalObjectCount.GetHashCode();
|
||||||
if (_unknownFields != null) {
|
if (_unknownFields != null) {
|
||||||
hash ^= _unknownFields.GetHashCode();
|
hash ^= _unknownFields.GetHashCode();
|
||||||
}
|
}
|
||||||
@@ -1107,6 +1226,14 @@ namespace MxGateway.Contracts.Proto.Galaxy {
|
|||||||
output.WriteRawMessage(this);
|
output.WriteRawMessage(this);
|
||||||
#else
|
#else
|
||||||
objects_.WriteTo(output, _repeated_objects_codec);
|
objects_.WriteTo(output, _repeated_objects_codec);
|
||||||
|
if (NextPageToken.Length != 0) {
|
||||||
|
output.WriteRawTag(18);
|
||||||
|
output.WriteString(NextPageToken);
|
||||||
|
}
|
||||||
|
if (TotalObjectCount != 0) {
|
||||||
|
output.WriteRawTag(24);
|
||||||
|
output.WriteInt32(TotalObjectCount);
|
||||||
|
}
|
||||||
if (_unknownFields != null) {
|
if (_unknownFields != null) {
|
||||||
_unknownFields.WriteTo(output);
|
_unknownFields.WriteTo(output);
|
||||||
}
|
}
|
||||||
@@ -1118,6 +1245,14 @@ namespace MxGateway.Contracts.Proto.Galaxy {
|
|||||||
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
|
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
|
||||||
void pb::IBufferMessage.InternalWriteTo(ref pb::WriteContext output) {
|
void pb::IBufferMessage.InternalWriteTo(ref pb::WriteContext output) {
|
||||||
objects_.WriteTo(ref output, _repeated_objects_codec);
|
objects_.WriteTo(ref output, _repeated_objects_codec);
|
||||||
|
if (NextPageToken.Length != 0) {
|
||||||
|
output.WriteRawTag(18);
|
||||||
|
output.WriteString(NextPageToken);
|
||||||
|
}
|
||||||
|
if (TotalObjectCount != 0) {
|
||||||
|
output.WriteRawTag(24);
|
||||||
|
output.WriteInt32(TotalObjectCount);
|
||||||
|
}
|
||||||
if (_unknownFields != null) {
|
if (_unknownFields != null) {
|
||||||
_unknownFields.WriteTo(ref output);
|
_unknownFields.WriteTo(ref output);
|
||||||
}
|
}
|
||||||
@@ -1129,6 +1264,12 @@ namespace MxGateway.Contracts.Proto.Galaxy {
|
|||||||
public int CalculateSize() {
|
public int CalculateSize() {
|
||||||
int size = 0;
|
int size = 0;
|
||||||
size += objects_.CalculateSize(_repeated_objects_codec);
|
size += objects_.CalculateSize(_repeated_objects_codec);
|
||||||
|
if (NextPageToken.Length != 0) {
|
||||||
|
size += 1 + pb::CodedOutputStream.ComputeStringSize(NextPageToken);
|
||||||
|
}
|
||||||
|
if (TotalObjectCount != 0) {
|
||||||
|
size += 1 + pb::CodedOutputStream.ComputeInt32Size(TotalObjectCount);
|
||||||
|
}
|
||||||
if (_unknownFields != null) {
|
if (_unknownFields != null) {
|
||||||
size += _unknownFields.CalculateSize();
|
size += _unknownFields.CalculateSize();
|
||||||
}
|
}
|
||||||
@@ -1142,6 +1283,12 @@ namespace MxGateway.Contracts.Proto.Galaxy {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
objects_.Add(other.objects_);
|
objects_.Add(other.objects_);
|
||||||
|
if (other.NextPageToken.Length != 0) {
|
||||||
|
NextPageToken = other.NextPageToken;
|
||||||
|
}
|
||||||
|
if (other.TotalObjectCount != 0) {
|
||||||
|
TotalObjectCount = other.TotalObjectCount;
|
||||||
|
}
|
||||||
_unknownFields = pb::UnknownFieldSet.MergeFrom(_unknownFields, other._unknownFields);
|
_unknownFields = pb::UnknownFieldSet.MergeFrom(_unknownFields, other._unknownFields);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1165,6 +1312,14 @@ namespace MxGateway.Contracts.Proto.Galaxy {
|
|||||||
objects_.AddEntriesFrom(input, _repeated_objects_codec);
|
objects_.AddEntriesFrom(input, _repeated_objects_codec);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case 18: {
|
||||||
|
NextPageToken = input.ReadString();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 24: {
|
||||||
|
TotalObjectCount = input.ReadInt32();
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
@@ -1188,6 +1343,14 @@ namespace MxGateway.Contracts.Proto.Galaxy {
|
|||||||
objects_.AddEntriesFrom(ref input, _repeated_objects_codec);
|
objects_.AddEntriesFrom(ref input, _repeated_objects_codec);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case 18: {
|
||||||
|
NextPageToken = input.ReadString();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 24: {
|
||||||
|
TotalObjectCount = input.ReadInt32();
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,10 +37,20 @@ message GetLastDeployTimeReply {
|
|||||||
google.protobuf.Timestamp time_of_last_deploy = 2;
|
google.protobuf.Timestamp time_of_last_deploy = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
message DiscoverHierarchyRequest {}
|
message DiscoverHierarchyRequest {
|
||||||
|
// Maximum number of objects to return. The server applies its default when
|
||||||
|
// unset and rejects non-positive values.
|
||||||
|
int32 page_size = 1;
|
||||||
|
// Opaque token returned by a previous DiscoverHierarchy response.
|
||||||
|
string page_token = 2;
|
||||||
|
}
|
||||||
|
|
||||||
message DiscoverHierarchyReply {
|
message DiscoverHierarchyReply {
|
||||||
repeated GalaxyObject objects = 1;
|
repeated GalaxyObject objects = 1;
|
||||||
|
// Non-empty when another page is available.
|
||||||
|
string next_page_token = 2;
|
||||||
|
// Total number of objects in the cached hierarchy at the time of the call.
|
||||||
|
int32 total_object_count = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
message WatchDeployEventsRequest {
|
message WatchDeployEventsRequest {
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
namespace MxGateway.Server.Configuration;
|
namespace MxGateway.Server.Configuration;
|
||||||
|
|
||||||
public sealed record EffectiveProtocolConfiguration(uint WorkerProtocolVersion);
|
public sealed record EffectiveProtocolConfiguration(
|
||||||
|
uint WorkerProtocolVersion,
|
||||||
|
int MaxGrpcMessageBytes);
|
||||||
|
|||||||
@@ -3,4 +3,7 @@ namespace MxGateway.Server.Configuration;
|
|||||||
public sealed record EffectiveSessionConfiguration(
|
public sealed record EffectiveSessionConfiguration(
|
||||||
int DefaultCommandTimeoutSeconds,
|
int DefaultCommandTimeoutSeconds,
|
||||||
int MaxSessions,
|
int MaxSessions,
|
||||||
|
int MaxPendingCommandsPerSession,
|
||||||
|
int DefaultLeaseSeconds,
|
||||||
|
int LeaseSweepIntervalSeconds,
|
||||||
bool AllowMultipleEventSubscribers);
|
bool AllowMultipleEventSubscribers);
|
||||||
|
|||||||
@@ -28,6 +28,9 @@ public sealed class GatewayConfigurationProvider(IOptions<GatewayOptions> option
|
|||||||
Sessions: new EffectiveSessionConfiguration(
|
Sessions: new EffectiveSessionConfiguration(
|
||||||
DefaultCommandTimeoutSeconds: value.Sessions.DefaultCommandTimeoutSeconds,
|
DefaultCommandTimeoutSeconds: value.Sessions.DefaultCommandTimeoutSeconds,
|
||||||
MaxSessions: value.Sessions.MaxSessions,
|
MaxSessions: value.Sessions.MaxSessions,
|
||||||
|
MaxPendingCommandsPerSession: value.Sessions.MaxPendingCommandsPerSession,
|
||||||
|
DefaultLeaseSeconds: value.Sessions.DefaultLeaseSeconds,
|
||||||
|
LeaseSweepIntervalSeconds: value.Sessions.LeaseSweepIntervalSeconds,
|
||||||
AllowMultipleEventSubscribers: value.Sessions.AllowMultipleEventSubscribers),
|
AllowMultipleEventSubscribers: value.Sessions.AllowMultipleEventSubscribers),
|
||||||
Events: new EffectiveEventConfiguration(
|
Events: new EffectiveEventConfiguration(
|
||||||
QueueCapacity: value.Events.QueueCapacity,
|
QueueCapacity: value.Events.QueueCapacity,
|
||||||
@@ -41,6 +44,8 @@ public sealed class GatewayConfigurationProvider(IOptions<GatewayOptions> option
|
|||||||
RecentFaultLimit: value.Dashboard.RecentFaultLimit,
|
RecentFaultLimit: value.Dashboard.RecentFaultLimit,
|
||||||
RecentSessionLimit: value.Dashboard.RecentSessionLimit,
|
RecentSessionLimit: value.Dashboard.RecentSessionLimit,
|
||||||
ShowTagValues: value.Dashboard.ShowTagValues),
|
ShowTagValues: value.Dashboard.ShowTagValues),
|
||||||
Protocol: new EffectiveProtocolConfiguration(value.Protocol.WorkerProtocolVersion));
|
Protocol: new EffectiveProtocolConfiguration(
|
||||||
|
value.Protocol.WorkerProtocolVersion,
|
||||||
|
value.Protocol.MaxGrpcMessageBytes));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -129,6 +129,14 @@ public sealed class GatewayOptionsValidator : IValidateOptions<GatewayOptions>
|
|||||||
options.MaxPendingCommandsPerSession,
|
options.MaxPendingCommandsPerSession,
|
||||||
"MxGateway:Sessions:MaxPendingCommandsPerSession must be greater than zero.",
|
"MxGateway:Sessions:MaxPendingCommandsPerSession must be greater than zero.",
|
||||||
failures);
|
failures);
|
||||||
|
AddIfNotPositive(
|
||||||
|
options.DefaultLeaseSeconds,
|
||||||
|
"MxGateway:Sessions:DefaultLeaseSeconds must be greater than zero.",
|
||||||
|
failures);
|
||||||
|
AddIfNotPositive(
|
||||||
|
options.LeaseSweepIntervalSeconds,
|
||||||
|
"MxGateway:Sessions:LeaseSweepIntervalSeconds must be greater than zero.",
|
||||||
|
failures);
|
||||||
|
|
||||||
if (options.AllowMultipleEventSubscribers)
|
if (options.AllowMultipleEventSubscribers)
|
||||||
{
|
{
|
||||||
@@ -179,6 +187,12 @@ public sealed class GatewayOptionsValidator : IValidateOptions<GatewayOptions>
|
|||||||
failures.Add(
|
failures.Add(
|
||||||
$"MxGateway:Protocol:WorkerProtocolVersion must be {GatewayContractInfo.WorkerProtocolVersion}.");
|
$"MxGateway:Protocol:WorkerProtocolVersion must be {GatewayContractInfo.WorkerProtocolVersion}.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (options.MaxGrpcMessageBytes is < MinimumMaxMessageBytes or > MaximumMaxMessageBytes)
|
||||||
|
{
|
||||||
|
failures.Add(
|
||||||
|
$"MxGateway:Protocol:MaxGrpcMessageBytes must be between {MinimumMaxMessageBytes} and {MaximumMaxMessageBytes}.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void AddIfBlank(string? value, string message, List<string> failures)
|
private static void AddIfBlank(string? value, string message, List<string> failures)
|
||||||
|
|||||||
@@ -5,4 +5,6 @@ namespace MxGateway.Server.Configuration;
|
|||||||
public sealed class ProtocolOptions
|
public sealed class ProtocolOptions
|
||||||
{
|
{
|
||||||
public uint WorkerProtocolVersion { get; init; } = GatewayContractInfo.WorkerProtocolVersion;
|
public uint WorkerProtocolVersion { get; init; } = GatewayContractInfo.WorkerProtocolVersion;
|
||||||
|
|
||||||
|
public int MaxGrpcMessageBytes { get; init; } = 16 * 1024 * 1024;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,5 +8,9 @@ public sealed class SessionOptions
|
|||||||
|
|
||||||
public int MaxPendingCommandsPerSession { get; init; } = 128;
|
public int MaxPendingCommandsPerSession { get; init; } = 128;
|
||||||
|
|
||||||
|
public int DefaultLeaseSeconds { get; init; } = 1800;
|
||||||
|
|
||||||
|
public int LeaseSweepIntervalSeconds { get; init; } = 30;
|
||||||
|
|
||||||
public bool AllowMultipleEventSubscribers { get; init; }
|
public bool AllowMultipleEventSubscribers { get; init; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -190,6 +190,8 @@ else
|
|||||||
|
|
||||||
private int CommandTimeoutSeconds() => GalaxyOptions.Value.CommandTimeoutSeconds;
|
private int CommandTimeoutSeconds() => GalaxyOptions.Value.CommandTimeoutSeconds;
|
||||||
|
|
||||||
private string? GalaxyConnectionStringDisplay() =>
|
private string GalaxyConnectionStringDisplay()
|
||||||
DashboardRedactor.Redact(GalaxyOptions.Value.ConnectionString);
|
{
|
||||||
|
return DashboardConnectionStringDisplay.GalaxyRepositoryConnectionString(GalaxyOptions.Value.ConnectionString);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
using Microsoft.Data.SqlClient;
|
||||||
|
|
||||||
|
namespace MxGateway.Server.Dashboard;
|
||||||
|
|
||||||
|
public static class DashboardConnectionStringDisplay
|
||||||
|
{
|
||||||
|
public static string GalaxyRepositoryConnectionString(string connectionString)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
SqlConnectionStringBuilder builder = new(connectionString);
|
||||||
|
SqlConnectionStringBuilder display = new()
|
||||||
|
{
|
||||||
|
DataSource = builder.DataSource,
|
||||||
|
InitialCatalog = builder.InitialCatalog,
|
||||||
|
IntegratedSecurity = builder.IntegratedSecurity,
|
||||||
|
Encrypt = builder.Encrypt,
|
||||||
|
TrustServerCertificate = builder.TrustServerCertificate,
|
||||||
|
};
|
||||||
|
|
||||||
|
return display.ConnectionString;
|
||||||
|
}
|
||||||
|
catch (ArgumentException)
|
||||||
|
{
|
||||||
|
return "[invalid connection string]";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,97 +2,11 @@ using MxGateway.Server.Galaxy;
|
|||||||
|
|
||||||
namespace MxGateway.Server.Dashboard;
|
namespace MxGateway.Server.Dashboard;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>Projects the precomputed Galaxy cache dashboard summary.</summary>
|
||||||
/// Projects a <see cref="GalaxyHierarchyCacheEntry"/> into a
|
|
||||||
/// <see cref="DashboardGalaxySummary"/> for the Blazor pages. Top-templates and
|
|
||||||
/// per-category breakdowns are computed here rather than stored on the cache so the
|
|
||||||
/// Galaxy namespace stays free of dashboard-presentation concepts.
|
|
||||||
/// </summary>
|
|
||||||
internal static class DashboardGalaxyProjector
|
internal static class DashboardGalaxyProjector
|
||||||
{
|
{
|
||||||
private const int TopTemplatesLimit = 10;
|
|
||||||
|
|
||||||
private static readonly IReadOnlyDictionary<int, string> CategoryNamesById = new Dictionary<int, string>
|
|
||||||
{
|
|
||||||
[1] = "WinPlatform",
|
|
||||||
[3] = "AppEngine",
|
|
||||||
[4] = "InTouchViewApp",
|
|
||||||
[10] = "UserDefined",
|
|
||||||
[11] = "FieldReference",
|
|
||||||
[13] = "Area",
|
|
||||||
[17] = "DIObject",
|
|
||||||
[24] = "DDESuiteLinkClient",
|
|
||||||
[26] = "OPCClient",
|
|
||||||
};
|
|
||||||
|
|
||||||
public static DashboardGalaxySummary Project(GalaxyHierarchyCacheEntry entry)
|
public static DashboardGalaxySummary Project(GalaxyHierarchyCacheEntry entry)
|
||||||
{
|
{
|
||||||
DashboardGalaxyStatus status = entry.Status switch
|
return entry.DashboardSummary;
|
||||||
{
|
|
||||||
GalaxyCacheStatus.Healthy => DashboardGalaxyStatus.Healthy,
|
|
||||||
GalaxyCacheStatus.Stale => DashboardGalaxyStatus.Stale,
|
|
||||||
GalaxyCacheStatus.Unavailable => DashboardGalaxyStatus.Unavailable,
|
|
||||||
_ => DashboardGalaxyStatus.Unknown,
|
|
||||||
};
|
|
||||||
|
|
||||||
IReadOnlyList<DashboardGalaxyTemplateUsage> topTemplates;
|
|
||||||
IReadOnlyList<DashboardGalaxyCategoryCount> objectCategories;
|
|
||||||
|
|
||||||
if (entry.Hierarchy.Count == 0)
|
|
||||||
{
|
|
||||||
topTemplates = Array.Empty<DashboardGalaxyTemplateUsage>();
|
|
||||||
objectCategories = Array.Empty<DashboardGalaxyCategoryCount>();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Dictionary<int, int> objectsByCategory = new();
|
|
||||||
Dictionary<string, int> templateUsage = new(StringComparer.OrdinalIgnoreCase);
|
|
||||||
|
|
||||||
foreach (GalaxyHierarchyRow row in entry.Hierarchy)
|
|
||||||
{
|
|
||||||
objectsByCategory.TryGetValue(row.CategoryId, out int categoryCount);
|
|
||||||
objectsByCategory[row.CategoryId] = categoryCount + 1;
|
|
||||||
|
|
||||||
if (row.TemplateChain.Count > 0)
|
|
||||||
{
|
|
||||||
string immediate = row.TemplateChain[0];
|
|
||||||
if (!string.IsNullOrWhiteSpace(immediate))
|
|
||||||
{
|
|
||||||
templateUsage.TryGetValue(immediate, out int templateCount);
|
|
||||||
templateUsage[immediate] = templateCount + 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
topTemplates = templateUsage
|
|
||||||
.OrderByDescending(entry => entry.Value)
|
|
||||||
.ThenBy(entry => entry.Key, StringComparer.OrdinalIgnoreCase)
|
|
||||||
.Take(TopTemplatesLimit)
|
|
||||||
.Select(entry => new DashboardGalaxyTemplateUsage(entry.Key, entry.Value))
|
|
||||||
.ToArray();
|
|
||||||
|
|
||||||
objectCategories = objectsByCategory
|
|
||||||
.OrderByDescending(entry => entry.Value)
|
|
||||||
.ThenBy(entry => entry.Key)
|
|
||||||
.Select(entry => new DashboardGalaxyCategoryCount(
|
|
||||||
entry.Key,
|
|
||||||
CategoryNamesById.TryGetValue(entry.Key, out string? name) ? name : $"Category {entry.Key}",
|
|
||||||
entry.Value))
|
|
||||||
.ToArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
return new DashboardGalaxySummary(
|
|
||||||
Status: status,
|
|
||||||
LastQueriedAt: entry.LastQueriedAt,
|
|
||||||
LastSuccessAt: entry.LastSuccessAt,
|
|
||||||
LastDeployTime: entry.LastDeployTime,
|
|
||||||
LastError: entry.LastError,
|
|
||||||
ObjectCount: entry.ObjectCount,
|
|
||||||
AreaCount: entry.AreaCount,
|
|
||||||
AttributeCount: entry.AttributeCount,
|
|
||||||
HistorizedAttributeCount: entry.HistorizedAttributeCount,
|
|
||||||
AlarmAttributeCount: entry.AlarmAttributeCount,
|
|
||||||
TopTemplates: topTemplates,
|
|
||||||
ObjectCategories: objectCategories);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ using Google.Protobuf.WellKnownTypes;
|
|||||||
using Microsoft.Data.SqlClient;
|
using Microsoft.Data.SqlClient;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using MxGateway.Contracts.Proto.Galaxy;
|
using MxGateway.Contracts.Proto.Galaxy;
|
||||||
|
using MxGateway.Server.Dashboard;
|
||||||
using MxGateway.Server.Grpc;
|
using MxGateway.Server.Grpc;
|
||||||
|
|
||||||
namespace MxGateway.Server.Galaxy;
|
namespace MxGateway.Server.Galaxy;
|
||||||
@@ -43,7 +44,16 @@ public sealed class GalaxyHierarchyCache : IGalaxyHierarchyCache
|
|||||||
{
|
{
|
||||||
GalaxyHierarchyCacheEntry snapshot = Volatile.Read(ref _current);
|
GalaxyHierarchyCacheEntry snapshot = Volatile.Read(ref _current);
|
||||||
GalaxyCacheStatus projected = ProjectStatus(snapshot);
|
GalaxyCacheStatus projected = ProjectStatus(snapshot);
|
||||||
return projected == snapshot.Status ? snapshot : snapshot with { Status = projected };
|
return projected == snapshot.Status
|
||||||
|
? snapshot
|
||||||
|
: snapshot with
|
||||||
|
{
|
||||||
|
Status = projected,
|
||||||
|
DashboardSummary = snapshot.DashboardSummary with
|
||||||
|
{
|
||||||
|
Status = MapDashboardStatus(projected),
|
||||||
|
},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,11 +111,23 @@ public sealed class GalaxyHierarchyCache : IGalaxyHierarchyCache
|
|||||||
|
|
||||||
List<GalaxyHierarchyRow> hierarchy = hierarchyTask.Result;
|
List<GalaxyHierarchyRow> hierarchy = hierarchyTask.Result;
|
||||||
List<GalaxyAttributeRow> attributes = attributesTask.Result;
|
List<GalaxyAttributeRow> attributes = attributesTask.Result;
|
||||||
DiscoverHierarchyReply reply = BuildReply(hierarchy, attributes);
|
IReadOnlyList<GalaxyObject> objects = BuildObjects(hierarchy, attributes);
|
||||||
|
|
||||||
int areaCount = hierarchy.Count(row => row.IsArea);
|
int areaCount = hierarchy.Count(row => row.IsArea);
|
||||||
int historized = attributes.Count(row => row.IsHistorized);
|
int historized = attributes.Count(row => row.IsHistorized);
|
||||||
int alarms = attributes.Count(row => row.IsAlarm);
|
int alarms = attributes.Count(row => row.IsAlarm);
|
||||||
|
DashboardGalaxySummary dashboardSummary = BuildDashboardSummary(
|
||||||
|
status: GalaxyCacheStatus.Healthy,
|
||||||
|
lastQueriedAt: queriedAt,
|
||||||
|
lastSuccessAt: queriedAt,
|
||||||
|
lastDeployTime: deployTime,
|
||||||
|
lastError: null,
|
||||||
|
hierarchy: hierarchy,
|
||||||
|
objectCount: hierarchy.Count,
|
||||||
|
areaCount: areaCount,
|
||||||
|
attributeCount: attributes.Count,
|
||||||
|
historizedAttributeCount: historized,
|
||||||
|
alarmAttributeCount: alarms);
|
||||||
|
|
||||||
long nextSequence = previous.Sequence + 1;
|
long nextSequence = previous.Sequence + 1;
|
||||||
GalaxyHierarchyCacheEntry next = new(
|
GalaxyHierarchyCacheEntry next = new(
|
||||||
@@ -115,9 +137,8 @@ public sealed class GalaxyHierarchyCache : IGalaxyHierarchyCache
|
|||||||
LastSuccessAt: queriedAt,
|
LastSuccessAt: queriedAt,
|
||||||
LastDeployTime: deployTime,
|
LastDeployTime: deployTime,
|
||||||
LastError: null,
|
LastError: null,
|
||||||
Hierarchy: hierarchy,
|
Objects: objects,
|
||||||
Attributes: attributes,
|
DashboardSummary: dashboardSummary,
|
||||||
Reply: reply,
|
|
||||||
ObjectCount: hierarchy.Count,
|
ObjectCount: hierarchy.Count,
|
||||||
AreaCount: areaCount,
|
AreaCount: areaCount,
|
||||||
AttributeCount: attributes.Count,
|
AttributeCount: attributes.Count,
|
||||||
@@ -146,13 +167,19 @@ public sealed class GalaxyHierarchyCache : IGalaxyHierarchyCache
|
|||||||
Status = previous.HasData ? GalaxyCacheStatus.Stale : GalaxyCacheStatus.Unavailable,
|
Status = previous.HasData ? GalaxyCacheStatus.Stale : GalaxyCacheStatus.Unavailable,
|
||||||
LastQueriedAt = queriedAt,
|
LastQueriedAt = queriedAt,
|
||||||
LastError = exception.Message,
|
LastError = exception.Message,
|
||||||
|
DashboardSummary = previous.DashboardSummary with
|
||||||
|
{
|
||||||
|
Status = MapDashboardStatus(previous.HasData ? GalaxyCacheStatus.Stale : GalaxyCacheStatus.Unavailable),
|
||||||
|
LastQueriedAt = queriedAt,
|
||||||
|
LastError = exception.Message,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
Volatile.Write(ref _current, failed);
|
Volatile.Write(ref _current, failed);
|
||||||
_firstLoad.TrySetResult();
|
_firstLoad.TrySetResult();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static DiscoverHierarchyReply BuildReply(
|
private static IReadOnlyList<GalaxyObject> BuildObjects(
|
||||||
IReadOnlyList<GalaxyHierarchyRow> hierarchy,
|
IReadOnlyList<GalaxyHierarchyRow> hierarchy,
|
||||||
IReadOnlyList<GalaxyAttributeRow> attributes)
|
IReadOnlyList<GalaxyAttributeRow> attributes)
|
||||||
{
|
{
|
||||||
@@ -160,14 +187,110 @@ public sealed class GalaxyHierarchyCache : IGalaxyHierarchyCache
|
|||||||
.GroupBy(a => a.GobjectId)
|
.GroupBy(a => a.GobjectId)
|
||||||
.ToDictionary(g => g.Key, g => g.ToList());
|
.ToDictionary(g => g.Key, g => g.ToList());
|
||||||
|
|
||||||
DiscoverHierarchyReply reply = new();
|
List<GalaxyObject> objects = new(hierarchy.Count);
|
||||||
foreach (GalaxyHierarchyRow row in hierarchy)
|
foreach (GalaxyHierarchyRow row in hierarchy)
|
||||||
{
|
{
|
||||||
reply.Objects.Add(GalaxyProtoMapper.MapObject(row, attributesByGobjectId));
|
objects.Add(GalaxyProtoMapper.MapObject(row, attributesByGobjectId));
|
||||||
}
|
}
|
||||||
return reply;
|
return objects;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static DashboardGalaxySummary BuildDashboardSummary(
|
||||||
|
GalaxyCacheStatus status,
|
||||||
|
DateTimeOffset? lastQueriedAt,
|
||||||
|
DateTimeOffset? lastSuccessAt,
|
||||||
|
DateTimeOffset? lastDeployTime,
|
||||||
|
string? lastError,
|
||||||
|
IReadOnlyList<GalaxyHierarchyRow> hierarchy,
|
||||||
|
int objectCount,
|
||||||
|
int areaCount,
|
||||||
|
int attributeCount,
|
||||||
|
int historizedAttributeCount,
|
||||||
|
int alarmAttributeCount)
|
||||||
|
{
|
||||||
|
IReadOnlyList<DashboardGalaxyTemplateUsage> topTemplates;
|
||||||
|
IReadOnlyList<DashboardGalaxyCategoryCount> objectCategories;
|
||||||
|
|
||||||
|
if (hierarchy.Count == 0)
|
||||||
|
{
|
||||||
|
topTemplates = Array.Empty<DashboardGalaxyTemplateUsage>();
|
||||||
|
objectCategories = Array.Empty<DashboardGalaxyCategoryCount>();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Dictionary<int, int> objectsByCategory = new();
|
||||||
|
Dictionary<string, int> templateUsage = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
foreach (GalaxyHierarchyRow row in hierarchy)
|
||||||
|
{
|
||||||
|
objectsByCategory.TryGetValue(row.CategoryId, out int categoryCount);
|
||||||
|
objectsByCategory[row.CategoryId] = categoryCount + 1;
|
||||||
|
|
||||||
|
if (row.TemplateChain.Count > 0)
|
||||||
|
{
|
||||||
|
string immediate = row.TemplateChain[0];
|
||||||
|
if (!string.IsNullOrWhiteSpace(immediate))
|
||||||
|
{
|
||||||
|
templateUsage.TryGetValue(immediate, out int templateCount);
|
||||||
|
templateUsage[immediate] = templateCount + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
topTemplates = templateUsage
|
||||||
|
.OrderByDescending(entry => entry.Value)
|
||||||
|
.ThenBy(entry => entry.Key, StringComparer.OrdinalIgnoreCase)
|
||||||
|
.Take(10)
|
||||||
|
.Select(entry => new DashboardGalaxyTemplateUsage(entry.Key, entry.Value))
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
objectCategories = objectsByCategory
|
||||||
|
.OrderByDescending(entry => entry.Value)
|
||||||
|
.ThenBy(entry => entry.Key)
|
||||||
|
.Select(entry => new DashboardGalaxyCategoryCount(
|
||||||
|
entry.Key,
|
||||||
|
ResolveCategoryName(entry.Key),
|
||||||
|
entry.Value))
|
||||||
|
.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
return new DashboardGalaxySummary(
|
||||||
|
Status: MapDashboardStatus(status),
|
||||||
|
LastQueriedAt: lastQueriedAt,
|
||||||
|
LastSuccessAt: lastSuccessAt,
|
||||||
|
LastDeployTime: lastDeployTime,
|
||||||
|
LastError: lastError,
|
||||||
|
ObjectCount: objectCount,
|
||||||
|
AreaCount: areaCount,
|
||||||
|
AttributeCount: attributeCount,
|
||||||
|
HistorizedAttributeCount: historizedAttributeCount,
|
||||||
|
AlarmAttributeCount: alarmAttributeCount,
|
||||||
|
TopTemplates: topTemplates,
|
||||||
|
ObjectCategories: objectCategories);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DashboardGalaxyStatus MapDashboardStatus(GalaxyCacheStatus status) => status switch
|
||||||
|
{
|
||||||
|
GalaxyCacheStatus.Healthy => DashboardGalaxyStatus.Healthy,
|
||||||
|
GalaxyCacheStatus.Stale => DashboardGalaxyStatus.Stale,
|
||||||
|
GalaxyCacheStatus.Unavailable => DashboardGalaxyStatus.Unavailable,
|
||||||
|
_ => DashboardGalaxyStatus.Unknown,
|
||||||
|
};
|
||||||
|
|
||||||
|
private static string ResolveCategoryName(int categoryId) => categoryId switch
|
||||||
|
{
|
||||||
|
1 => "WinPlatform",
|
||||||
|
3 => "AppEngine",
|
||||||
|
4 => "InTouchViewApp",
|
||||||
|
10 => "UserDefined",
|
||||||
|
11 => "FieldReference",
|
||||||
|
13 => "Area",
|
||||||
|
17 => "DIObject",
|
||||||
|
24 => "DDESuiteLinkClient",
|
||||||
|
26 => "OPCClient",
|
||||||
|
_ => $"Category {categoryId}",
|
||||||
|
};
|
||||||
|
|
||||||
private GalaxyCacheStatus ProjectStatus(GalaxyHierarchyCacheEntry snapshot)
|
private GalaxyCacheStatus ProjectStatus(GalaxyHierarchyCacheEntry snapshot)
|
||||||
{
|
{
|
||||||
if (snapshot.Status is GalaxyCacheStatus.Unknown or GalaxyCacheStatus.Unavailable)
|
if (snapshot.Status is GalaxyCacheStatus.Unknown or GalaxyCacheStatus.Unavailable)
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
using MxGateway.Contracts.Proto.Galaxy;
|
using MxGateway.Contracts.Proto.Galaxy;
|
||||||
|
using MxGateway.Server.Dashboard;
|
||||||
|
|
||||||
namespace MxGateway.Server.Galaxy;
|
namespace MxGateway.Server.Galaxy;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Immutable snapshot of the Galaxy Repository browse data held by
|
/// Immutable snapshot of the Galaxy Repository browse data held by
|
||||||
/// <see cref="GalaxyHierarchyCache"/>. Multiple gRPC clients share the same instance —
|
/// <see cref="GalaxyHierarchyCache"/>. Multiple gRPC clients share the same
|
||||||
/// the materialized <see cref="Reply"/> is produced once per refresh and reused.
|
/// materialized object list and precomputed dashboard projection.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed record GalaxyHierarchyCacheEntry(
|
public sealed record GalaxyHierarchyCacheEntry(
|
||||||
GalaxyCacheStatus Status,
|
GalaxyCacheStatus Status,
|
||||||
@@ -14,9 +15,8 @@ public sealed record GalaxyHierarchyCacheEntry(
|
|||||||
DateTimeOffset? LastSuccessAt,
|
DateTimeOffset? LastSuccessAt,
|
||||||
DateTimeOffset? LastDeployTime,
|
DateTimeOffset? LastDeployTime,
|
||||||
string? LastError,
|
string? LastError,
|
||||||
IReadOnlyList<GalaxyHierarchyRow> Hierarchy,
|
IReadOnlyList<GalaxyObject> Objects,
|
||||||
IReadOnlyList<GalaxyAttributeRow> Attributes,
|
DashboardGalaxySummary DashboardSummary,
|
||||||
DiscoverHierarchyReply? Reply,
|
|
||||||
int ObjectCount,
|
int ObjectCount,
|
||||||
int AreaCount,
|
int AreaCount,
|
||||||
int AttributeCount,
|
int AttributeCount,
|
||||||
@@ -30,9 +30,8 @@ public sealed record GalaxyHierarchyCacheEntry(
|
|||||||
LastSuccessAt: null,
|
LastSuccessAt: null,
|
||||||
LastDeployTime: null,
|
LastDeployTime: null,
|
||||||
LastError: null,
|
LastError: null,
|
||||||
Hierarchy: Array.Empty<GalaxyHierarchyRow>(),
|
Objects: Array.Empty<GalaxyObject>(),
|
||||||
Attributes: Array.Empty<GalaxyAttributeRow>(),
|
DashboardSummary: DashboardGalaxySummary.Unknown,
|
||||||
Reply: null,
|
|
||||||
ObjectCount: 0,
|
ObjectCount: 0,
|
||||||
AreaCount: 0,
|
AreaCount: 0,
|
||||||
AttributeCount: 0,
|
AttributeCount: 0,
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ public sealed class GalaxyRepositoryGrpcService(
|
|||||||
ILogger<GalaxyRepositoryGrpcService> logger) : ProtoGalaxyRepository.GalaxyRepositoryBase
|
ILogger<GalaxyRepositoryGrpcService> logger) : ProtoGalaxyRepository.GalaxyRepositoryBase
|
||||||
{
|
{
|
||||||
private static readonly TimeSpan FirstLoadWaitBudget = TimeSpan.FromSeconds(5);
|
private static readonly TimeSpan FirstLoadWaitBudget = TimeSpan.FromSeconds(5);
|
||||||
|
private const int DefaultDiscoverPageSize = 1000;
|
||||||
|
private const int MaxDiscoverPageSize = 5000;
|
||||||
|
|
||||||
public override async Task<TestConnectionReply> TestConnection(
|
public override async Task<TestConnectionReply> TestConnection(
|
||||||
TestConnectionRequest request,
|
TestConnectionRequest request,
|
||||||
@@ -59,16 +61,39 @@ public sealed class GalaxyRepositoryGrpcService(
|
|||||||
await WaitForCacheBootstrap(context.CancellationToken).ConfigureAwait(false);
|
await WaitForCacheBootstrap(context.CancellationToken).ConfigureAwait(false);
|
||||||
GalaxyDb.GalaxyHierarchyCacheEntry entry = cache.Current;
|
GalaxyDb.GalaxyHierarchyCacheEntry entry = cache.Current;
|
||||||
|
|
||||||
if (!entry.HasData || entry.Reply is null)
|
if (!entry.HasData)
|
||||||
{
|
{
|
||||||
throw new RpcException(new Status(
|
throw new RpcException(new Status(
|
||||||
StatusCode.Unavailable,
|
StatusCode.Unavailable,
|
||||||
ResolveUnavailableMessage(entry)));
|
ResolveUnavailableMessage(entry)));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Same materialized reply is shared across all clients — gRPC serialization is
|
int offset = ParsePageToken(request.PageToken);
|
||||||
// read-only and the entry is replaced atomically on the next refresh.
|
if (offset > entry.Objects.Count)
|
||||||
return entry.Reply;
|
{
|
||||||
|
throw new RpcException(new Status(
|
||||||
|
StatusCode.InvalidArgument,
|
||||||
|
"DiscoverHierarchy page_token is outside the current hierarchy."));
|
||||||
|
}
|
||||||
|
|
||||||
|
int pageSize = ResolvePageSize(request.PageSize);
|
||||||
|
int take = Math.Min(pageSize, entry.Objects.Count - offset);
|
||||||
|
DiscoverHierarchyReply reply = new()
|
||||||
|
{
|
||||||
|
TotalObjectCount = entry.Objects.Count,
|
||||||
|
};
|
||||||
|
for (int index = offset; index < offset + take; index++)
|
||||||
|
{
|
||||||
|
reply.Objects.Add(entry.Objects[index].Clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
int nextOffset = offset + take;
|
||||||
|
if (nextOffset < entry.Objects.Count)
|
||||||
|
{
|
||||||
|
reply.NextPageToken = nextOffset.ToString(System.Globalization.CultureInfo.InvariantCulture);
|
||||||
|
}
|
||||||
|
|
||||||
|
return reply;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async Task WatchDeployEvents(
|
public override async Task WatchDeployEvents(
|
||||||
@@ -144,6 +169,41 @@ public sealed class GalaxyRepositoryGrpcService(
|
|||||||
_ => "Galaxy cache has no data available.",
|
_ => "Galaxy cache has no data available.",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private static int ResolvePageSize(int requestedPageSize)
|
||||||
|
{
|
||||||
|
if (requestedPageSize < 0)
|
||||||
|
{
|
||||||
|
throw new RpcException(new Status(
|
||||||
|
StatusCode.InvalidArgument,
|
||||||
|
"DiscoverHierarchy page_size must be greater than zero when provided."));
|
||||||
|
}
|
||||||
|
|
||||||
|
int pageSize = requestedPageSize == 0 ? DefaultDiscoverPageSize : requestedPageSize;
|
||||||
|
return Math.Min(pageSize, MaxDiscoverPageSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int ParsePageToken(string pageToken)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(pageToken))
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!int.TryParse(
|
||||||
|
pageToken,
|
||||||
|
System.Globalization.NumberStyles.None,
|
||||||
|
System.Globalization.CultureInfo.InvariantCulture,
|
||||||
|
out int offset)
|
||||||
|
|| offset < 0)
|
||||||
|
{
|
||||||
|
throw new RpcException(new Status(
|
||||||
|
StatusCode.InvalidArgument,
|
||||||
|
"DiscoverHierarchy page_token is invalid."));
|
||||||
|
}
|
||||||
|
|
||||||
|
return offset;
|
||||||
|
}
|
||||||
|
|
||||||
[System.Diagnostics.CodeAnalysis.SuppressMessage(
|
[System.Diagnostics.CodeAnalysis.SuppressMessage(
|
||||||
"Style",
|
"Style",
|
||||||
"IDE0051:Remove unused private members",
|
"IDE0051:Remove unused private members",
|
||||||
|
|||||||
+11
@@ -1,4 +1,6 @@
|
|||||||
using Grpc.Core.Interceptors;
|
using Grpc.Core.Interceptors;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using MxGateway.Server.Configuration;
|
||||||
|
|
||||||
namespace MxGateway.Server.Security.Authorization;
|
namespace MxGateway.Server.Security.Authorization;
|
||||||
|
|
||||||
@@ -9,6 +11,15 @@ public static class GrpcAuthorizationServiceCollectionExtensions
|
|||||||
services.AddSingleton<GatewayGrpcScopeResolver>();
|
services.AddSingleton<GatewayGrpcScopeResolver>();
|
||||||
services.AddSingleton<IGatewayRequestIdentityAccessor, GatewayRequestIdentityAccessor>();
|
services.AddSingleton<IGatewayRequestIdentityAccessor, GatewayRequestIdentityAccessor>();
|
||||||
services.AddSingleton<GatewayGrpcAuthorizationInterceptor>();
|
services.AddSingleton<GatewayGrpcAuthorizationInterceptor>();
|
||||||
|
services
|
||||||
|
.AddOptions<global::Grpc.AspNetCore.Server.GrpcServiceOptions>()
|
||||||
|
.Configure<IConfiguration>((grpcOptions, configuration) =>
|
||||||
|
{
|
||||||
|
ProtocolOptions protocolOptions = new();
|
||||||
|
configuration.GetSection("MxGateway:Protocol").Bind(protocolOptions);
|
||||||
|
grpcOptions.MaxReceiveMessageSize = protocolOptions.MaxGrpcMessageBytes;
|
||||||
|
grpcOptions.MaxSendMessageSize = protocolOptions.MaxGrpcMessageBytes;
|
||||||
|
});
|
||||||
services.AddGrpc(options => options.Interceptors.Add<GatewayGrpcAuthorizationInterceptor>());
|
services.AddGrpc(options => options.Interceptors.Add<GatewayGrpcAuthorizationInterceptor>());
|
||||||
|
|
||||||
return services;
|
return services;
|
||||||
|
|||||||
@@ -27,6 +27,35 @@ public sealed class GatewaySession
|
|||||||
TimeSpan startupTimeout,
|
TimeSpan startupTimeout,
|
||||||
TimeSpan shutdownTimeout,
|
TimeSpan shutdownTimeout,
|
||||||
DateTimeOffset openedAt)
|
DateTimeOffset openedAt)
|
||||||
|
: this(
|
||||||
|
sessionId,
|
||||||
|
backendName,
|
||||||
|
pipeName,
|
||||||
|
nonce,
|
||||||
|
clientIdentity,
|
||||||
|
clientSessionName,
|
||||||
|
clientCorrelationId,
|
||||||
|
commandTimeout,
|
||||||
|
startupTimeout,
|
||||||
|
shutdownTimeout,
|
||||||
|
TimeSpan.FromMinutes(30),
|
||||||
|
openedAt)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public GatewaySession(
|
||||||
|
string sessionId,
|
||||||
|
string backendName,
|
||||||
|
string pipeName,
|
||||||
|
string nonce,
|
||||||
|
string? clientIdentity,
|
||||||
|
string? clientSessionName,
|
||||||
|
string? clientCorrelationId,
|
||||||
|
TimeSpan commandTimeout,
|
||||||
|
TimeSpan startupTimeout,
|
||||||
|
TimeSpan shutdownTimeout,
|
||||||
|
TimeSpan leaseDuration,
|
||||||
|
DateTimeOffset openedAt)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(sessionId))
|
if (string.IsNullOrWhiteSpace(sessionId))
|
||||||
{
|
{
|
||||||
@@ -58,8 +87,10 @@ public sealed class GatewaySession
|
|||||||
CommandTimeout = commandTimeout;
|
CommandTimeout = commandTimeout;
|
||||||
StartupTimeout = startupTimeout;
|
StartupTimeout = startupTimeout;
|
||||||
ShutdownTimeout = shutdownTimeout;
|
ShutdownTimeout = shutdownTimeout;
|
||||||
|
LeaseDuration = leaseDuration;
|
||||||
OpenedAt = openedAt;
|
OpenedAt = openedAt;
|
||||||
_lastClientActivityAt = openedAt;
|
_lastClientActivityAt = openedAt;
|
||||||
|
_leaseExpiresAt = openedAt + leaseDuration;
|
||||||
}
|
}
|
||||||
|
|
||||||
public string SessionId { get; }
|
public string SessionId { get; }
|
||||||
@@ -82,6 +113,8 @@ public sealed class GatewaySession
|
|||||||
|
|
||||||
public TimeSpan ShutdownTimeout { get; }
|
public TimeSpan ShutdownTimeout { get; }
|
||||||
|
|
||||||
|
public TimeSpan LeaseDuration { get; }
|
||||||
|
|
||||||
public DateTimeOffset OpenedAt { get; }
|
public DateTimeOffset OpenedAt { get; }
|
||||||
|
|
||||||
public int? WorkerProcessId => _workerClient?.ProcessId;
|
public int? WorkerProcessId => _workerClient?.ProcessId;
|
||||||
@@ -195,6 +228,7 @@ public sealed class GatewaySession
|
|||||||
lock (_syncRoot)
|
lock (_syncRoot)
|
||||||
{
|
{
|
||||||
_lastClientActivityAt = activityAt;
|
_lastClientActivityAt = activityAt;
|
||||||
|
_leaseExpiresAt = activityAt + LeaseDuration;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -210,7 +244,9 @@ public sealed class GatewaySession
|
|||||||
{
|
{
|
||||||
lock (_syncRoot)
|
lock (_syncRoot)
|
||||||
{
|
{
|
||||||
return _leaseExpiresAt is not null && _leaseExpiresAt <= now;
|
return _activeEventSubscriberCount == 0
|
||||||
|
&& _leaseExpiresAt is not null
|
||||||
|
&& _leaseExpiresAt <= now;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using MxGateway.Server.Configuration;
|
||||||
|
|
||||||
|
namespace MxGateway.Server.Sessions;
|
||||||
|
|
||||||
|
public sealed class SessionLeaseMonitorHostedService(
|
||||||
|
ISessionManager sessionManager,
|
||||||
|
IOptions<GatewayOptions> options,
|
||||||
|
ILogger<SessionLeaseMonitorHostedService> logger,
|
||||||
|
TimeProvider? timeProvider = null) : BackgroundService
|
||||||
|
{
|
||||||
|
private readonly TimeProvider _timeProvider = timeProvider ?? TimeProvider.System;
|
||||||
|
|
||||||
|
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||||
|
{
|
||||||
|
TimeSpan interval = TimeSpan.FromSeconds(Math.Max(1, options.Value.Sessions.LeaseSweepIntervalSeconds));
|
||||||
|
using PeriodicTimer timer = new(interval, _timeProvider);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
while (await timer.WaitForNextTickAsync(stoppingToken).ConfigureAwait(false))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await sessionManager
|
||||||
|
.CloseExpiredLeasesAsync(_timeProvider.GetUtcNow(), stoppingToken)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
logger.LogWarning(exception, "Session lease sweep failed.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -287,6 +287,7 @@ public sealed class SessionManager : ISessionManager
|
|||||||
TimeSpan commandTimeout = ResolveCommandTimeout(request.CommandTimeout);
|
TimeSpan commandTimeout = ResolveCommandTimeout(request.CommandTimeout);
|
||||||
TimeSpan startupTimeout = TimeSpan.FromSeconds(_options.Worker.StartupTimeoutSeconds);
|
TimeSpan startupTimeout = TimeSpan.FromSeconds(_options.Worker.StartupTimeoutSeconds);
|
||||||
TimeSpan shutdownTimeout = TimeSpan.FromSeconds(_options.Worker.ShutdownTimeoutSeconds);
|
TimeSpan shutdownTimeout = TimeSpan.FromSeconds(_options.Worker.ShutdownTimeoutSeconds);
|
||||||
|
TimeSpan leaseDuration = TimeSpan.FromSeconds(_options.Sessions.DefaultLeaseSeconds);
|
||||||
string pipeName = $"mxaccess-gateway-{Environment.ProcessId}-{sessionId}";
|
string pipeName = $"mxaccess-gateway-{Environment.ProcessId}-{sessionId}";
|
||||||
string nonce = CreateNonce();
|
string nonce = CreateNonce();
|
||||||
DateTimeOffset openedAt = _timeProvider.GetUtcNow();
|
DateTimeOffset openedAt = _timeProvider.GetUtcNow();
|
||||||
@@ -303,6 +304,7 @@ public sealed class SessionManager : ISessionManager
|
|||||||
commandTimeout,
|
commandTimeout,
|
||||||
startupTimeout,
|
startupTimeout,
|
||||||
shutdownTimeout,
|
shutdownTimeout,
|
||||||
|
leaseDuration,
|
||||||
openedAt);
|
openedAt);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ public static class SessionServiceCollectionExtensions
|
|||||||
services.AddSingleton<ISessionRegistry, SessionRegistry>();
|
services.AddSingleton<ISessionRegistry, SessionRegistry>();
|
||||||
services.AddSingleton<ISessionWorkerClientFactory, SessionWorkerClientFactory>();
|
services.AddSingleton<ISessionWorkerClientFactory, SessionWorkerClientFactory>();
|
||||||
services.AddSingleton<ISessionManager, SessionManager>();
|
services.AddSingleton<ISessionManager, SessionManager>();
|
||||||
|
services.AddHostedService<SessionLeaseMonitorHostedService>();
|
||||||
services.AddHostedService<SessionShutdownHostedService>();
|
services.AddHostedService<SessionShutdownHostedService>();
|
||||||
|
|
||||||
return services;
|
return services;
|
||||||
|
|||||||
@@ -231,11 +231,17 @@ public sealed class WorkerClient : IWorkerClient
|
|||||||
}
|
}
|
||||||
|
|
||||||
WorkerClientState state = State;
|
WorkerClientState state = State;
|
||||||
if (state is WorkerClientState.Closed or WorkerClientState.Faulted)
|
if (state == WorkerClientState.Closed)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (state == WorkerClientState.Faulted)
|
||||||
|
{
|
||||||
|
KillOwnedProcess("ShutdownFaulted");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
MarkClosing();
|
MarkClosing();
|
||||||
await EnqueueAsync(CreateShutdownEnvelope(timeout, "gateway-shutdown"), cancellationToken).ConfigureAwait(false);
|
await EnqueueAsync(CreateShutdownEnvelope(timeout, "gateway-shutdown"), cancellationToken).ConfigureAwait(false);
|
||||||
_outboundEnvelopes.Writer.TryComplete();
|
_outboundEnvelopes.Writer.TryComplete();
|
||||||
@@ -263,8 +269,7 @@ public sealed class WorkerClient : IWorkerClient
|
|||||||
public void Kill(string reason)
|
public void Kill(string reason)
|
||||||
{
|
{
|
||||||
ThrowIfDisposed();
|
ThrowIfDisposed();
|
||||||
_connection.ProcessHandle?.Process.Kill(entireProcessTree: true);
|
KillOwnedProcess(reason);
|
||||||
_metrics?.WorkerKilled(reason);
|
|
||||||
SetFaulted(
|
SetFaulted(
|
||||||
WorkerClientErrorCode.WorkerFaulted,
|
WorkerClientErrorCode.WorkerFaulted,
|
||||||
$"Worker was killed by the gateway: {reason}.",
|
$"Worker was killed by the gateway: {reason}.",
|
||||||
@@ -279,6 +284,7 @@ public sealed class WorkerClient : IWorkerClient
|
|||||||
}
|
}
|
||||||
|
|
||||||
_disposed = true;
|
_disposed = true;
|
||||||
|
KillOwnedProcess("Dispose");
|
||||||
_stopCts.Cancel();
|
_stopCts.Cancel();
|
||||||
_outboundEnvelopes.Writer.TryComplete();
|
_outboundEnvelopes.Writer.TryComplete();
|
||||||
_events.Writer.TryComplete();
|
_events.Writer.TryComplete();
|
||||||
@@ -607,12 +613,39 @@ public sealed class WorkerClient : IWorkerClient
|
|||||||
_stopCts.Cancel();
|
_stopCts.Cancel();
|
||||||
_outboundEnvelopes.Writer.TryComplete(fault);
|
_outboundEnvelopes.Writer.TryComplete(fault);
|
||||||
_events.Writer.TryComplete(fault);
|
_events.Writer.TryComplete(fault);
|
||||||
|
KillOwnedProcess(errorCode.ToString());
|
||||||
CompletePendingCommands(fault);
|
CompletePendingCommands(fault);
|
||||||
RecordWorkerStoppedOnce(errorCode.ToString());
|
RecordWorkerStoppedOnce(errorCode.ToString());
|
||||||
_metrics?.Fault(errorCode.ToString());
|
_metrics?.Fault(errorCode.ToString());
|
||||||
_logger.LogWarning(exception, "Worker client faulted for session {SessionId}: {Message}", SessionId, message);
|
_logger.LogWarning(exception, "Worker client faulted for session {SessionId}: {Message}", SessionId, message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void KillOwnedProcess(string reason)
|
||||||
|
{
|
||||||
|
WorkerProcessHandle? processHandle = _connection.ProcessHandle;
|
||||||
|
if (processHandle is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!processHandle.Process.HasExited)
|
||||||
|
{
|
||||||
|
processHandle.Process.Kill(entireProcessTree: true);
|
||||||
|
_metrics?.WorkerKilled(reason);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(
|
||||||
|
exception,
|
||||||
|
"Failed to kill worker process {ProcessId} for session {SessionId}.",
|
||||||
|
processHandle.ProcessId,
|
||||||
|
SessionId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void RecordWorkerStoppedOnce(string reason)
|
private void RecordWorkerStoppedOnce(string reason)
|
||||||
{
|
{
|
||||||
bool shouldRecord;
|
bool shouldRecord;
|
||||||
|
|||||||
@@ -25,6 +25,9 @@
|
|||||||
"Sessions": {
|
"Sessions": {
|
||||||
"DefaultCommandTimeoutSeconds": 30,
|
"DefaultCommandTimeoutSeconds": 30,
|
||||||
"MaxSessions": 64,
|
"MaxSessions": 64,
|
||||||
|
"MaxPendingCommandsPerSession": 128,
|
||||||
|
"DefaultLeaseSeconds": 1800,
|
||||||
|
"LeaseSweepIntervalSeconds": 30,
|
||||||
"AllowMultipleEventSubscribers": false
|
"AllowMultipleEventSubscribers": false
|
||||||
},
|
},
|
||||||
"Events": {
|
"Events": {
|
||||||
@@ -42,7 +45,8 @@
|
|||||||
"ShowTagValues": false
|
"ShowTagValues": false
|
||||||
},
|
},
|
||||||
"Protocol": {
|
"Protocol": {
|
||||||
"WorkerProtocolVersion": 1
|
"WorkerProtocolVersion": 1,
|
||||||
|
"MaxGrpcMessageBytes": 16777216
|
||||||
},
|
},
|
||||||
"Galaxy": {
|
"Galaxy": {
|
||||||
"ConnectionString": "Server=localhost;Database=ZB;Integrated Security=True;TrustServerCertificate=True;Encrypt=False;",
|
"ConnectionString": "Server=localhost;Database=ZB;Integrated Security=True;TrustServerCertificate=True;Encrypt=False;",
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ public sealed class GatewayOptionsTests
|
|||||||
|
|
||||||
Assert.Equal(30, options.Sessions.DefaultCommandTimeoutSeconds);
|
Assert.Equal(30, options.Sessions.DefaultCommandTimeoutSeconds);
|
||||||
Assert.Equal(64, options.Sessions.MaxSessions);
|
Assert.Equal(64, options.Sessions.MaxSessions);
|
||||||
|
Assert.Equal(1800, options.Sessions.DefaultLeaseSeconds);
|
||||||
|
Assert.Equal(30, options.Sessions.LeaseSweepIntervalSeconds);
|
||||||
Assert.False(options.Sessions.AllowMultipleEventSubscribers);
|
Assert.False(options.Sessions.AllowMultipleEventSubscribers);
|
||||||
|
|
||||||
Assert.Equal(10_000, options.Events.QueueCapacity);
|
Assert.Equal(10_000, options.Events.QueueCapacity);
|
||||||
@@ -45,6 +47,7 @@ public sealed class GatewayOptionsTests
|
|||||||
Assert.False(options.Dashboard.ShowTagValues);
|
Assert.False(options.Dashboard.ShowTagValues);
|
||||||
|
|
||||||
Assert.Equal(1u, options.Protocol.WorkerProtocolVersion);
|
Assert.Equal(1u, options.Protocol.WorkerProtocolVersion);
|
||||||
|
Assert.Equal(16 * 1024 * 1024, options.Protocol.MaxGrpcMessageBytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -56,22 +59,29 @@ public sealed class GatewayOptionsTests
|
|||||||
["MxGateway:Authentication:Mode"] = "Disabled",
|
["MxGateway:Authentication:Mode"] = "Disabled",
|
||||||
["MxGateway:Worker:ExecutablePath"] = @"C:\Gateway\MxGateway.Worker.exe",
|
["MxGateway:Worker:ExecutablePath"] = @"C:\Gateway\MxGateway.Worker.exe",
|
||||||
["MxGateway:Sessions:MaxSessions"] = "12",
|
["MxGateway:Sessions:MaxSessions"] = "12",
|
||||||
|
["MxGateway:Sessions:DefaultLeaseSeconds"] = "900",
|
||||||
["MxGateway:Events:QueueCapacity"] = "256",
|
["MxGateway:Events:QueueCapacity"] = "256",
|
||||||
["MxGateway:Dashboard:Enabled"] = "false"
|
["MxGateway:Dashboard:Enabled"] = "false",
|
||||||
|
["MxGateway:Protocol:MaxGrpcMessageBytes"] = "8388608"
|
||||||
});
|
});
|
||||||
|
|
||||||
Assert.Equal(AuthenticationMode.Disabled, options.Authentication.Mode);
|
Assert.Equal(AuthenticationMode.Disabled, options.Authentication.Mode);
|
||||||
Assert.Equal(@"C:\Gateway\MxGateway.Worker.exe", options.Worker.ExecutablePath);
|
Assert.Equal(@"C:\Gateway\MxGateway.Worker.exe", options.Worker.ExecutablePath);
|
||||||
Assert.Equal(12, options.Sessions.MaxSessions);
|
Assert.Equal(12, options.Sessions.MaxSessions);
|
||||||
|
Assert.Equal(900, options.Sessions.DefaultLeaseSeconds);
|
||||||
Assert.Equal(256, options.Events.QueueCapacity);
|
Assert.Equal(256, options.Events.QueueCapacity);
|
||||||
Assert.False(options.Dashboard.Enabled);
|
Assert.False(options.Dashboard.Enabled);
|
||||||
|
Assert.Equal(8 * 1024 * 1024, options.Protocol.MaxGrpcMessageBytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData("MxGateway:Worker:ExecutablePath", "worker.dll", "MxGateway:Worker:ExecutablePath must point to a .exe file.")]
|
[InlineData("MxGateway:Worker:ExecutablePath", "worker.dll", "MxGateway:Worker:ExecutablePath must point to a .exe file.")]
|
||||||
[InlineData("MxGateway:Worker:StartupProbeRetryAttempts", "0", "MxGateway:Worker:StartupProbeRetryAttempts must be greater than zero.")]
|
[InlineData("MxGateway:Worker:StartupProbeRetryAttempts", "0", "MxGateway:Worker:StartupProbeRetryAttempts must be greater than zero.")]
|
||||||
[InlineData("MxGateway:Worker:PipeConnectAttemptTimeoutMilliseconds", "0", "MxGateway:Worker:PipeConnectAttemptTimeoutMilliseconds must be greater than zero.")]
|
[InlineData("MxGateway:Worker:PipeConnectAttemptTimeoutMilliseconds", "0", "MxGateway:Worker:PipeConnectAttemptTimeoutMilliseconds must be greater than zero.")]
|
||||||
|
[InlineData("MxGateway:Sessions:DefaultLeaseSeconds", "0", "MxGateway:Sessions:DefaultLeaseSeconds must be greater than zero.")]
|
||||||
|
[InlineData("MxGateway:Sessions:LeaseSweepIntervalSeconds", "0", "MxGateway:Sessions:LeaseSweepIntervalSeconds must be greater than zero.")]
|
||||||
[InlineData("MxGateway:Events:QueueCapacity", "0", "MxGateway:Events:QueueCapacity must be greater than zero.")]
|
[InlineData("MxGateway:Events:QueueCapacity", "0", "MxGateway:Events:QueueCapacity must be greater than zero.")]
|
||||||
|
[InlineData("MxGateway:Protocol:MaxGrpcMessageBytes", "0", "MxGateway:Protocol:MaxGrpcMessageBytes must be between")]
|
||||||
[InlineData("MxGateway:Authentication:PepperSecretName", "", "MxGateway:Authentication:PepperSecretName is required")]
|
[InlineData("MxGateway:Authentication:PepperSecretName", "", "MxGateway:Authentication:PepperSecretName is required")]
|
||||||
[InlineData("MxGateway:Dashboard:PathBase", "dashboard", "MxGateway:Dashboard:PathBase must start with '/'.")]
|
[InlineData("MxGateway:Dashboard:PathBase", "dashboard", "MxGateway:Dashboard:PathBase must start with '/'.")]
|
||||||
public void Validation_InvalidConfiguration_FailsClearly(string key, string value, string expectedFailure)
|
public void Validation_InvalidConfiguration_FailsClearly(string key, string value, string expectedFailure)
|
||||||
|
|||||||
@@ -11,9 +11,9 @@ public sealed class GatewayContractInfoTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void GatewayProtocolVersion_StartsAtVersionOne()
|
public void GatewayProtocolVersion_IsVersionTwo()
|
||||||
{
|
{
|
||||||
Assert.Equal(1u, GatewayContractInfo.GatewayProtocolVersion);
|
Assert.Equal(2u, GatewayContractInfo.GatewayProtocolVersion);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ public sealed class GalaxyHierarchyCacheTests
|
|||||||
Assert.Equal(GalaxyCacheStatus.Unknown, entry.Status);
|
Assert.Equal(GalaxyCacheStatus.Unknown, entry.Status);
|
||||||
Assert.False(entry.HasData);
|
Assert.False(entry.HasData);
|
||||||
Assert.Equal(0, entry.ObjectCount);
|
Assert.Equal(0, entry.ObjectCount);
|
||||||
Assert.Null(entry.Reply);
|
Assert.Empty(entry.Objects);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
using MxGateway.Server.Dashboard;
|
||||||
|
|
||||||
|
namespace MxGateway.Tests.Gateway.Dashboard;
|
||||||
|
|
||||||
|
public sealed class DashboardConnectionStringDisplayTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void GalaxyRepositoryConnectionString_WithSqlCredentials_OnlyKeepsNonSecretFields()
|
||||||
|
{
|
||||||
|
string display = DashboardConnectionStringDisplay.GalaxyRepositoryConnectionString(
|
||||||
|
"Server=localhost;Database=ZB;User ID=mxuser;Password=secret;Encrypt=True;Trust Server Certificate=False;");
|
||||||
|
|
||||||
|
Assert.Contains("Data Source=localhost", display, StringComparison.Ordinal);
|
||||||
|
Assert.Contains("Initial Catalog=ZB", display, StringComparison.Ordinal);
|
||||||
|
Assert.Contains("Encrypt=True", display, StringComparison.Ordinal);
|
||||||
|
Assert.DoesNotContain("User", display, StringComparison.OrdinalIgnoreCase);
|
||||||
|
Assert.DoesNotContain("Password", display, StringComparison.OrdinalIgnoreCase);
|
||||||
|
Assert.DoesNotContain("secret", display, StringComparison.OrdinalIgnoreCase);
|
||||||
|
Assert.DoesNotContain("mxuser", display, StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -182,17 +182,27 @@ public sealed class DashboardSnapshotServiceTests
|
|||||||
LastQueriedAt = DateTimeOffset.Parse("2026-04-28T11:30:00Z"),
|
LastQueriedAt = DateTimeOffset.Parse("2026-04-28T11:30:00Z"),
|
||||||
LastSuccessAt = DateTimeOffset.Parse("2026-04-28T11:30:00Z"),
|
LastSuccessAt = DateTimeOffset.Parse("2026-04-28T11:30:00Z"),
|
||||||
LastDeployTime = DateTimeOffset.Parse("2026-04-28T09:00:00Z"),
|
LastDeployTime = DateTimeOffset.Parse("2026-04-28T09:00:00Z"),
|
||||||
Hierarchy =
|
DashboardSummary = new DashboardGalaxySummary(
|
||||||
[
|
DashboardGalaxyStatus.Healthy,
|
||||||
new GalaxyHierarchyRow { GobjectId = 1, TagName = "Pump_001", BrowseName = "Pump_001", CategoryId = 10, IsArea = false, TemplateChain = ["$Pump"] },
|
LastQueriedAt: DateTimeOffset.Parse("2026-04-28T11:30:00Z"),
|
||||||
new GalaxyHierarchyRow { GobjectId = 2, TagName = "Pump_002", BrowseName = "Pump_002", CategoryId = 10, IsArea = false, TemplateChain = ["$Pump"] },
|
LastSuccessAt: DateTimeOffset.Parse("2026-04-28T11:30:00Z"),
|
||||||
new GalaxyHierarchyRow { GobjectId = 3, TagName = "Area_A", BrowseName = "Area_A", CategoryId = 13, IsArea = true, TemplateChain = ["$Area"] },
|
LastDeployTime: DateTimeOffset.Parse("2026-04-28T09:00:00Z"),
|
||||||
],
|
LastError: null,
|
||||||
Attributes =
|
ObjectCount: 3,
|
||||||
[
|
AreaCount: 1,
|
||||||
new GalaxyAttributeRow { GobjectId = 1, AttributeName = "Speed", IsHistorized = true },
|
AttributeCount: 2,
|
||||||
new GalaxyAttributeRow { GobjectId = 1, AttributeName = "Status", IsAlarm = true },
|
HistorizedAttributeCount: 1,
|
||||||
],
|
AlarmAttributeCount: 1,
|
||||||
|
TopTemplates:
|
||||||
|
[
|
||||||
|
new DashboardGalaxyTemplateUsage("$Pump", 2),
|
||||||
|
new DashboardGalaxyTemplateUsage("$Area", 1),
|
||||||
|
],
|
||||||
|
ObjectCategories:
|
||||||
|
[
|
||||||
|
new DashboardGalaxyCategoryCount(10, "UserDefined", 2),
|
||||||
|
new DashboardGalaxyCategoryCount(13, "Area", 1),
|
||||||
|
]),
|
||||||
ObjectCount = 3,
|
ObjectCount = 3,
|
||||||
AreaCount = 1,
|
AreaCount = 1,
|
||||||
AttributeCount = 2,
|
AttributeCount = 2,
|
||||||
|
|||||||
@@ -0,0 +1,170 @@
|
|||||||
|
using Grpc.Core;
|
||||||
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
|
using MxGateway.Contracts.Proto.Galaxy;
|
||||||
|
using MxGateway.Server.Dashboard;
|
||||||
|
using MxGateway.Server.Galaxy;
|
||||||
|
using MxGateway.Server.Grpc;
|
||||||
|
|
||||||
|
namespace MxGateway.Tests.Gateway.Grpc;
|
||||||
|
|
||||||
|
public sealed class GalaxyRepositoryGrpcServiceTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public async Task DiscoverHierarchy_ReturnsRequestedPageAndTotals()
|
||||||
|
{
|
||||||
|
GalaxyRepositoryGrpcService service = CreateService(CreateEntry(CreateObjects(3)));
|
||||||
|
|
||||||
|
DiscoverHierarchyReply reply = await service.DiscoverHierarchy(
|
||||||
|
new DiscoverHierarchyRequest
|
||||||
|
{
|
||||||
|
PageSize = 2,
|
||||||
|
},
|
||||||
|
new TestServerCallContext());
|
||||||
|
|
||||||
|
Assert.Equal(2, reply.Objects.Count);
|
||||||
|
Assert.Equal("Object_001", reply.Objects[0].TagName);
|
||||||
|
Assert.Equal("Object_002", reply.Objects[1].TagName);
|
||||||
|
Assert.Equal("2", reply.NextPageToken);
|
||||||
|
Assert.Equal(3, reply.TotalObjectCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task DiscoverHierarchy_WithNextPageToken_ReturnsRemainingObjects()
|
||||||
|
{
|
||||||
|
GalaxyRepositoryGrpcService service = CreateService(CreateEntry(CreateObjects(3)));
|
||||||
|
|
||||||
|
DiscoverHierarchyReply reply = await service.DiscoverHierarchy(
|
||||||
|
new DiscoverHierarchyRequest
|
||||||
|
{
|
||||||
|
PageSize = 2,
|
||||||
|
PageToken = "2",
|
||||||
|
},
|
||||||
|
new TestServerCallContext());
|
||||||
|
|
||||||
|
GalaxyObject item = Assert.Single(reply.Objects);
|
||||||
|
Assert.Equal("Object_003", item.TagName);
|
||||||
|
Assert.Equal("", reply.NextPageToken);
|
||||||
|
Assert.Equal(3, reply.TotalObjectCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("-1", 1)]
|
||||||
|
[InlineData("not-an-offset", 1)]
|
||||||
|
[InlineData("4", 1)]
|
||||||
|
[InlineData("", -1)]
|
||||||
|
public async Task DiscoverHierarchy_WithInvalidPagingArguments_ReturnsInvalidArgument(
|
||||||
|
string pageToken,
|
||||||
|
int pageSize)
|
||||||
|
{
|
||||||
|
GalaxyRepositoryGrpcService service = CreateService(CreateEntry(CreateObjects(3)));
|
||||||
|
|
||||||
|
RpcException exception = await Assert.ThrowsAsync<RpcException>(
|
||||||
|
async () => await service.DiscoverHierarchy(
|
||||||
|
new DiscoverHierarchyRequest
|
||||||
|
{
|
||||||
|
PageSize = pageSize,
|
||||||
|
PageToken = pageToken,
|
||||||
|
},
|
||||||
|
new TestServerCallContext()));
|
||||||
|
|
||||||
|
Assert.Equal(StatusCode.InvalidArgument, exception.StatusCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static GalaxyRepositoryGrpcService CreateService(GalaxyHierarchyCacheEntry entry)
|
||||||
|
{
|
||||||
|
GalaxyRepositoryOptions options = new()
|
||||||
|
{
|
||||||
|
ConnectionString = "Server=localhost;Database=ZB;Integrated Security=True;Encrypt=False;",
|
||||||
|
};
|
||||||
|
return new GalaxyRepositoryGrpcService(
|
||||||
|
new global::MxGateway.Server.Galaxy.GalaxyRepository(options),
|
||||||
|
new StubGalaxyHierarchyCache(entry),
|
||||||
|
new GalaxyDeployNotifier(),
|
||||||
|
NullLogger<GalaxyRepositoryGrpcService>.Instance);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static GalaxyHierarchyCacheEntry CreateEntry(IReadOnlyList<GalaxyObject> objects)
|
||||||
|
{
|
||||||
|
return GalaxyHierarchyCacheEntry.Empty with
|
||||||
|
{
|
||||||
|
Status = GalaxyCacheStatus.Healthy,
|
||||||
|
LastSuccessAt = DateTimeOffset.UtcNow,
|
||||||
|
Objects = objects,
|
||||||
|
DashboardSummary = DashboardGalaxySummary.Unknown with
|
||||||
|
{
|
||||||
|
Status = DashboardGalaxyStatus.Healthy,
|
||||||
|
ObjectCount = objects.Count,
|
||||||
|
},
|
||||||
|
ObjectCount = objects.Count,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IReadOnlyList<GalaxyObject> CreateObjects(int count)
|
||||||
|
{
|
||||||
|
return Enumerable.Range(1, count)
|
||||||
|
.Select(index => new GalaxyObject
|
||||||
|
{
|
||||||
|
GobjectId = index,
|
||||||
|
TagName = $"Object_{index:000}",
|
||||||
|
BrowseName = $"Object_{index:000}",
|
||||||
|
})
|
||||||
|
.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class StubGalaxyHierarchyCache(GalaxyHierarchyCacheEntry current) : IGalaxyHierarchyCache
|
||||||
|
{
|
||||||
|
public GalaxyHierarchyCacheEntry Current { get; } = current;
|
||||||
|
|
||||||
|
public Task RefreshAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||||
|
|
||||||
|
public Task WaitForFirstLoadAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class TestServerCallContext(CancellationToken cancellationToken = default) : ServerCallContext
|
||||||
|
{
|
||||||
|
private readonly Metadata requestHeaders = [];
|
||||||
|
private readonly Metadata responseTrailers = [];
|
||||||
|
private readonly Dictionary<object, object> userState = [];
|
||||||
|
private Status status;
|
||||||
|
private WriteOptions? writeOptions;
|
||||||
|
|
||||||
|
protected override string MethodCore => "/galaxy_repository.v1.GalaxyRepository/DiscoverHierarchy";
|
||||||
|
|
||||||
|
protected override string HostCore => "localhost";
|
||||||
|
|
||||||
|
protected override string PeerCore => "ipv4:127.0.0.1:5000";
|
||||||
|
|
||||||
|
protected override DateTime DeadlineCore => DateTime.UtcNow.AddMinutes(1);
|
||||||
|
|
||||||
|
protected override Metadata RequestHeadersCore => requestHeaders;
|
||||||
|
|
||||||
|
protected override CancellationToken CancellationTokenCore => cancellationToken;
|
||||||
|
|
||||||
|
protected override Metadata ResponseTrailersCore => responseTrailers;
|
||||||
|
|
||||||
|
protected override Status StatusCore
|
||||||
|
{
|
||||||
|
get => status;
|
||||||
|
set => status = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override WriteOptions? WriteOptionsCore
|
||||||
|
{
|
||||||
|
get => writeOptions;
|
||||||
|
set => writeOptions = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override AuthContext AuthContextCore { get; } = new(
|
||||||
|
string.Empty,
|
||||||
|
new Dictionary<string, List<AuthProperty>>(StringComparer.Ordinal));
|
||||||
|
|
||||||
|
protected override IDictionary<object, object> UserStateCore => userState;
|
||||||
|
|
||||||
|
protected override Task WriteResponseHeadersAsyncCore(Metadata responseHeaders) => Task.CompletedTask;
|
||||||
|
|
||||||
|
protected override ContextPropagationToken CreatePropagationTokenCore(ContextPropagationOptions? options)
|
||||||
|
{
|
||||||
|
throw new NotSupportedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -32,6 +32,21 @@ public sealed class SessionManagerTests
|
|||||||
Assert.Equal(1, metrics.GetSnapshot().SessionsOpened);
|
Assert.Equal(1, metrics.GetSnapshot().SessionsOpened);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task OpenSessionAsync_SetsInitialDefaultLease()
|
||||||
|
{
|
||||||
|
ManualTimeProvider clock = new(DateTimeOffset.Parse("2026-04-29T10:00:00Z"));
|
||||||
|
GatewayOptions options = CreateOptions(defaultLeaseSeconds: 1800);
|
||||||
|
SessionManager manager = CreateManager(
|
||||||
|
new FakeSessionWorkerClientFactory(new FakeWorkerClient()),
|
||||||
|
options: options,
|
||||||
|
timeProvider: clock);
|
||||||
|
|
||||||
|
GatewaySession session = await manager.OpenSessionAsync(CreateOpenRequest(), "client-1", CancellationToken.None);
|
||||||
|
|
||||||
|
Assert.Equal(clock.GetUtcNow() + TimeSpan.FromMinutes(30), session.LeaseExpiresAt);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task OpenSessionAsync_GeneratesClientCorrelationIdFromClientNameAndSessionId()
|
public async Task OpenSessionAsync_GeneratesClientCorrelationIdFromClientNameAndSessionId()
|
||||||
{
|
{
|
||||||
@@ -77,6 +92,32 @@ public sealed class SessionManagerTests
|
|||||||
Assert.Equal(MxCommandKind.Ping, reply.Reply.Kind);
|
Assert.Equal(MxCommandKind.Ping, reply.Reply.Kind);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task InvokeAsync_WhenSessionReady_RefreshesLease()
|
||||||
|
{
|
||||||
|
GatewaySession session = new(
|
||||||
|
"session-lease-refresh",
|
||||||
|
"mxaccess",
|
||||||
|
"mxaccess-gateway-1-session-lease-refresh",
|
||||||
|
"nonce",
|
||||||
|
"client-1",
|
||||||
|
"test-session",
|
||||||
|
"client-correlation-1",
|
||||||
|
TimeSpan.FromSeconds(30),
|
||||||
|
TimeSpan.FromSeconds(5),
|
||||||
|
TimeSpan.FromSeconds(5),
|
||||||
|
TimeSpan.FromMinutes(30),
|
||||||
|
DateTimeOffset.UtcNow - TimeSpan.FromHours(1));
|
||||||
|
session.AttachWorkerClient(new FakeWorkerClient());
|
||||||
|
session.MarkReady();
|
||||||
|
DateTimeOffset? initialLease = session.LeaseExpiresAt;
|
||||||
|
|
||||||
|
await session.InvokeAsync(CreateCommand(MxCommandKind.Ping), CancellationToken.None);
|
||||||
|
|
||||||
|
Assert.True(session.LeaseExpiresAt > initialLease);
|
||||||
|
Assert.True(session.LeaseExpiresAt > DateTimeOffset.UtcNow);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task GatewaySessionSubscribeBulkAsync_ForwardsOneBulkCommandAndReturnsResults()
|
public async Task GatewaySessionSubscribeBulkAsync_ForwardsOneBulkCommandAndReturnsResults()
|
||||||
{
|
{
|
||||||
@@ -309,6 +350,23 @@ public sealed class SessionManagerTests
|
|||||||
Assert.Equal(0, activeClient.ShutdownCount);
|
Assert.Equal(0, activeClient.ShutdownCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task CloseExpiredLeasesAsync_DoesNotCloseActiveEventSubscriber()
|
||||||
|
{
|
||||||
|
FakeWorkerClient workerClient = new();
|
||||||
|
SessionManager manager = CreateManager(new FakeSessionWorkerClientFactory(workerClient));
|
||||||
|
GatewaySession session = await manager.OpenSessionAsync(CreateOpenRequest(), "client-1", CancellationToken.None);
|
||||||
|
DateTimeOffset now = DateTimeOffset.UtcNow;
|
||||||
|
session.ExtendLease(now.AddSeconds(-1));
|
||||||
|
using IDisposable eventSubscriber = session.AttachEventSubscriber(allowMultipleSubscribers: false);
|
||||||
|
|
||||||
|
int closedCount = await manager.CloseExpiredLeasesAsync(now, CancellationToken.None);
|
||||||
|
|
||||||
|
Assert.Equal(0, closedCount);
|
||||||
|
Assert.Equal(SessionState.Ready, session.State);
|
||||||
|
Assert.Equal(0, workerClient.ShutdownCount);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task ShutdownAsync_ClosesAllRegisteredSessions()
|
public async Task ShutdownAsync_ClosesAllRegisteredSessions()
|
||||||
{
|
{
|
||||||
@@ -334,16 +392,20 @@ public sealed class SessionManagerTests
|
|||||||
ISessionWorkerClientFactory factory,
|
ISessionWorkerClientFactory factory,
|
||||||
ISessionRegistry? registry = null,
|
ISessionRegistry? registry = null,
|
||||||
GatewayMetrics? metrics = null,
|
GatewayMetrics? metrics = null,
|
||||||
GatewayOptions? options = null)
|
GatewayOptions? options = null,
|
||||||
|
TimeProvider? timeProvider = null)
|
||||||
{
|
{
|
||||||
return new SessionManager(
|
return new SessionManager(
|
||||||
registry ?? new SessionRegistry(),
|
registry ?? new SessionRegistry(),
|
||||||
factory,
|
factory,
|
||||||
Options.Create(options ?? CreateOptions()),
|
Options.Create(options ?? CreateOptions()),
|
||||||
metrics ?? new GatewayMetrics());
|
metrics ?? new GatewayMetrics(),
|
||||||
|
timeProvider);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static GatewayOptions CreateOptions(int maxSessions = 64)
|
private static GatewayOptions CreateOptions(
|
||||||
|
int maxSessions = 64,
|
||||||
|
int defaultLeaseSeconds = 1800)
|
||||||
{
|
{
|
||||||
return new GatewayOptions
|
return new GatewayOptions
|
||||||
{
|
{
|
||||||
@@ -351,6 +413,7 @@ public sealed class SessionManagerTests
|
|||||||
{
|
{
|
||||||
DefaultCommandTimeoutSeconds = 30,
|
DefaultCommandTimeoutSeconds = 30,
|
||||||
MaxSessions = maxSessions,
|
MaxSessions = maxSessions,
|
||||||
|
DefaultLeaseSeconds = defaultLeaseSeconds,
|
||||||
},
|
},
|
||||||
Worker = new WorkerOptions
|
Worker = new WorkerOptions
|
||||||
{
|
{
|
||||||
@@ -540,4 +603,11 @@ public sealed class SessionManagerTests
|
|||||||
ShutdownReleased.TrySetResult();
|
ShutdownReleased.TrySetResult();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private sealed class ManualTimeProvider(DateTimeOffset start) : TimeProvider
|
||||||
|
{
|
||||||
|
private DateTimeOffset _now = start;
|
||||||
|
|
||||||
|
public override DateTimeOffset GetUtcNow() => _now;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -137,6 +137,36 @@ public sealed class WorkerClientTests
|
|||||||
Assert.Equal(WorkerClientState.Faulted, client.State);
|
Assert.Equal(WorkerClientState.Faulted, client.State);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ReadLoop_WhenClientFaults_KillsOwnedWorkerProcess()
|
||||||
|
{
|
||||||
|
await using PipePair pipePair = await PipePair.CreateAsync();
|
||||||
|
FakeWorkerProcess process = new();
|
||||||
|
await using WorkerClient client = CreateClient(
|
||||||
|
pipePair,
|
||||||
|
new WorkerClientOptions
|
||||||
|
{
|
||||||
|
EventChannelCapacity = 1,
|
||||||
|
HeartbeatGrace = TimeSpan.FromSeconds(30),
|
||||||
|
HeartbeatCheckInterval = TimeSpan.FromSeconds(30),
|
||||||
|
},
|
||||||
|
processHandle: CreateProcessHandle(process));
|
||||||
|
await CompleteHandshakeAsync(client, pipePair);
|
||||||
|
|
||||||
|
await pipePair.WorkerWriter.WriteAsync(
|
||||||
|
CreateEventEnvelope(sequence: 11, MxEventFamily.OnDataChange));
|
||||||
|
await pipePair.WorkerWriter.WriteAsync(
|
||||||
|
CreateEventEnvelope(sequence: 12, MxEventFamily.OnDataChange));
|
||||||
|
|
||||||
|
await WaitUntilAsync(
|
||||||
|
() => client.State == WorkerClientState.Faulted,
|
||||||
|
TestTimeout);
|
||||||
|
|
||||||
|
Assert.Equal(1, process.KillCount);
|
||||||
|
Assert.True(process.KillEntireProcessTree);
|
||||||
|
Assert.True(process.HasExited);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task ReadLoop_WhenPipeDisconnects_FaultsClient()
|
public async Task ReadLoop_WhenPipeDisconnects_FaultsClient()
|
||||||
{
|
{
|
||||||
@@ -191,6 +221,20 @@ public sealed class WorkerClientTests
|
|||||||
$"DisposeAsync took {elapsed.TotalMilliseconds:N0}ms.");
|
$"DisposeAsync took {elapsed.TotalMilliseconds:N0}ms.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task DisposeAsync_WhenOwnedWorkerStillRuns_KillsProcessBeforeDisposing()
|
||||||
|
{
|
||||||
|
await using PipePair pipePair = await PipePair.CreateAsync();
|
||||||
|
FakeWorkerProcess process = new();
|
||||||
|
WorkerClient client = CreateClient(pipePair, processHandle: CreateProcessHandle(process));
|
||||||
|
|
||||||
|
await client.DisposeAsync().AsTask().WaitAsync(TestTimeout);
|
||||||
|
|
||||||
|
Assert.Equal(1, process.KillCount);
|
||||||
|
Assert.True(process.KillEntireProcessTree);
|
||||||
|
Assert.True(process.Disposed);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task ReadLoop_WhenHeartbeatArrives_UpdatesLastHeartbeatAndWorkerProcess()
|
public async Task ReadLoop_WhenHeartbeatArrives_UpdatesLastHeartbeatAndWorkerProcess()
|
||||||
{
|
{
|
||||||
@@ -233,18 +277,28 @@ public sealed class WorkerClientTests
|
|||||||
private static WorkerClient CreateClient(
|
private static WorkerClient CreateClient(
|
||||||
PipePair pipePair,
|
PipePair pipePair,
|
||||||
WorkerClientOptions? options = null,
|
WorkerClientOptions? options = null,
|
||||||
GatewayMetrics? metrics = null)
|
GatewayMetrics? metrics = null,
|
||||||
|
WorkerProcessHandle? processHandle = null)
|
||||||
{
|
{
|
||||||
WorkerFrameProtocolOptions frameOptions = new(SessionId);
|
WorkerFrameProtocolOptions frameOptions = new(SessionId);
|
||||||
WorkerClientConnection connection = new(
|
WorkerClientConnection connection = new(
|
||||||
SessionId,
|
SessionId,
|
||||||
Nonce,
|
Nonce,
|
||||||
pipePair.GatewayStream,
|
pipePair.GatewayStream,
|
||||||
frameOptions);
|
frameOptions,
|
||||||
|
processHandle);
|
||||||
|
|
||||||
return new WorkerClient(connection, options, metrics);
|
return new WorkerClient(connection, options, metrics);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static WorkerProcessHandle CreateProcessHandle(FakeWorkerProcess process)
|
||||||
|
{
|
||||||
|
return new WorkerProcessHandle(
|
||||||
|
process,
|
||||||
|
new WorkerProcessCommandLine("MxGateway.Worker.exe", []),
|
||||||
|
DateTimeOffset.UtcNow);
|
||||||
|
}
|
||||||
|
|
||||||
private static async Task CompleteHandshakeAsync(
|
private static async Task CompleteHandshakeAsync(
|
||||||
WorkerClient client,
|
WorkerClient client,
|
||||||
PipePair pipePair)
|
PipePair pipePair)
|
||||||
@@ -438,4 +492,40 @@ public sealed class WorkerClientTests
|
|||||||
await GatewayStream.DisposeAsync();
|
await GatewayStream.DisposeAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private sealed class FakeWorkerProcess : IWorkerProcess
|
||||||
|
{
|
||||||
|
private readonly TaskCompletionSource _exited = new(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||||
|
|
||||||
|
public int Id { get; } = WorkerProcessId;
|
||||||
|
|
||||||
|
public bool HasExited { get; private set; }
|
||||||
|
|
||||||
|
public int? ExitCode { get; private set; }
|
||||||
|
|
||||||
|
public int KillCount { get; private set; }
|
||||||
|
|
||||||
|
public bool KillEntireProcessTree { get; private set; }
|
||||||
|
|
||||||
|
public bool Disposed { get; private set; }
|
||||||
|
|
||||||
|
public ValueTask WaitForExitAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
return new ValueTask(_exited.Task.WaitAsync(cancellationToken));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Kill(bool entireProcessTree)
|
||||||
|
{
|
||||||
|
KillCount++;
|
||||||
|
KillEntireProcessTree = entireProcessTree;
|
||||||
|
HasExited = true;
|
||||||
|
ExitCode = -1;
|
||||||
|
_exited.TrySetResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
Disposed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user