tools/pasture/flood/flood.cs answers as twenty fake servers (flood1..20.test) and sends signed Creates, Likes and Follows at a set rate; load.sh measures the answers, the queue's wait and processing times, its drain and a persona's home timeline meanwhile (docs/LOAD.md has the method and the runs). What the runs found: - Every unique index was partial on $type: "string", which MongoDB never uses for an equality lookup, so every post by ObjectURI, actor by ActorURI, deleted object, domain block, remote instance and the rest was a collection scan (280 ms a post lookup at 30 000 posts). They are partial on $gt: "" now, which an equality on a string implies; MongoDB.Entities rebuilds them in place at the next start. - Two inbox workers capped intake near 110 activities a second: Federation:InboxConcurrency and DeliveryConcurrency (default 8) set them. - The indexes the plan listed as missing: a post's boosts and replies, a persona's boosts, who follows an actor, timeline rows by author, a post's likes and pins. At 300 activities a second (200 let through, the rest 429 by the per-origin limit) the queue wait went from 29 s to 6 ms at p50; with the limits lifted PrivaPub processes about 900 a second, each in under 10 ms, and the home timeline stays under 20 ms. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LsXgEaXee4GCU1hwYgPJXw
128 lines
4.9 KiB
C#
128 lines
4.9 KiB
C#
using PrivaPub.Federation.Actors;
|
|
using PrivaPub.Federation.Objects;
|
|
using PrivaPub.Infrastructure.Jobs;
|
|
using PrivaPub.Infrastructure.Statistics;
|
|
using PrivaPub.Models.Jobs;
|
|
using PrivaPub.Models.Statistics;
|
|
using PrivaPub.Models.User;
|
|
|
|
using System.Diagnostics;
|
|
using System.Text.Json;
|
|
using System.Text.Json.Nodes;
|
|
|
|
using static PrivaPub.Federation.Objects.ActivityJson;
|
|
|
|
namespace PrivaPub.Federation.Inbox
|
|
{
|
|
public interface IActivityHandler
|
|
{
|
|
string Type { get; }
|
|
Task Handle(JsonNode activity, ForeignAvatar actor, CancellationToken token);
|
|
}
|
|
|
|
public class InboxProcessor : IJobHandler
|
|
{
|
|
readonly IRemoteActorService _remoteActors;
|
|
readonly IReadOnlyDictionary<string, IActivityHandler> _handlers;
|
|
readonly ILogger<InboxProcessor> _logger;
|
|
readonly IInteractionLedger _ledger;
|
|
readonly ILocalActorService _localActors;
|
|
|
|
readonly int _concurrency;
|
|
|
|
public InboxProcessor(IRemoteActorService remoteActors, IEnumerable<IActivityHandler> handlers, ILogger<InboxProcessor> logger,
|
|
IInteractionLedger ledger = default, ILocalActorService localActors = default,
|
|
Microsoft.Extensions.Options.IOptions<Infrastructure.Http.FederationOptions> federation = default)
|
|
{
|
|
_concurrency = Math.Max(1, federation?.Value.InboxConcurrency ?? 8);
|
|
_localActors = localActors;
|
|
_remoteActors = remoteActors;
|
|
_handlers = handlers.ToDictionary(h => h.Type, StringComparer.Ordinal);
|
|
_logger = logger;
|
|
_ledger = ledger;
|
|
}
|
|
|
|
public JobKind Kind => JobKind.ProcessInbox;
|
|
public int Concurrency => _concurrency;
|
|
public int MaxAttempts => 8;
|
|
public int PerHostLimit => 2;
|
|
|
|
public async Task<JobOutcome> Handle(Job job, CancellationToken token)
|
|
{
|
|
var started = Stopwatch.GetTimestamp();
|
|
var payload = JsonSerializer.Deserialize<InboxPayload>(job.Payload);
|
|
var activity = payload == default ? default : JsonNode.Parse(payload.Activity);
|
|
var type = Value(activity, "type");
|
|
if (type == default || !_handlers.TryGetValue(type, out var handler))
|
|
{
|
|
Record(job, payload, activity, type, started, new ArrivalVerdict(), Interactions.Dropped, "unknown-type");
|
|
return JobOutcome.Done;
|
|
}
|
|
|
|
var actor = await _remoteActors.GetActor(payload.ActorURI, refresh: false, token);
|
|
if (actor == default)
|
|
{
|
|
Record(job, payload, activity, type, started, new ArrivalVerdict(), Interactions.Deferred, "actor-unavailable");
|
|
return JobOutcome.Retry("the actor could not be loaded");
|
|
}
|
|
if (payload.ForwardedBy != default)
|
|
{
|
|
var (confirmed, drop) = await Forwarded.Confirm(activity, type, actor.ActorURI, _remoteActors, token);
|
|
if (drop != default)
|
|
{
|
|
Record(job, payload, activity, type, started, new ArrivalVerdict(), Interactions.Dropped, drop);
|
|
return JobOutcome.Done;
|
|
}
|
|
activity = confirmed;
|
|
}
|
|
|
|
var arrival = new Arrival(Id(activity), type, actor.ActorURI, payload.Inbox, payload.KeyId, payload.Algorithm,
|
|
payload.SignedHeaders ?? Array.Empty<string>(), payload.ReceivedAt ?? job.CreatedAt, activity["@context"]?.ToJsonString(),
|
|
payload.ForwardedBy == default ? payload.Activity : default);
|
|
Arrival.Current = arrival;
|
|
try
|
|
{
|
|
await LocalReferences.Canonicalise(activity, _localActors?.BaseAddress, token);
|
|
await handler.Handle(activity, actor, token);
|
|
}
|
|
catch (Exception ex) when (ex is not OperationCanceledException)
|
|
{
|
|
Record(job, payload, activity, type, started, arrival.Verdict, Interactions.Failed, ex.GetType().Name.ToLowerInvariant());
|
|
throw;
|
|
}
|
|
finally
|
|
{
|
|
Arrival.Current = default;
|
|
}
|
|
Record(job, payload, activity, type, started, arrival.Verdict, default, default);
|
|
_logger.LogInformation("Processed {Type} {Id} from {Actor}", type, Id(activity), actor.ActorURI);
|
|
return JobOutcome.Done;
|
|
}
|
|
|
|
void Record(Job job, InboxPayload payload, JsonNode activity, string type, long started, ArrivalVerdict verdict, string outcome, string reason)
|
|
{
|
|
if (_ledger == default || payload == default)
|
|
return;
|
|
var embedded = activity?["object"] as JsonObject;
|
|
_ledger.Record(new InteractionEvent
|
|
{
|
|
Channel = Interactions.In,
|
|
Host = Interactions.HostOf(payload.ActorURI),
|
|
Activity = type,
|
|
Object = verdict.ObjectType ?? Value(embedded, "type"),
|
|
Outcome = outcome ?? verdict.Outcome ?? Interactions.Accepted,
|
|
Reason = reason ?? verdict.Reason,
|
|
LatencyMs = (int)Stopwatch.GetElapsedTime(started).TotalMilliseconds,
|
|
WaitMs = (int)Math.Min(int.MaxValue, Math.Max(0, (DateTime.UtcNow - (payload.ReceivedAt ?? job.CreatedAt)).TotalMilliseconds)),
|
|
Attempt = job.Attempts,
|
|
Audience = verdict.Audience ?? ActivityShape.Of(activity).Audience,
|
|
LocalKind = verdict.LocalKind,
|
|
AgeSeconds = verdict.AgeSeconds,
|
|
Inbox = payload.Inbox,
|
|
Signature = payload.Algorithm == default ? default : "cavage:" + payload.Algorithm,
|
|
Features = type is "Create" or "Update" && embedded != default ? ObjectFeatures.Detect(embedded) : default
|
|
}, payload.ActorURI);
|
|
}
|
|
}
|
|
}
|