marina

Erlang CI

High-performance Erlang client for Apache Cassandra and ScyllaDB, speaking the CQL binary protocol directly over TCP.

Features

Requirements

Installation

Pin to the latest tag (recommended):

{deps, [
{marina, {git, "https://github.com/lpgauth/marina.git", {tag, "0.4.0"}}}
]}.

Or follow master:

{deps, [
{marina, {git, "https://github.com/lpgauth/marina.git", {branch, "master"}}}
]}.

Configuration

All settings are read from the marina application env.

NameTypeDefaultDescription
backlog_sizepos_integer()1024Per-connection shackle backlog.
bootstrap_ips[string()]["127.0.0.1"]IPs tried in order until one responds with system.peers.
bootstrap_retry_mspos_integer()500Delay before re-attempting bootstrap when no IP responded.
compressionboolean()falseNegotiate LZ4 compression on every connection.
keyspaceundefined | binary()undefinedDefault keyspace; issued as USE … after startup.
passwordbinary()undefinedPassword for PasswordAuthenticator.
pool_sizepos_integer()16Number of shackle connections per node.
pool_strategyrandom | round_robinrandomShackle's strategy for picking a connection from a per-node pool.
portpos_integer()9042Server port.
reconnectboolean()trueAuto-reconnect closed connections.
reconnect_time_maxpos_integer() | infinity120000Upper bound on the reconnect backoff (ms).
reconnect_time_minpos_integer()1500Lower bound on the reconnect backoff (ms).
socket_options[gen_tcp:option()]see marina_internal.hrlgen_tcp options applied to every connection.
strategyrandom | token_awaretoken_awareNode selection: random, or Murmur3-hash the routing_key to the replica.
usernamebinary()undefinedUsername for PasswordAuthenticator.

Usage

Start the application, then call the query API:

1> marina_app:start().
{ok, [granderl, metal, shackle, foil, marina, ...]}
2> marina:query(<<"SELECT id, name FROM users LIMIT 1">>, #{timeout => 1000}).
{ok, {result, _Metadata, 1, [[<<"…">>, <<"alice">>]]}}
3> marina:query(<<"SELECT * FROM users WHERE id = ?">>,
#{values => [Uuid],
routing_key => Uuid,
timeout => 1000}).
{ok, {result, _Metadata, 1, [Row]}}
4> marina:reusable_query(<<"SELECT * FROM users WHERE id = ?">>,
#{values => [Uuid], timeout => 1000}).
{ok, {result, _Metadata, 1, [Row]}}
5> marina:batch([
{query, <<"INSERT INTO kv (k, v) VALUES (1, 'a')">>, []},
{query, <<"INSERT INTO kv (k, v) VALUES (2, 'b')">>, []}
], #{batch_type => logged, timeout => 1000}).
{ok, undefined}

API surface

Synchronous:

Asynchronous (return a shackle:request_id(), consume via marina:receive_response/1):

Query options

query_opts() is a map; unknown keys are ignored.

KeyTypeDefault
batch_typelogged | unlogged | counterlogged
consistency_level?CONSISTENCY_*?CONSISTENCY_ONE
page_sizepos_integer()unset
paging_statebinary()unset
pidpid()self()
routing_keyinteger() | binary()undefined
skip_metadataboolean()false
timeoutpos_integer()1000
values[binary()]undefined

Errors

All marina:* calls return {ok, term()} | {error, error_reason()} where error_reason/0 (exported, added in 0.4.5) enumerates:

cql_error/0 is exported as a public type so callers can pattern-match against the server-error shape cleanly.

Architecture notes

Telemetry

marina emits two telemetry events at the request boundary. Attach handlers via telemetry:attach/4:

EventMeasurementsMetadata
[marina, request, sent]count => 1operation, pool, async
[marina, request, error]count => 1operation, reason

sent fires at each shackle dispatch — once per query / batch / prepare / execute. A reusable_query with a cache miss fires twice (prepare then execute), so the sum gives an accurate count of outgoing CQL operations. The error event fires when marina_pool:node/1 returns no pool (e.g. marina_pool_not_started).

Per-request shackle lifecycle (queue / send / receive) remains observable via shackle's own telemetry — marina's events surface the CQL-level intent without duplicating that work.

Development

make compile # rebar3 compile with strict warnings
make xref # cross-reference analysis
make dialyzer # success typing
make eunit # unit + integration tests (expects ScyllaDB at 172.18.0.2:9042)
make test # xref + eunit + dialyzer
make bench # ring-lookup micro-benchmark

The bundled Dockerfile produces the CI image (lpgauth/erlang-scylla:28.3.1-6.2.3-amd64) by layering OTP 28.3.1 (from the official erlang image) on top of scylladb/scylla:6.2.3. For local integration tests, run that image or a plain ScyllaDB container on 172.18.0.2:9042.

License

MIT — see LICENSE.