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:
-LAN-
2025-09-02 14:18:29 +08:00
committed by GitHub
parent 067b0d07c4
commit a32dde5428
4 changed files with 325 additions and 20 deletions

View File

@@ -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,

View File

@@ -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,