Skip to content

Commit d41cae6

Browse files
committed
Merge branch 'niki/dev/statemanagerfrontend' of https://github.com/nk-ag/exospherehost into niki/dev/statemanagerfrontend
2 parents 1132a62 + eaad059 commit d41cae6

10 files changed

Lines changed: 1775 additions & 106 deletions

File tree

state-manager/app/controller/enqueue_states.py

Lines changed: 30 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,49 @@
1-
from beanie.operators import In
1+
import asyncio
22

33
from ..models.enqueue_request import EnqueueRequestModel
44
from ..models.enqueue_response import EnqueueResponseModel, StateModel
55
from ..models.db.state import State
66
from ..models.state_status_enum import StateStatusEnum
77

88
from app.singletons.logs_manager import LogsManager
9+
from pymongo import ReturnDocument
910

1011
logger = LogsManager().get_logger()
1112

1213

14+
async def find_state(namespace_name: str, nodes: list[str]) -> State | None:
15+
data = await State.get_pymongo_collection().find_one_and_update(
16+
{
17+
"namespace_name": namespace_name,
18+
"status": StateStatusEnum.CREATED,
19+
"node_name": {
20+
"$in": nodes
21+
}
22+
},
23+
{
24+
"$set": {"status": StateStatusEnum.QUEUED}
25+
},
26+
return_document=ReturnDocument.AFTER
27+
)
28+
return State(**data) if data else None
29+
1330
async def enqueue_states(namespace_name: str, body: EnqueueRequestModel, x_exosphere_request_id: str) -> EnqueueResponseModel:
1431

1532
try:
1633
logger.info(f"Enqueuing states for namespace {namespace_name}", x_exosphere_request_id=x_exosphere_request_id)
1734

18-
states = await State.find(
19-
State.namespace_name == namespace_name,
20-
State.status == StateStatusEnum.CREATED,
21-
In(State.node_name, body.nodes)
22-
).limit(
23-
body.batch_size
24-
).to_list()
25-
26-
if states:
27-
await State.find(
28-
In(State.id, [state.id for state in states])
29-
).set(
30-
{
31-
"status": StateStatusEnum.QUEUED,
32-
}
33-
) # type: ignore
35+
# Create tasks for parallel execution
36+
tasks = [find_state(namespace_name, body.nodes) for _ in range(body.batch_size)]
37+
results = await asyncio.gather(*tasks, return_exceptions=True)
38+
39+
# Filter out None results and exceptions
40+
states = []
41+
for result in results:
42+
if isinstance(result, Exception):
43+
logger.error(f"Error finding state: {result}", x_exosphere_request_id=x_exosphere_request_id)
44+
continue
45+
if result is not None:
46+
states.append(result)
3447

3548
response = EnqueueResponseModel(
3649
count=len(states),

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

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
1+
from pymongo import IndexModel
12
from .base import BaseDatabaseModel
23
from ..state_status_enum import StateStatusEnum
34
from pydantic import Field
4-
from beanie import PydanticObjectId
5+
from beanie import Insert, PydanticObjectId, Replace, Save, before_event
6+
from pymongo.results import InsertManyResult
57
from typing import Any, Optional
8+
import hashlib
9+
import json
610

711

812
class State(BaseDatabaseModel):
@@ -15,4 +19,51 @@ class State(BaseDatabaseModel):
1519
inputs: dict[str, Any] = Field(..., description="Inputs of the state")
1620
outputs: dict[str, Any] = Field(..., description="Outputs of the state")
1721
error: Optional[str] = Field(None, description="Error message")
18-
parents: dict[str, PydanticObjectId] = Field(default_factory=dict, description="Parents of the state")
22+
parents: dict[str, PydanticObjectId] = Field(default_factory=dict, description="Parents of the state")
23+
does_unites: bool = Field(default=False, description="Whether this state unites other states")
24+
state_fingerprint: str = Field(default="", description="Fingerprint of the state")
25+
26+
@before_event([Insert, Replace, Save])
27+
def _generate_fingerprint(self):
28+
if not self.does_unites:
29+
self.state_fingerprint = ""
30+
return
31+
32+
data = {
33+
"node_name": self.node_name,
34+
"namespace_name": self.namespace_name,
35+
"identifier": self.identifier,
36+
"graph_name": self.graph_name,
37+
"run_id": self.run_id,
38+
"parents": {k: str(v) for k, v in self.parents.items()},
39+
}
40+
payload = json.dumps(
41+
data,
42+
sort_keys=True, # canonical key ordering at all levels
43+
separators=(",", ":"), # no whitespace variance
44+
ensure_ascii=True, # normalized non-ASCII escapes
45+
).encode("utf-8")
46+
self.state_fingerprint = hashlib.sha256(payload).hexdigest()
47+
48+
@classmethod
49+
async def insert_many(cls, documents: list["State"]) -> InsertManyResult:
50+
"""Override insert_many to ensure fingerprints are generated before insertion."""
51+
# Generate fingerprints for states that need them
52+
for state in documents:
53+
state._generate_fingerprint()
54+
55+
return await super().insert_many(documents) # type: ignore
56+
57+
class Settings:
58+
indexes = [
59+
IndexModel(
60+
[
61+
("state_fingerprint", 1)
62+
],
63+
unique=True,
64+
name="uniq_state_fingerprint_unites",
65+
partialFilterExpression={
66+
"does_unites": True
67+
}
68+
)
69+
]

state-manager/app/tasks/create_next_states.py

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from beanie import PydanticObjectId
2+
from pymongo.errors import DuplicateKeyError, BulkWriteError
23
from beanie.operators import In, NE
34
from app.singletons.logs_manager import LogsManager
45
from app.models.db.graph_template_model import GraphTemplate
@@ -58,6 +59,7 @@ async def check_unites_satisfied(namespace: str, graph_name: str, node_template:
5859
return False
5960
return True
6061

62+
6163
def get_dependents(syntax_string: str) -> DependentString:
6264
splits = syntax_string.split("${{")
6365
if len(splits) <= 1:
@@ -134,6 +136,7 @@ def generate_next_state(next_state_input_model: Type[BaseModel], next_state_node
134136
parents=new_parents,
135137
inputs=next_state_input_data,
136138
outputs={},
139+
does_unites=next_state_node_template.unites is not None,
137140
run_id=current_state.run_id,
138141
error=None
139142
)
@@ -231,10 +234,17 @@ async def get_input_model(node_template: NodeTemplate) -> Type[BaseModel]:
231234
parent_state = parents[next_state_node_template.unites.identifier]
232235

233236
new_unit_states.append(generate_next_state(next_state_input_model, next_state_node_template, parents, parent_state))
234-
235-
if len(new_unit_states) > 0:
236-
await State.insert_many(new_unit_states)
237-
237+
238+
try:
239+
if len(new_unit_states) > 0:
240+
await State.insert_many(new_unit_states)
241+
except (DuplicateKeyError, BulkWriteError):
242+
logger.warning(
243+
f"Caught duplicate key error for new unit states in namespace={namespace}, "
244+
f"graph={graph_name}, likely due to a race condition. "
245+
f"Attempted to insert {len(new_unit_states)} states"
246+
)
247+
238248
except Exception as e:
239249
await State.find(
240250
In(State.id, state_ids)

0 commit comments

Comments
 (0)