From 1b4898323214d413510678d9032afee0bb260325 Mon Sep 17 00:00:00 2001 From: jesper Date: Fri, 13 Feb 2026 13:30:12 +0100 Subject: [PATCH 1/5] Add RaffleDecorator --- apps/uno/bootstrap/api.go | 4 + apps/uno/infrastructure/decorator/raffle.go | 77 +++++++++++++++++++ .../infrastructure/postgres/registration.go | 2 +- 3 files changed, 82 insertions(+), 1 deletion(-) create mode 100644 apps/uno/infrastructure/decorator/raffle.go diff --git a/apps/uno/bootstrap/api.go b/apps/uno/bootstrap/api.go index a29fa5184d..b71e968ae5 100644 --- a/apps/uno/bootstrap/api.go +++ b/apps/uno/bootstrap/api.go @@ -9,6 +9,7 @@ import ( "uno/domain/service" "uno/http" "uno/infrastructure/external" + "uno/infrastructure/decorator" "uno/infrastructure/logging" "uno/infrastructure/postgres" "uno/infrastructure/telemetry" @@ -70,6 +71,9 @@ func RunApi() { weatherRepo := external.NewYrRepo(logger) databrusRepo := external.NewDatabrusRepo(logger) + // Initialize decorators + registrationRepo = decorator.NewRaffleDecorator(registrationRepo, notif) + // Initialize services authService := service.NewAuthService(sessionRepo, userRepo) happeningService := service.NewHappeningService(happeningRepo, userRepo, registrationRepo, banInfoRepo) diff --git a/apps/uno/infrastructure/decorator/raffle.go b/apps/uno/infrastructure/decorator/raffle.go new file mode 100644 index 0000000000..9fca1ca513 --- /dev/null +++ b/apps/uno/infrastructure/decorator/raffle.go @@ -0,0 +1,77 @@ +package decorator + +import ( + "context" + "sync" + "time" + "uno/domain/model" + "uno/domain/port" + + "github.com/jesperkha/notifier" +) + +const ( + // How long the raffle period lasts at the start of a bedpres registration period + RAFFLE_DURATION = time.Minute * 1 + // Maximum length of registration queue before blocking + MAX_QUEUE = 200 +) + +// RaffleDecorator wraps port.RegistrationRepo and adds raffle +// registration for bedpres happenings in a given time window. +// Implements port.RegistrationRepo +type RaffleDecorator struct { + repo port.RegistrationRepo + queue chan registration + mu *sync.Mutex +} + +type registration struct { + ctx context.Context + userID string + happeningID string + spotRanges []model.SpotRange + hostGroups []string + canSkipSpotRange bool +} + +func NewRaffleDecorator(repo port.RegistrationRepo, notif *notifier.Notifier) port.RegistrationRepo { + raffle := &RaffleDecorator{ + repo: repo, + queue: make(chan registration, MAX_QUEUE), + mu: &sync.Mutex{}, + } + + go raffle.start(notif) + return raffle +} + +// start the raffle backround job handling de-queueing registrations. +func (r *RaffleDecorator) start(notif *notifier.Notifier) { + done, finish := notif.Register() + + <-done + finish() +} + +// CreateRegistration implements port.RegistrationRepo. +func (r *RaffleDecorator) CreateRegistration( + ctx context.Context, + userID string, + happeningID string, + spotRanges []model.SpotRange, + hostGroups []string, + canSkipSpotRange bool, +) (*model.Registration, bool, error) { + return r.repo.CreateRegistration(ctx, userID, happeningID, spotRanges, hostGroups, canSkipSpotRange) +} + +// GetByUserAndHappening implements port.RegistrationRepo. +func (r *RaffleDecorator) GetByUserAndHappening(ctx context.Context, userID string, happeningID string) (*model.Registration, error) { + return r.repo.GetByUserAndHappening(ctx, userID, happeningID) +} + +// InsertAnswers implements port.RegistrationRepo. +func (r *RaffleDecorator) InsertAnswers(ctx context.Context, userID string, happeningID string, questions []model.QuestionAnswer) error { + return r.repo.InsertAnswers(ctx, userID, happeningID, questions) +} diff --git a/apps/uno/infrastructure/postgres/registration.go b/apps/uno/infrastructure/postgres/registration.go index 418387a9b4..0aca89b7ed 100644 --- a/apps/uno/infrastructure/postgres/registration.go +++ b/apps/uno/infrastructure/postgres/registration.go @@ -55,7 +55,6 @@ func (r *RegistrationRepo) GetByUserAndHappening(ctx context.Context, userID, ha // An error here could be because of an issue with locking the table or a DB error. func (r *RegistrationRepo) CreateRegistration( ctx context.Context, - userID, happeningID string, spotRanges []model.SpotRange, hostGroups []string, @@ -182,6 +181,7 @@ func (r *RegistrationRepo) CreateRegistration( return nil, false, sql.ErrNoRows } + // TODO: rar import-cycle hvor service bruker dette repo, men dette repo bruker service isRegistered := service.IsAvailableSpot( spotRanges, existingRegs, From 119d936af1daf92bed7d4d24194ecfa1aa2199b1 Mon Sep 17 00:00:00 2001 From: jesper Date: Fri, 13 Feb 2026 13:39:37 +0100 Subject: [PATCH 2/5] Pass model.Happening to CreateRegistration --- .../uno/domain/port/mocks/RegistrationRepo.go | 34 +++++++++---------- apps/uno/domain/port/registration.go | 3 +- apps/uno/domain/service/happening.go | 2 +- apps/uno/infrastructure/decorator/raffle.go | 4 +-- .../infrastructure/postgres/registration.go | 13 +++---- .../postgres/registration_test.go | 6 ++-- 6 files changed, 32 insertions(+), 30 deletions(-) diff --git a/apps/uno/domain/port/mocks/RegistrationRepo.go b/apps/uno/domain/port/mocks/RegistrationRepo.go index 3b14d363e3..87eb419f9f 100644 --- a/apps/uno/domain/port/mocks/RegistrationRepo.go +++ b/apps/uno/domain/port/mocks/RegistrationRepo.go @@ -39,8 +39,8 @@ func (_m *RegistrationRepo) EXPECT() *RegistrationRepo_Expecter { } // CreateRegistration provides a mock function for the type RegistrationRepo -func (_mock *RegistrationRepo) CreateRegistration(ctx context.Context, userID string, happeningID string, spotRanges []model.SpotRange, hostGroups []string, canSkipSpotRange bool) (*model.Registration, bool, error) { - ret := _mock.Called(ctx, userID, happeningID, spotRanges, hostGroups, canSkipSpotRange) +func (_mock *RegistrationRepo) CreateRegistration(ctx context.Context, userID string, happening model.Happening, spotRanges []model.SpotRange, hostGroups []string, canSkipSpotRange bool) (*model.Registration, bool, error) { + ret := _mock.Called(ctx, userID, happening, spotRanges, hostGroups, canSkipSpotRange) if len(ret) == 0 { panic("no return value specified for CreateRegistration") @@ -49,23 +49,23 @@ func (_mock *RegistrationRepo) CreateRegistration(ctx context.Context, userID st var r0 *model.Registration var r1 bool var r2 error - if returnFunc, ok := ret.Get(0).(func(context.Context, string, string, []model.SpotRange, []string, bool) (*model.Registration, bool, error)); ok { - return returnFunc(ctx, userID, happeningID, spotRanges, hostGroups, canSkipSpotRange) + if returnFunc, ok := ret.Get(0).(func(context.Context, string, model.Happening, []model.SpotRange, []string, bool) (*model.Registration, bool, error)); ok { + return returnFunc(ctx, userID, happening, spotRanges, hostGroups, canSkipSpotRange) } - if returnFunc, ok := ret.Get(0).(func(context.Context, string, string, []model.SpotRange, []string, bool) *model.Registration); ok { - r0 = returnFunc(ctx, userID, happeningID, spotRanges, hostGroups, canSkipSpotRange) + if returnFunc, ok := ret.Get(0).(func(context.Context, string, model.Happening, []model.SpotRange, []string, bool) *model.Registration); ok { + r0 = returnFunc(ctx, userID, happening, spotRanges, hostGroups, canSkipSpotRange) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*model.Registration) } } - if returnFunc, ok := ret.Get(1).(func(context.Context, string, string, []model.SpotRange, []string, bool) bool); ok { - r1 = returnFunc(ctx, userID, happeningID, spotRanges, hostGroups, canSkipSpotRange) + if returnFunc, ok := ret.Get(1).(func(context.Context, string, model.Happening, []model.SpotRange, []string, bool) bool); ok { + r1 = returnFunc(ctx, userID, happening, spotRanges, hostGroups, canSkipSpotRange) } else { r1 = ret.Get(1).(bool) } - if returnFunc, ok := ret.Get(2).(func(context.Context, string, string, []model.SpotRange, []string, bool) error); ok { - r2 = returnFunc(ctx, userID, happeningID, spotRanges, hostGroups, canSkipSpotRange) + if returnFunc, ok := ret.Get(2).(func(context.Context, string, model.Happening, []model.SpotRange, []string, bool) error); ok { + r2 = returnFunc(ctx, userID, happening, spotRanges, hostGroups, canSkipSpotRange) } else { r2 = ret.Error(2) } @@ -80,15 +80,15 @@ type RegistrationRepo_CreateRegistration_Call struct { // CreateRegistration is a helper method to define mock.On call // - ctx context.Context // - userID string -// - happeningID string +// - happening model.Happening // - spotRanges []model.SpotRange // - hostGroups []string // - canSkipSpotRange bool -func (_e *RegistrationRepo_Expecter) CreateRegistration(ctx interface{}, userID interface{}, happeningID interface{}, spotRanges interface{}, hostGroups interface{}, canSkipSpotRange interface{}) *RegistrationRepo_CreateRegistration_Call { - return &RegistrationRepo_CreateRegistration_Call{Call: _e.mock.On("CreateRegistration", ctx, userID, happeningID, spotRanges, hostGroups, canSkipSpotRange)} +func (_e *RegistrationRepo_Expecter) CreateRegistration(ctx interface{}, userID interface{}, happening interface{}, spotRanges interface{}, hostGroups interface{}, canSkipSpotRange interface{}) *RegistrationRepo_CreateRegistration_Call { + return &RegistrationRepo_CreateRegistration_Call{Call: _e.mock.On("CreateRegistration", ctx, userID, happening, spotRanges, hostGroups, canSkipSpotRange)} } -func (_c *RegistrationRepo_CreateRegistration_Call) Run(run func(ctx context.Context, userID string, happeningID string, spotRanges []model.SpotRange, hostGroups []string, canSkipSpotRange bool)) *RegistrationRepo_CreateRegistration_Call { +func (_c *RegistrationRepo_CreateRegistration_Call) Run(run func(ctx context.Context, userID string, happening model.Happening, spotRanges []model.SpotRange, hostGroups []string, canSkipSpotRange bool)) *RegistrationRepo_CreateRegistration_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 context.Context if args[0] != nil { @@ -98,9 +98,9 @@ func (_c *RegistrationRepo_CreateRegistration_Call) Run(run func(ctx context.Con if args[1] != nil { arg1 = args[1].(string) } - var arg2 string + var arg2 model.Happening if args[2] != nil { - arg2 = args[2].(string) + arg2 = args[2].(model.Happening) } var arg3 []model.SpotRange if args[3] != nil { @@ -131,7 +131,7 @@ func (_c *RegistrationRepo_CreateRegistration_Call) Return(registration *model.R return _c } -func (_c *RegistrationRepo_CreateRegistration_Call) RunAndReturn(run func(ctx context.Context, userID string, happeningID string, spotRanges []model.SpotRange, hostGroups []string, canSkipSpotRange bool) (*model.Registration, bool, error)) *RegistrationRepo_CreateRegistration_Call { +func (_c *RegistrationRepo_CreateRegistration_Call) RunAndReturn(run func(ctx context.Context, userID string, happening model.Happening, spotRanges []model.SpotRange, hostGroups []string, canSkipSpotRange bool) (*model.Registration, bool, error)) *RegistrationRepo_CreateRegistration_Call { _c.Call.Return(run) return _c } diff --git a/apps/uno/domain/port/registration.go b/apps/uno/domain/port/registration.go index 5fc2f9ee22..618c6884a5 100644 --- a/apps/uno/domain/port/registration.go +++ b/apps/uno/domain/port/registration.go @@ -9,7 +9,8 @@ type RegistrationRepo interface { GetByUserAndHappening(ctx context.Context, userID, happeningID string) (*model.Registration, error) CreateRegistration( ctx context.Context, - userID, happeningID string, + userID string, + happening model.Happening, spotRanges []model.SpotRange, hostGroups []string, canSkipSpotRange bool, diff --git a/apps/uno/domain/service/happening.go b/apps/uno/domain/service/happening.go index 43337538ea..5015fcaca7 100644 --- a/apps/uno/domain/service/happening.go +++ b/apps/uno/domain/service/happening.go @@ -371,7 +371,7 @@ func (hs *HappeningService) Register( _, isWaitlisted, err := hs.registrationRepo.CreateRegistration( ctx, userID, - happeningID, + happening, spotRanges, hostGroups, canSkipSpotRange, diff --git a/apps/uno/infrastructure/decorator/raffle.go b/apps/uno/infrastructure/decorator/raffle.go index 9fca1ca513..f1a55b87dd 100644 --- a/apps/uno/infrastructure/decorator/raffle.go +++ b/apps/uno/infrastructure/decorator/raffle.go @@ -58,12 +58,12 @@ func (r *RaffleDecorator) start(notif *notifier.Notifier) { func (r *RaffleDecorator) CreateRegistration( ctx context.Context, userID string, - happeningID string, + happening model.Happening, spotRanges []model.SpotRange, hostGroups []string, canSkipSpotRange bool, ) (*model.Registration, bool, error) { - return r.repo.CreateRegistration(ctx, userID, happeningID, spotRanges, hostGroups, canSkipSpotRange) + return r.repo.CreateRegistration(ctx, userID, happening, spotRanges, hostGroups, canSkipSpotRange) } // GetByUserAndHappening implements port.RegistrationRepo. diff --git a/apps/uno/infrastructure/postgres/registration.go b/apps/uno/infrastructure/postgres/registration.go index 0aca89b7ed..d809a2bf5b 100644 --- a/apps/uno/infrastructure/postgres/registration.go +++ b/apps/uno/infrastructure/postgres/registration.go @@ -55,14 +55,15 @@ func (r *RegistrationRepo) GetByUserAndHappening(ctx context.Context, userID, ha // An error here could be because of an issue with locking the table or a DB error. func (r *RegistrationRepo) CreateRegistration( ctx context.Context, - userID, happeningID string, + userID string, + happening model.Happening, spotRanges []model.SpotRange, hostGroups []string, canSkipSpotRange bool, ) (*model.Registration, bool, error) { r.logger.Info(ctx, "creating registration", "user_id", userID, - "happening_id", happeningID, + "happening_id", happening.ID, ) tx, err := r.db.BeginTxx(ctx, &sql.TxOptions{ @@ -93,11 +94,11 @@ func (r *RegistrationRepo) CreateRegistration( WHERE happening_id = $1 AND (status = 'registered' OR status = 'waiting') ` - err = tx.SelectContext(ctx, &existingRegsDB, query, happeningID) + err = tx.SelectContext(ctx, &existingRegsDB, query, happening.ID) if err != nil { r.logger.Error(ctx, "failed to get existing registrations for happening", "error", err, - "happening_id", happeningID, + "happening_id", happening.ID, ) return nil, false, err } @@ -209,12 +210,12 @@ func (r *RegistrationRepo) CreateRegistration( RETURNING user_id, happening_id, status, unregister_reason, created_at, prev_status, changed_at, changed_by ` - err = tx.GetContext(ctx, ®istrationDB, upsertQuery, userID, happeningID, string(status)) + err = tx.GetContext(ctx, ®istrationDB, upsertQuery, userID, happening.ID, string(status)) if err != nil { r.logger.Error(ctx, "failed to upsert registration", "error", err, "user_id", userID, - "happening_id", happeningID, + "happening_id", happening.ID, ) return nil, false, err } diff --git a/apps/uno/infrastructure/postgres/registration_test.go b/apps/uno/infrastructure/postgres/registration_test.go index 505c35d0a6..8429d6e96d 100644 --- a/apps/uno/infrastructure/postgres/registration_test.go +++ b/apps/uno/infrastructure/postgres/registration_test.go @@ -257,7 +257,7 @@ func TestRegistrationRepo_CreateRegistration(t *testing.T) { registration, isWaitlisted, err := repo.CreateRegistration( ctx, createdUser.ID, - createdHappening.ID, + createdHappening, []model.SpotRange{spotRange}, []string{}, false, @@ -340,7 +340,7 @@ func TestRegistrationRepo_CreateRegistrationWaitlisted(t *testing.T) { _, isWaitlisted1, err := repo.CreateRegistration( ctx, createdUser1.ID, - createdHappening.ID, + createdHappening, []model.SpotRange{spotRange}, []string{}, false, @@ -352,7 +352,7 @@ func TestRegistrationRepo_CreateRegistrationWaitlisted(t *testing.T) { registration2, isWaitlisted2, err := repo.CreateRegistration( ctx, createdUser2.ID, - createdHappening.ID, + createdHappening, []model.SpotRange{spotRange}, []string{}, false, From 8445b12de4c03c54b40ace417a9ff6b031179d35 Mon Sep 17 00:00:00 2001 From: jesper Date: Fri, 13 Feb 2026 14:25:43 +0100 Subject: [PATCH 3/5] Raffle decorator impl --- apps/uno/infrastructure/decorator/raffle.go | 115 ++++++++++++++++---- 1 file changed, 95 insertions(+), 20 deletions(-) diff --git a/apps/uno/infrastructure/decorator/raffle.go b/apps/uno/infrastructure/decorator/raffle.go index f1a55b87dd..e88ae5304d 100644 --- a/apps/uno/infrastructure/decorator/raffle.go +++ b/apps/uno/infrastructure/decorator/raffle.go @@ -2,6 +2,7 @@ package decorator import ( "context" + "errors" "sync" "time" "uno/domain/model" @@ -21,37 +22,42 @@ const ( // registration for bedpres happenings in a given time window. // Implements port.RegistrationRepo type RaffleDecorator struct { - repo port.RegistrationRepo - queue chan registration - mu *sync.Mutex + repo port.RegistrationRepo + mu *sync.Mutex + queues map[string]*eventQueue +} + +type eventQueue struct { + windowClose time.Time + queue chan registration } type registration struct { ctx context.Context userID string - happeningID string + happening model.Happening spotRanges []model.SpotRange hostGroups []string canSkipSpotRange bool } -func NewRaffleDecorator(repo port.RegistrationRepo, notif *notifier.Notifier) port.RegistrationRepo { - raffle := &RaffleDecorator{ - repo: repo, - queue: make(chan registration, MAX_QUEUE), - mu: &sync.Mutex{}, - } - - go raffle.start(notif) - return raffle +func newRegistration( + ctx context.Context, + userID string, + happening model.Happening, + spotRanges []model.SpotRange, + hostGroups []string, + canSkipSpotRange bool, +) registration { + return registration{ctx, userID, happening, spotRanges, hostGroups, canSkipSpotRange} } -// start the raffle backround job handling de-queueing registrations. -func (r *RaffleDecorator) start(notif *notifier.Notifier) { - done, finish := notif.Register() - - <-done - finish() +func NewRaffleDecorator(repo port.RegistrationRepo, notif *notifier.Notifier) port.RegistrationRepo { + return &RaffleDecorator{ + repo: repo, + queues: map[string]*eventQueue{}, + mu: &sync.Mutex{}, + } } // CreateRegistration implements port.RegistrationRepo. @@ -63,7 +69,76 @@ func (r *RaffleDecorator) CreateRegistration( hostGroups []string, canSkipSpotRange bool, ) (*model.Registration, bool, error) { - return r.repo.CreateRegistration(ctx, userID, happening, spotRanges, hostGroups, canSkipSpotRange) + shouldEnqueue := happening.IsBedpres() && happening.RegistrationStart != nil + + if !shouldEnqueue { + return r.repo.CreateRegistration(ctx, userID, happening, spotRanges, hostGroups, canSkipSpotRange) + } + + // This lock is also held by r.flushQueue, ensuring that the queue + // is flushed before any new registrations have the chance to go through. + r.mu.Lock() + + // SAFETY: asserted not nil + windowClose := (*happening.RegistrationStart).Add(RAFFLE_DURATION) + + // If raffle window is still open, get or create event queue and append the registrations + if time.Now().Before(windowClose) { + eq, ok := r.queues[happening.ID] + if !ok { + eq = &eventQueue{ + windowClose: windowClose, + queue: make(chan registration, MAX_QUEUE), + } + r.queues[happening.ID] = eq + } + r.mu.Unlock() + + eq.queue <- newRegistration(ctx, userID, happening, spotRanges, hostGroups, canSkipSpotRange) + return &model.Registration{ + UserID: userID, + HappeningID: happening.ID, + Status: model.RegistrationStatusPending, + UnregisterReason: nil, + CreatedAt: time.Now(), + PrevStatus: nil, + ChangedAt: nil, + ChangedBy: nil, + }, false, nil + } + + // Otherwise the window has closed and we need to empty the queue before registering + r.mu.Unlock() + err1 := r.flushQueue(happening.ID) + reg, waitlisted, err2 := r.repo.CreateRegistration(ctx, userID, happening, spotRanges, hostGroups, canSkipSpotRange) + + return reg, waitlisted, errors.Join(err1, err2) +} + +func (r *RaffleDecorator) flushQueue(happeningId string) error { + // Lock any new incomming registrations for this + // happening from trying to enqueue or register. + r.mu.Lock() + defer r.mu.Unlock() + + eq, ok := r.queues[happeningId] + + // No queue to flush + if !ok { + return nil + } + + var errs error + for { + select { + case reg := <-eq.queue: + _, _, err := r.repo.CreateRegistration(reg.ctx, reg.userID, reg.happening, reg.spotRanges, reg.hostGroups, reg.canSkipSpotRange) + errs = errors.Join(errs, err) + default: + delete(r.queues, happeningId) // SAFETY: locked + return errs + } + } } // GetByUserAndHappening implements port.RegistrationRepo. From 6faf5adc16dce45e81ae166756c9856b7b06005a Mon Sep 17 00:00:00 2001 From: jesper Date: Fri, 13 Feb 2026 15:27:59 +0100 Subject: [PATCH 4/5] Todos --- apps/uno/infrastructure/decorator/raffle.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/apps/uno/infrastructure/decorator/raffle.go b/apps/uno/infrastructure/decorator/raffle.go index e88ae5304d..1311b99c8d 100644 --- a/apps/uno/infrastructure/decorator/raffle.go +++ b/apps/uno/infrastructure/decorator/raffle.go @@ -11,6 +11,11 @@ import ( "github.com/jesperkha/notifier" ) +// TODO: raffle test: create a registration and poll its state for 15 seconds for a 10 second window +// TODO: add ID field to all Registration models (dto, model) +// TODO: make regRepo.BatchUpdateStatus() and insert the pending regs immediately as pending and batch update later +// TODO: make /happenings/{id}/registrations/{id}/status endpoint + const ( // How long the raffle period lasts at the start of a bedpres registration period RAFFLE_DURATION = time.Minute * 1 From c4ceb2174f9eaaa548ebdd7a43e37ad859fd72b3 Mon Sep 17 00:00:00 2001 From: jesperkha Date: Mon, 16 Feb 2026 12:17:51 +0100 Subject: [PATCH 5/5] New raffle goroutine --- apps/uno/infrastructure/decorator/raffle.go | 105 ++++++++------------ 1 file changed, 42 insertions(+), 63 deletions(-) diff --git a/apps/uno/infrastructure/decorator/raffle.go b/apps/uno/infrastructure/decorator/raffle.go index 1311b99c8d..fb354c4eae 100644 --- a/apps/uno/infrastructure/decorator/raffle.go +++ b/apps/uno/infrastructure/decorator/raffle.go @@ -2,7 +2,6 @@ package decorator import ( "context" - "errors" "sync" "time" "uno/domain/model" @@ -17,10 +16,11 @@ import ( // TODO: make /happenings/{id}/registrations/{id}/status endpoint const ( - // How long the raffle period lasts at the start of a bedpres registration period - RAFFLE_DURATION = time.Minute * 1 // Maximum length of registration queue before blocking MAX_QUEUE = 200 + + // How long the raffle period lasts at the start of a bedpres registration period + RAFFLE_DURATION = 10 * time.Second ) // RaffleDecorator wraps port.RegistrationRepo and adds raffle @@ -33,28 +33,11 @@ type RaffleDecorator struct { } type eventQueue struct { - windowClose time.Time - queue chan registration + queue chan registration } type registration struct { - ctx context.Context - userID string - happening model.Happening - spotRanges []model.SpotRange - hostGroups []string - canSkipSpotRange bool -} - -func newRegistration( - ctx context.Context, - userID string, - happening model.Happening, - spotRanges []model.SpotRange, - hostGroups []string, - canSkipSpotRange bool, -) registration { - return registration{ctx, userID, happening, spotRanges, hostGroups, canSkipSpotRange} + regId int } func NewRaffleDecorator(repo port.RegistrationRepo, notif *notifier.Notifier) port.RegistrationRepo { @@ -80,70 +63,66 @@ func (r *RaffleDecorator) CreateRegistration( return r.repo.CreateRegistration(ctx, userID, happening, spotRanges, hostGroups, canSkipSpotRange) } - // This lock is also held by r.flushQueue, ensuring that the queue + // This lock is also held by r.flushAndDeleteQueue, ensuring that the queue // is flushed before any new registrations have the chance to go through. r.mu.Lock() + defer r.mu.Unlock() // SAFETY: asserted not nil windowClose := (*happening.RegistrationStart).Add(RAFFLE_DURATION) - // If raffle window is still open, get or create event queue and append the registrations - if time.Now().Before(windowClose) { - eq, ok := r.queues[happening.ID] - if !ok { - eq = &eventQueue{ - windowClose: windowClose, - queue: make(chan registration, MAX_QUEUE), - } - r.queues[happening.ID] = eq + // If raffle window is closed, register normally without enqueuing. + if time.Now().After(windowClose) { + return r.repo.CreateRegistration(ctx, userID, happening, spotRanges, hostGroups, canSkipSpotRange) + } + + eq, ok := r.queues[happening.ID] + if !ok { + eq = &eventQueue{ + queue: make(chan registration, MAX_QUEUE), } - r.mu.Unlock() - - eq.queue <- newRegistration(ctx, userID, happening, spotRanges, hostGroups, canSkipSpotRange) - return &model.Registration{ - UserID: userID, - HappeningID: happening.ID, - Status: model.RegistrationStatusPending, - UnregisterReason: nil, - CreatedAt: time.Now(), - PrevStatus: nil, - ChangedAt: nil, - ChangedBy: nil, - }, false, nil + + go func() { + <-time.After(time.Until(windowClose)) + _ = r.flushAndDeleteQueue(happening.ID) + }() + + r.queues[happening.ID] = eq } - // Otherwise the window has closed and we need to empty the queue before registering - r.mu.Unlock() - err1 := r.flushQueue(happening.ID) - reg, waitlisted, err2 := r.repo.CreateRegistration(ctx, userID, happening, spotRanges, hostGroups, canSkipSpotRange) + eq.queue <- registration{ + regId: 0, // TODO: add regid + } - return reg, waitlisted, errors.Join(err1, err2) + return &model.Registration{ + UserID: userID, + HappeningID: happening.ID, + Status: model.RegistrationStatusPending, + UnregisterReason: nil, + CreatedAt: time.Now(), + PrevStatus: nil, + ChangedAt: nil, + ChangedBy: nil, + }, false, nil } -func (r *RaffleDecorator) flushQueue(happeningId string) error { +func (r *RaffleDecorator) flushAndDeleteQueue(happeningId string) error { // Lock any new incomming registrations for this // happening from trying to enqueue or register. r.mu.Lock() defer r.mu.Unlock() - eq, ok := r.queues[happeningId] + _, ok := r.queues[happeningId] // No queue to flush if !ok { return nil } - var errs error - for { - select { - case reg := <-eq.queue: - _, _, err := r.repo.CreateRegistration(reg.ctx, reg.userID, reg.happening, reg.spotRanges, reg.hostGroups, reg.canSkipSpotRange) - errs = errors.Join(errs, err) - default: - delete(r.queues, happeningId) // SAFETY: locked - return errs - } - } + // TODO: randomize and flush queue. + + delete(r.queues, happeningId) // SAFETY: locked + return nil } // GetByUserAndHappening implements port.RegistrationRepo.