Files
mxaccessgw/clients/go
Joseph Doherty 93633ce99c Cross-language ReadBulk stress benchmark
Adds a bench-read-bulk subcommand to every client CLI (.NET, Go, Rust,
Python, Java) and a PowerShell driver that runs all five concurrently
against the deployed gateway and prints a side-by-side comparison.

Each CLI''s bench:

  - Opens its own session, registers, subscribes to bulk-size tags so the
    worker''s MxAccessValueCache populates from real OnDataChange events.
  - Runs a warmup-seconds-long pre-loop with identical calls so JIT /
    connection-pool / first-call overhead is amortised before the
    measurement window.
  - Runs ReadBulk in a tight in-process loop for duration-seconds with
    per-call high-resolution latency capture (Stopwatch in .NET,
    time.Now in Go, std::time::Instant in Rust, time.perf_counter in
    Python, System.nanoTime in Java).
  - Unsubscribes + closes the session, then emits one JSON object with
    the shared schema: { language, durationMs, totalCalls, successfulCalls,
    failedCalls, totalReadResults, cachedReadResults, callsPerSecond,
    latencyMs: { p50, p95, p99, max, mean } }.

The PS driver (scripts/bench-read-bulk.ps1) launches one detached process
per client, waits for all to finish, parses the trailing JSON object from
each stdout, prints a comparison table, and persists the combined report
under artifacts/bench/. Quoting around Java''s `gradle --args="..."` is
handled by writing a one-shot .bat that cmd.exe runs; the .NET CLI''s
per-call gRPC timeout is auto-scaled to (Duration + Warmup + 30s) so the
channel-wide timeout doesn''t cancel the bench mid-loop.

Live 30-second steady-state run against the deployed gateway, all five
clients hitting the same six TestMachine_001..006.TestChangingInt tags:

  client    calls/sec  cached/total    p50 ms  p95 ms  p99 ms  max ms
  dotnet      171.78   30924/30924      3.84   14.06   40.41  542.48
  go          175.46   31590/31590      3.93   13.52   41.26  243.00
  rust        123.26   22188/22188      5.52   15.78   48.11  544.41
  python      145.79   26244/26244      4.86   14.85   41.65  645.84
  java        181.12   32604/32604      3.80   10.59   33.37  344.27

143,550 ReadBulk results across all five clients during the 30s window;
100% were was_cached = true (the worker''s cache fast-path never fell
through to the snapshot lifecycle). Aggregate read throughput ~800
calls/sec against five concurrent sessions sharing the same cached tags.

A second variant with bulk-size 20 sustained the same per-client call
rate while delivering 3.3x more values per call (~37,000 cached reads/sec
aggregate across the five concurrent sessions), confirming the linear
per-tag cache lookup inside one call is not a bottleneck at this scale.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 05:17:08 -04:00
..
2026-04-26 19:27:27 -04:00
2026-04-26 19:27:27 -04:00

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

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:

./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:

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:

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:

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:

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:

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:

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:

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:

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:

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:

$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