chore: 并入 iotlcm-worker-svr 历史到 worker-svr/(原提交 96f0f4e)
This commit is contained in:
@@ -0,0 +1,243 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user