github.com/crossplane/upjet@v1.3.0/pkg/terraform/provider_scheduler.go (about)

     1  // SPDX-FileCopyrightText: 2023 The Crossplane Authors <https://crossplane.io>
     2  //
     3  // SPDX-License-Identifier: Apache-2.0
     4  
     5  package terraform
     6  
     7  import (
     8  	"sync"
     9  
    10  	"github.com/crossplane/crossplane-runtime/pkg/logging"
    11  	"github.com/pkg/errors"
    12  
    13  	tferrors "github.com/crossplane/upjet/pkg/terraform/errors"
    14  )
    15  
    16  // ProviderHandle represents native plugin (Terraform provider) process
    17  // handles used by the various schedulers to map Terraform workspaces
    18  // to these processes.
    19  type ProviderHandle string
    20  
    21  const (
    22  	// InvalidProviderHandle is an invalid ProviderHandle.
    23  	InvalidProviderHandle ProviderHandle = ""
    24  
    25  	ttlMargin = 0.1
    26  )
    27  
    28  // ProviderScheduler represents a shared native plugin process scheduler.
    29  type ProviderScheduler interface {
    30  	// Start forks or reuses a native plugin process associated with
    31  	// the supplied ProviderHandle.
    32  	Start(ProviderHandle) (InUse, string, error)
    33  	// Stop terminates the native plugin process, if it exists, for
    34  	// the specified ProviderHandle.
    35  	Stop(ProviderHandle) error
    36  }
    37  
    38  // InUse keeps track of the usage of a shared resource,
    39  // like a native plugin process.
    40  type InUse interface {
    41  	// Increment marks one more user of a shared resource
    42  	// such as a native plugin process.
    43  	Increment()
    44  	// Decrement marks when a user of a shared resource,
    45  	// such as a native plugin process, has released the resource.
    46  	Decrement()
    47  }
    48  
    49  // noopInUse satisfies the InUse interface and is a noop implementation.
    50  type noopInUse struct{}
    51  
    52  func (noopInUse) Increment() {}
    53  
    54  func (noopInUse) Decrement() {}
    55  
    56  // NoOpProviderScheduler satisfied the ProviderScheduler interface
    57  // and is a noop implementation, i.e., it does not schedule any
    58  // native plugin processes.
    59  type NoOpProviderScheduler struct{}
    60  
    61  // NewNoOpProviderScheduler initializes a new NoOpProviderScheduler.
    62  func NewNoOpProviderScheduler() NoOpProviderScheduler {
    63  	return NoOpProviderScheduler{}
    64  }
    65  
    66  func (NoOpProviderScheduler) Start(ProviderHandle) (InUse, string, error) {
    67  	return noopInUse{}, "", nil
    68  }
    69  
    70  func (NoOpProviderScheduler) Stop(ProviderHandle) error {
    71  	return nil
    72  }
    73  
    74  type schedulerEntry struct {
    75  	ProviderRunner
    76  	inUse           int
    77  	invocationCount int
    78  }
    79  
    80  type providerInUse struct {
    81  	scheduler *SharedProviderScheduler
    82  	handle    ProviderHandle
    83  }
    84  
    85  func (p *providerInUse) Increment() {
    86  	p.scheduler.mu.Lock()
    87  	defer p.scheduler.mu.Unlock()
    88  	r := p.scheduler.runners[p.handle]
    89  	r.inUse++
    90  	r.invocationCount++
    91  }
    92  
    93  func (p *providerInUse) Decrement() {
    94  	p.scheduler.mu.Lock()
    95  	defer p.scheduler.mu.Unlock()
    96  	if p.scheduler.runners[p.handle].inUse == 0 {
    97  		return
    98  	}
    99  	p.scheduler.runners[p.handle].inUse--
   100  }
   101  
   102  // SharedProviderScheduler is a ProviderScheduler that
   103  // shares a native plugin (Terraform provider) process between
   104  // MR reconciliation loops whose MRs yield the same ProviderHandle, i.e.,
   105  // whose Terraform resource blocks are configuration-wise identical.
   106  // SharedProviderScheduler is configured with a max TTL and it will gracefully
   107  // attempt to replace ProviderRunners whose TTL exceed this maximum,
   108  // if they are not in-use.
   109  type SharedProviderScheduler struct {
   110  	runnerOpts []SharedProviderOption
   111  	runners    map[ProviderHandle]*schedulerEntry
   112  	ttl        int
   113  	mu         *sync.Mutex
   114  	logger     logging.Logger
   115  }
   116  
   117  // SharedProviderSchedulerOption represents an option to configure the
   118  // SharedProviderScheduler.
   119  type SharedProviderSchedulerOption func(scheduler *SharedProviderScheduler)
   120  
   121  // WithSharedProviderOptions configures the SharedProviderOptions to be
   122  // passed down to the managed SharedProviders.
   123  func WithSharedProviderOptions(opts ...SharedProviderOption) SharedProviderSchedulerOption {
   124  	return func(scheduler *SharedProviderScheduler) {
   125  		scheduler.runnerOpts = opts
   126  	}
   127  }
   128  
   129  // NewSharedProviderScheduler initializes a new SharedProviderScheduler
   130  // with the specified logger and options.
   131  func NewSharedProviderScheduler(l logging.Logger, ttl int, opts ...SharedProviderSchedulerOption) *SharedProviderScheduler {
   132  	scheduler := &SharedProviderScheduler{
   133  		mu:      &sync.Mutex{},
   134  		runners: make(map[ProviderHandle]*schedulerEntry),
   135  		logger:  l,
   136  		ttl:     ttl,
   137  	}
   138  	for _, o := range opts {
   139  		o(scheduler)
   140  	}
   141  	return scheduler
   142  }
   143  
   144  func (s *SharedProviderScheduler) Start(h ProviderHandle) (InUse, string, error) {
   145  	logger := s.logger.WithValues("handle", h, "ttl", s.ttl, "ttlMargin", ttlMargin)
   146  	s.mu.Lock()
   147  	defer s.mu.Unlock()
   148  
   149  	r := s.runners[h]
   150  	switch {
   151  	case r != nil && (r.invocationCount < s.ttl || r.inUse > 0):
   152  		if r.invocationCount > int(float64(s.ttl)*(1+ttlMargin)) {
   153  			logger.Debug("Reuse budget has been exceeded. Caller will need to retry.")
   154  			return nil, "", tferrors.NewRetryScheduleError(r.invocationCount, s.ttl)
   155  		}
   156  
   157  		logger.Debug("Reusing the provider runner", "invocationCount", r.invocationCount, "inUse", r.inUse)
   158  		rc, err := r.Start()
   159  		return &providerInUse{
   160  			scheduler: s,
   161  			handle:    h,
   162  		}, rc, errors.Wrapf(err, "cannot use already started provider with handle: %s", h)
   163  	case r != nil:
   164  		logger.Debug("The provider runner has expired. Attempting to stop...", "invocationCount", r.invocationCount, "inUse", r.inUse)
   165  		if err := r.Stop(); err != nil {
   166  			return nil, "", errors.Wrapf(err, "cannot schedule a new shared provider for handle: %s", h)
   167  		}
   168  	}
   169  
   170  	runner := NewSharedProvider(s.runnerOpts...)
   171  	r = &schedulerEntry{
   172  		ProviderRunner: runner,
   173  	}
   174  	runner.logger = logger
   175  	s.runners[h] = r
   176  	logger.Debug("Starting new shared provider...")
   177  	rc, err := s.runners[h].Start()
   178  	return &providerInUse{
   179  		scheduler: s,
   180  		handle:    h,
   181  	}, rc, errors.Wrapf(err, "cannot start the shared provider runner for handle: %s", h)
   182  }
   183  
   184  func (s *SharedProviderScheduler) Stop(ProviderHandle) error {
   185  	// noop
   186  	return nil
   187  }
   188  
   189  // WorkspaceProviderScheduler is a ProviderScheduler that
   190  // shares a native plugin (Terraform provider) process between
   191  // the Terraform CLI invocations in the context of a single
   192  // reconciliation loop (belonging to a single workspace).
   193  // When the managed.ExternalDisconnecter disconnects,
   194  // the scheduler terminates the native plugin process.
   195  type WorkspaceProviderScheduler struct {
   196  	runner ProviderRunner
   197  	logger logging.Logger
   198  	inUse  *workspaceInUse
   199  }
   200  
   201  type workspaceInUse struct {
   202  	wg *sync.WaitGroup
   203  }
   204  
   205  func (w *workspaceInUse) Increment() {
   206  	w.wg.Add(1)
   207  }
   208  
   209  func (w *workspaceInUse) Decrement() {
   210  	w.wg.Done()
   211  }
   212  
   213  // NewWorkspaceProviderScheduler initializes a new WorkspaceProviderScheduler.
   214  func NewWorkspaceProviderScheduler(l logging.Logger, opts ...SharedProviderOption) *WorkspaceProviderScheduler {
   215  	return &WorkspaceProviderScheduler{
   216  		logger: l,
   217  		runner: NewSharedProvider(append([]SharedProviderOption{WithNativeProviderLogger(l)}, opts...)...),
   218  		inUse: &workspaceInUse{
   219  			wg: &sync.WaitGroup{},
   220  		},
   221  	}
   222  }
   223  
   224  func (s *WorkspaceProviderScheduler) Start(h ProviderHandle) (InUse, string, error) {
   225  	s.logger.Debug("Starting workspace scoped provider runner.", "handle", h)
   226  	reattachConfig, err := s.runner.Start()
   227  	return s.inUse, reattachConfig, errors.Wrap(err, "cannot start a workspace provider runner")
   228  }
   229  
   230  func (s *WorkspaceProviderScheduler) Stop(h ProviderHandle) error {
   231  	s.logger.Debug("Attempting to stop workspace scoped shared provider runner.", "handle", h)
   232  	go func() {
   233  		s.inUse.wg.Wait()
   234  		s.logger.Debug("Provider runner not in-use, stopping it.", "handle", h)
   235  		if err := s.runner.Stop(); err != nil {
   236  			s.logger.Info("Failed to stop provider runner", "error", errors.Wrap(err, "cannot stop a workspace provider runner"))
   237  		}
   238  	}()
   239  	return nil
   240  }