Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,4 @@ build/
# 로컬 도구 바이너리 (Makefile 참고)
bin/
.env.grafana-mcp
.omc/
50 changes: 48 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,8 +1,54 @@
# act 바이너리는 https://github.com/nektos/act 릴리스에서 받아 bin/에 둔다 (git 미추적)
ENV_FILE := .env
RUNTIME_TARGETS := dev run check-port
REQUESTED_TARGETS := $(if $(MAKECMDGOALS),$(MAKECMDGOALS),dev)
NEEDS_RUNTIME_ENV := $(filter $(RUNTIME_TARGETS),$(REQUESTED_TARGETS))

ifneq ($(wildcard $(ENV_FILE)),)
include $(ENV_FILE)
export
endif

ifneq ($(NEEDS_RUNTIME_ENV),)
ifeq ($(wildcard $(ENV_FILE)),)
$(error $(ENV_FILE) file is required)
endif

PORT_DEFINED_IN_ENV := $(shell awk -F= '/^[[:space:]]*(export[[:space:]]+)?PORT[[:space:]]*=/ { print "1"; exit }' $(ENV_FILE))
ifeq ($(PORT_DEFINED_IN_ENV),)
$(error PORT is required in $(ENV_FILE))
endif

ifeq ($(strip $(PORT)),)
$(error PORT is required in $(ENV_FILE))
endif
endif

ACT = bin/act
TYPE = dev
HOST ?= 0.0.0.0

.PHONY: dev run check-port up deploy act-ci act-cd-dev act-cd-prod test-all profile branch-clear

dev: check-port
@echo "Starting dev server on $(HOST):$(PORT)"
uv run python -c "from server import app; from receiver import *; import uvicorn; uvicorn.run(app, host='$(HOST)', port=$(PORT))"

run: dev

check-port:
@if command -v lsof >/dev/null 2>&1; then \
if lsof -iTCP:$(PORT) -sTCP:LISTEN -Pn >/dev/null; then \
echo "ERROR: port $(PORT) is already in use."; \
lsof -iTCP:$(PORT) -sTCP:LISTEN -Pn; \
exit 1; \
fi; \
fi

up:
bash scripts/deploy/main.sh

.PHONY: act-ci act-cd-dev act-cd-prod
deploy: up

## CI workflow 로컬 테스트
act-ci:
Expand Down Expand Up @@ -39,4 +85,4 @@ profile:

branch-clear:
git fetch --all --prune
git branch --merged develop | egrep -v 'develop' | xargs -n 1 git branch -d
git branch --merged develop | egrep -v 'develop' | xargs -n 1 git branch -d
1 change: 0 additions & 1 deletion config.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,3 @@ class SentryConfig:
# SENTRY_DSN은 선택적 (없으면 Sentry 비활성화)
_sentry_dsn = os.getenv("SENTRY_DSN", "")
SENTRY_DSN = _sentry_dsn

30 changes: 30 additions & 0 deletions core/broker/internal/broker.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ def __str__(self):

class EventBroker:
event_dict: dict[str, list[Receiver]] = {}
_idle_event: asyncio.Event | None = None
_idle_loop: asyncio.AbstractEventLoop | None = None

@staticmethod
def add_receiver(event_name: str):
Expand Down Expand Up @@ -49,10 +51,12 @@ async def publish(event: Event):
async def counter(receiver, event):
global EVENT_COUNT
EVENT_COUNT += 1
EventBroker._mark_running()
try:
await receiver(event)
finally:
EVENT_COUNT -= 1
EventBroker._mark_done()

for receiver in receivers:
coroutines.append(counter(receiver, event))
Expand All @@ -65,3 +69,29 @@ def is_end():
return True
else:
return False

@staticmethod
async def wait_until_idle(timeout: float | None = None):
event = EventBroker._get_idle_event()
await asyncio.wait_for(event.wait(), timeout=timeout)

@staticmethod
def _get_idle_event():
loop = asyncio.get_running_loop()
if EventBroker._idle_event is None or EventBroker._idle_loop is not loop:
EventBroker._idle_event = asyncio.Event()
EventBroker._idle_loop = loop
if EventBroker.is_end():
EventBroker._idle_event.set()
return EventBroker._idle_event

@staticmethod
def _mark_running():
event = EventBroker._get_idle_event()
event.clear()

@staticmethod
def _mark_done():
if EventBroker.is_end():
event = EventBroker._get_idle_event()
event.set()
Comment on lines +73 to +97

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

