diff --git a/pkg/lifecycle/README.md b/pkg/lifecycle/README.md new file mode 100644 index 000000000..3feee562b --- /dev/null +++ b/pkg/lifecycle/README.md @@ -0,0 +1,42 @@ +# Service Lifecycle + +This package provides useful method to manage the lifecycle of a service or a group of services with convenient initializations of components. + +## Interface + +```go +type Service interface { + Name() string + Start(ctx context.Context) error + Stop(ctx context.Context) error +} +``` + +## Feature + +- Start and stop a group of services +- Monitor signals to stop services +- Graceful shutdown + +## Example + +```go +package main + +import ( + "context" + "syscall" + "time" + + "github.com/bnb-chain/inscription-storage-provider/pkg/lifecycle" + "http_server" + "rpc_server" +) + +func main() { + ctx := context.Background() + l := lifecycle.NewService(5 * time.Second) + l.RegisterServices(http_server, rpc_server) + l.Signals(syscall.SIGINT, syscall.SIGTERM).Init(ctx).StartServices(ctx).Wait(ctx) +} +``` diff --git a/pkg/lifecycle/lifecycle.go b/pkg/lifecycle/lifecycle.go new file mode 100644 index 000000000..18cb2ec15 --- /dev/null +++ b/pkg/lifecycle/lifecycle.go @@ -0,0 +1,124 @@ +package lifecycle + +import ( + "context" + "errors" + "os" + "os/signal" + "time" + + "github.com/bnb-chain/inscription-storage-provider/util/log" +) + +// Service provides abstract methods to control the lifecycle of a service +// +//go:generate mockgen -source=./lifecycle.go -destination=./mock/lifecycle_mock.go -package=mock +type Service interface { + // Name describe service name + Name() string + // Start a service, this method should be used in non-block form + Start(ctx context.Context) error + // Stop a service, this method should be used in non-block form + Stop(ctx context.Context) error +} + +// ServiceLifecycle manages services' lifecycle +type ServiceLifecycle struct { + innerCtx context.Context + innerCancel context.CancelFunc + services []Service + timeout time.Duration +} + +// NewService returns an initialized service lifecycle +func NewService(timeout time.Duration) *ServiceLifecycle { + innerCtx, innerCancel := context.WithCancel(context.Background()) + return &ServiceLifecycle{ + innerCtx: innerCtx, + innerCancel: innerCancel, + timeout: timeout, + } +} + +// RegisterServices register services of an application +func (s *ServiceLifecycle) RegisterServices(services ...Service) { + s.services = append(s.services, services...) +} + +// StartServices starts running services +func (s *ServiceLifecycle) StartServices(ctx context.Context) *ServiceLifecycle { + s.start(ctx) + return s +} + +func (s *ServiceLifecycle) start(ctx context.Context) { + for i, service := range s.services { + if err := service.Start(ctx); err != nil { + log.Errorf("Service %s starts error: %v", service.Name(), err) + s.services = s.services[:i] + s.innerCancel() + break + } else { + log.Infof("Service %s starts successfully", service.Name()) + } + } +} + +// Signals registers monitor signals +func (s *ServiceLifecycle) Signals(sigs ...os.Signal) *ServiceLifecycle { + go s.signals(sigs...) + return s +} + +func (s *ServiceLifecycle) signals(sigs ...os.Signal) { + sigCh := make(chan os.Signal, 1) + signal.Notify(sigCh, sigs...) + for { + select { + case <-s.innerCtx.Done(): + return + case sig := <-sigCh: + for _, j := range sigs { + if j == sig { + s.innerCancel() + return + } + } + } + } +} + +// Wait blocks until context is done +func (s *ServiceLifecycle) Wait(ctx context.Context) { + <-s.innerCtx.Done() + s.StopServices(ctx) +} + +// StopServices stop services when context is done or timeout +func (s *ServiceLifecycle) StopServices(ctx context.Context) { + gCtx, cancel := context.WithTimeout(context.Background(), s.timeout) + s.stop(ctx, cancel) + + <-gCtx.Done() + if errors.Is(gCtx.Err(), context.Canceled) { + log.Infow("Services stop working", "service config timeout", s.timeout) + } else if errors.Is(gCtx.Err(), context.DeadlineExceeded) { + log.Error("Timeout while stopping service, killing instance manually") + } +} + +func (s *ServiceLifecycle) stop(ctx context.Context, cancel context.CancelFunc) { + for _, service := range s.services { + if err := service.Stop(ctx); err != nil { + log.Errorf("Service %s stops failure: %v", service.Name(), err) + } else { + log.Infof("Service %s stops successfully!", service.Name()) + } + } + cancel() +} + +// Done check context is done +func (s *ServiceLifecycle) Done() <-chan struct{} { + return s.innerCtx.Done() +} diff --git a/pkg/lifecycle/lifecycle_test.go b/pkg/lifecycle/lifecycle_test.go new file mode 100644 index 000000000..3307bddde --- /dev/null +++ b/pkg/lifecycle/lifecycle_test.go @@ -0,0 +1,8 @@ +package lifecycle + +import "testing" + +// TODO(VM):Make up later +func Test(t *testing.T) { + +} diff --git a/pkg/lifecycle/mock/lifecycle_mock.go b/pkg/lifecycle/mock/lifecycle_mock.go new file mode 100644 index 000000000..848e45d2d --- /dev/null +++ b/pkg/lifecycle/mock/lifecycle_mock.go @@ -0,0 +1,77 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: ./lifecycle.go + +// Package mock is a generated GoMock package. +package mock + +import ( + context "context" + reflect "reflect" + + gomock "github.com/golang/mock/gomock" +) + +// MockService is a mock of Service interface. +type MockService struct { + ctrl *gomock.Controller + recorder *MockServiceMockRecorder +} + +// MockServiceMockRecorder is the mock recorder for MockService. +type MockServiceMockRecorder struct { + mock *MockService +} + +// NewMockService creates a new mock instance. +func NewMockService(ctrl *gomock.Controller) *MockService { + mock := &MockService{ctrl: ctrl} + mock.recorder = &MockServiceMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockService) EXPECT() *MockServiceMockRecorder { + return m.recorder +} + +// Name mocks base method. +func (m *MockService) Name() string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Name") + ret0, _ := ret[0].(string) + return ret0 +} + +// Name indicates an expected call of Name. +func (mr *MockServiceMockRecorder) Name() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Name", reflect.TypeOf((*MockService)(nil).Name)) +} + +// Start mocks base method. +func (m *MockService) Start(ctx context.Context) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Start", ctx) + ret0, _ := ret[0].(error) + return ret0 +} + +// Start indicates an expected call of Start. +func (mr *MockServiceMockRecorder) Start(ctx interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Start", reflect.TypeOf((*MockService)(nil).Start), ctx) +} + +// Stop mocks base method. +func (m *MockService) Stop(ctx context.Context) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Stop", ctx) + ret0, _ := ret[0].(error) + return ret0 +} + +// Stop indicates an expected call of Stop. +func (mr *MockServiceMockRecorder) Stop(ctx interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Stop", reflect.TypeOf((*MockService)(nil).Stop), ctx) +}