Skip to content

Commit 8f7bcc1

Browse files
committed
Implement retry policy and enhance errored state handling
- Added a retry policy model to manage state retries with configurable methods (fixed, linear, exponential). - Updated the errored state function to create a retry state if the maximum retries have not been reached, improving error recovery. - Enhanced the ErroredResponseModel to include a flag indicating whether a retry state was created. - Modified the GraphTemplate and State models to incorporate retry policy attributes, ensuring better state management. - Improved validation and error handling in the upsert_graph_template function to accommodate the new retry policy structure.
1 parent fb60376 commit 8f7bcc1

7 files changed

Lines changed: 64 additions & 5 deletions

File tree

state-manager/app/controller/errored_state.py

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,22 @@
55
from app.models.db.state import State
66
from app.models.state_status_enum import StateStatusEnum
77
from app.singletons.logs_manager import LogsManager
8+
from app.models.retry_policy_model import RetryPolicyModel, RetryMethod
9+
from app.models.db.graph_template_model import GraphTemplate
810

911
logger = LogsManager().get_logger()
1012

13+
def _calculate_enqueue_after(retry_policy: RetryPolicyModel, retry_count: int) -> int:
14+
# convert seconds to milliseconds
15+
if retry_policy.method == RetryMethod.FIXED:
16+
return (retry_policy.backoff_factor * 1000)
17+
elif retry_policy.method == RetryMethod.LINEAR:
18+
return (retry_policy.backoff_factor * retry_count) * 1000
19+
elif retry_policy.method == RetryMethod.EXPONENTIAL:
20+
return (retry_policy.backoff_factor ** retry_count) * 1000
21+
else:
22+
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid retry method")
23+
1124
async def errored_state(namespace_name: str, state_id: PydanticObjectId, body: ErroredRequestModel, x_exosphere_request_id: str) -> ErroredResponseModel:
1225

1326
try:
@@ -23,11 +36,35 @@ async def errored_state(namespace_name: str, state_id: PydanticObjectId, body: E
2336
if state.status == StateStatusEnum.EXECUTED:
2437
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="State is already executed")
2538

39+
graph_template = await GraphTemplate.get(namespace_name, state.graph_name)
40+
41+
retry_created = False
42+
43+
if state.retry_count < graph_template.retry_policy.max_retries:
44+
retry_state = State(
45+
node_name=state.node_name,
46+
namespace_name=state.namespace_name,
47+
identifier=state.identifier,
48+
graph_name=state.graph_name,
49+
run_id=state.run_id,
50+
status=StateStatusEnum.CREATED,
51+
inputs=state.inputs,
52+
outputs=state.outputs,
53+
error=body.error,
54+
parents=state.parents,
55+
does_unites=state.does_unites,
56+
enqueue_after=state.enqueue_after + _calculate_enqueue_after(graph_template.retry_policy, state.retry_count + 1),
57+
retry_count=state.retry_count + 1
58+
)
59+
retry_state = await retry_state.insert()
60+
logger.info(f"Retry state {retry_state.id} created for state {state_id}", x_exosphere_request_id=x_exosphere_request_id)
61+
retry_created = True
62+
2663
state.status = StateStatusEnum.ERRORED
2764
state.error = body.error
2865
await state.save()
2966

30-
return ErroredResponseModel(status=StateStatusEnum.ERRORED)
67+
return ErroredResponseModel(status=StateStatusEnum.ERRORED, retry_created=retry_created)
3168

3269
except Exception as e:
3370
logger.error(f"Error errored state {state_id} for namespace {namespace_name}", x_exosphere_request_id=x_exosphere_request_id, error=e)

state-manager/app/controller/upsert_graph_template.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ async def upsert_graph_template(namespace_name: str, graph_name: str, body: Upse
2727
Set({
2828
GraphTemplate.nodes: body.nodes, # type: ignore
2929
GraphTemplate.validation_status: GraphTemplateValidationStatus.PENDING, # type: ignore
30-
GraphTemplate.validation_errors: [] # type: ignore
30+
GraphTemplate.validation_errors: [], # type: ignore
31+
GraphTemplate.retry_policy: body.retry_policy # type: ignore
3132
})
3233
)
3334

