WebHost: turn Room.timeout from a database column into a flask app config field

This commit is contained in:
Fabian Dill
2025-12-01 23:52:13 +01:00
parent ac84b272c5
commit a79f71ad8b
9 changed files with 15 additions and 35 deletions

View File

@@ -26,6 +26,7 @@ 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,6 +38,5 @@ 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,7 +16,6 @@ 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,16 +124,14 @@ 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() - timedelta(days=3))
room.last_activity >= datetime.utcnow() - activity_timedelta)
for room in rooms:
# 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)
hosters[room.id.int % len(hosters)].start_room(room.id)
except AlreadyRunningException:
logging.info("Autohost reports as already running, not starting another.")
@@ -187,6 +185,7 @@ 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}"
@@ -198,7 +197,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.rooms_to_start, self.rooms_shutting_down, self.room_idle_timer),
name=self.name)
process.start()
self.process = process

View File

@@ -231,7 +231,8 @@ 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):
host: str, rooms_to_run: multiprocessing.Queue, rooms_shutting_down: multiprocessing.Queue,
room_idle_timeout: int):
from setproctitle import setproctitle
setproctitle(name)
@@ -316,7 +317,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.get(id=room_id).timeout
ctx.auto_shutdown = room_idle_timeout
if ctx.saving:
setattr(asyncio.current_task(), "save", lambda: ctx._save(True))
assert ctx.shutdown_task is None
@@ -347,7 +348,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.timeout)
datetime.timedelta(minutes=1, seconds=room_idle_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=room.timeout))
or room.last_activity < now - datetime.timedelta(seconds=app.config["ROOM_IDLE_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,7 +27,6 @@ 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 {{ room.timeout//60//60 }} hours of inactivity.
The server for this room will be paused after {{ config["ROOM_IDLE_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

@@ -19,7 +19,6 @@ __all__ = [
"create_room",
"start_room",
"stop_room",
"set_room_timeout",
"get_multidata_for_room",
"set_multidata_for_room",
"stop_autogen",
@@ -139,7 +138,6 @@ 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
@@ -152,14 +150,12 @@ 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=room.timeout + 5)
new_last_activity = datetime.utcnow() - timedelta(seconds=2 * 60 * 60 + 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:
@@ -186,28 +182,13 @@ 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:
@@ -219,7 +200,6 @@ 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:
@@ -258,9 +238,11 @@ 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)