Files
SocialPub/PrivaPub/Infrastructure/Data/Indexes.cs
T
thepraandClaude Opus 5.5 d1a91c40c4 Deliveries run on a Mongo job queue with leases, backoff and per-host limits
The single serial DeliveryWorker is replaced by Infrastructure/Jobs:
- Job rows are leased with one FindOneAndUpdate (oldest RunAt first, a
  two-minute lease) and a reaper returns expired leases every 30 s;
- enqueueing wakes the workers, which otherwise poll every five seconds;
- delivery runs eight at a time with at most two per host, so a slow or
  dead server holds two slots, not the queue;
- a failure waits n^4 + 15 + jitter seconds (Mastodon's curve) for up to
  16 attempts; a 4xx other than 408/429 is final, a 429 honours
  Retry-After;
- RemoteInstance is a per-host circuit breaker: ten consecutive failures
  quarantine a host for an hour, doubling to a week, and its jobs wait
  without spending attempts;
- a delivery is queued once per activity and inbox (unique DedupeKey), and
  finished jobs expire after seven days (TTL on FinishedAt).

Migration _004 moves pending Delivery rows into jobs and marks them
abandoned, so a rollback to the old worker cannot send them twice.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012CzABvBkbcFqoHdmi8b9WB
2026-10-01 11:09:35 +02:00

83 lines
3.2 KiB
C#

using MongoDB.Bson;
using MongoDB.Driver;
using MongoDB.Entities;
using PrivaPub.Models.Federation;
using PrivaPub.Models.Group;
using PrivaPub.Models.Jobs;
using PrivaPub.Models.Post;
using PrivaPub.Models.User;
namespace PrivaPub.Infrastructure.Data
{
public static class Indexes
{
public static async Task Create(CancellationToken token = default)
{
await Unique<Post>(p => p.ObjectURI, Builders<Post>.Filter.Type(p => p.ObjectURI, BsonType.String), token);
await Plain<Post>(token, p => p.GroupUserId, p => p.ID);
await Plain<Post>(token, p => p.GroupId, p => p.ID);
await Plain<Post>(token, p => p.ActorURI);
await Unique<DmPost>(p => p.ObjectURI, Builders<DmPost>.Filter.Type(p => p.ObjectURI, BsonType.String), token);
await Plain<DmPost>(token, p => p.GroupId, p => p.ID);
await Unique<ForeignAvatar>(a => a.ActorURI, Builders<ForeignAvatar>.Filter.Type(a => a.ActorURI, BsonType.String), token);
await Plain<ForeignAvatar>(token, a => a.PublicKeyId);
await DB.Default.Index<Follower>()
.Key(f => f.LocalActorId, KeyType.Ascending)
.Key(f => f.LocalActorKind, KeyType.Ascending)
.Key(f => f.ActorURI, KeyType.Ascending)
.Option(o => o.Unique = true)
.CreateAsync(token);
await Plain<Follower>(token, f => f.ActorURI);
await DB.Default.Index<RootToAvatar>()
.Key(r => r.RootId, KeyType.Ascending)
.Key(r => r.AvatarId, KeyType.Ascending)
.Option(o => o.Unique = true)
.CreateAsync(token);
await Plain<RootToAvatar>(token, r => r.AvatarId);
await Unique<ReservedName>(r => r.Name, Builders<ReservedName>.Filter.Type(r => r.Name, BsonType.String), token);
await Unique<RootUser>(u => u.UserName, Builders<RootUser>.Filter.Type(u => u.UserName, BsonType.String), token);
await Plain<Avatar>(token, a => a.UserName);
await Plain<Group>(token, g => g.UserName);
await Plain<Group>(token, g => g.InvitationCode);
await Plain<DmGroup>(token, g => g.ParticipantsKey);
await Plain<Delivery>(token, d => d.DeliveredAt, d => d.AbandonedAt, d => d.NextAttemptAt);
await Plain<Job>(token, j => j.Kind, j => j.State, j => j.RunAt);
await Plain<Job>(token, j => j.State, j => j.LeasedUntil);
await Unique<Job>(j => j.DedupeKey, Builders<Job>.Filter.Type(j => j.DedupeKey, BsonType.String), token);
await DB.Default.Index<Job>()
.Key(j => j.FinishedAt, KeyType.Ascending)
.Option(o => o.ExpireAfter = TimeSpan.FromDays(7))
.CreateAsync(token);
await Unique<RemoteInstance>(i => i.Host, Builders<RemoteInstance>.Filter.Type(i => i.Host, BsonType.String), token);
}
static async Task Unique<T>(System.Linq.Expressions.Expression<Func<T, object>> key, FilterDefinition<T> partial,
CancellationToken token) where T : IEntity =>
await DB.Default.Index<T>()
.Key(key, KeyType.Ascending)
.Option(o =>
{
o.Unique = true;
o.PartialFilterExpression = partial;
})
.CreateAsync(token);
static async Task Plain<T>(CancellationToken token, params System.Linq.Expressions.Expression<Func<T, object>>[] keys) where T : IEntity
{
var index = DB.Default.Index<T>();
foreach (var key in keys)
index.Key(key, KeyType.Ascending);
await index.CreateAsync(token);
}
}
}