Metadata-Version: 2.1
Name: EdpConfigProviderMessageHandler
Version: 1.5.1
Summary: MessageHandlerBuilder implementation to use with EdpConfigProvider
Author-email: Max Guenes <max.santos@e-deploy.com.br>
Requires-Python: <4,>=3.9
Description-Content-Type: text/markdown
Requires-Dist: edpconfigprovider<2,>=1.2.0
Requires-Dist: contextmessagebus<3,>=2.1.0

# EdpConfigProviderMessageHandlerBuilder

Builders that wire an `edpconfigprovider` config (and, optionally, a per-tenant
config) into a `messagehandler.MessageHandler`. The builders resolve the tenant
from the incoming `Message`/`Event` headers, populate the thread-local context,
and hand the resolved config(s) to your handler factory.

## Exports

- `ConfigProviderMessageHandlerBuilder[T]` — single-config builder.
- `MultiTenantConfigProviderMessageHandlerBuilder[T, U]` — extends the base
  builder with a per-tenant config (`U`) resolved via an
  `EnvTenantConfigFactory[U]` and cached per tenant, expiring on a TTL.

## How it works

`build_message_handler(message, event)`:

1. Resolves the context and the tenant from `message.headers` or
   `event.headers` as two independent values, and writes both to the current
   thread:
   - `_edp_context_name` comes **only** from the `context` header — the full
     host, e.g. `tenant.boh.example.com`. It is never derived from `tenant`,
     and is set to `None` when the header is absent.
   - `_edp_tenant_name` comes from the `tenant` header, falling back to
     `context` when `tenant` is absent, with `tenant_name_transform` applied
     (default: split on `.` and keep the first segment; pass `None` to
     disable).

   Both are written on every call, so a thread reused across requests never
   inherits the previous request's values.
2. If no tenant could be resolved and `tenant_mandatory=True`, raises
   `RuntimeError("No tenant")`. Otherwise, when the bus is a
   `MultiContextMessageBus`, calls `set_current_context(context_name)`.
3. Lazily builds the global config (`config_transformer.build_config()`,
   memoized under a lock, re-resolved once `config_ttl` elapses — 15 minutes
   by default) and calls `build_message_handler_with_config(config)`.
4. Any exception raised inside the pipeline is routed through
   `handle_exception` — if `exception_handler` was provided, it is invoked
   with the exception and its return value is returned; otherwise the
   exception is re-raised.

The multi-tenant variant additionally:

- Looks up the tenant via `get_tenant_name()` (reads `_edp_tenant_name`
  from the current thread, returning `None` if unset) inside
  `build_message_handler_with_config`.
- Resolves a per-tenant `U` via `tenant_config_factory.build_config(tenant)`,
  caching the result in `tenant_dict` with double-checked locking (a global
  `_cache_lock` guards a per-tenant `Lock` map).
- Delegates to the abstract
  `build_message_handler_with_tenant_config(config, tenant_config)`.
- If `build_tenant` returns `None`, the result is **not** cached and an error
  is logged; the call falls through to `handle_exception` with
  `Exception("No tenant")`.

## Cache expiration (TTL)

Both the global config and each tenant's config are cached, and **expire after
15 minutes by default** (900 seconds), then get re-resolved
from the provider on the next access. This is what keeps a long-lived process
— a container that never restarts — from serving a configuration that changed
hours ago.

| | Constructor | Environment variable |
|---|---|---|
| Global config | `config_ttl` | `EDP_CONFIG_TTL` |
| Per-tenant config | `tenant_config_ttl` | `EDP_TENANT_CONFIG_TTL` |

Values are in **seconds** and resolve in this order: constructor argument,
then environment variable, then the 15-minute default.

- `0` re-resolves on every call.
- **Any negative value** — `-1` by convention — disables expiration, restoring
  the cache-forever behavior of ≤1.4.0. Accepted both as a constructor
  argument and as an environment variable.
- `None` in the constructor means *not specified*: it falls through to the
  environment variable and then to the default.
- A malformed environment value is logged as a warning and the default is
  used.

Semantics:

