blob: e3b4572b8443c9ad91a172ec630bcc5bf5d1db0e [file] [log] [blame]
// Copyright 2021 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package remote
import (
"context"
"fmt"
"log"
"sort"
"strings"
"sync"
"time"
"golang.org/x/build/internal"
)
const (
remoteBuildletIdleTimeout = 30 * time.Minute
remoteBuildletCleanInterval = time.Minute
)
// BuildletClient is used in order to enable tests. The interface should contain all the buildlet.Client
// functions used by the callers.
type BuildletClient interface {
Close() error
GCEInstanceName() string
SetGCEInstanceName(v string)
SetName(name string)
}
// Session stores the metadata for a remote buildlet session.
type Session struct {
mu sync.Mutex
builderType string // default builder config to use if not overwritten
buildlet BuildletClient
created time.Time
expires time.Time
hostType string
name string // dup of key
user string // "user-foo" build key
}
// KeepAlive will renew the remote buildlet session by extending the expiration value. It will
// periodically extend the value until the provided context has been cancelled.
func (s *Session) KeepAlive(ctx context.Context) {
go internal.PeriodicallyDo(ctx, time.Minute, func(ctx context.Context, _ time.Time) {
s.renew(ctx)
})
}
// renew extends the expiration timestamp for a session.
func (s *Session) renew(ctx context.Context) {
s.mu.Lock()
defer s.mu.Unlock()
s.expires = time.Now().Add(remoteBuildletIdleTimeout)
}
// isExpired determines if the remote buildlet session has expired.
func (s *Session) isExpired() bool {
s.mu.Lock()
defer s.mu.Unlock()
// check that the expire timestamp has been set and that it has expired.
return !s.expires.IsZero() && s.expires.Before(time.Now())
}
// Buildlet returns the buildlet client associated with the Session.
func (s *Session) Buildlet() BuildletClient {
s.mu.Lock()
defer s.mu.Unlock()
return s.buildlet
}
// Name returns the buildlet's name.
func (s *Session) Name() string {
s.mu.Lock()
defer s.mu.Unlock()
return s.name
}
// SessionPool contains active remote buildlet sessions.
type SessionPool struct {
mu sync.RWMutex
once sync.Once
pollWait sync.WaitGroup
cancelPoll context.CancelFunc
m map[string]*Session // keyed by buildletName
}
// NewSessionPool creates a session pool which stores and provides access to active remote buildlet sessions.
// Either cancelling the context or calling close on the session pool will terminate any polling functions.
func NewSessionPool(ctx context.Context) *SessionPool {
ctx, cancel := context.WithCancel(ctx)
sp := &SessionPool{
cancelPoll: cancel,
m: map[string]*Session{},
}
sp.pollWait.Add(1)
go func() {
internal.PeriodicallyDo(ctx, remoteBuildletCleanInterval, func(ctx context.Context, _ time.Time) {
log.Printf("remote: cleaning up expired remote buildlets")
sp.destroyExpiredSessions(ctx)
})
sp.pollWait.Done()
}()
return sp
}
// AddSession adds the provided session to the session pool.
func (sp *SessionPool) AddSession(user, builderType, hostType string, bc BuildletClient) (name string) {
sp.mu.Lock()
defer sp.mu.Unlock()
for n := 0; ; n++ {
name = fmt.Sprintf("%s-%s-%d", user, builderType, n)
if _, ok := sp.m[name]; !ok {
now := time.Now()
sp.m[name] = &Session{
builderType: builderType,
buildlet: bc,
created: now,
expires: now.Add(remoteBuildletIdleTimeout),
hostType: hostType,
name: name,
user: user,
}
return name
}
}
}
// IsGCESession checks if the session is a GCE instance.
func (sp *SessionPool) IsGCESession(instName string) bool {
sp.mu.RLock()
defer sp.mu.RUnlock()
for _, s := range sp.m {
if s.buildlet.GCEInstanceName() == instName {
return true
}
}
return false
}
// destroyExpiredSessions destroys all sessions which have expired.
func (sp *SessionPool) destroyExpiredSessions(ctx context.Context) {
sp.mu.Lock()
var ss []*Session
for name, s := range sp.m {
if s.isExpired() {
ss = append(ss, s)
delete(sp.m, name)
}
}
sp.mu.Unlock()
// the sessions are no longer in the map. They can be mutated.
for _, s := range ss {
if err := s.buildlet.Close(); err != nil {
log.Printf("remote: unable to close buildlet connection %s", err)
}
}
}
// DestroySession destroys a session.
func (sp *SessionPool) DestroySession(buildletName string) error {
sp.mu.Lock()
s, ok := sp.m[buildletName]
if ok {
delete(sp.m, buildletName)
}
sp.mu.Unlock()
if !ok {
return fmt.Errorf("remote buildlet does not exist=%s", buildletName)
}
if err := s.buildlet.Close(); err != nil {
log.Printf("remote: unable to close buildlet connection %s: %s", buildletName, err)
}
return nil
}
// Close cancels the polling performed by the session pool. It waits for polling to conclude
// before returning.
func (sp *SessionPool) Close() {
sp.once.Do(func() {
sp.cancelPoll()
sp.pollWait.Wait()
})
}
// List returns a list of all active sessions sorted by session name.
func (sp *SessionPool) List() []*Session {
sp.mu.RLock()
defer sp.mu.RUnlock()
var ss []*Session
for _, s := range sp.m {
ss = append(ss, s)
}
sort.Slice(ss, func(i, j int) bool { return ss[i].name < ss[j].name })
return ss
}
// Len gives a count of how many sessions are in the pool.
func (sp *SessionPool) Len() int {
sp.mu.RLock()
defer sp.mu.RUnlock()
return len(sp.m)
}
// Session retrieves a session from the pool.
func (sp *SessionPool) Session(buildletName string) (*Session, error) {
sp.mu.Lock()
defer sp.mu.Unlock()
if rb, ok := sp.m[buildletName]; ok {
rb.expires = time.Now().Add(remoteBuildletIdleTimeout)
return rb, nil
}
return nil, fmt.Errorf("remote buildlet does not exist=%s", buildletName)
}
// userFromGomoteInstanceName returns the username part of a gomote
// remote instance name.
//
// The instance name is of two forms. The normal form is:
//
// user-bradfitz-linux-amd64-0
//
// The overloaded form to convey that the user accepts responsibility
// for changes to the underlying host is to prefix the same instance
// name with the string "mutable-", such as:
//
// mutable-user-bradfitz-darwin-amd64-10_8-0
//
// The mutable part is ignored by this function.
func userFromGomoteInstanceName(name string) string {
name = strings.TrimPrefix(name, "mutable-")
if !strings.HasPrefix(name, "user-") {
return ""
}
user := name[len("user-"):]
hyphen := strings.IndexByte(user, '-')
if hyphen == -1 {
return ""
}
return user[:hyphen]
}