init push
This commit is contained in:
165
internal/repository/mysql/email_outbox_repo.go
Normal file
165
internal/repository/mysql/email_outbox_repo.go
Normal file
@@ -0,0 +1,165 @@
|
||||
package mysql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
|
||||
"wucher/internal/domain/auth"
|
||||
)
|
||||
|
||||
func (r *AuthRepository) WithTransaction(ctx context.Context, fn func(repo auth.Repository, outbox auth.EmailOutboxWriter) error) error {
|
||||
if fn == nil {
|
||||
return nil
|
||||
}
|
||||
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
txRepo := &AuthRepository{db: tx}
|
||||
return fn(txRepo, txRepo)
|
||||
})
|
||||
}
|
||||
|
||||
func (r *AuthRepository) CreateEmailOutboxMessage(ctx context.Context, message *auth.EmailOutboxMessage) error {
|
||||
if message == nil {
|
||||
return gorm.ErrInvalidData
|
||||
}
|
||||
return r.db.WithContext(ctx).Create(message).Error
|
||||
}
|
||||
|
||||
func (r *AuthRepository) ClaimPendingEmailOutboxMessages(
|
||||
ctx context.Context,
|
||||
limit int,
|
||||
now time.Time,
|
||||
lockTTL time.Duration,
|
||||
workerID string,
|
||||
) ([]auth.EmailOutboxMessage, error) {
|
||||
if limit <= 0 {
|
||||
limit = 1
|
||||
}
|
||||
if lockTTL <= 0 {
|
||||
lockTTL = time.Minute
|
||||
}
|
||||
now = now.UTC()
|
||||
staleBefore := now.Add(-lockTTL)
|
||||
workerID = strings.TrimSpace(workerID)
|
||||
if workerID == "" {
|
||||
workerID = "worker"
|
||||
}
|
||||
|
||||
var messages []auth.EmailOutboxMessage
|
||||
err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.
|
||||
Clauses(clause.Locking{Strength: "UPDATE", Options: "SKIP LOCKED"}).
|
||||
Where(
|
||||
"((status = ? AND available_at <= ?) OR (status = ? AND locked_at IS NOT NULL AND locked_at < ?))",
|
||||
auth.EmailOutboxStatusPending,
|
||||
now,
|
||||
auth.EmailOutboxStatusProcessing,
|
||||
staleBefore,
|
||||
).
|
||||
Order("available_at ASC, created_at ASC").
|
||||
Limit(limit).
|
||||
Find(&messages).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if len(messages) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
ids := make([][]byte, 0, len(messages))
|
||||
for i := range messages {
|
||||
ids = append(ids, messages[i].ID)
|
||||
}
|
||||
|
||||
if err := tx.Model(&auth.EmailOutboxMessage{}).
|
||||
Where("id IN ?", ids).
|
||||
Updates(map[string]any{
|
||||
"status": auth.EmailOutboxStatusProcessing,
|
||||
"locked_at": now,
|
||||
"locked_by": workerID,
|
||||
"updated_at": now,
|
||||
"attempts": gorm.Expr("attempts + ?", 1),
|
||||
}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for i := range messages {
|
||||
messages[i].Status = auth.EmailOutboxStatusProcessing
|
||||
messages[i].Attempts++
|
||||
ts := now
|
||||
messages[i].LockedAt = &ts
|
||||
messages[i].LockedBy = workerID
|
||||
messages[i].UpdatedAt = now
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return messages, err
|
||||
}
|
||||
|
||||
func (r *AuthRepository) MarkEmailOutboxMessagePublished(ctx context.Context, id []byte, publishedAt time.Time) error {
|
||||
publishedAt = publishedAt.UTC()
|
||||
return r.db.WithContext(ctx).
|
||||
Model(&auth.EmailOutboxMessage{}).
|
||||
Where("id = ?", id).
|
||||
Updates(map[string]any{
|
||||
"status": auth.EmailOutboxStatusPublished,
|
||||
"published_at": publishedAt,
|
||||
"locked_at": nil,
|
||||
"locked_by": "",
|
||||
"last_error": "",
|
||||
"updated_at": publishedAt,
|
||||
}).Error
|
||||
}
|
||||
|
||||
func (r *AuthRepository) MarkEmailOutboxMessageRetry(ctx context.Context, id []byte, availableAt time.Time, lastErr string) error {
|
||||
availableAt = availableAt.UTC()
|
||||
return r.db.WithContext(ctx).
|
||||
Model(&auth.EmailOutboxMessage{}).
|
||||
Where("id = ?", id).
|
||||
Updates(map[string]any{
|
||||
"status": auth.EmailOutboxStatusPending,
|
||||
"available_at": availableAt,
|
||||
"locked_at": nil,
|
||||
"locked_by": "",
|
||||
"last_error": truncateOutboxError(strings.TrimSpace(lastErr), 4096),
|
||||
"updated_at": time.Now().UTC(),
|
||||
}).Error
|
||||
}
|
||||
|
||||
func (r *AuthRepository) MarkEmailOutboxMessageDead(ctx context.Context, id []byte, failedAt time.Time, lastErr string) error {
|
||||
failedAt = failedAt.UTC()
|
||||
return r.db.WithContext(ctx).
|
||||
Model(&auth.EmailOutboxMessage{}).
|
||||
Where("id = ?", id).
|
||||
Updates(map[string]any{
|
||||
"status": auth.EmailOutboxStatusDead,
|
||||
"available_at": failedAt,
|
||||
"locked_at": nil,
|
||||
"locked_by": "",
|
||||
"last_error": truncateOutboxError(strings.TrimSpace(lastErr), 4096),
|
||||
"updated_at": failedAt,
|
||||
}).Error
|
||||
}
|
||||
|
||||
func (r *AuthRepository) CountPendingEmailOutboxMessages(ctx context.Context, now time.Time) (int64, error) {
|
||||
var count int64
|
||||
err := r.db.WithContext(ctx).
|
||||
Model(&auth.EmailOutboxMessage{}).
|
||||
Where(
|
||||
"(status = ? AND available_at <= ?) OR status = ?",
|
||||
auth.EmailOutboxStatusPending,
|
||||
now.UTC(),
|
||||
auth.EmailOutboxStatusProcessing,
|
||||
).
|
||||
Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
|
||||
func truncateOutboxError(v string, max int) string {
|
||||
if max <= 0 || len(v) <= max {
|
||||
return v
|
||||
}
|
||||
return v[:max]
|
||||
}
|
||||
Reference in New Issue
Block a user