Files

244 lines
7.9 KiB
Go

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
}