Skip to content

Commit

Permalink
add service lifecycle module (#9)
Browse files Browse the repository at this point in the history
* fix mock file (#3)

Co-authored-by: DylanYong <dylan.y@nodereal.io>

* add service lifecycle module


Co-authored-by: DylanYong <dylan.y@nodereal.io>
  • Loading branch information
sysvm and yzhaoyu authored Dec 26, 2022
1 parent 1ea1a1a commit 4bcf160
Show file tree
Hide file tree
Showing 4 changed files with 251 additions and 0 deletions.
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.

0 comments on commit 4bcf160

Please sign in to comment.