mirror of
https://github.com/juanfont/headscale.git
synced 2026-04-01 15:03:23 +02:00
The default docker execute timeout (10s) is the root cause of "dockertest command timed out" errors across many integration tests on CI. On congested GitHub Actions runners, docker exec latency alone can consume 2-5 seconds of this budget before the command even starts inside the container. Replace the hardcoded 10s constant with a function that returns 20s on CI, doubling the budget for all container commands (tailscale status, headscale CLI, curl, etc.). Similarly, scale the default tailscale ping timeout from 200ms to 400ms on CI. This doubles the per-attempt budget and the docker exec timeout for pings (from 200ms*5=1s to 400ms*5=2s), giving more headroom for docker exec overhead. Updates #3125
129 lines
3.0 KiB
Go
129 lines
3.0 KiB
Go
package dockertestutil
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/juanfont/headscale/hscontrol/util"
|
|
"github.com/ory/dockertest/v3"
|
|
)
|
|
|
|
// defaultExecuteTimeout returns the timeout for docker exec commands.
|
|
// On CI runners, docker exec latency is higher due to resource
|
|
// contention, so the timeout is doubled.
|
|
func defaultExecuteTimeout() time.Duration {
|
|
if util.IsCI() {
|
|
return 20 * time.Second
|
|
}
|
|
|
|
return 10 * time.Second
|
|
}
|
|
|
|
var (
|
|
ErrDockertestCommandFailed = errors.New("dockertest command failed")
|
|
ErrDockertestCommandTimeout = errors.New("dockertest command timed out")
|
|
)
|
|
|
|
type ExecuteCommandConfig struct {
|
|
timeout time.Duration
|
|
}
|
|
|
|
type ExecuteCommandOption func(*ExecuteCommandConfig) error
|
|
|
|
func ExecuteCommandTimeout(timeout time.Duration) ExecuteCommandOption {
|
|
return ExecuteCommandOption(func(conf *ExecuteCommandConfig) error {
|
|
conf.timeout = timeout
|
|
return nil
|
|
})
|
|
}
|
|
|
|
// buffer is a goroutine safe bytes.buffer.
|
|
type buffer struct {
|
|
store bytes.Buffer
|
|
mutex sync.Mutex
|
|
}
|
|
|
|
// Write appends the contents of p to the buffer, growing the buffer as needed. It returns
|
|
// the number of bytes written.
|
|
func (b *buffer) Write(p []byte) (int, error) {
|
|
b.mutex.Lock()
|
|
defer b.mutex.Unlock()
|
|
|
|
return b.store.Write(p)
|
|
}
|
|
|
|
// String returns the contents of the unread portion of the buffer
|
|
// as a string.
|
|
func (b *buffer) String() string {
|
|
b.mutex.Lock()
|
|
defer b.mutex.Unlock()
|
|
|
|
return b.store.String()
|
|
}
|
|
|
|
func ExecuteCommand(
|
|
resource *dockertest.Resource,
|
|
cmd []string,
|
|
env []string,
|
|
options ...ExecuteCommandOption,
|
|
) (string, string, error) {
|
|
stdout := buffer{}
|
|
stderr := buffer{}
|
|
|
|
execConfig := ExecuteCommandConfig{
|
|
timeout: defaultExecuteTimeout(),
|
|
}
|
|
|
|
for _, opt := range options {
|
|
err := opt(&execConfig)
|
|
if err != nil {
|
|
return "", "", fmt.Errorf("execute-command/options: %w", err)
|
|
}
|
|
}
|
|
|
|
type result struct {
|
|
exitCode int
|
|
err error
|
|
}
|
|
|
|
resultChan := make(chan result, 1)
|
|
|
|
// Run your long running function in it's own goroutine and pass back it's
|
|
// response into our channel.
|
|
go func() {
|
|
exitCode, err := resource.Exec(
|
|
cmd,
|
|
dockertest.ExecOptions{
|
|
Env: append(env, "HEADSCALE_LOG_LEVEL=info"),
|
|
StdOut: &stdout,
|
|
StdErr: &stderr,
|
|
},
|
|
)
|
|
|
|
resultChan <- result{exitCode, err}
|
|
}()
|
|
|
|
// Listen on our channel AND a timeout channel - which ever happens first.
|
|
select {
|
|
case res := <-resultChan:
|
|
if res.err != nil {
|
|
return stdout.String(), stderr.String(), fmt.Errorf("command failed, stderr: %s: %w", stderr.String(), res.err)
|
|
}
|
|
|
|
if res.exitCode != 0 {
|
|
// Uncomment for debugging
|
|
// log.Println("Command: ", cmd)
|
|
// log.Println("stdout: ", stdout.String())
|
|
// log.Println("stderr: ", stderr.String())
|
|
return stdout.String(), stderr.String(), fmt.Errorf("command failed, stderr: %s: %w", stderr.String(), ErrDockertestCommandFailed)
|
|
}
|
|
|
|
return stdout.String(), stderr.String(), nil
|
|
case <-time.After(execConfig.timeout):
|
|
return stdout.String(), stderr.String(), fmt.Errorf("command failed, stderr: %s: %w", stderr.String(), ErrDockertestCommandTimeout)
|
|
}
|
|
}
|