Media are reserved for one post, atomically
An upload's owner checked that it was free, then attached it in a second step, so two posts asking for the same upload at once could both get it; a post now, or an edit, could take media a scheduled post held, which then failed when its time came (only logged) and was deleted; and a persona deleted or banned after scheduling a post still published it. Now a post claims its media in one conditional update before anything is written (its id is minted first), and a post that loses the race is refused with 422, the media it took put back. Scheduling reserves media the same way. Media held by a scheduled post are that post's alone, and its job publishes only for a persona still there whose root is neither deleted nor banned; otherwise what it held is trashed. MastodonScheduledStatusesTests: a post now and an edit can't take scheduled media, two racing posts never both get an upload, a gone persona's scheduled post never publishes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LsXgEaXee4GCU1hwYgPJXw
This commit is contained in:
1 parent
bb680e8cb8
commit
65938bb2a0
4 files changed
+117
-14
No files matched your search
@@ -116,9 +116,21 @@ namespace PrivaPub.Api.Mastodon.Controllers
|
||||
return Error(StatusCodes.Status422UnprocessableEntity, "Validation failed: Media attachments are not yours, already posted or too many");
|
||||
|
||||
var scheduled = new ScheduledStatus { AvatarId = MyId, ScheduledAt = at, Params = asked };
|
||||
await DB.Default.SaveAsync(scheduled, token);
|
||||
scheduled.ID = (string)scheduled.GenerateNewID();
|
||||
// reserved in one conditional update: a post or another scheduled post taking one meanwhile wins
|
||||
if (mediaIds.Count > 0)
|
||||
await DB.Default.Update<Models.Media.MediaAttachment>().Match(m => mediaIds.Contains(m.ID)).Modify(m => m.ScheduledStatusId, scheduled.ID).ExecuteAsync(token);
|
||||
{
|
||||
var reserved = await DB.Default.Update<Models.Media.MediaAttachment>()
|
||||
.Match(m => mediaIds.Contains(m.ID) && m.PostId == null && m.ScheduledStatusId == null && m.TrashedAt == null)
|
||||
.Modify(m => m.ScheduledStatusId, scheduled.ID).ExecuteAsync(token);
|
||||
if (reserved.MatchedCount != mediaIds.Count)
|
||||
{
|
||||
await DB.Default.Update<Models.Media.MediaAttachment>().Match(m => m.ScheduledStatusId == scheduled.ID)
|
||||
.Modify(m => m.ScheduledStatusId, null).ExecuteAsync(token);
|
||||
return Error(StatusCodes.Status422UnprocessableEntity, "Validation failed: Media attachments are not yours, already posted or too many");
|
||||
}
|
||||
}
|
||||
await DB.Default.SaveAsync(scheduled, token);
|
||||
await _jobs.EnqueueMany(new[] { ScheduledStatuses.JobFor(scheduled, new Uri(Me.BaseAddress).Host) }, token);
|
||||
if (cacheKey != default)
|
||||
_cache.Set(cacheKey + ":scheduled", scheduled.ID, TimeSpan.FromHours(1));
|
||||
|
||||
Reference in new issue
Block a user