package app import ( "context" "errors" "fmt" "os" "runtime" "runtime/debug" "strings" "sync" "time" "go.uber.org/zap" "github.com/anyproto/any-sync/app/logger" ) var ( // values of this vars will be defined while compilation AppName, GitCommit, GitBranch, GitState, GitSummary, BuildDate string name string ) var ( log = logger.NewNamed("app") StopDeadline = time.Minute StopWarningAfter = time.Second * 10 StartWarningAfter = time.Second * 10 ErrComponentNotFound = errors.New("component not found") ) // 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(ctx context.Context) (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 { parent *App components []Component mu sync.RWMutex startStat Stat stopStat Stat deviceState int versionName string anySyncVersion string componentListener func(comp Component) } // Name returns app name func (app *App) Name() string { return name } func (app *App) AppName() string { return AppName } // Version return app version func (app *App) Version() string { return GitSummary } // SetVersionName sets the custom application version func (app *App) SetVersionName(v string) { app.versionName = v } // VersionName returns a string with the settled app version or auto-generated version if it didn't set func (app *App) VersionName() string { if app.versionName != "" { return app.versionName } return AppName + ":" + GitSummary + "/any-sync:" + app.AnySyncVersion() } type Stat struct { SpentMsPerComp map[string]int64 SpentMsTotal int64 } // StartStat returns total time spent per comp for the last Start func (app *App) StartStat() Stat { app.mu.Lock() defer app.mu.Unlock() return app.startStat } // StopStat returns total time spent per comp for the last Close func (app *App) StopStat() Stat { app.mu.Lock() defer app.mu.Unlock() return app.stopStat } // 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) } // ChildApp creates a child container which has access to parent's components // It doesn't call Start on any of the parent's components func (app *App) ChildApp() *App { return &App{ parent: app, deviceState: app.deviceState, anySyncVersion: app.AnySyncVersion(), componentListener: app.componentListener, } } // 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() current := app for current != nil { for _, s := range current.components { if s.Name() == name { app.onComponent(s) return s } } current = current.parent } 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 GetComponent[t any](app *App) (t, error) { app.mu.RLock() defer app.mu.RUnlock() var empty t current := app for current != nil { for _, s := range current.components { if v, ok := s.(t); ok { app.onComponent(s) return v, nil } } current = current.parent } return empty, ErrComponentNotFound } // MustComponent - generic version of app.MustComponent func MustComponent[t any](app *App) t { component, err := GetComponent[t](app) if err != nil { panic(fmt.Errorf("component with interface %T is not found", new(t))) } return component } // ComponentNames returns all registered names func (app *App) ComponentNames() (names []string) { app.mu.RLock() defer app.mu.RUnlock() names = make([]string, 0, len(app.components)) current := app for current != nil { for _, c := range current.components { names = append(names, c.Name()) } current = current.parent } 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) var currentComponentStarting string done := make(chan struct{}) go func() { select { case <-done: return case <-time.After(StartWarningAfter): l := statLogger(app.stopStat, log).With(zap.String("in_progress", currentComponentStarting)) l.Warn("components start in progress") } }() closeServices := func(idx int) { for i := idx; i >= 0; i-- { if serviceClose, ok := app.components[i].(ComponentRunnable); ok { if e := serviceClose.Close(ctx); e != nil { log.Error("close error", zap.String("component", serviceClose.Name()), zap.Error(e)) } } } } for i, s := range app.components { if err = s.Init(app); err != nil { log.Error("can't init service", zap.String("service", s.Name()), zap.Error(err)) closeServices(i) return fmt.Errorf("can't init service '%s': %w", 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 { log.Error("can't run service", zap.String("service", serviceRun.Name()), zap.Error(err)) closeServices(i) return fmt.Errorf("can't run service '%s': %w", serviceRun.Name(), err) } spent := time.Since(start).Milliseconds() app.startStat.SpentMsTotal += spent app.startStat.SpentMsPerComp[s.Name()] = spent } } close(done) l := statLogger(app.stopStat, log) if app.startStat.SpentMsTotal > StartWarningAfter.Milliseconds() { l.Warn("all components started") } l.Debug("all components started") return } // IterateComponents iterates over all registered components. It's safe for concurrent use. func (app *App) IterateComponents(fn func(Component)) { app.mu.RLock() defer app.mu.RUnlock() for _, s := range app.components { fn(s) } } 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)) } } func statLogger(stat Stat, ctxLogger logger.CtxLogger) logger.CtxLogger { l := ctxLogger for k, v := range stat.SpentMsPerComp { l = l.With(zap.Int64(k, v)) } l = l.With(zap.Int64("total", stat.SpentMsTotal)) return l } // Close stops the application // All components with ComponentRunnable implementation will be closed in the reversed order func (app *App) Close(ctx context.Context) error { log.Debug("close components...") app.mu.RLock() defer app.mu.RUnlock() app.stopStat.SpentMsPerComp = make(map[string]int64) var currentComponentStopping string done := make(chan struct{}) go func() { select { case <-done: return case <-time.After(StopWarningAfter): statLogger(app.stopStat, log). With(zap.String("in_progress", currentComponentStopping)). Warn("components close in progress") } }() go func() { select { case <-done: return case <-time.After(StopDeadline): _, _ = 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 { start := time.Now() currentComponentStopping = app.components[i].Name() if e := serviceClose.Close(ctx); e != nil { errs = append(errs, fmt.Sprintf("Component '%s' close error: %v", serviceClose.Name(), e)) } spent := time.Since(start).Milliseconds() app.stopStat.SpentMsTotal += spent app.stopStat.SpentMsPerComp[app.components[i].Name()] = spent } } close(done) if len(errs) > 0 { return errors.New(strings.Join(errs, "\n")) } l := statLogger(app.stopStat, log) if app.stopStat.SpentMsTotal > StopWarningAfter.Milliseconds() { l.Warn("all components have been closed") } l.Debug("all components have been closed") return nil } func (app *App) SetDeviceState(state int) { if app == nil { return } app.mu.RLock() defer app.mu.RUnlock() app.deviceState = state for _, component := range app.components { if statable, ok := component.(ComponentStatable); ok { statable.StateChange(state) } } } var onceVersion sync.Once func (app *App) AnySyncVersion() string { onceVersion.Do(func() { info, ok := debug.ReadBuildInfo() if ok { for _, mod := range info.Deps { if mod.Path == "github.com/anyproto/any-sync" { app.anySyncVersion = mod.Version break } } } }) return app.anySyncVersion } func (app *App) onComponent(s Component) { if app.componentListener != nil { app.componentListener(s) } }