Fix UserAccountDelete delivery

pull/6534/head
Daniel Supernault 4 months ago
parent 68a6a5fa51
commit 45b5381970
No known key found for this signature in database
GPG Key ID: 23740873EE6F76A1

@ -19,7 +19,7 @@ use function Laravel\Prompts\table;
class UserAccountDelete extends Command class UserAccountDelete extends Command
{ {
protected $signature = 'app:user-account-delete protected $signature = 'app:user-account-delete
{--concurrency=150 : Number of concurrent deliveries} {--concurrency=500 : Number of concurrent deliveries}
{--chunk=500 : Number of inbox rows to process per DB chunk} {--chunk=500 : Number of inbox rows to process per DB chunk}
{--attempts=2 : Max attempts for retryable failures} {--attempts=2 : Max attempts for retryable failures}
{--dry-run : Build payload and audience, but do not send}'; {--dry-run : Build payload and audience, but do not send}';
@ -82,6 +82,22 @@ class UserAccountDelete extends Command
->distinct() ->distinct()
->count('shared_inbox'); ->count('shared_inbox');
try {
$testHeaders = HttpSignature::instanceActorSign(
config('app.url').'/inbox',
$payload,
);
if (empty($testHeaders) || ! isset($testHeaders['Signature'])) {
$this->error('Instance actor signing failed — run php artisan instance:actor');
return self::FAILURE;
}
} catch (\Exception $e) {
$this->error("Instance actor error: {$e->getMessage()}");
return self::FAILURE;
}
if ($this->option('dry-run')) { if ($this->option('dry-run')) {
$this->line('Dry run only.'); $this->line('Dry run only.');
$this->line("Audience size: {$totalTargets}"); $this->line("Audience size: {$totalTargets}");
@ -115,7 +131,6 @@ class UserAccountDelete extends Command
->orderBy('shared_inbox') ->orderBy('shared_inbox')
->chunk($chunkSize, function ($instances) use ( ->chunk($chunkSize, function ($instances) use (
$client, $client,
$profile,
$payload, $payload,
$concurrency, $concurrency,
$attempts, $attempts,
@ -141,7 +156,6 @@ class UserAccountDelete extends Command
$batch = $this->sendBatch( $batch = $this->sendBatch(
client: $client, client: $client,
urls: $pending, urls: $pending,
profile: $profile,
payload: $payload, payload: $payload,
concurrency: $concurrency concurrency: $concurrency
); );
@ -158,7 +172,7 @@ class UserAccountDelete extends Command
} }
if ($attempt < $attempts && $pending->isNotEmpty()) { if ($attempt < $attempts && $pending->isNotEmpty()) {
usleep($attempt * 250_000); usleep(100_000);
} }
} }
@ -241,17 +255,22 @@ class UserAccountDelete extends Command
protected function makeHttpClient(): Client protected function makeHttpClient(): Client
{ {
return new Client([ return new Client([
'timeout' => 8.0, 'timeout' => 5.0,
'connect_timeout' => 3.0, 'connect_timeout' => 2.0,
'http_errors' => false, 'http_errors' => false,
'allow_redirects' => false, 'allow_redirects' => false,
'curl' => [
CURLOPT_TCP_FASTOPEN => true,
CURLOPT_TCP_NODELAY => true,
CURLOPT_FORBID_REUSE => false,
CURLOPT_FRESH_CONNECT => false,
],
]); ]);
} }
protected function sendBatch( protected function sendBatch(
Client $client, Client $client,
Collection $urls, Collection $urls,
Profile $profile,
string $payload, string $payload,
int $concurrency int $concurrency
): array { ): array {
@ -263,15 +282,13 @@ class UserAccountDelete extends Command
$httpFailed = []; $httpFailed = [];
$retryable = collect(); $retryable = collect();
$requests = function () use ($client, $urls, $profile, $payload, $userAgent) { $requests = function () use ($client, $urls, $payload, $userAgent) {
foreach ($urls as $url) { foreach ($urls as $url) {
$headers = $this->normalizeHeaders( $headers = HttpSignature::instanceActorSign($url, $payload, [
HttpSignature::sign($profile, $url, $payload, [ 'Content-Type' => 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"',
'Content-Type' => 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', 'Accept' => 'application/ld+json; profile="https://www.w3.org/ns/activitystreams", application/activity+json, application/json',
'Accept' => 'application/ld+json; profile="https://www.w3.org/ns/activitystreams", application/activity+json, application/json', 'User-Agent' => $userAgent,
'User-Agent' => $userAgent, ]);
])
);
yield function () use ($client, $url, $headers, $payload) { yield function () use ($client, $url, $headers, $payload) {
return $client->postAsync($url, [ return $client->postAsync($url, [
@ -300,10 +317,7 @@ class UserAccountDelete extends Command
return; return;
} }
$httpFailed[$url] = [ $httpFailed[$url] = ['status' => $status, 'body' => null];
'status' => $status,
'body' => $this->truncateBody((string) $response->getBody()),
];
}, },
'rejected' => function ($reason, $index) use ($urls, &$retryable) { 'rejected' => function ($reason, $index) use ($urls, &$retryable) {
$url = $urls[$index]; $url = $urls[$index];
@ -329,37 +343,4 @@ class UserAccountDelete extends Command
{ {
return in_array($status, [408, 425, 429, 500, 502, 503, 504], true); return in_array($status, [408, 425, 429, 500, 502, 503, 504], true);
} }
protected function truncateBody(string $body, int $limit = 1000): ?string
{
$body = trim($body);
if ($body === '') {
return null;
}
return mb_strlen($body) > $limit
? mb_substr($body, 0, $limit).'…'
: $body;
}
protected function normalizeHeaders(array $headers): array
{
$normalized = [];
foreach ($headers as $key => $value) {
if (is_string($key)) {
$normalized[$key] = $value;
continue;
}
if (is_string($value) && str_contains($value, ':')) {
[$name, $headerValue] = explode(':', $value, 2);
$normalized[trim($name)] = trim($headerValue);
}
}
return $normalized;
}
} }

Loading…
Cancel
Save