@@ -44,7 +45,8 @@ async def upsert_graph_template(namespace_name: str, graph_name: str, body: Upse
4445
namespace=namespace_name,
4546
nodes=body.nodes,
4647
validation_status=GraphTemplateValidationStatus.PENDING,
47-
validation_errors=[]
48+
validation_errors=[],
49+
retry_policy=body.retry_policy
4850
).set_secrets(body.secrets)
4951
)
5052
except ValueError as e:
@@ -58,6 +60,7 @@ async def upsert_graph_template(namespace_name: str, graph_name: str, body: Upse
5860
validation_status=graph_template.validation_status,
5961
validation_errors=graph_template.validation_errors,
6062
secrets={secret_name: True for secret_name in graph_template.get_secrets().keys()},
63+
retry_policy=graph_template.retry_policy,
6164
created_at=graph_template.created_at,
6265
updated_at=graph_template.updated_at
6366
)

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from ..node_template_model import NodeTemplate
1212
from app.utils.encrypter import get_encrypter
1313
from app.models.dependent_string import DependentString
14-
14+
from app.models.retry_policy_model import RetryPolicyModel
1515

1616
class GraphTemplate(BaseDatabaseModel):
1717
name: str = Field(..., description="Name of the graph")
@@ -20,6 +20,7 @@ class GraphTemplate(BaseDatabaseModel):
2020
validation_status: GraphTemplateValidationStatus = Field(..., description="Validation status of the graph")
2121
validation_errors: List[str] = Field(default_factory=list, description="Validation errors of the graph")
2222
secrets: Dict[str, str] = Field(default_factory=dict, description="Secrets of the graph")
23+
retry_policy: RetryPolicyModel = Field(default_factory=RetryPolicyModel, description="Retry policy of the graph")
2324

2425
_node_by_identifier: Dict[str, NodeTemplate] | None = PrivateAttr(default=None)
2526
_parents_by_identifier: Dict[str, set[str]] | None = PrivateAttr(default=None) # type: ignore

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ class State(BaseDatabaseModel):
2424
does_unites: bool = Field(default=False, description="Whether this state unites other states")
2525
state_fingerprint: str = Field(default="", description="Fingerprint of the state")
2626
enqueue_after: int = Field(default_factory=lambda: int(time.time() * 1000), gt=0, description="Unix time in milliseconds after which the state should be enqueued")
27+
retry_count: int = Field(default=0, description="Number of times the state has been retried")
2728

2829
@before_event([Insert, Replace, Save])
2930
def _generate_fingerprint(self):
@@ -37,6 +38,7 @@ def _generate_fingerprint(self):
3738
"identifier": self.identifier,
3839
"graph_name": self.graph_name,
3940
"run_id": self.run_id,
41+
"retry_count": self.retry_count,
4042
"parents": {k: str(v) for k, v in self.parents.items()},
4143
}
4244
payload = json.dumps(

state-manager/app/models/errored_models.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,5 @@ class ErroredRequestModel(BaseModel):
77

88

99
class ErroredResponseModel(BaseModel):
10-
status: StateStatusEnum = Field(..., description="Status of the state")
10+
status: StateStatusEnum = Field(..., description="Status of the state")
11+
retry_created: bool = Field(default=False, description="Whether a retry state was created")

state-manager/app/models/graph_models.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,19 @@
33
from typing import Dict, List, Optional
44
from datetime import datetime
55
from .graph_template_validation_status import GraphTemplateValidationStatus
6+
from .retry_policy_model import RetryPolicyModel
67

78

89
class UpsertGraphTemplateRequest(BaseModel):
910
secrets: Dict[str, str] = Field(..., description="Dictionary of secrets that are used while graph execution")
1011
nodes: List[NodeTemplate] = Field(..., description="List of node templates that define the graph structure")
12+
retry_policy: RetryPolicyModel = Field(default_factory=RetryPolicyModel, description="Retry policy of the graph")
1113

1214

1315
class UpsertGraphTemplateResponse(BaseModel):
1416
nodes: List[NodeTemplate] = Field(..., description="List of node templates that define the graph structure")
1517
secrets: Dict[str, bool] = Field(..., description="Dictionary of secrets that are used while graph execution")
18+
retry_policy: RetryPolicyModel = Field(default_factory=RetryPolicyModel, description="Retry policy of the graph")
1619
created_at: datetime = Field(..., description="Timestamp when the graph template was created")
1720
updated_at: datetime = Field(..., description="Timestamp when the graph template was last updated")
1821
validation_status: GraphTemplateValidationStatus = Field(..., description="Current validation status of the graph template")
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
from pydantic import BaseModel, Field
2+
from enum import Enum
3+
4+
class RetryMethod(str, Enum):
5+
EXPONENTIAL = "EXPONENTIAL"
6+
LINEAR = "LINEAR"
7+
FIXED = "FIXED"
8+
9+
class RetryPolicyModel(BaseModel):
10+
max_retries: int = Field(default=3, description="The maximum number of retries", ge=0)
11+
method: RetryMethod = Field(default=RetryMethod.EXPONENTIAL, description="The method of retry")
12+
backoff_factor: int = Field(default=2, description="The backoff factor in seconds (default: 2 = 2 seconds)", gt=0)

0 commit comments

Comments
 (0)