Compare commits

..

2 Commits

Author SHA1 Message Date
Joseph Doherty 8a1f037d5a fix(gateway): resolve array attribute constraints by bare name via [] fallback 2026-06-18 03:25:48 -04:00
Joseph Doherty e328758c53 fix(client-python): regenerate only MxSparseArray delta, drop spurious grpcio version churn
Commit 0702551 regenerated all three protos with grpcio-tools 1.81.1 /
protobuf 6.33.5, stamping every _pb2_grpc.py with GRPC_GENERATED_VERSION
'1.81.1' and every _pb2.py with 'Protobuf Python Version: 6.33.5'.  The
project pins grpcio>=1.80,<2 and the installed runtime is 1.80.0, so the
1.81.1 stamp caused a version error at import time, breaking pytest.

Fix: restore all generated files to the main baseline (grpcio 1.80.0,
protobuf 6.31.1 stamps), then regenerate only mxaccess_gateway.proto
using an isolated venv pinned to grpcio==1.80.0, grpcio-tools==1.80.0,
protobuf==6.31.1.  The net diff vs main is a single-file change to
mxaccess_gateway_pb2.py whose DESCRIPTOR blob now encodes the new
MxSparseArray / MxSparseElement messages and sparse_array_value field.
No _pb2_grpc.py and no galaxy/worker _pb2.py files change.
2026-06-18 03:19:12 -04:00
9 changed files with 212 additions and 23 deletions
@@ -2,7 +2,7 @@
# Generated by the protocol buffer compiler. DO NOT EDIT!
# NO CHECKED-IN PROTOBUF GENCODE
# source: galaxy_repository.proto
# Protobuf Python Version: 6.33.5
# Protobuf Python Version: 6.31.1
"""Generated protocol buffer code."""
from google.protobuf import descriptor as _descriptor
from google.protobuf import descriptor_pool as _descriptor_pool
@@ -12,8 +12,8 @@ from google.protobuf.internal import builder as _builder
_runtime_version.ValidateProtobufRuntimeVersion(
_runtime_version.Domain.PUBLIC,
6,
33,
5,
31,
1,
'',
'galaxy_repository.proto'
)
@@ -5,7 +5,7 @@ import warnings
import galaxy_repository_pb2 as galaxy__repository__pb2
GRPC_GENERATED_VERSION = '1.81.1'
GRPC_GENERATED_VERSION = '1.80.0'
GRPC_VERSION = grpc.__version__
_version_not_supported = False
@@ -25,7 +25,7 @@ if _version_not_supported:
)
class GalaxyRepositoryStub:
class GalaxyRepositoryStub(object):
"""Wire-compatibility policy (ProtobufStyleGuide): this contract evolves
additively only. Never renumber or repurpose an existing field number or
enum value. When a field or enum value is removed, add a `reserved` range
@@ -72,7 +72,7 @@ class GalaxyRepositoryStub:
_registered_method=True)
class GalaxyRepositoryServicer:
class GalaxyRepositoryServicer(object):
"""Wire-compatibility policy (ProtobufStyleGuide): this contract evolves
additively only. Never renumber or repurpose an existing field number or
enum value. When a field or enum value is removed, add a `reserved` range
@@ -162,7 +162,7 @@ def add_GalaxyRepositoryServicer_to_server(servicer, server):
# This class is part of an EXPERIMENTAL API.
class GalaxyRepository:
class GalaxyRepository(object):
"""Wire-compatibility policy (ProtobufStyleGuide): this contract evolves
additively only. Never renumber or repurpose an existing field number or
enum value. When a field or enum value is removed, add a `reserved` range
@@ -2,7 +2,7 @@
# Generated by the protocol buffer compiler. DO NOT EDIT!
# NO CHECKED-IN PROTOBUF GENCODE
# source: mxaccess_gateway.proto
# Protobuf Python Version: 6.33.5
# Protobuf Python Version: 6.31.1
"""Generated protocol buffer code."""
from google.protobuf import descriptor as _descriptor
from google.protobuf import descriptor_pool as _descriptor_pool
@@ -12,8 +12,8 @@ from google.protobuf.internal import builder as _builder
_runtime_version.ValidateProtobufRuntimeVersion(
_runtime_version.Domain.PUBLIC,
6,
33,
5,
31,
1,
'',
'mxaccess_gateway.proto'
)
@@ -5,7 +5,7 @@ import warnings
import mxaccess_gateway_pb2 as mxaccess__gateway__pb2
GRPC_GENERATED_VERSION = '1.81.1'
GRPC_GENERATED_VERSION = '1.80.0'
GRPC_VERSION = grpc.__version__
_version_not_supported = False
@@ -25,7 +25,7 @@ if _version_not_supported:
)
class MxAccessGatewayStub:
class MxAccessGatewayStub(object):
"""Wire-compatibility policy (ProtobufStyleGuide): this contract evolves
additively only. Never renumber or repurpose an existing field number or
enum value. When a field or enum value is removed, add a `reserved` range
@@ -78,7 +78,7 @@ class MxAccessGatewayStub:
_registered_method=True)
class MxAccessGatewayServicer:
class MxAccessGatewayServicer(object):
"""Wire-compatibility policy (ProtobufStyleGuide): this contract evolves
additively only. Never renumber or repurpose an existing field number or
enum value. When a field or enum value is removed, add a `reserved` range
@@ -189,7 +189,7 @@ def add_MxAccessGatewayServicer_to_server(servicer, server):
# This class is part of an EXPERIMENTAL API.
class MxAccessGateway:
class MxAccessGateway(object):
"""Wire-compatibility policy (ProtobufStyleGuide): this contract evolves
additively only. Never renumber or repurpose an existing field number or
enum value. When a field or enum value is removed, add a `reserved` range
@@ -2,7 +2,7 @@
# Generated by the protocol buffer compiler. DO NOT EDIT!
# NO CHECKED-IN PROTOBUF GENCODE
# source: mxaccess_worker.proto
# Protobuf Python Version: 6.33.5
# Protobuf Python Version: 6.31.1
"""Generated protocol buffer code."""
from google.protobuf import descriptor as _descriptor
from google.protobuf import descriptor_pool as _descriptor_pool
@@ -12,8 +12,8 @@ from google.protobuf.internal import builder as _builder
_runtime_version.ValidateProtobufRuntimeVersion(
_runtime_version.Domain.PUBLIC,
6,
33,
5,
31,
1,
'',
'mxaccess_worker.proto'
)
@@ -4,7 +4,7 @@ import grpc
import warnings
GRPC_GENERATED_VERSION = '1.81.1'
GRPC_GENERATED_VERSION = '1.80.0'
GRPC_VERSION = grpc.__version__
_version_not_supported = False
@@ -196,11 +196,30 @@ public sealed class ConstraintEnforcer(
private GalaxyTagLookup? ResolveTarget(string tagAddress)
{
GalaxyHierarchyCacheEntry entry = cache.Current;
return !string.IsNullOrWhiteSpace(tagAddress)
&& entry.Index.TagsByAddress.TryGetValue(tagAddress, out GalaxyTagLookup? lookup)
? lookup
: null;
if (string.IsNullOrWhiteSpace(tagAddress))
{
return null;
}
IReadOnlyDictionary<string, GalaxyTagLookup> tagsByAddress = cache.Current.Index.TagsByAddress;
if (tagsByAddress.TryGetValue(tagAddress, out GalaxyTagLookup? lookup))
{
return lookup;
}
// Galaxy SQL keys array attributes by their suffixed FullTagReference (e.g. "Obj.Arr[]"),
// but callers pass the bare address ("Obj.Arr") before the worker-boundary normalization
// runs. Probe the suffixed form so a bare array name resolves to its array attribute,
// consistent with ArrayAddressNormalizer. Only build the suffixed string on a direct miss
// when the address is not already suffixed, and only accept it when it is truly an array.
if (!tagAddress.EndsWith("[]", StringComparison.Ordinal)
&& tagsByAddress.TryGetValue(tagAddress + "[]", out GalaxyTagLookup? arrayLookup)
&& arrayLookup.Attribute?.IsArray == true)
{
return arrayLookup;
}
return null;
}
private static bool MatchesPathOrTag(
@@ -141,6 +141,112 @@ public sealed class GatewayArrayWriteWiringTests
Assert.Equal(new[] { 0, 7, 0, 0 }, forwarded.ArrayValue.Int32Values.Values);
}
/// <summary>
/// A bare array <c>AddItem2</c> address is normalized to its writable array form on the wire,
/// and the normalized address lands in the tracked <see cref="SessionItemRegistration"/>.
/// </summary>
[Fact]
public async Task AddItem2_BareArrayAddress_NormalizedOnWireAndInRegistration()
{
CapturingWorkerClient worker = new();
GatewaySession session = CreateReadySession(worker);
WorkerCommand command = new()
{
Command = new MxCommand
{
Kind = MxCommandKind.AddItem2,
AddItem2 = new AddItem2Command
{
ServerHandle = 1,
ItemDefinition = "Obj.Arr",
},
},
};
worker.NextReply = new WorkerCommandReply
{
Reply = new MxCommandReply
{
Kind = MxCommandKind.AddItem2,
ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.Ok },
AddItem2 = new AddItem2Reply { ItemHandle = 43 },
},
};
await session.InvokeAsync(command, CancellationToken.None);
Assert.NotNull(worker.LastCommand);
Assert.Equal("Obj.Arr[]", worker.LastCommand!.Command.AddItem2.ItemDefinition);
MxCommand trackingCopy = new()
{
Kind = MxCommandKind.AddItem2,
AddItem2 = new AddItem2Command
{
ServerHandle = 1,
ItemDefinition = "Obj.Arr",
},
};
session.TrackCommandReply(trackingCopy, worker.NextReply.Reply);
Assert.True(session.TryGetItemRegistration(1, 43, out SessionItemRegistration registration));
Assert.Equal("Obj.Arr[]", registration.TagAddress);
}
/// <summary>
/// A sparse-array entry in a <see cref="WriteBulkCommand"/> is expanded to a full,
/// default-filled <see cref="MxArray"/> before reaching the worker; no sparse value is ever
/// forwarded inside a bulk write.
/// </summary>
[Fact]
public async Task WriteBulk_SparseArrayEntryValue_ExpandedBeforeReachingWorker()
{
CapturingWorkerClient worker = new();
GatewaySession session = CreateReadySession(worker);
WorkerCommand command = new()
{
Command = new MxCommand
{
Kind = MxCommandKind.WriteBulk,
WriteBulk = new WriteBulkCommand
{
ServerHandle = 1,
Entries =
{
new WriteBulkEntry
{
ItemHandle = 42,
Value = new MxValue
{
SparseArrayValue = new MxSparseArray
{
ElementDataType = MxDataType.Integer,
TotalLength = 4,
Elements =
{
new MxSparseElement
{
Index = 1,
Value = new MxValue { Int32Value = 7 },
},
},
},
},
},
},
},
},
};
await session.InvokeAsync(command, CancellationToken.None);
MxValue forwarded = worker.LastCommand!.Command.WriteBulk.Entries[0].Value;
Assert.Equal(MxValue.KindOneofCase.ArrayValue, forwarded.KindCase);
Assert.Equal(new[] { 0, 7, 0, 0 }, forwarded.ArrayValue.Int32Values.Values);
}
private static GatewaySession CreateReadySession(IWorkerClient workerClient)
{
GatewaySession session = new(
@@ -207,6 +207,62 @@ public sealed class ConstraintEnforcerTests
Assert.Equal("read_historized_only", failure.ConstraintName);
}
/// <summary>
/// A bare array attribute address (no trailing <c>[]</c>) resolves through the Galaxy index
/// even though arrays are keyed by their suffixed FullTagReference (e.g. "Pump_001.Levels[]").
/// Without the <c>[]</c> fallback in ResolveTarget the bare name misses the index and a
/// read-constrained key gets a spurious tag_metadata denial for an AddItem it should allow.
/// </summary>
[Fact]
public async Task CheckReadTagAsync_WithBareArrayName_ResolvesViaArraySuffixFallback()
{
ConstraintEnforcer enforcer = CreateEnforcer(out _);
ApiKeyIdentity identity = CreateIdentity(ApiKeyConstraints.Empty with
{
// A read constraint that covers the Pump_001 subtree; the array attribute is inside it.
ReadTagGlobs = ["Pump_001.*"],
});
ConstraintFailure? failure = await enforcer.CheckReadTagAsync(
identity,
"Pump_001.Levels",
CancellationToken.None);
// Before the fix: bare "Pump_001.Levels" misses the index (keyed "Pump_001.Levels[]") and
// returns a tag_metadata failure. After the fix: it resolves and is within scope -> null.
Assert.Null(failure);
}
/// <summary>
/// A bare non-array name that is genuinely absent from the index still resolves to null:
/// the <c>[]</c> probe must not manufacture a false positive for a scalar/missing tag.
/// </summary>
[Fact]
public async Task CheckReadTagAsync_WithMissingNonArrayName_StillFailsToResolve()
{
ConstraintEnforcer enforcer = CreateEnforcer(out _);
ApiKeyIdentity identity = CreateIdentity(ApiKeyConstraints.Empty with
{
ReadTagGlobs = ["Pump_001.*"],
});
// "Pump_001.Scalar" is not in the index, and "Pump_001.Scalar[]" is not either, so the
// suffix probe must not resolve it. A genuinely-unknown name behaves the same.
ConstraintFailure? missingScalar = await enforcer.CheckReadTagAsync(
identity,
"Pump_001.Scalar",
CancellationToken.None);
Assert.NotNull(missingScalar);
Assert.Equal("tag_metadata", missingScalar.ConstraintName);
ConstraintFailure? unknown = await enforcer.CheckReadTagAsync(
identity,
"DoesNotExist_999.Whatever",
CancellationToken.None);
Assert.NotNull(unknown);
Assert.Equal("tag_metadata", unknown.ConstraintName);
}
private static ConstraintEnforcer CreateEnforcer(out FakeAuditWriter auditWriter)
{
auditWriter = new FakeAuditWriter();
@@ -276,6 +332,14 @@ public sealed class ConstraintEnforcerTests
AttributeName = "NonHistorized",
FullTagReference = "Pump_001.NonHistorized",
},
new GalaxyAttribute
{
// Galaxy SQL keys array attributes by their suffixed FullTagReference,
// so the index entry is "Pump_001.Levels[]", not the bare name.
AttributeName = "Levels",
FullTagReference = "Pump_001.Levels[]",
IsArray = true,
},
},
},
new GalaxyObject