[backend/api] Add idempotency key support to NoteController.CreateNote (ISH-294)

This commit is contained in:
Laura Hausmann 2024-05-07 01:41:05 +02:00
parent 2519f382c5
commit d109f00d55
No known key found for this signature in database
GPG key ID: D044E84C5BE01605
3 changed files with 45 additions and 8 deletions

View file

@ -363,7 +363,7 @@ public class StatusController(
{
if (!hit.StartsWith('_')) break;
await Task.Delay(100);
hit = await cache.GetAsync<string>($"idempotency:{user.Id}:{idempotencyKey}") ??
hit = await cache.GetAsync<string>(key) ??
throw new Exception("Idempotency key status disappeared in for loop");
if (i >= 10)
throw GracefulException.RequestTimeout("Failed to resolve idempotency key note within 1000 ms");

View file

@ -1,6 +1,7 @@
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Net.Mime;
using AsyncKeyedLock;
using Iceshrimp.Backend.Controllers.Attributes;
using Iceshrimp.Backend.Controllers.Renderers;
using Iceshrimp.Shared.Schemas;
@ -24,9 +25,16 @@ public class NoteController(
DatabaseContext db,
NoteService noteSvc,
NoteRenderer noteRenderer,
UserRenderer userRenderer
UserRenderer userRenderer,
CacheService cache
) : ControllerBase
{
private static readonly AsyncKeyedLocker<string> KeyedLocker = new(o =>
{
o.PoolSize = 100;
o.PoolInitialFill = 5;
});
[HttpGet("{id}")]
[Authenticate]
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(NoteResponse))]
@ -209,6 +217,31 @@ public class NoteController(
{
var user = HttpContext.GetUserOrFail();
if (request.IdempotencyKey != null)
{
var key = $"idempotency:{user.Id}:{request.IdempotencyKey}";
string hit;
using (await KeyedLocker.LockAsync(key))
{
hit = await cache.FetchAsync(key, TimeSpan.FromHours(24), () => $"_:{HttpContext.TraceIdentifier}");
}
if (hit != $"_:{HttpContext.TraceIdentifier}")
{
for (var i = 0; i <= 10; i++)
{
if (!hit.StartsWith('_')) break;
await Task.Delay(100);
hit = await cache.GetAsync<string>(key) ??
throw new Exception("Idempotency key status disappeared in for loop");
if (i >= 10)
throw GracefulException.RequestTimeout("Failed to resolve idempotency key note within 1000 ms");
}
return await GetNote(hit);
}
}
var reply = request.ReplyId != null
? await db.Notes.Where(p => p.Id == request.ReplyId)
.IncludeCommonProperties()
@ -232,6 +265,9 @@ public class NoteController(
var note = await noteSvc.CreateNoteAsync(user, (Note.NoteVisibility)request.Visibility, request.Text,
request.Cw, reply, renote, attachments);
if (request.IdempotencyKey != null)
await cache.SetAsync($"idempotency:{user.Id}:{request.IdempotencyKey}", note.Id, TimeSpan.FromHours(24));
return Ok(await noteRenderer.RenderOne(note, user));
}
}

View file

@ -2,10 +2,11 @@ namespace Iceshrimp.Shared.Schemas;
public class NoteCreateRequest
{
public required string Text { get; set; }
public string? Cw { get; set; }
public string? ReplyId { get; set; }
public string? RenoteId { get; set; }
public List<string>? MediaIds { get; set; }
public required NoteVisibility Visibility { get; set; }
public required string Text { get; set; }
public string? Cw { get; set; }
public string? ReplyId { get; set; }
public string? RenoteId { get; set; }
public List<string>? MediaIds { get; set; }
public required NoteVisibility Visibility { get; set; }
public string? IdempotencyKey { get; set; }
}