Skip to content

Commit

Permalink
Lazily initialize docker registry client (#328)
Browse files Browse the repository at this point in the history
* Lazily attempt to initialize docker registry client

* Exit early if connection to docker daemon fails
* Fix mutex bugs
* Fix image buildinfo getting "stuck" and requiring killing of the process
* change execution order

---------

Co-authored-by: equanox <matthias@benchkram.de>
  • Loading branch information
rdnt and Equanox committed Apr 13, 2023
1 parent 36ec344 commit 40dafaf
Show file tree
Hide file tree
Showing 13 changed files with 64 additions and 59 deletions.
29 changes: 28 additions & 1 deletion bob/aggregate.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import (
"path/filepath"
"strings"

"github.com/benchkram/bob/pkg/dockermobyutil"

"github.com/benchkram/bob/bob/bobfile"
"github.com/benchkram/bob/bob/bobfile/project"
"github.com/benchkram/bob/bob/global"
Expand Down Expand Up @@ -158,7 +160,6 @@ func (b *B) Aggregate() (aggregate *bobfile.Bobfile, err error) {
task.WithLocalstore(b.local)
task.WithEnvStore(b.nix.EnvStore())
task.WithBuildinfoStore(b.buildInfoStore)
task.WithDockerRegistryClient(b.dockerRegistryClient)

// a task must always-rebuild when caching is disabled
if !b.enableCaching {
Expand Down Expand Up @@ -218,6 +219,32 @@ func (b *B) Aggregate() (aggregate *bobfile.Bobfile, err error) {
aggregate.Project = aggregate.Dir()
}

var dockerRegistryClientInitialized bool

// Assure tasks are correctly initialised with a docker registry client.
// Only one registry client must be created and shared between tasks,
// this reduces the pressure on garbage collection for big repos.
for i, task := range aggregate.BTasks {
target, err := task.Target()
errz.Fatal(err)

if target != nil && len(target.DockerImages()) > 0 {
if !dockerRegistryClientInitialized {
b.dockerRegistryClient, err = dockermobyutil.NewRegistryClient()
if errors.Is(err, dockermobyutil.ErrConnectionFailed) {
errz.Fatal(usererror.Wrapm(err, fmt.Sprintf("task `%s` exports an image, but docker is not reachable", task.Name())))
}
errz.Fatal(err)

dockerRegistryClientInitialized = true
}

task.WithDockerRegistryClient(b.dockerRegistryClient)
// modify index on map since tasks are passed by value
aggregate.BTasks[i] = task
}
}

err = aggregate.Verify()
errz.Fatal(err)

Expand Down
2 changes: 0 additions & 2 deletions bob/bob.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,6 @@ func newBob(opts ...Option) *B {
enableCaching: true,
allowInsecure: false,
maxParallel: runtime.NumCPU(),

dockerRegistryClient: dockermobyutil.NewRegistryClient(),
}

for _, opt := range opts {
Expand Down
5 changes: 0 additions & 5 deletions bob/bobfile/bobfile.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import (
"path/filepath"
"strings"

"github.com/benchkram/bob/pkg/dockermobyutil"
"github.com/benchkram/bob/pkg/nix"
storeclient "github.com/benchkram/bob/pkg/store-client"

Expand Down Expand Up @@ -139,9 +138,6 @@ func bobfileRead(dir string) (_ *Bobfile, err error) {
bobfile.RTasks = bobrun.RunMap{}
}

// a shared registry clients for all tasks.
dockerRegistryClient := dockermobyutil.NewRegistryClient()

// Assure tasks are initialized with their defaults
for key, task := range bobfile.BTasks {
task.SetDir(bobfile.dir)
Expand All @@ -153,7 +149,6 @@ func bobfileRead(dir string) (_ *Bobfile, err error) {
// This means switching to pointer types for most members.
task.SetEnv([]string{})
task.SetRebuildStrategy(bobtask.RebuildOnChange)
task.WithDockerRegistryClient(dockerRegistryClient)

// initialize docker registry for task
task.SetDependencies(initializeDependencies(dir, task.DependenciesDirty, bobfile))
Expand Down
7 changes: 3 additions & 4 deletions bob/playbook/build_internal.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,11 +130,10 @@ func (p *Playbook) build(ctx context.Context, task *bobtask.Task) (pt *processed
taskSuccessFul = true

err = p.TaskCompleted(task.TaskID)
if err != nil {
if errors.Is(err, ErrFailed) {
return pt, err
}
if errors.Is(err, ErrFailed) {
return pt, err
}
errz.Log(err)
errz.Fatal(err)

taskStatus, err := p.TaskStatus(task.Name())
Expand Down
11 changes: 8 additions & 3 deletions bobtask/target.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,15 @@ import (
)

// Target takes care of populating the targets members correctly.
// It returns a nil in case of a non existing target and a nil error.
// It returns a nil in case of a non-existing target and a nil error.
func (t *Task) Target() (empty target.Target, _ error) {
if t.target == nil {
return empty, nil
}

// attach docker registry client (if set) to target itself
t.target.WithDockerRegistryClient(t.dockerRegistryClient)

// ReadBuildInfo is dependent on the inputHash of the task.
// For this reason we cannot read build info on target creation,
// as this happens right after parsing the config.
Expand All @@ -41,8 +44,10 @@ func (t *Task) Target() (empty target.Target, _ error) {
return t.target, t.target.Resolve()
}

tt := t.target.WithExpected(&buildInfo.Target)
return tt, tt.Resolve()
// attach expected buildinfo
t.target.WithExpected(&buildInfo.Target)

return t.target, t.target.Resolve()
}

func (t *Task) TargetExists() bool {
Expand Down
17 changes: 0 additions & 17 deletions bobtask/target/options.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,5 @@
package target

import (
"github.com/benchkram/bob/bobtask/buildinfo"
"github.com/benchkram/bob/pkg/dockermobyutil"
)

type Option func(t *T)

func WithDir(dir string) Option {
Expand All @@ -24,15 +19,3 @@ func WithDockerImages(images []string) Option {
t.dockerImages = images
}
}

func WithDockerRegistryClient(dockerRegistryClient dockermobyutil.RegistryClient) Option {
return func(t *T) {
t.dockerRegistryClient = dockerRegistryClient
}
}

func WithExpected(bi *buildinfo.Targets) Option {
return func(t *T) {
t.expected = bi
}
}
13 changes: 6 additions & 7 deletions bobtask/target/target.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ type Target interface {
FilesystemEntriesRaw() []string
FilesystemEntriesRawPlain() []string

WithExpected(*buildinfo.Targets) *T
WithExpected(*buildinfo.Targets)
DockerImages() []string

// AsInvalidFiles returns all FilesystemEntriesRaw as invalid with the specified reason
Expand Down Expand Up @@ -68,10 +68,6 @@ func New(opts ...Option) *T {
opt(t)
}

if t.dockerRegistryClient == nil {
t.dockerRegistryClient = dockermobyutil.NewRegistryClient()
}

return t
}

Expand Down Expand Up @@ -109,9 +105,12 @@ func (t *T) FilesystemEntriesRawPlain() []string {
return append([]string{}, t.filesystemEntriesRaw...)
}

func (t *T) WithExpected(expected *buildinfo.Targets) *T {
func (t *T) WithExpected(expected *buildinfo.Targets) {
t.expected = expected
return t
}

func (t *T) WithDockerRegistryClient(c dockermobyutil.RegistryClient) {
t.dockerRegistryClient = c
}

func (t *T) DockerImages() []string {
Expand Down
1 change: 0 additions & 1 deletion bobtask/target_parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,6 @@ func (t *Task) parseTargets() error {
target.WithFilesystemEntries(filesystemEntries),
target.WithDockerImages(dockerImages),
target.WithDir(t.dir),
target.WithDockerRegistryClient(t.dockerRegistryClient),
)
}

Expand Down
4 changes: 0 additions & 4 deletions bobtask/task.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,10 +135,6 @@ func Make(opts ...TaskOption) Task {
opt(&t)
}

if t.dockerRegistryClient == nil {
t.dockerRegistryClient = dockermobyutil.NewRegistryClient()
}

return t
}

Expand Down
23 changes: 12 additions & 11 deletions pkg/dockermobyutil/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ import (
)

var (
ErrImageNotFound = fmt.Errorf("image not found")
ErrImageNotFound = fmt.Errorf("image not found")
ErrConnectionFailed = errors.New("connection to docker daemon failed")
)

type RegistryClient interface {
Expand All @@ -45,7 +46,7 @@ type R struct {
mutex *sync.Mutex
}

func NewRegistryClient() RegistryClient {
func NewRegistryClient() (RegistryClient, error) {
cli, err := client.NewClientWithOpts(
client.FromEnv,
client.WithAPIVersionNegotiation(),
Expand All @@ -59,14 +60,19 @@ func NewRegistryClient() RegistryClient {
archiveDir: os.TempDir(),
}

// Use a lock to supress parallel image reads on zfs.
// Use a lock to suppress parallel image reads on zfs.
info, err := r.client.Info(context.Background())
errz.Log(err)
if client.IsErrConnectionFailed(err) {
return nil, ErrConnectionFailed
} else if err != nil {
return nil, err
}

if info.Driver == "zfs" {
r.mutex = &sync.Mutex{}
}

return r
return r, nil
}

func (r *R) ImageExists(image string) (bool, error) {
Expand Down Expand Up @@ -107,17 +113,12 @@ func (r *R) ImageHash(image string) (string, error) {
func (r *R) imageSaveToPath(image string, savedir string) (pathToArchive string, _ error) {
if r.mutex != nil {
r.mutex.Lock()
defer r.mutex.Unlock()
}
reader, err := r.client.ImageSave(context.Background(), []string{image})
if err != nil {
if r.mutex != nil {
r.mutex.Unlock()
}
return "", err
}
if r.mutex != nil {
r.mutex.Unlock()
}
defer reader.Close()

body, err := io.ReadAll(reader)
Expand Down
3 changes: 2 additions & 1 deletion test/e2e/artifacts/artifacts_extraction_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,8 @@ var _ = Describe("Test artifact creation and extraction", func() {
var _ = Describe("Test artifact creation and extraction from docker targets", func() {
Context("in a fresh playground", func() {

mobyClient := dockermobyutil.NewRegistryClient()
mobyClient, err := dockermobyutil.NewRegistryClient()
Expect(err).NotTo(HaveOccurred())

It("should initialize bob playground", func() {
Expect(bob.CreatePlayground(bob.PlaygroundOptions{Dir: dir})).NotTo(HaveOccurred())
Expand Down
5 changes: 3 additions & 2 deletions test/e2e/artifacts/artifacts_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,11 +102,12 @@ var _ = Describe("Test artifact and target invalidation", func() {
})
})

// docker targets
// docker targets
var _ = Describe("Test artifact and docker-target invalidation", func() {
Context("in a fresh playground", func() {

mobyClient := dockermobyutil.NewRegistryClient()
mobyClient, err := dockermobyutil.NewRegistryClient()
Expect(err).NotTo(HaveOccurred())

It("should initialize bob playground", func() {
Expect(bob.CreatePlayground(bob.PlaygroundOptions{Dir: dir})).NotTo(HaveOccurred())
Expand Down
3 changes: 2 additions & 1 deletion test/e2e/artifacts/nobuildinfo_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,8 @@ var _ = Describe("Test artifact and target lifecycle without existing buildinfo"
var _ = Describe("Test artifact and target lifecycle for docker images without existing buildinfo", func() {
Context("in a fresh playground", func() {

mobyClient := dockermobyutil.NewRegistryClient()
mobyClient, err := dockermobyutil.NewRegistryClient()
Expect(err).NotTo(HaveOccurred())

It("should initialize bob playground", func() {
Expect(bob.CreatePlayground(bob.PlaygroundOptions{Dir: dir})).NotTo(HaveOccurred())
Expand Down

0 comments on commit 40dafaf

Please sign in to comment.