Half the memory per node; an abandoned UnifiedPush topic still has its row when a push 7+ days later decides on the 404, as long as it is pushed to at least every two weeks.
A cluster message whose topic is known on the receiving node but has no subscriber there was published to nobody and uncounted, and its Keepalive kept the topic from ever expiring: a passive node held every topic the active one published to. It is now counted in ntfy_cluster_messages_wasted_total and not published, so idle topics expire after topicExpungeAfter.
With several nodes, counts one node admitted in the last seconds of a key's period could be written after another node moved the row to the new period, and were added to the new day. They are the old day's usage, so they are dropped: an undercount of at most one sync interval, never a double count.
Rename listen-admin-unix to experimental-listen-admin-unix and drop the
/run/ntfy/admin.sock default: the CLI side (#2089, #2135) is not merged,
so nothing uses the socket yet. The startup warning no longer points at a
CLI fallback, and the docs say the commands do not use it yet.
The server uses the topic package instead of the limit service for the rate
visitors: a subscribe sets the rate visitor (user ID and masked IP), a poll,
a stream close and the manager's keepalive record subscriber activity, a
refused push notes an unknown topic (insert-only). The limiter key is derived
on every push from the rate visitor's current tier (refreshed in the
background every minute, never on the publish path), so a downgrade or a
deleted account charges the IP. Before a 404 for an abandoned topic the row
is read from the database. The Matrix gateway rejects the pushkey under the
same 7-day rule; its 12 h per-node rule is gone.
One row per topic (first_seen_at, subscribed_at, rate visitor user and IP,
updated_at), merged by rule (earliest, latest, last written) so the nodes can
write in any order, synced by the package's own 5 s worker from a dirty set.
User schema 10 -> 11 now creates this table.
"Rate visitor" is the term the docs already use. The table is called topic so it can hold more per-topic, cluster-wide state later. seen_at keeps its name, since a noted row's seen_at is a refused push, not a subscription.
Prune now forgets a key's bucket entry once forgetting it changes nothing:
every bucket is full again, no bandwidth is tallied in the key's current
period, and it has not been used for a minute. Daily counts live in the
counters and are unaffected. Prune loses its idle parameter, since a bucket
without refill never holds a key.
Note when a bucket is full again after every refill, including denied calls
and Tokens, so a slower rule (a tier downgrade) cannot get the key forgotten
early. Tally only bandwidth outside the counters, and keep the entry while
that tally is nonzero, so the usage it reports survives; tallying requests
would keep every key for the rest of its period.
- GET /v1/users?username= returns one user, so the CLI does not download all users
- Never replace a non-socket file at listen-admin-unix, and say so when the socket belongs to another user's server
- Log when the admin socket is listening; default to /var/run/ntfy on the BSDs too
- Deleting an admin user over HTTP returns 40302 (admin socket only), like the other admin-only operations
- Docker images ship a /run/ntfy directory for the socket
A chunk whose statement was sent but whose answer came after the deadline may have committed
(lab: 60 messages counted as 86-89 at 2.2-2.4 s to Postgres). It is now retired like a written
chunk; if it did not commit, its counts are lost, which fails open like an outage. Errors from
the server, errors before sending, and every SQLite error still retry.
The benchmarks (400k request-only keys, 50k publishers) measure a publish, the sync scan and
memory per key; they were used to compare the split counter/bucket shards with one merged map
(publish ~10% faster merged, sync scan 50x slower, memory the same), so the shards stay split.
Server.stop never closed the cluster, so a stopped node stayed registered and its
heartbeat kept running against the closed database, logging from leaked goroutines.
In CI that raced with captureLog's unsynchronized buffer in a later test. captureLog
now uses syncBuffer as well.
Like the Postgres pool: user.OpenSQLite opens the auth-file (case-sensitive LIKE),
NewSQLiteManager takes the handle and migrates it, and the usage store gets the same handle
instead of reaching into the manager (Manager.DB is gone). The usage table stays part of the
user schema.
A node cut off from its database only showed in the logs. The health check loop now always
runs and pings the primary too; ntfy_db_up (1/0, per pool, next to ntfy_db_pool_*) is the
result of the last check, so an alert can fire on ntfy_db_up{role="primary"} == 0.
The service no longer reads the database clock: no clock offset, no clock query per sync
round. Periods, the read-back watermark and the rows' updated_at all use the node's clock, so
the database's clock does not matter; the app nodes must run NTP (documented in config.md).