package mysql import ( "context" "crypto/sha256" "database/sql" "errors" "git.yuntuo.hk/k2safe/orbit/internal/fault" core "git.yuntuo.hk/k2safe/orbit/model" ) func concurrencyDigest(key string) *core.Digest { if key == "" { return nil } digest := core.Digest(sha256.Sum256(append([]byte("orbit-concurrency-key-v1\x00"), []byte(key)...))) return &digest } // concurrencyKeyValue 在 Go 中判断空字节串,避免 MySQL 文本排序规则将某些字符视为空。 func concurrencyKeyValue(key string) *string { if key == "" { return nil } return &key } // keyEligibility 在 LIMIT 前排除已占用资源,避免前 16 个同键任务挡住其他资源。 // 调用者已锁应用额度与队列;命名空间中的所有领取都遵守此顺序。 const keyEligibility = `(concurrency_key_digest IS NULL OR NOT EXISTS ( SELECT 1 FROM pulse_concurrency_limits k WHERE k.namespace_id=pulse_tasks.namespace_id AND k.scope_key='key' AND k.key_digest=pulse_tasks.concurrency_key_digest AND k.used_permits>=k.max_permits))` // reserveConcurrencyKey 为一个显式资源键预占单个许可。零 token 表示未启用或暂不可用。 // 序号使用资源行单调 revision;释放也递增,序号允许有间隔,禁止删除或重置资源行。 func reserveConcurrencyKey(ctx context.Context, tx *sql.Tx, ns core.ID, key string) (core.ID, uint64, error) { if key == "" { return "", 0, nil } id, err := core.NewID() if err != nil { return "", 0, err } digest := concurrencyDigest(key) _, err = tx.ExecContext(ctx, `INSERT INTO pulse_concurrency_limits(namespace_id,id,scope_key,key_digest,max_permits) VALUES (?,?,'key',?,1) ON DUPLICATE KEY UPDATE id=pulse_concurrency_limits.id`, ns, id, digest) if err != nil { return "", 0, err } var used, max uint32 var revision uint64 err = tx.QueryRowContext(ctx, `SELECT id,used_permits,max_permits,revision FROM pulse_concurrency_limits WHERE namespace_id=? AND scope_key='key' AND key_digest=? FOR UPDATE`, ns, digest).Scan(&id, &used, &max, &revision) if err != nil { return "", 0, err } if max != 1 { return "", 0, errors.New("key permit limit must be one") } if used != 0 { return id, 0, nil } // 为当前授权及后续释放各保留一次递增空间,不能溢出后重用旧序号。 if revision > ^uint64(0)-2 { return "", 0, fault.Conflict } r, err := tx.ExecContext(ctx, `UPDATE pulse_concurrency_limits SET used_permits=1,revision=revision+1,updated_at=UTC_TIMESTAMP(6) WHERE namespace_id=? AND id=? AND used_permits=0 AND revision=?`, ns, id, revision) if err := exactlyOne(r, err); err != nil { return "", 0, err } return id, revision + 1, nil }