Skip to content

Behaviors and selectors

For the complete documentation index see: llms.txt

All documentation pages available in markdown.

Behaviors control timeout, retry, and protocol settings per operation type. They replace the old per-policy model with a single, composable configuration.

Core concepts

  • Behavior — an immutable set of operational settings
  • Selectors — target which operation types a setting applies to (reads, writes, batches, etc.)
  • Session — created from a cluster with a specific behavior; all operations on that session use those settings

Creating a session with a behavior

Cluster cluster = new ClusterDefinition("localhost", 3000).connect();
Session defaultSession = cluster.createSession(Behavior.DEFAULT);

📖 API reference: ClusterDefinition(String,int) | ClusterDefinition.connect() | Cluster.createSession(Behavior)

from aerospike_sdk import Behavior, ClusterDefinition
cluster = await ClusterDefinition("localhost", 3000).connect()
default_session = cluster.create_session(Behavior.DEFAULT)

📖 API reference: Behavior.DEFAULT

Deriving behaviors

All custom behaviors derive from an existing one — deriveWithChanges in Java, derive_with_changes in Python:

Behavior production = Behavior.DEFAULT.deriveWithChanges("production", builder -> builder
.on(Selectors.all(), ops -> ops
.abandonCallAfter(Duration.ofSeconds(5))
)
);
Session productionSession = cluster.createSession(production);

📖 API reference: Cluster.createSession(Behavior)

from datetime import timedelta
from aerospike_sdk import Behavior
from aerospike_sdk.policy import Settings
production = Behavior.DEFAULT.derive_with_changes(
"production",
all=Settings(total_timeout=timedelta(seconds=5)),
)
production_session = cluster.create_session(production)

📖 API reference: Behavior.DEFAULT

Selectors reference

Selectors narrow which operations a setting applies to. Declare them from general to specific so that more specific selectors override broader ones (see How resolution works).

In Python, each selector is a keyword scope on derive_with_changes() (reads_batch=Settings(...)); scopes are single-axis and cascade the same way.

Entry points

SelectorTargets
Selectors.all()Every operation
Selectors.reads()All read operations
Selectors.writes()All write operations
Selectors.transaction()Transaction verify/roll operations

Narrowing reads

import java.time.Duration;
import com.aerospike.client.sdk.policy.Behavior;
import com.aerospike.client.sdk.policy.Behavior.Selectors;
Behavior readsTuned = Behavior.DEFAULT.deriveWithChanges("reads-tuned", b -> b
.on(Selectors.reads(), ops -> ops.abandonCallAfter(Duration.ofSeconds(2))) // all reads
.on(Selectors.reads().get(), ops -> ops.abandonCallAfter(Duration.ofMillis(500))) // single-key reads
.on(Selectors.reads().batch(), ops -> ops.abandonCallAfter(Duration.ofSeconds(5))) // batch reads
.on(Selectors.reads().query(), ops -> ops.abandonCallAfter(Duration.ofSeconds(30))) // dataset queries (primary-index and secondary-index queries)
);

📖 API reference: com.aerospike.client.sdk

from datetime import timedelta
from aerospike_sdk import Behavior
from aerospike_sdk.policy import Settings
# Python names each selector as a derive_with_changes() keyword argument
# that takes a Settings(...) value:
reads_tuned = Behavior.DEFAULT.derive_with_changes(
"reads-tuned",
reads=Settings(total_timeout=timedelta(seconds=2)), # all reads
reads_point=Settings(total_timeout=timedelta(milliseconds=500)), # single-key reads
reads_batch=Settings(total_timeout=timedelta(seconds=5)), # batch reads
reads_query=Settings(total_timeout=timedelta(seconds=30)), # dataset queries (primary-index and secondary-index queries)
)

📖 API reference: Behavior

Narrowing writes

