feat: add gateway client with env-driven assembly

Includes config aggregation for multi-source env keys, from_env and
from_settings factories with explicit shared-backend injection,
gather_bounded, top-level exports, tightened import-linter layers with
the gate removed from the Makefile, and the finalized .env.example.
This commit is contained in:
2026-07-20 07:47:05 -04:00
parent 936895919c
commit 7b9815f4bc
30 changed files with 1701 additions and 253 deletions
+36 -11
View File
@@ -51,8 +51,13 @@ class InMemoryGate:
g.probe_owner = owner
g.probe_expires = self._now() + self._cfg.probe_ttl_s
return GateDecision(
source_name=source_name, allowed=True, state=GateState.HALF_OPEN,
epoch=g.epoch, is_probe=True, probe_owner=owner, retry_after_s=0.0,
source_name=source_name,
allowed=True,
state=GateState.HALF_OPEN,
epoch=g.epoch,
is_probe=True,
probe_owner=owner,
retry_after_s=0.0,
)
async def try_enter(self, source_name: str, owner: str) -> GateDecision:
@@ -63,23 +68,36 @@ class InMemoryGate:
now = self._now()
if g.state is GateState.CLOSED:
return GateDecision(
source_name=source_name, allowed=True, state=GateState.CLOSED,
epoch=g.epoch, is_probe=False, probe_owner=None, retry_after_s=0.0,
source_name=source_name,
allowed=True,
state=GateState.CLOSED,
epoch=g.epoch,
is_probe=False,
probe_owner=None,
retry_after_s=0.0,
)
if g.state is GateState.OPEN:
if now >= g.open_until:
return self._grant_probe(g, source_name, owner)
return GateDecision(
source_name=source_name, allowed=False, state=GateState.OPEN,
epoch=g.epoch, is_probe=False, probe_owner=None,
source_name=source_name,
allowed=False,
state=GateState.OPEN,
epoch=g.epoch,
is_probe=False,
probe_owner=None,
retry_after_s=g.open_until - now,
)
# HALF_OPEN: 探针在途;租约过期则接管,否则拒绝(防惊群)
if now >= g.probe_expires:
return self._grant_probe(g, source_name, owner)
return GateDecision(
source_name=source_name, allowed=False, state=GateState.HALF_OPEN,
epoch=g.epoch, is_probe=False, probe_owner=None,
source_name=source_name,
allowed=False,
state=GateState.HALF_OPEN,
epoch=g.epoch,
is_probe=False,
probe_owner=None,
retry_after_s=g.probe_expires - now,
)
@@ -96,8 +114,13 @@ class InMemoryGate:
def _snapshot(self, g: _SourceGate, applied: bool) -> GateUpdate:
return GateUpdate(
applied=applied, state=g.state, epoch=g.epoch, failure_count=g.fails,
retry_after_s=max(0.0, g.open_until - self._now()) if g.state is GateState.OPEN else 0.0,
applied=applied,
state=g.state,
epoch=g.epoch,
failure_count=g.fails,
retry_after_s=max(0.0, g.open_until - self._now())
if g.state is GateState.OPEN
else 0.0,
)
def _open(self, g: _SourceGate, reason: str) -> None:
@@ -118,7 +141,9 @@ class InMemoryGate:
g.probe_expires = 0.0
return self._snapshot(g, applied=True)
async def record_failure(self, entry: GateDecision, reason: str, force_open: bool) -> GateUpdate:
async def record_failure(
self, entry: GateDecision, reason: str, force_open: bool
) -> GateUpdate:
g = self._gate(entry.source_name)
if not self._fenced(g, entry):
return self._snapshot(g, applied=False)
+3 -1
View File
@@ -27,7 +27,9 @@ _WINDOW_S = 60.0
class _MemoryPermit:
"""入场许可;release/settle 幂等(CHS _RedisPermit 同款 flag 语义)。"""
def __init__(self, limiter: InMemoryLimiter, source: str, lease_id: str, est: int, window: int) -> None:
def __init__(
self, limiter: InMemoryLimiter, source: str, lease_id: str, est: int, window: int
) -> None:
self._limiter = limiter
self._source = source
self._lease_id = lease_id