diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 86095586d..9bd060572 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -45,7 +45,7 @@ body: options: - Frontend (UI) - API (routes, auth, rate limits, settings) - - Running scripts (queue, sagas, pods, results) + - Running scripts (queue, pods, results) - Kafka events (consumers, DLQ, replay) - Live updates and notifications - Metrics, dashboards and logs diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index f3b54d16a..22d14887c 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -41,7 +41,7 @@ body: options: - Frontend (UI) - API (routes, auth, rate limits, settings) - - Running scripts (queue, sagas, pods, results) + - Running scripts (queue, pods, results) - Kafka events (consumers, DLQ, replay) - Live updates and notifications - Metrics, dashboards and logs diff --git a/.github/actions/e2e-ready/action.yml b/.github/actions/e2e-ready/action.yml index 5e7c5628c..b5e81e738 100644 --- a/.github/actions/e2e-ready/action.yml +++ b/.github/actions/e2e-ready/action.yml @@ -57,13 +57,19 @@ runs: timeout 120 bash -c 'until curl -ksf https://localhost/api/v1/health/live 2>/dev/null; do sleep 1; done' echo "Backend ready" # Most groups start at the newest message on a fresh Kafka, so anything sent before they join is never read - for group in k8s-worker result-processor saga-orchestrator notification-service sse-bridge-pool; do + for group in saga-orchestrator notification-service sse-bridge-pool; do echo "Waiting for consumer group $group..." timeout 120 bash -c "until docker compose exec -T kafka kafka-consumer-groups --bootstrap-server kafka:29092 --describe --group $group --state 2>/dev/null | grep -qw Stable; do sleep 2; done" || { docker compose exec -T kafka kafka-consumer-groups --bootstrap-server kafka:29092 --describe --all-groups --state exit 1 } done + # The worker's two subscribers join saga-orchestrator seconds apart, so Stable can come before execution_requested has its consumer + echo "Waiting for a consumer on execution_requested..." + timeout 120 bash -c "until docker compose exec -T kafka kafka-consumer-groups --bootstrap-server kafka:29092 --describe --group saga-orchestrator 2>/dev/null | awk '\$2 == \"execution_requested\" && \$7 != \"-\"' | grep -q .; do sleep 2; done" || { + docker compose exec -T kafka kafka-consumer-groups --bootstrap-server kafka:29092 --describe --group saga-orchestrator + exit 1 + } echo "Consumer groups ready" if [ "$WAIT_FOR_FRONTEND" = "true" ]; then echo "Waiting for frontend health..." diff --git a/.github/labeler.yml b/.github/labeler.yml index bbbce0f68..628037589 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -29,7 +29,7 @@ - backend/app/services/execution_service.py - backend/app/services/execution_queue.py - backend/app/services/runtime_settings.py - - backend/app/services/saga/** + - backend/app/services/execution_scheduler.py - backend/app/services/k8s_worker/** - backend/app/services/pod_monitor/** - backend/app/services/result_processor/** diff --git a/.github/labels.yml b/.github/labels.yml index ff33ef105..5aaabf9e8 100644 --- a/.github/labels.yml +++ b/.github/labels.yml @@ -15,7 +15,7 @@ description: "Kafka consumers and middlewares, producer, idempotency, DLQ, replay" - name: "area: execution" color: "#c5def5" - description: "Queue, sagas, k8s worker, pod monitor, result processor, executor pods, runtimes" + description: "Queue, scheduler, k8s worker, pod monitor, result processor, executor pods, runtimes" - name: "area: frontend" color: "#c5def5" description: "Svelte UI and the generated API client" diff --git a/AGENTS.md b/AGENTS.md index 0e1990377..e7082fb00 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -9,7 +9,7 @@ Platform for running scripts online in isolated Kubernetes pods. Users write Pyt | Frontend | Svelte 5 (runes), SvelteKit 3 (adapter-static SPA) on Vite 8, TanStack Query 6, TanStack Table 9, bits-ui 2, superforms 3 with formsnap, mode-watcher, TypeScript 6 strict, TailwindCSS 4, Node 26 | | Backend | FastAPI, Dishka DI, Beanie ODM (MongoDB), Python 3.14 (backend/.python-version), async throughout, httpx2 for HTTP | | Events | Kafka (KRaft, no ZooKeeper) + FastStream (Pydantic JSON) | -| Worker | one process (`app/worker.py`): k8s worker, pod monitor, result processor, saga orchestrator, DLQ retries | +| Worker | one process (`app/worker.py`): execution scheduler, k8s worker, pod monitor, result processor, DLQ retries | | Infra | MongoDB 9, Redis 8; optional Grafana, Jaeger, Victoria Metrics, Loki, OTel Collector (`--observability`) and Kafdrop (`--debug`) | | Runtimes | Python 3.11 to 3.14, Node 22/24/26, Ruby 3.3/3.4/4.0, Bash 5.3, Go 1.26/1.27 (app/runtime_registry.py) | @@ -46,7 +46,7 @@ frontend/ ├── routes/ # SvelteKit routes: +page.svelte per page, (auth)/ and admin/ guarded by +layout.ts ├── components/ # Reusable UI: admin/, editor/, plus shared top-level components ├── lib/api/ # Generated API client, never edited by hand (see Frontend: API Client) - ├── lib/admin/ # Admin utilities: events/, rate-limits/, sagas/ + ├── lib/admin/ # Admin utilities: events/, rate-limits/ ├── lib/editor/ # Execution state, the open script (draft), CodeMirror languages ├── lib/notifications/ # Notification SSE stream ├── lib/query-client.ts # the one TanStack Query client, provided by the root layout @@ -224,7 +224,7 @@ Rules: - Use `raise SpecificError(...) from original_exc` to preserve exception chains - Never catch `Exception` broadly; catch specific exception types (the two allowed cases follow) -A catch-all is allowed in two cases only. The first records or logs, then re-raises: `UnifiedProducer.produce` in `app/events/core/producer.py` marks the event as failed to publish, `IdempotencyMiddleware` and `DeadLetterMiddleware` in `app/events/middlewares.py` release the idempotency key and save the event to the DLQ, and `ExecutionService` marks the execution as errored before raising `InfrastructureError`. The second is a loop or batch boundary that must survive one bad item: the saga orchestrator's queue loop (`try_schedule_from_queue`), saga step execution and its compensation loop, the pod monitor's watch cycle (`PodMonitor.watch_cycle`) and per-event processing, k8s worker pod creation (publishes a pod-creation-failed event instead), notification delivery and the scheduled-notification batch, DLQ batch retry and per-event replay. A new catch-all needs the same justification and must log or record the error; never swallow it silently. +A catch-all is allowed in two cases only. The first records or logs, then re-raises: `UnifiedProducer.produce` in `app/events/core/producer.py` marks the event as failed to publish, `IdempotencyMiddleware` and `DeadLetterMiddleware` in `app/events/middlewares.py` release the idempotency key and save the event to the DLQ, `ExecutionService` marks the execution as errored before raising `InfrastructureError`, and `KubernetesWorker.create_pod` marks its `pod.create` span failed. The second is a loop or batch boundary that must survive one bad item: the execution scheduler's queue loop (`schedule_pending`, a pod that can't be created fails its execution and the loop goes on), the pod monitor's watch cycle (`PodMonitor.watch_cycle`) and per-event processing, notification delivery and the scheduled-notification batch, DLQ batch retry and per-event replay. A new catch-all needs the same justification and must log or record the error; never swallow it silently. ```python # Repository: returns None for reads; raises only on write constraints @@ -352,7 +352,7 @@ class SavedScriptChanges(TypedDict, total=False): - Pure Python, no Pydantic validation at construction - No dataclass repeats the fields of a document: the document is the entity - `event_id` and other UUID fields always use `default_factory`; never add None checks -- Nested dataclass fields: `context_data: SagaContextData = field(default_factory=SagaContextData)` +- Nested dataclass fields: `filter: ReplayFilter = field(default_factory=ReplayFilter)` - `domain/` imports nothing from `db/docs/`, since the documents import domain values ## Backend: Pydantic Schemas @@ -469,7 +469,7 @@ class ExecutionService: - Publish events via `UnifiedProducer`: `await self.producer.produce(event_to_produce=event, key=execution_id)` - Services orchestrate domain logic; no direct DB or HTTP calls - Services build new documents with the document's constructor and hand them to the repository; they never import `app.schemas_pydantic` -- Always check `None` from repos and raise a domain-specific `NotFoundError` subclass (e.g. `ExecutionNotFoundError`, `SagaNotFoundError`); never return `None` to a route +- Always check `None` from repos and raise a domain-specific `NotFoundError` subclass (e.g. `ExecutionNotFoundError`, `SavedScriptNotFoundError`); never return `None` to a route ## Backend: Runtime Registry @@ -523,7 +523,7 @@ class ExecutionRequestedEvent(BaseEvent): - Treat events as immutable: build a new event instead of mutating one. `BaseEvent` is not frozen, so nothing enforces this ### Kafka Topics -One topic per event type, and the topic name is the event type value itself, with no prefix (`execution_requested`, `create_pod_command`). `EventType` is a `str`, so no `.value` is needed. +One topic per event type, and the topic name is the event type value itself, with no prefix (`execution_requested`, `pod_created`). `EventType` is a `str`, so no `.value` is needed. ```python # Producer (app/events/core/producer.py): stores the event in MongoDB, then publishes @@ -535,21 +535,21 @@ await self.producer.produce(event_to_produce=event, key=execution_id) Subscribers live in `app/events/handlers.py`, one `_router()` factory per consumer that returns a `KafkaRouter`. The router carries the ack policy and middlewares; the handler is a module-level function that takes the body and the service and does nothing else, and the factory registers it by calling the subscriber decorator directly. ```python -async def start_saga(body: ExecutionRequestedEvent, saga: FromDishka[SagaOrchestrator]) -> None: - await saga.handle_execution_requested(body) +async def schedule_execution(body: ExecutionRequestedEvent, scheduler: FromDishka[ExecutionScheduler]) -> None: + await scheduler.handle_execution_requested(body) -def saga_router() -> KafkaRouter: +def scheduler_router() -> KafkaRouter: router = KafkaRouter( ack_policy=AckPolicy.ACK, middlewares=(DeadLetterMiddleware, idempotent(KeyStrategy.EVENT_BASED, ttl_seconds=3600)), ) - router.subscriber(EventType.EXECUTION_REQUESTED, group_id="saga-orchestrator")(start_saga) + router.subscriber(EventType.EXECUTION_REQUESTED, group_id="saga-orchestrator")(schedule_execution) return router ``` - Pass the `EventType` member as the topic, never a string literal -- String literal `group_id` per consumer group +- String literal `group_id` per consumer group. The worker's group stays `saga-orchestrator`, the name of the saga orchestrator the scheduler replaced, so its committed offsets carry over - One topic per subscriber, no filtering inside handlers. The one exception is the API's `sse-bridge-pool` subscriber, which takes the 17 execution, result and pod event types in one handler and forwards them to Redis for SSE - Consumers that must not run a handler twice put `idempotent(strategy, ttl_seconds=...)` from `app/events/middlewares.py` on their router - Consumers whose failures should be retried put `DeadLetterMiddleware` first on their router. It saves the failed event to the DLQ with the consumer group, and the worker's DLQ job republishes it with headers that `RetryTargetMiddleware` (on every broker) uses to skip it in the other groups. The SSE bridge has none, since a late SSE push is worse than a missed one @@ -561,19 +561,19 @@ Everything outside the API runs in one process, `python -m app.worker` (`backend 1. `Settings(override_path="config.worker.toml")` 2. `init_beanie()` on a tz-aware `AsyncMongoClient`, before the container exists 3. `create_worker_container(settings)` -4. Get `KafkaBroker` from the container and `broker.include_routers(k8s_worker_router(), result_processor_router(), saga_router())` +4. Get `KafkaBroker` from the container and `broker.include_routers(scheduler_router())` 5. `setup_dishka(container, broker=broker, auto_inject=True)` 6. `start_jobs(container)` prepares the cluster and starts the APScheduler jobs, then `FastStream(broker, on_shutdown=[scheduler.shutdown], after_shutdown=[container.close]).run()` | Part | Purpose | |------|---------| -| k8s_worker | Consumes create/delete pod commands, creates executor pods and ConfigMaps, applies the executor NetworkPolicy on startup | -| pod_monitor | Watches executor pods (watch cycle every 5s) and publishes pod and execution events; no subscribers | -| result_processor | Consumes completed, failed and timeout events, stores results, publishes result_stored or result_failed; moves the stored status to scheduled or running on pod_scheduled and pod_running | -| saga_orchestrator | Runs execution sagas, schedules the queue every 10s, checks saga timeouts every 30s | +| execution_scheduler | Consumes `execution_requested` and `execution_cancelled`, queues executions in Redis, starts them while slots are free, cancels them; checks the queue every 10s | +| k8s_worker | Creates executor pods and ConfigMaps and deletes cancelled pods when the scheduler calls it; applies the executor namespace, NetworkPolicy and ResourceQuota on startup | +| pod_monitor | Watches executor pods (watch cycle every 5s), publishes pod events, hands finished executions to the result processor, deletes the pod and tells the scheduler | +| result_processor | Stores the status and result, then publishes the terminal event and result_stored or result_failed; publishes nothing for an execution that already ended | | dlq_processor | APScheduler only (no Kafka consumer), retries due DLQ rows every 10s for the consumer group that failed | -Each consumer keeps its own `group_id`, so sharing a process doesn't change what Kafka delivers to it. New background work goes into this process: a router in `app/events/handlers.py` included next to the others, or one more APScheduler job. Run one worker replica: the pod watch and the jobs would run once per replica. Replays run in the API, and a TTL index on `completed_at` drops finished replay sessions after 48 hours. +Inside the worker the parts call each other directly; Kafka only brings the requests in and carries what happened out. New background work goes into this process: a router in `app/events/handlers.py` included next to the scheduler's, or one more APScheduler job. Run one worker replica: the pod watch and the jobs would run once per replica. Replays run in the API, and a TTL index on `completed_at` drops finished replay sessions after 48 hours. The API process consumes too: `sse-bridge-pool` and `notification-service` are registered in `app/core/dishka_lifespan.py`. @@ -680,20 +680,20 @@ Server state goes through TanStack Query with the options the `@tanstack/svelte- ```typescript import { createMutation, createQuery, keepPreviousData, useQueryClient } from '@tanstack/svelte-query'; -import { listSagasApiV1SagasGetOptions, cancelSagaApiV1SagasSagaIdCancelPostMutation, listSagasApiV1SagasGetQueryKey } from '#lib/api/index.ts'; +import { listExecutionsApiV1AdminExecutionsGetOptions, updatePriorityApiV1AdminExecutionsExecutionIdPriorityPutMutation, listExecutionsApiV1AdminExecutionsGetQueryKey } from '#lib/api/index.ts'; // reads: the callback is reactive, so new filters or a new page are a new key and a new fetch const list = createQuery(() => ({ - ...listSagasApiV1SagasGetOptions({ query: { state, skip, limit } }), + ...listExecutionsApiV1AdminExecutionsGetOptions({ query: { status, skip, limit } }), placeholderData: keepPreviousData, refetchInterval: 5_000, // polling; pauses in a hidden tab, stops on unmount })); // writes: invalidate what the write changed const queryClient = useQueryClient(); -const cancel = createMutation(() => ({ - ...cancelSagaApiV1SagasSagaIdCancelPostMutation(), - onSuccess: () => queryClient.invalidateQueries({ queryKey: listSagasApiV1SagasGetQueryKey() }), +const updatePriority = createMutation(() => ({ + ...updatePriorityApiV1AdminExecutionsExecutionIdPriorityPutMutation(), + onSuccess: () => queryClient.invalidateQueries({ queryKey: listExecutionsApiV1AdminExecutionsGetQueryKey() }), })); ``` diff --git a/README.md b/README.md index 27b845be2..265fda603 100644 --- a/README.md +++ b/README.md @@ -169,7 +169,7 @@ The status stays completed because the pod itself ran fine. The script's own fai system diagram -A Svelte 5 frontend talks to a FastAPI backend, which keeps its state in MongoDB and Redis and publishes every execution request to Kafka. The worker takes it from there: its saga orchestrator schedules the run, the k8s worker starts a locked-down pod, the pod monitor watches it, and the result processor stores the output. The frontend follows each run live over SSE. +A Svelte 5 frontend talks to a FastAPI backend, which keeps its state in MongoDB and Redis and publishes every execution request to Kafka. The worker takes it from there: its execution scheduler queues the run, the k8s worker starts a locked-down pod, the pod monitor watches it, and the result processor stores the output. The frontend follows each run live over SSE. ## License diff --git a/backend/app/api/routes/saga.py b/backend/app/api/routes/saga.py deleted file mode 100644 index 91f2a9ade..000000000 --- a/backend/app/api/routes/saga.py +++ /dev/null @@ -1,97 +0,0 @@ -from typing import Annotated - -from dishka import FromDishka -from dishka.integrations.fastapi import DishkaRoute -from fastapi import APIRouter, Depends, Query - -from app.api.dependencies import current_user -from app.db.docs import UserDocument -from app.domain.enums import SagaState -from app.schemas_pydantic.common import ErrorResponse -from app.schemas_pydantic.saga import ( - SagaCancellationResponse, - SagaListResponse, - SagaStatusResponse, -) -from app.services.saga import SagaService - -router = APIRouter( - prefix="/sagas", - tags=["sagas"], - route_class=DishkaRoute, -) - - -@router.get( - "/{saga_id}", - response_model=SagaStatusResponse, - responses={ - 403: {"model": ErrorResponse, "description": "Access denied"}, - 404: {"model": ErrorResponse, "description": "Saga not found"}, - }, -) -async def get_saga_status( - saga_id: str, - user: Annotated[UserDocument, Depends(current_user)], - saga_service: FromDishka[SagaService], -) -> SagaStatusResponse: - """Get saga status by ID.""" - saga = await saga_service.get_saga_with_access_check(saga_id, user) - return SagaStatusResponse.model_validate(saga) - - -@router.get( - "/execution/{execution_id}", - response_model=SagaListResponse, - responses={403: {"model": ErrorResponse, "description": "Access denied"}}, -) -async def get_execution_sagas( - execution_id: str, - user: Annotated[UserDocument, Depends(current_user)], - saga_service: FromDishka[SagaService], - state: Annotated[SagaState | None, Query(description="Filter by saga state")] = None, - limit: Annotated[int, Query(ge=1, le=1000)] = 100, - skip: Annotated[int, Query(ge=0)] = 0, -) -> SagaListResponse: - """Get all sagas for an execution.""" - result = await saga_service.get_execution_sagas(execution_id, user, state, limit=limit, skip=skip) - return SagaListResponse.model_validate(result) - - -@router.get("/", response_model=SagaListResponse) -async def list_sagas( - user: Annotated[UserDocument, Depends(current_user)], - saga_service: FromDishka[SagaService], - state: Annotated[SagaState | None, Query(description="Filter by saga state")] = None, - execution_id: Annotated[str | None, Query(description="Filter by execution ID")] = None, - search: Annotated[ - str | None, - Query(max_length=200, description="Search saga ID, saga name, execution ID or error message"), - ] = None, - limit: Annotated[int, Query(ge=1, le=1000)] = 100, - skip: Annotated[int, Query(ge=0)] = 0, -) -> SagaListResponse: - """List sagas accessible by the current user.""" - result = await saga_service.list_user_sagas( - user, state, execution_id=execution_id, search=search, limit=limit, skip=skip - ) - return SagaListResponse.model_validate(result) - - -@router.post( - "/{saga_id}/cancel", - response_model=SagaCancellationResponse, - responses={ - 400: {"model": ErrorResponse, "description": "Saga is not in a cancellable state"}, - 403: {"model": ErrorResponse, "description": "Access denied"}, - 404: {"model": ErrorResponse, "description": "Saga not found"}, - }, -) -async def cancel_saga( - saga_id: str, - user: Annotated[UserDocument, Depends(current_user)], - saga_service: FromDishka[SagaService], -) -> SagaCancellationResponse: - """Cancel a running saga.""" - result = await saga_service.cancel_saga(saga_id, user) - return SagaCancellationResponse.model_validate(result) diff --git a/backend/app/core/container.py b/backend/app/core/container.py index fe6d20508..e359c7d2b 100644 --- a/backend/app/core/container.py +++ b/backend/app/core/container.py @@ -10,6 +10,7 @@ DLQProvider, EventReplayProvider, ExecutionQueueProvider, + ExecutionSchedulerProvider, K8sWorkerProvider, KafkaServicesProvider, KubernetesProvider, @@ -20,7 +21,6 @@ RepositoryProvider, ResultProcessorProvider, RuntimeSettingsProvider, - SagaOrchestratorProvider, SettingsProvider, SSEProvider, UserServicesProvider, @@ -48,7 +48,6 @@ def create_app_container(settings: Settings) -> AsyncContainer: MessagingProvider(), DLQProvider(), ExecutionQueueProvider(), - SagaOrchestratorProvider(), KafkaServicesProvider(), SSEProvider(), AuthProvider(), @@ -80,6 +79,6 @@ def create_worker_container(settings: Settings) -> AsyncContainer: K8sWorkerProvider(), PodMonitorProvider(), ResultProcessorProvider(), - SagaOrchestratorProvider(), + ExecutionSchedulerProvider(), context={Settings: settings}, ) diff --git a/backend/app/core/providers.py b/backend/app/core/providers.py index ed3e83f6b..01330132c 100644 --- a/backend/app/core/providers.py +++ b/backend/app/core/providers.py @@ -22,13 +22,11 @@ ExecutionRepository, NotificationRepository, ReplayRepository, - SagaRepository, SavedScriptRepository, UserRepository, UserSettingsRepository, ) from app.dlq.manager import DLQManager -from app.domain.saga import SagaConfig from app.events import UnifiedProducer from app.events.core.transport import KafkaEventTransport from app.events.middlewares import RetryTargetMiddleware @@ -38,6 +36,7 @@ from app.services.event_replay import EventReplayService from app.services.event_service import EventService from app.services.execution_queue import ExecutionQueueService +from app.services.execution_scheduler import ExecutionScheduler from app.services.execution_service import ExecutionService from app.services.idempotency import IdempotencyConfig, IdempotencyManager, RedisIdempotencyRepository from app.services.k8s_worker import KubernetesWorker @@ -48,7 +47,6 @@ from app.services.rate_limit_service import RateLimitService from app.services.result_processor import ResultProcessor from app.services.runtime_settings import RuntimeSettingsLoader -from app.services.saga import SagaOrchestrator, SagaService from app.services.saved_script_service import SavedScriptService from app.services.sse import SSERedisBus, SSEService from app.services.user_settings_service import UserSettingsService @@ -253,10 +251,6 @@ class RepositoryProvider(Provider): def get_execution_repository(self, logger: structlog.stdlib.BoundLogger) -> ExecutionRepository: return ExecutionRepository(logger) - @provide - def get_saga_repository(self, logger: structlog.stdlib.BoundLogger) -> SagaRepository: - return SagaRepository(logger) - @provide def get_saved_script_repository(self, logger: structlog.stdlib.BoundLogger) -> SavedScriptRepository: return SavedScriptRepository(logger) @@ -445,19 +439,6 @@ def get_notification_scheduler( ) -def _create_default_saga_config() -> SagaConfig: - """Factory for default SagaConfig used by orchestrators.""" - return SagaConfig( - name="main-orchestrator", - timeout_seconds=300, - max_retries=3, - retry_delay_seconds=5, - enable_compensation=True, - store_events=True, - publish_commands=True, - ) - - class ExecutionQueueProvider(Provider): scope = Scope.APP @@ -474,21 +455,6 @@ def get_execution_queue_service( class BusinessServicesProvider(Provider): scope = Scope.REQUEST - @provide - def get_saga_service( - self, - saga_repository: SagaRepository, - execution_repository: ExecutionRepository, - saga_orchestrator: SagaOrchestrator, - logger: structlog.stdlib.BoundLogger, - ) -> SagaService: - return SagaService( - saga_repo=saga_repository, - execution_repo=execution_repository, - orchestrator=saga_orchestrator, - logger=logger, - ) - @provide def get_execution_service( self, @@ -602,6 +568,8 @@ def get_pod_monitor( api_client: k8s_client.ApiClient, logger: structlog.stdlib.BoundLogger, event_mapper: PodEventMapper, + results: ResultProcessor, + scheduler: ExecutionScheduler, tracer: trace.Tracer, ) -> PodMonitor: return PodMonitor( @@ -610,30 +578,31 @@ def get_pod_monitor( logger=logger, api_client=api_client, event_mapper=event_mapper, + results=results, + scheduler=scheduler, tracer=tracer, ) -class SagaOrchestratorProvider(Provider): +class ExecutionSchedulerProvider(Provider): scope = Scope.APP @provide - def get_saga_orchestrator( + def get_execution_scheduler( self, - saga_repository: SagaRepository, - kafka_producer: UnifiedProducer, - logger: structlog.stdlib.BoundLogger, queue_service: ExecutionQueueService, + k8s_worker: KubernetesWorker, + results: ResultProcessor, runtime_settings: RuntimeSettingsLoader, + logger: structlog.stdlib.BoundLogger, tracer: trace.Tracer, - ) -> SagaOrchestrator: - return SagaOrchestrator( - config=_create_default_saga_config(), - saga_repository=saga_repository, - producer=kafka_producer, - logger=logger, - queue_service=queue_service, + ) -> ExecutionScheduler: + return ExecutionScheduler( + queue=queue_service, + k8s_worker=k8s_worker, + results=results, runtime_settings=runtime_settings, + logger=logger, tracer=tracer, ) diff --git a/backend/app/db/__init__.py b/backend/app/db/__init__.py index fc9cc54e3..0e558ee79 100644 --- a/backend/app/db/__init__.py +++ b/backend/app/db/__init__.py @@ -6,7 +6,6 @@ ExecutionRepository, NotificationRepository, ReplayRepository, - SagaRepository, SavedScriptRepository, UserRepository, UserSettingsRepository, @@ -20,7 +19,6 @@ "ExecutionRepository", "NotificationRepository", "ReplayRepository", - "SagaRepository", "SavedScriptRepository", "UserRepository", "UserSettingsRepository", diff --git a/backend/app/db/docs/__init__.py b/backend/app/db/docs/__init__.py index 2505a4c40..74036dc52 100644 --- a/backend/app/db/docs/__init__.py +++ b/backend/app/db/docs/__init__.py @@ -15,7 +15,6 @@ from app.db.docs.replay import ( ReplaySessionDocument, ) -from app.db.docs.saga import SagaDocument from app.db.docs.saved_script import SavedScriptDocument from app.db.docs.user import UserDocument from app.db.docs.user_settings import UserSettingsDocument @@ -28,7 +27,6 @@ NotificationDocument, NotificationSubscriptionDocument, UserSettingsDocument, - SagaDocument, DLQMessageDocument, EventDocument, EventArchiveDocument, @@ -49,8 +47,6 @@ "NotificationSubscriptionDocument", # User Settings "UserSettingsDocument", - # Saga - "SagaDocument", # DLQ "DLQMessageDocument", # Event diff --git a/backend/app/db/docs/saga.py b/backend/app/db/docs/saga.py deleted file mode 100644 index 3b03c335b..000000000 --- a/backend/app/db/docs/saga.py +++ /dev/null @@ -1,49 +0,0 @@ -import dataclasses -from datetime import datetime, timezone -from typing import Annotated -from uuid import uuid4 - -from beanie import Document, Indexed -from pydantic import ConfigDict, Field -from pymongo import ASCENDING, IndexModel - -from app.domain.enums import SagaState -from app.domain.saga.models import SagaContextData - - -class SagaDocument(Document): - """Domain model for saga stored in database. - - Copied from Saga/SagaInstance dataclass. - """ - - saga_id: Annotated[str, Indexed(unique=True)] = Field(default_factory=lambda: str(uuid4())) - saga_name: Annotated[str, Indexed()] - execution_id: Annotated[str, Indexed()] - state: SagaState = SagaState.CREATED # Indexed via Settings.indexes - current_step: str | None = None - completed_steps: list[str] = Field(default_factory=list) - compensated_steps: list[str] = Field(default_factory=list) - context_data: SagaContextData = Field(default_factory=SagaContextData) - error_message: str | None = None - created_at: Annotated[datetime, Indexed()] = Field(default_factory=lambda: datetime.now(timezone.utc)) - updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) - completed_at: datetime | None = None - retry_count: int = 0 - - model_config = ConfigDict(from_attributes=True) - - class Settings: - name = "sagas" - use_state_management = True - use_revision = True - bson_encoders = {SagaContextData: dataclasses.asdict} - indexes = [ - IndexModel([("state", ASCENDING)], name="idx_saga_state"), - IndexModel([("state", ASCENDING), ("created_at", ASCENDING)], name="idx_saga_state_created"), - IndexModel( - [("execution_id", ASCENDING), ("saga_name", ASCENDING)], - unique=True, - name="idx_saga_execution_name_unique", - ), - ] diff --git a/backend/app/db/repositories/__init__.py b/backend/app/db/repositories/__init__.py index 0897e996c..c20bdb0cd 100644 --- a/backend/app/db/repositories/__init__.py +++ b/backend/app/db/repositories/__init__.py @@ -4,7 +4,6 @@ from app.db.repositories.execution_repository import ExecutionRepository from app.db.repositories.notification_repository import NotificationRepository from app.db.repositories.replay_repository import ReplayRepository -from app.db.repositories.saga_repository import SagaRepository from app.db.repositories.saved_script_repository import SavedScriptRepository from app.db.repositories.user_repository import UserRepository from app.db.repositories.user_settings_repository import UserSettingsRepository @@ -17,7 +16,6 @@ "ExecutionRepository", "NotificationRepository", "ReplayRepository", - "SagaRepository", "SavedScriptRepository", "UserSettingsRepository", "UserRepository", diff --git a/backend/app/db/repositories/saga_repository.py b/backend/app/db/repositories/saga_repository.py deleted file mode 100644 index 83e5587d4..000000000 --- a/backend/app/db/repositories/saga_repository.py +++ /dev/null @@ -1,230 +0,0 @@ -import re -from dataclasses import dataclass -from datetime import datetime, timezone -from typing import Any -from uuid import uuid4 - -import structlog -from beanie.exceptions import RevisionIdWasChanged -from beanie.odm.enums import SortDirection -from beanie.odm.operators.find import BaseFindOperator -from beanie.odm.queries.update import UpdateResponse -from beanie.operators import GT, LT, NE, Eq, In, Or, RegEx, Set -from monggregate import Pipeline, S - -from app.db.docs import ExecutionDocument, SagaDocument -from app.domain.enums import SAGA_ACTIVE, SagaState -from app.domain.saga import SagaChanges, SagaConcurrencyError, SagaFilter, SagaInvalidStateError - - -@dataclass -class SagaListResult: - sagas: list[SagaDocument] - total: int - skip: int - limit: int - - @property - def has_more(self) -> bool: - return (self.skip + len(self.sagas)) < self.total - - -class SagaRepository: - def __init__(self, logger: structlog.stdlib.BoundLogger) -> None: - self.logger = logger - - def _filter_conditions(self, saga_filter: SagaFilter) -> list[BaseFindOperator]: - """Build Beanie query conditions from SagaFilter.""" - conditions: list[BaseFindOperator] = [] - if saga_filter.state: - conditions.append(Eq(SagaDocument.state, saga_filter.state)) - if saga_filter.execution_ids: - conditions.append(In(SagaDocument.execution_id, saga_filter.execution_ids)) - if saga_filter.user_id: - conditions.append(Eq(SagaDocument.context_data.user_id, saga_filter.user_id)) - if saga_filter.saga_name: - conditions.append(Eq(SagaDocument.saga_name, saga_filter.saga_name)) - if saga_filter.created_after: - conditions.append(GT(SagaDocument.created_at, saga_filter.created_after)) - if saga_filter.created_before: - conditions.append(LT(SagaDocument.created_at, saga_filter.created_before)) - if saga_filter.error_status is True: - conditions.append(NE(SagaDocument.error_message, None)) - elif saga_filter.error_status is False: - conditions.append(Eq(SagaDocument.error_message, None)) - if saga_filter.search: - pattern = re.escape(saga_filter.search) - conditions.append( - Or( - RegEx(SagaDocument.saga_id, pattern, options="i"), - RegEx(SagaDocument.saga_name, pattern, options="i"), - RegEx(SagaDocument.execution_id, pattern, options="i"), - RegEx(SagaDocument.error_message, pattern, options="i"), - ) - ) - return conditions - - async def save_saga(self, saga_id: str, changes: SagaChanges) -> SagaDocument | None: - """Load document, apply partial updates, and persist via save_changes(). - - Only the provided fields are written (Beanie diffs against the loaded - snapshot), so concurrent changes to *other* fields are never overwritten. - - Returns None if the saga does not exist. Raises SagaConcurrencyError if - the document was modified between load and save (revision mismatch). - """ - doc = await SagaDocument.find_one(SagaDocument.saga_id == saga_id) - if not doc: - return None - for field, value in changes.items(): - setattr(doc, field, value) - doc.updated_at = datetime.now(timezone.utc) - try: - await doc.save_changes() - except RevisionIdWasChanged as exc: - raise SagaConcurrencyError(saga_id) from exc - return doc - - async def atomic_cancel_saga( - self, - saga_id: str, - error_message: str, - completed_at: datetime, - ) -> SagaDocument | None: - """Atomically cancel a saga using findOneAndUpdate. - - Bumps revision_id so any concurrent save_changes() (which filters on - the old revision_id) will see a mismatch and raise RevisionIdWasChanged. - - Returns None if the saga does not exist. Raises SagaInvalidStateError - if it exists but is no longer active. - """ - doc = await SagaDocument.find_one( - SagaDocument.saga_id == saga_id, - In(SagaDocument.state, list(SAGA_ACTIVE)), - ).update( - Set( - { # type: ignore[no-untyped-call] - SagaDocument.state: SagaState.CANCELLED, - SagaDocument.error_message: error_message, - SagaDocument.completed_at: completed_at, - SagaDocument.updated_at: datetime.now(timezone.utc), - "revision_id": uuid4(), - } - ), - response_type=UpdateResponse.NEW_DOCUMENT, - ) - if not isinstance(doc, SagaDocument): - existing = await SagaDocument.find_one(SagaDocument.saga_id == saga_id) - if not existing: - return None - raise SagaInvalidStateError(saga_id, existing.state, "cancel") - return doc - - async def get_or_create_saga(self, saga: SagaDocument) -> tuple[SagaDocument, bool]: - """Atomically get or create a saga by (execution_id, saga_name). - - Uses MongoDB findOneAndUpdate with $setOnInsert + upsert in a single - atomic round-trip. Returns (saga, created). - """ - insert_data = saga.model_dump() - insert_data.pop("id", None) - insert_data.pop("revision_id", None) - - doc = await SagaDocument.find_one( - SagaDocument.execution_id == saga.execution_id, - SagaDocument.saga_name == saga.saga_name, - ).upsert( - {"$setOnInsert": insert_data}, - on_insert=saga, - response_type=UpdateResponse.NEW_DOCUMENT, - upsert=True, - ) - assert isinstance(doc, SagaDocument) - created = doc.saga_id == saga.saga_id - return doc, created - - async def get_saga_by_execution_and_name(self, execution_id: str, saga_name: str) -> SagaDocument | None: - return await SagaDocument.find_one( - SagaDocument.execution_id == execution_id, - SagaDocument.saga_name == saga_name, - ) - - async def get_saga(self, saga_id: str) -> SagaDocument | None: - return await SagaDocument.find_one(SagaDocument.saga_id == saga_id) - - async def get_sagas_by_execution( - self, execution_id: str, state: SagaState | None = None, limit: int = 100, skip: int = 0 - ) -> SagaListResult: - conditions: list[BaseFindOperator] = [Eq(SagaDocument.execution_id, execution_id)] - if state: - conditions.append(Eq(SagaDocument.state, state)) - - query = SagaDocument.find(*conditions) - total = await query.count() - return SagaListResult( - sagas=await query.sort([("created_at", SortDirection.DESCENDING)]).skip(skip).limit(limit).to_list(), - total=total, - skip=skip, - limit=limit, - ) - - async def list_sagas(self, saga_filter: SagaFilter, limit: int = 100, skip: int = 0) -> SagaListResult: - conditions = self._filter_conditions(saga_filter) - query = SagaDocument.find(*conditions) - total = await query.count() - return SagaListResult( - sagas=await query.sort([("created_at", SortDirection.DESCENDING)]).skip(skip).limit(limit).to_list(), - total=total, - skip=skip, - limit=limit, - ) - - async def get_user_execution_ids(self, user_id: str) -> list[str]: - collection = ExecutionDocument.get_pymongo_collection() - result: list[str] = await collection.distinct("execution_id", {"user_id": user_id}) - return result - - async def find_timed_out_sagas( - self, - cutoff_time: datetime, - states: list[SagaState] | None = None, - limit: int = 100, - ) -> list[SagaDocument]: - states = states or [SagaState.RUNNING, SagaState.COMPENSATING] - return ( - await SagaDocument.find( - In(SagaDocument.state, states), - LT(SagaDocument.created_at, cutoff_time), - ) - .limit(limit) - .to_list() - ) - - async def get_saga_statistics(self, saga_filter: SagaFilter | None = None) -> dict[str, Any]: - conditions = self._filter_conditions(saga_filter) if saga_filter else [] - base_query = SagaDocument.find(*conditions) - total = await base_query.count() - - # Group by state - state_pipeline = Pipeline().group(by=S.field(SagaDocument.state), query={"count": S.sum(1)}) - states = {} - async for doc in base_query.aggregate(state_pipeline.export()): - states[doc["_id"]] = doc["count"] - - # Average duration for completed sagas - completed_conditions: list[BaseFindOperator] = [ - *conditions, - Eq(SagaDocument.state, SagaState.COMPLETED), - NE(SagaDocument.completed_at, None), - ] - duration_pipeline = ( - Pipeline() - .project(duration={"$subtract": [S.field(SagaDocument.completed_at), S.field(SagaDocument.created_at)]}) - .group(by=None, query={"avg_duration": S.avg("$duration")}) - ) - avg_duration = 0.0 - async for doc in SagaDocument.find(*completed_conditions).aggregate(duration_pipeline.export()): - avg_duration = doc["avg_duration"] / 1000.0 if doc["avg_duration"] else 0.0 - - return {"total": total, "by_state": states, "average_duration_seconds": avg_duration} diff --git a/backend/app/db/repositories/user_repository.py b/backend/app/db/repositories/user_repository.py index 6135628f9..37256ad3c 100644 --- a/backend/app/db/repositories/user_repository.py +++ b/backend/app/db/repositories/user_repository.py @@ -12,7 +12,6 @@ EventDocument, ExecutionDocument, NotificationDocument, - SagaDocument, SavedScriptDocument, UserDocument, UserSettingsDocument, @@ -117,7 +116,6 @@ async def delete_user(self, user_id: str, cascade: bool = False) -> UserDeleteRe notif_result = await NotificationDocument.find(NotificationDocument.user_id == user_id).delete() settings_result = await UserSettingsDocument.find(UserSettingsDocument.user_id == user_id).delete() events_result = await EventDocument.find(EventDocument.metadata.user_id == user_id).delete() - sagas_result = await SagaDocument.find(SagaDocument.context_data.user_id == user_id).delete() return UserDeleteResult( user_deleted=True, @@ -126,7 +124,6 @@ async def delete_user(self, user_id: str, cascade: bool = False) -> UserDeleteRe notifications=notif_result.deleted_count if notif_result else 0, user_settings=settings_result.deleted_count if settings_result else 0, events=events_result.deleted_count if events_result else 0, - sagas=sagas_result.deleted_count if sagas_result else 0, ) async def reset_user_password(self, user_id: str, hashed_password: SecretStr) -> bool: diff --git a/backend/app/dlq/models.py b/backend/app/dlq/models.py index 8b0bc8a37..cee1d7e17 100644 --- a/backend/app/dlq/models.py +++ b/backend/app/dlq/models.py @@ -129,14 +129,12 @@ def get_next_retry_time(self, retry_count: int) -> datetime: def retry_policy_for(event_type: EventType) -> RetryPolicy: """Determine retry policy from event type using category sets.""" - from app.infrastructure.kafka.topics import COMMAND_TYPES, EXECUTION_TYPES, POD_TYPES, RESULT_TYPES + from app.infrastructure.kafka.topics import EXECUTION_TYPES, POD_TYPES, RESULT_TYPES if event_type in EXECUTION_TYPES: return AGGRESSIVE_RETRY if event_type in POD_TYPES: return CAUTIOUS_RETRY - if event_type in COMMAND_TYPES: - return AGGRESSIVE_RETRY if event_type in RESULT_TYPES: return IMMEDIATE_RETRY return DEFAULT_RETRY diff --git a/backend/app/domain/enums/__init__.py b/backend/app/domain/enums/__init__.py index 26f8629a3..8150f79c1 100644 --- a/backend/app/domain/enums/__init__.py +++ b/backend/app/domain/enums/__init__.py @@ -14,7 +14,6 @@ NotificationStatus, ) from app.domain.enums.replay import REPLAY_TERMINAL, ReplayStatus, ReplayTarget, ReplayType -from app.domain.enums.saga import SAGA_ACTIVE, SAGA_TERMINAL, SagaState from app.domain.enums.sse import SSEControlEvent from app.domain.enums.storage import ExecutionErrorType, StorageType from app.domain.enums.user import UserRole @@ -46,10 +45,6 @@ "ReplayStatus", "ReplayTarget", "ReplayType", - # Saga - "SAGA_ACTIVE", - "SAGA_TERMINAL", - "SagaState", # SSE "SSEControlEvent", # Storage diff --git a/backend/app/domain/enums/events.py b/backend/app/domain/enums/events.py index 8927ca91b..e59502b79 100644 --- a/backend/app/domain/enums/events.py +++ b/backend/app/domain/enums/events.py @@ -68,20 +68,6 @@ class EventType(StringEnum): RESULT_STORED = "result_stored" RESULT_FAILED = "result_failed" - # Saga events - SAGA_STARTED = "saga_started" - SAGA_COMPLETED = "saga_completed" - SAGA_FAILED = "saga_failed" - SAGA_CANCELLED = "saga_cancelled" - SAGA_COMPENSATING = "saga_compensating" - SAGA_COMPENSATED = "saga_compensated" - - # Saga command events (orchestration) - CREATE_POD_COMMAND = "create_pod_command" - DELETE_POD_COMMAND = "delete_pod_command" - ALLOCATE_RESOURCES_COMMAND = "allocate_resources_command" - RELEASE_RESOURCES_COMMAND = "release_resources_command" - # DLQ events DLQ_MESSAGE_RECEIVED = "dlq_message_received" DLQ_MESSAGE_RETRIED = "dlq_message_retried" diff --git a/backend/app/domain/enums/saga.py b/backend/app/domain/enums/saga.py deleted file mode 100644 index 41c217f47..000000000 --- a/backend/app/domain/enums/saga.py +++ /dev/null @@ -1,29 +0,0 @@ -from app.core.utils import StringEnum - - -class SagaState(StringEnum): - """Saga execution states.""" - - _terminal: bool - - CREATED = "created" - RUNNING = "running" - COMPENSATING = "compensating" - COMPLETED = ("completed", True) - FAILED = ("failed", True) - TIMEOUT = ("timeout", True) - CANCELLED = ("cancelled", True) - - def __new__(cls, value: str, terminal: bool = False) -> "SagaState": - obj = str.__new__(cls, value) - obj._value_ = value - obj._terminal = terminal - return obj - - @property - def is_terminal(self) -> bool: - return self._terminal - - -SAGA_TERMINAL = frozenset(s for s in SagaState if s.is_terminal) -SAGA_ACTIVE = frozenset(s for s in SagaState if not s.is_terminal) diff --git a/backend/app/domain/events/__init__.py b/backend/app/domain/events/__init__.py index 49915f16f..a55b14e76 100644 --- a/backend/app/domain/events/__init__.py +++ b/backend/app/domain/events/__init__.py @@ -14,15 +14,11 @@ ) from app.domain.events.exceptions import EventNotFoundError from app.domain.events.typed import ( - # Saga Command Events - AllocateResourcesCommandEvent, # Base types # Security Events AuthFailedEvent, BaseEvent, ContainerStatusInfo, - CreatePodCommandEvent, - DeletePodCommandEvent, # DLQ Events DLQMessageDiscardedEvent, DLQMessageReceivedEvent, @@ -62,19 +58,11 @@ # Resource Events QuotaExceededEvent, RateLimitExceededEvent, - ReleaseResourcesCommandEvent, ResourceLimitExceededEvent, ResourceUsageDomain, # Result Events ResultFailedEvent, ResultStoredEvent, - # Saga Events - SagaCancelledEvent, - SagaCompensatedEvent, - SagaCompensatingEvent, - SagaCompletedEvent, - SagaFailedEvent, - SagaStartedEvent, # Script Events ScriptDeletedEvent, ScriptSavedEvent, @@ -154,18 +142,6 @@ "NotificationAllReadEvent", "NotificationClickedEvent", "NotificationPreferencesUpdatedEvent", - # Saga Events - "SagaStartedEvent", - "SagaCompletedEvent", - "SagaFailedEvent", - "SagaCancelledEvent", - "SagaCompensatingEvent", - "SagaCompensatedEvent", - # Saga Command Events - "CreatePodCommandEvent", - "DeletePodCommandEvent", - "AllocateResourcesCommandEvent", - "ReleaseResourcesCommandEvent", # Script Events "ScriptSavedEvent", "ScriptDeletedEvent", diff --git a/backend/app/domain/events/typed.py b/backend/app/domain/events/typed.py index a0e7ff36b..a9eecd1c9 100644 --- a/backend/app/domain/events/typed.py +++ b/backend/app/domain/events/typed.py @@ -341,100 +341,6 @@ class NotificationPreferencesUpdatedEvent(BaseEvent): changed_fields: list[str] = Field(default_factory=list) -class SagaStartedEvent(BaseEvent): - event_type: Literal[EventType.SAGA_STARTED] = EventType.SAGA_STARTED - saga_id: str - saga_name: str - execution_id: str - initial_event_id: str - - -class SagaCompletedEvent(BaseEvent): - event_type: Literal[EventType.SAGA_COMPLETED] = EventType.SAGA_COMPLETED - saga_id: str - saga_name: str - execution_id: str - completed_steps: list[str] = Field(default_factory=list) - - -class SagaFailedEvent(BaseEvent): - event_type: Literal[EventType.SAGA_FAILED] = EventType.SAGA_FAILED - saga_id: str - saga_name: str - execution_id: str - failed_step: str - error: str - - -class SagaCancelledEvent(BaseEvent): - event_type: Literal[EventType.SAGA_CANCELLED] = EventType.SAGA_CANCELLED - saga_id: str - saga_name: str - execution_id: str - reason: str - completed_steps: list[str] = Field(default_factory=list) - compensated_steps: list[str] = Field(default_factory=list) - cancelled_at: datetime | None = None - cancelled_by: str | None = None - - -class SagaCompensatingEvent(BaseEvent): - event_type: Literal[EventType.SAGA_COMPENSATING] = EventType.SAGA_COMPENSATING - saga_id: str - saga_name: str - execution_id: str - compensating_step: str - - -class SagaCompensatedEvent(BaseEvent): - event_type: Literal[EventType.SAGA_COMPENSATED] = EventType.SAGA_COMPENSATED - saga_id: str - saga_name: str - execution_id: str - compensated_steps: list[str] = Field(default_factory=list) - - -class CreatePodCommandEvent(BaseEvent): - event_type: Literal[EventType.CREATE_POD_COMMAND] = EventType.CREATE_POD_COMMAND - saga_id: str - execution_id: str - script: str - language: str - language_version: str - runtime_image: str - runtime_command: list[str] = Field(default_factory=list) - runtime_filename: str - timeout_seconds: int - cpu_limit: str - memory_limit: str - cpu_request: str - memory_request: str - priority: QueuePriority = QueuePriority.NORMAL - - -class DeletePodCommandEvent(BaseEvent): - event_type: Literal[EventType.DELETE_POD_COMMAND] = EventType.DELETE_POD_COMMAND - saga_id: str - execution_id: str - reason: str - pod_name: str | None = None - namespace: str | None = None - - -class AllocateResourcesCommandEvent(BaseEvent): - event_type: Literal[EventType.ALLOCATE_RESOURCES_COMMAND] = EventType.ALLOCATE_RESOURCES_COMMAND - execution_id: str - cpu_request: str - memory_request: str - - -class ReleaseResourcesCommandEvent(BaseEvent): - event_type: Literal[EventType.RELEASE_RESOURCES_COMMAND] = EventType.RELEASE_RESOURCES_COMMAND - execution_id: str - cpu_request: str - memory_request: str - - class ScriptSavedEvent(BaseEvent): event_type: Literal[EventType.SCRIPT_SAVED] = EventType.SCRIPT_SAVED script_id: str @@ -593,18 +499,6 @@ class DLQMessageDiscardedEvent(BaseEvent): | NotificationAllReadEvent | NotificationClickedEvent | NotificationPreferencesUpdatedEvent - # Saga Events - | SagaStartedEvent - | SagaCompletedEvent - | SagaFailedEvent - | SagaCancelledEvent - | SagaCompensatingEvent - | SagaCompensatedEvent - # Saga Command Events - | CreatePodCommandEvent - | DeletePodCommandEvent - | AllocateResourcesCommandEvent - | ReleaseResourcesCommandEvent # Script Events | ScriptSavedEvent | ScriptDeletedEvent diff --git a/backend/app/domain/execution/exceptions.py b/backend/app/domain/execution/exceptions.py index f11f1c12a..836b31de4 100644 --- a/backend/app/domain/execution/exceptions.py +++ b/backend/app/domain/execution/exceptions.py @@ -9,6 +9,7 @@ def __init__(self, execution_id: str) -> None: super().__init__("Execution", execution_id) +# --8<-- [start:ExecutionAccessDeniedError] class ExecutionAccessDeniedError(ForbiddenError): """Raised when a non-admin user accesses another user's execution.""" @@ -17,6 +18,9 @@ def __init__(self, execution_id: str) -> None: super().__init__(f"Access denied to execution '{execution_id}'") +# --8<-- [end:ExecutionAccessDeniedError] + + class ExecutionNotRetryableError(InvalidStateError): """Raised when retrying an execution that hasn't finished yet.""" diff --git a/backend/app/domain/saga/__init__.py b/backend/app/domain/saga/__init__.py deleted file mode 100644 index 1079066ee..000000000 --- a/backend/app/domain/saga/__init__.py +++ /dev/null @@ -1,27 +0,0 @@ -from app.domain.saga.exceptions import ( - SagaAccessDeniedError, - SagaConcurrencyError, - SagaInvalidStateError, - SagaNotFoundError, -) -from app.domain.saga.models import ( - SagaCancellationResult, - SagaChanges, - SagaConfig, - SagaContextData, - SagaFilter, - SagaQuery, -) - -__all__ = [ - "SagaCancellationResult", - "SagaChanges", - "SagaConfig", - "SagaContextData", - "SagaFilter", - "SagaQuery", - "SagaAccessDeniedError", - "SagaInvalidStateError", - "SagaConcurrencyError", - "SagaNotFoundError", -] diff --git a/backend/app/domain/saga/exceptions.py b/backend/app/domain/saga/exceptions.py deleted file mode 100644 index 4aa030ddd..000000000 --- a/backend/app/domain/saga/exceptions.py +++ /dev/null @@ -1,40 +0,0 @@ -from app.domain.enums import SagaState -from app.domain.exceptions import ConflictError, ForbiddenError, InvalidStateError, NotFoundError - - -class SagaNotFoundError(NotFoundError): - """Raised when a saga is not found.""" - - def __init__(self, saga_id: str) -> None: - super().__init__("Saga", saga_id) - - -# --8<-- [start:SagaAccessDeniedError] -class SagaAccessDeniedError(ForbiddenError): - """Raised when access to a saga is denied.""" - - def __init__(self, saga_id: str, user_id: str) -> None: - self.saga_id = saga_id - self.user_id = user_id - super().__init__(f"Access denied to saga '{saga_id}' for user '{user_id}'") - - -# --8<-- [end:SagaAccessDeniedError] - - -class SagaInvalidStateError(InvalidStateError): - """Raised when a saga operation is invalid for the current state.""" - - def __init__(self, saga_id: str, current_state: SagaState, operation: str) -> None: - self.saga_id = saga_id - self.current_state = current_state - self.operation = operation - super().__init__(f"Cannot {operation} saga '{saga_id}' in state '{current_state}'") - - -class SagaConcurrencyError(ConflictError): - """Raised when there's a concurrency conflict with saga operations.""" - - def __init__(self, saga_id: str) -> None: - self.saga_id = saga_id - super().__init__(f"Concurrency conflict for saga '{saga_id}'") diff --git a/backend/app/domain/saga/models.py b/backend/app/domain/saga/models.py deleted file mode 100644 index c0ebc64b9..000000000 --- a/backend/app/domain/saga/models.py +++ /dev/null @@ -1,80 +0,0 @@ -from dataclasses import dataclass, field -from datetime import datetime -from typing import TypedDict -from uuid import uuid4 - -from app.domain.enums import SagaState - - -@dataclass -class SagaContextData: - """Typed saga execution context. Populated incrementally by saga steps.""" - - execution_id: str = "" - language: str = "" - language_version: str | None = None - script: str = "" - timeout_seconds: int | None = None - pod_creation_triggered: bool = False - user_id: str = field(default_factory=lambda: str(uuid4())) - - -@dataclass -class SagaFilter: - """Filter criteria for saga queries.""" - - state: SagaState | None = None - execution_ids: list[str] | None = None - user_id: str | None = None - saga_name: str | None = None - created_after: datetime | None = None - created_before: datetime | None = None - error_status: bool | None = None - search: str | None = None - - -@dataclass -class SagaQuery: - """Query parameters for saga search.""" - - filter: SagaFilter - sort_by: str = "created_at" - sort_order: str = "desc" - limit: int = 100 - skip: int = 0 - - -@dataclass -class SagaConfig: - """Configuration for saga orchestration (domain).""" - - name: str - timeout_seconds: int = 300 - max_retries: int = 3 - retry_delay_seconds: int = 5 - enable_compensation: bool = True - store_events: bool = True - # When True, saga steps publish orchestration commands (e.g., to k8s worker). - publish_commands: bool = True - - -@dataclass -class SagaCancellationResult: - """Domain result for saga cancellation operations.""" - - success: bool - message: str - saga_id: str - - -class SagaChanges(TypedDict, total=False): - """Fields a saga step or the orchestrator writes; a missing key keeps the stored value.""" - - state: SagaState - current_step: str | None - completed_steps: list[str] - compensated_steps: list[str] - context_data: SagaContextData - error_message: str | None - completed_at: datetime | None - retry_count: int diff --git a/backend/app/domain/user/user_models.py b/backend/app/domain/user/user_models.py index 9b51055f9..48a5bfab6 100644 --- a/backend/app/domain/user/user_models.py +++ b/backend/app/domain/user/user_models.py @@ -39,7 +39,6 @@ class UserDeleteResult: notifications: int = 0 user_settings: int = 0 events: int = 0 - sagas: int = 0 @dataclass diff --git a/backend/app/events/handlers.py b/backend/app/events/handlers.py index 5c5313df2..9eb5de2ea 100644 --- a/backend/app/events/handlers.py +++ b/backend/app/events/handlers.py @@ -6,122 +6,41 @@ from app.domain.enums import EventType from app.domain.events import ( - CreatePodCommandEvent, - DeletePodCommandEvent, DomainEvent, ExecutionCancelledEvent, ExecutionCompletedEvent, ExecutionFailedEvent, ExecutionRequestedEvent, ExecutionTimeoutEvent, - PodRunningEvent, - PodScheduledEvent, ) from app.domain.idempotency import KeyStrategy from app.domain.sse import SSEExecutionEventData from app.events.middlewares import DeadLetterMiddleware, idempotent from app.infrastructure.kafka.topics import POD_EVENT_STATUS -from app.services.k8s_worker import KubernetesWorker +from app.services.execution_scheduler import ExecutionScheduler from app.services.notification_service import NotificationService -from app.services.result_processor import ResultProcessor -from app.services.saga import SagaOrchestrator from app.services.sse import SSERedisBus from app.settings import Settings _sse_field_names: frozenset[str] = frozenset(f.name for f in dataclasses.fields(SSEExecutionEventData)) -async def create_pod(body: CreatePodCommandEvent, worker: FromDishka[KubernetesWorker]) -> None: - await worker.handle_create_pod_command(body) +async def schedule_execution(body: ExecutionRequestedEvent, scheduler: FromDishka[ExecutionScheduler]) -> None: + await scheduler.handle_execution_requested(body) -async def delete_pod(body: DeletePodCommandEvent, worker: FromDishka[KubernetesWorker]) -> None: - await worker.handle_delete_pod_command(body) +async def cancel_execution(body: ExecutionCancelledEvent, scheduler: FromDishka[ExecutionScheduler]) -> None: + await scheduler.handle_execution_cancelled(body) -def k8s_worker_router() -> KafkaRouter: - router = KafkaRouter( - ack_policy=AckPolicy.ACK, - middlewares=(DeadLetterMiddleware, idempotent(KeyStrategy.CONTENT_HASH, ttl_seconds=3600)), - ) - router.subscriber(EventType.CREATE_POD_COMMAND, group_id="k8s-worker")(create_pod) - router.subscriber(EventType.DELETE_POD_COMMAND, group_id="k8s-worker")(delete_pod) - return router - - -async def store_completed_result(body: ExecutionCompletedEvent, processor: FromDishka[ResultProcessor]) -> None: - await processor.handle_execution_completed(body) - - -async def store_failed_result(body: ExecutionFailedEvent, processor: FromDishka[ResultProcessor]) -> None: - await processor.handle_execution_failed(body) - - -async def store_timeout_result(body: ExecutionTimeoutEvent, processor: FromDishka[ResultProcessor]) -> None: - await processor.handle_execution_timeout(body) - - -async def advance_on_pod_scheduled(body: PodScheduledEvent, processor: FromDishka[ResultProcessor]) -> None: - await processor.handle_pod_progress(body) - - -async def advance_on_pod_running(body: PodRunningEvent, processor: FromDishka[ResultProcessor]) -> None: - await processor.handle_pod_progress(body) - - -def result_processor_router() -> KafkaRouter: - router = KafkaRouter( - ack_policy=AckPolicy.ACK, - middlewares=(DeadLetterMiddleware, idempotent(KeyStrategy.CONTENT_HASH, ttl_seconds=7200)), - ) - router.subscriber( - EventType.EXECUTION_COMPLETED, group_id="result-processor", max_poll_records=1, auto_offset_reset="earliest" - )(store_completed_result) - router.subscriber( - EventType.EXECUTION_FAILED, group_id="result-processor", max_poll_records=1, auto_offset_reset="earliest" - )(store_failed_result) - router.subscriber( - EventType.EXECUTION_TIMEOUT, group_id="result-processor", max_poll_records=1, auto_offset_reset="earliest" - )(store_timeout_result) - router.subscriber(EventType.POD_SCHEDULED, group_id="result-processor", auto_offset_reset="earliest")( - advance_on_pod_scheduled - ) - router.subscriber(EventType.POD_RUNNING, group_id="result-processor", auto_offset_reset="earliest")( - advance_on_pod_running - ) - return router - - -async def start_saga(body: ExecutionRequestedEvent, saga: FromDishka[SagaOrchestrator]) -> None: - await saga.handle_execution_requested(body) - - -async def finish_saga_on_completed(body: ExecutionCompletedEvent, saga: FromDishka[SagaOrchestrator]) -> None: - await saga.handle_execution_completed(body) - - -async def finish_saga_on_failed(body: ExecutionFailedEvent, saga: FromDishka[SagaOrchestrator]) -> None: - await saga.handle_execution_failed(body) - - -async def finish_saga_on_timeout(body: ExecutionTimeoutEvent, saga: FromDishka[SagaOrchestrator]) -> None: - await saga.handle_execution_timeout(body) - - -async def finish_saga_on_cancelled(body: ExecutionCancelledEvent, saga: FromDishka[SagaOrchestrator]) -> None: - await saga.handle_execution_cancelled(body) - - -def saga_router() -> KafkaRouter: +def scheduler_router() -> KafkaRouter: router = KafkaRouter( ack_policy=AckPolicy.ACK, middlewares=(DeadLetterMiddleware, idempotent(KeyStrategy.EVENT_BASED, ttl_seconds=3600)), ) - router.subscriber(EventType.EXECUTION_REQUESTED, group_id="saga-orchestrator")(start_saga) - router.subscriber(EventType.EXECUTION_COMPLETED, group_id="saga-orchestrator")(finish_saga_on_completed) - router.subscriber(EventType.EXECUTION_FAILED, group_id="saga-orchestrator")(finish_saga_on_failed) - router.subscriber(EventType.EXECUTION_TIMEOUT, group_id="saga-orchestrator")(finish_saga_on_timeout) - router.subscriber(EventType.EXECUTION_CANCELLED, group_id="saga-orchestrator")(finish_saga_on_cancelled) + # The saga orchestrator's group id, so the offsets it committed carry over and a deploy skips no request + router.subscriber(EventType.EXECUTION_REQUESTED, group_id="saga-orchestrator")(schedule_execution) + router.subscriber(EventType.EXECUTION_CANCELLED, group_id="saga-orchestrator")(cancel_execution) return router diff --git a/backend/app/infrastructure/kafka/topics.py b/backend/app/infrastructure/kafka/topics.py index d595fe75d..a76fd635d 100644 --- a/backend/app/infrastructure/kafka/topics.py +++ b/backend/app/infrastructure/kafka/topics.py @@ -22,13 +22,6 @@ EventType.POD_DELETED, } -COMMAND_TYPES: set[EventType] = { - EventType.CREATE_POD_COMMAND, - EventType.DELETE_POD_COMMAND, - EventType.ALLOCATE_RESOURCES_COMMAND, - EventType.RELEASE_RESOURCES_COMMAND, -} - RESULT_TYPES: set[EventType] = { EventType.RESULT_STORED, EventType.RESULT_FAILED, @@ -50,10 +43,8 @@ EventType.USER_SETTINGS_UPDATED, } -# Events that signal the executor pod has reached a terminal state. -# The pod monitor uses this to decide when to delete the pod. -# Note: EXECUTION_CANCELLED is excluded — cancellation triggers pod deletion -# via the saga's DeletePodCommandEvent compensation, not the monitor. +# Events that signal the executor pod has reached a terminal state, so the pod monitor deletes the pod. +# A cancel deletes its pod in the execution scheduler instead. EXECUTION_TERMINAL_EVENT_TYPES: frozenset[EventType] = frozenset( { EventType.EXECUTION_COMPLETED, diff --git a/backend/app/main.py b/backend/app/main.py index ca707db77..e55b142da 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -10,7 +10,6 @@ health, notifications, replay, - saga, saved_scripts, sse, user_settings, @@ -135,7 +134,6 @@ def create_app(settings: Settings | None = None) -> FastAPI: app.include_router(admin_users_router, prefix=settings.API_V1_STR) app.include_router(user_settings.router, prefix=settings.API_V1_STR) app.include_router(notifications.router, prefix=settings.API_V1_STR) - app.include_router(saga.router, prefix=settings.API_V1_STR) logger.info("All routers configured") diff --git a/backend/app/schemas_pydantic/saga.py b/backend/app/schemas_pydantic/saga.py deleted file mode 100644 index 42fa276cd..000000000 --- a/backend/app/schemas_pydantic/saga.py +++ /dev/null @@ -1,46 +0,0 @@ -from datetime import datetime - -from pydantic import BaseModel, ConfigDict - -from app.domain.enums import SagaState - - -class SagaStatusResponse(BaseModel): - """Response schema for saga status""" - - model_config = ConfigDict(from_attributes=True, json_schema_serialization_defaults_required=True) - - saga_id: str - saga_name: str - execution_id: str - state: SagaState - current_step: str | None - completed_steps: list[str] - compensated_steps: list[str] - error_message: str | None - created_at: datetime - updated_at: datetime - completed_at: datetime | None - retry_count: int - - -class SagaListResponse(BaseModel): - """Response schema for saga list""" - - model_config = ConfigDict(from_attributes=True, json_schema_serialization_defaults_required=True) - - sagas: list[SagaStatusResponse] - total: int - skip: int - limit: int - has_more: bool - - -class SagaCancellationResponse(BaseModel): - """Response schema for saga cancellation""" - - model_config = ConfigDict(from_attributes=True, json_schema_serialization_defaults_required=True) - - success: bool - message: str - saga_id: str diff --git a/backend/app/schemas_pydantic/user.py b/backend/app/schemas_pydantic/user.py index 4ea0b16a3..efbc431af 100644 --- a/backend/app/schemas_pydantic/user.py +++ b/backend/app/schemas_pydantic/user.py @@ -109,7 +109,6 @@ class DeleteUserResponse(BaseModel): notifications: int = 0 user_settings: int = 0 events: int = 0 - sagas: int = 0 model_config = ConfigDict(from_attributes=True, json_schema_serialization_defaults_required=True) diff --git a/backend/app/services/execution_queue.py b/backend/app/services/execution_queue.py index 8d5e8f70f..04ccc289d 100644 --- a/backend/app/services/execution_queue.py +++ b/backend/app/services/execution_queue.py @@ -34,10 +34,6 @@ def _event_key(execution_id: str) -> str: return f"exec_queue:event:{execution_id}" -def _retry_key(execution_id: str) -> str: - return f"exec_queue:retries:{execution_id}" - - def _pending_key(priority: QueuePriority) -> str: return f"{_PENDING_PREFIX}{priority}" @@ -232,13 +228,6 @@ async def try_schedule(self, max_active: int) -> tuple[str, ExecutionRequestedEv ) return execution_id, event - async def increment_retry_count(self, execution_id: str) -> int: - """Atomically increment and return the retry count for an execution.""" - key = _retry_key(execution_id) - count: int = await self._redis.incr(key) - await self._redis.expire(key, _EVENT_TTL) - return count - async def release(self, execution_id: str) -> None: pipe = self._redis.pipeline(transaction=True) pipe.zrem(_RUNNING_KEY, execution_id) @@ -266,7 +255,6 @@ async def remove(self, execution_id: str) -> bool: for key in _PENDING_KEYS: pipe.zrem(key, execution_id) pipe.delete(_event_key(execution_id)) - pipe.delete(_retry_key(execution_id)) pipe.zrem(_RUNNING_KEY, execution_id) results = await pipe.execute() removed = any(results[: len(_PENDING_KEYS)]) or bool(results[-1]) diff --git a/backend/app/services/execution_scheduler.py b/backend/app/services/execution_scheduler.py new file mode 100644 index 000000000..d84cb1abb --- /dev/null +++ b/backend/app/services/execution_scheduler.py @@ -0,0 +1,77 @@ +import structlog +from opentelemetry import trace + +from app.domain.enums import ExecutionErrorType +from app.domain.events import ExecutionCancelledEvent, ExecutionFailedEvent, ExecutionRequestedEvent +from app.services.execution_queue import ExecutionQueueService +from app.services.k8s_worker import KubernetesWorker +from app.services.result_processor import ResultProcessor +from app.services.runtime_settings import RuntimeSettingsLoader + + +class ExecutionScheduler: + """Starts queued executions while the concurrency limit has room, and frees an execution's slots when it ends.""" + + def __init__( + self, + queue: ExecutionQueueService, + k8s_worker: KubernetesWorker, + results: ResultProcessor, + runtime_settings: RuntimeSettingsLoader, + logger: structlog.stdlib.BoundLogger, + tracer: trace.Tracer, + ) -> None: + self._queue = queue + self._k8s_worker = k8s_worker + self._results = results + self._runtime_settings = runtime_settings + self.logger = logger + self._tracer = tracer + + async def handle_execution_requested(self, event: ExecutionRequestedEvent) -> None: + """Queue a new execution and start it if a slot is free.""" + await self._queue.enqueue(event) + await self.schedule_pending() + + async def handle_execution_cancelled(self, event: ExecutionCancelledEvent) -> None: + """Take the execution out of the queue, store it as cancelled, delete its pod and free its slots. + + The cancel is stored before the pod goes, so the deleted pod's failure finds the execution finished. + """ + execution_id = event.execution_id + await self._queue.remove(execution_id) + await self._results.handle_execution_cancelled(event) + await self._k8s_worker.delete_pod(execution_id) + await self.finish(execution_id) + + async def finish(self, execution_id: str) -> None: + """Free an ended execution's slots, then start what the queue holds.""" + await self._queue.release(execution_id) + await self._queue.release_user_slot(execution_id) + await self.schedule_pending() + + async def schedule_pending(self) -> None: + """Start queued executions while slots are free, and report the queue's depth and active slots on the span.""" + with self._tracer.start_as_current_span("queue.schedule_pending") as span: + settings = await self._runtime_settings.get_effective_settings() + while (scheduled := await self._queue.try_schedule(settings.max_concurrent_executions)) is not None: + execution_id, event = scheduled + try: + await self._k8s_worker.create_pod(event) + except Exception as e: + self.logger.error("Failed to create pod", execution_id=execution_id, exc_info=True) + await self._results.handle_execution_failed( + ExecutionFailedEvent( + execution_id=execution_id, + aggregate_id=event.aggregate_id, + error_type=ExecutionErrorType.SYSTEM_ERROR, + exit_code=-1, + stderr=f"Failed to create pod: {e}", + resource_usage=None, + metadata=event.metadata, + ) + ) + await self._queue.release(execution_id) + await self._queue.release_user_slot(execution_id) + status = await self._queue.get_queue_status() + span.set_attributes({"queue.depth": status["queue_depth"], "queue.active": status["active_count"]}) diff --git a/backend/app/services/execution_service.py b/backend/app/services/execution_service.py index b04c0a6e2..efe7d5231 100644 --- a/backend/app/services/execution_service.py +++ b/backend/app/services/execution_service.py @@ -575,7 +575,7 @@ async def _publish_deletion_event(self, execution_id: str, user_id: str) -> None """Publish cancellation event for a deleted execution. Uses ExecutionCancelledEvent because no dedicated deletion event type - exists yet. The saga orchestrator treats both the same way. + exists yet. The execution scheduler treats both the same way. """ metadata = self._create_event_metadata(user_id=user_id) diff --git a/backend/app/services/k8s_worker/pod_builder.py b/backend/app/services/k8s_worker/pod_builder.py index ba7429436..7a8a1bc82 100644 --- a/backend/app/services/k8s_worker/pod_builder.py +++ b/backend/app/services/k8s_worker/pod_builder.py @@ -1,6 +1,6 @@ from kubernetes_asyncio import client as k8s_client -from app.domain.events import CreatePodCommandEvent +from app.domain.events import ExecutionRequestedEvent from app.runtime_registry import POD_RUNTIME_CONFIGS, PodRuntimeConfig, at_least from app.settings import Settings @@ -9,29 +9,28 @@ class PodBuilder: def __init__(self, settings: Settings): self._settings = settings - def build_pod_manifest(self, command: CreatePodCommandEvent) -> k8s_client.V1Pod: - execution_id = command.execution_id + def build_pod_manifest(self, event: ExecutionRequestedEvent) -> k8s_client.V1Pod: + execution_id = event.execution_id pod_name = f"executor-{execution_id}" - pod_config = POD_RUNTIME_CONFIGS.get(command.language, PodRuntimeConfig()) + pod_config = POD_RUNTIME_CONFIGS.get(event.language, PodRuntimeConfig()) - container = self._build_container(command, pod_config) - pod_spec = self._build_pod_spec(container, command, pod_config) + container = self._build_container(event, pod_config) + pod_spec = self._build_pod_spec(container, event, pod_config) metadata = self._build_pod_metadata( name=pod_name, execution_id=execution_id, - user_id=command.metadata.user_id, - language=command.language, - saga_id=command.saga_id, + user_id=event.metadata.user_id, + language=event.language, ) return k8s_client.V1Pod(api_version="v1", kind="Pod", metadata=metadata, spec=pod_spec) def build_config_map( - self, command: CreatePodCommandEvent, script_content: str, entrypoint_content: str + self, event: ExecutionRequestedEvent, script_content: str, entrypoint_content: str ) -> k8s_client.V1ConfigMap: """Build ConfigMap for script and entrypoint""" - execution_id = command.execution_id + execution_id = event.execution_id return k8s_client.V1ConfigMap( api_version="v1", @@ -43,27 +42,26 @@ def build_config_map( "app": "integr8s", "component": "execution-script", "execution-id": execution_id, - "saga-id": command.saga_id, }, ), - data={command.runtime_filename: script_content, "entrypoint.sh": entrypoint_content}, + data={event.runtime_filename: script_content, "entrypoint.sh": entrypoint_content}, ) - def _build_container(self, command: CreatePodCommandEvent, pod_config: PodRuntimeConfig) -> k8s_client.V1Container: - execution_id = command.execution_id + def _build_container(self, event: ExecutionRequestedEvent, pod_config: PodRuntimeConfig) -> k8s_client.V1Container: + execution_id = event.execution_id # Timeout is enforced by activeDeadlineSeconds on the pod spec - container_command = ["/bin/sh", "/entry/entrypoint.sh"] + command.runtime_command + container_command = ["/bin/sh", "/entry/entrypoint.sh"] + event.runtime_command - # Get resources - prefer command values, fallback to settings - cpu_request = command.cpu_request or self._settings.K8S_POD_CPU_REQUEST - memory_request = command.memory_request or self._settings.K8S_POD_MEMORY_REQUEST - cpu_limit = at_least(command.cpu_limit or self._settings.K8S_POD_CPU_LIMIT, pod_config.min_cpu) - memory_limit = at_least(command.memory_limit or self._settings.K8S_POD_MEMORY_LIMIT, pod_config.min_memory) + # Get resources - prefer the event's values, fallback to settings + cpu_request = event.cpu_request or self._settings.K8S_POD_CPU_REQUEST + memory_request = event.memory_request or self._settings.K8S_POD_MEMORY_REQUEST + cpu_limit = at_least(event.cpu_limit or self._settings.K8S_POD_CPU_LIMIT, pod_config.min_cpu) + memory_limit = at_least(event.memory_limit or self._settings.K8S_POD_MEMORY_LIMIT, pod_config.min_memory) container = k8s_client.V1Container( name="executor", - image=command.runtime_image, + image=event.runtime_image, command=container_command, volume_mounts=[ k8s_client.V1VolumeMount(name="script-volume", mount_path="/scripts", read_only=True), @@ -99,11 +97,11 @@ def _build_container(self, command: CreatePodCommandEvent, pod_config: PodRuntim return container def _build_pod_spec( - self, container: k8s_client.V1Container, command: CreatePodCommandEvent, pod_config: PodRuntimeConfig + self, container: k8s_client.V1Container, event: ExecutionRequestedEvent, pod_config: PodRuntimeConfig ) -> k8s_client.V1PodSpec: """Build pod specification""" - execution_id = command.execution_id - timeout = pod_config.timeout_for(command.timeout_seconds or 300) + execution_id = event.execution_id + timeout = pod_config.timeout_for(event.timeout_seconds or 300) spec = k8s_client.V1PodSpec( containers=[container], @@ -116,7 +114,7 @@ def _build_pod_spec( name="script-volume", config_map=k8s_client.V1ConfigMapVolumeSource( name=f"script-{execution_id}", - items=[k8s_client.V1KeyToPath(key=command.runtime_filename, path=command.runtime_filename)], + items=[k8s_client.V1KeyToPath(key=event.runtime_filename, path=event.runtime_filename)], ), ), k8s_client.V1Volume( @@ -158,26 +156,20 @@ def _build_pod_metadata( execution_id: str, user_id: str, language: str, - saga_id: str | None = None, ) -> k8s_client.V1ObjectMeta: - """Build pod metadata with saga tracking""" - labels = {"app": "integr8s", "component": "executor", "execution-id": execution_id, "language": language} - - labels["user-id"] = user_id[:63] # K8s label value limit - - # Add saga_id if provided (truncate to K8s label limit) - if saga_id: - labels["saga-id"] = saga_id[:63] - + """Build pod metadata""" + labels = { + "app": "integr8s", + "component": "executor", + "execution-id": execution_id, + "language": language, + "user-id": user_id[:63], # K8s label value limit + } annotations = { "integr8s.io/execution-id": execution_id, "integr8s.io/created-by": "kubernetes-worker", "integr8s.io/language": language, } - - if saga_id: - annotations["integr8s.io/saga-id"] = saga_id - return k8s_client.V1ObjectMeta( name=name, namespace=self._settings.K8S_NAMESPACE, labels=labels, annotations=annotations ) diff --git a/backend/app/services/k8s_worker/worker.py b/backend/app/services/k8s_worker/worker.py index 11794e488..cc50127db 100644 --- a/backend/app/services/k8s_worker/worker.py +++ b/backend/app/services/k8s_worker/worker.py @@ -8,13 +8,7 @@ from kubernetes_asyncio.client.rest import ApiException from opentelemetry import trace -from app.domain.enums import ExecutionErrorType -from app.domain.events import ( - CreatePodCommandEvent, - DeletePodCommandEvent, - ExecutionFailedEvent, - PodCreatedEvent, -) +from app.domain.events import ExecutionRequestedEvent, PodCreatedEvent from app.events import UnifiedProducer from app.runtime_registry import RUNTIME_REGISTRY from app.settings import Settings @@ -23,16 +17,9 @@ class KubernetesWorker: - """ - Worker service that creates Kubernetes pods from execution events. - - This service: - 1. Handles CreatePodCommand events from saga orchestrator - 2. Creates ConfigMaps with script content - 3. Creates Pods to execute the scripts - 4. Publishes PodCreated events + """Creates and deletes executor pods, and prepares the executor namespace at startup. - Lifecycle is managed by DI - consumer is injected already started. + The execution scheduler calls it once a queued execution gets a slot, and again to delete a cancelled one. """ def __init__( @@ -67,65 +54,46 @@ def __init__( self.logger.info("KubernetesWorker initialized", namespace=self._settings.K8S_NAMESPACE) - async def handle_create_pod_command(self, command: CreatePodCommandEvent) -> None: - """Handle create pod command from saga orchestrator""" - execution_id = command.execution_id - - # Check if already processing - if execution_id in self._active_creations: - self.logger.warning("Already creating pod for execution", execution_id=execution_id) - return + async def delete_pod(self, execution_id: str) -> None: + """Delete an execution's pod at once; K8s garbage-collects its ConfigMap through the ownerReference. - await self._create_pod_for_execution(command) - - async def handle_delete_pod_command(self, command: DeletePodCommandEvent) -> None: - """Handle delete pod command from saga orchestrator (compensation). - - Deleting the pod is sufficient — the ConfigMap has an ownerReference pointing - to the pod, so K8s garbage-collects it automatically. + A queued execution has no pod yet, so a 404 is expected. """ - execution_id = command.execution_id - self.logger.info("Deleting pod for execution", execution_id=execution_id, reason=command.reason) - + pod_name = f"executor-{execution_id}" try: - pod_name = f"executor-{execution_id}" await self.v1.delete_namespaced_pod( name=pod_name, namespace=self._settings.K8S_NAMESPACE, - grace_period_seconds=30, + grace_period_seconds=0, ) self.logger.info("Deleted pod, K8s garbage-collects its ConfigMap", pod_name=pod_name) - except ApiException as e: - self.logger.warning("Failed to delete pod", execution_id=execution_id, status=e.status, reason=e.reason) + if e.status != 404: + self.logger.warning("Failed to delete pod", execution_id=execution_id, status=e.status, reason=e.reason) - async def _create_pod_for_execution(self, command: CreatePodCommandEvent) -> None: - """Create pod for execution""" - execution_id = command.execution_id + async def create_pod(self, event: ExecutionRequestedEvent) -> None: + """Create the ConfigMap and pod for an execution, then publish pod_created.""" + execution_id = event.execution_id self._active_creations.add(execution_id) - with self._tracer.start_as_current_span("pod.create", attributes={"pod.language": command.language}) as span: + with self._tracer.start_as_current_span("pod.create", attributes={"pod.language": event.language}) as span: start_time = time.time() try: - script_content = command.script entrypoint_content = await self._get_entrypoint_script() - # Create ConfigMap config_map = self.pod_builder.build_config_map( - command=command, script_content=script_content, entrypoint_content=entrypoint_content + event=event, script_content=event.script, entrypoint_content=entrypoint_content ) - await self._create_config_map(config_map) - pod = self.pod_builder.build_pod_manifest(command=command) + pod = self.pod_builder.build_pod_manifest(event=event) created_pod = await self._create_pod(pod) # Set ownerReference so K8s garbage-collects the ConfigMap when the pod is deleted if created_pod and created_pod.metadata and created_pod.metadata.uid: await self._set_configmap_owner(config_map, created_pod) - # Publish PodCreated event - await self._publish_pod_created(command, pod) + await self._publish_pod_created(event, pod) span.set_attribute("pod.creation_result", "success") self.logger.info( @@ -134,12 +102,9 @@ async def _create_pod_for_execution(self, command: CreatePodCommandEvent) -> Non execution_id=execution_id, duration_seconds=round(time.time() - start_time, 2), ) - - except Exception as e: - self.logger.error("Failed to create pod for execution", execution_id=execution_id, exc_info=True) + except Exception: span.set_attribute("pod.creation_result", "failed") - await self._publish_pod_creation_failed(command, str(e)) - + raise finally: self._active_creations.discard(execution_id) span.set_attribute("pod.active_creations", len(self._active_creations)) @@ -224,29 +189,16 @@ async def _set_configmap_owner(self, config_map: k8s_client.V1ConfigMap, owner_p except ApiException as e: self.logger.warning("Failed to set ownerReference on ConfigMap", reason=e.reason) - async def _publish_pod_created(self, command: CreatePodCommandEvent, pod: k8s_client.V1Pod) -> None: + async def _publish_pod_created(self, event: ExecutionRequestedEvent, pod: k8s_client.V1Pod) -> None: """Publish pod created event""" - event = PodCreatedEvent( - execution_id=command.execution_id, - aggregate_id=command.aggregate_id, + pod_created = PodCreatedEvent( + execution_id=event.execution_id, + aggregate_id=event.aggregate_id, pod_name=pod.metadata.name, namespace=pod.metadata.namespace, - metadata=command.metadata, - ) - await self.producer.produce(event_to_produce=event, key=command.execution_id) - - async def _publish_pod_creation_failed(self, command: CreatePodCommandEvent, error: str) -> None: - """Publish pod creation failed event""" - event = ExecutionFailedEvent( - execution_id=command.execution_id, - aggregate_id=command.aggregate_id, - error_type=ExecutionErrorType.SYSTEM_ERROR, - exit_code=-1, - stderr=f"Failed to create pod: {error}", - resource_usage=None, - metadata=command.metadata, + metadata=event.metadata, ) - await self.producer.produce(event_to_produce=event, key=command.execution_id) + await self.producer.produce(event_to_produce=pod_created, key=event.execution_id) async def ensure_namespace_security(self) -> None: """Create the executor namespace and apply its security controls at startup. diff --git a/backend/app/services/pod_monitor/monitor.py b/backend/app/services/pod_monitor/monitor.py index dfc8adb07..a1e89f314 100644 --- a/backend/app/services/pod_monitor/monitor.py +++ b/backend/app/services/pod_monitor/monitor.py @@ -11,10 +11,20 @@ from opentelemetry import trace from app.core.utils import StringEnum +from app.domain.events import ( + ExecutionCompletedEvent, + ExecutionFailedEvent, + ExecutionTimeoutEvent, + PodRunningEvent, + PodScheduledEvent, + PodTerminatedEvent, +) from app.events import UnifiedProducer from app.infrastructure.kafka.topics import EXECUTION_TERMINAL_EVENT_TYPES +from app.services.execution_scheduler import ExecutionScheduler from app.services.pod_monitor.config import PodMonitorConfig -from app.services.pod_monitor.event_mapper import PodEventMapper, WatchEventType +from app.services.pod_monitor.event_mapper import PodEventMapper, PodMonitorEvent, WatchEventType +from app.services.result_processor import ResultProcessor _POD_PHASES = ("Pending", "Running", "Succeeded", "Failed", "Unknown") @@ -38,7 +48,7 @@ class PodEvent: class PodMonitor: """ - Monitors Kubernetes pods and publishes lifecycle events. + Monitors Kubernetes pods, stores what they report and publishes lifecycle events. Uses the list-then-watch pattern: - On first call (or after 410 Gone), LISTs all pods to get current state @@ -55,6 +65,8 @@ def __init__( logger: structlog.stdlib.BoundLogger, api_client: k8s_client.ApiClient, event_mapper: PodEventMapper, + results: ResultProcessor, + scheduler: ExecutionScheduler, tracer: trace.Tracer, ) -> None: self.logger = logger @@ -67,6 +79,8 @@ def __init__( # Components self._event_mapper = event_mapper self._producer = producer + self._results = results + self._scheduler = scheduler # Watch cursor — set from LIST on first run or after 410 Gone self._last_resource_version: str | None = None @@ -179,7 +193,7 @@ async def _process_pod_event(self, event: PodEvent) -> None: try: app_events = await self._event_mapper.map_pod_event(event.pod, event.event_type) for app_event in app_events: - await self._producer.produce(event_to_produce=app_event, key=app_event.execution_id) + await self._handle(app_event) except Exception: self.logger.error("Error processing pod event", exc_info=True) span.set_attribute("pod_monitor.error_type", ErrorType.PROCESSING_ERROR) @@ -200,6 +214,7 @@ async def _process_pod_event(self, event: PodEvent) -> None: if terminal: await self._delete_pod(event.pod) + await self._scheduler.finish(app_events[0].execution_id) if app_events: pod_name = event.pod.metadata.name if event.pod.metadata else "unknown" @@ -211,6 +226,21 @@ async def _process_pod_event(self, event: PodEvent) -> None: published=len(app_events), ) + async def _handle(self, event: PodMonitorEvent) -> None: + """Store a finished execution's result, or publish a pod event and store the status it implies.""" + match event: + case ExecutionCompletedEvent(): + await self._results.handle_execution_completed(event) + case ExecutionFailedEvent(): + await self._results.handle_execution_failed(event) + case ExecutionTimeoutEvent(): + await self._results.handle_execution_timeout(event) + case PodScheduledEvent() | PodRunningEvent(): + await self._producer.produce(event_to_produce=event, key=event.execution_id) + await self._results.handle_pod_progress(event) + case PodTerminatedEvent(): + await self._producer.produce(event_to_produce=event, key=event.execution_id) + def _track_pod_phase(self, event_type: WatchEventType, pod: k8s_client.V1Pod, phase: str | None) -> None: """Keep the per-phase pod count current for the pods.by.phase gauge.""" if not pod.metadata or not pod.metadata.name: diff --git a/backend/app/services/result_processor/processor.py b/backend/app/services/result_processor/processor.py index abb3e9178..ea9757d79 100644 --- a/backend/app/services/result_processor/processor.py +++ b/backend/app/services/result_processor/processor.py @@ -7,6 +7,7 @@ from app.domain.events import ( DomainEvent, EventMetadata, + ExecutionCancelledEvent, ExecutionCompletedEvent, ExecutionFailedEvent, ExecutionTimeoutEvent, @@ -15,7 +16,7 @@ ResultFailedEvent, ResultStoredEvent, ) -from app.domain.execution import ExecutionNotFoundError, ExecutionResultDomain +from app.domain.execution import ExecutionResultDomain from app.events import PRODUCE_ERRORS, UnifiedProducer from app.infrastructure.kafka.topics import POD_EVENT_STATUS from app.runtime_registry import parse_quantity @@ -23,7 +24,7 @@ class ResultProcessor: - """Service for processing execution completion events and storing results.""" + """Stores an execution's status and final result, then publishes what happened.""" def __init__( self, @@ -45,14 +46,12 @@ async def handle_pod_progress(self, event: PodScheduledEvent | PodRunningEvent) if await self._execution_repo.advance_status(event.execution_id, status): self.logger.info("Execution status advanced", execution_id=event.execution_id, status=status) - async def handle_execution_completed(self, event: DomainEvent) -> None: - """Handle execution completed event.""" - if not isinstance(event, ExecutionCompletedEvent): - raise TypeError(f"Expected ExecutionCompletedEvent, got {type(event).__name__}") - + async def handle_execution_completed(self, event: ExecutionCompletedEvent) -> None: + """Store a completed execution's result, then publish the event and result_stored.""" exec_obj = await self._execution_repo.get_execution(event.execution_id) if exec_obj is None: - raise ExecutionNotFoundError(event.execution_id) + self.logger.warning("Execution deleted before its result was stored", execution_id=event.execution_id) + return memory_mib = event.resource_usage.peak_memory_kb / 1024 limits = await self._runtime_settings.get_language_limits(exec_obj.lang) @@ -73,27 +72,14 @@ async def handle_execution_completed(self, event: DomainEvent) -> None: resource_usage=event.resource_usage, status=ExecutionStatus.COMPLETED, ) + await self._store(result, event.metadata, event.aggregate_id, announce=(event,)) - meta = event.metadata - try: - await self._execution_repo.write_terminal_result(result) - await self._publish_result_stored( - event.execution_id, len(event.stdout) + len(event.stderr), meta.user_id, event.aggregate_id - ) - except PRODUCE_ERRORS as e: - self.logger.error( - "Failed to handle ExecutionCompletedEvent", execution_id=event.execution_id, exc_info=True - ) - await self._publish_result_failed(event.execution_id, str(e), meta.user_id, event.aggregate_id) - - async def handle_execution_failed(self, event: DomainEvent) -> None: - """Handle execution failed event.""" - if not isinstance(event, ExecutionFailedEvent): - raise TypeError(f"Expected ExecutionFailedEvent, got {type(event).__name__}") - + async def handle_execution_failed(self, event: ExecutionFailedEvent) -> None: + """Store a failed execution's result, then publish the event and result_stored.""" exec_obj = await self._execution_repo.get_execution(event.execution_id) if exec_obj is None: - raise ExecutionNotFoundError(event.execution_id) + self.logger.warning("Execution deleted before its result was stored", execution_id=event.execution_id) + return attributes = { "execution.status": ExecutionStatus.FAILED, @@ -111,26 +97,14 @@ async def handle_execution_failed(self, event: DomainEvent) -> None: error_type=event.error_type, status=ExecutionStatus.FAILED, ) - meta = event.metadata - try: - await self._execution_repo.write_terminal_result(result) - await self._publish_result_stored( - event.execution_id, len(event.stdout) + len(event.stderr), meta.user_id, event.aggregate_id - ) - except PRODUCE_ERRORS as e: - self.logger.error( - "Failed to handle ExecutionFailedEvent", execution_id=event.execution_id, exc_info=True - ) - await self._publish_result_failed(event.execution_id, str(e), meta.user_id, event.aggregate_id) - - async def handle_execution_timeout(self, event: DomainEvent) -> None: - """Handle execution timeout event.""" - if not isinstance(event, ExecutionTimeoutEvent): - raise TypeError(f"Expected ExecutionTimeoutEvent, got {type(event).__name__}") + await self._store(result, event.metadata, event.aggregate_id, announce=(event,)) + async def handle_execution_timeout(self, event: ExecutionTimeoutEvent) -> None: + """Store a timed-out execution's result, then publish the event and result_stored.""" exec_obj = await self._execution_repo.get_execution(event.execution_id) if exec_obj is None: - raise ExecutionNotFoundError(event.execution_id) + self.logger.warning("Execution deleted before its result was stored", execution_id=event.execution_id) + return attributes: dict[str, AttributeValue] = { "execution.status": ExecutionStatus.TIMEOUT, @@ -148,17 +122,61 @@ async def handle_execution_timeout(self, event: DomainEvent) -> None: error_type=ExecutionErrorType.TIMEOUT, status=ExecutionStatus.TIMEOUT, ) - meta = event.metadata - try: - await self._execution_repo.write_terminal_result(result) - await self._publish_result_stored( - event.execution_id, len(event.stdout) + len(event.stderr), meta.user_id, event.aggregate_id - ) - except PRODUCE_ERRORS as e: - self.logger.error( - "Failed to handle ExecutionTimeoutEvent", execution_id=event.execution_id, exc_info=True + await self._store(result, event.metadata, event.aggregate_id, announce=(event,)) + + async def handle_execution_cancelled(self, event: ExecutionCancelledEvent) -> None: + """Store the cancelled status of an execution that hasn't finished, then publish result_stored.""" + exec_obj = await self._execution_repo.get_execution(event.execution_id) + if exec_obj is None: + self.logger.info("Cancelled execution was deleted", execution_id=event.execution_id) + return + + attributes = { + "execution.status": ExecutionStatus.CANCELLED, + "execution.lang_and_version": f"{exec_obj.lang}-{exec_obj.lang_version}", + } + with self._tracer.start_as_current_span("execution.result", attributes=attributes): + result = ExecutionResultDomain( + execution_id=event.execution_id, + exit_code=None, + stdout=None, + stderr=None, + status=ExecutionStatus.CANCELLED, + ) + # The API already published the cancel event + await self._store(result, event.metadata, event.aggregate_id, announce=()) + + async def _store( + self, + result: ExecutionResultDomain, + metadata: EventMetadata, + aggregate_id: str | None, + announce: tuple[DomainEvent, ...], + ) -> None: + """Write the terminal result, then publish the announce events and result_stored. + + Nothing is published when the execution already ended another way, so the failure of a + cancelled execution's pod neither overwrites the cancel nor notifies anyone. + """ + try: + if not await self._execution_repo.write_terminal_result(result): + self.logger.info( + "Execution already finished, result not stored", + execution_id=result.execution_id, + status=result.status, ) - await self._publish_result_failed(event.execution_id, str(e), meta.user_id, event.aggregate_id) + return + for event in announce: + await self._producer.produce(event_to_produce=event, key=result.execution_id) + await self._publish_result_stored( + result.execution_id, + len(result.stdout or "") + len(result.stderr or ""), + metadata.user_id, + aggregate_id, + ) + except PRODUCE_ERRORS as e: + self.logger.error("Failed to store execution result", execution_id=result.execution_id, exc_info=True) + await self._publish_result_failed(result.execution_id, str(e), metadata.user_id, aggregate_id) async def _publish_result_stored( self, execution_id: str, size_bytes: int, user_id: str, aggregate_id: str | None diff --git a/backend/app/services/saga/__init__.py b/backend/app/services/saga/__init__.py deleted file mode 100644 index db3ad9dd3..000000000 --- a/backend/app/services/saga/__init__.py +++ /dev/null @@ -1,26 +0,0 @@ -from app.domain.enums import SagaState -from app.domain.saga import SagaConfig -from app.services.saga.execution_saga import ( - CreatePodStep, - DeletePodCompensation, - ExecutionSaga, - ValidateExecutionStep, -) -from app.services.saga.saga_orchestrator import SagaOrchestrator -from app.services.saga.saga_service import SagaService -from app.services.saga.saga_step import CompensationStep, SagaContext, SagaStep - -__all__ = [ - "SagaOrchestrator", - "SagaService", - "SagaConfig", - "SagaState", - "SagaContext", - "SagaStep", - "CompensationStep", - "ExecutionSaga", - # Steps and compensations (execution saga) - "ValidateExecutionStep", - "CreatePodStep", - "DeletePodCompensation", -] diff --git a/backend/app/services/saga/execution_saga.py b/backend/app/services/saga/execution_saga.py deleted file mode 100644 index 7a53ddd3a..000000000 --- a/backend/app/services/saga/execution_saga.py +++ /dev/null @@ -1,147 +0,0 @@ -from typing import Any - -import structlog - -from app.domain.events import CreatePodCommandEvent, DeletePodCommandEvent, EventMetadata, ExecutionRequestedEvent -from app.events import UnifiedProducer - -from .saga_step import CompensationStep, SagaContext, SagaStep - - -class ValidateExecutionStep(SagaStep[ExecutionRequestedEvent]): - """Validate execution request.""" - - def __init__(self, logger: structlog.stdlib.BoundLogger) -> None: - super().__init__("validate_execution") - self.logger = logger - - async def execute(self, context: SagaContext, event: ExecutionRequestedEvent) -> bool: - self.logger.info("Validating execution", execution_id=event.execution_id) - - context.set("execution_id", event.execution_id) - context.set("language", event.language) - context.set("language_version", event.language_version) - context.set("script", event.script) - context.set("timeout_seconds", event.timeout_seconds) - - if len(event.script) > 1024 * 1024: - raise ValueError("Script size exceeds limit") - - return True - - def get_compensation(self) -> CompensationStep | None: - return None - - -class CreatePodStep(SagaStep[ExecutionRequestedEvent]): - """Create Kubernetes pod.""" - - def __init__(self, producer: UnifiedProducer, publish_commands: bool, logger: structlog.stdlib.BoundLogger) -> None: - super().__init__("create_pod") - self.producer = producer - self.publish_commands = publish_commands - self.logger = logger - - async def execute(self, context: SagaContext, event: ExecutionRequestedEvent) -> bool: - execution_id = context.get("execution_id") - - if not self.publish_commands: - self.logger.info( - "Skipping CreatePodCommandEvent publish", - execution_id=execution_id, - reason="publish_commands disabled", - ) - context.set("pod_creation_triggered", False) - return True - - self.logger.info("Publishing CreatePodCommandEvent", execution_id=execution_id) - - create_pod_cmd = CreatePodCommandEvent( - saga_id=context.saga_id, - execution_id=execution_id, - aggregate_id=event.aggregate_id, - script=event.script, - language=event.language, - language_version=event.language_version, - runtime_image=event.runtime_image, - runtime_command=event.runtime_command, - runtime_filename=event.runtime_filename, - timeout_seconds=event.timeout_seconds, - cpu_limit=event.cpu_limit, - memory_limit=event.memory_limit, - cpu_request=event.cpu_request, - memory_request=event.memory_request, - priority=event.priority, - metadata=EventMetadata( - service_name="saga-orchestrator", - service_version="1.0.0", - user_id=event.metadata.user_id, - ), - ) - - await self.producer.produce(event_to_produce=create_pod_cmd, key=execution_id) - - context.set("pod_creation_triggered", True) - self.logger.info("CreatePodCommandEvent published", execution_id=execution_id) - - return True - - def get_compensation(self) -> CompensationStep | None: - return DeletePodCompensation(producer=self.producer, logger=self.logger) - - -class DeletePodCompensation(CompensationStep): - """Delete created pod.""" - - def __init__(self, producer: UnifiedProducer, logger: structlog.stdlib.BoundLogger) -> None: - super().__init__("delete_pod") - self.producer = producer - self.logger = logger - - async def compensate(self, context: SagaContext) -> bool: - execution_id = context.get("execution_id") - if not execution_id or not context.get("pod_creation_triggered"): - return True - - self.logger.info("Publishing DeletePodCommandEvent", execution_id=execution_id) - - delete_pod_cmd = DeletePodCommandEvent( - saga_id=context.saga_id, - execution_id=execution_id, - aggregate_id=context.execution_id, - reason="Saga compensation due to failure", - metadata=EventMetadata( - service_name="saga-orchestrator", - service_version="1.0.0", - user_id=context.get("user_id"), - ), - ) - - await self.producer.produce(event_to_produce=delete_pod_cmd, key=execution_id) - - self.logger.info("DeletePodCommandEvent published", execution_id=execution_id) - return True - - -class ExecutionSaga: - """Saga for managing execution lifecycle.""" - - @classmethod - def get_name(cls) -> str: - return "execution_saga" - - def bind_dependencies( - self, - producer: UnifiedProducer, - publish_commands: bool, - logger: structlog.stdlib.BoundLogger, - ) -> None: - self._producer = producer - self._publish_commands = publish_commands - self._logger = logger - - def get_steps(self) -> list[SagaStep[Any]]: - return [ - ValidateExecutionStep(logger=self._logger), - CreatePodStep(producer=self._producer, publish_commands=self._publish_commands, logger=self._logger), - ] diff --git a/backend/app/services/saga/saga_orchestrator.py b/backend/app/services/saga/saga_orchestrator.py deleted file mode 100644 index 629097020..000000000 --- a/backend/app/services/saga/saga_orchestrator.py +++ /dev/null @@ -1,476 +0,0 @@ -import dataclasses -from contextlib import suppress -from datetime import UTC, datetime, timedelta -from uuid import uuid4 - -import structlog -from opentelemetry import trace -from opentelemetry.trace import SpanKind -from pydantic import TypeAdapter - -from app.db import SagaRepository -from app.db.docs import SagaDocument -from app.domain.enums import SagaState -from app.domain.events import ( - DomainEvent, - EventMetadata, - ExecutionCancelledEvent, - ExecutionCompletedEvent, - ExecutionFailedEvent, - ExecutionRequestedEvent, - ExecutionTimeoutEvent, - SagaCancelledEvent, - SagaStartedEvent, -) -from app.domain.saga import ( - SagaChanges, - SagaConcurrencyError, - SagaConfig, - SagaContextData, - SagaNotFoundError, -) -from app.events import PRODUCE_ERRORS, UnifiedProducer -from app.services.execution_queue import ExecutionQueueService -from app.services.runtime_settings import RuntimeSettingsLoader - -from .execution_saga import ExecutionSaga -from .saga_step import SagaContext - -_SAGA_NAME = ExecutionSaga.get_name() -_context_data = TypeAdapter(SagaContextData) - - -class SagaOrchestrator: - """Orchestrates saga execution and compensation.""" - - def __init__( - self, - config: SagaConfig, - saga_repository: SagaRepository, - producer: UnifiedProducer, - logger: structlog.stdlib.BoundLogger, - queue_service: ExecutionQueueService, - runtime_settings: RuntimeSettingsLoader, - tracer: trace.Tracer, - ): - self.config = config - self._producer = producer - self._repo: SagaRepository = saga_repository - self.logger = logger - self._queue = queue_service - self._runtime_settings = runtime_settings - self._tracer = tracer - - async def _save_saga(self, saga_id: str, changes: SagaChanges) -> SagaDocument: - """Persist partial saga updates. - - Raises: - SagaNotFoundError: If the saga does not exist. - """ - saved = await self._repo.save_saga(saga_id, changes) - if saved is None: - self.logger.warning("Saga not found for update", saga_id=saga_id) - raise SagaNotFoundError(saga_id) - return saved - - async def handle_execution_requested(self, event: DomainEvent) -> None: - """Handle EXECUTION_REQUESTED — enqueue and attempt to schedule.""" - if not isinstance(event, ExecutionRequestedEvent): - raise TypeError(f"Expected ExecutionRequestedEvent, got {type(event).__name__}") - await self._queue.enqueue(event) - await self.try_schedule_from_queue() - - async def handle_execution_completed(self, event: DomainEvent) -> None: - """Handle EXECUTION_COMPLETED — marks saga as completed.""" - if not isinstance(event, ExecutionCompletedEvent): - raise TypeError(f"Expected ExecutionCompletedEvent, got {type(event).__name__}") - await self._resolve_completion(event.execution_id, SagaState.COMPLETED) - - async def handle_execution_failed(self, event: DomainEvent) -> None: - """Handle EXECUTION_FAILED — marks saga as failed.""" - if not isinstance(event, ExecutionFailedEvent): - raise TypeError(f"Expected ExecutionFailedEvent, got {type(event).__name__}") - await self._resolve_completion( - event.execution_id, SagaState.FAILED, f"{event.error_type} (exit code {event.exit_code})" - ) - - async def handle_execution_timeout(self, event: DomainEvent) -> None: - """Handle EXECUTION_TIMEOUT — marks saga as timed out.""" - if not isinstance(event, ExecutionTimeoutEvent): - raise TypeError(f"Expected ExecutionTimeoutEvent, got {type(event).__name__}") - await self._resolve_completion( - event.execution_id, SagaState.TIMEOUT, f"Execution timed out after {event.timeout_seconds} seconds" - ) - - async def handle_execution_cancelled(self, event: DomainEvent) -> None: - """Handle EXECUTION_CANCELLED — remove from queue and mark saga as cancelled.""" - if not isinstance(event, ExecutionCancelledEvent): - raise TypeError(f"Expected ExecutionCancelledEvent, got {type(event).__name__}") - await self._queue.remove(event.execution_id) - await self._resolve_completion(event.execution_id, SagaState.CANCELLED, f"cancelled by {event.cancelled_by}") - - async def _resolve_completion(self, execution_id: str, state: SagaState, error_message: str | None = None) -> None: - """Look up the active saga for an execution and transition it to a terminal state. - - Always releases the queue slot and attempts to schedule the next pending - execution, even when no saga is found or the saga is already terminal. - ``release()`` and ``release_user_slot()`` are idempotent, so a missing slot is fine. - """ - saga = await self._repo.get_saga_by_execution_and_name(execution_id, _SAGA_NAME) - if not saga: - self.logger.debug("No execution_saga found for execution", execution_id=execution_id) - elif saga.state.is_terminal: - self.logger.debug("Saga already in terminal state", saga_id=saga.saga_id, state=saga.state) - else: - self.logger.info("Marking saga terminal state", saga_id=saga.saga_id, state=state) - # Deleting the user removes their sagas; the slot below still has to go - with suppress(SagaNotFoundError): - await self._save_saga( - saga.saga_id, - SagaChanges( - state=state, - error_message=error_message, - completed_at=datetime.now(UTC), - ), - ) - - await self._queue.release(execution_id) - await self._queue.release_user_slot(execution_id) - await self.try_schedule_from_queue() - - _MAX_SAGA_START_RETRIES = 3 - - async def try_schedule_from_queue(self) -> None: - """Try to schedule pending executions from the queue, and report its depth and active slots on the span.""" - with self._tracer.start_as_current_span("queue.schedule_pending") as span: - settings = await self._runtime_settings.get_effective_settings() - while True: - result = await self._queue.try_schedule(settings.max_concurrent_executions) - if result is None: - break - execution_id, event = result - try: - await self._start_saga(event) - except Exception: - retry_count = await self._queue.increment_retry_count(execution_id) - self.logger.error( - "Failed to start saga", - execution_id=execution_id, - retry_count=retry_count, - exc_info=True, - ) - await self._queue.release(execution_id) - if retry_count >= self._MAX_SAGA_START_RETRIES: - self.logger.error( - "Max saga start retries exceeded, dropping execution", - execution_id=execution_id, - ) - await self._resolve_completion( - execution_id, - SagaState.FAILED, - f"Failed to start saga after {retry_count} attempts", - ) - else: - await self._queue.enqueue(event) - break - status = await self._queue.get_queue_status() - span.set_attributes({"queue.depth": status["queue_depth"], "queue.active": status["active_count"]}) - - async def _start_saga(self, trigger_event: ExecutionRequestedEvent) -> str: - """Start a new saga instance.""" - execution_id = trigger_event.execution_id - self.logger.info("Starting saga", saga_name=_SAGA_NAME, execution_id=execution_id) - - candidate = SagaDocument( - saga_id=str(uuid4()), - saga_name=_SAGA_NAME, - execution_id=execution_id, - state=SagaState.RUNNING, - ) - - instance, created = await self._repo.get_or_create_saga(candidate) - if not created: - self.logger.info("Saga already exists for execution", saga_name=_SAGA_NAME, execution_id=execution_id) - return instance.saga_id - - self.logger.info("Started saga", saga_name=_SAGA_NAME, saga_id=instance.saga_id, execution_id=execution_id) - - if self._producer and self.config.store_events: - await self._publish_saga_started_event(instance, trigger_event) - - saga = self._create_saga_instance() - context = SagaContext(instance.saga_id, execution_id) - context.set("user_id", trigger_event.metadata.user_id) - - await self._execute_saga(saga, instance, context, trigger_event) - - return instance.saga_id - - def _create_saga_instance(self) -> ExecutionSaga: - """Create and bind an ExecutionSaga instance.""" - saga = ExecutionSaga() - saga.bind_dependencies( - producer=self._producer, - publish_commands=self.config.publish_commands, - logger=self.logger, - ) - return saga - - async def _execute_saga( - self, - saga: ExecutionSaga, - instance: SagaDocument, - context: SagaContext, - trigger_event: DomainEvent, - ) -> None: - """Execute saga steps.""" - try: - steps = saga.get_steps() - - for step in steps: - saved = await self._save_saga(instance.saga_id, SagaChanges(current_step=step.name)) - - if saved.state.is_terminal: - self.logger.info("Saga no longer active, stopping", saga_id=instance.saga_id, state=saved.state) - return - - self.logger.info("Executing saga step", step=step.name, saga_id=instance.saga_id) - - with self._tracer.start_as_current_span( - name="saga.step", - kind=SpanKind.INTERNAL, - attributes={ - "saga.name": instance.saga_name, - "saga.id": instance.saga_id, - "saga.step": step.name, - "execution.id": instance.execution_id, - }, - ): - success = await step.execute(context, trigger_event) - - if success: - await self._save_saga( - instance.saga_id, - SagaChanges( - completed_steps=[*saved.completed_steps, step.name], - context_data=_context_data.validate_python(context.data), - ), - ) - - compensation = step.get_compensation() - if compensation: - context.add_compensation(compensation) - else: - self.logger.error("Saga step failed", step=step.name, saga_id=instance.saga_id) - - if self.config.enable_compensation: - await self._compensate_saga(instance.saga_id, context) - else: - await self._fail_saga(instance.saga_id, "Step failed without compensation") - - return - - self.logger.info("Saga steps done, waiting for execution completion event", saga_id=instance.saga_id) - - except SagaConcurrencyError: - self.logger.info("Saga modified concurrently, stopping execution", saga_id=instance.saga_id) - except Exception as e: - self.logger.error("Error executing saga", saga_id=instance.saga_id, exc_info=True) - - if self.config.enable_compensation: - await self._compensate_saga(instance.saga_id, context) - else: - await self._fail_saga(instance.saga_id, str(e)) - - async def _compensate_saga(self, saga_id: str, context: SagaContext) -> None: - """Execute compensation steps.""" - self.logger.info("Starting compensation for saga", saga_id=saga_id) - - saga = await self._repo.get_saga(saga_id) - if not saga: - self.logger.error("Cannot compensate: saga not found", saga_id=saga_id) - return - - was_cancelled = saga.state == SagaState.CANCELLED - - if not was_cancelled: - await self._save_saga(saga_id, SagaChanges(state=SagaState.COMPENSATING)) - - compensated: list[str] = list(saga.compensated_steps) - failed_compensations: list[str] = [] - for compensation in reversed(context.compensations): - try: - self.logger.info("Executing compensation", compensation=compensation.name, saga_id=saga_id) - - success = await compensation.compensate(context) - - if success: - compensated.append(compensation.name) - else: - failed_compensations.append(compensation.name) - self.logger.error("Compensation failed", compensation=compensation.name, saga_id=saga_id) - - except Exception: - failed_compensations.append(compensation.name) - self.logger.error( - "Error in compensation", compensation=compensation.name, saga_id=saga_id, exc_info=True - ) - - if failed_compensations: - self.logger.error( - "Partial compensation, some steps could not be undone", - saga_id=saga_id, - failed_compensations=failed_compensations, - ) - - if was_cancelled: - await self._save_saga(saga_id, SagaChanges(compensated_steps=compensated)) - self.logger.info("Saga compensation completed after cancellation", saga_id=saga_id) - else: - await self._save_saga( - saga_id, - SagaChanges( - state=SagaState.FAILED, - error_message="Saga compensated due to failure", - completed_at=datetime.now(UTC), - compensated_steps=compensated, - ), - ) - self.logger.error("Saga failed after compensation", saga_id=saga_id) - - async def _fail_saga(self, saga_id: str, error_message: str) -> None: - """Mark saga as failed.""" - await self._save_saga( - saga_id, - SagaChanges( - state=SagaState.FAILED, - error_message=error_message, - completed_at=datetime.now(UTC), - ), - ) - self.logger.error("Saga failed", saga_id=saga_id, error_message=error_message) - - async def check_timeouts(self) -> None: - """Check for timed-out sagas and mark them. Single invocation.""" - cutoff_time = datetime.now(UTC) - timedelta(seconds=self.config.timeout_seconds) - timed_out = await self._repo.find_timed_out_sagas(cutoff_time) - for instance in timed_out: - self.logger.warning("Saga timed out", saga_id=instance.saga_id) - await self._save_saga( - instance.saga_id, - SagaChanges( - state=SagaState.TIMEOUT, - error_message=f"Saga timed out after {self.config.timeout_seconds} seconds", - completed_at=datetime.now(UTC), - ), - ) - - async def get_saga_status(self, saga_id: str) -> SagaDocument | None: - """Get saga instance status.""" - return await self._repo.get_saga(saga_id) - - async def get_execution_sagas(self, execution_id: str) -> list[SagaDocument]: - """Get all sagas for an execution, sorted by created_at descending (newest first).""" - result = await self._repo.get_sagas_by_execution(execution_id) - return result.sagas - - async def cancel_saga(self, saga_id: str) -> None: - """Cancel a running saga and trigger compensation. - - Uses an atomic findOneAndUpdate to set state=CANCELLED, eliminating - revision-check races with the step-execution loop. - - Raises SagaNotFoundError if saga doesn't exist. - Raises SagaInvalidStateError if saga is not in a cancellable state. - """ - saved = await self._repo.atomic_cancel_saga( - saga_id, - error_message="Saga cancelled by user request", - completed_at=datetime.now(UTC), - ) - if saved is None: - self.logger.warning("Saga not found for cancellation", saga_id=saga_id) - raise SagaNotFoundError(saga_id) - - self.logger.info( - "Saga cancellation initiated", - saga_id=saga_id, - execution_id=saved.execution_id, - user_id=saved.context_data.user_id, - ) - - if self._producer and self.config.store_events: - await self._publish_saga_cancelled_event(saved) - - if saved.completed_steps and self.config.enable_compensation: - saga = self._create_saga_instance() - context = SagaContext(saved.saga_id, saved.execution_id) - - for key, value in dataclasses.asdict(saved.context_data).items(): - context.set(key, value) - - steps = saga.get_steps() - for step in steps: - if step.name in saved.completed_steps: - compensation = step.get_compensation() - if compensation: - context.add_compensation(compensation) - - await self._compensate_saga(saga_id, context) - - self.logger.info("Saga cancelled successfully", saga_id=saga_id) - - async def _publish_saga_started_event( - self, - instance: SagaDocument, - trigger_event: ExecutionRequestedEvent, - ) -> None: - """Publish saga started event after the document is persisted.""" - try: - event = SagaStartedEvent( - saga_id=instance.saga_id, - saga_name=instance.saga_name, - execution_id=instance.execution_id, - aggregate_id=trigger_event.aggregate_id, - initial_event_id=trigger_event.event_id, - metadata=EventMetadata( - service_name="saga-orchestrator", - service_version="1.0.0", - user_id=trigger_event.metadata.user_id, - ), - ) - await self._producer.produce(event_to_produce=event, key=instance.execution_id) - self.logger.info("Published SagaStartedEvent", saga_id=instance.saga_id) - except PRODUCE_ERRORS: - self.logger.error("Failed to publish saga started event", exc_info=True) - - async def _publish_saga_cancelled_event(self, saga_instance: SagaDocument) -> None: - """Publish saga cancelled event.""" - try: - cancelled_by = saga_instance.context_data.user_id - metadata = EventMetadata( - service_name="saga-orchestrator", - service_version="1.0.0", - user_id=cancelled_by, - ) - - event = SagaCancelledEvent( - saga_id=saga_instance.saga_id, - saga_name=saga_instance.saga_name, - execution_id=saga_instance.execution_id, - aggregate_id=saga_instance.execution_id, - reason=saga_instance.error_message or "User requested cancellation", - completed_steps=saga_instance.completed_steps, - compensated_steps=saga_instance.compensated_steps, - cancelled_at=saga_instance.completed_at, - cancelled_by=cancelled_by, - metadata=metadata, - ) - - if self._producer: - await self._producer.produce(event_to_produce=event, key=saga_instance.execution_id) - - self.logger.info("Published cancellation event", saga_id=saga_instance.saga_id) - - except PRODUCE_ERRORS: - self.logger.error("Failed to publish saga cancellation event", exc_info=True) diff --git a/backend/app/services/saga/saga_service.py b/backend/app/services/saga/saga_service.py deleted file mode 100644 index a15031809..000000000 --- a/backend/app/services/saga/saga_service.py +++ /dev/null @@ -1,166 +0,0 @@ -import structlog - -from app.db import ExecutionRepository, SagaRepository -from app.db.docs import SagaDocument, UserDocument -from app.db.repositories.saga_repository import SagaListResult -from app.domain.enums import SagaState, UserRole -from app.domain.saga import ( - SagaAccessDeniedError, - SagaCancellationResult, - SagaFilter, - SagaNotFoundError, -) -from app.services.saga import SagaOrchestrator - - -class SagaService: - """Service for saga business logic and orchestration.""" - - def __init__( - self, - saga_repo: SagaRepository, - execution_repo: ExecutionRepository, - orchestrator: SagaOrchestrator, - logger: structlog.stdlib.BoundLogger, - ): - self.saga_repo = saga_repo - self.execution_repo = execution_repo - self.orchestrator = orchestrator - self.logger = logger - - self.logger.info( - "SagaService initialized", - saga_repo=type(saga_repo).__name__, - execution_repo=type(execution_repo).__name__, - orchestrator=type(orchestrator).__name__, - ) - - async def check_execution_access(self, execution_id: str, user: UserDocument) -> bool: - """Check if user has access to an execution.""" - # Admins have access to all executions - if user.role == UserRole.ADMIN: - return True - - # Check if user owns the execution - execution = await self.execution_repo.get_execution(execution_id) - if execution and execution.user_id == user.user_id: - return True - - self.logger.debug( - "Access denied to execution", - user_id=user.user_id, - execution_id=execution_id, - user_role=user.role, - execution_exists=execution is not None, - ) - return False - - async def get_saga_with_access_check(self, saga_id: str, user: UserDocument) -> SagaDocument: - """Get saga with access control.""" - self.logger.debug("Getting saga for user", saga_id=saga_id, user_id=user.user_id, user_role=user.role) - - saga = await self.saga_repo.get_saga(saga_id) - if not saga: - self.logger.warning("Saga not found", saga_id=saga_id) - raise SagaNotFoundError(saga_id) - - # Check access permissions - if not await self.check_execution_access(saga.execution_id, user): - self.logger.warning( - "Access denied to saga", - user_id=user.user_id, - saga_id=saga_id, - execution_id=saga.execution_id, - ) - raise SagaAccessDeniedError(saga_id, user.user_id) - - return saga - - async def get_execution_sagas( - self, execution_id: str, user: UserDocument, state: SagaState | None = None, limit: int = 100, skip: int = 0 - ) -> SagaListResult: - """Get sagas for an execution with access control.""" - # Check access to execution - if not await self.check_execution_access(execution_id, user): - self.logger.warning( - "Access denied to execution", - user_id=user.user_id, - execution_id=execution_id, - user_role=user.role, - ) - raise SagaAccessDeniedError(execution_id, user.user_id) - - return await self.saga_repo.get_sagas_by_execution(execution_id, state, limit=limit, skip=skip) - - async def list_user_sagas( - self, - user: UserDocument, - state: SagaState | None = None, - *, - execution_id: str | None = None, - search: str | None = None, - limit: int = 100, - skip: int = 0, - ) -> SagaListResult: - """List sagas accessible by user, optionally filtered by execution_id and a search term. - - The search term matches saga ID, saga name, execution ID or error message, case-insensitively. - """ - if execution_id: - if not await self.check_execution_access(execution_id, user): - raise SagaAccessDeniedError(execution_id, user.user_id) - saga_filter = SagaFilter(state=state, execution_ids=[execution_id], search=search) - return await self.saga_repo.list_sagas(saga_filter, limit, skip) - - saga_filter = SagaFilter(state=state, search=search) - - # Non-admin users can only see their own sagas - if user.role != UserRole.ADMIN: - user_execution_ids = await self.saga_repo.get_user_execution_ids(user.user_id) - if not user_execution_ids: - self.logger.debug( - "User has no executions, returning empty saga list", - user_id=user.user_id, - ) - return SagaListResult(sagas=[], total=0, skip=skip, limit=limit) - saga_filter.execution_ids = user_execution_ids - self.logger.debug( - "Filtering sagas for user", - user_id=user.user_id, - execution_count=len(user_execution_ids), - ) - - # Get sagas from repository - result = await self.saga_repo.list_sagas(saga_filter, limit, skip) - self.logger.debug( - "Listed sagas for user", - user_id=user.user_id, - count=len(result.sagas), - total=result.total, - state_filter=state, - ) - return result - - async def cancel_saga(self, saga_id: str, user: UserDocument) -> SagaCancellationResult: - """Cancel a saga with permission check.""" - self.logger.info( - "User requesting saga cancellation", - user_id=user.user_id, - saga_id=saga_id, - user_role=user.role, - ) - await self.get_saga_with_access_check(saga_id, user) - await self.orchestrator.cancel_saga(saga_id) - self.logger.info("User cancelled saga", user_id=user.user_id, saga_id=saga_id) - return SagaCancellationResult(success=True, message="Saga cancelled successfully", saga_id=saga_id) - - async def get_saga_statistics(self, user: UserDocument, include_all: bool = False) -> dict[str, object]: - """Get saga statistics.""" - saga_filter = None - - # Non-admin users can only see their own statistics - if user.role != UserRole.ADMIN or not include_all: - user_execution_ids = await self.saga_repo.get_user_execution_ids(user.user_id) - saga_filter = SagaFilter(execution_ids=user_execution_ids) - - return await self.saga_repo.get_saga_statistics(saga_filter) diff --git a/backend/app/services/saga/saga_step.py b/backend/app/services/saga/saga_step.py deleted file mode 100644 index bb3c136c7..000000000 --- a/backend/app/services/saga/saga_step.py +++ /dev/null @@ -1,75 +0,0 @@ -from abc import ABC, abstractmethod -from typing import Any, Generic, TypeVar - -from app.domain.events import DomainEvent - -T = TypeVar("T", bound=DomainEvent) - - -class SagaContext: - """Context passed between saga steps""" - - def __init__(self, saga_id: str, execution_id: str): - self.saga_id = saga_id - self.execution_id = execution_id - self.data: dict[str, Any] = {} - self.compensations: list[CompensationStep] = [] - - def set(self, key: str, value: Any) -> None: - """Set context data""" - self.data[key] = value - - def get(self, key: str, default: Any = None) -> Any: - """Get context data""" - return self.data.get(key, default) - - def add_compensation(self, compensation: "CompensationStep") -> None: - """Add compensation step""" - self.compensations.append(compensation) - - -class SagaStep(ABC, Generic[T]): - """Base class for saga steps""" - - def __init__(self, name: str): - self.name = name - - @abstractmethod - async def execute(self, context: SagaContext, event: T) -> bool: - """ - Execute the saga step - - Returns: - True if step succeeded and saga should continue - False if step failed and compensation should start - """ - pass - - @abstractmethod - def get_compensation(self) -> "CompensationStep | None": - """Get compensation step for this action""" - pass - - def __str__(self) -> str: - return f"SagaStep({self.name})" - - -class CompensationStep(ABC): - """Base class for compensation steps""" - - def __init__(self, name: str): - self.name = name - - @abstractmethod - async def compensate(self, context: SagaContext) -> bool: - """ - Execute compensation logic - - Returns: - True if compensation succeeded - False if compensation failed - """ - pass - - def __str__(self) -> str: - return f"CompensationStep({self.name})" diff --git a/backend/app/worker.py b/backend/app/worker.py index 09038fec3..a9e1e50ec 100644 --- a/backend/app/worker.py +++ b/backend/app/worker.py @@ -15,16 +15,16 @@ from app.core.metrics import setup_metrics from app.db.docs import ALL_DOCUMENTS from app.dlq.manager import DLQManager -from app.events.handlers import k8s_worker_router, result_processor_router, saga_router +from app.events.handlers import scheduler_router +from app.services.execution_scheduler import ExecutionScheduler from app.services.idempotency import IdempotencyManager from app.services.k8s_worker import KubernetesWorker from app.services.pod_monitor import PodMonitor -from app.services.saga import SagaOrchestrator from app.settings import Settings def main() -> None: - """Run everything outside the API: the Kafka consumers, the pod watch and the scheduled jobs.""" + """Run executions outside the API: the queue consumers, the pod watch and the scheduled jobs.""" settings = Settings(override_path="config.worker.toml") logger = setup_logger(settings.LOG_LEVEL) setup_log_exporter(settings, logger) @@ -40,7 +40,7 @@ async def run(settings: Settings, logger: structlog.stdlib.BoundLogger) -> None: container = create_worker_container(settings) broker: KafkaBroker = await container.get(KafkaBroker) - broker.include_routers(k8s_worker_router(), result_processor_router(), saga_router()) + broker.include_routers(scheduler_router()) setup_dishka(container, broker=broker, auto_inject=True) scheduler = await start_jobs(container) @@ -50,21 +50,21 @@ async def run(settings: Settings, logger: structlog.stdlib.BoundLogger) -> None: async def start_jobs(container: AsyncContainer) -> AsyncIOScheduler: - """Prepare the cluster, then schedule the pod watch, saga jobs, DLQ retries and the idempotency key count.""" + """Prepare the cluster, then schedule the pod watch, the queue check, DLQ retries and the idempotency key count.""" k8s_worker = await container.get(KubernetesWorker) await k8s_worker.ensure_namespace_security() await k8s_worker.ensure_image_pre_puller_daemonset() monitor = await container.get(PodMonitor) - orchestrator = await container.get(SagaOrchestrator) + execution_scheduler = await container.get(ExecutionScheduler) dlq_manager = await container.get(DLQManager) idempotency = await container.get(IdempotencyManager) scheduler = AsyncIOScheduler() jobs = ( ("pod_monitor_watch", monitor.watch_cycle, 5, 60), - ("saga_try_schedule", orchestrator.try_schedule_from_queue, 10, 30), - ("saga_check_timeouts", orchestrator.check_timeouts, 30, 60), + # A slot freed by its Redis deadline sends no event, so the queue is checked on a timer too + ("execution_schedule_pending", execution_scheduler.schedule_pending, 10, 30), ("dlq_monitor_retries", dlq_manager.process_monitoring_cycle, 10, 60), ("idempotency_report_keys", idempotency.report_active_keys, 30, 60), ) diff --git a/backend/grafana/provisioning/dashboards/integr8scode.json b/backend/grafana/provisioning/dashboards/integr8scode.json index 653eb1944..1f69f3977 100644 --- a/backend/grafana/provisioning/dashboards/integr8scode.json +++ b/backend/grafana/provisioning/dashboards/integr8scode.json @@ -246,7 +246,7 @@ }, { "datasource": "Victoria Metrics", - "description": "Executions the saga orchestrator has scheduled and not yet released", + "description": "Executions the scheduler has started and not yet released", "fieldConfig": { "defaults": { "color": { diff --git a/backend/tests/e2e/conftest.py b/backend/tests/e2e/conftest.py index 82e293848..f9cd033b7 100644 --- a/backend/tests/e2e/conftest.py +++ b/backend/tests/e2e/conftest.py @@ -16,7 +16,6 @@ from app.core.security import SecurityService from app.db.docs import EventDocument, ExecutionDocument, NotificationDocument -from app.db.docs.saga import SagaDocument from app.db.repositories import ( AdminEventsRepository, AdminSettingsRepository, @@ -24,7 +23,6 @@ EventRepository, ExecutionRepository, NotificationRepository, - SagaRepository, SavedScriptRepository, ) from app.domain.enums import EventType, ExecutionStatus, NotificationStatus, UserRole @@ -32,7 +30,6 @@ from app.runtime_registry import DEFAULT_LANG from app.schemas_pydantic.execution import ExecutionRequest, ExecutionResponse from app.schemas_pydantic.notification import NotificationListResponse, NotificationResponse -from app.schemas_pydantic.saga import SagaStatusResponse from app.schemas_pydantic.saved_script import SavedScriptCreateRequest from app.schemas_pydantic.user import UserCreate from app.services.admin import AdminUserService @@ -42,7 +39,6 @@ from app.services.idempotency import IdempotencyManager from app.services.notification_service import NotificationService from app.services.runtime_settings import RuntimeSettingsLoader -from app.services.saga import SagaService from app.services.saved_script_service import SavedScriptService from app.services.sse import SSERedisBus from app.services.user_settings_service import UserSettingsService @@ -179,11 +175,6 @@ async def saved_script_service(scope: AsyncContainer) -> SavedScriptService: return await scope.get(SavedScriptService) -@pytest.fixture -async def saga_service(scope: AsyncContainer) -> SagaService: - return await scope.get(SagaService) - - @pytest.fixture async def event_replay_service(scope: AsyncContainer) -> EventReplayService: return await scope.get(EventReplayService) @@ -229,11 +220,6 @@ async def execution_repository(scope: AsyncContainer) -> ExecutionRepository: return await scope.get(ExecutionRepository) -@pytest.fixture -async def saga_repository(scope: AsyncContainer) -> SagaRepository: - return await scope.get(SagaRepository) - - @pytest.fixture async def event_repository(scope: AsyncContainer) -> EventRepository: return await scope.get(EventRepository) @@ -341,18 +327,6 @@ async def store_execution( return created.execution_id -async def create_execution_with_saga( - client: AsyncClient, request: ExecutionRequest -) -> tuple[ExecutionResponse, SagaStatusResponse]: - """Create an execution and wait for POD_CREATED, which comes after the saga is stored and sends its command.""" - execution = await create_execution(client, request) - await wait_for_event(execution.execution_id, EventType.POD_CREATED) - - doc = await SagaDocument.find_one(SagaDocument.execution_id == execution.execution_id) - assert doc is not None, f"No saga document for {execution.execution_id} despite POD_CREATED" - return execution, SagaStatusResponse.model_validate(doc, from_attributes=True) - - async def create_execution_with_notification( client: AsyncClient, request: ExecutionRequest ) -> tuple[ExecutionResponse, NotificationResponse]: diff --git a/backend/tests/e2e/services/saga/test_saga_service.py b/backend/tests/e2e/services/saga/test_saga_service.py deleted file mode 100644 index f8ad9eae1..000000000 --- a/backend/tests/e2e/services/saga/test_saga_service.py +++ /dev/null @@ -1,292 +0,0 @@ -from datetime import datetime, timezone -from uuid import uuid4 - -import pytest - -from app.db.docs import ExecutionDocument, SagaDocument, UserDocument -from app.db.repositories import ExecutionRepository, SagaRepository -from app.domain.enums import SagaState, UserRole -from app.domain.saga import SagaAccessDeniedError, SagaContextData, SagaNotFoundError -from app.services.saga import SagaService - -pytestmark = [pytest.mark.e2e] - - -def make_test_user( - user_id: str = "test_user_1", - role: UserRole = UserRole.USER, -) -> UserDocument: - """Create a test user for saga access checks.""" - return UserDocument( - user_id=user_id, - username=user_id, - email=f"{user_id}@example.com", - role=role, - is_active=True, - is_superuser=role == UserRole.ADMIN, - hashed_password="unused", - created_at=datetime.now(timezone.utc), - updated_at=datetime.now(timezone.utc), - ) - - -async def create_execution_for_user( - exec_repo: ExecutionRepository, - user_id: str, -) -> str: - """Create an execution record for a user and return its ID.""" - execution = ExecutionDocument( - script="print('test')", - user_id=user_id, - ) - await exec_repo.create_execution(execution) - return execution.execution_id - - -async def create_saga_for_execution( - saga_repo: SagaRepository, - execution_id: str, - state: SagaState = SagaState.CREATED, - saga_name: str = "test_saga", -) -> SagaDocument: - """Create a saga for an execution with the given state.""" - saga = SagaDocument( - saga_id=str(uuid4()), - saga_name=saga_name, - execution_id=execution_id, - state=state, - current_step="step1", - completed_steps=[], - compensated_steps=[], - context_data=SagaContextData(), - created_at=datetime.now(timezone.utc), - updated_at=datetime.now(timezone.utc), - ) - created_saga, _ = await saga_repo.get_or_create_saga(saga) - return created_saga - - -class TestListUserSagas: - """Tests for list_user_sagas method.""" - - async def test_list_user_sagas_empty(self, saga_service: SagaService) -> None: - """List sagas for user with no sagas returns empty list.""" - # Use a unique user ID that has never been used - user = make_test_user(user_id=f"empty_user_{uuid4().hex[:8]}") - - result = await saga_service.list_user_sagas(user) - - assert result.total == 0 - assert len(result.sagas) == 0 - - async def test_list_user_sagas_with_limit( - self, saga_service: SagaService, execution_repository: ExecutionRepository, saga_repository: SagaRepository - ) -> None: - """List sagas respects limit parameter.""" - - # Create unique user - user_id = f"limit_user_{uuid4().hex[:8]}" - user = make_test_user(user_id=user_id) - - # Create 7 sagas for this user (more than limit of 5) - created_count = 7 - for i in range(created_count): - exec_id = await create_execution_for_user(execution_repository, user_id) - await create_saga_for_execution(saga_repository, exec_id, saga_name=f"saga_{i}") - - result = await saga_service.list_user_sagas(user, limit=5) - - assert len(result.sagas) == 5 - assert result.total == created_count - - async def test_list_user_sagas_with_skip( - self, saga_service: SagaService, execution_repository: ExecutionRepository, saga_repository: SagaRepository - ) -> None: - """List sagas respects skip parameter.""" - - # Create unique user - user_id = f"skip_user_{uuid4().hex[:8]}" - user = make_test_user(user_id=user_id) - - # Create 5 sagas for this user - created_count = 5 - for i in range(created_count): - exec_id = await create_execution_for_user(execution_repository, user_id) - await create_saga_for_execution(saga_repository, exec_id, saga_name=f"saga_{i}") - - # Get all sagas (skip=0) - result_all = await saga_service.list_user_sagas(user, skip=0, limit=10) - assert result_all.total == created_count - assert len(result_all.sagas) == created_count - - # Skip first 2 sagas - result_skip = await saga_service.list_user_sagas(user, skip=2, limit=10) - assert result_skip.total == created_count - assert len(result_skip.sagas) == created_count - 2 - - async def test_list_user_sagas_filter_by_state( - self, saga_service: SagaService, execution_repository: ExecutionRepository, saga_repository: SagaRepository - ) -> None: - """List sagas filtered by state returns only matching state.""" - - # Create unique user - user_id = f"state_user_{uuid4().hex[:8]}" - user = make_test_user(user_id=user_id) - - # Create sagas with different states - created_count = 3 - for i in range(created_count): - exec_id = await create_execution_for_user(execution_repository, user_id) - await create_saga_for_execution(saga_repository, exec_id, state=SagaState.CREATED, saga_name=f"created_{i}") - - # Create 2 sagas with RUNNING state - running_count = 2 - for i in range(running_count): - exec_id = await create_execution_for_user(execution_repository, user_id) - await create_saga_for_execution(saga_repository, exec_id, state=SagaState.RUNNING, saga_name=f"running_{i}") - - # Filter by CREATED state - result = await saga_service.list_user_sagas(user, state=SagaState.CREATED) - - assert result.total == created_count - assert len(result.sagas) == created_count - for saga in result.sagas: - assert saga.state == SagaState.CREATED - - async def test_list_user_sagas_search( - self, saga_service: SagaService, execution_repository: ExecutionRepository, saga_repository: SagaRepository - ) -> None: - """Search matches saga name or execution ID case-insensitively and treats regex characters literally.""" - - user_id = f"search_user_{uuid4().hex[:8]}" - user = make_test_user(user_id=user_id) - exec_ids = [await create_execution_for_user(execution_repository, user_id) for _ in range(3)] - await create_saga_for_execution(saga_repository, exec_ids[0], saga_name="Alpha_flow") - await create_saga_for_execution(saga_repository, exec_ids[1], saga_name="beta.flow") - await create_saga_for_execution(saga_repository, exec_ids[2], saga_name="gamma_flow") - - by_name = await saga_service.list_user_sagas(user, search="ALPHA") - assert [s.saga_name for s in by_name.sagas] == ["Alpha_flow"] - assert by_name.total == 1 - - literal_dot = await saga_service.list_user_sagas(user, search=".flow") - assert [s.saga_name for s in literal_dot.sagas] == ["beta.flow"] - - by_execution = await saga_service.list_user_sagas(user, search=exec_ids[2][:13]) - assert [s.execution_id for s in by_execution.sagas] == [exec_ids[2]] - - scoped = await saga_service.list_user_sagas(user, execution_id=exec_ids[0], search="beta") - assert scoped.total == 0 - assert scoped.sagas == [] - - async def test_admin_can_list_all_sagas(self, saga_service: SagaService) -> None: - """Admin user can list all sagas.""" - admin = make_test_user(user_id="admin_user", role=UserRole.ADMIN) - - result = await saga_service.list_user_sagas(admin) - - assert isinstance(result.sagas, list) - - -class TestGetSagaWithAccessCheck: - """Tests for get_saga_with_access_check method.""" - - async def test_get_saga_not_found(self, saga_service: SagaService) -> None: - """Get nonexistent saga raises SagaNotFoundError.""" - user = make_test_user() - - with pytest.raises(SagaNotFoundError): - await saga_service.get_saga_with_access_check("nonexistent-saga-id", user) - - -class TestCheckExecutionAccess: - """Tests for check_execution_access method.""" - - async def test_admin_has_access_to_any_execution( - self, saga_service: SagaService, execution_repository: ExecutionRepository - ) -> None: - """Admin has access to any execution.""" - admin = make_test_user(user_id="admin_user", role=UserRole.ADMIN) - - execution_id = await create_execution_for_user(execution_repository, f"other_user_{uuid4().hex[:8]}") - - has_access = await saga_service.check_execution_access(execution_id, admin) - assert has_access is True - - async def test_user_has_access_to_own_execution( - self, saga_service: SagaService, execution_repository: ExecutionRepository - ) -> None: - """User has access to their own execution.""" - user_id = f"saga_owner_user_{uuid4().hex[:8]}" - user = make_test_user(user_id=user_id) - - execution_id = await create_execution_for_user(execution_repository, user_id) - - has_access = await saga_service.check_execution_access(execution_id, user) - assert has_access is True - - async def test_user_no_access_to_other_execution( - self, saga_service: SagaService, execution_repository: ExecutionRepository - ) -> None: - """User does not have access to other user's execution.""" - other_user = make_test_user(user_id="different_user") - - execution_id = await create_execution_for_user(execution_repository, f"owner_user_{uuid4().hex[:8]}") - - has_access = await saga_service.check_execution_access(execution_id, other_user) - assert has_access is False - - async def test_access_to_nonexistent_execution(self, saga_service: SagaService) -> None: - """Access check for nonexistent execution returns False.""" - user = make_test_user() - - has_access = await saga_service.check_execution_access("nonexistent-id", user) - assert has_access is False - - -class TestGetExecutionSagas: - """Tests for get_execution_sagas method.""" - - async def test_get_execution_sagas_access_denied( - self, saga_service: SagaService, execution_repository: ExecutionRepository - ) -> None: - """Get sagas for execution without access raises error.""" - other_user = make_test_user(user_id="no_access_user") - - execution_id = await create_execution_for_user(execution_repository, f"owner_user_{uuid4().hex[:8]}") - - with pytest.raises(SagaAccessDeniedError): - await saga_service.get_execution_sagas(execution_id, other_user) - - async def test_get_execution_sagas_owner_access( - self, saga_service: SagaService, execution_repository: ExecutionRepository - ) -> None: - """Owner can get sagas for their execution.""" - user_id = f"saga_exec_owner_{uuid4().hex[:8]}" - user = make_test_user(user_id=user_id) - - execution_id = await create_execution_for_user(execution_repository, user_id) - - result = await saga_service.get_execution_sagas(execution_id, user) - - assert isinstance(result.sagas, list) - - -class TestGetSagaStatistics: - """Tests for get_saga_statistics method.""" - - async def test_get_saga_statistics_user(self, saga_service: SagaService) -> None: - """Get saga statistics for regular user.""" - user = make_test_user() - - stats = await saga_service.get_saga_statistics(user) - - assert isinstance(stats, dict) - - async def test_get_saga_statistics_admin_all(self, saga_service: SagaService) -> None: - """Admin can get all saga statistics.""" - admin = make_test_user(user_id="stats_admin", role=UserRole.ADMIN) - - stats = await saga_service.get_saga_statistics(admin, include_all=True) - - assert isinstance(stats, dict) diff --git a/backend/tests/e2e/test_execution_routes.py b/backend/tests/e2e/test_execution_routes.py index 66e4686b2..8f9012bcd 100644 --- a/backend/tests/e2e/test_execution_routes.py +++ b/backend/tests/e2e/test_execution_routes.py @@ -161,13 +161,12 @@ class TestExecutionCancel: async def test_cancel_running_execution( self, test_user: AsyncClient, long_running_execution_request: ExecutionRequest ) -> None: - """Running execution can be cancelled.""" + """A cancelled execution ends as cancelled at once, instead of running its 30 seconds to completion.""" response = await test_user.post("/api/v1/execute", json=long_running_execution_request.model_dump()) assert response.status_code == 200 exec_response = ExecutionResponse.model_validate(response.json()) - # Wait for pod creation (saga started + command dispatched) instead of blind sleep await wait_for_event(exec_response.execution_id, EventType.POD_CREATED) cancel_req = CancelExecutionRequest(reason="Test cancellation") @@ -182,6 +181,11 @@ async def test_cancel_running_execution( assert cancel_result.status == "cancellation_requested" assert cancel_result.message + await wait_for_event(exec_response.execution_id, EventType.RESULT_STORED, timeout=15.0) + result_response = await test_user.get(f"/api/v1/executions/{exec_response.execution_id}/result") + assert result_response.status_code == 200 + assert ExecutionResult.model_validate(result_response.json()).status == ExecutionStatus.CANCELLED + async def test_cancel_completed_execution_fails(self, test_user: AsyncClient) -> None: """Cannot cancel already completed execution.""" request = ExecutionRequest(script="print('quick')") diff --git a/backend/tests/e2e/test_k8s_worker_create_pod.py b/backend/tests/e2e/test_k8s_worker_create_pod.py index 109e161ae..5a9bbe2bc 100644 --- a/backend/tests/e2e/test_k8s_worker_create_pod.py +++ b/backend/tests/e2e/test_k8s_worker_create_pod.py @@ -10,14 +10,14 @@ async def test_worker_creates_configmap_and_pod(test_user: AsyncClient) -> None: - """Verify k8s-worker creates ConfigMap + Pod by running through the full pipeline.""" + """Verify the worker creates ConfigMap + Pod by running through the full pipeline.""" request = ExecutionRequest(script="print('k8s-test')") resp = await test_user.post("/api/v1/execute", json=request.model_dump()) assert resp.status_code == 200 execution_id = resp.json()["execution_id"] - # Saga dispatched CREATE_POD_COMMAND → k8s-worker created ConfigMap + Pod + # The worker took the execution from the queue and created its ConfigMap and pod await wait_for_event(execution_id, EventType.POD_CREATED) # Full pipeline completed: pod ran, result stored diff --git a/backend/tests/e2e/test_saga_routes.py b/backend/tests/e2e/test_saga_routes.py deleted file mode 100644 index 484163471..000000000 --- a/backend/tests/e2e/test_saga_routes.py +++ /dev/null @@ -1,287 +0,0 @@ -import pytest -from httpx2 import AsyncClient - -from app.domain.enums import SagaState -from app.schemas_pydantic.execution import ExecutionRequest -from app.schemas_pydantic.saga import ( - SagaCancellationResponse, - SagaListResponse, - SagaStatusResponse, -) -from tests.e2e.conftest import create_execution_with_saga - -pytestmark = [pytest.mark.e2e] - - -class TestGetSagaStatus: - """Tests for GET /api/v1/sagas/{saga_id}.""" - - async def test_get_saga_status( - self, - test_user: AsyncClient, - exec_request: ExecutionRequest, - ) -> None: - """Get saga status by ID returns valid response.""" - execution, saga = await create_execution_with_saga(test_user, exec_request) - - response = await test_user.get(f"/api/v1/sagas/{saga.saga_id}") - - assert response.status_code == 200 - result = SagaStatusResponse.model_validate(response.json()) - assert result.saga_id == saga.saga_id - assert result.execution_id == execution.execution_id - assert result.state in {SagaState.CREATED, SagaState.RUNNING} - assert result.saga_name - assert result.retry_count >= 0 - assert result.error_message is None - assert result.completed_steps is not None - - async def test_get_saga_not_found(self, test_user: AsyncClient) -> None: - """Get nonexistent saga returns 404.""" - response = await test_user.get("/api/v1/sagas/nonexistent-saga-id") - - assert response.status_code == 404 - - async def test_get_saga_access_denied( - self, - test_user: AsyncClient, - another_user: AsyncClient, - exec_request: ExecutionRequest, - ) -> None: - """Cannot access another user's saga.""" - _, saga = await create_execution_with_saga(test_user, exec_request) - - response = await another_user.get(f"/api/v1/sagas/{saga.saga_id}") - - assert response.status_code == 403 - - -class TestGetExecutionSagas: - """Tests for GET /api/v1/sagas/execution/{execution_id}.""" - - async def test_get_execution_sagas( - self, - test_user: AsyncClient, - exec_request: ExecutionRequest, - ) -> None: - """Get sagas for a specific execution.""" - execution, saga = await create_execution_with_saga(test_user, exec_request) - - response = await test_user.get(f"/api/v1/sagas/execution/{execution.execution_id}") - - assert response.status_code == 200 - result = SagaListResponse.model_validate(response.json()) - - assert result.total == 1 - assert len(result.sagas) == 1 - assert isinstance(result.has_more, bool) - - saga_ids = [s.saga_id for s in result.sagas] - assert saga.saga_id in saga_ids - - async def test_get_execution_sagas_with_pagination( - self, - test_user: AsyncClient, - exec_request: ExecutionRequest, - ) -> None: - """Pagination works for execution sagas.""" - execution, _ = await create_execution_with_saga(test_user, exec_request) - - response = await test_user.get( - f"/api/v1/sagas/execution/{execution.execution_id}", - params={"limit": 5, "skip": 0}, - ) - - assert response.status_code == 200 - result = SagaListResponse.model_validate(response.json()) - assert result.limit == 5 - assert result.skip == 0 - assert len(result.sagas) <= 5 - - async def test_get_execution_sagas_with_state_filter( - self, - test_user: AsyncClient, - exec_request: ExecutionRequest, - ) -> None: - """Filter sagas by state.""" - execution, saga = await create_execution_with_saga(test_user, exec_request) - - response = await test_user.get( - f"/api/v1/sagas/execution/{execution.execution_id}", - params={"state": saga.state}, - ) - - assert response.status_code == 200 - result = SagaListResponse.model_validate(response.json()) - assert len(result.sagas) == 1 - for s in result.sagas: - assert s.state == saga.state - - -class TestListSagas: - """Tests for GET /api/v1/sagas/.""" - - async def test_list_sagas( - self, - test_user: AsyncClient, - exec_request: ExecutionRequest, - ) -> None: - """List sagas for current user.""" - _, saga = await create_execution_with_saga(test_user, exec_request) - - response = await test_user.get("/api/v1/sagas/") - - assert response.status_code == 200 - result = SagaListResponse.model_validate(response.json()) - - assert result.total >= 1 - assert len(result.sagas) >= 1 - assert len(result.sagas) <= result.limit - - saga_ids = [s.saga_id for s in result.sagas] - assert saga.saga_id in saga_ids - - async def test_list_sagas_with_state_filter( - self, - test_user: AsyncClient, - exec_request: ExecutionRequest, - ) -> None: - """Filter sagas by state.""" - _, saga = await create_execution_with_saga(test_user, exec_request) - - response = await test_user.get( - "/api/v1/sagas/", - params={"state": saga.state}, - ) - - assert response.status_code == 200 - result = SagaListResponse.model_validate(response.json()) - - assert len(result.sagas) >= 1 - for s in result.sagas: - assert s.state == saga.state - - async def test_list_sagas_pagination( - self, - test_user: AsyncClient, - exec_request: ExecutionRequest, - ) -> None: - """Pagination works for saga list.""" - await create_execution_with_saga(test_user, exec_request) - - response = await test_user.get( - "/api/v1/sagas/", - params={"limit": 10, "skip": 0}, - ) - - assert response.status_code == 200 - result = SagaListResponse.model_validate(response.json()) - assert result.limit == 10 - assert result.skip == 0 - - async def test_list_sagas_search( - self, - test_user: AsyncClient, - exec_request: ExecutionRequest, - ) -> None: - """Search by saga ID returns that saga, and a term nothing matches returns none.""" - _, saga = await create_execution_with_saga(test_user, exec_request) - - response = await test_user.get("/api/v1/sagas/", params={"search": saga.saga_id.upper()}) - - assert response.status_code == 200 - result = SagaListResponse.model_validate(response.json()) - assert [s.saga_id for s in result.sagas] == [saga.saga_id] - assert result.total == 1 - - response = await test_user.get("/api/v1/sagas/", params={"search": "no-saga-has-this.*"}) - - assert response.status_code == 200 - result = SagaListResponse.model_validate(response.json()) - assert result.sagas == [] - assert result.total == 0 - - async def test_list_sagas_unauthenticated(self, client: AsyncClient) -> None: - """Unauthenticated request returns 401.""" - response = await client.get("/api/v1/sagas/") - - assert response.status_code == 401 - - -class TestCancelSaga: - """Tests for POST /api/v1/sagas/{saga_id}/cancel.""" - - async def test_cancel_saga( - self, - test_user: AsyncClient, - long_running_execution_request: ExecutionRequest, - ) -> None: - """Cancel a running saga.""" - execution, saga = await create_execution_with_saga( - test_user, - long_running_execution_request, - ) - - response = await test_user.post(f"/api/v1/sagas/{saga.saga_id}/cancel") - - assert response.status_code == 200 - result = SagaCancellationResponse.model_validate(response.json()) - assert result.saga_id == saga.saga_id - assert result.success is True - assert result.message is not None - - # cancel_saga sets state to CANCELLED synchronously in MongoDB - # before returning the HTTP response (compensation also runs inline). - status_resp = await test_user.get(f"/api/v1/sagas/{saga.saga_id}") - assert status_resp.status_code == 200 - updated_saga = SagaStatusResponse.model_validate(status_resp.json()) - assert updated_saga.state == SagaState.CANCELLED - - async def test_cancel_nonexistent_saga(self, test_user: AsyncClient) -> None: - """Cancel nonexistent saga returns 404.""" - response = await test_user.post("/api/v1/sagas/nonexistent-saga-id/cancel") - - assert response.status_code == 404 - - async def test_cancel_other_users_saga_forbidden( - self, - test_user: AsyncClient, - another_user: AsyncClient, - long_running_execution_request: ExecutionRequest, - ) -> None: - """Cannot cancel another user's saga.""" - _, saga = await create_execution_with_saga( - test_user, - long_running_execution_request, - ) - - response = await another_user.post(f"/api/v1/sagas/{saga.saga_id}/cancel") - - assert response.status_code == 403 - - -class TestSagaIsolation: - """Tests for saga access isolation between users.""" - - async def test_user_cannot_see_other_users_sagas( - self, - test_user: AsyncClient, - another_user: AsyncClient, - exec_request: ExecutionRequest, - ) -> None: - """User's saga list does not include other users' sagas.""" - _, saga = await create_execution_with_saga(test_user, exec_request) - assert saga.saga_id - - # Positive proof: owner CAN see the saga - owner_resp = await test_user.get("/api/v1/sagas/") - assert owner_resp.status_code == 200 - owner_result = SagaListResponse.model_validate(owner_resp.json()) - assert saga.saga_id in [s.saga_id for s in owner_result.sagas] - - # Negative proof: another user CANNOT see it - response = await another_user.get("/api/v1/sagas/") - assert response.status_code == 200 - - result = SagaListResponse.model_validate(response.json()) - assert saga.saga_id not in [s.saga_id for s in result.sagas] diff --git a/backend/tests/unit/app/test_main_app.py b/backend/tests/unit/app/test_main_app.py index 6dfaf24e2..727158cdf 100644 --- a/backend/tests/unit/app/test_main_app.py +++ b/backend/tests/unit/app/test_main_app.py @@ -53,7 +53,6 @@ def test_openapi_disabled_for_security(self, app: FastAPI) -> None: "/api/v1/scripts", "/api/v1/user/settings/", "/api/v1/notifications", - "/api/v1/sagas/", "/api/v1/replay/sessions", "/api/v1/events/executions/{execution_id}", "/api/v1/admin/users/", diff --git a/backend/tests/unit/app/test_worker_entrypoint.py b/backend/tests/unit/app/test_worker_entrypoint.py index db8dfd3cc..24e4cefcb 100644 --- a/backend/tests/unit/app/test_worker_entrypoint.py +++ b/backend/tests/unit/app/test_worker_entrypoint.py @@ -28,7 +28,7 @@ async def test_run_wires_the_routers_and_orders_shutdown( await module.run(test_settings, structlog.get_logger("test")) - assert len(broker.include_routers.call_args.args) == 3 + assert len(broker.include_routers.call_args.args) == 1 fast_stream.assert_called_once_with(broker, on_shutdown=[scheduler.shutdown], after_shutdown=[container.close]) fast_stream.return_value.run.assert_awaited_once() @@ -44,11 +44,10 @@ async def test_start_jobs_prepares_the_cluster_and_schedules_the_jobs(monkeypatc dependency.ensure_namespace_security.assert_awaited_once() dependency.ensure_image_pre_puller_daemonset.assert_awaited_once() - assert [c.kwargs["id"] for c in scheduler.add_job.call_args_list] == [ - "pod_monitor_watch", - "saga_try_schedule", - "saga_check_timeouts", - "dlq_monitor_retries", - "idempotency_report_keys", + assert [(c.kwargs["id"], c.args[0]) for c in scheduler.add_job.call_args_list] == [ + ("pod_monitor_watch", dependency.watch_cycle), + ("execution_schedule_pending", dependency.schedule_pending), + ("dlq_monitor_retries", dependency.process_monitoring_cycle), + ("idempotency_report_keys", dependency.report_active_keys), ] scheduler.start.assert_called_once() diff --git a/backend/tests/unit/core/test_exception_handlers.py b/backend/tests/unit/core/test_exception_handlers.py index f5dc33c2d..a35d976e8 100644 --- a/backend/tests/unit/core/test_exception_handlers.py +++ b/backend/tests/unit/core/test_exception_handlers.py @@ -50,7 +50,7 @@ def test_exception_maps_to_correct_status(self, exception: DomainError, expected async def _domain() -> None: - raise NotFoundError("Saga", "s-1") + raise NotFoundError("Script", "s-1") async def _http() -> None: @@ -78,7 +78,7 @@ def _app() -> FastAPI: @pytest.mark.parametrize( ("path", "body"), [ - ("/domain", {"status": 404, "detail": "Saga 's-1' not found"}), + ("/domain", {"status": 404, "detail": "Script 's-1' not found"}), ("/http", {"status": 401, "detail": "Not authenticated"}), ( "/validated?limit=x", diff --git a/backend/tests/unit/services/idempotency/test_idempotency_manager.py b/backend/tests/unit/services/idempotency/test_idempotency_manager.py index bbdacfc51..c24c67b00 100644 --- a/backend/tests/unit/services/idempotency/test_idempotency_manager.py +++ b/backend/tests/unit/services/idempotency/test_idempotency_manager.py @@ -35,7 +35,7 @@ def test_manager_generate_key_variants() -> None: def _event() -> MagicMock: ev = MagicMock(spec=BaseEvent) - ev.event_type = EventType.CREATE_POD_COMMAND + ev.event_type = EventType.EXECUTION_REQUESTED ev.event_id = "evt-1" return ev @@ -46,9 +46,9 @@ def _manager(repo: AsyncMock, tracer: trace.Tracer) -> IdempotencyManager: def _record(status: IdempotencyStatus) -> IdempotencyRecord: return IdempotencyRecord( - key="idempotency:create_pod_command:evt-1", + key="idempotency:execution_requested:evt-1", status=status, - event_type=EventType.CREATE_POD_COMMAND, + event_type=EventType.EXECUTION_REQUESTED, event_id="evt-1", created_at=datetime.now(timezone.utc) - timedelta(seconds=2), ttl_seconds=3600, @@ -83,7 +83,7 @@ async def test_marking_records_processing_duration( assert isinstance(seconds, float) assert seconds >= 2.0 assert (span.attributes["idempotency.event_type"], span.attributes["idempotency.status"]) == ( - EventType.CREATE_POD_COMMAND, + EventType.EXECUTION_REQUESTED, expected_status, ) @@ -113,7 +113,7 @@ async def test_check_and_reserve_of_a_new_key_reports_a_miss( assert (span.name, span.attributes) == ( "idempotency.check", { - "idempotency.event_type": EventType.CREATE_POD_COMMAND, + "idempotency.event_type": EventType.EXECUTION_REQUESTED, "idempotency.operation": "check_and_reserve", "idempotency.result": "miss", }, @@ -132,7 +132,7 @@ async def test_check_and_reserve_of_a_done_key_reports_a_hit_and_a_blocked_dupli blocked, check = span_exporter.get_finished_spans() assert (blocked.name, blocked.attributes) == ( "idempotency.duplicate_blocked", - {"idempotency.event_type": EventType.CREATE_POD_COMMAND}, + {"idempotency.event_type": EventType.EXECUTION_REQUESTED}, ) assert check.attributes is not None assert (check.name, check.attributes["idempotency.result"]) == ("idempotency.check", "hit") diff --git a/backend/tests/unit/services/pod_monitor/test_monitor.py b/backend/tests/unit/services/pod_monitor/test_monitor.py index 8b4bd7eab..dc1be24bf 100644 --- a/backend/tests/unit/services/pod_monitor/test_monitor.py +++ b/backend/tests/unit/services/pod_monitor/test_monitor.py @@ -1,4 +1,3 @@ -import types from datetime import datetime, timedelta, timezone from typing import Any from unittest.mock import AsyncMock, MagicMock, patch @@ -11,11 +10,18 @@ from opentelemetry import trace from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter -from app.domain.enums import EventType -from app.domain.events import DomainEvent +from app.domain.events import ( + DomainEvent, + EventMetadata, + ExecutionCompletedEvent, + PodRunningEvent, + ResourceUsageDomain, +) from app.events.core import UnifiedProducer +from app.services.execution_scheduler import ExecutionScheduler from app.services.pod_monitor import ErrorType, PodEventMapper, PodMonitor, PodMonitorConfig, WatchEventType from app.services.pod_monitor.monitor import PodEvent +from app.services.result_processor import ResultProcessor from tests.unit.conftest import ( make_mock_v1_api, make_mock_watch, @@ -26,6 +32,8 @@ _test_logger = structlog.get_logger("test.pod_monitor") +_METADATA = EventMetadata(service_name="pod-monitor", service_version="1.0.0", user_id="u1") + class FakeUnifiedProducer(UnifiedProducer): """Fake producer that captures events without Kafka.""" @@ -57,6 +65,8 @@ def make_pod_monitor( producer: UnifiedProducer | None = None, api_client: k8s_client.ApiClient | None = None, event_mapper: PodEventMapper | None = None, + results: ResultProcessor | None = None, + scheduler: ExecutionScheduler | None = None, mock_v1: Any | None = None, mock_watch: Any | None = None, pods: list[V1Pod] | None = None, @@ -76,6 +86,8 @@ def make_pod_monitor( logger=_test_logger, api_client=client, event_mapper=mapper, + results=results or AsyncMock(spec=ResultProcessor), + scheduler=scheduler or AsyncMock(spec=ExecutionScheduler), tracer=tracer, ) @@ -174,9 +186,11 @@ async def test_process_pod_event_full_flow(tracer: trace.Tracer, span_exporter: cfg.ignored_pod_phases = ["Unknown"] mapper = AsyncMock(spec=PodEventMapper) - mapper.map_pod_event.return_value = [types.SimpleNamespace(event_type=EventType.POD_RUNNING, execution_id="exec1")] + running = PodRunningEvent(execution_id="exec1", pod_name="test-pod", metadata=_METADATA) + mapper.map_pod_event.return_value = [running] producer = FakeUnifiedProducer() - pm = make_pod_monitor(tracer, config=cfg, event_mapper=mapper, producer=producer) + results = AsyncMock(spec=ResultProcessor) + pm = make_pod_monitor(tracer, config=cfg, event_mapper=mapper, producer=producer, results=results) await pm._process_pod_event( PodEvent(event_type=WatchEventType.ADDED, pod=make_pod(name="test-pod", phase="Running")) @@ -189,6 +203,7 @@ async def test_process_pod_event_full_flow(tracer: trace.Tracer, span_exporter: ) assert [key for _, key in producer.produced_events] == ["exec1", "exec1"] + assert results.handle_pod_progress.await_count == 2 assert span_attributes(span_exporter, "pod_monitor.event") == [ {"pod_monitor.watch_event": "ADDED", "pod.phase": "Running", "pod_monitor.published": 1}, {"pod_monitor.watch_event": "DELETED", "pod.phase": "Succeeded", "pod_monitor.published": 1}, @@ -243,12 +258,18 @@ async def test_relist_drops_pods_the_watch_missed( async def test_terminal_event_records_pod_lifetime(tracer: trace.Tracer, span_exporter: InMemorySpanExporter) -> None: mapper = AsyncMock(spec=PodEventMapper) - mapper.map_pod_event.return_value = [ - types.SimpleNamespace(event_type=EventType.EXECUTION_COMPLETED, execution_id="exec1") - ] + completed = ExecutionCompletedEvent( + execution_id="exec1", exit_code=0, resource_usage=ResourceUsageDomain(), metadata=_METADATA + ) + mapper.map_pod_event.return_value = [completed] mock_v1 = make_mock_v1_api() mock_v1.delete_namespaced_pod = AsyncMock() - pm = make_pod_monitor(tracer, event_mapper=mapper, mock_v1=mock_v1) + producer = FakeUnifiedProducer() + results = AsyncMock(spec=ResultProcessor) + scheduler = AsyncMock(spec=ExecutionScheduler) + pm = make_pod_monitor( + tracer, event_mapper=mapper, mock_v1=mock_v1, producer=producer, results=results, scheduler=scheduler + ) pod = make_pod(name="done", phase="Succeeded", labels={"language": "python"}) pod.metadata.creation_timestamp = datetime.now(timezone.utc) - timedelta(seconds=30) @@ -258,14 +279,19 @@ async def test_terminal_event_records_pod_lifetime(tracer: trace.Tracer, span_ex assert attributes["pod.lifetime_seconds"] >= 30 assert (attributes["pod.phase"], attributes["pod.language"]) == ("Succeeded", "python") mock_v1.delete_namespaced_pod.assert_awaited_once() + # The processor publishes the result once it's stored, and the slot frees for the next execution + results.handle_execution_completed.assert_awaited_once_with(completed) + assert producer.produced_events == [] + scheduler.finish.assert_awaited_once_with("exec1") async def test_non_terminal_event_records_no_lifetime( tracer: trace.Tracer, span_exporter: InMemorySpanExporter ) -> None: mapper = AsyncMock(spec=PodEventMapper) - mapper.map_pod_event.return_value = [types.SimpleNamespace(event_type=EventType.POD_RUNNING, execution_id="exec1")] - pm = make_pod_monitor(tracer, event_mapper=mapper) + mapper.map_pod_event.return_value = [PodRunningEvent(execution_id="exec1", pod_name="busy", metadata=_METADATA)] + scheduler = AsyncMock(spec=ExecutionScheduler) + pm = make_pod_monitor(tracer, event_mapper=mapper, scheduler=scheduler) pod = make_pod(name="busy", phase="Running", labels={"language": "python"}) pod.metadata.creation_timestamp = datetime.now(timezone.utc) @@ -273,6 +299,7 @@ async def test_non_terminal_event_records_no_lifetime( [attributes] = span_attributes(span_exporter, "pod_monitor.event") assert "pod.lifetime_seconds" not in attributes + scheduler.finish.assert_not_awaited() @pytest.mark.parametrize( diff --git a/backend/tests/unit/services/result_processor/test_processor.py b/backend/tests/unit/services/result_processor/test_processor.py index bf876b361..93d544f94 100644 --- a/backend/tests/unit/services/result_processor/test_processor.py +++ b/backend/tests/unit/services/result_processor/test_processor.py @@ -7,9 +7,10 @@ from app.db.docs import ExecutionDocument from app.domain.admin import SystemSettings -from app.domain.enums import ExecutionErrorType, ExecutionStatus +from app.domain.enums import EventType, ExecutionErrorType, ExecutionStatus from app.domain.events import ( EventMetadata, + ExecutionCancelledEvent, ExecutionCompletedEvent, ExecutionFailedEvent, ExecutionTimeoutEvent, @@ -17,7 +18,6 @@ PodScheduledEvent, ResourceUsageDomain, ) -from app.domain.execution import ExecutionNotFoundError from app.runtime_registry import POD_RUNTIME_CONFIGS, parse_quantity from app.services.result_processor import ResultProcessor from app.services.runtime_settings import RuntimeSettingsLoader @@ -50,45 +50,20 @@ def _make_processor( ) -class TestHandlerTypeGuards: - """Handlers must reject wrong event types with TypeError.""" - - async def test_completed_rejects_wrong_type(self, tracer: trace.Tracer) -> None: - processor = _make_processor(tracer) - wrong = ExecutionFailedEvent( - execution_id="e1", exit_code=1, error_type=ExecutionErrorType.SCRIPT_ERROR, metadata=_METADATA - ) - with pytest.raises(TypeError, match="Expected ExecutionCompletedEvent"): - await processor.handle_execution_completed(wrong) - - async def test_failed_rejects_wrong_type(self, tracer: trace.Tracer) -> None: - processor = _make_processor(tracer) - wrong = ExecutionCompletedEvent( - execution_id="e1", exit_code=0, resource_usage=ResourceUsageDomain(), metadata=_METADATA - ) - with pytest.raises(TypeError, match="Expected ExecutionFailedEvent"): - await processor.handle_execution_failed(wrong) - - async def test_timeout_rejects_wrong_type(self, tracer: trace.Tracer) -> None: - processor = _make_processor(tracer) - wrong = ExecutionCompletedEvent( - execution_id="e1", exit_code=0, resource_usage=ResourceUsageDomain(), metadata=_METADATA - ) - with pytest.raises(TypeError, match="Expected ExecutionTimeoutEvent"): - await processor.handle_execution_timeout(wrong) - - class TestHandleExecutionCompleted: - async def test_raises_when_execution_not_found(self, tracer: trace.Tracer) -> None: + async def test_skips_a_deleted_execution(self, tracer: trace.Tracer) -> None: repo = AsyncMock() repo.get_execution.return_value = None - processor = _make_processor(tracer, exec_repo=repo) + producer = AsyncMock() + processor = _make_processor(tracer, exec_repo=repo, producer=producer) event = ExecutionCompletedEvent( execution_id="missing", exit_code=0, resource_usage=ResourceUsageDomain(), metadata=_METADATA ) - with pytest.raises(ExecutionNotFoundError): - await processor.handle_execution_completed(event) + await processor.handle_execution_completed(event) + + repo.write_terminal_result.assert_not_awaited() + producer.produce.assert_not_awaited() async def test_stores_result_and_publishes(self, tracer: trace.Tracer, span_exporter: InMemorySpanExporter) -> None: repo = AsyncMock() @@ -114,7 +89,10 @@ async def test_stores_result_and_publishes(self, tracer: trace.Tracer, span_expo assert result_arg.execution_id == "e1" assert result_arg.status == ExecutionStatus.COMPLETED assert result_arg.exit_code == 0 - producer.produce.assert_awaited_once() + assert [c.kwargs["event_to_produce"].event_type for c in producer.produce.await_args_list] == [ + EventType.EXECUTION_COMPLETED, + EventType.RESULT_STORED, + ] [span] = span_exporter.get_finished_spans() assert (span.name, span.attributes) == ( "execution.result", @@ -183,7 +161,10 @@ async def test_stores_result_and_publishes(self, tracer: trace.Tracer, span_expo assert result_arg.execution_id == "e2" assert result_arg.status == ExecutionStatus.FAILED assert result_arg.exit_code == 1 - producer.produce.assert_awaited_once() + assert [c.kwargs["event_to_produce"].event_type for c in producer.produce.await_args_list] == [ + EventType.EXECUTION_FAILED, + EventType.RESULT_STORED, + ] [span] = span_exporter.get_finished_spans() assert span.attributes == { "execution.status": "failed", @@ -212,7 +193,10 @@ async def test_stores_result_and_publishes(self, tracer: trace.Tracer, span_expo assert result_arg.execution_id == "e3" assert result_arg.status == ExecutionStatus.TIMEOUT assert result_arg.exit_code == -1 - producer.produce.assert_awaited_once() + assert [c.kwargs["event_to_produce"].event_type for c in producer.produce.await_args_list] == [ + EventType.EXECUTION_TIMEOUT, + EventType.RESULT_STORED, + ] [span] = span_exporter.get_finished_spans() assert span.attributes == { "execution.status": "timeout", @@ -222,6 +206,56 @@ async def test_stores_result_and_publishes(self, tracer: trace.Tracer, span_expo } +class TestResultForAFinishedExecution: + """A cancelled execution's pod fails when it's deleted, and that late result must not reach anyone.""" + + async def test_publishes_nothing_when_the_execution_already_ended(self, tracer: trace.Tracer) -> None: + repo = AsyncMock() + repo.get_execution.return_value = ExecutionDocument( + script="x", execution_id="e4", lang="python", lang_version="3.11" + ) + repo.write_terminal_result.return_value = False + producer = AsyncMock() + processor = _make_processor(tracer, exec_repo=repo, producer=producer) + + event = ExecutionFailedEvent( + execution_id="e4", exit_code=143, error_type=ExecutionErrorType.SCRIPT_ERROR, metadata=_METADATA + ) + await processor.handle_execution_failed(event) + + repo.write_terminal_result.assert_awaited_once() + producer.produce.assert_not_awaited() + + +class TestHandleExecutionCancelled: + async def test_stores_cancelled_and_publishes_result_stored( + self, tracer: trace.Tracer, span_exporter: InMemorySpanExporter + ) -> None: + repo = AsyncMock() + repo.get_execution.return_value = ExecutionDocument( + script="x", execution_id="e5", lang="python", lang_version="3.11" + ) + repo.write_terminal_result.return_value = True + producer = AsyncMock() + processor = _make_processor(tracer, exec_repo=repo, producer=producer) + + await processor.handle_execution_cancelled( + ExecutionCancelledEvent(execution_id="e5", cancelled_by="u1", metadata=_METADATA) + ) + + result_arg = repo.write_terminal_result.call_args[0][0] + assert (result_arg.execution_id, result_arg.status, result_arg.exit_code) == ( + "e5", + ExecutionStatus.CANCELLED, + None, + ) + assert [c.kwargs["event_to_produce"].event_type for c in producer.produce.await_args_list] == [ + EventType.RESULT_STORED + ] + [span] = span_exporter.get_finished_spans() + assert span.attributes == {"execution.status": "cancelled", "execution.lang_and_version": "python-3.11"} + + class TestPodProgress: """Pod events move the stored execution status forward.""" diff --git a/backend/tests/unit/services/saga/__init__.py b/backend/tests/unit/services/saga/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/backend/tests/unit/services/saga/test_execution_saga_steps.py b/backend/tests/unit/services/saga/test_execution_saga_steps.py deleted file mode 100644 index ba630432f..000000000 --- a/backend/tests/unit/services/saga/test_execution_saga_steps.py +++ /dev/null @@ -1,111 +0,0 @@ -from unittest.mock import AsyncMock - -import pytest -import structlog - -from app.domain.events import DomainEvent, ExecutionRequestedEvent -from app.events.core import UnifiedProducer -from app.services.saga import ( - CreatePodStep, - DeletePodCompensation, - ExecutionSaga, - SagaContext, - ValidateExecutionStep, -) -from tests.conftest import make_execution_requested_event - -pytestmark = pytest.mark.unit - -_logger = structlog.get_logger() - - -def _req(timeout: int = 30, script: str = "print('x')") -> ExecutionRequestedEvent: - return make_execution_requested_event(execution_id="e1", script=script, timeout_seconds=timeout) - - -async def test_validate_execution_step_success_and_failures() -> None: - ctx = SagaContext("s1", "e1") - ok = await ValidateExecutionStep(logger=_logger).execute(ctx, _req()) - assert ok is True and ctx.get("execution_id") == "e1" - - # The admin settings bound the timeout (up to 3600 s), so the step accepts their maximum - ctx2 = SagaContext("s1", "e1") - assert await ValidateExecutionStep(logger=_logger).execute(ctx2, _req(timeout=3600)) is True - assert ctx2.get("timeout_seconds") == 3600 - - # Script too big → raises - ctx3 = SagaContext("s1", "e1") - big = "x" * (1024 * 1024 + 1) - with pytest.raises(ValueError, match="Script size exceeds limit"): - await ValidateExecutionStep(logger=_logger).execute(ctx3, _req(script=big)) - - -class _FakeProducer(UnifiedProducer): - """Fake UnifiedProducer for testing.""" - - def __init__(self) -> None: - self.events: list[DomainEvent] = [] - - async def produce( - self, event_to_produce: DomainEvent, key: str | None = None, headers: dict[str, str] | None = None - ) -> None: - self.events.append(event_to_produce) - - -async def test_create_pod_step_publish_flag_and_compensation() -> None: - prod = _FakeProducer() - - # Skip publish path - ctx = SagaContext("s1", "e1") - ctx.set("execution_id", "e1") - s1 = CreatePodStep(producer=prod, publish_commands=False, logger=_logger) - ok1 = await s1.execute(ctx, _req()) - assert ok1 is True and ctx.get("pod_creation_triggered") is False - - # Publish path succeeds - ctx2 = SagaContext("s2", "e2") - ctx2.set("execution_id", "e2") - s2 = CreatePodStep(producer=prod, publish_commands=True, logger=_logger) - ok2 = await s2.execute(ctx2, _req()) - assert ok2 is True and ctx2.get("pod_creation_triggered") is True and prod.events - - # DeletePod compensation triggers only when flagged and producer exists - comp = DeletePodCompensation(producer=prod, logger=_logger) - ctx2.set("pod_creation_triggered", True) - assert await comp.compensate(ctx2) is True - - -async def test_delete_pod_compensation_variants() -> None: - prod = _FakeProducer() - - # Not triggered -> True early - ctx = SagaContext("s", "e") - ctx.set("pod_creation_triggered", False) - assert await DeletePodCompensation(producer=prod, logger=_logger).compensate(ctx) is True - - # Triggered -> publishes delete command - ctx2 = SagaContext("s2", "e2") - ctx2.set("pod_creation_triggered", True) - ctx2.set("execution_id", "e2") - assert await DeletePodCompensation(producer=prod, logger=_logger).compensate(ctx2) is True - assert len(prod.events) == 1 - - # get_compensation return types - assert ValidateExecutionStep(logger=_logger).get_compensation() is None - assert isinstance( - CreatePodStep(prod, publish_commands=False, logger=_logger).get_compensation(), - DeletePodCompensation, - ) - - -def test_execution_saga_bind_and_get_steps_sets_flags_and_types() -> None: - s = ExecutionSaga() - s.bind_dependencies( - producer=AsyncMock(spec=UnifiedProducer), - publish_commands=True, - logger=_logger, - ) - steps = s.get_steps() - assert [step.name for step in steps] == ["validate_execution", "create_pod"] - cps = [st for st in steps if isinstance(st, CreatePodStep)][0] - assert cps.publish_commands is True diff --git a/backend/tests/unit/services/saga/test_saga_orchestrator.py b/backend/tests/unit/services/saga/test_saga_orchestrator.py deleted file mode 100644 index 9a052f47a..000000000 --- a/backend/tests/unit/services/saga/test_saga_orchestrator.py +++ /dev/null @@ -1,407 +0,0 @@ -from datetime import datetime, timezone -from typing import Any -from unittest.mock import AsyncMock - -import pytest -import structlog -from opentelemetry import trace -from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter - -from app.db.docs import SagaDocument -from app.db.repositories import SagaRepository -from app.domain.admin import SystemSettings -from app.domain.enums import ExecutionErrorType, SagaState -from app.domain.events import ( - DomainEvent, - EventMetadata, - ExecutionCompletedEvent, - ExecutionFailedEvent, - ExecutionRequestedEvent, - ResourceUsageDomain, -) -from app.domain.saga import ( - SagaChanges, - SagaConfig, - SagaNotFoundError, -) -from app.events.core import UnifiedProducer -from app.services.execution_queue import ExecutionQueueService -from app.services.runtime_settings import RuntimeSettingsLoader -from app.services.saga import ExecutionSaga, SagaOrchestrator -from tests.conftest import make_execution_requested_event - -pytestmark = pytest.mark.unit - -_test_logger = structlog.get_logger("test.services.saga.orchestrator") - - -class _FakeRepo(SagaRepository): - """Fake SagaRepository for testing.""" - - def __init__(self) -> None: - self.saved: list[SagaDocument] = [] - self.existing: dict[tuple[str, str], SagaDocument] = {} - self.fail_on_create: bool = False - - def _find_by_id(self, saga_id: str) -> SagaDocument | None: - for saga in [*self.saved, *self.existing.values()]: - if saga.saga_id == saga_id: - return saga - return None - - async def get_or_create_saga(self, saga: SagaDocument) -> tuple[SagaDocument, bool]: - if self.fail_on_create: - raise RuntimeError("Simulated DB failure") - key = (saga.execution_id, saga.saga_name) - if key in self.existing: - return self.existing[key], False - self.existing[key] = saga - self.saved.append(saga) - return saga, True - - async def get_saga(self, saga_id: str) -> SagaDocument | None: - return self._find_by_id(saga_id) - - async def get_saga_by_execution_and_name(self, execution_id: str, saga_name: str) -> SagaDocument | None: - return self.existing.get((execution_id, saga_name)) - - async def save_saga(self, saga_id: str, changes: SagaChanges) -> SagaDocument | None: - saga = self._find_by_id(saga_id) - if not saga: - return None - for field, value in changes.items(): - setattr(saga, field, value) - saga.updated_at = datetime.now(timezone.utc) - return saga - - async def atomic_cancel_saga(self, saga_id: str, error_message: str, completed_at: datetime) -> SagaDocument | None: - return await self.save_saga( - saga_id, SagaChanges(state=SagaState.CANCELLED, error_message=error_message, completed_at=completed_at) - ) - - -class _FakeProd(UnifiedProducer): - """Fake UnifiedProducer for testing.""" - - def __init__(self) -> None: - pass # Skip parent __init__ - - async def produce( - self, event_to_produce: DomainEvent, key: str | None = None, headers: dict[str, str] | None = None - ) -> None: - return None - - -class _FakeQueue(ExecutionQueueService): - """Fake ExecutionQueueService for testing.""" - - def __init__(self) -> None: - self.enqueued: list[ExecutionRequestedEvent] = [] - self._pending: list[tuple[str, ExecutionRequestedEvent]] = [] - self.released: list[str] = [] - self.released_user_slots: list[str] = [] - self._retry_counts: dict[str, int] = {} - - async def enqueue(self, event: ExecutionRequestedEvent) -> int: - self.enqueued.append(event) - self._pending.append((event.execution_id, event)) - return len(self._pending) - 1 - - async def try_schedule(self, max_active: int) -> tuple[str, ExecutionRequestedEvent] | None: - if not self._pending: - return None - return self._pending.pop(0) - - async def release(self, execution_id: str) -> None: - self.released.append(execution_id) - - async def release_user_slot(self, execution_id: str) -> None: - self.released_user_slots.append(execution_id) - - async def remove(self, execution_id: str) -> bool: - self._pending = [(eid, ev) for eid, ev in self._pending if eid != execution_id] - return True - - async def increment_retry_count(self, execution_id: str) -> int: - self._retry_counts[execution_id] = self._retry_counts.get(execution_id, 0) + 1 - return self._retry_counts[execution_id] - - async def update_priority(self, execution_id: str, new_priority: Any) -> bool: - return True - - async def get_queue_status(self) -> dict[str, int]: - return {"queue_depth": len(self._pending), "active_count": 0} - - async def get_pending_by_priority(self) -> dict[str, int]: - return {} - - -class _FakeRuntimeSettings(RuntimeSettingsLoader): - """Fake RuntimeSettingsLoader for testing.""" - - def __init__(self) -> None: - pass # Skip parent __init__ - - async def get_effective_settings(self) -> SystemSettings: - return SystemSettings( - max_timeout_seconds=30, - memory_limit="128Mi", - cpu_limit="100m", - max_concurrent_executions=10, - session_timeout_minutes=30, - ) - - -def _orch( - repo: SagaRepository | None = None, - queue: _FakeQueue | None = None, - tracer: trace.Tracer | None = None, -) -> SagaOrchestrator: - return SagaOrchestrator( - config=SagaConfig(name="t", enable_compensation=True, store_events=True, publish_commands=True), - saga_repository=repo or _FakeRepo(), - producer=_FakeProd(), - logger=_test_logger, - queue_service=queue or _FakeQueue(), - runtime_settings=_FakeRuntimeSettings(), - tracer=tracer or trace.NoOpTracer(), - ) - - -async def test_handle_event_enqueues_and_starts_saga() -> None: - fake_repo = _FakeRepo() - fake_queue = _FakeQueue() - orch = _orch(repo=fake_repo, queue=fake_queue) - event = make_execution_requested_event(execution_id="e") - await orch.handle_execution_requested(event) - # Event was enqueued - assert len(fake_queue.enqueued) == 1 - assert fake_queue.enqueued[0].execution_id == "e" - # The saga is created and fully executed (steps run to completion) - assert len(fake_repo.saved) >= 1 - first_saved = fake_repo.saved[0] - assert first_saved.execution_id == "e" - assert first_saved.saga_name == ExecutionSaga.get_name() - - -async def test_existing_saga_short_circuits() -> None: - fake_repo = _FakeRepo() - fake_queue = _FakeQueue() - saga_name = ExecutionSaga.get_name() - s = SagaDocument(saga_id="sX", saga_name=saga_name, execution_id="e", state=SagaState.RUNNING) - fake_repo.existing[("e", saga_name)] = s - orch = _orch(repo=fake_repo, queue=fake_queue) - # Should not create a duplicate — returns existing - await orch.handle_execution_requested(make_execution_requested_event(execution_id="e")) - # Event was still enqueued (queue is separate from saga) - assert len(fake_queue.enqueued) == 1 - # No new sagas saved — existing saga was returned as-is - assert fake_repo.saved == [] - # Slot stays held — execution is already running, released on completion - assert fake_queue.released == [] - - -async def test_resolve_completion_releases_queue() -> None: - fake_repo = _FakeRepo() - fake_queue = _FakeQueue() - orch = _orch(repo=fake_repo, queue=fake_queue) - - # First create a saga - event = make_execution_requested_event(execution_id="e1") - await orch.handle_execution_requested(event) - - # Now complete it - completed_event = ExecutionCompletedEvent( - execution_id="e1", - aggregate_id="e1", - exit_code=0, - stdout="ok", - stderr="", - resource_usage=ResourceUsageDomain( - execution_time_wall_seconds=1.0, - cpu_time_jiffies=100, - clk_tck_hertz=100, - peak_memory_kb=1024, - ), - metadata=EventMetadata(service_name="test", service_version="1.0.0", user_id="u"), - ) - await orch.handle_execution_completed(completed_event) - - # Execution should be released from queue, and its user slot with it - assert "e1" in fake_queue.released - assert "e1" in fake_queue.released_user_slots - - -async def test_start_saga_failure_requeues_execution() -> None: - fake_repo = _FakeRepo() - fake_queue = _FakeQueue() - fake_repo.fail_on_create = True - orch = _orch(repo=fake_repo, queue=fake_queue) - - event = make_execution_requested_event(execution_id="e1") - await orch.handle_execution_requested(event) - - # Event was enqueued initially - assert fake_queue.enqueued[0].execution_id == "e1" - # Slot was released on failure - assert "e1" in fake_queue.released - # Event was re-enqueued so the execution is not lost - assert len(fake_queue.enqueued) == 2 - assert fake_queue.enqueued[1].execution_id == "e1" - - -async def test_resolve_completion_releases_slot_when_saga_already_terminal() -> None: - """Slot must be released even when the saga is already in a terminal state.""" - fake_repo = _FakeRepo() - fake_queue = _FakeQueue() - orch = _orch(repo=fake_repo, queue=fake_queue) - - # Create a saga and move it to TIMEOUT (simulating check_timeouts) - event = make_execution_requested_event(execution_id="e1") - await orch.handle_execution_requested(event) - saga = fake_repo.saved[0] - saga.state = SagaState.TIMEOUT - - fake_queue.released.clear() - - # Now a COMPLETED event arrives — saga is already terminal - completed_event = ExecutionCompletedEvent( - execution_id="e1", - aggregate_id="e1", - exit_code=0, - stdout="ok", - stderr="", - resource_usage=ResourceUsageDomain( - execution_time_wall_seconds=1.0, - cpu_time_jiffies=100, - clk_tck_hertz=100, - peak_memory_kb=1024, - ), - metadata=EventMetadata(service_name="test", service_version="1.0.0", user_id="u"), - ) - await orch.handle_execution_completed(completed_event) - - # Slot released despite saga being already terminal - assert "e1" in fake_queue.released - - -async def test_resolve_completion_releases_slot_when_no_saga_found() -> None: - """Slot must be released even when no saga record exists.""" - fake_repo = _FakeRepo() - fake_queue = _FakeQueue() - orch = _orch(repo=fake_repo, queue=fake_queue) - - completed_event = ExecutionCompletedEvent( - execution_id="orphan", - aggregate_id="orphan", - exit_code=0, - stdout="ok", - stderr="", - resource_usage=ResourceUsageDomain( - execution_time_wall_seconds=1.0, - cpu_time_jiffies=100, - clk_tck_hertz=100, - peak_memory_kb=1024, - ), - metadata=EventMetadata(service_name="test", service_version="1.0.0", user_id="u"), - ) - await orch.handle_execution_completed(completed_event) - - # Slot released even though no saga was found - assert "orphan" in fake_queue.released - - -class _VanishingRepo(_FakeRepo): - """A repository whose saga disappears once ``vanished`` is set, as a user deletion does mid-flight.""" - - vanished = False - - async def save_saga(self, saga_id: str, changes: SagaChanges) -> SagaDocument | None: - return None if self.vanished else await super().save_saga(saga_id, changes) - - -async def test_resolve_completion_releases_slot_when_saga_deleted_before_update() -> None: - """A saga removed between the read and the update (user deletion) must not keep the slot.""" - fake_repo = _VanishingRepo() - fake_queue = _FakeQueue() - orch = _orch(repo=fake_repo, queue=fake_queue) - await orch.handle_execution_requested(make_execution_requested_event(execution_id="e1")) - fake_queue.released.clear() - fake_repo.vanished = True - - await orch.handle_execution_failed( - ExecutionFailedEvent( - execution_id="e1", - aggregate_id="e1", - exit_code=1, - error_type=ExecutionErrorType.SCRIPT_ERROR, - stdout="", - stderr="boom", - metadata=EventMetadata(service_name="test", service_version="1.0.0", user_id="u"), - ) - ) - - assert fake_queue.released == ["e1"] - - -async def test_max_retries_exceeded_fails_execution() -> None: - """After _MAX_SAGA_START_RETRIES failures, the execution is not re-enqueued and the slot is released.""" - fake_repo = _FakeRepo() - fake_queue = _FakeQueue() - fake_repo.fail_on_create = True - orch = _orch(repo=fake_repo, queue=fake_queue) - - event = make_execution_requested_event(execution_id="e1") - - # Simulate repeated failures by pre-setting retry count to threshold - 1 - fake_queue._retry_counts["e1"] = orch._MAX_SAGA_START_RETRIES - 1 - - await orch.handle_execution_requested(event) - - # Retry count reached max — execution should NOT be re-enqueued a second time - # (only the initial enqueue from handle_execution_requested) - assert len(fake_queue.enqueued) == 1 - # Slot was released - assert "e1" in fake_queue.released - - -async def test_retry_count_increments_across_failures() -> None: - """Each saga start failure increments the retry count via the queue service.""" - fake_repo = _FakeRepo() - fake_queue = _FakeQueue() - fake_repo.fail_on_create = True - orch = _orch(repo=fake_repo, queue=fake_queue) - - await orch.handle_execution_requested(make_execution_requested_event(execution_id="e1")) - for attempt in range(1, orch._MAX_SAGA_START_RETRIES): - assert fake_queue._retry_counts["e1"] == attempt - assert len(fake_queue.enqueued) == attempt + 1 - assert fake_queue.released_user_slots == [] - await orch.try_schedule_from_queue() - - # Giving up stops the re-enqueues and frees the user's slot - assert fake_queue._retry_counts["e1"] == orch._MAX_SAGA_START_RETRIES - assert len(fake_queue.enqueued) == orch._MAX_SAGA_START_RETRIES - assert fake_queue.released_user_slots == ["e1"] - - -async def test_scheduler_job_reports_queue_depth_and_active_slots( - monkeypatch: pytest.MonkeyPatch, tracer: trace.Tracer, span_exporter: InMemorySpanExporter -) -> None: - fake_queue = _FakeQueue() - monkeypatch.setattr(fake_queue, "get_queue_status", AsyncMock(return_value={"queue_depth": 3, "active_count": 2})) - - await _orch(queue=fake_queue, tracer=tracer).try_schedule_from_queue() - - [span] = span_exporter.get_finished_spans() - assert (span.name, span.attributes) == ("queue.schedule_pending", {"queue.depth": 3, "queue.active": 2}) - - -async def test_cancel_missing_saga_raises_not_found() -> None: - with pytest.raises(SagaNotFoundError): - await _orch().cancel_saga("missing") - - -async def test_update_of_missing_saga_raises_not_found() -> None: - with pytest.raises(SagaNotFoundError): - await _orch()._fail_saga("missing", "boom") diff --git a/backend/tests/unit/services/saga/test_saga_step_and_base.py b/backend/tests/unit/services/saga/test_saga_step_and_base.py deleted file mode 100644 index a77b12342..000000000 --- a/backend/tests/unit/services/saga/test_saga_step_and_base.py +++ /dev/null @@ -1,34 +0,0 @@ -import pytest - -from app.domain.events import SystemErrorEvent -from app.services.saga.saga_step import CompensationStep, SagaContext, SagaStep - -pytestmark = pytest.mark.unit - - -class _DummyComp(CompensationStep): - def __init__(self) -> None: - super().__init__("dummy") - - async def compensate(self, context: SagaContext) -> bool: - return True - - -async def test_context_add_compensation() -> None: - ctx = SagaContext("s1", "e1") - comp = _DummyComp() - ctx.add_compensation(comp) - assert len(ctx.compensations) == 1 - - -class _NamedStep(SagaStep[SystemErrorEvent]): - async def execute(self, context: SagaContext, event: SystemErrorEvent) -> bool: - return True - - def get_compensation(self) -> CompensationStep | None: - return None - - -def test_saga_step_str() -> None: - s = _NamedStep("nm") - assert str(s) == "SagaStep(nm)" diff --git a/backend/tests/unit/services/test_execution_queue.py b/backend/tests/unit/services/test_execution_queue.py index 1565849c2..740a704d3 100644 --- a/backend/tests/unit/services/test_execution_queue.py +++ b/backend/tests/unit/services/test_execution_queue.py @@ -164,7 +164,7 @@ async def test_update_priority_missing(queue_service: ExecutionQueueService, moc async def test_remove(queue_service: ExecutionQueueService, mock_redis: AsyncMock) -> None: pipe = MagicMock() # 5 ZREM results + 1 DELETE (event) + 1 DELETE (retry) + 1 SREM - pipe.execute = AsyncMock(return_value=[0, 0, 1, 0, 0, 1, 0, 0]) + pipe.execute = AsyncMock(return_value=[0, 0, 1, 0, 0, 1, 0]) mock_redis.pipeline.return_value = pipe result = await queue_service.remove("e1") assert result is True @@ -172,7 +172,7 @@ async def test_remove(queue_service: ExecutionQueueService, mock_redis: AsyncMoc async def test_remove_not_found(queue_service: ExecutionQueueService, mock_redis: AsyncMock) -> None: pipe = MagicMock() - pipe.execute = AsyncMock(return_value=[0, 0, 0, 0, 0, 0, 0, 0]) + pipe.execute = AsyncMock(return_value=[0, 0, 0, 0, 0, 0, 0]) mock_redis.pipeline.return_value = pipe result = await queue_service.remove("e1") assert result is False @@ -201,17 +201,6 @@ async def test_get_pending_by_priority(queue_service: ExecutionQueueService, moc assert QueuePriority.LOW not in counts -async def test_increment_retry_count(queue_service: ExecutionQueueService, mock_redis: AsyncMock) -> None: - mock_redis.incr = AsyncMock(return_value=1) - mock_redis.expire = AsyncMock(return_value=True) - - count = await queue_service.increment_retry_count("e1") - - assert count == 1 - mock_redis.incr.assert_awaited_once_with("exec_queue:retries:e1") - mock_redis.expire.assert_awaited_once() - - @pytest.mark.parametrize(("script_result", "expected"), [(1, True), (0, False)]) async def test_reserve_user_slot( queue_service: ExecutionQueueService, mock_redis: AsyncMock, script_result: int, expected: bool diff --git a/backend/tests/unit/services/test_execution_scheduler.py b/backend/tests/unit/services/test_execution_scheduler.py new file mode 100644 index 000000000..2418eb0b2 --- /dev/null +++ b/backend/tests/unit/services/test_execution_scheduler.py @@ -0,0 +1,122 @@ +from unittest.mock import AsyncMock, MagicMock + +import pytest +import structlog +from opentelemetry import trace +from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter + +from app.domain.admin import SystemSettings +from app.domain.enums import ExecutionErrorType +from app.domain.events import EventMetadata, ExecutionCancelledEvent, ExecutionFailedEvent, ExecutionRequestedEvent +from app.services.execution_queue import ExecutionQueueService +from app.services.execution_scheduler import ExecutionScheduler +from app.services.k8s_worker import KubernetesWorker +from app.services.result_processor import ResultProcessor +from app.services.runtime_settings import RuntimeSettingsLoader + +pytestmark = pytest.mark.unit + +_METADATA = EventMetadata(service_name="tests", service_version="1.0.0", user_id="u1") + + +def _requested(execution_id: str) -> ExecutionRequestedEvent: + return ExecutionRequestedEvent( + execution_id=execution_id, + aggregate_id=execution_id, + script="print(1)", + language="python", + language_version="3.14", + runtime_image="python:3.14-slim", + runtime_command=["python"], + runtime_filename="main.py", + timeout_seconds=30, + cpu_limit="100m", + memory_limit="128Mi", + cpu_request="50m", + memory_request="64Mi", + metadata=_METADATA, + ) + + +def _scheduler(tracer: trace.Tracer, scheduled: list[ExecutionRequestedEvent]) -> tuple[ExecutionScheduler, MagicMock]: + """A scheduler whose queue hands out ``scheduled`` in order; the returned mock records every dependency call.""" + queue = AsyncMock(spec=ExecutionQueueService) + queue.try_schedule.side_effect = [*((e.execution_id, e) for e in scheduled), None] + queue.get_queue_status.return_value = {"queue_depth": 3, "active_count": len(scheduled)} + runtime_settings = AsyncMock(spec=RuntimeSettingsLoader) + runtime_settings.get_effective_settings.return_value = SystemSettings(max_concurrent_executions=7) + calls = MagicMock() + calls.attach_mock(queue, "queue") + calls.attach_mock(AsyncMock(spec=KubernetesWorker), "k8s") + calls.attach_mock(AsyncMock(spec=ResultProcessor), "results") + scheduler = ExecutionScheduler( + queue=queue, + k8s_worker=calls.k8s, + results=calls.results, + runtime_settings=runtime_settings, + logger=structlog.get_logger("test.execution_scheduler"), + tracer=tracer, + ) + return scheduler, calls + + +async def test_a_request_is_queued_and_started_while_slots_are_free( + tracer: trace.Tracer, span_exporter: InMemorySpanExporter +) -> None: + first, second = _requested("e1"), _requested("e2") + scheduler, calls = _scheduler(tracer, [first, second]) + + await scheduler.handle_execution_requested(first) + + calls.queue.enqueue.assert_awaited_once_with(first) + assert calls.queue.try_schedule.await_args_list[0].args == (7,) + assert [c.args for c in calls.k8s.create_pod.await_args_list] == [(first,), (second,)] + [span] = [s for s in span_exporter.get_finished_spans() if s.name == "queue.schedule_pending"] + assert span.attributes == {"queue.depth": 3, "queue.active": 2} + + +async def test_a_pod_that_cannot_be_created_fails_the_execution_and_frees_its_slots(tracer: trace.Tracer) -> None: + broken, fine = _requested("e1"), _requested("e2") + scheduler, calls = _scheduler(tracer, [broken, fine]) + calls.k8s.create_pod.side_effect = [RuntimeError("quota exceeded"), None] + + await scheduler.schedule_pending() + + calls.results.handle_execution_failed.assert_awaited_once() + failed: ExecutionFailedEvent = calls.results.handle_execution_failed.await_args.args[0] + assert (failed.execution_id, failed.error_type, failed.exit_code, failed.stderr) == ( + "e1", + ExecutionErrorType.SYSTEM_ERROR, + -1, + "Failed to create pod: quota exceeded", + ) + calls.queue.release.assert_awaited_once_with("e1") + calls.queue.release_user_slot.assert_awaited_once_with("e1") + assert calls.k8s.create_pod.await_count == 2 + + +async def test_cancel_stores_the_cancel_before_it_deletes_the_pod(tracer: trace.Tracer) -> None: + scheduler, calls = _scheduler(tracer, []) + cancel = ExecutionCancelledEvent(execution_id="e1", cancelled_by="u1", metadata=_METADATA) + + await scheduler.handle_execution_cancelled(cancel) + + assert [name for name, *_ in calls.mock_calls][:5] == [ + "queue.remove", + "results.handle_execution_cancelled", + "k8s.delete_pod", + "queue.release", + "queue.release_user_slot", + ] + calls.k8s.delete_pod.assert_awaited_once_with("e1") + + +async def test_finish_frees_the_slots_and_starts_the_next_execution(tracer: trace.Tracer) -> None: + waiting = _requested("e2") + scheduler, calls = _scheduler(tracer, [waiting]) + + await scheduler.finish("e1") + + calls.queue.release.assert_awaited_once_with("e1") + calls.queue.release_user_slot.assert_awaited_once_with("e1") + calls.k8s.create_pod.assert_awaited_once_with(waiting) diff --git a/backend/tests/unit/services/test_pod_builder.py b/backend/tests/unit/services/test_pod_builder.py index 5ec28fdce..e81f376ab 100644 --- a/backend/tests/unit/services/test_pod_builder.py +++ b/backend/tests/unit/services/test_pod_builder.py @@ -4,7 +4,7 @@ from kubernetes_asyncio import client as k8s_client from app.domain.enums import QueuePriority -from app.domain.events import CreatePodCommandEvent, EventMetadata +from app.domain.events import EventMetadata, ExecutionRequestedEvent from app.runtime_registry import LANGUAGE_SPECS, POD_RUNTIME_CONFIGS, RUNTIME_REGISTRY from app.services.k8s_worker import PodBuilder from app.settings import Settings @@ -20,11 +20,10 @@ def pod_builder(self, test_settings: Settings) -> PodBuilder: return PodBuilder(settings=test_settings) @pytest.fixture - def create_pod_command(self) -> CreatePodCommandEvent: - """Create sample pod command event.""" - return CreatePodCommandEvent( + def requested(self) -> ExecutionRequestedEvent: + """Create a sample execution request.""" + return ExecutionRequestedEvent( execution_id=str(uuid4()), - saga_id=str(uuid4()), script="print('hello world')", language="python", language_version="3.11", @@ -40,9 +39,9 @@ def create_pod_command(self) -> CreatePodCommandEvent: metadata=EventMetadata(user_id=str(uuid4()), service_name="test-service", service_version="1.0.0"), ) - def test_build_pod_manifest(self, pod_builder: PodBuilder, create_pod_command: CreatePodCommandEvent) -> None: + def test_build_pod_manifest(self, pod_builder: PodBuilder, requested: ExecutionRequestedEvent) -> None: """Test building pod manifest.""" - pod = pod_builder.build_pod_manifest(create_pod_command) + pod = pod_builder.build_pod_manifest(requested) # Verify basic pod structure assert isinstance(pod, k8s_client.V1Pod) @@ -50,20 +49,19 @@ def test_build_pod_manifest(self, pod_builder: PodBuilder, create_pod_command: C assert pod.kind == "Pod" # Verify metadata - assert pod.metadata.name == f"executor-{create_pod_command.execution_id}" + assert pod.metadata.name == f"executor-{requested.execution_id}" assert pod.metadata.namespace == "integr8scode" assert pod.metadata.labels["app"] == "integr8s" assert pod.metadata.labels["component"] == "executor" - assert pod.metadata.labels["execution-id"] == create_pod_command.execution_id + assert pod.metadata.labels["execution-id"] == requested.execution_id assert pod.metadata.labels["language"] == "python" # Verify annotations - assert pod.metadata.annotations["integr8s.io/execution-id"] == create_pod_command.execution_id - assert pod.metadata.annotations["integr8s.io/saga-id"] == create_pod_command.saga_id + assert pod.metadata.annotations["integr8s.io/execution-id"] == requested.execution_id - def test_build_pod_spec_security(self, pod_builder: PodBuilder, create_pod_command: CreatePodCommandEvent) -> None: + def test_build_pod_spec_security(self, pod_builder: PodBuilder, requested: ExecutionRequestedEvent) -> None: """Test pod security settings.""" - pod = pod_builder.build_pod_manifest(create_pod_command) + pod = pod_builder.build_pod_manifest(requested) spec = pod.spec # Verify pod-level security @@ -80,11 +78,9 @@ def test_build_pod_spec_security(self, pod_builder: PodBuilder, create_pod_comma assert spec.host_pid is False assert spec.host_ipc is False - def test_container_security_context( - self, pod_builder: PodBuilder, create_pod_command: CreatePodCommandEvent - ) -> None: + def test_container_security_context(self, pod_builder: PodBuilder, requested: ExecutionRequestedEvent) -> None: """Test container security context.""" - pod = pod_builder.build_pod_manifest(create_pod_command) + pod = pod_builder.build_pod_manifest(requested) container = pod.spec.containers[0] # Verify container security @@ -100,12 +96,12 @@ def test_container_security_context( assert container.stdin is False assert container.tty is False - def test_container_resources(self, pod_builder: PodBuilder, create_pod_command: CreatePodCommandEvent) -> None: + def test_container_resources(self, pod_builder: PodBuilder, requested: ExecutionRequestedEvent) -> None: """Test container resource limits.""" - pod = pod_builder.build_pod_manifest(create_pod_command) + pod = pod_builder.build_pod_manifest(requested) container = pod.spec.containers[0] - # Verify resources from command + # Verify resources from the request assert container.resources.requests["cpu"] == "200m" assert container.resources.requests["memory"] == "256Mi" assert container.resources.limits["cpu"] == "1000m" @@ -113,9 +109,8 @@ def test_container_resources(self, pod_builder: PodBuilder, create_pod_command: def test_container_resources_defaults(self, pod_builder: PodBuilder, test_settings: Settings) -> None: """Test container resource defaults.""" - command = CreatePodCommandEvent( + event = ExecutionRequestedEvent( execution_id=str(uuid4()), - saga_id=str(uuid4()), script="print('hello')", language="python", language_version="3.11", @@ -136,7 +131,7 @@ def test_container_resources_defaults(self, pod_builder: PodBuilder, test_settin ), ) - pod = pod_builder.build_pod_manifest(command) + pod = pod_builder.build_pod_manifest(event) container = pod.spec.containers[0] # Verify default resources from settings @@ -145,9 +140,9 @@ def test_container_resources_defaults(self, pod_builder: PodBuilder, test_settin assert container.resources.limits["cpu"] == test_settings.K8S_POD_CPU_LIMIT assert container.resources.limits["memory"] == test_settings.K8S_POD_MEMORY_LIMIT - def test_pod_volumes(self, pod_builder: PodBuilder, create_pod_command: CreatePodCommandEvent) -> None: + def test_pod_volumes(self, pod_builder: PodBuilder, requested: ExecutionRequestedEvent) -> None: """Test pod volume configuration.""" - pod = pod_builder.build_pod_manifest(create_pod_command) + pod = pod_builder.build_pod_manifest(requested) volumes = {v.name: v for v in pod.spec.volumes} # Verify all required volumes @@ -158,11 +153,11 @@ def test_pod_volumes(self, pod_builder: PodBuilder, create_pod_command: CreatePo # Verify ConfigMap volumes script_vol = volumes["script-volume"] - assert script_vol.config_map.name == f"script-{create_pod_command.execution_id}" + assert script_vol.config_map.name == f"script-{requested.execution_id}" assert script_vol.config_map.items[0].key == "script.py" entrypoint_vol = volumes["entrypoint-volume"] - assert entrypoint_vol.config_map.name == f"script-{create_pod_command.execution_id}" + assert entrypoint_vol.config_map.name == f"script-{requested.execution_id}" assert entrypoint_vol.config_map.items[0].key == "entrypoint.sh" # Verify EmptyDir volumes with size limits @@ -172,9 +167,9 @@ def test_pod_volumes(self, pod_builder: PodBuilder, create_pod_command: CreatePo tmp_vol = volumes["tmp-volume"] assert tmp_vol.empty_dir.size_limit == "10Mi" - def test_container_volume_mounts(self, pod_builder: PodBuilder, create_pod_command: CreatePodCommandEvent) -> None: + def test_container_volume_mounts(self, pod_builder: PodBuilder, requested: ExecutionRequestedEvent) -> None: """Test container volume mounts.""" - pod = pod_builder.build_pod_manifest(create_pod_command) + pod = pod_builder.build_pod_manifest(requested) container = pod.spec.containers[0] mounts = {m.name: m for m in container.volume_mounts} @@ -191,12 +186,12 @@ def test_container_volume_mounts(self, pod_builder: PodBuilder, create_pod_comma assert mounts["tmp-volume"].mount_path == "/tmp" assert mounts["tmp-volume"].read_only is None # Writable - def test_build_config_map(self, pod_builder: PodBuilder, create_pod_command: CreatePodCommandEvent) -> None: + def test_build_config_map(self, pod_builder: PodBuilder, requested: ExecutionRequestedEvent) -> None: """Test ConfigMap creation.""" script_content = "print('hello world')" entrypoint_content = "#!/bin/sh\nexec $@" - config_map = pod_builder.build_config_map(create_pod_command, script_content, entrypoint_content) + config_map = pod_builder.build_config_map(requested, script_content, entrypoint_content) # Verify ConfigMap structure assert isinstance(config_map, k8s_client.V1ConfigMap) @@ -204,20 +199,17 @@ def test_build_config_map(self, pod_builder: PodBuilder, create_pod_command: Cre assert config_map.kind == "ConfigMap" # Verify metadata - assert config_map.metadata.name == f"script-{create_pod_command.execution_id}" + assert config_map.metadata.name == f"script-{requested.execution_id}" assert config_map.metadata.namespace == "integr8scode" - assert config_map.metadata.labels["execution-id"] == create_pod_command.execution_id - assert config_map.metadata.labels["saga-id"] == create_pod_command.saga_id + assert config_map.metadata.labels["execution-id"] == requested.execution_id # Verify data assert config_map.data["script.py"] == script_content assert config_map.data["entrypoint.sh"] == entrypoint_content - def test_pod_timeout_configuration( - self, pod_builder: PodBuilder, create_pod_command: CreatePodCommandEvent - ) -> None: + def test_pod_timeout_configuration(self, pod_builder: PodBuilder, requested: ExecutionRequestedEvent) -> None: """Test pod timeout configuration.""" - pod = pod_builder.build_pod_manifest(create_pod_command) + pod = pod_builder.build_pod_manifest(requested) # Verify active deadline is set assert pod.spec.active_deadline_seconds == 300 @@ -227,9 +219,8 @@ def test_pod_timeout_default( pod_builder: PodBuilder, ) -> None: """Test default pod timeout.""" - command = CreatePodCommandEvent( + event = ExecutionRequestedEvent( execution_id=str(uuid4()), - saga_id=str(uuid4()), script="print('hello')", language="python", language_version="3.11", @@ -246,28 +237,28 @@ def test_pod_timeout_default( metadata=EventMetadata(user_id=str(uuid4()), service_name="t", service_version="1"), ) - pod = pod_builder.build_pod_manifest(command) + pod = pod_builder.build_pod_manifest(event) # Default timeout should be 300 seconds assert pod.spec.active_deadline_seconds == 300 - def test_container_command(self, pod_builder: PodBuilder, create_pod_command: CreatePodCommandEvent) -> None: + def test_container_command(self, pod_builder: PodBuilder, requested: ExecutionRequestedEvent) -> None: """Test container command construction.""" - pod = pod_builder.build_pod_manifest(create_pod_command) + pod = pod_builder.build_pod_manifest(requested) container = pod.spec.containers[0] # Verify command expected_command = ["/bin/sh", "/entry/entrypoint.sh", "python", "/scripts/script.py"] assert container.command == expected_command - def test_container_environment(self, pod_builder: PodBuilder, create_pod_command: CreatePodCommandEvent) -> None: + def test_container_environment(self, pod_builder: PodBuilder, requested: ExecutionRequestedEvent) -> None: """Test container environment variables.""" - pod = pod_builder.build_pod_manifest(create_pod_command) + pod = pod_builder.build_pod_manifest(requested) container = pod.spec.containers[0] env_vars = {e.name: e.value for e in container.env} - assert env_vars["EXECUTION_ID"] == create_pod_command.execution_id + assert env_vars["EXECUTION_ID"] == requested.execution_id assert env_vars["OUTPUT_PATH"] == "/output" def test_pod_labels_truncation( @@ -277,9 +268,8 @@ def test_pod_labels_truncation( """Test label value truncation for K8s limits.""" long_id = "a" * 100 # Exceeds K8s 63 char limit - command = CreatePodCommandEvent( + event = ExecutionRequestedEvent( execution_id=long_id, - saga_id=long_id, script="print('test')", language="python", language_version="3.11", @@ -299,31 +289,27 @@ def test_pod_labels_truncation( ), ) - pod = pod_builder.build_pod_manifest(command) + pod = pod_builder.build_pod_manifest(event) # Verify labels are truncated to 63 chars assert len(pod.metadata.labels["user-id"]) == 63 - assert len(pod.metadata.labels["saga-id"]) == 63 # But annotations should have full values - assert pod.metadata.annotations["integr8s.io/saga-id"] == long_id + assert pod.metadata.annotations["integr8s.io/execution-id"] == long_id - def test_pod_restart_policy(self, pod_builder: PodBuilder, create_pod_command: CreatePodCommandEvent) -> None: + def test_pod_restart_policy(self, pod_builder: PodBuilder, requested: ExecutionRequestedEvent) -> None: """Test pod restart policy.""" - pod = pod_builder.build_pod_manifest(create_pod_command) + pod = pod_builder.build_pod_manifest(requested) # Should never restart (one-shot execution) assert pod.spec.restart_policy == "Never" -def _runtime_command( - language: str, *, cpu_limit: str, memory_limit: str, timeout_seconds: int -) -> CreatePodCommandEvent: +def _request(language: str, *, cpu_limit: str, memory_limit: str, timeout_seconds: int) -> ExecutionRequestedEvent: version = LANGUAGE_SPECS[language]["versions"][0] runtime = RUNTIME_REGISTRY[language][version] - return CreatePodCommandEvent( + return ExecutionRequestedEvent( execution_id=str(uuid4()), - saga_id=str(uuid4()), script="x", language=language, language_version=version, @@ -346,17 +332,17 @@ def builder(test_settings: Settings) -> PodBuilder: @pytest.mark.parametrize("language", [lang for lang, spec in LANGUAGE_SPECS.items() if "pod" not in spec]) def test_languages_without_pod_settings_keep_the_configured_pod(builder: PodBuilder, language: str) -> None: - command = _runtime_command(language, cpu_limit="100m", memory_limit="64Mi", timeout_seconds=5) + event = _request(language, cpu_limit="100m", memory_limit="64Mi", timeout_seconds=5) - pod = builder.build_pod_manifest(command) + pod = builder.build_pod_manifest(event) container = pod.spec.containers[0] volumes = {v.name: v for v in pod.spec.volumes} - assert container.command == ["/bin/sh", "/entry/entrypoint.sh", *command.runtime_command] + assert container.command == ["/bin/sh", "/entry/entrypoint.sh", *event.runtime_command] assert container.resources.limits == {"cpu": "100m", "memory": "64Mi"} assert container.resources.requests == {"cpu": "200m", "memory": "128Mi"} assert {e.name: e.value for e in container.env} == { - "EXECUTION_ID": command.execution_id, + "EXECUTION_ID": event.execution_id, "OUTPUT_PATH": "/output", } assert volumes["tmp-volume"].empty_dir.size_limit == "10Mi" @@ -365,9 +351,9 @@ def test_languages_without_pod_settings_keep_the_configured_pod(builder: PodBuil def test_go_pod_gets_its_cache_env_and_raised_minimums(builder: PodBuilder) -> None: - command = _runtime_command("go", cpu_limit="500m", memory_limit="128Mi", timeout_seconds=5) + event = _request("go", cpu_limit="500m", memory_limit="128Mi", timeout_seconds=5) - pod = builder.build_pod_manifest(command) + pod = builder.build_pod_manifest(event) container = pod.spec.containers[0] volumes = {v.name: v for v in pod.spec.volumes} go = POD_RUNTIME_CONFIGS["go"] @@ -381,7 +367,7 @@ def test_go_pod_gets_its_cache_env_and_raised_minimums(builder: PodBuilder) -> N "/scripts/main.go", ] assert {e.name: e.value for e in container.env} == { - "EXECUTION_ID": command.execution_id, + "EXECUTION_ID": event.execution_id, "OUTPUT_PATH": "/output", **go.env, } @@ -394,26 +380,26 @@ def test_go_pod_gets_its_cache_env_and_raised_minimums(builder: PodBuilder) -> N def test_go_pod_keeps_configured_limits_above_the_minimums(builder: PodBuilder) -> None: - command = _runtime_command("go", cpu_limit="2", memory_limit="1Gi", timeout_seconds=120) + event = _request("go", cpu_limit="2", memory_limit="1Gi", timeout_seconds=120) - pod = builder.build_pod_manifest(command) + pod = builder.build_pod_manifest(event) assert pod.spec.containers[0].resources.limits == {"cpu": "2", "memory": "1Gi"} assert pod.spec.active_deadline_seconds == 120 def test_unparseable_limit_is_rejected_when_a_minimum_applies(builder: PodBuilder) -> None: - command = _runtime_command("go", cpu_limit="lots", memory_limit="512Mi", timeout_seconds=60) + event = _request("go", cpu_limit="lots", memory_limit="512Mi", timeout_seconds=60) with pytest.raises(ValueError, match="Unsupported resource quantity: 'lots'"): - builder.build_pod_manifest(command) + builder.build_pod_manifest(event) def test_unknown_language_falls_back_to_default_pod_settings(builder: PodBuilder) -> None: - command = _runtime_command("python", cpu_limit="100m", memory_limit="64Mi", timeout_seconds=5) - command = command.model_copy(update={"language": "cobol"}) + event = _request("python", cpu_limit="100m", memory_limit="64Mi", timeout_seconds=5) + event = event.model_copy(update={"language": "cobol"}) - pod = builder.build_pod_manifest(command) + pod = builder.build_pod_manifest(event) volumes = {v.name: v for v in pod.spec.volumes} assert "cobol" not in POD_RUNTIME_CONFIGS diff --git a/docs/architecture/domain-dataclasses.md b/docs/architecture/domain-dataclasses.md index 17c46e6b6..bcfbc36c0 100644 --- a/docs/architecture/domain-dataclasses.md +++ b/docs/architecture/domain-dataclasses.md @@ -17,7 +17,6 @@ The opposite holds inside Pydantic models, and the code relies on that. When a d | `app/domain/user/user_models.py` | `UserChanges`, `UserDeleteResult`, `LoginResult` | | `app/domain/user/settings_models.py` | `DomainNotificationSettings`, `DomainEditorSettings`, `UserSettingsChanges`, `DomainUserSettingsChangedEvent` | | `app/domain/execution/models.py` | `ExecutionResultDomain`, `CancelResult`, `ResourceLimitsDomain` | -| `app/domain/saga/models.py` | `SagaContextData`, `SagaFilter`, `SagaConfig`, `SagaChanges` | | `app/domain/replay/models.py` | `ReplayFilter`, `ReplayConfig`, `ReplayError`, `ReplaySessionChanges` | | `app/domain/events/event_models.py` | `EventFilter`, `EventListResult`, `EventStatistics` | | `app/domain/events/typed.py` | `EventMetadata`, `ResourceUsageDomain` (next to the Pydantic events) | @@ -32,20 +31,18 @@ The opposite holds inside Pydantic models, and the code relies on that. When a d ## Writing one -`SagaContextData` in `app/domain/saga/models.py`, embedded in `SagaDocument.context_data`, shows the conventions for a value object: +`ReplayConfig` in `app/domain/replay/models.py`, embedded in `ReplaySessionDocument.config`, shows the conventions for a value object (trimmed): ```python @dataclass -class SagaContextData: - """Typed saga execution context. Populated incrementally by saga steps.""" - - execution_id: str = "" - language: str = "" - language_version: str | None = None - script: str = "" - timeout_seconds: int | None = None - pod_creation_triggered: bool = False - user_id: str = field(default_factory=lambda: str(uuid4())) +class ReplayConfig: + replay_type: ReplayType + target: ReplayTarget = ReplayTarget.KAFKA + filter: ReplayFilter = field(default_factory=ReplayFilter) + + speed_multiplier: float = 1.0 + batch_size: int = 100 + max_events: int | None = None ``` Mutable defaults and generated values use `field(default_factory=...)`. Timestamps are timezone-aware, from `datetime.now(timezone.utc)`. Generated ids use `default_factory=lambda: str(uuid4())`, so code never checks them for None. Enums are `StringEnum` subclasses. diff --git a/docs/architecture/domain-exceptions.md b/docs/architecture/domain-exceptions.md index 0f1598b00..92ea95aa7 100644 --- a/docs/architecture/domain-exceptions.md +++ b/docs/architecture/domain-exceptions.md @@ -28,7 +28,6 @@ Each domain package defines its own subclasses in an `exceptions.py` and exports DomainError ├── NotFoundError │ ├── ExecutionNotFoundError execution -│ ├── SagaNotFoundError saga │ ├── NotificationNotFoundError notification │ ├── SavedScriptNotFoundError saved_script │ ├── ReplaySessionNotFoundError replay @@ -40,20 +39,17 @@ DomainError ├── ThrottledError │ └── NotificationThrottledError notification ├── ConflictError -│ └── SagaConcurrencyError saga ├── UnauthorizedError │ ├── AuthenticationRequiredError user │ └── InvalidCredentialsError user ├── ForbiddenError │ ├── ExecutionAccessDeniedError execution -│ ├── SagaAccessDeniedError saga │ ├── AdminAccessRequiredError user │ ├── AccountDeactivatedError user │ └── CSRFValidationError user ├── InvalidStateError │ ├── ExecutionNotRetryableError execution │ ├── ExecutionTerminalError execution -│ ├── SagaInvalidStateError saga │ └── SelfDeletionError user ├── InfrastructureError │ └── ReplayOperationError replay @@ -64,10 +60,10 @@ The right column is the package under `app/domain/`, so `EventNotFoundError` liv `CSRFValidationError` never reaches the global handler: `CSRFMiddleware` catches it and answers 403 itself. -Subclasses that carry context take it in the constructor and keep it as attributes, so logs and tests can read `exc.saga_id` or `exc.limit` without parsing the message: +Subclasses that carry context take it in the constructor and keep it as attributes, so logs and tests can read `exc.execution_id` or `exc.limit` without parsing the message: ```python ---8<-- "backend/app/domain/saga/exceptions.py:SagaAccessDeniedError" +--8<-- "backend/app/domain/execution/exceptions.py:ExecutionAccessDeniedError" ``` ```python @@ -86,7 +82,7 @@ The body is `ErrorResponse(status, detail)` from `app/schemas_pydantic/common.py ## Where exceptions are raised -Repositories return `None` for a missing entity. They raise only when a write breaks a constraint: `UserRepository.create_user` raises `ConflictError` on a duplicate key, and `SagaRepository` raises `SagaConcurrencyError` on a revision mismatch and `SagaInvalidStateError` when cancelling a saga that is no longer active. +Repositories return `None` for a missing entity. They raise only when a write breaks a constraint: `UserRepository.create_user` raises `ConflictError` on a duplicate key. Services check for `None`, log, and raise the specific error. Trimmed from `SavedScriptService.get_saved_script`: @@ -100,18 +96,18 @@ return script The route just calls the service and returns the schema. It has no `None` check and no `HTTPException`. -Kafka handlers in the worker raise the same exceptions, for example `ExecutionNotFoundError` in the result processor. No HTTP mapping applies there: the error goes up to FastStream, whose telemetry middleware records it under `error_type` on `messaging_process_duration_seconds`. +Kafka handlers raise the same exceptions. No HTTP mapping applies there: the error goes up to FastStream, whose telemetry middleware records it under `error_type` on `messaging_process_duration_seconds`. ## Testing -Tests assert on the type and its fields instead of status codes. This one follows `tests/e2e/services/saga/test_saga_service.py`, which gets the service from the Dishka `scope` fixture and builds the caller with `make_test_user`: +Tests assert on the type and its fields instead of status codes. This one is from `tests/e2e/services/saved_script/test_saved_script_service.py`, which gets the service from its Dishka fixture: ```python -async def test_get_saga_not_found(scope: AsyncContainer) -> None: - svc: SagaService = await scope.get(SagaService) - with pytest.raises(SagaNotFoundError) as exc_info: - await svc.get_saga_with_access_check("nonexistent-saga-id", make_test_user()) - assert exc_info.value.identifier == "nonexistent-saga-id" +async def test_get_saved_script_not_found(self, saved_script_service: SavedScriptService) -> None: + user_id = _unique_user_id() + + with pytest.raises(SavedScriptNotFoundError): + await saved_script_service.get_saved_script("nonexistent-script-id", user_id) ``` ## Adding one diff --git a/docs/architecture/event-system-design.md b/docs/architecture/event-system-design.md index c9caf5d25..971c43378 100644 --- a/docs/architecture/event-system-design.md +++ b/docs/architecture/event-system-design.md @@ -85,7 +85,7 @@ async def produce(self, event_to_produce: DomainEvent, key: str) -> None: graph TB subgraph Producers API[API services] - Workers[Saga orchestrator, k8s worker,
pod monitor, result processor] + Workers[Worker: k8s worker,
pod monitor, result processor] end API --> Producer[UnifiedProducer] @@ -94,19 +94,15 @@ graph TB Producer --> Transport[KafkaEventTransport] --> Kafka[(Kafka)] subgraph Consumers - Saga[Saga orchestrator] - K8s[K8s worker] - Result[Result processor] + Sched[Worker: execution scheduler] APIc["API: sse-bridge-pool,
notification-service"] end - Kafka --> Saga - Kafka --> K8s - Kafka --> Result + Kafka --> Sched Kafka --> APIc ``` -The pod monitor only publishes. The DLQ manager is the one place that skips the outbox: it calls `broker.publish` directly for retries and its `dlq_message_*` status events. See [Kafka Topics](kafka-topic-architecture.md) for the consumer groups. +The worker consumes only `execution_requested` and `execution_cancelled`; inside it, the parts call each other directly. The DLQ manager is the one place that skips the outbox: it calls `broker.publish` directly for retries and its `dlq_message_*` status events. See [Kafka Topics](kafka-topic-architecture.md) for the consumer groups. ## Adding an event type diff --git a/docs/architecture/execution-queue.md b/docs/architecture/execution-queue.md index 149ec2aed..1badb14dd 100644 --- a/docs/architecture/execution-queue.md +++ b/docs/architecture/execution-queue.md @@ -1,19 +1,18 @@ # Execution queue -The saga orchestrator owns scheduling. It keeps pending executions in Redis, one sorted set per priority, and starts a saga only while fewer than `max_concurrent_executions` are running. Before any of that, the API caps how many executions one user can have queued or running at once. The queue itself is `ExecutionQueueService` in `backend/app/services/execution_queue.py`; the [saga orchestrator](../components/workers/saga_orchestrator.md) decides when to call it. +The execution scheduler in the worker owns scheduling. It keeps pending executions in Redis, one sorted set per priority, and creates a pod only while fewer than `max_concurrent_executions` are running. Before any of that, the API caps how many executions one user can have queued or running at once. The queue itself is `ExecutionQueueService` in `backend/app/services/execution_queue.py`; the [execution scheduler](../components/workers/execution_scheduler.md) decides when to call it. ```mermaid flowchart LR API[API] -->|Lua: reserve user slot| USER[(exec_queue:user:id)] - API -->|execution_requested| Kafka[(Kafka)] - Kafka --> SAGA[Saga orchestrator] - SAGA -->|ZADD| PENDING[(exec_queue:pending:priority
5 sorted sets)] - SAGA -->|Lua: prune, ZCARD, ZPOPMIN, ZADD| RUNNING[(exec_queue:running)] - SAGA -->|start saga| CMD[create_pod_command] - CMD --> Kafka - Kafka -->|completed, failed, timeout, cancelled| SAGA - SAGA -->|ZREM| RUNNING - SAGA -->|ZREM| USER + API -->|execution_requested, execution_cancelled| Kafka[(Kafka)] + Kafka --> SCHED[Execution scheduler] + SCHED -->|ZADD| PENDING[(exec_queue:pending:priority
5 sorted sets)] + SCHED -->|Lua: prune, ZCARD, ZPOPMIN, ZADD| RUNNING[(exec_queue:running)] + SCHED -->|create pod| K8S[K8s worker] + MON[Pod monitor] -->|pod finished| SCHED + SCHED -->|ZREM| RUNNING + SCHED -->|ZREM| USER ``` ## Redis keys @@ -25,7 +24,6 @@ flowchart LR | `exec_queue:user:{user_id}` | Sorted set | One user's queued and running ids, scored by the time the reservation expires | 24 hours, refreshed on each reservation | | `exec_queue:owner:{execution_id}` | String | The user an id was reserved for, so a release can find the user's set | 24 hours | | `exec_queue:event:{execution_id}` | String | The `ExecutionRequestedEvent` as JSON | 24 hours | -| `exec_queue:retries:{execution_id}` | Counter | Failed saga starts for that execution | 24 hours, refreshed on each increment | There are five pending sets: `critical`, `high`, `normal`, `low` and `background`. A higher priority always goes first, and within one priority the oldest entry goes first. @@ -35,29 +33,29 @@ Every execution submitted through `POST /api/v1/execute` starts as `normal`. Onl `ExecutionService.execute_script` reserves a slot for the user before it writes the execution. The reservation is a Lua script on `exec_queue:user:{user_id}`: it drops entries whose time has passed, and if the user still holds `max_executions_per_user` (admin setting, default 100, 1 to 1000) it refuses, and the API answers 429 with `ExecutionLimitExceededError`. Otherwise it adds the new id, which is generated before the insert for this reason. Retries go through the same path. If the insert or the publish fails, the service gives the slot back. -A reservation lasts 24 hours, as long as a queued event does. The orchestrator gives it back when the execution reaches a terminal state, including when it gives up after three failed saga starts, but not when it requeues after a single failed start. A release that never happens only holds the slot until it expires. +A reservation lasts 24 hours, as long as a queued event does. The scheduler gives it back when the execution ends, a cancel or a pod that couldn't be created included. A release that never happens only holds the slot until it expires. ## Scheduling `try_schedule` is one Lua script that gets the running set and the five pending sets as keys. It first drops running entries whose expiry has passed. If `ZCARD` on the running set is then at the cap, it returns nothing. Otherwise it walks the pending sets from `critical` to `background`, pops the first entry it finds with `ZPOPMIN`, adds the id to the running set with its expiry, and returns the id, its score and the priority index. The count check and the pop happen in one round trip, so two callers can't both squeeze past the cap. -Once the service has loaded the event, the slot gets the execution's own pod deadline plus 300 seconds: the timeout the execution was submitted with, raised to its language's minimum. The user's reservation moves to the same deadline, so a job that waited long in the queue keeps its place in the user's count while it runs. Until the event is loaded, the slot assumes the longest timeout an admin can set. A normal run releases the slot long before it expires, so expiry only matters when a release got lost, for example a saga that timed out without a terminal event. +Once the service has loaded the event, the slot gets the execution's own pod deadline plus 300 seconds: the timeout the execution was submitted with, raised to its language's minimum. The user's reservation moves to the same deadline, so a job that waited long in the queue keeps its place in the user's count while it runs. Until the event is loaded, the slot assumes the longest timeout an admin can set. A normal run releases the slot long before it expires, so expiry only matters when a release got lost, for example a pod deleted by hand while the worker was down. The service then loads the stored event. If that key already expired, the execution is lost: the id leaves the running set and its user's reservation, `queue.event_data_lost` goes up and nothing is scheduled. -`try_schedule_from_queue` reads `max_concurrent_executions` from the admin system settings (default 10, admins can set 1 to 100, cached for 60 seconds by `RuntimeSettingsLoader`). It calls `try_schedule` in a loop and starts a saga for each id it gets, until the queue is empty, the cap is reached or it pops an id whose event expired. That last case ends the pass even with entries still pending, and the next pass picks them up. +`schedule_pending` reads `max_concurrent_executions` from the admin system settings (default 10, admins can set 1 to 100, cached for 60 seconds by `RuntimeSettingsLoader`). It calls `try_schedule` in a loop and has the k8s worker create the pod for each id it gets, until the queue is empty, the cap is reached or it pops an id whose event expired. That last case ends the pass even with entries still pending, and the next pass picks them up. -If starting a saga raises, the orchestrator bumps `exec_queue:retries:{id}`, releases the slot and puts the event back at the end of its priority. On the third failure it gives up: the saga is marked failed if one was created, no event goes out, and the execution stays `queued` in MongoDB. Either way that scheduling pass stops. +If the pod can't be created, the scheduler stores the execution as `failed` with a `system_error`, releases both slots and goes on with the next id. -These events trigger a pass: +These trigger a pass: -| Event | Queue action before scheduling | -|-------|--------------------------------| +| Trigger | Queue action before scheduling | +|---------|--------------------------------| | `execution_requested` | `enqueue` (guarded by [idempotency](idempotency.md)) | -| `execution_completed`, `execution_failed`, `execution_timeout` | mark the saga terminal, `release`, `release_user_slot` | -| `execution_cancelled` | `remove` from every queue key, mark the saga cancelled, `release`, `release_user_slot` | +| the pod monitor sees a pod finish | `release`, `release_user_slot` | +| `execution_cancelled` | `remove` from every queue key, store `cancelled`, delete the pod, `release`, `release_user_slot` | -`release` is a `ZREM` on the running set plus deleting the event key, and `release_user_slot` looks up the owner and removes the id from that user's set, so both are safe on an id that holds nothing. On top of the events, the worker runs `try_schedule_from_queue` every 10 seconds through APScheduler (`app/worker.py`), which catches up after a restart or a lost event. +`release` is a `ZREM` on the running set plus deleting the event key, and `release_user_slot` looks up the owner and removes the id from that user's set, so both are safe on an id that holds nothing. On top of those, the worker runs `schedule_pending` every 10 seconds through APScheduler (`app/worker.py`), which catches up after a restart or a slot that expired. ## Admin endpoints @@ -75,4 +73,4 @@ The queue status reads Redis directly: `queue_depth` is the sum of `ZCARD` over ## Metrics -The queue's six series come from spans: `queue.enqueue` and `queue.schedule` from `ExecutionQueueService`, and `queue.schedule_pending` from the saga scheduler's 10-second job, which reads the depth and the running slots from Redis. So `queue_depth` and `queue_active` match the admin queue endpoint and survive restarts. The [metrics reference](../operations/metrics-reference.md#executions-and-queue) lists the series and their labels. +The queue's six series come from spans: `queue.enqueue` and `queue.schedule` from `ExecutionQueueService`, and `queue.schedule_pending` from every pass of the execution scheduler, which reads the depth and the running slots from Redis. So `queue_depth` and `queue_active` match the admin queue endpoint and survive restarts. The [metrics reference](../operations/metrics-reference.md#executions-and-queue) lists the series and their labels. diff --git a/docs/architecture/idempotency.md b/docs/architecture/idempotency.md index 60cedf973..ce1c71e5f 100644 --- a/docs/architecture/idempotency.md +++ b/docs/architecture/idempotency.md @@ -6,14 +6,14 @@ Kafka delivers at least once, so a consumer can see the same event twice. Handle | Caller | Events | Key strategy | TTL | |--------|--------|--------------|-----| -| Saga orchestrator | `execution_requested`, `execution_completed`, `execution_failed`, `execution_timeout`, `execution_cancelled` | `EVENT_BASED` | 1 hour | -| K8s worker | `create_pod_command`, `delete_pod_command` | `CONTENT_HASH` | 1 hour | -| Result processor | `execution_completed`, `execution_failed`, `execution_timeout` | `CONTENT_HASH` | 2 hours | +| Execution scheduler | `execution_requested`, `execution_cancelled` | `EVENT_BASED` | 1 hour | | API, `POST /api/v1/execute` with `Idempotency-Key` | none, see below | `CUSTOM` | 24 hours | The other subscribers, `sse-bridge-pool` and `notification-service`, don't use it. The subscriptions live in `backend/app/events/handlers.py`, one `KafkaRouter` per consumer, and the guard is a middleware on the router, so it covers every subscriber on it. -Keys aren't scoped by consumer group. The saga orchestrator and the result processor both guard `execution_completed`, `execution_failed` and `execution_timeout`, and they only stay out of each other's way because one keys on the event id and the other on a content hash. Two routers with the same strategy on the same event would skip each other's deliveries. +Keys aren't scoped by consumer group, so two routers with the same strategy on the same event would skip each other's deliveries. Only one router uses the manager today. + +Inside the worker, a finished pod doesn't need a key: the result processor's write only applies to an execution that is still active, so a pod reported twice stores its result once and publishes it once. ## Keys @@ -23,7 +23,7 @@ The manager builds a key from the event in one of three ways: --8<-- "backend/app/services/idempotency/idempotency_manager.py:generate_key" ``` -`EVENT_BASED` is `{event_type}:{event_id}` and is the default when a caller passes no strategy. `CONTENT_HASH` is a SHA-256 of the event JSON with `event_id`, `timestamp` and `metadata` removed, so an event published twice under two different ids still counts as one. `CUSTOM` is `{event_type}:{custom_key}` with a key the caller picks. Every key gets the `idempotency:` prefix. +`EVENT_BASED` is `{event_type}:{event_id}` and is the default when a caller passes no strategy. `CONTENT_HASH` is a SHA-256 of the event JSON with `event_id`, `timestamp` and `metadata` removed, so an event published twice under two different ids still counts as one; no router uses it today. `CUSTOM` is `{event_type}:{custom_key}` with a key the caller picks. Every key gets the `idempotency:` prefix. ## How a check works diff --git a/docs/architecture/kafka-topic-architecture.md b/docs/architecture/kafka-topic-architecture.md index d841146f2..1bddac9f3 100644 --- a/docs/architecture/kafka-topic-architecture.md +++ b/docs/architecture/kafka-topic-architecture.md @@ -9,10 +9,10 @@ await self._transport.publish(event_to_produce, event_to_produce.event_type, key That line in `UnifiedProducer.produce` is the whole routing layer. Subscribers name the same enum member: ```python -async def start_saga(body: ExecutionRequestedEvent, saga: FromDishka[SagaOrchestrator]) -> None: - await saga.handle_execution_requested(body) +async def schedule_execution(body: ExecutionRequestedEvent, scheduler: FromDishka[ExecutionScheduler]) -> None: + await scheduler.handle_execution_requested(body) -router.subscriber(EventType.EXECUTION_REQUESTED, group_id="saga-orchestrator")(start_saga) +router.subscriber(EventType.EXECUTION_REQUESTED, group_id="saga-orchestrator")(schedule_execution) ``` Each handler gets exactly one typed model. Nobody decodes a message only to drop it because the type doesn't match. @@ -21,9 +21,9 @@ Each handler gets exactly one typed model. Nobody decodes a message only to drop No code creates topics. The broker has `KAFKA_AUTO_CREATE_TOPICS_ENABLE` on, so a topic appears the first time a client uses it, with the broker defaults from `docker-compose.yaml`: 3 partitions (`KAFKA_NUM_PARTITIONS`) and 7 days of retention (`KAFKA_LOG_RETENTION_HOURS: 168`). Every topic gets the same settings. -`EventType` has 57 members, but the code constructs fewer than half of them, so most of those topics never come into existence. `app/infrastructure/kafka/topics.py` groups event types into sets such as `EXECUTION_TYPES`, `POD_TYPES` and `COMMAND_TYPES`. The DLQ uses those sets to choose a retry policy, and they have nothing to do with partitions or retention. +`EventType` has 47 members, but the code constructs fewer than half of them, so most of those topics never come into existence. `app/infrastructure/kafka/topics.py` groups event types into sets such as `EXECUTION_TYPES`, `POD_TYPES` and `RESULT_TYPES`. The DLQ uses those sets to choose a retry policy, and they have nothing to do with partitions or retention. -When the producer first publishes them, execution, pod, command, result and saga events are keyed by execution ID. User events use the username, notifications the user ID and settings events `user_settings_{user_id}`. Within a topic, Kafka sends every message with the same key to the same partition, so they stay in order. Resends don't keep those keys: a DLQ retry keys the message by `event_id`, and a replay by `aggregate_id`, falling back to `event_id`. So a retried or replayed event can land on another partition and arrive out of order with the rest of its execution. Across topics there is no ordering at all, so a group that reads several topics, like the SSE bridge, can see one execution's events out of order. With 3 partitions, up to three consumers in one group can share a topic. +When the producer first publishes them, execution, pod and result events are keyed by execution ID. User events use the username, notifications the user ID and settings events `user_settings_{user_id}`. Within a topic, Kafka sends every message with the same key to the same partition, so they stay in order. Resends don't keep those keys: a DLQ retry keys the message by `event_id`, and a replay by `aggregate_id`, falling back to `event_id`. So a retried or replayed event can land on another partition and arrive out of order with the rest of its execution. Across topics there is no ordering at all, so a group that reads several topics, like the SSE bridge, can see one execution's events out of order. With 3 partitions, up to three consumers in one group can share a topic. ## Consumer groups @@ -31,39 +31,19 @@ Group IDs are string literals in `app/events/handlers.py`. The `KAFKA_CONSUMER_G | Group | Runs in | Topics | |-------|---------|--------| -| `saga-orchestrator` | worker | `execution_requested`, `execution_completed`, `execution_failed`, `execution_timeout`, `execution_cancelled` | -| `k8s-worker` | worker | `create_pod_command`, `delete_pod_command` | -| `result-processor` | worker | `execution_completed`, `execution_failed`, `execution_timeout`, `pod_scheduled`, `pod_running` | +| `saga-orchestrator` | worker | `execution_requested`, `execution_cancelled` | | `notification-service` | API | `execution_completed`, `execution_failed`, `execution_timeout` | | `sse-bridge-pool` | API | 17 types: every `execution_*` event except `execution_accepted`, all seven `pod_*` events, `result_stored` and `result_failed` | -Kafka delivers each message to every group that subscribes to the topic, so one `execution_completed` reaches the saga orchestrator, the result processor, the notification service and the SSE bridge. The worker's three groups each get their own copy even though they share a process. The pod monitor and the DLQ job only publish. +The worker's group is the [execution scheduler](../components/workers/execution_scheduler.md). It kept the name of the saga orchestrator it replaced, so the offsets that group committed carried over. Kafka delivers each message to every group that subscribes to the topic, so one `execution_completed` reaches the notification service and the SSE bridge. Inside the worker the parts call each other directly. The pod monitor, the result processor and the DLQ job only publish. -Most subscribers use `AckPolicy.ACK`, which commits the offset after the handler returns, even when it raised. The SSE bridge uses `ACK_FIRST` and starts from the latest offset, since a browser only cares about events from now on. The result processor reads one record per poll and starts from the earliest offset. +Most subscribers use `AckPolicy.ACK`, which commits the offset after the handler returns, even when it raised. The SSE bridge uses `ACK_FIRST` and starts from the latest offset, since a browser only cares about events from now on. ## Request flow -The execution service publishes `ExecutionRequestedEvent` to `execution_requested`. Two groups receive it: the saga orchestrator queues it, and the SSE bridge forwards it to the browser. When a slot is free, the orchestrator publishes `CreatePodCommandEvent` to `create_pod_command`, which only the k8s worker reads. From there the pod, execution and result events flow back through their own topics, as described in the [overview](overview.md#execution-lifecycle). +The execution service publishes `ExecutionRequestedEvent` to `execution_requested`. Two groups receive it: the execution scheduler queues it, and the SSE bridge forwards it to the browser. When a slot is free, the scheduler creates the pod directly. From there the pod, execution and result events flow back through their own topics, as described in the [overview](overview.md#execution-lifecycle). -If the k8s worker is down, only `create_pod_command` and `delete_pod_command` pile up. SSE, notifications and the queue keep working, though the orchestrator stops scheduling once every concurrency slot is taken, since a slot frees up only when its execution finishes or is cancelled. - -## Sagas - -```mermaid -graph TD - Orchestrator[SagaOrchestrator] - ExecutionSaga["ExecutionSaga
(steps and compensations)"] - SagaRepo[(SagaRepository
MongoDB)] - Producer[UnifiedProducer] - Kafka[(Kafka)] - - Orchestrator --> SagaRepo - Orchestrator -- "bind_dependencies(producer, alloc_repo, ...)" --> ExecutionSaga - ExecutionSaga -- "create_pod step, delete_pod compensation" --> Producer - Producer --> Kafka -``` - -The orchestrator creates an `ExecutionSaga`, binds its dependencies explicitly and runs the steps one by one, saving the current step, the completed steps and the context to MongoDB after each one. Only plain data goes into that context, which is what lets `cancel_saga` rebuild the compensations later from the stored saga. When a step fails, the compensations of the completed steps run in reverse and publish commands like `delete_pod_command`. See [Saga Orchestrator](../components/workers/saga_orchestrator.md). +If the worker is down, `execution_requested` and `execution_cancelled` wait in Kafka for its group, and the API keeps answering. ## Replay @@ -81,9 +61,9 @@ The service runs inside the API process, and a session's live state is kept in t ## Dead letters -The DLQ is a MongoDB collection, `dlq_messages`, not a Kafka topic. `DLQManager.handle_message` stores a failed message there and schedules a retry by the event type's category: execution and command events get 5 retries with exponential backoff from 30 seconds, pod events 3 retries from 60 seconds, result events 3 immediate retries, and everything else 4 retries from 60 seconds. The worker's DLQ job republishes due messages to their original topic every 10 seconds and publishes `dlq_message_retried`. +The DLQ is a MongoDB collection, `dlq_messages`, not a Kafka topic. `DLQManager.handle_message` stores a failed message there and schedules a retry by the event type's category: execution events get 5 retries with exponential backoff from 30 seconds, pod events 3 retries from 60 seconds, result events 3 immediate retries, and everything else 4 retries from 60 seconds. The worker's DLQ job republishes due messages to their original topic every 10 seconds and publishes `dlq_message_retried`. -`DeadLetterMiddleware` on the k8s worker, result processor, saga orchestrator and notification routers calls `handle_message` when a handler raises. A retry goes back to the original topic with a header naming the failed consumer group, and every other group skips it. See [Dead Letter Queue](../components/dead-letter-queue.md). +`DeadLetterMiddleware` on the execution scheduler and notification routers calls `handle_message` when a handler raises. A retry goes back to the original topic with a header naming the failed consumer group, and every other group skips it. See [Dead Letter Queue](../components/dead-letter-queue.md). ## Key files diff --git a/docs/architecture/lifecycle.md b/docs/architecture/lifecycle.md index acc32f228..9e9370712 100644 --- a/docs/architecture/lifecycle.md +++ b/docs/architecture/lifecycle.md @@ -22,7 +22,7 @@ gunicorn runs the API with `WEB_CONCURRENCY` workers (2 by default), and each on ## Worker process -`app/worker.py` loads `Settings(override_path="config.worker.toml")`, sets up logging, runs `init_beanie`, builds its container with `create_worker_container`, registers the k8s worker, result processor and saga routers, calls `setup_dishka`, runs `start_jobs`, and then blocks on `FastStream(broker, on_shutdown=[scheduler.shutdown], after_shutdown=[container.close]).run()`. +`app/worker.py` loads `Settings(override_path="config.worker.toml")`, sets up logging, runs `init_beanie`, builds its container with `create_worker_container`, registers the execution scheduler's router, calls `setup_dishka`, runs `start_jobs`, and then blocks on `FastStream(broker, on_shutdown=[scheduler.shutdown], after_shutdown=[container.close]).run()`. FastStream 0.7 runs `on_shutdown` hooks before `broker.stop()` and `after_shutdown` hooks after it. `start_jobs` runs before `run()`, so the startup work happens before the broker connects; on the way out the scheduler stops before the broker drains, and the container closes last, which keeps Redis and the Kubernetes client open until the handlers are done. @@ -31,9 +31,9 @@ FastStream 0.7 runs `on_shutdown` hooks before `broker.stop()` and `after_shutdo | Job | Interval | Calls | |-----|----------|-------| | `pod_monitor_watch` | 5s | `PodMonitor.watch_cycle` | -| `saga_try_schedule` | 10s | `SagaOrchestrator.try_schedule_from_queue` | -| `saga_check_timeouts` | 30s | `SagaOrchestrator.check_timeouts` | +| `execution_schedule_pending` | 10s | `ExecutionScheduler.schedule_pending` | | `dlq_monitor_retries` | 10s | `DLQManager.process_monitoring_cycle` | +| `idempotency_report_keys` | 30s | `IdempotencyManager.report_active_keys` | `watch_cycle` calls `watch_pod_events()`. On a 410 from the API server it resets the resource version so the next cycle starts a fresh watch, and every error that ends the cycle is counted in the watch error and reconnect metrics. An error while handling one pod event is caught inside `PodMonitor` and counted as a watch error only, and the watch carries on. diff --git a/docs/architecture/overview.md b/docs/architecture/overview.md index 94b4b2619..4af2b0af1 100644 --- a/docs/architecture/overview.md +++ b/docs/architecture/overview.md @@ -32,7 +32,7 @@ Integr8sCode runs user scripts in Python, Node.js, Ruby, Bash or Go, each in its The browser loads the SPA from the frontend container, an nginx that serves HTTPS on port 5001 and proxies `/api` to the backend over HTTPS, checking the backend's own certificate. The backend runs FastAPI under Gunicorn with Uvicorn workers and serves both REST and SSE. Kafka runs in KRaft mode and creates topics on first use, so there is no topic setup step. Events travel as JSON that FastStream serializes from the Pydantic models. -The worker is one more container from the same backend image, running `python -m app.worker`. Inside it, the k8s worker creates pods through the Kubernetes API, the pod monitor watches them, the result processor writes results to MongoDB, and the saga orchestrator schedules executions with a Redis-backed queue and tracks sagas in MongoDB. A DLQ job retries failed events every 10 seconds. +The worker is one more container from the same backend image, running `python -m app.worker`. Inside it, the execution scheduler starts executions from a Redis-backed queue, the k8s worker creates their pods through the Kubernetes API, the pod monitor watches the pods, and the result processor writes results to MongoDB. A DLQ job retries failed events every 10 seconds. The observability profile adds an OpenTelemetry Collector that ships traces to Jaeger, metrics to VictoriaMetrics and logs to Loki, with Grafana on top of all three. Kafdrop is under the debug profile. No container runs once and exits. nginx and the backend each make their own TLS certificate when they first start, and the backend seeds the users before it serves. The worker reads k3s's own kubeconfig and creates the `integr8scode` namespace with its NetworkPolicy and ResourceQuota. @@ -69,7 +69,7 @@ graph TD ??? note "Legend" - Middlewares, outermost first: CORS, CacheControl, SecurityHeaders, RequestSizeLimit, CSRF, RateLimit, Metrics - Routes + DI: REST and SSE endpoints, with services injected by Dishka - - Services: auth, execution, events, SSE, idempotency, notifications, user settings, rate limits, saved scripts, replay, sagas, admin + - Services: auth, execution, events, SSE, idempotency, notifications, user settings, rate limits, saved scripts, replay, admin - Kafka layer: `UnifiedProducer` on the way out, FastStream subscribers on the way in - Observability: the OTel Collector, which forwards to VictoriaMetrics, Jaeger and Loki @@ -85,7 +85,7 @@ sequenceDiagram actor Client participant API participant Kafka - participant Saga as Saga Orchestrator + participant Sched as Execution Scheduler participant K8s as K8s Worker participant Monitor as Pod Monitor participant Result as Result Processor @@ -94,17 +94,17 @@ sequenceDiagram API->>Kafka: execution_requested API-->>Client: 200 {execution_id, status} Client->>API: GET /api/v1/events/executions/{id} (SSE) - Kafka->>Saga: execution_requested - Saga->>Kafka: create_pod_command - Kafka->>K8s: create_pod_command + Kafka->>Sched: execution_requested + Sched->>K8s: create_pod K8s->>Kafka: pod_created - Monitor->>Kafka: pod_scheduled, pod_running, execution_completed - Kafka->>Result: execution_completed - Result->>Kafka: result_stored + Monitor->>Kafka: pod_scheduled, pod_running + Monitor->>Result: execution_completed + Result->>Kafka: execution_completed, result_stored + Monitor->>Sched: finish Kafka->>API: events for this execution (sse-bridge-pool) API-->>Client: SSE frames, closes after result_stored - link Saga: Docs @ ../../components/workers/saga_orchestrator/ + link Sched: Docs @ ../../components/workers/execution_scheduler/ link K8s: Docs @ ../../components/workers/k8s_worker/ link Monitor: Docs @ ../../components/workers/pod_monitor/ link Result: Docs @ ../../components/workers/result_processor/ @@ -112,9 +112,9 @@ sequenceDiagram The rate limit middleware has already run by the time the route sees the request. If the client sent an `Idempotency-Key` header it used before, the [idempotency](idempotency.md) layer returns the earlier execution instead of creating a new one, or 409 while that first request is still in flight. Otherwise the execution service checks the language and version against the [runtime registry](runtime-registry.md) and rejects an unknown pair with 422. Then it stores the execution in MongoDB, publishes `execution_requested` and answers with the execution ID and its status. -The saga orchestrator puts the request into the [execution queue](execution-queue.md) and, once a slot is free, runs the saga steps, the last of which publishes `create_pod_command`. The k8s worker creates a ConfigMap with the script and a hardened pod, then publishes `pod_created`, or `execution_failed` if the Kubernetes API refused. The pod monitor watches the pod and publishes `pod_scheduled`, `pod_running` and finally one of `execution_completed`, `execution_failed` or `execution_timeout`. Then it deletes the pod, which frees its share of the ResourceQuota, and Kubernetes garbage-collects the ConfigMap through its owner reference. +The execution scheduler in the worker puts the request into the [execution queue](execution-queue.md) and, once a slot is free, has the k8s worker create a ConfigMap with the script and a hardened pod. The k8s worker publishes `pod_created`; if the Kubernetes API refused, the execution is stored as `failed` instead. The pod monitor watches the pod and publishes `pod_scheduled` and `pod_running`. When the pod ends, the monitor reads its output and hands `execution_completed`, `execution_failed` or `execution_timeout` to the result processor. Then it deletes the pod, which frees its share of the ResourceQuota, and Kubernetes garbage-collects the ConfigMap through its owner reference. -The result processor writes the final state and output to MongoDB and publishes `result_stored`, or `result_failed` if it couldn't. The saga orchestrator also listens for the terminal events: it marks the saga done, releases the queue slot and schedules the next execution. +The result processor writes the final state and output to MongoDB, then publishes the terminal event and `result_stored`, or `result_failed` if it couldn't. Last, the monitor tells the scheduler the execution finished, so its queue slot frees and the next execution starts. A cancel goes the same way in reverse: the scheduler takes `execution_cancelled`, stores the execution as `cancelled` and deletes its pod. ## SSE @@ -147,9 +147,9 @@ The stream opens with the execution's current status read from MongoDB, since ev Notifications use the same bus on `sse:notif:{user_id}` through `GET /api/v1/events/notifications/stream`. That stream stays open until the client goes away and sends a ping every 30 seconds. -## Kafka, sagas and replay +## Kafka and replay -Kafka carries all asynchronous work, with [one topic per event type](kafka-topic-architecture.md). [Sagas](../components/workers/saga_orchestrator.md) run the execution steps and publish compensating commands when a step fails. The [replay system](../components/event-replay.md) lets admins re-publish stored events. A failed handler's event goes to the [dead letter queue](../components/dead-letter-queue.md) and is retried for the consumer group that failed. +Kafka carries all asynchronous work, with [one topic per event type](kafka-topic-architecture.md). The [replay system](../components/event-replay.md) lets admins re-publish stored events. A failed handler's event goes to the [dead letter queue](../components/dead-letter-queue.md) and is retried for the consumer group that failed. ## Notifications diff --git a/docs/architecture/runtime-registry.md b/docs/architecture/runtime-registry.md index 06862c809..3a45827fc 100644 --- a/docs/architecture/runtime-registry.md +++ b/docs/architecture/runtime-registry.md @@ -26,7 +26,7 @@ The registry offers only versions that upstream still supports, and lists them n --8<-- "backend/app/runtime_registry.py:RuntimeConfig" ``` -The file is always `main.{file_ext}`, and the command is the interpreter plus `/scripts/main.{file_ext}`, so Python 3.12 gets `python:3.12-slim` and `["python", "/scripts/main.py"]`. `ExecutionService` copies image, command and file name into the `ExecutionRequestedEvent`, and they travel through the saga to the k8s worker. +The file is always `main.{file_ext}`, and the command is the interpreter plus `/scripts/main.{file_ext}`, so Python 3.12 gets `python:3.12-slim` and `["python", "/scripts/main.py"]`. `ExecutionService` copies image, command and file name into the `ExecutionRequestedEvent`, and the k8s worker builds the pod from that event. The worker puts the script into a ConfigMap, mounts it read-only at `/scripts`, and starts the container as `/bin/sh /entry/entrypoint.sh `. The entrypoint runs the command as plain arguments, with no shell in between. On startup the worker also runs a pre-puller DaemonSet that pulls every image in the registry. diff --git a/docs/architecture/services-overview.md b/docs/architecture/services-overview.md index d7b1dd573..59be78b59 100644 --- a/docs/architecture/services-overview.md +++ b/docs/architecture/services-overview.md @@ -10,8 +10,8 @@ Route handlers get these from Dishka. They hold the business logic, talk to Mong |--------|--------------| | `auth_service.py`, `login_lockout.py` | Login, registration, the current user from the JWT cookie, lockout after repeated failed logins. See [Authentication](authentication.md). | | `execution_service.py` | The facade behind `/execute`. Checks the runtime registry, stores the execution, publishes `ExecutionRequestedEvent`, and publishes `ExecutionCancelledEvent` on cancel or delete. Handles the `Idempotency-Key` header. | -| `execution_queue.py` | `ExecutionQueueService`, the Redis priority queue the saga orchestrator schedules from. See [Execution Queue](execution-queue.md). | -| `saga/` | `SagaOrchestrator`, `ExecutionSaga` and its steps, and `SagaService`, the read side behind the saga routes with ownership checks. | +| `execution_queue.py` | `ExecutionQueueService`, the Redis priority queue the execution scheduler starts executions from. See [Execution Queue](execution-queue.md). | +| `execution_scheduler.py` | `ExecutionScheduler`, which starts queued executions while slots are free, cancels them and frees their slots. | | `event_service.py` | Reading stored events. Publishing goes through `UnifiedProducer` directly. | | `event_replay/` | `EventReplayService`, which re-publishes stored events for admins. | | `sse/` | `SSERedisBus` and `SSEService`. See [SSE](../components/sse/sse-architecture.md). | @@ -28,39 +28,33 @@ The API process is a Kafka consumer too. Its lifespan registers two subscriber g ## Worker -The worker runs in its own container from the same backend image as the API, with the command `python -m app.worker`. Its entry point is `app/worker.py`, and every subscriber is registered in `app/events/handlers.py`. +The worker runs in its own container from the same backend image as the API, with the command `python -m app.worker`. Its entry point is `app/worker.py`, and its one Kafka router is in `app/events/handlers.py`. The parts call each other directly. | Part | Consumes | Publishes | |--------|----------|-----------| -| Saga orchestrator | `execution_requested`, `execution_completed`, `execution_failed`, `execution_timeout`, `execution_cancelled` | `create_pod_command`, `delete_pod_command`, `saga_started`, `saga_cancelled` | -| K8s worker | `create_pod_command`, `delete_pod_command` | `pod_created`, or `execution_failed` when pod creation fails | -| Pod monitor | nothing, it watches the Kubernetes API | `pod_scheduled`, `pod_running`, `pod_terminated`, `execution_completed`, `execution_failed`, `execution_timeout` | -| Result processor | `execution_completed`, `execution_failed`, `execution_timeout` | `result_stored`, or `result_failed` when storing fails | +| Execution scheduler | `execution_requested`, `execution_cancelled` | nothing itself | +| K8s worker | nothing, the scheduler calls it | `pod_created` | +| Pod monitor | nothing, it watches the Kubernetes API | `pod_scheduled`, `pod_running`, `pod_terminated` | +| Result processor | nothing, the pod monitor and the scheduler call it | `execution_completed`, `execution_failed`, `execution_timeout`, then `result_stored`, or `result_failed` when storing fails | | DLQ processor | nothing | retried events and `dlq_message_retried` | -The saga orchestrator puts each request into the execution queue and runs the saga once a slot is free. The saga steps are `validate_execution` and `create_pod`; the second one publishes the pod command. If a step fails, the compensations of the steps that already ran go in reverse order, and `delete_pod` publishes `delete_pod_command`. Terminal execution events mark the saga done and release the queue slot. `execution_cancelled` takes a queued request out of the queue and marks the saga cancelled, but it doesn't delete a pod that is already running. Two APScheduler jobs run next to the consumers: `check_timeouts` every 30 seconds and `try_schedule_from_queue` every 10 seconds. +The execution scheduler puts each request into the execution queue and has the k8s worker create the pod once a slot is free. A pod that can't be created ends the execution as `failed`. `execution_cancelled` takes the execution out of the queue, stores it as `cancelled`, deletes its pod and frees its slots. An APScheduler job runs `schedule_pending` every 10 seconds next to the consumers. The k8s worker creates a ConfigMap holding the script and the entrypoint, then the pod, then sets the pod as the ConfigMap's owner so Kubernetes deletes both together. The pod runs as user 1000 with a read-only root filesystem, all capabilities dropped, seccomp `RuntimeDefault`, no service account token, no service links and no host namespaces. The worker refuses to start if `K8S_NAMESPACE` is `default`. -The pod monitor lists and then watches pods labelled `app=integr8s,component=executor`. Each watch window lasts up to 30 seconds, and an APScheduler job checks every 5 seconds and opens the next one once the last has ended. It maps pod phases to events, reads the exit code, resource usage and output from the termination message and logs, and deletes the pod once it has published a terminal execution event. Deleting the pod frees its share of the ResourceQuota for the next one. +The pod monitor lists and then watches pods labelled `app=integr8s,component=executor`. Each watch window lasts up to 30 seconds, and an APScheduler job checks every 5 seconds and opens the next one once the last has ended. It maps pod phases to events, reads the exit code, resource usage and output from the termination message and logs, hands a finished execution to the result processor, deletes the pod and tells the scheduler, so the next execution starts. Deleting the pod frees its share of the ResourceQuota for the next one. -The result processor writes the final status, output and resource usage to MongoDB and records execution metrics. It doesn't touch Kubernetes. +The result processor writes the final status, output and resource usage to MongoDB, records execution metrics and only then publishes the terminal event. If the execution already ended, a cancel for example, it publishes nothing. It doesn't touch Kubernetes. Replay sessions run inside the API process, which owns the `EventReplayService` the admin routes use. MongoDB drops finished sessions 48 hours after they complete. -The DLQ processor runs `process_monitoring_cycle` every 10 seconds, which republishes due messages from the `dlq_messages` collection to their original topic for the consumer group that failed on them. The k8s worker, result processor, saga orchestrator and notification routers save their failed events there. See [Dead Letter Queue](../components/dead-letter-queue.md). +The DLQ processor runs `process_monitoring_cycle` every 10 seconds, which republishes due messages from the `dlq_messages` collection to their original topic for the consumer group that failed on them. The execution scheduler and notification routers save their failed events there. See [Dead Letter Queue](../components/dead-letter-queue.md). ## Idempotent consumers -Three workers put `IdempotencyMiddleware` from `app/events/middlewares.py` on their `KafkaRouter`. It reserves a key in Redis, runs the handler and marks the key completed or failed. A duplicate delivery finds the key and is skipped. +The execution scheduler's router carries `IdempotencyMiddleware` from `app/events/middlewares.py`, with the `EVENT_BASED` strategy and a 1 hour TTL. It reserves a key in Redis, runs the handler and marks the key completed or failed. A duplicate delivery finds the key and is skipped, so a redelivered `execution_requested` doesn't queue the execution again. -| Consumer | Key strategy | TTL | -|----------|--------------|-----| -| Saga orchestrator, all five events | `EVENT_BASED` | 1 hour | -| K8s worker, both commands | `CONTENT_HASH` | 1 hour | -| Result processor, all three events | `CONTENT_HASH` | 2 hours | - -`EVENT_BASED` keys on event type and ID, so it catches a redelivered message. `CONTENT_HASH` hashes the event without its ID, timestamp and metadata, so it also catches the same command published twice. When a caller doesn't pass a strategy, `IdempotencyManager.check_and_reserve` uses `EVENT_BASED`. +`EVENT_BASED` keys on event type and ID, so it catches a redelivered message. When a caller doesn't pass a strategy, `IdempotencyManager.check_and_reserve` uses `EVENT_BASED`. ## Network isolation diff --git a/docs/assets/images/system_diagram.svg b/docs/assets/images/system_diagram.svg index 6d07b9f05..c2d9d532e 100644 --- a/docs/assets/images/system_diagram.svg +++ b/docs/assets/images/system_diagram.svg @@ -1,4 +1,4 @@ -
HTTPS 5001
(SPA + Assets)
Browser SPA
HTTPS /api/v1/*
(REST/SSE endpoints)
frontend
(Nginx + Svelte)
SSE /api/v1/events/*
(EventSource)
CRUD:
- executions
- settings
- events
backend
(FastAPI + SSE)
Observability
Prom
remote_write
(metrics)
otlphtpp
otel-collectorJaeger
OTLP gRPC
(traces)
query
dashboards
(PromQL)
Grafana
kafdrop
Kubernetes API
consume commands and events
publish pod, execution and result events
worker
saga orchestrator
k8s worker
pod monitor
result processor
DLQ retries
create/delete,
watch/list pods
Redis
Mongo
Rate limiting keys
+ SSE pub/sub
consume for
API handlers
web UI:
topics/consumers
produce
domain events
and replays
results, sagas,
DLQ rows
execution queue,
idempotency keys
Project
Scope
log queries
Loki
Text is not SVG - cannot display
\ No newline at end of file +
HTTPS 5001
(SPA + Assets)
Browser SPA
HTTPS /api/v1/*
(REST/SSE endpoints)
frontend
(Nginx + Svelte)
SSE /api/v1/events/*
(EventSource)
CRUD:
- executions
- settings
- events
backend
(FastAPI + SSE)
Observability
Prom
remote_write
(metrics)
otlphtpp
otel-collectorJaeger
OTLP gRPC
(traces)
query
dashboards
(PromQL)
Grafana
kafdrop
Kubernetes API
consume execution requests and cancels
publish pod, execution and result events
worker
execution scheduler
k8s worker
pod monitor
result processor
DLQ retries
create/delete,
watch/list pods
Redis
Mongo
Rate limiting keys
+ SSE pub/sub
consume for
API handlers
web UI:
topics/consumers
produce
domain events
and replays
results,
DLQ rows
execution queue,
idempotency keys
Project
Scope
log queries
Loki
Text is not SVG - cannot display
\ No newline at end of file diff --git a/docs/components/dead-letter-queue.md b/docs/components/dead-letter-queue.md index 5afc39b33..044ab97dc 100644 --- a/docs/components/dead-letter-queue.md +++ b/docs/components/dead-letter-queue.md @@ -4,7 +4,7 @@ When a Kafka handler raises, its event goes to the dead letter queue, and the wo ## What happens to a failed event -Every subscriber except the SSE bridge uses `AckPolicy.ACK`, which commits the offset after the handler returns, whether it succeeded or raised. The k8s worker, result processor, saga orchestrator and notification routers also carry `DeadLetterMiddleware`: +Every subscriber except the SSE bridge uses `AckPolicy.ACK`, which commits the offset after the handler returns, whether it succeeded or raised. The execution scheduler and notification routers also carry `DeadLetterMiddleware`: ```python --8<-- "backend/app/events/middlewares.py:DeadLetterMiddleware" @@ -37,7 +37,7 @@ Exponential delays get 10% jitter. A message whose `retry_count` has reached the ## Storage -A row is one event in one consumer group. The unique index is on `event.event_id` plus `consumer_group`, so when the saga orchestrator and the result processor both fail on the same `execution_completed`, each gets its own row and its own retry. A TTL index deletes rows 7 days after `created_at`. +A row is one event in one consumer group. The unique index is on `event.event_id` plus `consumer_group`, so when two groups fail on the same event, each gets its own row and its own retry. A TTL index deletes rows 7 days after `created_at`. A database created before consumer groups were added still has the old unique index `idx_dlq_event_id` on `event.event_id` alone, and Beanie never drops indexes. Until someone runs `db.dlq_messages.dropIndex("idx_dlq_event_id")` once, a second group that fails on the same event can't save its row. diff --git a/docs/components/event-replay.md b/docs/components/event-replay.md index 95345ade5..22dae8a24 100644 --- a/docs/components/event-replay.md +++ b/docs/components/event-replay.md @@ -21,7 +21,7 @@ There are two admin-only ways in. ## What a replay does -The service reads matching events from the MongoDB `events` collection in batches and dispatches them one at a time. With a speed multiplier below 100 it keeps the original spacing between events, divided by the multiplier. The `kafka` target re-produces each stored event with the same `event_id` and the same payload, and no marker says it's a replay. Its timestamp is set to now unless `preserve_timestamps` is on. The gap to the next event is measured after that reset, so without `preserve_timestamps` the spacing is lost and the events go out back to back. Handlers behind an idempotency key skip a replayed event as a duplicate while that key lives (one hour for `execution_requested` in the saga orchestrator and for the k8s worker, two hours for the result processor). The `file` target appends each event as a JSON line to `target_file_path` on the API host. +The service reads matching events from the MongoDB `events` collection in batches and dispatches them one at a time. With a speed multiplier below 100 it keeps the original spacing between events, divided by the multiplier. The `kafka` target re-produces each stored event with the same `event_id` and the same payload, and no marker says it's a replay. Its timestamp is set to now unless `preserve_timestamps` is on. The gap to the next event is measured after that reset, so without `preserve_timestamps` the spacing is lost and the events go out back to back. Handlers behind an idempotency key skip a replayed event as a duplicate while that key lives (one hour for `execution_requested` and `execution_cancelled` in the execution scheduler). The `file` target appends each event as a JSON line to `target_file_path` on the API host. Session state lives in a dict in the API process and is mirrored to `replay_sessions`. A session started in one gunicorn worker can't be paused or read through `/api/v1/replay` from another, and an API restart drops the in-memory sessions. The admin status stream works from any worker, since it reads MongoDB and then Redis. diff --git a/docs/components/schema-manager.md b/docs/components/schema-manager.md index 98c266eec..bf42da52f 100644 --- a/docs/components/schema-manager.md +++ b/docs/components/schema-manager.md @@ -15,13 +15,12 @@ Four collections expire documents on their own: | `dlq_messages` | 7 days after `created_at` | | `replay_sessions` | 48 hours after `completed_at`; a session that hasn't finished has none and stays | -The 30 days for events is hardcoded in the document. `EVENT_RETENTION_DAYS` in config.toml is never read ([#293](https://github.com/HardMax71/Integr8sCode/issues/293)). Everything else stays until something deletes it. That includes `executions` and `sagas`. +The 30 days for events is hardcoded in the document. `EVENT_RETENTION_DAYS` in config.toml is never read ([#293](https://github.com/HardMax71/Integr8sCode/issues/293)). Everything else stays until something deletes it. That includes `executions`. ## Unique indexes the code relies on | Collection | Unique on | Why | |------------|-----------|-----| -| `sagas` | `execution_id` + `saga_name` | One saga of each name per execution; `get_or_create_saga` upserts on it | | `events` | `event_id` | The producer stores before publishing and skips a duplicate id, so a replayed event isn't stored twice | | `dlq_messages` | `event.event_id` + `consumer_group` | One DLQ row per event and consumer group | | `notification_subscriptions` | `user_id` + `channel` | One subscription per channel | @@ -30,7 +29,7 @@ The 30 days for events is hardcoded in the document. `EVENT_RETENTION_DAYS` in c | `system_settings` | `settings_id` | One `global` document; every read upserts it on this key | | `events_archive` | `event_id` | A retried admin delete upserts the same archive row | -`executions.execution_id`, `saved_scripts.script_id`, `sagas.saga_id`, `replay_sessions.session_id`, `audit_log.audit_id` and `notifications.notification_id` are unique as well. +`executions.execution_id`, `saved_scripts.script_id`, `replay_sessions.session_id`, `audit_log.audit_id` and `notifications.notification_id` are unique as well. Idempotency keys, the execution queue and the SSE channels live in Redis, not MongoDB. diff --git a/docs/components/sse/execution-sse-flow.md b/docs/components/sse/execution-sse-flow.md index 41fb5b6dc..cc4bbc0f0 100644 --- a/docs/components/sse/execution-sse-flow.md +++ b/docs/components/sse/execution-sse-flow.md @@ -24,7 +24,7 @@ sequenceDiagram API-->>B: each event, result_stored with the result ``` -The API writes the execution document and publishes `execution_requested`. The saga orchestrator queues it in Redis and, once a slot is free, runs the saga, which sends `create_pod_command`. The k8s worker creates the pod and publishes `pod_created`. The pod monitor turns pod phases into `pod_scheduled`, `pod_running` and finally `execution_completed`, `execution_failed` or `execution_timeout`. The result processor writes the outcome onto the execution document and publishes `result_stored`, or `result_failed` if it couldn't. Each of these goes through the Kafka bridge described in [SSE architecture](sse-architecture.md). +The API writes the execution document and publishes `execution_requested`. The worker's execution scheduler queues it in Redis and, once a slot is free, has the k8s worker create the pod, which publishes `pod_created`. The pod monitor turns pod phases into `pod_scheduled` and `pod_running`. When the pod ends, the result processor writes the outcome onto the execution document and publishes `execution_completed`, `execution_failed` or `execution_timeout`, then `result_stored`, or `result_failed` if it couldn't. Each of these goes through the Kafka bridge described in [SSE architecture](sse-architecture.md). The first event on the stream is a `status` control event built from the execution document. It holds the last status written to MongoDB, which the result processor moves to `scheduled` and `running` as the pod gets there. The SSE bridge sets the same `status` on the `pod_scheduled` and `pod_running` events it forwards, so the editor follows the pod. Redis doesn't replay what a browser missed, so when the opening status is already terminal the stream sends the stored result as `result_stored` and closes. `result_stored` itself carries only a storage reference, so the SSE service reads the result from MongoDB and attaches it before sending. The result processor publishes `result_stored` after its write returns, even when the write changed nothing because the execution was already terminal. Either way the browser gets what MongoDB holds, never a result that isn't stored. diff --git a/docs/components/sse/sse-architecture.md b/docs/components/sse/sse-architecture.md index 6c5e57e3b..ed6913971 100644 --- a/docs/components/sse/sse-architecture.md +++ b/docs/components/sse/sse-architecture.md @@ -4,7 +4,7 @@ The browser gets live updates over Server-Sent Events, and every live update com ```mermaid graph LR - Workers["Saga orchestrator, K8s worker,
pod monitor, result processor"] -->|domain events| Kafka[(Kafka)] + Workers["Worker: k8s worker,
pod monitor, result processor"] -->|domain events| Kafka[(Kafka)] Kafka -->|17 event types| Bridge["sse-bridge-pool subscriber
(API process)"] Bridge -->|"sse:exec:{id}"| Redis[(Redis pub/sub)] Notif["NotificationService
(API process)"] -->|"sse:notif:{user_id}"| Redis diff --git a/docs/components/workers/execution_scheduler.md b/docs/components/workers/execution_scheduler.md new file mode 100644 index 000000000..cd8029e9e --- /dev/null +++ b/docs/components/workers/execution_scheduler.md @@ -0,0 +1,40 @@ +# Execution scheduler + +The execution scheduler decides when an execution may start. It keeps no state of its own: the queue and the slots live in Redis, and an execution's status lives on its document in MongoDB. + +```mermaid +graph LR + Kafka[(Kafka)] -->|execution_requested| Sched[Execution scheduler] + Sched --> Queue[(Redis queue)] + Queue -->|slot free| Sched + Sched -->|create_pod| K8s[K8s worker] + Kafka -->|execution_cancelled| Sched + PodMon[Pod monitor] -->|finish| Sched +``` + +## How it works + +An `execution_requested` event goes into a Redis priority queue (`ExecutionQueueService`), then `schedule_pending` runs. That loop calls a Lua script that pops the highest priority execution only while the running set holds fewer than `max_concurrent_executions` entries, and asks the [k8s worker](k8s_worker.md) to create its pod. The limit is an admin system setting with a default of 10, and it's the concurrency gate for the whole platform. The [execution queue](../../architecture/execution-queue.md) page has the Redis keys. + +`schedule_pending` also runs when an execution ends and every 10 seconds from APScheduler. The timer covers a slot that frees itself: each running entry carries a deadline a little past the pod's own timeout, so a slot whose release got lost expires, and nothing announces that. + +If the pod can't be created, the scheduler stores the execution as `failed` with a `system_error` through the [result processor](result_processor.md), frees its slots and goes on with the next one. + +When the [pod monitor](pod_monitor.md) sees a pod finish, it calls `finish`: the scheduler frees the execution's queue slot and its user slot, then runs `schedule_pending`. + +## Cancel + +`execution_cancelled` comes from `POST /api/v1/executions/{id}/cancel`, and from a delete of an execution. The scheduler takes the execution out of the queue, stores it as `cancelled`, deletes its pod with no grace period and frees its slots. The cancel is stored before the pod goes, so when the deleted pod reports its failure, the execution has already ended and the result processor publishes nothing for it. + +## Topics + +The scheduler consumes `execution_requested` and `execution_cancelled` in the `saga-orchestrator` group, the name the removed saga orchestrator used, so the committed offsets carried over. The router has the Redis idempotency middleware and the DLQ middleware. It publishes nothing itself: the k8s worker publishes `pod_created`, and the result processor publishes the terminal events. + +## Key files + +| File | Purpose | +|------|---------| +| [`worker.py`](https://github.com/HardMax71/Integr8sCode/blob/main/backend/app/worker.py) | Entry point and APScheduler jobs | +| [`execution_scheduler.py`](https://github.com/HardMax71/Integr8sCode/blob/main/backend/app/services/execution_scheduler.py) | Queueing, starting, cancelling and freeing slots | +| [`execution_queue.py`](https://github.com/HardMax71/Integr8sCode/blob/main/backend/app/services/execution_queue.py) | Redis priority queue | +| [`handlers.py`](https://github.com/HardMax71/Integr8sCode/blob/main/backend/app/events/handlers.py) | The scheduler's Kafka router | diff --git a/docs/components/workers/index.md b/docs/components/workers/index.md index 41a917882..369cdc717 100644 --- a/docs/components/workers/index.md +++ b/docs/components/workers/index.md @@ -1,31 +1,31 @@ # Worker -One background process runs next to the API: `python -m app.worker`, in the `worker` container built from the backend image. Four parts of it carry an execution from request to stored result, and a fifth retries dead-lettered events. They share one broker connection and one Dishka container, but each consumer keeps its own Kafka consumer group, so every part still gets every event it subscribes to. +One background process runs next to the API: `python -m app.worker`, in the `worker` container built from the backend image. It runs executions: it takes them from the queue when a slot is free, creates their pods, watches the pods and stores what they print. A DLQ job retries events whose handler failed. -The parts talk to each other only through Kafka, one topic per event type, even inside the one process. The event names on the arrows below travel as those topics. MongoDB holds the durable state and Redis holds the execution queue and idempotency keys. +The parts are plain services in one process, and they call each other directly. Only the requests come in over Kafka: `execution_requested` and `execution_cancelled`, in the `saga-orchestrator` consumer group. The group keeps the name from before the saga was removed, so its committed offsets carried over. Everything the worker learns goes out as events for the API's SSE stream, its notifications and the event history. MongoDB holds the executions and Redis holds the queue and idempotency keys. ```mermaid graph LR - API[API] -->|execution_requested| Saga[Saga orchestrator] - Saga -->|create_pod_command| K8s[K8s worker] - K8s -->|pod_created| Kafka[(Kafka)] + API[API] -->|execution_requested / cancelled| Sched[Execution scheduler] + Sched --> Queue[(Redis queue)] + Sched --> K8s[K8s worker] K8s --> Pod[Executor pod] Pod -.watch.-> PodMon[Pod monitor] - PodMon -->|execution_completed / failed / timeout| Result[Result processor] - PodMon -->|same events| Saga - Result -->|result_stored| Kafka + PodMon --> Result[Result processor] + PodMon -->|slot free| Sched Result --> Mongo[(MongoDB)] + Result -->|execution_completed, result_stored| Kafka[(Kafka)] ``` | Part | What it does | How it runs | |------|--------------|-------------| -| [Saga orchestrator](saga_orchestrator.md) | Queues executions, runs the execution saga, sends pod commands | `saga-orchestrator` group; `try_schedule_from_queue` every 10s, `check_timeouts` every 30s | -| [K8s worker](k8s_worker.md) | Creates the script ConfigMap and the executor pod | `k8s-worker` group | -| [Pod monitor](pod_monitor.md) | Watches executor pods and publishes pod and execution events | watch cycle every 5s | -| [Result processor](result_processor.md) | Writes the result onto the execution document | `result-processor` group | +| [Execution scheduler](execution_scheduler.md) | Queues executions, starts them while slots are free, cancels them | `saga-orchestrator` group; `schedule_pending` every 10s | +| [K8s worker](k8s_worker.md) | Creates the script ConfigMap and the executor pod, deletes a cancelled pod | called by the scheduler | +| [Pod monitor](pod_monitor.md) | Watches executor pods, hands what they report to the result processor, deletes finished pods | watch cycle every 5s | +| [Result processor](result_processor.md) | Writes the status and result onto the execution document, then publishes the events | called by the pod monitor and the scheduler | | [DLQ processor](dlq_processor.md) | Republishes DLQ rows whose retry time has come | job every 10s | -The entry point is [`backend/app/worker.py`](https://github.com/HardMax71/Integr8sCode/blob/main/backend/app/worker.py). It loads `config.toml` plus the `config.worker.toml` override, initializes Beanie, builds the container with `create_worker_container`, registers the three routers and starts the FastStream app. On startup it applies the executor NetworkPolicy and the image pre-puller DaemonSet, then starts the scheduler. +The entry point is [`backend/app/worker.py`](https://github.com/HardMax71/Integr8sCode/blob/main/backend/app/worker.py). It loads `config.toml` plus the `config.worker.toml` override, initializes Beanie, builds the container with `create_worker_container`, registers the scheduler's router and starts the FastStream app. On startup it applies the executor namespace, NetworkPolicy, ResourceQuota and the image pre-puller DaemonSet, then starts the scheduled jobs. Replays run in the API, not here. See [Event replay](../event-replay.md). @@ -43,6 +43,6 @@ The worker reads the same `config.toml` as the container, so it connects to the ## Scaling -Compose runs one worker, and it has to stay at one. The pod monitor keeps the last seen phase of each pod in memory, so a second replica would watch the same pods and publish every event twice, and the scheduled jobs would run once per replica. The consumers alone would scale, since they go through consumer groups and guard each handler with a Redis idempotency key. The saga orchestrator keeps saga state in MongoDB and the queue in Redis, so both survive a restart, but nothing resumes a saga that was halfway through its steps when the process died. +Compose runs one worker, and it has to stay at one. The pod monitor keeps the last seen phase of each pod in memory, so a second replica would watch the same pods and store every result twice, and the scheduled jobs would run once per replica. The queue lives in Redis and the results in MongoDB, so both survive a restart. A pod that finished while the worker was down is picked up by the pod monitor's first list after the restart. A crash in any part restarts the whole container (`restart: unless-stopped`), and the consumers pick up from their committed offsets. diff --git a/docs/components/workers/k8s_worker.md b/docs/components/workers/k8s_worker.md index 603670b1f..f2692ac17 100644 --- a/docs/components/workers/k8s_worker.md +++ b/docs/components/workers/k8s_worker.md @@ -1,28 +1,27 @@ # K8s worker -The k8s worker turns saga commands into Kubernetes objects. It consumes `create_pod_command` and `delete_pod_command` with group id `k8s-worker`, and each handler runs behind a Redis idempotency key, so a redelivered command doesn't create a second pod. +The k8s worker turns an execution into Kubernetes objects. The [execution scheduler](execution_scheduler.md) calls it once a queued execution gets a slot, and again to delete the pod of a cancelled one. It consumes nothing from Kafka. ```mermaid graph LR - Kafka[(Kafka)] -->|create_pod_command| Worker[K8s worker] - Kafka -->|delete_pod_command| Worker + Sched[Execution scheduler] -->|create_pod / delete_pod| Worker[K8s worker] Worker -->|ConfigMap + Pod| K8s[Kubernetes API] - Worker -->|pod_created or execution_failed| Kafka + Worker -->|pod_created| Kafka[(Kafka)] ``` ## Creating a pod -For `create_pod_command` the worker creates a ConfigMap `script-{execution_id}` holding the user's script and `entrypoint.sh`, then a pod `executor-{execution_id}`. Once the pod exists, it patches the ConfigMap with an ownerReference to the pod, so Kubernetes deletes the ConfigMap together with the pod. If the Kubernetes API rejects that patch, the worker only logs a warning and the ConfigMap outlives the pod. Then it publishes `pod_created`. If anything else in that sequence fails, it publishes `execution_failed` with error type `system_error` and exit code -1 instead. +For a new execution the worker creates a ConfigMap `script-{execution_id}` holding the user's script and `entrypoint.sh`, then a pod `executor-{execution_id}`. Once the pod exists, it patches the ConfigMap with an ownerReference to the pod, so Kubernetes deletes the ConfigMap together with the pod. If the Kubernetes API rejects that patch, the worker only logs a warning and the ConfigMap outlives the pod. Then it publishes `pod_created`. If anything else in that sequence fails, the error goes back to the scheduler, which stores the execution as `failed` with error type `system_error` and exit code -1. A pod that already exists (409) counts as created, so a retried request doesn't make a second one. -The image and command come from the command event, which the API filled in from the runtime registry (`app/runtime_registry.py`). The registry can also give a language its own pod settings: extra env vars, a bigger `/tmp`, and minimum CPU, memory and timeout. The pod builder takes the larger of the configured value and the language minimum. Only Go uses this today: +The image and command come from the `execution_requested` event, which the API filled in from the runtime registry (`app/runtime_registry.py`). The registry can also give a language its own pod settings: extra env vars, a bigger `/tmp`, and minimum CPU, memory and timeout. The pod builder takes the larger of the configured value and the language minimum. Only Go uses this today: ```python --8<-- "backend/app/runtime_registry.py:LANGUAGE_SPECS" ``` -CPU and memory limits and the timeout arrive in the command, taken from the admin system settings when the execution was requested. Requests come from `K8S_POD_CPU_REQUEST` and `K8S_POD_MEMORY_REQUEST`. The timeout becomes the pod's `activeDeadlineSeconds`. +CPU and memory limits and the timeout arrive in the event, taken from the admin system settings when the execution was requested. Requests come from `K8S_POD_CPU_REQUEST` and `K8S_POD_MEMORY_REQUEST`. The timeout becomes the pod's `activeDeadlineSeconds`. -For `delete_pod_command`, which only the API's saga cancel route sends today, the worker deletes the pod with a 30 second grace period and lets the ownerReference take the ConfigMap with it. +For a cancelled execution the worker deletes the pod with no grace period and lets the ownerReference take the ConfigMap with it. A queued execution has no pod yet, so a 404 there is expected. ## Pod security @@ -40,17 +39,17 @@ The worker refuses to start if `K8S_NAMESPACE` is `default`. ## Startup -Before it consumes anything, the worker applies two things to the executor namespace. The first is the `executor-deny-all` NetworkPolicy, which selects pods labeled `component=executor` and allows no ingress and no egress, DNS included. The second is the Pod Security Admission labels for the `restricted` profile (enforce, warn and audit). The worker is the only thing that writes this policy, and it takes effect only if the cluster's network plugin enforces NetworkPolicy. +Before the worker consumes anything, it applies two things to the executor namespace. The first is the `executor-deny-all` NetworkPolicy, which selects pods labeled `component=executor` and allows no ingress and no egress, DNS included. The second is the Pod Security Admission labels for the `restricted` profile (enforce, warn and audit). The worker is the only thing that writes this policy, and it takes effect only if the cluster's network plugin enforces NetworkPolicy. It also applies the `runtime-image-pre-puller` DaemonSet, which pulls every image in the runtime registry on each node. The worker doesn't wait for those pulls, so an execution that arrives before they finish still waits for its image. -Every executor pod counts against the `executor-quota` ResourceQuota that the worker applies to its namespace on start (`K8S_NAMESPACE_CPU_QUOTA` and `K8S_NAMESPACE_MEMORY_QUOTA`, 32 CPU and 4Gi of requests). A pod over the quota is refused with `403 Forbidden`, and the worker publishes `pod_creation_failed`. +Every executor pod counts against the `executor-quota` ResourceQuota that the worker applies to its namespace on start (`K8S_NAMESPACE_CPU_QUOTA` and `K8S_NAMESPACE_MEMORY_QUOTA`, 32 CPU and 4Gi of requests). A pod over the quota is refused with `403 Forbidden`, and the execution is stored as `failed`. ## Key files | File | Purpose | |------|---------| | [`worker.py`](https://github.com/HardMax71/Integr8sCode/blob/main/backend/app/worker.py) | Entry point, startup setup | -| [`worker.py`](https://github.com/HardMax71/Integr8sCode/blob/main/backend/app/services/k8s_worker/worker.py) | Command handlers, namespace policy, pre-puller | +| [`worker.py`](https://github.com/HardMax71/Integr8sCode/blob/main/backend/app/services/k8s_worker/worker.py) | Pod creation and deletion, namespace policy, pre-puller | | [`pod_builder.py`](https://github.com/HardMax71/Integr8sCode/blob/main/backend/app/services/k8s_worker/pod_builder.py) | ConfigMap and pod spec | | [`runtime_registry.py`](https://github.com/HardMax71/Integr8sCode/blob/main/backend/app/runtime_registry.py) | Languages, images, per-language pod settings | diff --git a/docs/components/workers/pod_monitor.md b/docs/components/workers/pod_monitor.md index 5c51888c2..cc8b56d08 100644 --- a/docs/components/workers/pod_monitor.md +++ b/docs/components/workers/pod_monitor.md @@ -1,13 +1,15 @@ # Pod monitor -The pod monitor watches executor pods and turns what Kubernetes reports into events. It's the only component that reads a pod's output, and it publishes the events that end an execution. +The pod monitor watches executor pods and turns what Kubernetes reports into events. It's the only component that reads a pod's output. Executor pods have no network, so the output can't come any other way. ```mermaid graph LR K8s[Kubernetes API] -->|LIST, then WATCH| Monitor[Pod monitor] Monitor -->|read logs| K8s - Monitor -->|pod and execution events| Kafka[(Kafka)] + Monitor -->|pod events| Kafka[(Kafka)] + Monitor -->|completed / failed / timeout| Result[Result processor] Monitor -->|delete finished pod| K8s + Monitor -->|finish| Sched[Execution scheduler] ``` ## Watching @@ -31,11 +33,11 @@ It remembers the last phase seen per pod and drops an update with the same phase For terminal events the mapper reads the container's logs, where the entrypoint writes stdout and stderr as length-prefixed frames, and the termination message, which carries the exit code, wall time, CPU jiffies and peak memory. If the logs can't be read, a pod whose containers all exited 0 publishes nothing at all. Any other failed pod, a timed-out one included, still publishes `execution_failed` with the error taken from the pod status. -Every event goes through the producer, which stores it in the MongoDB `events` collection before sending it to Kafka. +`pod_scheduled`, `pod_running` and `pod_terminated` go straight to the producer, which stores each one in the MongoDB `events` collection before sending it to Kafka; the first two also move the stored status forward through the [result processor](result_processor.md). The three terminal events go to the result processor, which writes the result and only then publishes them. ## Cleanup -After publishing `execution_completed`, `execution_failed` or `execution_timeout`, the monitor deletes the pod with no grace period. The ConfigMap goes with it through its ownerReference. +After a terminal event, the monitor deletes the pod with no grace period, and the ConfigMap goes with it through its ownerReference. Then it tells the [execution scheduler](execution_scheduler.md) the execution finished, so its slot frees and the next queued execution starts. ## Key files diff --git a/docs/components/workers/result_processor.md b/docs/components/workers/result_processor.md index 1dec98984..463cf5a0d 100644 --- a/docs/components/workers/result_processor.md +++ b/docs/components/workers/result_processor.md @@ -1,17 +1,18 @@ # Result processor -The result processor writes the final state of every execution that reaches the k8s worker. The only other writer is the API, which marks an execution `error` when it can't publish `execution_requested`. It consumes `execution_completed`, `execution_failed` and `execution_timeout` with group id `result-processor`, one message per poll, and each handler runs behind a Redis idempotency key. It also consumes `pod_scheduled` and `pod_running`, and moves the stored status forward to `scheduled` or `running`; that update only applies from an earlier active status, so it never goes back and never overwrites a terminal one. +The result processor writes the final state of every execution that reaches the worker. The only other writer is the API, which marks an execution `error` when it can't publish `execution_requested`. The [pod monitor](pod_monitor.md) calls it with `execution_completed`, `execution_failed` or `execution_timeout`, and the [execution scheduler](execution_scheduler.md) with a cancel or a pod that couldn't be created. For `pod_scheduled` and `pod_running` it moves the stored status forward to `scheduled` or `running`; that update only applies from an earlier active status, so it never goes back and never overwrites a terminal one. ```mermaid graph LR - Kafka[(Kafka)] -->|completed / failed / timeout| Processor[Result processor] + Monitor[Pod monitor] -->|completed / failed / timeout| Processor[Result processor] + Sched[Execution scheduler] -->|cancelled / pod not created| Processor Processor -->|status, output, resource usage| Mongo[(executions)] - Processor -->|result_stored or result_failed| Kafka + Processor -->|terminal event, then result_stored or result_failed| Kafka[(Kafka)] ``` -For each event it loads the execution, records metrics, and writes the status, exit code, stdout, stderr, resource usage and error type onto the execution's document in the `executions` collection. There's no separate results collection. The write is conditional: it only applies while the execution is still in an active status, so a late or duplicate event can't overwrite a result that's already stored. A timeout is stored with exit code -1. +For each event it loads the execution, records metrics, and writes the status, exit code, stdout, stderr, resource usage and error type onto the execution's document in the `executions` collection. There's no separate results collection. The write is conditional: it only applies while the execution is still in an active status. When it doesn't apply, because the execution already ended another way, nothing is published. That keeps the failure a cancelled pod reports when it's deleted from overwriting the cancel or sending a notification. A timeout is stored with exit code -1, and a cancel with none. -After the write it publishes `result_stored`, which carries a storage reference rather than the output. The SSE service reads the output back from MongoDB when it forwards that event. If the MongoDB write or the Kafka publish fails, the processor tries to publish `result_failed` instead. That goes through the same producer, which stores the event in MongoDB first, so it fails too while MongoDB is down. +After the write it publishes the terminal event (`execution_completed`, `execution_failed` or `execution_timeout`; a cancel has its event from the API already), then `result_stored`, which carries a storage reference rather than the output. The SSE service reads the output back from MongoDB when it forwards that event. If the MongoDB write or the Kafka publish fails, the processor tries to publish `result_failed` instead. That goes through the same producer, which stores the event in MongoDB first, so it fails too while MongoDB is down. The result processor doesn't touch Kubernetes. The [pod monitor](pod_monitor.md) deletes finished pods, and their ConfigMaps go with them. @@ -19,6 +20,5 @@ The result processor doesn't touch Kubernetes. The [pod monitor](pod_monitor.md) | File | Purpose | |------|---------| -| [`worker.py`](https://github.com/HardMax71/Integr8sCode/blob/main/backend/app/worker.py) | Entry point | -| [`processor.py`](https://github.com/HardMax71/Integr8sCode/blob/main/backend/app/services/result_processor/processor.py) | Event handlers | +| [`processor.py`](https://github.com/HardMax71/Integr8sCode/blob/main/backend/app/services/result_processor/processor.py) | Writes and publishes results | | [`execution_repository.py`](https://github.com/HardMax71/Integr8sCode/blob/main/backend/app/db/repositories/execution_repository.py) | `write_terminal_result` | diff --git a/docs/components/workers/saga_orchestrator.md b/docs/components/workers/saga_orchestrator.md deleted file mode 100644 index 09e4939de..000000000 --- a/docs/components/workers/saga_orchestrator.md +++ /dev/null @@ -1,57 +0,0 @@ -# Saga orchestrator - -The saga orchestrator decides when an execution may start and sends the command that creates its pod. It keeps one saga document per execution in MongoDB and saves it after every step. There's no event sourcing: a saga's state is whatever that document says. - -```mermaid -graph LR - Kafka[(Kafka)] -->|execution_requested| Queue[(Redis queue)] - Queue -->|slot free| Saga[Execution saga] - Saga -->|create_pod_command| Kafka - Saga --> Mongo[(sagas collection)] - Kafka -->|completed / failed / timeout / cancelled| Saga -``` - -## How it works - -An `execution_requested` event doesn't start a saga right away. The orchestrator first puts it in a Redis priority queue (`ExecutionQueueService`), then calls `try_schedule_from_queue`. That runs a Lua script which pops the highest priority execution only if the active set holds fewer than `max_concurrent_executions` entries. The limit is an admin system setting with a default of 10, and it's the real concurrency gate for the whole platform. The same scheduling runs every 10 seconds from APScheduler, and again whenever an execution finishes. - -When an execution is popped, the orchestrator creates its saga document directly in the `RUNNING` state (a unique index on `execution_id` and `saga_name` stops a second saga for the same execution), publishes `saga_started`, and runs the two steps of the execution saga: validate, then create pod. Before each step the document gets `current_step`. After a step succeeds, the document gets the step name in `completed_steps` plus the saga context, and only then is the step's compensation registered. - -If a step raises or returns false, the registered compensations run in reverse order. The saga moves to `COMPENSATING` and ends as `FAILED`. Compensation only ever runs on this path and when someone cancels the saga through the API. - -After the last step the saga stays `RUNNING` and waits. `execution_completed`, `execution_failed`, `execution_timeout` and `execution_cancelled` each move it to the matching terminal state, release the execution's slot in the Redis queue, and try to schedule the next one. None of these runs compensation. - -## Saga states - -The `SagaState` enum has `CREATED`, `RUNNING`, `COMPENSATING`, `COMPLETED`, `FAILED`, `TIMEOUT` and `CANCELLED`. The last four are terminal. Nothing creates a saga in `CREATED`. - -```mermaid -stateDiagram-v2 - [*] --> RUNNING: slot free - RUNNING --> COMPENSATING: step failed - COMPENSATING --> FAILED: compensations ran - RUNNING --> COMPLETED: execution_completed - RUNNING --> FAILED: execution_failed - RUNNING --> TIMEOUT: execution_timeout or check_timeouts - COMPENSATING --> TIMEOUT: check_timeouts - RUNNING --> CANCELLED: execution_cancelled or POST /sagas/{id}/cancel - COMPENSATING --> CANCELLED: execution_cancelled or POST /sagas/{id}/cancel -``` - -`check_timeouts` runs every 30 seconds. It marks any `RUNNING` or `COMPENSATING` saga created more than 300 seconds ago as `TIMEOUT`. It only writes the state: it doesn't compensate and doesn't release the queue slot. - -`POST /api/v1/sagas/{saga_id}/cancel` runs in the API process, not in this worker. It sets `CANCELLED` atomically, publishes `saga_cancelled`, and runs the compensations of the steps that had completed: it publishes `delete_pod_command` only if `CreatePodStep` finished. Unlike `execution_cancelled`, it doesn't release the execution's slot in the Redis queue or schedule the next one, so the slot stays taken until a terminal event arrives for that execution. - -## Topics - -The orchestrator consumes `execution_requested`, `execution_completed`, `execution_failed`, `execution_timeout` and `execution_cancelled`, all with group id `saga-orchestrator`. It produces `saga_started` and `create_pod_command`. The worker never sends `delete_pod_command` in practice: create pod is the last step, so its compensation is never registered when a step fails. Only the API's cancel route sends it. - -## Key files - -| File | Purpose | -|------|---------| -| [`worker.py`](https://github.com/HardMax71/Integr8sCode/blob/main/backend/app/worker.py) | Entry point and APScheduler jobs | -| [`saga_orchestrator.py`](https://github.com/HardMax71/Integr8sCode/blob/main/backend/app/services/saga/saga_orchestrator.py) | Queueing, step loop, compensation, timeouts | -| [`execution_saga.py`](https://github.com/HardMax71/Integr8sCode/blob/main/backend/app/services/saga/execution_saga.py) | Steps and compensations | -| [`execution_queue.py`](https://github.com/HardMax71/Integr8sCode/blob/main/backend/app/services/execution_queue.py) | Redis priority queue | -| [`saga_repository.py`](https://github.com/HardMax71/Integr8sCode/blob/main/backend/app/db/repositories/saga_repository.py) | Saga persistence | diff --git a/docs/frontend/routing.md b/docs/frontend/routing.md index 9855afc50..f072c4c71 100644 --- a/docs/frontend/routing.md +++ b/docs/frontend/routing.md @@ -16,7 +16,6 @@ The frontend uses SvelteKit's filesystem router. Each page is a `+page.svelte` u | `/admin` | `routes/admin/+page.ts`, redirects to `/admin/events` | login, admin | | `/admin/events` | `routes/admin/events/+page.svelte` | login, admin | | `/admin/executions` | `routes/admin/executions/+page.svelte` | login, admin | -| `/admin/sagas` | `routes/admin/sagas/+page.svelte` | login, admin | | `/admin/users` | `routes/admin/users/+page.svelte` | login, admin | | `/admin/settings` | `routes/admin/settings/+page.svelte` | login, admin | diff --git a/docs/frontend/server-state.md b/docs/frontend/server-state.md index 62772bdb3..5d5690751 100644 --- a/docs/frontend/server-state.md +++ b/docs/frontend/server-state.md @@ -8,17 +8,17 @@ The hey-api `@tanstack/svelte-query` plugin generates the options, so there's no ## Reading -A list page builds its query from its own state, and the options callback reads that state, so a new filter, page or refresh rate is a new query key and TanStack fetches it: +A list page builds its query from its own state, and the options callback reads that state, so a new filter or page is a new query key and TanStack fetches it: ```typescript ---8<-- "frontend/src/routes/admin/sagas/+page.svelte:list" +--8<-- "frontend/src/routes/admin/executions/+page.svelte:list" ``` Every key is its own cache entry, so a slow response for an old filter can't overwrite a newer one. `keepPreviousData` keeps the last page on screen while the next one loads. `refetchInterval` is the polling: TanStack pauses it while the tab is hidden and stops it when the page unmounts. The page size is page state and the page index is derived from the filters, so a new filter goes back to the first page, since the old page may not exist under it. The table component binds both as a TanStack Table `PaginationState`, and the query turns them into `skip` and `limit`; the table only renders the page the server sent. `isPending` is true until the first data arrives, and the tables show their spinner for it. `isFetching` is true during any request, polls included, and only drives the refresh buttons. -A component fetches what only it shows. The page keeps the id of the open saga or event, and the detail modal runs the query for it; following a related event changes the id, which is a new key. A failed load closes the modal, since the interceptor has already shown the error. The replay banner follows the replay status stream while it's mounted, and closing it aborts the stream. +A component fetches what only it shows. The page keeps the id of the open user or event, and the detail modal runs the query for it; following a related event changes the id, which is a new key. A failed load closes the modal, since the interceptor has already shown the error. The replay banner follows the replay status stream while it's mounted, and closing it aborts the stream. ## Writing diff --git a/docs/getting-started.md b/docs/getting-started.md index f024e5006..a26d51dca 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -36,7 +36,7 @@ curl -k https://localhost:5001/api/v1/health/live ## Log in and run a script -Open [https://localhost:5001](https://localhost:5001) and accept the self-signed certificate. Log in with username `user` and password `user123`. For the admin pages (events, executions, sagas, users, system settings) use `admin` / `admin123`. The backend creates both accounts, or resets their passwords, every time it starts. Anywhere that isn't your laptop, put your own values in `.env`. +Open [https://localhost:5001](https://localhost:5001) and accept the self-signed certificate. Log in with username `user` and password `user123`. For the admin pages (events, executions, users, system settings) use `admin` / `admin123`. The backend creates both accounts, or resets their passwords, every time it starts. Anywhere that isn't your laptop, put your own values in `.env`. After login you land in the editor, which starts with a small Python script. Click **Run Script** and the editor shows the status while the pod runs, then the output once it finishes. The language picker offers what the runtime registry lists: Python 3.14, 3.13, 3.12 and 3.11, Node 26, 24 and 22, Ruby 4.0, 3.4 and 3.3, Go 1.27 and 1.26, and Bash 5.3. @@ -46,10 +46,10 @@ Limits are system-wide, not per execution. An execution request carries only the 1. The frontend posts the script, language and version to `POST /api/v1/execute`. 2. The API checks the version is offered, writes an execution document and publishes `execution_requested` to Kafka. -3. The saga orchestrator puts the execution in a Redis priority queue. When fewer than 10 executions are active (an admin setting), it runs the saga, which publishes `create_pod_command`. +3. The worker's execution scheduler puts the execution in a Redis priority queue. When fewer than 10 executions are active (an admin setting), it starts it. 4. The k8s worker creates a ConfigMap with the script and a locked-down pod. -5. The pod monitor sees the pod finish, reads its output and publishes `execution_completed`, `execution_failed` or `execution_timeout`, then deletes the pod. -6. The result processor writes the result onto the execution document and publishes `result_stored`. +5. The pod monitor sees the pod finish, reads its output and hands it to the result processor, then deletes the pod and frees the slot. +6. The result processor writes the result onto the execution document and publishes `execution_completed`, `execution_failed` or `execution_timeout`, then `result_stored`. 7. The API forwards these events from Kafka through Redis to your browser over Server-Sent Events. [Workers](components/workers/index.md) and [Execution SSE flow](components/sse/execution-sse-flow.md) go through each step. diff --git a/docs/index.md b/docs/index.md index 20a2d3385..4fa48296b 100644 --- a/docs/index.md +++ b/docs/index.md @@ -55,7 +55,7 @@ The API and the worker talk through Kafka, one topic per event type. Every event --- - Workers, SSE, the execution saga, the DLQ and MongoDB indexes + Workers, SSE, the DLQ and MongoDB indexes - :material-wrench: **[Operations](operations/deployment.md)** diff --git a/docs/operations/metrics-reference.md b/docs/operations/metrics-reference.md index f08d03f92..fdb83547e 100644 --- a/docs/operations/metrics-reference.md +++ b/docs/operations/metrics-reference.md @@ -82,7 +82,7 @@ All of these come from spans (see [Metrics made from spans](#metrics-made-from-s | `idempotency_processing_duration_seconds` | histogram | idempotency_event_type, idempotency_status | `idempotency.processing_seconds` on span `idempotency.processed`: reserving the key to marking it completed or failed | | `idempotency_keys_active` | gauge | none | span `idempotency.keys` from the worker's job every 30 s, which counts `idempotency:*` in Redis with `SCAN` | -Every Kafka publish also shows up in `messaging_publish_messages_message_total`; a publish that fails is marked on the stored event and not counted here. A handler that raises has no counter of its own: it's a `messaging_process_duration_seconds` sample with an `error_type` label, in the FastStream families below. `IdempotencyManager` runs in backend for `Idempotency-Key` requests and in the worker's k8s worker, result processor and saga orchestrator consumers. +Every Kafka publish also shows up in `messaging_publish_messages_message_total`; a publish that fails is marked on the stored event and not counted here. A handler that raises has no counter of its own: it's a `messaging_process_duration_seconds` sample with an `error_type` label, in the FastStream families below. `IdempotencyManager` runs in backend for `Idempotency-Key` requests and in the worker's execution scheduler consumer. ### Server-sent events (backend) @@ -181,7 +181,7 @@ Each login runs in an `auth.login` span; the other events leave a span named in ### Dead letter queue -`DLQManager` puts each step on a span. `handle_message` runs wherever a router's `DeadLetterMiddleware` catches a failure: the backend for notifications, and the k8s worker, result processor and saga orchestrator groups in the worker. Scheduled retries and the size report run from the worker's APScheduler job every 10 seconds. +`DLQManager` puts each step on a span. `handle_message` runs wherever a router's `DeadLetterMiddleware` catches a failure: the backend for notifications, and the execution scheduler's group in the worker. Scheduled retries and the size report run from the worker's APScheduler job every 10 seconds. | Metric | Type | Labels | Recorded from | |--------|------|--------|---------------| diff --git a/docs/operations/tracing.md b/docs/operations/tracing.md index 73307ceac..929a11793 100644 --- a/docs/operations/tracing.md +++ b/docs/operations/tracing.md @@ -22,7 +22,7 @@ config.toml sends traces to `http://otel-collector:4317` with a sampling rate of The broker is created with FastStream's `KafkaTelemetryMiddleware`. On publish it opens a producer span and writes the trace context into the message headers. On consume it reads the headers and opens a consumer span as a child. There's no custom consumer code involved. -A message published with no active span starts a new trace. That happens in three places: pod monitor events, which come from the Kubernetes watch; DLQ retries, which the DLQ processor publishes from an APScheduler job; and queued sagas that the scheduler job starts. A queued saga started while the orchestrator handles another event, such as another execution's completion, joins that event's trace instead. +A message published with no active span starts a new trace. That happens in three places: pod monitor events, which come from the Kubernetes watch; DLQ retries, which the DLQ processor publishes from an APScheduler job; and queued executions that the 10 second scheduler job starts. A queued execution started while the scheduler handles a request joins that request's trace instead. ## Spans and attributes @@ -33,7 +33,7 @@ Beyond the automatic HTTP, Mongo, outbound HTTPX and Kafka spans: | `POST /api/v1/execute` | language, language version, script length, `user.id`, `client.address` on the server span | | `EventRepository.store_event` | `event.type`, `event.id`, `execution.id` on the current span | | `UnifiedProducer.produce` | a `publish_event` span with `event.type` and `aggregate.id` | -| `SagaOrchestrator` | a `saga.step` span per step with saga name, id, step and execution id | +| `ExecutionScheduler` | a `queue.schedule_pending` span per pass, and a `pod.create` span per pod the k8s worker creates | | Webhook and Slack notifications | `notification.id`, `notification.channel` (and webhook host) on the current span | ## An execution, end to end @@ -43,7 +43,7 @@ sequenceDiagram participant Client participant API as Backend participant Kafka - participant Saga as Worker: saga orchestrator + participant Sched as Worker: execution scheduler participant K8s as Worker: k8s worker Client->>API: POST /api/v1/execute @@ -51,14 +51,14 @@ sequenceDiagram API->>Kafka: execution_requested Note over API: producer span, context in headers API-->>Client: 200 ExecutionResponse - Kafka->>Saga: execution_requested - Note over Saga: consumer span, saga.step spans - Saga->>Kafka: create_pod_command - Kafka->>K8s: create_pod_command - Note over K8s: consumer span + Kafka->>Sched: execution_requested + Note over Sched: consumer span, queue.schedule_pending span + Sched->>K8s: create_pod + Note over K8s: pod.create span + K8s->>Kafka: pod_created ``` -The API stores the event in MongoDB, publishes `execution_requested` and answers 200. The saga orchestrator picks it up in a consumer span that continues the same trace, runs its steps as `saga.step` spans and publishes `create_pod_command`. The k8s worker's consumer span joins the trace too. Its calls to the Kubernetes API go through kubernetes_asyncio, which isn't instrumented, so they don't show up as spans. From there, pod monitor events start fresh traces, as described above. +The API stores the event in MongoDB, publishes `execution_requested` and answers 200. The execution scheduler picks it up in a consumer span that continues the same trace and, when a slot is free, has the k8s worker create the pod in a `pod.create` span of the same trace. Its calls to the Kubernetes API go through kubernetes_asyncio, which isn't instrumented, so they don't show up as spans. From there, pod monitor events start fresh traces, as described above. ## Jaeger diff --git a/docs/reference/openapi.json b/docs/reference/openapi.json index 9bb22a11f..4af37bf82 100644 --- a/docs/reference/openapi.json +++ b/docs/reference/openapi.json @@ -712,36 +712,6 @@ { "$ref": "#/components/schemas/NotificationPreferencesUpdatedEvent" }, - { - "$ref": "#/components/schemas/SagaStartedEvent" - }, - { - "$ref": "#/components/schemas/SagaCompletedEvent" - }, - { - "$ref": "#/components/schemas/SagaFailedEvent" - }, - { - "$ref": "#/components/schemas/SagaCancelledEvent" - }, - { - "$ref": "#/components/schemas/SagaCompensatingEvent" - }, - { - "$ref": "#/components/schemas/SagaCompensatedEvent" - }, - { - "$ref": "#/components/schemas/CreatePodCommandEvent" - }, - { - "$ref": "#/components/schemas/DeletePodCommandEvent" - }, - { - "$ref": "#/components/schemas/AllocateResourcesCommandEvent" - }, - { - "$ref": "#/components/schemas/ReleaseResourcesCommandEvent" - }, { "$ref": "#/components/schemas/ScriptSavedEvent" }, @@ -821,16 +791,6 @@ "notification_all_read": "#/components/schemas/NotificationAllReadEvent", "notification_clicked": "#/components/schemas/NotificationClickedEvent", "notification_preferences_updated": "#/components/schemas/NotificationPreferencesUpdatedEvent", - "saga_started": "#/components/schemas/SagaStartedEvent", - "saga_completed": "#/components/schemas/SagaCompletedEvent", - "saga_failed": "#/components/schemas/SagaFailedEvent", - "saga_cancelled": "#/components/schemas/SagaCancelledEvent", - "saga_compensating": "#/components/schemas/SagaCompensatingEvent", - "saga_compensated": "#/components/schemas/SagaCompensatedEvent", - "create_pod_command": "#/components/schemas/CreatePodCommandEvent", - "delete_pod_command": "#/components/schemas/DeletePodCommandEvent", - "allocate_resources_command": "#/components/schemas/AllocateResourcesCommandEvent", - "release_resources_command": "#/components/schemas/ReleaseResourcesCommandEvent", "script_saved": "#/components/schemas/ScriptSavedEvent", "script_deleted": "#/components/schemas/ScriptDeletedEvent", "script_shared": "#/components/schemas/ScriptSharedEvent", @@ -4163,411 +4123,406 @@ } } } - }, - "/api/v1/sagas/{saga_id}": { - "get": { - "tags": [ - "sagas" - ], - "summary": "Get Saga Status", - "description": "Get saga status by ID.", - "operationId": "get_saga_status_api_v1_sagas__saga_id__get", - "parameters": [ - { - "name": "saga_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "title": "Saga Id" - } + } + }, + "components": { + "schemas": { + "AdminExecutionListResponse": { + "properties": { + "executions": { + "items": { + "$ref": "#/components/schemas/AdminExecutionResponse" + }, + "type": "array", + "title": "Executions" + }, + "total": { + "type": "integer", + "title": "Total" + }, + "limit": { + "type": "integer", + "title": "Limit" + }, + "skip": { + "type": "integer", + "title": "Skip" + }, + "has_more": { + "type": "boolean", + "title": "Has More" } + }, + "type": "object", + "required": [ + "executions", + "total", + "limit", + "skip", + "has_more" ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SagaStatusResponse" - } - } - } + "title": "AdminExecutionListResponse", + "description": "Paginated list of executions for admin." + }, + "AdminExecutionResponse": { + "properties": { + "execution_id": { + "type": "string", + "title": "Execution Id" }, - "default": { - "description": "Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } + "script": { + "type": "string", + "title": "Script" + }, + "status": { + "$ref": "#/components/schemas/ExecutionStatus" + }, + "lang": { + "type": "string", + "title": "Lang" + }, + "lang_version": { + "type": "string", + "title": "Lang Version" + }, + "stdout": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" } - } + ], + "title": "Stdout" }, - "403": { - "description": "Access denied", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } + "stderr": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" } - } + ], + "title": "Stderr" }, - "404": { - "description": "Saga not found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } + "exit_code": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" } - } - } - } - } - }, - "/api/v1/sagas/execution/{execution_id}": { - "get": { - "tags": [ - "sagas" - ], - "summary": "Get Execution Sagas", - "description": "Get all sagas for an execution.", - "operationId": "get_execution_sagas_api_v1_sagas_execution__execution_id__get", - "parameters": [ - { - "name": "execution_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "title": "Execution Id" - } + ], + "title": "Exit Code" }, - { - "name": "state", - "in": "query", - "required": false, - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/SagaState" - }, - { - "type": "null" - } - ], - "description": "Filter by saga state", - "title": "State" - }, - "description": "Filter by saga state" + "error_type": { + "anyOf": [ + { + "$ref": "#/components/schemas/ExecutionErrorType" + }, + { + "type": "null" + } + ] }, - { - "name": "limit", - "in": "query", - "required": false, - "schema": { - "type": "integer", - "maximum": 1000, - "minimum": 1, - "default": 100, - "title": "Limit" - } + "priority": { + "$ref": "#/components/schemas/QueuePriority", + "default": "normal" }, - { - "name": "skip", - "in": "query", - "required": false, - "schema": { - "type": "integer", - "minimum": 0, - "default": 0, - "title": "Skip" - } - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SagaListResponse" - } + "user_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" } - } + ], + "title": "User Id" }, - "default": { - "description": "Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } + "created_at": { + "type": "string", + "format": "date-time", + "title": "Created At" }, - "403": { - "description": "Access denied", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } + "updated_at": { + "type": "string", + "format": "date-time", + "title": "Updated At" } - } - } - }, - "/api/v1/sagas/": { - "get": { - "tags": [ - "sagas" + }, + "type": "object", + "required": [ + "execution_id", + "script", + "status", + "lang", + "lang_version", + "stdout", + "stderr", + "exit_code", + "error_type", + "priority", + "user_id", + "created_at", + "updated_at" ], - "summary": "List Sagas", - "description": "List sagas accessible by the current user.", - "operationId": "list_sagas_api_v1_sagas__get", - "parameters": [ - { - "name": "state", - "in": "query", - "required": false, - "schema": { - "anyOf": [ + "title": "AdminExecutionResponse", + "description": "Execution with priority for admin views." + }, + "AdminUserOverview": { + "properties": { + "user": { + "$ref": "#/components/schemas/UserResponse" + }, + "stats": { + "$ref": "#/components/schemas/EventStatistics" + }, + "derived_counts": { + "$ref": "#/components/schemas/DerivedCounts" + }, + "rate_limit_summary": { + "$ref": "#/components/schemas/RateLimitSummary" + }, + "recent_events": { + "items": { + "oneOf": [ { - "$ref": "#/components/schemas/SagaState" + "$ref": "#/components/schemas/ExecutionRequestedEvent" }, { - "type": "null" - } - ], - "description": "Filter by saga state", - "title": "State" - }, - "description": "Filter by saga state" - }, - { - "name": "execution_id", - "in": "query", - "required": false, - "schema": { - "anyOf": [ + "$ref": "#/components/schemas/ExecutionAcceptedEvent" + }, { - "type": "string" + "$ref": "#/components/schemas/ExecutionQueuedEvent" }, { - "type": "null" - } - ], - "description": "Filter by execution ID", - "title": "Execution Id" - }, - "description": "Filter by execution ID" - }, - { - "name": "search", - "in": "query", - "required": false, - "schema": { - "anyOf": [ + "$ref": "#/components/schemas/ExecutionStartedEvent" + }, { - "type": "string", - "maxLength": 200 + "$ref": "#/components/schemas/ExecutionRunningEvent" }, { - "type": "null" + "$ref": "#/components/schemas/ExecutionCompletedEvent" + }, + { + "$ref": "#/components/schemas/ExecutionFailedEvent" + }, + { + "$ref": "#/components/schemas/ExecutionTimeoutEvent" + }, + { + "$ref": "#/components/schemas/ExecutionCancelledEvent" + }, + { + "$ref": "#/components/schemas/PodCreatedEvent" + }, + { + "$ref": "#/components/schemas/PodScheduledEvent" + }, + { + "$ref": "#/components/schemas/PodRunningEvent" + }, + { + "$ref": "#/components/schemas/PodSucceededEvent" + }, + { + "$ref": "#/components/schemas/PodFailedEvent" + }, + { + "$ref": "#/components/schemas/PodTerminatedEvent" + }, + { + "$ref": "#/components/schemas/PodDeletedEvent" + }, + { + "$ref": "#/components/schemas/ResultStoredEvent" + }, + { + "$ref": "#/components/schemas/ResultFailedEvent" + }, + { + "$ref": "#/components/schemas/UserSettingsUpdatedEvent" + }, + { + "$ref": "#/components/schemas/UserRegisteredEvent" + }, + { + "$ref": "#/components/schemas/UserLoginEvent" + }, + { + "$ref": "#/components/schemas/UserLoggedInEvent" + }, + { + "$ref": "#/components/schemas/UserLoggedOutEvent" + }, + { + "$ref": "#/components/schemas/UserUpdatedEvent" + }, + { + "$ref": "#/components/schemas/UserDeletedEvent" + }, + { + "$ref": "#/components/schemas/NotificationCreatedEvent" + }, + { + "$ref": "#/components/schemas/NotificationSentEvent" + }, + { + "$ref": "#/components/schemas/NotificationDeliveredEvent" + }, + { + "$ref": "#/components/schemas/NotificationFailedEvent" + }, + { + "$ref": "#/components/schemas/NotificationReadEvent" + }, + { + "$ref": "#/components/schemas/NotificationAllReadEvent" + }, + { + "$ref": "#/components/schemas/NotificationClickedEvent" + }, + { + "$ref": "#/components/schemas/NotificationPreferencesUpdatedEvent" + }, + { + "$ref": "#/components/schemas/ScriptSavedEvent" + }, + { + "$ref": "#/components/schemas/ScriptDeletedEvent" + }, + { + "$ref": "#/components/schemas/ScriptSharedEvent" + }, + { + "$ref": "#/components/schemas/SecurityViolationEvent" + }, + { + "$ref": "#/components/schemas/RateLimitExceededEvent" + }, + { + "$ref": "#/components/schemas/AuthFailedEvent" + }, + { + "$ref": "#/components/schemas/ResourceLimitExceededEvent" + }, + { + "$ref": "#/components/schemas/QuotaExceededEvent" + }, + { + "$ref": "#/components/schemas/SystemErrorEvent" + }, + { + "$ref": "#/components/schemas/ServiceUnhealthyEvent" + }, + { + "$ref": "#/components/schemas/ServiceRecoveredEvent" + }, + { + "$ref": "#/components/schemas/DLQMessageReceivedEvent" + }, + { + "$ref": "#/components/schemas/DLQMessageRetriedEvent" + }, + { + "$ref": "#/components/schemas/DLQMessageDiscardedEvent" } ], - "description": "Search saga ID, saga name, execution ID or error message", - "title": "Search" - }, - "description": "Search saga ID, saga name, execution ID or error message" - }, - { - "name": "limit", - "in": "query", - "required": false, - "schema": { - "type": "integer", - "maximum": 1000, - "minimum": 1, - "default": 100, - "title": "Limit" - } - }, - { - "name": "skip", - "in": "query", - "required": false, - "schema": { - "type": "integer", - "minimum": 0, - "default": 0, - "title": "Skip" - } - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SagaListResponse" - } - } - } - }, - "default": { - "description": "Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - } - } - } - }, - "/api/v1/sagas/{saga_id}/cancel": { - "post": { - "tags": [ - "sagas" - ], - "summary": "Cancel Saga", - "description": "Cancel a running saga.", - "operationId": "cancel_saga_api_v1_sagas__saga_id__cancel_post", - "parameters": [ - { - "name": "saga_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "title": "Saga Id" - } - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SagaCancellationResponse" - } - } - } - }, - "default": { - "description": "Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, - "400": { - "description": "Saga is not in a cancellable state", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, - "403": { - "description": "Access denied", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, - "404": { - "description": "Saga not found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - } - } - } - } - }, - "components": { - "schemas": { - "AdminExecutionListResponse": { - "properties": { - "executions": { - "items": { - "$ref": "#/components/schemas/AdminExecutionResponse" + "discriminator": { + "propertyName": "event_type", + "mapping": { + "auth_failed": "#/components/schemas/AuthFailedEvent", + "dlq_message_discarded": "#/components/schemas/DLQMessageDiscardedEvent", + "dlq_message_received": "#/components/schemas/DLQMessageReceivedEvent", + "dlq_message_retried": "#/components/schemas/DLQMessageRetriedEvent", + "execution_accepted": "#/components/schemas/ExecutionAcceptedEvent", + "execution_cancelled": "#/components/schemas/ExecutionCancelledEvent", + "execution_completed": "#/components/schemas/ExecutionCompletedEvent", + "execution_failed": "#/components/schemas/ExecutionFailedEvent", + "execution_queued": "#/components/schemas/ExecutionQueuedEvent", + "execution_requested": "#/components/schemas/ExecutionRequestedEvent", + "execution_running": "#/components/schemas/ExecutionRunningEvent", + "execution_started": "#/components/schemas/ExecutionStartedEvent", + "execution_timeout": "#/components/schemas/ExecutionTimeoutEvent", + "notification_all_read": "#/components/schemas/NotificationAllReadEvent", + "notification_clicked": "#/components/schemas/NotificationClickedEvent", + "notification_created": "#/components/schemas/NotificationCreatedEvent", + "notification_delivered": "#/components/schemas/NotificationDeliveredEvent", + "notification_failed": "#/components/schemas/NotificationFailedEvent", + "notification_preferences_updated": "#/components/schemas/NotificationPreferencesUpdatedEvent", + "notification_read": "#/components/schemas/NotificationReadEvent", + "notification_sent": "#/components/schemas/NotificationSentEvent", + "pod_created": "#/components/schemas/PodCreatedEvent", + "pod_deleted": "#/components/schemas/PodDeletedEvent", + "pod_failed": "#/components/schemas/PodFailedEvent", + "pod_running": "#/components/schemas/PodRunningEvent", + "pod_scheduled": "#/components/schemas/PodScheduledEvent", + "pod_succeeded": "#/components/schemas/PodSucceededEvent", + "pod_terminated": "#/components/schemas/PodTerminatedEvent", + "quota_exceeded": "#/components/schemas/QuotaExceededEvent", + "rate_limit_exceeded": "#/components/schemas/RateLimitExceededEvent", + "resource_limit_exceeded": "#/components/schemas/ResourceLimitExceededEvent", + "result_failed": "#/components/schemas/ResultFailedEvent", + "result_stored": "#/components/schemas/ResultStoredEvent", + "script_deleted": "#/components/schemas/ScriptDeletedEvent", + "script_saved": "#/components/schemas/ScriptSavedEvent", + "script_shared": "#/components/schemas/ScriptSharedEvent", + "security_violation": "#/components/schemas/SecurityViolationEvent", + "service_recovered": "#/components/schemas/ServiceRecoveredEvent", + "service_unhealthy": "#/components/schemas/ServiceUnhealthyEvent", + "system_error": "#/components/schemas/SystemErrorEvent", + "user_deleted": "#/components/schemas/UserDeletedEvent", + "user_logged_in": "#/components/schemas/UserLoggedInEvent", + "user_logged_out": "#/components/schemas/UserLoggedOutEvent", + "user_login": "#/components/schemas/UserLoginEvent", + "user_registered": "#/components/schemas/UserRegisteredEvent", + "user_settings_updated": "#/components/schemas/UserSettingsUpdatedEvent", + "user_updated": "#/components/schemas/UserUpdatedEvent" + } + } }, "type": "array", - "title": "Executions" - }, - "total": { - "type": "integer", - "title": "Total" - }, - "limit": { - "type": "integer", - "title": "Limit" - }, - "skip": { - "type": "integer", - "title": "Skip" - }, - "has_more": { - "type": "boolean", - "title": "Has More" + "title": "Recent Events", + "default": [] } }, "type": "object", "required": [ - "executions", - "total", - "limit", - "skip", - "has_more" + "user", + "stats", + "derived_counts", + "rate_limit_summary", + "recent_events" ], - "title": "AdminExecutionListResponse", - "description": "Paginated list of executions for admin." + "title": "AdminUserOverview" }, - "AdminExecutionResponse": { + "AuthFailedEvent": { "properties": { - "execution_id": { + "event_id": { "type": "string", - "title": "Execution Id" + "title": "Event Id" }, - "script": { + "event_type": { "type": "string", - "title": "Script" - }, - "status": { - "$ref": "#/components/schemas/ExecutionStatus" + "const": "auth_failed", + "title": "Event Type", + "default": "auth_failed" }, - "lang": { + "event_version": { "type": "string", - "title": "Lang" + "title": "Event Version", + "default": "1.0" }, - "lang_version": { + "timestamp": { "type": "string", - "title": "Lang Version" + "format": "date-time", + "title": "Timestamp" }, - "stdout": { + "aggregate_id": { "anyOf": [ { "type": "string" @@ -4576,9 +4531,12 @@ "type": "null" } ], - "title": "Stdout" + "title": "Aggregate Id" }, - "stderr": { + "metadata": { + "$ref": "#/components/schemas/EventMetadata" + }, + "username": { "anyOf": [ { "type": "string" @@ -4587,34 +4545,63 @@ "type": "null" } ], - "title": "Stderr" + "title": "Username" }, - "exit_code": { + "reason": { + "type": "string", + "title": "Reason" + }, + "ip_address": { "anyOf": [ { - "type": "integer" + "type": "string" }, { "type": "null" } ], - "title": "Exit Code" - }, - "error_type": { + "title": "Ip Address" + } + }, + "type": "object", + "required": [ + "event_id", + "event_type", + "event_version", + "timestamp", + "metadata" + ], + "title": "AuthFailedEvent" + }, + "Body_login_api_v1_auth_login_post": { + "properties": { + "grant_type": { "anyOf": [ { - "$ref": "#/components/schemas/ExecutionErrorType" + "type": "string", + "pattern": "^password$" }, { "type": "null" } - ] + ], + "title": "Grant Type" }, - "priority": { - "$ref": "#/components/schemas/QueuePriority", - "default": "normal" + "username": { + "type": "string", + "title": "Username" }, - "user_id": { + "password": { + "type": "string", + "format": "password", + "title": "Password" + }, + "scope": { + "type": "string", + "title": "Scope", + "default": "" + }, + "client_id": { "anyOf": [ { "type": "string" @@ -4623,1904 +4610,55 @@ "type": "null" } ], - "title": "User Id" - }, - "created_at": { - "type": "string", - "format": "date-time", - "title": "Created At" + "title": "Client Id" }, - "updated_at": { - "type": "string", - "format": "date-time", - "title": "Updated At" - } - }, - "type": "object", - "required": [ - "execution_id", - "script", - "status", - "lang", - "lang_version", - "stdout", - "stderr", - "exit_code", - "error_type", - "priority", - "user_id", - "created_at", - "updated_at" - ], - "title": "AdminExecutionResponse", - "description": "Execution with priority for admin views." - }, - "AdminUserOverview": { - "properties": { - "user": { - "$ref": "#/components/schemas/UserResponse" - }, - "stats": { - "$ref": "#/components/schemas/EventStatistics" - }, - "derived_counts": { - "$ref": "#/components/schemas/DerivedCounts" - }, - "rate_limit_summary": { - "$ref": "#/components/schemas/RateLimitSummary" - }, - "recent_events": { - "items": { - "oneOf": [ - { - "$ref": "#/components/schemas/ExecutionRequestedEvent" - }, - { - "$ref": "#/components/schemas/ExecutionAcceptedEvent" - }, - { - "$ref": "#/components/schemas/ExecutionQueuedEvent" - }, - { - "$ref": "#/components/schemas/ExecutionStartedEvent" - }, - { - "$ref": "#/components/schemas/ExecutionRunningEvent" - }, - { - "$ref": "#/components/schemas/ExecutionCompletedEvent" - }, - { - "$ref": "#/components/schemas/ExecutionFailedEvent" - }, - { - "$ref": "#/components/schemas/ExecutionTimeoutEvent" - }, - { - "$ref": "#/components/schemas/ExecutionCancelledEvent" - }, - { - "$ref": "#/components/schemas/PodCreatedEvent" - }, - { - "$ref": "#/components/schemas/PodScheduledEvent" - }, - { - "$ref": "#/components/schemas/PodRunningEvent" - }, - { - "$ref": "#/components/schemas/PodSucceededEvent" - }, - { - "$ref": "#/components/schemas/PodFailedEvent" - }, - { - "$ref": "#/components/schemas/PodTerminatedEvent" - }, - { - "$ref": "#/components/schemas/PodDeletedEvent" - }, - { - "$ref": "#/components/schemas/ResultStoredEvent" - }, - { - "$ref": "#/components/schemas/ResultFailedEvent" - }, - { - "$ref": "#/components/schemas/UserSettingsUpdatedEvent" - }, - { - "$ref": "#/components/schemas/UserRegisteredEvent" - }, - { - "$ref": "#/components/schemas/UserLoginEvent" - }, - { - "$ref": "#/components/schemas/UserLoggedInEvent" - }, - { - "$ref": "#/components/schemas/UserLoggedOutEvent" - }, - { - "$ref": "#/components/schemas/UserUpdatedEvent" - }, - { - "$ref": "#/components/schemas/UserDeletedEvent" - }, - { - "$ref": "#/components/schemas/NotificationCreatedEvent" - }, - { - "$ref": "#/components/schemas/NotificationSentEvent" - }, - { - "$ref": "#/components/schemas/NotificationDeliveredEvent" - }, - { - "$ref": "#/components/schemas/NotificationFailedEvent" - }, - { - "$ref": "#/components/schemas/NotificationReadEvent" - }, - { - "$ref": "#/components/schemas/NotificationAllReadEvent" - }, - { - "$ref": "#/components/schemas/NotificationClickedEvent" - }, - { - "$ref": "#/components/schemas/NotificationPreferencesUpdatedEvent" - }, - { - "$ref": "#/components/schemas/SagaStartedEvent" - }, - { - "$ref": "#/components/schemas/SagaCompletedEvent" - }, - { - "$ref": "#/components/schemas/SagaFailedEvent" - }, - { - "$ref": "#/components/schemas/SagaCancelledEvent" - }, - { - "$ref": "#/components/schemas/SagaCompensatingEvent" - }, - { - "$ref": "#/components/schemas/SagaCompensatedEvent" - }, - { - "$ref": "#/components/schemas/CreatePodCommandEvent" - }, - { - "$ref": "#/components/schemas/DeletePodCommandEvent" - }, - { - "$ref": "#/components/schemas/AllocateResourcesCommandEvent" - }, - { - "$ref": "#/components/schemas/ReleaseResourcesCommandEvent" - }, - { - "$ref": "#/components/schemas/ScriptSavedEvent" - }, - { - "$ref": "#/components/schemas/ScriptDeletedEvent" - }, - { - "$ref": "#/components/schemas/ScriptSharedEvent" - }, - { - "$ref": "#/components/schemas/SecurityViolationEvent" - }, - { - "$ref": "#/components/schemas/RateLimitExceededEvent" - }, - { - "$ref": "#/components/schemas/AuthFailedEvent" - }, - { - "$ref": "#/components/schemas/ResourceLimitExceededEvent" - }, - { - "$ref": "#/components/schemas/QuotaExceededEvent" - }, - { - "$ref": "#/components/schemas/SystemErrorEvent" - }, - { - "$ref": "#/components/schemas/ServiceUnhealthyEvent" - }, - { - "$ref": "#/components/schemas/ServiceRecoveredEvent" - }, - { - "$ref": "#/components/schemas/DLQMessageReceivedEvent" - }, - { - "$ref": "#/components/schemas/DLQMessageRetriedEvent" - }, - { - "$ref": "#/components/schemas/DLQMessageDiscardedEvent" - } - ], - "discriminator": { - "propertyName": "event_type", - "mapping": { - "allocate_resources_command": "#/components/schemas/AllocateResourcesCommandEvent", - "auth_failed": "#/components/schemas/AuthFailedEvent", - "create_pod_command": "#/components/schemas/CreatePodCommandEvent", - "delete_pod_command": "#/components/schemas/DeletePodCommandEvent", - "dlq_message_discarded": "#/components/schemas/DLQMessageDiscardedEvent", - "dlq_message_received": "#/components/schemas/DLQMessageReceivedEvent", - "dlq_message_retried": "#/components/schemas/DLQMessageRetriedEvent", - "execution_accepted": "#/components/schemas/ExecutionAcceptedEvent", - "execution_cancelled": "#/components/schemas/ExecutionCancelledEvent", - "execution_completed": "#/components/schemas/ExecutionCompletedEvent", - "execution_failed": "#/components/schemas/ExecutionFailedEvent", - "execution_queued": "#/components/schemas/ExecutionQueuedEvent", - "execution_requested": "#/components/schemas/ExecutionRequestedEvent", - "execution_running": "#/components/schemas/ExecutionRunningEvent", - "execution_started": "#/components/schemas/ExecutionStartedEvent", - "execution_timeout": "#/components/schemas/ExecutionTimeoutEvent", - "notification_all_read": "#/components/schemas/NotificationAllReadEvent", - "notification_clicked": "#/components/schemas/NotificationClickedEvent", - "notification_created": "#/components/schemas/NotificationCreatedEvent", - "notification_delivered": "#/components/schemas/NotificationDeliveredEvent", - "notification_failed": "#/components/schemas/NotificationFailedEvent", - "notification_preferences_updated": "#/components/schemas/NotificationPreferencesUpdatedEvent", - "notification_read": "#/components/schemas/NotificationReadEvent", - "notification_sent": "#/components/schemas/NotificationSentEvent", - "pod_created": "#/components/schemas/PodCreatedEvent", - "pod_deleted": "#/components/schemas/PodDeletedEvent", - "pod_failed": "#/components/schemas/PodFailedEvent", - "pod_running": "#/components/schemas/PodRunningEvent", - "pod_scheduled": "#/components/schemas/PodScheduledEvent", - "pod_succeeded": "#/components/schemas/PodSucceededEvent", - "pod_terminated": "#/components/schemas/PodTerminatedEvent", - "quota_exceeded": "#/components/schemas/QuotaExceededEvent", - "rate_limit_exceeded": "#/components/schemas/RateLimitExceededEvent", - "release_resources_command": "#/components/schemas/ReleaseResourcesCommandEvent", - "resource_limit_exceeded": "#/components/schemas/ResourceLimitExceededEvent", - "result_failed": "#/components/schemas/ResultFailedEvent", - "result_stored": "#/components/schemas/ResultStoredEvent", - "saga_cancelled": "#/components/schemas/SagaCancelledEvent", - "saga_compensated": "#/components/schemas/SagaCompensatedEvent", - "saga_compensating": "#/components/schemas/SagaCompensatingEvent", - "saga_completed": "#/components/schemas/SagaCompletedEvent", - "saga_failed": "#/components/schemas/SagaFailedEvent", - "saga_started": "#/components/schemas/SagaStartedEvent", - "script_deleted": "#/components/schemas/ScriptDeletedEvent", - "script_saved": "#/components/schemas/ScriptSavedEvent", - "script_shared": "#/components/schemas/ScriptSharedEvent", - "security_violation": "#/components/schemas/SecurityViolationEvent", - "service_recovered": "#/components/schemas/ServiceRecoveredEvent", - "service_unhealthy": "#/components/schemas/ServiceUnhealthyEvent", - "system_error": "#/components/schemas/SystemErrorEvent", - "user_deleted": "#/components/schemas/UserDeletedEvent", - "user_logged_in": "#/components/schemas/UserLoggedInEvent", - "user_logged_out": "#/components/schemas/UserLoggedOutEvent", - "user_login": "#/components/schemas/UserLoginEvent", - "user_registered": "#/components/schemas/UserRegisteredEvent", - "user_settings_updated": "#/components/schemas/UserSettingsUpdatedEvent", - "user_updated": "#/components/schemas/UserUpdatedEvent" - } - } - }, - "type": "array", - "title": "Recent Events", - "default": [] - } - }, - "type": "object", - "required": [ - "user", - "stats", - "derived_counts", - "rate_limit_summary", - "recent_events" - ], - "title": "AdminUserOverview" - }, - "AllocateResourcesCommandEvent": { - "properties": { - "event_id": { - "type": "string", - "title": "Event Id" - }, - "event_type": { - "type": "string", - "const": "allocate_resources_command", - "title": "Event Type", - "default": "allocate_resources_command" - }, - "event_version": { - "type": "string", - "title": "Event Version", - "default": "1.0" - }, - "timestamp": { - "type": "string", - "format": "date-time", - "title": "Timestamp" - }, - "aggregate_id": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Aggregate Id" - }, - "metadata": { - "$ref": "#/components/schemas/EventMetadata" - }, - "execution_id": { - "type": "string", - "title": "Execution Id" - }, - "cpu_request": { - "type": "string", - "title": "Cpu Request" - }, - "memory_request": { - "type": "string", - "title": "Memory Request" - } - }, - "type": "object", - "required": [ - "event_id", - "event_type", - "event_version", - "timestamp", - "metadata" - ], - "title": "AllocateResourcesCommandEvent" - }, - "AuthFailedEvent": { - "properties": { - "event_id": { - "type": "string", - "title": "Event Id" - }, - "event_type": { - "type": "string", - "const": "auth_failed", - "title": "Event Type", - "default": "auth_failed" - }, - "event_version": { - "type": "string", - "title": "Event Version", - "default": "1.0" - }, - "timestamp": { - "type": "string", - "format": "date-time", - "title": "Timestamp" - }, - "aggregate_id": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Aggregate Id" - }, - "metadata": { - "$ref": "#/components/schemas/EventMetadata" - }, - "username": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Username" - }, - "reason": { - "type": "string", - "title": "Reason" - }, - "ip_address": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Ip Address" - } - }, - "type": "object", - "required": [ - "event_id", - "event_type", - "event_version", - "timestamp", - "metadata" - ], - "title": "AuthFailedEvent" - }, - "Body_login_api_v1_auth_login_post": { - "properties": { - "grant_type": { - "anyOf": [ - { - "type": "string", - "pattern": "^password$" - }, - { - "type": "null" - } - ], - "title": "Grant Type" - }, - "username": { - "type": "string", - "title": "Username" - }, - "password": { - "type": "string", - "format": "password", - "title": "Password" - }, - "scope": { - "type": "string", - "title": "Scope", - "default": "" - }, - "client_id": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Client Id" - }, - "client_secret": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "format": "password", - "title": "Client Secret" - } - }, - "type": "object", - "required": [ - "username", - "password" - ], - "title": "Body_login_api_v1_auth_login_post" - }, - "CancelExecutionRequest": { - "properties": { - "reason": { - "type": "string", - "title": "Reason", - "description": "Reason for cancellation", - "default": "User requested cancellation" - } - }, - "type": "object", - "title": "CancelExecutionRequest", - "description": "Model for cancelling an execution." - }, - "CancelResponse": { - "properties": { - "execution_id": { - "type": "string", - "title": "Execution Id" - }, - "status": { - "$ref": "#/components/schemas/CancelStatus" - }, - "message": { - "type": "string", - "title": "Message" - }, - "event_id": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Event Id", - "description": "Event ID for the cancellation event, if published" - } - }, - "type": "object", - "required": [ - "execution_id", - "status", - "message", - "event_id" - ], - "title": "CancelResponse", - "description": "Model for execution cancellation response." - }, - "CancelStatus": { - "type": "string", - "enum": [ - "already_cancelled", - "cancellation_requested" - ], - "title": "CancelStatus", - "description": "Outcome of a cancel request." - }, - "CleanupResponse": { - "properties": { - "removed_sessions": { - "type": "integer", - "title": "Removed Sessions" - }, - "message": { - "type": "string", - "title": "Message" - } - }, - "type": "object", - "required": [ - "removed_sessions", - "message" - ], - "title": "CleanupResponse", - "description": "Response schema for cleanup operations" - }, - "ContainerStatusInfo": { - "properties": { - "name": { - "type": "string", - "title": "Name" - }, - "ready": { - "type": "boolean", - "title": "Ready", - "default": false - }, - "restart_count": { - "type": "integer", - "title": "Restart Count", - "default": 0 - }, - "state": { - "type": "string", - "title": "State", - "default": "unknown" - } - }, - "type": "object", - "required": [ - "name" - ], - "title": "ContainerStatusInfo", - "description": "Container status information from Kubernetes pod." - }, - "CreatePodCommandEvent": { - "properties": { - "event_id": { - "type": "string", - "title": "Event Id" - }, - "event_type": { - "type": "string", - "const": "create_pod_command", - "title": "Event Type", - "default": "create_pod_command" - }, - "event_version": { - "type": "string", - "title": "Event Version", - "default": "1.0" - }, - "timestamp": { - "type": "string", - "format": "date-time", - "title": "Timestamp" - }, - "aggregate_id": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Aggregate Id" - }, - "metadata": { - "$ref": "#/components/schemas/EventMetadata" - }, - "saga_id": { - "type": "string", - "title": "Saga Id" - }, - "execution_id": { - "type": "string", - "title": "Execution Id" - }, - "script": { - "type": "string", - "title": "Script" - }, - "language": { - "type": "string", - "title": "Language" - }, - "language_version": { - "type": "string", - "title": "Language Version" - }, - "runtime_image": { - "type": "string", - "title": "Runtime Image" - }, - "runtime_command": { - "items": { - "type": "string" - }, - "type": "array", - "title": "Runtime Command" - }, - "runtime_filename": { - "type": "string", - "title": "Runtime Filename" - }, - "timeout_seconds": { - "type": "integer", - "title": "Timeout Seconds" - }, - "cpu_limit": { - "type": "string", - "title": "Cpu Limit" - }, - "memory_limit": { - "type": "string", - "title": "Memory Limit" - }, - "cpu_request": { - "type": "string", - "title": "Cpu Request" - }, - "memory_request": { - "type": "string", - "title": "Memory Request" - }, - "priority": { - "$ref": "#/components/schemas/QueuePriority", - "default": "normal" - } - }, - "type": "object", - "required": [ - "event_id", - "event_type", - "event_version", - "timestamp", - "metadata" - ], - "title": "CreatePodCommandEvent" - }, - "DLQMessageDiscardedEvent": { - "properties": { - "event_id": { - "type": "string", - "title": "Event Id" - }, - "event_type": { - "type": "string", - "const": "dlq_message_discarded", - "title": "Event Type", - "default": "dlq_message_discarded" - }, - "event_version": { - "type": "string", - "title": "Event Version", - "default": "1.0" - }, - "timestamp": { - "type": "string", - "format": "date-time", - "title": "Timestamp" - }, - "aggregate_id": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Aggregate Id" - }, - "metadata": { - "$ref": "#/components/schemas/EventMetadata" - }, - "dlq_event_id": { - "type": "string", - "title": "Dlq Event Id" - }, - "original_topic": { - "type": "string", - "title": "Original Topic" - }, - "original_event_type": { - "$ref": "#/components/schemas/EventType" - }, - "reason": { - "type": "string", - "title": "Reason" - }, - "retry_count": { - "type": "integer", - "title": "Retry Count" - } - }, - "type": "object", - "required": [ - "event_id", - "event_type", - "event_version", - "timestamp", - "metadata" - ], - "title": "DLQMessageDiscardedEvent", - "description": "Emitted when a DLQ message is discarded (max retries exceeded or manual discard)." - }, - "DLQMessageReceivedEvent": { - "properties": { - "event_id": { - "type": "string", - "title": "Event Id" - }, - "event_type": { - "type": "string", - "const": "dlq_message_received", - "title": "Event Type", - "default": "dlq_message_received" - }, - "event_version": { - "type": "string", - "title": "Event Version", - "default": "1.0" - }, - "timestamp": { - "type": "string", - "format": "date-time", - "title": "Timestamp" - }, - "aggregate_id": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Aggregate Id" - }, - "metadata": { - "$ref": "#/components/schemas/EventMetadata" - }, - "dlq_event_id": { - "type": "string", - "title": "Dlq Event Id" - }, - "original_topic": { - "type": "string", - "title": "Original Topic" - }, - "original_event_type": { - "$ref": "#/components/schemas/EventType" - }, - "error": { - "type": "string", - "title": "Error" - }, - "retry_count": { - "type": "integer", - "title": "Retry Count" - }, - "producer_id": { - "type": "string", - "title": "Producer Id" - }, - "failed_at": { - "type": "string", - "format": "date-time", - "title": "Failed At" - } - }, - "type": "object", - "required": [ - "event_id", - "event_type", - "event_version", - "timestamp", - "metadata" - ], - "title": "DLQMessageReceivedEvent", - "description": "Emitted when a message is received and persisted in the DLQ." - }, - "DLQMessageRetriedEvent": { - "properties": { - "event_id": { - "type": "string", - "title": "Event Id" - }, - "event_type": { - "type": "string", - "const": "dlq_message_retried", - "title": "Event Type", - "default": "dlq_message_retried" - }, - "event_version": { - "type": "string", - "title": "Event Version", - "default": "1.0" - }, - "timestamp": { - "type": "string", - "format": "date-time", - "title": "Timestamp" - }, - "aggregate_id": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Aggregate Id" - }, - "metadata": { - "$ref": "#/components/schemas/EventMetadata" - }, - "dlq_event_id": { - "type": "string", - "title": "Dlq Event Id" - }, - "original_topic": { - "type": "string", - "title": "Original Topic" - }, - "original_event_type": { - "$ref": "#/components/schemas/EventType" - }, - "retry_count": { - "type": "integer", - "title": "Retry Count" - }, - "retry_topic": { - "type": "string", - "title": "Retry Topic" - } - }, - "type": "object", - "required": [ - "event_id", - "event_type", - "event_version", - "timestamp", - "metadata" - ], - "title": "DLQMessageRetriedEvent", - "description": "Emitted when a DLQ message is retried." - }, - "DeleteNotificationResponse": { - "properties": { - "message": { - "type": "string", - "title": "Message" - } - }, - "type": "object", - "required": [ - "message" - ], - "title": "DeleteNotificationResponse", - "description": "Response schema for notification deletion" - }, - "DeletePodCommandEvent": { - "properties": { - "event_id": { - "type": "string", - "title": "Event Id" - }, - "event_type": { - "type": "string", - "const": "delete_pod_command", - "title": "Event Type", - "default": "delete_pod_command" - }, - "event_version": { - "type": "string", - "title": "Event Version", - "default": "1.0" - }, - "timestamp": { - "type": "string", - "format": "date-time", - "title": "Timestamp" - }, - "aggregate_id": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Aggregate Id" - }, - "metadata": { - "$ref": "#/components/schemas/EventMetadata" - }, - "saga_id": { - "type": "string", - "title": "Saga Id" - }, - "execution_id": { - "type": "string", - "title": "Execution Id" - }, - "reason": { - "type": "string", - "title": "Reason" - }, - "pod_name": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Pod Name" - }, - "namespace": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Namespace" - } - }, - "type": "object", - "required": [ - "event_id", - "event_type", - "event_version", - "timestamp", - "metadata" - ], - "title": "DeletePodCommandEvent" - }, - "DeleteResponse": { - "properties": { - "message": { - "type": "string", - "title": "Message" - }, - "execution_id": { - "type": "string", - "title": "Execution Id" - } - }, - "type": "object", - "required": [ - "message", - "execution_id" - ], - "title": "DeleteResponse", - "description": "Model for execution deletion response." - }, - "DeleteUserResponse": { - "properties": { - "user_deleted": { - "type": "boolean", - "title": "User Deleted" - }, - "executions": { - "type": "integer", - "title": "Executions", - "default": 0 - }, - "saved_scripts": { - "type": "integer", - "title": "Saved Scripts", - "default": 0 - }, - "notifications": { - "type": "integer", - "title": "Notifications", - "default": 0 - }, - "user_settings": { - "type": "integer", - "title": "User Settings", - "default": 0 - }, - "events": { - "type": "integer", - "title": "Events", - "default": 0 - }, - "sagas": { - "type": "integer", - "title": "Sagas", - "default": 0 - } - }, - "type": "object", - "required": [ - "user_deleted", - "executions", - "saved_scripts", - "notifications", - "user_settings", - "events", - "sagas" - ], - "title": "DeleteUserResponse", - "description": "Response model for user deletion." - }, - "DerivedCounts": { - "properties": { - "succeeded": { - "type": "integer", - "title": "Succeeded", - "default": 0 - }, - "failed": { - "type": "integer", - "title": "Failed", - "default": 0 - }, - "timeout": { - "type": "integer", - "title": "Timeout", - "default": 0 - }, - "cancelled": { - "type": "integer", - "title": "Cancelled", - "default": 0 - }, - "terminal_total": { - "type": "integer", - "title": "Terminal Total", - "default": 0 - } - }, - "type": "object", - "required": [ - "succeeded", - "failed", - "timeout", - "cancelled", - "terminal_total" - ], - "title": "DerivedCounts" - }, - "EditorSettings-Input": { - "properties": { - "font_size": { - "type": "integer", - "maximum": 32.0, - "minimum": 8.0, - "title": "Font Size", - "default": 14 - }, - "tab_size": { - "type": "integer", - "enum": [ - 2, - 4, - 8 - ], - "title": "Tab Size", - "default": 4 - }, - "use_tabs": { - "type": "boolean", - "title": "Use Tabs", - "default": false - }, - "word_wrap": { - "type": "boolean", - "title": "Word Wrap", - "default": true - }, - "show_line_numbers": { - "type": "boolean", - "title": "Show Line Numbers", - "default": true - } - }, - "type": "object", - "title": "EditorSettings", - "description": "Code editor preferences" - }, - "EditorSettings-Output": { - "properties": { - "font_size": { - "type": "integer", - "maximum": 32.0, - "minimum": 8.0, - "title": "Font Size", - "default": 14 - }, - "tab_size": { - "type": "integer", - "enum": [ - 2, - 4, - 8 - ], - "title": "Tab Size", - "default": 4 - }, - "use_tabs": { - "type": "boolean", - "title": "Use Tabs", - "default": false - }, - "word_wrap": { - "type": "boolean", - "title": "Word Wrap", - "default": true - }, - "show_line_numbers": { - "type": "boolean", - "title": "Show Line Numbers", - "default": true - } - }, - "type": "object", - "required": [ - "font_size", - "tab_size", - "use_tabs", - "word_wrap", - "show_line_numbers" - ], - "title": "EditorSettings", - "description": "Code editor preferences" - }, - "EndpointGroup": { - "type": "string", - "enum": [ - "execution", - "admin", - "sse", - "websocket", - "auth", - "public", - "api" - ], - "title": "EndpointGroup" - }, - "EndpointUsageStats": { - "properties": { - "algorithm": { - "$ref": "#/components/schemas/RateLimitAlgorithm" - }, - "remaining": { - "type": "integer", - "title": "Remaining" - } - }, - "type": "object", - "required": [ - "algorithm", - "remaining" - ], - "title": "EndpointUsageStats", - "description": "Usage statistics for a single endpoint (IETF RateLimit-style)." - }, - "Environment": { - "type": "string", - "enum": [ - "development", - "staging", - "production", - "test" - ], - "title": "Environment", - "description": "Deployment environments." - }, - "ErrorResponse": { - "properties": { - "status": { - "type": "integer", - "title": "Status", - "description": "HTTP status code" - }, - "detail": { - "type": "string", - "title": "Detail", - "description": "Human-readable error message" - } - }, - "type": "object", - "required": [ - "status", - "detail" - ], - "title": "ErrorResponse", - "description": "Every error the API returns: the `status` and `detail` members of RFC 9457 problem details." - }, - "EventBrowseRequest": { - "properties": { - "filters": { - "$ref": "#/components/schemas/EventFilter" - }, - "skip": { - "type": "integer", - "title": "Skip", - "default": 0 - }, - "limit": { - "type": "integer", - "maximum": 500.0, - "title": "Limit", - "default": 50 - } - }, - "type": "object", - "required": [ - "filters" - ], - "title": "EventBrowseRequest", - "description": "Request model for browsing events" - }, - "EventBrowseResponse": { - "properties": { - "events": { - "items": { - "oneOf": [ - { - "$ref": "#/components/schemas/ExecutionRequestedEvent" - }, - { - "$ref": "#/components/schemas/ExecutionAcceptedEvent" - }, - { - "$ref": "#/components/schemas/ExecutionQueuedEvent" - }, - { - "$ref": "#/components/schemas/ExecutionStartedEvent" - }, - { - "$ref": "#/components/schemas/ExecutionRunningEvent" - }, - { - "$ref": "#/components/schemas/ExecutionCompletedEvent" - }, - { - "$ref": "#/components/schemas/ExecutionFailedEvent" - }, - { - "$ref": "#/components/schemas/ExecutionTimeoutEvent" - }, - { - "$ref": "#/components/schemas/ExecutionCancelledEvent" - }, - { - "$ref": "#/components/schemas/PodCreatedEvent" - }, - { - "$ref": "#/components/schemas/PodScheduledEvent" - }, - { - "$ref": "#/components/schemas/PodRunningEvent" - }, - { - "$ref": "#/components/schemas/PodSucceededEvent" - }, - { - "$ref": "#/components/schemas/PodFailedEvent" - }, - { - "$ref": "#/components/schemas/PodTerminatedEvent" - }, - { - "$ref": "#/components/schemas/PodDeletedEvent" - }, - { - "$ref": "#/components/schemas/ResultStoredEvent" - }, - { - "$ref": "#/components/schemas/ResultFailedEvent" - }, - { - "$ref": "#/components/schemas/UserSettingsUpdatedEvent" - }, - { - "$ref": "#/components/schemas/UserRegisteredEvent" - }, - { - "$ref": "#/components/schemas/UserLoginEvent" - }, - { - "$ref": "#/components/schemas/UserLoggedInEvent" - }, - { - "$ref": "#/components/schemas/UserLoggedOutEvent" - }, - { - "$ref": "#/components/schemas/UserUpdatedEvent" - }, - { - "$ref": "#/components/schemas/UserDeletedEvent" - }, - { - "$ref": "#/components/schemas/NotificationCreatedEvent" - }, - { - "$ref": "#/components/schemas/NotificationSentEvent" - }, - { - "$ref": "#/components/schemas/NotificationDeliveredEvent" - }, - { - "$ref": "#/components/schemas/NotificationFailedEvent" - }, - { - "$ref": "#/components/schemas/NotificationReadEvent" - }, - { - "$ref": "#/components/schemas/NotificationAllReadEvent" - }, - { - "$ref": "#/components/schemas/NotificationClickedEvent" - }, - { - "$ref": "#/components/schemas/NotificationPreferencesUpdatedEvent" - }, - { - "$ref": "#/components/schemas/SagaStartedEvent" - }, - { - "$ref": "#/components/schemas/SagaCompletedEvent" - }, - { - "$ref": "#/components/schemas/SagaFailedEvent" - }, - { - "$ref": "#/components/schemas/SagaCancelledEvent" - }, - { - "$ref": "#/components/schemas/SagaCompensatingEvent" - }, - { - "$ref": "#/components/schemas/SagaCompensatedEvent" - }, - { - "$ref": "#/components/schemas/CreatePodCommandEvent" - }, - { - "$ref": "#/components/schemas/DeletePodCommandEvent" - }, - { - "$ref": "#/components/schemas/AllocateResourcesCommandEvent" - }, - { - "$ref": "#/components/schemas/ReleaseResourcesCommandEvent" - }, - { - "$ref": "#/components/schemas/ScriptSavedEvent" - }, - { - "$ref": "#/components/schemas/ScriptDeletedEvent" - }, - { - "$ref": "#/components/schemas/ScriptSharedEvent" - }, - { - "$ref": "#/components/schemas/SecurityViolationEvent" - }, - { - "$ref": "#/components/schemas/RateLimitExceededEvent" - }, - { - "$ref": "#/components/schemas/AuthFailedEvent" - }, - { - "$ref": "#/components/schemas/ResourceLimitExceededEvent" - }, - { - "$ref": "#/components/schemas/QuotaExceededEvent" - }, - { - "$ref": "#/components/schemas/SystemErrorEvent" - }, - { - "$ref": "#/components/schemas/ServiceUnhealthyEvent" - }, - { - "$ref": "#/components/schemas/ServiceRecoveredEvent" - }, - { - "$ref": "#/components/schemas/DLQMessageReceivedEvent" - }, - { - "$ref": "#/components/schemas/DLQMessageRetriedEvent" - }, - { - "$ref": "#/components/schemas/DLQMessageDiscardedEvent" - } - ], - "discriminator": { - "propertyName": "event_type", - "mapping": { - "allocate_resources_command": "#/components/schemas/AllocateResourcesCommandEvent", - "auth_failed": "#/components/schemas/AuthFailedEvent", - "create_pod_command": "#/components/schemas/CreatePodCommandEvent", - "delete_pod_command": "#/components/schemas/DeletePodCommandEvent", - "dlq_message_discarded": "#/components/schemas/DLQMessageDiscardedEvent", - "dlq_message_received": "#/components/schemas/DLQMessageReceivedEvent", - "dlq_message_retried": "#/components/schemas/DLQMessageRetriedEvent", - "execution_accepted": "#/components/schemas/ExecutionAcceptedEvent", - "execution_cancelled": "#/components/schemas/ExecutionCancelledEvent", - "execution_completed": "#/components/schemas/ExecutionCompletedEvent", - "execution_failed": "#/components/schemas/ExecutionFailedEvent", - "execution_queued": "#/components/schemas/ExecutionQueuedEvent", - "execution_requested": "#/components/schemas/ExecutionRequestedEvent", - "execution_running": "#/components/schemas/ExecutionRunningEvent", - "execution_started": "#/components/schemas/ExecutionStartedEvent", - "execution_timeout": "#/components/schemas/ExecutionTimeoutEvent", - "notification_all_read": "#/components/schemas/NotificationAllReadEvent", - "notification_clicked": "#/components/schemas/NotificationClickedEvent", - "notification_created": "#/components/schemas/NotificationCreatedEvent", - "notification_delivered": "#/components/schemas/NotificationDeliveredEvent", - "notification_failed": "#/components/schemas/NotificationFailedEvent", - "notification_preferences_updated": "#/components/schemas/NotificationPreferencesUpdatedEvent", - "notification_read": "#/components/schemas/NotificationReadEvent", - "notification_sent": "#/components/schemas/NotificationSentEvent", - "pod_created": "#/components/schemas/PodCreatedEvent", - "pod_deleted": "#/components/schemas/PodDeletedEvent", - "pod_failed": "#/components/schemas/PodFailedEvent", - "pod_running": "#/components/schemas/PodRunningEvent", - "pod_scheduled": "#/components/schemas/PodScheduledEvent", - "pod_succeeded": "#/components/schemas/PodSucceededEvent", - "pod_terminated": "#/components/schemas/PodTerminatedEvent", - "quota_exceeded": "#/components/schemas/QuotaExceededEvent", - "rate_limit_exceeded": "#/components/schemas/RateLimitExceededEvent", - "release_resources_command": "#/components/schemas/ReleaseResourcesCommandEvent", - "resource_limit_exceeded": "#/components/schemas/ResourceLimitExceededEvent", - "result_failed": "#/components/schemas/ResultFailedEvent", - "result_stored": "#/components/schemas/ResultStoredEvent", - "saga_cancelled": "#/components/schemas/SagaCancelledEvent", - "saga_compensated": "#/components/schemas/SagaCompensatedEvent", - "saga_compensating": "#/components/schemas/SagaCompensatingEvent", - "saga_completed": "#/components/schemas/SagaCompletedEvent", - "saga_failed": "#/components/schemas/SagaFailedEvent", - "saga_started": "#/components/schemas/SagaStartedEvent", - "script_deleted": "#/components/schemas/ScriptDeletedEvent", - "script_saved": "#/components/schemas/ScriptSavedEvent", - "script_shared": "#/components/schemas/ScriptSharedEvent", - "security_violation": "#/components/schemas/SecurityViolationEvent", - "service_recovered": "#/components/schemas/ServiceRecoveredEvent", - "service_unhealthy": "#/components/schemas/ServiceUnhealthyEvent", - "system_error": "#/components/schemas/SystemErrorEvent", - "user_deleted": "#/components/schemas/UserDeletedEvent", - "user_logged_in": "#/components/schemas/UserLoggedInEvent", - "user_logged_out": "#/components/schemas/UserLoggedOutEvent", - "user_login": "#/components/schemas/UserLoginEvent", - "user_registered": "#/components/schemas/UserRegisteredEvent", - "user_settings_updated": "#/components/schemas/UserSettingsUpdatedEvent", - "user_updated": "#/components/schemas/UserUpdatedEvent" - } - } - }, - "type": "array", - "title": "Events" - }, - "total": { - "type": "integer", - "title": "Total" - }, - "skip": { - "type": "integer", - "title": "Skip" - }, - "limit": { - "type": "integer", - "title": "Limit" - } - }, - "type": "object", - "required": [ - "events", - "total", - "skip", - "limit" - ], - "title": "EventBrowseResponse", - "description": "Response model for browsing events" - }, - "EventDeleteResponse": { - "properties": { - "message": { - "type": "string", - "title": "Message" - }, - "event_id": { - "type": "string", - "title": "Event Id" - } - }, - "type": "object", - "required": [ - "message", - "event_id" - ], - "title": "EventDeleteResponse", - "description": "Response model for event deletion" - }, - "EventDetailResponse": { - "properties": { - "event": { - "oneOf": [ - { - "$ref": "#/components/schemas/ExecutionRequestedEvent" - }, - { - "$ref": "#/components/schemas/ExecutionAcceptedEvent" - }, - { - "$ref": "#/components/schemas/ExecutionQueuedEvent" - }, - { - "$ref": "#/components/schemas/ExecutionStartedEvent" - }, - { - "$ref": "#/components/schemas/ExecutionRunningEvent" - }, - { - "$ref": "#/components/schemas/ExecutionCompletedEvent" - }, - { - "$ref": "#/components/schemas/ExecutionFailedEvent" - }, - { - "$ref": "#/components/schemas/ExecutionTimeoutEvent" - }, - { - "$ref": "#/components/schemas/ExecutionCancelledEvent" - }, - { - "$ref": "#/components/schemas/PodCreatedEvent" - }, - { - "$ref": "#/components/schemas/PodScheduledEvent" - }, - { - "$ref": "#/components/schemas/PodRunningEvent" - }, - { - "$ref": "#/components/schemas/PodSucceededEvent" - }, - { - "$ref": "#/components/schemas/PodFailedEvent" - }, - { - "$ref": "#/components/schemas/PodTerminatedEvent" - }, - { - "$ref": "#/components/schemas/PodDeletedEvent" - }, - { - "$ref": "#/components/schemas/ResultStoredEvent" - }, - { - "$ref": "#/components/schemas/ResultFailedEvent" - }, - { - "$ref": "#/components/schemas/UserSettingsUpdatedEvent" - }, - { - "$ref": "#/components/schemas/UserRegisteredEvent" - }, - { - "$ref": "#/components/schemas/UserLoginEvent" - }, - { - "$ref": "#/components/schemas/UserLoggedInEvent" - }, - { - "$ref": "#/components/schemas/UserLoggedOutEvent" - }, - { - "$ref": "#/components/schemas/UserUpdatedEvent" - }, - { - "$ref": "#/components/schemas/UserDeletedEvent" - }, - { - "$ref": "#/components/schemas/NotificationCreatedEvent" - }, - { - "$ref": "#/components/schemas/NotificationSentEvent" - }, - { - "$ref": "#/components/schemas/NotificationDeliveredEvent" - }, - { - "$ref": "#/components/schemas/NotificationFailedEvent" - }, - { - "$ref": "#/components/schemas/NotificationReadEvent" - }, - { - "$ref": "#/components/schemas/NotificationAllReadEvent" - }, - { - "$ref": "#/components/schemas/NotificationClickedEvent" - }, - { - "$ref": "#/components/schemas/NotificationPreferencesUpdatedEvent" - }, - { - "$ref": "#/components/schemas/SagaStartedEvent" - }, - { - "$ref": "#/components/schemas/SagaCompletedEvent" - }, - { - "$ref": "#/components/schemas/SagaFailedEvent" - }, - { - "$ref": "#/components/schemas/SagaCancelledEvent" - }, - { - "$ref": "#/components/schemas/SagaCompensatingEvent" - }, - { - "$ref": "#/components/schemas/SagaCompensatedEvent" - }, - { - "$ref": "#/components/schemas/CreatePodCommandEvent" - }, - { - "$ref": "#/components/schemas/DeletePodCommandEvent" - }, - { - "$ref": "#/components/schemas/AllocateResourcesCommandEvent" - }, - { - "$ref": "#/components/schemas/ReleaseResourcesCommandEvent" - }, - { - "$ref": "#/components/schemas/ScriptSavedEvent" - }, - { - "$ref": "#/components/schemas/ScriptDeletedEvent" - }, - { - "$ref": "#/components/schemas/ScriptSharedEvent" - }, - { - "$ref": "#/components/schemas/SecurityViolationEvent" - }, - { - "$ref": "#/components/schemas/RateLimitExceededEvent" - }, - { - "$ref": "#/components/schemas/AuthFailedEvent" - }, - { - "$ref": "#/components/schemas/ResourceLimitExceededEvent" - }, - { - "$ref": "#/components/schemas/QuotaExceededEvent" - }, - { - "$ref": "#/components/schemas/SystemErrorEvent" - }, - { - "$ref": "#/components/schemas/ServiceUnhealthyEvent" - }, - { - "$ref": "#/components/schemas/ServiceRecoveredEvent" - }, - { - "$ref": "#/components/schemas/DLQMessageReceivedEvent" - }, + "client_secret": { + "anyOf": [ { - "$ref": "#/components/schemas/DLQMessageRetriedEvent" + "type": "string" }, { - "$ref": "#/components/schemas/DLQMessageDiscardedEvent" + "type": "null" } ], - "title": "Event", - "discriminator": { - "propertyName": "event_type", - "mapping": { - "allocate_resources_command": "#/components/schemas/AllocateResourcesCommandEvent", - "auth_failed": "#/components/schemas/AuthFailedEvent", - "create_pod_command": "#/components/schemas/CreatePodCommandEvent", - "delete_pod_command": "#/components/schemas/DeletePodCommandEvent", - "dlq_message_discarded": "#/components/schemas/DLQMessageDiscardedEvent", - "dlq_message_received": "#/components/schemas/DLQMessageReceivedEvent", - "dlq_message_retried": "#/components/schemas/DLQMessageRetriedEvent", - "execution_accepted": "#/components/schemas/ExecutionAcceptedEvent", - "execution_cancelled": "#/components/schemas/ExecutionCancelledEvent", - "execution_completed": "#/components/schemas/ExecutionCompletedEvent", - "execution_failed": "#/components/schemas/ExecutionFailedEvent", - "execution_queued": "#/components/schemas/ExecutionQueuedEvent", - "execution_requested": "#/components/schemas/ExecutionRequestedEvent", - "execution_running": "#/components/schemas/ExecutionRunningEvent", - "execution_started": "#/components/schemas/ExecutionStartedEvent", - "execution_timeout": "#/components/schemas/ExecutionTimeoutEvent", - "notification_all_read": "#/components/schemas/NotificationAllReadEvent", - "notification_clicked": "#/components/schemas/NotificationClickedEvent", - "notification_created": "#/components/schemas/NotificationCreatedEvent", - "notification_delivered": "#/components/schemas/NotificationDeliveredEvent", - "notification_failed": "#/components/schemas/NotificationFailedEvent", - "notification_preferences_updated": "#/components/schemas/NotificationPreferencesUpdatedEvent", - "notification_read": "#/components/schemas/NotificationReadEvent", - "notification_sent": "#/components/schemas/NotificationSentEvent", - "pod_created": "#/components/schemas/PodCreatedEvent", - "pod_deleted": "#/components/schemas/PodDeletedEvent", - "pod_failed": "#/components/schemas/PodFailedEvent", - "pod_running": "#/components/schemas/PodRunningEvent", - "pod_scheduled": "#/components/schemas/PodScheduledEvent", - "pod_succeeded": "#/components/schemas/PodSucceededEvent", - "pod_terminated": "#/components/schemas/PodTerminatedEvent", - "quota_exceeded": "#/components/schemas/QuotaExceededEvent", - "rate_limit_exceeded": "#/components/schemas/RateLimitExceededEvent", - "release_resources_command": "#/components/schemas/ReleaseResourcesCommandEvent", - "resource_limit_exceeded": "#/components/schemas/ResourceLimitExceededEvent", - "result_failed": "#/components/schemas/ResultFailedEvent", - "result_stored": "#/components/schemas/ResultStoredEvent", - "saga_cancelled": "#/components/schemas/SagaCancelledEvent", - "saga_compensated": "#/components/schemas/SagaCompensatedEvent", - "saga_compensating": "#/components/schemas/SagaCompensatingEvent", - "saga_completed": "#/components/schemas/SagaCompletedEvent", - "saga_failed": "#/components/schemas/SagaFailedEvent", - "saga_started": "#/components/schemas/SagaStartedEvent", - "script_deleted": "#/components/schemas/ScriptDeletedEvent", - "script_saved": "#/components/schemas/ScriptSavedEvent", - "script_shared": "#/components/schemas/ScriptSharedEvent", - "security_violation": "#/components/schemas/SecurityViolationEvent", - "service_recovered": "#/components/schemas/ServiceRecoveredEvent", - "service_unhealthy": "#/components/schemas/ServiceUnhealthyEvent", - "system_error": "#/components/schemas/SystemErrorEvent", - "user_deleted": "#/components/schemas/UserDeletedEvent", - "user_logged_in": "#/components/schemas/UserLoggedInEvent", - "user_logged_out": "#/components/schemas/UserLoggedOutEvent", - "user_login": "#/components/schemas/UserLoginEvent", - "user_registered": "#/components/schemas/UserRegisteredEvent", - "user_settings_updated": "#/components/schemas/UserSettingsUpdatedEvent", - "user_updated": "#/components/schemas/UserUpdatedEvent" - } - } - }, - "related_events": { - "items": { - "$ref": "#/components/schemas/EventSummary" - }, - "type": "array", - "title": "Related Events" - }, - "timeline": { - "items": { - "$ref": "#/components/schemas/EventSummary" - }, - "type": "array", - "title": "Timeline" + "format": "password", + "title": "Client Secret" } }, "type": "object", "required": [ - "event", - "related_events", - "timeline" + "username", + "password" ], - "title": "EventDetailResponse", - "description": "Response model for event detail" + "title": "Body_login_api_v1_auth_login_post" }, - "EventFilter": { + "CancelExecutionRequest": { "properties": { - "event_types": { - "anyOf": [ - { - "items": { - "$ref": "#/components/schemas/EventType" - }, - "type": "array" - }, - { - "type": "null" - } - ], - "title": "Event Types" - }, - "aggregate_id": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Aggregate Id" - }, - "user_id": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "User Id" + "reason": { + "type": "string", + "title": "Reason", + "description": "Reason for cancellation", + "default": "User requested cancellation" + } + }, + "type": "object", + "title": "CancelExecutionRequest", + "description": "Model for cancelling an execution." + }, + "CancelResponse": { + "properties": { + "execution_id": { + "type": "string", + "title": "Execution Id" }, - "start_time": { - "anyOf": [ - { - "type": "string", - "format": "date-time" - }, - { - "type": "null" - } - ], - "title": "Start Time" + "status": { + "$ref": "#/components/schemas/CancelStatus" }, - "end_time": { - "anyOf": [ - { - "type": "string", - "format": "date-time" - }, - { - "type": "null" - } - ], - "title": "End Time" + "message": { + "type": "string", + "title": "Message" }, - "service_name": { + "event_id": { "anyOf": [ { "type": "string" @@ -6529,67 +4667,98 @@ "type": "null" } ], - "title": "Service Name" + "title": "Event Id", + "description": "Event ID for the cancellation event, if published" + } + }, + "type": "object", + "required": [ + "execution_id", + "status", + "message", + "event_id" + ], + "title": "CancelResponse", + "description": "Model for execution cancellation response." + }, + "CancelStatus": { + "type": "string", + "enum": [ + "already_cancelled", + "cancellation_requested" + ], + "title": "CancelStatus", + "description": "Outcome of a cancel request." + }, + "CleanupResponse": { + "properties": { + "removed_sessions": { + "type": "integer", + "title": "Removed Sessions" }, - "search_text": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Search Text" + "message": { + "type": "string", + "title": "Message" } }, "type": "object", - "title": "EventFilter", - "description": "Filter criteria for browsing events" + "required": [ + "removed_sessions", + "message" + ], + "title": "CleanupResponse", + "description": "Response schema for cleanup operations" }, - "EventMetadata": { + "ContainerStatusInfo": { "properties": { - "service_name": { + "name": { "type": "string", - "title": "Service Name" + "title": "Name" }, - "service_version": { - "type": "string", - "title": "Service Version" + "ready": { + "type": "boolean", + "title": "Ready", + "default": false }, - "user_id": { - "type": "string", - "title": "User Id" + "restart_count": { + "type": "integer", + "title": "Restart Count", + "default": 0 }, - "environment": { - "$ref": "#/components/schemas/Environment", - "default": "production" + "state": { + "type": "string", + "title": "State", + "default": "unknown" } }, "type": "object", "required": [ - "service_name", - "service_version", - "user_id" + "name" ], - "title": "EventMetadata", - "description": "Event metadata - embedded in all events." + "title": "ContainerStatusInfo", + "description": "Container status information from Kubernetes pod." }, - "EventReplayRequest": { + "DLQMessageDiscardedEvent": { "properties": { - "event_ids": { - "anyOf": [ - { - "items": { - "type": "string" - }, - "type": "array" - }, - { - "type": "null" - } - ], - "title": "Event Ids" + "event_id": { + "type": "string", + "title": "Event Id" + }, + "event_type": { + "type": "string", + "const": "dlq_message_discarded", + "title": "Event Type", + "default": "dlq_message_discarded" + }, + "event_version": { + "type": "string", + "title": "Event Version", + "default": "1.0" + }, + "timestamp": { + "type": "string", + "format": "date-time", + "title": "Timestamp" }, "aggregate_id": { "anyOf": [ @@ -6602,31 +4771,63 @@ ], "title": "Aggregate Id" }, - "start_time": { - "anyOf": [ - { - "type": "string", - "format": "date-time" - }, - { - "type": "null" - } - ], - "title": "Start Time" + "metadata": { + "$ref": "#/components/schemas/EventMetadata" }, - "end_time": { - "anyOf": [ - { - "type": "string", - "format": "date-time" - }, - { - "type": "null" - } - ], - "title": "End Time" + "dlq_event_id": { + "type": "string", + "title": "Dlq Event Id" }, - "target_service": { + "original_topic": { + "type": "string", + "title": "Original Topic" + }, + "original_event_type": { + "$ref": "#/components/schemas/EventType" + }, + "reason": { + "type": "string", + "title": "Reason" + }, + "retry_count": { + "type": "integer", + "title": "Retry Count" + } + }, + "type": "object", + "required": [ + "event_id", + "event_type", + "event_version", + "timestamp", + "metadata" + ], + "title": "DLQMessageDiscardedEvent", + "description": "Emitted when a DLQ message is discarded (max retries exceeded or manual discard)." + }, + "DLQMessageReceivedEvent": { + "properties": { + "event_id": { + "type": "string", + "title": "Event Id" + }, + "event_type": { + "type": "string", + "const": "dlq_message_received", + "title": "Event Type", + "default": "dlq_message_received" + }, + "event_version": { + "type": "string", + "title": "Event Version", + "default": "1.0" + }, + "timestamp": { + "type": "string", + "format": "date-time", + "title": "Timestamp" + }, + "aggregate_id": { "anyOf": [ { "type": "string" @@ -6635,33 +4836,74 @@ "type": "null" } ], - "title": "Target Service" + "title": "Aggregate Id" }, - "dry_run": { - "type": "boolean", - "title": "Dry Run", - "default": true + "metadata": { + "$ref": "#/components/schemas/EventMetadata" + }, + "dlq_event_id": { + "type": "string", + "title": "Dlq Event Id" + }, + "original_topic": { + "type": "string", + "title": "Original Topic" + }, + "original_event_type": { + "$ref": "#/components/schemas/EventType" + }, + "error": { + "type": "string", + "title": "Error" + }, + "retry_count": { + "type": "integer", + "title": "Retry Count" + }, + "producer_id": { + "type": "string", + "title": "Producer Id" + }, + "failed_at": { + "type": "string", + "format": "date-time", + "title": "Failed At" } }, "type": "object", - "title": "EventReplayRequest", - "description": "Request model for replaying events" + "required": [ + "event_id", + "event_type", + "event_version", + "timestamp", + "metadata" + ], + "title": "DLQMessageReceivedEvent", + "description": "Emitted when a message is received and persisted in the DLQ." }, - "EventReplayResponse": { + "DLQMessageRetriedEvent": { "properties": { - "dry_run": { - "type": "boolean", - "title": "Dry Run" + "event_id": { + "type": "string", + "title": "Event Id" }, - "total_events": { - "type": "integer", - "title": "Total Events" + "event_type": { + "type": "string", + "const": "dlq_message_retried", + "title": "Event Type", + "default": "dlq_message_retried" }, - "replay_id": { + "event_version": { "type": "string", - "title": "Replay Id" + "title": "Event Version", + "default": "1.0" }, - "session_id": { + "timestamp": { + "type": "string", + "format": "date-time", + "title": "Timestamp" + }, + "aggregate_id": { "anyOf": [ { "type": "string" @@ -6670,484 +4912,813 @@ "type": "null" } ], - "title": "Session Id" + "title": "Aggregate Id" }, - "status": { - "$ref": "#/components/schemas/ReplayStatus" + "metadata": { + "$ref": "#/components/schemas/EventMetadata" }, - "events_preview": { - "anyOf": [ - { - "items": { - "$ref": "#/components/schemas/EventSummary" - }, - "type": "array" - }, - { - "type": "null" - } - ], - "title": "Events Preview" + "dlq_event_id": { + "type": "string", + "title": "Dlq Event Id" + }, + "original_topic": { + "type": "string", + "title": "Original Topic" + }, + "original_event_type": { + "$ref": "#/components/schemas/EventType" + }, + "retry_count": { + "type": "integer", + "title": "Retry Count" + }, + "retry_topic": { + "type": "string", + "title": "Retry Topic" } }, "type": "object", "required": [ - "dry_run", - "total_events", - "replay_id", - "session_id", - "status", - "events_preview" + "event_id", + "event_type", + "event_version", + "timestamp", + "metadata" ], - "title": "EventReplayResponse", - "description": "Response model for event replay" + "title": "DLQMessageRetriedEvent", + "description": "Emitted when a DLQ message is retried." }, - "EventReplayStatusResponse": { + "DeleteNotificationResponse": { "properties": { - "session_id": { + "message": { "type": "string", - "title": "Session Id" + "title": "Message" + } + }, + "type": "object", + "required": [ + "message" + ], + "title": "DeleteNotificationResponse", + "description": "Response schema for notification deletion" + }, + "DeleteResponse": { + "properties": { + "message": { + "type": "string", + "title": "Message" }, - "status": { - "$ref": "#/components/schemas/ReplayStatus" + "execution_id": { + "type": "string", + "title": "Execution Id" + } + }, + "type": "object", + "required": [ + "message", + "execution_id" + ], + "title": "DeleteResponse", + "description": "Model for execution deletion response." + }, + "DeleteUserResponse": { + "properties": { + "user_deleted": { + "type": "boolean", + "title": "User Deleted" }, - "total_events": { + "executions": { "type": "integer", - "title": "Total Events" + "title": "Executions", + "default": 0 }, - "replayed_events": { + "saved_scripts": { "type": "integer", - "title": "Replayed Events" + "title": "Saved Scripts", + "default": 0 }, - "failed_events": { + "notifications": { "type": "integer", - "title": "Failed Events" + "title": "Notifications", + "default": 0 }, - "skipped_events": { + "user_settings": { "type": "integer", - "title": "Skipped Events" - }, - "replay_id": { - "type": "string", - "title": "Replay Id" + "title": "User Settings", + "default": 0 }, - "created_at": { - "type": "string", - "format": "date-time", - "title": "Created At" + "events": { + "type": "integer", + "title": "Events", + "default": 0 + } + }, + "type": "object", + "required": [ + "user_deleted", + "executions", + "saved_scripts", + "notifications", + "user_settings", + "events" + ], + "title": "DeleteUserResponse", + "description": "Response model for user deletion." + }, + "DerivedCounts": { + "properties": { + "succeeded": { + "type": "integer", + "title": "Succeeded", + "default": 0 }, - "started_at": { - "anyOf": [ - { - "type": "string", - "format": "date-time" - }, - { - "type": "null" - } - ], - "title": "Started At" + "failed": { + "type": "integer", + "title": "Failed", + "default": 0 }, - "completed_at": { - "anyOf": [ - { - "type": "string", - "format": "date-time" - }, - { - "type": "null" - } - ], - "title": "Completed At" + "timeout": { + "type": "integer", + "title": "Timeout", + "default": 0 }, - "errors": { - "anyOf": [ - { - "items": { - "$ref": "#/components/schemas/ReplayError" - }, - "type": "array" - }, - { - "type": "null" - } - ], - "title": "Errors" + "cancelled": { + "type": "integer", + "title": "Cancelled", + "default": 0 }, - "estimated_completion": { - "anyOf": [ - { - "type": "string", - "format": "date-time" - }, - { - "type": "null" - } - ], - "title": "Estimated Completion" + "terminal_total": { + "type": "integer", + "title": "Terminal Total", + "default": 0 + } + }, + "type": "object", + "required": [ + "succeeded", + "failed", + "timeout", + "cancelled", + "terminal_total" + ], + "title": "DerivedCounts" + }, + "EditorSettings-Input": { + "properties": { + "font_size": { + "type": "integer", + "maximum": 32.0, + "minimum": 8.0, + "title": "Font Size", + "default": 14 }, - "execution_results": { - "anyOf": [ - { - "items": { - "$ref": "#/components/schemas/ExecutionResult" - }, - "type": "array" - }, - { - "type": "null" - } + "tab_size": { + "type": "integer", + "enum": [ + 2, + 4, + 8 ], - "title": "Execution Results" + "title": "Tab Size", + "default": 4 }, - "progress_percentage": { - "type": "number", - "title": "Progress Percentage", - "readOnly": true + "use_tabs": { + "type": "boolean", + "title": "Use Tabs", + "default": false + }, + "word_wrap": { + "type": "boolean", + "title": "Word Wrap", + "default": true + }, + "show_line_numbers": { + "type": "boolean", + "title": "Show Line Numbers", + "default": true } }, "type": "object", - "required": [ - "session_id", - "status", - "total_events", - "replayed_events", - "failed_events", - "skipped_events", - "replay_id", - "created_at", - "started_at", - "completed_at", - "errors", - "estimated_completion", - "execution_results", - "progress_percentage" - ], - "title": "EventReplayStatusResponse", - "description": "Response model for replay status" + "title": "EditorSettings", + "description": "Code editor preferences" }, - "EventStatistics": { + "EditorSettings-Output": { "properties": { - "total_events": { + "font_size": { "type": "integer", - "title": "Total Events" - }, - "events_by_type": { - "items": { - "$ref": "#/components/schemas/EventTypeCount" - }, - "type": "array", - "title": "Events By Type" - }, - "events_by_service": { - "items": { - "$ref": "#/components/schemas/ServiceEventCount" - }, - "type": "array", - "title": "Events By Service" - }, - "events_by_hour": { - "items": { - "$ref": "#/components/schemas/HourlyEventCount" - }, - "type": "array", - "title": "Events By Hour" + "maximum": 32.0, + "minimum": 8.0, + "title": "Font Size", + "default": 14 }, - "top_users": { - "items": { - "$ref": "#/components/schemas/UserEventCount" - }, - "type": "array", - "title": "Top Users" + "tab_size": { + "type": "integer", + "enum": [ + 2, + 4, + 8 + ], + "title": "Tab Size", + "default": 4 }, - "error_rate": { - "type": "number", - "title": "Error Rate", - "default": 0.0 + "use_tabs": { + "type": "boolean", + "title": "Use Tabs", + "default": false }, - "avg_processing_time": { - "type": "number", - "title": "Avg Processing Time", - "default": 0.0 + "word_wrap": { + "type": "boolean", + "title": "Word Wrap", + "default": true }, - "start_time": { - "anyOf": [ - { - "type": "string", - "format": "date-time" - }, - { - "type": "null" - } - ], - "title": "Start Time" + "show_line_numbers": { + "type": "boolean", + "title": "Show Line Numbers", + "default": true + } + }, + "type": "object", + "required": [ + "font_size", + "tab_size", + "use_tabs", + "word_wrap", + "show_line_numbers" + ], + "title": "EditorSettings", + "description": "Code editor preferences" + }, + "EndpointGroup": { + "type": "string", + "enum": [ + "execution", + "admin", + "sse", + "websocket", + "auth", + "public", + "api" + ], + "title": "EndpointGroup" + }, + "EndpointUsageStats": { + "properties": { + "algorithm": { + "$ref": "#/components/schemas/RateLimitAlgorithm" }, - "end_time": { - "anyOf": [ - { - "type": "string", - "format": "date-time" - }, - { - "type": "null" - } - ], - "title": "End Time" + "remaining": { + "type": "integer", + "title": "Remaining" } }, "type": "object", "required": [ - "total_events", - "events_by_type", - "events_by_service", - "events_by_hour", - "top_users", - "error_rate", - "avg_processing_time", - "start_time", - "end_time" + "algorithm", + "remaining" ], - "title": "EventStatistics", - "description": "Event statistics with API schema extras.", - "example": { - "events_by_hour": [ - { - "count": 85, - "hour": "2024-01-20 10:00" - }, - { - "count": 92, - "hour": "2024-01-20 11:00" - } - ], - "events_by_service": [ - { - "count": 523, - "service_name": "api-gateway" - }, - { - "count": 1020, - "service_name": "execution-service" - } - ], - "events_by_type": [ - { - "count": 523, - "event_type": "EXECUTION_REQUESTED" - }, - { - "count": 498, - "event_type": "EXECUTION_COMPLETED" - }, - { - "count": 522, - "event_type": "POD_CREATED" - } - ], - "total_events": 1543 - } + "title": "EndpointUsageStats", + "description": "Usage statistics for a single endpoint (IETF RateLimit-style)." }, - "EventStatsResponse": { + "Environment": { + "type": "string", + "enum": [ + "development", + "staging", + "production", + "test" + ], + "title": "Environment", + "description": "Deployment environments." + }, + "ErrorResponse": { "properties": { - "total_events": { + "status": { "type": "integer", - "title": "Total Events" + "title": "Status", + "description": "HTTP status code" + }, + "detail": { + "type": "string", + "title": "Detail", + "description": "Human-readable error message" + } + }, + "type": "object", + "required": [ + "status", + "detail" + ], + "title": "ErrorResponse", + "description": "Every error the API returns: the `status` and `detail` members of RFC 9457 problem details." + }, + "EventBrowseRequest": { + "properties": { + "filters": { + "$ref": "#/components/schemas/EventFilter" }, - "events_by_type": { - "items": { - "$ref": "#/components/schemas/EventTypeCount" - }, - "type": "array", - "title": "Events By Type" + "skip": { + "type": "integer", + "title": "Skip", + "default": 0 }, - "events_by_hour": { + "limit": { + "type": "integer", + "maximum": 500.0, + "title": "Limit", + "default": 50 + } + }, + "type": "object", + "required": [ + "filters" + ], + "title": "EventBrowseRequest", + "description": "Request model for browsing events" + }, + "EventBrowseResponse": { + "properties": { + "events": { "items": { - "$ref": "#/components/schemas/HourlyEventCount" + "oneOf": [ + { + "$ref": "#/components/schemas/ExecutionRequestedEvent" + }, + { + "$ref": "#/components/schemas/ExecutionAcceptedEvent" + }, + { + "$ref": "#/components/schemas/ExecutionQueuedEvent" + }, + { + "$ref": "#/components/schemas/ExecutionStartedEvent" + }, + { + "$ref": "#/components/schemas/ExecutionRunningEvent" + }, + { + "$ref": "#/components/schemas/ExecutionCompletedEvent" + }, + { + "$ref": "#/components/schemas/ExecutionFailedEvent" + }, + { + "$ref": "#/components/schemas/ExecutionTimeoutEvent" + }, + { + "$ref": "#/components/schemas/ExecutionCancelledEvent" + }, + { + "$ref": "#/components/schemas/PodCreatedEvent" + }, + { + "$ref": "#/components/schemas/PodScheduledEvent" + }, + { + "$ref": "#/components/schemas/PodRunningEvent" + }, + { + "$ref": "#/components/schemas/PodSucceededEvent" + }, + { + "$ref": "#/components/schemas/PodFailedEvent" + }, + { + "$ref": "#/components/schemas/PodTerminatedEvent" + }, + { + "$ref": "#/components/schemas/PodDeletedEvent" + }, + { + "$ref": "#/components/schemas/ResultStoredEvent" + }, + { + "$ref": "#/components/schemas/ResultFailedEvent" + }, + { + "$ref": "#/components/schemas/UserSettingsUpdatedEvent" + }, + { + "$ref": "#/components/schemas/UserRegisteredEvent" + }, + { + "$ref": "#/components/schemas/UserLoginEvent" + }, + { + "$ref": "#/components/schemas/UserLoggedInEvent" + }, + { + "$ref": "#/components/schemas/UserLoggedOutEvent" + }, + { + "$ref": "#/components/schemas/UserUpdatedEvent" + }, + { + "$ref": "#/components/schemas/UserDeletedEvent" + }, + { + "$ref": "#/components/schemas/NotificationCreatedEvent" + }, + { + "$ref": "#/components/schemas/NotificationSentEvent" + }, + { + "$ref": "#/components/schemas/NotificationDeliveredEvent" + }, + { + "$ref": "#/components/schemas/NotificationFailedEvent" + }, + { + "$ref": "#/components/schemas/NotificationReadEvent" + }, + { + "$ref": "#/components/schemas/NotificationAllReadEvent" + }, + { + "$ref": "#/components/schemas/NotificationClickedEvent" + }, + { + "$ref": "#/components/schemas/NotificationPreferencesUpdatedEvent" + }, + { + "$ref": "#/components/schemas/ScriptSavedEvent" + }, + { + "$ref": "#/components/schemas/ScriptDeletedEvent" + }, + { + "$ref": "#/components/schemas/ScriptSharedEvent" + }, + { + "$ref": "#/components/schemas/SecurityViolationEvent" + }, + { + "$ref": "#/components/schemas/RateLimitExceededEvent" + }, + { + "$ref": "#/components/schemas/AuthFailedEvent" + }, + { + "$ref": "#/components/schemas/ResourceLimitExceededEvent" + }, + { + "$ref": "#/components/schemas/QuotaExceededEvent" + }, + { + "$ref": "#/components/schemas/SystemErrorEvent" + }, + { + "$ref": "#/components/schemas/ServiceUnhealthyEvent" + }, + { + "$ref": "#/components/schemas/ServiceRecoveredEvent" + }, + { + "$ref": "#/components/schemas/DLQMessageReceivedEvent" + }, + { + "$ref": "#/components/schemas/DLQMessageRetriedEvent" + }, + { + "$ref": "#/components/schemas/DLQMessageDiscardedEvent" + } + ], + "discriminator": { + "propertyName": "event_type", + "mapping": { + "auth_failed": "#/components/schemas/AuthFailedEvent", + "dlq_message_discarded": "#/components/schemas/DLQMessageDiscardedEvent", + "dlq_message_received": "#/components/schemas/DLQMessageReceivedEvent", + "dlq_message_retried": "#/components/schemas/DLQMessageRetriedEvent", + "execution_accepted": "#/components/schemas/ExecutionAcceptedEvent", + "execution_cancelled": "#/components/schemas/ExecutionCancelledEvent", + "execution_completed": "#/components/schemas/ExecutionCompletedEvent", + "execution_failed": "#/components/schemas/ExecutionFailedEvent", + "execution_queued": "#/components/schemas/ExecutionQueuedEvent", + "execution_requested": "#/components/schemas/ExecutionRequestedEvent", + "execution_running": "#/components/schemas/ExecutionRunningEvent", + "execution_started": "#/components/schemas/ExecutionStartedEvent", + "execution_timeout": "#/components/schemas/ExecutionTimeoutEvent", + "notification_all_read": "#/components/schemas/NotificationAllReadEvent", + "notification_clicked": "#/components/schemas/NotificationClickedEvent", + "notification_created": "#/components/schemas/NotificationCreatedEvent", + "notification_delivered": "#/components/schemas/NotificationDeliveredEvent", + "notification_failed": "#/components/schemas/NotificationFailedEvent", + "notification_preferences_updated": "#/components/schemas/NotificationPreferencesUpdatedEvent", + "notification_read": "#/components/schemas/NotificationReadEvent", + "notification_sent": "#/components/schemas/NotificationSentEvent", + "pod_created": "#/components/schemas/PodCreatedEvent", + "pod_deleted": "#/components/schemas/PodDeletedEvent", + "pod_failed": "#/components/schemas/PodFailedEvent", + "pod_running": "#/components/schemas/PodRunningEvent", + "pod_scheduled": "#/components/schemas/PodScheduledEvent", + "pod_succeeded": "#/components/schemas/PodSucceededEvent", + "pod_terminated": "#/components/schemas/PodTerminatedEvent", + "quota_exceeded": "#/components/schemas/QuotaExceededEvent", + "rate_limit_exceeded": "#/components/schemas/RateLimitExceededEvent", + "resource_limit_exceeded": "#/components/schemas/ResourceLimitExceededEvent", + "result_failed": "#/components/schemas/ResultFailedEvent", + "result_stored": "#/components/schemas/ResultStoredEvent", + "script_deleted": "#/components/schemas/ScriptDeletedEvent", + "script_saved": "#/components/schemas/ScriptSavedEvent", + "script_shared": "#/components/schemas/ScriptSharedEvent", + "security_violation": "#/components/schemas/SecurityViolationEvent", + "service_recovered": "#/components/schemas/ServiceRecoveredEvent", + "service_unhealthy": "#/components/schemas/ServiceUnhealthyEvent", + "system_error": "#/components/schemas/SystemErrorEvent", + "user_deleted": "#/components/schemas/UserDeletedEvent", + "user_logged_in": "#/components/schemas/UserLoggedInEvent", + "user_logged_out": "#/components/schemas/UserLoggedOutEvent", + "user_login": "#/components/schemas/UserLoginEvent", + "user_registered": "#/components/schemas/UserRegisteredEvent", + "user_settings_updated": "#/components/schemas/UserSettingsUpdatedEvent", + "user_updated": "#/components/schemas/UserUpdatedEvent" + } + } }, "type": "array", - "title": "Events By Hour" + "title": "Events" }, - "top_users": { - "items": { - "$ref": "#/components/schemas/UserEventCount" - }, - "type": "array", - "title": "Top Users" + "total": { + "type": "integer", + "title": "Total" }, - "error_rate": { - "type": "number", - "title": "Error Rate" + "skip": { + "type": "integer", + "title": "Skip" }, - "avg_processing_time": { - "type": "number", - "title": "Avg Processing Time" - } - }, - "type": "object", - "required": [ - "total_events", - "events_by_type", - "events_by_hour", - "top_users", - "error_rate", - "avg_processing_time" + "limit": { + "type": "integer", + "title": "Limit" + } + }, + "type": "object", + "required": [ + "events", + "total", + "skip", + "limit" ], - "title": "EventStatsResponse", - "description": "Response model for event statistics" + "title": "EventBrowseResponse", + "description": "Response model for browsing events" }, - "EventSummary": { + "EventDeleteResponse": { "properties": { - "event_id": { + "message": { "type": "string", - "title": "Event Id" - }, - "event_type": { - "$ref": "#/components/schemas/EventType" + "title": "Message" }, - "timestamp": { + "event_id": { "type": "string", - "format": "date-time", - "title": "Timestamp" - }, - "aggregate_id": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Aggregate Id" + "title": "Event Id" } }, "type": "object", "required": [ - "event_id", - "event_type", - "timestamp" - ], - "title": "EventSummary" - }, - "EventType": { - "type": "string", - "enum": [ - "execution_requested", - "execution_accepted", - "execution_queued", - "execution_started", - "execution_running", - "execution_completed", - "execution_failed", - "execution_timeout", - "execution_cancelled", - "pod_created", - "pod_scheduled", - "pod_running", - "pod_succeeded", - "pod_failed", - "pod_terminated", - "pod_deleted", - "user_registered", - "user_login", - "user_logged_in", - "user_logged_out", - "user_updated", - "user_deleted", - "user_settings_updated", - "notification_created", - "notification_sent", - "notification_delivered", - "notification_failed", - "notification_read", - "notification_all_read", - "notification_clicked", - "notification_preferences_updated", - "script_saved", - "script_deleted", - "script_shared", - "security_violation", - "rate_limit_exceeded", - "auth_failed", - "resource_limit_exceeded", - "quota_exceeded", - "system_error", - "service_unhealthy", - "service_recovered", - "result_stored", - "result_failed", - "saga_started", - "saga_completed", - "saga_failed", - "saga_cancelled", - "saga_compensating", - "saga_compensated", - "create_pod_command", - "delete_pod_command", - "allocate_resources_command", - "release_resources_command", - "dlq_message_received", - "dlq_message_retried", - "dlq_message_discarded" + "message", + "event_id" ], - "title": "EventType", - "description": "Event types used throughout the system." + "title": "EventDeleteResponse", + "description": "Response model for event deletion" }, - "EventTypeCount": { + "EventDetailResponse": { "properties": { - "event_type": { - "$ref": "#/components/schemas/EventType" + "event": { + "oneOf": [ + { + "$ref": "#/components/schemas/ExecutionRequestedEvent" + }, + { + "$ref": "#/components/schemas/ExecutionAcceptedEvent" + }, + { + "$ref": "#/components/schemas/ExecutionQueuedEvent" + }, + { + "$ref": "#/components/schemas/ExecutionStartedEvent" + }, + { + "$ref": "#/components/schemas/ExecutionRunningEvent" + }, + { + "$ref": "#/components/schemas/ExecutionCompletedEvent" + }, + { + "$ref": "#/components/schemas/ExecutionFailedEvent" + }, + { + "$ref": "#/components/schemas/ExecutionTimeoutEvent" + }, + { + "$ref": "#/components/schemas/ExecutionCancelledEvent" + }, + { + "$ref": "#/components/schemas/PodCreatedEvent" + }, + { + "$ref": "#/components/schemas/PodScheduledEvent" + }, + { + "$ref": "#/components/schemas/PodRunningEvent" + }, + { + "$ref": "#/components/schemas/PodSucceededEvent" + }, + { + "$ref": "#/components/schemas/PodFailedEvent" + }, + { + "$ref": "#/components/schemas/PodTerminatedEvent" + }, + { + "$ref": "#/components/schemas/PodDeletedEvent" + }, + { + "$ref": "#/components/schemas/ResultStoredEvent" + }, + { + "$ref": "#/components/schemas/ResultFailedEvent" + }, + { + "$ref": "#/components/schemas/UserSettingsUpdatedEvent" + }, + { + "$ref": "#/components/schemas/UserRegisteredEvent" + }, + { + "$ref": "#/components/schemas/UserLoginEvent" + }, + { + "$ref": "#/components/schemas/UserLoggedInEvent" + }, + { + "$ref": "#/components/schemas/UserLoggedOutEvent" + }, + { + "$ref": "#/components/schemas/UserUpdatedEvent" + }, + { + "$ref": "#/components/schemas/UserDeletedEvent" + }, + { + "$ref": "#/components/schemas/NotificationCreatedEvent" + }, + { + "$ref": "#/components/schemas/NotificationSentEvent" + }, + { + "$ref": "#/components/schemas/NotificationDeliveredEvent" + }, + { + "$ref": "#/components/schemas/NotificationFailedEvent" + }, + { + "$ref": "#/components/schemas/NotificationReadEvent" + }, + { + "$ref": "#/components/schemas/NotificationAllReadEvent" + }, + { + "$ref": "#/components/schemas/NotificationClickedEvent" + }, + { + "$ref": "#/components/schemas/NotificationPreferencesUpdatedEvent" + }, + { + "$ref": "#/components/schemas/ScriptSavedEvent" + }, + { + "$ref": "#/components/schemas/ScriptDeletedEvent" + }, + { + "$ref": "#/components/schemas/ScriptSharedEvent" + }, + { + "$ref": "#/components/schemas/SecurityViolationEvent" + }, + { + "$ref": "#/components/schemas/RateLimitExceededEvent" + }, + { + "$ref": "#/components/schemas/AuthFailedEvent" + }, + { + "$ref": "#/components/schemas/ResourceLimitExceededEvent" + }, + { + "$ref": "#/components/schemas/QuotaExceededEvent" + }, + { + "$ref": "#/components/schemas/SystemErrorEvent" + }, + { + "$ref": "#/components/schemas/ServiceUnhealthyEvent" + }, + { + "$ref": "#/components/schemas/ServiceRecoveredEvent" + }, + { + "$ref": "#/components/schemas/DLQMessageReceivedEvent" + }, + { + "$ref": "#/components/schemas/DLQMessageRetriedEvent" + }, + { + "$ref": "#/components/schemas/DLQMessageDiscardedEvent" + } + ], + "title": "Event", + "discriminator": { + "propertyName": "event_type", + "mapping": { + "auth_failed": "#/components/schemas/AuthFailedEvent", + "dlq_message_discarded": "#/components/schemas/DLQMessageDiscardedEvent", + "dlq_message_received": "#/components/schemas/DLQMessageReceivedEvent", + "dlq_message_retried": "#/components/schemas/DLQMessageRetriedEvent", + "execution_accepted": "#/components/schemas/ExecutionAcceptedEvent", + "execution_cancelled": "#/components/schemas/ExecutionCancelledEvent", + "execution_completed": "#/components/schemas/ExecutionCompletedEvent", + "execution_failed": "#/components/schemas/ExecutionFailedEvent", + "execution_queued": "#/components/schemas/ExecutionQueuedEvent", + "execution_requested": "#/components/schemas/ExecutionRequestedEvent", + "execution_running": "#/components/schemas/ExecutionRunningEvent", + "execution_started": "#/components/schemas/ExecutionStartedEvent", + "execution_timeout": "#/components/schemas/ExecutionTimeoutEvent", + "notification_all_read": "#/components/schemas/NotificationAllReadEvent", + "notification_clicked": "#/components/schemas/NotificationClickedEvent", + "notification_created": "#/components/schemas/NotificationCreatedEvent", + "notification_delivered": "#/components/schemas/NotificationDeliveredEvent", + "notification_failed": "#/components/schemas/NotificationFailedEvent", + "notification_preferences_updated": "#/components/schemas/NotificationPreferencesUpdatedEvent", + "notification_read": "#/components/schemas/NotificationReadEvent", + "notification_sent": "#/components/schemas/NotificationSentEvent", + "pod_created": "#/components/schemas/PodCreatedEvent", + "pod_deleted": "#/components/schemas/PodDeletedEvent", + "pod_failed": "#/components/schemas/PodFailedEvent", + "pod_running": "#/components/schemas/PodRunningEvent", + "pod_scheduled": "#/components/schemas/PodScheduledEvent", + "pod_succeeded": "#/components/schemas/PodSucceededEvent", + "pod_terminated": "#/components/schemas/PodTerminatedEvent", + "quota_exceeded": "#/components/schemas/QuotaExceededEvent", + "rate_limit_exceeded": "#/components/schemas/RateLimitExceededEvent", + "resource_limit_exceeded": "#/components/schemas/ResourceLimitExceededEvent", + "result_failed": "#/components/schemas/ResultFailedEvent", + "result_stored": "#/components/schemas/ResultStoredEvent", + "script_deleted": "#/components/schemas/ScriptDeletedEvent", + "script_saved": "#/components/schemas/ScriptSavedEvent", + "script_shared": "#/components/schemas/ScriptSharedEvent", + "security_violation": "#/components/schemas/SecurityViolationEvent", + "service_recovered": "#/components/schemas/ServiceRecoveredEvent", + "service_unhealthy": "#/components/schemas/ServiceUnhealthyEvent", + "system_error": "#/components/schemas/SystemErrorEvent", + "user_deleted": "#/components/schemas/UserDeletedEvent", + "user_logged_in": "#/components/schemas/UserLoggedInEvent", + "user_logged_out": "#/components/schemas/UserLoggedOutEvent", + "user_login": "#/components/schemas/UserLoginEvent", + "user_registered": "#/components/schemas/UserRegisteredEvent", + "user_settings_updated": "#/components/schemas/UserSettingsUpdatedEvent", + "user_updated": "#/components/schemas/UserUpdatedEvent" + } + } }, - "count": { - "type": "integer", - "title": "Count" - } - }, - "type": "object", - "required": [ - "event_type", - "count" - ], - "title": "EventTypeCount" - }, - "ExampleScripts": { - "properties": { - "scripts": { - "additionalProperties": { - "type": "string" + "related_events": { + "items": { + "$ref": "#/components/schemas/EventSummary" }, - "type": "object", - "title": "Scripts" + "type": "array", + "title": "Related Events" + }, + "timeline": { + "items": { + "$ref": "#/components/schemas/EventSummary" + }, + "type": "array", + "title": "Timeline" } }, "type": "object", "required": [ - "scripts" + "event", + "related_events", + "timeline" ], - "title": "ExampleScripts", - "description": "Model for example scripts." + "title": "EventDetailResponse", + "description": "Response model for event detail" }, - "ExecutionAcceptedEvent": { + "EventFilter": { "properties": { - "event_id": { - "type": "string", - "title": "Event Id" - }, - "event_type": { - "type": "string", - "const": "execution_accepted", - "title": "Event Type", - "default": "execution_accepted" - }, - "event_version": { - "type": "string", - "title": "Event Version", - "default": "1.0" - }, - "timestamp": { - "type": "string", - "format": "date-time", - "title": "Timestamp" + "event_types": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/EventType" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Event Types" }, "aggregate_id": { "anyOf": [ @@ -7160,126 +5731,148 @@ ], "title": "Aggregate Id" }, - "metadata": { - "$ref": "#/components/schemas/EventMetadata" + "user_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "User Id" }, - "execution_id": { - "type": "string", - "title": "Execution Id" + "start_time": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "Start Time" }, - "queue_position": { - "type": "integer", - "title": "Queue Position" + "end_time": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "End Time" }, - "estimated_wait_seconds": { + "service_name": { "anyOf": [ { - "type": "number" + "type": "string" }, { "type": "null" } ], - "title": "Estimated Wait Seconds" + "title": "Service Name" }, - "priority": { - "$ref": "#/components/schemas/QueuePriority", - "default": "normal" + "search_text": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Search Text" } }, "type": "object", - "required": [ - "event_id", - "event_type", - "event_version", - "timestamp", - "metadata" - ], - "title": "ExecutionAcceptedEvent" + "title": "EventFilter", + "description": "Filter criteria for browsing events" }, - "ExecutionCancelledEvent": { + "EventMetadata": { "properties": { - "event_id": { + "service_name": { "type": "string", - "title": "Event Id" + "title": "Service Name" }, - "event_type": { + "service_version": { "type": "string", - "const": "execution_cancelled", - "title": "Event Type", - "default": "execution_cancelled" + "title": "Service Version" }, - "event_version": { + "user_id": { "type": "string", - "title": "Event Version", - "default": "1.0" + "title": "User Id" }, - "timestamp": { - "type": "string", - "format": "date-time", - "title": "Timestamp" + "environment": { + "$ref": "#/components/schemas/Environment", + "default": "production" + } + }, + "type": "object", + "required": [ + "service_name", + "service_version", + "user_id" + ], + "title": "EventMetadata", + "description": "Event metadata - embedded in all events." + }, + "EventReplayRequest": { + "properties": { + "event_ids": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Event Ids" + }, + "aggregate_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Aggregate Id" + }, + "start_time": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "Start Time" }, - "aggregate_id": { + "end_time": { "anyOf": [ { - "type": "string" + "type": "string", + "format": "date-time" }, { "type": "null" } ], - "title": "Aggregate Id" - }, - "metadata": { - "$ref": "#/components/schemas/EventMetadata" - }, - "execution_id": { - "type": "string", - "title": "Execution Id" - }, - "cancelled_by": { - "type": "string", - "title": "Cancelled By" - }, - "force_terminated": { - "type": "boolean", - "title": "Force Terminated", - "default": false - } - }, - "type": "object", - "required": [ - "event_id", - "event_type", - "event_version", - "timestamp", - "metadata" - ], - "title": "ExecutionCancelledEvent" - }, - "ExecutionCompletedEvent": { - "properties": { - "event_id": { - "type": "string", - "title": "Event Id" - }, - "event_type": { - "type": "string", - "const": "execution_completed", - "title": "Event Type", - "default": "execution_completed" - }, - "event_version": { - "type": "string", - "title": "Event Version", - "default": "1.0" - }, - "timestamp": { - "type": "string", - "format": "date-time", - "title": "Timestamp" + "title": "End Time" }, - "aggregate_id": { + "target_service": { "anyOf": [ { "type": "string" @@ -7288,78 +5881,33 @@ "type": "null" } ], - "title": "Aggregate Id" - }, - "metadata": { - "$ref": "#/components/schemas/EventMetadata" - }, - "execution_id": { - "type": "string", - "title": "Execution Id" - }, - "exit_code": { - "type": "integer", - "title": "Exit Code" - }, - "resource_usage": { - "$ref": "#/components/schemas/ResourceUsageDomain" - }, - "stdout": { - "type": "string", - "title": "Stdout", - "default": "" + "title": "Target Service" }, - "stderr": { - "type": "string", - "title": "Stderr", - "default": "" + "dry_run": { + "type": "boolean", + "title": "Dry Run", + "default": true } }, "type": "object", - "required": [ - "event_id", - "event_type", - "event_version", - "timestamp", - "metadata" - ], - "title": "ExecutionCompletedEvent" - }, - "ExecutionErrorType": { - "type": "string", - "enum": [ - "system_error", - "timeout", - "resource_limit", - "script_error", - "permission_denied" - ], - "title": "ExecutionErrorType", - "description": "Types of execution errors." + "title": "EventReplayRequest", + "description": "Request model for replaying events" }, - "ExecutionFailedEvent": { + "EventReplayResponse": { "properties": { - "event_id": { - "type": "string", - "title": "Event Id" - }, - "event_type": { - "type": "string", - "const": "execution_failed", - "title": "Event Type", - "default": "execution_failed" + "dry_run": { + "type": "boolean", + "title": "Dry Run" }, - "event_version": { - "type": "string", - "title": "Event Version", - "default": "1.0" + "total_events": { + "type": "integer", + "title": "Total Events" }, - "timestamp": { + "replay_id": { "type": "string", - "format": "date-time", - "title": "Timestamp" + "title": "Replay Id" }, - "aggregate_id": { + "session_id": { "anyOf": [ { "type": "string" @@ -7368,143 +5916,111 @@ "type": "null" } ], - "title": "Aggregate Id" - }, - "metadata": { - "$ref": "#/components/schemas/EventMetadata" - }, - "execution_id": { - "type": "string", - "title": "Execution Id" - }, - "exit_code": { - "type": "integer", - "title": "Exit Code", - "default": 1 + "title": "Session Id" }, - "error_type": { - "$ref": "#/components/schemas/ExecutionErrorType" + "status": { + "$ref": "#/components/schemas/ReplayStatus" }, - "resource_usage": { + "events_preview": { "anyOf": [ { - "$ref": "#/components/schemas/ResourceUsageDomain" + "items": { + "$ref": "#/components/schemas/EventSummary" + }, + "type": "array" }, { "type": "null" } - ] - }, - "stdout": { - "type": "string", - "title": "Stdout", - "default": "" - }, - "stderr": { - "type": "string", - "title": "Stderr", - "default": "" + ], + "title": "Events Preview" } }, "type": "object", "required": [ - "event_id", - "event_type", - "event_version", - "timestamp", - "metadata" + "dry_run", + "total_events", + "replay_id", + "session_id", + "status", + "events_preview" ], - "title": "ExecutionFailedEvent" + "title": "EventReplayResponse", + "description": "Response model for event replay" }, - "ExecutionListResponse": { + "EventReplayStatusResponse": { "properties": { - "executions": { - "items": { - "$ref": "#/components/schemas/ExecutionResult" - }, - "type": "array", - "title": "Executions" + "session_id": { + "type": "string", + "title": "Session Id" }, - "total": { - "type": "integer", - "title": "Total" + "status": { + "$ref": "#/components/schemas/ReplayStatus" }, - "limit": { + "total_events": { "type": "integer", - "title": "Limit" + "title": "Total Events" }, - "skip": { + "replayed_events": { "type": "integer", - "title": "Skip" + "title": "Replayed Events" }, - "has_more": { - "type": "boolean", - "title": "Has More" - } - }, - "type": "object", - "required": [ - "executions", - "total", - "limit", - "skip", - "has_more" - ], - "title": "ExecutionListResponse", - "description": "Model for paginated execution list." - }, - "ExecutionQueuedEvent": { - "properties": { - "event_id": { - "type": "string", - "title": "Event Id" + "failed_events": { + "type": "integer", + "title": "Failed Events" }, - "event_type": { - "type": "string", - "const": "execution_queued", - "title": "Event Type", - "default": "execution_queued" + "skipped_events": { + "type": "integer", + "title": "Skipped Events" }, - "event_version": { + "replay_id": { "type": "string", - "title": "Event Version", - "default": "1.0" + "title": "Replay Id" }, - "timestamp": { + "created_at": { "type": "string", "format": "date-time", - "title": "Timestamp" + "title": "Created At" }, - "aggregate_id": { + "started_at": { "anyOf": [ { - "type": "string" + "type": "string", + "format": "date-time" }, { "type": "null" } ], - "title": "Aggregate Id" - }, - "metadata": { - "$ref": "#/components/schemas/EventMetadata" + "title": "Started At" }, - "execution_id": { - "type": "string", - "title": "Execution Id" + "completed_at": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "Completed At" }, - "position_in_queue": { + "errors": { "anyOf": [ { - "type": "integer" + "items": { + "$ref": "#/components/schemas/ReplayError" + }, + "type": "array" }, { "type": "null" } ], - "title": "Position In Queue" + "title": "Errors" }, - "estimated_start_time": { + "estimated_completion": { "anyOf": [ { "type": "string", @@ -7514,274 +6030,225 @@ "type": "null" } ], - "title": "Estimated Start Time" - } - }, - "type": "object", - "required": [ - "event_id", - "event_type", - "event_version", - "timestamp", - "metadata" - ], - "title": "ExecutionQueuedEvent" - }, - "ExecutionRequest": { - "properties": { - "script": { - "type": "string", - "maxLength": 50000, - "title": "Script", - "description": "Script content (max 50,000 characters)" - }, - "lang": { - "type": "string", - "title": "Lang", - "description": "Language name", - "default": "python" - }, - "lang_version": { - "type": "string", - "title": "Lang Version", - "description": "Language version to use for execution", - "default": "3.14" - } - }, - "type": "object", - "required": [ - "script" - ], - "title": "ExecutionRequest", - "description": "Model for execution request." - }, - "ExecutionRequestedEvent": { - "properties": { - "event_id": { - "type": "string", - "title": "Event Id" - }, - "event_type": { - "type": "string", - "const": "execution_requested", - "title": "Event Type", - "default": "execution_requested" - }, - "event_version": { - "type": "string", - "title": "Event Version", - "default": "1.0" - }, - "timestamp": { - "type": "string", - "format": "date-time", - "title": "Timestamp" + "title": "Estimated Completion" }, - "aggregate_id": { + "execution_results": { "anyOf": [ { - "type": "string" + "items": { + "$ref": "#/components/schemas/ExecutionResult" + }, + "type": "array" }, { "type": "null" } ], - "title": "Aggregate Id" - }, - "metadata": { - "$ref": "#/components/schemas/EventMetadata" - }, - "execution_id": { - "type": "string", - "title": "Execution Id" - }, - "script": { - "type": "string", - "title": "Script" - }, - "language": { - "type": "string", - "title": "Language" - }, - "language_version": { - "type": "string", - "title": "Language Version" - }, - "runtime_image": { - "type": "string", - "title": "Runtime Image" - }, - "runtime_command": { - "items": { - "type": "string" - }, - "type": "array", - "title": "Runtime Command" - }, - "runtime_filename": { - "type": "string", - "title": "Runtime Filename" - }, - "timeout_seconds": { - "type": "integer", - "title": "Timeout Seconds" - }, - "cpu_limit": { - "type": "string", - "title": "Cpu Limit" - }, - "memory_limit": { - "type": "string", - "title": "Memory Limit" - }, - "cpu_request": { - "type": "string", - "title": "Cpu Request" - }, - "memory_request": { - "type": "string", - "title": "Memory Request" + "title": "Execution Results" }, - "priority": { - "$ref": "#/components/schemas/QueuePriority", - "default": "normal" + "progress_percentage": { + "type": "number", + "title": "Progress Percentage", + "readOnly": true } }, "type": "object", "required": [ - "event_id", - "event_type", - "event_version", - "timestamp", - "metadata" + "session_id", + "status", + "total_events", + "replayed_events", + "failed_events", + "skipped_events", + "replay_id", + "created_at", + "started_at", + "completed_at", + "errors", + "estimated_completion", + "execution_results", + "progress_percentage" ], - "title": "ExecutionRequestedEvent" + "title": "EventReplayStatusResponse", + "description": "Response model for replay status" }, - "ExecutionResponse": { + "EventStatistics": { "properties": { - "execution_id": { - "type": "string", - "title": "Execution Id" + "total_events": { + "type": "integer", + "title": "Total Events" }, - "status": { - "$ref": "#/components/schemas/ExecutionStatus" - } - }, - "type": "object", - "required": [ - "execution_id", - "status" - ], - "title": "ExecutionResponse", - "description": "Model for execution response." - }, - "ExecutionResult": { - "properties": { - "execution_id": { - "type": "string", - "title": "Execution Id" + "events_by_type": { + "items": { + "$ref": "#/components/schemas/EventTypeCount" + }, + "type": "array", + "title": "Events By Type" }, - "status": { - "$ref": "#/components/schemas/ExecutionStatus" + "events_by_service": { + "items": { + "$ref": "#/components/schemas/ServiceEventCount" + }, + "type": "array", + "title": "Events By Service" }, - "stdout": { + "events_by_hour": { + "items": { + "$ref": "#/components/schemas/HourlyEventCount" + }, + "type": "array", + "title": "Events By Hour" + }, + "top_users": { + "items": { + "$ref": "#/components/schemas/UserEventCount" + }, + "type": "array", + "title": "Top Users" + }, + "error_rate": { + "type": "number", + "title": "Error Rate", + "default": 0.0 + }, + "avg_processing_time": { + "type": "number", + "title": "Avg Processing Time", + "default": 0.0 + }, + "start_time": { "anyOf": [ { - "type": "string" + "type": "string", + "format": "date-time" }, { "type": "null" } ], - "title": "Stdout" + "title": "Start Time" }, - "stderr": { + "end_time": { "anyOf": [ { - "type": "string" + "type": "string", + "format": "date-time" }, { "type": "null" } ], - "title": "Stderr" - }, - "lang": { - "type": "string", - "title": "Lang" + "title": "End Time" + } + }, + "type": "object", + "required": [ + "total_events", + "events_by_type", + "events_by_service", + "events_by_hour", + "top_users", + "error_rate", + "avg_processing_time", + "start_time", + "end_time" + ], + "title": "EventStatistics", + "description": "Event statistics with API schema extras.", + "example": { + "events_by_hour": [ + { + "count": 85, + "hour": "2024-01-20 10:00" + }, + { + "count": 92, + "hour": "2024-01-20 11:00" + } + ], + "events_by_service": [ + { + "count": 523, + "service_name": "api-gateway" + }, + { + "count": 1020, + "service_name": "execution-service" + } + ], + "events_by_type": [ + { + "count": 523, + "event_type": "EXECUTION_REQUESTED" + }, + { + "count": 498, + "event_type": "EXECUTION_COMPLETED" + }, + { + "count": 522, + "event_type": "POD_CREATED" + } + ], + "total_events": 1543 + } + }, + "EventStatsResponse": { + "properties": { + "total_events": { + "type": "integer", + "title": "Total Events" }, - "lang_version": { - "type": "string", - "title": "Lang Version" + "events_by_type": { + "items": { + "$ref": "#/components/schemas/EventTypeCount" + }, + "type": "array", + "title": "Events By Type" }, - "resource_usage": { - "anyOf": [ - { - "$ref": "#/components/schemas/ResourceUsage" - }, - { - "type": "null" - } - ] + "events_by_hour": { + "items": { + "$ref": "#/components/schemas/HourlyEventCount" + }, + "type": "array", + "title": "Events By Hour" }, - "exit_code": { - "anyOf": [ - { - "type": "integer" - }, - { - "type": "null" - } - ], - "title": "Exit Code" + "top_users": { + "items": { + "$ref": "#/components/schemas/UserEventCount" + }, + "type": "array", + "title": "Top Users" }, - "error_type": { - "anyOf": [ - { - "$ref": "#/components/schemas/ExecutionErrorType" - }, - { - "type": "null" - } - ] + "error_rate": { + "type": "number", + "title": "Error Rate" }, - "priority": { - "$ref": "#/components/schemas/QueuePriority", - "default": "normal" + "avg_processing_time": { + "type": "number", + "title": "Avg Processing Time" } }, "type": "object", "required": [ - "execution_id", - "status", - "stdout", - "stderr", - "lang", - "lang_version", - "resource_usage", - "exit_code", - "error_type", - "priority" + "total_events", + "events_by_type", + "events_by_hour", + "top_users", + "error_rate", + "avg_processing_time" ], - "title": "ExecutionResult", - "description": "Model for execution result." + "title": "EventStatsResponse", + "description": "Response model for event statistics" }, - "ExecutionRunningEvent": { + "EventSummary": { "properties": { "event_id": { "type": "string", "title": "Event Id" }, "event_type": { - "type": "string", - "const": "execution_running", - "title": "Event Type", - "default": "execution_running" - }, - "event_version": { - "type": "string", - "title": "Event Version", - "default": "1.0" + "$ref": "#/components/schemas/EventType" }, "timestamp": { "type": "string", @@ -7798,41 +6265,105 @@ } ], "title": "Aggregate Id" + } + }, + "type": "object", + "required": [ + "event_id", + "event_type", + "timestamp" + ], + "title": "EventSummary" + }, + "EventType": { + "type": "string", + "enum": [ + "execution_requested", + "execution_accepted", + "execution_queued", + "execution_started", + "execution_running", + "execution_completed", + "execution_failed", + "execution_timeout", + "execution_cancelled", + "pod_created", + "pod_scheduled", + "pod_running", + "pod_succeeded", + "pod_failed", + "pod_terminated", + "pod_deleted", + "user_registered", + "user_login", + "user_logged_in", + "user_logged_out", + "user_updated", + "user_deleted", + "user_settings_updated", + "notification_created", + "notification_sent", + "notification_delivered", + "notification_failed", + "notification_read", + "notification_all_read", + "notification_clicked", + "notification_preferences_updated", + "script_saved", + "script_deleted", + "script_shared", + "security_violation", + "rate_limit_exceeded", + "auth_failed", + "resource_limit_exceeded", + "quota_exceeded", + "system_error", + "service_unhealthy", + "service_recovered", + "result_stored", + "result_failed", + "dlq_message_received", + "dlq_message_retried", + "dlq_message_discarded" + ], + "title": "EventType", + "description": "Event types used throughout the system." + }, + "EventTypeCount": { + "properties": { + "event_type": { + "$ref": "#/components/schemas/EventType" }, - "metadata": { - "$ref": "#/components/schemas/EventMetadata" - }, - "execution_id": { - "type": "string", - "title": "Execution Id" - }, - "pod_name": { - "type": "string", - "title": "Pod Name" - }, - "progress_percentage": { - "anyOf": [ - { - "type": "integer" - }, - { - "type": "null" - } - ], - "title": "Progress Percentage" + "count": { + "type": "integer", + "title": "Count" } }, "type": "object", "required": [ - "event_id", "event_type", - "event_version", - "timestamp", - "metadata" + "count" ], - "title": "ExecutionRunningEvent" + "title": "EventTypeCount" }, - "ExecutionStartedEvent": { + "ExampleScripts": { + "properties": { + "scripts": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "title": "Scripts" + } + }, + "type": "object", + "required": [ + "scripts" + ], + "title": "ExampleScripts", + "description": "Model for example scripts." + }, + "ExecutionAcceptedEvent": { "properties": { "event_id": { "type": "string", @@ -7840,9 +6371,9 @@ }, "event_type": { "type": "string", - "const": "execution_started", + "const": "execution_accepted", "title": "Event Type", - "default": "execution_started" + "default": "execution_accepted" }, "event_version": { "type": "string", @@ -7872,31 +6403,24 @@ "type": "string", "title": "Execution Id" }, - "pod_name": { - "type": "string", - "title": "Pod Name" + "queue_position": { + "type": "integer", + "title": "Queue Position" }, - "node_name": { + "estimated_wait_seconds": { "anyOf": [ { - "type": "string" + "type": "number" }, { "type": "null" } ], - "title": "Node Name" + "title": "Estimated Wait Seconds" }, - "container_id": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Container Id" + "priority": { + "$ref": "#/components/schemas/QueuePriority", + "default": "normal" } }, "type": "object", @@ -7907,24 +6431,9 @@ "timestamp", "metadata" ], - "title": "ExecutionStartedEvent" - }, - "ExecutionStatus": { - "type": "string", - "enum": [ - "queued", - "scheduled", - "running", - "completed", - "failed", - "timeout", - "cancelled", - "error" - ], - "title": "ExecutionStatus", - "description": "Status of an execution." + "title": "ExecutionAcceptedEvent" }, - "ExecutionTimeoutEvent": { + "ExecutionCancelledEvent": { "properties": { "event_id": { "type": "string", @@ -7932,9 +6441,9 @@ }, "event_type": { "type": "string", - "const": "execution_timeout", + "const": "execution_cancelled", "title": "Event Type", - "default": "execution_timeout" + "default": "execution_cancelled" }, "event_version": { "type": "string", @@ -7946,219 +6455,45 @@ "format": "date-time", "title": "Timestamp" }, - "aggregate_id": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Aggregate Id" - }, - "metadata": { - "$ref": "#/components/schemas/EventMetadata" - }, - "execution_id": { - "type": "string", - "title": "Execution Id" - }, - "timeout_seconds": { - "type": "integer", - "title": "Timeout Seconds" - }, - "resource_usage": { - "$ref": "#/components/schemas/ResourceUsageDomain" - }, - "stdout": { - "type": "string", - "title": "Stdout", - "default": "" - }, - "stderr": { - "type": "string", - "title": "Stderr", - "default": "" - } - }, - "type": "object", - "required": [ - "event_id", - "event_type", - "event_version", - "timestamp", - "metadata" - ], - "title": "ExecutionTimeoutEvent" - }, - "ExportFormat": { - "type": "string", - "enum": [ - "csv", - "json" - ], - "title": "ExportFormat", - "description": "Supported export file formats." - }, - "HourlyEventCount": { - "properties": { - "hour": { - "type": "string", - "title": "Hour" - }, - "count": { - "type": "integer", - "title": "Count" - } - }, - "type": "object", - "required": [ - "hour", - "count" - ], - "title": "HourlyEventCount" - }, - "LanguageInfo": { - "properties": { - "versions": { - "items": { - "type": "string" - }, - "type": "array", - "title": "Versions" - }, - "file_ext": { - "type": "string", - "title": "File Ext" - } - }, - "type": "object", - "required": [ - "versions", - "file_ext" - ], - "title": "LanguageInfo", - "description": "Language runtime information." - }, - "LanguageLimits": { - "properties": { - "cpu_limit": { - "type": "string", - "title": "Cpu Limit" - }, - "memory_limit": { - "type": "string", - "title": "Memory Limit" - }, - "execution_timeout": { - "type": "integer", - "title": "Execution Timeout" - } - }, - "type": "object", - "required": [ - "cpu_limit", - "memory_limit", - "execution_timeout" - ], - "title": "LanguageLimits", - "description": "Limits a pod for one language gets: the admin limits raised to the language minimums." - }, - "LivenessResponse": { - "properties": { - "status": { - "type": "string", - "title": "Status", - "description": "Health status" - }, - "uptime_seconds": { - "type": "integer", - "title": "Uptime Seconds", - "description": "Server uptime in seconds" - }, - "timestamp": { - "type": "string", - "format": "date-time", - "title": "Timestamp", - "description": "Timestamp of health check" - } - }, - "type": "object", - "required": [ - "status", - "uptime_seconds", - "timestamp" - ], - "title": "LivenessResponse", - "description": "Response model for liveness probe." - }, - "LogLevel": { - "type": "string", - "enum": [ - "DEBUG", - "INFO", - "WARNING", - "ERROR", - "CRITICAL" - ], - "title": "LogLevel", - "description": "Log level options." - }, - "LoginMethod": { - "type": "string", - "enum": [ - "password", - "oauth", - "sso", - "api_key" - ], - "title": "LoginMethod", - "description": "User login methods." - }, - "LoginResponse": { - "properties": { - "message": { - "type": "string", - "title": "Message" - }, - "username": { - "type": "string", - "title": "Username" + "aggregate_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Aggregate Id" }, - "role": { - "$ref": "#/components/schemas/UserRole" + "metadata": { + "$ref": "#/components/schemas/EventMetadata" }, - "csrf_token": { + "execution_id": { "type": "string", - "title": "Csrf Token" - } - }, - "type": "object", - "required": [ - "message", - "username", - "role", - "csrf_token" - ], - "title": "LoginResponse", - "description": "Response model for successful login" - }, - "MessageResponse": { - "properties": { - "message": { + "title": "Execution Id" + }, + "cancelled_by": { "type": "string", - "title": "Message" + "title": "Cancelled By" + }, + "force_terminated": { + "type": "boolean", + "title": "Force Terminated", + "default": false } }, "type": "object", "required": [ - "message" + "event_id", + "event_type", + "event_version", + "timestamp", + "metadata" ], - "title": "MessageResponse", - "description": "Generic message response" + "title": "ExecutionCancelledEvent" }, - "NotificationAllReadEvent": { + "ExecutionCompletedEvent": { "properties": { "event_id": { "type": "string", @@ -8166,9 +6501,9 @@ }, "event_type": { "type": "string", - "const": "notification_all_read", + "const": "execution_completed", "title": "Event Type", - "default": "notification_all_read" + "default": "execution_completed" }, "event_version": { "type": "string", @@ -8194,18 +6529,26 @@ "metadata": { "$ref": "#/components/schemas/EventMetadata" }, - "user_id": { + "execution_id": { "type": "string", - "title": "User Id" + "title": "Execution Id" }, - "count": { + "exit_code": { "type": "integer", - "title": "Count" + "title": "Exit Code" }, - "read_at": { + "resource_usage": { + "$ref": "#/components/schemas/ResourceUsageDomain" + }, + "stdout": { "type": "string", - "format": "date-time", - "title": "Read At" + "title": "Stdout", + "default": "" + }, + "stderr": { + "type": "string", + "title": "Stderr", + "default": "" } }, "type": "object", @@ -8216,19 +6559,21 @@ "timestamp", "metadata" ], - "title": "NotificationAllReadEvent" + "title": "ExecutionCompletedEvent" }, - "NotificationChannel": { + "ExecutionErrorType": { "type": "string", "enum": [ - "in_app", - "webhook", - "slack" + "system_error", + "timeout", + "resource_limit", + "script_error", + "permission_denied" ], - "title": "NotificationChannel", - "description": "Notification delivery channels." + "title": "ExecutionErrorType", + "description": "Types of execution errors." }, - "NotificationClickedEvent": { + "ExecutionFailedEvent": { "properties": { "event_id": { "type": "string", @@ -8236,9 +6581,9 @@ }, "event_type": { "type": "string", - "const": "notification_clicked", + "const": "execution_failed", "title": "Event Type", - "default": "notification_clicked" + "default": "execution_failed" }, "event_version": { "type": "string", @@ -8264,20 +6609,109 @@ "metadata": { "$ref": "#/components/schemas/EventMetadata" }, - "notification_id": { + "execution_id": { "type": "string", - "title": "Notification Id" + "title": "Execution Id" }, - "user_id": { + "exit_code": { + "type": "integer", + "title": "Exit Code", + "default": 1 + }, + "error_type": { + "$ref": "#/components/schemas/ExecutionErrorType" + }, + "resource_usage": { + "anyOf": [ + { + "$ref": "#/components/schemas/ResourceUsageDomain" + }, + { + "type": "null" + } + ] + }, + "stdout": { "type": "string", - "title": "User Id" + "title": "Stdout", + "default": "" }, - "clicked_at": { + "stderr": { + "type": "string", + "title": "Stderr", + "default": "" + } + }, + "type": "object", + "required": [ + "event_id", + "event_type", + "event_version", + "timestamp", + "metadata" + ], + "title": "ExecutionFailedEvent" + }, + "ExecutionListResponse": { + "properties": { + "executions": { + "items": { + "$ref": "#/components/schemas/ExecutionResult" + }, + "type": "array", + "title": "Executions" + }, + "total": { + "type": "integer", + "title": "Total" + }, + "limit": { + "type": "integer", + "title": "Limit" + }, + "skip": { + "type": "integer", + "title": "Skip" + }, + "has_more": { + "type": "boolean", + "title": "Has More" + } + }, + "type": "object", + "required": [ + "executions", + "total", + "limit", + "skip", + "has_more" + ], + "title": "ExecutionListResponse", + "description": "Model for paginated execution list." + }, + "ExecutionQueuedEvent": { + "properties": { + "event_id": { + "type": "string", + "title": "Event Id" + }, + "event_type": { + "type": "string", + "const": "execution_queued", + "title": "Event Type", + "default": "execution_queued" + }, + "event_version": { + "type": "string", + "title": "Event Version", + "default": "1.0" + }, + "timestamp": { "type": "string", "format": "date-time", - "title": "Clicked At" + "title": "Timestamp" }, - "action": { + "aggregate_id": { "anyOf": [ { "type": "string" @@ -8286,20 +6720,78 @@ "type": "null" } ], - "title": "Action" + "title": "Aggregate Id" + }, + "metadata": { + "$ref": "#/components/schemas/EventMetadata" + }, + "execution_id": { + "type": "string", + "title": "Execution Id" + }, + "position_in_queue": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Position In Queue" + }, + "estimated_start_time": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "Estimated Start Time" + } + }, + "type": "object", + "required": [ + "event_id", + "event_type", + "event_version", + "timestamp", + "metadata" + ], + "title": "ExecutionQueuedEvent" + }, + "ExecutionRequest": { + "properties": { + "script": { + "type": "string", + "maxLength": 50000, + "title": "Script", + "description": "Script content (max 50,000 characters)" + }, + "lang": { + "type": "string", + "title": "Lang", + "description": "Language name", + "default": "python" + }, + "lang_version": { + "type": "string", + "title": "Lang Version", + "description": "Language version to use for execution", + "default": "3.14" } }, "type": "object", "required": [ - "event_id", - "event_type", - "event_version", - "timestamp", - "metadata" + "script" ], - "title": "NotificationClickedEvent" + "title": "ExecutionRequest", + "description": "Model for execution request." }, - "NotificationCreatedEvent": { + "ExecutionRequestedEvent": { "properties": { "event_id": { "type": "string", @@ -8307,9 +6799,9 @@ }, "event_type": { "type": "string", - "const": "notification_created", + "const": "execution_requested", "title": "Event Type", - "default": "notification_created" + "default": "execution_requested" }, "event_version": { "type": "string", @@ -8335,38 +6827,60 @@ "metadata": { "$ref": "#/components/schemas/EventMetadata" }, - "notification_id": { + "execution_id": { "type": "string", - "title": "Notification Id" + "title": "Execution Id" }, - "user_id": { + "script": { "type": "string", - "title": "User Id" + "title": "Script" }, - "subject": { + "language": { "type": "string", - "title": "Subject" + "title": "Language" }, - "body": { + "language_version": { "type": "string", - "title": "Body" + "title": "Language Version" }, - "severity": { - "$ref": "#/components/schemas/NotificationSeverity" + "runtime_image": { + "type": "string", + "title": "Runtime Image" }, - "tags": { + "runtime_command": { "items": { "type": "string" }, "type": "array", - "title": "Tags" + "title": "Runtime Command" }, - "channels": { - "items": { - "$ref": "#/components/schemas/NotificationChannel" - }, - "type": "array", - "title": "Channels" + "runtime_filename": { + "type": "string", + "title": "Runtime Filename" + }, + "timeout_seconds": { + "type": "integer", + "title": "Timeout Seconds" + }, + "cpu_limit": { + "type": "string", + "title": "Cpu Limit" + }, + "memory_limit": { + "type": "string", + "title": "Memory Limit" + }, + "cpu_request": { + "type": "string", + "title": "Cpu Request" + }, + "memory_request": { + "type": "string", + "title": "Memory Request" + }, + "priority": { + "$ref": "#/components/schemas/QueuePriority", + "default": "normal" } }, "type": "object", @@ -8377,31 +6891,36 @@ "timestamp", "metadata" ], - "title": "NotificationCreatedEvent" + "title": "ExecutionRequestedEvent" }, - "NotificationDeliveredEvent": { + "ExecutionResponse": { "properties": { - "event_id": { - "type": "string", - "title": "Event Id" - }, - "event_type": { + "execution_id": { "type": "string", - "const": "notification_delivered", - "title": "Event Type", - "default": "notification_delivered" + "title": "Execution Id" }, - "event_version": { + "status": { + "$ref": "#/components/schemas/ExecutionStatus" + } + }, + "type": "object", + "required": [ + "execution_id", + "status" + ], + "title": "ExecutionResponse", + "description": "Model for execution response." + }, + "ExecutionResult": { + "properties": { + "execution_id": { "type": "string", - "title": "Event Version", - "default": "1.0" + "title": "Execution Id" }, - "timestamp": { - "type": "string", - "format": "date-time", - "title": "Timestamp" + "status": { + "$ref": "#/components/schemas/ExecutionStatus" }, - "aggregate_id": { + "stdout": { "anyOf": [ { "type": "string" @@ -8410,39 +6929,80 @@ "type": "null" } ], - "title": "Aggregate Id" + "title": "Stdout" }, - "metadata": { - "$ref": "#/components/schemas/EventMetadata" + "stderr": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Stderr" }, - "notification_id": { + "lang": { "type": "string", - "title": "Notification Id" + "title": "Lang" }, - "user_id": { + "lang_version": { "type": "string", - "title": "User Id" + "title": "Lang Version" }, - "channel": { - "$ref": "#/components/schemas/NotificationChannel" + "resource_usage": { + "anyOf": [ + { + "$ref": "#/components/schemas/ResourceUsage" + }, + { + "type": "null" + } + ] }, - "delivered_at": { - "type": "string", - "format": "date-time", - "title": "Delivered At" + "exit_code": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Exit Code" + }, + "error_type": { + "anyOf": [ + { + "$ref": "#/components/schemas/ExecutionErrorType" + }, + { + "type": "null" + } + ] + }, + "priority": { + "$ref": "#/components/schemas/QueuePriority", + "default": "normal" } }, "type": "object", "required": [ - "event_id", - "event_type", - "event_version", - "timestamp", - "metadata" + "execution_id", + "status", + "stdout", + "stderr", + "lang", + "lang_version", + "resource_usage", + "exit_code", + "error_type", + "priority" ], - "title": "NotificationDeliveredEvent" + "title": "ExecutionResult", + "description": "Model for execution result." }, - "NotificationFailedEvent": { + "ExecutionRunningEvent": { "properties": { "event_id": { "type": "string", @@ -8450,9 +7010,9 @@ }, "event_type": { "type": "string", - "const": "notification_failed", + "const": "execution_running", "title": "Event Type", - "default": "notification_failed" + "default": "execution_running" }, "event_version": { "type": "string", @@ -8478,65 +7038,37 @@ "metadata": { "$ref": "#/components/schemas/EventMetadata" }, - "notification_id": { - "type": "string", - "title": "Notification Id" - }, - "user_id": { - "type": "string", - "title": "User Id" - }, - "channel": { - "$ref": "#/components/schemas/NotificationChannel" - }, - "error": { + "execution_id": { "type": "string", - "title": "Error" - }, - "retry_count": { - "type": "integer", - "title": "Retry Count", - "default": 0 - } - }, - "type": "object", - "required": [ - "event_id", - "event_type", - "event_version", - "timestamp", - "metadata" - ], - "title": "NotificationFailedEvent" - }, - "NotificationListResponse": { - "properties": { - "notifications": { - "items": { - "$ref": "#/components/schemas/NotificationResponse" - }, - "type": "array", - "title": "Notifications" + "title": "Execution Id" }, - "total": { - "type": "integer", - "title": "Total" + "pod_name": { + "type": "string", + "title": "Pod Name" }, - "unread_count": { - "type": "integer", - "title": "Unread Count" + "progress_percentage": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Progress Percentage" } }, "type": "object", "required": [ - "notifications", - "total", - "unread_count" + "event_id", + "event_type", + "event_version", + "timestamp", + "metadata" ], - "title": "NotificationListResponse", - "description": "Response schema for notification list endpoints" + "title": "ExecutionRunningEvent" }, - "NotificationPreferencesUpdatedEvent": { + "ExecutionStartedEvent": { "properties": { "event_id": { "type": "string", @@ -8544,9 +7076,9 @@ }, "event_type": { "type": "string", - "const": "notification_preferences_updated", + "const": "execution_started", "title": "Event Type", - "default": "notification_preferences_updated" + "default": "execution_started" }, "event_version": { "type": "string", @@ -8572,16 +7104,35 @@ "metadata": { "$ref": "#/components/schemas/EventMetadata" }, - "user_id": { + "execution_id": { "type": "string", - "title": "User Id" + "title": "Execution Id" }, - "changed_fields": { - "items": { - "type": "string" - }, - "type": "array", - "title": "Changed Fields" + "pod_name": { + "type": "string", + "title": "Pod Name" + }, + "node_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Node Name" + }, + "container_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Container Id" } }, "type": "object", @@ -8592,9 +7143,24 @@ "timestamp", "metadata" ], - "title": "NotificationPreferencesUpdatedEvent" + "title": "ExecutionStartedEvent" }, - "NotificationReadEvent": { + "ExecutionStatus": { + "type": "string", + "enum": [ + "queued", + "scheduled", + "running", + "completed", + "failed", + "timeout", + "cancelled", + "error" + ], + "title": "ExecutionStatus", + "description": "Status of an execution." + }, + "ExecutionTimeoutEvent": { "properties": { "event_id": { "type": "string", @@ -8602,9 +7168,9 @@ }, "event_type": { "type": "string", - "const": "notification_read", + "const": "execution_timeout", "title": "Event Type", - "default": "notification_read" + "default": "execution_timeout" }, "event_version": { "type": "string", @@ -8630,18 +7196,26 @@ "metadata": { "$ref": "#/components/schemas/EventMetadata" }, - "notification_id": { + "execution_id": { "type": "string", - "title": "Notification Id" + "title": "Execution Id" }, - "user_id": { + "timeout_seconds": { + "type": "integer", + "title": "Timeout Seconds" + }, + "resource_usage": { + "$ref": "#/components/schemas/ResourceUsageDomain" + }, + "stdout": { "type": "string", - "title": "User Id" + "title": "Stdout", + "default": "" }, - "read_at": { + "stderr": { "type": "string", - "format": "date-time", - "title": "Read At" + "title": "Stderr", + "default": "" } }, "type": "object", @@ -8652,77 +7226,175 @@ "timestamp", "metadata" ], - "title": "NotificationReadEvent" + "title": "ExecutionTimeoutEvent" }, - "NotificationResponse": { + "ExportFormat": { + "type": "string", + "enum": [ + "csv", + "json" + ], + "title": "ExportFormat", + "description": "Supported export file formats." + }, + "HourlyEventCount": { "properties": { - "notification_id": { + "hour": { "type": "string", - "title": "Notification Id" - }, - "channel": { - "$ref": "#/components/schemas/NotificationChannel" + "title": "Hour" }, - "status": { - "$ref": "#/components/schemas/NotificationStatus" + "count": { + "type": "integer", + "title": "Count" + } + }, + "type": "object", + "required": [ + "hour", + "count" + ], + "title": "HourlyEventCount" + }, + "LanguageInfo": { + "properties": { + "versions": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Versions" }, - "subject": { + "file_ext": { "type": "string", - "title": "Subject" + "title": "File Ext" + } + }, + "type": "object", + "required": [ + "versions", + "file_ext" + ], + "title": "LanguageInfo", + "description": "Language runtime information." + }, + "LanguageLimits": { + "properties": { + "cpu_limit": { + "type": "string", + "title": "Cpu Limit" }, - "body": { + "memory_limit": { "type": "string", - "title": "Body" + "title": "Memory Limit" }, - "action_url": { + "execution_timeout": { + "type": "integer", + "title": "Execution Timeout" + } + }, + "type": "object", + "required": [ + "cpu_limit", + "memory_limit", + "execution_timeout" + ], + "title": "LanguageLimits", + "description": "Limits a pod for one language gets: the admin limits raised to the language minimums." + }, + "LivenessResponse": { + "properties": { + "status": { "type": "string", - "title": "Action Url" + "title": "Status", + "description": "Health status" }, - "created_at": { + "uptime_seconds": { + "type": "integer", + "title": "Uptime Seconds", + "description": "Server uptime in seconds" + }, + "timestamp": { "type": "string", "format": "date-time", - "title": "Created At" - }, - "read_at": { - "anyOf": [ - { - "type": "string", - "format": "date-time" - }, - { - "type": "null" - } - ], - "title": "Read At" + "title": "Timestamp", + "description": "Timestamp of health check" + } + }, + "type": "object", + "required": [ + "status", + "uptime_seconds", + "timestamp" + ], + "title": "LivenessResponse", + "description": "Response model for liveness probe." + }, + "LogLevel": { + "type": "string", + "enum": [ + "DEBUG", + "INFO", + "WARNING", + "ERROR", + "CRITICAL" + ], + "title": "LogLevel", + "description": "Log level options." + }, + "LoginMethod": { + "type": "string", + "enum": [ + "password", + "oauth", + "sso", + "api_key" + ], + "title": "LoginMethod", + "description": "User login methods." + }, + "LoginResponse": { + "properties": { + "message": { + "type": "string", + "title": "Message" }, - "severity": { - "$ref": "#/components/schemas/NotificationSeverity" + "username": { + "type": "string", + "title": "Username" }, - "tags": { - "items": { - "type": "string" - }, - "type": "array", - "title": "Tags" + "role": { + "$ref": "#/components/schemas/UserRole" + }, + "csrf_token": { + "type": "string", + "title": "Csrf Token" } }, "type": "object", "required": [ - "notification_id", - "channel", - "status", - "subject", - "body", - "action_url", - "created_at", - "read_at", - "severity", - "tags" + "message", + "username", + "role", + "csrf_token" ], - "title": "NotificationResponse", - "description": "Response schema for notification endpoints" + "title": "LoginResponse", + "description": "Response model for successful login" }, - "NotificationSentEvent": { + "MessageResponse": { + "properties": { + "message": { + "type": "string", + "title": "Message" + } + }, + "type": "object", + "required": [ + "message" + ], + "title": "MessageResponse", + "description": "Generic message response" + }, + "NotificationAllReadEvent": { "properties": { "event_id": { "type": "string", @@ -8730,9 +7402,9 @@ }, "event_type": { "type": "string", - "const": "notification_sent", + "const": "notification_all_read", "title": "Event Type", - "default": "notification_sent" + "default": "notification_all_read" }, "event_version": { "type": "string", @@ -8758,21 +7430,18 @@ "metadata": { "$ref": "#/components/schemas/EventMetadata" }, - "notification_id": { - "type": "string", - "title": "Notification Id" - }, "user_id": { "type": "string", "title": "User Id" }, - "channel": { - "$ref": "#/components/schemas/NotificationChannel" + "count": { + "type": "integer", + "title": "Count" }, - "sent_at": { + "read_at": { "type": "string", "format": "date-time", - "title": "Sent At" + "title": "Read At" } }, "type": "object", @@ -8783,178 +7452,41 @@ "timestamp", "metadata" ], - "title": "NotificationSentEvent" - }, - "NotificationSettings-Input": { - "properties": { - "execution_completed": { - "type": "boolean", - "title": "Execution Completed", - "default": true - }, - "execution_failed": { - "type": "boolean", - "title": "Execution Failed", - "default": true - }, - "system_updates": { - "type": "boolean", - "title": "System Updates", - "default": true - }, - "security_alerts": { - "type": "boolean", - "title": "Security Alerts", - "default": true - }, - "channels": { - "items": { - "$ref": "#/components/schemas/NotificationChannel" - }, - "type": "array", - "title": "Channels", - "default": [ - "in_app" - ] - } - }, - "type": "object", - "title": "NotificationSettings", - "description": "User notification preferences" - }, - "NotificationSettings-Output": { - "properties": { - "execution_completed": { - "type": "boolean", - "title": "Execution Completed", - "default": true - }, - "execution_failed": { - "type": "boolean", - "title": "Execution Failed", - "default": true - }, - "system_updates": { - "type": "boolean", - "title": "System Updates", - "default": true - }, - "security_alerts": { - "type": "boolean", - "title": "Security Alerts", - "default": true - }, - "channels": { - "items": { - "$ref": "#/components/schemas/NotificationChannel" - }, - "type": "array", - "title": "Channels", - "default": [ - "in_app" - ] - } - }, - "type": "object", - "required": [ - "execution_completed", - "execution_failed", - "system_updates", - "security_alerts", - "channels" - ], - "title": "NotificationSettings", - "description": "User notification preferences" - }, - "NotificationSeverity": { - "type": "string", - "enum": [ - "low", - "medium", - "high", - "urgent" - ], - "title": "NotificationSeverity", - "description": "Notification severity levels." + "title": "NotificationAllReadEvent" }, - "NotificationStatus": { + "NotificationChannel": { "type": "string", "enum": [ - "pending", - "queued", - "sending", - "delivered", - "failed", - "skipped", - "read", - "clicked" + "in_app", + "webhook", + "slack" ], - "title": "NotificationStatus", - "description": "Notification delivery status." + "title": "NotificationChannel", + "description": "Notification delivery channels." }, - "NotificationSubscription": { + "NotificationClickedEvent": { "properties": { - "user_id": { + "event_id": { "type": "string", - "title": "User Id" - }, - "channel": { - "$ref": "#/components/schemas/NotificationChannel" - }, - "severities": { - "items": { - "$ref": "#/components/schemas/NotificationSeverity" - }, - "type": "array", - "title": "Severities" - }, - "include_tags": { - "items": { - "type": "string" - }, - "type": "array", - "title": "Include Tags" - }, - "exclude_tags": { - "items": { - "type": "string" - }, - "type": "array", - "title": "Exclude Tags" - }, - "enabled": { - "type": "boolean", - "title": "Enabled", - "default": true + "title": "Event Id" }, - "webhook_url": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Webhook Url" + "event_type": { + "type": "string", + "const": "notification_clicked", + "title": "Event Type", + "default": "notification_clicked" }, - "slack_webhook": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Slack Webhook" + "event_version": { + "type": "string", + "title": "Event Version", + "default": "1.0" }, - "quiet_hours_enabled": { - "type": "boolean", - "title": "Quiet Hours Enabled", - "default": false + "timestamp": { + "type": "string", + "format": "date-time", + "title": "Timestamp" }, - "quiet_hours_start": { + "aggregate_id": { "anyOf": [ { "type": "string" @@ -8963,9 +7495,25 @@ "type": "null" } ], - "title": "Quiet Hours Start" + "title": "Aggregate Id" }, - "quiet_hours_end": { + "metadata": { + "$ref": "#/components/schemas/EventMetadata" + }, + "notification_id": { + "type": "string", + "title": "Notification Id" + }, + "user_id": { + "type": "string", + "title": "User Id" + }, + "clicked_at": { + "type": "string", + "format": "date-time", + "title": "Clicked At" + }, + "action": { "anyOf": [ { "type": "string" @@ -8974,69 +7522,20 @@ "type": "null" } ], - "title": "Quiet Hours End" - }, - "timezone": { - "type": "string", - "title": "Timezone", - "default": "UTC" - }, - "batch_interval_minutes": { - "type": "integer", - "title": "Batch Interval Minutes", - "default": 60 - }, - "created_at": { - "type": "string", - "format": "date-time", - "title": "Created At" - }, - "updated_at": { - "type": "string", - "format": "date-time", - "title": "Updated At" - } - }, - "type": "object", - "required": [ - "user_id", - "channel", - "severities", - "include_tags", - "exclude_tags", - "enabled", - "webhook_url", - "slack_webhook", - "quiet_hours_enabled", - "quiet_hours_start", - "quiet_hours_end", - "timezone", - "batch_interval_minutes", - "created_at", - "updated_at" - ], - "title": "NotificationSubscription", - "description": "User subscription preferences for notifications" - }, - "PasswordResetRequest": { - "properties": { - "new_password": { - "type": "string", - "minLength": 8, - "format": "password", - "title": "New Password", - "description": "New password for the user", - "writeOnly": true + "title": "Action" } }, "type": "object", "required": [ - "new_password" + "event_id", + "event_type", + "event_version", + "timestamp", + "metadata" ], - "title": "PasswordResetRequest", - "description": "Request model for password reset" + "title": "NotificationClickedEvent" }, - "PodCreatedEvent": { + "NotificationCreatedEvent": { "properties": { "event_id": { "type": "string", @@ -9044,9 +7543,9 @@ }, "event_type": { "type": "string", - "const": "pod_created", + "const": "notification_created", "title": "Event Type", - "default": "pod_created" + "default": "notification_created" }, "event_version": { "type": "string", @@ -9072,18 +7571,38 @@ "metadata": { "$ref": "#/components/schemas/EventMetadata" }, - "execution_id": { + "notification_id": { "type": "string", - "title": "Execution Id" + "title": "Notification Id" }, - "pod_name": { + "user_id": { "type": "string", - "title": "Pod Name" + "title": "User Id" }, - "namespace": { + "subject": { "type": "string", - "title": "Namespace", - "default": "default" + "title": "Subject" + }, + "body": { + "type": "string", + "title": "Body" + }, + "severity": { + "$ref": "#/components/schemas/NotificationSeverity" + }, + "tags": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Tags" + }, + "channels": { + "items": { + "$ref": "#/components/schemas/NotificationChannel" + }, + "type": "array", + "title": "Channels" } }, "type": "object", @@ -9094,9 +7613,9 @@ "timestamp", "metadata" ], - "title": "PodCreatedEvent" + "title": "NotificationCreatedEvent" }, - "PodDeletedEvent": { + "NotificationDeliveredEvent": { "properties": { "event_id": { "type": "string", @@ -9104,9 +7623,9 @@ }, "event_type": { "type": "string", - "const": "pod_deleted", + "const": "notification_delivered", "title": "Event Type", - "default": "pod_deleted" + "default": "notification_delivered" }, "event_version": { "type": "string", @@ -9132,24 +7651,21 @@ "metadata": { "$ref": "#/components/schemas/EventMetadata" }, - "execution_id": { + "notification_id": { "type": "string", - "title": "Execution Id" + "title": "Notification Id" }, - "pod_name": { + "user_id": { "type": "string", - "title": "Pod Name" + "title": "User Id" }, - "reason": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Reason" + "channel": { + "$ref": "#/components/schemas/NotificationChannel" + }, + "delivered_at": { + "type": "string", + "format": "date-time", + "title": "Delivered At" } }, "type": "object", @@ -9160,9 +7676,9 @@ "timestamp", "metadata" ], - "title": "PodDeletedEvent" + "title": "NotificationDeliveredEvent" }, - "PodFailedEvent": { + "NotificationFailedEvent": { "properties": { "event_id": { "type": "string", @@ -9170,9 +7686,9 @@ }, "event_type": { "type": "string", - "const": "pod_failed", + "const": "notification_failed", "title": "Event Type", - "default": "pod_failed" + "default": "notification_failed" }, "event_version": { "type": "string", @@ -9198,53 +7714,87 @@ "metadata": { "$ref": "#/components/schemas/EventMetadata" }, - "execution_id": { + "notification_id": { "type": "string", - "title": "Execution Id" + "title": "Notification Id" }, - "pod_name": { + "user_id": { "type": "string", - "title": "Pod Name" + "title": "User Id" }, - "exit_code": { + "channel": { + "$ref": "#/components/schemas/NotificationChannel" + }, + "error": { + "type": "string", + "title": "Error" + }, + "retry_count": { "type": "integer", - "title": "Exit Code", - "default": 1 + "title": "Retry Count", + "default": 0 + } + }, + "type": "object", + "required": [ + "event_id", + "event_type", + "event_version", + "timestamp", + "metadata" + ], + "title": "NotificationFailedEvent" + }, + "NotificationListResponse": { + "properties": { + "notifications": { + "items": { + "$ref": "#/components/schemas/NotificationResponse" + }, + "type": "array", + "title": "Notifications" }, - "reason": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Reason" + "total": { + "type": "integer", + "title": "Total" }, - "message": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Message" + "unread_count": { + "type": "integer", + "title": "Unread Count" + } + }, + "type": "object", + "required": [ + "notifications", + "total", + "unread_count" + ], + "title": "NotificationListResponse", + "description": "Response schema for notification list endpoints" + }, + "NotificationPreferencesUpdatedEvent": { + "properties": { + "event_id": { + "type": "string", + "title": "Event Id" + }, + "event_type": { + "type": "string", + "const": "notification_preferences_updated", + "title": "Event Type", + "default": "notification_preferences_updated" + }, + "event_version": { + "type": "string", + "title": "Event Version", + "default": "1.0" }, - "stdout": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Stdout" + "timestamp": { + "type": "string", + "format": "date-time", + "title": "Timestamp" }, - "stderr": { + "aggregate_id": { "anyOf": [ { "type": "string" @@ -9253,7 +7803,21 @@ "type": "null" } ], - "title": "Stderr" + "title": "Aggregate Id" + }, + "metadata": { + "$ref": "#/components/schemas/EventMetadata" + }, + "user_id": { + "type": "string", + "title": "User Id" + }, + "changed_fields": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Changed Fields" } }, "type": "object", @@ -9264,9 +7828,9 @@ "timestamp", "metadata" ], - "title": "PodFailedEvent" + "title": "NotificationPreferencesUpdatedEvent" }, - "PodRunningEvent": { + "NotificationReadEvent": { "properties": { "event_id": { "type": "string", @@ -9274,9 +7838,9 @@ }, "event_type": { "type": "string", - "const": "pod_running", + "const": "notification_read", "title": "Event Type", - "default": "pod_running" + "default": "notification_read" }, "event_version": { "type": "string", @@ -9302,20 +7866,18 @@ "metadata": { "$ref": "#/components/schemas/EventMetadata" }, - "execution_id": { + "notification_id": { "type": "string", - "title": "Execution Id" + "title": "Notification Id" }, - "pod_name": { + "user_id": { "type": "string", - "title": "Pod Name" + "title": "User Id" }, - "container_statuses": { - "items": { - "$ref": "#/components/schemas/ContainerStatusInfo" - }, - "type": "array", - "title": "Container Statuses" + "read_at": { + "type": "string", + "format": "date-time", + "title": "Read At" } }, "type": "object", @@ -9326,9 +7888,77 @@ "timestamp", "metadata" ], - "title": "PodRunningEvent" + "title": "NotificationReadEvent" }, - "PodScheduledEvent": { + "NotificationResponse": { + "properties": { + "notification_id": { + "type": "string", + "title": "Notification Id" + }, + "channel": { + "$ref": "#/components/schemas/NotificationChannel" + }, + "status": { + "$ref": "#/components/schemas/NotificationStatus" + }, + "subject": { + "type": "string", + "title": "Subject" + }, + "body": { + "type": "string", + "title": "Body" + }, + "action_url": { + "type": "string", + "title": "Action Url" + }, + "created_at": { + "type": "string", + "format": "date-time", + "title": "Created At" + }, + "read_at": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "Read At" + }, + "severity": { + "$ref": "#/components/schemas/NotificationSeverity" + }, + "tags": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Tags" + } + }, + "type": "object", + "required": [ + "notification_id", + "channel", + "status", + "subject", + "body", + "action_url", + "created_at", + "read_at", + "severity", + "tags" + ], + "title": "NotificationResponse", + "description": "Response schema for notification endpoints" + }, + "NotificationSentEvent": { "properties": { "event_id": { "type": "string", @@ -9336,9 +7966,9 @@ }, "event_type": { "type": "string", - "const": "pod_scheduled", + "const": "notification_sent", "title": "Event Type", - "default": "pod_scheduled" + "default": "notification_sent" }, "event_version": { "type": "string", @@ -9364,18 +7994,21 @@ "metadata": { "$ref": "#/components/schemas/EventMetadata" }, - "execution_id": { + "notification_id": { "type": "string", - "title": "Execution Id" + "title": "Notification Id" }, - "pod_name": { + "user_id": { "type": "string", - "title": "Pod Name" + "title": "User Id" }, - "node_name": { + "channel": { + "$ref": "#/components/schemas/NotificationChannel" + }, + "sent_at": { "type": "string", - "title": "Node Name", - "default": "" + "format": "date-time", + "title": "Sent At" } }, "type": "object", @@ -9386,31 +8019,151 @@ "timestamp", "metadata" ], - "title": "PodScheduledEvent" + "title": "NotificationSentEvent" + }, + "NotificationSettings-Input": { + "properties": { + "execution_completed": { + "type": "boolean", + "title": "Execution Completed", + "default": true + }, + "execution_failed": { + "type": "boolean", + "title": "Execution Failed", + "default": true + }, + "system_updates": { + "type": "boolean", + "title": "System Updates", + "default": true + }, + "security_alerts": { + "type": "boolean", + "title": "Security Alerts", + "default": true + }, + "channels": { + "items": { + "$ref": "#/components/schemas/NotificationChannel" + }, + "type": "array", + "title": "Channels", + "default": [ + "in_app" + ] + } + }, + "type": "object", + "title": "NotificationSettings", + "description": "User notification preferences" + }, + "NotificationSettings-Output": { + "properties": { + "execution_completed": { + "type": "boolean", + "title": "Execution Completed", + "default": true + }, + "execution_failed": { + "type": "boolean", + "title": "Execution Failed", + "default": true + }, + "system_updates": { + "type": "boolean", + "title": "System Updates", + "default": true + }, + "security_alerts": { + "type": "boolean", + "title": "Security Alerts", + "default": true + }, + "channels": { + "items": { + "$ref": "#/components/schemas/NotificationChannel" + }, + "type": "array", + "title": "Channels", + "default": [ + "in_app" + ] + } + }, + "type": "object", + "required": [ + "execution_completed", + "execution_failed", + "system_updates", + "security_alerts", + "channels" + ], + "title": "NotificationSettings", + "description": "User notification preferences" + }, + "NotificationSeverity": { + "type": "string", + "enum": [ + "low", + "medium", + "high", + "urgent" + ], + "title": "NotificationSeverity", + "description": "Notification severity levels." + }, + "NotificationStatus": { + "type": "string", + "enum": [ + "pending", + "queued", + "sending", + "delivered", + "failed", + "skipped", + "read", + "clicked" + ], + "title": "NotificationStatus", + "description": "Notification delivery status." }, - "PodSucceededEvent": { + "NotificationSubscription": { "properties": { - "event_id": { + "user_id": { "type": "string", - "title": "Event Id" + "title": "User Id" }, - "event_type": { - "type": "string", - "const": "pod_succeeded", - "title": "Event Type", - "default": "pod_succeeded" + "channel": { + "$ref": "#/components/schemas/NotificationChannel" }, - "event_version": { - "type": "string", - "title": "Event Version", - "default": "1.0" + "severities": { + "items": { + "$ref": "#/components/schemas/NotificationSeverity" + }, + "type": "array", + "title": "Severities" }, - "timestamp": { - "type": "string", - "format": "date-time", - "title": "Timestamp" + "include_tags": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Include Tags" }, - "aggregate_id": { + "exclude_tags": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Exclude Tags" + }, + "enabled": { + "type": "boolean", + "title": "Enabled", + "default": true + }, + "webhook_url": { "anyOf": [ { "type": "string" @@ -9419,25 +8172,9 @@ "type": "null" } ], - "title": "Aggregate Id" - }, - "metadata": { - "$ref": "#/components/schemas/EventMetadata" - }, - "execution_id": { - "type": "string", - "title": "Execution Id" - }, - "pod_name": { - "type": "string", - "title": "Pod Name" - }, - "exit_code": { - "type": "integer", - "title": "Exit Code", - "default": 0 + "title": "Webhook Url" }, - "stdout": { + "slack_webhook": { "anyOf": [ { "type": "string" @@ -9446,9 +8183,14 @@ "type": "null" } ], - "title": "Stdout" + "title": "Slack Webhook" }, - "stderr": { + "quiet_hours_enabled": { + "type": "boolean", + "title": "Quiet Hours Enabled", + "default": false + }, + "quiet_hours_start": { "anyOf": [ { "type": "string" @@ -9457,42 +8199,9 @@ "type": "null" } ], - "title": "Stderr" - } - }, - "type": "object", - "required": [ - "event_id", - "event_type", - "event_version", - "timestamp", - "metadata" - ], - "title": "PodSucceededEvent" - }, - "PodTerminatedEvent": { - "properties": { - "event_id": { - "type": "string", - "title": "Event Id" - }, - "event_type": { - "type": "string", - "const": "pod_terminated", - "title": "Event Type", - "default": "pod_terminated" - }, - "event_version": { - "type": "string", - "title": "Event Version", - "default": "1.0" - }, - "timestamp": { - "type": "string", - "format": "date-time", - "title": "Timestamp" + "title": "Quiet Hours Start" }, - "aggregate_id": { + "quiet_hours_end": { "anyOf": [ { "type": "string" @@ -9501,120 +8210,69 @@ "type": "null" } ], - "title": "Aggregate Id" - }, - "metadata": { - "$ref": "#/components/schemas/EventMetadata" - }, - "execution_id": { - "type": "string", - "title": "Execution Id" + "title": "Quiet Hours End" }, - "pod_name": { + "timezone": { "type": "string", - "title": "Pod Name" + "title": "Timezone", + "default": "UTC" }, - "exit_code": { + "batch_interval_minutes": { "type": "integer", - "title": "Exit Code", - "default": 0 + "title": "Batch Interval Minutes", + "default": 60 }, - "reason": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Reason" + "created_at": { + "type": "string", + "format": "date-time", + "title": "Created At" }, - "message": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Message" - } - }, - "type": "object", - "required": [ - "event_id", - "event_type", - "event_version", - "timestamp", - "metadata" - ], - "title": "PodTerminatedEvent" - }, - "PriorityUpdateRequest": { - "properties": { - "priority": { - "$ref": "#/components/schemas/QueuePriority", - "description": "New priority level" + "updated_at": { + "type": "string", + "format": "date-time", + "title": "Updated At" } }, "type": "object", "required": [ - "priority" - ], - "title": "PriorityUpdateRequest", - "description": "Request to update execution priority." - }, - "QueuePriority": { - "type": "string", - "enum": [ - "critical", - "high", - "normal", - "low", - "background" + "user_id", + "channel", + "severities", + "include_tags", + "exclude_tags", + "enabled", + "webhook_url", + "slack_webhook", + "quiet_hours_enabled", + "quiet_hours_start", + "quiet_hours_end", + "timezone", + "batch_interval_minutes", + "created_at", + "updated_at" ], - "title": "QueuePriority", - "description": "Execution priority, ordered highest to lowest." + "title": "NotificationSubscription", + "description": "User subscription preferences for notifications" }, - "QueueStatusResponse": { + "PasswordResetRequest": { "properties": { - "queue_depth": { - "type": "integer", - "title": "Queue Depth", - "description": "Number of pending executions" - }, - "active_count": { - "type": "integer", - "title": "Active Count", - "description": "Number of actively running executions" - }, - "max_concurrent": { - "type": "integer", - "title": "Max Concurrent", - "description": "Maximum concurrent executions allowed" - }, - "by_priority": { - "additionalProperties": { - "type": "integer" - }, - "type": "object", - "title": "By Priority", - "description": "Pending count per priority" + "new_password": { + "type": "string", + "minLength": 8, + "format": "password", + "title": "New Password", + "description": "New password for the user", + "writeOnly": true } }, "type": "object", "required": [ - "queue_depth", - "active_count", - "max_concurrent", - "by_priority" - ], - "title": "QueueStatusResponse", - "description": "Queue status summary." + "new_password" + ], + "title": "PasswordResetRequest", + "description": "Request model for password reset" }, - "QuotaExceededEvent": { + "PodCreatedEvent": { "properties": { "event_id": { "type": "string", @@ -9622,9 +8280,9 @@ }, "event_type": { "type": "string", - "const": "quota_exceeded", + "const": "pod_created", "title": "Event Type", - "default": "quota_exceeded" + "default": "pod_created" }, "event_version": { "type": "string", @@ -9650,21 +8308,18 @@ "metadata": { "$ref": "#/components/schemas/EventMetadata" }, - "quota_type": { + "execution_id": { "type": "string", - "title": "Quota Type" - }, - "limit": { - "type": "integer", - "title": "Limit" + "title": "Execution Id" }, - "current_usage": { - "type": "integer", - "title": "Current Usage" + "pod_name": { + "type": "string", + "title": "Pod Name" }, - "user_id": { + "namespace": { "type": "string", - "title": "User Id" + "title": "Namespace", + "default": "default" } }, "type": "object", @@ -9675,19 +8330,9 @@ "timestamp", "metadata" ], - "title": "QuotaExceededEvent" - }, - "RateLimitAlgorithm": { - "type": "string", - "enum": [ - "sliding_window", - "token_bucket", - "fixed_window", - "leaky_bucket" - ], - "title": "RateLimitAlgorithm" + "title": "PodCreatedEvent" }, - "RateLimitExceededEvent": { + "PodDeletedEvent": { "properties": { "event_id": { "type": "string", @@ -9695,9 +8340,9 @@ }, "event_type": { "type": "string", - "const": "rate_limit_exceeded", + "const": "pod_deleted", "title": "Event Type", - "default": "rate_limit_exceeded" + "default": "pod_deleted" }, "event_version": { "type": "string", @@ -9723,7 +8368,15 @@ "metadata": { "$ref": "#/components/schemas/EventMetadata" }, - "user_id": { + "execution_id": { + "type": "string", + "title": "Execution Id" + }, + "pod_name": { + "type": "string", + "title": "Pod Name" + }, + "reason": { "anyOf": [ { "type": "string" @@ -9732,19 +8385,7 @@ "type": "null" } ], - "title": "User Id" - }, - "endpoint": { - "type": "string", - "title": "Endpoint" - }, - "limit": { - "type": "integer", - "title": "Limit" - }, - "window_seconds": { - "type": "integer", - "title": "Window Seconds" + "title": "Reason" } }, "type": "object", @@ -9755,185 +8396,113 @@ "timestamp", "metadata" ], - "title": "RateLimitExceededEvent" + "title": "PodDeletedEvent" }, - "RateLimitRuleRequest": { + "PodFailedEvent": { "properties": { - "endpoint_pattern": { + "event_id": { "type": "string", - "minLength": 1, - "title": "Endpoint Pattern", - "default": "" - }, - "group": { - "$ref": "#/components/schemas/EndpointGroup", - "default": "api" - }, - "requests": { - "type": "integer", - "minimum": 1.0, - "title": "Requests", - "default": 60 - }, - "window_seconds": { - "type": "integer", - "minimum": 1.0, - "title": "Window Seconds", - "default": 60 - }, - "algorithm": { - "$ref": "#/components/schemas/RateLimitAlgorithm", - "default": "sliding_window" - }, - "burst_multiplier": { - "type": "number", - "minimum": 1.0, - "title": "Burst Multiplier", - "default": 1.5 + "title": "Event Id" }, - "priority": { - "type": "integer", - "title": "Priority", - "default": 0 + "event_type": { + "type": "string", + "const": "pod_failed", + "title": "Event Type", + "default": "pod_failed" }, - "enabled": { - "type": "boolean", - "title": "Enabled", - "default": true - } - }, - "type": "object", - "title": "RateLimitRuleRequest", - "description": "Request model for rate limit rule." - }, - "RateLimitRuleResponse": { - "properties": { - "endpoint_pattern": { + "event_version": { "type": "string", - "title": "Endpoint Pattern" + "title": "Event Version", + "default": "1.0" }, - "group": { - "$ref": "#/components/schemas/EndpointGroup" + "timestamp": { + "type": "string", + "format": "date-time", + "title": "Timestamp" }, - "requests": { - "type": "integer", - "title": "Requests" + "aggregate_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Aggregate Id" }, - "window_seconds": { - "type": "integer", - "title": "Window Seconds" + "metadata": { + "$ref": "#/components/schemas/EventMetadata" }, - "algorithm": { - "$ref": "#/components/schemas/RateLimitAlgorithm" + "execution_id": { + "type": "string", + "title": "Execution Id" }, - "burst_multiplier": { - "type": "number", - "title": "Burst Multiplier", - "default": 1.5 + "pod_name": { + "type": "string", + "title": "Pod Name" }, - "priority": { + "exit_code": { "type": "integer", - "title": "Priority", - "default": 0 - }, - "enabled": { - "type": "boolean", - "title": "Enabled", - "default": true - } - }, - "type": "object", - "required": [ - "endpoint_pattern", - "group", - "requests", - "window_seconds", - "algorithm", - "burst_multiplier", - "priority", - "enabled" - ], - "title": "RateLimitRuleResponse", - "description": "Response model for rate limit rule." - }, - "RateLimitSummary": { - "properties": { - "bypass_rate_limit": { - "type": "boolean", - "title": "Bypass Rate Limit" - }, - "global_multiplier": { - "type": "number", - "title": "Global Multiplier" - }, - "has_custom_limits": { - "type": "boolean", - "title": "Has Custom Limits" - } - }, - "type": "object", - "required": [ - "bypass_rate_limit", - "global_multiplier", - "has_custom_limits" - ], - "title": "RateLimitSummary" - }, - "RateLimitUpdateRequest": { - "properties": { - "bypass_rate_limit": { - "type": "boolean", - "title": "Bypass Rate Limit", - "default": false - }, - "global_multiplier": { - "type": "number", - "exclusiveMinimum": 0.0, - "title": "Global Multiplier", - "default": 1.0 - }, - "rules": { - "items": { - "$ref": "#/components/schemas/RateLimitRuleRequest" - }, - "type": "array", - "title": "Rules", - "default": [] + "title": "Exit Code", + "default": 1 }, - "notes": { - "type": "string", - "title": "Notes", - "default": "" - } - }, - "type": "object", - "title": "RateLimitUpdateRequest", - "description": "Request model for updating user rate limits." - }, - "RateLimitUpdateResponse": { - "properties": { - "user_id": { - "type": "string", - "title": "User Id" + "reason": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Reason" }, - "updated": { - "type": "boolean", - "title": "Updated" + "message": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Message" }, - "config": { - "$ref": "#/components/schemas/UserRateLimitConfigResponse" + "stdout": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Stdout" + }, + "stderr": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Stderr" } }, "type": "object", "required": [ - "user_id", - "updated", - "config" + "event_id", + "event_type", + "event_version", + "timestamp", + "metadata" ], - "title": "RateLimitUpdateResponse", - "description": "Response model for rate limit update." + "title": "PodFailedEvent" }, - "ReleaseResourcesCommandEvent": { + "PodRunningEvent": { "properties": { "event_id": { "type": "string", @@ -9941,9 +8510,9 @@ }, "event_type": { "type": "string", - "const": "release_resources_command", + "const": "pod_running", "title": "Event Type", - "default": "release_resources_command" + "default": "pod_running" }, "event_version": { "type": "string", @@ -9973,13 +8542,16 @@ "type": "string", "title": "Execution Id" }, - "cpu_request": { + "pod_name": { "type": "string", - "title": "Cpu Request" + "title": "Pod Name" }, - "memory_request": { - "type": "string", - "title": "Memory Request" + "container_statuses": { + "items": { + "$ref": "#/components/schemas/ContainerStatusInfo" + }, + "type": "array", + "title": "Container Statuses" } }, "type": "object", @@ -9990,52 +8562,31 @@ "timestamp", "metadata" ], - "title": "ReleaseResourcesCommandEvent" + "title": "PodRunningEvent" }, - "ReplayConfigSchema": { + "PodScheduledEvent": { "properties": { - "replay_type": { - "$ref": "#/components/schemas/ReplayType" - }, - "target": { - "$ref": "#/components/schemas/ReplayTarget", - "default": "kafka" - }, - "filter": { - "$ref": "#/components/schemas/ReplayFilterSchema" - }, - "speed_multiplier": { - "type": "number", - "maximum": 100.0, - "minimum": 0.1, - "title": "Speed Multiplier", - "default": 1.0 + "event_id": { + "type": "string", + "title": "Event Id" }, - "preserve_timestamps": { - "type": "boolean", - "title": "Preserve Timestamps", - "default": false + "event_type": { + "type": "string", + "const": "pod_scheduled", + "title": "Event Type", + "default": "pod_scheduled" }, - "batch_size": { - "type": "integer", - "maximum": 1000.0, - "minimum": 1.0, - "title": "Batch Size", - "default": 100 + "event_version": { + "type": "string", + "title": "Event Version", + "default": "1.0" }, - "max_events": { - "anyOf": [ - { - "type": "integer", - "minimum": 1.0 - }, - { - "type": "null" - } - ], - "title": "Max Events" + "timestamp": { + "type": "string", + "format": "date-time", + "title": "Timestamp" }, - "target_file_path": { + "aggregate_id": { "anyOf": [ { "type": "string" @@ -10044,116 +8595,56 @@ "type": "null" } ], - "title": "Target File Path" + "title": "Aggregate Id" }, - "skip_errors": { - "type": "boolean", - "title": "Skip Errors", - "default": true + "metadata": { + "$ref": "#/components/schemas/EventMetadata" }, - "retry_failed": { - "type": "boolean", - "title": "Retry Failed", - "default": false + "execution_id": { + "type": "string", + "title": "Execution Id" }, - "retry_attempts": { - "type": "integer", - "title": "Retry Attempts", - "default": 3 + "pod_name": { + "type": "string", + "title": "Pod Name" }, - "enable_progress_tracking": { - "type": "boolean", - "title": "Enable Progress Tracking", - "default": true + "node_name": { + "type": "string", + "title": "Node Name", + "default": "" } }, "type": "object", "required": [ - "replay_type", - "target", - "filter", - "speed_multiplier", - "preserve_timestamps", - "batch_size", - "max_events", - "target_file_path", - "skip_errors", - "retry_failed", - "retry_attempts", - "enable_progress_tracking" + "event_id", + "event_type", + "event_version", + "timestamp", + "metadata" ], - "title": "ReplayConfigSchema" + "title": "PodScheduledEvent" }, - "ReplayError": { + "PodSucceededEvent": { "properties": { - "timestamp": { + "event_id": { "type": "string", - "format": "date-time", - "title": "Timestamp" + "title": "Event Id" }, - "error": { + "event_type": { "type": "string", - "title": "Error" - }, - "error_type": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Error Type" + "const": "pod_succeeded", + "title": "Event Type", + "default": "pod_succeeded" }, - "event_id": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Event Id" - } - }, - "type": "object", - "required": [ - "timestamp", - "error", - "error_type", - "event_id" - ], - "title": "ReplayError", - "description": "Error details for replay operations.\n\nAttributes:\n timestamp: When the error occurred.\n error: Human-readable error message.\n error_type: Python exception class name (e.g., \"ValueError\", \"KafkaException\").\n This is the result of `type(exception).__name__`, NOT the ErrorType enum.\n Present for session-level errors.\n event_id: ID of the event that failed to replay. Present for event-level errors." - }, - "ReplayFilter": { - "properties": { - "event_ids": { - "anyOf": [ - { - "items": { - "type": "string" - }, - "type": "array" - }, - { - "type": "null" - } - ], - "title": "Event Ids" + "event_version": { + "type": "string", + "title": "Event Version", + "default": "1.0" }, - "execution_id": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Execution Id" + "timestamp": { + "type": "string", + "format": "date-time", + "title": "Timestamp" }, "aggregate_id": { "anyOf": [ @@ -10166,59 +8657,105 @@ ], "title": "Aggregate Id" }, - "event_types": { - "anyOf": [ - { - "items": { - "$ref": "#/components/schemas/EventType" - }, - "type": "array" - }, - { - "type": "null" - } - ], - "title": "Event Types" + "metadata": { + "$ref": "#/components/schemas/EventMetadata" }, - "exclude_event_types": { + "execution_id": { + "type": "string", + "title": "Execution Id" + }, + "pod_name": { + "type": "string", + "title": "Pod Name" + }, + "exit_code": { + "type": "integer", + "title": "Exit Code", + "default": 0 + }, + "stdout": { "anyOf": [ { - "items": { - "$ref": "#/components/schemas/EventType" - }, - "type": "array" + "type": "string" }, { "type": "null" } ], - "title": "Exclude Event Types" + "title": "Stdout" }, - "start_time": { + "stderr": { "anyOf": [ { - "type": "string", - "format": "date-time" + "type": "string" }, { "type": "null" } ], - "title": "Start Time" + "title": "Stderr" + } + }, + "type": "object", + "required": [ + "event_id", + "event_type", + "event_version", + "timestamp", + "metadata" + ], + "title": "PodSucceededEvent" + }, + "PodTerminatedEvent": { + "properties": { + "event_id": { + "type": "string", + "title": "Event Id" }, - "end_time": { + "event_type": { + "type": "string", + "const": "pod_terminated", + "title": "Event Type", + "default": "pod_terminated" + }, + "event_version": { + "type": "string", + "title": "Event Version", + "default": "1.0" + }, + "timestamp": { + "type": "string", + "format": "date-time", + "title": "Timestamp" + }, + "aggregate_id": { "anyOf": [ { - "type": "string", - "format": "date-time" + "type": "string" }, { "type": "null" } ], - "title": "End Time" + "title": "Aggregate Id" }, - "user_id": { + "metadata": { + "$ref": "#/components/schemas/EventMetadata" + }, + "execution_id": { + "type": "string", + "title": "Execution Id" + }, + "pod_name": { + "type": "string", + "title": "Pod Name" + }, + "exit_code": { + "type": "integer", + "title": "Exit Code", + "default": 0 + }, + "reason": { "anyOf": [ { "type": "string" @@ -10227,9 +8764,9 @@ "type": "null" } ], - "title": "User Id" + "title": "Reason" }, - "service_name": { + "message": { "anyOf": [ { "type": "string" @@ -10238,38 +8775,175 @@ "type": "null" } ], - "title": "Service Name" + "title": "Message" } }, "type": "object", - "title": "ReplayFilter" + "required": [ + "event_id", + "event_type", + "event_version", + "timestamp", + "metadata" + ], + "title": "PodTerminatedEvent" }, - "ReplayFilterSchema": { + "PriorityUpdateRequest": { "properties": { - "event_ids": { + "priority": { + "$ref": "#/components/schemas/QueuePriority", + "description": "New priority level" + } + }, + "type": "object", + "required": [ + "priority" + ], + "title": "PriorityUpdateRequest", + "description": "Request to update execution priority." + }, + "QueuePriority": { + "type": "string", + "enum": [ + "critical", + "high", + "normal", + "low", + "background" + ], + "title": "QueuePriority", + "description": "Execution priority, ordered highest to lowest." + }, + "QueueStatusResponse": { + "properties": { + "queue_depth": { + "type": "integer", + "title": "Queue Depth", + "description": "Number of pending executions" + }, + "active_count": { + "type": "integer", + "title": "Active Count", + "description": "Number of actively running executions" + }, + "max_concurrent": { + "type": "integer", + "title": "Max Concurrent", + "description": "Maximum concurrent executions allowed" + }, + "by_priority": { + "additionalProperties": { + "type": "integer" + }, + "type": "object", + "title": "By Priority", + "description": "Pending count per priority" + } + }, + "type": "object", + "required": [ + "queue_depth", + "active_count", + "max_concurrent", + "by_priority" + ], + "title": "QueueStatusResponse", + "description": "Queue status summary." + }, + "QuotaExceededEvent": { + "properties": { + "event_id": { + "type": "string", + "title": "Event Id" + }, + "event_type": { + "type": "string", + "const": "quota_exceeded", + "title": "Event Type", + "default": "quota_exceeded" + }, + "event_version": { + "type": "string", + "title": "Event Version", + "default": "1.0" + }, + "timestamp": { + "type": "string", + "format": "date-time", + "title": "Timestamp" + }, + "aggregate_id": { "anyOf": [ { - "items": { - "type": "string" - }, - "type": "array" + "type": "string" }, { "type": "null" } ], - "title": "Event Ids" + "title": "Aggregate Id" + }, + "metadata": { + "$ref": "#/components/schemas/EventMetadata" + }, + "quota_type": { + "type": "string", + "title": "Quota Type" + }, + "limit": { + "type": "integer", + "title": "Limit" + }, + "current_usage": { + "type": "integer", + "title": "Current Usage" + }, + "user_id": { + "type": "string", + "title": "User Id" + } + }, + "type": "object", + "required": [ + "event_id", + "event_type", + "event_version", + "timestamp", + "metadata" + ], + "title": "QuotaExceededEvent" + }, + "RateLimitAlgorithm": { + "type": "string", + "enum": [ + "sliding_window", + "token_bucket", + "fixed_window", + "leaky_bucket" + ], + "title": "RateLimitAlgorithm" + }, + "RateLimitExceededEvent": { + "properties": { + "event_id": { + "type": "string", + "title": "Event Id" }, - "execution_id": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Execution Id" + "event_type": { + "type": "string", + "const": "rate_limit_exceeded", + "title": "Event Type", + "default": "rate_limit_exceeded" + }, + "event_version": { + "type": "string", + "title": "Event Version", + "default": "1.0" + }, + "timestamp": { + "type": "string", + "format": "date-time", + "title": "Timestamp" }, "aggregate_id": { "anyOf": [ @@ -10282,96 +8956,220 @@ ], "title": "Aggregate Id" }, - "event_types": { - "anyOf": [ - { - "items": { - "$ref": "#/components/schemas/EventType" - }, - "type": "array" - }, - { - "type": "null" - } - ], - "title": "Event Types" + "metadata": { + "$ref": "#/components/schemas/EventMetadata" }, - "exclude_event_types": { + "user_id": { "anyOf": [ { - "items": { - "$ref": "#/components/schemas/EventType" - }, - "type": "array" + "type": "string" }, { "type": "null" } ], - "title": "Exclude Event Types" + "title": "User Id" }, - "start_time": { - "anyOf": [ - { - "type": "string", - "format": "date-time" - }, - { - "type": "null" - } - ], - "title": "Start Time" + "endpoint": { + "type": "string", + "title": "Endpoint" }, - "end_time": { - "anyOf": [ - { - "type": "string", - "format": "date-time" - }, - { - "type": "null" - } - ], - "title": "End Time" + "limit": { + "type": "integer", + "title": "Limit" }, - "user_id": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], + "window_seconds": { + "type": "integer", + "title": "Window Seconds" + } + }, + "type": "object", + "required": [ + "event_id", + "event_type", + "event_version", + "timestamp", + "metadata" + ], + "title": "RateLimitExceededEvent" + }, + "RateLimitRuleRequest": { + "properties": { + "endpoint_pattern": { + "type": "string", + "minLength": 1, + "title": "Endpoint Pattern", + "default": "" + }, + "group": { + "$ref": "#/components/schemas/EndpointGroup", + "default": "api" + }, + "requests": { + "type": "integer", + "minimum": 1.0, + "title": "Requests", + "default": 60 + }, + "window_seconds": { + "type": "integer", + "minimum": 1.0, + "title": "Window Seconds", + "default": 60 + }, + "algorithm": { + "$ref": "#/components/schemas/RateLimitAlgorithm", + "default": "sliding_window" + }, + "burst_multiplier": { + "type": "number", + "minimum": 1.0, + "title": "Burst Multiplier", + "default": 1.5 + }, + "priority": { + "type": "integer", + "title": "Priority", + "default": 0 + }, + "enabled": { + "type": "boolean", + "title": "Enabled", + "default": true + } + }, + "type": "object", + "title": "RateLimitRuleRequest", + "description": "Request model for rate limit rule." + }, + "RateLimitRuleResponse": { + "properties": { + "endpoint_pattern": { + "type": "string", + "title": "Endpoint Pattern" + }, + "group": { + "$ref": "#/components/schemas/EndpointGroup" + }, + "requests": { + "type": "integer", + "title": "Requests" + }, + "window_seconds": { + "type": "integer", + "title": "Window Seconds" + }, + "algorithm": { + "$ref": "#/components/schemas/RateLimitAlgorithm" + }, + "burst_multiplier": { + "type": "number", + "title": "Burst Multiplier", + "default": 1.5 + }, + "priority": { + "type": "integer", + "title": "Priority", + "default": 0 + }, + "enabled": { + "type": "boolean", + "title": "Enabled", + "default": true + } + }, + "type": "object", + "required": [ + "endpoint_pattern", + "group", + "requests", + "window_seconds", + "algorithm", + "burst_multiplier", + "priority", + "enabled" + ], + "title": "RateLimitRuleResponse", + "description": "Response model for rate limit rule." + }, + "RateLimitSummary": { + "properties": { + "bypass_rate_limit": { + "type": "boolean", + "title": "Bypass Rate Limit" + }, + "global_multiplier": { + "type": "number", + "title": "Global Multiplier" + }, + "has_custom_limits": { + "type": "boolean", + "title": "Has Custom Limits" + } + }, + "type": "object", + "required": [ + "bypass_rate_limit", + "global_multiplier", + "has_custom_limits" + ], + "title": "RateLimitSummary" + }, + "RateLimitUpdateRequest": { + "properties": { + "bypass_rate_limit": { + "type": "boolean", + "title": "Bypass Rate Limit", + "default": false + }, + "global_multiplier": { + "type": "number", + "exclusiveMinimum": 0.0, + "title": "Global Multiplier", + "default": 1.0 + }, + "rules": { + "items": { + "$ref": "#/components/schemas/RateLimitRuleRequest" + }, + "type": "array", + "title": "Rules", + "default": [] + }, + "notes": { + "type": "string", + "title": "Notes", + "default": "" + } + }, + "type": "object", + "title": "RateLimitUpdateRequest", + "description": "Request model for updating user rate limits." + }, + "RateLimitUpdateResponse": { + "properties": { + "user_id": { + "type": "string", "title": "User Id" }, - "service_name": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Service Name" + "updated": { + "type": "boolean", + "title": "Updated" + }, + "config": { + "$ref": "#/components/schemas/UserRateLimitConfigResponse" } }, "type": "object", "required": [ - "event_ids", - "execution_id", - "aggregate_id", - "event_types", - "exclude_event_types", - "start_time", - "end_time", "user_id", - "service_name" + "updated", + "config" ], - "title": "ReplayFilterSchema" + "title": "RateLimitUpdateResponse", + "description": "Response model for rate limit update." }, - "ReplayRequest": { + "ReplayConfigSchema": { "properties": { "replay_type": { "$ref": "#/components/schemas/ReplayType" @@ -10381,7 +9179,7 @@ "default": "kafka" }, "filter": { - "$ref": "#/components/schemas/ReplayFilter" + "$ref": "#/components/schemas/ReplayFilterSchema" }, "speed_multiplier": { "type": "number", @@ -10414,11 +9212,6 @@ ], "title": "Max Events" }, - "skip_errors": { - "type": "boolean", - "title": "Skip Errors", - "default": true - }, "target_file_path": { "anyOf": [ { @@ -10430,6 +9223,11 @@ ], "title": "Target File Path" }, + "skip_errors": { + "type": "boolean", + "title": "Skip Errors", + "default": true + }, "retry_failed": { "type": "boolean", "title": "Retry Failed", @@ -10437,8 +9235,6 @@ }, "retry_attempts": { "type": "integer", - "maximum": 10.0, - "minimum": 1.0, "title": "Retry Attempts", "default": 3 }, @@ -10450,215 +9246,154 @@ }, "type": "object", "required": [ - "replay_type" + "replay_type", + "target", + "filter", + "speed_multiplier", + "preserve_timestamps", + "batch_size", + "max_events", + "target_file_path", + "skip_errors", + "retry_failed", + "retry_attempts", + "enable_progress_tracking" ], - "title": "ReplayRequest", - "description": "Request schema for creating replay sessions" + "title": "ReplayConfigSchema" }, - "ReplayResponse": { + "ReplayError": { "properties": { - "session_id": { + "timestamp": { "type": "string", - "title": "Session Id" - }, - "status": { - "$ref": "#/components/schemas/ReplayStatus" + "format": "date-time", + "title": "Timestamp" }, - "message": { + "error": { "type": "string", - "title": "Message" + "title": "Error" + }, + "error_type": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Error Type" + }, + "event_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Event Id" } }, "type": "object", "required": [ - "session_id", - "status", - "message" + "timestamp", + "error", + "error_type", + "event_id" ], - "title": "ReplayResponse", - "description": "Response schema for replay operations" + "title": "ReplayError", + "description": "Error details for replay operations.\n\nAttributes:\n timestamp: When the error occurred.\n error: Human-readable error message.\n error_type: Python exception class name (e.g., \"ValueError\", \"KafkaException\").\n This is the result of `type(exception).__name__`, NOT the ErrorType enum.\n Present for session-level errors.\n event_id: ID of the event that failed to replay. Present for event-level errors." }, - "ReplaySession": { + "ReplayFilter": { "properties": { - "session_id": { - "type": "string", - "title": "Session Id" - }, - "config": { - "$ref": "#/components/schemas/ReplayConfigSchema" - }, - "status": { - "$ref": "#/components/schemas/ReplayStatus", - "default": "created" - }, - "total_events": { - "type": "integer", - "title": "Total Events", - "default": 0 - }, - "replayed_events": { - "type": "integer", - "title": "Replayed Events", - "default": 0 - }, - "failed_events": { - "type": "integer", - "title": "Failed Events", - "default": 0 - }, - "skipped_events": { - "type": "integer", - "title": "Skipped Events", - "default": 0 - }, - "created_at": { - "type": "string", - "format": "date-time", - "title": "Created At" - }, - "started_at": { + "event_ids": { "anyOf": [ { - "type": "string", - "format": "date-time" + "items": { + "type": "string" + }, + "type": "array" }, { "type": "null" } ], - "title": "Started At" + "title": "Event Ids" }, - "completed_at": { + "execution_id": { "anyOf": [ { - "type": "string", - "format": "date-time" + "type": "string" }, { "type": "null" } ], - "title": "Completed At" + "title": "Execution Id" }, - "last_event_at": { + "aggregate_id": { "anyOf": [ { - "type": "string", - "format": "date-time" + "type": "string" }, { "type": "null" } ], - "title": "Last Event At" - }, - "errors": { - "items": { - "$ref": "#/components/schemas/ReplayError" - }, - "type": "array", - "title": "Errors" - } - }, - "type": "object", - "required": [ - "session_id", - "config", - "status", - "total_events", - "replayed_events", - "failed_events", - "skipped_events", - "created_at", - "started_at", - "completed_at", - "last_event_at", - "errors" - ], - "title": "ReplaySession" - }, - "ReplayStatus": { - "type": "string", - "enum": [ - "preview", - "scheduled", - "created", - "running", - "paused", - "completed", - "failed", - "cancelled" - ], - "title": "ReplayStatus" - }, - "ReplayTarget": { - "type": "string", - "enum": [ - "kafka", - "callback", - "file", - "test" - ], - "title": "ReplayTarget" - }, - "ReplayType": { - "type": "string", - "enum": [ - "execution", - "time_range", - "event_type", - "query", - "recovery" - ], - "title": "ReplayType" - }, - "ResourceLimitExceededEvent": { - "properties": { - "event_id": { - "type": "string", - "title": "Event Id" - }, - "event_type": { - "type": "string", - "const": "resource_limit_exceeded", - "title": "Event Type", - "default": "resource_limit_exceeded" - }, - "event_version": { - "type": "string", - "title": "Event Version", - "default": "1.0" - }, - "timestamp": { - "type": "string", - "format": "date-time", - "title": "Timestamp" + "title": "Aggregate Id" }, - "aggregate_id": { + "event_types": { "anyOf": [ { - "type": "string" + "items": { + "$ref": "#/components/schemas/EventType" + }, + "type": "array" }, { "type": "null" } ], - "title": "Aggregate Id" - }, - "metadata": { - "$ref": "#/components/schemas/EventMetadata" + "title": "Event Types" }, - "resource_type": { - "type": "string", - "title": "Resource Type" + "exclude_event_types": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/EventType" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Exclude Event Types" }, - "limit": { - "type": "integer", - "title": "Limit" + "start_time": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "Start Time" }, - "requested": { - "type": "integer", - "title": "Requested" + "end_time": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "End Time" }, "user_id": { "anyOf": [ @@ -10670,177 +9405,113 @@ } ], "title": "User Id" - } - }, - "type": "object", - "required": [ - "event_id", - "event_type", - "event_version", - "timestamp", - "metadata" - ], - "title": "ResourceLimitExceededEvent" - }, - "ResourceLimits": { - "properties": { - "cpu_limit": { - "type": "string", - "title": "Cpu Limit" - }, - "memory_limit": { - "type": "string", - "title": "Memory Limit" - }, - "cpu_request": { - "type": "string", - "title": "Cpu Request" - }, - "memory_request": { - "type": "string", - "title": "Memory Request" - }, - "execution_timeout": { - "type": "integer", - "title": "Execution Timeout" - }, - "supported_runtimes": { - "additionalProperties": { - "$ref": "#/components/schemas/LanguageInfo" - }, - "type": "object", - "title": "Supported Runtimes" }, - "language_limits": { - "additionalProperties": { - "$ref": "#/components/schemas/LanguageLimits" - }, - "type": "object", - "title": "Language Limits" + "service_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Service Name" } }, "type": "object", - "required": [ - "cpu_limit", - "memory_limit", - "cpu_request", - "memory_request", - "execution_timeout", - "supported_runtimes", - "language_limits" - ], - "title": "ResourceLimits", - "description": "Model for resource limits configuration." + "title": "ReplayFilter" }, - "ResourceUsage": { + "ReplayFilterSchema": { "properties": { - "execution_time_wall_seconds": { - "type": "number", - "title": "Execution Time Wall Seconds", - "default": 0.0 - }, - "cpu_time_jiffies": { - "type": "integer", - "title": "Cpu Time Jiffies", - "default": 0 + "event_ids": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Event Ids" }, - "clk_tck_hertz": { - "type": "integer", - "title": "Clk Tck Hertz", - "default": 0 + "execution_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Execution Id" }, - "peak_memory_kb": { - "type": "integer", - "title": "Peak Memory Kb", - "default": 0 - } - }, - "type": "object", - "required": [ - "execution_time_wall_seconds", - "cpu_time_jiffies", - "clk_tck_hertz", - "peak_memory_kb" - ], - "title": "ResourceUsage", - "description": "Model for execution resource usage." - }, - "ResourceUsageDomain": { - "properties": { - "execution_time_wall_seconds": { - "type": "number", - "title": "Execution Time Wall Seconds", - "default": 0.0 + "aggregate_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Aggregate Id" }, - "cpu_time_jiffies": { - "type": "integer", - "title": "Cpu Time Jiffies", - "default": 0 + "event_types": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/EventType" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Event Types" }, - "clk_tck_hertz": { - "type": "integer", - "title": "Clk Tck Hertz", - "default": 0 + "exclude_event_types": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/EventType" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Exclude Event Types" }, - "peak_memory_kb": { - "type": "integer", - "title": "Peak Memory Kb", - "default": 0 - } - }, - "type": "object", - "title": "ResourceUsageDomain", - "description": "Resource usage metrics from script execution." - }, - "RestoreSettingsRequest": { - "properties": { - "timestamp": { - "type": "string", - "format": "date-time", - "title": "Timestamp" + "start_time": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "Start Time" }, - "reason": { + "end_time": { "anyOf": [ { - "type": "string" + "type": "string", + "format": "date-time" }, { "type": "null" } ], - "title": "Reason" - } - }, - "type": "object", - "required": [ - "timestamp" - ], - "title": "RestoreSettingsRequest", - "description": "Request model for restoring settings" - }, - "ResultFailedEvent": { - "properties": { - "event_id": { - "type": "string", - "title": "Event Id" - }, - "event_type": { - "type": "string", - "const": "result_failed", - "title": "Event Type", - "default": "result_failed" - }, - "event_version": { - "type": "string", - "title": "Event Version", - "default": "1.0" - }, - "timestamp": { - "type": "string", - "format": "date-time", - "title": "Timestamp" + "title": "End Time" }, - "aggregate_id": { + "user_id": { "anyOf": [ { "type": "string" @@ -10849,219 +9520,276 @@ "type": "null" } ], - "title": "Aggregate Id" - }, - "metadata": { - "$ref": "#/components/schemas/EventMetadata" - }, - "execution_id": { - "type": "string", - "title": "Execution Id" + "title": "User Id" }, - "storage_type": { + "service_name": { "anyOf": [ { - "$ref": "#/components/schemas/StorageType" + "type": "string" }, { "type": "null" } - ] + ], + "title": "Service Name" } }, "type": "object", "required": [ - "event_id", - "event_type", - "event_version", - "timestamp", - "metadata" + "event_ids", + "execution_id", + "aggregate_id", + "event_types", + "exclude_event_types", + "start_time", + "end_time", + "user_id", + "service_name" ], - "title": "ResultFailedEvent" + "title": "ReplayFilterSchema" }, - "ResultStoredEvent": { + "ReplayRequest": { "properties": { - "event_id": { - "type": "string", - "title": "Event Id" + "replay_type": { + "$ref": "#/components/schemas/ReplayType" }, - "event_type": { - "type": "string", - "const": "result_stored", - "title": "Event Type", - "default": "result_stored" + "target": { + "$ref": "#/components/schemas/ReplayTarget", + "default": "kafka" }, - "event_version": { - "type": "string", - "title": "Event Version", - "default": "1.0" + "filter": { + "$ref": "#/components/schemas/ReplayFilter" }, - "timestamp": { - "type": "string", - "format": "date-time", - "title": "Timestamp" + "speed_multiplier": { + "type": "number", + "maximum": 100.0, + "minimum": 0.1, + "title": "Speed Multiplier", + "default": 1.0 }, - "aggregate_id": { + "preserve_timestamps": { + "type": "boolean", + "title": "Preserve Timestamps", + "default": false + }, + "batch_size": { + "type": "integer", + "maximum": 1000.0, + "minimum": 1.0, + "title": "Batch Size", + "default": 100 + }, + "max_events": { "anyOf": [ { - "type": "string" + "type": "integer", + "minimum": 1.0 }, { "type": "null" } ], - "title": "Aggregate Id" - }, - "metadata": { - "$ref": "#/components/schemas/EventMetadata" + "title": "Max Events" }, - "execution_id": { - "type": "string", - "title": "Execution Id" + "skip_errors": { + "type": "boolean", + "title": "Skip Errors", + "default": true }, - "storage_type": { + "target_file_path": { "anyOf": [ { - "$ref": "#/components/schemas/StorageType" + "type": "string" }, { "type": "null" } - ] + ], + "title": "Target File Path" }, - "storage_path": { - "type": "string", - "title": "Storage Path", - "default": "" + "retry_failed": { + "type": "boolean", + "title": "Retry Failed", + "default": false }, - "size_bytes": { + "retry_attempts": { "type": "integer", - "title": "Size Bytes", - "default": 0 + "maximum": 10.0, + "minimum": 1.0, + "title": "Retry Attempts", + "default": 3 + }, + "enable_progress_tracking": { + "type": "boolean", + "title": "Enable Progress Tracking", + "default": true } }, "type": "object", "required": [ - "event_id", - "event_type", - "event_version", - "timestamp", - "metadata" + "replay_type" ], - "title": "ResultStoredEvent" + "title": "ReplayRequest", + "description": "Request schema for creating replay sessions" }, - "SSEControlEvent": { - "type": "string", - "enum": [ - "status" + "ReplayResponse": { + "properties": { + "session_id": { + "type": "string", + "title": "Session Id" + }, + "status": { + "$ref": "#/components/schemas/ReplayStatus" + }, + "message": { + "type": "string", + "title": "Message" + } + }, + "type": "object", + "required": [ + "session_id", + "status", + "message" ], - "title": "SSEControlEvent", - "description": "Control events for execution SSE streams (not from Kafka)." + "title": "ReplayResponse", + "description": "Response schema for replay operations" }, - "SSEExecutionEventSchema": { + "ReplaySession": { "properties": { - "event_type": { - "anyOf": [ - { - "$ref": "#/components/schemas/EventType" - }, - { - "$ref": "#/components/schemas/SSEControlEvent" - } - ], - "title": "Event Type", - "description": "Event type identifier" - }, - "execution_id": { + "session_id": { "type": "string", - "title": "Execution Id", - "description": "Execution ID" + "title": "Session Id" }, - "timestamp": { - "anyOf": [ - { - "type": "string", - "format": "date-time" - }, - { - "type": "null" - } - ], - "title": "Timestamp", - "description": "Event timestamp" + "config": { + "$ref": "#/components/schemas/ReplayConfigSchema" }, - "event_id": { + "status": { + "$ref": "#/components/schemas/ReplayStatus", + "default": "created" + }, + "total_events": { + "type": "integer", + "title": "Total Events", + "default": 0 + }, + "replayed_events": { + "type": "integer", + "title": "Replayed Events", + "default": 0 + }, + "failed_events": { + "type": "integer", + "title": "Failed Events", + "default": 0 + }, + "skipped_events": { + "type": "integer", + "title": "Skipped Events", + "default": 0 + }, + "created_at": { + "type": "string", + "format": "date-time", + "title": "Created At" + }, + "started_at": { "anyOf": [ { - "type": "string" + "type": "string", + "format": "date-time" }, { "type": "null" } ], - "title": "Event Id", - "description": "Kafka event ID (null for control events)" + "title": "Started At" }, - "status": { + "completed_at": { "anyOf": [ { - "$ref": "#/components/schemas/ExecutionStatus" + "type": "string", + "format": "date-time" }, { "type": "null" } ], - "description": "Execution status (status control event only)" + "title": "Completed At" }, - "result": { + "last_event_at": { "anyOf": [ { - "$ref": "#/components/schemas/ExecutionResult" + "type": "string", + "format": "date-time" }, { "type": "null" } ], - "description": "Full execution result (result_stored only)" + "title": "Last Event At" + }, + "errors": { + "items": { + "$ref": "#/components/schemas/ReplayError" + }, + "type": "array", + "title": "Errors" } }, "type": "object", "required": [ - "event_type", - "execution_id", - "timestamp", - "event_id", + "session_id", + "config", "status", - "result" + "total_events", + "replayed_events", + "failed_events", + "skipped_events", + "created_at", + "started_at", + "completed_at", + "last_event_at", + "errors" ], - "title": "SSEExecutionEventSchema", - "description": "API schema for SSE execution stream events.\n\nAll fields are always present. Nullable fields carry null when not applicable:\nevent_id is null for control events; status only for the status control event;\nresult only for result_stored." + "title": "ReplaySession" }, - "SagaCancellationResponse": { - "properties": { - "success": { - "type": "boolean", - "title": "Success" - }, - "message": { - "type": "string", - "title": "Message" - }, - "saga_id": { - "type": "string", - "title": "Saga Id" - } - }, - "type": "object", - "required": [ - "success", - "message", - "saga_id" + "ReplayStatus": { + "type": "string", + "enum": [ + "preview", + "scheduled", + "created", + "running", + "paused", + "completed", + "failed", + "cancelled" + ], + "title": "ReplayStatus" + }, + "ReplayTarget": { + "type": "string", + "enum": [ + "kafka", + "callback", + "file", + "test" ], - "title": "SagaCancellationResponse", - "description": "Response schema for saga cancellation" + "title": "ReplayTarget" + }, + "ReplayType": { + "type": "string", + "enum": [ + "execution", + "time_range", + "event_type", + "query", + "recovery" + ], + "title": "ReplayType" }, - "SagaCancelledEvent": { + "ResourceLimitExceededEvent": { "properties": { "event_id": { "type": "string", @@ -11069,9 +9797,9 @@ }, "event_type": { "type": "string", - "const": "saga_cancelled", + "const": "resource_limit_exceeded", "title": "Event Type", - "default": "saga_cancelled" + "default": "resource_limit_exceeded" }, "event_version": { "type": "string", @@ -11097,49 +9825,19 @@ "metadata": { "$ref": "#/components/schemas/EventMetadata" }, - "saga_id": { - "type": "string", - "title": "Saga Id" - }, - "saga_name": { - "type": "string", - "title": "Saga Name" - }, - "execution_id": { - "type": "string", - "title": "Execution Id" - }, - "reason": { + "resource_type": { "type": "string", - "title": "Reason" - }, - "completed_steps": { - "items": { - "type": "string" - }, - "type": "array", - "title": "Completed Steps" + "title": "Resource Type" }, - "compensated_steps": { - "items": { - "type": "string" - }, - "type": "array", - "title": "Compensated Steps" + "limit": { + "type": "integer", + "title": "Limit" }, - "cancelled_at": { - "anyOf": [ - { - "type": "string", - "format": "date-time" - }, - { - "type": "null" - } - ], - "title": "Cancelled At" + "requested": { + "type": "integer", + "title": "Requested" }, - "cancelled_by": { + "user_id": { "anyOf": [ { "type": "string" @@ -11148,7 +9846,7 @@ "type": "null" } ], - "title": "Cancelled By" + "title": "User Id" } }, "type": "object", @@ -11159,97 +9857,126 @@ "timestamp", "metadata" ], - "title": "SagaCancelledEvent" + "title": "ResourceLimitExceededEvent" }, - "SagaCompensatedEvent": { + "ResourceLimits": { "properties": { - "event_id": { + "cpu_limit": { "type": "string", - "title": "Event Id" + "title": "Cpu Limit" }, - "event_type": { + "memory_limit": { "type": "string", - "const": "saga_compensated", - "title": "Event Type", - "default": "saga_compensated" + "title": "Memory Limit" }, - "event_version": { + "cpu_request": { "type": "string", - "title": "Event Version", - "default": "1.0" + "title": "Cpu Request" }, - "timestamp": { + "memory_request": { "type": "string", - "format": "date-time", - "title": "Timestamp" + "title": "Memory Request" }, - "aggregate_id": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Aggregate Id" + "execution_timeout": { + "type": "integer", + "title": "Execution Timeout" }, - "metadata": { - "$ref": "#/components/schemas/EventMetadata" + "supported_runtimes": { + "additionalProperties": { + "$ref": "#/components/schemas/LanguageInfo" + }, + "type": "object", + "title": "Supported Runtimes" }, - "saga_id": { - "type": "string", - "title": "Saga Id" + "language_limits": { + "additionalProperties": { + "$ref": "#/components/schemas/LanguageLimits" + }, + "type": "object", + "title": "Language Limits" + } + }, + "type": "object", + "required": [ + "cpu_limit", + "memory_limit", + "cpu_request", + "memory_request", + "execution_timeout", + "supported_runtimes", + "language_limits" + ], + "title": "ResourceLimits", + "description": "Model for resource limits configuration." + }, + "ResourceUsage": { + "properties": { + "execution_time_wall_seconds": { + "type": "number", + "title": "Execution Time Wall Seconds", + "default": 0.0 }, - "saga_name": { - "type": "string", - "title": "Saga Name" + "cpu_time_jiffies": { + "type": "integer", + "title": "Cpu Time Jiffies", + "default": 0 }, - "execution_id": { - "type": "string", - "title": "Execution Id" + "clk_tck_hertz": { + "type": "integer", + "title": "Clk Tck Hertz", + "default": 0 }, - "compensated_steps": { - "items": { - "type": "string" - }, - "type": "array", - "title": "Compensated Steps" + "peak_memory_kb": { + "type": "integer", + "title": "Peak Memory Kb", + "default": 0 + } + }, + "type": "object", + "required": [ + "execution_time_wall_seconds", + "cpu_time_jiffies", + "clk_tck_hertz", + "peak_memory_kb" + ], + "title": "ResourceUsage", + "description": "Model for execution resource usage." + }, + "ResourceUsageDomain": { + "properties": { + "execution_time_wall_seconds": { + "type": "number", + "title": "Execution Time Wall Seconds", + "default": 0.0 + }, + "cpu_time_jiffies": { + "type": "integer", + "title": "Cpu Time Jiffies", + "default": 0 + }, + "clk_tck_hertz": { + "type": "integer", + "title": "Clk Tck Hertz", + "default": 0 + }, + "peak_memory_kb": { + "type": "integer", + "title": "Peak Memory Kb", + "default": 0 } }, "type": "object", - "required": [ - "event_id", - "event_type", - "event_version", - "timestamp", - "metadata" - ], - "title": "SagaCompensatedEvent" + "title": "ResourceUsageDomain", + "description": "Resource usage metrics from script execution." }, - "SagaCompensatingEvent": { + "RestoreSettingsRequest": { "properties": { - "event_id": { - "type": "string", - "title": "Event Id" - }, - "event_type": { - "type": "string", - "const": "saga_compensating", - "title": "Event Type", - "default": "saga_compensating" - }, - "event_version": { - "type": "string", - "title": "Event Version", - "default": "1.0" - }, "timestamp": { "type": "string", "format": "date-time", "title": "Timestamp" }, - "aggregate_id": { + "reason": { "anyOf": [ { "type": "string" @@ -11258,39 +9985,17 @@ "type": "null" } ], - "title": "Aggregate Id" - }, - "metadata": { - "$ref": "#/components/schemas/EventMetadata" - }, - "saga_id": { - "type": "string", - "title": "Saga Id" - }, - "saga_name": { - "type": "string", - "title": "Saga Name" - }, - "execution_id": { - "type": "string", - "title": "Execution Id" - }, - "compensating_step": { - "type": "string", - "title": "Compensating Step" + "title": "Reason" } }, "type": "object", "required": [ - "event_id", - "event_type", - "event_version", - "timestamp", - "metadata" + "timestamp" ], - "title": "SagaCompensatingEvent" + "title": "RestoreSettingsRequest", + "description": "Request model for restoring settings" }, - "SagaCompletedEvent": { + "ResultFailedEvent": { "properties": { "event_id": { "type": "string", @@ -11298,9 +10003,9 @@ }, "event_type": { "type": "string", - "const": "saga_completed", + "const": "result_failed", "title": "Event Type", - "default": "saga_completed" + "default": "result_failed" }, "event_version": { "type": "string", @@ -11326,24 +10031,19 @@ "metadata": { "$ref": "#/components/schemas/EventMetadata" }, - "saga_id": { - "type": "string", - "title": "Saga Id" - }, - "saga_name": { - "type": "string", - "title": "Saga Name" - }, "execution_id": { "type": "string", "title": "Execution Id" }, - "completed_steps": { - "items": { - "type": "string" - }, - "type": "array", - "title": "Completed Steps" + "storage_type": { + "anyOf": [ + { + "$ref": "#/components/schemas/StorageType" + }, + { + "type": "null" + } + ] } }, "type": "object", @@ -11354,9 +10054,9 @@ "timestamp", "metadata" ], - "title": "SagaCompletedEvent" + "title": "ResultFailedEvent" }, - "SagaFailedEvent": { + "ResultStoredEvent": { "properties": { "event_id": { "type": "string", @@ -11364,9 +10064,9 @@ }, "event_type": { "type": "string", - "const": "saga_failed", + "const": "result_stored", "title": "Event Type", - "default": "saga_failed" + "default": "result_stored" }, "event_version": { "type": "string", @@ -11392,125 +10092,29 @@ "metadata": { "$ref": "#/components/schemas/EventMetadata" }, - "saga_id": { - "type": "string", - "title": "Saga Id" - }, - "saga_name": { - "type": "string", - "title": "Saga Name" - }, "execution_id": { "type": "string", "title": "Execution Id" }, - "failed_step": { - "type": "string", - "title": "Failed Step" - }, - "error": { - "type": "string", - "title": "Error" - } - }, - "type": "object", - "required": [ - "event_id", - "event_type", - "event_version", - "timestamp", - "metadata" - ], - "title": "SagaFailedEvent" - }, - "SagaListResponse": { - "properties": { - "sagas": { - "items": { - "$ref": "#/components/schemas/SagaStatusResponse" - }, - "type": "array", - "title": "Sagas" - }, - "total": { - "type": "integer", - "title": "Total" - }, - "skip": { - "type": "integer", - "title": "Skip" - }, - "limit": { - "type": "integer", - "title": "Limit" - }, - "has_more": { - "type": "boolean", - "title": "Has More" - } - }, - "type": "object", - "required": [ - "sagas", - "total", - "skip", - "limit", - "has_more" - ], - "title": "SagaListResponse", - "description": "Response schema for saga list" - }, - "SagaStartedEvent": { - "properties": { - "event_id": { - "type": "string", - "title": "Event Id" - }, - "event_type": { - "type": "string", - "const": "saga_started", - "title": "Event Type", - "default": "saga_started" - }, - "event_version": { - "type": "string", - "title": "Event Version", - "default": "1.0" - }, - "timestamp": { - "type": "string", - "format": "date-time", - "title": "Timestamp" - }, - "aggregate_id": { + "storage_type": { "anyOf": [ { - "type": "string" + "$ref": "#/components/schemas/StorageType" }, { "type": "null" } - ], - "title": "Aggregate Id" - }, - "metadata": { - "$ref": "#/components/schemas/EventMetadata" - }, - "saga_id": { - "type": "string", - "title": "Saga Id" - }, - "saga_name": { - "type": "string", - "title": "Saga Name" + ] }, - "execution_id": { + "storage_path": { "type": "string", - "title": "Execution Id" + "title": "Storage Path", + "default": "" }, - "initial_event_id": { - "type": "string", - "title": "Initial Event Id" + "size_bytes": { + "type": "integer", + "title": "Size Bytes", + "default": 0 } }, "type": "object", @@ -11521,65 +10125,49 @@ "timestamp", "metadata" ], - "title": "SagaStartedEvent" + "title": "ResultStoredEvent" }, - "SagaState": { + "SSEControlEvent": { "type": "string", "enum": [ - "created", - "running", - "compensating", - "completed", - "failed", - "timeout", - "cancelled" + "status" ], - "title": "SagaState", - "description": "Saga execution states." + "title": "SSEControlEvent", + "description": "Control events for execution SSE streams (not from Kafka)." }, - "SagaStatusResponse": { + "SSEExecutionEventSchema": { "properties": { - "saga_id": { - "type": "string", - "title": "Saga Id" - }, - "saga_name": { - "type": "string", - "title": "Saga Name" + "event_type": { + "anyOf": [ + { + "$ref": "#/components/schemas/EventType" + }, + { + "$ref": "#/components/schemas/SSEControlEvent" + } + ], + "title": "Event Type", + "description": "Event type identifier" }, "execution_id": { "type": "string", - "title": "Execution Id" - }, - "state": { - "$ref": "#/components/schemas/SagaState" + "title": "Execution Id", + "description": "Execution ID" }, - "current_step": { + "timestamp": { "anyOf": [ { - "type": "string" + "type": "string", + "format": "date-time" }, { "type": "null" } ], - "title": "Current Step" - }, - "completed_steps": { - "items": { - "type": "string" - }, - "type": "array", - "title": "Completed Steps" - }, - "compensated_steps": { - "items": { - "type": "string" - }, - "type": "array", - "title": "Compensated Steps" + "title": "Timestamp", + "description": "Event timestamp" }, - "error_message": { + "event_id": { "anyOf": [ { "type": "string" @@ -11588,52 +10176,43 @@ "type": "null" } ], - "title": "Error Message" - }, - "created_at": { - "type": "string", - "format": "date-time", - "title": "Created At" - }, - "updated_at": { - "type": "string", - "format": "date-time", - "title": "Updated At" + "title": "Event Id", + "description": "Kafka event ID (null for control events)" }, - "completed_at": { + "status": { "anyOf": [ { - "type": "string", - "format": "date-time" + "$ref": "#/components/schemas/ExecutionStatus" }, { "type": "null" } ], - "title": "Completed At" + "description": "Execution status (status control event only)" }, - "retry_count": { - "type": "integer", - "title": "Retry Count" + "result": { + "anyOf": [ + { + "$ref": "#/components/schemas/ExecutionResult" + }, + { + "type": "null" + } + ], + "description": "Full execution result (result_stored only)" } }, "type": "object", "required": [ - "saga_id", - "saga_name", + "event_type", "execution_id", - "state", - "current_step", - "completed_steps", - "compensated_steps", - "error_message", - "created_at", - "updated_at", - "completed_at", - "retry_count" + "timestamp", + "event_id", + "status", + "result" ], - "title": "SagaStatusResponse", - "description": "Response schema for saga status" + "title": "SSEExecutionEventSchema", + "description": "API schema for SSE execution stream events.\n\nAll fields are always present. Nullable fields carry null when not applicable:\nevent_id is null for control events; status only for the status control event;\nresult only for result_stored." }, "SavedScriptCreateRequest": { "properties": { diff --git a/docs/security/policies.md b/docs/security/policies.md index 8ac8eae9f..d1af46d48 100644 --- a/docs/security/policies.md +++ b/docs/security/policies.md @@ -51,7 +51,7 @@ The worker reaches the cluster with k3s's own kubeconfig, mounted read-only from The `executor-quota` ResourceQuota caps the total CPU and memory that pods in the namespace request, not what they use: a default pod requests 200m CPU but may use up to its 1000m limit. The worker applies it with `requests.cpu` from `K8S_NAMESPACE_CPU_QUOTA` (32) and `requests.memory` from `K8S_NAMESPACE_MEMORY_QUOTA` (4Gi). The API server refuses a pod that would go over it with `403 Forbidden`, and the worker publishes `pod_creation_failed` for its execution. -How many executions run at once is decided by the saga orchestrator's queue (`max_concurrent_executions`, 10 by default). Every pod requests 200m CPU and 128Mi, Go's included, since Go raises only its limits, so 10 executions request 2 CPU and 1.25Gi. The memory quota fills at 32 pods, which makes it a backstop that refuses pods only when the admin settings allow more than that at once. +How many executions run at once is decided by the execution scheduler's queue (`max_concurrent_executions`, 10 by default). Every pod requests 200m CPU and 128Mi, Go's included, since Go raises only its limits, so 10 executions request 2 CPU and 1.25Gi. The memory quota fills at 32 pods, which makes it a backstop that refuses pods only when the admin settings allow more than that at once. ## Network policy enforcement diff --git a/frontend/e2e/admin-sagas.spec.ts b/frontend/e2e/admin-sagas.spec.ts deleted file mode 100644 index 26e2f99fe..000000000 --- a/frontend/e2e/admin-sagas.spec.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { expect, test } from './fixtures'; -import { SagaState } from '#lib/api/types.gen.ts'; -import { fieldTitle } from '#lib/formatters.ts'; - -test('the state filter offers every saga state the API knows', async ({ adminPage }) => { - await adminPage.goto('/admin/sagas'); - await expect(adminPage.getByLabel(fieldTitle('state'), { exact: true }).locator('option')).toHaveText([ - 'All', - ...Object.values(SagaState), - ]); -}); - -test('clear empties the search', async ({ adminPage }) => { - await adminPage.goto('/admin/sagas'); - const search = adminPage.getByLabel(fieldTitle('search'), { exact: true }); - await search.fill('anything'); - await adminPage.getByRole('button', { name: 'Clear' }).click(); - await expect(search).toHaveValue(''); -}); - -test('the saga of a new execution shows up', async ({ adminPage, execution }) => { - await adminPage.goto('/admin/sagas'); - await adminPage.getByLabel(fieldTitle('execution_id'), { exact: true }).fill(execution.execution_id); - // The worker starts the saga a moment after the API accepts the execution; the page polls every 5 seconds - await expect(adminPage.getByRole('table', { name: 'Sagas' }).getByRole('row')).toHaveCount(2, { timeout: 15_000 }); - for (const key of ['saga_name', 'state', 'created_at']) { - await expect(adminPage.getByRole('columnheader', { name: fieldTitle(key), exact: true })).toBeVisible(); - } -}); - -test('auto-refresh turns off and its rate changes', async ({ adminPage }) => { - await adminPage.goto('/admin/sagas'); - const rate = adminPage.getByLabel('Refresh every'); - await rate.selectOption('10'); - await expect(rate).toHaveValue('10'); - await adminPage.getByRole('checkbox', { name: 'Auto-refresh' }).uncheck(); - await expect(rate).toBeHidden(); -}); diff --git a/frontend/src/components/admin/sagas/SagaDetailsModal.svelte b/frontend/src/components/admin/sagas/SagaDetailsModal.svelte deleted file mode 100644 index 41aa43bc8..000000000 --- a/frontend/src/components/admin/sagas/SagaDetailsModal.svelte +++ /dev/null @@ -1,147 +0,0 @@ - - - - {#if saga} - {@const stateInfo = getSagaStateInfo(saga.state)} -
-
-

Basic Information

-
-
-
{fieldTitle('saga_id')}
-
{saga.saga_id}
-
-
-
{fieldTitle('saga_name')}
-
{saga.saga_name}
-
-
-
{fieldTitle('execution_id')}
-
- -
-
-
-
{fieldTitle('state')}
-
{saga.state}
-
-
-
{fieldTitle('retry_count')}
-
{saga.retry_count}
-
-
-
-
-

Timing Information

-
-
-
{fieldTitle('created_at')}
-
{formatTimestamp(saga.created_at)}
-
-
-
{fieldTitle('updated_at')}
-
{formatTimestamp(saga.updated_at)}
-
-
-
{fieldTitle('completed_at')}
-
{formatTimestamp(saga.completed_at)}
-
-
-

- Duration - {formatDurationBetween(saga.created_at, saga.completed_at || saga.updated_at)} -

-
-
- -
-

Execution Steps

- - {#if saga.current_step} -
-
{fieldTitle('current_step')}
-
{saga.current_step}
-
- {/if} - -
-
-

- {fieldTitle('completed_steps')} ({saga.completed_steps.length}) -

- {#if saga.completed_steps.length > 0} -
    - {#each saga.completed_steps as step (step)} -
  • - {step} -
  • - {/each} -
- {:else} -

No completed steps

- {/if} -
-
-

- {fieldTitle('compensated_steps')} ({saga.compensated_steps.length}) -

- {#if saga.compensated_steps.length > 0} -
    - {#each saga.compensated_steps as step (step)} -
  • - {step} -
  • - {/each} -
- {:else} -

No compensated steps

- {/if} -
-
-
- - {#if saga.error_message} -
-

{fieldTitle('error_message')}

-
-
{saga.error_message}
-
-
- {/if} - {/if} -
diff --git a/frontend/src/components/admin/sagas/SagaFilters.svelte b/frontend/src/components/admin/sagas/SagaFilters.svelte deleted file mode 100644 index 2edf9d526..000000000 --- a/frontend/src/components/admin/sagas/SagaFilters.svelte +++ /dev/null @@ -1,46 +0,0 @@ - - - - -
-
- - -
-
- - -
-
- - -
-
- Actions - -
-
diff --git a/frontend/src/components/admin/sagas/SagaStatsCards.svelte b/frontend/src/components/admin/sagas/SagaStatsCards.svelte deleted file mode 100644 index f60367ddf..000000000 --- a/frontend/src/components/admin/sagas/SagaStatsCards.svelte +++ /dev/null @@ -1,29 +0,0 @@ - - -
- {#each Object.values(SagaState) as state (state)} - {@const info = SAGA_STATES[state]} - {@const count = getCount(state)} - {@const IconComponent = info.icon} -
-
- -
{count}
-
- -
- {/each} -
diff --git a/frontend/src/components/admin/sagas/SagasTable.svelte b/frontend/src/components/admin/sagas/SagasTable.svelte deleted file mode 100644 index a7ac0cf52..000000000 --- a/frontend/src/components/admin/sagas/SagasTable.svelte +++ /dev/null @@ -1,98 +0,0 @@ - - -{#snippet sagaCell(saga: SagaStatusResponse)} -
{saga.saga_name}
-
{fieldTitle('saga_id')} {saga.saga_id.slice(0, 8)}...
- -{/snippet} - -{#snippet stateCell(saga: SagaStatusResponse)} - {@const stateInfo = getSagaStateInfo(saga.state)} - {saga.state} - {#if saga.retry_count > 0} - ({fieldTitle('retry_count')} {saga.retry_count}) - {/if} -{/snippet} - -{#snippet progressCell(saga: SagaStatusResponse)} -
- {fieldTitle('completed_steps')} {saga.completed_steps.length} - {#if saga.current_step} -
{fieldTitle('current_step')} {saga.current_step}
- {/if} -
-{/snippet} - -{#snippet actionsCell(saga: SagaStatusResponse)} - -{/snippet} - -
- - {#if rowCount > 0} -
- -
- {/if} -
diff --git a/frontend/src/components/admin/sagas/__tests__/SagaDetailsModal.test.ts b/frontend/src/components/admin/sagas/__tests__/SagaDetailsModal.test.ts deleted file mode 100644 index 9e07ea388..000000000 --- a/frontend/src/components/admin/sagas/__tests__/SagaDetailsModal.test.ts +++ /dev/null @@ -1,141 +0,0 @@ -import { describe, it, expect, vi } from 'vitest'; -import { screen, waitFor } from '@testing-library/svelte'; -import { user, mockApi, renderWithQueryClient } from '#test/test-utils.ts'; -import { fakeSagaStatusResponse } from '#lib/api/@faker-js/faker.gen.ts'; -import { getSagaStatusApiV1SagasSagaIdGet, type SagaStatusResponse } from '#lib/api/index.ts'; -import { fieldTitle } from '#lib/formatters.ts'; -import SagaDetailsModal from '#components/admin/sagas/SagaDetailsModal.svelte'; - -vi.mock('#lib/formatters.ts', async (importOriginal) => ({ - ...(await importOriginal()), - formatTimestamp: vi.fn((ts: string | null) => (ts ? `ts:${ts}` : 'N/A')), - formatDurationBetween: vi.fn(() => '1m 30s'), -})); - -vi.mock('#lib/admin/sagas/index.ts', () => ({ - getSagaStateInfo: vi.fn(() => ({ color: 'badge-neutral' })), -})); - -async function renderModal(saga: SagaStatusResponse = fakeSagaStatusResponse()) { - mockApi(getSagaStatusApiV1SagasSagaIdGet).ok(saga); - const onClose = vi.fn(); - const onViewExecution = vi.fn(); - const result = renderWithQueryClient(SagaDetailsModal, { sagaId: saga.saga_id, onClose, onViewExecution }); - await screen.findByText('Basic Information'); - return { ...result, onClose, onViewExecution }; -} - -describe('SagaDetailsModal', () => { - it('loads the saga by its id', async () => { - await renderModal({ ...fakeSagaStatusResponse(), saga_id: 'saga-7' }); - await waitFor(() => - expect(vi.mocked(getSagaStatusApiV1SagasSagaIdGet)).toHaveBeenCalledWith( - expect.objectContaining({ path: { saga_id: 'saga-7' } }), - ), - ); - }); - - it('shows only the title until the saga has loaded', async () => { - vi.mocked(getSagaStatusApiV1SagasSagaIdGet).mockReturnValue(new Promise(() => {})); - renderWithQueryClient(SagaDetailsModal, { sagaId: 'saga-1', onClose: vi.fn(), onViewExecution: vi.fn() }); - expect(screen.getByText('Saga Details')).toBeInTheDocument(); - expect(screen.queryByText('Basic Information')).not.toBeInTheDocument(); - }); - - it.each(['Saga Details', 'Basic Information', 'Timing Information', 'Execution Steps'])( - 'shows %s section', - async (section) => { - await renderModal(); - expect(screen.getByText(section)).toBeInTheDocument(); - }, - ); - - it.each([ - 'saga_id', - 'saga_name', - 'execution_id', - 'state', - 'retry_count', - 'created_at', - 'updated_at', - 'completed_at', - ])('labels the %s field by its API name', async (key) => { - await renderModal(); - expect(screen.getByText(fieldTitle(key), { selector: 'dt' })).toBeInTheDocument(); - }); - - it('displays saga ID', async () => { - await renderModal({ ...fakeSagaStatusResponse(), saga_id: 'saga-test-123' }); - expect(screen.getByText('saga-test-123')).toBeInTheDocument(); - }); - - it('displays saga name', async () => { - await renderModal({ ...fakeSagaStatusResponse(), saga_name: 'test_saga' }); - expect(screen.getByText('test_saga')).toBeInTheDocument(); - }); - - it('displays execution ID as clickable button', async () => { - const saga = { ...fakeSagaStatusResponse(), execution_id: 'exec-999' }; - await renderModal(saga); - expect(screen.getByText('exec-999')).toBeInTheDocument(); - }); - - it('displays state badge', async () => { - await renderModal({ ...fakeSagaStatusResponse(), state: 'completed' }); - expect(screen.getByText('completed')).toHaveClass('badge'); - }); - - it('displays retry count', async () => { - await renderModal({ ...fakeSagaStatusResponse(), retry_count: 5 }); - expect(screen.getByText('5')).toBeInTheDocument(); - }); - - it('shows completed steps count', async () => { - await renderModal({ ...fakeSagaStatusResponse(), completed_steps: ['step1', 'step2'] }); - expect(screen.getByText('Completed Steps (2)')).toBeInTheDocument(); - }); - - it('lists completed steps', async () => { - await renderModal({ ...fakeSagaStatusResponse(), completed_steps: ['validate', 'allocate'] }); - expect(screen.getByText('validate')).toBeInTheDocument(); - expect(screen.getByText('allocate')).toBeInTheDocument(); - }); - - it.each([ - ['completed', 'No completed steps'], - ['compensated', 'No compensated steps'], - ] as const)('shows "%s" empty state when no steps', async (_type, emptyText) => { - await renderModal({ ...fakeSagaStatusResponse(), completed_steps: [], compensated_steps: [] }); - expect(screen.getByText(emptyText)).toBeInTheDocument(); - }); - - it('shows compensated steps count', async () => { - await renderModal({ ...fakeSagaStatusResponse(), compensated_steps: ['step1'] }); - expect(screen.getByText('Compensated Steps (1)')).toBeInTheDocument(); - }); - - it.each([ - ['current step', { current_step: 'create_pod' }, 'Current Step', 'create_pod'], - ['error message', { error_message: 'Pod creation failed' }, 'Error Message', 'Pod creation failed'], - ] as const)('shows %s when present', async (_label, overrides, sectionText, valueText) => { - await renderModal({ ...fakeSagaStatusResponse(), ...overrides }); - expect(screen.getByText(sectionText)).toBeInTheDocument(); - expect(screen.getByText(valueText)).toBeInTheDocument(); - }); - - it.each([ - ['current step', { current_step: null }, 'Current Step'], - ['error section', { error_message: null }, 'Error Message'], - ] as const)('hides %s when null', async (_label, overrides, hiddenText) => { - await renderModal({ ...fakeSagaStatusResponse(), ...overrides }); - expect(screen.queryByText(hiddenText)).not.toBeInTheDocument(); - }); - - it('calls onViewExecution when execution ID button clicked', async () => { - const saga = { ...fakeSagaStatusResponse(), execution_id: 'exec-click' }; - const { onViewExecution, onClose } = await renderModal(saga); - await user.click(screen.getByText('exec-click')); - expect(onClose).toHaveBeenCalledOnce(); - expect(onViewExecution).toHaveBeenCalledWith('exec-click'); - }); -}); diff --git a/frontend/src/components/admin/sagas/__tests__/SagaFilters.test.ts b/frontend/src/components/admin/sagas/__tests__/SagaFilters.test.ts deleted file mode 100644 index 77e14b644..000000000 --- a/frontend/src/components/admin/sagas/__tests__/SagaFilters.test.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { describe, it, expect, vi } from 'vitest'; -import { render, screen } from '@testing-library/svelte'; -import { user } from '#test/test-utils.ts'; -import { SagaState } from '#lib/api/index.ts'; -import SagaFilters from '#components/admin/sagas/SagaFilters.svelte'; - -function renderFilters() { - const onClear = vi.fn(); - const result = render(SagaFilters, { - props: { filters: { search: null, state: null, execution_id: null }, onClear }, - }); - return { ...result, onClear }; -} - -describe('SagaFilters', () => { - it.each(['Search', 'State', 'Execution Id'])('renders %s filter input', (label) => { - renderFilters(); - expect(screen.getByLabelText(label)).toBeInTheDocument(); - }); - - it('renders Clear Filters button', () => { - renderFilters(); - expect(screen.getByText('Clear Filters')).toBeInTheDocument(); - }); - - it('state filter offers All, then every saga state as the API names it', () => { - renderFilters(); - const select = screen.getByLabelText('State') as HTMLSelectElement; - expect(Array.from(select.options).map((o) => o.text)).toEqual(['All', ...Object.values(SagaState)]); - expect(Array.from(select.options).map((o) => o.value)).toEqual(['', ...Object.values(SagaState)]); - }); - - it('calls onClear when Clear Filters is clicked', async () => { - const { onClear } = renderFilters(); - await user.click(screen.getByText('Clear Filters')); - expect(onClear).toHaveBeenCalledOnce(); - }); -}); diff --git a/frontend/src/components/admin/sagas/__tests__/SagaStatsCards.test.ts b/frontend/src/components/admin/sagas/__tests__/SagaStatsCards.test.ts deleted file mode 100644 index 4c1fc7da4..000000000 --- a/frontend/src/components/admin/sagas/__tests__/SagaStatsCards.test.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { render, screen } from '@testing-library/svelte'; -import { fakeSagaStatusResponse } from '#lib/api/@faker-js/faker.gen.ts'; -import { SagaState, type SagaStatusResponse } from '#lib/api/index.ts'; -import SagaStatsCards from '#components/admin/sagas/SagaStatsCards.svelte'; - -function renderCards(sagas: SagaStatusResponse[] = []) { - return render(SagaStatsCards, { props: { sagas } }); -} - -describe('SagaStatsCards', () => { - it.each(Object.values(SagaState))('titles a card with the raw state %s', (state) => { - renderCards(); - expect(screen.getByText(state)).toBeInTheDocument(); - }); - - it('shows zero counts when no sagas', () => { - renderCards([]); - const zeros = screen.getAllByText('0'); - expect(zeros.length).toBe(Object.values(SagaState).length); - }); - - it('shows correct count for each state', () => { - const sagas: SagaStatusResponse[] = [ - { ...fakeSagaStatusResponse(), saga_id: 's1', state: 'running' }, - { ...fakeSagaStatusResponse(), saga_id: 's2', state: 'running' }, - { ...fakeSagaStatusResponse(), saga_id: 's3', state: 'completed' }, - { ...fakeSagaStatusResponse(), saga_id: 's4', state: 'failed' }, - ]; - renderCards(sagas); - expect(screen.getByText('2')).toBeInTheDocument(); - const ones = screen.getAllByText('1'); - expect(ones.length).toBe(2); - const zeros = screen.getAllByText('0'); - expect(zeros.length).toBe(Object.values(SagaState).length - 3); - }); - - it('updates counts when sagas change', () => { - const { rerender } = renderCards([{ ...fakeSagaStatusResponse(), state: 'running' }]); - expect(screen.getByText('1')).toBeInTheDocument(); - rerender({ - sagas: [ - { ...fakeSagaStatusResponse(), saga_id: 's1', state: 'completed' }, - { ...fakeSagaStatusResponse(), saga_id: 's2', state: 'completed' }, - ], - }); - expect(screen.getByText('2')).toBeInTheDocument(); - }); -}); diff --git a/frontend/src/components/admin/sagas/__tests__/SagasTable.test.ts b/frontend/src/components/admin/sagas/__tests__/SagasTable.test.ts deleted file mode 100644 index 6463d2524..000000000 --- a/frontend/src/components/admin/sagas/__tests__/SagasTable.test.ts +++ /dev/null @@ -1,124 +0,0 @@ -import { describe, it, expect, vi } from 'vitest'; -import { render, screen, within } from '@testing-library/svelte'; -import { user } from '#test/test-utils.ts'; -import { fieldTitle } from '#lib/formatters.ts'; -import { fakeSagaStatusResponse } from '#lib/api/@faker-js/faker.gen.ts'; -import SagasTable from '#components/admin/sagas/SagasTable.svelte'; - -vi.mock('#lib/formatters.ts', async (importOriginal) => ({ - ...(await importOriginal()), - formatTimestamp: vi.fn((ts: string) => `ts:${ts}`), - formatDurationBetween: vi.fn(() => '1m 30s'), -})); - -vi.mock('#lib/admin/sagas/index.ts', () => ({ - getSagaStateInfo: vi.fn(() => ({ color: 'badge-neutral', bgColor: 'bg-muted' })), -})); - -function renderTable(props: Record = {}) { - const onViewDetails = vi.fn(); - const onViewExecution = vi.fn(); - const result = render(SagasTable, { - props: { - sagas: [], - rowCount: 0, - pagination: { pageIndex: 0, pageSize: 10 }, - loading: false, - onViewDetails, - onViewExecution, - ...props, - }, - }); - return { ...result, onViewDetails, onViewExecution }; -} - -describe('SagasTable', () => { - it('shows loading state when loading with no sagas', () => { - renderTable({ loading: true, sagas: [] }); - expect(screen.getByText('Loading sagas...')).toBeInTheDocument(); - }); - - it('shows empty state when no sagas and not loading', () => { - renderTable({ sagas: [], loading: false }); - expect(screen.getByText('No sagas found')).toBeInTheDocument(); - }); - - it('names each column by its API key', () => { - renderTable({ sagas: [fakeSagaStatusResponse()], rowCount: 1 }); - expect(screen.getAllByRole('columnheader').map((h) => h.textContent?.trim())).toEqual( - ['saga_name', 'state', 'progress', 'created_at', 'duration', 'actions'].map(fieldTitle), - ); - }); - - it('renders one row per saga in desktop table', () => { - const sagas = Array.from({ length: 4 }, () => fakeSagaStatusResponse()); - renderTable({ sagas }); - const rows = within(screen.getByRole('table', { name: 'Sagas' })).getAllByRole('row'); - expect(rows).toHaveLength(sagas.length + 1); - }); - - it('renders a card for each saga for narrow screens', () => { - renderTable({ sagas: Array.from({ length: 2 }, () => fakeSagaStatusResponse()), rowCount: 2 }); - expect(within(screen.getByRole('list', { name: 'Sagas' })).getAllByRole('listitem')).toHaveLength(2); - }); - - it('shows the page range and the total', () => { - renderTable({ - sagas: Array.from({ length: 10 }, () => fakeSagaStatusResponse()), - rowCount: 25, - pagination: { pageIndex: 1, pageSize: 10 }, - }); - expect(screen.getByText('Showing 11 - 20 of 25 sagas')).toBeInTheDocument(); - }); - - it('displays saga name', () => { - renderTable({ sagas: [{ ...fakeSagaStatusResponse(), saga_name: 'my_saga' }] }); - const names = screen.getAllByText('my_saga'); - expect(names.length).toBeGreaterThanOrEqual(1); - }); - - it('displays truncated saga_id', () => { - renderTable({ sagas: [{ ...fakeSagaStatusResponse(), saga_id: 'abcdefghijklmno' }] }); - const truncated = screen.getAllByText('Saga Id abcdefgh...'); - expect(truncated.length).toBeGreaterThanOrEqual(1); - }); - - it('shows the raw state in the badge', () => { - renderTable({ sagas: [{ ...fakeSagaStatusResponse(), state: 'running' }] }); - const badges = screen.getAllByText('running'); - expect(badges.length).toBeGreaterThanOrEqual(1); - }); - - it('shows retry count when > 0', () => { - renderTable({ sagas: [{ ...fakeSagaStatusResponse(), retry_count: 3 }] }); - const retries = screen.getAllByText('(Retry Count 3)'); - expect(retries.length).toBeGreaterThanOrEqual(1); - }); - - it('shows completed steps count', () => { - renderTable({ sagas: [{ ...fakeSagaStatusResponse(), completed_steps: ['step1', 'step2'] }] }); - const stepsText = screen.getAllByText('Completed Steps 2'); - expect(stepsText.length).toBeGreaterThanOrEqual(1); - }); - - it('shows current step when present', () => { - renderTable({ sagas: [{ ...fakeSagaStatusResponse(), current_step: 'create_pod' }] }); - const currentSteps = screen.getAllByText('Current Step create_pod'); - expect(currentSteps.length).toBeGreaterThanOrEqual(1); - }); - - it('calls onViewDetails when View Details clicked', async () => { - const saga = { ...fakeSagaStatusResponse(), saga_id: 'saga-click' }; - const { onViewDetails } = renderTable({ sagas: [saga] }); - const btns = screen.getAllByText('View Details'); - await user.click(btns[0]!); - expect(onViewDetails).toHaveBeenCalledWith('saga-click'); - }); - - it('calls onViewExecution when the execution link is clicked', async () => { - const saga = { ...fakeSagaStatusResponse(), execution_id: 'exec-click' }; - const { onViewExecution } = renderTable({ sagas: [saga] }); - await user.click(screen.getAllByRole('button', { name: 'Execution Id exec-cli...' })[0]!); - expect(onViewExecution).toHaveBeenCalledWith('exec-click'); - }); -}); diff --git a/frontend/src/components/admin/sagas/index.ts b/frontend/src/components/admin/sagas/index.ts deleted file mode 100644 index 0715f585d..000000000 --- a/frontend/src/components/admin/sagas/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export { default as SagaStatsCards } from '#components/admin/sagas/SagaStatsCards.svelte'; -export { default as SagaFilters } from '#components/admin/sagas/SagaFilters.svelte'; -export { default as SagasTable } from '#components/admin/sagas/SagasTable.svelte'; -export { default as SagaDetailsModal } from '#components/admin/sagas/SagaDetailsModal.svelte'; diff --git a/frontend/src/lib/admin/constants.ts b/frontend/src/lib/admin/constants.ts index e473ddc9d..c3775654f 100644 --- a/frontend/src/lib/admin/constants.ts +++ b/frontend/src/lib/admin/constants.ts @@ -2,7 +2,6 @@ export const ADMIN_ROUTES = [ { path: '/admin/events', sidebarLabel: 'Event Browser', pageHeading: 'Event Browser' }, { path: '/admin/executions', sidebarLabel: 'Executions', pageHeading: 'Execution Management' }, - { path: '/admin/sagas', sidebarLabel: 'Sagas', pageHeading: 'Saga Management' }, { path: '/admin/users', sidebarLabel: 'Users', pageHeading: 'User Management' }, { path: '/admin/settings', sidebarLabel: 'Settings', pageHeading: 'System Settings' }, ] as const; diff --git a/frontend/src/lib/admin/index.ts b/frontend/src/lib/admin/index.ts index 8882a5331..7f17ee491 100644 --- a/frontend/src/lib/admin/index.ts +++ b/frontend/src/lib/admin/index.ts @@ -2,6 +2,5 @@ export * from '#lib/admin/constants.ts'; // Domain-specific exports -export * from '#lib/admin/sagas/index.ts'; export * from '#lib/admin/events/index.ts'; export * from '#lib/admin/rate-limits/index.ts'; diff --git a/frontend/src/lib/admin/sagas/__tests__/sagaStates.test.ts b/frontend/src/lib/admin/sagas/__tests__/sagaStates.test.ts deleted file mode 100644 index 9fe8fa062..000000000 --- a/frontend/src/lib/admin/sagas/__tests__/sagaStates.test.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { SagaState } from '#lib/api/index.ts'; -import { SAGA_STATES, getSagaStateInfo } from '#lib/admin/sagas/sagaStates.ts'; - -describe('sagaStates', () => { - describe('SAGA_STATES', () => { - it('covers every state the API sends', () => { - expect(Object.keys(SAGA_STATES).sort()).toEqual(Object.values(SagaState).sort()); - }); - - it('each state has required properties', () => { - Object.values(SAGA_STATES).forEach((state) => { - expect(state).toHaveProperty('color'); - expect(state).toHaveProperty('bgColor'); - expect(state).toHaveProperty('icon'); - }); - }); - }); - - describe('getSagaStateInfo', () => { - it('returns correct info for known states', () => { - const running = getSagaStateInfo('running'); - expect(running.color).toBe('badge-info'); - }); - - it('returns info for all valid states', () => { - for (const state of Object.values(SagaState)) { - const info = getSagaStateInfo(state); - expect(info.color).toBeTruthy(); - } - }); - }); -}); diff --git a/frontend/src/lib/admin/sagas/index.ts b/frontend/src/lib/admin/sagas/index.ts deleted file mode 100644 index ca298e1a7..000000000 --- a/frontend/src/lib/admin/sagas/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from '#lib/admin/sagas/sagaStates.ts'; diff --git a/frontend/src/lib/admin/sagas/sagaStates.ts b/frontend/src/lib/admin/sagas/sagaStates.ts deleted file mode 100644 index 4ce83722a..000000000 --- a/frontend/src/lib/admin/sagas/sagaStates.ts +++ /dev/null @@ -1,55 +0,0 @@ -/** - * Saga state configurations - */ -import { Plus, Loader, TriangleAlert, CircleCheckBig, CircleX, Clock } from '@lucide/svelte'; -import type { SagaState } from '#lib/api/index.ts'; - -export interface SagaStateConfig { - color: string; - bgColor: string; - icon: typeof CircleCheckBig; -} - -export const SAGA_STATES: Record = { - created: { - color: 'badge-neutral', - bgColor: 'bg-muted', - icon: Plus, - }, - running: { - color: 'badge-info', - bgColor: 'bg-info-soft', - icon: Loader, - }, - compensating: { - color: 'badge-warning', - bgColor: 'bg-warning-soft', - icon: TriangleAlert, - }, - completed: { - color: 'badge-success', - bgColor: 'bg-success-soft', - icon: CircleCheckBig, - }, - failed: { - color: 'badge-danger', - bgColor: 'bg-danger-soft', - icon: CircleX, - }, - timeout: { - color: 'badge-warning', - bgColor: 'bg-orange-soft', - icon: Clock, - }, - cancelled: { - color: 'badge-neutral', - bgColor: 'bg-muted', - icon: CircleX, - }, -}; - -export function getSagaStateInfo(state: SagaState): SagaStateConfig { - return SAGA_STATES[state]; -} - -// Filter type for saga state filters - empty string means "all" diff --git a/frontend/src/lib/api/@faker-js/faker.gen.ts b/frontend/src/lib/api/@faker-js/faker.gen.ts index 0bc1cc60e..b9a039a63 100644 --- a/frontend/src/lib/api/@faker-js/faker.gen.ts +++ b/frontend/src/lib/api/@faker-js/faker.gen.ts @@ -2,7 +2,7 @@ import { faker, type Faker } from '@faker-js/faker'; -import type { AdminExecutionListResponse, AdminExecutionResponse, AdminUserOverview, AllocateResourcesCommandEvent, AuthFailedEvent, BodyLoginApiV1AuthLoginPost, BrowseEventsApiV1AdminEventsBrowsePostData, BrowseEventsApiV1AdminEventsBrowsePostErrors, BrowseEventsApiV1AdminEventsBrowsePostResponses, CancelExecutionApiV1ExecutionsExecutionIdCancelPostData, CancelExecutionApiV1ExecutionsExecutionIdCancelPostErrors, CancelExecutionApiV1ExecutionsExecutionIdCancelPostResponses, CancelExecutionRequest, CancelReplaySessionApiV1ReplaySessionsSessionIdCancelPostData, CancelReplaySessionApiV1ReplaySessionsSessionIdCancelPostErrors, CancelReplaySessionApiV1ReplaySessionsSessionIdCancelPostResponses, CancelResponse, CancelSagaApiV1SagasSagaIdCancelPostData, CancelSagaApiV1SagasSagaIdCancelPostErrors, CancelSagaApiV1SagasSagaIdCancelPostResponses, CancelStatus, CleanupOldSessionsApiV1ReplayCleanupPostData, CleanupOldSessionsApiV1ReplayCleanupPostErrors, CleanupOldSessionsApiV1ReplayCleanupPostResponses, CleanupResponse, ContainerStatusInfo, CreateExecutionApiV1ExecutePostData, CreateExecutionApiV1ExecutePostErrors, CreateExecutionApiV1ExecutePostResponses, CreatePodCommandEvent, CreateReplaySessionApiV1ReplaySessionsPostData, CreateReplaySessionApiV1ReplaySessionsPostErrors, CreateReplaySessionApiV1ReplaySessionsPostResponses, CreateSavedScriptApiV1ScriptsPostData, CreateSavedScriptApiV1ScriptsPostErrors, CreateSavedScriptApiV1ScriptsPostResponses, CreateUserApiV1AdminUsersPostData, CreateUserApiV1AdminUsersPostErrors, CreateUserApiV1AdminUsersPostResponses, DeleteEventApiV1AdminEventsEventIdDeleteData, DeleteEventApiV1AdminEventsEventIdDeleteErrors, DeleteEventApiV1AdminEventsEventIdDeleteResponses, DeleteExecutionApiV1ExecutionsExecutionIdDeleteData, DeleteExecutionApiV1ExecutionsExecutionIdDeleteErrors, DeleteExecutionApiV1ExecutionsExecutionIdDeleteResponses, DeleteNotificationApiV1NotificationsNotificationIdDeleteData, DeleteNotificationApiV1NotificationsNotificationIdDeleteErrors, DeleteNotificationApiV1NotificationsNotificationIdDeleteResponses, DeleteNotificationResponse, DeletePodCommandEvent, DeleteResponse, DeleteSavedScriptApiV1ScriptsScriptIdDeleteData, DeleteSavedScriptApiV1ScriptsScriptIdDeleteErrors, DeleteUserApiV1AdminUsersUserIdDeleteData, DeleteUserApiV1AdminUsersUserIdDeleteErrors, DeleteUserApiV1AdminUsersUserIdDeleteResponses, DeleteUserResponse, DerivedCounts, DlqMessageDiscardedEvent, DlqMessageReceivedEvent, DlqMessageRetriedEvent, EditorSettingsInput, EditorSettingsOutput, EndpointGroup, EndpointUsageStats, Environment, ErrorResponse, EventBrowseRequest, EventBrowseResponse, EventDeleteResponse, EventDetailResponse, EventFilter, EventMetadata, EventReplayRequest, EventReplayResponse, EventReplayStatusResponse, EventReplayStatusResponseWritable, EventStatistics, EventStatsResponse, EventSummary, EventType, EventTypeCount, ExampleScripts, ExecutionAcceptedEvent, ExecutionCancelledEvent, ExecutionCompletedEvent, ExecutionErrorType, ExecutionEventsApiV1EventsExecutionsExecutionIdGetData, ExecutionEventsApiV1EventsExecutionsExecutionIdGetErrors, ExecutionEventsApiV1EventsExecutionsExecutionIdGetResponses, ExecutionFailedEvent, ExecutionListResponse, ExecutionQueuedEvent, ExecutionRequest, ExecutionRequestedEvent, ExecutionResponse, ExecutionResult, ExecutionRunningEvent, ExecutionStartedEvent, ExecutionStatus, ExecutionTimeoutEvent, ExportEventsApiV1AdminEventsExportExportFormatGetData, ExportEventsApiV1AdminEventsExportExportFormatGetErrors, ExportEventsApiV1AdminEventsExportExportFormatGetResponses, ExportFormat, GetCurrentUserProfileApiV1AuthMeGetErrors, GetCurrentUserProfileApiV1AuthMeGetResponses, GetDefaultRateLimitRulesApiV1AdminRateLimitsDefaultsGetErrors, GetDefaultRateLimitRulesApiV1AdminRateLimitsDefaultsGetResponses, GetEventDetailApiV1AdminEventsEventIdGetData, GetEventDetailApiV1AdminEventsEventIdGetErrors, GetEventDetailApiV1AdminEventsEventIdGetResponses, GetEventStatsApiV1AdminEventsStatsGetData, GetEventStatsApiV1AdminEventsStatsGetErrors, GetEventStatsApiV1AdminEventsStatsGetResponses, GetExampleScriptsApiV1ExampleScriptsGetErrors, GetExampleScriptsApiV1ExampleScriptsGetResponses, GetExecutionEventsApiV1ExecutionsExecutionIdEventsGetData, GetExecutionEventsApiV1ExecutionsExecutionIdEventsGetErrors, GetExecutionEventsApiV1ExecutionsExecutionIdEventsGetResponses, GetExecutionSagasApiV1SagasExecutionExecutionIdGetData, GetExecutionSagasApiV1SagasExecutionExecutionIdGetErrors, GetExecutionSagasApiV1SagasExecutionExecutionIdGetResponses, GetK8sResourceLimitsApiV1K8sLimitsGetErrors, GetK8sResourceLimitsApiV1K8sLimitsGetResponses, GetNotificationsApiV1NotificationsGetData, GetNotificationsApiV1NotificationsGetErrors, GetNotificationsApiV1NotificationsGetResponses, GetQueueStatusApiV1AdminExecutionsQueueGetErrors, GetQueueStatusApiV1AdminExecutionsQueueGetResponses, GetReplaySessionApiV1ReplaySessionsSessionIdGetData, GetReplaySessionApiV1ReplaySessionsSessionIdGetErrors, GetReplaySessionApiV1ReplaySessionsSessionIdGetResponses, GetResultApiV1ExecutionsExecutionIdResultGetData, GetResultApiV1ExecutionsExecutionIdResultGetErrors, GetResultApiV1ExecutionsExecutionIdResultGetResponses, GetSagaStatusApiV1SagasSagaIdGetData, GetSagaStatusApiV1SagasSagaIdGetErrors, GetSagaStatusApiV1SagasSagaIdGetResponses, GetSavedScriptApiV1ScriptsScriptIdGetData, GetSavedScriptApiV1ScriptsScriptIdGetErrors, GetSavedScriptApiV1ScriptsScriptIdGetResponses, GetSettingsHistoryApiV1UserSettingsHistoryGetData, GetSettingsHistoryApiV1UserSettingsHistoryGetErrors, GetSettingsHistoryApiV1UserSettingsHistoryGetResponses, GetSubscriptionsApiV1NotificationsSubscriptionsGetErrors, GetSubscriptionsApiV1NotificationsSubscriptionsGetResponses, GetSystemSettingsApiV1AdminSettingsGetErrors, GetSystemSettingsApiV1AdminSettingsGetResponses, GetUnreadCountApiV1NotificationsUnreadCountGetErrors, GetUnreadCountApiV1NotificationsUnreadCountGetResponses, GetUserApiV1AdminUsersUserIdGetData, GetUserApiV1AdminUsersUserIdGetErrors, GetUserApiV1AdminUsersUserIdGetResponses, GetUserExecutionsApiV1UserExecutionsGetData, GetUserExecutionsApiV1UserExecutionsGetErrors, GetUserExecutionsApiV1UserExecutionsGetResponses, GetUserOverviewApiV1AdminUsersUserIdOverviewGetData, GetUserOverviewApiV1AdminUsersUserIdOverviewGetErrors, GetUserOverviewApiV1AdminUsersUserIdOverviewGetResponses, GetUserRateLimitsApiV1AdminRateLimitsUserIdGetData, GetUserRateLimitsApiV1AdminRateLimitsUserIdGetErrors, GetUserRateLimitsApiV1AdminRateLimitsUserIdGetResponses, GetUserSettingsApiV1UserSettingsGetErrors, GetUserSettingsApiV1UserSettingsGetResponses, HourlyEventCount, LanguageInfo, LanguageLimits, ListExecutionsApiV1AdminExecutionsGetData, ListExecutionsApiV1AdminExecutionsGetErrors, ListExecutionsApiV1AdminExecutionsGetResponses, ListReplaySessionsApiV1ReplaySessionsGetData, ListReplaySessionsApiV1ReplaySessionsGetErrors, ListReplaySessionsApiV1ReplaySessionsGetResponses, ListSagasApiV1SagasGetData, ListSagasApiV1SagasGetErrors, ListSagasApiV1SagasGetResponses, ListSavedScriptsApiV1ScriptsGetErrors, ListSavedScriptsApiV1ScriptsGetResponses, ListUsersApiV1AdminUsersGetData, ListUsersApiV1AdminUsersGetErrors, ListUsersApiV1AdminUsersGetResponses, LivenessApiV1HealthLiveGetErrors, LivenessApiV1HealthLiveGetResponses, LivenessResponse, LoginApiV1AuthLoginPostData, LoginApiV1AuthLoginPostErrors, LoginApiV1AuthLoginPostResponses, LoginMethod, LoginResponse, LogLevel, LogoutApiV1AuthLogoutPostErrors, LogoutApiV1AuthLogoutPostResponses, MarkAllReadApiV1NotificationsMarkAllReadPostErrors, MarkNotificationReadApiV1NotificationsNotificationIdReadPutData, MarkNotificationReadApiV1NotificationsNotificationIdReadPutErrors, MessageResponse, NotificationAllReadEvent, NotificationChannel, NotificationClickedEvent, NotificationCreatedEvent, NotificationDeliveredEvent, NotificationFailedEvent, NotificationListResponse, NotificationPreferencesUpdatedEvent, NotificationReadEvent, NotificationResponse, NotificationSentEvent, NotificationSettingsInput, NotificationSettingsOutput, NotificationSeverity, NotificationStatus, NotificationStreamApiV1EventsNotificationsStreamGetErrors, NotificationStreamApiV1EventsNotificationsStreamGetResponses, NotificationSubscription, PasswordResetRequest, PauseReplaySessionApiV1ReplaySessionsSessionIdPausePostData, PauseReplaySessionApiV1ReplaySessionsSessionIdPausePostErrors, PauseReplaySessionApiV1ReplaySessionsSessionIdPausePostResponses, PodCreatedEvent, PodDeletedEvent, PodFailedEvent, PodRunningEvent, PodScheduledEvent, PodSucceededEvent, PodTerminatedEvent, PriorityUpdateRequest, QueuePriority, QueueStatusResponse, QuotaExceededEvent, RateLimitAlgorithm, RateLimitExceededEvent, RateLimitRuleRequest, RateLimitRuleResponse, RateLimitSummary, RateLimitUpdateRequest, RateLimitUpdateResponse, RegisterApiV1AuthRegisterPostData, RegisterApiV1AuthRegisterPostErrors, RegisterApiV1AuthRegisterPostResponses, ReleaseResourcesCommandEvent, ReplayConfigSchema, ReplayError, ReplayEventsApiV1AdminEventsReplayPostData, ReplayEventsApiV1AdminEventsReplayPostErrors, ReplayEventsApiV1AdminEventsReplayPostResponses, ReplayFilter, ReplayFilterSchema, ReplayRequest, ReplayResponse, ReplaySession, ReplayStatus, ReplayTarget, ReplayType, ResetSystemSettingsApiV1AdminSettingsResetPostErrors, ResetSystemSettingsApiV1AdminSettingsResetPostResponses, ResetUserPasswordApiV1AdminUsersUserIdResetPasswordPostData, ResetUserPasswordApiV1AdminUsersUserIdResetPasswordPostErrors, ResetUserPasswordApiV1AdminUsersUserIdResetPasswordPostResponses, ResetUserRateLimitsApiV1AdminRateLimitsUserIdResetPostData, ResetUserRateLimitsApiV1AdminRateLimitsUserIdResetPostErrors, ResetUserRateLimitsApiV1AdminRateLimitsUserIdResetPostResponses, ResourceLimitExceededEvent, ResourceLimits, ResourceUsage, ResourceUsageDomain, RestoreSettingsApiV1UserSettingsRestorePostData, RestoreSettingsApiV1UserSettingsRestorePostErrors, RestoreSettingsApiV1UserSettingsRestorePostResponses, RestoreSettingsRequest, ResultFailedEvent, ResultStoredEvent, ResumeReplaySessionApiV1ReplaySessionsSessionIdResumePostData, ResumeReplaySessionApiV1ReplaySessionsSessionIdResumePostErrors, ResumeReplaySessionApiV1ReplaySessionsSessionIdResumePostResponses, RetryExecutionApiV1ExecutionsExecutionIdRetryPostData, RetryExecutionApiV1ExecutionsExecutionIdRetryPostErrors, RetryExecutionApiV1ExecutionsExecutionIdRetryPostResponses, SagaCancellationResponse, SagaCancelledEvent, SagaCompensatedEvent, SagaCompensatingEvent, SagaCompletedEvent, SagaFailedEvent, SagaListResponse, SagaStartedEvent, SagaState, SagaStatusResponse, SavedScriptCreateRequest, SavedScriptListResponse, SavedScriptResponse, SavedScriptUpdate, ScriptDeletedEvent, ScriptSavedEvent, ScriptSharedEvent, SecurityViolationEvent, ServiceEventCount, ServiceRecoveredEvent, ServiceUnhealthyEvent, SessionConfigSummary, SessionSummary, SessionSummaryWritable, SettingsHistoryEntry, SettingsHistoryResponse, SseControlEvent, SseExecutionEventSchema, StartReplaySessionApiV1ReplaySessionsSessionIdStartPostData, StartReplaySessionApiV1ReplaySessionsSessionIdStartPostErrors, StartReplaySessionApiV1ReplaySessionsSessionIdStartPostResponses, StorageType, StreamReplayStatusApiV1AdminEventsReplaySessionIdStatusGetData, StreamReplayStatusApiV1AdminEventsReplaySessionIdStatusGetErrors, StreamReplayStatusApiV1AdminEventsReplaySessionIdStatusGetResponses, SubscriptionsResponse, SubscriptionUpdate, SystemErrorEvent, SystemSettingsSchemaInput, SystemSettingsSchemaOutput, Theme, ThemeUpdateRequest, UnlockResponse, UnlockUserApiV1AdminUsersUserIdUnlockPostData, UnlockUserApiV1AdminUsersUserIdUnlockPostErrors, UnlockUserApiV1AdminUsersUserIdUnlockPostResponses, UnreadCountResponse, UpdateCustomSettingApiV1UserSettingsCustomKeyPutData, UpdateCustomSettingApiV1UserSettingsCustomKeyPutErrors, UpdateCustomSettingApiV1UserSettingsCustomKeyPutResponses, UpdateEditorSettingsApiV1UserSettingsEditorPutData, UpdateEditorSettingsApiV1UserSettingsEditorPutErrors, UpdateEditorSettingsApiV1UserSettingsEditorPutResponses, UpdateNotificationSettingsApiV1UserSettingsNotificationsPutData, UpdateNotificationSettingsApiV1UserSettingsNotificationsPutErrors, UpdateNotificationSettingsApiV1UserSettingsNotificationsPutResponses, UpdatePriorityApiV1AdminExecutionsExecutionIdPriorityPutData, UpdatePriorityApiV1AdminExecutionsExecutionIdPriorityPutErrors, UpdatePriorityApiV1AdminExecutionsExecutionIdPriorityPutResponses, UpdateSavedScriptApiV1ScriptsScriptIdPutData, UpdateSavedScriptApiV1ScriptsScriptIdPutErrors, UpdateSavedScriptApiV1ScriptsScriptIdPutResponses, UpdateSubscriptionApiV1NotificationsSubscriptionsChannelPutData, UpdateSubscriptionApiV1NotificationsSubscriptionsChannelPutErrors, UpdateSubscriptionApiV1NotificationsSubscriptionsChannelPutResponses, UpdateSystemSettingsApiV1AdminSettingsPutData, UpdateSystemSettingsApiV1AdminSettingsPutErrors, UpdateSystemSettingsApiV1AdminSettingsPutResponses, UpdateThemeApiV1UserSettingsThemePutData, UpdateThemeApiV1UserSettingsThemePutErrors, UpdateThemeApiV1UserSettingsThemePutResponses, UpdateUserApiV1AdminUsersUserIdPutData, UpdateUserApiV1AdminUsersUserIdPutErrors, UpdateUserApiV1AdminUsersUserIdPutResponses, UpdateUserRateLimitsApiV1AdminRateLimitsUserIdPutData, UpdateUserRateLimitsApiV1AdminRateLimitsUserIdPutErrors, UpdateUserRateLimitsApiV1AdminRateLimitsUserIdPutResponses, UpdateUserSettingsApiV1UserSettingsPutData, UpdateUserSettingsApiV1UserSettingsPutErrors, UpdateUserSettingsApiV1UserSettingsPutResponses, UserCreate, UserCreateWritable, UserDeletedEvent, UserEventCount, UserListResponse, UserLoggedInEvent, UserLoggedOutEvent, UserLoginEvent, UserRateLimitConfigResponse, UserRateLimitsResponse, UserRegisteredEvent, UserResponse, UserRole, UserSettings, UserSettingsUpdate, UserSettingsUpdatedEvent, UserUpdate, UserUpdatedEvent, UserUpdateWritable } from '../types.gen'; +import type { AdminExecutionListResponse, AdminExecutionResponse, AdminUserOverview, AuthFailedEvent, BodyLoginApiV1AuthLoginPost, BrowseEventsApiV1AdminEventsBrowsePostData, BrowseEventsApiV1AdminEventsBrowsePostErrors, BrowseEventsApiV1AdminEventsBrowsePostResponses, CancelExecutionApiV1ExecutionsExecutionIdCancelPostData, CancelExecutionApiV1ExecutionsExecutionIdCancelPostErrors, CancelExecutionApiV1ExecutionsExecutionIdCancelPostResponses, CancelExecutionRequest, CancelReplaySessionApiV1ReplaySessionsSessionIdCancelPostData, CancelReplaySessionApiV1ReplaySessionsSessionIdCancelPostErrors, CancelReplaySessionApiV1ReplaySessionsSessionIdCancelPostResponses, CancelResponse, CancelStatus, CleanupOldSessionsApiV1ReplayCleanupPostData, CleanupOldSessionsApiV1ReplayCleanupPostErrors, CleanupOldSessionsApiV1ReplayCleanupPostResponses, CleanupResponse, ContainerStatusInfo, CreateExecutionApiV1ExecutePostData, CreateExecutionApiV1ExecutePostErrors, CreateExecutionApiV1ExecutePostResponses, CreateReplaySessionApiV1ReplaySessionsPostData, CreateReplaySessionApiV1ReplaySessionsPostErrors, CreateReplaySessionApiV1ReplaySessionsPostResponses, CreateSavedScriptApiV1ScriptsPostData, CreateSavedScriptApiV1ScriptsPostErrors, CreateSavedScriptApiV1ScriptsPostResponses, CreateUserApiV1AdminUsersPostData, CreateUserApiV1AdminUsersPostErrors, CreateUserApiV1AdminUsersPostResponses, DeleteEventApiV1AdminEventsEventIdDeleteData, DeleteEventApiV1AdminEventsEventIdDeleteErrors, DeleteEventApiV1AdminEventsEventIdDeleteResponses, DeleteExecutionApiV1ExecutionsExecutionIdDeleteData, DeleteExecutionApiV1ExecutionsExecutionIdDeleteErrors, DeleteExecutionApiV1ExecutionsExecutionIdDeleteResponses, DeleteNotificationApiV1NotificationsNotificationIdDeleteData, DeleteNotificationApiV1NotificationsNotificationIdDeleteErrors, DeleteNotificationApiV1NotificationsNotificationIdDeleteResponses, DeleteNotificationResponse, DeleteResponse, DeleteSavedScriptApiV1ScriptsScriptIdDeleteData, DeleteSavedScriptApiV1ScriptsScriptIdDeleteErrors, DeleteUserApiV1AdminUsersUserIdDeleteData, DeleteUserApiV1AdminUsersUserIdDeleteErrors, DeleteUserApiV1AdminUsersUserIdDeleteResponses, DeleteUserResponse, DerivedCounts, DlqMessageDiscardedEvent, DlqMessageReceivedEvent, DlqMessageRetriedEvent, EditorSettingsInput, EditorSettingsOutput, EndpointGroup, EndpointUsageStats, Environment, ErrorResponse, EventBrowseRequest, EventBrowseResponse, EventDeleteResponse, EventDetailResponse, EventFilter, EventMetadata, EventReplayRequest, EventReplayResponse, EventReplayStatusResponse, EventReplayStatusResponseWritable, EventStatistics, EventStatsResponse, EventSummary, EventType, EventTypeCount, ExampleScripts, ExecutionAcceptedEvent, ExecutionCancelledEvent, ExecutionCompletedEvent, ExecutionErrorType, ExecutionEventsApiV1EventsExecutionsExecutionIdGetData, ExecutionEventsApiV1EventsExecutionsExecutionIdGetErrors, ExecutionEventsApiV1EventsExecutionsExecutionIdGetResponses, ExecutionFailedEvent, ExecutionListResponse, ExecutionQueuedEvent, ExecutionRequest, ExecutionRequestedEvent, ExecutionResponse, ExecutionResult, ExecutionRunningEvent, ExecutionStartedEvent, ExecutionStatus, ExecutionTimeoutEvent, ExportEventsApiV1AdminEventsExportExportFormatGetData, ExportEventsApiV1AdminEventsExportExportFormatGetErrors, ExportEventsApiV1AdminEventsExportExportFormatGetResponses, ExportFormat, GetCurrentUserProfileApiV1AuthMeGetErrors, GetCurrentUserProfileApiV1AuthMeGetResponses, GetDefaultRateLimitRulesApiV1AdminRateLimitsDefaultsGetErrors, GetDefaultRateLimitRulesApiV1AdminRateLimitsDefaultsGetResponses, GetEventDetailApiV1AdminEventsEventIdGetData, GetEventDetailApiV1AdminEventsEventIdGetErrors, GetEventDetailApiV1AdminEventsEventIdGetResponses, GetEventStatsApiV1AdminEventsStatsGetData, GetEventStatsApiV1AdminEventsStatsGetErrors, GetEventStatsApiV1AdminEventsStatsGetResponses, GetExampleScriptsApiV1ExampleScriptsGetErrors, GetExampleScriptsApiV1ExampleScriptsGetResponses, GetExecutionEventsApiV1ExecutionsExecutionIdEventsGetData, GetExecutionEventsApiV1ExecutionsExecutionIdEventsGetErrors, GetExecutionEventsApiV1ExecutionsExecutionIdEventsGetResponses, GetK8sResourceLimitsApiV1K8sLimitsGetErrors, GetK8sResourceLimitsApiV1K8sLimitsGetResponses, GetNotificationsApiV1NotificationsGetData, GetNotificationsApiV1NotificationsGetErrors, GetNotificationsApiV1NotificationsGetResponses, GetQueueStatusApiV1AdminExecutionsQueueGetErrors, GetQueueStatusApiV1AdminExecutionsQueueGetResponses, GetReplaySessionApiV1ReplaySessionsSessionIdGetData, GetReplaySessionApiV1ReplaySessionsSessionIdGetErrors, GetReplaySessionApiV1ReplaySessionsSessionIdGetResponses, GetResultApiV1ExecutionsExecutionIdResultGetData, GetResultApiV1ExecutionsExecutionIdResultGetErrors, GetResultApiV1ExecutionsExecutionIdResultGetResponses, GetSavedScriptApiV1ScriptsScriptIdGetData, GetSavedScriptApiV1ScriptsScriptIdGetErrors, GetSavedScriptApiV1ScriptsScriptIdGetResponses, GetSettingsHistoryApiV1UserSettingsHistoryGetData, GetSettingsHistoryApiV1UserSettingsHistoryGetErrors, GetSettingsHistoryApiV1UserSettingsHistoryGetResponses, GetSubscriptionsApiV1NotificationsSubscriptionsGetErrors, GetSubscriptionsApiV1NotificationsSubscriptionsGetResponses, GetSystemSettingsApiV1AdminSettingsGetErrors, GetSystemSettingsApiV1AdminSettingsGetResponses, GetUnreadCountApiV1NotificationsUnreadCountGetErrors, GetUnreadCountApiV1NotificationsUnreadCountGetResponses, GetUserApiV1AdminUsersUserIdGetData, GetUserApiV1AdminUsersUserIdGetErrors, GetUserApiV1AdminUsersUserIdGetResponses, GetUserExecutionsApiV1UserExecutionsGetData, GetUserExecutionsApiV1UserExecutionsGetErrors, GetUserExecutionsApiV1UserExecutionsGetResponses, GetUserOverviewApiV1AdminUsersUserIdOverviewGetData, GetUserOverviewApiV1AdminUsersUserIdOverviewGetErrors, GetUserOverviewApiV1AdminUsersUserIdOverviewGetResponses, GetUserRateLimitsApiV1AdminRateLimitsUserIdGetData, GetUserRateLimitsApiV1AdminRateLimitsUserIdGetErrors, GetUserRateLimitsApiV1AdminRateLimitsUserIdGetResponses, GetUserSettingsApiV1UserSettingsGetErrors, GetUserSettingsApiV1UserSettingsGetResponses, HourlyEventCount, LanguageInfo, LanguageLimits, ListExecutionsApiV1AdminExecutionsGetData, ListExecutionsApiV1AdminExecutionsGetErrors, ListExecutionsApiV1AdminExecutionsGetResponses, ListReplaySessionsApiV1ReplaySessionsGetData, ListReplaySessionsApiV1ReplaySessionsGetErrors, ListReplaySessionsApiV1ReplaySessionsGetResponses, ListSavedScriptsApiV1ScriptsGetErrors, ListSavedScriptsApiV1ScriptsGetResponses, ListUsersApiV1AdminUsersGetData, ListUsersApiV1AdminUsersGetErrors, ListUsersApiV1AdminUsersGetResponses, LivenessApiV1HealthLiveGetErrors, LivenessApiV1HealthLiveGetResponses, LivenessResponse, LoginApiV1AuthLoginPostData, LoginApiV1AuthLoginPostErrors, LoginApiV1AuthLoginPostResponses, LoginMethod, LoginResponse, LogLevel, LogoutApiV1AuthLogoutPostErrors, LogoutApiV1AuthLogoutPostResponses, MarkAllReadApiV1NotificationsMarkAllReadPostErrors, MarkNotificationReadApiV1NotificationsNotificationIdReadPutData, MarkNotificationReadApiV1NotificationsNotificationIdReadPutErrors, MessageResponse, NotificationAllReadEvent, NotificationChannel, NotificationClickedEvent, NotificationCreatedEvent, NotificationDeliveredEvent, NotificationFailedEvent, NotificationListResponse, NotificationPreferencesUpdatedEvent, NotificationReadEvent, NotificationResponse, NotificationSentEvent, NotificationSettingsInput, NotificationSettingsOutput, NotificationSeverity, NotificationStatus, NotificationStreamApiV1EventsNotificationsStreamGetErrors, NotificationStreamApiV1EventsNotificationsStreamGetResponses, NotificationSubscription, PasswordResetRequest, PauseReplaySessionApiV1ReplaySessionsSessionIdPausePostData, PauseReplaySessionApiV1ReplaySessionsSessionIdPausePostErrors, PauseReplaySessionApiV1ReplaySessionsSessionIdPausePostResponses, PodCreatedEvent, PodDeletedEvent, PodFailedEvent, PodRunningEvent, PodScheduledEvent, PodSucceededEvent, PodTerminatedEvent, PriorityUpdateRequest, QueuePriority, QueueStatusResponse, QuotaExceededEvent, RateLimitAlgorithm, RateLimitExceededEvent, RateLimitRuleRequest, RateLimitRuleResponse, RateLimitSummary, RateLimitUpdateRequest, RateLimitUpdateResponse, RegisterApiV1AuthRegisterPostData, RegisterApiV1AuthRegisterPostErrors, RegisterApiV1AuthRegisterPostResponses, ReplayConfigSchema, ReplayError, ReplayEventsApiV1AdminEventsReplayPostData, ReplayEventsApiV1AdminEventsReplayPostErrors, ReplayEventsApiV1AdminEventsReplayPostResponses, ReplayFilter, ReplayFilterSchema, ReplayRequest, ReplayResponse, ReplaySession, ReplayStatus, ReplayTarget, ReplayType, ResetSystemSettingsApiV1AdminSettingsResetPostErrors, ResetSystemSettingsApiV1AdminSettingsResetPostResponses, ResetUserPasswordApiV1AdminUsersUserIdResetPasswordPostData, ResetUserPasswordApiV1AdminUsersUserIdResetPasswordPostErrors, ResetUserPasswordApiV1AdminUsersUserIdResetPasswordPostResponses, ResetUserRateLimitsApiV1AdminRateLimitsUserIdResetPostData, ResetUserRateLimitsApiV1AdminRateLimitsUserIdResetPostErrors, ResetUserRateLimitsApiV1AdminRateLimitsUserIdResetPostResponses, ResourceLimitExceededEvent, ResourceLimits, ResourceUsage, ResourceUsageDomain, RestoreSettingsApiV1UserSettingsRestorePostData, RestoreSettingsApiV1UserSettingsRestorePostErrors, RestoreSettingsApiV1UserSettingsRestorePostResponses, RestoreSettingsRequest, ResultFailedEvent, ResultStoredEvent, ResumeReplaySessionApiV1ReplaySessionsSessionIdResumePostData, ResumeReplaySessionApiV1ReplaySessionsSessionIdResumePostErrors, ResumeReplaySessionApiV1ReplaySessionsSessionIdResumePostResponses, RetryExecutionApiV1ExecutionsExecutionIdRetryPostData, RetryExecutionApiV1ExecutionsExecutionIdRetryPostErrors, RetryExecutionApiV1ExecutionsExecutionIdRetryPostResponses, SavedScriptCreateRequest, SavedScriptListResponse, SavedScriptResponse, SavedScriptUpdate, ScriptDeletedEvent, ScriptSavedEvent, ScriptSharedEvent, SecurityViolationEvent, ServiceEventCount, ServiceRecoveredEvent, ServiceUnhealthyEvent, SessionConfigSummary, SessionSummary, SessionSummaryWritable, SettingsHistoryEntry, SettingsHistoryResponse, SseControlEvent, SseExecutionEventSchema, StartReplaySessionApiV1ReplaySessionsSessionIdStartPostData, StartReplaySessionApiV1ReplaySessionsSessionIdStartPostErrors, StartReplaySessionApiV1ReplaySessionsSessionIdStartPostResponses, StorageType, StreamReplayStatusApiV1AdminEventsReplaySessionIdStatusGetData, StreamReplayStatusApiV1AdminEventsReplaySessionIdStatusGetErrors, StreamReplayStatusApiV1AdminEventsReplaySessionIdStatusGetResponses, SubscriptionsResponse, SubscriptionUpdate, SystemErrorEvent, SystemSettingsSchemaInput, SystemSettingsSchemaOutput, Theme, ThemeUpdateRequest, UnlockResponse, UnlockUserApiV1AdminUsersUserIdUnlockPostData, UnlockUserApiV1AdminUsersUserIdUnlockPostErrors, UnlockUserApiV1AdminUsersUserIdUnlockPostResponses, UnreadCountResponse, UpdateCustomSettingApiV1UserSettingsCustomKeyPutData, UpdateCustomSettingApiV1UserSettingsCustomKeyPutErrors, UpdateCustomSettingApiV1UserSettingsCustomKeyPutResponses, UpdateEditorSettingsApiV1UserSettingsEditorPutData, UpdateEditorSettingsApiV1UserSettingsEditorPutErrors, UpdateEditorSettingsApiV1UserSettingsEditorPutResponses, UpdateNotificationSettingsApiV1UserSettingsNotificationsPutData, UpdateNotificationSettingsApiV1UserSettingsNotificationsPutErrors, UpdateNotificationSettingsApiV1UserSettingsNotificationsPutResponses, UpdatePriorityApiV1AdminExecutionsExecutionIdPriorityPutData, UpdatePriorityApiV1AdminExecutionsExecutionIdPriorityPutErrors, UpdatePriorityApiV1AdminExecutionsExecutionIdPriorityPutResponses, UpdateSavedScriptApiV1ScriptsScriptIdPutData, UpdateSavedScriptApiV1ScriptsScriptIdPutErrors, UpdateSavedScriptApiV1ScriptsScriptIdPutResponses, UpdateSubscriptionApiV1NotificationsSubscriptionsChannelPutData, UpdateSubscriptionApiV1NotificationsSubscriptionsChannelPutErrors, UpdateSubscriptionApiV1NotificationsSubscriptionsChannelPutResponses, UpdateSystemSettingsApiV1AdminSettingsPutData, UpdateSystemSettingsApiV1AdminSettingsPutErrors, UpdateSystemSettingsApiV1AdminSettingsPutResponses, UpdateThemeApiV1UserSettingsThemePutData, UpdateThemeApiV1UserSettingsThemePutErrors, UpdateThemeApiV1UserSettingsThemePutResponses, UpdateUserApiV1AdminUsersUserIdPutData, UpdateUserApiV1AdminUsersUserIdPutErrors, UpdateUserApiV1AdminUsersUserIdPutResponses, UpdateUserRateLimitsApiV1AdminRateLimitsUserIdPutData, UpdateUserRateLimitsApiV1AdminRateLimitsUserIdPutErrors, UpdateUserRateLimitsApiV1AdminRateLimitsUserIdPutResponses, UpdateUserSettingsApiV1UserSettingsPutData, UpdateUserSettingsApiV1UserSettingsPutErrors, UpdateUserSettingsApiV1UserSettingsPutResponses, UserCreate, UserCreateWritable, UserDeletedEvent, UserEventCount, UserListResponse, UserLoggedInEvent, UserLoggedOutEvent, UserLoginEvent, UserRateLimitConfigResponse, UserRateLimitsResponse, UserRegisteredEvent, UserResponse, UserRole, UserSettings, UserSettingsUpdate, UserSettingsUpdatedEvent, UserUpdate, UserUpdatedEvent, UserUpdateWritable } from '../types.gen'; export type Options = { faker?: Faker; @@ -97,8 +97,7 @@ export const fakeDeleteUserResponse = (options?: Options): DeleteUserResponse => saved_scripts: resolveCondition(options?.useDefault ?? false, f) ? 0 : f.number.int(), notifications: resolveCondition(options?.useDefault ?? false, f) ? 0 : f.number.int(), user_settings: resolveCondition(options?.useDefault ?? false, f) ? 0 : f.number.int(), - events: resolveCondition(options?.useDefault ?? false, f) ? 0 : f.number.int(), - sagas: resolveCondition(options?.useDefault ?? false, f) ? 0 : f.number.int() + events: resolveCondition(options?.useDefault ?? false, f) ? 0 : f.number.int() }; }; @@ -192,21 +191,6 @@ export const fakeEventMetadata = (options?: Options): EventMetadata => { }; }; -export const fakeAllocateResourcesCommandEvent = (options?: Options): AllocateResourcesCommandEvent => { - const f = options?.faker ?? faker; - return { - event_id: f.string.sample(), - event_type: 'allocate_resources_command', - event_version: resolveCondition(options?.useDefault ?? false, f) ? '1.0' : f.string.sample(), - timestamp: f.date.recent().toISOString(), - ...!resolveCondition(options?.includeOptional ?? true, f) ? {} : { aggregate_id: f.datatype.boolean() ? f.string.sample() : null }, - metadata: fakeEventMetadata(options), - ...!resolveCondition(options?.includeOptional ?? true, f) ? {} : { execution_id: f.string.sample() }, - ...!resolveCondition(options?.includeOptional ?? true, f) ? {} : { cpu_request: f.string.sample() }, - ...!resolveCondition(options?.includeOptional ?? true, f) ? {} : { memory_request: f.string.sample() } - }; -}; - export const fakeAuthFailedEvent = (options?: Options): AuthFailedEvent => { const f = options?.faker ?? faker; return { @@ -222,23 +206,6 @@ export const fakeAuthFailedEvent = (options?: Options): AuthFailedEvent => { }; }; -export const fakeDeletePodCommandEvent = (options?: Options): DeletePodCommandEvent => { - const f = options?.faker ?? faker; - return { - event_id: f.string.sample(), - event_type: 'delete_pod_command', - event_version: resolveCondition(options?.useDefault ?? false, f) ? '1.0' : f.string.sample(), - timestamp: f.date.recent().toISOString(), - ...!resolveCondition(options?.includeOptional ?? true, f) ? {} : { aggregate_id: f.datatype.boolean() ? f.string.sample() : null }, - metadata: fakeEventMetadata(options), - ...!resolveCondition(options?.includeOptional ?? true, f) ? {} : { saga_id: f.string.sample() }, - ...!resolveCondition(options?.includeOptional ?? true, f) ? {} : { execution_id: f.string.sample() }, - ...!resolveCondition(options?.includeOptional ?? true, f) ? {} : { reason: f.string.sample() }, - ...!resolveCondition(options?.includeOptional ?? true, f) ? {} : { pod_name: f.datatype.boolean() ? f.string.sample() : null }, - ...!resolveCondition(options?.includeOptional ?? true, f) ? {} : { namespace: f.datatype.boolean() ? f.string.sample() : null } - }; -}; - export const fakeEventReplayRequest = (options?: Options): EventReplayRequest => { const f = options?.faker ?? faker; return { @@ -298,16 +265,6 @@ export const fakeEventType = (options?: Options): EventType => { 'service_recovered', 'result_stored', 'result_failed', - 'saga_started', - 'saga_completed', - 'saga_failed', - 'saga_cancelled', - 'saga_compensating', - 'saga_compensated', - 'create_pod_command', - 'delete_pod_command', - 'allocate_resources_command', - 'release_resources_command', 'dlq_message_received', 'dlq_message_retried', 'dlq_message_discarded' @@ -976,32 +933,6 @@ export const fakeAdminExecutionListResponse = (options?: Options): AdminExecutio }; }; -export const fakeCreatePodCommandEvent = (options?: Options): CreatePodCommandEvent => { - const f = options?.faker ?? faker; - return { - event_id: f.string.sample(), - event_type: 'create_pod_command', - event_version: resolveCondition(options?.useDefault ?? false, f) ? '1.0' : f.string.sample(), - timestamp: f.date.recent().toISOString(), - ...!resolveCondition(options?.includeOptional ?? true, f) ? {} : { aggregate_id: f.datatype.boolean() ? f.string.sample() : null }, - metadata: fakeEventMetadata(options), - ...!resolveCondition(options?.includeOptional ?? true, f) ? {} : { saga_id: f.string.sample() }, - ...!resolveCondition(options?.includeOptional ?? true, f) ? {} : { execution_id: f.string.sample() }, - ...!resolveCondition(options?.includeOptional ?? true, f) ? {} : { script: f.string.sample() }, - ...!resolveCondition(options?.includeOptional ?? true, f) ? {} : { language: f.string.sample() }, - ...!resolveCondition(options?.includeOptional ?? true, f) ? {} : { language_version: f.string.sample() }, - ...!resolveCondition(options?.includeOptional ?? true, f) ? {} : { runtime_image: f.string.sample() }, - ...!resolveCondition(options?.includeOptional ?? true, f) ? {} : { runtime_command: f.helpers.multiple(() => f.string.sample()) }, - ...!resolveCondition(options?.includeOptional ?? true, f) ? {} : { runtime_filename: f.string.sample() }, - ...!resolveCondition(options?.includeOptional ?? true, f) ? {} : { timeout_seconds: f.number.int() }, - ...!resolveCondition(options?.includeOptional ?? true, f) ? {} : { cpu_limit: f.string.sample() }, - ...!resolveCondition(options?.includeOptional ?? true, f) ? {} : { memory_limit: f.string.sample() }, - ...!resolveCondition(options?.includeOptional ?? true, f) ? {} : { cpu_request: f.string.sample() }, - ...!resolveCondition(options?.includeOptional ?? true, f) ? {} : { memory_request: f.string.sample() }, - ...!resolveCondition(options?.includeOptional ?? true, f) ? {} : { priority: fakeQueuePriority(options) } - }; -}; - export const fakeExecutionAcceptedEvent = (options?: Options): ExecutionAcceptedEvent => { const f = options?.faker ?? faker; return { @@ -1156,21 +1087,6 @@ export const fakeRateLimitUpdateRequest = (options?: Options): RateLimitUpdateRe }; }; -export const fakeReleaseResourcesCommandEvent = (options?: Options): ReleaseResourcesCommandEvent => { - const f = options?.faker ?? faker; - return { - event_id: f.string.sample(), - event_type: 'release_resources_command', - event_version: resolveCondition(options?.useDefault ?? false, f) ? '1.0' : f.string.sample(), - timestamp: f.date.recent().toISOString(), - ...!resolveCondition(options?.includeOptional ?? true, f) ? {} : { aggregate_id: f.datatype.boolean() ? f.string.sample() : null }, - metadata: fakeEventMetadata(options), - ...!resolveCondition(options?.includeOptional ?? true, f) ? {} : { execution_id: f.string.sample() }, - ...!resolveCondition(options?.includeOptional ?? true, f) ? {} : { cpu_request: f.string.sample() }, - ...!resolveCondition(options?.includeOptional ?? true, f) ? {} : { memory_request: f.string.sample() } - }; -}; - export const fakeReplayError = (options?: Options): ReplayError => { const f = options?.faker ?? faker; return { @@ -1495,158 +1411,6 @@ export const fakeSseExecutionEventSchema = (options?: Options): SseExecutionEven }; }; -export const fakeSagaCancellationResponse = (options?: Options): SagaCancellationResponse => { - const f = options?.faker ?? faker; - return { - success: f.datatype.boolean(), - message: f.string.sample(), - saga_id: f.string.sample() - }; -}; - -export const fakeSagaCancelledEvent = (options?: Options): SagaCancelledEvent => { - const f = options?.faker ?? faker; - return { - event_id: f.string.sample(), - event_type: 'saga_cancelled', - event_version: resolveCondition(options?.useDefault ?? false, f) ? '1.0' : f.string.sample(), - timestamp: f.date.recent().toISOString(), - ...!resolveCondition(options?.includeOptional ?? true, f) ? {} : { aggregate_id: f.datatype.boolean() ? f.string.sample() : null }, - metadata: fakeEventMetadata(options), - ...!resolveCondition(options?.includeOptional ?? true, f) ? {} : { saga_id: f.string.sample() }, - ...!resolveCondition(options?.includeOptional ?? true, f) ? {} : { saga_name: f.string.sample() }, - ...!resolveCondition(options?.includeOptional ?? true, f) ? {} : { execution_id: f.string.sample() }, - ...!resolveCondition(options?.includeOptional ?? true, f) ? {} : { reason: f.string.sample() }, - ...!resolveCondition(options?.includeOptional ?? true, f) ? {} : { completed_steps: f.helpers.multiple(() => f.string.sample()) }, - ...!resolveCondition(options?.includeOptional ?? true, f) ? {} : { compensated_steps: f.helpers.multiple(() => f.string.sample()) }, - ...!resolveCondition(options?.includeOptional ?? true, f) ? {} : { cancelled_at: f.datatype.boolean() ? f.date.recent().toISOString() : null }, - ...!resolveCondition(options?.includeOptional ?? true, f) ? {} : { cancelled_by: f.datatype.boolean() ? f.string.sample() : null } - }; -}; - -export const fakeSagaCompensatedEvent = (options?: Options): SagaCompensatedEvent => { - const f = options?.faker ?? faker; - return { - event_id: f.string.sample(), - event_type: 'saga_compensated', - event_version: resolveCondition(options?.useDefault ?? false, f) ? '1.0' : f.string.sample(), - timestamp: f.date.recent().toISOString(), - ...!resolveCondition(options?.includeOptional ?? true, f) ? {} : { aggregate_id: f.datatype.boolean() ? f.string.sample() : null }, - metadata: fakeEventMetadata(options), - ...!resolveCondition(options?.includeOptional ?? true, f) ? {} : { saga_id: f.string.sample() }, - ...!resolveCondition(options?.includeOptional ?? true, f) ? {} : { saga_name: f.string.sample() }, - ...!resolveCondition(options?.includeOptional ?? true, f) ? {} : { execution_id: f.string.sample() }, - ...!resolveCondition(options?.includeOptional ?? true, f) ? {} : { compensated_steps: f.helpers.multiple(() => f.string.sample()) } - }; -}; - -export const fakeSagaCompensatingEvent = (options?: Options): SagaCompensatingEvent => { - const f = options?.faker ?? faker; - return { - event_id: f.string.sample(), - event_type: 'saga_compensating', - event_version: resolveCondition(options?.useDefault ?? false, f) ? '1.0' : f.string.sample(), - timestamp: f.date.recent().toISOString(), - ...!resolveCondition(options?.includeOptional ?? true, f) ? {} : { aggregate_id: f.datatype.boolean() ? f.string.sample() : null }, - metadata: fakeEventMetadata(options), - ...!resolveCondition(options?.includeOptional ?? true, f) ? {} : { saga_id: f.string.sample() }, - ...!resolveCondition(options?.includeOptional ?? true, f) ? {} : { saga_name: f.string.sample() }, - ...!resolveCondition(options?.includeOptional ?? true, f) ? {} : { execution_id: f.string.sample() }, - ...!resolveCondition(options?.includeOptional ?? true, f) ? {} : { compensating_step: f.string.sample() } - }; -}; - -export const fakeSagaCompletedEvent = (options?: Options): SagaCompletedEvent => { - const f = options?.faker ?? faker; - return { - event_id: f.string.sample(), - event_type: 'saga_completed', - event_version: resolveCondition(options?.useDefault ?? false, f) ? '1.0' : f.string.sample(), - timestamp: f.date.recent().toISOString(), - ...!resolveCondition(options?.includeOptional ?? true, f) ? {} : { aggregate_id: f.datatype.boolean() ? f.string.sample() : null }, - metadata: fakeEventMetadata(options), - ...!resolveCondition(options?.includeOptional ?? true, f) ? {} : { saga_id: f.string.sample() }, - ...!resolveCondition(options?.includeOptional ?? true, f) ? {} : { saga_name: f.string.sample() }, - ...!resolveCondition(options?.includeOptional ?? true, f) ? {} : { execution_id: f.string.sample() }, - ...!resolveCondition(options?.includeOptional ?? true, f) ? {} : { completed_steps: f.helpers.multiple(() => f.string.sample()) } - }; -}; - -export const fakeSagaFailedEvent = (options?: Options): SagaFailedEvent => { - const f = options?.faker ?? faker; - return { - event_id: f.string.sample(), - event_type: 'saga_failed', - event_version: resolveCondition(options?.useDefault ?? false, f) ? '1.0' : f.string.sample(), - timestamp: f.date.recent().toISOString(), - ...!resolveCondition(options?.includeOptional ?? true, f) ? {} : { aggregate_id: f.datatype.boolean() ? f.string.sample() : null }, - metadata: fakeEventMetadata(options), - ...!resolveCondition(options?.includeOptional ?? true, f) ? {} : { saga_id: f.string.sample() }, - ...!resolveCondition(options?.includeOptional ?? true, f) ? {} : { saga_name: f.string.sample() }, - ...!resolveCondition(options?.includeOptional ?? true, f) ? {} : { execution_id: f.string.sample() }, - ...!resolveCondition(options?.includeOptional ?? true, f) ? {} : { failed_step: f.string.sample() }, - ...!resolveCondition(options?.includeOptional ?? true, f) ? {} : { error: f.string.sample() } - }; -}; - -export const fakeSagaStartedEvent = (options?: Options): SagaStartedEvent => { - const f = options?.faker ?? faker; - return { - event_id: f.string.sample(), - event_type: 'saga_started', - event_version: resolveCondition(options?.useDefault ?? false, f) ? '1.0' : f.string.sample(), - timestamp: f.date.recent().toISOString(), - ...!resolveCondition(options?.includeOptional ?? true, f) ? {} : { aggregate_id: f.datatype.boolean() ? f.string.sample() : null }, - metadata: fakeEventMetadata(options), - ...!resolveCondition(options?.includeOptional ?? true, f) ? {} : { saga_id: f.string.sample() }, - ...!resolveCondition(options?.includeOptional ?? true, f) ? {} : { saga_name: f.string.sample() }, - ...!resolveCondition(options?.includeOptional ?? true, f) ? {} : { execution_id: f.string.sample() }, - ...!resolveCondition(options?.includeOptional ?? true, f) ? {} : { initial_event_id: f.string.sample() } - }; -}; - -export const fakeSagaState = (options?: Options): SagaState => { - const f = options?.faker ?? faker; - return f.helpers.arrayElement([ - 'created', - 'running', - 'compensating', - 'completed', - 'failed', - 'timeout', - 'cancelled' - ]); -}; - -export const fakeSagaStatusResponse = (options?: Options): SagaStatusResponse => { - const f = options?.faker ?? faker; - return { - saga_id: f.string.sample(), - saga_name: f.string.sample(), - execution_id: f.string.sample(), - state: fakeSagaState(options), - current_step: f.datatype.boolean() ? f.string.sample() : null, - completed_steps: f.helpers.multiple(() => f.string.sample()), - compensated_steps: f.helpers.multiple(() => f.string.sample()), - error_message: f.datatype.boolean() ? f.string.sample() : null, - created_at: f.date.recent().toISOString(), - updated_at: f.date.recent().toISOString(), - completed_at: f.datatype.boolean() ? f.date.recent().toISOString() : null, - retry_count: f.number.int() - }; -}; - -export const fakeSagaListResponse = (options?: Options): SagaListResponse => { - const f = options?.faker ?? faker; - return { - sagas: f.helpers.multiple(() => fakeSagaStatusResponse(options)), - total: f.number.int(), - skip: f.number.int(), - limit: f.number.int(), - has_more: f.datatype.boolean() - }; -}; - export const fakeSavedScriptCreateRequest = (options?: Options): SavedScriptCreateRequest => { const f = options?.faker ?? faker; return { @@ -2289,16 +2053,6 @@ export const fakeAdminUserOverview = (options?: Options): AdminUserOverview => { fakeNotificationAllReadEvent(options), fakeNotificationClickedEvent(options), fakeNotificationPreferencesUpdatedEvent(options), - fakeSagaStartedEvent(options), - fakeSagaCompletedEvent(options), - fakeSagaFailedEvent(options), - fakeSagaCancelledEvent(options), - fakeSagaCompensatingEvent(options), - fakeSagaCompensatedEvent(options), - fakeCreatePodCommandEvent(options), - fakeDeletePodCommandEvent(options), - fakeAllocateResourcesCommandEvent(options), - fakeReleaseResourcesCommandEvent(options), fakeScriptSavedEvent(options), fakeScriptDeletedEvent(options), fakeScriptSharedEvent(options), @@ -2354,16 +2108,6 @@ export const fakeEventBrowseResponse = (options?: Options): EventBrowseResponse fakeNotificationAllReadEvent(options), fakeNotificationClickedEvent(options), fakeNotificationPreferencesUpdatedEvent(options), - fakeSagaStartedEvent(options), - fakeSagaCompletedEvent(options), - fakeSagaFailedEvent(options), - fakeSagaCancelledEvent(options), - fakeSagaCompensatingEvent(options), - fakeSagaCompensatedEvent(options), - fakeCreatePodCommandEvent(options), - fakeDeletePodCommandEvent(options), - fakeAllocateResourcesCommandEvent(options), - fakeReleaseResourcesCommandEvent(options), fakeScriptSavedEvent(options), fakeScriptDeletedEvent(options), fakeScriptSharedEvent(options), @@ -2422,16 +2166,6 @@ export const fakeEventDetailResponse = (options?: Options): EventDetailResponse fakeNotificationAllReadEvent(options), fakeNotificationClickedEvent(options), fakeNotificationPreferencesUpdatedEvent(options), - fakeSagaStartedEvent(options), - fakeSagaCompletedEvent(options), - fakeSagaFailedEvent(options), - fakeSagaCancelledEvent(options), - fakeSagaCompensatingEvent(options), - fakeSagaCompensatedEvent(options), - fakeCreatePodCommandEvent(options), - fakeDeletePodCommandEvent(options), - fakeAllocateResourcesCommandEvent(options), - fakeReleaseResourcesCommandEvent(options), fakeScriptSavedEvent(options), fakeScriptDeletedEvent(options), fakeScriptSharedEvent(options), @@ -2676,16 +2410,6 @@ export const fakeGetExecutionEventsApiV1ExecutionsExecutionIdEventsGetResponse20 fakeNotificationAllReadEvent(options), fakeNotificationClickedEvent(options), fakeNotificationPreferencesUpdatedEvent(options), - fakeSagaStartedEvent(options), - fakeSagaCompletedEvent(options), - fakeSagaFailedEvent(options), - fakeSagaCancelledEvent(options), - fakeSagaCompensatingEvent(options), - fakeSagaCompensatedEvent(options), - fakeCreatePodCommandEvent(options), - fakeDeletePodCommandEvent(options), - fakeAllocateResourcesCommandEvent(options), - fakeReleaseResourcesCommandEvent(options), fakeScriptSavedEvent(options), fakeScriptDeletedEvent(options), fakeScriptSharedEvent(options), @@ -3408,76 +3132,3 @@ export const fakeDeleteNotificationApiV1NotificationsNotificationIdDeleteRequest export const fakeDeleteNotificationApiV1NotificationsNotificationIdDeleteResponse200 = (options?: Options): DeleteNotificationApiV1NotificationsNotificationIdDeleteResponses[200] => fakeDeleteNotificationResponse(options); export const fakeDeleteNotificationApiV1NotificationsNotificationIdDeleteResponsedefault = (options?: Options): DeleteNotificationApiV1NotificationsNotificationIdDeleteErrors['default'] => fakeErrorResponse(options); - -export const fakeGetSagaStatusApiV1SagasSagaIdGetRequest = (options?: Options): Omit => { - const f = options?.faker ?? faker; - return { - path: { - saga_id: f.string.sample() - } - }; -}; - -export const fakeGetSagaStatusApiV1SagasSagaIdGetResponse200 = (options?: Options): GetSagaStatusApiV1SagasSagaIdGetResponses[200] => fakeSagaStatusResponse(options); - -export const fakeGetSagaStatusApiV1SagasSagaIdGetResponse403 = (options?: Options): GetSagaStatusApiV1SagasSagaIdGetErrors[403] => fakeErrorResponse(options); - -export const fakeGetSagaStatusApiV1SagasSagaIdGetResponse404 = (options?: Options): GetSagaStatusApiV1SagasSagaIdGetErrors[404] => fakeErrorResponse(options); - -export const fakeGetSagaStatusApiV1SagasSagaIdGetResponsedefault = (options?: Options): GetSagaStatusApiV1SagasSagaIdGetErrors['default'] => fakeErrorResponse(options); - -export const fakeGetExecutionSagasApiV1SagasExecutionExecutionIdGetRequest = (options?: Options): Omit => { - const f = options?.faker ?? faker; - return { - path: { - execution_id: f.string.sample() - }, - query: { - ...!resolveCondition(options?.includeOptional ?? true, f) ? {} : { state: f.datatype.boolean() ? fakeSagaState(options) : null }, - ...!resolveCondition(options?.includeOptional ?? true, f) ? {} : { limit: resolveCondition(options?.useDefault ?? false, f) ? 100 : f.number.int({ min: 1, max: 1000 }) }, - ...!resolveCondition(options?.includeOptional ?? true, f) ? {} : { skip: resolveCondition(options?.useDefault ?? false, f) ? 0 : f.number.int({ min: 0 }) } - } - }; -}; - -export const fakeGetExecutionSagasApiV1SagasExecutionExecutionIdGetResponse200 = (options?: Options): GetExecutionSagasApiV1SagasExecutionExecutionIdGetResponses[200] => fakeSagaListResponse(options); - -export const fakeGetExecutionSagasApiV1SagasExecutionExecutionIdGetResponse403 = (options?: Options): GetExecutionSagasApiV1SagasExecutionExecutionIdGetErrors[403] => fakeErrorResponse(options); - -export const fakeGetExecutionSagasApiV1SagasExecutionExecutionIdGetResponsedefault = (options?: Options): GetExecutionSagasApiV1SagasExecutionExecutionIdGetErrors['default'] => fakeErrorResponse(options); - -export const fakeListSagasApiV1SagasGetRequest = (options?: Options): Omit => { - const f = options?.faker ?? faker; - return { - query: { - ...!resolveCondition(options?.includeOptional ?? true, f) ? {} : { state: f.datatype.boolean() ? fakeSagaState(options) : null }, - ...!resolveCondition(options?.includeOptional ?? true, f) ? {} : { execution_id: f.datatype.boolean() ? f.string.sample() : null }, - ...!resolveCondition(options?.includeOptional ?? true, f) ? {} : { search: f.datatype.boolean() ? f.string.alpha({ length: { min: 0, max: 200 } }) : null }, - ...!resolveCondition(options?.includeOptional ?? true, f) ? {} : { limit: resolveCondition(options?.useDefault ?? false, f) ? 100 : f.number.int({ min: 1, max: 1000 }) }, - ...!resolveCondition(options?.includeOptional ?? true, f) ? {} : { skip: resolveCondition(options?.useDefault ?? false, f) ? 0 : f.number.int({ min: 0 }) } - } - }; -}; - -export const fakeListSagasApiV1SagasGetResponse200 = (options?: Options): ListSagasApiV1SagasGetResponses[200] => fakeSagaListResponse(options); - -export const fakeListSagasApiV1SagasGetResponsedefault = (options?: Options): ListSagasApiV1SagasGetErrors['default'] => fakeErrorResponse(options); - -export const fakeCancelSagaApiV1SagasSagaIdCancelPostRequest = (options?: Options): Omit => { - const f = options?.faker ?? faker; - return { - path: { - saga_id: f.string.sample() - } - }; -}; - -export const fakeCancelSagaApiV1SagasSagaIdCancelPostResponse200 = (options?: Options): CancelSagaApiV1SagasSagaIdCancelPostResponses[200] => fakeSagaCancellationResponse(options); - -export const fakeCancelSagaApiV1SagasSagaIdCancelPostResponse400 = (options?: Options): CancelSagaApiV1SagasSagaIdCancelPostErrors[400] => fakeErrorResponse(options); - -export const fakeCancelSagaApiV1SagasSagaIdCancelPostResponse403 = (options?: Options): CancelSagaApiV1SagasSagaIdCancelPostErrors[403] => fakeErrorResponse(options); - -export const fakeCancelSagaApiV1SagasSagaIdCancelPostResponse404 = (options?: Options): CancelSagaApiV1SagasSagaIdCancelPostErrors[404] => fakeErrorResponse(options); - -export const fakeCancelSagaApiV1SagasSagaIdCancelPostResponsedefault = (options?: Options): CancelSagaApiV1SagasSagaIdCancelPostErrors['default'] => fakeErrorResponse(options); diff --git a/frontend/src/lib/api/@tanstack/svelte-query.gen.ts b/frontend/src/lib/api/@tanstack/svelte-query.gen.ts index 3c5c6c877..3ff42b764 100644 --- a/frontend/src/lib/api/@tanstack/svelte-query.gen.ts +++ b/frontend/src/lib/api/@tanstack/svelte-query.gen.ts @@ -3,8 +3,8 @@ import { type InfiniteData, infiniteQueryOptions, type MutationOptions, queryOptions } from '@tanstack/svelte-query'; import { client } from '../client.gen'; -import { browseEventsApiV1AdminEventsBrowsePost, cancelExecutionApiV1ExecutionsExecutionIdCancelPost, cancelReplaySessionApiV1ReplaySessionsSessionIdCancelPost, cancelSagaApiV1SagasSagaIdCancelPost, cleanupOldSessionsApiV1ReplayCleanupPost, createExecutionApiV1ExecutePost, createReplaySessionApiV1ReplaySessionsPost, createSavedScriptApiV1ScriptsPost, createUserApiV1AdminUsersPost, deleteEventApiV1AdminEventsEventIdDelete, deleteExecutionApiV1ExecutionsExecutionIdDelete, deleteNotificationApiV1NotificationsNotificationIdDelete, deleteSavedScriptApiV1ScriptsScriptIdDelete, deleteUserApiV1AdminUsersUserIdDelete, exportEventsApiV1AdminEventsExportExportFormatGet, getCurrentUserProfileApiV1AuthMeGet, getDefaultRateLimitRulesApiV1AdminRateLimitsDefaultsGet, getEventDetailApiV1AdminEventsEventIdGet, getEventStatsApiV1AdminEventsStatsGet, getExampleScriptsApiV1ExampleScriptsGet, getExecutionEventsApiV1ExecutionsExecutionIdEventsGet, getExecutionSagasApiV1SagasExecutionExecutionIdGet, getK8sResourceLimitsApiV1K8sLimitsGet, getNotificationsApiV1NotificationsGet, getQueueStatusApiV1AdminExecutionsQueueGet, getReplaySessionApiV1ReplaySessionsSessionIdGet, getResultApiV1ExecutionsExecutionIdResultGet, getSagaStatusApiV1SagasSagaIdGet, getSavedScriptApiV1ScriptsScriptIdGet, getSettingsHistoryApiV1UserSettingsHistoryGet, getSubscriptionsApiV1NotificationsSubscriptionsGet, getSystemSettingsApiV1AdminSettingsGet, getUnreadCountApiV1NotificationsUnreadCountGet, getUserApiV1AdminUsersUserIdGet, getUserExecutionsApiV1UserExecutionsGet, getUserOverviewApiV1AdminUsersUserIdOverviewGet, getUserRateLimitsApiV1AdminRateLimitsUserIdGet, getUserSettingsApiV1UserSettingsGet, listExecutionsApiV1AdminExecutionsGet, listReplaySessionsApiV1ReplaySessionsGet, listSagasApiV1SagasGet, listSavedScriptsApiV1ScriptsGet, listUsersApiV1AdminUsersGet, livenessApiV1HealthLiveGet, loginApiV1AuthLoginPost, logoutApiV1AuthLogoutPost, markAllReadApiV1NotificationsMarkAllReadPost, markNotificationReadApiV1NotificationsNotificationIdReadPut, type Options, pauseReplaySessionApiV1ReplaySessionsSessionIdPausePost, registerApiV1AuthRegisterPost, replayEventsApiV1AdminEventsReplayPost, resetSystemSettingsApiV1AdminSettingsResetPost, resetUserPasswordApiV1AdminUsersUserIdResetPasswordPost, resetUserRateLimitsApiV1AdminRateLimitsUserIdResetPost, restoreSettingsApiV1UserSettingsRestorePost, resumeReplaySessionApiV1ReplaySessionsSessionIdResumePost, retryExecutionApiV1ExecutionsExecutionIdRetryPost, startReplaySessionApiV1ReplaySessionsSessionIdStartPost, unlockUserApiV1AdminUsersUserIdUnlockPost, updateCustomSettingApiV1UserSettingsCustomKeyPut, updateEditorSettingsApiV1UserSettingsEditorPut, updateNotificationSettingsApiV1UserSettingsNotificationsPut, updatePriorityApiV1AdminExecutionsExecutionIdPriorityPut, updateSavedScriptApiV1ScriptsScriptIdPut, updateSubscriptionApiV1NotificationsSubscriptionsChannelPut, updateSystemSettingsApiV1AdminSettingsPut, updateThemeApiV1UserSettingsThemePut, updateUserApiV1AdminUsersUserIdPut, updateUserRateLimitsApiV1AdminRateLimitsUserIdPut, updateUserSettingsApiV1UserSettingsPut } from '../sdk.gen'; -import type { BrowseEventsApiV1AdminEventsBrowsePostData, BrowseEventsApiV1AdminEventsBrowsePostError, BrowseEventsApiV1AdminEventsBrowsePostResponse, CancelExecutionApiV1ExecutionsExecutionIdCancelPostData, CancelExecutionApiV1ExecutionsExecutionIdCancelPostError, CancelExecutionApiV1ExecutionsExecutionIdCancelPostResponse, CancelReplaySessionApiV1ReplaySessionsSessionIdCancelPostData, CancelReplaySessionApiV1ReplaySessionsSessionIdCancelPostError, CancelReplaySessionApiV1ReplaySessionsSessionIdCancelPostResponse, CancelSagaApiV1SagasSagaIdCancelPostData, CancelSagaApiV1SagasSagaIdCancelPostError, CancelSagaApiV1SagasSagaIdCancelPostResponse, CleanupOldSessionsApiV1ReplayCleanupPostData, CleanupOldSessionsApiV1ReplayCleanupPostError, CleanupOldSessionsApiV1ReplayCleanupPostResponse, CreateExecutionApiV1ExecutePostData, CreateExecutionApiV1ExecutePostError, CreateExecutionApiV1ExecutePostResponse, CreateReplaySessionApiV1ReplaySessionsPostData, CreateReplaySessionApiV1ReplaySessionsPostError, CreateReplaySessionApiV1ReplaySessionsPostResponse, CreateSavedScriptApiV1ScriptsPostData, CreateSavedScriptApiV1ScriptsPostError, CreateSavedScriptApiV1ScriptsPostResponse, CreateUserApiV1AdminUsersPostData, CreateUserApiV1AdminUsersPostError, CreateUserApiV1AdminUsersPostResponse, DeleteEventApiV1AdminEventsEventIdDeleteData, DeleteEventApiV1AdminEventsEventIdDeleteError, DeleteEventApiV1AdminEventsEventIdDeleteResponse, DeleteExecutionApiV1ExecutionsExecutionIdDeleteData, DeleteExecutionApiV1ExecutionsExecutionIdDeleteError, DeleteExecutionApiV1ExecutionsExecutionIdDeleteResponse, DeleteNotificationApiV1NotificationsNotificationIdDeleteData, DeleteNotificationApiV1NotificationsNotificationIdDeleteError, DeleteNotificationApiV1NotificationsNotificationIdDeleteResponse, DeleteSavedScriptApiV1ScriptsScriptIdDeleteData, DeleteSavedScriptApiV1ScriptsScriptIdDeleteError, DeleteSavedScriptApiV1ScriptsScriptIdDeleteResponse, DeleteUserApiV1AdminUsersUserIdDeleteData, DeleteUserApiV1AdminUsersUserIdDeleteError, DeleteUserApiV1AdminUsersUserIdDeleteResponse, ExportEventsApiV1AdminEventsExportExportFormatGetData, ExportEventsApiV1AdminEventsExportExportFormatGetError, GetCurrentUserProfileApiV1AuthMeGetData, GetCurrentUserProfileApiV1AuthMeGetError, GetCurrentUserProfileApiV1AuthMeGetResponse, GetDefaultRateLimitRulesApiV1AdminRateLimitsDefaultsGetData, GetDefaultRateLimitRulesApiV1AdminRateLimitsDefaultsGetError, GetDefaultRateLimitRulesApiV1AdminRateLimitsDefaultsGetResponse, GetEventDetailApiV1AdminEventsEventIdGetData, GetEventDetailApiV1AdminEventsEventIdGetError, GetEventDetailApiV1AdminEventsEventIdGetResponse, GetEventStatsApiV1AdminEventsStatsGetData, GetEventStatsApiV1AdminEventsStatsGetError, GetEventStatsApiV1AdminEventsStatsGetResponse, GetExampleScriptsApiV1ExampleScriptsGetData, GetExampleScriptsApiV1ExampleScriptsGetError, GetExampleScriptsApiV1ExampleScriptsGetResponse, GetExecutionEventsApiV1ExecutionsExecutionIdEventsGetData, GetExecutionEventsApiV1ExecutionsExecutionIdEventsGetError, GetExecutionEventsApiV1ExecutionsExecutionIdEventsGetResponse, GetExecutionSagasApiV1SagasExecutionExecutionIdGetData, GetExecutionSagasApiV1SagasExecutionExecutionIdGetError, GetExecutionSagasApiV1SagasExecutionExecutionIdGetResponse, GetK8sResourceLimitsApiV1K8sLimitsGetData, GetK8sResourceLimitsApiV1K8sLimitsGetError, GetK8sResourceLimitsApiV1K8sLimitsGetResponse, GetNotificationsApiV1NotificationsGetData, GetNotificationsApiV1NotificationsGetError, GetNotificationsApiV1NotificationsGetResponse, GetQueueStatusApiV1AdminExecutionsQueueGetData, GetQueueStatusApiV1AdminExecutionsQueueGetError, GetQueueStatusApiV1AdminExecutionsQueueGetResponse, GetReplaySessionApiV1ReplaySessionsSessionIdGetData, GetReplaySessionApiV1ReplaySessionsSessionIdGetError, GetReplaySessionApiV1ReplaySessionsSessionIdGetResponse, GetResultApiV1ExecutionsExecutionIdResultGetData, GetResultApiV1ExecutionsExecutionIdResultGetError, GetResultApiV1ExecutionsExecutionIdResultGetResponse, GetSagaStatusApiV1SagasSagaIdGetData, GetSagaStatusApiV1SagasSagaIdGetError, GetSagaStatusApiV1SagasSagaIdGetResponse, GetSavedScriptApiV1ScriptsScriptIdGetData, GetSavedScriptApiV1ScriptsScriptIdGetError, GetSavedScriptApiV1ScriptsScriptIdGetResponse, GetSettingsHistoryApiV1UserSettingsHistoryGetData, GetSettingsHistoryApiV1UserSettingsHistoryGetError, GetSettingsHistoryApiV1UserSettingsHistoryGetResponse, GetSubscriptionsApiV1NotificationsSubscriptionsGetData, GetSubscriptionsApiV1NotificationsSubscriptionsGetError, GetSubscriptionsApiV1NotificationsSubscriptionsGetResponse, GetSystemSettingsApiV1AdminSettingsGetData, GetSystemSettingsApiV1AdminSettingsGetError, GetSystemSettingsApiV1AdminSettingsGetResponse, GetUnreadCountApiV1NotificationsUnreadCountGetData, GetUnreadCountApiV1NotificationsUnreadCountGetError, GetUnreadCountApiV1NotificationsUnreadCountGetResponse, GetUserApiV1AdminUsersUserIdGetData, GetUserApiV1AdminUsersUserIdGetError, GetUserApiV1AdminUsersUserIdGetResponse, GetUserExecutionsApiV1UserExecutionsGetData, GetUserExecutionsApiV1UserExecutionsGetError, GetUserExecutionsApiV1UserExecutionsGetResponse, GetUserOverviewApiV1AdminUsersUserIdOverviewGetData, GetUserOverviewApiV1AdminUsersUserIdOverviewGetError, GetUserOverviewApiV1AdminUsersUserIdOverviewGetResponse, GetUserRateLimitsApiV1AdminRateLimitsUserIdGetData, GetUserRateLimitsApiV1AdminRateLimitsUserIdGetError, GetUserRateLimitsApiV1AdminRateLimitsUserIdGetResponse, GetUserSettingsApiV1UserSettingsGetData, GetUserSettingsApiV1UserSettingsGetError, GetUserSettingsApiV1UserSettingsGetResponse, ListExecutionsApiV1AdminExecutionsGetData, ListExecutionsApiV1AdminExecutionsGetError, ListExecutionsApiV1AdminExecutionsGetResponse, ListReplaySessionsApiV1ReplaySessionsGetData, ListReplaySessionsApiV1ReplaySessionsGetError, ListReplaySessionsApiV1ReplaySessionsGetResponse, ListSagasApiV1SagasGetData, ListSagasApiV1SagasGetError, ListSagasApiV1SagasGetResponse, ListSavedScriptsApiV1ScriptsGetData, ListSavedScriptsApiV1ScriptsGetError, ListSavedScriptsApiV1ScriptsGetResponse, ListUsersApiV1AdminUsersGetData, ListUsersApiV1AdminUsersGetError, ListUsersApiV1AdminUsersGetResponse, LivenessApiV1HealthLiveGetData, LivenessApiV1HealthLiveGetError, LivenessApiV1HealthLiveGetResponse, LoginApiV1AuthLoginPostData, LoginApiV1AuthLoginPostError, LoginApiV1AuthLoginPostResponse, LogoutApiV1AuthLogoutPostData, LogoutApiV1AuthLogoutPostError, LogoutApiV1AuthLogoutPostResponse, MarkAllReadApiV1NotificationsMarkAllReadPostData, MarkAllReadApiV1NotificationsMarkAllReadPostError, MarkAllReadApiV1NotificationsMarkAllReadPostResponse, MarkNotificationReadApiV1NotificationsNotificationIdReadPutData, MarkNotificationReadApiV1NotificationsNotificationIdReadPutError, MarkNotificationReadApiV1NotificationsNotificationIdReadPutResponse, PauseReplaySessionApiV1ReplaySessionsSessionIdPausePostData, PauseReplaySessionApiV1ReplaySessionsSessionIdPausePostError, PauseReplaySessionApiV1ReplaySessionsSessionIdPausePostResponse, RegisterApiV1AuthRegisterPostData, RegisterApiV1AuthRegisterPostError, RegisterApiV1AuthRegisterPostResponse, ReplayEventsApiV1AdminEventsReplayPostData, ReplayEventsApiV1AdminEventsReplayPostError, ReplayEventsApiV1AdminEventsReplayPostResponse, ResetSystemSettingsApiV1AdminSettingsResetPostData, ResetSystemSettingsApiV1AdminSettingsResetPostError, ResetSystemSettingsApiV1AdminSettingsResetPostResponse, ResetUserPasswordApiV1AdminUsersUserIdResetPasswordPostData, ResetUserPasswordApiV1AdminUsersUserIdResetPasswordPostError, ResetUserPasswordApiV1AdminUsersUserIdResetPasswordPostResponse, ResetUserRateLimitsApiV1AdminRateLimitsUserIdResetPostData, ResetUserRateLimitsApiV1AdminRateLimitsUserIdResetPostError, ResetUserRateLimitsApiV1AdminRateLimitsUserIdResetPostResponse, RestoreSettingsApiV1UserSettingsRestorePostData, RestoreSettingsApiV1UserSettingsRestorePostError, RestoreSettingsApiV1UserSettingsRestorePostResponse, ResumeReplaySessionApiV1ReplaySessionsSessionIdResumePostData, ResumeReplaySessionApiV1ReplaySessionsSessionIdResumePostError, ResumeReplaySessionApiV1ReplaySessionsSessionIdResumePostResponse, RetryExecutionApiV1ExecutionsExecutionIdRetryPostData, RetryExecutionApiV1ExecutionsExecutionIdRetryPostError, RetryExecutionApiV1ExecutionsExecutionIdRetryPostResponse, StartReplaySessionApiV1ReplaySessionsSessionIdStartPostData, StartReplaySessionApiV1ReplaySessionsSessionIdStartPostError, StartReplaySessionApiV1ReplaySessionsSessionIdStartPostResponse, UnlockUserApiV1AdminUsersUserIdUnlockPostData, UnlockUserApiV1AdminUsersUserIdUnlockPostError, UnlockUserApiV1AdminUsersUserIdUnlockPostResponse, UpdateCustomSettingApiV1UserSettingsCustomKeyPutData, UpdateCustomSettingApiV1UserSettingsCustomKeyPutError, UpdateCustomSettingApiV1UserSettingsCustomKeyPutResponse, UpdateEditorSettingsApiV1UserSettingsEditorPutData, UpdateEditorSettingsApiV1UserSettingsEditorPutError, UpdateEditorSettingsApiV1UserSettingsEditorPutResponse, UpdateNotificationSettingsApiV1UserSettingsNotificationsPutData, UpdateNotificationSettingsApiV1UserSettingsNotificationsPutError, UpdateNotificationSettingsApiV1UserSettingsNotificationsPutResponse, UpdatePriorityApiV1AdminExecutionsExecutionIdPriorityPutData, UpdatePriorityApiV1AdminExecutionsExecutionIdPriorityPutError, UpdatePriorityApiV1AdminExecutionsExecutionIdPriorityPutResponse, UpdateSavedScriptApiV1ScriptsScriptIdPutData, UpdateSavedScriptApiV1ScriptsScriptIdPutError, UpdateSavedScriptApiV1ScriptsScriptIdPutResponse, UpdateSubscriptionApiV1NotificationsSubscriptionsChannelPutData, UpdateSubscriptionApiV1NotificationsSubscriptionsChannelPutError, UpdateSubscriptionApiV1NotificationsSubscriptionsChannelPutResponse, UpdateSystemSettingsApiV1AdminSettingsPutData, UpdateSystemSettingsApiV1AdminSettingsPutError, UpdateSystemSettingsApiV1AdminSettingsPutResponse, UpdateThemeApiV1UserSettingsThemePutData, UpdateThemeApiV1UserSettingsThemePutError, UpdateThemeApiV1UserSettingsThemePutResponse, UpdateUserApiV1AdminUsersUserIdPutData, UpdateUserApiV1AdminUsersUserIdPutError, UpdateUserApiV1AdminUsersUserIdPutResponse, UpdateUserRateLimitsApiV1AdminRateLimitsUserIdPutData, UpdateUserRateLimitsApiV1AdminRateLimitsUserIdPutError, UpdateUserRateLimitsApiV1AdminRateLimitsUserIdPutResponse, UpdateUserSettingsApiV1UserSettingsPutData, UpdateUserSettingsApiV1UserSettingsPutError, UpdateUserSettingsApiV1UserSettingsPutResponse } from '../types.gen'; +import { browseEventsApiV1AdminEventsBrowsePost, cancelExecutionApiV1ExecutionsExecutionIdCancelPost, cancelReplaySessionApiV1ReplaySessionsSessionIdCancelPost, cleanupOldSessionsApiV1ReplayCleanupPost, createExecutionApiV1ExecutePost, createReplaySessionApiV1ReplaySessionsPost, createSavedScriptApiV1ScriptsPost, createUserApiV1AdminUsersPost, deleteEventApiV1AdminEventsEventIdDelete, deleteExecutionApiV1ExecutionsExecutionIdDelete, deleteNotificationApiV1NotificationsNotificationIdDelete, deleteSavedScriptApiV1ScriptsScriptIdDelete, deleteUserApiV1AdminUsersUserIdDelete, exportEventsApiV1AdminEventsExportExportFormatGet, getCurrentUserProfileApiV1AuthMeGet, getDefaultRateLimitRulesApiV1AdminRateLimitsDefaultsGet, getEventDetailApiV1AdminEventsEventIdGet, getEventStatsApiV1AdminEventsStatsGet, getExampleScriptsApiV1ExampleScriptsGet, getExecutionEventsApiV1ExecutionsExecutionIdEventsGet, getK8sResourceLimitsApiV1K8sLimitsGet, getNotificationsApiV1NotificationsGet, getQueueStatusApiV1AdminExecutionsQueueGet, getReplaySessionApiV1ReplaySessionsSessionIdGet, getResultApiV1ExecutionsExecutionIdResultGet, getSavedScriptApiV1ScriptsScriptIdGet, getSettingsHistoryApiV1UserSettingsHistoryGet, getSubscriptionsApiV1NotificationsSubscriptionsGet, getSystemSettingsApiV1AdminSettingsGet, getUnreadCountApiV1NotificationsUnreadCountGet, getUserApiV1AdminUsersUserIdGet, getUserExecutionsApiV1UserExecutionsGet, getUserOverviewApiV1AdminUsersUserIdOverviewGet, getUserRateLimitsApiV1AdminRateLimitsUserIdGet, getUserSettingsApiV1UserSettingsGet, listExecutionsApiV1AdminExecutionsGet, listReplaySessionsApiV1ReplaySessionsGet, listSavedScriptsApiV1ScriptsGet, listUsersApiV1AdminUsersGet, livenessApiV1HealthLiveGet, loginApiV1AuthLoginPost, logoutApiV1AuthLogoutPost, markAllReadApiV1NotificationsMarkAllReadPost, markNotificationReadApiV1NotificationsNotificationIdReadPut, type Options, pauseReplaySessionApiV1ReplaySessionsSessionIdPausePost, registerApiV1AuthRegisterPost, replayEventsApiV1AdminEventsReplayPost, resetSystemSettingsApiV1AdminSettingsResetPost, resetUserPasswordApiV1AdminUsersUserIdResetPasswordPost, resetUserRateLimitsApiV1AdminRateLimitsUserIdResetPost, restoreSettingsApiV1UserSettingsRestorePost, resumeReplaySessionApiV1ReplaySessionsSessionIdResumePost, retryExecutionApiV1ExecutionsExecutionIdRetryPost, startReplaySessionApiV1ReplaySessionsSessionIdStartPost, unlockUserApiV1AdminUsersUserIdUnlockPost, updateCustomSettingApiV1UserSettingsCustomKeyPut, updateEditorSettingsApiV1UserSettingsEditorPut, updateNotificationSettingsApiV1UserSettingsNotificationsPut, updatePriorityApiV1AdminExecutionsExecutionIdPriorityPut, updateSavedScriptApiV1ScriptsScriptIdPut, updateSubscriptionApiV1NotificationsSubscriptionsChannelPut, updateSystemSettingsApiV1AdminSettingsPut, updateThemeApiV1UserSettingsThemePut, updateUserApiV1AdminUsersUserIdPut, updateUserRateLimitsApiV1AdminRateLimitsUserIdPut, updateUserSettingsApiV1UserSettingsPut } from '../sdk.gen'; +import type { BrowseEventsApiV1AdminEventsBrowsePostData, BrowseEventsApiV1AdminEventsBrowsePostError, BrowseEventsApiV1AdminEventsBrowsePostResponse, CancelExecutionApiV1ExecutionsExecutionIdCancelPostData, CancelExecutionApiV1ExecutionsExecutionIdCancelPostError, CancelExecutionApiV1ExecutionsExecutionIdCancelPostResponse, CancelReplaySessionApiV1ReplaySessionsSessionIdCancelPostData, CancelReplaySessionApiV1ReplaySessionsSessionIdCancelPostError, CancelReplaySessionApiV1ReplaySessionsSessionIdCancelPostResponse, CleanupOldSessionsApiV1ReplayCleanupPostData, CleanupOldSessionsApiV1ReplayCleanupPostError, CleanupOldSessionsApiV1ReplayCleanupPostResponse, CreateExecutionApiV1ExecutePostData, CreateExecutionApiV1ExecutePostError, CreateExecutionApiV1ExecutePostResponse, CreateReplaySessionApiV1ReplaySessionsPostData, CreateReplaySessionApiV1ReplaySessionsPostError, CreateReplaySessionApiV1ReplaySessionsPostResponse, CreateSavedScriptApiV1ScriptsPostData, CreateSavedScriptApiV1ScriptsPostError, CreateSavedScriptApiV1ScriptsPostResponse, CreateUserApiV1AdminUsersPostData, CreateUserApiV1AdminUsersPostError, CreateUserApiV1AdminUsersPostResponse, DeleteEventApiV1AdminEventsEventIdDeleteData, DeleteEventApiV1AdminEventsEventIdDeleteError, DeleteEventApiV1AdminEventsEventIdDeleteResponse, DeleteExecutionApiV1ExecutionsExecutionIdDeleteData, DeleteExecutionApiV1ExecutionsExecutionIdDeleteError, DeleteExecutionApiV1ExecutionsExecutionIdDeleteResponse, DeleteNotificationApiV1NotificationsNotificationIdDeleteData, DeleteNotificationApiV1NotificationsNotificationIdDeleteError, DeleteNotificationApiV1NotificationsNotificationIdDeleteResponse, DeleteSavedScriptApiV1ScriptsScriptIdDeleteData, DeleteSavedScriptApiV1ScriptsScriptIdDeleteError, DeleteSavedScriptApiV1ScriptsScriptIdDeleteResponse, DeleteUserApiV1AdminUsersUserIdDeleteData, DeleteUserApiV1AdminUsersUserIdDeleteError, DeleteUserApiV1AdminUsersUserIdDeleteResponse, ExportEventsApiV1AdminEventsExportExportFormatGetData, ExportEventsApiV1AdminEventsExportExportFormatGetError, GetCurrentUserProfileApiV1AuthMeGetData, GetCurrentUserProfileApiV1AuthMeGetError, GetCurrentUserProfileApiV1AuthMeGetResponse, GetDefaultRateLimitRulesApiV1AdminRateLimitsDefaultsGetData, GetDefaultRateLimitRulesApiV1AdminRateLimitsDefaultsGetError, GetDefaultRateLimitRulesApiV1AdminRateLimitsDefaultsGetResponse, GetEventDetailApiV1AdminEventsEventIdGetData, GetEventDetailApiV1AdminEventsEventIdGetError, GetEventDetailApiV1AdminEventsEventIdGetResponse, GetEventStatsApiV1AdminEventsStatsGetData, GetEventStatsApiV1AdminEventsStatsGetError, GetEventStatsApiV1AdminEventsStatsGetResponse, GetExampleScriptsApiV1ExampleScriptsGetData, GetExampleScriptsApiV1ExampleScriptsGetError, GetExampleScriptsApiV1ExampleScriptsGetResponse, GetExecutionEventsApiV1ExecutionsExecutionIdEventsGetData, GetExecutionEventsApiV1ExecutionsExecutionIdEventsGetError, GetExecutionEventsApiV1ExecutionsExecutionIdEventsGetResponse, GetK8sResourceLimitsApiV1K8sLimitsGetData, GetK8sResourceLimitsApiV1K8sLimitsGetError, GetK8sResourceLimitsApiV1K8sLimitsGetResponse, GetNotificationsApiV1NotificationsGetData, GetNotificationsApiV1NotificationsGetError, GetNotificationsApiV1NotificationsGetResponse, GetQueueStatusApiV1AdminExecutionsQueueGetData, GetQueueStatusApiV1AdminExecutionsQueueGetError, GetQueueStatusApiV1AdminExecutionsQueueGetResponse, GetReplaySessionApiV1ReplaySessionsSessionIdGetData, GetReplaySessionApiV1ReplaySessionsSessionIdGetError, GetReplaySessionApiV1ReplaySessionsSessionIdGetResponse, GetResultApiV1ExecutionsExecutionIdResultGetData, GetResultApiV1ExecutionsExecutionIdResultGetError, GetResultApiV1ExecutionsExecutionIdResultGetResponse, GetSavedScriptApiV1ScriptsScriptIdGetData, GetSavedScriptApiV1ScriptsScriptIdGetError, GetSavedScriptApiV1ScriptsScriptIdGetResponse, GetSettingsHistoryApiV1UserSettingsHistoryGetData, GetSettingsHistoryApiV1UserSettingsHistoryGetError, GetSettingsHistoryApiV1UserSettingsHistoryGetResponse, GetSubscriptionsApiV1NotificationsSubscriptionsGetData, GetSubscriptionsApiV1NotificationsSubscriptionsGetError, GetSubscriptionsApiV1NotificationsSubscriptionsGetResponse, GetSystemSettingsApiV1AdminSettingsGetData, GetSystemSettingsApiV1AdminSettingsGetError, GetSystemSettingsApiV1AdminSettingsGetResponse, GetUnreadCountApiV1NotificationsUnreadCountGetData, GetUnreadCountApiV1NotificationsUnreadCountGetError, GetUnreadCountApiV1NotificationsUnreadCountGetResponse, GetUserApiV1AdminUsersUserIdGetData, GetUserApiV1AdminUsersUserIdGetError, GetUserApiV1AdminUsersUserIdGetResponse, GetUserExecutionsApiV1UserExecutionsGetData, GetUserExecutionsApiV1UserExecutionsGetError, GetUserExecutionsApiV1UserExecutionsGetResponse, GetUserOverviewApiV1AdminUsersUserIdOverviewGetData, GetUserOverviewApiV1AdminUsersUserIdOverviewGetError, GetUserOverviewApiV1AdminUsersUserIdOverviewGetResponse, GetUserRateLimitsApiV1AdminRateLimitsUserIdGetData, GetUserRateLimitsApiV1AdminRateLimitsUserIdGetError, GetUserRateLimitsApiV1AdminRateLimitsUserIdGetResponse, GetUserSettingsApiV1UserSettingsGetData, GetUserSettingsApiV1UserSettingsGetError, GetUserSettingsApiV1UserSettingsGetResponse, ListExecutionsApiV1AdminExecutionsGetData, ListExecutionsApiV1AdminExecutionsGetError, ListExecutionsApiV1AdminExecutionsGetResponse, ListReplaySessionsApiV1ReplaySessionsGetData, ListReplaySessionsApiV1ReplaySessionsGetError, ListReplaySessionsApiV1ReplaySessionsGetResponse, ListSavedScriptsApiV1ScriptsGetData, ListSavedScriptsApiV1ScriptsGetError, ListSavedScriptsApiV1ScriptsGetResponse, ListUsersApiV1AdminUsersGetData, ListUsersApiV1AdminUsersGetError, ListUsersApiV1AdminUsersGetResponse, LivenessApiV1HealthLiveGetData, LivenessApiV1HealthLiveGetError, LivenessApiV1HealthLiveGetResponse, LoginApiV1AuthLoginPostData, LoginApiV1AuthLoginPostError, LoginApiV1AuthLoginPostResponse, LogoutApiV1AuthLogoutPostData, LogoutApiV1AuthLogoutPostError, LogoutApiV1AuthLogoutPostResponse, MarkAllReadApiV1NotificationsMarkAllReadPostData, MarkAllReadApiV1NotificationsMarkAllReadPostError, MarkAllReadApiV1NotificationsMarkAllReadPostResponse, MarkNotificationReadApiV1NotificationsNotificationIdReadPutData, MarkNotificationReadApiV1NotificationsNotificationIdReadPutError, MarkNotificationReadApiV1NotificationsNotificationIdReadPutResponse, PauseReplaySessionApiV1ReplaySessionsSessionIdPausePostData, PauseReplaySessionApiV1ReplaySessionsSessionIdPausePostError, PauseReplaySessionApiV1ReplaySessionsSessionIdPausePostResponse, RegisterApiV1AuthRegisterPostData, RegisterApiV1AuthRegisterPostError, RegisterApiV1AuthRegisterPostResponse, ReplayEventsApiV1AdminEventsReplayPostData, ReplayEventsApiV1AdminEventsReplayPostError, ReplayEventsApiV1AdminEventsReplayPostResponse, ResetSystemSettingsApiV1AdminSettingsResetPostData, ResetSystemSettingsApiV1AdminSettingsResetPostError, ResetSystemSettingsApiV1AdminSettingsResetPostResponse, ResetUserPasswordApiV1AdminUsersUserIdResetPasswordPostData, ResetUserPasswordApiV1AdminUsersUserIdResetPasswordPostError, ResetUserPasswordApiV1AdminUsersUserIdResetPasswordPostResponse, ResetUserRateLimitsApiV1AdminRateLimitsUserIdResetPostData, ResetUserRateLimitsApiV1AdminRateLimitsUserIdResetPostError, ResetUserRateLimitsApiV1AdminRateLimitsUserIdResetPostResponse, RestoreSettingsApiV1UserSettingsRestorePostData, RestoreSettingsApiV1UserSettingsRestorePostError, RestoreSettingsApiV1UserSettingsRestorePostResponse, ResumeReplaySessionApiV1ReplaySessionsSessionIdResumePostData, ResumeReplaySessionApiV1ReplaySessionsSessionIdResumePostError, ResumeReplaySessionApiV1ReplaySessionsSessionIdResumePostResponse, RetryExecutionApiV1ExecutionsExecutionIdRetryPostData, RetryExecutionApiV1ExecutionsExecutionIdRetryPostError, RetryExecutionApiV1ExecutionsExecutionIdRetryPostResponse, StartReplaySessionApiV1ReplaySessionsSessionIdStartPostData, StartReplaySessionApiV1ReplaySessionsSessionIdStartPostError, StartReplaySessionApiV1ReplaySessionsSessionIdStartPostResponse, UnlockUserApiV1AdminUsersUserIdUnlockPostData, UnlockUserApiV1AdminUsersUserIdUnlockPostError, UnlockUserApiV1AdminUsersUserIdUnlockPostResponse, UpdateCustomSettingApiV1UserSettingsCustomKeyPutData, UpdateCustomSettingApiV1UserSettingsCustomKeyPutError, UpdateCustomSettingApiV1UserSettingsCustomKeyPutResponse, UpdateEditorSettingsApiV1UserSettingsEditorPutData, UpdateEditorSettingsApiV1UserSettingsEditorPutError, UpdateEditorSettingsApiV1UserSettingsEditorPutResponse, UpdateNotificationSettingsApiV1UserSettingsNotificationsPutData, UpdateNotificationSettingsApiV1UserSettingsNotificationsPutError, UpdateNotificationSettingsApiV1UserSettingsNotificationsPutResponse, UpdatePriorityApiV1AdminExecutionsExecutionIdPriorityPutData, UpdatePriorityApiV1AdminExecutionsExecutionIdPriorityPutError, UpdatePriorityApiV1AdminExecutionsExecutionIdPriorityPutResponse, UpdateSavedScriptApiV1ScriptsScriptIdPutData, UpdateSavedScriptApiV1ScriptsScriptIdPutError, UpdateSavedScriptApiV1ScriptsScriptIdPutResponse, UpdateSubscriptionApiV1NotificationsSubscriptionsChannelPutData, UpdateSubscriptionApiV1NotificationsSubscriptionsChannelPutError, UpdateSubscriptionApiV1NotificationsSubscriptionsChannelPutResponse, UpdateSystemSettingsApiV1AdminSettingsPutData, UpdateSystemSettingsApiV1AdminSettingsPutError, UpdateSystemSettingsApiV1AdminSettingsPutResponse, UpdateThemeApiV1UserSettingsThemePutData, UpdateThemeApiV1UserSettingsThemePutError, UpdateThemeApiV1UserSettingsThemePutResponse, UpdateUserApiV1AdminUsersUserIdPutData, UpdateUserApiV1AdminUsersUserIdPutError, UpdateUserApiV1AdminUsersUserIdPutResponse, UpdateUserRateLimitsApiV1AdminRateLimitsUserIdPutData, UpdateUserRateLimitsApiV1AdminRateLimitsUserIdPutError, UpdateUserRateLimitsApiV1AdminRateLimitsUserIdPutResponse, UpdateUserSettingsApiV1UserSettingsPutData, UpdateUserSettingsApiV1UserSettingsPutError, UpdateUserSettingsApiV1UserSettingsPutResponse } from '../types.gen'; /** * Login @@ -1407,82 +1407,3 @@ export const deleteNotificationApiV1NotificationsNotificationIdDeleteMutation = }; return mutationOptions; }; - -export const getSagaStatusApiV1SagasSagaIdGetQueryKey = (options: Options) => createQueryKey('getSagaStatusApiV1SagasSagaIdGet', options); - -/** - * Get Saga Status - * - * Get saga status by ID. - */ -export const getSagaStatusApiV1SagasSagaIdGetOptions = (options: Options) => queryOptions>({ - queryFn: async ({ queryKey, signal }) => { - const { data } = await getSagaStatusApiV1SagasSagaIdGet({ - ...options, - ...queryKey[0], - signal, - throwOnError: true - }); - return data; - }, - queryKey: getSagaStatusApiV1SagasSagaIdGetQueryKey(options) -}); - -export const getExecutionSagasApiV1SagasExecutionExecutionIdGetQueryKey = (options: Options) => createQueryKey('getExecutionSagasApiV1SagasExecutionExecutionIdGet', options); - -/** - * Get Execution Sagas - * - * Get all sagas for an execution. - */ -export const getExecutionSagasApiV1SagasExecutionExecutionIdGetOptions = (options: Options) => queryOptions>({ - queryFn: async ({ queryKey, signal }) => { - const { data } = await getExecutionSagasApiV1SagasExecutionExecutionIdGet({ - ...options, - ...queryKey[0], - signal, - throwOnError: true - }); - return data; - }, - queryKey: getExecutionSagasApiV1SagasExecutionExecutionIdGetQueryKey(options) -}); - -export const listSagasApiV1SagasGetQueryKey = (options?: Options) => createQueryKey('listSagasApiV1SagasGet', options); - -/** - * List Sagas - * - * List sagas accessible by the current user. - */ -export const listSagasApiV1SagasGetOptions = (options?: Options) => queryOptions>({ - queryFn: async ({ queryKey, signal }) => { - const { data } = await listSagasApiV1SagasGet({ - ...options, - ...queryKey[0], - signal, - throwOnError: true - }); - return data; - }, - queryKey: listSagasApiV1SagasGetQueryKey(options) -}); - -/** - * Cancel Saga - * - * Cancel a running saga. - */ -export const cancelSagaApiV1SagasSagaIdCancelPostMutation = (options?: Partial>): MutationOptions> => { - const mutationOptions: MutationOptions> = { - mutationFn: async (fnOptions) => { - const { data } = await cancelSagaApiV1SagasSagaIdCancelPost({ - ...options, - ...fnOptions, - throwOnError: true - }); - return data; - } - }; - return mutationOptions; -}; diff --git a/frontend/src/lib/api/index.ts b/frontend/src/lib/api/index.ts index 5c744cbc3..028614833 100644 --- a/frontend/src/lib/api/index.ts +++ b/frontend/src/lib/api/index.ts @@ -1,7 +1,7 @@ // This file is auto-generated by @hey-api/openapi-ts -export { browseEventsApiV1AdminEventsBrowsePostOptions, browseEventsApiV1AdminEventsBrowsePostQueryKey, cancelExecutionApiV1ExecutionsExecutionIdCancelPostMutation, cancelReplaySessionApiV1ReplaySessionsSessionIdCancelPostMutation, cancelSagaApiV1SagasSagaIdCancelPostMutation, cleanupOldSessionsApiV1ReplayCleanupPostMutation, createExecutionApiV1ExecutePostMutation, createReplaySessionApiV1ReplaySessionsPostMutation, createSavedScriptApiV1ScriptsPostMutation, createUserApiV1AdminUsersPostMutation, deleteEventApiV1AdminEventsEventIdDeleteMutation, deleteExecutionApiV1ExecutionsExecutionIdDeleteMutation, deleteNotificationApiV1NotificationsNotificationIdDeleteMutation, deleteSavedScriptApiV1ScriptsScriptIdDeleteMutation, deleteUserApiV1AdminUsersUserIdDeleteMutation, exportEventsApiV1AdminEventsExportExportFormatGetOptions, exportEventsApiV1AdminEventsExportExportFormatGetQueryKey, getCurrentUserProfileApiV1AuthMeGetOptions, getCurrentUserProfileApiV1AuthMeGetQueryKey, getDefaultRateLimitRulesApiV1AdminRateLimitsDefaultsGetOptions, getDefaultRateLimitRulesApiV1AdminRateLimitsDefaultsGetQueryKey, getEventDetailApiV1AdminEventsEventIdGetOptions, getEventDetailApiV1AdminEventsEventIdGetQueryKey, getEventStatsApiV1AdminEventsStatsGetOptions, getEventStatsApiV1AdminEventsStatsGetQueryKey, getExampleScriptsApiV1ExampleScriptsGetOptions, getExampleScriptsApiV1ExampleScriptsGetQueryKey, getExecutionEventsApiV1ExecutionsExecutionIdEventsGetOptions, getExecutionEventsApiV1ExecutionsExecutionIdEventsGetQueryKey, getExecutionSagasApiV1SagasExecutionExecutionIdGetOptions, getExecutionSagasApiV1SagasExecutionExecutionIdGetQueryKey, getK8sResourceLimitsApiV1K8sLimitsGetOptions, getK8sResourceLimitsApiV1K8sLimitsGetQueryKey, getNotificationsApiV1NotificationsGetInfiniteOptions, getNotificationsApiV1NotificationsGetInfiniteQueryKey, getNotificationsApiV1NotificationsGetOptions, getNotificationsApiV1NotificationsGetQueryKey, getQueueStatusApiV1AdminExecutionsQueueGetOptions, getQueueStatusApiV1AdminExecutionsQueueGetQueryKey, getReplaySessionApiV1ReplaySessionsSessionIdGetOptions, getReplaySessionApiV1ReplaySessionsSessionIdGetQueryKey, getResultApiV1ExecutionsExecutionIdResultGetOptions, getResultApiV1ExecutionsExecutionIdResultGetQueryKey, getSagaStatusApiV1SagasSagaIdGetOptions, getSagaStatusApiV1SagasSagaIdGetQueryKey, getSavedScriptApiV1ScriptsScriptIdGetOptions, getSavedScriptApiV1ScriptsScriptIdGetQueryKey, getSettingsHistoryApiV1UserSettingsHistoryGetOptions, getSettingsHistoryApiV1UserSettingsHistoryGetQueryKey, getSubscriptionsApiV1NotificationsSubscriptionsGetOptions, getSubscriptionsApiV1NotificationsSubscriptionsGetQueryKey, getSystemSettingsApiV1AdminSettingsGetOptions, getSystemSettingsApiV1AdminSettingsGetQueryKey, getUnreadCountApiV1NotificationsUnreadCountGetOptions, getUnreadCountApiV1NotificationsUnreadCountGetQueryKey, getUserApiV1AdminUsersUserIdGetOptions, getUserApiV1AdminUsersUserIdGetQueryKey, getUserExecutionsApiV1UserExecutionsGetOptions, getUserExecutionsApiV1UserExecutionsGetQueryKey, getUserOverviewApiV1AdminUsersUserIdOverviewGetOptions, getUserOverviewApiV1AdminUsersUserIdOverviewGetQueryKey, getUserRateLimitsApiV1AdminRateLimitsUserIdGetOptions, getUserRateLimitsApiV1AdminRateLimitsUserIdGetQueryKey, getUserSettingsApiV1UserSettingsGetOptions, getUserSettingsApiV1UserSettingsGetQueryKey, listExecutionsApiV1AdminExecutionsGetOptions, listExecutionsApiV1AdminExecutionsGetQueryKey, listReplaySessionsApiV1ReplaySessionsGetOptions, listReplaySessionsApiV1ReplaySessionsGetQueryKey, listSagasApiV1SagasGetOptions, listSagasApiV1SagasGetQueryKey, listSavedScriptsApiV1ScriptsGetOptions, listSavedScriptsApiV1ScriptsGetQueryKey, listUsersApiV1AdminUsersGetInfiniteOptions, listUsersApiV1AdminUsersGetInfiniteQueryKey, listUsersApiV1AdminUsersGetOptions, listUsersApiV1AdminUsersGetQueryKey, livenessApiV1HealthLiveGetOptions, livenessApiV1HealthLiveGetQueryKey, loginApiV1AuthLoginPostMutation, logoutApiV1AuthLogoutPostMutation, markAllReadApiV1NotificationsMarkAllReadPostMutation, markNotificationReadApiV1NotificationsNotificationIdReadPutMutation, pauseReplaySessionApiV1ReplaySessionsSessionIdPausePostMutation, type QueryKey, registerApiV1AuthRegisterPostMutation, replayEventsApiV1AdminEventsReplayPostMutation, resetSystemSettingsApiV1AdminSettingsResetPostMutation, resetUserPasswordApiV1AdminUsersUserIdResetPasswordPostMutation, resetUserRateLimitsApiV1AdminRateLimitsUserIdResetPostMutation, restoreSettingsApiV1UserSettingsRestorePostMutation, resumeReplaySessionApiV1ReplaySessionsSessionIdResumePostMutation, retryExecutionApiV1ExecutionsExecutionIdRetryPostMutation, startReplaySessionApiV1ReplaySessionsSessionIdStartPostMutation, unlockUserApiV1AdminUsersUserIdUnlockPostMutation, updateCustomSettingApiV1UserSettingsCustomKeyPutMutation, updateEditorSettingsApiV1UserSettingsEditorPutMutation, updateNotificationSettingsApiV1UserSettingsNotificationsPutMutation, updatePriorityApiV1AdminExecutionsExecutionIdPriorityPutMutation, updateSavedScriptApiV1ScriptsScriptIdPutMutation, updateSubscriptionApiV1NotificationsSubscriptionsChannelPutMutation, updateSystemSettingsApiV1AdminSettingsPutMutation, updateThemeApiV1UserSettingsThemePutMutation, updateUserApiV1AdminUsersUserIdPutMutation, updateUserRateLimitsApiV1AdminRateLimitsUserIdPutMutation, updateUserSettingsApiV1UserSettingsPutMutation } from './@tanstack/svelte-query.gen'; +export { browseEventsApiV1AdminEventsBrowsePostOptions, browseEventsApiV1AdminEventsBrowsePostQueryKey, cancelExecutionApiV1ExecutionsExecutionIdCancelPostMutation, cancelReplaySessionApiV1ReplaySessionsSessionIdCancelPostMutation, cleanupOldSessionsApiV1ReplayCleanupPostMutation, createExecutionApiV1ExecutePostMutation, createReplaySessionApiV1ReplaySessionsPostMutation, createSavedScriptApiV1ScriptsPostMutation, createUserApiV1AdminUsersPostMutation, deleteEventApiV1AdminEventsEventIdDeleteMutation, deleteExecutionApiV1ExecutionsExecutionIdDeleteMutation, deleteNotificationApiV1NotificationsNotificationIdDeleteMutation, deleteSavedScriptApiV1ScriptsScriptIdDeleteMutation, deleteUserApiV1AdminUsersUserIdDeleteMutation, exportEventsApiV1AdminEventsExportExportFormatGetOptions, exportEventsApiV1AdminEventsExportExportFormatGetQueryKey, getCurrentUserProfileApiV1AuthMeGetOptions, getCurrentUserProfileApiV1AuthMeGetQueryKey, getDefaultRateLimitRulesApiV1AdminRateLimitsDefaultsGetOptions, getDefaultRateLimitRulesApiV1AdminRateLimitsDefaultsGetQueryKey, getEventDetailApiV1AdminEventsEventIdGetOptions, getEventDetailApiV1AdminEventsEventIdGetQueryKey, getEventStatsApiV1AdminEventsStatsGetOptions, getEventStatsApiV1AdminEventsStatsGetQueryKey, getExampleScriptsApiV1ExampleScriptsGetOptions, getExampleScriptsApiV1ExampleScriptsGetQueryKey, getExecutionEventsApiV1ExecutionsExecutionIdEventsGetOptions, getExecutionEventsApiV1ExecutionsExecutionIdEventsGetQueryKey, getK8sResourceLimitsApiV1K8sLimitsGetOptions, getK8sResourceLimitsApiV1K8sLimitsGetQueryKey, getNotificationsApiV1NotificationsGetInfiniteOptions, getNotificationsApiV1NotificationsGetInfiniteQueryKey, getNotificationsApiV1NotificationsGetOptions, getNotificationsApiV1NotificationsGetQueryKey, getQueueStatusApiV1AdminExecutionsQueueGetOptions, getQueueStatusApiV1AdminExecutionsQueueGetQueryKey, getReplaySessionApiV1ReplaySessionsSessionIdGetOptions, getReplaySessionApiV1ReplaySessionsSessionIdGetQueryKey, getResultApiV1ExecutionsExecutionIdResultGetOptions, getResultApiV1ExecutionsExecutionIdResultGetQueryKey, getSavedScriptApiV1ScriptsScriptIdGetOptions, getSavedScriptApiV1ScriptsScriptIdGetQueryKey, getSettingsHistoryApiV1UserSettingsHistoryGetOptions, getSettingsHistoryApiV1UserSettingsHistoryGetQueryKey, getSubscriptionsApiV1NotificationsSubscriptionsGetOptions, getSubscriptionsApiV1NotificationsSubscriptionsGetQueryKey, getSystemSettingsApiV1AdminSettingsGetOptions, getSystemSettingsApiV1AdminSettingsGetQueryKey, getUnreadCountApiV1NotificationsUnreadCountGetOptions, getUnreadCountApiV1NotificationsUnreadCountGetQueryKey, getUserApiV1AdminUsersUserIdGetOptions, getUserApiV1AdminUsersUserIdGetQueryKey, getUserExecutionsApiV1UserExecutionsGetOptions, getUserExecutionsApiV1UserExecutionsGetQueryKey, getUserOverviewApiV1AdminUsersUserIdOverviewGetOptions, getUserOverviewApiV1AdminUsersUserIdOverviewGetQueryKey, getUserRateLimitsApiV1AdminRateLimitsUserIdGetOptions, getUserRateLimitsApiV1AdminRateLimitsUserIdGetQueryKey, getUserSettingsApiV1UserSettingsGetOptions, getUserSettingsApiV1UserSettingsGetQueryKey, listExecutionsApiV1AdminExecutionsGetOptions, listExecutionsApiV1AdminExecutionsGetQueryKey, listReplaySessionsApiV1ReplaySessionsGetOptions, listReplaySessionsApiV1ReplaySessionsGetQueryKey, listSavedScriptsApiV1ScriptsGetOptions, listSavedScriptsApiV1ScriptsGetQueryKey, listUsersApiV1AdminUsersGetInfiniteOptions, listUsersApiV1AdminUsersGetInfiniteQueryKey, listUsersApiV1AdminUsersGetOptions, listUsersApiV1AdminUsersGetQueryKey, livenessApiV1HealthLiveGetOptions, livenessApiV1HealthLiveGetQueryKey, loginApiV1AuthLoginPostMutation, logoutApiV1AuthLogoutPostMutation, markAllReadApiV1NotificationsMarkAllReadPostMutation, markNotificationReadApiV1NotificationsNotificationIdReadPutMutation, pauseReplaySessionApiV1ReplaySessionsSessionIdPausePostMutation, type QueryKey, registerApiV1AuthRegisterPostMutation, replayEventsApiV1AdminEventsReplayPostMutation, resetSystemSettingsApiV1AdminSettingsResetPostMutation, resetUserPasswordApiV1AdminUsersUserIdResetPasswordPostMutation, resetUserRateLimitsApiV1AdminRateLimitsUserIdResetPostMutation, restoreSettingsApiV1UserSettingsRestorePostMutation, resumeReplaySessionApiV1ReplaySessionsSessionIdResumePostMutation, retryExecutionApiV1ExecutionsExecutionIdRetryPostMutation, startReplaySessionApiV1ReplaySessionsSessionIdStartPostMutation, unlockUserApiV1AdminUsersUserIdUnlockPostMutation, updateCustomSettingApiV1UserSettingsCustomKeyPutMutation, updateEditorSettingsApiV1UserSettingsEditorPutMutation, updateNotificationSettingsApiV1UserSettingsNotificationsPutMutation, updatePriorityApiV1AdminExecutionsExecutionIdPriorityPutMutation, updateSavedScriptApiV1ScriptsScriptIdPutMutation, updateSubscriptionApiV1NotificationsSubscriptionsChannelPutMutation, updateSystemSettingsApiV1AdminSettingsPutMutation, updateThemeApiV1UserSettingsThemePutMutation, updateUserApiV1AdminUsersUserIdPutMutation, updateUserRateLimitsApiV1AdminRateLimitsUserIdPutMutation, updateUserSettingsApiV1UserSettingsPutMutation } from './@tanstack/svelte-query.gen'; export { client, type CreateClientConfig } from './client.gen'; -export { browseEventsApiV1AdminEventsBrowsePost, cancelExecutionApiV1ExecutionsExecutionIdCancelPost, cancelReplaySessionApiV1ReplaySessionsSessionIdCancelPost, cancelSagaApiV1SagasSagaIdCancelPost, cleanupOldSessionsApiV1ReplayCleanupPost, createExecutionApiV1ExecutePost, createReplaySessionApiV1ReplaySessionsPost, createSavedScriptApiV1ScriptsPost, createUserApiV1AdminUsersPost, deleteEventApiV1AdminEventsEventIdDelete, deleteExecutionApiV1ExecutionsExecutionIdDelete, deleteNotificationApiV1NotificationsNotificationIdDelete, deleteSavedScriptApiV1ScriptsScriptIdDelete, deleteUserApiV1AdminUsersUserIdDelete, executionEventsApiV1EventsExecutionsExecutionIdGet, exportEventsApiV1AdminEventsExportExportFormatGet, getCurrentUserProfileApiV1AuthMeGet, getDefaultRateLimitRulesApiV1AdminRateLimitsDefaultsGet, getEventDetailApiV1AdminEventsEventIdGet, getEventStatsApiV1AdminEventsStatsGet, getExampleScriptsApiV1ExampleScriptsGet, getExecutionEventsApiV1ExecutionsExecutionIdEventsGet, getExecutionSagasApiV1SagasExecutionExecutionIdGet, getK8sResourceLimitsApiV1K8sLimitsGet, getNotificationsApiV1NotificationsGet, getQueueStatusApiV1AdminExecutionsQueueGet, getReplaySessionApiV1ReplaySessionsSessionIdGet, getResultApiV1ExecutionsExecutionIdResultGet, getSagaStatusApiV1SagasSagaIdGet, getSavedScriptApiV1ScriptsScriptIdGet, getSettingsHistoryApiV1UserSettingsHistoryGet, getSubscriptionsApiV1NotificationsSubscriptionsGet, getSystemSettingsApiV1AdminSettingsGet, getUnreadCountApiV1NotificationsUnreadCountGet, getUserApiV1AdminUsersUserIdGet, getUserExecutionsApiV1UserExecutionsGet, getUserOverviewApiV1AdminUsersUserIdOverviewGet, getUserRateLimitsApiV1AdminRateLimitsUserIdGet, getUserSettingsApiV1UserSettingsGet, listExecutionsApiV1AdminExecutionsGet, listReplaySessionsApiV1ReplaySessionsGet, listSagasApiV1SagasGet, listSavedScriptsApiV1ScriptsGet, listUsersApiV1AdminUsersGet, livenessApiV1HealthLiveGet, loginApiV1AuthLoginPost, logoutApiV1AuthLogoutPost, markAllReadApiV1NotificationsMarkAllReadPost, markNotificationReadApiV1NotificationsNotificationIdReadPut, notificationStreamApiV1EventsNotificationsStreamGet, type Options, pauseReplaySessionApiV1ReplaySessionsSessionIdPausePost, registerApiV1AuthRegisterPost, replayEventsApiV1AdminEventsReplayPost, resetSystemSettingsApiV1AdminSettingsResetPost, resetUserPasswordApiV1AdminUsersUserIdResetPasswordPost, resetUserRateLimitsApiV1AdminRateLimitsUserIdResetPost, restoreSettingsApiV1UserSettingsRestorePost, resumeReplaySessionApiV1ReplaySessionsSessionIdResumePost, retryExecutionApiV1ExecutionsExecutionIdRetryPost, startReplaySessionApiV1ReplaySessionsSessionIdStartPost, streamReplayStatusApiV1AdminEventsReplaySessionIdStatusGet, unlockUserApiV1AdminUsersUserIdUnlockPost, updateCustomSettingApiV1UserSettingsCustomKeyPut, updateEditorSettingsApiV1UserSettingsEditorPut, updateNotificationSettingsApiV1UserSettingsNotificationsPut, updatePriorityApiV1AdminExecutionsExecutionIdPriorityPut, updateSavedScriptApiV1ScriptsScriptIdPut, updateSubscriptionApiV1NotificationsSubscriptionsChannelPut, updateSystemSettingsApiV1AdminSettingsPut, updateThemeApiV1UserSettingsThemePut, updateUserApiV1AdminUsersUserIdPut, updateUserRateLimitsApiV1AdminRateLimitsUserIdPut, updateUserSettingsApiV1UserSettingsPut } from './sdk.gen'; -export { type AdminExecutionListResponse, type AdminExecutionResponse, type AdminUserOverview, type AllocateResourcesCommandEvent, type AuthFailedEvent, type BodyLoginApiV1AuthLoginPost, type BrowseEventsApiV1AdminEventsBrowsePostData, type BrowseEventsApiV1AdminEventsBrowsePostError, type BrowseEventsApiV1AdminEventsBrowsePostErrors, type BrowseEventsApiV1AdminEventsBrowsePostResponse, type BrowseEventsApiV1AdminEventsBrowsePostResponses, type CancelExecutionApiV1ExecutionsExecutionIdCancelPostData, type CancelExecutionApiV1ExecutionsExecutionIdCancelPostError, type CancelExecutionApiV1ExecutionsExecutionIdCancelPostErrors, type CancelExecutionApiV1ExecutionsExecutionIdCancelPostResponse, type CancelExecutionApiV1ExecutionsExecutionIdCancelPostResponses, type CancelExecutionRequest, type CancelReplaySessionApiV1ReplaySessionsSessionIdCancelPostData, type CancelReplaySessionApiV1ReplaySessionsSessionIdCancelPostError, type CancelReplaySessionApiV1ReplaySessionsSessionIdCancelPostErrors, type CancelReplaySessionApiV1ReplaySessionsSessionIdCancelPostResponse, type CancelReplaySessionApiV1ReplaySessionsSessionIdCancelPostResponses, type CancelResponse, type CancelSagaApiV1SagasSagaIdCancelPostData, type CancelSagaApiV1SagasSagaIdCancelPostError, type CancelSagaApiV1SagasSagaIdCancelPostErrors, type CancelSagaApiV1SagasSagaIdCancelPostResponse, type CancelSagaApiV1SagasSagaIdCancelPostResponses, CancelStatus, type CleanupOldSessionsApiV1ReplayCleanupPostData, type CleanupOldSessionsApiV1ReplayCleanupPostError, type CleanupOldSessionsApiV1ReplayCleanupPostErrors, type CleanupOldSessionsApiV1ReplayCleanupPostResponse, type CleanupOldSessionsApiV1ReplayCleanupPostResponses, type CleanupResponse, type ClientOptions, type ContainerStatusInfo, type CreateExecutionApiV1ExecutePostData, type CreateExecutionApiV1ExecutePostError, type CreateExecutionApiV1ExecutePostErrors, type CreateExecutionApiV1ExecutePostResponse, type CreateExecutionApiV1ExecutePostResponses, type CreatePodCommandEvent, type CreateReplaySessionApiV1ReplaySessionsPostData, type CreateReplaySessionApiV1ReplaySessionsPostError, type CreateReplaySessionApiV1ReplaySessionsPostErrors, type CreateReplaySessionApiV1ReplaySessionsPostResponse, type CreateReplaySessionApiV1ReplaySessionsPostResponses, type CreateSavedScriptApiV1ScriptsPostData, type CreateSavedScriptApiV1ScriptsPostError, type CreateSavedScriptApiV1ScriptsPostErrors, type CreateSavedScriptApiV1ScriptsPostResponse, type CreateSavedScriptApiV1ScriptsPostResponses, type CreateUserApiV1AdminUsersPostData, type CreateUserApiV1AdminUsersPostError, type CreateUserApiV1AdminUsersPostErrors, type CreateUserApiV1AdminUsersPostResponse, type CreateUserApiV1AdminUsersPostResponses, type DeleteEventApiV1AdminEventsEventIdDeleteData, type DeleteEventApiV1AdminEventsEventIdDeleteError, type DeleteEventApiV1AdminEventsEventIdDeleteErrors, type DeleteEventApiV1AdminEventsEventIdDeleteResponse, type DeleteEventApiV1AdminEventsEventIdDeleteResponses, type DeleteExecutionApiV1ExecutionsExecutionIdDeleteData, type DeleteExecutionApiV1ExecutionsExecutionIdDeleteError, type DeleteExecutionApiV1ExecutionsExecutionIdDeleteErrors, type DeleteExecutionApiV1ExecutionsExecutionIdDeleteResponse, type DeleteExecutionApiV1ExecutionsExecutionIdDeleteResponses, type DeleteNotificationApiV1NotificationsNotificationIdDeleteData, type DeleteNotificationApiV1NotificationsNotificationIdDeleteError, type DeleteNotificationApiV1NotificationsNotificationIdDeleteErrors, type DeleteNotificationApiV1NotificationsNotificationIdDeleteResponse, type DeleteNotificationApiV1NotificationsNotificationIdDeleteResponses, type DeleteNotificationResponse, type DeletePodCommandEvent, type DeleteResponse, type DeleteSavedScriptApiV1ScriptsScriptIdDeleteData, type DeleteSavedScriptApiV1ScriptsScriptIdDeleteError, type DeleteSavedScriptApiV1ScriptsScriptIdDeleteErrors, type DeleteSavedScriptApiV1ScriptsScriptIdDeleteResponse, type DeleteSavedScriptApiV1ScriptsScriptIdDeleteResponses, type DeleteUserApiV1AdminUsersUserIdDeleteData, type DeleteUserApiV1AdminUsersUserIdDeleteError, type DeleteUserApiV1AdminUsersUserIdDeleteErrors, type DeleteUserApiV1AdminUsersUserIdDeleteResponse, type DeleteUserApiV1AdminUsersUserIdDeleteResponses, type DeleteUserResponse, type DerivedCounts, type DlqMessageDiscardedEvent, type DlqMessageReceivedEvent, type DlqMessageRetriedEvent, type EditorSettingsInput, type EditorSettingsOutput, EndpointGroup, type EndpointUsageStats, Environment, type ErrorResponse, type EventBrowseRequest, type EventBrowseResponse, type EventDeleteResponse, type EventDetailResponse, type EventFilter, type EventMetadata, type EventReplayRequest, type EventReplayResponse, type EventReplayStatusResponse, type EventReplayStatusResponseWritable, type EventStatistics, type EventStatsResponse, type EventSummary, EventType, type EventTypeCount, type ExampleScripts, type ExecutionAcceptedEvent, type ExecutionCancelledEvent, type ExecutionCompletedEvent, ExecutionErrorType, type ExecutionEventsApiV1EventsExecutionsExecutionIdGetData, type ExecutionEventsApiV1EventsExecutionsExecutionIdGetError, type ExecutionEventsApiV1EventsExecutionsExecutionIdGetErrors, type ExecutionEventsApiV1EventsExecutionsExecutionIdGetResponse, type ExecutionEventsApiV1EventsExecutionsExecutionIdGetResponses, type ExecutionFailedEvent, type ExecutionListResponse, type ExecutionQueuedEvent, type ExecutionRequest, type ExecutionRequestedEvent, type ExecutionResponse, type ExecutionResult, type ExecutionRunningEvent, type ExecutionStartedEvent, ExecutionStatus, type ExecutionTimeoutEvent, type ExportEventsApiV1AdminEventsExportExportFormatGetData, type ExportEventsApiV1AdminEventsExportExportFormatGetError, type ExportEventsApiV1AdminEventsExportExportFormatGetErrors, type ExportEventsApiV1AdminEventsExportExportFormatGetResponses, ExportFormat, type GetCurrentUserProfileApiV1AuthMeGetData, type GetCurrentUserProfileApiV1AuthMeGetError, type GetCurrentUserProfileApiV1AuthMeGetErrors, type GetCurrentUserProfileApiV1AuthMeGetResponse, type GetCurrentUserProfileApiV1AuthMeGetResponses, type GetDefaultRateLimitRulesApiV1AdminRateLimitsDefaultsGetData, type GetDefaultRateLimitRulesApiV1AdminRateLimitsDefaultsGetError, type GetDefaultRateLimitRulesApiV1AdminRateLimitsDefaultsGetErrors, type GetDefaultRateLimitRulesApiV1AdminRateLimitsDefaultsGetResponse, type GetDefaultRateLimitRulesApiV1AdminRateLimitsDefaultsGetResponses, type GetEventDetailApiV1AdminEventsEventIdGetData, type GetEventDetailApiV1AdminEventsEventIdGetError, type GetEventDetailApiV1AdminEventsEventIdGetErrors, type GetEventDetailApiV1AdminEventsEventIdGetResponse, type GetEventDetailApiV1AdminEventsEventIdGetResponses, type GetEventStatsApiV1AdminEventsStatsGetData, type GetEventStatsApiV1AdminEventsStatsGetError, type GetEventStatsApiV1AdminEventsStatsGetErrors, type GetEventStatsApiV1AdminEventsStatsGetResponse, type GetEventStatsApiV1AdminEventsStatsGetResponses, type GetExampleScriptsApiV1ExampleScriptsGetData, type GetExampleScriptsApiV1ExampleScriptsGetError, type GetExampleScriptsApiV1ExampleScriptsGetErrors, type GetExampleScriptsApiV1ExampleScriptsGetResponse, type GetExampleScriptsApiV1ExampleScriptsGetResponses, type GetExecutionEventsApiV1ExecutionsExecutionIdEventsGetData, type GetExecutionEventsApiV1ExecutionsExecutionIdEventsGetError, type GetExecutionEventsApiV1ExecutionsExecutionIdEventsGetErrors, type GetExecutionEventsApiV1ExecutionsExecutionIdEventsGetResponse, type GetExecutionEventsApiV1ExecutionsExecutionIdEventsGetResponses, type GetExecutionSagasApiV1SagasExecutionExecutionIdGetData, type GetExecutionSagasApiV1SagasExecutionExecutionIdGetError, type GetExecutionSagasApiV1SagasExecutionExecutionIdGetErrors, type GetExecutionSagasApiV1SagasExecutionExecutionIdGetResponse, type GetExecutionSagasApiV1SagasExecutionExecutionIdGetResponses, type GetK8sResourceLimitsApiV1K8sLimitsGetData, type GetK8sResourceLimitsApiV1K8sLimitsGetError, type GetK8sResourceLimitsApiV1K8sLimitsGetErrors, type GetK8sResourceLimitsApiV1K8sLimitsGetResponse, type GetK8sResourceLimitsApiV1K8sLimitsGetResponses, type GetNotificationsApiV1NotificationsGetData, type GetNotificationsApiV1NotificationsGetError, type GetNotificationsApiV1NotificationsGetErrors, type GetNotificationsApiV1NotificationsGetResponse, type GetNotificationsApiV1NotificationsGetResponses, type GetQueueStatusApiV1AdminExecutionsQueueGetData, type GetQueueStatusApiV1AdminExecutionsQueueGetError, type GetQueueStatusApiV1AdminExecutionsQueueGetErrors, type GetQueueStatusApiV1AdminExecutionsQueueGetResponse, type GetQueueStatusApiV1AdminExecutionsQueueGetResponses, type GetReplaySessionApiV1ReplaySessionsSessionIdGetData, type GetReplaySessionApiV1ReplaySessionsSessionIdGetError, type GetReplaySessionApiV1ReplaySessionsSessionIdGetErrors, type GetReplaySessionApiV1ReplaySessionsSessionIdGetResponse, type GetReplaySessionApiV1ReplaySessionsSessionIdGetResponses, type GetResultApiV1ExecutionsExecutionIdResultGetData, type GetResultApiV1ExecutionsExecutionIdResultGetError, type GetResultApiV1ExecutionsExecutionIdResultGetErrors, type GetResultApiV1ExecutionsExecutionIdResultGetResponse, type GetResultApiV1ExecutionsExecutionIdResultGetResponses, type GetSagaStatusApiV1SagasSagaIdGetData, type GetSagaStatusApiV1SagasSagaIdGetError, type GetSagaStatusApiV1SagasSagaIdGetErrors, type GetSagaStatusApiV1SagasSagaIdGetResponse, type GetSagaStatusApiV1SagasSagaIdGetResponses, type GetSavedScriptApiV1ScriptsScriptIdGetData, type GetSavedScriptApiV1ScriptsScriptIdGetError, type GetSavedScriptApiV1ScriptsScriptIdGetErrors, type GetSavedScriptApiV1ScriptsScriptIdGetResponse, type GetSavedScriptApiV1ScriptsScriptIdGetResponses, type GetSettingsHistoryApiV1UserSettingsHistoryGetData, type GetSettingsHistoryApiV1UserSettingsHistoryGetError, type GetSettingsHistoryApiV1UserSettingsHistoryGetErrors, type GetSettingsHistoryApiV1UserSettingsHistoryGetResponse, type GetSettingsHistoryApiV1UserSettingsHistoryGetResponses, type GetSubscriptionsApiV1NotificationsSubscriptionsGetData, type GetSubscriptionsApiV1NotificationsSubscriptionsGetError, type GetSubscriptionsApiV1NotificationsSubscriptionsGetErrors, type GetSubscriptionsApiV1NotificationsSubscriptionsGetResponse, type GetSubscriptionsApiV1NotificationsSubscriptionsGetResponses, type GetSystemSettingsApiV1AdminSettingsGetData, type GetSystemSettingsApiV1AdminSettingsGetError, type GetSystemSettingsApiV1AdminSettingsGetErrors, type GetSystemSettingsApiV1AdminSettingsGetResponse, type GetSystemSettingsApiV1AdminSettingsGetResponses, type GetUnreadCountApiV1NotificationsUnreadCountGetData, type GetUnreadCountApiV1NotificationsUnreadCountGetError, type GetUnreadCountApiV1NotificationsUnreadCountGetErrors, type GetUnreadCountApiV1NotificationsUnreadCountGetResponse, type GetUnreadCountApiV1NotificationsUnreadCountGetResponses, type GetUserApiV1AdminUsersUserIdGetData, type GetUserApiV1AdminUsersUserIdGetError, type GetUserApiV1AdminUsersUserIdGetErrors, type GetUserApiV1AdminUsersUserIdGetResponse, type GetUserApiV1AdminUsersUserIdGetResponses, type GetUserExecutionsApiV1UserExecutionsGetData, type GetUserExecutionsApiV1UserExecutionsGetError, type GetUserExecutionsApiV1UserExecutionsGetErrors, type GetUserExecutionsApiV1UserExecutionsGetResponse, type GetUserExecutionsApiV1UserExecutionsGetResponses, type GetUserOverviewApiV1AdminUsersUserIdOverviewGetData, type GetUserOverviewApiV1AdminUsersUserIdOverviewGetError, type GetUserOverviewApiV1AdminUsersUserIdOverviewGetErrors, type GetUserOverviewApiV1AdminUsersUserIdOverviewGetResponse, type GetUserOverviewApiV1AdminUsersUserIdOverviewGetResponses, type GetUserRateLimitsApiV1AdminRateLimitsUserIdGetData, type GetUserRateLimitsApiV1AdminRateLimitsUserIdGetError, type GetUserRateLimitsApiV1AdminRateLimitsUserIdGetErrors, type GetUserRateLimitsApiV1AdminRateLimitsUserIdGetResponse, type GetUserRateLimitsApiV1AdminRateLimitsUserIdGetResponses, type GetUserSettingsApiV1UserSettingsGetData, type GetUserSettingsApiV1UserSettingsGetError, type GetUserSettingsApiV1UserSettingsGetErrors, type GetUserSettingsApiV1UserSettingsGetResponse, type GetUserSettingsApiV1UserSettingsGetResponses, type HourlyEventCount, type LanguageInfo, type LanguageLimits, type ListExecutionsApiV1AdminExecutionsGetData, type ListExecutionsApiV1AdminExecutionsGetError, type ListExecutionsApiV1AdminExecutionsGetErrors, type ListExecutionsApiV1AdminExecutionsGetResponse, type ListExecutionsApiV1AdminExecutionsGetResponses, type ListReplaySessionsApiV1ReplaySessionsGetData, type ListReplaySessionsApiV1ReplaySessionsGetError, type ListReplaySessionsApiV1ReplaySessionsGetErrors, type ListReplaySessionsApiV1ReplaySessionsGetResponse, type ListReplaySessionsApiV1ReplaySessionsGetResponses, type ListSagasApiV1SagasGetData, type ListSagasApiV1SagasGetError, type ListSagasApiV1SagasGetErrors, type ListSagasApiV1SagasGetResponse, type ListSagasApiV1SagasGetResponses, type ListSavedScriptsApiV1ScriptsGetData, type ListSavedScriptsApiV1ScriptsGetError, type ListSavedScriptsApiV1ScriptsGetErrors, type ListSavedScriptsApiV1ScriptsGetResponse, type ListSavedScriptsApiV1ScriptsGetResponses, type ListUsersApiV1AdminUsersGetData, type ListUsersApiV1AdminUsersGetError, type ListUsersApiV1AdminUsersGetErrors, type ListUsersApiV1AdminUsersGetResponse, type ListUsersApiV1AdminUsersGetResponses, type LivenessApiV1HealthLiveGetData, type LivenessApiV1HealthLiveGetError, type LivenessApiV1HealthLiveGetErrors, type LivenessApiV1HealthLiveGetResponse, type LivenessApiV1HealthLiveGetResponses, type LivenessResponse, type LoginApiV1AuthLoginPostData, type LoginApiV1AuthLoginPostError, type LoginApiV1AuthLoginPostErrors, type LoginApiV1AuthLoginPostResponse, type LoginApiV1AuthLoginPostResponses, LoginMethod, type LoginResponse, LogLevel, type LogoutApiV1AuthLogoutPostData, type LogoutApiV1AuthLogoutPostError, type LogoutApiV1AuthLogoutPostErrors, type LogoutApiV1AuthLogoutPostResponse, type LogoutApiV1AuthLogoutPostResponses, type MarkAllReadApiV1NotificationsMarkAllReadPostData, type MarkAllReadApiV1NotificationsMarkAllReadPostError, type MarkAllReadApiV1NotificationsMarkAllReadPostErrors, type MarkAllReadApiV1NotificationsMarkAllReadPostResponse, type MarkAllReadApiV1NotificationsMarkAllReadPostResponses, type MarkNotificationReadApiV1NotificationsNotificationIdReadPutData, type MarkNotificationReadApiV1NotificationsNotificationIdReadPutError, type MarkNotificationReadApiV1NotificationsNotificationIdReadPutErrors, type MarkNotificationReadApiV1NotificationsNotificationIdReadPutResponse, type MarkNotificationReadApiV1NotificationsNotificationIdReadPutResponses, type MessageResponse, type NotificationAllReadEvent, NotificationChannel, type NotificationClickedEvent, type NotificationCreatedEvent, type NotificationDeliveredEvent, type NotificationFailedEvent, type NotificationListResponse, type NotificationPreferencesUpdatedEvent, type NotificationReadEvent, type NotificationResponse, type NotificationSentEvent, type NotificationSettingsInput, type NotificationSettingsOutput, NotificationSeverity, NotificationStatus, type NotificationStreamApiV1EventsNotificationsStreamGetData, type NotificationStreamApiV1EventsNotificationsStreamGetError, type NotificationStreamApiV1EventsNotificationsStreamGetErrors, type NotificationStreamApiV1EventsNotificationsStreamGetResponse, type NotificationStreamApiV1EventsNotificationsStreamGetResponses, type NotificationSubscription, type PasswordResetRequest, type PauseReplaySessionApiV1ReplaySessionsSessionIdPausePostData, type PauseReplaySessionApiV1ReplaySessionsSessionIdPausePostError, type PauseReplaySessionApiV1ReplaySessionsSessionIdPausePostErrors, type PauseReplaySessionApiV1ReplaySessionsSessionIdPausePostResponse, type PauseReplaySessionApiV1ReplaySessionsSessionIdPausePostResponses, type PodCreatedEvent, type PodDeletedEvent, type PodFailedEvent, type PodRunningEvent, type PodScheduledEvent, type PodSucceededEvent, type PodTerminatedEvent, type PriorityUpdateRequest, QueuePriority, type QueueStatusResponse, type QuotaExceededEvent, RateLimitAlgorithm, type RateLimitExceededEvent, type RateLimitRuleRequest, type RateLimitRuleResponse, type RateLimitSummary, type RateLimitUpdateRequest, type RateLimitUpdateResponse, type RegisterApiV1AuthRegisterPostData, type RegisterApiV1AuthRegisterPostError, type RegisterApiV1AuthRegisterPostErrors, type RegisterApiV1AuthRegisterPostResponse, type RegisterApiV1AuthRegisterPostResponses, type ReleaseResourcesCommandEvent, type ReplayConfigSchema, type ReplayError, type ReplayEventsApiV1AdminEventsReplayPostData, type ReplayEventsApiV1AdminEventsReplayPostError, type ReplayEventsApiV1AdminEventsReplayPostErrors, type ReplayEventsApiV1AdminEventsReplayPostResponse, type ReplayEventsApiV1AdminEventsReplayPostResponses, type ReplayFilter, type ReplayFilterSchema, type ReplayRequest, type ReplayResponse, type ReplaySession, ReplayStatus, ReplayTarget, ReplayType, type ResetSystemSettingsApiV1AdminSettingsResetPostData, type ResetSystemSettingsApiV1AdminSettingsResetPostError, type ResetSystemSettingsApiV1AdminSettingsResetPostErrors, type ResetSystemSettingsApiV1AdminSettingsResetPostResponse, type ResetSystemSettingsApiV1AdminSettingsResetPostResponses, type ResetUserPasswordApiV1AdminUsersUserIdResetPasswordPostData, type ResetUserPasswordApiV1AdminUsersUserIdResetPasswordPostError, type ResetUserPasswordApiV1AdminUsersUserIdResetPasswordPostErrors, type ResetUserPasswordApiV1AdminUsersUserIdResetPasswordPostResponse, type ResetUserPasswordApiV1AdminUsersUserIdResetPasswordPostResponses, type ResetUserRateLimitsApiV1AdminRateLimitsUserIdResetPostData, type ResetUserRateLimitsApiV1AdminRateLimitsUserIdResetPostError, type ResetUserRateLimitsApiV1AdminRateLimitsUserIdResetPostErrors, type ResetUserRateLimitsApiV1AdminRateLimitsUserIdResetPostResponse, type ResetUserRateLimitsApiV1AdminRateLimitsUserIdResetPostResponses, type ResourceLimitExceededEvent, type ResourceLimits, type ResourceUsage, type ResourceUsageDomain, type RestoreSettingsApiV1UserSettingsRestorePostData, type RestoreSettingsApiV1UserSettingsRestorePostError, type RestoreSettingsApiV1UserSettingsRestorePostErrors, type RestoreSettingsApiV1UserSettingsRestorePostResponse, type RestoreSettingsApiV1UserSettingsRestorePostResponses, type RestoreSettingsRequest, type ResultFailedEvent, type ResultStoredEvent, type ResumeReplaySessionApiV1ReplaySessionsSessionIdResumePostData, type ResumeReplaySessionApiV1ReplaySessionsSessionIdResumePostError, type ResumeReplaySessionApiV1ReplaySessionsSessionIdResumePostErrors, type ResumeReplaySessionApiV1ReplaySessionsSessionIdResumePostResponse, type ResumeReplaySessionApiV1ReplaySessionsSessionIdResumePostResponses, type RetryExecutionApiV1ExecutionsExecutionIdRetryPostData, type RetryExecutionApiV1ExecutionsExecutionIdRetryPostError, type RetryExecutionApiV1ExecutionsExecutionIdRetryPostErrors, type RetryExecutionApiV1ExecutionsExecutionIdRetryPostResponse, type RetryExecutionApiV1ExecutionsExecutionIdRetryPostResponses, type SagaCancellationResponse, type SagaCancelledEvent, type SagaCompensatedEvent, type SagaCompensatingEvent, type SagaCompletedEvent, type SagaFailedEvent, type SagaListResponse, type SagaStartedEvent, SagaState, type SagaStatusResponse, type SavedScriptCreateRequest, type SavedScriptListResponse, type SavedScriptResponse, type SavedScriptUpdate, type ScriptDeletedEvent, type ScriptSavedEvent, type ScriptSharedEvent, type SecurityViolationEvent, type ServiceEventCount, type ServiceRecoveredEvent, type ServiceUnhealthyEvent, type SessionConfigSummary, type SessionSummary, type SessionSummaryWritable, type SettingsHistoryEntry, type SettingsHistoryResponse, SseControlEvent, type SseExecutionEventSchema, type StartReplaySessionApiV1ReplaySessionsSessionIdStartPostData, type StartReplaySessionApiV1ReplaySessionsSessionIdStartPostError, type StartReplaySessionApiV1ReplaySessionsSessionIdStartPostErrors, type StartReplaySessionApiV1ReplaySessionsSessionIdStartPostResponse, type StartReplaySessionApiV1ReplaySessionsSessionIdStartPostResponses, StorageType, type StreamReplayStatusApiV1AdminEventsReplaySessionIdStatusGetData, type StreamReplayStatusApiV1AdminEventsReplaySessionIdStatusGetError, type StreamReplayStatusApiV1AdminEventsReplaySessionIdStatusGetErrors, type StreamReplayStatusApiV1AdminEventsReplaySessionIdStatusGetResponse, type StreamReplayStatusApiV1AdminEventsReplaySessionIdStatusGetResponses, type SubscriptionsResponse, type SubscriptionUpdate, type SystemErrorEvent, type SystemSettingsSchemaInput, type SystemSettingsSchemaOutput, Theme, type ThemeUpdateRequest, type UnlockResponse, type UnlockUserApiV1AdminUsersUserIdUnlockPostData, type UnlockUserApiV1AdminUsersUserIdUnlockPostError, type UnlockUserApiV1AdminUsersUserIdUnlockPostErrors, type UnlockUserApiV1AdminUsersUserIdUnlockPostResponse, type UnlockUserApiV1AdminUsersUserIdUnlockPostResponses, type UnreadCountResponse, type UpdateCustomSettingApiV1UserSettingsCustomKeyPutData, type UpdateCustomSettingApiV1UserSettingsCustomKeyPutError, type UpdateCustomSettingApiV1UserSettingsCustomKeyPutErrors, type UpdateCustomSettingApiV1UserSettingsCustomKeyPutResponse, type UpdateCustomSettingApiV1UserSettingsCustomKeyPutResponses, type UpdateEditorSettingsApiV1UserSettingsEditorPutData, type UpdateEditorSettingsApiV1UserSettingsEditorPutError, type UpdateEditorSettingsApiV1UserSettingsEditorPutErrors, type UpdateEditorSettingsApiV1UserSettingsEditorPutResponse, type UpdateEditorSettingsApiV1UserSettingsEditorPutResponses, type UpdateNotificationSettingsApiV1UserSettingsNotificationsPutData, type UpdateNotificationSettingsApiV1UserSettingsNotificationsPutError, type UpdateNotificationSettingsApiV1UserSettingsNotificationsPutErrors, type UpdateNotificationSettingsApiV1UserSettingsNotificationsPutResponse, type UpdateNotificationSettingsApiV1UserSettingsNotificationsPutResponses, type UpdatePriorityApiV1AdminExecutionsExecutionIdPriorityPutData, type UpdatePriorityApiV1AdminExecutionsExecutionIdPriorityPutError, type UpdatePriorityApiV1AdminExecutionsExecutionIdPriorityPutErrors, type UpdatePriorityApiV1AdminExecutionsExecutionIdPriorityPutResponse, type UpdatePriorityApiV1AdminExecutionsExecutionIdPriorityPutResponses, type UpdateSavedScriptApiV1ScriptsScriptIdPutData, type UpdateSavedScriptApiV1ScriptsScriptIdPutError, type UpdateSavedScriptApiV1ScriptsScriptIdPutErrors, type UpdateSavedScriptApiV1ScriptsScriptIdPutResponse, type UpdateSavedScriptApiV1ScriptsScriptIdPutResponses, type UpdateSubscriptionApiV1NotificationsSubscriptionsChannelPutData, type UpdateSubscriptionApiV1NotificationsSubscriptionsChannelPutError, type UpdateSubscriptionApiV1NotificationsSubscriptionsChannelPutErrors, type UpdateSubscriptionApiV1NotificationsSubscriptionsChannelPutResponse, type UpdateSubscriptionApiV1NotificationsSubscriptionsChannelPutResponses, type UpdateSystemSettingsApiV1AdminSettingsPutData, type UpdateSystemSettingsApiV1AdminSettingsPutError, type UpdateSystemSettingsApiV1AdminSettingsPutErrors, type UpdateSystemSettingsApiV1AdminSettingsPutResponse, type UpdateSystemSettingsApiV1AdminSettingsPutResponses, type UpdateThemeApiV1UserSettingsThemePutData, type UpdateThemeApiV1UserSettingsThemePutError, type UpdateThemeApiV1UserSettingsThemePutErrors, type UpdateThemeApiV1UserSettingsThemePutResponse, type UpdateThemeApiV1UserSettingsThemePutResponses, type UpdateUserApiV1AdminUsersUserIdPutData, type UpdateUserApiV1AdminUsersUserIdPutError, type UpdateUserApiV1AdminUsersUserIdPutErrors, type UpdateUserApiV1AdminUsersUserIdPutResponse, type UpdateUserApiV1AdminUsersUserIdPutResponses, type UpdateUserRateLimitsApiV1AdminRateLimitsUserIdPutData, type UpdateUserRateLimitsApiV1AdminRateLimitsUserIdPutError, type UpdateUserRateLimitsApiV1AdminRateLimitsUserIdPutErrors, type UpdateUserRateLimitsApiV1AdminRateLimitsUserIdPutResponse, type UpdateUserRateLimitsApiV1AdminRateLimitsUserIdPutResponses, type UpdateUserSettingsApiV1UserSettingsPutData, type UpdateUserSettingsApiV1UserSettingsPutError, type UpdateUserSettingsApiV1UserSettingsPutErrors, type UpdateUserSettingsApiV1UserSettingsPutResponse, type UpdateUserSettingsApiV1UserSettingsPutResponses, type UserCreate, type UserCreateWritable, type UserDeletedEvent, type UserEventCount, type UserListResponse, type UserLoggedInEvent, type UserLoggedOutEvent, type UserLoginEvent, type UserRateLimitConfigResponse, type UserRateLimitsResponse, type UserRegisteredEvent, type UserResponse, UserRole, type UserSettings, type UserSettingsUpdate, type UserSettingsUpdatedEvent, type UserUpdate, type UserUpdatedEvent, type UserUpdateWritable } from './types.gen'; -export { zAdminExecutionListResponse, zAdminExecutionResponse, zAdminUserOverview, zAllocateResourcesCommandEvent, zAuthFailedEvent, zBodyLoginApiV1AuthLoginPost, zBrowseEventsApiV1AdminEventsBrowsePostBody, zBrowseEventsApiV1AdminEventsBrowsePostResponse, zCancelExecutionApiV1ExecutionsExecutionIdCancelPostBody, zCancelExecutionApiV1ExecutionsExecutionIdCancelPostPath, zCancelExecutionApiV1ExecutionsExecutionIdCancelPostResponse, zCancelExecutionRequest, zCancelReplaySessionApiV1ReplaySessionsSessionIdCancelPostPath, zCancelReplaySessionApiV1ReplaySessionsSessionIdCancelPostResponse, zCancelResponse, zCancelSagaApiV1SagasSagaIdCancelPostPath, zCancelSagaApiV1SagasSagaIdCancelPostResponse, zCancelStatus, zCleanupOldSessionsApiV1ReplayCleanupPostQuery, zCleanupOldSessionsApiV1ReplayCleanupPostResponse, zCleanupResponse, zContainerStatusInfo, zCreateExecutionApiV1ExecutePostBody, zCreateExecutionApiV1ExecutePostHeaders, zCreateExecutionApiV1ExecutePostResponse, zCreatePodCommandEvent, zCreateReplaySessionApiV1ReplaySessionsPostBody, zCreateReplaySessionApiV1ReplaySessionsPostResponse, zCreateSavedScriptApiV1ScriptsPostBody, zCreateSavedScriptApiV1ScriptsPostResponse, zCreateUserApiV1AdminUsersPostBody, zCreateUserApiV1AdminUsersPostResponse, zDeleteEventApiV1AdminEventsEventIdDeletePath, zDeleteEventApiV1AdminEventsEventIdDeleteResponse, zDeleteExecutionApiV1ExecutionsExecutionIdDeletePath, zDeleteExecutionApiV1ExecutionsExecutionIdDeleteResponse, zDeleteNotificationApiV1NotificationsNotificationIdDeletePath, zDeleteNotificationApiV1NotificationsNotificationIdDeleteResponse, zDeleteNotificationResponse, zDeletePodCommandEvent, zDeleteResponse, zDeleteSavedScriptApiV1ScriptsScriptIdDeletePath, zDeleteSavedScriptApiV1ScriptsScriptIdDeleteResponse, zDeleteUserApiV1AdminUsersUserIdDeletePath, zDeleteUserApiV1AdminUsersUserIdDeleteQuery, zDeleteUserApiV1AdminUsersUserIdDeleteResponse, zDeleteUserResponse, zDerivedCounts, zDlqMessageDiscardedEvent, zDlqMessageReceivedEvent, zDlqMessageRetriedEvent, zEditorSettingsInput, zEditorSettingsOutput, zEndpointGroup, zEndpointUsageStats, zEnvironment, zErrorResponse, zEventBrowseRequest, zEventBrowseResponse, zEventDeleteResponse, zEventDetailResponse, zEventFilter, zEventMetadata, zEventReplayRequest, zEventReplayResponse, zEventReplayStatusResponse, zEventReplayStatusResponseWritable, zEventStatistics, zEventStatsResponse, zEventSummary, zEventType, zEventTypeCount, zExampleScripts, zExecutionAcceptedEvent, zExecutionCancelledEvent, zExecutionCompletedEvent, zExecutionErrorType, zExecutionEventsApiV1EventsExecutionsExecutionIdGetPath, zExecutionEventsApiV1EventsExecutionsExecutionIdGetResponse, zExecutionFailedEvent, zExecutionListResponse, zExecutionQueuedEvent, zExecutionRequest, zExecutionRequestedEvent, zExecutionResponse, zExecutionResult, zExecutionRunningEvent, zExecutionStartedEvent, zExecutionStatus, zExecutionTimeoutEvent, zExportEventsApiV1AdminEventsExportExportFormatGetPath, zExportEventsApiV1AdminEventsExportExportFormatGetQuery, zExportFormat, zGetCurrentUserProfileApiV1AuthMeGetResponse, zGetDefaultRateLimitRulesApiV1AdminRateLimitsDefaultsGetResponse, zGetEventDetailApiV1AdminEventsEventIdGetPath, zGetEventDetailApiV1AdminEventsEventIdGetResponse, zGetEventStatsApiV1AdminEventsStatsGetQuery, zGetEventStatsApiV1AdminEventsStatsGetResponse, zGetExampleScriptsApiV1ExampleScriptsGetResponse, zGetExecutionEventsApiV1ExecutionsExecutionIdEventsGetPath, zGetExecutionEventsApiV1ExecutionsExecutionIdEventsGetQuery, zGetExecutionEventsApiV1ExecutionsExecutionIdEventsGetResponse, zGetExecutionSagasApiV1SagasExecutionExecutionIdGetPath, zGetExecutionSagasApiV1SagasExecutionExecutionIdGetQuery, zGetExecutionSagasApiV1SagasExecutionExecutionIdGetResponse, zGetK8sResourceLimitsApiV1K8sLimitsGetResponse, zGetNotificationsApiV1NotificationsGetQuery, zGetNotificationsApiV1NotificationsGetResponse, zGetQueueStatusApiV1AdminExecutionsQueueGetResponse, zGetReplaySessionApiV1ReplaySessionsSessionIdGetPath, zGetReplaySessionApiV1ReplaySessionsSessionIdGetResponse, zGetResultApiV1ExecutionsExecutionIdResultGetPath, zGetResultApiV1ExecutionsExecutionIdResultGetResponse, zGetSagaStatusApiV1SagasSagaIdGetPath, zGetSagaStatusApiV1SagasSagaIdGetResponse, zGetSavedScriptApiV1ScriptsScriptIdGetPath, zGetSavedScriptApiV1ScriptsScriptIdGetResponse, zGetSettingsHistoryApiV1UserSettingsHistoryGetQuery, zGetSettingsHistoryApiV1UserSettingsHistoryGetResponse, zGetSubscriptionsApiV1NotificationsSubscriptionsGetResponse, zGetSystemSettingsApiV1AdminSettingsGetResponse, zGetUnreadCountApiV1NotificationsUnreadCountGetResponse, zGetUserApiV1AdminUsersUserIdGetPath, zGetUserApiV1AdminUsersUserIdGetResponse, zGetUserExecutionsApiV1UserExecutionsGetQuery, zGetUserExecutionsApiV1UserExecutionsGetResponse, zGetUserOverviewApiV1AdminUsersUserIdOverviewGetPath, zGetUserOverviewApiV1AdminUsersUserIdOverviewGetResponse, zGetUserRateLimitsApiV1AdminRateLimitsUserIdGetPath, zGetUserRateLimitsApiV1AdminRateLimitsUserIdGetResponse, zGetUserSettingsApiV1UserSettingsGetResponse, zHourlyEventCount, zLanguageInfo, zLanguageLimits, zListExecutionsApiV1AdminExecutionsGetQuery, zListExecutionsApiV1AdminExecutionsGetResponse, zListReplaySessionsApiV1ReplaySessionsGetQuery, zListReplaySessionsApiV1ReplaySessionsGetResponse, zListSagasApiV1SagasGetQuery, zListSagasApiV1SagasGetResponse, zListSavedScriptsApiV1ScriptsGetResponse, zListUsersApiV1AdminUsersGetQuery, zListUsersApiV1AdminUsersGetResponse, zLivenessApiV1HealthLiveGetResponse, zLivenessResponse, zLoginApiV1AuthLoginPostBody, zLoginApiV1AuthLoginPostResponse, zLoginMethod, zLoginResponse, zLogLevel, zLogoutApiV1AuthLogoutPostResponse, zMarkAllReadApiV1NotificationsMarkAllReadPostResponse, zMarkNotificationReadApiV1NotificationsNotificationIdReadPutPath, zMarkNotificationReadApiV1NotificationsNotificationIdReadPutResponse, zMessageResponse, zNotificationAllReadEvent, zNotificationChannel, zNotificationClickedEvent, zNotificationCreatedEvent, zNotificationDeliveredEvent, zNotificationFailedEvent, zNotificationListResponse, zNotificationPreferencesUpdatedEvent, zNotificationReadEvent, zNotificationResponse, zNotificationSentEvent, zNotificationSettingsInput, zNotificationSettingsOutput, zNotificationSeverity, zNotificationStatus, zNotificationStreamApiV1EventsNotificationsStreamGetResponse, zNotificationSubscription, zPasswordResetRequest, zPauseReplaySessionApiV1ReplaySessionsSessionIdPausePostPath, zPauseReplaySessionApiV1ReplaySessionsSessionIdPausePostResponse, zPodCreatedEvent, zPodDeletedEvent, zPodFailedEvent, zPodRunningEvent, zPodScheduledEvent, zPodSucceededEvent, zPodTerminatedEvent, zPriorityUpdateRequest, zQueuePriority, zQueueStatusResponse, zQuotaExceededEvent, zRateLimitAlgorithm, zRateLimitExceededEvent, zRateLimitRuleRequest, zRateLimitRuleResponse, zRateLimitSummary, zRateLimitUpdateRequest, zRateLimitUpdateResponse, zRegisterApiV1AuthRegisterPostBody, zRegisterApiV1AuthRegisterPostResponse, zReleaseResourcesCommandEvent, zReplayConfigSchema, zReplayError, zReplayEventsApiV1AdminEventsReplayPostBody, zReplayEventsApiV1AdminEventsReplayPostResponse, zReplayFilter, zReplayFilterSchema, zReplayRequest, zReplayResponse, zReplaySession, zReplayStatus, zReplayTarget, zReplayType, zResetSystemSettingsApiV1AdminSettingsResetPostResponse, zResetUserPasswordApiV1AdminUsersUserIdResetPasswordPostBody, zResetUserPasswordApiV1AdminUsersUserIdResetPasswordPostPath, zResetUserPasswordApiV1AdminUsersUserIdResetPasswordPostResponse, zResetUserRateLimitsApiV1AdminRateLimitsUserIdResetPostPath, zResetUserRateLimitsApiV1AdminRateLimitsUserIdResetPostResponse, zResourceLimitExceededEvent, zResourceLimits, zResourceUsage, zResourceUsageDomain, zRestoreSettingsApiV1UserSettingsRestorePostBody, zRestoreSettingsApiV1UserSettingsRestorePostResponse, zRestoreSettingsRequest, zResultFailedEvent, zResultStoredEvent, zResumeReplaySessionApiV1ReplaySessionsSessionIdResumePostPath, zResumeReplaySessionApiV1ReplaySessionsSessionIdResumePostResponse, zRetryExecutionApiV1ExecutionsExecutionIdRetryPostPath, zRetryExecutionApiV1ExecutionsExecutionIdRetryPostResponse, zSagaCancellationResponse, zSagaCancelledEvent, zSagaCompensatedEvent, zSagaCompensatingEvent, zSagaCompletedEvent, zSagaFailedEvent, zSagaListResponse, zSagaStartedEvent, zSagaState, zSagaStatusResponse, zSavedScriptCreateRequest, zSavedScriptListResponse, zSavedScriptResponse, zSavedScriptUpdate, zScriptDeletedEvent, zScriptSavedEvent, zScriptSharedEvent, zSecurityViolationEvent, zServiceEventCount, zServiceRecoveredEvent, zServiceUnhealthyEvent, zSessionConfigSummary, zSessionSummary, zSessionSummaryWritable, zSettingsHistoryEntry, zSettingsHistoryResponse, zSseControlEvent, zSseExecutionEventSchema, zStartReplaySessionApiV1ReplaySessionsSessionIdStartPostPath, zStartReplaySessionApiV1ReplaySessionsSessionIdStartPostResponse, zStorageType, zStreamReplayStatusApiV1AdminEventsReplaySessionIdStatusGetPath, zStreamReplayStatusApiV1AdminEventsReplaySessionIdStatusGetResponse, zSubscriptionsResponse, zSubscriptionUpdate, zSystemErrorEvent, zSystemSettingsSchemaInput, zSystemSettingsSchemaOutput, zTheme, zThemeUpdateRequest, zUnlockResponse, zUnlockUserApiV1AdminUsersUserIdUnlockPostPath, zUnlockUserApiV1AdminUsersUserIdUnlockPostResponse, zUnreadCountResponse, zUpdateCustomSettingApiV1UserSettingsCustomKeyPutBody, zUpdateCustomSettingApiV1UserSettingsCustomKeyPutPath, zUpdateCustomSettingApiV1UserSettingsCustomKeyPutResponse, zUpdateEditorSettingsApiV1UserSettingsEditorPutBody, zUpdateEditorSettingsApiV1UserSettingsEditorPutResponse, zUpdateNotificationSettingsApiV1UserSettingsNotificationsPutBody, zUpdateNotificationSettingsApiV1UserSettingsNotificationsPutResponse, zUpdatePriorityApiV1AdminExecutionsExecutionIdPriorityPutBody, zUpdatePriorityApiV1AdminExecutionsExecutionIdPriorityPutPath, zUpdatePriorityApiV1AdminExecutionsExecutionIdPriorityPutResponse, zUpdateSavedScriptApiV1ScriptsScriptIdPutBody, zUpdateSavedScriptApiV1ScriptsScriptIdPutPath, zUpdateSavedScriptApiV1ScriptsScriptIdPutResponse, zUpdateSubscriptionApiV1NotificationsSubscriptionsChannelPutBody, zUpdateSubscriptionApiV1NotificationsSubscriptionsChannelPutPath, zUpdateSubscriptionApiV1NotificationsSubscriptionsChannelPutResponse, zUpdateSystemSettingsApiV1AdminSettingsPutBody, zUpdateSystemSettingsApiV1AdminSettingsPutResponse, zUpdateThemeApiV1UserSettingsThemePutBody, zUpdateThemeApiV1UserSettingsThemePutResponse, zUpdateUserApiV1AdminUsersUserIdPutBody, zUpdateUserApiV1AdminUsersUserIdPutPath, zUpdateUserApiV1AdminUsersUserIdPutResponse, zUpdateUserRateLimitsApiV1AdminRateLimitsUserIdPutBody, zUpdateUserRateLimitsApiV1AdminRateLimitsUserIdPutPath, zUpdateUserRateLimitsApiV1AdminRateLimitsUserIdPutResponse, zUpdateUserSettingsApiV1UserSettingsPutBody, zUpdateUserSettingsApiV1UserSettingsPutResponse, zUserCreate, zUserCreateWritable, zUserDeletedEvent, zUserEventCount, zUserListResponse, zUserLoggedInEvent, zUserLoggedOutEvent, zUserLoginEvent, zUserRateLimitConfigResponse, zUserRateLimitsResponse, zUserRegisteredEvent, zUserResponse, zUserRole, zUserSettings, zUserSettingsUpdate, zUserSettingsUpdatedEvent, zUserUpdate, zUserUpdatedEvent, zUserUpdateWritable } from './zod.gen'; +export { browseEventsApiV1AdminEventsBrowsePost, cancelExecutionApiV1ExecutionsExecutionIdCancelPost, cancelReplaySessionApiV1ReplaySessionsSessionIdCancelPost, cleanupOldSessionsApiV1ReplayCleanupPost, createExecutionApiV1ExecutePost, createReplaySessionApiV1ReplaySessionsPost, createSavedScriptApiV1ScriptsPost, createUserApiV1AdminUsersPost, deleteEventApiV1AdminEventsEventIdDelete, deleteExecutionApiV1ExecutionsExecutionIdDelete, deleteNotificationApiV1NotificationsNotificationIdDelete, deleteSavedScriptApiV1ScriptsScriptIdDelete, deleteUserApiV1AdminUsersUserIdDelete, executionEventsApiV1EventsExecutionsExecutionIdGet, exportEventsApiV1AdminEventsExportExportFormatGet, getCurrentUserProfileApiV1AuthMeGet, getDefaultRateLimitRulesApiV1AdminRateLimitsDefaultsGet, getEventDetailApiV1AdminEventsEventIdGet, getEventStatsApiV1AdminEventsStatsGet, getExampleScriptsApiV1ExampleScriptsGet, getExecutionEventsApiV1ExecutionsExecutionIdEventsGet, getK8sResourceLimitsApiV1K8sLimitsGet, getNotificationsApiV1NotificationsGet, getQueueStatusApiV1AdminExecutionsQueueGet, getReplaySessionApiV1ReplaySessionsSessionIdGet, getResultApiV1ExecutionsExecutionIdResultGet, getSavedScriptApiV1ScriptsScriptIdGet, getSettingsHistoryApiV1UserSettingsHistoryGet, getSubscriptionsApiV1NotificationsSubscriptionsGet, getSystemSettingsApiV1AdminSettingsGet, getUnreadCountApiV1NotificationsUnreadCountGet, getUserApiV1AdminUsersUserIdGet, getUserExecutionsApiV1UserExecutionsGet, getUserOverviewApiV1AdminUsersUserIdOverviewGet, getUserRateLimitsApiV1AdminRateLimitsUserIdGet, getUserSettingsApiV1UserSettingsGet, listExecutionsApiV1AdminExecutionsGet, listReplaySessionsApiV1ReplaySessionsGet, listSavedScriptsApiV1ScriptsGet, listUsersApiV1AdminUsersGet, livenessApiV1HealthLiveGet, loginApiV1AuthLoginPost, logoutApiV1AuthLogoutPost, markAllReadApiV1NotificationsMarkAllReadPost, markNotificationReadApiV1NotificationsNotificationIdReadPut, notificationStreamApiV1EventsNotificationsStreamGet, type Options, pauseReplaySessionApiV1ReplaySessionsSessionIdPausePost, registerApiV1AuthRegisterPost, replayEventsApiV1AdminEventsReplayPost, resetSystemSettingsApiV1AdminSettingsResetPost, resetUserPasswordApiV1AdminUsersUserIdResetPasswordPost, resetUserRateLimitsApiV1AdminRateLimitsUserIdResetPost, restoreSettingsApiV1UserSettingsRestorePost, resumeReplaySessionApiV1ReplaySessionsSessionIdResumePost, retryExecutionApiV1ExecutionsExecutionIdRetryPost, startReplaySessionApiV1ReplaySessionsSessionIdStartPost, streamReplayStatusApiV1AdminEventsReplaySessionIdStatusGet, unlockUserApiV1AdminUsersUserIdUnlockPost, updateCustomSettingApiV1UserSettingsCustomKeyPut, updateEditorSettingsApiV1UserSettingsEditorPut, updateNotificationSettingsApiV1UserSettingsNotificationsPut, updatePriorityApiV1AdminExecutionsExecutionIdPriorityPut, updateSavedScriptApiV1ScriptsScriptIdPut, updateSubscriptionApiV1NotificationsSubscriptionsChannelPut, updateSystemSettingsApiV1AdminSettingsPut, updateThemeApiV1UserSettingsThemePut, updateUserApiV1AdminUsersUserIdPut, updateUserRateLimitsApiV1AdminRateLimitsUserIdPut, updateUserSettingsApiV1UserSettingsPut } from './sdk.gen'; +export { type AdminExecutionListResponse, type AdminExecutionResponse, type AdminUserOverview, type AuthFailedEvent, type BodyLoginApiV1AuthLoginPost, type BrowseEventsApiV1AdminEventsBrowsePostData, type BrowseEventsApiV1AdminEventsBrowsePostError, type BrowseEventsApiV1AdminEventsBrowsePostErrors, type BrowseEventsApiV1AdminEventsBrowsePostResponse, type BrowseEventsApiV1AdminEventsBrowsePostResponses, type CancelExecutionApiV1ExecutionsExecutionIdCancelPostData, type CancelExecutionApiV1ExecutionsExecutionIdCancelPostError, type CancelExecutionApiV1ExecutionsExecutionIdCancelPostErrors, type CancelExecutionApiV1ExecutionsExecutionIdCancelPostResponse, type CancelExecutionApiV1ExecutionsExecutionIdCancelPostResponses, type CancelExecutionRequest, type CancelReplaySessionApiV1ReplaySessionsSessionIdCancelPostData, type CancelReplaySessionApiV1ReplaySessionsSessionIdCancelPostError, type CancelReplaySessionApiV1ReplaySessionsSessionIdCancelPostErrors, type CancelReplaySessionApiV1ReplaySessionsSessionIdCancelPostResponse, type CancelReplaySessionApiV1ReplaySessionsSessionIdCancelPostResponses, type CancelResponse, CancelStatus, type CleanupOldSessionsApiV1ReplayCleanupPostData, type CleanupOldSessionsApiV1ReplayCleanupPostError, type CleanupOldSessionsApiV1ReplayCleanupPostErrors, type CleanupOldSessionsApiV1ReplayCleanupPostResponse, type CleanupOldSessionsApiV1ReplayCleanupPostResponses, type CleanupResponse, type ClientOptions, type ContainerStatusInfo, type CreateExecutionApiV1ExecutePostData, type CreateExecutionApiV1ExecutePostError, type CreateExecutionApiV1ExecutePostErrors, type CreateExecutionApiV1ExecutePostResponse, type CreateExecutionApiV1ExecutePostResponses, type CreateReplaySessionApiV1ReplaySessionsPostData, type CreateReplaySessionApiV1ReplaySessionsPostError, type CreateReplaySessionApiV1ReplaySessionsPostErrors, type CreateReplaySessionApiV1ReplaySessionsPostResponse, type CreateReplaySessionApiV1ReplaySessionsPostResponses, type CreateSavedScriptApiV1ScriptsPostData, type CreateSavedScriptApiV1ScriptsPostError, type CreateSavedScriptApiV1ScriptsPostErrors, type CreateSavedScriptApiV1ScriptsPostResponse, type CreateSavedScriptApiV1ScriptsPostResponses, type CreateUserApiV1AdminUsersPostData, type CreateUserApiV1AdminUsersPostError, type CreateUserApiV1AdminUsersPostErrors, type CreateUserApiV1AdminUsersPostResponse, type CreateUserApiV1AdminUsersPostResponses, type DeleteEventApiV1AdminEventsEventIdDeleteData, type DeleteEventApiV1AdminEventsEventIdDeleteError, type DeleteEventApiV1AdminEventsEventIdDeleteErrors, type DeleteEventApiV1AdminEventsEventIdDeleteResponse, type DeleteEventApiV1AdminEventsEventIdDeleteResponses, type DeleteExecutionApiV1ExecutionsExecutionIdDeleteData, type DeleteExecutionApiV1ExecutionsExecutionIdDeleteError, type DeleteExecutionApiV1ExecutionsExecutionIdDeleteErrors, type DeleteExecutionApiV1ExecutionsExecutionIdDeleteResponse, type DeleteExecutionApiV1ExecutionsExecutionIdDeleteResponses, type DeleteNotificationApiV1NotificationsNotificationIdDeleteData, type DeleteNotificationApiV1NotificationsNotificationIdDeleteError, type DeleteNotificationApiV1NotificationsNotificationIdDeleteErrors, type DeleteNotificationApiV1NotificationsNotificationIdDeleteResponse, type DeleteNotificationApiV1NotificationsNotificationIdDeleteResponses, type DeleteNotificationResponse, type DeleteResponse, type DeleteSavedScriptApiV1ScriptsScriptIdDeleteData, type DeleteSavedScriptApiV1ScriptsScriptIdDeleteError, type DeleteSavedScriptApiV1ScriptsScriptIdDeleteErrors, type DeleteSavedScriptApiV1ScriptsScriptIdDeleteResponse, type DeleteSavedScriptApiV1ScriptsScriptIdDeleteResponses, type DeleteUserApiV1AdminUsersUserIdDeleteData, type DeleteUserApiV1AdminUsersUserIdDeleteError, type DeleteUserApiV1AdminUsersUserIdDeleteErrors, type DeleteUserApiV1AdminUsersUserIdDeleteResponse, type DeleteUserApiV1AdminUsersUserIdDeleteResponses, type DeleteUserResponse, type DerivedCounts, type DlqMessageDiscardedEvent, type DlqMessageReceivedEvent, type DlqMessageRetriedEvent, type EditorSettingsInput, type EditorSettingsOutput, EndpointGroup, type EndpointUsageStats, Environment, type ErrorResponse, type EventBrowseRequest, type EventBrowseResponse, type EventDeleteResponse, type EventDetailResponse, type EventFilter, type EventMetadata, type EventReplayRequest, type EventReplayResponse, type EventReplayStatusResponse, type EventReplayStatusResponseWritable, type EventStatistics, type EventStatsResponse, type EventSummary, EventType, type EventTypeCount, type ExampleScripts, type ExecutionAcceptedEvent, type ExecutionCancelledEvent, type ExecutionCompletedEvent, ExecutionErrorType, type ExecutionEventsApiV1EventsExecutionsExecutionIdGetData, type ExecutionEventsApiV1EventsExecutionsExecutionIdGetError, type ExecutionEventsApiV1EventsExecutionsExecutionIdGetErrors, type ExecutionEventsApiV1EventsExecutionsExecutionIdGetResponse, type ExecutionEventsApiV1EventsExecutionsExecutionIdGetResponses, type ExecutionFailedEvent, type ExecutionListResponse, type ExecutionQueuedEvent, type ExecutionRequest, type ExecutionRequestedEvent, type ExecutionResponse, type ExecutionResult, type ExecutionRunningEvent, type ExecutionStartedEvent, ExecutionStatus, type ExecutionTimeoutEvent, type ExportEventsApiV1AdminEventsExportExportFormatGetData, type ExportEventsApiV1AdminEventsExportExportFormatGetError, type ExportEventsApiV1AdminEventsExportExportFormatGetErrors, type ExportEventsApiV1AdminEventsExportExportFormatGetResponses, ExportFormat, type GetCurrentUserProfileApiV1AuthMeGetData, type GetCurrentUserProfileApiV1AuthMeGetError, type GetCurrentUserProfileApiV1AuthMeGetErrors, type GetCurrentUserProfileApiV1AuthMeGetResponse, type GetCurrentUserProfileApiV1AuthMeGetResponses, type GetDefaultRateLimitRulesApiV1AdminRateLimitsDefaultsGetData, type GetDefaultRateLimitRulesApiV1AdminRateLimitsDefaultsGetError, type GetDefaultRateLimitRulesApiV1AdminRateLimitsDefaultsGetErrors, type GetDefaultRateLimitRulesApiV1AdminRateLimitsDefaultsGetResponse, type GetDefaultRateLimitRulesApiV1AdminRateLimitsDefaultsGetResponses, type GetEventDetailApiV1AdminEventsEventIdGetData, type GetEventDetailApiV1AdminEventsEventIdGetError, type GetEventDetailApiV1AdminEventsEventIdGetErrors, type GetEventDetailApiV1AdminEventsEventIdGetResponse, type GetEventDetailApiV1AdminEventsEventIdGetResponses, type GetEventStatsApiV1AdminEventsStatsGetData, type GetEventStatsApiV1AdminEventsStatsGetError, type GetEventStatsApiV1AdminEventsStatsGetErrors, type GetEventStatsApiV1AdminEventsStatsGetResponse, type GetEventStatsApiV1AdminEventsStatsGetResponses, type GetExampleScriptsApiV1ExampleScriptsGetData, type GetExampleScriptsApiV1ExampleScriptsGetError, type GetExampleScriptsApiV1ExampleScriptsGetErrors, type GetExampleScriptsApiV1ExampleScriptsGetResponse, type GetExampleScriptsApiV1ExampleScriptsGetResponses, type GetExecutionEventsApiV1ExecutionsExecutionIdEventsGetData, type GetExecutionEventsApiV1ExecutionsExecutionIdEventsGetError, type GetExecutionEventsApiV1ExecutionsExecutionIdEventsGetErrors, type GetExecutionEventsApiV1ExecutionsExecutionIdEventsGetResponse, type GetExecutionEventsApiV1ExecutionsExecutionIdEventsGetResponses, type GetK8sResourceLimitsApiV1K8sLimitsGetData, type GetK8sResourceLimitsApiV1K8sLimitsGetError, type GetK8sResourceLimitsApiV1K8sLimitsGetErrors, type GetK8sResourceLimitsApiV1K8sLimitsGetResponse, type GetK8sResourceLimitsApiV1K8sLimitsGetResponses, type GetNotificationsApiV1NotificationsGetData, type GetNotificationsApiV1NotificationsGetError, type GetNotificationsApiV1NotificationsGetErrors, type GetNotificationsApiV1NotificationsGetResponse, type GetNotificationsApiV1NotificationsGetResponses, type GetQueueStatusApiV1AdminExecutionsQueueGetData, type GetQueueStatusApiV1AdminExecutionsQueueGetError, type GetQueueStatusApiV1AdminExecutionsQueueGetErrors, type GetQueueStatusApiV1AdminExecutionsQueueGetResponse, type GetQueueStatusApiV1AdminExecutionsQueueGetResponses, type GetReplaySessionApiV1ReplaySessionsSessionIdGetData, type GetReplaySessionApiV1ReplaySessionsSessionIdGetError, type GetReplaySessionApiV1ReplaySessionsSessionIdGetErrors, type GetReplaySessionApiV1ReplaySessionsSessionIdGetResponse, type GetReplaySessionApiV1ReplaySessionsSessionIdGetResponses, type GetResultApiV1ExecutionsExecutionIdResultGetData, type GetResultApiV1ExecutionsExecutionIdResultGetError, type GetResultApiV1ExecutionsExecutionIdResultGetErrors, type GetResultApiV1ExecutionsExecutionIdResultGetResponse, type GetResultApiV1ExecutionsExecutionIdResultGetResponses, type GetSavedScriptApiV1ScriptsScriptIdGetData, type GetSavedScriptApiV1ScriptsScriptIdGetError, type GetSavedScriptApiV1ScriptsScriptIdGetErrors, type GetSavedScriptApiV1ScriptsScriptIdGetResponse, type GetSavedScriptApiV1ScriptsScriptIdGetResponses, type GetSettingsHistoryApiV1UserSettingsHistoryGetData, type GetSettingsHistoryApiV1UserSettingsHistoryGetError, type GetSettingsHistoryApiV1UserSettingsHistoryGetErrors, type GetSettingsHistoryApiV1UserSettingsHistoryGetResponse, type GetSettingsHistoryApiV1UserSettingsHistoryGetResponses, type GetSubscriptionsApiV1NotificationsSubscriptionsGetData, type GetSubscriptionsApiV1NotificationsSubscriptionsGetError, type GetSubscriptionsApiV1NotificationsSubscriptionsGetErrors, type GetSubscriptionsApiV1NotificationsSubscriptionsGetResponse, type GetSubscriptionsApiV1NotificationsSubscriptionsGetResponses, type GetSystemSettingsApiV1AdminSettingsGetData, type GetSystemSettingsApiV1AdminSettingsGetError, type GetSystemSettingsApiV1AdminSettingsGetErrors, type GetSystemSettingsApiV1AdminSettingsGetResponse, type GetSystemSettingsApiV1AdminSettingsGetResponses, type GetUnreadCountApiV1NotificationsUnreadCountGetData, type GetUnreadCountApiV1NotificationsUnreadCountGetError, type GetUnreadCountApiV1NotificationsUnreadCountGetErrors, type GetUnreadCountApiV1NotificationsUnreadCountGetResponse, type GetUnreadCountApiV1NotificationsUnreadCountGetResponses, type GetUserApiV1AdminUsersUserIdGetData, type GetUserApiV1AdminUsersUserIdGetError, type GetUserApiV1AdminUsersUserIdGetErrors, type GetUserApiV1AdminUsersUserIdGetResponse, type GetUserApiV1AdminUsersUserIdGetResponses, type GetUserExecutionsApiV1UserExecutionsGetData, type GetUserExecutionsApiV1UserExecutionsGetError, type GetUserExecutionsApiV1UserExecutionsGetErrors, type GetUserExecutionsApiV1UserExecutionsGetResponse, type GetUserExecutionsApiV1UserExecutionsGetResponses, type GetUserOverviewApiV1AdminUsersUserIdOverviewGetData, type GetUserOverviewApiV1AdminUsersUserIdOverviewGetError, type GetUserOverviewApiV1AdminUsersUserIdOverviewGetErrors, type GetUserOverviewApiV1AdminUsersUserIdOverviewGetResponse, type GetUserOverviewApiV1AdminUsersUserIdOverviewGetResponses, type GetUserRateLimitsApiV1AdminRateLimitsUserIdGetData, type GetUserRateLimitsApiV1AdminRateLimitsUserIdGetError, type GetUserRateLimitsApiV1AdminRateLimitsUserIdGetErrors, type GetUserRateLimitsApiV1AdminRateLimitsUserIdGetResponse, type GetUserRateLimitsApiV1AdminRateLimitsUserIdGetResponses, type GetUserSettingsApiV1UserSettingsGetData, type GetUserSettingsApiV1UserSettingsGetError, type GetUserSettingsApiV1UserSettingsGetErrors, type GetUserSettingsApiV1UserSettingsGetResponse, type GetUserSettingsApiV1UserSettingsGetResponses, type HourlyEventCount, type LanguageInfo, type LanguageLimits, type ListExecutionsApiV1AdminExecutionsGetData, type ListExecutionsApiV1AdminExecutionsGetError, type ListExecutionsApiV1AdminExecutionsGetErrors, type ListExecutionsApiV1AdminExecutionsGetResponse, type ListExecutionsApiV1AdminExecutionsGetResponses, type ListReplaySessionsApiV1ReplaySessionsGetData, type ListReplaySessionsApiV1ReplaySessionsGetError, type ListReplaySessionsApiV1ReplaySessionsGetErrors, type ListReplaySessionsApiV1ReplaySessionsGetResponse, type ListReplaySessionsApiV1ReplaySessionsGetResponses, type ListSavedScriptsApiV1ScriptsGetData, type ListSavedScriptsApiV1ScriptsGetError, type ListSavedScriptsApiV1ScriptsGetErrors, type ListSavedScriptsApiV1ScriptsGetResponse, type ListSavedScriptsApiV1ScriptsGetResponses, type ListUsersApiV1AdminUsersGetData, type ListUsersApiV1AdminUsersGetError, type ListUsersApiV1AdminUsersGetErrors, type ListUsersApiV1AdminUsersGetResponse, type ListUsersApiV1AdminUsersGetResponses, type LivenessApiV1HealthLiveGetData, type LivenessApiV1HealthLiveGetError, type LivenessApiV1HealthLiveGetErrors, type LivenessApiV1HealthLiveGetResponse, type LivenessApiV1HealthLiveGetResponses, type LivenessResponse, type LoginApiV1AuthLoginPostData, type LoginApiV1AuthLoginPostError, type LoginApiV1AuthLoginPostErrors, type LoginApiV1AuthLoginPostResponse, type LoginApiV1AuthLoginPostResponses, LoginMethod, type LoginResponse, LogLevel, type LogoutApiV1AuthLogoutPostData, type LogoutApiV1AuthLogoutPostError, type LogoutApiV1AuthLogoutPostErrors, type LogoutApiV1AuthLogoutPostResponse, type LogoutApiV1AuthLogoutPostResponses, type MarkAllReadApiV1NotificationsMarkAllReadPostData, type MarkAllReadApiV1NotificationsMarkAllReadPostError, type MarkAllReadApiV1NotificationsMarkAllReadPostErrors, type MarkAllReadApiV1NotificationsMarkAllReadPostResponse, type MarkAllReadApiV1NotificationsMarkAllReadPostResponses, type MarkNotificationReadApiV1NotificationsNotificationIdReadPutData, type MarkNotificationReadApiV1NotificationsNotificationIdReadPutError, type MarkNotificationReadApiV1NotificationsNotificationIdReadPutErrors, type MarkNotificationReadApiV1NotificationsNotificationIdReadPutResponse, type MarkNotificationReadApiV1NotificationsNotificationIdReadPutResponses, type MessageResponse, type NotificationAllReadEvent, NotificationChannel, type NotificationClickedEvent, type NotificationCreatedEvent, type NotificationDeliveredEvent, type NotificationFailedEvent, type NotificationListResponse, type NotificationPreferencesUpdatedEvent, type NotificationReadEvent, type NotificationResponse, type NotificationSentEvent, type NotificationSettingsInput, type NotificationSettingsOutput, NotificationSeverity, NotificationStatus, type NotificationStreamApiV1EventsNotificationsStreamGetData, type NotificationStreamApiV1EventsNotificationsStreamGetError, type NotificationStreamApiV1EventsNotificationsStreamGetErrors, type NotificationStreamApiV1EventsNotificationsStreamGetResponse, type NotificationStreamApiV1EventsNotificationsStreamGetResponses, type NotificationSubscription, type PasswordResetRequest, type PauseReplaySessionApiV1ReplaySessionsSessionIdPausePostData, type PauseReplaySessionApiV1ReplaySessionsSessionIdPausePostError, type PauseReplaySessionApiV1ReplaySessionsSessionIdPausePostErrors, type PauseReplaySessionApiV1ReplaySessionsSessionIdPausePostResponse, type PauseReplaySessionApiV1ReplaySessionsSessionIdPausePostResponses, type PodCreatedEvent, type PodDeletedEvent, type PodFailedEvent, type PodRunningEvent, type PodScheduledEvent, type PodSucceededEvent, type PodTerminatedEvent, type PriorityUpdateRequest, QueuePriority, type QueueStatusResponse, type QuotaExceededEvent, RateLimitAlgorithm, type RateLimitExceededEvent, type RateLimitRuleRequest, type RateLimitRuleResponse, type RateLimitSummary, type RateLimitUpdateRequest, type RateLimitUpdateResponse, type RegisterApiV1AuthRegisterPostData, type RegisterApiV1AuthRegisterPostError, type RegisterApiV1AuthRegisterPostErrors, type RegisterApiV1AuthRegisterPostResponse, type RegisterApiV1AuthRegisterPostResponses, type ReplayConfigSchema, type ReplayError, type ReplayEventsApiV1AdminEventsReplayPostData, type ReplayEventsApiV1AdminEventsReplayPostError, type ReplayEventsApiV1AdminEventsReplayPostErrors, type ReplayEventsApiV1AdminEventsReplayPostResponse, type ReplayEventsApiV1AdminEventsReplayPostResponses, type ReplayFilter, type ReplayFilterSchema, type ReplayRequest, type ReplayResponse, type ReplaySession, ReplayStatus, ReplayTarget, ReplayType, type ResetSystemSettingsApiV1AdminSettingsResetPostData, type ResetSystemSettingsApiV1AdminSettingsResetPostError, type ResetSystemSettingsApiV1AdminSettingsResetPostErrors, type ResetSystemSettingsApiV1AdminSettingsResetPostResponse, type ResetSystemSettingsApiV1AdminSettingsResetPostResponses, type ResetUserPasswordApiV1AdminUsersUserIdResetPasswordPostData, type ResetUserPasswordApiV1AdminUsersUserIdResetPasswordPostError, type ResetUserPasswordApiV1AdminUsersUserIdResetPasswordPostErrors, type ResetUserPasswordApiV1AdminUsersUserIdResetPasswordPostResponse, type ResetUserPasswordApiV1AdminUsersUserIdResetPasswordPostResponses, type ResetUserRateLimitsApiV1AdminRateLimitsUserIdResetPostData, type ResetUserRateLimitsApiV1AdminRateLimitsUserIdResetPostError, type ResetUserRateLimitsApiV1AdminRateLimitsUserIdResetPostErrors, type ResetUserRateLimitsApiV1AdminRateLimitsUserIdResetPostResponse, type ResetUserRateLimitsApiV1AdminRateLimitsUserIdResetPostResponses, type ResourceLimitExceededEvent, type ResourceLimits, type ResourceUsage, type ResourceUsageDomain, type RestoreSettingsApiV1UserSettingsRestorePostData, type RestoreSettingsApiV1UserSettingsRestorePostError, type RestoreSettingsApiV1UserSettingsRestorePostErrors, type RestoreSettingsApiV1UserSettingsRestorePostResponse, type RestoreSettingsApiV1UserSettingsRestorePostResponses, type RestoreSettingsRequest, type ResultFailedEvent, type ResultStoredEvent, type ResumeReplaySessionApiV1ReplaySessionsSessionIdResumePostData, type ResumeReplaySessionApiV1ReplaySessionsSessionIdResumePostError, type ResumeReplaySessionApiV1ReplaySessionsSessionIdResumePostErrors, type ResumeReplaySessionApiV1ReplaySessionsSessionIdResumePostResponse, type ResumeReplaySessionApiV1ReplaySessionsSessionIdResumePostResponses, type RetryExecutionApiV1ExecutionsExecutionIdRetryPostData, type RetryExecutionApiV1ExecutionsExecutionIdRetryPostError, type RetryExecutionApiV1ExecutionsExecutionIdRetryPostErrors, type RetryExecutionApiV1ExecutionsExecutionIdRetryPostResponse, type RetryExecutionApiV1ExecutionsExecutionIdRetryPostResponses, type SavedScriptCreateRequest, type SavedScriptListResponse, type SavedScriptResponse, type SavedScriptUpdate, type ScriptDeletedEvent, type ScriptSavedEvent, type ScriptSharedEvent, type SecurityViolationEvent, type ServiceEventCount, type ServiceRecoveredEvent, type ServiceUnhealthyEvent, type SessionConfigSummary, type SessionSummary, type SessionSummaryWritable, type SettingsHistoryEntry, type SettingsHistoryResponse, SseControlEvent, type SseExecutionEventSchema, type StartReplaySessionApiV1ReplaySessionsSessionIdStartPostData, type StartReplaySessionApiV1ReplaySessionsSessionIdStartPostError, type StartReplaySessionApiV1ReplaySessionsSessionIdStartPostErrors, type StartReplaySessionApiV1ReplaySessionsSessionIdStartPostResponse, type StartReplaySessionApiV1ReplaySessionsSessionIdStartPostResponses, StorageType, type StreamReplayStatusApiV1AdminEventsReplaySessionIdStatusGetData, type StreamReplayStatusApiV1AdminEventsReplaySessionIdStatusGetError, type StreamReplayStatusApiV1AdminEventsReplaySessionIdStatusGetErrors, type StreamReplayStatusApiV1AdminEventsReplaySessionIdStatusGetResponse, type StreamReplayStatusApiV1AdminEventsReplaySessionIdStatusGetResponses, type SubscriptionsResponse, type SubscriptionUpdate, type SystemErrorEvent, type SystemSettingsSchemaInput, type SystemSettingsSchemaOutput, Theme, type ThemeUpdateRequest, type UnlockResponse, type UnlockUserApiV1AdminUsersUserIdUnlockPostData, type UnlockUserApiV1AdminUsersUserIdUnlockPostError, type UnlockUserApiV1AdminUsersUserIdUnlockPostErrors, type UnlockUserApiV1AdminUsersUserIdUnlockPostResponse, type UnlockUserApiV1AdminUsersUserIdUnlockPostResponses, type UnreadCountResponse, type UpdateCustomSettingApiV1UserSettingsCustomKeyPutData, type UpdateCustomSettingApiV1UserSettingsCustomKeyPutError, type UpdateCustomSettingApiV1UserSettingsCustomKeyPutErrors, type UpdateCustomSettingApiV1UserSettingsCustomKeyPutResponse, type UpdateCustomSettingApiV1UserSettingsCustomKeyPutResponses, type UpdateEditorSettingsApiV1UserSettingsEditorPutData, type UpdateEditorSettingsApiV1UserSettingsEditorPutError, type UpdateEditorSettingsApiV1UserSettingsEditorPutErrors, type UpdateEditorSettingsApiV1UserSettingsEditorPutResponse, type UpdateEditorSettingsApiV1UserSettingsEditorPutResponses, type UpdateNotificationSettingsApiV1UserSettingsNotificationsPutData, type UpdateNotificationSettingsApiV1UserSettingsNotificationsPutError, type UpdateNotificationSettingsApiV1UserSettingsNotificationsPutErrors, type UpdateNotificationSettingsApiV1UserSettingsNotificationsPutResponse, type UpdateNotificationSettingsApiV1UserSettingsNotificationsPutResponses, type UpdatePriorityApiV1AdminExecutionsExecutionIdPriorityPutData, type UpdatePriorityApiV1AdminExecutionsExecutionIdPriorityPutError, type UpdatePriorityApiV1AdminExecutionsExecutionIdPriorityPutErrors, type UpdatePriorityApiV1AdminExecutionsExecutionIdPriorityPutResponse, type UpdatePriorityApiV1AdminExecutionsExecutionIdPriorityPutResponses, type UpdateSavedScriptApiV1ScriptsScriptIdPutData, type UpdateSavedScriptApiV1ScriptsScriptIdPutError, type UpdateSavedScriptApiV1ScriptsScriptIdPutErrors, type UpdateSavedScriptApiV1ScriptsScriptIdPutResponse, type UpdateSavedScriptApiV1ScriptsScriptIdPutResponses, type UpdateSubscriptionApiV1NotificationsSubscriptionsChannelPutData, type UpdateSubscriptionApiV1NotificationsSubscriptionsChannelPutError, type UpdateSubscriptionApiV1NotificationsSubscriptionsChannelPutErrors, type UpdateSubscriptionApiV1NotificationsSubscriptionsChannelPutResponse, type UpdateSubscriptionApiV1NotificationsSubscriptionsChannelPutResponses, type UpdateSystemSettingsApiV1AdminSettingsPutData, type UpdateSystemSettingsApiV1AdminSettingsPutError, type UpdateSystemSettingsApiV1AdminSettingsPutErrors, type UpdateSystemSettingsApiV1AdminSettingsPutResponse, type UpdateSystemSettingsApiV1AdminSettingsPutResponses, type UpdateThemeApiV1UserSettingsThemePutData, type UpdateThemeApiV1UserSettingsThemePutError, type UpdateThemeApiV1UserSettingsThemePutErrors, type UpdateThemeApiV1UserSettingsThemePutResponse, type UpdateThemeApiV1UserSettingsThemePutResponses, type UpdateUserApiV1AdminUsersUserIdPutData, type UpdateUserApiV1AdminUsersUserIdPutError, type UpdateUserApiV1AdminUsersUserIdPutErrors, type UpdateUserApiV1AdminUsersUserIdPutResponse, type UpdateUserApiV1AdminUsersUserIdPutResponses, type UpdateUserRateLimitsApiV1AdminRateLimitsUserIdPutData, type UpdateUserRateLimitsApiV1AdminRateLimitsUserIdPutError, type UpdateUserRateLimitsApiV1AdminRateLimitsUserIdPutErrors, type UpdateUserRateLimitsApiV1AdminRateLimitsUserIdPutResponse, type UpdateUserRateLimitsApiV1AdminRateLimitsUserIdPutResponses, type UpdateUserSettingsApiV1UserSettingsPutData, type UpdateUserSettingsApiV1UserSettingsPutError, type UpdateUserSettingsApiV1UserSettingsPutErrors, type UpdateUserSettingsApiV1UserSettingsPutResponse, type UpdateUserSettingsApiV1UserSettingsPutResponses, type UserCreate, type UserCreateWritable, type UserDeletedEvent, type UserEventCount, type UserListResponse, type UserLoggedInEvent, type UserLoggedOutEvent, type UserLoginEvent, type UserRateLimitConfigResponse, type UserRateLimitsResponse, type UserRegisteredEvent, type UserResponse, UserRole, type UserSettings, type UserSettingsUpdate, type UserSettingsUpdatedEvent, type UserUpdate, type UserUpdatedEvent, type UserUpdateWritable } from './types.gen'; +export { zAdminExecutionListResponse, zAdminExecutionResponse, zAdminUserOverview, zAuthFailedEvent, zBodyLoginApiV1AuthLoginPost, zBrowseEventsApiV1AdminEventsBrowsePostBody, zBrowseEventsApiV1AdminEventsBrowsePostResponse, zCancelExecutionApiV1ExecutionsExecutionIdCancelPostBody, zCancelExecutionApiV1ExecutionsExecutionIdCancelPostPath, zCancelExecutionApiV1ExecutionsExecutionIdCancelPostResponse, zCancelExecutionRequest, zCancelReplaySessionApiV1ReplaySessionsSessionIdCancelPostPath, zCancelReplaySessionApiV1ReplaySessionsSessionIdCancelPostResponse, zCancelResponse, zCancelStatus, zCleanupOldSessionsApiV1ReplayCleanupPostQuery, zCleanupOldSessionsApiV1ReplayCleanupPostResponse, zCleanupResponse, zContainerStatusInfo, zCreateExecutionApiV1ExecutePostBody, zCreateExecutionApiV1ExecutePostHeaders, zCreateExecutionApiV1ExecutePostResponse, zCreateReplaySessionApiV1ReplaySessionsPostBody, zCreateReplaySessionApiV1ReplaySessionsPostResponse, zCreateSavedScriptApiV1ScriptsPostBody, zCreateSavedScriptApiV1ScriptsPostResponse, zCreateUserApiV1AdminUsersPostBody, zCreateUserApiV1AdminUsersPostResponse, zDeleteEventApiV1AdminEventsEventIdDeletePath, zDeleteEventApiV1AdminEventsEventIdDeleteResponse, zDeleteExecutionApiV1ExecutionsExecutionIdDeletePath, zDeleteExecutionApiV1ExecutionsExecutionIdDeleteResponse, zDeleteNotificationApiV1NotificationsNotificationIdDeletePath, zDeleteNotificationApiV1NotificationsNotificationIdDeleteResponse, zDeleteNotificationResponse, zDeleteResponse, zDeleteSavedScriptApiV1ScriptsScriptIdDeletePath, zDeleteSavedScriptApiV1ScriptsScriptIdDeleteResponse, zDeleteUserApiV1AdminUsersUserIdDeletePath, zDeleteUserApiV1AdminUsersUserIdDeleteQuery, zDeleteUserApiV1AdminUsersUserIdDeleteResponse, zDeleteUserResponse, zDerivedCounts, zDlqMessageDiscardedEvent, zDlqMessageReceivedEvent, zDlqMessageRetriedEvent, zEditorSettingsInput, zEditorSettingsOutput, zEndpointGroup, zEndpointUsageStats, zEnvironment, zErrorResponse, zEventBrowseRequest, zEventBrowseResponse, zEventDeleteResponse, zEventDetailResponse, zEventFilter, zEventMetadata, zEventReplayRequest, zEventReplayResponse, zEventReplayStatusResponse, zEventReplayStatusResponseWritable, zEventStatistics, zEventStatsResponse, zEventSummary, zEventType, zEventTypeCount, zExampleScripts, zExecutionAcceptedEvent, zExecutionCancelledEvent, zExecutionCompletedEvent, zExecutionErrorType, zExecutionEventsApiV1EventsExecutionsExecutionIdGetPath, zExecutionEventsApiV1EventsExecutionsExecutionIdGetResponse, zExecutionFailedEvent, zExecutionListResponse, zExecutionQueuedEvent, zExecutionRequest, zExecutionRequestedEvent, zExecutionResponse, zExecutionResult, zExecutionRunningEvent, zExecutionStartedEvent, zExecutionStatus, zExecutionTimeoutEvent, zExportEventsApiV1AdminEventsExportExportFormatGetPath, zExportEventsApiV1AdminEventsExportExportFormatGetQuery, zExportFormat, zGetCurrentUserProfileApiV1AuthMeGetResponse, zGetDefaultRateLimitRulesApiV1AdminRateLimitsDefaultsGetResponse, zGetEventDetailApiV1AdminEventsEventIdGetPath, zGetEventDetailApiV1AdminEventsEventIdGetResponse, zGetEventStatsApiV1AdminEventsStatsGetQuery, zGetEventStatsApiV1AdminEventsStatsGetResponse, zGetExampleScriptsApiV1ExampleScriptsGetResponse, zGetExecutionEventsApiV1ExecutionsExecutionIdEventsGetPath, zGetExecutionEventsApiV1ExecutionsExecutionIdEventsGetQuery, zGetExecutionEventsApiV1ExecutionsExecutionIdEventsGetResponse, zGetK8sResourceLimitsApiV1K8sLimitsGetResponse, zGetNotificationsApiV1NotificationsGetQuery, zGetNotificationsApiV1NotificationsGetResponse, zGetQueueStatusApiV1AdminExecutionsQueueGetResponse, zGetReplaySessionApiV1ReplaySessionsSessionIdGetPath, zGetReplaySessionApiV1ReplaySessionsSessionIdGetResponse, zGetResultApiV1ExecutionsExecutionIdResultGetPath, zGetResultApiV1ExecutionsExecutionIdResultGetResponse, zGetSavedScriptApiV1ScriptsScriptIdGetPath, zGetSavedScriptApiV1ScriptsScriptIdGetResponse, zGetSettingsHistoryApiV1UserSettingsHistoryGetQuery, zGetSettingsHistoryApiV1UserSettingsHistoryGetResponse, zGetSubscriptionsApiV1NotificationsSubscriptionsGetResponse, zGetSystemSettingsApiV1AdminSettingsGetResponse, zGetUnreadCountApiV1NotificationsUnreadCountGetResponse, zGetUserApiV1AdminUsersUserIdGetPath, zGetUserApiV1AdminUsersUserIdGetResponse, zGetUserExecutionsApiV1UserExecutionsGetQuery, zGetUserExecutionsApiV1UserExecutionsGetResponse, zGetUserOverviewApiV1AdminUsersUserIdOverviewGetPath, zGetUserOverviewApiV1AdminUsersUserIdOverviewGetResponse, zGetUserRateLimitsApiV1AdminRateLimitsUserIdGetPath, zGetUserRateLimitsApiV1AdminRateLimitsUserIdGetResponse, zGetUserSettingsApiV1UserSettingsGetResponse, zHourlyEventCount, zLanguageInfo, zLanguageLimits, zListExecutionsApiV1AdminExecutionsGetQuery, zListExecutionsApiV1AdminExecutionsGetResponse, zListReplaySessionsApiV1ReplaySessionsGetQuery, zListReplaySessionsApiV1ReplaySessionsGetResponse, zListSavedScriptsApiV1ScriptsGetResponse, zListUsersApiV1AdminUsersGetQuery, zListUsersApiV1AdminUsersGetResponse, zLivenessApiV1HealthLiveGetResponse, zLivenessResponse, zLoginApiV1AuthLoginPostBody, zLoginApiV1AuthLoginPostResponse, zLoginMethod, zLoginResponse, zLogLevel, zLogoutApiV1AuthLogoutPostResponse, zMarkAllReadApiV1NotificationsMarkAllReadPostResponse, zMarkNotificationReadApiV1NotificationsNotificationIdReadPutPath, zMarkNotificationReadApiV1NotificationsNotificationIdReadPutResponse, zMessageResponse, zNotificationAllReadEvent, zNotificationChannel, zNotificationClickedEvent, zNotificationCreatedEvent, zNotificationDeliveredEvent, zNotificationFailedEvent, zNotificationListResponse, zNotificationPreferencesUpdatedEvent, zNotificationReadEvent, zNotificationResponse, zNotificationSentEvent, zNotificationSettingsInput, zNotificationSettingsOutput, zNotificationSeverity, zNotificationStatus, zNotificationStreamApiV1EventsNotificationsStreamGetResponse, zNotificationSubscription, zPasswordResetRequest, zPauseReplaySessionApiV1ReplaySessionsSessionIdPausePostPath, zPauseReplaySessionApiV1ReplaySessionsSessionIdPausePostResponse, zPodCreatedEvent, zPodDeletedEvent, zPodFailedEvent, zPodRunningEvent, zPodScheduledEvent, zPodSucceededEvent, zPodTerminatedEvent, zPriorityUpdateRequest, zQueuePriority, zQueueStatusResponse, zQuotaExceededEvent, zRateLimitAlgorithm, zRateLimitExceededEvent, zRateLimitRuleRequest, zRateLimitRuleResponse, zRateLimitSummary, zRateLimitUpdateRequest, zRateLimitUpdateResponse, zRegisterApiV1AuthRegisterPostBody, zRegisterApiV1AuthRegisterPostResponse, zReplayConfigSchema, zReplayError, zReplayEventsApiV1AdminEventsReplayPostBody, zReplayEventsApiV1AdminEventsReplayPostResponse, zReplayFilter, zReplayFilterSchema, zReplayRequest, zReplayResponse, zReplaySession, zReplayStatus, zReplayTarget, zReplayType, zResetSystemSettingsApiV1AdminSettingsResetPostResponse, zResetUserPasswordApiV1AdminUsersUserIdResetPasswordPostBody, zResetUserPasswordApiV1AdminUsersUserIdResetPasswordPostPath, zResetUserPasswordApiV1AdminUsersUserIdResetPasswordPostResponse, zResetUserRateLimitsApiV1AdminRateLimitsUserIdResetPostPath, zResetUserRateLimitsApiV1AdminRateLimitsUserIdResetPostResponse, zResourceLimitExceededEvent, zResourceLimits, zResourceUsage, zResourceUsageDomain, zRestoreSettingsApiV1UserSettingsRestorePostBody, zRestoreSettingsApiV1UserSettingsRestorePostResponse, zRestoreSettingsRequest, zResultFailedEvent, zResultStoredEvent, zResumeReplaySessionApiV1ReplaySessionsSessionIdResumePostPath, zResumeReplaySessionApiV1ReplaySessionsSessionIdResumePostResponse, zRetryExecutionApiV1ExecutionsExecutionIdRetryPostPath, zRetryExecutionApiV1ExecutionsExecutionIdRetryPostResponse, zSavedScriptCreateRequest, zSavedScriptListResponse, zSavedScriptResponse, zSavedScriptUpdate, zScriptDeletedEvent, zScriptSavedEvent, zScriptSharedEvent, zSecurityViolationEvent, zServiceEventCount, zServiceRecoveredEvent, zServiceUnhealthyEvent, zSessionConfigSummary, zSessionSummary, zSessionSummaryWritable, zSettingsHistoryEntry, zSettingsHistoryResponse, zSseControlEvent, zSseExecutionEventSchema, zStartReplaySessionApiV1ReplaySessionsSessionIdStartPostPath, zStartReplaySessionApiV1ReplaySessionsSessionIdStartPostResponse, zStorageType, zStreamReplayStatusApiV1AdminEventsReplaySessionIdStatusGetPath, zStreamReplayStatusApiV1AdminEventsReplaySessionIdStatusGetResponse, zSubscriptionsResponse, zSubscriptionUpdate, zSystemErrorEvent, zSystemSettingsSchemaInput, zSystemSettingsSchemaOutput, zTheme, zThemeUpdateRequest, zUnlockResponse, zUnlockUserApiV1AdminUsersUserIdUnlockPostPath, zUnlockUserApiV1AdminUsersUserIdUnlockPostResponse, zUnreadCountResponse, zUpdateCustomSettingApiV1UserSettingsCustomKeyPutBody, zUpdateCustomSettingApiV1UserSettingsCustomKeyPutPath, zUpdateCustomSettingApiV1UserSettingsCustomKeyPutResponse, zUpdateEditorSettingsApiV1UserSettingsEditorPutBody, zUpdateEditorSettingsApiV1UserSettingsEditorPutResponse, zUpdateNotificationSettingsApiV1UserSettingsNotificationsPutBody, zUpdateNotificationSettingsApiV1UserSettingsNotificationsPutResponse, zUpdatePriorityApiV1AdminExecutionsExecutionIdPriorityPutBody, zUpdatePriorityApiV1AdminExecutionsExecutionIdPriorityPutPath, zUpdatePriorityApiV1AdminExecutionsExecutionIdPriorityPutResponse, zUpdateSavedScriptApiV1ScriptsScriptIdPutBody, zUpdateSavedScriptApiV1ScriptsScriptIdPutPath, zUpdateSavedScriptApiV1ScriptsScriptIdPutResponse, zUpdateSubscriptionApiV1NotificationsSubscriptionsChannelPutBody, zUpdateSubscriptionApiV1NotificationsSubscriptionsChannelPutPath, zUpdateSubscriptionApiV1NotificationsSubscriptionsChannelPutResponse, zUpdateSystemSettingsApiV1AdminSettingsPutBody, zUpdateSystemSettingsApiV1AdminSettingsPutResponse, zUpdateThemeApiV1UserSettingsThemePutBody, zUpdateThemeApiV1UserSettingsThemePutResponse, zUpdateUserApiV1AdminUsersUserIdPutBody, zUpdateUserApiV1AdminUsersUserIdPutPath, zUpdateUserApiV1AdminUsersUserIdPutResponse, zUpdateUserRateLimitsApiV1AdminRateLimitsUserIdPutBody, zUpdateUserRateLimitsApiV1AdminRateLimitsUserIdPutPath, zUpdateUserRateLimitsApiV1AdminRateLimitsUserIdPutResponse, zUpdateUserSettingsApiV1UserSettingsPutBody, zUpdateUserSettingsApiV1UserSettingsPutResponse, zUserCreate, zUserCreateWritable, zUserDeletedEvent, zUserEventCount, zUserListResponse, zUserLoggedInEvent, zUserLoggedOutEvent, zUserLoginEvent, zUserRateLimitConfigResponse, zUserRateLimitsResponse, zUserRegisteredEvent, zUserResponse, zUserRole, zUserSettings, zUserSettingsUpdate, zUserSettingsUpdatedEvent, zUserUpdate, zUserUpdatedEvent, zUserUpdateWritable } from './zod.gen'; diff --git a/frontend/src/lib/api/sdk.gen.ts b/frontend/src/lib/api/sdk.gen.ts index 716975460..3cccbaaad 100644 --- a/frontend/src/lib/api/sdk.gen.ts +++ b/frontend/src/lib/api/sdk.gen.ts @@ -2,7 +2,7 @@ import { type Client, type ClientMeta, type Options as Options2, type RequestResult, type ServerSentEventsResult, type TDataShape, urlSearchParamsBodySerializer } from './client'; import { client } from './client.gen'; -import type { BrowseEventsApiV1AdminEventsBrowsePostData, BrowseEventsApiV1AdminEventsBrowsePostErrors, BrowseEventsApiV1AdminEventsBrowsePostResponses, CancelExecutionApiV1ExecutionsExecutionIdCancelPostData, CancelExecutionApiV1ExecutionsExecutionIdCancelPostErrors, CancelExecutionApiV1ExecutionsExecutionIdCancelPostResponses, CancelReplaySessionApiV1ReplaySessionsSessionIdCancelPostData, CancelReplaySessionApiV1ReplaySessionsSessionIdCancelPostErrors, CancelReplaySessionApiV1ReplaySessionsSessionIdCancelPostResponses, CancelSagaApiV1SagasSagaIdCancelPostData, CancelSagaApiV1SagasSagaIdCancelPostErrors, CancelSagaApiV1SagasSagaIdCancelPostResponses, CleanupOldSessionsApiV1ReplayCleanupPostData, CleanupOldSessionsApiV1ReplayCleanupPostErrors, CleanupOldSessionsApiV1ReplayCleanupPostResponses, CreateExecutionApiV1ExecutePostData, CreateExecutionApiV1ExecutePostErrors, CreateExecutionApiV1ExecutePostResponses, CreateReplaySessionApiV1ReplaySessionsPostData, CreateReplaySessionApiV1ReplaySessionsPostErrors, CreateReplaySessionApiV1ReplaySessionsPostResponses, CreateSavedScriptApiV1ScriptsPostData, CreateSavedScriptApiV1ScriptsPostErrors, CreateSavedScriptApiV1ScriptsPostResponses, CreateUserApiV1AdminUsersPostData, CreateUserApiV1AdminUsersPostErrors, CreateUserApiV1AdminUsersPostResponses, DeleteEventApiV1AdminEventsEventIdDeleteData, DeleteEventApiV1AdminEventsEventIdDeleteErrors, DeleteEventApiV1AdminEventsEventIdDeleteResponses, DeleteExecutionApiV1ExecutionsExecutionIdDeleteData, DeleteExecutionApiV1ExecutionsExecutionIdDeleteErrors, DeleteExecutionApiV1ExecutionsExecutionIdDeleteResponses, DeleteNotificationApiV1NotificationsNotificationIdDeleteData, DeleteNotificationApiV1NotificationsNotificationIdDeleteErrors, DeleteNotificationApiV1NotificationsNotificationIdDeleteResponses, DeleteSavedScriptApiV1ScriptsScriptIdDeleteData, DeleteSavedScriptApiV1ScriptsScriptIdDeleteErrors, DeleteSavedScriptApiV1ScriptsScriptIdDeleteResponses, DeleteUserApiV1AdminUsersUserIdDeleteData, DeleteUserApiV1AdminUsersUserIdDeleteErrors, DeleteUserApiV1AdminUsersUserIdDeleteResponses, ExecutionEventsApiV1EventsExecutionsExecutionIdGetData, ExecutionEventsApiV1EventsExecutionsExecutionIdGetErrors, ExecutionEventsApiV1EventsExecutionsExecutionIdGetResponse, ExecutionEventsApiV1EventsExecutionsExecutionIdGetResponses, ExportEventsApiV1AdminEventsExportExportFormatGetData, ExportEventsApiV1AdminEventsExportExportFormatGetErrors, ExportEventsApiV1AdminEventsExportExportFormatGetResponses, GetCurrentUserProfileApiV1AuthMeGetData, GetCurrentUserProfileApiV1AuthMeGetErrors, GetCurrentUserProfileApiV1AuthMeGetResponses, GetDefaultRateLimitRulesApiV1AdminRateLimitsDefaultsGetData, GetDefaultRateLimitRulesApiV1AdminRateLimitsDefaultsGetErrors, GetDefaultRateLimitRulesApiV1AdminRateLimitsDefaultsGetResponses, GetEventDetailApiV1AdminEventsEventIdGetData, GetEventDetailApiV1AdminEventsEventIdGetErrors, GetEventDetailApiV1AdminEventsEventIdGetResponses, GetEventStatsApiV1AdminEventsStatsGetData, GetEventStatsApiV1AdminEventsStatsGetErrors, GetEventStatsApiV1AdminEventsStatsGetResponses, GetExampleScriptsApiV1ExampleScriptsGetData, GetExampleScriptsApiV1ExampleScriptsGetErrors, GetExampleScriptsApiV1ExampleScriptsGetResponses, GetExecutionEventsApiV1ExecutionsExecutionIdEventsGetData, GetExecutionEventsApiV1ExecutionsExecutionIdEventsGetErrors, GetExecutionEventsApiV1ExecutionsExecutionIdEventsGetResponses, GetExecutionSagasApiV1SagasExecutionExecutionIdGetData, GetExecutionSagasApiV1SagasExecutionExecutionIdGetErrors, GetExecutionSagasApiV1SagasExecutionExecutionIdGetResponses, GetK8sResourceLimitsApiV1K8sLimitsGetData, GetK8sResourceLimitsApiV1K8sLimitsGetErrors, GetK8sResourceLimitsApiV1K8sLimitsGetResponses, GetNotificationsApiV1NotificationsGetData, GetNotificationsApiV1NotificationsGetErrors, GetNotificationsApiV1NotificationsGetResponses, GetQueueStatusApiV1AdminExecutionsQueueGetData, GetQueueStatusApiV1AdminExecutionsQueueGetErrors, GetQueueStatusApiV1AdminExecutionsQueueGetResponses, GetReplaySessionApiV1ReplaySessionsSessionIdGetData, GetReplaySessionApiV1ReplaySessionsSessionIdGetErrors, GetReplaySessionApiV1ReplaySessionsSessionIdGetResponses, GetResultApiV1ExecutionsExecutionIdResultGetData, GetResultApiV1ExecutionsExecutionIdResultGetErrors, GetResultApiV1ExecutionsExecutionIdResultGetResponses, GetSagaStatusApiV1SagasSagaIdGetData, GetSagaStatusApiV1SagasSagaIdGetErrors, GetSagaStatusApiV1SagasSagaIdGetResponses, GetSavedScriptApiV1ScriptsScriptIdGetData, GetSavedScriptApiV1ScriptsScriptIdGetErrors, GetSavedScriptApiV1ScriptsScriptIdGetResponses, GetSettingsHistoryApiV1UserSettingsHistoryGetData, GetSettingsHistoryApiV1UserSettingsHistoryGetErrors, GetSettingsHistoryApiV1UserSettingsHistoryGetResponses, GetSubscriptionsApiV1NotificationsSubscriptionsGetData, GetSubscriptionsApiV1NotificationsSubscriptionsGetErrors, GetSubscriptionsApiV1NotificationsSubscriptionsGetResponses, GetSystemSettingsApiV1AdminSettingsGetData, GetSystemSettingsApiV1AdminSettingsGetErrors, GetSystemSettingsApiV1AdminSettingsGetResponses, GetUnreadCountApiV1NotificationsUnreadCountGetData, GetUnreadCountApiV1NotificationsUnreadCountGetErrors, GetUnreadCountApiV1NotificationsUnreadCountGetResponses, GetUserApiV1AdminUsersUserIdGetData, GetUserApiV1AdminUsersUserIdGetErrors, GetUserApiV1AdminUsersUserIdGetResponses, GetUserExecutionsApiV1UserExecutionsGetData, GetUserExecutionsApiV1UserExecutionsGetErrors, GetUserExecutionsApiV1UserExecutionsGetResponses, GetUserOverviewApiV1AdminUsersUserIdOverviewGetData, GetUserOverviewApiV1AdminUsersUserIdOverviewGetErrors, GetUserOverviewApiV1AdminUsersUserIdOverviewGetResponses, GetUserRateLimitsApiV1AdminRateLimitsUserIdGetData, GetUserRateLimitsApiV1AdminRateLimitsUserIdGetErrors, GetUserRateLimitsApiV1AdminRateLimitsUserIdGetResponses, GetUserSettingsApiV1UserSettingsGetData, GetUserSettingsApiV1UserSettingsGetErrors, GetUserSettingsApiV1UserSettingsGetResponses, ListExecutionsApiV1AdminExecutionsGetData, ListExecutionsApiV1AdminExecutionsGetErrors, ListExecutionsApiV1AdminExecutionsGetResponses, ListReplaySessionsApiV1ReplaySessionsGetData, ListReplaySessionsApiV1ReplaySessionsGetErrors, ListReplaySessionsApiV1ReplaySessionsGetResponses, ListSagasApiV1SagasGetData, ListSagasApiV1SagasGetErrors, ListSagasApiV1SagasGetResponses, ListSavedScriptsApiV1ScriptsGetData, ListSavedScriptsApiV1ScriptsGetErrors, ListSavedScriptsApiV1ScriptsGetResponses, ListUsersApiV1AdminUsersGetData, ListUsersApiV1AdminUsersGetErrors, ListUsersApiV1AdminUsersGetResponses, LivenessApiV1HealthLiveGetData, LivenessApiV1HealthLiveGetErrors, LivenessApiV1HealthLiveGetResponses, LoginApiV1AuthLoginPostData, LoginApiV1AuthLoginPostErrors, LoginApiV1AuthLoginPostResponses, LogoutApiV1AuthLogoutPostData, LogoutApiV1AuthLogoutPostErrors, LogoutApiV1AuthLogoutPostResponses, MarkAllReadApiV1NotificationsMarkAllReadPostData, MarkAllReadApiV1NotificationsMarkAllReadPostErrors, MarkAllReadApiV1NotificationsMarkAllReadPostResponses, MarkNotificationReadApiV1NotificationsNotificationIdReadPutData, MarkNotificationReadApiV1NotificationsNotificationIdReadPutErrors, MarkNotificationReadApiV1NotificationsNotificationIdReadPutResponses, NotificationStreamApiV1EventsNotificationsStreamGetData, NotificationStreamApiV1EventsNotificationsStreamGetErrors, NotificationStreamApiV1EventsNotificationsStreamGetResponse, NotificationStreamApiV1EventsNotificationsStreamGetResponses, PauseReplaySessionApiV1ReplaySessionsSessionIdPausePostData, PauseReplaySessionApiV1ReplaySessionsSessionIdPausePostErrors, PauseReplaySessionApiV1ReplaySessionsSessionIdPausePostResponses, RegisterApiV1AuthRegisterPostData, RegisterApiV1AuthRegisterPostErrors, RegisterApiV1AuthRegisterPostResponses, ReplayEventsApiV1AdminEventsReplayPostData, ReplayEventsApiV1AdminEventsReplayPostErrors, ReplayEventsApiV1AdminEventsReplayPostResponses, ResetSystemSettingsApiV1AdminSettingsResetPostData, ResetSystemSettingsApiV1AdminSettingsResetPostErrors, ResetSystemSettingsApiV1AdminSettingsResetPostResponses, ResetUserPasswordApiV1AdminUsersUserIdResetPasswordPostData, ResetUserPasswordApiV1AdminUsersUserIdResetPasswordPostErrors, ResetUserPasswordApiV1AdminUsersUserIdResetPasswordPostResponses, ResetUserRateLimitsApiV1AdminRateLimitsUserIdResetPostData, ResetUserRateLimitsApiV1AdminRateLimitsUserIdResetPostErrors, ResetUserRateLimitsApiV1AdminRateLimitsUserIdResetPostResponses, RestoreSettingsApiV1UserSettingsRestorePostData, RestoreSettingsApiV1UserSettingsRestorePostErrors, RestoreSettingsApiV1UserSettingsRestorePostResponses, ResumeReplaySessionApiV1ReplaySessionsSessionIdResumePostData, ResumeReplaySessionApiV1ReplaySessionsSessionIdResumePostErrors, ResumeReplaySessionApiV1ReplaySessionsSessionIdResumePostResponses, RetryExecutionApiV1ExecutionsExecutionIdRetryPostData, RetryExecutionApiV1ExecutionsExecutionIdRetryPostErrors, RetryExecutionApiV1ExecutionsExecutionIdRetryPostResponses, StartReplaySessionApiV1ReplaySessionsSessionIdStartPostData, StartReplaySessionApiV1ReplaySessionsSessionIdStartPostErrors, StartReplaySessionApiV1ReplaySessionsSessionIdStartPostResponses, StreamReplayStatusApiV1AdminEventsReplaySessionIdStatusGetData, StreamReplayStatusApiV1AdminEventsReplaySessionIdStatusGetErrors, StreamReplayStatusApiV1AdminEventsReplaySessionIdStatusGetResponse, StreamReplayStatusApiV1AdminEventsReplaySessionIdStatusGetResponses, UnlockUserApiV1AdminUsersUserIdUnlockPostData, UnlockUserApiV1AdminUsersUserIdUnlockPostErrors, UnlockUserApiV1AdminUsersUserIdUnlockPostResponses, UpdateCustomSettingApiV1UserSettingsCustomKeyPutData, UpdateCustomSettingApiV1UserSettingsCustomKeyPutErrors, UpdateCustomSettingApiV1UserSettingsCustomKeyPutResponses, UpdateEditorSettingsApiV1UserSettingsEditorPutData, UpdateEditorSettingsApiV1UserSettingsEditorPutErrors, UpdateEditorSettingsApiV1UserSettingsEditorPutResponses, UpdateNotificationSettingsApiV1UserSettingsNotificationsPutData, UpdateNotificationSettingsApiV1UserSettingsNotificationsPutErrors, UpdateNotificationSettingsApiV1UserSettingsNotificationsPutResponses, UpdatePriorityApiV1AdminExecutionsExecutionIdPriorityPutData, UpdatePriorityApiV1AdminExecutionsExecutionIdPriorityPutErrors, UpdatePriorityApiV1AdminExecutionsExecutionIdPriorityPutResponses, UpdateSavedScriptApiV1ScriptsScriptIdPutData, UpdateSavedScriptApiV1ScriptsScriptIdPutErrors, UpdateSavedScriptApiV1ScriptsScriptIdPutResponses, UpdateSubscriptionApiV1NotificationsSubscriptionsChannelPutData, UpdateSubscriptionApiV1NotificationsSubscriptionsChannelPutErrors, UpdateSubscriptionApiV1NotificationsSubscriptionsChannelPutResponses, UpdateSystemSettingsApiV1AdminSettingsPutData, UpdateSystemSettingsApiV1AdminSettingsPutErrors, UpdateSystemSettingsApiV1AdminSettingsPutResponses, UpdateThemeApiV1UserSettingsThemePutData, UpdateThemeApiV1UserSettingsThemePutErrors, UpdateThemeApiV1UserSettingsThemePutResponses, UpdateUserApiV1AdminUsersUserIdPutData, UpdateUserApiV1AdminUsersUserIdPutErrors, UpdateUserApiV1AdminUsersUserIdPutResponses, UpdateUserRateLimitsApiV1AdminRateLimitsUserIdPutData, UpdateUserRateLimitsApiV1AdminRateLimitsUserIdPutErrors, UpdateUserRateLimitsApiV1AdminRateLimitsUserIdPutResponses, UpdateUserSettingsApiV1UserSettingsPutData, UpdateUserSettingsApiV1UserSettingsPutErrors, UpdateUserSettingsApiV1UserSettingsPutResponses } from './types.gen'; +import type { BrowseEventsApiV1AdminEventsBrowsePostData, BrowseEventsApiV1AdminEventsBrowsePostErrors, BrowseEventsApiV1AdminEventsBrowsePostResponses, CancelExecutionApiV1ExecutionsExecutionIdCancelPostData, CancelExecutionApiV1ExecutionsExecutionIdCancelPostErrors, CancelExecutionApiV1ExecutionsExecutionIdCancelPostResponses, CancelReplaySessionApiV1ReplaySessionsSessionIdCancelPostData, CancelReplaySessionApiV1ReplaySessionsSessionIdCancelPostErrors, CancelReplaySessionApiV1ReplaySessionsSessionIdCancelPostResponses, CleanupOldSessionsApiV1ReplayCleanupPostData, CleanupOldSessionsApiV1ReplayCleanupPostErrors, CleanupOldSessionsApiV1ReplayCleanupPostResponses, CreateExecutionApiV1ExecutePostData, CreateExecutionApiV1ExecutePostErrors, CreateExecutionApiV1ExecutePostResponses, CreateReplaySessionApiV1ReplaySessionsPostData, CreateReplaySessionApiV1ReplaySessionsPostErrors, CreateReplaySessionApiV1ReplaySessionsPostResponses, CreateSavedScriptApiV1ScriptsPostData, CreateSavedScriptApiV1ScriptsPostErrors, CreateSavedScriptApiV1ScriptsPostResponses, CreateUserApiV1AdminUsersPostData, CreateUserApiV1AdminUsersPostErrors, CreateUserApiV1AdminUsersPostResponses, DeleteEventApiV1AdminEventsEventIdDeleteData, DeleteEventApiV1AdminEventsEventIdDeleteErrors, DeleteEventApiV1AdminEventsEventIdDeleteResponses, DeleteExecutionApiV1ExecutionsExecutionIdDeleteData, DeleteExecutionApiV1ExecutionsExecutionIdDeleteErrors, DeleteExecutionApiV1ExecutionsExecutionIdDeleteResponses, DeleteNotificationApiV1NotificationsNotificationIdDeleteData, DeleteNotificationApiV1NotificationsNotificationIdDeleteErrors, DeleteNotificationApiV1NotificationsNotificationIdDeleteResponses, DeleteSavedScriptApiV1ScriptsScriptIdDeleteData, DeleteSavedScriptApiV1ScriptsScriptIdDeleteErrors, DeleteSavedScriptApiV1ScriptsScriptIdDeleteResponses, DeleteUserApiV1AdminUsersUserIdDeleteData, DeleteUserApiV1AdminUsersUserIdDeleteErrors, DeleteUserApiV1AdminUsersUserIdDeleteResponses, ExecutionEventsApiV1EventsExecutionsExecutionIdGetData, ExecutionEventsApiV1EventsExecutionsExecutionIdGetErrors, ExecutionEventsApiV1EventsExecutionsExecutionIdGetResponse, ExecutionEventsApiV1EventsExecutionsExecutionIdGetResponses, ExportEventsApiV1AdminEventsExportExportFormatGetData, ExportEventsApiV1AdminEventsExportExportFormatGetErrors, ExportEventsApiV1AdminEventsExportExportFormatGetResponses, GetCurrentUserProfileApiV1AuthMeGetData, GetCurrentUserProfileApiV1AuthMeGetErrors, GetCurrentUserProfileApiV1AuthMeGetResponses, GetDefaultRateLimitRulesApiV1AdminRateLimitsDefaultsGetData, GetDefaultRateLimitRulesApiV1AdminRateLimitsDefaultsGetErrors, GetDefaultRateLimitRulesApiV1AdminRateLimitsDefaultsGetResponses, GetEventDetailApiV1AdminEventsEventIdGetData, GetEventDetailApiV1AdminEventsEventIdGetErrors, GetEventDetailApiV1AdminEventsEventIdGetResponses, GetEventStatsApiV1AdminEventsStatsGetData, GetEventStatsApiV1AdminEventsStatsGetErrors, GetEventStatsApiV1AdminEventsStatsGetResponses, GetExampleScriptsApiV1ExampleScriptsGetData, GetExampleScriptsApiV1ExampleScriptsGetErrors, GetExampleScriptsApiV1ExampleScriptsGetResponses, GetExecutionEventsApiV1ExecutionsExecutionIdEventsGetData, GetExecutionEventsApiV1ExecutionsExecutionIdEventsGetErrors, GetExecutionEventsApiV1ExecutionsExecutionIdEventsGetResponses, GetK8sResourceLimitsApiV1K8sLimitsGetData, GetK8sResourceLimitsApiV1K8sLimitsGetErrors, GetK8sResourceLimitsApiV1K8sLimitsGetResponses, GetNotificationsApiV1NotificationsGetData, GetNotificationsApiV1NotificationsGetErrors, GetNotificationsApiV1NotificationsGetResponses, GetQueueStatusApiV1AdminExecutionsQueueGetData, GetQueueStatusApiV1AdminExecutionsQueueGetErrors, GetQueueStatusApiV1AdminExecutionsQueueGetResponses, GetReplaySessionApiV1ReplaySessionsSessionIdGetData, GetReplaySessionApiV1ReplaySessionsSessionIdGetErrors, GetReplaySessionApiV1ReplaySessionsSessionIdGetResponses, GetResultApiV1ExecutionsExecutionIdResultGetData, GetResultApiV1ExecutionsExecutionIdResultGetErrors, GetResultApiV1ExecutionsExecutionIdResultGetResponses, GetSavedScriptApiV1ScriptsScriptIdGetData, GetSavedScriptApiV1ScriptsScriptIdGetErrors, GetSavedScriptApiV1ScriptsScriptIdGetResponses, GetSettingsHistoryApiV1UserSettingsHistoryGetData, GetSettingsHistoryApiV1UserSettingsHistoryGetErrors, GetSettingsHistoryApiV1UserSettingsHistoryGetResponses, GetSubscriptionsApiV1NotificationsSubscriptionsGetData, GetSubscriptionsApiV1NotificationsSubscriptionsGetErrors, GetSubscriptionsApiV1NotificationsSubscriptionsGetResponses, GetSystemSettingsApiV1AdminSettingsGetData, GetSystemSettingsApiV1AdminSettingsGetErrors, GetSystemSettingsApiV1AdminSettingsGetResponses, GetUnreadCountApiV1NotificationsUnreadCountGetData, GetUnreadCountApiV1NotificationsUnreadCountGetErrors, GetUnreadCountApiV1NotificationsUnreadCountGetResponses, GetUserApiV1AdminUsersUserIdGetData, GetUserApiV1AdminUsersUserIdGetErrors, GetUserApiV1AdminUsersUserIdGetResponses, GetUserExecutionsApiV1UserExecutionsGetData, GetUserExecutionsApiV1UserExecutionsGetErrors, GetUserExecutionsApiV1UserExecutionsGetResponses, GetUserOverviewApiV1AdminUsersUserIdOverviewGetData, GetUserOverviewApiV1AdminUsersUserIdOverviewGetErrors, GetUserOverviewApiV1AdminUsersUserIdOverviewGetResponses, GetUserRateLimitsApiV1AdminRateLimitsUserIdGetData, GetUserRateLimitsApiV1AdminRateLimitsUserIdGetErrors, GetUserRateLimitsApiV1AdminRateLimitsUserIdGetResponses, GetUserSettingsApiV1UserSettingsGetData, GetUserSettingsApiV1UserSettingsGetErrors, GetUserSettingsApiV1UserSettingsGetResponses, ListExecutionsApiV1AdminExecutionsGetData, ListExecutionsApiV1AdminExecutionsGetErrors, ListExecutionsApiV1AdminExecutionsGetResponses, ListReplaySessionsApiV1ReplaySessionsGetData, ListReplaySessionsApiV1ReplaySessionsGetErrors, ListReplaySessionsApiV1ReplaySessionsGetResponses, ListSavedScriptsApiV1ScriptsGetData, ListSavedScriptsApiV1ScriptsGetErrors, ListSavedScriptsApiV1ScriptsGetResponses, ListUsersApiV1AdminUsersGetData, ListUsersApiV1AdminUsersGetErrors, ListUsersApiV1AdminUsersGetResponses, LivenessApiV1HealthLiveGetData, LivenessApiV1HealthLiveGetErrors, LivenessApiV1HealthLiveGetResponses, LoginApiV1AuthLoginPostData, LoginApiV1AuthLoginPostErrors, LoginApiV1AuthLoginPostResponses, LogoutApiV1AuthLogoutPostData, LogoutApiV1AuthLogoutPostErrors, LogoutApiV1AuthLogoutPostResponses, MarkAllReadApiV1NotificationsMarkAllReadPostData, MarkAllReadApiV1NotificationsMarkAllReadPostErrors, MarkAllReadApiV1NotificationsMarkAllReadPostResponses, MarkNotificationReadApiV1NotificationsNotificationIdReadPutData, MarkNotificationReadApiV1NotificationsNotificationIdReadPutErrors, MarkNotificationReadApiV1NotificationsNotificationIdReadPutResponses, NotificationStreamApiV1EventsNotificationsStreamGetData, NotificationStreamApiV1EventsNotificationsStreamGetErrors, NotificationStreamApiV1EventsNotificationsStreamGetResponse, NotificationStreamApiV1EventsNotificationsStreamGetResponses, PauseReplaySessionApiV1ReplaySessionsSessionIdPausePostData, PauseReplaySessionApiV1ReplaySessionsSessionIdPausePostErrors, PauseReplaySessionApiV1ReplaySessionsSessionIdPausePostResponses, RegisterApiV1AuthRegisterPostData, RegisterApiV1AuthRegisterPostErrors, RegisterApiV1AuthRegisterPostResponses, ReplayEventsApiV1AdminEventsReplayPostData, ReplayEventsApiV1AdminEventsReplayPostErrors, ReplayEventsApiV1AdminEventsReplayPostResponses, ResetSystemSettingsApiV1AdminSettingsResetPostData, ResetSystemSettingsApiV1AdminSettingsResetPostErrors, ResetSystemSettingsApiV1AdminSettingsResetPostResponses, ResetUserPasswordApiV1AdminUsersUserIdResetPasswordPostData, ResetUserPasswordApiV1AdminUsersUserIdResetPasswordPostErrors, ResetUserPasswordApiV1AdminUsersUserIdResetPasswordPostResponses, ResetUserRateLimitsApiV1AdminRateLimitsUserIdResetPostData, ResetUserRateLimitsApiV1AdminRateLimitsUserIdResetPostErrors, ResetUserRateLimitsApiV1AdminRateLimitsUserIdResetPostResponses, RestoreSettingsApiV1UserSettingsRestorePostData, RestoreSettingsApiV1UserSettingsRestorePostErrors, RestoreSettingsApiV1UserSettingsRestorePostResponses, ResumeReplaySessionApiV1ReplaySessionsSessionIdResumePostData, ResumeReplaySessionApiV1ReplaySessionsSessionIdResumePostErrors, ResumeReplaySessionApiV1ReplaySessionsSessionIdResumePostResponses, RetryExecutionApiV1ExecutionsExecutionIdRetryPostData, RetryExecutionApiV1ExecutionsExecutionIdRetryPostErrors, RetryExecutionApiV1ExecutionsExecutionIdRetryPostResponses, StartReplaySessionApiV1ReplaySessionsSessionIdStartPostData, StartReplaySessionApiV1ReplaySessionsSessionIdStartPostErrors, StartReplaySessionApiV1ReplaySessionsSessionIdStartPostResponses, StreamReplayStatusApiV1AdminEventsReplaySessionIdStatusGetData, StreamReplayStatusApiV1AdminEventsReplaySessionIdStatusGetErrors, StreamReplayStatusApiV1AdminEventsReplaySessionIdStatusGetResponse, StreamReplayStatusApiV1AdminEventsReplaySessionIdStatusGetResponses, UnlockUserApiV1AdminUsersUserIdUnlockPostData, UnlockUserApiV1AdminUsersUserIdUnlockPostErrors, UnlockUserApiV1AdminUsersUserIdUnlockPostResponses, UpdateCustomSettingApiV1UserSettingsCustomKeyPutData, UpdateCustomSettingApiV1UserSettingsCustomKeyPutErrors, UpdateCustomSettingApiV1UserSettingsCustomKeyPutResponses, UpdateEditorSettingsApiV1UserSettingsEditorPutData, UpdateEditorSettingsApiV1UserSettingsEditorPutErrors, UpdateEditorSettingsApiV1UserSettingsEditorPutResponses, UpdateNotificationSettingsApiV1UserSettingsNotificationsPutData, UpdateNotificationSettingsApiV1UserSettingsNotificationsPutErrors, UpdateNotificationSettingsApiV1UserSettingsNotificationsPutResponses, UpdatePriorityApiV1AdminExecutionsExecutionIdPriorityPutData, UpdatePriorityApiV1AdminExecutionsExecutionIdPriorityPutErrors, UpdatePriorityApiV1AdminExecutionsExecutionIdPriorityPutResponses, UpdateSavedScriptApiV1ScriptsScriptIdPutData, UpdateSavedScriptApiV1ScriptsScriptIdPutErrors, UpdateSavedScriptApiV1ScriptsScriptIdPutResponses, UpdateSubscriptionApiV1NotificationsSubscriptionsChannelPutData, UpdateSubscriptionApiV1NotificationsSubscriptionsChannelPutErrors, UpdateSubscriptionApiV1NotificationsSubscriptionsChannelPutResponses, UpdateSystemSettingsApiV1AdminSettingsPutData, UpdateSystemSettingsApiV1AdminSettingsPutErrors, UpdateSystemSettingsApiV1AdminSettingsPutResponses, UpdateThemeApiV1UserSettingsThemePutData, UpdateThemeApiV1UserSettingsThemePutErrors, UpdateThemeApiV1UserSettingsThemePutResponses, UpdateUserApiV1AdminUsersUserIdPutData, UpdateUserApiV1AdminUsersUserIdPutErrors, UpdateUserApiV1AdminUsersUserIdPutResponses, UpdateUserRateLimitsApiV1AdminRateLimitsUserIdPutData, UpdateUserRateLimitsApiV1AdminRateLimitsUserIdPutErrors, UpdateUserRateLimitsApiV1AdminRateLimitsUserIdPutResponses, UpdateUserSettingsApiV1UserSettingsPutData, UpdateUserSettingsApiV1UserSettingsPutErrors, UpdateUserSettingsApiV1UserSettingsPutResponses } from './types.gen'; export type Options = Options2 & { /** @@ -649,31 +649,3 @@ export const getUnreadCountApiV1NotificationsUnreadCountGet = (options: Options): RequestResult => (options.client ?? client).delete({ url: '/api/v1/notifications/{notification_id}', ...options }); - -/** - * Get Saga Status - * - * Get saga status by ID. - */ -export const getSagaStatusApiV1SagasSagaIdGet = (options: Options): RequestResult => (options.client ?? client).get({ url: '/api/v1/sagas/{saga_id}', ...options }); - -/** - * Get Execution Sagas - * - * Get all sagas for an execution. - */ -export const getExecutionSagasApiV1SagasExecutionExecutionIdGet = (options: Options): RequestResult => (options.client ?? client).get({ url: '/api/v1/sagas/execution/{execution_id}', ...options }); - -/** - * List Sagas - * - * List sagas accessible by the current user. - */ -export const listSagasApiV1SagasGet = (options?: Options): RequestResult => (options?.client ?? client).get({ url: '/api/v1/sagas/', ...options }); - -/** - * Cancel Saga - * - * Cancel a running saga. - */ -export const cancelSagaApiV1SagasSagaIdCancelPost = (options: Options): RequestResult => (options.client ?? client).post({ url: '/api/v1/sagas/{saga_id}/cancel', ...options }); diff --git a/frontend/src/lib/api/types.gen.ts b/frontend/src/lib/api/types.gen.ts index c9dbe9d73..fe6fa47d9 100644 --- a/frontend/src/lib/api/types.gen.ts +++ b/frontend/src/lib/api/types.gen.ts @@ -94,46 +94,7 @@ export type AdminUserOverview = { /** * Recent Events */ - recent_events: Array; -}; - -/** - * AllocateResourcesCommandEvent - */ -export type AllocateResourcesCommandEvent = { - /** - * Event Id - */ - event_id: string; - /** - * Event Type - */ - event_type: 'allocate_resources_command'; - /** - * Event Version - */ - event_version: string; - /** - * Timestamp - */ - timestamp: string; - /** - * Aggregate Id - */ - aggregate_id?: string | null; - metadata: EventMetadata; - /** - * Execution Id - */ - execution_id?: string; - /** - * Cpu Request - */ - cpu_request?: string; - /** - * Memory Request - */ - memory_request?: string; + recent_events: Array; }; /** @@ -296,86 +257,6 @@ export type ContainerStatusInfo = { state?: string; }; -/** - * CreatePodCommandEvent - */ -export type CreatePodCommandEvent = { - /** - * Event Id - */ - event_id: string; - /** - * Event Type - */ - event_type: 'create_pod_command'; - /** - * Event Version - */ - event_version: string; - /** - * Timestamp - */ - timestamp: string; - /** - * Aggregate Id - */ - aggregate_id?: string | null; - metadata: EventMetadata; - /** - * Saga Id - */ - saga_id?: string; - /** - * Execution Id - */ - execution_id?: string; - /** - * Script - */ - script?: string; - /** - * Language - */ - language?: string; - /** - * Language Version - */ - language_version?: string; - /** - * Runtime Image - */ - runtime_image?: string; - /** - * Runtime Command - */ - runtime_command?: Array; - /** - * Runtime Filename - */ - runtime_filename?: string; - /** - * Timeout Seconds - */ - timeout_seconds?: number; - /** - * Cpu Limit - */ - cpu_limit?: string; - /** - * Memory Limit - */ - memory_limit?: string; - /** - * Cpu Request - */ - cpu_request?: string; - /** - * Memory Request - */ - memory_request?: string; - priority?: QueuePriority; -}; - /** * DLQMessageDiscardedEvent * @@ -534,53 +415,6 @@ export type DeleteNotificationResponse = { message: string; }; -/** - * DeletePodCommandEvent - */ -export type DeletePodCommandEvent = { - /** - * Event Id - */ - event_id: string; - /** - * Event Type - */ - event_type: 'delete_pod_command'; - /** - * Event Version - */ - event_version: string; - /** - * Timestamp - */ - timestamp: string; - /** - * Aggregate Id - */ - aggregate_id?: string | null; - metadata: EventMetadata; - /** - * Saga Id - */ - saga_id?: string; - /** - * Execution Id - */ - execution_id?: string; - /** - * Reason - */ - reason?: string; - /** - * Pod Name - */ - pod_name?: string | null; - /** - * Namespace - */ - namespace?: string | null; -}; - /** * DeleteResponse * @@ -627,10 +461,6 @@ export type DeleteUserResponse = { * Events */ events: number; - /** - * Sagas - */ - sagas: number; }; /** @@ -811,7 +641,7 @@ export type EventBrowseResponse = { /** * Events */ - events: Array; + events: Array; /** * Total */ @@ -918,26 +748,6 @@ export type EventDetailResponse = { } & NotificationClickedEvent) | ({ event_type: 'notification_preferences_updated'; } & NotificationPreferencesUpdatedEvent) | ({ - event_type: 'saga_started'; - } & SagaStartedEvent) | ({ - event_type: 'saga_completed'; - } & SagaCompletedEvent) | ({ - event_type: 'saga_failed'; - } & SagaFailedEvent) | ({ - event_type: 'saga_cancelled'; - } & SagaCancelledEvent) | ({ - event_type: 'saga_compensating'; - } & SagaCompensatingEvent) | ({ - event_type: 'saga_compensated'; - } & SagaCompensatedEvent) | ({ - event_type: 'create_pod_command'; - } & CreatePodCommandEvent) | ({ - event_type: 'delete_pod_command'; - } & DeletePodCommandEvent) | ({ - event_type: 'allocate_resources_command'; - } & AllocateResourcesCommandEvent) | ({ - event_type: 'release_resources_command'; - } & ReleaseResourcesCommandEvent) | ({ event_type: 'script_saved'; } & ScriptSavedEvent) | ({ event_type: 'script_deleted'; @@ -1300,16 +1110,6 @@ export const EventType = { SERVICE_RECOVERED: 'service_recovered', RESULT_STORED: 'result_stored', RESULT_FAILED: 'result_failed', - SAGA_STARTED: 'saga_started', - SAGA_COMPLETED: 'saga_completed', - SAGA_FAILED: 'saga_failed', - SAGA_CANCELLED: 'saga_cancelled', - SAGA_COMPENSATING: 'saga_compensating', - SAGA_COMPENSATED: 'saga_compensated', - CREATE_POD_COMMAND: 'create_pod_command', - DELETE_POD_COMMAND: 'delete_pod_command', - ALLOCATE_RESOURCES_COMMAND: 'allocate_resources_command', - RELEASE_RESOURCES_COMMAND: 'release_resources_command', DLQ_MESSAGE_RECEIVED: 'dlq_message_received', DLQ_MESSAGE_RETRIED: 'dlq_message_retried', DLQ_MESSAGE_DISCARDED: 'dlq_message_discarded' @@ -3249,45 +3049,6 @@ export type RateLimitUpdateResponse = { config: UserRateLimitConfigResponse; }; -/** - * ReleaseResourcesCommandEvent - */ -export type ReleaseResourcesCommandEvent = { - /** - * Event Id - */ - event_id: string; - /** - * Event Type - */ - event_type: 'release_resources_command'; - /** - * Event Version - */ - event_version: string; - /** - * Timestamp - */ - timestamp: string; - /** - * Aggregate Id - */ - aggregate_id?: string | null; - metadata: EventMetadata; - /** - * Execution Id - */ - execution_id?: string; - /** - * Cpu Request - */ - cpu_request?: string; - /** - * Memory Request - */ - memory_request?: string; -}; - /** * ReplayConfigSchema */ @@ -3737,369 +3498,31 @@ export type ResourceUsageDomain = { */ clk_tck_hertz?: number; /** - * Peak Memory Kb - */ - peak_memory_kb?: number; -}; - -/** - * RestoreSettingsRequest - * - * Request model for restoring settings - */ -export type RestoreSettingsRequest = { - /** - * Timestamp - */ - timestamp: string; - /** - * Reason - */ - reason?: string | null; -}; - -/** - * ResultFailedEvent - */ -export type ResultFailedEvent = { - /** - * Event Id - */ - event_id: string; - /** - * Event Type - */ - event_type: 'result_failed'; - /** - * Event Version - */ - event_version: string; - /** - * Timestamp - */ - timestamp: string; - /** - * Aggregate Id - */ - aggregate_id?: string | null; - metadata: EventMetadata; - /** - * Execution Id - */ - execution_id?: string; - storage_type?: StorageType | null; -}; - -/** - * ResultStoredEvent - */ -export type ResultStoredEvent = { - /** - * Event Id - */ - event_id: string; - /** - * Event Type - */ - event_type: 'result_stored'; - /** - * Event Version - */ - event_version: string; - /** - * Timestamp - */ - timestamp: string; - /** - * Aggregate Id - */ - aggregate_id?: string | null; - metadata: EventMetadata; - /** - * Execution Id - */ - execution_id?: string; - storage_type?: StorageType | null; - /** - * Storage Path - */ - storage_path?: string; - /** - * Size Bytes - */ - size_bytes?: number; -}; - -/** - * SSEControlEvent - * - * Control events for execution SSE streams (not from Kafka). - */ -export const SseControlEvent = { STATUS: 'status' } as const; - -/** - * SSEControlEvent - * - * Control events for execution SSE streams (not from Kafka). - */ -export type SseControlEvent = typeof SseControlEvent[keyof typeof SseControlEvent]; - -/** - * SSEExecutionEventSchema - * - * API schema for SSE execution stream events. - * - * All fields are always present. Nullable fields carry null when not applicable: - * event_id is null for control events; status only for the status control event; - * result only for result_stored. - */ -export type SseExecutionEventSchema = { - /** - * Event Type - * - * Event type identifier - */ - event_type: EventType | SseControlEvent; - /** - * Execution Id - * - * Execution ID - */ - execution_id: string; - /** - * Timestamp - * - * Event timestamp - */ - timestamp: string | null; - /** - * Event Id - * - * Kafka event ID (null for control events) - */ - event_id: string | null; - /** - * Execution status (status control event only) - */ - status: ExecutionStatus | null; - /** - * Full execution result (result_stored only) - */ - result: ExecutionResult | null; -}; - -/** - * SagaCancellationResponse - * - * Response schema for saga cancellation - */ -export type SagaCancellationResponse = { - /** - * Success - */ - success: boolean; - /** - * Message - */ - message: string; - /** - * Saga Id - */ - saga_id: string; -}; - -/** - * SagaCancelledEvent - */ -export type SagaCancelledEvent = { - /** - * Event Id - */ - event_id: string; - /** - * Event Type - */ - event_type: 'saga_cancelled'; - /** - * Event Version - */ - event_version: string; - /** - * Timestamp - */ - timestamp: string; - /** - * Aggregate Id - */ - aggregate_id?: string | null; - metadata: EventMetadata; - /** - * Saga Id - */ - saga_id?: string; - /** - * Saga Name - */ - saga_name?: string; - /** - * Execution Id - */ - execution_id?: string; - /** - * Reason - */ - reason?: string; - /** - * Completed Steps - */ - completed_steps?: Array; - /** - * Compensated Steps - */ - compensated_steps?: Array; - /** - * Cancelled At - */ - cancelled_at?: string | null; - /** - * Cancelled By - */ - cancelled_by?: string | null; -}; - -/** - * SagaCompensatedEvent - */ -export type SagaCompensatedEvent = { - /** - * Event Id - */ - event_id: string; - /** - * Event Type - */ - event_type: 'saga_compensated'; - /** - * Event Version - */ - event_version: string; - /** - * Timestamp - */ - timestamp: string; - /** - * Aggregate Id - */ - aggregate_id?: string | null; - metadata: EventMetadata; - /** - * Saga Id - */ - saga_id?: string; - /** - * Saga Name - */ - saga_name?: string; - /** - * Execution Id - */ - execution_id?: string; - /** - * Compensated Steps - */ - compensated_steps?: Array; -}; - -/** - * SagaCompensatingEvent - */ -export type SagaCompensatingEvent = { - /** - * Event Id - */ - event_id: string; - /** - * Event Type - */ - event_type: 'saga_compensating'; - /** - * Event Version - */ - event_version: string; - /** - * Timestamp - */ - timestamp: string; - /** - * Aggregate Id - */ - aggregate_id?: string | null; - metadata: EventMetadata; - /** - * Saga Id - */ - saga_id?: string; - /** - * Saga Name - */ - saga_name?: string; - /** - * Execution Id - */ - execution_id?: string; - /** - * Compensating Step - */ - compensating_step?: string; -}; - -/** - * SagaCompletedEvent - */ -export type SagaCompletedEvent = { - /** - * Event Id - */ - event_id: string; - /** - * Event Type - */ - event_type: 'saga_completed'; - /** - * Event Version - */ - event_version: string; - /** - * Timestamp - */ - timestamp: string; - /** - * Aggregate Id - */ - aggregate_id?: string | null; - metadata: EventMetadata; - /** - * Saga Id - */ - saga_id?: string; - /** - * Saga Name + * Peak Memory Kb */ - saga_name?: string; + peak_memory_kb?: number; +}; + +/** + * RestoreSettingsRequest + * + * Request model for restoring settings + */ +export type RestoreSettingsRequest = { /** - * Execution Id + * Timestamp */ - execution_id?: string; + timestamp: string; /** - * Completed Steps + * Reason */ - completed_steps?: Array; + reason?: string | null; }; /** - * SagaFailedEvent + * ResultFailedEvent */ -export type SagaFailedEvent = { +export type ResultFailedEvent = { /** * Event Id */ @@ -4107,7 +3530,7 @@ export type SagaFailedEvent = { /** * Event Type */ - event_type: 'saga_failed'; + event_type: 'result_failed'; /** * Event Version */ @@ -4121,60 +3544,17 @@ export type SagaFailedEvent = { */ aggregate_id?: string | null; metadata: EventMetadata; - /** - * Saga Id - */ - saga_id?: string; - /** - * Saga Name - */ - saga_name?: string; /** * Execution Id */ execution_id?: string; - /** - * Failed Step - */ - failed_step?: string; - /** - * Error - */ - error?: string; -}; - -/** - * SagaListResponse - * - * Response schema for saga list - */ -export type SagaListResponse = { - /** - * Sagas - */ - sagas: Array; - /** - * Total - */ - total: number; - /** - * Skip - */ - skip: number; - /** - * Limit - */ - limit: number; - /** - * Has More - */ - has_more: boolean; + storage_type?: StorageType | null; }; /** - * SagaStartedEvent + * ResultStoredEvent */ -export type SagaStartedEvent = { +export type ResultStoredEvent = { /** * Event Id */ @@ -4182,7 +3562,7 @@ export type SagaStartedEvent = { /** * Event Type */ - event_type: 'saga_started'; + event_type: 'result_stored'; /** * Event Version */ @@ -4196,97 +3576,77 @@ export type SagaStartedEvent = { */ aggregate_id?: string | null; metadata: EventMetadata; - /** - * Saga Id - */ - saga_id?: string; - /** - * Saga Name - */ - saga_name?: string; /** * Execution Id */ execution_id?: string; + storage_type?: StorageType | null; + /** + * Storage Path + */ + storage_path?: string; /** - * Initial Event Id + * Size Bytes */ - initial_event_id?: string; + size_bytes?: number; }; /** - * SagaState + * SSEControlEvent * - * Saga execution states. + * Control events for execution SSE streams (not from Kafka). */ -export const SagaState = { - CREATED: 'created', - RUNNING: 'running', - COMPENSATING: 'compensating', - COMPLETED: 'completed', - FAILED: 'failed', - TIMEOUT: 'timeout', - CANCELLED: 'cancelled' -} as const; +export const SseControlEvent = { STATUS: 'status' } as const; /** - * SagaState + * SSEControlEvent * - * Saga execution states. + * Control events for execution SSE streams (not from Kafka). */ -export type SagaState = typeof SagaState[keyof typeof SagaState]; +export type SseControlEvent = typeof SseControlEvent[keyof typeof SseControlEvent]; /** - * SagaStatusResponse + * SSEExecutionEventSchema + * + * API schema for SSE execution stream events. * - * Response schema for saga status + * All fields are always present. Nullable fields carry null when not applicable: + * event_id is null for control events; status only for the status control event; + * result only for result_stored. */ -export type SagaStatusResponse = { - /** - * Saga Id - */ - saga_id: string; +export type SseExecutionEventSchema = { /** - * Saga Name + * Event Type + * + * Event type identifier */ - saga_name: string; + event_type: EventType | SseControlEvent; /** * Execution Id + * + * Execution ID */ execution_id: string; - state: SagaState; - /** - * Current Step - */ - current_step: string | null; - /** - * Completed Steps - */ - completed_steps: Array; - /** - * Compensated Steps - */ - compensated_steps: Array; - /** - * Error Message - */ - error_message: string | null; /** - * Created At + * Timestamp + * + * Event timestamp */ - created_at: string; + timestamp: string | null; /** - * Updated At + * Event Id + * + * Kafka event ID (null for control events) */ - updated_at: string; + event_id: string | null; /** - * Completed At + * Execution status (status control event only) */ - completed_at: string | null; + status: ExecutionStatus | null; /** - * Retry Count + * Full execution result (result_stored only) */ - retry_count: number; + result: ExecutionResult | null; }; /** @@ -6085,7 +5445,7 @@ export type GetExecutionEventsApiV1ExecutionsExecutionIdEventsGetResponses = { * * Successful Response */ - 200: Array; + 200: Array; }; export type GetExecutionEventsApiV1ExecutionsExecutionIdEventsGetResponse = GetExecutionEventsApiV1ExecutionsExecutionIdEventsGetResponses[keyof GetExecutionEventsApiV1ExecutionsExecutionIdEventsGetResponses]; @@ -8048,184 +7408,3 @@ export type DeleteNotificationApiV1NotificationsNotificationIdDeleteResponses = }; export type DeleteNotificationApiV1NotificationsNotificationIdDeleteResponse = DeleteNotificationApiV1NotificationsNotificationIdDeleteResponses[keyof DeleteNotificationApiV1NotificationsNotificationIdDeleteResponses]; - -export type GetSagaStatusApiV1SagasSagaIdGetData = { - body?: never; - path: { - /** - * Saga Id - */ - saga_id: string; - }; - query?: never; - url: '/api/v1/sagas/{saga_id}'; -}; - -export type GetSagaStatusApiV1SagasSagaIdGetErrors = { - /** - * Access denied - */ - 403: ErrorResponse; - /** - * Saga not found - */ - 404: ErrorResponse; - /** - * Error - */ - default: ErrorResponse; -}; - -export type GetSagaStatusApiV1SagasSagaIdGetError = GetSagaStatusApiV1SagasSagaIdGetErrors[keyof GetSagaStatusApiV1SagasSagaIdGetErrors]; - -export type GetSagaStatusApiV1SagasSagaIdGetResponses = { - /** - * Successful Response - */ - 200: SagaStatusResponse; -}; - -export type GetSagaStatusApiV1SagasSagaIdGetResponse = GetSagaStatusApiV1SagasSagaIdGetResponses[keyof GetSagaStatusApiV1SagasSagaIdGetResponses]; - -export type GetExecutionSagasApiV1SagasExecutionExecutionIdGetData = { - body?: never; - path: { - /** - * Execution Id - */ - execution_id: string; - }; - query?: { - /** - * State - * - * Filter by saga state - */ - state?: SagaState | null; - /** - * Limit - */ - limit?: number; - /** - * Skip - */ - skip?: number; - }; - url: '/api/v1/sagas/execution/{execution_id}'; -}; - -export type GetExecutionSagasApiV1SagasExecutionExecutionIdGetErrors = { - /** - * Access denied - */ - 403: ErrorResponse; - /** - * Error - */ - default: ErrorResponse; -}; - -export type GetExecutionSagasApiV1SagasExecutionExecutionIdGetError = GetExecutionSagasApiV1SagasExecutionExecutionIdGetErrors[keyof GetExecutionSagasApiV1SagasExecutionExecutionIdGetErrors]; - -export type GetExecutionSagasApiV1SagasExecutionExecutionIdGetResponses = { - /** - * Successful Response - */ - 200: SagaListResponse; -}; - -export type GetExecutionSagasApiV1SagasExecutionExecutionIdGetResponse = GetExecutionSagasApiV1SagasExecutionExecutionIdGetResponses[keyof GetExecutionSagasApiV1SagasExecutionExecutionIdGetResponses]; - -export type ListSagasApiV1SagasGetData = { - body?: never; - path?: never; - query?: { - /** - * State - * - * Filter by saga state - */ - state?: SagaState | null; - /** - * Execution Id - * - * Filter by execution ID - */ - execution_id?: string | null; - /** - * Search - * - * Search saga ID, saga name, execution ID or error message - */ - search?: string | null; - /** - * Limit - */ - limit?: number; - /** - * Skip - */ - skip?: number; - }; - url: '/api/v1/sagas/'; -}; - -export type ListSagasApiV1SagasGetErrors = { - /** - * Error - */ - default: ErrorResponse; -}; - -export type ListSagasApiV1SagasGetError = ListSagasApiV1SagasGetErrors[keyof ListSagasApiV1SagasGetErrors]; - -export type ListSagasApiV1SagasGetResponses = { - /** - * Successful Response - */ - 200: SagaListResponse; -}; - -export type ListSagasApiV1SagasGetResponse = ListSagasApiV1SagasGetResponses[keyof ListSagasApiV1SagasGetResponses]; - -export type CancelSagaApiV1SagasSagaIdCancelPostData = { - body?: never; - path: { - /** - * Saga Id - */ - saga_id: string; - }; - query?: never; - url: '/api/v1/sagas/{saga_id}/cancel'; -}; - -export type CancelSagaApiV1SagasSagaIdCancelPostErrors = { - /** - * Saga is not in a cancellable state - */ - 400: ErrorResponse; - /** - * Access denied - */ - 403: ErrorResponse; - /** - * Saga not found - */ - 404: ErrorResponse; - /** - * Error - */ - default: ErrorResponse; -}; - -export type CancelSagaApiV1SagasSagaIdCancelPostError = CancelSagaApiV1SagasSagaIdCancelPostErrors[keyof CancelSagaApiV1SagasSagaIdCancelPostErrors]; - -export type CancelSagaApiV1SagasSagaIdCancelPostResponses = { - /** - * Successful Response - */ - 200: SagaCancellationResponse; -}; - -export type CancelSagaApiV1SagasSagaIdCancelPostResponse = CancelSagaApiV1SagasSagaIdCancelPostResponses[keyof CancelSagaApiV1SagasSagaIdCancelPostResponses]; diff --git a/frontend/src/lib/api/zod.gen.ts b/frontend/src/lib/api/zod.gen.ts index 0bd666159..3f5451036 100644 --- a/frontend/src/lib/api/zod.gen.ts +++ b/frontend/src/lib/api/zod.gen.ts @@ -94,8 +94,7 @@ export const zDeleteUserResponse = z.object({ saved_scripts: z._default(z.int(), 0), notifications: z._default(z.int(), 0), user_settings: z._default(z.int(), 0), - events: z._default(z.int(), 0), - sagas: z._default(z.int(), 0) + events: z._default(z.int(), 0) }); /** @@ -200,21 +199,6 @@ export const zEventMetadata = z.object({ environment: z._default(z.optional(zEnvironment), 'production') }); -/** - * AllocateResourcesCommandEvent - */ -export const zAllocateResourcesCommandEvent = z.object({ - event_id: z.string(), - event_type: z._default(z.literal('allocate_resources_command'), 'allocate_resources_command'), - event_version: z._default(z.string(), '1.0'), - timestamp: z.iso.datetime(), - aggregate_id: z.nullish(z.string()), - metadata: zEventMetadata, - execution_id: z.optional(z.string()), - cpu_request: z.optional(z.string()), - memory_request: z.optional(z.string()) -}); - /** * AuthFailedEvent */ @@ -230,23 +214,6 @@ export const zAuthFailedEvent = z.object({ ip_address: z.nullish(z.string()) }); -/** - * DeletePodCommandEvent - */ -export const zDeletePodCommandEvent = z.object({ - event_id: z.string(), - event_type: z._default(z.literal('delete_pod_command'), 'delete_pod_command'), - event_version: z._default(z.string(), '1.0'), - timestamp: z.iso.datetime(), - aggregate_id: z.nullish(z.string()), - metadata: zEventMetadata, - saga_id: z.optional(z.string()), - execution_id: z.optional(z.string()), - reason: z.optional(z.string()), - pod_name: z.nullish(z.string()), - namespace: z.nullish(z.string()) -}); - /** * EventReplayRequest * @@ -311,16 +278,6 @@ export const zEventType = z.enum([ 'service_recovered', 'result_stored', 'result_failed', - 'saga_started', - 'saga_completed', - 'saga_failed', - 'saga_cancelled', - 'saga_compensating', - 'saga_compensated', - 'create_pod_command', - 'delete_pod_command', - 'allocate_resources_command', - 'release_resources_command', 'dlq_message_received', 'dlq_message_retried', 'dlq_message_discarded' @@ -1044,32 +1001,6 @@ export const zAdminExecutionListResponse = z.object({ has_more: z.boolean() }); -/** - * CreatePodCommandEvent - */ -export const zCreatePodCommandEvent = z.object({ - event_id: z.string(), - event_type: z._default(z.literal('create_pod_command'), 'create_pod_command'), - event_version: z._default(z.string(), '1.0'), - timestamp: z.iso.datetime(), - aggregate_id: z.nullish(z.string()), - metadata: zEventMetadata, - saga_id: z.optional(z.string()), - execution_id: z.optional(z.string()), - script: z.optional(z.string()), - language: z.optional(z.string()), - language_version: z.optional(z.string()), - runtime_image: z.optional(z.string()), - runtime_command: z.optional(z.array(z.string())), - runtime_filename: z.optional(z.string()), - timeout_seconds: z.optional(z.int()), - cpu_limit: z.optional(z.string()), - memory_limit: z.optional(z.string()), - cpu_request: z.optional(z.string()), - memory_request: z.optional(z.string()), - priority: z._default(z.optional(zQueuePriority), 'normal') -}); - /** * ExecutionAcceptedEvent */ @@ -1237,21 +1168,6 @@ export const zRateLimitUpdateRequest = z.object({ notes: z._default(z.optional(z.string()), '') }); -/** - * ReleaseResourcesCommandEvent - */ -export const zReleaseResourcesCommandEvent = z.object({ - event_id: z.string(), - event_type: z._default(z.literal('release_resources_command'), 'release_resources_command'), - event_version: z._default(z.string(), '1.0'), - timestamp: z.iso.datetime(), - aggregate_id: z.nullish(z.string()), - metadata: zEventMetadata, - execution_id: z.optional(z.string()), - cpu_request: z.optional(z.string()), - memory_request: z.optional(z.string()) -}); - /** * ReplayError * @@ -1616,166 +1532,6 @@ export const zSseExecutionEventSchema = z.object({ result: z.nullable(zExecutionResult) }); -/** - * SagaCancellationResponse - * - * Response schema for saga cancellation - */ -export const zSagaCancellationResponse = z.object({ - success: z.boolean(), - message: z.string(), - saga_id: z.string() -}); - -/** - * SagaCancelledEvent - */ -export const zSagaCancelledEvent = z.object({ - event_id: z.string(), - event_type: z._default(z.literal('saga_cancelled'), 'saga_cancelled'), - event_version: z._default(z.string(), '1.0'), - timestamp: z.iso.datetime(), - aggregate_id: z.nullish(z.string()), - metadata: zEventMetadata, - saga_id: z.optional(z.string()), - saga_name: z.optional(z.string()), - execution_id: z.optional(z.string()), - reason: z.optional(z.string()), - completed_steps: z.optional(z.array(z.string())), - compensated_steps: z.optional(z.array(z.string())), - cancelled_at: z.nullish(z.iso.datetime()), - cancelled_by: z.nullish(z.string()) -}); - -/** - * SagaCompensatedEvent - */ -export const zSagaCompensatedEvent = z.object({ - event_id: z.string(), - event_type: z._default(z.literal('saga_compensated'), 'saga_compensated'), - event_version: z._default(z.string(), '1.0'), - timestamp: z.iso.datetime(), - aggregate_id: z.nullish(z.string()), - metadata: zEventMetadata, - saga_id: z.optional(z.string()), - saga_name: z.optional(z.string()), - execution_id: z.optional(z.string()), - compensated_steps: z.optional(z.array(z.string())) -}); - -/** - * SagaCompensatingEvent - */ -export const zSagaCompensatingEvent = z.object({ - event_id: z.string(), - event_type: z._default(z.literal('saga_compensating'), 'saga_compensating'), - event_version: z._default(z.string(), '1.0'), - timestamp: z.iso.datetime(), - aggregate_id: z.nullish(z.string()), - metadata: zEventMetadata, - saga_id: z.optional(z.string()), - saga_name: z.optional(z.string()), - execution_id: z.optional(z.string()), - compensating_step: z.optional(z.string()) -}); - -/** - * SagaCompletedEvent - */ -export const zSagaCompletedEvent = z.object({ - event_id: z.string(), - event_type: z._default(z.literal('saga_completed'), 'saga_completed'), - event_version: z._default(z.string(), '1.0'), - timestamp: z.iso.datetime(), - aggregate_id: z.nullish(z.string()), - metadata: zEventMetadata, - saga_id: z.optional(z.string()), - saga_name: z.optional(z.string()), - execution_id: z.optional(z.string()), - completed_steps: z.optional(z.array(z.string())) -}); - -/** - * SagaFailedEvent - */ -export const zSagaFailedEvent = z.object({ - event_id: z.string(), - event_type: z._default(z.literal('saga_failed'), 'saga_failed'), - event_version: z._default(z.string(), '1.0'), - timestamp: z.iso.datetime(), - aggregate_id: z.nullish(z.string()), - metadata: zEventMetadata, - saga_id: z.optional(z.string()), - saga_name: z.optional(z.string()), - execution_id: z.optional(z.string()), - failed_step: z.optional(z.string()), - error: z.optional(z.string()) -}); - -/** - * SagaStartedEvent - */ -export const zSagaStartedEvent = z.object({ - event_id: z.string(), - event_type: z._default(z.literal('saga_started'), 'saga_started'), - event_version: z._default(z.string(), '1.0'), - timestamp: z.iso.datetime(), - aggregate_id: z.nullish(z.string()), - metadata: zEventMetadata, - saga_id: z.optional(z.string()), - saga_name: z.optional(z.string()), - execution_id: z.optional(z.string()), - initial_event_id: z.optional(z.string()) -}); - -/** - * SagaState - * - * Saga execution states. - */ -export const zSagaState = z.enum([ - 'created', - 'running', - 'compensating', - 'completed', - 'failed', - 'timeout', - 'cancelled' -]); - -/** - * SagaStatusResponse - * - * Response schema for saga status - */ -export const zSagaStatusResponse = z.object({ - saga_id: z.string(), - saga_name: z.string(), - execution_id: z.string(), - state: zSagaState, - current_step: z.nullable(z.string()), - completed_steps: z.array(z.string()), - compensated_steps: z.array(z.string()), - error_message: z.nullable(z.string()), - created_at: z.iso.datetime(), - updated_at: z.iso.datetime(), - completed_at: z.nullable(z.iso.datetime()), - retry_count: z.int() -}); - -/** - * SagaListResponse - * - * Response schema for saga list - */ -export const zSagaListResponse = z.object({ - sagas: z.array(zSagaStatusResponse), - total: z.int(), - skip: z.int(), - limit: z.int(), - has_more: z.boolean() -}); - /** * SavedScriptCreateRequest */ @@ -2475,16 +2231,6 @@ export const zAdminUserOverview = z.object({ zNotificationAllReadEvent, zNotificationClickedEvent, zNotificationPreferencesUpdatedEvent, - zSagaStartedEvent, - zSagaCompletedEvent, - zSagaFailedEvent, - zSagaCancelledEvent, - zSagaCompensatingEvent, - zSagaCompensatedEvent, - zCreatePodCommandEvent, - zDeletePodCommandEvent, - zAllocateResourcesCommandEvent, - zReleaseResourcesCommandEvent, zScriptSavedEvent, zScriptDeletedEvent, zScriptSharedEvent, @@ -2542,16 +2288,6 @@ export const zEventBrowseResponse = z.object({ zNotificationAllReadEvent, zNotificationClickedEvent, zNotificationPreferencesUpdatedEvent, - zSagaStartedEvent, - zSagaCompletedEvent, - zSagaFailedEvent, - zSagaCancelledEvent, - zSagaCompensatingEvent, - zSagaCompensatedEvent, - zCreatePodCommandEvent, - zDeletePodCommandEvent, - zAllocateResourcesCommandEvent, - zReleaseResourcesCommandEvent, zScriptSavedEvent, zScriptDeletedEvent, zScriptSharedEvent, @@ -2612,16 +2348,6 @@ export const zEventDetailResponse = z.object({ zNotificationAllReadEvent, zNotificationClickedEvent, zNotificationPreferencesUpdatedEvent, - zSagaStartedEvent, - zSagaCompletedEvent, - zSagaFailedEvent, - zSagaCancelledEvent, - zSagaCompensatingEvent, - zSagaCompensatedEvent, - zCreatePodCommandEvent, - zDeletePodCommandEvent, - zAllocateResourcesCommandEvent, - zReleaseResourcesCommandEvent, zScriptSavedEvent, zScriptDeletedEvent, zScriptSharedEvent, @@ -2818,16 +2544,6 @@ export const zGetExecutionEventsApiV1ExecutionsExecutionIdEventsGetResponse = z. zNotificationAllReadEvent, zNotificationClickedEvent, zNotificationPreferencesUpdatedEvent, - zSagaStartedEvent, - zSagaCompletedEvent, - zSagaFailedEvent, - zSagaCancelledEvent, - zSagaCompensatingEvent, - zSagaCompensatedEvent, - zCreatePodCommandEvent, - zDeletePodCommandEvent, - zAllocateResourcesCommandEvent, - zReleaseResourcesCommandEvent, zScriptSavedEvent, zScriptDeletedEvent, zScriptSharedEvent, @@ -3361,49 +3077,3 @@ export const zDeleteNotificationApiV1NotificationsNotificationIdDeletePath = z.o * Successful Response */ export const zDeleteNotificationApiV1NotificationsNotificationIdDeleteResponse = zDeleteNotificationResponse; - -export const zGetSagaStatusApiV1SagasSagaIdGetPath = z.object({ - saga_id: z.string() -}); - -/** - * Successful Response - */ -export const zGetSagaStatusApiV1SagasSagaIdGetResponse = zSagaStatusResponse; - -export const zGetExecutionSagasApiV1SagasExecutionExecutionIdGetPath = z.object({ - execution_id: z.string() -}); - -export const zGetExecutionSagasApiV1SagasExecutionExecutionIdGetQuery = z.object({ - state: z.nullish(zSagaState), - limit: z._default(z.optional(z.int().check(z.gte(1), z.lte(1000))), 100), - skip: z._default(z.optional(z.int().check(z.gte(0))), 0) -}); - -/** - * Successful Response - */ -export const zGetExecutionSagasApiV1SagasExecutionExecutionIdGetResponse = zSagaListResponse; - -export const zListSagasApiV1SagasGetQuery = z.object({ - state: z.nullish(zSagaState), - execution_id: z.nullish(z.string()), - search: z.nullish(z.string().check(z.maxLength(200))), - limit: z._default(z.optional(z.int().check(z.gte(1), z.lte(1000))), 100), - skip: z._default(z.optional(z.int().check(z.gte(0))), 0) -}); - -/** - * Successful Response - */ -export const zListSagasApiV1SagasGetResponse = zSagaListResponse; - -export const zCancelSagaApiV1SagasSagaIdCancelPostPath = z.object({ - saga_id: z.string() -}); - -/** - * Successful Response - */ -export const zCancelSagaApiV1SagasSagaIdCancelPostResponse = zSagaCancellationResponse; diff --git a/frontend/src/routes/admin/__tests__/AdminLayout.test.ts b/frontend/src/routes/admin/__tests__/AdminLayout.test.ts index a981fc627..5371875e5 100644 --- a/frontend/src/routes/admin/__tests__/AdminLayout.test.ts +++ b/frontend/src/routes/admin/__tests__/AdminLayout.test.ts @@ -32,17 +32,15 @@ describe('AdminLayout', () => { expect(screen.getByText('Event Browser')).toBeInTheDocument(); }); expect(screen.getByText('Executions')).toBeInTheDocument(); - expect(screen.getByText('Sagas')).toBeInTheDocument(); expect(screen.getByText('Users')).toBeInTheDocument(); expect(screen.getByText('Settings')).toBeInTheDocument(); const adminLinks = screen.getAllByRole('link').filter((l) => l.getAttribute('href')?.startsWith('/admin/')); - expect(adminLinks.length).toBe(5); + expect(adminLinks.length).toBe(4); const hrefs = adminLinks.map((l) => l.getAttribute('href')); expect(hrefs).toContain('/admin/events'); expect(hrefs).toContain('/admin/executions'); - expect(hrefs).toContain('/admin/sagas'); expect(hrefs).toContain('/admin/users'); expect(hrefs).toContain('/admin/settings'); }); @@ -50,7 +48,6 @@ describe('AdminLayout', () => { it.each([ ['/admin/events', 'Event Browser'], ['/admin/executions', 'Executions'], - ['/admin/sagas', 'Sagas'], ['/admin/users', 'Users'], ['/admin/settings', 'Settings'], ])('highlights active link for path "%s"', async (path, label) => { diff --git a/frontend/src/routes/admin/__tests__/AdminSagas.test.ts b/frontend/src/routes/admin/__tests__/AdminSagas.test.ts deleted file mode 100644 index 8f9bf470b..000000000 --- a/frontend/src/routes/admin/__tests__/AdminSagas.test.ts +++ /dev/null @@ -1,376 +0,0 @@ -import { describe, it, expect, beforeEach, vi } from 'vitest'; -import { screen, waitFor, within } from '@testing-library/svelte'; -import { tick } from 'svelte'; -import { user, mockApi, renderWithQueryClient } from '#test/test-utils.ts'; -import { fakeSagaStatusResponse } from '#lib/api/@faker-js/faker.gen.ts'; -import { listSagasApiV1SagasGet, getSagaStatusApiV1SagasSagaIdGet, type SagaStatusResponse } from '#lib/api/index.ts'; - -import AdminSagas from '#routes/admin/sagas/+page.svelte'; - -async function renderWithSagas(sagas = Array.from({ length: 5 }, () => fakeSagaStatusResponse())) { - mockApi(listSagasApiV1SagasGet).ok({ sagas, total: sagas.length, skip: 0, limit: 10, has_more: false }); - - const result = renderWithQueryClient(AdminSagas); - await screen.findByRole('table', { name: 'Sagas' }); - return result; -} - -describe('AdminSagas', () => { - beforeEach(() => { - mockApi(listSagasApiV1SagasGet).ok({ sagas: [], total: 0, skip: 0, limit: 10, has_more: false }); - mockApi(getSagaStatusApiV1SagasSagaIdGet).ok(fakeSagaStatusResponse()); - }); - - describe('initial loading', () => { - it('calls listSagas on mount', async () => { - renderWithQueryClient(AdminSagas); - await waitFor(() => expect(vi.mocked(listSagasApiV1SagasGet)).toHaveBeenCalled()); - }); - - it('displays loading state', async () => { - mockApi(listSagasApiV1SagasGet).mock.mockImplementation(() => new Promise(() => {})); - renderWithQueryClient(AdminSagas); - await tick(); - expect(screen.getByText(/loading sagas/i)).toBeInTheDocument(); - }); - - it('displays empty state when no sagas', async () => { - renderWithQueryClient(AdminSagas); - expect(await screen.findByText(/no sagas found/i)).toBeInTheDocument(); - }); - }); - - describe('saga list rendering', () => { - it('displays saga data in table', async () => { - const sagas: SagaStatusResponse[] = [ - { ...fakeSagaStatusResponse(), saga_name: 'test_saga', state: 'running' }, - ]; - await renderWithSagas(sagas); - expect(screen.getAllByText('test_saga')).toHaveLength(2); - }); - - it('displays multiple sagas', async () => { - const sagas = Array.from({ length: 3 }, (_, i) => ({ - ...fakeSagaStatusResponse(), - saga_id: `saga-${i + 1}`, - })); - await renderWithSagas(sagas); - expect(screen.getAllByText(/saga-1/)).toHaveLength(2); - }); - - it('shows state badges with correct labels', async () => { - const sagas: SagaStatusResponse[] = [ - { ...fakeSagaStatusResponse(), saga_id: 's1', state: 'completed' }, - { ...fakeSagaStatusResponse(), saga_id: 's2', state: 'failed' }, - { ...fakeSagaStatusResponse(), saga_id: 's3', state: 'running' }, - ]; - await renderWithSagas(sagas); - // States appear in both table rows and stats cards - expect(screen.getAllByText('completed').length).toBeGreaterThan(0); - expect(screen.getAllByText('failed').length).toBeGreaterThan(0); - expect(screen.getAllByText('running').length).toBeGreaterThan(0); - }); - - it('shows retry count when > 0', async () => { - const sagas = [{ ...fakeSagaStatusResponse(), retry_count: 3 }]; - await renderWithSagas(sagas); - expect(screen.getAllByText('(Retry Count 3)').length).toBeGreaterThan(0); - }); - }); - - describe('stats cards', () => { - it('displays state counts', async () => { - const sagas: SagaStatusResponse[] = [ - { ...fakeSagaStatusResponse(), saga_id: 's1', state: 'completed' }, - { ...fakeSagaStatusResponse(), saga_id: 's2', state: 'completed' }, - { ...fakeSagaStatusResponse(), saga_id: 's3', state: 'failed' }, - ]; - await renderWithSagas(sagas); - const terms = screen.getAllByRole('term').map((term) => term.textContent); - expect(screen.getAllByRole('definition')[terms.indexOf('completed')]).toHaveTextContent('2'); - }); - }); - - describe('auto-refresh', () => { - it('auto-refreshes at specified interval', async () => { - await renderWithSagas(); - vi.setTimerTickMode('manual'); - expect(vi.mocked(listSagasApiV1SagasGet)).toHaveBeenCalledTimes(1); - - await vi.advanceTimersByTimeAsync(5000); - expect(vi.mocked(listSagasApiV1SagasGet)).toHaveBeenCalledTimes(2); - - await vi.advanceTimersByTimeAsync(5000); - expect(vi.mocked(listSagasApiV1SagasGet)).toHaveBeenCalledTimes(3); - }); - - it('manual refresh button works', async () => { - await renderWithSagas(); - vi.clearAllMocks(); - - const refreshButton = screen.getByRole('button', { name: /refresh now/i }); - await user.click(refreshButton); - - await waitFor(() => expect(vi.mocked(listSagasApiV1SagasGet)).toHaveBeenCalled()); - }); - - it('toggling auto-refresh off stops polling', async () => { - await renderWithSagas(); - - const checkbox = screen.getByRole('checkbox', { name: /auto-refresh/i }); - await user.click(checkbox); - - vi.clearAllMocks(); - await vi.advanceTimersByTimeAsync(10000); - - expect(vi.mocked(listSagasApiV1SagasGet)).not.toHaveBeenCalled(); - }); - }); - - describe('filters', () => { - it('filters by state', async () => { - await renderWithSagas(); - vi.clearAllMocks(); - - const stateSelect = screen.getByLabelText(/state/i); - await user.selectOptions(stateSelect, 'running'); - - await waitFor(() => { - expect(vi.mocked(listSagasApiV1SagasGet)).toHaveBeenCalledWith( - expect.objectContaining({ - query: expect.objectContaining({ state: 'running' }), - }), - ); - }); - }); - - it('sends the search to the API, so it covers every page', async () => { - const alpha = { ...fakeSagaStatusResponse(), saga_id: 's1', saga_name: 'alpha_saga' }; - await renderWithSagas([alpha, { ...fakeSagaStatusResponse(), saga_id: 's2', saga_name: 'beta_saga' }]); - mockApi(listSagasApiV1SagasGet).ok({ sagas: [alpha], total: 1, skip: 0, limit: 10, has_more: false }); - - await user.type(screen.getByLabelText(/search/i), 'alpha'); - - await waitFor(() => { - expect(vi.mocked(listSagasApiV1SagasGet)).toHaveBeenLastCalledWith( - expect.objectContaining({ query: expect.objectContaining({ search: 'alpha', skip: 0 }) }), - ); - expect(screen.queryAllByText('beta_saga')).toHaveLength(0); - }); - expect(screen.getAllByText('alpha_saga').length).toBeGreaterThan(0); - }); - - it('filters by execution ID', async () => { - const sagas = [ - { ...fakeSagaStatusResponse(), saga_id: 's1', execution_id: 'exec-abc' }, - { ...fakeSagaStatusResponse(), saga_id: 's2', execution_id: 'exec-xyz' }, - ]; - await renderWithSagas(sagas); - - const matchingSaga = [{ ...fakeSagaStatusResponse(), saga_id: 's1', execution_id: 'exec-abc' }]; - mockApi(listSagasApiV1SagasGet).ok({ sagas: matchingSaga, total: 1, skip: 0, limit: 10, has_more: false }); - - const execInput = screen.getByLabelText(/execution id/i); - await user.type(execInput, 'abc'); - - await waitFor(() => { - expect(vi.mocked(listSagasApiV1SagasGet)).toHaveBeenCalledWith( - expect.objectContaining({ - query: expect.objectContaining({ execution_id: 'abc' }), - }), - ); - expect(screen.getAllByText(/exec-abc/).length).toBeGreaterThan(0); - }); - }); - - it('clears filters on clear button click', async () => { - await renderWithSagas(); - - const stateSelect = screen.getByLabelText(/state/i); - await user.selectOptions(stateSelect, 'failed'); - - vi.clearAllMocks(); - const clearButton = screen.getByRole('button', { name: /clear filters/i }); - await user.click(clearButton); - - await waitFor(() => { - expect(vi.mocked(listSagasApiV1SagasGet)).toHaveBeenCalledWith( - expect.objectContaining({ - query: expect.objectContaining({ state: null }), - }), - ); - }); - }); - }); - - describe('saga details modal', () => { - it('opens modal on View Details click', async () => { - const saga = { - ...fakeSagaStatusResponse(), - saga_name: 'execution_saga', - completed_steps: ['validate_execution', 'create_pod'], - }; - mockApi(getSagaStatusApiV1SagasSagaIdGet).ok(saga); - await renderWithSagas([saga]); - - const [viewBtn] = screen.getAllByRole('button', { name: /view details/i }); - await user.click(viewBtn!); - - await waitFor(() => expect(screen.getByRole('dialog')).toBeInTheDocument()); - expect(screen.getByText('Saga Details')).toBeInTheDocument(); - }); - - it('shows the execution from the details and closes them', async () => { - const saga = { ...fakeSagaStatusResponse(), execution_id: 'exec-from-details' }; - mockApi(getSagaStatusApiV1SagasSagaIdGet).ok(saga); - await renderWithSagas([saga]); - - await user.click(within(screen.getByRole('table')).getByRole('button', { name: /view details/i })); - await user.click(await within(await screen.findByRole('dialog')).findByText('exec-from-details')); - - await waitFor(() => - expect(vi.mocked(listSagasApiV1SagasGet)).toHaveBeenLastCalledWith( - expect.objectContaining({ query: expect.objectContaining({ execution_id: 'exec-from-details' }) }), - ), - ); - await waitFor(() => expect(screen.queryByRole('dialog')).not.toBeInTheDocument()); - }); - - it('displays saga information in modal', async () => { - const saga: SagaStatusResponse = { - ...fakeSagaStatusResponse(), - saga_id: 'saga-detail-test', - saga_name: 'execution_saga', - state: 'completed', - completed_steps: ['validate_execution'], - retry_count: 2, - }; - mockApi(getSagaStatusApiV1SagasSagaIdGet).ok(saga); - await renderWithSagas([saga]); - - const [viewBtn] = screen.getAllByRole('button', { name: /view details/i }); - await user.click(viewBtn!); - - await waitFor(() => { - expect(screen.getByText('saga-detail-test')).toBeInTheDocument(); - }); - }); - - it('shows error message when saga has error', async () => { - const saga: SagaStatusResponse = { - ...fakeSagaStatusResponse(), - state: 'failed', - error_message: 'Pod creation failed: timeout', - }; - mockApi(getSagaStatusApiV1SagasSagaIdGet).ok(saga); - await renderWithSagas([saga]); - - const [viewBtn] = screen.getAllByRole('button', { name: /view details/i }); - await user.click(viewBtn!); - - await waitFor(() => { - expect(screen.getByText(/pod creation failed/i)).toBeInTheDocument(); - }); - }); - - it('closes modal on close button click', async () => { - const saga = fakeSagaStatusResponse(); - mockApi(getSagaStatusApiV1SagasSagaIdGet).ok(saga); - await renderWithSagas([saga]); - - const [viewBtn] = screen.getAllByRole('button', { name: /view details/i }); - await user.click(viewBtn!); - await waitFor(() => expect(screen.getByRole('dialog')).toBeInTheDocument()); - - await user.click(screen.getByRole('button', { name: 'Close dialog' })); - - await waitFor(() => { - expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); - }); - }); - - it('shows compensated steps', async () => { - const saga: SagaStatusResponse = { - ...fakeSagaStatusResponse(), - saga_name: 'execution_saga', - state: 'failed', - completed_steps: ['validate_execution', 'create_pod'], - compensated_steps: ['delete_pod'], - }; - mockApi(getSagaStatusApiV1SagasSagaIdGet).ok(saga); - await renderWithSagas([saga]); - - const [viewBtn] = screen.getAllByRole('button', { name: /view details/i }); - await user.click(viewBtn!); - - await waitFor(() => expect(screen.getByRole('dialog')).toBeInTheDocument()); - expect(screen.getByText('delete_pod')).toBeInTheDocument(); - }); - }); - - describe('view execution sagas', () => { - it('loads sagas for specific execution', async () => { - const executionSagas = [{ ...fakeSagaStatusResponse(), execution_id: 'exec-target' }]; - mockApi(listSagasApiV1SagasGet).ok({ - sagas: executionSagas, - total: 1, - skip: 0, - limit: 10, - has_more: false, - }); - await renderWithSagas([{ ...fakeSagaStatusResponse(), execution_id: 'exec-target' }]); - - const [execButton] = screen.getAllByRole('button', { name: /execution/i }); - await user.click(execButton!); - await waitFor(() => { - expect(vi.mocked(listSagasApiV1SagasGet)).toHaveBeenCalledWith( - expect.objectContaining({ - query: expect.objectContaining({ execution_id: 'exec-target' }), - }), - ); - }); - }); - }); - - describe('pagination', () => { - it('shows pagination when items exist', async () => { - const sagas = Array.from({ length: 5 }, () => fakeSagaStatusResponse()); - mockApi(listSagasApiV1SagasGet).ok({ sagas, total: 25, skip: 0, limit: 10, has_more: true }); - - renderWithQueryClient(AdminSagas); - expect(await screen.findByText(/showing/i)).toBeInTheDocument(); - }); - - it('changes page size', async () => { - const sagas = Array.from({ length: 5 }, () => fakeSagaStatusResponse()); - mockApi(listSagasApiV1SagasGet).ok({ sagas, total: 25, skip: 0, limit: 10, has_more: true }); - - renderWithQueryClient(AdminSagas); - await waitFor(() => expect(vi.mocked(listSagasApiV1SagasGet)).toHaveBeenCalled()); - - vi.clearAllMocks(); - const pageSizeSelect = screen.getByDisplayValue('10 / page'); - await user.selectOptions(pageSizeSelect, '25'); - - await waitFor(() => { - expect(vi.mocked(listSagasApiV1SagasGet)).toHaveBeenCalledWith( - expect.objectContaining({ - query: expect.objectContaining({ limit: 25 }), - }), - ); - }); - }); - }); - - describe('refresh rate control', () => { - it('polls at initial refresh rate', async () => { - await renderWithSagas(); - vi.setTimerTickMode('manual'); - - vi.clearAllMocks(); - await vi.advanceTimersByTimeAsync(5000); - - expect(vi.mocked(listSagasApiV1SagasGet)).toHaveBeenCalledTimes(1); - }); - }); -}); diff --git a/frontend/src/routes/admin/executions/+page.svelte b/frontend/src/routes/admin/executions/+page.svelte index 69fa2a4bd..f3acfd2d4 100644 --- a/frontend/src/routes/admin/executions/+page.svelte +++ b/frontend/src/routes/admin/executions/+page.svelte @@ -30,6 +30,7 @@ }); const queryClient = useQueryClient(); + // --8<-- [start:list] const list = createQuery(() => ({ ...listExecutionsApiV1AdminExecutionsGetOptions({ query: { ...filters, user_id: filters.user_id || null, limit: pageSize, skip: pageIndex * pageSize }, @@ -37,6 +38,7 @@ placeholderData: keepPreviousData, refetchInterval: POLL_INTERVAL_MS, })); + // --8<-- [end:list] const queue = createQuery(() => ({ ...getQueueStatusApiV1AdminExecutionsQueueGetOptions(), refetchInterval: POLL_INTERVAL_MS, diff --git a/frontend/src/routes/admin/sagas/+page.svelte b/frontend/src/routes/admin/sagas/+page.svelte deleted file mode 100644 index b1b96d29a..000000000 --- a/frontend/src/routes/admin/sagas/+page.svelte +++ /dev/null @@ -1,75 +0,0 @@ - - -
-
-

Saga Management

-

Monitor and debug distributed transactions

-
- - - -
- list.refetch()} - /> -
- - (filters = noFilters())} /> - - ({ pageIndex, pageSize }), - (next) => { - pageSize = next.pageSize; - pageIndex = next.pageIndex; - } - } - loading={list.isPending} - onViewDetails={(id) => (selectedSagaId = id)} - onViewExecution={showExecution} - /> -
- -{#if selectedSagaId} - (selectedSagaId = '')} onViewExecution={showExecution} /> -{/if} diff --git a/mkdocs.yml b/mkdocs.yml index 6912dbed0..3d196264a 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -149,7 +149,7 @@ nav: - Execution Flow: components/sse/execution-sse-flow.md - Worker: - Overview: components/workers/index.md - - Saga Orchestrator: components/workers/saga_orchestrator.md + - Execution Scheduler: components/workers/execution_scheduler.md - K8s Worker: components/workers/k8s_worker.md - Pod Monitor: components/workers/pod_monitor.md - Result Processor: components/workers/result_processor.md