import java.time.Duration;
import com.aerospike.client.sdk.policy.Behavior;
import com.aerospike.client.sdk.policy.Behavior.Selectors;
Behavior writesTuned = Behavior.DEFAULT.deriveWithChanges("writes-tuned", b -> b
.on(Selectors.writes(), ops -> ops.abandonCallAfter(Duration.ofSeconds(2))) // all writes
.on(Selectors.writes().retryable(), ops -> ops.maximumNumberOfCallAttempts(4)) // idempotent writes (safe to retry)
.on(Selectors.writes().nonRetryable(), ops -> ops.maximumNumberOfCallAttempts(1)) // non-idempotent writes
.on(Selectors.writes().retryable().point(), ops -> ops.sendKey(true)) // single-key retryable writes
.on(Selectors.writes().retryable().batch(), ops -> ops.abandonCallAfter(Duration.ofSeconds(5))) // batch retryable writes
.on(Selectors.writes().nonRetryable().point(), ops -> ops.sendKey(true)) // single-key non-retryable writes
.on(Selectors.writes().nonRetryable().batch(), ops -> ops.abandonCallAfter(Duration.ofSeconds(5))) // batch non-retryable writes
);

📖 API reference: com.aerospike.client.sdk

from datetime import timedelta
from aerospike_sdk import Behavior
from aerospike_sdk.policy import Settings
# Python write scopes narrow by one dimension each. They layer in the order
# writes -> writes_ap / writes_sc -> retryability -> shape, so a single-key
# retryable write picks up writes, writes_retryable, and writes_point.
writes_tuned = Behavior.DEFAULT.derive_with_changes(
"writes-tuned",
writes=Settings(total_timeout=timedelta(seconds=2)), # all writes
writes_retryable=Settings(max_retries=3), # idempotent writes (safe to retry)
writes_non_retryable=Settings(max_retries=0), # non-idempotent writes
writes_point=Settings(send_key=True), # single-key writes
writes_batch=Settings(total_timeout=timedelta(seconds=5)), # batch writes
)

📖 API reference: Behavior

Mode selection (AP vs CP)

Append .ap() or .cp() for mode-specific settings. Select mode last for best compile-time type safety:

import java.time.Duration;
import com.aerospike.client.sdk.policy.Behavior;
import com.aerospike.client.sdk.policy.Behavior.Selectors;
import com.aerospike.client.sdk.policy.CommitLevel;
import com.aerospike.client.sdk.policy.ReadModeAP;
Behavior byMode = Behavior.DEFAULT.deriveWithChanges("by-mode", b -> b
// Recommended: mode last -> full type safety
.on(Selectors.reads().batch().ap(), ops -> ops.readMode(ReadModeAP.ALL)) // exposes readMode()
.on(Selectors.writes().retryable().point().ap(), ops -> ops.commitLevel(CommitLevel.COMMIT_ALL)) // exposes commitLevel()
// Works but loses type-specific methods in IDE
.on(Selectors.writes().ap().retryable().point(), ops -> ops.abandonCallAfter(Duration.ofSeconds(2))) // commitLevel() not visible at compile time
);

📖 API reference: com.aerospike.client.sdk

from aerospike_native import CommitLevel, ReadModeAP, ReadModeSC
from aerospike_sdk import Behavior
from aerospike_sdk.policy import Settings
# Python selects the namespace mode with its own scopes, which apply on top of
# reads / writes and below the shape scopes:
by_mode = Behavior.DEFAULT.derive_with_changes(
"by-mode",
reads_ap=Settings(read_mode_ap=ReadModeAP.ALL), # reads on AP namespaces (read_mode_ap)
reads_sc=Settings(read_mode_sc=ReadModeSC.LINEARIZE), # reads on SC namespaces (read_mode_sc)
writes_ap=Settings(commit_level=CommitLevel.COMMIT_ALL), # writes on AP namespaces (commit_level)
writes_sc=Settings(durable_delete=True), # writes on SC namespaces (durable_delete)
)

📖 API reference: Behavior.DEFAULT

Available settings

Common (available on all selectors)

SettingDescription
abandonCallAfter(Duration)Total timeout for the entire operation (including retries)
waitForCallToComplete(Duration)Socket timeout per individual attempt
waitForConnectionToComplete(Duration)Connection establishment timeout
waitForSocketResponseAfterCallFails(Duration)How long to wait for a socket response after the call has already timed out
maximumNumberOfCallAttempts(int)Max attempts, including the first (Python max_retries counts retries only)
delayBetweenRetries(Duration)Fixed delay between retry attempts
replicaOrder(Replica)Which replica to read/write first (SEQUENCE, MASTER, etc.)
sendKey(boolean)Whether to send the user key to the server
useCompression(boolean)Enable network compression