브로커에 기능 추가되면 반드시 합리적인 이유 필요함

1 change: 1 addition & 0 deletions core/lifecycle/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@
from .internal.parameter import Parameter
from .internal.caller import Caller
from .internal.profiler import LifecycleProfiler
from .internal.sink import add_lifecycle_sink, remove_lifecycle_sink, emit_lifecycle
from .metrics import LifecycleMetrics
2 changes: 2 additions & 0 deletions core/lifecycle/internal/hlife.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from .caller import Caller
from .parameter import Parameter
from .profiler import LifecycleProfiler
from .sink import emit_lifecycle
from core.lifecycle.metrics import LifecycleMetrics
from typing import Any
from loguru import logger
Expand Down Expand Up @@ -133,4 +134,5 @@ def close(self):
else:
event_names.append(str(e))

emit_lifecycle(self)
logger.debug(f"{self}")
2 changes: 2 additions & 0 deletions core/lifecycle/internal/rlife.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,5 +109,7 @@ def on_exit(self):

def close(self):
"""Lifecycle 종료 시 로깅"""
from .sink import emit_lifecycle

emit_lifecycle(self)
logger.debug(self)
40 changes: 40 additions & 0 deletions core/lifecycle/internal/sink.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
from __future__ import annotations

from typing import Callable

from .lifecycle import LifeCycle

LifecycleSink = Callable[[LifeCycle], None]


class LifecycleSinkRegistry:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

이거 용도 뭐임?

def __init__(self) -> None:
self._sinks: list[LifecycleSink] = []

def add(self, sink: LifecycleSink) -> LifecycleSink:
if sink not in self._sinks:
self._sinks.append(sink)
return sink

def remove(self, sink: LifecycleSink) -> None:
if sink in self._sinks:
self._sinks.remove(sink)

def emit(self, lifecycle: LifeCycle) -> None:
for sink in list(self._sinks):
sink(lifecycle)


_sink_registry = LifecycleSinkRegistry()


def add_lifecycle_sink(sink: LifecycleSink) -> LifecycleSink:
return _sink_registry.add(sink)


def remove_lifecycle_sink(sink: LifecycleSink) -> None:
_sink_registry.remove(sink)


def emit_lifecycle(lifecycle: LifeCycle) -> None:
_sink_registry.emit(lifecycle)
36 changes: 36 additions & 0 deletions core/lifecycle/tests/test_sink.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
"""lifecycle sink 테스트"""

from core.lifecycle import LifeCycle
from core.lifecycle.internal.sink import LifecycleSinkRegistry


class TestLifecycleSinkRegistry:
def test_add_ignores_duplicate_sink(self) -> None:
registry = LifecycleSinkRegistry()
lifecycle = LifeCycle.create()
emitted: list[str] = []

def sink(lifecycle: LifeCycle) -> None:
emitted.append(lifecycle.id)

registry.add(sink)
registry.add(sink)

registry.emit(lifecycle)

assert emitted == [lifecycle.id]

def test_remove_sink(self) -> None:
registry = LifecycleSinkRegistry()
lifecycle = LifeCycle.create()
emitted: list[str] = []

def sink(lifecycle: LifeCycle) -> None:
emitted.append(lifecycle.id)

registry.add(sink)
registry.remove(sink)

registry.emit(lifecycle)

assert emitted == []
19 changes: 6 additions & 13 deletions handler/board/storage/internal/cursor_repository.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
from functools import cache
import aiosqlite
from data.board import Point, PointRange, Tiles, Section, TileKind
from config import BoardConfig
import os
from utils.sql import get_sql

SQL_PATH = os.path.join(os.path.dirname(__file__), "sql", "cursor") + os.sep

Expand All @@ -15,22 +15,15 @@
DB = aiosqlite.Connection


@cache
def get_sql(path: str):
with open(f"{SQL_PATH}{path}", "r", encoding="utf-8") as f:
query = f.read()
return query


async def set_table(db: DB):
query = get_sql(TABLE_SET)
query = get_sql(SQL_PATH, TABLE_SET)

await db.execute(query)
await db.commit()


async def get_section(db: DB, point: Point):
query = get_sql(SECTION_GET)
query = get_sql(SQL_PATH, SECTION_GET)

async with db.execute(query, (point.x, point.y)) as cur:
row = await cur.fetchone()
Expand All @@ -52,7 +45,7 @@ async def get_section(db: DB, point: Point):


