Skip to content
Merged
37 changes: 37 additions & 0 deletions services/press-defect-data-simulator-service/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
FROM python:3.10

# 작업 디렉토리 설정
WORKDIR /app

# 시스템 패키지 업데이트 및 필요한 패키지 설치
RUN apt-get update && apt-get install -y \
gcc \
g++ \
&& rm -rf /var/lib/apt/lists/*

# Python 의존성 파일 복사
COPY requirements.txt .

# Python 패키지 설치
RUN pip install --no-cache-dir --upgrade pip && \
pip install --no-cache-dir -r requirements.txt

# 애플리케이션 코드 복사
COPY ./app ./app

# 로그 디렉토리 생성
RUN mkdir -p /app/logs /app/logs/simulation /app/logs/errors /app/logs/results

# 포트 8013 노출
EXPOSE 8013

# 환경 변수 설정
ENV PYTHONPATH=/app
ENV PYTHONUNBUFFERED=1

# 헬스체크 설정
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
CMD curl -f http://localhost:8013/health || exit 1

Comment on lines +32 to +35
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

HEALTHCHECK will fail: curl isn’t installed

The image never installs curl, yet the HEALTHCHECK uses curl. Either install curl or switch to wget/python.

 RUN apt-get update && apt-get install -y \
-    gcc \
-    g++ \
+    gcc \
+    g++ \
+    curl \
     && rm -rf /var/lib/apt/lists/*

Alternatively:

-HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
-    CMD curl -f http://localhost:8013/health || exit 1
+HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
+    CMD python - <<'PY' || exit 1
+import sys, urllib.request
+try:
+    with urllib.request.urlopen("http://localhost:8013/health", timeout=8) as r:
+        sys.exit(0 if r.status == 200 else 1)
+except Exception:
+    sys.exit(1)
+PY
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# 헬스체크 설정
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
CMD curl -f http://localhost:8013/health || exit 1
# Install necessary build tools (and curl for the healthcheck)
RUN apt-get update && apt-get install -y \
gcc \
g++ \
curl \
&& rm -rf /var/lib/apt/lists/*
# 헬스체크 설정
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
CMD curl -f http://localhost:8013/health || exit 1
🤖 Prompt for AI Agents
In services/press-defect-data-simulator-service/Dockerfile around lines 32-35
the HEALTHCHECK uses curl but the image never installs curl; either install curl
in the image or change the HEALTHCHECK to use an available tool. Fix by adding a
package install step for curl in the Dockerfile matching the base image (e.g.,
for Alpine add apk add --no-cache curl; for Debian/Ubuntu add apt-get update &&
apt-get install -y curl && rm -rf /var/lib/apt/lists/*), or replace the
HEALTHCHECK command with an equivalent using wget or a small python one-liner if
python is present.

# 애플리케이션 실행
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8013"]
142 changes: 142 additions & 0 deletions services/press-defect-data-simulator-service/app/config/settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import os
from typing import Optional
from pydantic_settings import BaseSettings
from dotenv import load_dotenv

# .env 파일 로드
load_dotenv()

class Settings(BaseSettings):
"""애플리케이션 설정"""

# 서비스 기본 정보
service_name: str = "Painting Process Data Simulator"
service_version: str = "1.0.0"
service_description: str = "자동차 부품 프레스 구멍 검사 데이터 시뮬레이터"

# Azure Storage 설정
azure_connection_string: str = os.getenv("AZURE_CONNECTION_STRING")
azure_container_name: str = "simulator-data"
azure_press_defect_path: str = "press-defect"

# 모델 서비스 설정
model_service_url: str = os.getenv("MODEL_SERVICE_URL", "http://127.0.0.1:8000")
model_service_predict_endpoint: str = "/predict/inspection"
model_service_health_endpoint: str = "/health"
model_service_timeout: int = 300 # 5분 (21장 이미지 처리)
Comment on lines +18 to +26
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Avoid os.getenv in field defaults; let Pydantic Settings load env vars with type safety

Using os.getenv at import time bypasses pydantic-settings’ env loading and can yield type-mismatch defaults (None for str). Use Field(..., env="ENV_NAME") and proper Optional types.

-    azure_connection_string: str = os.getenv("AZURE_CONNECTION_STRING")
+    azure_connection_string: Optional[str] = Field(default=None, env="AZURE_CONNECTION_STRING")
@@
-    model_service_url: str = os.getenv("MODEL_SERVICE_URL", "http://127.0.0.1:8000")
+    model_service_url: str = Field(default="http://127.0.0.1:8000", env="MODEL_SERVICE_URL")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
azure_connection_string: str = os.getenv("AZURE_CONNECTION_STRING")
azure_container_name: str = "simulator-data"
azure_press_defect_path: str = "press-defect"
# 모델 서비스 설정
model_service_url: str = os.getenv("MODEL_SERVICE_URL", "http://127.0.0.1:8000")
model_service_predict_endpoint: str = "/predict/inspection"
model_service_health_endpoint: str = "/health"
model_service_timeout: int = 300 # 5분 (21장 이미지 처리)
azure_connection_string: Optional[str] = Field(default=None, env="AZURE_CONNECTION_STRING")
azure_container_name: str = "simulator-data"
azure_press_defect_path: str = "press-defect"
# 모델 서비스 설정
model_service_url: str = Field(default="http://127.0.0.1:8000", env="MODEL_SERVICE_URL")
model_service_predict_endpoint: str = "/predict/inspection"
model_service_health_endpoint: str = "/health"
model_service_timeout: int = 300 # 5분 (21장 이미지 처리)
🤖 Prompt for AI Agents
In services/press-defect-data-simulator-service/app/config/settings.py around
lines 18–26, the fields use os.getenv at import time which bypasses pydantic
Settings env handling and can produce wrong defaults; change these to Pydantic
Field declarations that bind to env names and proper types: make
azure_connection_string Optional[str] with Field(None,
env="AZURE_CONNECTION_STRING"), leave azure_container_name and
azure_press_defect_path as string constants or use Field("simulator-data",
env="AZURE_CONTAINER_NAME") / Field("press-defect",
env="AZURE_PRESS_DEFECT_PATH") if you want them configurable, replace
model_service_url with Field("http://127.0.0.1:8000", env="MODEL_SERVICE_URL"),
model_service_predict_endpoint and model_service_health_endpoint can be
constants or Fields if needed, and model_service_timeout should be an int
Field(300, env="MODEL_SERVICE_TIMEOUT") so pydantic will cast the env var to
int; remove all os.getenv usages and rely on pydantic Settings to load and
validate types.


# 스케줄러 설정
scheduler_interval_seconds: int = 60 # 1분마다 실행
max_inspection_count: int = 79 # inspection_001 ~ inspection_079
start_inspection_id: int = 1

# HTTP 클라이언트 설정
http_timeout: int = 300 # 5분
http_retries: int = 3
http_retry_delay: int = 5 # 5초

# 로그 설정
log_level: str = "INFO"
log_dir: str = "logs"
log_file_name: str = "simulator.log"
log_max_size: int = 10 * 1024 * 1024 # 10MB
log_backup_count: int = 5

# 시뮬레이션 설정
simulation_enabled: bool = False # 기본적으로 비활성화
current_inspection_id: int = 1
total_simulations: int = 0
successful_simulations: int = 0
failed_simulations: int = 0

# 개발/운영 모드
debug_mode: bool = os.getenv("DEBUG", "false").lower() == "true"

class Config:
case_sensitive = False
env_file = ".env"

Comment on lines +55 to +58
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Pydantic v2 config style: use SettingsConfigDict instead of v1-style Config

In pydantic-settings v2, prefer model_config = SettingsConfigDict(...). This also keeps consistency with the existing press-fault service.

-    class Config:
-        case_sensitive = False
-        env_file = ".env"
+    model_config = SettingsConfigDict(
+        case_sensitive=False,
+        env_file=".env",
+        env_file_encoding="utf-8",
+        extra="ignore",
+    )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
class Config:
case_sensitive = False
env_file = ".env"
# replace the v1-style Config class with a v2-style SettingsConfigDict
- class Config:
- case_sensitive = False
model_config = SettingsConfigDict(
case_sensitive=False,
env_file=".env",
env_file_encoding="utf-8",
extra="ignore",
)
🤖 Prompt for AI Agents
In services/press-defect-data-simulator-service/app/config/settings.py around
lines 55–58, replace the v1-style inner Config class with a v2-style
model_config: import SettingsConfigDict from pydantic_settings and set
model_config = SettingsConfigDict(case_sensitive=False, env_file=".env") on the
settings model, removing the old Config class; ensure you update imports
accordingly so the file no longer defines class Config and uses model_config
instead.

# 전역 설정 인스턴스
settings = Settings()

# 설정 정보 출력용 함수
def get_settings_summary():
"""설정 정보 요약 (민감한 정보 제외)"""
return {
"service": {
"name": settings.service_name,
"version": settings.service_version,
"description": settings.service_description
},
"azure_storage": {
"container_name": settings.azure_container_name,
"press_defect_path": settings.azure_press_defect_path,
"connection_configured": bool(settings.azure_connection_string)
},
"model_service": {
"url": settings.model_service_url,
"predict_endpoint": settings.model_service_predict_endpoint,
"timeout": settings.model_service_timeout
},
"scheduler": {
"interval_seconds": settings.scheduler_interval_seconds,
"max_inspection_count": settings.max_inspection_count,
"enabled": settings.simulation_enabled
},
"logging": {
"level": settings.log_level,
"directory": settings.log_dir,
"debug_mode": settings.debug_mode
}
}

# 환경 변수 검증
def validate_settings():
"""필수 설정 검증"""
errors = []

if not settings.azure_connection_string:
errors.append("AZURE_CONNECTION_STRING이 설정되지 않았습니다.")

if not settings.model_service_url:
errors.append("MODEL_SERVICE_URL이 설정되지 않았습니다.")

if settings.scheduler_interval_seconds < 10:
errors.append("스케줄러 간격은 최소 10초 이상이어야 합니다.")

if settings.max_inspection_count < 1:
errors.append("inspection 개수는 최소 1개 이상이어야 합니다.")

return errors

# 설정 업데이트 함수들
def update_simulation_status(enabled: bool):
"""시뮬레이션 상태 업데이트"""
settings.simulation_enabled = enabled

def increment_simulation_stats(success: bool):
"""시뮬레이션 통계 업데이트"""
settings.total_simulations += 1
if success:
settings.successful_simulations += 1
else:
settings.failed_simulations += 1

Comment on lines +112 to +124
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Thread-safety: protect shared mutable state updates

update_simulation_status/increment_simulation_stats mutate shared state accessed from an async app and a background thread. Add a lock to avoid races.

+from threading import Lock
+
+# Protect runtime mutable stats across threads/tasks
+_stats_lock = Lock()
@@
 def update_simulation_status(enabled: bool):
     """시뮬레이션 상태 업데이트"""
-    settings.simulation_enabled = enabled
+    with _stats_lock:
+        settings.simulation_enabled = enabled
@@
 def increment_simulation_stats(success: bool):
     """시뮬레이션 통계 업데이트"""
-    settings.total_simulations += 1
-    if success:
-        settings.successful_simulations += 1
-    else:
-        settings.failed_simulations += 1
+    with _stats_lock:
+        settings.total_simulations += 1
+        if success:
+            settings.successful_simulations += 1
+        else:
+            settings.failed_simulations += 1
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# 설정 업데이트 함수들
def update_simulation_status(enabled: bool):
"""시뮬레이션 상태 업데이트"""
settings.simulation_enabled = enabled
def increment_simulation_stats(success: bool):
"""시뮬레이션 통계 업데이트"""
settings.total_simulations += 1
if success:
settings.successful_simulations += 1
else:
settings.failed_simulations += 1
from threading import Lock
# Protect runtime mutable stats across threads/tasks
_stats_lock = Lock()
# 설정 업데이트 함수들
def update_simulation_status(enabled: bool):
"""시뮬레이션 상태 업데이트"""
with _stats_lock:
settings.simulation_enabled = enabled
def increment_simulation_stats(success: bool):
"""시뮬레이션 통계 업데이트"""
with _stats_lock:
settings.total_simulations += 1
if success:
settings.successful_simulations += 1
else:
settings.failed_simulations += 1
🤖 Prompt for AI Agents
In services/press-defect-data-simulator-service/app/config/settings.py around
lines 112 to 124, the functions update_simulation_status and
increment_simulation_stats mutate shared module-level state without
synchronization, causing race conditions between async tasks and a background
thread; fix by creating a module-level threading.Lock (e.g., settings_lock) and
wrapping all reads/writes to settings.simulation_enabled,
settings.total_simulations, settings.successful_simulations, and
settings.failed_simulations in a with settings_lock: block so mutations are
atomic and exceptions still release the lock.

def update_current_inspection_id(inspection_id: int):
"""현재 처리 중인 inspection ID 업데이트"""
settings.current_inspection_id = inspection_id

Comment on lines +125 to +128
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Apply the same lock for current_inspection_id updates

current_inspection_id is also shared across routes and the scheduler.

 def update_current_inspection_id(inspection_id: int):
     """현재 처리 중인 inspection ID 업데이트"""
-    settings.current_inspection_id = inspection_id
+    with _stats_lock:
+        settings.current_inspection_id = inspection_id
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def update_current_inspection_id(inspection_id: int):
"""현재 처리 중인 inspection ID 업데이트"""
settings.current_inspection_id = inspection_id
def update_current_inspection_id(inspection_id: int):
"""현재 처리 중인 inspection ID 업데이트"""
with _stats_lock:
settings.current_inspection_id = inspection_id
🤖 Prompt for AI Agents
In services/press-defect-data-simulator-service/app/config/settings.py around
lines 125 to 128, the update_current_inspection_id function mutates a shared
variable without using the same synchronization primitive used elsewhere; wrap
the update in the shared lock (use the same lock object used for other shared
settings, e.g., settings.current_inspection_lock or whatever lock variable is
defined in this module) so the assignment to settings.current_inspection_id is
performed while holding that lock (use a context manager or acquire/release
around the assignment).

def get_simulation_stats():
"""시뮬레이션 통계 반환"""
success_rate = 0
if settings.total_simulations > 0:
success_rate = (settings.successful_simulations / settings.total_simulations) * 100

return {
"total_simulations": settings.total_simulations,
"successful_simulations": settings.successful_simulations,
"failed_simulations": settings.failed_simulations,
"success_rate": round(success_rate, 2),
"current_inspection_id": settings.current_inspection_id,
"simulation_enabled": settings.simulation_enabled
}
Loading
Loading