1
0
mirror of https://github.com/muety/wakapi.git synced 2023-08-10 21:12:56 +03:00

feat: implement computation of users first heartbeats data time

This commit is contained in:
Ferdinand Mütsch 2022-12-23 13:41:32 +01:00
parent ebcf87ea93
commit 8a94fef06b
10 changed files with 117 additions and 20 deletions

View File

@ -33,6 +33,7 @@ const (
KeyLatestTotalTime = "latest_total_time" KeyLatestTotalTime = "latest_total_time"
KeyLatestTotalUsers = "latest_total_users" KeyLatestTotalUsers = "latest_total_users"
KeyLastImportImport = "last_import" KeyLastImportImport = "last_import"
KeyFirstHeartbeat = "first_heartbeat"
KeyNewsbox = "newsbox" KeyNewsbox = "newsbox"
SimpleDateFormat = "2006-01-02" SimpleDateFormat = "2006-01-02"

View File

@ -183,14 +183,14 @@ func main() {
reportService = services.NewReportService(summaryService, userService, mailService) reportService = services.NewReportService(summaryService, userService, mailService)
diagnosticsService = services.NewDiagnosticsService(diagnosticsRepository) diagnosticsService = services.NewDiagnosticsService(diagnosticsRepository)
housekeepingService = services.NewHousekeepingService(userService, heartbeatService, summaryService) housekeepingService = services.NewHousekeepingService(userService, heartbeatService, summaryService)
miscService = services.NewMiscService(userService, summaryService, keyValueService) miscService = services.NewMiscService(userService, heartbeatService, summaryService, keyValueService)
// Schedule background tasks // Schedule background tasks
go aggregationService.Schedule() go aggregationService.Schedule()
go leaderboardService.Schedule() go leaderboardService.Schedule()
go reportService.Schedule() go reportService.Schedule()
go housekeepingService.Schedule() go housekeepingService.Schedule()
go miscService.ScheduleCountTotalTime() go miscService.Schedule()
routes.Init() routes.Init()

View File

@ -1,6 +1,9 @@
package view package view
import "github.com/muety/wakapi/models" import (
"github.com/muety/wakapi/models"
"time"
)
type SettingsViewModel struct { type SettingsViewModel struct {
User *models.User User *models.User
@ -10,6 +13,7 @@ type SettingsViewModel struct {
Projects []string Projects []string
SubscriptionPrice string SubscriptionPrice string
DataRetentionMonths int DataRetentionMonths int
UserFirstData time.Time
SupportContact string SupportContact string
ApiKey string ApiKey string
Success string Success string

View File

@ -34,6 +34,17 @@ func (r *KeyValueRepository) GetString(key string) (*models.KeyStringValue, erro
return kv, nil return kv, nil
} }
func (r *KeyValueRepository) Search(like string) ([]*models.KeyStringValue, error) {
var keyValues []*models.KeyStringValue
if err := r.db.Table("key_string_values").
Where("`key` like ?", like).
Find(&keyValues).
Error; err != nil {
return nil, err
}
return keyValues, nil
}
func (r *KeyValueRepository) PutString(kv *models.KeyStringValue) error { func (r *KeyValueRepository) PutString(kv *models.KeyStringValue) error {
result := r.db. result := r.db.
Clauses(clause.OnConflict{ Clauses(clause.OnConflict{

View File

@ -43,6 +43,7 @@ type IKeyValueRepository interface {
GetString(string) (*models.KeyStringValue, error) GetString(string) (*models.KeyStringValue, error)
PutString(*models.KeyStringValue) error PutString(*models.KeyStringValue) error
DeleteString(string) error DeleteString(string) error
Search(string) ([]*models.KeyStringValue, error)
} }
type ILanguageMappingRepository interface { type ILanguageMappingRepository interface {

View File

@ -739,6 +739,13 @@ func (h *SettingsHandler) buildViewModel(r *http.Request) *view.SettingsViewMode
subscriptionPrice = h.config.Subscriptions.StandardPrice subscriptionPrice = h.config.Subscriptions.StandardPrice
} }
// user first data
var firstData time.Time
firstDataKv := h.keyValueSrvc.MustGetString(fmt.Sprintf("%s_%s", conf.KeyFirstHeartbeat, user.ID))
if firstDataKv.Value != "" {
firstData, _ = time.Parse(time.RFC822Z, firstDataKv.Value)
}
return &view.SettingsViewModel{ return &view.SettingsViewModel{
User: user, User: user,
LanguageMappings: mappings, LanguageMappings: mappings,
@ -746,6 +753,7 @@ func (h *SettingsHandler) buildViewModel(r *http.Request) *view.SettingsViewMode
Labels: combinedLabels, Labels: combinedLabels,
Projects: projects, Projects: projects,
ApiKey: user.ApiKey, ApiKey: user.ApiKey,
UserFirstData: firstData,
SubscriptionPrice: subscriptionPrice, SubscriptionPrice: subscriptionPrice,
SupportContact: h.config.App.SupportContact, SupportContact: h.config.App.SupportContact,
DataRetentionMonths: h.config.App.DataRetentionMonths, DataRetentionMonths: h.config.App.DataRetentionMonths,

View File

@ -22,6 +22,10 @@ func (srv *KeyValueService) GetString(key string) (*models.KeyStringValue, error
return srv.repository.GetString(key) return srv.repository.GetString(key)
} }
func (srv *KeyValueService) GetByPrefix(prefix string) ([]*models.KeyStringValue, error) {
return srv.repository.Search(prefix + "%")
}
func (srv *KeyValueService) MustGetString(key string) *models.KeyStringValue { func (srv *KeyValueService) MustGetString(key string) *models.KeyStringValue {
kv, err := srv.repository.GetString(key) kv, err := srv.repository.GetString(key)
if err != nil { if err != nil {

View File

@ -1,6 +1,7 @@
package services package services
import ( import (
"fmt"
"github.com/emvi/logbuch" "github.com/emvi/logbuch"
"github.com/muety/artifex/v2" "github.com/muety/artifex/v2"
"github.com/muety/wakapi/config" "github.com/muety/wakapi/config"
@ -15,23 +16,27 @@ import (
const ( const (
countUsersEvery = 1 * time.Hour countUsersEvery = 1 * time.Hour
computeOldestDataEvery = 6 * time.Hour
) )
var countLock = sync.Mutex{} var countLock = sync.Mutex{}
var firstDataLock = sync.Mutex{}
type MiscService struct { type MiscService struct {
config *config.Config config *config.Config
userService IUserService userService IUserService
heartbeatService IHeartbeatService
summaryService ISummaryService summaryService ISummaryService
keyValueService IKeyValueService keyValueService IKeyValueService
queueDefault *artifex.Dispatcher queueDefault *artifex.Dispatcher
queueWorkers *artifex.Dispatcher queueWorkers *artifex.Dispatcher
} }
func NewMiscService(userService IUserService, summaryService ISummaryService, keyValueService IKeyValueService) *MiscService { func NewMiscService(userService IUserService, heartbeatService IHeartbeatService, summaryService ISummaryService, keyValueService IKeyValueService) *MiscService {
return &MiscService{ return &MiscService{
config: config.Get(), config: config.Get(),
userService: userService, userService: userService,
heartbeatService: heartbeatService,
summaryService: summaryService, summaryService: summaryService,
keyValueService: keyValueService, keyValueService: keyValueService,
queueDefault: config.GetDefaultQueue(), queueDefault: config.GetDefaultQueue(),
@ -39,11 +44,28 @@ func NewMiscService(userService IUserService, summaryService ISummaryService, ke
} }
} }
func (srv *MiscService) ScheduleCountTotalTime() { func (srv *MiscService) Schedule() {
logbuch.Info("scheduling total time counting") logbuch.Info("scheduling total time counting")
if _, err := srv.queueDefault.DispatchEvery(srv.CountTotalTime, countUsersEvery); err != nil { if _, err := srv.queueDefault.DispatchEvery(srv.CountTotalTime, countUsersEvery); err != nil {
config.Log().Error("failed to schedule user counting jobs, %v", err) config.Log().Error("failed to schedule user counting jobs, %v", err)
} }
logbuch.Info("scheduling first data computing")
if _, err := srv.queueDefault.DispatchEvery(srv.ComputeOldestHeartbeats, computeOldestDataEvery); err != nil {
config.Log().Error("failed to schedule first data computing jobs, %v", err)
}
// run once initially for a fresh instance
if !srv.existsUsersTotalTime() {
if err := srv.queueDefault.Dispatch(srv.CountTotalTime); err != nil {
config.Log().Error("failed to dispatch user counting jobs, %v", err)
}
}
if !srv.existsUsersFirstData() {
if err := srv.queueDefault.Dispatch(srv.ComputeOldestHeartbeats); err != nil {
config.Log().Error("failed to dispatch first data computing jobs, %v", err)
}
}
} }
func (srv *MiscService) CountTotalTime() { func (srv *MiscService) CountTotalTime() {
@ -95,6 +117,39 @@ func (srv *MiscService) CountTotalTime() {
}(&pendingJobs) }(&pendingJobs)
} }
func (srv *MiscService) ComputeOldestHeartbeats() {
logbuch.Info("computing users' first data")
if err := srv.queueWorkers.Dispatch(func() {
if ok := firstDataLock.TryLock(); !ok {
config.Log().Warn("couldn't acquire lock for computing users' first data, job is still pending")
return
}
defer firstDataLock.Unlock()
results, err := srv.heartbeatService.GetFirstByUsers()
if err != nil {
config.Log().Error("failed to compute users' first data, %v", err)
}
for _, entry := range results {
if entry.Time.T().IsZero() {
continue
}
kvKey := fmt.Sprintf("%s_%s", config.KeyFirstHeartbeat, entry.User)
if err := srv.keyValueService.PutString(&models.KeyStringValue{
Key: kvKey,
Value: entry.Time.T().Format(time.RFC822Z),
}); err != nil {
config.Log().Error("failed to save user's first heartbeat time: %v", err)
}
}
}); err != nil {
config.Log().Error("failed to enqueue computing first data for user, %v", err)
}
}
func (srv *MiscService) countUserTotalTime(userId string) time.Duration { func (srv *MiscService) countUserTotalTime(userId string) time.Duration {
result, err := srv.summaryService.Aliased(time.Time{}, time.Now(), &models.User{ID: userId}, srv.summaryService.Retrieve, nil, false) result, err := srv.summaryService.Aliased(time.Time{}, time.Now(), &models.User{ID: userId}, srv.summaryService.Retrieve, nil, false)
if err != nil { if err != nil {
@ -103,3 +158,13 @@ func (srv *MiscService) countUserTotalTime(userId string) time.Duration {
} }
return result.TotalTime() return result.TotalTime()
} }
func (srv *MiscService) existsUsersTotalTime() bool {
results, _ := srv.keyValueService.GetByPrefix(config.KeyLatestTotalTime)
return len(results) > 0
}
func (srv *MiscService) existsUsersFirstData() bool {
results, _ := srv.keyValueService.GetByPrefix(config.KeyFirstHeartbeat)
return len(results) > 0
}

View File

@ -12,7 +12,7 @@ type IAggregationService interface {
} }
type IMiscService interface { type IMiscService interface {
ScheduleCountTotalTime() Schedule()
CountTotalTime() CountTotalTime()
} }
@ -52,6 +52,7 @@ type IDiagnosticsService interface {
type IKeyValueService interface { type IKeyValueService interface {
GetString(string) (*models.KeyStringValue, error) GetString(string) (*models.KeyStringValue, error)
MustGetString(string) *models.KeyStringValue MustGetString(string) *models.KeyStringValue
GetByPrefix(string) ([]*models.KeyStringValue, error)
PutString(*models.KeyStringValue) error PutString(*models.KeyStringValue) error
DeleteString(string) error DeleteString(string) error
} }

View File

@ -701,10 +701,12 @@
<br> <br>
{{ end }} {{ end }}
{{ if not .UserFirstData.IsZero }}
<span class="block text-sm text-gray-600"> <span class="block text-sm text-gray-600">
Your currently oldest data point is from <span class="text-gray-300 font-semibold">2022-01-01</span>. Your currently oldest data point is from <span class="text-gray-300 font-semibold">{{ .UserFirstData | datetime }}</span>.
</span> </span>
<br> <br>
{{ end }}
<span class="font-semibold text-gray-300">Subscription status:</span> <span class="font-semibold text-gray-300">Subscription status:</span>
<span class="text-gray-600 ml-1 text-sm"> <span class="text-gray-600 ml-1 text-sm">