# Behaviors and Selectors

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

```java
Cluster cluster = new ClusterDefinition("localhost", 3000).connect();

Session defaultSession = cluster.createSession(Behavior.DEFAULT);
```

```python
from aerospike_sdk import Behavior, ClusterDefinition

cluster = await ClusterDefinition("localhost", 3000).connect()

default_session = cluster.create_session(Behavior.DEFAULT)
```

## Deriving behaviors

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

```java
Behavior production = Behavior.DEFAULT.deriveWithChanges("production", builder -> builder

    .on(Selectors.all(), ops -> ops

        .abandonCallAfter(Duration.ofSeconds(5))

    )

);

Session productionSession = cluster.createSession(production);
```

```python
from datetime import timedelta

from aerospike_sdk import Behavior

production = Behavior.DEFAULT.derive_with_changes(

    "production",

    total_timeout=timedelta(seconds=5),

)

production_session = cluster.create_session(production)
```

## Selectors reference

Selectors narrow which operations a setting applies to. Settings cascade from general to specific — more specific selectors override broader ones.

### Entry points

| Selector | Targets |
| --- | --- |
| `Selectors.all()` | Every operation |
| `Selectors.reads()` | All read operations |
| `Selectors.writes()` | All write operations |
| `Selectors.transaction()` | Transaction verify/roll operations |

### Narrowing reads

```java
Selectors.reads()              // all reads

Selectors.reads().get()        // single-key reads

Selectors.reads().batch()      // batch reads

Selectors.reads().query()      // dataset queries (scans, SI queries)
```

```python
# Python has no Selectors.reads() / .get() / .batch() / .query() API. Configure reads

# via flat kwargs on Behavior (e.g. total_timeout, replica, consistency_level,

# max_concurrent_nodes, record_queue_size) for the whole session; you cannot target only

# one read shape (single-key vs batch vs query) inside a single behavior.
```

### Narrowing writes

```java
Selectors.writes()                         // all writes

Selectors.writes().retryable()             // idempotent writes (safe to retry)

Selectors.writes().nonRetryable()          // non-idempotent writes

Selectors.writes().retryable().point()     // single-key retryable writes

Selectors.writes().retryable().batch()     // batch retryable writes

Selectors.writes().nonRetryable().point()  // single-key non-retryable writes

Selectors.writes().nonRetryable().batch()  // batch non-retryable writes
```

```python
# No Selectors.writes() narrowing in Python (retryable vs point vs batch). Use one

# behavior’s max_retries / retry_delay / commit_level for all writes on that session,

# or split workloads across multiple behaviors if you need different write tuning.
```

### Mode selection (AP vs CP)

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

```java
// Recommended: mode last -> full type safety

Selectors.reads().batch().ap()               // exposes readMode()

Selectors.writes().retryable().point().ap()  // exposes commitLevel()

// Works but loses type-specific methods in IDE

Selectors.writes().ap().retryable().point()  // commitLevel() not visible at compile time
```

```python
from aerospike_sdk import Behavior

# Python has no .ap() / .cp() selector chain. Use predefined bases (e.g.

# Behavior.DEFAULT, Behavior.READ_FAST, Behavior.FAST_RACK_AWARE) for AP-style setups,

# and Behavior.STRICTLY_CONSISTENT for SC. Further tune with derive_with_changes(...)

# and kwargs such as replica, consistency_level, and commit_level.
```

## Available settings

### Common (available on all selectors)

| Setting | Description |
| --- | --- |
| `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 retry attempts |
| `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().*`:

| Setting | Description |
| --- | --- |
| `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()`:

| Setting | Description |
| --- | --- |
| `recordQueueSize(int)` | Client-side result buffer size |

### Read-specific

| Setting | Selector | Description |
| --- | --- | --- |
| `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

| Setting | Selector | Description |
| --- | --- | --- |
| `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

```java
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))

    )

);
```

```python
from datetime import timedelta

from aerospike_sdk import Behavior

# No per-operation txn_verify / txn_roll selectors in Python; retry settings apply to

# the whole behavior. Split sessions only if you need different globals for verify vs roll.

custom = Behavior.DEFAULT.derive_with_changes(

    "custom",

    max_retries=10,

    retry_delay=timedelta(seconds=1),

)
```

## 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 order. More specific selectors override broader ones:

```plaintext
all()  ->  reads()  ->  reads().batch()  ->  reads().batch().ap()

                         ^

              writes()  ->  writes().retryable()  ->  writes().retryable().point()
```

Within a single behavior, this is **last-writer wins** — if two patches match the same operation, the more specific one takes precedence.

```java
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))

    )

);
```

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**.

```python
from datetime import timedelta

from aerospike_sdk import Behavior

# Python has no per-selector cascade. Model different operation timeouts

# with separate behaviors, one per workload.

tiered_writes = Behavior.DEFAULT.derive_with_changes(

    "tiered_writes", total_timeout=timedelta(seconds=2))

tiered_reads = Behavior.DEFAULT.derive_with_changes(

    "tiered_reads", total_timeout=timedelta(seconds=1))

tiered_batch_reads = Behavior.DEFAULT.derive_with_changes(

    "tiered_batch_reads", total_timeout=timedelta(seconds=5))

# Create a session per workload

writes_session = cluster.create_session(tiered_writes)

reads_session = cluster.create_session(tiered_reads)

batch_reads_session = cluster.create_session(tiered_batch_reads)
```

### 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:

```plaintext
DEFAULT

                 (all: 1s, 3 retries)

                   /            \

                  v              v

          production           reporting

       (all: 5s, 3 retries)   (all: 30s)

              |

              v

          highLoad

       (all: 5s, 3 retries)

       (batch reads: 16 concurrent nodes)
```

```java
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)

    )

);
```

```python
from datetime import timedelta

from aerospike_sdk import Behavior

production = Behavior.DEFAULT.derive_with_changes(

    "production",

    total_timeout=timedelta(seconds=5),

    max_retries=3,

)

high_load = production.derive_with_changes(

    "high_load",

    max_concurrent_nodes=16,

)
```

Resolution for a **batch read** on `highLoad`:

| Step | Source | What happens |
| --- | --- | --- |
| 1 | `DEFAULT` resolves | `all()` -> 1s timeout, 3 retries |
| 2 | `production` patches applied | `all()` overrides -> 5s timeout, 3 retries |
| 3 | `highLoad` patches applied | `reads().batch()` adds -> 16 concurrent nodes |
| **Result** |  | **5s timeout, 3 retries, 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.

```java
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))

    )

);
```

Resolution for a **batch read** on `child`:

| Step | Source | Timeout |
| --- | --- | --- |
| 1 | `base`: `all()` | 5s |
| 2 | `base`: `reads()` overrides | 2s |
| 3 | `base` resolved for batch read | **2s** (parent done) |
| 4 | `child`: `reads().batch()` overrides | **10s** (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

```java
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);
```

```python
from datetime import timedelta

from aerospike_sdk import Behavior

reporting = Behavior.DEFAULT.derive_with_changes(

    "reporting",

    total_timeout=timedelta(seconds=30),

)

default_session = cluster.create_session(Behavior.DEFAULT)

reporting_session = cluster.create_session(reporting)
```

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