Batch-specific

Available on .reads().batch(), .writes().*.batch(), and .transaction().*:

SettingDescription
maxConcurrentNodes(int)Max parallel node connections for batch
allowInlineMemoryAccess(boolean)Allow batch sub-commands to run inline on memory-only namespaces
allowInlineSsdAccess(boolean)Allow batch sub-commands to run inline on SSD-backed namespaces

Query-specific

Available on .reads().query() and .writes().*.query():

SettingDescription
recordQueueSize(int)Client-side result buffer size

Read-specific

SettingSelectorDescription
resetTtlOnReadAtPercent(int).reads()Reset record TTL on read when remaining TTL drops below this percentage
readMode(ReadModeAP).reads().ap()AP read consistency (ONE, ALL)
consistency(ReadModeSC).reads().cp()SC read consistency (SESSION, LINEARIZE)

Write-specific

SettingSelectorDescription
useDurableDelete(boolean).writes()Use durable delete for tombstones
simulateXdrWrite(boolean).writes()Simulate XDR (cross-datacenter replication) write
commitLevel(CommitLevel).writes().*.ap()AP write durability (COMMIT_ALL, COMMIT_MASTER)

Transaction operations

Behavior custom = Behavior.DEFAULT.deriveWithChanges("custom", builder -> builder
.on(Selectors.transaction().txnVerify(), ops -> ops
.maximumNumberOfCallAttempts(10)
)
.on(Selectors.transaction().txnRoll(), ops -> ops
.delayBetweenRetries(Duration.ofSeconds(1))
)
);

📖 API reference: com.aerospike.client.sdk

from datetime import timedelta
from aerospike_sdk import Behavior
from aerospike_sdk.policy import Settings
custom = Behavior.DEFAULT.derive_with_changes(
"custom",
system_txn_verify=Settings(max_retries=9), # 10 attempts
system_txn_roll=Settings(retry_delay=timedelta(seconds=1)),
)

📖 API reference: Behavior.DEFAULT

How resolution works

Settings resolve through two independent dimensions: selector cascade within a single behavior, and inheritance across derived behaviors.

Dimension 1: Selector cascade

Each .on(selector, ...) call adds a patch. When the SDK needs settings for a specific operation, it applies all matching patches in the order you declared them. Declare general selectors first so more specific ones override them:

all() -> reads() -> reads().batch() -> reads().batch().ap()
^
writes() -> writes().retryable() -> writes().retryable().point()

Within a single Java behavior, this is last-writer wins — if two patches match the same operation, the one declared later takes precedence, even if it is broader.

In Python, keyword order does not matter. Scopes always apply from least to most specific: all → reads / writes → mode (reads_ap, writes_sc, …) → retryability (writes only) → shape (reads_batch, writes_point, …). A shape scope therefore overrides a mode scope, so reads_batch wins over reads_ap for an AP batch read.

Behavior tiered = Behavior.DEFAULT.deriveWithChanges("tiered", builder -> builder
.on(Selectors.all(), ops -> ops
.abandonCallAfter(Duration.ofSeconds(2))
)
.on(Selectors.reads(), ops -> ops
.abandonCallAfter(Duration.ofSeconds(1))
)
.on(Selectors.reads().batch(), ops -> ops
.abandonCallAfter(Duration.ofSeconds(5))
)
);

📖 API reference: com.aerospike.client.sdk

Resolution for a batch read: all() sets 2s, reads() narrows to 1s, reads().batch() narrows to 5s. Resolution for a single-key read: all() sets 2s, reads() narrows to 1s. The reads().batch() patch doesn’t match. Resolution for a write: only all() matches -> 2s.

from datetime import timedelta
from aerospike_sdk import Behavior
from aerospike_sdk.policy import Settings
tiered = Behavior.DEFAULT.derive_with_changes(
"tiered",
all=Settings(total_timeout=timedelta(seconds=2)),
reads=Settings(total_timeout=timedelta(seconds=1)),
reads_batch=Settings(total_timeout=timedelta(seconds=5)),
)

📖 API reference: Behavior.DEFAULT

Dimension 2: Behavior inheritance

