SDKs

Java SDK

The Java SDK ships as a Multi-Release JAR (MRJAR). On JDK 22+, it uses java.lang.foreign.MemorySegment (FFM API) for zero-copy off-heap access. On JDK 17–21, it falls back to sun.misc.Unsafe. Hot-swapping uses AtomicReference<AttosMemoryMap> with swap-before-close ordering, matching the Go SDK invariant.

Installation

Requires Java 17+. For FFM-accelerated access, run on JDK 22+.

xml
<dependency>
    <groupId>dev.attos</groupId>
    <artifactId>attos-java-sdk</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>

The JVM must be launched with --enable-native-access=ALL-UNNAMED when running on JDK 22+ with the FFM accessor.

Configuration & Initialisation

AttosConfig is a fluent builder controlling every open and reload call made by AttosClient.

SetterDefaultEffect
setValidateChecksum(true)falseVerifies CRC32-IEEE trailer on open. Streams entire file; one-time cost.
setPinToMemory(true)falseCalls mlock(2) via JNA. Prevents OS page-outs. Requires CAP_IPC_LOCK or ulimit -l.
setPollIntervalSeconds(n)0 (disabled)Starts a daemon ScheduledExecutorService that calls reload() every n seconds.
setLogger(slf4jLogger)class-level loggersInjects a caller-supplied SLF4J Logger for all SDK operational messages.
Main.java
import dev.attos.sdk.AttosClient;
import dev.attos.sdk.AttosConfig;
import java.io.File;

AttosConfig config = new AttosConfig()
    .setValidateChecksum(true)
    .setPinToMemory(true)
    .setPollIntervalSeconds(class="text-amber-300">30);  class="text-zinc-class="text-amber-300">500">// background sync every class="text-amber-300">30 s

class="text-zinc-class="text-amber-300">500">// AttosClient implements AutoCloseable
try (AttosClient client = new AttosClient(new File("data.attos"), config)) {
    class="text-zinc-class="text-amber-300">500">// ... lookups
}

MRJAR Accessor Selection

AttosMemoryMap selects its backing accessor at construction time via reflection:

  1. Attempt to load dev.attos.sdk.ffm.FfmMemoryAccessor — available only on JDK 22+ via the MRJAR META-INF/versions/22 layer.
  2. If that fails, load dev.attos.sdk.unsafe.UnsafeMemoryAccessor (works on JDK 17–21 via sun.misc.Unsafe).

Both implementations satisfy the same MemoryAccessor SPI contract and produce identical lookup results with identical allocation profiles.

Core API — Lookups

Callers supply a pre-allocated destination buffer. The value is copied from the off-heap region into that buffer — no JVM heap allocation.

Single key

java
byte[] key    = "user-class="text-amber-300">42".getBytes();
byte[] buffer = new byte[class="text-amber-300">512]; class="text-zinc-class="text-amber-300">500">// reuse across calls

int len = client.get(key, class="text-amber-300">0, key.length, buffer);
if (len >= class="text-amber-300">0) {
    String value = new String(buffer, class="text-amber-300">0, len);
    System.out.println(value);
}
class="text-zinc-class="text-amber-300">500">// len == -class="text-amber-300">1 → key not found (no exception thrown)

Multi-dimensional lookup

java
class="text-zinc-class="text-amber-300">500">// Factors in alphabetical column-name order, separated by  internally
byte[] result = client.multiLookup(new byte[][] {
    "us-east-class="text-amber-300">1".getBytes(),
    "enterprise".getBytes(),
});
if (result != null) {
    System.out.println(new String(result));
}

Full scan

java
client.scan((key, value) -> {
    System.out.printf("%s → %s%n",
        new String(key), new String(value));
    return true; class="text-zinc-class="text-amber-300">500">// return false to halt
});

Hot-Swap Lifecycle

AttosSyncService.reload(path) follows the same swap-before-close invariant as the Go SDK:

  1. Open and validate the new file.
  2. AtomicReference.getAndSet(newMap) — publishes the new reference atomically.
  3. Call oldMap.close() only after the swap — safe because all in-flight readers captured the old reference before the exchange.
java
class="text-zinc-class="text-amber-300">500">// Triggered externally (webhook, file-watcher, etc.):
client.reload("/var/lib/attos/data.v2.attos");

If setPollIntervalSeconds(n) is set, AttosClient starts a single daemon thread that calls reload on the last successfully-loaded path every n seconds automatically.

Error Handling

All error conditions surface as subtypes of AttosException (RuntimeException). Lookup methods do not declare checked exceptions.

ExceptionTrigger condition
AttosKeyNotFoundExceptionKey absent (get() returns -1; no exception — use return value)
AttosChecksumExceptionCRC32 mismatch during open (validateChecksum=true)
AttosCorruptedExceptionHeader too small, invalid magic, invalid version, offset OOB
AttosMapClosedExceptionLookup called after shutdown()

Note: get() signals a missing key via return value -1, not an exception, to avoid JVM exception overhead on the hot path.

java
try (AttosClient client = new AttosClient(file, config)) {
    int len = client.get(key, class="text-amber-300">0, key.length, buffer);
    if (len < class="text-amber-300">0) {
        class="text-zinc-class="text-amber-300">500">// key not found — handle gracefully
    }
} catch (AttosChecksumException e) {
    log.error("CRC32 mismatch on open: {}", e.getMessage());
} catch (AttosCorruptedException e) {
    log.error("Binary header invalid: {}", e.getMessage());
} catch (AttosMapClosedException e) {
    log.error("Client already shut down");
}

Plan limits

When a sync hits a plan limit (for example more than 3 active nodes or 10 GB of bandwidth a month on Free), the control plane answers 402 (or 429) with a JSON body whose code says which limit. The SDK keeps serving the data it already has, pauses syncing for 5 minutes (or the Retry-After seconds, capped at 1 hour) and then retries; lookups are never affected. See Plans, limits & error codes for every code and how to fix it.

How nodes are counted
  • A node is one running SDK, identified by its node ID. It counts as active while it has synced in the last 5 minutes (SDKs check for updates every 30 seconds), so a stopped server drops out within 5 minutes.
  • The node ID defaults to the server's hostname, so a restarted process is still one node. Set ATTOS_NODE_ID (or the SDK's node ID option) to choose it yourself; processes sharing a hostname count as one node.
  • Paid plans bill extra nodes on the monthly average: the node count is recorded every hour and the month's average (rounded) is compared with the included nodes (50 on Pro, 10 on Starter) when the month closes, at $2 per extra node on Pro ($3 on Starter, where node overage is off unless you switch it on). The peak is shown for information only.
  • Example: a rolling deploy of 50 pods briefly shows about 100 nodes while old and new pods overlap, but only for a few minutes, so the month still averages about 50 and nothing extra is billed.
  • Past the included nodes: with overage on, extra nodes keep syncing and are billed; with it off (and on Free, capped at 3), a new node gets 402 and keeps serving whatever data it already has, while nodes already syncing are unaffected.

Benchmarks

JMH benchmark on JDK 22, linux/amd64, 11th Gen Intel Core i5-1155G7 @ 2.50 GHz with FFM accessor and -XX:+AlwaysPreTouch:

bash
Benchmark                     Mode  Cnt   Score   Error  Units
AttosBenchmark.lookup        avgt   10  ~25.4 ±  0.6  ns/op
AttosBenchmark.multiLookup   avgt   10  ~28.1 ±  0.8  ns/op
                             GC:    0 allocations during steady-state