Skip to content

Commit

Permalink
Merge pull request #142 from UiPath/feature/configurable-attempts-and…
Browse files Browse the repository at this point in the history
…-timeout

Configurable call timeouts and max attempts
  • Loading branch information
thschmitt authored Feb 4, 2025
2 parents 8086db0 + 70a06b9 commit b618e2b
Show file tree
Hide file tree
Showing 7 changed files with 111 additions and 6 deletions.
10 changes: 10 additions & 0 deletions commandline/command_builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,14 @@ func (b CommandBuilder) createOperationCommand(operation parser.Operation) *Comm
tenant = config.Tenant
}
insecure := context.Bool(FlagNameInsecure) || config.Insecure
timeout := time.Duration(context.Int(FlagNameCallTimeout)) * time.Second
if timeout < 0 {
return fmt.Errorf("Invalid value for '%s'", FlagNameCallTimeout)
}
maxAttempts := context.Int(FlagNameMaxAttempts)
if maxAttempts < 1 {
return fmt.Errorf("Invalid value for '%s'", FlagNameMaxAttempts)
}
debug := context.Bool(FlagNameDebug) || config.Debug
identityUri, err := b.createIdentityUri(context, *config, baseUri)
if err != nil {
Expand All @@ -343,6 +351,8 @@ func (b CommandBuilder) createOperationCommand(operation parser.Operation) *Comm
parameters,
config.Auth,
insecure,
timeout,
maxAttempts,
debug,
*identityUri,
operation.Plugin)
Expand Down
12 changes: 12 additions & 0 deletions commandline/flag_builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ const FlagNameIdentityUri = "identity-uri"
const FlagNameServiceVersion = "service-version"
const FlagNameHelp = "help"
const FlagNameVersion = "version"
const FlagNameCallTimeout = "call-timeout"
const FlagNameMaxAttempts = "max-attempts"

