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 }