207 lines
6.5 KiB
Markdown
207 lines
6.5 KiB
Markdown
# PLBA
|
|
|
|
`PLBA` (`Platform Runtime for Business Applications`) - runtime для бизнес-приложений.
|
|
|
|
Платформа берет на себя инфраструктурные обязанности:
|
|
- lifecycle приложения
|
|
- запуск и остановку воркеров
|
|
- health/status
|
|
- tracing
|
|
- logging
|
|
- control plane
|
|
- загрузку конфигурации
|
|
|
|
Бизнес-приложение на базе `plba` собирается вокруг трех уровней:
|
|
- `ApplicationModule` собирает приложение и регистрирует воркеры
|
|
- `Worker` управляет исполнением и lifecycle
|
|
- `Routine` реализует бизнес-функцию
|
|
|
|
`Routine` не является контрактом `plba`. Это рекомендуемый архитектурный паттерн для прикладного кода.
|
|
|
|
Правила построения приложений на платформе собраны отдельно в [application_guidelines.md](/Users/alex/Dev_projects_v2/apps/plba/requirements/application_guidelines.md).
|
|
|
|
## Runtime model
|
|
|
|
1. приложение объявляет `ApplicationModule`
|
|
2. модуль регистрирует один или несколько `Worker`
|
|
3. `RuntimeManager` запускает все воркеры
|
|
4. каждый `Worker` запускает свою бизнес-активность
|
|
5. runtime агрегирует health и status
|
|
6. runtime останавливает воркеры graceful или forcefully
|
|
|
|
## Main contracts
|
|
|
|
### `ApplicationModule`
|
|
|
|
Описывает, из чего состоит приложение.
|
|
|
|
Ответственность:
|
|
- дать имя модуля
|
|
- зарегистрировать воркеры
|
|
- зарегистрировать health contributors при необходимости
|
|
- собрать прикладные зависимости
|
|
|
|
### `Worker`
|
|
|
|
Главный runtime-контракт платформы.
|
|
|
|
Контракт:
|
|
- `name`
|
|
- `critical`
|
|
- `start()`
|
|
- `stop(force=False)`
|
|
- `health()`
|
|
- `status()`
|
|
|
|
`Worker` отвечает только за runtime-поведение:
|
|
- как запускается бизнес-активность
|
|
- в одном потоке или нескольких
|
|
- single-run или loop
|
|
- graceful shutdown
|
|
- интерпретацию ошибок в `health/status`
|
|
|
|
### `Routine`
|
|
|
|
Рекомендуемый application-level паттерн.
|
|
|
|
`Routine` описывает бизнес-функцию:
|
|
- что читать
|
|
- какие сервисы вызывать
|
|
- какие бизнес-решения принимать
|
|
- что сохранять или отправлять наружу
|
|
|
|
Обычно воркер получает одну routine через конструктор и вызывает ее в `start()` или во внутренних helper-методах.
|
|
|
|
## Minimal example
|
|
|
|
```python
|
|
from threading import Event, Lock, Thread
|
|
from time import sleep
|
|
|
|
from plba import (
|
|
ApplicationModule,
|
|
Worker,
|
|
WorkerHealth,
|
|
WorkerStatus,
|
|
create_runtime,
|
|
)
|
|
|
|
|
|
class OrdersRoutine:
|
|
def __init__(self, service) -> None:
|
|
self._service = service
|
|
|
|
def run(self) -> None:
|
|
self._service.process_new_orders()
|
|
|
|
|
|
class OrdersWorker(Worker):
|
|
def __init__(self, routine: OrdersRoutine, interval: float = 1.0) -> None:
|
|
self._routine = routine
|
|
self._interval = interval
|
|
self._thread: Thread | None = None
|
|
self._stop_requested = Event()
|
|
self._lock = Lock()
|
|
self._in_flight = 0
|
|
self._failures = 0
|
|
|
|
@property
|
|
def name(self) -> str:
|
|
return "orders-worker"
|
|
|
|
@property
|
|
def critical(self) -> bool:
|
|
return True
|
|
|
|
def start(self) -> None:
|
|
if self._thread and self._thread.is_alive():
|
|
return
|
|
self._stop_requested.clear()
|
|
self._thread = Thread(target=self._run_loop, daemon=True)
|
|
self._thread.start()
|
|
|
|
def stop(self, force: bool = False) -> None:
|
|
del force
|
|
self._stop_requested.set()
|
|
|
|
def health(self) -> WorkerHealth:
|
|
if self._failures > 0:
|
|
return WorkerHealth(self.name, "degraded", self.critical)
|
|
return WorkerHealth(self.name, "ok", self.critical)
|
|
|
|
def status(self) -> WorkerStatus:
|
|
alive = self._thread is not None and self._thread.is_alive()
|
|
state = "busy" if self._in_flight else "idle"
|
|
if not alive:
|
|
state = "stopped"
|
|
elif self._stop_requested.is_set():
|
|
state = "stopping"
|
|
return WorkerStatus(name=self.name, state=state, in_flight=self._in_flight)
|
|
|
|
def _run_loop(self) -> None:
|
|
while not self._stop_requested.is_set():
|
|
with self._lock:
|
|
self._in_flight += 1
|
|
try:
|
|
self._routine.run()
|
|
except Exception:
|
|
self._failures += 1
|
|
finally:
|
|
with self._lock:
|
|
self._in_flight -= 1
|
|
sleep(self._interval)
|
|
|
|
|
|
class OrdersModule(ApplicationModule):
|
|
@property
|
|
def name(self) -> str:
|
|
return "orders"
|
|
|
|
def register(self, registry) -> None:
|
|
service = OrderService()
|
|
routine = OrdersRoutine(service)
|
|
registry.add_worker(OrdersWorker(routine))
|
|
|
|
|
|
runtime = create_runtime(OrdersModule(), config_path="config.yml")
|
|
runtime.start()
|
|
```
|
|
|
|
## Health and status
|
|
|
|
Практика такая:
|
|
- `Routine` выполняет бизнес-работу
|
|
- `Worker` ловит ее ошибки
|
|
- `Worker` интерпретирует outcome в `health()` и `status()`
|
|
|
|
То есть routine не выставляет health напрямую.
|
|
|
|
## In-memory queue
|
|
|
|
`InMemoryTaskQueue` остается в платформе как простой in-memory utility.
|
|
|
|
Это не базовый платформенный контракт и не обязательный паттерн архитектуры.
|
|
Ее можно использовать в прикладном коде как локальный буфер между компонентами, если это действительно помогает.
|
|
|
|
```python
|
|
from plba import InMemoryTaskQueue
|
|
|
|
queue = InMemoryTaskQueue[str]()
|
|
queue.put("payload")
|
|
item = queue.get(timeout=0.1)
|
|
```
|
|
|
|
## Public API
|
|
|
|
Основные публичные сущности:
|
|
- `ApplicationModule`
|
|
- `Worker`
|
|
- `WorkerHealth`
|
|
- `WorkerStatus`
|
|
- `RuntimeManager`
|
|
- `WorkerSupervisor`
|
|
- `TraceService`
|
|
- `HealthRegistry`
|
|
- `InMemoryTaskQueue`
|
|
- `create_runtime(...)`
|