package rabbitmq import ( "context" "encoding/json" "fmt" "net/url" "strings" "time" "github.com/gogf/gf/v2/errors/gerror" "github.com/gogf/gf/v2/frame/g" "git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/consts" "git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/library/rabbitmq" "git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/model" ) const channelRefreshConsumer = "iotlcm-worker-channel-refresh" type channelRefreshConfig struct { Address string `json:"address"` Timeout time.Duration `json:"timeout"` } type channelRefreshResponse struct { Code int `json:"code"` Message string `json:"message"` } func (c *Consumers) initChannelRefresh(ctx context.Context) error { if !rabbitmq.Enabled(ctx) { g.Log().Info(ctx, "channel refresh consumer skipped, RabbitMQ disabled") return nil } if err := rabbitmq.ConsumeJSON[model.ChannelRefreshMessage]( ctx, consts.QueueChannelRefresh, channelRefreshConsumer, c.handleChannelRefresh, ); err != nil { return err } g.Log().Infof(ctx, "channel refresh consumer started, queue:%s", consts.QueueChannelRefresh) return nil } func (c *Consumers) handleChannelRefresh(ctx context.Context, message model.ChannelRefreshMessage) error { switch strings.ToLower(strings.TrimSpace(message.Type)) { case "channel": if strings.TrimSpace(message.ChanKey) == "" { g.Log().Errorf(ctx, "channel refresh message dropped, chanKey is empty") return nil } if err := refreshChannelWithRetry(ctx, message.ChanKey); err != nil { g.Log().Errorf(ctx, "channel refresh failed, chanKey=%s, err=%+v", message.ChanKey, err) return err } return nil case "template": if strings.TrimSpace(message.TempKey) == "" { g.Log().Errorf(ctx, "channel refresh message dropped, tempKey is empty") return nil } return refreshTemplateChannels(ctx, message.TempKey) case "channel_rename": oldChanKey := strings.TrimSpace(message.OldChanKey) newChanKey := strings.TrimSpace(message.NewChanKey) if oldChanKey == "" || newChanKey == "" || oldChanKey == newChanKey { g.Log().Errorf(ctx, "channel rename message dropped, oldChanKey=%q newChanKey=%q", oldChanKey, newChanKey) return nil } if err := renameChannelWithRetry(ctx, oldChanKey, newChanKey); err != nil { // 返回 error 让消费框架 Nack 并重新入队:rename 各步骤幂等, // 重复投递安全;静默 Ack 会导致旧通道实例永久残留,只能重启恢复。 g.Log().Errorf(ctx, "channel rename failed after retries, will requeue, oldChanKey=%s newChanKey=%s err=%+v", oldChanKey, newChanKey, err) return err } return nil default: g.Log().Errorf(ctx, "channel refresh message dropped, unsupported type=%s", message.Type) return nil } } func renameChannelWithRetry(ctx context.Context, oldChanKey, newChanKey string) error { var err error for attempt := 1; attempt <= 3; attempt++ { err = renameChannel(ctx, oldChanKey, newChanKey) if err == nil { return nil } if attempt == 3 { break } delay := time.Second if attempt == 2 { delay = 3 * time.Second } select { case <-ctx.Done(): return ctx.Err() case <-time.After(delay): } } return gerror.Wrapf(err, "channel rename failed after %d attempts, oldChanKey=%s newChanKey=%s", 3, oldChanKey, newChanKey) } // renameChannel is ordered so that the old runtime remains available until // both upstreams have built the new instance. Card data and ICCID routes are // intentionally left untouched for operations staff to migrate manually. func renameChannel(ctx context.Context, oldChanKey, newChanKey string) error { if err := refreshChannel(ctx, newChanKey); err != nil { return gerror.Wrap(err, "refresh new channel failed") } if err := removeChannelTarget(ctx, "upstream.gateway", oldChanKey); err != nil { return gerror.Wrap(err, "remove old gateway channel failed") } if err := removeChannelTarget(ctx, "upstream.proxy", oldChanKey); err != nil { return gerror.Wrap(err, "remove old proxy channel failed") } g.Log().Infof(ctx, "channel instance rebuilt after rename, oldChanKey=%s newChanKey=%s", oldChanKey, newChanKey) return nil } func refreshTemplateChannels(ctx context.Context, tempKey string) error { var rows []struct { ChanKey string `json:"chanKey"` } if err := g.DB().Model("t_sim_channels").Ctx(ctx). Fields("chan_key"). Where("temp_key", strings.TrimSpace(tempKey)). Scan(&rows); err != nil { return gerror.Wrap(err, "query template channels failed") } var firstErr error for _, row := range rows { if err := refreshChannelWithRetry(ctx, row.ChanKey); err != nil { g.Log().Errorf(ctx, "template channel refresh failed, tempKey=%s, chanKey=%s, err=%+v", tempKey, row.ChanKey, err) if firstErr == nil { firstErr = err } } } return firstErr } func refreshChannelWithRetry(ctx context.Context, chanKey string) error { var err error for attempt := 1; attempt <= 3; attempt++ { err = refreshChannel(ctx, chanKey) if err == nil { return nil } if attempt == 3 { break } delay := time.Second if attempt == 2 { delay = 3 * time.Second } select { case <-ctx.Done(): return gerror.Wrapf(err, "channel refresh canceled after %d attempts, chanKey=%s", attempt, chanKey) case <-time.After(delay): } } return gerror.Wrapf(err, "channel refresh failed after %d attempts, chanKey=%s", 3, chanKey) } func refreshChannel(ctx context.Context, chanKey string) error { chanKey = strings.TrimSpace(chanKey) if chanKey == "" { return gerror.New("chanKey is required") } if err := refreshChannelTarget(ctx, "upstream.gateway", chanKey); err != nil { return gerror.Wrap(err, "refresh gateway channel failed") } if err := refreshChannelTarget(ctx, "upstream.proxy", chanKey); err != nil { return gerror.Wrap(err, "refresh proxy channel failed") } return nil } func refreshChannelTarget(ctx context.Context, configKey, chanKey string) error { return channelControlTarget(ctx, configKey, "/internal/channel/refresh", chanKey) } func removeChannelTarget(ctx context.Context, configKey, chanKey string) error { return channelControlTarget(ctx, configKey, "/internal/channel/remove", chanKey) } func channelControlTarget(ctx context.Context, configKey, path, chanKey string) error { configValue, err := g.Cfg().Get(ctx, configKey) if err != nil { return gerror.Wrapf(err, "read upstream configuration failed: %s", configKey) } if configValue == nil || configValue.IsNil() { return gerror.Newf("upstream configuration is missing: %s", configKey) } config := channelRefreshConfig{} if err = configValue.Scan(&config); err != nil { return gerror.Wrapf(err, "parse upstream configuration failed: %s", configKey) } address := strings.TrimSpace(config.Address) if address == "" { return gerror.Newf("upstream address is empty: %s", configKey) } if config.Timeout <= 0 { config.Timeout = 10 * time.Second } requestCtx, cancel := context.WithTimeout(ctx, config.Timeout) defer cancel() client := g.Client() client.SetTimeout(config.Timeout) controlURL := fmt.Sprintf("http://%s%s?chanKey=%s", address, path, url.QueryEscape(chanKey)) response, err := client.Post(requestCtx, controlURL, nil) if err != nil { return gerror.Wrapf(err, "request upstream channel control failed: target=%s path=%s", configKey, path) } defer response.Close() output := channelRefreshResponse{} statusCode := 0 if response.Response != nil { statusCode = response.StatusCode } if err = json.Unmarshal(response.ReadAll(), &output); err != nil { return gerror.Wrapf(err, "parse upstream channel control response failed: target=%s path=%s status=%d", configKey, path, statusCode) } if output.Code != 0 { if output.Message == "" { output.Message = "upstream channel control failed" } return gerror.Newf("upstream channel control failed: target=%s path=%s status=%d message=%s", configKey, path, statusCode, output.Message) } if statusCode < 200 || statusCode >= 300 { return gerror.Newf("upstream channel control failed: target=%s path=%s status=%d", configKey, path, statusCode) } return nil }