Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add service lifecycle module #9

Merged
merged 13 commits into from
Dec 26, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions pkg/lifecycle/README.md
Original file line number Diff line number Diff line change
@@ -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)
}
```
124 changes: 124 additions & 0 deletions pkg/lifecycle/lifecycle.go
Original file line number Diff line number Diff line change
@@ -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()
}
8 changes: 8 additions & 0 deletions pkg/lifecycle/lifecycle_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package lifecycle

import "testing"

// TODO(VM):Make up later
func Test(t *testing.T) {

}
77 changes: 77 additions & 0 deletions pkg/lifecycle/mock/lifecycle_mock.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.