# v0.90.0 — Freshness Controller & Self-Tuning

> **Status:** Planned
> **Scope:** Large
> **User promise:** *"Tell it how fresh I need data; it figures out the rest."*
> **Blocked by:** [v0.89.0](v0.89.0.md)
> **Renumbered:** previously v0.85.0. The user-facing `target_freshness`
> control moved forward to [v0.86.0](v0.86.0.md) so the knob v1.0 freezes gets
> real usage before the freeze; this release makes it authoritative.

## Theme

Users do not want to reason about scheduler intervals, batch sizes, worker
counts and fallback thresholds. They want to say how stale the answer is
allowed to be. [v0.86.0](v0.86.0.md) started accepting that statement. This
release makes pg_trickle act on it continuously and demotes every other knob to
an override.

> **"Tell pg_trickle how fresh you need the answer, not how to schedule it."**

This mirrors where the surrounding ecosystem has landed — PostgreSQL's own
materialized views still require explicit `REFRESH MATERIALIZED VIEW`, while
Timescale exposes continuous-aggregate refresh policies and Materialize exposes
freshness and lag as first-class concepts. Freshness is the concept users
already have; pg_trickle should accept it directly.

## Items

### SLA-1: `target_freshness` becomes authoritative

```sql
SELECT pgtrickle.create_stream_table(
    'sales_by_region',
    $$SELECT region, SUM(amount) FROM orders GROUP BY region$$,
    target_freshness => '2 seconds'
);

SELECT pgtrickle.alter_stream_table(
    'sales_by_region', target_freshness => '1 minute'
);
```

`target_freshness` is defined as the p95 age of the data visible in the stream
table, measured commit-to-visible using the metric shipped in v0.81.0. In
v0.86.0 it was translated once into the existing settings. From this release the
scheduler owns every derived parameter continuously, and the explicit knobs
(`refresh_interval`, batch sizes, concurrency) become optional overrides that
are reported as such by `explain()`.

Special values `'on_commit'` and `'manual'` keep the meaning defined in
v0.86.0.

### SLA-2: The controller

A closed-loop controller per stream table decides, once per scheduler tick:

- **when** to refresh (interval derived from observed refresh cost and inflow
  rate, not a fixed timer)
- **DIFFERENTIAL vs FULL**, using measured cost rather than a static delta-ratio
  threshold
- **batch size** for pipelined refresh, from observed row width and memory
  budget
- **concurrency**, within the global bound from v0.87.0
- **priority**, so a 1-second SLA outranks a 1-hour SLA under contention
- **whether to defer**, when the instance is overloaded (v0.87.0 backpressure)

The controller uses hysteresis — a decision changes only after three consecutive
consistent measurements — and its inputs and last decision are exposed in
`explain()` so behaviour is never mysterious.

### SLA-3: Adaptive worker pool sizing

Replace the static worker count with automatic sizing driven by real signals:

- CPU utilisation > 70% → reduce workers by one (do not starve OLTP)
- refresh queue depth > 2× workers → add one worker
- all workers idle for three consecutive ticks → shrink to the configured floor
- an SLA at risk of breach → add workers up to the configured ceiling

New GUCs: `pg_trickle.adaptive_workers = true`,
`pg_trickle.adaptive_workers_min = 1`, `pg_trickle.adaptive_workers_max = 8`
(bounded by `max_worker_processes` and by the authoritative cluster limit from
[v0.85.0](v0.85.0.md) OPS-81-1).

> The names `pg_trickle.min_workers` / `pg_trickle.max_workers` are **not**
> available: `pg_trickle.max_workers` is a retired GUC that `just docs-lint`
> blocks in active documentation. Reusing a retired name with new semantics
> would break the lint and mislead anyone upgrading from a version that had it.

### SLA-4: SLA reporting and alerting

Freshness becomes a reported, checkable status rather than something users infer
from timestamps:

```
SELECT * FROM pgtrickle.freshness();
```

```
 stream_table      | target | p50    | p95    | p99    | status
-------------------+--------+--------+--------+--------+---------------
 sales_by_region   | 2s     | 310ms  | 730ms  | 1.4s   | meeting SLA
 customer_360      | 5s     | 4.1s   | 9.8s   | 21s    | BREACHING
```

- `pg_stat_pgtrickle` (shipped v0.86.0) gains `p95_freshness_ms` and
  `sla_status`; `target_freshness_ms` is already there
- `health_check()` reports sustained breaches with the controller's diagnosis
  (for example: *"refresh cost 8.2s exceeds 5s target; consider a coarser
  target or reducing join breadth"*)
- Prometheus/OTel metrics expose target, actual and breach duration

### SLA-5: Continuous infeasible-SLA detection

The creation-time check shipped in v0.86.0 (UX-7). This release adds the
continuous half: a stream table whose workload has grown past feasibility is
flagged in `health_check()` and by the controller instead of consuming
resources in a losing race.

```
WARNING:  target_freshness '500 ms' is not achievable for this query
DETAIL:   Minimum observed refresh cost is 3.4 s (dominated by orders → line_items join).
HINT:     Use target_freshness >= '5 s', or narrow the defining query.
```

### SLA-6: Retire the knobs users should not need

Every GUC and per-stream-table option is audited and classified as *user-facing*,
*advanced override*, or *internal*. Internal knobs are removed or hidden;
advanced overrides are documented as "only if the controller is wrong, and please
tell us why". The documented configuration surface shrinks measurably, and the
config advisor from v0.81.0 becomes a translator from workload description to
`target_freshness` rather than a list of settings to hand-tune.

## Exit criteria

- [ ] The controller owns every derived parameter for stream tables with a
      `target_freshness`; explicit knobs are reported as overrides by
      `explain()`
- [ ] Controller meets p95 targets for a mixed workload of 100 stream tables
      across 1s/10s/1m targets in a soak test
- [ ] Adaptive worker sizing demonstrated to shrink under OLTP load and grow
      under SLA pressure, without oscillation, and never above the
      authoritative cluster worker limit from v0.85.0
- [ ] `pgtrickle.freshness()` and `sla_status` shipped, documented and exported
      to Prometheus/OTel
- [ ] Infeasible targets flagged continuously, not only at creation
- [ ] Documented configuration surface reduced; every remaining user-facing GUC
      justified in `docs/CONFIGURATION.md`
- [ ] No GUC removed in SLA-6 without a deprecation entry in `UPGRADING.md`;
      removals land here rather than after the v0.93.0 feature freeze
