Make sync status non-blocking

This commit is contained in:
Mikhail Chusavitin
2026-03-17 18:34:28 +03:00
parent a054fc7564
commit b0a106415f
4 changed files with 78 additions and 78 deletions

View File

@@ -238,6 +238,22 @@ func (cm *ConnectionManager) Disconnect() {
cm.lastError = nil
}
// MarkOffline closes the current connection and preserves the last observed error.
func (cm *ConnectionManager) MarkOffline(err error) {
cm.mu.Lock()
defer cm.mu.Unlock()
if cm.db != nil {
sqlDB, dbErr := cm.db.DB()
if dbErr == nil {
sqlDB.Close()
}
}
cm.db = nil
cm.lastError = err
cm.lastCheck = time.Now()
}
// GetLastError returns the last connection error (thread-safe)
func (cm *ConnectionManager) GetLastError() error {
cm.mu.RLock()

View File

@@ -78,41 +78,18 @@ type SyncReadinessResponse struct {
// GetStatus returns current sync status
// GET /api/sync/status
func (h *SyncHandler) GetStatus(c *gin.Context) {
// Check online status by pinging MariaDB
isOnline := h.checkOnline()
// Get sync times
connStatus := h.connMgr.GetStatus()
isOnline := connStatus.IsConnected && strings.TrimSpace(connStatus.LastError) == ""
lastComponentSync := h.localDB.GetComponentSyncTime()
lastPricelistSync := h.localDB.GetLastSyncTime()
// Get counts
componentsCount := h.localDB.CountLocalComponents()
pricelistsCount := h.localDB.CountLocalPricelists()
// Get server pricelist count if online
serverPricelists := 0
needPricelistSync := false
lastPricelistAttemptAt := h.localDB.GetLastPricelistSyncAttemptAt()
lastPricelistSyncStatus := h.localDB.GetLastPricelistSyncStatus()
lastPricelistSyncError := h.localDB.GetLastPricelistSyncError()
hasIncompleteServerSync := false
knownServerChangesMissing := false
if isOnline {
status, err := h.syncService.GetStatus()
if err == nil {
serverPricelists = status.ServerPricelists
needPricelistSync = status.NeedsSync
lastPricelistAttemptAt = status.LastAttemptAt
lastPricelistSyncStatus = status.LastSyncStatus
lastPricelistSyncError = status.LastSyncError
hasIncompleteServerSync = status.IncompleteServerSync
knownServerChangesMissing = status.KnownServerChangesMiss
}
}
// Check if component sync is needed (older than 24 hours)
hasFailedSync := strings.EqualFold(lastPricelistSyncStatus, "failed")
needComponentSync := h.localDB.NeedComponentSync(24)
readiness := h.getReadinessCached(10 * time.Second)
readiness := h.getReadinessLocal()
c.JSON(http.StatusOK, SyncStatusResponse{
LastComponentSync: lastComponentSync,
@@ -120,14 +97,14 @@ func (h *SyncHandler) GetStatus(c *gin.Context) {
LastPricelistAttemptAt: lastPricelistAttemptAt,
LastPricelistSyncStatus: lastPricelistSyncStatus,
LastPricelistSyncError: lastPricelistSyncError,
HasIncompleteServerSync: hasIncompleteServerSync,
KnownServerChangesMiss: knownServerChangesMissing,
HasIncompleteServerSync: hasFailedSync,
KnownServerChangesMiss: hasFailedSync,
IsOnline: isOnline,
ComponentsCount: componentsCount,
PricelistsCount: pricelistsCount,
ServerPricelists: serverPricelists,
ServerPricelists: 0,
NeedComponentSync: needComponentSync,
NeedPricelistSync: needPricelistSync,
NeedPricelistSync: lastPricelistSync == nil || hasFailedSync,
Readiness: readiness,
})
}
@@ -537,8 +514,8 @@ type SyncError struct {
// GetInfo returns sync information for modal
// GET /api/sync/info
func (h *SyncHandler) GetInfo(c *gin.Context) {
// Check online status by pinging MariaDB
isOnline := h.checkOnline()
connStatus := h.connMgr.GetStatus()
isOnline := connStatus.IsConnected && strings.TrimSpace(connStatus.LastError) == ""
// Get DB connection info
var dbHost, dbUser, dbName string
@@ -553,8 +530,9 @@ func (h *SyncHandler) GetInfo(c *gin.Context) {
lastPricelistAttemptAt := h.localDB.GetLastPricelistSyncAttemptAt()
lastPricelistSyncStatus := h.localDB.GetLastPricelistSyncStatus()
lastPricelistSyncError := h.localDB.GetLastPricelistSyncError()
needPricelistSync := false
hasIncompleteServerSync := false
hasFailedSync := strings.EqualFold(lastPricelistSyncStatus, "failed")
needPricelistSync := lastPricelistSync == nil || hasFailedSync
hasIncompleteServerSync := hasFailedSync
// Get local counts
configCount := h.localDB.CountConfigurations()
@@ -587,16 +565,7 @@ func (h *SyncHandler) GetInfo(c *gin.Context) {
syncErrors = syncErrors[:10]
}
readiness := h.getReadinessCached(10 * time.Second)
if isOnline {
if status, err := h.syncService.GetStatus(); err == nil {
lastPricelistAttemptAt = status.LastAttemptAt
lastPricelistSyncStatus = status.LastSyncStatus
lastPricelistSyncError = status.LastSyncError
needPricelistSync = status.NeedsSync
hasIncompleteServerSync = status.IncompleteServerSync
}
}
readiness := h.getReadinessLocal()
c.JSON(http.StatusOK, SyncInfoResponse{
DBHost: dbHost,
@@ -671,19 +640,12 @@ func (h *SyncHandler) SyncStatusPartial(c *gin.Context) {
// Get pending count
pendingCount := h.localDB.GetPendingCount()
readiness := h.getReadinessCached(10 * time.Second)
readiness := h.getReadinessLocal()
isBlocked := readiness != nil && readiness.Blocked
lastPricelistSyncStatus := h.localDB.GetLastPricelistSyncStatus()
lastPricelistSyncError := h.localDB.GetLastPricelistSyncError()
hasIncompleteServerSync := false
if !isOffline {
if status, err := h.syncService.GetStatus(); err == nil {
lastPricelistSyncStatus = status.LastSyncStatus
lastPricelistSyncError = status.LastSyncError
hasIncompleteServerSync = status.IncompleteServerSync
}
}
hasFailedSync := strings.EqualFold(lastPricelistSyncStatus, "failed")
hasIncompleteServerSync := hasFailedSync
slog.Debug("rendering sync status", "is_offline", isOffline, "pending_count", pendingCount, "sync_blocked", isBlocked)
@@ -721,20 +683,29 @@ func (h *SyncHandler) SyncStatusPartial(c *gin.Context) {
}
}
func (h *SyncHandler) getReadinessCached(maxAge time.Duration) *sync.SyncReadiness {
func (h *SyncHandler) getReadinessLocal() *sync.SyncReadiness {
h.readinessMu.Lock()
if h.readinessCached != nil && time.Since(h.readinessCachedAt) < maxAge {
if h.readinessCached != nil && time.Since(h.readinessCachedAt) < 10*time.Second {
cached := *h.readinessCached
h.readinessMu.Unlock()
return &cached
}
h.readinessMu.Unlock()
readiness, err := h.syncService.GetReadiness()
if err != nil && readiness == nil {
state, err := h.localDB.GetSyncGuardState()
if err != nil || state == nil {
return nil
}
readiness := &sync.SyncReadiness{
Status: state.Status,
Blocked: state.Status == sync.ReadinessBlocked,
ReasonCode: state.ReasonCode,
ReasonText: state.ReasonText,
RequiredMinAppVersion: state.RequiredMinAppVersion,
LastCheckedAt: state.LastCheckedAt,
}
h.readinessMu.Lock()
h.readinessCached = readiness
h.readinessCachedAt = time.Now()

View File

@@ -248,35 +248,20 @@ func (s *Service) GetStatus() (*SyncStatus, error) {
lastAttempt := s.localDB.GetLastPricelistSyncAttemptAt()
lastSyncStatus := s.localDB.GetLastPricelistSyncStatus()
lastSyncError := s.localDB.GetLastPricelistSyncError()
// Count server pricelists (only if already connected, don't reconnect)
serverCount := 0
connStatus := s.getConnectionStatus()
if connStatus.IsConnected {
if mariaDB, err := s.getDB(); err == nil && mariaDB != nil {
pricelistRepo := repository.NewPricelistRepository(mariaDB)
activeCount, err := pricelistRepo.CountActive()
if err == nil {
serverCount = int(activeCount)
}
}
}
// Count local pricelists
localCount := s.localDB.CountLocalPricelists()
needsSync, _ := s.NeedSync()
hasFailedSync := strings.EqualFold(lastSyncStatus, "failed")
needsSync := lastSync == nil || hasFailedSync
return &SyncStatus{
LastSyncAt: lastSync,
LastAttemptAt: lastAttempt,
LastSyncStatus: lastSyncStatus,
LastSyncError: lastSyncError,
ServerPricelists: serverCount,
ServerPricelists: 0,
LocalPricelists: int(localCount),
NeedsSync: needsSync,
IncompleteServerSync: needsSync && strings.EqualFold(lastSyncStatus, "failed"),
KnownServerChangesMiss: needsSync,
IncompleteServerSync: hasFailedSync,
KnownServerChangesMiss: hasFailedSync,
}, nil
}
@@ -447,11 +432,29 @@ func (s *Service) recordPricelistSyncFailure(syncErr error) {
if s.localDB == nil || syncErr == nil {
return
}
s.markConnectionBroken(syncErr)
if err := s.localDB.SetPricelistSyncResult("failed", syncErr.Error(), time.Now()); err != nil {
slog.Warn("failed to persist pricelist sync failure state", "error", err)
}
}
func (s *Service) markConnectionBroken(err error) {
if err == nil || s.connMgr == nil {
return
}
msg := strings.ToLower(err.Error())
switch {
case strings.Contains(msg, "i/o timeout"),
strings.Contains(msg, "invalid connection"),
strings.Contains(msg, "bad connection"),
strings.Contains(msg, "connection reset"),
strings.Contains(msg, "broken pipe"),
strings.Contains(msg, "unexpected eof"):
s.connMgr.MarkOffline(err)
}
}
func (s *Service) syncNewPricelistSnapshot(localPL *localdb.LocalPricelist) (int, error) {
if localPL == nil {
return 0, fmt.Errorf("local pricelist is nil")
@@ -990,6 +993,7 @@ func (s *Service) PushPendingChanges() (int, error) {
for _, change := range sortedChanges {
err := s.pushSingleChange(&change)
if err != nil {
s.markConnectionBroken(err)
slog.Warn("failed to push change", "id", change.ID, "type", change.EntityType, "operation", change.Operation, "error", err)
// Increment attempts
s.localDB.IncrementPendingChangeAttempts(change.ID, err.Error())