Skip to content

Commit c9d18d7

Browse files
committed
Add prune and re-enqueue signal functionality
- Introduced new routes for pruning states and re-enqueuing states after a specified time. - Added corresponding controller functions to handle the logic for pruning and re-enqueuing states, including validation and error handling. - Created new signal models for request and response structures related to pruning and re-enqueuing operations. - Updated the State model to include a new field for enqueue_after, enhancing state management capabilities. - Enhanced logging for better traceability of operations related to state management.
1 parent 21bfc1d commit c9d18d7

7 files changed

Lines changed: 155 additions & 5 deletions

File tree

state-manager/app/controller/enqueue_states.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import asyncio
2+
import time
23

34
from ..models.enqueue_request import EnqueueRequestModel
45
from ..models.enqueue_response import EnqueueResponseModel, StateModel
@@ -18,7 +19,8 @@ async def find_state(namespace_name: str, nodes: list[str]) -> State | None:
1819
"status": StateStatusEnum.CREATED,
1920
"node_name": {
2021
"$in": nodes
21-
}
22+
},
23+
"enqueue_after": {"$lte": int(time.time() * 1000)}
2224
},
2325
{
2426
"$set": {"status": StateStatusEnum.QUEUED}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
from app.models.signal_models import PruneRequestModel, SignalResponseModel
2+
from fastapi import HTTPException, status
3+
from beanie import PydanticObjectId
4+
5+
from app.models.db.state import State
6+
from app.models.state_status_enum import StateStatusEnum
7+
from app.singletons.logs_manager import LogsManager
8+
9+
logger = LogsManager().get_logger()
10+
11+
async def prune_signal(namespace_name: str, state_id: PydanticObjectId, body: PruneRequestModel, x_exosphere_request_id: str) -> SignalResponseModel:
12+
13+
try:
14+
logger.info(f"Recieved prune signal for state {state_id} for namespace {namespace_name}", x_exosphere_request_id=x_exosphere_request_id)
15+
16+
state = await State.find_one(State.id == state_id)
17+
18+
if not state:
19+
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="State not found")
20+
21+
if state.status != StateStatusEnum.CREATED:
22+
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="State is not created")
23+
24+
state.status = StateStatusEnum.PRUNED
25+
state.data = body.data
26+
await state.save()
27+
28+
return SignalResponseModel(status=StateStatusEnum.PRUNED, enqueue_after=state.enqueue_after)
29+
30+
except Exception as e:
31+
logger.error(f"Error pruning state {state_id} for namespace {namespace_name}", x_exosphere_request_id=x_exosphere_request_id, error=e)
32+
raise
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
from app.models.signal_models import ReEnqueueAfterRequestModel, SignalResponseModel
2+
from fastapi import HTTPException, status
3+
from beanie import PydanticObjectId
4+
5+
from app.models.db.state import State
6+
from app.models.state_status_enum import StateStatusEnum
7+
from app.singletons.logs_manager import LogsManager
8+
9+
logger = LogsManager().get_logger()
10+
11+
async def re_queue_after_signal(namespace_name: str, state_id: PydanticObjectId, body: ReEnqueueAfterRequestModel, x_exosphere_request_id: str) -> SignalResponseModel:
12+
13+
try:
14+
logger.info(f"Recieved re-queue after signal for state {state_id} for namespace {namespace_name}", x_exosphere_request_id=x_exosphere_request_id)
15+
16+
state = await State.find_one(State.id == state_id)
17+
18+
if not state:
19+
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="State not found")
20+
21+
if state.status != StateStatusEnum.CREATED:
22+
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="State is not created")
23+
24+
state.status = StateStatusEnum.CREATED
25+
state.enqueue_after = state.enqueue_after + body.enqueue_after
26+
await state.save()
27+
28+
return SignalResponseModel(status=StateStatusEnum.CREATED, enqueue_after=state.enqueue_after)
29+
30+
except Exception as e:
31+
logger.error(f"Error re-queueing state {state_id} for namespace {namespace_name}", x_exosphere_request_id=x_exosphere_request_id, error=e)
32+
raise

state-manager/app/models/db/state.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from typing import Any, Optional
88
import hashlib
99
import json
10-
10+
import time
1111

1212
class State(BaseDatabaseModel):
1313
node_name: str = Field(..., description="Name of the node of the state")
@@ -18,10 +18,12 @@ class State(BaseDatabaseModel):
1818
status: StateStatusEnum = Field(..., description="Status of the state")
1919
inputs: dict[str, Any] = Field(..., description="Inputs of the state")
2020
outputs: dict[str, Any] = Field(..., description="Outputs of the state")
21+
data: dict[str, Any] = Field(default_factory=dict, description="Data of the state")
2122
error: Optional[str] = Field(None, description="Error message")
2223
parents: dict[str, PydanticObjectId] = Field(default_factory=dict, description="Parents of the state")
2324
does_unites: bool = Field(default=False, description="Whether this state unites other states")
2425
state_fingerprint: str = Field(default="", description="Fingerprint of the state")
26+
enqueue_after: int = Field(default_factory=lambda: int(time.time() * 1000), description="Unix time in milliseconds after which the state should be enqueued")
2527

