diff --git a/.gitignore b/.gitignore index dd4f44c..5125374 100644 --- a/.gitignore +++ b/.gitignore @@ -24,4 +24,6 @@ bin/* openspec/ .github/ .claude/ +.opencode/ AGENTS.md +CLAUDE.md diff --git a/config/sample_webconfig.conf b/config/sample_webconfig.conf index 075dc47..249ec43 100644 --- a/config/sample_webconfig.conf +++ b/config/sample_webconfig.conf @@ -173,16 +173,29 @@ webconfig { "127.0.0.1" ] keyspace = "webconfig" - timeout_in_sec = 5 - connect_timeout_in_sec = 5 + timeout_in_sec = 10 + connect_timeout_in_sec = 10 concurrent_queries = 5 - connections = 5 + connections = 2 local_dc = "" page_size = 50 user = "dbuser" test_keyspace = "test_webconfig" is_ssl_enabled = true + // TCP keepalive interval in seconds (default: 30) + socket_keepalive_sec = 30 + + // Exponential reconnection policy settings. + // reconnect_max_retries: number of attempts before giving up (0 = never retry). + reconnect_initial_interval_ms = 2000 + reconnect_max_retries = 10 + reconnect_max_interval_sec = 60 + + // Disable initial host lookup (default: false). + // Set to true when Cassandra hosts are only accessible by IP and DNS is unavailable. + disable_initial_host_lookup = false + // TLS/SSL configuration (optional when is_ssl_enabled = true) // If tls block is not provided or incomplete, will use insecure TLS tls { @@ -196,6 +209,11 @@ webconfig { // Skip certificate verification (INSECURE - for testing only) // When true, allows TLS without certificates or CA validation insecure_skip_verify = false + + // Override SNI hostname sent during TLS handshake. + // Required when the certificate uses DNS SANs only (no IP SANs). + // Leave empty to use the host IP address (gocql default). + server_name = "" } } @@ -205,15 +223,28 @@ webconfig { "192.168.1.114" ] keyspace = "yugabytedb" - timeout_in_sec = 5 - connect_timeout_in_sec = 5 + timeout_in_sec = 10 + connect_timeout_in_sec = 10 concurrent_queries = 5 - connections = 5 + connections = 2 local_dc = "" page_size = 50 user = "dbuser" test_keyspace = "test_yugabytedb" + // TCP keepalive interval in seconds (default: 30) + socket_keepalive_sec = 30 + + // Exponential reconnection policy settings. + // reconnect_max_retries: number of attempts before giving up (0 = never retry). + reconnect_initial_interval_ms = 2000 + reconnect_max_retries = 10 + reconnect_max_interval_sec = 60 + + // Disable initial host lookup (default: false). + // Set to true when Cassandra hosts are only accessible by IP and DNS is unavailable. + disable_initial_host_lookup = false + // TODO change to false for CI/CD is_ssl_enabled = true @@ -230,6 +261,11 @@ webconfig { // Skip certificate verification (INSECURE - for testing only) // When true, allows TLS without certificates or CA validation insecure_skip_verify = false + + // Override SNI hostname sent during TLS handshake. + // Required when the certificate uses DNS SANs only (no IP SANs). + // Leave empty to use the host IP address (gocql default). + server_name = "" } } } diff --git a/db/cassandra/cassandra_client.go b/db/cassandra/cassandra_client.go index 675f388..0145d6e 100644 --- a/db/cassandra/cassandra_client.go +++ b/db/cassandra/cassandra_client.go @@ -36,13 +36,19 @@ import ( ) const ( - ProtocolVersion = 4 - DefaultKeyspace = "webconfig" - DefaultTestKeyspace = "test_webconfig" - DisableInitialHostLookup = false - DefaultSleepTimeInMillisecond = 10 - DefaultConnections = 2 - DefaultPageSize = 50 + ProtocolVersion = 4 + DefaultKeyspace = "webconfig" + DefaultTestKeyspace = "test_webconfig" + DefaultDisableInitialHostLookup = false + DefaultSleepTimeInMillisecond = 10 + DefaultConnections = 2 + DefaultPageSize = 50 + DefaultTimeoutSec = 10 + DefaultConnectTimeoutSec = 10 + DefaultSocketKeepaliveSec = 30 + DefaultReconnectInitialIntervalMs = 2000 + DefaultReconnectMaxRetries = 10 + DefaultReconnectMaxIntervalSec = 60 ) // if 'wifi_schema_v2_enabled'=true, v1.3 is also supported @@ -103,10 +109,11 @@ func NewCassandraClient(conf *configuration.Config, testOnly bool) (*CassandraCl cluster.Consistency = gocql.LocalQuorum cluster.ProtoVersion = ProtocolVersion - cluster.DisableInitialHostLookup = DisableInitialHostLookup - cluster.Timeout = time.Duration(dbconf.GetInt32("timeout_in_sec", 1)) * time.Second - cluster.ConnectTimeout = time.Duration(dbconf.GetInt32("connect_timeout_in_sec", 1)) * time.Second + cluster.DisableInitialHostLookup = dbconf.GetBoolean("disable_initial_host_lookup", DefaultDisableInitialHostLookup) + cluster.Timeout = time.Duration(dbconf.GetInt32("timeout_in_sec", DefaultTimeoutSec)) * time.Second + cluster.ConnectTimeout = time.Duration(dbconf.GetInt32("connect_timeout_in_sec", DefaultConnectTimeoutSec)) * time.Second cluster.NumConns = int(dbconf.GetInt32("connections", DefaultConnections)) + cluster.SocketKeepalive = time.Duration(dbconf.GetInt32("socket_keepalive_sec", DefaultSocketKeepaliveSec)) * time.Second cluster.RetryPolicy = &gocql.DowngradingConsistencyRetryPolicy{ ConsistencyLevelsToTry: []gocql.Consistency{ @@ -116,9 +123,20 @@ func NewCassandraClient(conf *configuration.Config, testOnly bool) (*CassandraCl }, } + reconnectIntervalMs := dbconf.GetInt32("reconnect_initial_interval_ms", DefaultReconnectInitialIntervalMs) + reconnectMaxRetries := int(dbconf.GetInt32("reconnect_max_retries", DefaultReconnectMaxRetries)) + reconnectMaxIntervalSec := dbconf.GetInt32("reconnect_max_interval_sec", DefaultReconnectMaxIntervalSec) + cluster.ReconnectionPolicy = &gocql.ExponentialReconnectionPolicy{ + InitialInterval: time.Duration(reconnectIntervalMs) * time.Millisecond, + MaxRetries: reconnectMaxRetries, + MaxInterval: time.Duration(reconnectMaxIntervalSec) * time.Second, + } + localDc := dbconf.GetString("local_dc") if len(localDc) > 0 { - cluster.PoolConfig.HostSelectionPolicy = gocql.DCAwareRoundRobinPolicy(localDc) + cluster.PoolConfig.HostSelectionPolicy = gocql.TokenAwareHostPolicy( + gocql.DCAwareRoundRobinPolicy(localDc), + ) } var password string @@ -145,14 +163,15 @@ func NewCassandraClient(conf *configuration.Config, testOnly bool) (*CassandraCl } if isSslEnabled { - tlsConfig, err := loadCassandraTLSConfig(dbconf, dbdriver) + insecureSkipVerify := dbconf.GetBoolean("tls.insecure_skip_verify") + tlsConfig, err := loadCassandraTLSConfig(dbconf, dbdriver, insecureSkipVerify) if err != nil { return nil, common.NewError(err) } sslOpts := &gocql.SslOptions{ Config: tlsConfig, - EnableHostVerification: false, + EnableHostVerification: !insecureSkipVerify, } cluster.SslOpts = sslOpts @@ -197,20 +216,19 @@ func NewCassandraClient(conf *configuration.Config, testOnly bool) (*CassandraCl // loadCassandraTLSConfig loads TLS configuration for Cassandra connection. // Returns a tls.Config with certificates loaded from the configuration. // The function expects tls.{} block under the database driver config (cassandra or yugabyte). -func loadCassandraTLSConfig(dbconf *configuration.Config, dbdriver string) (*tls.Config, error) { - // Check insecure_skip_verify flag first - insecureSkipVerify := dbconf.GetBoolean("tls.insecure_skip_verify") - +func loadCassandraTLSConfig(dbconf *configuration.Config, dbdriver string, insecureSkipVerify bool) (*tls.Config, error) { // Load client certificates for mTLS if provided (optional when insecure_skip_verify is true) certFile := dbconf.GetString("tls.cert_file") keyFile := dbconf.GetString("tls.key_file") caCertFile := dbconf.GetString("tls.ca_cert_file") + serverName := dbconf.GetString("tls.server_name") // Create TLS config for Cassandra connection. // Prefer modern ECDHE+AEAD suites for forward secrecy; keep TLS_RSA_WITH_AES_128_CBC_SHA // last as a fallback for legacy Cassandra 3.11.x nodes that only negotiate that suite. tlsConfig := &tls.Config{ MinVersion: tls.VersionTLS12, + ServerName: serverName, CipherSuites: []uint16{ tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,