33from typing import Any
44
55from django .conf import settings
6- from django .db .models import Q
6+ from django .db .models import F , Q
77from django .db .utils import IntegrityError
88from django .utils import timezone
99from utils .cache_service import CacheService
@@ -238,6 +238,61 @@ def _get_reprocessing_interval_from_config(
238238 workflow_log .log_error (logger = logger , message = error_msg )
239239 return None
240240
241+ @staticmethod
242+ def _safe_str (value : Any ) -> str :
243+ """Convert value to string, return empty string if None.
244+
245+ Args:
246+ value: Value to convert
247+
248+ Returns:
249+ str: String representation or empty string
250+ """
251+ return "" if value is None else str (value )
252+
253+ @staticmethod
254+ def _truncate_hash (file_hash : str | None ) -> str :
255+ """Truncate hash for logging purposes.
256+
257+ Args:
258+ file_hash: Hash string to truncate
259+
260+ Returns:
261+ str: Truncated hash (first 16 chars) or 'None' if missing
262+ """
263+ return file_hash [:16 ] if file_hash else "None"
264+
265+ @staticmethod
266+ def _increment_file_history (
267+ file_history : FileHistory ,
268+ status : ExecutionStatus ,
269+ result : Any ,
270+ metadata : str | None ,
271+ error : str | None ,
272+ ) -> FileHistory :
273+ """Update existing file history with incremented execution count.
274+
275+ Args:
276+ file_history: FileHistory instance to update
277+ status: New execution status
278+ result: Execution result
279+ metadata: Execution metadata
280+ error: Error message if any
281+
282+ Returns:
283+ FileHistory: Updated file history instance
284+ """
285+ FileHistory .objects .filter (id = file_history .id ).update (
286+ execution_count = F ("execution_count" ) + 1 ,
287+ status = status ,
288+ result = str (result ),
289+ metadata = FileHistoryHelper ._safe_str (metadata ),
290+ error = FileHistoryHelper ._safe_str (error ),
291+ )
292+ # Refresh from DB to get updated values
293+ file_history .refresh_from_db ()
294+ return file_history
295+
241296 @staticmethod
242297 def create_file_history (
243298 file_hash : FileHash ,
@@ -248,7 +303,11 @@ def create_file_history(
248303 error : str | None = None ,
249304 is_api : bool = False ,
250305 ) -> FileHistory :
251- """Create a new file history record or return existing one.
306+ """Create a new file history record or increment existing one's execution count.
307+
308+ This method implements execution count tracking:
309+ - If file history exists: increments execution_count atomically
310+ - If file history is new: creates with execution_count=1
252311
253312 Args:
254313 file_hash (FileHash): The file hash for the file.
@@ -260,44 +319,64 @@ def create_file_history(
260319 is_api (bool): Whether this is for API workflow (affects file_path handling).
261320
262321 Returns:
263- FileHistory: Either newly created or existing file history record.
322+ FileHistory: Either newly created or updated file history record.
264323 """
265324 file_path = file_hash .file_path if not is_api else None
266325
267- # Prepare data for creation
326+ # Check if file history already exists
327+ existing_history = FileHistoryHelper .get_file_history (
328+ workflow = workflow ,
329+ cache_key = file_hash .file_hash ,
330+ provider_file_uuid = file_hash .provider_file_uuid ,
331+ file_path = file_path ,
332+ )
333+
334+ if existing_history :
335+ # File history exists - increment execution count atomically
336+ updated_history = FileHistoryHelper ._increment_file_history (
337+ existing_history , status , result , metadata , error
338+ )
339+ logger .info (
340+ f"Updated FileHistory record (execution_count: { updated_history .execution_count } ) - "
341+ f"file_name='{ file_hash .file_name } ', file_path='{ file_hash .file_path } ', "
342+ f"file_hash='{ FileHistoryHelper ._truncate_hash (file_hash .file_hash )} ', "
343+ f"workflow={ workflow } "
344+ )
345+ return updated_history
346+
347+ # File history doesn't exist - create new record with execution_count=1
268348 create_data = {
269349 "workflow" : workflow ,
270350 "cache_key" : file_hash .file_hash ,
271351 "provider_file_uuid" : file_hash .provider_file_uuid ,
272352 "status" : status ,
273353 "result" : str (result ),
274- "metadata" : str (metadata ) if metadata else "" ,
275- "error" : str (error ) if error else "" ,
354+ "metadata" : FileHistoryHelper . _safe_str (metadata ),
355+ "error" : FileHistoryHelper . _safe_str (error ),
276356 "file_path" : file_path ,
357+ "execution_count" : 1 ,
277358 }
278359
279360 try :
280- # Try to create the file history record
281361 file_history = FileHistory .objects .create (** create_data )
282362 logger .info (
283- f"Created new FileHistory record - "
363+ f"Created new FileHistory record (execution_count: 1) - "
284364 f"file_name='{ file_hash .file_name } ', file_path='{ file_hash .file_path } ', "
285- f"file_hash='{ file_hash . file_hash [: 16 ] if file_hash .file_hash else 'None' } ', "
365+ f"file_hash='{ FileHistoryHelper . _truncate_hash ( file_hash .file_hash ) } ', "
286366 f"workflow={ workflow } "
287367 )
288368 return file_history
289369
290370 except IntegrityError as e :
291- # Race condition detected - another worker created the record
292- # Try to retrieve the existing record
371+ # Race condition: another worker created the record between our check and create
293372 logger .info (
294- f"FileHistory constraint violation (expected in concurrent environment ) - "
373+ f"FileHistory constraint violation (race condition ) - "
295374 f"file_name='{ file_hash .file_name } ', file_path='{ file_hash .file_path } ', "
296- f"file_hash='{ file_hash . file_hash [: 16 ] if file_hash .file_hash else 'None' } ', "
297- f"workflow={ workflow } . Error: { str ( e ) } "
375+ f"file_hash='{ FileHistoryHelper . _truncate_hash ( file_hash .file_hash ) } ', "
376+ f"workflow={ workflow } . Error: { e !s } "
298377 )
299378
300- # Use the existing get_file_history method to retrieve the record
379+ # Retrieve the record created by another worker and increment it
301380 existing_record = FileHistoryHelper .get_file_history (
302381 workflow = workflow ,
303382 cache_key = file_hash .file_hash ,
@@ -306,18 +385,22 @@ def create_file_history(
306385 )
307386
308387 if existing_record :
309- logger . info (
310- f"Retrieved existing FileHistory record after constraint violation - "
311- f"ID: { existing_record . id } , workflow= { workflow } "
388+ # Increment the existing record
389+ updated_record = FileHistoryHelper . _increment_file_history (
390+ existing_record , status , result , metadata , error
312391 )
313- return existing_record
314- else :
315- # This should rarely happen, but if we can't find the existing record,
316- # log the issue and re-raise the original exception
317- logger .error (
318- f"Failed to retrieve existing FileHistory record after constraint violation - "
319- f"file_name='{ file_hash .file_name } ', workflow={ workflow } "
392+ logger .info (
393+ f"Retrieved and updated existing FileHistory record (execution_count: { updated_record .execution_count } ) - "
394+ f"ID: { updated_record .id } , workflow={ workflow } "
320395 )
396+ return updated_record
397+
398+ # This should rarely happen - existing record not found after IntegrityError
399+ logger .exception (
400+ f"Failed to retrieve existing FileHistory record after constraint violation - "
401+ f"file_name='{ file_hash .file_name } ', workflow={ workflow } "
402+ )
403+ raise
321404
322405 @staticmethod
323406 def clear_history_for_workflow (
0 commit comments