blockchain/internal/lib/cassandra/lock.go

127 lines
2.8 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 cassandra
import (
"context"
"fmt"
"time"
"github.com/scylladb/gocqlx/v3/qb"
)
const (
defaultTTLSec = 30
defaultRetry = 3
baseDelay = 100 * time.Millisecond
)
// 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
func (db *CassandraDB) TryLock(
ctx context.Context,
document any,
keyspace string,
opts ...LockOption,
) error {
// 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)
applied, err := q.ExecCASRelease()
if err != nil {
return err
}
if !applied {
return fmt.Errorf("failed to acquire lock")
}
return nil
}
// UnLock 砍掉鎖,其實就是 Delete
func (db *CassandraDB) UnLock(ctx context.Context, filter any, keyspace string) error {
if filter == nil {
return fmt.Errorf("unlock failed: nil filter")
}
metadata, err := GenerateTableMetadata(filter, keyspace)
if err != nil {
return fmt.Errorf("unlock: generate metadata failed: %w", err)
}
if len(metadata.Columns) == 0 {
return fmt.Errorf("unlock failed: missing primary key in struct")
}
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)
applied, err := q.ExecCASRelease()
if err == nil && applied {
return nil
}
if err != nil {
lastErr = fmt.Errorf("unlock error: %w", err)
} else if !applied {
lastErr = fmt.Errorf("unlock not applied: row not found or not visible yet")
}
time.Sleep(baseDelay * time.Duration(1<<i)) // 100ms → 200ms → 400ms
}
return fmt.Errorf("unlock failed after retries: %w", lastErr)
}