feat(m9): CLI cached-call retry/discard command group

Adds `cached-call retry` and `cached-call discard` subcommands that relay
to the existing Deployer-gated RetryParkedMessageCommand /
DiscardParkedMessageCommand via the central SiteCallAuditActor → site relay.
ManagementCommandRegistry already covered both types via reflection auto-discovery.
CommandTreeTests updated to include cached-call (group count 16 → 17).
This commit is contained in:
Joseph Doherty
2026-06-18 10:13:56 -04:00
parent 0f04afbdf1
commit efcdd18794
6 changed files with 293 additions and 3 deletions
@@ -0,0 +1,97 @@
using System.CommandLine;
using System.CommandLine.Parsing;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Management;
namespace ZB.MOM.WW.ScadaBridge.CLI.Commands;
/// <summary>
/// Builds the <c>cached-call</c> command group with sub-commands for retrying and
/// discarding parked cached-call operations. Both sub-commands relay to the existing
/// Deployer-gated <see cref="RetryParkedMessageCommand"/> /
/// <see cref="DiscardParkedMessageCommand"/> management commands via the central
/// <c>SiteCallAuditActor</c> → site relay.
/// </summary>
public static class CachedCallCommands
{
// Options are static so the parsed values can be read back from both SetAction
// and the internal BuildRetryCommand / BuildDiscardCommand helpers (used by tests).
private static readonly Option<string> RetrySiteIdOption =
new("--site-id") { Description = "Site identifier of the parked operation", Required = true };
private static readonly Option<string> RetryMessageIdOption =
new("--tracked-operation-id") { Description = "Tracked operation ID (MessageId) of the parked cached call", Required = true };
private static readonly Option<string> DiscardSiteIdOption =
new("--site-id") { Description = "Site identifier of the parked operation", Required = true };
private static readonly Option<string> DiscardMessageIdOption =
new("--tracked-operation-id") { Description = "Tracked operation ID (MessageId) of the parked cached call", Required = true };
/// <summary>
/// Builds the <c>cached-call</c> command group.
/// </summary>
/// <param name="urlOption">Global <c>--url</c> option for the management API endpoint.</param>
/// <param name="formatOption">Global <c>--format</c> option for output format.</param>
/// <param name="usernameOption">Global <c>--username</c> option for authentication.</param>
/// <param name="passwordOption">Global <c>--password</c> option for authentication.</param>
/// <returns>The configured <c>cached-call</c> command with <c>retry</c> and <c>discard</c> sub-commands.</returns>
public static Command Build(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
{
var command = new Command("cached-call") { Description = "Manage parked cached-call operations (ExternalSystem.CachedCall / Database.CachedWrite)" };
command.Add(BuildRetry(urlOption, formatOption, usernameOption, passwordOption));
command.Add(BuildDiscard(urlOption, formatOption, usernameOption, passwordOption));
return command;
}
private static Command BuildRetry(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
{
var cmd = new Command("retry") { Description = "Retry a parked cached-call operation" };
cmd.Add(RetrySiteIdOption);
cmd.Add(RetryMessageIdOption);
cmd.SetAction(async (ParseResult result) =>
await CommandHelpers.ExecuteCommandAsync(
result, urlOption, formatOption, usernameOption, passwordOption,
BuildRetryCommand(result)));
return cmd;
}
private static Command BuildDiscard(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
{
var cmd = new Command("discard") { Description = "Discard a parked cached-call operation" };
cmd.Add(DiscardSiteIdOption);
cmd.Add(DiscardMessageIdOption);
cmd.SetAction(async (ParseResult result) =>
await CommandHelpers.ExecuteCommandAsync(
result, urlOption, formatOption, usernameOption, passwordOption,
BuildDiscardCommand(result)));
return cmd;
}
/// <summary>
/// Builds a <see cref="RetryParkedMessageCommand"/> from a parsed <c>cached-call retry</c>
/// invocation. Exposed internally so command-construction tests can assert field mapping
/// without triggering the HTTP call.
/// </summary>
/// <param name="result">The parsed command-line result from the <c>cached-call retry</c> invocation.</param>
/// <returns>A <see cref="RetryParkedMessageCommand"/> populated from the parsed result.</returns>
internal static RetryParkedMessageCommand BuildRetryCommand(ParseResult result)
{
var siteId = result.GetValue(RetrySiteIdOption)!;
var messageId = result.GetValue(RetryMessageIdOption)!;
return new RetryParkedMessageCommand(siteId, messageId);
}
/// <summary>
/// Builds a <see cref="DiscardParkedMessageCommand"/> from a parsed <c>cached-call discard</c>
/// invocation. Exposed internally so command-construction tests can assert field mapping
/// without triggering the HTTP call.
/// </summary>
/// <param name="result">The parsed command-line result from the <c>cached-call discard</c> invocation.</param>
/// <returns>A <see cref="DiscardParkedMessageCommand"/> populated from the parsed result.</returns>
internal static DiscardParkedMessageCommand BuildDiscardCommand(ParseResult result)
{
var siteId = result.GetValue(DiscardSiteIdOption)!;
var messageId = result.GetValue(DiscardMessageIdOption)!;
return new DiscardParkedMessageCommand(siteId, messageId);
}
}
+1
View File
@@ -34,6 +34,7 @@ rootCommand.Add(SharedScriptCommands.Build(urlOption, formatOption, usernameOpti
rootCommand.Add(DbConnectionCommands.Build(urlOption, formatOption, usernameOption, passwordOption));
rootCommand.Add(ApiMethodCommands.Build(urlOption, formatOption, usernameOption, passwordOption));
rootCommand.Add(BundleCommands.Build(urlOption, formatOption, usernameOption, passwordOption));
rootCommand.Add(CachedCallCommands.Build(urlOption, formatOption, usernameOption, passwordOption));
rootCommand.SetAction(_ =>
{
+34
View File
@@ -1743,6 +1743,40 @@ Site identifiers and connection names are environment-specific; the importer fir
---
### `cached-call` — Retry or discard parked cached-call operations
Manage parked `ExternalSystem.CachedCall` and `Database.CachedWrite` operations at a
site. Both sub-commands relay via the central `SiteCallAuditActor` → site relay and
require the **Deployer** role.
#### `cached-call retry`
Retry a parked cached-call operation at the specified site.
```sh
scadabridge --url <url> cached-call retry --site-id <string> --tracked-operation-id <string>
```
| Option | Required | Description |
|--------|----------|-------------|
| `--site-id` | yes | Site identifier where the operation is parked |
| `--tracked-operation-id` | yes | Tracked operation ID (MessageId) of the parked cached call |
#### `cached-call discard`
Discard a parked cached-call operation at the specified site.
```sh
scadabridge --url <url> cached-call discard --site-id <string> --tracked-operation-id <string>
```
| Option | Required | Description |
|--------|----------|-------------|
| `--site-id` | yes | Site identifier where the operation is parked |
| `--tracked-operation-id` | yes | Tracked operation ID (MessageId) of the parked cached call |
---
## Architecture Notes
The CLI connects to the Central cluster using Akka.NET's `ClusterClient`. It does not join the cluster — it contacts the `ClusterClientReceptionist` on one of the configured Central nodes and sends commands to the `ManagementActor` at path `/user/management`.