Skip to content

Commit

Permalink
Merge pull request #13 from Clever/custom-errors
Browse files Browse the repository at this point in the history
Custom errors
  • Loading branch information
samfishman authored Nov 29, 2017
2 parents 7eae071 + 726764a commit 2e9d2d1
Show file tree
Hide file tree
Showing 3 changed files with 63 additions and 54 deletions.
69 changes: 40 additions & 29 deletions cmd/sfncli/error_names.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,21 @@ import (

// TaskFailureError is the error reported when failing an activity task.
type TaskFailureError interface {
error
ErrorName() string
ErrorCause() string

error
}

// sendTaskFailure handles sending AWS `SendTaskFailure`.
func (t TaskRunner) sendTaskFailure(err TaskFailureError) error {
t.logger.ErrorD("send-task-failure", logger.M{"name": err.ErrorName(), "error": err.Error()})
t.logger.ErrorD("send-task-failure", logger.M{"name": err.ErrorName(), "cause": err.ErrorCause()})

// don't use SendTaskFailureWithContext, since the failure itself could be from the parent
// context being cancelled, but we still want to report to AWS the failure of the task.
_, sendErr := t.sfnapi.SendTaskFailure(&sfn.SendTaskFailureInput{
Cause: aws.String(err.Error()),
Error: aws.String(err.ErrorName()),
Cause: aws.String(err.ErrorCause()),
TaskToken: &t.taskToken,
})
if sendErr != nil {
Expand All @@ -42,74 +44,83 @@ type TaskFailureUnknown struct {
error
}

func (t TaskFailureUnknown) ErrorName() string { return "sfncli.Unknown" }
func (t TaskFailureUnknown) ErrorName() string { return "sfncli.Unknown" }
func (t TaskFailureUnknown) ErrorCause() string { return t.Error() }

// TaskFailureTaskInputNotJSON is used when the input to the task is not a JSON object.
type TaskFailureTaskInputNotJSON struct {
input string
}

func (t TaskFailureTaskInputNotJSON) Error() string {
func (t TaskFailureTaskInputNotJSON) ErrorName() string { return "sfncli.TaskInputNotJSON" }
func (t TaskFailureTaskInputNotJSON) ErrorCause() string {
return fmt.Sprintf("task input not valid JSON: '%s'", t.input)
}

func (t TaskFailureTaskInputNotJSON) ErrorName() string { return "sfncli.TaskInputNotJSON" }
func (t TaskFailureTaskInputNotJSON) Error() string { return t.ErrorCause() }

// TaskFailureCommandNotFound is used when the command passed to sfncli is not found.
type TaskFailureCommandNotFound struct {
path string
}

func (t TaskFailureCommandNotFound) Error() string {
func (t TaskFailureCommandNotFound) ErrorName() string { return "sfncli.CommandNotFound" }
func (t TaskFailureCommandNotFound) ErrorCause() string {
return fmt.Sprintf("command not found: '%s'", t.path)
}

func (t TaskFailureCommandNotFound) ErrorName() string { return "sfncli.CommandNotFound" }
func (t TaskFailureCommandNotFound) Error() string { return t.ErrorCause() }

// TaskFailureCommandKilled happens when the command is sent a kill signal by the OS.
type TaskFailureCommandKilled struct {
stderr string
}

func (t TaskFailureCommandKilled) Error() string { return t.stderr }

func (t TaskFailureCommandKilled) ErrorName() string { return "sfncli.CommandKilled" }
func (t TaskFailureCommandKilled) ErrorName() string { return "sfncli.CommandKilled" }
func (t TaskFailureCommandKilled) ErrorCause() string { return t.stderr }
func (t TaskFailureCommandKilled) Error() string {
return fmt.Sprintf("%s: %s", t.ErrorName(), t.ErrorCause())
}

// TaskFailureCommandKilled happens when the command exits with a nonzero exit code and doesn't specifiy its own error name in the output.
type TaskFailureCommandExitedNonzero struct {
stderr string
}

func (t TaskFailureCommandExitedNonzero) Error() string { return t.stderr }

func (t TaskFailureCommandExitedNonzero) ErrorName() string { return "sfncli.CommandExitedNonzero" }

// TaskFailureCustomErrorName happens when the command exits with a nonzero exit code and outputs a custom error name to stdout.
type TaskFailureCustomErrorName struct {
errorName string
stderr string
func (t TaskFailureCommandExitedNonzero) ErrorName() string { return "sfncli.CommandExitedNonzero" }
func (t TaskFailureCommandExitedNonzero) ErrorCause() string { return t.stderr }
func (t TaskFailureCommandExitedNonzero) Error() string {
return fmt.Sprintf("%s: %s", t.ErrorName(), t.ErrorCause())
}

func (t TaskFailureCustomErrorName) Error() string { return t.stderr }
// TaskFailureCustom happens when the command exits with a nonzero exit code and outputs a custom error name to stdout.
type TaskFailureCustom struct {
Err string `json:"error"`
Cause string `json:"cause"`
}

func (t TaskFailureCustomErrorName) ErrorName() string { return t.errorName }
func (t TaskFailureCustom) ErrorName() string { return t.Err }
func (t TaskFailureCustom) ErrorCause() string { return t.Cause }
func (t TaskFailureCustom) Error() string {
return fmt.Sprintf("%s: %s", t.ErrorName(), t.ErrorCause())
}

// TaskFailureTaskOutputNotJSON is used when the output of the task is not a JSON object.
type TaskFailureTaskOutputNotJSON struct {
output string
}

func (t TaskFailureTaskOutputNotJSON) Error() string {
func (t TaskFailureTaskOutputNotJSON) ErrorName() string { return "sfncli.TaskOutputNotJSON" }
func (t TaskFailureTaskOutputNotJSON) ErrorCause() string {
return fmt.Sprintf("stdout not valid JSON: '%s'", t.output)
}

func (t TaskFailureTaskOutputNotJSON) ErrorName() string { return "sfncli.TaskOutputNotJSON" }
func (t TaskFailureTaskOutputNotJSON) Error() string { return t.ErrorCause() }

// TaskFailureCommandKilled happens when sfncli receives SIGTERM.
type TaskFailureCommandTerminated struct {
stderr string
}

func (t TaskFailureCommandTerminated) Error() string { return t.stderr }

func (t TaskFailureCommandTerminated) ErrorName() string { return "sfncli.CommandTerminated" }
func (t TaskFailureCommandTerminated) ErrorName() string { return "sfncli.CommandTerminated" }
func (t TaskFailureCommandTerminated) ErrorCause() string { return t.stderr }
func (t TaskFailureCommandTerminated) Error() string {
return fmt.Sprintf("%s: %s", t.ErrorName(), t.ErrorCause())
}
22 changes: 10 additions & 12 deletions cmd/sfncli/runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,20 +111,20 @@ func (t *TaskRunner) Process(ctx context.Context, args []string, input string) e
t.logger.InfoD("exec-command-start", logger.M{"args": args, "cmd": t.cmd, "workdirectory": tmpDir})
}
if err := t.execCmd.Run(); err != nil {
stderr := strings.TrimSpace(stderrbuf.String()) // remove trailing newline
customErrorName := parseCustomErrorNameFromStdout(stdoutbuf.String())
stderr := strings.TrimSpace(stderrbuf.String()) // remove trailing newline
customError, _ := parseCustomErrorFromStdout(stdoutbuf.String()) // ignore parsing errors
if t.receivedSigterm {
if customErrorName != "" {
return t.sendTaskFailure(TaskFailureCustomErrorName{errorName: customErrorName, stderr: stderr})
if customError.ErrorName() != "" {
return t.sendTaskFailure(customError)
}
return t.sendTaskFailure(TaskFailureCommandTerminated{stderr: stderr})
}
switch err := err.(type) {
case *os.PathError:
return t.sendTaskFailure(TaskFailureCommandNotFound{path: err.Path})
case *exec.ExitError:
if customErrorName != "" {
return t.sendTaskFailure(TaskFailureCustomErrorName{errorName: customErrorName, stderr: stderr})
if customError.ErrorName() != "" {
return t.sendTaskFailure(customError)
}
status := err.ProcessState.Sys().(syscall.WaitStatus)
switch {
Expand Down Expand Up @@ -201,12 +201,10 @@ func signalProcess(pid int, signal os.Signal) {
proc.Signal(signal)
}

func parseCustomErrorNameFromStdout(stdout string) string {
var customError struct {
ErrorName string `json:"error_name"`
}
json.Unmarshal([]byte(taskOutputFromStdout(stdout)), &customError)
return customError.ErrorName
func parseCustomErrorFromStdout(stdout string) (TaskFailureCustom, error) {
var customError TaskFailureCustom
err := json.Unmarshal([]byte(taskOutputFromStdout(stdout)), &customError)
return customError, err
}

func taskOutputFromStdout(stdout string) string {
Expand Down
26 changes: 13 additions & 13 deletions cmd/sfncli/runner_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ func TestTaskFailureTaskInputNotJSON(t *testing.T) {
defer controller.Finish()
mockSFN := mocksfn.NewMockSFNAPI(controller)
mockSFN.EXPECT().SendTaskFailure(&sfn.SendTaskFailureInput{
Cause: aws.String(expectedError.Error()),
Cause: aws.String(expectedError.ErrorCause()),
Error: aws.String(expectedError.ErrorName()),
TaskToken: aws.String(mockTaskToken),
})
Expand Down Expand Up @@ -111,7 +111,7 @@ func TestTaskFailureCommandNotFound(t *testing.T) {
defer controller.Finish()
mockSFN := mocksfn.NewMockSFNAPI(controller)
mockSFN.EXPECT().SendTaskFailure(&sfn.SendTaskFailureInput{
Cause: aws.String(expectedError.Error()),
Cause: aws.String(expectedError.ErrorCause()),
Error: aws.String(expectedError.ErrorName()),
TaskToken: aws.String(mockTaskToken),
})
Expand All @@ -132,7 +132,7 @@ func TestTaskFailureCommandKilled(t *testing.T) {
defer controller.Finish()
mockSFN := mocksfn.NewMockSFNAPI(controller)
mockSFN.EXPECT().SendTaskFailure(&sfn.SendTaskFailureInput{
Cause: aws.String(expectedError.Error()),
Cause: aws.String(expectedError.ErrorCause()),
Error: aws.String(expectedError.ErrorName()),
TaskToken: aws.String(mockTaskToken),
})
Expand All @@ -157,7 +157,7 @@ func TestTaskFailureCommandExitedNonzero(t *testing.T) {
defer controller.Finish()
mockSFN := mocksfn.NewMockSFNAPI(controller)
mockSFN.EXPECT().SendTaskFailure(&sfn.SendTaskFailureInput{
Cause: aws.String(expectedError.Error()),
Cause: aws.String(expectedError.ErrorCause()),
Error: aws.String(expectedError.ErrorName()),
TaskToken: aws.String(mockTaskToken),
})
Expand All @@ -171,14 +171,14 @@ func TestTaskFailureCustomErrorName(t *testing.T) {
testCtx, testCtxCancel := context.WithCancel(context.Background())
defer testCtxCancel()
cmd := "stderr_stdout_exitcode.sh"
cmdArgs := []string{"stderr", `{"error_name": "custom.error_name"}`, "10"}
expectedError := TaskFailureCustomErrorName{errorName: "custom.error_name", stderr: "stderr"}
cmdArgs := []string{"stderr", `{"error": "custom.error_name", "cause": "bar"}`, "10"}
expectedError := TaskFailureCustom{Err: "custom.error_name", Cause: "bar"}

controller := gomock.NewController(t)
defer controller.Finish()
mockSFN := mocksfn.NewMockSFNAPI(controller)
mockSFN.EXPECT().SendTaskFailure(&sfn.SendTaskFailureInput{
Cause: aws.String(expectedError.Error()),
Cause: aws.String(expectedError.ErrorCause()),
Error: aws.String(expectedError.ErrorName()),
TaskToken: aws.String(mockTaskToken),
})
Expand All @@ -199,7 +199,7 @@ func TestTaskFailureTaskOutputNotJSON(t *testing.T) {
defer controller.Finish()
mockSFN := mocksfn.NewMockSFNAPI(controller)
mockSFN.EXPECT().SendTaskFailure(&sfn.SendTaskFailureInput{
Cause: aws.String(expectedError.Error()),
Cause: aws.String(expectedError.ErrorCause()),
Error: aws.String(expectedError.ErrorName()),
TaskToken: aws.String(mockTaskToken),
})
Expand All @@ -220,7 +220,7 @@ func TestTaskFailureCommandTerminated(t *testing.T) {
defer controller.Finish()
mockSFN := mocksfn.NewMockSFNAPI(controller)
mockSFN.EXPECT().SendTaskFailure(&sfn.SendTaskFailureInput{
Cause: aws.String(expectedError.Error()),
Cause: aws.String(expectedError.ErrorCause()),
Error: aws.String(expectedError.ErrorName()),
TaskToken: aws.String(mockTaskToken),
})
Expand All @@ -238,14 +238,14 @@ func TestTaskFailureCommandTerminated(t *testing.T) {
testCtx, testCtxCancel := context.WithCancel(context.Background())
defer testCtxCancel()
cmd := "stderr_stdout_exitcode_onsigterm.sh"
cmdArgs := []string{"stderr", `{"error_name": "custom.error_name"}`, "1"}
expectedError := TaskFailureCustomErrorName{errorName: "custom.error_name", stderr: "stderr"}
cmdArgs := []string{"stderr", `{"error": "custom.error_name", "cause": "foo"}`, "1"}
expectedError := TaskFailureCustom{Err: "custom.error_name", Cause: "foo"}

controller := gomock.NewController(t)
defer controller.Finish()
mockSFN := mocksfn.NewMockSFNAPI(controller)
mockSFN.EXPECT().SendTaskFailure(&sfn.SendTaskFailureInput{
Cause: aws.String(expectedError.Error()),
Cause: aws.String(expectedError.ErrorCause()),
Error: aws.String(expectedError.ErrorName()),
TaskToken: aws.String(mockTaskToken),
})
Expand All @@ -270,7 +270,7 @@ func TestTaskFailureCommandTerminated(t *testing.T) {
defer controller.Finish()
mockSFN := mocksfn.NewMockSFNAPI(controller)
mockSFN.EXPECT().SendTaskFailure(&sfn.SendTaskFailureInput{
Cause: aws.String(expectedError.Error()),
Cause: aws.String(expectedError.ErrorCause()),
Error: aws.String(expectedError.ErrorName()),
TaskToken: aws.String(mockTaskToken),
})
Expand Down

0 comments on commit 2e9d2d1

Please sign in to comment.