Main Thread Safety
Bukkit state may only be read on the server's main thread, so anything doing I/O snapshots first and sends afterwards.
A Bukkit server runs game logic on one thread. Plugin data structures reachable from that logic are not thread-safe, and reading them from a background thread produces corruption that surfaces far from its cause.
Snapshot on, send off
DPC API Faction Sync is the clearest worked example in the codebase, and its class comment states the rule outright. The work is split in two:
collectSnapshot()— runs on the main thread. It walks the faction service and the members lists, and returns an immutableSyncSnapshot.dispatchAsync()— safe from any thread. It serializes the snapshot and hands it to the JDKHttpClient, which does the I/O on its own pool.
The scheduled task is therefore registered with runTaskTimer — the main thread variant — even though its purpose is a network call. This looks backwards until you see the split: the expensive part is already asynchronous inside HttpClient, and scheduling the task asynchronously would only move the unsafe part off the main thread.
The general rule
The pattern generalises to anything the plugin does off-thread: collect an immutable snapshot on the main thread, then act on the snapshot elsewhere. Never hold a reference to live Bukkit or service state across a thread boundary.
Where it also shows up
- Faction Events pass
!server.isPrimaryThreadto the BukkitEventconstructor, so listeners are told truthfully which thread they are on. - Gate checks
isChunkLoadedbefore reading a block, avoiding a synchronous chunk load on the main thread. - The Service Layer caches use
ConcurrentHashMapandCopyOnWriteArrayList, because reads can legitimately arrive from async listeners even when writes do not.
Related
Getting this wrong is not usually a crash — it is a rare, unreproducible inconsistency. That is why the constraint is written into a class comment rather than left implicit.