feat(config): 구글 시트 설정 데이터 인메모리 캐싱 적용 및 동기화 최적화
- 설정 조회 속도 개선을 위한 인메모리 캐싱 도입: read_config(force_refresh=False)로 변경하여 캐시가 존재하고 유효 기간(10분) 내인 경우 구글 시트 API 호출을 건너뛰고 메모리 데이터 즉시 반환 / 다중 스레드 환경을 고려한 threading.Lock 및 Double-Checked Locking 적용으로 동시성 안전 보장 - 실시간 데이터 일관성 유지: write_config(config) 함수 성공 시 새로 작성한 설정 로우(rows) 데이터로 로컬 캐시 즉시 최신화 - 신규 제어 장치 및 Warm up 추가: /api/config/refresh (POST) API 신설 (구글 시트 웹 문서에서 직접 변경 시 강제로 캐시를 갱신할 수 있는 기능 제공) / 서버 시작(startup_event) 시 캐시 초기 적재(Warm up) 처리 추가로 첫 로딩 딜레이 완전 차단
This commit is contained in:
@@ -9,6 +9,7 @@ import io
|
|||||||
import math
|
import math
|
||||||
import datetime
|
import datetime
|
||||||
import configparser
|
import configparser
|
||||||
|
import threading
|
||||||
import urllib.request
|
import urllib.request
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
import uuid
|
import uuid
|
||||||
@@ -219,11 +220,23 @@ SETTINGS_SPREADSHEET_ID = '1k7SNEdIaRtGXzQNdJSQAiNSDRwzOePDRTLHY_mNCO8M'
|
|||||||
MANUAL_HISTORY_FILE = 'manual_history.dat'
|
MANUAL_HISTORY_FILE = 'manual_history.dat'
|
||||||
|
|
||||||
# ----- Utility Functions -----
|
# ----- Utility Functions -----
|
||||||
def read_config():
|
def read_config(force_refresh=False):
|
||||||
config = configparser.ConfigParser(allow_no_value=True)
|
global _CONFIG_ROWS_CACHE, _CONFIG_ROWS_CACHE_TIME
|
||||||
config.optionxform = str
|
|
||||||
|
|
||||||
rows = []
|
rows = []
|
||||||
|
|
||||||
|
# 1. 캐시 유효성 판단 (force_refresh가 아니고, 캐시가 존재하며, TTL이 지나지 않은 경우)
|
||||||
|
if not force_refresh and _CONFIG_ROWS_CACHE is not None:
|
||||||
|
if time.time() - _CONFIG_ROWS_CACHE_TIME < CONFIG_CACHE_TTL:
|
||||||
|
rows = _CONFIG_ROWS_CACHE
|
||||||
|
|
||||||
|
# 2. 캐시가 없거나 만료된 경우 구글 시트에서 직접 새로 로드
|
||||||
|
if not rows:
|
||||||
|
with _CONFIG_LOCK:
|
||||||
|
# 락 대기 중에 다른 스레드에서 캐시를 갱신했을 수 있으므로 다시 검사 (Double-Checked Locking)
|
||||||
|
if not force_refresh and _CONFIG_ROWS_CACHE is not None and time.time() - _CONFIG_ROWS_CACHE_TIME < CONFIG_CACHE_TTL:
|
||||||
|
rows = _CONFIG_ROWS_CACHE
|
||||||
|
else:
|
||||||
for _ in range(3):
|
for _ in range(3):
|
||||||
try:
|
try:
|
||||||
ws = get_settings_worksheet()
|
ws = get_settings_worksheet()
|
||||||
@@ -233,6 +246,14 @@ def read_config():
|
|||||||
time.sleep(1.5)
|
time.sleep(1.5)
|
||||||
print(f"Google Sheets 불러오기 재시도 중...: {e}")
|
print(f"Google Sheets 불러오기 재시도 중...: {e}")
|
||||||
|
|
||||||
|
if rows:
|
||||||
|
_CONFIG_ROWS_CACHE = rows
|
||||||
|
_CONFIG_ROWS_CACHE_TIME = time.time()
|
||||||
|
|
||||||
|
# 3. 로드된 로우 데이터를 ConfigParser 형식으로 파싱
|
||||||
|
config = configparser.ConfigParser(allow_no_value=True)
|
||||||
|
config.optionxform = str
|
||||||
|
|
||||||
if rows and len(rows) > 1:
|
if rows and len(rows) > 1:
|
||||||
for row in rows:
|
for row in rows:
|
||||||
if not row or not row[0].strip():
|
if not row or not row[0].strip():
|
||||||
@@ -256,10 +277,10 @@ def read_config():
|
|||||||
config.set(section, key, val)
|
config.set(section, key, val)
|
||||||
else:
|
else:
|
||||||
config.set(section, key)
|
config.set(section, key)
|
||||||
config.set(section, key)
|
|
||||||
return config
|
return config
|
||||||
|
|
||||||
def write_config(config):
|
def write_config(config):
|
||||||
|
global _CONFIG_ROWS_CACHE, _CONFIG_ROWS_CACHE_TIME
|
||||||
rows = [["대분류 (Section)", "항목 (Key)", "설정값 1", "설정값 2", "설정값 3", "설정값 4", "설정값 5"]]
|
rows = [["대분류 (Section)", "항목 (Key)", "설정값 1", "설정값 2", "설정값 3", "설정값 4", "설정값 5"]]
|
||||||
for section in config.sections():
|
for section in config.sections():
|
||||||
items = config.items(section)
|
items = config.items(section)
|
||||||
@@ -278,6 +299,11 @@ def write_config(config):
|
|||||||
ws = get_settings_worksheet()
|
ws = get_settings_worksheet()
|
||||||
ws.clear()
|
ws.clear()
|
||||||
ws.update(values=rows)
|
ws.update(values=rows)
|
||||||
|
|
||||||
|
# 구글 시트에 저장이 성공하면 로컬 캐시도 즉시 최신화
|
||||||
|
with _CONFIG_LOCK:
|
||||||
|
_CONFIG_ROWS_CACHE = rows
|
||||||
|
_CONFIG_ROWS_CACHE_TIME = time.time()
|
||||||
return
|
return
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Google Sheets 저장 시도 {attempt+1}/3 실패: {e}")
|
print(f"Google Sheets 저장 시도 {attempt+1}/3 실패: {e}")
|
||||||
@@ -318,6 +344,16 @@ async def get_config():
|
|||||||
return JSONResponse(data)
|
return JSONResponse(data)
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/config/refresh")
|
||||||
|
async def refresh_config():
|
||||||
|
"""구글 시트로부터 실시간으로 설정을 동기화하여 캐시를 강제 갱신합니다."""
|
||||||
|
try:
|
||||||
|
read_config(force_refresh=True)
|
||||||
|
return {"status": "success", "message": "설정 캐시가 성공적으로 갱신되었습니다."}
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=500, detail=f"설정 캐시 갱신 실패: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class ProductGroup(BaseModel):
|
class ProductGroup(BaseModel):
|
||||||
old_code: str
|
old_code: str
|
||||||
@@ -416,7 +452,12 @@ def format_phone_number(num_str):
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
# ----- Google Sheets Global Cache -----
|
# ----- Google Sheets Config Cache & Global Sheets Cache -----
|
||||||
|
_CONFIG_ROWS_CACHE = None
|
||||||
|
_CONFIG_ROWS_CACHE_TIME = 0.0
|
||||||
|
_CONFIG_LOCK = threading.Lock()
|
||||||
|
CONFIG_CACHE_TTL = 600.0 # 캐시 유효 시간: 10분
|
||||||
|
|
||||||
gs_client_global = None
|
gs_client_global = None
|
||||||
worksheet_manual_global = None
|
worksheet_manual_global = None
|
||||||
worksheet_settings_global = None
|
worksheet_settings_global = None
|
||||||
@@ -492,6 +533,11 @@ async def startup_event():
|
|||||||
get_sms_history_worksheet()
|
get_sms_history_worksheet()
|
||||||
get_manual_history_worksheet()
|
get_manual_history_worksheet()
|
||||||
print("구글 시트 연동 완료!")
|
print("구글 시트 연동 완료!")
|
||||||
|
|
||||||
|
# 캐시 초기 적재 (Warm up cache)
|
||||||
|
print("구글 시트 설정 캐시 초기 적재 시작...")
|
||||||
|
read_config(force_refresh=True)
|
||||||
|
print("구글 시트 설정 캐시 적재 완료!")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"구글 시트 연동 초기화 실패 (사용 전 권한을 확인하세요): {str(e)}")
|
print(f"구글 시트 연동 초기화 실패 (사용 전 권한을 확인하세요): {str(e)}")
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user