openmeter / pkg /framework /lockr /session.go
Leon4gr45's picture
Upload folder using huggingface_hub (part 9)
fea99b3 verified
Raw
History Blame Contribute Delete
6.7 kB
package lockr
import (
"context"
"database/sql"
"errors"
"fmt"
"log/slog"
"sync"
"sync/atomic"
"entgo.io/ent/dialect"
entsql "entgo.io/ent/dialect/sql"
"github.com/openmeterio/openmeter/pkg/clock"
"github.com/openmeterio/openmeter/pkg/framework/pgdriver"
)
var (
ErrNoLockAcquired = errors.New("lock could not be acquired")
ErrNoLockReleased = errors.New("lock could not be released")
ErrSessionLockerDone = errors.New("session locker is already closed")
ErrSessionLockerBusy = errors.New("session locker is blocked by another lock request")
)
type Releaser func(context.Context) error
type releaser struct {
mu sync.Mutex
done bool
locker *SessionLocker
key Key
}
func (r *releaser) release(ctx context.Context) error {
r.mu.Lock()
defer r.mu.Unlock()
if r.done {
return nil
}
rErr := r.locker.release(ctx, r.key)
if rErr != nil {
if !errors.Is(rErr, ErrNoLockReleased) && !errors.Is(rErr, ErrSessionLockerDone) {
return rErr
}
}
r.done = true
// Release references to locker and key so they can be GC'd
r.locker = nil
r.key = nil
return rErr
}
type SessionLockerConfig struct {
Logger *slog.Logger
PostgresDriver *pgdriver.Driver
}
// SessionLocker is a locker that uses PostgreSQL advisory locks to acquire locks.
// It requires a dedicated connection to acquire locks.
type SessionLocker struct {
logger *slog.Logger
driver *pgdriver.Driver
conn *sql.Conn
closed atomic.Bool
closer func()
mu sync.Mutex
once sync.Once
}
func NewSessionLockr(config SessionLockerConfig) (*SessionLocker, error) {
if config.Logger == nil {
return nil, errors.New("logger is required")
}
if config.PostgresDriver == nil {
return nil, errors.New("postgres driver is required")
}
id := clock.Now().UTC().UnixNano()
logger := config.Logger.With("component", "session-lockr", "id", id)
return &SessionLocker{
logger: logger,
driver: config.PostgresDriver,
}, nil
}
func (l *SessionLocker) Start(ctx context.Context) error {
l.mu.Lock()
defer l.mu.Unlock()
var err error
if l.conn == nil {
l.conn, err = l.driver.DB().Conn(ctx)
if err != nil {
return fmt.Errorf("failed to get postgres connection: %w", err)
}
}
l.closer = sync.OnceFunc(func() {
if err := l.conn.Close(); err != nil {
l.logger.Error("failed to close postgres connection: some session-level advisory locks might be dangling", "error", err)
}
})
return err
}
func (l *SessionLocker) lock(ctx context.Context, key Key, nonblocking bool) (Releaser, error) {
if l.closed.Load() {
return nil, ErrSessionLockerDone
}
lockFunc := "pg_advisory_lock"
if nonblocking {
lockFunc = "pg_try_advisory_lock"
}
q, args := entsql.Dialect(dialect.Postgres).
SelectExpr(entsql.ExprFunc(func(b *entsql.Builder) {
b.WriteString(lockFunc)
b.WriteString("(")
b.Arg(int64(key.Hash64()))
b.WriteString(")")
})).
Query()
rows, err := l.conn.QueryContext(ctx, q, args...)
defer func() {
if rows != nil {
if err := rows.Close(); err != nil {
l.logger.Warn("failed to close session-level advisory lock result", "error", err)
}
}
}()
if err != nil {
return nil, fmt.Errorf("failed to acquire session-level advisory lock: %w", checkForTimeout(err))
}
var lockAcquired bool
if nonblocking {
for rows.Next() {
if err := rows.Scan(&lockAcquired); err != nil {
return nil, fmt.Errorf("failed to scan session-level advisory lock result: %w", err)
}
}
} else {
lockAcquired = true
for rows.Next() {
}
}
if err = rows.Err(); err != nil {
return nil, checkForTimeout(err)
}
if !lockAcquired {
return nil, ErrNoLockAcquired
}
r := &releaser{
locker: l,
key: key,
}
return r.release, nil
}
// TryLock attempts to acquire a lock for the given key in a non-blocking way and returns a Releaser that can be used
// to release the lock if it is successfully acquired. The ErrNoLockAcquired is acquiring the lock is denied by the database server.
// It may return ErrSessionLockerBusy if the SessionLocker is blocked by another caller, indicating that the lock request may be retried.
// The ErrSessionLockerDone is returned if SessionLocker is closed, meaning it cannot be used for acquiring locks.
func (l *SessionLocker) TryLock(ctx context.Context, key Key) (Releaser, error) {
mutexLocked := l.mu.TryLock()
if !mutexLocked {
return nil, ErrSessionLockerBusy
}
defer l.mu.Unlock()
return l.lock(ctx, key, true)
}
func (l *SessionLocker) TryLockWithScopes(ctx context.Context, scopes ...string) (Releaser, error) {
k, err := NewKey(scopes...)
if err != nil {
return nil, err
}
return l.TryLock(ctx, k)
}
// Lock blocks until a lock is acquired and returns a Releaser that can be used to release the lock if it is successfully acquired.
// The ErrNoLockAcquired is acquiring the lock is denied by the database server.
// The ErrSessionLockerDone is returned if SessionLocker is closed, meaning it cannot be used for acquiring locks.
func (l *SessionLocker) Lock(ctx context.Context, key Key) (Releaser, error) {
l.mu.Lock()
defer l.mu.Unlock()
return l.lock(ctx, key, false)
}
func (l *SessionLocker) LockWithScopes(ctx context.Context, scopes ...string) (Releaser, error) {
k, err := NewKey(scopes...)
if err != nil {
return nil, err
}
return l.Lock(ctx, k)
}
func (l *SessionLocker) release(ctx context.Context, key Key) error {
if l.closed.Load() {
return ErrSessionLockerDone
}
q, args := entsql.Dialect(dialect.Postgres).
SelectExpr(entsql.ExprFunc(func(b *entsql.Builder) {
b.WriteString("pg_advisory_unlock")
b.WriteString("(")
b.Arg(int64(key.Hash64()))
b.WriteString(")")
})).
Query()
rows, err := l.conn.QueryContext(ctx, q, args...)
defer func() {
if rows != nil {
if err = rows.Close(); err != nil {
l.logger.Warn("failed to close session-level advisory lock result", "error", err)
}
}
}()
if err != nil {
return fmt.Errorf("failed to release session-level advisory lock: %w", checkForTimeout(err))
}
var lockReleased bool
for rows.Next() {
if err = rows.Scan(&lockReleased); err != nil {
return fmt.Errorf("failed to scan session-level advisory lock release result: %w", err)
}
}
if err = rows.Err(); err != nil {
return checkForTimeout(err)
}
if !lockReleased {
return ErrNoLockReleased
}
return nil
}
// Close releases all locks held by the SessionLocker and closes the underlying database connection.
func (l *SessionLocker) Close() {
l.mu.Lock()
defer l.mu.Unlock()
if l.closed.Load() {
return
}
l.closer()
l.closed.Store(true)
// Release references to conn and closer so it can be GC'd
l.conn = nil
l.closer = nil
}