Commit Graph
135 Commits
Author SHA1 Message Date
binwiederhier c113e20677 Share daily limits through the usage table, drop the midnight reset and the stats queue
The server builds the usage store on the user database (the shared pool on
Postgres, the auth-file on SQLite) and starts the limiter before serving. A
visitor without a user writes its message count from a tenth of the cap on,
everyone else from the first message; anonymous emails get the daily cap the
replenish interval implies. The account API reports reset_at. The user stats
columns are no longer read or written.

Claude-Session: https://claude.ai/code/session_01DeXuhSjq5Gta5qPJgSceFe
2026-10-30 20:41:36 +00:00
Philipp C. Heckel 3d967b0db0 Merge pull request #2078 from cangen185-sudo/codex/fix-parse-size-terabytes
fix(util): accept terabyte size units without overflow
2026-10-28 22:29:39 -04:00
binwiederhier 4bbb4418a5 One limiter model in package limit, replacing the three limiter types in the visitor
Allow(key, rule, n), Refund, Tokens and Usage on a service that keeps a
token bucket and a daily count per key and kind, sharded by key, with the
rule passed in on every call. The server's visitor keeps its identity and
the resolved limits and asks the service; rate.Limiter, util.RateLimiter
and util.FixedLimiter are gone from it (subscriptions stay a per-node
gauge). util.RateLimiter, NewBytesLimiter and BurnTokens had no users
left and are removed.

Behavior changes: refunds work for every kind (a revert on the bandwidth
limiter hands the bytes back), a tier change applies on the next request
without handing back full buckets, a changed tier limit applies without
a tier ID change, and the daily email limit is a real daily cap on top
of the bucket. Step 2 of plans/261025-shared-rate-limits.md.
2026-10-25 15:50:03 +00:00
cangen185-sudo a67aee787f fix(util): accept terabyte size units without overflow 2026-10-20 16:10:57 +08:00
binwiederhier 134d8bb653 Show each cluster node's view of its peers: status endpoint, per-peer metrics, CLI
A link that only works one way (a firewall dropping the cluster port in one direction) left
both nodes healthy: the sender only logged lost batches and bumped an unlabeled error counter,
and the receiver had no way to see it never heard from that peer. Every node now counts the
message batches it sends to and receives from each peer, and shows them:

- GET /v1/cluster/status on the cluster listener (secret-authenticated): role, leadership,
  health, and per peer the outbound and inbound counters, last success and last error
- ntfy_cluster_peer_batches_sent_total{peer,result}, ntfy_cluster_peer_batches_received_total{peer}
  and ntfy_cluster_peer_last_send_success_timestamp_seconds{peer}
- ntfy cluster status [--json], which reads the listener and secret from the server config and
  lists the link problems

/v1/health and /v1/ready are unchanged.
2026-10-18 22:59:21 +00:00
binwiederhier 85d7d3f1a4 De-duplicate subscription topics and limit them to 150
Repeating a topic in a subscription replayed its cache once per repetition.
2026-10-18 13:00:55 +00:00
binwiederhier 9c709731df Fixes 2026-10-07 14:46:32 +00:00
binwiederhier e536434097 Pass the BatchingQueue buffer size from the caller
The message cache decides how many batches may wait for its writer (queueBufferedBatches in
message/cache.go, next to the shutdown flush timeout it has to fit in); util stays policy-free.
2026-10-07 12:30:36 +00:00
binwiederhier 99d55175d4 Keep the message batch writer ahead of publish bursts
On cron bursts (top of the hour), the single batch writer fell behind: it wrote one INSERT round
trip per message, so against a busy remote Postgres it managed ~100 rows/s while ~300/s arrived.
New messages took up to ~40s to reach the database (polls missed them), and publishes blocked on
the unbuffered queue for up to ~25s.

- Postgres: write each batch with one multi-row INSERT (up to 1000 rows per statement) instead of
  one statement per row. SQLite keeps the row-by-row path.
