fix: resolve code-review findings (locally verified)
Server-054/055/056, Contracts-020/021/022, Tests-036/038/039, IntegrationTests-030/031/032 (+033 deferred to live rig), Client.Dotnet-026/028/029 (+027 won't-fix), Client.Go-030..034, Client.Python-032..036, Client.Rust-033..038. Key fix: SessionEventDistributor orphaned a subscriber that registered after the pump completed but before disposal (Server-056) -> register paths now complete late registrants under _lifecycleLock; regression test added. The racy dashboard-mirror gRPC test made deterministic (Tests-039). Verified green locally: gateway Tests targeted classes (GatewaySession, SessionEventDistributor, GatewayOptionsValidator, ProtobufContractRoundTrip, GatewaySessionDashboardMirror) + dotnet/go/python/rust client suites.
This commit is contained in:
@@ -247,24 +247,78 @@ one line per event in text mode or one JSON object per event with `-json`.
|
||||
The `mxgw-go` CLI emits JSON with redacted API keys for commands that connect to
|
||||
the gateway:
|
||||
|
||||
### Subcommand reference
|
||||
|
||||
Every subcommand wired into the CLI. All accept the common flags
|
||||
(`-endpoint`, `-plaintext`, `-api-key` / `-api-key-env`, `-ca-cert`,
|
||||
`-server-name-override`, `-call-timeout`) and most accept `-json`.
|
||||
|
||||
| Command | Purpose |
|
||||
|---|---|
|
||||
| `version` | Print client/contract versions. |
|
||||
| `open-session` | Open a gateway session and print its id. |
|
||||
| `close-session` | Close a session by id. |
|
||||
| `ping` | Round-trip a `PING` command (`-session-id`, `-message`). |
|
||||
| `register` | Register a client name on a session (`-session-id`, `-client-name`). |
|
||||
| `add-item` | Add an item handle (`-session-id`, `-server-handle`, `-item`). |
|
||||
| `advise` | Advise (subscribe) one item (`-session-id`, `-server-handle`, `-item-handle`). |
|
||||
| `subscribe-bulk` | Advise many items in one call. |
|
||||
| `unsubscribe-bulk` | Unadvise many item handles in one call. |
|
||||
| `read-bulk` | Read snapshots for many item handles in one call. |
|
||||
| `write` | Write one value (`-type`, `-value`). |
|
||||
| `write-bulk` | Write many values (`-item-handles`, `-values`, counts must match). |
|
||||
| `write2-bulk` | `write-bulk` with a shared `-timestamp-value` (RFC 3339). |
|
||||
| `write-secured-bulk` | Secured bulk write (`-current-user-id`, `-verifier-user-id`). |
|
||||
| `write-secured2-bulk` | Secured bulk write with a shared timestamp. |
|
||||
| `bench-read-bulk` | Throughput benchmark (`-duration-seconds`, `-warmup-seconds`, `-bulk-size`). |
|
||||
| `stream-events` | Stream item-value events for a session (`-session-id`, `-limit`). |
|
||||
| `stream-alarms` | Stream the alarm feed (`-filter-prefix`, `-limit`). |
|
||||
| `acknowledge-alarm` | Acknowledge an alarm reference. |
|
||||
| `smoke` | End-to-end smoke workflow against one item. |
|
||||
| `galaxy-test-connection` | Probe the Galaxy Repository RPC connection. |
|
||||
| `galaxy-last-deploy` | Print the most recent deploy event. |
|
||||
| `galaxy-discover` | Discover deployed objects. |
|
||||
| `galaxy-watch` | Stream deploy events until Ctrl+C or `-limit`. |
|
||||
| `galaxy-browse` | Lazy/eager browse of the Galaxy object tree. |
|
||||
| `batch` | Read commands from stdin (see below). |
|
||||
|
||||
```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 ping -session-id <id> -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 write-bulk -session-id <id> -server-handle 1 -item-handles 1,2 -values 10,20 -type int32 -plaintext -json
|
||||
go run ./cmd/mxgw-go read-bulk -session-id <id> -item-handles 1,2 -plaintext -json
|
||||
go run ./cmd/mxgw-go stream-events -session-id <id> -plaintext -json
|
||||
go run ./cmd/mxgw-go stream-alarms -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
|
||||
go run ./cmd/mxgw-go galaxy-browse -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.
|
||||
|
||||
### `batch` mode
|
||||
|
||||
`batch` reads one command line at a time from stdin and dispatches each through
|
||||
the same routing as the standalone subcommands; it is the interface the
|
||||
cross-language E2E harness drives. After every command's output it writes the
|
||||
end-of-result sentinel line `__MXGW_BATCH_EOR__` to stdout and flushes, so the
|
||||
harness can frame each result. Blank/whitespace-only lines are skipped; only
|
||||
stdin EOF ends the session. Command errors are serialised as a JSON object
|
||||
(`{"error":...,"type":"error"}`) to stdout (not stderr) and still followed by the
|
||||
sentinel, so a failing command does not abort the batch. The input scanner
|
||||
buffer is widened to 16 MiB so a single long command line (e.g. a bulk write with
|
||||
thousands of handles) does not trip bufio's default 64 KiB token-too-long limit;
|
||||
a line that still exceeds 16 MiB surfaces as a framed error and ends the session.
|
||||
|
||||
Use TLS options for a secured gateway:
|
||||
|
||||
```powershell
|
||||
|
||||
@@ -837,7 +837,14 @@ func runStreamEvents(ctx context.Context, args []string, stdout, stderr io.Write
|
||||
defer client.Close()
|
||||
|
||||
session := mxgateway.NewSessionForID(client, *sessionID)
|
||||
streamCtx, cancelStream := context.WithCancel(ctx)
|
||||
|
||||
// Ctrl+C on a long-running stream-events command cancels the gRPC stream
|
||||
// cleanly (the gateway sees codes.Canceled rather than a torn TCP
|
||||
// connection) and the deferred subscription.Close()/client.Close() run.
|
||||
signalCtx, stopSignals := signal.NotifyContext(ctx, os.Interrupt, syscall.SIGTERM)
|
||||
defer stopSignals()
|
||||
|
||||
streamCtx, cancelStream := context.WithCancel(signalCtx)
|
||||
defer cancelStream()
|
||||
subscription, err := session.SubscribeEventsAfter(streamCtx, *after)
|
||||
if err != nil {
|
||||
@@ -1035,15 +1042,17 @@ func runSmoke(ctx context.Context, args []string, stdout, stderr io.Writer) erro
|
||||
}
|
||||
|
||||
func closeSmokeSession(ctx context.Context, session *mxgateway.Session, primaryErr error) error {
|
||||
closeCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
// Compute the close timeout once so a single context (and a single
|
||||
// deferred cancel) is allocated: default 5s, shortened to the caller's
|
||||
// remaining deadline when that is sooner.
|
||||
closeTimeout := 5 * time.Second
|
||||
if deadline, ok := ctx.Deadline(); ok {
|
||||
if until := time.Until(deadline); until > 0 && until < 5*time.Second {
|
||||
cancel()
|
||||
closeCtx, cancel = context.WithTimeout(context.Background(), until)
|
||||
defer cancel()
|
||||
if until := time.Until(deadline); until > 0 && until < closeTimeout {
|
||||
closeTimeout = until
|
||||
}
|
||||
}
|
||||
closeCtx, cancel := context.WithTimeout(context.Background(), closeTimeout)
|
||||
defer cancel()
|
||||
|
||||
_, closeErr := session.Close(closeCtx)
|
||||
if primaryErr != nil {
|
||||
@@ -1490,6 +1499,12 @@ func runGalaxyWatch(ctx context.Context, args []string, stdout, stderr io.Writer
|
||||
count++
|
||||
if *limit > 0 && count >= *limit {
|
||||
cancelStream()
|
||||
// Drain so the WatchDeployEvents goroutine can exit instead
|
||||
// of blocking on a send into the buffered events channel
|
||||
// while the deferred client.Close() tears the stream down
|
||||
// underneath it (mirrors the signal-cancel branch below).
|
||||
for range events {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
case streamErr, ok := <-errs:
|
||||
|
||||
@@ -537,3 +537,43 @@ func TestRunBatchHandlesLongCommandLine(t *testing.T) {
|
||||
t.Fatalf("EOR sentinel count = %d, want 2 (one per command, even when first is too long); out length = %d", count, len(out))
|
||||
}
|
||||
}
|
||||
|
||||
// TestRunBenchReadBulkRejectsNonPositiveDuration pins the -duration-seconds
|
||||
// positivity guard so the bench window cannot be configured to zero/negative.
|
||||
func TestRunBenchReadBulkRejectsNonPositiveDuration(t *testing.T) {
|
||||
var stdout, stderr bytes.Buffer
|
||||
err := runWithIO(t.Context(), []string{"bench-read-bulk", "-duration-seconds", "0"}, &stdout, &stderr)
|
||||
if err == nil || !strings.Contains(err.Error(), "duration-seconds must be positive") {
|
||||
t.Fatalf("bench-read-bulk -duration-seconds 0 error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestRunStreamEventsRequiresSessionID pins the session-id guard so stream-events
|
||||
// fails fast before dialing when no session id is supplied.
|
||||
func TestRunStreamEventsRequiresSessionID(t *testing.T) {
|
||||
var stdout, stderr bytes.Buffer
|
||||
err := runWithIO(t.Context(), []string{"stream-events", "-plaintext", "-api-key", "test"}, &stdout, &stderr)
|
||||
if err == nil || !strings.Contains(err.Error(), "session-id is required") {
|
||||
t.Fatalf("stream-events without -session-id error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestRunWriteBulkVariantRejectsMismatchedHandlesAndValues pins the len-mismatch
|
||||
// guard so a write-bulk with unequal item-handles / values counts fails fast
|
||||
// before any dial.
|
||||
func TestRunWriteBulkVariantRejectsMismatchedHandlesAndValues(t *testing.T) {
|
||||
var stdout, stderr bytes.Buffer
|
||||
err := runWithIO(t.Context(), []string{
|
||||
"write-bulk",
|
||||
"-session-id", "s1",
|
||||
"-server-handle", "1",
|
||||
"-item-handles", "1,2",
|
||||
"-values", "10",
|
||||
"-type", "int32",
|
||||
"-plaintext",
|
||||
"-api-key", "test",
|
||||
}, &stdout, &stderr)
|
||||
if err == nil || !strings.Contains(err.Error(), "does not match values count") {
|
||||
t.Fatalf("write-bulk mismatched handles/values error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user