Split out of #9812, which restores the project invitation endpoint from a guaranteed HTTP 500. This is the reliability gap underneath it, left out of that PR deliberately to keep the fix reviewable.
ProjectInvitationsViewset.create persists the invitations and then publishes a task per row:
created_invitations = ProjectMemberInvite.objects.bulk_create(
project_invitations, batch_size=10, ignore_conflicts=True
)
for invitation in created_invitations:
project_invitation.delay(...)
The rows are committed before the first delay(). If the broker is unreachable partway through, earlier invitations exist and have been emailed, later ones exist and have not, and the endpoint returns 500. A client that retries then re-sends invitations that already went out, since bulk_create(ignore_conflicts=True) will not recreate the row but the loop will publish for it again.
Worth covering:
- persist an outbox record in the same transaction as the invitation, so the intent to send is committed atomically with the row
- publish from the outbox with retries, rather than inline in the request
- key idempotency on
ProjectMemberInvite id so a retry cannot email the same invitee twice
WorkspaceInvitationsViewset has the same shape, so whatever lands here probably wants to apply there too.
Happy to pick this up if it is wanted, though the outbox table and where the publisher runs are design calls worth agreeing first.
Split out of #9812, which restores the project invitation endpoint from a guaranteed HTTP 500. This is the reliability gap underneath it, left out of that PR deliberately to keep the fix reviewable.
ProjectInvitationsViewset.createpersists the invitations and then publishes a task per row:The rows are committed before the first
delay(). If the broker is unreachable partway through, earlier invitations exist and have been emailed, later ones exist and have not, and the endpoint returns 500. A client that retries then re-sends invitations that already went out, sincebulk_create(ignore_conflicts=True)will not recreate the row but the loop will publish for it again.Worth covering:
ProjectMemberInviteid so a retry cannot email the same invitee twiceWorkspaceInvitationsViewsethas the same shape, so whatever lands here probably wants to apply there too.Happy to pick this up if it is wanted, though the outbox table and where the publisher runs are design calls worth agreeing first.