diff --git a/source/Octopus.Tentacle.Client/EventDriven/CommandContext.cs b/source/Octopus.Tentacle.Client/EventDriven/CommandContext.cs
new file mode 100644
index 000000000..0f34b8108
--- /dev/null
+++ b/source/Octopus.Tentacle.Client/EventDriven/CommandContext.cs
@@ -0,0 +1,26 @@
+using Octopus.Tentacle.Client.Scripts;
+using Octopus.Tentacle.Contracts;
+
+namespace Octopus.Tentacle.Client.EventDriven
+{
+ ///
+ /// This class holds the context of where we are up to within the script execution life cycle.
+ /// When executing a script, there are several stages it goes through (e.g. starting the script, periodically checking status for completion, completing the script).
+ /// To be able to progress through these cycles in an event-driven environment, we need to remember some state, and then keep passing that state back into the script executor.
+ ///
+ public class CommandContext
+ {
+ public CommandContext(ScriptTicket scriptTicket,
+ long nextLogSequence,
+ ScriptServiceVersion scripServiceVersionUsed)
+ {
+ ScriptTicket = scriptTicket;
+ NextLogSequence = nextLogSequence;
+ ScripServiceVersionUsed = scripServiceVersionUsed;
+ }
+
+ public ScriptTicket ScriptTicket { get; }
+ public long NextLogSequence { get; }
+ public ScriptServiceVersion ScripServiceVersionUsed { get; }
+ }
+}
\ No newline at end of file
diff --git a/source/Octopus.Tentacle.Client/ScriptExecutor.cs b/source/Octopus.Tentacle.Client/ScriptExecutor.cs
new file mode 100644
index 000000000..9f61aaf6a
--- /dev/null
+++ b/source/Octopus.Tentacle.Client/ScriptExecutor.cs
@@ -0,0 +1,118 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using Octopus.Tentacle.Client.EventDriven;
+using Octopus.Tentacle.Client.Execution;
+using Octopus.Tentacle.Client.Observability;
+using Octopus.Tentacle.Client.Scripts;
+using Octopus.Tentacle.Client.Scripts.Models;
+using Octopus.Tentacle.Client.ServiceHelpers;
+using Octopus.Tentacle.Contracts;
+using Octopus.Tentacle.Contracts.Logging;
+using Octopus.Tentacle.Contracts.Observability;
+
+namespace Octopus.Tentacle.Client
+{
+ ///
+ /// Executes scripts, on the best available script service.
+ ///
+ public class ScriptExecutor : IScriptExecutor
+ {
+ readonly ITentacleClientTaskLog logger;
+ readonly ClientOperationMetricsBuilder operationMetricsBuilder;
+ readonly TentacleClientOptions clientOptions;
+ readonly AllClients allClients;
+ readonly RpcCallExecutor rpcCallExecutor;
+ readonly TimeSpan onCancellationAbandonCompleteScriptAfter;
+
+ public ScriptExecutor(AllClients allClients,
+ ITentacleClientTaskLog logger,
+ ITentacleClientObserver tentacleClientObserver,
+ TentacleClientOptions clientOptions,
+ TimeSpan onCancellationAbandonCompleteScriptAfter)
+ : this(
+ allClients,
+ logger,
+ tentacleClientObserver,
+ // For now, we do not support operation based metrics when used outside the TentacleClient. So just plug in a builder to discard.
+ ClientOperationMetricsBuilder.Start(),
+ clientOptions,
+ onCancellationAbandonCompleteScriptAfter)
+ {
+ }
+
+ internal ScriptExecutor(AllClients allClients,
+ ITentacleClientTaskLog logger,
+ ITentacleClientObserver tentacleClientObserver,
+ ClientOperationMetricsBuilder operationMetricsBuilder,
+ TentacleClientOptions clientOptions,
+ TimeSpan onCancellationAbandonCompleteScriptAfter)
+ {
+ this.allClients = allClients;
+ this.logger = logger;
+ this.clientOptions = clientOptions;
+ this.onCancellationAbandonCompleteScriptAfter = onCancellationAbandonCompleteScriptAfter;
+ this.operationMetricsBuilder = operationMetricsBuilder;
+ rpcCallExecutor = RpcCallExecutorFactory.Create(this.clientOptions.RpcRetrySettings.RetryDuration, tentacleClientObserver);
+ }
+
+ public async Task StartScript(ExecuteScriptCommand executeScriptCommand,
+ StartScriptIsBeingReAttempted startScriptIsBeingReAttempted,
+ CancellationToken cancellationToken)
+ {
+ var scriptServiceVersionToUse = await DetermineScriptServiceVersionToUse(cancellationToken);
+
+ var scriptExecutorFactory = CreateScriptExecutorFactory();
+ var scriptExecutor = scriptExecutorFactory.CreateScriptExecutor(scriptServiceVersionToUse);
+
+ return await scriptExecutor.StartScript(executeScriptCommand, startScriptIsBeingReAttempted, cancellationToken);
+ }
+
+ public async Task GetStatus(CommandContext ticketForNextNextStatus, CancellationToken cancellationToken)
+ {
+ var scriptExecutorFactory = CreateScriptExecutorFactory();
+ var scriptExecutor = scriptExecutorFactory.CreateScriptExecutor(ticketForNextNextStatus.ScripServiceVersionUsed);
+
+ return await scriptExecutor.GetStatus(ticketForNextNextStatus, cancellationToken);
+ }
+
+ public async Task CancelScript(CommandContext ticketForNextNextStatus)
+ {
+ var scriptExecutorFactory = CreateScriptExecutorFactory();
+ var scriptExecutor = scriptExecutorFactory.CreateScriptExecutor(ticketForNextNextStatus.ScripServiceVersionUsed);
+
+ return await scriptExecutor.CancelScript(ticketForNextNextStatus);
+ }
+
+ public async Task CompleteScript(CommandContext ticketForNextNextStatus, CancellationToken cancellationToken)
+ {
+ var scriptExecutorFactory = CreateScriptExecutorFactory();
+ var scriptExecutor = scriptExecutorFactory.CreateScriptExecutor(ticketForNextNextStatus.ScripServiceVersionUsed);
+
+ return await scriptExecutor.CompleteScript(ticketForNextNextStatus, cancellationToken);
+ }
+
+ ScriptExecutorFactory CreateScriptExecutorFactory()
+ {
+ return new ScriptExecutorFactory(allClients,
+ rpcCallExecutor,
+ operationMetricsBuilder,
+ onCancellationAbandonCompleteScriptAfter,
+ clientOptions,
+ logger);
+ }
+
+ async Task DetermineScriptServiceVersionToUse(CancellationToken cancellationToken)
+ {
+ try
+ {
+ var scriptServiceVersionSelector = new ScriptServiceVersionSelector(allClients.CapabilitiesServiceV2, logger, rpcCallExecutor, clientOptions, operationMetricsBuilder);
+ return await scriptServiceVersionSelector.DetermineScriptServiceVersionToUse(cancellationToken);
+ }
+ catch (Exception ex) when (cancellationToken.IsCancellationRequested)
+ {
+ throw new OperationCanceledException("Script execution was cancelled", ex);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/source/Octopus.Tentacle.Client/Scripts/IScriptExecutor.cs b/source/Octopus.Tentacle.Client/Scripts/IScriptExecutor.cs
new file mode 100644
index 000000000..a29731f65
--- /dev/null
+++ b/source/Octopus.Tentacle.Client/Scripts/IScriptExecutor.cs
@@ -0,0 +1,42 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using Octopus.Tentacle.Client.EventDriven;
+using Octopus.Tentacle.Client.Scripts.Models;
+using Octopus.Tentacle.Contracts;
+
+namespace Octopus.Tentacle.Client.Scripts
+{
+ public interface IScriptExecutor
+ {
+ ///
+ /// Start the script.
+ ///
+ /// The result, which includes the CommandContext for the next command
+ Task StartScript(ExecuteScriptCommand command,
+ StartScriptIsBeingReAttempted startScriptIsBeingReAttempted,
+ CancellationToken scriptExecutionCancellationToken);
+
+ ///
+ /// Get the status.
+ ///
+ /// The CommandContext from the previous command
+ ///
+ /// The result, which includes the CommandContext for the next command
+ Task GetStatus(CommandContext commandContext, CancellationToken scriptExecutionCancellationToken);
+
+ ///
+ /// Cancel the script.
+ ///
+ /// The CommandContext from the previous command
+ /// The result, which includes the CommandContext for the next command
+ Task CancelScript(CommandContext commandContext);
+
+ ///
+ /// Complete the script.
+ ///
+ /// The CommandContext from the previous command
+ ///
+ Task CompleteScript(CommandContext commandContext, CancellationToken scriptExecutionCancellationToken);
+ }
+}
\ No newline at end of file
diff --git a/source/Octopus.Tentacle.Client/Scripts/IScriptOrchestrator.cs b/source/Octopus.Tentacle.Client/Scripts/IScriptOrchestrator.cs
deleted file mode 100644
index c0026d809..000000000
--- a/source/Octopus.Tentacle.Client/Scripts/IScriptOrchestrator.cs
+++ /dev/null
@@ -1,12 +0,0 @@
-using System;
-using System.Threading;
-using System.Threading.Tasks;
-using Octopus.Tentacle.Client.Scripts.Models;
-
-namespace Octopus.Tentacle.Client.Scripts
-{
- interface IScriptOrchestrator
- {
- Task ExecuteScript(ExecuteScriptCommand command, CancellationToken scriptExecutionCancellationToken);
- }
-}
\ No newline at end of file
diff --git a/source/Octopus.Tentacle.Client/Scripts/IScriptOrchestratorFactory.cs b/source/Octopus.Tentacle.Client/Scripts/IScriptOrchestratorFactory.cs
deleted file mode 100644
index 3e3055057..000000000
--- a/source/Octopus.Tentacle.Client/Scripts/IScriptOrchestratorFactory.cs
+++ /dev/null
@@ -1,11 +0,0 @@
-using System;
-using System.Threading;
-using System.Threading.Tasks;
-
-namespace Octopus.Tentacle.Client.Scripts
-{
- interface IScriptOrchestratorFactory
- {
- Task CreateOrchestrator(CancellationToken cancellationToken);
- }
-}
\ No newline at end of file
diff --git a/source/Octopus.Tentacle.Client/Scripts/KubernetesScriptServiceV1Orchestrator.cs b/source/Octopus.Tentacle.Client/Scripts/KubernetesScriptServiceV1Executor.cs
similarity index 62%
rename from source/Octopus.Tentacle.Client/Scripts/KubernetesScriptServiceV1Orchestrator.cs
rename to source/Octopus.Tentacle.Client/Scripts/KubernetesScriptServiceV1Executor.cs
index b4e614c57..28843f938 100644
--- a/source/Octopus.Tentacle.Client/Scripts/KubernetesScriptServiceV1Orchestrator.cs
+++ b/source/Octopus.Tentacle.Client/Scripts/KubernetesScriptServiceV1Executor.cs
@@ -1,9 +1,9 @@
using System;
-using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Halibut;
using Halibut.ServiceModel;
+using Octopus.Tentacle.Client.EventDriven;
using Octopus.Tentacle.Client.Execution;
using Octopus.Tentacle.Client.Observability;
using Octopus.Tentacle.Client.Scripts.Models;
@@ -15,37 +15,32 @@
namespace Octopus.Tentacle.Client.Scripts
{
- class KubernetesScriptServiceV1Orchestrator : ObservingScriptOrchestrator
+ class KubernetesScriptServiceV1Executor : IScriptExecutor
{
readonly IAsyncClientKubernetesScriptServiceV1 clientKubernetesScriptServiceV1;
readonly RpcCallExecutor rpcCallExecutor;
readonly ClientOperationMetricsBuilder clientOperationMetricsBuilder;
readonly TimeSpan onCancellationAbandonCompleteScriptAfter;
readonly ITentacleClientTaskLog logger;
+ readonly TentacleClientOptions clientOptions;
- public KubernetesScriptServiceV1Orchestrator(
+ public KubernetesScriptServiceV1Executor(
IAsyncClientKubernetesScriptServiceV1 clientKubernetesScriptServiceV1,
- IScriptObserverBackoffStrategy scriptObserverBackOffStrategy,
RpcCallExecutor rpcCallExecutor,
ClientOperationMetricsBuilder clientOperationMetricsBuilder,
- OnScriptStatusResponseReceived onScriptStatusResponseReceived,
- OnScriptCompleted onScriptCompleted,
TimeSpan onCancellationAbandonCompleteScriptAfter,
TentacleClientOptions clientOptions,
ITentacleClientTaskLog logger)
- : base(scriptObserverBackOffStrategy,
- onScriptStatusResponseReceived,
- onScriptCompleted,
- clientOptions)
{
this.clientKubernetesScriptServiceV1 = clientKubernetesScriptServiceV1;
this.rpcCallExecutor = rpcCallExecutor;
this.clientOperationMetricsBuilder = clientOperationMetricsBuilder;
this.onCancellationAbandonCompleteScriptAfter = onCancellationAbandonCompleteScriptAfter;
+ this.clientOptions = clientOptions;
this.logger = logger;
}
- protected override StartKubernetesScriptCommandV1 Map(ExecuteScriptCommand command)
+ StartKubernetesScriptCommandV1 Map(ExecuteScriptCommand command)
{
if (command is not ExecuteKubernetesScriptCommand kubernetesScriptCommand)
throw new InvalidOperationException($"Invalid execute script command received. Expected {nameof(ExecuteKubernetesScriptCommand)}, but received {command.GetType().Name}.");
@@ -73,17 +68,18 @@ protected override StartKubernetesScriptCommandV1 Map(ExecuteScriptCommand comma
kubernetesScriptCommand.IsRawScript);
}
- protected override ScriptExecutionStatus MapToStatus(KubernetesScriptStatusResponseV1 response)
- => new(response.Logs);
-
- protected override ScriptExecutionResult MapToResult(KubernetesScriptStatusResponseV1 response)
- => new(response.State, response.ExitCode);
-
- protected override ProcessState GetState(KubernetesScriptStatusResponseV1 response) => response.State;
+ static ScriptOperationExecutionResult Map(KubernetesScriptStatusResponseV1 scriptStatusResponse)
+ {
+ return new(
+ new ScriptStatus(scriptStatusResponse.State, scriptStatusResponse.ExitCode, scriptStatusResponse.Logs),
+ new CommandContext(scriptStatusResponse.ScriptTicket, scriptStatusResponse.NextLogSequence, ScriptServiceVersion.KubernetesScriptServiceVersion1));
+ }
- protected override async Task StartScript(StartKubernetesScriptCommandV1 command, CancellationToken scriptExecutionCancellationToken)
+ public async Task StartScript(ExecuteScriptCommand executeScriptCommand,
+ StartScriptIsBeingReAttempted startScriptIsBeingReAttempted,
+ CancellationToken scriptExecutionCancellationToken)
{
- KubernetesScriptStatusResponseV1 scriptStatusResponse;
+ var command = Map(executeScriptCommand);
var startScriptCallsConnectedCount = 0;
try
{
@@ -104,14 +100,16 @@ void OnErrorAction(Exception ex)
}
}
- scriptStatusResponse = await rpcCallExecutor.Execute(
- retriesEnabled: ClientOptions.RpcRetrySettings.RetriesEnabled,
+ var scriptStatusResponse = await rpcCallExecutor.Execute(
+ retriesEnabled: clientOptions.RpcRetrySettings.RetriesEnabled,
RpcCall.Create(nameof(IKubernetesScriptServiceV1.StartScript)),
StartScriptAction,
OnErrorAction,
logger,
clientOperationMetricsBuilder,
scriptExecutionCancellationToken).ConfigureAwait(false);
+
+ return Map(scriptStatusResponse);
}
catch (Exception ex) when (scriptExecutionCancellationToken.IsCancellationRequested)
{
@@ -125,65 +123,37 @@ void OnErrorAction(Exception ex)
if (!startScriptCallIsConnecting || startScriptCallIsBeingRetried)
{
- // We have to assume the script started executing and call CancelScript and CompleteScript
- // We don't have a response so we need to create one to continue the execution flow
- scriptStatusResponse = new KubernetesScriptStatusResponseV1(
- command.ScriptTicket,
- ProcessState.Pending,
- ScriptExitCodes.RunningExitCode,
- new List(),
- 0);
-
- try
- {
- await ObserveUntilCompleteThenFinish(scriptStatusResponse, scriptExecutionCancellationToken).ConfigureAwait(false);
- }
- catch (Exception observerUntilCompleteException)
- {
- // Throw an error so the caller knows that execution of the script was cancelled
- throw new OperationCanceledException("Script execution was cancelled", observerUntilCompleteException);
- }
-
- // Throw an error so the caller knows that execution of the script was cancelled
- throw new OperationCanceledException("Script execution was cancelled");
+ // We want to cancel the potentially started script, and wait till it finishes. By returning a result, the outer orchestration will take care of this.
+ return ScriptOperationExecutionResult.CreateScriptStartedResult(command.ScriptTicket, ScriptServiceVersion.KubernetesScriptServiceVersion1);
}
// If the StartScript call was not in-flight or being retries then we know the script has not started executing on Tentacle
// So can exit without calling CancelScript or CompleteScript
throw new OperationCanceledException("Script execution was cancelled", ex);
}
-
- return scriptStatusResponse;
}
- protected override async Task GetStatus(KubernetesScriptStatusResponseV1 lastStatusResponse, CancellationToken scriptExecutionCancellationToken)
+ public async Task GetStatus(CommandContext commandContext, CancellationToken scriptExecutionCancellationToken)
{
- try
+ async Task GetStatusAction(CancellationToken ct)
{
- async Task GetStatusAction(CancellationToken ct)
- {
- var request = new KubernetesScriptStatusRequestV1(lastStatusResponse.ScriptTicket, lastStatusResponse.NextLogSequence);
- var result = await clientKubernetesScriptServiceV1.GetStatusAsync(request, new HalibutProxyRequestOptions(ct));
+ var request = new KubernetesScriptStatusRequestV1(commandContext.ScriptTicket, commandContext.NextLogSequence);
+ var result = await clientKubernetesScriptServiceV1.GetStatusAsync(request, new HalibutProxyRequestOptions(ct));
- return result;
- }
-
- return await rpcCallExecutor.Execute(
- retriesEnabled: ClientOptions.RpcRetrySettings.RetriesEnabled,
- RpcCall.Create(nameof(IKubernetesScriptServiceV1.GetStatus)),
- GetStatusAction,
- logger,
- clientOperationMetricsBuilder,
- scriptExecutionCancellationToken).ConfigureAwait(false);
- }
- catch (Exception e) when (e is OperationCanceledException && scriptExecutionCancellationToken.IsCancellationRequested)
- {
- // Return the last known response without logs when cancellation occurs and let the script execution go into the CancelScript and CompleteScript flow
- return new KubernetesScriptStatusResponseV1(lastStatusResponse.ScriptTicket, lastStatusResponse.State, lastStatusResponse.ExitCode, new List(), lastStatusResponse.NextLogSequence);
+ return result;
}
+
+ var kubernetesScriptStatusResponseV1 = await rpcCallExecutor.Execute(
+ retriesEnabled: clientOptions.RpcRetrySettings.RetriesEnabled,
+ RpcCall.Create(nameof(IKubernetesScriptServiceV1.GetStatus)),
+ GetStatusAction,
+ logger,
+ clientOperationMetricsBuilder,
+ scriptExecutionCancellationToken).ConfigureAwait(false);
+ return Map(kubernetesScriptStatusResponseV1);
}
- protected override async Task Cancel(KubernetesScriptStatusResponseV1 lastStatusResponse, CancellationToken scriptExecutionCancellationToken)
+ public async Task CancelScript(CommandContext lastStatusResponse)
{
async Task CancelScriptAction(CancellationToken ct)
{
@@ -197,17 +167,18 @@ async Task CancelScriptAction(CancellationToke
// If script execution is already triggering RPC Retries and then the script execution is cancelled there is a high chance that the cancel RPC call will fail as well and go into RPC retries.
// We could potentially reduce the time to failure by not retrying the cancel RPC Call if the previous RPC call was already triggering RPC Retries.
- return await rpcCallExecutor.Execute(
- retriesEnabled: ClientOptions.RpcRetrySettings.RetriesEnabled,
+ var kubernetesScriptStatusResponseV1 = await rpcCallExecutor.Execute(
+ retriesEnabled: clientOptions.RpcRetrySettings.RetriesEnabled,
RpcCall.Create(nameof(IKubernetesScriptServiceV1.CancelScript)),
CancelScriptAction,
logger,
clientOperationMetricsBuilder,
// We don't want to cancel this operation as it is responsible for stopping the script executing on the Tentacle
CancellationToken.None).ConfigureAwait(false);
+ return Map(kubernetesScriptStatusResponseV1);
}
- protected override async Task Finish(KubernetesScriptStatusResponseV1 lastStatusResponse, CancellationToken scriptExecutionCancellationToken)
+ public async Task CompleteScript(CommandContext lastStatusResponse, CancellationToken scriptExecutionCancellationToken)
{
try
{
@@ -234,7 +205,7 @@ await rpcCallExecutor.ExecuteWithNoRetries(
logger.Verbose(ex);
}
- return lastStatusResponse;
+ return null;
}
}
}
\ No newline at end of file
diff --git a/source/Octopus.Tentacle.Client/Scripts/ObservingScriptOrchestrator.cs b/source/Octopus.Tentacle.Client/Scripts/ObservingScriptOrchestrator.cs
index 47d7fd64c..4aa6882aa 100644
--- a/source/Octopus.Tentacle.Client/Scripts/ObservingScriptOrchestrator.cs
+++ b/source/Octopus.Tentacle.Client/Scripts/ObservingScriptOrchestrator.cs
@@ -6,21 +6,20 @@
namespace Octopus.Tentacle.Client.Scripts
{
- abstract class ObservingScriptOrchestrator : IScriptOrchestrator
+ public sealed class ObservingScriptOrchestrator
{
readonly IScriptObserverBackoffStrategy scriptObserverBackOffStrategy;
readonly OnScriptStatusResponseReceived onScriptStatusResponseReceived;
readonly OnScriptCompleted onScriptCompleted;
+ readonly IScriptExecutor scriptExecutor;
- protected TentacleClientOptions ClientOptions { get; }
-
- protected ObservingScriptOrchestrator(
+ public ObservingScriptOrchestrator(
IScriptObserverBackoffStrategy scriptObserverBackOffStrategy,
OnScriptStatusResponseReceived onScriptStatusResponseReceived,
OnScriptCompleted onScriptCompleted,
- TentacleClientOptions clientOptions)
+ IScriptExecutor scriptExecutor)
{
- ClientOptions = clientOptions;
+ this.scriptExecutor = scriptExecutor;
this.scriptObserverBackOffStrategy = scriptObserverBackOffStrategy;
this.onScriptStatusResponseReceived = onScriptStatusResponseReceived;
this.onScriptCompleted = onScriptCompleted;
@@ -28,11 +27,11 @@ protected ObservingScriptOrchestrator(
public async Task ExecuteScript(ExecuteScriptCommand command, CancellationToken scriptExecutionCancellationToken)
{
- var mappedStartCommand = Map(command);
-
- var scriptStatusResponse = await StartScript(mappedStartCommand, scriptExecutionCancellationToken).ConfigureAwait(false);
+ var startScriptResult = await scriptExecutor.StartScript(command,
+ StartScriptIsBeingReAttempted.FirstAttempt, // This is not re-entrant so this should be true.
+ scriptExecutionCancellationToken).ConfigureAwait(false);
- scriptStatusResponse = await ObserveUntilCompleteThenFinish(scriptStatusResponse, scriptExecutionCancellationToken).ConfigureAwait(false);
+ var scriptStatus = await ObserveUntilCompleteThenFinish(startScriptResult, scriptExecutionCancellationToken).ConfigureAwait(false);
if (scriptExecutionCancellationToken.IsCancellationRequested)
{
@@ -40,45 +39,49 @@ public async Task ExecuteScript(ExecuteScriptCommand comm
throw new OperationCanceledException("Script execution was cancelled");
}
- var mappedResponse = MapToResult(scriptStatusResponse);
-
- return new ScriptExecutionResult(mappedResponse.State, mappedResponse.ExitCode);
+ return new ScriptExecutionResult(scriptStatus.State, scriptStatus.ExitCode!.Value);
}
- protected async Task ObserveUntilCompleteThenFinish(
- TScriptStatusResponse scriptStatusResponse,
+ async Task ObserveUntilCompleteThenFinish(
+ ScriptOperationExecutionResult startScriptResult,
CancellationToken scriptExecutionCancellationToken)
{
- OnScriptStatusResponseReceived(scriptStatusResponse);
+ OnScriptStatusResponseReceived(startScriptResult.ScriptStatus);
- var lastScriptStatus = await ObserveUntilComplete(scriptStatusResponse, scriptExecutionCancellationToken).ConfigureAwait(false);
+ var observingUntilCompleteResult = await ObserveUntilComplete(startScriptResult, scriptExecutionCancellationToken).ConfigureAwait(false);
await onScriptCompleted(scriptExecutionCancellationToken).ConfigureAwait(false);
+
+ var completeScriptResponse = await scriptExecutor.CompleteScript(observingUntilCompleteResult.ContextForNextCommand, scriptExecutionCancellationToken).ConfigureAwait(false);
- lastScriptStatus = await Finish(lastScriptStatus, scriptExecutionCancellationToken).ConfigureAwait(false);
+ // V1 can return a result when completing. But other versions do not.
+ // The behaviour we are maintaining is that the result to use for V1 is that of "complete"
+ // but the result to use for other versions is the last observing result.
+ var scriptStatusResponse = completeScriptResponse ?? observingUntilCompleteResult.ScriptStatus;
+ OnScriptStatusResponseReceived(scriptStatusResponse);
- return lastScriptStatus;
+ return scriptStatusResponse;
}
- async Task ObserveUntilComplete(
- TScriptStatusResponse scriptStatusResponse,
+ async Task ObserveUntilComplete(
+ ScriptOperationExecutionResult startScriptResult,
CancellationToken scriptExecutionCancellationToken)
{
- var lastStatusResponse = scriptStatusResponse;
var iteration = 0;
var cancellationIteration = 0;
+ var lastResult = startScriptResult;
- while (GetState(lastStatusResponse) != ProcessState.Complete)
+ while (lastResult.ScriptStatus.State != ProcessState.Complete)
{
if (scriptExecutionCancellationToken.IsCancellationRequested)
{
- lastStatusResponse = await Cancel(lastStatusResponse, scriptExecutionCancellationToken).ConfigureAwait(false);
+ lastResult = await scriptExecutor.CancelScript(lastResult.ContextForNextCommand).ConfigureAwait(false);
}
else
{
try
{
- lastStatusResponse = await GetStatus(lastStatusResponse, scriptExecutionCancellationToken).ConfigureAwait(false);
+ lastResult = await scriptExecutor.GetStatus(lastResult.ContextForNextCommand, scriptExecutionCancellationToken).ConfigureAwait(false);
}
catch (Exception)
{
@@ -91,9 +94,9 @@ async Task ObserveUntilComplete(
}
}
- OnScriptStatusResponseReceived(lastStatusResponse);
+ OnScriptStatusResponseReceived(lastResult.ScriptStatus);
- if (GetState(lastStatusResponse) == ProcessState.Complete)
+ if (lastResult.ScriptStatus.State == ProcessState.Complete)
{
continue;
}
@@ -112,28 +115,13 @@ await Task.Delay(scriptObserverBackOffStrategy.GetBackoff(++iteration), scriptEx
}
}
- return lastStatusResponse;
+ return lastResult;
}
- protected abstract TStartCommand Map(ExecuteScriptCommand command);
-
- protected abstract ScriptExecutionStatus MapToStatus(TScriptStatusResponse response);
-
- protected abstract ScriptExecutionResult MapToResult(TScriptStatusResponse response);
-
- protected abstract ProcessState GetState(TScriptStatusResponse response);
-
- protected abstract Task StartScript(TStartCommand command, CancellationToken scriptExecutionCancellationToken);
-
- protected abstract Task GetStatus(TScriptStatusResponse lastStatusResponse, CancellationToken scriptExecutionCancellationToken);
-
- protected abstract Task Cancel(TScriptStatusResponse lastStatusResponse, CancellationToken scriptExecutionCancellationToken);
-
- protected abstract Task Finish(TScriptStatusResponse lastStatusResponse, CancellationToken scriptExecutionCancellationToken);
-
- protected void OnScriptStatusResponseReceived(TScriptStatusResponse scriptStatusResponse)
+ void OnScriptStatusResponseReceived(ScriptStatus scriptStatusResponse)
{
- onScriptStatusResponseReceived(MapToStatus(scriptStatusResponse));
+ var scriptExecutionStatus = new ScriptExecutionStatus(scriptStatusResponse.Logs);
+ onScriptStatusResponseReceived(scriptExecutionStatus);
}
}
}
\ No newline at end of file
diff --git a/source/Octopus.Tentacle.Client/Scripts/ScriptExecutorFactory.cs b/source/Octopus.Tentacle.Client/Scripts/ScriptExecutorFactory.cs
new file mode 100644
index 000000000..a67824d6f
--- /dev/null
+++ b/source/Octopus.Tentacle.Client/Scripts/ScriptExecutorFactory.cs
@@ -0,0 +1,73 @@
+using System;
+using Octopus.Tentacle.Client.Execution;
+using Octopus.Tentacle.Client.Observability;
+using Octopus.Tentacle.Client.ServiceHelpers;
+using Octopus.Tentacle.Contracts.Logging;
+
+namespace Octopus.Tentacle.Client.Scripts
+{
+ class ScriptExecutorFactory
+ {
+ readonly RpcCallExecutor rpcCallExecutor;
+ readonly ClientOperationMetricsBuilder clientOperationMetricsBuilder;
+ readonly TimeSpan onCancellationAbandonCompleteScriptAfter;
+ readonly ITentacleClientTaskLog logger;
+
+ readonly AllClients allClients;
+ readonly TentacleClientOptions clientOptions;
+
+ public ScriptExecutorFactory(
+ AllClients allClients,
+ RpcCallExecutor rpcCallExecutor,
+ ClientOperationMetricsBuilder clientOperationMetricsBuilder,
+ TimeSpan onCancellationAbandonCompleteScriptAfter,
+ TentacleClientOptions clientOptions,
+ ITentacleClientTaskLog logger)
+ {
+ this.allClients = allClients;
+ this.rpcCallExecutor = rpcCallExecutor;
+ this.clientOperationMetricsBuilder = clientOperationMetricsBuilder;
+ this.onCancellationAbandonCompleteScriptAfter = onCancellationAbandonCompleteScriptAfter;
+ this.clientOptions = clientOptions;
+ this.logger = logger;
+ }
+
+ public IScriptExecutor CreateScriptExecutor(ScriptServiceVersion scriptServiceToUse)
+ {
+ if (scriptServiceToUse == ScriptServiceVersion.ScriptServiceVersion1)
+ {
+ return new ScriptServiceV1Executor(
+ allClients.ScriptServiceV1,
+ rpcCallExecutor,
+ clientOperationMetricsBuilder,
+ logger);
+ }
+
+ if (scriptServiceToUse == ScriptServiceVersion.ScriptServiceVersion2)
+ {
+ return new ScriptServiceV2Executor(
+ allClients.ScriptServiceV2,
+ rpcCallExecutor,
+ clientOperationMetricsBuilder,
+ onCancellationAbandonCompleteScriptAfter,
+ clientOptions,
+ logger);
+ }
+
+ if (scriptServiceToUse == ScriptServiceVersion.KubernetesScriptServiceVersion1)
+ {
+ return new KubernetesScriptServiceV1Executor(
+ allClients.KubernetesScriptServiceV1,
+ rpcCallExecutor,
+ clientOperationMetricsBuilder,
+ onCancellationAbandonCompleteScriptAfter,
+ clientOptions,
+ logger);
+ }
+
+ throw new InvalidOperationException($"Unknown ScriptServiceVersion {scriptServiceToUse}");
+ }
+
+
+ }
+}
\ No newline at end of file
diff --git a/source/Octopus.Tentacle.Client/Scripts/ScriptOperationExecutionResult.cs b/source/Octopus.Tentacle.Client/Scripts/ScriptOperationExecutionResult.cs
new file mode 100644
index 000000000..a2b16b858
--- /dev/null
+++ b/source/Octopus.Tentacle.Client/Scripts/ScriptOperationExecutionResult.cs
@@ -0,0 +1,29 @@
+using Octopus.Tentacle.Client.EventDriven;
+using Octopus.Tentacle.Contracts;
+using System.Collections.Generic;
+
+namespace Octopus.Tentacle.Client.Scripts
+{
+ public class ScriptOperationExecutionResult
+ {
+ public ScriptStatus ScriptStatus { get; }
+ public CommandContext ContextForNextCommand { get; }
+
+ public ScriptOperationExecutionResult(ScriptStatus scriptStatus, CommandContext contextForNextCommand)
+ {
+ ScriptStatus = scriptStatus;
+ ContextForNextCommand = contextForNextCommand;
+ }
+
+ ///
+ /// Create a result object for when we have most likely started a script, but cancellation has started, and we want to wait for
+ /// this script to finish.
+ ///
+ internal static ScriptOperationExecutionResult CreateScriptStartedResult(ScriptTicket scriptTicket, ScriptServiceVersion scripServiceVersionUsed)
+ {
+ var scriptStatus = new ScriptStatus(ProcessState.Pending, null, new List());
+ var contextForNextCommand = new CommandContext(scriptTicket, 0, scripServiceVersionUsed);
+ return new(scriptStatus, contextForNextCommand);
+ }
+ }
+}
\ No newline at end of file
diff --git a/source/Octopus.Tentacle.Client/Scripts/ScriptOrchestratorFactory.cs b/source/Octopus.Tentacle.Client/Scripts/ScriptOrchestratorFactory.cs
deleted file mode 100644
index e0cfe0697..000000000
--- a/source/Octopus.Tentacle.Client/Scripts/ScriptOrchestratorFactory.cs
+++ /dev/null
@@ -1,173 +0,0 @@
-using System;
-using System.Threading;
-using System.Threading.Tasks;
-using Halibut.ServiceModel;
-using Octopus.Tentacle.Client.Capabilities;
-using Octopus.Tentacle.Client.Execution;
-using Octopus.Tentacle.Client.Kubernetes;
-using Octopus.Tentacle.Client.Observability;
-using Octopus.Tentacle.Contracts.Capabilities;
-using Octopus.Tentacle.Contracts.ClientServices;
-using Octopus.Tentacle.Contracts.Logging;
-using Octopus.Tentacle.Contracts.Observability;
-
-namespace Octopus.Tentacle.Client.Scripts
-{
- class ScriptOrchestratorFactory : IScriptOrchestratorFactory
- {
- readonly IScriptObserverBackoffStrategy scriptObserverBackOffStrategy;
- readonly RpcCallExecutor rpcCallExecutor;
- readonly ClientOperationMetricsBuilder clientOperationMetricsBuilder;
- readonly OnScriptStatusResponseReceived onScriptStatusResponseReceived;
- readonly OnScriptCompleted onScriptCompleted;
- readonly TimeSpan onCancellationAbandonCompleteScriptAfter;
- readonly ITentacleClientTaskLog logger;
-
- readonly IAsyncClientScriptService clientScriptServiceV1;
- readonly IAsyncClientScriptServiceV2 clientScriptServiceV2;
- readonly IAsyncClientKubernetesScriptServiceV1 clientKubernetesScriptServiceV1;
- readonly IAsyncClientCapabilitiesServiceV2 clientCapabilitiesServiceV2;
- readonly TentacleClientOptions clientOptions;
-
- public ScriptOrchestratorFactory(
- IAsyncClientScriptService clientScriptServiceV1,
- IAsyncClientScriptServiceV2 clientScriptServiceV2,
- IAsyncClientKubernetesScriptServiceV1 clientKubernetesScriptServiceV1,
- IAsyncClientCapabilitiesServiceV2 clientCapabilitiesServiceV2,
- IScriptObserverBackoffStrategy scriptObserverBackOffStrategy,
- RpcCallExecutor rpcCallExecutor,
- ClientOperationMetricsBuilder clientOperationMetricsBuilder,
- OnScriptStatusResponseReceived onScriptStatusResponseReceived,
- OnScriptCompleted onScriptCompleted,
- TimeSpan onCancellationAbandonCompleteScriptAfter,
- TentacleClientOptions clientOptions,
- ITentacleClientTaskLog logger)
- {
- this.clientScriptServiceV1 = clientScriptServiceV1;
- this.clientScriptServiceV2 = clientScriptServiceV2;
- this.clientKubernetesScriptServiceV1 = clientKubernetesScriptServiceV1;
- this.clientCapabilitiesServiceV2 = clientCapabilitiesServiceV2;
- this.scriptObserverBackOffStrategy = scriptObserverBackOffStrategy;
- this.rpcCallExecutor = rpcCallExecutor;
- this.clientOperationMetricsBuilder = clientOperationMetricsBuilder;
- this.onScriptStatusResponseReceived = onScriptStatusResponseReceived;
- this.onScriptCompleted = onScriptCompleted;
- this.onCancellationAbandonCompleteScriptAfter = onCancellationAbandonCompleteScriptAfter;
- this.clientOptions = clientOptions;
- this.logger = logger;
- }
-
- public async Task CreateOrchestrator(CancellationToken cancellationToken)
- {
- ScriptServiceVersion scriptServiceToUse;
- try
- {
- scriptServiceToUse = await DetermineScriptServiceVersionToUse(cancellationToken);
- }
- catch (Exception ex) when (cancellationToken.IsCancellationRequested)
- {
- throw new OperationCanceledException("Script execution was cancelled", ex);
- }
-
- if (scriptServiceToUse == ScriptServiceVersion.ScriptServiceVersion1)
- {
- return new ScriptServiceV1Orchestrator(
- clientScriptServiceV1,
- scriptObserverBackOffStrategy,
- rpcCallExecutor,
- clientOperationMetricsBuilder,
- onScriptStatusResponseReceived,
- onScriptCompleted,
- clientOptions,
- logger);
- }
-
- if (scriptServiceToUse == ScriptServiceVersion.ScriptServiceVersion2)
- {
- return new ScriptServiceV2Orchestrator(
- clientScriptServiceV2,
- scriptObserverBackOffStrategy,
- rpcCallExecutor,
- clientOperationMetricsBuilder,
- onScriptStatusResponseReceived,
- onScriptCompleted,
- onCancellationAbandonCompleteScriptAfter,
- clientOptions,
- logger);
- }
-
- if (scriptServiceToUse == ScriptServiceVersion.KubernetesScriptServiceVersion1)
- {
- return new KubernetesScriptServiceV1Orchestrator(
- clientKubernetesScriptServiceV1,
- scriptObserverBackOffStrategy,
- rpcCallExecutor,
- clientOperationMetricsBuilder,
- onScriptStatusResponseReceived,
- onScriptCompleted,
- onCancellationAbandonCompleteScriptAfter,
- clientOptions,
- logger);
- }
-
- throw new InvalidOperationException($"Unknown ScriptServiceVersion {scriptServiceToUse}");
- }
-
- async Task DetermineScriptServiceVersionToUse(CancellationToken cancellationToken)
- {
- logger.Verbose("Determining ScriptService version to use");
-
- async Task GetCapabilitiesFunc(CancellationToken ct)
- {
- var result = await clientCapabilitiesServiceV2.GetCapabilitiesAsync(new HalibutProxyRequestOptions(ct));
-
- return result;
- }
-
- var tentacleCapabilities = await rpcCallExecutor.Execute(
- retriesEnabled: clientOptions.RpcRetrySettings.RetriesEnabled,
- RpcCall.Create(nameof(ICapabilitiesServiceV2.GetCapabilities)),
- GetCapabilitiesFunc,
- logger,
- clientOperationMetricsBuilder,
- cancellationToken);
-
- logger.Verbose($"Discovered Tentacle capabilities: {string.Join(",", tentacleCapabilities.SupportedCapabilities)}");
-
- // Check if we support any kubernetes script service.
- // It's implied (and tested) that GetCapabilities will only return Kubernetes or non-Kubernetes script services, never a mix
- if (tentacleCapabilities.HasAnyKubernetesScriptService())
- {
- return DetermineKubernetesScriptServiceVersionToUse();
- }
-
- return DetermineShellScriptServiceVersionToUse(tentacleCapabilities);
- }
-
- ScriptServiceVersion DetermineShellScriptServiceVersionToUse(CapabilitiesResponseV2 tentacleCapabilities)
- {
- if (tentacleCapabilities.HasScriptServiceV2())
- {
- logger.Verbose("Using ScriptServiceV2");
- logger.Verbose(clientOptions.RpcRetrySettings.RetriesEnabled
- ? $"RPC call retries are enabled. Retry timeout {rpcCallExecutor.RetryTimeout.TotalSeconds} seconds"
- : "RPC call retries are disabled.");
- return ScriptServiceVersion.ScriptServiceVersion2;
- }
-
- logger.Verbose("RPC call retries are enabled but will not be used for Script Execution as a compatible ScriptService was not found. Please upgrade Tentacle to enable this feature.");
- logger.Verbose("Using ScriptServiceV1");
- return ScriptServiceVersion.ScriptServiceVersion1;
- }
-
- ScriptServiceVersion DetermineKubernetesScriptServiceVersionToUse()
- {
- logger.Verbose($"Using KubernetesScriptServiceV1");
- logger.Verbose(clientOptions.RpcRetrySettings.RetriesEnabled
- ? $"RPC call retries are enabled. Retry timeout {rpcCallExecutor.RetryTimeout.TotalSeconds} seconds"
- : "RPC call retries are disabled.");
-
- return ScriptServiceVersion.KubernetesScriptServiceVersion1;
- }
- }
-}
\ No newline at end of file
diff --git a/source/Octopus.Tentacle.Client/Scripts/ScriptServiceV1Orchestrator.cs b/source/Octopus.Tentacle.Client/Scripts/ScriptServiceV1Executor.cs
similarity index 62%
rename from source/Octopus.Tentacle.Client/Scripts/ScriptServiceV1Orchestrator.cs
rename to source/Octopus.Tentacle.Client/Scripts/ScriptServiceV1Executor.cs
index 12b42f64f..2445edfb3 100644
--- a/source/Octopus.Tentacle.Client/Scripts/ScriptServiceV1Orchestrator.cs
+++ b/source/Octopus.Tentacle.Client/Scripts/ScriptServiceV1Executor.cs
@@ -3,6 +3,7 @@
using System.Threading;
using System.Threading.Tasks;
using Halibut.ServiceModel;
+using Octopus.Tentacle.Client.EventDriven;
using Octopus.Tentacle.Client.Execution;
using Octopus.Tentacle.Client.Observability;
using Octopus.Tentacle.Client.Scripts.Models;
@@ -13,28 +14,19 @@
namespace Octopus.Tentacle.Client.Scripts
{
- class ScriptServiceV1Orchestrator : ObservingScriptOrchestrator
+ class ScriptServiceV1Executor : IScriptExecutor
{
-
readonly RpcCallExecutor rpcCallExecutor;
readonly ClientOperationMetricsBuilder clientOperationMetricsBuilder;
readonly ITentacleClientTaskLog logger;
readonly IAsyncClientScriptService clientScriptServiceV1;
- public ScriptServiceV1Orchestrator(
+ public ScriptServiceV1Executor(
IAsyncClientScriptService clientScriptServiceV1,
- IScriptObserverBackoffStrategy scriptObserverBackOffStrategy,
RpcCallExecutor rpcCallExecutor,
ClientOperationMetricsBuilder clientOperationMetricsBuilder,
- OnScriptStatusResponseReceived onScriptStatusResponseReceived,
- OnScriptCompleted onScriptCompleted,
- TentacleClientOptions clientOptions,
ITentacleClientTaskLog logger)
- : base(scriptObserverBackOffStrategy,
- onScriptStatusResponseReceived,
- onScriptCompleted,
- clientOptions)
{
this.clientScriptServiceV1 = clientScriptServiceV1;
this.rpcCallExecutor = rpcCallExecutor;
@@ -42,10 +34,10 @@ public ScriptServiceV1Orchestrator(
this.logger = logger;
}
- protected override StartScriptCommand Map(ExecuteScriptCommand command)
+ StartScriptCommand Map(ExecuteScriptCommand command)
{
if (command is not ExecuteShellScriptCommand shellScriptCommand)
- throw new InvalidOperationException($"{nameof(ScriptServiceV2Orchestrator)} only supports commands of type {nameof(ExecuteShellScriptCommand)}.");
+ throw new InvalidOperationException($"{nameof(ScriptServiceV2Executor)} only supports commands of type {nameof(ExecuteShellScriptCommand)}.");
return new StartScriptCommand(
shellScriptCommand.ScriptBody,
@@ -58,16 +50,29 @@ protected override StartScriptCommand Map(ExecuteScriptCommand command)
shellScriptCommand.Files.ToArray());
}
- protected override ScriptExecutionStatus MapToStatus(ScriptStatusResponse response)
- => new(response.Logs);
-
- protected override ScriptExecutionResult MapToResult(ScriptStatusResponse response)
- => new(response.State, response.ExitCode);
+ static ScriptOperationExecutionResult Map(ScriptStatusResponse scriptStatusResponse)
+ {
+ return new (
+ MapToScriptStatus(scriptStatusResponse),
+ new CommandContext(scriptStatusResponse.Ticket, scriptStatusResponse.NextLogSequence, ScriptServiceVersion.ScriptServiceVersion1));
+ }
- protected override ProcessState GetState(ScriptStatusResponse response) => response.State;
+ static ScriptStatus MapToScriptStatus(ScriptStatusResponse scriptStatusResponse)
+ {
+ return new ScriptStatus(scriptStatusResponse.State, scriptStatusResponse.ExitCode, scriptStatusResponse.Logs);
+ }
- protected override async Task StartScript(StartScriptCommand command, CancellationToken scriptExecutionCancellationToken)
+ public async Task StartScript(ExecuteScriptCommand executeScriptCommand,
+ StartScriptIsBeingReAttempted startScriptIsBeingReAttempted,
+ CancellationToken scriptExecutionCancellationToken)
{
+ // Script Service v1 is not idempotent, do not allow it to be re-attempted as it may run a second time.
+ if (startScriptIsBeingReAttempted == StartScriptIsBeingReAttempted.PossiblyBeingReAttempted)
+ {
+ throw new UnsafeStartAttemptException("Cannot start V1 script service if there is a chance it has been attempted before.");
+ }
+
+ var command = Map(executeScriptCommand);
var scriptTicket = await rpcCallExecutor.ExecuteWithNoRetries(
RpcCall.Create(nameof(IScriptService.StartScript)),
async ct =>
@@ -80,16 +85,16 @@ protected override async Task StartScript(StartScriptComma
clientOperationMetricsBuilder,
scriptExecutionCancellationToken).ConfigureAwait(false);
- return new ScriptStatusResponse(scriptTicket, ProcessState.Pending, 0, new List(), 0);
+ return Map(new ScriptStatusResponse(scriptTicket, ProcessState.Pending, 0, new List(), 0));
}
- protected override async Task GetStatus(ScriptStatusResponse lastStatusResponse, CancellationToken scriptExecutionCancellationToken)
+ public async Task GetStatus(CommandContext commandContext, CancellationToken scriptExecutionCancellationToken)
{
var scriptStatusResponseV1 = await rpcCallExecutor.ExecuteWithNoRetries(
RpcCall.Create(nameof(IScriptService.GetStatus)),
async ct =>
{
- var request = new ScriptStatusRequest(lastStatusResponse.Ticket, lastStatusResponse.NextLogSequence);
+ var request = new ScriptStatusRequest(commandContext.ScriptTicket, commandContext.NextLogSequence);
var result = await clientScriptServiceV1.GetStatusAsync(request, new HalibutProxyRequestOptions(ct));
return result;
@@ -98,16 +103,16 @@ protected override async Task GetStatus(ScriptStatusRespon
clientOperationMetricsBuilder,
scriptExecutionCancellationToken).ConfigureAwait(false);
- return scriptStatusResponseV1;
+ return Map(scriptStatusResponseV1);
}
- protected override async Task Cancel(ScriptStatusResponse lastStatusResponse, CancellationToken scriptExecutionCancellationToken)
+ public async Task CancelScript(CommandContext lastStatusResponse)
{
var response = await rpcCallExecutor.ExecuteWithNoRetries(
RpcCall.Create(nameof(IScriptService.CancelScript)),
async ct =>
{
- var request = new CancelScriptCommand(lastStatusResponse.Ticket, lastStatusResponse.NextLogSequence);
+ var request = new CancelScriptCommand(lastStatusResponse.ScriptTicket, lastStatusResponse.NextLogSequence);
var result = await clientScriptServiceV1.CancelScriptAsync(request, new HalibutProxyRequestOptions(ct));
return result;
@@ -116,16 +121,16 @@ protected override async Task Cancel(ScriptStatusResponse
clientOperationMetricsBuilder,
CancellationToken.None).ConfigureAwait(false);
- return response;
+ return Map(response);
}
- protected override async Task Finish(ScriptStatusResponse lastStatusResponse, CancellationToken scriptExecutionCancellationToken)
+ public async Task CompleteScript(CommandContext lastStatusResponse, CancellationToken scriptExecutionCancellationToken)
{
var response = await rpcCallExecutor.ExecuteWithNoRetries(
RpcCall.Create(nameof(IScriptService.CompleteScript)),
async ct =>
{
- var request = new CompleteScriptCommand(lastStatusResponse.Ticket, lastStatusResponse.NextLogSequence);
+ var request = new CompleteScriptCommand(lastStatusResponse.ScriptTicket, lastStatusResponse.NextLogSequence);
var result = await clientScriptServiceV1.CompleteScriptAsync(request, new HalibutProxyRequestOptions(ct));
return result;
@@ -134,9 +139,7 @@ protected override async Task Finish(ScriptStatusResponse
clientOperationMetricsBuilder,
CancellationToken.None).ConfigureAwait(false);
- OnScriptStatusResponseReceived(response);
-
- return response;
+ return MapToScriptStatus(response);
}
}
}
\ No newline at end of file
diff --git a/source/Octopus.Tentacle.Client/Scripts/ScriptServiceV2Orchestrator.cs b/source/Octopus.Tentacle.Client/Scripts/ScriptServiceV2Executor.cs
similarity index 61%
rename from source/Octopus.Tentacle.Client/Scripts/ScriptServiceV2Orchestrator.cs
rename to source/Octopus.Tentacle.Client/Scripts/ScriptServiceV2Executor.cs
index 149f7a6ae..51a79f410 100644
--- a/source/Octopus.Tentacle.Client/Scripts/ScriptServiceV2Orchestrator.cs
+++ b/source/Octopus.Tentacle.Client/Scripts/ScriptServiceV2Executor.cs
@@ -1,9 +1,9 @@
using System;
-using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Halibut;
using Halibut.ServiceModel;
+using Octopus.Tentacle.Client.EventDriven;
using Octopus.Tentacle.Client.Execution;
using Octopus.Tentacle.Client.Observability;
using Octopus.Tentacle.Client.Scripts.Models;
@@ -15,40 +15,35 @@
namespace Octopus.Tentacle.Client.Scripts
{
- class ScriptServiceV2Orchestrator : ObservingScriptOrchestrator
+ class ScriptServiceV2Executor : IScriptExecutor
{
readonly IAsyncClientScriptServiceV2 clientScriptServiceV2;
readonly RpcCallExecutor rpcCallExecutor;
readonly ClientOperationMetricsBuilder clientOperationMetricsBuilder;
readonly TimeSpan onCancellationAbandonCompleteScriptAfter;
readonly ITentacleClientTaskLog logger;
+ readonly TentacleClientOptions clientOptions;
- public ScriptServiceV2Orchestrator(
+ public ScriptServiceV2Executor(
IAsyncClientScriptServiceV2 clientScriptServiceV2,
- IScriptObserverBackoffStrategy scriptObserverBackOffStrategy,
RpcCallExecutor rpcCallExecutor,
ClientOperationMetricsBuilder clientOperationMetricsBuilder,
- OnScriptStatusResponseReceived onScriptStatusResponseReceived,
- OnScriptCompleted onScriptCompleted,
TimeSpan onCancellationAbandonCompleteScriptAfter,
TentacleClientOptions clientOptions,
ITentacleClientTaskLog logger)
- : base(scriptObserverBackOffStrategy,
- onScriptStatusResponseReceived,
- onScriptCompleted,
- clientOptions)
{
this.clientScriptServiceV2 = clientScriptServiceV2;
this.rpcCallExecutor = rpcCallExecutor;
this.clientOperationMetricsBuilder = clientOperationMetricsBuilder;
this.onCancellationAbandonCompleteScriptAfter = onCancellationAbandonCompleteScriptAfter;
+ this.clientOptions = clientOptions;
this.logger = logger;
}
- protected override StartScriptCommandV2 Map(ExecuteScriptCommand command)
+ StartScriptCommandV2 Map(ExecuteScriptCommand command)
{
if (command is not ExecuteShellScriptCommand shellScriptCommand)
- throw new InvalidOperationException($"{nameof(ScriptServiceV2Orchestrator)} only supports commands of type {nameof(ExecuteShellScriptCommand)}.");
+ throw new InvalidOperationException($"{nameof(ScriptServiceV2Executor)} only supports commands of type {nameof(ExecuteShellScriptCommand)}.");
return new StartScriptCommandV2(
shellScriptCommand.ScriptBody,
@@ -62,17 +57,19 @@ protected override StartScriptCommandV2 Map(ExecuteScriptCommand command)
shellScriptCommand.Scripts,
shellScriptCommand.Files.ToArray());
}
-
- protected override ScriptExecutionStatus MapToStatus(ScriptStatusResponseV2 response)
- => new(response.Logs);
-
- protected override ScriptExecutionResult MapToResult(ScriptStatusResponseV2 response)
- => new(response.State, response.ExitCode);
-
- protected override ProcessState GetState(ScriptStatusResponseV2 response) => response.State;
-
- protected override async Task StartScript(StartScriptCommandV2 command, CancellationToken scriptExecutionCancellationToken)
+
+ static ScriptOperationExecutionResult Map(ScriptStatusResponseV2 scriptStatusResponse)
{
+ return new (
+ new ScriptStatus(scriptStatusResponse.State, scriptStatusResponse.ExitCode, scriptStatusResponse.Logs),
+ new CommandContext(scriptStatusResponse.Ticket, scriptStatusResponse.NextLogSequence, ScriptServiceVersion.ScriptServiceVersion2));
+ }
+
+ public async Task StartScript(ExecuteScriptCommand executeScriptCommand,
+ StartScriptIsBeingReAttempted startScriptIsBeingReAttempted,
+ CancellationToken scriptExecutionCancellationToken)
+ {
+ var command = Map(executeScriptCommand);
ScriptStatusResponseV2 scriptStatusResponse;
var startScriptCallsConnectedCount = 0;
try
@@ -95,13 +92,15 @@ void OnErrorAction(Exception ex)
}
scriptStatusResponse = await rpcCallExecutor.Execute(
- retriesEnabled: ClientOptions.RpcRetrySettings.RetriesEnabled,
+ retriesEnabled: clientOptions.RpcRetrySettings.RetriesEnabled,
RpcCall.Create(nameof(IScriptServiceV2.StartScript)),
StartScriptAction,
OnErrorAction,
logger,
clientOperationMetricsBuilder,
scriptExecutionCancellationToken).ConfigureAwait(false);
+
+ return Map(scriptStatusResponse);
}
catch (Exception ex) when (scriptExecutionCancellationToken.IsCancellationRequested)
{
@@ -115,69 +114,43 @@ void OnErrorAction(Exception ex)
if (!startScriptCallIsConnecting || startScriptCallIsBeingRetried)
{
- // We have to assume the script started executing and call CancelScript and CompleteScript
- // We don't have a response so we need to create one to continue the execution flow
- scriptStatusResponse = new ScriptStatusResponseV2(
- command.ScriptTicket,
- ProcessState.Pending,
- ScriptExitCodes.RunningExitCode,
- new List(),
- 0);
-
- try
- {
- await ObserveUntilCompleteThenFinish(scriptStatusResponse, scriptExecutionCancellationToken).ConfigureAwait(false);
- }
- catch (Exception observerUntilCompleteException)
- {
- // Throw an error so the caller knows that execution of the script was cancelled
- throw new OperationCanceledException("Script execution was cancelled", observerUntilCompleteException);
- }
-
- // Throw an error so the caller knows that execution of the script was cancelled
- throw new OperationCanceledException("Script execution was cancelled");
+ // We want to cancel the potentially started script, and wait till it finishes. By returning a result, the outer orchestration will take care of this.
+ return ScriptOperationExecutionResult.CreateScriptStartedResult(command.ScriptTicket, ScriptServiceVersion.ScriptServiceVersion2);
}
// If the StartScript call was not in-flight or being retries then we know the script has not started executing on Tentacle
// So can exit without calling CancelScript or CompleteScript
throw new OperationCanceledException("Script execution was cancelled", ex);
}
-
- return scriptStatusResponse;
}
- protected override async Task GetStatus(ScriptStatusResponseV2 lastStatusResponse, CancellationToken scriptExecutionCancellationToken)
+
+
+ public async Task GetStatus(CommandContext commandContext, CancellationToken scriptExecutionCancellationToken)
{
- try
+ async Task GetStatusAction(CancellationToken ct)
{
- async Task GetStatusAction(CancellationToken ct)
- {
- var request = new ScriptStatusRequestV2(lastStatusResponse.Ticket, lastStatusResponse.NextLogSequence);
- var result = await clientScriptServiceV2.GetStatusAsync(request, new HalibutProxyRequestOptions(ct));
+ var request = new ScriptStatusRequestV2(commandContext.ScriptTicket, commandContext.NextLogSequence);
+ var result = await clientScriptServiceV2.GetStatusAsync(request, new HalibutProxyRequestOptions(ct));
- return result;
- }
-
- return await rpcCallExecutor.Execute(
- retriesEnabled: ClientOptions.RpcRetrySettings.RetriesEnabled,
- RpcCall.Create(nameof(IScriptServiceV2.GetStatus)),
- GetStatusAction,
- logger,
- clientOperationMetricsBuilder,
- scriptExecutionCancellationToken).ConfigureAwait(false);
- }
- catch (Exception e) when (e is OperationCanceledException && scriptExecutionCancellationToken.IsCancellationRequested)
- {
- // Return the last known response without logs when cancellation occurs and let the script execution go into the CancelScript and CompleteScript flow
- return new ScriptStatusResponseV2(lastStatusResponse.Ticket, lastStatusResponse.State, lastStatusResponse.ExitCode, new List(), lastStatusResponse.NextLogSequence);
+ return result;
}
+
+ var scriptStatusResponseV2 = await rpcCallExecutor.Execute(
+ retriesEnabled: clientOptions.RpcRetrySettings.RetriesEnabled,
+ RpcCall.Create(nameof(IScriptServiceV2.GetStatus)),
+ GetStatusAction,
+ logger,
+ clientOperationMetricsBuilder,
+ scriptExecutionCancellationToken).ConfigureAwait(false);
+ return Map(scriptStatusResponseV2);
}
- protected override async Task Cancel(ScriptStatusResponseV2 lastStatusResponse, CancellationToken scriptExecutionCancellationToken)
+ public async Task CancelScript(CommandContext lastStatusResponse)
{
async Task CancelScriptAction(CancellationToken ct)
{
- var request = new CancelScriptCommandV2(lastStatusResponse.Ticket, lastStatusResponse.NextLogSequence);
+ var request = new CancelScriptCommandV2(lastStatusResponse.ScriptTicket, lastStatusResponse.NextLogSequence);
var result = await clientScriptServiceV2.CancelScriptAsync(request, new HalibutProxyRequestOptions(ct));
return result;
@@ -187,17 +160,18 @@ async Task CancelScriptAction(CancellationToken ct)
// If script execution is already triggering RPC Retries and then the script execution is cancelled there is a high chance that the cancel RPC call will fail as well and go into RPC retries.
// We could potentially reduce the time to failure by not retrying the cancel RPC Call if the previous RPC call was already triggering RPC Retries.
- return await rpcCallExecutor.Execute(
- retriesEnabled: ClientOptions.RpcRetrySettings.RetriesEnabled,
+ var scriptStatusResponseV2 = await rpcCallExecutor.Execute(
+ retriesEnabled: clientOptions.RpcRetrySettings.RetriesEnabled,
RpcCall.Create(nameof(IScriptServiceV2.CancelScript)),
CancelScriptAction,
logger,
clientOperationMetricsBuilder,
// We don't want to cancel this operation as it is responsible for stopping the script executing on the Tentacle
CancellationToken.None).ConfigureAwait(false);
+ return Map(scriptStatusResponseV2);
}
- protected override async Task Finish(ScriptStatusResponseV2 lastStatusResponse, CancellationToken scriptExecutionCancellationToken)
+ public async Task CompleteScript(CommandContext lastStatusResponse, CancellationToken scriptExecutionCancellationToken)
{
try
{
@@ -212,7 +186,7 @@ await rpcCallExecutor.ExecuteWithNoRetries(
RpcCall.Create(nameof(IScriptServiceV2.CompleteScript)),
async ct =>
{
- var request = new CompleteScriptCommandV2(lastStatusResponse.Ticket);
+ var request = new CompleteScriptCommandV2(lastStatusResponse.ScriptTicket);
await clientScriptServiceV2.CompleteScriptAsync(request, new HalibutProxyRequestOptions(ct));
},
logger,
@@ -225,7 +199,7 @@ await rpcCallExecutor.ExecuteWithNoRetries(
logger.Verbose(ex);
}
- return lastStatusResponse;
+ return null;
}
}
}
\ No newline at end of file
diff --git a/source/Octopus.Tentacle.Client/Scripts/ScriptServiceVersion.cs b/source/Octopus.Tentacle.Client/Scripts/ScriptServiceVersion.cs
index 035ea0f53..972256b9b 100644
--- a/source/Octopus.Tentacle.Client/Scripts/ScriptServiceVersion.cs
+++ b/source/Octopus.Tentacle.Client/Scripts/ScriptServiceVersion.cs
@@ -1,10 +1,9 @@
namespace Octopus.Tentacle.Client.Scripts
{
- record ScriptServiceVersion(string Value)
+ public record ScriptServiceVersion(string Value)
{
public static ScriptServiceVersion ScriptServiceVersion1 = new(nameof(ScriptServiceVersion1));
public static ScriptServiceVersion ScriptServiceVersion2 = new(nameof(ScriptServiceVersion2));
- public static ScriptServiceVersion KubernetesScriptServiceVersion1Alpha = new(nameof(KubernetesScriptServiceVersion1Alpha));
public static ScriptServiceVersion KubernetesScriptServiceVersion1 = new(nameof(KubernetesScriptServiceVersion1));
public override string ToString() => Value;
diff --git a/source/Octopus.Tentacle.Client/Scripts/ScriptServiceVersionSelector.cs b/source/Octopus.Tentacle.Client/Scripts/ScriptServiceVersionSelector.cs
new file mode 100644
index 000000000..c8d7c57e2
--- /dev/null
+++ b/source/Octopus.Tentacle.Client/Scripts/ScriptServiceVersionSelector.cs
@@ -0,0 +1,94 @@
+using System.Threading;
+using System.Threading.Tasks;
+using Halibut.ServiceModel;
+using Octopus.Tentacle.Client.Capabilities;
+using Octopus.Tentacle.Client.Execution;
+using Octopus.Tentacle.Client.Kubernetes;
+using Octopus.Tentacle.Client.Observability;
+using Octopus.Tentacle.Contracts.Capabilities;
+using Octopus.Tentacle.Contracts.ClientServices;
+using Octopus.Tentacle.Contracts.Logging;
+using Octopus.Tentacle.Contracts.Observability;
+
+namespace Octopus.Tentacle.Client.Scripts
+{
+ public class ScriptServiceVersionSelector
+ {
+ readonly ITentacleClientTaskLog logger;
+ readonly IAsyncClientCapabilitiesServiceV2 clientCapabilitiesServiceV2;
+ readonly RpcCallExecutor rpcCallExecutor;
+ readonly TentacleClientOptions clientOptions;
+ readonly ClientOperationMetricsBuilder clientOperationMetricsBuilder;
+
+ internal ScriptServiceVersionSelector(
+ IAsyncClientCapabilitiesServiceV2 clientCapabilitiesServiceV2,
+ ITentacleClientTaskLog logger,
+ RpcCallExecutor rpcCallExecutor,
+ TentacleClientOptions clientOptions,
+ ClientOperationMetricsBuilder clientOperationMetricsBuilder)
+ {
+ this.clientCapabilitiesServiceV2 = clientCapabilitiesServiceV2;
+ this.logger = logger;
+ this.rpcCallExecutor = rpcCallExecutor;
+ this.clientOptions = clientOptions;
+ this.clientOperationMetricsBuilder = clientOperationMetricsBuilder;
+ }
+
+ public async Task DetermineScriptServiceVersionToUse(CancellationToken cancellationToken)
+ {
+ logger.Verbose("Determining ScriptService version to use");
+
+ async Task GetCapabilitiesFunc(CancellationToken ct)
+ {
+ var result = await clientCapabilitiesServiceV2.GetCapabilitiesAsync(new HalibutProxyRequestOptions(ct));
+
+ return result;
+ }
+
+ var tentacleCapabilities = await rpcCallExecutor.Execute(
+ retriesEnabled: clientOptions.RpcRetrySettings.RetriesEnabled,
+ RpcCall.Create(nameof(ICapabilitiesServiceV2.GetCapabilities)),
+ GetCapabilitiesFunc,
+ logger,
+ clientOperationMetricsBuilder,
+ cancellationToken);
+
+ logger.Verbose($"Discovered Tentacle capabilities: {string.Join(",", tentacleCapabilities.SupportedCapabilities)}");
+
+ // Check if we support any kubernetes script service.
+ // It's implied (and tested) that GetCapabilities will only return Kubernetes or non-Kubernetes script services, never a mix
+ if (tentacleCapabilities.HasAnyKubernetesScriptService())
+ {
+ return DetermineKubernetesScriptServiceVersionToUse();
+ }
+
+ return DetermineShellScriptServiceVersionToUse(tentacleCapabilities);
+ }
+
+ ScriptServiceVersion DetermineShellScriptServiceVersionToUse(CapabilitiesResponseV2 tentacleCapabilities)
+ {
+ if (tentacleCapabilities.HasScriptServiceV2())
+ {
+ logger.Verbose("Using ScriptServiceV2");
+ logger.Verbose(clientOptions.RpcRetrySettings.RetriesEnabled
+ ? $"RPC call retries are enabled. Retry timeout {rpcCallExecutor.RetryTimeout.TotalSeconds} seconds"
+ : "RPC call retries are disabled.");
+ return ScriptServiceVersion.ScriptServiceVersion2;
+ }
+
+ logger.Verbose("RPC call retries are enabled but will not be used for Script Execution as a compatible ScriptService was not found. Please upgrade Tentacle to enable this feature.");
+ logger.Verbose("Using ScriptServiceV1");
+ return ScriptServiceVersion.ScriptServiceVersion1;
+ }
+
+ ScriptServiceVersion DetermineKubernetesScriptServiceVersionToUse()
+ {
+ logger.Verbose($"Using KubernetesScriptServiceV1");
+ logger.Verbose(clientOptions.RpcRetrySettings.RetriesEnabled
+ ? $"RPC call retries are enabled. Retry timeout {rpcCallExecutor.RetryTimeout.TotalSeconds} seconds"
+ : "RPC call retries are disabled.");
+
+ return ScriptServiceVersion.KubernetesScriptServiceVersion1;
+ }
+ }
+}
\ No newline at end of file
diff --git a/source/Octopus.Tentacle.Client/Scripts/UnsafeStartAttemptException.cs b/source/Octopus.Tentacle.Client/Scripts/UnsafeStartAttemptException.cs
new file mode 100644
index 000000000..1ae27b201
--- /dev/null
+++ b/source/Octopus.Tentacle.Client/Scripts/UnsafeStartAttemptException.cs
@@ -0,0 +1,20 @@
+using System;
+
+namespace Octopus.Tentacle.Client.Scripts
+{
+ ///
+ /// This is thrown when we are attempting to start a script using a V1 script service, but we are trying to do so for the second time.
+ /// For resilient deployments, we need to ensure idempotency for our operations, including starting a script.
+ /// Idempotency was built into script service V2 onwards.
+ /// If we attempt to use a V1 script service on the resilient pipeline, then we are allowed to start the script.
+ /// But if anything goes wrong, and we need to reattempt, then we cannot guarantee the script has not already started.
+ /// In this case, the safest action is to fail (with this exception).
+ ///
+ public class UnsafeStartAttemptException : Exception
+ {
+ public UnsafeStartAttemptException(string message) : base(message)
+ {
+
+ }
+ }
+}
\ No newline at end of file
diff --git a/source/Octopus.Tentacle.Client/ServiceHelpers/AllClients.cs b/source/Octopus.Tentacle.Client/ServiceHelpers/AllClients.cs
new file mode 100644
index 000000000..57bd2f416
--- /dev/null
+++ b/source/Octopus.Tentacle.Client/ServiceHelpers/AllClients.cs
@@ -0,0 +1,41 @@
+using Halibut;
+using Octopus.Tentacle.Contracts;
+using Octopus.Tentacle.Contracts.Capabilities;
+using Octopus.Tentacle.Contracts.ClientServices;
+using Octopus.Tentacle.Contracts.KubernetesScriptServiceV1;
+using Octopus.Tentacle.Contracts.ScriptServiceV2;
+
+namespace Octopus.Tentacle.Client.ServiceHelpers
+{
+ public class AllClients
+ {
+ public IAsyncClientScriptService ScriptServiceV1 { get; }
+ public IAsyncClientScriptServiceV2 ScriptServiceV2 { get; }
+ public IAsyncClientKubernetesScriptServiceV1 KubernetesScriptServiceV1 { get; }
+ public IAsyncClientFileTransferService ClientFileTransferServiceV1 { get; }
+ public IAsyncClientCapabilitiesServiceV2 CapabilitiesServiceV2 { get; }
+
+ public AllClients(IHalibutRuntime halibutRuntime, ServiceEndPoint serviceEndPoint) : this(halibutRuntime, serviceEndPoint, null)
+ {
+ }
+
+
+ internal AllClients(IHalibutRuntime halibutRuntime, ServiceEndPoint serviceEndPoint, ITentacleServiceDecoratorFactory? tentacleServicesDecoratorFactory)
+ {
+ ScriptServiceV1 = halibutRuntime.CreateAsyncClient(serviceEndPoint);
+ ScriptServiceV2 = halibutRuntime.CreateAsyncClient(serviceEndPoint);
+ KubernetesScriptServiceV1 = halibutRuntime.CreateAsyncClient(serviceEndPoint);
+ ClientFileTransferServiceV1 = halibutRuntime.CreateAsyncClient(serviceEndPoint);
+ CapabilitiesServiceV2 = halibutRuntime.CreateAsyncClient(serviceEndPoint).WithBackwardsCompatability();
+
+ if (tentacleServicesDecoratorFactory != null)
+ {
+ ScriptServiceV1 = tentacleServicesDecoratorFactory.Decorate(ScriptServiceV1);
+ ScriptServiceV2 = tentacleServicesDecoratorFactory.Decorate(ScriptServiceV2);
+ KubernetesScriptServiceV1 = tentacleServicesDecoratorFactory.Decorate(KubernetesScriptServiceV1);
+ ClientFileTransferServiceV1 = tentacleServicesDecoratorFactory.Decorate(ClientFileTransferServiceV1);
+ CapabilitiesServiceV2 = tentacleServicesDecoratorFactory.Decorate(CapabilitiesServiceV2);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/source/Octopus.Tentacle.Client/StartScriptIsBeingReAttempted.cs b/source/Octopus.Tentacle.Client/StartScriptIsBeingReAttempted.cs
new file mode 100644
index 000000000..e9ab7ac44
--- /dev/null
+++ b/source/Octopus.Tentacle.Client/StartScriptIsBeingReAttempted.cs
@@ -0,0 +1,10 @@
+using System;
+
+namespace Octopus.Tentacle.Client
+{
+ public enum StartScriptIsBeingReAttempted
+ {
+ FirstAttempt,
+ PossiblyBeingReAttempted
+ }
+}
\ No newline at end of file
diff --git a/source/Octopus.Tentacle.Client/TentacleClient.cs b/source/Octopus.Tentacle.Client/TentacleClient.cs
index b5ca6e82d..f5e9ead37 100644
--- a/source/Octopus.Tentacle.Client/TentacleClient.cs
+++ b/source/Octopus.Tentacle.Client/TentacleClient.cs
@@ -8,13 +8,11 @@
using Octopus.Tentacle.Client.Observability;
using Octopus.Tentacle.Client.Scripts;
using Octopus.Tentacle.Client.Scripts.Models;
+using Octopus.Tentacle.Client.ServiceHelpers;
using Octopus.Tentacle.Contracts;
using Octopus.Tentacle.Contracts.Capabilities;
-using Octopus.Tentacle.Contracts.ClientServices;
-using Octopus.Tentacle.Contracts.KubernetesScriptServiceV1;
using Octopus.Tentacle.Contracts.Logging;
using Octopus.Tentacle.Contracts.Observability;
-using Octopus.Tentacle.Contracts.ScriptServiceV2;
using ITentacleClientObserver = Octopus.Tentacle.Contracts.Observability.ITentacleClientObserver;
namespace Octopus.Tentacle.Client
@@ -24,13 +22,9 @@ public class TentacleClient : ITentacleClient
readonly IScriptObserverBackoffStrategy scriptObserverBackOffStrategy;
readonly ITentacleClientObserver tentacleClientObserver;
readonly RpcCallExecutor rpcCallExecutor;
-
- readonly IAsyncClientScriptService scriptServiceV1;
- readonly IAsyncClientScriptServiceV2 scriptServiceV2;
- readonly IAsyncClientKubernetesScriptServiceV1 kubernetesScriptServiceV1;
- readonly IAsyncClientFileTransferService clientFileTransferServiceV1;
- readonly IAsyncClientCapabilitiesServiceV2 capabilitiesServiceV2;
+
readonly TentacleClientOptions clientOptions;
+ readonly AllClients allClients;
public static void CacheServiceWasNotFoundResponseMessages(IHalibutRuntime halibutRuntime)
{
@@ -69,7 +63,7 @@ internal TentacleClient(
this.tentacleClientObserver = tentacleClientObserver.DecorateWithNonThrowingTentacleClientObserver();
this.clientOptions = clientOptions;
-
+
if (halibutRuntime.OverrideErrorResponseMessageCaching == null)
{
// Best effort to make sure the HalibutRuntime has been configured to Cache ServiceNotFoundExceptions
@@ -77,20 +71,7 @@ internal TentacleClient(
throw new ArgumentException("Ensure that TentacleClient.CacheServiceWasNotFoundResponseMessages has been called for the HalibutRuntime", nameof(halibutRuntime));
}
- scriptServiceV1 = halibutRuntime.CreateAsyncClient(serviceEndPoint);
- scriptServiceV2 = halibutRuntime.CreateAsyncClient(serviceEndPoint);
- kubernetesScriptServiceV1 = halibutRuntime.CreateAsyncClient(serviceEndPoint);
- clientFileTransferServiceV1 = halibutRuntime.CreateAsyncClient(serviceEndPoint);
- capabilitiesServiceV2 = halibutRuntime.CreateAsyncClient(serviceEndPoint).WithBackwardsCompatability();
-
- if (tentacleServicesDecoratorFactory != null)
- {
- scriptServiceV1 = tentacleServicesDecoratorFactory.Decorate(scriptServiceV1);
- scriptServiceV2 = tentacleServicesDecoratorFactory.Decorate(scriptServiceV2);
- kubernetesScriptServiceV1 = tentacleServicesDecoratorFactory.Decorate(kubernetesScriptServiceV1);
- clientFileTransferServiceV1 = tentacleServicesDecoratorFactory.Decorate(clientFileTransferServiceV1);
- capabilitiesServiceV2 = tentacleServicesDecoratorFactory.Decorate(capabilitiesServiceV2);
- }
+ allClients = new AllClients(halibutRuntime, serviceEndPoint, tentacleServicesDecoratorFactory);
rpcCallExecutor = RpcCallExecutorFactory.Create(this.clientOptions.RpcRetrySettings.RetryDuration, this.tentacleClientObserver);
}
@@ -104,7 +85,7 @@ public async Task UploadFile(string fileName, string path, DataStr
async Task UploadFileAction(CancellationToken ct)
{
logger.Info($"Beginning upload of {fileName} to Tentacle");
- var result = await clientFileTransferServiceV1.UploadFileAsync(path, package, new HalibutProxyRequestOptions(ct));
+ var result = await allClients.ClientFileTransferServiceV1.UploadFileAsync(path, package, new HalibutProxyRequestOptions(ct));
logger.Info("Upload complete");
return result;
@@ -139,7 +120,7 @@ async Task UploadFileAction(CancellationToken ct)
async Task DownloadFileAction(CancellationToken ct)
{
logger.Info($"Beginning download of {Path.GetFileName(remotePath)} from Tentacle");
- var result = await clientFileTransferServiceV1.DownloadFileAsync(remotePath, new HalibutProxyRequestOptions(ct));
+ var result = await allClients.ClientFileTransferServiceV1.DownloadFileAsync(remotePath, new HalibutProxyRequestOptions(ct));
logger.Info("Download complete");
return result;
@@ -177,21 +158,18 @@ public async Task ExecuteScript(ExecuteScriptCommand exec
try
{
- var factory = new ScriptOrchestratorFactory(
- scriptServiceV1,
- scriptServiceV2,
- kubernetesScriptServiceV1,
- capabilitiesServiceV2,
- scriptObserverBackOffStrategy,
- rpcCallExecutor,
+ var scriptExecutor = new ScriptExecutor(
+ allClients,
+ logger,
+ tentacleClientObserver,
operationMetricsBuilder,
+ clientOptions,
+ OnCancellationAbandonCompleteScriptAfter);
+
+ var orchestrator = new ObservingScriptOrchestrator(scriptObserverBackOffStrategy,
onScriptStatusResponseReceived,
onScriptCompleted,
- OnCancellationAbandonCompleteScriptAfter,
- clientOptions,
- logger);
-
- var orchestrator = await factory.CreateOrchestrator(scriptExecutionCancellationToken);
+ scriptExecutor);
var result = await orchestrator.ExecuteScript(executeScriptCommand, scriptExecutionCancellationToken);
diff --git a/source/Octopus.Tentacle.Contracts/ScriptStatus.cs b/source/Octopus.Tentacle.Contracts/ScriptStatus.cs
new file mode 100644
index 000000000..de6a7aa77
--- /dev/null
+++ b/source/Octopus.Tentacle.Contracts/ScriptStatus.cs
@@ -0,0 +1,18 @@
+using System.Collections.Generic;
+
+namespace Octopus.Tentacle.Contracts
+{
+ public class ScriptStatus
+ {
+ public ProcessState State { get; }
+ public int? ExitCode { get; }
+ public List Logs { get; }
+
+ public ScriptStatus(ProcessState state, int? exitCode, List logs)
+ {
+ State = state;
+ ExitCode = exitCode;
+ Logs = logs;
+ }
+ }
+}
\ No newline at end of file
diff --git a/source/Tentacle.sln.DotSettings b/source/Tentacle.sln.DotSettings
index 0e7802a6d..2c88b318a 100644
--- a/source/Tentacle.sln.DotSettings
+++ b/source/Tentacle.sln.DotSettings
@@ -183,6 +183,7 @@
True
True
True
+ True
True
True
True