ignite-client
An async Rust thin client for Apache Ignite 2.x, implementing the Ignite Binary Client Protocol over TCP.
New in 0.4.0 — Cache entry TTL / expiry. Attach a per-entry lifetime to any cache handle with
cache.with_expiry_policy(...): entries can expire a configurable time after creation, update, or access. See Expiry / TTL.New in 0.3.0 — Partition awareness / affinity routing. The client connects to every configured cluster node, computes each key's primary node locally, and sends cache operations straight to it — eliminating the server-side proxy hop. A pure optimization with a fail-safe fallback to the default channel, so results never change. See Partition awareness.
Table of Contents
- Features
- Protocol Reference
- Project Structure
- Quick Start
- API Reference
- Architecture
- Codec Details
- Comparison with Existing Rust Clients
- Running Tests
- License
Features
| Capability | Status |
|---|---|
| Protocol 1.7.0 handshake | ✅ |
| Authentication (username/password) | ✅ |
OP_QUERY_SQL_FIELDS (SELECT) |
✅ |
| Automatic cursor pagination | ✅ |
| DML: INSERT / UPDATE / DELETE | ✅ |
Transactions (TX_START / TX_END) |
✅ |
| Drop-based rollback | ✅ |
| Async pipelining (multiple in-flight requests per connection) | ✅ |
| deadpool connection pool | ✅ |
| Full wire type coverage (Null, Bool, Byte…Long, Float, Double, String, UUID, Date, Time, Timestamp, Decimal, arrays) | ✅ |
| KV cache API (get, put, get_all, put_all, contains_key, remove, replace, …) | ✅ |
| Cache entry TTL / expiry policies (create / update / access) | ✅ |
TLS (rustls + native system CA bundle) |
✅ |
Streaming QueryStream cursor |
✅ |
| TCP keepalive | ✅ |
| Client-side request timeouts | ✅ |
| Multi-node connection registry (one pool per node, keyed by node UUID) | ✅ |
| Partition awareness / affinity routing (primary-node routing for KV ops) | ✅ |
| Server endpoint discovery (auto-learn nodes not in the address list) | ✅ |
Read-from-backup / DC-aware routing for read-only ops (DC_AWARE, negotiated) |
✅ |
Protocol Reference
This client implements the Apache Ignite 2.x Thin Client Binary Protocol.
| Document | URL |
|---|---|
| Thin Client Overview | https://ignite.apache.org/docs/latest/thin-clients/getting-started-with-thin-clients |
| Binary Client Protocol spec | https://ignite.apache.org/docs/latest/binary-client-protocol/binary-client-protocol |
| Data Format (type codes, wire encoding) | https://ignite.apache.org/docs/latest/binary-client-protocol/data-format |
| SQL and Scan Queries operations | https://ignite.apache.org/docs/latest/binary-client-protocol/sql-and-scan-queries |
| Cache operations | https://ignite.apache.org/docs/latest/binary-client-protocol/key-value-queries |
| Transaction operations | https://ignite.apache.org/docs/latest/binary-client-protocol/transactions |
| Error codes | https://ignite.apache.org/docs/latest/binary-client-protocol/error-codes |
Protocol version
The handshake negotiates protocol version 1.7.0 — the highest version supported by the Apache Ignite 2.x series. GridGain 8.x (the commercial fork) uses the same protocol version.
Port
Ignite thin client port defaults to 10800.
Project Structure
This is a single crate — no workspace members.
ignite-client/
├── Cargo.toml ← package manifest (ignite-v2-client, crate: ignite_client)
├── src/
│ ├── lib.rs ← public re-exports
│ ├── client.rs ← IgniteClient: query, execute, begin_transaction, cache, …
│ ├── transaction.rs ← Transaction: query, execute, commit, rollback, cache, drop
│ ├── cache.rs ← IgniteCache: get, put, get_all, put_all, remove, … (affinity-routed); with_expiry_policy
│ ├── affinity.rs ← partition awareness: key hashing, rendezvous partition/mask,
│ │ CACHE_PARTITIONS codec, AffinityContext (mappings + refresh)
│ ├── channel.rs ← ChannelRegistry: one pool per node, node-UUID→pool routing,
│ │ round-robin fallback, lazy mapping refresh, endpoint discovery
│ ├── discovery.rs ← server endpoint discovery codec (CLUSTER_GROUP_GET_NODE_ENDPOINTS)
│ ├── stream.rs ← QueryStream: lazily-paged streaming cursor
│ ├── query.rs ← QueryResult, Row, Column, ColumnType, UpdateResult
│ ├── pool.rs ← IgniteClientConfig, deadpool manager
│ ├── error.rs ← IgniteError
│ ├── protocol/ ← pure codec layer: no I/O, no async
│ │ ├── mod.rs
│ │ ├── types.rs ← IgniteValue enum + column_type(), ColumnType, op/type codes
│ │ ├── codec.rs ← encode_value / decode_value roundtrip, UUID/byte-array readers
│ │ ├── handshake.rs ← protocol 1.7.0 handshake encoding + server node-UUID parsing
│ │ ├── error.rs ← ProtocolError
│ │ └── messages.rs ← SqlFieldsRequest, TxStart/End, cache ops, response header (topology version)
│ └── transport/ ← async TCP layer
│ ├── mod.rs
│ ├── connection.rs ← IgniteConnection (pipelined, multiplexed; node UUID, topology version)
│ ├── error.rs ← TransportError
│ └── tls.rs ← build_tls_config (rustls + native-certs)
├── tests/
│ ├── smoke.rs ← 35 broad end-to-end integration tests
│ ├── metadata.rs ← 10 schema/system-view metadata tests
│ ├── functional_query.rs ← 6 SQL query behaviour tests (pagination, mixed KV+SQL, errors)
│ ├── functional_cache.rs ← 5 KV cache API lifecycle and operation tests
│ ├── transaction.rs ← 3 concurrent transaction correctness tests
│ ├── partition_awareness.rs ← 4 affinity-routing tests (roundtrip, discovery, DC read path, parity)
│ └── expiry.rs ← 3 TTL tests (creation / update / access expiry)
├── local-cluster/ ← scripts + config for a local 3-node test cluster
│ ├── ignite-config.xml ← static-discovery node config (partitioned cache, thin connector)
│ ├── start.sh ← launch 3 nodes on ports 10800/10801/10802
│ └── stop.sh ← stop the local nodes
└── docs/
└── partition-awareness-plan.md ← design/port plan for the affinity feature
The protocol and affinity modules have no I/O dependency, so their codec and
routing-logic tests run without a live Ignite node.
Quick Start
Add to Cargo.toml:
[]
= { = "../ignite-client" }
= { = "1", = ["full"] }
SELECT query
use ;
async
Per-value type inspection
QueryResult::columns exposes result-set column names. The Ignite 2.x
OP_QUERY_SQL_FIELDS protocol carries only column names in the first-page
metadata — no per-column type codes. Type information is available per value
via IgniteValue::column_type(), which reads the 1-byte wire tag that
accompanies every encoded value. The only case that returns
ColumnType::Unknown is IgniteValue::Null.
use ;
let result = client
.query
.await?;
if let Some = result.first_row
// Dispatch on value type while iterating rows.
for row in &result.rows
Column names are accessible immediately on a QueryStream before any rows are
consumed:
let stream = client
.query_stream
.await?;
for col in &stream.columns
DML
let updated = client
.execute
.await?;
println!;
Transaction
let mut tx = client.begin_transaction.await?;
tx.execute.await?;
tx.execute.await?;
tx.commit.await?;
// If commit() is not called, Drop triggers a fire-and-forget rollback.
Transaction helper (auto-commit/rollback)
let result = client
.with_transaction
.await?;
KV cache
let cache = client.get_or_create_cache.await?;
cache.put.await?;
let val = cache.get.await?;
println!; // String("hello")
Expiry / TTL
cache.with_expiry_policy(policy) returns a new handle whose operations apply a
per-entry time-to-live. The policy carries three independent durations — applied
when an entry is created, updated (overwritten), and accessed (read)
— each one of Eternal, Immediate, Millis(n), or Unchanged (leave the
entry's current lifetime alone).
use ;
use Duration;
let cache = client.get_or_create_cache.await?;
// New entries live 30 minutes; reads/updates don't change that.
let ttl = new;
cache.with_expiry_policy
.put
.await?;
// A "sliding" session: every read extends the lifetime by 30 minutes.
let sliding = new;
let v = cache.with_expiry_policy.get.await?;
Requires the server's EXPIRY_POLICY feature (protocol ≥ 1.6, always available
on the 1.7 servers this client targets). The base handle (no policy) uses the
cache's configured default.
Streaming cursor
use StreamExt;
let mut stream = client
.query_stream
.await?;
while let Some = stream.next.await
TLS
let config = new
.with_tls; // use system CA bundle
// .with_tls_accept_invalid_certs() // for self-signed / dev certs
let client = new;
Authentication
let config = new
.with_auth
.with_pool_size;
API Reference
IgniteClientConfig
connect_timeout is also used as the deadpool wait and create timeout.
request_timeout is applied per-request inside send_and_receive.
new() seeds addresses with the single address; with_addresses() replaces
the list and sets address to the first entry. Partition awareness is enabled
automatically once two or more addresses are configured (see
Partition awareness).
IgniteClient
IgniteClient is Clone — share a single instance across tasks; it wraps an
Arc'd per-node channel registry and a shared affinity context. Cache
operations are automatically routed to the key's owning node when partition
awareness is enabled.
Transaction
Transaction isolation levels map to Ignite protocol values:
TxIsolation |
Protocol value |
|---|---|
ReadCommitted |
0 |
RepeatableRead |
1 |
Serializable |
2 |
Transaction concurrency modes:
TxConcurrency |
Protocol value |
|---|---|
Optimistic |
0 |
Pessimistic |
1 |
Note: DML inside thin-client transactions requires the Ignite node to be started with
-DIGNITE_ALLOW_DML_INSIDE_TRANSACTION=trueand the table must useATOMICITY=TRANSACTIONAL.
IgniteCache
Obtained via client.cache(), client.get_or_create_cache(), or
transaction.cache(). Cheap to clone — holds an i32 cache ID and either the
channel registry + affinity context (non-transactional, affinity-routed) or a
transaction connection.
Cache names are case-insensitive —
cache_id()upper-cases the name before hashing. Use a consistent case (the test suite uses upper-case names).
Expiry policy types:
/// TTL applied for one event (create / update / access).
/// Per-entry lifetime applied on create, update, and access.
The three durations map directly to JCache's getExpiryForCreation /
Update / Access, but use plain Rust naming. with_expiry_policy mirrors the
Java thin client's withExpirePolicy; the policy rides in the cache-op header
(flag 0x04 + three i64 durations).
QueryResult / Row
Column and ColumnType
QueryStream
A lazily-paged result stream returned by client.query_stream() and
transaction.query_stream(). Rows are yielded one at a time; subsequent pages
are fetched from the server only when the current page is exhausted. The
server-side cursor is closed automatically when the stream is exhausted or
dropped mid-iteration.
Use futures::StreamExt to drive the stream with .next().await.
IgniteValue type system
IgniteValue maps every Ignite wire type to a Rust variant:
Wire encoding follows the spec at https://ignite.apache.org/docs/latest/binary-client-protocol/data-format
Notable encoding details:
- UUID: 16 bytes big-endian (most-significant bytes first, matching Java's
UUID.getMostSignificantBits()/getLeastSignificantBits()) - Decimal:
[i32: scale][i32: byte_count][bytes: two's-complement big-endian magnitude] - Timestamp:
[i64: epoch_ms][i32: nanoseconds_fraction] - Null: type code 101 with no payload; any typed field may be null
Architecture
Request multiplexing
A single IgniteConnection supports many concurrent requests without
serialising them through a mutex on reads:
Caller A ──request(id=1)──┐ ┌──response(id=1)──▶ Caller A
│ TCP socket │
Caller B ──request(id=2)──┤ ─────────────▶│
│ │ background
Caller C ──request(id=3)──┘ │ reader task
└──response(id=3)──▶ Caller C
response(id=2)──▶ Caller B
Requests are written to a Mutex<SplitSink> (contended only on write, not on
read). Each caller registers a oneshot::Sender in a shared HashMap<i64, Sender> keyed by request_id. The background reader task peeks the first 8
bytes of each response frame, looks up the sender, and delivers the payload.
This is the same design used by tokio-postgres and redis-rs.
Connection pool
IgniteClient wraps a deadpool managed
pool of IgniteConnection objects. Pool behaviour:
max_pool_sizeconnections maximum (default 10)- Each connection is health-checked on recycle via
is_alive()(AtomicBool) - Connections are created on demand, not pre-warmed
connect_timeoutis applied as both the deadpoolwaitandcreatetimeout- TCP keepalive is applied to every socket (60 s idle, 15 s interval)
Transaction connections
Transactions use a dedicated TCP connection that is not drawn from the pool.
This avoids pool exhaustion when many concurrent long-running transactions are
in flight. The connection is closed when the Transaction is dropped.
Pagination
OP_QUERY_SQL_FIELDS returns a first page with a cursor_id and a has_more
flag.
client.query()/transaction.query()— automatically fetches all subsequent pages viaOP_QUERY_SQL_FIELDS_CURSOR_GET_PAGEand returns a fully materialisedQueryResult.client.query_stream()/transaction.query_stream()— returns aQueryStreamthat fetches pages lazily as the consumer polls the stream. The server-side cursor is closed when the stream is exhausted or dropped.
The page_size config field controls how many rows are returned per server
round-trip (default 1024).
Partition awareness
Without partition awareness every request goes to one configured node, and the
server silently re-routes each key to its owning node — an extra network hop.
With partition awareness the client computes the owning node locally and sends
the request straight to it. The design mirrors the Java thin client
(org.apache.ignite.internal.client.thin):
┌──────────────── IgniteClient ─────────────────┐
│ AffinityContext (Arc-shared) │
│ • partition → primary-node UUID mappings │
│ • topology version + single-flight refresh │
└───────────────────────────────────────────────┘
│ affinity_node(cache_id, key)
▼
key ─▶ affinity_hash(key) ─▶ partition(hash, mask, parts) ─▶ node UUID
│
▼
┌──────────── ChannelRegistry ──────────────┐
│ pool[node-1] pool[node-2] pool[node-3] │ ← one deadpool pool per node
│ node UUID → pool index (learned) │
└───────────────────────────────────────────┘
│ get(target) → owning node, else default round-robin
▼
request sent to the primary node
How it works:
- Handshake — connecting to each node learns its node UUID (parsed from the protocol 1.7 handshake success response) and keys that node's pool.
- Mapping fetch — on first use of a cache, or after a topology change, the
client issues
CACHE_PARTITIONS(op 1101) and decodes thepartition → nodetable. Fetches are single-flighted per cache. - Routing — for a single-key op,
affinity_hash(key)(a faithful port of the JVMhashCode()for primitives /String/UUID) feeds the rendezvouspartition(...)function; the resulting partition selects the primary node's pool. - Topology tracking — every response header carries the affinity topology version; a bump marks the held mappings stale and triggers a lazy refresh.
Fail safe. Partition awareness is a pure optimization. Any miss — PA
disabled, no mapping yet, unknown/unsupported key type, unknown node, or a dead
target pool — falls back to the default channel, so observable results are
always identical to the single-node path. Multi-key ops (get_all, put_all),
SQL, and transactions use the default channel.
Endpoint discovery. When partition awareness is enabled, on first use the
client also asks the cluster for the full set of server node endpoints
(CLUSTER_GROUP_GET_NODE_ENDPOINTS, op 5102) and opens channels to any node not
in the configured address list — so you can list a single bootstrap address and
still route to every node. Toggle with with_endpoint_discovery(true|false).
Read-from-backup (DC-aware). Mirroring the Java thin client, read-only ops
(get, contains_key) are routed with primary = false: when the client and
server have negotiated the DC_AWARE feature and the client has a
data-center id set (with_data_center_id("DC1")), the server includes a
same-data-center partition map in CACHE_PARTITIONS responses and reads go to a
DC-local backup owner; otherwise the DC map equals the primary map and reads go
to the primary. The feature is negotiated at handshake (the client advertises
DC_AWARE; the DC-aware wire format is used only if the server also supports
it — DC_AWARE landed in Ignite 2.18), so it is a safe no-op against older
servers. Writes always route to the primary.
This is verified end to end against a 2-data-center cluster: with the client set
to dcId = DC1 and nodes split DC1/DC1/DC2, reads of keys whose primary is the
DC2 node route to a DC1 backup instead — see the pa_dc_aware_read_path_is_correct
test and the Local cluster DC-demo mode.
Enablement. Auto-on when two or more addresses are configured; override with
with_partition_awareness(true|false). Configure the cluster with:
let config = new
.with_addresses;
let client = new;
let cache = client.get_or_create_cache.await?;
cache.put.await?; // routed to key 42's node
Scope: routing currently covers primitive, String, and UUID keys (the types
with well-defined, exactly-replicable Java hash codes). Custom affinity-key
field extraction is future work.
Codec Details
The src/protocol/ module contains the codec with no I/O dependency, making
it independently testable.
Frame format
[i32 LE: payload_length] ← length prefix handled by LengthDelimitedCodec
[payload bytes] ← the codec layer strips the prefix before delivery
Request payload format
[i16 LE: op_code]
[i64 LE: request_id]
[operation-specific fields …]
Response payload format
[i64 LE: request_id]
[i32 LE: status] ← 0 = success; non-zero = server error
if status != 0:
[string: error_message]
if status == 0:
[operation-specific response …]
Java string hashing
Cache IDs and field IDs in the binary protocol are derived using Java's
String.hashCode() algorithm. The java_hash() function in types.rs
replicates this:
The cache_id() helper derives the cache ID from a cache name. The name is
upper-cased first, so cache names are effectively case-insensitive:
Comparison with Existing Rust Clients
| vkulichenko | ptupitsyn fork | this crate | |
|---|---|---|---|
| SQL queries | ✗ | ✗ | ✅ |
| Cursor pagination | ✗ | ✗ | ✅ |
| Streaming cursor | ✗ | ✗ | ✅ |
| Transactions | ✗ | ✗ | ✅ |
| Async I/O (tokio) | ✗ | ✗ | ✅ |
| Connection pool | ✗ | ✗ | ✅ |
| UUID / Date / Timestamp / Decimal | ✗ | ✗ | ✅ |
| Null handling | ✗ | ✗ | ✅ |
| Query parameters | ✗ | ✗ | ✅ |
| TLS | ✗ | ✗ | ✅ |
| KV get/put | ✅ | ✅ | ✅ |
| Partition awareness / affinity routing | ✗ | ✗ | ✅ |
| Last commit | 2020 | 2020 | 2026 |
| Intended use | learning exercise | abandoned fork | production |
The vkulichenko / ptupitsyn implementations are synchronous, blocking, cover
only get / put on primitives, and have not been updated since 2020. They
are not suitable as a dependency baseline for production work.
Running Tests
Unit tests (no live node required)
Covers:
IgniteValuecodec roundtrip for every type (Null, Bool, Byte, Short, Int, Long, Float, Double, Char, String, UUID, Date, Timestamp, Time, Decimal, ByteArray, RawObject)- Two's-complement Decimal encoding (positive, negative, zero, boundary)
java_hash()against known Java reference valuesSqlFieldsRequestencode/decode- Transaction start/end encoding
- Expiry policy:
ExpiryDurationwire sentinels (-2/-1/0/ms) and the cache-header expiry flag + durations (ordered beforetx_id) - Partition awareness (
src/affinity.rs,src/channel.rs):affinity_hash()JVMhashCodegolden vectors per key type (Int, Long, Bool, Byte/Short/Char, String, UUID) and unsupported-kind fallback- rendezvous
partition()/calculate_mask()/safe_abs()golden vectors CACHE_PARTITIONSrequest encoder + response decoder (against byte fixtures)AffinityContextrouting decisions, topology-version ordering, single-flight refresh;PoolSelectornode→pool mapping and round-robin fallbackCACHE_PARTITIONSopcode (1101), node-UUID and feature-bitmask handshake parsing, and response-header topology-version capture- endpoint discovery (
src/discovery.rs):CLUSTER_GROUP_GET_NODE_ENDPOINTS(opcode 5102) request/response codec and typed-string/raw-UUID readers DC_AWAREnegotiation + read-from-backup routing: handshake feature-bit advertisement/negotiation, DC-awareCACHE_PARTITIONSrequest/response codec, andaffinity_nodeprimary-vs-backup selection
Integration tests (require a live Ignite 2.x node on localhost:10800)
All integration test modules connect to localhost:10800 with no
authentication. Start Ignite before running them — the easiest way is the
bundled local 3-node cluster.
DML-inside-transaction tests (used by smoke.rs and transaction.rs) require
the JVM system property that enables DML in thin-client transactions, and the
SQL DATE temporal test expects a UTC server timezone:
# Linux / macOS
# Windows (example wrapper script)
(The local-cluster/start.sh script sets both of these automatically.)
The partition_awareness.rs module exercises affinity routing. Point it at the
whole cluster with the IGNITE_ADDRS environment variable (comma-separated);
without it the tests use the single default address and still exercise the full
routing pipeline against one node:
IGNITE_ADDRS=localhost:10800,localhost:10801,localhost:10802 \
Always run integration tests with --test-threads=1. Parallel DDL
(CREATE / DROP TABLE) causes schema lock contention, and many concurrent
connections from parallel tests can exhaust Ignite's handshake thread pool,
producing Unable to perform handshake within timeout errors.
Run all integration tests across all modules in one command:
Or run individual modules:
Windows note: If Windows Smart App Control blocks newly compiled test binaries, redirect the build output to AppData:
set CARGO_TARGET_DIR=%APPDATA%\ignite-test-target cargo test --tests -- --nocapture --test-threads=1
tests/smoke.rs — 35 tests
Broad end-to-end coverage of every public API surface. Tests are prefixed
smoke_.
- Basic connectivity (
SELECT 1) - SQL query and DML (
query,execute) - Multi-page cursor pagination
query_stream(lazy streaming) and early-drop cursor close- Configurable
page_size(forces multi-page fetch) begin_transaction,begin_transaction_with(explicit concurrency/isolation/timeout)with_transaction(auto-commit and rollback/Drop paths)Transaction::query,Transaction::execute,Transaction::query_streamTransaction::cache(KV ops inside a transaction — commit and rollback)IgniteCache: get, put, put_if_absent, get_all, put_all, contains_key, remove, replace, get_and_put, get_and_remove, get_and_replace, get_sizecache_names,get_or_create_cache,destroy_cacheRow::get,get_by_name,len,is_empty,columns,valuesQueryResult::columnsandrow_countIgniteValue::column_type()for per-value type accuracypool_status(),with_pool_size(),with_auth()builder fields- Type roundtrips: Bool, Int, Long, Double, String, Byte, Short, Float, ByteArray, Decimal, UUID, Null
- Date, Time, Timestamp as query parameters
- Server-side error propagation
- TLS config builder (no network), TLS graceful failure to plaintext server
- Concurrent queries on a shared client
tests/metadata.rs — 10 tests
Rust port of selected tests from Apache Ignite's JdbcThinMetadataSelfTest.java,
adapted to use SYS.* system views via OP_QUERY_SQL_FIELDS rather than JDBC
DatabaseMetaData.
| Test | What it verifies |
|---|---|
metadata_result_set_column_names_and_types |
JOIN query column names and per-value column_type() |
metadata_decimal_and_date_column_types |
ColumnType::Decimal and ColumnType::Date round-trip |
metadata_tables_visible_in_sys_tables_view |
Created table appears in SYS.TABLES |
metadata_public_tables_present_in_sys_tables_view |
Multiple tables visible together |
metadata_schemas_visible_in_sys_schemas_view |
PUBLIC and SYS schemas present |
metadata_sys_schemas_filter_returns_no_rows_for_unknown_pattern |
Empty result for unknown schema |
metadata_columns_visible_in_sys_table_columns_view |
Column names and PK flag in SYS.TABLE_COLUMNS |
metadata_pk_index_visible_in_sys_indexes_view |
PK and secondary index in SYS.INDEXES |
metadata_all_table_indexes_present_in_sys_indexes_view |
Indexes from multiple tables visible |
metadata_query_unknown_table_returns_error |
Non-existent table query returns Err |
tests/functional_query.rs — 6 tests
Rust port of Apache Ignite's FunctionalQueryTest.java (indexing module).
| Test | Java source | What it verifies |
|---|---|---|
functional_query_sql_fields_pagination |
testQueries |
100-row insert; query 50 rows with small page_size; multi-page cursor |
functional_query_sql |
testSql |
CREATE TABLE, INSERT, SELECT by parameter, SELECT with page_size=1 |
functional_query_empty_table |
testGettingEmptyResultWhenQueryingEmptyTable |
Empty table query returns non-error result with 0 rows |
functional_query_mixed_sql_and_cache |
testMixedQueryAndCacheApiOperations |
SQL INSERT then KV cache.put; SELECT sees both |
functional_query_server_error |
testSqlParameterValidation |
Invalid SQL / missing table returns Err |
functional_query_empty_sql |
testEmptyQuery |
Empty SQL string returns Err |
tests/functional_cache.rs — 5 tests
Rust port of selected tests from Apache Ignite's FunctionalTest.java (thin
client internal tests), covering the KV cache API.
| Test | Java source | What it verifies |
|---|---|---|
functional_cache_management |
testCacheManagement |
Full cache lifecycle: create → get_size → name in cache_names → destroy → name absent |
functional_cache_put_get |
testPutGet |
put, get, contains_key, overwrite, remove, multi-type keys |
functional_cache_atomic_put_get |
testAtomicPutGet |
get_and_put, get_and_remove, put_if_absent, get_and_replace sequences |
functional_cache_batch_put_get |
testBatchPutGet |
put_all / get_all (full + partial) / remove_all subset / get_size |
functional_cache_remove_replace |
testRemoveReplace |
replace (true/false) and remove / remove_all on a 100-entry dataset |
tests/transaction.rs — 3 tests
Rust port of selected tests from Apache Ignite's BlockingTxOpsTest.java.
| Test | Java source | What it verifies |
|---|---|---|
tx_sum_invariant |
testTransactionalConsistency (Pessimistic/RepeatableRead) |
5 concurrent tasks × 100 key-transfer iterations; sum of all values remains 0 |
tx_sum_invariant_optimistic |
testTransactionalConsistency (Optimistic/Serializable) |
Same invariant with conflict-retry loop |
tx_all_cache_ops_inside_tx |
testBlockingOps |
Every cache operation works correctly inside an explicit transaction: put, get, contains_key, put_all, get_all, put_if_absent, replace, get_and_put, get_and_remove, get_and_replace, remove, remove_all |
tests/partition_awareness.rs — 4 tests
End-to-end affinity-routing tests. Set IGNITE_ADDRS to route across multiple
nodes; otherwise they run against the single default address.
| Test | What it verifies |
|---|---|
pa_put_get_roundtrip_is_correct |
A spread of keys put/get correctly with partition awareness on — the routed write/read path returns every stored value |
pa_endpoint_discovery_reaches_unconfigured_nodes |
Configured with one node + PA forced on, the client discovers the rest of the cluster and still serves every key |
pa_dc_aware_read_path_is_correct |
On a DC-demo cluster (IGNITE_DC_DEMO=1), exercises the DC read path with the client's dcId = DC1 (request carries the dcId, DC partition map is decoded, reads route through it) and asserts correct values |
pa_on_and_off_agree |
Reading the same keys through a PA-on and a PA-off client yields identical results (the fail-safe guarantee: routing never changes observable results) |
Enable RUST_LOG=ignite_client=debug to see routing decisions, e.g.
fetched cache partitions cache_id=… version=… mappings=… applicable=….
tests/expiry.rs — 3 tests
Cache entry TTL behaviour against a live node (any cluster — TTL is per-operation).
| Test | What it verifies |
|---|---|
expiry_creation_ttl |
A freshly inserted entry is gone after its creation TTL elapses |
expiry_update_ttl |
An eternal entry only starts expiring once overwritten (update TTL) |
expiry_access_ttl |
Reading an entry through an access-TTL handle sets its lifetime |
Local 3-node test cluster
The local-cluster/ directory contains everything needed to run a partitioned
3-node Apache Ignite cluster on localhost from a local Ignite binary:
| File | Purpose |
|---|---|
ignite-config.xml |
Node config: static localhost discovery, a partitioned cache, thin-client connector |
start.sh |
Launch three nodes sequentially on thin ports 10800 / 10801 / 10802 |
stop.sh |
Stop the local nodes |
Usage:
# point IGNITE_HOME at your local Apache Ignite install (contains bin/ignite.sh)
start.sh auto-selects Java 11 (via /usr/libexec/java_home -v 11 on macOS) and
sets -DIGNITE_ALLOW_DML_INSIDE_TRANSACTION=true and -Duser.timezone=UTC so the
full integration suite — including the transaction and temporal tests — passes.
Logs are written to /tmp/ignite-node{1,2,3}.log.
DC-demo mode. Set IGNITE_DC_DEMO=1 to start a 2-data-center cluster (nodes
1 & 2 → DC1, node 3 → DC2, via -DIGNITE_DATA_CENTER_ID). Combined with the
pre-defined DC_DEMO cache (FULL_SYNC + readFromBackup) and a client
dcId = DC1, this drives the DC read path in pa_dc_aware_read_path_is_correct
(reads of DC2-primary keys route to a DC1 backup). Requires Ignite ≥ 2.18 (when
DC_AWARE was added):
IGNITE_DC_DEMO=1
Run the complete suite against it:
Cargo.lock dependency pins
All dependencies are pinned to exact versions for reproducible builds.
The workspace root Cargo.toml [workspace.dependencies] section is the
single place to update them:
| Crate | Pinned version |
|---|---|
tokio |
=1.50.0 |
tokio-util |
=0.7.18 |
bytes |
=1.11.1 |
thiserror |
=2.0.18 |
tracing |
=0.1.44 |
futures |
=0.3.32 |
bigdecimal |
=0.4.10 |
uuid |
=1.21.0 |
deadpool |
=0.13.0 |
async-trait |
=0.1.89 |
num-bigint |
=0.4.6 |
socket2 |
=0.6.2 |
rustls |
=0.23.37 |
tokio-rustls |
=0.26.4 |
rustls-native-certs |
=0.8.3 |
License
Apache License 2.0. See LICENSE.
Apache Ignite is a registered trademark of The Apache Software Foundation. This project is not affiliated with or endorsed by the Apache Software Foundation or GridGain Systems.