mirror of
https://github.com/anyproto/anytype-heart.git
synced 2025-06-11 02:13:41 +09:00
basic app
This commit is contained in:
parent
fb3935cf59
commit
6279f8c45f
2 changed files with 332 additions and 0 deletions
169
app/app.go
Normal file
169
app/app.go
Normal file
|
@ -0,0 +1,169 @@
|
|||
package app
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var (
|
||||
// values of this vars will be defined while compilation
|
||||
version string
|
||||
name string
|
||||
)
|
||||
|
||||
// Component is a minimal interface for a common app.Component
|
||||
type Component interface {
|
||||
// Init will be called first
|
||||
// When returned error is not nil - app start will be aborted
|
||||
Init(a *App) (err error)
|
||||
// Name must return unique service name
|
||||
Name() (name string)
|
||||
}
|
||||
|
||||
// ComponentRunnable is an interface for realizing ability to start background processes or deep configure service
|
||||
type ComponentRunnable interface {
|
||||
Component
|
||||
// Run will be called after init stage
|
||||
// Non-nil error also will be aborted app start
|
||||
Run() error
|
||||
// Close will be called when app shutting down
|
||||
// Also will be called when service return error on Init or Run stage
|
||||
// Non-nil error will be printed to log
|
||||
Close() error
|
||||
}
|
||||
|
||||
// App is the central part of the application
|
||||
// It contains and manages all components
|
||||
type App struct {
|
||||
components []Component
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// Name returns app name
|
||||
func (app *App) Name() string {
|
||||
return name
|
||||
}
|
||||
|
||||
// Version return app version
|
||||
func (app *App) Version() string {
|
||||
return version
|
||||
}
|
||||
|
||||
// Register adds service to registry
|
||||
// All components will be started in the order they were registered
|
||||
func (app *App) Register(s Component) *App {
|
||||
app.mu.Lock()
|
||||
defer app.mu.Unlock()
|
||||
for _, es := range app.components {
|
||||
if s.Name() == es.Name() {
|
||||
panic(fmt.Errorf("component '%s' already registered", s.Name()))
|
||||
}
|
||||
}
|
||||
app.components = append(app.components, s)
|
||||
return app
|
||||
}
|
||||
|
||||
// Component returns service by name
|
||||
// If service with given name wasn't registered, nil will be returned
|
||||
func (app *App) Component(name string) Component {
|
||||
app.mu.RLock()
|
||||
defer app.mu.RUnlock()
|
||||
for _, s := range app.components {
|
||||
if s.Name() == name {
|
||||
return s
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// MustComponent is like Component, but it will panic if service wasn't found
|
||||
func (app *App) MustComponent(name string) Component {
|
||||
s := app.Component(name)
|
||||
if s == nil {
|
||||
panic(fmt.Errorf("component '%s' not registered", name))
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// ComponentNames returns all registered names
|
||||
func (app *App) ComponentNames() (names []string) {
|
||||
app.mu.RLock()
|
||||
defer app.mu.RUnlock()
|
||||
names = make([]string, len(app.components))
|
||||
for i, c := range app.components {
|
||||
names[i] = c.Name()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Start starts the application
|
||||
// All registered services will be initialized and started
|
||||
func (app *App) Start() (err error) {
|
||||
app.mu.RLock()
|
||||
defer app.mu.RUnlock()
|
||||
|
||||
closeServices := func(idx int) {
|
||||
for i := idx; i >= 0; i-- {
|
||||
if serviceClose, ok := app.components[i].(ComponentRunnable); ok {
|
||||
if e := serviceClose.Close(); e != nil {
|
||||
logrus.Warnf("Component '%s' close error: %v", serviceClose.Name(), e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for i, s := range app.components {
|
||||
if err = s.Init(app); err != nil {
|
||||
closeServices(i)
|
||||
return fmt.Errorf("can't init service '%s': %v", s.Name(), err)
|
||||
}
|
||||
}
|
||||
|
||||
for i, s := range app.components {
|
||||
if serviceRun, ok := s.(ComponentRunnable); ok {
|
||||
if err = serviceRun.Run(); err != nil {
|
||||
closeServices(i)
|
||||
return fmt.Errorf("can't run service '%s': %v", serviceRun.Name(), err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Close stops the application
|
||||
// All components with ComponentRunnable implementation will be closed in the reversed order
|
||||
func (app *App) Close() error {
|
||||
logrus.Infof("Close components...")
|
||||
app.mu.RLock()
|
||||
defer app.mu.RUnlock()
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
select {
|
||||
case <-done:
|
||||
return
|
||||
case <-time.After(time.Minute):
|
||||
panic("app.Close timeout")
|
||||
}
|
||||
}()
|
||||
|
||||
var errs []string
|
||||
for i := len(app.components) - 1; i >= 0; i-- {
|
||||
if serviceClose, ok := app.components[i].(ComponentRunnable); ok {
|
||||
logrus.Debugf("Close '%s'", serviceClose.Name())
|
||||
if e := serviceClose.Close(); e != nil {
|
||||
errs = append(errs, fmt.Sprintf("Component '%s' close error: %v", serviceClose.Name(), e))
|
||||
}
|
||||
}
|
||||
}
|
||||
close(done)
|
||||
if len(errs) > 0 {
|
||||
return errors.New(strings.Join(errs, "\n"))
|
||||
}
|
||||
return nil
|
||||
}
|
163
app/app_test.go
Normal file
163
app/app_test.go
Normal file
|
@ -0,0 +1,163 @@
|
|||
package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestAppServiceRegistry(t *testing.T) {
|
||||
app := new(App)
|
||||
t.Run("Register", func(t *testing.T) {
|
||||
app.Register(newTestService(testTypeRunnable, "c1", nil, nil))
|
||||
app.Register(newTestService(testTypeRunnable, "r1", nil, nil))
|
||||
app.Register(newTestService(testTypeComponent, "s1", nil, nil))
|
||||
})
|
||||
t.Run("Component", func(t *testing.T) {
|
||||
assert.Nil(t, app.Component("not-registered"))
|
||||
for _, name := range []string{"c1", "r1", "s1"} {
|
||||
s := app.Component(name)
|
||||
assert.NotNil(t, s, name)
|
||||
assert.Equal(t, name, s.Name())
|
||||
}
|
||||
})
|
||||
t.Run("MustComponent", func(t *testing.T) {
|
||||
for _, name := range []string{"c1", "r1", "s1"} {
|
||||
assert.NotPanics(t, func() { app.MustComponent(name) }, name)
|
||||
}
|
||||
assert.Panics(t, func() { app.MustComponent("not-registered") })
|
||||
})
|
||||
t.Run("ComponentNames", func(t *testing.T) {
|
||||
names := app.ComponentNames()
|
||||
assert.Equal(t, names, []string{"c1", "r1", "s1"})
|
||||
})
|
||||
}
|
||||
|
||||
func TestAppStart(t *testing.T) {
|
||||
t.Run("SuccessStartStop", func(t *testing.T) {
|
||||
app := new(App)
|
||||
seq := new(testSeq)
|
||||
services := [...]iTestService{
|
||||
newTestService(testTypeRunnable, "c1", nil, seq),
|
||||
newTestService(testTypeRunnable, "r1", nil, seq),
|
||||
newTestService(testTypeComponent, "s1", nil, seq),
|
||||
newTestService(testTypeRunnable, "c2", nil, seq),
|
||||
}
|
||||
for _, s := range services {
|
||||
app.Register(s)
|
||||
}
|
||||
assert.Nil(t, app.Start())
|
||||
assert.Nil(t, app.Close())
|
||||
|
||||
var actual []testIds
|
||||
for _, s := range services {
|
||||
actual = append(actual, s.Ids())
|
||||
}
|
||||
|
||||
expected := []testIds{
|
||||
{1, 5, 10},
|
||||
{2, 6, 9},
|
||||
{3, 0, 0},
|
||||
{4, 7, 8},
|
||||
}
|
||||
|
||||
assert.Equal(t, expected, actual)
|
||||
})
|
||||
|
||||
t.Run("InitError", func(t *testing.T) {
|
||||
app := new(App)
|
||||
seq := new(testSeq)
|
||||
expectedErr := fmt.Errorf("testError")
|
||||
services := [...]iTestService{
|
||||
newTestService(testTypeRunnable, "c1", nil, seq),
|
||||
newTestService(testTypeRunnable, "c2", expectedErr, seq),
|
||||
}
|
||||
for _, s := range services {
|
||||
app.Register(s)
|
||||
}
|
||||
|
||||
err := app.Start()
|
||||
assert.NotNil(t, err)
|
||||
assert.Contains(t, err.Error(), expectedErr.Error())
|
||||
|
||||
var actual []testIds
|
||||
for _, s := range services {
|
||||
actual = append(actual, s.Ids())
|
||||
}
|
||||
|
||||
expected := []testIds{
|
||||
{1, 0, 4},
|
||||
{2, 0, 3},
|
||||
}
|
||||
assert.Equal(t, expected, actual)
|
||||
})
|
||||
}
|
||||
|
||||
const (
|
||||
testTypeComponent int = iota
|
||||
testTypeRunnable
|
||||
)
|
||||
|
||||
func newTestService(componentType int, name string, err error, seq *testSeq) (s iTestService) {
|
||||
ts := testComponent{name: name, err: err, seq: seq}
|
||||
switch componentType {
|
||||
case testTypeComponent:
|
||||
return &ts
|
||||
case testTypeRunnable:
|
||||
return &testRunnable{testComponent: ts}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type iTestService interface {
|
||||
Component
|
||||
Ids() (ids testIds)
|
||||
}
|
||||
|
||||
type testIds struct {
|
||||
initId int64
|
||||
runId int64
|
||||
closeId int64
|
||||
}
|
||||
|
||||
type testComponent struct {
|
||||
name string
|
||||
err error
|
||||
seq *testSeq
|
||||
ids testIds
|
||||
}
|
||||
|
||||
func (t *testComponent) Init(a *App) error {
|
||||
t.ids.initId = t.seq.New()
|
||||
return t.err
|
||||
}
|
||||
|
||||
func (t *testComponent) Name() string { return t.name }
|
||||
|
||||
func (t *testComponent) Ids() testIds {
|
||||
return t.ids
|
||||
}
|
||||
|
||||
type testRunnable struct {
|
||||
testComponent
|
||||
}
|
||||
|
||||
func (t *testRunnable) Run() error {
|
||||
t.ids.runId = t.seq.New()
|
||||
return t.err
|
||||
}
|
||||
|
||||
func (t *testRunnable) Close() error {
|
||||
t.ids.closeId = t.seq.New()
|
||||
return t.err
|
||||
}
|
||||
|
||||
type testSeq struct {
|
||||
seq int64
|
||||
}
|
||||
|
||||
func (ts *testSeq) New() int64 {
|
||||
return atomic.AddInt64(&ts.seq, 1)
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue