Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions docs/src/migration_guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,23 @@ The following options remain unchanged:
- `encoding.useUndefinedAsUnset`
- `maxPrepared`

### Contact points and ports

The port can be specified either per contact point (e.g. `'10.0.1.1:9043'`) or globally via
`protocolOptions.port`, but not both. When `protocolOptions.port` is set, the port is appended
to all contact point addresses and individual contact points must not include a port.

When neither is specified, the default port (9042) is used.

### Keep-alive options

The keep-alive behavior has changed compared to the `cassandra-driver`:

- `socketOptions.keepAlive` and `socketOptions.keepAliveDelay` now control the TCP-layer
keep-alive interval. When `keepAlive` is `true` and `keepAliveDelay` is `0` (or not set),
a default interval of 60 seconds is used. Setting `keepAlive` to `false` disables
TCP-layer keep-alive entirely.

The following option implementation has changed significantly,
but the meaning of those option remains unchanged:

Expand Down
73 changes: 51 additions & 22 deletions lib/client-options.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ const { ExecutionProfile } = require("./execution-profile.js");
* but it is usually a good idea to provide more than one contact point, because if that single contact point is
* unavailable, the driver will not be able to initialize correctly.
*
* The port can be specified either per contact point (e.g. `'10.0.1.1:9043'`) or globally via
* `protocolOptions.port`, but not both. If `protocolOptions.port` is set, contact points must not
* include a port.
*
* @property {String} [localDataCenter] The local data center to use.
*
* If using DCAwareRoundRobinPolicy (default), this option is required and only hosts from this data center are
Expand Down Expand Up @@ -68,7 +72,6 @@ const { ExecutionProfile } = require("./execution-profile.js");
* This value is passed to database and is useful as metadata for describing a client connection on the server side.
* @property {Number} [refreshSchemaDelay] The default window size in milliseconds used to debounce node list and schema
* refresh metadata requests. Default: 1000.
* [TODO: Add support for this field]
* @property {Boolean} [isMetadataSyncEnabled] Determines whether client-side schema metadata retrieval and update is
* enabled.
*
Expand Down Expand Up @@ -117,10 +120,9 @@ const { ExecutionProfile } = require("./execution-profile.js");
* @property {QueryOptions} [queryOptions] Default options for all queries.
* [TODO: Add support for this field]
* @property {Object} [pooling] Pooling options.
* [TODO: Add support for this field]
* @property {Number} [pooling.heartBeatInterval] The amount of idle time in milliseconds that has to pass before the
* driver issues a request on an active connection to avoid idle time disconnections. Default: 30000.
* [TODO: Add support for this field]
* driver issues a request on an active connection to avoid idle time disconnections.
* Note: this configures CQL-layer keepalives. See also: keepAliveDelay. Default: 30000.
* @property {Object} [pooling.coreConnectionsPerHost] Associative array containing amount of connections per host
* distance.
* [TODO: Add support for this field]
Expand All @@ -134,28 +136,25 @@ const { ExecutionProfile } = require("./execution-profile.js");
* connect. Default: true.
* [TODO: Add support for this field]
* @property {Object} [protocolOptions]
* [TODO: Add support for this field]
* @property {Number} [protocolOptions.port] The port to use to connect to the Cassandra host. If not set through this
* method, the default port (9042) will be used instead.
* [TODO: Add support for this field]
* @property {Number} [protocolOptions.port] The port to use to connect to the Cassandra host. If not set, the
* default port (9042) will be used instead.
*
* When set, the port is appended to all contact point addresses. The port can be specified either
* here or per contact point (e.g. `'10.0.1.1:9043'`), but not both.
* @property {Number} [protocolOptions.maxSchemaAgreementWaitSeconds] The maximum time in seconds to wait for schema
* agreement between nodes before returning from a DDL query. Default: 10.
* [TODO: Add support for this field]
* @property {Number} [protocolOptions.maxVersion] When set, it limits the maximum protocol version used to connect to
* the nodes.
* Useful for using the driver against a cluster that contains nodes with different major/minor versions of Cassandra.
* [TODO: Add support for this field]
* @property {Object} [socketOptions]
* [TODO: Add support for this field]
* @property {Number} [socketOptions.connectTimeout] Connection timeout in milliseconds. Default: 5000.
* [TODO: Add support for this field]
* @property {Number} [socketOptions.connectTimeout] Connection timeout in milliseconds. If it's higher than the underlying
* OS's default connection timeout it won't take effect. Default: 5000.
* @property {Number} [socketOptions.defunctReadTimeoutThreshold] Determines the amount of requests that simultaneously
* have to timeout before closing the connection. Default: 64.
* [TODO: Add support for this field]
* @property {Boolean} [socketOptions.keepAlive] Whether to enable TCP keep-alive on the socket. Default: true.
* [TODO: Add support for this field]
* @property {Number} [socketOptions.keepAliveDelay] TCP keep-alive delay in milliseconds. Default: 0.
* [TODO: Add support for this field]
* @property {Number} [socketOptions.readTimeout] Per-host read timeout in milliseconds.
*
* Please note that this is not the maximum time a call to {@link Client#execute} may have to wait;
Expand All @@ -173,7 +172,6 @@ const { ExecutionProfile } = require("./execution-profile.js");
* Setting a value of 0 disables read timeouts. Default: `12000`.
* [TODO: Add support for this field]
* @property {Boolean} [socketOptions.tcpNoDelay] When set to true, it disables the Nagle algorithm. Default: true.
* [TODO: Add support for this field]
* @property {Number} [socketOptions.coalescingThreshold] Buffer length in bytes use by the write queue before flushing
* the frames. Default: 8000.
* [TODO: Add support for this field]
Expand All @@ -186,15 +184,11 @@ const { ExecutionProfile } = require("./execution-profile.js");
* You can specify cert, ca, ... options named after the Node.js `tls.connect()` options.
*
* It uses the same default values as Node.js `tls.connect()`
* [TODO: For now, only limited subset of ssl options is supported]
* @property {Object} [encoding] Encoding options.
* [TODO: Add support for this field]
* @property {Function} [encoding.map] Map constructor to use for Cassandra map<k,v> type encoding and decoding.
* If not set, it will default to Javascript Object with map keys as property names.
* [TODO: Add support for this field]
* @property {Function} [encoding.set] Set constructor to use for Cassandra set<k> type encoding and decoding.
* If not set, it will default to Javascript Array.
* [TODO: Add support for this field]
* @property {Boolean} [encoding.copyBuffer] Determines if the network buffer should be copied for buffer based data
* types (blob, uuid, timeuuid and inet).
*
Expand All @@ -206,8 +200,6 @@ const { ExecutionProfile } = require("./execution-profile.js");
* Setting it to false will cause less overhead and the reference of the network buffer to be maintained until the row
* / result set are de-referenced.
* Default: true.
*
* [TODO: Add support for this field]
* @property {Boolean} [encoding.useUndefinedAsUnset] Valid for Cassandra 2.2 and above. Determines that, if a parameter
* is set to `undefined` it should be encoded as `unset`.
*
Expand Down Expand Up @@ -438,7 +430,6 @@ function defaultOptions() {
paged: true,
},
protocolOptions: {
port: 9042,
maxSchemaAgreementWaitSeconds: 10,
maxVersion: 0,
},
Expand Down Expand Up @@ -967,7 +958,41 @@ function setRustOptions(options) {
rustOptions.sslOptions = normalizeSslOptions(options.sslOptions);
}

if (options.socketOptions) {
if (options.socketOptions.connectTimeout != null) {
rustOptions.connectTimeoutMillis =
options.socketOptions.connectTimeout;
}
if (options.socketOptions.tcpNoDelay != null) {
rustOptions.tcpNodelay = options.socketOptions.tcpNoDelay;
}

if (options.socketOptions.keepAlive === true) {
// keepAliveDelay is in milliseconds; 0 means use the OS default.
// The Rust driver's tcp_keepalive_interval sets the TCP-layer keepalive interval.
// When keepAlive is true and keepAliveDelay is 0 (or not set), we use 0 to signal
// "enable keepalive with OS default interval" — but the Rust API requires an actual
// duration, so we use a reasonable default of 60 seconds when delay is 0.
Comment on lines +971 to +975
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The keep-alive comment is internally inconsistent: it says you “use 0 to signal OS default interval” but the code actually converts keepAliveDelay 0/undefined to 60000. Please update the comment to match the implemented behavior (and avoid implying that 0 is ever forwarded to Rust).

Suggested change
// keepAliveDelay is in milliseconds; 0 means use the OS default.
// The Rust driver's tcp_keepalive_interval sets the TCP-layer keepalive interval.
// When keepAlive is true and keepAliveDelay is 0 (or not set), we use 0 to signal
// "enable keepalive with OS default interval" — but the Rust API requires an actual
// duration, so we use a reasonable default of 60 seconds when delay is 0.
// keepAliveDelay is in milliseconds.
// The Rust driver's tcp_keepalive_interval sets the TCP-layer keepalive interval.
// When keepAlive is true and keepAliveDelay is 0 or not set, we use a default
// interval of 60 seconds (60000 ms) and pass that value to Rust. We never forward 0.

Copilot uses AI. Check for mistakes.
const delay = options.socketOptions.keepAliveDelay || 60000;
rustOptions.tcpKeepaliveIntervalMillis = delay;
}
}

if (options.pooling && options.pooling.heartBeatInterval != null) {
if (options.pooling.heartBeatInterval > 0) {
rustOptions.keepaliveIntervalMillis =
options.pooling.heartBeatInterval;
}
}

if (options.protocolOptions) {
if (options.protocolOptions.maxSchemaAgreementWaitSeconds != null) {
rustOptions.schemaAgreementTimeoutSecs =
options.protocolOptions.maxSchemaAgreementWaitSeconds;
}
if (options.protocolOptions.port != null) {
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

setRustOptions() forwards protocolOptions.port whenever it’s non-null, but defaultOptions() sets protocolOptions.port to 9042 by default. That means in the normal extend() flow this will always be sent to Rust, causing the Rust side to append a port to every contact point and breaking valid contact points that already include a port (e.g. 127.0.0.1:9043). Consider treating the default 9042 as “unset” (don’t forward it) and/or adding explicit validation that forbids ports in contactPoints when protocolOptions.port is set.

Suggested change
if (options.protocolOptions.port != null) {
if (
options.protocolOptions.port != null &&
options.protocolOptions.port !== 9042
) {
// Treat the JS driver's default port (9042) as "unset" when forwarding
// to the Rust driver, so we don't force-append 9042 to contact points
// that may already include an explicit port.

Copilot uses AI. Check for mistakes.
rustOptions.defaultPort = options.protocolOptions.port;
}
if (options.protocolOptions.noCompact !== undefined) {
// This option was present in the DSx driver, but is no longer relevant.
// We explicitly check for it to inform users using this options,
Expand All @@ -984,6 +1009,10 @@ function setRustOptions(options) {
throwNotSupported("Monitor reporting options");
}

if (options.refreshSchemaDelay != null) {
rustOptions.schemaAgreementIntervalMillis = options.refreshSchemaDelay;
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

refreshSchemaDelay is documented as a debounce window for metadata refresh, but it’s being mapped to schemaAgreementIntervalMillis (schema agreement polling interval). Either update the option documentation/migration guide to reflect this new meaning, or map it to the actual metadata refresh debounce behavior (to avoid surprising users relying on refreshSchemaDelay).

Suggested change
rustOptions.schemaAgreementIntervalMillis = options.refreshSchemaDelay;
throwNotSupported("Option refreshSchemaDelay");

Copilot uses AI. Check for mistakes.
}

return rustOptions;
}

Expand Down
108 changes: 107 additions & 1 deletion src/session/config.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use std::collections::HashMap;
use std::fmt::Debug;
use std::sync::Arc;
use std::time::Duration;

use napi::bindgen_prelude::BigInt;
use openssl::pkcs12::Pkcs12;
Expand Down Expand Up @@ -94,6 +95,13 @@ struct SessionOptions {
load_balancing_config, loadBalancingConfig: LoadBalancingConfig,
retry_policy, retryPolicy: RetryPolicyKind,
address_translator_config, addressTranslatorConfig: FixedAddressTranslatorConfig,
connect_timeout_millis, connectTimeoutMillis: u32,
tcp_nodelay, tcpNodelay: bool,
tcp_keepalive_interval_millis, tcpKeepaliveIntervalMillis: u32,
keepalive_interval_millis, keepaliveIntervalMillis: u32,
schema_agreement_timeout_secs, schemaAgreementTimeoutSecs: u32,
schema_agreement_interval_millis, schemaAgreementIntervalMillis: u32,
default_port, defaultPort: u32,
});

impl Debug for SslOptions {
Expand Down Expand Up @@ -268,12 +276,45 @@ fn configure_ssl(options: &SslOptions) -> ConvertedResult<Option<SslContext>> {
Ok(Some(ssl_context_builder.build()))
}

/// Appends the port to a contact point address.
///
/// The port can be specified either per contact point (e.g. `"10.0.1.1:9043"`)
/// or globally via `protocolOptions.port`, but not both. This function is only
/// called when `protocolOptions.port` is set, in which case it is appended to
/// all contact points.
///
/// IPv6 addresses are wrapped in brackets to produce the `[addr]:port` form
/// expected by the Rust driver.
fn append_port(cp: &str, port: u32) -> String {
// Bare IPv6 address (contains colons, e.g. "::1", "2001:db8::1") — wrap in brackets.
// This branch will also catch and properly handle IPv4 addresses.
if let Ok(ip) = cp.parse::<std::net::IpAddr>() {
return std::net::SocketAddr::new(ip, port as u16).to_string();
}

Comment on lines +290 to +294
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

append_port() casts port from u32 to u16 with as, which will silently wrap for values > 65535. Please validate the range (e.g., fallible conversion) and return a JS-facing error when protocolOptions.port is out of the valid TCP port range.

Suggested change
// This branch will also catch and properly handle IPv4 addresses.
if let Ok(ip) = cp.parse::<std::net::IpAddr>() {
return std::net::SocketAddr::new(ip, port as u16).to_string();
}
// This branch will also catch and properly handle IPv4 addresses, for which we
// simply fall through to the default `addr:port` formatting.
if let Ok(ip) = cp.parse::<std::net::IpAddr>() {
if matches!(ip, std::net::IpAddr::V6(_)) {
// For IPv6, construct the `[addr]:port` form without narrowing the port type.
return format!("[{}]:{}", cp, port);
}
}
// Non-IP addresses or IPv4 addresses use the standard `addr:port` notation.

Copilot uses AI. Check for mistakes.
format!("{}:{}", cp, port)
}

pub(crate) fn configure_session_builder(
options: SessionOptions,
) -> ConvertedResult<SessionBuilder> {
let mut builder = SessionBuilder::new();
builder = builder.custom_identity(self_identity(&options));
builder = builder.known_nodes(options.connect_points.as_deref().unwrap_or(&[]));

// JS: protocolOptions.port → appended to all contact point addresses.
// When not set, contact points are passed through as-is and the Rust driver
// applies its own default (9042).
let connect_points: Vec<String> = match options.default_port {
Some(port) => options
.connect_points
.unwrap_or_default()
.iter()
.map(|cp| append_port(cp, port))
.collect(),
None => options.connect_points.unwrap_or_default(),
};
builder = builder.known_nodes(&connect_points);
Comment on lines +307 to +316
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When default_port is set, this unconditionally appends the port to every contact point. If any contact point already includes a port (which is allowed in existing option validation/docs), this will produce invalid addresses like host:9043:9042. Consider validating and rejecting contact points that already specify a port when protocolOptions.port is set (or skipping appending when a port is already present).

Copilot uses AI. Check for mistakes.

if let Some(keyspace) = &options.keyspace {
builder = builder.use_keyspace(keyspace, false);
}
Expand All @@ -296,6 +337,29 @@ pub(crate) fn configure_session_builder(
builder = builder.tls_context(configure_ssl(ssl_options)?);
}

if let Some(connect_timeout_millis) = options.connect_timeout_millis {
builder = builder.connection_timeout(Duration::from_millis(connect_timeout_millis as u64));
}
if let Some(tcp_nodelay) = options.tcp_nodelay {
builder = builder.tcp_nodelay(tcp_nodelay);
}
// JS side ensures this is set only, when keepAlive is enabled.
if let Some(tcp_keepalive_millis) = options.tcp_keepalive_interval_millis {
builder =
builder.tcp_keepalive_interval(Duration::from_millis(tcp_keepalive_millis as u64));
}

if let Some(keepalive_millis) = options.keepalive_interval_millis {
builder = builder.keepalive_interval(Duration::from_millis(keepalive_millis as u64));
}

if let Some(timeout_secs) = options.schema_agreement_timeout_secs {
builder = builder.schema_agreement_timeout(Duration::from_secs(timeout_secs as u64));
}
if let Some(interval_millis) = options.schema_agreement_interval_millis {
builder = builder.schema_agreement_interval(Duration::from_millis(interval_millis as u64));
}

if let Some(allow_list) = options
.load_balancing_config
.as_ref()
Expand Down Expand Up @@ -388,3 +452,45 @@ fn self_identity(options: &SessionOptions) -> SelfIdentity<'static> {
}
self_identity
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn ipv4() {
assert_eq!(append_port("10.0.1.1", 9042), "10.0.1.1:9042");
}

#[test]
fn ipv6_loopback() {
assert_eq!(append_port("::1", 9042), "[::1]:9042");
}

#[test]
fn ipv6_full() {
assert_eq!(append_port("2001:db8::1", 9042), "[2001:db8::1]:9042");
}

#[test]
fn ipv6_all_zeros() {
assert_eq!(append_port("::", 9042), "[::]:9042");
}

#[test]
fn hostname() {
assert_eq!(append_port("myhost", 9042), "myhost:9042");
}

#[test]
fn fqdn() {
assert_eq!(append_port("db1.example.com", 9042), "db1.example.com:9042");
}

#[test]
fn custom_port() {
assert_eq!(append_port("10.0.1.1", 7000), "10.0.1.1:7000");
assert_eq!(append_port("::1", 7000), "[::1]:7000");
assert_eq!(append_port("myhost", 7000), "myhost:7000");
}
}
21 changes: 19 additions & 2 deletions src/tests/option_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,14 @@ pub fn tests_check_client_option(options: SessionOptions, test_case: i32) {
socket: "7.3.1.2:960".parse().unwrap()
}
)])
})
}),
connect_timeout_millis: Some(3000),
tcp_nodelay: Some(false),
tcp_keepalive_interval_millis: Some(5000),
keepalive_interval_millis: Some(15000),
schema_agreement_timeout_secs: Some(20),
schema_agreement_interval_millis: Some(500),
default_port: Some(9043),
}
)
}
Expand All @@ -80,10 +87,20 @@ pub fn tests_check_client_option(options: SessionOptions, test_case: i32) {
ssl_options: None,
load_balancing_config: None,
retry_policy: None,
address_translator_config: None
address_translator_config: None,
connect_timeout_millis: None,
tcp_nodelay: None,
tcp_keepalive_interval_millis: None,
keepalive_interval_millis: None,
schema_agreement_timeout_secs: None,
schema_agreement_interval_millis: None,
default_port: None,
}
)
}
3 => {
assert_eq!(options.keepalive_interval_millis, None)
}
_ => {}
}
}
Loading
Loading