github.com/shoshinnikita/budget-manager@v0.7.1-0.20220131195411-8c46ff1c6778/internal/app/app.go (about) 1 package app 2 3 import ( 4 "context" 5 "time" 6 7 "github.com/ShoshinNikita/budget-manager/internal/db" 8 "github.com/ShoshinNikita/budget-manager/internal/db/pg" 9 "github.com/ShoshinNikita/budget-manager/internal/db/sqlite" 10 "github.com/ShoshinNikita/budget-manager/internal/logger" 11 "github.com/ShoshinNikita/budget-manager/internal/pkg/errors" 12 "github.com/ShoshinNikita/budget-manager/internal/web" 13 ) 14 15 type App struct { 16 config Config 17 version string 18 gitHash string 19 20 db Database 21 log logger.Logger 22 server *web.Server 23 24 shutdownSignal chan struct{} 25 } 26 27 type Database interface { 28 InitMonth(ctx context.Context, year int, month time.Month) error 29 Shutdown() error 30 31 web.Database 32 } 33 34 // NewApp returns a new instance of App 35 func NewApp(cfg Config, log logger.Logger, version, gitHash string) *App { 36 return &App{ 37 config: cfg, 38 version: version, 39 gitHash: gitHash, 40 // 41 log: log, 42 // 43 shutdownSignal: make(chan struct{}), 44 } 45 } 46 47 // PrepareComponents prepares logger, db and web server 48 func (app *App) PrepareComponents() error { 49 app.log.Debug("prepare database") 50 if err := app.prepareDB(); err != nil { 51 return errors.Wrap(err, "couldn't prepare database") 52 } 53 54 app.log.Debug("prepare web server") 55 if err := app.prepareWebServer(); err != nil { 56 return errors.Wrap(err, "couldn't prepare web server") 57 } 58 59 return nil 60 } 61 62 func (app *App) prepareDB() (err error) { 63 switch app.config.DB.Type { 64 case db.Postgres: 65 app.log.Debug("db type is PostgreSQL") 66 app.db, err = pg.NewDB(app.config.DB.Postgres, app.log) 67 68 case db.Sqlite3: 69 app.log.Debug("db type is SQLite") 70 app.db, err = sqlite.NewDB(app.config.DB.SQLite, app.log) 71 72 default: 73 err = errors.New("unsupported DB type") 74 } 75 if err != nil { 76 return errors.Wrap(err, "couldn't create DB connection") 77 } 78 79 // Init the current month 80 if err := app.initMonth(time.Now()); err != nil { 81 return errors.Wrap(err, "couldn't init the current month") 82 } 83 84 return nil 85 } 86 87 //nolint:unparam 88 func (app *App) prepareWebServer() error { 89 app.server = web.NewServer(app.config.Server, app.db, app.log, app.version, app.gitHash) 90 return nil 91 } 92 93 // Run runs web server. This method should be called in a goroutine 94 func (app *App) Run() error { 95 app.log.WithFields(logger.Fields{ 96 "version": app.version, 97 "git_hash": app.gitHash, 98 }).Info("start app") 99 100 errCh := make(chan error, 2) 101 startBackroundJob := func(errorMsg string, f func() error) { 102 go func() { 103 err := f() 104 if err != nil { 105 app.log.WithError(err).Error(errorMsg) 106 } 107 errCh <- err 108 }() 109 } 110 startBackroundJob("web server failed", app.server.ListenAndServer) 111 startBackroundJob("month init failed", app.startMonthInit) 112 113 return <-errCh 114 } 115 116 // Shutdown shutdowns the app components 117 func (app *App) Shutdown() { 118 app.log.Info("shutdown app") 119 close(app.shutdownSignal) 120 121 app.log.Debug("shutdown web server") 122 if err := app.server.Shutdown(); err != nil { 123 app.log.WithError(err).Error("couldn't shutdown the server gracefully") 124 } 125 126 app.log.Debug("shutdown the database") 127 if err := app.db.Shutdown(); err != nil { 128 app.log.WithError(err).Error("couldn't shutdown the db gracefully") 129 } 130 } 131 132 func (app *App) startMonthInit() error { 133 for { 134 after := calculateTimeToNextMonthInit(time.Now()) 135 136 select { 137 case now := <-time.After(after): 138 app.log.WithField("date", now.Format("2006-01-02")).Info("init a new month") 139 140 if err := app.initMonth(now); err != nil { 141 return errors.Wrap(err, "couldn't init a new month") 142 } 143 144 case <-app.shutdownSignal: 145 return nil 146 } 147 } 148 } 149 150 // calculateTimeToNextMonthInit returns time left to the start (00:00) of the next month 151 func calculateTimeToNextMonthInit(now time.Time) time.Duration { 152 nextMonth := time.Date(now.Year(), now.Month()+1, 1, 0, 0, 0, 0, now.Location()) 153 return nextMonth.Sub(now) 154 } 155 156 // initMonth inits month for the passed date 157 func (app *App) initMonth(t time.Time) error { 158 year, month, _ := t.Date() 159 return app.db.InitMonth(context.Background(), year, month) 160 }