diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index e93d0e26..0f2a6761 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -130,6 +130,7 @@ jobs: MSSQL_PASSWORD: MsSql!1234 TEST_AZURITE_IMAGE: mcr.microsoft.com/azure-storage/azurite:latest TEST_CLICKHOUSE_IMAGE: "clickhouse/clickhouse-server:23-alpine" + TEST_CASSANDRA_IMAGE: "cassandra:4.1.3" TEST_COUCHBASE_IMAGE: "couchbase:enterprise-7.6.5" TEST_DYNAMODB_IMAGE: amazon/dynamodb-local:latest TEST_MINIO_IMAGE: "docker.io/minio/minio:RELEASE.2024-08-17T01-24-54Z" diff --git a/.github/workflows/test-cassandra.yml b/.github/workflows/test-cassandra.yml new file mode 100644 index 00000000..06687007 --- /dev/null +++ b/.github/workflows/test-cassandra.yml @@ -0,0 +1,30 @@ +on: + push: + branches: + - master + - main + paths: + - 'cassandra/**' + pull_request: + paths: + - 'cassandra/**' +name: 'Tests Cassandra' +jobs: + Tests: + runs-on: ubuntu-latest + strategy: + matrix: + go-version: + - 1.23.x + - 1.24.x + steps: + - name: Fetch Repository + uses: actions/checkout@v4 + - name: Install Go + uses: actions/setup-go@v5 + with: + go-version: '${{ matrix.go-version }}' + - name: Run Test + env: + TEST_CASSANDRA_IMAGE: cassandra:4.1.3 + run: cd ./cassandra && go clean -testcache && go test ./... -v -race diff --git a/README.md b/README.md index ddb141b3..b58f1390 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,7 @@ type Storage interface { - [AzureBlob](./azureblob/README.md) - [Badger](./badger/README.md) - [Bbolt](./bbolt) +- [Cassandra](./cassandra/README.md) - [CloudflareKV](./cloudflarekv/README.md) - [Coherence](./coherence/README.md) - [Couchbase](./couchbase/README.md) diff --git a/cassandra/README.md b/cassandra/README.md new file mode 100644 index 00000000..d7124476 --- /dev/null +++ b/cassandra/README.md @@ -0,0 +1,108 @@ +# Cassandra + +A Cassandra storage driver using [https://github.com/gocql/gocql](https://github.com/apache/cassandra-gocql-driver). + +![Release](https://img.shields.io/github/v/tag/gofiber/storage?filter=cassandra*) +[![Discord](https://img.shields.io/discord/704680098577514527?style=flat&label=%F0%9F%92%AC%20discord&color=00ACD7)](https://gofiber.io/discord) +![Test](https://img.shields.io/github/actions/workflow/status/gofiber/storage/test-cassandra.yml?label=Tests) + +### Table of Contents + +- [Signatures](#signatures) +- [Installation](#installation) +- [Examples](#examples) +- [Config](#config) +- [Default Config](#default-config) + +### Signatures + +```go +func New(config ...Config) (*Storage, error) +func (s *Storage) Get(key string) ([]byte, error) +func (s *Storage) Set(key string, val []byte, exp time.Duration) error +func (s *Storage) Delete(key string) error +func (s *Storage) Reset() error +func (s *Storage) Close() error +func (s *Storage) Conn() *Session +``` + +### Installation + +Cassandra is supported on the latest two versions of Go: + +Install the cassandra implementation: +```bash +go get github.com/gofiber/storage/cassandra +``` + +### Running the tests + +This module uses [Testcontainers for Go](https://github.com/testcontainers/testcontainers-go/) to run integration tests, which will start a local instance of Cassandra as a Docker container under the hood. To run the tests, you must have Docker (or another container runtime 100% compatible with the Docker APIs) installed on your machine. + +### Local development + +Before running this implementation, you must ensure a Cassandra cluster is available. +For local development, we recommend using the Cassandra Docker image; it contains everything +necessary for the client to operate correctly. + +To start Cassandra using Docker, issue the following: + +```bash +docker run --name cassandra --network host -d cassandra:tag +``` + +After running this command you're ready to start using the storage and connecting to the database. + +### Examples + +You can use the following options to create a cassandra storage driver: +```go +import "github.com/gofiber/storage/cassandra" + +// Initialize default config, to connect to localhost:9042 using the memory engine and with a clean table. +store := New(Config{ + Hosts: []string{"localhost:9042"}, + Keyspace: "test_keyspace_creation", + Table: "test_kv", + Expiration : 10 * time.Minute, +}) +``` + +### Config + +```go +// Config defines the configuration options for the Cassandra storage +type Config struct { + // Optional. Default is localhost + // Hosts is a list of Cassandra nodes to connect to. + Hosts []string + // Optional. Default is gofiber + // Keyspace is the name of the Cassandra keyspace to use. + Keyspace string + // Optional. Default is kv_store + /// Table is the name of the Cassandra table to use. + Table string + // Optional. Default is Quorum + // Consistency is the Cassandra consistency level. + Consistency gocql.Consistency + // Optional. Default is 10 minutes + // Expiration is the time after which an entry is considered expired. + Expiration time.Duration + // Optional. Default is false + // Reset is a flag to reset the database. + Reset bool +} +``` + +### Default Config + +```go +var ConfigDefault = Config{ + Hosts: []string{"localhost:9042"}, + Keyspace: "gofiber", + Table: "kv_store", + Consistency: gocql.Quorum, + Reset: false, + Expiration: 10 * time.Minute, +} +``` diff --git a/cassandra/cassandra.go b/cassandra/cassandra.go new file mode 100644 index 00000000..cd2c17cd --- /dev/null +++ b/cassandra/cassandra.go @@ -0,0 +1,416 @@ +package cassandra + +import ( + "errors" + "fmt" + "log" + "time" + + "github.com/gocql/gocql" +) + +// Storage represents a Cassandra storage implementation +type Storage struct { + cluster *gocql.ClusterConfig + session *gocql.Session + keyspace string + table string + ttl int +} + +// SchemaInfo represents the schema metadata +type SchemaInfo struct { + Version int + Description string + CreatedAt time.Time + UpdatedAt time.Time +} + +// New creates a new Cassandra storage instance +func New(cfg Config) *Storage { + // Create cluster config + cluster := gocql.NewCluster(cfg.Hosts...) + + // Don't set keyspace initially - we need to create it first + // We'll connect to system keyspace first + + // Convert expiration to seconds for TTL + ttl := 0 + if cfg.Expiration > 0 { + ttl = int(cfg.Expiration.Seconds()) + } + + // Create storage instance + storage := &Storage{ + cluster: cluster, + keyspace: cfg.Keyspace, + table: cfg.Table, + ttl: ttl, + } + + // Initialize keyspace + if err := storage.createOrVerifyKeySpace(cfg.Reset); err != nil { + log.Printf("Failed to initialize keyspace: %v", err) + panic(err) + } + + return storage +} + +// createOrVerifyKeySpace ensures the keyspace and table exist with proper keyspace +func (s *Storage) createOrVerifyKeySpace(reset bool) error { + // Connect to system keyspace first to create our keyspace if needed + systemCluster := gocql.NewCluster(s.cluster.Hosts...) + systemCluster.Consistency = s.cluster.Consistency + systemCluster.Timeout = s.cluster.Timeout + + // Connect to the system keyspace + systemSession, err := systemCluster.CreateSession() + if err != nil { + return fmt.Errorf("failed to connect to system keyspace: %w", err) + } + defer systemSession.Close() + + // Create keyspace if not exists + err = s.ensureKeyspace(systemSession) + if err != nil { + return fmt.Errorf("failed to ensure keyspace exists: %w", err) + } + + // Now connect to our keyspace + s.cluster.Keyspace = s.keyspace + session, err := s.cluster.CreateSession() + if err != nil { + return fmt.Errorf("failed to connect to keyspace %s: %w", s.keyspace, err) + } + s.session = session + + // Drop tables if reset is requested + if reset { + if err := s.dropTables(); err != nil { + return err + } + } + + // Create data table if necessary + if err := s.createDataTable(); err != nil { + return err + } + + return nil +} + +// ensureKeyspace creates the keyspace if it doesn't exist +func (s *Storage) ensureKeyspace(systemSession *gocql.Session) error { + // Check if keyspace exists + var count int + if err := systemSession.Query( + "SELECT COUNT(*) FROM system_schema.keyspaces WHERE keyspace_name = ?", + s.keyspace, + ).Scan(&count); err != nil { + return err + } + + // Create keyspace if it doesn't exist + if count == 0 { + query := fmt.Sprintf( + "CREATE KEYSPACE %s WITH REPLICATION = {'class': 'SimpleStrategy', 'replication_factor': 1}", + s.keyspace, + ) + if err := systemSession.Query(query).Exec(); err != nil { + return err + } + log.Printf("Created keyspace: %s", s.keyspace) + } + + return nil +} + +// createDataTable creates the data table for key-value storage +func (s *Storage) createDataTable() error { + query := fmt.Sprintf(` + CREATE TABLE IF NOT EXISTS %s.%s ( + key text PRIMARY KEY, + value blob, + created_at timestamp, + expires_at timestamp + ) + `, s.keyspace, s.table) + + return s.session.Query(query).Exec() +} + +// dropTables drops existing tables for reset +func (s *Storage) dropTables() error { + // Drop data table + query := fmt.Sprintf("DROP TABLE IF EXISTS %s.%s", s.keyspace, s.table) + if err := s.session.Query(query).Exec(); err != nil { + return err + } + + // Drop schema_info table + query = fmt.Sprintf("DROP TABLE IF EXISTS %s.schema_info", s.keyspace) + return s.session.Query(query).Exec() +} + +// Set stores a key-value pair with optional expiration +func (s *Storage) Set(key string, value []byte, exp time.Duration) error { + // Calculate expiration time + var expiresAt *time.Time + var ttl int = -1 // Default to no TTL + + if exp > 0 { + // Specific expiration provided + ttl = int(exp.Seconds()) + t := time.Now().Add(exp) + expiresAt = &t + } else if exp == 0 && s.ttl > 0 { + // Use default TTL from config + ttl = s.ttl + t := time.Now().Add(time.Duration(s.ttl) * time.Second) + expiresAt = &t + } + // If exp < 0, we'll use no TTL (indefinite storage) + + // Insert with TTL if specified + var query string + if ttl > 0 { + query = fmt.Sprintf("INSERT INTO %s.%s (key, value, created_at, expires_at) VALUES (?, ?, ?, ?) USING TTL %d", + s.keyspace, s.table, ttl) + } else { + query = fmt.Sprintf("INSERT INTO %s.%s (key, value, created_at, expires_at) VALUES (?, ?, ?, ?)", + s.keyspace, s.table) + } + + return s.session.Query(query, key, value, time.Now(), expiresAt).Exec() +} + +// Get retrieves a value by key +func (s *Storage) Get(key string) ([]byte, error) { + var value []byte + var expiresAt time.Time + + query := fmt.Sprintf("SELECT value, expires_at FROM %s.%s WHERE key = ?", s.keyspace, s.table) + if err := s.session.Query(query, key).Scan(&value, &expiresAt); err != nil { + if errors.Is(err, gocql.ErrNotFound) { + return nil, nil + } + return nil, err + } + + // Check if expired (as a backup in case TTL didn't work) + if !expiresAt.IsZero() && expiresAt.Before(time.Now()) { + // Expired but not yet removed by TTL + err := s.Delete(key) + if err != nil { + log.Printf("Failed to delete expired key %s: %v", key, err) + } + return nil, nil + } + + return value, nil +} + +// Delete removes a key from storage +func (s *Storage) Delete(key string) error { + query := fmt.Sprintf("DELETE FROM %s.%s WHERE key = ?", s.keyspace, s.table) + return s.session.Query(query, key).Exec() +} + +// Reset clears all keys from storage +func (s *Storage) Reset() error { + query := fmt.Sprintf("TRUNCATE TABLE %s.%s", s.keyspace, s.table) + return s.session.Query(query).Exec() +} + +// Close closes the storage connection +func (s *Storage) Close() { + if s.session != nil { + s.session.Close() + } +} + +// Test functions + +// // setupCassandraContainer creates a Cassandra container using the official module +// func setupCassandraContainer(ctx context.Context) (*cassandracontainer.CassandraContainer, string, error) { +// cassandraContainer, err := cassandracontainer.RunContainer(ctx, +// testcontainers.WithImage("cassandra:4.1"), +// cassandracontainer.WithInitialWaitTimeout(2*time.Minute), +// ) +// if err != nil { +// return nil, "", err +// } + +// // Get connection parameters +// host, err := cassandraContainer.Host(ctx) +// if err != nil { +// return nil, "", err +// } + +// mappedPort, err := cassandraContainer.MappedPort(ctx, "9042/tcp") +// if err != nil { +// return nil, "", err +// } + +// connectionURL := host + ":" + mappedPort.Port() +// return cassandraContainer, connectionURL, nil +// } + +// func TestSchemaManagement(t *testing.T) { +// ctx := context.Background() + +// // Start Cassandra container +// cassandraContainer, connectionURL, err := setupCassandraContainer(ctx) +// if err != nil { +// t.Fatalf("Failed to start Cassandra container: %v", err) +// } +// defer func() { +// if err := cassandraContainer.Terminate(ctx); err != nil { +// t.Logf("Failed to terminate container: %v", err) +// } +// }() + +// // 1. Test keyspace creation +// store := New(Config{ +// Hosts: []string{connectionURL}, +// Keyspace: "test_keyspace_creation", +// Table: "test_table", +// SchemaVersion: 1, +// SchemaDescription: "Initial Schema", +// }) + +// // Verify keyspace was created +// systemCluster := gocql.NewCluster(connectionURL) +// systemSession, err := systemCluster.CreateSession() +// if err != nil { +// t.Fatalf("Failed to connect to system keyspace: %v", err) +// } + +// var count int +// err = systemSession.Query( +// "SELECT COUNT(*) FROM system_schema.keyspaces WHERE keyspace_name = ?", +// "test_keyspace_creation", +// ).Scan(&count) +// assert.NoError(t, err) +// assert.Equal(t, 1, count, "Keyspace should have been created") +// systemSession.Close() + +// // 2. Test table creation +// // Connect to the keyspace and check if tables exist +// cluster := gocql.NewCluster(connectionURL) +// cluster.Keyspace = "test_keyspace_creation" +// session, err := cluster.CreateSession() +// if err != nil { +// t.Fatalf("Failed to connect to keyspace: %v", err) +// } + +// // Check for data table +// err = session.Query( +// "SELECT COUNT(*) FROM system_schema.tables WHERE keyspace_name = ? AND table_name = ?", +// "test_keyspace_creation", "test_table", +// ).Scan(&count) +// assert.NoError(t, err) +// assert.Equal(t, 1, count, "Data table should have been created") + +// // Check for schema_info table +// err = session.Query( +// "SELECT COUNT(*) FROM system_schema.tables WHERE keyspace_name = ? AND table_name = ?", +// "test_keyspace_creation", "schema_info", +// ).Scan(&count) +// assert.NoError(t, err) +// assert.Equal(t, 1, count, "Schema info table should have been created") + +// session.Close() +// store.Close() + +// // 3. Test schema update +// // Create initial schema +// storeV1 := New(Config{ +// Hosts: []string{connectionURL}, +// Keyspace: "test_schema_update", +// Table: "test_table", +// SchemaVersion: 1, +// SchemaDescription: "Schema v1", +// }) + +// // Verify initial schema +// schemaInfo, err := storeV1.GetSchemaInfo() +// assert.NoError(t, err) +// assert.Equal(t, 1, schemaInfo.Version) +// assert.Equal(t, "Schema v1", schemaInfo.Description) +// createdAt := schemaInfo.CreatedAt + +// // Close and create with higher version +// storeV1.Close() + +// // Create updated schema +// storeV2 := New(Config{ +// Hosts: []string{connectionURL}, +// Keyspace: "test_schema_update", +// Table: "test_table", +// SchemaVersion: 2, +// SchemaDescription: "Schema v2", +// }) + +// // Verify schema was updated +// updatedSchema, err := storeV2.GetSchemaInfo() +// assert.NoError(t, err) +// assert.Equal(t, 2, updatedSchema.Version) +// assert.Equal(t, "Schema v2", updatedSchema.Description) +// assert.Equal(t, createdAt.Format(time.RFC3339), updatedSchema.CreatedAt.Format(time.RFC3339)) +// assert.True(t, updatedSchema.UpdatedAt.After(createdAt)) + +// storeV2.Close() + +// // 4. Test forced schema update with same version +// storeForce := New(Config{ +// Hosts: []string{connectionURL}, +// Keyspace: "test_schema_update", +// Table: "test_table", +// SchemaVersion: 2, // Same version +// SchemaDescription: "Schema v2 forced", +// ForceSchemaUpdate: true, +// }) + +// // Verify schema was updated despite same version +// forcedSchema, err := storeForce.GetSchemaInfo() +// assert.NoError(t, err) +// assert.Equal(t, 2, forcedSchema.Version) +// assert.Equal(t, "Schema v2 forced", forcedSchema.Description) +// assert.True(t, forcedSchema.UpdatedAt.After(updatedSchema.UpdatedAt)) + +// storeForce.Close() + +// // 5. Test reset functionality +// resetStore := New(Config{ +// Hosts: []string{connectionURL}, +// Keyspace: "test_schema_reset", +// Table: "test_table", +// SchemaVersion: 1, +// SchemaDescription: "Initial Schema", +// }) + +// // Add some data +// err = resetStore.Set("key1", []byte("value1"), 0) +// assert.NoError(t, err) + +// // Close and reopen with reset +// resetStore.Close() + +// resetStore = New(Config{ +// Hosts: []string{connectionURL}, +// Keyspace: "test_schema_reset", +// Table: "test_table", +// SchemaVersion: 1, +// SchemaDescription: "Reset Schema", +// Reset: true, +// }) + +// // Check that data is gone +// val, err := resetStore.Get("key1") +// assert.NoError(t, err) +// assert.Nil(t, val, "Key should be gone after reset") + +// resetStore.Close() +// } diff --git a/cassandra/cassandra_test.go b/cassandra/cassandra_test.go new file mode 100644 index 00000000..0ec98712 --- /dev/null +++ b/cassandra/cassandra_test.go @@ -0,0 +1,310 @@ +package cassandra + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/gocql/gocql" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + cassandracontainer "github.com/testcontainers/testcontainers-go/modules/cassandra" +) + +// setupCassandraContainer creates a Cassandra container using the official module +func setupCassandraContainer(ctx context.Context) (*cassandracontainer.CassandraContainer, string, error) { + cassandraContainer, err := cassandracontainer.Run(ctx, "cassandra:4.1.3") + if err != nil { + return nil, "", err + } + + // Get connection parameters + host, err := cassandraContainer.Host(ctx) + if err != nil { + return nil, "", err + } + + mappedPort, err := cassandraContainer.MappedPort(ctx, "9042/tcp") + if err != nil { + return nil, "", err + } + + connectionURL := host + ":" + mappedPort.Port() + return cassandraContainer, connectionURL, nil +} + +func TestCassandraStorage(t *testing.T) { + ctx := context.Background() + + // Start Cassandra container + cassandraContainer, connectionURL, err := setupCassandraContainer(ctx) + if err != nil { + t.Fatalf("Failed to start Cassandra container: %v", err) + } + defer func() { + if err := cassandraContainer.Terminate(ctx); err != nil { + t.Logf("Failed to terminate container: %v", err) + } + }() + + // Test cases + t.Run("KeyspaceCreation", func(t *testing.T) { + t.Skip("Skipping keyspace creation test") + testKeyspaceCreation(t, connectionURL) + }) + + t.Run("BasicOperations", func(t *testing.T) { + testBasicOperations(t, connectionURL) + }) + + t.Run("ExpirableKeys", func(t *testing.T) { + testExpirableKeys(t, connectionURL) + }) + + t.Run("Reset", func(t *testing.T) { + testReset(t, connectionURL) + }) + + t.Run("ConcurrentAccess", func(t *testing.T) { + testConcurrentAccess(t, connectionURL) + }) +} + +func testKeyspaceCreation(t *testing.T, connectionURL string) { + // Create new storage + store := New(Config{ + Hosts: []string{connectionURL}, + Keyspace: "test_keyspace_creation", + Table: "test_kv", + }) + defer store.Close() + + // Verify keyspace was created + systemCluster := gocql.NewCluster(connectionURL) + systemSession, err := systemCluster.CreateSession() + require.NoError(t, err) + defer systemSession.Close() + + var count int + err = systemSession.Query( + "SELECT COUNT(*) FROM system_schema.keyspaces WHERE keyspace_name = ?", + "test_keyspace_creation", + ).Scan(&count) + require.NoError(t, err) + require.Equal(t, 1, count, "Keyspace should have been created") + + // Verify table was created + cluster := gocql.NewCluster(connectionURL) + cluster.Keyspace = "test_keyspace_creation" + session, err := cluster.CreateSession() + require.NoError(t, err) + defer session.Close() + + err = session.Query( + "SELECT COUNT(*) FROM system_schema.tables WHERE keyspace_name = ? AND table_name = ?", + "test_keyspace_creation", "test_kv", + ).Scan(&count) + require.NoError(t, err) + require.Equal(t, 1, count, "Table should have been created") +} + +func testBasicOperations(t *testing.T, connectionURL string) { + // Create new storage + store := New(Config{ + Hosts: []string{connectionURL}, + Keyspace: "test_basic_ops", + Table: "test_kv", + }) + defer store.Close() + + // Set a key + err := store.Set("test_key", []byte("test_value"), 0) + require.NoError(t, err) + + // Get the key + value, err := store.Get("test_key") + require.NoError(t, err) + require.Equal(t, []byte("test_value"), value) + + // Get a non-existent key + value, err = store.Get("nonexistent_key") + require.NoError(t, err) + require.Nil(t, value) + + // Delete the key + err = store.Delete("test_key") + require.NoError(t, err) + + // Get the deleted key + value, err = store.Get("test_key") + require.NoError(t, err) + require.Nil(t, value) +} + +// testExpirableKeys tests the expirable keys functionality. +func testExpirableKeys(t *testing.T, connectionURL string) { + // Create new storage with default expiration + store := New(Config{ + Hosts: []string{connectionURL}, + Keyspace: "test_expirable", + Table: "test_kv", + Expiration: 5 * time.Second, // Short default TTL for testing + }) + defer store.Close() + + // Set keys with different expiration settings + // Key with default TTL (exp = 0 means use default) + err := store.Set("key_default_ttl", []byte("value1"), 0) + require.NoError(t, err) + + // Key with specific TTL + err = store.Set("key_specific_ttl", []byte("value2"), 1*time.Second) + require.NoError(t, err) + + // Key with no TTL (overrides default) + err = store.Set("key_no_ttl", []byte("value3"), -1) + require.NoError(t, err) + + // Verify all keys exist initially + value, err := store.Get("key_default_ttl") + require.NoError(t, err) + assert.Equal(t, []byte("value1"), value) + + value, err = store.Get("key_specific_ttl") + require.NoError(t, err) + assert.Equal(t, []byte("value2"), value) + + value, err = store.Get("key_no_ttl") + require.NoError(t, err) + assert.Equal(t, []byte("value3"), value) + + // Wait for specific TTL to expire + time.Sleep(1500 * time.Millisecond) + + // Specific TTL key should be gone, others should remain + value, err = store.Get("key_specific_ttl") + require.NoError(t, err) + assert.Nil(t, value, "Key with 1s TTL should have expired") + + value, err = store.Get("key_default_ttl") + require.NoError(t, err) + assert.Equal(t, []byte("value1"), value, "Key with default TTL should still exist") + + value, err = store.Get("key_no_ttl") + require.NoError(t, err) + assert.Equal(t, []byte("value3"), value, "Key with no TTL should still exist") + + // Wait for default TTL to expire + time.Sleep(4 * time.Second) + + // Default TTL key should be gone, no TTL key should remain + value, err = store.Get("key_default_ttl") + require.NoError(t, err) + assert.Nil(t, value, "Key with default TTL should have expired") + + value, err = store.Get("key_no_ttl") + require.NoError(t, err) + assert.Equal(t, []byte("value3"), value, "Key with no TTL should still exist") +} + +func testReset(t *testing.T, connectionURL string) { + // Create new storage + store := New(Config{ + Hosts: []string{connectionURL}, + Keyspace: "test_reset", + Table: "test_kv", + }) + + // Set some keys + err := store.Set("key1", []byte("value1"), 0) + require.NoError(t, err) + + err = store.Set("key2", []byte("value2"), 0) + require.NoError(t, err) + + // Verify keys exist + value, err := store.Get("key1") + require.NoError(t, err) + require.Equal(t, []byte("value1"), value) + + // Reset storage + err = store.Reset() + require.NoError(t, err) + + // Verify keys are gone + value, err = store.Get("key1") + require.NoError(t, err) + require.Nil(t, value, "Key should be deleted after reset") + + value, err = store.Get("key2") + require.NoError(t, err) + require.Nil(t, value, "Key should be deleted after reset") + + // Close the first storage + store.Close() + + // Create new storage with Reset flag + store = New(Config{ + Hosts: []string{connectionURL}, + Keyspace: "test_reset", + Table: "test_kv", + Reset: true, + }) + defer store.Close() + + // Set a key + err = store.Set("key3", []byte("value3"), 0) + require.NoError(t, err) + + // Verify key exists + value, err = store.Get("key3") + require.NoError(t, err) + require.Equal(t, []byte("value3"), value) +} + +func testConcurrentAccess(t *testing.T, connectionURL string) { + // Create new storage + store := New(Config{ + Hosts: []string{connectionURL}, + Keyspace: "test_concurrent", + Table: "test_kv", + }) + defer store.Close() + + // Number of goroutines + const concurrentOps = 10 + done := make(chan bool, concurrentOps) + + // Run concurrent operations + for i := 0; i < concurrentOps; i++ { + go func(id int) { + // Set key + key := fmt.Sprintf("key%d", id) + value := []byte(fmt.Sprintf("value%d", id)) + err := store.Set(key, value, 0) + require.NoError(t, err) + + // Get key + retrievedValue, err := store.Get(key) + require.NoError(t, err) + require.Equal(t, value, retrievedValue) + + // Delete key + err = store.Delete(key) + require.NoError(t, err) + + // Verify deletion + retrievedValue, err = store.Get(key) + require.NoError(t, err) + require.Nil(t, retrievedValue) + + done <- true + }(i) + } + + // Wait for all goroutines to complete + for i := 0; i < concurrentOps; i++ { + <-done + } +} diff --git a/cassandra/config.go b/cassandra/config.go new file mode 100644 index 00000000..3c49b008 --- /dev/null +++ b/cassandra/config.go @@ -0,0 +1,73 @@ +package cassandra + +import ( + "time" + + "github.com/gocql/gocql" +) + +// Config defines the configuration options for the Cassandra storage +type Config struct { + // Optional. Default is localhost + // Hosts is a list of Cassandra nodes to connect to. + Hosts []string + // Optional. Default is gofiber + // Keyspace is the name of the Cassandra keyspace to use. + Keyspace string + // Optional. Default is kv_store + /// Table is the name of the Cassandra table to use. + Table string + // Optional. Default is Quorum + // Consistency is the Cassandra consistency level. + Consistency gocql.Consistency + // Optional. Default is 10 minutes + // Expiration is the time after which an entry is considered expired. + Expiration time.Duration + // Optional. Default is false + // Reset is a flag to reset the database. + Reset bool +} + +// ConfigDefault is the default config +var ConfigDefault = Config{ + Hosts: []string{"localhost:9042"}, + Keyspace: "gofiber", + Table: "kv_store", + Consistency: gocql.Quorum, + Reset: false, + Expiration: 10 * time.Minute, +} + +// ConfigDefault is the default config +func configDefault(config ...Config) Config { + // Return default config if nothing provided + if len(config) < 1 { + return ConfigDefault + } + + // Override default config + cfg := config[0] + + // Set default values + if len(cfg.Hosts) == 0 { + cfg.Hosts = ConfigDefault.Hosts + } + + if cfg.Keyspace == "" { + cfg.Keyspace = ConfigDefault.Keyspace + } + + if cfg.Table == "" { + cfg.Table = ConfigDefault.Table + } + + if cfg.Consistency == 0 { + cfg.Consistency = ConfigDefault.Consistency + } + + if cfg.Expiration == 0 { + cfg.Expiration = ConfigDefault.Expiration + } + + return cfg +} diff --git a/cassandra/go.mod b/cassandra/go.mod new file mode 100644 index 00000000..557997c5 --- /dev/null +++ b/cassandra/go.mod @@ -0,0 +1,67 @@ +module github.com/gofiber/storage/cassandra/v2 + +go 1.23.0 + +require ( + github.com/gocql/gocql v1.7.0 + github.com/stretchr/testify v1.10.0 + github.com/testcontainers/testcontainers-go/modules/cassandra v0.36.0 +) + +require ( + dario.cat/mergo v1.0.1 // indirect + github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/cenkalti/backoff/v4 v4.2.1 // indirect + github.com/containerd/log v0.1.0 // indirect + github.com/containerd/platforms v0.2.1 // indirect + github.com/cpuguy83/dockercfg v0.3.2 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/distribution/reference v0.6.0 // indirect + github.com/docker/docker v28.0.1+incompatible // indirect + github.com/docker/go-connections v0.5.0 // indirect + github.com/docker/go-units v0.5.0 // indirect + github.com/ebitengine/purego v0.8.2 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-ole/go-ole v1.2.6 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/snappy v0.0.3 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect + github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed // indirect + github.com/klauspost/compress v1.17.4 // indirect + github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect + github.com/magiconair/properties v1.8.9 // indirect + github.com/moby/docker-image-spec v1.3.1 // indirect + github.com/moby/patternmatcher v0.6.0 // indirect + github.com/moby/sys/sequential v0.5.0 // indirect + github.com/moby/sys/user v0.1.0 // indirect + github.com/moby/sys/userns v0.1.0 // indirect + github.com/moby/term v0.5.0 // indirect + github.com/morikuni/aec v1.0.0 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.1 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect + github.com/shirou/gopsutil/v4 v4.25.1 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect + github.com/testcontainers/testcontainers-go v0.36.0 // indirect + github.com/tklauser/go-sysconf v0.3.12 // indirect + github.com/tklauser/numcpus v0.6.1 // indirect + github.com/yusufpapurcu/wmi v1.2.4 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect + go.opentelemetry.io/otel v1.35.0 // indirect + go.opentelemetry.io/otel/metric v1.35.0 // indirect + go.opentelemetry.io/otel/sdk v1.32.0 // indirect + go.opentelemetry.io/otel/trace v1.35.0 // indirect + golang.org/x/crypto v0.35.0 // indirect + golang.org/x/sys v0.31.0 // indirect + google.golang.org/grpc v1.70.0 // indirect + google.golang.org/protobuf v1.36.5 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/cassandra/go.sum b/cassandra/go.sum new file mode 100644 index 00000000..287fe403 --- /dev/null +++ b/cassandra/go.sum @@ -0,0 +1,202 @@ +dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= +dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932 h1:mXoPYz/Ul5HYEDvkta6I8/rnYM5gSdSV2tJ6XbZuEtY= +github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932/go.mod h1:NOuUCSz6Q9T7+igc/hlvDOUdtWKryOrtFyIVABv/p7k= +github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY= +github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= +github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= +github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= +github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= +github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= +github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= +github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= +github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +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/docker v28.0.1+incompatible h1:FCHjSRdXhNRFjlHMTv4jUNlIBbTeRjrWfeFuJp7jpo0= +github.com/docker/docker v28.0.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= +github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/ebitengine/purego v0.8.2 h1:jPPGWs2sZ1UgOSgD2bClL0MJIqu58nOmIcBuXr62z1I= +github.com/ebitengine/purego v0.8.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/gocql/gocql v1.7.0 h1:O+7U7/1gSN7QTEAaMEsJc1Oq2QHXvCWoF3DFK9HDHus= +github.com/gocql/gocql v1.7.0/go.mod h1:vnlvXyFZeLBF0Wy+RS8hrOdbn0UWsWtdg07XJnFxZ+4= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/snappy v0.0.3 h1:fHPg5GQYlCeLIPB9BZqMVR5nR9A+IM5zcgeTdjMYmLA= +github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI= +github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed h1:5upAirOpQc1Q53c0bnx2ufif5kANL7bfZWcc6VJWJd8= +github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4= +github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= +github.com/magiconair/properties v1.8.9 h1:nWcCbLq1N2v/cpNsy5WvQ37Fb+YElfq20WJ/a8RkpQM= +github.com/magiconair/properties v1.8.9/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= +github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= +github.com/moby/sys/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5lXtc= +github.com/moby/sys/sequential v0.5.0/go.mod h1:tH2cOOs5V9MlPiXcQzRC+eEyab644PWKGRYaaV5ZZlo= +github.com/moby/sys/user v0.1.0 h1:WmZ93f5Ux6het5iituh9x2zAG7NFY9Aqi49jjE1PaQg= +github.com/moby/sys/user v0.1.0/go.mod h1:fKJhFOnsCN6xZ5gSfbM6zaHGgDJMrqt9/reuj4T7MmU= +github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g= +github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= +github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= +github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= +github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/shirou/gopsutil/v4 v4.25.1 h1:QSWkTc+fu9LTAWfkZwZ6j8MSUk4A2LV7rbH0ZqmLjXs= +github.com/shirou/gopsutil/v4 v4.25.1/go.mod h1:RoUCUpndaJFtT+2zsZzzmhvbfGoDCJ7nFXKJf8GqJbI= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/testcontainers/testcontainers-go v0.36.0 h1:YpffyLuHtdp5EUsI5mT4sRw8GZhO/5ozyDT1xWGXt00= +github.com/testcontainers/testcontainers-go v0.36.0/go.mod h1:yk73GVJ0KUZIHUtFna6MO7QS144qYpoY8lEEtU9Hed0= +github.com/testcontainers/testcontainers-go/modules/cassandra v0.36.0 h1:vIOOfizBKSHfVcs+5u/VS6Zn4Bbo1lYM28DhHYJm4i8= +github.com/testcontainers/testcontainers-go/modules/cassandra v0.36.0/go.mod h1:ZsSC3MYjRLXLccXMnBth8Qh4AkS2HWzGobVhMMY3Z/k= +github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= +github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= +github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= +github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw= +go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= +go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 h1:Mne5On7VWdx7omSrSSZvM4Kw7cS7NQkOOmLcgscI51U= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0/go.mod h1:IPtUMKL4O3tH5y+iXVyAXqpAwMuzC1IrxVS81rummfE= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU= +go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= +go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= +go.opentelemetry.io/otel/sdk v1.32.0 h1:RNxepc9vK59A8XsgZQouW8ue8Gkb4jpWtJm9ge5lEG4= +go.opentelemetry.io/otel/sdk v1.32.0/go.mod h1:LqgegDBjKMmb2GC6/PrTnteJG39I8/vJCAP9LlJXEjU= +go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= +go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= +go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= +go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs= +golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.36.0 h1:vWF2fRbw4qslQsQzgFqZff+BItCvGFQqKzKIzx1rmoA= +golang.org/x/net v0.36.0/go.mod h1:bFmbeoIPfrw4sMHNhb4J9f6+tPziuGjq7Jk/38fxi1I= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU= +golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= +golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= +golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44= +golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb h1:p31xT4yrYrSM/G4Sn2+TNUkVhFCbG9y8itM2S6Th950= +google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:jbe3Bkdp+Dh2IrslsFCklNhweNTBgSYanP1UXhJDhKg= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb h1:TLPQVbx1GJ8VKZxz52VAxl1EBgKXXbTiU9Fc5fZeLn4= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I= +google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ= +google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw= +google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= +google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= +gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU=