Initial commit: Open sourcing all of the Maple Open Technologies code.
This commit is contained in:
commit
755d54a99d
2010 changed files with 448675 additions and 0 deletions
|
|
@ -0,0 +1,228 @@
|
|||
package tokenmanager
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/maplefile/client"
|
||||
"codeberg.org/mapleopentech/monorepo/native/desktop/maplefile/internal/service/auth"
|
||||
"codeberg.org/mapleopentech/monorepo/native/desktop/maplefile/internal/usecase/session"
|
||||
)
|
||||
|
||||
// Manager handles automatic token refresh with graceful shutdown
|
||||
type Manager struct {
|
||||
config Config
|
||||
client *client.Client
|
||||
authService *auth.Service
|
||||
getSession *session.GetByIdUseCase
|
||||
logger *zap.Logger
|
||||
|
||||
// Lifecycle management
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
stopCh chan struct{} // Signal to stop
|
||||
stoppedCh chan struct{} // Confirmation of stopped
|
||||
running atomic.Bool // Thread-safe running flag
|
||||
|
||||
// Refresh state management
|
||||
mu sync.Mutex
|
||||
consecutiveFailures int
|
||||
}
|
||||
|
||||
// New creates a new token manager
|
||||
func New(
|
||||
config Config,
|
||||
client *client.Client,
|
||||
authService *auth.Service,
|
||||
getSession *session.GetByIdUseCase,
|
||||
logger *zap.Logger,
|
||||
) *Manager {
|
||||
return &Manager{
|
||||
config: config,
|
||||
client: client,
|
||||
authService: authService,
|
||||
getSession: getSession,
|
||||
logger: logger.Named("token-manager"),
|
||||
}
|
||||
}
|
||||
|
||||
// Start begins the token refresh background process
|
||||
// Safe to call multiple times - will only start once
|
||||
func (m *Manager) Start() {
|
||||
// Only start if not already running
|
||||
if !m.running.CompareAndSwap(false, true) {
|
||||
m.logger.Debug("Token manager already running, skipping start")
|
||||
return
|
||||
}
|
||||
|
||||
m.ctx, m.cancel = context.WithCancel(context.Background())
|
||||
m.stopCh = make(chan struct{})
|
||||
m.stoppedCh = make(chan struct{})
|
||||
m.consecutiveFailures = 0
|
||||
|
||||
m.logger.Info("Token manager starting")
|
||||
go m.refreshLoop()
|
||||
}
|
||||
|
||||
// Stop gracefully stops the token refresh background process
|
||||
// Blocks until stopped or context deadline exceeded
|
||||
func (m *Manager) Stop(ctx context.Context) error {
|
||||
if !m.running.Load() {
|
||||
m.logger.Debug("Token manager not running, nothing to stop")
|
||||
return nil
|
||||
}
|
||||
|
||||
m.logger.Info("Token manager stopping...")
|
||||
|
||||
// Signal stop
|
||||
close(m.stopCh)
|
||||
|
||||
// Wait for goroutine to finish or timeout
|
||||
select {
|
||||
case <-m.stoppedCh:
|
||||
m.logger.Info("Token manager stopped gracefully")
|
||||
return nil
|
||||
case <-ctx.Done():
|
||||
m.logger.Warn("Token manager stop timeout, forcing cancellation")
|
||||
m.cancel()
|
||||
// Wait a bit more for cancellation to take effect
|
||||
select {
|
||||
case <-m.stoppedCh:
|
||||
m.logger.Info("Token manager stopped after forced cancellation")
|
||||
return nil
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
m.logger.Error("Token manager failed to stop cleanly")
|
||||
return ctx.Err()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// IsRunning returns true if the token manager is currently running
|
||||
func (m *Manager) IsRunning() bool {
|
||||
return m.running.Load()
|
||||
}
|
||||
|
||||
// refreshLoop is the background goroutine that checks and refreshes tokens
|
||||
func (m *Manager) refreshLoop() {
|
||||
defer close(m.stoppedCh)
|
||||
defer m.running.Store(false)
|
||||
defer m.logger.Info("Token refresh loop exited")
|
||||
|
||||
ticker := time.NewTicker(m.config.CheckInterval)
|
||||
defer ticker.Stop()
|
||||
|
||||
m.logger.Info("Token refresh loop started",
|
||||
zap.Duration("check_interval", m.config.CheckInterval),
|
||||
zap.Duration("refresh_before_expiry", m.config.RefreshBeforeExpiry))
|
||||
|
||||
// Do initial check immediately
|
||||
if err := m.checkAndRefresh(); err != nil {
|
||||
m.logger.Error("Initial token refresh check failed", zap.Error(err))
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-m.stopCh:
|
||||
m.logger.Info("Token refresh loop received stop signal")
|
||||
return
|
||||
|
||||
case <-m.ctx.Done():
|
||||
m.logger.Info("Token refresh loop context cancelled")
|
||||
return
|
||||
|
||||
case <-ticker.C:
|
||||
if err := m.checkAndRefresh(); err != nil {
|
||||
m.logger.Error("Token refresh check failed", zap.Error(err))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// checkAndRefresh checks if token refresh is needed and performs it
|
||||
func (m *Manager) checkAndRefresh() error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
// Get current session
|
||||
sess, err := m.getSession.Execute()
|
||||
if err != nil {
|
||||
m.logger.Debug("No session found, skipping refresh check", zap.Error(err))
|
||||
return nil // Not an error - user might not be logged in
|
||||
}
|
||||
|
||||
if sess == nil {
|
||||
m.logger.Debug("Session is nil, skipping refresh check")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Check if session is still valid
|
||||
if sess.IsExpired() {
|
||||
m.logger.Warn("Session has expired, forcing logout")
|
||||
return m.forceLogout()
|
||||
}
|
||||
|
||||
// Check if refresh is needed
|
||||
timeUntilExpiry := time.Until(sess.ExpiresAt)
|
||||
if timeUntilExpiry > m.config.RefreshBeforeExpiry {
|
||||
// No refresh needed yet
|
||||
if m.consecutiveFailures > 0 {
|
||||
// Reset failure counter on successful check
|
||||
m.logger.Info("Session valid, resetting failure counter")
|
||||
m.consecutiveFailures = 0
|
||||
}
|
||||
m.logger.Debug("Token refresh not needed yet",
|
||||
zap.Duration("time_until_expiry", timeUntilExpiry))
|
||||
return nil
|
||||
}
|
||||
|
||||
// Refresh needed
|
||||
m.logger.Info("Token refresh needed",
|
||||
zap.Duration("time_until_expiry", timeUntilExpiry))
|
||||
|
||||
// Attempt refresh (with background context, not the manager's context)
|
||||
refreshCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if err := m.client.RefreshToken(refreshCtx); err != nil {
|
||||
m.consecutiveFailures++
|
||||
m.logger.Error("Token refresh failed",
|
||||
zap.Error(err),
|
||||
zap.Int("consecutive_failures", m.consecutiveFailures),
|
||||
zap.Int("max_failures", m.config.MaxConsecutiveFailures))
|
||||
|
||||
if m.consecutiveFailures >= m.config.MaxConsecutiveFailures {
|
||||
m.logger.Error("Max consecutive refresh failures reached, forcing logout")
|
||||
return m.forceLogout()
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// Success - reset failure counter
|
||||
m.consecutiveFailures = 0
|
||||
m.logger.Info("Token refreshed successfully",
|
||||
zap.Duration("time_until_old_expiry", timeUntilExpiry))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// forceLogout forces a logout due to refresh failures
|
||||
func (m *Manager) forceLogout() error {
|
||||
m.logger.Warn("Forcing logout due to token refresh issues")
|
||||
|
||||
// Use background context since manager might be shutting down
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if err := m.authService.Logout(ctx); err != nil {
|
||||
m.logger.Error("Failed to force logout", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
m.logger.Info("Force logout completed successfully")
|
||||
return nil
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue