package cassandra import ( "context" "errors" "fmt" "time" "github.com/gocql/gocql" "github.com/scylladb/gocqlx/v3/qb" ) const ( defaultTTLSec = 30 defaultRetry = 3 baseDelay = 100 * time.Millisecond ) // 使用 error.go 中定義的統一錯誤 // LockOption 用來設定 TryLock 的 TTL 行為 type LockOption func(*lockOptions) type lockOptions struct { ttlSeconds int // TTL,單位秒;<=0 代表不 expire } func WithLockTTL(d time.Duration) LockOption { return func(o *lockOptions) { o.ttlSeconds = int(d.Seconds()) } } // WithNoLockExpire 永不自動解鎖 func WithNoLockExpire() LockOption { return func(o *lockOptions) { o.ttlSeconds = 0 } } // TryLock 嘗試在表上插入一筆唯一鍵(IF NOT EXISTS)作為鎖 // 預設 30 秒 TTL,可透過 option 調整或取消 TTL // keyspace 如果為空,則使用初始化時設定的預設 keyspace func (db *CassandraDB) TryLock( ctx context.Context, document any, keyspace string, opts ...LockOption, ) error { keyspace = getKeyspace(db, keyspace) // 1. 解析 metadata metadata, err := GenerateTableMetadata(document, keyspace) if err != nil { return err } // 2. 組合 option options := &lockOptions{ttlSeconds: defaultTTLSec} for _, opt := range opts { opt(options) } // 3. 建 TTL 子句 builder := qb.Insert(metadata.Name). Unique(). // IF NOT EXISTS Columns(metadata.Columns...) if options.ttlSeconds > 0 { ttl := time.Duration(options.ttlSeconds) * time.Second builder = builder.TTL(ttl) } stmt, names := builder.ToCql() // 4. 執行 CAS q := db.GetSession().Query(stmt, names).BindStruct(document). WithContext(ctx). WithTimestamp(time.Now().UnixNano() / 1e3). SerialConsistency(gocql.Serial) applied, err := q.ExecCASRelease() if err != nil { return err } if !applied { return ErrAcquireLockFailed.WithTable(metadata.Name) } return nil } // UnLock 釋放鎖,其實就是 Delete // keyspace 如果為空,則使用初始化時設定的預設 keyspace func (db *CassandraDB) UnLock(ctx context.Context, filter any, keyspace string) error { keyspace = getKeyspace(db, keyspace) if filter == nil { return errors.New("unlock: filter cannot be nil") } metadata, err := GenerateTableMetadata(filter, keyspace) if err != nil { return fmt.Errorf("unlock: failed to generate metadata: %w", err) } if len(metadata.Columns) == 0 { return fmt.Errorf("unlock: missing primary key in struct (table: %s)", metadata.Name) } var lastErr error for i := 0; i < defaultRetry; i++ { builder := qb.Delete(metadata.Name).Existing() // 動態添加 WHERE 條件 for _, key := range metadata.PartKey { builder = builder.Where(qb.Eq(key)) } stmt, names := builder.ToCql() q := db.GetSession().Query(stmt, names).BindStruct(filter). WithContext(ctx). WithTimestamp(time.Now().UnixNano() / 1e3). SerialConsistency(gocql.Serial) applied, err := q.ExecCASRelease() if err == nil && applied { return nil } if err != nil { lastErr = fmt.Errorf("unlock: execution failed (table: %s, attempt: %d/%d): %w", metadata.Name, i+1, defaultRetry, err) } else if !applied { lastErr = fmt.Errorf("unlock: operation not applied - row not found or not visible yet (table: %s)", metadata.Name) } time.Sleep(baseDelay * time.Duration(1<