Astral Realms Documentation Help

Catalog Import & Resolve Worker

The master module (com.astralrealms.heads.AstralHeads extends MageHicPlugin, a headless MageHic platform plugin — not a Bukkit/Paper plugin) is the background process that keeps the shared heads catalog populated. It has no commands, actions, or placeholders; it exists purely to run two services started from onEnable: ImportService and ResolveService.

Pipeline

ImportService (every pull-interval) downloads config.yml's `url` (a CSV: name, category, hash, tags) to <data>/heads.csv ▼ parses it (RFC-4180 CSV, one HASH-type row per line) ▼ bulk-inserts each row into queued_heads (type=HASH), skipping rows whose deterministic id already exists in `heads`, and leaving already-queued rows (PENDING/PROCESSING/FAILED) untouched ▼ ResolveService (every worker.pump-interval, started alongside ImportService) on startup: requeues any PROCESSING row older than worker.stale-timeout (crash recovery) ▼ claims up to worker.batch-size PENDING rows whose next_attempt_at <= NOW() (bounded by worker.max-in-flight in-flight resolves at a time) ▼ dispatches each claimed row to the resolver for its lookup_type (NAME / UUID / HASH), each gated by its own per-endpoint rate limiter ▼ on success: writes a HeadEntity into `heads`, deletes the queued_heads row on failure: marks FAILED (terminal) or reschedules with exponential backoff (worker.base-backoff * 2^attempts, capped at worker.max-backoff), unless attempts >= worker.max-attempts (then terminal regardless)

Only HASH-type rows are ever produced by ImportService today (the CSV import path); NAME/UUID rows — resolving a specific player's skin on demand — would need to be inserted into queued_heads by some other caller, but no such caller exists in this codebase.

Data model

master/src/main/resources/schema.sql defines both tables (shared with the paper module — see Overview — data model for heads).

queued_heads

Mapped to QueuedHead (@Entity("queued_heads")).

Column

Field

Notes

id

uniqueId

Primary key.

lookup_type

type (QueuedHead.Type)

NAME, UUID, or HASH — a CHECK constraint enforces exactly one of username/uuid/texture_hash is set to match.

username/uuid/texture_hash

The lookup key, one populated per lookup_type. Each has a UNIQUE index, collapsing duplicate requests for the same key.

name/category/tags

Catalog metadata carried through to heads on resolve (set by ImportService for HASH rows).

priority

priority

Higher claims sooner; always 0 from ImportService.

status

status (QueuedHead.Status)

PENDINGPROCESSING (claimed, SELECT ... FOR UPDATE SKIP LOCKED) → deleted on success, or FAILED (terminal; acts as a negative cache during its backoff window).

attempts

attempts

Incremented per failed resolve.

last_error

lastError

Truncated to 255 chars.

next_attempt_at

nextAttemptAt

Claim gate for backoff; defaults to NOW().

Resolvers

ResolveService holds one HeadResolver per QueuedHead.Type, each acquiring rate-limit tokens (unbounded wait — only the network call itself is timed by worker.request-timeout) before calling out:

Type

Resolver

Endpoint(s)

Behavior

NAME

NameResolver

api.minecraftservices.com (name→UUID) then session server (profile)

Wraps MojangAPI.findByUsername, then HeadFactory::player.

UUID

UuidResolver

Session server only

Wraps MojangAPI.findByUniqueId, then HeadFactory::player.

HASH

HashResolver

textures.minecraft.net/texture/<hash> (CDN, HEAD-style existence check)

Issues a GET (following redirects) purely to validate the hash exists; on 200 builds the head locally from the hash via HeadFactory::catalog (no further Mojang call needed) — a 404/400/410 is a terminal failure, anything else is retried.

ResolveException#terminal() distinguishes a permanent failure (row will never succeed — marked FAILED immediately, acting as a negative cache) from a transient one (network blip, 5xx, timeout — retried with backoff). ResolveException.from(...) also infers terminal from the message text for exceptions raised outside a resolver (matching 204/400/404/410).

Rate limiting

RateLimiter enforces a separate requests-per-minute budget per Endpoint (SESSIONSERVER, NAME_LOOKUP, TEXTURE_CDN), configured under worker.rate-limits. The shipped defaults are already halved from Mojang's documented public limits as a safety margin (see Configuration below).

Configuration

The master module loads its own database.properties (pointing at the same heads schema — note it also sets useBulkStmts=false, unlike the paper module's database.properties) and config.yml, into HeadsConfiguration (a distinct record from the paper module's config of the same name).

url: "https://cdn.jsdelivr.net/gh/TheLuca98/MinecraftHeads@master/heads.csv" pull-interval: "2d" worker: pump-interval: "1s" batch-size: 100 max-in-flight: 200 max-attempts: 5 base-backoff: "1m" max-backoff: "6h" request-timeout: "30s" stale-timeout: "15m" rate-limits: sessionserver-per-minute: 100 name-lookup-per-minute: 30 texture-cdn-per-minute: 300

Key

Type

Default

Description

url

String

(none — required)

The CSV catalog URL ImportService downloads (name,category,hash,tags header, \|-separated tags).

pull-interval

Duration

(none — required)

How often ImportService re-downloads and re-imports the CSV.

worker.pump-interval

Duration

1s

How often ResolveService claims a new batch.

worker.batch-size

int

100

Rows claimed per tick (also capped by remaining max-in-flight capacity).

worker.max-in-flight

int

200

Ceiling on concurrently-resolving rows.

worker.max-attempts

int

5

Attempts before a row is marked FAILED regardless of its failure's own terminal flag.

worker.base-backoff

Duration

1m

Retry backoff base; doubles per attempt.

worker.max-backoff

Duration

6h

Retry backoff ceiling.

worker.request-timeout

Duration

30s

Per-request timeout before a resolve attempt is treated as failed (not counted against the rate-limit wait).

worker.stale-timeout

Duration

15m

PROCESSING rows older than this are requeued to PENDING on startup (crash recovery).

worker.rate-limits.sessionserver-per-minute

int

100

Budget for UUID/NAME profile lookups (session server).

worker.rate-limits.name-lookup-per-minute

int

30

Budget for username→UUID lookups.

worker.rate-limits.texture-cdn-per-minute

int

300

Budget for texture-CDN existence checks.

Every worker.* field falls back to its default independently (WorkerSettings) when absent or non-positive, so a partial or missing worker: block never disables the worker — it just runs with defaults.

Last modified: 25 July 2026