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+.
<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.
| Setter | Default | Effect |
|---|---|---|
| setValidateChecksum(true) | false | Verifies CRC32-IEEE trailer on open. Streams entire file; one-time cost. |
| setPinToMemory(true) | false | Calls 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 loggers | Injects a caller-supplied SLF4J Logger for all SDK operational messages. |
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:
- Attempt to load
dev.attos.sdk.ffm.FfmMemoryAccessor— available only on JDK 22+ via the MRJARMETA-INF/versions/22layer. - If that fails, load
dev.attos.sdk.unsafe.UnsafeMemoryAccessor(works on JDK 17–21 viasun.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
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
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
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:
- Open and validate the new file.
AtomicReference.getAndSet(newMap)— publishes the new reference atomically.- Call
oldMap.close()only after the swap — safe because all in-flight readers captured the old reference before the exchange.
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.
| Exception | Trigger condition |
|---|---|
| AttosKeyNotFoundException | Key absent (get() returns -1; no exception — use return value) |
| AttosChecksumException | CRC32 mismatch during open (validateChecksum=true) |
| AttosCorruptedException | Header too small, invalid magic, invalid version, offset OOB |
| AttosMapClosedException | Lookup 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.
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.
- 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
402and 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:
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