11import os
22
3- from datetime import datetime
3+ from datetime import datetime , timedelta
44from sqlalchemy import create_engine , Column , Integer , String , DateTime , Boolean
55from sqlalchemy .orm import declarative_base , sessionmaker
66from sqlalchemy .sql import func , text
77
8+ import orjson
9+
810engine = create_engine (os .getenv ('POSTGRES_URL' ), pool_recycle = 3600 , pool_pre_ping = True )
911
1012Base = declarative_base ()
@@ -18,6 +20,24 @@ class SavedPackageData(Base):
1820 created_at = Column (DateTime , nullable = False , default = func .now ())
1921 updated_at = Column (DateTime , nullable = False , onupdate = func .now (), default = func .now ())
2022
23+ class CachedDiscordUser (Base ):
24+ """Cache of upstream /user payloads (diswho or Discord bot API).
25+
26+ Discord usernames + avatars don't move often; the upstream is rate-
27+ limited (429s on bursts) so a small Postgres-backed cache absorbs
28+ the per-package fan-out and stops us hammering Discord every time
29+ a user opens a heavy package.
30+ """
31+ __tablename__ = 'cached_discord_user'
32+
33+ user_id = Column (String (255 ), primary_key = True )
34+ payload = Column (String , nullable = False ) # JSON-serialized upstream response
35+ cached_at = Column (DateTime , nullable = False , default = func .now ())
36+
37+
38+ CACHED_DISCORD_USER_TTL_HOURS = 24
39+
40+
2141class PackageProcessStatus (Base ):
2242 __tablename__ = 'package_process_status'
2343
@@ -93,6 +113,33 @@ def fetch_package_status(package_id, session):
93113 status = session .query (PackageProcessStatus ).filter_by (package_id = package_id ).order_by (PackageProcessStatus .created_at .desc ()).first ()
94114 return status
95115
116+ def get_cached_discord_user (user_id , session ):
117+ """Return the cached upstream user dict, or None if absent or stale."""
118+ row = session .query (CachedDiscordUser ).filter_by (user_id = user_id ).first ()
119+ if not row :
120+ return None
121+ if datetime .now () - row .cached_at > timedelta (hours = CACHED_DISCORD_USER_TTL_HOURS ):
122+ return None
123+ return orjson .loads (row .payload )
124+
125+
126+ def cache_discord_user (user_id , payload , session ):
127+ """Upsert a user payload into the cache.
128+
129+ Caller must only pass valid upstream responses — never None, never
130+ error envelopes — so the cache doesn't pin failures and starve
131+ legitimate retries.
132+ """
133+ serialized = orjson .dumps (payload ).decode ()
134+ existing = session .query (CachedDiscordUser ).filter_by (user_id = user_id ).first ()
135+ if existing :
136+ existing .payload = serialized
137+ existing .cached_at = datetime .now ()
138+ else :
139+ session .add (CachedDiscordUser (user_id = user_id , payload = serialized ))
140+ session .commit ()
141+
142+
96143def fetch_pending_packages ():
97144 session = Session ()
98145 # select * from package_process_status pps where step <> 'PROCESSED' and error_message_code is null
0 commit comments