Skip to content

Commit 2e77cc5

Browse files
authored
Merge pull request #4 from adeptvin1/fix/revert-and-apply-fixes
Fix/revert and apply fixes
2 parents 8edcb8c + 9b0105a commit 2e77cc5

2 files changed

Lines changed: 43 additions & 535 deletions

File tree

client/api/endpoints.py

Lines changed: 43 additions & 173 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,6 @@ def default(self, obj):
4141

4242
# Сет для отслеживания активных экспериментов
4343
active_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

399363
async 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

Comments
 (0)