From 2363f2e0a33c1790780105cc911102af742c0d37 Mon Sep 17 00:00:00 2001 From: Jet Chiang Date: Thu, 13 Aug 2026 16:17:19 -0400 Subject: [PATCH 01/10] move store test fixture to use postgres and update all comments and docs to remove redis reference Signed-off-by: Jet Chiang --- README.md | 4 +- benchmarking/locust/common/ateapi_pb2_grpc.py | 2 +- .../actoridentity/actoridentity_test.go | 1 + .../controlapi/actor_snapshot_test.go | 5 +- cmd/ateapi/internal/controlapi/actor_test.go | 3 +- cmd/ateapi/internal/controlapi/crash_test.go | 1 + .../internal/controlapi/functional_test.go | 45 ++---- cmd/ateapi/internal/controlapi/syncer_test.go | 34 ++--- .../controlapi/workflow_suspend_test.go | 16 +-- .../controlapi/workflow_testutil_test.go | 2 + .../internal/store/storecontract/contract.go | 9 +- .../internal/store/storetest/storetest.go | 114 ++++++++++++--- .../internal/workercache/workercache.go | 2 +- .../internal/workercache/workercache_test.go | 6 +- cmd/ateapi/main.go | 136 ++---------------- cmd/ateapi/main_test.go | 17 +-- .../podidentitysigner/podidentitysigner.go | 2 +- .../servicednssigner/servicednssigner.go | 2 +- docs/architecture.md | 4 +- docs/code-style-guide.md | 2 +- docs/dev/valkey-direct-access.md | 9 -- docs/roadmap.md | 3 +- docs/threat-model.md | 2 +- pkg/proto/ateapipb/ateapi.proto | 2 +- pkg/proto/ateapipb/ateapi_grpc.pb.go | 4 +- 25 files changed, 172 insertions(+), 255 deletions(-) delete mode 100644 docs/dev/valkey-direct-access.md diff --git a/README.md b/README.md index 56151941a..4d0f3a5dd 100644 --- a/README.md +++ b/README.md @@ -91,7 +91,7 @@ To quickly set up the complete environment: # create cluster and local registry hack/create-kind-cluster.sh -# install ate, valkey, rustfs +# install ate, PostgreSQL, rustfs hack/install-ate-kind.sh --deploy-ate-system # install counter demo @@ -128,7 +128,7 @@ curl -X POST -H "Host: my-counter-1.demo.actors.resources.substrate.ate.dev" -i gcloud auth application-default login --project=${PROJECT_ID} ``` -3. Provision the required GCP resources (GKE cluster, Redis, GCS, and IAM bindings): +3. Provision the required GCP resources (GKE cluster, GCS, and IAM bindings): ```bash go run ./tools/setup-gcp bootstrap ``` diff --git a/benchmarking/locust/common/ateapi_pb2_grpc.py b/benchmarking/locust/common/ateapi_pb2_grpc.py index efaba37cf..b4a33fe48 100644 --- a/benchmarking/locust/common/ateapi_pb2_grpc.py +++ b/benchmarking/locust/common/ateapi_pb2_grpc.py @@ -292,7 +292,7 @@ def ListActors(self, request, context): raise NotImplementedError('Method not implemented!') def CreateAtespace(self, request, context): - """Create a new Atespace. Substrate-native, stored in Redis. + """Create a new Atespace. Substrate-native, stored in PostgreSQL. """ context.set_code(grpc.StatusCode.UNIMPLEMENTED) context.set_details('Method not implemented!') diff --git a/cmd/ateapi/internal/actoridentity/actoridentity_test.go b/cmd/ateapi/internal/actoridentity/actoridentity_test.go index e91420d38..3dd56f448 100644 --- a/cmd/ateapi/internal/actoridentity/actoridentity_test.go +++ b/cmd/ateapi/internal/actoridentity/actoridentity_test.go @@ -243,6 +243,7 @@ func seedActor(t *testing.T, ctx context.Context, st store.Interface, f actorFix t.Helper() actorRef := resources.ActorRef{Atespace: testAtespace, Name: testActorName} + storetest.MustCreateAtespace(t, ctx, st, actorRef.Atespace) actor := &ateapipb.Actor{ Metadata: &ateapipb.ResourceMetadata{Atespace: actorRef.Atespace, Name: actorRef.Name}, Status: f.status, diff --git a/cmd/ateapi/internal/controlapi/actor_snapshot_test.go b/cmd/ateapi/internal/controlapi/actor_snapshot_test.go index 0193adeed..3e5435d0a 100644 --- a/cmd/ateapi/internal/controlapi/actor_snapshot_test.go +++ b/cmd/ateapi/internal/controlapi/actor_snapshot_test.go @@ -353,13 +353,14 @@ func TestCreateActorSnapshotTag_RejectsUnsetScope(t *testing.T) { } // serviceWithActorSnapshotTag seeds an ActorSnapshot and a tag pointing at it -// in a miniredis-backed store, and returns a Service over it. +// in a PostgreSQL-backed store, and returns a Service over it. func serviceWithActorSnapshotTag(t *testing.T, tag *ateapipb.ActorSnapshotTag) (*Service, *ateapipb.ActorSnapshotTag) { t.Helper() persistence, cleanup := storetest.SetupTestStore(t) t.Cleanup(cleanup) atespace, name := tag.GetMetadata().GetAtespace(), tag.GetMetadata().GetName() + storetest.MustCreateAtespace(t, context.Background(), persistence, atespace) snapshot, err := persistence.CreateActorSnapshot(context.Background(), &ateapipb.ActorSnapshot{ Metadata: &ateapipb.ResourceMetadata{Atespace: atespace, Name: "snapshot-" + name}, SnapshotUri: "gs://my-bucket/snapshots/" + atespace + "/snapshot-" + name, @@ -381,6 +382,7 @@ func TestUpdateActorSnapshotTag_DeleteRecreateRace(t *testing.T) { ctx := context.Background() persistence, cleanup := storetest.SetupTestStore(t) t.Cleanup(cleanup) + storetest.MustCreateAtespace(t, ctx, persistence, testAtespace) for _, name := range []string{"snapshot-1", "snapshot-2"} { if _, err := persistence.CreateActorSnapshot(ctx, &ateapipb.ActorSnapshot{ @@ -460,6 +462,7 @@ func TestUpdateActorSnapshotTag_ConcurrentUnguardedUpdate(t *testing.T) { ctx := context.Background() persistence, cleanup := storetest.SetupTestStore(t) t.Cleanup(cleanup) + storetest.MustCreateAtespace(t, ctx, persistence, testAtespace) if _, err := persistence.CreateActorSnapshot(ctx, &ateapipb.ActorSnapshot{ Metadata: &ateapipb.ResourceMetadata{Atespace: testAtespace, Name: "snapshot-1"}, diff --git a/cmd/ateapi/internal/controlapi/actor_test.go b/cmd/ateapi/internal/controlapi/actor_test.go index 809d7e25b..e40296282 100644 --- a/cmd/ateapi/internal/controlapi/actor_test.go +++ b/cmd/ateapi/internal/controlapi/actor_test.go @@ -753,12 +753,13 @@ func withSelector(labels map[string]string) func(*ateapipb.UpdateActorRequest) { } } -// serviceWithActor seeds one actor in a miniredis-backed store and returns a +// serviceWithActor seeds one actor in a PostgreSQL-backed store and returns a // Service over it. func serviceWithActor(t *testing.T, actor *ateapipb.Actor) (*Service, *ateapipb.Actor) { t.Helper() persistence, cleanup := storetest.SetupTestStore(t) t.Cleanup(cleanup) + storetest.MustCreateAtespace(t, context.Background(), persistence, actor.GetMetadata().GetAtespace()) created, err := persistence.CreateActor(context.Background(), actor) if err != nil { diff --git a/cmd/ateapi/internal/controlapi/crash_test.go b/cmd/ateapi/internal/controlapi/crash_test.go index 620981e87..81b7d6132 100644 --- a/cmd/ateapi/internal/controlapi/crash_test.go +++ b/cmd/ateapi/internal/controlapi/crash_test.go @@ -36,6 +36,7 @@ import ( // tests can assert they are cleared when the actor crashes. func seedActor(t *testing.T, ctx context.Context, st store.Interface, actorRef resources.ActorRef) { t.Helper() + storetest.MustCreateAtespace(t, ctx, st, actorRef.Atespace) if _, err := st.CreateActor(ctx, &ateapipb.Actor{ Metadata: &ateapipb.ResourceMetadata{Name: actorRef.Name, Atespace: actorRef.Atespace}, Status: ateapipb.Actor_STATUS_RUNNING, diff --git a/cmd/ateapi/internal/controlapi/functional_test.go b/cmd/ateapi/internal/controlapi/functional_test.go index 0087b0da1..52da545c8 100644 --- a/cmd/ateapi/internal/controlapi/functional_test.go +++ b/cmd/ateapi/internal/controlapi/functional_test.go @@ -26,7 +26,8 @@ import ( "testing" "time" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/store/ateredis" + "github.com/agent-substrate/substrate/cmd/ateapi/internal/store" + "github.com/agent-substrate/substrate/cmd/ateapi/internal/store/storetest" "github.com/agent-substrate/substrate/cmd/ateapi/internal/workercache" "github.com/agent-substrate/substrate/internal/ateinterceptors" "github.com/agent-substrate/substrate/internal/proto/ateletpb" @@ -38,10 +39,8 @@ import ( "github.com/agent-substrate/substrate/pkg/client/informers/externalversions" listersv1alpha1 "github.com/agent-substrate/substrate/pkg/client/listers/api/v1alpha1" "github.com/agent-substrate/substrate/pkg/proto/ateapipb" - "github.com/alicebob/miniredis/v2" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" - "github.com/redis/go-redis/v9" sdkmetric "go.opentelemetry.io/otel/sdk/metric" "google.golang.org/grpc" "google.golang.org/grpc/codes" @@ -269,12 +268,11 @@ func (f *FakeAteletServer) lastRestoreRequest() *ateletpb.RestoreRequest { } type testContext struct { - mr *miniredis.Miniredis service *Service client ateapipb.ControlClient k8sClient kubernetes.Interface substrateClient versioned.Interface - persistence *ateredis.Persistence + persistence store.Interface workerCache *workercache.Cache fakeAtelet *FakeAteletServer cleanup func() @@ -286,27 +284,17 @@ type testContext struct { // setupTest sets up a fully isolated test environment. func setupTest(t *testing.T, ns string) *testContext { t.Helper() - // 1. Start Miniredis - mr, err := miniredis.Run() - if err != nil { - t.Fatalf("failed to start miniredis: %v", err) - } - - rdb := redis.NewClusterClient(&redis.ClusterOptions{ - Addrs: []string{mr.Addr()}, - }) - persistence := ateredis.NewPersistence(rdb) + // 1. Start an isolated PostgreSQL-backed store. + persistence, _ := storetest.SetupTestStore(t) // 2. Initialize Clientsets using global cfg k8sClient, err := kubernetes.NewForConfig(cfg) if err != nil { - mr.Close() t.Fatalf("failed to create k8s clientset: %v", err) } substrateClient, err := versioned.NewForConfig(cfg) if err != nil { - mr.Close() t.Fatalf("failed to create substrate clientset: %v", err) } @@ -341,7 +329,6 @@ func setupTest(t *testing.T, ns string) *testContext { wc := workercache.New(persistence, 5*time.Minute) if err := wc.Start(ctx); err != nil { cancel() - mr.Close() t.Fatalf("failed to start worker cache: %v", err) } @@ -355,7 +342,6 @@ func setupTest(t *testing.T, ns string) *testContext { instruments, err := NewInstruments(sdkmetric.NewMeterProvider(sdkmetric.WithReader(sdkmetric.NewManualReader())).Meter("ateapi")) if err != nil { cancel() - mr.Close() t.Fatalf("failed to create metric instruments: %v", err) } mockPlugin := volume.NewMockVolumePlugin() @@ -375,7 +361,6 @@ func setupTest(t *testing.T, ns string) *testContext { lis, err := net.Listen("tcp", "localhost:0") if err != nil { cancel() - mr.Close() t.Fatalf("failed to listen: %v", err) } @@ -389,7 +374,6 @@ func setupTest(t *testing.T, ns string) *testContext { if err != nil { grpcServer.Stop() cancel() - mr.Close() t.Fatalf("failed to connect: %v", err) } @@ -406,7 +390,6 @@ func setupTest(t *testing.T, ns string) *testContext { conn.Close() grpcServer.Stop() cancel() - mr.Close() t.Fatalf("failed to create namespace %s: %v", ns, err) } @@ -415,7 +398,6 @@ func setupTest(t *testing.T, ns string) *testContext { conn.Close() grpcServer.Stop() cancel() - mr.Close() t.Fatalf("failed to seed test atespace %q: %v", testAtespace, err) } @@ -423,12 +405,9 @@ func setupTest(t *testing.T, ns string) *testContext { conn.Close() grpcServer.Stop() cancel() - rdb.Close() - mr.Close() } return &testContext{ - mr: mr, service: service, client: client, k8sClient: k8sClient, @@ -1646,11 +1625,11 @@ func TestListActors_Pagination(t *testing.T) { } } -// TestListWorkers tests that workers mirrored to Redis are listed. +// TestListWorkers tests that workers mirrored to the store are listed. // Workflow: // 1. Creates a mock WorkerPool in Kubernetes. // 2. Creates a mock worker Pod in Kubernetes belonging to that pool. -// 3. Waits for the background WorkerPoolSyncer to mirror it to Redis. +// 3. Waits for the background WorkerPoolSyncer to mirror it to the store. // 4. Calls ListWorkers RPC. // 5. Verifies that the worker appears in the response. func TestListWorkers(t *testing.T) { @@ -1697,7 +1676,7 @@ func TestListWorkers(t *testing.T) { // 1. Creates a mock ActorTemplate. // 2. Creates a mock Atelet Pod in 'ate-system' namespace on 'node1'. // 3. Creates a mock worker Pod in the test namespace on 'node1'. -// 4. Waits for the WorkerPoolSyncer to mirror the worker to Redis. +// 4. Waits for the WorkerPoolSyncer to mirror the worker to the store. // 5. Creates an actor (starts as SUSPENDED). // 6. Calls ResumeActor RPC. // 7. Verifies that the fake Atelet received the Restore call. @@ -2020,7 +1999,7 @@ func TestResumeActor_Reentrancy(t *testing.T) { t.Fatalf("expected ResumeActor to fail due to atelet error") } - // Verify actor state is RESUMING in Redis! + // Verify actor state is RESUMING in the store. actor, err := tc.persistence.GetActor(context.Background(), resources.ActorRef{Atespace: testAtespace, Name: name}) if err != nil { t.Fatalf("failed to get actor from store: %v", err) @@ -2059,7 +2038,7 @@ func TestResumeActor_Reentrancy(t *testing.T) { // 1. Creates a mock ActorTemplate. // 2. Creates a mock Atelet Pod on 'node1'. // 3. Creates a mock worker Pod on 'node1'. -// 4. Waits for the WorkerPoolSyncer to mirror the worker to Redis. +// 4. Waits for the WorkerPoolSyncer to mirror the worker to the store. // 5. Creates an actor. // 6. Calls ResumeActor to transition it to RUNNING. // 7. Calls SuspendActor RPC. @@ -2252,7 +2231,7 @@ func TestSuspendActor(t *testing.T) { // 1. Creates a mock ActorTemplate. // 2. Creates a mock Atelet Pod on 'node1'. // 3. Creates a mock worker Pod on 'node1'. -// 4. Waits for the WorkerPoolSyncer to mirror the worker to Redis. +// 4. Waits for the WorkerPoolSyncer to mirror the worker to the store. // 5. Creates an actor. // 6. Calls ResumeActor to transition it to RUNNING. // 7. Calls PauseActor RPC. @@ -3114,7 +3093,7 @@ func TestSuspendActor_DanglingWorker(t *testing.T) { t.Errorf("expected FailedPrecondition error, got %v", err) } - // 4. Verify it becomes CRASHED in Redis + // 4. Verify it becomes CRASHED in the store. getResp, err := tc.client.GetActor(context.Background(), &ateapipb.GetActorRequest{ Actor: &ateapipb.ObjectRef{Atespace: testAtespace, Name: name}, }) diff --git a/cmd/ateapi/internal/controlapi/syncer_test.go b/cmd/ateapi/internal/controlapi/syncer_test.go index 917ef0299..32c51fff9 100644 --- a/cmd/ateapi/internal/controlapi/syncer_test.go +++ b/cmd/ateapi/internal/controlapi/syncer_test.go @@ -41,7 +41,7 @@ import ( "k8s.io/client-go/kubernetes/fake" ) -// setupSyncerTest sets up a real store with fake Redis and a fake K8s client with informer. +// setupSyncerTest sets up a real PostgreSQL store and a fake K8s client with informer. func setupSyncerTest(t *testing.T, ctx context.Context, initPools ...*atev1alpha1.WorkerPool) (store.Interface, *fake.Clientset, *atefake.Clientset, func()) { persistence, fakeK8s, fakeAte, _, cleanup := setupSyncerTestWithStore(t, ctx, nil, initPools...) return persistence, fakeK8s, fakeAte, cleanup @@ -99,12 +99,12 @@ func TestSyncer_Lifecycle(t *testing.T) { persistence, fakeK8s, _, cleanup := setupSyncerTest(t, ctx, pool) defer func() { - // Stop syncer before closing store to prevent panics on closed miniredis. + // Stop syncer before closing the store. cancel() cleanup() }() - // 1. Verify no workers in Redis initially + // 1. Verify no workers in the store initially. workers, err := persistence.ListWorkers(context.Background(), store.ListOptions{PageSize: 1000}) if err != nil { t.Fatalf("failed to list workers: %v", err) @@ -138,7 +138,7 @@ func TestSyncer_Lifecycle(t *testing.T) { err = wait.PollUntilContextTimeout(context.Background(), 50*time.Millisecond, 500*time.Millisecond, true, func(ctx context.Context) (bool, error) { _, err := persistence.GetWorker(ctx, ns, poolName, podName) if err == nil { - return false, fmt.Errorf("worker unexpectedly found in Redis") + return false, fmt.Errorf("worker unexpectedly found in store") } if !errors.Is(err, store.ErrNotFound) { return false, err @@ -184,7 +184,7 @@ func TestSyncer_Lifecycle(t *testing.T) { return true, nil }) if err != nil { - t.Fatalf("Worker not found in Redis after update: %v", err) + t.Fatalf("Worker not found in store after update: %v", err) } // 8. Delete it @@ -205,7 +205,7 @@ func TestSyncer_Lifecycle(t *testing.T) { return false, nil }) if err != nil { - t.Fatalf("Worker still found in Redis after deletion: %v", err) + t.Fatalf("Worker still found in store after deletion: %v", err) } } @@ -226,7 +226,7 @@ func TestSyncer_DeleteBoundWorker_ClearsActor(t *testing.T) { persistence, fakeK8s, _, cleanup := setupSyncerTest(t, ctx, workerPool) defer func() { - // Stop syncer before closing store to prevent panics on closed miniredis. + // Stop syncer before closing the store. cancel() cleanup() }() @@ -328,7 +328,7 @@ func TestSyncer_OmittedFields(t *testing.T) { persistence, fakeK8s, _, cleanup := setupSyncerTest(t, ctx, pool) defer func() { - // Stop syncer before closing store to prevent panics on closed miniredis. + // Stop syncer before closing the store. cancel() cleanup() }() @@ -359,7 +359,7 @@ func TestSyncer_OmittedFields(t *testing.T) { t.Fatalf("failed to create pod: %v", err) } - // Verify that it is created in Redis with empty SandboxClass and empty Labels + // Verify that it is created in the store with empty SandboxClass and empty Labels. err = wait.PollUntilContextTimeout(context.Background(), 100*time.Millisecond, 2*time.Second, true, func(ctx context.Context) (bool, error) { w, err := persistence.GetWorker(ctx, ns, poolName, podName) if err != nil { @@ -862,7 +862,7 @@ func TestSyncer_UpdateWorker_RetryOnVersionConflict(t *testing.T) { return cs }, pool) defer func() { - // Stop syncer before closing store to prevent panics on closed miniredis. + // Stop syncer before closing the store. cancel() cleanup() }() @@ -928,7 +928,7 @@ func TestSyncer_UpdateWorker_RetryOnVersionConflict(t *testing.T) { t.Fatalf("pool informer cache failed to update: %v", err) } - // Configure conflictStore to inject a concurrent version bump in Redis when the syncer calls UpdateWorker. + // Configure conflictStore to inject a concurrent version bump when the syncer calls UpdateWorker. cs.onUpdate = func(c context.Context, w *ateapipb.Worker) { if cw, err := cs.Interface.GetWorker(c, ns, poolName, podName); err == nil { cw.NodeName = "node2" @@ -938,7 +938,7 @@ func TestSyncer_UpdateWorker_RetryOnVersionConflict(t *testing.T) { // Touch the pod ONCE in K8s so the syncer reconciles it. The first reconcile's // UpdateWorker hits ErrVersionConflict (injected by conflictStore), which requeues - // the key with backoff; the retry re-fetches the latest version from Redis. + // the record with backoff; the retry re-fetches the latest version from the store. updatedPod, err := fakeK8s.CoreV1().Pods(ns).Get(context.Background(), podName, metav1.GetOptions{}) if err != nil { t.Fatalf("failed to get pod: %v", err) @@ -951,7 +951,7 @@ func TestSyncer_UpdateWorker_RetryOnVersionConflict(t *testing.T) { t.Fatalf("failed to update pod: %v", err) } - // Verify that the worker in Redis eventually gets updated to the new SandboxClass despite the version conflict. + // Verify that the worker eventually gets updated to the new SandboxClass despite the version conflict. err = wait.PollUntilContextTimeout(context.Background(), 100*time.Millisecond, 5*time.Second, true, func(ctx context.Context) (bool, error) { w, err := persistence.GetWorker(ctx, ns, poolName, podName) if err != nil { @@ -979,7 +979,7 @@ func TestSyncer_RequeueOnMissingWorkerPool(t *testing.T) { persistence, fakeK8s, fakeAte, syncer, cleanup := setupSyncerTestWithStore(t, ctx, nil) // no pools yet defer func() { - // Stop syncer before closing store to prevent panics on closed miniredis. + // Stop syncer before closing the store. cancel() cleanup() }() @@ -1066,7 +1066,7 @@ func TestSyncer_SoftDelete_ViaInformer(t *testing.T) { persistence, fakeK8s, _, cleanup := setupSyncerTest(t, ctx, pool) defer func() { - // Stop syncer before closing store to prevent panics on closed miniredis. + // Stop syncer before closing the store. cancel() cleanup() }() @@ -1143,7 +1143,7 @@ func TestSyncer_PodRecreatedWithNewUID(t *testing.T) { persistence, fakeK8s, _, cleanup := setupSyncerTest(t, ctx, pool) defer func() { - // Stop syncer before closing store to prevent panics on closed miniredis. + // Stop syncer before closing the store. cancel() cleanup() }() @@ -1243,7 +1243,7 @@ func TestSyncer_DeleteNeverEligiblePod(t *testing.T) { persistence, fakeK8s, _, cleanup := setupSyncerTest(t, ctx, pool) defer func() { - // Stop syncer before closing store to prevent panics on closed miniredis. + // Stop syncer before closing the store. cancel() cleanup() }() diff --git a/cmd/ateapi/internal/controlapi/workflow_suspend_test.go b/cmd/ateapi/internal/controlapi/workflow_suspend_test.go index 38c42c318..e15e5aff7 100644 --- a/cmd/ateapi/internal/controlapi/workflow_suspend_test.go +++ b/cmd/ateapi/internal/controlapi/workflow_suspend_test.go @@ -20,14 +20,11 @@ import ( "testing" "github.com/agent-substrate/substrate/cmd/ateapi/internal/store" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/store/ateredis" "github.com/agent-substrate/substrate/cmd/ateapi/internal/store/storetest" "github.com/agent-substrate/substrate/internal/resources" atev1alpha1 "github.com/agent-substrate/substrate/pkg/api/v1alpha1" listersv1alpha1 "github.com/agent-substrate/substrate/pkg/client/listers/api/v1alpha1" "github.com/agent-substrate/substrate/pkg/proto/ateapipb" - "github.com/alicebob/miniredis/v2" - "github.com/redis/go-redis/v9" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -216,17 +213,10 @@ func TestSuspendActor_CrashesWhenSuspendingActorMissingWorkerPod(t *testing.T) { } } -// newTestPersistence returns a store backed by a throwaway miniredis. +// newTestPersistence returns an isolated PostgreSQL-backed store. func newTestPersistence(t *testing.T) store.Interface { - t.Helper() - mr, err := miniredis.Run() - if err != nil { - t.Fatalf("failed to start miniredis: %v", err) - } - t.Cleanup(mr.Close) - rdb := redis.NewClusterClient(&redis.ClusterOptions{Addrs: []string{mr.Addr()}}) - t.Cleanup(func() { rdb.Close() }) //nolint:errcheck // test cleanup - return ateredis.NewPersistence(rdb) + persistence, _ := storetest.SetupTestStore(t) + return persistence } // newDanglingDialer returns a dialer whose informer cache has no pods, so diff --git a/cmd/ateapi/internal/controlapi/workflow_testutil_test.go b/cmd/ateapi/internal/controlapi/workflow_testutil_test.go index b0afec726..0452675b3 100644 --- a/cmd/ateapi/internal/controlapi/workflow_testutil_test.go +++ b/cmd/ateapi/internal/controlapi/workflow_testutil_test.go @@ -20,6 +20,7 @@ import ( "testing" "github.com/agent-substrate/substrate/cmd/ateapi/internal/store" + "github.com/agent-substrate/substrate/cmd/ateapi/internal/store/storetest" "github.com/agent-substrate/substrate/internal/resources" atev1alpha1 "github.com/agent-substrate/substrate/pkg/api/v1alpha1" listersv1alpha1 "github.com/agent-substrate/substrate/pkg/client/listers/api/v1alpha1" @@ -59,6 +60,7 @@ func seedWorkflowActor(t *testing.T, ctx context.Context, st store.Interface, ac for _, opt := range opts { opt(actor) } + storetest.MustCreateAtespace(t, ctx, st, actorRef.Atespace) if _, err := st.CreateActor(ctx, actor); err != nil { t.Fatalf("seed actor: %v", err) } diff --git a/cmd/ateapi/internal/store/storecontract/contract.go b/cmd/ateapi/internal/store/storecontract/contract.go index fe3855a8f..429a6f4b4 100644 --- a/cmd/ateapi/internal/store/storecontract/contract.go +++ b/cmd/ateapi/internal/store/storecontract/contract.go @@ -61,9 +61,7 @@ func newTestActorTemplateVersion(atespace, name, template string) *ateapipb.Acto } // mustCreateAtespace creates the atespace an actor test is about to populate. -// Backends that enforce the actor->atespace foreign key (atepg) reject -// CreateActor for a nonexistent atespace, so every actor test needs a real -// parent atespace even though ateredis doesn't check. +// The PostgreSQL store enforces the actor->atespace foreign key. func mustCreateAtespace(t *testing.T, s store.Interface, name string) { t.Helper() if _, err := s.CreateAtespace(context.Background(), newTestAtespace(name)); err != nil { @@ -97,9 +95,8 @@ func receiveEvent(t *testing.T, ch <-chan store.WorkerEvent) store.WorkerEvent { // against a fresh store.Interface built by setup for each subtest. setup is // responsible for its own cleanup (e.g. via t.Cleanup). // -// Backend-specific behavior (e.g. ateredis's multi-shard pagination, atepg's -// foreign-key races and transactional notifications) is NOT covered here; see -// each backend's own test file for that. +// PostgreSQL-specific behavior such as foreign-key races and transactional +// notifications is not covered here; see atepg's own test file for that. func RunContractTests(t *testing.T, setup func(t *testing.T) store.Interface) { runActorContractTests(t, setup) runWorkerContractTests(t, setup) diff --git a/cmd/ateapi/internal/store/storetest/storetest.go b/cmd/ateapi/internal/store/storetest/storetest.go index e92f3e5f5..768f5e869 100644 --- a/cmd/ateapi/internal/store/storetest/storetest.go +++ b/cmd/ateapi/internal/store/storetest/storetest.go @@ -12,37 +12,119 @@ // See the License for the specific language governing permissions and // limitations under the License. +// Package storetest provides isolated PostgreSQL-backed stores for tests. package storetest import ( + "context" + "errors" + "fmt" + "sync" + "sync/atomic" "testing" + "time" "github.com/agent-substrate/substrate/cmd/ateapi/internal/store" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/store/ateredis" - "github.com/alicebob/miniredis/v2" - "github.com/redis/go-redis/v9" + "github.com/agent-substrate/substrate/cmd/ateapi/internal/store/atepg" + "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "github.com/jackc/pgx/v5/pgxpool" + "github.com/testcontainers/testcontainers-go/modules/postgres" ) -// SetupTestStore starts a miniredis server and returns a real store implementation -// backed by it, along with a cleanup function. +var ( + containerOnce sync.Once + adminPool *pgxpool.Pool + containerPG *postgres.PostgresContainer + containerErr error + databaseCount atomic.Uint64 +) + +// SetupTestStore returns a real PostgreSQL-backed store with a database unique +// to this test. A shared container keeps this suitable for packages that run +// subtests in parallel; databases are dropped during cleanup. func SetupTestStore(t *testing.T) (store.Interface, func()) { t.Helper() + return SetupPostgresPersistence(t), func() {} +} - mr, err := miniredis.Run() - if err != nil { - t.Fatalf("failed to start miniredis: %v", err) +// SetupPostgresPersistence returns an isolated atepg persistence instance. +func SetupPostgresPersistence(t *testing.T) *atepg.Persistence { + t.Helper() + ctx := context.Background() + admin := requireAdminPool(t) + databaseName := fmt.Sprintf("ateapi_test_%d", databaseCount.Add(1)) + if _, err := admin.Exec(ctx, "CREATE DATABASE "+databaseName); err != nil { + t.Fatalf("creating PostgreSQL test database: %v", err) } - rdb := redis.NewClusterClient(&redis.ClusterOptions{ - Addrs: []string{mr.Addr()}, + config := admin.Config().Copy() + config.ConnConfig.Database = databaseName + pool, err := pgxpool.NewWithConfig(ctx, config) + if err != nil { + t.Fatalf("connecting to PostgreSQL test database: %v", err) + } + persistence, err := atepg.NewPersistence(ctx, pool) + if err != nil { + pool.Close() + t.Fatalf("creating PostgreSQL persistence: %v", err) + } + t.Cleanup(func() { + pool.Close() + if _, err := admin.Exec(context.Background(), "DROP DATABASE "+databaseName); err != nil { + t.Errorf("dropping PostgreSQL test database: %v", err) + } }) + return persistence +} - persistence := ateredis.NewPersistence(rdb) - - cleanup := func() { - rdb.Close() - mr.Close() +// MustCreateAtespace creates name unless it already exists. PostgreSQL enforces +// the parent relationship for actor and snapshot records, so test fixtures use +// this before seeding those resources. +func MustCreateAtespace(t *testing.T, ctx context.Context, s store.Interface, name string) { + t.Helper() + _, err := s.CreateAtespace(ctx, &ateapipb.Atespace{Metadata: &ateapipb.ResourceMetadata{Name: name}}) + if err != nil && !errors.Is(err, store.ErrAlreadyExists) { + t.Fatalf("creating test atespace %q: %v", name, err) } +} - return persistence, cleanup +func requireAdminPool(t *testing.T) *pgxpool.Pool { + t.Helper() + containerOnce.Do(func() { + ctx := context.Background() + container, err := postgres.Run(ctx, "postgres:18-alpine", + postgres.WithDatabase("postgres"), + postgres.WithUsername("postgres"), + postgres.WithPassword("postgres"), + ) + if err != nil { + containerErr = err + return + } + containerPG = container + dsn, err := container.ConnectionString(ctx, "sslmode=disable") + if err != nil { + containerErr = err + return + } + adminPool, containerErr = pgxpool.New(ctx, dsn) + if containerErr != nil { + return + } + var pingErr error + for i := 0; i < 30; i++ { + pingErr = adminPool.Ping(ctx) + if pingErr == nil { + return + } + time.Sleep(500 * time.Millisecond) + } + adminPool.Close() + adminPool = nil + containerErr = fmt.Errorf("pinging PostgreSQL testcontainer after retries: %w", pingErr) + }) + if containerErr != nil { + t.Skipf("PostgreSQL testcontainer unavailable (requires Docker): %v", containerErr) + } + return adminPool } diff --git a/cmd/ateapi/internal/workercache/workercache.go b/cmd/ateapi/internal/workercache/workercache.go index 25846e4f8..d2175e904 100644 --- a/cmd/ateapi/internal/workercache/workercache.go +++ b/cmd/ateapi/internal/workercache/workercache.go @@ -37,7 +37,7 @@ const relistPageSize = 1000 // Cache maintains an in-memory snapshot of all workers. // // TODO: add metrics — at minimum a gauge for worker count, a counter for -// resync events, and a counter for failed PUBLISH operations (in ateredis). +// resync events, and a counter for failed worker-watch notifications. type Cache struct { store store.Interface relistInterval time.Duration diff --git a/cmd/ateapi/internal/workercache/workercache_test.go b/cmd/ateapi/internal/workercache/workercache_test.go index 09d77d018..14024866e 100644 --- a/cmd/ateapi/internal/workercache/workercache_test.go +++ b/cmd/ateapi/internal/workercache/workercache_test.go @@ -240,7 +240,7 @@ func TestCache_MultipleDisconnects(t *testing.T) { func TestCache_WatchClosedOnListWorkersFailure(t *testing.T) { fs := newFakeStore() - fs.listErr = errors.New("valkey unavailable") + fs.listErr = errors.New("store unavailable") c := workercache.New(fs, time.Hour) if err := c.Start(t.Context()); err == nil { @@ -358,9 +358,9 @@ func TestCache_Relist_FailureIsNonFatal(t *testing.T) { t.Fatalf("Start: %v", err) } - // Make ListWorkers fail to simulate a transient Valkey error. + // Make ListWorkers fail to simulate a transient store error. fs.mu.Lock() - fs.listErr = errors.New("valkey unavailable") + fs.listErr = errors.New("store unavailable") fs.mu.Unlock() // Wait long enough for at least one relist attempt. diff --git a/cmd/ateapi/main.go b/cmd/ateapi/main.go index 5d1d3e0d5..7a1bf61bc 100644 --- a/cmd/ateapi/main.go +++ b/cmd/ateapi/main.go @@ -32,7 +32,6 @@ import ( "github.com/agent-substrate/substrate/cmd/ateapi/internal/oidcjwt" "github.com/agent-substrate/substrate/cmd/ateapi/internal/store" "github.com/agent-substrate/substrate/cmd/ateapi/internal/store/atepg" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/store/ateredis" "github.com/agent-substrate/substrate/cmd/ateapi/internal/workercache" "github.com/agent-substrate/substrate/internal/ateapiauth" "github.com/agent-substrate/substrate/internal/ateinterceptors" @@ -43,11 +42,9 @@ import ( "github.com/agent-substrate/substrate/pkg/client/clientset/versioned" "github.com/agent-substrate/substrate/pkg/client/informers/externalversions" "github.com/agent-substrate/substrate/pkg/proto/ateapipb" - "github.com/redis/go-redis/v9" "github.com/spf13/pflag" "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc" "go.opentelemetry.io/otel" - "golang.org/x/oauth2/google" "google.golang.org/grpc" "google.golang.org/grpc/credentials" "google.golang.org/grpc/keepalive" @@ -65,15 +62,9 @@ var ( metricsListenAddr = pflag.String("metrics-listen-addr", ":9090", "Address and port the prometheus metrics server should listen on.") grpcServerCredBundle = pflag.String("grpc-server-cred-bundle", "", "File with the server TLS credential bundle.") - redisClusterAddress = pflag.String("redis-cluster-address", "", "The address of the redis cluster.") - redisCACerts = pflag.String("redis-ca-certs", "", "The file that contains the CA certificate for Redis cluster.") - redisUseIAMAuth = pflag.String("redis-use-iam-auth", "true", "Whether to use Google IAM authentication for Redis/Valkey.") - redisTLSServerName = pflag.String("redis-tls-server-name", "", "The ServerName to use for Redis TLS hostname verification.") - redisClientCert = pflag.String("redis-client-cert", "", "The file containing client TLS certificate/key credential bundle for Redis/Valkey.") - authenticationConfigFile = pflag.String("authentication-config", "", "YAML file configuring trusted JWT providers.") - storeBackend = pflag.String("store-backend", "redis", "The persistence backend to use: redis|postgres.") - postgresConnectionString = pflag.String("postgres-connection-string", "", "PostgreSQL connection string (libpq DSN or URI), used when --store-backend=postgres.") + + postgresConnectionString = pflag.String("postgres-connection-string", "", "PostgreSQL connection string (libpq DSN or URI).") actorIDJWTPoolFile = pflag.String("actor-id-jwt-pool", "", "The file that contains the serialized JWT authority pool for signing actor JWTs") egressGatewayAddress = pflag.String("egress-gateway-address", "", "Address of the egress PEP. Empty disables tunneled egress.") @@ -282,11 +273,6 @@ func loadFlagsFromEnv() { flag *string env string }{ - {redisClusterAddress, "ATE_API_REDIS_ADDRESS"}, - {redisUseIAMAuth, "ATE_API_REDIS_USE_IAM_AUTH"}, - {redisTLSServerName, "ATE_API_REDIS_TLS_SERVER_NAME"}, - {redisClientCert, "ATE_API_REDIS_CLIENT_CERT"}, - {storeBackend, "ATE_API_STORE_BACKEND"}, {postgresConnectionString, "ATE_API_POSTGRES_CONNECTION_STRING"}, } for _, o := range overrides { @@ -300,13 +286,8 @@ func logFlagValues(ctx context.Context) { slog.InfoContext(ctx, "Final flag values", slog.String("grpc-listen-addr", *listenAddr), slog.String("grpc-server-cred-bundle", *grpcServerCredBundle), - slog.String("redis-cluster-address", *redisClusterAddress), - slog.String("redis-ca-certs", *redisCACerts), - slog.String("redis-use-iam-auth", *redisUseIAMAuth), - slog.String("redis-tls-server-name", *redisTLSServerName), - slog.String("redis-client-cert", *redisClientCert), slog.String("authentication-config", *authenticationConfigFile), - slog.String("store-backend", *storeBackend), + slog.String("postgres-connection-string", *postgresConnectionString), slog.String("actor-id-jwt-pool", *actorIDJWTPoolFile), slog.String("actor-id-ca-pool", *actorIDCAPoolFile), slog.String("pod-identity-ca-certs", *podIdentityCACerts), @@ -316,114 +297,17 @@ func logFlagValues(ctx context.Context) { ) } -// connectStore builds the store.Interface for the selected --store-backend. -// Startup fails if the selected backend's configuration is missing or the -// database can't be reached. +// connectStore builds the PostgreSQL-backed store.Interface. Startup fails if +// its configuration is missing or the database can't be reached. func connectStore(ctx context.Context) (store.Interface, error) { - switch *storeBackend { - case "redis": - redisClient, err := connectRedis(ctx) - if err != nil { - return nil, fmt.Errorf("setting up Redis/Valkey: %w", err) - } - return ateredis.NewPersistence(redisClient), nil - case "postgres": - if *postgresConnectionString == "" { - return nil, fmt.Errorf("--store-backend=postgres requires --postgres-connection-string") - } - persistence, err := atepg.Connect(ctx, *postgresConnectionString) - if err != nil { - return nil, fmt.Errorf("setting up PostgreSQL: %w", err) - } - return persistence, nil - default: - return nil, fmt.Errorf("unknown --store-backend %q (want redis|postgres)", *storeBackend) + if *postgresConnectionString == "" { + return nil, fmt.Errorf("--postgres-connection-string is required") } -} - -// connectRedis builds the Redis/Valkey TLS config, plumbs IAM auth if -// requested, opens the cluster client, and pings with retries. -func connectRedis(ctx context.Context) (*redis.ClusterClient, error) { - tlsConfig, err := buildRedisTLSConfig(ctx) + persistence, err := atepg.Connect(ctx, *postgresConnectionString) if err != nil { - return nil, err - } - - clusterOpts := &redis.ClusterOptions{ - Addrs: []string{*redisClusterAddress}, - TLSConfig: tlsConfig, - } - - if *redisUseIAMAuth != "false" { - creds, err := google.FindDefaultCredentials(ctx, "https://www.googleapis.com/auth/cloud-platform") - if err != nil { - return nil, fmt.Errorf("find default credentials for Redis IAM auth: %w", err) - } - tokenSource := creds.TokenSource - clusterOpts.CredentialsProvider = func() (string, string) { - tok, err := tokenSource.Token() - if err != nil { - slog.Error("Failed to fetch Redis IAM token", slog.Any("err", err)) - return "default", "" - } - return "default", tok.AccessToken - } - slog.InfoContext(ctx, "Using Google IAM authentication for Redis connection") - } else { - slog.InfoContext(ctx, "Skipping Google IAM authentication for Redis connection") - } - - client := redis.NewClusterClient(clusterOpts) - if err := pingRedisWithRetries(ctx, client); err != nil { - return nil, err - } - return client, nil -} - -func buildRedisTLSConfig(ctx context.Context) (*tls.Config, error) { - tlsConfig := &tls.Config{MinVersion: tls.VersionTLS13} - if *redisCACerts != "" { - ca, err := os.ReadFile(*redisCACerts) - if err != nil { - return nil, fmt.Errorf("read Redis CA cert: %w", err) - } - caPool := x509.NewCertPool() - if !caPool.AppendCertsFromPEM(ca) { - return nil, fmt.Errorf("parse Redis CA cert from %s", *redisCACerts) - } - tlsConfig.RootCAs = caPool - slog.InfoContext(ctx, "Using custom CA cert for Redis", slog.String("path", *redisCACerts)) - } - if *redisTLSServerName != "" { - tlsConfig.ServerName = *redisTLSServerName - slog.InfoContext(ctx, "Using custom ServerName for Redis TLS verification", slog.String("name", *redisTLSServerName)) - } - if *redisClientCert != "" { - cert, err := credbundle.Parse(*redisClientCert) - if err != nil { - return nil, fmt.Errorf("parse Redis client credential bundle: %w", err) - } - tlsConfig.Certificates = []tls.Certificate{*cert} - slog.InfoContext(ctx, "Using client TLS certificate for Redis/Valkey", slog.String("path", *redisClientCert)) - } - return tlsConfig, nil -} - -func pingRedisWithRetries(ctx context.Context, client *redis.ClusterClient) error { - var pingErr error - for i := 0; i < 30; i++ { - pingErr = client.Ping(ctx).Err() - if pingErr == nil { - return nil - } - slog.WarnContext(ctx, "Failed to connect to Redis/Valkey, retrying...", slog.Int("attempt", i+1), slog.Any("err", pingErr)) - select { - case <-ctx.Done(): - return ctx.Err() - case <-time.After(2 * time.Second): - } + return nil, fmt.Errorf("setting up PostgreSQL: %w", err) } - return fmt.Errorf("ping Redis/Valkey after 30 retries: %w", pingErr) + return persistence, nil } // newKubeClients builds the standard Kubernetes clientset and the ate diff --git a/cmd/ateapi/main_test.go b/cmd/ateapi/main_test.go index 1aa9b9db2..b5c19f1b8 100644 --- a/cmd/ateapi/main_test.go +++ b/cmd/ateapi/main_test.go @@ -20,28 +20,15 @@ import ( "testing" ) -func TestConnectStoreRejectsUnknownBackend(t *testing.T) { - oldBackend := *storeBackend - t.Cleanup(func() { *storeBackend = oldBackend }) - *storeBackend = "unknown" - - _, err := connectStore(context.Background()) - if err == nil || !strings.Contains(err.Error(), `unknown --store-backend "unknown"`) { - t.Fatalf("connectStore() error = %v, want unknown-backend error", err) - } -} - func TestConnectStoreRequiresPostgresConnectionString(t *testing.T) { - oldBackend, oldDSN := *storeBackend, *postgresConnectionString + oldDSN := *postgresConnectionString t.Cleanup(func() { - *storeBackend = oldBackend *postgresConnectionString = oldDSN }) - *storeBackend = "postgres" *postgresConnectionString = "" _, err := connectStore(context.Background()) - if err == nil || !strings.Contains(err.Error(), "requires --postgres-connection-string") { + if err == nil || !strings.Contains(err.Error(), "--postgres-connection-string is required") { t.Fatalf("connectStore() error = %v, want missing-connection-string error", err) } } diff --git a/cmd/podcertcontroller/internal/podidentitysigner/podidentitysigner.go b/cmd/podcertcontroller/internal/podidentitysigner/podidentitysigner.go index 3ea8c6688..a33e6b9ad 100644 --- a/cmd/podcertcontroller/internal/podidentitysigner/podidentitysigner.go +++ b/cmd/podcertcontroller/internal/podidentitysigner/podidentitysigner.go @@ -158,7 +158,7 @@ func (h *Impl) MakeCert(ctx context.Context, pcr *certsv1beta1.PodCertificateReq KeyUsage: x509.KeyUsageDigitalSignature, ExtKeyUsage: extKeyUsages(pod, pcr.ObjectMeta.Namespace, pcr.Spec.ServiceAccountName), // Link the leaf to its issuing CA by key id so verifiers can disambiguate - // a multi-CA trust bundle (e.g. valkey trusts both the servicedns and + // a multi-CA trust bundle when a service trusts both the servicedns and // podidentity CAs). // https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.1 AuthorityKeyId: parent.SubjectKeyId, diff --git a/cmd/podcertcontroller/internal/servicednssigner/servicednssigner.go b/cmd/podcertcontroller/internal/servicednssigner/servicednssigner.go index 1a995b20d..058de5588 100644 --- a/cmd/podcertcontroller/internal/servicednssigner/servicednssigner.go +++ b/cmd/podcertcontroller/internal/servicednssigner/servicednssigner.go @@ -171,7 +171,7 @@ func (h *Impl) MakeCert(ctx context.Context, pcr *certsv1beta1.PodCertificateReq DNSNames: dnsNames, KeyUsage: x509.KeyUsageDigitalSignature, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}, - // Link the leaf to its issuing CA by key id. Needed this for Valkey + // Link the leaf to its issuing CA by key id. Services use this // to understand which CA to use when validating a client cert. AuthorityKeyId: parent.SubjectKeyId, } diff --git a/docs/architecture.md b/docs/architecture.md index 258ac967d..ff1f6be44 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -226,7 +226,7 @@ environment definitions. These resources represent the high-frequency, ephemeral state of individual actors and workers. They are stored in a high-performance, low-latency state -store (currently ValKey/Redis) to support real-time operations. +store (PostgreSQL) to support real-time operations. * **Actor**: A specific instance of an ActorTemplate. An Actor record tracks its globally unique identifier, physical location (Worker IP), current @@ -303,7 +303,7 @@ The brain of the system. It exposes a gRPC API for the data plane and CLI to manage actor lifecycles. * **State Store**: Tracks the mapping of Actors to Workers in a - high-performance Redis store. + PostgreSQL store. * **Scheduler**: Selects a ready worker for a resumption request. diff --git a/docs/code-style-guide.md b/docs/code-style-guide.md index 77e253768..3935bb0ae 100644 --- a/docs/code-style-guide.md +++ b/docs/code-style-guide.md @@ -37,7 +37,7 @@ far from its cause. - Standard library `testing` only — no assertion or mocking frameworks. - Table-driven tests with `t.Run` subtests are the default shape. -- Prefer a real fake when one exists: `miniredis` for the store, `envtest` for +- Prefer a real test implementation when one exists: the PostgreSQL test fixture for the store, `envtest` for the Kubernetes API. Release resources with `t.Cleanup`. ## TODOs diff --git a/docs/dev/valkey-direct-access.md b/docs/dev/valkey-direct-access.md deleted file mode 100644 index 1dbf508ae..000000000 --- a/docs/dev/valkey-direct-access.md +++ /dev/null @@ -1,9 +0,0 @@ -# Accessing valkey directly - -Valkey is the state store used by `ate-api-server` to track actor and worker records. Direct access is useful for debugging state issues. - -> **Warning:** Avoid destructive commands (`FLUSHALL`, `DEL`, etc.) on a live cluster. - -To open a `valkey-cli` session: - -1. `kubectl exec -n=ate-system -it valkey-cluster-0 -- valkey-cli -h valkey-cluster-service -c --tls --cacert /etc/valkey-ca/ca.crt --cert /run/servicedns.podcert.ate.dev/credential-bundle.pem --key /run/servicedns.podcert.ate.dev/credential-bundle.pem` diff --git a/docs/roadmap.md b/docs/roadmap.md index 3d6584509..a2730f233 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -48,7 +48,6 @@ Below is a collection of finer-grained efforts which we believe align with the a ### Storage -* Decide: Is Redis/ValKey the right answer for API storage? * gVisor snapshot/resume optimizations * storage tiering (local zswap, local SSD, peer-to-peer, blob) * incremental snapshots @@ -79,7 +78,7 @@ Below is a collection of finer-grained efforts which we believe align with the a * Provisioning load test and benchmarking compute/infrastructure * Storage and visualization for benchmark results * Integrate debugging into load tests -* State Store Scale: Horizontal sharding support (via Redis Hash Tags) to enable management of 1M+ concurrent actors. +* State Store Scale: PostgreSQL scaling and partitioning support to enable management of 1M+ concurrent actors. * Disk-Only Resume Policy: Support for cost-optimized hibernation where only the filesystem state is preserved, skipping the RAM restore for stateless or "cold" start-capable agents. ### Testing diff --git a/docs/threat-model.md b/docs/threat-model.md index c0dbbba43..89aa14c87 100644 --- a/docs/threat-model.md +++ b/docs/threat-model.md @@ -46,7 +46,7 @@ Substrate is an early, fast moving product. It is full of debate and subject to * **atenet-router:** Substrate runs Envoy with an `ext_proc` external processor to handle Actor ingress. The ext_proc extracts the Actor name and Atespace from the HTTP `Host` header, calls the Substrate API to resume the Actor and obtain its current worker assignment, and selects that worker as a dynamic backend. The router then connects with mTLS to `atunnel` on worker port 443; `atunnel` validates the router identity and forwards traffic only to the Actor currently assigned to that worker. * **Object Storage:** Used to store actor snapshots. * **Filesystem support:** Container local filesystem is saved in snapshots, future integrations likely to include networked storage. -* **Substrate Database:** Currently Valkey (Redis-compatible API). The choice of backend database/interface is under active debate. +* **Substrate Database:** PostgreSQL. * **Kubernetes:** The underlying infrastructure that Substrate runs on is expected to be Kubernetes. # Threats and Mitigations diff --git a/pkg/proto/ateapipb/ateapi.proto b/pkg/proto/ateapipb/ateapi.proto index 366515607..e4f42640d 100644 --- a/pkg/proto/ateapipb/ateapi.proto +++ b/pkg/proto/ateapipb/ateapi.proto @@ -71,7 +71,7 @@ service Control { // List Actors. rpc ListActors(ListActorsRequest) returns (ListActorsResponse) {} - // Create a new Atespace. Substrate-native, stored in Redis. + // Create a new Atespace. Substrate-native, stored in PostgreSQL. rpc CreateAtespace(CreateAtespaceRequest) returns (Atespace) {} // Get an Atespace by name. diff --git a/pkg/proto/ateapipb/ateapi_grpc.pb.go b/pkg/proto/ateapipb/ateapi_grpc.pb.go index 0afe6735f..1c08b4bb8 100644 --- a/pkg/proto/ateapipb/ateapi_grpc.pb.go +++ b/pkg/proto/ateapipb/ateapi_grpc.pb.go @@ -102,7 +102,7 @@ type ControlClient interface { ListWorkers(ctx context.Context, in *ListWorkersRequest, opts ...grpc.CallOption) (*ListWorkersResponse, error) // List Actors. ListActors(ctx context.Context, in *ListActorsRequest, opts ...grpc.CallOption) (*ListActorsResponse, error) - // Create a new Atespace. Substrate-native, stored in Redis. + // Create a new Atespace. Substrate-native, stored in PostgreSQL. CreateAtespace(ctx context.Context, in *CreateAtespaceRequest, opts ...grpc.CallOption) (*Atespace, error) // Get an Atespace by name. GetAtespace(ctx context.Context, in *GetAtespaceRequest, opts ...grpc.CallOption) (*Atespace, error) @@ -461,7 +461,7 @@ type ControlServer interface { ListWorkers(context.Context, *ListWorkersRequest) (*ListWorkersResponse, error) // List Actors. ListActors(context.Context, *ListActorsRequest) (*ListActorsResponse, error) - // Create a new Atespace. Substrate-native, stored in Redis. + // Create a new Atespace. Substrate-native, stored in PostgreSQL. CreateAtespace(context.Context, *CreateAtespaceRequest) (*Atespace, error) // Get an Atespace by name. GetAtespace(context.Context, *GetAtespaceRequest) (*Atespace, error) From 2e841eaf07a7670d05ba46b0d0fb7bc30a6d96eb Mon Sep 17 00:00:00 2001 From: Jet Chiang Date: Thu, 13 Aug 2026 16:17:32 -0400 Subject: [PATCH 02/10] remove ateredis Signed-off-by: Jet Chiang --- .../internal/store/ateredis/ateredis.go | 1586 -------- .../internal/store/ateredis/ateredis_test.go | 3238 ----------------- .../internal/store/ateredis/contract_test.go | 31 - 3 files changed, 4855 deletions(-) delete mode 100644 cmd/ateapi/internal/store/ateredis/ateredis.go delete mode 100644 cmd/ateapi/internal/store/ateredis/ateredis_test.go delete mode 100644 cmd/ateapi/internal/store/ateredis/contract_test.go diff --git a/cmd/ateapi/internal/store/ateredis/ateredis.go b/cmd/ateapi/internal/store/ateredis/ateredis.go deleted file mode 100644 index a27962754..000000000 --- a/cmd/ateapi/internal/store/ateredis/ateredis.go +++ /dev/null @@ -1,1586 +0,0 @@ -// Copyright 2026 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// Package ateredis is an ate storage backend built on Redis. -// -// Actors are stored in keys of the form -// `actor::`. They are -// stored as DBActor JSON-serialized objects, which lets us manipulate them from -// Redis lua. -// -// Workers are stored in keys of the form -// `worker:::`, holding a DBWorker JSON object. -// -// Note that redis lua scripting has a restriction that informed the data design -// here -- a lua script must predeclare all keys it is going to access. It -// cannot read one key, then derive another key from the value, and read it. -// This is why we store the worker status inline in the Actor. -// -// Additionally, redis / valkey in cluster mode have a serious restriction that -// informs our data model: it is not possible for a single "action" to touch -// keys that hash to to different cluster slots. This includes lua scripts. The -// biggest implication here is that it is not possible to atomically mark an -// actor as scheduled on a worker, and the worker as busy. So we need to be -// very careful about the order in which we take these actions. -// -// Note also (but I cannot find documentation one way or another) that Redis Lua -// is not ACID --- power failure, etc may leave us with half of the effects of a -// script applied. -package ateredis - -import ( - "context" - "crypto/sha256" - "encoding/base64" - "encoding/hex" - "encoding/json" - "errors" - "fmt" - "log/slog" - "sort" - "sync" - "time" - - "github.com/agent-substrate/substrate/cmd/ateapi/internal/store" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" - "github.com/google/uuid" - "github.com/redis/go-redis/v9" - "google.golang.org/protobuf/encoding/protojson" - "google.golang.org/protobuf/proto" - "google.golang.org/protobuf/types/known/timestamppb" -) - -// globalAtespace in Substrate is represented by "". -const globalAtespace = "" - -type workerPubSubMsg struct { - Type int `json:"t"` - Worker string `json:"w"` // protojson-encoded Worker -} - -type redisClient interface { - redis.Cmdable - ForEachMaster(ctx context.Context, fn func(ctx context.Context, client *redis.Client) error) error - Watch(ctx context.Context, fn func(*redis.Tx) error, keys ...string) error - Subscribe(ctx context.Context, channels ...string) *redis.PubSub -} - -// Persistence is a service that stores information about applications in Redis. -type Persistence struct { - rdb redisClient - lockTTL time.Duration -} - -var _ store.Interface = (*Persistence)(nil) - -// NewPersistence creates a new Persistence. -func NewPersistence(redisClient *redis.ClusterClient) *Persistence { - return &Persistence{ - rdb: redisClient, - lockTTL: defaultLockTTL, - } -} - -// actorDBKey returns the Redis key an actor is stored under. The encoding is -// "actor::" and must not change: existing databases hold keys -// in this form. -func actorDBKey(actorRef resources.ActorRef) string { - return "actor:" + actorRef.Atespace + ":" + actorRef.Name -} - -// actorScanPattern returns the SCAN match pattern for listing actors. An empty -// atespace lists across all atespaces (actor:*); a non-empty atespace scopes the -// scan to that atespace (actor::*). -func actorScanPattern(atespace string) string { - if atespace == globalAtespace { - return "actor:*" - } - return "actor:" + atespace + ":*" -} - -func actorSnapshotDBKey(atespace, name string) string { - return "actor-snapshot:" + atespace + ":" + name -} - -func actorSnapshotScanPattern(atespace string) string { - if atespace == globalAtespace { - return "actor-snapshot:*" - } - return "actor-snapshot:" + atespace + ":*" -} - -func actorSnapshotTagDBKey(atespace, name string) string { - return "actor-snapshot-tag:" + atespace + ":" + name -} - -func actorSnapshotTagScanPattern(atespace string) string { - return "actor-snapshot-tag:" + atespace + ":*" -} - -func atespaceDBKey(name string) string { - return "atespace:" + name -} - -func (s *Persistence) CreateAtespace(ctx context.Context, atespace *ateapipb.Atespace) (*ateapipb.Atespace, error) { - dbKey := atespaceDBKey(atespace.GetMetadata().GetName()) - - dbAtespace := proto.Clone(atespace).(*ateapipb.Atespace) - // Atespace is global-scoped: identity is the name alone (atespace stays empty). - dbAtespace.Metadata = newCreateMetadata(globalAtespace, atespace.GetMetadata().GetName()) - - dbBytes, err := protojson.Marshal(dbAtespace) - if err != nil { - return nil, fmt.Errorf("in protojson.Marshal: %w", err) - } - ok, err := s.rdb.SetNX(ctx, dbKey, dbBytes, 0).Result() - if err != nil { - return nil, fmt.Errorf("while executing redis set: %w", err) - } - if !ok { - return nil, store.ErrAlreadyExists - } - return dbAtespace, nil -} - -func (s *Persistence) GetAtespace(ctx context.Context, name string) (*ateapipb.Atespace, error) { - dbKey := atespaceDBKey(name) - dbBytes, err := s.rdb.Get(ctx, dbKey).Bytes() - if err != nil { - if errors.Is(err, redis.Nil) { - return nil, store.ErrNotFound - } - return nil, fmt.Errorf("while getting atespace key %q: %w", dbKey, err) - } - atespace := &ateapipb.Atespace{} - if err := protojson.Unmarshal(dbBytes, atespace); err != nil { - return nil, fmt.Errorf("while unmarshaling atespace: %w", err) - } - if atespace.GetMetadata().GetName() != name { - return nil, fmt.Errorf("(impossible) mismatch between stored name and key %q", dbKey) - } - return atespace, nil -} - -// AtespaceExists reports whether the atespace object exists. This is a plain -// EXISTS check and is NOT atomic with respect to a concurrent DeleteAtespace. -func (s *Persistence) AtespaceExists(ctx context.Context, name string) (bool, error) { - n, err := s.rdb.Exists(ctx, atespaceDBKey(name)).Result() - if err != nil { - return false, fmt.Errorf("while checking atespace existence: %w", err) - } - return n > 0, nil -} - -func (s *Persistence) ListAtespaces(ctx context.Context, opts store.ListOptions) (store.ListResponse[*ateapipb.Atespace], error) { - var result []*ateapipb.Atespace - nextToken, err := s.listPage(ctx, "atespace:*", opts.PageSize, opts.PageToken, func(ctx context.Context, master *redis.Client, keys []string) (int, error) { - atespaces, err := fetchProtos(ctx, master, keys, func() *ateapipb.Atespace { return &ateapipb.Atespace{} }) - if err != nil { - return 0, err - } - result = append(result, atespaces...) - return len(atespaces), nil - }) - if err != nil { - return store.ListResponse[*ateapipb.Atespace]{}, err - } - return store.ListResponse[*ateapipb.Atespace]{Items: result, NextPageToken: nextToken}, nil -} - -// DeleteAtespace deletes an empty atespace. Returns store.ErrNotFound if the -// atespace does not exist, or store.ErrFailedPrecondition if any Actor, -// ActorSnapshotTag, ActorTemplate or ActorTemplateVersion still lives in it. -func (s *Persistence) DeleteAtespace(ctx context.Context, name string) (*ateapipb.Atespace, error) { - dbKey := atespaceDBKey(name) - - // Read first, so a missing atespace returns NotFound (not a silent no-op) and - // so we can return the deleted resource. - currentVal, err := s.rdb.Get(ctx, dbKey).Bytes() - if err != nil { - if errors.Is(err, redis.Nil) { - return nil, store.ErrNotFound - } - return nil, fmt.Errorf("while getting atespace key %q: %w", dbKey, err) - } - - deleted := &ateapipb.Atespace{} - if err := protojson.Unmarshal(currentVal, deleted); err != nil { - return nil, fmt.Errorf("in protojson.Unmarshal: %w", err) - } - - // Reject a non-empty atespace. - actors, err := s.ListActors(ctx, name, store.ListOptions{PageSize: 1}) - if err != nil { - return nil, fmt.Errorf("while checking atespace emptiness: %w", err) - } - if len(actors.Items) > 0 { - return nil, store.ErrFailedPrecondition - } - hasTags, err := s.hasMatching(ctx, actorSnapshotTagScanPattern(name)) - if err != nil { - return nil, fmt.Errorf("while checking ActorSnapshot tags: %w", err) - } - if hasTags { - return nil, store.ErrFailedPrecondition - } - hasTemplates, err := s.hasMatching(ctx, actorTemplateScanPattern(name)) - if err != nil { - return nil, fmt.Errorf("while checking ActorTemplates: %w", err) - } - if hasTemplates { - return nil, store.ErrFailedPrecondition - } - hasVersions, err := s.hasMatching(ctx, actorTemplateVersionScanPattern(name)) - if err != nil { - return nil, fmt.Errorf("while checking ActorTemplateVersions: %w", err) - } - if hasVersions { - return nil, store.ErrFailedPrecondition - } - if err := s.rdb.Del(ctx, dbKey).Err(); err != nil { - return nil, fmt.Errorf("while deleting atespace key %q: %w", dbKey, err) - } - return deleted, nil -} - -func (s *Persistence) hasMatching(ctx context.Context, pattern string) (bool, error) { - masters, err := s.getSortedMasters(ctx) - if err != nil { - return false, err - } - for _, master := range masters { - for cursor := uint64(0); ; { - keys, next, err := master.Scan(ctx, cursor, pattern, 1).Result() - if err != nil { - return false, err - } - if len(keys) > 0 { - return true, nil - } - if cursor = next; cursor == 0 { - break - } - } - } - return false, nil -} - -func actorTemplateDBKey(templateRef resources.ActorTemplateRef) string { - return "actor-template:" + templateRef.Atespace + ":" + templateRef.Name -} - -func actorTemplateScanPattern(atespace string) string { - if atespace == globalAtespace { - return "actor-template:*" - } - return "actor-template:" + atespace + ":*" -} - -func actorTemplateVersionDBKey(versionRef resources.ActorTemplateVersionRef) string { - return "actor-template-version:" + versionRef.Atespace + ":" + versionRef.Name -} - -func actorTemplateVersionScanPattern(atespace string) string { - if atespace == globalAtespace { - return "actor-template-version:*" - } - return "actor-template-version:" + atespace + ":*" -} - -func (s *Persistence) CreateActorTemplate(ctx context.Context, template *ateapipb.ActorTemplate) (*ateapipb.ActorTemplate, error) { - dbKey := actorTemplateDBKey(resources.ActorTemplateRefFromActorTemplate(template)) - - dbTemplate := proto.Clone(template).(*ateapipb.ActorTemplate) - dbTemplate.Metadata = newCreateMetadata(template.GetMetadata().GetAtespace(), template.GetMetadata().GetName()) - - dbBytes, err := protojson.Marshal(dbTemplate) - if err != nil { - return nil, fmt.Errorf("in protojson.Marshal: %w", err) - } - ok, err := s.rdb.SetNX(ctx, dbKey, dbBytes, 0).Result() - if err != nil { - return nil, fmt.Errorf("while executing redis set: %w", err) - } - if !ok { - return nil, store.ErrAlreadyExists - } - return dbTemplate, nil -} - -func (s *Persistence) GetActorTemplate(ctx context.Context, templateRef resources.ActorTemplateRef) (*ateapipb.ActorTemplate, error) { - dbKey := actorTemplateDBKey(templateRef) - dbBytes, err := s.rdb.Get(ctx, dbKey).Bytes() - if err != nil { - if errors.Is(err, redis.Nil) { - return nil, store.ErrNotFound - } - return nil, fmt.Errorf("while getting actor template key %q: %w", dbKey, err) - } - template := &ateapipb.ActorTemplate{} - if err := protojson.Unmarshal(dbBytes, template); err != nil { - return nil, fmt.Errorf("while unmarshaling actor template: %w", err) - } - if resources.ActorTemplateRefFromActorTemplate(template) != templateRef { - return nil, fmt.Errorf("(impossible) mismatch between stored identity and key %q", dbKey) - } - return template, nil -} - -// ActorTemplateExists reports whether the ActorTemplate exists. This is a -// plain EXISTS check and is NOT atomic with respect to a concurrent -// DeleteActorTemplate. -func (s *Persistence) ActorTemplateExists(ctx context.Context, templateRef resources.ActorTemplateRef) (bool, error) { - n, err := s.rdb.Exists(ctx, actorTemplateDBKey(templateRef)).Result() - if err != nil { - return false, fmt.Errorf("while checking actor template existence: %w", err) - } - return n > 0, nil -} - -// validateUpdateActorTemplateMutation reports whether a template mutation left -// the fields it does not own alone. -func validateUpdateActorTemplateMutation(storedTemplate, mutatedTemplate *ateapipb.ActorTemplate) error { - if stored, mutated := storedTemplate.GetMetadata().GetAtespace(), mutatedTemplate.GetMetadata().GetAtespace(); stored != mutated { - return fmt.Errorf("metadata.atespace is immutable: mutation changed it from %q to %q", stored, mutated) - } - if stored, mutated := storedTemplate.GetMetadata().GetName(), mutatedTemplate.GetMetadata().GetName(); stored != mutated { - return fmt.Errorf("metadata.name is immutable: mutation changed it from %q to %q", stored, mutated) - } - return nil -} - -func (s *Persistence) UpdateActorTemplate(ctx context.Context, templateRef resources.ActorTemplateRef, mutate func(*ateapipb.ActorTemplate) error) (*ateapipb.ActorTemplate, error) { - dbKey := actorTemplateDBKey(templateRef) - for range updateMaxAttempts { - var dbTemplate *ateapipb.ActorTemplate - var abortErr error - - err := s.rdb.Watch(ctx, func(tx *redis.Tx) error { - currentVal, err := tx.Get(ctx, dbKey).Bytes() - if err != nil { - if errors.Is(err, redis.Nil) { - return store.ErrNotFound - } - return fmt.Errorf("while getting actor template: %w", err) - } - - currentTemplate := &ateapipb.ActorTemplate{} - if err := protojson.Unmarshal(currentVal, currentTemplate); err != nil { - return fmt.Errorf("in protojson.Unmarshal: %w", err) - } - - // Snapshot the stored state before handing the template to mutate. - // mutate is free to edit anything it is given. - templateBeforeMutation := proto.Clone(currentTemplate).(*ateapipb.ActorTemplate) - if err := mutate(currentTemplate); err != nil { - abortErr = err - return err - } - if err := validateUpdateActorTemplateMutation(templateBeforeMutation, currentTemplate); err != nil { - abortErr = err - return err - } - // The stored metadata is authoritative; derive the next metadata - // from it, discarding whatever mutate made of it. - currentTemplate.Metadata = newUpdateMetadata(templateBeforeMutation.GetMetadata()) - - newVal, err := protojson.Marshal(currentTemplate) - if err != nil { - return fmt.Errorf("in protojson.Marshal: %w", err) - } - - if _, err := tx.TxPipelined(ctx, func(pipe redis.Pipeliner) error { - pipe.Set(ctx, dbKey, newVal, 0) - return nil - }); err != nil { - return err - } - dbTemplate = currentTemplate - return nil - }, dbKey) - - switch { - case err == nil: - return dbTemplate, nil - case abortErr != nil: - return nil, abortErr - case errors.Is(err, store.ErrNotFound): - return nil, store.ErrNotFound - case errors.Is(err, redis.TxFailedErr): - // A concurrent write landed between WATCH and EXEC, so mutate never - // saw it. Re-read and run it against the newer state. - continue - default: - return nil, fmt.Errorf("while executing update actor template transaction: %w", err) - } - } - - // Only the TxFailedErr branch continues the loop, so getting here means every - // attempt lost the race. - return nil, store.ErrVersionConflict -} - -func (s *Persistence) ListActorTemplates(ctx context.Context, atespace string, opts store.ListOptions) (store.ListResponse[*ateapipb.ActorTemplate], error) { - var result []*ateapipb.ActorTemplate - nextToken, err := s.listPage(ctx, actorTemplateScanPattern(atespace), opts.PageSize, opts.PageToken, func(ctx context.Context, master *redis.Client, keys []string) (int, error) { - templates, err := fetchProtos(ctx, master, keys, func() *ateapipb.ActorTemplate { return &ateapipb.ActorTemplate{} }) - if err != nil { - return 0, err - } - result = append(result, templates...) - return len(templates), nil - }) - if err != nil { - return store.ListResponse[*ateapipb.ActorTemplate]{}, err - } - return store.ListResponse[*ateapipb.ActorTemplate]{Items: result, NextPageToken: nextToken}, nil -} - -// DeleteActorTemplate deletes an ActorTemplate with no remaining versions. -// Returns store.ErrNotFound if the template does not exist, or -// store.ErrFailedPrecondition while any ActorTemplateVersion still names it -// as parent. -func (s *Persistence) DeleteActorTemplate(ctx context.Context, templateRef resources.ActorTemplateRef) (*ateapipb.ActorTemplate, error) { - dbKey := actorTemplateDBKey(templateRef) - - // Read first, so a missing template returns NotFound (not a silent no-op) - // and so we can return the deleted resource. - currentVal, err := s.rdb.Get(ctx, dbKey).Bytes() - if err != nil { - if errors.Is(err, redis.Nil) { - return nil, store.ErrNotFound - } - return nil, fmt.Errorf("while getting actor template key %q: %w", dbKey, err) - } - - deleted := &ateapipb.ActorTemplate{} - if err := protojson.Unmarshal(currentVal, deleted); err != nil { - return nil, fmt.Errorf("in protojson.Unmarshal: %w", err) - } - - // Reject while any version still names this template as parent. The - // parent lives in the stored value, not the key, so probe via the - // filtered list (pageSize 1 stops at the first match). - versions, err := s.ListActorTemplateVersions(ctx, globalAtespace, templateRef, store.ListOptions{PageSize: 1}) - if err != nil { - return nil, fmt.Errorf("while checking for remaining versions: %w", err) - } - if len(versions.Items) > 0 { - return nil, store.ErrFailedPrecondition - } - if err := s.rdb.Del(ctx, dbKey).Err(); err != nil { - return nil, fmt.Errorf("while deleting actor template key %q: %w", dbKey, err) - } - return deleted, nil -} - -func (s *Persistence) CreateActorTemplateVersion(ctx context.Context, atv *ateapipb.ActorTemplateVersion) (*ateapipb.ActorTemplateVersion, error) { - dbKey := actorTemplateVersionDBKey(resources.ActorTemplateVersionRefFromActorTemplateVersion(atv)) - - dbVersion := proto.Clone(atv).(*ateapipb.ActorTemplateVersion) - dbVersion.Metadata = newCreateMetadata(atv.GetMetadata().GetAtespace(), atv.GetMetadata().GetName()) - - dbBytes, err := protojson.Marshal(dbVersion) - if err != nil { - return nil, fmt.Errorf("in protojson.Marshal: %w", err) - } - ok, err := s.rdb.SetNX(ctx, dbKey, dbBytes, 0).Result() - if err != nil { - return nil, fmt.Errorf("while executing redis set: %w", err) - } - if !ok { - return nil, store.ErrAlreadyExists - } - return dbVersion, nil -} - -func (s *Persistence) GetActorTemplateVersion(ctx context.Context, versionRef resources.ActorTemplateVersionRef) (*ateapipb.ActorTemplateVersion, error) { - dbKey := actorTemplateVersionDBKey(versionRef) - dbBytes, err := s.rdb.Get(ctx, dbKey).Bytes() - if err != nil { - if errors.Is(err, redis.Nil) { - return nil, store.ErrNotFound - } - return nil, fmt.Errorf("while getting actor template version key %q: %w", dbKey, err) - } - version := &ateapipb.ActorTemplateVersion{} - if err := protojson.Unmarshal(dbBytes, version); err != nil { - return nil, fmt.Errorf("while unmarshaling actor template version: %w", err) - } - if resources.ActorTemplateVersionRefFromActorTemplateVersion(version) != versionRef { - return nil, fmt.Errorf("(impossible) mismatch between stored identity and key %q", dbKey) - } - return version, nil -} - -// ListActorTemplateVersions lists ActorTemplateVersions in an atespace (all -// atespaces when atespace is ""), filtered to one parent template when -// actorTemplateRef is non-zero. atespace scopes the versions scanned, not the -// parent: stored parent refs are fully qualified, so the filter matches the -// parent's atespace and name. -func (s *Persistence) ListActorTemplateVersions(ctx context.Context, atespace string, actorTemplateRef resources.ActorTemplateRef, opts store.ListOptions) (store.ListResponse[*ateapipb.ActorTemplateVersion], error) { - var result []*ateapipb.ActorTemplateVersion - nextToken, err := s.listPage(ctx, actorTemplateVersionScanPattern(atespace), opts.PageSize, opts.PageToken, func(ctx context.Context, master *redis.Client, keys []string) (int, error) { - versions, err := fetchProtos(ctx, master, keys, func() *ateapipb.ActorTemplateVersion { return &ateapipb.ActorTemplateVersion{} }) - if err != nil { - return 0, err - } - matched := 0 - for _, v := range versions { - if actorTemplateRef != (resources.ActorTemplateRef{}) && resources.ActorTemplateRefFromObjectRef(v.GetActorTemplate()) != actorTemplateRef { - continue - } - result = append(result, v) - matched++ - } - return matched, nil - }) - if err != nil { - return store.ListResponse[*ateapipb.ActorTemplateVersion]{}, err - } - return store.ListResponse[*ateapipb.ActorTemplateVersion]{Items: result, NextPageToken: nextToken}, nil -} - -// DeleteActorTemplateVersion deletes an ActorTemplateVersion together with -// its recorded golden snapshot, if any. Returns store.ErrNotFound if -// the version does not exist, or store.ErrFailedPrecondition while it is its -// parent's default_version_on_create. -func (s *Persistence) DeleteActorTemplateVersion(ctx context.Context, versionRef resources.ActorTemplateVersionRef) (*ateapipb.ActorTemplateVersion, error) { - dbKey := actorTemplateVersionDBKey(versionRef) - - currentVal, err := s.rdb.Get(ctx, dbKey).Bytes() - if err != nil { - if errors.Is(err, redis.Nil) { - return nil, store.ErrNotFound - } - return nil, fmt.Errorf("while getting actor template version key %q: %w", dbKey, err) - } - - deleted := &ateapipb.ActorTemplateVersion{} - if err := protojson.Unmarshal(currentVal, deleted); err != nil { - return nil, fmt.Errorf("in protojson.Unmarshal: %w", err) - } - - // Reject while the parent still names this version as its default. - parent, err := s.GetActorTemplate(ctx, resources.ActorTemplateRef{Atespace: versionRef.Atespace, Name: deleted.GetActorTemplate().GetName()}) - if err != nil && !errors.Is(err, store.ErrNotFound) { - return nil, fmt.Errorf("while getting parent actor template: %w", err) - } - if resources.ActorTemplateVersionRefFromObjectRef(parent.GetDefaultVersionOnCreate()) == versionRef { - return nil, store.ErrFailedPrecondition - } - // TODO(actor-template-versions): also reject while any Actor or - // ActorSnapshot references this version, once those resources carry - // template-version fields. - - if golden := deleted.GetGoldenSnapshot(); golden != nil { - goldenKey := actorSnapshotDBKey(golden.GetAtespace(), golden.GetName()) - if err := s.rdb.Del(ctx, goldenKey).Err(); err != nil { - return nil, fmt.Errorf("while deleting golden snapshot key %q: %w", goldenKey, err) - } - } - - if err := s.rdb.Del(ctx, dbKey).Err(); err != nil { - return nil, fmt.Errorf("while deleting actor template version key %q: %w", dbKey, err) - } - return deleted, nil -} - -func workerDBKey(namespace, poolName, podName string) string { - return "worker:" + namespace + ":" + poolName + ":" + podName -} - -func marshalWorkerEvent(eventType store.WorkerEventType, worker *ateapipb.Worker) (string, error) { - workerJSON, err := protojson.Marshal(worker) - if err != nil { - return "", fmt.Errorf("in protojson.Marshal: %w", err) - } - msg, err := json.Marshal(workerPubSubMsg{Type: int(eventType), Worker: string(workerJSON)}) - if err != nil { - return "", fmt.Errorf("in json.Marshal: %w", err) - } - return string(msg), nil -} - -func unmarshalWorkerEvent(payload string) (store.WorkerEvent, error) { - var msg workerPubSubMsg - if err := json.Unmarshal([]byte(payload), &msg); err != nil { - return store.WorkerEvent{}, fmt.Errorf("in json.Unmarshal: %w", err) - } - worker := &ateapipb.Worker{} - if err := protojson.Unmarshal([]byte(msg.Worker), worker); err != nil { - return store.WorkerEvent{}, fmt.Errorf("in protojson.Unmarshal: %w", err) - } - return store.WorkerEvent{Type: store.WorkerEventType(msg.Type), Worker: worker}, nil -} - -const workerPubSubChannel = "worker-changes" - -// subscribeConfirmTimeout bounds WatchWorkers' wait for the SUBSCRIBE -// confirmation. -const subscribeConfirmTimeout = 5 * time.Second - -func (s *Persistence) publishWorkerEvent(ctx context.Context, eventType store.WorkerEventType, worker *ateapipb.Worker) { - payload, err := marshalWorkerEvent(eventType, worker) - if err != nil { - slog.ErrorContext(ctx, "worker event marshal failed", slog.Any("err", err)) - return - } - if err := s.rdb.Publish(ctx, workerPubSubChannel, payload).Err(); err != nil { - slog.ErrorContext(ctx, "worker event publish failed", slog.Any("err", err)) - } -} - -func (s *Persistence) WatchWorkers(ctx context.Context) (*store.WorkerWatch, error) { - // watchCtx scopes the subscription's lifetime: it is cancelled either by the - // caller via WorkerWatch.Close or when the parent ctx is cancelled. - watchCtx, cancel := context.WithCancel(ctx) - pubsub := s.rdb.Subscribe(watchCtx, workerPubSubChannel) - // Subscribe sends the SUBSCRIBE command asynchronously; wait for the - // confirmation reply so that events published after WatchWorkers returns - // are guaranteed to be delivered to this subscription. - receiveCtx, receiveCancel := context.WithTimeout(watchCtx, subscribeConfirmTimeout) - defer receiveCancel() - if _, err := pubsub.Receive(receiveCtx); err != nil { - pubsub.Close() - cancel() - return nil, fmt.Errorf("while confirming worker subscription: %w", err) - } - ch := make(chan store.WorkerEvent, 128) - go func() { - defer close(ch) - defer pubsub.Close() - msgCh := pubsub.Channel() - for { - select { - case <-watchCtx.Done(): - return - case msg, ok := <-msgCh: - if !ok { - return - } - event, err := unmarshalWorkerEvent(msg.Payload) - if err != nil { - slog.ErrorContext(ctx, "worker event unmarshal failed", slog.Any("err", err)) - continue - } - select { - case ch <- event: - case <-watchCtx.Done(): - return - } - } - } - }() - return store.NewWorkerWatch(ch, cancel), nil -} - -// DebugClearAll flushes all data from Redis. -func (s *Persistence) DebugClearAll(ctx context.Context) error { - // Iterate through every Primary (Master) node in the cluster - err := s.rdb.ForEachMaster(ctx, func(ctx context.Context, master *redis.Client) error { - // Log which shard we are currently flushing (optional but helpful for debugging) - shardAddr := master.Options().Addr - fmt.Printf("Flushing shard: %s\n", shardAddr) - - // Execute the flush on this specific shard - return master.FlushAllAsync(ctx).Err() - }) - return err -} - -func (s *Persistence) GetActor(ctx context.Context, actorRef resources.ActorRef) (*ateapipb.Actor, error) { - dbKey := actorDBKey(actorRef) - - dbActorBytes, err := s.rdb.Get(ctx, dbKey).Bytes() - if err != nil { - if errors.Is(err, redis.Nil) { - return nil, store.ErrNotFound - } - return nil, fmt.Errorf("while getting actor key %q: %w", dbKey, err) - } - - actor := &ateapipb.Actor{} - if err := protojson.Unmarshal(dbActorBytes, actor); err != nil { - return nil, fmt.Errorf("while unmarshaling actor: %w", err) - } - - if resources.ActorRefFromActor(actor) != actorRef { - return nil, fmt.Errorf("(impossible) mismatch between stored name/atespace and key") - } - - return actor, nil -} - -func (s *Persistence) CreateActor(ctx context.Context, actor *ateapipb.Actor) (*ateapipb.Actor, error) { - dbKey := actorDBKey(resources.ActorRefFromActor(actor)) - - // Clone so we don't stomp the caller's copy, then attach fresh server-owned - // metadata carrying the caller-specified identity. - dbActor := proto.Clone(actor).(*ateapipb.Actor) - dbActor.Metadata = newCreateMetadata(actor.GetMetadata().GetAtespace(), actor.GetMetadata().GetName()) - - dbActorBytes, err := protojson.Marshal(dbActor) - if err != nil { - return nil, fmt.Errorf("in protojson.Marshal: %w", err) - } - - ok, err := s.rdb.SetNX(ctx, dbKey, dbActorBytes, 0).Result() - if err != nil { - return nil, fmt.Errorf("while executing redis set: %w", err) - } - if !ok { - return nil, store.ErrAlreadyExists - } - - return dbActor, nil -} - -func (s *Persistence) CreateActorSnapshot(ctx context.Context, snapshot *ateapipb.ActorSnapshot) (*ateapipb.ActorSnapshot, error) { - dbKey := actorSnapshotDBKey(snapshot.GetMetadata().GetAtespace(), snapshot.GetMetadata().GetName()) - dbSnapshot := proto.Clone(snapshot).(*ateapipb.ActorSnapshot) - dbSnapshot.Metadata = newCreateMetadata(snapshot.GetMetadata().GetAtespace(), snapshot.GetMetadata().GetName()) - b, err := protojson.Marshal(dbSnapshot) - if err != nil { - return nil, fmt.Errorf("while marshaling actor snapshot: %w", err) - } - ok, err := s.rdb.SetNX(ctx, dbKey, b, 0).Result() - if err != nil { - return nil, fmt.Errorf("while creating actor snapshot: %w", err) - } - if !ok { - return nil, store.ErrAlreadyExists - } - return dbSnapshot, nil -} - -func (s *Persistence) GetActorSnapshot(ctx context.Context, atespace, name string) (*ateapipb.ActorSnapshot, error) { - dbKey := actorSnapshotDBKey(atespace, name) - b, err := s.rdb.Get(ctx, dbKey).Bytes() - if err != nil { - if errors.Is(err, redis.Nil) { - return nil, store.ErrNotFound - } - return nil, fmt.Errorf("while getting actor snapshot key %q: %w", dbKey, err) - } - snapshot := &ateapipb.ActorSnapshot{} - if err := protojson.Unmarshal(b, snapshot); err != nil { - return nil, fmt.Errorf("while unmarshaling actor snapshot: %w", err) - } - return snapshot, nil -} - -func (s *Persistence) GetActorSnapshotTag(ctx context.Context, atespace, name string) (*ateapipb.ActorSnapshotTag, error) { - b, err := s.rdb.Get(ctx, actorSnapshotTagDBKey(atespace, name)).Bytes() - if err != nil { - if errors.Is(err, redis.Nil) { - return nil, store.ErrNotFound - } - return nil, fmt.Errorf("while getting actor snapshot tag %s/%s: %w", atespace, name, err) - } - tag := &ateapipb.ActorSnapshotTag{} - if err := protojson.Unmarshal(b, tag); err != nil { - return nil, fmt.Errorf("while unmarshaling actor snapshot tag %s/%s: %w", atespace, name, err) - } - return tag, nil -} - -func (s *Persistence) ListActorSnapshots(ctx context.Context, atespace string, opts store.ListOptions) (store.ListResponse[*ateapipb.ActorSnapshot], error) { - var result []*ateapipb.ActorSnapshot - nextToken, err := s.listPage(ctx, actorSnapshotScanPattern(atespace), opts.PageSize, opts.PageToken, func(ctx context.Context, master *redis.Client, keys []string) (int, error) { - cmds, err := master.Pipelined(ctx, func(pipe redis.Pipeliner) error { - for _, key := range keys { - pipe.Get(ctx, key) - } - return nil - }) - if err != nil && !errors.Is(err, redis.Nil) { - return 0, fmt.Errorf("while fetching actor snapshots in shard %s: %w", master.Options().Addr, err) - } - collected := 0 - for _, cmd := range cmds { - getCmd, ok := cmd.(*redis.StringCmd) - if !ok || errors.Is(getCmd.Err(), redis.Nil) { - continue - } - if getCmd.Err() != nil { - return 0, fmt.Errorf("while getting actor snapshot: %w", getCmd.Err()) - } - snapshot := &ateapipb.ActorSnapshot{} - if err := protojson.Unmarshal([]byte(getCmd.Val()), snapshot); err != nil { - return 0, fmt.Errorf("while unmarshaling actor snapshot: %w", err) - } - result = append(result, snapshot) - collected++ - } - return collected, nil - }) - if err != nil { - return store.ListResponse[*ateapipb.ActorSnapshot]{}, err - } - return store.ListResponse[*ateapipb.ActorSnapshot]{Items: result, NextPageToken: nextToken}, nil -} - -func (s *Persistence) CreateActorSnapshotTag(ctx context.Context, atespace, name string, tag *ateapipb.ActorSnapshotTag) (*ateapipb.ActorSnapshotTag, error) { - if _, err := s.GetActorSnapshot(ctx, atespace, name); err != nil { - return nil, err - } - dbTag := proto.Clone(tag).(*ateapipb.ActorSnapshotTag) - dbTag.Metadata = newCreateMetadata(tag.GetMetadata().GetAtespace(), tag.GetMetadata().GetName()) - dbTag.Snapshot = &ateapipb.ObjectRef{Atespace: atespace, Name: name} - b, err := protojson.Marshal(dbTag) - if err != nil { - return nil, fmt.Errorf("while marshaling actor snapshot tag: %w", err) - } - tagKey := actorSnapshotTagDBKey(dbTag.GetMetadata().GetAtespace(), dbTag.GetMetadata().GetName()) - created, err := s.rdb.SetNX(ctx, tagKey, b, 0).Result() - if err != nil { - return nil, fmt.Errorf("while creating actor snapshot tag: %w", err) - } - if !created { - existing, err := s.rdb.Get(ctx, tagKey).Bytes() - if err != nil { - return nil, fmt.Errorf("while getting actor snapshot tag: %w", err) - } - existingTag := &ateapipb.ActorSnapshotTag{} - if err := protojson.Unmarshal(existing, existingTag); err != nil { - return nil, fmt.Errorf("while unmarshaling actor snapshot tag: %w", err) - } - if existingTag.GetSnapshot().GetAtespace() != atespace || existingTag.GetSnapshot().GetName() != name || existingTag.GetScope() != tag.GetScope() { - return nil, store.ErrAlreadyExists - } - return existingTag, nil - } - return dbTag, nil -} - -func validateUpdateActorSnapshotTagMutation(storedTag, mutatedTag *ateapipb.ActorSnapshotTag) error { - if stored, mutated := storedTag.GetMetadata().GetAtespace(), mutatedTag.GetMetadata().GetAtespace(); stored != mutated { - return fmt.Errorf("metadata.atespace is immutable: mutation changed it from %q to %q", stored, mutated) - } - if stored, mutated := storedTag.GetMetadata().GetName(), mutatedTag.GetMetadata().GetName(); stored != mutated { - return fmt.Errorf("metadata.name is immutable: mutation changed it from %q to %q", stored, mutated) - } - if stored, mutated := storedTag.GetSnapshot().GetAtespace(), mutatedTag.GetSnapshot().GetAtespace(); stored != mutated { - return fmt.Errorf("snapshot.atespace is immutable: mutation changed it from %q to %q", stored, mutated) - } - if stored, mutated := storedTag.GetSnapshot().GetName(), mutatedTag.GetSnapshot().GetName(); stored != mutated { - return fmt.Errorf("snapshot.name is immutable: mutation changed it from %q to %q", stored, mutated) - } - return nil -} - -// updateActorSnapshotTagMaxAttempts bounds how many times UpdateActorSnapshotTag -// re-runs its read-modify-write after a concurrent writer invalidates the -// transaction. -const updateActorSnapshotTagMaxAttempts = 5 - -func (s *Persistence) UpdateActorSnapshotTag(ctx context.Context, atespace, name string, mutate func(*ateapipb.ActorSnapshotTag) error) (*ateapipb.ActorSnapshotTag, error) { - tagKey := actorSnapshotTagDBKey(atespace, name) - for range updateActorSnapshotTagMaxAttempts { - var dbTag *ateapipb.ActorSnapshotTag - var abortErr error - - err := s.rdb.Watch(ctx, func(tx *redis.Tx) error { - currentVal, err := tx.Get(ctx, tagKey).Bytes() - if err != nil { - if errors.Is(err, redis.Nil) { - return store.ErrNotFound - } - return fmt.Errorf("while getting actor snapshot tag %s/%s: %w", atespace, name, err) - } - - currentTag := &ateapipb.ActorSnapshotTag{} - if err := protojson.Unmarshal(currentVal, currentTag); err != nil { - return fmt.Errorf("while unmarshaling actor snapshot tag %s/%s: %w", atespace, name, err) - } - - // Snapshot the stored state before handing the tag to mutate. - // mutate is free to edit anything it is given. - tagBeforeMutation := proto.Clone(currentTag).(*ateapipb.ActorSnapshotTag) - if err := mutate(currentTag); err != nil { - abortErr = err - return err - } - if err := validateUpdateActorSnapshotTagMutation(tagBeforeMutation, currentTag); err != nil { - abortErr = err - return err - } - // The stored metadata is authoritative; derive the next metadata - // from it, discarding whatever mutate made of it. - currentTag.Metadata = newUpdateMetadata(tagBeforeMutation.GetMetadata()) - - newVal, err := protojson.Marshal(currentTag) - if err != nil { - return fmt.Errorf("while marshaling actor snapshot tag: %w", err) - } - - if _, err := tx.TxPipelined(ctx, func(pipe redis.Pipeliner) error { - pipe.Set(ctx, tagKey, newVal, 0) - return nil - }); err != nil { - return err - } - dbTag = currentTag - return nil - }, tagKey) - - switch { - case err == nil: - return dbTag, nil - case abortErr != nil: - return nil, abortErr - case errors.Is(err, store.ErrNotFound): - return nil, store.ErrNotFound - case errors.Is(err, redis.TxFailedErr): - // A concurrent write landed before we could commit. - // Retry. - continue - default: - return nil, fmt.Errorf("while executing update actor snapshot tag transaction: %w", err) - } - } - - // Only the TxFailedErr branch continues the loop, so getting here means every - // attempt lost the race. - return nil, store.ErrVersionConflict -} - -func (s *Persistence) DeleteActorSnapshotTag(ctx context.Context, atespace, name string) (*ateapipb.ActorSnapshotTag, error) { - tag, err := s.GetActorSnapshotTag(ctx, atespace, name) - if err != nil { - return nil, err - } - tagKey := actorSnapshotTagDBKey(atespace, name) - if n, err := s.rdb.Del(ctx, tagKey).Result(); err != nil { - return nil, fmt.Errorf("while deleting actor snapshot tag: %w", err) - } else if n == 0 { - return nil, store.ErrNotFound - } - return tag, nil -} - -func (s *Persistence) CreateWorker(ctx context.Context, worker *ateapipb.Worker) error { - dbKey := workerDBKey(worker.GetWorkerNamespace(), worker.GetWorkerPool(), worker.GetWorkerPod()) - - // Clone because we will update the version field, and we don't want to - // stomp the caller's copy. - dbWorker := proto.Clone(worker).(*ateapipb.Worker) - dbWorker.Version = 1 - - dbWorkerBytes, err := protojson.Marshal(dbWorker) - if err != nil { - return fmt.Errorf("in protojson.Marshal: %w", err) - } - - ok, err := s.rdb.SetNX(ctx, dbKey, dbWorkerBytes, 0).Result() - if err != nil { - return fmt.Errorf("while executing redis set: %w", err) - } - if !ok { - return store.ErrAlreadyExists - } - - s.publishWorkerEvent(ctx, store.WorkerEventCreated, dbWorker) - return nil -} - -func (s *Persistence) GetWorker(ctx context.Context, namespace, pool, pod string) (*ateapipb.Worker, error) { - dbKey := workerDBKey(namespace, pool, pod) - - dbWorkerBytes, err := s.rdb.Get(ctx, dbKey).Bytes() - if err != nil { - if errors.Is(err, redis.Nil) { - return nil, store.ErrNotFound - } - return nil, fmt.Errorf("while getting worker key %q: %w", dbKey, err) - } - - worker := &ateapipb.Worker{} - if err := protojson.Unmarshal(dbWorkerBytes, worker); err != nil { - return nil, fmt.Errorf("in protojson.Unmarshal: %w", err) - } - - if worker.GetWorkerNamespace() != namespace || worker.GetWorkerPool() != pool || worker.GetWorkerPod() != pod { - return nil, fmt.Errorf("(impossible) mismatch between stored namespace/pool/pod and key") - } - - return worker, nil -} - -func (s *Persistence) UpdateWorker(ctx context.Context, worker *ateapipb.Worker, expectedVersion int64) error { - dbKey := workerDBKey(worker.GetWorkerNamespace(), worker.GetWorkerPool(), worker.GetWorkerPod()) - - // Clone because we will update the version field, and we don't want to - // stomp the caller's copy. - dbWorker := proto.Clone(worker).(*ateapipb.Worker) - - err := s.rdb.Watch(ctx, func(tx *redis.Tx) error { - currentVal, err := tx.Get(ctx, dbKey).Bytes() - if err != nil { - if errors.Is(err, redis.Nil) { - return store.ErrNotFound - } - return fmt.Errorf("while getting worker: %w", err) - } - - currentWorker := &ateapipb.Worker{} - if err := protojson.Unmarshal(currentVal, currentWorker); err != nil { - return fmt.Errorf("in protojson.Unmarshal: %w", err) - } - - if currentWorker.GetVersion() != expectedVersion { - return store.ErrVersionConflict - } - dbWorker.Version = currentWorker.GetVersion() + 1 - if currentWorker.GetWorkerNamespace() != dbWorker.GetWorkerNamespace() { - return fmt.Errorf("worker_namespace is immutable") - } - if currentWorker.GetWorkerPool() != dbWorker.GetWorkerPool() { - return fmt.Errorf("worker_pool is immutable") - } - if currentWorker.GetWorkerPod() != dbWorker.GetWorkerPod() { - return fmt.Errorf("worker_pod is immutable") - } - if currentWorker.GetIp() != dbWorker.GetIp() { - return fmt.Errorf("ip is immutable") - } - newVal, err := protojson.Marshal(dbWorker) - if err != nil { - return fmt.Errorf("in protojson.Marshal: %w", err) - } - - _, err = tx.TxPipelined(ctx, func(pipe redis.Pipeliner) error { - pipe.Set(ctx, dbKey, newVal, 0) - return nil - }) - return err - }, dbKey) - if err != nil { - if errors.Is(err, store.ErrNotFound) { - return store.ErrNotFound - } - if errors.Is(err, store.ErrVersionConflict) || errors.Is(err, redis.TxFailedErr) { - return store.ErrVersionConflict - } - return fmt.Errorf("while executing update worker transaction: %w", err) - } - - s.publishWorkerEvent(ctx, store.WorkerEventUpdated, dbWorker) - return nil -} - -func (s *Persistence) DeleteWorker(ctx context.Context, namespace, pool, pod string) error { - dbKey := workerDBKey(namespace, pool, pod) - err := s.rdb.Del(ctx, dbKey).Err() - if err != nil { - return fmt.Errorf("while deleting worker key %q: %w", dbKey, err) - } - s.publishWorkerEvent(ctx, store.WorkerEventDeleted, &ateapipb.Worker{ - WorkerNamespace: namespace, - WorkerPod: pod, - }) - return nil -} - -func (s *Persistence) DeleteActor(ctx context.Context, actorRef resources.ActorRef) (*ateapipb.Actor, error) { - dbKey := actorDBKey(actorRef) - var deleted *ateapipb.Actor - err := s.rdb.Watch(ctx, func(tx *redis.Tx) error { - currentVal, err := tx.Get(ctx, dbKey).Bytes() - if err != nil { - if errors.Is(err, redis.Nil) { - return store.ErrNotFound - } - return fmt.Errorf("while getting actor: %w", err) - } - - currentActor := &ateapipb.Actor{} - if err := protojson.Unmarshal(currentVal, currentActor); err != nil { - return fmt.Errorf("in protojson.Unmarshal: %w", err) - } - - if currentActor.GetStatus() != ateapipb.Actor_STATUS_DELETING { - return store.ErrFailedPrecondition - } - - if _, err := tx.TxPipelined(ctx, func(pipe redis.Pipeliner) error { - pipe.Del(ctx, dbKey) - return nil - }); err != nil { - return err - } - deleted = currentActor - return nil - }, dbKey) - - if err != nil { - if errors.Is(err, redis.TxFailedErr) { - return nil, store.ErrVersionConflict - } - return nil, err - } - - return deleted, nil -} - -// validateUpdateActorMutation reports whether an actor mutation left the fields it does -// not own alone. -func validateUpdateActorMutation(storedActor, mutatedActor *ateapipb.Actor) error { - if stored, mutated := storedActor.GetMetadata().GetAtespace(), mutatedActor.GetMetadata().GetAtespace(); stored != mutated { - return fmt.Errorf("metadata.atespace is immutable: mutation changed it from %q to %q", stored, mutated) - } - if stored, mutated := storedActor.GetMetadata().GetName(), mutatedActor.GetMetadata().GetName(); stored != mutated { - return fmt.Errorf("metadata.name is immutable: mutation changed it from %q to %q", stored, mutated) - } - if stored, mutated := storedActor.GetActorTemplateNamespace(), mutatedActor.GetActorTemplateNamespace(); stored != mutated { - return fmt.Errorf("actor_template_namespace is immutable: mutation changed it from %q to %q", stored, mutated) - } - if stored, mutated := storedActor.GetActorTemplateName(), mutatedActor.GetActorTemplateName(); stored != mutated { - return fmt.Errorf("actor_template_name is immutable: mutation changed it from %q to %q", stored, mutated) - } - return nil -} - -// updateMaxAttempts bounds how many times UpdateActor or UpdateActorTemplate re-runs its -// read-modify-write after a concurrent writer invalidates the transaction. -const updateMaxAttempts = 5 - -func (s *Persistence) UpdateActor(ctx context.Context, actorRef resources.ActorRef, mutate func(*ateapipb.Actor) error) (*ateapipb.Actor, error) { - dbKey := actorDBKey(actorRef) - for range updateMaxAttempts { - var dbActor *ateapipb.Actor - var abortErr error - - err := s.rdb.Watch(ctx, func(tx *redis.Tx) error { - currentVal, err := tx.Get(ctx, dbKey).Bytes() - if err != nil { - if errors.Is(err, redis.Nil) { - return store.ErrNotFound - } - return fmt.Errorf("while getting actor: %w", err) - } - - currentActor := &ateapipb.Actor{} - if err := protojson.Unmarshal(currentVal, currentActor); err != nil { - return fmt.Errorf("in protojson.Unmarshal: %w", err) - } - - // Snapshot the stored state before handing the actor to mutate. - // mutate is free to edit anything it is given. - actorBeforeMutation := proto.Clone(currentActor).(*ateapipb.Actor) - if err := mutate(currentActor); err != nil { - abortErr = err - return err - } - if err := validateUpdateActorMutation(actorBeforeMutation, currentActor); err != nil { - abortErr = err - return err - } - // The stored metadata is authoritative; derive the next metadata - // from it, discarding whatever mutate made of it. - currentActor.Metadata = newUpdateMetadata(actorBeforeMutation.GetMetadata()) - - newVal, err := protojson.Marshal(currentActor) - if err != nil { - return fmt.Errorf("in protojson.Marshal: %w", err) - } - - if _, err := tx.TxPipelined(ctx, func(pipe redis.Pipeliner) error { - pipe.Set(ctx, dbKey, newVal, 0) - return nil - }); err != nil { - return err - } - dbActor = currentActor - return nil - }, dbKey) - - switch { - case err == nil: - return dbActor, nil - case abortErr != nil: - return nil, abortErr - case errors.Is(err, store.ErrNotFound): - return nil, store.ErrNotFound - case errors.Is(err, redis.TxFailedErr): - // A concurrent write landed between WATCH and EXEC, so mutate never - // saw it. Re-read and run it against the newer state. - continue - default: - return nil, fmt.Errorf("while executing update actor transaction: %w", err) - } - } - - // Only the TxFailedErr branch continues the loop, so getting here means every - // attempt lost the race. - return nil, store.ErrVersionConflict -} - -func (s *Persistence) ListWorkers(ctx context.Context, opts store.ListOptions) (store.ListResponse[*ateapipb.Worker], error) { - var result []*ateapipb.Worker - nextToken, err := s.listPage(ctx, "worker:*", opts.PageSize, opts.PageToken, func(ctx context.Context, master *redis.Client, keys []string) (int, error) { - workers, err := fetchProtos(ctx, master, keys, func() *ateapipb.Worker { return &ateapipb.Worker{} }) - if err != nil { - return 0, err - } - result = append(result, workers...) - return len(workers), nil - }) - if err != nil { - return store.ListResponse[*ateapipb.Worker]{}, err - } - return store.ListResponse[*ateapipb.Worker]{Items: result, NextPageToken: nextToken}, nil -} - -type pageToken struct { - ShardHash string `json:"shard_hash"` - Cursor uint64 `json:"cursor"` -} - -func encodePageToken(token pageToken) string { - b, _ := json.Marshal(token) - return base64.StdEncoding.EncodeToString(b) -} - -func decodePageToken(tokenStr string) (pageToken, error) { - var token pageToken - if tokenStr == "" { - return token, nil - } - b, err := base64.StdEncoding.DecodeString(tokenStr) - if err != nil { - return token, err - } - err = json.Unmarshal(b, &token) - return token, err -} - -func hashShardAddr(addr string) string { - h := sha256.Sum256([]byte(addr)) - return hex.EncodeToString(h[:]) -} - -// ListActors lists actors, scoped to the given atespace. An empty atespace lists -// across all atespaces (SCAN actor:*); a non-empty atespace restricts the scan to -// that atespace (SCAN actor::*). -func (s *Persistence) ListActors(ctx context.Context, atespace string, opts store.ListOptions) (store.ListResponse[*ateapipb.Actor], error) { - var result []*ateapipb.Actor - nextToken, err := s.listPage(ctx, actorScanPattern(atespace), opts.PageSize, opts.PageToken, func(ctx context.Context, master *redis.Client, keys []string) (int, error) { - actors, err := fetchProtos(ctx, master, keys, func() *ateapipb.Actor { return &ateapipb.Actor{} }) - if err != nil { - return 0, err - } - result = append(result, actors...) - return len(actors), nil - }) - if err != nil { - return store.ListResponse[*ateapipb.Actor]{}, err - } - return store.ListResponse[*ateapipb.Actor]{Items: result, NextPageToken: nextToken}, nil -} - -// listPage SCANs pattern across the redis masters from the page token, feeding key batches to collect and returns the next-page token. -func (s *Persistence) listPage(ctx context.Context, pattern string, pageSize int32, pageTokenStr string, collect func(ctx context.Context, master *redis.Client, keys []string) (int, error)) (string, error) { - token, err := decodePageToken(pageTokenStr) - if err != nil { - return "", fmt.Errorf("invalid page token: %w", err) - } - - masters, err := s.getSortedMasters(ctx) - if err != nil { - return "", err - } - - startIndex, err := findStartingShard(masters, token.ShardHash) - if err != nil { - return "", err - } - - i := startIndex - cursor := token.Cursor - collected := 0 - - for i < len(masters) && collected < int(pageSize) { - master := masters[i] - remaining := int(pageSize) - collected - - var keys []string - keys, cursor, err = master.Scan(ctx, cursor, pattern, int64(remaining)).Result() - if err != nil { - return "", fmt.Errorf("while scanning shard %s: %w", master.Options().Addr, err) - } - - if len(keys) > 0 { - n, err := collect(ctx, master, keys) - if err != nil { - return "", err - } - collected += n - } - - if cursor == 0 { - i++ - } - } - - var nextToken string - if i < len(masters) { - nextToken = encodePageToken(pageToken{ - ShardHash: hashShardAddr(masters[i].Options().Addr), - Cursor: cursor, - }) - } - - return nextToken, nil -} - -func (s *Persistence) getSortedMasters(ctx context.Context) ([]*redis.Client, error) { - var mu sync.Mutex - var masters []*redis.Client - // ForEachMaster invokes the callback concurrently, one goroutine per master. - err := s.rdb.ForEachMaster(ctx, func(ctx context.Context, master *redis.Client) error { - mu.Lock() - defer mu.Unlock() - masters = append(masters, master) - return nil - }) - if err != nil { - return nil, fmt.Errorf("while listing redis masters: %w", err) - } - - sort.Slice(masters, func(i, j int) bool { - return masters[i].Options().Addr < masters[j].Options().Addr - }) - return masters, nil -} - -func findStartingShard(masters []*redis.Client, shardHash string) (int, error) { - if shardHash == "" { - return 0, nil - } - for i, m := range masters { - if hashShardAddr(m.Options().Addr) == shardHash { - return i, nil - } - } - return 0, fmt.Errorf("topology changed: shard with hash %s not found (aborted)", shardHash) -} - -// fetchProtos fetches keys into newMsg-created messages. -func fetchProtos[M proto.Message](ctx context.Context, master *redis.Client, keys []string, newMsg func() M) ([]M, error) { - cmds, err := master.Pipelined(ctx, func(pipe redis.Pipeliner) error { - for _, key := range keys { - pipe.Get(ctx, key) - } - return nil - }) - if err != nil && !errors.Is(err, redis.Nil) { - return nil, fmt.Errorf("while fetching keys in shard %s: %w", master.Options().Addr, err) - } - - var out []M - for _, cmd := range cmds { - getCmd, ok := cmd.(*redis.StringCmd) - if !ok { - continue - } - if getCmd.Err() != nil { - if errors.Is(getCmd.Err(), redis.Nil) { - continue - } - return nil, fmt.Errorf("while getting key: %w", getCmd.Err()) - } - - msg := newMsg() - if err := protojson.Unmarshal([]byte(getCmd.Val()), msg); err != nil { - return nil, fmt.Errorf("in protojson.Unmarshal: %w", err) - } - out = append(out, msg) - } - return out, nil -} - -// lockRenewScript extends key's TTL only if it is still owned by ARGV[1], -// atomically. Returns 1 if renewed, 0 if the lock was lost (expired and -// possibly reacquired by someone else, or otherwise deleted). -var lockRenewScript = redis.NewScript(` - if redis.call("get", KEYS[1]) == ARGV[1] then - return redis.call("pexpire", KEYS[1], ARGV[2]) - else - return 0 - end -`) - -// lockReleaseScript deletes key only if it is still owned by ARGV[1], -// atomically, so a caller can never release a lock it no longer holds. -var lockReleaseScript = redis.NewScript(` - if redis.call("get", KEYS[1]) == ARGV[1] then - return redis.call("del", KEYS[1]) - else - return 0 - end -`) - -// defaultLockTTL is how long a lock may go unrenewed before another client -// can reclaim it. -const defaultLockTTL = 30 * time.Second - -func (s *Persistence) AcquireLock(ctx context.Context, key string) (*store.Lock, error) { - ttl := s.lockTTL - value := uuid.New().String() - - ok, err := s.rdb.SetNX(ctx, key, value, ttl).Result() - if err != nil { - return nil, fmt.Errorf("while acquiring lock for %q: %w", key, err) - } - if !ok { - return nil, store.ErrLockConflict - } - - // leaseCtx is cancelled either by Close, or by the renewal loop below if it - // ever stops without Close having been called (i.e. the lease was lost). - leaseCtx, cancel := context.WithCancel(ctx) - renewalDone := make(chan struct{}) - - go func() { - defer close(renewalDone) - defer cancel() - s.renewLockLoop(leaseCtx, key, value, ttl) - }() - - closeFn := func() { - cancel() - <-renewalDone // wait for the renewal loop to stop before releasing. - - releaseCtx, releaseCancel := context.WithTimeout(context.Background(), 5*time.Second) - defer releaseCancel() - if err := s.releaseLock(releaseCtx, key, value); err != nil { - slog.WarnContext(releaseCtx, "failed to release lock, relying on TTL to reclaim it", "key", key, "error", err) - } - } - - return store.NewLock(leaseCtx, closeFn), nil -} - -const ( - // renewIntervalDivisor and renewRetryPeriodDivisor set the renewal loop's - // steady-state cadence and in-failure retry spacing as fractions of ttl: - // interval = ttl/renewIntervalDivisor, retryPeriod = ttl/renewRetryPeriodDivisor. - renewIntervalDivisor = 3 - renewRetryPeriodDivisor = 10 - // renewDeadlineFraction bounds how much of the lock's TTL the renewal loop - // may spend retrying after its last successful renewal before conceding the - // lease as lost. - renewDeadlineFraction = 2.0 / 3.0 -) - -func (s *Persistence) renewLockLoop(ctx context.Context, key, value string, ttl time.Duration) { - interval := ttl / renewIntervalDivisor - renewDeadline := time.Duration(float64(ttl) * renewDeadlineFraction) - - lastRenewed := time.Now() - timer := time.NewTimer(interval) - defer timer.Stop() - - for { - select { - case <-ctx.Done(): - return - case <-timer.C: - renewCtx, cancel := context.WithDeadline(ctx, lastRenewed.Add(renewDeadline)) - renewed := s.tryRenewLock(renewCtx, key, value, ttl) - cancel() - if !renewed { - return - } - lastRenewed = time.Now() - timer.Reset(interval) - } - } -} - -func (s *Persistence) tryRenewLock(ctx context.Context, key, value string, ttl time.Duration) bool { - retryPeriod := ttl / renewRetryPeriodDivisor - - retry := time.NewTimer(0) // first attempt fires immediately. - defer retry.Stop() - - for { - select { - case <-ctx.Done(): - if errors.Is(ctx.Err(), context.DeadlineExceeded) { - slog.WarnContext(ctx, "failed to renew lock and its renew deadline has elapsed, treating lease as lost", "key", key) - } - return false - - case <-retry.C: - renewed, err := s.renewLock(ctx, key, value, ttl) - - if ctx.Err() != nil { - return false // deadline elapsed or Close raced with this attempt. - } - - switch { - case err == nil && renewed: - return true - - case err == nil && !renewed: - slog.WarnContext(ctx, "lock renewal found lease no longer owned", "key", key) - return false - - default: - slog.WarnContext(ctx, "failed to renew lock, retrying before its renew deadline elapses", "key", key, "error", err) - retry.Reset(retryPeriod) - } - } - } -} - -func (s *Persistence) renewLock(ctx context.Context, key, value string, ttl time.Duration) (bool, error) { - res, err := lockRenewScript.Run(ctx, s.rdb, []string{key}, value, ttl.Milliseconds()).Result() - if err != nil { - return false, fmt.Errorf("while renewing lock for %q: %w", key, err) - } - renewed, _ := res.(int64) - return renewed == 1, nil -} - -func (s *Persistence) releaseLock(ctx context.Context, key, value string) error { - _, err := lockReleaseScript.Run(ctx, s.rdb, []string{key}, value).Result() - if err != nil { - return fmt.Errorf("while releasing lock for %q with value %q: %w", key, value, err) - } - return nil -} - -func newCreateMetadata(atespace, name string) *ateapipb.ResourceMetadata { - now := timestamppb.Now() - return &ateapipb.ResourceMetadata{ - Atespace: atespace, - Name: name, - Uid: uuid.NewString(), - Version: 1, - CreateTime: now, - UpdateTime: now, - } -} - -func newUpdateMetadata(current *ateapipb.ResourceMetadata) *ateapipb.ResourceMetadata { - next := proto.Clone(current).(*ateapipb.ResourceMetadata) - next.Version = current.GetVersion() + 1 - next.UpdateTime = timestamppb.Now() - return next -} diff --git a/cmd/ateapi/internal/store/ateredis/ateredis_test.go b/cmd/ateapi/internal/store/ateredis/ateredis_test.go deleted file mode 100644 index 23b9a2536..000000000 --- a/cmd/ateapi/internal/store/ateredis/ateredis_test.go +++ /dev/null @@ -1,3238 +0,0 @@ -// Copyright 2026 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package ateredis - -import ( - "context" - "errors" - "fmt" - "sort" - "strings" - "sync" - "testing" - "testing/synctest" - "time" - - "github.com/alicebob/miniredis/v2" - "github.com/google/go-cmp/cmp" - "github.com/redis/go-redis/v9" - "google.golang.org/protobuf/encoding/protojson" - "google.golang.org/protobuf/proto" - "google.golang.org/protobuf/testing/protocmp" - - "github.com/agent-substrate/substrate/cmd/ateapi/internal/store" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" -) - -func setupTest(t *testing.T) (*miniredis.Miniredis, *Persistence, context.Context) { - mr, err := miniredis.Run() - if err != nil { - t.Fatalf("failed to start miniredis: %v", err) - } - t.Cleanup(mr.Close) - // Miniredis runs as a single node, but ClusterClient can work with it - // if we don't use cluster-specific commands that miniredis doesn't support. - // Miniredis supports most standard commands. - rdb := redis.NewClusterClient(&redis.ClusterOptions{ - Addrs: []string{mr.Addr()}, - }) - t.Cleanup(func() { rdb.Close() }) - return mr, NewPersistence(rdb), t.Context() -} - -// testAtespace is the atespace used by tests that create a single actor. Actors -// are atespace-scoped, so a real atespace must always be part of their identity. -const testAtespace = "test-atespace" - -// Atomic cmp options to skip individual server-owned ResourceMetadata fields in -// proto diffs. Compose the ones a given assertion needs — e.g. ignore uid and -// timestamps but keep version when the test asserts a specific version. -var ( - ignoreUID = protocmp.IgnoreFields(&ateapipb.ResourceMetadata{}, "uid") - ignoreVersion = protocmp.IgnoreFields(&ateapipb.ResourceMetadata{}, "version") - ignoreTimestamps = protocmp.IgnoreFields(&ateapipb.ResourceMetadata{}, "create_time", "update_time") -) - -func TestGetActor_NotFound(t *testing.T) { - _, s, ctx := setupTest(t) - - _, err := s.GetActor(ctx, resources.ActorRef{Atespace: testAtespace, Name: "non-existent"}) - if !errors.Is(err, store.ErrNotFound) { - t.Errorf("expected ErrNotFound, got %v", err) - } -} - -func TestCreateActor_Success(t *testing.T) { - _, s, ctx := setupTest(t) - - actor := &ateapipb.Actor{ - Metadata: &ateapipb.ResourceMetadata{Name: "actor-1", Atespace: testAtespace}, - ActorTemplateNamespace: "default", - ActorTemplateName: "test-template", - Status: ateapipb.Actor_STATUS_SUSPENDED, - } - - created, err := s.CreateActor(ctx, actor) - if err != nil { - t.Fatalf("CreateActor failed: %v", err) - } - - // CreateActor returns the stored resource with server-assigned metadata. - if created.GetMetadata().GetUid() == "" { - t.Errorf("CreateActor returned empty uid; want server-assigned uid") - } - if created.GetMetadata().GetVersion() != 1 { - t.Errorf("CreateActor returned version %d, want 1", created.GetMetadata().GetVersion()) - } - if created.GetMetadata().GetCreateTime() == nil || created.GetMetadata().GetUpdateTime() == nil { - t.Errorf("CreateActor returned unset create/update time") - } - - // The input must not be mutated. - if actor.GetMetadata().GetUid() != "" || actor.GetMetadata().GetVersion() != 0 { - t.Errorf("CreateActor must not mutate its input, got metadata %v", actor.GetMetadata()) - } - - // The returned resource is exactly what GetActor reads back. - got, err := s.GetActor(ctx, resources.ActorRefFromActor(actor)) - if err != nil { - t.Fatalf("GetActor failed: %v", err) - } - if diff := cmp.Diff(created, got, protocmp.Transform()); diff != "" { - t.Errorf("CreateActor return does not match stored state (-created +got):\n%s", diff) - } - - // Structurally: the input fields plus server-assigned metadata. - expected := proto.Clone(actor).(*ateapipb.Actor) - expected.Metadata.Version = 1 - if diff := cmp.Diff(expected, created, protocmp.Transform(), ignoreUID, ignoreTimestamps); diff != "" { - t.Errorf("CreateActor returned unexpected actor (-want +got):\n%s", diff) - } -} - -func TestCreateActor_AlreadyExists(t *testing.T) { - _, s, ctx := setupTest(t) - - actor := &ateapipb.Actor{ - Metadata: &ateapipb.ResourceMetadata{Name: "actor-1", Atespace: testAtespace}, - ActorTemplateNamespace: "default", - ActorTemplateName: "test-template", - Status: ateapipb.Actor_STATUS_SUSPENDED, - } - - _, err := s.CreateActor(ctx, actor) - if err != nil { - t.Fatalf("CreateActor failed: %v", err) - } - - _, err = s.CreateActor(ctx, actor) - if err == nil { - t.Errorf("expected error creating existing actor, got nil") - } -} - -// newTestActor returns an unsaved actor for the UpdateActor tests. -func newTestActor(name string) *ateapipb.Actor { - return &ateapipb.Actor{ - Metadata: &ateapipb.ResourceMetadata{Name: name, Atespace: testAtespace}, - ActorTemplateNamespace: "default", - ActorTemplateName: "test-template", - Status: ateapipb.Actor_STATUS_SUSPENDED, - } -} - -func TestUpdateActor_Success(t *testing.T) { - _, s, ctx := setupTest(t) - actor := newTestActor("actor-1") - created, err := s.CreateActor(ctx, actor) - if err != nil { - t.Fatalf("CreateActor failed: %v", err) - } - - actorRef := resources.ActorRefFromActor(actor) - updated, err := s.UpdateActor(ctx, actorRef, func(toUpdate *ateapipb.Actor) error { - toUpdate.Status = ateapipb.Actor_STATUS_RUNNING - return nil - }) - if err != nil { - t.Fatalf("UpdateActor failed: %v", err) - } - - // UpdateActor returns the stored resource: the mutation applied and version - // advanced, with uid and create_time preserved from creation. - if updated.GetStatus() != ateapipb.Actor_STATUS_RUNNING { - t.Errorf("UpdateActor returned status %v, want RUNNING", updated.GetStatus()) - } - if updated.GetMetadata().GetVersion() != 2 { - t.Errorf("UpdateActor returned version %d, want 2", updated.GetMetadata().GetVersion()) - } - if updated.GetMetadata().GetUid() != created.GetMetadata().GetUid() { - t.Errorf("uid changed on update: got %q, want %q", updated.GetMetadata().GetUid(), created.GetMetadata().GetUid()) - } - if !updated.GetMetadata().GetCreateTime().AsTime().Equal(created.GetMetadata().GetCreateTime().AsTime()) { - t.Errorf("create_time changed on update: got %v, want %v", updated.GetMetadata().GetCreateTime().AsTime(), created.GetMetadata().GetCreateTime().AsTime()) - } - - // The returned resource is exactly what GetActor reads back. - got, err := s.GetActor(ctx, actorRef) - if err != nil { - t.Fatalf("GetActor failed: %v", err) - } - if diff := cmp.Diff(updated, got, protocmp.Transform()); diff != "" { - t.Errorf("UpdateActor return does not match stored state (-updated +got):\n%s", diff) - } -} - -func TestUpdateActor_MutateErrorAreNotRetried(t *testing.T) { - _, s, ctx := setupTest(t) - actor := newTestActor("actor-1") - created, err := s.CreateActor(ctx, actor) - if err != nil { - t.Fatalf("CreateActor failed: %v", err) - } - - var mutationError = errors.New("mutation error") - - actorRef := resources.ActorRefFromActor(actor) - callsToMutateFn := 0 - _, err = s.UpdateActor(ctx, actorRef, func(toUpdate *ateapipb.Actor) error { - callsToMutateFn++ - toUpdate.Status = ateapipb.Actor_STATUS_RUNNING - return fmt.Errorf("actor %s: %w", actorRef, mutationError) - }) - // The error must arrive intact - if !errors.Is(err, mutationError) { - t.Errorf("UpdateActor error = %v, want one wrapping mutationError", err) - } - // Mutation errors are non-retriable - if callsToMutateFn != 1 { - t.Errorf("mutate ran %d times, want exactly 1 (a rejected precondition must not be retried)", callsToMutateFn) - } - - got, err := s.GetActor(ctx, actorRef) - if err != nil { - t.Fatalf("GetActor failed: %v", err) - } - if diff := cmp.Diff(created, got, protocmp.Transform()); diff != "" { - t.Errorf("aborted mutation was persisted (-created +got):\n%s", diff) - } -} - -func TestUpdateActor_DiscardsServerOwnedFieldsEdits(t *testing.T) { - _, s, ctx := setupTest(t) - - actor := newTestActor("actor-1") - created, err := s.CreateActor(ctx, actor) - if err != nil { - t.Fatalf("CreateActor failed: %v", err) - } - - actorRef := resources.ActorRefFromActor(actor) - updated, err := s.UpdateActor(ctx, actorRef, func(toUpdate *ateapipb.Actor) error { - // Metadata is server-owned: a closure must not be able to change it. - toUpdate.Metadata.Uid = "forged-uid" - toUpdate.Metadata.Version = 99 - toUpdate.Metadata.CreateTime = nil - toUpdate.Metadata.UpdateTime = nil - toUpdate.Status = ateapipb.Actor_STATUS_RUNNING - return nil - }) - if err != nil { - t.Fatalf("UpdateActor failed: %v", err) - } - - if got := updated.GetMetadata().GetUid(); got != created.GetMetadata().GetUid() { - t.Errorf("uid = %q, want the server-assigned %q", got, created.GetMetadata().GetUid()) - } - if got := updated.GetMetadata().GetVersion(); got != created.GetMetadata().GetVersion()+1 { - t.Errorf("version = %d, want %d (one past the stored version, not the forged value)", got, created.GetMetadata().GetVersion()+1) - } - if got := updated.GetMetadata().GetCreateTime(); got == nil || !got.AsTime().Equal(created.GetMetadata().GetCreateTime().AsTime()) { - t.Errorf("create_time = %v, want the creation value %v", got, created.GetMetadata().GetCreateTime()) - } - if updated.GetStatus() != ateapipb.Actor_STATUS_RUNNING { - t.Errorf("status = %v, want RUNNING: discarding metadata edits must not discard the mutation", updated.GetStatus()) - } -} - -// TestUpdateActor_RejectsImmutableFieldChange covers the fields a mutation may -// not touch. Unlike the server-owned metadata, which is silently restored, -// these fail the call: a caller that renamed an actor or repointed its template -// asked for something the store cannot do, and must hear about it. -func TestUpdateActor_RejectsImmutableFieldChange(t *testing.T) { - tests := []struct { - name string - mutate func(toUpdate *ateapipb.Actor) - wantField string - }{ - { - name: "atespace", - mutate: func(toUpdate *ateapipb.Actor) { toUpdate.Metadata.Atespace = "other-atespace" }, - wantField: "metadata.atespace", - }, - { - name: "name", - mutate: func(toUpdate *ateapipb.Actor) { toUpdate.Metadata.Name = "other-name" }, - wantField: "metadata.name", - }, - { - name: "actor template namespace", - mutate: func(toUpdate *ateapipb.Actor) { toUpdate.ActorTemplateNamespace = "other-ns" }, - wantField: "actor_template_namespace", - }, - { - name: "actor template name", - mutate: func(toUpdate *ateapipb.Actor) { toUpdate.ActorTemplateName = "other-template" }, - wantField: "actor_template_name", - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - _, s, ctx := setupTest(t) - actor := newTestActor("actor-1") - created, err := s.CreateActor(ctx, actor) - if err != nil { - t.Fatalf("CreateActor failed: %v", err) - } - - actorRef := resources.ActorRefFromActor(actor) - _, err = s.UpdateActor(ctx, actorRef, func(toUpdate *ateapipb.Actor) error { - // Paired with a legitimate edit, so the rejection cannot be - // mistaken for a no-op mutation. - toUpdate.Status = ateapipb.Actor_STATUS_RUNNING - tt.mutate(toUpdate) - return nil - }) - // The message must name the offending field: the closure is buggy, - // and whoever has to fix it only has this error to go on. - if want := tt.wantField + " is immutable"; err == nil || !strings.Contains(err.Error(), want) { - t.Errorf("UpdateActor changing %s = %v, want an error containing %q", tt.name, err, want) - } - - got, err := s.GetActor(ctx, actorRef) - if err != nil { - t.Fatalf("GetActor failed: %v", err) - } - if diff := cmp.Diff(created, got, protocmp.Transform()); diff != "" { - t.Errorf("rejected mutation was persisted anyway (-created +got):\n%s", diff) - } - }) - } -} - -// watchInterceptor runs before each WATCH'd transaction body, so a test can -// write the watched key from another connection and make EXEC fail the way a -// real concurrent writer would. -type watchInterceptor struct { - redisClient - before func() -} - -func (w *watchInterceptor) Watch(ctx context.Context, fn func(*redis.Tx) error, keys ...string) error { - return w.redisClient.Watch(ctx, func(tx *redis.Tx) error { - w.before() - return fn(tx) - }, keys...) -} - -func TestUpdateActor_RetriesOnConcurrentWrite(t *testing.T) { - mr, s, ctx := setupTest(t) - actor := newTestActor("actor-1") - if _, err := s.CreateActor(ctx, actor); err != nil { - t.Fatalf("CreateActor failed: %v", err) - } - actorRef := resources.ActorRefFromActor(actor) - - // A separate client, so its write lands outside the transaction's connection. - otherClient := redis.NewClusterClient(&redis.ClusterOptions{Addrs: []string{mr.Addr()}}) - t.Cleanup(func() { otherClient.Close() }) - - attempts := 0 - interceptor := &watchInterceptor{redisClient: s.rdb, before: func() { - // Only the first attempt races. We do this to make sure the second retry - // will succeed. - if attempts > 0 { - return - } - concurrent, err := s.GetActor(ctx, actorRef) - if err != nil { - t.Errorf("GetActor for concurrent write failed: %v", err) - return - } - concurrent.WorkerSelector = &ateapipb.Selector{MatchLabels: map[string]string{"tier": "paid"}} - val, err := protojson.Marshal(concurrent) - if err != nil { - t.Errorf("protojson.Marshal failed: %v", err) - return - } - if err := otherClient.Set(ctx, actorDBKey(actorRef), val, 0).Err(); err != nil { - t.Errorf("concurrent Set failed: %v", err) - } - }} - racing := &Persistence{rdb: interceptor, lockTTL: defaultLockTTL} - - updated, err := racing.UpdateActor(ctx, actorRef, func(toUpdate *ateapipb.Actor) error { - attempts++ - toUpdate.Status = ateapipb.Actor_STATUS_RUNNING - return nil - }) - if err != nil { - t.Fatalf("UpdateActor failed: %v", err) - } - if attempts < 2 { - t.Errorf("mutate ran %d times, want at least 2: the firts write is racey and must be rejected", attempts) - } - if updated.GetStatus() != ateapipb.Actor_STATUS_RUNNING { - t.Errorf("status = %v, want RUNNING", updated.GetStatus()) - } - // 1. The concurrent tx wrote "tier: paid" worker selector. This change should survive instead of - // being reverted by a mutation computed against the older state. - if got := updated.GetWorkerSelector().GetMatchLabels()["tier"]; got != "paid" { - t.Errorf("worker_selector[tier] = %q, want %q: the retry clobbered the concurrent write", got, "paid") - } -} - -func TestUpdateActor_NotFound(t *testing.T) { - _, s, ctx := setupTest(t) - _, err := s.UpdateActor(ctx, resources.ActorRef{Atespace: testAtespace, Name: "non-existent"}, func(toUpdate *ateapipb.Actor) error { - t.Error("mutate must not run for a missing actor") - return nil - }) - if !errors.Is(err, store.ErrNotFound) { - t.Errorf("expected store.ErrNotFound, got %v", err) - } -} - -func TestUpdateActor_RejectsStaleUID(t *testing.T) { - _, s, ctx := setupTest(t) - - original, err := s.CreateActor(ctx, newTestActor("actor-1")) - if err != nil { - t.Fatalf("CreateActor failed: %v", err) - } - actorRef := resources.ActorRefFromActor(original) - if _, err := s.UpdateActor(ctx, actorRef, func(toUpdate *ateapipb.Actor) error { - toUpdate.Status = ateapipb.Actor_STATUS_DELETING - return nil - }); err != nil { - t.Fatalf("marking actor deleting failed: %v", err) - } - if _, err := s.DeleteActor(ctx, actorRef); err != nil { - t.Fatalf("DeleteActor failed: %v", err) - } - recreated, err := s.CreateActor(ctx, newTestActor("actor-1")) - if err != nil { - t.Fatalf("recreate CreateActor failed: %v", err) - } - if recreated.GetMetadata().GetUid() == original.GetMetadata().GetUid() { - t.Fatalf("recreated actor reused uid %s, want a fresh one", recreated.GetMetadata().GetUid()) - } - - // Pins the incarnation alone: the observed actor carries the original uid and - // no version. - pinUID := &ateapipb.Actor{ - Metadata: &ateapipb.ResourceMetadata{Uid: original.GetMetadata().GetUid(), Version: store.AnyVersion}, - } - _, err = s.UpdateActor(ctx, actorRef, store.WithPrecondition(pinUID, func(toUpdate *ateapipb.Actor) error { - t.Error("mutate ran past its precondition once the pinned incarnation was gone") - toUpdate.Status = ateapipb.Actor_STATUS_RUNNING - return nil - })) - if !errors.Is(err, store.ErrUIDConflict) { - t.Errorf("UpdateActor error = %v, want one matching store.ErrUIDConflict", err) - } - - // The version guard was waived, so this is the incarnation failure alone. - if errors.Is(err, store.ErrVersionConflict) { - t.Errorf("UpdateActor error = %v, want no store.ErrVersionConflict match: no version was pinned", err) - } - - stored, err := s.GetActor(ctx, actorRef) - if err != nil { - t.Fatalf("GetActor failed: %v", err) - } - if got := stored.GetMetadata().GetVersion(); got != recreated.GetMetadata().GetVersion() { - t.Errorf("version = %d, want %d: the rejected update still wrote", got, recreated.GetMetadata().GetVersion()) - } -} - -func TestUpdateActor_RejectsStaleVersion(t *testing.T) { - _, s, ctx := setupTest(t) - - created, err := s.CreateActor(ctx, newTestActor("actor-1")) - if err != nil { - t.Fatalf("CreateActor failed: %v", err) - } - actorRef := resources.ActorRefFromActor(created) - - if _, err := s.UpdateActor(ctx, actorRef, func(toUpdate *ateapipb.Actor) error { - toUpdate.Status = ateapipb.Actor_STATUS_RUNNING - return nil - }); err != nil { - t.Fatalf("UpdateActor failed: %v", err) - } - - // The write above moved the version, so created is now a stale observation. - _, err = s.UpdateActor(ctx, actorRef, store.WithPrecondition(created, func(toUpdate *ateapipb.Actor) error { - t.Error("mutate ran past its precondition once the pinned version had moved") - toUpdate.Status = ateapipb.Actor_STATUS_SUSPENDED - return nil - })) - if !errors.Is(err, store.ErrVersionConflict) { - t.Errorf("UpdateActor error = %v, want one matching store.ErrVersionConflict", err) - } - // The uid still matches, so this is not the incarnation failure: callers key - // their retry decision off the difference. - if errors.Is(err, store.ErrUIDConflict) { - t.Errorf("UpdateActor error = %v, want no store.ErrUIDConflict match: the incarnation is unchanged", err) - } - -} - -func TestUpdateWorker_NotFound(t *testing.T) { - mr, s, ctx := setupTest(t) - defer mr.Close() - - worker := &ateapipb.Worker{ - WorkerNamespace: "default", - WorkerPool: "pool-1", - WorkerPod: "non-existent", - } - err := s.UpdateWorker(ctx, worker, 1) - if !errors.Is(err, store.ErrNotFound) { - t.Errorf("expected store.ErrNotFound, got %v", err) - } -} - -func TestGetWorker_NotFound(t *testing.T) { - _, s, ctx := setupTest(t) - - _, err := s.GetWorker(ctx, "default", "pool-1", "non-existent") - if !errors.Is(err, store.ErrNotFound) { - t.Errorf("expected ErrNotFound, got %v", err) - } -} - -func TestCreateWorker_Success(t *testing.T) { - _, s, ctx := setupTest(t) - - watch, err := s.WatchWorkers(ctx) - if err != nil { - t.Fatalf("WatchWorkers failed: %v", err) - } - - worker := &ateapipb.Worker{ - WorkerNamespace: "default", - WorkerPool: "pool-1", - WorkerPod: "pod-1", - } - - err = s.CreateWorker(ctx, worker) - if err != nil { - t.Fatalf("CreateWorker failed: %v", err) - } - - got, err := s.GetWorker(ctx, "default", "pool-1", "pod-1") - if err != nil { - t.Fatalf("GetWorker failed: %v", err) - } - - if got.Version != 1 { - t.Errorf("expected version 1, got %d", got.Version) - } - - worker.Version = 1 - if diff := cmp.Diff(worker, got, protocmp.Transform()); diff != "" { - t.Errorf("GetWorker returned unexpected worker (-want +got):\n%s", diff) - } - - event := receiveEvent(t, watch.Events) - if event.Type != store.WorkerEventCreated { - t.Errorf("expected WorkerEventCreated, got %v", event.Type) - } - if diff := cmp.Diff(worker, event.Worker, protocmp.Transform()); diff != "" { - t.Errorf("created event worker mismatch (-want +got):\n%s", diff) - } -} - -func TestUpdateWorker_Success(t *testing.T) { - _, s, ctx := setupTest(t) - - worker := &ateapipb.Worker{ - WorkerNamespace: "default", - WorkerPool: "pool-1", - WorkerPod: "pod-1", - } - - if err := s.CreateWorker(ctx, worker); err != nil { - t.Fatalf("CreateWorker failed: %v", err) - } - - // Subscribe after create so the create event doesn't pollute the channel. - watch, err := s.WatchWorkers(ctx) - if err != nil { - t.Fatalf("WatchWorkers failed: %v", err) - } - - worker.Assignment = &ateapipb.Assignment{ - ActorTemplate: &ateapipb.KubeNamespacedObjectRef{ - Namespace: "default", - Name: "test-template", - }, - Actor: &ateapipb.ObjectRef{ - Name: "actor-1", - }, - ActorUid: "actor-1-uid", - } - - if err := s.UpdateWorker(ctx, worker, 1); err != nil { - t.Fatalf("UpdateWorker failed: %v", err) - } - - got, err := s.GetWorker(ctx, "default", "pool-1", "pod-1") - if err != nil { - t.Fatalf("GetWorker failed: %v", err) - } - - if got.Version != 2 { - t.Errorf("expected version 2, got %d", got.Version) - } - - worker.Version = 2 - if diff := cmp.Diff(worker, got, protocmp.Transform()); diff != "" { - t.Errorf("UpdateWorker yielded unexpected state in DB (-want +got):\n%s", diff) - } - - event := receiveEvent(t, watch.Events) - if event.Type != store.WorkerEventUpdated { - t.Errorf("expected WorkerEventUpdated, got %v", event.Type) - } - if diff := cmp.Diff(worker, event.Worker, protocmp.Transform()); diff != "" { - t.Errorf("updated event worker mismatch (-want +got):\n%s", diff) - } -} - -func TestDeleteWorker(t *testing.T) { - _, s, ctx := setupTest(t) - - worker := &ateapipb.Worker{ - WorkerNamespace: "default", - WorkerPool: "pool-1", - WorkerPod: "pod-1", - } - - if err := s.CreateWorker(ctx, worker); err != nil { - t.Fatalf("CreateWorker failed: %v", err) - } - - // Subscribe after create so the create event doesn't pollute the channel. - watch, err := s.WatchWorkers(ctx) - if err != nil { - t.Fatalf("WatchWorkers failed: %v", err) - } - - if err := s.DeleteWorker(ctx, "default", "pool-1", "pod-1"); err != nil { - t.Fatalf("DeleteWorker failed: %v", err) - } - - _, err = s.GetWorker(ctx, "default", "pool-1", "pod-1") - if !errors.Is(err, store.ErrNotFound) { - t.Errorf("expected ErrNotFound after delete, got %v", err) - } - - event := receiveEvent(t, watch.Events) - if event.Type != store.WorkerEventDeleted { - t.Errorf("expected WorkerEventDeleted, got %v", event.Type) - } - want := &ateapipb.Worker{WorkerNamespace: "default", WorkerPod: "pod-1"} - if diff := cmp.Diff(want, event.Worker, protocmp.Transform()); diff != "" { - t.Errorf("deleted event worker mismatch (-want +got):\n%s", diff) - } -} - -func TestDeleteActor(t *testing.T) { - tests := []struct { - name string - status ateapipb.Actor_Status - wantErr error - }{ - {name: "suspended", status: ateapipb.Actor_STATUS_SUSPENDED, wantErr: store.ErrFailedPrecondition}, - {name: "crashed", status: ateapipb.Actor_STATUS_CRASHED, wantErr: store.ErrFailedPrecondition}, - {name: "deleting", status: ateapipb.Actor_STATUS_DELETING}, - {name: "running", status: ateapipb.Actor_STATUS_RUNNING, wantErr: store.ErrFailedPrecondition}, - {name: "paused", status: ateapipb.Actor_STATUS_PAUSED, wantErr: store.ErrFailedPrecondition}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - _, s, ctx := setupTest(t) - - actor := &ateapipb.Actor{ - Metadata: &ateapipb.ResourceMetadata{Name: "actor-1", Atespace: testAtespace}, - ActorTemplateNamespace: "default", - ActorTemplateName: "test-template", - Status: tt.status, - } - - if _, err := s.CreateActor(ctx, actor); err != nil { - t.Fatalf("CreateActor failed: %v", err) - } - - deleted, err := s.DeleteActor(ctx, resources.ActorRef{Atespace: testAtespace, Name: "actor-1"}) - if tt.wantErr != nil { - if !errors.Is(err, tt.wantErr) { - t.Errorf("DeleteActor: expected %v, got %v", tt.wantErr, err) - } - return - } - if err != nil { - t.Fatalf("DeleteActor failed: %v", err) - } - // DeleteActor returns the deleted resource. - if got := deleted.GetMetadata().GetName(); got != "actor-1" { - t.Errorf("deleted actor name = %q, want actor-1", got) - } - - if _, err := s.GetActor(ctx, resources.ActorRef{Atespace: testAtespace, Name: "actor-1"}); !errors.Is(err, store.ErrNotFound) { - t.Errorf("expected ErrNotFound after delete, got %v", err) - } - }) - } -} - -func TestDeleteActor_NotFound(t *testing.T) { - _, s, ctx := setupTest(t) - - _, err := s.DeleteActor(ctx, resources.ActorRef{Atespace: testAtespace, Name: "non-existent"}) - if !errors.Is(err, store.ErrNotFound) { - t.Errorf("expected ErrNotFound deleting non-existent actor, got %v", err) - } -} - -func TestListWorkers(t *testing.T) { - _, s, ctx := setupTest(t) - - worker1 := &ateapipb.Worker{ - WorkerNamespace: "ns1", - WorkerPool: "pool1", - WorkerPod: "pod1", - } - worker2 := &ateapipb.Worker{ - WorkerNamespace: "ns1", - WorkerPool: "pool1", - WorkerPod: "pod2", - } - if err := s.CreateWorker(ctx, worker1); err != nil { - t.Fatalf("failed to create worker1: %v", err) - } - if err := s.CreateWorker(ctx, worker2); err != nil { - t.Fatalf("failed to create worker2: %v", err) - } - - workersResp, err := s.ListWorkers(ctx, store.ListOptions{PageSize: 1000}) - if err != nil { - t.Fatalf("ListWorkers failed: %v", err) - } - workers := workersResp.Items - - if len(workers) != 2 { - t.Errorf("expected 2 workers, got %d", len(workers)) - } - - found1 := false - found2 := false - for _, w := range workers { - if w.GetWorkerPod() == "pod1" { - found1 = true - } - if w.GetWorkerPod() == "pod2" { - found2 = true - } - } - if !found1 || !found2 { - t.Errorf("did not find all workers: found1=%t, found2=%t", found1, found2) - } -} - -func TestListActors(t *testing.T) { - _, s, ctx := setupTest(t) - - actor1 := &ateapipb.Actor{ - - Metadata: &ateapipb.ResourceMetadata{Name: "id1", Atespace: testAtespace}, - ActorTemplateNamespace: "ns1", - ActorTemplateName: "tmpl1", - Status: ateapipb.Actor_STATUS_SUSPENDED, - LatestSnapshot: &ateapipb.ObjectRef{Atespace: testAtespace, Name: "snapshot-1"}, - } - actor2 := &ateapipb.Actor{ - Metadata: &ateapipb.ResourceMetadata{Name: "id2", Atespace: testAtespace}, - ActorTemplateNamespace: "ns1", - ActorTemplateName: "tmpl1", - Status: ateapipb.Actor_STATUS_SUSPENDED, - LatestSnapshot: &ateapipb.ObjectRef{Atespace: testAtespace, Name: "snapshot-2"}, - } - - if _, err := s.CreateActor(ctx, actor1); err != nil { - t.Fatalf("failed to create actor1: %v", err) - } - if _, err := s.CreateActor(ctx, actor2); err != nil { - t.Fatalf("failed to create actor2: %v", err) - } - - actorsResp, err := s.ListActors(ctx, "", store.ListOptions{PageSize: 1000}) - if err != nil { - t.Fatalf("ListActors failed: %v", err) - } - actors := actorsResp.Items - - if len(actors) != 2 { - t.Errorf("expected 2 actors, got %d", len(actors)) - } - - found1 := false - found2 := false - for _, a := range actors { - if a.GetMetadata().GetName() == "id1" { - found1 = true - } - if a.GetMetadata().GetName() == "id2" { - found2 = true - } - } - if !found1 || !found2 { - t.Errorf("did not find all actors: found1=%t, found2=%t", found1, found2) - } -} - -func TestActorSnapshotLifecycle(t *testing.T) { - _, s, ctx := setupTest(t) - snapshot := &ateapipb.ActorSnapshot{ - Metadata: &ateapipb.ResourceMetadata{Atespace: testAtespace, Name: "snapshot-1"}, - SourceActor: &ateapipb.ObjectRef{Atespace: testAtespace, Name: "actor-1"}, - SourceActorUid: "actor-uid", - SourceActorVersion: 7, - ContentScope: ateapipb.SnapshotContentScope_SNAPSHOT_CONTENT_SCOPE_FULL, - SnapshotUri: "gs://bucket/root/snapshots/" + testAtespace + "/snapshot-1", - } - created, err := s.CreateActorSnapshot(ctx, snapshot) - if err != nil { - t.Fatalf("CreateActorSnapshot: %v", err) - } - got, err := s.GetActorSnapshot(ctx, testAtespace, "snapshot-1") - if err != nil { - t.Fatalf("GetActorSnapshot: %v", err) - } - // The store round-trips the whole resource, snapshot_uri included: it is - // an ordinary field now, not a value the store keeps beside the record. - if !proto.Equal(created, got) { - t.Fatalf("GetActorSnapshot = %v, want %v", got, created) - } - tag := &ateapipb.ActorSnapshotTag{ - Metadata: &ateapipb.ResourceMetadata{Atespace: testAtespace, Name: "before-upgrade"}, - Scope: ateapipb.ActorSnapshotTagScope_ACTOR_SNAPSHOT_TAG_SCOPE_ATESPACE, - } - tagged, err := s.CreateActorSnapshotTag(ctx, testAtespace, "snapshot-1", tag) - if err != nil || tagged.GetSnapshot().GetName() != "snapshot-1" { - t.Fatalf("CreateActorSnapshotTag = (%v, %v), want stable tag", tagged, err) - } - resolvedTag, err := s.GetActorSnapshotTag(ctx, testAtespace, "before-upgrade") - if err != nil || !proto.Equal(tagged, resolvedTag) { - t.Fatalf("GetActorSnapshotTag = (%v, %v), want tagged tag", resolvedTag, err) - } - byTag, err := s.GetActorSnapshot(ctx, resolvedTag.GetSnapshot().GetAtespace(), resolvedTag.GetSnapshot().GetName()) - if err != nil || !proto.Equal(created, byTag) { - t.Fatalf("GetActorSnapshot(resolved tag target) = (%v, %v), want tagged snapshot", byTag, err) - } - if _, err := s.CreateActorSnapshot(ctx, &ateapipb.ActorSnapshot{ - Metadata: &ateapipb.ResourceMetadata{Atespace: "other", Name: "snapshot-2"}, - SnapshotUri: "gs://bucket/root/snapshots/other/snapshot-2", - }); err != nil { - t.Fatalf("CreateActorSnapshot second snapshot: %v", err) - } - otherTag := &ateapipb.ActorSnapshotTag{Metadata: &ateapipb.ResourceMetadata{Atespace: "other", Name: "before-upgrade"}, Scope: ateapipb.ActorSnapshotTagScope_ACTOR_SNAPSHOT_TAG_SCOPE_ATESPACE} - if _, err := s.CreateActorSnapshotTag(ctx, "other", "snapshot-2", otherTag); err != nil { - t.Fatalf("same tag name in another Atespace: %v", err) - } - if _, err := s.CreateActorSnapshotTag(ctx, "other", "snapshot-2", tag); !errors.Is(err, store.ErrAlreadyExists) { - t.Fatalf("duplicate Atespace tag error = %v, want ErrAlreadyExists", err) - } - differentScope := proto.Clone(tag).(*ateapipb.ActorSnapshotTag) - differentScope.Scope = ateapipb.ActorSnapshotTagScope_ACTOR_SNAPSHOT_TAG_SCOPE_PUBLISHED - if _, err := s.CreateActorSnapshotTag(ctx, testAtespace, "snapshot-1", differentScope); !errors.Is(err, store.ErrAlreadyExists) { - t.Fatalf("re-tag with different scope error = %v, want ErrAlreadyExists", err) - } - tagged, err = s.UpdateActorSnapshotTag(ctx, testAtespace, "before-upgrade", func(toUpdate *ateapipb.ActorSnapshotTag) error { - toUpdate.Scope = ateapipb.ActorSnapshotTagScope_ACTOR_SNAPSHOT_TAG_SCOPE_PUBLISHED - return nil - }) - if err != nil || tagged.GetScope() != ateapipb.ActorSnapshotTagScope_ACTOR_SNAPSHOT_TAG_SCOPE_PUBLISHED { - t.Fatalf("UpdateActorSnapshotTag = (%v, %v), want published", tagged, err) - } - if resolvedTag, err = s.GetActorSnapshotTag(ctx, testAtespace, "before-upgrade"); err != nil || resolvedTag.GetScope() != ateapipb.ActorSnapshotTagScope_ACTOR_SNAPSHOT_TAG_SCOPE_PUBLISHED { - t.Fatalf("tag after publication = (%v, %v), want published scope", resolvedTag, err) - } - if byTag, err = s.GetActorSnapshot(ctx, resolvedTag.GetSnapshot().GetAtespace(), resolvedTag.GetSnapshot().GetName()); err != nil || byTag.GetMetadata().GetUid() != created.GetMetadata().GetUid() { - t.Fatalf("snapshot after publication = (%v, %v), want same address", byTag, err) - } - listed, err := s.ListActorSnapshots(ctx, testAtespace, store.ListOptions{PageSize: 10}) - if err != nil || len(listed.Items) != 1 { - t.Fatalf("ListActorSnapshots = (%v, %v), want one", listed.Items, err) - } - - deleted, err := s.DeleteActorSnapshotTag(ctx, testAtespace, "before-upgrade") - if err != nil || deleted.GetMetadata().GetName() != "before-upgrade" { - t.Fatalf("DeleteActorSnapshotTag = (%v, %v)", deleted, err) - } - if _, err := s.GetActorSnapshotTag(ctx, testAtespace, "before-upgrade"); !errors.Is(err, store.ErrNotFound) { - t.Fatalf("deleted tag lookup = %v, want ErrNotFound", err) - } - if got, err := s.GetActorSnapshot(ctx, testAtespace, "snapshot-1"); err != nil || got.GetMetadata().GetUid() != created.GetMetadata().GetUid() { - t.Fatalf("snapshot after tag deletion = (%v, %v), want retained metadata", got, err) - } -} - -// seedTaggedSnapshot stores a snapshot and an Atespace-scoped tag pointing at -// it, and returns the stored tag. -func seedTaggedSnapshot(t *testing.T, s *Persistence, ctx context.Context, snapshotName, tagName string) *ateapipb.ActorSnapshotTag { - t.Helper() - if _, err := s.CreateActorSnapshot(ctx, &ateapipb.ActorSnapshot{ - Metadata: &ateapipb.ResourceMetadata{Atespace: testAtespace, Name: snapshotName}, - SnapshotUri: "gs://bucket/root/snapshots/" + testAtespace + "/" + snapshotName, - }); err != nil { - t.Fatalf("CreateActorSnapshot(%s) failed: %v", snapshotName, err) - } - tagged, err := s.CreateActorSnapshotTag(ctx, testAtespace, snapshotName, &ateapipb.ActorSnapshotTag{ - Metadata: &ateapipb.ResourceMetadata{Atespace: testAtespace, Name: tagName}, - Scope: ateapipb.ActorSnapshotTagScope_ACTOR_SNAPSHOT_TAG_SCOPE_ATESPACE, - }) - if err != nil { - t.Fatalf("CreateActorSnapshotTag(%s) failed: %v", tagName, err) - } - return tagged -} - -func TestUpdateActorSnapshotTag_MutateErrorAreNotRetried(t *testing.T) { - _, s, ctx := setupTest(t) - tagged := seedTaggedSnapshot(t, s, ctx, "snapshot-1", "tag-1") - - var mutationError = errors.New("mutation error") - - callsToMutateFn := 0 - _, err := s.UpdateActorSnapshotTag(ctx, testAtespace, "tag-1", func(toUpdate *ateapipb.ActorSnapshotTag) error { - callsToMutateFn++ - toUpdate.Scope = ateapipb.ActorSnapshotTagScope_ACTOR_SNAPSHOT_TAG_SCOPE_PUBLISHED - return fmt.Errorf("tag %s/%s: %w", testAtespace, "tag-1", mutationError) - }) - // The error must arrive intact - if !errors.Is(err, mutationError) { - t.Errorf("UpdateActorSnapshotTag error = %v, want one wrapping mutationError", err) - } - // Mutation errors are non-retriable - if callsToMutateFn != 1 { - t.Errorf("mutate ran %d times, want exactly 1 (a rejected precondition must not be retried)", callsToMutateFn) - } - - got, err := s.GetActorSnapshotTag(ctx, testAtespace, "tag-1") - if err != nil { - t.Fatalf("GetActorSnapshotTag failed: %v", err) - } - if diff := cmp.Diff(tagged, got, protocmp.Transform()); diff != "" { - t.Errorf("aborted mutation was persisted (-tagged +got):\n%s", diff) - } -} - -func TestUpdateActorSnapshotTag_DiscardsServerOwnedFieldsEdits(t *testing.T) { - _, s, ctx := setupTest(t) - tagged := seedTaggedSnapshot(t, s, ctx, "snapshot-1", "tag-1") - - updated, err := s.UpdateActorSnapshotTag(ctx, testAtespace, "tag-1", func(toUpdate *ateapipb.ActorSnapshotTag) error { - // Metadata is server-owned: a closure must not be able to change it. - toUpdate.Metadata.Uid = "forged-uid" - toUpdate.Metadata.Version = 99 - toUpdate.Metadata.CreateTime = nil - toUpdate.Metadata.UpdateTime = nil - toUpdate.Scope = ateapipb.ActorSnapshotTagScope_ACTOR_SNAPSHOT_TAG_SCOPE_PUBLISHED - return nil - }) - if err != nil { - t.Fatalf("UpdateActorSnapshotTag failed: %v", err) - } - - if got := updated.GetMetadata().GetUid(); got != tagged.GetMetadata().GetUid() { - t.Errorf("uid = %q, want the server-assigned %q", got, tagged.GetMetadata().GetUid()) - } - if got := updated.GetMetadata().GetVersion(); got != tagged.GetMetadata().GetVersion()+1 { - t.Errorf("version = %d, want %d (one past the stored version, not the forged value)", got, tagged.GetMetadata().GetVersion()+1) - } - if got := updated.GetMetadata().GetCreateTime(); got == nil || !got.AsTime().Equal(tagged.GetMetadata().GetCreateTime().AsTime()) { - t.Errorf("create_time = %v, want the creation value %v", got, tagged.GetMetadata().GetCreateTime()) - } - if got, want := updated.GetScope(), ateapipb.ActorSnapshotTagScope_ACTOR_SNAPSHOT_TAG_SCOPE_PUBLISHED; got != want { - t.Errorf("scope = %v, want %v: discarding metadata edits must not discard the mutation", got, want) - } -} - -// TestUpdateActorSnapshotTag_RejectsImmutableFieldChange covers the fields a -// mutation may not touch. Unlike the server-owned metadata, which is silently -// restored, these fail the call: a caller that renamed a tag or repointed it at -// another snapshot asked for something the store cannot do, and must hear about -// it. -func TestUpdateActorSnapshotTag_RejectsImmutableFieldChange(t *testing.T) { - tests := []struct { - name string - mutate func(toUpdate *ateapipb.ActorSnapshotTag) - wantField string - }{ - { - name: "atespace", - mutate: func(toUpdate *ateapipb.ActorSnapshotTag) { toUpdate.Metadata.Atespace = "other-atespace" }, - wantField: "metadata.atespace", - }, - { - name: "name", - mutate: func(toUpdate *ateapipb.ActorSnapshotTag) { toUpdate.Metadata.Name = "other-name" }, - wantField: "metadata.name", - }, - { - name: "snapshot atespace", - mutate: func(toUpdate *ateapipb.ActorSnapshotTag) { toUpdate.Snapshot.Atespace = "other-atespace" }, - wantField: "snapshot.atespace", - }, - { - name: "snapshot name", - mutate: func(toUpdate *ateapipb.ActorSnapshotTag) { toUpdate.Snapshot.Name = "other-snapshot" }, - wantField: "snapshot.name", - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - _, s, ctx := setupTest(t) - tagged := seedTaggedSnapshot(t, s, ctx, "snapshot-1", "tag-1") - - _, err := s.UpdateActorSnapshotTag(ctx, testAtespace, "tag-1", func(toUpdate *ateapipb.ActorSnapshotTag) error { - // Paired with a legitimate edit, so the rejection cannot be - // mistaken for a no-op mutation. - toUpdate.Scope = ateapipb.ActorSnapshotTagScope_ACTOR_SNAPSHOT_TAG_SCOPE_PUBLISHED - tt.mutate(toUpdate) - return nil - }) - // The message must name the offending field: the closure is buggy, - // and whoever has to fix it only has this error to go on. - if want := tt.wantField + " is immutable"; err == nil || !strings.Contains(err.Error(), want) { - t.Errorf("UpdateActorSnapshotTag changing %s = %v, want an error containing %q", tt.name, err, want) - } - - got, err := s.GetActorSnapshotTag(ctx, testAtespace, "tag-1") - if err != nil { - t.Fatalf("GetActorSnapshotTag failed: %v", err) - } - if diff := cmp.Diff(tagged, got, protocmp.Transform()); diff != "" { - t.Errorf("rejected mutation was persisted anyway (-tagged +got):\n%s", diff) - } - }) - } -} - -func TestUpdateActorSnapshotTag_RetriesOnConcurrentWrite(t *testing.T) { - mr, s, ctx := setupTest(t) - seedTaggedSnapshot(t, s, ctx, "snapshot-1", "tag-1") - tagKey := actorSnapshotTagDBKey(testAtespace, "tag-1") - - // A separate client, so its write lands outside the transaction's connection. - otherClient := redis.NewClusterClient(&redis.ClusterOptions{Addrs: []string{mr.Addr()}}) - t.Cleanup(func() { otherClient.Close() }) - - attempts := 0 - interceptor := &watchInterceptor{redisClient: s.rdb, before: func() { - // Only the first attempt races. We do this to make sure the second retry - // will succeed. - if attempts > 0 { - return - } - concurrent, err := s.GetActorSnapshotTag(ctx, testAtespace, "tag-1") - if err != nil { - t.Errorf("GetActorSnapshotTag for concurrent write failed: %v", err) - return - } - // Repointing the tag is not something a mutation may do, but a writer - // holding the key can: the retry must carry it forward, not revert it. - concurrent.Snapshot = &ateapipb.ObjectRef{Atespace: testAtespace, Name: "snapshot-2"} - val, err := protojson.Marshal(concurrent) - if err != nil { - t.Errorf("protojson.Marshal failed: %v", err) - return - } - if err := otherClient.Set(ctx, tagKey, val, 0).Err(); err != nil { - t.Errorf("concurrent Set failed: %v", err) - } - }} - racing := &Persistence{rdb: interceptor, lockTTL: defaultLockTTL} - - updated, err := racing.UpdateActorSnapshotTag(ctx, testAtespace, "tag-1", func(toUpdate *ateapipb.ActorSnapshotTag) error { - attempts++ - toUpdate.Scope = ateapipb.ActorSnapshotTagScope_ACTOR_SNAPSHOT_TAG_SCOPE_PUBLISHED - return nil - }) - if err != nil { - t.Fatalf("UpdateActorSnapshotTag failed: %v", err) - } - if attempts < 2 { - t.Errorf("mutate ran %d times, want at least 2: the first write is racey and must be rejected", attempts) - } - if got, want := updated.GetScope(), ateapipb.ActorSnapshotTagScope_ACTOR_SNAPSHOT_TAG_SCOPE_PUBLISHED; got != want { - t.Errorf("scope = %v, want %v", got, want) - } - // The concurrent tx repointed the tag at snapshot-2. That change should - // survive instead of being reverted by a mutation computed against the - // older state. - if got := updated.GetSnapshot().GetName(); got != "snapshot-2" { - t.Errorf("snapshot.name = %q, want %q: the retry clobbered the concurrent write", got, "snapshot-2") - } -} - -func TestUpdateActorSnapshotTag_NotFound(t *testing.T) { - _, s, ctx := setupTest(t) - _, err := s.UpdateActorSnapshotTag(ctx, testAtespace, "does-not-exist", func(toUpdate *ateapipb.ActorSnapshotTag) error { - t.Error("mutate must not run for a missing tag") - return nil - }) - if !errors.Is(err, store.ErrNotFound) { - t.Errorf("expected store.ErrNotFound, got %v", err) - } -} - -func TestUpdateActorSnapshotTag_RejectsStaleUID(t *testing.T) { - _, s, ctx := setupTest(t) - - original := seedTaggedSnapshot(t, s, ctx, "snapshot-1", "tag-1") - if _, err := s.DeleteActorSnapshotTag(ctx, testAtespace, "tag-1"); err != nil { - t.Fatalf("DeleteActorSnapshotTag failed: %v", err) - } - recreated, err := s.CreateActorSnapshotTag(ctx, testAtespace, "snapshot-1", &ateapipb.ActorSnapshotTag{ - Metadata: &ateapipb.ResourceMetadata{Atespace: testAtespace, Name: "tag-1"}, - Scope: ateapipb.ActorSnapshotTagScope_ACTOR_SNAPSHOT_TAG_SCOPE_ATESPACE, - }) - if err != nil { - t.Fatalf("re-tag CreateActorSnapshotTag failed: %v", err) - } - if recreated.GetMetadata().GetUid() == original.GetMetadata().GetUid() { - t.Fatalf("recreated tag reused uid %s, want a fresh one", recreated.GetMetadata().GetUid()) - } - // The version reset to 1 along with the uid, so a version guard alone would - // have waved this write through. Only the uid distinguishes the lifecycles. - if got, want := recreated.GetMetadata().GetVersion(), original.GetMetadata().GetVersion(); got != want { - t.Fatalf("recreated version = %d, want %d: the version cannot tell the lifecycles apart", got, want) - } - - // Pins the incarnation alone: the observed tag carries the original uid and - // no version. - pinUID := &ateapipb.ActorSnapshotTag{ - Metadata: &ateapipb.ResourceMetadata{Uid: original.GetMetadata().GetUid(), Version: store.AnyVersion}, - } - _, err = s.UpdateActorSnapshotTag(ctx, testAtespace, "tag-1", store.WithPrecondition(pinUID, func(toUpdate *ateapipb.ActorSnapshotTag) error { - t.Error("mutate ran past its precondition once the pinned incarnation was gone") - toUpdate.Scope = ateapipb.ActorSnapshotTagScope_ACTOR_SNAPSHOT_TAG_SCOPE_PUBLISHED - return nil - })) - if !errors.Is(err, store.ErrUIDConflict) { - t.Errorf("UpdateActorSnapshotTag error = %v, want one matching store.ErrUIDConflict", err) - } - - // The version guard was waived, so this is the incarnation failure alone. - if errors.Is(err, store.ErrVersionConflict) { - t.Errorf("UpdateActorSnapshotTag error = %v, want no store.ErrVersionConflict match: no version was pinned", err) - } - - stored, err := s.GetActorSnapshotTag(ctx, testAtespace, "tag-1") - if err != nil { - t.Fatalf("GetActorSnapshotTag failed: %v", err) - } - if diff := cmp.Diff(recreated, stored, protocmp.Transform()); diff != "" { - t.Errorf("the rejected update still wrote (-recreated +stored):\n%s", diff) - } -} - -func TestUpdateActorSnapshotTag_RejectsStaleVersion(t *testing.T) { - _, s, ctx := setupTest(t) - - tagged := seedTaggedSnapshot(t, s, ctx, "snapshot-1", "tag-1") - - if _, err := s.UpdateActorSnapshotTag(ctx, testAtespace, "tag-1", func(toUpdate *ateapipb.ActorSnapshotTag) error { - toUpdate.Scope = ateapipb.ActorSnapshotTagScope_ACTOR_SNAPSHOT_TAG_SCOPE_PUBLISHED - return nil - }); err != nil { - t.Fatalf("UpdateActorSnapshotTag failed: %v", err) - } - - // The write above moved the version, so tagged is now a stale observation. - _, err := s.UpdateActorSnapshotTag(ctx, testAtespace, "tag-1", store.WithPrecondition(tagged, func(toUpdate *ateapipb.ActorSnapshotTag) error { - t.Error("mutate ran past its precondition once the pinned version had moved") - toUpdate.Scope = ateapipb.ActorSnapshotTagScope_ACTOR_SNAPSHOT_TAG_SCOPE_ATESPACE - return nil - })) - if !errors.Is(err, store.ErrVersionConflict) { - t.Errorf("UpdateActorSnapshotTag error = %v, want one matching store.ErrVersionConflict", err) - } - // The uid still matches, so this is not the incarnation failure: callers key - // their retry decision off the difference. - if errors.Is(err, store.ErrUIDConflict) { - t.Errorf("UpdateActorSnapshotTag error = %v, want no store.ErrUIDConflict match: the incarnation is unchanged", err) - } -} - -func TestUpdateWorker_Conflict(t *testing.T) { - _, s, ctx := setupTest(t) - - worker := &ateapipb.Worker{ - WorkerNamespace: "default", - WorkerPool: "pool-1", - WorkerPod: "pod-1", - } - - err := s.CreateWorker(ctx, worker) - if err != nil { - t.Fatalf("CreateWorker failed: %v", err) - } - - // Fetch instance 1 - worker1, err := s.GetWorker(ctx, "default", "pool-1", "pod-1") - if err != nil { - t.Fatalf("GetWorker failed: %v", err) - } - - // Fetch instance 2 - worker2, err := s.GetWorker(ctx, "default", "pool-1", "pod-1") - if err != nil { - t.Fatalf("GetWorker failed: %v", err) - } - - // Update instance 1 - worker1.Assignment = &ateapipb.Assignment{ - Actor: &ateapipb.ObjectRef{Atespace: "team-a", Name: "actor-1"}, - ActorUid: "actor-1-uid", - } - err = s.UpdateWorker(ctx, worker1, worker1.Version) - if err != nil { - t.Fatalf("UpdateWorker failed: %v", err) - } - - // Try to update instance 2 - worker2.Assignment = &ateapipb.Assignment{ - Actor: &ateapipb.ObjectRef{Atespace: "team-a", Name: "actor-2"}, - ActorUid: "actor-2-uid", - } - err = s.UpdateWorker(ctx, worker2, worker2.Version) - if !errors.Is(err, store.ErrVersionConflict) { - t.Errorf("expected ErrVersionConflict, got %v", err) - } -} - -func TestCreateWorker_AlreadyExists(t *testing.T) { - _, s, ctx := setupTest(t) - - worker := &ateapipb.Worker{ - WorkerNamespace: "default", - WorkerPool: "pool-1", - WorkerPod: "pod-1", - } - - err := s.CreateWorker(ctx, worker) - if err != nil { - t.Fatalf("CreateWorker failed: %v", err) - } - - err = s.CreateWorker(ctx, worker) - if !errors.Is(err, store.ErrAlreadyExists) { - t.Errorf("expected ErrAlreadyExists, got %v", err) - } -} - -func TestListWorkers_Empty(t *testing.T) { - _, s, ctx := setupTest(t) - - workers, err := s.ListWorkers(ctx, store.ListOptions{PageSize: 1000}) - if err != nil { - t.Fatalf("ListWorkers failed: %v", err) - } - - if len(workers.Items) != 0 { - t.Errorf("expected 0 workers, got %d", len(workers.Items)) - } -} - -func TestListWorkers_Pagination(t *testing.T) { - _, s, ctx := setupTest(t) - - for i := 0; i < 5; i++ { - worker := &ateapipb.Worker{ - WorkerNamespace: "ns1", - WorkerPool: "pool1", - WorkerPod: fmt.Sprintf("pod%d", i), - } - if err := s.CreateWorker(ctx, worker); err != nil { - t.Fatalf("failed to create worker %d: %v", i, err) - } - } - - var allWorkers []*ateapipb.Worker - pageToken := "" - - for { - page, err := s.ListWorkers(ctx, store.ListOptions{PageSize: 2, PageToken: pageToken}) - if err != nil { - t.Fatalf("ListWorkers failed: %v", err) - } - - allWorkers = append(allWorkers, page.Items...) - pageToken = page.NextPageToken - if pageToken == "" { - break - } - } - - if len(allWorkers) != 5 { - t.Fatalf("expected 5 workers total, got %d", len(allWorkers)) - } - - seen := make(map[string]bool) - for _, w := range allWorkers { - if seen[w.GetWorkerPod()] { - t.Errorf("duplicate worker found in paginated results: %s", w.GetWorkerPod()) - } - seen[w.GetWorkerPod()] = true - } -} - -func TestListAtespaces_Pagination(t *testing.T) { - _, s, ctx := setupTest(t) - - for i := 0; i < 5; i++ { - if _, err := s.CreateAtespace(ctx, newTestAtespace(fmt.Sprintf("team-%d", i))); err != nil { - t.Fatalf("failed to create atespace %d: %v", i, err) - } - } - - var allAtespaces []*ateapipb.Atespace - pageToken := "" - - for { - page, err := s.ListAtespaces(ctx, store.ListOptions{PageSize: 2, PageToken: pageToken}) - if err != nil { - t.Fatalf("ListAtespaces failed: %v", err) - } - - allAtespaces = append(allAtespaces, page.Items...) - pageToken = page.NextPageToken - if pageToken == "" { - break - } - } - - if len(allAtespaces) != 5 { - t.Fatalf("expected 5 atespaces total, got %d", len(allAtespaces)) - } - - seen := make(map[string]bool) - for _, a := range allAtespaces { - if seen[a.GetMetadata().GetName()] { - t.Errorf("duplicate atespace found in paginated results: %s", a.GetMetadata().GetName()) - } - seen[a.GetMetadata().GetName()] = true - } -} - -func TestListActors_Empty(t *testing.T) { - _, s, ctx := setupTest(t) - - actors, err := s.ListActors(ctx, "", store.ListOptions{PageSize: 1000}) - if err != nil { - t.Fatalf("ListActors failed: %v", err) - } - - if len(actors.Items) != 0 { - t.Errorf("expected 0 actors, got %d", len(actors.Items)) - } -} - -func TestListActors_Pagination(t *testing.T) { - _, s, ctx := setupTest(t) - - for i := 0; i < 5; i++ { - actor := &ateapipb.Actor{ - Metadata: &ateapipb.ResourceMetadata{Name: fmt.Sprintf("name%d", i), Atespace: testAtespace}, - ActorTemplateNamespace: "ns1", - ActorTemplateName: "tmpl1", - Status: ateapipb.Actor_STATUS_SUSPENDED, - } - if _, err := s.CreateActor(ctx, actor); err != nil { - t.Fatalf("failed to create actor %d: %v", i, err) - } - } - - var allActors []*ateapipb.Actor - pageToken := "" - - for { - page, err := s.ListActors(ctx, "", store.ListOptions{PageSize: 2, PageToken: pageToken}) - if err != nil { - t.Fatalf("ListActors failed: %v", err) - } - - allActors = append(allActors, page.Items...) - pageToken = page.NextPageToken - if pageToken == "" { - break - } - } - - if len(allActors) != 5 { - t.Fatalf("expected 5 actors total, got %d", len(allActors)) - } - - seen := make(map[string]bool) - for _, a := range allActors { - if seen[a.GetMetadata().GetName()] { - t.Errorf("duplicate actor found in paginated results: %s", a.GetMetadata().GetName()) - } - seen[a.GetMetadata().GetName()] = true - } -} - -func TestAcquireLock_Success(t *testing.T) { - mr, s, ctx := setupTest(t) - - key := "test-lock" - - lock, err := s.AcquireLock(ctx, key) - if err != nil { - t.Fatalf("AcquireLock failed: %v", err) - } - defer lock.Close() - - if !mr.Exists(key) { - t.Errorf("expected lock key to exist after AcquireLock") - } -} - -func TestAcquireLock_Conflict(t *testing.T) { - _, s, ctx := setupTest(t) - - key := "test-lock" - - lock, err := s.AcquireLock(ctx, key) - if err != nil { - t.Fatalf("first AcquireLock failed: %v", err) - } - defer lock.Close() - - _, err = s.AcquireLock(ctx, key) - if !errors.Is(err, store.ErrLockConflict) { - t.Errorf("second AcquireLock error = %v, want ErrLockConflict", err) - } -} - -func TestLock_Close_ReleasesLockImmediately(t *testing.T) { - mr, s, ctx := setupTest(t) - - key := "test-lock" - - lock, err := s.AcquireLock(ctx, key) - if err != nil { - t.Fatalf("AcquireLock failed: %v", err) - } - - lock.Close() - - // Close should release the key immediately rather than making the next - // caller wait out the rest of the TTL. - if mr.Exists(key) { - t.Errorf("expected lock to be deleted after Close") - } - - next, err := s.AcquireLock(ctx, key) - if err != nil { - t.Fatalf("AcquireLock after Close failed: %v", err) - } - next.Close() -} - -func TestLock_Close_CancelsContext(t *testing.T) { - _, s, ctx := setupTest(t) - - lock, err := s.AcquireLock(ctx, "test-lock") - if err != nil { - t.Fatalf("AcquireLock failed: %v", err) - } - - lock.Close() - - select { - case <-lock.Context().Done(): - case <-time.After(time.Second): - t.Fatal("expected lock.Context() to be cancelled after Close") - } -} - -func TestLock_Close_ReleasesEvenAfterParentContextCancelled(t *testing.T) { - mr, s, _ := setupTest(t) - - key := "test-lock" - parentCtx, parentCancel := context.WithCancel(context.Background()) - - lock, err := s.AcquireLock(parentCtx, key) - if err != nil { - t.Fatalf("AcquireLock failed: %v", err) - } - - // Simulate the caller's own context dying independently of Close, e.g. an - // upstream RPC deadline. The renewal loop should stop as a result. - parentCancel() - - select { - case <-lock.Context().Done(): - case <-time.After(time.Second): - t.Fatal("expected lock.Context() to be cancelled once the parent context is cancelled") - } - - // A real caller's `defer lock.Close()` still runs after this. Close must - // still release the key even though the context it was acquired with is - // already dead, since it releases via context.Background() internally. - lock.Close() - - if mr.Exists(key) { - t.Errorf("expected Close to release the lock even though the parent context was already cancelled") - } -} - -func TestAcquireLock_ExpiresAndIsReacquirableAfterHolderCrashes(t *testing.T) { - mr, s, _ := setupTest(t) - - key := "test-lock" - ttl := 300 * time.Millisecond - s.lockTTL = ttl - - parentCtx, parentCancel := context.WithCancel(context.Background()) - lock, err := s.AcquireLock(parentCtx, key) - if err != nil { - t.Fatalf("AcquireLock failed: %v", err) - } - - // Simulate a hard crash: the holder disappears without ever calling - // Close (e.g. the process is killed), so the key is never explicitly - // released and is left to expire on its own TTL. Canceling the parent - // context stops the renewal loop the same way process death would, - // without releasing the key the way Close does. - parentCancel() - select { - case <-lock.Context().Done(): - case <-time.After(time.Second): - t.Fatal("expected lock.Context() to be cancelled once the parent context is cancelled") - } - - if !mr.Exists(key) { - t.Fatal("expected the key to still exist right after the crash; only Close deletes it") - } - if _, err := s.AcquireLock(context.Background(), key); !errors.Is(err, store.ErrLockConflict) { - t.Errorf("AcquireLock before TTL expiry: err = %v, want ErrLockConflict", err) - } - - // Simulate real time passing with no renewer left alive, until the key's - // actual Redis TTL elapses. miniredis's TTLs are purely virtual -- - // stored durations decremented only by FastForward, never by wall-clock - // time -- so a real time.Sleep here would not expire the key at all. - mr.FastForward(ttl + time.Second) - - if mr.Exists(key) { - t.Fatal("expected the key to have expired in Redis once its TTL elapsed") - } - - newOwner, err := s.AcquireLock(context.Background(), key) - if err != nil { - t.Fatalf("AcquireLock after crash + TTL expiry failed: %v", err) - } - defer newOwner.Close() -} - -func TestLock_Close_DoesNotStealALockReacquiredAfterLeaseLoss(t *testing.T) { - mr, s, ctx := setupTest(t) - - key := "test-lock" - ttl := 300 * time.Millisecond - s.lockTTL = ttl - - lock, err := s.AcquireLock(ctx, key) - if err != nil { - t.Fatalf("AcquireLock failed: %v", err) - } - - // Lose the lease out from under the renewal loop. - mr.Del(key) - select { - case <-lock.Context().Done(): - case <-time.After(time.Second): - t.Fatal("expected lock.Context() to be cancelled once the lease is lost") - } - - // A different holder acquires the same key once it's free. - newOwner, err := s.AcquireLock(ctx, key) - if err != nil { - t.Fatalf("AcquireLock by new owner failed: %v", err) - } - defer newOwner.Close() - - // The original Lock no longer owns the key; Close must be a safe no-op - // rather than deleting the new owner's lock out from under it. - lock.Close() - - if !mr.Exists(key) { - t.Errorf("expected the new owner's lock to survive the old Lock's Close, but the key was deleted") - } -} - -func TestLock_Close_Idempotent(t *testing.T) { - _, s, ctx := setupTest(t) - - lock, err := s.AcquireLock(ctx, "test-lock") - if err != nil { - t.Fatalf("AcquireLock failed: %v", err) - } - - lock.Close() - lock.Close() // must not panic or double-release. -} - -func TestRenewDeadlineFractionLeavesRetryHeadroom(t *testing.T) { - const minRetries = 2 - - intervalFraction := 1.0 / renewIntervalDivisor - retryPeriodFraction := 1.0 / renewRetryPeriodDivisor - floor := intervalFraction + minRetries*retryPeriodFraction - - if renewDeadlineFraction <= floor { - t.Fatalf("renewDeadlineFraction (%v) must exceed intervalFraction + %d*retryPeriodFraction (%v) to leave room for %d retries; "+ - "at or below intervalFraction (%v) alone, the very first renewal attempt would already find the deadline elapsed", - renewDeadlineFraction, minRetries, floor, minRetries, intervalFraction) - } -} - -func TestAcquireLock_RenewsUntilClosed(t *testing.T) { - synctest.Test(t, func(t *testing.T) { - mock := &mockRedisClient{SetNXFunc: acquires, EvalShaFunc: renews} - s := &Persistence{rdb: mock, lockTTL: defaultLockTTL} - - lock, err := s.AcquireLock(t.Context(), "test-lock") - if err != nil { - t.Fatalf("AcquireLock failed: %v", err) - } - defer lock.Close() - - time.Sleep(3 * defaultLockTTL) - synctest.Wait() - - if err := lock.Context().Err(); err != nil { - t.Errorf("lock.Context().Err() = %v, want nil (lease still held)", err) - } - }) -} - -func TestLock_ContextCancelled_OnLeaseLost(t *testing.T) { - synctest.Test(t, func(t *testing.T) { - mock := &mockRedisClient{SetNXFunc: acquires, EvalShaFunc: leaseLost} - s := &Persistence{rdb: mock, lockTTL: defaultLockTTL} - - lock, err := s.AcquireLock(t.Context(), "test-lock") - if err != nil { - t.Fatalf("AcquireLock failed: %v", err) - } - defer lock.Close() - - time.Sleep(defaultLockTTL) - synctest.Wait() - - if err := lock.Context().Err(); err == nil { - t.Error("expected lock.Context() to be cancelled once renewal detects the lease is lost") - } - }) -} - -func TestAcquireLock_RenewalRecoversFromTransientError(t *testing.T) { - synctest.Test(t, func(t *testing.T) { - // Clears with margin to spare before the renew deadline, after a - // couple of retryPeriod-spaced attempts. - renewDeadline := time.Duration(float64(defaultLockTTL) * renewDeadlineFraction) - retryPeriod := defaultLockTTL / renewRetryPeriodDivisor - errorClearsAt := time.Now().Add(renewDeadline - 2*retryPeriod) - - mock := &mockRedisClient{SetNXFunc: acquires, EvalShaFunc: failsUntil(errorClearsAt, errors.New("connection refused"))} - s := &Persistence{rdb: mock, lockTTL: defaultLockTTL} - - lock, err := s.AcquireLock(t.Context(), "test-lock") - if err != nil { - t.Fatalf("AcquireLock failed: %v", err) - } - defer lock.Close() - - time.Sleep(2 * defaultLockTTL) - synctest.Wait() - - if err := lock.Context().Err(); err != nil { - t.Errorf("lock.Context().Err() = %v, want nil (renewal should have recovered from the transient error)", err) - } - }) -} - -func TestAcquireLock_RenewalGivesUpOncePersistentErrorOutlastsTTL(t *testing.T) { - synctest.Test(t, func(t *testing.T) { - mock := &mockRedisClient{SetNXFunc: acquires, EvalShaFunc: failsWith(errors.New("connection refused"))} - s := &Persistence{rdb: mock, lockTTL: defaultLockTTL} - - lock, err := s.AcquireLock(t.Context(), "test-lock") - if err != nil { - t.Fatalf("AcquireLock failed: %v", err) - } - defer lock.Close() - - time.Sleep(defaultLockTTL) // past the renew deadline (renewDeadlineFraction * defaultLockTTL) - synctest.Wait() - - if err := lock.Context().Err(); err == nil { - t.Error("expected lock.Context() to be cancelled once the persistent error outlasts the renew deadline") - } - }) -} - -func TestAcquireLock_RenewalGivesUpWhenRedisHangsUntilDeadline(t *testing.T) { - synctest.Test(t, func(t *testing.T) { - mock := &mockRedisClient{SetNXFunc: acquires, EvalShaFunc: hangs} - s := &Persistence{rdb: mock, lockTTL: defaultLockTTL} - - lock, err := s.AcquireLock(t.Context(), "test-lock") - if err != nil { - t.Fatalf("AcquireLock failed: %v", err) - } - - time.Sleep(defaultLockTTL) - synctest.Wait() - - if err := lock.Context().Err(); err == nil { - t.Error("expected lock.Context() to be cancelled once every renewal attempt hangs past the renew deadline") - } - }) -} - -func TestAcquireLock_RenewalGivesUpAfterMixOfFastFailuresThenHang(t *testing.T) { - synctest.Test(t, func(t *testing.T) { - mock := &mockRedisClient{SetNXFunc: acquires, EvalShaFunc: failsNTimesThenHangs(2, errors.New("connection refused"))} - s := &Persistence{rdb: mock, lockTTL: defaultLockTTL} - - lock, err := s.AcquireLock(t.Context(), "test-lock") - if err != nil { - t.Fatalf("AcquireLock failed: %v", err) - } - - time.Sleep(defaultLockTTL) - synctest.Wait() - - if err := lock.Context().Err(); err == nil { - t.Error("expected lock.Context() to be cancelled once the renew deadline elapses, whether attempts fail fast or hang") - } - }) -} - -func receiveEvent(t *testing.T, ch <-chan store.WorkerEvent) store.WorkerEvent { - t.Helper() - select { - case event, ok := <-ch: - if !ok { - t.Fatal("watch channel closed unexpectedly") - } - return event - case <-time.After(2 * time.Second): - t.Fatal("timed out waiting for worker event") - return store.WorkerEvent{} // unreachable - } -} - -func TestListActors_ScopedByAtespace(t *testing.T) { - _, s, ctx := setupTest(t) - - mkActor := func(atespace, name string) *ateapipb.Actor { - return &ateapipb.Actor{ - Metadata: &ateapipb.ResourceMetadata{Name: name, Atespace: atespace}, - ActorTemplateNamespace: "ns1", - ActorTemplateName: "tmpl1", - Status: ateapipb.Actor_STATUS_SUSPENDED, - } - } - for _, a := range []*ateapipb.Actor{ - mkActor("team-a", "a1"), - mkActor("team-a", "a2"), - mkActor("team-b", "b1"), - } { - if _, err := s.CreateActor(ctx, a); err != nil { - t.Fatalf("CreateActor(%s/%s) failed: %v", a.GetMetadata().GetAtespace(), a.GetMetadata().GetName(), err) - } - } - - // List is scoped to one atespace. - teamA, err := s.ListActors(ctx, "team-a", store.ListOptions{PageSize: 1000}) - if err != nil { - t.Fatalf("ListActors(team-a) failed: %v", err) - } - if got := actorNameSet(teamA.Items); !got["a1"] || !got["a2"] || got["b1"] || len(got) != 2 { - t.Errorf("ListActors(team-a) = %v, want exactly {a1, a2}", got) - } - - teamB, err := s.ListActors(ctx, "team-b", store.ListOptions{PageSize: 1000}) - if err != nil { - t.Fatalf("ListActors(team-b) failed: %v", err) - } - if got := actorNameSet(teamB.Items); !got["b1"] || got["a1"] || len(got) != 1 { - t.Errorf("ListActors(team-b) = %v, want exactly {b1}", got) - } - - // An empty atespace lists across all atespaces (the admin/dev `-A` view). - all, err := s.ListActors(ctx, "", store.ListOptions{PageSize: 1000}) - if err != nil { - t.Fatalf("ListActors(all) failed: %v", err) - } - if got := actorNameSet(all.Items); !got["a1"] || !got["a2"] || !got["b1"] || len(got) != 3 { - t.Errorf("ListActors(all) = %v, want exactly {a1, a2, b1}", got) - } - - // Get is scoped too: right atespace hits, wrong/empty atespace misses. - if _, err := s.GetActor(ctx, resources.ActorRef{Atespace: "team-a", Name: "a1"}); err != nil { - t.Errorf("GetActor(team-a, a1) failed: %v", err) - } - if _, err := s.GetActor(ctx, resources.ActorRef{Atespace: "team-b", Name: "a1"}); !errors.Is(err, store.ErrNotFound) { - t.Errorf("GetActor(team-b, a1) = %v, want ErrNotFound", err) - } - if _, err := s.GetActor(ctx, resources.ActorRef{Atespace: "", Name: "a1"}); !errors.Is(err, store.ErrNotFound) { - t.Errorf("GetActor(empty, a1) = %v, want ErrNotFound", err) - } -} - -func actorNameSet(actors []*ateapipb.Actor) map[string]bool { - set := make(map[string]bool, len(actors)) - for _, a := range actors { - set[a.GetMetadata().GetName()] = true - } - return set -} - -func newTestAtespace(name string) *ateapipb.Atespace { - return &ateapipb.Atespace{Metadata: &ateapipb.ResourceMetadata{Name: name}} -} - -func TestCreateAtespace_Success(t *testing.T) { - _, s, ctx := setupTest(t) - - want := newTestAtespace("team-a") - created, err := s.CreateAtespace(ctx, want) - if err != nil { - t.Fatalf("CreateAtespace failed: %v", err) - } - - // CreateAtespace returns the stored resource with server-assigned metadata. - if created.GetMetadata().GetUid() == "" { - t.Errorf("CreateAtespace returned empty uid; want server-assigned uid") - } - if created.GetMetadata().GetVersion() != 1 { - t.Errorf("CreateAtespace returned version %d, want 1", created.GetMetadata().GetVersion()) - } - - // The returned resource is exactly what GetAtespace reads back. - got, err := s.GetAtespace(ctx, "team-a") - if err != nil { - t.Fatalf("GetAtespace failed: %v", err) - } - if diff := cmp.Diff(created, got, protocmp.Transform()); diff != "" { - t.Errorf("CreateAtespace return does not match stored state (-created +got):\n%s", diff) - } - - // want is the pre-create input; the server stamps uid, version, and timestamps. - if diff := cmp.Diff(want, created, protocmp.Transform(), ignoreUID, ignoreTimestamps, ignoreVersion); diff != "" { - t.Errorf("CreateAtespace returned unexpected atespace (-want +got):\n%s", diff) - } -} - -func TestCreateAtespace_AlreadyExists(t *testing.T) { - _, s, ctx := setupTest(t) - - if _, err := s.CreateAtespace(ctx, newTestAtespace("team-a")); err != nil { - t.Fatalf("first CreateAtespace failed: %v", err) - } - if _, err := s.CreateAtespace(ctx, newTestAtespace("team-a")); !errors.Is(err, store.ErrAlreadyExists) { - t.Errorf("expected ErrAlreadyExists, got %v", err) - } -} - -func TestGetAtespace_NotFound(t *testing.T) { - _, s, ctx := setupTest(t) - - if _, err := s.GetAtespace(ctx, "nope"); !errors.Is(err, store.ErrNotFound) { - t.Errorf("expected ErrNotFound, got %v", err) - } -} - -func TestAtespaceExists(t *testing.T) { - _, s, ctx := setupTest(t) - - if ok, err := s.AtespaceExists(ctx, "team-a"); err != nil || ok { - t.Fatalf("AtespaceExists before create = (%v, %v), want (false, nil)", ok, err) - } - if _, err := s.CreateAtespace(ctx, newTestAtespace("team-a")); err != nil { - t.Fatalf("CreateAtespace failed: %v", err) - } - if ok, err := s.AtespaceExists(ctx, "team-a"); err != nil || !ok { - t.Fatalf("AtespaceExists after create = (%v, %v), want (true, nil)", ok, err) - } -} - -func TestListAtespaces(t *testing.T) { - _, s, ctx := setupTest(t) - - names := []string{"team-a", "team-b", "team-c"} - for _, n := range names { - if _, err := s.CreateAtespace(ctx, newTestAtespace(n)); err != nil { - t.Fatalf("CreateAtespace(%s) failed: %v", n, err) - } - } - gotResp, err := s.ListAtespaces(ctx, store.ListOptions{PageSize: 1000}) - if err != nil { - t.Fatalf("ListAtespaces failed: %v", err) - } - got := gotResp.Items - if len(got) != len(names) { - t.Fatalf("ListAtespaces returned %d atespaces, want %d", len(got), len(names)) - } - gotNames := map[string]bool{} - for _, a := range got { - gotNames[a.GetMetadata().GetName()] = true - } - for _, n := range names { - if !gotNames[n] { - t.Errorf("ListAtespaces missing %q; got %v", n, gotNames) - } - } -} - -func TestListAtespaces_Empty(t *testing.T) { - _, s, ctx := setupTest(t) - - got, err := s.ListAtespaces(ctx, store.ListOptions{PageSize: 1000}) - if err != nil { - t.Fatalf("ListAtespaces failed: %v", err) - } - if len(got.Items) != 0 { - t.Errorf("ListAtespaces on empty store = %v, want empty", got.Items) - } -} - -func TestDeleteAtespace_Empty(t *testing.T) { - _, s, ctx := setupTest(t) - - if _, err := s.CreateAtespace(ctx, newTestAtespace("team-a")); err != nil { - t.Fatalf("CreateAtespace failed: %v", err) - } - deleted, err := s.DeleteAtespace(ctx, "team-a") - if err != nil { - t.Fatalf("DeleteAtespace failed: %v", err) - } - // DeleteAtespace returns the deleted resource. - if got := deleted.GetMetadata().GetName(); got != "team-a" { - t.Errorf("deleted atespace name = %q, want team-a", got) - } - if _, err := s.GetAtespace(ctx, "team-a"); !errors.Is(err, store.ErrNotFound) { - t.Errorf("after delete, GetAtespace = %v, want ErrNotFound", err) - } -} - -func TestDeleteAtespace_WithTags_Rejected(t *testing.T) { - _, s, ctx := setupTest(t) - - if _, err := s.CreateAtespace(ctx, newTestAtespace("team-a")); err != nil { - t.Fatalf("CreateAtespace: %v", err) - } - if _, err := s.CreateActorSnapshot(ctx, &ateapipb.ActorSnapshot{ - Metadata: &ateapipb.ResourceMetadata{Atespace: "team-a", Name: "snapshot-1"}, - SnapshotUri: "gs://bucket/root/snapshots/team-a/snapshot-1", - }); err != nil { - t.Fatalf("CreateActorSnapshot: %v", err) - } - if _, err := s.CreateActorSnapshotTag(ctx, "team-a", "snapshot-1", &ateapipb.ActorSnapshotTag{ - Metadata: &ateapipb.ResourceMetadata{Atespace: "team-a", Name: "keep-me"}, - Scope: ateapipb.ActorSnapshotTagScope_ACTOR_SNAPSHOT_TAG_SCOPE_PUBLISHED, - }); err != nil { - t.Fatalf("CreateActorSnapshotTag: %v", err) - } - if _, err := s.DeleteAtespace(ctx, "team-a"); !errors.Is(err, store.ErrFailedPrecondition) { - t.Fatalf("DeleteAtespace = %v, want ErrFailedPrecondition", err) - } - if _, err := s.GetActorSnapshotTag(ctx, "team-a", "keep-me"); err != nil { - t.Fatalf("GetActorSnapshotTag after rejected deletion: %v", err) - } - if _, err := s.GetAtespace(ctx, "team-a"); err != nil { - t.Fatalf("GetAtespace after rejected deletion: %v", err) - } -} - -func TestDeleteAtespace_WithActorTemplates_Rejected(t *testing.T) { - _, s, ctx := setupTest(t) - - if _, err := s.CreateAtespace(ctx, newTestAtespace("team-a")); err != nil { - t.Fatalf("CreateAtespace: %v", err) - } - if _, err := s.CreateActorTemplate(ctx, newTestActorTemplate("team-a", "tmpl-a")); err != nil { - t.Fatalf("CreateActorTemplate: %v", err) - } - if _, err := s.DeleteAtespace(ctx, "team-a"); !errors.Is(err, store.ErrFailedPrecondition) { - t.Fatalf("DeleteAtespace with templates = %v, want ErrFailedPrecondition", err) - } - - if _, err := s.DeleteActorTemplate(ctx, resources.ActorTemplateRef{Atespace: "team-a", Name: "tmpl-a"}); err != nil { - t.Fatalf("DeleteActorTemplate: %v", err) - } - if _, err := s.DeleteAtespace(ctx, "team-a"); err != nil { - t.Errorf("DeleteAtespace after template removed = %v, want nil", err) - } -} - -func TestDeleteAtespace_WithActorTemplateVersions_Rejected(t *testing.T) { - _, s, ctx := setupTest(t) - - if _, err := s.CreateAtespace(ctx, newTestAtespace("team-a")); err != nil { - t.Fatalf("CreateAtespace: %v", err) - } - if _, err := s.CreateActorTemplateVersion(ctx, newTestActorTemplateVersion("team-a", "tmpl-a-v1", "tmpl-a")); err != nil { - t.Fatalf("CreateActorTemplateVersion: %v", err) - } - if _, err := s.DeleteAtespace(ctx, "team-a"); !errors.Is(err, store.ErrFailedPrecondition) { - t.Fatalf("DeleteAtespace with versions = %v, want ErrFailedPrecondition", err) - } - - if _, err := s.DeleteActorTemplateVersion(ctx, resources.ActorTemplateVersionRef{Atespace: "team-a", Name: "tmpl-a-v1"}); err != nil { - t.Fatalf("DeleteActorTemplateVersion: %v", err) - } - if _, err := s.DeleteAtespace(ctx, "team-a"); err != nil { - t.Errorf("DeleteAtespace after version removed = %v, want nil", err) - } -} - -func TestDeleteAtespace_NotFound(t *testing.T) { - _, s, ctx := setupTest(t) - - if _, err := s.DeleteAtespace(ctx, "nope"); !errors.Is(err, store.ErrNotFound) { - t.Errorf("expected ErrNotFound, got %v", err) - } -} - -func TestDeleteAtespace_NonEmpty_Rejected(t *testing.T) { - _, s, ctx := setupTest(t) - - if _, err := s.CreateAtespace(ctx, newTestAtespace("team-a")); err != nil { - t.Fatalf("CreateAtespace failed: %v", err) - } - if _, err := s.CreateActor(ctx, &ateapipb.Actor{Metadata: &ateapipb.ResourceMetadata{Name: "id1", Atespace: "team-a"}, Status: ateapipb.Actor_STATUS_SUSPENDED}); err != nil { - t.Fatalf("CreateActor failed: %v", err) - } - if _, err := s.DeleteAtespace(ctx, "team-a"); !errors.Is(err, store.ErrFailedPrecondition) { - t.Errorf("DeleteAtespace on non-empty = %v, want ErrFailedPrecondition", err) - } - // The atespace must survive a rejected delete. - if _, err := s.GetAtespace(ctx, "team-a"); err != nil { - t.Errorf("atespace should still exist after rejected delete, got %v", err) - } -} - -func TestDeleteAtespace_EmptyAfterActorsRemoved(t *testing.T) { - _, s, ctx := setupTest(t) - - if _, err := s.CreateAtespace(ctx, newTestAtespace("team-a")); err != nil { - t.Fatalf("CreateAtespace failed: %v", err) - } - if _, err := s.CreateActor(ctx, &ateapipb.Actor{Metadata: &ateapipb.ResourceMetadata{Name: "id1", Atespace: "team-a"}, Status: ateapipb.Actor_STATUS_DELETING}); err != nil { - t.Fatalf("CreateActor failed: %v", err) - } - if _, err := s.DeleteAtespace(ctx, "team-a"); !errors.Is(err, store.ErrFailedPrecondition) { - t.Fatalf("expected rejection while non-empty, got %v", err) - } - if _, err := s.DeleteActor(ctx, resources.ActorRef{Atespace: "team-a", Name: "id1"}); err != nil { - t.Fatalf("DeleteActor failed: %v", err) - } - if _, err := s.DeleteAtespace(ctx, "team-a"); err != nil { - t.Errorf("DeleteAtespace after actor removed = %v, want nil (re-scan should find it empty)", err) - } -} - -func TestDeleteAtespace_EmptyWhileOtherAtespaceNonEmpty(t *testing.T) { - _, s, ctx := setupTest(t) - - if _, err := s.CreateAtespace(ctx, newTestAtespace("team-a")); err != nil { - t.Fatalf("CreateAtespace(team-a) failed: %v", err) - } - if _, err := s.CreateAtespace(ctx, newTestAtespace("team-b")); err != nil { - t.Fatalf("CreateAtespace(team-b) failed: %v", err) - } - // Actor lives ONLY in team-b. - if _, err := s.CreateActor(ctx, &ateapipb.Actor{Metadata: &ateapipb.ResourceMetadata{Name: "id1", Atespace: "team-b"}, Status: ateapipb.Actor_STATUS_SUSPENDED}); err != nil { - t.Fatalf("CreateActor failed: %v", err) - } - - // team-a is empty → delete must succeed. - if _, err := s.DeleteAtespace(ctx, "team-a"); err != nil { - t.Errorf("DeleteAtespace(team-a, empty) = %v, want nil (must not be blocked by team-b's actor)", err) - } - if _, err := s.GetAtespace(ctx, "team-a"); !errors.Is(err, store.ErrNotFound) { - t.Errorf("after delete, GetAtespace(team-a) = %v, want ErrNotFound", err) - } - // team-b is still non-empty → still rejected. - if _, err := s.DeleteAtespace(ctx, "team-b"); !errors.Is(err, store.ErrFailedPrecondition) { - t.Errorf("DeleteAtespace(team-b, non-empty) = %v, want ErrFailedPrecondition", err) - } -} - -// concurrentMasterClient fakes a cluster with several masters. Like the real -// ClusterClient.ForEachMaster, it invokes the callback concurrently, one -// goroutine per master. -type concurrentMasterClient struct { - redisClient - masters []*redis.Client -} - -func (c *concurrentMasterClient) ForEachMaster(ctx context.Context, fn func(ctx context.Context, client *redis.Client) error) error { - var wg sync.WaitGroup - errCh := make(chan error, 1) - for _, master := range c.masters { - wg.Add(1) - go func(master *redis.Client) { - defer wg.Done() - if err := fn(ctx, master); err != nil { - select { - case errCh <- err: - default: - } - } - }(master) - } - wg.Wait() - select { - case err := <-errCh: - return err - default: - return nil - } -} - -// TestGetSortedMasters_ConcurrentCallbacks guards against dropping a shard -// when ForEachMaster's concurrent callbacks append to the shared slice: a -// dropped master makes ListActors silently skip every actor on that shard. -// Run with -race; the pre-fix unsynchronized append fails here. -func TestGetSortedMasters_ConcurrentCallbacks(t *testing.T) { - const numMasters = 8 - fake := &concurrentMasterClient{} - want := make([]string, 0, numMasters) - for i := range numMasters { - addr := fmt.Sprintf("shard-%d:6379", i) - // Never connected to: getSortedMasters only reads Options().Addr. - fake.masters = append(fake.masters, redis.NewClient(&redis.Options{Addr: addr})) - want = append(want, addr) - } - sort.Strings(want) - s := &Persistence{rdb: fake} - - for range 100 { - masters, err := s.getSortedMasters(context.Background()) - if err != nil { - t.Fatalf("getSortedMasters failed: %v", err) - } - got := make([]string, 0, len(masters)) - for _, m := range masters { - got = append(got, m.Options().Addr) - } - if diff := cmp.Diff(want, got); diff != "" { - t.Fatalf("getSortedMasters returned wrong masters (-want +got):\n%s", diff) - } - } -} - -// TestListActors_MultiMaster_Pagination verifies that pagination across multiple -// Redis master shards collects items from every shard without skipping or duplicating -// shards when page boundaries align with shard boundaries. -func TestListActors_MultiMaster_Pagination(t *testing.T) { - ctx := context.Background() - const numShards = 3 - - type shard struct { - client *redis.Client - clusterClient *redis.ClusterClient - } - var shards []shard - for i := 0; i < numShards; i++ { - mr, err := miniredis.Run() - if err != nil { - t.Fatalf("failed to start miniredis %d: %v", i, err) - } - - client := redis.NewClient(&redis.Options{Addr: mr.Addr()}) - clusterClient := redis.NewClusterClient(&redis.ClusterOptions{Addrs: []string{mr.Addr()}}) - defer client.Close() - defer clusterClient.Close() - - shards = append(shards, shard{ - client: client, - clusterClient: clusterClient, - }) - } - - sort.Slice(shards, func(i, j int) bool { - return shards[i].client.Options().Addr < shards[j].client.Options().Addr - }) - - var clients []*redis.Client - for shardIdx, sh := range shards { - clients = append(clients, sh.client) - tempS := &Persistence{rdb: sh.clusterClient} - for itemIdx := 0; itemIdx < 3; itemIdx++ { - actor := &ateapipb.Actor{ - Metadata: &ateapipb.ResourceMetadata{ - Name: fmt.Sprintf("actor-shard%d-item%d", shardIdx, itemIdx), - Atespace: testAtespace, - }, - ActorTemplateNamespace: "default", - ActorTemplateName: "test-template", - Status: ateapipb.Actor_STATUS_SUSPENDED, - } - if _, err := tempS.CreateActor(ctx, actor); err != nil { - t.Fatalf("failed to seed actor: %v", err) - } - } - } - - fake := &concurrentMasterClient{ - redisClient: shards[0].clusterClient, - masters: clients, - } - s := &Persistence{rdb: fake} - - var allActors []*ateapipb.Actor - pageToken := "" - for { - page, err := s.ListActors(ctx, testAtespace, store.ListOptions{PageSize: 2, PageToken: pageToken}) - if err != nil { - t.Fatalf("ListActors failed: %v", err) - } - allActors = append(allActors, page.Items...) - if page.NextPageToken == "" { - break - } - pageToken = page.NextPageToken - } - - if len(allActors) != 9 { - t.Fatalf("expected 9 total actors across %d shards, got %d", numShards, len(allActors)) - } - - seen := make(map[string]bool) - for _, a := range allActors { - if seen[a.GetMetadata().GetName()] { - t.Errorf("duplicate actor found in paginated results: %s", a.GetMetadata().GetName()) - } - seen[a.GetMetadata().GetName()] = true - } -} - -// newMultiMasterStore returns a Persistence whose master iteration spans -// numShards independent miniredis instances, plus a per-shard Persistence for -// seeding data onto a specific shard. -func newMultiMasterStore(t *testing.T, numShards int) (*Persistence, []*Persistence) { - t.Helper() - type shard struct { - client *redis.Client - clusterClient *redis.ClusterClient - } - var shards []shard - for i := 0; i < numShards; i++ { - mr, err := miniredis.Run() - if err != nil { - t.Fatalf("failed to start miniredis %d: %v", i, err) - } - t.Cleanup(mr.Close) - client := redis.NewClient(&redis.Options{Addr: mr.Addr()}) - clusterClient := redis.NewClusterClient(&redis.ClusterOptions{Addrs: []string{mr.Addr()}}) - t.Cleanup(func() { client.Close() }) - t.Cleanup(func() { clusterClient.Close() }) - shards = append(shards, shard{client: client, clusterClient: clusterClient}) - } - sort.Slice(shards, func(i, j int) bool { - return shards[i].client.Options().Addr < shards[j].client.Options().Addr - }) - var clients []*redis.Client - var perShard []*Persistence - for _, sh := range shards { - clients = append(clients, sh.client) - perShard = append(perShard, &Persistence{rdb: sh.clusterClient}) - } - fake := &concurrentMasterClient{redisClient: shards[0].clusterClient, masters: clients} - return &Persistence{rdb: fake}, perShard -} - -// TestListWorkers_MultiMaster_Pagination mirrors -// TestListActors_MultiMaster_Pagination for ListWorkers, sweeping page sizes -// so page boundaries both do and do not align with shard boundaries (the -// aligned case is the #425 shard-skip regression). -func TestListWorkers_MultiMaster_Pagination(t *testing.T) { - ctx := context.Background() - const numShards = 3 - for _, pageSize := range []int32{1, 2, 3, 4} { - t.Run(fmt.Sprintf("pageSize=%d", pageSize), func(t *testing.T) { - s, perShard := newMultiMasterStore(t, numShards) - for shardIdx, ps := range perShard { - for itemIdx := 0; itemIdx < 3; itemIdx++ { - worker := &ateapipb.Worker{ - WorkerNamespace: "ns", - WorkerPool: "pool", - WorkerPod: fmt.Sprintf("pod-shard%d-item%d", shardIdx, itemIdx), - } - if err := ps.CreateWorker(ctx, worker); err != nil { - t.Fatalf("failed to seed worker: %v", err) - } - } - } - - seen := make(map[string]bool) - pageToken := "" - for { - page, err := s.ListWorkers(ctx, store.ListOptions{PageSize: pageSize, PageToken: pageToken}) - if err != nil { - t.Fatalf("ListWorkers: %v", err) - } - for _, w := range page.Items { - if seen[w.GetWorkerPod()] { - t.Errorf("duplicate worker in paginated results: %s", w.GetWorkerPod()) - } - seen[w.GetWorkerPod()] = true - } - if page.NextPageToken == "" { - break - } - pageToken = page.NextPageToken - } - if len(seen) != numShards*3 { - t.Fatalf("expected %d workers across %d shards, got %d", numShards*3, numShards, len(seen)) - } - }) - } -} - -// TestListAtespaces_MultiMaster_Pagination mirrors -// TestListWorkers_MultiMaster_Pagination for ListAtespaces. -func TestListAtespaces_MultiMaster_Pagination(t *testing.T) { - ctx := context.Background() - const numShards = 3 - for _, pageSize := range []int32{1, 2, 3, 4} { - t.Run(fmt.Sprintf("pageSize=%d", pageSize), func(t *testing.T) { - s, perShard := newMultiMasterStore(t, numShards) - for shardIdx, ps := range perShard { - for itemIdx := 0; itemIdx < 3; itemIdx++ { - atespace := &ateapipb.Atespace{ - Metadata: &ateapipb.ResourceMetadata{ - Name: fmt.Sprintf("space-shard%d-item%d", shardIdx, itemIdx), - }, - } - if _, err := ps.CreateAtespace(ctx, atespace); err != nil { - t.Fatalf("failed to seed atespace: %v", err) - } - } - } - - seen := make(map[string]bool) - pageToken := "" - for { - page, err := s.ListAtespaces(ctx, store.ListOptions{PageSize: pageSize, PageToken: pageToken}) - if err != nil { - t.Fatalf("ListAtespaces: %v", err) - } - for _, a := range page.Items { - if seen[a.GetMetadata().GetName()] { - t.Errorf("duplicate atespace in paginated results: %s", a.GetMetadata().GetName()) - } - seen[a.GetMetadata().GetName()] = true - } - if page.NextPageToken == "" { - break - } - pageToken = page.NextPageToken - } - if len(seen) != numShards*3 { - t.Fatalf("expected %d atespaces across %d shards, got %d", numShards*3, numShards, len(seen)) - } - }) - } -} - -type setNXFunc func(ctx context.Context, key string, value interface{}, ttl time.Duration) *redis.BoolCmd - -type evalFunc func(ctx context.Context, sha1 string, keys []string, args ...interface{}) *redis.Cmd - -type mockRedisClient struct { - redisClient - - SetNXFunc setNXFunc - EvalShaFunc evalFunc -} - -func (m *mockRedisClient) SetNX(ctx context.Context, key string, value interface{}, ttl time.Duration) *redis.BoolCmd { - return m.SetNXFunc(ctx, key, value, ttl) -} - -func (m *mockRedisClient) EvalSha(ctx context.Context, sha1 string, keys []string, args ...interface{}) *redis.Cmd { - return m.EvalShaFunc(ctx, sha1, keys, args...) -} - -// intCmd and errCmd build the two possible shapes of a script-eval result: -// intCmd for the CAS script's 1 (applied) / 0 (not owned) return value, and -// errCmd for a failed call. -func intCmd(ctx context.Context, v int64) *redis.Cmd { - cmd := redis.NewCmd(ctx) - cmd.SetVal(v) - return cmd -} - -func errCmd(ctx context.Context, err error) *redis.Cmd { - cmd := redis.NewCmd(ctx) - cmd.SetErr(err) - return cmd -} - -// acquires is a setNXFunc reporting the lock was acquired. -func acquires(ctx context.Context, key string, value interface{}, ttl time.Duration) *redis.BoolCmd { - cmd := redis.NewBoolCmd(ctx) - cmd.SetVal(true) - return cmd -} - -// renews is an evalFunc reporting a successful renewal. -func renews(ctx context.Context, sha1 string, keys []string, args ...interface{}) *redis.Cmd { - return intCmd(ctx, 1) -} - -// leaseLost is an evalFunc reporting that the CAS check found we no longer -// own the key (someone else took over, or it was deleted) -- Mode 6: an -// authoritative "you don't hold this anymore," not a retryable failure. -func leaseLost(ctx context.Context, sha1 string, keys []string, args ...interface{}) *redis.Cmd { - return intCmd(ctx, 0) -} - -// failsWith returns an evalFunc that always fails fast with err. -func failsWith(err error) evalFunc { - return func(ctx context.Context, sha1 string, keys []string, args ...interface{}) *redis.Cmd { - return errCmd(ctx, err) - } -} - -// hangs is an evalFunc that blocks until ctx is done, simulating an -// unresponsive Redis. -func hangs(ctx context.Context, sha1 string, keys []string, args ...interface{}) *redis.Cmd { - <-ctx.Done() - return errCmd(ctx, ctx.Err()) -} - -// failsUntil returns an evalFunc that fails fast with err until t, then -// reports a successful renewal. -func failsUntil(t time.Time, err error) evalFunc { - return func(ctx context.Context, sha1 string, keys []string, args ...interface{}) *redis.Cmd { - if time.Now().Before(t) { - return errCmd(ctx, err) - } - return intCmd(ctx, 1) - } -} - -// failsNTimesThenHangs returns an evalFunc that fails fast with err for its -// first n calls, then hangs (as hangs does) on every call after that. -func failsNTimesThenHangs(n int, err error) evalFunc { - var mu sync.Mutex - left := n - return func(ctx context.Context, sha1 string, keys []string, args ...interface{}) *redis.Cmd { - mu.Lock() - fail := left > 0 - if fail { - left-- - } - mu.Unlock() - - if fail { - return errCmd(ctx, err) - } - return hangs(ctx, sha1, keys, args...) - } -} - -func newTestActorTemplate(atespace, name string) *ateapipb.ActorTemplate { - return &ateapipb.ActorTemplate{Metadata: &ateapipb.ResourceMetadata{Atespace: atespace, Name: name}} -} - -func newTestActorTemplateVersion(atespace, name, template string) *ateapipb.ActorTemplateVersion { - return &ateapipb.ActorTemplateVersion{ - Metadata: &ateapipb.ResourceMetadata{Atespace: atespace, Name: name}, - ActorTemplate: &ateapipb.ObjectRef{Atespace: atespace, Name: template}, - SandboxConfig: &ateapipb.SandboxConfig{PauseImage: "pause@sha256:abc"}, - Phase: &ateapipb.ActorTemplateVersionPhase{Phase: ateapipb.ActorTemplateVersionPhase_PHASE_INITIAL}, - } -} - -func TestActorTemplateLifecycle(t *testing.T) { - _, s, ctx := setupTest(t) - - want := newTestActorTemplate("team-a", "tmpl-a") - created, err := s.CreateActorTemplate(ctx, want) - if err != nil { - t.Fatalf("CreateActorTemplate failed: %v", err) - } - if created.GetMetadata().GetUid() == "" { - t.Errorf("CreateActorTemplate returned empty uid; want server-assigned uid") - } - if created.GetMetadata().GetVersion() != 1 { - t.Errorf("CreateActorTemplate returned version %d, want 1", created.GetMetadata().GetVersion()) - } - if created.GetMetadata().GetCreateTime() == nil || created.GetMetadata().GetUpdateTime() == nil { - t.Errorf("CreateActorTemplate returned unset create/update time") - } - // The input must not be mutated. - if want.GetMetadata().GetUid() != "" || want.GetMetadata().GetVersion() != 0 { - t.Errorf("CreateActorTemplate must not mutate its input, got metadata %v", want.GetMetadata()) - } - - got, err := s.GetActorTemplate(ctx, resources.ActorTemplateRef{Atespace: "team-a", Name: "tmpl-a"}) - if err != nil { - t.Fatalf("GetActorTemplate failed: %v", err) - } - if diff := cmp.Diff(created, got, protocmp.Transform()); diff != "" { - t.Errorf("CreateActorTemplate return does not match stored state (-created +got):\n%s", diff) - } - - listResp, err := s.ListActorTemplates(ctx, "team-a", store.ListOptions{PageSize: 1000}) - if err != nil { - t.Fatalf("ListActorTemplates failed: %v", err) - } - list := listResp.Items - if len(list) != 1 || list[0].GetMetadata().GetName() != "tmpl-a" { - t.Errorf("ListActorTemplates = %v, want [tmpl-a]", list) - } - - deleted, err := s.DeleteActorTemplate(ctx, resources.ActorTemplateRef{Atespace: "team-a", Name: "tmpl-a"}) - if err != nil { - t.Fatalf("DeleteActorTemplate failed: %v", err) - } - if diff := cmp.Diff(created, deleted, protocmp.Transform()); diff != "" { - t.Errorf("DeleteActorTemplate returned unexpected resource (-created +deleted):\n%s", diff) - } - if _, err := s.GetActorTemplate(ctx, resources.ActorTemplateRef{Atespace: "team-a", Name: "tmpl-a"}); !errors.Is(err, store.ErrNotFound) { - t.Errorf("after delete, GetActorTemplate = %v, want ErrNotFound", err) - } -} - -func TestCreateActorTemplate_AlreadyExists(t *testing.T) { - _, s, ctx := setupTest(t) - - if _, err := s.CreateActorTemplate(ctx, newTestActorTemplate("team-a", "tmpl-a")); err != nil { - t.Fatalf("first CreateActorTemplate failed: %v", err) - } - if _, err := s.CreateActorTemplate(ctx, newTestActorTemplate("team-a", "tmpl-a")); !errors.Is(err, store.ErrAlreadyExists) { - t.Errorf("expected ErrAlreadyExists, got %v", err) - } -} - -func TestGetActorTemplate_NotFound(t *testing.T) { - _, s, ctx := setupTest(t) - - if _, err := s.GetActorTemplate(ctx, resources.ActorTemplateRef{Atespace: "team-a", Name: "nope"}); !errors.Is(err, store.ErrNotFound) { - t.Errorf("expected ErrNotFound, got %v", err) - } -} - -func TestActorTemplateExists(t *testing.T) { - _, s, ctx := setupTest(t) - - if ok, err := s.ActorTemplateExists(ctx, resources.ActorTemplateRef{Atespace: "team-a", Name: "tmpl-a"}); err != nil || ok { - t.Fatalf("ActorTemplateExists before create = (%v, %v), want (false, nil)", ok, err) - } - if _, err := s.CreateActorTemplate(ctx, newTestActorTemplate("team-a", "tmpl-a")); err != nil { - t.Fatalf("CreateActorTemplate failed: %v", err) - } - if ok, err := s.ActorTemplateExists(ctx, resources.ActorTemplateRef{Atespace: "team-a", Name: "tmpl-a"}); err != nil || !ok { - t.Fatalf("ActorTemplateExists after create = (%v, %v), want (true, nil)", ok, err) - } -} - -func TestUpdateActorTemplate_Success(t *testing.T) { - _, s, ctx := setupTest(t) - - created, err := s.CreateActorTemplate(ctx, newTestActorTemplate("team-a", "tmpl-a")) - if err != nil { - t.Fatalf("CreateActorTemplate failed: %v", err) - } - - updated, err := s.UpdateActorTemplate(ctx, resources.ActorTemplateRef{Atespace: "team-a", Name: "tmpl-a"}, func(dbTemplate *ateapipb.ActorTemplate) error { - dbTemplate.DefaultVersionOnCreate = &ateapipb.ObjectRef{Atespace: "team-a", Name: "tmpl-a-v1"} - return nil - }) - if err != nil { - t.Fatalf("UpdateActorTemplate failed: %v", err) - } - - // UpdateActorTemplate returns the stored resource: the mutation applied and - // version advanced, with uid and create_time preserved from creation. - if got := updated.GetDefaultVersionOnCreate().GetName(); got != "tmpl-a-v1" { - t.Errorf("default_version_on_create = %q, want %q", got, "tmpl-a-v1") - } - if updated.GetMetadata().GetVersion() != 2 { - t.Errorf("UpdateActorTemplate returned version %d, want 2", updated.GetMetadata().GetVersion()) - } - if updated.GetMetadata().GetUid() != created.GetMetadata().GetUid() { - t.Errorf("uid changed on update: got %q, want %q", updated.GetMetadata().GetUid(), created.GetMetadata().GetUid()) - } - if !updated.GetMetadata().GetCreateTime().AsTime().Equal(created.GetMetadata().GetCreateTime().AsTime()) { - t.Errorf("create_time changed on update: got %v, want %v", updated.GetMetadata().GetCreateTime().AsTime(), created.GetMetadata().GetCreateTime().AsTime()) - } - - // The returned resource is exactly what GetActorTemplate reads back. - got, err := s.GetActorTemplate(ctx, resources.ActorTemplateRef{Atespace: "team-a", Name: "tmpl-a"}) - if err != nil { - t.Fatalf("GetActorTemplate failed: %v", err) - } - if diff := cmp.Diff(updated, got, protocmp.Transform()); diff != "" { - t.Errorf("UpdateActorTemplate return does not match stored state (-updated +got):\n%s", diff) - } -} - -func TestUpdateActorTemplate_MutateErrorsAreNotRetried(t *testing.T) { - _, s, ctx := setupTest(t) - - created, err := s.CreateActorTemplate(ctx, newTestActorTemplate("team-a", "tmpl-a")) - if err != nil { - t.Fatalf("CreateActorTemplate failed: %v", err) - } - - var mutationError = errors.New("mutation error") - - callsToMutateFn := 0 - _, err = s.UpdateActorTemplate(ctx, resources.ActorTemplateRef{Atespace: "team-a", Name: "tmpl-a"}, func(dbTemplate *ateapipb.ActorTemplate) error { - callsToMutateFn++ - dbTemplate.DefaultVersionOnCreate = &ateapipb.ObjectRef{Atespace: "team-a", Name: "tmpl-a-v1"} - return fmt.Errorf("template tmpl-a: %w", mutationError) - }) - // The error must arrive intact - if !errors.Is(err, mutationError) { - t.Errorf("UpdateActorTemplate error = %v, want one wrapping mutationError", err) - } - // Mutation errors are non-retriable - if callsToMutateFn != 1 { - t.Errorf("mutate ran %d times, want exactly 1 (a rejected precondition must not be retried)", callsToMutateFn) - } - - got, err := s.GetActorTemplate(ctx, resources.ActorTemplateRef{Atespace: "team-a", Name: "tmpl-a"}) - if err != nil { - t.Fatalf("GetActorTemplate failed: %v", err) - } - if diff := cmp.Diff(created, got, protocmp.Transform()); diff != "" { - t.Errorf("aborted mutation was persisted (-created +got):\n%s", diff) - } -} - -func TestUpdateActorTemplate_DiscardsServerOwnedFieldsEdits(t *testing.T) { - _, s, ctx := setupTest(t) - - created, err := s.CreateActorTemplate(ctx, newTestActorTemplate("team-a", "tmpl-a")) - if err != nil { - t.Fatalf("CreateActorTemplate failed: %v", err) - } - - updated, err := s.UpdateActorTemplate(ctx, resources.ActorTemplateRef{Atespace: "team-a", Name: "tmpl-a"}, func(dbTemplate *ateapipb.ActorTemplate) error { - // Metadata is server-owned: a closure must not be able to change it. - dbTemplate.Metadata.Uid = "forged-uid" - dbTemplate.Metadata.Version = 99 - dbTemplate.Metadata.CreateTime = nil - dbTemplate.Metadata.UpdateTime = nil - dbTemplate.DefaultVersionOnCreate = &ateapipb.ObjectRef{Atespace: "team-a", Name: "tmpl-a-v1"} - return nil - }) - if err != nil { - t.Fatalf("UpdateActorTemplate failed: %v", err) - } - - if got := updated.GetMetadata().GetUid(); got != created.GetMetadata().GetUid() { - t.Errorf("uid = %q, want the server-assigned %q", got, created.GetMetadata().GetUid()) - } - if got := updated.GetMetadata().GetVersion(); got != created.GetMetadata().GetVersion()+1 { - t.Errorf("version = %d, want %d (one past the stored version, not the forged value)", got, created.GetMetadata().GetVersion()+1) - } - if got := updated.GetMetadata().GetCreateTime(); got == nil || !got.AsTime().Equal(created.GetMetadata().GetCreateTime().AsTime()) { - t.Errorf("create_time = %v, want the creation value %v", got, created.GetMetadata().GetCreateTime()) - } - if got := updated.GetDefaultVersionOnCreate().GetName(); got != "tmpl-a-v1" { - t.Errorf("default_version_on_create = %q, want %q: discarding metadata edits must not discard the mutation", got, "tmpl-a-v1") - } -} - -// TestUpdateActorTemplate_RejectsImmutableFieldChange covers the fields a -// mutation may not touch. Unlike the server-owned metadata, which is silently -// restored, these fail the call: a caller that renamed a template asked for -// something the store cannot do, and must hear about it. -func TestUpdateActorTemplate_RejectsImmutableFieldChange(t *testing.T) { - tests := []struct { - name string - mutate func(dbTemplate *ateapipb.ActorTemplate) - wantField string - }{ - { - name: "atespace", - mutate: func(dbTemplate *ateapipb.ActorTemplate) { dbTemplate.Metadata.Atespace = "other-atespace" }, - wantField: "metadata.atespace", - }, - { - name: "name", - mutate: func(dbTemplate *ateapipb.ActorTemplate) { dbTemplate.Metadata.Name = "other-name" }, - wantField: "metadata.name", - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - _, s, ctx := setupTest(t) - created, err := s.CreateActorTemplate(ctx, newTestActorTemplate("team-a", "tmpl-a")) - if err != nil { - t.Fatalf("CreateActorTemplate failed: %v", err) - } - - _, err = s.UpdateActorTemplate(ctx, resources.ActorTemplateRef{Atespace: "team-a", Name: "tmpl-a"}, func(dbTemplate *ateapipb.ActorTemplate) error { - // Paired with a legitimate edit, so the rejection cannot be - // mistaken for a no-op mutation. - dbTemplate.DefaultVersionOnCreate = &ateapipb.ObjectRef{Atespace: "team-a", Name: "tmpl-a-v1"} - tt.mutate(dbTemplate) - return nil - }) - // The message must name the offending field: the closure is buggy, - // and whoever has to fix it only has this error to go on. - if want := tt.wantField + " is immutable"; err == nil || !strings.Contains(err.Error(), want) { - t.Errorf("UpdateActorTemplate changing %s = %v, want an error containing %q", tt.name, err, want) - } - - got, err := s.GetActorTemplate(ctx, resources.ActorTemplateRef{Atespace: "team-a", Name: "tmpl-a"}) - if err != nil { - t.Fatalf("GetActorTemplate failed: %v", err) - } - if diff := cmp.Diff(created, got, protocmp.Transform()); diff != "" { - t.Errorf("rejected mutation was persisted anyway (-created +got):\n%s", diff) - } - }) - } -} - -func TestUpdateActorTemplate_RetriesOnConcurrentWrite(t *testing.T) { - mr, s, ctx := setupTest(t) - if _, err := s.CreateActorTemplate(ctx, newTestActorTemplate("team-a", "tmpl-a")); err != nil { - t.Fatalf("CreateActorTemplate failed: %v", err) - } - - // A separate client, so its write lands outside the transaction's connection. - otherClient := redis.NewClusterClient(&redis.ClusterOptions{Addrs: []string{mr.Addr()}}) - t.Cleanup(func() { otherClient.Close() }) - - attempts := 0 - interceptor := &watchInterceptor{redisClient: s.rdb, before: func() { - // Only the first attempt races. We do this to make sure the second retry - // will succeed. - if attempts > 0 { - return - } - concurrent, err := s.GetActorTemplate(ctx, resources.ActorTemplateRef{Atespace: "team-a", Name: "tmpl-a"}) - if err != nil { - t.Errorf("GetActorTemplate for concurrent write failed: %v", err) - return - } - concurrent.DefaultVersionOnCreate = &ateapipb.ObjectRef{Atespace: "team-a", Name: "tmpl-a-v1"} - val, err := protojson.Marshal(concurrent) - if err != nil { - t.Errorf("protojson.Marshal failed: %v", err) - return - } - if err := otherClient.Set(ctx, actorTemplateDBKey(resources.ActorTemplateRef{Atespace: "team-a", Name: "tmpl-a"}), val, 0).Err(); err != nil { - t.Errorf("concurrent Set failed: %v", err) - } - }} - racing := &Persistence{rdb: interceptor, lockTTL: defaultLockTTL} - - updated, err := racing.UpdateActorTemplate(ctx, resources.ActorTemplateRef{Atespace: "team-a", Name: "tmpl-a"}, func(dbTemplate *ateapipb.ActorTemplate) error { - attempts++ - // The template's only mutable field belongs to the concurrent writer - // in this test; an empty mutation still exercises the retry path. - return nil - }) - if err != nil { - t.Fatalf("UpdateActorTemplate failed: %v", err) - } - if attempts < 2 { - t.Errorf("mutate ran %d times, want at least 2: the first write is racey and must be rejected", attempts) - } - // The concurrent tx wrote default_version_on_create. This change should - // survive instead of being reverted by a mutation computed against the - // older state. - if got := updated.GetDefaultVersionOnCreate().GetName(); got != "tmpl-a-v1" { - t.Errorf("default_version_on_create = %q, want %q: the retry clobbered the concurrent write", got, "tmpl-a-v1") - } -} - -func TestUpdateActorTemplate_NotFound(t *testing.T) { - _, s, ctx := setupTest(t) - _, err := s.UpdateActorTemplate(ctx, resources.ActorTemplateRef{Atespace: "team-a", Name: "non-existent"}, func(dbTemplate *ateapipb.ActorTemplate) error { - t.Error("mutate must not run for a missing template") - return nil - }) - if !errors.Is(err, store.ErrNotFound) { - t.Errorf("expected store.ErrNotFound, got %v", err) - } -} - -func TestUpdateActorTemplate_RejectsStaleVersion(t *testing.T) { - _, s, ctx := setupTest(t) - - created, err := s.CreateActorTemplate(ctx, newTestActorTemplate("team-a", "tmpl-a")) - if err != nil { - t.Fatalf("CreateActorTemplate failed: %v", err) - } - if _, err := s.UpdateActorTemplate(ctx, resources.ActorTemplateRef{Atespace: "team-a", Name: "tmpl-a"}, func(dbTemplate *ateapipb.ActorTemplate) error { - dbTemplate.DefaultVersionOnCreate = &ateapipb.ObjectRef{Atespace: "team-a", Name: "tmpl-a-v1"} - return nil - }); err != nil { - t.Fatalf("UpdateActorTemplate failed: %v", err) - } - - // created still carries the pre-update version, so the pin is stale. - _, err = s.UpdateActorTemplate(ctx, resources.ActorTemplateRef{Atespace: "team-a", Name: "tmpl-a"}, store.WithPrecondition(created, func(dbTemplate *ateapipb.ActorTemplate) error { - t.Error("mutate ran past its precondition once the pinned version had moved") - dbTemplate.DefaultVersionOnCreate = nil - return nil - })) - if !errors.Is(err, store.ErrVersionConflict) { - t.Errorf("UpdateActorTemplate error = %v, want one matching store.ErrVersionConflict", err) - } - // The uid still matches, so this is not the incarnation failure: callers key - // their retry decision off the difference. - if errors.Is(err, store.ErrUIDConflict) { - t.Errorf("UpdateActorTemplate error = %v, want no store.ErrUIDConflict match: the incarnation is unchanged", err) - } -} - -func TestDeleteActorTemplate_NotFound(t *testing.T) { - _, s, ctx := setupTest(t) - - if _, err := s.DeleteActorTemplate(ctx, resources.ActorTemplateRef{Atespace: "team-a", Name: "nope"}); !errors.Is(err, store.ErrNotFound) { - t.Errorf("expected ErrNotFound, got %v", err) - } -} - -func TestDeleteActorTemplate_HasVersions_Rejected(t *testing.T) { - _, s, ctx := setupTest(t) - - if _, err := s.CreateActorTemplate(ctx, newTestActorTemplate("team-a", "tmpl-a")); err != nil { - t.Fatalf("CreateActorTemplate failed: %v", err) - } - // A version parented to a DIFFERENT template must not block the delete. - if _, err := s.CreateActorTemplateVersion(ctx, newTestActorTemplateVersion("team-a", "tmpl-b-v1", "tmpl-b")); err != nil { - t.Fatalf("CreateActorTemplateVersion failed: %v", err) - } - if _, err := s.CreateActorTemplateVersion(ctx, newTestActorTemplateVersion("team-a", "tmpl-a-v1", "tmpl-a")); err != nil { - t.Fatalf("CreateActorTemplateVersion failed: %v", err) - } - - if _, err := s.DeleteActorTemplate(ctx, resources.ActorTemplateRef{Atespace: "team-a", Name: "tmpl-a"}); !errors.Is(err, store.ErrFailedPrecondition) { - t.Fatalf("DeleteActorTemplate with versions = %v, want ErrFailedPrecondition", err) - } - // The template must survive a rejected delete. - if _, err := s.GetActorTemplate(ctx, resources.ActorTemplateRef{Atespace: "team-a", Name: "tmpl-a"}); err != nil { - t.Fatalf("template should still exist after rejected delete, got %v", err) - } - - if _, err := s.DeleteActorTemplateVersion(ctx, resources.ActorTemplateVersionRef{Atespace: "team-a", Name: "tmpl-a-v1"}); err != nil { - t.Fatalf("DeleteActorTemplateVersion failed: %v", err) - } - if _, err := s.DeleteActorTemplate(ctx, resources.ActorTemplateRef{Atespace: "team-a", Name: "tmpl-a"}); err != nil { - t.Errorf("DeleteActorTemplate after versions removed = %v, want nil", err) - } -} - -func TestActorTemplateVersionLifecycle(t *testing.T) { - _, s, ctx := setupTest(t) - - want := newTestActorTemplateVersion("team-a", "tmpl-a-v1", "tmpl-a") - created, err := s.CreateActorTemplateVersion(ctx, want) - if err != nil { - t.Fatalf("CreateActorTemplateVersion failed: %v", err) - } - if created.GetMetadata().GetUid() == "" { - t.Errorf("CreateActorTemplateVersion returned empty uid; want server-assigned uid") - } - if created.GetMetadata().GetVersion() != 1 { - t.Errorf("CreateActorTemplateVersion returned version %d, want 1", created.GetMetadata().GetVersion()) - } - // The caller-built spec and status are persisted verbatim. - if diff := cmp.Diff(want, created, protocmp.Transform(), ignoreUID, ignoreVersion, ignoreTimestamps); diff != "" { - t.Errorf("CreateActorTemplateVersion returned unexpected resource (-want +got):\n%s", diff) - } - - got, err := s.GetActorTemplateVersion(ctx, resources.ActorTemplateVersionRef{Atespace: "team-a", Name: "tmpl-a-v1"}) - if err != nil { - t.Fatalf("GetActorTemplateVersion failed: %v", err) - } - if diff := cmp.Diff(created, got, protocmp.Transform()); diff != "" { - t.Errorf("CreateActorTemplateVersion return does not match stored state (-created +got):\n%s", diff) - } - - deleted, err := s.DeleteActorTemplateVersion(ctx, resources.ActorTemplateVersionRef{Atespace: "team-a", Name: "tmpl-a-v1"}) - if err != nil { - t.Fatalf("DeleteActorTemplateVersion failed: %v", err) - } - if diff := cmp.Diff(created, deleted, protocmp.Transform()); diff != "" { - t.Errorf("DeleteActorTemplateVersion returned unexpected resource (-created +deleted):\n%s", diff) - } - if _, err := s.GetActorTemplateVersion(ctx, resources.ActorTemplateVersionRef{Atespace: "team-a", Name: "tmpl-a-v1"}); !errors.Is(err, store.ErrNotFound) { - t.Errorf("after delete, GetActorTemplateVersion = %v, want ErrNotFound", err) - } -} - -func TestCreateActorTemplateVersion_AlreadyExists(t *testing.T) { - _, s, ctx := setupTest(t) - - if _, err := s.CreateActorTemplateVersion(ctx, newTestActorTemplateVersion("team-a", "v1", "tmpl-a")); err != nil { - t.Fatalf("first CreateActorTemplateVersion failed: %v", err) - } - if _, err := s.CreateActorTemplateVersion(ctx, newTestActorTemplateVersion("team-a", "v1", "tmpl-a")); !errors.Is(err, store.ErrAlreadyExists) { - t.Errorf("expected ErrAlreadyExists, got %v", err) - } -} - -func TestDeleteActorTemplateVersion_IsParentDefault_Rejected(t *testing.T) { - _, s, ctx := setupTest(t) - - if _, err := s.CreateActorTemplate(ctx, newTestActorTemplate("team-a", "tmpl-a")); err != nil { - t.Fatalf("CreateActorTemplate failed: %v", err) - } - if _, err := s.CreateActorTemplateVersion(ctx, newTestActorTemplateVersion("team-a", "tmpl-a-v1", "tmpl-a")); err != nil { - t.Fatalf("CreateActorTemplateVersion failed: %v", err) - } - if _, err := s.UpdateActorTemplate(ctx, resources.ActorTemplateRef{Atespace: "team-a", Name: "tmpl-a"}, func(dbTemplate *ateapipb.ActorTemplate) error { - dbTemplate.DefaultVersionOnCreate = &ateapipb.ObjectRef{Atespace: "team-a", Name: "tmpl-a-v1"} - return nil - }); err != nil { - t.Fatalf("UpdateActorTemplate failed: %v", err) - } - - if _, err := s.DeleteActorTemplateVersion(ctx, resources.ActorTemplateVersionRef{Atespace: "team-a", Name: "tmpl-a-v1"}); !errors.Is(err, store.ErrFailedPrecondition) { - t.Fatalf("DeleteActorTemplateVersion while default = %v, want ErrFailedPrecondition", err) - } - // The version must survive a rejected delete. - if _, err := s.GetActorTemplateVersion(ctx, resources.ActorTemplateVersionRef{Atespace: "team-a", Name: "tmpl-a-v1"}); err != nil { - t.Fatalf("version should still exist after rejected delete, got %v", err) - } - - // Clearing the default unblocks the delete. - if _, err := s.UpdateActorTemplate(ctx, resources.ActorTemplateRef{Atespace: "team-a", Name: "tmpl-a"}, func(dbTemplate *ateapipb.ActorTemplate) error { - dbTemplate.DefaultVersionOnCreate = nil - return nil - }); err != nil { - t.Fatalf("UpdateActorTemplate (clear default) failed: %v", err) - } - if _, err := s.DeleteActorTemplateVersion(ctx, resources.ActorTemplateVersionRef{Atespace: "team-a", Name: "tmpl-a-v1"}); err != nil { - t.Errorf("DeleteActorTemplateVersion after clearing default = %v, want nil", err) - } -} - -func TestDeleteActorTemplateVersion_MissingParent_Allowed(t *testing.T) { - _, s, ctx := setupTest(t) - - if _, err := s.CreateActorTemplateVersion(ctx, newTestActorTemplateVersion("team-a", "orphan-v1", "gone")); err != nil { - t.Fatalf("CreateActorTemplateVersion failed: %v", err) - } - if _, err := s.DeleteActorTemplateVersion(ctx, resources.ActorTemplateVersionRef{Atespace: "team-a", Name: "orphan-v1"}); err != nil { - t.Errorf("DeleteActorTemplateVersion with missing parent = %v, want nil", err) - } -} - -func TestDeleteActorTemplateVersion_DeletesGoldenSnapshot(t *testing.T) { - _, s, ctx := setupTest(t) - - if _, err := s.CreateActorSnapshot(ctx, &ateapipb.ActorSnapshot{ - Metadata: &ateapipb.ResourceMetadata{Atespace: "ate-golden", Name: "golden-1"}, - SnapshotUri: "gs://bucket/root/snapshots/ate-golden/golden-1", - }); err != nil { - t.Fatalf("CreateActorSnapshot failed: %v", err) - } - version := newTestActorTemplateVersion("team-a", "tmpl-a-v1", "tmpl-a") - version.GoldenSnapshot = &ateapipb.ObjectRef{Atespace: "ate-golden", Name: "golden-1"} - if _, err := s.CreateActorTemplateVersion(ctx, version); err != nil { - t.Fatalf("CreateActorTemplateVersion failed: %v", err) - } - - if _, err := s.DeleteActorTemplateVersion(ctx, resources.ActorTemplateVersionRef{Atespace: "team-a", Name: "tmpl-a-v1"}); err != nil { - t.Fatalf("DeleteActorTemplateVersion failed: %v", err) - } - if _, err := s.GetActorSnapshot(ctx, "ate-golden", "golden-1"); !errors.Is(err, store.ErrNotFound) { - t.Errorf("golden snapshot after delete = %v, want ErrNotFound", err) - } -} - -func TestDeleteActorTemplateVersion_GoldenSnapshotAlreadyGone(t *testing.T) { - _, s, ctx := setupTest(t) - - version := newTestActorTemplateVersion("team-a", "tmpl-a-v1", "tmpl-a") - version.GoldenSnapshot = &ateapipb.ObjectRef{Atespace: "ate-golden", Name: "never-created"} - if _, err := s.CreateActorTemplateVersion(ctx, version); err != nil { - t.Fatalf("CreateActorTemplateVersion failed: %v", err) - } - if _, err := s.DeleteActorTemplateVersion(ctx, resources.ActorTemplateVersionRef{Atespace: "team-a", Name: "tmpl-a-v1"}); err != nil { - t.Errorf("DeleteActorTemplateVersion with missing golden snapshot = %v, want nil", err) - } -} - -func TestListActorTemplates_Pagination(t *testing.T) { - _, s, ctx := setupTest(t) - - for i := 0; i < 5; i++ { - if _, err := s.CreateActorTemplate(ctx, newTestActorTemplate("team-a", fmt.Sprintf("tmpl-%d", i))); err != nil { - t.Fatalf("failed to create template %d: %v", i, err) - } - } - - var all []*ateapipb.ActorTemplate - pageToken := "" - for { - page, err := s.ListActorTemplates(ctx, "team-a", store.ListOptions{PageSize: 2, PageToken: pageToken}) - if err != nil { - t.Fatalf("ListActorTemplates failed: %v", err) - } - all = append(all, page.Items...) - pageToken = page.NextPageToken - if pageToken == "" { - break - } - } - - if len(all) != 5 { - t.Fatalf("expected 5 templates total, got %d", len(all)) - } - seen := make(map[string]bool) - for _, tmpl := range all { - if seen[tmpl.GetMetadata().GetName()] { - t.Errorf("duplicate template found in paginated results: %s", tmpl.GetMetadata().GetName()) - } - seen[tmpl.GetMetadata().GetName()] = true - } -} - -func TestListActorTemplateVersions_ParentFilter(t *testing.T) { - _, s, ctx := setupTest(t) - - // Interleave versions of two templates. - for i := 0; i < 3; i++ { - if _, err := s.CreateActorTemplateVersion(ctx, newTestActorTemplateVersion("team-a", fmt.Sprintf("tmpl-a-v%d", i), "tmpl-a")); err != nil { - t.Fatalf("failed to create tmpl-a version %d: %v", i, err) - } - } - for i := 0; i < 2; i++ { - if _, err := s.CreateActorTemplateVersion(ctx, newTestActorTemplateVersion("team-a", fmt.Sprintf("tmpl-b-v%d", i), "tmpl-b")); err != nil { - t.Fatalf("failed to create tmpl-b version %d: %v", i, err) - } - } - - unfiltered, err := s.ListActorTemplateVersions(ctx, "team-a", resources.ActorTemplateRef{}, store.ListOptions{PageSize: 1000}) - if err != nil { - t.Fatalf("ListActorTemplateVersions(all) failed: %v", err) - } - if len(unfiltered.Items) != 5 { - t.Fatalf("unfiltered list returned %d versions, want 5", len(unfiltered.Items)) - } - - // Filtered list, paged with a small page size to exercise the - // matched-count pagination semantics. - var filtered []*ateapipb.ActorTemplateVersion - pageToken := "" - for { - page, err := s.ListActorTemplateVersions(ctx, "team-a", resources.ActorTemplateRef{Atespace: "team-a", Name: "tmpl-a"}, store.ListOptions{PageSize: 2, PageToken: pageToken}) - if err != nil { - t.Fatalf("ListActorTemplateVersions(tmpl-a) failed: %v", err) - } - filtered = append(filtered, page.Items...) - pageToken = page.NextPageToken - if pageToken == "" { - break - } - } - - if len(filtered) != 3 { - t.Fatalf("filtered list returned %d versions, want 3", len(filtered)) - } - seen := make(map[string]bool) - for _, v := range filtered { - if v.GetActorTemplate().GetName() != "tmpl-a" { - t.Errorf("filtered list returned version %q of template %q", v.GetMetadata().GetName(), v.GetActorTemplate().GetName()) - } - if seen[v.GetMetadata().GetName()] { - t.Errorf("duplicate version found in paginated results: %s", v.GetMetadata().GetName()) - } - seen[v.GetMetadata().GetName()] = true - } - - // The filter matches the parent's atespace too: scanning all atespaces - // with team-a's tmpl-a must not pick up team-b versions whose parent - // merely shares the name. - if _, err := s.CreateActorTemplateVersion(ctx, newTestActorTemplateVersion("team-b", "tmpl-a-v0", "tmpl-a")); err != nil { - t.Fatalf("failed to create team-b version: %v", err) - } - crossAtespace, err := s.ListActorTemplateVersions(ctx, "", resources.ActorTemplateRef{Atespace: "team-a", Name: "tmpl-a"}, store.ListOptions{PageSize: 1000}) - if err != nil { - t.Fatalf("ListActorTemplateVersions(all atespaces, team-a/tmpl-a) failed: %v", err) - } - if len(crossAtespace.Items) != 3 { - t.Errorf("cross-atespace filtered list returned %d versions, want 3: team-b/tmpl-a versions must not match", len(crossAtespace.Items)) - } -} - -func TestActorTemplates_AtespaceIsolation(t *testing.T) { - _, s, ctx := setupTest(t) - - // The same name in two atespaces is two distinct resources. - inA, err := s.CreateActorTemplate(ctx, newTestActorTemplate("team-a", "tmpl")) - if err != nil { - t.Fatalf("CreateActorTemplate in team-a failed: %v", err) - } - inB, err := s.CreateActorTemplate(ctx, newTestActorTemplate("team-b", "tmpl")) - if err != nil { - t.Fatalf("CreateActorTemplate in team-b = %v, want nil: the name is only taken in team-a", err) - } - if inA.GetMetadata().GetUid() == inB.GetMetadata().GetUid() { - t.Fatalf("templates in different atespaces share uid %q", inA.GetMetadata().GetUid()) - } - - got, err := s.GetActorTemplate(ctx, resources.ActorTemplateRef{Atespace: "team-b", Name: "tmpl"}) - if err != nil { - t.Fatalf("GetActorTemplate(team-b) failed: %v", err) - } - if got.GetMetadata().GetUid() != inB.GetMetadata().GetUid() { - t.Errorf("GetActorTemplate(team-b) returned uid %q, want team-b's %q", got.GetMetadata().GetUid(), inB.GetMetadata().GetUid()) - } - - // The wrong atespace is a clean NotFound, not an internal error. - if _, err := s.GetActorTemplate(ctx, resources.ActorTemplateRef{Atespace: "team-c", Name: "tmpl"}); !errors.Is(err, store.ErrNotFound) { - t.Errorf("GetActorTemplate(team-c) = %v, want ErrNotFound", err) - } - if ok, err := s.ActorTemplateExists(ctx, resources.ActorTemplateRef{Atespace: "team-c", Name: "tmpl"}); err != nil || ok { - t.Errorf("ActorTemplateExists(team-c) = (%v, %v), want (false, nil)", ok, err) - } - - // Deleting in one atespace leaves the other's untouched. - if _, err := s.DeleteActorTemplate(ctx, resources.ActorTemplateRef{Atespace: "team-a", Name: "tmpl"}); err != nil { - t.Fatalf("DeleteActorTemplate(team-a) failed: %v", err) - } - if _, err := s.GetActorTemplate(ctx, resources.ActorTemplateRef{Atespace: "team-b", Name: "tmpl"}); err != nil { - t.Errorf("GetActorTemplate(team-b) after deleting team-a's = %v, want nil", err) - } -} - -func TestListActorTemplates_AtespaceFilter(t *testing.T) { - _, s, ctx := setupTest(t) - - for _, tmpl := range []struct{ atespace, name string }{ - {"team-a", "tmpl-1"}, {"team-a", "tmpl-2"}, {"team-b", "tmpl-3"}, - } { - if _, err := s.CreateActorTemplate(ctx, newTestActorTemplate(tmpl.atespace, tmpl.name)); err != nil { - t.Fatalf("CreateActorTemplate(%s/%s) failed: %v", tmpl.atespace, tmpl.name, err) - } - } - - scopedResp, err := s.ListActorTemplates(ctx, "team-a", store.ListOptions{PageSize: 1000}) - if err != nil { - t.Fatalf("ListActorTemplates(team-a) failed: %v", err) - } - scoped := scopedResp.Items - if len(scoped) != 2 { - t.Errorf("ListActorTemplates(team-a) returned %d templates, want 2", len(scoped)) - } - for _, tmpl := range scoped { - if got := tmpl.GetMetadata().GetAtespace(); got != "team-a" { - t.Errorf("scoped list leaked template %q from atespace %q", tmpl.GetMetadata().GetName(), got) - } - } - - allResp, err := s.ListActorTemplates(ctx, "", store.ListOptions{PageSize: 1000}) - if err != nil { - t.Fatalf("ListActorTemplates(all) failed: %v", err) - } - if len(allResp.Items) != 3 { - t.Errorf("ListActorTemplates(all) returned %d templates, want 3", len(allResp.Items)) - } -} - -func TestActorTemplateVersions_AtespaceIsolation(t *testing.T) { - _, s, ctx := setupTest(t) - - if _, err := s.CreateActorTemplateVersion(ctx, newTestActorTemplateVersion("team-a", "tmpl-v1", "tmpl")); err != nil { - t.Fatalf("CreateActorTemplateVersion in team-a failed: %v", err) - } - if _, err := s.CreateActorTemplateVersion(ctx, newTestActorTemplateVersion("team-b", "tmpl-v1", "tmpl")); err != nil { - t.Fatalf("CreateActorTemplateVersion in team-b = %v, want nil: the name is only taken in team-a", err) - } - - // The wrong atespace is a clean NotFound, not an internal error. - if _, err := s.GetActorTemplateVersion(ctx, resources.ActorTemplateVersionRef{Atespace: "team-c", Name: "tmpl-v1"}); !errors.Is(err, store.ErrNotFound) { - t.Errorf("GetActorTemplateVersion(team-c) = %v, want ErrNotFound", err) - } - - // Versions of the same-named parent list per atespace. - scopedResp, err := s.ListActorTemplateVersions(ctx, "team-a", resources.ActorTemplateRef{Atespace: "team-a", Name: "tmpl"}, store.ListOptions{PageSize: 1000}) - if err != nil { - t.Fatalf("ListActorTemplateVersions(team-a, tmpl) failed: %v", err) - } - scoped := scopedResp.Items - if len(scoped) != 1 || scoped[0].GetMetadata().GetAtespace() != "team-a" { - t.Errorf("ListActorTemplateVersions(team-a, tmpl) = %v, want team-a's tmpl-v1 only", scoped) - } - - // Deleting in one atespace leaves the other's untouched. - if _, err := s.DeleteActorTemplateVersion(ctx, resources.ActorTemplateVersionRef{Atespace: "team-a", Name: "tmpl-v1"}); err != nil { - t.Fatalf("DeleteActorTemplateVersion(team-a) failed: %v", err) - } - if _, err := s.GetActorTemplateVersion(ctx, resources.ActorTemplateVersionRef{Atespace: "team-b", Name: "tmpl-v1"}); err != nil { - t.Errorf("GetActorTemplateVersion(team-b) after deleting team-a's = %v, want nil", err) - } -} - -func TestDeleteActorTemplate_VersionInOtherAtespace_NotBlocking(t *testing.T) { - _, s, ctx := setupTest(t) - - if _, err := s.CreateActorTemplate(ctx, newTestActorTemplate("team-a", "tmpl-a")); err != nil { - t.Fatalf("CreateActorTemplate failed: %v", err) - } - // A version of a same-named template in ANOTHER atespace must not block - // the delete. - if _, err := s.CreateActorTemplateVersion(ctx, newTestActorTemplateVersion("team-b", "tmpl-a-v1", "tmpl-a")); err != nil { - t.Fatalf("CreateActorTemplateVersion failed: %v", err) - } - if _, err := s.DeleteActorTemplate(ctx, resources.ActorTemplateRef{Atespace: "team-a", Name: "tmpl-a"}); err != nil { - t.Errorf("DeleteActorTemplate = %v, want nil: the only version lives in team-b", err) - } -} diff --git a/cmd/ateapi/internal/store/ateredis/contract_test.go b/cmd/ateapi/internal/store/ateredis/contract_test.go deleted file mode 100644 index 3b9df0054..000000000 --- a/cmd/ateapi/internal/store/ateredis/contract_test.go +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright 2026 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package ateredis - -import ( - "testing" - - "github.com/agent-substrate/substrate/cmd/ateapi/internal/store" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/store/storecontract" -) - -// TestContractSuite runs the backend-neutral store.Interface assertions -// against a miniredis-backed Persistence. -func TestContractSuite(t *testing.T) { - storecontract.RunContractTests(t, func(t *testing.T) store.Interface { - _, persistence, _ := setupTest(t) - return persistence - }) -} From 792b208750b970871cf3eda13cd4567ddb0492df Mon Sep 17 00:00:00 2001 From: Jet Chiang Date: Thu, 13 Aug 2026 16:18:00 -0400 Subject: [PATCH 03/10] go mod, vendor, license Signed-off-by: Jet Chiang --- .../github.com/alicebob/miniredis/v2/LICENSE | 21 - .../alicebob/miniredis/v2/fpconv/LICENSE.txt | 26 - .../alicebob/miniredis/v2/geohash/LICENSE | 22 - .../alicebob/miniredis/v2/gopher-json/LICENSE | 24 - .../alicebob/miniredis/v2/hyperloglog/LICENSE | 21 - .../alicebob/miniredis/v2/metro/LICENSE | 24 - .../github.com/dgryski/go-rendezvous/LICENSE | 21 - LICENSES/github.com/redis/go-redis/v9/LICENSE | 25 - LICENSES/github.com/yuin/gopher-lua/LICENSE | 21 - LICENSES/go.uber.org/atomic/LICENSE.txt | 19 - go.mod | 7 +- go.sum | 18 - .../alicebob/miniredis/v2/.gitignore | 6 - .../alicebob/miniredis/v2/CHANGELOG.md | 349 - .../github.com/alicebob/miniredis/v2/LICENSE | 21 - .../github.com/alicebob/miniredis/v2/Makefile | 33 - .../alicebob/miniredis/v2/README.md | 343 - .../github.com/alicebob/miniredis/v2/check.go | 63 - .../alicebob/miniredis/v2/cmd_client.go | 68 - .../alicebob/miniredis/v2/cmd_cluster.go | 122 - .../alicebob/miniredis/v2/cmd_command.go | 14 - .../alicebob/miniredis/v2/cmd_connection.go | 281 - .../alicebob/miniredis/v2/cmd_generic.go | 789 -- .../alicebob/miniredis/v2/cmd_geo.go | 577 -- .../alicebob/miniredis/v2/cmd_hash.go | 797 -- .../alicebob/miniredis/v2/cmd_hll.go | 71 - .../alicebob/miniredis/v2/cmd_info.go | 43 - .../alicebob/miniredis/v2/cmd_list.go | 931 -- .../alicebob/miniredis/v2/cmd_object.go | 50 - .../alicebob/miniredis/v2/cmd_pubsub.go | 254 - .../alicebob/miniredis/v2/cmd_scripting.go | 346 - .../alicebob/miniredis/v2/cmd_server.go | 153 - .../alicebob/miniredis/v2/cmd_set.go | 701 -- .../alicebob/miniredis/v2/cmd_sorted_set.go | 1857 ---- .../alicebob/miniredis/v2/cmd_stream.go | 1696 ---- .../alicebob/miniredis/v2/cmd_string.go | 1166 --- .../alicebob/miniredis/v2/cmd_transactions.go | 140 - vendor/github.com/alicebob/miniredis/v2/db.go | 824 -- .../alicebob/miniredis/v2/direct.go | 862 -- .../alicebob/miniredis/v2/fpconv/LICENSE.txt | 26 - .../alicebob/miniredis/v2/fpconv/Makefile | 6 - .../alicebob/miniredis/v2/fpconv/README.md | 3 - .../alicebob/miniredis/v2/fpconv/dtoa.go | 286 - .../alicebob/miniredis/v2/fpconv/fp.go | 96 - .../alicebob/miniredis/v2/fpconv/powers.go | 82 - .../github.com/alicebob/miniredis/v2/geo.go | 46 - .../alicebob/miniredis/v2/geohash/LICENSE | 22 - .../alicebob/miniredis/v2/geohash/README.md | 2 - .../alicebob/miniredis/v2/geohash/base32.go | 44 - .../alicebob/miniredis/v2/geohash/geohash.go | 269 - .../alicebob/miniredis/v2/gopher-json/LICENSE | 24 - .../miniredis/v2/gopher-json/README.md | 1 - .../alicebob/miniredis/v2/gopher-json/json.go | 189 - .../github.com/alicebob/miniredis/v2/hll.go | 42 - .../alicebob/miniredis/v2/hyperloglog/LICENSE | 21 - .../miniredis/v2/hyperloglog/README.md | 1 - .../miniredis/v2/hyperloglog/compressed.go | 180 - .../miniredis/v2/hyperloglog/hyperloglog.go | 424 - .../miniredis/v2/hyperloglog/registers.go | 114 - .../miniredis/v2/hyperloglog/sparse.go | 92 - .../miniredis/v2/hyperloglog/utils.go | 69 - .../github.com/alicebob/miniredis/v2/keys.go | 83 - .../github.com/alicebob/miniredis/v2/lua.go | 312 - .../alicebob/miniredis/v2/metro/LICENSE | 24 - .../alicebob/miniredis/v2/metro/README.md | 1 - .../alicebob/miniredis/v2/metro/metro64.go | 87 - .../alicebob/miniredis/v2/miniredis.go | 792 -- .../github.com/alicebob/miniredis/v2/opts.go | 60 - .../alicebob/miniredis/v2/proto/Makefile | 2 - .../alicebob/miniredis/v2/proto/client.go | 60 - .../alicebob/miniredis/v2/proto/proto.go | 288 - .../alicebob/miniredis/v2/proto/types.go | 102 - .../alicebob/miniredis/v2/pubsub.go | 240 - .../github.com/alicebob/miniredis/v2/redis.go | 269 - .../alicebob/miniredis/v2/server/Makefile | 9 - .../alicebob/miniredis/v2/server/cmdmeta.go | 17 - .../alicebob/miniredis/v2/server/proto.go | 157 - .../alicebob/miniredis/v2/server/server.go | 519 -- .../alicebob/miniredis/v2/size/readme.md | 2 - .../alicebob/miniredis/v2/size/size.go | 138 - .../alicebob/miniredis/v2/sorted_set.go | 98 - .../alicebob/miniredis/v2/stream.go | 514 -- .../github.com/dgryski/go-rendezvous/LICENSE | 21 - .../github.com/dgryski/go-rendezvous/rdv.go | 79 - .../github.com/redis/go-redis/v9/.gitignore | 19 - .../redis/go-redis/v9/.golangci.yml | 36 - .../redis/go-redis/v9/.prettierrc.yml | 4 - .../redis/go-redis/v9/CONTRIBUTING.md | 118 - vendor/github.com/redis/go-redis/v9/LICENSE | 25 - vendor/github.com/redis/go-redis/v9/Makefile | 122 - vendor/github.com/redis/go-redis/v9/README.md | 596 -- .../redis/go-redis/v9/RELEASE-NOTES.md | 859 -- .../github.com/redis/go-redis/v9/RELEASING.md | 15 - .../redis/go-redis/v9/acl_commands.go | 116 - .../github.com/redis/go-redis/v9/adapters.go | 118 - .../github.com/redis/go-redis/v9/auth/auth.go | 61 - .../v9/auth/reauth_credentials_listener.go | 47 - .../redis/go-redis/v9/bitmap_commands.go | 197 - .../redis/go-redis/v9/cluster_commands.go | 205 - .../github.com/redis/go-redis/v9/command.go | 8027 ----------------- .../go-redis/v9/command_policy_resolver.go | 209 - .../github.com/redis/go-redis/v9/commands.go | 797 -- vendor/github.com/redis/go-redis/v9/doc.go | 4 - .../redis/go-redis/v9/docker-compose.yml | 176 - vendor/github.com/redis/go-redis/v9/error.go | 363 - .../redis/go-redis/v9/generic_commands.go | 392 - .../redis/go-redis/v9/geo_commands.go | 161 - .../redis/go-redis/v9/hash_commands.go | 619 -- .../redis/go-redis/v9/hotkeys_commands.go | 122 - .../redis/go-redis/v9/hyperloglog_commands.go | 42 - .../redis/go-redis/v9/internal/arg.go | 58 - .../conn_reauth_credentials_listener.go | 100 - .../internal/auth/streaming/cred_listeners.go | 77 - .../v9/internal/auth/streaming/manager.go | 137 - .../v9/internal/auth/streaming/pool_hook.go | 241 - .../go-redis/v9/internal/hashtag/hashtag.go | 90 - .../redis/go-redis/v9/internal/hscan/hscan.go | 207 - .../go-redis/v9/internal/hscan/structmap.go | 125 - .../v9/internal/interfaces/interfaces.go | 59 - .../redis/go-redis/v9/internal/internal.go | 29 - .../redis/go-redis/v9/internal/log.go | 79 - .../maintnotifications/logs/log_messages.go | 663 -- .../redis/go-redis/v9/internal/once.go | 63 - .../go-redis/v9/internal/otel/metrics.go | 279 - .../redis/go-redis/v9/internal/pool/conn.go | 948 -- .../go-redis/v9/internal/pool/conn_check.go | 59 - .../v9/internal/pool/conn_check_dummy.go | 20 - .../go-redis/v9/internal/pool/conn_state.go | 343 - .../redis/go-redis/v9/internal/pool/hooks.go | 165 - .../redis/go-redis/v9/internal/pool/pool.go | 1378 --- .../go-redis/v9/internal/pool/pool_single.go | 104 - .../go-redis/v9/internal/pool/pool_sticky.go | 214 - .../redis/go-redis/v9/internal/pool/pubsub.go | 81 - .../go-redis/v9/internal/pool/want_conn.go | 115 - .../go-redis/v9/internal/proto/reader.go | 648 -- .../v9/internal/proto/redis_errors.go | 527 -- .../redis/go-redis/v9/internal/proto/scan.go | 185 - .../go-redis/v9/internal/proto/writer.go | 242 - .../redis/go-redis/v9/internal/rand/rand.go | 50 - .../redis/go-redis/v9/internal/redis.go | 3 - .../v9/internal/routing/aggregator.go | 1000 -- .../go-redis/v9/internal/routing/policy.go | 144 - .../v9/internal/routing/shard_picker.go | 57 - .../redis/go-redis/v9/internal/semaphore.go | 193 - .../redis/go-redis/v9/internal/util.go | 113 - .../go-redis/v9/internal/util/atomic_max.go | 97 - .../go-redis/v9/internal/util/atomic_min.go | 96 - .../go-redis/v9/internal/util/convert.go | 41 - .../redis/go-redis/v9/internal/util/safe.go | 11 - .../go-redis/v9/internal/util/strconv.go | 19 - .../redis/go-redis/v9/internal/util/type.go | 5 - .../redis/go-redis/v9/internal/util/unsafe.go | 17 - .../github.com/redis/go-redis/v9/iterator.go | 66 - vendor/github.com/redis/go-redis/v9/json.go | 650 -- .../redis/go-redis/v9/list_commands.go | 297 - .../v9/maintnotifications/FEATURES.md | 235 - .../go-redis/v9/maintnotifications/README.md | 73 - .../v9/maintnotifications/circuit_breaker.go | 353 - .../go-redis/v9/maintnotifications/config.go | 457 - .../go-redis/v9/maintnotifications/errors.go | 76 - .../v9/maintnotifications/example_hooks.go | 101 - .../v9/maintnotifications/handoff_worker.go | 525 -- .../go-redis/v9/maintnotifications/hooks.go | 60 - .../go-redis/v9/maintnotifications/manager.go | 362 - .../v9/maintnotifications/pool_hook.go | 182 - .../push_notification_handler.go | 524 -- .../go-redis/v9/maintnotifications/state.go | 24 - .../github.com/redis/go-redis/v9/options.go | 815 -- .../redis/go-redis/v9/osscluster.go | 2488 ----- .../redis/go-redis/v9/osscluster_commands.go | 109 - .../redis/go-redis/v9/osscluster_router.go | 992 -- vendor/github.com/redis/go-redis/v9/otel.go | 204 - .../github.com/redis/go-redis/v9/pipeline.go | 136 - .../redis/go-redis/v9/probabilistic.go | 1481 --- vendor/github.com/redis/go-redis/v9/pubsub.go | 812 -- .../redis/go-redis/v9/pubsub_commands.go | 88 - .../redis/go-redis/v9/push/errors.go | 176 - .../redis/go-redis/v9/push/handler.go | 14 - .../redis/go-redis/v9/push/handler_context.go | 44 - .../redis/go-redis/v9/push/processor.go | 203 - .../github.com/redis/go-redis/v9/push/push.go | 7 - .../redis/go-redis/v9/push/registry.go | 61 - .../redis/go-redis/v9/push_notifications.go | 21 - vendor/github.com/redis/go-redis/v9/redis.go | 1630 ---- vendor/github.com/redis/go-redis/v9/result.go | 196 - vendor/github.com/redis/go-redis/v9/ring.go | 953 -- vendor/github.com/redis/go-redis/v9/script.go | 84 - .../redis/go-redis/v9/scripting_commands.go | 215 - .../redis/go-redis/v9/search_builders.go | 825 -- .../redis/go-redis/v9/search_commands.go | 3069 ------- .../github.com/redis/go-redis/v9/sentinel.go | 1248 --- .../redis/go-redis/v9/set_commands.go | 356 - .../redis/go-redis/v9/sortedset_commands.go | 796 -- .../redis/go-redis/v9/stream_commands.go | 601 -- .../redis/go-redis/v9/string_commands.go | 755 -- .../redis/go-redis/v9/timeseries_commands.go | 977 -- vendor/github.com/redis/go-redis/v9/tx.go | 151 - .../github.com/redis/go-redis/v9/universal.go | 390 - .../redis/go-redis/v9/vectorset_commands.go | 358 - .../github.com/redis/go-redis/v9/version.go | 6 - vendor/github.com/yuin/gopher-lua/.gitignore | 1 - vendor/github.com/yuin/gopher-lua/LICENSE | 21 - vendor/github.com/yuin/gopher-lua/Makefile | 10 - vendor/github.com/yuin/gopher-lua/README.rst | 890 -- vendor/github.com/yuin/gopher-lua/_state.go | 2093 ----- vendor/github.com/yuin/gopher-lua/_vm.go | 1049 --- vendor/github.com/yuin/gopher-lua/alloc.go | 79 - vendor/github.com/yuin/gopher-lua/ast/ast.go | 29 - vendor/github.com/yuin/gopher-lua/ast/expr.go | 138 - vendor/github.com/yuin/gopher-lua/ast/misc.go | 17 - vendor/github.com/yuin/gopher-lua/ast/stmt.go | 107 - .../github.com/yuin/gopher-lua/ast/token.go | 22 - vendor/github.com/yuin/gopher-lua/auxlib.go | 465 - vendor/github.com/yuin/gopher-lua/baselib.go | 597 -- .../github.com/yuin/gopher-lua/channellib.go | 184 - vendor/github.com/yuin/gopher-lua/compile.go | 1869 ---- vendor/github.com/yuin/gopher-lua/config.go | 43 - .../yuin/gopher-lua/coroutinelib.go | 112 - vendor/github.com/yuin/gopher-lua/debuglib.go | 173 - vendor/github.com/yuin/gopher-lua/function.go | 193 - vendor/github.com/yuin/gopher-lua/iolib.go | 749 -- vendor/github.com/yuin/gopher-lua/linit.go | 54 - vendor/github.com/yuin/gopher-lua/loadlib.go | 128 - vendor/github.com/yuin/gopher-lua/mathlib.go | 231 - vendor/github.com/yuin/gopher-lua/opcode.go | 371 - vendor/github.com/yuin/gopher-lua/oslib.go | 236 - vendor/github.com/yuin/gopher-lua/package.go | 7 - .../github.com/yuin/gopher-lua/parse/Makefile | 7 - .../github.com/yuin/gopher-lua/parse/lexer.go | 549 -- .../yuin/gopher-lua/parse/parser.go | 1385 --- .../yuin/gopher-lua/parse/parser.go.y | 535 -- vendor/github.com/yuin/gopher-lua/pm/pm.go | 638 -- vendor/github.com/yuin/gopher-lua/state.go | 2306 ----- .../github.com/yuin/gopher-lua/stringlib.go | 448 - vendor/github.com/yuin/gopher-lua/table.go | 387 - vendor/github.com/yuin/gopher-lua/tablelib.go | 100 - vendor/github.com/yuin/gopher-lua/utils.go | 265 - vendor/github.com/yuin/gopher-lua/value.go | 215 - vendor/github.com/yuin/gopher-lua/vm.go | 2465 ----- vendor/go.uber.org/atomic/.codecov.yml | 19 - vendor/go.uber.org/atomic/.gitignore | 15 - vendor/go.uber.org/atomic/CHANGELOG.md | 127 - vendor/go.uber.org/atomic/LICENSE.txt | 19 - vendor/go.uber.org/atomic/Makefile | 79 - vendor/go.uber.org/atomic/README.md | 63 - vendor/go.uber.org/atomic/bool.go | 88 - vendor/go.uber.org/atomic/bool_ext.go | 53 - vendor/go.uber.org/atomic/doc.go | 23 - vendor/go.uber.org/atomic/duration.go | 89 - vendor/go.uber.org/atomic/duration_ext.go | 40 - vendor/go.uber.org/atomic/error.go | 72 - vendor/go.uber.org/atomic/error_ext.go | 39 - vendor/go.uber.org/atomic/float32.go | 77 - vendor/go.uber.org/atomic/float32_ext.go | 76 - vendor/go.uber.org/atomic/float64.go | 77 - vendor/go.uber.org/atomic/float64_ext.go | 76 - vendor/go.uber.org/atomic/gen.go | 27 - vendor/go.uber.org/atomic/int32.go | 109 - vendor/go.uber.org/atomic/int64.go | 109 - vendor/go.uber.org/atomic/nocmp.go | 35 - vendor/go.uber.org/atomic/pointer_go118.go | 31 - .../atomic/pointer_go118_pre119.go | 60 - vendor/go.uber.org/atomic/pointer_go119.go | 61 - vendor/go.uber.org/atomic/string.go | 72 - vendor/go.uber.org/atomic/string_ext.go | 54 - vendor/go.uber.org/atomic/time.go | 55 - vendor/go.uber.org/atomic/time_ext.go | 36 - vendor/go.uber.org/atomic/uint32.go | 109 - vendor/go.uber.org/atomic/uint64.go | 109 - vendor/go.uber.org/atomic/uintptr.go | 109 - vendor/go.uber.org/atomic/unsafe_pointer.go | 65 - vendor/go.uber.org/atomic/value.go | 31 - vendor/modules.txt | 41 - 273 files changed, 1 insertion(+), 86809 deletions(-) delete mode 100644 LICENSES/github.com/alicebob/miniredis/v2/LICENSE delete mode 100644 LICENSES/github.com/alicebob/miniredis/v2/fpconv/LICENSE.txt delete mode 100644 LICENSES/github.com/alicebob/miniredis/v2/geohash/LICENSE delete mode 100644 LICENSES/github.com/alicebob/miniredis/v2/gopher-json/LICENSE delete mode 100644 LICENSES/github.com/alicebob/miniredis/v2/hyperloglog/LICENSE delete mode 100644 LICENSES/github.com/alicebob/miniredis/v2/metro/LICENSE delete mode 100644 LICENSES/github.com/dgryski/go-rendezvous/LICENSE delete mode 100644 LICENSES/github.com/redis/go-redis/v9/LICENSE delete mode 100644 LICENSES/github.com/yuin/gopher-lua/LICENSE delete mode 100644 LICENSES/go.uber.org/atomic/LICENSE.txt delete mode 100644 vendor/github.com/alicebob/miniredis/v2/.gitignore delete mode 100644 vendor/github.com/alicebob/miniredis/v2/CHANGELOG.md delete mode 100644 vendor/github.com/alicebob/miniredis/v2/LICENSE delete mode 100644 vendor/github.com/alicebob/miniredis/v2/Makefile delete mode 100644 vendor/github.com/alicebob/miniredis/v2/README.md delete mode 100644 vendor/github.com/alicebob/miniredis/v2/check.go delete mode 100644 vendor/github.com/alicebob/miniredis/v2/cmd_client.go delete mode 100644 vendor/github.com/alicebob/miniredis/v2/cmd_cluster.go delete mode 100644 vendor/github.com/alicebob/miniredis/v2/cmd_command.go delete mode 100644 vendor/github.com/alicebob/miniredis/v2/cmd_connection.go delete mode 100644 vendor/github.com/alicebob/miniredis/v2/cmd_generic.go delete mode 100644 vendor/github.com/alicebob/miniredis/v2/cmd_geo.go delete mode 100644 vendor/github.com/alicebob/miniredis/v2/cmd_hash.go delete mode 100644 vendor/github.com/alicebob/miniredis/v2/cmd_hll.go delete mode 100644 vendor/github.com/alicebob/miniredis/v2/cmd_info.go delete mode 100644 vendor/github.com/alicebob/miniredis/v2/cmd_list.go delete mode 100644 vendor/github.com/alicebob/miniredis/v2/cmd_object.go delete mode 100644 vendor/github.com/alicebob/miniredis/v2/cmd_pubsub.go delete mode 100644 vendor/github.com/alicebob/miniredis/v2/cmd_scripting.go delete mode 100644 vendor/github.com/alicebob/miniredis/v2/cmd_server.go delete mode 100644 vendor/github.com/alicebob/miniredis/v2/cmd_set.go delete mode 100644 vendor/github.com/alicebob/miniredis/v2/cmd_sorted_set.go delete mode 100644 vendor/github.com/alicebob/miniredis/v2/cmd_stream.go delete mode 100644 vendor/github.com/alicebob/miniredis/v2/cmd_string.go delete mode 100644 vendor/github.com/alicebob/miniredis/v2/cmd_transactions.go delete mode 100644 vendor/github.com/alicebob/miniredis/v2/db.go delete mode 100644 vendor/github.com/alicebob/miniredis/v2/direct.go delete mode 100644 vendor/github.com/alicebob/miniredis/v2/fpconv/LICENSE.txt delete mode 100644 vendor/github.com/alicebob/miniredis/v2/fpconv/Makefile delete mode 100644 vendor/github.com/alicebob/miniredis/v2/fpconv/README.md delete mode 100644 vendor/github.com/alicebob/miniredis/v2/fpconv/dtoa.go delete mode 100644 vendor/github.com/alicebob/miniredis/v2/fpconv/fp.go delete mode 100644 vendor/github.com/alicebob/miniredis/v2/fpconv/powers.go delete mode 100644 vendor/github.com/alicebob/miniredis/v2/geo.go delete mode 100644 vendor/github.com/alicebob/miniredis/v2/geohash/LICENSE delete mode 100644 vendor/github.com/alicebob/miniredis/v2/geohash/README.md delete mode 100644 vendor/github.com/alicebob/miniredis/v2/geohash/base32.go delete mode 100644 vendor/github.com/alicebob/miniredis/v2/geohash/geohash.go delete mode 100644 vendor/github.com/alicebob/miniredis/v2/gopher-json/LICENSE delete mode 100644 vendor/github.com/alicebob/miniredis/v2/gopher-json/README.md delete mode 100644 vendor/github.com/alicebob/miniredis/v2/gopher-json/json.go delete mode 100644 vendor/github.com/alicebob/miniredis/v2/hll.go delete mode 100644 vendor/github.com/alicebob/miniredis/v2/hyperloglog/LICENSE delete mode 100644 vendor/github.com/alicebob/miniredis/v2/hyperloglog/README.md delete mode 100644 vendor/github.com/alicebob/miniredis/v2/hyperloglog/compressed.go delete mode 100644 vendor/github.com/alicebob/miniredis/v2/hyperloglog/hyperloglog.go delete mode 100644 vendor/github.com/alicebob/miniredis/v2/hyperloglog/registers.go delete mode 100644 vendor/github.com/alicebob/miniredis/v2/hyperloglog/sparse.go delete mode 100644 vendor/github.com/alicebob/miniredis/v2/hyperloglog/utils.go delete mode 100644 vendor/github.com/alicebob/miniredis/v2/keys.go delete mode 100644 vendor/github.com/alicebob/miniredis/v2/lua.go delete mode 100644 vendor/github.com/alicebob/miniredis/v2/metro/LICENSE delete mode 100644 vendor/github.com/alicebob/miniredis/v2/metro/README.md delete mode 100644 vendor/github.com/alicebob/miniredis/v2/metro/metro64.go delete mode 100644 vendor/github.com/alicebob/miniredis/v2/miniredis.go delete mode 100644 vendor/github.com/alicebob/miniredis/v2/opts.go delete mode 100644 vendor/github.com/alicebob/miniredis/v2/proto/Makefile delete mode 100644 vendor/github.com/alicebob/miniredis/v2/proto/client.go delete mode 100644 vendor/github.com/alicebob/miniredis/v2/proto/proto.go delete mode 100644 vendor/github.com/alicebob/miniredis/v2/proto/types.go delete mode 100644 vendor/github.com/alicebob/miniredis/v2/pubsub.go delete mode 100644 vendor/github.com/alicebob/miniredis/v2/redis.go delete mode 100644 vendor/github.com/alicebob/miniredis/v2/server/Makefile delete mode 100644 vendor/github.com/alicebob/miniredis/v2/server/cmdmeta.go delete mode 100644 vendor/github.com/alicebob/miniredis/v2/server/proto.go delete mode 100644 vendor/github.com/alicebob/miniredis/v2/server/server.go delete mode 100644 vendor/github.com/alicebob/miniredis/v2/size/readme.md delete mode 100644 vendor/github.com/alicebob/miniredis/v2/size/size.go delete mode 100644 vendor/github.com/alicebob/miniredis/v2/sorted_set.go delete mode 100644 vendor/github.com/alicebob/miniredis/v2/stream.go delete mode 100644 vendor/github.com/dgryski/go-rendezvous/LICENSE delete mode 100644 vendor/github.com/dgryski/go-rendezvous/rdv.go delete mode 100644 vendor/github.com/redis/go-redis/v9/.gitignore delete mode 100644 vendor/github.com/redis/go-redis/v9/.golangci.yml delete mode 100644 vendor/github.com/redis/go-redis/v9/.prettierrc.yml delete mode 100644 vendor/github.com/redis/go-redis/v9/CONTRIBUTING.md delete mode 100644 vendor/github.com/redis/go-redis/v9/LICENSE delete mode 100644 vendor/github.com/redis/go-redis/v9/Makefile delete mode 100644 vendor/github.com/redis/go-redis/v9/README.md delete mode 100644 vendor/github.com/redis/go-redis/v9/RELEASE-NOTES.md delete mode 100644 vendor/github.com/redis/go-redis/v9/RELEASING.md delete mode 100644 vendor/github.com/redis/go-redis/v9/acl_commands.go delete mode 100644 vendor/github.com/redis/go-redis/v9/adapters.go delete mode 100644 vendor/github.com/redis/go-redis/v9/auth/auth.go delete mode 100644 vendor/github.com/redis/go-redis/v9/auth/reauth_credentials_listener.go delete mode 100644 vendor/github.com/redis/go-redis/v9/bitmap_commands.go delete mode 100644 vendor/github.com/redis/go-redis/v9/cluster_commands.go delete mode 100644 vendor/github.com/redis/go-redis/v9/command.go delete mode 100644 vendor/github.com/redis/go-redis/v9/command_policy_resolver.go delete mode 100644 vendor/github.com/redis/go-redis/v9/commands.go delete mode 100644 vendor/github.com/redis/go-redis/v9/doc.go delete mode 100644 vendor/github.com/redis/go-redis/v9/docker-compose.yml delete mode 100644 vendor/github.com/redis/go-redis/v9/error.go delete mode 100644 vendor/github.com/redis/go-redis/v9/generic_commands.go delete mode 100644 vendor/github.com/redis/go-redis/v9/geo_commands.go delete mode 100644 vendor/github.com/redis/go-redis/v9/hash_commands.go delete mode 100644 vendor/github.com/redis/go-redis/v9/hotkeys_commands.go delete mode 100644 vendor/github.com/redis/go-redis/v9/hyperloglog_commands.go delete mode 100644 vendor/github.com/redis/go-redis/v9/internal/arg.go delete mode 100644 vendor/github.com/redis/go-redis/v9/internal/auth/streaming/conn_reauth_credentials_listener.go delete mode 100644 vendor/github.com/redis/go-redis/v9/internal/auth/streaming/cred_listeners.go delete mode 100644 vendor/github.com/redis/go-redis/v9/internal/auth/streaming/manager.go delete mode 100644 vendor/github.com/redis/go-redis/v9/internal/auth/streaming/pool_hook.go delete mode 100644 vendor/github.com/redis/go-redis/v9/internal/hashtag/hashtag.go delete mode 100644 vendor/github.com/redis/go-redis/v9/internal/hscan/hscan.go delete mode 100644 vendor/github.com/redis/go-redis/v9/internal/hscan/structmap.go delete mode 100644 vendor/github.com/redis/go-redis/v9/internal/interfaces/interfaces.go delete mode 100644 vendor/github.com/redis/go-redis/v9/internal/internal.go delete mode 100644 vendor/github.com/redis/go-redis/v9/internal/log.go delete mode 100644 vendor/github.com/redis/go-redis/v9/internal/maintnotifications/logs/log_messages.go delete mode 100644 vendor/github.com/redis/go-redis/v9/internal/once.go delete mode 100644 vendor/github.com/redis/go-redis/v9/internal/otel/metrics.go delete mode 100644 vendor/github.com/redis/go-redis/v9/internal/pool/conn.go delete mode 100644 vendor/github.com/redis/go-redis/v9/internal/pool/conn_check.go delete mode 100644 vendor/github.com/redis/go-redis/v9/internal/pool/conn_check_dummy.go delete mode 100644 vendor/github.com/redis/go-redis/v9/internal/pool/conn_state.go delete mode 100644 vendor/github.com/redis/go-redis/v9/internal/pool/hooks.go delete mode 100644 vendor/github.com/redis/go-redis/v9/internal/pool/pool.go delete mode 100644 vendor/github.com/redis/go-redis/v9/internal/pool/pool_single.go delete mode 100644 vendor/github.com/redis/go-redis/v9/internal/pool/pool_sticky.go delete mode 100644 vendor/github.com/redis/go-redis/v9/internal/pool/pubsub.go delete mode 100644 vendor/github.com/redis/go-redis/v9/internal/pool/want_conn.go delete mode 100644 vendor/github.com/redis/go-redis/v9/internal/proto/reader.go delete mode 100644 vendor/github.com/redis/go-redis/v9/internal/proto/redis_errors.go delete mode 100644 vendor/github.com/redis/go-redis/v9/internal/proto/scan.go delete mode 100644 vendor/github.com/redis/go-redis/v9/internal/proto/writer.go delete mode 100644 vendor/github.com/redis/go-redis/v9/internal/rand/rand.go delete mode 100644 vendor/github.com/redis/go-redis/v9/internal/redis.go delete mode 100644 vendor/github.com/redis/go-redis/v9/internal/routing/aggregator.go delete mode 100644 vendor/github.com/redis/go-redis/v9/internal/routing/policy.go delete mode 100644 vendor/github.com/redis/go-redis/v9/internal/routing/shard_picker.go delete mode 100644 vendor/github.com/redis/go-redis/v9/internal/semaphore.go delete mode 100644 vendor/github.com/redis/go-redis/v9/internal/util.go delete mode 100644 vendor/github.com/redis/go-redis/v9/internal/util/atomic_max.go delete mode 100644 vendor/github.com/redis/go-redis/v9/internal/util/atomic_min.go delete mode 100644 vendor/github.com/redis/go-redis/v9/internal/util/convert.go delete mode 100644 vendor/github.com/redis/go-redis/v9/internal/util/safe.go delete mode 100644 vendor/github.com/redis/go-redis/v9/internal/util/strconv.go delete mode 100644 vendor/github.com/redis/go-redis/v9/internal/util/type.go delete mode 100644 vendor/github.com/redis/go-redis/v9/internal/util/unsafe.go delete mode 100644 vendor/github.com/redis/go-redis/v9/iterator.go delete mode 100644 vendor/github.com/redis/go-redis/v9/json.go delete mode 100644 vendor/github.com/redis/go-redis/v9/list_commands.go delete mode 100644 vendor/github.com/redis/go-redis/v9/maintnotifications/FEATURES.md delete mode 100644 vendor/github.com/redis/go-redis/v9/maintnotifications/README.md delete mode 100644 vendor/github.com/redis/go-redis/v9/maintnotifications/circuit_breaker.go delete mode 100644 vendor/github.com/redis/go-redis/v9/maintnotifications/config.go delete mode 100644 vendor/github.com/redis/go-redis/v9/maintnotifications/errors.go delete mode 100644 vendor/github.com/redis/go-redis/v9/maintnotifications/example_hooks.go delete mode 100644 vendor/github.com/redis/go-redis/v9/maintnotifications/handoff_worker.go delete mode 100644 vendor/github.com/redis/go-redis/v9/maintnotifications/hooks.go delete mode 100644 vendor/github.com/redis/go-redis/v9/maintnotifications/manager.go delete mode 100644 vendor/github.com/redis/go-redis/v9/maintnotifications/pool_hook.go delete mode 100644 vendor/github.com/redis/go-redis/v9/maintnotifications/push_notification_handler.go delete mode 100644 vendor/github.com/redis/go-redis/v9/maintnotifications/state.go delete mode 100644 vendor/github.com/redis/go-redis/v9/options.go delete mode 100644 vendor/github.com/redis/go-redis/v9/osscluster.go delete mode 100644 vendor/github.com/redis/go-redis/v9/osscluster_commands.go delete mode 100644 vendor/github.com/redis/go-redis/v9/osscluster_router.go delete mode 100644 vendor/github.com/redis/go-redis/v9/otel.go delete mode 100644 vendor/github.com/redis/go-redis/v9/pipeline.go delete mode 100644 vendor/github.com/redis/go-redis/v9/probabilistic.go delete mode 100644 vendor/github.com/redis/go-redis/v9/pubsub.go delete mode 100644 vendor/github.com/redis/go-redis/v9/pubsub_commands.go delete mode 100644 vendor/github.com/redis/go-redis/v9/push/errors.go delete mode 100644 vendor/github.com/redis/go-redis/v9/push/handler.go delete mode 100644 vendor/github.com/redis/go-redis/v9/push/handler_context.go delete mode 100644 vendor/github.com/redis/go-redis/v9/push/processor.go delete mode 100644 vendor/github.com/redis/go-redis/v9/push/push.go delete mode 100644 vendor/github.com/redis/go-redis/v9/push/registry.go delete mode 100644 vendor/github.com/redis/go-redis/v9/push_notifications.go delete mode 100644 vendor/github.com/redis/go-redis/v9/redis.go delete mode 100644 vendor/github.com/redis/go-redis/v9/result.go delete mode 100644 vendor/github.com/redis/go-redis/v9/ring.go delete mode 100644 vendor/github.com/redis/go-redis/v9/script.go delete mode 100644 vendor/github.com/redis/go-redis/v9/scripting_commands.go delete mode 100644 vendor/github.com/redis/go-redis/v9/search_builders.go delete mode 100644 vendor/github.com/redis/go-redis/v9/search_commands.go delete mode 100644 vendor/github.com/redis/go-redis/v9/sentinel.go delete mode 100644 vendor/github.com/redis/go-redis/v9/set_commands.go delete mode 100644 vendor/github.com/redis/go-redis/v9/sortedset_commands.go delete mode 100644 vendor/github.com/redis/go-redis/v9/stream_commands.go delete mode 100644 vendor/github.com/redis/go-redis/v9/string_commands.go delete mode 100644 vendor/github.com/redis/go-redis/v9/timeseries_commands.go delete mode 100644 vendor/github.com/redis/go-redis/v9/tx.go delete mode 100644 vendor/github.com/redis/go-redis/v9/universal.go delete mode 100644 vendor/github.com/redis/go-redis/v9/vectorset_commands.go delete mode 100644 vendor/github.com/redis/go-redis/v9/version.go delete mode 100644 vendor/github.com/yuin/gopher-lua/.gitignore delete mode 100644 vendor/github.com/yuin/gopher-lua/LICENSE delete mode 100644 vendor/github.com/yuin/gopher-lua/Makefile delete mode 100644 vendor/github.com/yuin/gopher-lua/README.rst delete mode 100644 vendor/github.com/yuin/gopher-lua/_state.go delete mode 100644 vendor/github.com/yuin/gopher-lua/_vm.go delete mode 100644 vendor/github.com/yuin/gopher-lua/alloc.go delete mode 100644 vendor/github.com/yuin/gopher-lua/ast/ast.go delete mode 100644 vendor/github.com/yuin/gopher-lua/ast/expr.go delete mode 100644 vendor/github.com/yuin/gopher-lua/ast/misc.go delete mode 100644 vendor/github.com/yuin/gopher-lua/ast/stmt.go delete mode 100644 vendor/github.com/yuin/gopher-lua/ast/token.go delete mode 100644 vendor/github.com/yuin/gopher-lua/auxlib.go delete mode 100644 vendor/github.com/yuin/gopher-lua/baselib.go delete mode 100644 vendor/github.com/yuin/gopher-lua/channellib.go delete mode 100644 vendor/github.com/yuin/gopher-lua/compile.go delete mode 100644 vendor/github.com/yuin/gopher-lua/config.go delete mode 100644 vendor/github.com/yuin/gopher-lua/coroutinelib.go delete mode 100644 vendor/github.com/yuin/gopher-lua/debuglib.go delete mode 100644 vendor/github.com/yuin/gopher-lua/function.go delete mode 100644 vendor/github.com/yuin/gopher-lua/iolib.go delete mode 100644 vendor/github.com/yuin/gopher-lua/linit.go delete mode 100644 vendor/github.com/yuin/gopher-lua/loadlib.go delete mode 100644 vendor/github.com/yuin/gopher-lua/mathlib.go delete mode 100644 vendor/github.com/yuin/gopher-lua/opcode.go delete mode 100644 vendor/github.com/yuin/gopher-lua/oslib.go delete mode 100644 vendor/github.com/yuin/gopher-lua/package.go delete mode 100644 vendor/github.com/yuin/gopher-lua/parse/Makefile delete mode 100644 vendor/github.com/yuin/gopher-lua/parse/lexer.go delete mode 100644 vendor/github.com/yuin/gopher-lua/parse/parser.go delete mode 100644 vendor/github.com/yuin/gopher-lua/parse/parser.go.y delete mode 100644 vendor/github.com/yuin/gopher-lua/pm/pm.go delete mode 100644 vendor/github.com/yuin/gopher-lua/state.go delete mode 100644 vendor/github.com/yuin/gopher-lua/stringlib.go delete mode 100644 vendor/github.com/yuin/gopher-lua/table.go delete mode 100644 vendor/github.com/yuin/gopher-lua/tablelib.go delete mode 100644 vendor/github.com/yuin/gopher-lua/utils.go delete mode 100644 vendor/github.com/yuin/gopher-lua/value.go delete mode 100644 vendor/github.com/yuin/gopher-lua/vm.go delete mode 100644 vendor/go.uber.org/atomic/.codecov.yml delete mode 100644 vendor/go.uber.org/atomic/.gitignore delete mode 100644 vendor/go.uber.org/atomic/CHANGELOG.md delete mode 100644 vendor/go.uber.org/atomic/LICENSE.txt delete mode 100644 vendor/go.uber.org/atomic/Makefile delete mode 100644 vendor/go.uber.org/atomic/README.md delete mode 100644 vendor/go.uber.org/atomic/bool.go delete mode 100644 vendor/go.uber.org/atomic/bool_ext.go delete mode 100644 vendor/go.uber.org/atomic/doc.go delete mode 100644 vendor/go.uber.org/atomic/duration.go delete mode 100644 vendor/go.uber.org/atomic/duration_ext.go delete mode 100644 vendor/go.uber.org/atomic/error.go delete mode 100644 vendor/go.uber.org/atomic/error_ext.go delete mode 100644 vendor/go.uber.org/atomic/float32.go delete mode 100644 vendor/go.uber.org/atomic/float32_ext.go delete mode 100644 vendor/go.uber.org/atomic/float64.go delete mode 100644 vendor/go.uber.org/atomic/float64_ext.go delete mode 100644 vendor/go.uber.org/atomic/gen.go delete mode 100644 vendor/go.uber.org/atomic/int32.go delete mode 100644 vendor/go.uber.org/atomic/int64.go delete mode 100644 vendor/go.uber.org/atomic/nocmp.go delete mode 100644 vendor/go.uber.org/atomic/pointer_go118.go delete mode 100644 vendor/go.uber.org/atomic/pointer_go118_pre119.go delete mode 100644 vendor/go.uber.org/atomic/pointer_go119.go delete mode 100644 vendor/go.uber.org/atomic/string.go delete mode 100644 vendor/go.uber.org/atomic/string_ext.go delete mode 100644 vendor/go.uber.org/atomic/time.go delete mode 100644 vendor/go.uber.org/atomic/time_ext.go delete mode 100644 vendor/go.uber.org/atomic/uint32.go delete mode 100644 vendor/go.uber.org/atomic/uint64.go delete mode 100644 vendor/go.uber.org/atomic/uintptr.go delete mode 100644 vendor/go.uber.org/atomic/unsafe_pointer.go delete mode 100644 vendor/go.uber.org/atomic/value.go diff --git a/LICENSES/github.com/alicebob/miniredis/v2/LICENSE b/LICENSES/github.com/alicebob/miniredis/v2/LICENSE deleted file mode 100644 index bb02657ca..000000000 --- a/LICENSES/github.com/alicebob/miniredis/v2/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2014 Harmen - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/LICENSES/github.com/alicebob/miniredis/v2/fpconv/LICENSE.txt b/LICENSES/github.com/alicebob/miniredis/v2/fpconv/LICENSE.txt deleted file mode 100644 index 0a0af2e8f..000000000 --- a/LICENSES/github.com/alicebob/miniredis/v2/fpconv/LICENSE.txt +++ /dev/null @@ -1,26 +0,0 @@ -This code is derived from the C code in redis-7.2.0/deps/fpconv/*, which has -this license: - -Boost Software License - Version 1.0 - August 17th, 2003 - -Permission is hereby granted, free of charge, to any person or organization -obtaining a copy of the software and accompanying documentation covered by -this license (the "Software") to use, reproduce, display, distribute, -execute, and transmit the Software, and to prepare derivative works of the -Software, and to permit third-parties to whom the Software is furnished to -do so, all subject to the following: - -The copyright notices in the Software and this entire statement, including -the above license grant, this restriction and the following disclaimer, -must be included in all copies of the Software, in whole or in part, and -all derivative works of the Software, unless such copies or derivative -works are solely in the form of machine-executable object code generated by -a source language processor. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT -SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE -FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, -ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. diff --git a/LICENSES/github.com/alicebob/miniredis/v2/geohash/LICENSE b/LICENSES/github.com/alicebob/miniredis/v2/geohash/LICENSE deleted file mode 100644 index c0190c9a6..000000000 --- a/LICENSES/github.com/alicebob/miniredis/v2/geohash/LICENSE +++ /dev/null @@ -1,22 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2015 Michael McLoughlin - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - diff --git a/LICENSES/github.com/alicebob/miniredis/v2/gopher-json/LICENSE b/LICENSES/github.com/alicebob/miniredis/v2/gopher-json/LICENSE deleted file mode 100644 index 68a49daad..000000000 --- a/LICENSES/github.com/alicebob/miniredis/v2/gopher-json/LICENSE +++ /dev/null @@ -1,24 +0,0 @@ -This is free and unencumbered software released into the public domain. - -Anyone is free to copy, modify, publish, use, compile, sell, or -distribute this software, either in source code form or as a compiled -binary, for any purpose, commercial or non-commercial, and by any -means. - -In jurisdictions that recognize copyright laws, the author or authors -of this software dedicate any and all copyright interest in the -software to the public domain. We make this dedication for the benefit -of the public at large and to the detriment of our heirs and -successors. We intend this dedication to be an overt act of -relinquishment in perpetuity of all present and future rights to this -software under copyright law. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR -OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, -ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -OTHER DEALINGS IN THE SOFTWARE. - -For more information, please refer to diff --git a/LICENSES/github.com/alicebob/miniredis/v2/hyperloglog/LICENSE b/LICENSES/github.com/alicebob/miniredis/v2/hyperloglog/LICENSE deleted file mode 100644 index 8436fdb43..000000000 --- a/LICENSES/github.com/alicebob/miniredis/v2/hyperloglog/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2017 Axiom Inc. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/LICENSES/github.com/alicebob/miniredis/v2/metro/LICENSE b/LICENSES/github.com/alicebob/miniredis/v2/metro/LICENSE deleted file mode 100644 index 6243b617c..000000000 --- a/LICENSES/github.com/alicebob/miniredis/v2/metro/LICENSE +++ /dev/null @@ -1,24 +0,0 @@ -This package is a mechanical translation of the reference C++ code for -MetroHash, available at https://github.com/jandrewrogers/MetroHash - -The MIT License (MIT) - -Copyright (c) 2016 Damian Gryski - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/LICENSES/github.com/dgryski/go-rendezvous/LICENSE b/LICENSES/github.com/dgryski/go-rendezvous/LICENSE deleted file mode 100644 index 22080f736..000000000 --- a/LICENSES/github.com/dgryski/go-rendezvous/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2017-2020 Damian Gryski - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. diff --git a/LICENSES/github.com/redis/go-redis/v9/LICENSE b/LICENSES/github.com/redis/go-redis/v9/LICENSE deleted file mode 100644 index f4967dbc5..000000000 --- a/LICENSES/github.com/redis/go-redis/v9/LICENSE +++ /dev/null @@ -1,25 +0,0 @@ -Copyright (c) 2013 The github.com/redis/go-redis Authors. -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are -met: - - * Redistributions of source code must retain the above copyright -notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above -copyright notice, this list of conditions and the following disclaimer -in the documentation and/or other materials provided with the -distribution. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/LICENSES/github.com/yuin/gopher-lua/LICENSE b/LICENSES/github.com/yuin/gopher-lua/LICENSE deleted file mode 100644 index 4daf480a2..000000000 --- a/LICENSES/github.com/yuin/gopher-lua/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2015 Yusuke Inuzuka - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/LICENSES/go.uber.org/atomic/LICENSE.txt b/LICENSES/go.uber.org/atomic/LICENSE.txt deleted file mode 100644 index 8765c9fbc..000000000 --- a/LICENSES/go.uber.org/atomic/LICENSE.txt +++ /dev/null @@ -1,19 +0,0 @@ -Copyright (c) 2016 Uber Technologies, Inc. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. diff --git a/go.mod b/go.mod index 17665f007..518fd49a7 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,6 @@ require ( cloud.google.com/go/resourcemanager v1.13.0 cloud.google.com/go/serviceusage v1.14.0 cloud.google.com/go/storage v1.62.1 - github.com/alicebob/miniredis/v2 v2.37.0 github.com/aws/aws-sdk-go-v2 v1.41.7 github.com/aws/aws-sdk-go-v2/config v1.32.17 github.com/aws/aws-sdk-go-v2/service/s3 v1.101.0 @@ -31,7 +30,6 @@ require ( github.com/opencontainers/runtime-spec v1.3.0 github.com/pelletier/go-toml/v2 v2.4.0 github.com/prometheus/client_golang v1.23.2 - github.com/redis/go-redis/v9 v9.18.0 github.com/spf13/cobra v1.10.2 github.com/spf13/pflag v1.0.10 github.com/spiffe/go-spiffe/v2 v2.6.0 @@ -49,7 +47,6 @@ require ( go.opentelemetry.io/otel/sdk v1.43.0 go.opentelemetry.io/otel/sdk/metric v1.43.0 go.opentelemetry.io/otel/trace v1.43.0 - golang.org/x/oauth2 v0.36.0 golang.org/x/sync v0.21.0 golang.org/x/sys v0.46.0 google.golang.org/api v0.274.0 @@ -106,7 +103,6 @@ require ( github.com/containerd/platforms v0.2.1 // indirect github.com/cpuguy83/dockercfg v0.3.2 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/distribution/reference v0.6.0 // indirect github.com/docker/cli v29.5.3+incompatible // indirect github.com/docker/docker-credential-helpers v0.9.3 // indirect @@ -194,18 +190,17 @@ require ( github.com/tklauser/numcpus v0.12.0 // indirect github.com/ugorji/go/codec v1.3.1 // indirect github.com/x448/float16 v0.8.4 // indirect - github.com/yuin/gopher-lua v1.1.1 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect github.com/zeromq/goczmq v4.1.0+incompatible // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/detectors/gcp v1.43.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 // indirect go.opentelemetry.io/proto/otlp v1.10.0 // indirect - go.uber.org/atomic v1.11.0 // indirect go.yaml.in/yaml/v2 v2.4.4 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/crypto v0.52.0 // indirect golang.org/x/net v0.55.0 // indirect + golang.org/x/oauth2 v0.36.0 // indirect golang.org/x/term v0.43.0 // indirect golang.org/x/text v0.39.0 // indirect golang.org/x/time v0.15.0 // indirect diff --git a/go.sum b/go.sum index 82a1dace3..5409d1763 100644 --- a/go.sum +++ b/go.sum @@ -44,8 +44,6 @@ github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1 github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= -github.com/alicebob/miniredis/v2 v2.37.0 h1:RheObYW32G1aiJIj81XVt78ZHJpHonHLHW7OLIshq68= -github.com/alicebob/miniredis/v2 v2.37.0/go.mod h1:TcL7YfarKPGDAthEtl5NBeHZfeUQj6OXMm/+iu5cLMM= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/asaskevich/EventBus v0.0.0-20200907212545-49d423059eef h1:2JGTg6JapxP9/R33ZaagQtAM4EkkSYnIAlOG5EI8gkM= @@ -88,10 +86,6 @@ github.com/aws/smithy-go v1.25.1 h1:J8ERsGSU7d+aCmdQur5Txg6bVoYelvQJgtZehD12GkI= github.com/aws/smithy-go v1.25.1/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= -github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= -github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= -github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= @@ -123,8 +117,6 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= -github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/docker/cli v29.5.3+incompatible h1:nbEFfz774vBwQ5KRYv7c/AghjReqnGISvrRhzjV0evs= @@ -253,8 +245,6 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao= github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= -github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4= -github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -352,8 +342,6 @@ github.com/prometheus/otlptranslator v1.0.0 h1:s0LJW/iN9dkIH+EnhiD3BlkkP5QVIUVEo github.com/prometheus/otlptranslator v1.0.0/go.mod h1:vRYWnXvI6aWGpsdY/mOT/cbeVRBlPWtBNDb7kGR3uKM= github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc= github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo= -github.com/redis/go-redis/v9 v9.18.0 h1:pMkxYPkEbMPwRdenAzUNyFNrDgHx9U+DrBabWNfSRQs= -github.com/redis/go-redis/v9 v9.18.0/go.mod h1:k3ufPphLU5YXwNTUcCRXGxUoF1fqxnhFQmscfkCoDA0= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= @@ -395,12 +383,8 @@ github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zd github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= -github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M= -github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= -github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= -github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= github.com/zeromq/goczmq v4.1.0+incompatible h1:cGVQaU6kIwwrGso0Pgbl84tzAz/h7FJ3wYQjSonjFFc= github.com/zeromq/goczmq v4.1.0+incompatible/go.mod h1:1uZybAJoSRCvZMH2rZxEwWBSmC4T7CB/xQOfChwPEzg= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= @@ -435,8 +419,6 @@ go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09 go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g= go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk= -go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= -go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= diff --git a/vendor/github.com/alicebob/miniredis/v2/.gitignore b/vendor/github.com/alicebob/miniredis/v2/.gitignore deleted file mode 100644 index 8016b4be3..000000000 --- a/vendor/github.com/alicebob/miniredis/v2/.gitignore +++ /dev/null @@ -1,6 +0,0 @@ -/integration/redis_src/ -/integration/dump.rdb -*.swp -/integration/nodes.conf -.idea/ -miniredis.iml diff --git a/vendor/github.com/alicebob/miniredis/v2/CHANGELOG.md b/vendor/github.com/alicebob/miniredis/v2/CHANGELOG.md deleted file mode 100644 index 2c8cb85f1..000000000 --- a/vendor/github.com/alicebob/miniredis/v2/CHANGELOG.md +++ /dev/null @@ -1,349 +0,0 @@ -## Changelog - - -## v2.37.0 - -- suport HEXPIRE (thanks @mojixcoder) - - -## v2.36.1 - -- support CLUSTER SHARDS (thanks @dadrus) - - -## v2.36.0 - -- return actual server address by CLUSTER NODES (thanks @nastik-kum) -- support DUMP and RESTORE (thanks @alyssaruth) -- support EVALRO (thanks @max-frank) -- add WAIT command as no-op (thanks @aroullet) -- support info stats (thanks @destinyoooo) -- add "-*" keys -- compare against Redis 8.4.0 - - -## v2.35.0 - -- add Lua redis.setresp({2,3}) -- embed gopher-json package -- fix XAUTOCLAIM (thanks @kgunning) -- fix writeXpending (thanks @gnpaone) -- fix BLMOVE TTL special case -- constants for key types @alyssaruth - - -### v2.34.0 - -- fix ZINTERSTORE where target is one of the source sets -- added support for ZRank and ZRevRank with score (thanks Jeff Howell) -- fix MEMORY subcommand casing (thanks @joshaber) -- use streamCmp in Xtrim (thanks @daniel-cohere) - - -### v2.33.0 - -- minimum Go version is now 1.17 -- fix integer overflow (thanks @wszaranski) -- test against the last BSD redis (7.2.4) -- ignore 'redis.set_repl()' call (thanks @TingluoHuang) -- various build fixes (thanks @wszaranski) -- add StartAddrTLS function (thanks @agriffaut) -- support for the NOMKSTREAM option for XADD (thanks @Jahaja) -- return empty array for SRANDMEMBER on nonexistent key (thanks @WKBae) - - -### v2.32.1 - -- support for SINTERCARD (thanks @s-barr-fetch) -- support for EXPIRETIME and PEXPIRETIME (thanks @wszaranski) -- fix GEO* units to be case insensitive - - -### v2.31.1 - -- support COUNT in SCAN and ZSCAN (thanks @BarakSilverfort) -- support for OBJECT IDLETIME (thanks @nerd2) -- support for HRANDFIELD (thanks @sejin-P) - - -### v2.31.0 - -- support for MEMORY USAGE (thanks @davidroman0O) -- test against Redis 7.2.0 -- support for CLIENT SETNAME/GETNAME (thanks @mr-karan) -- fix very small numbers (thanks @zsh1995) -- use the same float-to-string logic real Redis uses - - -### v2.30.5 - -- support SMISMEMBER (thanks @sandyharvie) - - -### v2.30.4 - -- fix ZADD LT/LG (thanks @sejin-P) -- fix COPY (thanks @jerargus) -- quicker SPOP - - -### v2.30.3 - -- fix lua error_reply (thanks @pkierski) -- fix use of blocking functions in lua -- support for ZMSCORE (thanks @lsgndln) -- lua cache (thanks @tonyhb) - - -### v2.30.2 - -- support MINID in XADD (thanks @nathan-cormier) -- support BLMOVE (thanks @sevein) -- fix COMMAND (thanks @pje) -- fix 'XREAD ... $' on a non-existing stream - - -### v2.30.1 - -- support SET NX GET special case - - -### v2.30.0 - -- implement redis 7.0.x (from 6.X). Main changes: - - test against 7.0.7 - - update error messages - - support nx|xx|gt|lt options in [P]EXPIRE[AT] - - update how deleted items are processed in pending queues in streams - - -### v2.23.1 - -- resolve $ to latest ID in XREAD (thanks @josh-hook) -- handle disconnect in blocking functions (thanks @jgirtakovskis) -- fix type conversion bug in redisToLua (thanks Sandy Harvie) -- BRPOP{LPUSH} timeout can be float since 6.0 - - -### v2.23.0 - -- basic INFO support (thanks @kirill-a-belov) -- support COUNT in SSCAN (thanks @Abdi-dd) -- test and support Go 1.19 -- support LPOS (thanks @ianstarz) -- support XPENDING, XGROUP {CREATECONSUMER,DESTROY,DELCONSUMER}, XINFO {CONSUMERS,GROUPS}, XCLAIM (thanks @sandyharvie) - - -### v2.22.0 - -- set miniredis.DumpMaxLineLen to get more Dump() info (thanks @afjoseph) -- fix invalid resposne of COMMAND (thanks @zsh1995) -- fix possibility to generate duplicate IDs in XADD (thanks @readams) -- adds support for XAUTOCLAIM min-idle parameter (thanks @readams) - - -### v2.21.0 - -- support for GETEX (thanks @dntj) -- support for GT and LT in ZADD (thanks @lsgndln) -- support for XAUTOCLAIM (thanks @randall-fulton) - - -### v2.20.0 - -- back to support Go >= 1.14 (thanks @ajatprabha and @marcind) - - -### v2.19.0 - -- support for TYPE in SCAN (thanks @0xDiddi) -- update BITPOS (thanks @dirkm) -- fix a lua redis.call() return value (thanks @mpetronic) -- update ZRANGE (thanks @valdemarpereira) - - -### v2.18.0 - -- support for ZUNION (thanks @propan) -- support for COPY (thanks @matiasinsaurralde and @rockitbaby) -- support for LMOVE (thanks @btwear) - - -### v2.17.0 - -- added miniredis.RunT(t) - - -### v2.16.1 - -- fix ZINTERSTORE with sets (thanks @lingjl2010 and @okhowang) -- fix exclusive ranges in XRANGE (thanks @joseotoro) - - -### v2.16.0 - -- simplify some code (thanks @zonque) -- support for EXAT/PXAT in SET -- support for XTRIM (thanks @joseotoro) -- support for ZRANDMEMBER -- support for redis.log() in lua (thanks @dirkm) - - -### v2.15.2 - -- Fix race condition in blocking code (thanks @zonque and @robx) -- XREAD accepts '$' as ID (thanks @bradengroom) - - -### v2.15.1 - -- EVAL should cache the script (thanks @guoshimin) - - -### v2.15.0 - -- target redis 6.2 and added new args to various commands -- support for all hyperlog commands (thanks @ilbaktin) -- support for GETDEL (thanks @wszaranski) - - -### v2.14.5 - -- added XPENDING -- support for BLOCK option in XREAD and XREADGROUP - - -### v2.14.4 - -- fix BITPOS error (thanks @xiaoyuzdy) -- small fixes for XREAD, XACK, and XDEL. Mostly error cases. -- fix empty EXEC return type (thanks @ashanbrown) -- fix XDEL (thanks @svakili and @yvesf) -- fix FLUSHALL for streams (thanks @svakili) - - -### v2.14.3 - -- fix problem where Lua code didn't set the selected DB -- update to redis 6.0.10 (thanks @lazappa) - - -### v2.14.2 - -- update LUA dependency -- deal with (p)unsubscribe when there are no channels - - -### v2.14.1 - -- mod tidy - - -### v2.14.0 - -- support for HELLO and the RESP3 protocol -- KEEPTTL in SET (thanks @johnpena) - - -### v2.13.3 - -- support Go 1.14 and 1.15 -- update the `Check...()` methods -- support for XREAD (thanks @pieterlexis) - - -### v2.13.2 - -- Use SAN instead of CN in self signed cert for testing (thanks @johejo) -- Travis CI now tests against the most recent two versions of Go (thanks @johejo) -- changed unit and integration tests to compare raw payloads, not parsed payloads -- remove "redigo" dependency - - -### v2.13.1 - -- added HSTRLEN -- minimal support for ACL users in AUTH - - -### v2.13.0 - -- added RunTLS(...) -- added SetError(...) - - -### v2.12.0 - -- redis 6 -- Lua json update (thanks @gsmith85) -- CLUSTER commands (thanks @kratisto) -- fix TOUCH -- fix a shutdown race condition - - -### v2.11.4 - -- ZUNIONSTORE now supports standard set types (thanks @wshirey) - - -### v2.11.3 - -- support for TOUCH (thanks @cleroux) -- support for cluster and stream commands (thanks @kak-tus) - - -### v2.11.2 - -- make sure Lua code is executed concurrently -- add command GEORADIUSBYMEMBER (thanks @kyeett) - - -### v2.11.1 - -- globals protection for Lua code (thanks @vk-outreach) -- HSET update (thanks @carlgreen) -- fix BLPOP block on shutdown (thanks @Asalle) - - -### v2.11.0 - -- added XRANGE/XREVRANGE, XADD, and XLEN (thanks @skateinmars) -- added GEODIST -- improved precision for geohashes, closer to what real redis does -- use 128bit floats internally for INCRBYFLOAT and related (thanks @timnd) - - -### v2.10.1 - -- added m.Server() - - -### v2.10.0 - -- added UNLINK -- fix DEL zero-argument case -- cleanup some direct access commands -- added GEOADD, GEOPOS, GEORADIUS, and GEORADIUS_RO - - -### v2.9.1 - -- fix issue with ZRANGEBYLEX -- fix issue with BRPOPLPUSH and direct access - - -### v2.9.0 - -- proper versioned import of github.com/gomodule/redigo (thanks @yfei1) -- fix messages generated by PSUBSCRIBE -- optional internal seed (thanks @zikaeroh) - - -### v2.8.0 - -Proper `v2` in go.mod. - - -### older - -See https://github.com/alicebob/miniredis/releases for the full changelog diff --git a/vendor/github.com/alicebob/miniredis/v2/LICENSE b/vendor/github.com/alicebob/miniredis/v2/LICENSE deleted file mode 100644 index bb02657ca..000000000 --- a/vendor/github.com/alicebob/miniredis/v2/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2014 Harmen - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/vendor/github.com/alicebob/miniredis/v2/Makefile b/vendor/github.com/alicebob/miniredis/v2/Makefile deleted file mode 100644 index 2b5ec3eca..000000000 --- a/vendor/github.com/alicebob/miniredis/v2/Makefile +++ /dev/null @@ -1,33 +0,0 @@ -.PHONY: test -test: ### Run unit tests - go test ./... - -.PHONY: testrace -testrace: ### Run unit tests with race detector - go test -race ./... - -.PHONY: int -int: ### Run integration tests (doesn't download redis server) - ${MAKE} -C integration int - -.PHONY: ci -ci: ### Run full tests suite (including download and compilation of proper redis server) - ${MAKE} test - ${MAKE} -C integration redis_src/redis-server int - ${MAKE} testrace - -.PHONY: clean -clean: ### Clean integration test files and remove compiled redis from integration/redis_src - ${MAKE} -C integration clean - -.PHONY: help -help: -ifeq ($(UNAME), Linux) - @grep -P '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | \ - awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}' -else - @# this is not tested, but prepared in advance for you, Mac drivers - @awk -F ':.*###' '$$0 ~ FS {printf "%15s%s\n", $$1 ":", $$2}' \ - $(MAKEFILE_LIST) | grep -v '@awk' | sort -endif - diff --git a/vendor/github.com/alicebob/miniredis/v2/README.md b/vendor/github.com/alicebob/miniredis/v2/README.md deleted file mode 100644 index b2282ffb1..000000000 --- a/vendor/github.com/alicebob/miniredis/v2/README.md +++ /dev/null @@ -1,343 +0,0 @@ -# Miniredis - -Pure Go Redis test server, used in Go unittests. - - -## - -Sometimes you want to test code which uses Redis, without making it a full-blown -integration test. -Miniredis implements (parts of) the Redis server, to be used in unittests. It -enables a simple, cheap, in-memory, Redis replacement, with a real TCP interface. Think of it as the Redis version of `net/http/httptest`. - -It saves you from using mock code, and since the redis server lives in the -test process you can query for values directly, without going through the server -stack. - -There are no dependencies on external binaries, so you can easily integrate it in automated build processes. - -Be sure to import v2: -``` -import "github.com/alicebob/miniredis/v2" -``` - -## Commands - -Implemented commands: - - - Connection (complete) - - AUTH -- see RequireAuth() - - ECHO - - HELLO -- see RequireUserAuth() - - PING - - SELECT - - SWAPDB - - QUIT - - Key - - COPY - - DEL - - DUMP -- partly, only handles string keys - - EXISTS - - EXPIRE - - EXPIREAT - - EXPIRETIME - - KEYS - - MOVE - - PERSIST - - PEXPIRE - - PEXPIREAT - - PEXPIRETIME - - PTTL - - RANDOMKEY -- see m.Seed(...) - - RENAME - - RENAMENX - - RESTORE -- partly, only handles string keys - - SCAN - - TOUCH - - TTL - - TYPE - - UNLINK - - WAIT -- no-op - - Transactions (complete) - - DISCARD - - EXEC - - MULTI - - UNWATCH - - WATCH - - Server - - DBSIZE - - FLUSHALL - - FLUSHDB - - TIME -- returns time.Now() or value set by SetTime() - - COMMAND -- partly - - INFO -- partly, returns only "clients" section with one field "connected_clients" - - String keys (complete) - - APPEND - - BITCOUNT - - BITOP - - BITPOS - - DECR - - DECRBY - - GET - - GETBIT - - GETRANGE - - GETSET - - GETDEL - - GETEX - - INCR - - INCRBY - - INCRBYFLOAT - - MGET - - MSET - - MSETNX - - PSETEX - - SET - - SETBIT - - SETEX - - SETNX - - SETRANGE - - STRLEN - - Hash keys (complete) - - HDEL - - HEXISTS - - HGET - - HGETALL - - HINCRBY - - HINCRBYFLOAT - - HKEYS - - HLEN - - HMGET - - HMSET - - HRANDFIELD - - HSET - - HSETNX - - HSTRLEN - - HVALS - - HSCAN - - List keys (complete) - - BLPOP - - BRPOP - - BRPOPLPUSH - - LINDEX - - LINSERT - - LLEN - - LPOP - - LPUSH - - LPUSHX - - LRANGE - - LREM - - LSET - - LTRIM - - RPOP - - RPOPLPUSH - - RPUSH - - RPUSHX - - LMOVE - - BLMOVE - - Pub/Sub (complete) - - PSUBSCRIBE - - PUBLISH - - PUBSUB - - PUNSUBSCRIBE - - SUBSCRIBE - - UNSUBSCRIBE - - Set keys (complete) - - SADD - - SCARD - - SDIFF - - SDIFFSTORE - - SINTER - - SINTERSTORE - - SINTERCARD - - SISMEMBER - - SMEMBERS - - SMISMEMBER - - SMOVE - - SPOP -- see m.Seed(...) - - SRANDMEMBER -- see m.Seed(...) - - SREM - - SSCAN - - SUNION - - SUNIONSTORE - - Sorted Set keys (complete) - - ZADD - - ZCARD - - ZCOUNT - - ZINCRBY - - ZINTER - - ZINTERSTORE - - ZLEXCOUNT - - ZPOPMIN - - ZPOPMAX - - ZRANDMEMBER - - ZRANGE - - ZRANGEBYLEX - - ZRANGEBYSCORE - - ZRANK - - ZREM - - ZREMRANGEBYLEX - - ZREMRANGEBYRANK - - ZREMRANGEBYSCORE - - ZREVRANGE - - ZREVRANGEBYLEX - - ZREVRANGEBYSCORE - - ZREVRANK - - ZSCORE - - ZUNION - - ZUNIONSTORE - - ZSCAN - - Stream keys - - XACK - - XADD - - XAUTOCLAIM - - XCLAIM - - XDEL - - XGROUP CREATE - - XGROUP CREATECONSUMER - - XGROUP DESTROY - - XGROUP DELCONSUMER - - XINFO STREAM -- partly - - XINFO GROUPS - - XINFO CONSUMERS -- partly - - XLEN - - XRANGE - - XREAD - - XREADGROUP - - XREVRANGE - - XPENDING - - XTRIM - - Scripting - - EVAL - - EVALSHA - - SCRIPT LOAD - - SCRIPT EXISTS - - SCRIPT FLUSH - - GEO - - GEOADD - - GEODIST - - ~~GEOHASH~~ - - GEOPOS - - GEORADIUS - - GEORADIUS_RO - - GEORADIUSBYMEMBER - - GEORADIUSBYMEMBER_RO - - Cluster - - CLUSTER SLOTS - - CLUSTER KEYSLOT - - CLUSTER NODES - - CLUSTER SHARDS - - HyperLogLog (complete) - - PFADD - - PFCOUNT - - PFMERGE - - -## TTLs, key expiration, and time - -Since miniredis is intended to be used in unittests TTLs don't decrease -automatically. You can use `TTL()` to get the TTL (as a time.Duration) of a -key. It will return 0 when no TTL is set. - -`m.FastForward(d)` can be used to decrement all TTLs. All TTLs which become <= -0 will be removed. - -EXPIREAT and PEXPIREAT values will be -converted to a duration. For that you can either set m.SetTime(t) to use that -time as the base for the (P)EXPIREAT conversion, or don't call SetTime(), in -which case time.Now() will be used. - -SetTime() also sets the value returned by TIME, which defaults to time.Now(). -It is not updated by FastForward, only by SetTime. - -## Randomness and Seed() - -Miniredis will use `math/rand`'s global RNG for randomness unless a seed is -provided by calling `m.Seed(...)`. If a seed is provided, then miniredis will -use its own RNG based on that seed. - -Commands which use randomness are: RANDOMKEY, SPOP, and SRANDMEMBER. - -## Example - -``` Go - -import ( - ... - "github.com/alicebob/miniredis/v2" - ... -) - -func TestSomething(t *testing.T) { - s := miniredis.RunT(t) - - // Optionally set some keys your code expects: - s.Set("foo", "bar") - s.HSet("some", "other", "key") - - // Run your code and see if it behaves. - // An example using the redigo library from "github.com/gomodule/redigo/redis": - c, err := redis.Dial("tcp", s.Addr()) - _, err = c.Do("SET", "foo", "bar") - - // Optionally check values in redis... - if got, err := s.Get("foo"); err != nil || got != "bar" { - t.Error("'foo' has the wrong value") - } - // ... or use a helper for that: - s.CheckGet(t, "foo", "bar") - - // TTL and expiration: - s.Set("foo", "bar") - s.SetTTL("foo", 10*time.Second) - s.FastForward(11 * time.Second) - if s.Exists("foo") { - t.Fatal("'foo' should not have existed anymore") - } -} -``` - -## Not supported - -Commands which will probably not be implemented: - - - CLUSTER (all) - - ~~CLUSTER *~~ - - ~~READONLY~~ - - ~~READWRITE~~ - - Key - - ~~MIGRATE~~ - - ~~OBJECT~~ - - Scripting - - ~~FCALL / FCALL_RO *~~ - - ~~FUNCTION *~~ - - ~~SCRIPT DEBUG~~ - - ~~SCRIPT KILL~~ - - Server - - ~~BGSAVE~~ - - ~~BGWRITEAOF~~ - - ~~CLIENT *~~ - - ~~CONFIG *~~ - - ~~DEBUG *~~ - - ~~LASTSAVE~~ - - ~~MONITOR~~ - - ~~ROLE~~ - - ~~SAVE~~ - - ~~SHUTDOWN~~ - - ~~SLAVEOF~~ - - ~~SLOWLOG~~ - - ~~SYNC~~ - - -## &c. - -Integration tests are run against Redis 8.4.0. The [./integration](./integration/) subdir -compares miniredis against a real redis instance. - -The Redis 6 RESP3 protocol is supported. If there are problems, please open -an issue. - -If you want to test Redis Sentinel have a look at [minisentinel](https://github.com/Bose/minisentinel). - -A changelog is kept at [CHANGELOG.md](https://github.com/alicebob/miniredis/blob/master/CHANGELOG.md). - -[![Go Reference](https://pkg.go.dev/badge/github.com/alicebob/miniredis/v2.svg)](https://pkg.go.dev/github.com/alicebob/miniredis/v2) diff --git a/vendor/github.com/alicebob/miniredis/v2/check.go b/vendor/github.com/alicebob/miniredis/v2/check.go deleted file mode 100644 index acd0d5539..000000000 --- a/vendor/github.com/alicebob/miniredis/v2/check.go +++ /dev/null @@ -1,63 +0,0 @@ -package miniredis - -import ( - "reflect" - "sort" -) - -// T is implemented by Testing.T -type T interface { - Helper() - Errorf(string, ...interface{}) -} - -// CheckGet does not call Errorf() iff there is a string key with the -// expected value. Normal use case is `m.CheckGet(t, "username", "theking")`. -func (m *Miniredis) CheckGet(t T, key, expected string) { - t.Helper() - - found, err := m.Get(key) - if err != nil { - t.Errorf("GET error, key %#v: %v", key, err) - return - } - if found != expected { - t.Errorf("GET error, key %#v: Expected %#v, got %#v", key, expected, found) - return - } -} - -// CheckList does not call Errorf() iff there is a list key with the -// expected values. -// Normal use case is `m.CheckGet(t, "favorite_colors", "red", "green", "infrared")`. -func (m *Miniredis) CheckList(t T, key string, expected ...string) { - t.Helper() - - found, err := m.List(key) - if err != nil { - t.Errorf("List error, key %#v: %v", key, err) - return - } - if !reflect.DeepEqual(expected, found) { - t.Errorf("List error, key %#v: Expected %#v, got %#v", key, expected, found) - return - } -} - -// CheckSet does not call Errorf() iff there is a set key with the -// expected values. -// Normal use case is `m.CheckSet(t, "visited", "Rome", "Stockholm", "Dublin")`. -func (m *Miniredis) CheckSet(t T, key string, expected ...string) { - t.Helper() - - found, err := m.Members(key) - if err != nil { - t.Errorf("Set error, key %#v: %v", key, err) - return - } - sort.Strings(expected) - if !reflect.DeepEqual(expected, found) { - t.Errorf("Set error, key %#v: Expected %#v, got %#v", key, expected, found) - return - } -} diff --git a/vendor/github.com/alicebob/miniredis/v2/cmd_client.go b/vendor/github.com/alicebob/miniredis/v2/cmd_client.go deleted file mode 100644 index ca9fcd9a4..000000000 --- a/vendor/github.com/alicebob/miniredis/v2/cmd_client.go +++ /dev/null @@ -1,68 +0,0 @@ -package miniredis - -import ( - "fmt" - "strings" - - "github.com/alicebob/miniredis/v2/server" -) - -// commandsClient handles client operations. -func commandsClient(m *Miniredis) { - m.srv.Register("CLIENT", m.cmdClient) -} - -// CLIENT -func (m *Miniredis) cmdClient(c *server.Peer, cmd string, args []string) { - if len(args) == 0 { - setDirty(c) - c.WriteError("ERR wrong number of arguments for 'client' command") - return - } - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - switch cmd := strings.ToUpper(args[0]); cmd { - case "SETNAME": - m.cmdClientSetName(c, args[1:]) - case "GETNAME": - m.cmdClientGetName(c, args[1:]) - default: - setDirty(c) - c.WriteError(fmt.Sprintf("ERR unknown subcommand '%s'. Try CLIENT HELP.", cmd)) - } - }) -} - -// CLIENT SETNAME -func (m *Miniredis) cmdClientSetName(c *server.Peer, args []string) { - if len(args) != 1 { - setDirty(c) - c.WriteError("ERR wrong number of arguments for 'client setname' command") - return - } - - name := args[0] - if strings.ContainsAny(name, " \n") { - setDirty(c) - c.WriteError("ERR Client names cannot contain spaces, newlines or special characters.") - return - - } - c.ClientName = name - c.WriteOK() -} - -// CLIENT GETNAME -func (m *Miniredis) cmdClientGetName(c *server.Peer, args []string) { - if len(args) > 0 { - setDirty(c) - c.WriteError("ERR wrong number of arguments for 'client getname' command") - return - } - - if c.ClientName == "" { - c.WriteNull() - } else { - c.WriteBulk(c.ClientName) - } -} diff --git a/vendor/github.com/alicebob/miniredis/v2/cmd_cluster.go b/vendor/github.com/alicebob/miniredis/v2/cmd_cluster.go deleted file mode 100644 index 4f7c77f42..000000000 --- a/vendor/github.com/alicebob/miniredis/v2/cmd_cluster.go +++ /dev/null @@ -1,122 +0,0 @@ -// Commands from https://redis.io/commands#cluster - -package miniredis - -import ( - "fmt" - "strings" - - "github.com/alicebob/miniredis/v2/server" -) - -// commandsCluster handles some cluster operations. -func commandsCluster(m *Miniredis) { - m.srv.Register("CLUSTER", m.cmdCluster) -} - -func (m *Miniredis) cmdCluster(c *server.Peer, cmd string, args []string) { - if !m.handleAuth(c) { - return - } - - if len(args) < 1 { - setDirty(c) - c.WriteError(errWrongNumber(cmd)) - return - } - switch strings.ToUpper(args[0]) { - case "SLOTS": - m.cmdClusterSlots(c, cmd, args) - case "KEYSLOT": - m.cmdClusterKeySlot(c, cmd, args) - case "NODES": - m.cmdClusterNodes(c, cmd, args) - case "SHARDS": - m.cmdClusterShards(c, cmd, args) - default: - setDirty(c) - c.WriteError(fmt.Sprintf("ERR 'CLUSTER %s' not supported", strings.Join(args, " "))) - return - } -} - -// CLUSTER SLOTS -func (m *Miniredis) cmdClusterSlots(c *server.Peer, cmd string, args []string) { - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - c.WriteLen(1) - c.WriteLen(3) - c.WriteInt(0) - c.WriteInt(16383) - c.WriteLen(3) - c.WriteBulk(m.srv.Addr().IP.String()) - c.WriteInt(m.srv.Addr().Port) - c.WriteBulk("09dbe9720cda62f7865eabc5fd8857c5d2678366") - }) -} - -// CLUSTER KEYSLOT -func (m *Miniredis) cmdClusterKeySlot(c *server.Peer, cmd string, args []string) { - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - c.WriteInt(163) - }) -} - -// CLUSTER NODES -func (m *Miniredis) cmdClusterNodes(c *server.Peer, cmd string, args []string) { - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - // do not try to use m.Addr() here, as m is blocked by this tx. - addr := m.srv.Addr() - port := m.srv.Addr().Port - c.WriteBulk(fmt.Sprintf("e7d1eecce10fd6bb5eb35b9f99a514335d9ba9ca %s@%d myself,master - 0 0 1 connected 0-16383", addr, port)) - }) -} - -// CLUSTER SHARDS -func (m *Miniredis) cmdClusterShards(c *server.Peer, cmd string, args []string) { - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - addr := m.srv.Addr() - host := addr.IP.String() - port := addr.Port - - // Array of shards (we return 1 shard) - c.WriteLen(1) - - // Shard is a map with 2 keys: "slots" and "nodes" - c.WriteMapLen(2) - - // "slots": flat list of start/end pairs (inclusive ranges) - c.WriteBulk("slots") - c.WriteLen(2) - c.WriteInt(0) - c.WriteInt(16383) - - // "nodes": array of node maps - c.WriteBulk("nodes") - c.WriteLen(1) - - // Node map. - // (id, endpoint, ip, port, role, replication-offset, health) - c.WriteMapLen(6) - - c.WriteBulk("id") - c.WriteBulk("13f84e686106847b76671957dd348fde540a77bb") - - //c.WriteBulk("endpoint") - //c.WriteBulk(host) // or host:port if your client expects that - - c.WriteBulk("ip") - c.WriteBulk(host) - - c.WriteBulk("port") - c.WriteInt(port) - - c.WriteBulk("role") - c.WriteBulk("master") - - c.WriteBulk("replication-offset") - c.WriteInt(0) - - c.WriteBulk("health") - c.WriteBulk("online") - }) -} diff --git a/vendor/github.com/alicebob/miniredis/v2/cmd_command.go b/vendor/github.com/alicebob/miniredis/v2/cmd_command.go deleted file mode 100644 index 8f73b2bac..000000000 --- a/vendor/github.com/alicebob/miniredis/v2/cmd_command.go +++ /dev/null @@ -1,14 +0,0 @@ -// Command 'COMMAND' from https://redis.io/commands#server - -package miniredis - -import "github.com/alicebob/miniredis/v2/server" - -func (m *Miniredis) cmdCommand(c *server.Peer, cmd string, args []string) { - // Got from redis 5.0.7 with - // echo 'COMMAND' | nc redis_addr redis_port - - res := "*200\r\n*6\r\n$12\r\nhincrbyfloat\r\n:4\r\n*3\r\n+write\r\n+denyoom\r\n+fast\r\n:1\r\n:1\r\n:1\r\n*6\r\n$10\r\nxreadgroup\r\n:-7\r\n*3\r\n+write\r\n+noscript\r\n+movablekeys\r\n:1\r\n:1\r\n:1\r\n*6\r\n$10\r\nsdiffstore\r\n:-3\r\n*2\r\n+write\r\n+denyoom\r\n:1\r\n:-1\r\n:1\r\n*6\r\n$8\r\nlastsave\r\n:1\r\n*2\r\n+random\r\n+fast\r\n:0\r\n:0\r\n:0\r\n*6\r\n$5\r\nsetnx\r\n:3\r\n*3\r\n+write\r\n+denyoom\r\n+fast\r\n:1\r\n:1\r\n:1\r\n*6\r\n$8\r\nbzpopmax\r\n:-3\r\n*3\r\n+write\r\n+noscript\r\n+fast\r\n:1\r\n:-2\r\n:1\r\n*6\r\n$12\r\npunsubscribe\r\n:-1\r\n*4\r\n+pubsub\r\n+noscript\r\n+loading\r\n+stale\r\n:0\r\n:0\r\n:0\r\n*6\r\n$4\r\nxack\r\n:-4\r\n*2\r\n+write\r\n+fast\r\n:1\r\n:1\r\n:1\r\n*6\r\n$10\r\npfselftest\r\n:1\r\n*1\r\n+admin\r\n:0\r\n:0\r\n:0\r\n*6\r\n$6\r\nsubstr\r\n:4\r\n*1\r\n+readonly\r\n:1\r\n:1\r\n:1\r\n*6\r\n$8\r\nsmembers\r\n:2\r\n*2\r\n+readonly\r\n+sort_for_script\r\n:1\r\n:1\r\n:1\r\n*6\r\n$11\r\nunsubscribe\r\n:-1\r\n*4\r\n+pubsub\r\n+noscript\r\n+loading\r\n+stale\r\n:0\r\n:0\r\n:0\r\n*6\r\n$11\r\nzinterstore\r\n:-4\r\n*3\r\n+write\r\n+denyoom\r\n+movablekeys\r\n:0\r\n:0\r\n:0\r\n*6\r\n$6\r\nstrlen\r\n:2\r\n*2\r\n+readonly\r\n+fast\r\n:1\r\n:1\r\n:1\r\n*6\r\n$7\r\npfmerge\r\n:-2\r\n*2\r\n+write\r\n+denyoom\r\n:1\r\n:-1\r\n:1\r\n*6\r\n$9\r\nrandomkey\r\n:1\r\n*2\r\n+readonly\r\n+random\r\n:0\r\n:0\r\n:0\r\n*6\r\n$6\r\nlolwut\r\n:-1\r\n*1\r\n+readonly\r\n:0\r\n:0\r\n:0\r\n*6\r\n$4\r\nrpop\r\n:2\r\n*2\r\n+write\r\n+fast\r\n:1\r\n:1\r\n:1\r\n*6\r\n$5\r\nhkeys\r\n:2\r\n*2\r\n+readonly\r\n+sort_for_script\r\n:1\r\n:1\r\n:1\r\n*6\r\n$6\r\nclient\r\n:-2\r\n*2\r\n+admin\r\n+noscript\r\n:0\r\n:0\r\n:0\r\n*6\r\n$6\r\nmodule\r\n:-2\r\n*2\r\n+admin\r\n+noscript\r\n:0\r\n:0\r\n:0\r\n*6\r\n$7\r\nslowlog\r\n:-2\r\n*2\r\n+admin\r\n+random\r\n:0\r\n:0\r\n:0\r\n*6\r\n$7\r\ngeohash\r\n:-2\r\n*1\r\n+readonly\r\n:1\r\n:1\r\n:1\r\n*6\r\n$6\r\nlrange\r\n:4\r\n*1\r\n+readonly\r\n:1\r\n:1\r\n:1\r\n*6\r\n$4\r\nping\r\n:-1\r\n*2\r\n+stale\r\n+fast\r\n:0\r\n:0\r\n:0\r\n*6\r\n$8\r\nbitcount\r\n:-2\r\n*1\r\n+readonly\r\n:1\r\n:1\r\n:1\r\n*6\r\n$6\r\npubsub\r\n:-2\r\n*4\r\n+pubsub\r\n+random\r\n+loading\r\n+stale\r\n:0\r\n:0\r\n:0\r\n*6\r\n$4\r\nrole\r\n:1\r\n*3\r\n+noscript\r\n+loading\r\n+stale\r\n:0\r\n:0\r\n:0\r\n*6\r\n$4\r\nhget\r\n:3\r\n*2\r\n+readonly\r\n+fast\r\n:1\r\n:1\r\n:1\r\n*6\r\n$6\r\nobject\r\n:-2\r\n*2\r\n+readonly\r\n+random\r\n:2\r\n:2\r\n:1\r\n*6\r\n$9\r\nzrevrange\r\n:-4\r\n*1\r\n+readonly\r\n:1\r\n:1\r\n:1\r\n*6\r\n$7\r\nhincrby\r\n:4\r\n*3\r\n+write\r\n+denyoom\r\n+fast\r\n:1\r\n:1\r\n:1\r\n*6\r\n$9\r\nzlexcount\r\n:4\r\n*2\r\n+readonly\r\n+fast\r\n:1\r\n:1\r\n:1\r\n*6\r\n$5\r\nscard\r\n:2\r\n*2\r\n+readonly\r\n+fast\r\n:1\r\n:1\r\n:1\r\n*6\r\n$6\r\nappend\r\n:3\r\n*2\r\n+write\r\n+denyoom\r\n:1\r\n:1\r\n:1\r\n*6\r\n$7\r\nhstrlen\r\n:3\r\n*2\r\n+readonly\r\n+fast\r\n:1\r\n:1\r\n:1\r\n*6\r\n$6\r\nconfig\r\n:-2\r\n*4\r\n+admin\r\n+noscript\r\n+loading\r\n+stale\r\n:0\r\n:0\r\n:0\r\n*6\r\n$4\r\nhset\r\n:-4\r\n*3\r\n+write\r\n+denyoom\r\n+fast\r\n:1\r\n:1\r\n:1\r\n*6\r\n$16\r\nzrevrangebyscore\r\n:-4\r\n*1\r\n+readonly\r\n:1\r\n:1\r\n:1\r\n*6\r\n$4\r\nincr\r\n:2\r\n*3\r\n+write\r\n+denyoom\r\n+fast\r\n:1\r\n:1\r\n:1\r\n*6\r\n$6\r\nsetbit\r\n:4\r\n*2\r\n+write\r\n+denyoom\r\n:1\r\n:1\r\n:1\r\n*6\r\n$9\r\nrpoplpush\r\n:3\r\n*2\r\n+write\r\n+denyoom\r\n:1\r\n:2\r\n:1\r\n*6\r\n$6\r\nxclaim\r\n:-6\r\n*3\r\n+write\r\n+random\r\n+fast\r\n:1\r\n:1\r\n:1\r\n*6\r\n$11\r\nsinterstore\r\n:-3\r\n*2\r\n+write\r\n+denyoom\r\n:1\r\n:-1\r\n:1\r\n*6\r\n$7\r\npublish\r\n:3\r\n*4\r\n+pubsub\r\n+loading\r\n+stale\r\n+fast\r\n:0\r\n:0\r\n:0\r\n*6\r\n$5\r\nhscan\r\n:-3\r\n*2\r\n+readonly\r\n+random\r\n:1\r\n:1\r\n:1\r\n*6\r\n$5\r\nmulti\r\n:1\r\n*2\r\n+noscript\r\n+fast\r\n:0\r\n:0\r\n:0\r\n*6\r\n$3\r\nset\r\n:-3\r\n*2\r\n+write\r\n+denyoom\r\n:1\r\n:1\r\n:1\r\n*6\r\n$6\r\nlpushx\r\n:-3\r\n*3\r\n+write\r\n+denyoom\r\n+fast\r\n:1\r\n:1\r\n:1\r\n*6\r\n$16\r\nzremrangebyscore\r\n:4\r\n*1\r\n+write\r\n:1\r\n:1\r\n:1\r\n*6\r\n$9\r\npexpireat\r\n:3\r\n*2\r\n+write\r\n+fast\r\n:1\r\n:1\r\n:1\r\n*6\r\n$4\r\nhdel\r\n:-3\r\n*2\r\n+write\r\n+fast\r\n:1\r\n:1\r\n:1\r\n*6\r\n$12\r\nbgrewriteaof\r\n:1\r\n*2\r\n+admin\r\n+noscript\r\n:0\r\n:0\r\n:0\r\n*6\r\n$7\r\nmigrate\r\n:-6\r\n*3\r\n+write\r\n+random\r\n+movablekeys\r\n:0\r\n:0\r\n:0\r\n*6\r\n$9\r\nreplicaof\r\n:3\r\n*3\r\n+admin\r\n+noscript\r\n+stale\r\n:0\r\n:0\r\n:0\r\n*6\r\n$5\r\ntouch\r\n:-2\r\n*2\r\n+readonly\r\n+fast\r\n:1\r\n:1\r\n:1\r\n*6\r\n$6\r\nxsetid\r\n:3\r\n*3\r\n+write\r\n+denyoom\r\n+fast\r\n:1\r\n:1\r\n:1\r\n*6\r\n$5\r\nbitop\r\n:-4\r\n*2\r\n+write\r\n+denyoom\r\n:2\r\n:-1\r\n:1\r\n*6\r\n$6\r\nswapdb\r\n:3\r\n*2\r\n+write\r\n+fast\r\n:0\r\n:0\r\n:0\r\n*6\r\n$5\r\nsdiff\r\n:-2\r\n*2\r\n+readonly\r\n+sort_for_script\r\n:1\r\n:-1\r\n:1\r\n*6\r\n$6\r\nlindex\r\n:3\r\n*1\r\n+readonly\r\n:1\r\n:1\r\n:1\r\n*6\r\n$4\r\nwait\r\n:3\r\n*1\r\n+noscript\r\n:0\r\n:0\r\n:0\r\n*6\r\n$4\r\nlrem\r\n:4\r\n*1\r\n+write\r\n:1\r\n:1\r\n:1\r\n*6\r\n$6\r\nhsetnx\r\n:4\r\n*3\r\n+write\r\n+denyoom\r\n+fast\r\n:1\r\n:1\r\n:1\r\n*6\r\n$8\r\ngetrange\r\n:4\r\n*1\r\n+readonly\r\n:1\r\n:1\r\n:1\r\n*6\r\n$4\r\nhlen\r\n:2\r\n*2\r\n+readonly\r\n+fast\r\n:1\r\n:1\r\n:1\r\n*6\r\n$4\r\npost\r\n:-1\r\n*2\r\n+loading\r\n+stale\r\n:0\r\n:0\r\n:0\r\n*6\r\n$9\r\nsismember\r\n:3\r\n*2\r\n+readonly\r\n+fast\r\n:1\r\n:1\r\n:1\r\n*6\r\n$7\r\nunwatch\r\n:1\r\n*2\r\n+noscript\r\n+fast\r\n:0\r\n:0\r\n:0\r\n*6\r\n$5\r\nlpush\r\n:-3\r\n*3\r\n+write\r\n+denyoom\r\n+fast\r\n:1\r\n:1\r\n:1\r\n*6\r\n$4\r\nscan\r\n:-2\r\n*2\r\n+readonly\r\n+random\r\n:0\r\n:0\r\n:0\r\n*6\r\n$5\r\nsmove\r\n:4\r\n*2\r\n+write\r\n+fast\r\n:1\r\n:2\r\n:1\r\n*6\r\n$7\r\ncluster\r\n:-2\r\n*1\r\n+admin\r\n:0\r\n:0\r\n:0\r\n*6\r\n$6\r\nbgsave\r\n:-1\r\n*2\r\n+admin\r\n+noscript\r\n:0\r\n:0\r\n:0\r\n*6\r\n$4\r\ndump\r\n:2\r\n*2\r\n+readonly\r\n+random\r\n:1\r\n:1\r\n:1\r\n*6\r\n$7\r\nlatency\r\n:-2\r\n*4\r\n+admin\r\n+noscript\r\n+loading\r\n+stale\r\n:0\r\n:0\r\n:0\r\n*6\r\n$8\r\nbzpopmin\r\n:-3\r\n*3\r\n+write\r\n+noscript\r\n+fast\r\n:1\r\n:-2\r\n:1\r\n*6\r\n$6\r\ngetbit\r\n:3\r\n*2\r\n+readonly\r\n+fast\r\n:1\r\n:1\r\n:1\r\n*6\r\n$7\r\nhgetall\r\n:2\r\n*2\r\n+readonly\r\n+random\r\n:1\r\n:1\r\n:1\r\n*6\r\n$6\r\nrename\r\n:3\r\n*1\r\n+write\r\n:1\r\n:2\r\n:1\r\n*6\r\n$9\r\nsubscribe\r\n:-2\r\n*4\r\n+pubsub\r\n+noscript\r\n+loading\r\n+stale\r\n:0\r\n:0\r\n:0\r\n*6\r\n$4\r\nxdel\r\n:-3\r\n*2\r\n+write\r\n+fast\r\n:1\r\n:1\r\n:1\r\n*6\r\n$15\r\nzremrangebyrank\r\n:4\r\n*1\r\n+write\r\n:1\r\n:1\r\n:1\r\n*6\r\n$4\r\ntype\r\n:2\r\n*2\r\n+readonly\r\n+fast\r\n:1\r\n:1\r\n:1\r\n*6\r\n$6\r\nscript\r\n:-2\r\n*1\r\n+noscript\r\n:0\r\n:0\r\n:0\r\n*6\r\n$5\r\nhmset\r\n:-4\r\n*3\r\n+write\r\n+denyoom\r\n+fast\r\n:1\r\n:1\r\n:1\r\n*6\r\n$6\r\nsunion\r\n:-2\r\n*2\r\n+readonly\r\n+sort_for_script\r\n:1\r\n:-1\r\n:1\r\n*6\r\n$4\r\nmget\r\n:-2\r\n*2\r\n+readonly\r\n+fast\r\n:1\r\n:-1\r\n:1\r\n*6\r\n$10\r\nbrpoplpush\r\n:4\r\n*3\r\n+write\r\n+denyoom\r\n+noscript\r\n:1\r\n:2\r\n:1\r\n*6\r\n$6\r\ngeoadd\r\n:-5\r\n*2\r\n+write\r\n+denyoom\r\n:1\r\n:1\r\n:1\r\n*6\r\n$6\r\ndecrby\r\n:3\r\n*3\r\n+write\r\n+denyoom\r\n+fast\r\n:1\r\n:1\r\n:1\r\n*6\r\n$4\r\necho\r\n:2\r\n*1\r\n+fast\r\n:0\r\n:0\r\n:0\r\n*6\r\n$6\r\ndbsize\r\n:1\r\n*2\r\n+readonly\r\n+fast\r\n:0\r\n:0\r\n:0\r\n*6\r\n$5\r\nzcard\r\n:2\r\n*2\r\n+readonly\r\n+fast\r\n:1\r\n:1\r\n:1\r\n*6\r\n$6\r\nselect\r\n:2\r\n*2\r\n+loading\r\n+fast\r\n:0\r\n:0\r\n:0\r\n*6\r\n$4\r\nsadd\r\n:-3\r\n*3\r\n+write\r\n+denyoom\r\n+fast\r\n:1\r\n:1\r\n:1\r\n*6\r\n$5\r\nhost:\r\n:-1\r\n*2\r\n+loading\r\n+stale\r\n:0\r\n:0\r\n:0\r\n*6\r\n$5\r\nsscan\r\n:-3\r\n*2\r\n+readonly\r\n+random\r\n:1\r\n:1\r\n:1\r\n*6\r\n$12\r\ngeoradius_ro\r\n:-6\r\n*2\r\n+readonly\r\n+movablekeys\r\n:1\r\n:1\r\n:1\r\n*6\r\n$7\r\nmonitor\r\n:1\r\n*2\r\n+admin\r\n+noscript\r\n:0\r\n:0\r\n:0\r\n*6\r\n$14\r\nzremrangebylex\r\n:4\r\n*1\r\n+write\r\n:1\r\n:1\r\n:1\r\n*6\r\n$11\r\nsunionstore\r\n:-3\r\n*2\r\n+write\r\n+denyoom\r\n:1\r\n:-1\r\n:1\r\n*6\r\n$5\r\nzscan\r\n:-3\r\n*2\r\n+readonly\r\n+random\r\n:1\r\n:1\r\n:1\r\n*6\r\n$9\r\nreadwrite\r\n:1\r\n*1\r\n+fast\r\n:0\r\n:0\r\n:0\r\n*6\r\n$6\r\nxgroup\r\n:-2\r\n*2\r\n+write\r\n+denyoom\r\n:2\r\n:2\r\n:1\r\n*6\r\n$5\r\nsetex\r\n:4\r\n*2\r\n+write\r\n+denyoom\r\n:1\r\n:1\r\n:1\r\n*6\r\n$4\r\nsave\r\n:1\r\n*2\r\n+admin\r\n+noscript\r\n:0\r\n:0\r\n:0\r\n*6\r\n$5\r\nhvals\r\n:2\r\n*2\r\n+readonly\r\n+sort_for_script\r\n:1\r\n:1\r\n:1\r\n*6\r\n$5\r\nwatch\r\n:-2\r\n*2\r\n+noscript\r\n+fast\r\n:1\r\n:-1\r\n:1\r\n*6\r\n$7\r\nhexists\r\n:3\r\n*2\r\n+readonly\r\n+fast\r\n:1\r\n:1\r\n:1\r\n*6\r\n$4\r\ninfo\r\n:-1\r\n*3\r\n+random\r\n+loading\r\n+stale\r\n:0\r\n:0\r\n:0\r\n*6\r\n$5\r\npsync\r\n:3\r\n*3\r\n+readonly\r\n+admin\r\n+noscript\r\n:0\r\n:0\r\n:0\r\n*6\r\n$11\r\nzrangebylex\r\n:-4\r\n*1\r\n+readonly\r\n:1\r\n:1\r\n:1\r\n*6\r\n$4\r\nzadd\r\n:-4\r\n*3\r\n+write\r\n+denyoom\r\n+fast\r\n:1\r\n:1\r\n:1\r\n*6\r\n$4\r\nxlen\r\n:2\r\n*2\r\n+readonly\r\n+fast\r\n:1\r\n:1\r\n:1\r\n*6\r\n$4\r\nauth\r\n:2\r\n*4\r\n+noscript\r\n+loading\r\n+stale\r\n+fast\r\n:0\r\n:0\r\n:0\r\n*6\r\n$4\r\nsrem\r\n:-3\r\n*2\r\n+write\r\n+fast\r\n:1\r\n:1\r\n:1\r\n*6\r\n$9\r\ngeoradius\r\n:-6\r\n*2\r\n+write\r\n+movablekeys\r\n:1\r\n:1\r\n:1\r\n*6\r\n$4\r\nexec\r\n:1\r\n*2\r\n+noscript\r\n+skip_monitor\r\n:0\r\n:0\r\n:0\r\n*6\r\n$7\r\npfcount\r\n:-2\r\n*1\r\n+readonly\r\n:1\r\n:-1\r\n:1\r\n*6\r\n$7\r\nzpopmin\r\n:-2\r\n*2\r\n+write\r\n+fast\r\n:1\r\n:1\r\n:1\r\n*6\r\n$4\r\nmove\r\n:3\r\n*2\r\n+write\r\n+fast\r\n:1\r\n:1\r\n:1\r\n*6\r\n$5\r\nxtrim\r\n:-2\r\n*3\r\n+write\r\n+random\r\n+fast\r\n:1\r\n:1\r\n:1\r\n*6\r\n$6\r\nasking\r\n:1\r\n*1\r\n+fast\r\n:0\r\n:0\r\n:0\r\n*6\r\n$4\r\npttl\r\n:2\r\n*3\r\n+readonly\r\n+random\r\n+fast\r\n:1\r\n:1\r\n:1\r\n*6\r\n$11\r\nsrandmember\r\n:-2\r\n*2\r\n+readonly\r\n+random\r\n:1\r\n:1\r\n:1\r\n*6\r\n$8\r\nflushall\r\n:-1\r\n*1\r\n+write\r\n:0\r\n:0\r\n:0\r\n*6\r\n$4\r\nsort\r\n:-2\r\n*3\r\n+write\r\n+denyoom\r\n+movablekeys\r\n:1\r\n:1\r\n:1\r\n*6\r\n$3\r\ndel\r\n:-2\r\n*1\r\n+write\r\n:1\r\n:-1\r\n:1\r\n*6\r\n$14\r\nrestore-asking\r\n:-4\r\n*3\r\n+write\r\n+denyoom\r\n+asking\r\n:1\r\n:1\r\n:1\r\n*6\r\n$10\r\npsubscribe\r\n:-2\r\n*4\r\n+pubsub\r\n+noscript\r\n+loading\r\n+stale\r\n:0\r\n:0\r\n:0\r\n*6\r\n$4\r\ndecr\r\n:2\r\n*3\r\n+write\r\n+denyoom\r\n+fast\r\n:1\r\n:1\r\n:1\r\n*6\r\n$6\r\nincrby\r\n:3\r\n*3\r\n+write\r\n+denyoom\r\n+fast\r\n:1\r\n:1\r\n:1\r\n*6\r\n$14\r\nzrevrangebylex\r\n:-4\r\n*1\r\n+readonly\r\n:1\r\n:1\r\n:1\r\n*6\r\n$8\r\nbitfield\r\n:-2\r\n*2\r\n+write\r\n+denyoom\r\n:1\r\n:1\r\n:1\r\n*6\r\n$6\r\nexists\r\n:-2\r\n*2\r\n+readonly\r\n+fast\r\n:1\r\n:-1\r\n:1\r\n*6\r\n$8\r\nreplconf\r\n:-1\r\n*4\r\n+admin\r\n+noscript\r\n+loading\r\n+stale\r\n:0\r\n:0\r\n:0\r\n*6\r\n$7\r\nzincrby\r\n:4\r\n*3\r\n+write\r\n+denyoom\r\n+fast\r\n:1\r\n:1\r\n:1\r\n*6\r\n$5\r\nblpop\r\n:-3\r\n*2\r\n+write\r\n+noscript\r\n:1\r\n:-2\r\n:1\r\n*6\r\n$4\r\nlpop\r\n:2\r\n*2\r\n+write\r\n+fast\r\n:1\r\n:1\r\n:1\r\n*6\r\n$3\r\nttl\r\n:2\r\n*3\r\n+readonly\r\n+random\r\n+fast\r\n:1\r\n:1\r\n:1\r\n*6\r\n$5\r\nxread\r\n:-4\r\n*3\r\n+readonly\r\n+noscript\r\n+movablekeys\r\n:1\r\n:1\r\n:1\r\n*6\r\n$5\r\nrpush\r\n:-3\r\n*3\r\n+write\r\n+denyoom\r\n+fast\r\n:1\r\n:1\r\n:1\r\n*6\r\n$8\r\nzrevrank\r\n:3\r\n*2\r\n+readonly\r\n+fast\r\n:1\r\n:1\r\n:1\r\n*6\r\n$11\r\nincrbyfloat\r\n:3\r\n*3\r\n+write\r\n+denyoom\r\n+fast\r\n:1\r\n:1\r\n:1\r\n*6\r\n$5\r\nbrpop\r\n:-3\r\n*2\r\n+write\r\n+noscript\r\n:1\r\n:-2\r\n:1\r\n*6\r\n$4\r\nxadd\r\n:-5\r\n*4\r\n+write\r\n+denyoom\r\n+random\r\n+fast\r\n:1\r\n:1\r\n:1\r\n*6\r\n$8\r\nsetrange\r\n:4\r\n*2\r\n+write\r\n+denyoom\r\n:1\r\n:1\r\n:1\r\n*6\r\n$17\r\ngeoradiusbymember\r\n:-5\r\n*2\r\n+write\r\n+movablekeys\r\n:1\r\n:1\r\n:1\r\n*6\r\n$6\r\nunlink\r\n:-2\r\n*2\r\n+write\r\n+fast\r\n:1\r\n:-1\r\n:1\r\n*6\r\n$8\r\nexpireat\r\n:3\r\n*2\r\n+write\r\n+fast\r\n:1\r\n:1\r\n:1\r\n*6\r\n$5\r\ndebug\r\n:-2\r\n*2\r\n+admin\r\n+noscript\r\n:0\r\n:0\r\n:0\r\n*6\r\n$20\r\ngeoradiusbymember_ro\r\n:-5\r\n*2\r\n+readonly\r\n+movablekeys\r\n:1\r\n:1\r\n:1\r\n*6\r\n$4\r\nlset\r\n:4\r\n*2\r\n+write\r\n+denyoom\r\n:1\r\n:1\r\n:1\r\n*6\r\n$6\r\nzscore\r\n:3\r\n*2\r\n+readonly\r\n+fast\r\n:1\r\n:1\r\n:1\r\n*6\r\n$4\r\nllen\r\n:2\r\n*2\r\n+readonly\r\n+fast\r\n:1\r\n:1\r\n:1\r\n*6\r\n$4\r\ntime\r\n:1\r\n*2\r\n+random\r\n+fast\r\n:0\r\n:0\r\n:0\r\n*6\r\n$8\r\nshutdown\r\n:-1\r\n*4\r\n+admin\r\n+noscript\r\n+loading\r\n+stale\r\n:0\r\n:0\r\n:0\r\n*6\r\n$7\r\nevalsha\r\n:-3\r\n*2\r\n+noscript\r\n+movablekeys\r\n:0\r\n:0\r\n:0\r\n*6\r\n$6\r\nzcount\r\n:4\r\n*2\r\n+readonly\r\n+fast\r\n:1\r\n:1\r\n:1\r\n*6\r\n$6\r\nmemory\r\n:-2\r\n*2\r\n+readonly\r\n+random\r\n:0\r\n:0\r\n:0\r\n*6\r\n$5\r\nxinfo\r\n:-2\r\n*2\r\n+readonly\r\n+random\r\n:2\r\n:2\r\n:1\r\n*6\r\n$8\r\nxpending\r\n:-3\r\n*2\r\n+readonly\r\n+random\r\n:1\r\n:1\r\n:1\r\n*6\r\n$4\r\neval\r\n:-3\r\n*2\r\n+noscript\r\n+movablekeys\r\n:0\r\n:0\r\n:0\r\n*6\r\n$6\r\nxrange\r\n:-4\r\n*1\r\n+readonly\r\n:1\r\n:1\r\n:1\r\n*6\r\n$7\r\nrestore\r\n:-4\r\n*2\r\n+write\r\n+denyoom\r\n:1\r\n:1\r\n:1\r\n*6\r\n$7\r\nzpopmax\r\n:-2\r\n*2\r\n+write\r\n+fast\r\n:1\r\n:1\r\n:1\r\n*6\r\n$4\r\nmset\r\n:-3\r\n*2\r\n+write\r\n+denyoom\r\n:1\r\n:-1\r\n:2\r\n*6\r\n$4\r\nspop\r\n:-2\r\n*3\r\n+write\r\n+random\r\n+fast\r\n:1\r\n:1\r\n:1\r\n*6\r\n$5\r\nltrim\r\n:4\r\n*1\r\n+write\r\n:1\r\n:1\r\n:1\r\n*6\r\n$5\r\nzrank\r\n:3\r\n*2\r\n+readonly\r\n+fast\r\n:1\r\n:1\r\n:1\r\n*6\r\n$9\r\nxrevrange\r\n:-4\r\n*1\r\n+readonly\r\n:1\r\n:1\r\n:1\r\n*6\r\n$3\r\nget\r\n:2\r\n*2\r\n+readonly\r\n+fast\r\n:1\r\n:1\r\n:1\r\n*6\r\n$7\r\nflushdb\r\n:-1\r\n*1\r\n+write\r\n:0\r\n:0\r\n:0\r\n*6\r\n$5\r\nhmget\r\n:-3\r\n*2\r\n+readonly\r\n+fast\r\n:1\r\n:1\r\n:1\r\n*6\r\n$6\r\nmsetnx\r\n:-3\r\n*2\r\n+write\r\n+denyoom\r\n:1\r\n:-1\r\n:2\r\n*6\r\n$7\r\npersist\r\n:2\r\n*2\r\n+write\r\n+fast\r\n:1\r\n:1\r\n:1\r\n*6\r\n$11\r\nzunionstore\r\n:-4\r\n*3\r\n+write\r\n+denyoom\r\n+movablekeys\r\n:0\r\n:0\r\n:0\r\n*6\r\n$7\r\ncommand\r\n:0\r\n*3\r\n+random\r\n+loading\r\n+stale\r\n:0\r\n:0\r\n:0\r\n*6\r\n$8\r\nrenamenx\r\n:3\r\n*2\r\n+write\r\n+fast\r\n:1\r\n:2\r\n:1\r\n*6\r\n$6\r\nzrange\r\n:-4\r\n*1\r\n+readonly\r\n:1\r\n:1\r\n:1\r\n*6\r\n$7\r\npexpire\r\n:3\r\n*2\r\n+write\r\n+fast\r\n:1\r\n:1\r\n:1\r\n*6\r\n$4\r\nkeys\r\n:2\r\n*2\r\n+readonly\r\n+sort_for_script\r\n:0\r\n:0\r\n:0\r\n*6\r\n$4\r\nzrem\r\n:-3\r\n*2\r\n+write\r\n+fast\r\n:1\r\n:1\r\n:1\r\n*6\r\n$5\r\npfadd\r\n:-2\r\n*3\r\n+write\r\n+denyoom\r\n+fast\r\n:1\r\n:1\r\n:1\r\n*6\r\n$6\r\npsetex\r\n:4\r\n*2\r\n+write\r\n+denyoom\r\n:1\r\n:1\r\n:1\r\n*6\r\n$13\r\nzrangebyscore\r\n:-4\r\n*1\r\n+readonly\r\n:1\r\n:1\r\n:1\r\n*6\r\n$4\r\nsync\r\n:1\r\n*3\r\n+readonly\r\n+admin\r\n+noscript\r\n:0\r\n:0\r\n:0\r\n*6\r\n$7\r\npfdebug\r\n:-3\r\n*1\r\n+write\r\n:0\r\n:0\r\n:0\r\n*6\r\n$7\r\ndiscard\r\n:1\r\n*2\r\n+noscript\r\n+fast\r\n:0\r\n:0\r\n:0\r\n*6\r\n$8\r\nreadonly\r\n:1\r\n*1\r\n+fast\r\n:0\r\n:0\r\n:0\r\n*6\r\n$7\r\ngeodist\r\n:-4\r\n*1\r\n+readonly\r\n:1\r\n:1\r\n:1\r\n*6\r\n$6\r\ngeopos\r\n:-2\r\n*1\r\n+readonly\r\n:1\r\n:1\r\n:1\r\n*6\r\n$6\r\nbitpos\r\n:-3\r\n*1\r\n+readonly\r\n:1\r\n:1\r\n:1\r\n*6\r\n$6\r\nsinter\r\n:-2\r\n*2\r\n+readonly\r\n+sort_for_script\r\n:1\r\n:-1\r\n:1\r\n*6\r\n$6\r\ngetset\r\n:3\r\n*2\r\n+write\r\n+denyoom\r\n:1\r\n:1\r\n:1\r\n*6\r\n$7\r\nslaveof\r\n:3\r\n*3\r\n+admin\r\n+noscript\r\n+stale\r\n:0\r\n:0\r\n:0\r\n*6\r\n$6\r\nrpushx\r\n:-3\r\n*3\r\n+write\r\n+denyoom\r\n+fast\r\n:1\r\n:1\r\n:1\r\n*6\r\n$7\r\nlinsert\r\n:5\r\n*2\r\n+write\r\n+denyoom\r\n:1\r\n:1\r\n:1\r\n*6\r\n$6\r\nexpire\r\n:3\r\n*2\r\n+write\r\n+fast\r\n:1\r\n:1\r\n:1\r\n" - - c.WriteRaw(res) -} diff --git a/vendor/github.com/alicebob/miniredis/v2/cmd_connection.go b/vendor/github.com/alicebob/miniredis/v2/cmd_connection.go deleted file mode 100644 index b4ec55d7d..000000000 --- a/vendor/github.com/alicebob/miniredis/v2/cmd_connection.go +++ /dev/null @@ -1,281 +0,0 @@ -// Commands from https://redis.io/commands#connection - -package miniredis - -import ( - "fmt" - "strings" - - "github.com/alicebob/miniredis/v2/server" -) - -func commandsConnection(m *Miniredis) { - m.srv.Register("AUTH", m.cmdAuth) - m.srv.Register("ECHO", m.cmdEcho) - m.srv.Register("HELLO", m.cmdHello) - m.srv.Register("PING", m.cmdPing) - m.srv.Register("QUIT", m.cmdQuit) - m.srv.Register("SELECT", m.cmdSelect) - m.srv.Register("SWAPDB", m.cmdSwapdb) -} - -// PING -func (m *Miniredis) cmdPing(c *server.Peer, cmd string, args []string) { - if !m.handleAuth(c) { - return - } - - if len(args) > 1 { - setDirty(c) - c.WriteError(errWrongNumber(cmd)) - return - } - - payload := "" - if len(args) > 0 { - payload = args[0] - } - - // PING is allowed in subscribed state - if sub := getCtx(c).subscriber; sub != nil { - c.Block(func(c *server.Writer) { - c.WriteLen(2) - c.WriteBulk("pong") - c.WriteBulk(payload) - }) - return - } - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - if payload == "" { - c.WriteInline("PONG") - return - } - c.WriteBulk(payload) - }) -} - -// AUTH -func (m *Miniredis) cmdAuth(c *server.Peer, cmd string, args []string) { - if len(args) < 1 { - setDirty(c) - c.WriteError(errWrongNumber(cmd)) - return - } - - if len(args) > 2 { - c.WriteError(msgSyntaxError) - return - } - if m.checkPubsub(c, cmd) { - return - } - ctx := getCtx(c) - if ctx.nested { - c.WriteError(msgNotFromScripts(ctx.nestedSHA)) - return - } - - var opts = struct { - username string - password string - }{ - username: "default", - password: args[0], - } - if len(args) == 2 { - opts.username, opts.password = args[0], args[1] - } - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - if len(m.passwords) == 0 && opts.username == "default" { - c.WriteError("ERR AUTH called without any password configured for the default user. Are you sure your configuration is correct?") - return - } - setPW, ok := m.passwords[opts.username] - if !ok { - c.WriteError("WRONGPASS invalid username-password pair") - return - } - if setPW != opts.password { - c.WriteError("WRONGPASS invalid username-password pair") - return - } - - ctx.authenticated = true - c.WriteOK() - }) -} - -// HELLO -func (m *Miniredis) cmdHello(c *server.Peer, cmd string, args []string) { - if len(args) < 1 { - c.WriteError(errWrongNumber(cmd)) - return - } - - var opts struct { - version int - username string - password string - } - - if ok := optIntErr(c, args[0], &opts.version, "ERR Protocol version is not an integer or out of range"); !ok { - return - } - args = args[1:] - - switch opts.version { - case 2, 3: - default: - c.WriteError("NOPROTO unsupported protocol version") - return - } - - var checkAuth bool - for len(args) > 0 { - switch strings.ToUpper(args[0]) { - case "AUTH": - if len(args) < 3 { - c.WriteError(fmt.Sprintf("ERR Syntax error in HELLO option '%s'", args[0])) - return - } - opts.username, opts.password, args = args[1], args[2], args[3:] - checkAuth = true - case "SETNAME": - if len(args) < 2 { - c.WriteError(fmt.Sprintf("ERR Syntax error in HELLO option '%s'", args[0])) - return - } - _, args = args[1], args[2:] - default: - c.WriteError(fmt.Sprintf("ERR Syntax error in HELLO option '%s'", args[0])) - return - } - } - - if len(m.passwords) == 0 && opts.username == "default" { - // redis ignores legacy "AUTH" if it's not enabled. - checkAuth = false - } - if checkAuth { - setPW, ok := m.passwords[opts.username] - if !ok { - c.WriteError("WRONGPASS invalid username-password pair") - return - } - if setPW != opts.password { - c.WriteError("WRONGPASS invalid username-password pair") - return - } - getCtx(c).authenticated = true - } - - c.Resp3 = opts.version == 3 - - c.WriteMapLen(7) - c.WriteBulk("server") - c.WriteBulk("miniredis") - c.WriteBulk("version") - c.WriteBulk("8.4.0") - c.WriteBulk("proto") - c.WriteInt(opts.version) - c.WriteBulk("id") - c.WriteInt(42) - c.WriteBulk("mode") - c.WriteBulk("standalone") - c.WriteBulk("role") - c.WriteBulk("master") - c.WriteBulk("modules") // "modules": [ - c.WriteLen(1) // we have 1: "vectorset" - c.WriteMapLen(4) // { - c.WriteBulk("name") // - c.WriteBulk("vectorset") // - c.WriteBulk("ver") // - c.WriteInt(1) // - c.WriteBulk("path") // - c.WriteBulk("") // - c.WriteBulk("args") // - c.WriteLen(0) // ]} end modules -} - -// ECHO -func (m *Miniredis) cmdEcho(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, exactly(1)) { - return - } - - msg := args[0] - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - c.WriteBulk(msg) - }) -} - -// SELECT -func (m *Miniredis) cmdSelect(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, exactly(1)) { - return - } - - var opts struct { - id int - } - if ok := optInt(c, args[0], &opts.id); !ok { - return - } - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - if opts.id < 0 { - c.WriteError(msgDBIndexOutOfRange) - setDirty(c) - return - } - - ctx.selectedDB = opts.id - c.WriteOK() - }) -} - -// SWAPDB -func (m *Miniredis) cmdSwapdb(c *server.Peer, cmd string, args []string) { - if len(args) != 2 { - setDirty(c) - c.WriteError(errWrongNumber(cmd)) - return - } - if !m.handleAuth(c) { - return - } - - var opts struct { - id1 int - id2 int - } - - if ok := optIntErr(c, args[0], &opts.id1, "ERR invalid first DB index"); !ok { - return - } - if ok := optIntErr(c, args[1], &opts.id2, "ERR invalid second DB index"); !ok { - return - } - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - if opts.id1 < 0 || opts.id2 < 0 { - c.WriteError(msgDBIndexOutOfRange) - setDirty(c) - return - } - - m.swapDB(opts.id1, opts.id2) - - c.WriteOK() - }) -} - -// QUIT -func (m *Miniredis) cmdQuit(c *server.Peer, cmd string, args []string) { - // QUIT isn't transactionfied and accepts any arguments. - c.WriteOK() - c.Close() -} diff --git a/vendor/github.com/alicebob/miniredis/v2/cmd_generic.go b/vendor/github.com/alicebob/miniredis/v2/cmd_generic.go deleted file mode 100644 index 08c47a28f..000000000 --- a/vendor/github.com/alicebob/miniredis/v2/cmd_generic.go +++ /dev/null @@ -1,789 +0,0 @@ -// Commands from https://redis.io/commands#generic - -package miniredis - -import ( - "errors" - "fmt" - "sort" - "strconv" - "strings" - "time" - - "github.com/alicebob/miniredis/v2/server" -) - -const ( - // expiretimeReplyNoExpiration is return value for EXPIRETIME and PEXPIRETIME if the key exists but has no associated expiration time - expiretimeReplyNoExpiration = -1 - // expiretimeReplyMissingKey is return value for EXPIRETIME and PEXPIRETIME if the key does not exist - expiretimeReplyMissingKey = -2 -) - -func inSeconds(t time.Time) int { - return int(t.Unix()) -} - -func inMilliSeconds(t time.Time) int { - return int(t.UnixMilli()) -} - -// commandsGeneric handles EXPIRE, TTL, PERSIST, &c. -func commandsGeneric(m *Miniredis) { - m.srv.Register("COPY", m.cmdCopy) - m.srv.Register("DEL", m.cmdDel) - m.srv.Register("DUMP", m.cmdDump, server.ReadOnlyOption()) - m.srv.Register("EXISTS", m.cmdExists, server.ReadOnlyOption()) - m.srv.Register("EXPIRE", makeCmdExpire(m, false, time.Second)) - m.srv.Register("EXPIREAT", makeCmdExpire(m, true, time.Second)) - m.srv.Register("EXPIRETIME", m.makeCmdExpireTime(inSeconds), server.ReadOnlyOption()) - m.srv.Register("PEXPIRETIME", m.makeCmdExpireTime(inMilliSeconds), server.ReadOnlyOption()) - m.srv.Register("KEYS", m.cmdKeys, server.ReadOnlyOption()) - // MIGRATE - m.srv.Register("MOVE", m.cmdMove) - // OBJECT - m.srv.Register("PERSIST", m.cmdPersist) - m.srv.Register("PEXPIRE", makeCmdExpire(m, false, time.Millisecond)) - m.srv.Register("PEXPIREAT", makeCmdExpire(m, true, time.Millisecond)) - m.srv.Register("PTTL", m.cmdPTTL, server.ReadOnlyOption()) - m.srv.Register("RANDOMKEY", m.cmdRandomkey, server.ReadOnlyOption()) - m.srv.Register("RENAME", m.cmdRename) - m.srv.Register("RENAMENX", m.cmdRenamenx) - m.srv.Register("RESTORE", m.cmdRestore) - m.srv.Register("TOUCH", m.cmdTouch, server.ReadOnlyOption()) - m.srv.Register("TTL", m.cmdTTL, server.ReadOnlyOption()) - m.srv.Register("TYPE", m.cmdType, server.ReadOnlyOption()) - m.srv.Register("SCAN", m.cmdScan, server.ReadOnlyOption()) - // SORT - m.srv.Register("UNLINK", m.cmdDel) - m.srv.Register("WAIT", m.cmdWait) -} - -type expireOpts struct { - key string - value int - nx bool - xx bool - gt bool - lt bool -} - -func expireParse(cmd string, args []string) (*expireOpts, error) { - var opts expireOpts - - opts.key = args[0] - if err := optIntSimple(args[1], &opts.value); err != nil { - return nil, err - } - args = args[2:] - for len(args) > 0 { - switch strings.ToLower(args[0]) { - case "nx": - opts.nx = true - case "xx": - opts.xx = true - case "gt": - opts.gt = true - case "lt": - opts.lt = true - default: - return nil, fmt.Errorf("ERR Unsupported option %s", args[0]) - } - args = args[1:] - } - if opts.gt && opts.lt { - return nil, errors.New("ERR GT and LT options at the same time are not compatible") - } - if opts.nx && (opts.xx || opts.gt || opts.lt) { - return nil, errors.New("ERR NX and XX, GT or LT options at the same time are not compatible") - } - return &opts, nil -} - -// generic expire command for EXPIRE, PEXPIRE, EXPIREAT, PEXPIREAT -// d is the time unit. If unix is set it'll be seen as a unixtimestamp and -// converted to a duration. -func makeCmdExpire(m *Miniredis, unix bool, d time.Duration) func(*server.Peer, string, []string) { - return func(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, atLeast(2)) { - return - } - - opts, err := expireParse(cmd, args) - if err != nil { - setDirty(c) - c.WriteError(err.Error()) - return - } - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - db := m.db(ctx.selectedDB) - - // Key must be present. - if _, ok := db.keys[opts.key]; !ok { - c.WriteInt(0) - return - } - - oldTTL, ok := db.ttl[opts.key] - - var newTTL time.Duration - if unix { - newTTL = m.at(opts.value, d) - } else { - newTTL = time.Duration(opts.value) * d - } - - // > NX -- Set expiry only when the key has no expiry - if opts.nx && ok { - c.WriteInt(0) - return - } - // > XX -- Set expiry only when the key has an existing expiry - if opts.xx && !ok { - c.WriteInt(0) - return - } - // > GT -- Set expiry only when the new expiry is greater than current one - // (no exp == infinity) - if opts.gt && (!ok || newTTL <= oldTTL) { - c.WriteInt(0) - return - } - // > LT -- Set expiry only when the new expiry is less than current one - if opts.lt && ok && newTTL > oldTTL { - c.WriteInt(0) - return - } - db.ttl[opts.key] = newTTL - db.incr(opts.key) - db.checkTTL(opts.key) - c.WriteInt(1) - }) - } -} - -// makeCmdExpireTime creates server command function that returns the absolute Unix timestamp (since January 1, 1970) -// at which the given key will expire, in unit selected by time result strategy (e.g. seconds, milliseconds). -// For more information see redis documentation for [expiretime] and [pexpiretime]. -// -// [expiretime]: https://redis.io/commands/expiretime/ -// [pexpiretime]: https://redis.io/commands/pexpiretime/ -func (m *Miniredis) makeCmdExpireTime(timeResultStrategy func(time.Time) int) server.Cmd { - return func(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, exactly(1)) { - return - } - - key := args[0] - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - db := m.db(ctx.selectedDB) - - if _, ok := db.keys[key]; !ok { - c.WriteInt(expiretimeReplyMissingKey) - return - } - - ttl, ok := db.ttl[key] - if !ok { - c.WriteInt(expiretimeReplyNoExpiration) - return - } - - c.WriteInt(timeResultStrategy(m.effectiveNow().Add(ttl))) - }) - } -} - -// TOUCH -func (m *Miniredis) cmdTouch(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, atLeast(1)) { - return - } - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - db := m.db(ctx.selectedDB) - - count := 0 - for _, key := range args { - if db.exists(key) { - count++ - } - } - c.WriteInt(count) - }) -} - -// TTL -func (m *Miniredis) cmdTTL(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, exactly(1)) { - return - } - - key := args[0] - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - db := m.db(ctx.selectedDB) - - if _, ok := db.keys[key]; !ok { - // No such key - c.WriteInt(-2) - return - } - - v, ok := db.ttl[key] - if !ok { - // no expire value - c.WriteInt(-1) - return - } - c.WriteInt(int(v.Seconds())) - }) -} - -// PTTL -func (m *Miniredis) cmdPTTL(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, exactly(1)) { - return - } - - key := args[0] - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - db := m.db(ctx.selectedDB) - - if _, ok := db.keys[key]; !ok { - // no such key - c.WriteInt(-2) - return - } - - v, ok := db.ttl[key] - if !ok { - // no expire value - c.WriteInt(-1) - return - } - c.WriteInt(int(v.Nanoseconds() / 1000000)) - }) -} - -// PERSIST -func (m *Miniredis) cmdPersist(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, exactly(1)) { - return - } - - key := args[0] - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - db := m.db(ctx.selectedDB) - - if _, ok := db.keys[key]; !ok { - // no such key - c.WriteInt(0) - return - } - - if _, ok := db.ttl[key]; !ok { - // no expire value - c.WriteInt(0) - return - } - delete(db.ttl, key) - db.incr(key) - c.WriteInt(1) - }) -} - -// DEL and UNLINK -func (m *Miniredis) cmdDel(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, atLeast(1)) { - return - } - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - db := m.db(ctx.selectedDB) - - count := 0 - for _, key := range args { - if db.exists(key) { - count++ - } - db.del(key, true) // delete expire - } - c.WriteInt(count) - }) -} - -// DUMP -func (m *Miniredis) cmdDump(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, exactly(1)) { - return - } - - key := args[0] - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - db := m.db(ctx.selectedDB) - keyType, exists := db.keys[key] - if !exists { - c.WriteNull() - } else if keyType != keyTypeString { - c.WriteError(msgWrongType) - } else { - c.WriteBulk(db.stringGet(key)) - } - }) -} - -// TYPE -func (m *Miniredis) cmdType(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, exactly(1)) { - return - } - - key := args[0] - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - db := m.db(ctx.selectedDB) - - t, ok := db.keys[key] - if !ok { - c.WriteInline("none") - return - } - - c.WriteInline(t) - }) -} - -// EXISTS -func (m *Miniredis) cmdExists(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, atLeast(1)) { - return - } - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - db := m.db(ctx.selectedDB) - - found := 0 - for _, k := range args { - if db.exists(k) { - found++ - } - } - c.WriteInt(found) - }) -} - -// MOVE -func (m *Miniredis) cmdMove(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, exactly(2)) { - return - } - - var opts struct { - key string - targetDB int - } - - opts.key = args[0] - opts.targetDB, _ = strconv.Atoi(args[1]) - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - if ctx.selectedDB == opts.targetDB { - c.WriteError("ERR source and destination objects are the same") - return - } - db := m.db(ctx.selectedDB) - targetDB := m.db(opts.targetDB) - - if !db.move(opts.key, targetDB) { - c.WriteInt(0) - return - } - c.WriteInt(1) - }) -} - -// KEYS -func (m *Miniredis) cmdKeys(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, exactly(1)) { - return - } - - key := args[0] - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - db := m.db(ctx.selectedDB) - - keys, _ := matchKeys(db.allKeys(), key) - c.WriteLen(len(keys)) - for _, s := range keys { - c.WriteBulk(s) - } - }) -} - -// RANDOMKEY -func (m *Miniredis) cmdRandomkey(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, exactly(0)) { - return - } - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - db := m.db(ctx.selectedDB) - - if len(db.keys) == 0 { - c.WriteNull() - return - } - nr := m.randIntn(len(db.keys)) - for k := range db.keys { - if nr == 0 { - c.WriteBulk(k) - return - } - nr-- - } - }) -} - -// RENAME -func (m *Miniredis) cmdRename(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, exactly(2)) { - return - } - - opts := struct { - from string - to string - }{ - from: args[0], - to: args[1], - } - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - db := m.db(ctx.selectedDB) - - if !db.exists(opts.from) { - c.WriteError(msgKeyNotFound) - return - } - - db.rename(opts.from, opts.to) - c.WriteOK() - }) -} - -// RENAMENX -func (m *Miniredis) cmdRenamenx(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, exactly(2)) { - return - } - - opts := struct { - from string - to string - }{ - from: args[0], - to: args[1], - } - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - db := m.db(ctx.selectedDB) - - if !db.exists(opts.from) { - c.WriteError(msgKeyNotFound) - return - } - - if db.exists(opts.to) { - c.WriteInt(0) - return - } - - db.rename(opts.from, opts.to) - c.WriteInt(1) - }) -} - -type restoreOpts struct { - key string - serializedValue string - rawTtl string - replace bool - absTtl bool -} - -func restoreParse(args []string) *restoreOpts { - var opts restoreOpts - - opts.key, opts.rawTtl, opts.serializedValue, args = args[0], args[1], args[2], args[3:] - - for len(args) > 0 { - switch arg := strings.ToUpper(args[0]); arg { - case "REPLACE": - opts.replace = true - case "ABSTTL": - opts.absTtl = true - default: - return nil - } - - args = args[1:] - } - - return &opts -} - -// RESTORE -func (m *Miniredis) cmdRestore(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, atLeast(3)) { - return - } - - var opts = restoreParse(args) - if opts == nil { - setDirty(c) - c.WriteError(msgSyntaxError) - return - } - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - db := m.db(ctx.selectedDB) - - _, keyExists := db.keys[opts.key] - if keyExists && !opts.replace { - setDirty(c) - c.WriteError("BUSYKEY Target key name already exists.") - return - } - - ttl, err := strconv.Atoi(opts.rawTtl) - if err != nil || ttl < 0 { - c.WriteError(msgInvalidInt) - return - } - - db.stringSet(opts.key, opts.serializedValue) - - if ttl != 0 { - if opts.absTtl { - db.ttl[opts.key] = m.at(ttl, time.Millisecond) - } else { - db.ttl[opts.key] = time.Duration(ttl) * time.Millisecond - } - } - - c.WriteOK() - }) -} - -type scanOpts struct { - cursor int - count int - withMatch bool - match string - withType bool - _type string -} - -func scanParse(cmd string, args []string) (*scanOpts, error) { - var opts scanOpts - if err := optIntSimple(args[0], &opts.cursor); err != nil { - return nil, errors.New(msgInvalidCursor) - } - args = args[1:] - - // MATCH, COUNT and TYPE options - for len(args) > 0 { - if strings.ToLower(args[0]) == "count" { - if len(args) < 2 { - return nil, errors.New(msgSyntaxError) - } - count, err := strconv.Atoi(args[1]) - if err != nil || count < 0 { - return nil, errors.New(msgInvalidInt) - } - if count == 0 { - return nil, errors.New(msgSyntaxError) - } - opts.count = count - args = args[2:] - continue - } - if strings.ToLower(args[0]) == "match" { - if len(args) < 2 { - return nil, errors.New(msgSyntaxError) - } - opts.withMatch = true - opts.match, args = args[1], args[2:] - continue - } - if strings.ToLower(args[0]) == "type" { - if len(args) < 2 { - return nil, errors.New(msgSyntaxError) - } - opts.withType = true - opts._type, args = strings.ToLower(args[1]), args[2:] - continue - } - return nil, errors.New(msgSyntaxError) - } - return &opts, nil -} - -// SCAN -func (m *Miniredis) cmdScan(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, atLeast(1)) { - return - } - - opts, err := scanParse(cmd, args) - if err != nil { - setDirty(c) - c.WriteError(err.Error()) - return - } - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - db := m.db(ctx.selectedDB) - // We return _all_ (matched) keys every time, so that cursors work. - // We ignore "COUNT", which is allowed according to the Redis docs. - var keys []string - - if opts.withType { - keys = make([]string, 0) - for k, t := range db.keys { - // type must be given exactly; no pattern matching is performed - if t == opts._type { - keys = append(keys, k) - } - } - } else { - keys = db.allKeys() - } - - sort.Strings(keys) // To make things deterministic. - - if opts.withMatch { - keys, _ = matchKeys(keys, opts.match) - } - - // we only ever return all at once, so no non-zero cursor can every be valid - if opts.cursor != 0 { - c.WriteLen(2) - c.WriteBulk("0") // no next cursor - c.WriteLen(0) // no elements - return - } - cursorValue := 0 // we don't use cursors - c.WriteLen(2) - c.WriteBulk(fmt.Sprintf("%d", cursorValue)) - c.WriteLen(len(keys)) - for _, k := range keys { - c.WriteBulk(k) - } - }) -} - -type copyOpts struct { - from string - to string - destinationDB int - replace bool -} - -func copyParse(cmd string, args []string) (*copyOpts, error) { - opts := copyOpts{ - destinationDB: -1, - } - - opts.from, opts.to, args = args[0], args[1], args[2:] - for len(args) > 0 { - switch strings.ToLower(args[0]) { - case "db": - if len(args) < 2 { - return nil, errors.New(msgSyntaxError) - } - if err := optIntSimple(args[1], &opts.destinationDB); err != nil { - return nil, err - } - if opts.destinationDB < 0 { - return nil, errors.New(msgDBIndexOutOfRange) - } - args = args[2:] - case "replace": - opts.replace = true - args = args[1:] - default: - return nil, errors.New(msgSyntaxError) - } - } - return &opts, nil -} - -// COPY -func (m *Miniredis) cmdCopy(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, atLeast(2)) { - return - } - - opts, err := copyParse(cmd, args) - if err != nil { - setDirty(c) - c.WriteError(err.Error()) - return - } - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - fromDB, toDB := ctx.selectedDB, opts.destinationDB - if toDB == -1 { - toDB = fromDB - } - - if fromDB == toDB && opts.from == opts.to { - c.WriteError("ERR source and destination objects are the same") - return - } - - if !m.db(fromDB).exists(opts.from) { - c.WriteInt(0) - return - } - - if !opts.replace { - if m.db(toDB).exists(opts.to) { - c.WriteInt(0) - return - } - } - - m.copy(m.db(fromDB), opts.from, m.db(toDB), opts.to) - c.WriteInt(1) - }) -} - -// WAIT -func (m *Miniredis) cmdWait(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, exactly(2)) { - return - } - nReplicas, err := strconv.Atoi(args[0]) - if err != nil || nReplicas < 0 { - c.WriteError(msgInvalidInt) - return - } - timeout, err := strconv.Atoi(args[1]) - if err != nil { - c.WriteError(msgInvalidInt) - return - } - if timeout < 0 { - c.WriteError(msgTimeoutNegative) - return - } - // WAIT always returns 0 when called on a standalone instance - c.WriteInt(0) -} diff --git a/vendor/github.com/alicebob/miniredis/v2/cmd_geo.go b/vendor/github.com/alicebob/miniredis/v2/cmd_geo.go deleted file mode 100644 index 12cf99add..000000000 --- a/vendor/github.com/alicebob/miniredis/v2/cmd_geo.go +++ /dev/null @@ -1,577 +0,0 @@ -// Commands from https://redis.io/commands#geo - -package miniredis - -import ( - "fmt" - "sort" - "strconv" - "strings" - - "github.com/alicebob/miniredis/v2/server" -) - -// commandsGeo handles GEOADD, GEORADIUS etc. -func commandsGeo(m *Miniredis) { - m.srv.Register("GEOADD", m.cmdGeoadd) - m.srv.Register("GEODIST", m.cmdGeodist, server.ReadOnlyOption()) - m.srv.Register("GEOPOS", m.cmdGeopos, server.ReadOnlyOption()) - m.srv.Register("GEORADIUS", m.cmdGeoradius) - m.srv.Register("GEORADIUS_RO", m.cmdGeoradius, server.ReadOnlyOption()) - m.srv.Register("GEORADIUSBYMEMBER", m.cmdGeoradiusbymember) - m.srv.Register("GEORADIUSBYMEMBER_RO", m.cmdGeoradiusbymember, server.ReadOnlyOption()) -} - -// GEOADD -func (m *Miniredis) cmdGeoadd(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, atLeast(3)) { - return - } - - if len(args[1:])%3 != 0 { - setDirty(c) - c.WriteError(errWrongNumber(cmd)) - return - } - - key, args := args[0], args[1:] - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - db := m.db(ctx.selectedDB) - - if db.exists(key) && db.t(key) != keyTypeSortedSet { - c.WriteError(ErrWrongType.Error()) - return - } - - toSet := map[string]float64{} - for len(args) > 2 { - rawLong, rawLat, name := args[0], args[1], args[2] - args = args[3:] - longitude, err := strconv.ParseFloat(rawLong, 64) - if err != nil { - c.WriteError("ERR value is not a valid float") - return - } - latitude, err := strconv.ParseFloat(rawLat, 64) - if err != nil { - c.WriteError("ERR value is not a valid float") - return - } - - if latitude < -85.05112878 || - latitude > 85.05112878 || - longitude < -180 || - longitude > 180 { - c.WriteError(fmt.Sprintf("ERR invalid longitude,latitude pair %.6f,%.6f", longitude, latitude)) - return - } - - toSet[name] = float64(toGeohash(longitude, latitude)) - } - - set := 0 - for name, score := range toSet { - if db.ssetAdd(key, score, name) { - set++ - } - } - c.WriteInt(set) - }) -} - -// GEODIST -func (m *Miniredis) cmdGeodist(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, atLeast(3)) { - return - } - - key, from, to, args := args[0], args[1], args[2], args[3:] - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - db := m.db(ctx.selectedDB) - if !db.exists(key) { - c.WriteNull() - return - } - if db.t(key) != keyTypeSortedSet { - c.WriteError(ErrWrongType.Error()) - return - } - - unit := "m" - if len(args) > 0 { - unit, args = args[0], args[1:] - } - if len(args) > 0 { - c.WriteError(msgSyntaxError) - return - } - - toMeter := parseUnit(unit) - if toMeter == 0 { - c.WriteError(msgUnsupportedUnit) - return - } - - members := db.sortedsetKeys[key] - fromD, okFrom := members.get(from) - toD, okTo := members.get(to) - if !okFrom || !okTo { - c.WriteNull() - return - } - - fromLo, fromLat := fromGeohash(uint64(fromD)) - toLo, toLat := fromGeohash(uint64(toD)) - - dist := distance(fromLat, fromLo, toLat, toLo) / toMeter - c.WriteBulk(fmt.Sprintf("%.4f", dist)) - }) -} - -// GEOPOS -func (m *Miniredis) cmdGeopos(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, atLeast(1)) { - return - } - - key, args := args[0], args[1:] - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - db := m.db(ctx.selectedDB) - - if db.exists(key) && db.t(key) != keyTypeSortedSet { - c.WriteError(ErrWrongType.Error()) - return - } - - c.WriteLen(len(args)) - for _, l := range args { - if !db.ssetExists(key, l) { - c.WriteLen(-1) - continue - } - score := db.ssetScore(key, l) - c.WriteLen(2) - long, lat := fromGeohash(uint64(score)) - c.WriteBulk(fmt.Sprintf("%f", long)) - c.WriteBulk(fmt.Sprintf("%f", lat)) - } - }) -} - -type geoDistance struct { - Name string - Score float64 - Distance float64 - Longitude float64 - Latitude float64 -} - -// GEORADIUS and GEORADIUS_RO -func (m *Miniredis) cmdGeoradius(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, atLeast(5)) { - return - } - - key := args[0] - longitude, err := strconv.ParseFloat(args[1], 64) - if err != nil { - setDirty(c) - c.WriteError(errWrongNumber(cmd)) - return - } - latitude, err := strconv.ParseFloat(args[2], 64) - if err != nil { - setDirty(c) - c.WriteError(errWrongNumber(cmd)) - return - } - radius, err := strconv.ParseFloat(args[3], 64) - if err != nil || radius < 0 { - setDirty(c) - c.WriteError(errWrongNumber(cmd)) - return - } - toMeter := parseUnit(args[4]) - if toMeter == 0 { - setDirty(c) - c.WriteError(errWrongNumber(cmd)) - return - } - args = args[5:] - - var opts struct { - withDist bool - withCoord bool - direction direction // unsorted - count int - withStore bool - storeKey string - withStoredist bool - storedistKey string - } - for len(args) > 0 { - arg := args[0] - args = args[1:] - switch strings.ToUpper(arg) { - case "WITHCOORD": - opts.withCoord = true - case "WITHDIST": - opts.withDist = true - case "ASC": - opts.direction = asc - case "DESC": - opts.direction = desc - case "COUNT": - if len(args) == 0 { - setDirty(c) - c.WriteError("ERR syntax error") - return - } - n, err := strconv.Atoi(args[0]) - if err != nil { - setDirty(c) - c.WriteError(msgInvalidInt) - return - } - if n <= 0 { - setDirty(c) - c.WriteError("ERR COUNT must be > 0") - return - } - args = args[1:] - opts.count = n - case "STORE": - if len(args) == 0 { - setDirty(c) - c.WriteError("ERR syntax error") - return - } - opts.withStore = true - opts.storeKey = args[0] - args = args[1:] - case "STOREDIST": - if len(args) == 0 { - setDirty(c) - c.WriteError("ERR syntax error") - return - } - opts.withStoredist = true - opts.storedistKey = args[0] - args = args[1:] - default: - setDirty(c) - c.WriteError("ERR syntax error") - return - } - } - - if strings.ToUpper(cmd) == "GEORADIUS_RO" && (opts.withStore || opts.withStoredist) { - setDirty(c) - c.WriteError("ERR syntax error") - return - } - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - if (opts.withStore || opts.withStoredist) && (opts.withDist || opts.withCoord) { - c.WriteError("ERR STORE option in GEORADIUS is not compatible with WITHDIST, WITHHASH and WITHCOORDS options") - return - } - - db := m.db(ctx.selectedDB) - members := db.ssetElements(key) - - matches := withinRadius(members, longitude, latitude, radius*toMeter) - - // deal with ASC/DESC - if opts.direction != unsorted { - sort.Slice(matches, func(i, j int) bool { - if opts.direction == desc { - return matches[i].Distance > matches[j].Distance - } - return matches[i].Distance < matches[j].Distance - }) - } - - // deal with COUNT - if opts.count > 0 && len(matches) > opts.count { - matches = matches[:opts.count] - } - - // deal with "STORE x" - if opts.withStore { - db.del(opts.storeKey, true) - for _, member := range matches { - db.ssetAdd(opts.storeKey, member.Score, member.Name) - } - c.WriteInt(len(matches)) - return - } - - // deal with "STOREDIST x" - if opts.withStoredist { - db.del(opts.storedistKey, true) - for _, member := range matches { - db.ssetAdd(opts.storedistKey, member.Distance/toMeter, member.Name) - } - c.WriteInt(len(matches)) - return - } - - c.WriteLen(len(matches)) - for _, member := range matches { - if !opts.withDist && !opts.withCoord { - c.WriteBulk(member.Name) - continue - } - - len := 1 - if opts.withDist { - len++ - } - if opts.withCoord { - len++ - } - c.WriteLen(len) - c.WriteBulk(member.Name) - if opts.withDist { - c.WriteBulk(fmt.Sprintf("%.4f", member.Distance/toMeter)) - } - if opts.withCoord { - c.WriteLen(2) - c.WriteBulk(fmt.Sprintf("%f", member.Longitude)) - c.WriteBulk(fmt.Sprintf("%f", member.Latitude)) - } - } - }) -} - -// GEORADIUSBYMEMBER and GEORADIUSBYMEMBER_RO -func (m *Miniredis) cmdGeoradiusbymember(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, atLeast(4)) { - return - } - - opts := struct { - key string - member string - radius float64 - toMeter float64 - - withDist bool - withCoord bool - direction direction // unsorted - count int - withStore bool - storeKey string - withStoredist bool - storedistKey string - }{ - key: args[0], - member: args[1], - } - - r, err := strconv.ParseFloat(args[2], 64) - if err != nil || r < 0 { - setDirty(c) - c.WriteError(errWrongNumber(cmd)) - return - } - opts.radius = r - - opts.toMeter = parseUnit(args[3]) - if opts.toMeter == 0 { - setDirty(c) - c.WriteError(errWrongNumber(cmd)) - return - } - args = args[4:] - - for len(args) > 0 { - arg := args[0] - args = args[1:] - switch strings.ToUpper(arg) { - case "WITHCOORD": - opts.withCoord = true - case "WITHDIST": - opts.withDist = true - case "ASC": - opts.direction = asc - case "DESC": - opts.direction = desc - case "COUNT": - if len(args) == 0 { - setDirty(c) - c.WriteError("ERR syntax error") - return - } - n, err := strconv.Atoi(args[0]) - if err != nil { - setDirty(c) - c.WriteError(msgInvalidInt) - return - } - if n <= 0 { - setDirty(c) - c.WriteError("ERR COUNT must be > 0") - return - } - args = args[1:] - opts.count = n - case "STORE": - if len(args) == 0 { - setDirty(c) - c.WriteError("ERR syntax error") - return - } - opts.withStore = true - opts.storeKey = args[0] - args = args[1:] - case "STOREDIST": - if len(args) == 0 { - setDirty(c) - c.WriteError("ERR syntax error") - return - } - opts.withStoredist = true - opts.storedistKey = args[0] - args = args[1:] - default: - setDirty(c) - c.WriteError("ERR syntax error") - return - } - } - - if strings.ToUpper(cmd) == "GEORADIUSBYMEMBER_RO" && (opts.withStore || opts.withStoredist) { - setDirty(c) - c.WriteError("ERR syntax error") - return - } - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - if (opts.withStore || opts.withStoredist) && (opts.withDist || opts.withCoord) { - c.WriteError("ERR STORE option in GEORADIUS is not compatible with WITHDIST, WITHHASH and WITHCOORDS options") - return - } - - db := m.db(ctx.selectedDB) - if !db.exists(opts.key) { - c.WriteNull() - return - } - - if db.t(opts.key) != keyTypeSortedSet { - c.WriteError(ErrWrongType.Error()) - return - } - - // get position of member - if !db.ssetExists(opts.key, opts.member) { - c.WriteError("ERR could not decode requested zset member") - return - } - score := db.ssetScore(opts.key, opts.member) - longitude, latitude := fromGeohash(uint64(score)) - - members := db.ssetElements(opts.key) - matches := withinRadius(members, longitude, latitude, opts.radius*opts.toMeter) - - // deal with ASC/DESC - if opts.direction != unsorted { - sort.Slice(matches, func(i, j int) bool { - if opts.direction == desc { - return matches[i].Distance > matches[j].Distance - } - return matches[i].Distance < matches[j].Distance - }) - } - - // deal with COUNT - if opts.count > 0 && len(matches) > opts.count { - matches = matches[:opts.count] - } - - // deal with "STORE x" - if opts.withStore { - db.del(opts.storeKey, true) - for _, member := range matches { - db.ssetAdd(opts.storeKey, member.Score, member.Name) - } - c.WriteInt(len(matches)) - return - } - - // deal with "STOREDIST x" - if opts.withStoredist { - db.del(opts.storedistKey, true) - for _, member := range matches { - db.ssetAdd(opts.storedistKey, member.Distance/opts.toMeter, member.Name) - } - c.WriteInt(len(matches)) - return - } - - c.WriteLen(len(matches)) - for _, member := range matches { - if !opts.withDist && !opts.withCoord { - c.WriteBulk(member.Name) - continue - } - - len := 1 - if opts.withDist { - len++ - } - if opts.withCoord { - len++ - } - c.WriteLen(len) - c.WriteBulk(member.Name) - if opts.withDist { - c.WriteBulk(fmt.Sprintf("%.4f", member.Distance/opts.toMeter)) - } - if opts.withCoord { - c.WriteLen(2) - c.WriteBulk(fmt.Sprintf("%f", member.Longitude)) - c.WriteBulk(fmt.Sprintf("%f", member.Latitude)) - } - } - }) -} - -func withinRadius(members []ssElem, longitude, latitude, radius float64) []geoDistance { - matches := []geoDistance{} - for _, el := range members { - elLo, elLat := fromGeohash(uint64(el.score)) - distanceInMeter := distance(latitude, longitude, elLat, elLo) - - if distanceInMeter <= radius { - matches = append(matches, geoDistance{ - Name: el.member, - Score: el.score, - Distance: distanceInMeter, - Longitude: elLo, - Latitude: elLat, - }) - } - } - return matches -} - -func parseUnit(u string) float64 { - switch strings.ToLower(u) { - case "m": - return 1 - case "km": - return 1000 - case "mi": - return 1609.34 - case "ft": - return 0.3048 - default: - return 0 - } -} diff --git a/vendor/github.com/alicebob/miniredis/v2/cmd_hash.go b/vendor/github.com/alicebob/miniredis/v2/cmd_hash.go deleted file mode 100644 index 9c5e68667..000000000 --- a/vendor/github.com/alicebob/miniredis/v2/cmd_hash.go +++ /dev/null @@ -1,797 +0,0 @@ -// Commands from https://redis.io/commands#hash - -package miniredis - -import ( - "fmt" - "math/big" - "strconv" - "strings" - "time" - - "github.com/alicebob/miniredis/v2/server" -) - -// commandsHash handles all hash value operations. -func commandsHash(m *Miniredis) { - m.srv.Register("HDEL", m.cmdHdel) - m.srv.Register("HEXISTS", m.cmdHexists, server.ReadOnlyOption()) - m.srv.Register("HGET", m.cmdHget, server.ReadOnlyOption()) - m.srv.Register("HGETALL", m.cmdHgetall, server.ReadOnlyOption()) - m.srv.Register("HINCRBY", m.cmdHincrby) - m.srv.Register("HINCRBYFLOAT", m.cmdHincrbyfloat) - m.srv.Register("HKEYS", m.cmdHkeys, server.ReadOnlyOption()) - m.srv.Register("HLEN", m.cmdHlen, server.ReadOnlyOption()) - m.srv.Register("HMGET", m.cmdHmget, server.ReadOnlyOption()) - m.srv.Register("HMSET", m.cmdHmset) - m.srv.Register("HSET", m.cmdHset) - m.srv.Register("HSETNX", m.cmdHsetnx) - m.srv.Register("HSTRLEN", m.cmdHstrlen, server.ReadOnlyOption()) - m.srv.Register("HVALS", m.cmdHvals, server.ReadOnlyOption()) - m.srv.Register("HSCAN", m.cmdHscan, server.ReadOnlyOption()) - m.srv.Register("HRANDFIELD", m.cmdHrandfield, server.ReadOnlyOption()) - m.srv.Register("HEXPIRE", m.cmdHexpire) -} - -// HSET -func (m *Miniredis) cmdHset(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, atLeast(3)) { - return - } - - key, pairs := args[0], args[1:] - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - db := m.db(ctx.selectedDB) - - if len(pairs)%2 == 1 { - c.WriteError(errWrongNumber(cmd)) - return - } - - if t, ok := db.keys[key]; ok && t != keyTypeHash { - c.WriteError(msgWrongType) - return - } - - new := db.hashSet(key, pairs...) - c.WriteInt(new) - }) -} - -// HSETNX -func (m *Miniredis) cmdHsetnx(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, exactly(3)) { - return - } - - opts := struct { - key string - field string - value string - }{ - key: args[0], - field: args[1], - value: args[2], - } - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - db := m.db(ctx.selectedDB) - - if t, ok := db.keys[opts.key]; ok && t != keyTypeHash { - c.WriteError(msgWrongType) - return - } - - if _, ok := db.hashKeys[opts.key]; !ok { - db.hashKeys[opts.key] = map[string]string{} - db.keys[opts.key] = keyTypeHash - } - _, ok := db.hashKeys[opts.key][opts.field] - if ok { - c.WriteInt(0) - return - } - db.hashKeys[opts.key][opts.field] = opts.value - db.incr(opts.key) - c.WriteInt(1) - }) -} - -// HMSET -func (m *Miniredis) cmdHmset(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, atLeast(3)) { - return - } - - key, args := args[0], args[1:] - if len(args)%2 != 0 { - setDirty(c) - c.WriteError(errWrongNumber(cmd)) - return - } - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - db := m.db(ctx.selectedDB) - - if t, ok := db.keys[key]; ok && t != keyTypeHash { - c.WriteError(msgWrongType) - return - } - - for len(args) > 0 { - field, value := args[0], args[1] - args = args[2:] - db.hashSet(key, field, value) - } - c.WriteOK() - }) -} - -// HGET -func (m *Miniredis) cmdHget(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, exactly(2)) { - return - } - - key, field := args[0], args[1] - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - db := m.db(ctx.selectedDB) - - t, ok := db.keys[key] - if !ok { - c.WriteNull() - return - } - if t != keyTypeHash { - c.WriteError(msgWrongType) - return - } - value, ok := db.hashKeys[key][field] - if !ok { - c.WriteNull() - return - } - c.WriteBulk(value) - }) -} - -// HDEL -func (m *Miniredis) cmdHdel(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, atLeast(2)) { - return - } - - opts := struct { - key string - fields []string - }{ - key: args[0], - fields: args[1:], - } - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - db := m.db(ctx.selectedDB) - - t, ok := db.keys[opts.key] - if !ok { - // No key is zero deleted - c.WriteInt(0) - return - } - if t != keyTypeHash { - c.WriteError(msgWrongType) - return - } - - deleted := 0 - for _, f := range opts.fields { - _, ok := db.hashKeys[opts.key][f] - if !ok { - continue - } - delete(db.hashKeys[opts.key], f) - deleted++ - } - c.WriteInt(deleted) - - // Nothing left. Remove the whole key. - if len(db.hashKeys[opts.key]) == 0 { - db.del(opts.key, true) - } - }) -} - -// HEXISTS -func (m *Miniredis) cmdHexists(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, exactly(2)) { - return - } - - opts := struct { - key string - field string - }{ - key: args[0], - field: args[1], - } - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - db := m.db(ctx.selectedDB) - - t, ok := db.keys[opts.key] - if !ok { - c.WriteInt(0) - return - } - if t != keyTypeHash { - c.WriteError(msgWrongType) - return - } - - if _, ok := db.hashKeys[opts.key][opts.field]; !ok { - c.WriteInt(0) - return - } - c.WriteInt(1) - }) -} - -// HGETALL -func (m *Miniredis) cmdHgetall(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, exactly(1)) { - return - } - - key := args[0] - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - db := m.db(ctx.selectedDB) - - t, ok := db.keys[key] - if !ok { - c.WriteMapLen(0) - return - } - if t != keyTypeHash { - c.WriteError(msgWrongType) - return - } - - c.WriteMapLen(len(db.hashKeys[key])) - for _, k := range db.hashFields(key) { - c.WriteBulk(k) - c.WriteBulk(db.hashGet(key, k)) - } - }) -} - -// HKEYS -func (m *Miniredis) cmdHkeys(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, exactly(1)) { - return - } - - key := args[0] - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - db := m.db(ctx.selectedDB) - - if !db.exists(key) { - c.WriteLen(0) - return - } - if db.t(key) != keyTypeHash { - c.WriteError(msgWrongType) - return - } - - fields := db.hashFields(key) - c.WriteLen(len(fields)) - for _, f := range fields { - c.WriteBulk(f) - } - }) -} - -// HSTRLEN -func (m *Miniredis) cmdHstrlen(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, exactly(2)) { - return - } - - hash, key := args[0], args[1] - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - db := m.db(ctx.selectedDB) - - t, ok := db.keys[hash] - if !ok { - c.WriteInt(0) - return - } - if t != keyTypeHash { - c.WriteError(msgWrongType) - return - } - - keys := db.hashKeys[hash] - c.WriteInt(len(keys[key])) - }) -} - -// HVALS -func (m *Miniredis) cmdHvals(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, exactly(1)) { - return - } - - key := args[0] - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - db := m.db(ctx.selectedDB) - - t, ok := db.keys[key] - if !ok { - c.WriteLen(0) - return - } - if t != keyTypeHash { - c.WriteError(msgWrongType) - return - } - - vals := db.hashValues(key) - c.WriteLen(len(vals)) - for _, v := range vals { - c.WriteBulk(v) - } - }) -} - -// HLEN -func (m *Miniredis) cmdHlen(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, exactly(1)) { - return - } - - key := args[0] - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - db := m.db(ctx.selectedDB) - - t, ok := db.keys[key] - if !ok { - c.WriteInt(0) - return - } - if t != keyTypeHash { - c.WriteError(msgWrongType) - return - } - - c.WriteInt(len(db.hashKeys[key])) - }) -} - -// HMGET -func (m *Miniredis) cmdHmget(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, atLeast(2)) { - return - } - - key := args[0] - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - db := m.db(ctx.selectedDB) - - if t, ok := db.keys[key]; ok && t != keyTypeHash { - c.WriteError(msgWrongType) - return - } - - f, ok := db.hashKeys[key] - if !ok { - f = map[string]string{} - } - - c.WriteLen(len(args) - 1) - for _, k := range args[1:] { - v, ok := f[k] - if !ok { - c.WriteNull() - continue - } - c.WriteBulk(v) - } - }) -} - -// HINCRBY -func (m *Miniredis) cmdHincrby(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, exactly(3)) { - return - } - - opts := struct { - key string - field string - delta int - }{ - key: args[0], - field: args[1], - } - if ok := optInt(c, args[2], &opts.delta); !ok { - return - } - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - db := m.db(ctx.selectedDB) - - if t, ok := db.keys[opts.key]; ok && t != keyTypeHash { - c.WriteError(msgWrongType) - return - } - - v, err := db.hashIncr(opts.key, opts.field, opts.delta) - if err != nil { - c.WriteError(err.Error()) - return - } - c.WriteInt(v) - }) -} - -// HINCRBYFLOAT -func (m *Miniredis) cmdHincrbyfloat(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, exactly(3)) { - return - } - - opts := struct { - key string - field string - delta *big.Float - }{ - key: args[0], - field: args[1], - } - delta, _, err := big.ParseFloat(args[2], 10, 128, 0) - if err != nil { - setDirty(c) - c.WriteError(msgInvalidFloat) - return - } - opts.delta = delta - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - db := m.db(ctx.selectedDB) - - if t, ok := db.keys[opts.key]; ok && t != keyTypeHash { - c.WriteError(msgWrongType) - return - } - - v, err := db.hashIncrfloat(opts.key, opts.field, opts.delta) - if err != nil { - c.WriteError(err.Error()) - return - } - c.WriteBulk(formatBig(v)) - }) -} - -// HSCAN -func (m *Miniredis) cmdHscan(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, atLeast(2)) { - return - } - - opts := struct { - key string - cursor int - withMatch bool - match string - }{ - key: args[0], - } - if ok := optIntErr(c, args[1], &opts.cursor, msgInvalidCursor); !ok { - return - } - args = args[2:] - - // MATCH and COUNT options - for len(args) > 0 { - if strings.ToLower(args[0]) == "count" { - // we do nothing with count - if len(args) < 2 { - setDirty(c) - c.WriteError(msgSyntaxError) - return - } - _, err := strconv.Atoi(args[1]) - if err != nil { - setDirty(c) - c.WriteError(msgInvalidInt) - return - } - args = args[2:] - continue - } - if strings.ToLower(args[0]) == "match" { - if len(args) < 2 { - setDirty(c) - c.WriteError(msgSyntaxError) - return - } - opts.withMatch = true - opts.match, args = args[1], args[2:] - continue - } - setDirty(c) - c.WriteError(msgSyntaxError) - return - } - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - db := m.db(ctx.selectedDB) - // return _all_ (matched) keys every time - - if opts.cursor != 0 { - // Invalid cursor. - c.WriteLen(2) - c.WriteBulk("0") // no next cursor - c.WriteLen(0) // no elements - return - } - if db.exists(opts.key) && db.t(opts.key) != keyTypeHash { - c.WriteError(ErrWrongType.Error()) - return - } - - members := db.hashFields(opts.key) - if opts.withMatch { - members, _ = matchKeys(members, opts.match) - } - - c.WriteLen(2) - c.WriteBulk("0") // no next cursor - // HSCAN gives key, values. - c.WriteLen(len(members) * 2) - for _, k := range members { - c.WriteBulk(k) - c.WriteBulk(db.hashGet(opts.key, k)) - } - }) -} - -// HRANDFIELD -func (m *Miniredis) cmdHrandfield(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, between(1, 3)) { - return - } - - opts := struct { - key string - count int - countSet bool - withValues bool - }{ - key: args[0], - } - - if len(args) > 1 { - if ok := optIntErr(c, args[1], &opts.count, msgInvalidInt); !ok { - return - } - opts.countSet = true - } - - if len(args) == 3 { - if strings.ToLower(args[2]) == "withvalues" { - opts.withValues = true - } else { - setDirty(c) - c.WriteError(msgSyntaxError) - return - } - } - - withTx(m, c, func(peer *server.Peer, ctx *connCtx) { - db := m.db(ctx.selectedDB) - members := db.hashFields(opts.key) - m.shuffle(members) - - if !opts.countSet { - // > When called with just the key argument, return a random field from the - // hash value stored at key. - if len(members) == 0 { - peer.WriteNull() - return - } - peer.WriteBulk(members[0]) - return - } - - if len(members) > abs(opts.count) { - members = members[:abs(opts.count)] - } - switch { - case opts.count >= 0: - // if count is positive there can't be duplicates, and the length is restricted - case opts.count < 0: - // if count is negative there can be duplicates, but length will match - if len(members) > 0 { - for len(members) < -opts.count { - members = append(members, members[m.randIntn(len(members))]) - } - } - } - - if opts.withValues { - peer.WriteMapLen(len(members)) - for _, m := range members { - peer.WriteBulk(m) - peer.WriteBulk(db.hashGet(opts.key, m)) - } - return - } - peer.WriteLen(len(members)) - for _, m := range members { - peer.WriteBulk(m) - } - }) -} - -// HEXPIRE -func (m *Miniredis) cmdHexpire(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, atLeast(5)) { - return - } - - opts, err := parseHExpireArgs(args) - if err != "" { - setDirty(c) - c.WriteError(err) - return - } - - withTx(m, c, func(peer *server.Peer, ctx *connCtx) { - db := m.db(ctx.selectedDB) - - if _, ok := db.keys[opts.key]; !ok { - c.WriteLen(len(opts.fields)) - for range opts.fields { - c.WriteInt(-2) - } - return - } - - if db.t(opts.key) != keyTypeHash { - c.WriteError(msgWrongType) - return - } - - fieldTTLs := db.hashTTLs[opts.key] - if fieldTTLs == nil { - fieldTTLs = map[string]time.Duration{} - db.hashTTLs[opts.key] = fieldTTLs - } - - c.WriteLen(len(opts.fields)) - for _, field := range opts.fields { - if _, ok := db.hashKeys[opts.key][field]; !ok { - c.WriteInt(-2) - continue - } - - currentTtl, ok := fieldTTLs[field] - newTTL := time.Duration(opts.ttl) * time.Second - - // NX -- For each specified field, - // set expiration only when the field has no expiration. - if opts.nx && ok { - c.WriteInt(0) - continue - } - - // XX -- For each specified field, - // set expiration only when the field has an existing expiration. - if opts.xx && !ok { - c.WriteInt(0) - continue - } - - // GT -- For each specified field, - // set expiration only when the new expiration is greater than current one. - if opts.gt && (!ok || newTTL <= currentTtl) { - c.WriteInt(0) - continue - } - - // LT -- For each specified field, - // set expiration only when the new expiration is less than current one. - if opts.lt && ok && newTTL >= currentTtl { - c.WriteInt(0) - continue - } - - fieldTTLs[field] = newTTL - c.WriteInt(1) - } - }) -} - -type hexpireOpts struct { - key string - ttl int - nx bool - xx bool - gt bool - lt bool - fields []string -} - -func parseHExpireArgs(args []string) (hexpireOpts, string) { - var opts hexpireOpts - opts.key = args[0] - - if err := optIntSimple(args[1], &opts.ttl); err != nil { - return hexpireOpts{}, err.Error() - } - - args = args[2:] - - for len(args) > 0 { - switch strings.ToLower(args[0]) { - case "nx": - opts.nx = true - args = args[1:] - case "xx": - opts.xx = true - args = args[1:] - case "gt": - opts.gt = true - args = args[1:] - case "lt": - opts.lt = true - args = args[1:] - case "fields": - var numFields int - if err := optIntSimple(args[1], &numFields); err != nil { - return hexpireOpts{}, msgNumFieldsInvalid - } - if numFields <= 0 { - return hexpireOpts{}, msgNumFieldsInvalid - } - - // FIELDS numFields field1 field2 ... - if len(args) < 2+numFields { - return hexpireOpts{}, msgNumFieldsParameter - } - - opts.fields = append([]string{}, args[2:2+numFields]...) - args = args[2+numFields:] - default: - return hexpireOpts{}, fmt.Sprintf(msgMandatoryArgument, "FIELDS") - } - } - - if opts.gt && opts.lt { - return hexpireOpts{}, msgGTandLT - } - - if opts.nx && (opts.xx || opts.gt || opts.lt) { - return hexpireOpts{}, msgNXandXXGTLT - } - - return opts, "" -} - -func abs(n int) int { - if n < 0 { - return -n - } - return n -} diff --git a/vendor/github.com/alicebob/miniredis/v2/cmd_hll.go b/vendor/github.com/alicebob/miniredis/v2/cmd_hll.go deleted file mode 100644 index 7bfc9504d..000000000 --- a/vendor/github.com/alicebob/miniredis/v2/cmd_hll.go +++ /dev/null @@ -1,71 +0,0 @@ -package miniredis - -import "github.com/alicebob/miniredis/v2/server" - -// commandsHll handles all hll related operations. -func commandsHll(m *Miniredis) { - m.srv.Register("PFADD", m.cmdPfadd) - m.srv.Register("PFCOUNT", m.cmdPfcount, server.ReadOnlyOption()) - m.srv.Register("PFMERGE", m.cmdPfmerge) -} - -// PFADD -func (m *Miniredis) cmdPfadd(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, atLeast(2)) { - return - } - - key, items := args[0], args[1:] - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - db := m.db(ctx.selectedDB) - - if db.exists(key) && db.t(key) != keyTypeHll { - c.WriteError(ErrNotValidHllValue.Error()) - return - } - - altered := db.hllAdd(key, items...) - c.WriteInt(altered) - }) -} - -// PFCOUNT -func (m *Miniredis) cmdPfcount(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, atLeast(1)) { - return - } - - keys := args - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - db := m.db(ctx.selectedDB) - - count, err := db.hllCount(keys) - if err != nil { - c.WriteError(err.Error()) - return - } - - c.WriteInt(count) - }) -} - -// PFMERGE -func (m *Miniredis) cmdPfmerge(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, atLeast(1)) { - return - } - - keys := args - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - db := m.db(ctx.selectedDB) - - if err := db.hllMerge(keys); err != nil { - c.WriteError(err.Error()) - return - } - c.WriteOK() - }) -} diff --git a/vendor/github.com/alicebob/miniredis/v2/cmd_info.go b/vendor/github.com/alicebob/miniredis/v2/cmd_info.go deleted file mode 100644 index 9fa847112..000000000 --- a/vendor/github.com/alicebob/miniredis/v2/cmd_info.go +++ /dev/null @@ -1,43 +0,0 @@ -package miniredis - -import ( - "fmt" - "strings" - - "github.com/alicebob/miniredis/v2/server" -) - -const ( - clientsSectionName = "clients" - clientsSectionContent = "# Clients\nconnected_clients:%d\r\n" - - statsSectionName = "stats" - statsSectionContent = "# Stats\ntotal_connections_received:%d\r\ntotal_commands_processed:%d\r\n" -) - -// Command 'INFO' from https://redis.io/commands/info/ -func (m *Miniredis) cmdInfo(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, between(0, 1)) { - return - } - var result string - if len(args) == 0 { - result = fmt.Sprintf(clientsSectionContent, m.Server().ClientsLen()) + fmt.Sprintf(statsSectionContent, m.Server().TotalConnections(), m.Server().TotalCommands()) - c.WriteBulk(result) - return - } - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - switch section := strings.ToLower(args[0]); section { - case clientsSectionName: - result = fmt.Sprintf(clientsSectionContent, m.Server().ClientsLen()) - case statsSectionName: - result = fmt.Sprintf(statsSectionContent, m.Server().TotalConnections(), m.Server().TotalCommands()) - default: - setDirty(c) - c.WriteError(fmt.Sprintf("section (%s) is not supported", section)) - return - } - c.WriteBulk(result) - }) -} diff --git a/vendor/github.com/alicebob/miniredis/v2/cmd_list.go b/vendor/github.com/alicebob/miniredis/v2/cmd_list.go deleted file mode 100644 index 9de16359d..000000000 --- a/vendor/github.com/alicebob/miniredis/v2/cmd_list.go +++ /dev/null @@ -1,931 +0,0 @@ -// Commands from https://redis.io/commands#list - -package miniredis - -import ( - "strconv" - "strings" - "time" - - "github.com/alicebob/miniredis/v2/server" -) - -type leftright int - -const ( - left leftright = iota - right -) - -// commandsList handles list commands (mostly L*) -func commandsList(m *Miniredis) { - m.srv.Register("BLPOP", m.cmdBlpop) - m.srv.Register("BRPOP", m.cmdBrpop) - m.srv.Register("BRPOPLPUSH", m.cmdBrpoplpush) - m.srv.Register("LINDEX", m.cmdLindex, server.ReadOnlyOption()) - m.srv.Register("LPOS", m.cmdLpos, server.ReadOnlyOption()) - m.srv.Register("LINSERT", m.cmdLinsert) - m.srv.Register("LLEN", m.cmdLlen, server.ReadOnlyOption()) - m.srv.Register("LPOP", m.cmdLpop) - m.srv.Register("LPUSH", m.cmdLpush) - m.srv.Register("LPUSHX", m.cmdLpushx) - m.srv.Register("LRANGE", m.cmdLrange, server.ReadOnlyOption()) - m.srv.Register("LREM", m.cmdLrem) - m.srv.Register("LSET", m.cmdLset) - m.srv.Register("LTRIM", m.cmdLtrim) - m.srv.Register("RPOP", m.cmdRpop) - m.srv.Register("RPOPLPUSH", m.cmdRpoplpush) - m.srv.Register("RPUSH", m.cmdRpush) - m.srv.Register("RPUSHX", m.cmdRpushx) - m.srv.Register("LMOVE", m.cmdLmove) - m.srv.Register("BLMOVE", m.cmdBlmove) -} - -// BLPOP -func (m *Miniredis) cmdBlpop(c *server.Peer, cmd string, args []string) { - m.cmdBXpop(c, cmd, args, left) -} - -// BRPOP -func (m *Miniredis) cmdBrpop(c *server.Peer, cmd string, args []string) { - m.cmdBXpop(c, cmd, args, right) -} - -func (m *Miniredis) cmdBXpop(c *server.Peer, cmd string, args []string, lr leftright) { - if !m.isValidCMD(c, cmd, args, atLeast(2)) { - return - } - - var opts struct { - keys []string - timeout time.Duration - } - - if ok := optDuration(c, args[len(args)-1], &opts.timeout); !ok { - return - } - opts.keys = args[:len(args)-1] - - blocking( - m, - c, - opts.timeout, - func(c *server.Peer, ctx *connCtx) bool { - db := m.db(ctx.selectedDB) - for _, key := range opts.keys { - if !db.exists(key) { - continue - } - if db.t(key) != keyTypeList { - c.WriteError(msgWrongType) - return true - } - - if len(db.listKeys[key]) == 0 { - continue - } - c.WriteLen(2) - c.WriteBulk(key) - var v string - switch lr { - case left: - v = db.listLpop(key) - case right: - v = db.listPop(key) - } - c.WriteBulk(v) - return true - } - return false - }, - func(c *server.Peer) { - // timeout - c.WriteLen(-1) - }, - ) -} - -// LINDEX -func (m *Miniredis) cmdLindex(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, exactly(2)) { - return - } - - key, offsets := args[0], args[1] - - offset, err := strconv.Atoi(offsets) - if err != nil || offsets == "-0" { - setDirty(c) - c.WriteError(msgInvalidInt) - return - } - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - db := m.db(ctx.selectedDB) - - t, ok := db.keys[key] - if !ok { - // No such key - c.WriteNull() - return - } - if t != keyTypeList { - c.WriteError(msgWrongType) - return - } - - l := db.listKeys[key] - if offset < 0 { - offset = len(l) + offset - } - if offset < 0 || offset > len(l)-1 { - c.WriteNull() - return - } - c.WriteBulk(l[offset]) - }) -} - -// LPOS key element [RANK rank] [COUNT num-matches] [MAXLEN len] -func (m *Miniredis) cmdLpos(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, atLeast(2)) { - return - } - - // Extract options from arguments if present. - // - // Redis allows duplicate options and uses the last specified. - // `LPOS key term RANK 1 RANK 2` is effectively the same as - // `LPOS key term RANK 2` - if len(args)%2 == 1 { - setDirty(c) - c.WriteError(msgSyntaxError) - return - } - rank, count := 1, 1 // Default values - var maxlen int // Default value is the list length (see below) - var countSpecified, maxlenSpecified bool - if len(args) > 2 { - for i := 2; i < len(args); i++ { - if i%2 == 0 { - val := args[i+1] - var err error - switch strings.ToLower(args[i]) { - case "rank": - if rank, err = strconv.Atoi(val); err != nil { - setDirty(c) - c.WriteError(msgInvalidInt) - return - } - if rank == 0 { - setDirty(c) - c.WriteError(msgRankIsZero) - return - } - case "count": - countSpecified = true - if count, err = strconv.Atoi(val); err != nil || count < 0 { - setDirty(c) - c.WriteError(msgCountIsNegative) - return - } - case "maxlen": - maxlenSpecified = true - if maxlen, err = strconv.Atoi(val); err != nil || maxlen < 0 { - setDirty(c) - c.WriteError(msgMaxLengthIsNegative) - return - } - default: - setDirty(c) - c.WriteError(msgSyntaxError) - return - } - } - } - } - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - db := m.db(ctx.selectedDB) - key, element := args[0], args[1] - t, ok := db.keys[key] - if !ok { - // No such key - c.WriteNull() - return - } - if t != keyTypeList { - c.WriteError(msgWrongType) - return - } - l := db.listKeys[key] - - // RANK cannot be zero (see above). - // If RANK is positive search forward (left to right). - // If RANK is negative search backward (right to left). - // Iterator returns true to continue iterating. - iterate := func(iterator func(i int, e string) bool) { - comparisons := len(l) - // Only use max length if specified, not zero, and less than total length. - // When max length is specified, but is zero, this means "unlimited". - if maxlenSpecified && maxlen != 0 && maxlen < len(l) { - comparisons = maxlen - } - if rank > 0 { - for i := 0; i < comparisons; i++ { - if resume := iterator(i, l[i]); !resume { - return - } - } - } else if rank < 0 { - start := len(l) - 1 - end := len(l) - comparisons - for i := start; i >= end; i-- { - if resume := iterator(i, l[i]); !resume { - return - } - } - } - } - - var currentRank, currentCount int - vals := make([]int, 0, count) - iterate(func(i int, e string) bool { - if e == element { - currentRank++ - // Only collect values only after surpassing the absolute value of rank. - if rank > 0 && currentRank < rank { - return true - } - if rank < 0 && currentRank < -rank { - return true - } - vals = append(vals, i) - currentCount++ - if currentCount == count { - return false - } - } - return true - }) - - if !countSpecified && len(vals) == 0 { - c.WriteNull() - return - } - if !countSpecified && len(vals) == 1 { - c.WriteInt(vals[0]) - return - } - c.WriteLen(len(vals)) - for _, val := range vals { - c.WriteInt(val) - } - }) -} - -// LINSERT -func (m *Miniredis) cmdLinsert(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, exactly(4)) { - return - } - - key := args[0] - where := 0 - switch strings.ToLower(args[1]) { - case "before": - where = -1 - case "after": - where = +1 - default: - setDirty(c) - c.WriteError(msgSyntaxError) - return - } - pivot := args[2] - value := args[3] - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - db := m.db(ctx.selectedDB) - - t, ok := db.keys[key] - if !ok { - // No such key - c.WriteInt(0) - return - } - if t != keyTypeList { - c.WriteError(msgWrongType) - return - } - - l := db.listKeys[key] - for i, el := range l { - if el != pivot { - continue - } - - if where < 0 { - l = append(l[:i], append(listKey{value}, l[i:]...)...) - } else { - if i == len(l)-1 { - l = append(l, value) - } else { - l = append(l[:i+1], append(listKey{value}, l[i+1:]...)...) - } - } - db.listKeys[key] = l - db.incr(key) - c.WriteInt(len(l)) - return - } - c.WriteInt(-1) - }) -} - -// LLEN -func (m *Miniredis) cmdLlen(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, exactly(1)) { - return - } - - key := args[0] - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - db := m.db(ctx.selectedDB) - - t, ok := db.keys[key] - if !ok { - // No such key. That's zero length. - c.WriteInt(0) - return - } - if t != keyTypeList { - c.WriteError(msgWrongType) - return - } - - c.WriteInt(len(db.listKeys[key])) - }) -} - -// LPOP -func (m *Miniredis) cmdLpop(c *server.Peer, cmd string, args []string) { - m.cmdXpop(c, cmd, args, left) -} - -// RPOP -func (m *Miniredis) cmdRpop(c *server.Peer, cmd string, args []string) { - m.cmdXpop(c, cmd, args, right) -} - -func (m *Miniredis) cmdXpop(c *server.Peer, cmd string, args []string, lr leftright) { - if !m.isValidCMD(c, cmd, args, atLeast(1)) { - return - } - - var opts struct { - key string - withCount bool - count int - } - - opts.key, args = args[0], args[1:] - if len(args) > 0 { - if ok := optInt(c, args[0], &opts.count); !ok { - return - } - if opts.count < 0 { - setDirty(c) - c.WriteError(msgOutOfRange) - return - } - opts.withCount = true - args = args[1:] - } - if len(args) > 0 { - setDirty(c) - c.WriteError(errWrongNumber(cmd)) - return - } - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - db := m.db(ctx.selectedDB) - - if !db.exists(opts.key) { - // non-existing key is fine - if opts.withCount && !c.Resp3 { - // zero-length list in this specific case. Looks like a redis bug to me. - c.WriteLen(-1) - return - } - c.WriteNull() - return - } - if db.t(opts.key) != keyTypeList { - c.WriteError(msgWrongType) - return - } - - if opts.withCount { - var popped []string - for opts.count > 0 && len(db.listKeys[opts.key]) > 0 { - switch lr { - case left: - popped = append(popped, db.listLpop(opts.key)) - case right: - popped = append(popped, db.listPop(opts.key)) - } - opts.count -= 1 - } - c.WriteStrings(popped) - return - } - - var elem string - switch lr { - case left: - elem = db.listLpop(opts.key) - case right: - elem = db.listPop(opts.key) - } - c.WriteBulk(elem) - }) -} - -// LPUSH -func (m *Miniredis) cmdLpush(c *server.Peer, cmd string, args []string) { - m.cmdXpush(c, cmd, args, left) -} - -// RPUSH -func (m *Miniredis) cmdRpush(c *server.Peer, cmd string, args []string) { - m.cmdXpush(c, cmd, args, right) -} - -func (m *Miniredis) cmdXpush(c *server.Peer, cmd string, args []string, lr leftright) { - if !m.isValidCMD(c, cmd, args, atLeast(2)) { - return - } - - key, args := args[0], args[1:] - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - db := m.db(ctx.selectedDB) - - if db.exists(key) && db.t(key) != keyTypeList { - c.WriteError(msgWrongType) - return - } - - var newLen int - for _, value := range args { - switch lr { - case left: - newLen = db.listLpush(key, value) - case right: - newLen = db.listPush(key, value) - } - } - c.WriteInt(newLen) - }) -} - -// LPUSHX -func (m *Miniredis) cmdLpushx(c *server.Peer, cmd string, args []string) { - m.cmdXpushx(c, cmd, args, left) -} - -// RPUSHX -func (m *Miniredis) cmdRpushx(c *server.Peer, cmd string, args []string) { - m.cmdXpushx(c, cmd, args, right) -} - -func (m *Miniredis) cmdXpushx(c *server.Peer, cmd string, args []string, lr leftright) { - if !m.isValidCMD(c, cmd, args, atLeast(2)) { - return - } - - key, args := args[0], args[1:] - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - db := m.db(ctx.selectedDB) - - if !db.exists(key) { - c.WriteInt(0) - return - } - if db.t(key) != keyTypeList { - c.WriteError(msgWrongType) - return - } - - var newLen int - for _, value := range args { - switch lr { - case left: - newLen = db.listLpush(key, value) - case right: - newLen = db.listPush(key, value) - } - } - c.WriteInt(newLen) - }) -} - -// LRANGE -func (m *Miniredis) cmdLrange(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, exactly(3)) { - return - } - - opts := struct { - key string - start int - end int - }{ - key: args[0], - } - if ok := optInt(c, args[1], &opts.start); !ok { - return - } - if ok := optInt(c, args[2], &opts.end); !ok { - return - } - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - db := m.db(ctx.selectedDB) - - if t, ok := db.keys[opts.key]; ok && t != keyTypeList { - c.WriteError(msgWrongType) - return - } - - l := db.listKeys[opts.key] - if len(l) == 0 { - c.WriteLen(0) - return - } - - rs, re := redisRange(len(l), opts.start, opts.end, false) - c.WriteLen(re - rs) - for _, el := range l[rs:re] { - c.WriteBulk(el) - } - }) -} - -// LREM -func (m *Miniredis) cmdLrem(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, exactly(3)) { - return - } - - var opts struct { - key string - count int - value string - } - opts.key = args[0] - if ok := optInt(c, args[1], &opts.count); !ok { - return - } - opts.value = args[2] - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - db := m.db(ctx.selectedDB) - - if !db.exists(opts.key) { - c.WriteInt(0) - return - } - if db.t(opts.key) != keyTypeList { - c.WriteError(msgWrongType) - return - } - - l := db.listKeys[opts.key] - if opts.count < 0 { - reverseSlice(l) - } - deleted := 0 - newL := []string{} - toDelete := len(l) - if opts.count < 0 { - toDelete = -opts.count - } - if opts.count > 0 { - toDelete = opts.count - } - for _, el := range l { - if el == opts.value { - if toDelete > 0 { - deleted++ - toDelete-- - continue - } - } - newL = append(newL, el) - } - if opts.count < 0 { - reverseSlice(newL) - } - if len(newL) == 0 { - db.del(opts.key, true) - } else { - db.listKeys[opts.key] = newL - db.incr(opts.key) - } - - c.WriteInt(deleted) - }) -} - -// LSET -func (m *Miniredis) cmdLset(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, exactly(3)) { - return - } - - var opts struct { - key string - index int - value string - } - opts.key = args[0] - if ok := optInt(c, args[1], &opts.index); !ok { - return - } - opts.value = args[2] - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - db := m.db(ctx.selectedDB) - - if !db.exists(opts.key) { - c.WriteError(msgKeyNotFound) - return - } - if db.t(opts.key) != keyTypeList { - c.WriteError(msgWrongType) - return - } - - l := db.listKeys[opts.key] - index := opts.index - if index < 0 { - index = len(l) + index - } - if index < 0 || index > len(l)-1 { - c.WriteError(msgOutOfRange) - return - } - l[index] = opts.value - db.incr(opts.key) - - c.WriteOK() - }) -} - -// LTRIM -func (m *Miniredis) cmdLtrim(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, exactly(3)) { - return - } - - var opts struct { - key string - start int - end int - } - - opts.key = args[0] - if ok := optInt(c, args[1], &opts.start); !ok { - return - } - if ok := optInt(c, args[2], &opts.end); !ok { - return - } - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - db := m.db(ctx.selectedDB) - - t, ok := db.keys[opts.key] - if !ok { - c.WriteOK() - return - } - if t != keyTypeList { - c.WriteError(msgWrongType) - return - } - - l := db.listKeys[opts.key] - rs, re := redisRange(len(l), opts.start, opts.end, false) - l = l[rs:re] - if len(l) == 0 { - db.del(opts.key, true) - } else { - db.listKeys[opts.key] = l - db.incr(opts.key) - } - c.WriteOK() - }) -} - -// RPOPLPUSH -func (m *Miniredis) cmdRpoplpush(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, exactly(2)) { - return - } - - src, dst := args[0], args[1] - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - db := m.db(ctx.selectedDB) - - if !db.exists(src) { - c.WriteNull() - return - } - if db.t(src) != keyTypeList || (db.exists(dst) && db.t(dst) != keyTypeList) { - c.WriteError(msgWrongType) - return - } - elem := db.listPop(src) - db.listLpush(dst, elem) - c.WriteBulk(elem) - }) -} - -// BRPOPLPUSH -func (m *Miniredis) cmdBrpoplpush(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, exactly(3)) { - return - } - - var opts struct { - src string - dst string - timeout time.Duration - } - opts.src = args[0] - opts.dst = args[1] - if ok := optDuration(c, args[2], &opts.timeout); !ok { - return - } - - blocking( - m, - c, - opts.timeout, - func(c *server.Peer, ctx *connCtx) bool { - db := m.db(ctx.selectedDB) - - if !db.exists(opts.src) { - return false - } - if db.t(opts.src) != keyTypeList || (db.exists(opts.dst) && db.t(opts.dst) != keyTypeList) { - c.WriteError(msgWrongType) - return true - } - if len(db.listKeys[opts.src]) == 0 { - return false - } - elem := db.listPop(opts.src) - db.listLpush(opts.dst, elem) - c.WriteBulk(elem) - return true - }, - func(c *server.Peer) { - // timeout - c.WriteLen(-1) - }, - ) -} - -// LMOVE -func (m *Miniredis) cmdLmove(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, exactly(4)) { - return - } - - opts := struct { - src string - dst string - srcDir string - dstDir string - }{ - src: args[0], - dst: args[1], - srcDir: strings.ToLower(args[2]), - dstDir: strings.ToLower(args[3]), - } - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - db := m.db(ctx.selectedDB) - - if !db.exists(opts.src) { - c.WriteNull() - return - } - if db.t(opts.src) != keyTypeList || (db.exists(opts.dst) && db.t(opts.dst) != keyTypeList) { - c.WriteError(msgWrongType) - return - } - var elem string - switch opts.srcDir { - case "left": - elem = db.listLpop(opts.src) - case "right": - elem = db.listPop(opts.src) - default: - c.WriteError(msgSyntaxError) - return - } - - switch opts.dstDir { - case "left": - db.listLpush(opts.dst, elem) - case "right": - db.listPush(opts.dst, elem) - default: - c.WriteError(msgSyntaxError) - return - } - c.WriteBulk(elem) - }) -} - -// BLMOVE -func (m *Miniredis) cmdBlmove(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, exactly(5)) { - return - } - - opts := struct { - src string - dst string - srcDir string - dstDir string - timeout time.Duration - }{ - src: args[0], - dst: args[1], - srcDir: strings.ToLower(args[2]), - dstDir: strings.ToLower(args[3]), - } - if ok := optDuration(c, args[len(args)-1], &opts.timeout); !ok { - return - } - - blocking( - m, - c, - opts.timeout, - func(c *server.Peer, ctx *connCtx) bool { - db := m.db(ctx.selectedDB) - - if !db.exists(opts.src) { - return false - } - if db.t(opts.src) != keyTypeList || (db.exists(opts.dst) && db.t(opts.dst) != keyTypeList) { - c.WriteError(msgWrongType) - return true - } - - var ( - elem string - ttl = db.ttl[opts.src] // in case we empty the array (deletes the entry) - ) - switch opts.srcDir { - case "left": - elem = db.listLpop(opts.src) - case "right": - elem = db.listPop(opts.src) - default: - c.WriteError(msgSyntaxError) - return true - } - - switch opts.dstDir { - case "left": - db.listLpush(opts.dst, elem) - case "right": - db.listPush(opts.dst, elem) - default: - c.WriteError(msgSyntaxError) - return true - } - if ttl > 0 { - db.ttl[opts.dst] = ttl - } - - c.WriteBulk(elem) - return true - }, - func(c *server.Peer) { - // timeout - c.WriteLen(-1) - }, - ) -} diff --git a/vendor/github.com/alicebob/miniredis/v2/cmd_object.go b/vendor/github.com/alicebob/miniredis/v2/cmd_object.go deleted file mode 100644 index e8117c529..000000000 --- a/vendor/github.com/alicebob/miniredis/v2/cmd_object.go +++ /dev/null @@ -1,50 +0,0 @@ -package miniredis - -import ( - "fmt" - "strings" - - "github.com/alicebob/miniredis/v2/server" -) - -// commandsObject handles all object operations. -func commandsObject(m *Miniredis) { - m.srv.Register("OBJECT", m.cmdObject) -} - -// OBJECT -func (m *Miniredis) cmdObject(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, atLeast(1)) { - return - } - - switch sub := strings.ToLower(args[0]); sub { - case "idletime": - m.cmdObjectIdletime(c, args[1:]) - default: - setDirty(c) - c.WriteError(fmt.Sprintf(msgFObjectUsage, sub)) - } -} - -// OBJECT IDLETIME -func (m *Miniredis) cmdObjectIdletime(c *server.Peer, args []string) { - if len(args) != 1 { - setDirty(c) - c.WriteError(errWrongNumber("object|idletime")) - return - } - key := args[0] - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - db := m.db(ctx.selectedDB) - - t, ok := db.lru[key] - if !ok { - c.WriteNull() - return - } - - c.WriteInt(int(db.master.effectiveNow().Sub(t).Seconds())) - }) -} diff --git a/vendor/github.com/alicebob/miniredis/v2/cmd_pubsub.go b/vendor/github.com/alicebob/miniredis/v2/cmd_pubsub.go deleted file mode 100644 index 571431e8e..000000000 --- a/vendor/github.com/alicebob/miniredis/v2/cmd_pubsub.go +++ /dev/null @@ -1,254 +0,0 @@ -// Commands from https://redis.io/commands#pubsub - -package miniredis - -import ( - "fmt" - "strings" - - "github.com/alicebob/miniredis/v2/server" -) - -// commandsPubsub handles all PUB/SUB operations. -func commandsPubsub(m *Miniredis) { - m.srv.Register("SUBSCRIBE", m.cmdSubscribe) - m.srv.Register("UNSUBSCRIBE", m.cmdUnsubscribe) - m.srv.Register("PSUBSCRIBE", m.cmdPsubscribe) - m.srv.Register("PUNSUBSCRIBE", m.cmdPunsubscribe) - m.srv.Register("PUBLISH", m.cmdPublish) - m.srv.Register("PUBSUB", m.cmdPubSub) -} - -// SUBSCRIBE -func (m *Miniredis) cmdSubscribe(c *server.Peer, cmd string, args []string) { - if len(args) < 1 { - setDirty(c) - c.WriteError(errWrongNumber(cmd)) - return - } - if !m.handleAuth(c) { - return - } - ctx := getCtx(c) - if ctx.nested { - c.WriteError(msgNotFromScripts(ctx.nestedSHA)) - return - } - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - sub := m.subscribedState(c) - for _, channel := range args { - n := sub.Subscribe(channel) - c.Block(func(w *server.Writer) { - w.WritePushLen(3) - w.WriteBulk("subscribe") - w.WriteBulk(channel) - w.WriteInt(n) - }) - } - }) -} - -// UNSUBSCRIBE -func (m *Miniredis) cmdUnsubscribe(c *server.Peer, cmd string, args []string) { - if !m.handleAuth(c) { - return - } - ctx := getCtx(c) - if ctx.nested { - c.WriteError(msgNotFromScripts(ctx.nestedSHA)) - return - } - - channels := args - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - sub := m.subscribedState(c) - - if len(channels) == 0 { - channels = sub.Channels() - } - - // there is no de-duplication - for _, channel := range channels { - n := sub.Unsubscribe(channel) - c.Block(func(w *server.Writer) { - w.WritePushLen(3) - w.WriteBulk("unsubscribe") - w.WriteBulk(channel) - w.WriteInt(n) - }) - } - if len(channels) == 0 { - // special case: there is always a reply - c.Block(func(w *server.Writer) { - w.WritePushLen(3) - w.WriteBulk("unsubscribe") - w.WriteNull() - w.WriteInt(0) - }) - } - - if sub.Count() == 0 { - endSubscriber(m, c) - } - }) -} - -// PSUBSCRIBE -func (m *Miniredis) cmdPsubscribe(c *server.Peer, cmd string, args []string) { - if len(args) < 1 { - setDirty(c) - c.WriteError(errWrongNumber(cmd)) - return - } - if !m.handleAuth(c) { - return - } - ctx := getCtx(c) - if ctx.nested { - c.WriteError(msgNotFromScripts(ctx.nestedSHA)) - return - } - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - sub := m.subscribedState(c) - for _, pat := range args { - n := sub.Psubscribe(pat) - c.Block(func(w *server.Writer) { - w.WritePushLen(3) - w.WriteBulk("psubscribe") - w.WriteBulk(pat) - w.WriteInt(n) - }) - } - }) -} - -// PUNSUBSCRIBE -func (m *Miniredis) cmdPunsubscribe(c *server.Peer, cmd string, args []string) { - if !m.handleAuth(c) { - return - } - ctx := getCtx(c) - if ctx.nested { - c.WriteError(msgNotFromScripts(ctx.nestedSHA)) - return - } - - patterns := args - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - sub := m.subscribedState(c) - - if len(patterns) == 0 { - patterns = sub.Patterns() - } - - // there is no de-duplication - for _, pat := range patterns { - n := sub.Punsubscribe(pat) - c.Block(func(w *server.Writer) { - w.WritePushLen(3) - w.WriteBulk("punsubscribe") - w.WriteBulk(pat) - w.WriteInt(n) - }) - } - if len(patterns) == 0 { - // special case: there is always a reply - c.Block(func(w *server.Writer) { - w.WritePushLen(3) - w.WriteBulk("punsubscribe") - w.WriteNull() - w.WriteInt(0) - }) - } - - if sub.Count() == 0 { - endSubscriber(m, c) - } - }) -} - -// PUBLISH -func (m *Miniredis) cmdPublish(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, exactly(2)) { - return - } - - channel, mesg := args[0], args[1] - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - c.WriteInt(m.publish(channel, mesg)) - }) -} - -// PUBSUB -func (m *Miniredis) cmdPubSub(c *server.Peer, cmd string, args []string) { - if len(args) < 1 { - setDirty(c) - c.WriteError(errWrongNumber(cmd)) - return - } - - if m.checkPubsub(c, cmd) { - return - } - - subcommand := strings.ToUpper(args[0]) - subargs := args[1:] - var argsOk bool - - switch subcommand { - case "CHANNELS": - argsOk = len(subargs) < 2 - case "NUMSUB": - argsOk = true - case "NUMPAT": - argsOk = len(subargs) == 0 - default: - setDirty(c) - c.WriteError(fmt.Sprintf(msgFPubsubUsageSimple, subcommand)) - return - } - - if !argsOk { - setDirty(c) - c.WriteError(fmt.Sprintf(msgFPubsubUsage, subcommand)) - return - } - - if !m.handleAuth(c) { - return - } - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - switch subcommand { - case "CHANNELS": - pat := "" - if len(subargs) == 1 { - pat = subargs[0] - } - - allsubs := m.allSubscribers() - channels := activeChannels(allsubs, pat) - - c.WriteLen(len(channels)) - for _, channel := range channels { - c.WriteBulk(channel) - } - - case "NUMSUB": - subs := m.allSubscribers() - c.WriteLen(len(subargs) * 2) - for _, channel := range subargs { - c.WriteBulk(channel) - c.WriteInt(countSubs(subs, channel)) - } - - case "NUMPAT": - c.WriteInt(countPsubs(m.allSubscribers())) - } - }) -} diff --git a/vendor/github.com/alicebob/miniredis/v2/cmd_scripting.go b/vendor/github.com/alicebob/miniredis/v2/cmd_scripting.go deleted file mode 100644 index 32705b858..000000000 --- a/vendor/github.com/alicebob/miniredis/v2/cmd_scripting.go +++ /dev/null @@ -1,346 +0,0 @@ -package miniredis - -import ( - "crypto/sha1" - "encoding/hex" - "fmt" - "io" - "strconv" - "strings" - "sync" - - lua "github.com/yuin/gopher-lua" - "github.com/yuin/gopher-lua/parse" - - luajson "github.com/alicebob/miniredis/v2/gopher-json" - "github.com/alicebob/miniredis/v2/server" -) - -func commandsScripting(m *Miniredis) { - m.srv.Register("EVAL", m.cmdEval) - m.srv.Register("EVAL_RO", m.cmdEvalro, server.ReadOnlyOption()) - m.srv.Register("EVALSHA", m.cmdEvalsha) - m.srv.Register("EVALSHA_RO", m.cmdEvalshaRo, server.ReadOnlyOption()) - m.srv.Register("SCRIPT", m.cmdScript) -} - -var ( - parsedScripts = sync.Map{} -) - -// Execute lua. Needs to run m.Lock()ed, from within withTx(). -// Returns true if the lua was OK (and hence should be cached). -func (m *Miniredis) runLuaScript(c *server.Peer, sha, script string, readOnly bool, args []string) bool { - l := lua.NewState(lua.Options{SkipOpenLibs: true}) - defer l.Close() - - // Taken from the go-lua manual - for _, pair := range []struct { - n string - f lua.LGFunction - }{ - {lua.LoadLibName, lua.OpenPackage}, - {lua.BaseLibName, lua.OpenBase}, - {lua.CoroutineLibName, lua.OpenCoroutine}, - {lua.TabLibName, lua.OpenTable}, - {lua.StringLibName, lua.OpenString}, - {lua.MathLibName, lua.OpenMath}, - {lua.DebugLibName, lua.OpenDebug}, - } { - if err := l.CallByParam(lua.P{ - Fn: l.NewFunction(pair.f), - NRet: 0, - Protect: true, - }, lua.LString(pair.n)); err != nil { - panic(err) - } - } - - luajson.Preload(l) - requireGlobal(l, "cjson", "json") - - // set global variable KEYS - keysTable := l.NewTable() - keysS, args := args[0], args[1:] - keysLen, err := strconv.Atoi(keysS) - if err != nil { - c.WriteError(msgInvalidInt) - return false - } - if keysLen < 0 { - c.WriteError(msgNegativeKeysNumber) - return false - } - if keysLen > len(args) { - c.WriteError(msgInvalidKeysNumber) - return false - } - keys, args := args[:keysLen], args[keysLen:] - for i, k := range keys { - l.RawSet(keysTable, lua.LNumber(i+1), lua.LString(k)) - } - l.SetGlobal("KEYS", keysTable) - - argvTable := l.NewTable() - for i, a := range args { - l.RawSet(argvTable, lua.LNumber(i+1), lua.LString(a)) - } - l.SetGlobal("ARGV", argvTable) - - redisFuncs, redisConstants := mkLua(m.srv, c, sha, readOnly) - // Register command handlers - l.Push(l.NewFunction(func(l *lua.LState) int { - mod := l.RegisterModule("redis", redisFuncs).(*lua.LTable) - for k, v := range redisConstants { - mod.RawSetString(k, v) - } - l.Push(mod) - return 1 - })) - l.RegisterModule("os", mkLuaOS()) - - _ = doScript(l, protectGlobals) - - l.Push(lua.LString("redis")) - l.Call(1, 0) - - // lua can call redis.setresp(...), but it's tmp state. - oldresp := c.Resp3 - if err := doScript(l, script); err != nil { - c.WriteError(err.Error()) - return false - } - - luaToRedis(l, c, l.Get(1)) - c.Resp3 = oldresp - c.SwitchResp3 = nil - return true -} - -// doScript pre-compiles the given script into a Lua prototype, -// then executes the pre-compiled function against the given lua state. -// -// This is thread-safe. -func doScript(l *lua.LState, script string) error { - proto, err := compile(script) - if err != nil { - return fmt.Errorf(errLuaParseError(err)) - } - - lfunc := l.NewFunctionFromProto(proto) - l.Push(lfunc) - if err := l.PCall(0, lua.MultRet, nil); err != nil { - // ensure we wrap with the correct format. - return fmt.Errorf(errLuaParseError(err)) - } - - return nil -} - -func compile(script string) (*lua.FunctionProto, error) { - if val, ok := parsedScripts.Load(script); ok { - return val.(*lua.FunctionProto), nil - } - chunk, err := parse.Parse(strings.NewReader(script), "") - if err != nil { - return nil, err - } - proto, err := lua.Compile(chunk, "") - if err != nil { - return nil, err - } - parsedScripts.Store(script, proto) - return proto, nil -} - -// Shared implementation for EVAL and EVALRO -func (m *Miniredis) cmdEvalShared(c *server.Peer, cmd string, readOnly bool, args []string) { - if !m.isValidCMD(c, cmd, args, atLeast(2)) { - return - } - - ctx := getCtx(c) - if ctx.nested { - c.WriteError(msgNotFromScripts(ctx.nestedSHA)) - return - } - - script, args := args[0], args[1:] - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - sha := sha1Hex(script) - ok := m.runLuaScript(c, sha, script, readOnly, args) - if ok { - m.scripts[sha] = script - } - }) -} - -// Wrapper function for EVAL command -func (m *Miniredis) cmdEval(c *server.Peer, cmd string, args []string) { - m.cmdEvalShared(c, cmd, false, args) -} - -// Shared implementation for EVALSHA and EVALSHA_RO -func (m *Miniredis) cmdEvalshaShared(c *server.Peer, cmd string, readOnly bool, args []string) { - if !m.isValidCMD(c, cmd, args, atLeast(2)) { - return - } - - ctx := getCtx(c) - if ctx.nested { - c.WriteError(msgNotFromScripts(ctx.nestedSHA)) - return - } - - sha, args := args[0], args[1:] - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - script, ok := m.scripts[sha] - if !ok { - c.WriteError(msgNoScriptFound) - return - } - - m.runLuaScript(c, sha, script, readOnly, args) - }) -} - -// Wrapper function for EVALSHA command -func (m *Miniredis) cmdEvalsha(c *server.Peer, cmd string, args []string) { - m.cmdEvalshaShared(c, cmd, false, args) -} - -// Wrapper function for EVALRO command -func (m *Miniredis) cmdEvalro(c *server.Peer, cmd string, args []string) { - m.cmdEvalShared(c, cmd, true, args) -} - -// Wrapper function for EVALSHA_RO command -func (m *Miniredis) cmdEvalshaRo(c *server.Peer, cmd string, args []string) { - m.cmdEvalshaShared(c, cmd, true, args) -} - -func (m *Miniredis) cmdScript(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, atLeast(1)) { - return - } - - ctx := getCtx(c) - if ctx.nested { - c.WriteError(msgNotFromScripts(ctx.nestedSHA)) - return - } - - var opts struct { - subcmd string - script string - } - - opts.subcmd, args = args[0], args[1:] - - switch strings.ToLower(opts.subcmd) { - case "load": - if len(args) != 1 { - setDirty(c) - c.WriteError(fmt.Sprintf(msgFScriptUsage, "LOAD")) - return - } - opts.script = args[0] - case "exists": - if len(args) == 0 { - setDirty(c) - c.WriteError(errWrongNumber("script|exists")) - return - } - case "flush": - if len(args) == 1 { - switch strings.ToUpper(args[0]) { - case "SYNC", "ASYNC": - args = args[1:] - default: - } - } - if len(args) != 0 { - setDirty(c) - c.WriteError(msgScriptFlush) - return - } - default: - setDirty(c) - c.WriteError(fmt.Sprintf(msgFScriptUsageSimple, strings.ToUpper(opts.subcmd))) - return - } - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - switch strings.ToLower(opts.subcmd) { - case "load": - if _, err := parse.Parse(strings.NewReader(opts.script), "user_script"); err != nil { - c.WriteError(errLuaParseError(err)) - return - } - sha := sha1Hex(opts.script) - m.scripts[sha] = opts.script - c.WriteBulk(sha) - case "exists": - c.WriteLen(len(args)) - for _, arg := range args { - if _, ok := m.scripts[arg]; ok { - c.WriteInt(1) - } else { - c.WriteInt(0) - } - } - case "flush": - m.scripts = map[string]string{} - c.WriteOK() - } - }) -} - -func sha1Hex(s string) string { - h := sha1.New() - io.WriteString(h, s) - return hex.EncodeToString(h.Sum(nil)) -} - -// requireGlobal imports module modName into the global namespace with the -// identifier id. panics if an error results from the function execution -func requireGlobal(l *lua.LState, id, modName string) { - if err := l.CallByParam(lua.P{ - Fn: l.GetGlobal("require"), - NRet: 1, - Protect: true, - }, lua.LString(modName)); err != nil { - panic(err) - } - mod := l.Get(-1) - l.Pop(1) - - l.SetGlobal(id, mod) -} - -// the following script protects globals -// it is based on: http://metalua.luaforge.net/src/lib/strict.lua.html -var protectGlobals = ` -local dbg=debug -local mt = {} -setmetatable(_G, mt) -mt.__newindex = function (t, n, v) - if dbg.getinfo(2) then - local w = dbg.getinfo(2, "S").what - if w ~= "C" then - error("Script attempted to create global variable '"..tostring(n).."'", 2) - end - end - rawset(t, n, v) -end -mt.__index = function (t, n) - if dbg.getinfo(2) and dbg.getinfo(2, "S").what ~= "C" then - error("Script attempted to access nonexistent global variable '"..tostring(n).."'", 2) - end - return rawget(t, n) -end -debug = nil - -` diff --git a/vendor/github.com/alicebob/miniredis/v2/cmd_server.go b/vendor/github.com/alicebob/miniredis/v2/cmd_server.go deleted file mode 100644 index 79e19fbc7..000000000 --- a/vendor/github.com/alicebob/miniredis/v2/cmd_server.go +++ /dev/null @@ -1,153 +0,0 @@ -// Commands from https://redis.io/commands#server - -package miniredis - -import ( - "fmt" - "strconv" - "strings" - - "github.com/alicebob/miniredis/v2/server" - "github.com/alicebob/miniredis/v2/size" -) - -func commandsServer(m *Miniredis) { - m.srv.Register("COMMAND", m.cmdCommand) - m.srv.Register("DBSIZE", m.cmdDbsize, server.ReadOnlyOption()) - m.srv.Register("FLUSHALL", m.cmdFlushall) - m.srv.Register("FLUSHDB", m.cmdFlushdb) - m.srv.Register("INFO", m.cmdInfo) - m.srv.Register("TIME", m.cmdTime) - m.srv.Register("MEMORY", m.cmdMemory) -} - -// MEMORY -func (m *Miniredis) cmdMemory(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, atLeast(1)) { - return - } - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - db := m.db(ctx.selectedDB) - - cmd, args := strings.ToLower(args[0]), args[1:] - switch cmd { - case "usage": - if len(args) < 1 { - setDirty(c) - c.WriteError(errWrongNumber("memory|usage")) - return - } - if len(args) > 1 { - setDirty(c) - c.WriteError(msgSyntaxError) - return - } - - var ( - value interface{} - ok bool - ) - switch db.keys[args[0]] { - case keyTypeString: - value, ok = db.stringKeys[args[0]] - case keyTypeSet: - value, ok = db.setKeys[args[0]] - case keyTypeHash: - value, ok = db.hashKeys[args[0]] - case keyTypeList: - value, ok = db.listKeys[args[0]] - case keyTypeHll: - value, ok = db.hllKeys[args[0]] - case keyTypeSortedSet: - value, ok = db.sortedsetKeys[args[0]] - case keyTypeStream: - value, ok = db.streamKeys[args[0]] - } - if !ok { - c.WriteNull() - return - } - c.WriteInt(size.Of(value)) - default: - c.WriteError(fmt.Sprintf(msgMemorySubcommand, strings.ToUpper(cmd))) - } - }) -} - -// DBSIZE -func (m *Miniredis) cmdDbsize(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, exactly(0)) { - return - } - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - db := m.db(ctx.selectedDB) - - c.WriteInt(len(db.keys)) - }) -} - -// FLUSHALL -func (m *Miniredis) cmdFlushall(c *server.Peer, cmd string, args []string) { - if len(args) > 0 && strings.ToLower(args[0]) == "async" { - args = args[1:] - } - if len(args) > 0 { - setDirty(c) - c.WriteError(msgSyntaxError) - return - } - if !m.handleAuth(c) { - return - } - if m.checkPubsub(c, cmd) { - return - } - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - m.flushAll() - c.WriteOK() - }) -} - -// FLUSHDB -func (m *Miniredis) cmdFlushdb(c *server.Peer, cmd string, args []string) { - if len(args) > 0 && strings.ToLower(args[0]) == "async" { - args = args[1:] - } - if len(args) > 0 { - setDirty(c) - c.WriteError(msgSyntaxError) - return - } - if !m.handleAuth(c) { - return - } - if m.checkPubsub(c, cmd) { - return - } - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - m.db(ctx.selectedDB).flush() - c.WriteOK() - }) -} - -// TIME -func (m *Miniredis) cmdTime(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, exactly(0)) { - return - } - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - now := m.effectiveNow() - nanos := now.UnixNano() - seconds := nanos / 1_000_000_000 - microseconds := (nanos / 1_000) % 1_000_000 - - c.WriteLen(2) - c.WriteBulk(strconv.FormatInt(seconds, 10)) - c.WriteBulk(strconv.FormatInt(microseconds, 10)) - }) -} diff --git a/vendor/github.com/alicebob/miniredis/v2/cmd_set.go b/vendor/github.com/alicebob/miniredis/v2/cmd_set.go deleted file mode 100644 index ec35d5636..000000000 --- a/vendor/github.com/alicebob/miniredis/v2/cmd_set.go +++ /dev/null @@ -1,701 +0,0 @@ -// Commands from https://redis.io/commands#set - -package miniredis - -import ( - "fmt" - "strconv" - "strings" - - "github.com/alicebob/miniredis/v2/server" -) - -// commandsSet handles all set value operations. -func commandsSet(m *Miniredis) { - m.srv.Register("SADD", m.cmdSadd) - m.srv.Register("SCARD", m.cmdScard, server.ReadOnlyOption()) - m.srv.Register("SDIFF", m.cmdSdiff, server.ReadOnlyOption()) - m.srv.Register("SDIFFSTORE", m.cmdSdiffstore) - m.srv.Register("SINTERCARD", m.cmdSintercard, server.ReadOnlyOption()) - m.srv.Register("SINTER", m.cmdSinter, server.ReadOnlyOption()) - m.srv.Register("SINTERSTORE", m.cmdSinterstore) - m.srv.Register("SISMEMBER", m.cmdSismember, server.ReadOnlyOption()) - m.srv.Register("SMEMBERS", m.cmdSmembers, server.ReadOnlyOption()) - m.srv.Register("SMISMEMBER", m.cmdSmismember, server.ReadOnlyOption()) - m.srv.Register("SMOVE", m.cmdSmove) - m.srv.Register("SPOP", m.cmdSpop) - m.srv.Register("SRANDMEMBER", m.cmdSrandmember, server.ReadOnlyOption()) - m.srv.Register("SREM", m.cmdSrem) - m.srv.Register("SUNION", m.cmdSunion, server.ReadOnlyOption()) - m.srv.Register("SUNIONSTORE", m.cmdSunionstore) - m.srv.Register("SSCAN", m.cmdSscan, server.ReadOnlyOption()) -} - -// SADD -func (m *Miniredis) cmdSadd(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, atLeast(2)) { - return - } - - key, elems := args[0], args[1:] - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - db := m.db(ctx.selectedDB) - - if db.exists(key) && db.t(key) != keyTypeSet { - c.WriteError(ErrWrongType.Error()) - return - } - - added := db.setAdd(key, elems...) - c.WriteInt(added) - }) -} - -// SCARD -func (m *Miniredis) cmdScard(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, exactly(1)) { - return - } - - key := args[0] - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - db := m.db(ctx.selectedDB) - - if !db.exists(key) { - c.WriteInt(0) - return - } - - if db.t(key) != "set" { - c.WriteError(ErrWrongType.Error()) - return - } - - members := db.setMembers(key) - c.WriteInt(len(members)) - }) -} - -// SDIFF -func (m *Miniredis) cmdSdiff(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, atLeast(1)) { - return - } - - keys := args - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - db := m.db(ctx.selectedDB) - - set, err := db.setDiff(keys) - if err != nil { - c.WriteError(err.Error()) - return - } - - c.WriteSetLen(len(set)) - for k := range set { - c.WriteBulk(k) - } - }) -} - -// SDIFFSTORE -func (m *Miniredis) cmdSdiffstore(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, atLeast(2)) { - return - } - - dest, keys := args[0], args[1:] - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - db := m.db(ctx.selectedDB) - - set, err := db.setDiff(keys) - if err != nil { - c.WriteError(err.Error()) - return - } - - db.del(dest, true) - db.setSet(dest, set) - c.WriteInt(len(set)) - }) -} - -// SINTER -func (m *Miniredis) cmdSinter(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, atLeast(1)) { - return - } - - keys := args - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - db := m.db(ctx.selectedDB) - - set, err := db.setInter(keys) - if err != nil { - c.WriteError(err.Error()) - return - } - - c.WriteLen(len(set)) - for k := range set { - c.WriteBulk(k) - } - }) -} - -// SINTERSTORE -func (m *Miniredis) cmdSinterstore(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, atLeast(2)) { - return - } - - dest, keys := args[0], args[1:] - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - db := m.db(ctx.selectedDB) - - set, err := db.setInter(keys) - if err != nil { - c.WriteError(err.Error()) - return - } - - db.del(dest, true) - db.setSet(dest, set) - c.WriteInt(len(set)) - }) -} - -// SINTERCARD -func (m *Miniredis) cmdSintercard(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, atLeast(2)) { - return - } - - opts := struct { - keys []string - limit int - }{} - - numKeys, err := strconv.Atoi(args[0]) - if err != nil { - setDirty(c) - c.WriteError("ERR numkeys should be greater than 0") - return - } - if numKeys < 1 { - setDirty(c) - c.WriteError("ERR numkeys should be greater than 0") - return - } - - args = args[1:] - if len(args) < numKeys { - setDirty(c) - c.WriteError("ERR Number of keys can't be greater than number of args") - return - } - opts.keys = args[:numKeys] - - args = args[numKeys:] - if len(args) == 2 && strings.ToLower(args[0]) == "limit" { - l, err := strconv.Atoi(args[1]) - if err != nil { - setDirty(c) - c.WriteError(msgInvalidInt) - return - } - if l < 0 { - setDirty(c) - c.WriteError(msgLimitIsNegative) - return - } - opts.limit = l - } else if len(args) > 0 { - setDirty(c) - c.WriteError(msgSyntaxError) - return - } - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - db := m.db(ctx.selectedDB) - - count, err := db.setIntercard(opts.keys, opts.limit) - if err != nil { - c.WriteError(err.Error()) - return - } - c.WriteInt(count) - }) -} - -// SISMEMBER -func (m *Miniredis) cmdSismember(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, exactly(2)) { - return - } - - key, value := args[0], args[1] - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - db := m.db(ctx.selectedDB) - - if !db.exists(key) { - c.WriteInt(0) - return - } - - if db.t(key) != "set" { - c.WriteError(ErrWrongType.Error()) - return - } - - if db.setIsMember(key, value) { - c.WriteInt(1) - return - } - c.WriteInt(0) - }) -} - -// SMEMBERS -func (m *Miniredis) cmdSmembers(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, exactly(1)) { - return - } - - key := args[0] - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - db := m.db(ctx.selectedDB) - - if !db.exists(key) { - c.WriteSetLen(0) - return - } - - if db.t(key) != "set" { - c.WriteError(ErrWrongType.Error()) - return - } - - members := db.setMembers(key) - - c.WriteSetLen(len(members)) - for _, elem := range members { - c.WriteBulk(elem) - } - }) -} - -// SMISMEMBER -func (m *Miniredis) cmdSmismember(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, atLeast(2)) { - return - } - - key, values := args[0], args[1:] - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - db := m.db(ctx.selectedDB) - - if !db.exists(key) { - c.WriteLen(len(values)) - for range values { - c.WriteInt(0) - } - return - } - - if db.t(key) != "set" { - c.WriteError(ErrWrongType.Error()) - return - } - - c.WriteLen(len(values)) - for _, value := range values { - if db.setIsMember(key, value) { - c.WriteInt(1) - } else { - c.WriteInt(0) - } - } - return - }) -} - -// SMOVE -func (m *Miniredis) cmdSmove(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, exactly(3)) { - return - } - - src, dst, member := args[0], args[1], args[2] - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - db := m.db(ctx.selectedDB) - - if !db.exists(src) { - c.WriteInt(0) - return - } - - if db.t(src) != "set" { - c.WriteError(ErrWrongType.Error()) - return - } - - if db.exists(dst) && db.t(dst) != "set" { - c.WriteError(ErrWrongType.Error()) - return - } - - if !db.setIsMember(src, member) { - c.WriteInt(0) - return - } - db.setRem(src, member) - db.setAdd(dst, member) - c.WriteInt(1) - }) -} - -// SPOP -func (m *Miniredis) cmdSpop(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, atLeast(1)) { - return - } - - opts := struct { - key string - withCount bool - count int - }{ - count: 1, - } - opts.key, args = args[0], args[1:] - - if len(args) > 0 { - v, err := strconv.Atoi(args[0]) - if err != nil { - setDirty(c) - c.WriteError(msgInvalidInt) - return - } - if v < 0 { - setDirty(c) - c.WriteError(msgOutOfRange) - return - } - opts.count = v - opts.withCount = true - args = args[1:] - } - if len(args) > 0 { - setDirty(c) - c.WriteError(msgInvalidInt) - return - } - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - db := m.db(ctx.selectedDB) - - if !db.exists(opts.key) { - if !opts.withCount { - c.WriteNull() - return - } - c.WriteLen(0) - return - } - - if db.t(opts.key) != "set" { - c.WriteError(ErrWrongType.Error()) - return - } - - var deleted []string - members := db.setMembers(opts.key) - for i := 0; i < opts.count; i++ { - if len(members) == 0 { - break - } - i := m.randIntn(len(members)) - member := members[i] - members = delElem(members, i) - db.setRem(opts.key, member) - deleted = append(deleted, member) - } - // without `count` return a single value - if !opts.withCount { - if len(deleted) == 0 { - c.WriteNull() - return - } - c.WriteBulk(deleted[0]) - return - } - // with `count` return a list - c.WriteLen(len(deleted)) - for _, v := range deleted { - c.WriteBulk(v) - } - }) -} - -// SRANDMEMBER -func (m *Miniredis) cmdSrandmember(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, atLeast(1)) { - return - } - - if len(args) > 2 { - setDirty(c) - c.WriteError(msgSyntaxError) - return - } - - key := args[0] - count := 0 - withCount := false - if len(args) == 2 { - var err error - count, err = strconv.Atoi(args[1]) - if err != nil { - setDirty(c) - c.WriteError(msgInvalidInt) - return - } - withCount = true - } - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - db := m.db(ctx.selectedDB) - - if !db.exists(key) { - if withCount { - c.WriteLen(0) - return - } - c.WriteNull() - return - } - - if db.t(key) != "set" { - c.WriteError(ErrWrongType.Error()) - return - } - - members := db.setMembers(key) - if count < 0 { - // Non-unique elements is allowed with negative count. - c.WriteLen(-count) - for count != 0 { - member := members[m.randIntn(len(members))] - c.WriteBulk(member) - count++ - } - return - } - - // Must be unique elements. - m.shuffle(members) - if count > len(members) { - count = len(members) - } - if !withCount { - c.WriteBulk(members[0]) - return - } - c.WriteLen(count) - for i := range make([]struct{}, count) { - c.WriteBulk(members[i]) - } - }) -} - -// SREM -func (m *Miniredis) cmdSrem(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, atLeast(2)) { - return - } - - key, fields := args[0], args[1:] - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - db := m.db(ctx.selectedDB) - - if !db.exists(key) { - c.WriteInt(0) - return - } - - if db.t(key) != "set" { - c.WriteError(ErrWrongType.Error()) - return - } - - c.WriteInt(db.setRem(key, fields...)) - }) -} - -// SUNION -func (m *Miniredis) cmdSunion(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, atLeast(1)) { - return - } - - keys := args - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - db := m.db(ctx.selectedDB) - - set, err := db.setUnion(keys) - if err != nil { - c.WriteError(err.Error()) - return - } - - c.WriteLen(len(set)) - for k := range set { - c.WriteBulk(k) - } - }) -} - -// SUNIONSTORE -func (m *Miniredis) cmdSunionstore(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, atLeast(2)) { - return - } - - dest, keys := args[0], args[1:] - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - db := m.db(ctx.selectedDB) - - set, err := db.setUnion(keys) - if err != nil { - c.WriteError(err.Error()) - return - } - - db.del(dest, true) - db.setSet(dest, set) - c.WriteInt(len(set)) - }) -} - -// SSCAN -func (m *Miniredis) cmdSscan(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, atLeast(2)) { - return - } - - var opts struct { - key string - value int - cursor int - count int - withMatch bool - match string - } - - opts.key = args[0] - if ok := optIntErr(c, args[1], &opts.cursor, msgInvalidCursor); !ok { - return - } - args = args[2:] - - // MATCH and COUNT options - for len(args) > 0 { - if strings.ToLower(args[0]) == "count" { - if len(args) < 2 { - setDirty(c) - c.WriteError(msgSyntaxError) - return - } - count, err := strconv.Atoi(args[1]) - if err != nil || count < 0 { - setDirty(c) - c.WriteError(msgInvalidInt) - return - } - if count == 0 { - setDirty(c) - c.WriteError(msgSyntaxError) - return - } - opts.count = count - args = args[2:] - continue - } - if strings.ToLower(args[0]) == "match" { - if len(args) < 2 { - setDirty(c) - c.WriteError(msgSyntaxError) - return - } - opts.withMatch = true - opts.match = args[1] - args = args[2:] - continue - } - setDirty(c) - c.WriteError(msgSyntaxError) - return - } - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - db := m.db(ctx.selectedDB) - // return _all_ (matched) keys every time - if db.exists(opts.key) && db.t(opts.key) != "set" { - c.WriteError(ErrWrongType.Error()) - return - } - members := db.setMembers(opts.key) - if opts.withMatch { - members, _ = matchKeys(members, opts.match) - } - low := opts.cursor - high := low + opts.count - // validate high is correct - if high > len(members) || high == 0 { - high = len(members) - } - if opts.cursor > high { - // invalid cursor - c.WriteLen(2) - c.WriteBulk("0") // no next cursor - c.WriteLen(0) // no elements - return - } - cursorValue := low + opts.count - if cursorValue > len(members) { - cursorValue = 0 // no next cursor - } - members = members[low:high] - c.WriteLen(2) - c.WriteBulk(fmt.Sprintf("%d", cursorValue)) - c.WriteLen(len(members)) - for _, k := range members { - c.WriteBulk(k) - } - - }) -} - -func delElem(ls []string, i int) []string { - // this swap+truncate is faster but changes behaviour: - // ls[i] = ls[len(ls)-1] - // ls = ls[:len(ls)-1] - // so we do the dumb thing: - ls = append(ls[:i], ls[i+1:]...) - return ls -} diff --git a/vendor/github.com/alicebob/miniredis/v2/cmd_sorted_set.go b/vendor/github.com/alicebob/miniredis/v2/cmd_sorted_set.go deleted file mode 100644 index 1491a74f4..000000000 --- a/vendor/github.com/alicebob/miniredis/v2/cmd_sorted_set.go +++ /dev/null @@ -1,1857 +0,0 @@ -// Commands from https://redis.io/commands#sorted_set - -package miniredis - -import ( - "errors" - "fmt" - "math" - "sort" - "strconv" - "strings" - - "github.com/alicebob/miniredis/v2/server" -) - -// commandsSortedSet handles all sorted set operations. -func commandsSortedSet(m *Miniredis) { - m.srv.Register("ZADD", m.cmdZadd) - m.srv.Register("ZCARD", m.cmdZcard, server.ReadOnlyOption()) - m.srv.Register("ZCOUNT", m.cmdZcount, server.ReadOnlyOption()) - m.srv.Register("ZINCRBY", m.cmdZincrby) - m.srv.Register("ZINTER", m.makeCmdZinter(false), server.ReadOnlyOption()) - m.srv.Register("ZINTERSTORE", m.makeCmdZinter(true)) - m.srv.Register("ZLEXCOUNT", m.cmdZlexcount, server.ReadOnlyOption()) - m.srv.Register("ZRANGE", m.cmdZrange, server.ReadOnlyOption()) - m.srv.Register("ZRANGEBYLEX", m.makeCmdZrangebylex(false), server.ReadOnlyOption()) - m.srv.Register("ZRANGEBYSCORE", m.makeCmdZrangebyscore(false), server.ReadOnlyOption()) - m.srv.Register("ZRANK", m.makeCmdZrank(false), server.ReadOnlyOption()) - m.srv.Register("ZREM", m.cmdZrem) - m.srv.Register("ZREMRANGEBYLEX", m.cmdZremrangebylex) - m.srv.Register("ZREMRANGEBYRANK", m.cmdZremrangebyrank) - m.srv.Register("ZREMRANGEBYSCORE", m.cmdZremrangebyscore) - m.srv.Register("ZREVRANGE", m.cmdZrevrange, server.ReadOnlyOption()) - m.srv.Register("ZREVRANGEBYLEX", m.makeCmdZrangebylex(true), server.ReadOnlyOption()) - m.srv.Register("ZREVRANGEBYSCORE", m.makeCmdZrangebyscore(true), server.ReadOnlyOption()) - m.srv.Register("ZREVRANK", m.makeCmdZrank(true), server.ReadOnlyOption()) - m.srv.Register("ZSCORE", m.cmdZscore, server.ReadOnlyOption()) - m.srv.Register("ZMSCORE", m.cmdZMscore, server.ReadOnlyOption()) - m.srv.Register("ZUNION", m.cmdZunion, server.ReadOnlyOption()) - m.srv.Register("ZUNIONSTORE", m.cmdZunionstore) - m.srv.Register("ZSCAN", m.cmdZscan, server.ReadOnlyOption()) - m.srv.Register("ZPOPMAX", m.cmdZpopmax(true)) - m.srv.Register("ZPOPMIN", m.cmdZpopmax(false)) - m.srv.Register("ZRANDMEMBER", m.cmdZrandmember, server.ReadOnlyOption()) -} - -// ZADD -func (m *Miniredis) cmdZadd(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, atLeast(3)) { - return - } - - var opts struct { - key string - nx bool - xx bool - gt bool - lt bool - ch bool - incr bool - } - elems := map[string]float64{} - - opts.key = args[0] - args = args[1:] -outer: - for len(args) > 0 { - switch strings.ToUpper(args[0]) { - case "NX": - opts.nx = true - args = args[1:] - continue - case "XX": - opts.xx = true - args = args[1:] - continue - case "GT": - opts.gt = true - args = args[1:] - continue - case "LT": - opts.lt = true - args = args[1:] - continue - case "CH": - opts.ch = true - args = args[1:] - continue - case "INCR": - opts.incr = true - args = args[1:] - continue - default: - break outer - } - } - - if len(args) == 0 || len(args)%2 != 0 { - setDirty(c) - c.WriteError(msgSyntaxError) - return - } - for len(args) > 0 { - score, err := strconv.ParseFloat(args[0], 64) - if err != nil { - setDirty(c) - c.WriteError(msgInvalidFloat) - return - } - elems[args[1]] = score - args = args[2:] - } - - if opts.xx && opts.nx { - setDirty(c) - c.WriteError(msgXXandNX) - return - } - - if opts.gt && opts.lt || - opts.gt && opts.nx || - opts.lt && opts.nx { - setDirty(c) - c.WriteError(msgGTLTandNX) - return - } - - if opts.incr && len(elems) > 1 { - setDirty(c) - c.WriteError(msgSingleElementPair) - return - } - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - db := m.db(ctx.selectedDB) - - if db.exists(opts.key) && db.t(opts.key) != keyTypeSortedSet { - c.WriteError(ErrWrongType.Error()) - return - } - - if opts.incr { - for member, delta := range elems { - if opts.nx && db.ssetExists(opts.key, member) { - c.WriteNull() - return - } - if opts.xx && !db.ssetExists(opts.key, member) { - c.WriteNull() - return - } - newScore := db.ssetIncrby(opts.key, member, delta) - c.WriteFloat(newScore) - } - return - } - - res := 0 - for member, score := range elems { - exists := db.ssetExists(opts.key, member) - if opts.nx && exists { - continue - } - if opts.xx && !exists { - continue - } - old := db.ssetScore(opts.key, member) - if opts.gt && exists && score <= old { - continue - } - if opts.lt && exists && score >= old { - continue - } - if db.ssetAdd(opts.key, score, member) { - res++ - } else { - if opts.ch && old != score { - // if 'CH' is specified, only count changed keys - res++ - } - } - } - c.WriteInt(res) - }) -} - -// ZCARD -func (m *Miniredis) cmdZcard(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, exactly(1)) { - return - } - - key := args[0] - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - db := m.db(ctx.selectedDB) - - if !db.exists(key) { - c.WriteInt(0) - return - } - - if db.t(key) != keyTypeSortedSet { - c.WriteError(ErrWrongType.Error()) - return - } - - c.WriteInt(db.ssetCard(key)) - }) -} - -// ZCOUNT -func (m *Miniredis) cmdZcount(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, exactly(3)) { - return - } - - var ( - opts struct { - key string - min float64 - minIncl bool - max float64 - maxIncl bool - } - err error - ) - - opts.key = args[0] - opts.min, opts.minIncl, err = parseFloatRange(args[1]) - if err != nil { - setDirty(c) - c.WriteError(msgInvalidMinMax) - return - } - opts.max, opts.maxIncl, err = parseFloatRange(args[2]) - if err != nil { - setDirty(c) - c.WriteError(msgInvalidMinMax) - return - } - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - db := m.db(ctx.selectedDB) - - if !db.exists(opts.key) { - c.WriteInt(0) - return - } - - if db.t(opts.key) != keyTypeSortedSet { - c.WriteError(ErrWrongType.Error()) - return - } - - members := db.ssetElements(opts.key) - members = withSSRange(members, opts.min, opts.minIncl, opts.max, opts.maxIncl) - c.WriteInt(len(members)) - }) -} - -// ZINCRBY -func (m *Miniredis) cmdZincrby(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, exactly(3)) { - return - } - - var opts struct { - key string - delta float64 - member string - } - - opts.key = args[0] - d, err := strconv.ParseFloat(args[1], 64) - if err != nil { - setDirty(c) - c.WriteError(msgInvalidFloat) - return - } - opts.delta = d - opts.member = args[2] - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - db := m.db(ctx.selectedDB) - - if db.exists(opts.key) && db.t(opts.key) != keyTypeSortedSet { - c.WriteError(msgWrongType) - return - } - newScore := db.ssetIncrby(opts.key, opts.member, opts.delta) - c.WriteFloat(newScore) - }) -} - -// ZINTERSTORE and ZINTER -func (m *Miniredis) makeCmdZinter(store bool) func(c *server.Peer, cmd string, args []string) { - return func(c *server.Peer, cmd string, args []string) { - minArgs := 2 - if store { - minArgs++ - } - if !m.isValidCMD(c, cmd, args, atLeast(minArgs)) { - return - } - - var opts = struct { - Store bool // if true this is ZINTERSTORE - Destination string // only relevant if $store is true - Keys []string - Aggregate string - WithWeights bool - Weights []float64 - WithScores bool // only for ZINTER - }{ - Store: store, - Aggregate: "sum", - } - - if store { - opts.Destination = args[0] - args = args[1:] - } - numKeys, err := strconv.Atoi(args[0]) - if err != nil { - setDirty(c) - c.WriteError(msgInvalidInt) - return - } - args = args[1:] - if len(args) < numKeys { - setDirty(c) - c.WriteError(msgSyntaxError) - return - } - if numKeys <= 0 { - setDirty(c) - c.WriteError("ERR at least 1 input key is needed for ZUNIONSTORE/ZINTERSTORE") - return - } - opts.Keys = args[:numKeys] - args = args[numKeys:] - - for len(args) > 0 { - switch strings.ToLower(args[0]) { - case "weights": - if len(args) < numKeys+1 { - setDirty(c) - c.WriteError(msgSyntaxError) - return - } - for i := 0; i < numKeys; i++ { - f, err := strconv.ParseFloat(args[i+1], 64) - if err != nil { - setDirty(c) - c.WriteError("ERR weight value is not a float") - return - } - opts.Weights = append(opts.Weights, f) - } - opts.WithWeights = true - args = args[numKeys+1:] - case "aggregate": - if len(args) < 2 { - setDirty(c) - c.WriteError(msgSyntaxError) - return - } - aggregate := strings.ToLower(args[1]) - switch aggregate { - case "sum", "min", "max": - opts.Aggregate = aggregate - default: - setDirty(c) - c.WriteError(msgSyntaxError) - return - } - args = args[2:] - case "withscores": - if store { - setDirty(c) - c.WriteError(msgSyntaxError) - return - } - opts.WithScores = true - args = args[1:] - default: - setDirty(c) - c.WriteError(msgSyntaxError) - return - } - } - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - db := m.db(ctx.selectedDB) - - // We collect everything and remove all keys which turned out not to be - // present in every set. - sset := map[string]float64{} - counts := map[string]int{} - for i, key := range opts.Keys { - if !db.exists(key) { - continue - } - - var set map[string]float64 - switch db.t(key) { - case keyTypeSet: - set = map[string]float64{} - for elem := range db.setKeys[key] { - set[elem] = 1.0 - } - case keyTypeSortedSet: - set = db.sortedSet(key) - default: - c.WriteError(msgWrongType) - return - } - for member, score := range set { - if opts.WithWeights { - score *= opts.Weights[i] - } - counts[member]++ - old, ok := sset[member] - if !ok { - sset[member] = score - continue - } - switch opts.Aggregate { - default: - panic("Invalid aggregate") - case "sum": - sset[member] += score - case "min": - if score < old { - sset[member] = score - } - case "max": - if score > old { - sset[member] = score - } - } - } - } - for key, count := range counts { - if count != numKeys { - delete(sset, key) - } - } - - if opts.Store { - // ZINTERSTORE mode - db.del(opts.Destination, true) - db.ssetSet(opts.Destination, sset) - c.WriteInt(len(sset)) - return - } - // ZINTER mode - size := len(sset) - if opts.WithScores { - size *= 2 - } - c.WriteLen(size) - for _, l := range sortedKeys(sset) { - c.WriteBulk(l) - if opts.WithScores { - c.WriteFloat(sset[l]) - } - } - }) - } -} - -// ZLEXCOUNT -func (m *Miniredis) cmdZlexcount(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, exactly(3)) { - return - } - - var opts = struct { - Key string - Min string - Max string - }{ - Key: args[0], - Min: args[1], - Max: args[2], - } - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - min, minIncl, minErr := parseLexrange(opts.Min) - max, maxIncl, maxErr := parseLexrange(opts.Max) - if minErr != nil || maxErr != nil { - c.WriteError(msgInvalidRangeItem) - return - } - - db := m.db(ctx.selectedDB) - - if !db.exists(opts.Key) { - c.WriteInt(0) - return - } - - if db.t(opts.Key) != keyTypeSortedSet { - c.WriteError(ErrWrongType.Error()) - return - } - - members := db.ssetMembers(opts.Key) - // Just key sort. If scores are not the same we don't care. - sort.Strings(members) - members = withLexRange(members, min, minIncl, max, maxIncl) - - c.WriteInt(len(members)) - }) -} - -// ZRANGE -func (m *Miniredis) cmdZrange(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, atLeast(3)) { - return - } - - var opts struct { - Key string - Min string - Max string - WithScores bool - ByScore bool - ByLex bool - Reverse bool - WithLimit bool - Offset string - Count string - } - - opts.Key, opts.Min, opts.Max = args[0], args[1], args[2] - args = args[3:] - - for len(args) > 0 { - switch strings.ToLower(args[0]) { - case "byscore": - opts.ByScore = true - args = args[1:] - case "bylex": - opts.ByLex = true - args = args[1:] - case "rev": - opts.Reverse = true - args = args[1:] - case "limit": - opts.WithLimit = true - args = args[1:] - if len(args) < 2 { - c.WriteError(msgSyntaxError) - return - } - opts.Offset = args[0] - opts.Count = args[1] - args = args[2:] - case "withscores": - opts.WithScores = true - args = args[1:] - default: - c.WriteError(msgSyntaxError) - return - } - } - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - switch { - case opts.ByScore && opts.ByLex: - c.WriteError(msgSyntaxError) - case opts.ByScore: - runRangeByScore(m, c, ctx, optsRangeByScore{ - Key: opts.Key, - Min: opts.Min, - Max: opts.Max, - Reverse: opts.Reverse, - WithLimit: opts.WithLimit, - Offset: opts.Offset, - Count: opts.Count, - WithScores: opts.WithScores, - }) - case opts.ByLex: - runRangeByLex(m, c, ctx, optsRangeByLex{ - Key: opts.Key, - Min: opts.Min, - Max: opts.Max, - Reverse: opts.Reverse, - WithLimit: opts.WithLimit, - Offset: opts.Offset, - Count: opts.Count, - WithScores: opts.WithScores, - }) - default: - if opts.WithLimit { - c.WriteError(msgLimitCombination) - return - } - runRange(m, c, ctx, optsRange{ - Key: opts.Key, - Min: opts.Min, - Max: opts.Max, - Reverse: opts.Reverse, - WithScores: opts.WithScores, - }) - } - }) -} - -// ZREVRANGE -func (m *Miniredis) cmdZrevrange(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, atLeast(3)) { - return - } - - var opts = optsRange{ - Reverse: true, - Key: args[0], - Min: args[1], - Max: args[2], - } - args = args[3:] - - for len(args) > 0 { - switch strings.ToLower(args[0]) { - case "withscores": - opts.WithScores = true - args = args[1:] - default: - c.WriteError(msgSyntaxError) - return - } - } - - withTx(m, c, func(c *server.Peer, cctx *connCtx) { - runRange(m, c, cctx, opts) - }) -} - -// ZRANGEBYLEX and ZREVRANGEBYLEX -func (m *Miniredis) makeCmdZrangebylex(reverse bool) server.Cmd { - return func(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, atLeast(3)) { - return - } - - opts := optsRangeByLex{ - Reverse: reverse, - Key: args[0], - Min: args[1], - Max: args[2], - } - args = args[3:] - - for len(args) > 0 { - switch strings.ToLower(args[0]) { - case "limit": - opts.WithLimit = true - args = args[1:] - if len(args) < 2 { - c.WriteError(msgSyntaxError) - return - } - opts.Offset = args[0] - opts.Count = args[1] - args = args[2:] - continue - default: - // Syntax error - setDirty(c) - c.WriteError(msgSyntaxError) - return - } - } - - withTx(m, c, func(c *server.Peer, cctx *connCtx) { - runRangeByLex(m, c, cctx, opts) - }) - } -} - -// ZRANGEBYSCORE and ZREVRANGEBYSCORE -func (m *Miniredis) makeCmdZrangebyscore(reverse bool) server.Cmd { - return func(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, atLeast(3)) { - return - } - - var opts = optsRangeByScore{ - Reverse: reverse, - Key: args[0], - Min: args[1], - Max: args[2], - } - args = args[3:] - - for len(args) > 0 { - if strings.ToLower(args[0]) == "limit" { - opts.WithLimit = true - args = args[1:] - if len(args) < 2 { - c.WriteError(msgSyntaxError) - return - } - opts.Offset = args[0] - opts.Count = args[1] - args = args[2:] - continue - } - if strings.ToLower(args[0]) == "withscores" { - opts.WithScores = true - args = args[1:] - continue - } - setDirty(c) - c.WriteError(msgSyntaxError) - return - } - - withTx(m, c, func(c *server.Peer, cctx *connCtx) { - runRangeByScore(m, c, cctx, opts) - }) - } -} - -// ZRANK and ZREVRANK -func (m *Miniredis) makeCmdZrank(reverse bool) server.Cmd { - return func(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, atLeast(2)) { - return - } - - key, member := args[0], args[1] - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - db := m.db(ctx.selectedDB) - - withScore := false - if len(args) > 0 && strings.ToUpper(args[len(args)-1]) == "WITHSCORE" { - withScore = true - args = args[:len(args)-1] - } - - if len(args) > 2 { - setDirty(c) - c.WriteError(msgSyntaxError) - return - } - - if !db.exists(key) { - if withScore { - c.WriteLen(-1) - } else { - c.WriteNull() - } - return - } - - if db.t(key) != keyTypeSortedSet { - c.WriteError(ErrWrongType.Error()) - return - } - - direction := asc - if reverse { - direction = desc - } - rank, ok := db.ssetRank(key, member, direction) - if !ok { - if withScore { - c.WriteLen(-1) - } else { - c.WriteNull() - } - return - } - - if withScore { - c.WriteLen(2) - c.WriteInt(rank) - c.WriteFloat(db.ssetScore(key, member)) - } else { - c.WriteInt(rank) - } - }) - } -} - -// ZREM -func (m *Miniredis) cmdZrem(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, atLeast(2)) { - return - } - - key, members := args[0], args[1:] - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - db := m.db(ctx.selectedDB) - - if !db.exists(key) { - c.WriteInt(0) - return - } - - if db.t(key) != keyTypeSortedSet { - c.WriteError(ErrWrongType.Error()) - return - } - - deleted := 0 - for _, member := range members { - if db.ssetRem(key, member) { - deleted++ - } - } - c.WriteInt(deleted) - }) -} - -// ZREMRANGEBYLEX -func (m *Miniredis) cmdZremrangebylex(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, exactly(3)) { - return - } - - var opts = struct { - Key string - Min string - Max string - }{ - Key: args[0], - Min: args[1], - Max: args[2], - } - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - min, minIncl, minErr := parseLexrange(opts.Min) - max, maxIncl, maxErr := parseLexrange(opts.Max) - if minErr != nil || maxErr != nil { - c.WriteError(msgInvalidRangeItem) - return - } - - db := m.db(ctx.selectedDB) - - if !db.exists(opts.Key) { - c.WriteInt(0) - return - } - - if db.t(opts.Key) != keyTypeSortedSet { - c.WriteError(ErrWrongType.Error()) - return - } - - members := db.ssetMembers(opts.Key) - // Just key sort. If scores are not the same we don't care. - sort.Strings(members) - members = withLexRange(members, min, minIncl, max, maxIncl) - - for _, el := range members { - db.ssetRem(opts.Key, el) - } - c.WriteInt(len(members)) - }) -} - -// ZREMRANGEBYRANK -func (m *Miniredis) cmdZremrangebyrank(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, exactly(3)) { - return - } - - var opts struct { - key string - start int - end int - } - - opts.key = args[0] - if ok := optInt(c, args[1], &opts.start); !ok { - return - } - if ok := optInt(c, args[2], &opts.end); !ok { - return - } - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - db := m.db(ctx.selectedDB) - - if !db.exists(opts.key) { - c.WriteInt(0) - return - } - - if db.t(opts.key) != keyTypeSortedSet { - c.WriteError(ErrWrongType.Error()) - return - } - - members := db.ssetMembers(opts.key) - rs, re := redisRange(len(members), opts.start, opts.end, false) - for _, el := range members[rs:re] { - db.ssetRem(opts.key, el) - } - c.WriteInt(re - rs) - }) -} - -// ZREMRANGEBYSCORE -func (m *Miniredis) cmdZremrangebyscore(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, exactly(3)) { - return - } - - var ( - opts struct { - key string - min float64 - minIncl bool - max float64 - maxIncl bool - } - err error - ) - opts.key = args[0] - opts.min, opts.minIncl, err = parseFloatRange(args[1]) - if err != nil { - setDirty(c) - c.WriteError(msgInvalidMinMax) - return - } - opts.max, opts.maxIncl, err = parseFloatRange(args[2]) - if err != nil { - setDirty(c) - c.WriteError(msgInvalidMinMax) - return - } - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - db := m.db(ctx.selectedDB) - - if !db.exists(opts.key) { - c.WriteInt(0) - return - } - - if db.t(opts.key) != keyTypeSortedSet { - c.WriteError(ErrWrongType.Error()) - return - } - - members := db.ssetElements(opts.key) - members = withSSRange(members, opts.min, opts.minIncl, opts.max, opts.maxIncl) - - for _, el := range members { - db.ssetRem(opts.key, el.member) - } - c.WriteInt(len(members)) - }) -} - -// ZSCORE -func (m *Miniredis) cmdZscore(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, exactly(2)) { - return - } - - key, member := args[0], args[1] - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - db := m.db(ctx.selectedDB) - - if !db.exists(key) { - c.WriteNull() - return - } - - if db.t(key) != keyTypeSortedSet { - c.WriteError(ErrWrongType.Error()) - return - } - - if !db.ssetExists(key, member) { - c.WriteNull() - return - } - - c.WriteFloat(db.ssetScore(key, member)) - }) -} - -// ZMSCORE -func (m *Miniredis) cmdZMscore(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, atLeast(2)) { - return - } - - key, members := args[0], args[1:] - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - db := m.db(ctx.selectedDB) - - if !db.exists(key) { - c.WriteLen(len(members)) - for range members { - c.WriteNull() - } - return - } - - if db.t(key) != keyTypeSortedSet { - c.WriteError(ErrWrongType.Error()) - return - } - - c.WriteLen(len(members)) - for _, member := range members { - if !db.ssetExists(key, member) { - c.WriteNull() - continue - } - c.WriteFloat(db.ssetScore(key, member)) - } - }) -} - -// parseFloatRange handles ZRANGEBYSCORE floats. They are inclusive unless the -// string starts with '(' -func parseFloatRange(s string) (float64, bool, error) { - if len(s) == 0 { - return 0, false, nil - } - inclusive := true - if s[0] == '(' { - s = s[1:] - inclusive = false - } - switch strings.ToLower(s) { - case "+inf": - return math.Inf(+1), true, nil - case "-inf": - return math.Inf(-1), true, nil - default: - f, err := strconv.ParseFloat(s, 64) - return f, inclusive, err - } -} - -// withSSRange limits a list of sorted set elements by the ZRANGEBYSCORE range -// logic. -func withSSRange(members ssElems, min float64, minIncl bool, max float64, maxIncl bool) ssElems { - gt := func(a, b float64) bool { return a > b } - gteq := func(a, b float64) bool { return a >= b } - - mincmp := gt - if minIncl { - mincmp = gteq - } - for i, m := range members { - if mincmp(m.score, min) { - members = members[i:] - goto checkmax - } - } - // all elements were smaller - return nil - -checkmax: - maxcmp := gteq - if maxIncl { - maxcmp = gt - } - for i, m := range members { - if maxcmp(m.score, max) { - members = members[:i] - break - } - } - - return members -} - -// withLexRange limits a list of sorted set elements. -func withLexRange(members []string, min string, minIncl bool, max string, maxIncl bool) []string { - if max == "-" || min == "+" { - return nil - } - if min != "-" { - found := false - if minIncl { - for i, m := range members { - if m >= min { - members = members[i:] - found = true - break - } - } - } else { - // Excluding min - for i, m := range members { - if m > min { - members = members[i:] - found = true - break - } - } - } - if !found { - return nil - } - } - if max != "+" { - if maxIncl { - for i, m := range members { - if m > max { - members = members[:i] - break - } - } - } else { - // Excluding max - for i, m := range members { - if m >= max { - members = members[:i] - break - } - } - } - } - return members -} - -// ZUNION -func (m *Miniredis) cmdZunion(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, atLeast(2)) { - return - } - - numKeys, err := strconv.Atoi(args[0]) - if err != nil { - setDirty(c) - c.WriteError(msgInvalidInt) - return - } - args = args[1:] - if len(args) < numKeys { - setDirty(c) - c.WriteError(msgSyntaxError) - return - } - if numKeys <= 0 { - setDirty(c) - c.WriteError("ERR at least 1 input key is needed for ZUNION") - return - } - keys := args[:numKeys] - args = args[numKeys:] - - withScores := false - if len(args) > 0 && strings.ToUpper(args[len(args)-1]) == "WITHSCORES" { - withScores = true - args = args[:len(args)-1] - } - - opts := zunionOptions{ - Keys: keys, - WithWeights: false, - Weights: []float64{}, - Aggregate: "sum", - } - - if err := opts.parseArgs(args, numKeys); err != nil { - setDirty(c) - c.WriteError(err.Error()) - return - } - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - db := m.db(ctx.selectedDB) - - sset, err := executeZUnion(db, opts) - if err != nil { - c.WriteError(err.Error()) - return - } - - if withScores { - c.WriteLen(len(sset) * 2) - } else { - c.WriteLen(len(sset)) - } - for _, el := range sset.byScore(asc) { - c.WriteBulk(el.member) - if withScores { - c.WriteFloat(el.score) - } - } - }) -} - -// ZUNIONSTORE -func (m *Miniredis) cmdZunionstore(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, atLeast(3)) { - return - } - - destination := args[0] - numKeys, err := strconv.Atoi(args[1]) - if err != nil { - setDirty(c) - c.WriteError(msgInvalidInt) - return - } - args = args[2:] - if len(args) < numKeys { - setDirty(c) - c.WriteError(msgSyntaxError) - return - } - if numKeys <= 0 { - setDirty(c) - c.WriteError("ERR at least 1 input key is needed for ZUNIONSTORE/ZINTERSTORE") - return - } - keys := args[:numKeys] - args = args[numKeys:] - - opts := zunionOptions{ - Keys: keys, - WithWeights: false, - Weights: []float64{}, - Aggregate: "sum", - } - - if err := opts.parseArgs(args, numKeys); err != nil { - setDirty(c) - c.WriteError(err.Error()) - return - } - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - db := m.db(ctx.selectedDB) - deleteDest := true - for _, key := range keys { - if destination == key { - deleteDest = false - } - } - if deleteDest { - db.del(destination, true) - } - - sset, err := executeZUnion(db, opts) - if err != nil { - c.WriteError(err.Error()) - return - } - db.ssetSet(destination, sset) - c.WriteInt(sset.card()) - }) -} - -type zunionOptions struct { - Keys []string - WithWeights bool - Weights []float64 - Aggregate string -} - -func (opts *zunionOptions) parseArgs(args []string, numKeys int) error { - for len(args) > 0 { - switch strings.ToLower(args[0]) { - case "weights": - if len(args) < numKeys+1 { - return errors.New(msgSyntaxError) - } - for i := 0; i < numKeys; i++ { - f, err := strconv.ParseFloat(args[i+1], 64) - if err != nil { - return errors.New("ERR weight value is not a float") - } - opts.Weights = append(opts.Weights, f) - } - opts.WithWeights = true - args = args[numKeys+1:] - case "aggregate": - if len(args) < 2 { - return errors.New(msgSyntaxError) - } - opts.Aggregate = strings.ToLower(args[1]) - switch opts.Aggregate { - default: - return errors.New(msgSyntaxError) - case "sum", "min", "max": - } - args = args[2:] - default: - return errors.New(msgSyntaxError) - } - } - return nil -} - -func executeZUnion(db *RedisDB, opts zunionOptions) (sortedSet, error) { - sset := sortedSet{} - for i, key := range opts.Keys { - if !db.exists(key) { - continue - } - - var set map[string]float64 - switch db.t(key) { - case keyTypeSet: - set = map[string]float64{} - for elem := range db.setKeys[key] { - set[elem] = 1.0 - } - case keyTypeSortedSet: - set = db.sortedSet(key) - default: - return nil, errors.New(msgWrongType) - } - - for member, score := range set { - if opts.WithWeights { - score *= opts.Weights[i] - } - old, ok := sset[member] - if !ok { - sset[member] = score - continue - } - switch opts.Aggregate { - default: - panic("Invalid aggregate") - case "sum": - sset[member] += score - case "min": - if score < old { - sset[member] = score - } - case "max": - if score > old { - sset[member] = score - } - } - } - } - - return sset, nil -} - -// ZSCAN -func (m *Miniredis) cmdZscan(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, atLeast(2)) { - return - } - - var opts struct { - key string - cursor int - count int - withMatch bool - match string - } - - opts.key = args[0] - if ok := optIntErr(c, args[1], &opts.cursor, msgInvalidCursor); !ok { - return - } - args = args[2:] - // MATCH and COUNT options - for len(args) > 0 { - if strings.ToLower(args[0]) == "count" { - if len(args) < 2 { - setDirty(c) - c.WriteError(msgSyntaxError) - return - } - count, err := strconv.Atoi(args[1]) - if err != nil { - setDirty(c) - c.WriteError(msgInvalidInt) - return - } - if count <= 0 { - setDirty(c) - c.WriteError(msgSyntaxError) - return - } - opts.count = count - args = args[2:] - continue - } - if strings.ToLower(args[0]) == "match" { - if len(args) < 2 { - setDirty(c) - c.WriteError(msgSyntaxError) - return - } - opts.withMatch = true - opts.match = args[1] - args = args[2:] - continue - } - setDirty(c) - c.WriteError(msgSyntaxError) - return - } - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - db := m.db(ctx.selectedDB) - if db.exists(opts.key) && db.t(opts.key) != keyTypeSortedSet { - c.WriteError(ErrWrongType.Error()) - return - } - - members := db.ssetMembers(opts.key) - if opts.withMatch { - members, _ = matchKeys(members, opts.match) - } - - low := opts.cursor - high := low + opts.count - // validate high is correct - if high > len(members) || high == 0 { - high = len(members) - } - if opts.cursor > high { - // invalid cursor - c.WriteLen(2) - c.WriteBulk("0") // no next cursor - c.WriteLen(0) // no elements - return - } - cursorValue := low + opts.count - if cursorValue >= len(members) { - cursorValue = 0 // no next cursor - } - members = members[low:high] - - c.WriteLen(2) - c.WriteBulk(fmt.Sprintf("%d", cursorValue)) - // HSCAN gives key, values. - c.WriteLen(len(members) * 2) - for _, k := range members { - c.WriteBulk(k) - c.WriteFloat(db.ssetScore(opts.key, k)) - } - }) -} - -// ZPOPMAX and ZPOPMIN -func (m *Miniredis) cmdZpopmax(reverse bool) server.Cmd { - return func(c *server.Peer, cmd string, args []string) { - if len(args) < 1 { - setDirty(c) - c.WriteError(errWrongNumber(cmd)) - return - } - if !m.handleAuth(c) { - return - } - - key := args[0] - count := 1 - var err error - if len(args) > 1 { - count, err = strconv.Atoi(args[1]) - if err != nil || count < 0 { - setDirty(c) - c.WriteError(msgInvalidRange) - return - } - } - - withScores := true - if len(args) > 2 { - c.WriteError(msgSyntaxError) - return - } - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - db := m.db(ctx.selectedDB) - - if !db.exists(key) { - c.WriteLen(0) - return - } - - if db.t(key) != keyTypeSortedSet { - c.WriteError(ErrWrongType.Error()) - return - } - - members := db.ssetMembers(key) - if reverse { - reverseSlice(members) - } - rs, re := redisRange(len(members), 0, count-1, false) - if withScores { - c.WriteLen((re - rs) * 2) - } else { - c.WriteLen(re - rs) - } - for _, el := range members[rs:re] { - c.WriteBulk(el) - if withScores { - c.WriteFloat(db.ssetScore(key, el)) - } - db.ssetRem(key, el) - } - }) - } -} - -// ZRANDMEMBER -func (m *Miniredis) cmdZrandmember(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, atLeast(1)) { - return - } - - var opts struct { - key string - withCount bool - count int - withScores bool - } - - opts.key = args[0] - args = args[1:] - - if len(args) > 0 { - // can be negative - if ok := optInt(c, args[0], &opts.count); !ok { - return - } - opts.withCount = true - args = args[1:] - } - - if len(args) > 0 && strings.ToUpper(args[0]) == "WITHSCORES" { - opts.withScores = true - args = args[1:] - } - - if len(args) > 0 { - setDirty(c) - c.WriteError(errWrongNumber(cmd)) - return - } - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - db := m.db(ctx.selectedDB) - - if !db.exists(opts.key) { - if opts.withCount { - c.WriteLen(0) - } else { - c.WriteNull() - } - return - } - - if db.t(opts.key) != keyTypeSortedSet { - c.WriteError(ErrWrongType.Error()) - return - } - - if !opts.withCount { - member := db.ssetRandomMember(opts.key) - if member == "" { - c.WriteNull() - return - } - c.WriteBulk(member) - return - } - - var members []string - switch { - case opts.count == 0: - c.WriteStrings(nil) - return - case opts.count > 0: - allMembers := db.ssetMembers(opts.key) - db.master.shuffle(allMembers) - if len(allMembers) > opts.count { - allMembers = allMembers[:opts.count] - } - members = allMembers - case opts.count < 0: - for i := 0; i < -opts.count; i++ { - members = append(members, db.ssetRandomMember(opts.key)) - } - } - if opts.withScores { - c.WriteLen(len(members) * 2) - for _, m := range members { - c.WriteBulk(m) - c.WriteFloat(db.ssetScore(opts.key, m)) - } - return - } - c.WriteStrings(members) - }) -} - -type optsRange struct { - Key string - Min string - Max string - Reverse bool - WithScores bool -} - -func runRange(m *Miniredis, c *server.Peer, cctx *connCtx, opts optsRange) { - min, minErr := strconv.Atoi(opts.Min) - max, maxErr := strconv.Atoi(opts.Max) - if minErr != nil || maxErr != nil { - c.WriteError(msgInvalidInt) - return - } - - db := m.db(cctx.selectedDB) - - if !db.exists(opts.Key) { - c.WriteLen(0) - return - } - - if db.t(opts.Key) != keyTypeSortedSet { - c.WriteError(ErrWrongType.Error()) - return - } - - members := db.ssetMembers(opts.Key) - if opts.Reverse { - reverseSlice(members) - } - rs, re := redisRange(len(members), min, max, false) - if opts.WithScores { - c.WriteLen((re - rs) * 2) - } else { - c.WriteLen(re - rs) - } - for _, el := range members[rs:re] { - c.WriteBulk(el) - if opts.WithScores { - c.WriteFloat(db.ssetScore(opts.Key, el)) - } - } -} - -type optsRangeByScore struct { - Key string - Min string - Max string - Reverse bool - WithLimit bool - Offset string - Count string - WithScores bool -} - -func runRangeByScore(m *Miniredis, c *server.Peer, cctx *connCtx, opts optsRangeByScore) { - var limitOffset, limitCount int - var err error - if opts.WithLimit { - limitOffset, err = strconv.Atoi(opts.Offset) - if err != nil { - c.WriteError(msgInvalidInt) - return - } - limitCount, err = strconv.Atoi(opts.Count) - if err != nil { - c.WriteError(msgInvalidInt) - return - } - } - min, minIncl, minErr := parseFloatRange(opts.Min) - max, maxIncl, maxErr := parseFloatRange(opts.Max) - if minErr != nil || maxErr != nil { - c.WriteError(msgInvalidMinMax) - return - } - - db := m.db(cctx.selectedDB) - - if !db.exists(opts.Key) { - c.WriteLen(0) - return - } - - if db.t(opts.Key) != keyTypeSortedSet { - c.WriteError(ErrWrongType.Error()) - return - } - - members := db.ssetElements(opts.Key) - if opts.Reverse { - min, max = max, min - minIncl, maxIncl = maxIncl, minIncl - } - members = withSSRange(members, min, minIncl, max, maxIncl) - if opts.Reverse { - reverseElems(members) - } - - // Apply LIMIT ranges. That's . Unlike RANGE. - if opts.WithLimit { - if limitOffset < 0 { - members = ssElems{} - } else { - if limitOffset < len(members) { - members = members[limitOffset:] - } else { - // out of range - members = ssElems{} - } - if limitCount >= 0 { - if len(members) > limitCount { - members = members[:limitCount] - } - } - } - } - - if opts.WithScores { - c.WriteLen(len(members) * 2) - } else { - c.WriteLen(len(members)) - } - for _, el := range members { - c.WriteBulk(el.member) - if opts.WithScores { - c.WriteFloat(el.score) - } - } -} - -type optsRangeByLex struct { - Key string - Min string - Max string - Reverse bool - WithLimit bool - Offset string - Count string - WithScores bool -} - -func runRangeByLex(m *Miniredis, c *server.Peer, cctx *connCtx, opts optsRangeByLex) { - var limitOffset, limitCount int - var err error - if opts.WithLimit { - limitOffset, err = strconv.Atoi(opts.Offset) - if err != nil { - c.WriteError(msgInvalidInt) - return - } - limitCount, err = strconv.Atoi(opts.Count) - if err != nil { - c.WriteError(msgInvalidInt) - return - } - } - min, minIncl, minErr := parseLexrange(opts.Min) - max, maxIncl, maxErr := parseLexrange(opts.Max) - if minErr != nil || maxErr != nil { - c.WriteError(msgInvalidRangeItem) - return - } - - db := m.db(cctx.selectedDB) - - if !db.exists(opts.Key) { - c.WriteLen(0) - return - } - - if db.t(opts.Key) != keyTypeSortedSet { - c.WriteError(ErrWrongType.Error()) - return - } - - members := db.ssetMembers(opts.Key) - // Just key sort. If scores are not the same we don't care. - sort.Strings(members) - if opts.Reverse { - min, max = max, min - minIncl, maxIncl = maxIncl, minIncl - } - members = withLexRange(members, min, minIncl, max, maxIncl) - if opts.Reverse { - reverseSlice(members) - } - - // Apply LIMIT ranges. That's . Unlike RANGE. - if opts.WithLimit { - if limitOffset < 0 { - members = nil - } else { - if limitOffset < len(members) { - members = members[limitOffset:] - } else { - // out of range - members = nil - } - if limitCount >= 0 { - if len(members) > limitCount { - members = members[:limitCount] - } - } - } - } - - c.WriteLen(len(members)) - for _, el := range members { - c.WriteBulk(el) - } -} - -// optLexrange handles ZRANGE{,BYLEX} ranges. They start with '[', '(', or are -// '+' or '-'. -// Sets destValue and destInclusive. destValue can be '+' or '-'. -func parseLexrange(s string) (string, bool, error) { - if len(s) == 0 { - return "", false, errors.New(msgInvalidRangeItem) - } - - if s == "+" || s == "-" { - return s, false, nil - } - - switch s[0] { - case '(': - return s[1:], false, nil - case '[': - return s[1:], true, nil - default: - return "", false, errors.New(msgInvalidRangeItem) - } -} - -func sortedKeys(m map[string]float64) []string { - var keys []string - for k := range m { - keys = append(keys, k) - } - sort.Strings(keys) - return keys -} diff --git a/vendor/github.com/alicebob/miniredis/v2/cmd_stream.go b/vendor/github.com/alicebob/miniredis/v2/cmd_stream.go deleted file mode 100644 index f2cccbb51..000000000 --- a/vendor/github.com/alicebob/miniredis/v2/cmd_stream.go +++ /dev/null @@ -1,1696 +0,0 @@ -// Commands from https://redis.io/commands#stream - -package miniredis - -import ( - "errors" - "fmt" - "sort" - "strconv" - "strings" - "time" - - "github.com/alicebob/miniredis/v2/server" -) - -// commandsStream handles all stream operations. -func commandsStream(m *Miniredis) { - m.srv.Register("XADD", m.cmdXadd) - m.srv.Register("XLEN", m.cmdXlen, server.ReadOnlyOption()) - m.srv.Register("XREAD", m.cmdXread, server.ReadOnlyOption()) - m.srv.Register("XRANGE", m.makeCmdXrange(false), server.ReadOnlyOption()) - m.srv.Register("XREVRANGE", m.makeCmdXrange(true), server.ReadOnlyOption()) - m.srv.Register("XGROUP", m.cmdXgroup) - m.srv.Register("XINFO", m.cmdXinfo) - m.srv.Register("XREADGROUP", m.cmdXreadgroup) - m.srv.Register("XACK", m.cmdXack) - m.srv.Register("XDEL", m.cmdXdel) - m.srv.Register("XPENDING", m.cmdXpending, server.ReadOnlyOption()) - m.srv.Register("XTRIM", m.cmdXtrim) - m.srv.Register("XAUTOCLAIM", m.cmdXautoclaim) - m.srv.Register("XCLAIM", m.cmdXclaim) -} - -// XADD -func (m *Miniredis) cmdXadd(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, atLeast(4)) { - return - } - - key, args := args[0], args[1:] - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - maxlen := -1 - minID := "" - makeStream := true - if strings.ToLower(args[0]) == "nomkstream" { - args = args[1:] - makeStream = false - } - if strings.ToLower(args[0]) == "maxlen" { - args = args[1:] - // we don't treat "~" special - if args[0] == "~" { - args = args[1:] - } - n, err := strconv.Atoi(args[0]) - if err != nil { - c.WriteError(msgInvalidInt) - return - } - if n < 0 { - c.WriteError("ERR The MAXLEN argument must be >= 0.") - return - } - maxlen = n - args = args[1:] - } else if strings.ToLower(args[0]) == "minid" { - args = args[1:] - // we don't treat "~" special - if args[0] == "~" { - args = args[1:] - } - minID = args[0] - args = args[1:] - } - if len(args) < 1 { - c.WriteError(errWrongNumber(cmd)) - return - } - entryID, args := args[0], args[1:] - - // args must be composed of field/value pairs. - if len(args) == 0 || len(args)%2 != 0 { - c.WriteError("ERR wrong number of arguments for XADD") // non-default message - return - } - - var values []string - for len(args) > 0 { - values = append(values, args[0], args[1]) - args = args[2:] - } - - db := m.db(ctx.selectedDB) - s, err := db.stream(key) - if err != nil { - c.WriteError(err.Error()) - return - } - if s == nil { - if !makeStream { - c.WriteNull() - return - } - s, _ = db.newStream(key) - } - - newID, err := s.add(entryID, values, m.effectiveNow()) - if err != nil { - switch err { - case errInvalidEntryID: - c.WriteError(msgInvalidStreamID) - default: - c.WriteError(err.Error()) - } - return - } - if maxlen >= 0 { - s.trim(maxlen) - } - if minID != "" { - s.trimBefore(minID) - } - db.incr(key) - - c.WriteBulk(newID) - }) -} - -// XLEN -func (m *Miniredis) cmdXlen(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, exactly(1)) { - return - } - - key := args[0] - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - db := m.db(ctx.selectedDB) - - s, err := db.stream(key) - if err != nil { - c.WriteError(err.Error()) - } - if s == nil { - // No such key. That's zero length. - c.WriteInt(0) - return - } - - c.WriteInt(len(s.entries)) - }) -} - -// XRANGE and XREVRANGE -func (m *Miniredis) makeCmdXrange(reverse bool) server.Cmd { - return func(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, atLeast(3)) { - return - } - if len(args) == 4 || len(args) > 5 { - setDirty(c) - c.WriteError(msgSyntaxError) - return - } - - opts := struct { - key string - startKey string - startExclusive bool - endKey string - endExclusive bool - }{ - key: args[0], - startKey: args[1], - endKey: args[2], - } - if strings.HasPrefix(opts.startKey, "(") { - opts.startExclusive = true - opts.startKey = opts.startKey[1:] - if opts.startKey == "-" || opts.startKey == "+" { - setDirty(c) - c.WriteError(msgInvalidStreamID) - return - } - } - if strings.HasPrefix(opts.endKey, "(") { - opts.endExclusive = true - opts.endKey = opts.endKey[1:] - if opts.endKey == "-" || opts.endKey == "+" { - setDirty(c) - c.WriteError(msgInvalidStreamID) - return - } - } - - countArg := "0" - if len(args) == 5 { - if strings.ToLower(args[3]) != "count" { - setDirty(c) - c.WriteError(msgSyntaxError) - return - } - countArg = args[4] - } - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - start, err := formatStreamRangeBound(opts.startKey, true, reverse) - if err != nil { - c.WriteError(msgInvalidStreamID) - return - } - end, err := formatStreamRangeBound(opts.endKey, false, reverse) - if err != nil { - c.WriteError(msgInvalidStreamID) - return - } - count, err := strconv.Atoi(countArg) - if err != nil { - c.WriteError(msgInvalidInt) - return - } - - db := m.db(ctx.selectedDB) - - if !db.exists(opts.key) { - c.WriteLen(0) - return - } - - if db.t(opts.key) != keyTypeStream { - c.WriteError(ErrWrongType.Error()) - return - } - - var entries = db.streamKeys[opts.key].entries - if reverse { - entries = reversedStreamEntries(entries) - } - if count == 0 { - count = len(entries) - } - - var returnedEntries []StreamEntry - for _, entry := range entries { - if len(returnedEntries) == count { - break - } - - if !reverse { - // Break if entry ID > end - if streamCmp(entry.ID, end) == 1 { - break - } - - // Continue if entry ID < start - if streamCmp(entry.ID, start) == -1 { - continue - } - } else { - // Break if entry iD < end - if streamCmp(entry.ID, end) == -1 { - break - } - - // Continue if entry ID > start. - if streamCmp(entry.ID, start) == 1 { - continue - } - } - - // Continue if start exclusive and entry ID == start - if opts.startExclusive && streamCmp(entry.ID, start) == 0 { - continue - } - // Continue if end exclusive and entry ID == end - if opts.endExclusive && streamCmp(entry.ID, end) == 0 { - continue - } - - returnedEntries = append(returnedEntries, entry) - } - - c.WriteLen(len(returnedEntries)) - for _, entry := range returnedEntries { - c.WriteLen(2) - c.WriteBulk(entry.ID) - c.WriteLen(len(entry.Values)) - for _, v := range entry.Values { - c.WriteBulk(v) - } - } - }) - } -} - -// XGROUP -func (m *Miniredis) cmdXgroup(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, atLeast(1)) { - return - } - - subCmd, args := strings.ToLower(args[0]), args[1:] - switch subCmd { - case "create": - m.cmdXgroupCreate(c, cmd, args) - case "destroy": - m.cmdXgroupDestroy(c, cmd, args) - case "createconsumer": - m.cmdXgroupCreateconsumer(c, cmd, args) - case "delconsumer": - m.cmdXgroupDelconsumer(c, cmd, args) - case "help", - "setid": - err := fmt.Sprintf("ERR 'XGROUP %s' not supported", subCmd) - setDirty(c) - c.WriteError(err) - default: - setDirty(c) - c.WriteError(fmt.Sprintf( - "ERR unknown subcommand '%s'. Try XGROUP HELP.", - subCmd, - )) - } -} - -// XGROUP CREATE -func (m *Miniredis) cmdXgroupCreate(c *server.Peer, cmd string, args []string) { - if len(args) != 3 && len(args) != 4 { - setDirty(c) - c.WriteError(errWrongNumber("CREATE")) - return - } - stream, group, id := args[0], args[1], args[2] - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - db := m.db(ctx.selectedDB) - - s, err := db.stream(stream) - if err != nil { - c.WriteError(err.Error()) - return - } - if s == nil && len(args) == 4 && strings.ToUpper(args[3]) == "MKSTREAM" { - if s, err = db.newStream(stream); err != nil { - c.WriteError(err.Error()) - return - } - } - if s == nil { - c.WriteError(msgXgroupKeyNotFound) - return - } - - if err := s.createGroup(group, id); err != nil { - c.WriteError(err.Error()) - return - } - - c.WriteOK() - }) -} - -// XGROUP DESTROY -func (m *Miniredis) cmdXgroupDestroy(c *server.Peer, cmd string, args []string) { - if len(args) != 2 { - setDirty(c) - c.WriteError(errWrongNumber("DESTROY")) - return - } - stream, groupName := args[0], args[1] - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - db := m.db(ctx.selectedDB) - - s, err := db.stream(stream) - if err != nil { - c.WriteError(err.Error()) - return - } - if s == nil { - c.WriteError(msgXgroupKeyNotFound) - return - } - - if _, ok := s.groups[groupName]; !ok { - c.WriteInt(0) - return - } - delete(s.groups, groupName) - c.WriteInt(1) - }) -} - -// XGROUP CREATECONSUMER -func (m *Miniredis) cmdXgroupCreateconsumer(c *server.Peer, cmd string, args []string) { - if len(args) != 3 { - setDirty(c) - c.WriteError(errWrongNumber("CREATECONSUMER")) - return - } - key, groupName, consumerName := args[0], args[1], args[2] - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - db := m.db(ctx.selectedDB) - - s, err := db.stream(key) - if err != nil { - c.WriteError(err.Error()) - return - } - if s == nil { - c.WriteError(msgXgroupKeyNotFound) - return - } - - g, ok := s.groups[groupName] - if !ok { - err := fmt.Sprintf("NOGROUP No such consumer group '%s' for key name '%s'", groupName, key) - c.WriteError(err) - return - } - - if _, ok = g.consumers[consumerName]; ok { - c.WriteInt(0) - return - } - g.consumers[consumerName] = &consumer{} - c.WriteInt(1) - }) -} - -// XGROUP DELCONSUMER -func (m *Miniredis) cmdXgroupDelconsumer(c *server.Peer, cmd string, args []string) { - if len(args) != 3 { - setDirty(c) - c.WriteError(errWrongNumber("DELCONSUMER")) - return - } - key, groupName, consumerName := args[0], args[1], args[2] - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - db := m.db(ctx.selectedDB) - - s, err := db.stream(key) - if err != nil { - c.WriteError(err.Error()) - return - } - if s == nil { - c.WriteError(msgXgroupKeyNotFound) - return - } - - g, ok := s.groups[groupName] - if !ok { - err := fmt.Sprintf("NOGROUP No such consumer group '%s' for key name '%s'", groupName, key) - c.WriteError(err) - return - } - - consumer, ok := g.consumers[consumerName] - if !ok { - c.WriteInt(0) - return - } - defer delete(g.consumers, consumerName) - - if consumer.numPendingEntries > 0 { - newPending := make([]pendingEntry, 0) - for _, entry := range g.pending { - if entry.consumer != consumerName { - newPending = append(newPending, entry) - } - } - g.pending = newPending - } - c.WriteInt(consumer.numPendingEntries) - }) -} - -// XINFO -func (m *Miniredis) cmdXinfo(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, atLeast(1)) { - return - } - - subCmd, args := strings.ToUpper(args[0]), args[1:] - switch subCmd { - case "STREAM": - m.cmdXinfoStream(c, args) - case "CONSUMERS": - m.cmdXinfoConsumers(c, args) - case "GROUPS": - m.cmdXinfoGroups(c, args) - case "HELP": - err := fmt.Sprintf("'XINFO %s' not supported", strings.Join(args, " ")) - setDirty(c) - c.WriteError(err) - default: - setDirty(c) - c.WriteError(fmt.Sprintf( - "ERR unknown subcommand or wrong number of arguments for '%s'. Try XINFO HELP.", - subCmd, - )) - } -} - -// XINFO STREAM -// Produces only part of full command output -func (m *Miniredis) cmdXinfoStream(c *server.Peer, args []string) { - if len(args) < 1 { - setDirty(c) - c.WriteError(errWrongNumber("STREAM")) - return - } - key := args[0] - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - db := m.db(ctx.selectedDB) - - s, err := db.stream(key) - if err != nil { - c.WriteError(err.Error()) - return - } - if s == nil { - c.WriteError(msgKeyNotFound) - return - } - - c.WriteMapLen(1) - c.WriteBulk("length") - c.WriteInt(len(s.entries)) - }) -} - -// XINFO GROUPS -func (m *Miniredis) cmdXinfoGroups(c *server.Peer, args []string) { - if len(args) != 1 { - setDirty(c) - c.WriteError(errWrongNumber("GROUPS")) - return - } - key := args[0] - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - db := m.db(ctx.selectedDB) - - s, err := db.stream(key) - if err != nil { - c.WriteError(err.Error()) - return - } - if s == nil { - c.WriteError(msgKeyNotFound) - return - } - - c.WriteLen(len(s.groups)) - for name, g := range s.groups { - c.WriteMapLen(6) - - c.WriteBulk("name") - c.WriteBulk(name) - c.WriteBulk("consumers") - c.WriteInt(len(g.consumers)) - c.WriteBulk("pending") - c.WriteInt(len(g.activePending())) - c.WriteBulk("last-delivered-id") - c.WriteBulk(g.lastID) - c.WriteBulk("entries-read") - c.WriteNull() - c.WriteBulk("lag") - c.WriteInt(len(g.stream.entries)) - } - }) -} - -// XINFO CONSUMERS -// Please note that this is only a partial implementation, for it does not -// return each consumer's "idle" value, which indicates "the number of -// milliseconds that have passed since the consumer last interacted with the -// server." -func (m *Miniredis) cmdXinfoConsumers(c *server.Peer, args []string) { - if len(args) != 2 { - setDirty(c) - c.WriteError(errWrongNumber("CONSUMERS")) - return - } - key, groupName := args[0], args[1] - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - db := m.db(ctx.selectedDB) - - s, err := db.stream(key) - if err != nil { - c.WriteError(err.Error()) - return - } - if s == nil { - c.WriteError(msgKeyNotFound) - return - } - - g, ok := s.groups[groupName] - if !ok { - err := fmt.Sprintf("NOGROUP No such consumer group '%s' for key name '%s'", groupName, key) - c.WriteError(err) - return - } - - var consumerNames []string - for name := range g.consumers { - consumerNames = append(consumerNames, name) - } - sort.Strings(consumerNames) - - c.WriteLen(len(consumerNames)) - for _, name := range consumerNames { - cons := g.consumers[name] - - c.WriteMapLen(4) - c.WriteBulk("name") - c.WriteBulk(name) - c.WriteBulk("pending") - c.WriteInt(cons.numPendingEntries) - // TODO: these times aren't set for all commands - c.WriteBulk("idle") - c.WriteInt(m.sinceMilli(cons.lastSeen)) - c.WriteBulk("inactive") - c.WriteInt(m.sinceMilli(cons.lastSuccess)) - } - }) -} - -func (m *Miniredis) sinceMilli(t time.Time) int { - if t.IsZero() { - return -1 - } - return int(m.effectiveNow().Sub(t).Milliseconds()) -} - -// XREADGROUP -func (m *Miniredis) cmdXreadgroup(c *server.Peer, cmd string, args []string) { - // XREADGROUP GROUP group consumer STREAMS key ID - if !m.isValidCMD(c, cmd, args, atLeast(6)) { - return - } - - var opts struct { - group string - consumer string - count int - noack bool - streams []string - ids []string - block bool - blockTimeout time.Duration - } - - if strings.ToUpper(args[0]) != "GROUP" { - setDirty(c) - c.WriteError(msgSyntaxError) - return - } - - opts.group, opts.consumer, args = args[1], args[2], args[3:] - - var err error -parsing: - for len(args) > 0 { - switch strings.ToUpper(args[0]) { - case "COUNT": - if len(args) < 2 { - err = errors.New(errWrongNumber(cmd)) - break parsing - } - - opts.count, err = strconv.Atoi(args[1]) - if err != nil { - break parsing - } - - args = args[2:] - case "BLOCK": - err = parseBlock(cmd, args, &opts.block, &opts.blockTimeout) - if err != nil { - break parsing - } - args = args[2:] - case "NOACK": - args = args[1:] - opts.noack = true - case "STREAMS": - args = args[1:] - - if len(args)%2 != 0 { - err = errors.New(msgXreadUnbalanced) - break parsing - } - - opts.streams, opts.ids = args[0:len(args)/2], args[len(args)/2:] - break parsing - default: - err = fmt.Errorf("ERR incorrect argument %s", args[0]) - break parsing - } - } - - if err != nil { - setDirty(c) - c.WriteError(err.Error()) - return - } - - if len(opts.streams) == 0 || len(opts.ids) == 0 { - setDirty(c) - c.WriteError(errWrongNumber(cmd)) - return - } - - for _, id := range opts.ids { - if id != `>` { - opts.block = false - } - } - - if !opts.block { - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - db := m.db(ctx.selectedDB) - res, err := xreadgroup( - db, - opts.group, - opts.consumer, - opts.noack, - opts.streams, - opts.ids, - opts.count, - m.effectiveNow(), - ) - if err != nil { - c.WriteError(err.Error()) - return - } - writeXread(c, opts.streams, res) - }) - return - } - - blocking( - m, - c, - opts.blockTimeout, - func(c *server.Peer, ctx *connCtx) bool { - db := m.db(ctx.selectedDB) - res, err := xreadgroup( - db, - opts.group, - opts.consumer, - opts.noack, - opts.streams, - opts.ids, - opts.count, - m.effectiveNow(), - ) - if err != nil { - c.WriteError(err.Error()) - return true - } - if len(res) == 0 { - return false - } - writeXread(c, opts.streams, res) - return true - }, - func(c *server.Peer) { // timeout - c.WriteLen(-1) - }, - ) -} - -func xreadgroup( - db *RedisDB, - group, - consumer string, - noack bool, - streams []string, - ids []string, - count int, - now time.Time, -) (map[string][]StreamEntry, error) { - res := map[string][]StreamEntry{} - for i, key := range streams { - id := ids[i] - - g, err := db.streamGroup(key, group) - if err != nil { - return nil, err - } - if g == nil { - return nil, errXreadgroup(key, group) - } - - if _, err := parseStreamID(id); id != `>` && err != nil { - return nil, err - } - entries := g.readGroup(now, consumer, id, count, noack) - if id == `>` && len(entries) == 0 { - continue - } - - res[key] = entries - } - return res, nil -} - -// XACK -func (m *Miniredis) cmdXack(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, atLeast(3)) { - return - } - - key, group, ids := args[0], args[1], args[2:] - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - db := m.db(ctx.selectedDB) - g, err := db.streamGroup(key, group) - if err != nil { - c.WriteError(err.Error()) - return - } - if g == nil { - c.WriteInt(0) - return - } - - cnt, err := g.ack(ids) - if err != nil { - c.WriteError(err.Error()) - return - } - c.WriteInt(cnt) - }) -} - -// XDEL -func (m *Miniredis) cmdXdel(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, atLeast(2)) { - return - } - - stream, ids := args[0], args[1:] - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - db := m.db(ctx.selectedDB) - s, err := db.stream(stream) - if err != nil { - c.WriteError(err.Error()) - return - } - if s == nil { - c.WriteInt(0) - return - } - - n, err := s.delete(ids) - if err != nil { - c.WriteError(err.Error()) - return - } - db.incr(stream) - c.WriteInt(n) - }) -} - -// XREAD -func (m *Miniredis) cmdXread(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, atLeast(3)) { - return - } - - var ( - opts struct { - count int - streams []string - ids []string - block bool - blockTimeout time.Duration - } - err error - ) - -parsing: - for len(args) > 0 { - switch strings.ToUpper(args[0]) { - case "COUNT": - if len(args) < 2 { - err = errors.New(errWrongNumber(cmd)) - break parsing - } - - opts.count, err = strconv.Atoi(args[1]) - if err != nil { - break parsing - } - args = args[2:] - case "BLOCK": - err = parseBlock(cmd, args, &opts.block, &opts.blockTimeout) - if err != nil { - break parsing - } - args = args[2:] - case "STREAMS": - args = args[1:] - - if len(args)%2 != 0 { - err = errors.New(msgXreadUnbalanced) - break parsing - } - - opts.streams, opts.ids = args[0:len(args)/2], args[len(args)/2:] - for i, id := range opts.ids { - if _, err := parseStreamID(id); id != `$` && err != nil { - setDirty(c) - c.WriteError(msgInvalidStreamID) - return - } else if id == "$" { - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - db := m.db(getCtx(c).selectedDB) - stream, ok := db.streamKeys[opts.streams[i]] - if ok { - opts.ids[i] = stream.lastID() - } else { - opts.ids[i] = "0-0" - } - }) - } - } - args = nil - break parsing - default: - err = fmt.Errorf("ERR incorrect argument %s", args[0]) - break parsing - } - } - if err != nil { - setDirty(c) - c.WriteError(err.Error()) - return - } - - if !opts.block { - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - db := m.db(ctx.selectedDB) - res := xread(db, opts.streams, opts.ids, opts.count) - writeXread(c, opts.streams, res) - }) - return - } - blocking( - m, - c, - opts.blockTimeout, - func(c *server.Peer, ctx *connCtx) bool { - db := m.db(ctx.selectedDB) - res := xread(db, opts.streams, opts.ids, opts.count) - if len(res) == 0 { - return false - } - writeXread(c, opts.streams, res) - return true - }, - func(c *server.Peer) { // timeout - c.WriteLen(-1) - }, - ) -} - -func xread(db *RedisDB, streams []string, ids []string, count int) map[string][]StreamEntry { - res := map[string][]StreamEntry{} - for i := range streams { - stream := streams[i] - id := ids[i] - - var s, ok = db.streamKeys[stream] - if !ok { - continue - } - entries := s.entries - if len(entries) == 0 { - continue - } - - entryCount := count - if entryCount == 0 { - entryCount = len(entries) - } - - var returnedEntries []StreamEntry - for _, entry := range entries { - if len(returnedEntries) == entryCount { - break - } - if id == "$" { - id = s.lastID() - } - if streamCmp(entry.ID, id) <= 0 { - continue - } - returnedEntries = append(returnedEntries, entry) - } - if len(returnedEntries) > 0 { - res[stream] = returnedEntries - } - } - return res -} - -func writeXread(c *server.Peer, streams []string, res map[string][]StreamEntry) { - if len(res) == 0 { - c.WriteLen(-1) - return - } - c.WriteLen(len(res)) - for _, stream := range streams { - entries, ok := res[stream] - if !ok { - continue - } - c.WriteLen(2) - c.WriteBulk(stream) - c.WriteLen(len(entries)) - for _, entry := range entries { - c.WriteLen(2) - c.WriteBulk(entry.ID) - c.WriteLen(len(entry.Values)) - for _, v := range entry.Values { - c.WriteBulk(v) - } - } - } -} - -// XPENDING -func (m *Miniredis) cmdXpending(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, atLeast(2)) { - return - } - - var opts struct { - key string - group string - summary bool - idle time.Duration - start, end string - count int - consumer *string - } - - opts.key, opts.group, args = args[0], args[1], args[2:] - opts.summary = true - if len(args) >= 3 { - opts.summary = false - - if strings.ToUpper(args[0]) == "IDLE" { - idleMs, err := strconv.ParseInt(args[1], 10, 64) - if err != nil { - setDirty(c) - c.WriteError(msgInvalidInt) - return - } - opts.idle = time.Duration(idleMs) * time.Millisecond - - args = args[2:] - if len(args) < 3 { - setDirty(c) - c.WriteError(msgSyntaxError) - return - } - } - - var err error - opts.start, err = formatStreamRangeBound(args[0], true, false) - if err != nil { - setDirty(c) - c.WriteError(msgInvalidStreamID) - return - } - opts.end, err = formatStreamRangeBound(args[1], false, false) - if err != nil { - setDirty(c) - c.WriteError(msgInvalidStreamID) - return - } - opts.count, err = strconv.Atoi(args[2]) // negative is allowed - if err != nil { - setDirty(c) - c.WriteError(msgInvalidInt) - return - } - args = args[3:] - - if len(args) == 1 { - opts.consumer, args = &args[0], args[1:] - } - } - if len(args) != 0 { - setDirty(c) - c.WriteError(msgSyntaxError) - return - } - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - db := m.db(ctx.selectedDB) - g, err := db.streamGroup(opts.key, opts.group) - if err != nil { - c.WriteError(err.Error()) - return - } - if g == nil { - c.WriteError(errReadgroup(opts.key, opts.group).Error()) - return - } - - if opts.summary { - writeXpendingSummary(c, *g) - return - } - writeXpending(m.effectiveNow(), c, *g, opts.idle, opts.start, opts.end, opts.count, opts.consumer) - }) -} - -func writeXpendingSummary(c *server.Peer, g streamGroup) { - pend := g.activePending() - if len(pend) == 0 { - c.WriteLen(4) - c.WriteInt(0) - c.WriteNull() - c.WriteNull() - c.WriteLen(-1) - return - } - - // format: - // - number of pending - // - smallest ID - // - highest ID - // - all consumers with > 0 pending items - c.WriteLen(4) - c.WriteInt(len(pend)) - c.WriteBulk(pend[0].id) - c.WriteBulk(pend[len(pend)-1].id) - cons := map[string]int{} - for id := range g.consumers { - cnt := g.pendingCount(id) - if cnt > 0 { - cons[id] = cnt - } - } - c.WriteLen(len(cons)) - var ids []string - for id := range cons { - ids = append(ids, id) - } - sort.Strings(ids) // be predicatable - for _, id := range ids { - c.WriteLen(2) - c.WriteBulk(id) - c.WriteBulk(strconv.Itoa(cons[id])) - } -} - -func writeXpending( - now time.Time, - c *server.Peer, - g streamGroup, - idle time.Duration, - start, - end string, - count int, - consumer *string, -) { - if len(g.pending) == 0 || count < 0 { - c.WriteLen(0) - return - } - - // format, list of: - // - message ID - // - consumer - // - milliseconds since delivery - // - delivery count - type entry struct { - id string - consumer string - millis int - count int - } - var res []entry - for _, p := range g.pending { - if len(res) >= count { - break - } - if consumer != nil && p.consumer != *consumer { - continue - } - if streamCmp(p.id, start) < 0 { - continue - } - if streamCmp(p.id, end) > 0 { - continue - } - timeSinceLastDelivery := now.Sub(p.lastDelivery) - if timeSinceLastDelivery >= idle { - res = append(res, entry{ - id: p.id, - consumer: p.consumer, - millis: int(timeSinceLastDelivery.Milliseconds()), - count: p.deliveryCount, - }) - } - } - c.WriteLen(len(res)) - for _, e := range res { - c.WriteLen(4) - c.WriteBulk(e.id) - c.WriteBulk(e.consumer) - c.WriteInt(e.millis) - c.WriteInt(e.count) - } -} - -// XTRIM -func (m *Miniredis) cmdXtrim(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, atLeast(3)) { - return - } - - var opts struct { - stream string - strategy string - maxLen int // for MAXLEN - threshold string // for MINID - withLimit bool // "LIMIT" - withExact bool // "=" - withNearly bool // "~" - } - - opts.stream, opts.strategy, args = args[0], strings.ToUpper(args[1]), args[2:] - - if opts.strategy != "MAXLEN" && opts.strategy != "MINID" { - setDirty(c) - c.WriteError(msgXtrimInvalidStrategy) - return - } - - // Ignore nearly exact trimming parameters. - switch args[0] { - case "=": - opts.withExact = true - args = args[1:] - case "~": - opts.withNearly = true - args = args[1:] - } - - switch opts.strategy { - case "MAXLEN": - maxLen, err := strconv.Atoi(args[0]) - if err != nil { - setDirty(c) - c.WriteError(msgXtrimInvalidMaxLen) - return - } - opts.maxLen = maxLen - case "MINID": - opts.threshold = args[0] - } - args = args[1:] - - if len(args) == 2 && strings.ToUpper(args[0]) == "LIMIT" { - // Ignore LIMIT. - opts.withLimit = true - if _, err := strconv.Atoi(args[1]); err != nil { - setDirty(c) - c.WriteError(msgInvalidInt) - return - } - - args = args[2:] - } - - if len(args) != 0 { - setDirty(c) - c.WriteError(fmt.Sprintf("ERR incorrect argument %s", args[0])) - return - } - - if opts.withLimit && !opts.withNearly { - setDirty(c) - c.WriteError(fmt.Sprintf(msgXtrimInvalidLimit)) - return - } - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - db := m.db(ctx.selectedDB) - s, err := db.stream(opts.stream) - if err != nil { - setDirty(c) - c.WriteError(err.Error()) - return - } - if s == nil { - c.WriteInt(0) - return - } - - switch opts.strategy { - case "MAXLEN": - entriesBefore := len(s.entries) - s.trim(opts.maxLen) - c.WriteInt(entriesBefore - len(s.entries)) - case "MINID": - n := s.trimBefore(opts.threshold) - c.WriteInt(n) - } - }) -} - -// XAUTOCLAIM -func (m *Miniredis) cmdXautoclaim(c *server.Peer, cmd string, args []string) { - // XAUTOCLAIM key group consumer min-idle-time start - if !m.isValidCMD(c, cmd, args, atLeast(5)) { - return - } - - var opts struct { - key string - group string - consumer string - minIdleTime time.Duration - start string - justId bool - count int - } - - opts.key, opts.group, opts.consumer = args[0], args[1], args[2] - n, err := strconv.Atoi(args[3]) - if err != nil { - setDirty(c) - c.WriteError("ERR Invalid min-idle-time argument for XAUTOCLAIM") - return - } - opts.minIdleTime = time.Millisecond * time.Duration(n) - - start_, err := formatStreamRangeBound(args[4], true, false) - if err != nil { - c.WriteError(msgInvalidStreamID) - return - } - opts.start = start_ - - args = args[5:] - - opts.count = 100 -parsing: - for len(args) > 0 { - switch strings.ToUpper(args[0]) { - case "COUNT": - if len(args) < 2 { - err = errors.New(errWrongNumber(cmd)) - break parsing - } - - opts.count, err = strconv.Atoi(args[1]) - if err != nil { - break parsing - } - - args = args[2:] - case "JUSTID": - args = args[1:] - opts.justId = true - default: - err = errors.New(msgSyntaxError) - break parsing - } - } - - if err != nil { - setDirty(c) - c.WriteError(err.Error()) - return - } - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - db := m.db(ctx.selectedDB) - g, err := db.streamGroup(opts.key, opts.group) - if err != nil { - c.WriteError(err.Error()) - return - } - if g == nil { - c.WriteError(errReadgroup(opts.key, opts.group).Error()) - return - } - - nextCallId, entries := xautoclaim(m.effectiveNow(), *g, opts.minIdleTime, opts.start, opts.count, opts.consumer) - writeXautoclaim(c, nextCallId, entries, opts.justId) - }) -} - -func xautoclaim( - now time.Time, - g streamGroup, - minIdleTime time.Duration, - start string, - count int, - consumerID string, -) (string, []StreamEntry) { - nextCallId := "0-0" - if len(g.pending) == 0 || count < 0 { - return nextCallId, nil - } - - msgs := g.pendingAfterOrEqual(start) - var res []StreamEntry - for i, p := range msgs { - if minIdleTime > 0 && now.Before(p.lastDelivery.Add(minIdleTime)) { - continue - } - - prevConsumerID := p.consumer - if _, ok := g.consumers[consumerID]; !ok { - g.consumers[consumerID] = &consumer{} - } - p.consumer = consumerID - - _, entry := g.stream.get(p.id) - // not found. Weird? - if entry == nil { - // TODO: support third element of return from XAUTOCLAIM, which - // should delete entries not found in the PEL during XAUTOCLAIM. - // (Introduced in Redis 7.0) - continue - } - - p.deliveryCount += 1 - p.lastDelivery = now - - g.consumers[prevConsumerID].numPendingEntries-- - g.consumers[consumerID].numPendingEntries++ - - msgs[i] = p - res = append(res, *entry) - - if len(res) >= count { - if len(msgs) > i+1 { - nextCallId = msgs[i+1].id - } - break - } - } - return nextCallId, res -} - -func writeXautoclaim(c *server.Peer, nextCallId string, res []StreamEntry, justId bool) { - c.WriteLen(3) - c.WriteBulk(nextCallId) - c.WriteLen(len(res)) - for _, entry := range res { - if justId { - c.WriteBulk(entry.ID) - continue - } - - c.WriteLen(2) - c.WriteBulk(entry.ID) - c.WriteLen(len(entry.Values)) - for _, v := range entry.Values { - c.WriteBulk(v) - } - } - // TODO: see "Redis 7" note - c.WriteLen(0) -} - -// XCLAIM -func (m *Miniredis) cmdXclaim(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, atLeast(5)) { - return - } - - var opts struct { - key string - groupName string - consumerName string - minIdleTime time.Duration - newLastDelivery time.Time - ids []string - retryCount *int - force bool - justId bool - } - - opts.key, opts.groupName, opts.consumerName = args[0], args[1], args[2] - - minIdleTimeMillis, err := strconv.Atoi(args[3]) - if err != nil { - setDirty(c) - c.WriteError("ERR Invalid min-idle-time argument for XCLAIM") - return - } - opts.minIdleTime = time.Millisecond * time.Duration(minIdleTimeMillis) - - opts.newLastDelivery = m.effectiveNow() - opts.ids = append(opts.ids, args[4]) - - args = args[5:] - for len(args) > 0 { - arg := strings.ToUpper(args[0]) - if arg == "IDLE" || - arg == "TIME" || - arg == "RETRYCOUNT" || - arg == "FORCE" || - arg == "JUSTID" { - break - } - opts.ids = append(opts.ids, arg) - args = args[1:] - } - - for len(args) > 0 { - arg := strings.ToUpper(args[0]) - switch arg { - case "IDLE": - idleMs, err := strconv.ParseInt(args[1], 10, 64) - if err != nil { - setDirty(c) - c.WriteError("ERR Invalid IDLE option argument for XCLAIM") - return - } - if idleMs < 0 { - idleMs = 0 - } - opts.newLastDelivery = m.effectiveNow().Add(time.Millisecond * time.Duration(-idleMs)) - args = args[2:] - case "TIME": - timeMs, err := strconv.ParseInt(args[1], 10, 64) - if err != nil { - setDirty(c) - c.WriteError("ERR Invalid TIME option argument for XCLAIM") - return - } - opts.newLastDelivery = time.UnixMilli(timeMs) - args = args[2:] - case "RETRYCOUNT": - retryCount, err := strconv.Atoi(args[1]) - if err != nil { - setDirty(c) - c.WriteError("ERR Invalid RETRYCOUNT option argument for XCLAIM") - return - } - opts.retryCount = &retryCount - args = args[2:] - case "FORCE": - opts.force = true - args = args[1:] - case "JUSTID": - opts.justId = true - args = args[1:] - default: - setDirty(c) - c.WriteError(fmt.Sprintf("ERR Unrecognized XCLAIM option '%s'", args[0])) - return - } - } - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - db := m.db(ctx.selectedDB) - - g, err := db.streamGroup(opts.key, opts.groupName) - if err != nil { - c.WriteError(err.Error()) - return - } - if g == nil { - c.WriteError(errReadgroup(opts.key, opts.groupName).Error()) - return - } - - claimedEntryIDs := m.xclaim(g, opts.consumerName, opts.minIdleTime, opts.newLastDelivery, opts.ids, opts.retryCount, opts.force) - writeXclaim(c, g.stream, claimedEntryIDs, opts.justId) - }) -} - -func (m *Miniredis) xclaim( - group *streamGroup, - consumerName string, - minIdleTime time.Duration, - newLastDelivery time.Time, - ids []string, - retryCount *int, - force bool, -) (claimedEntryIDs []string) { - for _, id := range ids { - pelPos, pelEntry := group.searchPending(id) - if pelEntry == nil { - group.setLastSeen(consumerName, m.effectiveNow()) - if !force { - continue - } - - if pelPos < len(group.pending) { - group.pending = append(group.pending[:pelPos+1], group.pending[pelPos:]...) - } else { - group.pending = append(group.pending, pendingEntry{}) - } - pelEntry = &group.pending[pelPos] - - *pelEntry = pendingEntry{ - id: id, - consumer: consumerName, - deliveryCount: 1, - } - group.setLastSuccess(consumerName, m.effectiveNow()) - } else { - group.consumers[pelEntry.consumer].numPendingEntries-- - pelEntry.consumer = consumerName - } - - if retryCount != nil { - pelEntry.deliveryCount = *retryCount - } else { - pelEntry.deliveryCount++ - } - pelEntry.lastDelivery = newLastDelivery - - // redis7: don't report entries which are deleted by now - if _, e := group.stream.get(id); e == nil { - continue - } - - claimedEntryIDs = append(claimedEntryIDs, id) - } - if len(claimedEntryIDs) == 0 { - group.setLastSeen(consumerName, m.effectiveNow()) - return - } - - if _, ok := group.consumers[consumerName]; !ok { - group.consumers[consumerName] = &consumer{} - } - consumer := group.consumers[consumerName] - consumer.numPendingEntries += len(claimedEntryIDs) - - group.setLastSuccess(consumerName, m.effectiveNow()) - return -} - -func writeXclaim(c *server.Peer, stream *streamKey, claimedEntryIDs []string, justId bool) { - c.WriteLen(len(claimedEntryIDs)) - for _, id := range claimedEntryIDs { - if justId { - c.WriteBulk(id) - continue - } - - _, entry := stream.get(id) - if entry == nil { - c.WriteNull() - continue - } - - c.WriteLen(2) - c.WriteBulk(entry.ID) - c.WriteStrings(entry.Values) - } -} - -func parseBlock(cmd string, args []string, block *bool, timeout *time.Duration) error { - if len(args) < 2 { - return errors.New(errWrongNumber(cmd)) - } - (*block) = true - ms, err := strconv.Atoi(args[1]) - if err != nil { - return errors.New(msgInvalidInt) - } - if ms < 0 { - return errors.New("ERR timeout is negative") - } - (*timeout) = time.Millisecond * time.Duration(ms) - return nil -} diff --git a/vendor/github.com/alicebob/miniredis/v2/cmd_string.go b/vendor/github.com/alicebob/miniredis/v2/cmd_string.go deleted file mode 100644 index 61c673270..000000000 --- a/vendor/github.com/alicebob/miniredis/v2/cmd_string.go +++ /dev/null @@ -1,1166 +0,0 @@ -// Commands from https://redis.io/commands#string - -package miniredis - -import ( - "math/big" - "math/bits" - "strconv" - "strings" - "time" - - "github.com/alicebob/miniredis/v2/server" -) - -// commandsString handles all string value operations. -func commandsString(m *Miniredis) { - m.srv.Register("APPEND", m.cmdAppend) - m.srv.Register("BITCOUNT", m.cmdBitcount, server.ReadOnlyOption()) - m.srv.Register("BITOP", m.cmdBitop) - m.srv.Register("BITPOS", m.cmdBitpos, server.ReadOnlyOption()) - m.srv.Register("DECRBY", m.cmdDecrby) - m.srv.Register("DECR", m.cmdDecr) - m.srv.Register("GETBIT", m.cmdGetbit, server.ReadOnlyOption()) - m.srv.Register("GET", m.cmdGet, server.ReadOnlyOption()) - m.srv.Register("GETEX", m.cmdGetex) - m.srv.Register("GETRANGE", m.cmdGetrange, server.ReadOnlyOption()) - m.srv.Register("GETSET", m.cmdGetset) - m.srv.Register("GETDEL", m.cmdGetdel) - m.srv.Register("INCRBYFLOAT", m.cmdIncrbyfloat) - m.srv.Register("INCRBY", m.cmdIncrby) - m.srv.Register("INCR", m.cmdIncr) - m.srv.Register("MGET", m.cmdMget, server.ReadOnlyOption()) - m.srv.Register("MSET", m.cmdMset) - m.srv.Register("MSETNX", m.cmdMsetnx) - m.srv.Register("PSETEX", m.cmdPsetex) - m.srv.Register("SETBIT", m.cmdSetbit) - m.srv.Register("SETEX", m.cmdSetex) - m.srv.Register("SET", m.cmdSet) - m.srv.Register("SETNX", m.cmdSetnx) - m.srv.Register("SETRANGE", m.cmdSetrange) - m.srv.Register("STRLEN", m.cmdStrlen, server.ReadOnlyOption()) -} - -// SET -func (m *Miniredis) cmdSet(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, atLeast(2)) { - return - } - - var opts struct { - key string - value string - nx bool // set iff not exists - xx bool // set iff exists - keepttl bool // set keepttl - ttlSet bool - ttl time.Duration - get bool - } - - opts.key, opts.value, args = args[0], args[1], args[2:] - for len(args) > 0 { - timeUnit := time.Second - switch arg := strings.ToUpper(args[0]); arg { - case "NX": - opts.nx = true - args = args[1:] - continue - case "XX": - opts.xx = true - args = args[1:] - continue - case "KEEPTTL": - opts.keepttl = true - args = args[1:] - continue - case "PX", "PXAT": - timeUnit = time.Millisecond - fallthrough - case "EX", "EXAT": - if len(args) < 2 { - setDirty(c) - c.WriteError(msgInvalidInt) - return - } - if opts.ttlSet { - // multiple ex/exat/px/pxat options set - setDirty(c) - c.WriteError(msgSyntaxError) - return - } - expire, err := strconv.Atoi(args[1]) - if err != nil { - setDirty(c) - c.WriteError(msgInvalidInt) - return - } - if expire <= 0 { - setDirty(c) - c.WriteError(msgInvalidSETime) - return - } - - if arg == "PXAT" || arg == "EXAT" { - opts.ttl = m.at(expire, timeUnit) - } else { - opts.ttl = time.Duration(expire) * timeUnit - } - opts.ttlSet = true - - args = args[2:] - continue - case "GET": - opts.get = true - args = args[1:] - continue - default: - setDirty(c) - c.WriteError(msgSyntaxError) - return - } - } - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - db := m.db(ctx.selectedDB) - - readonly := false - if opts.nx { - if db.exists(opts.key) { - if opts.get { - // special case for SET NX GET - readonly = true - } else { - c.WriteNull() - return - } - } - } - if opts.xx { - if !db.exists(opts.key) { - if opts.get { - // special case for SET XX GET - readonly = true - } else { - c.WriteNull() - return - } - } - } - if opts.keepttl { - if val, ok := db.ttl[opts.key]; ok { - opts.ttl = val - } - } - if opts.get { - if t, ok := db.keys[opts.key]; ok && t != keyTypeString { - c.WriteError(msgWrongType) - return - } - } - - old, existed := db.stringKeys[opts.key] - if !readonly { - db.del(opts.key, true) // be sure to remove existing values of other type keys. - // a vanilla SET clears the expire - if opts.ttl >= 0 { // EXAT/PXAT can expire right away - db.stringSet(opts.key, opts.value) - } - if opts.ttl != 0 { - db.ttl[opts.key] = opts.ttl - } - } - if opts.get { - if !existed { - c.WriteNull() - } else { - c.WriteBulk(old) - } - return - } - c.WriteOK() - }) -} - -// SETEX -func (m *Miniredis) cmdSetex(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, exactly(3)) { - return - } - - key := args[0] - ttl, err := strconv.Atoi(args[1]) - if err != nil { - setDirty(c) - c.WriteError(msgInvalidInt) - return - } - if ttl <= 0 { - setDirty(c) - c.WriteError(msgInvalidSETEXTime) - return - } - value := args[2] - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - db := m.db(ctx.selectedDB) - - db.del(key, true) // Clear any existing keys. - db.stringSet(key, value) - db.ttl[key] = time.Duration(ttl) * time.Second - c.WriteOK() - }) -} - -// PSETEX -func (m *Miniredis) cmdPsetex(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, exactly(3)) { - return - } - - var opts struct { - key string - ttl int - value string - } - - opts.key = args[0] - if ok := optInt(c, args[1], &opts.ttl); !ok { - return - } - if opts.ttl <= 0 { - setDirty(c) - c.WriteError(msgInvalidPSETEXTime) - return - } - opts.value = args[2] - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - db := m.db(ctx.selectedDB) - - db.del(opts.key, true) // Clear any existing keys. - db.stringSet(opts.key, opts.value) - db.ttl[opts.key] = time.Duration(opts.ttl) * time.Millisecond - c.WriteOK() - }) -} - -// SETNX -func (m *Miniredis) cmdSetnx(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, exactly(2)) { - return - } - - key, value := args[0], args[1] - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - db := m.db(ctx.selectedDB) - - if _, ok := db.keys[key]; ok { - c.WriteInt(0) - return - } - - db.stringSet(key, value) - c.WriteInt(1) - }) -} - -// MSET -func (m *Miniredis) cmdMset(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, atLeast(2)) { - return - } - - if len(args)%2 != 0 { - setDirty(c) - // non-default error message - c.WriteError("ERR wrong number of arguments for MSET") - return - } - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - db := m.db(ctx.selectedDB) - - for len(args) > 0 { - key, value := args[0], args[1] - args = args[2:] - - db.del(key, true) // clear TTL - db.stringSet(key, value) - } - c.WriteOK() - }) -} - -// MSETNX -func (m *Miniredis) cmdMsetnx(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, atLeast(2)) { - return - } - - if len(args)%2 != 0 { - setDirty(c) - // non-default error message (yes, with 'MSET'). - c.WriteError("ERR wrong number of arguments for MSET") - return - } - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - db := m.db(ctx.selectedDB) - - keys := map[string]string{} - existing := false - for len(args) > 0 { - key := args[0] - value := args[1] - args = args[2:] - keys[key] = value - if _, ok := db.keys[key]; ok { - existing = true - } - } - - res := 0 - if !existing { - res = 1 - for k, v := range keys { - // Nothing to delete. That's the whole point. - db.stringSet(k, v) - } - } - c.WriteInt(res) - }) -} - -// GET -func (m *Miniredis) cmdGet(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, exactly(1)) { - return - } - - key := args[0] - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - db := m.db(ctx.selectedDB) - - if !db.exists(key) { - c.WriteNull() - return - } - if db.t(key) != keyTypeString { - c.WriteError(msgWrongType) - return - } - - c.WriteBulk(db.stringGet(key)) - }) -} - -// GETEX -func (m *Miniredis) cmdGetex(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, atLeast(1)) { - return - } - - var opts struct { - key string - ttl time.Duration - persist bool // remove existing TTL on the key. - } - - opts.key, args = args[0], args[1:] - if len(args) > 0 { - timeUnit := time.Second - switch arg := strings.ToUpper(args[0]); arg { - case "PERSIST": - if len(args) > 1 { - setDirty(c) - c.WriteError(msgSyntaxError) - return - } - opts.persist = true - case "PX", "PXAT": - timeUnit = time.Millisecond - fallthrough - case "EX", "EXAT": - if len(args) != 2 { - setDirty(c) - c.WriteError(msgSyntaxError) - return - } - expire, err := strconv.Atoi(args[1]) - if err != nil { - setDirty(c) - c.WriteError(msgInvalidInt) - return - } - if expire <= 0 { - setDirty(c) - c.WriteError(msgInvalidSETime) - return - } - - if arg == "PXAT" || arg == "EXAT" { - opts.ttl = m.at(expire, timeUnit) - } else { - opts.ttl = time.Duration(expire) * timeUnit - } - default: - setDirty(c) - c.WriteError(msgSyntaxError) - return - } - } - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - db := m.db(ctx.selectedDB) - - if !db.exists(opts.key) { - c.WriteNull() - return - } - switch { - case opts.persist: - delete(db.ttl, opts.key) - case opts.ttl != 0: - db.ttl[opts.key] = opts.ttl - } - - if db.t(opts.key) != keyTypeString { - c.WriteError(msgWrongType) - return - } - - c.WriteBulk(db.stringGet(opts.key)) - }) -} - -// GETSET -func (m *Miniredis) cmdGetset(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, exactly(2)) { - return - } - - key, value := args[0], args[1] - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - db := m.db(ctx.selectedDB) - - if t, ok := db.keys[key]; ok && t != keyTypeString { - c.WriteError(msgWrongType) - return - } - - old, ok := db.stringKeys[key] - db.stringSet(key, value) - // a GETSET clears the ttl - delete(db.ttl, key) - - if !ok { - c.WriteNull() - return - } - c.WriteBulk(old) - }) -} - -// GETDEL -func (m *Miniredis) cmdGetdel(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, exactly(1)) { - return - } - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - db := m.db(ctx.selectedDB) - - key := args[0] - - if !db.exists(key) { - c.WriteNull() - return - } - - if db.t(key) != keyTypeString { - c.WriteError(msgWrongType) - return - } - - v := db.stringGet(key) - db.del(key, true) - c.WriteBulk(v) - }) -} - -// MGET -func (m *Miniredis) cmdMget(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, atLeast(1)) { - return - } - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - db := m.db(ctx.selectedDB) - - c.WriteLen(len(args)) - for _, k := range args { - if t, ok := db.keys[k]; !ok || t != keyTypeString { - c.WriteNull() - continue - } - v, ok := db.stringKeys[k] - if !ok { - // Should not happen, we just checked keys[] - c.WriteNull() - continue - } - c.WriteBulk(v) - } - }) -} - -// INCR -func (m *Miniredis) cmdIncr(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, exactly(1)) { - return - } - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - db := m.db(ctx.selectedDB) - - key := args[0] - if t, ok := db.keys[key]; ok && t != keyTypeString { - c.WriteError(msgWrongType) - return - } - v, err := db.stringIncr(key, +1) - if err != nil { - c.WriteError(err.Error()) - return - } - // Don't touch TTL - c.WriteInt(v) - }) -} - -// INCRBY -func (m *Miniredis) cmdIncrby(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, exactly(2)) { - return - } - - var opts struct { - key string - delta int - } - opts.key = args[0] - if ok := optInt(c, args[1], &opts.delta); !ok { - return - } - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - db := m.db(ctx.selectedDB) - - if t, ok := db.keys[opts.key]; ok && t != keyTypeString { - c.WriteError(msgWrongType) - return - } - - v, err := db.stringIncr(opts.key, opts.delta) - if err != nil { - c.WriteError(err.Error()) - return - } - // Don't touch TTL - c.WriteInt(v) - }) -} - -// INCRBYFLOAT -func (m *Miniredis) cmdIncrbyfloat(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, exactly(2)) { - return - } - - key := args[0] - delta, _, err := big.ParseFloat(args[1], 10, 128, 0) - if err != nil { - setDirty(c) - c.WriteError(msgInvalidFloat) - return - } - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - db := m.db(ctx.selectedDB) - - if t, ok := db.keys[key]; ok && t != keyTypeString { - c.WriteError(msgWrongType) - return - } - - v, err := db.stringIncrfloat(key, delta) - if err != nil { - c.WriteError(err.Error()) - return - } - // Don't touch TTL - c.WriteBulk(formatBig(v)) - }) -} - -// DECR -func (m *Miniredis) cmdDecr(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, exactly(1)) { - return - } - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - db := m.db(ctx.selectedDB) - - key := args[0] - if t, ok := db.keys[key]; ok && t != keyTypeString { - c.WriteError(msgWrongType) - return - } - v, err := db.stringIncr(key, -1) - if err != nil { - c.WriteError(err.Error()) - return - } - // Don't touch TTL - c.WriteInt(v) - }) -} - -// DECRBY -func (m *Miniredis) cmdDecrby(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, exactly(2)) { - return - } - - var opts struct { - key string - delta int - } - opts.key = args[0] - if ok := optInt(c, args[1], &opts.delta); !ok { - return - } - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - db := m.db(ctx.selectedDB) - - if t, ok := db.keys[opts.key]; ok && t != keyTypeString { - c.WriteError(msgWrongType) - return - } - - v, err := db.stringIncr(opts.key, -opts.delta) - if err != nil { - c.WriteError(err.Error()) - return - } - // Don't touch TTL - c.WriteInt(v) - }) -} - -// STRLEN -func (m *Miniredis) cmdStrlen(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, exactly(1)) { - return - } - - key := args[0] - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - db := m.db(ctx.selectedDB) - - if t, ok := db.keys[key]; ok && t != keyTypeString { - c.WriteError(msgWrongType) - return - } - - c.WriteInt(len(db.stringKeys[key])) - }) -} - -// APPEND -func (m *Miniredis) cmdAppend(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, exactly(2)) { - return - } - - key, value := args[0], args[1] - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - db := m.db(ctx.selectedDB) - - if t, ok := db.keys[key]; ok && t != keyTypeString { - c.WriteError(msgWrongType) - return - } - - newValue := db.stringKeys[key] + value - db.stringSet(key, newValue) - - c.WriteInt(len(newValue)) - }) -} - -// GETRANGE -func (m *Miniredis) cmdGetrange(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, exactly(3)) { - return - } - - var opts struct { - key string - start int - end int - } - opts.key = args[0] - if ok := optInt(c, args[1], &opts.start); !ok { - return - } - if ok := optInt(c, args[2], &opts.end); !ok { - return - } - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - db := m.db(ctx.selectedDB) - - if t, ok := db.keys[opts.key]; ok && t != keyTypeString { - c.WriteError(msgWrongType) - return - } - - v := db.stringKeys[opts.key] - c.WriteBulk(withRange(v, opts.start, opts.end)) - }) -} - -// SETRANGE -func (m *Miniredis) cmdSetrange(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, exactly(3)) { - return - } - - var opts struct { - key string - pos int - subst string - } - opts.key = args[0] - if ok := optInt(c, args[1], &opts.pos); !ok { - return - } - if opts.pos < 0 { - setDirty(c) - c.WriteError("ERR offset is out of range") - return - } - opts.subst = args[2] - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - db := m.db(ctx.selectedDB) - - if t, ok := db.keys[opts.key]; ok && t != keyTypeString { - c.WriteError(msgWrongType) - return - } - - v := []byte(db.stringKeys[opts.key]) - end := opts.pos + len(opts.subst) - if len(v) < end { - newV := make([]byte, end) - copy(newV, v) - v = newV - } - copy(v[opts.pos:end], opts.subst) - db.stringSet(opts.key, string(v)) - c.WriteInt(len(v)) - }) -} - -// BITCOUNT -func (m *Miniredis) cmdBitcount(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, atLeast(1)) { - return - } - - var opts struct { - useRange bool - start int - end int - key string - } - opts.key, args = args[0], args[1:] - if len(args) >= 2 { - opts.useRange = true - if ok := optInt(c, args[0], &opts.start); !ok { - return - } - if ok := optInt(c, args[1], &opts.end); !ok { - return - } - args = args[2:] - } - if len(args) != 0 { - c.WriteError(msgSyntaxError) - return - } - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - db := m.db(ctx.selectedDB) - - if !db.exists(opts.key) { - c.WriteInt(0) - return - } - if db.t(opts.key) != keyTypeString { - c.WriteError(msgWrongType) - return - } - - // Real redis only checks after it knows the key is there and a string. - if len(args) != 0 { - c.WriteError(msgSyntaxError) - return - } - - v := db.stringKeys[opts.key] - if opts.useRange { - v = withRange(v, opts.start, opts.end) - } - - c.WriteInt(countBits([]byte(v))) - }) -} - -// BITOP -func (m *Miniredis) cmdBitop(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, atLeast(3)) { - return - } - - var opts struct { - op string - target string - input []string - } - opts.op = strings.ToUpper(args[0]) - opts.target = args[1] - opts.input = args[2:] - - // 'op' is tested when the transaction is executed. - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - db := m.db(ctx.selectedDB) - - switch opts.op { - case "AND", "OR", "XOR": - first := opts.input[0] - if t, ok := db.keys[first]; ok && t != keyTypeString { - c.WriteError(msgWrongType) - return - } - res := []byte(db.stringKeys[first]) - for _, vk := range opts.input[1:] { - if t, ok := db.keys[vk]; ok && t != keyTypeString { - c.WriteError(msgWrongType) - return - } - v := db.stringKeys[vk] - cb := map[string]func(byte, byte) byte{ - "AND": func(a, b byte) byte { return a & b }, - "OR": func(a, b byte) byte { return a | b }, - "XOR": func(a, b byte) byte { return a ^ b }, - }[opts.op] - res = sliceBinOp(cb, res, []byte(v)) - } - db.del(opts.target, false) // Keep TTL - if len(res) == 0 { - db.del(opts.target, true) - } else { - db.stringSet(opts.target, string(res)) - } - c.WriteInt(len(res)) - case "NOT": - // NOT only takes a single argument. - if len(opts.input) != 1 { - c.WriteError("ERR BITOP NOT must be called with a single source key.") - return - } - key := opts.input[0] - if t, ok := db.keys[key]; ok && t != keyTypeString { - c.WriteError(msgWrongType) - return - } - value := []byte(db.stringKeys[key]) - for i := range value { - value[i] = ^value[i] - } - db.del(opts.target, false) // Keep TTL - if len(value) == 0 { - db.del(opts.target, true) - } else { - db.stringSet(opts.target, string(value)) - } - c.WriteInt(len(value)) - default: - c.WriteError(msgSyntaxError) - } - }) -} - -// BITPOS -func (m *Miniredis) cmdBitpos(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, between(2, 4)) { - return - } - - var opts struct { - Key string - Bit int - Start int - End int - WithEnd bool - } - - opts.Key = args[0] - if ok := optInt(c, args[1], &opts.Bit); !ok { - return - } - if len(args) > 2 { - if ok := optInt(c, args[2], &opts.Start); !ok { - return - } - } - if len(args) > 3 { - if ok := optInt(c, args[3], &opts.End); !ok { - return - } - opts.WithEnd = true - } - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - db := m.db(ctx.selectedDB) - - if t, ok := db.keys[opts.Key]; ok && t != keyTypeString { - c.WriteError(msgWrongType) - return - } else if !ok { - // non-existing key behaves differently - if opts.Bit == 0 { - c.WriteInt(0) - } else { - c.WriteInt(-1) - } - return - } - value := db.stringKeys[opts.Key] - start := opts.Start - end := opts.End - if start < 0 { - start += len(value) - if start < 0 { - start = 0 - } - } - if start > len(value) { - start = len(value) - } - - if opts.WithEnd { - if end < 0 { - end += len(value) - } - if end < 0 { - end = 0 - } - end++ // +1 for redis end semantics - if end > len(value) { - end = len(value) - } - } else { - end = len(value) - } - - if start != 0 || opts.WithEnd { - if end < start { - value = "" - } else { - value = value[start:end] - } - } - pos := bitPos([]byte(value), opts.Bit == 1) - if pos >= 0 { - pos += start * 8 - } - // Special case when looking for 0, but not when start and end are - // given. - if opts.Bit == 0 && pos == -1 && !opts.WithEnd && len(value) > 0 { - pos = start*8 + len(value)*8 - } - c.WriteInt(pos) - }) -} - -// GETBIT -func (m *Miniredis) cmdGetbit(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, exactly(2)) { - return - } - - var opts struct { - key string - bit int - } - opts.key = args[0] - if ok := optIntErr(c, args[1], &opts.bit, "ERR bit offset is not an integer or out of range"); !ok { - return - } - if opts.bit < 0 { - setDirty(c) - c.WriteError("ERR bit offset is not an integer or out of range") - return - } - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - db := m.db(ctx.selectedDB) - - if t, ok := db.keys[opts.key]; ok && t != keyTypeString { - c.WriteError(msgWrongType) - return - } - value := db.stringKeys[opts.key] - - ourByteNr := opts.bit / 8 - var ourByte byte - if ourByteNr > len(value)-1 { - ourByte = '\x00' - } else { - ourByte = value[ourByteNr] - } - res := 0 - if toBits(ourByte)[opts.bit%8] { - res = 1 - } - c.WriteInt(res) - }) -} - -// SETBIT -func (m *Miniredis) cmdSetbit(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, exactly(3)) { - return - } - - var opts struct { - key string - bit int - newBit int - } - opts.key = args[0] - if ok := optIntErr(c, args[1], &opts.bit, "ERR bit offset is not an integer or out of range"); !ok { - return - } - if opts.bit < 0 { - setDirty(c) - c.WriteError("ERR bit offset is not an integer or out of range") - return - } - if ok := optIntErr(c, args[2], &opts.newBit, "ERR bit is not an integer or out of range"); !ok { - return - } - if opts.newBit != 0 && opts.newBit != 1 { - setDirty(c) - c.WriteError("ERR bit is not an integer or out of range") - return - } - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - db := m.db(ctx.selectedDB) - - if t, ok := db.keys[opts.key]; ok && t != keyTypeString { - c.WriteError(msgWrongType) - return - } - value := []byte(db.stringKeys[opts.key]) - - ourByteNr := opts.bit / 8 - ourBitNr := opts.bit % 8 - if ourByteNr > len(value)-1 { - // Too short. Expand. - newValue := make([]byte, ourByteNr+1) - copy(newValue, value) - value = newValue - } - old := 0 - if toBits(value[ourByteNr])[ourBitNr] { - old = 1 - } - if opts.newBit == 0 { - value[ourByteNr] &^= 1 << uint8(7-ourBitNr) - } else { - value[ourByteNr] |= 1 << uint8(7-ourBitNr) - } - db.stringSet(opts.key, string(value)) - - c.WriteInt(old) - }) -} - -// Redis range. both start and end can be negative. -func withRange(v string, start, end int) string { - s, e := redisRange(len(v), start, end, true /* string getrange symantics */) - return v[s:e] -} - -func countBits(v []byte) int { - count := 0 - for _, b := range []byte(v) { - count += bits.OnesCount8(uint8(b)) - } - return count -} - -// sliceBinOp applies an operator to all slice elements, with Redis string -// padding logic. -func sliceBinOp(f func(a, b byte) byte, a, b []byte) []byte { - maxl := len(a) - if len(b) > maxl { - maxl = len(b) - } - lA := make([]byte, maxl) - copy(lA, a) - lB := make([]byte, maxl) - copy(lB, b) - res := make([]byte, maxl) - for i := range res { - res[i] = f(lA[i], lB[i]) - } - return res -} - -// Return the number of the first bit set/unset. -func bitPos(s []byte, bit bool) int { - for i, b := range s { - for j, set := range toBits(b) { - if set == bit { - return i*8 + j - } - } - } - return -1 -} - -// toBits changes a byte in 8 bools. -func toBits(s byte) [8]bool { - r := [8]bool{} - for i := range r { - if s&(uint8(1)< version { - // Abort! Abort! - stopTx(ctx) - c.WriteLen(-1) - return - } - } - - c.WriteLen(len(ctx.transaction)) - for _, cb := range ctx.transaction { - cb(c, ctx) - } - // wake up anyone who waits on anything. - m.signal.Broadcast() - - stopTx(ctx) -} - -// DISCARD -func (m *Miniredis) cmdDiscard(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, exactly(0)) { - return - } - - ctx := getCtx(c) - if !inTx(ctx) { - c.WriteError("ERR DISCARD without MULTI") - return - } - - stopTx(ctx) - c.WriteOK() -} - -// WATCH -func (m *Miniredis) cmdWatch(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, atLeast(1)) { - return - } - - ctx := getCtx(c) - if ctx.nested { - c.WriteError(msgNotFromScripts(ctx.nestedSHA)) - return - } - if inTx(ctx) { - c.WriteError("ERR WATCH in MULTI") - return - } - - m.Lock() - defer m.Unlock() - db := m.db(ctx.selectedDB) - - for _, key := range args { - watch(db, ctx, key) - } - c.WriteOK() -} - -// UNWATCH -func (m *Miniredis) cmdUnwatch(c *server.Peer, cmd string, args []string) { - if !m.isValidCMD(c, cmd, args, exactly(0)) { - return - } - - // Doesn't matter if UNWATCH is in a TX or not. Looks like a Redis bug to me. - unwatch(getCtx(c)) - - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - // Do nothing if it's called in a transaction. - c.WriteOK() - }) -} diff --git a/vendor/github.com/alicebob/miniredis/v2/db.go b/vendor/github.com/alicebob/miniredis/v2/db.go deleted file mode 100644 index 97bdf7c3a..000000000 --- a/vendor/github.com/alicebob/miniredis/v2/db.go +++ /dev/null @@ -1,824 +0,0 @@ -package miniredis - -import ( - "errors" - "fmt" - "math" - "math/big" - "sort" - "strconv" - "time" -) - -var ( - errInvalidEntryID = errors.New("stream ID is invalid") -) - -// exists also updates the lru -func (db *RedisDB) exists(k string) bool { - _, ok := db.keys[k] - if ok { - db.lru[k] = db.master.effectiveNow() - } - return ok -} - -// t gives the type of a key, or "" -func (db *RedisDB) t(k string) string { - return db.keys[k] -} - -// incr increases the version and the lru timestamp -func (db *RedisDB) incr(k string) { - db.lru[k] = db.master.effectiveNow() - db.keyVersion[k]++ -} - -// allKeys returns all keys. Sorted. -func (db *RedisDB) allKeys() []string { - res := make([]string, 0, len(db.keys)) - for k := range db.keys { - res = append(res, k) - } - sort.Strings(res) // To make things deterministic. - return res -} - -// flush removes all keys and values. -func (db *RedisDB) flush() { - db.keys = map[string]string{} - db.lru = map[string]time.Time{} - db.stringKeys = map[string]string{} - db.hashKeys = map[string]hashKey{} - db.listKeys = map[string]listKey{} - db.setKeys = map[string]setKey{} - db.hllKeys = map[string]*hll{} - db.sortedsetKeys = map[string]sortedSet{} - db.ttl = map[string]time.Duration{} - db.hashTTLs = map[string]map[string]time.Duration{} - db.streamKeys = map[string]*streamKey{} -} - -// move something to another db. Will return ok. Or not. -func (db *RedisDB) move(key string, to *RedisDB) bool { - if _, ok := to.keys[key]; ok { - return false - } - - t, ok := db.keys[key] - if !ok { - return false - } - to.keys[key] = db.keys[key] - switch t { - case keyTypeString: - to.stringKeys[key] = db.stringKeys[key] - case keyTypeHash: - to.hashKeys[key] = db.hashKeys[key] - if fieldTTLs, ok := db.hashTTLs[key]; ok { - to.hashTTLs[key] = fieldTTLs - } - case keyTypeList: - to.listKeys[key] = db.listKeys[key] - case keyTypeSet: - to.setKeys[key] = db.setKeys[key] - case keyTypeSortedSet: - to.sortedsetKeys[key] = db.sortedsetKeys[key] - case keyTypeStream: - to.streamKeys[key] = db.streamKeys[key] - case keyTypeHll: - to.hllKeys[key] = db.hllKeys[key] - default: - panic("unhandled key type") - } - if v, ok := db.ttl[key]; ok { - to.ttl[key] = v - } - to.incr(key) - db.del(key, true) - return true -} - -func (db *RedisDB) rename(from, to string) { - db.del(to, true) - switch db.t(from) { - case keyTypeString: - db.stringKeys[to] = db.stringKeys[from] - case keyTypeHash: - db.hashKeys[to] = db.hashKeys[from] - if fieldTTLs, ok := db.hashTTLs[from]; ok { - db.hashTTLs[to] = fieldTTLs - } - case keyTypeList: - db.listKeys[to] = db.listKeys[from] - case keyTypeSet: - db.setKeys[to] = db.setKeys[from] - case keyTypeSortedSet: - db.sortedsetKeys[to] = db.sortedsetKeys[from] - case keyTypeStream: - db.streamKeys[to] = db.streamKeys[from] - case keyTypeHll: - db.hllKeys[to] = db.hllKeys[from] - default: - panic("missing case") - } - db.keys[to] = db.keys[from] - if v, ok := db.ttl[from]; ok { - db.ttl[to] = v - } - db.incr(to) - - db.del(from, true) -} - -func (db *RedisDB) del(k string, delTTL bool) { - if !db.exists(k) { - return - } - t := db.t(k) - delete(db.keys, k) - delete(db.lru, k) - db.keyVersion[k]++ - if delTTL { - delete(db.ttl, k) - } - switch t { - case keyTypeString: - delete(db.stringKeys, k) - case keyTypeHash: - delete(db.hashKeys, k) - delete(db.hashTTLs, k) - case keyTypeList: - delete(db.listKeys, k) - case keyTypeSet: - delete(db.setKeys, k) - case keyTypeSortedSet: - delete(db.sortedsetKeys, k) - case keyTypeStream: - delete(db.streamKeys, k) - case keyTypeHll: - delete(db.hllKeys, k) - default: - panic("Unknown key type: " + t) - } -} - -// stringGet returns the string key or "" on error/nonexists. -func (db *RedisDB) stringGet(k string) string { - if t, ok := db.keys[k]; !ok || t != keyTypeString { - return "" - } - return db.stringKeys[k] -} - -// stringSet force set()s a key. Does not touch expire. -func (db *RedisDB) stringSet(k, v string) { - db.del(k, false) - db.keys[k] = keyTypeString - db.stringKeys[k] = v - db.incr(k) -} - -// change int key value -func (db *RedisDB) stringIncr(k string, delta int) (int, error) { - v := 0 - if sv, ok := db.stringKeys[k]; ok { - var err error - v, err = strconv.Atoi(sv) - if err != nil { - return 0, ErrIntValueError - } - } - - if delta > 0 { - if math.MaxInt-delta < v { - return 0, ErrIntValueOverflowError - } - } else { - if math.MinInt-delta > v { - return 0, ErrIntValueOverflowError - } - } - - v += delta - db.stringSet(k, strconv.Itoa(v)) - return v, nil -} - -// change float key value -func (db *RedisDB) stringIncrfloat(k string, delta *big.Float) (*big.Float, error) { - v := big.NewFloat(0.0) - v.SetPrec(128) - if sv, ok := db.stringKeys[k]; ok { - var err error - v, _, err = big.ParseFloat(sv, 10, 128, 0) - if err != nil { - return nil, ErrFloatValueError - } - } - v.Add(v, delta) - db.stringSet(k, formatBig(v)) - return v, nil -} - -// listLpush is 'left push', aka unshift. Returns the new length. -func (db *RedisDB) listLpush(k, v string) int { - l, ok := db.listKeys[k] - if !ok { - db.keys[k] = keyTypeList - } - l = append([]string{v}, l...) - db.listKeys[k] = l - db.incr(k) - return len(l) -} - -// 'left pop', aka shift. -func (db *RedisDB) listLpop(k string) string { - l := db.listKeys[k] - el := l[0] - l = l[1:] - if len(l) == 0 { - db.del(k, true) - } else { - db.listKeys[k] = l - } - db.incr(k) - return el -} - -func (db *RedisDB) listPush(k string, v ...string) int { - l, ok := db.listKeys[k] - if !ok { - db.keys[k] = keyTypeList - } - l = append(l, v...) - db.listKeys[k] = l - db.incr(k) - return len(l) -} - -func (db *RedisDB) listPop(k string) string { - l := db.listKeys[k] - el := l[len(l)-1] - l = l[:len(l)-1] - if len(l) == 0 { - db.del(k, true) - } else { - db.listKeys[k] = l - db.incr(k) - } - return el -} - -// setset replaces a whole set. -func (db *RedisDB) setSet(k string, set setKey) { - db.keys[k] = keyTypeSet - db.setKeys[k] = set - db.incr(k) -} - -// setadd adds members to a set. Returns nr of new keys. -func (db *RedisDB) setAdd(k string, elems ...string) int { - s, ok := db.setKeys[k] - if !ok { - s = setKey{} - db.keys[k] = keyTypeSet - } - added := 0 - for _, e := range elems { - if _, ok := s[e]; !ok { - added++ - } - s[e] = struct{}{} - } - db.setKeys[k] = s - db.incr(k) - return added -} - -// setrem removes members from a set. Returns nr of deleted keys. -func (db *RedisDB) setRem(k string, fields ...string) int { - s, ok := db.setKeys[k] - if !ok { - return 0 - } - removed := 0 - for _, f := range fields { - if _, ok := s[f]; ok { - removed++ - delete(s, f) - } - } - if len(s) == 0 { - db.del(k, true) - } else { - db.setKeys[k] = s - } - db.incr(k) - return removed -} - -// All members of a set. -func (db *RedisDB) setMembers(k string) []string { - set := db.setKeys[k] - members := make([]string, 0, len(set)) - for k := range set { - members = append(members, k) - } - sort.Strings(members) - return members -} - -// Is a SET value present? -func (db *RedisDB) setIsMember(k, v string) bool { - set, ok := db.setKeys[k] - if !ok { - return false - } - _, ok = set[v] - return ok -} - -// hashFields returns all (sorted) keys ('fields') for a hash key. -func (db *RedisDB) hashFields(k string) []string { - v := db.hashKeys[k] - var r []string - for k := range v { - r = append(r, k) - } - sort.Strings(r) - return r -} - -// hashValues returns all (sorted) values a hash key. -func (db *RedisDB) hashValues(k string) []string { - h := db.hashKeys[k] - var r []string - for _, v := range h { - r = append(r, v) - } - sort.Strings(r) - return r -} - -// hashGet a value -func (db *RedisDB) hashGet(key, field string) string { - return db.hashKeys[key][field] -} - -// hashSet returns the number of new keys -func (db *RedisDB) hashSet(k string, fv ...string) int { - if t, ok := db.keys[k]; ok && t != keyTypeHash { - db.del(k, true) - } - db.keys[k] = keyTypeHash - if _, ok := db.hashKeys[k]; !ok { - db.hashKeys[k] = map[string]string{} - } - new := 0 - for idx := 0; idx < len(fv)-1; idx = idx + 2 { - f, v := fv[idx], fv[idx+1] - _, ok := db.hashKeys[k][f] - db.hashKeys[k][f] = v - db.incr(k) - if !ok { - new++ - } - } - return new -} - -// hashIncr changes int key value -func (db *RedisDB) hashIncr(key, field string, delta int) (int, error) { - v := 0 - if h, ok := db.hashKeys[key]; ok { - if f, ok := h[field]; ok { - var err error - v, err = strconv.Atoi(f) - if err != nil { - return 0, ErrIntValueError - } - } - } - v += delta - db.hashSet(key, field, strconv.Itoa(v)) - return v, nil -} - -// hashIncrfloat changes float key value -func (db *RedisDB) hashIncrfloat(key, field string, delta *big.Float) (*big.Float, error) { - v := big.NewFloat(0.0) - v.SetPrec(128) - if h, ok := db.hashKeys[key]; ok { - if f, ok := h[field]; ok { - var err error - v, _, err = big.ParseFloat(f, 10, 128, 0) - if err != nil { - return nil, ErrFloatValueError - } - } - } - v.Add(v, delta) - db.hashSet(key, field, formatBig(v)) - return v, nil -} - -// sortedSet set returns a sortedSet as map -func (db *RedisDB) sortedSet(key string) map[string]float64 { - ss := db.sortedsetKeys[key] - return map[string]float64(ss) -} - -// ssetSet sets a complete sorted set. -func (db *RedisDB) ssetSet(key string, sset sortedSet) { - db.keys[key] = keyTypeSortedSet - db.incr(key) - db.sortedsetKeys[key] = sset -} - -// ssetAdd adds member to a sorted set. Returns whether this was a new member. -func (db *RedisDB) ssetAdd(key string, score float64, member string) bool { - ss, ok := db.sortedsetKeys[key] - if !ok { - ss = newSortedSet() - db.keys[key] = keyTypeSortedSet - } - _, ok = ss[member] - ss[member] = score - db.sortedsetKeys[key] = ss - db.incr(key) - return !ok -} - -// All members from a sorted set, ordered by score. -func (db *RedisDB) ssetMembers(key string) []string { - ss, ok := db.sortedsetKeys[key] - if !ok { - return nil - } - elems := ss.byScore(asc) - members := make([]string, 0, len(elems)) - for _, e := range elems { - members = append(members, e.member) - } - return members -} - -// All members+scores from a sorted set, ordered by score. -func (db *RedisDB) ssetElements(key string) ssElems { - ss, ok := db.sortedsetKeys[key] - if !ok { - return nil - } - return ss.byScore(asc) -} - -func (db *RedisDB) ssetRandomMember(key string) string { - elems := db.ssetElements(key) - if len(elems) == 0 { - return "" - } - return elems[db.master.randIntn(len(elems))].member -} - -// ssetCard is the sorted set cardinality. -func (db *RedisDB) ssetCard(key string) int { - ss := db.sortedsetKeys[key] - return ss.card() -} - -// ssetRank is the sorted set rank. -func (db *RedisDB) ssetRank(key, member string, d direction) (int, bool) { - ss := db.sortedsetKeys[key] - return ss.rankByScore(member, d) -} - -// ssetScore is sorted set score. -func (db *RedisDB) ssetScore(key, member string) float64 { - ss := db.sortedsetKeys[key] - return ss[member] -} - -// ssetMScore returns multiple scores of a list of members in a sorted set. -func (db *RedisDB) ssetMScore(key string, members []string) []float64 { - scores := make([]float64, 0, len(members)) - ss := db.sortedsetKeys[key] - for _, member := range members { - scores = append(scores, ss[member]) - } - return scores -} - -// ssetRem is sorted set key delete. -func (db *RedisDB) ssetRem(key, member string) bool { - ss := db.sortedsetKeys[key] - _, ok := ss[member] - delete(ss, member) - if len(ss) == 0 { - // Delete key on removal of last member - db.del(key, true) - } - return ok -} - -// ssetExists tells if a member exists in a sorted set. -func (db *RedisDB) ssetExists(key, member string) bool { - ss := db.sortedsetKeys[key] - _, ok := ss[member] - return ok -} - -// ssetIncrby changes float sorted set score. -func (db *RedisDB) ssetIncrby(k, m string, delta float64) float64 { - ss, ok := db.sortedsetKeys[k] - if !ok { - ss = newSortedSet() - db.keys[k] = keyTypeSortedSet - db.sortedsetKeys[k] = ss - } - - v, _ := ss.get(m) - v += delta - ss.set(v, m) - db.incr(k) - return v -} - -// setDiff implements the logic behind SDIFF* -func (db *RedisDB) setDiff(keys []string) (setKey, error) { - key := keys[0] - keys = keys[1:] - if db.exists(key) && db.t(key) != keyTypeSet { - return nil, ErrWrongType - } - s := setKey{} - for k := range db.setKeys[key] { - s[k] = struct{}{} - } - for _, sk := range keys { - if !db.exists(sk) { - continue - } - if db.t(sk) != keyTypeSet { - return nil, ErrWrongType - } - for e := range db.setKeys[sk] { - delete(s, e) - } - } - return s, nil -} - -// setInter implements the logic behind SINTER* -// len keys needs to be > 0 -func (db *RedisDB) setInter(keys []string) (setKey, error) { - // all keys must either not exist, or be of type "set". - for _, key := range keys { - if db.exists(key) && db.t(key) != keyTypeSet { - return nil, ErrWrongType - } - } - - key := keys[0] - keys = keys[1:] - if !db.exists(key) { - return nil, nil - } - if db.t(key) != keyTypeSet { - return nil, ErrWrongType - } - s := setKey{} - for k := range db.setKeys[key] { - s[k] = struct{}{} - } - for _, sk := range keys { - if !db.exists(sk) { - return setKey{}, nil - } - if db.t(sk) != keyTypeSet { - return nil, ErrWrongType - } - other := db.setKeys[sk] - for e := range s { - if _, ok := other[e]; ok { - continue - } - delete(s, e) - } - } - return s, nil -} - -// setIntercard implements the logic behind SINTER* -// len keys needs to be > 0 -func (db *RedisDB) setIntercard(keys []string, limit int) (int, error) { - // all keys must either not exist, or be of type "set". - allExist := true - for _, key := range keys { - exists := db.exists(key) - allExist = allExist && exists - if exists && db.t(key) != "set" { - return 0, ErrWrongType - } - } - - if !allExist { - return 0, nil - } - - smallestKey := keys[0] - smallestIdx := 0 - for i, key := range keys { - if len(db.setKeys[key]) < len(db.setKeys[smallestKey]) { - smallestKey = key - smallestIdx = i - } - } - keys[smallestIdx] = keys[len(keys)-1] - keys = keys[:len(keys)-1] - - count := 0 - for item := range db.setKeys[smallestKey] { - inIntersection := true - for _, key := range keys { - if _, ok := db.setKeys[key][item]; !ok { - inIntersection = false - break - } - } - if inIntersection { - count++ - if count == limit { - break - } - } - } - - return count, nil -} - -// setUnion implements the logic behind SUNION* -func (db *RedisDB) setUnion(keys []string) (setKey, error) { - key := keys[0] - keys = keys[1:] - if db.exists(key) && db.t(key) != "set" { - return nil, ErrWrongType - } - s := setKey{} - for k := range db.setKeys[key] { - s[k] = struct{}{} - } - for _, sk := range keys { - if !db.exists(sk) { - continue - } - if db.t(sk) != "set" { - return nil, ErrWrongType - } - for e := range db.setKeys[sk] { - s[e] = struct{}{} - } - } - return s, nil -} - -func (db *RedisDB) newStream(key string) (*streamKey, error) { - if s, err := db.stream(key); err != nil { - return nil, err - } else if s != nil { - return nil, fmt.Errorf("ErrAlreadyExists") - } - - db.keys[key] = keyTypeStream - s := newStreamKey() - db.streamKeys[key] = s - db.incr(key) - return s, nil -} - -// return existing stream, or nil. -func (db *RedisDB) stream(key string) (*streamKey, error) { - if db.exists(key) && db.t(key) != keyTypeStream { - return nil, ErrWrongType - } - - return db.streamKeys[key], nil -} - -// return existing stream group, or nil. -func (db *RedisDB) streamGroup(key, group string) (*streamGroup, error) { - s, err := db.stream(key) - if err != nil || s == nil { - return nil, err - } - return s.groups[group], nil -} - -// fastForward proceeds the current timestamp with duration, works as a time machine -func (db *RedisDB) fastForward(duration time.Duration) { - for _, key := range db.allKeys() { - if value, ok := db.ttl[key]; ok { - db.ttl[key] = value - duration - db.checkTTL(key) - } - - // Handle hash field TTLs - if db.t(key) == keyTypeHash { - db.checkHashFieldTTL(key, duration) - } - } -} - -func (db *RedisDB) checkHashFieldTTL(key string, duration time.Duration) { - fieldTTLs, ok := db.hashTTLs[key] - if !ok { - return - } - - for field, ttl := range fieldTTLs { - fieldTTLs[field] = ttl - duration - if fieldTTLs[field] <= 0 { - // Delete the expired field - delete(db.hashKeys[key], field) - delete(fieldTTLs, field) - - // If hash is now empty, delete the entire key - if len(db.hashKeys[key]) == 0 { - db.del(key, true) - } - } - } -} - -func (db *RedisDB) checkTTL(key string) { - if v, ok := db.ttl[key]; ok && v <= 0 { - db.del(key, true) - } -} - -// hllAdd adds members to a hll. Returns 1 if at least 1 if internal HyperLogLog was altered, otherwise 0 -func (db *RedisDB) hllAdd(k string, elems ...string) int { - s, ok := db.hllKeys[k] - if !ok { - s = newHll() - db.keys[k] = keyTypeHll - } - hllAltered := 0 - for _, e := range elems { - if s.Add([]byte(e)) { - hllAltered = 1 - } - } - db.hllKeys[k] = s - db.incr(k) - return hllAltered -} - -// hllCount estimates the amount of members added to hll by hllAdd. If called with several arguments, hllCount returns a sum of estimations -func (db *RedisDB) hllCount(keys []string) (int, error) { - countOverall := 0 - for _, key := range keys { - if db.exists(key) && db.t(key) != keyTypeHll { - return 0, ErrNotValidHllValue - } - if !db.exists(key) { - continue - } - countOverall += db.hllKeys[key].Count() - } - - return countOverall, nil -} - -// hllMerge merges all the hlls provided as keys to the first key. Creates a new hll in the first key if it contains nothing -func (db *RedisDB) hllMerge(keys []string) error { - for _, key := range keys { - if db.exists(key) && db.t(key) != keyTypeHll { - return ErrNotValidHllValue - } - } - - destKey := keys[0] - restKeys := keys[1:] - - var destHll *hll - if db.exists(destKey) { - destHll = db.hllKeys[destKey] - } else { - destHll = newHll() - } - - for _, key := range restKeys { - if !db.exists(key) { - continue - } - destHll.Merge(db.hllKeys[key]) - } - - db.hllKeys[destKey] = destHll - db.keys[destKey] = keyTypeHll - db.incr(destKey) - - return nil -} diff --git a/vendor/github.com/alicebob/miniredis/v2/direct.go b/vendor/github.com/alicebob/miniredis/v2/direct.go deleted file mode 100644 index ff527b800..000000000 --- a/vendor/github.com/alicebob/miniredis/v2/direct.go +++ /dev/null @@ -1,862 +0,0 @@ -package miniredis - -// Commands to modify and query our databases directly. - -import ( - "errors" - "math/big" - "time" -) - -var ( - // ErrKeyNotFound is returned when a key doesn't exist. - ErrKeyNotFound = errors.New(msgKeyNotFound) - - // ErrWrongType when a key is not the right type. - ErrWrongType = errors.New(msgWrongType) - - // ErrNotValidHllValue when a key is not a valid HyperLogLog string value. - ErrNotValidHllValue = errors.New(msgNotValidHllValue) - - // ErrIntValueError can returned by INCRBY - ErrIntValueError = errors.New(msgInvalidInt) - - // ErrIntValueOverflowError can be returned by INCR, DECR, INCRBY, DECRBY - ErrIntValueOverflowError = errors.New(msgIntOverflow) - - // ErrFloatValueError can returned by INCRBYFLOAT - ErrFloatValueError = errors.New(msgInvalidFloat) -) - -// Select sets the DB id for all direct commands. -func (m *Miniredis) Select(i int) { - m.Lock() - defer m.Unlock() - m.selectedDB = i -} - -// Keys returns all keys from the selected database, sorted. -func (m *Miniredis) Keys() []string { - return m.DB(m.selectedDB).Keys() -} - -// Keys returns all keys, sorted. -func (db *RedisDB) Keys() []string { - db.master.Lock() - defer db.master.Unlock() - - return db.allKeys() -} - -// FlushAll removes all keys from all databases. -func (m *Miniredis) FlushAll() { - m.Lock() - defer m.Unlock() - defer m.signal.Broadcast() - - m.flushAll() -} - -func (m *Miniredis) flushAll() { - for _, db := range m.dbs { - db.flush() - } -} - -// FlushDB removes all keys from the selected database. -func (m *Miniredis) FlushDB() { - m.DB(m.selectedDB).FlushDB() -} - -// FlushDB removes all keys. -func (db *RedisDB) FlushDB() { - db.master.Lock() - defer db.master.Unlock() - defer db.master.signal.Broadcast() - - db.flush() -} - -// Get returns string keys added with SET. -func (m *Miniredis) Get(k string) (string, error) { - return m.DB(m.selectedDB).Get(k) -} - -// Get returns a string key. -func (db *RedisDB) Get(k string) (string, error) { - db.master.Lock() - defer db.master.Unlock() - - if !db.exists(k) { - return "", ErrKeyNotFound - } - if db.t(k) != keyTypeString { - return "", ErrWrongType - } - return db.stringGet(k), nil -} - -// Set sets a string key. Removes expire. -func (m *Miniredis) Set(k, v string) error { - return m.DB(m.selectedDB).Set(k, v) -} - -// Set sets a string key. Removes expire. -// Unlike redis the key can't be an existing non-string key. -func (db *RedisDB) Set(k, v string) error { - db.master.Lock() - defer db.master.Unlock() - defer db.master.signal.Broadcast() - - if db.exists(k) && db.t(k) != keyTypeString { - return ErrWrongType - } - db.del(k, true) // Remove expire - db.stringSet(k, v) - return nil -} - -// Incr changes a int string value by delta. -func (m *Miniredis) Incr(k string, delta int) (int, error) { - return m.DB(m.selectedDB).Incr(k, delta) -} - -// Incr changes a int string value by delta. -func (db *RedisDB) Incr(k string, delta int) (int, error) { - db.master.Lock() - defer db.master.Unlock() - defer db.master.signal.Broadcast() - - if db.exists(k) && db.t(k) != keyTypeString { - return 0, ErrWrongType - } - - return db.stringIncr(k, delta) -} - -// IncrByFloat increments the float value of a key by the given delta. -// is an alias for Miniredis.Incrfloat -func (m *Miniredis) IncrByFloat(k string, delta float64) (float64, error) { - return m.Incrfloat(k, delta) -} - -// Incrfloat changes a float string value by delta. -func (m *Miniredis) Incrfloat(k string, delta float64) (float64, error) { - return m.DB(m.selectedDB).Incrfloat(k, delta) -} - -// Incrfloat changes a float string value by delta. -func (db *RedisDB) Incrfloat(k string, delta float64) (float64, error) { - db.master.Lock() - defer db.master.Unlock() - defer db.master.signal.Broadcast() - - if db.exists(k) && db.t(k) != keyTypeString { - return 0, ErrWrongType - } - - v, err := db.stringIncrfloat(k, big.NewFloat(delta)) - if err != nil { - return 0, err - } - vf, _ := v.Float64() - return vf, nil -} - -// List returns the list k, or an error if it's not there or something else. -// This is the same as the Redis command `LRANGE 0 -1`, but you can do your own -// range-ing. -func (m *Miniredis) List(k string) ([]string, error) { - return m.DB(m.selectedDB).List(k) -} - -// List returns the list k, or an error if it's not there or something else. -// This is the same as the Redis command `LRANGE 0 -1`, but you can do your own -// range-ing. -func (db *RedisDB) List(k string) ([]string, error) { - db.master.Lock() - defer db.master.Unlock() - - if !db.exists(k) { - return nil, ErrKeyNotFound - } - if db.t(k) != keyTypeList { - return nil, ErrWrongType - } - return db.listKeys[k], nil -} - -// Lpush prepends one value to a list. Returns the new length. -func (m *Miniredis) Lpush(k, v string) (int, error) { - return m.DB(m.selectedDB).Lpush(k, v) -} - -// Lpush prepends one value to a list. Returns the new length. -func (db *RedisDB) Lpush(k, v string) (int, error) { - db.master.Lock() - defer db.master.Unlock() - defer db.master.signal.Broadcast() - - if db.exists(k) && db.t(k) != keyTypeList { - return 0, ErrWrongType - } - return db.listLpush(k, v), nil -} - -// Lpop removes and returns the last element in a list. -func (m *Miniredis) Lpop(k string) (string, error) { - return m.DB(m.selectedDB).Lpop(k) -} - -// Lpop removes and returns the last element in a list. -func (db *RedisDB) Lpop(k string) (string, error) { - db.master.Lock() - defer db.master.Unlock() - defer db.master.signal.Broadcast() - - if !db.exists(k) { - return "", ErrKeyNotFound - } - if db.t(k) != keyTypeList { - return "", ErrWrongType - } - return db.listLpop(k), nil -} - -// RPush appends one or multiple values to a list. Returns the new length. -// An alias for Push -func (m *Miniredis) RPush(k string, v ...string) (int, error) { - return m.Push(k, v...) -} - -// Push add element at the end. Returns the new length. -func (m *Miniredis) Push(k string, v ...string) (int, error) { - return m.DB(m.selectedDB).Push(k, v...) -} - -// Push add element at the end. Is called RPUSH in redis. Returns the new length. -func (db *RedisDB) Push(k string, v ...string) (int, error) { - db.master.Lock() - defer db.master.Unlock() - defer db.master.signal.Broadcast() - - if db.exists(k) && db.t(k) != keyTypeList { - return 0, ErrWrongType - } - return db.listPush(k, v...), nil -} - -// RPop is an alias for Pop -func (m *Miniredis) RPop(k string) (string, error) { - return m.Pop(k) -} - -// Pop removes and returns the last element. Is called RPOP in Redis. -func (m *Miniredis) Pop(k string) (string, error) { - return m.DB(m.selectedDB).Pop(k) -} - -// Pop removes and returns the last element. Is called RPOP in Redis. -func (db *RedisDB) Pop(k string) (string, error) { - db.master.Lock() - defer db.master.Unlock() - defer db.master.signal.Broadcast() - - if !db.exists(k) { - return "", ErrKeyNotFound - } - if db.t(k) != keyTypeList { - return "", ErrWrongType - } - - return db.listPop(k), nil -} - -// SAdd adds keys to a set. Returns the number of new keys. -// Alias for SetAdd -func (m *Miniredis) SAdd(k string, elems ...string) (int, error) { - return m.SetAdd(k, elems...) -} - -// SetAdd adds keys to a set. Returns the number of new keys. -func (m *Miniredis) SetAdd(k string, elems ...string) (int, error) { - return m.DB(m.selectedDB).SetAdd(k, elems...) -} - -// SetAdd adds keys to a set. Returns the number of new keys. -func (db *RedisDB) SetAdd(k string, elems ...string) (int, error) { - db.master.Lock() - defer db.master.Unlock() - defer db.master.signal.Broadcast() - - if db.exists(k) && db.t(k) != keyTypeSet { - return 0, ErrWrongType - } - return db.setAdd(k, elems...), nil -} - -// SMembers returns all keys in a set, sorted. -// Alias for Members. -func (m *Miniredis) SMembers(k string) ([]string, error) { - return m.Members(k) -} - -// Members returns all keys in a set, sorted. -func (m *Miniredis) Members(k string) ([]string, error) { - return m.DB(m.selectedDB).Members(k) -} - -// Members gives all set keys. Sorted. -func (db *RedisDB) Members(k string) ([]string, error) { - db.master.Lock() - defer db.master.Unlock() - - if !db.exists(k) { - return nil, ErrKeyNotFound - } - if db.t(k) != keyTypeSet { - return nil, ErrWrongType - } - return db.setMembers(k), nil -} - -// SIsMember tells if value is in the set. -// Alias for IsMember -func (m *Miniredis) SIsMember(k, v string) (bool, error) { - return m.IsMember(k, v) -} - -// IsMember tells if value is in the set. -func (m *Miniredis) IsMember(k, v string) (bool, error) { - return m.DB(m.selectedDB).IsMember(k, v) -} - -// IsMember tells if value is in the set. -func (db *RedisDB) IsMember(k, v string) (bool, error) { - db.master.Lock() - defer db.master.Unlock() - - if !db.exists(k) { - return false, ErrKeyNotFound - } - if db.t(k) != keyTypeSet { - return false, ErrWrongType - } - return db.setIsMember(k, v), nil -} - -// HKeys returns all (sorted) keys ('fields') for a hash key. -func (m *Miniredis) HKeys(k string) ([]string, error) { - return m.DB(m.selectedDB).HKeys(k) -} - -// HKeys returns all (sorted) keys ('fields') for a hash key. -func (db *RedisDB) HKeys(key string) ([]string, error) { - db.master.Lock() - defer db.master.Unlock() - - if !db.exists(key) { - return nil, ErrKeyNotFound - } - if db.t(key) != keyTypeHash { - return nil, ErrWrongType - } - return db.hashFields(key), nil -} - -// Del deletes a key and any expiration value. Returns whether there was a key. -func (m *Miniredis) Del(k string) bool { - return m.DB(m.selectedDB).Del(k) -} - -// Del deletes a key and any expiration value. Returns whether there was a key. -func (db *RedisDB) Del(k string) bool { - db.master.Lock() - defer db.master.Unlock() - defer db.master.signal.Broadcast() - - if !db.exists(k) { - return false - } - db.del(k, true) - return true -} - -// Unlink deletes a key and any expiration value. Returns where there was a key. -// It's exactly the same as Del() and is not async. It is here for the consistency. -func (m *Miniredis) Unlink(k string) bool { - return m.Del(k) -} - -// Unlink deletes a key and any expiration value. Returns where there was a key. -// It's exactly the same as Del() and is not async. It is here for the consistency. -func (db *RedisDB) Unlink(k string) bool { - return db.Del(k) -} - -// TTL is the left over time to live. As set via EXPIRE, PEXPIRE, EXPIREAT, -// PEXPIREAT. -// Note: this direct function returns 0 if there is no TTL set, unlike redis, -// which returns -1. -func (m *Miniredis) TTL(k string) time.Duration { - return m.DB(m.selectedDB).TTL(k) -} - -// TTL is the left over time to live. As set via EXPIRE, PEXPIRE, EXPIREAT, -// PEXPIREAT. -// 0 if not set. -func (db *RedisDB) TTL(k string) time.Duration { - db.master.Lock() - defer db.master.Unlock() - - return db.ttl[k] -} - -// SetTTL sets the TTL of a key. -func (m *Miniredis) SetTTL(k string, ttl time.Duration) { - m.DB(m.selectedDB).SetTTL(k, ttl) -} - -// SetTTL sets the time to live of a key. -func (db *RedisDB) SetTTL(k string, ttl time.Duration) { - db.master.Lock() - defer db.master.Unlock() - defer db.master.signal.Broadcast() - - db.ttl[k] = ttl - db.incr(k) -} - -// Type gives the type of a key, or "" -func (m *Miniredis) Type(k string) string { - return m.DB(m.selectedDB).Type(k) -} - -// Type gives the type of a key, or "" -func (db *RedisDB) Type(k string) string { - db.master.Lock() - defer db.master.Unlock() - - return db.t(k) -} - -// Exists tells whether a key exists. -func (m *Miniredis) Exists(k string) bool { - return m.DB(m.selectedDB).Exists(k) -} - -// Exists tells whether a key exists. -func (db *RedisDB) Exists(k string) bool { - db.master.Lock() - defer db.master.Unlock() - - return db.exists(k) -} - -// HGet returns hash keys added with HSET. -// This will return an empty string if the key is not set. Redis would return -// a nil. -// Returns empty string when the key is of a different type. -func (m *Miniredis) HGet(k, f string) string { - return m.DB(m.selectedDB).HGet(k, f) -} - -// HGet returns hash keys added with HSET. -// Returns empty string when the key is of a different type. -func (db *RedisDB) HGet(k, f string) string { - db.master.Lock() - defer db.master.Unlock() - - h, ok := db.hashKeys[k] - if !ok { - return "" - } - return h[f] -} - -// HSet sets hash keys. -// If there is another key by the same name it will be gone. -func (m *Miniredis) HSet(k string, fv ...string) { - m.DB(m.selectedDB).HSet(k, fv...) -} - -// HSet sets hash keys. -// If there is another key by the same name it will be gone. -func (db *RedisDB) HSet(k string, fv ...string) { - db.master.Lock() - defer db.master.Unlock() - defer db.master.signal.Broadcast() - - db.hashSet(k, fv...) -} - -// HDel deletes a hash key. -func (m *Miniredis) HDel(k, f string) { - m.DB(m.selectedDB).HDel(k, f) -} - -// HDel deletes a hash key. -func (db *RedisDB) HDel(k, f string) { - db.master.Lock() - defer db.master.Unlock() - defer db.master.signal.Broadcast() - - db.hdel(k, f) -} - -func (db *RedisDB) hdel(k, f string) { - if _, ok := db.hashKeys[k]; !ok { - return - } - delete(db.hashKeys[k], f) - db.incr(k) -} - -// HIncrBy increases the integer value of a hash field by delta (int). -func (m *Miniredis) HIncrBy(k, f string, delta int) (int, error) { - return m.HIncr(k, f, delta) -} - -// HIncr increases a key/field by delta (int). -func (m *Miniredis) HIncr(k, f string, delta int) (int, error) { - return m.DB(m.selectedDB).HIncr(k, f, delta) -} - -// HIncr increases a key/field by delta (int). -func (db *RedisDB) HIncr(k, f string, delta int) (int, error) { - db.master.Lock() - defer db.master.Unlock() - defer db.master.signal.Broadcast() - - return db.hashIncr(k, f, delta) -} - -// HIncrByFloat increases a key/field by delta (float). -func (m *Miniredis) HIncrByFloat(k, f string, delta float64) (float64, error) { - return m.HIncrfloat(k, f, delta) -} - -// HIncrfloat increases a key/field by delta (float). -func (m *Miniredis) HIncrfloat(k, f string, delta float64) (float64, error) { - return m.DB(m.selectedDB).HIncrfloat(k, f, delta) -} - -// HIncrfloat increases a key/field by delta (float). -func (db *RedisDB) HIncrfloat(k, f string, delta float64) (float64, error) { - db.master.Lock() - defer db.master.Unlock() - defer db.master.signal.Broadcast() - - v, err := db.hashIncrfloat(k, f, big.NewFloat(delta)) - if err != nil { - return 0, err - } - vf, _ := v.Float64() - return vf, nil -} - -// SRem removes fields from a set. Returns number of deleted fields. -func (m *Miniredis) SRem(k string, fields ...string) (int, error) { - return m.DB(m.selectedDB).SRem(k, fields...) -} - -// SRem removes fields from a set. Returns number of deleted fields. -func (db *RedisDB) SRem(k string, fields ...string) (int, error) { - db.master.Lock() - defer db.master.Unlock() - defer db.master.signal.Broadcast() - - if !db.exists(k) { - return 0, ErrKeyNotFound - } - if db.t(k) != keyTypeSet { - return 0, ErrWrongType - } - return db.setRem(k, fields...), nil -} - -// ZAdd adds a score,member to a sorted set. -func (m *Miniredis) ZAdd(k string, score float64, member string) (bool, error) { - return m.DB(m.selectedDB).ZAdd(k, score, member) -} - -// ZAdd adds a score,member to a sorted set. -func (db *RedisDB) ZAdd(k string, score float64, member string) (bool, error) { - db.master.Lock() - defer db.master.Unlock() - defer db.master.signal.Broadcast() - - if db.exists(k) && db.t(k) != keyTypeSortedSet { - return false, ErrWrongType - } - return db.ssetAdd(k, score, member), nil -} - -// ZMembers returns all members of a sorted set by score -func (m *Miniredis) ZMembers(k string) ([]string, error) { - return m.DB(m.selectedDB).ZMembers(k) -} - -// ZMembers returns all members of a sorted set by score -func (db *RedisDB) ZMembers(k string) ([]string, error) { - db.master.Lock() - defer db.master.Unlock() - - if !db.exists(k) { - return nil, ErrKeyNotFound - } - if db.t(k) != keyTypeSortedSet { - return nil, ErrWrongType - } - return db.ssetMembers(k), nil -} - -// SortedSet returns a raw string->float64 map. -func (m *Miniredis) SortedSet(k string) (map[string]float64, error) { - return m.DB(m.selectedDB).SortedSet(k) -} - -// SortedSet returns a raw string->float64 map. -func (db *RedisDB) SortedSet(k string) (map[string]float64, error) { - db.master.Lock() - defer db.master.Unlock() - - if !db.exists(k) { - return nil, ErrKeyNotFound - } - if db.t(k) != keyTypeSortedSet { - return nil, ErrWrongType - } - return db.sortedSet(k), nil -} - -// ZRem deletes a member. Returns whether the was a key. -func (m *Miniredis) ZRem(k, member string) (bool, error) { - return m.DB(m.selectedDB).ZRem(k, member) -} - -// ZRem deletes a member. Returns whether the was a key. -func (db *RedisDB) ZRem(k, member string) (bool, error) { - db.master.Lock() - defer db.master.Unlock() - defer db.master.signal.Broadcast() - - if !db.exists(k) { - return false, ErrKeyNotFound - } - if db.t(k) != keyTypeSortedSet { - return false, ErrWrongType - } - return db.ssetRem(k, member), nil -} - -// ZScore gives the score of a sorted set member. -func (m *Miniredis) ZScore(k, member string) (float64, error) { - return m.DB(m.selectedDB).ZScore(k, member) -} - -// ZScore gives the score of a sorted set member. -func (db *RedisDB) ZScore(k, member string) (float64, error) { - db.master.Lock() - defer db.master.Unlock() - - if !db.exists(k) { - return 0, ErrKeyNotFound - } - if db.t(k) != keyTypeSortedSet { - return 0, ErrWrongType - } - return db.ssetScore(k, member), nil -} - -// ZScore gives scores of a list of members in a sorted set. -func (m *Miniredis) ZMScore(k string, members ...string) ([]float64, error) { - return m.DB(m.selectedDB).ZMScore(k, members) -} - -func (db *RedisDB) ZMScore(k string, members []string) ([]float64, error) { - db.master.Lock() - defer db.master.Unlock() - - if !db.exists(k) { - return nil, ErrKeyNotFound - } - if db.t(k) != keyTypeSortedSet { - return nil, ErrWrongType - } - return db.ssetMScore(k, members), nil -} - -// XAdd adds an entry to a stream. `id` can be left empty or be '*'. -// If a value is given normal XADD rules apply. Values should be an even -// length. -func (m *Miniredis) XAdd(k string, id string, values []string) (string, error) { - return m.DB(m.selectedDB).XAdd(k, id, values) -} - -// XAdd adds an entry to a stream. `id` can be left empty or be '*'. -// If a value is given normal XADD rules apply. Values should be an even -// length. -func (db *RedisDB) XAdd(k string, id string, values []string) (string, error) { - db.master.Lock() - defer db.master.Unlock() - defer db.master.signal.Broadcast() - - s, err := db.stream(k) - if err != nil { - return "", err - } - if s == nil { - s, _ = db.newStream(k) - } - - return s.add(id, values, db.master.effectiveNow()) -} - -// Stream returns a slice of stream entries. Oldest first. -func (m *Miniredis) Stream(k string) ([]StreamEntry, error) { - return m.DB(m.selectedDB).Stream(k) -} - -// Stream returns a slice of stream entries. Oldest first. -func (db *RedisDB) Stream(key string) ([]StreamEntry, error) { - db.master.Lock() - defer db.master.Unlock() - - s, err := db.stream(key) - if err != nil { - return nil, err - } - if s == nil { - return nil, nil - } - return s.entries, nil -} - -// Publish a message to subscribers. Returns the number of receivers. -func (m *Miniredis) Publish(channel, message string) int { - m.Lock() - defer m.Unlock() - - return m.publish(channel, message) -} - -// PubSubChannels is "PUBSUB CHANNELS ". An empty pattern is fine -// (meaning all channels). -// Returned channels will be ordered alphabetically. -func (m *Miniredis) PubSubChannels(pattern string) []string { - m.Lock() - defer m.Unlock() - - return activeChannels(m.allSubscribers(), pattern) -} - -// PubSubNumSub is "PUBSUB NUMSUB [channels]". It returns all channels with their -// subscriber count. -func (m *Miniredis) PubSubNumSub(channels ...string) map[string]int { - m.Lock() - defer m.Unlock() - - subs := m.allSubscribers() - res := map[string]int{} - for _, channel := range channels { - res[channel] = countSubs(subs, channel) - } - return res -} - -// PubSubNumPat is "PUBSUB NUMPAT" -func (m *Miniredis) PubSubNumPat() int { - m.Lock() - defer m.Unlock() - - return countPsubs(m.allSubscribers()) -} - -// PfAdd adds keys to a hll. Returns the flag which equals to 1 if the inner hll value has been changed. -func (m *Miniredis) PfAdd(k string, elems ...string) (int, error) { - return m.DB(m.selectedDB).HllAdd(k, elems...) -} - -// HllAdd adds keys to a hll. Returns the flag which equals to true if the inner hll value has been changed. -func (db *RedisDB) HllAdd(k string, elems ...string) (int, error) { - db.master.Lock() - defer db.master.Unlock() - - if db.exists(k) && db.t(k) != keyTypeHll { - return 0, ErrWrongType - } - return db.hllAdd(k, elems...), nil -} - -// PfCount returns an estimation of the amount of elements previously added to a hll. -func (m *Miniredis) PfCount(keys ...string) (int, error) { - return m.DB(m.selectedDB).HllCount(keys...) -} - -// HllCount returns an estimation of the amount of elements previously added to a hll. -func (db *RedisDB) HllCount(keys ...string) (int, error) { - db.master.Lock() - defer db.master.Unlock() - - return db.hllCount(keys) -} - -// PfMerge merges all the input hlls into a hll under destKey key. -func (m *Miniredis) PfMerge(destKey string, sourceKeys ...string) error { - return m.DB(m.selectedDB).HllMerge(destKey, sourceKeys...) -} - -// HllMerge merges all the input hlls into a hll under destKey key. -func (db *RedisDB) HllMerge(destKey string, sourceKeys ...string) error { - db.master.Lock() - defer db.master.Unlock() - - return db.hllMerge(append([]string{destKey}, sourceKeys...)) -} - -// Copy a value. -// Needs the IDs of both the source and dest DBs (which can differ). -// Returns ErrKeyNotFound if src does not exist. -// Overwrites dest if it already exists (unlike the redis command, which needs a flag to allow that). -func (m *Miniredis) Copy(srcDB int, src string, destDB int, dest string) error { - return m.copy(m.DB(srcDB), src, m.DB(destDB), dest) -} - -func (m *Miniredis) SCard(key string) (int, error) { - return m.DB(m.selectedDB).SCard(key) -} - -func (db *RedisDB) SCard(key string) (int, error) { - db.master.Lock() - defer db.master.Unlock() - if !db.exists(key) { - return 0, nil - } - if db.t(key) != "set" { - return 0, ErrWrongType - } - return len(db.setMembers(key)), nil -} - -// return "" if there were no members -func (m *Miniredis) SRandMember(key string) (string, error) { - return m.DB(m.selectedDB).SRandMember(key) -} - -func (db *RedisDB) SRandMember(key string) (string, error) { - db.master.Lock() - defer db.master.Unlock() - if !db.exists(key) { - return "", nil - } - if db.t(key) != "set" { - return "", ErrWrongType - } - members := db.setMembers(key) - if len(members) == 0 { - return "", nil - } - db.master.shuffle(members) - return members[0], nil -} diff --git a/vendor/github.com/alicebob/miniredis/v2/fpconv/LICENSE.txt b/vendor/github.com/alicebob/miniredis/v2/fpconv/LICENSE.txt deleted file mode 100644 index 0a0af2e8f..000000000 --- a/vendor/github.com/alicebob/miniredis/v2/fpconv/LICENSE.txt +++ /dev/null @@ -1,26 +0,0 @@ -This code is derived from the C code in redis-7.2.0/deps/fpconv/*, which has -this license: - -Boost Software License - Version 1.0 - August 17th, 2003 - -Permission is hereby granted, free of charge, to any person or organization -obtaining a copy of the software and accompanying documentation covered by -this license (the "Software") to use, reproduce, display, distribute, -execute, and transmit the Software, and to prepare derivative works of the -Software, and to permit third-parties to whom the Software is furnished to -do so, all subject to the following: - -The copyright notices in the Software and this entire statement, including -the above license grant, this restriction and the following disclaimer, -must be included in all copies of the Software, in whole or in part, and -all derivative works of the Software, unless such copies or derivative -works are solely in the form of machine-executable object code generated by -a source language processor. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT -SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE -FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, -ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. diff --git a/vendor/github.com/alicebob/miniredis/v2/fpconv/Makefile b/vendor/github.com/alicebob/miniredis/v2/fpconv/Makefile deleted file mode 100644 index d32d4bdcd..000000000 --- a/vendor/github.com/alicebob/miniredis/v2/fpconv/Makefile +++ /dev/null @@ -1,6 +0,0 @@ -.PHONY: test fuzz -test: - go test - -fuzz: - go test -fuzz=Fuzz diff --git a/vendor/github.com/alicebob/miniredis/v2/fpconv/README.md b/vendor/github.com/alicebob/miniredis/v2/fpconv/README.md deleted file mode 100644 index c210e6033..000000000 --- a/vendor/github.com/alicebob/miniredis/v2/fpconv/README.md +++ /dev/null @@ -1,3 +0,0 @@ -This is a translation of the actual C code in Redis (7.2) which does the float --> string conversion. -Strconv does a close enough job, but we can use the exact same logic, so why not. diff --git a/vendor/github.com/alicebob/miniredis/v2/fpconv/dtoa.go b/vendor/github.com/alicebob/miniredis/v2/fpconv/dtoa.go deleted file mode 100644 index 251fc4f3b..000000000 --- a/vendor/github.com/alicebob/miniredis/v2/fpconv/dtoa.go +++ /dev/null @@ -1,286 +0,0 @@ -package fpconv - -import ( - "math" -) - -var ( - fracmask uint64 = 0x000FFFFFFFFFFFFF - expmask uint64 = 0x7FF0000000000000 - hiddenbit uint64 = 0x0010000000000000 - signmask uint64 = 0x8000000000000000 - expbias int64 = 1023 + 52 - zeros = []rune("0000000000000000000000") - - tens = []uint64{ - 10000000000000000000, - 1000000000000000000, - 100000000000000000, - 10000000000000000, - 1000000000000000, - 100000000000000, - 10000000000000, - 1000000000000, - 100000000000, - 10000000000, - 1000000000, - 100000000, - 10000000, - 1000000, - 100000, - 10000, - 1000, - 100, - 10, - 1} -) - -func absv(n int) int { - if n < 0 { - return -n - } - return n -} - -func minv(a, b int) int { - if a < b { - return a - } - return b -} - -func Dtoa(d float64) string { - var ( - dest [25]rune // Note C has 24, which is broken - digits [18]rune - - str_len int = 0 - neg = false - ) - - if get_dbits(d)&signmask != 0 { - dest[0] = '-' - str_len++ - neg = true - } - - if spec := filter_special(d, dest[str_len:]); spec != 0 { - return string(dest[:str_len+spec]) - } - - var ( - k int = 0 - ndigits int = grisu2(d, &digits, &k) - ) - - str_len += emit_digits(&digits, ndigits, dest[str_len:], k, neg) - return string(dest[:str_len]) -} - -func filter_special(fp float64, dest []rune) int { - if fp == 0.0 { - dest[0] = '0' - return 1 - } - - if math.IsNaN(fp) { - dest[0] = 'n' - dest[1] = 'a' - dest[2] = 'n' - return 3 - } - if math.IsInf(fp, 0) { - dest[0] = 'i' - dest[1] = 'n' - dest[2] = 'f' - return 3 - } - return 0 -} - -func grisu2(d float64, digits *[18]rune, K *int) int { - w := build_fp(d) - - lower, upper := get_normalized_boundaries(w) - - w = normalize(w) - - var k int64 - cp := find_cachedpow10(upper.exp, &k) - - w = multiply(w, cp) - upper = multiply(upper, cp) - lower = multiply(lower, cp) - - lower.frac++ - upper.frac-- - - *K = int(-k) - - return generate_digits(w, upper, lower, digits[:], K) -} - -func emit_digits(digits *[18]rune, ndigits int, dest []rune, K int, neg bool) int { - exp := int(absv(K + ndigits - 1)) - - /* write plain integer */ - if K >= 0 && (exp < (ndigits + 7)) { - copy(dest, digits[:ndigits]) - copy(dest[ndigits:], zeros[:K]) - - return ndigits + K - } - - /* write decimal w/o scientific notation */ - if K < 0 && (K > -7 || exp < 4) { - offset := int(ndigits - absv(K)) - /* fp < 1.0 -> write leading zero */ - if offset <= 0 { - offset = -offset - dest[0] = '0' - dest[1] = '.' - copy(dest[2:], zeros[:offset]) - copy(dest[offset+2:], digits[:ndigits]) - - return ndigits + 2 + offset - - /* fp > 1.0 */ - } else { - copy(dest, digits[:offset]) - dest[offset] = '.' - copy(dest[offset+1:], digits[offset:offset+ndigits-offset]) - - return ndigits + 1 - } - } - /* write decimal w/ scientific notation */ - l := 18 // was: 18-neg - if neg { - l-- - } - ndigits = minv(ndigits, l) - - var idx int = 0 - dest[idx] = digits[0] - idx++ - - if ndigits > 1 { - dest[idx] = '.' - idx++ - copy(dest[idx:], digits[+1:ndigits-1+1]) - idx += ndigits - 1 - } - - dest[idx] = 'e' - idx++ - - sign := '+' - if K+ndigits-1 < 0 { - sign = '-' - } - dest[idx] = sign - idx++ - - var cent rune = 0 - - if exp > 99 { - cent = rune(exp / 100) - dest[idx] = cent + '0' - idx++ - exp -= int(cent) * 100 - } - if exp > 9 { - dec := rune(exp / 10) - dest[idx] = dec + '0' - idx++ - exp -= int(dec) * 10 - } else if cent != 0 { - dest[idx] = '0' - idx++ - } - - dest[idx] = rune(exp%10) + '0' - idx++ - - return idx -} - -func generate_digits(fp, upper, lower Fp, digits []rune, K *int) int { - var ( - wfrac = uint64(upper.frac - fp.frac) - delta = uint64(upper.frac - lower.frac) - ) - - one := Fp{ - frac: 1 << -upper.exp, - exp: upper.exp, - } - - part1 := uint64(upper.frac >> -one.exp) - part2 := uint64(upper.frac & (one.frac - 1)) - - var ( - idx = 0 - kappa = 10 - index = 10 - ) - /* 1000000000 */ - for ; kappa > 0; index++ { - div := tens[index] - digit := part1 / div - - if digit != 0 || idx != 0 { - digits[idx] = rune(digit) + '0' - idx++ - } - - part1 -= digit * div - kappa-- - - tmp := (part1 << -one.exp) + part2 - if tmp <= delta { - *K += kappa - round_digit(digits, idx, delta, tmp, div<<-one.exp, wfrac) - - return idx - } - } - - /* 10 */ - index = 18 - for { - var unit uint64 = tens[index] - part2 *= 10 - delta *= 10 - kappa-- - - digit := part2 >> -one.exp - if digit != 0 || idx != 0 { - digits[idx] = rune(digit) + '0' - idx++ - } - - part2 &= uint64(one.frac) - 1 - if part2 < delta { - *K += kappa - round_digit(digits, idx, delta, part2, uint64(one.frac), wfrac*unit) - - return idx - } - - index-- - } -} - -func round_digit(digits []rune, - ndigits int, - delta uint64, - rem uint64, - kappa uint64, - frac uint64) { - for rem < frac && delta-rem >= kappa && - (rem+kappa < frac || frac-rem > rem+kappa-frac) { - digits[ndigits-1]-- - rem += kappa - } -} diff --git a/vendor/github.com/alicebob/miniredis/v2/fpconv/fp.go b/vendor/github.com/alicebob/miniredis/v2/fpconv/fp.go deleted file mode 100644 index 490646363..000000000 --- a/vendor/github.com/alicebob/miniredis/v2/fpconv/fp.go +++ /dev/null @@ -1,96 +0,0 @@ -package fpconv - -import ( - "math" -) - -type ( - Fp struct { - frac uint64 - exp int64 - } -) - -func build_fp(d float64) Fp { - bits := get_dbits(d) - - fp := Fp{ - frac: bits & fracmask, - exp: int64((bits & expmask) >> 52), - } - - if fp.exp != 0 { - fp.frac += hiddenbit - fp.exp -= expbias - } else { - fp.exp = -expbias + 1 - } - - return fp -} - -func normalize(fp Fp) Fp { - for (fp.frac & hiddenbit) == 0 { - fp.frac <<= 1 - fp.exp-- - } - - var shift int64 = 64 - 52 - 1 - fp.frac <<= shift - fp.exp -= shift - return fp -} - -func multiply(a, b Fp) Fp { - lomask := uint64(0x00000000FFFFFFFF) - - var ( - ah_bl = uint64((a.frac >> 32) * (b.frac & lomask)) - al_bh = uint64((a.frac & lomask) * (b.frac >> 32)) - al_bl = uint64((a.frac & lomask) * (b.frac & lomask)) - ah_bh = uint64((a.frac >> 32) * (b.frac >> 32)) - ) - - tmp := uint64((ah_bl & lomask) + (al_bh & lomask) + (al_bl >> 32)) - /* round up */ - tmp += uint64(1) << 31 - - return Fp{ - ah_bh + (ah_bl >> 32) + (al_bh >> 32) + (tmp >> 32), - a.exp + b.exp + 64, - } -} - -func get_dbits(d float64) uint64 { - return math.Float64bits(d) -} - -func get_normalized_boundaries(fp Fp) (Fp, Fp) { - upper := Fp{ - frac: (fp.frac << 1) + 1, - exp: fp.exp - 1, - } - for (upper.frac & (hiddenbit << 1)) == 0 { - upper.frac <<= 1 - upper.exp-- - } - - var u_shift int64 = 64 - 52 - 2 - - upper.frac <<= u_shift - upper.exp = upper.exp - u_shift - - l_shift := int64(1) - if fp.frac == hiddenbit { - l_shift = 2 - } - - lower := Fp{ - frac: (fp.frac << l_shift) - 1, - exp: fp.exp - l_shift, - } - - lower.frac <<= lower.exp - upper.exp - lower.exp = upper.exp - return lower, upper -} diff --git a/vendor/github.com/alicebob/miniredis/v2/fpconv/powers.go b/vendor/github.com/alicebob/miniredis/v2/fpconv/powers.go deleted file mode 100644 index 24725f914..000000000 --- a/vendor/github.com/alicebob/miniredis/v2/fpconv/powers.go +++ /dev/null @@ -1,82 +0,0 @@ -package fpconv - -var ( - npowers int64 = 87 - steppowers int64 = 8 - firstpower int64 = -348 /* 10 ^ -348 */ - - expmax = -32 - expmin = -60 - - powers_ten = []Fp{ - {18054884314459144840, -1220}, {13451937075301367670, -1193}, - {10022474136428063862, -1166}, {14934650266808366570, -1140}, - {11127181549972568877, -1113}, {16580792590934885855, -1087}, - {12353653155963782858, -1060}, {18408377700990114895, -1034}, - {13715310171984221708, -1007}, {10218702384817765436, -980}, - {15227053142812498563, -954}, {11345038669416679861, -927}, - {16905424996341287883, -901}, {12595523146049147757, -874}, - {9384396036005875287, -847}, {13983839803942852151, -821}, - {10418772551374772303, -794}, {15525180923007089351, -768}, - {11567161174868858868, -741}, {17236413322193710309, -715}, - {12842128665889583758, -688}, {9568131466127621947, -661}, - {14257626930069360058, -635}, {10622759856335341974, -608}, - {15829145694278690180, -582}, {11793632577567316726, -555}, - {17573882009934360870, -529}, {13093562431584567480, -502}, - {9755464219737475723, -475}, {14536774485912137811, -449}, - {10830740992659433045, -422}, {16139061738043178685, -396}, - {12024538023802026127, -369}, {17917957937422433684, -343}, - {13349918974505688015, -316}, {9946464728195732843, -289}, - {14821387422376473014, -263}, {11042794154864902060, -236}, - {16455045573212060422, -210}, {12259964326927110867, -183}, - {18268770466636286478, -157}, {13611294676837538539, -130}, - {10141204801825835212, -103}, {15111572745182864684, -77}, - {11258999068426240000, -50}, {16777216000000000000, -24}, - {12500000000000000000, 3}, {9313225746154785156, 30}, - {13877787807814456755, 56}, {10339757656912845936, 83}, - {15407439555097886824, 109}, {11479437019748901445, 136}, - {17105694144590052135, 162}, {12744735289059618216, 189}, - {9495567745759798747, 216}, {14149498560666738074, 242}, - {10542197943230523224, 269}, {15709099088952724970, 295}, - {11704190886730495818, 322}, {17440603504673385349, 348}, - {12994262207056124023, 375}, {9681479787123295682, 402}, - {14426529090290212157, 428}, {10748601772107342003, 455}, - {16016664761464807395, 481}, {11933345169920330789, 508}, - {17782069995880619868, 534}, {13248674568444952270, 561}, - {9871031767461413346, 588}, {14708983551653345445, 614}, - {10959046745042015199, 641}, {16330252207878254650, 667}, - {12166986024289022870, 694}, {18130221999122236476, 720}, - {13508068024458167312, 747}, {10064294952495520794, 774}, - {14996968138956309548, 800}, {11173611982879273257, 827}, - {16649979327439178909, 853}, {12405201291620119593, 880}, - {9242595204427927429, 907}, {13772540099066387757, 933}, - {10261342003245940623, 960}, {15290591125556738113, 986}, - {11392378155556871081, 1013}, {16975966327722178521, 1039}, - {12648080533535911531, 1066}, - } -) - -func find_cachedpow10(exp int64, k *int64) Fp { - one_log_ten := 0.30102999566398114 - - approx := int64(float64(-(exp + npowers)) * one_log_ten) - idx := int((approx - firstpower) / steppowers) - - for { - current := int(exp + powers_ten[idx].exp + 64) - - if current < expmin { - idx++ - continue - } - - if current > expmax { - idx-- - continue - } - - *k = (firstpower + int64(idx)*steppowers) - - return powers_ten[idx] - } -} diff --git a/vendor/github.com/alicebob/miniredis/v2/geo.go b/vendor/github.com/alicebob/miniredis/v2/geo.go deleted file mode 100644 index 3028a1670..000000000 --- a/vendor/github.com/alicebob/miniredis/v2/geo.go +++ /dev/null @@ -1,46 +0,0 @@ -package miniredis - -import ( - "math" - - "github.com/alicebob/miniredis/v2/geohash" -) - -func toGeohash(long, lat float64) uint64 { - return geohash.EncodeIntWithPrecision(lat, long, 52) -} - -func fromGeohash(score uint64) (float64, float64) { - lat, long := geohash.DecodeIntWithPrecision(score, 52) - return long, lat -} - -// haversin(θ) function -func hsin(theta float64) float64 { - return math.Pow(math.Sin(theta/2), 2) -} - -// distance function returns the distance (in meters) between two points of -// a given longitude and latitude relatively accurately (using a spherical -// approximation of the Earth) through the Haversin Distance Formula for -// great arc distance on a sphere with accuracy for small distances -// point coordinates are supplied in degrees and converted into rad. in the func -// distance returned is meters -// http://en.wikipedia.org/wiki/Haversine_formula -// Source: https://gist.github.com/cdipaolo/d3f8db3848278b49db68 -func distance(lat1, lon1, lat2, lon2 float64) float64 { - // convert to radians - // must cast radius as float to multiply later - var la1, lo1, la2, lo2 float64 - la1 = lat1 * math.Pi / 180 - lo1 = lon1 * math.Pi / 180 - la2 = lat2 * math.Pi / 180 - lo2 = lon2 * math.Pi / 180 - - earth := 6372797.560856 // Earth radius in METERS, according to src/geohash_helper.c - - // calculate - h := hsin(la2-la1) + math.Cos(la1)*math.Cos(la2)*hsin(lo2-lo1) - - return 2 * earth * math.Asin(math.Sqrt(h)) -} diff --git a/vendor/github.com/alicebob/miniredis/v2/geohash/LICENSE b/vendor/github.com/alicebob/miniredis/v2/geohash/LICENSE deleted file mode 100644 index c0190c9a6..000000000 --- a/vendor/github.com/alicebob/miniredis/v2/geohash/LICENSE +++ /dev/null @@ -1,22 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2015 Michael McLoughlin - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - diff --git a/vendor/github.com/alicebob/miniredis/v2/geohash/README.md b/vendor/github.com/alicebob/miniredis/v2/geohash/README.md deleted file mode 100644 index c1a12d144..000000000 --- a/vendor/github.com/alicebob/miniredis/v2/geohash/README.md +++ /dev/null @@ -1,2 +0,0 @@ -This is a (selected) copy of github.com/mmcloughlin/geohash with the latitude -range changed from 90 to ~85, to align with the algorithm use by Redis. diff --git a/vendor/github.com/alicebob/miniredis/v2/geohash/base32.go b/vendor/github.com/alicebob/miniredis/v2/geohash/base32.go deleted file mode 100644 index 916b272b9..000000000 --- a/vendor/github.com/alicebob/miniredis/v2/geohash/base32.go +++ /dev/null @@ -1,44 +0,0 @@ -package geohash - -// encoding encapsulates an encoding defined by a given base32 alphabet. -type encoding struct { - encode string - decode [256]byte -} - -// newEncoding constructs a new encoding defined by the given alphabet, -// which must be a 32-byte string. -func newEncoding(encoder string) *encoding { - e := new(encoding) - e.encode = encoder - for i := 0; i < len(e.decode); i++ { - e.decode[i] = 0xff - } - for i := 0; i < len(encoder); i++ { - e.decode[encoder[i]] = byte(i) - } - return e -} - -// Decode string into bits of a 64-bit word. The string s may be at most 12 -// characters. -func (e *encoding) Decode(s string) uint64 { - x := uint64(0) - for i := 0; i < len(s); i++ { - x = (x << 5) | uint64(e.decode[s[i]]) - } - return x -} - -// Encode bits of 64-bit word into a string. -func (e *encoding) Encode(x uint64) string { - b := [12]byte{} - for i := 0; i < 12; i++ { - b[11-i] = e.encode[x&0x1f] - x >>= 5 - } - return string(b[:]) -} - -// Base32Encoding with the Geohash alphabet. -var base32encoding = newEncoding("0123456789bcdefghjkmnpqrstuvwxyz") diff --git a/vendor/github.com/alicebob/miniredis/v2/geohash/geohash.go b/vendor/github.com/alicebob/miniredis/v2/geohash/geohash.go deleted file mode 100644 index 0e0ca2b28..000000000 --- a/vendor/github.com/alicebob/miniredis/v2/geohash/geohash.go +++ /dev/null @@ -1,269 +0,0 @@ -// Package geohash provides encoding and decoding of string and integer -// geohashes. -package geohash - -import ( - "math" -) - -const ( - ENC_LAT = 85.05112878 - ENC_LONG = 180.0 -) - -// Direction represents directions in the latitute/longitude space. -type Direction int - -// Cardinal and intercardinal directions -const ( - North Direction = iota - NorthEast - East - SouthEast - South - SouthWest - West - NorthWest -) - -// Encode the point (lat, lng) as a string geohash with the standard 12 -// characters of precision. -func Encode(lat, lng float64) string { - return EncodeWithPrecision(lat, lng, 12) -} - -// EncodeWithPrecision encodes the point (lat, lng) as a string geohash with -// the specified number of characters of precision (max 12). -func EncodeWithPrecision(lat, lng float64, chars uint) string { - bits := 5 * chars - inthash := EncodeIntWithPrecision(lat, lng, bits) - enc := base32encoding.Encode(inthash) - return enc[12-chars:] -} - -// encodeInt provides a Go implementation of integer geohash. This is the -// default implementation of EncodeInt, but optimized versions are provided -// for certain architectures. -func EncodeInt(lat, lng float64) uint64 { - latInt := encodeRange(lat, ENC_LAT) - lngInt := encodeRange(lng, ENC_LONG) - return interleave(latInt, lngInt) -} - -// EncodeIntWithPrecision encodes the point (lat, lng) to an integer with the -// specified number of bits. -func EncodeIntWithPrecision(lat, lng float64, bits uint) uint64 { - hash := EncodeInt(lat, lng) - return hash >> (64 - bits) -} - -// Box represents a rectangle in latitude/longitude space. -type Box struct { - MinLat float64 - MaxLat float64 - MinLng float64 - MaxLng float64 -} - -// Center returns the center of the box. -func (b Box) Center() (lat, lng float64) { - lat = (b.MinLat + b.MaxLat) / 2.0 - lng = (b.MinLng + b.MaxLng) / 2.0 - return -} - -// Contains decides whether (lat, lng) is contained in the box. The -// containment test is inclusive of the edges and corners. -func (b Box) Contains(lat, lng float64) bool { - return (b.MinLat <= lat && lat <= b.MaxLat && - b.MinLng <= lng && lng <= b.MaxLng) -} - -// errorWithPrecision returns the error range in latitude and longitude for in -// integer geohash with bits of precision. -func errorWithPrecision(bits uint) (latErr, lngErr float64) { - b := int(bits) - latBits := b / 2 - lngBits := b - latBits - latErr = math.Ldexp(180.0, -latBits) - lngErr = math.Ldexp(360.0, -lngBits) - return -} - -// BoundingBox returns the region encoded by the given string geohash. -func BoundingBox(hash string) Box { - bits := uint(5 * len(hash)) - inthash := base32encoding.Decode(hash) - return BoundingBoxIntWithPrecision(inthash, bits) -} - -// BoundingBoxIntWithPrecision returns the region encoded by the integer -// geohash with the specified precision. -func BoundingBoxIntWithPrecision(hash uint64, bits uint) Box { - fullHash := hash << (64 - bits) - latInt, lngInt := deinterleave(fullHash) - lat := decodeRange(latInt, ENC_LAT) - lng := decodeRange(lngInt, ENC_LONG) - latErr, lngErr := errorWithPrecision(bits) - return Box{ - MinLat: lat, - MaxLat: lat + latErr, - MinLng: lng, - MaxLng: lng + lngErr, - } -} - -// BoundingBoxInt returns the region encoded by the given 64-bit integer -// geohash. -func BoundingBoxInt(hash uint64) Box { - return BoundingBoxIntWithPrecision(hash, 64) -} - -// DecodeCenter decodes the string geohash to the central point of the bounding box. -func DecodeCenter(hash string) (lat, lng float64) { - box := BoundingBox(hash) - return box.Center() -} - -// DecodeIntWithPrecision decodes the provided integer geohash with bits of -// precision to a (lat, lng) point. -func DecodeIntWithPrecision(hash uint64, bits uint) (lat, lng float64) { - box := BoundingBoxIntWithPrecision(hash, bits) - return box.Center() -} - -// DecodeInt decodes the provided 64-bit integer geohash to a (lat, lng) point. -func DecodeInt(hash uint64) (lat, lng float64) { - return DecodeIntWithPrecision(hash, 64) -} - -// Neighbors returns a slice of geohash strings that correspond to the provided -// geohash's neighbors. -func Neighbors(hash string) []string { - box := BoundingBox(hash) - lat, lng := box.Center() - latDelta := box.MaxLat - box.MinLat - lngDelta := box.MaxLng - box.MinLng - precision := uint(len(hash)) - return []string{ - // N - EncodeWithPrecision(lat+latDelta, lng, precision), - // NE, - EncodeWithPrecision(lat+latDelta, lng+lngDelta, precision), - // E, - EncodeWithPrecision(lat, lng+lngDelta, precision), - // SE, - EncodeWithPrecision(lat-latDelta, lng+lngDelta, precision), - // S, - EncodeWithPrecision(lat-latDelta, lng, precision), - // SW, - EncodeWithPrecision(lat-latDelta, lng-lngDelta, precision), - // W, - EncodeWithPrecision(lat, lng-lngDelta, precision), - // NW - EncodeWithPrecision(lat+latDelta, lng-lngDelta, precision), - } -} - -// NeighborsInt returns a slice of uint64s that correspond to the provided hash's -// neighbors at 64-bit precision. -func NeighborsInt(hash uint64) []uint64 { - return NeighborsIntWithPrecision(hash, 64) -} - -// NeighborsIntWithPrecision returns a slice of uint64s that correspond to the -// provided hash's neighbors at the given precision. -func NeighborsIntWithPrecision(hash uint64, bits uint) []uint64 { - box := BoundingBoxIntWithPrecision(hash, bits) - lat, lng := box.Center() - latDelta := box.MaxLat - box.MinLat - lngDelta := box.MaxLng - box.MinLng - return []uint64{ - // N - EncodeIntWithPrecision(lat+latDelta, lng, bits), - // NE, - EncodeIntWithPrecision(lat+latDelta, lng+lngDelta, bits), - // E, - EncodeIntWithPrecision(lat, lng+lngDelta, bits), - // SE, - EncodeIntWithPrecision(lat-latDelta, lng+lngDelta, bits), - // S, - EncodeIntWithPrecision(lat-latDelta, lng, bits), - // SW, - EncodeIntWithPrecision(lat-latDelta, lng-lngDelta, bits), - // W, - EncodeIntWithPrecision(lat, lng-lngDelta, bits), - // NW - EncodeIntWithPrecision(lat+latDelta, lng-lngDelta, bits), - } -} - -// Neighbor returns a geohash string that corresponds to the provided -// geohash's neighbor in the provided direction -func Neighbor(hash string, direction Direction) string { - return Neighbors(hash)[direction] -} - -// NeighborInt returns a uint64 that corresponds to the provided hash's -// neighbor in the provided direction at 64-bit precision. -func NeighborInt(hash uint64, direction Direction) uint64 { - return NeighborsIntWithPrecision(hash, 64)[direction] -} - -// NeighborIntWithPrecision returns a uint64s that corresponds to the -// provided hash's neighbor in the provided direction at the given precision. -func NeighborIntWithPrecision(hash uint64, bits uint, direction Direction) uint64 { - return NeighborsIntWithPrecision(hash, bits)[direction] -} - -// precalculated for performance -var exp232 = math.Exp2(32) - -// Encode the position of x within the range -r to +r as a 32-bit integer. -func encodeRange(x, r float64) uint32 { - p := (x + r) / (2 * r) - return uint32(p * exp232) -} - -// Decode the 32-bit range encoding X back to a value in the range -r to +r. -func decodeRange(X uint32, r float64) float64 { - p := float64(X) / exp232 - x := 2*r*p - r - return x -} - -// Spread out the 32 bits of x into 64 bits, where the bits of x occupy even -// bit positions. -func spread(x uint32) uint64 { - X := uint64(x) - X = (X | (X << 16)) & 0x0000ffff0000ffff - X = (X | (X << 8)) & 0x00ff00ff00ff00ff - X = (X | (X << 4)) & 0x0f0f0f0f0f0f0f0f - X = (X | (X << 2)) & 0x3333333333333333 - X = (X | (X << 1)) & 0x5555555555555555 - return X -} - -// Interleave the bits of x and y. In the result, x and y occupy even and odd -// bitlevels, respectively. -func interleave(x, y uint32) uint64 { - return spread(x) | (spread(y) << 1) -} - -// Squash the even bitlevels of X into a 32-bit word. Odd bitlevels of X are -// ignored, and may take any value. -func squash(X uint64) uint32 { - X &= 0x5555555555555555 - X = (X | (X >> 1)) & 0x3333333333333333 - X = (X | (X >> 2)) & 0x0f0f0f0f0f0f0f0f - X = (X | (X >> 4)) & 0x00ff00ff00ff00ff - X = (X | (X >> 8)) & 0x0000ffff0000ffff - X = (X | (X >> 16)) & 0x00000000ffffffff - return uint32(X) -} - -// Deinterleave the bits of X into 32-bit words containing the even and odd -// bitlevels of X, respectively. -func deinterleave(X uint64) (uint32, uint32) { - return squash(X), squash(X >> 1) -} diff --git a/vendor/github.com/alicebob/miniredis/v2/gopher-json/LICENSE b/vendor/github.com/alicebob/miniredis/v2/gopher-json/LICENSE deleted file mode 100644 index 68a49daad..000000000 --- a/vendor/github.com/alicebob/miniredis/v2/gopher-json/LICENSE +++ /dev/null @@ -1,24 +0,0 @@ -This is free and unencumbered software released into the public domain. - -Anyone is free to copy, modify, publish, use, compile, sell, or -distribute this software, either in source code form or as a compiled -binary, for any purpose, commercial or non-commercial, and by any -means. - -In jurisdictions that recognize copyright laws, the author or authors -of this software dedicate any and all copyright interest in the -software to the public domain. We make this dedication for the benefit -of the public at large and to the detriment of our heirs and -successors. We intend this dedication to be an overt act of -relinquishment in perpetuity of all present and future rights to this -software under copyright law. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR -OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, -ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -OTHER DEALINGS IN THE SOFTWARE. - -For more information, please refer to diff --git a/vendor/github.com/alicebob/miniredis/v2/gopher-json/README.md b/vendor/github.com/alicebob/miniredis/v2/gopher-json/README.md deleted file mode 100644 index 0459a1d8e..000000000 --- a/vendor/github.com/alicebob/miniredis/v2/gopher-json/README.md +++ /dev/null @@ -1 +0,0 @@ -Copied from https://github.com/layeh/gopher-json and https://github.com/alicebob/gopher-json diff --git a/vendor/github.com/alicebob/miniredis/v2/gopher-json/json.go b/vendor/github.com/alicebob/miniredis/v2/gopher-json/json.go deleted file mode 100644 index 11561333d..000000000 --- a/vendor/github.com/alicebob/miniredis/v2/gopher-json/json.go +++ /dev/null @@ -1,189 +0,0 @@ -package json - -import ( - "encoding/json" - "errors" - - "github.com/yuin/gopher-lua" -) - -// Preload adds json to the given Lua state's package.preload table. After it -// has been preloaded, it can be loaded using require: -// -// local json = require("json") -func Preload(L *lua.LState) { - L.PreloadModule("json", Loader) -} - -// Loader is the module loader function. -func Loader(L *lua.LState) int { - t := L.NewTable() - L.SetFuncs(t, api) - L.Push(t) - return 1 -} - -var api = map[string]lua.LGFunction{ - "decode": apiDecode, - "encode": apiEncode, -} - -func apiDecode(L *lua.LState) int { - if L.GetTop() != 1 { - L.Error(lua.LString("bad argument #1 to decode"), 1) - return 0 - } - str := L.CheckString(1) - - value, err := Decode(L, []byte(str)) - if err != nil { - L.Push(lua.LNil) - L.Push(lua.LString(err.Error())) - return 2 - } - L.Push(value) - return 1 -} - -func apiEncode(L *lua.LState) int { - if L.GetTop() != 1 { - L.Error(lua.LString("bad argument #1 to encode"), 1) - return 0 - } - value := L.CheckAny(1) - - data, err := Encode(value) - if err != nil { - L.Push(lua.LNil) - L.Push(lua.LString(err.Error())) - return 2 - } - L.Push(lua.LString(string(data))) - return 1 -} - -var ( - errNested = errors.New("cannot encode recursively nested tables to JSON") - errSparseArray = errors.New("cannot encode sparse array") - errInvalidKeys = errors.New("cannot encode mixed or invalid key types") -) - -type invalidTypeError lua.LValueType - -func (i invalidTypeError) Error() string { - return `cannot encode ` + lua.LValueType(i).String() + ` to JSON` -} - -// Encode returns the JSON encoding of value. -func Encode(value lua.LValue) ([]byte, error) { - return json.Marshal(jsonValue{ - LValue: value, - visited: make(map[*lua.LTable]bool), - }) -} - -type jsonValue struct { - lua.LValue - visited map[*lua.LTable]bool -} - -func (j jsonValue) MarshalJSON() (data []byte, err error) { - switch converted := j.LValue.(type) { - case lua.LBool: - data, err = json.Marshal(bool(converted)) - case lua.LNumber: - data, err = json.Marshal(float64(converted)) - case *lua.LNilType: - data = []byte(`null`) - case lua.LString: - data, err = json.Marshal(string(converted)) - case *lua.LTable: - if j.visited[converted] { - return nil, errNested - } - j.visited[converted] = true - - key, value := converted.Next(lua.LNil) - - switch key.Type() { - case lua.LTNil: // empty table - data = []byte(`[]`) - case lua.LTNumber: - arr := make([]jsonValue, 0, converted.Len()) - expectedKey := lua.LNumber(1) - for key != lua.LNil { - if key.Type() != lua.LTNumber { - err = errInvalidKeys - return - } - if expectedKey != key { - err = errSparseArray - return - } - arr = append(arr, jsonValue{value, j.visited}) - expectedKey++ - key, value = converted.Next(key) - } - data, err = json.Marshal(arr) - case lua.LTString: - obj := make(map[string]jsonValue) - for key != lua.LNil { - if key.Type() != lua.LTString { - err = errInvalidKeys - return - } - obj[key.String()] = jsonValue{value, j.visited} - key, value = converted.Next(key) - } - data, err = json.Marshal(obj) - default: - err = errInvalidKeys - } - default: - err = invalidTypeError(j.LValue.Type()) - } - return -} - -// Decode converts the JSON encoded data to Lua values. -func Decode(L *lua.LState, data []byte) (lua.LValue, error) { - var value interface{} - err := json.Unmarshal(data, &value) - if err != nil { - return nil, err - } - return DecodeValue(L, value), nil -} - -// DecodeValue converts the value to a Lua value. -// -// This function only converts values that the encoding/json package decodes to. -// All other values will return lua.LNil. -func DecodeValue(L *lua.LState, value interface{}) lua.LValue { - switch converted := value.(type) { - case bool: - return lua.LBool(converted) - case float64: - return lua.LNumber(converted) - case string: - return lua.LString(converted) - case json.Number: - return lua.LString(converted) - case []interface{}: - arr := L.CreateTable(len(converted), 0) - for _, item := range converted { - arr.Append(DecodeValue(L, item)) - } - return arr - case map[string]interface{}: - tbl := L.CreateTable(0, len(converted)) - for key, item := range converted { - tbl.RawSetH(lua.LString(key), DecodeValue(L, item)) - } - return tbl - case nil: - return lua.LNil - } - - return lua.LNil -} diff --git a/vendor/github.com/alicebob/miniredis/v2/hll.go b/vendor/github.com/alicebob/miniredis/v2/hll.go deleted file mode 100644 index d00ad78a2..000000000 --- a/vendor/github.com/alicebob/miniredis/v2/hll.go +++ /dev/null @@ -1,42 +0,0 @@ -package miniredis - -import ( - "github.com/alicebob/miniredis/v2/hyperloglog" -) - -type hll struct { - inner *hyperloglog.Sketch -} - -func newHll() *hll { - return &hll{ - inner: hyperloglog.New14(), - } -} - -// Add returns true if cardinality has been changed, or false otherwise. -func (h *hll) Add(item []byte) bool { - return h.inner.Insert(item) -} - -// Count returns the estimation of a set cardinality. -func (h *hll) Count() int { - return int(h.inner.Estimate()) -} - -// Merge merges the other hll into original one (not making a copy but doing this in place). -func (h *hll) Merge(other *hll) { - _ = h.inner.Merge(other.inner) -} - -// Bytes returns raw-bytes representation of hll data structure. -func (h *hll) Bytes() []byte { - dataBytes, _ := h.inner.MarshalBinary() - return dataBytes -} - -func (h *hll) copy() *hll { - return &hll{ - inner: h.inner.Clone(), - } -} diff --git a/vendor/github.com/alicebob/miniredis/v2/hyperloglog/LICENSE b/vendor/github.com/alicebob/miniredis/v2/hyperloglog/LICENSE deleted file mode 100644 index 8436fdb43..000000000 --- a/vendor/github.com/alicebob/miniredis/v2/hyperloglog/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2017 Axiom Inc. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/vendor/github.com/alicebob/miniredis/v2/hyperloglog/README.md b/vendor/github.com/alicebob/miniredis/v2/hyperloglog/README.md deleted file mode 100644 index 0fac68df2..000000000 --- a/vendor/github.com/alicebob/miniredis/v2/hyperloglog/README.md +++ /dev/null @@ -1 +0,0 @@ -This is a copy of github.com/axiomhq/hyperloglog. \ No newline at end of file diff --git a/vendor/github.com/alicebob/miniredis/v2/hyperloglog/compressed.go b/vendor/github.com/alicebob/miniredis/v2/hyperloglog/compressed.go deleted file mode 100644 index 4b908be46..000000000 --- a/vendor/github.com/alicebob/miniredis/v2/hyperloglog/compressed.go +++ /dev/null @@ -1,180 +0,0 @@ -package hyperloglog - -import "encoding/binary" - -// Original author of this file is github.com/clarkduvall/hyperloglog -type iterable interface { - decode(i int, last uint32) (uint32, int) - Len() int - Iter() *iterator -} - -type iterator struct { - i int - last uint32 - v iterable -} - -func (iter *iterator) Next() uint32 { - n, i := iter.v.decode(iter.i, iter.last) - iter.last = n - iter.i = i - return n -} - -func (iter *iterator) Peek() uint32 { - n, _ := iter.v.decode(iter.i, iter.last) - return n -} - -func (iter iterator) HasNext() bool { - return iter.i < iter.v.Len() -} - -type compressedList struct { - count uint32 - last uint32 - b variableLengthList -} - -func (v *compressedList) Clone() *compressedList { - if v == nil { - return nil - } - - newV := &compressedList{ - count: v.count, - last: v.last, - } - - newV.b = make(variableLengthList, len(v.b)) - copy(newV.b, v.b) - return newV -} - -func (v *compressedList) MarshalBinary() (data []byte, err error) { - // Marshal the variableLengthList - bdata, err := v.b.MarshalBinary() - if err != nil { - return nil, err - } - - // At least 4 bytes for the two fixed sized values plus the size of bdata. - data = make([]byte, 0, 4+4+len(bdata)) - - // Marshal the count and last values. - data = append(data, []byte{ - // Number of items in the list. - byte(v.count >> 24), - byte(v.count >> 16), - byte(v.count >> 8), - byte(v.count), - // The last item in the list. - byte(v.last >> 24), - byte(v.last >> 16), - byte(v.last >> 8), - byte(v.last), - }...) - - // Append the list - return append(data, bdata...), nil -} - -func (v *compressedList) UnmarshalBinary(data []byte) error { - if len(data) < 12 { - return ErrorTooShort - } - - // Set the count. - v.count, data = binary.BigEndian.Uint32(data[:4]), data[4:] - - // Set the last value. - v.last, data = binary.BigEndian.Uint32(data[:4]), data[4:] - - // Set the list. - sz, data := binary.BigEndian.Uint32(data[:4]), data[4:] - v.b = make([]uint8, sz) - if uint32(len(data)) < sz { - return ErrorTooShort - } - for i := uint32(0); i < sz; i++ { - v.b[i] = data[i] - } - return nil -} - -func newCompressedList() *compressedList { - v := &compressedList{} - v.b = make(variableLengthList, 0) - return v -} - -func (v *compressedList) Len() int { - return len(v.b) -} - -func (v *compressedList) decode(i int, last uint32) (uint32, int) { - n, i := v.b.decode(i, last) - return n + last, i -} - -func (v *compressedList) Append(x uint32) { - v.count++ - v.b = v.b.Append(x - v.last) - v.last = x -} - -func (v *compressedList) Iter() *iterator { - return &iterator{0, 0, v} -} - -type variableLengthList []uint8 - -func (v variableLengthList) MarshalBinary() (data []byte, err error) { - // 4 bytes for the size of the list, and a byte for each element in the - // list. - data = make([]byte, 0, 4+v.Len()) - - // Length of the list. We only need 32 bits because the size of the set - // couldn't exceed that on 32 bit architectures. - sz := v.Len() - data = append(data, []byte{ - byte(sz >> 24), - byte(sz >> 16), - byte(sz >> 8), - byte(sz), - }...) - - // Marshal each element in the list. - for i := 0; i < sz; i++ { - data = append(data, v[i]) - } - - return data, nil -} - -func (v variableLengthList) Len() int { - return len(v) -} - -func (v *variableLengthList) Iter() *iterator { - return &iterator{0, 0, v} -} - -func (v variableLengthList) decode(i int, last uint32) (uint32, int) { - var x uint32 - j := i - for ; v[j]&0x80 != 0; j++ { - x |= uint32(v[j]&0x7f) << (uint(j-i) * 7) - } - x |= uint32(v[j]) << (uint(j-i) * 7) - return x, j + 1 -} - -func (v variableLengthList) Append(x uint32) variableLengthList { - for x&0xffffff80 != 0 { - v = append(v, uint8((x&0x7f)|0x80)) - x >>= 7 - } - return append(v, uint8(x&0x7f)) -} diff --git a/vendor/github.com/alicebob/miniredis/v2/hyperloglog/hyperloglog.go b/vendor/github.com/alicebob/miniredis/v2/hyperloglog/hyperloglog.go deleted file mode 100644 index 826639158..000000000 --- a/vendor/github.com/alicebob/miniredis/v2/hyperloglog/hyperloglog.go +++ /dev/null @@ -1,424 +0,0 @@ -package hyperloglog - -import ( - "encoding/binary" - "errors" - "fmt" - "math" - "sort" -) - -const ( - capacity = uint8(16) - pp = uint8(25) - mp = uint32(1) << pp - version = 1 -) - -// Sketch is a HyperLogLog data-structure for the count-distinct problem, -// approximating the number of distinct elements in a multiset. -type Sketch struct { - p uint8 - b uint8 - m uint32 - alpha float64 - tmpSet set - sparseList *compressedList - regs *registers -} - -// New returns a HyperLogLog Sketch with 2^14 registers (precision 14) -func New() *Sketch { - return New14() -} - -// New14 returns a HyperLogLog Sketch with 2^14 registers (precision 14) -func New14() *Sketch { - sk, _ := newSketch(14, true) - return sk -} - -// New16 returns a HyperLogLog Sketch with 2^16 registers (precision 16) -func New16() *Sketch { - sk, _ := newSketch(16, true) - return sk -} - -// NewNoSparse returns a HyperLogLog Sketch with 2^14 registers (precision 14) -// that will not use a sparse representation -func NewNoSparse() *Sketch { - sk, _ := newSketch(14, false) - return sk -} - -// New16NoSparse returns a HyperLogLog Sketch with 2^16 registers (precision 16) -// that will not use a sparse representation -func New16NoSparse() *Sketch { - sk, _ := newSketch(16, false) - return sk -} - -// newSketch returns a HyperLogLog Sketch with 2^precision registers -func newSketch(precision uint8, sparse bool) (*Sketch, error) { - if precision < 4 || precision > 18 { - return nil, fmt.Errorf("p has to be >= 4 and <= 18") - } - m := uint32(math.Pow(2, float64(precision))) - s := &Sketch{ - m: m, - p: precision, - alpha: alpha(float64(m)), - } - if sparse { - s.tmpSet = set{} - s.sparseList = newCompressedList() - } else { - s.regs = newRegisters(m) - } - return s, nil -} - -func (sk *Sketch) sparse() bool { - return sk.sparseList != nil -} - -// Clone returns a deep copy of sk. -func (sk *Sketch) Clone() *Sketch { - return &Sketch{ - b: sk.b, - p: sk.p, - m: sk.m, - alpha: sk.alpha, - tmpSet: sk.tmpSet.Clone(), - sparseList: sk.sparseList.Clone(), - regs: sk.regs.clone(), - } -} - -// Converts to normal if the sparse list is too large. -func (sk *Sketch) maybeToNormal() { - if uint32(len(sk.tmpSet))*100 > sk.m { - sk.mergeSparse() - if uint32(sk.sparseList.Len()) > sk.m { - sk.toNormal() - } - } -} - -// Merge takes another Sketch and combines it with Sketch h. -// If Sketch h is using the sparse Sketch, it will be converted -// to the normal Sketch. -func (sk *Sketch) Merge(other *Sketch) error { - if other == nil { - // Nothing to do - return nil - } - cpOther := other.Clone() - - if sk.p != cpOther.p { - return errors.New("precisions must be equal") - } - - if sk.sparse() && other.sparse() { - for k := range other.tmpSet { - sk.tmpSet.add(k) - } - for iter := other.sparseList.Iter(); iter.HasNext(); { - sk.tmpSet.add(iter.Next()) - } - sk.maybeToNormal() - return nil - } - - if sk.sparse() { - sk.toNormal() - } - - if cpOther.sparse() { - for k := range cpOther.tmpSet { - i, r := decodeHash(k, cpOther.p, pp) - sk.insert(i, r) - } - - for iter := cpOther.sparseList.Iter(); iter.HasNext(); { - i, r := decodeHash(iter.Next(), cpOther.p, pp) - sk.insert(i, r) - } - } else { - if sk.b < cpOther.b { - sk.regs.rebase(cpOther.b - sk.b) - sk.b = cpOther.b - } else { - cpOther.regs.rebase(sk.b - cpOther.b) - cpOther.b = sk.b - } - - for i, v := range cpOther.regs.tailcuts { - v1 := v.get(0) - if v1 > sk.regs.get(uint32(i)*2) { - sk.regs.set(uint32(i)*2, v1) - } - v2 := v.get(1) - if v2 > sk.regs.get(1+uint32(i)*2) { - sk.regs.set(1+uint32(i)*2, v2) - } - } - } - return nil -} - -// Convert from sparse Sketch to dense Sketch. -func (sk *Sketch) toNormal() { - if len(sk.tmpSet) > 0 { - sk.mergeSparse() - } - - sk.regs = newRegisters(sk.m) - for iter := sk.sparseList.Iter(); iter.HasNext(); { - i, r := decodeHash(iter.Next(), sk.p, pp) - sk.insert(i, r) - } - - sk.tmpSet = nil - sk.sparseList = nil -} - -func (sk *Sketch) insert(i uint32, r uint8) bool { - changed := false - if r-sk.b >= capacity { - //overflow - db := sk.regs.min() - if db > 0 { - sk.b += db - sk.regs.rebase(db) - changed = true - } - } - if r > sk.b { - val := r - sk.b - if c1 := capacity - 1; c1 < val { - val = c1 - } - - if val > sk.regs.get(i) { - sk.regs.set(i, val) - changed = true - } - } - return changed -} - -// Insert adds element e to sketch -func (sk *Sketch) Insert(e []byte) bool { - x := hash(e) - return sk.InsertHash(x) -} - -// InsertHash adds hash x to sketch -func (sk *Sketch) InsertHash(x uint64) bool { - if sk.sparse() { - changed := sk.tmpSet.add(encodeHash(x, sk.p, pp)) - if !changed { - return false - } - if uint32(len(sk.tmpSet))*100 > sk.m/2 { - sk.mergeSparse() - if uint32(sk.sparseList.Len()) > sk.m/2 { - sk.toNormal() - } - } - return true - } else { - i, r := getPosVal(x, sk.p) - return sk.insert(uint32(i), r) - } -} - -// Estimate returns the cardinality of the Sketch -func (sk *Sketch) Estimate() uint64 { - if sk.sparse() { - sk.mergeSparse() - return uint64(linearCount(mp, mp-sk.sparseList.count)) - } - - sum, ez := sk.regs.sumAndZeros(sk.b) - m := float64(sk.m) - var est float64 - - var beta func(float64) float64 - if sk.p < 16 { - beta = beta14 - } else { - beta = beta16 - } - - if sk.b == 0 { - est = (sk.alpha * m * (m - ez) / (sum + beta(ez))) - } else { - est = (sk.alpha * m * m / sum) - } - - return uint64(est + 0.5) -} - -func (sk *Sketch) mergeSparse() { - if len(sk.tmpSet) == 0 { - return - } - - keys := make(uint64Slice, 0, len(sk.tmpSet)) - for k := range sk.tmpSet { - keys = append(keys, k) - } - sort.Sort(keys) - - newList := newCompressedList() - for iter, i := sk.sparseList.Iter(), 0; iter.HasNext() || i < len(keys); { - if !iter.HasNext() { - newList.Append(keys[i]) - i++ - continue - } - - if i >= len(keys) { - newList.Append(iter.Next()) - continue - } - - x1, x2 := iter.Peek(), keys[i] - if x1 == x2 { - newList.Append(iter.Next()) - i++ - } else if x1 > x2 { - newList.Append(x2) - i++ - } else { - newList.Append(iter.Next()) - } - } - - sk.sparseList = newList - sk.tmpSet = set{} -} - -// MarshalBinary implements the encoding.BinaryMarshaler interface. -func (sk *Sketch) MarshalBinary() (data []byte, err error) { - // Marshal a version marker. - data = append(data, version) - // Marshal p. - data = append(data, sk.p) - // Marshal b - data = append(data, sk.b) - - if sk.sparse() { - // It's using the sparse Sketch. - data = append(data, byte(1)) - - // Add the tmp_set - tsdata, err := sk.tmpSet.MarshalBinary() - if err != nil { - return nil, err - } - data = append(data, tsdata...) - - // Add the sparse Sketch - sdata, err := sk.sparseList.MarshalBinary() - if err != nil { - return nil, err - } - return append(data, sdata...), nil - } - - // It's using the dense Sketch. - data = append(data, byte(0)) - - // Add the dense sketch Sketch. - sz := len(sk.regs.tailcuts) - data = append(data, []byte{ - byte(sz >> 24), - byte(sz >> 16), - byte(sz >> 8), - byte(sz), - }...) - - // Marshal each element in the list. - for i := 0; i < len(sk.regs.tailcuts); i++ { - data = append(data, byte(sk.regs.tailcuts[i])) - } - - return data, nil -} - -// ErrorTooShort is an error that UnmarshalBinary try to parse too short -// binary. -var ErrorTooShort = errors.New("too short binary") - -// UnmarshalBinary implements the encoding.BinaryUnmarshaler interface. -func (sk *Sketch) UnmarshalBinary(data []byte) error { - if len(data) < 8 { - return ErrorTooShort - } - - // Unmarshal version. We may need this in the future if we make - // non-compatible changes. - _ = data[0] - - // Unmarshal p. - p := data[1] - - // Unmarshal b. - sk.b = data[2] - - // Determine if we need a sparse Sketch - sparse := data[3] == byte(1) - - // Make a newSketch Sketch if the precision doesn't match or if the Sketch was used - if sk.p != p || sk.regs != nil || len(sk.tmpSet) > 0 || (sk.sparseList != nil && sk.sparseList.Len() > 0) { - newh, err := newSketch(p, sparse) - if err != nil { - return err - } - newh.b = sk.b - *sk = *newh - } - - // h is now initialised with the correct p. We just need to fill the - // rest of the details out. - if sparse { - // Using the sparse Sketch. - - // Unmarshal the tmp_set. - tssz := binary.BigEndian.Uint32(data[4:8]) - sk.tmpSet = make(map[uint32]struct{}, tssz) - - // We need to unmarshal tssz values in total, and each value requires us - // to read 4 bytes. - tsLastByte := int((tssz * 4) + 8) - for i := 8; i < tsLastByte; i += 4 { - k := binary.BigEndian.Uint32(data[i : i+4]) - sk.tmpSet[k] = struct{}{} - } - - // Unmarshal the sparse Sketch. - return sk.sparseList.UnmarshalBinary(data[tsLastByte:]) - } - - // Using the dense Sketch. - sk.sparseList = nil - sk.tmpSet = nil - dsz := binary.BigEndian.Uint32(data[4:8]) - sk.regs = newRegisters(dsz * 2) - data = data[8:] - - for i, val := range data { - sk.regs.tailcuts[i] = reg(val) - if uint8(sk.regs.tailcuts[i]<<4>>4) > 0 { - sk.regs.nz-- - } - if uint8(sk.regs.tailcuts[i]>>4) > 0 { - sk.regs.nz-- - } - } - - return nil -} diff --git a/vendor/github.com/alicebob/miniredis/v2/hyperloglog/registers.go b/vendor/github.com/alicebob/miniredis/v2/hyperloglog/registers.go deleted file mode 100644 index 19bb5d47f..000000000 --- a/vendor/github.com/alicebob/miniredis/v2/hyperloglog/registers.go +++ /dev/null @@ -1,114 +0,0 @@ -package hyperloglog - -import ( - "math" -) - -type reg uint8 -type tailcuts []reg - -type registers struct { - tailcuts - nz uint32 -} - -func (r *reg) set(offset, val uint8) bool { - var isZero bool - if offset == 0 { - isZero = *r < 16 - tmpVal := uint8((*r) << 4 >> 4) - *r = reg(tmpVal | (val << 4)) - } else { - isZero = *r&0x0f == 0 - tmpVal := uint8((*r) >> 4 << 4) - *r = reg(tmpVal | val) - } - return isZero -} - -func (r *reg) get(offset uint8) uint8 { - if offset == 0 { - return uint8((*r) >> 4) - } - return uint8((*r) << 4 >> 4) -} - -func newRegisters(size uint32) *registers { - return ®isters{ - tailcuts: make(tailcuts, size/2), - nz: size, - } -} - -func (rs *registers) clone() *registers { - if rs == nil { - return nil - } - tc := make([]reg, len(rs.tailcuts)) - copy(tc, rs.tailcuts) - return ®isters{ - tailcuts: tc, - nz: rs.nz, - } -} - -func (rs *registers) rebase(delta uint8) { - nz := uint32(len(rs.tailcuts)) * 2 - for i := range rs.tailcuts { - for j := uint8(0); j < 2; j++ { - val := rs.tailcuts[i].get(j) - if val >= delta { - rs.tailcuts[i].set(j, val-delta) - if val-delta > 0 { - nz-- - } - } - } - } - rs.nz = nz -} - -func (rs *registers) set(i uint32, val uint8) { - offset, index := uint8(i)&1, i/2 - if rs.tailcuts[index].set(offset, val) { - rs.nz-- - } -} - -func (rs *registers) get(i uint32) uint8 { - offset, index := uint8(i)&1, i/2 - return rs.tailcuts[index].get(offset) -} - -func (rs *registers) sumAndZeros(base uint8) (res, ez float64) { - for _, r := range rs.tailcuts { - for j := uint8(0); j < 2; j++ { - v := float64(base + r.get(j)) - if v == 0 { - ez++ - } - res += 1.0 / math.Pow(2.0, v) - } - } - rs.nz = uint32(ez) - return res, ez -} - -func (rs *registers) min() uint8 { - if rs.nz > 0 { - return 0 - } - min := uint8(math.MaxUint8) - for _, r := range rs.tailcuts { - if r == 0 || min == 0 { - return 0 - } - if val := uint8(r << 4 >> 4); val < min { - min = val - } - if val := uint8(r >> 4); val < min { - min = val - } - } - return min -} diff --git a/vendor/github.com/alicebob/miniredis/v2/hyperloglog/sparse.go b/vendor/github.com/alicebob/miniredis/v2/hyperloglog/sparse.go deleted file mode 100644 index 8c457d327..000000000 --- a/vendor/github.com/alicebob/miniredis/v2/hyperloglog/sparse.go +++ /dev/null @@ -1,92 +0,0 @@ -package hyperloglog - -import ( - "math/bits" -) - -func getIndex(k uint32, p, pp uint8) uint32 { - if k&1 == 1 { - return bextr32(k, 32-p, p) - } - return bextr32(k, pp-p+1, p) -} - -// Encode a hash to be used in the sparse representation. -func encodeHash(x uint64, p, pp uint8) uint32 { - idx := uint32(bextr(x, 64-pp, pp)) - if bextr(x, 64-pp, pp-p) == 0 { - zeros := bits.LeadingZeros64((bextr(x, 0, 64-pp)<> 24), - byte(sl >> 16), - byte(sl >> 8), - byte(sl), - }...) - - // Marshal each element in the set. - for k := range s { - data = append(data, []byte{ - byte(k >> 24), - byte(k >> 16), - byte(k >> 8), - byte(k), - }...) - } - - return data, nil -} - -type uint64Slice []uint32 - -func (p uint64Slice) Len() int { return len(p) } -func (p uint64Slice) Less(i, j int) bool { return p[i] < p[j] } -func (p uint64Slice) Swap(i, j int) { p[i], p[j] = p[j], p[i] } diff --git a/vendor/github.com/alicebob/miniredis/v2/hyperloglog/utils.go b/vendor/github.com/alicebob/miniredis/v2/hyperloglog/utils.go deleted file mode 100644 index 896bf7e74..000000000 --- a/vendor/github.com/alicebob/miniredis/v2/hyperloglog/utils.go +++ /dev/null @@ -1,69 +0,0 @@ -package hyperloglog - -import ( - "github.com/alicebob/miniredis/v2/metro" - "math" - "math/bits" -) - -var hash = hashFunc - -func beta14(ez float64) float64 { - zl := math.Log(ez + 1) - return -0.370393911*ez + - 0.070471823*zl + - 0.17393686*math.Pow(zl, 2) + - 0.16339839*math.Pow(zl, 3) + - -0.09237745*math.Pow(zl, 4) + - 0.03738027*math.Pow(zl, 5) + - -0.005384159*math.Pow(zl, 6) + - 0.00042419*math.Pow(zl, 7) -} - -func beta16(ez float64) float64 { - zl := math.Log(ez + 1) - return -0.37331876643753059*ez + - -1.41704077448122989*zl + - 0.40729184796612533*math.Pow(zl, 2) + - 1.56152033906584164*math.Pow(zl, 3) + - -0.99242233534286128*math.Pow(zl, 4) + - 0.26064681399483092*math.Pow(zl, 5) + - -0.03053811369682807*math.Pow(zl, 6) + - 0.00155770210179105*math.Pow(zl, 7) -} - -func alpha(m float64) float64 { - switch m { - case 16: - return 0.673 - case 32: - return 0.697 - case 64: - return 0.709 - } - return 0.7213 / (1 + 1.079/m) -} - -func getPosVal(x uint64, p uint8) (uint64, uint8) { - i := bextr(x, 64-p, p) // {x63,...,x64-p} - w := x<

> start) & ((1 << length) - 1) -} - -func bextr32(v uint32, start, length uint8) uint32 { - return (v >> start) & ((1 << length) - 1) -} - -func hashFunc(e []byte) uint64 { - return metro.Hash64(e, 1337) -} diff --git a/vendor/github.com/alicebob/miniredis/v2/keys.go b/vendor/github.com/alicebob/miniredis/v2/keys.go deleted file mode 100644 index 058e0a79a..000000000 --- a/vendor/github.com/alicebob/miniredis/v2/keys.go +++ /dev/null @@ -1,83 +0,0 @@ -package miniredis - -// Translate the 'KEYS' or 'PSUBSCRIBE' argument ('foo*', 'f??', &c.) into a regexp. - -import ( - "bytes" - "regexp" -) - -// patternRE compiles a glob to a regexp. Returns nil if the given -// pattern will never match anything. -// The general strategy is to sandwich all non-meta characters between \Q...\E. -func patternRE(k string) *regexp.Regexp { - re := bytes.Buffer{} - re.WriteString(`(?s)^\Q`) - for i := 0; i < len(k); i++ { - p := k[i] - switch p { - case '*': - re.WriteString(`\E.*\Q`) - case '?': - re.WriteString(`\E.\Q`) - case '[': - charClass := bytes.Buffer{} - i++ - for ; i < len(k); i++ { - if k[i] == ']' { - break - } - if k[i] == '\\' { - if i == len(k)-1 { - // Ends with a '\'. U-huh. - return nil - } - charClass.WriteByte(k[i]) - i++ - charClass.WriteByte(k[i]) - continue - } - charClass.WriteByte(k[i]) - } - if charClass.Len() == 0 { - // '[]' is valid in Redis, but matches nothing. - return nil - } - re.WriteString(`\E[`) - re.Write(charClass.Bytes()) - re.WriteString(`]\Q`) - - case '\\': - if i == len(k)-1 { - // Ends with a '\'. U-huh. - return nil - } - // Forget the \, keep the next char. - i++ - re.WriteByte(k[i]) - continue - default: - re.WriteByte(p) - } - } - re.WriteString(`\E$`) - return regexp.MustCompile(re.String()) -} - -// matchKeys filters only matching keys. -// The returned boolean is whether the match pattern was valid -func matchKeys(keys []string, match string) ([]string, bool) { - re := patternRE(match) - if re == nil { - // Special case: the given pattern won't match anything or is invalid. - return nil, false - } - var res []string - for _, k := range keys { - if !re.MatchString(k) { - continue - } - res = append(res, k) - } - return res, true -} diff --git a/vendor/github.com/alicebob/miniredis/v2/lua.go b/vendor/github.com/alicebob/miniredis/v2/lua.go deleted file mode 100644 index 29f3aeaec..000000000 --- a/vendor/github.com/alicebob/miniredis/v2/lua.go +++ /dev/null @@ -1,312 +0,0 @@ -package miniredis - -import ( - "bufio" - "bytes" - "fmt" - "strings" - - lua "github.com/yuin/gopher-lua" - - "github.com/alicebob/miniredis/v2/server" -) - -var luaRedisConstants = map[string]lua.LValue{ - "LOG_DEBUG": lua.LNumber(0), - "LOG_VERBOSE": lua.LNumber(1), - "LOG_NOTICE": lua.LNumber(2), - "LOG_WARNING": lua.LNumber(3), -} - -func mkLua(srv *server.Server, c *server.Peer, sha string, readOnly bool) (map[string]lua.LGFunction, map[string]lua.LValue) { - mkCall := func(failFast bool) func(l *lua.LState) int { - // one server.Ctx for a single Lua run - pCtx := &connCtx{} - if getCtx(c).authenticated { - pCtx.authenticated = true - } - pCtx.nested = true - pCtx.nestedSHA = sha - pCtx.selectedDB = getCtx(c).selectedDB - - return func(l *lua.LState) int { - top := l.GetTop() - if top == 0 { - l.Error(lua.LString(fmt.Sprintf("Please specify at least one argument for this redis lib call script: %s, &c.", sha)), 1) - return 0 - } - var args []string - for i := 1; i <= top; i++ { - switch a := l.Get(i).(type) { - case lua.LNumber: - args = append(args, a.String()) - case lua.LString: - args = append(args, string(a)) - default: - l.Error(lua.LString(fmt.Sprintf("Lua redis lib command arguments must be strings or integers script: %s, &c.", sha)), 1) - return 0 - } - } - if len(args) == 0 { - l.Error(lua.LString(msgNotFromScripts(sha)), 1) - return 0 - } - - if readOnly && len(args) > 0 { - if srv.IsRegisteredCommand(args[0]) && !srv.IsReadOnlyCommand(args[0]) { - if failFast { - l.Error(lua.LString("Write commands are not allowed in read-only scripts"), 1) - return 0 - } - // pcall() mode - return error table - res := &lua.LTable{} - res.RawSetString("err", lua.LString("Write commands are not allowed in read-only scripts")) - l.Push(res) - return 1 - } - } - - buf := &bytes.Buffer{} - wr := bufio.NewWriter(buf) - peer := server.NewPeer(wr) - peer.Ctx = pCtx - srv.Dispatch(peer, args) - wr.Flush() - - res, err := server.ParseReply(bufio.NewReader(buf)) - if err != nil { - if failFast { - // call() mode - if strings.Contains(err.Error(), "ERR unknown command") { - l.Error(lua.LString(fmt.Sprintf("Unknown Redis command called from script script: %s, &c.", sha)), 1) - } else { - l.Error(lua.LString(err.Error()), 1) - } - return 0 - } - // pcall() mode - res := &lua.LTable{} - if strings.Contains(err.Error(), "ERR unknown command") { - res.RawSetString("err", lua.LString("ERR Unknown Redis command called from script")) - } else { - res.RawSetString("err", lua.LString(err.Error())) - } - l.Push(res) - return 1 - } - - if res == nil { - l.Push(lua.LFalse) - } else { - switch r := res.(type) { - case int64: - l.Push(lua.LNumber(r)) - case int: - l.Push(lua.LNumber(r)) - case []uint8: - l.Push(lua.LString(string(r))) - case []interface{}: - l.Push(redisToLua(l, r)) - case server.Simple: - l.Push(luaStatusReply(string(r))) - case string: - l.Push(lua.LString(r)) - case error: - l.Error(lua.LString(r.Error()), 1) - return 0 - default: - panic(fmt.Sprintf("type not handled (%T)", r)) - } - } - return 1 - } - } - - return map[string]lua.LGFunction{ - "call": mkCall(true), - "pcall": mkCall(false), - "error_reply": func(l *lua.LState) int { - v := l.Get(1) - msg, ok := v.(lua.LString) - if !ok { - l.Error(lua.LString("wrong number or type of arguments"), 1) - return 0 - } - res := &lua.LTable{} - parts := strings.SplitN(msg.String(), " ", 2) - // '-' at the beginging will be added as a part of error response - if parts[0] != "" && parts[0][0] == '-' { - parts[0] = parts[0][1:] - } - var final_msg string - if len(parts) == 2 { - final_msg = fmt.Sprintf("%s %s", parts[0], parts[1]) - } else { - final_msg = fmt.Sprintf("ERR %s", parts[0]) - } - res.RawSetString("err", lua.LString(final_msg)) - l.Push(res) - return 1 - }, - "log": func(l *lua.LState) int { - level := l.CheckInt(1) - msg := l.CheckString(2) - _, _ = level, msg - // do nothing by default. To see logs uncomment: - // fmt.Printf("%v: %v", level, msg) - return 0 - }, - "status_reply": func(l *lua.LState) int { - v := l.Get(1) - msg, ok := v.(lua.LString) - if !ok { - l.Error(lua.LString("wrong number or type of arguments"), 1) - return 0 - } - res := luaStatusReply(string(msg)) - l.Push(res) - return 1 - }, - "sha1hex": func(l *lua.LState) int { - top := l.GetTop() - if top != 1 { - l.Error(lua.LString("wrong number of arguments"), 1) - return 0 - } - msg := lua.LVAsString(l.Get(1)) - l.Push(lua.LString(sha1Hex(msg))) - return 1 - }, - "replicate_commands": func(l *lua.LState) int { - // always succeeds since 7.0.0 - l.Push(lua.LTrue) - return 1 - }, - "set_repl": func(l *lua.LState) int { - top := l.GetTop() - if top != 1 { - l.Error(lua.LString("wrong number of arguments"), 1) - return 0 - } - // ignored - return 1 - }, - "setresp": func(l *lua.LState) int { - level := l.CheckInt(1) - toresp3 := false - switch level { - case 2: - toresp3 = false - case 3: - toresp3 = true - default: - l.Error(lua.LString("RESP version must be 2 or 3"), 1) - return 0 - } - c.SwitchResp3 = &toresp3 - return 0 - }, - }, luaRedisConstants -} - -func luaToRedis(l *lua.LState, c *server.Peer, value lua.LValue) { - if value == nil { - c.WriteNull() - return - } - - switch t := value.(type) { - case *lua.LNilType: - c.WriteNull() - case lua.LBool: - if lua.LVAsBool(value) { - c.WriteInt(1) - } else { - c.WriteNull() - } - case lua.LNumber: - c.WriteInt(int(lua.LVAsNumber(value))) - case lua.LString: - s := lua.LVAsString(value) - c.WriteBulk(s) - case *lua.LTable: - // special case for tables with an 'err' or 'ok' field - // note: according to the docs this only counts when 'err' or 'ok' is - // the only field. - if s := t.RawGetString("err"); s.Type() != lua.LTNil { - c.WriteError(s.String()) - return - } - if s := t.RawGetString("ok"); s.Type() != lua.LTNil { - c.WriteInline(s.String()) - return - } - - result := []lua.LValue{} - for j := 1; true; j++ { - val := l.GetTable(value, lua.LNumber(j)) - if val == nil { - result = append(result, val) - continue - } - - if val.Type() == lua.LTNil { - break - } - - result = append(result, val) - } - - c.WriteLen(len(result)) - for _, r := range result { - luaToRedis(l, c, r) - } - default: - panic(fmt.Sprintf("wat: %T", t)) - } -} - -func redisToLua(l *lua.LState, res []interface{}) *lua.LTable { - rettb := l.NewTable() - for _, e := range res { - var v lua.LValue - if e == nil { - v = lua.LFalse - } else { - switch et := e.(type) { - case int: - v = lua.LNumber(et) - case int64: - v = lua.LNumber(et) - case []uint8: - v = lua.LString(string(et)) - case []interface{}: - v = redisToLua(l, et) - case string: - v = lua.LString(et) - default: - // TODO: oops? - v = lua.LString(e.(string)) - } - } - l.RawSet(rettb, lua.LNumber(rettb.Len()+1), v) - } - return rettb -} - -func luaStatusReply(msg string) *lua.LTable { - tab := &lua.LTable{} - tab.RawSetString("ok", lua.LString(msg)) - return tab -} - -// Our very minimal "os." lua lib. -func mkLuaOS() map[string]lua.LGFunction { - return map[string]lua.LGFunction{ - // > Returns an approximation of the amount in seconds of CPU time used by the program - "clock": func(l *lua.LState) int { - l.Push(lua.LNumber(42)) - return 1 - }, - } -} diff --git a/vendor/github.com/alicebob/miniredis/v2/metro/LICENSE b/vendor/github.com/alicebob/miniredis/v2/metro/LICENSE deleted file mode 100644 index 6243b617c..000000000 --- a/vendor/github.com/alicebob/miniredis/v2/metro/LICENSE +++ /dev/null @@ -1,24 +0,0 @@ -This package is a mechanical translation of the reference C++ code for -MetroHash, available at https://github.com/jandrewrogers/MetroHash - -The MIT License (MIT) - -Copyright (c) 2016 Damian Gryski - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/vendor/github.com/alicebob/miniredis/v2/metro/README.md b/vendor/github.com/alicebob/miniredis/v2/metro/README.md deleted file mode 100644 index 07e4ee9f7..000000000 --- a/vendor/github.com/alicebob/miniredis/v2/metro/README.md +++ /dev/null @@ -1 +0,0 @@ -This is a partial copy of github.com/dgryski/go-metro. \ No newline at end of file diff --git a/vendor/github.com/alicebob/miniredis/v2/metro/metro64.go b/vendor/github.com/alicebob/miniredis/v2/metro/metro64.go deleted file mode 100644 index 5b3db9a90..000000000 --- a/vendor/github.com/alicebob/miniredis/v2/metro/metro64.go +++ /dev/null @@ -1,87 +0,0 @@ -package metro - -import "encoding/binary" - -func Hash64(buffer []byte, seed uint64) uint64 { - - const ( - k0 = 0xD6D018F5 - k1 = 0xA2AA033B - k2 = 0x62992FC1 - k3 = 0x30BC5B29 - ) - - ptr := buffer - - hash := (seed + k2) * k0 - - if len(ptr) >= 32 { - v := [4]uint64{hash, hash, hash, hash} - - for len(ptr) >= 32 { - v[0] += binary.LittleEndian.Uint64(ptr[:8]) * k0 - v[0] = rotate_right(v[0], 29) + v[2] - v[1] += binary.LittleEndian.Uint64(ptr[8:16]) * k1 - v[1] = rotate_right(v[1], 29) + v[3] - v[2] += binary.LittleEndian.Uint64(ptr[16:24]) * k2 - v[2] = rotate_right(v[2], 29) + v[0] - v[3] += binary.LittleEndian.Uint64(ptr[24:32]) * k3 - v[3] = rotate_right(v[3], 29) + v[1] - ptr = ptr[32:] - } - - v[2] ^= rotate_right(((v[0]+v[3])*k0)+v[1], 37) * k1 - v[3] ^= rotate_right(((v[1]+v[2])*k1)+v[0], 37) * k0 - v[0] ^= rotate_right(((v[0]+v[2])*k0)+v[3], 37) * k1 - v[1] ^= rotate_right(((v[1]+v[3])*k1)+v[2], 37) * k0 - hash += v[0] ^ v[1] - } - - if len(ptr) >= 16 { - v0 := hash + (binary.LittleEndian.Uint64(ptr[:8]) * k2) - v0 = rotate_right(v0, 29) * k3 - v1 := hash + (binary.LittleEndian.Uint64(ptr[8:16]) * k2) - v1 = rotate_right(v1, 29) * k3 - v0 ^= rotate_right(v0*k0, 21) + v1 - v1 ^= rotate_right(v1*k3, 21) + v0 - hash += v1 - ptr = ptr[16:] - } - - if len(ptr) >= 8 { - hash += binary.LittleEndian.Uint64(ptr[:8]) * k3 - ptr = ptr[8:] - hash ^= rotate_right(hash, 55) * k1 - } - - if len(ptr) >= 4 { - hash += uint64(binary.LittleEndian.Uint32(ptr[:4])) * k3 - hash ^= rotate_right(hash, 26) * k1 - ptr = ptr[4:] - } - - if len(ptr) >= 2 { - hash += uint64(binary.LittleEndian.Uint16(ptr[:2])) * k3 - ptr = ptr[2:] - hash ^= rotate_right(hash, 48) * k1 - } - - if len(ptr) >= 1 { - hash += uint64(ptr[0]) * k3 - hash ^= rotate_right(hash, 37) * k1 - } - - hash ^= rotate_right(hash, 28) - hash *= k0 - hash ^= rotate_right(hash, 29) - - return hash -} - -func Hash64Str(buffer string, seed uint64) uint64 { - return Hash64([]byte(buffer), seed) -} - -func rotate_right(v uint64, k uint) uint64 { - return (v >> k) | (v << (64 - k)) -} diff --git a/vendor/github.com/alicebob/miniredis/v2/miniredis.go b/vendor/github.com/alicebob/miniredis/v2/miniredis.go deleted file mode 100644 index 7e65f1635..000000000 --- a/vendor/github.com/alicebob/miniredis/v2/miniredis.go +++ /dev/null @@ -1,792 +0,0 @@ -// Package miniredis is a pure Go Redis test server, for use in Go unittests. -// There are no dependencies on system binaries, and every server you start -// will be empty. -// -// import "github.com/alicebob/miniredis/v2" -// -// Start a server with `s := miniredis.RunT(t)`, it'll be shutdown via a t.Cleanup(). -// Or do everything manual: `s, err := miniredis.Run(); defer s.Close()` -// -// Point your Redis client to `s.Addr()` or `s.Host(), s.Port()`. -// -// Set keys directly via s.Set(...) and similar commands, or use a Redis client. -// -// For direct use you can select a Redis database with either `s.Select(12); -// s.Get("foo")` or `s.DB(12).Get("foo")`. -package miniredis - -import ( - "context" - "crypto/tls" - "fmt" - "math/rand" - "strconv" - "strings" - "sync" - "time" - - "github.com/alicebob/miniredis/v2/proto" - "github.com/alicebob/miniredis/v2/server" -) - -var DumpMaxLineLen = 60 - -type hashKey map[string]string -type listKey []string -type setKey map[string]struct{} - -// RedisDB holds a single (numbered) Redis database. -type RedisDB struct { - master *Miniredis // pointer to the lock in Miniredis - id int // db id - keys map[string]string // Master map of keys with their type - stringKeys map[string]string // GET/SET &c. keys - hashKeys map[string]hashKey // MGET/MSET &c. keys - listKeys map[string]listKey // LPUSH &c. keys - setKeys map[string]setKey // SADD &c. keys - hllKeys map[string]*hll // PFADD &c. keys - sortedsetKeys map[string]sortedSet // ZADD &c. keys - streamKeys map[string]*streamKey // XADD &c. keys - ttl map[string]time.Duration // effective TTL values - hashTTLs map[string]map[string]time.Duration // Hash TTL values - lru map[string]time.Time // last recently used ( read or written to ) - keyVersion map[string]uint // used to watch values -} - -// Miniredis is a Redis server implementation. -type Miniredis struct { - sync.Mutex - srv *server.Server - port int - passwords map[string]string // username password - dbs map[int]*RedisDB - selectedDB int // DB id used in the direct Get(), Set() &c. - scripts map[string]string // sha1 -> lua src - signal *sync.Cond - now time.Time // time.Now() if not set. - subscribers map[*Subscriber]struct{} - rand *rand.Rand - Ctx context.Context - CtxCancel context.CancelFunc -} - -type txCmd func(*server.Peer, *connCtx) - -// database id + key combo -type dbKey struct { - db int - key string -} - -// connCtx has all state for a single connection. -// (this struct was named before context.Context existed) -type connCtx struct { - selectedDB int // selected DB - authenticated bool // auth enabled and a valid AUTH seen - transaction []txCmd // transaction callbacks. Or nil. - dirtyTransaction bool // any error during QUEUEing - watch map[dbKey]uint // WATCHed keys - subscriber *Subscriber // client is in PUBSUB mode if not nil - nested bool // this is called via Lua - nestedSHA string // set to the SHA of the nesting function -} - -// NewMiniRedis makes a new, non-started, Miniredis object. -func NewMiniRedis() *Miniredis { - m := Miniredis{ - dbs: map[int]*RedisDB{}, - scripts: map[string]string{}, - subscribers: map[*Subscriber]struct{}{}, - } - m.Ctx, m.CtxCancel = context.WithCancel(context.Background()) - m.signal = sync.NewCond(&m) - return &m -} - -func newRedisDB(id int, m *Miniredis) RedisDB { - return RedisDB{ - id: id, - master: m, - keys: map[string]string{}, - lru: map[string]time.Time{}, - stringKeys: map[string]string{}, - hashKeys: map[string]hashKey{}, - listKeys: map[string]listKey{}, - setKeys: map[string]setKey{}, - hllKeys: map[string]*hll{}, - sortedsetKeys: map[string]sortedSet{}, - streamKeys: map[string]*streamKey{}, - ttl: map[string]time.Duration{}, - hashTTLs: make(map[string]map[string]time.Duration), - keyVersion: map[string]uint{}, - } -} - -// Run creates and Start()s a Miniredis. -func Run() (*Miniredis, error) { - m := NewMiniRedis() - return m, m.Start() -} - -// Run creates and Start()s a Miniredis, TLS version. -func RunTLS(cfg *tls.Config) (*Miniredis, error) { - m := NewMiniRedis() - return m, m.StartTLS(cfg) -} - -// Tester is a minimal version of a testing.T -type Tester interface { - Fatalf(string, ...interface{}) - Cleanup(func()) - Logf(format string, args ...interface{}) -} - -// RunT start a new miniredis, pass it a testing.T. It also registers the cleanup after your test is done. -func RunT(t Tester) *Miniredis { - m := NewMiniRedis() - if err := m.Start(); err != nil { - t.Fatalf("could not start miniredis: %s", err) - // not reached - } - t.Cleanup(m.Close) - return m -} - -func runWithClient(t Tester) (*Miniredis, *proto.Client) { - m := RunT(t) - - c, err := proto.Dial(m.Addr()) - if err != nil { - t.Fatalf("could not connect to miniredis: %s", err) - } - t.Cleanup(func() { - if err = c.Close(); err != nil { - t.Logf("error closing connection to miniredis: %s", err) - } - }) - - return m, c -} - -// Start starts a server. It listens on a random port on localhost. See also -// Addr(). -func (m *Miniredis) Start() error { - s, err := server.NewServer(fmt.Sprintf("127.0.0.1:%d", m.port)) - if err != nil { - return err - } - return m.start(s) -} - -// Start starts a server, TLS version. -func (m *Miniredis) StartTLS(cfg *tls.Config) error { - s, err := server.NewServerTLS(fmt.Sprintf("127.0.0.1:%d", m.port), cfg) - if err != nil { - return err - } - return m.start(s) -} - -// StartAddr runs miniredis with a given addr. Examples: "127.0.0.1:6379", -// ":6379", or "127.0.0.1:0" -func (m *Miniredis) StartAddr(addr string) error { - s, err := server.NewServer(addr) - if err != nil { - return err - } - return m.start(s) -} - -// StartAddrTLS runs miniredis with a given addr, TLS version. -func (m *Miniredis) StartAddrTLS(addr string, cfg *tls.Config) error { - s, err := server.NewServerTLS(addr, cfg) - if err != nil { - return err - } - return m.start(s) -} - -func (m *Miniredis) start(s *server.Server) error { - m.Lock() - defer m.Unlock() - m.srv = s - m.port = s.Addr().Port - - commandsConnection(m) - commandsGeneric(m) - commandsServer(m) - commandsString(m) - commandsHash(m) - commandsList(m) - commandsPubsub(m) - commandsSet(m) - commandsSortedSet(m) - commandsStream(m) - commandsTransaction(m) - commandsScripting(m) - commandsGeo(m) - commandsCluster(m) - commandsHll(m) - commandsClient(m) - commandsObject(m) - - return nil -} - -// Restart restarts a Close()d server on the same port. Values will be -// preserved. -func (m *Miniredis) Restart() error { - return m.Start() -} - -// Close shuts down a Miniredis. -func (m *Miniredis) Close() { - m.Lock() - - if m.srv == nil { - m.Unlock() - return - } - srv := m.srv - m.srv = nil - m.CtxCancel() - m.Unlock() - - // the OnDisconnect callbacks can lock m, so run Close() outside the lock. - srv.Close() - -} - -// RequireAuth makes every connection need to AUTH first. This is the old 'AUTH [password] command. -// Remove it by setting an empty string. -func (m *Miniredis) RequireAuth(pw string) { - m.RequireUserAuth("default", pw) -} - -// Add a username/password, for use with 'AUTH [username] [password]'. -// There are currently no access controls for commands implemented. -// Disable access for the user with an empty password. -func (m *Miniredis) RequireUserAuth(username, pw string) { - m.Lock() - defer m.Unlock() - if m.passwords == nil { - m.passwords = map[string]string{} - } - if pw == "" { - delete(m.passwords, username) - return - } - m.passwords[username] = pw -} - -// DB returns a DB by ID. -func (m *Miniredis) DB(i int) *RedisDB { - m.Lock() - defer m.Unlock() - return m.db(i) -} - -// get DB. No locks! -func (m *Miniredis) db(i int) *RedisDB { - if db, ok := m.dbs[i]; ok { - return db - } - db := newRedisDB(i, m) // main miniredis has our mutex. - m.dbs[i] = &db - return &db -} - -// SwapDB swaps DBs by IDs. -func (m *Miniredis) SwapDB(i, j int) bool { - m.Lock() - defer m.Unlock() - return m.swapDB(i, j) -} - -// swap DB. No locks! -func (m *Miniredis) swapDB(i, j int) bool { - db1 := m.db(i) - db2 := m.db(j) - - db1.id = j - db2.id = i - - m.dbs[i] = db2 - m.dbs[j] = db1 - - return true -} - -// Addr returns '127.0.0.1:12345'. Can be given to a Dial(). See also Host() -// and Port(), which return the same things. -func (m *Miniredis) Addr() string { - m.Lock() - defer m.Unlock() - return m.srv.Addr().String() -} - -// Host returns the host part of Addr(). -func (m *Miniredis) Host() string { - m.Lock() - defer m.Unlock() - return m.srv.Addr().IP.String() -} - -// Port returns the (random) port part of Addr(). -func (m *Miniredis) Port() string { - m.Lock() - defer m.Unlock() - return strconv.Itoa(m.srv.Addr().Port) -} - -// CommandCount returns the number of processed commands. -func (m *Miniredis) CommandCount() int { - m.Lock() - defer m.Unlock() - return int(m.srv.TotalCommands()) -} - -// CurrentConnectionCount returns the number of currently connected clients. -func (m *Miniredis) CurrentConnectionCount() int { - m.Lock() - defer m.Unlock() - return m.srv.ClientsLen() -} - -// TotalConnectionCount returns the number of client connections since server start. -func (m *Miniredis) TotalConnectionCount() int { - m.Lock() - defer m.Unlock() - return int(m.srv.TotalConnections()) -} - -// FastForward decreases all TTLs by the given duration. All TTLs <= 0 will be -// expired. -func (m *Miniredis) FastForward(duration time.Duration) { - m.Lock() - defer m.Unlock() - for _, db := range m.dbs { - db.fastForward(duration) - } -} - -// Server returns the underlying server to allow custom commands to be implemented -func (m *Miniredis) Server() *server.Server { - return m.srv -} - -// IsReadOnlyCommand checks if a command is marked as read-only -func (m *Miniredis) IsReadOnlyCommand(cmd string) bool { - if m.srv == nil { - return false - } - return m.srv.IsReadOnlyCommand(cmd) -} - -// Dump returns a text version of the selected DB, usable for debugging. -// -// Dump limits the maximum length of each key:value to "DumpMaxLineLen" characters. -// To increase that, call something like: -// -// miniredis.DumpMaxLineLen = 1024 -// mr, _ = miniredis.Run() -// mr.Dump() -func (m *Miniredis) Dump() string { - m.Lock() - defer m.Unlock() - - var ( - maxLen = DumpMaxLineLen - indent = " " - db = m.db(m.selectedDB) - r = "" - v = func(s string) string { - suffix := "" - if len(s) > maxLen { - suffix = fmt.Sprintf("...(%d)", len(s)) - s = s[:maxLen-len(suffix)] - } - return fmt.Sprintf("%q%s", s, suffix) - } - ) - - for _, k := range db.allKeys() { - r += fmt.Sprintf("- %s\n", k) - t := db.t(k) - switch t { - case keyTypeString: - r += fmt.Sprintf("%s%s\n", indent, v(db.stringKeys[k])) - case keyTypeHash: - for _, hk := range db.hashFields(k) { - r += fmt.Sprintf("%s%s: %s\n", indent, hk, v(db.hashGet(k, hk))) - } - case keyTypeList: - for _, lk := range db.listKeys[k] { - r += fmt.Sprintf("%s%s\n", indent, v(lk)) - } - case keyTypeSet: - for _, mk := range db.setMembers(k) { - r += fmt.Sprintf("%s%s\n", indent, v(mk)) - } - case keyTypeSortedSet: - for _, el := range db.ssetElements(k) { - r += fmt.Sprintf("%s%f: %s\n", indent, el.score, v(el.member)) - } - case keyTypeStream: - for _, entry := range db.streamKeys[k].entries { - r += fmt.Sprintf("%s%s\n", indent, entry.ID) - ev := entry.Values - for i := 0; i < len(ev)/2; i++ { - r += fmt.Sprintf("%s%s%s: %s\n", indent, indent, v(ev[2*i]), v(ev[2*i+1])) - } - } - case keyTypeHll: - for _, entry := range db.hllKeys { - r += fmt.Sprintf("%s%s\n", indent, v(string(entry.Bytes()))) - } - default: - r += fmt.Sprintf("%s(a %s, fixme!)\n", indent, t) - } - } - return r -} - -// SetTime sets the time against which EXPIREAT values are compared, and the -// time used in stream entry IDs. Will use time.Now() if this is not set. -func (m *Miniredis) SetTime(t time.Time) { - m.Lock() - defer m.Unlock() - m.now = t -} - -// make every command return this message. For example: -// -// LOADING Redis is loading the dataset in memory -// MASTERDOWN Link with MASTER is down and replica-serve-stale-data is set to 'no'. -// -// Clear it with an empty string. Don't add newlines. -func (m *Miniredis) SetError(msg string) { - cb := server.Hook(nil) - if msg != "" { - cb = func(c *server.Peer, cmd string, args ...string) bool { - c.WriteError(msg) - return true - } - } - m.srv.SetPreHook(cb) -} - -type argRequirements struct { - minimum int - maximum *int -} - -func atLeast(n int) argRequirements { - return argRequirements{n, nil} -} - -func between(a int, b int) argRequirements { - return argRequirements{a, &b} -} - -func exactly(n int) argRequirements { - return argRequirements{n, &n} -} - -// isValidCMD returns true if command is valid and can be executed. -func (m *Miniredis) isValidCMD(c *server.Peer, cmd string, args []string, argReqs argRequirements) bool { - if len(args) < argReqs.minimum || (argReqs.maximum != nil && len(args) > *argReqs.maximum) { - setDirty(c) - c.WriteError(errWrongNumber(cmd)) - return false - } - - if !m.handleAuth(c) { - return false - } - if m.checkPubsub(c, cmd) { - return false - } - - return true -} - -// handleAuth returns false if connection has no access. It sends the reply. -func (m *Miniredis) handleAuth(c *server.Peer) bool { - if getCtx(c).nested { - return true - } - - m.Lock() - defer m.Unlock() - if len(m.passwords) == 0 { - return true - } - if !getCtx(c).authenticated { - c.WriteError("NOAUTH Authentication required.") - return false - } - return true -} - -// handlePubsub sends an error to the user if the connection is in PUBSUB mode. -// It'll return true if it did. -func (m *Miniredis) checkPubsub(c *server.Peer, cmd string) bool { - if getCtx(c).nested { - return false - } - - m.Lock() - defer m.Unlock() - - ctx := getCtx(c) - if ctx.subscriber == nil { - return false - } - - prefix := "ERR " - if strings.ToLower(cmd) == "exec" { - prefix = "EXECABORT Transaction discarded because of: " - } - c.WriteError(fmt.Sprintf( - "%sCan't execute '%s': only (P)SUBSCRIBE / (P)UNSUBSCRIBE / PING / QUIT are allowed in this context", - prefix, - strings.ToLower(cmd), - )) - return true -} - -func getCtx(c *server.Peer) *connCtx { - if c.Ctx == nil { - c.Ctx = &connCtx{} - } - return c.Ctx.(*connCtx) -} - -func startTx(ctx *connCtx) { - ctx.transaction = []txCmd{} - ctx.dirtyTransaction = false -} - -func stopTx(ctx *connCtx) { - ctx.transaction = nil - unwatch(ctx) -} - -func inTx(ctx *connCtx) bool { - return ctx.transaction != nil -} - -func addTxCmd(ctx *connCtx, cb txCmd) { - ctx.transaction = append(ctx.transaction, cb) -} - -func watch(db *RedisDB, ctx *connCtx, key string) { - if ctx.watch == nil { - ctx.watch = map[dbKey]uint{} - } - ctx.watch[dbKey{db: db.id, key: key}] = db.keyVersion[key] // Can be 0. -} - -func unwatch(ctx *connCtx) { - ctx.watch = nil -} - -// setDirty can be called even when not in an tx. Is an no-op then. -func setDirty(c *server.Peer) { - if c.Ctx == nil { - // No transaction. Not relevant. - return - } - getCtx(c).dirtyTransaction = true -} - -func (m *Miniredis) addSubscriber(s *Subscriber) { - m.subscribers[s] = struct{}{} -} - -// closes and remove the subscriber. -func (m *Miniredis) removeSubscriber(s *Subscriber) { - _, ok := m.subscribers[s] - delete(m.subscribers, s) - if ok { - s.Close() - } -} - -func (m *Miniredis) publish(c, msg string) int { - n := 0 - for s := range m.subscribers { - n += s.Publish(c, msg) - } - return n -} - -// enter 'subscribed state', or return the existing one. -func (m *Miniredis) subscribedState(c *server.Peer) *Subscriber { - ctx := getCtx(c) - sub := ctx.subscriber - if sub != nil { - return sub - } - - sub = newSubscriber() - m.addSubscriber(sub) - - c.OnDisconnect(func() { - m.Lock() - m.removeSubscriber(sub) - m.Unlock() - }) - - ctx.subscriber = sub - - go monitorPublish(c, sub.publish) - go monitorPpublish(c, sub.ppublish) - - return sub -} - -// whenever the p?sub count drops to 0 subscribed state should be stopped, and -// all redis commands are allowed again. -func endSubscriber(m *Miniredis, c *server.Peer) { - ctx := getCtx(c) - if sub := ctx.subscriber; sub != nil { - m.removeSubscriber(sub) // will Close() the sub - } - ctx.subscriber = nil -} - -// Start a new pubsub subscriber. It can (un) subscribe to channels and -// patterns, and has a channel to get published messages. Close it with -// Close(). -// Does not close itself when there are no subscriptions left. -func (m *Miniredis) NewSubscriber() *Subscriber { - sub := newSubscriber() - - m.Lock() - m.addSubscriber(sub) - m.Unlock() - - return sub -} - -func (m *Miniredis) allSubscribers() []*Subscriber { - var subs []*Subscriber - for s := range m.subscribers { - subs = append(subs, s) - } - return subs -} - -func (m *Miniredis) Seed(seed int) { - m.Lock() - defer m.Unlock() - - // m.rand is not safe for concurrent use. - m.rand = rand.New(rand.NewSource(int64(seed))) -} - -func (m *Miniredis) randIntn(n int) int { - if m.rand == nil { - return rand.Intn(n) - } - return m.rand.Intn(n) -} - -// shuffle shuffles a list of strings. Kinda. -func (m *Miniredis) shuffle(l []string) { - for range l { - i := m.randIntn(len(l)) - j := m.randIntn(len(l)) - l[i], l[j] = l[j], l[i] - } -} - -func (m *Miniredis) effectiveNow() time.Time { - if !m.now.IsZero() { - return m.now - } - return time.Now().UTC() -} - -// convert a unixtimestamp to a duration, to use an absolute time as TTL. -// d can be either time.Second or time.Millisecond. -func (m *Miniredis) at(i int, d time.Duration) time.Duration { - var ts time.Time - switch d { - case time.Millisecond: - ts = time.Unix(int64(i/1000), 1000000*int64(i%1000)) - case time.Second: - ts = time.Unix(int64(i), 0) - default: - panic("invalid time unit (d). Fixme!") - } - now := m.effectiveNow() - return ts.Sub(now) -} - -// copy does not mind if dst already exists. -func (m *Miniredis) copy( - srcDB *RedisDB, src string, - destDB *RedisDB, dst string, -) error { - if !srcDB.exists(src) { - return ErrKeyNotFound - } - - switch srcDB.t(src) { - case keyTypeString: - destDB.stringKeys[dst] = srcDB.stringKeys[src] - case keyTypeHash: - destDB.hashKeys[dst] = copyHashKey(srcDB.hashKeys[src]) - case keyTypeList: - destDB.listKeys[dst] = copyListKey(srcDB.listKeys[src]) - case keyTypeSet: - destDB.setKeys[dst] = copySetKey(srcDB.setKeys[src]) - case keyTypeSortedSet: - destDB.sortedsetKeys[dst] = copySortedSet(srcDB.sortedsetKeys[src]) - case keyTypeStream: - destDB.streamKeys[dst] = srcDB.streamKeys[src].copy() - case keyTypeHll: - destDB.hllKeys[dst] = srcDB.hllKeys[src].copy() - default: - panic("missing case") - } - destDB.keys[dst] = srcDB.keys[src] - destDB.incr(dst) - if v, ok := srcDB.ttl[src]; ok { - destDB.ttl[dst] = v - } - return nil -} - -func copyHashKey(orig hashKey) hashKey { - cpy := hashKey{} - for k, v := range orig { - cpy[k] = v - } - return cpy -} - -func copyListKey(orig listKey) listKey { - cpy := make(listKey, len(orig)) - copy(cpy, orig) - return cpy -} - -func copySetKey(orig setKey) setKey { - cpy := setKey{} - for k, v := range orig { - cpy[k] = v - } - return cpy -} - -func copySortedSet(orig sortedSet) sortedSet { - cpy := sortedSet{} - for k, v := range orig { - cpy[k] = v - } - return cpy -} diff --git a/vendor/github.com/alicebob/miniredis/v2/opts.go b/vendor/github.com/alicebob/miniredis/v2/opts.go deleted file mode 100644 index 5b29c78c2..000000000 --- a/vendor/github.com/alicebob/miniredis/v2/opts.go +++ /dev/null @@ -1,60 +0,0 @@ -package miniredis - -import ( - "errors" - "math" - "strconv" - "time" - - "github.com/alicebob/miniredis/v2/server" -) - -// optInt parses an int option in a command. -// Writes "invalid integer" error to c if it's not a valid integer. Returns -// whether or not things were okay. -func optInt(c *server.Peer, src string, dest *int) bool { - return optIntErr(c, src, dest, msgInvalidInt) -} - -func optIntErr(c *server.Peer, src string, dest *int, errMsg string) bool { - n, err := strconv.Atoi(src) - if err != nil { - setDirty(c) - c.WriteError(errMsg) - return false - } - *dest = n - return true -} - -// optIntSimple sets dest or returns an error -func optIntSimple(src string, dest *int) error { - n, err := strconv.Atoi(src) - if err != nil { - return errors.New(msgInvalidInt) - } - *dest = n - return nil -} - -func optDuration(c *server.Peer, src string, dest *time.Duration) bool { - n, err := strconv.ParseFloat(src, 64) - if err != nil { - setDirty(c) - c.WriteError(msgInvalidTimeout) - return false - } - if n < 0 { - setDirty(c) - c.WriteError(msgTimeoutNegative) - return false - } - if math.IsInf(n, 0) { - setDirty(c) - c.WriteError(msgTimeoutIsOutOfRange) - return false - } - - *dest = time.Duration(n*1_000_000) * time.Microsecond - return true -} diff --git a/vendor/github.com/alicebob/miniredis/v2/proto/Makefile b/vendor/github.com/alicebob/miniredis/v2/proto/Makefile deleted file mode 100644 index b9ef39496..000000000 --- a/vendor/github.com/alicebob/miniredis/v2/proto/Makefile +++ /dev/null @@ -1,2 +0,0 @@ -test: - go test diff --git a/vendor/github.com/alicebob/miniredis/v2/proto/client.go b/vendor/github.com/alicebob/miniredis/v2/proto/client.go deleted file mode 100644 index 92f57baf1..000000000 --- a/vendor/github.com/alicebob/miniredis/v2/proto/client.go +++ /dev/null @@ -1,60 +0,0 @@ -package proto - -import ( - "bufio" - "crypto/tls" - "net" -) - -type Client struct { - c net.Conn - r *bufio.Reader -} - -func Dial(addr string) (*Client, error) { - c, err := net.Dial("tcp", addr) - if err != nil { - return nil, err - } - - return &Client{ - c: c, - r: bufio.NewReader(c), - }, nil -} - -func DialTLS(addr string, cfg *tls.Config) (*Client, error) { - c, err := tls.Dial("tcp", addr, cfg) - if err != nil { - return nil, err - } - - return &Client{ - c: c, - r: bufio.NewReader(c), - }, nil -} - -func (c *Client) Close() error { - return c.c.Close() -} - -func (c *Client) Do(cmd ...string) (string, error) { - if err := Write(c.c, cmd); err != nil { - return "", err - } - return Read(c.r) -} - -func (c *Client) Read() (string, error) { - return Read(c.r) -} - -// Do() + ReadStrings() -func (c *Client) DoStrings(cmd ...string) ([]string, error) { - res, err := c.Do(cmd...) - if err != nil { - return nil, err - } - return ReadStrings(res) -} diff --git a/vendor/github.com/alicebob/miniredis/v2/proto/proto.go b/vendor/github.com/alicebob/miniredis/v2/proto/proto.go deleted file mode 100644 index e378faf18..000000000 --- a/vendor/github.com/alicebob/miniredis/v2/proto/proto.go +++ /dev/null @@ -1,288 +0,0 @@ -package proto - -import ( - "bufio" - "errors" - "fmt" - "io" - "strconv" - "strings" -) - -var ( - ErrProtocol = errors.New("unsupported protocol") - ErrUnexpected = errors.New("not what you asked for") -) - -func readLine(r *bufio.Reader) (string, error) { - line, err := r.ReadString('\n') - if err != nil { - return "", err - } - if len(line) < 3 { - return "", ErrProtocol - } - return line, nil -} - -// Read an array, with all elements are the raw redis commands -// Also reads sets and maps. -func ReadArray(b string) ([]string, error) { - r := bufio.NewReader(strings.NewReader(b)) - line, err := readLine(r) - if err != nil { - return nil, err - } - - elems := 0 - switch line[0] { - default: - return nil, ErrUnexpected - case '*', '>', '~': - // *: array - // >: push data - // ~: set - length, err := strconv.Atoi(line[1 : len(line)-2]) - if err != nil { - return nil, err - } - elems = length - case '%': - // we also read maps. - length, err := strconv.Atoi(line[1 : len(line)-2]) - if err != nil { - return nil, err - } - elems = length * 2 - } - - var res []string - for i := 0; i < elems; i++ { - next, err := Read(r) - if err != nil { - return nil, err - } - res = append(res, next) - } - return res, nil -} - -func ReadString(b string) (string, error) { - r := bufio.NewReader(strings.NewReader(b)) - line, err := readLine(r) - if err != nil { - return "", err - } - - switch line[0] { - default: - return "", ErrUnexpected - case '$': - // bulk strings are: `$5\r\nhello\r\n` - length, err := strconv.Atoi(line[1 : len(line)-2]) - if err != nil { - return "", err - } - if length < 0 { - // -1 is a nil response - return line, nil - } - var ( - buf = make([]byte, length+2) - pos = 0 - ) - for pos < length+2 { - n, err := r.Read(buf[pos:]) - if err != nil { - return "", err - } - pos += n - } - return string(buf[:len(buf)-2]), nil - } -} - -func readInline(b string) (string, error) { - if len(b) < 3 { - return "", ErrUnexpected - } - return b[1 : len(b)-2], nil -} - -func ReadError(b string) (string, error) { - if len(b) < 1 { - return "", ErrUnexpected - } - - switch b[0] { - default: - return "", ErrUnexpected - case '-': - return readInline(b) - } -} - -func ReadStrings(b string) ([]string, error) { - elems, err := ReadArray(b) - if err != nil { - return nil, err - } - var res []string - for _, e := range elems { - s, err := ReadString(e) - if err != nil { - return nil, err - } - res = append(res, s) - } - return res, nil -} - -// Read a single command, returning it raw. Used to read replies from redis. -// Understands RESP3 proto. -func Read(r *bufio.Reader) (string, error) { - line, err := readLine(r) - if err != nil { - return "", err - } - - switch line[0] { - default: - return "", ErrProtocol - case '+', '-', ':', ',', '_': - // +: inline string - // -: errors - // :: integer - // ,: float - // _: null - // Simple line based replies. - return line, nil - case '$': - // bulk strings are: `$5\r\nhello\r\n` - length, err := strconv.Atoi(line[1 : len(line)-2]) - if err != nil { - return "", err - } - if length < 0 { - // -1 is a nil response - return line, nil - } - var ( - buf = make([]byte, length+2) - pos = 0 - ) - for pos < length+2 { - n, err := r.Read(buf[pos:]) - if err != nil { - return "", err - } - pos += n - } - return line + string(buf), nil - case '*', '>', '~': - // arrays are: `*6\r\n...` - // pushdata is: `>6\r\n...` - // sets are: `~6\r\n...` - length, err := strconv.Atoi(line[1 : len(line)-2]) - if err != nil { - return "", err - } - for i := 0; i < length; i++ { - next, err := Read(r) - if err != nil { - return "", err - } - line += next - } - return line, nil - case '%': - // maps are: `%3\r\n...` - length, err := strconv.Atoi(line[1 : len(line)-2]) - if err != nil { - return "", err - } - for i := 0; i < length*2; i++ { - next, err := Read(r) - if err != nil { - return "", err - } - line += next - } - return line, nil - } -} - -// Write a command in RESP3 proto. Used to write commands to redis. -// Currently only supports string arrays. -func Write(w io.Writer, cmd []string) error { - if _, err := fmt.Fprintf(w, "*%d\r\n", len(cmd)); err != nil { - return err - } - for _, c := range cmd { - if _, err := fmt.Fprintf(w, "$%d\r\n%s\r\n", len(c), c); err != nil { - return err - } - } - return nil -} - -// Parse into interfaces. `b` must contain exactly a single command (which can be nested). -func Parse(b string) (interface{}, error) { - if len(b) < 1 { - return nil, ErrUnexpected - } - - switch b[0] { - default: - return "", ErrProtocol - case '+': - return readInline(b) - case '-': - e, err := readInline(b) - if err != nil { - return nil, err - } - return errors.New(e), nil - case ':': - e, err := readInline(b) - if err != nil { - return nil, err - } - return strconv.Atoi(e) - case '$': - return ReadString(b) - case '*': - elems, err := ReadArray(b) - if err != nil { - return nil, err - } - var res []interface{} - for _, elem := range elems { - e, err := Parse(elem) - if err != nil { - return nil, err - } - res = append(res, e) - } - return res, nil - case '%': - elems, err := ReadArray(b) - if err != nil { - return nil, err - } - var res = map[interface{}]interface{}{} - for len(elems) > 1 { - key, err := Parse(elems[0]) - if err != nil { - return nil, err - } - value, err := Parse(elems[1]) - if err != nil { - return nil, err - } - res[key] = value - elems = elems[2:] - } - return res, nil - } -} diff --git a/vendor/github.com/alicebob/miniredis/v2/proto/types.go b/vendor/github.com/alicebob/miniredis/v2/proto/types.go deleted file mode 100644 index 0b3b7c9af..000000000 --- a/vendor/github.com/alicebob/miniredis/v2/proto/types.go +++ /dev/null @@ -1,102 +0,0 @@ -package proto - -import ( - "fmt" - "strings" -) - -// Byte-safe string -func String(s string) string { - return fmt.Sprintf("$%d\r\n%s\r\n", len(s), s) -} - -// Inline string -func Inline(s string) string { - return inline('+', s) -} - -// Error -func Error(s string) string { - return inline('-', s) -} - -func inline(r rune, s string) string { - return fmt.Sprintf("%s%s\r\n", string(r), s) -} - -// Int -func Int(n int) string { - return fmt.Sprintf(":%d\r\n", n) -} - -// Float -func Float(n float64) string { - return fmt.Sprintf(",%g\r\n", n) -} - -const ( - Nil = "$-1\r\n" - NilResp3 = "_\r\n" - NilList = "*-1\r\n" -) - -// Array assembles the args in a list. Args should be raw redis commands. -// Example: Array(String("foo"), String("bar")) -func Array(args ...string) string { - return fmt.Sprintf("*%d\r\n", len(args)) + strings.Join(args, "") -} - -// Push assembles the args for push-data. Args should be raw redis commands. -// Example: Push(String("foo"), String("bar")) -func Push(args ...string) string { - return fmt.Sprintf(">%d\r\n", len(args)) + strings.Join(args, "") -} - -// Strings is a helper to build 1 dimensional string arrays. -func Strings(args ...string) string { - var strings []string - for _, a := range args { - strings = append(strings, String(a)) - } - return Array(strings...) -} - -// Ints is a helper to build 1 dimensional int arrays. -func Ints(args ...int) string { - var ints []string - for _, a := range args { - ints = append(ints, Int(a)) - } - return Array(ints...) -} - -// Map assembles the args in a map. Args should be raw redis commands. -// Must be an even number of arguments. -// Example: Map(String("foo"), String("bar")) -func Map(args ...string) string { - return fmt.Sprintf("%%%d\r\n", len(args)/2) + strings.Join(args, "") -} - -// StringMap is is a wrapper to get a map of (bulk)strings. -func StringMap(args ...string) string { - var strings []string - for _, a := range args { - strings = append(strings, String(a)) - } - return Map(strings...) -} - -// Set assembles the args in a map. Args should be raw redis commands. -// Example: Set(String("foo"), String("bar")) -func Set(args ...string) string { - return fmt.Sprintf("~%d\r\n", len(args)) + strings.Join(args, "") -} - -// StringSet is is a wrapper to get a set of (bulk)strings. -func StringSet(args ...string) string { - var strings []string - for _, a := range args { - strings = append(strings, String(a)) - } - return Set(strings...) -} diff --git a/vendor/github.com/alicebob/miniredis/v2/pubsub.go b/vendor/github.com/alicebob/miniredis/v2/pubsub.go deleted file mode 100644 index bb31f80a8..000000000 --- a/vendor/github.com/alicebob/miniredis/v2/pubsub.go +++ /dev/null @@ -1,240 +0,0 @@ -package miniredis - -import ( - "regexp" - "sort" - "sync" - - "github.com/alicebob/miniredis/v2/server" -) - -// PubsubMessage is what gets broadcasted over pubsub channels. -type PubsubMessage struct { - Channel string - Message string -} - -type PubsubPmessage struct { - Pattern string - Channel string - Message string -} - -// Subscriber has the (p)subscriptions. -type Subscriber struct { - publish chan PubsubMessage - ppublish chan PubsubPmessage - channels map[string]struct{} - patterns map[string]*regexp.Regexp - mu sync.Mutex -} - -// Make a new subscriber. The channel is not buffered, so you will need to keep -// reading using Messages(). Use Close() when done, or unsubscribe. -func newSubscriber() *Subscriber { - return &Subscriber{ - publish: make(chan PubsubMessage), - ppublish: make(chan PubsubPmessage), - channels: map[string]struct{}{}, - patterns: map[string]*regexp.Regexp{}, - } -} - -// Close the listening channel -func (s *Subscriber) Close() { - close(s.publish) - close(s.ppublish) -} - -// Count the total number of channels and patterns -func (s *Subscriber) Count() int { - s.mu.Lock() - defer s.mu.Unlock() - return s.count() -} - -func (s *Subscriber) count() int { - return len(s.channels) + len(s.patterns) -} - -// Subscribe to a channel. Returns the total number of (p)subscriptions after -// subscribing. -func (s *Subscriber) Subscribe(c string) int { - s.mu.Lock() - defer s.mu.Unlock() - - s.channels[c] = struct{}{} - return s.count() -} - -// Unsubscribe a channel. Returns the total number of (p)subscriptions after -// unsubscribing. -func (s *Subscriber) Unsubscribe(c string) int { - s.mu.Lock() - defer s.mu.Unlock() - - delete(s.channels, c) - return s.count() -} - -// Subscribe to a pattern. Returns the total number of (p)subscriptions after -// subscribing. -func (s *Subscriber) Psubscribe(pat string) int { - s.mu.Lock() - defer s.mu.Unlock() - - s.patterns[pat] = patternRE(pat) - return s.count() -} - -// Unsubscribe a pattern. Returns the total number of (p)subscriptions after -// unsubscribing. -func (s *Subscriber) Punsubscribe(pat string) int { - s.mu.Lock() - defer s.mu.Unlock() - - delete(s.patterns, pat) - return s.count() -} - -// List all subscribed channels, in alphabetical order -func (s *Subscriber) Channels() []string { - s.mu.Lock() - defer s.mu.Unlock() - - var cs []string - for c := range s.channels { - cs = append(cs, c) - } - sort.Strings(cs) - return cs -} - -// List all subscribed patterns, in alphabetical order -func (s *Subscriber) Patterns() []string { - s.mu.Lock() - defer s.mu.Unlock() - - var ps []string - for p := range s.patterns { - ps = append(ps, p) - } - sort.Strings(ps) - return ps -} - -// Publish a message. Will return return how often we sent the message (can be -// a match for a subscription and for a psubscription. -func (s *Subscriber) Publish(c, msg string) int { - s.mu.Lock() - defer s.mu.Unlock() - - found := 0 - -subs: - for sub := range s.channels { - if sub == c { - s.publish <- PubsubMessage{c, msg} - found++ - break subs - } - } - -pats: - for orig, pat := range s.patterns { - if pat != nil && pat.MatchString(c) { - s.ppublish <- PubsubPmessage{orig, c, msg} - found++ - break pats - } - } - - return found -} - -// The channel to read messages for this subscriber. Only for messages matching -// a SUBSCRIBE. -func (s *Subscriber) Messages() <-chan PubsubMessage { - return s.publish -} - -// The channel to read messages for this subscriber. Only for messages matching -// a PSUBSCRIBE. -func (s *Subscriber) Pmessages() <-chan PubsubPmessage { - return s.ppublish -} - -// List all pubsub channels. If `pat` isn't empty channels names must match the -// pattern. Channels are returned alphabetically. -func activeChannels(subs []*Subscriber, pat string) []string { - channels := map[string]struct{}{} - for _, s := range subs { - for c := range s.channels { - channels[c] = struct{}{} - } - } - - var cpat *regexp.Regexp - if pat != "" { - cpat = patternRE(pat) - } - - var cs []string - for k := range channels { - if cpat != nil && !cpat.MatchString(k) { - continue - } - cs = append(cs, k) - } - sort.Strings(cs) - return cs -} - -// Count all subscribed (not psubscribed) clients for the given channel -// pattern. Channels are returned alphabetically. -func countSubs(subs []*Subscriber, channel string) int { - n := 0 - for _, p := range subs { - for c := range p.channels { - if c == channel { - n++ - break - } - } - } - return n -} - -// Count the total of all client psubscriptions. -func countPsubs(subs []*Subscriber) int { - n := 0 - for _, p := range subs { - n += len(p.patterns) - } - return n -} - -func monitorPublish(conn *server.Peer, msgs <-chan PubsubMessage) { - for msg := range msgs { - conn.Block(func(c *server.Writer) { - c.WritePushLen(3) - c.WriteBulk("message") - c.WriteBulk(msg.Channel) - c.WriteBulk(msg.Message) - c.Flush() - }) - } -} - -func monitorPpublish(conn *server.Peer, msgs <-chan PubsubPmessage) { - for msg := range msgs { - conn.Block(func(c *server.Writer) { - c.WritePushLen(4) - c.WriteBulk("pmessage") - c.WriteBulk(msg.Pattern) - c.WriteBulk(msg.Channel) - c.WriteBulk(msg.Message) - c.Flush() - }) - } -} diff --git a/vendor/github.com/alicebob/miniredis/v2/redis.go b/vendor/github.com/alicebob/miniredis/v2/redis.go deleted file mode 100644 index eae0e2ffb..000000000 --- a/vendor/github.com/alicebob/miniredis/v2/redis.go +++ /dev/null @@ -1,269 +0,0 @@ -package miniredis - -import ( - "context" - "fmt" - "math" - "math/big" - "strings" - "sync" - "time" - - "github.com/alicebob/miniredis/v2/server" -) - -const ( - keyTypeString = "string" - keyTypeHash = "hash" - keyTypeList = "list" - keyTypeSet = "set" - keyTypeHll = "hll" - keyTypeSortedSet = "zset" - keyTypeStream = "stream" -) - -const ( - msgWrongType = "WRONGTYPE Operation against a key holding the wrong kind of value" - msgNotValidHllValue = "WRONGTYPE Key is not a valid HyperLogLog string value." - msgInvalidInt = "ERR value is not an integer or out of range" - msgIntOverflow = "ERR increment or decrement would overflow" - msgInvalidFloat = "ERR value is not a valid float" - msgInvalidMinMax = "ERR min or max is not a float" - msgInvalidRangeItem = "ERR min or max not valid string range item" - msgInvalidTimeout = "ERR timeout is not a float or out of range" - msgInvalidRange = "ERR value is out of range, must be positive" - msgSyntaxError = "ERR syntax error" - msgKeyNotFound = "ERR no such key" - msgOutOfRange = "ERR index out of range" - msgInvalidCursor = "ERR invalid cursor" - msgXXandNX = "ERR XX and NX options at the same time are not compatible" - msgTimeoutNegative = "ERR timeout is negative" - msgTimeoutIsOutOfRange = "ERR timeout is out of range" - msgInvalidSETime = "ERR invalid expire time in set" - msgInvalidSETEXTime = "ERR invalid expire time in setex" - msgInvalidPSETEXTime = "ERR invalid expire time in psetex" - msgInvalidKeysNumber = "ERR Number of keys can't be greater than number of args" - msgNegativeKeysNumber = "ERR Number of keys can't be negative" - msgFScriptUsage = "ERR unknown subcommand or wrong number of arguments for '%s'. Try SCRIPT HELP." - msgFScriptUsageSimple = "ERR unknown subcommand '%s'. Try SCRIPT HELP." - msgFPubsubUsage = "ERR unknown subcommand or wrong number of arguments for '%s'. Try PUBSUB HELP." - msgFPubsubUsageSimple = "ERR unknown subcommand '%s'. Try PUBSUB HELP." - msgFObjectUsage = "ERR unknown subcommand '%s'. Try OBJECT HELP." - msgScriptFlush = "ERR SCRIPT FLUSH only support SYNC|ASYNC option" - msgSingleElementPair = "ERR INCR option supports a single increment-element pair" - msgGTLTandNX = "ERR GT, LT, and/or NX options at the same time are not compatible" - msgInvalidStreamID = "ERR Invalid stream ID specified as stream command argument" - msgStreamIDTooSmall = "ERR The ID specified in XADD is equal or smaller than the target stream top item" - msgStreamIDZero = "ERR The ID specified in XADD must be greater than 0-0" - msgNoScriptFound = "NOSCRIPT No matching script. Please use EVAL." - msgUnsupportedUnit = "ERR unsupported unit provided. please use M, KM, FT, MI" - msgXreadUnbalanced = "ERR Unbalanced 'xread' list of streams: for each stream key an ID or '$' must be specified." - msgXgroupKeyNotFound = "ERR The XGROUP subcommand requires the key to exist. Note that for CREATE you may want to use the MKSTREAM option to create an empty stream automatically." - msgXtrimInvalidStrategy = "ERR unsupported XTRIM strategy. Please use MAXLEN, MINID" - msgXtrimInvalidMaxLen = "ERR value is not an integer or out of range" - msgXtrimInvalidLimit = "ERR syntax error, LIMIT cannot be used without the special ~ option" - msgDBIndexOutOfRange = "ERR DB index is out of range" - msgLimitCombination = "ERR syntax error, LIMIT is only supported in combination with either BYSCORE or BYLEX" - msgRankIsZero = "ERR RANK can't be zero: use 1 to start from the first match, 2 from the second ... or use negative to start from the end of the list" - msgCountIsNegative = "ERR COUNT can't be negative" - msgMaxLengthIsNegative = "ERR MAXLEN can't be negative" - msgLimitIsNegative = "ERR LIMIT can't be negative" - msgMemorySubcommand = "ERR unknown subcommand '%s'. Try MEMORY HELP." - msgNumFieldsParameter = "ERR The `numfields` parameter must match the number of arguments" - msgNumFieldsInvalid = "ERR Parameter `numFields` should be greater than 0" - msgMandatoryArgument = "ERR Mandatory argument %s is missing or not at the right position" - msgGTandLT = "ERR GT and LT options at the same time are not compatible" - msgNXandXXGTLT = "ERR NX and XX, GT or LT options at the same time are not compatible" -) - -func errWrongNumber(cmd string) string { - return fmt.Sprintf("ERR wrong number of arguments for '%s' command", strings.ToLower(cmd)) -} - -func errLuaParseError(err error) string { - return fmt.Sprintf("ERR Error compiling script (new function): %s", err.Error()) -} - -func errReadgroup(key, group string) error { - return fmt.Errorf("NOGROUP No such key '%s' or consumer group '%s'", key, group) -} - -func errXreadgroup(key, group string) error { - return fmt.Errorf("NOGROUP No such key '%s' or consumer group '%s' in XREADGROUP with GROUP option", key, group) -} - -func msgNotFromScripts(sha string) string { - return fmt.Sprintf("This Redis command is not allowed from script script: %s, &c", sha) -} - -// withTx wraps the non-argument-checking part of command handling code in -// transaction logic. -func withTx( - m *Miniredis, - c *server.Peer, - cb txCmd, -) { - ctx := getCtx(c) - - if ctx.nested { - // this is a call via Lua's .call(). It's already locked. - cb(c, ctx) - m.signal.Broadcast() - return - } - - if inTx(ctx) { - addTxCmd(ctx, cb) - c.WriteInline("QUEUED") - return - } - m.Lock() - cb(c, ctx) - // done, wake up anyone who waits on anything. - m.signal.Broadcast() - m.Unlock() -} - -// blockCmd is executed returns whether it is done -type blockCmd func(*server.Peer, *connCtx) bool - -// blocking keeps trying a command until the callback returns true. Calls -// onTimeout after the timeout (or when we call this in a transaction). -func blocking( - m *Miniredis, - c *server.Peer, - timeout time.Duration, - cb blockCmd, - onTimeout func(*server.Peer), -) { - var ( - ctx = getCtx(c) - ) - if inTx(ctx) { - addTxCmd(ctx, func(c *server.Peer, ctx *connCtx) { - if !cb(c, ctx) { - onTimeout(c) - } - }) - c.WriteInline("QUEUED") - return - } - - localCtx, cancel := context.WithCancel(m.Ctx) - defer cancel() - timedOut := false - if timeout != 0 { - go setCondTimer(localCtx, m.signal, &timedOut, timeout) - } - go func() { - <-localCtx.Done() - m.signal.Broadcast() // main loop might miss this signal - }() - - if !ctx.nested { - // this is a call via Lua's .call(). It's already locked. - m.Lock() - defer m.Unlock() - } - for { - if c.Closed() { - return - } - - if m.Ctx.Err() != nil { - return - } - - done := cb(c, ctx) - if done { - return - } - - if timedOut { - onTimeout(c) - return - } - - m.signal.Wait() - } -} - -func setCondTimer(ctx context.Context, sig *sync.Cond, timedOut *bool, timeout time.Duration) { - dl := time.NewTimer(timeout) - defer dl.Stop() - select { - case <-dl.C: - sig.L.Lock() // for timedOut - *timedOut = true - sig.Broadcast() // main loop might miss this signal - sig.L.Unlock() - case <-ctx.Done(): - } -} - -// formatBig formats a float the way redis does -func formatBig(v *big.Float) string { - // Format with %f and strip trailing 0s. - if v.IsInf() { - return "inf" - } - // if math.IsInf(v, -1) { - // return "-inf" - // } - return stripZeros(fmt.Sprintf("%.17f", v)) -} - -func stripZeros(sv string) string { - for strings.Contains(sv, ".") { - if sv[len(sv)-1] != '0' { - break - } - // Remove trailing 0s. - sv = sv[:len(sv)-1] - // Ends with a '.'. - if sv[len(sv)-1] == '.' { - sv = sv[:len(sv)-1] - break - } - } - return sv -} - -// redisRange gives Go offsets for something l long with start/end in -// Redis semantics. Both start and end can be negative. -// Used for string range and list range things. -// The results can be used as: v[start:end] -// Note that GETRANGE (on a string key) never returns an empty string when end -// is a large negative number. -func redisRange(l, start, end int, stringSymantics bool) (int, int) { - if start < 0 { - start = l + start - if start < 0 { - start = 0 - } - } - if start > l { - start = l - } - - if end < 0 { - end = l + end - if end < 0 { - end = -1 - if stringSymantics { - end = 0 - } - } - } - if end < math.MaxInt32 { - end++ // end argument is inclusive in Redis. - } - if end > l { - end = l - } - - if end < start { - return 0, 0 - } - return start, end -} diff --git a/vendor/github.com/alicebob/miniredis/v2/server/Makefile b/vendor/github.com/alicebob/miniredis/v2/server/Makefile deleted file mode 100644 index c82e336f9..000000000 --- a/vendor/github.com/alicebob/miniredis/v2/server/Makefile +++ /dev/null @@ -1,9 +0,0 @@ -.PHONY: all build test - -all: build test - -build: - go build - -test: - go test diff --git a/vendor/github.com/alicebob/miniredis/v2/server/cmdmeta.go b/vendor/github.com/alicebob/miniredis/v2/server/cmdmeta.go deleted file mode 100644 index a1be7e151..000000000 --- a/vendor/github.com/alicebob/miniredis/v2/server/cmdmeta.go +++ /dev/null @@ -1,17 +0,0 @@ -package server - -// cmdMeta holds metadata about a registered command -type cmdMeta struct { - handler Cmd - readOnly bool -} - -// CmdOption is a function that configures command metadata -type CmdOption func(*cmdMeta) - -// ReadOnlyOption marks a command as read-only -func ReadOnlyOption() CmdOption { - return func(meta *cmdMeta) { - meta.readOnly = true - } -} diff --git a/vendor/github.com/alicebob/miniredis/v2/server/proto.go b/vendor/github.com/alicebob/miniredis/v2/server/proto.go deleted file mode 100644 index f62e1d73f..000000000 --- a/vendor/github.com/alicebob/miniredis/v2/server/proto.go +++ /dev/null @@ -1,157 +0,0 @@ -package server - -import ( - "bufio" - "errors" - "strconv" -) - -type Simple string - -// ErrProtocol is the general error for unexpected input -var ErrProtocol = errors.New("invalid request") - -// client always sends arrays with bulk strings -func readArray(rd *bufio.Reader) ([]string, error) { - line, err := rd.ReadString('\n') - if err != nil { - return nil, err - } - if len(line) < 3 { - return nil, ErrProtocol - } - - switch line[0] { - default: - return nil, ErrProtocol - case '*': - l, err := strconv.Atoi(line[1 : len(line)-2]) - if err != nil { - return nil, err - } - // l can be -1 - var fields []string - for ; l > 0; l-- { - s, err := readString(rd) - if err != nil { - return nil, err - } - fields = append(fields, s) - } - return fields, nil - } -} - -func readString(rd *bufio.Reader) (string, error) { - line, err := rd.ReadString('\n') - if err != nil { - return "", err - } - if len(line) < 3 { - return "", ErrProtocol - } - - switch line[0] { - default: - return "", ErrProtocol - case '+', '-', ':': - // +: simple string - // -: errors - // :: integer - // Simple line based replies. - return string(line[1 : len(line)-2]), nil - case '$': - // bulk strings are: `$5\r\nhello\r\n` - length, err := strconv.Atoi(line[1 : len(line)-2]) - if err != nil { - return "", err - } - if length < 0 { - // -1 is a nil response - return "", nil - } - var ( - buf = make([]byte, length+2) - pos = 0 - ) - for pos < length+2 { - n, err := rd.Read(buf[pos:]) - if err != nil { - return "", err - } - pos += n - } - return string(buf[:length]), nil - } -} - -// parse a reply -func ParseReply(rd *bufio.Reader) (interface{}, error) { - line, err := rd.ReadString('\n') - if err != nil { - return nil, err - } - if len(line) < 3 { - return nil, ErrProtocol - } - - switch line[0] { - default: - return nil, ErrProtocol - case '+': - // +: simple string - return Simple(line[1 : len(line)-2]), nil - case '-': - // -: errors - return nil, errors.New(string(line[1 : len(line)-2])) - case ':': - // :: integer - v := line[1 : len(line)-2] - if v == "" { - return 0, nil - } - n, err := strconv.Atoi(v) - if err != nil { - return nil, ErrProtocol - } - return n, nil - case '$': - // bulk strings are: `$5\r\nhello\r\n` - length, err := strconv.Atoi(line[1 : len(line)-2]) - if err != nil { - return "", err - } - if length < 0 { - // -1 is a nil response - return nil, nil - } - var ( - buf = make([]byte, length+2) - pos = 0 - ) - for pos < length+2 { - n, err := rd.Read(buf[pos:]) - if err != nil { - return "", err - } - pos += n - } - return string(buf[:length]), nil - case '*': - // array - l, err := strconv.Atoi(line[1 : len(line)-2]) - if err != nil { - return nil, ErrProtocol - } - // l can be -1 - var fields []interface{} - for ; l > 0; l-- { - s, err := ParseReply(rd) - if err != nil { - return nil, err - } - fields = append(fields, s) - } - return fields, nil - } -} diff --git a/vendor/github.com/alicebob/miniredis/v2/server/server.go b/vendor/github.com/alicebob/miniredis/v2/server/server.go deleted file mode 100644 index af36105f4..000000000 --- a/vendor/github.com/alicebob/miniredis/v2/server/server.go +++ /dev/null @@ -1,519 +0,0 @@ -package server - -import ( - "bufio" - "crypto/tls" - "fmt" - "net" - "strings" - "sync" - "unicode" - - "github.com/alicebob/miniredis/v2/fpconv" -) - -func errUnknownCommand(cmd string, args []string) string { - s := fmt.Sprintf("ERR unknown command `%s`, with args beginning with: ", cmd) - if len(args) > 20 { - args = args[:20] - } - for _, a := range args { - s += fmt.Sprintf("`%s`, ", a) - } - return s -} - -// Cmd is what Register expects -type Cmd func(c *Peer, cmd string, args []string) - -type DisconnectHandler func(c *Peer) - -// Hook is can be added to run before every cmd. Return true if the command is done. -type Hook func(*Peer, string, ...string) bool - -// Server is a simple redis server -type Server struct { - l net.Listener - cmds map[string]*cmdMeta - preHook Hook - peers map[net.Conn]struct{} - mu sync.Mutex - wg sync.WaitGroup - infoConns int - infoCmds int -} - -// NewServer makes a server listening on addr. Close with .Close(). -func NewServer(addr string) (*Server, error) { - l, err := net.Listen("tcp", addr) - if err != nil { - return nil, err - } - return newServer(l), nil -} - -func NewServerTLS(addr string, cfg *tls.Config) (*Server, error) { - l, err := tls.Listen("tcp", addr, cfg) - if err != nil { - return nil, err - } - return newServer(l), nil -} - -func newServer(l net.Listener) *Server { - s := Server{ - cmds: map[string]*cmdMeta{}, - peers: map[net.Conn]struct{}{}, - l: l, - } - - s.wg.Add(1) - go func() { - defer s.wg.Done() - s.serve(l) - - s.mu.Lock() - for c := range s.peers { - c.Close() - } - s.mu.Unlock() - }() - return &s -} - -// (un)set a hook which is ran before every call. It returns true if the command is done. -func (s *Server) SetPreHook(h Hook) { - s.mu.Lock() - s.preHook = h - s.mu.Unlock() -} - -func (s *Server) serve(l net.Listener) { - for { - conn, err := l.Accept() - if err != nil { - return - } - s.ServeConn(conn) - } -} - -// ServeConn handles a net.Conn. Nice with net.Pipe() -func (s *Server) ServeConn(conn net.Conn) { - s.wg.Add(1) - s.mu.Lock() - s.peers[conn] = struct{}{} - s.infoConns++ - s.mu.Unlock() - - go func() { - defer s.wg.Done() - defer conn.Close() - - s.servePeer(conn) - - s.mu.Lock() - delete(s.peers, conn) - s.mu.Unlock() - }() -} - -// Addr has the net.Addr struct -func (s *Server) Addr() *net.TCPAddr { - s.mu.Lock() - defer s.mu.Unlock() - if s.l == nil { - return nil - } - return s.l.Addr().(*net.TCPAddr) -} - -// Close a server started with NewServer. It will wait until all clients are -// closed. -func (s *Server) Close() { - s.mu.Lock() - if s.l != nil { - s.l.Close() - } - s.l = nil - s.mu.Unlock() - - s.wg.Wait() -} - -// Register a command. It can't have been registered before. Safe to call on a -// running server. -func (s *Server) Register(cmd string, f Cmd, options ...CmdOption) error { - s.mu.Lock() - defer s.mu.Unlock() - cmd = strings.ToUpper(cmd) - if _, ok := s.cmds[cmd]; ok { - return fmt.Errorf("command already registered: %s", cmd) - } - - meta := &cmdMeta{ - handler: f, - readOnly: false, - } - for _, option := range options { - option(meta) - } - s.cmds[cmd] = meta - - return nil -} - -func (s *Server) servePeer(c net.Conn) { - r := bufio.NewReader(c) - peer := &Peer{ - w: bufio.NewWriter(c), - } - - defer func() { - for _, f := range peer.onDisconnect { - f() - } - }() - - readCh := make(chan []string) - - go func() { - defer close(readCh) - - for { - args, err := readArray(r) - if err != nil { - peer.Close() - return - } - - readCh <- args - } - }() - - for args := range readCh { - s.Dispatch(peer, args) - peer.Flush() - - if peer.Closed() { - c.Close() - } - } -} - -func (s *Server) Dispatch(c *Peer, args []string) { - cmd, args := args[0], args[1:] - cmdUp := strings.ToUpper(cmd) - s.mu.Lock() - h := s.preHook - s.mu.Unlock() - if h != nil { - if h(c, cmdUp, args...) { - return - } - } - - s.mu.Lock() - cmdMeta, ok := s.cmds[cmdUp] - s.mu.Unlock() - if !ok { - c.WriteError(errUnknownCommand(cmd, args)) - return - } - - s.mu.Lock() - s.infoCmds++ - s.mu.Unlock() - cmdMeta.handler(c, cmdUp, args) - if c.SwitchResp3 != nil { - c.Resp3 = *c.SwitchResp3 - c.SwitchResp3 = nil - } -} - -// TotalCommands is total (known) commands since this the server started -func (s *Server) TotalCommands() int { - s.mu.Lock() - defer s.mu.Unlock() - return s.infoCmds -} - -// IsRegisteredCommand checks if a command is registered -func (s *Server) IsRegisteredCommand(cmd string) bool { - s.mu.Lock() - defer s.mu.Unlock() - cmdUp := strings.ToUpper(cmd) - _, ok := s.cmds[cmdUp] - return ok -} - -// IsReadOnlyCommand checks if a command is marked as read-only -func (s *Server) IsReadOnlyCommand(cmd string) bool { - s.mu.Lock() - defer s.mu.Unlock() - cmdUp := strings.ToUpper(cmd) - if cmdMeta, ok := s.cmds[cmdUp]; ok { - return cmdMeta.readOnly - } - return false -} - -// ClientsLen gives the number of connected clients right now -func (s *Server) ClientsLen() int { - s.mu.Lock() - defer s.mu.Unlock() - return len(s.peers) -} - -// TotalConnections give the number of clients connected since the server -// started, including the currently connected ones -func (s *Server) TotalConnections() int { - s.mu.Lock() - defer s.mu.Unlock() - return s.infoConns -} - -// Peer is a client connected to the server -type Peer struct { - w *bufio.Writer - closed bool - Resp3 bool - SwitchResp3 *bool // we'll switch to this version _after_ the command - Ctx interface{} // anything goes, server won't touch this - onDisconnect []func() // list of callbacks - mu sync.Mutex // for Block() - ClientName string // client name set by CLIENT SETNAME -} - -func NewPeer(w *bufio.Writer) *Peer { - return &Peer{ - w: w, - } -} - -// Flush the write buffer. Called automatically after every redis command -func (c *Peer) Flush() { - c.mu.Lock() - defer c.mu.Unlock() - c.w.Flush() -} - -// Close the client connection after the current command is done. -func (c *Peer) Close() { - c.mu.Lock() - defer c.mu.Unlock() - c.closed = true -} - -// Return true if the peer connection closed. -func (c *Peer) Closed() bool { - c.mu.Lock() - defer c.mu.Unlock() - return c.closed -} - -// Register a function to execute on disconnect. There can be multiple -// functions registered. -func (c *Peer) OnDisconnect(f func()) { - c.onDisconnect = append(c.onDisconnect, f) -} - -// issue multiple calls, guarded with a mutex -func (c *Peer) Block(f func(*Writer)) { - c.mu.Lock() - defer c.mu.Unlock() - f(&Writer{c.w, c.Resp3}) -} - -// WriteError writes a redis 'Error' -func (c *Peer) WriteError(e string) { - c.Block(func(w *Writer) { - w.WriteError(e) - }) -} - -// WriteInline writes a redis inline string -func (c *Peer) WriteInline(s string) { - c.Block(func(w *Writer) { - w.WriteInline(s) - }) -} - -// WriteOK write the inline string `OK` -func (c *Peer) WriteOK() { - c.WriteInline("OK") -} - -// WriteBulk writes a bulk string -func (c *Peer) WriteBulk(s string) { - c.Block(func(w *Writer) { - w.WriteBulk(s) - }) -} - -// WriteNull writes a redis Null element -func (c *Peer) WriteNull() { - c.Block(func(w *Writer) { - w.WriteNull() - }) -} - -// WriteLen starts an array with the given length -func (c *Peer) WriteLen(n int) { - c.Block(func(w *Writer) { - w.WriteLen(n) - }) -} - -// WriteMapLen starts a map with the given length (number of keys) -func (c *Peer) WriteMapLen(n int) { - c.Block(func(w *Writer) { - w.WriteMapLen(n) - }) -} - -// WriteSetLen starts a set with the given length (number of elements) -func (c *Peer) WriteSetLen(n int) { - c.Block(func(w *Writer) { - w.WriteSetLen(n) - }) -} - -// WritePushLen starts a push-data array with the given length -func (c *Peer) WritePushLen(n int) { - c.Block(func(w *Writer) { - w.WritePushLen(n) - }) -} - -// WriteInt writes an integer -func (c *Peer) WriteInt(n int) { - c.Block(func(w *Writer) { - w.WriteInt(n) - }) -} - -// WriteFloat writes a float -func (c *Peer) WriteFloat(n float64) { - c.Block(func(w *Writer) { - w.WriteFloat(n) - }) -} - -// WriteRaw writes a raw redis response -func (c *Peer) WriteRaw(s string) { - c.Block(func(w *Writer) { - w.WriteRaw(s) - }) -} - -// WriteStrings is a helper to (bulk)write a string list -func (c *Peer) WriteStrings(strs []string) { - c.Block(func(w *Writer) { - w.WriteStrings(strs) - }) -} - -func toInline(s string) string { - return strings.Map(func(r rune) rune { - if unicode.IsSpace(r) { - return ' ' - } - return r - }, s) -} - -// A Writer is given to the callback in Block() -type Writer struct { - w *bufio.Writer - resp3 bool -} - -// WriteError writes a redis 'Error' -func (w *Writer) WriteError(e string) { - fmt.Fprintf(w.w, "-%s\r\n", toInline(e)) -} - -func (w *Writer) WriteLen(n int) { - fmt.Fprintf(w.w, "*%d\r\n", n) -} - -func (w *Writer) WriteMapLen(n int) { - if w.resp3 { - fmt.Fprintf(w.w, "%%%d\r\n", n) - return - } - w.WriteLen(n * 2) -} - -func (w *Writer) WriteSetLen(n int) { - if w.resp3 { - fmt.Fprintf(w.w, "~%d\r\n", n) - return - } - w.WriteLen(n) -} - -func (w *Writer) WritePushLen(n int) { - if w.resp3 { - fmt.Fprintf(w.w, ">%d\r\n", n) - return - } - w.WriteLen(n) -} - -// WriteBulk writes a bulk string -func (w *Writer) WriteBulk(s string) { - fmt.Fprintf(w.w, "$%d\r\n%s\r\n", len(s), s) -} - -// WriteStrings writes a list of strings (bulk) -func (w *Writer) WriteStrings(strs []string) { - w.WriteLen(len(strs)) - for _, s := range strs { - w.WriteBulk(s) - } -} - -// WriteInt writes an integer -func (w *Writer) WriteInt(n int) { - fmt.Fprintf(w.w, ":%d\r\n", n) -} - -// WriteFloat writes a float -func (w *Writer) WriteFloat(n float64) { - if w.resp3 { - fmt.Fprintf(w.w, ",%s\r\n", formatFloat(n)) - return - } - w.WriteBulk(formatFloat(n)) -} - -// WriteNull writes a redis Null element -func (w *Writer) WriteNull() { - if w.resp3 { - fmt.Fprint(w.w, "_\r\n") - return - } - fmt.Fprintf(w.w, "$-1\r\n") -} - -// WriteInline writes a redis inline string -func (w *Writer) WriteInline(s string) { - fmt.Fprintf(w.w, "+%s\r\n", toInline(s)) -} - -// WriteRaw writes a raw redis response -func (w *Writer) WriteRaw(s string) { - fmt.Fprint(w.w, s) -} - -func (w *Writer) Flush() { - w.w.Flush() -} - -// formatFloat formats a float the way redis does. -// Redis uses a method called "grisu2", which we ported from C. -func formatFloat(v float64) string { - return fpconv.Dtoa(v) -} diff --git a/vendor/github.com/alicebob/miniredis/v2/size/readme.md b/vendor/github.com/alicebob/miniredis/v2/size/readme.md deleted file mode 100644 index 89220e459..000000000 --- a/vendor/github.com/alicebob/miniredis/v2/size/readme.md +++ /dev/null @@ -1,2 +0,0 @@ - -Credits to DmitriyVTitov on his package https://github.com/DmitriyVTitov/size diff --git a/vendor/github.com/alicebob/miniredis/v2/size/size.go b/vendor/github.com/alicebob/miniredis/v2/size/size.go deleted file mode 100644 index 43fee6e21..000000000 --- a/vendor/github.com/alicebob/miniredis/v2/size/size.go +++ /dev/null @@ -1,138 +0,0 @@ -package size - -import ( - "reflect" - "unsafe" -) - -// Of returns the size of 'v' in bytes. -// If there is an error during calculation, Of returns -1. -func Of(v interface{}) int { - // Cache with every visited pointer so we don't count two pointers - // to the same memory twice. - cache := make(map[uintptr]bool) - return sizeOf(reflect.Indirect(reflect.ValueOf(v)), cache) -} - -// sizeOf returns the number of bytes the actual data represented by v occupies in memory. -// If there is an error, sizeOf returns -1. -func sizeOf(v reflect.Value, cache map[uintptr]bool) int { - switch v.Kind() { - - case reflect.Array: - sum := 0 - for i := 0; i < v.Len(); i++ { - s := sizeOf(v.Index(i), cache) - if s < 0 { - return -1 - } - sum += s - } - - return sum + (v.Cap()-v.Len())*int(v.Type().Elem().Size()) - - case reflect.Slice: - // return 0 if this node has been visited already - if cache[v.Pointer()] { - return 0 - } - cache[v.Pointer()] = true - - sum := 0 - for i := 0; i < v.Len(); i++ { - s := sizeOf(v.Index(i), cache) - if s < 0 { - return -1 - } - sum += s - } - - sum += (v.Cap() - v.Len()) * int(v.Type().Elem().Size()) - - return sum + int(v.Type().Size()) - - case reflect.Struct: - sum := 0 - for i, n := 0, v.NumField(); i < n; i++ { - s := sizeOf(v.Field(i), cache) - if s < 0 { - return -1 - } - sum += s - } - - // Look for struct padding. - padding := int(v.Type().Size()) - for i, n := 0, v.NumField(); i < n; i++ { - padding -= int(v.Field(i).Type().Size()) - } - - return sum + padding - - case reflect.String: - s := v.String() - hdr := (*reflect.StringHeader)(unsafe.Pointer(&s)) - if cache[hdr.Data] { - return int(v.Type().Size()) - } - cache[hdr.Data] = true - return len(s) + int(v.Type().Size()) - - case reflect.Ptr: - // return Ptr size if this node has been visited already (infinite recursion) - if cache[v.Pointer()] { - return int(v.Type().Size()) - } - cache[v.Pointer()] = true - if v.IsNil() { - return int(reflect.New(v.Type()).Type().Size()) - } - s := sizeOf(reflect.Indirect(v), cache) - if s < 0 { - return -1 - } - return s + int(v.Type().Size()) - - case reflect.Bool, - reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, - reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, - reflect.Int, reflect.Uint, - reflect.Chan, - reflect.Uintptr, - reflect.Float32, reflect.Float64, reflect.Complex64, reflect.Complex128, - reflect.Func: - return int(v.Type().Size()) - - case reflect.Map: - // return 0 if this node has been visited already (infinite recursion) - if cache[v.Pointer()] { - return 0 - } - cache[v.Pointer()] = true - sum := 0 - keys := v.MapKeys() - for i := range keys { - val := v.MapIndex(keys[i]) - // calculate size of key and value separately - sv := sizeOf(val, cache) - if sv < 0 { - return -1 - } - sum += sv - sk := sizeOf(keys[i], cache) - if sk < 0 { - return -1 - } - sum += sk - } - // Include overhead due to unused map buckets. 10.79 comes - // from https://golang.org/src/runtime/map.go. - return sum + int(v.Type().Size()) + int(float64(len(keys))*10.79) - - case reflect.Interface: - return sizeOf(v.Elem(), cache) + int(v.Type().Size()) - - } - - return -1 -} diff --git a/vendor/github.com/alicebob/miniredis/v2/sorted_set.go b/vendor/github.com/alicebob/miniredis/v2/sorted_set.go deleted file mode 100644 index 96ebd5d71..000000000 --- a/vendor/github.com/alicebob/miniredis/v2/sorted_set.go +++ /dev/null @@ -1,98 +0,0 @@ -package miniredis - -// The most KISS way to implement a sorted set. Luckily we don't care about -// performance that much. - -import ( - "sort" -) - -type direction int - -const ( - unsorted direction = iota - asc - desc -) - -type sortedSet map[string]float64 - -type ssElem struct { - score float64 - member string -} -type ssElems []ssElem - -type byScore ssElems - -func (sse byScore) Len() int { return len(sse) } -func (sse byScore) Swap(i, j int) { sse[i], sse[j] = sse[j], sse[i] } -func (sse byScore) Less(i, j int) bool { - if sse[i].score != sse[j].score { - return sse[i].score < sse[j].score - } - return sse[i].member < sse[j].member -} - -func newSortedSet() sortedSet { - return sortedSet{} -} - -func (ss *sortedSet) card() int { - return len(*ss) -} - -func (ss *sortedSet) set(score float64, member string) { - (*ss)[member] = score -} - -func (ss *sortedSet) get(member string) (float64, bool) { - v, ok := (*ss)[member] - return v, ok -} - -// elems gives the list of ssElem, ready to sort. -func (ss *sortedSet) elems() ssElems { - elems := make(ssElems, 0, len(*ss)) - for e, s := range *ss { - elems = append(elems, ssElem{s, e}) - } - return elems -} - -func (ss *sortedSet) byScore(d direction) ssElems { - elems := ss.elems() - sort.Sort(byScore(elems)) - if d == desc { - reverseElems(elems) - } - return ssElems(elems) -} - -// rankByScore gives the (0-based) index of member, or returns false. -func (ss *sortedSet) rankByScore(member string, d direction) (int, bool) { - if _, ok := (*ss)[member]; !ok { - return 0, false - } - for i, e := range ss.byScore(d) { - if e.member == member { - return i, true - } - } - // Can't happen - return 0, false -} - -func reverseSlice(o []string) { - for i := range make([]struct{}, len(o)/2) { - other := len(o) - 1 - i - o[i], o[other] = o[other], o[i] - } -} - -func reverseElems(o ssElems) { - for i := range make([]struct{}, len(o)/2) { - other := len(o) - 1 - i - o[i], o[other] = o[other], o[i] - } -} diff --git a/vendor/github.com/alicebob/miniredis/v2/stream.go b/vendor/github.com/alicebob/miniredis/v2/stream.go deleted file mode 100644 index 27cc5209b..000000000 --- a/vendor/github.com/alicebob/miniredis/v2/stream.go +++ /dev/null @@ -1,514 +0,0 @@ -// Basic stream implementation. - -package miniredis - -import ( - "errors" - "fmt" - "math" - "sort" - "strconv" - "strings" - "sync" - "time" -) - -// a Stream is a list of entries, lowest ID (oldest) first, and all "groups". -type streamKey struct { - entries []StreamEntry - groups map[string]*streamGroup - lastAllocatedID string - mu sync.Mutex -} - -// a StreamEntry is an entry in a stream. The ID is always of the form -// "123-123". -// Values is an ordered list of key-value pairs. -type StreamEntry struct { - ID string - Values []string -} - -type streamGroup struct { - stream *streamKey - lastID string - pending []pendingEntry - consumers map[string]*consumer -} - -type consumer struct { - numPendingEntries int - // these timestamps aren't tracked perfectly - lastSeen time.Time // "idle" XINFO key - lastSuccess time.Time // "inactive" XINFO key -} - -type pendingEntry struct { - id string - consumer string - deliveryCount int - lastDelivery time.Time -} - -func newStreamKey() *streamKey { - return &streamKey{ - groups: map[string]*streamGroup{}, - } -} - -// generateID doesn't lock the mutex -func (s *streamKey) generateID(ts uint64) string { - next := fmt.Sprintf("%d-%d", ts, 0) - if s.lastAllocatedID != "" && streamCmp(s.lastAllocatedID, next) >= 0 { - last, _ := parseStreamID(s.lastAllocatedID) - next = fmt.Sprintf("%d-%d", last[0], last[1]+1) - } - - lastID := s.lastIDUnlocked() - if streamCmp(lastID, next) >= 0 { - last, _ := parseStreamID(lastID) - next = fmt.Sprintf("%d-%d", last[0], last[1]+1) - } - - s.lastAllocatedID = next - return next -} - -// lastID locks the mutex -func (s *streamKey) lastID() string { - s.mu.Lock() - defer s.mu.Unlock() - - return s.lastIDUnlocked() -} - -// lastID doesn't lock the mutex -func (s *streamKey) lastIDUnlocked() string { - if len(s.entries) == 0 { - return "0-0" - } - - return s.entries[len(s.entries)-1].ID -} - -func (s *streamKey) copy() *streamKey { - s.mu.Lock() - defer s.mu.Unlock() - - cpy := &streamKey{ - entries: s.entries, - } - groups := map[string]*streamGroup{} - for k, v := range s.groups { - gr := v.copy() - gr.stream = cpy - groups[k] = gr - } - cpy.groups = groups - return cpy -} - -func parseStreamID(id string) ([2]uint64, error) { - var ( - res [2]uint64 - err error - ) - parts := strings.SplitN(id, "-", 2) - res[0], err = strconv.ParseUint(parts[0], 10, 64) - if err != nil { - return res, errors.New(msgInvalidStreamID) - } - if len(parts) == 2 { - res[1], err = strconv.ParseUint(parts[1], 10, 64) - if err != nil { - return res, errors.New(msgInvalidStreamID) - } - } - return res, nil -} - -// compares two stream IDs (of the full format: "123-123"). Returns: -1, 0, 1 -// The given IDs should be valid stream IDs. -func streamCmp(a, b string) int { - ap, _ := parseStreamID(a) - bp, _ := parseStreamID(b) - - switch { - case ap[0] < bp[0]: - return -1 - case ap[0] > bp[0]: - return 1 - case ap[1] < bp[1]: - return -1 - case ap[1] > bp[1]: - return 1 - default: - return 0 - } -} - -// formatStreamID makes a full id ("42-42") out of a partial one ("42") -func formatStreamID(id string) (string, error) { - var ts [2]uint64 - parts := strings.SplitN(id, "-", 2) - - if len(parts) > 0 { - p, err := strconv.ParseUint(parts[0], 10, 64) - if err != nil { - return "", errInvalidEntryID - } - ts[0] = p - } - if len(parts) > 1 { - p, err := strconv.ParseUint(parts[1], 10, 64) - if err != nil { - return "", errInvalidEntryID - } - ts[1] = p - } - return fmt.Sprintf("%d-%d", ts[0], ts[1]), nil -} - -func formatStreamRangeBound(id string, start bool, reverse bool) (string, error) { - if id == "-" { - return "0-0", nil - } - - if id == "+" { - return fmt.Sprintf("%d-%d", uint64(math.MaxUint64), uint64(math.MaxUint64)), nil - } - - if id == "0" { - return "0-0", nil - } - - parts := strings.Split(id, "-") - if len(parts) == 2 { - return formatStreamID(id) - } - - // Incomplete IDs case - ts, err := strconv.ParseUint(parts[0], 10, 64) - if err != nil { - return "", errInvalidEntryID - } - - if (!start && !reverse) || (start && reverse) { - return fmt.Sprintf("%d-%d", ts, uint64(math.MaxUint64)), nil - } - - return fmt.Sprintf("%d-%d", ts, 0), nil -} - -func reversedStreamEntries(o []StreamEntry) []StreamEntry { - newStream := make([]StreamEntry, len(o)) - for i, e := range o { - newStream[len(o)-i-1] = e - } - return newStream -} - -func (s *streamKey) createGroup(group, id string) error { - s.mu.Lock() - defer s.mu.Unlock() - - if _, ok := s.groups[group]; ok { - return errors.New("BUSYGROUP Consumer Group name already exists") - } - - if id == "$" { - id = s.lastIDUnlocked() - } - s.groups[group] = &streamGroup{ - stream: s, - lastID: id, - consumers: map[string]*consumer{}, - } - return nil -} - -// streamAdd adds an entry to a stream. Returns the new entry ID. -// If id is empty, "*", or "123-*", the ID will be generated automatically. -// `values` should have an even length. -func (s *streamKey) add(entryID string, values []string, now time.Time) (string, error) { - s.mu.Lock() - defer s.mu.Unlock() - - switch { - case entryID == "" || entryID == "*": - entryID = s.generateID(uint64(now.UnixMilli())) - default: - // "-*" - parts := strings.Split(entryID, "-") - if len(parts) == 2 && parts[1] == "*" { - if ts, err := strconv.ParseUint(parts[0], 10, 64); err == nil { - entryID = s.generateID(uint64(ts)) - } - } - } - - entryID, err := formatStreamID(entryID) - if err != nil { - return "", err - } - if entryID == "0-0" { - return "", errors.New(msgStreamIDZero) - } - if streamCmp(s.lastIDUnlocked(), entryID) != -1 { - return "", errors.New(msgStreamIDTooSmall) - } - - s.entries = append(s.entries, StreamEntry{ - ID: entryID, - Values: values, - }) - return entryID, nil -} - -func (s *streamKey) trim(n int) { - s.mu.Lock() - defer s.mu.Unlock() - - if len(s.entries) > n { - s.entries = s.entries[len(s.entries)-n:] - } -} - -// trimBefore deletes entries with an id less than the provided id -// and returns the number of entries deleted -func (s *streamKey) trimBefore(id string) int { - s.mu.Lock() - var delete []string - for _, entry := range s.entries { - if streamCmp(entry.ID, id) < 0 { - delete = append(delete, entry.ID) - } else { - break - } - } - s.mu.Unlock() - s.delete(delete) - return len(delete) -} - -// all entries after "id" -func (s *streamKey) after(id string) []StreamEntry { - s.mu.Lock() - defer s.mu.Unlock() - - pos := sort.Search(len(s.entries), func(i int) bool { - return streamCmp(id, s.entries[i].ID) < 0 - }) - return s.entries[pos:] -} - -// get a stream entry by ID -// Also returns the position in the entries slice, if found. -func (s *streamKey) get(id string) (int, *StreamEntry) { - s.mu.Lock() - defer s.mu.Unlock() - - pos := sort.Search(len(s.entries), func(i int) bool { - return streamCmp(id, s.entries[i].ID) <= 0 - }) - if len(s.entries) <= pos || s.entries[pos].ID != id { - return 0, nil - } - return pos, &s.entries[pos] -} - -func (g *streamGroup) readGroup( - now time.Time, - consumerID, - id string, - count int, - noack bool, -) []StreamEntry { - if id == ">" { - // undelivered messages - msgs := g.stream.after(g.lastID) - if len(msgs) == 0 { - return nil - } - - if count > 0 && len(msgs) > count { - msgs = msgs[:count] - } - - if !noack { - shouldAppend := len(g.pending) == 0 - for _, msg := range msgs { - if !shouldAppend { - shouldAppend = streamCmp(msg.ID, g.pending[len(g.pending)-1].id) == 1 - } - - var entry *pendingEntry - if shouldAppend { - g.pending = append(g.pending, pendingEntry{}) - entry = &g.pending[len(g.pending)-1] - } else { - var pos int - pos, entry = g.searchPending(msg.ID) - if entry == nil { - g.pending = append(g.pending[:pos+1], g.pending[pos:]...) - entry = &g.pending[pos] - } else { - g.consumers[entry.consumer].numPendingEntries-- - } - } - - *entry = pendingEntry{ - id: msg.ID, - consumer: consumerID, - deliveryCount: 1, - lastDelivery: now, - } - } - } - if _, ok := g.consumers[consumerID]; !ok { - g.consumers[consumerID] = &consumer{} - } - g.consumers[consumerID].numPendingEntries += len(msgs) - g.lastID = msgs[len(msgs)-1].ID - return msgs - } - - // re-deliver messages from the pending list. - // con := gr.consumers[consumerID] - msgs := g.pendingAfter(id) - var res []StreamEntry - for i, p := range msgs { - if p.consumer != consumerID { - continue - } - _, entry := g.stream.get(p.id) - // not found. Weird? - if entry == nil { - continue - } - p.deliveryCount += 1 - p.lastDelivery = now - msgs[i] = p - res = append(res, *entry) - } - return res -} - -func (g *streamGroup) searchPending(id string) (int, *pendingEntry) { - pos := sort.Search(len(g.pending), func(i int) bool { - return streamCmp(id, g.pending[i].id) <= 0 - }) - if pos >= len(g.pending) || g.pending[pos].id != id { - return pos, nil - } - return pos, &g.pending[pos] -} - -func (g *streamGroup) ack(ids []string) (int, error) { - count := 0 - for _, id := range ids { - if _, err := parseStreamID(id); err != nil { - return 0, errors.New(msgInvalidStreamID) - } - - pos, entry := g.searchPending(id) - if entry == nil { - continue - } - - consumer := g.consumers[entry.consumer] - consumer.numPendingEntries-- - - g.pending = append(g.pending[:pos], g.pending[pos+1:]...) - // don't count deleted entries - if _, e := g.stream.get(id); e == nil { - continue - } - count++ - } - return count, nil -} - -func (s *streamKey) delete(ids []string) (int, error) { - count := 0 - for _, id := range ids { - if _, err := parseStreamID(id); err != nil { - return 0, errors.New(msgInvalidStreamID) - } - - i, entry := s.get(id) - if entry == nil { - continue - } - - s.entries = append(s.entries[:i], s.entries[i+1:]...) - count++ - } - return count, nil -} - -func (g *streamGroup) pendingAfterOrEqual(id string) []pendingEntry { - pos := sort.Search(len(g.pending), func(i int) bool { - return streamCmp(id, g.pending[i].id) <= 0 - }) - return g.pending[pos:] -} - -func (g *streamGroup) pendingAfter(id string) []pendingEntry { - pos := sort.Search(len(g.pending), func(i int) bool { - return streamCmp(id, g.pending[i].id) < 0 - }) - return g.pending[pos:] -} - -func (g *streamGroup) pendingCount(consumer string) int { - n := 0 - for _, p := range g.activePending() { - if p.consumer == consumer { - n++ - } - } - return n -} - -// pending entries without the entries deleted from the group -func (g *streamGroup) activePending() []pendingEntry { - var pe []pendingEntry - for _, p := range g.pending { - // drop deleted ones - if _, e := g.stream.get(p.id); e == nil { - continue - } - p := p - pe = append(pe, p) - } - return pe -} - -func (g *streamGroup) copy() *streamGroup { - cns := map[string]*consumer{} - for k, v := range g.consumers { - c := *v - cns[k] = &c - } - return &streamGroup{ - // don't copy stream - lastID: g.lastID, - pending: g.pending, - consumers: cns, - } -} - -func (g *streamGroup) setLastSeen(c string, t time.Time) { - cons, ok := g.consumers[c] - if !ok { - cons = &consumer{} - } - cons.lastSeen = t - g.consumers[c] = cons -} - -func (g *streamGroup) setLastSuccess(c string, t time.Time) { - g.setLastSeen(c, t) - g.consumers[c].lastSuccess = t -} diff --git a/vendor/github.com/dgryski/go-rendezvous/LICENSE b/vendor/github.com/dgryski/go-rendezvous/LICENSE deleted file mode 100644 index 22080f736..000000000 --- a/vendor/github.com/dgryski/go-rendezvous/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2017-2020 Damian Gryski - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. diff --git a/vendor/github.com/dgryski/go-rendezvous/rdv.go b/vendor/github.com/dgryski/go-rendezvous/rdv.go deleted file mode 100644 index 7a6f8203c..000000000 --- a/vendor/github.com/dgryski/go-rendezvous/rdv.go +++ /dev/null @@ -1,79 +0,0 @@ -package rendezvous - -type Rendezvous struct { - nodes map[string]int - nstr []string - nhash []uint64 - hash Hasher -} - -type Hasher func(s string) uint64 - -func New(nodes []string, hash Hasher) *Rendezvous { - r := &Rendezvous{ - nodes: make(map[string]int, len(nodes)), - nstr: make([]string, len(nodes)), - nhash: make([]uint64, len(nodes)), - hash: hash, - } - - for i, n := range nodes { - r.nodes[n] = i - r.nstr[i] = n - r.nhash[i] = hash(n) - } - - return r -} - -func (r *Rendezvous) Lookup(k string) string { - // short-circuit if we're empty - if len(r.nodes) == 0 { - return "" - } - - khash := r.hash(k) - - var midx int - var mhash = xorshiftMult64(khash ^ r.nhash[0]) - - for i, nhash := range r.nhash[1:] { - if h := xorshiftMult64(khash ^ nhash); h > mhash { - midx = i + 1 - mhash = h - } - } - - return r.nstr[midx] -} - -func (r *Rendezvous) Add(node string) { - r.nodes[node] = len(r.nstr) - r.nstr = append(r.nstr, node) - r.nhash = append(r.nhash, r.hash(node)) -} - -func (r *Rendezvous) Remove(node string) { - // find index of node to remove - nidx := r.nodes[node] - - // remove from the slices - l := len(r.nstr) - r.nstr[nidx] = r.nstr[l] - r.nstr = r.nstr[:l] - - r.nhash[nidx] = r.nhash[l] - r.nhash = r.nhash[:l] - - // update the map - delete(r.nodes, node) - moved := r.nstr[nidx] - r.nodes[moved] = nidx -} - -func xorshiftMult64(x uint64) uint64 { - x ^= x >> 12 // a - x ^= x << 25 // b - x ^= x >> 27 // c - return x * 2685821657736338717 -} diff --git a/vendor/github.com/redis/go-redis/v9/.gitignore b/vendor/github.com/redis/go-redis/v9/.gitignore deleted file mode 100644 index 93affec7f..000000000 --- a/vendor/github.com/redis/go-redis/v9/.gitignore +++ /dev/null @@ -1,19 +0,0 @@ -*.rdb -testdata/* -.idea/ -.DS_Store -*.tar.gz -*.dic -redis8tests.sh -coverage.txt -**/coverage.txt -.vscode -tmp/* -*.test -extra/redisotel-native/metrics-collector-app/ -# maintenanceNotifications upgrade documentation (temporary) -maintenanceNotifications/docs/ - -# Docker-generated files (TLS certificates, cluster data, etc.) -dockers/*/tls/ -dockers/osscluster-tls/ diff --git a/vendor/github.com/redis/go-redis/v9/.golangci.yml b/vendor/github.com/redis/go-redis/v9/.golangci.yml deleted file mode 100644 index dd13c2c29..000000000 --- a/vendor/github.com/redis/go-redis/v9/.golangci.yml +++ /dev/null @@ -1,36 +0,0 @@ -version: "2" -run: - timeout: 5m - tests: false -linters: - settings: - staticcheck: - checks: - - all - # Incorrect or missing package comment. - # https://staticcheck.dev/docs/checks/#ST1000 - - -ST1000 - # Omit embedded fields from selector expression. - # https://staticcheck.dev/docs/checks/#QF1008 - - -QF1008 - - -ST1003 - exclusions: - generated: lax - presets: - - comments - - common-false-positives - - legacy - - std-error-handling - paths: - - third_party$ - - builtin$ - - examples$ -formatters: - enable: - - gofmt - exclusions: - generated: lax - paths: - - third_party$ - - builtin$ - - examples$ diff --git a/vendor/github.com/redis/go-redis/v9/.prettierrc.yml b/vendor/github.com/redis/go-redis/v9/.prettierrc.yml deleted file mode 100644 index 8b7f044ad..000000000 --- a/vendor/github.com/redis/go-redis/v9/.prettierrc.yml +++ /dev/null @@ -1,4 +0,0 @@ -semi: false -singleQuote: true -proseWrap: always -printWidth: 100 diff --git a/vendor/github.com/redis/go-redis/v9/CONTRIBUTING.md b/vendor/github.com/redis/go-redis/v9/CONTRIBUTING.md deleted file mode 100644 index 8c68c522e..000000000 --- a/vendor/github.com/redis/go-redis/v9/CONTRIBUTING.md +++ /dev/null @@ -1,118 +0,0 @@ -# Contributing - -## Introduction - -We appreciate your interest in considering contributing to go-redis. -Community contributions mean a lot to us. - -## Contributions we need - -You may already know how you'd like to contribute, whether it's a fix for a bug you -encountered, or a new feature your team wants to use. - -If you don't know where to start, consider improving -documentation, bug triaging, and writing tutorials are all examples of -helpful contributions that mean less work for you. - -## Your First Contribution - -Unsure where to begin contributing? You can start by looking through -[help-wanted -issues](https://github.com/redis/go-redis/issues?q=is%3Aopen+is%3Aissue+label%3ahelp-wanted). - -Never contributed to open source before? Here are a couple of friendly -tutorials: - -- -- - -## Getting Started - -Here's how to get started with your code contribution: - -1. Create your own fork of go-redis -2. Do the changes in your fork -3. If you need a development environment, run `make docker.start`. - -> Note: this clones and builds the docker containers specified in `docker-compose.yml`, to understand more about -> the infrastructure that will be started you can check the `docker-compose.yml`. You also have the possiblity -> to specify the redis image that will be pulled with the env variable `CLIENT_LIBS_TEST_IMAGE`. -> By default the docker image that will be pulled and started is `redislabs/client-libs-test:8.2.1-pre`. -> If you want to test with newer Redis version, using a newer version of `redislabs/client-libs-test` should work out of the box. - -4. While developing, make sure the tests pass by running `make test` (if you have the docker containers running, `make test.ci` may be sufficient). -> Note: `make test` will try to start all containers, run the tests with `make test.ci` and then stop all containers. -5. If you like the change and think the project could use it, send a - pull request - -To see what else is part of the automation, run `invoke -l` - - -## Testing - -### Setting up Docker -To run the tests, you need to have Docker installed and running. If you are using a host OS that does not support -docker host networks out of the box (e.g. Windows, OSX), you need to set up a docker desktop and enable docker host networks. - -### Running tests -Call `make test` to run all tests. - -Continuous Integration uses these same wrappers to run all of these -tests against multiple versions of redis. Feel free to test your -changes against all the go versions supported, as declared by the -[build.yml](./.github/workflows/build.yml) file. - -### Troubleshooting - -If you get any errors when running `make test`, make sure -that you are using supported versions of Docker and go. - -## How to Report a Bug - -### Security Vulnerabilities - -**NOTE**: If you find a security vulnerability, do NOT open an issue. -Email [Redis Open Source ()](mailto:oss@redis.com) instead. - -In order to determine whether you are dealing with a security issue, ask -yourself these two questions: - -- Can I access something that's not mine, or something I shouldn't - have access to? -- Can I disable something for other people? - -If the answer to either of those two questions are *yes*, then you're -probably dealing with a security issue. Note that even if you answer -*no* to both questions, you may still be dealing with a security -issue, so if you're unsure, just email [us](mailto:oss@redis.com). - -### Everything Else - -When filing an issue, make sure to answer these five questions: - -1. What version of go-redis are you using? -2. What version of redis are you using? -3. What did you do? -4. What did you expect to see? -5. What did you see instead? - -## Suggest a feature or enhancement - -If you'd like to contribute a new feature, make sure you check our -issue list to see if someone has already proposed it. Work may already -be underway on the feature you want or we may have rejected a -feature like it already. - -If you don't see anything, open a new issue that describes the feature -you would like and how it should work. - -## Code review process - -The core team regularly looks at pull requests. We will provide -feedback as soon as possible. After receiving our feedback, please respond -within two weeks. After that time, we may close your PR if it isn't -showing any activity. - -## Support - -Maintainers can provide limited support to contributors on discord: https://discord.gg/W4txy5AeKM diff --git a/vendor/github.com/redis/go-redis/v9/LICENSE b/vendor/github.com/redis/go-redis/v9/LICENSE deleted file mode 100644 index f4967dbc5..000000000 --- a/vendor/github.com/redis/go-redis/v9/LICENSE +++ /dev/null @@ -1,25 +0,0 @@ -Copyright (c) 2013 The github.com/redis/go-redis Authors. -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are -met: - - * Redistributions of source code must retain the above copyright -notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above -copyright notice, this list of conditions and the following disclaimer -in the documentation and/or other materials provided with the -distribution. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/github.com/redis/go-redis/v9/Makefile b/vendor/github.com/redis/go-redis/v9/Makefile deleted file mode 100644 index 370f3880c..000000000 --- a/vendor/github.com/redis/go-redis/v9/Makefile +++ /dev/null @@ -1,122 +0,0 @@ -GO_MOD_DIRS := $(shell find . -type f -name 'go.mod' -exec dirname {} \; | sort) -REDIS_VERSION ?= 8.6 -RE_CLUSTER ?= false -RCE_DOCKER ?= true -CLIENT_LIBS_TEST_IMAGE ?= redislabs/client-libs-test:custom-21860421418-debian-amd64 - -docker.start: - export RE_CLUSTER=$(RE_CLUSTER) && \ - export RCE_DOCKER=$(RCE_DOCKER) && \ - export REDIS_VERSION=$(REDIS_VERSION) && \ - export CLIENT_LIBS_TEST_IMAGE=$(CLIENT_LIBS_TEST_IMAGE) && \ - docker compose --profile all up -d --quiet-pull - -docker.stop: - docker compose --profile all down - -docker.e2e.start: - @echo "Starting Redis and cae-resp-proxy for E2E tests..." - docker compose --profile e2e up -d --quiet-pull - @echo "Waiting for services to be ready..." - @sleep 3 - @echo "Services ready!" - -docker.e2e.stop: - @echo "Stopping E2E services..." - docker compose --profile e2e down - -test: - $(MAKE) docker.start - @if [ -z "$(REDIS_VERSION)" ]; then \ - echo "REDIS_VERSION not set, running all tests"; \ - $(MAKE) test.ci; \ - else \ - MAJOR_VERSION=$$(echo "$(REDIS_VERSION)" | cut -d. -f1); \ - if [ "$$MAJOR_VERSION" -ge 8 ]; then \ - echo "REDIS_VERSION $(REDIS_VERSION) >= 8, running all tests"; \ - $(MAKE) test.ci; \ - else \ - echo "REDIS_VERSION $(REDIS_VERSION) < 8, skipping vector_sets tests"; \ - $(MAKE) test.ci.skip-vectorsets; \ - fi; \ - fi - $(MAKE) docker.stop - -test.ci: - set -e; for dir in $(GO_MOD_DIRS); do \ - echo "go test in $${dir}"; \ - (cd "$${dir}" && \ - export RE_CLUSTER=$(RE_CLUSTER) && \ - export RCE_DOCKER=$(RCE_DOCKER) && \ - export REDIS_VERSION=$(REDIS_VERSION) && \ - go mod tidy -compat=1.18 && \ - go vet && \ - go test -v -coverprofile=coverage.txt -covermode=atomic ./... -race -skip Example); \ - done - cd internal/customvet && go build . - go vet -vettool ./internal/customvet/customvet - -test.ci.skip-vectorsets: - set -e; for dir in $(GO_MOD_DIRS); do \ - echo "go test in $${dir} (skipping vector sets)"; \ - (cd "$${dir}" && \ - export RE_CLUSTER=$(RE_CLUSTER) && \ - export RCE_DOCKER=$(RCE_DOCKER) && \ - export REDIS_VERSION=$(REDIS_VERSION) && \ - go mod tidy -compat=1.18 && \ - go vet && \ - go test -v -coverprofile=coverage.txt -covermode=atomic ./... -race \ - -run '^(?!.*(?:VectorSet|vectorset|ExampleClient_vectorset)).*$$' -skip Example); \ - done - cd internal/customvet && go build . - go vet -vettool ./internal/customvet/customvet - -bench: - export RE_CLUSTER=$(RE_CLUSTER) && \ - export RCE_DOCKER=$(RCE_DOCKER) && \ - export REDIS_VERSION=$(REDIS_VERSION) && \ - go test ./... -test.run=NONE -test.bench=. -test.benchmem -skip Example - -test.e2e: - @echo "Running E2E tests with auto-start proxy..." - $(MAKE) docker.e2e.start - @echo "Running tests..." - @E2E_SCENARIO_TESTS=true go test -v ./maintnotifications/e2e/ -timeout 30m || ($(MAKE) docker.e2e.stop && exit 1) - $(MAKE) docker.e2e.stop - @echo "E2E tests completed!" - -test.e2e.docker: - @echo "Running Docker-compatible E2E tests..." - $(MAKE) docker.e2e.start - @echo "Running unified injector tests..." - @E2E_SCENARIO_TESTS=true go test -v -run "TestUnifiedInjector|TestCreateTestFaultInjectorLogic|TestFaultInjectorClientCreation" ./maintnotifications/e2e/ -timeout 10m || ($(MAKE) docker.e2e.stop && exit 1) - $(MAKE) docker.e2e.stop - @echo "Docker E2E tests completed!" - -test.e2e.logic: - @echo "Running E2E logic tests (no proxy required)..." - @E2E_SCENARIO_TESTS=true \ - REDIS_ENDPOINTS_CONFIG_PATH=/tmp/test_endpoints_verify.json \ - FAULT_INJECTION_API_URL=http://localhost:8080 \ - go test -v -run "TestCreateTestFaultInjectorLogic|TestFaultInjectorClientCreation" ./maintnotifications/e2e/ - @echo "Logic tests completed!" - -.PHONY: all test test.ci test.ci.skip-vectorsets bench fmt test.e2e test.e2e.logic docker.e2e.start docker.e2e.stop - -build: - export RE_CLUSTER=$(RE_CLUSTER) && \ - export RCE_DOCKER=$(RCE_DOCKER) && \ - export REDIS_VERSION=$(REDIS_VERSION) && \ - go build . - -fmt: - gofumpt -w ./ - goimports -w -local github.com/redis/go-redis ./ - -go_mod_tidy: - set -e; for dir in $(GO_MOD_DIRS); do \ - echo "go mod tidy in $${dir}"; \ - (cd "$${dir}" && \ - go get -u ./... && \ - go mod tidy -compat=1.18); \ - done diff --git a/vendor/github.com/redis/go-redis/v9/README.md b/vendor/github.com/redis/go-redis/v9/README.md deleted file mode 100644 index 160714ab0..000000000 --- a/vendor/github.com/redis/go-redis/v9/README.md +++ /dev/null @@ -1,596 +0,0 @@ -# Redis client for Go - -[![build workflow](https://github.com/redis/go-redis/actions/workflows/build.yml/badge.svg)](https://github.com/redis/go-redis/actions) -[![PkgGoDev](https://pkg.go.dev/badge/github.com/redis/go-redis/v9)](https://pkg.go.dev/github.com/redis/go-redis/v9?tab=doc) -[![Documentation](https://img.shields.io/badge/redis-documentation-informational)](https://redis.io/docs/latest/develop/clients/go/) -[![Go Report Card](https://goreportcard.com/badge/github.com/redis/go-redis/v9)](https://goreportcard.com/report/github.com/redis/go-redis/v9) -[![codecov](https://codecov.io/github/redis/go-redis/graph/badge.svg?token=tsrCZKuSSw)](https://codecov.io/github/redis/go-redis) - -[![Discord](https://img.shields.io/discord/697882427875393627.svg?style=social&logo=discord)](https://discord.gg/W4txy5AeKM) -[![Twitch](https://img.shields.io/twitch/status/redisinc?style=social)](https://www.twitch.tv/redisinc) -[![YouTube](https://img.shields.io/youtube/channel/views/UCD78lHSwYqMlyetR0_P4Vig?style=social)](https://www.youtube.com/redisinc) -[![Twitter](https://img.shields.io/twitter/follow/redisinc?style=social)](https://twitter.com/redisinc) -[![Stack Exchange questions](https://img.shields.io/stackexchange/stackoverflow/t/go-redis?style=social&logo=stackoverflow&label=Stackoverflow)](https://stackoverflow.com/questions/tagged/go-redis) - -> go-redis is the official Redis client library for the Go programming language. It offers a straightforward interface for interacting with Redis servers. - -## Supported versions - -In `go-redis` we are aiming to support the last three releases of Redis. Currently, this means we do support: -- [Redis 8.0](https://raw.githubusercontent.com/redis/redis/8.0/00-RELEASENOTES) - using Redis CE 8.0 -- [Redis 8.2](https://raw.githubusercontent.com/redis/redis/8.2/00-RELEASENOTES) - using Redis CE 8.2 -- [Redis 8.4](https://raw.githubusercontent.com/redis/redis/8.4/00-RELEASENOTES) - using Redis CE 8.4 - -Although the `go.mod` states it requires at minimum `go 1.21`, our CI is configured to run the tests against all three -versions of Redis and multiple versions of Go ([1.21](https://go.dev/doc/devel/release#go1.21.0), -[1.23](https://go.dev/doc/devel/release#go1.23.0), oldstable, and stable). We observe that some modules related test may not pass with -Redis Stack 7.2 and some commands are changed with Redis CE 8.0. -Although it is not officially supported, `go-redis/v9` should be able to work with any Redis 7.0+. -Please do refer to the documentation and the tests if you experience any issues. - -## How do I Redis? - -[Learn for free at Redis University](https://university.redis.com/) - -[Build faster with the Redis Launchpad](https://launchpad.redis.com/) - -[Try the Redis Cloud](https://redis.com/try-free/) - -[Dive in developer tutorials](https://developer.redis.com/) - -[Join the Redis community](https://redis.com/community/) - -[Work at Redis](https://redis.com/company/careers/jobs/) - - -## Resources - -- [Discussions](https://github.com/redis/go-redis/discussions) -- [Chat](https://discord.gg/W4txy5AeKM) -- [Reference](https://pkg.go.dev/github.com/redis/go-redis/v9) -- [Examples](https://pkg.go.dev/github.com/redis/go-redis/v9#pkg-examples) - -## old documentation - -- [English](https://redis.uptrace.dev) -- [简体中文](https://redis.uptrace.dev/zh/) - -## Ecosystem - -- [Entra ID (Azure AD)](https://github.com/redis/go-redis-entraid) -- [Distributed Locks](https://github.com/bsm/redislock) -- [Redis Cache](https://github.com/go-redis/cache) -- [Rate limiting](https://github.com/go-redis/redis_rate) - -## Features - -- Redis commands except QUIT and SYNC. -- Automatic connection pooling. -- [StreamingCredentialsProvider (e.g. entra id, oauth)](#1-streaming-credentials-provider-highest-priority) (experimental) -- [Pub/Sub](https://redis.uptrace.dev/guide/go-redis-pubsub.html). -- [Pipelines and transactions](https://redis.uptrace.dev/guide/go-redis-pipelines.html). -- [Scripting](https://redis.uptrace.dev/guide/lua-scripting.html). -- [Redis Sentinel](https://redis.uptrace.dev/guide/go-redis-sentinel.html). -- [Redis Cluster](https://redis.uptrace.dev/guide/go-redis-cluster.html). -- [Redis Performance Monitoring](https://redis.uptrace.dev/guide/redis-performance-monitoring.html). -- [Redis Probabilistic [RedisStack]](https://redis.io/docs/data-types/probabilistic/) -- [Customizable read and write buffers size.](#custom-buffer-sizes) - -## Installation - -go-redis supports 2 last Go versions and requires a Go version with -[modules](https://github.com/golang/go/wiki/Modules) support. So make sure to initialize a Go -module: - -```shell -go mod init github.com/my/repo -``` - -Then install go-redis/**v9**: - -```shell -go get github.com/redis/go-redis/v9 -``` - -## Quickstart - -```go -import ( - "context" - "fmt" - - "github.com/redis/go-redis/v9" -) - -var ctx = context.Background() - -func ExampleClient() { - rdb := redis.NewClient(&redis.Options{ - Addr: "localhost:6379", - Password: "", // no password set - DB: 0, // use default DB - }) - defer rdb.Close() - - err := rdb.Set(ctx, "key", "value", 0).Err() - if err != nil { - panic(err) - } - - val, err := rdb.Get(ctx, "key").Result() - if err != nil { - panic(err) - } - fmt.Println("key", val) - - val2, err := rdb.Get(ctx, "key2").Result() - if err == redis.Nil { - fmt.Println("key2 does not exist") - } else if err != nil { - panic(err) - } else { - fmt.Println("key2", val2) - } - // Output: key value - // key2 does not exist -} -``` - -### Authentication - -The Redis client supports multiple ways to provide authentication credentials, with a clear priority order. Here are the available options: - -#### 1. Streaming Credentials Provider (Highest Priority) - Experimental feature - -The streaming credentials provider allows for dynamic credential updates during the connection lifetime. This is particularly useful for managed identity services and token-based authentication. - -```go -type StreamingCredentialsProvider interface { - Subscribe(listener CredentialsListener) (Credentials, UnsubscribeFunc, error) -} - -type CredentialsListener interface { - OnNext(credentials Credentials) // Called when credentials are updated - OnError(err error) // Called when an error occurs -} - -type Credentials interface { - BasicAuth() (username string, password string) - RawCredentials() string -} -``` - -Example usage: -```go -rdb := redis.NewClient(&redis.Options{ - Addr: "localhost:6379", - StreamingCredentialsProvider: &MyCredentialsProvider{}, -}) -``` - -**Note:** The streaming credentials provider can be used with [go-redis-entraid](https://github.com/redis/go-redis-entraid) to enable Entra ID (formerly Azure AD) authentication. This allows for seamless integration with Azure's managed identity services and token-based authentication. - -Example with Entra ID: -```go -import ( - "github.com/redis/go-redis/v9" - "github.com/redis/go-redis-entraid" -) - -// Create an Entra ID credentials provider -provider := entraid.NewDefaultAzureIdentityProvider() - -// Configure Redis client with Entra ID authentication -rdb := redis.NewClient(&redis.Options{ - Addr: "your-redis-server.redis.cache.windows.net:6380", - StreamingCredentialsProvider: provider, - TLSConfig: &tls.Config{ - MinVersion: tls.VersionTLS12, - }, -}) -``` - -#### 2. Context-based Credentials Provider - -The context-based provider allows credentials to be determined at the time of each operation, using the context. - -```go -rdb := redis.NewClient(&redis.Options{ - Addr: "localhost:6379", - CredentialsProviderContext: func(ctx context.Context) (string, string, error) { - // Return username, password, and any error - return "user", "pass", nil - }, -}) -``` - -#### 3. Regular Credentials Provider - -A simple function-based provider that returns static credentials. - -```go -rdb := redis.NewClient(&redis.Options{ - Addr: "localhost:6379", - CredentialsProvider: func() (string, string) { - // Return username and password - return "user", "pass" - }, -}) -``` - -#### 4. Username/Password Fields (Lowest Priority) - -The most basic way to provide credentials is through the `Username` and `Password` fields in the options. - -```go -rdb := redis.NewClient(&redis.Options{ - Addr: "localhost:6379", - Username: "user", - Password: "pass", -}) -``` - -#### Priority Order - -The client will use credentials in the following priority order: -1. Streaming Credentials Provider (if set) -2. Context-based Credentials Provider (if set) -3. Regular Credentials Provider (if set) -4. Username/Password fields (if set) - -If none of these are set, the client will attempt to connect without authentication. - -### Protocol Version - -The client supports both RESP2 and RESP3 protocols. You can specify the protocol version in the options: - -```go -rdb := redis.NewClient(&redis.Options{ - Addr: "localhost:6379", - Password: "", // no password set - DB: 0, // use default DB - Protocol: 3, // specify 2 for RESP 2 or 3 for RESP 3 -}) -``` - -### Connecting via a redis url - -go-redis also supports connecting via the -[redis uri specification](https://github.com/redis/redis-specifications/tree/master/uri/redis.txt). -The example below demonstrates how the connection can easily be configured using a string, adhering -to this specification. - -```go -import ( - "github.com/redis/go-redis/v9" -) - -func ExampleClient() *redis.Client { - url := "redis://user:password@localhost:6379/0?protocol=3" - opts, err := redis.ParseURL(url) - if err != nil { - panic(err) - } - - return redis.NewClient(opts) -} - -``` - -### Instrument with OpenTelemetry - -```go -import ( - "github.com/redis/go-redis/v9" - "github.com/redis/go-redis/extra/redisotel/v9" - "errors" -) - -func main() { - ... - rdb := redis.NewClient(&redis.Options{...}) - - if err := errors.Join(redisotel.InstrumentTracing(rdb), redisotel.InstrumentMetrics(rdb)); err != nil { - log.Fatal(err) - } -``` - - -### Buffer Size Configuration - -go-redis uses 32KiB read and write buffers by default for optimal performance. For high-throughput applications or large pipelines, you can customize buffer sizes: - -```go -rdb := redis.NewClient(&redis.Options{ - Addr: "localhost:6379", - ReadBufferSize: 1024 * 1024, // 1MiB read buffer - WriteBufferSize: 1024 * 1024, // 1MiB write buffer -}) -``` - -### Advanced Configuration - -go-redis supports extending the client identification phase to allow projects to send their own custom client identification. - -#### Default Client Identification - -By default, go-redis automatically sends the client library name and version during the connection process. This feature is available in redis-server as of version 7.2. As a result, the command is "fire and forget", meaning it should fail silently, in the case that the redis server does not support this feature. - -#### Disabling Identity Verification - -When connection identity verification is not required or needs to be explicitly disabled, a `DisableIdentity` configuration option exists. -Initially there was a typo and the option was named `DisableIndentity` instead of `DisableIdentity`. The misspelled option is marked as Deprecated and will be removed in V10 of this library. -Although both options will work at the moment, the correct option is `DisableIdentity`. The deprecated option will be removed in V10 of this library, so please use the correct option name to avoid any issues. - -To disable verification, set the `DisableIdentity` option to `true` in the Redis client options: - -```go -rdb := redis.NewClient(&redis.Options{ - Addr: "localhost:6379", - Password: "", - DB: 0, - DisableIdentity: true, // Disable set-info on connect -}) -``` - -#### Unstable RESP3 Structures for RediSearch Commands -When integrating Redis with application functionalities using RESP3, it's important to note that some response structures aren't final yet. This is especially true for more complex structures like search and query results. We recommend using RESP2 when using the search and query capabilities, but we plan to stabilize the RESP3-based API-s in the coming versions. You can find more guidance in the upcoming release notes. - -To enable unstable RESP3, set the option in your client configuration: - -```go -redis.NewClient(&redis.Options{ - UnstableResp3: true, - }) -``` -**Note:** When UnstableResp3 mode is enabled, it's necessary to use RawResult() and RawVal() to retrieve a raw data. - Since, raw response is the only option for unstable search commands Val() and Result() calls wouldn't have any affect on them: - -```go -res1, err := client.FTSearchWithArgs(ctx, "txt", "foo bar", &redis.FTSearchOptions{}).RawResult() -val1 := client.FTSearchWithArgs(ctx, "txt", "foo bar", &redis.FTSearchOptions{}).RawVal() -``` - -#### Redis-Search Default Dialect - -In the Redis-Search module, **the default dialect is 2**. If needed, you can explicitly specify a different dialect using the appropriate configuration in your queries. - -**Important**: Be aware that the query dialect may impact the results returned. If needed, you can revert to a different dialect version by passing the desired dialect in the arguments of the command you want to execute. -For example: -``` - res2, err := rdb.FTSearchWithArgs(ctx, - "idx:bicycle", - "@pickup_zone:[CONTAINS $bike]", - &redis.FTSearchOptions{ - Params: map[string]interface{}{ - "bike": "POINT(-0.1278 51.5074)", - }, - DialectVersion: 3, - }, - ).Result() -``` -You can find further details in the [query dialect documentation](https://redis.io/docs/latest/develop/interact/search-and-query/advanced-concepts/dialects/). - -#### Custom buffer sizes -Prior to v9.12, the buffer size was the default go value of 4096 bytes. Starting from v9.12, -go-redis uses 32KiB read and write buffers by default for optimal performance. -For high-throughput applications or large pipelines, you can customize buffer sizes: - -```go -rdb := redis.NewClient(&redis.Options{ - Addr: "localhost:6379", - ReadBufferSize: 1024 * 1024, // 1MiB read buffer - WriteBufferSize: 1024 * 1024, // 1MiB write buffer -}) -``` - -**Important**: If you experience any issues with the default buffer sizes, please try setting them to the go default of 4096 bytes. - -## Contributing -We welcome contributions to the go-redis library! If you have a bug fix, feature request, or improvement, please open an issue or pull request on GitHub. -We appreciate your help in making go-redis better for everyone. -If you are interested in contributing to the go-redis library, please check out our [contributing guidelines](CONTRIBUTING.md) for more information on how to get started. - -## Look and feel - -Some corner cases: - -```go -// SET key value EX 10 NX -set, err := rdb.SetNX(ctx, "key", "value", 10*time.Second).Result() - -// SET key value keepttl NX -set, err := rdb.SetNX(ctx, "key", "value", redis.KeepTTL).Result() - -// SORT list LIMIT 0 2 ASC -vals, err := rdb.Sort(ctx, "list", &redis.Sort{Offset: 0, Count: 2, Order: "ASC"}).Result() - -// ZRANGEBYSCORE zset -inf +inf WITHSCORES LIMIT 0 2 -vals, err := rdb.ZRangeByScoreWithScores(ctx, "zset", &redis.ZRangeBy{ - Min: "-inf", - Max: "+inf", - Offset: 0, - Count: 2, -}).Result() - -// ZINTERSTORE out 2 zset1 zset2 WEIGHTS 2 3 AGGREGATE SUM -vals, err := rdb.ZInterStore(ctx, "out", &redis.ZStore{ - Keys: []string{"zset1", "zset2"}, - Weights: []int64{2, 3} -}).Result() - -// EVAL "return {KEYS[1],ARGV[1]}" 1 "key" "hello" -vals, err := rdb.Eval(ctx, "return {KEYS[1],ARGV[1]}", []string{"key"}, "hello").Result() - -// custom command -res, err := rdb.Do(ctx, "set", "key", "value").Result() -``` - -## Typed Errors - -go-redis provides typed error checking functions for common Redis errors: - -```go -// Cluster and replication errors -redis.IsLoadingError(err) // Redis is loading the dataset -redis.IsReadOnlyError(err) // Write to read-only replica -redis.IsClusterDownError(err) // Cluster is down -redis.IsTryAgainError(err) // Command should be retried -redis.IsMasterDownError(err) // Master is down -redis.IsMovedError(err) // Returns (address, true) if key moved -redis.IsAskError(err) // Returns (address, true) if key being migrated - -// Connection and resource errors -redis.IsMaxClientsError(err) // Maximum clients reached -redis.IsAuthError(err) // Authentication failed (NOAUTH, WRONGPASS, unauthenticated) -redis.IsPermissionError(err) // Permission denied (NOPERM) -redis.IsOOMError(err) // Out of memory (OOM) - -// Transaction errors -redis.IsExecAbortError(err) // Transaction aborted (EXECABORT) -``` - -### Error Wrapping in Hooks - -When wrapping errors in hooks, use custom error types with `Unwrap()` method (preferred) or `fmt.Errorf` with `%w`. Always call `cmd.SetErr()` to preserve error type information: - -```go -// Custom error type (preferred) -type AppError struct { - Code string - RequestID string - Err error -} - -func (e *AppError) Error() string { - return fmt.Sprintf("[%s] request_id=%s: %v", e.Code, e.RequestID, e.Err) -} - -func (e *AppError) Unwrap() error { - return e.Err -} - -// Hook implementation -func (h MyHook) ProcessHook(next redis.ProcessHook) redis.ProcessHook { - return func(ctx context.Context, cmd redis.Cmder) error { - err := next(ctx, cmd) - if err != nil { - // Wrap with custom error type - wrappedErr := &AppError{ - Code: "REDIS_ERROR", - RequestID: getRequestID(ctx), - Err: err, - } - cmd.SetErr(wrappedErr) - return wrappedErr // Return wrapped error to preserve it - } - return nil - } -} - -// Typed error detection works through wrappers -if redis.IsLoadingError(err) { - // Retry logic -} - -// Extract custom error if needed -var appErr *AppError -if errors.As(err, &appErr) { - log.Printf("Request: %s", appErr.RequestID) -} -``` - -Alternatively, use `fmt.Errorf` with `%w`: -```go -wrappedErr := fmt.Errorf("context: %w", err) -cmd.SetErr(wrappedErr) -``` - -### Pipeline Hook Example - -For pipeline operations, use `ProcessPipelineHook`: - -```go -type PipelineLoggingHook struct{} - -func (h PipelineLoggingHook) DialHook(next redis.DialHook) redis.DialHook { - return next -} - -func (h PipelineLoggingHook) ProcessHook(next redis.ProcessHook) redis.ProcessHook { - return next -} - -func (h PipelineLoggingHook) ProcessPipelineHook(next redis.ProcessPipelineHook) redis.ProcessPipelineHook { - return func(ctx context.Context, cmds []redis.Cmder) error { - start := time.Now() - - // Execute the pipeline - err := next(ctx, cmds) - - duration := time.Since(start) - log.Printf("Pipeline executed %d commands in %v", len(cmds), duration) - - // Process individual command errors - // Note: Individual command errors are already set on each cmd by the pipeline execution - for _, cmd := range cmds { - if cmdErr := cmd.Err(); cmdErr != nil { - // Check for specific error types using typed error functions - if redis.IsAuthError(cmdErr) { - log.Printf("Auth error in pipeline command %s: %v", cmd.Name(), cmdErr) - } else if redis.IsPermissionError(cmdErr) { - log.Printf("Permission error in pipeline command %s: %v", cmd.Name(), cmdErr) - } - - // Optionally wrap individual command errors to add context - // The wrapped error preserves type information through errors.As() - wrappedErr := fmt.Errorf("pipeline cmd %s failed: %w", cmd.Name(), cmdErr) - cmd.SetErr(wrappedErr) - } - } - - // Return the pipeline-level error (connection errors, etc.) - // You can wrap it if needed, or return it as-is - return err - } -} - -// Register the hook -rdb.AddHook(PipelineLoggingHook{}) - -// Use pipeline - errors are still properly typed -pipe := rdb.Pipeline() -pipe.Set(ctx, "key1", "value1", 0) -pipe.Get(ctx, "key2") -_, err := pipe.Exec(ctx) -``` - -## Run the test - -Recommended to use Docker, just need to run: -```shell -make test -``` - -## See also - -- [Golang ORM](https://bun.uptrace.dev) for PostgreSQL, MySQL, MSSQL, and SQLite -- [Golang PostgreSQL](https://bun.uptrace.dev/postgres/) -- [Golang HTTP router](https://bunrouter.uptrace.dev/) -- [Golang ClickHouse ORM](https://github.com/uptrace/go-clickhouse) - -## Contributors - -> The go-redis project was originally initiated by :star: [**uptrace/uptrace**](https://github.com/uptrace/uptrace). -> Uptrace is an open-source APM tool that supports distributed tracing, metrics, and logs. You can -> use it to monitor applications and set up automatic alerts to receive notifications via email, -> Slack, Telegram, and others. -> -> See [OpenTelemetry](https://github.com/redis/go-redis/tree/master/example/otel) example which -> demonstrates how you can use Uptrace to monitor go-redis. - -Thanks to all the people who already contributed! - - - - diff --git a/vendor/github.com/redis/go-redis/v9/RELEASE-NOTES.md b/vendor/github.com/redis/go-redis/v9/RELEASE-NOTES.md deleted file mode 100644 index 7b705ee68..000000000 --- a/vendor/github.com/redis/go-redis/v9/RELEASE-NOTES.md +++ /dev/null @@ -1,859 +0,0 @@ -# Release Notes - -# 9.18.0 (2026-02-16) - -## 🚀 Highlights - -### Redis 8.6 Support - -Added support for Redis 8.6, including new commands and features for streams idempotent production and HOTKEYS. - -### Smart Client Handoff (Maintenance Notifications) for Cluster - -This release introduces comprehensive support for Redis Cluster maintenance notifications via SMIGRATING/SMIGRATED push notifications. The client now automatically handles slot migrations by: -- **Relaxing timeouts during migration** (SMIGRATING) to prevent false failures -- **Triggering lazy cluster state reloads** upon completion (SMIGRATED) -- Enabling seamless operations during Redis Enterprise maintenance windows - -([#3643](https://github.com/redis/go-redis/pull/3643)) by [@ndyakov](https://github.com/ndyakov) - -### OpenTelemetry Native Metrics Support - -Added comprehensive OpenTelemetry metrics support following the [OpenTelemetry Database Client Semantic Conventions](https://opentelemetry.io/docs/specs/semconv/database/database-metrics/). The implementation uses a Bridge Pattern to keep the core library dependency-free while providing optional metrics instrumentation through the new `extra/redisotel-native` package. - -**Metric groups include:** -- Command metrics: Operation duration with retry tracking -- Connection basic: Connection count and creation time -- Resiliency: Errors, handoffs, timeout relaxation -- Connection advanced: Wait time and use time -- Pubsub metrics: Published and received messages -- Stream metrics: Processing duration and maintenance notifications - -([#3637](https://github.com/redis/go-redis/pull/3637)) by [@ofekshenawa](https://github.com/ofekshenawa) - -## ✨ New Features - -- **HOTKEYS Commands**: Added support for Redis HOTKEYS feature for identifying hot keys based on CPU consumption and network utilization ([#3695](https://github.com/redis/go-redis/pull/3695)) by [@ofekshenawa](https://github.com/ofekshenawa) -- **Streams Idempotent Production**: Added support for Redis 8.6+ Streams Idempotent Production with `ProducerID`, `IdempotentID`, `IdempotentAuto` in `XAddArgs` and new `XCFGSET` command ([#3693](https://github.com/redis/go-redis/pull/3693)) by [@ofekshenawa](https://github.com/ofekshenawa) -- **NaN Values for TimeSeries**: Added support for NaN (Not a Number) values in Redis time series commands ([#3687](https://github.com/redis/go-redis/pull/3687)) by [@ofekshenawa](https://github.com/ofekshenawa) -- **DialerRetries Options**: Added `DialerRetries` and `DialerRetryTimeout` to `ClusterOptions`, `RingOptions`, and `FailoverOptions` ([#3686](https://github.com/redis/go-redis/pull/3686)) by [@naveenchander30](https://github.com/naveenchander30) -- **ConnMaxLifetimeJitter**: Added jitter configuration to distribute connection expiration times and prevent thundering herd ([#3666](https://github.com/redis/go-redis/pull/3666)) by [@cyningsun](https://github.com/cyningsun) -- **Digest Helper Functions**: Added `DigestString` and `DigestBytes` helper functions for client-side xxh3 hashing compatible with Redis DIGEST command ([#3679](https://github.com/redis/go-redis/pull/3679)) by [@ofekshenawa](https://github.com/ofekshenawa) -- **SMIGRATED New Format**: Updated SMIGRATED parser to support new format and remember original host:port ([#3697](https://github.com/redis/go-redis/pull/3697)) by [@ndyakov](https://github.com/ndyakov) -- **Cluster State Reload Interval**: Added cluster state reload interval option for maintenance notifications ([#3663](https://github.com/redis/go-redis/pull/3663)) by [@ndyakov](https://github.com/ndyakov) - -## 🐛 Bug Fixes - -- **PubSub nil pointer dereference**: Fixed nil pointer dereference in PubSub after `WithTimeout()` - `pubSubPool` is now properly cloned ([#3710](https://github.com/redis/go-redis/pull/3710)) by [@Copilot](https://github.com/apps/copilot-swe-agent) -- **MaintNotificationsConfig nil check**: Guard against nil `MaintNotificationsConfig` in `initConn` ([#3707](https://github.com/redis/go-redis/pull/3707)) by [@veeceey](https://github.com/veeceey) -- **wantConnQueue zombie elements**: Fixed zombie `wantConn` elements accumulation in `wantConnQueue` ([#3680](https://github.com/redis/go-redis/pull/3680)) by [@cyningsun](https://github.com/cyningsun) -- **XADD/XTRIM approx flag**: Fixed XADD and XTRIM to use `=` when approx is false ([#3684](https://github.com/redis/go-redis/pull/3684)) by [@ndyakov](https://github.com/ndyakov) -- **Sentinel timeout retry**: When connection to a sentinel times out, attempt to connect to other sentinels ([#3654](https://github.com/redis/go-redis/pull/3654)) by [@cxljs](https://github.com/cxljs) - -## ⚡ Performance - -- **Fuzz test optimization**: Eliminated repeated string conversions, used functional approach for cleaner operation selection ([#3692](https://github.com/redis/go-redis/pull/3692)) by [@feiguoL](https://github.com/feiguoL) -- **Pre-allocate capacity**: Pre-allocate slice capacity to prevent multiple capacity expansions ([#3689](https://github.com/redis/go-redis/pull/3689)) by [@feelshu](https://github.com/feelshu) - -## 🧪 Testing - -- **Comprehensive TLS tests**: Added comprehensive TLS tests and example for standalone, cluster, and certificate authentication ([#3681](https://github.com/redis/go-redis/pull/3681)) by [@ndyakov](https://github.com/ndyakov) -- **Redis 8.6**: Updated CI to use Redis 8.6-pre ([#3685](https://github.com/redis/go-redis/pull/3685)) by [@ndyakov](https://github.com/ndyakov) - -## 🧰 Maintenance - -- **Deprecation warnings**: Added deprecation warnings for commands based on Redis documentation ([#3673](https://github.com/redis/go-redis/pull/3673)) by [@ndyakov](https://github.com/ndyakov) -- **Use errors.Join()**: Replaced custom error join function with standard library `errors.Join()` ([#3653](https://github.com/redis/go-redis/pull/3653)) by [@cxljs](https://github.com/cxljs) -- **Use Go 1.21 min/max**: Use Go 1.21's built-in min/max functions ([#3656](https://github.com/redis/go-redis/pull/3656)) by [@cxljs](https://github.com/cxljs) -- **Proper formatting**: Code formatting improvements ([#3670](https://github.com/redis/go-redis/pull/3670)) by [@12ya](https://github.com/12ya) -- **Set commands documentation**: Added comprehensive documentation to all set command methods ([#3642](https://github.com/redis/go-redis/pull/3642)) by [@iamamirsalehi](https://github.com/iamamirsalehi) -- **MaxActiveConns docs**: Added default value documentation for `MaxActiveConns` ([#3674](https://github.com/redis/go-redis/pull/3674)) by [@codykaup](https://github.com/codykaup) -- **README example update**: Updated README example ([#3657](https://github.com/redis/go-redis/pull/3657)) by [@cxljs](https://github.com/cxljs) -- **Cluster maintnotif example**: Added example application for cluster maintenance notifications ([#3651](https://github.com/redis/go-redis/pull/3651)) by [@ndyakov](https://github.com/ndyakov) - -## 👥 Contributors - -We'd like to thank all the contributors who worked on this release! - -[@12ya](https://github.com/12ya), [@Copilot](https://github.com/apps/copilot-swe-agent), [@codykaup](https://github.com/codykaup), [@cxljs](https://github.com/cxljs), [@cyningsun](https://github.com/cyningsun), [@feelshu](https://github.com/feelshu), [@feiguoL](https://github.com/feiguoL), [@iamamirsalehi](https://github.com/iamamirsalehi), [@naveenchander30](https://github.com/naveenchander30), [@ndyakov](https://github.com/ndyakov), [@ofekshenawa](https://github.com/ofekshenawa), [@veeceey](https://github.com/veeceey) - ---- - -**Full Changelog**: https://github.com/redis/go-redis/compare/v9.17.0...v9.18.0 - -# 9.18.0-beta.2 (2025-12-09) - -## 🚀 Highlights - -### Go Version Update - -This release updates the minimum required Go version to 1.21. This is part of a gradual migration strategy where the minimum supported Go version will be three versions behind the latest release. With each new Go version release, we will bump the minimum version by one, ensuring compatibility while staying current with the Go ecosystem. - -### Stability Improvements - -This release includes several important stability fixes: -- Fixed a critical panic in the handoff worker manager that could occur when handling nil errors -- Improved test reliability for Smart Client Handoff functionality -- Fixed logging format issues that could cause runtime errors - -## ✨ New Features - -- OpenTelemetry metrics improvements for nil response handling ([#3638](https://github.com/redis/go-redis/pull/3638)) by [@fengve](https://github.com/fengve) - -## 🐛 Bug Fixes - -- Fixed panic on nil error in handoffWorkerManager closeConnFromRequest ([#3633](https://github.com/redis/go-redis/pull/3633)) by [@ccoVeille](https://github.com/ccoVeille) -- Fixed bad sprintf syntax in logging ([#3632](https://github.com/redis/go-redis/pull/3632)) by [@ccoVeille](https://github.com/ccoVeille) - -## 🧰 Maintenance - -- Updated minimum Go version to 1.21 ([#3640](https://github.com/redis/go-redis/pull/3640)) by [@ndyakov](https://github.com/ndyakov) -- Use Go 1.20 idiomatic string<->byte conversion ([#3435](https://github.com/redis/go-redis/pull/3435)) by [@justinhwang](https://github.com/justinhwang) -- Reduce flakiness of Smart Client Handoff test ([#3641](https://github.com/redis/go-redis/pull/3641)) by [@kiryazovi-redis](https://github.com/kiryazovi-redis) -- Revert PR #3634 (Observability metrics phase1) ([#3635](https://github.com/redis/go-redis/pull/3635)) by [@ofekshenawa](https://github.com/ofekshenawa) - -## 👥 Contributors - -We'd like to thank all the contributors who worked on this release! - -[@justinhwang](https://github.com/justinhwang), [@ndyakov](https://github.com/ndyakov), [@kiryazovi-redis](https://github.com/kiryazovi-redis), [@fengve](https://github.com/fengve), [@ccoVeille](https://github.com/ccoVeille), [@ofekshenawa](https://github.com/ofekshenawa) - ---- - -**Full Changelog**: https://github.com/redis/go-redis/compare/v9.18.0-beta.1...v9.18.0-beta.2 - -# 9.18.0-beta.1 (2025-12-01) - -## 🚀 Highlights - -### Request and Response Policy Based Routing in Cluster Mode - -This beta release introduces comprehensive support for Redis COMMAND-based request and response policy routing for cluster clients. This feature enables intelligent command routing and response aggregation based on Redis command metadata. - -**Key Features:** -- **Command Policy Loader**: Automatically parses and caches COMMAND metadata with routing/aggregation hints -- **Enhanced Routing Engine**: Supports all request policies including: - - `default(keyless)` - Commands without keys - - `default(hashslot)` - Commands with hash slot routing - - `all_shards` - Commands that need to run on all shards - - `all_nodes` - Commands that need to run on all nodes - - `multi_shard` - Commands that span multiple shards - - `special` - Commands with custom routing logic -- **Response Aggregator**: Intelligently combines multi-shard replies based on response policies: - - `all_succeeded` - All shards must succeed - - `one_succeeded` - At least one shard must succeed - - `agg_sum` - Aggregate numeric responses - - `special` - Custom aggregation logic (e.g., FT.CURSOR) -- **Raw Command Support**: Policies are enforced on `Client.Do(ctx, args...)` - -This feature is particularly useful for Redis Stack commands like RediSearch that need to operate across multiple shards in a cluster. - -### Connection Pool Improvements - -Fixed a critical defect in the connection pool's turn management mechanism that could lead to connection leaks under certain conditions. The fix ensures proper 1:1 correspondence between turns and connections. - -## ✨ New Features - -- Request and Response Policy Based Routing in Cluster Mode ([#3422](https://github.com/redis/go-redis/pull/3422)) by [@ofekshenawa](https://github.com/ofekshenawa) - -## 🐛 Bug Fixes - -- Fixed connection pool turn management to prevent connection leaks ([#3626](https://github.com/redis/go-redis/pull/3626)) by [@cyningsun](https://github.com/cyningsun) - -## 🧰 Maintenance - -- chore(deps): bump rojopolis/spellcheck-github-actions from 0.54.0 to 0.55.0 ([#3627](https://github.com/redis/go-redis/pull/3627)) - -## 👥 Contributors - -We'd like to thank all the contributors who worked on this release! - -[@cyningsun](https://github.com/cyningsun), [@ofekshenawa](https://github.com/ofekshenawa), [@ndyakov](https://github.com/ndyakov) - ---- - -**Full Changelog**: https://github.com/redis/go-redis/compare/v9.17.1...v9.18.0-beta.1 - -# 9.17.1 (2025-11-25) - -## 🐛 Bug Fixes - -- add wait to keyless commands list ([#3615](https://github.com/redis/go-redis/pull/3615)) by [@marcoferrer](https://github.com/marcoferrer) -- fix(time): remove cached time optimization ([#3611](https://github.com/redis/go-redis/pull/3611)) by [@ndyakov](https://github.com/ndyakov) - -## 🧰 Maintenance - -- chore(deps): bump golangci/golangci-lint-action from 9.0.0 to 9.1.0 ([#3609](https://github.com/redis/go-redis/pull/3609)) -- chore(deps): bump actions/checkout from 5 to 6 ([#3610](https://github.com/redis/go-redis/pull/3610)) -- chore(script): fix help call in tag.sh ([#3606](https://github.com/redis/go-redis/pull/3606)) by [@ndyakov](https://github.com/ndyakov) - -## Contributors -We'd like to thank all the contributors who worked on this release! - -[@marcoferrer](https://github.com/marcoferrer) and [@ndyakov](https://github.com/ndyakov) - ---- - -**Full Changelog**: https://github.com/redis/go-redis/compare/v9.17.0...v9.17.1 - -# 9.17.0 (2025-11-19) - -## 🚀 Highlights - -### Redis 8.4 Support -Added support for Redis 8.4, including new commands and features ([#3572](https://github.com/redis/go-redis/pull/3572)) - -### Typed Errors -Introduced typed errors for better error handling using `errors.As` instead of string checks. Errors can now be wrapped and set to commands in hooks without breaking library functionality ([#3602](https://github.com/redis/go-redis/pull/3602)) - -### New Commands -- **CAS/CAD Commands**: Added support for Compare-And-Set/Compare-And-Delete operations with conditional matching (`IFEQ`, `IFNE`, `IFDEQ`, `IFDNE`) ([#3583](https://github.com/redis/go-redis/pull/3583), [#3595](https://github.com/redis/go-redis/pull/3595)) -- **MSETEX**: Atomically set multiple key-value pairs with expiration options and conditional modes ([#3580](https://github.com/redis/go-redis/pull/3580)) -- **XReadGroup CLAIM**: Consume both incoming and idle pending entries from streams in a single call ([#3578](https://github.com/redis/go-redis/pull/3578)) -- **ACL Commands**: Added `ACLGenPass`, `ACLUsers`, and `ACLWhoAmI` ([#3576](https://github.com/redis/go-redis/pull/3576)) -- **SLOWLOG Commands**: Added `SLOWLOG LEN` and `SLOWLOG RESET` ([#3585](https://github.com/redis/go-redis/pull/3585)) -- **LATENCY Commands**: Added `LATENCY LATEST` and `LATENCY RESET` ([#3584](https://github.com/redis/go-redis/pull/3584)) - -### Search & Vector Improvements -- **Hybrid Search**: Added **EXPERIMENTAL** support for the new `FT.HYBRID` command ([#3573](https://github.com/redis/go-redis/pull/3573)) -- **Vector Range**: Added `VRANGE` command for vector sets ([#3543](https://github.com/redis/go-redis/pull/3543)) -- **FT.INFO Enhancements**: Added vector-specific attributes in FT.INFO response ([#3596](https://github.com/redis/go-redis/pull/3596)) - -### Connection Pool Improvements -- **Improved Connection Success Rate**: Implemented FIFO queue-based fairness and context pattern for connection creation to prevent premature cancellation under high concurrency ([#3518](https://github.com/redis/go-redis/pull/3518)) -- **Connection State Machine**: Resolved race conditions and improved pool performance with proper state tracking ([#3559](https://github.com/redis/go-redis/pull/3559)) -- **Pool Performance**: Significant performance improvements with faster semaphores, lockless hook manager, and reduced allocations (47-67% faster Get/Put operations) ([#3565](https://github.com/redis/go-redis/pull/3565)) - -### Metrics & Observability -- **Canceled Metric Attribute**: Added 'canceled' metrics attribute to distinguish context cancellation errors from other errors ([#3566](https://github.com/redis/go-redis/pull/3566)) - -## ✨ New Features - -- Typed errors with wrapping support ([#3602](https://github.com/redis/go-redis/pull/3602)) by [@ndyakov](https://github.com/ndyakov) -- CAS/CAD commands (marked as experimental) ([#3583](https://github.com/redis/go-redis/pull/3583), [#3595](https://github.com/redis/go-redis/pull/3595)) by [@ndyakov](https://github.com/ndyakov), [@htemelski-redis](https://github.com/htemelski-redis) -- MSETEX command support ([#3580](https://github.com/redis/go-redis/pull/3580)) by [@ofekshenawa](https://github.com/ofekshenawa) -- XReadGroup CLAIM argument ([#3578](https://github.com/redis/go-redis/pull/3578)) by [@ofekshenawa](https://github.com/ofekshenawa) -- ACL commands: GenPass, Users, WhoAmI ([#3576](https://github.com/redis/go-redis/pull/3576)) by [@destinyoooo](https://github.com/destinyoooo) -- SLOWLOG commands: LEN, RESET ([#3585](https://github.com/redis/go-redis/pull/3585)) by [@destinyoooo](https://github.com/destinyoooo) -- LATENCY commands: LATEST, RESET ([#3584](https://github.com/redis/go-redis/pull/3584)) by [@destinyoooo](https://github.com/destinyoooo) -- Hybrid search command (FT.HYBRID) ([#3573](https://github.com/redis/go-redis/pull/3573)) by [@htemelski-redis](https://github.com/htemelski-redis) -- Vector range command (VRANGE) ([#3543](https://github.com/redis/go-redis/pull/3543)) by [@cxljs](https://github.com/cxljs) -- Vector-specific attributes in FT.INFO ([#3596](https://github.com/redis/go-redis/pull/3596)) by [@ndyakov](https://github.com/ndyakov) -- Improved connection pool success rate with FIFO queue ([#3518](https://github.com/redis/go-redis/pull/3518)) by [@cyningsun](https://github.com/cyningsun) -- Canceled metrics attribute for context errors ([#3566](https://github.com/redis/go-redis/pull/3566)) by [@pvragov](https://github.com/pvragov) - -## 🐛 Bug Fixes - -- Fixed Failover Client MaintNotificationsConfig ([#3600](https://github.com/redis/go-redis/pull/3600)) by [@ajax16384](https://github.com/ajax16384) -- Fixed ACLGenPass function to use the bit parameter ([#3597](https://github.com/redis/go-redis/pull/3597)) by [@destinyoooo](https://github.com/destinyoooo) -- Return error instead of panic from commands ([#3568](https://github.com/redis/go-redis/pull/3568)) by [@dragneelfps](https://github.com/dragneelfps) -- Safety harness in `joinErrors` to prevent panic ([#3577](https://github.com/redis/go-redis/pull/3577)) by [@manisharma](https://github.com/manisharma) - -## ⚡ Performance - -- Connection state machine with race condition fixes ([#3559](https://github.com/redis/go-redis/pull/3559)) by [@ndyakov](https://github.com/ndyakov) -- Pool performance improvements: 47-67% faster Get/Put, 33% less memory, 50% fewer allocations ([#3565](https://github.com/redis/go-redis/pull/3565)) by [@ndyakov](https://github.com/ndyakov) - -## 🧪 Testing & Infrastructure - -- Updated to Redis 8.4.0 image ([#3603](https://github.com/redis/go-redis/pull/3603)) by [@ndyakov](https://github.com/ndyakov) -- Added Redis 8.4-RC1-pre to CI ([#3572](https://github.com/redis/go-redis/pull/3572)) by [@ndyakov](https://github.com/ndyakov) -- Refactored tests for idiomatic Go ([#3561](https://github.com/redis/go-redis/pull/3561), [#3562](https://github.com/redis/go-redis/pull/3562), [#3563](https://github.com/redis/go-redis/pull/3563)) by [@12ya](https://github.com/12ya) - -## 👥 Contributors - -We'd like to thank all the contributors who worked on this release! - -[@12ya](https://github.com/12ya), [@ajax16384](https://github.com/ajax16384), [@cxljs](https://github.com/cxljs), [@cyningsun](https://github.com/cyningsun), [@destinyoooo](https://github.com/destinyoooo), [@dragneelfps](https://github.com/dragneelfps), [@htemelski-redis](https://github.com/htemelski-redis), [@manisharma](https://github.com/manisharma), [@ndyakov](https://github.com/ndyakov), [@ofekshenawa](https://github.com/ofekshenawa), [@pvragov](https://github.com/pvragov) - ---- - -**Full Changelog**: https://github.com/redis/go-redis/compare/v9.16.0...v9.17.0 - -# 9.16.0 (2025-10-23) - -## 🚀 Highlights - -### Maintenance Notifications Support - -This release introduces comprehensive support for Redis maintenance notifications, enabling applications to handle server maintenance events gracefully. The new `maintnotifications` package provides: - -- **RESP3 Push Notifications**: Full support for Redis RESP3 protocol push notifications -- **Connection Handoff**: Automatic connection migration during server maintenance with configurable retry policies and circuit breakers -- **Graceful Degradation**: Configurable timeout relaxation during maintenance windows to prevent false failures -- **Event-Driven Architecture**: Background workers with on-demand scaling for efficient handoff processing -- **Production-Ready**: Comprehensive E2E testing framework and monitoring capabilities - -For detailed usage examples and configuration options, see the [maintenance notifications documentation](maintnotifications/README.md). - -## ✨ New Features - -- **Trace Filtering**: Add support for filtering traces for specific commands, including pipeline operations and dial operations ([#3519](https://github.com/redis/go-redis/pull/3519), [#3550](https://github.com/redis/go-redis/pull/3550)) - - New `TraceCmdFilter` option to selectively trace commands - - Reduces overhead by excluding high-frequency or low-value commands from traces - -## 🐛 Bug Fixes - -- **Pipeline Error Handling**: Fix issue where pipeline repeatedly sets the same error ([#3525](https://github.com/redis/go-redis/pull/3525)) -- **Connection Pool**: Ensure re-authentication does not interfere with connection handoff operations ([#3547](https://github.com/redis/go-redis/pull/3547)) - -## 🔧 Improvements - -- **Hash Commands**: Update hash command implementations ([#3523](https://github.com/redis/go-redis/pull/3523)) -- **OpenTelemetry**: Use `metric.WithAttributeSet` to avoid unnecessary attribute copying in redisotel ([#3552](https://github.com/redis/go-redis/pull/3552)) - -## 📚 Documentation - -- **Cluster Client**: Add explanation for why `MaxRetries` is disabled for `ClusterClient` ([#3551](https://github.com/redis/go-redis/pull/3551)) - -## 🧪 Testing & Infrastructure - -- **E2E Testing**: Upgrade E2E testing framework with improved reliability and coverage ([#3541](https://github.com/redis/go-redis/pull/3541)) -- **Release Process**: Improved resiliency of the release process ([#3530](https://github.com/redis/go-redis/pull/3530)) - -## 📦 Dependencies - -- Bump `rojopolis/spellcheck-github-actions` from 0.51.0 to 0.52.0 ([#3520](https://github.com/redis/go-redis/pull/3520)) -- Bump `github/codeql-action` from 3 to 4 ([#3544](https://github.com/redis/go-redis/pull/3544)) - -## 👥 Contributors - -We'd like to thank all the contributors who worked on this release! - -[@ndyakov](https://github.com/ndyakov), [@htemelski-redis](https://github.com/htemelski-redis), [@Sovietaced](https://github.com/Sovietaced), [@Udhayarajan](https://github.com/Udhayarajan), [@boekkooi-impossiblecloud](https://github.com/boekkooi-impossiblecloud), [@Pika-Gopher](https://github.com/Pika-Gopher), [@cxljs](https://github.com/cxljs), [@huiyifyj](https://github.com/huiyifyj), [@omid-h70](https://github.com/omid-h70) - ---- - -**Full Changelog**: https://github.com/redis/go-redis/compare/v9.14.0...v9.16.0 - - -# 9.15.0 was accidentally released. Please use version 9.16.0 instead. - -# 9.15.0-beta.3 (2025-09-26) - -## Highlights -This beta release includes a pre-production version of processing push notifications and hitless upgrades. - -# Changes - -- chore: Update hash_commands.go ([#3523](https://github.com/redis/go-redis/pull/3523)) - -## 🚀 New Features - -- feat: RESP3 notifications support & Hitless notifications handling ([#3418](https://github.com/redis/go-redis/pull/3418)) - -## 🐛 Bug Fixes - -- fix: pipeline repeatedly sets the error ([#3525](https://github.com/redis/go-redis/pull/3525)) - -## 🧰 Maintenance - -- chore(deps): bump rojopolis/spellcheck-github-actions from 0.51.0 to 0.52.0 ([#3520](https://github.com/redis/go-redis/pull/3520)) -- feat(e2e-testing): maintnotifications e2e and refactor ([#3526](https://github.com/redis/go-redis/pull/3526)) -- feat(tag.sh): Improved resiliency of the release process ([#3530](https://github.com/redis/go-redis/pull/3530)) - -## Contributors -We'd like to thank all the contributors who worked on this release! - -[@cxljs](https://github.com/cxljs), [@ndyakov](https://github.com/ndyakov), [@htemelski-redis](https://github.com/htemelski-redis), and [@omid-h70](https://github.com/omid-h70) - - -# 9.15.0-beta.1 (2025-09-10) - -## Highlights -This beta release includes a pre-production version of processing push notifications and hitless upgrades. - -### Hitless Upgrades -Hitless upgrades is a major new feature that allows for zero-downtime upgrades in Redis clusters. -You can find more information in the [Hitless Upgrades documentation](https://github.com/redis/go-redis/tree/master/hitless). - -# Changes - -## 🚀 New Features -- [CAE-1088] & [CAE-1072] feat: RESP3 notifications support & Hitless notifications handling ([#3418](https://github.com/redis/go-redis/pull/3418)) - -## Contributors -We'd like to thank all the contributors who worked on this release! - -[@ndyakov](https://github.com/ndyakov), [@htemelski-redis](https://github.com/htemelski-redis), [@ofekshenawa](https://github.com/ofekshenawa) - - -# 9.14.0 (2025-09-10) - -## Highlights -- Added batch process method to the pipeline ([#3510](https://github.com/redis/go-redis/pull/3510)) - -# Changes - -## 🚀 New Features - -- Added batch process method to the pipeline ([#3510](https://github.com/redis/go-redis/pull/3510)) - -## 🐛 Bug Fixes - -- fix: SetErr on Cmd if the command cannot be queued correctly in multi/exec ([#3509](https://github.com/redis/go-redis/pull/3509)) - -## 🧰 Maintenance - -- Updates release drafter config to exclude dependabot ([#3511](https://github.com/redis/go-redis/pull/3511)) -- chore(deps): bump actions/setup-go from 5 to 6 ([#3504](https://github.com/redis/go-redis/pull/3504)) - -## Contributors -We'd like to thank all the contributors who worked on this release! - -[@elena-kolevska](https://github.com/elena-kolevksa), [@htemelski-redis](https://github.com/htemelski-redis) and [@ndyakov](https://github.com/ndyakov) - - -# 9.13.0 (2025-09-03) - -## Highlights -- Pipeliner expose queued commands ([#3496](https://github.com/redis/go-redis/pull/3496)) -- Ensure that JSON.GET returns Nil response ([#3470](https://github.com/redis/go-redis/pull/3470)) -- Fixes on Read and Write buffer sizes and UniversalOptions - -## Changes -- Pipeliner expose queued commands ([#3496](https://github.com/redis/go-redis/pull/3496)) -- fix(test): fix a timing issue in pubsub test ([#3498](https://github.com/redis/go-redis/pull/3498)) -- Allow users to enable read-write splitting in failover mode. ([#3482](https://github.com/redis/go-redis/pull/3482)) -- Set the read/write buffer size of the sentinel client to 4KiB ([#3476](https://github.com/redis/go-redis/pull/3476)) - -## 🚀 New Features - -- fix(otel): register wait metrics ([#3499](https://github.com/redis/go-redis/pull/3499)) -- Support subscriptions against cluster slave nodes ([#3480](https://github.com/redis/go-redis/pull/3480)) -- Add wait metrics to otel ([#3493](https://github.com/redis/go-redis/pull/3493)) -- Clean failing timeout implementation ([#3472](https://github.com/redis/go-redis/pull/3472)) - -## 🐛 Bug Fixes - -- Do not assume that all non-IP hosts are loopbacks ([#3085](https://github.com/redis/go-redis/pull/3085)) -- Ensure that JSON.GET returns Nil response ([#3470](https://github.com/redis/go-redis/pull/3470)) - -## 🧰 Maintenance - -- fix(otel): register wait metrics ([#3499](https://github.com/redis/go-redis/pull/3499)) -- fix(make test): Add default env in makefile ([#3491](https://github.com/redis/go-redis/pull/3491)) -- Update the introduction to running tests in README.md ([#3495](https://github.com/redis/go-redis/pull/3495)) -- test: Add comprehensive edge case tests for IncrByFloat command ([#3477](https://github.com/redis/go-redis/pull/3477)) -- Set the default read/write buffer size of Redis connection to 32KiB ([#3483](https://github.com/redis/go-redis/pull/3483)) -- Bumps test image to 8.2.1-pre ([#3478](https://github.com/redis/go-redis/pull/3478)) -- fix UniversalOptions miss ReadBufferSize and WriteBufferSize options ([#3485](https://github.com/redis/go-redis/pull/3485)) -- chore(deps): bump actions/checkout from 4 to 5 ([#3484](https://github.com/redis/go-redis/pull/3484)) -- Removes dry run for stale issues policy ([#3471](https://github.com/redis/go-redis/pull/3471)) -- Update otel metrics URL ([#3474](https://github.com/redis/go-redis/pull/3474)) - -## Contributors -We'd like to thank all the contributors who worked on this release! - -[@LINKIWI](https://github.com/LINKIWI), [@cxljs](https://github.com/cxljs), [@cybersmeashish](https://github.com/cybersmeashish), [@elena-kolevska](https://github.com/elena-kolevska), [@htemelski-redis](https://github.com/htemelski-redis), [@mwhooker](https://github.com/mwhooker), [@ndyakov](https://github.com/ndyakov), [@ofekshenawa](https://github.com/ofekshenawa), [@suever](https://github.com/suever) - - -# 9.12.1 (2025-08-11) -## 🚀 Highlights -In the last version (9.12.0) the client introduced bigger write and read buffer sized. The default value we set was 512KiB. -However, users reported that this is too big for most use cases and can lead to high memory usage. -In this version the default value is changed to 256KiB. The `README.md` was updated to reflect the -correct default value and include a note that the default value can be changed. - -## 🐛 Bug Fixes - -- fix(options): Add buffer sizes to failover. Update README ([#3468](https://github.com/redis/go-redis/pull/3468)) - -## 🧰 Maintenance - -- fix(options): Add buffer sizes to failover. Update README ([#3468](https://github.com/redis/go-redis/pull/3468)) -- chore: update & fix otel example ([#3466](https://github.com/redis/go-redis/pull/3466)) - -## Contributors -We'd like to thank all the contributors who worked on this release! - -[@ndyakov](https://github.com/ndyakov) and [@vmihailenco](https://github.com/vmihailenco) - -# 9.12.0 (2025-08-05) - -## 🚀 Highlights - -- This release includes support for [Redis 8.2](https://redis.io/docs/latest/operate/oss_and_stack/stack-with-enterprise/release-notes/redisce/redisos-8.2-release-notes/). -- Introduces an experimental Query Builders for `FTSearch`, `FTAggregate` and other search commands. -- Adds support for `EPSILON` option in `FT.VSIM`. -- Includes bug fixes and improvements contributed by the community related to ring and [redisotel](https://github.com/redis/go-redis/tree/master/extra/redisotel). - -## Changes -- Improve stale issue workflow ([#3458](https://github.com/redis/go-redis/pull/3458)) -- chore(ci): Add 8.2 rc2 pre build for CI ([#3459](https://github.com/redis/go-redis/pull/3459)) -- Added new stream commands ([#3450](https://github.com/redis/go-redis/pull/3450)) -- feat: Add "skip_verify" to Sentinel ([#3428](https://github.com/redis/go-redis/pull/3428)) -- fix: `errors.Join` requires Go 1.20 or later ([#3442](https://github.com/redis/go-redis/pull/3442)) -- DOC-4344 document quickstart examples ([#3426](https://github.com/redis/go-redis/pull/3426)) -- feat(bitop): add support for the new bitop operations ([#3409](https://github.com/redis/go-redis/pull/3409)) - -## 🚀 New Features - -- feat: recover addIdleConn may occur panic ([#2445](https://github.com/redis/go-redis/pull/2445)) -- feat(ring): specify custom health check func via HeartbeatFn option ([#2940](https://github.com/redis/go-redis/pull/2940)) -- Add Query Builder for RediSearch commands ([#3436](https://github.com/redis/go-redis/pull/3436)) -- add configurable buffer sizes for Redis connections ([#3453](https://github.com/redis/go-redis/pull/3453)) -- Add VAMANA vector type to RediSearch ([#3449](https://github.com/redis/go-redis/pull/3449)) -- VSIM add `EPSILON` option ([#3454](https://github.com/redis/go-redis/pull/3454)) -- Add closing support to otel metrics instrumentation ([#3444](https://github.com/redis/go-redis/pull/3444)) - -## 🐛 Bug Fixes - -- fix(redisotel): fix buggy append in reportPoolStats ([#3122](https://github.com/redis/go-redis/pull/3122)) -- fix(search): return results even if doc is empty ([#3457](https://github.com/redis/go-redis/pull/3457)) -- [ISSUE-3402]: Ring.Pipelined return dial timeout error ([#3403](https://github.com/redis/go-redis/pull/3403)) - -## 🧰 Maintenance - -- Merges stale issues jobs into one job with two steps ([#3463](https://github.com/redis/go-redis/pull/3463)) -- improve code readability ([#3446](https://github.com/redis/go-redis/pull/3446)) -- chore(release): 9.12.0-beta.1 ([#3460](https://github.com/redis/go-redis/pull/3460)) -- DOC-5472 time series doc examples ([#3443](https://github.com/redis/go-redis/pull/3443)) -- Add VAMANA compression algorithm tests ([#3461](https://github.com/redis/go-redis/pull/3461)) -- bumped redis 8.2 version used in the CI/CD ([#3451](https://github.com/redis/go-redis/pull/3451)) - -## Contributors -We'd like to thank all the contributors who worked on this release! - -[@andy-stark-redis](https://github.com/andy-stark-redis), [@cxljs](https://github.com/cxljs), [@elena-kolevska](https://github.com/elena-kolevska), [@htemelski-redis](https://github.com/htemelski-redis), [@jouir](https://github.com/jouir), [@monkey92t](https://github.com/monkey92t), [@ndyakov](https://github.com/ndyakov), [@ofekshenawa](https://github.com/ofekshenawa), [@rokn](https://github.com/rokn), [@smnvdev](https://github.com/smnvdev), [@strobil](https://github.com/strobil) and [@wzy9607](https://github.com/wzy9607) - -## New Contributors -* [@htemelski-redis](https://github.com/htemelski-redis) made their first contribution in [#3409](https://github.com/redis/go-redis/pull/3409) -* [@smnvdev](https://github.com/smnvdev) made their first contribution in [#3403](https://github.com/redis/go-redis/pull/3403) -* [@rokn](https://github.com/rokn) made their first contribution in [#3444](https://github.com/redis/go-redis/pull/3444) - -# 9.11.0 (2025-06-24) - -## 🚀 Highlights - -Fixes TxPipeline to work correctly in cluster scenarios, allowing execution of commands -only in the same slot. - -# Changes - -## 🚀 New Features - -- Set cluster slot for `scan` commands, rather than random ([#2623](https://github.com/redis/go-redis/pull/2623)) -- Add CredentialsProvider field to UniversalOptions ([#2927](https://github.com/redis/go-redis/pull/2927)) -- feat(redisotel): add WithCallerEnabled option ([#3415](https://github.com/redis/go-redis/pull/3415)) - -## 🐛 Bug Fixes - -- fix(txpipeline): keyless commands should take the slot of the keyed ([#3411](https://github.com/redis/go-redis/pull/3411)) -- fix(loading): cache the loaded flag for slave nodes ([#3410](https://github.com/redis/go-redis/pull/3410)) -- fix(txpipeline): should return error on multi/exec on multiple slots ([#3408](https://github.com/redis/go-redis/pull/3408)) -- fix: check if the shard exists to avoid returning nil ([#3396](https://github.com/redis/go-redis/pull/3396)) - -## 🧰 Maintenance - -- feat: optimize connection pool waitTurn ([#3412](https://github.com/redis/go-redis/pull/3412)) -- chore(ci): update CI redis builds ([#3407](https://github.com/redis/go-redis/pull/3407)) -- chore: remove a redundant method from `Ring`, `Client` and `ClusterClient` ([#3401](https://github.com/redis/go-redis/pull/3401)) -- test: refactor TestBasicCredentials using table-driven tests ([#3406](https://github.com/redis/go-redis/pull/3406)) -- perf: reduce unnecessary memory allocation operations ([#3399](https://github.com/redis/go-redis/pull/3399)) -- fix: insert entry during iterating over a map ([#3398](https://github.com/redis/go-redis/pull/3398)) -- DOC-5229 probabilistic data type examples ([#3413](https://github.com/redis/go-redis/pull/3413)) -- chore(deps): bump rojopolis/spellcheck-github-actions from 0.49.0 to 0.51.0 ([#3414](https://github.com/redis/go-redis/pull/3414)) - -## Contributors -We'd like to thank all the contributors who worked on this release! - -[@andy-stark-redis](https://github.com/andy-stark-redis), [@boekkooi-impossiblecloud](https://github.com/boekkooi-impossiblecloud), [@cxljs](https://github.com/cxljs), [@dcherubini](https://github.com/dcherubini), [@dependabot[bot]](https://github.com/apps/dependabot), [@iamamirsalehi](https://github.com/iamamirsalehi), [@ndyakov](https://github.com/ndyakov), [@pete-woods](https://github.com/pete-woods), [@twz915](https://github.com/twz915) and [dependabot[bot]](https://github.com/apps/dependabot) - -# 9.10.0 (2025-06-06) - -## 🚀 Highlights - -`go-redis` now supports [vector sets](https://redis.io/docs/latest/develop/data-types/vector-sets/). This data type is marked -as "in preview" in Redis and its support in `go-redis` is marked as experimental. You can find examples in the documentation and -in the `doctests` folder. - -# Changes - -## 🚀 New Features - -- feat: support vectorset ([#3375](https://github.com/redis/go-redis/pull/3375)) - -## 🧰 Maintenance - -- Add the missing NewFloatSliceResult for testing ([#3393](https://github.com/redis/go-redis/pull/3393)) -- DOC-5078 vector set examples ([#3394](https://github.com/redis/go-redis/pull/3394)) - -## Contributors -We'd like to thank all the contributors who worked on this release! - -[@AndBobsYourUncle](https://github.com/AndBobsYourUncle), [@andy-stark-redis](https://github.com/andy-stark-redis), [@fukua95](https://github.com/fukua95) and [@ndyakov](https://github.com/ndyakov) - - - -# 9.9.0 (2025-05-27) - -## 🚀 Highlights -- **Token-based Authentication**: Added `StreamingCredentialsProvider` for dynamic credential updates (experimental) - - Can be used with [go-redis-entraid](https://github.com/redis/go-redis-entraid) for Azure AD authentication -- **Connection Statistics**: Added connection waiting statistics for better monitoring -- **Failover Improvements**: Added `ParseFailoverURL` for easier failover configuration -- **Ring Client Enhancements**: Added shard access methods for better Pub/Sub management - -## ✨ New Features -- Added `StreamingCredentialsProvider` for token-based authentication ([#3320](https://github.com/redis/go-redis/pull/3320)) - - Supports dynamic credential updates - - Includes connection close hooks - - Note: Currently marked as experimental -- Added `ParseFailoverURL` for parsing failover URLs ([#3362](https://github.com/redis/go-redis/pull/3362)) -- Added connection waiting statistics ([#2804](https://github.com/redis/go-redis/pull/2804)) -- Added new utility functions: - - `ParseFloat` and `MustParseFloat` in public utils package ([#3371](https://github.com/redis/go-redis/pull/3371)) - - Unit tests for `Atoi`, `ParseInt`, `ParseUint`, and `ParseFloat` ([#3377](https://github.com/redis/go-redis/pull/3377)) -- Added Ring client shard access methods: - - `GetShardClients()` to retrieve all active shard clients - - `GetShardClientForKey(key string)` to get the shard client for a specific key ([#3388](https://github.com/redis/go-redis/pull/3388)) - -## 🐛 Bug Fixes -- Fixed routing reads to loading slave nodes ([#3370](https://github.com/redis/go-redis/pull/3370)) -- Added support for nil lag in XINFO GROUPS ([#3369](https://github.com/redis/go-redis/pull/3369)) -- Fixed pool acquisition timeout issues ([#3381](https://github.com/redis/go-redis/pull/3381)) -- Optimized unnecessary copy operations ([#3376](https://github.com/redis/go-redis/pull/3376)) - -## 📚 Documentation -- Updated documentation for XINFO GROUPS with nil lag support ([#3369](https://github.com/redis/go-redis/pull/3369)) -- Added package-level comments for new features - -## ⚡ Performance and Reliability -- Optimized `ReplaceSpaces` function ([#3383](https://github.com/redis/go-redis/pull/3383)) -- Set default value for `Options.Protocol` in `init()` ([#3387](https://github.com/redis/go-redis/pull/3387)) -- Exported pool errors for public consumption ([#3380](https://github.com/redis/go-redis/pull/3380)) - -## 🔧 Dependencies and Infrastructure -- Updated Redis CI to version 8.0.1 ([#3372](https://github.com/redis/go-redis/pull/3372)) -- Updated spellcheck GitHub Actions ([#3389](https://github.com/redis/go-redis/pull/3389)) -- Removed unused parameters ([#3382](https://github.com/redis/go-redis/pull/3382), [#3384](https://github.com/redis/go-redis/pull/3384)) - -## 🧪 Testing -- Added unit tests for pool acquisition timeout ([#3381](https://github.com/redis/go-redis/pull/3381)) -- Added unit tests for utility functions ([#3377](https://github.com/redis/go-redis/pull/3377)) - -## 👥 Contributors - -We would like to thank all the contributors who made this release possible: - -[@ndyakov](https://github.com/ndyakov), [@ofekshenawa](https://github.com/ofekshenawa), [@LINKIWI](https://github.com/LINKIWI), [@iamamirsalehi](https://github.com/iamamirsalehi), [@fukua95](https://github.com/fukua95), [@lzakharov](https://github.com/lzakharov), [@DengY11](https://github.com/DengY11) - -## 📝 Changelog - -For a complete list of changes, see the [full changelog](https://github.com/redis/go-redis/compare/v9.8.0...v9.9.0). - -# 9.8.0 (2025-04-30) - -## 🚀 Highlights -- **Redis 8 Support**: Full compatibility with Redis 8.0, including testing and CI integration -- **Enhanced Hash Operations**: Added support for new hash commands (`HGETDEL`, `HGETEX`, `HSETEX`) and `HSTRLEN` command -- **Search Improvements**: Enabled Search DIALECT 2 by default and added `CountOnly` argument for `FT.Search` - -## ✨ New Features -- Added support for new hash commands: `HGETDEL`, `HGETEX`, `HSETEX` ([#3305](https://github.com/redis/go-redis/pull/3305)) -- Added `HSTRLEN` command for hash operations ([#2843](https://github.com/redis/go-redis/pull/2843)) -- Added `Do` method for raw query by single connection from `pool.Conn()` ([#3182](https://github.com/redis/go-redis/pull/3182)) -- Prevent false-positive marshaling by treating zero time.Time as empty in isEmptyValue ([#3273](https://github.com/redis/go-redis/pull/3273)) -- Added FailoverClusterClient support for Universal client ([#2794](https://github.com/redis/go-redis/pull/2794)) -- Added support for cluster mode with `IsClusterMode` config parameter ([#3255](https://github.com/redis/go-redis/pull/3255)) -- Added client name support in `HELLO` RESP handshake ([#3294](https://github.com/redis/go-redis/pull/3294)) -- **Enabled Search DIALECT 2 by default** ([#3213](https://github.com/redis/go-redis/pull/3213)) -- Added read-only option for failover configurations ([#3281](https://github.com/redis/go-redis/pull/3281)) -- Added `CountOnly` argument for `FT.Search` to use `LIMIT 0 0` ([#3338](https://github.com/redis/go-redis/pull/3338)) -- Added `DB` option support in `NewFailoverClusterClient` ([#3342](https://github.com/redis/go-redis/pull/3342)) -- Added `nil` check for the options when creating a client ([#3363](https://github.com/redis/go-redis/pull/3363)) - -## 🐛 Bug Fixes -- Fixed `PubSub` concurrency safety issues ([#3360](https://github.com/redis/go-redis/pull/3360)) -- Fixed panic caused when argument is `nil` ([#3353](https://github.com/redis/go-redis/pull/3353)) -- Improved error handling when fetching master node from sentinels ([#3349](https://github.com/redis/go-redis/pull/3349)) -- Fixed connection pool timeout issues and increased retries ([#3298](https://github.com/redis/go-redis/pull/3298)) -- Fixed context cancellation error leading to connection spikes on Primary instances ([#3190](https://github.com/redis/go-redis/pull/3190)) -- Fixed RedisCluster client to consider `MASTERDOWN` a retriable error ([#3164](https://github.com/redis/go-redis/pull/3164)) -- Fixed tracing to show complete commands instead of truncated versions ([#3290](https://github.com/redis/go-redis/pull/3290)) -- Fixed OpenTelemetry instrumentation to prevent multiple span reporting ([#3168](https://github.com/redis/go-redis/pull/3168)) -- Fixed `FT.Search` Limit argument and added `CountOnly` argument for limit 0 0 ([#3338](https://github.com/redis/go-redis/pull/3338)) -- Fixed missing command in interface ([#3344](https://github.com/redis/go-redis/pull/3344)) -- Fixed slot calculation for `COUNTKEYSINSLOT` command ([#3327](https://github.com/redis/go-redis/pull/3327)) -- Updated PubSub implementation with correct context ([#3329](https://github.com/redis/go-redis/pull/3329)) - -## 📚 Documentation -- Added hash search examples ([#3357](https://github.com/redis/go-redis/pull/3357)) -- Fixed documentation comments ([#3351](https://github.com/redis/go-redis/pull/3351)) -- Added `CountOnly` search example ([#3345](https://github.com/redis/go-redis/pull/3345)) -- Added examples for list commands: `LLEN`, `LPOP`, `LPUSH`, `LRANGE`, `RPOP`, `RPUSH` ([#3234](https://github.com/redis/go-redis/pull/3234)) -- Added `SADD` and `SMEMBERS` command examples ([#3242](https://github.com/redis/go-redis/pull/3242)) -- Updated `README.md` to use Redis Discord guild ([#3331](https://github.com/redis/go-redis/pull/3331)) -- Updated `HExpire` command documentation ([#3355](https://github.com/redis/go-redis/pull/3355)) -- Featured OpenTelemetry instrumentation more prominently ([#3316](https://github.com/redis/go-redis/pull/3316)) -- Updated `README.md` with additional information ([#310ce55](https://github.com/redis/go-redis/commit/310ce55)) - -## ⚡ Performance and Reliability -- Bound connection pool background dials to configured dial timeout ([#3089](https://github.com/redis/go-redis/pull/3089)) -- Ensured context isn't exhausted via concurrent query ([#3334](https://github.com/redis/go-redis/pull/3334)) - -## 🔧 Dependencies and Infrastructure -- Updated testing image to Redis 8.0-RC2 ([#3361](https://github.com/redis/go-redis/pull/3361)) -- Enabled CI for Redis CE 8.0 ([#3274](https://github.com/redis/go-redis/pull/3274)) -- Updated various dependencies: - - Bumped golangci/golangci-lint-action from 6.5.0 to 7.0.0 ([#3354](https://github.com/redis/go-redis/pull/3354)) - - Bumped rojopolis/spellcheck-github-actions ([#3336](https://github.com/redis/go-redis/pull/3336)) - - Bumped golang.org/x/net in example/otel ([#3308](https://github.com/redis/go-redis/pull/3308)) -- Migrated golangci-lint configuration to v2 format ([#3354](https://github.com/redis/go-redis/pull/3354)) - -## ⚠️ Breaking Changes -- **Enabled Search DIALECT 2 by default** ([#3213](https://github.com/redis/go-redis/pull/3213)) -- Dropped RedisGears (Triggers and Functions) support ([#3321](https://github.com/redis/go-redis/pull/3321)) -- Dropped FT.PROFILE command that was never enabled ([#3323](https://github.com/redis/go-redis/pull/3323)) - -## 🔒 Security -- Fixed network error handling on SETINFO (CVE-2025-29923) ([#3295](https://github.com/redis/go-redis/pull/3295)) - -## 🧪 Testing -- Added integration tests for Redis 8 behavior changes in Redis Search ([#3337](https://github.com/redis/go-redis/pull/3337)) -- Added vector types INT8 and UINT8 tests ([#3299](https://github.com/redis/go-redis/pull/3299)) -- Added test codes for search_commands.go ([#3285](https://github.com/redis/go-redis/pull/3285)) -- Fixed example test sorting ([#3292](https://github.com/redis/go-redis/pull/3292)) - -## 👥 Contributors - -We would like to thank all the contributors who made this release possible: - -[@alexander-menshchikov](https://github.com/alexander-menshchikov), [@EXPEbdodla](https://github.com/EXPEbdodla), [@afti](https://github.com/afti), [@dmaier-redislabs](https://github.com/dmaier-redislabs), [@four_leaf_clover](https://github.com/four_leaf_clover), [@alohaglenn](https://github.com/alohaglenn), [@gh73962](https://github.com/gh73962), [@justinmir](https://github.com/justinmir), [@LINKIWI](https://github.com/LINKIWI), [@liushuangbill](https://github.com/liushuangbill), [@golang88](https://github.com/golang88), [@gnpaone](https://github.com/gnpaone), [@ndyakov](https://github.com/ndyakov), [@nikolaydubina](https://github.com/nikolaydubina), [@oleglacto](https://github.com/oleglacto), [@andy-stark-redis](https://github.com/andy-stark-redis), [@rodneyosodo](https://github.com/rodneyosodo), [@dependabot](https://github.com/dependabot), [@rfyiamcool](https://github.com/rfyiamcool), [@frankxjkuang](https://github.com/frankxjkuang), [@fukua95](https://github.com/fukua95), [@soleymani-milad](https://github.com/soleymani-milad), [@ofekshenawa](https://github.com/ofekshenawa), [@khasanovbi](https://github.com/khasanovbi) - - -# Old Changelog -## Unreleased - -### Changed - -* `go-redis` won't skip span creation if the parent spans is not recording. ([#2980](https://github.com/redis/go-redis/issues/2980)) - Users can use the OpenTelemetry sampler to control the sampling behavior. - For instance, you can use the `ParentBased(NeverSample())` sampler from `go.opentelemetry.io/otel/sdk/trace` to keep - a similar behavior (drop orphan spans) of `go-redis` as before. - -## [9.0.5](https://github.com/redis/go-redis/compare/v9.0.4...v9.0.5) (2023-05-29) - - -### Features - -* Add ACL LOG ([#2536](https://github.com/redis/go-redis/issues/2536)) ([31ba855](https://github.com/redis/go-redis/commit/31ba855ddebc38fbcc69a75d9d4fb769417cf602)) -* add field protocol to setupClusterQueryParams ([#2600](https://github.com/redis/go-redis/issues/2600)) ([840c25c](https://github.com/redis/go-redis/commit/840c25cb6f320501886a82a5e75f47b491e46fbe)) -* add protocol option ([#2598](https://github.com/redis/go-redis/issues/2598)) ([3917988](https://github.com/redis/go-redis/commit/391798880cfb915c4660f6c3ba63e0c1a459e2af)) - - - -## [9.0.4](https://github.com/redis/go-redis/compare/v9.0.3...v9.0.4) (2023-05-01) - - -### Bug Fixes - -* reader float parser ([#2513](https://github.com/redis/go-redis/issues/2513)) ([46f2450](https://github.com/redis/go-redis/commit/46f245075e6e3a8bd8471f9ca67ea95fd675e241)) - - -### Features - -* add client info command ([#2483](https://github.com/redis/go-redis/issues/2483)) ([b8c7317](https://github.com/redis/go-redis/commit/b8c7317cc6af444603731f7017c602347c0ba61e)) -* no longer verify HELLO error messages ([#2515](https://github.com/redis/go-redis/issues/2515)) ([7b4f217](https://github.com/redis/go-redis/commit/7b4f2179cb5dba3d3c6b0c6f10db52b837c912c8)) -* read the structure to increase the judgment of the omitempty op… ([#2529](https://github.com/redis/go-redis/issues/2529)) ([37c057b](https://github.com/redis/go-redis/commit/37c057b8e597c5e8a0e372337f6a8ad27f6030af)) - - - -## [9.0.3](https://github.com/redis/go-redis/compare/v9.0.2...v9.0.3) (2023-04-02) - -### New Features - -- feat(scan): scan time.Time sets the default decoding (#2413) -- Add support for CLUSTER LINKS command (#2504) -- Add support for acl dryrun command (#2502) -- Add support for COMMAND GETKEYS & COMMAND GETKEYSANDFLAGS (#2500) -- Add support for LCS Command (#2480) -- Add support for BZMPOP (#2456) -- Adding support for ZMPOP command (#2408) -- Add support for LMPOP (#2440) -- feat: remove pool unused fields (#2438) -- Expiretime and PExpireTime (#2426) -- Implement `FUNCTION` group of commands (#2475) -- feat(zadd): add ZAddLT and ZAddGT (#2429) -- Add: Support for COMMAND LIST command (#2491) -- Add support for BLMPOP (#2442) -- feat: check pipeline.Do to prevent confusion with Exec (#2517) -- Function stats, function kill, fcall and fcall_ro (#2486) -- feat: Add support for CLUSTER SHARDS command (#2507) -- feat(cmd): support for adding byte,bit parameters to the bitpos command (#2498) - -### Fixed - -- fix: eval api cmd.SetFirstKeyPos (#2501) -- fix: limit the number of connections created (#2441) -- fixed #2462 v9 continue support dragonfly, it's Hello command return "NOAUTH Authentication required" error (#2479) -- Fix for internal/hscan/structmap.go:89:23: undefined: reflect.Pointer (#2458) -- fix: group lag can be null (#2448) - -### Maintenance - -- Updating to the latest version of redis (#2508) -- Allowing for running tests on a port other than the fixed 6380 (#2466) -- redis 7.0.8 in tests (#2450) -- docs: Update redisotel example for v9 (#2425) -- chore: update go mod, Upgrade golang.org/x/net version to 0.7.0 (#2476) -- chore: add Chinese translation (#2436) -- chore(deps): bump github.com/bsm/gomega from 1.20.0 to 1.26.0 (#2421) -- chore(deps): bump github.com/bsm/ginkgo/v2 from 2.5.0 to 2.7.0 (#2420) -- chore(deps): bump actions/setup-go from 3 to 4 (#2495) -- docs: add instructions for the HSet api (#2503) -- docs: add reading lag field comment (#2451) -- test: update go mod before testing(go mod tidy) (#2423) -- docs: fix comment typo (#2505) -- test: remove testify (#2463) -- refactor: change ListElementCmd to KeyValuesCmd. (#2443) -- fix(appendArg): appendArg case special type (#2489) - -## [9.0.2](https://github.com/redis/go-redis/compare/v9.0.1...v9.0.2) (2023-02-01) - -### Features - -* upgrade OpenTelemetry, use the new metrics API. ([#2410](https://github.com/redis/go-redis/issues/2410)) ([e29e42c](https://github.com/redis/go-redis/commit/e29e42cde2755ab910d04185025dc43ce6f59c65)) - -## v9 2023-01-30 - -### Breaking - -- Changed Pipelines to not be thread-safe any more. - -### Added - -- Added support for [RESP3](https://github.com/antirez/RESP3/blob/master/spec.md) protocol. It was - contributed by @monkey92t who has done the majority of work in this release. -- Added `ContextTimeoutEnabled` option that controls whether the client respects context timeouts - and deadlines. See - [Redis Timeouts](https://redis.uptrace.dev/guide/go-redis-debugging.html#timeouts) for details. -- Added `ParseClusterURL` to parse URLs into `ClusterOptions`, for example, - `redis://user:password@localhost:6789?dial_timeout=3&read_timeout=6s&addr=localhost:6790&addr=localhost:6791`. -- Added metrics instrumentation using `redisotel.IstrumentMetrics`. See - [documentation](https://redis.uptrace.dev/guide/go-redis-monitoring.html) -- Added `redis.HasErrorPrefix` to help working with errors. - -### Changed - -- Removed asynchronous cancellation based on the context timeout. It was racy in v8 and is - completely gone in v9. -- Reworked hook interface and added `DialHook`. -- Replaced `redisotel.NewTracingHook` with `redisotel.InstrumentTracing`. See - [example](example/otel) and - [documentation](https://redis.uptrace.dev/guide/go-redis-monitoring.html). -- Replaced `*redis.Z` with `redis.Z` since it is small enough to be passed as value without making - an allocation. -- Renamed the option `MaxConnAge` to `ConnMaxLifetime`. -- Renamed the option `IdleTimeout` to `ConnMaxIdleTime`. -- Removed connection reaper in favor of `MaxIdleConns`. -- Removed `WithContext` since `context.Context` can be passed directly as an arg. -- Removed `Pipeline.Close` since there is no real need to explicitly manage pipeline resources and - it can be safely reused via `sync.Pool` etc. `Pipeline.Discard` is still available if you want to - reset commands for some reason. - -### Fixed - -- Improved and fixed pipeline retries. -- As usually, added support for more commands and fixed some bugs. diff --git a/vendor/github.com/redis/go-redis/v9/RELEASING.md b/vendor/github.com/redis/go-redis/v9/RELEASING.md deleted file mode 100644 index 1115db4e3..000000000 --- a/vendor/github.com/redis/go-redis/v9/RELEASING.md +++ /dev/null @@ -1,15 +0,0 @@ -# Releasing - -1. Run `release.sh` script which updates versions in go.mod files and pushes a new branch to GitHub: - -```shell -TAG=v1.0.0 ./scripts/release.sh -``` - -2. Open a pull request and wait for the build to finish. - -3. Merge the pull request and run `tag.sh` to create tags for packages: - -```shell -TAG=v1.0.0 ./scripts/tag.sh -``` diff --git a/vendor/github.com/redis/go-redis/v9/acl_commands.go b/vendor/github.com/redis/go-redis/v9/acl_commands.go deleted file mode 100644 index 0a8a195ce..000000000 --- a/vendor/github.com/redis/go-redis/v9/acl_commands.go +++ /dev/null @@ -1,116 +0,0 @@ -package redis - -import "context" - -type ACLCmdable interface { - ACLDryRun(ctx context.Context, username string, command ...interface{}) *StringCmd - - ACLLog(ctx context.Context, count int64) *ACLLogCmd - ACLLogReset(ctx context.Context) *StatusCmd - - ACLGenPass(ctx context.Context, bit int) *StringCmd - - ACLSetUser(ctx context.Context, username string, rules ...string) *StatusCmd - ACLDelUser(ctx context.Context, username string) *IntCmd - ACLUsers(ctx context.Context) *StringSliceCmd - ACLWhoAmI(ctx context.Context) *StringCmd - ACLList(ctx context.Context) *StringSliceCmd - - ACLCat(ctx context.Context) *StringSliceCmd - ACLCatArgs(ctx context.Context, options *ACLCatArgs) *StringSliceCmd -} - -type ACLCatArgs struct { - Category string -} - -func (c cmdable) ACLDryRun(ctx context.Context, username string, command ...interface{}) *StringCmd { - args := make([]interface{}, 0, 3+len(command)) - args = append(args, "acl", "dryrun", username) - args = append(args, command...) - cmd := NewStringCmd(ctx, args...) - _ = c(ctx, cmd) - return cmd -} - -func (c cmdable) ACLLog(ctx context.Context, count int64) *ACLLogCmd { - args := make([]interface{}, 0, 3) - args = append(args, "acl", "log") - if count > 0 { - args = append(args, count) - } - cmd := NewACLLogCmd(ctx, args...) - _ = c(ctx, cmd) - return cmd -} - -func (c cmdable) ACLLogReset(ctx context.Context) *StatusCmd { - cmd := NewStatusCmd(ctx, "acl", "log", "reset") - _ = c(ctx, cmd) - return cmd -} - -func (c cmdable) ACLDelUser(ctx context.Context, username string) *IntCmd { - cmd := NewIntCmd(ctx, "acl", "deluser", username) - _ = c(ctx, cmd) - return cmd -} - -func (c cmdable) ACLSetUser(ctx context.Context, username string, rules ...string) *StatusCmd { - args := make([]interface{}, 3+len(rules)) - args[0] = "acl" - args[1] = "setuser" - args[2] = username - for i, rule := range rules { - args[i+3] = rule - } - cmd := NewStatusCmd(ctx, args...) - _ = c(ctx, cmd) - return cmd -} - -func (c cmdable) ACLGenPass(ctx context.Context, bit int) *StringCmd { - args := make([]interface{}, 0, 3) - args = append(args, "acl", "genpass") - if bit > 0 { - args = append(args, bit) - } - cmd := NewStringCmd(ctx, args...) - _ = c(ctx, cmd) - return cmd -} - -func (c cmdable) ACLUsers(ctx context.Context) *StringSliceCmd { - cmd := NewStringSliceCmd(ctx, "acl", "users") - _ = c(ctx, cmd) - return cmd -} - -func (c cmdable) ACLWhoAmI(ctx context.Context) *StringCmd { - cmd := NewStringCmd(ctx, "acl", "whoami") - _ = c(ctx, cmd) - return cmd -} - -func (c cmdable) ACLList(ctx context.Context) *StringSliceCmd { - cmd := NewStringSliceCmd(ctx, "acl", "list") - _ = c(ctx, cmd) - return cmd -} - -func (c cmdable) ACLCat(ctx context.Context) *StringSliceCmd { - cmd := NewStringSliceCmd(ctx, "acl", "cat") - _ = c(ctx, cmd) - return cmd -} - -func (c cmdable) ACLCatArgs(ctx context.Context, options *ACLCatArgs) *StringSliceCmd { - // if there is a category passed, build new cmd, if there isn't - use the ACLCat method - if options != nil && options.Category != "" { - cmd := NewStringSliceCmd(ctx, "acl", "cat", options.Category) - _ = c(ctx, cmd) - return cmd - } - - return c.ACLCat(ctx) -} diff --git a/vendor/github.com/redis/go-redis/v9/adapters.go b/vendor/github.com/redis/go-redis/v9/adapters.go deleted file mode 100644 index 952a4c266..000000000 --- a/vendor/github.com/redis/go-redis/v9/adapters.go +++ /dev/null @@ -1,118 +0,0 @@ -package redis - -import ( - "context" - "errors" - "net" - "time" - - "github.com/redis/go-redis/v9/internal/interfaces" - "github.com/redis/go-redis/v9/push" -) - -// ErrInvalidCommand is returned when an invalid command is passed to ExecuteCommand. -var ErrInvalidCommand = errors.New("invalid command type") - -// ErrInvalidPool is returned when the pool type is not supported. -var ErrInvalidPool = errors.New("invalid pool type") - -// newClientAdapter creates a new client adapter for regular Redis clients. -func newClientAdapter(client *baseClient) interfaces.ClientInterface { - return &clientAdapter{client: client} -} - -// clientAdapter adapts a Redis client to implement interfaces.ClientInterface. -type clientAdapter struct { - client *baseClient -} - -// GetOptions returns the client options. -func (ca *clientAdapter) GetOptions() interfaces.OptionsInterface { - return &optionsAdapter{options: ca.client.opt} -} - -// GetPushProcessor returns the client's push notification processor. -func (ca *clientAdapter) GetPushProcessor() interfaces.NotificationProcessor { - return &pushProcessorAdapter{processor: ca.client.pushProcessor} -} - -// optionsAdapter adapts Redis options to implement interfaces.OptionsInterface. -type optionsAdapter struct { - options *Options -} - -// GetReadTimeout returns the read timeout. -func (oa *optionsAdapter) GetReadTimeout() time.Duration { - return oa.options.ReadTimeout -} - -// GetWriteTimeout returns the write timeout. -func (oa *optionsAdapter) GetWriteTimeout() time.Duration { - return oa.options.WriteTimeout -} - -// GetNetwork returns the network type. -func (oa *optionsAdapter) GetNetwork() string { - return oa.options.Network -} - -// GetAddr returns the connection address. -func (oa *optionsAdapter) GetAddr() string { - return oa.options.Addr -} - -// GetNodeAddress returns the address of the Redis node as reported by the server. -// For cluster clients, this is the endpoint from CLUSTER SLOTS before any transformation. -// For standalone clients, this defaults to Addr. -func (oa *optionsAdapter) GetNodeAddress() string { - return oa.options.NodeAddress -} - -// IsTLSEnabled returns true if TLS is enabled. -func (oa *optionsAdapter) IsTLSEnabled() bool { - return oa.options.TLSConfig != nil -} - -// GetProtocol returns the protocol version. -func (oa *optionsAdapter) GetProtocol() int { - return oa.options.Protocol -} - -// GetPoolSize returns the connection pool size. -func (oa *optionsAdapter) GetPoolSize() int { - return oa.options.PoolSize -} - -// NewDialer returns a new dialer function for the connection. -func (oa *optionsAdapter) NewDialer() func(context.Context) (net.Conn, error) { - baseDialer := oa.options.NewDialer() - return func(ctx context.Context) (net.Conn, error) { - // Extract network and address from the options - network := oa.options.Network - addr := oa.options.Addr - return baseDialer(ctx, network, addr) - } -} - -// pushProcessorAdapter adapts a push.NotificationProcessor to implement interfaces.NotificationProcessor. -type pushProcessorAdapter struct { - processor push.NotificationProcessor -} - -// RegisterHandler registers a handler for a specific push notification name. -func (ppa *pushProcessorAdapter) RegisterHandler(pushNotificationName string, handler interface{}, protected bool) error { - if pushHandler, ok := handler.(push.NotificationHandler); ok { - return ppa.processor.RegisterHandler(pushNotificationName, pushHandler, protected) - } - return errors.New("handler must implement push.NotificationHandler") -} - -// UnregisterHandler removes a handler for a specific push notification name. -func (ppa *pushProcessorAdapter) UnregisterHandler(pushNotificationName string) error { - return ppa.processor.UnregisterHandler(pushNotificationName) -} - -// GetHandler returns the handler for a specific push notification name. -func (ppa *pushProcessorAdapter) GetHandler(pushNotificationName string) interface{} { - return ppa.processor.GetHandler(pushNotificationName) -} diff --git a/vendor/github.com/redis/go-redis/v9/auth/auth.go b/vendor/github.com/redis/go-redis/v9/auth/auth.go deleted file mode 100644 index 1f5c80224..000000000 --- a/vendor/github.com/redis/go-redis/v9/auth/auth.go +++ /dev/null @@ -1,61 +0,0 @@ -// Package auth package provides authentication-related interfaces and types. -// It also includes a basic implementation of credentials using username and password. -package auth - -// StreamingCredentialsProvider is an interface that defines the methods for a streaming credentials provider. -// It is used to provide credentials for authentication. -// The CredentialsListener is used to receive updates when the credentials change. -type StreamingCredentialsProvider interface { - // Subscribe subscribes to the credentials provider for updates. - // It returns the current credentials, a cancel function to unsubscribe from the provider, - // and an error if any. - // TODO(ndyakov): Should we add context to the Subscribe method? - Subscribe(listener CredentialsListener) (Credentials, UnsubscribeFunc, error) -} - -// UnsubscribeFunc is a function that is used to cancel the subscription to the credentials provider. -// It is used to unsubscribe from the provider when the credentials are no longer needed. -type UnsubscribeFunc func() error - -// CredentialsListener is an interface that defines the methods for a credentials listener. -// It is used to receive updates when the credentials change. -// The OnNext method is called when the credentials change. -// The OnError method is called when an error occurs while requesting the credentials. -type CredentialsListener interface { - OnNext(credentials Credentials) - OnError(err error) -} - -// Credentials is an interface that defines the methods for credentials. -// It is used to provide the credentials for authentication. -type Credentials interface { - // BasicAuth returns the username and password for basic authentication. - BasicAuth() (username string, password string) - // RawCredentials returns the raw credentials as a string. - // This can be used to extract the username and password from the raw credentials or - // additional information if present in the token. - RawCredentials() string -} - -type basicAuth struct { - username string - password string -} - -// RawCredentials returns the raw credentials as a string. -func (b *basicAuth) RawCredentials() string { - return b.username + ":" + b.password -} - -// BasicAuth returns the username and password for basic authentication. -func (b *basicAuth) BasicAuth() (username string, password string) { - return b.username, b.password -} - -// NewBasicCredentials creates a new Credentials object from the given username and password. -func NewBasicCredentials(username, password string) Credentials { - return &basicAuth{ - username: username, - password: password, - } -} diff --git a/vendor/github.com/redis/go-redis/v9/auth/reauth_credentials_listener.go b/vendor/github.com/redis/go-redis/v9/auth/reauth_credentials_listener.go deleted file mode 100644 index 40076a0b1..000000000 --- a/vendor/github.com/redis/go-redis/v9/auth/reauth_credentials_listener.go +++ /dev/null @@ -1,47 +0,0 @@ -package auth - -// ReAuthCredentialsListener is a struct that implements the CredentialsListener interface. -// It is used to re-authenticate the credentials when they are updated. -// It contains: -// - reAuth: a function that takes the new credentials and returns an error if any. -// - onErr: a function that takes an error and handles it. -type ReAuthCredentialsListener struct { - reAuth func(credentials Credentials) error - onErr func(err error) -} - -// OnNext is called when the credentials are updated. -// It calls the reAuth function with the new credentials. -// If the reAuth function returns an error, it calls the onErr function with the error. -func (c *ReAuthCredentialsListener) OnNext(credentials Credentials) { - if c.reAuth == nil { - return - } - - err := c.reAuth(credentials) - if err != nil { - c.OnError(err) - } -} - -// OnError is called when an error occurs. -// It can be called from both the credentials provider and the reAuth function. -func (c *ReAuthCredentialsListener) OnError(err error) { - if c.onErr == nil { - return - } - - c.onErr(err) -} - -// NewReAuthCredentialsListener creates a new ReAuthCredentialsListener. -// Implements the auth.CredentialsListener interface. -func NewReAuthCredentialsListener(reAuth func(credentials Credentials) error, onErr func(err error)) *ReAuthCredentialsListener { - return &ReAuthCredentialsListener{ - reAuth: reAuth, - onErr: onErr, - } -} - -// Ensure ReAuthCredentialsListener implements the CredentialsListener interface. -var _ CredentialsListener = (*ReAuthCredentialsListener)(nil) diff --git a/vendor/github.com/redis/go-redis/v9/bitmap_commands.go b/vendor/github.com/redis/go-redis/v9/bitmap_commands.go deleted file mode 100644 index 86aa9b7ef..000000000 --- a/vendor/github.com/redis/go-redis/v9/bitmap_commands.go +++ /dev/null @@ -1,197 +0,0 @@ -package redis - -import ( - "context" - "errors" -) - -type BitMapCmdable interface { - GetBit(ctx context.Context, key string, offset int64) *IntCmd - SetBit(ctx context.Context, key string, offset int64, value int) *IntCmd - BitCount(ctx context.Context, key string, bitCount *BitCount) *IntCmd - BitOpAnd(ctx context.Context, destKey string, keys ...string) *IntCmd - BitOpOr(ctx context.Context, destKey string, keys ...string) *IntCmd - BitOpXor(ctx context.Context, destKey string, keys ...string) *IntCmd - BitOpDiff(ctx context.Context, destKey string, keys ...string) *IntCmd - BitOpDiff1(ctx context.Context, destKey string, keys ...string) *IntCmd - BitOpAndOr(ctx context.Context, destKey string, keys ...string) *IntCmd - BitOpOne(ctx context.Context, destKey string, keys ...string) *IntCmd - BitOpNot(ctx context.Context, destKey string, key string) *IntCmd - BitPos(ctx context.Context, key string, bit int64, pos ...int64) *IntCmd - BitPosSpan(ctx context.Context, key string, bit int8, start, end int64, span string) *IntCmd - BitField(ctx context.Context, key string, values ...interface{}) *IntSliceCmd - BitFieldRO(ctx context.Context, key string, values ...interface{}) *IntSliceCmd -} - -func (c cmdable) GetBit(ctx context.Context, key string, offset int64) *IntCmd { - cmd := NewIntCmd(ctx, "getbit", key, offset) - _ = c(ctx, cmd) - return cmd -} - -func (c cmdable) SetBit(ctx context.Context, key string, offset int64, value int) *IntCmd { - cmd := NewIntCmd( - ctx, - "setbit", - key, - offset, - value, - ) - _ = c(ctx, cmd) - return cmd -} - -type BitCount struct { - Start, End int64 - Unit string // BYTE(default) | BIT -} - -const BitCountIndexByte string = "BYTE" -const BitCountIndexBit string = "BIT" - -func (c cmdable) BitCount(ctx context.Context, key string, bitCount *BitCount) *IntCmd { - args := make([]any, 2, 5) - args[0] = "bitcount" - args[1] = key - if bitCount != nil { - args = append(args, bitCount.Start, bitCount.End) - if bitCount.Unit != "" { - if bitCount.Unit != BitCountIndexByte && bitCount.Unit != BitCountIndexBit { - cmd := NewIntCmd(ctx) - cmd.SetErr(errors.New("redis: invalid bitcount index")) - return cmd - } - args = append(args, bitCount.Unit) - } - } - cmd := NewIntCmd(ctx, args...) - _ = c(ctx, cmd) - return cmd -} - -func (c cmdable) bitOp(ctx context.Context, op, destKey string, keys ...string) *IntCmd { - args := make([]interface{}, 3+len(keys)) - args[0] = "bitop" - args[1] = op - args[2] = destKey - for i, key := range keys { - args[3+i] = key - } - cmd := NewIntCmd(ctx, args...) - _ = c(ctx, cmd) - return cmd -} - -// BitOpAnd creates a new bitmap in which users are members of all given bitmaps -func (c cmdable) BitOpAnd(ctx context.Context, destKey string, keys ...string) *IntCmd { - return c.bitOp(ctx, "and", destKey, keys...) -} - -// BitOpOr creates a new bitmap in which users are member of at least one given bitmap -func (c cmdable) BitOpOr(ctx context.Context, destKey string, keys ...string) *IntCmd { - return c.bitOp(ctx, "or", destKey, keys...) -} - -// BitOpXor creates a new bitmap in which users are the result of XORing all given bitmaps -func (c cmdable) BitOpXor(ctx context.Context, destKey string, keys ...string) *IntCmd { - return c.bitOp(ctx, "xor", destKey, keys...) -} - -// BitOpNot creates a new bitmap in which users are not members of a given bitmap -func (c cmdable) BitOpNot(ctx context.Context, destKey string, key string) *IntCmd { - return c.bitOp(ctx, "not", destKey, key) -} - -// BitOpDiff creates a new bitmap in which users are members of bitmap X but not of any of bitmaps Y1, Y2, … -// Introduced with Redis 8.2 -func (c cmdable) BitOpDiff(ctx context.Context, destKey string, keys ...string) *IntCmd { - return c.bitOp(ctx, "diff", destKey, keys...) -} - -// BitOpDiff1 creates a new bitmap in which users are members of one or more of bitmaps Y1, Y2, … but not members of bitmap X -// Introduced with Redis 8.2 -func (c cmdable) BitOpDiff1(ctx context.Context, destKey string, keys ...string) *IntCmd { - return c.bitOp(ctx, "diff1", destKey, keys...) -} - -// BitOpAndOr creates a new bitmap in which users are members of bitmap X and also members of one or more of bitmaps Y1, Y2, … -// Introduced with Redis 8.2 -func (c cmdable) BitOpAndOr(ctx context.Context, destKey string, keys ...string) *IntCmd { - return c.bitOp(ctx, "andor", destKey, keys...) -} - -// BitOpOne creates a new bitmap in which users are members of exactly one of the given bitmaps -// Introduced with Redis 8.2 -func (c cmdable) BitOpOne(ctx context.Context, destKey string, keys ...string) *IntCmd { - return c.bitOp(ctx, "one", destKey, keys...) -} - -// BitPos is an API before Redis version 7.0, cmd: bitpos key bit start end -// if you need the `byte | bit` parameter, please use `BitPosSpan`. -func (c cmdable) BitPos(ctx context.Context, key string, bit int64, pos ...int64) *IntCmd { - args := make([]interface{}, 3+len(pos)) - args[0] = "bitpos" - args[1] = key - args[2] = bit - switch len(pos) { - case 0: - case 1: - args[3] = pos[0] - case 2: - args[3] = pos[0] - args[4] = pos[1] - default: - cmd := NewIntCmd(ctx) - cmd.SetErr(errors.New("too many arguments")) - return cmd - } - cmd := NewIntCmd(ctx, args...) - _ = c(ctx, cmd) - return cmd -} - -// BitPosSpan supports the `byte | bit` parameters in redis version 7.0, -// the bitpos command defaults to using byte type for the `start-end` range, -// which means it counts in bytes from start to end. you can set the value -// of "span" to determine the type of `start-end`. -// span = "bit", cmd: bitpos key bit start end bit -// span = "byte", cmd: bitpos key bit start end byte -func (c cmdable) BitPosSpan(ctx context.Context, key string, bit int8, start, end int64, span string) *IntCmd { - cmd := NewIntCmd(ctx, "bitpos", key, bit, start, end, span) - _ = c(ctx, cmd) - return cmd -} - -// BitField accepts multiple values: -// - BitField("set", "i1", "offset1", "value1","cmd2", "type2", "offset2", "value2") -// - BitField([]string{"cmd1", "type1", "offset1", "value1","cmd2", "type2", "offset2", "value2"}) -// - BitField([]interface{}{"cmd1", "type1", "offset1", "value1","cmd2", "type2", "offset2", "value2"}) -func (c cmdable) BitField(ctx context.Context, key string, values ...interface{}) *IntSliceCmd { - args := make([]interface{}, 2, 2+len(values)) - args[0] = "bitfield" - args[1] = key - args = appendArgs(args, values) - cmd := NewIntSliceCmd(ctx, args...) - _ = c(ctx, cmd) - return cmd -} - -// BitFieldRO - Read-only variant of the BITFIELD command. -// It is like the original BITFIELD but only accepts GET subcommand and can safely be used in read-only replicas. -// - BitFieldRO(ctx, key, "", "", "","") -func (c cmdable) BitFieldRO(ctx context.Context, key string, values ...interface{}) *IntSliceCmd { - args := make([]interface{}, 2, 2+len(values)) - args[0] = "BITFIELD_RO" - args[1] = key - if len(values)%2 != 0 { - c := NewIntSliceCmd(ctx) - c.SetErr(errors.New("BitFieldRO: invalid number of arguments, must be even")) - return c - } - for i := 0; i < len(values); i += 2 { - args = append(args, "GET", values[i], values[i+1]) - } - cmd := NewIntSliceCmd(ctx, args...) - _ = c(ctx, cmd) - return cmd -} diff --git a/vendor/github.com/redis/go-redis/v9/cluster_commands.go b/vendor/github.com/redis/go-redis/v9/cluster_commands.go deleted file mode 100644 index a02683f20..000000000 --- a/vendor/github.com/redis/go-redis/v9/cluster_commands.go +++ /dev/null @@ -1,205 +0,0 @@ -package redis - -import "context" - -type ClusterCmdable interface { - ClusterMyShardID(ctx context.Context) *StringCmd - ClusterMyID(ctx context.Context) *StringCmd - ClusterSlots(ctx context.Context) *ClusterSlotsCmd - ClusterShards(ctx context.Context) *ClusterShardsCmd - ClusterLinks(ctx context.Context) *ClusterLinksCmd - ClusterNodes(ctx context.Context) *StringCmd - ClusterMeet(ctx context.Context, host, port string) *StatusCmd - ClusterForget(ctx context.Context, nodeID string) *StatusCmd - ClusterReplicate(ctx context.Context, nodeID string) *StatusCmd - ClusterResetSoft(ctx context.Context) *StatusCmd - ClusterResetHard(ctx context.Context) *StatusCmd - ClusterInfo(ctx context.Context) *StringCmd - ClusterKeySlot(ctx context.Context, key string) *IntCmd - ClusterGetKeysInSlot(ctx context.Context, slot int, count int) *StringSliceCmd - ClusterCountFailureReports(ctx context.Context, nodeID string) *IntCmd - ClusterCountKeysInSlot(ctx context.Context, slot int) *IntCmd - ClusterDelSlots(ctx context.Context, slots ...int) *StatusCmd - ClusterDelSlotsRange(ctx context.Context, min, max int) *StatusCmd - ClusterSaveConfig(ctx context.Context) *StatusCmd - ClusterSlaves(ctx context.Context, nodeID string) *StringSliceCmd - ClusterFailover(ctx context.Context) *StatusCmd - ClusterAddSlots(ctx context.Context, slots ...int) *StatusCmd - ClusterAddSlotsRange(ctx context.Context, min, max int) *StatusCmd - ReadOnly(ctx context.Context) *StatusCmd - ReadWrite(ctx context.Context) *StatusCmd -} - -func (c cmdable) ClusterMyShardID(ctx context.Context) *StringCmd { - cmd := NewStringCmd(ctx, "cluster", "myshardid") - _ = c(ctx, cmd) - return cmd -} - -func (c cmdable) ClusterMyID(ctx context.Context) *StringCmd { - cmd := NewStringCmd(ctx, "cluster", "myid") - _ = c(ctx, cmd) - return cmd -} - -// ClusterSlots returns the mapping of cluster slots to nodes. -// -// Deprecated: Use ClusterShards instead as of Redis 7.0.0. -func (c cmdable) ClusterSlots(ctx context.Context) *ClusterSlotsCmd { - cmd := NewClusterSlotsCmd(ctx, "cluster", "slots") - _ = c(ctx, cmd) - return cmd -} - -func (c cmdable) ClusterShards(ctx context.Context) *ClusterShardsCmd { - cmd := NewClusterShardsCmd(ctx, "cluster", "shards") - _ = c(ctx, cmd) - return cmd -} - -func (c cmdable) ClusterLinks(ctx context.Context) *ClusterLinksCmd { - cmd := NewClusterLinksCmd(ctx, "cluster", "links") - _ = c(ctx, cmd) - return cmd -} - -func (c cmdable) ClusterNodes(ctx context.Context) *StringCmd { - cmd := NewStringCmd(ctx, "cluster", "nodes") - _ = c(ctx, cmd) - return cmd -} - -func (c cmdable) ClusterMeet(ctx context.Context, host, port string) *StatusCmd { - cmd := NewStatusCmd(ctx, "cluster", "meet", host, port) - _ = c(ctx, cmd) - return cmd -} - -func (c cmdable) ClusterForget(ctx context.Context, nodeID string) *StatusCmd { - cmd := NewStatusCmd(ctx, "cluster", "forget", nodeID) - _ = c(ctx, cmd) - return cmd -} - -func (c cmdable) ClusterReplicate(ctx context.Context, nodeID string) *StatusCmd { - cmd := NewStatusCmd(ctx, "cluster", "replicate", nodeID) - _ = c(ctx, cmd) - return cmd -} - -func (c cmdable) ClusterResetSoft(ctx context.Context) *StatusCmd { - cmd := NewStatusCmd(ctx, "cluster", "reset", "soft") - _ = c(ctx, cmd) - return cmd -} - -func (c cmdable) ClusterResetHard(ctx context.Context) *StatusCmd { - cmd := NewStatusCmd(ctx, "cluster", "reset", "hard") - _ = c(ctx, cmd) - return cmd -} - -func (c cmdable) ClusterInfo(ctx context.Context) *StringCmd { - cmd := NewStringCmd(ctx, "cluster", "info") - _ = c(ctx, cmd) - return cmd -} - -func (c cmdable) ClusterKeySlot(ctx context.Context, key string) *IntCmd { - cmd := NewIntCmd(ctx, "cluster", "keyslot", key) - _ = c(ctx, cmd) - return cmd -} - -func (c cmdable) ClusterGetKeysInSlot(ctx context.Context, slot int, count int) *StringSliceCmd { - cmd := NewStringSliceCmd(ctx, "cluster", "getkeysinslot", slot, count) - _ = c(ctx, cmd) - return cmd -} - -func (c cmdable) ClusterCountFailureReports(ctx context.Context, nodeID string) *IntCmd { - cmd := NewIntCmd(ctx, "cluster", "count-failure-reports", nodeID) - _ = c(ctx, cmd) - return cmd -} - -func (c cmdable) ClusterCountKeysInSlot(ctx context.Context, slot int) *IntCmd { - cmd := NewIntCmd(ctx, "cluster", "countkeysinslot", slot) - _ = c(ctx, cmd) - return cmd -} - -func (c cmdable) ClusterDelSlots(ctx context.Context, slots ...int) *StatusCmd { - args := make([]interface{}, 2+len(slots)) - args[0] = "cluster" - args[1] = "delslots" - for i, slot := range slots { - args[2+i] = slot - } - cmd := NewStatusCmd(ctx, args...) - _ = c(ctx, cmd) - return cmd -} - -func (c cmdable) ClusterDelSlotsRange(ctx context.Context, min, max int) *StatusCmd { - size := max - min + 1 - slots := make([]int, size) - for i := 0; i < size; i++ { - slots[i] = min + i - } - return c.ClusterDelSlots(ctx, slots...) -} - -func (c cmdable) ClusterSaveConfig(ctx context.Context) *StatusCmd { - cmd := NewStatusCmd(ctx, "cluster", "saveconfig") - _ = c(ctx, cmd) - return cmd -} - -// ClusterSlaves lists the replica nodes of a master node. -// -// Deprecated: Use ClusterReplicas instead as of Redis 5.0.0. -func (c cmdable) ClusterSlaves(ctx context.Context, nodeID string) *StringSliceCmd { - cmd := NewStringSliceCmd(ctx, "cluster", "slaves", nodeID) - _ = c(ctx, cmd) - return cmd -} - -func (c cmdable) ClusterFailover(ctx context.Context) *StatusCmd { - cmd := NewStatusCmd(ctx, "cluster", "failover") - _ = c(ctx, cmd) - return cmd -} - -func (c cmdable) ClusterAddSlots(ctx context.Context, slots ...int) *StatusCmd { - args := make([]interface{}, 2+len(slots)) - args[0] = "cluster" - args[1] = "addslots" - for i, num := range slots { - args[2+i] = num - } - cmd := NewStatusCmd(ctx, args...) - _ = c(ctx, cmd) - return cmd -} - -func (c cmdable) ClusterAddSlotsRange(ctx context.Context, min, max int) *StatusCmd { - size := max - min + 1 - slots := make([]int, size) - for i := 0; i < size; i++ { - slots[i] = min + i - } - return c.ClusterAddSlots(ctx, slots...) -} - -func (c cmdable) ReadOnly(ctx context.Context) *StatusCmd { - cmd := NewStatusCmd(ctx, "readonly") - _ = c(ctx, cmd) - return cmd -} - -func (c cmdable) ReadWrite(ctx context.Context) *StatusCmd { - cmd := NewStatusCmd(ctx, "readwrite") - _ = c(ctx, cmd) - return cmd -} diff --git a/vendor/github.com/redis/go-redis/v9/command.go b/vendor/github.com/redis/go-redis/v9/command.go deleted file mode 100644 index a2a2f0519..000000000 --- a/vendor/github.com/redis/go-redis/v9/command.go +++ /dev/null @@ -1,8027 +0,0 @@ -package redis - -import ( - "bufio" - "context" - "fmt" - "maps" - "net" - "regexp" - "strconv" - "strings" - "sync" - "time" - - "github.com/redis/go-redis/v9/internal" - "github.com/redis/go-redis/v9/internal/hscan" - "github.com/redis/go-redis/v9/internal/proto" - "github.com/redis/go-redis/v9/internal/routing" - "github.com/redis/go-redis/v9/internal/util" -) - -// keylessCommands contains Redis commands that have empty key specifications (9th slot empty) -// Only includes core Redis commands, excludes FT.*, ts.*, timeseries.*, search.* and subcommands -var keylessCommands = map[string]struct{}{ - "acl": {}, - "asking": {}, - "auth": {}, - "bgrewriteaof": {}, - "bgsave": {}, - "client": {}, - "cluster": {}, - "config": {}, - "debug": {}, - "discard": {}, - "echo": {}, - "exec": {}, - "failover": {}, - "function": {}, - "hello": {}, - "hotkeys": {}, - "latency": {}, - "lolwut": {}, - "module": {}, - "monitor": {}, - "multi": {}, - "pfselftest": {}, - "ping": {}, - "psubscribe": {}, - "psync": {}, - "publish": {}, - "pubsub": {}, - "punsubscribe": {}, - "quit": {}, - "readonly": {}, - "readwrite": {}, - "replconf": {}, - "replicaof": {}, - "role": {}, - "save": {}, - "script": {}, - "select": {}, - "shutdown": {}, - "slaveof": {}, - "slowlog": {}, - "subscribe": {}, - "swapdb": {}, - "sync": {}, - "unsubscribe": {}, - "unwatch": {}, - "wait": {}, -} - -// CmdTyper interface for getting command type -type CmdTyper interface { - GetCmdType() CmdType -} - -// CmdTypeGetter interface for getting command type without circular imports -type CmdTypeGetter interface { - GetCmdType() CmdType -} - -type CmdType uint8 - -const ( - CmdTypeGeneric CmdType = iota - CmdTypeString - CmdTypeInt - CmdTypeBool - CmdTypeFloat - CmdTypeStringSlice - CmdTypeIntSlice - CmdTypeFloatSlice - CmdTypeBoolSlice - CmdTypeMapStringString - CmdTypeMapStringInt - CmdTypeMapStringInterface - CmdTypeMapStringInterfaceSlice - CmdTypeSlice - CmdTypeStatus - CmdTypeDuration - CmdTypeTime - CmdTypeKeyValueSlice - CmdTypeStringStructMap - CmdTypeXMessageSlice - CmdTypeXStreamSlice - CmdTypeXPending - CmdTypeXPendingExt - CmdTypeXAutoClaim - CmdTypeXAutoClaimJustID - CmdTypeXInfoConsumers - CmdTypeXInfoGroups - CmdTypeXInfoStream - CmdTypeXInfoStreamFull - CmdTypeZSlice - CmdTypeZWithKey - CmdTypeScan - CmdTypeClusterSlots - CmdTypeGeoLocation - CmdTypeGeoSearchLocation - CmdTypeGeoPos - CmdTypeCommandsInfo - CmdTypeSlowLog - CmdTypeMapStringStringSlice - CmdTypeMapMapStringInterface - CmdTypeKeyValues - CmdTypeZSliceWithKey - CmdTypeFunctionList - CmdTypeFunctionStats - CmdTypeLCS - CmdTypeKeyFlags - CmdTypeClusterLinks - CmdTypeClusterShards - CmdTypeRankWithScore - CmdTypeClientInfo - CmdTypeACLLog - CmdTypeInfo - CmdTypeMonitor - CmdTypeJSON - CmdTypeJSONSlice - CmdTypeIntPointerSlice - CmdTypeScanDump - CmdTypeBFInfo - CmdTypeCFInfo - CmdTypeCMSInfo - CmdTypeTopKInfo - CmdTypeTDigestInfo - CmdTypeFTSynDump - CmdTypeAggregate - CmdTypeFTInfo - CmdTypeFTSpellCheck - CmdTypeFTSearch - CmdTypeTSTimestampValue - CmdTypeTSTimestampValueSlice - CmdTypeHotKeys -) - -type ( - CmdTypeXAutoClaimValue struct { - messages []XMessage - start string - } - - CmdTypeXAutoClaimJustIDValue struct { - ids []string - start string - } - - CmdTypeScanValue struct { - keys []string - cursor uint64 - } - - CmdTypeKeyValuesValue struct { - key string - values []string - } - - CmdTypeZSliceWithKeyValue struct { - key string - zSlice []Z - } -) - -type Cmder interface { - // command name. - // e.g. "set k v ex 10" -> "set", "cluster info" -> "cluster". - Name() string - - // full command name. - // e.g. "set k v ex 10" -> "set", "cluster info" -> "cluster info". - FullName() string - - // all args of the command. - // e.g. "set k v ex 10" -> "[set k v ex 10]". - Args() []interface{} - - // format request and response string. - // e.g. "set k v ex 10" -> "set k v ex 10: OK", "get k" -> "get k: v". - String() string - - // Clone creates a copy of the command. - Clone() Cmder - - stringArg(int) string - firstKeyPos() int8 - SetFirstKeyPos(int8) - stepCount() int8 - SetStepCount(int8) - - readTimeout() *time.Duration - readReply(rd *proto.Reader) error - readRawReply(rd *proto.Reader) error - SetErr(error) - Err() error - - // GetCmdType returns the command type for fast value extraction - GetCmdType() CmdType -} - -func setCmdsErr(cmds []Cmder, e error) { - for _, cmd := range cmds { - if cmd.Err() == nil { - cmd.SetErr(e) - } - } -} - -func cmdsFirstErr(cmds []Cmder) error { - for _, cmd := range cmds { - if err := cmd.Err(); err != nil { - return err - } - } - return nil -} - -func writeCmds(wr *proto.Writer, cmds []Cmder) error { - for _, cmd := range cmds { - if err := writeCmd(wr, cmd); err != nil { - return err - } - } - return nil -} - -func writeCmd(wr *proto.Writer, cmd Cmder) error { - return wr.WriteArgs(cmd.Args()) -} - -// cmdFirstKeyPos returns the position of the first key in the command's arguments. -// If the command does not have a key, it returns 0. -// TODO: Use the data in CommandInfo to determine the first key position. -func cmdFirstKeyPos(cmd Cmder) int { - if pos := cmd.firstKeyPos(); pos != 0 { - return int(pos) - } - - name := cmd.Name() - - // first check if the command is keyless - if _, ok := keylessCommands[name]; ok { - return 0 - } - - switch name { - case "eval", "evalsha", "eval_ro", "evalsha_ro": - if cmd.stringArg(2) != "0" { - return 3 - } - - return 0 - case "publish": - return 1 - case "memory": - // https://github.com/redis/redis/issues/7493 - if cmd.stringArg(1) == "usage" { - return 2 - } - } - return 1 -} - -func cmdString(cmd Cmder, val interface{}) string { - b := make([]byte, 0, 64) - - for i, arg := range cmd.Args() { - if i > 0 { - b = append(b, ' ') - } - b = internal.AppendArg(b, arg) - } - - if err := cmd.Err(); err != nil { - b = append(b, ": "...) - b = append(b, err.Error()...) - } else if val != nil { - b = append(b, ": "...) - b = internal.AppendArg(b, val) - } - - return util.BytesToString(b) -} - -//------------------------------------------------------------------------------ - -type baseCmd struct { - ctx context.Context - args []interface{} - err error - keyPos int8 - _stepCount int8 - rawVal interface{} - _readTimeout *time.Duration - cmdType CmdType -} - -var _ Cmder = (*Cmd)(nil) - -func (cmd *baseCmd) Name() string { - if len(cmd.args) == 0 { - return "" - } - // Cmd name must be lower cased. - return internal.ToLower(cmd.stringArg(0)) -} - -func (cmd *baseCmd) FullName() string { - switch name := cmd.Name(); name { - case "cluster", "command": - if len(cmd.args) == 1 { - return name - } - if s2, ok := cmd.args[1].(string); ok { - return name + " " + s2 - } - return name - default: - return name - } -} - -func (cmd *baseCmd) Args() []interface{} { - return cmd.args -} - -func (cmd *baseCmd) stringArg(pos int) string { - if pos < 0 || pos >= len(cmd.args) { - return "" - } - arg := cmd.args[pos] - switch v := arg.(type) { - case string: - return v - case []byte: - return string(v) - default: - // TODO: consider using appendArg - return fmt.Sprint(v) - } -} - -func (cmd *baseCmd) firstKeyPos() int8 { - return cmd.keyPos -} - -func (cmd *baseCmd) SetFirstKeyPos(keyPos int8) { - cmd.keyPos = keyPos -} - -func (cmd *baseCmd) stepCount() int8 { - return cmd._stepCount -} - -func (cmd *baseCmd) SetStepCount(stepCount int8) { - cmd._stepCount = stepCount -} - -func (cmd *baseCmd) SetErr(e error) { - cmd.err = e -} - -func (cmd *baseCmd) Err() error { - return cmd.err -} - -func (cmd *baseCmd) readTimeout() *time.Duration { - return cmd._readTimeout -} - -func (cmd *baseCmd) setReadTimeout(d time.Duration) { - cmd._readTimeout = &d -} - -func (cmd *baseCmd) readRawReply(rd *proto.Reader) (err error) { - cmd.rawVal, err = rd.ReadReply() - return err -} - -func (cmd *baseCmd) GetCmdType() CmdType { - return cmd.cmdType -} - -func (cmd *baseCmd) cloneBaseCmd() baseCmd { - var readTimeout *time.Duration - if cmd._readTimeout != nil { - timeout := *cmd._readTimeout - readTimeout = &timeout - } - - // Create a copy of args slice - args := make([]interface{}, len(cmd.args)) - copy(args, cmd.args) - - return baseCmd{ - ctx: cmd.ctx, - args: args, - err: cmd.err, - keyPos: cmd.keyPos, - _stepCount: cmd._stepCount, - rawVal: cmd.rawVal, - _readTimeout: readTimeout, - cmdType: cmd.cmdType, - } -} - -//------------------------------------------------------------------------------ - -type Cmd struct { - baseCmd - - val interface{} -} - -func NewCmd(ctx context.Context, args ...interface{}) *Cmd { - return &Cmd{ - baseCmd: baseCmd{ - ctx: ctx, - args: args, - cmdType: CmdTypeGeneric, - }, - } -} - -func (cmd *Cmd) String() string { - return cmdString(cmd, cmd.val) -} - -func (cmd *Cmd) SetVal(val interface{}) { - cmd.val = val -} - -func (cmd *Cmd) Val() interface{} { - return cmd.val -} - -func (cmd *Cmd) Result() (interface{}, error) { - return cmd.val, cmd.err -} - -func (cmd *Cmd) Text() (string, error) { - if cmd.err != nil { - return "", cmd.err - } - return toString(cmd.val) -} - -func toString(val interface{}) (string, error) { - switch val := val.(type) { - case string: - return val, nil - default: - err := fmt.Errorf("redis: unexpected type=%T for String", val) - return "", err - } -} - -func (cmd *Cmd) Int() (int, error) { - if cmd.err != nil { - return 0, cmd.err - } - switch val := cmd.val.(type) { - case int64: - return int(val), nil - case string: - return strconv.Atoi(val) - default: - err := fmt.Errorf("redis: unexpected type=%T for Int", val) - return 0, err - } -} - -func (cmd *Cmd) Int64() (int64, error) { - if cmd.err != nil { - return 0, cmd.err - } - return toInt64(cmd.val) -} - -func toInt64(val interface{}) (int64, error) { - switch val := val.(type) { - case int64: - return val, nil - case string: - return strconv.ParseInt(val, 10, 64) - default: - err := fmt.Errorf("redis: unexpected type=%T for Int64", val) - return 0, err - } -} - -func (cmd *Cmd) Uint64() (uint64, error) { - if cmd.err != nil { - return 0, cmd.err - } - return toUint64(cmd.val) -} - -func toUint64(val interface{}) (uint64, error) { - switch val := val.(type) { - case int64: - return uint64(val), nil - case string: - return strconv.ParseUint(val, 10, 64) - default: - err := fmt.Errorf("redis: unexpected type=%T for Uint64", val) - return 0, err - } -} - -func (cmd *Cmd) Float32() (float32, error) { - if cmd.err != nil { - return 0, cmd.err - } - return toFloat32(cmd.val) -} - -func toFloat32(val interface{}) (float32, error) { - switch val := val.(type) { - case int64: - return float32(val), nil - case string: - f, err := strconv.ParseFloat(val, 32) - if err != nil { - return 0, err - } - return float32(f), nil - default: - err := fmt.Errorf("redis: unexpected type=%T for Float32", val) - return 0, err - } -} - -func (cmd *Cmd) Float64() (float64, error) { - if cmd.err != nil { - return 0, cmd.err - } - return toFloat64(cmd.val) -} - -func toFloat64(val interface{}) (float64, error) { - switch val := val.(type) { - case int64: - return float64(val), nil - case string: - return strconv.ParseFloat(val, 64) - default: - err := fmt.Errorf("redis: unexpected type=%T for Float64", val) - return 0, err - } -} - -func (cmd *Cmd) Bool() (bool, error) { - if cmd.err != nil { - return false, cmd.err - } - return toBool(cmd.val) -} - -func toBool(val interface{}) (bool, error) { - switch val := val.(type) { - case bool: - return val, nil - case int64: - return val != 0, nil - case string: - return strconv.ParseBool(val) - default: - err := fmt.Errorf("redis: unexpected type=%T for Bool", val) - return false, err - } -} - -func (cmd *Cmd) Slice() ([]interface{}, error) { - if cmd.err != nil { - return nil, cmd.err - } - switch val := cmd.val.(type) { - case []interface{}: - return val, nil - default: - return nil, fmt.Errorf("redis: unexpected type=%T for Slice", val) - } -} - -func (cmd *Cmd) StringSlice() ([]string, error) { - slice, err := cmd.Slice() - if err != nil { - return nil, err - } - - ss := make([]string, len(slice)) - for i, iface := range slice { - val, err := toString(iface) - if err != nil { - return nil, err - } - ss[i] = val - } - return ss, nil -} - -func (cmd *Cmd) Int64Slice() ([]int64, error) { - slice, err := cmd.Slice() - if err != nil { - return nil, err - } - - nums := make([]int64, len(slice)) - for i, iface := range slice { - val, err := toInt64(iface) - if err != nil { - return nil, err - } - nums[i] = val - } - return nums, nil -} - -func (cmd *Cmd) Uint64Slice() ([]uint64, error) { - slice, err := cmd.Slice() - if err != nil { - return nil, err - } - - nums := make([]uint64, len(slice)) - for i, iface := range slice { - val, err := toUint64(iface) - if err != nil { - return nil, err - } - nums[i] = val - } - return nums, nil -} - -func (cmd *Cmd) Float32Slice() ([]float32, error) { - slice, err := cmd.Slice() - if err != nil { - return nil, err - } - - floats := make([]float32, len(slice)) - for i, iface := range slice { - val, err := toFloat32(iface) - if err != nil { - return nil, err - } - floats[i] = val - } - return floats, nil -} - -func (cmd *Cmd) Float64Slice() ([]float64, error) { - slice, err := cmd.Slice() - if err != nil { - return nil, err - } - - floats := make([]float64, len(slice)) - for i, iface := range slice { - val, err := toFloat64(iface) - if err != nil { - return nil, err - } - floats[i] = val - } - return floats, nil -} - -func (cmd *Cmd) BoolSlice() ([]bool, error) { - slice, err := cmd.Slice() - if err != nil { - return nil, err - } - - bools := make([]bool, len(slice)) - for i, iface := range slice { - val, err := toBool(iface) - if err != nil { - return nil, err - } - bools[i] = val - } - return bools, nil -} - -func (cmd *Cmd) readReply(rd *proto.Reader) (err error) { - cmd.val, err = rd.ReadReply() - return err -} - -func (cmd *Cmd) Clone() Cmder { - return &Cmd{ - baseCmd: cmd.cloneBaseCmd(), - val: cmd.val, - } -} - -//------------------------------------------------------------------------------ - -type SliceCmd struct { - baseCmd - - val []interface{} -} - -var _ Cmder = (*SliceCmd)(nil) - -func NewSliceCmd(ctx context.Context, args ...interface{}) *SliceCmd { - return &SliceCmd{ - baseCmd: baseCmd{ - ctx: ctx, - args: args, - cmdType: CmdTypeSlice, - }, - } -} - -func (cmd *SliceCmd) SetVal(val []interface{}) { - cmd.val = val -} - -func (cmd *SliceCmd) Val() []interface{} { - return cmd.val -} - -func (cmd *SliceCmd) Result() ([]interface{}, error) { - return cmd.val, cmd.err -} - -func (cmd *SliceCmd) String() string { - return cmdString(cmd, cmd.val) -} - -// Scan scans the results from the map into a destination struct. The map keys -// are matched in the Redis struct fields by the `redis:"field"` tag. -func (cmd *SliceCmd) Scan(dst interface{}) error { - if cmd.err != nil { - return cmd.err - } - - // Pass the list of keys and values. - // Skip the first two args for: HMGET key - var args []interface{} - if cmd.args[0] == "hmget" { - args = cmd.args[2:] - } else { - // Otherwise, it's: MGET field field ... - args = cmd.args[1:] - } - - return hscan.Scan(dst, args, cmd.val) -} - -func (cmd *SliceCmd) readReply(rd *proto.Reader) (err error) { - cmd.val, err = rd.ReadSlice() - return err -} - -func (cmd *SliceCmd) Clone() Cmder { - var val []interface{} - if cmd.val != nil { - val = make([]interface{}, len(cmd.val)) - copy(val, cmd.val) - } - return &SliceCmd{ - baseCmd: cmd.cloneBaseCmd(), - val: val, - } -} - -//------------------------------------------------------------------------------ - -type StatusCmd struct { - baseCmd - - val string -} - -var _ Cmder = (*StatusCmd)(nil) - -func NewStatusCmd(ctx context.Context, args ...interface{}) *StatusCmd { - return &StatusCmd{ - baseCmd: baseCmd{ - ctx: ctx, - args: args, - cmdType: CmdTypeStatus, - }, - } -} - -func (cmd *StatusCmd) SetVal(val string) { - cmd.val = val -} - -func (cmd *StatusCmd) Val() string { - return cmd.val -} - -func (cmd *StatusCmd) Result() (string, error) { - return cmd.val, cmd.err -} - -func (cmd *StatusCmd) Bytes() ([]byte, error) { - return util.StringToBytes(cmd.val), cmd.err -} - -func (cmd *StatusCmd) String() string { - return cmdString(cmd, cmd.val) -} - -func (cmd *StatusCmd) readReply(rd *proto.Reader) (err error) { - cmd.val, err = rd.ReadString() - return err -} - -func (cmd *StatusCmd) Clone() Cmder { - return &StatusCmd{ - baseCmd: cmd.cloneBaseCmd(), - val: cmd.val, - } -} - -//------------------------------------------------------------------------------ - -type IntCmd struct { - baseCmd - - val int64 -} - -var _ Cmder = (*IntCmd)(nil) - -func NewIntCmd(ctx context.Context, args ...interface{}) *IntCmd { - return &IntCmd{ - baseCmd: baseCmd{ - ctx: ctx, - args: args, - cmdType: CmdTypeInt, - }, - } -} - -func (cmd *IntCmd) SetVal(val int64) { - cmd.val = val -} - -func (cmd *IntCmd) Val() int64 { - return cmd.val -} - -func (cmd *IntCmd) Result() (int64, error) { - return cmd.val, cmd.err -} - -func (cmd *IntCmd) Uint64() (uint64, error) { - return uint64(cmd.val), cmd.err -} - -func (cmd *IntCmd) String() string { - return cmdString(cmd, cmd.val) -} - -func (cmd *IntCmd) readReply(rd *proto.Reader) (err error) { - cmd.val, err = rd.ReadInt() - return err -} - -func (cmd *IntCmd) Clone() Cmder { - return &IntCmd{ - baseCmd: cmd.cloneBaseCmd(), - val: cmd.val, - } -} - -//------------------------------------------------------------------------------ - -// DigestCmd is a command that returns a uint64 xxh3 hash digest. -// -// This command is specifically designed for the Redis DIGEST command, -// which returns the xxh3 hash of a key's value as a hex string. -// The hex string is automatically parsed to a uint64 value. -// -// The digest can be used for optimistic locking with SetIFDEQ, SetIFDNE, -// and DelExArgs commands. -// -// For examples of client-side digest generation and usage patterns, see: -// example/digest-optimistic-locking/ -// -// Redis 8.4+. See https://redis.io/commands/digest/ -type DigestCmd struct { - baseCmd - - val uint64 -} - -var _ Cmder = (*DigestCmd)(nil) - -func NewDigestCmd(ctx context.Context, args ...interface{}) *DigestCmd { - return &DigestCmd{ - baseCmd: baseCmd{ - ctx: ctx, - args: args, - }, - } -} - -func (cmd *DigestCmd) SetVal(val uint64) { - cmd.val = val -} - -func (cmd *DigestCmd) Val() uint64 { - return cmd.val -} - -func (cmd *DigestCmd) Result() (uint64, error) { - return cmd.val, cmd.err -} - -func (cmd *DigestCmd) String() string { - return cmdString(cmd, cmd.val) -} - -func (cmd *DigestCmd) Clone() Cmder { - return &DigestCmd{ - baseCmd: cmd.cloneBaseCmd(), - val: cmd.val, - } -} - -func (cmd *DigestCmd) readReply(rd *proto.Reader) (err error) { - // Redis DIGEST command returns a hex string (e.g., "a1b2c3d4e5f67890") - // We parse it as a uint64 xxh3 hash value - var hexStr string - hexStr, err = rd.ReadString() - if err != nil { - return err - } - - // Parse hex string to uint64 - cmd.val, err = strconv.ParseUint(hexStr, 16, 64) - return err -} - -//------------------------------------------------------------------------------ - -type IntSliceCmd struct { - baseCmd - - val []int64 -} - -var _ Cmder = (*IntSliceCmd)(nil) - -func NewIntSliceCmd(ctx context.Context, args ...interface{}) *IntSliceCmd { - return &IntSliceCmd{ - baseCmd: baseCmd{ - ctx: ctx, - args: args, - cmdType: CmdTypeIntSlice, - }, - } -} - -func (cmd *IntSliceCmd) SetVal(val []int64) { - cmd.val = val -} - -func (cmd *IntSliceCmd) Val() []int64 { - return cmd.val -} - -func (cmd *IntSliceCmd) Result() ([]int64, error) { - return cmd.val, cmd.err -} - -func (cmd *IntSliceCmd) String() string { - return cmdString(cmd, cmd.val) -} - -func (cmd *IntSliceCmd) readReply(rd *proto.Reader) error { - n, err := rd.ReadArrayLen() - if err != nil { - return err - } - cmd.val = make([]int64, n) - for i := 0; i < len(cmd.val); i++ { - if cmd.val[i], err = rd.ReadInt(); err != nil { - return err - } - } - return nil -} - -func (cmd *IntSliceCmd) Clone() Cmder { - var val []int64 - if cmd.val != nil { - val = make([]int64, len(cmd.val)) - copy(val, cmd.val) - } - return &IntSliceCmd{ - baseCmd: cmd.cloneBaseCmd(), - val: val, - } -} - -//------------------------------------------------------------------------------ - -type DurationCmd struct { - baseCmd - - val time.Duration - precision time.Duration -} - -var _ Cmder = (*DurationCmd)(nil) - -func NewDurationCmd(ctx context.Context, precision time.Duration, args ...interface{}) *DurationCmd { - return &DurationCmd{ - baseCmd: baseCmd{ - ctx: ctx, - args: args, - cmdType: CmdTypeDuration, - }, - precision: precision, - } -} - -func (cmd *DurationCmd) SetVal(val time.Duration) { - cmd.val = val -} - -func (cmd *DurationCmd) Val() time.Duration { - return cmd.val -} - -func (cmd *DurationCmd) Result() (time.Duration, error) { - return cmd.val, cmd.err -} - -func (cmd *DurationCmd) String() string { - return cmdString(cmd, cmd.val) -} - -func (cmd *DurationCmd) readReply(rd *proto.Reader) error { - n, err := rd.ReadInt() - if err != nil { - return err - } - switch n { - // -2 if the key does not exist - // -1 if the key exists but has no associated expire - case -2, -1: - cmd.val = time.Duration(n) - default: - cmd.val = time.Duration(n) * cmd.precision - } - return nil -} - -func (cmd *DurationCmd) Clone() Cmder { - return &DurationCmd{ - baseCmd: cmd.cloneBaseCmd(), - val: cmd.val, - precision: cmd.precision, - } -} - -//------------------------------------------------------------------------------ - -type TimeCmd struct { - baseCmd - - val time.Time -} - -var _ Cmder = (*TimeCmd)(nil) - -func NewTimeCmd(ctx context.Context, args ...interface{}) *TimeCmd { - return &TimeCmd{ - baseCmd: baseCmd{ - ctx: ctx, - args: args, - cmdType: CmdTypeTime, - }, - } -} - -func (cmd *TimeCmd) SetVal(val time.Time) { - cmd.val = val -} - -func (cmd *TimeCmd) Val() time.Time { - return cmd.val -} - -func (cmd *TimeCmd) Result() (time.Time, error) { - return cmd.val, cmd.err -} - -func (cmd *TimeCmd) String() string { - return cmdString(cmd, cmd.val) -} - -func (cmd *TimeCmd) readReply(rd *proto.Reader) error { - if err := rd.ReadFixedArrayLen(2); err != nil { - return err - } - second, err := rd.ReadInt() - if err != nil { - return err - } - microsecond, err := rd.ReadInt() - if err != nil { - return err - } - cmd.val = time.Unix(second, microsecond*1000) - return nil -} - -func (cmd *TimeCmd) Clone() Cmder { - return &TimeCmd{ - baseCmd: cmd.cloneBaseCmd(), - val: cmd.val, - } -} - -//------------------------------------------------------------------------------ - -type BoolCmd struct { - baseCmd - - val bool -} - -var _ Cmder = (*BoolCmd)(nil) - -func NewBoolCmd(ctx context.Context, args ...interface{}) *BoolCmd { - return &BoolCmd{ - baseCmd: baseCmd{ - ctx: ctx, - args: args, - cmdType: CmdTypeBool, - }, - } -} - -func (cmd *BoolCmd) SetVal(val bool) { - cmd.val = val -} - -func (cmd *BoolCmd) Val() bool { - return cmd.val -} - -func (cmd *BoolCmd) Result() (bool, error) { - return cmd.val, cmd.err -} - -func (cmd *BoolCmd) String() string { - return cmdString(cmd, cmd.val) -} - -func (cmd *BoolCmd) readReply(rd *proto.Reader) (err error) { - cmd.val, err = rd.ReadBool() - - // `SET key value NX` returns nil when key already exists. But - // `SETNX key value` returns bool (0/1). So convert nil to bool. - if err == Nil { - cmd.val = false - err = nil - } - return err -} - -func (cmd *BoolCmd) Clone() Cmder { - return &BoolCmd{ - baseCmd: cmd.cloneBaseCmd(), - val: cmd.val, - } -} - -//------------------------------------------------------------------------------ - -type StringCmd struct { - baseCmd - - val string -} - -var _ Cmder = (*StringCmd)(nil) - -func NewStringCmd(ctx context.Context, args ...interface{}) *StringCmd { - return &StringCmd{ - baseCmd: baseCmd{ - ctx: ctx, - args: args, - cmdType: CmdTypeString, - }, - } -} - -func (cmd *StringCmd) SetVal(val string) { - cmd.val = val -} - -func (cmd *StringCmd) Val() string { - return cmd.val -} - -func (cmd *StringCmd) Result() (string, error) { - return cmd.val, cmd.err -} - -func (cmd *StringCmd) Bytes() ([]byte, error) { - return util.StringToBytes(cmd.val), cmd.err -} - -func (cmd *StringCmd) Bool() (bool, error) { - if cmd.err != nil { - return false, cmd.err - } - return strconv.ParseBool(cmd.val) -} - -func (cmd *StringCmd) Int() (int, error) { - if cmd.err != nil { - return 0, cmd.err - } - return strconv.Atoi(cmd.val) -} - -func (cmd *StringCmd) Int64() (int64, error) { - if cmd.err != nil { - return 0, cmd.err - } - return strconv.ParseInt(cmd.val, 10, 64) -} - -func (cmd *StringCmd) Uint64() (uint64, error) { - if cmd.err != nil { - return 0, cmd.err - } - return strconv.ParseUint(cmd.val, 10, 64) -} - -func (cmd *StringCmd) Float32() (float32, error) { - if cmd.err != nil { - return 0, cmd.err - } - f, err := strconv.ParseFloat(cmd.val, 32) - if err != nil { - return 0, err - } - return float32(f), nil -} - -func (cmd *StringCmd) Float64() (float64, error) { - if cmd.err != nil { - return 0, cmd.err - } - return strconv.ParseFloat(cmd.val, 64) -} - -func (cmd *StringCmd) Time() (time.Time, error) { - if cmd.err != nil { - return time.Time{}, cmd.err - } - return time.Parse(time.RFC3339Nano, cmd.val) -} - -func (cmd *StringCmd) Scan(val interface{}) error { - if cmd.err != nil { - return cmd.err - } - return proto.Scan([]byte(cmd.val), val) -} - -func (cmd *StringCmd) String() string { - return cmdString(cmd, cmd.val) -} - -func (cmd *StringCmd) readReply(rd *proto.Reader) (err error) { - cmd.val, err = rd.ReadString() - return err -} - -func (cmd *StringCmd) Clone() Cmder { - return &StringCmd{ - baseCmd: cmd.cloneBaseCmd(), - val: cmd.val, - } -} - -//------------------------------------------------------------------------------ - -type FloatCmd struct { - baseCmd - - val float64 -} - -var _ Cmder = (*FloatCmd)(nil) - -func NewFloatCmd(ctx context.Context, args ...interface{}) *FloatCmd { - return &FloatCmd{ - baseCmd: baseCmd{ - ctx: ctx, - args: args, - cmdType: CmdTypeFloat, - }, - } -} - -func (cmd *FloatCmd) SetVal(val float64) { - cmd.val = val -} - -func (cmd *FloatCmd) Val() float64 { - return cmd.val -} - -func (cmd *FloatCmd) Result() (float64, error) { - return cmd.val, cmd.err -} - -func (cmd *FloatCmd) String() string { - return cmdString(cmd, cmd.val) -} - -func (cmd *FloatCmd) readReply(rd *proto.Reader) (err error) { - cmd.val, err = rd.ReadFloat() - return err -} - -func (cmd *FloatCmd) Clone() Cmder { - return &FloatCmd{ - baseCmd: cmd.cloneBaseCmd(), - val: cmd.val, - } -} - -//------------------------------------------------------------------------------ - -type FloatSliceCmd struct { - baseCmd - - val []float64 -} - -var _ Cmder = (*FloatSliceCmd)(nil) - -func NewFloatSliceCmd(ctx context.Context, args ...interface{}) *FloatSliceCmd { - return &FloatSliceCmd{ - baseCmd: baseCmd{ - ctx: ctx, - args: args, - cmdType: CmdTypeFloatSlice, - }, - } -} - -func (cmd *FloatSliceCmd) SetVal(val []float64) { - cmd.val = val -} - -func (cmd *FloatSliceCmd) Val() []float64 { - return cmd.val -} - -func (cmd *FloatSliceCmd) Result() ([]float64, error) { - return cmd.val, cmd.err -} - -func (cmd *FloatSliceCmd) String() string { - return cmdString(cmd, cmd.val) -} - -func (cmd *FloatSliceCmd) readReply(rd *proto.Reader) error { - n, err := rd.ReadArrayLen() - if err != nil { - return err - } - - cmd.val = make([]float64, n) - for i := 0; i < len(cmd.val); i++ { - switch num, err := rd.ReadFloat(); { - case err == Nil: - cmd.val[i] = 0 - case err != nil: - return err - default: - cmd.val[i] = num - } - } - return nil -} - -func (cmd *FloatSliceCmd) Clone() Cmder { - var val []float64 - if cmd.val != nil { - val = make([]float64, len(cmd.val)) - copy(val, cmd.val) - } - return &FloatSliceCmd{ - baseCmd: cmd.cloneBaseCmd(), - val: val, - } -} - -//------------------------------------------------------------------------------ - -type StringSliceCmd struct { - baseCmd - - val []string -} - -var _ Cmder = (*StringSliceCmd)(nil) - -func NewStringSliceCmd(ctx context.Context, args ...interface{}) *StringSliceCmd { - return &StringSliceCmd{ - baseCmd: baseCmd{ - ctx: ctx, - args: args, - cmdType: CmdTypeStringSlice, - }, - } -} - -func (cmd *StringSliceCmd) SetVal(val []string) { - cmd.val = val -} - -func (cmd *StringSliceCmd) Val() []string { - return cmd.val -} - -func (cmd *StringSliceCmd) Result() ([]string, error) { - return cmd.val, cmd.err -} - -func (cmd *StringSliceCmd) String() string { - return cmdString(cmd, cmd.val) -} - -func (cmd *StringSliceCmd) ScanSlice(container interface{}) error { - return proto.ScanSlice(cmd.val, container) -} - -func (cmd *StringSliceCmd) readReply(rd *proto.Reader) error { - n, err := rd.ReadArrayLen() - if err != nil { - return err - } - cmd.val = make([]string, n) - for i := 0; i < len(cmd.val); i++ { - switch s, err := rd.ReadString(); { - case err == Nil: - cmd.val[i] = "" - case err != nil: - return err - default: - cmd.val[i] = s - } - } - return nil -} - -func (cmd *StringSliceCmd) Clone() Cmder { - var val []string - if cmd.val != nil { - val = make([]string, len(cmd.val)) - copy(val, cmd.val) - } - return &StringSliceCmd{ - baseCmd: cmd.cloneBaseCmd(), - val: val, - } -} - -//------------------------------------------------------------------------------ - -type KeyValue struct { - Key string - Value string -} - -type KeyValueSliceCmd struct { - baseCmd - - val []KeyValue -} - -var _ Cmder = (*KeyValueSliceCmd)(nil) - -func NewKeyValueSliceCmd(ctx context.Context, args ...interface{}) *KeyValueSliceCmd { - return &KeyValueSliceCmd{ - baseCmd: baseCmd{ - ctx: ctx, - args: args, - cmdType: CmdTypeKeyValueSlice, - }, - } -} - -func (cmd *KeyValueSliceCmd) SetVal(val []KeyValue) { - cmd.val = val -} - -func (cmd *KeyValueSliceCmd) Val() []KeyValue { - return cmd.val -} - -func (cmd *KeyValueSliceCmd) Result() ([]KeyValue, error) { - return cmd.val, cmd.err -} - -func (cmd *KeyValueSliceCmd) String() string { - return cmdString(cmd, cmd.val) -} - -// Many commands will respond to two formats: -// 1. 1) "one" -// 2. (double) 1 -// 2. 1) "two" -// 2. (double) 2 -// -// OR: -// 1. "two" -// 2. (double) 2 -// 3. "one" -// 4. (double) 1 -func (cmd *KeyValueSliceCmd) readReply(rd *proto.Reader) error { // nolint:dupl - n, err := rd.ReadArrayLen() - if err != nil { - return err - } - - // If the n is 0, can't continue reading. - if n == 0 { - cmd.val = make([]KeyValue, 0) - return nil - } - - typ, err := rd.PeekReplyType() - if err != nil { - return err - } - array := typ == proto.RespArray - - if array { - cmd.val = make([]KeyValue, n) - } else { - cmd.val = make([]KeyValue, n/2) - } - - for i := 0; i < len(cmd.val); i++ { - if array { - if err = rd.ReadFixedArrayLen(2); err != nil { - return err - } - } - - if cmd.val[i].Key, err = rd.ReadString(); err != nil { - return err - } - - if cmd.val[i].Value, err = rd.ReadString(); err != nil { - return err - } - } - - return nil -} - -func (cmd *KeyValueSliceCmd) Clone() Cmder { - var val []KeyValue - if cmd.val != nil { - val = make([]KeyValue, len(cmd.val)) - copy(val, cmd.val) - } - return &KeyValueSliceCmd{ - baseCmd: cmd.cloneBaseCmd(), - val: val, - } -} - -//------------------------------------------------------------------------------ - -type BoolSliceCmd struct { - baseCmd - - val []bool -} - -var _ Cmder = (*BoolSliceCmd)(nil) - -func NewBoolSliceCmd(ctx context.Context, args ...interface{}) *BoolSliceCmd { - return &BoolSliceCmd{ - baseCmd: baseCmd{ - ctx: ctx, - args: args, - cmdType: CmdTypeBoolSlice, - }, - } -} - -func (cmd *BoolSliceCmd) SetVal(val []bool) { - cmd.val = val -} - -func (cmd *BoolSliceCmd) Val() []bool { - return cmd.val -} - -func (cmd *BoolSliceCmd) Result() ([]bool, error) { - return cmd.val, cmd.err -} - -func (cmd *BoolSliceCmd) String() string { - return cmdString(cmd, cmd.val) -} - -func (cmd *BoolSliceCmd) readReply(rd *proto.Reader) error { - n, err := rd.ReadArrayLen() - if err != nil { - return err - } - cmd.val = make([]bool, n) - for i := 0; i < len(cmd.val); i++ { - if cmd.val[i], err = rd.ReadBool(); err != nil { - return err - } - } - return nil -} - -func (cmd *BoolSliceCmd) Clone() Cmder { - var val []bool - if cmd.val != nil { - val = make([]bool, len(cmd.val)) - copy(val, cmd.val) - } - return &BoolSliceCmd{ - baseCmd: cmd.cloneBaseCmd(), - val: val, - } -} - -//------------------------------------------------------------------------------ - -type MapStringStringCmd struct { - baseCmd - - val map[string]string -} - -var _ Cmder = (*MapStringStringCmd)(nil) - -func NewMapStringStringCmd(ctx context.Context, args ...interface{}) *MapStringStringCmd { - return &MapStringStringCmd{ - baseCmd: baseCmd{ - ctx: ctx, - args: args, - cmdType: CmdTypeMapStringString, - }, - } -} - -func (cmd *MapStringStringCmd) Val() map[string]string { - return cmd.val -} - -func (cmd *MapStringStringCmd) SetVal(val map[string]string) { - cmd.val = val -} - -func (cmd *MapStringStringCmd) Result() (map[string]string, error) { - return cmd.val, cmd.err -} - -func (cmd *MapStringStringCmd) String() string { - return cmdString(cmd, cmd.val) -} - -// Scan scans the results from the map into a destination struct. The map keys -// are matched in the Redis struct fields by the `redis:"field"` tag. -func (cmd *MapStringStringCmd) Scan(dest interface{}) error { - if cmd.err != nil { - return cmd.err - } - - strct, err := hscan.Struct(dest) - if err != nil { - return err - } - - for k, v := range cmd.val { - if err := strct.Scan(k, v); err != nil { - return err - } - } - - return nil -} - -func (cmd *MapStringStringCmd) readReply(rd *proto.Reader) error { - n, err := rd.ReadMapLen() - if err != nil { - return err - } - - cmd.val = make(map[string]string, n) - for i := 0; i < n; i++ { - key, err := rd.ReadString() - if err != nil { - return err - } - - value, err := rd.ReadString() - if err != nil { - return err - } - - cmd.val[key] = value - } - return nil -} - -func (cmd *MapStringStringCmd) Clone() Cmder { - var val map[string]string - if cmd.val != nil { - val = make(map[string]string, len(cmd.val)) - for k, v := range cmd.val { - val[k] = v - } - } - return &MapStringStringCmd{ - baseCmd: cmd.cloneBaseCmd(), - val: val, - } -} - -//------------------------------------------------------------------------------ - -type MapStringIntCmd struct { - baseCmd - - val map[string]int64 -} - -var _ Cmder = (*MapStringIntCmd)(nil) - -func NewMapStringIntCmd(ctx context.Context, args ...interface{}) *MapStringIntCmd { - return &MapStringIntCmd{ - baseCmd: baseCmd{ - ctx: ctx, - args: args, - cmdType: CmdTypeMapStringInt, - }, - } -} - -func (cmd *MapStringIntCmd) SetVal(val map[string]int64) { - cmd.val = val -} - -func (cmd *MapStringIntCmd) Val() map[string]int64 { - return cmd.val -} - -func (cmd *MapStringIntCmd) Result() (map[string]int64, error) { - return cmd.val, cmd.err -} - -func (cmd *MapStringIntCmd) String() string { - return cmdString(cmd, cmd.val) -} - -func (cmd *MapStringIntCmd) readReply(rd *proto.Reader) error { - n, err := rd.ReadMapLen() - if err != nil { - return err - } - - cmd.val = make(map[string]int64, n) - for i := 0; i < n; i++ { - key, err := rd.ReadString() - if err != nil { - return err - } - - nn, err := rd.ReadInt() - if err != nil { - return err - } - cmd.val[key] = nn - } - return nil -} - -func (cmd *MapStringIntCmd) Clone() Cmder { - var val map[string]int64 - if cmd.val != nil { - val = make(map[string]int64, len(cmd.val)) - for k, v := range cmd.val { - val[k] = v - } - } - return &MapStringIntCmd{ - baseCmd: cmd.cloneBaseCmd(), - val: val, - } -} - -// ------------------------------------------------------------------------------ -type MapStringSliceInterfaceCmd struct { - baseCmd - val map[string][]interface{} -} - -func NewMapStringSliceInterfaceCmd(ctx context.Context, args ...interface{}) *MapStringSliceInterfaceCmd { - return &MapStringSliceInterfaceCmd{ - baseCmd: baseCmd{ - ctx: ctx, - args: args, - cmdType: CmdTypeMapStringInterfaceSlice, - }, - } -} - -func (cmd *MapStringSliceInterfaceCmd) String() string { - return cmdString(cmd, cmd.val) -} - -func (cmd *MapStringSliceInterfaceCmd) SetVal(val map[string][]interface{}) { - cmd.val = val -} - -func (cmd *MapStringSliceInterfaceCmd) Result() (map[string][]interface{}, error) { - return cmd.val, cmd.err -} - -func (cmd *MapStringSliceInterfaceCmd) Val() map[string][]interface{} { - return cmd.val -} - -func (cmd *MapStringSliceInterfaceCmd) readReply(rd *proto.Reader) (err error) { - readType, err := rd.PeekReplyType() - if err != nil { - return err - } - - cmd.val = make(map[string][]interface{}) - - switch readType { - case proto.RespMap: - n, err := rd.ReadMapLen() - if err != nil { - return err - } - for i := 0; i < n; i++ { - k, err := rd.ReadString() - if err != nil { - return err - } - nn, err := rd.ReadArrayLen() - if err != nil { - return err - } - cmd.val[k] = make([]interface{}, nn) - for j := 0; j < nn; j++ { - value, err := rd.ReadReply() - if err != nil { - return err - } - cmd.val[k][j] = value - } - } - case proto.RespArray: - // RESP2 response - n, err := rd.ReadArrayLen() - if err != nil { - return err - } - - for i := 0; i < n; i++ { - // Each entry in this array is itself an array with key details - itemLen, err := rd.ReadArrayLen() - if err != nil { - return err - } - - key, err := rd.ReadString() - if err != nil { - return err - } - cmd.val[key] = make([]interface{}, 0, itemLen-1) - for j := 1; j < itemLen; j++ { - // Read the inner array for timestamp-value pairs - data, err := rd.ReadReply() - if err != nil { - return err - } - cmd.val[key] = append(cmd.val[key], data) - } - } - } - - return nil -} - -func (cmd *MapStringSliceInterfaceCmd) Clone() Cmder { - var val map[string][]interface{} - if cmd.val != nil { - val = make(map[string][]interface{}, len(cmd.val)) - for k, v := range cmd.val { - if v != nil { - newSlice := make([]interface{}, len(v)) - copy(newSlice, v) - val[k] = newSlice - } - } - } - return &MapStringSliceInterfaceCmd{ - baseCmd: cmd.cloneBaseCmd(), - val: val, - } -} - -//------------------------------------------------------------------------------ - -type StringStructMapCmd struct { - baseCmd - - val map[string]struct{} -} - -var _ Cmder = (*StringStructMapCmd)(nil) - -func NewStringStructMapCmd(ctx context.Context, args ...interface{}) *StringStructMapCmd { - return &StringStructMapCmd{ - baseCmd: baseCmd{ - ctx: ctx, - args: args, - cmdType: CmdTypeStringStructMap, - }, - } -} - -func (cmd *StringStructMapCmd) SetVal(val map[string]struct{}) { - cmd.val = val -} - -func (cmd *StringStructMapCmd) Val() map[string]struct{} { - return cmd.val -} - -func (cmd *StringStructMapCmd) Result() (map[string]struct{}, error) { - return cmd.val, cmd.err -} - -func (cmd *StringStructMapCmd) String() string { - return cmdString(cmd, cmd.val) -} - -func (cmd *StringStructMapCmd) readReply(rd *proto.Reader) error { - n, err := rd.ReadArrayLen() - if err != nil { - return err - } - - cmd.val = make(map[string]struct{}, n) - for i := 0; i < n; i++ { - key, err := rd.ReadString() - if err != nil { - return err - } - cmd.val[key] = struct{}{} - } - return nil -} - -func (cmd *StringStructMapCmd) Clone() Cmder { - var val map[string]struct{} - if cmd.val != nil { - val = maps.Clone(cmd.val) - } - return &StringStructMapCmd{ - baseCmd: cmd.cloneBaseCmd(), - val: val, - } -} - -//------------------------------------------------------------------------------ - -type XMessage struct { - ID string - Values map[string]interface{} - // MillisElapsedFromDelivery is the number of milliseconds since the entry was last delivered. - // Only populated when using XREADGROUP with CLAIM argument for claimed entries. - MillisElapsedFromDelivery int64 - // DeliveredCount is the number of times the entry was delivered. - // Only populated when using XREADGROUP with CLAIM argument for claimed entries. - DeliveredCount int64 -} - -type XMessageSliceCmd struct { - baseCmd - - val []XMessage -} - -var _ Cmder = (*XMessageSliceCmd)(nil) - -func NewXMessageSliceCmd(ctx context.Context, args ...interface{}) *XMessageSliceCmd { - return &XMessageSliceCmd{ - baseCmd: baseCmd{ - ctx: ctx, - args: args, - cmdType: CmdTypeXMessageSlice, - }, - } -} - -func (cmd *XMessageSliceCmd) SetVal(val []XMessage) { - cmd.val = val -} - -func (cmd *XMessageSliceCmd) Val() []XMessage { - return cmd.val -} - -func (cmd *XMessageSliceCmd) Result() ([]XMessage, error) { - return cmd.val, cmd.err -} - -func (cmd *XMessageSliceCmd) String() string { - return cmdString(cmd, cmd.val) -} - -func (cmd *XMessageSliceCmd) readReply(rd *proto.Reader) (err error) { - cmd.val, err = readXMessageSlice(rd) - return err -} - -func (cmd *XMessageSliceCmd) Clone() Cmder { - var val []XMessage - if cmd.val != nil { - val = make([]XMessage, len(cmd.val)) - for i, msg := range cmd.val { - val[i] = XMessage{ - ID: msg.ID, - } - if msg.Values != nil { - val[i].Values = make(map[string]interface{}, len(msg.Values)) - for k, v := range msg.Values { - val[i].Values[k] = v - } - } - } - } - return &XMessageSliceCmd{ - baseCmd: cmd.cloneBaseCmd(), - val: val, - } -} - -func readXMessageSlice(rd *proto.Reader) ([]XMessage, error) { - n, err := rd.ReadArrayLen() - if err != nil { - return nil, err - } - - msgs := make([]XMessage, n) - for i := 0; i < len(msgs); i++ { - if msgs[i], err = readXMessage(rd); err != nil { - return nil, err - } - } - return msgs, nil -} - -func readXMessage(rd *proto.Reader) (XMessage, error) { - // Read array length can be 2 or 4 (with CLAIM metadata) - n, err := rd.ReadArrayLen() - if err != nil { - return XMessage{}, err - } - - if n != 2 && n != 4 { - return XMessage{}, fmt.Errorf("redis: got %d elements in the XMessage array, expected 2 or 4", n) - } - - id, err := rd.ReadString() - if err != nil { - return XMessage{}, err - } - - v, err := stringInterfaceMapParser(rd) - if err != nil { - if err != proto.Nil { - return XMessage{}, err - } - } - - msg := XMessage{ - ID: id, - Values: v, - } - - if n == 4 { - msg.MillisElapsedFromDelivery, err = rd.ReadInt() - if err != nil { - return XMessage{}, err - } - - msg.DeliveredCount, err = rd.ReadInt() - if err != nil { - return XMessage{}, err - } - } - - return msg, nil -} - -func stringInterfaceMapParser(rd *proto.Reader) (map[string]interface{}, error) { - n, err := rd.ReadMapLen() - if err != nil { - return nil, err - } - - m := make(map[string]interface{}, n) - for i := 0; i < n; i++ { - key, err := rd.ReadString() - if err != nil { - return nil, err - } - - value, err := rd.ReadString() - if err != nil { - return nil, err - } - - m[key] = value - } - return m, nil -} - -//------------------------------------------------------------------------------ - -type XStream struct { - Stream string - Messages []XMessage -} - -type XStreamSliceCmd struct { - baseCmd - - val []XStream -} - -var _ Cmder = (*XStreamSliceCmd)(nil) - -func NewXStreamSliceCmd(ctx context.Context, args ...interface{}) *XStreamSliceCmd { - return &XStreamSliceCmd{ - baseCmd: baseCmd{ - ctx: ctx, - args: args, - cmdType: CmdTypeXStreamSlice, - }, - } -} - -func (cmd *XStreamSliceCmd) SetVal(val []XStream) { - cmd.val = val -} - -func (cmd *XStreamSliceCmd) Val() []XStream { - return cmd.val -} - -func (cmd *XStreamSliceCmd) Result() ([]XStream, error) { - return cmd.val, cmd.err -} - -func (cmd *XStreamSliceCmd) String() string { - return cmdString(cmd, cmd.val) -} - -func (cmd *XStreamSliceCmd) readReply(rd *proto.Reader) error { - typ, err := rd.PeekReplyType() - if err != nil { - return err - } - - var n int - if typ == proto.RespMap { - n, err = rd.ReadMapLen() - } else { - n, err = rd.ReadArrayLen() - } - if err != nil { - return err - } - cmd.val = make([]XStream, n) - for i := 0; i < len(cmd.val); i++ { - if typ != proto.RespMap { - if err = rd.ReadFixedArrayLen(2); err != nil { - return err - } - } - if cmd.val[i].Stream, err = rd.ReadString(); err != nil { - return err - } - if cmd.val[i].Messages, err = readXMessageSlice(rd); err != nil { - return err - } - } - return nil -} - -func (cmd *XStreamSliceCmd) Clone() Cmder { - var val []XStream - if cmd.val != nil { - val = make([]XStream, len(cmd.val)) - for i, stream := range cmd.val { - val[i] = XStream{ - Stream: stream.Stream, - } - if stream.Messages != nil { - val[i].Messages = make([]XMessage, len(stream.Messages)) - for j, msg := range stream.Messages { - val[i].Messages[j] = XMessage{ - ID: msg.ID, - } - if msg.Values != nil { - val[i].Messages[j].Values = make(map[string]interface{}, len(msg.Values)) - for k, v := range msg.Values { - val[i].Messages[j].Values[k] = v - } - } - } - } - } - } - return &XStreamSliceCmd{ - baseCmd: cmd.cloneBaseCmd(), - val: val, - } -} - -//------------------------------------------------------------------------------ - -type XPending struct { - Count int64 - Lower string - Higher string - Consumers map[string]int64 -} - -type XPendingCmd struct { - baseCmd - val *XPending -} - -var _ Cmder = (*XPendingCmd)(nil) - -func NewXPendingCmd(ctx context.Context, args ...interface{}) *XPendingCmd { - return &XPendingCmd{ - baseCmd: baseCmd{ - ctx: ctx, - args: args, - cmdType: CmdTypeXPending, - }, - } -} - -func (cmd *XPendingCmd) SetVal(val *XPending) { - cmd.val = val -} - -func (cmd *XPendingCmd) Val() *XPending { - return cmd.val -} - -func (cmd *XPendingCmd) Result() (*XPending, error) { - return cmd.val, cmd.err -} - -func (cmd *XPendingCmd) String() string { - return cmdString(cmd, cmd.val) -} - -func (cmd *XPendingCmd) readReply(rd *proto.Reader) error { - var err error - if err = rd.ReadFixedArrayLen(4); err != nil { - return err - } - cmd.val = &XPending{} - - if cmd.val.Count, err = rd.ReadInt(); err != nil { - return err - } - - if cmd.val.Lower, err = rd.ReadString(); err != nil && err != Nil { - return err - } - - if cmd.val.Higher, err = rd.ReadString(); err != nil && err != Nil { - return err - } - - n, err := rd.ReadArrayLen() - if err != nil && err != Nil { - return err - } - cmd.val.Consumers = make(map[string]int64, n) - for i := 0; i < n; i++ { - if err = rd.ReadFixedArrayLen(2); err != nil { - return err - } - - consumerName, err := rd.ReadString() - if err != nil { - return err - } - consumerPending, err := rd.ReadInt() - if err != nil { - return err - } - cmd.val.Consumers[consumerName] = consumerPending - } - return nil -} - -func (cmd *XPendingCmd) Clone() Cmder { - var val *XPending - if cmd.val != nil { - val = &XPending{ - Count: cmd.val.Count, - Lower: cmd.val.Lower, - Higher: cmd.val.Higher, - } - if cmd.val.Consumers != nil { - val.Consumers = make(map[string]int64, len(cmd.val.Consumers)) - for k, v := range cmd.val.Consumers { - val.Consumers[k] = v - } - } - } - return &XPendingCmd{ - baseCmd: cmd.cloneBaseCmd(), - val: val, - } -} - -//------------------------------------------------------------------------------ - -type XPendingExt struct { - ID string - Consumer string - Idle time.Duration - RetryCount int64 -} - -type XPendingExtCmd struct { - baseCmd - val []XPendingExt -} - -var _ Cmder = (*XPendingExtCmd)(nil) - -func NewXPendingExtCmd(ctx context.Context, args ...interface{}) *XPendingExtCmd { - return &XPendingExtCmd{ - baseCmd: baseCmd{ - ctx: ctx, - args: args, - cmdType: CmdTypeXPendingExt, - }, - } -} - -func (cmd *XPendingExtCmd) SetVal(val []XPendingExt) { - cmd.val = val -} - -func (cmd *XPendingExtCmd) Val() []XPendingExt { - return cmd.val -} - -func (cmd *XPendingExtCmd) Result() ([]XPendingExt, error) { - return cmd.val, cmd.err -} - -func (cmd *XPendingExtCmd) String() string { - return cmdString(cmd, cmd.val) -} - -func (cmd *XPendingExtCmd) readReply(rd *proto.Reader) error { - n, err := rd.ReadArrayLen() - if err != nil { - return err - } - cmd.val = make([]XPendingExt, n) - - for i := 0; i < len(cmd.val); i++ { - if err = rd.ReadFixedArrayLen(4); err != nil { - return err - } - - if cmd.val[i].ID, err = rd.ReadString(); err != nil { - return err - } - - if cmd.val[i].Consumer, err = rd.ReadString(); err != nil && err != Nil { - return err - } - - idle, err := rd.ReadInt() - if err != nil && err != Nil { - return err - } - cmd.val[i].Idle = time.Duration(idle) * time.Millisecond - - if cmd.val[i].RetryCount, err = rd.ReadInt(); err != nil && err != Nil { - return err - } - } - - return nil -} - -func (cmd *XPendingExtCmd) Clone() Cmder { - var val []XPendingExt - if cmd.val != nil { - val = make([]XPendingExt, len(cmd.val)) - copy(val, cmd.val) - } - return &XPendingExtCmd{ - baseCmd: cmd.cloneBaseCmd(), - val: val, - } -} - -//------------------------------------------------------------------------------ - -type XAutoClaimCmd struct { - baseCmd - - start string - val []XMessage -} - -var _ Cmder = (*XAutoClaimCmd)(nil) - -func NewXAutoClaimCmd(ctx context.Context, args ...interface{}) *XAutoClaimCmd { - return &XAutoClaimCmd{ - baseCmd: baseCmd{ - ctx: ctx, - args: args, - cmdType: CmdTypeXAutoClaim, - }, - } -} - -func (cmd *XAutoClaimCmd) SetVal(val []XMessage, start string) { - cmd.val = val - cmd.start = start -} - -func (cmd *XAutoClaimCmd) Val() (messages []XMessage, start string) { - return cmd.val, cmd.start -} - -func (cmd *XAutoClaimCmd) Result() (messages []XMessage, start string, err error) { - return cmd.val, cmd.start, cmd.err -} - -func (cmd *XAutoClaimCmd) String() string { - return cmdString(cmd, cmd.val) -} - -func (cmd *XAutoClaimCmd) readReply(rd *proto.Reader) error { - n, err := rd.ReadArrayLen() - if err != nil { - return err - } - - switch n { - case 2, // Redis 6 - 3: // Redis 7: - // ok - default: - return fmt.Errorf("redis: got %d elements in XAutoClaim reply, wanted 2/3", n) - } - - cmd.start, err = rd.ReadString() - if err != nil { - return err - } - - cmd.val, err = readXMessageSlice(rd) - if err != nil { - return err - } - - if n >= 3 { - if err := rd.DiscardNext(); err != nil { - return err - } - } - - return nil -} - -func (cmd *XAutoClaimCmd) Clone() Cmder { - var val []XMessage - if cmd.val != nil { - val = make([]XMessage, len(cmd.val)) - for i, msg := range cmd.val { - val[i] = XMessage{ - ID: msg.ID, - } - if msg.Values != nil { - val[i].Values = make(map[string]interface{}, len(msg.Values)) - for k, v := range msg.Values { - val[i].Values[k] = v - } - } - } - } - return &XAutoClaimCmd{ - baseCmd: cmd.cloneBaseCmd(), - start: cmd.start, - val: val, - } -} - -//------------------------------------------------------------------------------ - -type XAutoClaimJustIDCmd struct { - baseCmd - - start string - val []string -} - -var _ Cmder = (*XAutoClaimJustIDCmd)(nil) - -func NewXAutoClaimJustIDCmd(ctx context.Context, args ...interface{}) *XAutoClaimJustIDCmd { - return &XAutoClaimJustIDCmd{ - baseCmd: baseCmd{ - ctx: ctx, - args: args, - cmdType: CmdTypeXAutoClaimJustID, - }, - } -} - -func (cmd *XAutoClaimJustIDCmd) SetVal(val []string, start string) { - cmd.val = val - cmd.start = start -} - -func (cmd *XAutoClaimJustIDCmd) Val() (ids []string, start string) { - return cmd.val, cmd.start -} - -func (cmd *XAutoClaimJustIDCmd) Result() (ids []string, start string, err error) { - return cmd.val, cmd.start, cmd.err -} - -func (cmd *XAutoClaimJustIDCmd) String() string { - return cmdString(cmd, cmd.val) -} - -func (cmd *XAutoClaimJustIDCmd) readReply(rd *proto.Reader) error { - n, err := rd.ReadArrayLen() - if err != nil { - return err - } - - switch n { - case 2, // Redis 6 - 3: // Redis 7: - // ok - default: - return fmt.Errorf("redis: got %d elements in XAutoClaimJustID reply, wanted 2/3", n) - } - - cmd.start, err = rd.ReadString() - if err != nil { - return err - } - - nn, err := rd.ReadArrayLen() - if err != nil { - return err - } - - cmd.val = make([]string, nn) - for i := 0; i < nn; i++ { - cmd.val[i], err = rd.ReadString() - if err != nil { - return err - } - } - - if n >= 3 { - if err := rd.DiscardNext(); err != nil { - return err - } - } - - return nil -} - -func (cmd *XAutoClaimJustIDCmd) Clone() Cmder { - var val []string - if cmd.val != nil { - val = make([]string, len(cmd.val)) - copy(val, cmd.val) - } - return &XAutoClaimJustIDCmd{ - baseCmd: cmd.cloneBaseCmd(), - start: cmd.start, - val: val, - } -} - -//------------------------------------------------------------------------------ - -type XInfoConsumersCmd struct { - baseCmd - val []XInfoConsumer -} - -type XInfoConsumer struct { - Name string - Pending int64 - Idle time.Duration - Inactive time.Duration -} - -var _ Cmder = (*XInfoConsumersCmd)(nil) - -func NewXInfoConsumersCmd(ctx context.Context, stream string, group string) *XInfoConsumersCmd { - return &XInfoConsumersCmd{ - baseCmd: baseCmd{ - ctx: ctx, - args: []interface{}{"xinfo", "consumers", stream, group}, - cmdType: CmdTypeXInfoConsumers, - }, - } -} - -func (cmd *XInfoConsumersCmd) SetVal(val []XInfoConsumer) { - cmd.val = val -} - -func (cmd *XInfoConsumersCmd) Val() []XInfoConsumer { - return cmd.val -} - -func (cmd *XInfoConsumersCmd) Result() ([]XInfoConsumer, error) { - return cmd.val, cmd.err -} - -func (cmd *XInfoConsumersCmd) String() string { - return cmdString(cmd, cmd.val) -} - -func (cmd *XInfoConsumersCmd) readReply(rd *proto.Reader) error { - n, err := rd.ReadArrayLen() - if err != nil { - return err - } - cmd.val = make([]XInfoConsumer, n) - - for i := 0; i < len(cmd.val); i++ { - nn, err := rd.ReadMapLen() - if err != nil { - return err - } - - var key string - for f := 0; f < nn; f++ { - key, err = rd.ReadString() - if err != nil { - return err - } - - switch key { - case "name": - cmd.val[i].Name, err = rd.ReadString() - case "pending": - cmd.val[i].Pending, err = rd.ReadInt() - case "idle": - var idle int64 - idle, err = rd.ReadInt() - cmd.val[i].Idle = time.Duration(idle) * time.Millisecond - case "inactive": - var inactive int64 - inactive, err = rd.ReadInt() - cmd.val[i].Inactive = time.Duration(inactive) * time.Millisecond - default: - return fmt.Errorf("redis: unexpected content %s in XINFO CONSUMERS reply", key) - } - if err != nil { - return err - } - } - } - - return nil -} - -func (cmd *XInfoConsumersCmd) Clone() Cmder { - var val []XInfoConsumer - if cmd.val != nil { - val = make([]XInfoConsumer, len(cmd.val)) - copy(val, cmd.val) - } - return &XInfoConsumersCmd{ - baseCmd: cmd.cloneBaseCmd(), - val: val, - } -} - -//------------------------------------------------------------------------------ - -type XInfoGroupsCmd struct { - baseCmd - val []XInfoGroup -} - -type XInfoGroup struct { - Name string - Consumers int64 - Pending int64 - LastDeliveredID string - EntriesRead int64 - // Lag represents the number of pending messages in the stream not yet - // delivered to this consumer group. Returns -1 when the lag cannot be determined. - Lag int64 -} - -var _ Cmder = (*XInfoGroupsCmd)(nil) - -func NewXInfoGroupsCmd(ctx context.Context, stream string) *XInfoGroupsCmd { - return &XInfoGroupsCmd{ - baseCmd: baseCmd{ - ctx: ctx, - args: []interface{}{"xinfo", "groups", stream}, - cmdType: CmdTypeXInfoGroups, - }, - } -} - -func (cmd *XInfoGroupsCmd) SetVal(val []XInfoGroup) { - cmd.val = val -} - -func (cmd *XInfoGroupsCmd) Val() []XInfoGroup { - return cmd.val -} - -func (cmd *XInfoGroupsCmd) Result() ([]XInfoGroup, error) { - return cmd.val, cmd.err -} - -func (cmd *XInfoGroupsCmd) String() string { - return cmdString(cmd, cmd.val) -} - -func (cmd *XInfoGroupsCmd) readReply(rd *proto.Reader) error { - n, err := rd.ReadArrayLen() - if err != nil { - return err - } - cmd.val = make([]XInfoGroup, n) - - for i := 0; i < len(cmd.val); i++ { - group := &cmd.val[i] - - nn, err := rd.ReadMapLen() - if err != nil { - return err - } - - var key string - for j := 0; j < nn; j++ { - key, err = rd.ReadString() - if err != nil { - return err - } - - switch key { - case "name": - group.Name, err = rd.ReadString() - if err != nil { - return err - } - case "consumers": - group.Consumers, err = rd.ReadInt() - if err != nil { - return err - } - case "pending": - group.Pending, err = rd.ReadInt() - if err != nil { - return err - } - case "last-delivered-id": - group.LastDeliveredID, err = rd.ReadString() - if err != nil { - return err - } - case "entries-read": - group.EntriesRead, err = rd.ReadInt() - if err != nil && err != Nil { - return err - } - case "lag": - group.Lag, err = rd.ReadInt() - - // lag: the number of entries in the stream that are still waiting to be delivered - // to the group's consumers, or a NULL(Nil) when that number can't be determined. - // In that case, we return -1. - if err != nil && err != Nil { - return err - } else if err == Nil { - group.Lag = -1 - } - default: - return fmt.Errorf("redis: unexpected key %q in XINFO GROUPS reply", key) - } - } - } - - return nil -} - -func (cmd *XInfoGroupsCmd) Clone() Cmder { - var val []XInfoGroup - if cmd.val != nil { - val = make([]XInfoGroup, len(cmd.val)) - copy(val, cmd.val) - } - return &XInfoGroupsCmd{ - baseCmd: cmd.cloneBaseCmd(), - val: val, - } -} - -//------------------------------------------------------------------------------ - -type XInfoStreamCmd struct { - baseCmd - val *XInfoStream -} - -type XInfoStream struct { - Length int64 - RadixTreeKeys int64 - RadixTreeNodes int64 - Groups int64 - LastGeneratedID string - MaxDeletedEntryID string - EntriesAdded int64 - FirstEntry XMessage - LastEntry XMessage - RecordedFirstEntryID string - - IDMPDuration int64 - IDMPMaxSize int64 - PIDsTracked int64 - IIDsTracked int64 - IIDsAdded int64 - IIDsDuplicates int64 -} - -var _ Cmder = (*XInfoStreamCmd)(nil) - -func NewXInfoStreamCmd(ctx context.Context, stream string) *XInfoStreamCmd { - return &XInfoStreamCmd{ - baseCmd: baseCmd{ - ctx: ctx, - args: []interface{}{"xinfo", "stream", stream}, - cmdType: CmdTypeXInfoStream, - }, - } -} - -func (cmd *XInfoStreamCmd) SetVal(val *XInfoStream) { - cmd.val = val -} - -func (cmd *XInfoStreamCmd) Val() *XInfoStream { - return cmd.val -} - -func (cmd *XInfoStreamCmd) Result() (*XInfoStream, error) { - return cmd.val, cmd.err -} - -func (cmd *XInfoStreamCmd) String() string { - return cmdString(cmd, cmd.val) -} - -func (cmd *XInfoStreamCmd) readReply(rd *proto.Reader) error { - n, err := rd.ReadMapLen() - if err != nil { - return err - } - cmd.val = &XInfoStream{} - - for i := 0; i < n; i++ { - key, err := rd.ReadString() - if err != nil { - return err - } - switch key { - case "length": - cmd.val.Length, err = rd.ReadInt() - if err != nil { - return err - } - case "radix-tree-keys": - cmd.val.RadixTreeKeys, err = rd.ReadInt() - if err != nil { - return err - } - case "radix-tree-nodes": - cmd.val.RadixTreeNodes, err = rd.ReadInt() - if err != nil { - return err - } - case "groups": - cmd.val.Groups, err = rd.ReadInt() - if err != nil { - return err - } - case "last-generated-id": - cmd.val.LastGeneratedID, err = rd.ReadString() - if err != nil { - return err - } - case "max-deleted-entry-id": - cmd.val.MaxDeletedEntryID, err = rd.ReadString() - if err != nil { - return err - } - case "entries-added": - cmd.val.EntriesAdded, err = rd.ReadInt() - if err != nil { - return err - } - case "first-entry": - cmd.val.FirstEntry, err = readXMessage(rd) - if err != nil && err != Nil { - return err - } - case "last-entry": - cmd.val.LastEntry, err = readXMessage(rd) - if err != nil && err != Nil { - return err - } - case "recorded-first-entry-id": - cmd.val.RecordedFirstEntryID, err = rd.ReadString() - if err != nil { - return err - } - case "idmp-duration": - cmd.val.IDMPDuration, err = rd.ReadInt() - if err != nil { - return err - } - case "idmp-maxsize": - cmd.val.IDMPMaxSize, err = rd.ReadInt() - if err != nil { - return err - } - case "pids-tracked": - cmd.val.PIDsTracked, err = rd.ReadInt() - if err != nil { - return err - } - case "iids-tracked": - cmd.val.IIDsTracked, err = rd.ReadInt() - if err != nil { - return err - } - case "iids-added": - cmd.val.IIDsAdded, err = rd.ReadInt() - if err != nil { - return err - } - case "iids-duplicates": - cmd.val.IIDsDuplicates, err = rd.ReadInt() - if err != nil { - return err - } - default: - return fmt.Errorf("redis: unexpected key %q in XINFO STREAM reply", key) - } - } - return nil -} - -func (cmd *XInfoStreamCmd) Clone() Cmder { - var val *XInfoStream - if cmd.val != nil { - val = &XInfoStream{ - Length: cmd.val.Length, - RadixTreeKeys: cmd.val.RadixTreeKeys, - RadixTreeNodes: cmd.val.RadixTreeNodes, - Groups: cmd.val.Groups, - LastGeneratedID: cmd.val.LastGeneratedID, - MaxDeletedEntryID: cmd.val.MaxDeletedEntryID, - EntriesAdded: cmd.val.EntriesAdded, - RecordedFirstEntryID: cmd.val.RecordedFirstEntryID, - } - // Clone XMessage fields - val.FirstEntry = XMessage{ - ID: cmd.val.FirstEntry.ID, - } - if cmd.val.FirstEntry.Values != nil { - val.FirstEntry.Values = make(map[string]interface{}, len(cmd.val.FirstEntry.Values)) - for k, v := range cmd.val.FirstEntry.Values { - val.FirstEntry.Values[k] = v - } - } - val.LastEntry = XMessage{ - ID: cmd.val.LastEntry.ID, - } - if cmd.val.LastEntry.Values != nil { - val.LastEntry.Values = make(map[string]interface{}, len(cmd.val.LastEntry.Values)) - for k, v := range cmd.val.LastEntry.Values { - val.LastEntry.Values[k] = v - } - } - } - return &XInfoStreamCmd{ - baseCmd: cmd.cloneBaseCmd(), - val: val, - } -} - -//------------------------------------------------------------------------------ - -type XInfoStreamFullCmd struct { - baseCmd - val *XInfoStreamFull -} - -type XInfoStreamFull struct { - Length int64 - RadixTreeKeys int64 - RadixTreeNodes int64 - LastGeneratedID string - MaxDeletedEntryID string - EntriesAdded int64 - Entries []XMessage - Groups []XInfoStreamGroup - RecordedFirstEntryID string - IDMPDuration int64 - IDMPMaxSize int64 - PIDsTracked int64 - IIDsTracked int64 - IIDsAdded int64 - IIDsDuplicates int64 -} - -type XInfoStreamGroup struct { - Name string - LastDeliveredID string - EntriesRead int64 - Lag int64 - PelCount int64 - Pending []XInfoStreamGroupPending - Consumers []XInfoStreamConsumer -} - -type XInfoStreamGroupPending struct { - ID string - Consumer string - DeliveryTime time.Time - DeliveryCount int64 -} - -type XInfoStreamConsumer struct { - Name string - SeenTime time.Time - ActiveTime time.Time - PelCount int64 - Pending []XInfoStreamConsumerPending -} - -type XInfoStreamConsumerPending struct { - ID string - DeliveryTime time.Time - DeliveryCount int64 -} - -var _ Cmder = (*XInfoStreamFullCmd)(nil) - -func NewXInfoStreamFullCmd(ctx context.Context, args ...interface{}) *XInfoStreamFullCmd { - return &XInfoStreamFullCmd{ - baseCmd: baseCmd{ - ctx: ctx, - args: args, - cmdType: CmdTypeXInfoStreamFull, - }, - } -} - -func (cmd *XInfoStreamFullCmd) SetVal(val *XInfoStreamFull) { - cmd.val = val -} - -func (cmd *XInfoStreamFullCmd) Val() *XInfoStreamFull { - return cmd.val -} - -func (cmd *XInfoStreamFullCmd) Result() (*XInfoStreamFull, error) { - return cmd.val, cmd.err -} - -func (cmd *XInfoStreamFullCmd) String() string { - return cmdString(cmd, cmd.val) -} - -func (cmd *XInfoStreamFullCmd) readReply(rd *proto.Reader) error { - n, err := rd.ReadMapLen() - if err != nil { - return err - } - - cmd.val = &XInfoStreamFull{} - - for i := 0; i < n; i++ { - key, err := rd.ReadString() - if err != nil { - return err - } - - switch key { - case "length": - cmd.val.Length, err = rd.ReadInt() - if err != nil { - return err - } - case "radix-tree-keys": - cmd.val.RadixTreeKeys, err = rd.ReadInt() - if err != nil { - return err - } - case "radix-tree-nodes": - cmd.val.RadixTreeNodes, err = rd.ReadInt() - if err != nil { - return err - } - case "last-generated-id": - cmd.val.LastGeneratedID, err = rd.ReadString() - if err != nil { - return err - } - case "entries-added": - cmd.val.EntriesAdded, err = rd.ReadInt() - if err != nil { - return err - } - case "entries": - cmd.val.Entries, err = readXMessageSlice(rd) - if err != nil { - return err - } - case "groups": - cmd.val.Groups, err = readStreamGroups(rd) - if err != nil { - return err - } - case "max-deleted-entry-id": - cmd.val.MaxDeletedEntryID, err = rd.ReadString() - if err != nil { - return err - } - case "recorded-first-entry-id": - cmd.val.RecordedFirstEntryID, err = rd.ReadString() - if err != nil { - return err - } - case "idmp-duration": - cmd.val.IDMPDuration, err = rd.ReadInt() - if err != nil { - return err - } - case "idmp-maxsize": - cmd.val.IDMPMaxSize, err = rd.ReadInt() - if err != nil { - return err - } - case "pids-tracked": - cmd.val.PIDsTracked, err = rd.ReadInt() - if err != nil { - return err - } - case "iids-tracked": - cmd.val.IIDsTracked, err = rd.ReadInt() - if err != nil { - return err - } - case "iids-added": - cmd.val.IIDsAdded, err = rd.ReadInt() - if err != nil { - return err - } - case "iids-duplicates": - cmd.val.IIDsDuplicates, err = rd.ReadInt() - if err != nil { - return err - } - default: - return fmt.Errorf("redis: unexpected key %q in XINFO STREAM FULL reply", key) - } - } - return nil -} - -func readStreamGroups(rd *proto.Reader) ([]XInfoStreamGroup, error) { - n, err := rd.ReadArrayLen() - if err != nil { - return nil, err - } - groups := make([]XInfoStreamGroup, 0, n) - for i := 0; i < n; i++ { - nn, err := rd.ReadMapLen() - if err != nil { - return nil, err - } - - group := XInfoStreamGroup{} - - for j := 0; j < nn; j++ { - key, err := rd.ReadString() - if err != nil { - return nil, err - } - - switch key { - case "name": - group.Name, err = rd.ReadString() - if err != nil { - return nil, err - } - case "last-delivered-id": - group.LastDeliveredID, err = rd.ReadString() - if err != nil { - return nil, err - } - case "entries-read": - group.EntriesRead, err = rd.ReadInt() - if err != nil && err != Nil { - return nil, err - } - case "lag": - // lag: the number of entries in the stream that are still waiting to be delivered - // to the group's consumers, or a NULL(Nil) when that number can't be determined. - group.Lag, err = rd.ReadInt() - if err != nil && err != Nil { - return nil, err - } - case "pel-count": - group.PelCount, err = rd.ReadInt() - if err != nil { - return nil, err - } - case "pending": - group.Pending, err = readXInfoStreamGroupPending(rd) - if err != nil { - return nil, err - } - case "consumers": - group.Consumers, err = readXInfoStreamConsumers(rd) - if err != nil { - return nil, err - } - default: - return nil, fmt.Errorf("redis: unexpected key %q in XINFO STREAM FULL reply", key) - } - } - - groups = append(groups, group) - } - - return groups, nil -} - -func readXInfoStreamGroupPending(rd *proto.Reader) ([]XInfoStreamGroupPending, error) { - n, err := rd.ReadArrayLen() - if err != nil { - return nil, err - } - - pending := make([]XInfoStreamGroupPending, 0, n) - - for i := 0; i < n; i++ { - if err = rd.ReadFixedArrayLen(4); err != nil { - return nil, err - } - - p := XInfoStreamGroupPending{} - - p.ID, err = rd.ReadString() - if err != nil { - return nil, err - } - - p.Consumer, err = rd.ReadString() - if err != nil { - return nil, err - } - - delivery, err := rd.ReadInt() - if err != nil { - return nil, err - } - p.DeliveryTime = time.Unix(delivery/1000, delivery%1000*int64(time.Millisecond)) - - p.DeliveryCount, err = rd.ReadInt() - if err != nil { - return nil, err - } - - pending = append(pending, p) - } - - return pending, nil -} - -func readXInfoStreamConsumers(rd *proto.Reader) ([]XInfoStreamConsumer, error) { - n, err := rd.ReadArrayLen() - if err != nil { - return nil, err - } - - consumers := make([]XInfoStreamConsumer, 0, n) - - for i := 0; i < n; i++ { - nn, err := rd.ReadMapLen() - if err != nil { - return nil, err - } - - c := XInfoStreamConsumer{} - - for f := 0; f < nn; f++ { - cKey, err := rd.ReadString() - if err != nil { - return nil, err - } - - switch cKey { - case "name": - c.Name, err = rd.ReadString() - case "seen-time": - seen, err := rd.ReadInt() - if err != nil { - return nil, err - } - c.SeenTime = time.UnixMilli(seen) - case "active-time": - active, err := rd.ReadInt() - if err != nil { - return nil, err - } - c.ActiveTime = time.UnixMilli(active) - case "pel-count": - c.PelCount, err = rd.ReadInt() - case "pending": - pendingNumber, err := rd.ReadArrayLen() - if err != nil { - return nil, err - } - - c.Pending = make([]XInfoStreamConsumerPending, 0, pendingNumber) - - for pn := 0; pn < pendingNumber; pn++ { - if err = rd.ReadFixedArrayLen(3); err != nil { - return nil, err - } - - p := XInfoStreamConsumerPending{} - - p.ID, err = rd.ReadString() - if err != nil { - return nil, err - } - - delivery, err := rd.ReadInt() - if err != nil { - return nil, err - } - p.DeliveryTime = time.Unix(delivery/1000, delivery%1000*int64(time.Millisecond)) - - p.DeliveryCount, err = rd.ReadInt() - if err != nil { - return nil, err - } - - c.Pending = append(c.Pending, p) - } - default: - return nil, fmt.Errorf("redis: unexpected content %s "+ - "in XINFO STREAM FULL reply", cKey) - } - if err != nil { - return nil, err - } - } - consumers = append(consumers, c) - } - - return consumers, nil -} - -func (cmd *XInfoStreamFullCmd) Clone() Cmder { - var val *XInfoStreamFull - if cmd.val != nil { - val = &XInfoStreamFull{ - Length: cmd.val.Length, - RadixTreeKeys: cmd.val.RadixTreeKeys, - RadixTreeNodes: cmd.val.RadixTreeNodes, - LastGeneratedID: cmd.val.LastGeneratedID, - MaxDeletedEntryID: cmd.val.MaxDeletedEntryID, - EntriesAdded: cmd.val.EntriesAdded, - RecordedFirstEntryID: cmd.val.RecordedFirstEntryID, - } - // Clone Entries - if cmd.val.Entries != nil { - val.Entries = make([]XMessage, len(cmd.val.Entries)) - for i, msg := range cmd.val.Entries { - val.Entries[i] = XMessage{ - ID: msg.ID, - } - if msg.Values != nil { - val.Entries[i].Values = make(map[string]interface{}, len(msg.Values)) - for k, v := range msg.Values { - val.Entries[i].Values[k] = v - } - } - } - } - // Clone Groups - simplified copy for now due to complexity - if cmd.val.Groups != nil { - val.Groups = make([]XInfoStreamGroup, len(cmd.val.Groups)) - copy(val.Groups, cmd.val.Groups) - } - } - return &XInfoStreamFullCmd{ - baseCmd: cmd.cloneBaseCmd(), - val: val, - } -} - -//------------------------------------------------------------------------------ - -type ZSliceCmd struct { - baseCmd - - val []Z -} - -var _ Cmder = (*ZSliceCmd)(nil) - -func NewZSliceCmd(ctx context.Context, args ...interface{}) *ZSliceCmd { - return &ZSliceCmd{ - baseCmd: baseCmd{ - ctx: ctx, - args: args, - cmdType: CmdTypeZSlice, - }, - } -} - -func (cmd *ZSliceCmd) SetVal(val []Z) { - cmd.val = val -} - -func (cmd *ZSliceCmd) Val() []Z { - return cmd.val -} - -func (cmd *ZSliceCmd) Result() ([]Z, error) { - return cmd.val, cmd.err -} - -func (cmd *ZSliceCmd) String() string { - return cmdString(cmd, cmd.val) -} - -func (cmd *ZSliceCmd) readReply(rd *proto.Reader) error { // nolint:dupl - n, err := rd.ReadArrayLen() - if err != nil { - return err - } - - // If the n is 0, can't continue reading. - if n == 0 { - cmd.val = make([]Z, 0) - return nil - } - - typ, err := rd.PeekReplyType() - if err != nil { - return err - } - array := typ == proto.RespArray - - if array { - cmd.val = make([]Z, n) - } else { - cmd.val = make([]Z, n/2) - } - - for i := 0; i < len(cmd.val); i++ { - if array { - if err = rd.ReadFixedArrayLen(2); err != nil { - return err - } - } - - if cmd.val[i].Member, err = rd.ReadString(); err != nil { - return err - } - - if cmd.val[i].Score, err = rd.ReadFloat(); err != nil { - return err - } - } - - return nil -} - -func (cmd *ZSliceCmd) Clone() Cmder { - var val []Z - if cmd.val != nil { - val = make([]Z, len(cmd.val)) - copy(val, cmd.val) - } - return &ZSliceCmd{ - baseCmd: cmd.cloneBaseCmd(), - val: val, - } -} - -//------------------------------------------------------------------------------ - -type ZWithKeyCmd struct { - baseCmd - - val *ZWithKey -} - -var _ Cmder = (*ZWithKeyCmd)(nil) - -func NewZWithKeyCmd(ctx context.Context, args ...interface{}) *ZWithKeyCmd { - return &ZWithKeyCmd{ - baseCmd: baseCmd{ - ctx: ctx, - args: args, - cmdType: CmdTypeZWithKey, - }, - } -} - -func (cmd *ZWithKeyCmd) SetVal(val *ZWithKey) { - cmd.val = val -} - -func (cmd *ZWithKeyCmd) Val() *ZWithKey { - return cmd.val -} - -func (cmd *ZWithKeyCmd) Result() (*ZWithKey, error) { - return cmd.val, cmd.err -} - -func (cmd *ZWithKeyCmd) String() string { - return cmdString(cmd, cmd.val) -} - -func (cmd *ZWithKeyCmd) readReply(rd *proto.Reader) (err error) { - if err = rd.ReadFixedArrayLen(3); err != nil { - return err - } - cmd.val = &ZWithKey{} - - if cmd.val.Key, err = rd.ReadString(); err != nil { - return err - } - if cmd.val.Member, err = rd.ReadString(); err != nil { - return err - } - if cmd.val.Score, err = rd.ReadFloat(); err != nil { - return err - } - - return nil -} - -func (cmd *ZWithKeyCmd) Clone() Cmder { - var val *ZWithKey - if cmd.val != nil { - val = &ZWithKey{ - Z: Z{ - Score: cmd.val.Score, - Member: cmd.val.Member, - }, - Key: cmd.val.Key, - } - } - return &ZWithKeyCmd{ - baseCmd: cmd.cloneBaseCmd(), - val: val, - } -} - -//------------------------------------------------------------------------------ - -type ScanCmd struct { - baseCmd - - page []string - cursor uint64 - - process cmdable -} - -var _ Cmder = (*ScanCmd)(nil) - -func NewScanCmd(ctx context.Context, process cmdable, args ...interface{}) *ScanCmd { - return &ScanCmd{ - baseCmd: baseCmd{ - ctx: ctx, - args: args, - cmdType: CmdTypeScan, - }, - process: process, - } -} - -func (cmd *ScanCmd) SetVal(page []string, cursor uint64) { - cmd.page = page - cmd.cursor = cursor -} - -func (cmd *ScanCmd) Val() (keys []string, cursor uint64) { - return cmd.page, cmd.cursor -} - -func (cmd *ScanCmd) Result() (keys []string, cursor uint64, err error) { - return cmd.page, cmd.cursor, cmd.err -} - -func (cmd *ScanCmd) String() string { - return cmdString(cmd, cmd.page) -} - -func (cmd *ScanCmd) readReply(rd *proto.Reader) error { - if err := rd.ReadFixedArrayLen(2); err != nil { - return err - } - - cursor, err := rd.ReadUint() - if err != nil { - return err - } - cmd.cursor = cursor - - n, err := rd.ReadArrayLen() - if err != nil { - return err - } - cmd.page = make([]string, n) - - for i := 0; i < len(cmd.page); i++ { - if cmd.page[i], err = rd.ReadString(); err != nil { - return err - } - } - return nil -} - -func (cmd *ScanCmd) Clone() Cmder { - var page []string - if cmd.page != nil { - page = make([]string, len(cmd.page)) - copy(page, cmd.page) - } - return &ScanCmd{ - baseCmd: cmd.cloneBaseCmd(), - page: page, - cursor: cmd.cursor, - process: cmd.process, - } -} - -// Iterator creates a new ScanIterator. -func (cmd *ScanCmd) Iterator() *ScanIterator { - return &ScanIterator{ - cmd: cmd, - } -} - -//------------------------------------------------------------------------------ - -type ClusterNode struct { - ID string - Addr string - NetworkingMetadata map[string]string -} - -type ClusterSlot struct { - Start int - End int - Nodes []ClusterNode -} - -type ClusterSlotsCmd struct { - baseCmd - - val []ClusterSlot -} - -var _ Cmder = (*ClusterSlotsCmd)(nil) - -func NewClusterSlotsCmd(ctx context.Context, args ...interface{}) *ClusterSlotsCmd { - return &ClusterSlotsCmd{ - baseCmd: baseCmd{ - ctx: ctx, - args: args, - cmdType: CmdTypeClusterSlots, - }, - } -} - -func (cmd *ClusterSlotsCmd) SetVal(val []ClusterSlot) { - cmd.val = val -} - -func (cmd *ClusterSlotsCmd) Val() []ClusterSlot { - return cmd.val -} - -func (cmd *ClusterSlotsCmd) Result() ([]ClusterSlot, error) { - return cmd.val, cmd.err -} - -func (cmd *ClusterSlotsCmd) String() string { - return cmdString(cmd, cmd.val) -} - -func (cmd *ClusterSlotsCmd) readReply(rd *proto.Reader) error { - n, err := rd.ReadArrayLen() - if err != nil { - return err - } - cmd.val = make([]ClusterSlot, n) - - for i := 0; i < len(cmd.val); i++ { - n, err = rd.ReadArrayLen() - if err != nil { - return err - } - if n < 2 { - return fmt.Errorf("redis: got %d elements in cluster info, expected at least 2", n) - } - - start, err := rd.ReadInt() - if err != nil { - return err - } - - end, err := rd.ReadInt() - if err != nil { - return err - } - - // subtract start and end. - nodes := make([]ClusterNode, n-2) - - for j := 0; j < len(nodes); j++ { - nn, err := rd.ReadArrayLen() - if err != nil { - return err - } - if nn < 2 || nn > 4 { - return fmt.Errorf("got %d elements in cluster info address, expected 2, 3, or 4", n) - } - - ip, err := rd.ReadString() - if err != nil { - return err - } - - port, err := rd.ReadString() - if err != nil { - return err - } - - nodes[j].Addr = net.JoinHostPort(ip, port) - - if nn >= 3 { - id, err := rd.ReadString() - if err != nil { - return err - } - nodes[j].ID = id - } - - if nn >= 4 { - metadataLength, err := rd.ReadMapLen() - if err != nil { - return err - } - - networkingMetadata := make(map[string]string, metadataLength) - - for i := 0; i < metadataLength; i++ { - key, err := rd.ReadString() - if err != nil { - return err - } - value, err := rd.ReadString() - if err != nil { - return err - } - networkingMetadata[key] = value - } - - nodes[j].NetworkingMetadata = networkingMetadata - } - } - - cmd.val[i] = ClusterSlot{ - Start: int(start), - End: int(end), - Nodes: nodes, - } - } - - return nil -} - -func (cmd *ClusterSlotsCmd) Clone() Cmder { - var val []ClusterSlot - if cmd.val != nil { - val = make([]ClusterSlot, len(cmd.val)) - for i, slot := range cmd.val { - val[i] = ClusterSlot{ - Start: slot.Start, - End: slot.End, - } - if slot.Nodes != nil { - val[i].Nodes = make([]ClusterNode, len(slot.Nodes)) - for j, node := range slot.Nodes { - val[i].Nodes[j] = ClusterNode{ - ID: node.ID, - Addr: node.Addr, - } - if node.NetworkingMetadata != nil { - val[i].Nodes[j].NetworkingMetadata = make(map[string]string, len(node.NetworkingMetadata)) - for k, v := range node.NetworkingMetadata { - val[i].Nodes[j].NetworkingMetadata[k] = v - } - } - } - } - } - } - return &ClusterSlotsCmd{ - baseCmd: cmd.cloneBaseCmd(), - val: val, - } -} - -//------------------------------------------------------------------------------ - -// GeoLocation is used with GeoAdd to add geospatial location. -type GeoLocation struct { - Name string - Longitude, Latitude, Dist float64 - GeoHash int64 -} - -// GeoRadiusQuery is used with GeoRadius to query geospatial index. -type GeoRadiusQuery struct { - Radius float64 - // Can be m, km, ft, or mi. Default is km. - Unit string - WithCoord bool - WithDist bool - WithGeoHash bool - Count int - // Can be ASC or DESC. Default is no sort order. - Sort string - Store string - StoreDist string - - // WithCoord+WithDist+WithGeoHash - withLen int -} - -type GeoLocationCmd struct { - baseCmd - - q *GeoRadiusQuery - locations []GeoLocation -} - -var _ Cmder = (*GeoLocationCmd)(nil) - -func NewGeoLocationCmd(ctx context.Context, q *GeoRadiusQuery, args ...interface{}) *GeoLocationCmd { - return &GeoLocationCmd{ - baseCmd: baseCmd{ - ctx: ctx, - args: geoLocationArgs(q, args...), - cmdType: CmdTypeGeoLocation, - }, - q: q, - } -} - -func geoLocationArgs(q *GeoRadiusQuery, args ...interface{}) []interface{} { - args = append(args, q.Radius) - if q.Unit != "" { - args = append(args, q.Unit) - } else { - args = append(args, "km") - } - if q.WithCoord { - args = append(args, "withcoord") - q.withLen++ - } - if q.WithDist { - args = append(args, "withdist") - q.withLen++ - } - if q.WithGeoHash { - args = append(args, "withhash") - q.withLen++ - } - if q.Count > 0 { - args = append(args, "count", q.Count) - } - if q.Sort != "" { - args = append(args, q.Sort) - } - if q.Store != "" { - args = append(args, "store") - args = append(args, q.Store) - } - if q.StoreDist != "" { - args = append(args, "storedist") - args = append(args, q.StoreDist) - } - return args -} - -func (cmd *GeoLocationCmd) SetVal(locations []GeoLocation) { - cmd.locations = locations -} - -func (cmd *GeoLocationCmd) Val() []GeoLocation { - return cmd.locations -} - -func (cmd *GeoLocationCmd) Result() ([]GeoLocation, error) { - return cmd.locations, cmd.err -} - -func (cmd *GeoLocationCmd) String() string { - return cmdString(cmd, cmd.locations) -} - -func (cmd *GeoLocationCmd) readReply(rd *proto.Reader) error { - n, err := rd.ReadArrayLen() - if err != nil { - return err - } - cmd.locations = make([]GeoLocation, n) - - for i := 0; i < len(cmd.locations); i++ { - // only name - if cmd.q.withLen == 0 { - if cmd.locations[i].Name, err = rd.ReadString(); err != nil { - return err - } - continue - } - - // +name - if err = rd.ReadFixedArrayLen(cmd.q.withLen + 1); err != nil { - return err - } - - if cmd.locations[i].Name, err = rd.ReadString(); err != nil { - return err - } - if cmd.q.WithDist { - if cmd.locations[i].Dist, err = rd.ReadFloat(); err != nil { - return err - } - } - if cmd.q.WithGeoHash { - if cmd.locations[i].GeoHash, err = rd.ReadInt(); err != nil { - return err - } - } - if cmd.q.WithCoord { - if err = rd.ReadFixedArrayLen(2); err != nil { - return err - } - if cmd.locations[i].Longitude, err = rd.ReadFloat(); err != nil { - return err - } - if cmd.locations[i].Latitude, err = rd.ReadFloat(); err != nil { - return err - } - } - } - - return nil -} - -func (cmd *GeoLocationCmd) Clone() Cmder { - var q *GeoRadiusQuery - if cmd.q != nil { - q = &GeoRadiusQuery{ - Radius: cmd.q.Radius, - Unit: cmd.q.Unit, - WithCoord: cmd.q.WithCoord, - WithDist: cmd.q.WithDist, - WithGeoHash: cmd.q.WithGeoHash, - Count: cmd.q.Count, - Sort: cmd.q.Sort, - Store: cmd.q.Store, - StoreDist: cmd.q.StoreDist, - withLen: cmd.q.withLen, - } - } - var locations []GeoLocation - if cmd.locations != nil { - locations = make([]GeoLocation, len(cmd.locations)) - copy(locations, cmd.locations) - } - return &GeoLocationCmd{ - baseCmd: cmd.cloneBaseCmd(), - q: q, - locations: locations, - } -} - -//------------------------------------------------------------------------------ - -// GeoSearchQuery is used for GEOSearch/GEOSearchStore command query. -type GeoSearchQuery struct { - Member string - - // Latitude and Longitude when using FromLonLat option. - Longitude float64 - Latitude float64 - - // Distance and unit when using ByRadius option. - // Can use m, km, ft, or mi. Default is km. - Radius float64 - RadiusUnit string - - // Height, width and unit when using ByBox option. - // Can be m, km, ft, or mi. Default is km. - BoxWidth float64 - BoxHeight float64 - BoxUnit string - - // Can be ASC or DESC. Default is no sort order. - Sort string - Count int - CountAny bool -} - -type GeoSearchLocationQuery struct { - GeoSearchQuery - - WithCoord bool - WithDist bool - WithHash bool -} - -type GeoSearchStoreQuery struct { - GeoSearchQuery - - // When using the StoreDist option, the command stores the items in a - // sorted set populated with their distance from the center of the circle or box, - // as a floating-point number, in the same unit specified for that shape. - StoreDist bool -} - -func geoSearchLocationArgs(q *GeoSearchLocationQuery, args []interface{}) []interface{} { - args = geoSearchArgs(&q.GeoSearchQuery, args) - - if q.WithCoord { - args = append(args, "withcoord") - } - if q.WithDist { - args = append(args, "withdist") - } - if q.WithHash { - args = append(args, "withhash") - } - - return args -} - -func geoSearchArgs(q *GeoSearchQuery, args []interface{}) []interface{} { - if q.Member != "" { - args = append(args, "frommember", q.Member) - } else { - args = append(args, "fromlonlat", q.Longitude, q.Latitude) - } - - if q.Radius > 0 { - if q.RadiusUnit == "" { - q.RadiusUnit = "km" - } - args = append(args, "byradius", q.Radius, q.RadiusUnit) - } else { - if q.BoxUnit == "" { - q.BoxUnit = "km" - } - args = append(args, "bybox", q.BoxWidth, q.BoxHeight, q.BoxUnit) - } - - if q.Sort != "" { - args = append(args, q.Sort) - } - - if q.Count > 0 { - args = append(args, "count", q.Count) - if q.CountAny { - args = append(args, "any") - } - } - - return args -} - -type GeoSearchLocationCmd struct { - baseCmd - - opt *GeoSearchLocationQuery - val []GeoLocation -} - -var _ Cmder = (*GeoSearchLocationCmd)(nil) - -func NewGeoSearchLocationCmd( - ctx context.Context, opt *GeoSearchLocationQuery, args ...interface{}, -) *GeoSearchLocationCmd { - return &GeoSearchLocationCmd{ - baseCmd: baseCmd{ - ctx: ctx, - args: geoSearchLocationArgs(opt, args), - cmdType: CmdTypeGeoSearchLocation, - }, - opt: opt, - } -} - -func (cmd *GeoSearchLocationCmd) SetVal(val []GeoLocation) { - cmd.val = val -} - -func (cmd *GeoSearchLocationCmd) Val() []GeoLocation { - return cmd.val -} - -func (cmd *GeoSearchLocationCmd) Result() ([]GeoLocation, error) { - return cmd.val, cmd.err -} - -func (cmd *GeoSearchLocationCmd) String() string { - return cmdString(cmd, cmd.val) -} - -func (cmd *GeoSearchLocationCmd) readReply(rd *proto.Reader) error { - n, err := rd.ReadArrayLen() - if err != nil { - return err - } - - cmd.val = make([]GeoLocation, n) - for i := 0; i < n; i++ { - _, err = rd.ReadArrayLen() - if err != nil { - return err - } - - var loc GeoLocation - - loc.Name, err = rd.ReadString() - if err != nil { - return err - } - if cmd.opt.WithDist { - loc.Dist, err = rd.ReadFloat() - if err != nil { - return err - } - } - if cmd.opt.WithHash { - loc.GeoHash, err = rd.ReadInt() - if err != nil { - return err - } - } - if cmd.opt.WithCoord { - if err = rd.ReadFixedArrayLen(2); err != nil { - return err - } - loc.Longitude, err = rd.ReadFloat() - if err != nil { - return err - } - loc.Latitude, err = rd.ReadFloat() - if err != nil { - return err - } - } - - cmd.val[i] = loc - } - - return nil -} - -func (cmd *GeoSearchLocationCmd) Clone() Cmder { - var opt *GeoSearchLocationQuery - if cmd.opt != nil { - opt = &GeoSearchLocationQuery{ - GeoSearchQuery: GeoSearchQuery{ - Member: cmd.opt.Member, - Longitude: cmd.opt.Longitude, - Latitude: cmd.opt.Latitude, - Radius: cmd.opt.Radius, - RadiusUnit: cmd.opt.RadiusUnit, - BoxWidth: cmd.opt.BoxWidth, - BoxHeight: cmd.opt.BoxHeight, - BoxUnit: cmd.opt.BoxUnit, - Sort: cmd.opt.Sort, - Count: cmd.opt.Count, - CountAny: cmd.opt.CountAny, - }, - WithCoord: cmd.opt.WithCoord, - WithDist: cmd.opt.WithDist, - WithHash: cmd.opt.WithHash, - } - } - var val []GeoLocation - if cmd.val != nil { - val = make([]GeoLocation, len(cmd.val)) - copy(val, cmd.val) - } - return &GeoSearchLocationCmd{ - baseCmd: cmd.cloneBaseCmd(), - opt: opt, - val: val, - } -} - -//------------------------------------------------------------------------------ - -type GeoPos struct { - Longitude, Latitude float64 -} - -type GeoPosCmd struct { - baseCmd - - val []*GeoPos -} - -var _ Cmder = (*GeoPosCmd)(nil) - -func NewGeoPosCmd(ctx context.Context, args ...interface{}) *GeoPosCmd { - return &GeoPosCmd{ - baseCmd: baseCmd{ - ctx: ctx, - args: args, - cmdType: CmdTypeGeoPos, - }, - } -} - -func (cmd *GeoPosCmd) SetVal(val []*GeoPos) { - cmd.val = val -} - -func (cmd *GeoPosCmd) Val() []*GeoPos { - return cmd.val -} - -func (cmd *GeoPosCmd) Result() ([]*GeoPos, error) { - return cmd.val, cmd.err -} - -func (cmd *GeoPosCmd) String() string { - return cmdString(cmd, cmd.val) -} - -func (cmd *GeoPosCmd) readReply(rd *proto.Reader) error { - n, err := rd.ReadArrayLen() - if err != nil { - return err - } - cmd.val = make([]*GeoPos, n) - - for i := 0; i < len(cmd.val); i++ { - err = rd.ReadFixedArrayLen(2) - if err != nil { - if err == Nil { - cmd.val[i] = nil - continue - } - return err - } - - longitude, err := rd.ReadFloat() - if err != nil { - return err - } - latitude, err := rd.ReadFloat() - if err != nil { - return err - } - - cmd.val[i] = &GeoPos{ - Longitude: longitude, - Latitude: latitude, - } - } - - return nil -} - -func (cmd *GeoPosCmd) Clone() Cmder { - var val []*GeoPos - if cmd.val != nil { - val = make([]*GeoPos, len(cmd.val)) - for i, pos := range cmd.val { - if pos != nil { - val[i] = &GeoPos{ - Longitude: pos.Longitude, - Latitude: pos.Latitude, - } - } - } - } - return &GeoPosCmd{ - baseCmd: cmd.cloneBaseCmd(), - val: val, - } -} - -//------------------------------------------------------------------------------ - -type CommandInfo struct { - Name string - Arity int8 - Flags []string - ACLFlags []string - FirstKeyPos int8 - LastKeyPos int8 - StepCount int8 - ReadOnly bool - CommandPolicy *routing.CommandPolicy -} - -type CommandsInfoCmd struct { - baseCmd - - val map[string]*CommandInfo -} - -var _ Cmder = (*CommandsInfoCmd)(nil) - -func NewCommandsInfoCmd(ctx context.Context, args ...interface{}) *CommandsInfoCmd { - return &CommandsInfoCmd{ - baseCmd: baseCmd{ - ctx: ctx, - args: args, - cmdType: CmdTypeCommandsInfo, - }, - } -} - -func (cmd *CommandsInfoCmd) SetVal(val map[string]*CommandInfo) { - cmd.val = val -} - -func (cmd *CommandsInfoCmd) Val() map[string]*CommandInfo { - return cmd.val -} - -func (cmd *CommandsInfoCmd) Result() (map[string]*CommandInfo, error) { - return cmd.val, cmd.err -} - -func (cmd *CommandsInfoCmd) String() string { - return cmdString(cmd, cmd.val) -} - -func (cmd *CommandsInfoCmd) readReply(rd *proto.Reader) error { - const numArgRedis5 = 6 - const numArgRedis6 = 7 - const numArgRedis7 = 10 // Also matches redis 8 - - n, err := rd.ReadArrayLen() - if err != nil { - return err - } - cmd.val = make(map[string]*CommandInfo, n) - - for i := 0; i < n; i++ { - nn, err := rd.ReadArrayLen() - if err != nil { - return err - } - - switch nn { - case numArgRedis5, numArgRedis6, numArgRedis7: - // ok - default: - return fmt.Errorf("redis: got %d elements in COMMAND reply, wanted 6/7/10", nn) - } - - cmdInfo := &CommandInfo{} - if cmdInfo.Name, err = rd.ReadString(); err != nil { - return err - } - - arity, err := rd.ReadInt() - if err != nil { - return err - } - cmdInfo.Arity = int8(arity) - - flagLen, err := rd.ReadArrayLen() - if err != nil { - return err - } - cmdInfo.Flags = make([]string, flagLen) - for f := 0; f < len(cmdInfo.Flags); f++ { - switch s, err := rd.ReadString(); { - case err == Nil: - cmdInfo.Flags[f] = "" - case err != nil: - return err - default: - if !cmdInfo.ReadOnly && s == "readonly" { - cmdInfo.ReadOnly = true - } - cmdInfo.Flags[f] = s - } - } - - firstKeyPos, err := rd.ReadInt() - if err != nil { - return err - } - cmdInfo.FirstKeyPos = int8(firstKeyPos) - - lastKeyPos, err := rd.ReadInt() - if err != nil { - return err - } - cmdInfo.LastKeyPos = int8(lastKeyPos) - - stepCount, err := rd.ReadInt() - if err != nil { - return err - } - cmdInfo.StepCount = int8(stepCount) - - if nn >= numArgRedis6 { - aclFlagLen, err := rd.ReadArrayLen() - if err != nil { - return err - } - cmdInfo.ACLFlags = make([]string, aclFlagLen) - for f := 0; f < len(cmdInfo.ACLFlags); f++ { - switch s, err := rd.ReadString(); { - case err == Nil: - cmdInfo.ACLFlags[f] = "" - case err != nil: - return err - default: - cmdInfo.ACLFlags[f] = s - } - } - } - - if nn >= numArgRedis7 { - // The 8th argument is an array of tips. - tipsLen, err := rd.ReadArrayLen() - if err != nil { - return err - } - - rawTips := make(map[string]string, tipsLen) - if cmdInfo.ReadOnly { - rawTips[routing.ReadOnlyCMD] = "" - } - for f := 0; f < tipsLen; f++ { - tip, err := rd.ReadString() - if err != nil { - return err - } - - k, v, ok := strings.Cut(tip, ":") - if !ok { - // Handle tips that don't have a colon (like "nondeterministic_output") - rawTips[tip] = "" - } else { - // Handle normal key:value tips - rawTips[k] = v - } - } - cmdInfo.CommandPolicy = parseCommandPolicies(rawTips, cmdInfo.FirstKeyPos) - - if err := rd.DiscardNext(); err != nil { - return err - } - if err := rd.DiscardNext(); err != nil { - return err - } - } - - cmd.val[cmdInfo.Name] = cmdInfo - } - - return nil -} - -func (cmd *CommandsInfoCmd) Clone() Cmder { - var val map[string]*CommandInfo - if cmd.val != nil { - val = make(map[string]*CommandInfo, len(cmd.val)) - for k, v := range cmd.val { - if v != nil { - newInfo := &CommandInfo{ - Name: v.Name, - Arity: v.Arity, - FirstKeyPos: v.FirstKeyPos, - LastKeyPos: v.LastKeyPos, - StepCount: v.StepCount, - ReadOnly: v.ReadOnly, - CommandPolicy: v.CommandPolicy, // CommandPolicy can be shared as it's immutable - } - if v.Flags != nil { - newInfo.Flags = make([]string, len(v.Flags)) - copy(newInfo.Flags, v.Flags) - } - if v.ACLFlags != nil { - newInfo.ACLFlags = make([]string, len(v.ACLFlags)) - copy(newInfo.ACLFlags, v.ACLFlags) - } - val[k] = newInfo - } - } - } - return &CommandsInfoCmd{ - baseCmd: cmd.cloneBaseCmd(), - val: val, - } -} - -//------------------------------------------------------------------------------ - -type cmdsInfoCache struct { - fn func(ctx context.Context) (map[string]*CommandInfo, error) - - once internal.Once - refreshLock sync.Mutex - cmds map[string]*CommandInfo -} - -func newCmdsInfoCache(fn func(ctx context.Context) (map[string]*CommandInfo, error)) *cmdsInfoCache { - return &cmdsInfoCache{ - fn: fn, - } -} - -func (c *cmdsInfoCache) Get(ctx context.Context) (map[string]*CommandInfo, error) { - c.refreshLock.Lock() - defer c.refreshLock.Unlock() - - err := c.once.Do(func() error { - cmds, err := c.fn(ctx) - if err != nil { - return err - } - - lowerCmds := make(map[string]*CommandInfo, len(cmds)) - - // Extensions have cmd names in upper case. Convert them to lower case. - for k, v := range cmds { - lowerCmds[internal.ToLower(k)] = v - } - - c.cmds = lowerCmds - return nil - }) - return c.cmds, err -} - -func (c *cmdsInfoCache) Refresh() { - c.refreshLock.Lock() - defer c.refreshLock.Unlock() - - c.once = internal.Once{} -} - -// ------------------------------------------------------------------------------ -const requestPolicy = "request_policy" -const responsePolicy = "response_policy" - -func parseCommandPolicies(commandInfoTips map[string]string, firstKeyPos int8) *routing.CommandPolicy { - req := routing.ReqDefault - resp := routing.RespDefaultKeyless - if firstKeyPos > 0 { - resp = routing.RespDefaultHashSlot - } - - tips := make(map[string]string, len(commandInfoTips)) - for k, v := range commandInfoTips { - if k == requestPolicy { - if p, err := routing.ParseRequestPolicy(v); err == nil { - req = p - } - continue - } - if k == responsePolicy { - if p, err := routing.ParseResponsePolicy(v); err == nil { - resp = p - } - continue - } - tips[k] = v - } - - return &routing.CommandPolicy{Request: req, Response: resp, Tips: tips} -} - -//------------------------------------------------------------------------------ - -type SlowLog struct { - ID int64 - Time time.Time - Duration time.Duration - Args []string - // These are also optional fields emitted only by Redis 4.0 or greater: - // https://redis.io/commands/slowlog#output-format - ClientAddr string - ClientName string -} - -type SlowLogCmd struct { - baseCmd - - val []SlowLog -} - -var _ Cmder = (*SlowLogCmd)(nil) - -func NewSlowLogCmd(ctx context.Context, args ...interface{}) *SlowLogCmd { - return &SlowLogCmd{ - baseCmd: baseCmd{ - ctx: ctx, - args: args, - cmdType: CmdTypeSlowLog, - }, - } -} - -func (cmd *SlowLogCmd) SetVal(val []SlowLog) { - cmd.val = val -} - -func (cmd *SlowLogCmd) Val() []SlowLog { - return cmd.val -} - -func (cmd *SlowLogCmd) Result() ([]SlowLog, error) { - return cmd.val, cmd.err -} - -func (cmd *SlowLogCmd) String() string { - return cmdString(cmd, cmd.val) -} - -func (cmd *SlowLogCmd) readReply(rd *proto.Reader) error { - n, err := rd.ReadArrayLen() - if err != nil { - return err - } - cmd.val = make([]SlowLog, n) - - for i := 0; i < len(cmd.val); i++ { - nn, err := rd.ReadArrayLen() - if err != nil { - return err - } - if nn < 4 { - return fmt.Errorf("redis: got %d elements in slowlog get, expected at least 4", nn) - } - - if cmd.val[i].ID, err = rd.ReadInt(); err != nil { - return err - } - - createdAt, err := rd.ReadInt() - if err != nil { - return err - } - cmd.val[i].Time = time.Unix(createdAt, 0) - - costs, err := rd.ReadInt() - if err != nil { - return err - } - cmd.val[i].Duration = time.Duration(costs) * time.Microsecond - - cmdLen, err := rd.ReadArrayLen() - if err != nil { - return err - } - if cmdLen < 1 { - return fmt.Errorf("redis: got %d elements commands reply in slowlog get, expected at least 1", cmdLen) - } - - cmd.val[i].Args = make([]string, cmdLen) - for f := 0; f < len(cmd.val[i].Args); f++ { - cmd.val[i].Args[f], err = rd.ReadString() - if err != nil { - return err - } - } - - if nn >= 5 { - if cmd.val[i].ClientAddr, err = rd.ReadString(); err != nil { - return err - } - } - - if nn >= 6 { - if cmd.val[i].ClientName, err = rd.ReadString(); err != nil { - return err - } - } - } - - return nil -} - -func (cmd *SlowLogCmd) Clone() Cmder { - var val []SlowLog - if cmd.val != nil { - val = make([]SlowLog, len(cmd.val)) - for i, log := range cmd.val { - val[i] = SlowLog{ - ID: log.ID, - Time: log.Time, - Duration: log.Duration, - ClientAddr: log.ClientAddr, - ClientName: log.ClientName, - } - if log.Args != nil { - val[i].Args = make([]string, len(log.Args)) - copy(val[i].Args, log.Args) - } - } - } - return &SlowLogCmd{ - baseCmd: cmd.cloneBaseCmd(), - val: val, - } -} - -//----------------------------------------------------------------------- - -type Latency struct { - Name string - Time time.Time - Latest time.Duration - Max time.Duration -} - -type LatencyCmd struct { - baseCmd - val []Latency -} - -var _ Cmder = (*LatencyCmd)(nil) - -func NewLatencyCmd(ctx context.Context, args ...interface{}) *LatencyCmd { - return &LatencyCmd{ - baseCmd: baseCmd{ - ctx: ctx, - args: args, - }, - } -} - -func (cmd *LatencyCmd) SetVal(val []Latency) { - cmd.val = val -} - -func (cmd *LatencyCmd) Val() []Latency { - return cmd.val -} - -func (cmd *LatencyCmd) Result() ([]Latency, error) { - return cmd.val, cmd.err -} - -func (cmd *LatencyCmd) String() string { - return cmdString(cmd, cmd.val) -} - -func (cmd *LatencyCmd) readReply(rd *proto.Reader) error { - n, err := rd.ReadArrayLen() - if err != nil { - return err - } - cmd.val = make([]Latency, n) - for i := 0; i < len(cmd.val); i++ { - nn, err := rd.ReadArrayLen() - if err != nil { - return err - } - if nn < 3 { - return fmt.Errorf("redis: got %d elements in latency get, expected at least 3", nn) - } - if cmd.val[i].Name, err = rd.ReadString(); err != nil { - return err - } - createdAt, err := rd.ReadInt() - if err != nil { - return err - } - cmd.val[i].Time = time.Unix(createdAt, 0) - latest, err := rd.ReadInt() - if err != nil { - return err - } - cmd.val[i].Latest = time.Duration(latest) * time.Millisecond - maximum, err := rd.ReadInt() - if err != nil { - return err - } - cmd.val[i].Max = time.Duration(maximum) * time.Millisecond - } - return nil -} - -func (cmd *LatencyCmd) Clone() Cmder { - var val []Latency - if cmd.val != nil { - val = make([]Latency, len(cmd.val)) - copy(val, cmd.val) - } - return &LatencyCmd{ - baseCmd: cmd.cloneBaseCmd(), - val: val, - } -} - -//----------------------------------------------------------------------- - -// HotKeysSlotRange represents a slot or slot range in the response. -// Single element slice = individual slot, two element slice = slot range [start, end]. -type HotKeysSlotRange []int64 - -// HotKeysKeyEntry represents a hot key entry with its metric value. -type HotKeysKeyEntry struct { - Key string - Value interface{} // Can be int64 or string -} - -// HotKeysResult represents the response data from HOTKEYS GET command. -// Field names match the Redis response format. -type HotKeysResult struct { - TrackingActive bool - SampleRatio uint8 - SelectedSlots []HotKeysSlotRange - SampledCommandsSelectedSlots time.Duration // Present when sample-ratio > 1 and selected-slots is not empty - AllCommandsSelectedSlots time.Duration // Present when selected-slots is not empty - AllCommandsAllSlots time.Duration - NetBytesSampledCommandsSelectedSlots int64 // Present when sample-ratio > 1 and selected-slots is not empty - NetBytesAllCommandsSelectedSlots int64 // Present when selected-slots is not empty - NetBytesAllCommandsAllSlots int64 - CollectionStartTime time.Time - CollectionDuration time.Duration - UsedCPUSys time.Duration - UsedCPUUser time.Duration - TotalNetBytes int64 - ByCPUTime []HotKeysKeyEntry - ByNetBytes []HotKeysKeyEntry -} - -type HotKeysCmd struct { - baseCmd - - val *HotKeysResult -} - -var _ Cmder = (*HotKeysCmd)(nil) - -func NewHotKeysCmd(ctx context.Context, args ...interface{}) *HotKeysCmd { - return &HotKeysCmd{ - baseCmd: baseCmd{ - ctx: ctx, - args: args, - cmdType: CmdTypeHotKeys, - }, - } -} - -func (cmd *HotKeysCmd) SetVal(val *HotKeysResult) { - cmd.val = val -} - -func (cmd *HotKeysCmd) Val() *HotKeysResult { - return cmd.val -} - -func (cmd *HotKeysCmd) Result() (*HotKeysResult, error) { - return cmd.val, cmd.err -} - -func (cmd *HotKeysCmd) String() string { - return cmdString(cmd, cmd.val) -} - -func (cmd *HotKeysCmd) readReply(rd *proto.Reader) error { - // HOTKEYS GET response is wrapped in an array for aggregation support - arrayLen, err := rd.ReadArrayLen() - if err != nil { - return err - } - - if arrayLen == 0 { - // Empty array means no tracking was started or after reset - cmd.val = nil - return nil - } - - // Read the first (and typically only) element which is a map - n, err := rd.ReadMapLen() - if err != nil { - return err - } - - result := &HotKeysResult{} - data := make(map[string]interface{}, n) - - for i := 0; i < n; i++ { - k, err := rd.ReadString() - if err != nil { - return err - } - v, err := rd.ReadReply() - if err != nil { - if err == Nil { - data[k] = Nil - continue - } - if err, ok := err.(proto.RedisError); ok { - data[k] = err - continue - } - return err - } - data[k] = v - } - - if v, ok := data["tracking-active"].(int64); ok { - result.TrackingActive = v == 1 - } - if v, ok := data["sample-ratio"].(int64); ok { - result.SampleRatio = uint8(v) - } - if v, ok := data["selected-slots"].([]interface{}); ok { - result.SelectedSlots = make([]HotKeysSlotRange, 0, len(v)) - for _, slot := range v { - switch s := slot.(type) { - case int64: - // Single slot - result.SelectedSlots = append(result.SelectedSlots, HotKeysSlotRange{s}) - case []interface{}: - // Slot range - slotRange := make(HotKeysSlotRange, 0, len(s)) - for _, sr := range s { - if val, ok := sr.(int64); ok { - slotRange = append(slotRange, val) - } - } - result.SelectedSlots = append(result.SelectedSlots, slotRange) - } - } - } - if v, ok := data["sampled-commands-selected-slots-us"].(int64); ok { - result.SampledCommandsSelectedSlots = time.Duration(v) * time.Microsecond - } - if v, ok := data["all-commands-selected-slots-us"].(int64); ok { - result.AllCommandsSelectedSlots = time.Duration(v) * time.Microsecond - } - if v, ok := data["all-commands-all-slots-us"].(int64); ok { - result.AllCommandsAllSlots = time.Duration(v) * time.Microsecond - } - if v, ok := data["net-bytes-sampled-commands-selected-slots"].(int64); ok { - result.NetBytesSampledCommandsSelectedSlots = v - } - if v, ok := data["net-bytes-all-commands-selected-slots"].(int64); ok { - result.NetBytesAllCommandsSelectedSlots = v - } - if v, ok := data["net-bytes-all-commands-all-slots"].(int64); ok { - result.NetBytesAllCommandsAllSlots = v - } - if v, ok := data["collection-start-time-unix-ms"].(int64); ok { - result.CollectionStartTime = time.UnixMilli(v) - } - if v, ok := data["collection-duration-ms"].(int64); ok { - result.CollectionDuration = time.Duration(v) * time.Millisecond - } - if v, ok := data["used-cpu-sys-ms"].(int64); ok { - result.UsedCPUSys = time.Duration(v) * time.Millisecond - } - if v, ok := data["used-cpu-user-ms"].(int64); ok { - result.UsedCPUUser = time.Duration(v) * time.Millisecond - } - if v, ok := data["total-net-bytes"].(int64); ok { - result.TotalNetBytes = v - } - - if v, ok := data["by-cpu-time-us"].([]interface{}); ok { - result.ByCPUTime = parseHotKeysKeyEntries(v) - } - - if v, ok := data["by-net-bytes"].([]interface{}); ok { - result.ByNetBytes = parseHotKeysKeyEntries(v) - } - - cmd.val = result - return nil -} - -// parseHotKeysKeyEntries parses the key-value pairs from HOTKEYS GET response. -func parseHotKeysKeyEntries(v []interface{}) []HotKeysKeyEntry { - entries := make([]HotKeysKeyEntry, 0, len(v)/2) - for i := 0; i < len(v); i += 2 { - if i+1 < len(v) { - key, keyOk := v[i].(string) - if keyOk { - entries = append(entries, HotKeysKeyEntry{ - Key: key, - Value: v[i+1], // Can be int64 or string - }) - } - } - } - return entries -} - -func (cmd *HotKeysCmd) Clone() Cmder { - var val *HotKeysResult - if cmd.val != nil { - val = &HotKeysResult{ - TrackingActive: cmd.val.TrackingActive, - SampleRatio: cmd.val.SampleRatio, - SampledCommandsSelectedSlots: cmd.val.SampledCommandsSelectedSlots, - AllCommandsSelectedSlots: cmd.val.AllCommandsSelectedSlots, - AllCommandsAllSlots: cmd.val.AllCommandsAllSlots, - NetBytesSampledCommandsSelectedSlots: cmd.val.NetBytesSampledCommandsSelectedSlots, - NetBytesAllCommandsSelectedSlots: cmd.val.NetBytesAllCommandsSelectedSlots, - NetBytesAllCommandsAllSlots: cmd.val.NetBytesAllCommandsAllSlots, - CollectionStartTime: cmd.val.CollectionStartTime, - CollectionDuration: cmd.val.CollectionDuration, - UsedCPUSys: cmd.val.UsedCPUSys, - UsedCPUUser: cmd.val.UsedCPUUser, - TotalNetBytes: cmd.val.TotalNetBytes, - } - if cmd.val.SelectedSlots != nil { - val.SelectedSlots = make([]HotKeysSlotRange, len(cmd.val.SelectedSlots)) - for i, sr := range cmd.val.SelectedSlots { - val.SelectedSlots[i] = make(HotKeysSlotRange, len(sr)) - copy(val.SelectedSlots[i], sr) - } - } - if cmd.val.ByCPUTime != nil { - val.ByCPUTime = make([]HotKeysKeyEntry, len(cmd.val.ByCPUTime)) - copy(val.ByCPUTime, cmd.val.ByCPUTime) - } - if cmd.val.ByNetBytes != nil { - val.ByNetBytes = make([]HotKeysKeyEntry, len(cmd.val.ByNetBytes)) - copy(val.ByNetBytes, cmd.val.ByNetBytes) - } - } - return &HotKeysCmd{ - baseCmd: cmd.cloneBaseCmd(), - val: val, - } -} - -//----------------------------------------------------------------------- - -type MapStringInterfaceCmd struct { - baseCmd - - val map[string]interface{} -} - -var _ Cmder = (*MapStringInterfaceCmd)(nil) - -func NewMapStringInterfaceCmd(ctx context.Context, args ...interface{}) *MapStringInterfaceCmd { - return &MapStringInterfaceCmd{ - baseCmd: baseCmd{ - ctx: ctx, - args: args, - cmdType: CmdTypeMapStringInterface, - }, - } -} - -func (cmd *MapStringInterfaceCmd) SetVal(val map[string]interface{}) { - cmd.val = val -} - -func (cmd *MapStringInterfaceCmd) Val() map[string]interface{} { - return cmd.val -} - -func (cmd *MapStringInterfaceCmd) Result() (map[string]interface{}, error) { - return cmd.val, cmd.err -} - -func (cmd *MapStringInterfaceCmd) String() string { - return cmdString(cmd, cmd.val) -} - -func (cmd *MapStringInterfaceCmd) readReply(rd *proto.Reader) error { - n, err := rd.ReadMapLen() - if err != nil { - return err - } - - cmd.val = make(map[string]interface{}, n) - for i := 0; i < n; i++ { - k, err := rd.ReadString() - if err != nil { - return err - } - v, err := rd.ReadReply() - if err != nil { - if err == Nil { - cmd.val[k] = Nil - continue - } - if err, ok := err.(proto.RedisError); ok { - cmd.val[k] = err - continue - } - return err - } - cmd.val[k] = v - } - return nil -} - -func (cmd *MapStringInterfaceCmd) Clone() Cmder { - var val map[string]interface{} - if cmd.val != nil { - val = make(map[string]interface{}, len(cmd.val)) - for k, v := range cmd.val { - val[k] = v - } - } - return &MapStringInterfaceCmd{ - baseCmd: cmd.cloneBaseCmd(), - val: val, - } -} - -//----------------------------------------------------------------------- - -type MapStringStringSliceCmd struct { - baseCmd - - val []map[string]string -} - -var _ Cmder = (*MapStringStringSliceCmd)(nil) - -func NewMapStringStringSliceCmd(ctx context.Context, args ...interface{}) *MapStringStringSliceCmd { - return &MapStringStringSliceCmd{ - baseCmd: baseCmd{ - ctx: ctx, - args: args, - cmdType: CmdTypeMapStringStringSlice, - }, - } -} - -func (cmd *MapStringStringSliceCmd) SetVal(val []map[string]string) { - cmd.val = val -} - -func (cmd *MapStringStringSliceCmd) Val() []map[string]string { - return cmd.val -} - -func (cmd *MapStringStringSliceCmd) Result() ([]map[string]string, error) { - return cmd.val, cmd.err -} - -func (cmd *MapStringStringSliceCmd) String() string { - return cmdString(cmd, cmd.val) -} - -func (cmd *MapStringStringSliceCmd) readReply(rd *proto.Reader) error { - n, err := rd.ReadArrayLen() - if err != nil { - return err - } - - cmd.val = make([]map[string]string, n) - for i := 0; i < n; i++ { - nn, err := rd.ReadMapLen() - if err != nil { - return err - } - cmd.val[i] = make(map[string]string, nn) - for f := 0; f < nn; f++ { - k, err := rd.ReadString() - if err != nil { - return err - } - - v, err := rd.ReadString() - if err != nil { - return err - } - cmd.val[i][k] = v - } - } - return nil -} - -func (cmd *MapStringStringSliceCmd) Clone() Cmder { - var val []map[string]string - if cmd.val != nil { - val = make([]map[string]string, len(cmd.val)) - for i, m := range cmd.val { - if m != nil { - val[i] = make(map[string]string, len(m)) - for k, v := range m { - val[i][k] = v - } - } - } - } - return &MapStringStringSliceCmd{ - baseCmd: cmd.cloneBaseCmd(), - val: val, - } -} - -// ----------------------------------------------------------------------- - -// MapMapStringInterfaceCmd represents a command that returns a map of strings to interface{}. -type MapMapStringInterfaceCmd struct { - baseCmd - val map[string]interface{} -} - -func NewMapMapStringInterfaceCmd(ctx context.Context, args ...interface{}) *MapMapStringInterfaceCmd { - return &MapMapStringInterfaceCmd{ - baseCmd: baseCmd{ - ctx: ctx, - args: args, - cmdType: CmdTypeMapMapStringInterface, - }, - } -} - -func (cmd *MapMapStringInterfaceCmd) String() string { - return cmdString(cmd, cmd.val) -} - -func (cmd *MapMapStringInterfaceCmd) SetVal(val map[string]interface{}) { - cmd.val = val -} - -func (cmd *MapMapStringInterfaceCmd) Result() (map[string]interface{}, error) { - return cmd.val, cmd.err -} - -func (cmd *MapMapStringInterfaceCmd) Val() map[string]interface{} { - return cmd.val -} - -// readReply will try to parse the reply from the proto.Reader for both resp2 and resp3 -func (cmd *MapMapStringInterfaceCmd) readReply(rd *proto.Reader) (err error) { - data, err := rd.ReadReply() - if err != nil { - return err - } - resultMap := map[string]interface{}{} - - switch midResponse := data.(type) { - case map[interface{}]interface{}: // resp3 will return map - for k, v := range midResponse { - stringKey, ok := k.(string) - if !ok { - return fmt.Errorf("redis: invalid map key %#v", k) - } - resultMap[stringKey] = v - } - case []interface{}: // resp2 will return array of arrays - n := len(midResponse) - for i := 0; i < n; i++ { - finalArr, ok := midResponse[i].([]interface{}) // final array that we need to transform to map - if !ok { - return fmt.Errorf("redis: unexpected response %#v", data) - } - m := len(finalArr) - if m%2 != 0 { // since this should be map, keys should be even number - return fmt.Errorf("redis: unexpected response %#v", data) - } - - for j := 0; j < m; j += 2 { - stringKey, ok := finalArr[j].(string) // the first one - if !ok { - return fmt.Errorf("redis: invalid map key %#v", finalArr[i]) - } - resultMap[stringKey] = finalArr[j+1] // second one is value - } - } - default: - return fmt.Errorf("redis: unexpected response %#v", data) - } - - cmd.val = resultMap - return nil -} - -func (cmd *MapMapStringInterfaceCmd) Clone() Cmder { - var val map[string]interface{} - if cmd.val != nil { - val = make(map[string]interface{}, len(cmd.val)) - for k, v := range cmd.val { - val[k] = v - } - } - return &MapMapStringInterfaceCmd{ - baseCmd: cmd.cloneBaseCmd(), - val: val, - } -} - -//----------------------------------------------------------------------- - -type MapStringInterfaceSliceCmd struct { - baseCmd - - val []map[string]interface{} -} - -var _ Cmder = (*MapStringInterfaceSliceCmd)(nil) - -func NewMapStringInterfaceSliceCmd(ctx context.Context, args ...interface{}) *MapStringInterfaceSliceCmd { - return &MapStringInterfaceSliceCmd{ - baseCmd: baseCmd{ - ctx: ctx, - args: args, - cmdType: CmdTypeMapStringInterfaceSlice, - }, - } -} - -func (cmd *MapStringInterfaceSliceCmd) SetVal(val []map[string]interface{}) { - cmd.val = val -} - -func (cmd *MapStringInterfaceSliceCmd) Val() []map[string]interface{} { - return cmd.val -} - -func (cmd *MapStringInterfaceSliceCmd) Result() ([]map[string]interface{}, error) { - return cmd.val, cmd.err -} - -func (cmd *MapStringInterfaceSliceCmd) String() string { - return cmdString(cmd, cmd.val) -} - -func (cmd *MapStringInterfaceSliceCmd) readReply(rd *proto.Reader) error { - n, err := rd.ReadArrayLen() - if err != nil { - return err - } - - cmd.val = make([]map[string]interface{}, n) - for i := 0; i < n; i++ { - nn, err := rd.ReadMapLen() - if err != nil { - return err - } - cmd.val[i] = make(map[string]interface{}, nn) - for f := 0; f < nn; f++ { - k, err := rd.ReadString() - if err != nil { - return err - } - v, err := rd.ReadReply() - if err != nil { - if err != Nil { - return err - } - } - cmd.val[i][k] = v - } - } - return nil -} - -func (cmd *MapStringInterfaceSliceCmd) Clone() Cmder { - var val []map[string]interface{} - if cmd.val != nil { - val = make([]map[string]interface{}, len(cmd.val)) - for i, m := range cmd.val { - if m != nil { - val[i] = make(map[string]interface{}, len(m)) - for k, v := range m { - val[i][k] = v - } - } - } - } - return &MapStringInterfaceSliceCmd{ - baseCmd: cmd.cloneBaseCmd(), - val: val, - } -} - -//------------------------------------------------------------------------------ - -type KeyValuesCmd struct { - baseCmd - - key string - val []string -} - -var _ Cmder = (*KeyValuesCmd)(nil) - -func NewKeyValuesCmd(ctx context.Context, args ...interface{}) *KeyValuesCmd { - return &KeyValuesCmd{ - baseCmd: baseCmd{ - ctx: ctx, - args: args, - cmdType: CmdTypeKeyValues, - }, - } -} - -func (cmd *KeyValuesCmd) SetVal(key string, val []string) { - cmd.key = key - cmd.val = val -} - -func (cmd *KeyValuesCmd) Val() (string, []string) { - return cmd.key, cmd.val -} - -func (cmd *KeyValuesCmd) Result() (string, []string, error) { - return cmd.key, cmd.val, cmd.err -} - -func (cmd *KeyValuesCmd) String() string { - return cmdString(cmd, cmd.val) -} - -func (cmd *KeyValuesCmd) readReply(rd *proto.Reader) (err error) { - if err = rd.ReadFixedArrayLen(2); err != nil { - return err - } - - cmd.key, err = rd.ReadString() - if err != nil { - return err - } - - n, err := rd.ReadArrayLen() - if err != nil { - return err - } - cmd.val = make([]string, n) - for i := 0; i < n; i++ { - cmd.val[i], err = rd.ReadString() - if err != nil { - return err - } - } - - return nil -} - -func (cmd *KeyValuesCmd) Clone() Cmder { - var val []string - if cmd.val != nil { - val = make([]string, len(cmd.val)) - copy(val, cmd.val) - } - return &KeyValuesCmd{ - baseCmd: cmd.cloneBaseCmd(), - key: cmd.key, - val: val, - } -} - -//------------------------------------------------------------------------------ - -type ZSliceWithKeyCmd struct { - baseCmd - - key string - val []Z -} - -var _ Cmder = (*ZSliceWithKeyCmd)(nil) - -func NewZSliceWithKeyCmd(ctx context.Context, args ...interface{}) *ZSliceWithKeyCmd { - return &ZSliceWithKeyCmd{ - baseCmd: baseCmd{ - ctx: ctx, - args: args, - cmdType: CmdTypeZSliceWithKey, - }, - } -} - -func (cmd *ZSliceWithKeyCmd) SetVal(key string, val []Z) { - cmd.key = key - cmd.val = val -} - -func (cmd *ZSliceWithKeyCmd) Val() (string, []Z) { - return cmd.key, cmd.val -} - -func (cmd *ZSliceWithKeyCmd) Result() (string, []Z, error) { - return cmd.key, cmd.val, cmd.err -} - -func (cmd *ZSliceWithKeyCmd) String() string { - return cmdString(cmd, cmd.val) -} - -func (cmd *ZSliceWithKeyCmd) readReply(rd *proto.Reader) (err error) { - if err = rd.ReadFixedArrayLen(2); err != nil { - return err - } - - cmd.key, err = rd.ReadString() - if err != nil { - return err - } - - n, err := rd.ReadArrayLen() - if err != nil { - return err - } - - typ, err := rd.PeekReplyType() - if err != nil { - return err - } - array := typ == proto.RespArray - - if array { - cmd.val = make([]Z, n) - } else { - cmd.val = make([]Z, n/2) - } - - for i := 0; i < len(cmd.val); i++ { - if array { - if err = rd.ReadFixedArrayLen(2); err != nil { - return err - } - } - - if cmd.val[i].Member, err = rd.ReadString(); err != nil { - return err - } - - if cmd.val[i].Score, err = rd.ReadFloat(); err != nil { - return err - } - } - - return nil -} - -func (cmd *ZSliceWithKeyCmd) Clone() Cmder { - var val []Z - if cmd.val != nil { - val = make([]Z, len(cmd.val)) - copy(val, cmd.val) - } - return &ZSliceWithKeyCmd{ - baseCmd: cmd.cloneBaseCmd(), - key: cmd.key, - val: val, - } -} - -type Function struct { - Name string - Description string - Flags []string -} - -type Library struct { - Name string - Engine string - Functions []Function - Code string -} - -type FunctionListCmd struct { - baseCmd - - val []Library -} - -var _ Cmder = (*FunctionListCmd)(nil) - -func NewFunctionListCmd(ctx context.Context, args ...interface{}) *FunctionListCmd { - return &FunctionListCmd{ - baseCmd: baseCmd{ - ctx: ctx, - args: args, - cmdType: CmdTypeFunctionList, - }, - } -} - -func (cmd *FunctionListCmd) SetVal(val []Library) { - cmd.val = val -} - -func (cmd *FunctionListCmd) String() string { - return cmdString(cmd, cmd.val) -} - -func (cmd *FunctionListCmd) Val() []Library { - return cmd.val -} - -func (cmd *FunctionListCmd) Result() ([]Library, error) { - return cmd.val, cmd.err -} - -func (cmd *FunctionListCmd) First() (*Library, error) { - if cmd.err != nil { - return nil, cmd.err - } - if len(cmd.val) > 0 { - return &cmd.val[0], nil - } - return nil, Nil -} - -func (cmd *FunctionListCmd) readReply(rd *proto.Reader) (err error) { - n, err := rd.ReadArrayLen() - if err != nil { - return err - } - - libraries := make([]Library, n) - for i := 0; i < n; i++ { - nn, err := rd.ReadMapLen() - if err != nil { - return err - } - - library := Library{} - for f := 0; f < nn; f++ { - key, err := rd.ReadString() - if err != nil { - return err - } - - switch key { - case "library_name": - library.Name, err = rd.ReadString() - case "engine": - library.Engine, err = rd.ReadString() - case "functions": - library.Functions, err = cmd.readFunctions(rd) - case "library_code": - library.Code, err = rd.ReadString() - default: - return fmt.Errorf("redis: function list unexpected key %s", key) - } - - if err != nil { - return err - } - } - - libraries[i] = library - } - cmd.val = libraries - return nil -} - -func (cmd *FunctionListCmd) readFunctions(rd *proto.Reader) ([]Function, error) { - n, err := rd.ReadArrayLen() - if err != nil { - return nil, err - } - - functions := make([]Function, n) - for i := 0; i < n; i++ { - nn, err := rd.ReadMapLen() - if err != nil { - return nil, err - } - - function := Function{} - for f := 0; f < nn; f++ { - key, err := rd.ReadString() - if err != nil { - return nil, err - } - - switch key { - case "name": - if function.Name, err = rd.ReadString(); err != nil { - return nil, err - } - case "description": - if function.Description, err = rd.ReadString(); err != nil && err != Nil { - return nil, err - } - case "flags": - // resp set - nx, err := rd.ReadArrayLen() - if err != nil { - return nil, err - } - - function.Flags = make([]string, nx) - for j := 0; j < nx; j++ { - if function.Flags[j], err = rd.ReadString(); err != nil { - return nil, err - } - } - default: - return nil, fmt.Errorf("redis: function list unexpected key %s", key) - } - } - - functions[i] = function - } - return functions, nil -} - -func (cmd *FunctionListCmd) Clone() Cmder { - var val []Library - if cmd.val != nil { - val = make([]Library, len(cmd.val)) - for i, lib := range cmd.val { - val[i] = Library{ - Name: lib.Name, - Engine: lib.Engine, - Code: lib.Code, - } - if lib.Functions != nil { - val[i].Functions = make([]Function, len(lib.Functions)) - for j, fn := range lib.Functions { - val[i].Functions[j] = Function{ - Name: fn.Name, - Description: fn.Description, - } - if fn.Flags != nil { - val[i].Functions[j].Flags = make([]string, len(fn.Flags)) - copy(val[i].Functions[j].Flags, fn.Flags) - } - } - } - } - } - return &FunctionListCmd{ - baseCmd: cmd.cloneBaseCmd(), - val: val, - } -} - -// FunctionStats contains information about the scripts currently executing on the server, and the available engines -// - Engines: -// Statistics about the engine like number of functions and number of libraries -// - RunningScript: -// The script currently running on the shard we're connecting to. -// For Redis Enterprise and Redis Cloud, this represents the -// function with the longest running time, across all the running functions, on all shards -// - RunningScripts -// All scripts currently running in a Redis Enterprise clustered database. -// Only available on Redis Enterprise -type FunctionStats struct { - Engines []Engine - isRunning bool - rs RunningScript - allrs []RunningScript -} - -func (fs *FunctionStats) Running() bool { - return fs.isRunning -} - -func (fs *FunctionStats) RunningScript() (RunningScript, bool) { - return fs.rs, fs.isRunning -} - -// AllRunningScripts returns all scripts currently running in a Redis Enterprise clustered database. -// Only available on Redis Enterprise -func (fs *FunctionStats) AllRunningScripts() []RunningScript { - return fs.allrs -} - -type RunningScript struct { - Name string - Command []string - Duration time.Duration -} - -type Engine struct { - Language string - LibrariesCount int64 - FunctionsCount int64 -} - -type FunctionStatsCmd struct { - baseCmd - val FunctionStats -} - -var _ Cmder = (*FunctionStatsCmd)(nil) - -func NewFunctionStatsCmd(ctx context.Context, args ...interface{}) *FunctionStatsCmd { - return &FunctionStatsCmd{ - baseCmd: baseCmd{ - ctx: ctx, - args: args, - cmdType: CmdTypeFunctionStats, - }, - } -} - -func (cmd *FunctionStatsCmd) SetVal(val FunctionStats) { - cmd.val = val -} - -func (cmd *FunctionStatsCmd) String() string { - return cmdString(cmd, cmd.val) -} - -func (cmd *FunctionStatsCmd) Val() FunctionStats { - return cmd.val -} - -func (cmd *FunctionStatsCmd) Result() (FunctionStats, error) { - return cmd.val, cmd.err -} - -func (cmd *FunctionStatsCmd) readReply(rd *proto.Reader) (err error) { - n, err := rd.ReadMapLen() - if err != nil { - return err - } - - var key string - var result FunctionStats - for f := 0; f < n; f++ { - key, err = rd.ReadString() - if err != nil { - return err - } - - switch key { - case "running_script": - result.rs, result.isRunning, err = cmd.readRunningScript(rd) - case "engines": - result.Engines, err = cmd.readEngines(rd) - case "all_running_scripts": // Redis Enterprise only - result.allrs, result.isRunning, err = cmd.readRunningScripts(rd) - default: - return fmt.Errorf("redis: function stats unexpected key %s", key) - } - - if err != nil { - return err - } - } - - cmd.val = result - return nil -} - -func (cmd *FunctionStatsCmd) readRunningScript(rd *proto.Reader) (RunningScript, bool, error) { - err := rd.ReadFixedMapLen(3) - if err != nil { - if err == Nil { - return RunningScript{}, false, nil - } - return RunningScript{}, false, err - } - - var runningScript RunningScript - for i := 0; i < 3; i++ { - key, err := rd.ReadString() - if err != nil { - return RunningScript{}, false, err - } - - switch key { - case "name": - runningScript.Name, err = rd.ReadString() - case "duration_ms": - runningScript.Duration, err = cmd.readDuration(rd) - case "command": - runningScript.Command, err = cmd.readCommand(rd) - default: - return RunningScript{}, false, fmt.Errorf("redis: function stats unexpected running_script key %s", key) - } - - if err != nil { - return RunningScript{}, false, err - } - } - - return runningScript, true, nil -} - -func (cmd *FunctionStatsCmd) readEngines(rd *proto.Reader) ([]Engine, error) { - n, err := rd.ReadMapLen() - if err != nil { - return nil, err - } - - engines := make([]Engine, 0, n) - for i := 0; i < n; i++ { - engine := Engine{} - engine.Language, err = rd.ReadString() - if err != nil { - return nil, err - } - - err = rd.ReadFixedMapLen(2) - if err != nil { - return nil, fmt.Errorf("redis: function stats unexpected %s engine map length", engine.Language) - } - - for i := 0; i < 2; i++ { - key, err := rd.ReadString() - switch key { - case "libraries_count": - engine.LibrariesCount, err = rd.ReadInt() - case "functions_count": - engine.FunctionsCount, err = rd.ReadInt() - } - if err != nil { - return nil, err - } - } - - engines = append(engines, engine) - } - return engines, nil -} - -func (cmd *FunctionStatsCmd) readDuration(rd *proto.Reader) (time.Duration, error) { - t, err := rd.ReadInt() - if err != nil { - return time.Duration(0), err - } - return time.Duration(t) * time.Millisecond, nil -} - -func (cmd *FunctionStatsCmd) readCommand(rd *proto.Reader) ([]string, error) { - n, err := rd.ReadArrayLen() - if err != nil { - return nil, err - } - - command := make([]string, 0, n) - for i := 0; i < n; i++ { - x, err := rd.ReadString() - if err != nil { - return nil, err - } - command = append(command, x) - } - - return command, nil -} - -func (cmd *FunctionStatsCmd) readRunningScripts(rd *proto.Reader) ([]RunningScript, bool, error) { - n, err := rd.ReadArrayLen() - if err != nil { - return nil, false, err - } - - runningScripts := make([]RunningScript, 0, n) - for i := 0; i < n; i++ { - rs, _, err := cmd.readRunningScript(rd) - if err != nil { - return nil, false, err - } - runningScripts = append(runningScripts, rs) - } - - return runningScripts, len(runningScripts) > 0, nil -} - -func (cmd *FunctionStatsCmd) Clone() Cmder { - val := FunctionStats{ - isRunning: cmd.val.isRunning, - rs: cmd.val.rs, // RunningScript is a simple struct, can be copied directly - } - if cmd.val.Engines != nil { - val.Engines = make([]Engine, len(cmd.val.Engines)) - copy(val.Engines, cmd.val.Engines) - } - if cmd.val.allrs != nil { - val.allrs = make([]RunningScript, len(cmd.val.allrs)) - for i, rs := range cmd.val.allrs { - val.allrs[i] = RunningScript{ - Name: rs.Name, - Duration: rs.Duration, - } - if rs.Command != nil { - val.allrs[i].Command = make([]string, len(rs.Command)) - copy(val.allrs[i].Command, rs.Command) - } - } - } - return &FunctionStatsCmd{ - baseCmd: cmd.cloneBaseCmd(), - val: val, - } -} - -//------------------------------------------------------------------------------ - -// LCSQuery is a parameter used for the LCS command -type LCSQuery struct { - Key1 string - Key2 string - Len bool - Idx bool - MinMatchLen int - WithMatchLen bool -} - -// LCSMatch is the result set of the LCS command. -type LCSMatch struct { - MatchString string - Matches []LCSMatchedPosition - Len int64 -} - -type LCSMatchedPosition struct { - Key1 LCSPosition - Key2 LCSPosition - - // only for withMatchLen is true - MatchLen int64 -} - -type LCSPosition struct { - Start int64 - End int64 -} - -type LCSCmd struct { - baseCmd - - // 1: match string - // 2: match len - // 3: match idx LCSMatch - readType uint8 - val *LCSMatch -} - -func NewLCSCmd(ctx context.Context, q *LCSQuery) *LCSCmd { - args := make([]interface{}, 3, 7) - args[0] = "lcs" - args[1] = q.Key1 - args[2] = q.Key2 - - cmd := &LCSCmd{readType: 1} - if q.Len { - cmd.readType = 2 - args = append(args, "len") - } else if q.Idx { - cmd.readType = 3 - args = append(args, "idx") - if q.MinMatchLen != 0 { - args = append(args, "minmatchlen", q.MinMatchLen) - } - if q.WithMatchLen { - args = append(args, "withmatchlen") - } - } - cmd.baseCmd = baseCmd{ - ctx: ctx, - args: args, - cmdType: CmdTypeLCS, - } - - return cmd -} - -func (cmd *LCSCmd) SetVal(val *LCSMatch) { - cmd.val = val -} - -func (cmd *LCSCmd) String() string { - return cmdString(cmd, cmd.val) -} - -func (cmd *LCSCmd) Val() *LCSMatch { - return cmd.val -} - -func (cmd *LCSCmd) Result() (*LCSMatch, error) { - return cmd.val, cmd.err -} - -func (cmd *LCSCmd) readReply(rd *proto.Reader) (err error) { - lcs := &LCSMatch{} - switch cmd.readType { - case 1: - // match string - if lcs.MatchString, err = rd.ReadString(); err != nil { - return err - } - case 2: - // match len - if lcs.Len, err = rd.ReadInt(); err != nil { - return err - } - case 3: - // read LCSMatch - if err = rd.ReadFixedMapLen(2); err != nil { - return err - } - - // read matches or len field - for i := 0; i < 2; i++ { - key, err := rd.ReadString() - if err != nil { - return err - } - - switch key { - case "matches": - // read array of matched positions - if lcs.Matches, err = cmd.readMatchedPositions(rd); err != nil { - return err - } - case "len": - // read match length - if lcs.Len, err = rd.ReadInt(); err != nil { - return err - } - } - } - } - - cmd.val = lcs - return nil -} - -func (cmd *LCSCmd) readMatchedPositions(rd *proto.Reader) ([]LCSMatchedPosition, error) { - n, err := rd.ReadArrayLen() - if err != nil { - return nil, err - } - - positions := make([]LCSMatchedPosition, n) - for i := 0; i < n; i++ { - pn, err := rd.ReadArrayLen() - if err != nil { - return nil, err - } - - if positions[i].Key1, err = cmd.readPosition(rd); err != nil { - return nil, err - } - if positions[i].Key2, err = cmd.readPosition(rd); err != nil { - return nil, err - } - - // read match length if WithMatchLen is true - if pn > 2 { - if positions[i].MatchLen, err = rd.ReadInt(); err != nil { - return nil, err - } - } - } - - return positions, nil -} - -func (cmd *LCSCmd) readPosition(rd *proto.Reader) (pos LCSPosition, err error) { - if err = rd.ReadFixedArrayLen(2); err != nil { - return pos, err - } - if pos.Start, err = rd.ReadInt(); err != nil { - return pos, err - } - if pos.End, err = rd.ReadInt(); err != nil { - return pos, err - } - - return pos, nil -} - -func (cmd *LCSCmd) Clone() Cmder { - var val *LCSMatch - if cmd.val != nil { - val = &LCSMatch{ - MatchString: cmd.val.MatchString, - Len: cmd.val.Len, - } - if cmd.val.Matches != nil { - val.Matches = make([]LCSMatchedPosition, len(cmd.val.Matches)) - copy(val.Matches, cmd.val.Matches) - } - } - return &LCSCmd{ - baseCmd: cmd.cloneBaseCmd(), - readType: cmd.readType, - val: val, - } -} - -// ------------------------------------------------------------------------ - -type KeyFlags struct { - Key string - Flags []string -} - -type KeyFlagsCmd struct { - baseCmd - - val []KeyFlags -} - -var _ Cmder = (*KeyFlagsCmd)(nil) - -func NewKeyFlagsCmd(ctx context.Context, args ...interface{}) *KeyFlagsCmd { - return &KeyFlagsCmd{ - baseCmd: baseCmd{ - ctx: ctx, - args: args, - cmdType: CmdTypeKeyFlags, - }, - } -} - -func (cmd *KeyFlagsCmd) SetVal(val []KeyFlags) { - cmd.val = val -} - -func (cmd *KeyFlagsCmd) Val() []KeyFlags { - return cmd.val -} - -func (cmd *KeyFlagsCmd) Result() ([]KeyFlags, error) { - return cmd.val, cmd.err -} - -func (cmd *KeyFlagsCmd) String() string { - return cmdString(cmd, cmd.val) -} - -func (cmd *KeyFlagsCmd) readReply(rd *proto.Reader) error { - n, err := rd.ReadArrayLen() - if err != nil { - return err - } - - if n == 0 { - cmd.val = make([]KeyFlags, 0) - return nil - } - - cmd.val = make([]KeyFlags, n) - - for i := 0; i < len(cmd.val); i++ { - - if err = rd.ReadFixedArrayLen(2); err != nil { - return err - } - - if cmd.val[i].Key, err = rd.ReadString(); err != nil { - return err - } - flagsLen, err := rd.ReadArrayLen() - if err != nil { - return err - } - cmd.val[i].Flags = make([]string, flagsLen) - - for j := 0; j < flagsLen; j++ { - if cmd.val[i].Flags[j], err = rd.ReadString(); err != nil { - return err - } - } - } - - return nil -} - -func (cmd *KeyFlagsCmd) Clone() Cmder { - var val []KeyFlags - if cmd.val != nil { - val = make([]KeyFlags, len(cmd.val)) - for i, kf := range cmd.val { - val[i] = KeyFlags{ - Key: kf.Key, - } - if kf.Flags != nil { - val[i].Flags = make([]string, len(kf.Flags)) - copy(val[i].Flags, kf.Flags) - } - } - } - return &KeyFlagsCmd{ - baseCmd: cmd.cloneBaseCmd(), - val: val, - } -} - -// --------------------------------------------------------------------------------------------------- - -type ClusterLink struct { - Direction string - Node string - CreateTime int64 - Events string - SendBufferAllocated int64 - SendBufferUsed int64 -} - -type ClusterLinksCmd struct { - baseCmd - - val []ClusterLink -} - -var _ Cmder = (*ClusterLinksCmd)(nil) - -func NewClusterLinksCmd(ctx context.Context, args ...interface{}) *ClusterLinksCmd { - return &ClusterLinksCmd{ - baseCmd: baseCmd{ - ctx: ctx, - args: args, - cmdType: CmdTypeClusterLinks, - }, - } -} - -func (cmd *ClusterLinksCmd) SetVal(val []ClusterLink) { - cmd.val = val -} - -func (cmd *ClusterLinksCmd) Val() []ClusterLink { - return cmd.val -} - -func (cmd *ClusterLinksCmd) Result() ([]ClusterLink, error) { - return cmd.val, cmd.err -} - -func (cmd *ClusterLinksCmd) String() string { - return cmdString(cmd, cmd.val) -} - -func (cmd *ClusterLinksCmd) readReply(rd *proto.Reader) error { - n, err := rd.ReadArrayLen() - if err != nil { - return err - } - cmd.val = make([]ClusterLink, n) - - for i := 0; i < len(cmd.val); i++ { - m, err := rd.ReadMapLen() - if err != nil { - return err - } - - for j := 0; j < m; j++ { - key, err := rd.ReadString() - if err != nil { - return err - } - - switch key { - case "direction": - cmd.val[i].Direction, err = rd.ReadString() - case "node": - cmd.val[i].Node, err = rd.ReadString() - case "create-time": - cmd.val[i].CreateTime, err = rd.ReadInt() - case "events": - cmd.val[i].Events, err = rd.ReadString() - case "send-buffer-allocated": - cmd.val[i].SendBufferAllocated, err = rd.ReadInt() - case "send-buffer-used": - cmd.val[i].SendBufferUsed, err = rd.ReadInt() - default: - return fmt.Errorf("redis: unexpected key %q in CLUSTER LINKS reply", key) - } - - if err != nil { - return err - } - } - } - - return nil -} - -func (cmd *ClusterLinksCmd) Clone() Cmder { - var val []ClusterLink - if cmd.val != nil { - val = make([]ClusterLink, len(cmd.val)) - copy(val, cmd.val) - } - return &ClusterLinksCmd{ - baseCmd: cmd.cloneBaseCmd(), - val: val, - } -} - -// ------------------------------------------------------------------------------------------------------------------ - -type SlotRange struct { - Start int64 - End int64 -} - -type Node struct { - ID string - Endpoint string - IP string - Hostname string - Port int64 - TLSPort int64 - Role string - ReplicationOffset int64 - Health string -} - -type ClusterShard struct { - Slots []SlotRange - Nodes []Node -} - -type ClusterShardsCmd struct { - baseCmd - - val []ClusterShard -} - -var _ Cmder = (*ClusterShardsCmd)(nil) - -func NewClusterShardsCmd(ctx context.Context, args ...interface{}) *ClusterShardsCmd { - return &ClusterShardsCmd{ - baseCmd: baseCmd{ - ctx: ctx, - args: args, - cmdType: CmdTypeClusterShards, - }, - } -} - -func (cmd *ClusterShardsCmd) SetVal(val []ClusterShard) { - cmd.val = val -} - -func (cmd *ClusterShardsCmd) Val() []ClusterShard { - return cmd.val -} - -func (cmd *ClusterShardsCmd) Result() ([]ClusterShard, error) { - return cmd.val, cmd.err -} - -func (cmd *ClusterShardsCmd) String() string { - return cmdString(cmd, cmd.val) -} - -func (cmd *ClusterShardsCmd) readReply(rd *proto.Reader) error { - n, err := rd.ReadArrayLen() - if err != nil { - return err - } - cmd.val = make([]ClusterShard, n) - - for i := 0; i < n; i++ { - m, err := rd.ReadMapLen() - if err != nil { - return err - } - - for j := 0; j < m; j++ { - key, err := rd.ReadString() - if err != nil { - return err - } - - switch key { - case "slots": - l, err := rd.ReadArrayLen() - if err != nil { - return err - } - for k := 0; k < l; k += 2 { - start, err := rd.ReadInt() - if err != nil { - return err - } - - end, err := rd.ReadInt() - if err != nil { - return err - } - - cmd.val[i].Slots = append(cmd.val[i].Slots, SlotRange{Start: start, End: end}) - } - case "nodes": - nodesLen, err := rd.ReadArrayLen() - if err != nil { - return err - } - cmd.val[i].Nodes = make([]Node, nodesLen) - for k := 0; k < nodesLen; k++ { - nodeMapLen, err := rd.ReadMapLen() - if err != nil { - return err - } - - for l := 0; l < nodeMapLen; l++ { - nodeKey, err := rd.ReadString() - if err != nil { - return err - } - - switch nodeKey { - case "id": - cmd.val[i].Nodes[k].ID, err = rd.ReadString() - case "endpoint": - cmd.val[i].Nodes[k].Endpoint, err = rd.ReadString() - case "ip": - cmd.val[i].Nodes[k].IP, err = rd.ReadString() - case "hostname": - cmd.val[i].Nodes[k].Hostname, err = rd.ReadString() - case "port": - cmd.val[i].Nodes[k].Port, err = rd.ReadInt() - case "tls-port": - cmd.val[i].Nodes[k].TLSPort, err = rd.ReadInt() - case "role": - cmd.val[i].Nodes[k].Role, err = rd.ReadString() - case "replication-offset": - cmd.val[i].Nodes[k].ReplicationOffset, err = rd.ReadInt() - case "health": - cmd.val[i].Nodes[k].Health, err = rd.ReadString() - default: - return fmt.Errorf("redis: unexpected key %q in CLUSTER SHARDS node reply", nodeKey) - } - - if err != nil { - return err - } - } - } - default: - return fmt.Errorf("redis: unexpected key %q in CLUSTER SHARDS reply", key) - } - } - } - - return nil -} - -func (cmd *ClusterShardsCmd) Clone() Cmder { - var val []ClusterShard - if cmd.val != nil { - val = make([]ClusterShard, len(cmd.val)) - for i, shard := range cmd.val { - val[i] = ClusterShard{} - if shard.Slots != nil { - val[i].Slots = make([]SlotRange, len(shard.Slots)) - copy(val[i].Slots, shard.Slots) - } - if shard.Nodes != nil { - val[i].Nodes = make([]Node, len(shard.Nodes)) - copy(val[i].Nodes, shard.Nodes) - } - } - } - return &ClusterShardsCmd{ - baseCmd: cmd.cloneBaseCmd(), - val: val, - } -} - -// ----------------------------------------- - -type RankScore struct { - Rank int64 - Score float64 -} - -type RankWithScoreCmd struct { - baseCmd - - val RankScore -} - -var _ Cmder = (*RankWithScoreCmd)(nil) - -func NewRankWithScoreCmd(ctx context.Context, args ...interface{}) *RankWithScoreCmd { - return &RankWithScoreCmd{ - baseCmd: baseCmd{ - ctx: ctx, - args: args, - cmdType: CmdTypeRankWithScore, - }, - } -} - -func (cmd *RankWithScoreCmd) SetVal(val RankScore) { - cmd.val = val -} - -func (cmd *RankWithScoreCmd) Val() RankScore { - return cmd.val -} - -func (cmd *RankWithScoreCmd) Result() (RankScore, error) { - return cmd.val, cmd.err -} - -func (cmd *RankWithScoreCmd) String() string { - return cmdString(cmd, cmd.val) -} - -func (cmd *RankWithScoreCmd) readReply(rd *proto.Reader) error { - if err := rd.ReadFixedArrayLen(2); err != nil { - return err - } - - rank, err := rd.ReadInt() - if err != nil { - return err - } - - score, err := rd.ReadFloat() - if err != nil { - return err - } - - cmd.val = RankScore{Rank: rank, Score: score} - - return nil -} - -func (cmd *RankWithScoreCmd) Clone() Cmder { - return &RankWithScoreCmd{ - baseCmd: cmd.cloneBaseCmd(), - val: cmd.val, // RankScore is a simple struct, can be copied directly - } -} - -// -------------------------------------------------------------------------------------------------- - -// ClientFlags is redis-server client flags, copy from redis/src/server.h (redis 7.0) -type ClientFlags uint64 - -const ( - ClientSlave ClientFlags = 1 << 0 /* This client is a replica */ - ClientMaster ClientFlags = 1 << 1 /* This client is a master */ - ClientMonitor ClientFlags = 1 << 2 /* This client is a slave monitor, see MONITOR */ - ClientMulti ClientFlags = 1 << 3 /* This client is in a MULTI context */ - ClientBlocked ClientFlags = 1 << 4 /* The client is waiting in a blocking operation */ - ClientDirtyCAS ClientFlags = 1 << 5 /* Watched keys modified. EXEC will fail. */ - ClientCloseAfterReply ClientFlags = 1 << 6 /* Close after writing entire reply. */ - ClientUnBlocked ClientFlags = 1 << 7 /* This client was unblocked and is stored in server.unblocked_clients */ - ClientScript ClientFlags = 1 << 8 /* This is a non-connected client used by Lua */ - ClientAsking ClientFlags = 1 << 9 /* Client issued the ASKING command */ - ClientCloseASAP ClientFlags = 1 << 10 /* Close this client ASAP */ - ClientUnixSocket ClientFlags = 1 << 11 /* Client connected via Unix domain socket */ - ClientDirtyExec ClientFlags = 1 << 12 /* EXEC will fail for errors while queueing */ - ClientMasterForceReply ClientFlags = 1 << 13 /* Queue replies even if is master */ - ClientForceAOF ClientFlags = 1 << 14 /* Force AOF propagation of current cmd. */ - ClientForceRepl ClientFlags = 1 << 15 /* Force replication of current cmd. */ - ClientPrePSync ClientFlags = 1 << 16 /* Instance don't understand PSYNC. */ - ClientReadOnly ClientFlags = 1 << 17 /* Cluster client is in read-only state. */ - ClientPubSub ClientFlags = 1 << 18 /* Client is in Pub/Sub mode. */ - ClientPreventAOFProp ClientFlags = 1 << 19 /* Don't propagate to AOF. */ - ClientPreventReplProp ClientFlags = 1 << 20 /* Don't propagate to slaves. */ - ClientPreventProp ClientFlags = ClientPreventAOFProp | ClientPreventReplProp - ClientPendingWrite ClientFlags = 1 << 21 /* Client has output to send but a-write handler is yet not installed. */ - ClientReplyOff ClientFlags = 1 << 22 /* Don't send replies to client. */ - ClientReplySkipNext ClientFlags = 1 << 23 /* Set ClientREPLY_SKIP for next cmd */ - ClientReplySkip ClientFlags = 1 << 24 /* Don't send just this reply. */ - ClientLuaDebug ClientFlags = 1 << 25 /* Run EVAL in debug mode. */ - ClientLuaDebugSync ClientFlags = 1 << 26 /* EVAL debugging without fork() */ - ClientModule ClientFlags = 1 << 27 /* Non connected client used by some module. */ - ClientProtected ClientFlags = 1 << 28 /* Client should not be freed for now. */ - ClientExecutingCommand ClientFlags = 1 << 29 /* Indicates that the client is currently in the process of handling - a command. usually this will be marked only during call() - however, blocked clients might have this flag kept until they - will try to reprocess the command. */ - ClientPendingCommand ClientFlags = 1 << 30 /* Indicates the client has a fully * parsed command ready for execution. */ - ClientTracking ClientFlags = 1 << 31 /* Client enabled keys tracking in order to perform client side caching. */ - ClientTrackingBrokenRedir ClientFlags = 1 << 32 /* Target client is invalid. */ - ClientTrackingBCAST ClientFlags = 1 << 33 /* Tracking in BCAST mode. */ - ClientTrackingOptIn ClientFlags = 1 << 34 /* Tracking in opt-in mode. */ - ClientTrackingOptOut ClientFlags = 1 << 35 /* Tracking in opt-out mode. */ - ClientTrackingCaching ClientFlags = 1 << 36 /* CACHING yes/no was given, depending on optin/optout mode. */ - ClientTrackingNoLoop ClientFlags = 1 << 37 /* Don't send invalidation messages about writes performed by myself.*/ - ClientInTimeoutTable ClientFlags = 1 << 38 /* This client is in the timeout table. */ - ClientProtocolError ClientFlags = 1 << 39 /* Protocol error chatting with it. */ - ClientCloseAfterCommand ClientFlags = 1 << 40 /* Close after executing commands * and writing entire reply. */ - ClientDenyBlocking ClientFlags = 1 << 41 /* Indicate that the client should not be blocked. currently, turned on inside MULTI, Lua, RM_Call, and AOF client */ - ClientReplRDBOnly ClientFlags = 1 << 42 /* This client is a replica that only wants RDB without replication buffer. */ - ClientNoEvict ClientFlags = 1 << 43 /* This client is protected against client memory eviction. */ - ClientAllowOOM ClientFlags = 1 << 44 /* Client used by RM_Call is allowed to fully execute scripts even when in OOM */ - ClientNoTouch ClientFlags = 1 << 45 /* This client will not touch LFU/LRU stats. */ - ClientPushing ClientFlags = 1 << 46 /* This client is pushing notifications. */ -) - -// ClientInfo is redis-server ClientInfo, not go-redis *Client -type ClientInfo struct { - ID int64 // redis version 2.8.12, a unique 64-bit client ID - Addr string // address/port of the client - LAddr string // address/port of local address client connected to (bind address) - FD int64 // file descriptor corresponding to the socket - Name string // the name set by the client with CLIENT SETNAME - Age time.Duration // total duration of the connection in seconds - Idle time.Duration // idle time of the connection in seconds - Flags ClientFlags // client flags (see below) - DB int // current database ID - Sub int // number of channel subscriptions - PSub int // number of pattern matching subscriptions - SSub int // redis version 7.0.3, number of shard channel subscriptions - Multi int // number of commands in a MULTI/EXEC context - Watch int // redis version 7.4 RC1, number of keys this client is currently watching. - QueryBuf int // qbuf, query buffer length (0 means no query pending) - QueryBufFree int // qbuf-free, free space of the query buffer (0 means the buffer is full) - ArgvMem int // incomplete arguments for the next command (already extracted from query buffer) - MultiMem int // redis version 7.0, memory is used up by buffered multi commands - BufferSize int // rbs, usable size of buffer - BufferPeak int // rbp, peak used size of buffer in last 5 sec interval - OutputBufferLength int // obl, output buffer length - OutputListLength int // oll, output list length (replies are queued in this list when the buffer is full) - OutputMemory int // omem, output buffer memory usage - TotalMemory int // tot-mem, total memory consumed by this client in its various buffers - TotalNetIn int // tot-net-in, total network input - TotalNetOut int // tot-net-out, total network output - TotalCmds int // tot-cmds, total number of commands processed - IoThread int // io-thread id - Events string // file descriptor events (see below) - LastCmd string // cmd, last command played - User string // the authenticated username of the client - Redir int64 // client id of current client tracking redirection - Resp int // redis version 7.0, client RESP protocol version - LibName string // redis version 7.2, client library name - LibVer string // redis version 7.2, client library version -} - -type ClientInfoCmd struct { - baseCmd - - val *ClientInfo -} - -var _ Cmder = (*ClientInfoCmd)(nil) - -func NewClientInfoCmd(ctx context.Context, args ...interface{}) *ClientInfoCmd { - return &ClientInfoCmd{ - baseCmd: baseCmd{ - ctx: ctx, - args: args, - cmdType: CmdTypeClientInfo, - }, - } -} - -func (cmd *ClientInfoCmd) SetVal(val *ClientInfo) { - cmd.val = val -} - -func (cmd *ClientInfoCmd) String() string { - return cmdString(cmd, cmd.val) -} - -func (cmd *ClientInfoCmd) Val() *ClientInfo { - return cmd.val -} - -func (cmd *ClientInfoCmd) Result() (*ClientInfo, error) { - return cmd.val, cmd.err -} - -func (cmd *ClientInfoCmd) readReply(rd *proto.Reader) (err error) { - txt, err := rd.ReadString() - if err != nil { - return err - } - - // sds o = catClientInfoString(sdsempty(), c); - // o = sdscatlen(o,"\n",1); - // addReplyVerbatim(c,o,sdslen(o),"txt"); - // sdsfree(o); - cmd.val, err = parseClientInfo(strings.TrimSpace(txt)) - return err -} - -// fmt.Sscanf() cannot handle null values -func parseClientInfo(txt string) (info *ClientInfo, err error) { - info = &ClientInfo{} - for _, s := range strings.Split(txt, " ") { - kv := strings.Split(s, "=") - if len(kv) != 2 { - return nil, fmt.Errorf("redis: unexpected client info data (%s)", s) - } - key, val := kv[0], kv[1] - - switch key { - case "id": - info.ID, err = strconv.ParseInt(val, 10, 64) - case "addr": - info.Addr = val - case "laddr": - info.LAddr = val - case "fd": - info.FD, err = strconv.ParseInt(val, 10, 64) - case "name": - info.Name = val - case "age": - var age int - if age, err = strconv.Atoi(val); err == nil { - info.Age = time.Duration(age) * time.Second - } - case "idle": - var idle int - if idle, err = strconv.Atoi(val); err == nil { - info.Idle = time.Duration(idle) * time.Second - } - case "flags": - if val == "N" { - break - } - - for i := 0; i < len(val); i++ { - switch val[i] { - case 'S': - info.Flags |= ClientSlave - case 'O': - info.Flags |= ClientSlave | ClientMonitor - case 'M': - info.Flags |= ClientMaster - case 'P': - info.Flags |= ClientPubSub - case 'x': - info.Flags |= ClientMulti - case 'b': - info.Flags |= ClientBlocked - case 't': - info.Flags |= ClientTracking - case 'R': - info.Flags |= ClientTrackingBrokenRedir - case 'B': - info.Flags |= ClientTrackingBCAST - case 'd': - info.Flags |= ClientDirtyCAS - case 'c': - info.Flags |= ClientCloseAfterCommand - case 'u': - info.Flags |= ClientUnBlocked - case 'A': - info.Flags |= ClientCloseASAP - case 'U': - info.Flags |= ClientUnixSocket - case 'r': - info.Flags |= ClientReadOnly - case 'e': - info.Flags |= ClientNoEvict - case 'T': - info.Flags |= ClientNoTouch - default: - return nil, fmt.Errorf("redis: unexpected client info flags(%s)", string(val[i])) - } - } - case "db": - info.DB, err = strconv.Atoi(val) - case "sub": - info.Sub, err = strconv.Atoi(val) - case "psub": - info.PSub, err = strconv.Atoi(val) - case "ssub": - info.SSub, err = strconv.Atoi(val) - case "multi": - info.Multi, err = strconv.Atoi(val) - case "watch": - info.Watch, err = strconv.Atoi(val) - case "qbuf": - info.QueryBuf, err = strconv.Atoi(val) - case "qbuf-free": - info.QueryBufFree, err = strconv.Atoi(val) - case "argv-mem": - info.ArgvMem, err = strconv.Atoi(val) - case "multi-mem": - info.MultiMem, err = strconv.Atoi(val) - case "rbs": - info.BufferSize, err = strconv.Atoi(val) - case "rbp": - info.BufferPeak, err = strconv.Atoi(val) - case "obl": - info.OutputBufferLength, err = strconv.Atoi(val) - case "oll": - info.OutputListLength, err = strconv.Atoi(val) - case "omem": - info.OutputMemory, err = strconv.Atoi(val) - case "tot-mem": - info.TotalMemory, err = strconv.Atoi(val) - case "tot-net-in": - info.TotalNetIn, err = strconv.Atoi(val) - case "tot-net-out": - info.TotalNetOut, err = strconv.Atoi(val) - case "tot-cmds": - info.TotalCmds, err = strconv.Atoi(val) - case "events": - info.Events = val - case "cmd": - info.LastCmd = val - case "user": - info.User = val - case "redir": - info.Redir, err = strconv.ParseInt(val, 10, 64) - case "resp": - info.Resp, err = strconv.Atoi(val) - case "lib-name": - info.LibName = val - case "lib-ver": - info.LibVer = val - case "io-thread": - info.IoThread, err = strconv.Atoi(val) - default: - return nil, fmt.Errorf("redis: unexpected client info key(%s)", key) - } - - if err != nil { - return nil, err - } - } - - return info, nil -} - -func (cmd *ClientInfoCmd) Clone() Cmder { - var val *ClientInfo - if cmd.val != nil { - val = &ClientInfo{ - ID: cmd.val.ID, - Addr: cmd.val.Addr, - LAddr: cmd.val.LAddr, - FD: cmd.val.FD, - Name: cmd.val.Name, - Age: cmd.val.Age, - Idle: cmd.val.Idle, - Flags: cmd.val.Flags, - DB: cmd.val.DB, - Sub: cmd.val.Sub, - PSub: cmd.val.PSub, - SSub: cmd.val.SSub, - Multi: cmd.val.Multi, - Watch: cmd.val.Watch, - QueryBuf: cmd.val.QueryBuf, - QueryBufFree: cmd.val.QueryBufFree, - ArgvMem: cmd.val.ArgvMem, - MultiMem: cmd.val.MultiMem, - BufferSize: cmd.val.BufferSize, - BufferPeak: cmd.val.BufferPeak, - OutputBufferLength: cmd.val.OutputBufferLength, - OutputListLength: cmd.val.OutputListLength, - OutputMemory: cmd.val.OutputMemory, - TotalMemory: cmd.val.TotalMemory, - IoThread: cmd.val.IoThread, - Events: cmd.val.Events, - LastCmd: cmd.val.LastCmd, - User: cmd.val.User, - Redir: cmd.val.Redir, - Resp: cmd.val.Resp, - LibName: cmd.val.LibName, - LibVer: cmd.val.LibVer, - } - } - return &ClientInfoCmd{ - baseCmd: cmd.cloneBaseCmd(), - val: val, - } -} - -// ------------------------------------------- - -type ACLLogEntry struct { - Count int64 - Reason string - Context string - Object string - Username string - AgeSeconds float64 - ClientInfo *ClientInfo - EntryID int64 - TimestampCreated int64 - TimestampLastUpdated int64 -} - -type ACLLogCmd struct { - baseCmd - - val []*ACLLogEntry -} - -var _ Cmder = (*ACLLogCmd)(nil) - -func NewACLLogCmd(ctx context.Context, args ...interface{}) *ACLLogCmd { - return &ACLLogCmd{ - baseCmd: baseCmd{ - ctx: ctx, - args: args, - cmdType: CmdTypeACLLog, - }, - } -} - -func (cmd *ACLLogCmd) SetVal(val []*ACLLogEntry) { - cmd.val = val -} - -func (cmd *ACLLogCmd) Val() []*ACLLogEntry { - return cmd.val -} - -func (cmd *ACLLogCmd) Result() ([]*ACLLogEntry, error) { - return cmd.val, cmd.err -} - -func (cmd *ACLLogCmd) String() string { - return cmdString(cmd, cmd.val) -} - -func (cmd *ACLLogCmd) readReply(rd *proto.Reader) error { - n, err := rd.ReadArrayLen() - if err != nil { - return err - } - - cmd.val = make([]*ACLLogEntry, n) - for i := 0; i < n; i++ { - cmd.val[i] = &ACLLogEntry{} - entry := cmd.val[i] - respLen, err := rd.ReadMapLen() - if err != nil { - return err - } - for j := 0; j < respLen; j++ { - key, err := rd.ReadString() - if err != nil { - return err - } - - switch key { - case "count": - entry.Count, err = rd.ReadInt() - case "reason": - entry.Reason, err = rd.ReadString() - case "context": - entry.Context, err = rd.ReadString() - case "object": - entry.Object, err = rd.ReadString() - case "username": - entry.Username, err = rd.ReadString() - case "age-seconds": - entry.AgeSeconds, err = rd.ReadFloat() - case "client-info": - txt, err := rd.ReadString() - if err != nil { - return err - } - entry.ClientInfo, err = parseClientInfo(strings.TrimSpace(txt)) - if err != nil { - return err - } - case "entry-id": - entry.EntryID, err = rd.ReadInt() - case "timestamp-created": - entry.TimestampCreated, err = rd.ReadInt() - case "timestamp-last-updated": - entry.TimestampLastUpdated, err = rd.ReadInt() - default: - return fmt.Errorf("redis: unexpected key %q in ACL LOG reply", key) - } - - if err != nil { - return err - } - } - } - - return nil -} - -func (cmd *ACLLogCmd) Clone() Cmder { - var val []*ACLLogEntry - if cmd.val != nil { - val = make([]*ACLLogEntry, len(cmd.val)) - for i, entry := range cmd.val { - if entry != nil { - val[i] = &ACLLogEntry{ - Count: entry.Count, - Reason: entry.Reason, - Context: entry.Context, - Object: entry.Object, - Username: entry.Username, - AgeSeconds: entry.AgeSeconds, - EntryID: entry.EntryID, - TimestampCreated: entry.TimestampCreated, - TimestampLastUpdated: entry.TimestampLastUpdated, - } - // Clone ClientInfo if present - if entry.ClientInfo != nil { - val[i].ClientInfo = &ClientInfo{ - ID: entry.ClientInfo.ID, - Addr: entry.ClientInfo.Addr, - LAddr: entry.ClientInfo.LAddr, - FD: entry.ClientInfo.FD, - Name: entry.ClientInfo.Name, - Age: entry.ClientInfo.Age, - Idle: entry.ClientInfo.Idle, - Flags: entry.ClientInfo.Flags, - DB: entry.ClientInfo.DB, - Sub: entry.ClientInfo.Sub, - PSub: entry.ClientInfo.PSub, - SSub: entry.ClientInfo.SSub, - Multi: entry.ClientInfo.Multi, - Watch: entry.ClientInfo.Watch, - QueryBuf: entry.ClientInfo.QueryBuf, - QueryBufFree: entry.ClientInfo.QueryBufFree, - ArgvMem: entry.ClientInfo.ArgvMem, - MultiMem: entry.ClientInfo.MultiMem, - BufferSize: entry.ClientInfo.BufferSize, - BufferPeak: entry.ClientInfo.BufferPeak, - OutputBufferLength: entry.ClientInfo.OutputBufferLength, - OutputListLength: entry.ClientInfo.OutputListLength, - OutputMemory: entry.ClientInfo.OutputMemory, - TotalMemory: entry.ClientInfo.TotalMemory, - IoThread: entry.ClientInfo.IoThread, - Events: entry.ClientInfo.Events, - LastCmd: entry.ClientInfo.LastCmd, - User: entry.ClientInfo.User, - Redir: entry.ClientInfo.Redir, - Resp: entry.ClientInfo.Resp, - LibName: entry.ClientInfo.LibName, - LibVer: entry.ClientInfo.LibVer, - } - } - } - } - } - return &ACLLogCmd{ - baseCmd: cmd.cloneBaseCmd(), - val: val, - } -} - -// LibraryInfo holds the library info. -type LibraryInfo struct { - LibName *string - LibVer *string -} - -// WithLibraryName returns a valid LibraryInfo with library name only. -func WithLibraryName(libName string) LibraryInfo { - return LibraryInfo{LibName: &libName} -} - -// WithLibraryVersion returns a valid LibraryInfo with library version only. -func WithLibraryVersion(libVer string) LibraryInfo { - return LibraryInfo{LibVer: &libVer} -} - -// ------------------------------------------- - -type InfoCmd struct { - baseCmd - val map[string]map[string]string -} - -var _ Cmder = (*InfoCmd)(nil) - -func NewInfoCmd(ctx context.Context, args ...interface{}) *InfoCmd { - return &InfoCmd{ - baseCmd: baseCmd{ - ctx: ctx, - args: args, - cmdType: CmdTypeInfo, - }, - } -} - -func (cmd *InfoCmd) SetVal(val map[string]map[string]string) { - cmd.val = val -} - -func (cmd *InfoCmd) Val() map[string]map[string]string { - return cmd.val -} - -func (cmd *InfoCmd) Result() (map[string]map[string]string, error) { - return cmd.val, cmd.err -} - -func (cmd *InfoCmd) String() string { - return cmdString(cmd, cmd.val) -} - -func (cmd *InfoCmd) readReply(rd *proto.Reader) error { - val, err := rd.ReadString() - if err != nil { - return err - } - - section := "" - scanner := bufio.NewScanner(strings.NewReader(val)) - for scanner.Scan() { - line := scanner.Text() - if strings.HasPrefix(line, "#") { - if cmd.val == nil { - cmd.val = make(map[string]map[string]string) - } - section = strings.TrimPrefix(line, "# ") - cmd.val[section] = make(map[string]string) - } else if line != "" { - if section == "Modules" { - moduleRe := regexp.MustCompile(`module:name=(.+?),(.+)$`) - kv := moduleRe.FindStringSubmatch(line) - if len(kv) == 3 { - cmd.val[section][kv[1]] = kv[2] - } - } else { - kv := strings.SplitN(line, ":", 2) - if len(kv) == 2 { - cmd.val[section][kv[0]] = kv[1] - } - } - } - } - - return nil -} - -func (cmd *InfoCmd) Item(section, key string) string { - if cmd.val == nil { - return "" - } else if cmd.val[section] == nil { - return "" - } else { - return cmd.val[section][key] - } -} - -func (cmd *InfoCmd) Clone() Cmder { - var val map[string]map[string]string - if cmd.val != nil { - val = make(map[string]map[string]string, len(cmd.val)) - for section, sectionMap := range cmd.val { - if sectionMap != nil { - val[section] = make(map[string]string, len(sectionMap)) - for k, v := range sectionMap { - val[section][k] = v - } - } - } - } - return &InfoCmd{ - baseCmd: cmd.cloneBaseCmd(), - val: val, - } -} - -type MonitorStatus int - -const ( - monitorStatusIdle MonitorStatus = iota - monitorStatusStart - monitorStatusStop -) - -type MonitorCmd struct { - baseCmd - ch chan string - status MonitorStatus - mu sync.Mutex -} - -func newMonitorCmd(ctx context.Context, ch chan string) *MonitorCmd { - return &MonitorCmd{ - baseCmd: baseCmd{ - ctx: ctx, - args: []interface{}{"monitor"}, - cmdType: CmdTypeMonitor, - }, - ch: ch, - status: monitorStatusIdle, - mu: sync.Mutex{}, - } -} - -func (cmd *MonitorCmd) String() string { - return cmdString(cmd, nil) -} - -func (cmd *MonitorCmd) readReply(rd *proto.Reader) error { - ctx, cancel := context.WithCancel(cmd.ctx) - go func(ctx context.Context) { - for { - select { - case <-ctx.Done(): - return - default: - err := cmd.readMonitor(rd, cancel) - if err != nil { - cmd.err = err - return - } - } - } - }(ctx) - return nil -} - -func (cmd *MonitorCmd) readMonitor(rd *proto.Reader, cancel context.CancelFunc) error { - for { - cmd.mu.Lock() - st := cmd.status - pk, _ := rd.Peek(1) - cmd.mu.Unlock() - if len(pk) != 0 && st == monitorStatusStart { - cmd.mu.Lock() - line, err := rd.ReadString() - cmd.mu.Unlock() - if err != nil { - return err - } - cmd.ch <- line - } - if st == monitorStatusStop { - cancel() - break - } - } - return nil -} - -func (cmd *MonitorCmd) Start() { - cmd.mu.Lock() - defer cmd.mu.Unlock() - cmd.status = monitorStatusStart -} - -func (cmd *MonitorCmd) Stop() { - cmd.mu.Lock() - defer cmd.mu.Unlock() - cmd.status = monitorStatusStop -} - -type VectorScoreSliceCmd struct { - baseCmd - - val []VectorScore -} - -var _ Cmder = (*VectorScoreSliceCmd)(nil) - -func NewVectorInfoSliceCmd(ctx context.Context, args ...any) *VectorScoreSliceCmd { - return &VectorScoreSliceCmd{ - baseCmd: baseCmd{ - ctx: ctx, - args: args, - }, - } -} - -func (cmd *VectorScoreSliceCmd) SetVal(val []VectorScore) { - cmd.val = val -} - -func (cmd *VectorScoreSliceCmd) Val() []VectorScore { - return cmd.val -} - -func (cmd *VectorScoreSliceCmd) Result() ([]VectorScore, error) { - return cmd.val, cmd.err -} - -func (cmd *VectorScoreSliceCmd) String() string { - return cmdString(cmd, cmd.val) -} - -func (cmd *VectorScoreSliceCmd) readReply(rd *proto.Reader) error { - n, err := rd.ReadMapLen() - if err != nil { - return err - } - - cmd.val = make([]VectorScore, n) - for i := 0; i < n; i++ { - name, err := rd.ReadString() - if err != nil { - return err - } - cmd.val[i].Name = name - - score, err := rd.ReadFloat() - if err != nil { - return err - } - cmd.val[i].Score = score - } - - return nil -} - -func (cmd *VectorScoreSliceCmd) Clone() Cmder { - return &VectorScoreSliceCmd{ - baseCmd: cmd.cloneBaseCmd(), - val: cmd.val, - } -} - -func (cmd *MonitorCmd) Clone() Cmder { - // MonitorCmd cannot be safely cloned due to channels and goroutines - // Return a new MonitorCmd with the same channel - return newMonitorCmd(cmd.ctx, cmd.ch) -} - -// ExtractCommandValue extracts the value from a command result using the fast enum-based approach -func ExtractCommandValue(cmd interface{}) (interface{}, error) { - // First try to get the command type using the interface - if cmdTypeGetter, ok := cmd.(CmdTypeGetter); ok { - cmdType := cmdTypeGetter.GetCmdType() - - // Use fast type-based extraction - switch cmdType { - case CmdTypeGeneric: - if genericCmd, ok := cmd.(interface { - Val() interface{} - Err() error - }); ok { - return genericCmd.Val(), genericCmd.Err() - } - case CmdTypeString: - if stringCmd, ok := cmd.(interface { - Val() string - Err() error - }); ok { - return stringCmd.Val(), stringCmd.Err() - } - case CmdTypeInt: - if intCmd, ok := cmd.(interface { - Val() int64 - Err() error - }); ok { - return intCmd.Val(), intCmd.Err() - } - case CmdTypeBool: - if boolCmd, ok := cmd.(interface { - Val() bool - Err() error - }); ok { - return boolCmd.Val(), boolCmd.Err() - } - case CmdTypeFloat: - if floatCmd, ok := cmd.(interface { - Val() float64 - Err() error - }); ok { - return floatCmd.Val(), floatCmd.Err() - } - case CmdTypeStatus: - if statusCmd, ok := cmd.(interface { - Val() string - Err() error - }); ok { - return statusCmd.Val(), statusCmd.Err() - } - case CmdTypeDuration: - if durationCmd, ok := cmd.(interface { - Val() time.Duration - Err() error - }); ok { - return durationCmd.Val(), durationCmd.Err() - } - case CmdTypeTime: - if timeCmd, ok := cmd.(interface { - Val() time.Time - Err() error - }); ok { - return timeCmd.Val(), timeCmd.Err() - } - case CmdTypeStringStructMap: - if structMapCmd, ok := cmd.(interface { - Val() map[string]struct{} - Err() error - }); ok { - return structMapCmd.Val(), structMapCmd.Err() - } - case CmdTypeXMessageSlice: - if xMessageSliceCmd, ok := cmd.(interface { - Val() []XMessage - Err() error - }); ok { - return xMessageSliceCmd.Val(), xMessageSliceCmd.Err() - } - case CmdTypeXStreamSlice: - if xStreamSliceCmd, ok := cmd.(interface { - Val() []XStream - Err() error - }); ok { - return xStreamSliceCmd.Val(), xStreamSliceCmd.Err() - } - case CmdTypeXPending: - if xPendingCmd, ok := cmd.(interface { - Val() *XPending - Err() error - }); ok { - return xPendingCmd.Val(), xPendingCmd.Err() - } - case CmdTypeXPendingExt: - if xPendingExtCmd, ok := cmd.(interface { - Val() []XPendingExt - Err() error - }); ok { - return xPendingExtCmd.Val(), xPendingExtCmd.Err() - } - case CmdTypeXAutoClaim: - if xAutoClaimCmd, ok := cmd.(interface { - Val() ([]XMessage, string) - Err() error - }); ok { - messages, start := xAutoClaimCmd.Val() - return CmdTypeXAutoClaimValue{messages: messages, start: start}, xAutoClaimCmd.Err() - } - case CmdTypeXAutoClaimJustID: - if xAutoClaimJustIDCmd, ok := cmd.(interface { - Val() ([]string, string) - Err() error - }); ok { - ids, start := xAutoClaimJustIDCmd.Val() - return CmdTypeXAutoClaimJustIDValue{ids: ids, start: start}, xAutoClaimJustIDCmd.Err() - } - case CmdTypeXInfoConsumers: - if xInfoConsumersCmd, ok := cmd.(interface { - Val() []XInfoConsumer - Err() error - }); ok { - return xInfoConsumersCmd.Val(), xInfoConsumersCmd.Err() - } - case CmdTypeXInfoGroups: - if xInfoGroupsCmd, ok := cmd.(interface { - Val() []XInfoGroup - Err() error - }); ok { - return xInfoGroupsCmd.Val(), xInfoGroupsCmd.Err() - } - case CmdTypeXInfoStream: - if xInfoStreamCmd, ok := cmd.(interface { - Val() *XInfoStream - Err() error - }); ok { - return xInfoStreamCmd.Val(), xInfoStreamCmd.Err() - } - case CmdTypeXInfoStreamFull: - if xInfoStreamFullCmd, ok := cmd.(interface { - Val() *XInfoStreamFull - Err() error - }); ok { - return xInfoStreamFullCmd.Val(), xInfoStreamFullCmd.Err() - } - case CmdTypeZSlice: - if zSliceCmd, ok := cmd.(interface { - Val() []Z - Err() error - }); ok { - return zSliceCmd.Val(), zSliceCmd.Err() - } - case CmdTypeZWithKey: - if zWithKeyCmd, ok := cmd.(interface { - Val() *ZWithKey - Err() error - }); ok { - return zWithKeyCmd.Val(), zWithKeyCmd.Err() - } - case CmdTypeScan: - if scanCmd, ok := cmd.(interface { - Val() ([]string, uint64) - Err() error - }); ok { - keys, cursor := scanCmd.Val() - return CmdTypeScanValue{keys: keys, cursor: cursor}, scanCmd.Err() - } - case CmdTypeClusterSlots: - if clusterSlotsCmd, ok := cmd.(interface { - Val() []ClusterSlot - Err() error - }); ok { - return clusterSlotsCmd.Val(), clusterSlotsCmd.Err() - } - case CmdTypeGeoLocation: - if geoLocationCmd, ok := cmd.(interface { - Val() []GeoLocation - Err() error - }); ok { - return geoLocationCmd.Val(), geoLocationCmd.Err() - } - case CmdTypeGeoSearchLocation: - if geoSearchLocationCmd, ok := cmd.(interface { - Val() []GeoLocation - Err() error - }); ok { - return geoSearchLocationCmd.Val(), geoSearchLocationCmd.Err() - } - case CmdTypeGeoPos: - if geoPosCmd, ok := cmd.(interface { - Val() []*GeoPos - Err() error - }); ok { - return geoPosCmd.Val(), geoPosCmd.Err() - } - case CmdTypeCommandsInfo: - if commandsInfoCmd, ok := cmd.(interface { - Val() map[string]*CommandInfo - Err() error - }); ok { - return commandsInfoCmd.Val(), commandsInfoCmd.Err() - } - case CmdTypeSlowLog: - if slowLogCmd, ok := cmd.(interface { - Val() []SlowLog - Err() error - }); ok { - return slowLogCmd.Val(), slowLogCmd.Err() - } - case CmdTypeHotKeys: - if hotKeysCmd, ok := cmd.(interface { - Val() *HotKeysResult - Err() error - }); ok { - return hotKeysCmd.Val(), hotKeysCmd.Err() - } - case CmdTypeKeyValues: - if keyValuesCmd, ok := cmd.(interface { - Val() (string, []string) - Err() error - }); ok { - key, values := keyValuesCmd.Val() - return CmdTypeKeyValuesValue{key: key, values: values}, keyValuesCmd.Err() - } - case CmdTypeZSliceWithKey: - if zSliceWithKeyCmd, ok := cmd.(interface { - Val() (string, []Z) - Err() error - }); ok { - key, zSlice := zSliceWithKeyCmd.Val() - return CmdTypeZSliceWithKeyValue{key: key, zSlice: zSlice}, zSliceWithKeyCmd.Err() - } - case CmdTypeFunctionList: - if functionListCmd, ok := cmd.(interface { - Val() []Library - Err() error - }); ok { - return functionListCmd.Val(), functionListCmd.Err() - } - case CmdTypeFunctionStats: - if functionStatsCmd, ok := cmd.(interface { - Val() FunctionStats - Err() error - }); ok { - return functionStatsCmd.Val(), functionStatsCmd.Err() - } - case CmdTypeLCS: - if lcsCmd, ok := cmd.(interface { - Val() *LCSMatch - Err() error - }); ok { - return lcsCmd.Val(), lcsCmd.Err() - } - case CmdTypeKeyFlags: - if keyFlagsCmd, ok := cmd.(interface { - Val() []KeyFlags - Err() error - }); ok { - return keyFlagsCmd.Val(), keyFlagsCmd.Err() - } - case CmdTypeClusterLinks: - if clusterLinksCmd, ok := cmd.(interface { - Val() []ClusterLink - Err() error - }); ok { - return clusterLinksCmd.Val(), clusterLinksCmd.Err() - } - case CmdTypeClusterShards: - if clusterShardsCmd, ok := cmd.(interface { - Val() []ClusterShard - Err() error - }); ok { - return clusterShardsCmd.Val(), clusterShardsCmd.Err() - } - case CmdTypeRankWithScore: - if rankWithScoreCmd, ok := cmd.(interface { - Val() RankScore - Err() error - }); ok { - return rankWithScoreCmd.Val(), rankWithScoreCmd.Err() - } - case CmdTypeClientInfo: - if clientInfoCmd, ok := cmd.(interface { - Val() *ClientInfo - Err() error - }); ok { - return clientInfoCmd.Val(), clientInfoCmd.Err() - } - case CmdTypeACLLog: - if aclLogCmd, ok := cmd.(interface { - Val() []*ACLLogEntry - Err() error - }); ok { - return aclLogCmd.Val(), aclLogCmd.Err() - } - case CmdTypeInfo: - if infoCmd, ok := cmd.(interface { - Val() string - Err() error - }); ok { - return infoCmd.Val(), infoCmd.Err() - } - case CmdTypeMonitor: - if monitorCmd, ok := cmd.(interface { - Val() string - Err() error - }); ok { - return monitorCmd.Val(), monitorCmd.Err() - } - case CmdTypeJSON: - if jsonCmd, ok := cmd.(interface { - Val() string - Err() error - }); ok { - return jsonCmd.Val(), jsonCmd.Err() - } - case CmdTypeJSONSlice: - if jsonSliceCmd, ok := cmd.(interface { - Val() []interface{} - Err() error - }); ok { - return jsonSliceCmd.Val(), jsonSliceCmd.Err() - } - case CmdTypeIntPointerSlice: - if intPointerSliceCmd, ok := cmd.(interface { - Val() []*int64 - Err() error - }); ok { - return intPointerSliceCmd.Val(), intPointerSliceCmd.Err() - } - case CmdTypeScanDump: - if scanDumpCmd, ok := cmd.(interface { - Val() ScanDump - Err() error - }); ok { - return scanDumpCmd.Val(), scanDumpCmd.Err() - } - case CmdTypeBFInfo: - if bfInfoCmd, ok := cmd.(interface { - Val() BFInfo - Err() error - }); ok { - return bfInfoCmd.Val(), bfInfoCmd.Err() - } - case CmdTypeCFInfo: - if cfInfoCmd, ok := cmd.(interface { - Val() CFInfo - Err() error - }); ok { - return cfInfoCmd.Val(), cfInfoCmd.Err() - } - case CmdTypeCMSInfo: - if cmsInfoCmd, ok := cmd.(interface { - Val() CMSInfo - Err() error - }); ok { - return cmsInfoCmd.Val(), cmsInfoCmd.Err() - } - case CmdTypeTopKInfo: - if topKInfoCmd, ok := cmd.(interface { - Val() TopKInfo - Err() error - }); ok { - return topKInfoCmd.Val(), topKInfoCmd.Err() - } - case CmdTypeTDigestInfo: - if tDigestInfoCmd, ok := cmd.(interface { - Val() TDigestInfo - Err() error - }); ok { - return tDigestInfoCmd.Val(), tDigestInfoCmd.Err() - } - case CmdTypeFTSearch: - if ftSearchCmd, ok := cmd.(interface { - Val() FTSearchResult - Err() error - }); ok { - return ftSearchCmd.Val(), ftSearchCmd.Err() - } - case CmdTypeFTInfo: - if ftInfoCmd, ok := cmd.(interface { - Val() FTInfoResult - Err() error - }); ok { - return ftInfoCmd.Val(), ftInfoCmd.Err() - } - case CmdTypeFTSpellCheck: - if ftSpellCheckCmd, ok := cmd.(interface { - Val() []SpellCheckResult - Err() error - }); ok { - return ftSpellCheckCmd.Val(), ftSpellCheckCmd.Err() - } - case CmdTypeFTSynDump: - if ftSynDumpCmd, ok := cmd.(interface { - Val() []FTSynDumpResult - Err() error - }); ok { - return ftSynDumpCmd.Val(), ftSynDumpCmd.Err() - } - case CmdTypeAggregate: - if aggregateCmd, ok := cmd.(interface { - Val() *FTAggregateResult - Err() error - }); ok { - return aggregateCmd.Val(), aggregateCmd.Err() - } - case CmdTypeTSTimestampValue: - if tsTimestampValueCmd, ok := cmd.(interface { - Val() TSTimestampValue - Err() error - }); ok { - return tsTimestampValueCmd.Val(), tsTimestampValueCmd.Err() - } - case CmdTypeTSTimestampValueSlice: - if tsTimestampValueSliceCmd, ok := cmd.(interface { - Val() []TSTimestampValue - Err() error - }); ok { - return tsTimestampValueSliceCmd.Val(), tsTimestampValueSliceCmd.Err() - } - case CmdTypeStringSlice: - if stringSliceCmd, ok := cmd.(interface { - Val() []string - Err() error - }); ok { - return stringSliceCmd.Val(), stringSliceCmd.Err() - } - case CmdTypeIntSlice: - if intSliceCmd, ok := cmd.(interface { - Val() []int64 - Err() error - }); ok { - return intSliceCmd.Val(), intSliceCmd.Err() - } - case CmdTypeBoolSlice: - if boolSliceCmd, ok := cmd.(interface { - Val() []bool - Err() error - }); ok { - return boolSliceCmd.Val(), boolSliceCmd.Err() - } - case CmdTypeFloatSlice: - if floatSliceCmd, ok := cmd.(interface { - Val() []float64 - Err() error - }); ok { - return floatSliceCmd.Val(), floatSliceCmd.Err() - } - case CmdTypeSlice: - if sliceCmd, ok := cmd.(interface { - Val() []interface{} - Err() error - }); ok { - return sliceCmd.Val(), sliceCmd.Err() - } - case CmdTypeKeyValueSlice: - if keyValueSliceCmd, ok := cmd.(interface { - Val() []KeyValue - Err() error - }); ok { - return keyValueSliceCmd.Val(), keyValueSliceCmd.Err() - } - case CmdTypeMapStringString: - if mapCmd, ok := cmd.(interface { - Val() map[string]string - Err() error - }); ok { - return mapCmd.Val(), mapCmd.Err() - } - case CmdTypeMapStringInt: - if mapCmd, ok := cmd.(interface { - Val() map[string]int64 - Err() error - }); ok { - return mapCmd.Val(), mapCmd.Err() - } - case CmdTypeMapStringInterfaceSlice: - if mapCmd, ok := cmd.(interface { - Val() []map[string]interface{} - Err() error - }); ok { - return mapCmd.Val(), mapCmd.Err() - } - case CmdTypeMapStringInterface: - if mapCmd, ok := cmd.(interface { - Val() map[string]interface{} - Err() error - }); ok { - return mapCmd.Val(), mapCmd.Err() - } - case CmdTypeMapStringStringSlice: - if mapCmd, ok := cmd.(interface { - Val() []map[string]string - Err() error - }); ok { - return mapCmd.Val(), mapCmd.Err() - } - case CmdTypeMapMapStringInterface: - if mapCmd, ok := cmd.(interface { - Val() map[string]interface{} - Err() error - }); ok { - return mapCmd.Val(), mapCmd.Err() - } - default: - // For unknown command types, return nil - return nil, nil - } - } - - // If we can't get the command type, return nil - return nil, nil -} diff --git a/vendor/github.com/redis/go-redis/v9/command_policy_resolver.go b/vendor/github.com/redis/go-redis/v9/command_policy_resolver.go deleted file mode 100644 index da8c6d314..000000000 --- a/vendor/github.com/redis/go-redis/v9/command_policy_resolver.go +++ /dev/null @@ -1,209 +0,0 @@ -package redis - -import ( - "context" - "strings" - - "github.com/redis/go-redis/v9/internal/routing" -) - -type ( - module = string - commandName = string -) - -var defaultPolicies = map[module]map[commandName]*routing.CommandPolicy{ - "ft": { - "create": { - Request: routing.ReqDefault, - Response: routing.RespDefaultKeyless, - }, - "search": { - Request: routing.ReqDefault, - Response: routing.RespDefaultKeyless, - Tips: map[string]string{ - routing.ReadOnlyCMD: "", - }, - }, - "aggregate": { - Request: routing.ReqDefault, - Response: routing.RespDefaultKeyless, - Tips: map[string]string{ - routing.ReadOnlyCMD: "", - }, - }, - "dictadd": { - Request: routing.ReqDefault, - Response: routing.RespDefaultKeyless, - }, - "dictdump": { - Request: routing.ReqDefault, - Response: routing.RespDefaultKeyless, - Tips: map[string]string{ - routing.ReadOnlyCMD: "", - }, - }, - "dictdel": { - Request: routing.ReqDefault, - Response: routing.RespDefaultKeyless, - }, - "suglen": { - Request: routing.ReqDefault, - Response: routing.RespDefaultHashSlot, - Tips: map[string]string{ - routing.ReadOnlyCMD: "", - }, - }, - "cursor": { - Request: routing.ReqSpecial, - Response: routing.RespDefaultKeyless, - Tips: map[string]string{ - routing.ReadOnlyCMD: "", - }, - }, - "sugadd": { - Request: routing.ReqDefault, - Response: routing.RespDefaultHashSlot, - }, - "sugget": { - Request: routing.ReqDefault, - Response: routing.RespDefaultHashSlot, - Tips: map[string]string{ - routing.ReadOnlyCMD: "", - }, - }, - "sugdel": { - Request: routing.ReqDefault, - Response: routing.RespDefaultHashSlot, - }, - "spellcheck": { - Request: routing.ReqDefault, - Response: routing.RespDefaultKeyless, - Tips: map[string]string{ - routing.ReadOnlyCMD: "", - }, - }, - "explain": { - Request: routing.ReqDefault, - Response: routing.RespDefaultKeyless, - Tips: map[string]string{ - routing.ReadOnlyCMD: "", - }, - }, - "explaincli": { - Request: routing.ReqDefault, - Response: routing.RespDefaultKeyless, - Tips: map[string]string{ - routing.ReadOnlyCMD: "", - }, - }, - "aliasadd": { - Request: routing.ReqDefault, - Response: routing.RespDefaultKeyless, - }, - "aliasupdate": { - Request: routing.ReqDefault, - Response: routing.RespDefaultKeyless, - }, - "aliasdel": { - Request: routing.ReqDefault, - Response: routing.RespDefaultKeyless, - }, - "info": { - Request: routing.ReqDefault, - Response: routing.RespDefaultKeyless, - Tips: map[string]string{ - routing.ReadOnlyCMD: "", - }, - }, - "tagvals": { - Request: routing.ReqDefault, - Response: routing.RespDefaultKeyless, - Tips: map[string]string{ - routing.ReadOnlyCMD: "", - }, - }, - "syndump": { - Request: routing.ReqDefault, - Response: routing.RespDefaultKeyless, - Tips: map[string]string{ - routing.ReadOnlyCMD: "", - }, - }, - "synupdate": { - Request: routing.ReqDefault, - Response: routing.RespDefaultKeyless, - }, - "profile": { - Request: routing.ReqDefault, - Response: routing.RespDefaultKeyless, - Tips: map[string]string{ - routing.ReadOnlyCMD: "", - }, - }, - "alter": { - Request: routing.ReqDefault, - Response: routing.RespDefaultKeyless, - }, - "dropindex": { - Request: routing.ReqDefault, - Response: routing.RespDefaultKeyless, - }, - "drop": { - Request: routing.ReqDefault, - Response: routing.RespDefaultKeyless, - }, - }, -} - -type CommandInfoResolveFunc func(ctx context.Context, cmd Cmder) *routing.CommandPolicy - -type commandInfoResolver struct { - resolveFunc CommandInfoResolveFunc - fallBackResolver *commandInfoResolver -} - -func NewCommandInfoResolver(resolveFunc CommandInfoResolveFunc) *commandInfoResolver { - return &commandInfoResolver{ - resolveFunc: resolveFunc, - } -} - -func NewDefaultCommandPolicyResolver() *commandInfoResolver { - return NewCommandInfoResolver(func(ctx context.Context, cmd Cmder) *routing.CommandPolicy { - module := "core" - command := cmd.Name() - cmdParts := strings.Split(command, ".") - if len(cmdParts) == 2 { - module = cmdParts[0] - command = cmdParts[1] - } - - if policy, ok := defaultPolicies[module][command]; ok { - return policy - } - - return nil - }) -} - -func (r *commandInfoResolver) GetCommandPolicy(ctx context.Context, cmd Cmder) *routing.CommandPolicy { - if r.resolveFunc == nil { - return nil - } - - policy := r.resolveFunc(ctx, cmd) - if policy != nil { - return policy - } - - if r.fallBackResolver != nil { - return r.fallBackResolver.GetCommandPolicy(ctx, cmd) - } - - return nil -} - -func (r *commandInfoResolver) SetFallbackResolver(fallbackResolver *commandInfoResolver) { - r.fallBackResolver = fallbackResolver -} diff --git a/vendor/github.com/redis/go-redis/v9/commands.go b/vendor/github.com/redis/go-redis/v9/commands.go deleted file mode 100644 index 219fe464b..000000000 --- a/vendor/github.com/redis/go-redis/v9/commands.go +++ /dev/null @@ -1,797 +0,0 @@ -package redis - -import ( - "context" - "encoding" - "errors" - "fmt" - "io" - "net" - "reflect" - "runtime" - "strings" - "time" - - "github.com/redis/go-redis/v9/internal" -) - -// KeepTTL is a Redis KEEPTTL option to keep existing TTL, it requires your redis-server version >= 6.0, -// otherwise you will receive an error: (error) ERR syntax error. -// For example: -// -// rdb.Set(ctx, key, value, redis.KeepTTL) -const KeepTTL = -1 - -func usePrecise(dur time.Duration) bool { - return dur < time.Second || dur%time.Second != 0 -} - -func formatMs(ctx context.Context, dur time.Duration) int64 { - if dur > 0 && dur < time.Millisecond { - internal.Logger.Printf( - ctx, - "specified duration is %s, but minimal supported value is %s - truncating to 1ms", - dur, time.Millisecond, - ) - return 1 - } - return int64(dur / time.Millisecond) -} - -func formatSec(ctx context.Context, dur time.Duration) int64 { - if dur > 0 && dur < time.Second { - internal.Logger.Printf( - ctx, - "specified duration is %s, but minimal supported value is %s - truncating to 1s", - dur, time.Second, - ) - return 1 - } - return int64(dur / time.Second) -} - -func appendArgs(dst, src []interface{}) []interface{} { - if len(src) == 1 { - return appendArg(dst, src[0]) - } - - if cap(dst) < len(dst)+len(src) { - newDst := make([]interface{}, len(dst), len(dst)+len(src)) - copy(newDst, dst) - dst = newDst - } - dst = append(dst, src...) - return dst -} - -func appendArg(dst []interface{}, arg interface{}) []interface{} { - switch arg := arg.(type) { - case []string: - for _, s := range arg { - dst = append(dst, s) - } - return dst - case []interface{}: - dst = append(dst, arg...) - return dst - case map[string]interface{}: - for k, v := range arg { - dst = append(dst, k, v) - } - return dst - case map[string]string: - for k, v := range arg { - dst = append(dst, k, v) - } - return dst - case time.Time, time.Duration, encoding.BinaryMarshaler, net.IP: - return append(dst, arg) - case nil: - return dst - default: - // scan struct field - v := reflect.ValueOf(arg) - if v.Type().Kind() == reflect.Ptr { - if v.IsNil() { - // error: arg is not a valid object - return dst - } - v = v.Elem() - } - - if v.Type().Kind() == reflect.Struct { - return appendStructField(dst, v) - } - - return append(dst, arg) - } -} - -// appendStructField appends the field and value held by the structure v to dst, and returns the appended dst. -func appendStructField(dst []interface{}, v reflect.Value) []interface{} { - typ := v.Type() - for i := 0; i < typ.NumField(); i++ { - tag := typ.Field(i).Tag.Get("redis") - if tag == "" || tag == "-" { - continue - } - name, opt, _ := strings.Cut(tag, ",") - if name == "" { - continue - } - - field := v.Field(i) - - // miss field - if omitEmpty(opt) && isEmptyValue(field) { - continue - } - - if field.CanInterface() { - dst = append(dst, name, field.Interface()) - } - } - - return dst -} - -func omitEmpty(opt string) bool { - for opt != "" { - var name string - name, opt, _ = strings.Cut(opt, ",") - if name == "omitempty" { - return true - } - } - return false -} - -func isEmptyValue(v reflect.Value) bool { - switch v.Kind() { - case reflect.Array, reflect.Map, reflect.Slice, reflect.String: - return v.Len() == 0 - case reflect.Bool: - return !v.Bool() - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - return v.Int() == 0 - case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: - return v.Uint() == 0 - case reflect.Float32, reflect.Float64: - return v.Float() == 0 - case reflect.Interface, reflect.Pointer: - return v.IsNil() - case reflect.Struct: - if v.Type() == reflect.TypeOf(time.Time{}) { - return v.IsZero() - } - // Only supports the struct time.Time, - // subsequent iterations will follow the func Scan support decoder. - } - return false -} - -type Cmdable interface { - Pipeline() Pipeliner - Pipelined(ctx context.Context, fn func(Pipeliner) error) ([]Cmder, error) - - TxPipelined(ctx context.Context, fn func(Pipeliner) error) ([]Cmder, error) - TxPipeline() Pipeliner - - Command(ctx context.Context) *CommandsInfoCmd - CommandList(ctx context.Context, filter *FilterBy) *StringSliceCmd - CommandGetKeys(ctx context.Context, commands ...interface{}) *StringSliceCmd - CommandGetKeysAndFlags(ctx context.Context, commands ...interface{}) *KeyFlagsCmd - ClientGetName(ctx context.Context) *StringCmd - Echo(ctx context.Context, message interface{}) *StringCmd - Ping(ctx context.Context) *StatusCmd - Quit(ctx context.Context) *StatusCmd - Unlink(ctx context.Context, keys ...string) *IntCmd - - BgRewriteAOF(ctx context.Context) *StatusCmd - BgSave(ctx context.Context) *StatusCmd - ClientKill(ctx context.Context, ipPort string) *StatusCmd - ClientKillByFilter(ctx context.Context, keys ...string) *IntCmd - ClientList(ctx context.Context) *StringCmd - ClientInfo(ctx context.Context) *ClientInfoCmd - ClientPause(ctx context.Context, dur time.Duration) *BoolCmd - ClientUnpause(ctx context.Context) *BoolCmd - ClientID(ctx context.Context) *IntCmd - ClientUnblock(ctx context.Context, id int64) *IntCmd - ClientUnblockWithError(ctx context.Context, id int64) *IntCmd - ClientMaintNotifications(ctx context.Context, enabled bool, endpointType string) *StatusCmd - ConfigGet(ctx context.Context, parameter string) *MapStringStringCmd - ConfigResetStat(ctx context.Context) *StatusCmd - ConfigSet(ctx context.Context, parameter, value string) *StatusCmd - ConfigRewrite(ctx context.Context) *StatusCmd - DBSize(ctx context.Context) *IntCmd - FlushAll(ctx context.Context) *StatusCmd - FlushAllAsync(ctx context.Context) *StatusCmd - FlushDB(ctx context.Context) *StatusCmd - FlushDBAsync(ctx context.Context) *StatusCmd - Info(ctx context.Context, section ...string) *StringCmd - LastSave(ctx context.Context) *IntCmd - Save(ctx context.Context) *StatusCmd - Shutdown(ctx context.Context) *StatusCmd - ShutdownSave(ctx context.Context) *StatusCmd - ShutdownNoSave(ctx context.Context) *StatusCmd - SlaveOf(ctx context.Context, host, port string) *StatusCmd - SlowLogGet(ctx context.Context, num int64) *SlowLogCmd - SlowLogLen(ctx context.Context) *IntCmd - SlowLogReset(ctx context.Context) *StatusCmd - Time(ctx context.Context) *TimeCmd - DebugObject(ctx context.Context, key string) *StringCmd - MemoryUsage(ctx context.Context, key string, samples ...int) *IntCmd - Latency(ctx context.Context) *LatencyCmd - LatencyReset(ctx context.Context, events ...interface{}) *StatusCmd - - ModuleLoadex(ctx context.Context, conf *ModuleLoadexConfig) *StringCmd - - ACLCmdable - BitMapCmdable - ClusterCmdable - GenericCmdable - GeoCmdable - HashCmdable - HyperLogLogCmdable - ListCmdable - ProbabilisticCmdable - PubSubCmdable - ScriptingFunctionsCmdable - SearchCmdable - SetCmdable - SortedSetCmdable - StringCmdable - StreamCmdable - TimeseriesCmdable - JSONCmdable - VectorSetCmdable -} - -type StatefulCmdable interface { - Cmdable - Auth(ctx context.Context, password string) *StatusCmd - AuthACL(ctx context.Context, username, password string) *StatusCmd - Select(ctx context.Context, index int) *StatusCmd - SwapDB(ctx context.Context, index1, index2 int) *StatusCmd - ClientSetName(ctx context.Context, name string) *BoolCmd - ClientSetInfo(ctx context.Context, info LibraryInfo) *StatusCmd - Hello(ctx context.Context, ver int, username, password, clientName string) *MapStringInterfaceCmd -} - -var ( - _ Cmdable = (*Client)(nil) - _ Cmdable = (*Tx)(nil) - _ Cmdable = (*Ring)(nil) - _ Cmdable = (*ClusterClient)(nil) - _ Cmdable = (*Pipeline)(nil) -) - -type cmdable func(ctx context.Context, cmd Cmder) error - -type statefulCmdable func(ctx context.Context, cmd Cmder) error - -//------------------------------------------------------------------------------ - -func (c statefulCmdable) Auth(ctx context.Context, password string) *StatusCmd { - cmd := NewStatusCmd(ctx, "auth", password) - _ = c(ctx, cmd) - return cmd -} - -// AuthACL Perform an AUTH command, using the given user and pass. -// Should be used to authenticate the current connection with one of the connections defined in the ACL list -// when connecting to a Redis 6.0 instance, or greater, that is using the Redis ACL system. -func (c statefulCmdable) AuthACL(ctx context.Context, username, password string) *StatusCmd { - cmd := NewStatusCmd(ctx, "auth", username, password) - _ = c(ctx, cmd) - return cmd -} - -func (c cmdable) Wait(ctx context.Context, numSlaves int, timeout time.Duration) *IntCmd { - cmd := NewIntCmd(ctx, "wait", numSlaves, int(timeout/time.Millisecond)) - cmd.setReadTimeout(timeout) - _ = c(ctx, cmd) - return cmd -} - -func (c cmdable) WaitAOF(ctx context.Context, numLocal, numSlaves int, timeout time.Duration) *IntCmd { - cmd := NewIntCmd(ctx, "waitAOF", numLocal, numSlaves, int(timeout/time.Millisecond)) - cmd.setReadTimeout(timeout) - _ = c(ctx, cmd) - return cmd -} - -func (c statefulCmdable) Select(ctx context.Context, index int) *StatusCmd { - cmd := NewStatusCmd(ctx, "select", index) - _ = c(ctx, cmd) - return cmd -} - -func (c statefulCmdable) SwapDB(ctx context.Context, index1, index2 int) *StatusCmd { - cmd := NewStatusCmd(ctx, "swapdb", index1, index2) - _ = c(ctx, cmd) - return cmd -} - -// ClientSetName assigns a name to the connection. -func (c statefulCmdable) ClientSetName(ctx context.Context, name string) *BoolCmd { - cmd := NewBoolCmd(ctx, "client", "setname", name) - _ = c(ctx, cmd) - return cmd -} - -// ClientSetInfo sends a CLIENT SETINFO command with the provided info. -func (c statefulCmdable) ClientSetInfo(ctx context.Context, info LibraryInfo) *StatusCmd { - err := info.Validate() - if err != nil { - panic(err.Error()) - } - - var cmd *StatusCmd - if info.LibName != nil { - libName := fmt.Sprintf("go-redis(%s,%s)", *info.LibName, internal.ReplaceSpaces(runtime.Version())) - cmd = NewStatusCmd(ctx, "client", "setinfo", "LIB-NAME", libName) - } else { - cmd = NewStatusCmd(ctx, "client", "setinfo", "LIB-VER", *info.LibVer) - } - - _ = c(ctx, cmd) - return cmd -} - -// Validate checks if only one field in the struct is non-nil. -func (info LibraryInfo) Validate() error { - if info.LibName != nil && info.LibVer != nil { - return errors.New("both LibName and LibVer cannot be set at the same time") - } - if info.LibName == nil && info.LibVer == nil { - return errors.New("at least one of LibName and LibVer should be set") - } - return nil -} - -// Hello sets the resp protocol used. -func (c statefulCmdable) Hello(ctx context.Context, - ver int, username, password, clientName string, -) *MapStringInterfaceCmd { - args := make([]interface{}, 0, 7) - args = append(args, "hello", ver) - if password != "" { - if username != "" { - args = append(args, "auth", username, password) - } else { - args = append(args, "auth", "default", password) - } - } - if clientName != "" { - args = append(args, "setname", clientName) - } - cmd := NewMapStringInterfaceCmd(ctx, args...) - _ = c(ctx, cmd) - return cmd -} - -//------------------------------------------------------------------------------ - -func (c cmdable) Command(ctx context.Context) *CommandsInfoCmd { - cmd := NewCommandsInfoCmd(ctx, "command") - _ = c(ctx, cmd) - return cmd -} - -// FilterBy is used for the `CommandList` command parameter. -type FilterBy struct { - Module string - ACLCat string - Pattern string -} - -func (c cmdable) CommandList(ctx context.Context, filter *FilterBy) *StringSliceCmd { - args := make([]interface{}, 0, 5) - args = append(args, "command", "list") - if filter != nil { - if filter.Module != "" { - args = append(args, "filterby", "module", filter.Module) - } else if filter.ACLCat != "" { - args = append(args, "filterby", "aclcat", filter.ACLCat) - } else if filter.Pattern != "" { - args = append(args, "filterby", "pattern", filter.Pattern) - } - } - cmd := NewStringSliceCmd(ctx, args...) - _ = c(ctx, cmd) - return cmd -} - -func (c cmdable) CommandGetKeys(ctx context.Context, commands ...interface{}) *StringSliceCmd { - args := make([]interface{}, 2+len(commands)) - args[0] = "command" - args[1] = "getkeys" - copy(args[2:], commands) - cmd := NewStringSliceCmd(ctx, args...) - _ = c(ctx, cmd) - return cmd -} - -func (c cmdable) CommandGetKeysAndFlags(ctx context.Context, commands ...interface{}) *KeyFlagsCmd { - args := make([]interface{}, 2+len(commands)) - args[0] = "command" - args[1] = "getkeysandflags" - copy(args[2:], commands) - cmd := NewKeyFlagsCmd(ctx, args...) - _ = c(ctx, cmd) - return cmd -} - -// ClientGetName returns the name of the connection. -func (c cmdable) ClientGetName(ctx context.Context) *StringCmd { - cmd := NewStringCmd(ctx, "client", "getname") - _ = c(ctx, cmd) - return cmd -} - -func (c cmdable) Echo(ctx context.Context, message interface{}) *StringCmd { - cmd := NewStringCmd(ctx, "echo", message) - _ = c(ctx, cmd) - return cmd -} - -func (c cmdable) Ping(ctx context.Context) *StatusCmd { - cmd := NewStatusCmd(ctx, "ping") - _ = c(ctx, cmd) - return cmd -} - -func (c cmdable) Do(ctx context.Context, args ...interface{}) *Cmd { - cmd := NewCmd(ctx, args...) - _ = c(ctx, cmd) - return cmd -} - -// Quit closes the connection. -// -// Deprecated: Just close the connection instead as of Redis 7.2.0. -func (c cmdable) Quit(_ context.Context) *StatusCmd { - panic("not implemented") -} - -//------------------------------------------------------------------------------ - -func (c cmdable) BgRewriteAOF(ctx context.Context) *StatusCmd { - cmd := NewStatusCmd(ctx, "bgrewriteaof") - _ = c(ctx, cmd) - return cmd -} - -func (c cmdable) BgSave(ctx context.Context) *StatusCmd { - cmd := NewStatusCmd(ctx, "bgsave") - _ = c(ctx, cmd) - return cmd -} - -func (c cmdable) ClientKill(ctx context.Context, ipPort string) *StatusCmd { - cmd := NewStatusCmd(ctx, "client", "kill", ipPort) - _ = c(ctx, cmd) - return cmd -} - -// ClientKillByFilter is new style syntax, while the ClientKill is old -// -// CLIENT KILL