WebHost: Update UTC datetime usage (timezone-naive) (#4906)

This commit is contained in:
josephwhite
2026-03-10 13:57:48 -04:00
committed by GitHub
parent c3659fb3ef
commit 4b37283d22
8 changed files with 42 additions and 24 deletions

View File

@@ -18,6 +18,8 @@ import logging
import warnings
from argparse import Namespace
from datetime import datetime, timezone
from settings import Settings, get_settings
from time import sleep
from typing import BinaryIO, Coroutine, Optional, Set, Dict, Any, Union, TypeGuard
@@ -1291,6 +1293,15 @@ def is_iterable_except_str(obj: object) -> TypeGuard[typing.Iterable[typing.Any]
return isinstance(obj, typing.Iterable)
def utcnow() -> datetime:
"""
Implementation of Python's datetime.utcnow() function for use after deprecation.
Needed for timezone-naive UTC datetimes stored in databases with PonyORM (upstream).
https://ponyorm.org/ponyorm-list/2014-August/000113.html
"""
return datetime.now(timezone.utc).replace(tzinfo=None)
class DaemonThreadPoolExecutor(concurrent.futures.ThreadPoolExecutor):
"""
ThreadPoolExecutor that uses daemonic threads that do not keep the program alive.

View File

@@ -4,14 +4,14 @@ import json
import logging
import multiprocessing
import typing
from datetime import timedelta, datetime
from datetime import timedelta
from threading import Event, Thread
from typing import Any
from uuid import UUID
from pony.orm import db_session, select, commit, PrimaryKey
from Utils import restricted_loads
from Utils import restricted_loads, utcnow
from .locker import Locker, AlreadyRunningException
_stop_event = Event()
@@ -129,10 +129,10 @@ def autohost(config: dict):
with db_session:
rooms = select(
room for room in Room if
room.last_activity >= datetime.utcnow() - timedelta(days=3))
room.last_activity >= utcnow() - timedelta(days=3))
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):
if room.last_activity >= utcnow() - timedelta(seconds=room.timeout + 5):
hosters[room.id.int % len(hosters)].start_room(room.id)
except AlreadyRunningException:

View File

@@ -172,7 +172,7 @@ class WebHostContext(Context):
room.multisave = pickle.dumps(self.get_save())
# saving only occurs on activity, so we can "abuse" this information to mark this as last_activity
if not exit_save: # we don't want to count a shutdown as activity, which would restart the server again
room.last_activity = datetime.datetime.utcnow()
room.last_activity = Utils.utcnow()
return True
def get_save(self) -> dict:
@@ -367,8 +367,7 @@ def run_server_process(name: str, ponyconfig: dict, static_server_data: dict,
with db_session:
# 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)
room.last_activity = Utils.utcnow() - datetime.timedelta(minutes=1, seconds=room.timeout)
del room
tear_down_logging(room_id)
logging.info(f"Shutting down room {room_id} on {name}.")

View File

@@ -1,8 +1,9 @@
from datetime import timedelta, datetime
from datetime import timedelta
from flask import render_template
from pony.orm import count
from Utils import utcnow
from WebHostLib import app, cache
from .models import Room, Seed
@@ -10,6 +11,6 @@ from .models import Room, Seed
@app.route('/', methods=['GET', 'POST'])
@cache.cached(timeout=300) # cache has to appear under app route for caching to work
def landing():
rooms = count(room for room in Room if room.creation_time >= datetime.utcnow() - timedelta(days=7))
seeds = count(seed for seed in Seed if seed.creation_time >= datetime.utcnow() - timedelta(days=7))
rooms = count(room for room in Room if room.creation_time >= utcnow() - timedelta(days=7))
seeds = count(seed for seed in Seed if seed.creation_time >= utcnow() - timedelta(days=7))
return render_template("landing.html", rooms=rooms, seeds=seeds)

View File

@@ -9,11 +9,12 @@ from flask import request, redirect, url_for, render_template, Response, session
from pony.orm import count, commit, db_session
from werkzeug.utils import secure_filename
from worlds.AutoWorld import AutoWorldRegister, World
from . import app, cache
from .markdown import render_markdown
from .models import Seed, Room, Command, UUID, uuid4
from Utils import title_sorted
from Utils import title_sorted, utcnow
class WebWorldTheme(StrEnum):
DIRT = "dirt"
@@ -233,11 +234,12 @@ def host_room(room: UUID):
if room is None:
return abort(404)
now = datetime.datetime.utcnow()
now = 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))
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)
)
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
# due to "pony.orm.core.OptimisticCheckError: Object Room was updated outside of current transaction"

View File

@@ -2,6 +2,8 @@ from datetime import datetime
from uuid import UUID, uuid4
from pony.orm import Database, PrimaryKey, Required, Set, Optional, buffer, LongStr
from Utils import utcnow
db = Database()
STATE_QUEUED = 0
@@ -20,8 +22,8 @@ class Slot(db.Entity):
class Room(db.Entity):
id = PrimaryKey(UUID, default=uuid4)
last_activity = Required(datetime, default=lambda: datetime.utcnow(), index=True)
creation_time = Required(datetime, default=lambda: datetime.utcnow(), index=True) # index used by landing page
last_activity: datetime = Required(datetime, default=lambda: utcnow(), index=True)
creation_time: datetime = Required(datetime, default=lambda: utcnow(), index=True) # index used by landing page
owner = Required(UUID, index=True)
commands = Set('Command')
seed = Required('Seed', index=True)
@@ -38,7 +40,7 @@ class Seed(db.Entity):
rooms = Set(Room)
multidata = Required(bytes, lazy=True)
owner = Required(UUID, index=True)
creation_time = Required(datetime, default=lambda: datetime.utcnow(), index=True) # index used by landing page
creation_time: datetime = Required(datetime, default=lambda: utcnow(), index=True) # index used by landing page
slots = Set(Slot)
spoiler = Optional(LongStr, lazy=True)
meta = Required(LongStr, default=lambda: "{\"race\": false}") # additional meta information/tags

View File

@@ -10,7 +10,7 @@ from werkzeug.exceptions import abort
from MultiServer import Context, get_saving_second
from NetUtils import ClientStatus, Hint, NetworkItem, NetworkSlot, SlotType
from Utils import restricted_loads, KeyedDefaultDict
from Utils import restricted_loads, KeyedDefaultDict, utcnow
from . import app, cache
from .models import GameDataPackage, Room
@@ -273,9 +273,10 @@ class TrackerData:
Does not include players who have no activity recorded.
"""
last_activity: Dict[TeamPlayer, datetime.timedelta] = {}
now = datetime.datetime.utcnow()
now = utcnow()
for (team, player), timestamp in self._multisave.get("client_activity_timers", []):
last_activity[team, player] = now - datetime.datetime.utcfromtimestamp(timestamp)
from_timestamp = datetime.datetime.fromtimestamp(timestamp, datetime.timezone.utc).replace(tzinfo=None)
last_activity[team, player] = now - from_timestamp
return last_activity

View File

@@ -6,6 +6,7 @@ import zipfile
from pathlib import Path
from typing import TYPE_CHECKING, Iterable, Optional, cast
from Utils import utcnow
from WebHostLib import to_python
if TYPE_CHECKING:
@@ -133,7 +134,7 @@ def stop_room(app_client: "FlaskClient",
room_id: str,
timeout: Optional[float] = None,
simulate_idle: bool = True) -> None:
from datetime import datetime, timedelta
from datetime import timedelta
from time import sleep
from pony.orm import db_session
@@ -151,10 +152,11 @@ def stop_room(app_client: "FlaskClient",
with db_session:
room: Room = Room.get(id=room_uuid)
now = utcnow()
if simulate_idle:
new_last_activity = datetime.utcnow() - timedelta(seconds=room.timeout + 5)
new_last_activity = now - timedelta(seconds=room.timeout + 5)
else:
new_last_activity = datetime.utcnow() - timedelta(days=3)
new_last_activity = now - timedelta(days=3)
room.last_activity = new_last_activity
address = f"localhost:{room.last_port}" if room.last_port > 0 else None
if address: