Skip to content

Async workers

The gameserver runs as a single FastAPI process. The target topology is lazy-on-read for most periodic work, supplemented by a small number of in-process asyncio background tasks. Heavy scheduled jobs (cron, celery, apscheduler) are not part of the target topology unless explicitly noted below.

This page inventories every recurring responsibility, the trigger that drives it, and where the implementation lives.

Topology overview

Mechanism Use case Source
Lazy-on-read Per-player counters that decay/grow with wall-clock time (turns, rep decay). The recompute happens the first time the player's record is read after the threshold is crossed. Per-service (see below)
In-process asyncio.create_task Continuous loops bound to the FastAPI process lifetime (websocket cleanup, rate-limit cleanup). services/gameserver/src/main.py, services/gameserver/src/middleware/rate_limit.py
FastAPI BackgroundTasks One-shot, request-scoped follow-up work (PayPal webhook handling, Nexus generation). Per-route
External webhook Third-party-driven events (PayPal subscription state changes). services/gameserver/src/api/routes/paypal.py
Admin-triggered Operator-initiated maintenance tasks (force market recompute, rebuild Central Nexus). Admin routes

The gameserver does not run apscheduler, celery, or a worker queue. Where downstream environments need a hard cron schedule (e.g. nightly reputation decay across millions of players), the target is to add an external scheduler that calls a privileged admin endpoint, rather than introducing a worker process.

Inventory

Turn regeneration

Trigger. Lazy-on-read. Cadence. Daily — turns reset 24 hours after the last reset, computed from Player.turn_reset_at. Responsibility. When a player action reads their current turn balance, RankingService.refresh_daily_turns checks whether turn_reset_at is older than the daily window. If so, it recomputes the player's max turns (rank bonus included) and refills. Idempotency. Idempotent — the recompute is keyed off turn_reset_at, so a second call within the same window is a no-op. Source. services/gameserver/src/services/ranking_service.py

The target is explicitly not a background tick. A player who never logs in does not have their turns refilled; they receive the new allotment the next time they act.

WebSocket heartbeat cleanup

Trigger. In-process asyncio.create_task started at FastAPI startup. Cadence. Every 30 seconds. Responsibility. Disconnect WebSocket clients whose last heartbeat is more than 300 seconds old. Idempotency. Idempotent — already-disconnected clients are skipped. Source. services/gameserver/src/main.py (_heartbeat_cleanup_loop), services/gameserver/src/services/websocket_service.py

Rate-limit table cleanup

Trigger. In-process asyncio.create_task started by the rate-limit middleware on first use. Cadence. Every 60 seconds. Responsibility. Evict expired entries from the in-memory rate-limit windows. Idempotency. Idempotent. Source. services/gameserver/src/middleware/rate_limit.py

Personal reputation decay

Trigger. Lazy-on-read with admin-callable backfill. Cadence. Weekly — 5 points of decay applied per week toward 0 (alignment regresses to neutral). Responsibility. PersonalReputationService.apply_weekly_decay reads personal_reputation and reduces magnitude by 5, never crossing 0. Should be called when the player's profile is read after a week of inactivity. Idempotency. Idempotent over a given week — needs an "applied this week" gate; the target is to track last_reputation_decay_at on the player and short-circuit if applied within the same week. Source. services/gameserver/src/services/personal_reputation_service.py

For backfilling all players at once (e.g. after an outage), the target is an admin-only endpoint that iterates the player table and calls the same service per player.

Faction reputation decay

Trigger. Lazy-on-read. Cadence. Per-faction; rate is configured on the Faction model (reputation_decay_rate). Responsibility. FactionService.apply_reputation_decay reduces a player's reputation toward each faction's reputation_neutral baseline. Idempotency. Idempotent if Reputation.last_decay_at is read and updated atomically. Source. services/gameserver/src/services/faction_service.py

Market price updates

Trigger. Admin-triggered + read-side recompute. Cadence. Per-station, every Station.market_update_frequency hours (default 6). Responsibility. Each port computes current_price from base_price, supply level, demand level, and active price modifiers. The target read-path checks last_market_update and recomputes lazily when stale; admins can force a system-wide recompute via POST /admin/ports/update-stock-levels. Idempotency. Idempotent — the recompute is deterministic given stock levels. Source. services/gameserver/src/services/economy_analytics_service.py, services/gameserver/src/services/realtime_market_service.py, services/gameserver/src/api/routes/admin_comprehensive.py

