From 5997324c3a97d0200114e7f45343c16a867e7409 Mon Sep 17 00:00:00 2001 From: Mika Cohen Date: Wed, 10 Jun 2026 20:24:09 -0600 Subject: [PATCH 01/10] docs: draft v0.5.2 changelog --- CHANGELOG.md | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 545b07d2..79770ede 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,9 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.5.2] - 2026-06-10 + +### Added + +- Added per-backend `stat_missing` retry probing, so backends that correctly answer `STAT` with `430` can help retry missing articles faster. + +### Changed + +- Wired response write metrics to runtime config and reduced response-metrics logging noise. + ### Fixed -- Backend DNS resolution now uses hickory’s TTL-aware caching behavior, with refreshed lookup handling that avoids unnecessary IPv4 cache clears on IPv6-unreachable failures. +- Backend DNS lookups now respect TTL. +- Hardened retry-path routing and guard handling around pending counts, capacity-weighted initial article probing, and idle-pool preference. ## [0.5.1] - 2026-06-05 @@ -560,6 +571,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Development documentation [0.5.1]: https://github.com/mjc/nntp-proxy/compare/v0.5.0...v0.5.1 +[0.5.2]: https://github.com/mjc/nntp-proxy/compare/v0.5.1...v0.5.2 [0.5.0]: https://github.com/mjc/nntp-proxy/compare/v0.4.0...v0.5.0 [0.4.0]: https://github.com/mjc/nntp-proxy/compare/v0.3.0...v0.4.0 [0.3.0]: https://github.com/mjc/nntp-proxy/compare/v0.2.3...v0.3.0 From aeb9ce4ab69426e0a8909fee66bc7fd6461c78ae Mon Sep 17 00:00:00 2001 From: Mika Cohen Date: Wed, 10 Jun 2026 20:29:53 -0600 Subject: [PATCH 02/10] docs: document stat_missing backend option --- docs/operator/configuration.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/operator/configuration.md b/docs/operator/configuration.md index e58b5598..5589496a 100644 --- a/docs/operator/configuration.md +++ b/docs/operator/configuration.md @@ -113,6 +113,7 @@ password = "reader-password" | `name` | required | Friendly name used in logs and the TUI | | `username` / `password` | unset | Backend auth | | `max_connections` | `10` | Per-backend pool size | +| `stat_missing` | `0` | Probe missing articles with `STAT` before `ARTICLE`/`BODY`/`HEAD` on this backend. Enable it on backends that correctly return `430` to speed up retrying missing articles. | | `tier` | `0` | Lower tiers are preferred first | | `use_tls` | `false` | TLS to the backend | | `tls_verify_cert` | `true` | Keep enabled in production | @@ -137,6 +138,7 @@ Currently supported backend environment fields: - `USERNAME` - `PASSWORD` - `MAX_CONNECTIONS` +- `STAT_MISSING` - `USE_TLS` - `TLS_VERIFY_CERT` - `TLS_CERT_PATH` From 120231661bc5a8df6fe98773087bcf930e351222 Mon Sep 17 00:00:00 2001 From: Mika Cohen Date: Wed, 10 Jun 2026 20:33:07 -0600 Subject: [PATCH 03/10] docs: cover stat_missing in docker and nix examples --- Dockerfile | 2 ++ docker-compose.yml | 4 ++++ docs/operator/operations.md | 3 +++ nix/module.nix | 1 + 4 files changed, 10 insertions(+) diff --git a/Dockerfile b/Dockerfile index 8f6c4eeb..8c756741 100644 --- a/Dockerfile +++ b/Dockerfile @@ -60,6 +60,7 @@ ENV NNTP_PROXY_PORT=8119 \ # ENV NNTP_SERVER_0_HOST=news.example.com # ENV NNTP_SERVER_0_PORT=119 # ENV NNTP_SERVER_0_NAME="News Server 1" +# ENV NNTP_SERVER_0_STAT_MISSING=1 # ENV NNTP_SERVER_0_USERNAME="" # ENV NNTP_SERVER_0_PASSWORD="" # ENV NNTP_SERVER_0_MAX_CONNECTIONS=10 @@ -68,6 +69,7 @@ ENV NNTP_PROXY_PORT=8119 \ # ENV NNTP_SERVER_1_HOST=news2.example.com # ENV NNTP_SERVER_1_PORT=119 # ENV NNTP_SERVER_1_NAME="News Server 2" +# ENV NNTP_SERVER_1_STAT_MISSING=1 # ENV NNTP_SERVER_1_USERNAME="" # ENV NNTP_SERVER_1_PASSWORD="" # ENV NNTP_SERVER_1_MAX_CONNECTIONS=10 diff --git a/docker-compose.yml b/docker-compose.yml index a792f314..a1359b22 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -16,6 +16,7 @@ services: NNTP_SERVER_0_HOST: news.example.com NNTP_SERVER_0_PORT: 119 NNTP_SERVER_0_NAME: "Primary News Server" + NNTP_SERVER_0_STAT_MISSING: 1 # Probe 430s with STAT on backends that support it # NNTP_SERVER_0_USERNAME: your_username # Optional: backend authentication # NNTP_SERVER_0_PASSWORD: your_password # Optional: backend authentication # NNTP_SERVER_0_MAX_CONNECTIONS: 10 # Optional: max connections (default: 10) @@ -87,18 +88,21 @@ services: NNTP_SERVER_0_HOST: news1.example.com NNTP_SERVER_0_PORT: 119 NNTP_SERVER_0_NAME: "News Server 1" + NNTP_SERVER_0_STAT_MISSING: 1 NNTP_SERVER_0_USERNAME: ${BACKEND_USER_0:-} NNTP_SERVER_0_PASSWORD: ${BACKEND_PASS_0:-} NNTP_SERVER_1_HOST: news2.example.com NNTP_SERVER_1_PORT: 119 NNTP_SERVER_1_NAME: "News Server 2" + NNTP_SERVER_1_STAT_MISSING: 1 NNTP_SERVER_1_USERNAME: ${BACKEND_USER_1:-} NNTP_SERVER_1_PASSWORD: ${BACKEND_PASS_1:-} NNTP_SERVER_2_HOST: news3.example.com NNTP_SERVER_2_PORT: 119 NNTP_SERVER_2_NAME: "News Server 3" + NNTP_SERVER_2_STAT_MISSING: 1 NNTP_SERVER_2_USERNAME: ${BACKEND_USER_2:-} NNTP_SERVER_2_PASSWORD: ${BACKEND_PASS_2:-} diff --git a/docs/operator/operations.md b/docs/operator/operations.md index eead5fed..be2ca4a4 100644 --- a/docs/operator/operations.md +++ b/docs/operator/operations.md @@ -17,11 +17,14 @@ docker run -d \ -e NNTP_SERVER_0_HOST=news.example.com \ -e NNTP_SERVER_0_PORT=119 \ -e NNTP_SERVER_0_NAME=Primary \ + -e NNTP_SERVER_0_STAT_MISSING=1 \ -e NNTP_SERVER_0_USERNAME="$BACKEND_USER" \ -e NNTP_SERVER_0_PASSWORD="$BACKEND_PASS" \ nntp-proxy ``` +Set `NNTP_SERVER_*_STAT_MISSING=1` on backends that correctly return `430` for missing articles if you want the proxy to prefetch those misses with `STAT`. + The repository also includes [../../docker-compose.yml](../../docker-compose.yml). ## Metrics and state files diff --git a/nix/module.nix b/nix/module.nix index 2fda7708..18ee8309 100644 --- a/nix/module.nix +++ b/nix/module.nix @@ -136,6 +136,7 @@ in { host = "news.example.com"; port = 563; name = "Primary"; + stat_missing = 1; use_tls = true; tls_verify_cert = true; max_connections = 20; From 53dee345bf3b6d4d008b56197cc6ddba1be0cb94 Mon Sep 17 00:00:00 2001 From: Mika Cohen Date: Thu, 11 Jun 2026 09:33:56 -0600 Subject: [PATCH 04/10] chore: finalize v0.5.2 release metadata --- Cargo.lock | 2 +- Cargo.toml | 2 +- config.full.toml | 18 ++++++++++++++++++ docs/operator/configuration.md | 26 +++++++++++++++++++------- 4 files changed, 39 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 987da501..9c63d8f9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2161,7 +2161,7 @@ dependencies = [ [[package]] name = "nntp-proxy" -version = "0.5.1" +version = "0.5.2" dependencies = [ "anyhow", "arrayvec", diff --git a/Cargo.toml b/Cargo.toml index 5c271480..082e5684 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nntp-proxy" -version = "0.5.1" +version = "0.5.2" edition = "2024" rust-version = "1.88" description = "NNTP proxy server with per-command backend multiplexing, caching, metrics, and TUI dashboard" diff --git a/config.full.toml b/config.full.toml index 162a9c95..2dbcc581 100644 --- a/config.full.toml +++ b/config.full.toml @@ -19,6 +19,10 @@ validate_yenc = true # Minimum log level for debug.log file (default: "warn") # Options: "error", "warn", "info", "debug", "trace" log_file_level = "warn" +# Optional interval in seconds for response write metrics logging (default: disabled) +# response_write_metrics_secs = 10 +# Optional interval in seconds for client writer lock contention metrics (default: disabled) +# client_writer_lock_metrics_secs = 10 # Optional stats persistence path used by the TUI dashboard # stats_file = "/var/lib/nntp-proxy/stats.json" @@ -31,6 +35,20 @@ backend_selection = "least-loaded" # Enable adaptive availability prechecking for STAT/HEAD commands adaptive_precheck = false +# Queue backpressure keeps article routing from piling more work onto +# already-saturated backend connection pools. +[routing.queue.backpressure] +enabled = true +# Soft threshold for queued requests per backend connection, as a percentage. +# Above this, routing penalizes the backend but may still select it. +soft_waiters_per_connection_percent = 25 +# Hard threshold for queued requests per backend connection, as a percentage. +# Above this, routing filters the backend while alternatives exist in the tier. +hard_waiters_per_connection_percent = 50 +# Sleep duration in milliseconds before retrying when every eligible backend +# in the preferred tier is above the hard threshold. +all_busy_sleep_ms = 1 + # Memory Configuration # These settings affect transport buffers and buffer pools, not article storage. [memory] diff --git a/docs/operator/configuration.md b/docs/operator/configuration.md index 5589496a..4343d203 100644 --- a/docs/operator/configuration.md +++ b/docs/operator/configuration.md @@ -17,13 +17,14 @@ Keep the config ordered from general settings to concrete identities: 1. `[proxy]` 2. `[routing]` -3. `[memory]` -4. `[cache]` -5. `[cache.disk]` -6. `[health_check]` -7. `[client_auth]` -8. `[[client_auth.users]]` -9. `[[servers]]` +3. `[routing.queue.backpressure]` +4. `[memory]` +5. `[cache]` +6. `[cache.disk]` +7. `[health_check]` +8. `[client_auth]` +9. `[[client_auth.users]]` +10. `[[servers]]` See [../../config.full.toml](../../config.full.toml) for a complete example. @@ -38,6 +39,8 @@ See [../../config.full.toml](../../config.full.toml) for a complete example. | `threads` | `1` | Use `0` for CPU-count worker threads | | `validate_yenc` | `true` | Validate yEnc structure/checksums | | `log_file_level` | `"warn"` | Filter for the optional local `debug.log` appender | +| `response_write_metrics_secs` | unset | Optional interval for response write metrics logging | +| `client_writer_lock_metrics_secs` | unset | Optional interval for client-writer lock contention metrics logging | | `stats_file` | unset | If unset, metrics persistence defaults to `stats.json` next to the config file | ### `[routing]` @@ -48,6 +51,15 @@ See [../../config.full.toml](../../config.full.toml) for a complete example. | `backend_selection` | `least-loaded` | Or `weighted-round-robin` | | `adaptive_precheck` | `false` | Adaptive availability precheck for `STAT`/`HEAD` | +### `[routing.queue.backpressure]` + +| Field | Default | Notes | +| --- | --- | --- | +| `enabled` | `true` | Enable queue-pressure-aware backend filtering | +| `soft_waiters_per_connection_percent` | `25` | Penalize backends above this queued-requests-per-connection threshold | +| `hard_waiters_per_connection_percent` | `50` | Filter backends above this threshold while alternatives exist in the tier | +| `all_busy_sleep_ms` | `1` | Sleep before retrying when every eligible backend in the preferred tier is hard-saturated | + ### `[memory]` These settings control transport memory, not stored article bodies. From 937dc3cf89dc2aff5cffdff9be249b1c5443888e Mon Sep 17 00:00:00 2001 From: Mika Cohen Date: Fri, 12 Jun 2026 16:01:56 -0600 Subject: [PATCH 05/10] chore: align 0.5.2 changelog with release commits --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 79770ede..60966a87 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,12 +16,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Wired response write metrics to runtime config and reduced response-metrics logging noise. +- Added queue backpressure routing configuration in `config.full.toml` and documented + `[routing.queue.backpressure]` in operator configuration docs. ### Fixed - Backend DNS lookups now respect TTL. - Hardened retry-path routing and guard handling around pending counts, capacity-weighted initial article probing, and idle-pool preference. +### Docs + +- Documented the `stat_missing` backend option and routing behavior in operator docs. +- Updated `docker-compose.yml`/`Dockerfile` examples and Nix module documentation + to cover `stat_missing` deployment usage. +- Added release metadata/docs updates for v0.5.2 configuration fields such as + response write and client-writer lock contention metric intervals. + ## [0.5.1] - 2026-06-05 ### Fixed From a6b7ad486f6ea02265d2012bc2d50a916a655826 Mon Sep 17 00:00:00 2001 From: Mika Cohen Date: Fri, 12 Jun 2026 16:03:18 -0600 Subject: [PATCH 06/10] chore: refine 0.5.2 changelog release notes --- CHANGELOG.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 60966a87..342f937e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -## [0.5.2] - 2026-06-10 +## [0.5.2] - 2026-06-11 ### Added @@ -18,6 +18,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Wired response write metrics to runtime config and reduced response-metrics logging noise. - Added queue backpressure routing configuration in `config.full.toml` and documented `[routing.queue.backpressure]` in operator configuration docs. +- Finalized v0.5.2 release metadata and config coverage, including response-write and + client-writer lock contention interval settings. ### Fixed From aa1dd1b874042c94f66b44c6e0182e3e01c24848 Mon Sep 17 00:00:00 2001 From: Mika Cohen Date: Fri, 12 Jun 2026 16:05:33 -0600 Subject: [PATCH 07/10] chore: expand 0.5.2 changelog with merged fixes --- CHANGELOG.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 342f937e..bb5da901 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Added per-backend `stat_missing` retry probing, so backends that correctly answer `STAT` with `430` can help retry missing articles faster. +- Added runtime CPU pinning support for multi-threaded workloads via a new `CpuPinning` runtime mode, using worker-thread startup hooks so pinned threads are the ones running proxy work. +- Added [`rustix`](https://docs.rs/rustix)-based affinity handling for Linux CPU pinning so we can remove the direct `nix` pinning dependency path. ### Changed @@ -20,11 +22,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 `[routing.queue.backpressure]` in operator configuration docs. - Finalized v0.5.2 release metadata and config coverage, including response-write and client-writer lock contention interval settings. +- Removed legacy CLI compatibility aliases and aliases that are now obsolete after the cleanup pass: `--no-tui`, `--backend-strategy`, `--cache-capacity`, `--cache-ttl`, `--ttl-secs`, `--cache-articles`, `--store-articles`, and legacy `NNTP_PROXY_CACHE_*` env fallbacks. +- Removed the dead ordered large-transfer pipeline path and associated retry gate/feature code now that it is permanently disabled. +- Replaced visibility and internal API cleanup work from the Rust 1.88/private-first pass, including module privacy tightening and simplification of small conditional branches. +- Replaced `ResponseTransferError::into_anyhow` and `duration_polyfill` with idiomatic Rust 1.88-safe error and duration handling at call sites. +- Adopted private-first encapsulation by tightening crate module visibility where public re-exports already provide the intended external surface. ### Fixed - Backend DNS lookups now respect TTL. - Hardened retry-path routing and guard handling around pending counts, capacity-weighted initial article probing, and idle-pool preference. +- Fixed a user-active connection-count regression that could diverge from true active-session totals; the dashboard now consistently reports active sessions from typed user-metric counters. +- Migrated user/session metric collection to typed newtypes (including `ZERO` constructors) and arithmetic helpers so typed counters are incremented at source and cannot be accidentally mixed. +- Fixed metric and stats storage consistency by updating update paths to use typed metrics throughout collection, reducing drift in user gauges and totals. ### Docs @@ -33,6 +43,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 to cover `stat_missing` deployment usage. - Added release metadata/docs updates for v0.5.2 configuration fields such as response write and client-writer lock contention metric intervals. +- Added documentation for cleanup and migration context from the maintenance PR stack + around private-first cleanup, deprecated options removal, and runtime pinning behavior. ## [0.5.1] - 2026-06-05 From 70b669f52250749815f14404da7cb4b0e4651f21 Mon Sep 17 00:00:00 2001 From: Mika Cohen Date: Mon, 15 Jun 2026 11:38:14 -0600 Subject: [PATCH 08/10] docs: note recent TUI fixes in changelog --- CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bb5da901..89ac51a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed + +- Fixed TUI session and user connection counts so active sessions stay tied to the session-owned gauge instead of drifting or disappearing. +- Reduced TUI render allocations by writing directly into the buffer, borrowing chart-label names, and using stack-backed formatting where possible. + +### Changed + +- Pushed the session-count model through typed metrics snapshots, dashboard conversions, and TUI rendering so count mixups become compile-time errors instead of runtime bugs. + ## [0.5.2] - 2026-06-11 ### Added From 0a9e16753c2e016d06f37ee460c7fcd6e50b6b03 Mon Sep 17 00:00:00 2001 From: Mika Cohen Date: Mon, 15 Jun 2026 11:45:16 -0600 Subject: [PATCH 09/10] docs: move TUI notes into 0.5.2 --- CHANGELOG.md | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 89ac51a5..aaa0c945 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,15 +7,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -### Fixed - -- Fixed TUI session and user connection counts so active sessions stay tied to the session-owned gauge instead of drifting or disappearing. -- Reduced TUI render allocations by writing directly into the buffer, borrowing chart-label names, and using stack-backed formatting where possible. - -### Changed - -- Pushed the session-count model through typed metrics snapshots, dashboard conversions, and TUI rendering so count mixups become compile-time errors instead of runtime bugs. - ## [0.5.2] - 2026-06-11 ### Added @@ -23,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added per-backend `stat_missing` retry probing, so backends that correctly answer `STAT` with `430` can help retry missing articles faster. - Added runtime CPU pinning support for multi-threaded workloads via a new `CpuPinning` runtime mode, using worker-thread startup hooks so pinned threads are the ones running proxy work. - Added [`rustix`](https://docs.rs/rustix)-based affinity handling for Linux CPU pinning so we can remove the direct `nix` pinning dependency path. +- Added TUI session/user-count lifecycle fixes so active sessions stay tied to the session-owned gauge instead of drifting or disappearing. ### Changed @@ -36,6 +28,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Replaced visibility and internal API cleanup work from the Rust 1.88/private-first pass, including module privacy tightening and simplification of small conditional branches. - Replaced `ResponseTransferError::into_anyhow` and `duration_polyfill` with idiomatic Rust 1.88-safe error and duration handling at call sites. - Adopted private-first encapsulation by tightening crate module visibility where public re-exports already provide the intended external surface. +- Pushed the session-count model through typed metrics snapshots, dashboard conversions, and TUI rendering so count mixups become compile-time errors instead of runtime bugs. ### Fixed @@ -44,6 +37,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fixed a user-active connection-count regression that could diverge from true active-session totals; the dashboard now consistently reports active sessions from typed user-metric counters. - Migrated user/session metric collection to typed newtypes (including `ZERO` constructors) and arithmetic helpers so typed counters are incremented at source and cannot be accidentally mixed. - Fixed metric and stats storage consistency by updating update paths to use typed metrics throughout collection, reducing drift in user gauges and totals. +- Reduced TUI render allocations by writing directly into the buffer, borrowing chart-label names, and using stack-backed formatting where possible. ### Docs From f0f6f5a383bd56078bdeb52bd47cef80e5b4ad0e Mon Sep 17 00:00:00 2001 From: Mika Cohen Date: Fri, 11 Sep 2026 12:00:20 -0600 Subject: [PATCH 10/10] docs: vary release changelog bullet openings --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aaa0c945..e00820d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,8 +12,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Added per-backend `stat_missing` retry probing, so backends that correctly answer `STAT` with `430` can help retry missing articles faster. -- Added runtime CPU pinning support for multi-threaded workloads via a new `CpuPinning` runtime mode, using worker-thread startup hooks so pinned threads are the ones running proxy work. -- Added [`rustix`](https://docs.rs/rustix)-based affinity handling for Linux CPU pinning so we can remove the direct `nix` pinning dependency path. +- Runtime CPU pinning support is now available for multi-threaded workloads via a new `CpuPinning` runtime mode, using worker-thread startup hooks so pinned threads are the ones running proxy work. +- Linux CPU pinning now uses [`rustix`](https://docs.rs/rustix)-based affinity handling, so we can remove the direct `nix` pinning dependency path. - Added TUI session/user-count lifecycle fixes so active sessions stay tied to the session-owned gauge instead of drifting or disappearing. ### Changed