| package lockr_test |
|
|
| import ( |
| "context" |
| "database/sql" |
| "fmt" |
| "sync" |
| "testing" |
| "time" |
|
|
| "github.com/stretchr/testify/require" |
|
|
| "github.com/openmeterio/openmeter/openmeter/ent/db" |
| "github.com/openmeterio/openmeter/openmeter/testutils" |
| "github.com/openmeterio/openmeter/pkg/framework/entutils" |
| "github.com/openmeterio/openmeter/pkg/framework/entutils/entdriver" |
| "github.com/openmeterio/openmeter/pkg/framework/lockr" |
| "github.com/openmeterio/openmeter/pkg/framework/pgdriver" |
| "github.com/openmeterio/openmeter/pkg/framework/transaction" |
| ) |
|
|
| |
|
|
| type creator struct { |
| db *db.Client |
| } |
|
|
| var _ transaction.Creator = &creator{} |
|
|
| func (c *creator) Tx(ctx context.Context) (context.Context, transaction.Driver, error) { |
| txCtx, rawConfig, eDriver, err := c.db.HijackTx(ctx, &sql.TxOptions{ |
| ReadOnly: false, |
| }) |
| if err != nil { |
| return nil, nil, fmt.Errorf("failed to hijack transaction: %w", err) |
| } |
| return txCtx, entutils.NewTxDriver(eDriver, rawConfig), nil |
| } |
|
|
| func TestLockerLockForTx(t *testing.T) { |
| withDBClient := func(fn func(t *testing.T, client *db.Client)) func(t *testing.T) { |
| return func(t *testing.T) { |
| testdb := testutils.InitPostgresDB(t, testutils.PostgresDBStateEmpty) |
| dbClient := testdb.EntDriver.Client() |
| t.Cleanup(func() { testdb.Close(t) }) |
|
|
| fn(t, dbClient) |
| } |
| } |
|
|
| t.Run("Should error if not in a transaction", withDBClient(func(t *testing.T, client *db.Client) { |
| locker, err := lockr.NewLocker(&lockr.LockerConfig{ |
| Logger: testutils.NewLogger(t), |
| }) |
| require.NoError(t, err) |
|
|
| key, err := lockr.NewKey("test") |
| require.NoError(t, err) |
|
|
| err = locker.LockForTX(context.Background(), key) |
| require.Error(t, err) |
| require.ErrorContains(t, err, "lockr only works in a transaction, but driver not found") |
| })) |
|
|
| t.Run("Should acquire a lock", withDBClient(func(t *testing.T, client *db.Client) { |
| txCreator := &creator{db: client} |
|
|
| locker, err := lockr.NewLocker(&lockr.LockerConfig{ |
| Logger: testutils.NewLogger(t), |
| }) |
| require.NoError(t, err) |
|
|
| require.NoError(t, transaction.RunWithNoValue(context.Background(), txCreator, func(ctx context.Context) error { |
| key, err := lockr.NewKey("test") |
| if err != nil { |
| t.Fatalf("failed to create key: %v", err) |
| } |
|
|
| require.NoError(t, locker.LockForTX(ctx, key)) |
|
|
| return nil |
| })) |
| })) |
|
|
| t.Run("Should be able to acquire same lock twice if in same transaction", withDBClient(func(t *testing.T, client *db.Client) { |
| txCreator := &creator{db: client} |
|
|
| locker, err := lockr.NewLocker(&lockr.LockerConfig{ |
| Logger: testutils.NewLogger(t), |
| }) |
| require.NoError(t, err) |
|
|
| require.NoError(t, transaction.RunWithNoValue(context.Background(), txCreator, func(ctx context.Context) error { |
| key, err := lockr.NewKey("test") |
| if err != nil { |
| t.Fatalf("failed to create key: %v", err) |
| } |
|
|
| require.NoError(t, locker.LockForTX(ctx, key)) |
|
|
| require.NoError(t, locker.LockForTX(ctx, key)) |
|
|
| return nil |
| })) |
| })) |
|
|
| t.Run("Should be able to acquire same lock in sub-transaction", withDBClient(func(t *testing.T, client *db.Client) { |
| txCreator := &creator{db: client} |
|
|
| locker, err := lockr.NewLocker(&lockr.LockerConfig{ |
| Logger: testutils.NewLogger(t), |
| }) |
| require.NoError(t, err) |
|
|
| require.NoError(t, transaction.RunWithNoValue(context.Background(), txCreator, func(ctx context.Context) error { |
| key, err := lockr.NewKey("test") |
| if err != nil { |
| t.Fatalf("failed to create key: %v", err) |
| } |
|
|
| require.NoError(t, locker.LockForTX(ctx, key)) |
|
|
| require.NoError(t, transaction.RunWithNoValue(ctx, txCreator, func(ctx context.Context) error { |
| require.NoError(t, locker.LockForTX(ctx, key)) |
|
|
| require.NoError(t, transaction.RunWithNoValue(ctx, txCreator, func(ctx context.Context) error { |
| require.NoError(t, locker.LockForTX(ctx, key)) |
|
|
| return nil |
| })) |
|
|
| return nil |
| })) |
|
|
| return nil |
| })) |
| })) |
|
|
| t.Run("Should wait while acquiring lock from parallel transactions", withDBClient(func(t *testing.T, client *db.Client) { |
| txCreator := &creator{db: client} |
|
|
| locker, err := lockr.NewLocker(&lockr.LockerConfig{ |
| Logger: testutils.NewLogger(t), |
| }) |
| require.NoError(t, err) |
|
|
| key, err := lockr.NewKey("test") |
| require.NoError(t, err) |
|
|
| |
| |
| trigTwo := make(chan struct{}, 1) |
|
|
| wg := sync.WaitGroup{} |
| wg.Add(2) |
|
|
| finCh := make(chan string, 4) |
|
|
| go func() { |
| defer wg.Done() |
|
|
| require.NoError(t, transaction.RunWithNoValue(context.Background(), txCreator, func(ctx context.Context) error { |
| finCh <- "1 start" |
|
|
| require.NoError(t, locker.LockForTX(ctx, key)) |
|
|
| trigTwo <- struct{}{} |
|
|
| |
| time.Sleep(1 * time.Second) |
|
|
| finCh <- "1 done" |
|
|
| return nil |
| })) |
| }() |
|
|
| go func() { |
| defer wg.Done() |
|
|
| timeoutCtx, cancel := context.WithTimeout(context.Background(), time.Second) |
| defer cancel() |
|
|
| require.NoError(t, transaction.RunWithNoValue(context.Background(), txCreator, func(ctx context.Context) error { |
| for { |
| select { |
| case <-timeoutCtx.Done(): |
| require.Fail(t, "first routine failed to acquire the lock in time") |
| return nil |
| case <-trigTwo: |
| finCh <- "2 start" |
| require.NoError(t, locker.LockForTX(ctx, key)) |
| finCh <- "2 done" |
|
|
| return nil |
| } |
| } |
| })) |
| }() |
|
|
| wg.Wait() |
| close(finCh) |
|
|
| |
| results := []string{} |
|
|
| for fin := range finCh { |
| results = append(results, fin) |
| } |
|
|
| |
| require.Equal(t, []string{"1 start", "2 start", "1 done", "2 done"}, results) |
| })) |
|
|
| t.Run("Should error if acquiring lock takes longer than timeout", func(t *testing.T) { |
| lockTimeout := time.Second * 3 |
|
|
| testDB := testutils.InitPostgresDB(t, testutils.PostgresDBStateEmpty) |
| t.Cleanup(func() { testDB.Close(t) }) |
|
|
| pgdrv, err := pgdriver.NewPostgresDriver( |
| t.Context(), |
| testDB.URL, |
| pgdriver.WithLockTimeout(lockTimeout), |
| ) |
| if err != nil { |
| t.Fatalf("failed to get pg driver: %s", err) |
| } |
|
|
| client := entdriver.NewEntPostgresDriver(pgdrv.DB()).Client() |
|
|
| defer func() { |
| _ = client.Close() |
| _ = pgdrv.Close() |
|
|
| time.Sleep(1 * time.Second) |
| }() |
|
|
| txCreator := &creator{db: client} |
|
|
| locker, err := lockr.NewLocker(&lockr.LockerConfig{ |
| Logger: testutils.NewLogger(t), |
| }) |
| require.NoError(t, err) |
|
|
| key, err := lockr.NewKey("test") |
| require.NoError(t, err) |
|
|
| |
| |
| |
| trigTwo := make(chan struct{}, 1) |
|
|
| wg := sync.WaitGroup{} |
| wg.Add(2) |
|
|
| go func() { |
| defer wg.Done() |
|
|
| require.NoError(t, transaction.RunWithNoValue(context.Background(), txCreator, func(ctx context.Context) error { |
| require.NoError(t, locker.LockForTX(ctx, key)) |
|
|
| trigTwo <- struct{}{} |
|
|
| |
| time.Sleep(lockTimeout + time.Second) |
|
|
| return nil |
| })) |
| }() |
|
|
| go func() { |
| defer wg.Done() |
|
|
| timeoutCtx, cancel := context.WithTimeout(context.Background(), time.Second) |
| defer cancel() |
|
|
| |
| require.Error(t, transaction.RunWithNoValue(context.Background(), txCreator, func(ctx context.Context) error { |
| for { |
| select { |
| case <-timeoutCtx.Done(): |
| require.Fail(t, "first routine failed to acquire the lock in time") |
| return nil |
| |
| case <-trigTwo: |
| |
| err := locker.LockForTX(ctx, key) |
| require.Error(t, err) |
| require.ErrorIs(t, err, lockr.ErrLockTimeout) |
|
|
| return err |
| } |
| } |
| })) |
| }() |
|
|
| wg.Wait() |
| }) |
| } |
|
|