@@ -41,8 +41,6 @@ def default(self, obj):
4141
4242# Сет для отслеживания активных экспериментов
4343active_experiments : Set [str ] = set ()
44- # Блокировка для предотвращения race conditions при работе с active_experiments
45- active_experiments_lock = asyncio .Lock ()
4644
4745
4846# Функция для преобразования документа MongoDB в JSON-сериализуемый формат
@@ -318,132 +316,66 @@ async def get_experiment_stats_optimized(experiment_id: str, db) -> Dict:
318316 }
319317
320318
321- async def check_and_update_group_status (experiment_id : str , db ):
322- """Проверяет и обновляет статус группы экспериментов после завершения эксперимента.
323-
324- Находит все группы, содержащие завершившийся эксперимент, и проверяет,
325- завершились ли все эксперименты в группе. Если да, обновляет статус группы на COMPLETED.
319+ async def check_and_update_group_status (experiment_id : str ):
320+ """
321+ Checks if all experiments in the group containing the given experiment_id
322+ are completed or failed. If so, updates the group status to COMPLETED.
326323 """
327324 try :
328- # Находим все группы, содержащие этот эксперимент
329- # В MongoDB запрос {"experiment_ids": value} находит документы, где value содержится в массиве
330- groups = await db .groups .find ({
331- "experiment_ids" : experiment_id ,
332- "state" : ExperimentState .RUNNING # Проверяем только запущенные группы
333- }).to_list (None )
334-
335- if not groups :
336- logger .debug (f"No running groups found for experiment { experiment_id } " )
337- return
325+ db = await get_database ()
338326
339- logger .info (f"Found { len (groups )} running group(s) containing experiment { experiment_id } " )
327+ # Find groups that contain this experiment
328+ cursor = db .groups .find ({"experiment_ids" : experiment_id })
340329
341- for group in groups :
342- group_id = str ( group ["_id" ])
330+ async for group in cursor :
331+ group_id = group ["_id" ]
343332 experiment_ids = group .get ("experiment_ids" , [])
344333
345334 if not experiment_ids :
346- logger .warning (f"Group { group_id } has no experiment_ids" )
347335 continue
348336
349- logger .debug (f"Checking group { group_id } with { len (experiment_ids )} experiments" )
350-
351- # Проверяем статусы всех экспериментов в группе
352- all_finished = True
337+ # Convert string IDs to ObjectIds
338+ exp_object_ids = []
339+ for eid in experiment_ids :
340+ if ObjectId .is_valid (eid ):
341+ exp_object_ids .append (ObjectId (eid ))
353342
354- for exp_id in experiment_ids :
355- try :
356- if not ObjectId .is_valid (exp_id ):
357- logger .warning (f"Invalid experiment ID format in group { group_id } : { exp_id } " )
358- all_finished = False
359- break
360-
361- experiment = await db .experiments .find_one ({"_id" : ObjectId (exp_id )})
362- if not experiment :
363- logger .warning (f"Experiment { exp_id } not found in database for group { group_id } " )
364- all_finished = False
365- break
366-
367- exp_state = experiment .get ("state" )
368-
369- # Если хотя бы один эксперимент еще выполняется или приостановлен, группа не завершена
370- if exp_state in [ExperimentState .RUNNING , ExperimentState .PENDING , ExperimentState .PAUSED ]:
371- all_finished = False
372- logger .debug (f"Group { group_id } not finished: experiment { exp_id } is { exp_state } " )
373- break
374-
375- except Exception as exp_error :
376- logger .error (f"Error checking experiment { exp_id } in group { group_id } : { str (exp_error )} " )
377- all_finished = False
378- break
343+ if not exp_object_ids :
344+ continue
345+
346+ # Count experiments that are NOT completed or failed
347+ unfinished_count = await db .experiments .count_documents ({
348+ "_id" : {"$in" : exp_object_ids },
349+ "state" : {"$nin" : [ExperimentState .COMPLETED , ExperimentState .FAILED ]}
350+ })
379351
380- # Если все эксперименты завершены (completed или failed), обновляем статус группы
381- if all_finished :
382- try :
383- await with_retry (
384- db .groups .update_one ,
385- {"_id" : ObjectId (group_id )},
386- {"$set" : {"state" : ExperimentState .COMPLETED , "updated_at" : datetime .now ()}}
387- )
388- logger .info (f"Group { group_id } status updated to COMPLETED - all experiments finished" )
389- except Exception as update_error :
390- logger .error (f"Failed to update group { group_id } status: { str (update_error )} " )
391- else :
392- logger .debug (f"Group { group_id } not yet completed - some experiments still running" )
393-
352+ if unfinished_count == 0 :
353+ logger .info (f"All experiments in group { group_id } are finished. Marking group as COMPLETED." )
354+ await db .groups .update_one (
355+ {"_id" : group_id },
356+ {"$set" : {"state" : ExperimentState .COMPLETED }}
357+ )
358+
394359 except Exception as e :
395- logger .error (f"Error checking group status for experiment { experiment_id } : { str (e )} " , exc_info = True )
396- # Не прерываем выполнение, если проверка группы не удалась
360+ logger .error (f"Error checking group status for experiment { experiment_id } : { str (e )} " )
397361
398362
399363async def run_experiment (experiment_id : str ):
400364 """Фоновая задача для выполнения эксперимента с динамической нагрузкой."""
401- # Используем блокировку для предотвращения race conditions
402- async with active_experiments_lock :
403- if experiment_id in active_experiments :
404- logger .warning (f"Experiment { experiment_id } is already running" )
405- return
406-
407- # Проверяем статус эксперимента в БД перед запуском
408- try :
409- db = await get_database ()
410- experiment = await db .experiments .find_one ({"_id" : ObjectId (experiment_id )})
411- if not experiment :
412- logger .error (f"Experiment { experiment_id } not found" )
413- return
414-
415- # Если эксперимент уже завершен или провалился, не запускаем его снова
416- exp_state = experiment .get ("state" )
417- if exp_state in [ExperimentState .COMPLETED , ExperimentState .FAILED ]:
418- logger .info (f"Experiment { experiment_id } is already { exp_state } , skipping" )
419- return
420-
421- # Если эксперимент уже выполняется (по статусу в БД), не запускаем его снова
422- if exp_state == ExperimentState .RUNNING :
423- logger .warning (f"Experiment { experiment_id } is already running (status in DB)" )
424- return
425-
426- except Exception as e :
427- logger .error (f"Error checking experiment { experiment_id } status: { str (e )} " )
428- return
429-
430- # Добавляем в активные эксперименты только после всех проверок
431- active_experiments .add (experiment_id )
432-
433- # Инициализация переменных вне блока блокировки
365+ if experiment_id in active_experiments :
366+ logger .warning (f"Experiment { experiment_id } is already running" )
367+ return
368+
369+ active_experiments .add (experiment_id )
434370 semaphore = asyncio .Semaphore (MAX_CONCURRENT_TASKS )
435371 save_semaphore = asyncio .Semaphore (1 ) # Семафор для сохранения результатов
436372
437373 try :
438- # Получаем эксперимент еще раз для работы
439374 db = await get_database ()
440- experiment = await with_retry (_get_experiment_from_db , experiment_id , db )
375+ experiment = await with_retry (get_experiment_by_id , experiment_id , db )
441376
442377 if not experiment :
443378 logger .error (f"Experiment { experiment_id } not found" )
444- # Удаляем из активных, если эксперимент не найден
445- async with active_experiments_lock :
446- active_experiments .discard (experiment_id )
447379 return
448380
449381 settings = experiment ["settings" ]
@@ -622,18 +554,9 @@ async def run_experiment(experiment_id: str):
622554 except Exception as update_error :
623555 logger .error (f"Failed to update experiment state: { str (update_error )} " )
624556 finally :
625- # Проверяем и обновляем статус группы экспериментов после завершения эксперимента
626- # Это выполнится независимо от того, как завершился эксперимент (успешно, с ошибкой или отменен)
627- if db is not None :
628- try :
629- await check_and_update_group_status (experiment_id , db )
630- except Exception as group_check_error :
631- # Логируем ошибку, но не прерываем выполнение
632- logger .error (f"Failed to check group status in finally block: { str (group_check_error )} " )
633-
634- # Удаляем эксперимент из активных с блокировкой
635- async with active_experiments_lock :
636- active_experiments .discard (experiment_id ) # Используем discard вместо remove для безопасности
557+ active_experiments .remove (experiment_id )
558+ # Check and update group status
559+ await check_and_update_group_status (experiment_id )
637560
638561
639562@router .get ("/experiment_stats" )
@@ -887,73 +810,20 @@ async def manage_group(
887810 if not group :
888811 raise HTTPException (status_code = 404 , detail = "Group not found" )
889812
890- current_group_state = group .get ("state" , ExperimentState .PENDING )
891-
892813 new_state = ""
893814 if state == "start" :
894- # Проверяем, не завершена ли уже группа
895- if current_group_state == ExperimentState .COMPLETED :
896- logger .warning (f"Group { group_id } is already completed, cannot start" )
897- raise HTTPException (
898- status_code = 400 ,
899- detail = "Group is already completed and cannot be started again"
900- )
901-
902- # Проверяем, не запущена ли уже группа
903- if current_group_state == ExperimentState .RUNNING :
904- logger .warning (f"Group { group_id } is already running" )
905- # Возвращаем текущий статус без повторного запуска
906- return {"_id" : group_id , "state" : current_group_state , "message" : "Group is already running" }
907-
908815 new_state = ExperimentState .RUNNING
909-
910- # Запускаем только те эксперименты, которые еще не завершены и не запущены
911- experiment_ids = group .get ("experiment_ids" , [])
912- started_count = 0
913- skipped_count = 0
914-
915- for exp_id in experiment_ids :
916- try :
917- # Проверяем статус эксперимента перед запуском
918- experiment = await db .experiments .find_one ({"_id" : ObjectId (exp_id )})
919- if not experiment :
920- logger .warning (f"Experiment { exp_id } not found, skipping" )
921- skipped_count += 1
922- continue
923-
924- exp_state = experiment .get ("state" )
925-
926- # Пропускаем уже завершенные или провалившиеся эксперименты
927- if exp_state in [ExperimentState .COMPLETED , ExperimentState .FAILED ]:
928- logger .debug (f"Experiment { exp_id } is already { exp_state } , skipping" )
929- skipped_count += 1
930- continue
931-
932- # Проверяем, не запущен ли уже эксперимент
933- async with active_experiments_lock :
934- if exp_id in active_experiments :
935- logger .debug (f"Experiment { exp_id } is already running, skipping" )
936- skipped_count += 1
937- continue
938-
939- # Запускаем эксперимент
940- asyncio .create_task (run_experiment (exp_id ))
941- started_count += 1
942-
943- except Exception as e :
944- logger .error (f"Error starting experiment { exp_id } : { str (e )} " )
945- skipped_count += 1
946-
947- logger .info (f"Group { group_id } : started { started_count } experiments, skipped { skipped_count } " )
948-
816+ # Запускаем все эксперименты в группе
817+ for exp_id in group ["experiment_ids" ]:
818+ asyncio .create_task (run_experiment (exp_id ))
949819 elif state == "pause" :
950820 new_state = ExperimentState .PAUSED
951821 elif state == "stop" :
952822 new_state = ExperimentState .COMPLETED
953823
954824 await db .groups .update_one (
955825 {"_id" : ObjectId (group_id )},
956- {"$set" : {"state" : new_state , "updated_at" : datetime . now () }}
826+ {"$set" : {"state" : new_state }}
957827 )
958828
959829 return {"_id" : group_id , "state" : new_state }
0 commit comments