diff --git a/backend/app/api/app.go b/backend/app/api/app.go index 73d7809..5d285d3 100644 --- a/backend/app/api/app.go +++ b/backend/app/api/app.go @@ -1,22 +1,18 @@ package main import ( - "time" - "github.com/hay-kot/homebox/backend/internal/core/services" "github.com/hay-kot/homebox/backend/internal/core/services/reporting/eventbus" "github.com/hay-kot/homebox/backend/internal/data/ent" "github.com/hay-kot/homebox/backend/internal/data/repo" "github.com/hay-kot/homebox/backend/internal/sys/config" "github.com/hay-kot/homebox/backend/pkgs/mailer" - "github.com/hay-kot/httpkit/server" ) type app struct { conf *config.Config mailer mailer.Mailer db *ent.Client - server *server.Server repos *repo.AllRepos services *services.AllServices bus *eventbus.EventBus @@ -37,13 +33,3 @@ func new(conf *config.Config) *app { return s } - -func (a *app) startBgTask(t time.Duration, fn func()) { - timer := time.NewTimer(t) - - for { - timer.Reset(t) - a.server.Background(fn) - <-timer.C - } -} diff --git a/backend/app/api/bgrunner.go b/backend/app/api/bgrunner.go new file mode 100644 index 0000000..ce4b7cc --- /dev/null +++ b/backend/app/api/bgrunner.go @@ -0,0 +1,37 @@ +package main + +import ( + "context" + "time" +) + +type BackgroundTask struct { + name string + Interval time.Duration + Fn func(context.Context) +} + +func (tsk *BackgroundTask) Name() string { + return tsk.name +} + +func NewTask(name string, interval time.Duration, fn func(context.Context)) *BackgroundTask { + return &BackgroundTask{ + Interval: interval, + Fn: fn, + } +} + +func (tsk *BackgroundTask) Start(ctx context.Context) error { + timer := time.NewTimer(tsk.Interval) + + for { + select { + case <-ctx.Done(): + return nil + case <-timer.C: + timer.Reset(tsk.Interval) + tsk.Fn(ctx) + } + } +} diff --git a/backend/app/api/main.go b/backend/app/api/main.go index 1389285..0a352e9 100644 --- a/backend/app/api/main.go +++ b/backend/app/api/main.go @@ -23,7 +23,7 @@ import ( "github.com/hay-kot/homebox/backend/internal/sys/config" "github.com/hay-kot/homebox/backend/internal/web/mid" "github.com/hay-kot/httpkit/errchain" - "github.com/hay-kot/httpkit/server" + "github.com/hay-kot/httpkit/graceful" "github.com/rs/zerolog" "github.com/rs/zerolog/log" "github.com/rs/zerolog/pkgerrors" @@ -178,41 +178,54 @@ func run(cfg *config.Config) error { middleware.StripSlashes, ) - chain := errchain.New(mid.Errors(app.server, logger)) + chain := errchain.New(mid.Errors(logger)) app.mountRoutes(router, chain, app.repos) - app.server = server.NewServer( - server.WithHost(app.conf.Web.Host), - server.WithPort(app.conf.Web.Port), - server.WithReadTimeout(app.conf.Web.ReadTimeout), - server.WithWriteTimeout(app.conf.Web.WriteTimeout), - server.WithIdleTimeout(app.conf.Web.IdleTimeout), - ) - log.Info().Msgf("Starting HTTP Server on %s:%s", app.server.Host, app.server.Port) + runner := graceful.NewRunner() + + runner.AddFunc("server", func(ctx context.Context) error { + httpserver := http.Server{ + Addr: fmt.Sprintf("%s:%s", cfg.Web.Host, cfg.Web.Port), + Handler: router, + ReadTimeout: cfg.Web.ReadTimeout, + WriteTimeout: cfg.Web.WriteTimeout, + IdleTimeout: cfg.Web.IdleTimeout, + } + + go func() { + <-ctx.Done() + _ = httpserver.Shutdown(context.Background()) + }() + + log.Info().Msgf("Server is running on %s:%s", cfg.Web.Host, cfg.Web.Port) + return httpserver.ListenAndServe() + }) // ========================================================================= // Start Reoccurring Tasks - go app.bus.Run() + runner.AddFunc("eventbus", app.bus.Run) - go app.startBgTask(time.Duration(24)*time.Hour, func() { - _, err := app.repos.AuthTokens.PurgeExpiredTokens(context.Background()) + runner.AddPlugin(NewTask("purge-tokens", time.Duration(24)*time.Hour, func(ctx context.Context) { + _, err := app.repos.AuthTokens.PurgeExpiredTokens(ctx) if err != nil { log.Error(). Err(err). Msg("failed to purge expired tokens") } - }) - go app.startBgTask(time.Duration(24)*time.Hour, func() { - _, err := app.repos.Groups.InvitationPurge(context.Background()) + })) + + runner.AddPlugin(NewTask("purge-invitations", time.Duration(24)*time.Hour, func(ctx context.Context) { + _, err := app.repos.Groups.InvitationPurge(ctx) if err != nil { log.Error(). Err(err). Msg("failed to purge expired invitations") } - }) - go app.startBgTask(time.Duration(1)*time.Hour, func() { + })) + + runner.AddPlugin(NewTask("send-notifications", time.Duration(1)*time.Hour, func(ctx context.Context) { now := time.Now() if now.Hour() == 8 { @@ -224,7 +237,7 @@ func run(cfg *config.Config) error { Msg("failed to send notifiers") } } - }) + })) // TODO: Remove through external API that does setup if cfg.Demo { @@ -233,13 +246,24 @@ func run(cfg *config.Config) error { } if cfg.Debug.Enabled { - debugrouter := app.debugRouter() - go func() { - if err := http.ListenAndServe(":"+cfg.Debug.Port, debugrouter); err != nil { - log.Fatal().Err(err).Msg("failed to start debug server") + runner.AddFunc("debug", func(ctx context.Context) error { + debugserver := http.Server{ + Addr: fmt.Sprintf("%s:%s", cfg.Web.Host, cfg.Debug.Port), + Handler: app.debugRouter(), + ReadTimeout: cfg.Web.ReadTimeout, + WriteTimeout: cfg.Web.WriteTimeout, + IdleTimeout: cfg.Web.IdleTimeout, } - }() + + go func() { + <-ctx.Done() + _ = debugserver.Shutdown(context.Background()) + }() + + log.Info().Msgf("Debug server is running on %s:%s", cfg.Web.Host, cfg.Debug.Port) + return debugserver.ListenAndServe() + }) } - return app.server.Start(router) + return runner.Start(context.Background()) } diff --git a/backend/go.mod b/backend/go.mod index 1b9bfea..8a0f9d9 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -14,7 +14,7 @@ require ( github.com/gocarina/gocsv v0.0.0-20231116093920-b87c2d0e983a github.com/google/uuid v1.6.0 github.com/gorilla/schema v1.2.1 - github.com/hay-kot/httpkit v0.0.6 + github.com/hay-kot/httpkit v0.0.9 github.com/mattn/go-sqlite3 v1.14.22 github.com/olahol/melody v1.1.4 github.com/pkg/errors v0.9.1 diff --git a/backend/go.sum b/backend/go.sum index 755099b..fec9706 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -86,6 +86,12 @@ github.com/hashicorp/hcl/v2 v2.19.1 h1://i05Jqznmb2EXqa39Nsvyan2o5XyMowW5fnCKW5R github.com/hashicorp/hcl/v2 v2.19.1/go.mod h1:ThLC89FV4p9MPW804KVbe/cEXoQ8NZEh+JtMeeGErHE= github.com/hay-kot/httpkit v0.0.6 h1:BidC4UrkS7zRhoTdpKLeF8ODJPKcOZkJ2tk2t2ZIQjQ= github.com/hay-kot/httpkit v0.0.6/go.mod h1:1s/OJwWRyH6tBtTw76jTp6kwBYvjswziXaokPQH7eKQ= +github.com/hay-kot/httpkit v0.0.7 h1:KxGi+MwXFavfFUfJEMpye5cnMef9TlFu3v7UZipUB8U= +github.com/hay-kot/httpkit v0.0.7/go.mod h1:AD22YluZrvBDxmtB3Pw2SOyp3A2PZqcmBZa0+COrhoU= +github.com/hay-kot/httpkit v0.0.8 h1:n+Z5z35YZcdD9cGwbnIPRbrgDw9LY6lqakH4zYr5z+A= +github.com/hay-kot/httpkit v0.0.8/go.mod h1:AD22YluZrvBDxmtB3Pw2SOyp3A2PZqcmBZa0+COrhoU= +github.com/hay-kot/httpkit v0.0.9 h1:hu2TPY9awmIYWXxWGubaXl2U61pPvaVsm9YwboBRGu0= +github.com/hay-kot/httpkit v0.0.9/go.mod h1:AD22YluZrvBDxmtB3Pw2SOyp3A2PZqcmBZa0+COrhoU= github.com/jarcoal/httpmock v1.3.0 h1:2RJ8GP0IIaWwcC9Fp2BmVi8Kog3v2Hn7VXM3fTd+nuc= github.com/jarcoal/httpmock v1.3.0/go.mod h1:3yb8rc4BI7TCBhFY8ng0gjuLKJNquuDNiPaZjnENuYg= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= diff --git a/backend/internal/core/services/reporting/eventbus/eventbus.go b/backend/internal/core/services/reporting/eventbus/eventbus.go index e221b44..cf606fe 100644 --- a/backend/internal/core/services/reporting/eventbus/eventbus.go +++ b/backend/internal/core/services/reporting/eventbus/eventbus.go @@ -2,6 +2,7 @@ package eventbus import ( + "context" "sync" "github.com/google/uuid" @@ -43,24 +44,29 @@ func New() *EventBus { } } -func (e *EventBus) Run() { +func (e *EventBus) Run(ctx context.Context) error { if e.started { panic("event bus already started") } e.started = true - for event := range e.ch { - e.mu.RLock() - arr, ok := e.subscribers[event.event] - e.mu.RUnlock() + for { + select { + case <-ctx.Done(): + return nil + case event := <-e.ch: + e.mu.RLock() + arr, ok := e.subscribers[event.event] + e.mu.RUnlock() - if !ok { - continue - } + if !ok { + continue + } - for _, fn := range arr { - fn(event.data) + for _, fn := range arr { + fn(event.data) + } } } } diff --git a/backend/internal/data/repo/main_test.go b/backend/internal/data/repo/main_test.go index ad5fcb9..47e5ec0 100644 --- a/backend/internal/data/repo/main_test.go +++ b/backend/internal/data/repo/main_test.go @@ -45,7 +45,9 @@ func TestMain(m *testing.M) { log.Fatalf("failed opening connection to sqlite: %v", err) } - go tbus.Run() + go func() { + _ = tbus.Run(context.Background()) + }() err = client.Schema.Create(context.Background()) if err != nil { diff --git a/backend/internal/sys/config/conf.go b/backend/internal/sys/config/conf.go index 3b84e7d..8b7b23c 100644 --- a/backend/internal/sys/config/conf.go +++ b/backend/internal/sys/config/conf.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "os" + "time" "github.com/ardanlabs/conf/v3" ) @@ -39,12 +40,12 @@ type DebugConf struct { } type WebConfig struct { - Port string `yaml:"port" conf:"default:7745"` - Host string `yaml:"host"` - MaxUploadSize int64 `yaml:"max_file_upload" conf:"default:10"` - ReadTimeout int `yaml:"read_timeout" conf:"default:10"` - WriteTimeout int `yaml:"write_timeout" conf:"default:10"` - IdleTimeout int `yaml:"idle_timeout" conf:"default:30"` + Port string `yaml:"port" conf:"default:7745"` + Host string `yaml:"host"` + MaxUploadSize int64 `yaml:"max_file_upload" conf:"default:10"` + ReadTimeout time.Duration `yaml:"read_timeout" conf:"default:10s"` + WriteTimeout time.Duration `yaml:"write_timeout" conf:"default:10s"` + IdleTimeout time.Duration `yaml:"idle_timeout" conf:"default:30s"` } // New parses the CLI/Config file and returns a Config struct. If the file argument is an empty string, the diff --git a/backend/internal/web/mid/errors.go b/backend/internal/web/mid/errors.go index c619477..c8b04d6 100644 --- a/backend/internal/web/mid/errors.go +++ b/backend/internal/web/mid/errors.go @@ -16,7 +16,7 @@ type ErrorResponse struct { Fields map[string]string `json:"fields,omitempty"` } -func Errors(svr *server.Server, log zerolog.Logger) errchain.ErrorHandler { +func Errors(log zerolog.Logger) errchain.ErrorHandler { return func(h errchain.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { err := h.ServeHTTP(w, r) @@ -71,14 +71,6 @@ func Errors(svr *server.Server, log zerolog.Logger) errchain.ErrorHandler { if err := server.JSON(w, code, resp); err != nil { log.Err(err).Msg("failed to write response") } - - // If Showdown error, return error - if server.IsShutdownError(err) { - err := svr.Shutdown(err.Error()) - if err != nil { - log.Err(err).Msg("failed to shutdown server") - } - } } }) } diff --git a/frontend/components/Form/DatePicker.vue b/frontend/components/Form/DatePicker.vue index 2a681ea..e4bc7bc 100644 --- a/frontend/components/Form/DatePicker.vue +++ b/frontend/components/Form/DatePicker.vue @@ -3,7 +3,7 @@ - +