Files
wehub-resource-sync 5357c39144
Fuzzer / Run Fuzzer (push) Has been cancelled
Race tests / Go race tests (ubuntu-22.04) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:01:40 +08:00

265 lines
7.8 KiB
Go

// Copyright 2023 Dolthub, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package svcs
import (
"context"
"errors"
"sync"
)
// RunState indicates whether the controller invoked a service's Run method
// before calling Stop. Stop implementations should use this to skip cleanup
// that depends on Run having executed (e.g. waiting on a goroutine that was
// only started inside Run).
type RunState int
const (
// RunNotInvoked means the controller did not call Run on this service.
// This happens when initialization fails before all services are started,
// or when the controller is stopped before transitioning to the running state.
RunNotInvoked RunState = iota
// RunInvoked means the controller called Run on this service.
RunInvoked
)
// A Service is a runnable unit of functionality that a Controller can
// take responsibility for. It has an |Init| function, which can error, and
// which should do all of the initialization and validation work necessary to
// bring the service up. It has a |Run| function, which will be called in a
// separate go-routine and should run and provide the functionality associated
// with the service until the |Stop| function is called.
//
// Stop receives a RunState indicating whether Run was invoked. When
// RunNotInvoked, Stop should only undo work done in Init (e.g. close a
// listener that was created). When RunInvoked, Stop should also wait for the
// Run goroutine to complete if necessary.
type Service interface {
Init(context.Context) error
Run(context.Context)
Stop(RunState) error
}
// AnonService is a simple struct for building Service instances with lambdas
// or funcs, instead of creating an interface implementation.
type AnonService struct {
InitF func(context.Context) error
RunF func(context.Context)
StopF func(RunState) error
}
func (a AnonService) Init(ctx context.Context) error {
if a.InitF == nil {
return nil
}
return a.InitF(ctx)
}
func (a AnonService) Run(ctx context.Context) {
if a.RunF == nil {
return
}
a.RunF(ctx)
}
func (a AnonService) Stop(rs RunState) error {
if a.StopF == nil {
return nil
}
return a.StopF(rs)
}
// A Controller is responsible for initializing a number of registered
// services, running them all, and stopping them all when requested. Services
// are registered with |Register(Service)|. When |Start| is called, the
// services are all initialized, in the order of their registration, and if
// every service initializes successfully, they are |Run| concurrently. When
// |Stop| is called, services are stopped in reverse-registration order. |Stop|
// returns once the corresponding |Stop| method on all successfully |Init|ed
// services has returned. |Stop| does not explicitly block for the goroutines
// where the |Run| methods are called to complete. A Service's |Stop| function
// should use the |RunState| parameter to determine whether |Run| was invoked
// and wait for it to complete if necessary.
//
// Any attempt to register a service after |Start| or |Stop| has been called
// will return an error.
//
// If an error occurs when initializing the services of a Controller, the
// Stop functions of any already initialized Services are called in
// reverse-order with RunNotInvoked. The error which caused the initialization
// error is returned.
//
// In the case that all Services Init successfully, the error returned from
// |Start| is the first non-nil error which is returned from the |Stop|
// functions, in the order they are called.
//
// If |Stop| is called before |Start|, |Start| will return an error. |Register|
// will also begin returning an error after |Stop| is called, if it is called
// before |Start|.
//
// |WaitForStart| can be called at any time on a Controller. It will block
// until |Start| is called. After |Start| is called, if all the services
// successfully initialize, it will return |nil|. Otherwise it will return the
// same error |Start| returned.
//
// |WaitForStop| can be called at any time on a Controller. It will block until
// |Start| is called and initialization fails, or until |Stop| is called. It
// will return the same error which |Start| returned.
type Controller struct {
mu sync.Mutex
services []Service
initErr error
stopErr error
startCh chan struct{}
stopCh chan struct{}
stoppedCh chan struct{}
state controllerState
}
type controllerState int
const (
controllerState_created controllerState = iota
controllerState_starting controllerState = iota
controllerState_running controllerState = iota
controllerState_stopping controllerState = iota
controllerState_stopped controllerState = iota
)
func NewController() *Controller {
return &Controller{
startCh: make(chan struct{}),
stopCh: make(chan struct{}),
stoppedCh: make(chan struct{}),
}
}
func (c *Controller) WaitForStart() error {
<-c.startCh
c.mu.Lock()
err := c.initErr
c.mu.Unlock()
return err
}
func (c *Controller) WaitForStop() error {
<-c.stoppedCh
c.mu.Lock()
var err error
if c.initErr != nil {
err = c.initErr
} else if c.stopErr != nil {
err = c.stopErr
}
c.mu.Unlock()
return err
}
func (c *Controller) Register(svc Service) error {
c.mu.Lock()
if c.state != controllerState_created {
c.mu.Unlock()
return errors.New("Controller: cannot Register a service on a controller which was already started or stopped")
}
c.services = append(c.services, svc)
c.mu.Unlock()
return nil
}
func (c *Controller) Stop() {
c.mu.Lock()
if c.state == controllerState_created {
// Nothing ever ran, we can transition directly to stopped.
// TODO: Is a more correct contract to put an error into initErr here? The services never started successfully...
c.state = controllerState_stopped
close(c.startCh)
close(c.stoppedCh)
c.mu.Unlock()
return
} else if c.state == controllerState_stopped {
// We already stopped.
c.mu.Unlock()
return
} else if c.state != controllerState_stopping {
// We should only do this transition once. We signal to |Start|
// by closing the |stopCh|.
close(c.stopCh)
c.state = controllerState_stopping
c.mu.Unlock()
}
<-c.stoppedCh
}
func (c *Controller) Start(ctx context.Context) error {
c.mu.Lock()
if c.state != controllerState_created {
c.mu.Unlock()
return errors.New("Controller: cannot start service controller after is has been started or stopped")
}
c.state = controllerState_starting
svcs := make([]Service, len(c.services))
copy(svcs, c.services)
c.mu.Unlock()
for i, s := range svcs {
err := s.Init(ctx)
if err != nil {
for j := i - 1; j >= 0; j-- {
svcs[j].Stop(RunNotInvoked)
}
c.mu.Lock()
c.state = controllerState_stopped
c.initErr = err
close(c.startCh)
close(c.stoppedCh)
c.mu.Unlock()
return err
}
}
close(c.startCh)
c.mu.Lock()
runInvoked := false
if c.state == controllerState_starting {
c.state = controllerState_running
c.mu.Unlock()
for _, s := range svcs {
go s.Run(ctx)
}
runInvoked = true
<-c.stopCh
} else {
// We were stopped while initializing. Start shutting things down.
c.mu.Unlock()
}
rs := RunNotInvoked
if runInvoked {
rs = RunInvoked
}
var stopErr error
for i := len(svcs) - 1; i >= 0; i-- {
err := svcs[i].Stop(rs)
if err != nil && stopErr == nil {
stopErr = err
}
}
c.mu.Lock()
if stopErr != nil {
c.stopErr = stopErr
}
c.state = controllerState_stopped
close(c.stoppedCh)
c.mu.Unlock()
return stopErr
}