mirror of
https://github.com/fnproject/fn.git
synced 2022-10-28 21:29:17 +03:00
fn: tests for hung and bad docker repo during docker-pull (#1298)
* fn: tests for hung and bad docker repo during docker-pull
This commit is contained in:
@@ -227,6 +227,7 @@ func NewDockerDriver(cfg *Config) (drivers.Driver, error) {
|
|||||||
MaxTmpFsInodes: cfg.MaxTmpFsInodes,
|
MaxTmpFsInodes: cfg.MaxTmpFsInodes,
|
||||||
EnableReadOnlyRootFs: !cfg.DisableReadOnlyRootFs,
|
EnableReadOnlyRootFs: !cfg.DisableReadOnlyRootFs,
|
||||||
EnableTini: !cfg.DisableTini,
|
EnableTini: !cfg.DisableTini,
|
||||||
|
MaxRetries: cfg.MaxDockerRetries,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -436,6 +436,110 @@ type dummyReader struct {
|
|||||||
io.Reader
|
io.Reader
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDockerPullHungRepo(t *testing.T) {
|
||||||
|
hung, cancel := context.WithCancel(context.Background())
|
||||||
|
garbageServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// version check seem to have a sane timeout in docker, let's serve this, then stop
|
||||||
|
if r.URL.String() == "/v2/" {
|
||||||
|
w.WriteHeader(200)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
<-hung.Done()
|
||||||
|
}))
|
||||||
|
defer garbageServer.Close()
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
dest := strings.TrimPrefix(garbageServer.URL, "http://")
|
||||||
|
|
||||||
|
app := &models.App{ID: "app_id"}
|
||||||
|
fn := &models.Fn{
|
||||||
|
ID: "fn_id",
|
||||||
|
Image: dest + "/fnproject/fn-test-utils",
|
||||||
|
ResourceConfig: models.ResourceConfig{
|
||||||
|
Timeout: 5,
|
||||||
|
IdleTimeout: 10,
|
||||||
|
Memory: 128,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
url := "http://127.0.0.1:8080/invoke/" + fn.ID
|
||||||
|
|
||||||
|
ls := logs.NewMock()
|
||||||
|
cfg, err := NewConfig()
|
||||||
|
cfg.MaxDockerRetries = 1
|
||||||
|
cfg.HotStartTimeout = time.Duration(5) * time.Second
|
||||||
|
a := New(NewDirectCallDataAccess(ls, new(mqs.Mock)), WithConfig(cfg))
|
||||||
|
defer checkClose(t, a)
|
||||||
|
|
||||||
|
req, err := http.NewRequest("GET", url, &dummyReader{Reader: strings.NewReader(`{}`)})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("unexpected error building request", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var out bytes.Buffer
|
||||||
|
callI, err := a.GetCall(FromHTTPFnRequest(app, fn, req), WithWriter(&out))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = a.Submit(callI)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("submit should error!")
|
||||||
|
}
|
||||||
|
errS := err.Error()
|
||||||
|
if !strings.HasPrefix(errS, "Failed to pull image ") || !strings.Contains(errS, "context deadline exceeded") {
|
||||||
|
t.Fatalf("unexpected error %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDockerPullBadRepo(t *testing.T) {
|
||||||
|
|
||||||
|
garbageServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
fmt.Fprintln(w, "enjoy this lovely garbage")
|
||||||
|
}))
|
||||||
|
defer garbageServer.Close()
|
||||||
|
|
||||||
|
dest := strings.TrimPrefix(garbageServer.URL, "http://")
|
||||||
|
|
||||||
|
app := &models.App{ID: "app_id"}
|
||||||
|
fn := &models.Fn{
|
||||||
|
ID: "fn_id",
|
||||||
|
Image: dest + "/fnproject/fn-test-utils",
|
||||||
|
ResourceConfig: models.ResourceConfig{
|
||||||
|
Timeout: 5,
|
||||||
|
IdleTimeout: 10,
|
||||||
|
Memory: 128,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
url := "http://127.0.0.1:8080/invoke/" + fn.ID
|
||||||
|
|
||||||
|
ls := logs.NewMock()
|
||||||
|
cfg, err := NewConfig()
|
||||||
|
cfg.MaxDockerRetries = 1
|
||||||
|
a := New(NewDirectCallDataAccess(ls, new(mqs.Mock)), WithConfig(cfg))
|
||||||
|
defer checkClose(t, a)
|
||||||
|
|
||||||
|
req, err := http.NewRequest("GET", url, &dummyReader{Reader: strings.NewReader(`{}`)})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("unexpected error building request", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var out bytes.Buffer
|
||||||
|
callI, err := a.GetCall(FromHTTPFnRequest(app, fn, req), WithWriter(&out))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = a.Submit(callI)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("submit should error!")
|
||||||
|
}
|
||||||
|
if !strings.HasPrefix(err.Error(), "Failed to pull image ") {
|
||||||
|
t.Fatalf("unexpected error %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestHTTPWithoutContentLengthWorks(t *testing.T) {
|
func TestHTTPWithoutContentLengthWorks(t *testing.T) {
|
||||||
// TODO it may be a good idea to mock out the http server and use a real
|
// TODO it may be a good idea to mock out the http server and use a real
|
||||||
// response writer with sync, and also test that this works with async + log
|
// response writer with sync, and also test that this works with async + log
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ type Config struct {
|
|||||||
IOFSAgentPath string `json:"iofs_path"`
|
IOFSAgentPath string `json:"iofs_path"`
|
||||||
IOFSMountRoot string `json:"iofs_mount_root"`
|
IOFSMountRoot string `json:"iofs_mount_root"`
|
||||||
IOFSOpts string `json:"iofs_opts"`
|
IOFSOpts string `json:"iofs_opts"`
|
||||||
|
MaxDockerRetries uint64 `json:"max_docker_retries"`
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|||||||
@@ -24,6 +24,9 @@ type cookie struct {
|
|||||||
task drivers.ContainerTask
|
task drivers.ContainerTask
|
||||||
// pointer to docker driver
|
// pointer to docker driver
|
||||||
drv *DockerDriver
|
drv *DockerDriver
|
||||||
|
|
||||||
|
// do we need to remove container at exit?
|
||||||
|
isCreated bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *cookie) configureLogger(log logrus.FieldLogger) {
|
func (c *cookie) configureLogger(log logrus.FieldLogger) {
|
||||||
@@ -199,7 +202,10 @@ func (c *cookie) configureEnv(log logrus.FieldLogger) {
|
|||||||
|
|
||||||
// implements Cookie
|
// implements Cookie
|
||||||
func (c *cookie) Close(ctx context.Context) error {
|
func (c *cookie) Close(ctx context.Context) error {
|
||||||
err := c.drv.removeContainer(ctx, c.task.Id())
|
var err error
|
||||||
|
if c.isCreated {
|
||||||
|
err = c.drv.removeContainer(ctx, c.task.Id())
|
||||||
|
}
|
||||||
c.drv.unpickPool(c)
|
c.drv.unpickPool(c)
|
||||||
c.drv.unpickNetwork(c)
|
c.drv.unpickNetwork(c)
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ func NewDocker(conf drivers.Config) *DockerDriver {
|
|||||||
driver := &DockerDriver{
|
driver := &DockerDriver{
|
||||||
cancel: cancel,
|
cancel: cancel,
|
||||||
conf: conf,
|
conf: conf,
|
||||||
docker: newClient(ctx),
|
docker: newClient(ctx, conf.MaxRetries),
|
||||||
hostname: hostname,
|
hostname: hostname,
|
||||||
auths: auths,
|
auths: auths,
|
||||||
}
|
}
|
||||||
@@ -275,6 +275,11 @@ func (drv *DockerDriver) PrepareCookie(ctx context.Context, c drivers.Cookie) er
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// here let's assume we have created container, logically this should be after 'CreateContainer', but we
|
||||||
|
// are not 100% sure that *any* failure to CreateContainer does not ever leave a container around especially
|
||||||
|
// going through fsouza+docker-api.
|
||||||
|
cookie.isCreated = true
|
||||||
|
|
||||||
cookie.opts.Context = ctx
|
cookie.opts.Context = ctx
|
||||||
_, err = drv.docker.CreateContainer(cookie.opts)
|
_, err = drv.docker.CreateContainer(cookie.opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ type dockerClient interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO: switch to github.com/docker/engine-api
|
// TODO: switch to github.com/docker/engine-api
|
||||||
func newClient(ctx context.Context) dockerClient {
|
func newClient(ctx context.Context, maxRetries uint64) dockerClient {
|
||||||
// TODO this was much easier, don't need special settings at the moment
|
// TODO this was much easier, don't need special settings at the moment
|
||||||
// docker, err := docker.NewClient(conf.Docker)
|
// docker, err := docker.NewClient(conf.Docker)
|
||||||
client, err := docker.NewClientFromEnv()
|
client, err := docker.NewClientFromEnv()
|
||||||
@@ -59,12 +59,18 @@ func newClient(ctx context.Context) dockerClient {
|
|||||||
logrus.WithError(err).Fatal("couldn't connect to docker daemon")
|
logrus.WithError(err).Fatal("couldn't connect to docker daemon")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// punch in default if not set
|
||||||
|
if maxRetries == 0 {
|
||||||
|
maxRetries = 10
|
||||||
|
}
|
||||||
|
|
||||||
go listenEventLoop(ctx, client)
|
go listenEventLoop(ctx, client)
|
||||||
return &dockerWrap{client}
|
return &dockerWrap{docker: client, maxRetries: maxRetries}
|
||||||
}
|
}
|
||||||
|
|
||||||
type dockerWrap struct {
|
type dockerWrap struct {
|
||||||
docker *docker.Client
|
docker *docker.Client
|
||||||
|
maxRetries uint64
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -210,13 +216,13 @@ func RegisterViews(tagKeys []string, latencyDist []float64) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *dockerWrap) retry(ctx context.Context, logger logrus.FieldLogger, f func() error) error {
|
func (d *dockerWrap) retry(ctx context.Context, logger logrus.FieldLogger, f func() error) error {
|
||||||
var i int
|
var i uint64
|
||||||
var err error
|
var err error
|
||||||
defer func() { stats.Record(ctx, dockerRetriesMeasure.M(int64(i))) }()
|
defer func() { stats.Record(ctx, dockerRetriesMeasure.M(int64(i))) }()
|
||||||
|
|
||||||
var b common.Backoff
|
var b common.Backoff
|
||||||
// 10 retries w/o change to backoff is ~13s if ops take ~0 time
|
// 10 retries w/o change to backoff is ~13s if ops take ~0 time
|
||||||
for ; i < 10; i++ {
|
for ; i < d.maxRetries; i++ {
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
stats.Record(ctx, dockerTimeoutMeasure.M(0))
|
stats.Record(ctx, dockerTimeoutMeasure.M(0))
|
||||||
|
|||||||
@@ -252,6 +252,7 @@ type Config struct {
|
|||||||
MaxTmpFsInodes uint64 `json:"max_tmpfs_inodes"`
|
MaxTmpFsInodes uint64 `json:"max_tmpfs_inodes"`
|
||||||
EnableReadOnlyRootFs bool `json:"enable_readonly_rootfs"`
|
EnableReadOnlyRootFs bool `json:"enable_readonly_rootfs"`
|
||||||
EnableTini bool `json:"enable_tini"`
|
EnableTini bool `json:"enable_tini"`
|
||||||
|
MaxRetries uint64 `json:"max_retries"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func average(samples []Stat) (Stat, bool) {
|
func average(samples []Stat) (Stat, bool) {
|
||||||
|
|||||||
Reference in New Issue
Block a user