Fix: Resolve workflow_node_execution primary key conflicts with UUID v7 (#24643)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
@@ -7,9 +7,12 @@ import logging
|
||||
from collections.abc import Sequence
|
||||
from typing import Optional, Union
|
||||
|
||||
import psycopg2.errors
|
||||
from sqlalchemy import UnaryExpression, asc, desc, select
|
||||
from sqlalchemy.engine import Engine
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from tenacity import before_sleep_log, retry, retry_if_exception, stop_after_attempt
|
||||
|
||||
from core.model_runtime.utils.encoders import jsonable_encoder
|
||||
from core.workflow.entities.workflow_node_execution import (
|
||||
@@ -21,6 +24,7 @@ from core.workflow.nodes.enums import NodeType
|
||||
from core.workflow.repositories.workflow_node_execution_repository import OrderConfig, WorkflowNodeExecutionRepository
|
||||
from core.workflow.workflow_type_encoder import WorkflowRuntimeTypeConverter
|
||||
from libs.helper import extract_tenant_id
|
||||
from libs.uuid_utils import uuidv7
|
||||
from models import (
|
||||
Account,
|
||||
CreatorUserRole,
|
||||
@@ -186,18 +190,31 @@ class SQLAlchemyWorkflowNodeExecutionRepository(WorkflowNodeExecutionRepository)
|
||||
db_model.finished_at = domain_model.finished_at
|
||||
return db_model
|
||||
|
||||
def _is_duplicate_key_error(self, exception: BaseException) -> bool:
|
||||
"""Check if the exception is a duplicate key constraint violation."""
|
||||
return isinstance(exception, IntegrityError) and isinstance(exception.orig, psycopg2.errors.UniqueViolation)
|
||||
|
||||
def _regenerate_id_on_duplicate(
|
||||
self, execution: WorkflowNodeExecution, db_model: WorkflowNodeExecutionModel
|
||||
) -> None:
|
||||
"""Regenerate UUID v7 for both domain and database models when duplicate key detected."""
|
||||
new_id = str(uuidv7())
|
||||
logger.warning(
|
||||
"Duplicate key conflict for workflow node execution ID %s, generating new UUID v7: %s", db_model.id, new_id
|
||||
)
|
||||
db_model.id = new_id
|
||||
execution.id = new_id
|
||||
|
||||
def save(self, execution: WorkflowNodeExecution) -> None:
|
||||
"""
|
||||
Save or update a NodeExecution domain entity to the database.
|
||||
|
||||
This method serves as a domain-to-database adapter that:
|
||||
1. Converts the domain entity to its database representation
|
||||
2. Persists the database model using SQLAlchemy's merge operation
|
||||
2. Checks for existing records and updates or inserts accordingly
|
||||
3. Maintains proper multi-tenancy by including tenant context during conversion
|
||||
4. Updates the in-memory cache for faster subsequent lookups
|
||||
|
||||
The method handles both creating new records and updating existing ones through
|
||||
SQLAlchemy's merge operation.
|
||||
5. Handles duplicate key conflicts by retrying with a new UUID v7
|
||||
|
||||
Args:
|
||||
execution: The NodeExecution domain entity to persist
|
||||
@@ -205,19 +222,62 @@ class SQLAlchemyWorkflowNodeExecutionRepository(WorkflowNodeExecutionRepository)
|
||||
# Convert domain model to database model using tenant context and other attributes
|
||||
db_model = self.to_db_model(execution)
|
||||
|
||||
# Create a new database session
|
||||
with self._session_factory() as session:
|
||||
# SQLAlchemy merge intelligently handles both insert and update operations
|
||||
# based on the presence of the primary key
|
||||
session.merge(db_model)
|
||||
session.commit()
|
||||
# Use tenacity for retry logic with duplicate key handling
|
||||
@retry(
|
||||
stop=stop_after_attempt(3),
|
||||
retry=retry_if_exception(self._is_duplicate_key_error),
|
||||
before_sleep=before_sleep_log(logger, logging.WARNING),
|
||||
reraise=True,
|
||||
)
|
||||
def _save_with_retry():
|
||||
try:
|
||||
self._persist_to_database(db_model)
|
||||
except IntegrityError as e:
|
||||
if self._is_duplicate_key_error(e):
|
||||
# Generate new UUID and retry
|
||||
self._regenerate_id_on_duplicate(execution, db_model)
|
||||
raise # Let tenacity handle the retry
|
||||
else:
|
||||
# Different integrity error, don't retry
|
||||
logger.exception("Non-duplicate key integrity error while saving workflow node execution")
|
||||
raise
|
||||
|
||||
# Update the in-memory cache for faster subsequent lookups
|
||||
# Only cache if we have a node_execution_id to use as the cache key
|
||||
try:
|
||||
_save_with_retry()
|
||||
|
||||
# Update the in-memory cache after successful save
|
||||
if db_model.node_execution_id:
|
||||
logger.debug("Updating cache for node_execution_id: %s", db_model.node_execution_id)
|
||||
self._node_execution_cache[db_model.node_execution_id] = db_model
|
||||
|
||||
except Exception as e:
|
||||
logger.exception("Failed to save workflow node execution after all retries")
|
||||
raise
|
||||
|
||||
def _persist_to_database(self, db_model: WorkflowNodeExecutionModel) -> None:
|
||||
"""
|
||||
Persist the database model to the database.
|
||||
|
||||
Checks if a record with the same ID exists and either updates it or creates a new one.
|
||||
|
||||
Args:
|
||||
db_model: The database model to persist
|
||||
"""
|
||||
with self._session_factory() as session:
|
||||
# Check if record already exists
|
||||
existing = session.get(WorkflowNodeExecutionModel, db_model.id)
|
||||
|
||||
if existing:
|
||||
# Update existing record by copying all non-private attributes
|
||||
for key, value in db_model.__dict__.items():
|
||||
if not key.startswith("_"):
|
||||
setattr(existing, key, value)
|
||||
else:
|
||||
# Add new record
|
||||
session.add(db_model)
|
||||
|
||||
session.commit()
|
||||
|
||||
def get_db_models_by_workflow_run(
|
||||
self,
|
||||
workflow_run_id: str,
|
||||
|
@@ -2,7 +2,6 @@ from collections.abc import Mapping
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from typing import Any, Optional, Union
|
||||
from uuid import uuid4
|
||||
|
||||
from core.app.entities.app_invoke_entities import AdvancedChatAppGenerateEntity, WorkflowAppGenerateEntity
|
||||
from core.app.entities.queue_entities import (
|
||||
@@ -29,6 +28,7 @@ from core.workflow.repositories.workflow_node_execution_repository import Workfl
|
||||
from core.workflow.system_variable import SystemVariable
|
||||
from core.workflow.workflow_entry import WorkflowEntry
|
||||
from libs.datetime_utils import naive_utc_now
|
||||
from libs.uuid_utils import uuidv7
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -266,7 +266,7 @@ class WorkflowCycleManager:
|
||||
"""Get execution ID from system variables or generate a new one."""
|
||||
if self._workflow_system_variables and self._workflow_system_variables.workflow_execution_id:
|
||||
return str(self._workflow_system_variables.workflow_execution_id)
|
||||
return str(uuid4())
|
||||
return str(uuidv7())
|
||||
|
||||
def _save_and_cache_workflow_execution(self, execution: WorkflowExecution) -> WorkflowExecution:
|
||||
"""Save workflow execution to repository and cache it."""
|
||||
@@ -371,7 +371,7 @@ class WorkflowCycleManager:
|
||||
}
|
||||
|
||||
domain_execution = WorkflowNodeExecution(
|
||||
id=str(uuid4()),
|
||||
id=str(uuidv7()),
|
||||
workflow_id=workflow_execution.workflow_id,
|
||||
workflow_execution_id=workflow_execution.id_,
|
||||
predecessor_node_id=event.predecessor_node_id,
|
||||
|
Reference in New Issue
Block a user