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  }