A derived behavior starts with the fully resolved settings from its parent, then applies its own patches on top. This forms a tree:

DEFAULT
(all: 1s, 3 attempts)
/ \
v v
production reporting
(all: 5s, 3 attempts) (all: 30s)
|
v
highLoad
(all: 5s, 3 attempts)
(batch reads: 16 concurrent nodes)
Behavior production = Behavior.DEFAULT.deriveWithChanges("production", builder -> builder
.on(Selectors.all(), ops -> ops
.abandonCallAfter(Duration.ofSeconds(5))
.maximumNumberOfCallAttempts(3)
)
);
Behavior highLoad = production.deriveWithChanges("highLoad", builder -> builder
.on(Selectors.reads().batch(), ops -> ops
.maxConcurrentNodes(16)
)
);

📖 API reference: com.aerospike.client.sdk

from datetime import timedelta
from aerospike_sdk import Behavior
from aerospike_sdk.policy import Settings
production = Behavior.DEFAULT.derive_with_changes(
"production",
all=Settings(total_timeout=timedelta(seconds=5), max_retries=2), # 3 attempts
)
high_load = production.derive_with_changes(
"high_load",
reads_batch=Settings(max_concurrent_nodes=16),
)

📖 API reference: Behavior.DEFAULT

Resolution for a batch read on highLoad:

StepSourceWhat happens
1DEFAULT resolvesall() -> 1s timeout, 3 attempts
2production patches appliedall() overrides -> 5s timeout, 3 attempts
3highLoad patches appliedreads().batch() adds -> 16 concurrent nodes
Result5s timeout, 3 attempts, 16 concurrent nodes

A child’s patches can override anything from the parent — parent settings are a starting point, not a floor.

Both dimensions combined

The two dimensions compose naturally. Within each behavior in the chain, selector patches cascade from general to specific. Across behaviors, the child’s resolved matrix starts from the parent’s fully resolved matrix.

Behavior base = Behavior.DEFAULT.deriveWithChanges("base", builder -> builder
.on(Selectors.all(), ops -> ops
.abandonCallAfter(Duration.ofSeconds(5))
)
.on(Selectors.reads(), ops -> ops
.abandonCallAfter(Duration.ofSeconds(2))
)
);
Behavior child = base.deriveWithChanges("child", builder -> builder
.on(Selectors.reads().batch(), ops -> ops
.abandonCallAfter(Duration.ofSeconds(10))
)
);

📖 API reference: com.aerospike.client.sdk

from datetime import timedelta
from aerospike_sdk import Behavior
from aerospike_sdk.policy import Settings
base = Behavior.DEFAULT.derive_with_changes(
"base",
all=Settings(total_timeout=timedelta(seconds=5)),
reads=Settings(total_timeout=timedelta(seconds=2)),
)
child = base.derive_with_changes(
"child",
reads_batch=Settings(total_timeout=timedelta(seconds=10)),
)

📖 API reference: Behavior.DEFAULT

Resolution for a batch read on child:

StepSourceTimeout
1base: all()5s
2base: reads() overrides2s
3base resolved for batch read2s (parent done)
4child: reads().batch() overrides10s (final)

Resolution for a single-key read on child: inherits 2s from base (no child patch matches) -> 2s. Resolution for a write on child: inherits 5s from base’s all() -> 5s.

Multiple sessions, one cluster

Behavior reporting = Behavior.DEFAULT.deriveWithChanges("reporting", builder -> builder
.on(Selectors.all(), ops -> ops
.abandonCallAfter(Duration.ofSeconds(30))
)
);
Session defaultSession = cluster.createSession(Behavior.DEFAULT);
Session reportingSession = cluster.createSession(reporting);

📖 API reference: Cluster.createSession(Behavior)

from datetime import timedelta
from aerospike_sdk import Behavior
from aerospike_sdk.policy import Settings
reporting = Behavior.DEFAULT.derive_with_changes(
"reporting",
all=Settings(total_timeout=timedelta(seconds=30)),
)
default_session = cluster.create_session(Behavior.DEFAULT)
reporting_session = cluster.create_session(reporting)

📖 API reference: Behavior.DEFAULT

Both sessions share the same cluster connection pool but have different operational settings.