Planetary production tick

Trigger. Lazy-on-read. Cadence. Hourly (production rates are per-hour). Responsibility. When a player reads their planet, PlanetaryService computes elapsed time since last_production and adds the appropriate fuel/organics/equipment/research output. Population growth uses the same trigger. Idempotency. Idempotent — driven off last_production. Source. services/gameserver/src/services/planetary_service.py (_calculate_production_rates, last_production)

Siege detection and effects

Trigger. Lazy-on-read — siege state is recomputed when the planet is touched. Cadence. Per-action. Responsibility. PlanetaryService.check_and_update_siege and apply_siege_effects set/clear under_siege, deal periodic damage during a siege, and apply morale changes. Idempotency. Idempotent — keyed off siege_started_at. Source. services/gameserver/src/services/planetary_service.py

Election scheduling and tally

Trigger. Admin/governor-initiated. Cadence. Configurable — default election_frequency_days = 90 (see services/gameserver/src/services/galaxy_service.py). Responsibility. A governor schedules an election via POST /regions/my-region/elections. The election row carries voting_starts_at and voting_ends_at. Tally happens lazily on read of the election results endpoint after voting_ends_at. Idempotency. Tally is idempotent — once the result is persisted, subsequent reads return the stored result. Source. services/gameserver/src/services/regional_governance_service.py, services/gameserver/src/models/region.py (RegionalElection)

Treaty expiry

Trigger. Lazy-on-read. Cadence. Driven by each treaty's expires_at. Responsibility. Treaty reads check expires_at and mark the treaty inactive if past the threshold. Effects of expiry (e.g. lifting a non-aggression pact) are applied when downstream code reads the treaty. Idempotency. Idempotent. Source. services/gameserver/src/services/regional_governance_service.py, services/gameserver/src/models/region.py (RegionalTreaty)

Subscription billing renewal

Trigger. External webhook from PayPal. Cadence. Per-subscription billing cycle (PayPal-driven). Responsibility. POST /paypal/webhooks/paypal receives BILLING.SUBSCRIPTION.PAYMENT.SUCCEEDED, BILLING.SUBSCRIPTION.CANCELLED, etc., and updates User.subscription_status and User.subscription_expires_at (or the equivalent on Region). The handler runs in a BackgroundTasks follow-up to keep the webhook fast. Idempotency. Idempotent — webhooks are deduped by event ID; replayed events are no-ops. Source. services/gameserver/src/api/routes/paypal.py, services/gameserver/src/services/paypal_service.py

There is no in-process renewal poller; PayPal is the source of truth for subscription state.

Subscription expiry sweep

Trigger. Lazy-on-read on each request that depends on is_galactic_citizen. Cadence. Per-request. Responsibility. When a request that requires citizenship is served, the auth layer checks User.subscription_expires_at. If expired, citizenship is dropped and downstream gates fail. Idempotency. Idempotent. Source. services/gameserver/src/auth/dependencies.py, services/gameserver/src/models/user.py

Auto-bounty placement

Trigger. Event-driven — fires on personal-reputation transitions across thresholds. Cadence. Per personal-reputation change. Responsibility. When personal_reputation crosses -500, -750, or -1000, BountyService adds a system bounty (5k / 25k / 100k credits) to the player's settings.bounties. The check runs during the request that caused the reputation change. Idempotency. Idempotent — system bounties are keyed by threshold; re-emitting the same threshold replaces the existing entry. Source. services/gameserver/src/services/bounty_service.py (BOUNTY_THRESHOLDS, _get_system_bounties)

ARIA inactivity decay

Trigger. Lazy-on-read. Cadence. Daily — 1 point of relationship decay per day inactive. Responsibility. aria_personal_intelligence_service.apply_inactivity_decay reads Player.aria_relationship_score, computes days inactive from last_game_login, and decays the score by min(days_inactive, current_score). Idempotency. Needs a marker on the player; the target is to update last_game_login immediately after decay so a second call within the same day is a no-op. Source. services/gameserver/src/services/aria_personal_intelligence_service.py