const FlagValueFromStdIn = "-"
const FlagValueOutputFormatJson = "json"
Expand All @@ -33,6 +35,8 @@ var FlagNamesPredefined = []string{
FlagNameOrganization,
FlagNameTenant,
FlagNameInsecure,
FlagNameCallTimeout,
FlagNameMaxAttempts,
FlagNameOutputFormat,
FlagNameQuery,
FlagNameWait,
Expand Down Expand Up @@ -118,6 +122,14 @@ func (b FlagBuilder) defaultFlags(hidden bool) []*FlagDefinition {
WithEnvVarName("UIPATH_INSECURE").
WithDefaultValue(false).
WithHidden(hidden),
NewFlag(FlagNameCallTimeout, "Call Timeout", FlagTypeInteger).
WithEnvVarName("UIPATH_CALL_TIMEOUT").
WithDefaultValue(60).
WithHidden(true),
NewFlag(FlagNameMaxAttempts, "Max Attempts", FlagTypeInteger).
WithEnvVarName("UIPATH_MAX_ATTEMPTS").
WithDefaultValue(3).
WithHidden(true),
NewFlag(FlagNameOutputFormat, fmt.Sprintf("Set output format: %s (default), %s", FlagValueOutputFormatJson, FlagValueOutputFormatText), FlagTypeString).
WithEnvVarName("UIPATH_OUTPUT").
WithDefaultValue("").
Expand Down
23 changes: 22 additions & 1 deletion executor/execution_context.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package executor

import (
"net/url"
"time"

"github.com/UiPath/uipathcli/config"
"github.com/UiPath/uipathcli/plugin"
Expand All @@ -21,6 +22,8 @@ type ExecutionContext struct {
Parameters ExecutionParameters
AuthConfig config.AuthConfig
Insecure bool
Timeout time.Duration
MaxAttempts int
Debug bool
IdentityUri url.URL
Plugin plugin.CommandPlugin
Expand All @@ -37,8 +40,26 @@ func NewExecutionContext(
parameters []ExecutionParameter,
authConfig config.AuthConfig,
insecure bool,
timeout time.Duration,
maxAttempts int,
debug bool,
identityUri url.URL,
plugin plugin.CommandPlugin) *ExecutionContext {
return &ExecutionContext{organization, tenant, method, uri, route, contentType, input, parameters, authConfig, insecure, debug, identityUri, plugin}
return &ExecutionContext{
organization,
tenant,
method,
uri,
route,
contentType,
input,
parameters,
authConfig,
insecure,
timeout,
maxAttempts,
debug,
identityUri,
plugin,
}
}
5 changes: 2 additions & 3 deletions executor/http_executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import (
"net/url"
"runtime"
"strings"
"time"

"github.com/UiPath/uipathcli/auth"
"github.com/UiPath/uipathcli/config"
Expand Down Expand Up @@ -44,7 +43,7 @@ type HttpExecutor struct {
}

func (e HttpExecutor) Call(context ExecutionContext, writer output.OutputWriter, logger log.Logger) error {
return resiliency.Retry(func() error {
return resiliency.RetryN(context.MaxAttempts, func() error {
return e.call(context, writer, logger)
})
}
Expand Down Expand Up @@ -345,7 +344,7 @@ func (e HttpExecutor) call(context ExecutionContext, writer output.OutputWriter,

transport := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: context.Insecure}, //nolint // This is user configurable and disabled by default
ResponseHeaderTimeout: 60 * time.Second,
ResponseHeaderTimeout: context.Timeout,
}
client := &http.Client{Transport: transport}
if context.Debug {
Expand Down
18 changes: 17 additions & 1 deletion test/show_command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,23 @@ paths:
names = append(names, parameter["name"].(string))
}

expectedNames := []string{"debug", "profile", "uri", "organization", "tenant", "insecure", "output", "query", "wait", "wait-timeout", "file", "identity-uri", "service-version", "help"}
expectedNames := []string{
"debug",
"profile",
"uri",
"organization",
"tenant",
"insecure",
"call-timeout",
"max-attempts",
"output",
"query",
"wait",
"wait-timeout",
"file",
"identity-uri",
"service-version",
"help"}
if !reflect.DeepEqual(names, expectedNames) {
t.Errorf("Unexpected global parameters in output, expected: %v but got: %v", expectedNames, names)
}
Expand Down
42 changes: 42 additions & 0 deletions test/validation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,48 @@ import (
"testing"
)

func TestInvalidCallTimeoutShowsError(t *testing.T) {
definition := `
paths:
/ping:
get:
summary: Simple ping
`

context := NewContextBuilder().
WithDefinition("myservice", definition).
WithResponse(200, "").
Build()

result := RunCli([]string{"myservice", "get-ping", "--call-timeout", "-1"}, context)

expected := "Invalid value for 'call-timeout'"
if !strings.Contains(result.StdErr, expected) {
t.Errorf("stderr does not invalid call-timeout error error, expected: %v, got: %v", expected, result.StdErr)
}
}

func TestInvalidMaxAttemptsShowsError(t *testing.T) {
definition := `
paths:
/ping:
get:
summary: Simple ping
`

context := NewContextBuilder().
WithDefinition("myservice", definition).
WithResponse(200, "").
Build()

result := RunCli([]string{"myservice", "get-ping", "--max-attempts", "0"}, context)

expected := "Invalid value for 'max-attempts'"
if !strings.Contains(result.StdErr, expected) {
t.Errorf("stderr does not invalid call-attempts error error, expected: %v, got: %v", expected, result.StdErr)
}
}

func TestMissingRequiredParameterShowsError(t *testing.T) {
definition := `
paths:
Expand Down
7 changes: 6 additions & 1 deletion utils/resiliency/retry.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,16 @@ const MaxAttempts = 3

// Retries the given function up to 3 times when it returns an RetryableError.
func Retry(f func() error) error {
return RetryN(MaxAttempts, f)
}

// Retries the given function up to n times when it returns an RetryableError.
func RetryN(maxAttempts int, f func() error) error {
var err error
for i := 1; ; i++ {
err = f()
_, retryable := err.(*RetryableError)
if !retryable || i == MaxAttempts {
if !retryable || i == maxAttempts {
return err
}
time.Sleep(1 * time.Second)
Expand Down

0 comments on commit b618e2b

Please sign in to comment.