Caching¶
The SDK ships with a small, in-process, TTL-based cache. It is not a requirement — every Fetch* method bypasses it — but the Get* methods wrap their Fetch* counterpart with a cache lookup so repeated calls within the TTL window return instantly without re-hitting APNIC.
Source: cache.go.
The cache struct¶
classDiagram
class cache {
-mu sync.RWMutex
-ttl time.Duration
-data map[string]cacheEntry
+get(key) (interface{}, bool)
+set(key, data)
}
class cacheEntry {
+data interface{}
+lastUpdated time.Time
}
class Client {
+cache *cache
}
Client --> cache : owns
cache --> cacheEntry : stores by key cache is a sync.RWMutex-guarded map[string]cacheEntry. Each entry holds the cached value (interface{}) and its lastUpdated timestamp. get takes a read lock; set takes a write lock. The TTL is stored on the cache, not per-key, so all keys share the same lifetime.
get returns (data, true) only if the key exists and time.Since(entry.lastUpdated) < ttl. An expired entry is treated as a miss (it is not proactively evicted; it is overwritten on the next set).
Get* vs Fetch*¶
This is the central distinction in the SDK's public API:
| Method family | Cache behavior | When to use |
|---|---|---|
Get* (e.g. GetDelegatedEntries) | Checks cache; on hit returns immediately. On miss, calls the Fetch* counterpart, caches the result, and returns it. | Repeated reads of slowly-changing data within a session. |
Fetch* (e.g. FetchDelegatedEntries) | Always hits the network. Never reads from or writes to the cache. | When freshness matters more than latency. |
Fetch* methods are the building blocks; Get* methods are the cached convenience wrappers. The cache lookup and population is the only thing Get* adds.
flowchart TD
Call["caller invokes GetX(ctx)"] --> Lookup["cache.get(key)"]
Lookup --> Hit{"entry exists<br/>AND not expired?"}
Hit -->|"yes"| ReturnCached["return cached value<br/>(no network)"]
Hit -->|"no"| Fetch["call FetchX(ctx)<br/>(network)"]
Fetch --> FetchOk{"err?"}
FetchOk -->|"err"| RetErr["return err<br/>(nothing cached)"]
FetchOk -->|"ok"| Store["cache.set(key, result)"]
Store --> ReturnFresh["return result"] Note the failure path: if Fetch* errors, nothing is cached, so the next Get* call retries the network. There is no negative caching.
Cache Keys¶
Keys are package-level string constants, one per data type:
| Key constant | Value | Method |
|---|---|---|
cacheKeyDelegated | "delegated" | GetDelegatedEntries |
cacheKeyExtended | "extended" | GetExtendedEntries |
cacheKeyAssigned | "assigned" | GetAssignedEntries |
cacheKeyIPv6Assigned | "ipv6-assigned" | GetIPv6AssignedEntries |
cacheKeyLegacy | "legacy" | GetLegacyEntries |
cacheKeyTransfers | "transfers" | GetTransfers |
cacheKeyChanges | "changes" | GetChanges |
cacheKeyIRR(objType) | "irr:" + objType | GetIRRDatabase |
IRR is the only keyed-by-parameter cache: each object type (inetnum, aut-num, route, ...) gets its own entry, so GetIRRDatabase(ctx, "inetnum") and GetIRRDatabase(ctx, "route") do not collide.
Data types without a Get* variant — RDAP lookups, REx, RRDP, BGP thyme files, whois — are always fetched fresh. This is deliberate: those are point-in-time or query-parameterized lookups where staleness is rarely acceptable and caching by parameter would balloon memory.
TTL Control¶
WithCacheTTL(ttl) sets the cache's TTL. The default is 30 * time.Minute.
| Value | Effect |
|---|---|
30 * time.Minute (default) | Entries live 30 minutes. |
| Any positive duration | Entries live that long. |
0 | Entries always expire (since time.Since(lastUpdated) >= 0 is always true), so Get* always misses and behaves like Fetch*. This effectively disables caching without removing the wrapper. |
There is no negative TTL or "disable" flag; 0 is the disable sentinel.
flowchart LR
New["NewClient()"] --> Def["cache.ttl = 30min"]
Def --> Opt{"WithCacheTTL(d)?"}
Opt -->|"yes"| Set["cache.ttl = d"]
Opt -->|"no"| Keep["keep 30min"]
Set --> Ready["client.cache ready"]
Keep --> Ready Concurrency¶
The sync.RWMutex makes the cache safe for concurrent use across goroutines. get uses RLock (multiple concurrent readers); set uses Lock (exclusive). The common case — many Get* reads after a single populate — runs fully parallel under the read lock.
Because the cache is per-Client and the SDK is typically used with one shared Client, the cache is effectively a process-wide singleton for that client's data. Two goroutines calling GetDelegatedEntries simultaneously on a cold cache will both miss and both fetch; the second set simply overwrites the first. If you want to avoid the duplicate fetch, use a singleflight wrapper at the call site — the SDK does not do it internally.
What Gets Cached¶
The cache stores the parsed result, not the raw bytes. GetDelegatedEntries caches []DelegatedEntry; GetTransfers caches *TransfersResult; GetIRRDatabase caches *IRRDatabase. This means a cache hit skips not only the network but also the parser — important for large files where parsing is a meaningful fraction of total latency.
graph LR
subgraph Cold["Cold cache (miss)"]
direction LR
Net1["network fetch<br/>(chunked download)"] --> Parse1["parseXFull<br/>(scanner / decoder)"] --> Store1["cache.set"]
end
subgraph Warm["Warm cache (hit)"]
direction LR
Hit1["cache.get"] --> Return1["return parsed value<br/>— no network, no parse"]
end
Cold --> Warm Interaction with Chunked Download¶
Because Get* calls Fetch* which calls fetchReader which may chunk, a cold-cache Get* for a large file pays the full chunked-download + parse cost once, then serves subsequent calls from memory for the TTL window. The chunked-download configuration (WithMaxConcurrentDownloads, etc.) affects only the cold path; warm hits are pure map lookups.
Example¶
client := apnic.NewClient(
apnic.WithCacheTTL(10 * time.Minute), // default 30min
)
// First call: network + parse, then cached.
entries, err := client.GetDelegatedEntries(ctx)
// Within 10 minutes: instant cache hit, no network.
again, err := client.GetDelegatedEntries(ctx)
// Bypass the cache for a guaranteed-fresh read.
fresh, err := client.FetchDelegatedEntries(ctx)
Next¶
- Parser Design — what runs on the cold path between the network fetch and the cache
set. - HTTP Client — the transport beneath every
Fetch*.