Files
wehub-resource-sync e0e362d700
SDK Tests / changes (push) Successful in 2m29s
Real E2E Tests / changes (push) Successful in 2m29s
SDK Tests / Python SDK Tests (sandbox) (push) Waiting to run
SDK Tests / CLI Quality (push) Waiting to run
SDK Tests / CLI Tests (push) Waiting to run
SDK Tests / Python SDK Quality (code-interpreter) (push) Waiting to run
SDK Tests / Python SDK Quality (sandbox) (push) Waiting to run
SDK Tests / Python SDK Tests (code-interpreter) (push) Waiting to run
SDK Tests / JavaScript SDK Quality And Tests (code-interpreter) (push) Waiting to run
SDK Tests / JavaScript SDK Quality And Tests (sandbox) (push) Waiting to run
SDK Tests / Kotlin SDK Quality And Tests (sandbox) (push) Waiting to run
SDK Tests / Kotlin SDK Quality And Tests (code-interpreter) (push) Waiting to run
SDK Tests / C# SDK Quality And Tests (code-interpreter) (push) Waiting to run
SDK Tests / C# SDK Quality And Tests (sandbox) (push) Waiting to run
SDK Tests / Go SDK Quality And Tests (push) Waiting to run
Deploy Docs Pages / build (push) Has been cancelled
Deploy Docs Pages / deploy (push) Has been cancelled
Real E2E Tests / JavaScript E2E (docker bridge) (push) Has been cancelled
Real E2E Tests / Python E2E (docker bridge) (push) Has been cancelled
Real E2E Tests / Java E2E (docker bridge) (push) Has been cancelled
Real E2E Tests / C# E2E (docker bridge) (push) Has been cancelled
Real E2E Tests / Go E2E (docker bridge) (push) Has been cancelled
Real E2E Tests / Real E2E CI (push) Has been cancelled
SDK Tests / SDK CI (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:39:33 +08:00

130 lines
4.6 KiB
Python

# Copyright 2026 Alibaba Group Holding Ltd.
#
# 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.
"""Renew access path: eligibility checks then ``renew_expiration``."""
from __future__ import annotations
import asyncio
import logging
from datetime import datetime, timedelta, timezone
from typing import TYPE_CHECKING
from fastapi import HTTPException
from opensandbox_server.api.schema import RenewSandboxExpirationRequest
from opensandbox_server.integrations.renew_intent.intent import RenewIntent
from opensandbox_server.integrations.renew_intent.logutil import (
RENEW_EVENT_FAILED,
RENEW_EVENT_SUCCEEDED,
RENEW_SOURCE_REDIS_QUEUE,
RENEW_SOURCE_SERVER_PROXY,
renew_bundle,
)
if TYPE_CHECKING:
from opensandbox_server.services.extension_service import ExtensionService
from opensandbox_server.services.sandbox_service import SandboxService
logger = logging.getLogger(__name__)
def _http_detail_str(detail: object) -> str:
if isinstance(detail, dict):
return str(detail.get("message", detail))
return str(detail)
class AccessRenewController:
"""Eligibility gates and ``renew_expiration``; rate limiting is expected upstream (ingress / proxy)."""
def __init__(
self,
sandbox_service: "SandboxService",
extension_service: "ExtensionService",
) -> None:
self._sandbox_service = sandbox_service
self._extension_service = extension_service
def _try_renew_sync(self, sandbox_id: str, *, source: str) -> bool:
try:
sandbox = self._sandbox_service.get_sandbox(sandbox_id)
except HTTPException:
return False
if sandbox.status.state.lower() != "running":
return False
if sandbox.expires_at is None:
return False
extend = self._extension_service.get_access_renew_extend_seconds(sandbox_id)
if extend is None:
return False
now = datetime.now(timezone.utc)
current = sandbox.expires_at
if current.tzinfo is None:
current = current.replace(tzinfo=timezone.utc)
candidate = now + timedelta(seconds=extend)
new_expires = max(candidate, current)
req = RenewSandboxExpirationRequest(expires_at=new_expires)
try:
self._sandbox_service.renew_expiration(sandbox_id, req)
except HTTPException as exc:
detail_s = _http_detail_str(exc.detail)
line, ex = renew_bundle(
event=RENEW_EVENT_FAILED,
source=source,
sandbox_id=sandbox_id,
skip_reason="renew_expiration_rejected",
http_detail=detail_s,
http_status=getattr(exc, "status_code", None),
)
logger.warning(f"renew_intent {line} detail={detail_s}", extra=ex)
return False
except Exception as exc:
line, ex = renew_bundle(
event=RENEW_EVENT_FAILED,
source=source,
sandbox_id=sandbox_id,
skip_reason="renew_expiration_error",
error_type=type(exc).__name__,
)
logger.exception(f"renew_intent {line}", extra=ex)
return False
new_expires_iso = new_expires.isoformat()
line, ex = renew_bundle(
event=RENEW_EVENT_SUCCEEDED,
source=source,
sandbox_id=sandbox_id,
new_expires_at=new_expires_iso,
)
logger.info(f"renew_intent {line}", extra=ex)
return True
def attempt_renew_sync(self, sandbox_id: str, *, source: str = RENEW_SOURCE_SERVER_PROXY) -> bool:
"""Run gates + renew (sync)."""
return self._try_renew_sync(sandbox_id, source=source)
async def renew_after_gates(self, sandbox_id: str, *, source: str) -> None:
"""Run renew in a worker thread (caller holds per-sandbox serialization)."""
await asyncio.to_thread(self._try_renew_sync, sandbox_id, source=source)
async def process_intent_after_lock(self, intent: RenewIntent) -> None:
await self.renew_after_gates(intent.sandbox_id, source=RENEW_SOURCE_REDIS_QUEUE)