Async I/O
Run async I/O off the main thread with endstone.asyncio, and return results safely.
Some work - HTTP requests, database queries, anything that waits on the network - would stall the whole server if you ran it on the main thread. endstone.asyncio gives you a background asyncio event loop to run that work on instead.
The loop runs on a dedicated worker thread, separate from the server's main thread. The Endstone API is not thread-safe: calling any of it - players, worlds, the server - from inside a coroutine will corrupt state or crash the server. Treat a coroutine as a sandbox for pure I/O and computation, and hop back to the main thread (via the scheduler) whenever you need to touch the game.
The threading model
submit() moves work onto the worker thread; the scheduler moves the result back:
Run a coroutine
A coroutine is a function defined with async def. Calling it doesn't run the body - it returns a coroutine object that an event loop drives to completion, pausing at each await while it waits on slow work. submit() hands that coroutine object to the background loop and returns immediately, so the server keeps ticking while the work is in flight:
import aiohttp
import endstone.asyncio
from endstone.plugin import Plugin
class MyPlugin(Plugin):
api_version = "0.11"
def on_enable(self) -> None:
endstone.asyncio.submit(self.fetch_python_org())
async def fetch_python_org(self) -> None:
async with aiohttp.ClientSession() as session:
async with session.get("https://www.python.org") as response:
html = await response.text()
print("Body:", html[:15], "...") # plain Python is fine off-thread
# we're on the worker thread here - do NOT call the Endstone API(aiohttp here is just an example async HTTP client - add whatever async library you need as a dependency.)
Return results to the main thread
To act on the result - broadcasting it, storing it on a player - you have to be back on the main thread. The scheduler is the bridge: run_task queues a callback to run on the main thread on the next tick, and it's safe to call from the background thread.
import aiohttp
import endstone.asyncio
from endstone.plugin import Plugin
class MyPlugin(Plugin):
api_version = "0.11"
def on_enable(self) -> None:
endstone.asyncio.submit(self.fetch_python_org())
async def fetch_python_org(self) -> None:
async with aiohttp.ClientSession() as session:
async with session.get("https://www.python.org") as response:
html = await response.text()
# hop back to the main thread before touching the API
body = html[:15]
self.server.scheduler.run_task(
self, lambda: self.server.broadcast_message(f"Body: {body}...")
)run_task only schedules the call - the lambda itself runs on the main thread, where broadcast_message is safe.
Don't block waiting for a result
submit() returns a concurrent.futures.Future. It's tempting to call future.result() to read the value, but that blocks the calling thread until the coroutine finishes - on the main thread, that freezes the entire server. Never do this:
# WRONG - freezes the server until the request completes
future = endstone.asyncio.submit(self.fetch_python_org())
future.result()Let the coroutine schedule its own follow-up instead, as above. If you'd rather keep the scheduling out of the coroutine, have it return the value and attach a callback to the future - the callback still runs on the worker thread, so it has to bounce through the scheduler too:
def on_enable(self) -> None:
future = endstone.asyncio.submit(self.fetch_python_org())
future.add_done_callback(
lambda f: self.server.scheduler.run_task(
self, lambda: self.server.broadcast_message(f"Body: {f.result()}...")
)
)
async def fetch_python_org(self) -> str:
async with aiohttp.ClientSession() as session:
async with session.get("https://www.python.org") as response:
return (await response.text())[:15]The event loop
endstone.asyncio.get_loop() returns the background loop, starting it on first use, in case you need to drive it directly - for example, to integrate another async library. Most plugins only ever need submit().