Renamed module path git.mchus.pro/mchus/quoteforge → git.mchus.pro/mchus/priceforge, renamed package quoteforge → priceforge, moved binary from cmd/qfs to cmd/pfs.
635 lines
19 KiB
Go
635 lines
19 KiB
Go
package handlers
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"html/template"
|
|
"log/slog"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
stdsync "sync"
|
|
"time"
|
|
|
|
qfassets "git.mchus.pro/mchus/priceforge"
|
|
"git.mchus.pro/mchus/priceforge/internal/db"
|
|
"git.mchus.pro/mchus/priceforge/internal/localdb"
|
|
"git.mchus.pro/mchus/priceforge/internal/services/sync"
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
// SyncHandler handles sync API endpoints
|
|
type SyncHandler struct {
|
|
localDB *localdb.LocalDB
|
|
syncService *sync.Service
|
|
connMgr *db.ConnectionManager
|
|
autoSyncInterval time.Duration
|
|
onlineGraceFactor float64
|
|
tmpl *template.Template
|
|
readinessMu stdsync.Mutex
|
|
readinessCached *sync.SyncReadiness
|
|
readinessCachedAt time.Time
|
|
}
|
|
|
|
// NewSyncHandler creates a new sync handler
|
|
func NewSyncHandler(localDB *localdb.LocalDB, syncService *sync.Service, connMgr *db.ConnectionManager, templatesPath string, autoSyncInterval time.Duration) (*SyncHandler, error) {
|
|
// Load sync_status partial template
|
|
partialPath := filepath.Join(templatesPath, "partials", "sync_status.html")
|
|
var tmpl *template.Template
|
|
var err error
|
|
if stat, statErr := os.Stat(templatesPath); statErr == nil && stat.IsDir() {
|
|
tmpl, err = template.ParseFiles(partialPath)
|
|
} else {
|
|
tmpl, err = template.ParseFS(qfassets.TemplatesFS, "web/templates/partials/sync_status.html")
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &SyncHandler{
|
|
localDB: localDB,
|
|
syncService: syncService,
|
|
connMgr: connMgr,
|
|
autoSyncInterval: autoSyncInterval,
|
|
onlineGraceFactor: 1.10,
|
|
tmpl: tmpl,
|
|
}, nil
|
|
}
|
|
|
|
// SyncStatusResponse represents the sync status
|
|
type SyncStatusResponse struct {
|
|
LastComponentSync *time.Time `json:"last_component_sync"`
|
|
LastPricelistSync *time.Time `json:"last_pricelist_sync"`
|
|
IsOnline bool `json:"is_online"`
|
|
ComponentsCount int64 `json:"components_count"`
|
|
PricelistsCount int64 `json:"pricelists_count"`
|
|
ServerPricelists int `json:"server_pricelists"`
|
|
NeedComponentSync bool `json:"need_component_sync"`
|
|
NeedPricelistSync bool `json:"need_pricelist_sync"`
|
|
Readiness *sync.SyncReadiness `json:"readiness,omitempty"`
|
|
}
|
|
|
|
type SyncReadinessResponse struct {
|
|
Status string `json:"status"`
|
|
Blocked bool `json:"blocked"`
|
|
ReasonCode string `json:"reason_code,omitempty"`
|
|
ReasonText string `json:"reason_text,omitempty"`
|
|
RequiredMinAppVersion *string `json:"required_min_app_version,omitempty"`
|
|
LastCheckedAt *time.Time `json:"last_checked_at,omitempty"`
|
|
}
|
|
|
|
// 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
|
|
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
|
|
if isOnline {
|
|
status, err := h.syncService.GetStatus()
|
|
if err == nil {
|
|
serverPricelists = status.ServerPricelists
|
|
needPricelistSync = status.NeedsSync
|
|
}
|
|
}
|
|
|
|
// Check if component sync is needed (older than 24 hours)
|
|
needComponentSync := h.localDB.NeedComponentSync(24)
|
|
readiness := h.getReadinessCached(10 * time.Second)
|
|
|
|
c.JSON(http.StatusOK, SyncStatusResponse{
|
|
LastComponentSync: lastComponentSync,
|
|
LastPricelistSync: lastPricelistSync,
|
|
IsOnline: isOnline,
|
|
ComponentsCount: componentsCount,
|
|
PricelistsCount: pricelistsCount,
|
|
ServerPricelists: serverPricelists,
|
|
NeedComponentSync: needComponentSync,
|
|
NeedPricelistSync: needPricelistSync,
|
|
Readiness: readiness,
|
|
})
|
|
}
|
|
|
|
// GetReadiness returns sync readiness guard status.
|
|
// GET /api/sync/readiness
|
|
func (h *SyncHandler) GetReadiness(c *gin.Context) {
|
|
readiness, err := h.syncService.GetReadiness()
|
|
if err != nil && readiness == nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{
|
|
"error": err.Error(),
|
|
})
|
|
return
|
|
}
|
|
if readiness == nil {
|
|
c.JSON(http.StatusOK, SyncReadinessResponse{Status: sync.ReadinessUnknown, Blocked: false})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, SyncReadinessResponse{
|
|
Status: readiness.Status,
|
|
Blocked: readiness.Blocked,
|
|
ReasonCode: readiness.ReasonCode,
|
|
ReasonText: readiness.ReasonText,
|
|
RequiredMinAppVersion: readiness.RequiredMinAppVersion,
|
|
LastCheckedAt: readiness.LastCheckedAt,
|
|
})
|
|
}
|
|
|
|
func (h *SyncHandler) ensureSyncReadiness(c *gin.Context) bool {
|
|
readiness, err := h.syncService.EnsureReadinessForSync()
|
|
if err == nil {
|
|
return true
|
|
}
|
|
|
|
blocked := &sync.SyncBlockedError{}
|
|
if errors.As(err, &blocked) {
|
|
c.JSON(http.StatusLocked, gin.H{
|
|
"success": false,
|
|
"error": blocked.Error(),
|
|
"reason_code": blocked.Readiness.ReasonCode,
|
|
"reason_text": blocked.Readiness.ReasonText,
|
|
"required_min_app_version": blocked.Readiness.RequiredMinAppVersion,
|
|
"status": blocked.Readiness.Status,
|
|
"blocked": true,
|
|
"last_checked_at": blocked.Readiness.LastCheckedAt,
|
|
})
|
|
return false
|
|
}
|
|
|
|
c.JSON(http.StatusInternalServerError, gin.H{
|
|
"success": false,
|
|
"error": err.Error(),
|
|
})
|
|
_ = readiness
|
|
return false
|
|
}
|
|
|
|
// SyncResultResponse represents sync operation result
|
|
type SyncResultResponse struct {
|
|
Success bool `json:"success"`
|
|
Message string `json:"message"`
|
|
Synced int `json:"synced"`
|
|
Duration string `json:"duration"`
|
|
}
|
|
|
|
// SyncComponents syncs components from MariaDB to local SQLite
|
|
// POST /api/sync/components
|
|
func (h *SyncHandler) SyncComponents(c *gin.Context) {
|
|
if !h.ensureSyncReadiness(c) {
|
|
return
|
|
}
|
|
|
|
// Get database connection from ConnectionManager
|
|
mariaDB, err := h.connMgr.GetDB()
|
|
if err != nil {
|
|
c.JSON(http.StatusServiceUnavailable, gin.H{
|
|
"success": false,
|
|
"error": "Database connection failed: " + err.Error(),
|
|
})
|
|
return
|
|
}
|
|
|
|
result, err := h.localDB.SyncComponents(mariaDB)
|
|
if err != nil {
|
|
slog.Error("component sync failed", "error", err)
|
|
c.JSON(http.StatusInternalServerError, gin.H{
|
|
"success": false,
|
|
"error": err.Error(),
|
|
})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, SyncResultResponse{
|
|
Success: true,
|
|
Message: "Components synced successfully",
|
|
Synced: result.TotalSynced,
|
|
Duration: result.Duration.String(),
|
|
})
|
|
}
|
|
|
|
// SyncPricelists syncs pricelists from MariaDB to local SQLite
|
|
// POST /api/sync/pricelists
|
|
func (h *SyncHandler) SyncPricelists(c *gin.Context) {
|
|
if !h.ensureSyncReadiness(c) {
|
|
return
|
|
}
|
|
|
|
startTime := time.Now()
|
|
synced, err := h.syncService.SyncPricelists()
|
|
if err != nil {
|
|
slog.Error("pricelist sync failed", "error", err)
|
|
c.JSON(http.StatusInternalServerError, gin.H{
|
|
"success": false,
|
|
"error": err.Error(),
|
|
})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, SyncResultResponse{
|
|
Success: true,
|
|
Message: "Pricelists synced successfully",
|
|
Synced: synced,
|
|
Duration: time.Since(startTime).String(),
|
|
})
|
|
h.syncService.RecordSyncHeartbeat()
|
|
}
|
|
|
|
// SyncAllResponse represents result of full sync
|
|
type SyncAllResponse struct {
|
|
Success bool `json:"success"`
|
|
Message string `json:"message"`
|
|
PendingPushed int `json:"pending_pushed"`
|
|
ComponentsSynced int `json:"components_synced"`
|
|
PricelistsSynced int `json:"pricelists_synced"`
|
|
ProjectsImported int `json:"projects_imported"`
|
|
ProjectsUpdated int `json:"projects_updated"`
|
|
ProjectsSkipped int `json:"projects_skipped"`
|
|
ConfigurationsImported int `json:"configurations_imported"`
|
|
ConfigurationsUpdated int `json:"configurations_updated"`
|
|
ConfigurationsSkipped int `json:"configurations_skipped"`
|
|
Duration string `json:"duration"`
|
|
}
|
|
|
|
// SyncAll performs full bidirectional sync:
|
|
// - push pending local changes (projects/configurations) to server
|
|
// - pull components, pricelists, projects, and configurations from server
|
|
// POST /api/sync/all
|
|
func (h *SyncHandler) SyncAll(c *gin.Context) {
|
|
if !h.ensureSyncReadiness(c) {
|
|
return
|
|
}
|
|
|
|
startTime := time.Now()
|
|
var pendingPushed, componentsSynced, pricelistsSynced int
|
|
|
|
// Push local pending changes first (projects/configurations)
|
|
pendingPushed, err := h.syncService.PushPendingChanges()
|
|
if err != nil {
|
|
slog.Error("pending push failed during full sync", "error", err)
|
|
c.JSON(http.StatusInternalServerError, gin.H{
|
|
"success": false,
|
|
"error": "Pending changes push failed: " + err.Error(),
|
|
})
|
|
return
|
|
}
|
|
|
|
// Sync components
|
|
mariaDB, err := h.connMgr.GetDB()
|
|
if err != nil {
|
|
c.JSON(http.StatusServiceUnavailable, gin.H{
|
|
"success": false,
|
|
"error": "Database connection failed: " + err.Error(),
|
|
})
|
|
return
|
|
}
|
|
|
|
compResult, err := h.localDB.SyncComponents(mariaDB)
|
|
if err != nil {
|
|
slog.Error("component sync failed during full sync", "error", err)
|
|
c.JSON(http.StatusInternalServerError, gin.H{
|
|
"success": false,
|
|
"error": "Component sync failed: " + err.Error(),
|
|
})
|
|
return
|
|
}
|
|
componentsSynced = compResult.TotalSynced
|
|
|
|
// Sync pricelists
|
|
pricelistsSynced, err = h.syncService.SyncPricelists()
|
|
if err != nil {
|
|
slog.Error("pricelist sync failed during full sync", "error", err)
|
|
c.JSON(http.StatusInternalServerError, gin.H{
|
|
"success": false,
|
|
"error": "Pricelist sync failed: " + err.Error(),
|
|
"pending_pushed": pendingPushed,
|
|
"components_synced": componentsSynced,
|
|
})
|
|
return
|
|
}
|
|
|
|
projectsResult, err := h.syncService.ImportProjectsToLocal()
|
|
if err != nil {
|
|
slog.Error("project import failed during full sync", "error", err)
|
|
c.JSON(http.StatusInternalServerError, gin.H{
|
|
"success": false,
|
|
"error": "Project import failed: " + err.Error(),
|
|
"pending_pushed": pendingPushed,
|
|
"components_synced": componentsSynced,
|
|
"pricelists_synced": pricelistsSynced,
|
|
})
|
|
return
|
|
}
|
|
|
|
configsResult, err := h.syncService.ImportConfigurationsToLocal()
|
|
if err != nil {
|
|
slog.Error("configuration import failed during full sync", "error", err)
|
|
c.JSON(http.StatusInternalServerError, gin.H{
|
|
"success": false,
|
|
"error": "Configuration import failed: " + err.Error(),
|
|
"pending_pushed": pendingPushed,
|
|
"components_synced": componentsSynced,
|
|
"pricelists_synced": pricelistsSynced,
|
|
"projects_imported": projectsResult.Imported,
|
|
"projects_updated": projectsResult.Updated,
|
|
"projects_skipped": projectsResult.Skipped,
|
|
})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, SyncAllResponse{
|
|
Success: true,
|
|
Message: "Full sync completed successfully",
|
|
PendingPushed: pendingPushed,
|
|
ComponentsSynced: componentsSynced,
|
|
PricelistsSynced: pricelistsSynced,
|
|
ProjectsImported: projectsResult.Imported,
|
|
ProjectsUpdated: projectsResult.Updated,
|
|
ProjectsSkipped: projectsResult.Skipped,
|
|
ConfigurationsImported: configsResult.Imported,
|
|
ConfigurationsUpdated: configsResult.Updated,
|
|
ConfigurationsSkipped: configsResult.Skipped,
|
|
Duration: time.Since(startTime).String(),
|
|
})
|
|
h.syncService.RecordSyncHeartbeat()
|
|
}
|
|
|
|
// checkOnline checks if MariaDB is accessible
|
|
func (h *SyncHandler) checkOnline() bool {
|
|
return h.connMgr.IsOnline()
|
|
}
|
|
|
|
// PushPendingChanges pushes all pending changes to the server
|
|
// POST /api/sync/push
|
|
func (h *SyncHandler) PushPendingChanges(c *gin.Context) {
|
|
if !h.ensureSyncReadiness(c) {
|
|
return
|
|
}
|
|
|
|
startTime := time.Now()
|
|
pushed, err := h.syncService.PushPendingChanges()
|
|
if err != nil {
|
|
slog.Error("push pending changes failed", "error", err)
|
|
c.JSON(http.StatusInternalServerError, gin.H{
|
|
"success": false,
|
|
"error": err.Error(),
|
|
})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, SyncResultResponse{
|
|
Success: true,
|
|
Message: "Pending changes pushed successfully",
|
|
Synced: pushed,
|
|
Duration: time.Since(startTime).String(),
|
|
})
|
|
h.syncService.RecordSyncHeartbeat()
|
|
}
|
|
|
|
// GetPendingCount returns the number of pending changes
|
|
// GET /api/sync/pending/count
|
|
func (h *SyncHandler) GetPendingCount(c *gin.Context) {
|
|
count := h.localDB.GetPendingCount()
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"count": count,
|
|
})
|
|
}
|
|
|
|
// GetPendingChanges returns all pending changes
|
|
// GET /api/sync/pending
|
|
func (h *SyncHandler) GetPendingChanges(c *gin.Context) {
|
|
changes, err := h.localDB.GetPendingChanges()
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{
|
|
"error": err.Error(),
|
|
})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"changes": changes,
|
|
})
|
|
}
|
|
|
|
// SyncInfoResponse represents sync information for the modal
|
|
type SyncInfoResponse struct {
|
|
// Connection
|
|
DBHost string `json:"db_host"`
|
|
DBUser string `json:"db_user"`
|
|
DBName string `json:"db_name"`
|
|
|
|
// Status
|
|
IsOnline bool `json:"is_online"`
|
|
LastSyncAt *time.Time `json:"last_sync_at"`
|
|
|
|
// Statistics
|
|
LotCount int64 `json:"lot_count"`
|
|
LotLogCount int64 `json:"lot_log_count"`
|
|
ConfigCount int64 `json:"config_count"`
|
|
ProjectCount int64 `json:"project_count"`
|
|
|
|
// Pending changes
|
|
PendingChanges []localdb.PendingChange `json:"pending_changes"`
|
|
|
|
// Errors
|
|
ErrorCount int `json:"error_count"`
|
|
Errors []SyncError `json:"errors,omitempty"`
|
|
|
|
// Readiness guard
|
|
Readiness *sync.SyncReadiness `json:"readiness,omitempty"`
|
|
}
|
|
|
|
type SyncUsersStatusResponse struct {
|
|
IsOnline bool `json:"is_online"`
|
|
AutoSyncIntervalSeconds int64 `json:"auto_sync_interval_seconds"`
|
|
OnlineThresholdSeconds int64 `json:"online_threshold_seconds"`
|
|
GeneratedAt time.Time `json:"generated_at"`
|
|
Users []sync.UserSyncStatus `json:"users"`
|
|
}
|
|
|
|
// SyncError represents a sync error
|
|
type SyncError struct {
|
|
Timestamp time.Time `json:"timestamp"`
|
|
Message string `json:"message"`
|
|
}
|
|
|
|
// 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()
|
|
|
|
// Get DB connection info
|
|
var dbHost, dbUser, dbName string
|
|
if settings, err := h.localDB.GetSettings(); err == nil {
|
|
dbHost = settings.Host + ":" + fmt.Sprintf("%d", settings.Port)
|
|
dbUser = settings.User
|
|
dbName = settings.Database
|
|
}
|
|
|
|
// Get sync times
|
|
lastPricelistSync := h.localDB.GetLastSyncTime()
|
|
|
|
// Get MariaDB counts (if online)
|
|
var lotCount, lotLogCount int64
|
|
if isOnline {
|
|
if mariaDB, err := h.connMgr.GetDB(); err == nil {
|
|
mariaDB.Table("lot").Count(&lotCount)
|
|
mariaDB.Table("lot_log").Count(&lotLogCount)
|
|
}
|
|
}
|
|
|
|
// Get local counts
|
|
configCount := h.localDB.CountConfigurations()
|
|
projectCount := h.localDB.CountProjects()
|
|
|
|
// Get error count (only changes with LastError != "")
|
|
errorCount := int(h.localDB.CountErroredChanges())
|
|
|
|
// Get pending changes
|
|
changes, err := h.localDB.GetPendingChanges()
|
|
if err != nil {
|
|
slog.Error("failed to get pending changes for sync info", "error", err)
|
|
changes = []localdb.PendingChange{}
|
|
}
|
|
|
|
var syncErrors []SyncError
|
|
for _, change := range changes {
|
|
if change.LastError != "" {
|
|
syncErrors = append(syncErrors, SyncError{
|
|
Timestamp: change.CreatedAt,
|
|
Message: change.LastError,
|
|
})
|
|
}
|
|
}
|
|
|
|
// Limit to last 10 errors
|
|
if len(syncErrors) > 10 {
|
|
syncErrors = syncErrors[:10]
|
|
}
|
|
|
|
readiness := h.getReadinessCached(10 * time.Second)
|
|
|
|
c.JSON(http.StatusOK, SyncInfoResponse{
|
|
DBHost: dbHost,
|
|
DBUser: dbUser,
|
|
DBName: dbName,
|
|
IsOnline: isOnline,
|
|
LastSyncAt: lastPricelistSync,
|
|
LotCount: lotCount,
|
|
LotLogCount: lotLogCount,
|
|
ConfigCount: configCount,
|
|
ProjectCount: projectCount,
|
|
PendingChanges: changes,
|
|
ErrorCount: errorCount,
|
|
Errors: syncErrors,
|
|
Readiness: readiness,
|
|
})
|
|
}
|
|
|
|
// GetUsersStatus returns last sync timestamps for users with sync heartbeats.
|
|
// GET /api/sync/users-status
|
|
func (h *SyncHandler) GetUsersStatus(c *gin.Context) {
|
|
threshold := time.Duration(float64(h.autoSyncInterval) * h.onlineGraceFactor)
|
|
isOnline := h.checkOnline()
|
|
|
|
if !isOnline {
|
|
c.JSON(http.StatusOK, SyncUsersStatusResponse{
|
|
IsOnline: false,
|
|
AutoSyncIntervalSeconds: int64(h.autoSyncInterval.Seconds()),
|
|
OnlineThresholdSeconds: int64(threshold.Seconds()),
|
|
GeneratedAt: time.Now().UTC(),
|
|
Users: []sync.UserSyncStatus{},
|
|
})
|
|
return
|
|
}
|
|
|
|
// Keep current client heartbeat fresh so app version is available in the table.
|
|
h.syncService.RecordSyncHeartbeat()
|
|
|
|
users, err := h.syncService.ListUserSyncStatuses(threshold)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{
|
|
"error": err.Error(),
|
|
})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, SyncUsersStatusResponse{
|
|
IsOnline: true,
|
|
AutoSyncIntervalSeconds: int64(h.autoSyncInterval.Seconds()),
|
|
OnlineThresholdSeconds: int64(threshold.Seconds()),
|
|
GeneratedAt: time.Now().UTC(),
|
|
Users: users,
|
|
})
|
|
}
|
|
|
|
// SyncStatusPartial renders the sync status partial for htmx
|
|
// GET /partials/sync-status
|
|
func (h *SyncHandler) SyncStatusPartial(c *gin.Context) {
|
|
// Check online status from middleware
|
|
isOfflineValue, exists := c.Get("is_offline")
|
|
isOffline := false
|
|
if exists {
|
|
isOffline = isOfflineValue.(bool)
|
|
} else {
|
|
// Fallback: check directly if middleware didn't set it
|
|
isOffline = !h.checkOnline()
|
|
slog.Warn("is_offline not found in context, checking directly")
|
|
}
|
|
|
|
// Get pending count
|
|
pendingCount := h.localDB.GetPendingCount()
|
|
readiness := h.getReadinessCached(10 * time.Second)
|
|
isBlocked := readiness != nil && readiness.Blocked
|
|
|
|
slog.Debug("rendering sync status", "is_offline", isOffline, "pending_count", pendingCount, "sync_blocked", isBlocked)
|
|
|
|
data := gin.H{
|
|
"IsOffline": isOffline,
|
|
"PendingCount": pendingCount,
|
|
"IsBlocked": isBlocked,
|
|
"BlockedReason": func() string {
|
|
if readiness == nil {
|
|
return ""
|
|
}
|
|
return readiness.ReasonText
|
|
}(),
|
|
}
|
|
|
|
c.Header("Content-Type", "text/html; charset=utf-8")
|
|
if err := h.tmpl.ExecuteTemplate(c.Writer, "sync_status", data); err != nil {
|
|
slog.Error("failed to render sync_status template", "error", err)
|
|
c.String(http.StatusInternalServerError, "Template error: "+err.Error())
|
|
}
|
|
}
|
|
|
|
func (h *SyncHandler) getReadinessCached(maxAge time.Duration) *sync.SyncReadiness {
|
|
h.readinessMu.Lock()
|
|
if h.readinessCached != nil && time.Since(h.readinessCachedAt) < maxAge {
|
|
cached := *h.readinessCached
|
|
h.readinessMu.Unlock()
|
|
return &cached
|
|
}
|
|
h.readinessMu.Unlock()
|
|
|
|
readiness, err := h.syncService.GetReadiness()
|
|
if err != nil && readiness == nil {
|
|
return nil
|
|
}
|
|
|
|
h.readinessMu.Lock()
|
|
h.readinessCached = readiness
|
|
h.readinessCachedAt = time.Now()
|
|
h.readinessMu.Unlock()
|
|
return readiness
|
|
}
|