Skip to content

Commit b27e9bb

Browse files
committedFeb 21, 2025
fix: deadlock caused by not using open transaction during batch insert
fix: regenerate durations upon language mapping update fix: minor ui
1 parent 7b7f5e9 commit b27e9bb

File tree

7 files changed

+1357
-1310
lines changed

7 files changed

+1357
-1310
lines changed
 

‎config/eventbus.go

+13-12
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,19 @@ type ApplicationEvent struct {
88
}
99

1010
const (
11-
TopicUser = "user.*"
12-
TopicHeartbeat = "heartbeat.*"
13-
TopicProjectLabel = "project_label.*"
14-
EventUserUpdate = "user.update"
15-
EventUserDelete = "user.delete"
16-
EventHeartbeatCreate = "heartbeat.create"
17-
EventProjectLabelCreate = "project_label.create"
18-
EventProjectLabelDelete = "project_label.delete"
19-
EventWakatimeFailure = "wakatime.failure"
20-
FieldPayload = "payload"
21-
FieldUser = "user"
22-
FieldUserId = "user.id"
11+
TopicUser = "user.*"
12+
TopicHeartbeat = "heartbeat.*"
13+
TopicProjectLabel = "project_label.*"
14+
EventUserUpdate = "user.update"
15+
EventUserDelete = "user.delete"
16+
EventHeartbeatCreate = "heartbeat.create"
17+
EventProjectLabelCreate = "project_label.create"
18+
EventProjectLabelDelete = "project_label.delete"
19+
EventWakatimeFailure = "wakatime.failure"
20+
EventLanguageMappingsChanged = "language_mappings.changed"
21+
FieldPayload = "payload"
22+
FieldUser = "user"
23+
FieldUserId = "user.id"
2324
)
2425

2526
var eventHub *hub.Hub

‎coverage/coverage.out

+1,307-1,295
Large diffs are not rendered by default.

‎models/user.go

+4
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,10 @@ func (u *User) HeartbeatsTimeout() time.Duration {
144144
return DefaultHeartbeatsTimeout
145145
}
146146

147+
func (u *User) HeartbeatsTimeoutMin() int {
148+
return int(u.HeartbeatsTimeout() / time.Minute)
149+
}
150+
147151
// WakaTimeURL returns the user's effective WakaTime URL, i.e. a custom one (which could also point to another Wakapi instance) or fallback if not specified otherwise.
148152
func (u *User) WakaTimeURL(fallback string) string {
149153
if u.WakatimeApiUrl != "" {

‎repositories/base.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ func InsertBatchChunked[T any](data []T, model T, db *gorm.DB) error {
5151
return db.Transaction(func(tx *gorm.DB) error {
5252
chunks := slice.Chunk[T](data, chunkSize)
5353
for _, chunk := range chunks {
54-
if err := insertBatch[T](chunk, model, db); err != nil {
54+
if err := insertBatch[T](chunk, model, tx); err != nil {
5555
return err
5656
}
5757
}

‎services/duration.go

+17
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,23 @@ func NewDurationService(durationRepository repositories.IDurationRepository, hea
5656
}
5757
}(&sub1)
5858

59+
sub2 := srv.eventBus.Subscribe(0, config.EventLanguageMappingsChanged)
60+
go func(sub *hub.Subscription) {
61+
for m := range sub.Receiver {
62+
userId := m.Fields[config.FieldUserId].(string)
63+
user, err := srv.userService.GetUserById(userId)
64+
if err != nil {
65+
config.Log().Error("user not found for regenerating durations after language mapping change", "user", userId)
66+
continue
67+
}
68+
69+
slog.Info("regenerating durations because language mappings were updated", "user", userId)
70+
srv.queue.Dispatch(func() {
71+
srv.Regenerate(user, true)
72+
})
73+
}
74+
}(&sub2)
75+
5976
return srv
6077
}
6178

‎services/language_mapping.go

+13
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package services
22

33
import (
44
"errors"
5+
"github.com/leandro-lugaresi/hub"
56
"github.com/muety/wakapi/config"
67
"github.com/muety/wakapi/models"
78
"github.com/muety/wakapi/repositories"
@@ -12,12 +13,14 @@ import (
1213
type LanguageMappingService struct {
1314
config *config.Config
1415
cache *cache.Cache
16+
eventBus *hub.Hub
1517
repository repositories.ILanguageMappingRepository
1618
}
1719

1820
func NewLanguageMappingService(languageMappingsRepo repositories.ILanguageMappingRepository) *LanguageMappingService {
1921
return &LanguageMappingService{
2022
config: config.Get(),
23+
eventBus: config.EventBus(),
2124
repository: languageMappingsRepo,
2225
cache: cache.New(24*time.Hour, 24*time.Hour),
2326
}
@@ -60,6 +63,7 @@ func (srv *LanguageMappingService) Create(mapping *models.LanguageMapping) (*mod
6063
}
6164

6265
srv.cache.Delete(result.UserID)
66+
srv.notifyUpdate(mapping)
6367
return result, nil
6468
}
6569

@@ -69,10 +73,19 @@ func (srv *LanguageMappingService) Delete(mapping *models.LanguageMapping) error
6973
}
7074
err := srv.repository.Delete(mapping.ID)
7175
srv.cache.Delete(mapping.UserID)
76+
srv.notifyUpdate(mapping)
7277
return err
7378
}
7479

7580
func (srv *LanguageMappingService) getServerMappings() map[string]string {
7681
// https://dave.cheney.net/2017/04/30/if-a-map-isnt-a-reference-variable-what-is-it
7782
return srv.config.App.GetCustomLanguages()
7883
}
84+
85+
func (srv *LanguageMappingService) notifyUpdate(mapping *models.LanguageMapping) {
86+
name := config.EventLanguageMappingsChanged
87+
srv.eventBus.Publish(hub.Message{
88+
Name: name,
89+
Fields: map[string]interface{}{config.FieldPayload: mapping, config.FieldUserId: mapping.UserID},
90+
})
91+
}

‎views/settings.tpl.html

+2-2
Original file line numberDiff line numberDiff line change
@@ -464,9 +464,9 @@ <h3 class="inline-block font-semibold text-gray-300">Add Rule</h3>
464464
<div class="flex-col w-full md:w-2/3 inline-block space-y-4">
465465
<div class="flex justify-between items-center">
466466
<div class="flex flex-col flex-grow gap-y-1">
467-
<label class="font-semibold text-gray-300" for="heartbeats_timeout">Timeout / offset (seconds)</label>
467+
<label class="font-semibold text-gray-300" for="heartbeats_timeout">Timeout / offset (minutes)</label>
468468
<div class="flex gap-x-2 items-center">
469-
<input class="input-default" type="number" id="heartbeats_timeout" name="heartbeats_timeout" style="max-width: 100px;" placeholder="1" min="1" max="60" step="1" required value="{{ .User.HeartbeatsTimeoutSec }}">
469+
<input class="input-default" type="number" id="heartbeats_timeout" name="heartbeats_timeout" style="max-width: 100px;" placeholder="1" min="1" max="60" step="1" required value="{{ .User.HeartbeatsTimeoutMin }}">
470470
<span class="text-gray-600 text-sm">(min. 1 min, max. 60 min)</span>
471471
</div>
472472
</div>

0 commit comments

Comments
 (0)
Please sign in to comment.