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:
parent
ebcf87ea93
commit
8a94fef06b
@ -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"
|
||||||
|
4
main.go
4
main.go
@ -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()
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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{
|
||||||
|
@ -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 {
|
||||||
|
@ -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,
|
||||||
|
@ -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 {
|
||||||
|
@ -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"
|
||||||
@ -14,36 +15,57 @@ 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
|
||||||
summaryService ISummaryService
|
heartbeatService IHeartbeatService
|
||||||
keyValueService IKeyValueService
|
summaryService ISummaryService
|
||||||
queueDefault *artifex.Dispatcher
|
keyValueService IKeyValueService
|
||||||
queueWorkers *artifex.Dispatcher
|
queueDefault *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,
|
||||||
summaryService: summaryService,
|
heartbeatService: heartbeatService,
|
||||||
keyValueService: keyValueService,
|
summaryService: summaryService,
|
||||||
queueDefault: config.GetDefaultQueue(),
|
keyValueService: keyValueService,
|
||||||
queueWorkers: config.GetQueue(config.QueueProcessing),
|
queueDefault: config.GetDefaultQueue(),
|
||||||
|
queueWorkers: config.GetQueue(config.QueueProcessing),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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">
|
||||||
|
Loading…
Reference in New Issue
Block a user