async def get_iter_by_section_range(db: DB, point_range: PointRange):
query = get_sql(SECTION_GET_BY_RANGE)
query = get_sql(SQL_PATH, SECTION_GET_BY_RANGE)
pr = point_range

async with db.execute(query, (pr.left, pr.right, pr.bottom, pr.top)) as cur:
Expand Down Expand Up @@ -97,7 +90,7 @@ async def create_section(db: DB, section: Section):
point = section.point
tiles = section.tiles

query = get_sql(SECTION_CREATE)
query = get_sql(SQL_PATH, SECTION_CREATE)

await db.execute(query, (point.x, point.y, tiles.data))
await db.commit()
Expand All @@ -107,7 +100,7 @@ async def update_section(db: DB, section: Section):
point = section.point
tiles = section.tiles

query = get_sql(SECTION_UPDATE)
query = get_sql(SQL_PATH, SECTION_UPDATE)

await db.execute(query, (tiles.data, point.x, point.y))
await db.commit()
21 changes: 7 additions & 14 deletions handler/board/storage/internal/repository.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
from functools import cache
import aiosqlite
from contextlib import asynccontextmanager
from data.board import Point, PointRange, Tiles, Section
from config import BoardConfig
import os
from utils.sql import get_sql

SQL_PATH = os.path.join(os.path.dirname(__file__), "sql", "map") + os.sep

Expand All @@ -17,13 +17,6 @@
DB = aiosqlite.Connection


@cache
def get_sql(path: str):
with open(f"{SQL_PATH}{path}", "r", encoding="utf-8") as f:
query = f.read()
return query


@asynccontextmanager
async def get_db():
db = await aiosqlite.connect(BoardConfig.DB_PATH)
Expand All @@ -42,14 +35,14 @@ async def get_db():
# return wrapper

async def set_table(db: DB):
query = get_sql(TABLE_SET)
query = get_sql(SQL_PATH, TABLE_SET)

await db.execute(query)
await db.commit()


async def get_section(db: DB, point: Point):
query = get_sql(SECTION_GET)
query = get_sql(SQL_PATH, SECTION_GET)

async with db.execute(query, (point.x, point.y)) as cur:
row = await cur.fetchone()
Expand All @@ -71,7 +64,7 @@ async def get_section(db: DB, point: Point):


async def get_iter_by_section_range(db: DB, point_range: PointRange):
query = get_sql(SECTION_GET_BY_RANGE)
query = get_sql(SQL_PATH, SECTION_GET_BY_RANGE)
pr = point_range

async with db.execute(query, (pr.left, pr.right, pr.bottom, pr.top)) as cur:
Expand Down Expand Up @@ -117,7 +110,7 @@ async def create_section(db: DB, section: Section):
tiles = section.tiles
flag = section.flag

query = get_sql(SECTION_CREATE)
query = get_sql(SQL_PATH, SECTION_CREATE)

await db.execute(query, (point.x, point.y, tiles.data, flag))
await db.commit()
Expand All @@ -127,7 +120,7 @@ async def update_section(db: DB, section: Section):
point = section.point
tiles = section.tiles

query = get_sql(SECTION_UPDATE)
query = get_sql(SQL_PATH, SECTION_UPDATE)

await db.execute(query, (tiles.data, point.x, point.y))
await db.commit()
Expand All @@ -137,7 +130,7 @@ async def update_section_flag(db: DB, section: Section):
point = section.point
flag = section.flag

query = get_sql(SECTION_FLAG_UPDATE)
query = get_sql(SQL_PATH, SECTION_FLAG_UPDATE)

await db.execute(query, (flag, point.x, point.y))
await db.commit()
6 changes: 6 additions & 0 deletions handler/cursor/internal/cursor_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,15 @@ async def create(cls, cursor: Cursor) -> None:
await EventBroker.publish(event=event)

@classmethod
@LifeCycle.with_async_lifecycle(
factory=HLife.create_factory("CursorHandler", "delete")
)
async def delete(cls, id: str) -> None:
hlife = HLife.get_lifecycle()
if id in cls.cursor_dict:
old_cur = cls.cursor_dict[id].copy()
del cls.cursor_dict[id]
hlife.set_snapshot(before=old_cur, after=None)

@classmethod
async def get_by_id(cls, id: str) -> Cursor:
Expand Down
Loading
Loading