1
0
Fork 0
mirror of https://github.com/anyproto/anytype-heart.git synced 2025-06-08 05:47:07 +09:00
anytype-heart/app/app.go
2023-05-24 17:10:52 +02:00

265 lines
6.3 KiB
Go

package app
import (
"context"
"errors"
"fmt"
"os"
"runtime"
"strings"
"sync"
"time"
"github.com/anyproto/anytype-heart/metrics"
"github.com/anyproto/anytype-heart/pkg/lib/logging"
)
var (
// values of this vars will be defined while compilation
GitCommit, GitBranch, GitState, GitSummary, BuildDate string
name string
)
var log = logging.Logger("anytype-mw-app")
// 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(ctx context.Context) (err 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() (err error)
}
type ComponentStatable interface {
StateChange(state int)
}
// App is the central part of the application
// It contains and manages all components
type App struct {
components []Component
mu sync.RWMutex
startStat StartStat
deviceState int
}
// Name returns app name
func (app *App) Name() string {
return name
}
// Version return app version
func (app *App) Version() string {
return GitSummary
}
type StartStat struct {
SpentMsPerComp map[string]int64
SpentMsTotal int64
}
// StartStat returns total time spent per comp
func (app *App) StartStat() StartStat {
app.mu.Lock()
defer app.mu.Unlock()
return app.startStat
}
// VersionDescription return the full info about the build
func (app *App) VersionDescription() string {
return VersionDescription()
}
func Version() string {
return GitSummary
}
func VersionDescription() string {
return fmt.Sprintf("build on %s from %s at #%s(%s)", BuildDate, GitBranch, GitCommit, GitState)
}
// 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
}
func MustComponent[i any](app *App) i {
app.mu.RLock()
defer app.mu.RUnlock()
for _, s := range app.components {
if v, ok := s.(i); ok {
return v
}
}
dummy := new(i)
panic(fmt.Errorf("component with interface %T is not found", dummy))
}
// 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(ctx context.Context) (err error) {
app.mu.RLock()
defer app.mu.RUnlock()
app.startStat.SpentMsPerComp = make(map[string]int64)
closeServices := func(idx int) {
for i := idx; i >= 0; i-- {
if serviceClose, ok := app.components[i].(ComponentRunnable); ok {
if e := serviceClose.Close(); e != nil {
log.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 {
start := time.Now()
if err = serviceRun.Run(ctx); err != nil {
closeServices(i)
return fmt.Errorf("can't run service '%s': %v", serviceRun.Name(), err)
}
spent := time.Since(start).Milliseconds()
app.startStat.SpentMsTotal += spent
app.startStat.SpentMsPerComp[s.Name()] = spent
}
}
stat := app.startStat
if stat.SpentMsTotal > 300 {
log.Errorf("AccountCreate app start takes %dms: %v", stat.SpentMsTotal, stat.SpentMsPerComp)
}
var request string
if v, ok := ctx.Value(metrics.CtxKeyRequest).(string); ok {
request = v
}
metrics.SharedClient.RecordEvent(metrics.AppStart{
Request: request,
TotalMs: stat.SpentMsTotal,
PerCompMs: stat.SpentMsPerComp})
log.Debugf("All components started")
return
}
func stackAllGoroutines() []byte {
buf := make([]byte, 1024)
for {
n := runtime.Stack(buf, true)
if n < len(buf) {
return buf[:n]
}
buf = make([]byte, 2*len(buf))
}
}
// Close stops the application
// All components with ComponentRunnable implementation will be closed in the reversed order
func (app *App) Close() error {
log.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):
_, _ = os.Stderr.Write([]byte("app.Close timeout\n"))
_, _ = os.Stderr.Write(stackAllGoroutines())
panic("app.Close timeout")
}
}()
var errs []string
for i := len(app.components) - 1; i >= 0; i-- {
if serviceClose, ok := app.components[i].(ComponentRunnable); ok {
log.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"))
}
log.Debugf("All components have been closed")
return nil
}
func (app *App) SetDeviceState(state int) {
if app == nil {
return
}
app.deviceState = state
for _, component := range app.components {
if statable, ok := component.(ComponentStatable); ok {
statable.StateChange(state)
}
}
}