- Skip DeleteScheduledBySequenceID when the client did not set a sequence ID: the sequence ID is
  then the message's own fresh ID, so no scheduled message can match. This saves a transaction on
  the primary for almost every publish.
- Buffer up to 100 batches in the BatchingQueue, so a briefly slow writer does not stall publishes.
- dbtest: add a latency proxy (CreateTestPostgresWithLatency, WithLatency) to test round trips.
2026-10-07 11:54:46 +00:00
binwiederhier 1275f52b9a Bump deps 2026-10-07 03:39:36 +00:00
Philipp C. Heckel 9f3d87e36e Merge pull request #1931 from ALPHACOM-Brehmer/dev
Support RFC 3339 timestamps for delays
2026-10-06 23:23:44 -04:00
binwiederhier 8ead5751ed Enforce the burn debt cap per reservation, and say what it does not cover
A review of this PR pointed out that the cap was computed from a separate
read of the available tokens: a read and a reservation are two operations, so
anything consuming the limiter in between makes the read stale and the burn
lands deeper than one burst. The cap now comes from each reservation's own
delay, which the limiter computes atomically with the consumption, and the
chunk that would run past one burst of replenishment is cancelled.

That holds the cap against any amount of ordinary concurrent consumption. It
does not make concurrent BURNS of one limiter safe: two burners can each
reserve a chunk that looked acceptable, and the cap is then exceeded by about
a chunk per racing burner (roughly three bursts with 32 of them, measured).
Both the old and the new code behave that way, so this is a documented
contract rather than a fix: callers burning one limiter from several
goroutines serialize, which RateLimiter.Burn does with its own mutex and the
cluster does by burning peer usage from a single flush loop.

Tests cover the two invariants that hold: the cap survives ordinary traffic
racing the burn, and serialized burners share one burst of debt instead of
each taking one.
2026-10-07 00:54:14 +00:00
binwiederhier bbbafcea5d Add the util primitives for clustering
A bounded non-blocking batching queue, a token-burn helper for consumption
that happened elsewhere, and MaxTime. Nothing uses them yet; they land on
their own so they can be reviewed as primitives rather than inside the
cluster diff.

LingerQueue differs from BatchingQueue in that producers never block: a full
queue drops and says so, and Close flushes the remainder before closing the
consumer channel, so per-peer queues can come and go with the nodes.

BurnTokens caps the debt it creates at one burst beyond what is available, so
usage reported from another node can drain a bucket but cannot lock a visitor
out for an unbounded time.
2026-10-07 00:03:00 +00:00
Philipp C. Heckel 5a41860bd6 Merge pull request #1967 from beemines/feat/delayed-notification-timezone
Add timezone support for delayed notifications
2026-10-05 17:46:23 -04:00
Martin Brehmer 2ced8e24c6 Merge branch 'main' into dev 2026-10-05 07:46:07 +02:00
binwiederhier c3c7d6eff1 Bound the shutdown flush, so a wedged database cannot block Stop forever
The final batch write has no deadline (no statement_timeout, and database/sql
is used without a context), so an unresponsive database would hold Stop until
systemd's stop timeout fires and SIGKILLs us. Cache.Close now gives up after
5 seconds, both on handing the pending messages to the batch writer and on
waiting for it, and logs that it did.
2026-10-03 16:00:29 +00:00
binwiederhier bc1e879dc4 Persist batched messages on shutdown; graceful SIGTERM in serve
With cache-batch-size/-timeout set (ntfy.sh: 100 / 1500ms), accepted messages
wait in a BatchingQueue before they are written. Nothing drained that queue on
shutdown, and `ntfy serve` did not handle SIGTERM at all (only SIGHUP), so
every restart dropped up to one batch timeout's worth of accepted messages
from the cache: live subscribers got them, pollers and since= replays never
did.

- util.BatchingQueue.Close emits the pending elements and closes the output
  channel (Enqueue after Close is a no-op)
- message.Cache.Close closes the queue and waits for the batch writer before
  closing the database
