fix(status): status save no longer fails on Synapse's 1-per-10s presence limit
Reported: 'Failed to save status — server may be rate limiting. Try again.'
on the first status change in a month. Root cause: Synapse rate-limits
PUT /presence/{user}/status to ONE request per 10 s per user by default
(rc_presence.per_user: per_second 0.1, burst_count 1), shared across all of
the user's devices, and our presence heartbeat (online/away on visibility and
activity changes, from every open tab/device) spends that budget — so a manual
save that lands within 10 s of a heartbeat gets a 429, which the form showed
as a dead end.
Two fixes: (1) the status save waits out Retry-After (bounded to ~25 s) via
setPresenceWithRetry instead of failing, and the error text now says what
actually happened (rate limit / server text / offline); (2) the heartbeat
dedupes — it only sends when presence or status actually changes (/sync
already keeps us online), so it stops burning the budget in the first place.
Unit-tested; reproduced headless with a routed 10 s limiter: heartbeat ok →
save 429 → retried 8 s later → saved.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { MatrixError } from 'matrix-js-sdk';
|
||||
import { PresenceDeduper, describePresenceError, setPresenceWithRetry } from './presenceWrite';
|
||||
|
||||
const limited = (retryMs = 3000) =>
|
||||
new MatrixError(
|
||||
{ errcode: 'M_LIMIT_EXCEEDED', error: 'Too Many Requests', retry_after_ms: retryMs },
|
||||
429,
|
||||
);
|
||||
|
||||
test('429 is retried after Retry-After, then succeeds', async () => {
|
||||
let n = 0;
|
||||
const waits: number[] = [];
|
||||
const mx = {
|
||||
setPresence: async () => {
|
||||
n += 1;
|
||||
if (n < 3) throw limited(3000);
|
||||
return {};
|
||||
},
|
||||
};
|
||||
await setPresenceWithRetry(
|
||||
mx,
|
||||
{ presence: 'online', status_msg: 'hi' },
|
||||
{
|
||||
sleepFn: async (ms) => {
|
||||
waits.push(ms);
|
||||
},
|
||||
},
|
||||
);
|
||||
assert.equal(n, 3);
|
||||
assert.deepEqual(waits, [3000, 3000]);
|
||||
});
|
||||
|
||||
test('gives up once the total wait would exceed the cap; other errors are not retried', async () => {
|
||||
const mx = {
|
||||
setPresence: async () => {
|
||||
throw limited(20_000);
|
||||
},
|
||||
};
|
||||
await assert.rejects(
|
||||
() =>
|
||||
setPresenceWithRetry(
|
||||
mx,
|
||||
{ presence: 'online' },
|
||||
{ maxWaitMs: 25_000, sleepFn: async () => undefined },
|
||||
),
|
||||
/Too Many/,
|
||||
);
|
||||
let calls = 0;
|
||||
const forbidden = {
|
||||
setPresence: async () => {
|
||||
calls += 1;
|
||||
throw new MatrixError({ errcode: 'M_FORBIDDEN', error: 'nope' }, 403);
|
||||
},
|
||||
};
|
||||
await assert.rejects(() => setPresenceWithRetry(forbidden, { presence: 'online' }));
|
||||
assert.equal(calls, 1);
|
||||
});
|
||||
|
||||
test('deduper skips identical writes and notices changes', () => {
|
||||
const d = new PresenceDeduper();
|
||||
assert.equal(d.shouldSend({ presence: 'online', status_msg: 'a' }), true);
|
||||
d.sent({ presence: 'online', status_msg: 'a' });
|
||||
assert.equal(d.shouldSend({ presence: 'online', status_msg: 'a' }), false);
|
||||
assert.equal(d.shouldSend({ presence: 'unavailable', status_msg: 'a' }), true);
|
||||
assert.equal(d.shouldSend({ presence: 'online', status_msg: 'b' }), true);
|
||||
d.reset();
|
||||
assert.equal(d.shouldSend({ presence: 'online', status_msg: 'a' }), true);
|
||||
});
|
||||
|
||||
test('error wording', () => {
|
||||
assert.match(describePresenceError(limited()), /once every 10 seconds/);
|
||||
assert.equal(
|
||||
describePresenceError(
|
||||
new MatrixError({ errcode: 'M_FORBIDDEN', error: 'Presence is disabled' }, 403),
|
||||
),
|
||||
'Failed to save status: Presence is disabled',
|
||||
);
|
||||
assert.match(describePresenceError(new TypeError('Failed to fetch')), /couldn't reach/);
|
||||
});
|
||||
Reference in New Issue
Block a user