Skip to content

v0.8.0-rc2

Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 07 Sep 22:39

GRAIN🌾 v0.8.0-rc2

Stabilizing the client release β€” and teaching the relay to stay healthy
Release candidate Β· 9.7.2026
Full Changelog: v0.8.0-rc1...v0.8.0-rc2


rc1 shipped the client library and the web client built on it. rc2 is what running rc1 on a live relay taught us: a full storage map was silently dropping writes, retention wasn't actually draining, and a single bad filter could kill a subscription. This candidate fixes the data-loss path end to end, makes NIP-11 tell the truth, lets the config repair itself, and turns the relay browser into a self-discovering one built on NIP-66 monitors.

If you ran rc1 in production, rc2 is the one to run β€” and map_size_mb now defaults to 64 GB, so fresh deploys have real headroom.


πŸ›Ÿ Durability β€” no more silent write loss

rc1 could accept an event, ack OK, and then fail to store it. nostrdb's writer commits asynchronously, so when the LMDB map filled the write failed on a background thread while the client had already been told "accepted." rc2 closes that gap from both ends.

  • No more crash-loop under subscription churn. A late relay message racing a closing subscription hit a send on closed channel panic in the client relay pool (which the relay uses for outbox/backup-relay work), crashing and restarting the process β€” every relay in the CI matrix was panicking. Sends now serialize against subscription teardown, so a message that arrives mid-close is dropped instead of taking the process down. Covered by a new -race regression test.
  • Retention actually drains now. The purge walked an unfiltered until query that returns storage order, not time order β€” so it never paged past the newest events and the backlog just grew. It now pages per kind in created-at order, deleting oldest-first until the retention window is satisfied. This was the root cause of the map filling in the first place.
  • Writes are refused before the map fills. A new map-usage gauge reads LMDB's fill level (a new ndb_map_usage accessor in the fork) and logs it on an interval β€” INFO normally, WARN at 80%, ERROR at 95%. At 97% the write path starts rejecting new events with error: relay storage unavailable instead of silently losing them, leaving free pages for the purge's own delete transactions to run.
  • Silent writer failures are now visible. The fork counts every writer-thread failure (map full, bad txn); grain polls that counter and logs nostrdb writer failures detected β€” accepted events may not have been stored when it climbs, so the "OK-but-not-durable" gap can never hide again.
  • Reader stability under load. The environment now opens with MDB_NOTLS and a raised reader-slot ceiling, so Go's goroutine-to-thread migration can't exhaust LMDB reader slots and start failing read transactions under concurrency. This pairs with upstream's new stale-reader clearing on init (synced from damus-io) β€” MDB_NOTLS stops live-process slot orphaning from thread migration, while the reader check reclaims slots left behind by dead processes.
  • Bigger default map. map_size_mb defaults to 65536 (64 GB) β€” the old 4 GB default filled fast on a busy relay. Existing configs are bumped on load.
  • Lower write latency. Dropped a per-write fsync (O_SYNC) on the log path and removed hot-path request logging that was adding latency on every connection.
For operators β€” the map-usage knob and the upgrade path
  • Watch for Database map filling / Database map nearly full in the logs β€” that's your signal to raise map_size_mb or let retention catch up.
  • The reject threshold (97%) deliberately sits below full so the purge can always run. If you see relay storage unavailable acks, the map is nearly full and not draining β€” check your retention config.
  • These fixes required changes to the vendored nostrdb C library (grain maintains its own fork). No action needed on upgrade; the dev Docker build rebuilds the C lib from source.
  • rc2 also re-syncs the fork with damus-io upstream, picking up the stale-reader check above and ndb_compact() (selective DB compaction) β€” the latter is groundwork; grain doesn't invoke it yet, but it's the reclaim path that pairs with the map-usage gauge and per-kind purge.
For developers β€” the REQ robustness fix
  • A subscription with multiple filters where one couldn't be converted to the storage layer's form used to fail the entire REQ. grain now drops only the unconvertible filter and runs the rest, and sorts results newest-first.

πŸ›°οΈ Self-discovering relay monitors β€” NIP-66 (#104)

The known-relays browser no longer shows the mailbox union β€” it shows a bounded, liveness-backed set discovered from NIP-66 relay monitors, with no hardcoded monitor list.

  • Discovers its own monitors. grain finds kind-10166 monitor announcements across the relays it already touches (widening to a sample of known relays if none surface), then pulls each monitor's kind-30166 relay records β€” URL, RTT, supported NIPs, network, requirements, geohash.
  • Trusts by consensus. Monitors whose reports are mostly uncorroborated are discarded; a relay has to be reported by at least K trusted monitors to make the set. Results rank by agreement, then RTT, then URL.
  • Self-heals. A health-roll re-discovers on an interval and evicts monitors whose data has gone stale past their own declared publish frequency β€” so the set stays current and propagates as the monitor network changes.
  • Richer browser. The relay browser shows the NIP-66 metadata and adds "monitored only" and "hide auth/paid" filters. The header's x/y relays count now reflects this browsable set.

The client library exposes the whole thing β€” DiscoverRelays(ctx), DiscoveredRelays() []DiscoveredRelayView, and StartDiscoveryRoll(ctx, interval) β€” documented in the client-library guide. Discovery is advisory: your configured and own relays are always kept as a fallback source, per the NIP.


πŸ“‡ NIP-11 now reflects your actual config

rc1 served a limitation block of static zeros. It's now live.

  • limitation is config-informed β€” max_message_length, max_subscriptions, max_limit, auth_required (from public/private mode), restricted_writes (from the whitelist), and max_content_length, a new config knob you can set from the admin panel. It's wired into NIP-11, into event validation, and into config validation.
  • created-at limits (created_at_lower_limit / created_at_upper_limit) are derived from your event_time_constraints.
  • Metadata you couldn't reach before β€” relay_countries, language_tags, and tags β€” are now editable from the dashboard and served in NIP-11.

🧰 Config that repairs itself

You shouldn't have to hand-edit YAML, and an older config shouldn't break the app.

  • Legacy config shapes are auto-corrected on load β€” an out-of-date config is migrated in place rather than rejected, and the admin is prompted where something needs attention.
  • The admin forms mirror the live config. The event-purge form and the backup-relay badge were showing stale or default state instead of what the relay is actually running β€” they now reflect the loaded config, so "enabled with no URL" and the purge-form/dashboard mismatch are gone.
  • Size limits re-apply on reload β€” changing them and reloading now takes effect instead of silently keeping the old values.
  • keep_kinds β€” a per-kind purge exclude list, so you can retain specific kinds (e.g. relay lists, profiles) even when their category is being purged. Complements the existing kinds_to_purge allow-list.
  • No more false "relay unclaimed" banner on login/refresh from a transient NIP-11 blip.

πŸ“š Docs

  • The client-library guide now documents NIP-66 relay discovery (the consumer API, consensus, and health-roll) and the DiscoveredRelayView type (#101).
  • Docker setup is web-UI-first, the dead GRAIN_ENV var is gone, the internal design doc was folded into the guide, and the README now points at a de-versioned docs hub with the stale links fixed.

⬆️ Upgrading from rc1

  • No config migration required β€” legacy shapes are corrected automatically and map_size_mb is bumped on load. Review the new max_content_length and keep_kinds knobs if you want them.
  • This remains a release candidate β€” please file anything you hit before the final v0.8.0.

Issues: #101 Β· #104