From 8e558d8dee26e4d9ced3161b4b6ccd06fc492db0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ferdinand=20M=C3=BCtsch?= Date: Thu, 17 Mar 2022 11:35:20 +0100 Subject: [PATCH] chore: introduce heartbeat max age --- README.md | 5 +++++ config.default.yml | 1 + config/config.go | 9 +++++++++ models/heartbeat.go | 5 +++++ routes/api/heartbeat.go | 2 +- 5 files changed, 21 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 4509b26..d94d560 100644 --- a/README.md +++ b/README.md @@ -128,6 +128,11 @@ You can specify configuration options either via a config file (default: `config | YAML Key / Env. Variable | Default | Description | |------------------------------------------------------------------------------|--------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `env` /
`ENVIRONMENT` | `dev` | Whether to use development- or production settings | +| `app.aggregation_time`
`WAKAPI_AGGREGATION_TIME` | `02:15` | Time of day at which to periodically run summary generation for all users | +| `app.report_time_weekly`
`WAKAPI_REPORT_TIME_WEEKLY` | `fri,18:00` | Week day and time at which to send e-mail reports | +| `app.import_batch_size`
`WAKAPI_IMPORT_BATCH_SIZE` | `50` | Size of batches of heartbeats to insert to the database during importing from external services | +| `app.inactive_days`
`WAKAPI_INACTIVE_DAYS` | `7` | Number of days after which to consider a user inactive (only for metrics) | +| `app.heartbeat_max_age`
`WAKAPI_HEARTBEAT_MAX_AGE` | `4320h` | Maximum acceptable age of a heartbeat (see [`ParseDuration`](https://pkg.go.dev/time#ParseDuration)) | | `app.custom_languages` | - | Map from file endings to language names | | `app.avatar_url_template` | (see [`config.default.yml`](config.default.yml)) | URL template for external user avatar images (e.g. from [Dicebear](https://dicebear.com) or [Gravatar](https://gravatar.com)) | | `server.port` /
`WAKAPI_PORT` | `3000` | Port to listen on | diff --git a/config.default.yml b/config.default.yml index a6fcaa7..1509c45 100644 --- a/config.default.yml +++ b/config.default.yml @@ -16,6 +16,7 @@ app: report_time_weekly: 'fri,18:00' # time at which to fan out weekly reports (format: ',') inactive_days: 7 # time of previous days within a user must have logged in to be considered active import_batch_size: 50 # maximum number of heartbeats to insert into the database within one transaction + heartbeat_max_age: '4320h' # maximum acceptable age of a heartbeat (see https://pkg.go.dev/time#ParseDuration) custom_languages: vue: Vue jsx: JSX diff --git a/config/config.go b/config/config.go index 1182a53..b80e71a 100644 --- a/config/config.go +++ b/config/config.go @@ -68,6 +68,7 @@ type appConfig struct { ImportBackoffMin int `yaml:"import_backoff_min" default:"5" env:"WAKAPI_IMPORT_BACKOFF_MIN"` ImportBatchSize int `yaml:"import_batch_size" default:"50" env:"WAKAPI_IMPORT_BATCH_SIZE"` InactiveDays int `yaml:"inactive_days" default:"7" env:"WAKAPI_INACTIVE_DAYS"` + HeartbeatMaxAge string `yaml:"heartbeat_max_age" default:"4320h" env:"WAKAPI_HEARTBEAT_MAX_AGE"` CountCacheTTLMin int `yaml:"count_cache_ttl_min" default:"30" env:"WAKAPI_COUNT_CACHE_TTL_MIN"` AvatarURLTemplate string `yaml:"avatar_url_template" default:"api/avatar/{username_hash}.svg"` CustomLanguages map[string]string `yaml:"custom_languages"` @@ -242,6 +243,11 @@ func (c *appConfig) GetWeeklyReportTime() string { return strings.Split(c.ReportTimeWeekly, ",")[1] } +func (c *appConfig) HeartbeatsMaxAge() time.Duration { + d, _ := time.ParseDuration(c.HeartbeatMaxAge) + return d +} + func (c *dbConfig) IsSQLite() bool { return c.Dialect == "sqlite3" } @@ -400,6 +406,9 @@ func Load(version string) *Config { if _, err := time.Parse("15:04", config.App.AggregationTime); err != nil { logbuch.Fatal("invalid interval set for aggregation_time") } + if _, err := time.ParseDuration(config.App.HeartbeatMaxAge); err != nil { + logbuch.Fatal("invalid duration set for heartbeat_max_age") + } Set(config) return Get() diff --git a/models/heartbeat.go b/models/heartbeat.go index f1bdd6a..98cb45d 100644 --- a/models/heartbeat.go +++ b/models/heartbeat.go @@ -34,6 +34,11 @@ func (h *Heartbeat) Valid() bool { return h.User != nil && h.UserID != "" && h.User.ID == h.UserID && h.Time != CustomTime(time.Time{}) } +func (h *Heartbeat) Timely(maxAge time.Duration) bool { + now := time.Now() + return now.Sub(h.Time.T()) <= maxAge && h.Time.T().Before(now) +} + func (h *Heartbeat) Augment(languageMappings map[string]string) { maxPrec := -1 // precision / mapping complexity -> more concrete ones shall take precedence for ending, value := range languageMappings { diff --git a/routes/api/heartbeat.go b/routes/api/heartbeat.go index fca3f86..68e1bff 100644 --- a/routes/api/heartbeat.go +++ b/routes/api/heartbeat.go @@ -86,7 +86,7 @@ func (h *HeartbeatApiHandler) Post(w http.ResponseWriter, r *http.Request) { hb.UserID = user.ID hb.UserAgent = userAgent - if !hb.Valid() { + if !hb.Valid() || !hb.Timely(h.config.App.HeartbeatsMaxAge()) { w.WriteHeader(http.StatusBadRequest) w.Write([]byte("invalid heartbeat object")) return