Перейти к основному содержимому

Singleton — один активный плагин на вид

singleton — самый простой класс: один активный плагин обрабатывает все вызовы вида. Используется для видов, где «несколько активных» не имеет смысла: один активный payment-провайдер на приложение, одно векторное хранилище, один orchestrator, один LLM-backend.

Шаг 1. Объявить вид

kinds/payment_provider/v1.yaml
kind: payment_provider
kind_api_version: 1.0.0
description: Payment provider — charge, refund, status check.

hooks:
- name: charge
dispatch: singleton
input_schema: schemas/charge.input.json
output_schema: schemas/charge.output.json
mcp_exposed: true
- name: refund
dispatch: singleton
input_schema: schemas/refund.input.json
output_schema: schemas/refund.output.json
mcp_exposed: true

Шаг 2. Написать две реализации

plugins/payment_provider/stripe/dagstack.toml
[plugin]
name = "stripe"
kind = "payment_provider"
runtime = "in_process"
core_version = ">=0.1.0,<1.0.0"
priority = 50
plugins/payment_provider/stripe/plugin.py
class StripeProvider:
async def setup(self, ctx):
self._http = ctx.registry.resource_registry.get("http_client")
self._config = ctx.config

async def charge(self, input):
response = await self._http.post(
"https://api.stripe.com/v1/charges",
headers={"Authorization": f"Bearer {self._config['api_key']}"},
json={"amount": input.amount_cents, "currency": input.currency, "source": input.source},
)
return {"transaction_id": response.json()["id"], "status": "succeeded"}
plugins/payment_provider/internal/dagstack.toml
[plugin]
name = "internal"
kind = "payment_provider"
runtime = "in_process"
core_version = ">=0.1.0,<1.0.0"
priority = 40

Теперь в реестре два плагина вида payment_provider с разными приоритетами.

Шаг 3. Выбор активного

По ADR-0002 §1, алгоритм:

  1. Routing-policy приложения (если задана) — например, per-tenant выбор: premium-tenants используют Stripe, internal — internal-provider.
  2. Env-overrideDAGSTACK_ACTIVE_PAYMENT_PROVIDER=internal.
  3. Priority — плагин с priority=50 (stripe) победит над priority=40 (internal).
  4. Ambiguity — если оба имели бы priority=50, ядро бросит AmbiguousPlugin на старте.

Шаг 4. Вызов

payment = registry.get_plugin("payment_provider")
result = await payment.charge(ChargeInput(amount_cents=1999, currency="USD", source="tok_visa"))
print(result.transaction_id)

Picking a specific plugin explicitly (when needed):

payment = registry.get_plugin("payment_provider", name="internal")

Переключение активного плагина

Через env-переменную — не требует перезапуска:

export DAGSTACK_ACTIVE_PAYMENT_PROVIDER=internal
python main.py

Через routing-policy — runtime, per-tenant:

:::info Phase 2 scope Per-call routing-policy (set_routing_policy(kind, policy=...)) — часть дорожной карты Phase 2. В 0.1.0-rc.2 выбор идёт только по priority + env-override + явному name=. Для tenant-driven routing сегодня выбирайте активный provider в коде приложения через registry.get_plugin("payment_provider", name=...). :::

Типичные ошибки

СимптомПричина
AmbiguousPlugin: equal priority for kind=payment_provider на стартеДва плагина вида имеют равный priority и нет env-override. Различить приоритеты или использовать DAGSTACK_ACTIVE_PAYMENT_PROVIDER=....
KindUnknown: payment_providerHookspec вида не зарегистрирован; вызвать registry.add_hookspecs(...) для модуля вида перед registry.discover("plugins/").
RuntimeNotSupportedПлагин объявил только runtime = "mcp_http", а host адаптер не сконфигурирован.

Singleton для других доменов — тот же паттерн

Тот же walkthrough применим для любого вида, где нужен «один активный»:

  • llm — один LLM-backend на приложение (OpenAI / Anthropic / local).
  • vector_store — одно векторное хранилище (Qdrant / pgvector).
  • orchestrator — один оркестратор UoW на runtime.
  • cache — один кеш-backend (Redis / memcached).
  • auth_provider — один primary auth (OAuth / SAML).

Механика одинаковая: декларация hookspec, две+ реализации, выбор по priority / env-override / routing-policy.

См. также