You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
buildx/controller/remote/session.go

218 lines
4.4 KiB
Go

package remote
import (
"context"
"io"
"sync"
"sync/atomic"
"github.com/docker/buildx/build"
"github.com/docker/buildx/controller/pb"
"github.com/docker/buildx/controller/processes"
"github.com/moby/buildkit/client"
"github.com/pkg/errors"
)
type session struct {
manager *sessionManager
buildOnGoing atomic.Bool
statusChan chan *pb.StatusResponse
cancelBuild func()
buildOptions *pb.BuildOptions
inputPipe *io.PipeWriter
result *build.ResultHandle
processes *processes.Manager
mu sync.Mutex
}
func (s *session) close() {
s.mu.Lock()
defer s.mu.Unlock()
s.processes.CancelRunningProcesses()
if s.cancelBuild != nil {
s.cancelBuild()
}
if s.result != nil {
s.result.Done()
}
}
func (s *session) getProcess(id string) (*processes.Process, bool) {
return s.processes.Get(id)
}
func (s *session) listProcesses() []*pb.ProcessInfo {
return s.processes.ListProcesses()
}
func (s *session) deleteProcess(id string) error {
return s.processes.DeleteProcess(id)
}
func (s *session) startProcess(id string, cfg *pb.InvokeConfig) (*processes.Process, error) {
res := s.result
return s.processes.StartProcess(id, res, cfg)
}
func (s *session) build(ctx context.Context, options *pb.BuildOptions, wrapBuildError func(error) error) (*client.SolveResponse, error) {
s.mu.Lock()
if !s.buildOnGoing.CompareAndSwap(false, true) {
s.mu.Unlock()
return nil, errors.New("build ongoing")
}
if s.processes != nil {
s.processes.CancelRunningProcesses()
}
s.processes = processes.NewManager()
s.result = nil
s.statusChan = make(chan *pb.StatusResponse)
inR, inW := io.Pipe()
s.inputPipe = inW
bCtx, cancel := context.WithCancel(ctx)
var doneOnce sync.Once
s.cancelBuild = func() {
doneOnce.Do(func() {
s.mu.Lock()
cancel()
close(s.statusChan)
s.statusChan = nil
s.buildOnGoing.Store(false)
s.mu.Unlock()
})
}
defer s.cancelBuild()
s.manager.updateCond.Broadcast()
s.mu.Unlock()
resp, res, buildErr := s.manager.buildFunc(bCtx, options, inR, pb.NewProgressWriter(s.statusChan))
s.mu.Lock()
// NOTE: buildFunc can return *build.ResultHandle even on error (e.g. when it's implemented using (github.com/docker/buildx/controller/build).RunBuild).
if res != nil {
s.result = res
s.buildOptions = options
s.manager.updateCond.Broadcast()
if buildErr != nil {
buildErr = wrapBuildError(buildErr)
}
}
s.mu.Unlock()
return resp, buildErr
}
func (s *session) getStatusChan() chan *pb.StatusResponse {
s.mu.Lock()
defer s.mu.Unlock()
return s.statusChan
}
func (s *session) getInputWriter() *io.PipeWriter {
s.mu.Lock()
defer s.mu.Unlock()
return s.inputPipe
}
func (s *session) getBuildOptions() *pb.BuildOptions {
s.mu.Lock()
defer s.mu.Unlock()
return s.buildOptions
}
func newSessionManager(buildFunc BuildFunc) *sessionManager {
var mu sync.Mutex
return &sessionManager{
updateCond: sync.NewCond(&mu),
updateCondMu: &mu,
buildFunc: buildFunc,
}
}
type sessionManager struct {
sessions sync.Map
buildFunc BuildFunc
updateCond *sync.Cond
updateCondMu *sync.Mutex
created atomic.Bool
}
func (m *sessionManager) newSession(ref string) *session {
s := &session{manager: m, processes: processes.NewManager()}
m.add(ref, s)
return s
}
func (m *sessionManager) add(id string, v *session) {
m.sessions.Store(id, v)
m.updateCond.Broadcast()
m.created.Store(true)
}
func (m *sessionManager) get(id string) *session {
v, ok := m.sessions.Load(id)
if !ok {
return nil
}
return v.(*session)
}
func (m *sessionManager) delete(id string) error {
v, ok := m.sessions.LoadAndDelete(id)
if !ok {
return errors.Errorf("unknown session %q", id)
}
v.(*session).close()
return nil
}
func (m *sessionManager) list() (res []string) {
m.sessions.Range(func(key, value any) bool {
res = append(res, key.(string))
return true
})
return
}
func (m *sessionManager) close() error {
m.sessions.Range(func(key, value any) bool {
value.(*session).close()
return true
})
return nil
}
func (m *sessionManager) waitAndGet(ctx context.Context, id string, f func(s *session) bool) (*session, error) {
go func() {
<-ctx.Done()
m.updateCond.Broadcast()
}()
for {
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
}
s := m.get(id)
if s == nil || !f(s) {
m.updateCondMu.Lock()
m.updateCond.Wait()
m.updateCondMu.Unlock()
continue
}
return s, nil
}
}
func (m *sessionManager) sessionCreated() bool {
return m.created.Load()
}