fix: prevent config creation hang on pricelist sync

SyncPricelistsIfNeeded was called synchronously in Create(), blocking
the HTTP response for several seconds while pricelist data was fetched.
Users clicking multiple times caused 6+ duplicate configurations.

- Run SyncPricelistsIfNeeded in a goroutine so Create() returns immediately
- Add TryLock mutex to SyncPricelistsIfNeeded to skip concurrent calls

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Mikhail Chusavitin
2026-03-30 12:34:57 +03:00
parent da5414c708
commit 7ae804d2d3
2 changed files with 19 additions and 9 deletions

View File

@@ -49,11 +49,13 @@ func NewLocalConfigurationService(
// Create creates a new configuration in local SQLite and queues it for sync
func (s *LocalConfigurationService) Create(ownerUsername string, req *CreateConfigRequest) (*models.Configuration, error) {
// If online, check for new pricelists first
// If online, trigger pricelist sync in the background — do not block config creation
if s.isOnline() {
if err := s.syncService.SyncPricelistsIfNeeded(); err != nil {
// Log but don't fail - we can still use local pricelists
}
go func() {
if err := s.syncService.SyncPricelistsIfNeeded(); err != nil {
// Log but don't fail - we can still use local pricelists
}
}()
}
projectUUID, err := s.resolveProjectUUID(ownerUsername, req.ProjectUUID)

View File

@@ -7,6 +7,7 @@ import (
"log/slog"
"sort"
"strings"
"sync"
"time"
"git.mchus.pro/mchus/quoteforge/internal/appmeta"
@@ -22,9 +23,10 @@ var ErrOffline = errors.New("database is offline")
// Service handles synchronization between MariaDB and local SQLite
type Service struct {
connMgr *db.ConnectionManager
localDB *localdb.LocalDB
directDB *gorm.DB
connMgr *db.ConnectionManager
localDB *localdb.LocalDB
directDB *gorm.DB
pricelistMu sync.Mutex // prevents concurrent pricelist syncs
}
// NewService creates a new sync service
@@ -939,9 +941,15 @@ func (s *Service) GetPricelistForOffline(serverPricelistID uint) (*localdb.Local
return localPL, nil
}
// SyncPricelistsIfNeeded checks for new pricelists and syncs if needed
// This should be called before creating a new configuration when online
// SyncPricelistsIfNeeded checks for new pricelists and syncs if needed.
// If a sync is already in progress, returns immediately without blocking.
func (s *Service) SyncPricelistsIfNeeded() error {
if !s.pricelistMu.TryLock() {
slog.Debug("pricelist sync already in progress, skipping")
return nil
}
defer s.pricelistMu.Unlock()
needSync, err := s.NeedSync()
if err != nil {
slog.Warn("failed to check if sync needed", "error", err)