Files
telecom-platform/apps/carrier-connector/internal/smdp/operations.go
T
nutcas3 a5acfa142f feat: Add SM-DP+ manager with multi-carrier load balancing, health checking, and profile operations
- Add DefaultManagerConfig with 30s health checks, 3 retries, and circuit breaker configuration
- Add CarrierConfig struct with ES2 settings, MCC/MNC, priority, and feature support
- Add ToCarrier method converting CarrierConfig to Carrier with metrics and capabilities
- Add HealthChecker with periodic carrier health monitoring and status evaluation
- Add LoadBalancer with weighted round-robin, least
2026-05-01 20:11:02 +03:00

201 lines
5.6 KiB
Go

package smdp
import (
"context"
"fmt"
"time"
"github.com/nutcas3/telecom-platform/apps/carrier-connector/internal/es2"
"github.com/sirupsen/logrus"
)
func (m *SMDPManager) DownloadProfile(ctx context.Context, req *ProfileRequest) (*ProfileResponse, error) {
startTime := time.Now()
m.logger.WithFields(logrus.Fields{
"eid": req.EID,
"preferred": req.PreferredCarrier,
}).Info("Processing profile download request")
selectedCarrier, err := m.SelectCarrier(ctx, req)
if err != nil {
return &ProfileResponse{
Success: false,
StatusMessage: fmt.Sprintf("Carrier selection failed: %v", err),
ResponseTime: time.Since(startTime),
}, err
}
response, err := m.downloadWithRetry(ctx, req, selectedCarrier, 0)
if err != nil && m.config.EnableFailover {
m.logger.WithError(err).Warn("Primary carrier failed, attempting failover")
return m.handleFailover(ctx, req, selectedCarrier.ID, startTime)
}
response.ResponseTime = time.Since(startTime)
return response, err
}
func (m *SMDPManager) SelectCarrier(ctx context.Context, req *ProfileRequest) (*Carrier, error) {
m.carriersMutex.RLock()
defer m.carriersMutex.RUnlock()
var activeCarriers []*Carrier
for _, carrier := range m.carriers {
if carrier.IsActive && carrier.HealthStatus == CarrierStatusHealthy {
activeCarriers = append(activeCarriers, carrier)
}
}
if len(activeCarriers) == 0 {
return nil, fmt.Errorf("no healthy carriers available")
}
if req.PreferredCarrier != "" {
if carrier, exists := m.carriers[req.PreferredCarrier]; exists &&
carrier.IsActive && carrier.HealthStatus == CarrierStatusHealthy {
return carrier, nil
}
}
if m.config.EnableLoadBalancing {
return m.loadBalancer.SelectCarrier(activeCarriers, req)
}
return m.getHighestPriorityCarrier(activeCarriers), nil
}
func (m *SMDPManager) downloadWithRetry(ctx context.Context, req *ProfileRequest, carrier *Carrier, attempt int) (*ProfileResponse, error) {
m.clientsMutex.RLock()
client, exists := m.es2Clients[carrier.ID]
m.clientsMutex.RUnlock()
if !exists {
return nil, fmt.Errorf("ES2+ client not found for carrier %s", carrier.ID)
}
downloadReq := &es2.DownloadProfileRequest{
EID: req.EID,
ICCID: req.ICCID,
ProfileType: req.ProfileType,
ConfirmationCode: req.ConfirmationCode,
}
startTime := time.Now()
resp, err := client.DownloadProfile(ctx, downloadReq)
responseTime := time.Since(startTime)
m.updateCarrierMetrics(carrier.ID, err == nil, responseTime, err)
if err != nil {
m.logger.WithFields(logrus.Fields{
"carrier_id": carrier.ID,
"attempt": attempt + 1,
"error": err,
}).Error("Profile download failed")
if attempt < m.config.MaxRetries {
time.Sleep(m.config.RetryDelay)
return m.downloadWithRetry(ctx, req, carrier, attempt+1)
}
return &ProfileResponse{
Success: false,
CarrierID: carrier.ID,
StatusMessage: fmt.Sprintf("Download failed after %d attempts: %v", attempt+1, err),
ResponseTime: responseTime,
}, err
}
m.logger.WithFields(logrus.Fields{
"carrier_id": carrier.ID,
"execution_status": resp.ExecutionStatus,
"response_time": responseTime,
}).Info("Profile download successful")
return &ProfileResponse{
Success: true,
CarrierID: carrier.ID,
ExecutionStatus: resp.ExecutionStatus,
StatusMessage: resp.StatusMessage,
ResponseTime: responseTime,
}, nil
}
// handleFailover attempts to download profile using alternative carriers
func (m *SMDPManager) handleFailover(ctx context.Context, req *ProfileRequest, failedCarrierID string, startTime time.Time) (*ProfileResponse, error) {
m.carriersMutex.RLock()
defer m.carriersMutex.RUnlock()
var retriedOn []string
for carrierID, carrier := range m.carriers {
if carrierID == failedCarrierID || !carrier.IsActive || carrier.HealthStatus != CarrierStatusHealthy {
continue
}
m.logger.WithField("carrier_id", carrierID).Info("Attempting failover to carrier")
response, err := m.downloadWithRetry(ctx, req, carrier, 0)
if err == nil && response.Success {
response.RetriedOn = retriedOn
response.ResponseTime = time.Since(startTime)
return response, nil
}
retriedOn = append(retriedOn, carrierID)
}
return &ProfileResponse{
Success: false,
StatusMessage: "All carriers failed during failover",
ResponseTime: time.Since(startTime),
RetriedOn: retriedOn,
}, fmt.Errorf("failover exhausted all carriers")
}
// updateCarrierMetrics updates the metrics for a carrier
func (m *SMDPManager) updateCarrierMetrics(carrierID string, success bool, responseTime time.Duration, err error) {
m.carriersMutex.Lock()
defer m.carriersMutex.Unlock()
carrier, exists := m.carriers[carrierID]
if !exists {
return
}
carrier.Metrics.TotalRequests++
if success {
carrier.Metrics.SuccessfulRequests++
} else {
carrier.Metrics.FailedRequests++
carrier.Metrics.LastError = err.Error()
carrier.Metrics.LastErrorTime = time.Now()
}
if carrier.Metrics.TotalRequests == 1 {
carrier.Metrics.AverageResponseTime = responseTime
} else {
carrier.Metrics.AverageResponseTime = time.Duration(
(int64(carrier.Metrics.AverageResponseTime)*int64(carrier.Metrics.TotalRequests-1) + int64(responseTime)) / int64(carrier.Metrics.TotalRequests),
)
}
carrier.Metrics.RequestRate = float64(carrier.Metrics.TotalRequests) / time.Since(time.Now().Add(-time.Minute)).Seconds()
}
func (m *SMDPManager) getHighestPriorityCarrier(carriers []*Carrier) *Carrier {
if len(carriers) == 0 {
return nil
}
highestPriority := carriers[0]
for _, carrier := range carriers {
if carrier.Priority > highestPriority.Priority {
highestPriority = carrier
}
}
return highestPriority
}