Skip to content

Commit

Permalink
Add timeout setting to Steps
Browse files Browse the repository at this point in the history
This feature allows a Task author to specify a Step timeout in a
Taskrun.

An example use case is when a Task author would like to execute a
Step for setting up an execution environment. One may expect this
Step to execute within a few seconds. If the execution time takes
longer than expected one may rather want to fail fast instead of
waiting for the TaskRun timeout to abort the TaskRun.

Closes #1690
  • Loading branch information
Peaorl committed Oct 6, 2020
1 parent 7544938 commit 02cfa1f
Show file tree
Hide file tree
Showing 17 changed files with 338 additions and 59 deletions.
2 changes: 2 additions & 0 deletions cmd/entrypoint/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ var (
terminationPath = flag.String("termination_path", "/tekton/termination", "If specified, file to write upon termination")
results = flag.String("results", "", "If specified, list of file names that might contain task results")
waitPollingInterval = time.Second
timeout = flag.Duration("timeout", time.Duration(0), "If specified, sets timeout for step")
)

func cp(src, dst string) error {
Expand Down Expand Up @@ -103,6 +104,7 @@ func main() {
Runner: &realRunner{},
PostWriter: &realPostWriter{},
Results: strings.Split(*results, ","),
Timeout: timeout,
}

// Copy any creds injected by the controller into the $HOME directory of the current
Expand Down
11 changes: 9 additions & 2 deletions cmd/entrypoint/runner.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package main

import (
"context"
"os"
"os/exec"
"os/signal"
Expand All @@ -19,7 +20,7 @@ type realRunner struct {

var _ entrypoint.Runner = (*realRunner)(nil)

func (rr *realRunner) Run(args ...string) error {
func (rr *realRunner) Run(ctx context.Context, args ...string) error {
if len(args) == 0 {
return nil
}
Expand All @@ -33,7 +34,7 @@ func (rr *realRunner) Run(args ...string) error {
signal.Notify(rr.signals)
defer signal.Reset()

cmd := exec.Command(name, args...)
cmd := exec.CommandContext(ctx, name, args...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
// dedicated PID group used to forward signals to
Expand All @@ -42,6 +43,9 @@ func (rr *realRunner) Run(args ...string) error {

// Start defined command
if err := cmd.Start(); err != nil {
if ctx.Err() == context.DeadlineExceeded {
return context.DeadlineExceeded
}
return err
}

Expand All @@ -57,6 +61,9 @@ func (rr *realRunner) Run(args ...string) error {

// Wait for command to exit
if err := cmd.Wait(); err != nil {
if ctx.Err() == context.DeadlineExceeded {
return context.DeadlineExceeded
}
return err
}

Expand Down
19 changes: 18 additions & 1 deletion cmd/entrypoint/runner_test.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package main

import (
"context"
"os"
"syscall"
"testing"
"time"
)

// TestRealRunnerSignalForwarding will artificially put an interrupt signal (SIGINT) in the rr.signals chan.
Expand All @@ -14,9 +16,24 @@ func TestRealRunnerSignalForwarding(t *testing.T) {
rr := realRunner{}
rr.signals = make(chan os.Signal, 1)
rr.signals <- syscall.SIGINT
if err := rr.Run("sleep", "3600"); err.Error() == "signal: interrupt" {
if err := rr.Run(context.Background(), "sleep", "3600"); err.Error() == "signal: interrupt" {
t.Logf("SIGINT forwarded to Entrypoint")
} else {
t.Fatalf("Unexpected error received: %v", err)
}
}

// TestRealRunnerTimeout tests whether cmd is killed after a millisecond even though it's supposed to sleep for 10 milliseconds.
func TestRealRunnerTimeout(t *testing.T) {
rr := realRunner{}
timeout := time.Millisecond
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
if err := rr.Run(ctx, "sleep", "0.01"); err != nil {
if err != context.DeadlineExceeded {
t.Fatalf("unexpected error received: %v", err)
}
} else {
t.Fatalf("step didn't timeout")
}
}
21 changes: 21 additions & 0 deletions docs/tasks.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ weight: 1
- [Defining `Steps`](#defining-steps)
- [Reserved directories](#reserved-directories)
- [Running scripts within `Steps`](#running-scripts-within-steps)
- [Specifying a timeout](#specifying-a-timeout)
- [Specifying `Parameters`](#specifying-parameters)
- [Specifying `Resources`](#specifying-resources)
- [Specifying `Workspaces`](#specifying-workspaces)
Expand Down Expand Up @@ -241,7 +242,27 @@ steps:
#!/usr/bin/env bash
/bin/my-binary
```
#### Specifying a timeout

A `Step` can specify a `timeout` field.
If the `Step` execution time exceeds the specified timeout, the `Step` kills
its running process and any subsequent `Steps` in the `TaskRun` will not be
executed. The `TaskRun` is placed into a `Failed` condition. An accompanying log
describing which `Step` timed out is written as the `Failed` condition's message.

The timeout specification follows the duration format as specified in the [Go time package](https://golang.org/pkg/time/#ParseDuration) (e.g. 1s or 1ms).

The example `Step` below is supposed to sleep for 60 seconds but will be canceled by the specified 5 second timeout.
```yaml
steps:
- name: sleep-then-timeout
image: ubuntu
script: |
#!/usr/bin/env bash
echo "I am supposed to sleep for 60 seconds!"
sleep 60
timeout: 5s
```
### Specifying `Parameters`

You can specify parameters, such as compilation flags or artifact names, that you want to supply to the `Task` at execution time.
Expand Down
6 changes: 4 additions & 2 deletions pkg/apis/pipeline/v1beta1/task_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,10 +125,12 @@ type Step struct {
//
// If Script is not empty, the Step cannot have an Command or Args.
Script string `json:"script,omitempty"`
// Timeout is the time after which the step times out. Defaults to never.
// Refer to Go's ParseDuration documentation for expected format: https://golang.org/pkg/time/#ParseDuration
Timeout *metav1.Duration `json:"timeout,omitempty"`
}

// Sidecar embeds the Container type, which allows it to include fields not
// provided by Container.
// Sidecar has nearly the same data structure as Step, consisting of a Container and an optional Script, but does not have the ability to timeout.
type Sidecar struct {
corev1.Container `json:",inline"`

Expand Down
7 changes: 7 additions & 0 deletions pkg/apis/pipeline/v1beta1/task_validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"fmt"
"path/filepath"
"strings"
"time"

"github.com/tektoncd/pipeline/pkg/apis/validate"
"github.com/tektoncd/pipeline/pkg/substitution"
Expand Down Expand Up @@ -159,6 +160,12 @@ func validateStep(s Step, names sets.String) (errs *apis.FieldError) {
names.Insert(s.Name)
}

if s.Timeout != nil {
if s.Timeout.Duration < time.Duration(0) {
return apis.ErrInvalidValue(s.Timeout.Duration, "negative timeout")
}
}

for j, vm := range s.VolumeMounts {
if strings.HasPrefix(vm.MountPath, "/tekton/") &&
!strings.HasPrefix(vm.MountPath, "/tekton/home") {
Expand Down
13 changes: 13 additions & 0 deletions pkg/apis/pipeline/v1beta1/task_validation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,14 @@ package v1beta1_test
import (
"context"
"testing"
"time"

"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1"
"github.com/tektoncd/pipeline/test/diff"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"knative.dev/pkg/apis"
)

Expand Down Expand Up @@ -930,6 +932,17 @@ func TestTaskSpecValidateError(t *testing.T) {
Message: `non-existent variable in "\n\t\t\t\t#!/usr/bin/env bash\n\t\t\t\thello \"$(context.task.missing)\""`,
Paths: []string{"steps[0].script"},
},
}, {
name: "negative timeout string",
fields: fields{
Steps: []v1beta1.Step{{
Timeout: &metav1.Duration{Duration: -10 * time.Second},
}},
},
expectedError: apis.FieldError{
Message: "invalid value: -10s",
Paths: []string{"steps[0].negative timeout"},
},
}}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
Expand Down
5 changes: 5 additions & 0 deletions pkg/apis/pipeline/v1beta1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

29 changes: 26 additions & 3 deletions pkg/entrypoint/entrypointer.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ limitations under the License.
package entrypoint

import (
"context"
"fmt"
"io/ioutil"
"os"
Expand Down Expand Up @@ -63,6 +64,8 @@ type Entrypointer struct {

// Results is the set of files that might contain task results
Results []string
// Timeout is an optional user-specified duration within which the Step must complete
Timeout *time.Duration
}

// Waiter encapsulates waiting for files to exist.
Expand All @@ -73,7 +76,7 @@ type Waiter interface {

// Runner encapsulates running commands.
type Runner interface {
Run(args ...string) error
Run(ctx context.Context, args ...string) error
}

// PostWriter encapsulates writing a file when complete.
Expand Down Expand Up @@ -106,21 +109,41 @@ func (e Entrypointer) Go() error {
Value: time.Now().Format(timeFormat),
ResultType: v1beta1.InternalTektonResultType,
})

return err
}
}

if e.Entrypoint != "" {
e.Args = append([]string{e.Entrypoint}, e.Args...)
}

output = append(output, v1beta1.PipelineResourceResult{
Key: "StartedAt",
Value: time.Now().Format(timeFormat),
ResultType: v1beta1.InternalTektonResultType,
})

err := e.Runner.Run(e.Args...)
var err error
if e.Timeout != nil && *e.Timeout < time.Duration(0) {
err = fmt.Errorf("negative timeout specified")
}

if err == nil {
ctx := context.Background()
var cancel context.CancelFunc
if e.Timeout != nil && *e.Timeout != time.Duration(0) {
ctx, cancel = context.WithTimeout(ctx, *e.Timeout)
defer cancel()
}
err = e.Runner.Run(ctx, e.Args...)
if err == context.DeadlineExceeded {
output = append(output, v1beta1.PipelineResourceResult{
Key: "Reason",
Value: "TimeoutExceeded",
ResultType: v1beta1.InternalTektonResultType,
})
}
}

// Write the post file *no matter what*
e.WritePostFile(e.PostFile, err)
Expand Down
Loading

0 comments on commit 02cfa1f

Please sign in to comment.