- serve stops the server on SIGTERM/SIGINT and waits for Stop to finish (Run
  returns as soon as the listeners close); Stop is idempotent now, since the
  signal handler and serve both call it
2026-10-03 15:06:09 +00:00
Bee mines 7961282490 Add timezone support for delayed notifications 2026-09-21 14:27:20 +08:00
Martin Brehmer dde1188dd4 Support RFC 3339 timestamps for delays
Add support for RFC 3339 timestamps in the Delay property, allowing
messages to be scheduled at a precise point in time.
2026-09-02 10:05:07 +02:00
binwiederhier 7ed7fea081 Limit memory usage in templates 2026-08-03 18:34:29 +02:00
binwiederhier a001ac5195 Template execution rework 2026-07-08 21:08:07 +02:00
binwiederhier c5e5be0746 Merge branch 'main' of github.com:binwiederhier/ntfy into 1511-ampersand 2026-06-24 05:42:12 -04:00
binwiederhier fd716e4807 Phase 1 2026-06-12 11:07:48 -04:00
binwiederhier 1742302f83 More tests and human review 2026-03-21 16:27:41 -04:00
binwiederhier ef314960d0 Refactor 2026-03-17 20:53:41 -04:00
binwiederhier 4699ed3ffd Fix UTF-8 insert failures in Postgres 2026-03-15 21:03:18 -04:00
Philipp C. Heckel 08bf71b248 Merge pull request #1406 from tanhuaan/main
refactor: use slices.Contains to simplify code
2026-02-01 17:31:40 -05:00
tanhuaan 0e79c4bd2a refactor: use slices.Contains to simplify code
Signed-off-by: tanhuaan <tanhuaan@outlook.com>
2025-07-30 16:32:57 +08:00
binwiederhier f59df0f40a Works 2025-07-21 17:44:00 +02:00
binwiederhier d87d8a2db4 fmt 2025-07-21 11:31:38 +02:00
binwiederhier 50c564d8a2 AI docs 2025-07-21 11:24:58 +02:00
binwiederhier 1f34c39eb0 Refactor a little 2025-07-19 22:52:08 +02:00
binwiederhier 892e82ceb8 Remove underscore functions 2025-07-19 22:41:53 +02:00
binwiederhier 8b4834929d Clean code 2025-07-19 22:30:07 +02:00
binwiederhier dde07adbdc Add some limits 2025-07-19 16:46:53 +02:00
binwiederhier 57df16dd62 Remove UUID 2025-07-19 15:44:49 +02:00
binwiederhier 4603802f62 WIP 2025-07-16 21:50:29 +02:00
binwiederhier 2a468493f9 any 2025-07-13 12:45:00 +02:00
Hunter Kehoe 650f492d7d make tests happy 2025-07-07 22:47:41 -06:00
Hunter Kehoe 1f2c76e63d copy subset of Sprig template functions 2025-07-07 22:23:32 -06:00
binwiederhier 849884c947 Change to "proxy-forwarded-header" and add "proxy-trusted-addrs" 2025-05-31 22:39:18 -04:00
binwiederhier b9c176ddba Tests 2024-03-22 22:01:41 -04:00
binwiederhier de65d07518 Simplify(?) templating cases 2024-03-20 21:33:54 -04:00
binwiederhier 94f60fb5b8 Lint 2024-03-07 12:53:39 -05:00
binwiederhier f2cd1edc57 Add some helper for base-url 2024-03-07 12:44:31 -05:00
binwiederhier 243123fd7e Convert duration flags, add docs 2024-03-07 12:22:35 -05:00
binwiederhier 36b33030f3 Add message-{size|delay}-limit 2024-03-07 11:53:12 -05:00
binwiederhier 1c3ed3ea40 Do not allow empty passwords when creating users 2023-11-19 05:47:41 -05:00
binwiederhier 22f48c5ad3 Change mod path 2023-11-16 20:54:58 -05:00
binwiederhier d9387dac99 Refine logic 2023-09-24 17:59:23 -04:00