Files
orbit/internal/storage/mysql/event_subscriptions.go

178 lines
6.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package mysql
import (
"context"
"crypto/sha256"
"database/sql"
"encoding/hex"
"encoding/json"
"errors"
"math"
"git.yuntuo.hk/k2safe/orbit/internal/fault"
core "git.yuntuo.hk/k2safe/orbit/model"
"git.yuntuo.hk/k2safe/orbit/platform/auth"
"git.yuntuo.hk/k2safe/orbit/platform/events"
driver "github.com/go-sql-driver/mysql"
)
// EventStore 管理独立结果订阅;消费者事务不锁任务、队列、应用额度或结果流行。
// 创建时按认证→请求身份→队列共享锁→结果流锁顺序确定提交边界。
type EventStore struct{ *TaskStore }
func NewEventStore(db *sql.DB) *EventStore { return &EventStore{NewTaskStore(db)} }
var _ events.Store = (*EventStore)(nil)
const subscriptionColumns = `id,queue_id,credential_id,name,revision,enabled,filter,cursor_sequence,pending_count,max_pending,max_attempts,created_at,updated_at`
func readSubscription(ctx context.Context, tx *sql.Tx, t auth.Token, p auth.Permissions, id core.ID, lock bool) (events.Subscription, error) {
var out events.Subscription
var raw []byte
q := `SELECT ` + subscriptionColumns + ` FROM orbit_subscriptions WHERE namespace_id=? AND id=? AND credential_id=? AND queue_id IS NOT NULL`
if lock {
q += ` FOR UPDATE`
}
err := tx.QueryRowContext(ctx, q, t.NamespaceID, id, t.CredentialID).Scan(&out.ID, &out.QueueID, &out.CredentialID, &out.Name, &out.Revision, &out.Enabled, &raw, &out.CursorSequence, &out.PendingCount, &out.MaxPending, &out.MaxAttempts, &out.CreatedAt, &out.UpdatedAt)
if errors.Is(err, sql.ErrNoRows) {
return out, fault.NotFound
}
if err != nil {
return out, err
}
if !p.AllowsQueue(out.QueueID) {
return events.Subscription{}, fault.NotFound
}
if err = json.Unmarshal(raw, &out.Types); err != nil {
return out, err
}
return out, nil
}
func (s *EventStore) CreateSubscription(ctx context.Context, t auth.Token, key string, in events.CreateInput) (events.SubscriptionResult, error) {
var out events.SubscriptionResult
err := s.transaction(ctx, t, "events:manage", func(tx *sql.Tx, p auth.Permissions) error {
if !p.AllowsQueue(in.QueueID) {
return fault.Forbidden
}
id, err := core.NewID()
if err != nil {
return err
}
identity, cached, err := reserveIdentity(ctx, tx, t.NamespaceID, "subscription.create", string(t.CredentialID), key, "subscription", id, in)
if err != nil {
return err
}
if cached != nil {
if err = json.Unmarshal(cached, &out); err != nil {
return err
}
out.Deduplicated = true
return nil
}
var queue core.ID
if err = tx.QueryRowContext(ctx, `SELECT id FROM pulse_queues WHERE namespace_id=? AND id=? FOR SHARE`, t.NamespaceID, in.QueueID).Scan(&queue); errors.Is(err, sql.ErrNoRows) {
return fault.NotFound
} else if err != nil {
return err
}
head, retained, err := lockEventStream(ctx, tx, t.NamespaceID)
if err != nil {
return err
}
cursor := head
if in.FromSequence != nil {
cursor = *in.FromSequence
}
if cursor > head {
return fault.Invalid("from_sequence is ahead of committed history")
}
if cursor < retained-1 {
return events.HistoryExpired
}
var count int
if err = tx.QueryRowContext(ctx, `SELECT COUNT(*) FROM orbit_subscriptions WHERE namespace_id=?`, t.NamespaceID).Scan(&count); err != nil {
return err
}
if count >= events.MaxSubscriptions {
return events.SubscriptionLimit
}
filter, err := json.Marshal(in.Types)
if err != nil {
return err
}
_, err = tx.ExecContext(ctx, `INSERT INTO orbit_subscriptions(namespace_id,id,name,credential_id,filter,queue_id,cursor_sequence,max_pending,max_attempts) VALUES (?,?,?,?,?,?,?,?,?)`, t.NamespaceID, id, in.Name, t.CredentialID, string(filter), in.QueueID, cursor, in.MaxPending, in.MaxAttempts)
var me *driver.MySQLError
if errors.As(err, &me) && me.Number == 1062 {
return fault.Conflict
}
if err != nil {
return err
}
out.Subscription, err = readSubscription(ctx, tx, t, p, id, false)
if err != nil {
return err
}
if err = appendAudit(ctx, tx, t, identity, id, "subscription", "subscription.create", "create durable result subscription", nil, out.Subscription); err != nil {
return err
}
return saveIdentity(ctx, tx, t.NamespaceID, identity, out)
})
return out, err
}
func (s *EventStore) GetSubscription(ctx context.Context, t auth.Token, id core.ID) (events.Subscription, error) {
var out events.Subscription
err := s.transaction(ctx, t, "events:read", func(tx *sql.Tx, p auth.Permissions) error {
var err error
out, err = readSubscription(ctx, tx, t, p, id, false)
return err
})
return out, err
}
func (s *EventStore) ControlSubscription(ctx context.Context, t auth.Token, id core.ID, key string, in events.ControlInput) (events.SubscriptionResult, error) {
var out events.SubscriptionResult
err := s.transaction(ctx, t, "events:manage", func(tx *sql.Tx, p auth.Permissions) error {
identity, cached, err := reserveIdentity(ctx, tx, t.NamespaceID, "subscription.control", eventScope(t.CredentialID, id), key, "subscription", id, in)
if err != nil {
return err
}
before, err := readSubscription(ctx, tx, t, p, id, true)
if err != nil {
return err
}
if cached != nil {
if err = json.Unmarshal(cached, &out); err != nil {
return err
}
out.Deduplicated = true
return nil
}
if before.Revision != in.ExpectedRevision || before.Revision == math.MaxUint64 {
return fault.Conflict
}
if _, err = tx.ExecContext(ctx, `UPDATE orbit_subscriptions SET enabled=?,revision=revision+1,updated_at=UTC_TIMESTAMP(6) WHERE namespace_id=? AND id=?`, in.Enabled, t.NamespaceID, id); err != nil {
return err
}
out.Subscription, err = readSubscription(ctx, tx, t, p, id, false)
if err != nil {
return err
}
if err = appendAudit(ctx, tx, t, identity, id, "subscription", "subscription.control", in.Reason, before, out.Subscription); err != nil {
return err
}
return saveIdentity(ctx, tx, t.NamespaceID, identity, out)
})
return out, err
}
// eventScope 将带域隔离的复合资源身份压缩到已有64字符幂等作用域,避免截断或串联歧义。
func eventScope(ids ...core.ID) string {
h := sha256.New()
h.Write([]byte("orbit-event-scope-v1"))
for _, id := range ids {
h.Write([]byte{0})
h.Write([]byte(id))
}
return hex.EncodeToString(h.Sum(nil))
}