Skip to content

Allow post-close cancellations with frozen waitlist progression - #3003

Open
mroderick wants to merge 4 commits into
codebar:masterfrom
mroderick:feat/post-close-rsvp-waitlist
Open

mroderick wants to merge 4 commits into
codebar:masterfrom
mroderick:feat/post-close-rsvp-waitlist

Conversation

@mroderick

@mroderick mroderick commented Oct 9, 2026 •

Copy link
Copy Markdown
Collaborator

Once a custom RSVP close time passes, members on the main list can still cancel, and each cancellation fills the freed seat from the waiting list until a hard freeze 3.5 hours before the workshop starts. New joins stay blocked from the close time onward. Closes #2990.

  • Waiting-list join and leave stop at the earlier of the custom close time and the 3.5-hour freeze. Cancellation and promotion stop at the freeze.
  • A cancel frees a seat only when the invitation held one, and promotion takes the first auto-RSVP waiting-list entry in created order.
  • Admin removals use the same promotion path, and only while the workshop is in the future.
  • Invitation pages hide join, leave, and cancel controls once the matching gate has passed.

Review notes

  • Start with the seat claim and the promotion pop. release_seat claims the seat under a row lock, and WaitingList.promote_next pops the head in a transaction with FOR UPDATE SKIP LOCKED. The two must stay consistent: a repeated cancel must not free the same seat twice, and two promotions must not take the same entry. Test suites cannot prove that interleaving, so this part needs the closest read.
  • Gate semantics: join and leave use the earlier of the custom close time and the freeze, cancel and promotion use the freeze alone. Actions stop at the instant, not after it.
  • Deliberately not done: a reject admitted just before the freeze can still promote and send email after it, because the gate is read once at request start. Re-checking it after the seat claim is a product call. Admin additions ignore the close times. Cancelling from the waiting list keeps the waiting-list entry.
Behaviour by action
Action Stops at
Join the main list or the waiting list The earlier of the custom close time and the 3.5-hour freeze
Leave the waiting list Same as join
Cancel a main-list spot The 3.5-hour freeze
Promote from the waiting list after a cancel The 3.5-hour freeze for member cancels, the workshop start for admin removals

The cross-role promotion spec calls promote_next. This change replaces WaitingList.next_spot with promote_next, which confirms the invitation and removes the waiting-list entry in one step.

@mroderick
mroderick force-pushed the feat/post-close-rsvp-waitlist branch from 43cdb71 to 83bc64f Compare October 9, 2026 07:55
Adds the two time gates from issue codebar#2990. waitlist_closes_at is the earlier of the custom RSVP close time and the freeze, so new joins stop at the configured close. cancellations_open? tracks the freeze alone, so a member may cancel after the close and until the freeze. The freeze is the hard cutoff 3.5 hours before the start, requirement 4 of the issue, and rsvp_freezes_at owns that offset alone. The RSVP flows that apply these gates follow in the next commit.
A cancel frees a seat only when the invitation held one, so rejecting an invitation that never answered must not consume a waiting-list entry. release_seat claims the seat with a row-locked read-modify-write, so duplicate concurrent cancels free it once. promote_next pops the head in a transaction with FOR UPDATE SKIP LOCKED so concurrent promoters take different entries, and confirms with update! so a failed confirmation rolls the pop back instead of emailing a spot that was never granted.

The FIFO order and the seat-freed rule are pinned by specs. The locking behaviour cannot be exercised by the single-threaded suite, so this message is where it is recorded. WaitingList.next_spot still serves the current RSVP flows until the next commit replaces it.
…eeze

Once the custom RSVP close time passes, new joins to the main list and the waiting list stay blocked, while main-list members may cancel until the freeze. Each cancel that frees a seat promotes the first auto-RSVP waiting-list entry in created order. Admin removals use the same promotion path while the workshop is in the future.

This commit replaces WaitingList.next_spot in the RSVP flows and removes it, now that promote_next covers it. The end-to-end pins cover the reject path, the admin removal path, and the waitlist join and leave gates.
@mroderick
mroderick force-pushed the feat/post-close-rsvp-waitlist branch from 83bc64f to 0d1a071 Compare October 9, 2026 09:05
@mroderick
mroderick marked this pull request as ready for review October 9, 2026 09:16
@mroderick
mroderick requested a review from olleolleolle October 9, 2026 09:16
Comment thread app/models/waiting_list.rb Outdated
return unless next_spot

invitation = next_spot.invitation
next_spot.destroy

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as we're using update! on line :37 I think it makes sense to also hard error if a record doesn't get destroyed by using destroy! here

@mroderick mroderick Oct 9, 2026 •

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed to destroy! in f3f1988. It fixes more than style: promote_next already raises through update! when confirming the invitation fails, so a silent destroy failure would leave the entry on the waiting list while the invitation becomes attending. The entry would stay at the head of the list and a later promotion could confirm the same invitation again. destroy! raises and rolls the transaction back instead.

Thank you 🙏

promote_next already raises through update! when confirming the
invitation fails, so a silent destroy failure left the entry on the
waitlist while the invitation became attending, allowing a repeat
promotion and duplicate attending email. destroy! rolls the whole
transaction back instead.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Update custom RSVP closure logic: Enable cancellations and waitlist progression until 3.5h before event

2 participants