perf, add index and transaction around create campaign

Signed-off-by: RonniSkansing <rskansing@gmail.com>
This commit is contained in:
RonniSkansing
2026-09-05 16:32:28 +02:00
parent 2670c40132
commit 09fc4bdfba
2 changed files with 45 additions and 6 deletions
@@ -2,6 +2,7 @@ package database
import (
"github.com/google/uuid"
"gorm.io/gorm"
)
const (
@@ -20,3 +21,11 @@ type RecipientGroupRecipient struct {
func (RecipientGroupRecipient) TableName() string {
return RECIPIENT_GROUP_RECIPIENT_TABLE
}
// Migrate adds an index leading with recipient_group_id. The existing unique index
// leads with recipient_id, so counting or listing a group's members
// (WHERE recipient_group_id = ?) could not use it and scanned the recipients per
// group, making the group list quadratic in recipients times groups.
func (RecipientGroupRecipient) Migrate(db *gorm.DB) error {
return db.Exec(`CREATE INDEX IF NOT EXISTS idx_rgr_group ON recipient_group_recipients(recipient_group_id, recipient_id)`).Error
}
+36 -6
View File
@@ -502,6 +502,36 @@ func (c *Campaign) insertScheduledRecipient(
}
}
// insertScheduledRecipients writes the materialized campaign recipients. With no
// lure codes it inserts them in one transaction so the rows share a single fsync;
// per row autocommit is fsync bound and made a large campaign exceed the server
// write timeout during create. Lure code campaigns keep the per row path, because a
// code redraw on a conflict runs on a second connection and would block on the
// transaction's own write lock.
func (c *Campaign) insertScheduledRecipients(
ctx context.Context,
allocator *lureCodeAllocator,
campaignRecipients []*model.CampaignRecipient,
) error {
if allocator != nil && allocator.state.enabled {
for _, cr := range campaignRecipients {
if err := c.insertScheduledRecipient(ctx, allocator, cr); err != nil {
return err
}
}
return nil
}
return c.CampaignRecipientRepository.DB.Transaction(func(tx *gorm.DB) error {
txRepo := &repository.CampaignRecipient{DB: tx}
for _, cr := range campaignRecipients {
if _, err := txRepo.Insert(ctx, cr); err != nil {
return err
}
}
return nil
})
}
// snapshotLureSettings copies the template's lure URL settings and training flag
// onto the campaign while it still holds no recipients.
//
@@ -975,12 +1005,12 @@ func (c *Campaign) schedule(
c.Logger.Errorw("failed to allocate lure code", "error", err)
return err
}
// save
err = c.insertScheduledRecipient(ctx, allocator, campaignRecipients[i])
if err != nil {
c.Logger.Errorw("failed to create campaign", "error", err)
return errs.Wrap(err)
}
}
// insert all rows in one transaction so a large campaign does not exceed the
// server write timeout from per row fsyncs
if err = c.insertScheduledRecipients(ctx, allocator, campaignRecipients); err != nil {
c.Logger.Errorw("failed to create campaign", "error", err)
return errs.Wrap(err)
}
err = c.setMostNotableCampaignEvent(
ctx,