5e375f6d3d
Adds five new MXAccess command kinds (WriteBulk, Write2Bulk,
WriteSecuredBulk, WriteSecured2Bulk, ReadBulk) that ride the existing
"one round-trip, per-entry results" bulk shape used by AddItemBulk and
SubscribeBulk today. MXAccess COM has no native bulk API; the worker
runs each bulk operation as a sequential loop on its STA, returning
one BulkWriteResult / BulkReadResult per requested entry so per-item
MXAccess failures surface as was_successful=false rather than throwing.
ReadBulk has no MXAccess analogue. The worker satisfies it by:
- Returning the last cached OnDataChange payload (was_cached=true)
when the requested tag is already in the session''s item registry
AND advised — the existing subscription is NOT touched, since the
caller did not create it.
- Otherwise taking the AddItem + Advise + wait-for-OnDataChange +
UnAdvise + RemoveItem snapshot lifecycle itself (was_cached=false)
and leaving the session exactly as it was. The wait pumps Windows
messages on the STA so the inbound MXAccess event can dispatch
while the executor still holds the thread.
The new MxAccessValueCache lives on each MxAccessSession, shared with
MxAccessBaseEventSink which populates it on every OnDataChange after
the event clears the outbound queue. Eviction on RemoveItem keeps
reused MXAccess handles from serving stale values from a previous
lifetime.
Gateway-side authorization wires WriteBulk/Write2Bulk to invoke:write,
WriteSecuredBulk/WriteSecured2Bulk to invoke:secure, ReadBulk to
invoke:read. The constraint-filter pipeline is refactored from a single
BulkConstraintPlan record into an abstract base plus three concretes
(SubscribeBulk, WriteBulk, ReadBulk), each owning its own denied-entry
merge so the dispatch site never branches on reply shape. A new
FilterWriteBulkAsync<TEntry> generic over the four write-entry shapes
runs CheckWriteHandleAsync per entry; denied entries surface as the
BulkWriteResult shape, preserving original-index order.
All five language clients (.NET, Go, Rust, Python, Java) gained the
five new methods following their existing bulk pattern, with regenerated
protobufs.
Tests added:
- MxAccessValueCacheTests (6 cases) — Set/TryGet, Remove resets the
version, TryWaitForUpdate signals on Set, pump step fires each poll.
- MxAccessBaseEventSinkTests — OnDataChange populates the cache,
ValueCache property exposes the bound instance.
- MxAccessCommandExecutorTests — four bulk-write variants (per-entry
success/failure, value+timestamp forwarding, secured user ids),
ReadBulk snapshot lifecycle on uncached tag (timeout surfaces as
was_successful=false), invalid-payload reply.
- GatewayGrpcScopeResolverTests — five new MxCommandKind cases.
- SessionManagerTests — WriteBulk and ReadBulk forwarding through
FakeWorkerHarness; ReadBulk forwards timeout_ms.
- Per-client (.NET, Go, Rust, Python, Java) — WriteBulk builds the
right command and returns per-entry results, ReadBulk forwards the
timeout and unpacks the was_cached flag.
Cross-language e2e CLI subcommands for the new bulks are deliberately
scoped out of this change (each of the five client CLIs would need
five new subcommands plus matching phases in
scripts/run-client-e2e-tests.ps1); coverage equivalent to the existing
bulk-subscribe coverage is provided by worker + gateway + per-client
unit tests.
Docs updated in the same commit: gateway.md (Public MXAccess Command
Surface), docs/DesignDecisions.md (new "Bulk Command Family" section
with the ReadBulk cache-then-snapshot rationale), and every client
README.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
242 lines
8.7 KiB
Markdown
242 lines
8.7 KiB
Markdown
# Go Client
|
|
|
|
The Go client module contains the generated MXAccess Gateway protobuf bindings,
|
|
a small handwritten `mxgateway` package, and the `mxgw-go` test CLI scaffold.
|
|
The module uses the shared proto inputs documented in
|
|
`../../docs/ClientProtoGeneration.md` so gateway and client contracts stay in
|
|
sync.
|
|
|
|
## Layout
|
|
|
|
```text
|
|
clients/go/
|
|
go.mod
|
|
generate-proto.ps1
|
|
internal/generated/
|
|
mxgateway/
|
|
cmd/mxgw-go/
|
|
```
|
|
|
|
`internal/generated` contains code produced by `protoc`, `protoc-gen-go`, and
|
|
`protoc-gen-go-grpc`. Do not edit generated files by hand.
|
|
|
|
## Regenerating Protobuf Bindings
|
|
|
|
Run generation after the shared `.proto` files or the Go output path changes:
|
|
|
|
```powershell
|
|
./generate-proto.ps1
|
|
```
|
|
|
|
The script uses the tool paths recorded in `../../docs/ToolchainLinks.md`.
|
|
|
|
## Build And Test
|
|
|
|
Run the Go module checks from `clients/go`:
|
|
|
|
```powershell
|
|
go test ./...
|
|
go build ./...
|
|
go vet ./...
|
|
```
|
|
|
|
The tests parse the shared JSON fixtures, exercise value and status conversion,
|
|
use `bufconn` for fake gateway auth and streaming behavior, and cover CLI JSON
|
|
redaction.
|
|
|
|
## Packaging
|
|
|
|
Build a local CLI executable from `clients/go`:
|
|
|
|
```powershell
|
|
New-Item -ItemType Directory -Force ../../artifacts/clients/go | Out-Null
|
|
go build -o ../../artifacts/clients/go/mxgw-go.exe ./cmd/mxgw-go
|
|
```
|
|
|
|
Install the CLI into the active `GOBIN` or `GOPATH/bin`:
|
|
|
|
```powershell
|
|
go install ./cmd/mxgw-go
|
|
```
|
|
|
|
Other Go modules can consume the library package with the module path
|
|
`gitea.dohertylan.com/dohertj2/mxaccessgw/clients/go/mxgateway`.
|
|
|
|
## Client API
|
|
|
|
Use `mxgateway.Dial` with `mxgateway.Options` to configure plaintext or TLS
|
|
transport, API-key metadata, dial timeout, and per-call timeout:
|
|
|
|
```go
|
|
client, err := mxgateway.Dial(ctx, mxgateway.Options{
|
|
Endpoint: "localhost:5000",
|
|
APIKey: os.Getenv("MXGATEWAY_API_KEY"),
|
|
Plaintext: true,
|
|
})
|
|
```
|
|
|
|
`Client.OpenSession` returns a `Session` with helpers for `Register`,
|
|
`AddItem`, `AddItem2`, `Advise`, `Write`, the full bulk family
|
|
(`AddItemBulk`, `AdviseItemBulk`, `RemoveItemBulk`, `UnAdviseItemBulk`,
|
|
`SubscribeBulk`, `UnsubscribeBulk`, `WriteBulk`, `Write2Bulk`,
|
|
`WriteSecuredBulk`, `WriteSecured2Bulk`, `ReadBulk`), `Events`, and
|
|
`Close`. Bulk variants carry a list of entries in one round-trip and
|
|
return one result per entry; per-entry MXAccess failures appear as
|
|
`was_successful = false` and never return as Go errors. `ReadBulk` accepts
|
|
a `time.Duration` per-tag timeout and returns cached `OnDataChange`
|
|
values when the tag is already advised (`WasCached = true`) without
|
|
touching the existing subscription. Prefer
|
|
`SubscribeEvents` or `SubscribeEventsAfter` for long-running streams because the
|
|
returned subscription owns cancellation and exposes `Close` for deterministic
|
|
goroutine cleanup. `Events` and `EventsAfter` are a compatibility shim with a
|
|
bounded internal buffer: if the consumer drains too slowly the buffer fills,
|
|
the underlying stream is cancelled, and a terminal `EventResult` carrying
|
|
`ErrEventBufferOverflow` is delivered as the channel's last item before it
|
|
closes — so a slow consumer can distinguish dropped events from a normal
|
|
end-of-stream. `SubscribeEvents` blocks instead of dropping, so use it when no
|
|
events may be lost. Raw protobuf messages remain available through the
|
|
`mxgateway` package aliases and the `Raw` helper methods. Typed errors support
|
|
`errors.As` for `GatewayError`, `CommandError`, and `MxAccessError`; command
|
|
errors preserve the raw reply.
|
|
|
|
`Dial` and `DialGalaxy` create the connection lazily (`grpc.NewClient`): a
|
|
gateway that is briefly unavailable no longer turns into a hard error — the
|
|
connection recovers once the gateway comes up. To keep fail-fast behavior,
|
|
both run a readiness probe bounded by `DialTimeout` (default 10s, or the
|
|
context deadline when sooner) and return a `*GatewayError` if the gateway
|
|
cannot be reached in that window.
|
|
|
|
For retry, timeout, and auth handling, `GatewayError.Code()` exposes the
|
|
wrapped gRPC `codes.Code`, and `mxgateway.IsTransient(err)` reports whether a
|
|
failure (`Unavailable`, `DeadlineExceeded`, `ResourceExhausted`, `Aborted`)
|
|
may succeed on retry — so callers do not have to unwrap the error and call
|
|
`status.Code` themselves.
|
|
|
|
## Galaxy Repository browse
|
|
|
|
The `GalaxyRepository` service (proto package `galaxy_repository.v1`) is a
|
|
read-only metadata-only browse over the AVEVA System Platform Galaxy
|
|
Repository. It uses the same API-key authentication as the MXAccess Gateway
|
|
and requires the `metadata:read` scope. Use `mxgateway.DialGalaxy` to obtain a
|
|
`*GalaxyClient` that mirrors the connection-management conventions of
|
|
`Client`:
|
|
|
|
```go
|
|
galaxy, err := mxgateway.DialGalaxy(ctx, mxgateway.Options{
|
|
Endpoint: "localhost:5000",
|
|
APIKey: os.Getenv("MXGATEWAY_API_KEY"),
|
|
Plaintext: true,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer galaxy.Close()
|
|
|
|
ok, err := galaxy.TestConnection(ctx)
|
|
deployTime, present, err := galaxy.GetLastDeployTime(ctx)
|
|
objects, err := galaxy.DiscoverHierarchy(ctx)
|
|
```
|
|
|
|
`GetLastDeployTime` returns `(time.Time{}, false, nil)` when the server
|
|
reports `present=false` (no deploy recorded). `DiscoverHierarchy` returns
|
|
the generated `*GalaxyObject` slice with each object's dynamic attributes
|
|
populated for direct contract access.
|
|
|
|
### Watching deploy events
|
|
|
|
`WatchDeployEvents` opens a server-streaming subscription. The server emits a
|
|
bootstrap event with the current Galaxy state immediately on subscribe, then
|
|
one `DeployEvent` per new deploy. `Sequence` is monotonic per server start;
|
|
gaps signal dropped events. Pass a non-nil `lastSeenDeployTime` to suppress the
|
|
bootstrap event when resuming from a known checkpoint:
|
|
|
|
```go
|
|
streamCtx, cancel := context.WithCancel(ctx)
|
|
defer cancel()
|
|
|
|
events, errs, err := galaxy.WatchDeployEvents(streamCtx, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for {
|
|
select {
|
|
case ev, ok := <-events:
|
|
if !ok {
|
|
return nil // stream completed (server EOF or ctx cancelled)
|
|
}
|
|
log.Printf("seq=%d objects=%d attrs=%d",
|
|
ev.GetSequence(), ev.GetObjectCount(), ev.GetAttributeCount())
|
|
case streamErr := <-errs:
|
|
if streamErr != nil {
|
|
return streamErr // *GatewayError
|
|
}
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
}
|
|
}
|
|
```
|
|
|
|
Cancel the supplied context to tear down the stream cleanly. Both channels
|
|
close after EOF, cancellation, or a terminal error; surfaced errors are wrapped
|
|
in `*GatewayError`.
|
|
|
|
The CLI exposes the same RPC via `galaxy-watch`:
|
|
|
|
```powershell
|
|
go run ./cmd/mxgw-go galaxy-watch -plaintext
|
|
go run ./cmd/mxgw-go galaxy-watch -plaintext -json
|
|
go run ./cmd/mxgw-go galaxy-watch -plaintext -last-seen-deploy-time 2026-04-28T10:00:00Z
|
|
go run ./cmd/mxgw-go galaxy-watch -plaintext -limit 5
|
|
```
|
|
|
|
The command runs until Ctrl+C (or the optional `-limit` is reached) and prints
|
|
one line per event in text mode or one JSON object per event with `-json`.
|
|
|
|
## CLI
|
|
|
|
The `mxgw-go` CLI emits JSON with redacted API keys for commands that connect to
|
|
the gateway:
|
|
|
|
```powershell
|
|
go run ./cmd/mxgw-go version -json
|
|
go run ./cmd/mxgw-go open-session -endpoint localhost:5000 -plaintext -json
|
|
go run ./cmd/mxgw-go register -session-id <id> -client-name mxgw-go -plaintext -json
|
|
go run ./cmd/mxgw-go add-item -session-id <id> -server-handle 1 -item Area001.Tag.Value -plaintext -json
|
|
go run ./cmd/mxgw-go advise -session-id <id> -server-handle 1 -item-handle 1 -plaintext -json
|
|
go run ./cmd/mxgw-go write -session-id <id> -server-handle 1 -item-handle 1 -type int32 -value 123 -plaintext -json
|
|
go run ./cmd/mxgw-go stream-events -session-id <id> -plaintext -json
|
|
go run ./cmd/mxgw-go smoke -item Area001.Tag.Value -plaintext -json
|
|
go run ./cmd/mxgw-go galaxy-test-connection -plaintext -json
|
|
go run ./cmd/mxgw-go galaxy-last-deploy -plaintext -json
|
|
go run ./cmd/mxgw-go galaxy-discover -plaintext -json
|
|
go run ./cmd/mxgw-go galaxy-watch -plaintext -json
|
|
```
|
|
|
|
Use `-api-key-env MXGATEWAY_API_KEY` or `-api-key <key>` when authentication is
|
|
enabled. CLI output redacts the key value and never writes the raw secret.
|
|
|
|
Use TLS options for a secured gateway:
|
|
|
|
```powershell
|
|
go run ./cmd/mxgw-go smoke -endpoint mxgateway.example.local:5001 -ca-cert C:\certs\mxgateway-ca.pem -server-name-override mxgateway.example.local -api-key-env MXGATEWAY_API_KEY -item Area001.Tag.Value -json
|
|
```
|
|
|
|
## Integration Checks
|
|
|
|
Run live checks only when a gateway and MXAccess-backed worker are available:
|
|
|
|
```powershell
|
|
$env:MXGATEWAY_INTEGRATION = '1'
|
|
$env:MXGATEWAY_ENDPOINT = 'localhost:5000'
|
|
$env:MXGATEWAY_API_KEY = '<gateway-api-key>'
|
|
$env:MXGATEWAY_TEST_ITEM = 'Area001.Tag.Value'
|
|
go run ./cmd/mxgw-go smoke -endpoint $env:MXGATEWAY_ENDPOINT -plaintext -api-key-env MXGATEWAY_API_KEY -item $env:MXGATEWAY_TEST_ITEM -json
|
|
```
|
|
|
|
## Related Documentation
|
|
|
|
- [Client Packaging](../../docs/ClientPackaging.md)
|
|
- [Client Proto Generation](../../docs/ClientProtoGeneration.md)
|
|
- [Go Client Detailed Design](./GoClientDesign.md)
|