Compare commits

..

3 Commits

Author SHA1 Message Date
NewSoupVi
9657f92932 Update docs/world maintainer.md
Co-authored-by: RoobyRoo <thegreenrobby@gmail.com>
2026-02-09 00:06:59 +01:00
NewSoupVi
35e667791f Update docs/world maintainer.md
Co-authored-by: Duck <31627079+duckboycool@users.noreply.github.com>
2026-02-08 20:49:03 +01:00
NewSoupVi
ce7c54ef9d Docs: Add ability for world maintainers to opt into allowing core to merge small PRs without their approval 2025-12-02 00:14:15 +01:00
10 changed files with 58 additions and 15 deletions

View File

@@ -26,7 +26,6 @@ app.jinja_env.filters['get_file_safe_name'] = get_file_safe_name
app.config["SELFHOST"] = True # application process is in charge of running the websites
app.config["GENERATORS"] = 8 # maximum concurrent world gens
app.config["HOSTERS"] = 8 # maximum concurrent room hosters
app.config["ROOM_IDLE_TIMEOUT"] = 2 * 60 * 60 # seconds of idle before a Room spins down
app.config["SELFLAUNCH"] = True # application process is in charge of launching Rooms.
app.config["SELFLAUNCHCERT"] = None # can point to a SSL Certificate to encrypt Room websocket connections
app.config["SELFLAUNCHKEY"] = None # can point to a SSL Certificate Key to encrypt Room websocket connections

View File

@@ -38,5 +38,6 @@ def room_info(room_id: UUID) -> Dict[str, Any]:
"players": get_players(room.seed),
"last_port": room.last_port,
"last_activity": room.last_activity,
"timeout": room.timeout,
"downloads": downloads,
}

View File

@@ -16,6 +16,7 @@ def get_rooms():
"creation_time": room.creation_time,
"last_activity": room.last_activity,
"last_port": room.last_port,
"timeout": room.timeout,
"tracker": to_url(room.tracker),
})
return jsonify(response)

View File

@@ -124,14 +124,16 @@ def autohost(config: dict):
hoster = MultiworldInstance(config, x)
hosters.append(hoster)
hoster.start()
activity_timedelta = timedelta(seconds=config["ROOM_IDLE_TIMEOUT"] + 5)
while not stop_event.wait(0.1):
with db_session:
rooms = select(
room for room in Room if
room.last_activity >= datetime.utcnow() - activity_timedelta)
room.last_activity >= datetime.utcnow() - timedelta(days=3))
for room in rooms:
hosters[room.id.int % len(hosters)].start_room(room.id)
# we have to filter twice, as the per-room timeout can't currently be PonyORM transpiled.
if room.last_activity >= datetime.utcnow() - timedelta(seconds=room.timeout + 5):
hosters[room.id.int % len(hosters)].start_room(room.id)
except AlreadyRunningException:
logging.info("Autohost reports as already running, not starting another.")
@@ -185,7 +187,6 @@ class MultiworldInstance():
self.cert = config["SELFLAUNCHCERT"]
self.key = config["SELFLAUNCHKEY"]
self.host = config["HOST_ADDRESS"]
self.room_idle_timer = config["ROOM_IDLE_TIMEOUT"]
self.rooms_to_start = multiprocessing.Queue()
self.rooms_shutting_down = multiprocessing.Queue()
self.name = f"MultiHoster{id}"
@@ -197,7 +198,7 @@ class MultiworldInstance():
process = multiprocessing.Process(group=None, target=run_server_process,
args=(self.name, self.ponyconfig, get_static_server_data(),
self.cert, self.key, self.host,
self.rooms_to_start, self.rooms_shutting_down, self.room_idle_timer),
self.rooms_to_start, self.rooms_shutting_down),
name=self.name)
process.start()
self.process = process

View File

