golang.org/x/build@v0.0.0-20240506185731-218518f32b70/internal/coordinator/remote/remote.go (about)

     1  // Copyright 2021 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  //go:build linux || darwin
     6  
     7  package remote
     8  
     9  import (
    10  	"context"
    11  	"fmt"
    12  	"log"
    13  	"sort"
    14  	"sync"
    15  	"time"
    16  
    17  	"golang.org/x/build/buildlet"
    18  	"golang.org/x/build/internal"
    19  )
    20  
    21  const (
    22  	remoteBuildletIdleTimeout   = 30 * time.Minute
    23  	remoteBuildletCleanInterval = time.Minute
    24  )
    25  
    26  // Session stores the metadata for a remote buildlet Session.
    27  type Session struct {
    28  	BuilderType string // default builder config to use if not overwritten
    29  	Created     time.Time
    30  	Expires     time.Time
    31  	HostType    string
    32  	ID          string // unique identifier for instance "user-bradfitz-linux-amd64-0"
    33  	OwnerID     string // identity aware proxy user id: "accounts.google.com:userIDvalue"
    34  	buildlet    buildlet.Client
    35  }
    36  
    37  // renew extends the expiration timestamp for a session.
    38  // The SessionPool lock should be held before calling.
    39  func (s *Session) renew() {
    40  	s.Expires = time.Now().Add(remoteBuildletIdleTimeout)
    41  }
    42  
    43  // isExpired determines if the remote buildlet session has expired.
    44  // The SessionPool lock should be held before calling.
    45  func (s *Session) isExpired() bool {
    46  	return !s.Expires.IsZero() && s.Expires.Before(time.Now())
    47  }
    48  
    49  // SessionPool contains active remote buildlet sessions.
    50  type SessionPool struct {
    51  	mu sync.RWMutex
    52  
    53  	once       sync.Once
    54  	pollWait   sync.WaitGroup
    55  	cancelPoll context.CancelFunc
    56  	m          map[string]*Session // keyed by buildletName
    57  }
    58  
    59  // NewSessionPool creates a session pool which stores and provides access to active remote buildlet sessions.
    60  // Either cancelling the context or calling close on the session pool will terminate any polling functions.
    61  func NewSessionPool(ctx context.Context) *SessionPool {
    62  	ctx, cancel := context.WithCancel(ctx)
    63  	sp := &SessionPool{
    64  		cancelPoll: cancel,
    65  		m:          map[string]*Session{},
    66  	}
    67  	sp.pollWait.Add(1)
    68  	go func() {
    69  		internal.PeriodicallyDo(ctx, remoteBuildletCleanInterval, func(ctx context.Context, _ time.Time) {
    70  			sp.destroyExpiredSessions(ctx)
    71  		})
    72  		sp.pollWait.Done()
    73  	}()
    74  	return sp
    75  }
    76  
    77  // AddSession adds the provided session to the session pool.
    78  func (sp *SessionPool) AddSession(ownerID, username, builderType, hostType string, bc buildlet.Client) (name string) {
    79  	sp.mu.Lock()
    80  	defer sp.mu.Unlock()
    81  
    82  	for n := 0; ; n++ {
    83  		name = fmt.Sprintf("%s-%s-%d", username, builderType, n)
    84  		if _, ok := sp.m[name]; !ok {
    85  			now := time.Now()
    86  			sp.m[name] = &Session{
    87  				BuilderType: builderType,
    88  				buildlet:    bc,
    89  				Created:     now,
    90  				Expires:     now.Add(remoteBuildletIdleTimeout),
    91  				HostType:    hostType,
    92  				ID:          name,
    93  				OwnerID:     ownerID,
    94  			}
    95  			return name
    96  		}
    97  	}
    98  }
    99  
   100  // IsSession is true if the instance is found in the session pool. The instance name is the not the public
   101  // name of the instance. It is the name of the instance as it is tracked in the cloud service.
   102  func (sp *SessionPool) IsSession(instName string) bool {
   103  	sp.mu.RLock()
   104  	defer sp.mu.RUnlock()
   105  
   106  	for _, s := range sp.m {
   107  		if s.buildlet.InstanceName() == instName {
   108  			return true
   109  		}
   110  	}
   111  	return false
   112  }
   113  
   114  // destroyExpiredSessions destroys all sessions which have expired.
   115  func (sp *SessionPool) destroyExpiredSessions(ctx context.Context) {
   116  	sp.mu.Lock()
   117  	var ss []*Session
   118  	for name, s := range sp.m {
   119  		if s.isExpired() {
   120  			ss = append(ss, s)
   121  			delete(sp.m, name)
   122  		}
   123  	}
   124  	sp.mu.Unlock()
   125  	// the sessions are no longer in the map. They can be mutated.
   126  	for _, s := range ss {
   127  		log.Printf("remote: destroying expired buildlet %s", s.ID)
   128  		if err := s.buildlet.Close(); err != nil {
   129  			log.Printf("remote: unable to close buildlet connection for %s: %s", s.ID, err)
   130  		}
   131  	}
   132  }
   133  
   134  // DestroySession destroys a session.
   135  func (sp *SessionPool) DestroySession(buildletName string) error {
   136  	sp.mu.Lock()
   137  	s, ok := sp.m[buildletName]
   138  	if ok {
   139  		delete(sp.m, buildletName)
   140  	}
   141  	sp.mu.Unlock()
   142  	if !ok {
   143  		return fmt.Errorf("remote buildlet does not exist=%s", buildletName)
   144  	}
   145  	if err := s.buildlet.Close(); err != nil {
   146  		log.Printf("remote: unable to close buildlet connection %s: %s", buildletName, err)
   147  	}
   148  	return nil
   149  }
   150  
   151  // Close cancels the polling performed by the session pool. It waits for polling to conclude
   152  // before returning.
   153  func (sp *SessionPool) Close() {
   154  	sp.once.Do(func() {
   155  		sp.cancelPoll()
   156  		sp.pollWait.Wait()
   157  	})
   158  }
   159  
   160  // List returns a list of all active sessions sorted by session ID.
   161  func (sp *SessionPool) List() []*Session {
   162  	sp.mu.RLock()
   163  	defer sp.mu.RUnlock()
   164  
   165  	var ss []*Session
   166  	for _, s := range sp.m {
   167  		ss = append(ss, &Session{
   168  			BuilderType: s.BuilderType,
   169  			Expires:     s.Expires,
   170  			HostType:    s.HostType,
   171  			ID:          s.ID,
   172  			OwnerID:     s.OwnerID,
   173  			Created:     s.Created,
   174  		})
   175  	}
   176  	sort.Slice(ss, func(i, j int) bool { return ss[i].ID < ss[j].ID })
   177  	return ss
   178  }
   179  
   180  // Len gives a count of how many sessions are in the pool.
   181  func (sp *SessionPool) Len() int {
   182  	sp.mu.RLock()
   183  	defer sp.mu.RUnlock()
   184  
   185  	return len(sp.m)
   186  }
   187  
   188  // Session retrieves information about the instance associated with a session from the pool.
   189  func (sp *SessionPool) Session(buildletName string) (*Session, error) {
   190  	sp.mu.Lock()
   191  	defer sp.mu.Unlock()
   192  
   193  	if s, ok := sp.m[buildletName]; ok {
   194  		s.renew()
   195  		return &Session{
   196  			BuilderType: s.BuilderType,
   197  			Expires:     s.Expires,
   198  			HostType:    s.HostType,
   199  			ID:          s.ID,
   200  			OwnerID:     s.OwnerID,
   201  		}, nil
   202  	}
   203  	return nil, fmt.Errorf("remote buildlet does not exist=%s", buildletName)
   204  }
   205  
   206  // BuildletClient returns the buildlet client associated with the Session.
   207  func (sp *SessionPool) BuildletClient(buildletName string) (buildlet.Client, error) {
   208  	sp.mu.RLock()
   209  	defer sp.mu.RUnlock()
   210  
   211  	s, ok := sp.m[buildletName]
   212  	if !ok {
   213  		return nil, fmt.Errorf("remote buildlet does not exist=%s", buildletName)
   214  	}
   215  	return s.buildlet, nil
   216  }
   217  
   218  // KeepAlive will renew the remote buildlet session by extending the expiration value. It will
   219  // periodically extend the value until the provided context has been cancelled.
   220  func (sp *SessionPool) KeepAlive(ctx context.Context, buildletName string) error {
   221  	sp.mu.Lock()
   222  	defer sp.mu.Unlock()
   223  
   224  	s, ok := sp.m[buildletName]
   225  	if !ok {
   226  		return fmt.Errorf("remote buildlet does not exist=%s", buildletName)
   227  	}
   228  	s.renew()
   229  	go internal.PeriodicallyDo(ctx, time.Minute, func(ctx context.Context, _ time.Time) {
   230  		sp.mu.Lock()
   231  		ses, ok := sp.m[buildletName]
   232  		if !ok {
   233  			log.Printf("remote: KeepAlive unable to retrieve %s in order to renew the timeout", buildletName)
   234  			return
   235  		}
   236  		ses.renew()
   237  		sp.mu.Unlock()
   238  	})
   239  	return nil
   240  }
   241  
   242  // RenewTimeout will renew the remote buildlet session by extending the expiration value.
   243  func (sp *SessionPool) RenewTimeout(buildletName string) error {
   244  	sp.mu.Lock()
   245  	defer sp.mu.Unlock()
   246  
   247  	s, ok := sp.m[buildletName]
   248  	if !ok {
   249  		return fmt.Errorf("remote buildlet does not exist=%s", buildletName)
   250  	}
   251  	s.renew()
   252  	return nil
   253  }