ARIA market intelligence decay

Trigger. Lazy-on-read per (player, sector) pair. Cadence. 5% decay per day, applied compounding-ly when intelligence is read. Responsibility. Reduces prediction_confidence and intelligence_quality on ARIAMarketIntelligence rows that haven't been refreshed. Idempotency. Idempotent — driven off last_observation_at. Source. services/gameserver/src/services/aria_personal_intelligence_service.py (_decay_sector_intelligence)

AI recommendation expiry

Trigger. Lazy-on-read. Cadence. Per AIRecommendation.expires_at. Responsibility. Reads filter by expires_at > now(); expired rows are excluded and may be reaped by the next admin sweep. Idempotency. Idempotent. Source. services/gameserver/src/models/ai_trading.py, services/gameserver/src/services/ai_trading_service.py

Refresh-token expiry

Trigger. Lazy-on-read on POST /auth/refresh. Cadence. Per-request. Responsibility. Tokens past expires_at are rejected. Cleanup of expired rows happens during admin security cleanup. Idempotency. Idempotent. Source. services/gameserver/src/models/refresh_token.py, services/gameserver/src/auth/jwt.py

Audit log retention

Trigger. Admin-triggered. Cadence. On demand via POST /admin/security/cleanup. Responsibility. Purge audit-log entries older than the configured retention horizon. The target is operator-driven, not automatic. Idempotency. Idempotent. Source. services/gameserver/src/api/routes/admin_comprehensive.py, services/gameserver/src/services/audit_service.py

Sector / galaxy event scheduling

Trigger. Admin-authored events with start_time / end_time. Cadence. Per event. Responsibility. Reads of a sector or galaxy include the active events whose started_at <= now() < expires_at. Activation and deactivation are driven by admin endpoints (POST /admin/events/{id}/activate, POST /admin/events/{id}/deactivate); the read path filters by time range. Idempotency. Idempotent. Source. services/gameserver/src/api/routes/events.py, services/gameserver/src/models/game_event.py

Warp-tunnel collapse

Trigger. Lazy-on-read of artificial tunnels. Cadence. Per-traversal — expected_lifetime is checked when a ship attempts traversal. Responsibility. If expected_lifetime is past, the tunnel transitions to COLLAPSED and traversal fails. Idempotency. Idempotent. Source. services/gameserver/src/models/warp_tunnel.py

Central Nexus generation

Trigger. Admin-triggered, runs as a FastAPI BackgroundTasks follow-up. Cadence. On demand. Responsibility. POST /nexus/generate schedules NexusGenerationService.generate_nexus to run after the response. The endpoint immediately returns a job-id; clients poll /nexus/status. Idempotency. Not idempotent — generates a fresh nexus each call. Production should gate the endpoint to prevent concurrent runs. Source. services/gameserver/src/api/routes/nexus.py, services/gameserver/src/services/nexus_generation_service.py

Default admin user creation

Trigger. FastAPI startup hook. Cadence. Once per process. Responsibility. Ensure a default admin exists; idempotently creates one from environment configuration if absent. Idempotency. Idempotent. Source. services/gameserver/src/main.py, services/gameserver/src/auth/admin.py (create_default_admin)

Cross-cutting properties

  • No external scheduler is required for normal operation. Every recurring responsibility is either lazy-on-read, request-scoped, or admin-triggered. The two long-running asyncio tasks (heartbeat, rate-limit cleanup) are tied to the FastAPI process lifetime.
  • Idempotency is mandatory. Every recompute uses a "last applied at" marker so re-running is safe. Tasks that don't have one (e.g. ARIA inactivity decay's per-day gate) need to add one before they're called from anywhere but the read path that knows it's the first read of the day.
  • Backfills go through admin endpoints. Where a one-time bulk operation is needed (e.g. after an outage), the target is an admin-only POST that the operator triggers, not a process that runs continuously.
  • Multi-process safety. Lazy-on-read recomputes inside a request can race with a parallel request for the same row. Services that mutate during recompute (turn refresh, planetary production) should hold a row-level lock (SELECT ... FOR UPDATE) before applying the delta. The bounty service models this correctly (bounty_service.py locks the placer row before placing a bounty).