@@ -231,8 +231,7 @@ def set_up_logging(room_id) -> logging.Logger:
def run_server_process(name: str, ponyconfig: dict, static_server_data: dict,
cert_file: typing.Optional[str], cert_key_file: typing.Optional[str],
host: str, rooms_to_run: multiprocessing.Queue, rooms_shutting_down: multiprocessing.Queue,
room_idle_timeout: int):
host: str, rooms_to_run: multiprocessing.Queue, rooms_shutting_down: multiprocessing.Queue):
from setproctitle import setproctitle
setproctitle(name)
@@ -317,7 +316,7 @@ def run_server_process(name: str, ponyconfig: dict, static_server_data: dict,
else:
ctx.logger.exception("Could not determine port. Likely hosting failure.")
with db_session:
ctx.auto_shutdown = room_idle_timeout
ctx.auto_shutdown = Room.get(id=room_id).timeout
if ctx.saving:
setattr(asyncio.current_task(), "save", lambda: ctx._save(True))
assert ctx.shutdown_task is None
@@ -348,7 +347,7 @@ def run_server_process(name: str, ponyconfig: dict, static_server_data: dict,
# ensure the Room does not spin up again on its own, minute of safety buffer
room = Room.get(id=room_id)
room.last_activity = datetime.datetime.utcnow() - \
datetime.timedelta(minutes=1, seconds=room_idle_timeout)
datetime.timedelta(minutes=1, seconds=room.timeout)
del room
logging.info(f"Shutting down room {room_id} on {name}.")
finally:

View File

@@ -231,7 +231,7 @@ def host_room(room: UUID):
now = datetime.datetime.utcnow()
# indicate that the page should reload to get the assigned port
should_refresh = ((not room.last_port and now - room.creation_time < datetime.timedelta(seconds=3))
or room.last_activity < now - datetime.timedelta(seconds=app.config["ROOM_IDLE_TIMEOUT"]))
or room.last_activity < now - datetime.timedelta(seconds=room.timeout))
if now - room.last_activity > datetime.timedelta(minutes=1):
# we only set last_activity if needed, otherwise parallel access on /room will cause an internal server error

View File

@@ -27,6 +27,7 @@ class Room(db.Entity):
seed = Required('Seed', index=True)
multisave = Optional(buffer, lazy=True)
show_spoiler = Required(int, default=0) # 0 -> never, 1 -> after completion, -> 2 always
timeout = Required(int, default=lambda: 2 * 60 * 60) # seconds since last activity to shutdown
tracker = Optional(UUID, index=True)
# Port special value -1 means the server errored out. Another attempt can be made with a page refresh
last_port = Optional(int, default=lambda: 0)

View File

@@ -29,7 +29,7 @@
and a <a href="{{ url_for("get_multiworld_sphere_tracker", tracker=room.tracker) }}">Sphere Tracker</a> enabled.
<br />
{% endif %}
The server for this room will be paused after {{ config["ROOM_IDLE_TIMEOUT"]//60//60 }} hours of inactivity.
The server for this room will be paused after {{ room.timeout//60//60 }} hours of inactivity.
Should you wish to continue later,
anyone can simply refresh this page and the server will resume.<br>
{% if room.last_port == -1 %}

View File

@@ -21,6 +21,29 @@ Unless these are shared between multiple people, we expect the following from ea
of development.
* Let us know of long periods of unavailability.
## Authority
For a Pull Request into a world to be merged, one of the world maintainers of that world has to approve it.
This applies to all Pull Requests, no matter how small, with the sole exception of patching security vulnerabilities.
World maintainers can partially opt out of this,
allowing core maintainers to merge pull requests which they deem critical and "obvious" enough.
There is no one singular definition of what Pull Requests fit this criteria -
You are trusting the core maintainers of Archipelago to be reasonable about their judgement.
Some examples of Pull Requests like this include:
- Fixing a broken link in documentation
- Correcting a typo
- Fixing a crash where the intent of the code is obvious (e.g. an indentation error due to typing 3 spaces instead of 4)
To do this, they can add a comment in [CODEOWNERS](./CODEOWNERS) under their game:
```
# APQuest
# Core is allowed to merge some types of PRs without my approval as described in "world maintainer.md"
/worlds/apquest/ @NewSoupVi
```
## Becoming a World Maintainer
### Adding a World

View File

@@ -19,6 +19,7 @@ __all__ = [
"create_room",
"start_room",
"stop_room",
"set_room_timeout",
"get_multidata_for_room",
"set_multidata_for_room",
"stop_autogen",
@@ -138,6 +139,7 @@ def stop_room(app_client: "FlaskClient",
from pony.orm import db_session
from WebHostLib.models import Command, Room
from WebHostLib import app
poll_interval = 2
@@ -150,12 +152,14 @@ def stop_room(app_client: "FlaskClient",
with db_session:
room: Room = Room.get(id=room_uuid)
if simulate_idle:
new_last_activity = datetime.utcnow() - timedelta(seconds=2 * 60 * 60 + 5)
new_last_activity = datetime.utcnow() - timedelta(seconds=room.timeout + 5)
else:
new_last_activity = datetime.utcnow() - timedelta(days=3)
room.last_activity = new_last_activity
address = f"localhost:{room.last_port}" if room.last_port > 0 else None
if address:
original_timeout = room.timeout
room.timeout = 1 # avoid spinning it up again
Command(room=room, commandtext="/exit")
try:
@@ -182,13 +186,28 @@ def stop_room(app_client: "FlaskClient",
room = Room.get(id=room_uuid)
room.last_port = 0 # easier to detect when the host is up this way
if address:
room.timeout = original_timeout
room.last_activity = new_last_activity
print("timeout restored")
def set_room_timeout(room_id: str, timeout: float) -> None:
from pony.orm import db_session
from WebHostLib.models import Room
from WebHostLib import app
room_uuid = to_python(room_id)
with db_session:
room: Room = Room.get(id=room_uuid)
room.timeout = timeout
def get_multidata_for_room(webhost_client: "FlaskClient", room_id: str) -> bytes:
from pony.orm import db_session
from WebHostLib.models import Room
from WebHostLib import app
room_uuid = to_python(room_id)
with db_session:
@@ -200,6 +219,7 @@ def set_multidata_for_room(webhost_client: "FlaskClient", room_id: str, data: by
from pony.orm import db_session
from WebHostLib.models import Room
from WebHostLib import app
room_uuid = to_python(room_id)
with db_session:
@@ -238,11 +258,9 @@ def _stop_webhost_mp(name_filter: str, graceful: bool = True) -> None:
proc.kill()
proc.join()
def stop_autogen(graceful: bool = True) -> None:
# FIXME: this name filter is jank, but there seems to be no way to add a custom prefix for a Pool
_stop_webhost_mp("SpawnPoolWorker-", graceful)
def stop_autohost(graceful: bool = True) -> None:
_stop_webhost_mp("MultiHoster", graceful)