Skip to content
Merged
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
24 changes: 16 additions & 8 deletions docs/agents.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,8 +106,9 @@ async def main() -> None:
asyncio.run(main())
```

A hand-built chain, which is what you need for per-method budgets, request-budget
propagation or wait-for-ready:
A hand-built chain, which is what you need for per-method budgets — request-budget
propagation and wait-for-ready are reachable from a settings object too, through their
own optional blocks:

```python
from grpc_client_kit import (
Expand Down Expand Up @@ -298,7 +299,9 @@ Settings and collaborator protocols, all `runtime_checkable` and all exported:
`RetrySettingsProtocol`, `CircuitBreakerSettingsProtocol`, `LoadBalancerSettingsProtocol`,
`HealthCheckerSettingsProtocol`, `ChannelProviderProtocol`, `HealthCheckerProtocol`,
`HealthStatusCallbackProtocol`, `GrpcClientMetricsProtocol`, `RetryMetricsProtocol`,
`CircuitBreakerMetricsProtocol`. Also `metadata_to_dict(metadata)` and `__version__`.
`CircuitBreakerMetricsProtocol`, and the three that describe the optional settings blocks —
`GrpcChannelExtrasProtocol`, `GrpcObservabilityExtrasProtocol`,
`FullGrpcClientSettingsProtocol`. Also `metadata_to_dict(metadata)` and `__version__`.

Names that exist but are **not** re-exported at package level — import them from the module
named beside them:
Expand All @@ -312,11 +315,12 @@ named beside them:
| `AsyncPassiveOutlierInterceptor`, `DEFAULT_QUARANTINE_SECONDS` | `grpc_client_kit.interceptors.outlier` |
| `validate_target`, `MIN_PORT`, `MAX_PORT` | `grpc_client_kit.validation` |
| `create_aio_channel` | `grpc_client_kit.utils` |
| `GrpcChannelExtrasProtocol`, `GrpcObservabilityExtrasProtocol`, `FullGrpcClientSettingsProtocol` | `grpc_client_kit.protocols` |

`ChannelWrapper` and `chain_token` in `grpc_client_kit.channel` are pool internals, left out
of the public surface on purpose: exporting them would freeze the pool's implementation into
the compatibility contract. Do not build on them.
`ChannelWrapper` and `chain_token` in `grpc_client_kit.channel`, and `MethodCircuitState` in
`grpc_client_kit.interceptors.circuit_breaker`, are internals left out of the public surface on
purpose: exporting them would freeze those implementations into the compatibility contract. Do
not build on them — a breaker's state is read through `get_states()`, which returns
`CircuitBreakerStatus` snapshots.

### Settings objects

Expand Down Expand Up @@ -423,7 +427,11 @@ Per-method budgets are the one thing settings cannot express: `timeout` carries
both. Native retries *without* kit retries are fully supported.
19. **Batteries are opt-in, and a missing one is a warning, not an error.**
`import grpc_client_kit` never reaches for an extra. `HealthChecker` resolves on first
attribute access and raises `ImportError` naming `[health]`. Tracing, metrics and the
attribute access and raises `ImportError` naming `[health]` — an `ImportError` and not an
`AttributeError`, so a broken install says so rather than looking like a name that never
existed. The cost is that `hasattr(grpc_client_kit, "HealthChecker")` **propagates that
ImportError instead of returning `False`**, and so does `getattr` with a default; probe with
`importlib.util.find_spec("grpc_health")` or catch the ImportError. Tracing, metrics and the
deadline budget layers are left out of the chain, with a log line, when their extra is
absent — the chain still builds and the calls still run.
20. **There is no sync API and no thread safety.** Everything here assumes one event loop.
Expand Down
6 changes: 3 additions & 3 deletions docs/guide/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,9 @@ No `timeout` block, no timeout interceptor — and therefore no deadline.
These are **optional** and read through `getattr`, so a minimal settings
object stays valid: `credentials`, `options`, `compression`
(`GrpcChannelExtrasProtocol`), plus `sensitive_headers` and
`metrics_registry` (`GrpcObservabilityExtrasProtocol`). Both protocols live in
`grpc_client_kit.protocols`, along with `FullGrpcClientSettingsProtocol` for
settings that carry everything.
`metrics_registry` (`GrpcObservabilityExtrasProtocol`). Both protocols are
exported from `grpc_client_kit` like the rest, along with
`FullGrpcClientSettingsProtocol` for settings that carry everything.

`connectivity` is read the same way — a `ConnectivityConfig` under that name
[tunes the channels](channels.md#from-a-settings-object), its absence leaves
Expand Down
12 changes: 8 additions & 4 deletions docs/guide/deadlines.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,10 +78,14 @@ grants.

Two things worth knowing before you wire it:

- **There is no settings block for this layer.** A
[factory-built chain](configuration.md#settings-objects) carries timeouts,
retries and the breaker; a chain that propagates budgets is a hand-built one,
handed to `GrpcClient(interceptors=...)`. Not to
- **A settings object can carry this layer too.** A `deadline_budget` block
with `reserve_for_next` is read from
[the settings object](configuration.md#settings-objects) with `getattr` — so
a factory-built chain gets the layer in
[its proper place](#where-it-sits-in-the-chain), with no hand-built chain
involved. What settings cannot express is anything finer — per-method caps
above all; that is what `build_interceptors` is for, and the chain it returns
goes to `GrpcClient(interceptors=...)`. Not to
`create_client(interceptors=...)`, which places what it is given in the outer
slot, above the layers this one has to read —
[why](configuration.md#a-settings-object-end-to-end).
Expand Down
39 changes: 27 additions & 12 deletions docs/guide/health.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,24 @@ pool = ChannelPool(health_checker=checker) # pooled channels get health flags t
await checker.stop() # closes the probe channels
```

Each probe asks for the **overall** server status — the empty service name of
the Health v1 protocol — rather than a per-service one, so a target is healthy
when the process behind it says it is serving at all.
Each probe asks for the service named by `service`, which defaults to the
empty name of the Health v1 protocol — the **overall** server status, so a
target is healthy when the process behind it says it is serving at all. Pass
`service="users.v1.Users"` to ask about one service instead, which is what the
standard protocol is for when a process serves several.

`HealthChecker` is resolved lazily, so `import grpc_client_kit` works on a
bare install; touching `grpc_client_kit.HealthChecker` without the extra
raises an `ImportError` naming it. `HealthCheckerNotRunningError` lives in
`grpc_client_kit.health`, which likewise needs the extra.
raises an `ImportError` naming it. It is an `ImportError` rather than an
`AttributeError` on purpose — a missing extra is an install problem and should
say so — which does mean `hasattr(grpc_client_kit, "HealthChecker")` raises it
rather than answering `False`, and `getattr` with a default does not swallow it
either. Probe for the extra with `importlib.util.find_spec("grpc_health")`, or
catch the `ImportError`.

`HealthCheckerNotRunningError` is a top-level export from `grpc_client_kit`
and needs no extra at all: the caller who meets it is a caller of a balancer,
and a balancer works on a bare install.

## An unchecked target is not a healthy target

Expand Down Expand Up @@ -79,7 +89,7 @@ Three reasons, each sufficient on its own:

- a pooled channel carries the client's interceptor chain, so probing through
it would pollute client metrics and count towards circuit breakers;
- pooled channels belong to application traffic and its lifecycle, while
- pooled channels belong to application traffic and its lifecycle, while
monitoring must run on its own schedule;
- a failed probe closes its channel to force a reconnect, which must never
happen to a channel that application RPCs are holding.
Expand All @@ -90,14 +100,19 @@ Three reasons, each sufficient on its own:
| :--- | :--- | :--- |
| `check_interval` | `30.0` | Seconds between checks of a healthy target |
| `timeout` | `5.0` | Budget for one probe RPC |
| `max_backoff` | `300.0` | Cap on the backoff of a failing target |
| `service` | `""` | Service to probe; the empty name asks about the server as a whole |
| `max_backoff` | `300.0` | Cap on the backoff of a failing target; the checker's own default, not read from settings |

The factory builds a checker only when the settings carry both a
`health_checker` block **and** `targets`, and passes it `check_interval`,
`timeout`, `insecure` and `credentials`. It does **not** forward `options` or
`compression`: if your application channels need specific channel options for
the probes to negotiate HTTP/2 identically, construct `HealthChecker`
yourself and hand it to the pool and the balancer.
`health_checker` block **and** `targets`. It passes `check_interval` and
`timeout` from that block, the block's optional `service`, and `insecure`,
`credentials`, `options` and `compression` from the settings themselves — the
last two so the probes negotiate HTTP/2 exactly as the application channels do,
rather than through a differently configured connection.

Anything the settings cannot express — `on_status_change`, `max_backoff`,
`fail_fast_callback` — means constructing `HealthChecker` yourself and handing
it to the pool and the balancer.

Entering the factory's `async with` starts the checker; leaving it stops the
checker and closes its probe channels. A factory used without that block warns
Expand Down
Loading