WeKnora deployment source with the reviewed wiki identity, summary and recovery fixes
Anydoc / anydoc parser engine (push) Canceled after 0s
App / Format, vet, test, and build (push) Canceled after 0s
CLI / test (macos-latest / go 1.26) (push) Canceled after 0s
CLI / test (ubuntu-latest / go 1.26) (push) Canceled after 0s
CLI / test (windows-latest / go 1.26) (push) Canceled after 0s
Build and Push Docker Image / build-ui (push) Canceled after 0s
Build and Push Docker Image / build-docreader (push) Canceled after 0s
Build and Push Docker Image / build-sandbox (push) Canceled after 0s
Build and Push Docker Image / build-app (amd64, linux/amd64, ubuntu-latest) (push) Canceled after 0s
Build and Push Docker Image / build-app (arm64, linux/arm64, ubuntu-24.04-arm) (push) Canceled after 0s
DocReader / Python tests and Go client tests (push) Canceled after 0s
Build & Publish (dsh-weknora) / Typecheck, test, pack (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (pinned) (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (latest, advisory) (push) Canceled after 0s
Frontend / Test, type-check, and build (push) Canceled after 0s
Go Lint Cache / warm lint cache (.) (push) Canceled after 0s
Go Lint Cache / warm lint cache (cli) (push) Canceled after 0s
Go Lint Cache / warm lint cache (client) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.10) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.11) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.12) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.13) (push) Canceled after 0s
Build and Push Docker Image / merge (push) Canceled after 0s
Build & Publish (dsh-weknora) / Publish to npm (push) Canceled after 0s
Build & Publish (mcp-server) / Build sdist + wheel (push) Canceled after 0s
Build & Publish (mcp-server) / Publish to PyPI (push) Canceled after 0s
Anydoc / anydoc parser engine (push) Canceled after 0s
App / Format, vet, test, and build (push) Canceled after 0s
CLI / test (macos-latest / go 1.26) (push) Canceled after 0s
CLI / test (ubuntu-latest / go 1.26) (push) Canceled after 0s
CLI / test (windows-latest / go 1.26) (push) Canceled after 0s
Build and Push Docker Image / build-ui (push) Canceled after 0s
Build and Push Docker Image / build-docreader (push) Canceled after 0s
Build and Push Docker Image / build-sandbox (push) Canceled after 0s
Build and Push Docker Image / build-app (amd64, linux/amd64, ubuntu-latest) (push) Canceled after 0s
Build and Push Docker Image / build-app (arm64, linux/arm64, ubuntu-24.04-arm) (push) Canceled after 0s
DocReader / Python tests and Go client tests (push) Canceled after 0s
Build & Publish (dsh-weknora) / Typecheck, test, pack (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (pinned) (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (latest, advisory) (push) Canceled after 0s
Frontend / Test, type-check, and build (push) Canceled after 0s
Go Lint Cache / warm lint cache (.) (push) Canceled after 0s
Go Lint Cache / warm lint cache (cli) (push) Canceled after 0s
Go Lint Cache / warm lint cache (client) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.10) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.11) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.12) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.13) (push) Canceled after 0s
Build and Push Docker Image / merge (push) Canceled after 0s
Build & Publish (dsh-weknora) / Publish to npm (push) Canceled after 0s
Build & Publish (mcp-server) / Build sdist + wheel (push) Canceled after 0s
Build & Publish (mcp-server) / Publish to PyPI (push) Canceled after 0s
This commit is contained in:
@@ -0,0 +1,269 @@
|
||||
package redislock_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/alicebob/miniredis/v2"
|
||||
"github.com/redis/go-redis/v9"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/Tencent/WeKnora/internal/common/redislock"
|
||||
)
|
||||
|
||||
func newTokenLockTestClient(t *testing.T) (*redis.Client, *miniredis.Miniredis) {
|
||||
t.Helper()
|
||||
|
||||
mini := miniredis.RunT(t)
|
||||
client := redis.NewClient(&redis.Options{Addr: mini.Addr()})
|
||||
t.Cleanup(func() { require.NoError(t, client.Close()) })
|
||||
return client, mini
|
||||
}
|
||||
|
||||
func TestRenewableLockWaitOnlyLimitsAcquisition(t *testing.T) {
|
||||
client, _ := newTokenLockTestClient(t)
|
||||
err := redislock.WithRenewableLockWait(context.Background(), client, "lock:long", time.Second, 10*time.Millisecond, 15*time.Millisecond, func(ctx context.Context) error {
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
require.NoError(t, ctx.Err(), "acquisition deadline must not cancel running synthesis")
|
||||
return redislock.EnsureOwnership(ctx)
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, client.Set(context.Background(), "lock:busy", "other-owner", time.Minute).Err())
|
||||
called := false
|
||||
err = redislock.WithRenewableLockWait(context.Background(), client, "lock:busy", time.Second, 10*time.Millisecond, 15*time.Millisecond, func(context.Context) error {
|
||||
called = true
|
||||
return nil
|
||||
})
|
||||
require.ErrorIs(t, err, context.DeadlineExceeded)
|
||||
require.False(t, called)
|
||||
require.Equal(t, "other-owner", client.Get(context.Background(), "lock:busy").Val())
|
||||
}
|
||||
|
||||
func TestEnsureOwnershipRejectsStaleWriterBeforeNextRenewal(t *testing.T) {
|
||||
client, _ := newTokenLockTestClient(t)
|
||||
err := redislock.WithRenewableLockWait(context.Background(), client, "lock:stale", time.Minute, 20*time.Second, time.Second, func(ctx context.Context) error {
|
||||
require.NoError(t, client.Set(context.Background(), "lock:stale", "successor", time.Minute).Err())
|
||||
err := redislock.EnsureOwnership(ctx)
|
||||
require.ErrorIs(t, err, redislock.ErrLockOwnershipLost)
|
||||
require.Error(t, ctx.Err())
|
||||
return err
|
||||
})
|
||||
require.ErrorIs(t, err, redislock.ErrLockOwnershipLost)
|
||||
require.Equal(t, "successor", client.Get(context.Background(), "lock:stale").Val(), "stale release must not delete the successor")
|
||||
}
|
||||
|
||||
func TestNewTokenReturnsUniqueOpaqueOwners(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
first, err := redislock.NewToken()
|
||||
require.NoError(t, err)
|
||||
second, err := redislock.NewToken()
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, first, 32)
|
||||
require.Len(t, second, 32)
|
||||
require.NotEqual(t, first, second)
|
||||
}
|
||||
|
||||
func TestTokenLockAcquireAndReleaseRequiresOwnership(t *testing.T) {
|
||||
client, _ := newTokenLockTestClient(t)
|
||||
ctx := context.Background()
|
||||
key := "lock:ownership"
|
||||
|
||||
acquired, err := redislock.TryAcquire(ctx, client, key, "owner-a", time.Minute)
|
||||
require.NoError(t, err)
|
||||
require.True(t, acquired)
|
||||
|
||||
acquired, err = redislock.TryAcquire(ctx, client, key, "owner-b", time.Minute)
|
||||
require.NoError(t, err)
|
||||
require.False(t, acquired)
|
||||
|
||||
released, err := redislock.Release(ctx, client, key, "owner-b")
|
||||
require.NoError(t, err)
|
||||
require.False(t, released)
|
||||
require.Equal(t, "owner-a", client.Get(ctx, key).Val())
|
||||
|
||||
released, err = redislock.Release(ctx, client, key, "owner-a")
|
||||
require.NoError(t, err)
|
||||
require.True(t, released)
|
||||
require.ErrorIs(t, client.Get(ctx, key).Err(), redis.Nil)
|
||||
}
|
||||
|
||||
func TestTokenLockRenewRequiresOwnership(t *testing.T) {
|
||||
client, mini := newTokenLockTestClient(t)
|
||||
ctx := context.Background()
|
||||
key := "lock:renew"
|
||||
|
||||
require.NoError(t, client.Set(ctx, key, "owner-a", time.Second).Err())
|
||||
renewed, err := redislock.Renew(ctx, client, key, "owner-b", time.Minute)
|
||||
require.NoError(t, err)
|
||||
require.False(t, renewed)
|
||||
|
||||
renewed, err = redislock.Renew(ctx, client, key, "owner-a", time.Minute)
|
||||
require.NoError(t, err)
|
||||
require.True(t, renewed)
|
||||
mini.FastForward(2 * time.Second)
|
||||
require.Equal(t, "owner-a", client.Get(ctx, key).Val())
|
||||
}
|
||||
|
||||
func TestTokenLockRejectsUnsafeLeaseConfiguration(t *testing.T) {
|
||||
client, _ := newTokenLockTestClient(t)
|
||||
ctx := context.Background()
|
||||
|
||||
_, err := redislock.TryAcquire(ctx, client, "lock:key", "owner", 0)
|
||||
require.Error(t, err)
|
||||
_, err = redislock.Renew(ctx, client, "lock:key", "owner", -time.Second)
|
||||
require.Error(t, err)
|
||||
err = redislock.WithRenewableLock(
|
||||
ctx,
|
||||
client,
|
||||
"lock:key",
|
||||
time.Second,
|
||||
time.Second,
|
||||
func(context.Context) error { return nil },
|
||||
)
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestWithRenewableLockReturnsCallbackErrorAndReleases(t *testing.T) {
|
||||
client, _ := newTokenLockTestClient(t)
|
||||
ctx := context.Background()
|
||||
key := "lock:callback"
|
||||
want := errors.New("callback failed")
|
||||
|
||||
err := redislock.WithRenewableLock(
|
||||
ctx,
|
||||
client,
|
||||
key,
|
||||
time.Minute,
|
||||
20*time.Second,
|
||||
func(context.Context) error { return want },
|
||||
)
|
||||
|
||||
require.ErrorIs(t, err, want)
|
||||
require.ErrorIs(t, client.Get(ctx, key).Err(), redis.Nil)
|
||||
}
|
||||
|
||||
func TestWithRenewableLockHonorsContextWhileWaiting(t *testing.T) {
|
||||
client, _ := newTokenLockTestClient(t)
|
||||
ctx := context.Background()
|
||||
key := "lock:busy"
|
||||
require.NoError(t, client.Set(ctx, key, "owner-a", time.Minute).Err())
|
||||
|
||||
waitCtx, cancel := context.WithTimeout(ctx, 25*time.Millisecond)
|
||||
defer cancel()
|
||||
called := false
|
||||
err := redislock.WithRenewableLock(
|
||||
waitCtx,
|
||||
client,
|
||||
key,
|
||||
time.Minute,
|
||||
20*time.Second,
|
||||
func(context.Context) error {
|
||||
called = true
|
||||
return nil
|
||||
},
|
||||
)
|
||||
|
||||
require.ErrorIs(t, err, context.DeadlineExceeded)
|
||||
require.False(t, called)
|
||||
}
|
||||
|
||||
func TestWithRenewableLockCancelsCallbackAfterOwnershipLoss(t *testing.T) {
|
||||
client, _ := newTokenLockTestClient(t)
|
||||
key := "lock:lost"
|
||||
|
||||
err := redislock.WithRenewableLock(
|
||||
context.Background(),
|
||||
client,
|
||||
key,
|
||||
100*time.Millisecond,
|
||||
10*time.Millisecond,
|
||||
func(lockCtx context.Context) error {
|
||||
require.NoError(t, client.Set(context.Background(), key, "replacement", time.Minute).Err())
|
||||
select {
|
||||
case <-lockCtx.Done():
|
||||
return nil
|
||||
case <-time.After(time.Second):
|
||||
return errors.New("callback was not cancelled")
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "ownership lost")
|
||||
require.Equal(t, "replacement", client.Get(context.Background(), key).Val())
|
||||
}
|
||||
|
||||
func TestWithRenewableLockReportsOwnershipLossAtRelease(t *testing.T) {
|
||||
client, _ := newTokenLockTestClient(t)
|
||||
key := "lock:lost-before-renewal"
|
||||
|
||||
err := redislock.WithRenewableLock(
|
||||
context.Background(),
|
||||
client,
|
||||
key,
|
||||
time.Minute,
|
||||
20*time.Second,
|
||||
func(context.Context) error {
|
||||
return client.Set(context.Background(), key, "replacement", time.Minute).Err()
|
||||
},
|
||||
)
|
||||
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "ownership lost")
|
||||
require.Equal(t, "replacement", client.Get(context.Background(), key).Val())
|
||||
}
|
||||
|
||||
func TestOwnershipContextSurvivesCallerCancellation(t *testing.T) {
|
||||
client, _ := newTokenLockTestClient(t)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
err := redislock.WithRenewableLock(
|
||||
ctx,
|
||||
client,
|
||||
"lock:caller-cancel",
|
||||
time.Minute,
|
||||
20*time.Second,
|
||||
func(lockCtx context.Context) error {
|
||||
ownershipCtx := redislock.OwnershipContext(lockCtx)
|
||||
cancel()
|
||||
require.ErrorIs(t, lockCtx.Err(), context.Canceled)
|
||||
select {
|
||||
case <-ownershipCtx.Done():
|
||||
t.Fatal("caller cancellation must not imply lock ownership loss")
|
||||
default:
|
||||
}
|
||||
return nil
|
||||
},
|
||||
)
|
||||
|
||||
require.ErrorIs(t, err, context.Canceled)
|
||||
}
|
||||
|
||||
func TestOwnershipContextCancelsWhenRenewalLosesOwnership(t *testing.T) {
|
||||
client, _ := newTokenLockTestClient(t)
|
||||
key := "lock:ownership-context"
|
||||
|
||||
err := redislock.WithRenewableLock(
|
||||
context.Background(),
|
||||
client,
|
||||
key,
|
||||
100*time.Millisecond,
|
||||
10*time.Millisecond,
|
||||
func(lockCtx context.Context) error {
|
||||
ownershipCtx := redislock.OwnershipContext(lockCtx)
|
||||
require.NoError(t, client.Set(context.Background(), key, "replacement", time.Minute).Err())
|
||||
select {
|
||||
case <-ownershipCtx.Done():
|
||||
return nil
|
||||
case <-time.After(time.Second):
|
||||
return errors.New("ownership context was not cancelled")
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
require.ErrorIs(t, err, redislock.ErrLockOwnershipLost)
|
||||
}
|
||||
Reference in New Issue
Block a user