Send the moderator DM after responding, not before
docker-build-push / build-push (push) Successful in 5s
docker-build-push / build-push (push) Successful in 5s
The first real pre-block in production exposed this. Timeline from the logs: 00:24:54.53 Mastodon calls (signup form render) 00:24:55.33 ipapi.is answers (cold lookup, new range) 00:24:55.89 ip_blocks POST 200 <- block installed, 1.35s in 00:24:56.47 statuses POST 200 <- moderator DM, 582ms 00:24:56.53 Mastodon gives up at its 2.0s budget The block landed comfortably inside the budget and did its job -- the range is blocked and no account was created from it. But the answer never got back, because a moderator DM is a whole extra Mastodon round trip sitting on the synchronous response path. Nothing the caller acts on depends on that DM. It now goes out as a FastAPI BackgroundTask, which runs after the response is sent, so a cold lookup answers in ~1.35s instead of ~1.95s. Only classification and registering the block have to happen before we answer; telling a human can wait. reputation_check() takes a notify callable, defaulting to sending immediately so any non-HTTP caller keeps the old behaviour; the endpoint passes one that collects and defers. Tests assert the block is registered before the DM goes out, and that a DM that raises cannot stop a block being installed.
This commit is contained in:
1 parent
37feb109b3
commit
9e4456b7df
2 files changed
+66
-8
No files matched your search
@@ -1036,6 +1036,8 @@ def reputation_api_tests():
|
||||
main.register_ip_block = (
|
||||
lambda ip, acct, org, cidr="", comment="":
|
||||
ipblocks.append((ip, cidr, comment)) or True)
|
||||
# TestClient runs BackgroundTasks to completion before returning, so the
|
||||
# deferred moderator DMs are still observable in `dms` after a request.
|
||||
main.register_email_domain_block = (
|
||||
lambda domain, acct: domain_blocks.append((domain, acct)) or True)
|
||||
|
||||
@@ -1251,6 +1253,38 @@ def reputation_api_tests():
|
||||
active = {e["cidr"] for e in client.get("/api/blocks/ip-ranges").json()["items"]}
|
||||
assert "203.0.113.0/24" not in active, "an expired block must not be published"
|
||||
|
||||
# N. The moderator DM is deferred until after the response. It costs a
|
||||
# whole Mastodon round trip (582ms measured), and the caller is a live
|
||||
# signup on a ~2s budget -- keeping it inline blew that budget the
|
||||
# first time a real bad range showed up in production.
|
||||
sent_order = []
|
||||
main.register_ip_block = (
|
||||
lambda ip, acct, org, cidr="", comment="":
|
||||
sent_order.append("block") or ipblocks.append((ip, cidr, comment)) or True)
|
||||
main.dm_moderator = lambda message: sent_order.append("dm") or dms.append(message)
|
||||
classifications["198.51.103.7"] = ("datacenter", "Late DM Inc", True, "198.51.103.0/24")
|
||||
|
||||
dms.clear()
|
||||
r = ask({"ip": "198.51.103.7", "source": "defer-check"})
|
||||
assert r.json()["blocked"] is True, r.json()
|
||||
assert sent_order == ["block", "dm"], \
|
||||
f"the block must be registered before the DM is sent, got {sent_order}"
|
||||
assert any("198.51.103.0/24" in d for d in dms), "the DM still goes out, just later"
|
||||
|
||||
# ...and the response body never depends on the DM having been sent.
|
||||
main.dm_moderator = lambda message: (_ for _ in ()).throw(
|
||||
RuntimeError("moderator DM must not be able to break a lookup"))
|
||||
classifications["198.51.104.7"] = ("datacenter", "Broken DM Inc", True, "198.51.104.0/24")
|
||||
try:
|
||||
r = ask({"ip": "198.51.104.7", "source": "defer-check"})
|
||||
except RuntimeError:
|
||||
# TestClient surfaces background-task errors; the important part is
|
||||
# that the block was still installed and the answer computed first.
|
||||
pass
|
||||
assert main.reputation_block_installed("ip", "198.51.104.0/24"), \
|
||||
"a failing DM must not prevent the block being registered"
|
||||
main.dm_moderator = lambda message: dms.append(message)
|
||||
|
||||
# Email domain blocks don't expire in Mastodon, so they carry none here
|
||||
# either: recorded with no expiry, they stay installed indefinitely.
|
||||
main.record_reputation_block("email", "rep-forever.test", "", "")
|
||||
|
||||
Reference in new issue
Block a user