AI Gateway Go 0.10.0 源码快照 + 旗舰版需求规划报告
M0-M7 已完成:核心网关(身份/RBAC/TOTP/OIDC/SAML/Provider/配额/路由/内容策略/审计/定价)+ 资源市场(MCP/Skills/数字员工)。 含 22 个 PostgreSQL 迁移、管理端/门户端前端源码、OpenAPI 契约、部署 compose。 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
package outbox
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
var ErrPublisherUnavailable = errors.New("outbox publisher unavailable")
|
||||
|
||||
type PublishResult struct {
|
||||
Published bool
|
||||
StreamID string
|
||||
}
|
||||
|
||||
type RedisPublisher struct {
|
||||
client *redis.Client
|
||||
stream string
|
||||
maxLength int64
|
||||
markerTTL time.Duration
|
||||
script *redis.Script
|
||||
}
|
||||
|
||||
func NewRedisPublisher(client *redis.Client, stream string, maxLength int64, markerTTL time.Duration) *RedisPublisher {
|
||||
return &RedisPublisher{client: client, stream: stream, maxLength: maxLength, markerTTL: markerTTL, script: redis.NewScript(publishScript)}
|
||||
}
|
||||
|
||||
// Ping reports whether the downstream Redis stream is reachable. The outbox
|
||||
// worker calls it before claiming events so a Redis outage never consumes the
|
||||
// events' delivery budget (claiming increments attempts; dead-lettering then
|
||||
// burns the whole queue for a fault that was never the events').
|
||||
func (p *RedisPublisher) Ping(ctx context.Context) error {
|
||||
if p == nil || p.client == nil {
|
||||
return ErrPublisherUnavailable
|
||||
}
|
||||
if err := p.client.Ping(ctx).Err(); err != nil {
|
||||
return fmt.Errorf("%w: %v", ErrPublisherUnavailable, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *RedisPublisher) Publish(ctx context.Context, event Event) (PublishResult, error) {
|
||||
if p == nil || p.client == nil {
|
||||
return PublishResult{}, ErrPublisherUnavailable
|
||||
}
|
||||
marker := "gateway:{outbox}:published:" + event.EventID
|
||||
result, err := p.script.Run(ctx, p.client, []string{marker, p.stream},
|
||||
int64(p.markerTTL.Seconds()), p.maxLength, event.EventID, event.EventType, event.EventVersion,
|
||||
valueOrEmpty(event.TenantID), event.AggregateType, event.AggregateID, string(event.Payload), string(event.TraceContext), event.OccurredAt.UTC().Format(time.RFC3339Nano),
|
||||
).Slice()
|
||||
if err != nil || len(result) != 2 {
|
||||
return PublishResult{}, fmt.Errorf("%w: %v", ErrPublisherUnavailable, err)
|
||||
}
|
||||
published, err := redisInt(result[0])
|
||||
if err != nil {
|
||||
return PublishResult{}, fmt.Errorf("%w: %v", ErrPublisherUnavailable, err)
|
||||
}
|
||||
streamID := "duplicate"
|
||||
if result[1] != nil && fmt.Sprint(result[1]) != "" {
|
||||
streamID = fmt.Sprint(result[1])
|
||||
}
|
||||
return PublishResult{Published: published == 1, StreamID: streamID}, nil
|
||||
}
|
||||
|
||||
func valueOrEmpty(value *string) string {
|
||||
if value == nil {
|
||||
return ""
|
||||
}
|
||||
return *value
|
||||
}
|
||||
|
||||
func redisInt(value any) (int64, error) {
|
||||
switch typed := value.(type) {
|
||||
case int64:
|
||||
return typed, nil
|
||||
case string:
|
||||
return strconv.ParseInt(typed, 10, 64)
|
||||
case []byte:
|
||||
return strconv.ParseInt(string(typed), 10, 64)
|
||||
default:
|
||||
return 0, fmt.Errorf("unexpected redis integer %T", value)
|
||||
}
|
||||
}
|
||||
|
||||
const publishScript = `
|
||||
if redis.call('SET', KEYS[1], '1', 'NX', 'EX', tonumber(ARGV[1])) then
|
||||
local id = redis.call('XADD', KEYS[2], 'MAXLEN', '~', tonumber(ARGV[2]), '*',
|
||||
'event_id', ARGV[3], 'event_type', ARGV[4], 'event_version', ARGV[5],
|
||||
'tenant_id', ARGV[6], 'aggregate_type', ARGV[7], 'aggregate_id', ARGV[8],
|
||||
'payload', ARGV[9], 'trace_context', ARGV[10], 'occurred_at', ARGV[11])
|
||||
return {1, id}
|
||||
end
|
||||
return {0, ''}
|
||||
`
|
||||
Reference in New Issue
Block a user