2628
@before_event([Insert, Replace, Save])
2729
def _generate_fingerprint(self):
@@ -65,5 +67,29 @@ class Settings:
6567
partialFilterExpression={
6668
"does_unites": True
6769
}
70+
),
71+
IndexModel(
72+
[
73+
("enqueue_after", 1)
74+
],
75+
name="idx_enqueue_after"
76+
),
77+
IndexModel(
78+
[
79+
("status", 1)
80+
],
81+
name="idx_status"
82+
),
83+
IndexModel(
84+
[
85+
("namespace_name", 1),
86+
],
87+
name="idx_namespace_name"
88+
),
89+
IndexModel(
90+
[
91+
("node_name", 1),
92+
],
93+
name="idx_node_name"
6894
)
6995
]
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
from pydantic import BaseModel, Field
2+
from .state_status_enum import StateStatusEnum
3+
from typing import Any
4+
5+
6+
class SignalResponseModel(BaseModel):
7+
enqueue_after: int = Field(..., description="Unix time in milliseconds after which the state should be re-enqueued")
8+
status: StateStatusEnum = Field(..., description="Status of the state")
9+
10+
class PruneRequestModel(BaseModel):
11+
data: dict[str, Any] = Field(..., description="Data of the state")
12+
13+
class ReEnqueueAfterRequestModel(BaseModel):
14+
enqueue_after: int = Field(..., description="Unix time in milliseconds after which the state should be re-enqueued")

state-manager/app/models/state_status_enum.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,8 @@ class StateStatusEnum(str, Enum):
66
CREATED = 'CREATED'
77
QUEUED = 'QUEUED'
88
EXECUTED = 'EXECUTED'
9-
NEXT_CREATED = 'NEXT_CREATED'
10-
RETRY_CREATED = 'RETRY_CREATED'
11-
TIMEDOUT = 'TIMEDOUT'
129
ERRORED = 'ERRORED'
1310
CANCELLED = 'CANCELLED'
1411
SUCCESS = 'SUCCESS'
1512
NEXT_CREATED_ERROR = 'NEXT_CREATED_ERROR'
13+
PRUNED = 'PRUNED'

state-manager/app/routes.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,14 @@
4040
from .models.graph_structure_models import GraphStructureResponse
4141
from .controller.get_graph_structure import get_graph_structure
4242

43+
### singnals
44+
from .models.signal_models import SignalResponseModel
45+
from .models.signal_models import PruneRequestModel
46+
from .controller.prune_signal import prune_signal
47+
from .models.signal_models import ReEnqueueAfterRequestModel
48+
from .controller.re_queue_after_singal import re_queue_after_signal
49+
50+
4351
logger = LogsManager().get_logger()
4452

4553
router = APIRouter(prefix="/v0/namespace/{namespace_name}")
@@ -145,6 +153,44 @@ async def errored_state_route(namespace_name: str, state_id: str, body: ErroredR
145153
return await errored_state(namespace_name, PydanticObjectId(state_id), body, x_exosphere_request_id)
146154

147155

156+
@router.post(
157+
"/states/{state_id}/prune",
158+
response_model=SignalResponseModel,
159+
status_code=status.HTTP_200_OK,
160+
response_description="State skipped successfully",
161+
tags=["state"]
162+
)
163+
async def prune_state_route(namespace_name: str, state_id: str, body: PruneRequestModel, request: Request, api_key: str = Depends(check_api_key)):
164+
x_exosphere_request_id = getattr(request.state, "x_exosphere_request_id", str(uuid4()))
165+
166+
if api_key:
167+
logger.info(f"API key is valid for namespace {namespace_name}", x_exosphere_request_id=x_exosphere_request_id)
168+
else:
169+
logger.error(f"API key is invalid for namespace {namespace_name}", x_exosphere_request_id=x_exosphere_request_id)
170+
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid API key")
171+
172+
return await prune_signal(namespace_name, PydanticObjectId(state_id), body, x_exosphere_request_id)
173+
174+
175+
@router.post(
176+
"/states/{state_id}/re-enqueue-after",
177+
response_model=SignalResponseModel,
178+
status_code=status.HTTP_200_OK,
179+
response_description="State re-enqueued successfully",
180+
tags=["state"]
181+
)
182+
async def re_enqueue_after_state_route(namespace_name: str, state_id: str, body: ReEnqueueAfterRequestModel, request: Request, api_key: str = Depends(check_api_key)):
183+
x_exosphere_request_id = getattr(request.state, "x_exosphere_request_id", str(uuid4()))
184+
185+
if api_key:
186+
logger.info(f"API key is valid for namespace {namespace_name}", x_exosphere_request_id=x_exosphere_request_id)
187+
else:
188+
logger.error(f"API key is invalid for namespace {namespace_name}", x_exosphere_request_id=x_exosphere_request_id)
189+
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid API key")
190+
191+
return await re_queue_after_signal(namespace_name, PydanticObjectId(state_id), body, x_exosphere_request_id)
192+
193+
148194
@router.put(
149195
"/graph/{graph_name}",
150196
response_model=UpsertGraphTemplateResponse,

0 commit comments

Comments
 (0)