Files
Artemis-Eapil/playground/backend_manager/app/services/bigquery_service.py
T
chenmingxuan 2ddec53954
CI / Python 3.12 / ubuntu-latest (push) Canceled after 0s
CI / Python 3.12 / windows-latest (push) Canceled after 0s
CI / Frontend tests and production build (push) Canceled after 0s
CI / Build and smoke-test Python packages (push) Canceled after 0s
feat: add Chinese UI, Responses models, and mirrored Windows setup
Localize the web console and persist custom model and capability settings.
Use OpenAI Responses exclusively and move runtime limits into environment configuration.
Bootstrap uv and the virtual environment on Windows, support optional China mirrors,
and package source distributions without environments, build output, or credentials.

Update regression tests and repository quality checks for the new behavior.
2026-09-22 19:16:17 +08:00

156 lines
6.7 KiB
Python

# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from datetime import datetime, timezone
import logging
from typing import Optional
from app.config import settings
from app.schemas.session_schema import SessionRecord, SessionStatus
logger = logging.getLogger("artemis.bigquery")
try:
from google.cloud import bigquery
from google.cloud.exceptions import NotFound
BQ_AVAILABLE = True
except ImportError:
BQ_AVAILABLE = False
class BigQueryMappingService:
"""Manages session metadata and user-to-container mapping records in BigQuery."""
def __init__(self):
self._local_cache: dict[str, SessionRecord] = {}
self._client: bigquery.Client | None = None
self._table_ref: str = (
f"{settings.GCP_PROJECT_ID}.{settings.BQ_DATASET}.{settings.BQ_TABLE}"
)
if BQ_AVAILABLE and not settings.USE_LOCAL_FALLBACK_DB:
try:
self._client = bigquery.Client(project=settings.GCP_PROJECT_ID)
self._ensure_dataset_and_table()
logger.info(f"[BigQuery] Connected to BigQuery table: {self._table_ref}")
except Exception as e:
logger.warning(
f"[BigQuery] Failed to initialize BigQuery client: {e}. Falling back to in-memory store."
)
self._client = None
else:
logger.info("[BigQuery] Using in-memory fallback database for local/test execution.")
def _ensure_dataset_and_table(self):
"""Ensure the destination dataset and partitioned table exist in BigQuery."""
if not self._client:
return
try:
dataset_ref = bigquery.DatasetReference(settings.GCP_PROJECT_ID, settings.BQ_DATASET)
self._client.get_dataset(dataset_ref)
except NotFound:
dataset = bigquery.Dataset(dataset_ref)
dataset.location = "US"
self._client.create_dataset(dataset, timeout=30)
logger.info(f"[BigQuery] Created dataset {settings.BQ_DATASET}")
schema = [
bigquery.SchemaField("user_id", "STRING", mode="REQUIRED"),
bigquery.SchemaField("session_id", "STRING", mode="REQUIRED"),
bigquery.SchemaField("artemis_container_id", "STRING", mode="NULLABLE"),
bigquery.SchemaField("artemis_container_name", "STRING", mode="NULLABLE"),
bigquery.SchemaField("artemis_internal_ip", "STRING", mode="NULLABLE"),
bigquery.SchemaField("cuttlefish_instance_id", "STRING", mode="NULLABLE"),
bigquery.SchemaField("cuttlefish_adb_port", "INTEGER", mode="NULLABLE"),
bigquery.SchemaField("cuttlefish_webrtc_port", "INTEGER", mode="NULLABLE"),
bigquery.SchemaField("status", "STRING", mode="REQUIRED"),
bigquery.SchemaField("created_at", "TIMESTAMP", mode="REQUIRED"),
bigquery.SchemaField("last_heartbeat_at", "TIMESTAMP", mode="REQUIRED"),
bigquery.SchemaField("expires_at", "TIMESTAMP", mode="REQUIRED"),
bigquery.SchemaField("error_message", "STRING", mode="NULLABLE"),
]
table = bigquery.Table(self._table_ref, schema=schema)
table.time_partitioning = bigquery.TimePartitioning(
type_=bigquery.TimePartitioningType.DAY,
field="created_at",
)
try:
self._client.get_table(self._table_ref)
except NotFound:
self._client.create_table(table)
logger.info(f"[BigQuery] Created partitioned table {self._table_ref}")
async def save_session(self, record: SessionRecord) -> None:
"""Upsert a session record in the cache and BigQuery."""
self._local_cache[record.session_id] = record
if self._client:
try:
row = {
"user_id": record.user_id,
"session_id": record.session_id,
"artemis_container_id": record.artemis_container_id,
"artemis_container_name": record.artemis_container_name,
"artemis_internal_ip": record.artemis_internal_ip,
"cuttlefish_instance_id": record.cuttlefish_instance_id,
"cuttlefish_adb_port": record.cuttlefish_adb_port,
"cuttlefish_webrtc_port": record.cuttlefish_webrtc_port,
"status": record.status.value,
"created_at": record.created_at.isoformat(),
"last_heartbeat_at": record.last_heartbeat_at.isoformat(),
"expires_at": record.expires_at.isoformat(),
"error_message": record.error_message,
}
errors = self._client.insert_rows_json(self._table_ref, [row])
if errors:
logger.error(
f"[BigQuery] Insert errors for session {record.session_id}: {errors}"
)
except Exception as e:
logger.error(f"[BigQuery] Streaming insert failed: {e}")
async def get_session(self, session_id: str) -> SessionRecord | None:
"""Retrieve a session record by ID."""
return self._local_cache.get(session_id)
async def list_user_sessions(self, user_id: str) -> list[SessionRecord]:
"""List active/valid sessions belonging to a specific user."""
return [
rec
for rec in self._local_cache.values()
if rec.user_id == user_id and rec.status != SessionStatus.TERMINATED
]
async def list_all_active_sessions(self) -> list[SessionRecord]:
"""List all non-terminated sessions for maintenance/reaping."""
return [
rec
for rec in self._local_cache.values()
if rec.status in (SessionStatus.PROVISIONING, SessionStatus.ACTIVE, SessionStatus.IDLE)
]
async def update_status(
self, session_id: str, status: SessionStatus, error_message: str | None = None
) -> None:
"""Update the status of an existing session record."""
record = self._local_cache.get(session_id)
if record:
record.status = status
record.error_message = error_message
await self.save_session(record)
bigquery_service = BigQueryMappingService()