- Expiration is lazy: the value is re-resolved on the first access **after**
  the TTL elapses, never by a background thread. Per-tenant entries expire
  independently of one another.
- Refreshes are serialized — the global config by a builder-level lock, each
  tenant by its existing per-tenant lock — so a burst of concurrent requests
  triggers a single rebuild, not one per thread.
- **Stale-on-error:** if a refresh raises (or `build_tenant` returns `None`),
  the previously loaded value keeps being served and the failure is logged.
  The TTL window restarts, so a broken provider is retried once per window
  rather than on every request. An exception on the *first* load still
  propagates — there is nothing to fall back to.

## Usage

### Single-config builder

```python
from edpconfigprovidermessagehandler import ConfigProviderMessageHandlerBuilder
from messagehandler import MessageHandler


class MyHandlerBuilder(ConfigProviderMessageHandlerBuilder[MyConfig]):
    def build_message_handler_with_config(self, config: MyConfig) -> MessageHandler:
        return MyHandler(config)


builder = MyHandlerBuilder(
    config_transformer=my_config_mapper,
    message_bus=bus,
    tenant_mandatory=True,
    exception_handler=lambda ex: NoopHandler(),  # optional
    config_ttl=300,  # optional: override the 15-minute default
)
handler = builder.build_message_handler(message=msg, event=None)
```

### Multi-tenant builder

```python
from edpconfigprovidermessagehandler import MultiTenantConfigProviderMessageHandlerBuilder
from edpconfigprovider.util.environment import EnvTenantConfigFactory
from messagehandler import MessageHandler


class MyMultiTenantBuilder(
    MultiTenantConfigProviderMessageHandlerBuilder[MyConfig, MyTenantConfig]
):
    def build_message_handler_with_tenant_config(
        self,
        config: MyConfig,
        tenant_config: MyTenantConfig,
    ) -> MessageHandler:
        return MyHandler(config, tenant_config)


builder = MyMultiTenantBuilder(
    config_transformer=my_config_mapper,
    tenant_config_factory=EnvTenantConfigFactory(...),
    message_bus=bus,
    tenant_mandatory=True,
    exception_handler=lambda ex: NoopHandler(),  # optional
    config_ttl=300,  # optional: override the 15-minute default
    tenant_config_ttl=600,  # optional: override the 15-minute default
)
handler = builder.build_message_handler(message=msg, event=None)
```

## Constructor parameters

| Parameter | Type | Notes |
|---|---|---|
| `config_transformer` | `ConfigMapper[T]` | Builds the global config. Result is cached on the builder. |
| `tenant_config_factory` | `EnvTenantConfigFactory[U]` | *(multi-tenant only)* Builds the per-tenant config. |
| `message_bus` | `Optional[MessageBus]` | If a `MultiContextMessageBus`, its current context is switched to the resolved context name (the `context` header), not to the tenant. |
| `tenant_mandatory` | `bool` (default `True`) | When `True`, missing tenant raises `RuntimeError("No tenant")`. |
| `exception_handler` | `Optional[Callable[[Exception], Any]]` | Intercepts all exceptions from the build pipeline; if `None`, exceptions propagate. |
| `tenant_name_transform` | `Optional[Callable[[str], str]]` | Normalizes the tenant name extracted from headers. Defaults to splitting on `.` and keeping the first segment; pass `None` to disable. |
| `config_ttl` | `Optional[float]` | Seconds before the global config is re-resolved. Defaults to `EDP_CONFIG_TTL`, then to `900.0` (15 min). Pass `None` to never expire. |
| `tenant_config_ttl` | `Optional[float]` | *(multi-tenant only)* Seconds before a tenant's config is re-resolved. Defaults to `EDP_TENANT_CONFIG_TTL`, then to `900.0` (15 min). Pass `None` to never expire. |

## Extension points

- `build_message_handler_with_config(config)` — required on the base builder.
- `build_message_handler_with_tenant_config(config, tenant_config)` — required
  on the multi-tenant builder.
- `build_tenant(tenant_name)` — defaults to
  `tenant_config_factory.build_config(tenant_name)`; override to customize
  tenant resolution.
