| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621 |
- package repository
- import (
- "context"
- "database/sql"
- "encoding/json"
- "errors"
- "fmt"
- "strings"
- "time"
- dbent "github.com/Wei-Shaw/sub2api/ent"
- dbusagecleanuptask "github.com/Wei-Shaw/sub2api/ent/usagecleanuptask"
- "github.com/Wei-Shaw/sub2api/internal/pkg/pagination"
- "github.com/Wei-Shaw/sub2api/internal/service"
- )
- type usageCleanupRepository struct {
- client *dbent.Client
- sql sqlExecutor
- }
- func NewUsageCleanupRepository(client *dbent.Client, sqlDB *sql.DB) service.UsageCleanupRepository {
- return newUsageCleanupRepositoryWithSQL(client, sqlDB)
- }
- func newUsageCleanupRepositoryWithSQL(client *dbent.Client, sqlq sqlExecutor) *usageCleanupRepository {
- return &usageCleanupRepository{client: client, sql: sqlq}
- }
- func (r *usageCleanupRepository) CreateTask(ctx context.Context, task *service.UsageCleanupTask) error {
- if task == nil {
- return nil
- }
- if r.client != nil {
- return r.createTaskWithEnt(ctx, task)
- }
- return r.createTaskWithSQL(ctx, task)
- }
- func (r *usageCleanupRepository) ListTasks(ctx context.Context, params pagination.PaginationParams) ([]service.UsageCleanupTask, *pagination.PaginationResult, error) {
- if r.client != nil {
- return r.listTasksWithEnt(ctx, params)
- }
- var total int64
- if err := scanSingleRow(ctx, r.sql, "SELECT COUNT(*) FROM usage_cleanup_tasks", nil, &total); err != nil {
- return nil, nil, err
- }
- if total == 0 {
- return []service.UsageCleanupTask{}, paginationResultFromTotal(0, params), nil
- }
- query := `
- SELECT id, status, filters, created_by, deleted_rows, error_message,
- canceled_by, canceled_at,
- started_at, finished_at, created_at, updated_at
- FROM usage_cleanup_tasks
- ORDER BY created_at DESC, id DESC
- LIMIT $1 OFFSET $2
- `
- rows, err := r.sql.QueryContext(ctx, query, params.Limit(), params.Offset())
- if err != nil {
- return nil, nil, err
- }
- defer func() { _ = rows.Close() }()
- tasks := make([]service.UsageCleanupTask, 0)
- for rows.Next() {
- var task service.UsageCleanupTask
- var filtersJSON []byte
- var errMsg sql.NullString
- var canceledBy sql.NullInt64
- var canceledAt sql.NullTime
- var startedAt sql.NullTime
- var finishedAt sql.NullTime
- if err := rows.Scan(
- &task.ID,
- &task.Status,
- &filtersJSON,
- &task.CreatedBy,
- &task.DeletedRows,
- &errMsg,
- &canceledBy,
- &canceledAt,
- &startedAt,
- &finishedAt,
- &task.CreatedAt,
- &task.UpdatedAt,
- ); err != nil {
- return nil, nil, err
- }
- if err := json.Unmarshal(filtersJSON, &task.Filters); err != nil {
- return nil, nil, fmt.Errorf("parse cleanup filters: %w", err)
- }
- if errMsg.Valid {
- task.ErrorMsg = &errMsg.String
- }
- if canceledBy.Valid {
- v := canceledBy.Int64
- task.CanceledBy = &v
- }
- if canceledAt.Valid {
- task.CanceledAt = &canceledAt.Time
- }
- if startedAt.Valid {
- task.StartedAt = &startedAt.Time
- }
- if finishedAt.Valid {
- task.FinishedAt = &finishedAt.Time
- }
- tasks = append(tasks, task)
- }
- if err := rows.Err(); err != nil {
- return nil, nil, err
- }
- return tasks, paginationResultFromTotal(total, params), nil
- }
- func (r *usageCleanupRepository) ClaimNextPendingTask(ctx context.Context, staleRunningAfterSeconds int64) (*service.UsageCleanupTask, error) {
- if staleRunningAfterSeconds <= 0 {
- staleRunningAfterSeconds = 1800
- }
- query := `
- WITH next AS (
- SELECT id
- FROM usage_cleanup_tasks
- WHERE status = $1
- OR (
- status = $2
- AND started_at IS NOT NULL
- AND started_at < NOW() - ($3 * interval '1 second')
- )
- ORDER BY created_at ASC
- LIMIT 1
- FOR UPDATE SKIP LOCKED
- )
- UPDATE usage_cleanup_tasks AS tasks
- SET status = $4,
- started_at = NOW(),
- finished_at = NULL,
- error_message = NULL,
- updated_at = NOW()
- FROM next
- WHERE tasks.id = next.id
- RETURNING tasks.id, tasks.status, tasks.filters, tasks.created_by, tasks.deleted_rows, tasks.error_message,
- tasks.started_at, tasks.finished_at, tasks.created_at, tasks.updated_at
- `
- var task service.UsageCleanupTask
- var filtersJSON []byte
- var errMsg sql.NullString
- var startedAt sql.NullTime
- var finishedAt sql.NullTime
- if err := scanSingleRow(
- ctx,
- r.sql,
- query,
- []any{
- service.UsageCleanupStatusPending,
- service.UsageCleanupStatusRunning,
- staleRunningAfterSeconds,
- service.UsageCleanupStatusRunning,
- },
- &task.ID,
- &task.Status,
- &filtersJSON,
- &task.CreatedBy,
- &task.DeletedRows,
- &errMsg,
- &startedAt,
- &finishedAt,
- &task.CreatedAt,
- &task.UpdatedAt,
- ); err != nil {
- if errors.Is(err, sql.ErrNoRows) {
- return nil, nil
- }
- return nil, err
- }
- if err := json.Unmarshal(filtersJSON, &task.Filters); err != nil {
- return nil, fmt.Errorf("parse cleanup filters: %w", err)
- }
- if errMsg.Valid {
- task.ErrorMsg = &errMsg.String
- }
- if startedAt.Valid {
- task.StartedAt = &startedAt.Time
- }
- if finishedAt.Valid {
- task.FinishedAt = &finishedAt.Time
- }
- return &task, nil
- }
- func (r *usageCleanupRepository) GetTaskStatus(ctx context.Context, taskID int64) (string, error) {
- if r.client != nil {
- return r.getTaskStatusWithEnt(ctx, taskID)
- }
- var status string
- if err := scanSingleRow(ctx, r.sql, "SELECT status FROM usage_cleanup_tasks WHERE id = $1", []any{taskID}, &status); err != nil {
- return "", err
- }
- return status, nil
- }
- func (r *usageCleanupRepository) UpdateTaskProgress(ctx context.Context, taskID int64, deletedRows int64) error {
- if r.client != nil {
- return r.updateTaskProgressWithEnt(ctx, taskID, deletedRows)
- }
- query := `
- UPDATE usage_cleanup_tasks
- SET deleted_rows = $1,
- updated_at = NOW()
- WHERE id = $2
- `
- _, err := r.sql.ExecContext(ctx, query, deletedRows, taskID)
- return err
- }
- func (r *usageCleanupRepository) CancelTask(ctx context.Context, taskID int64, canceledBy int64) (bool, error) {
- if r.client != nil {
- return r.cancelTaskWithEnt(ctx, taskID, canceledBy)
- }
- query := `
- UPDATE usage_cleanup_tasks
- SET status = $1,
- canceled_by = $3,
- canceled_at = NOW(),
- finished_at = NOW(),
- error_message = NULL,
- updated_at = NOW()
- WHERE id = $2
- AND status IN ($4, $5)
- RETURNING id
- `
- var id int64
- err := scanSingleRow(ctx, r.sql, query, []any{
- service.UsageCleanupStatusCanceled,
- taskID,
- canceledBy,
- service.UsageCleanupStatusPending,
- service.UsageCleanupStatusRunning,
- }, &id)
- if errors.Is(err, sql.ErrNoRows) {
- return false, nil
- }
- if err != nil {
- return false, err
- }
- return true, nil
- }
- func (r *usageCleanupRepository) MarkTaskSucceeded(ctx context.Context, taskID int64, deletedRows int64) error {
- if r.client != nil {
- return r.markTaskSucceededWithEnt(ctx, taskID, deletedRows)
- }
- query := `
- UPDATE usage_cleanup_tasks
- SET status = $1,
- deleted_rows = $2,
- finished_at = NOW(),
- updated_at = NOW()
- WHERE id = $3
- `
- _, err := r.sql.ExecContext(ctx, query, service.UsageCleanupStatusSucceeded, deletedRows, taskID)
- return err
- }
- func (r *usageCleanupRepository) MarkTaskFailed(ctx context.Context, taskID int64, deletedRows int64, errorMsg string) error {
- if r.client != nil {
- return r.markTaskFailedWithEnt(ctx, taskID, deletedRows, errorMsg)
- }
- query := `
- UPDATE usage_cleanup_tasks
- SET status = $1,
- deleted_rows = $2,
- error_message = $3,
- finished_at = NOW(),
- updated_at = NOW()
- WHERE id = $4
- `
- _, err := r.sql.ExecContext(ctx, query, service.UsageCleanupStatusFailed, deletedRows, errorMsg, taskID)
- return err
- }
- func (r *usageCleanupRepository) DeleteUsageLogsBatch(ctx context.Context, filters service.UsageCleanupFilters, limit int) (int64, error) {
- if filters.StartTime.IsZero() || filters.EndTime.IsZero() {
- return 0, fmt.Errorf("cleanup filters missing time range")
- }
- whereClause, args := buildUsageCleanupWhere(filters)
- if whereClause == "" {
- return 0, fmt.Errorf("cleanup filters missing time range")
- }
- args = append(args, limit)
- if db, ok := r.sql.(*sql.DB); ok {
- return r.deleteUsageLogsBatchWithRollupInvalidation(ctx, db, whereClause, args)
- }
- query := fmt.Sprintf(`
- WITH target AS (
- SELECT id
- FROM usage_logs
- WHERE %s
- ORDER BY created_at ASC, id ASC
- LIMIT $%d
- )
- DELETE FROM usage_logs
- WHERE id IN (SELECT id FROM target)
- RETURNING created_at
- `, whereClause, len(args))
- rows, err := r.sql.QueryContext(ctx, query, args...)
- if err != nil {
- return 0, err
- }
- defer func() { _ = rows.Close() }()
- var deleted int64
- for rows.Next() {
- deleted++
- }
- if err := rows.Err(); err != nil {
- return 0, err
- }
- return deleted, nil
- }
- func (r *usageCleanupRepository) deleteUsageLogsBatchWithRollupInvalidation(ctx context.Context, db *sql.DB, whereClause string, args []any) (int64, error) {
- tx, err := db.BeginTx(ctx, nil)
- if err != nil {
- return 0, err
- }
- rollback := func(err error) (int64, error) {
- _ = tx.Rollback()
- return 0, err
- }
- if err := lockGroupUsageRollupState(ctx, tx); err != nil {
- return rollback(err)
- }
- query := fmt.Sprintf(`
- WITH target AS (
- SELECT id
- FROM usage_logs
- WHERE %s
- ORDER BY created_at ASC, id ASC
- LIMIT $%d
- )
- DELETE FROM usage_logs
- WHERE id IN (SELECT id FROM target)
- RETURNING created_at
- `, whereClause, len(args))
- rows, err := tx.QueryContext(ctx, query, args...)
- if err != nil {
- return rollback(err)
- }
- var deleted int64
- var earliestDeletedAt time.Time
- for rows.Next() {
- var deletedAt time.Time
- if err := rows.Scan(&deletedAt); err != nil {
- _ = rows.Close()
- return rollback(err)
- }
- deleted++
- if earliestDeletedAt.IsZero() || deletedAt.Before(earliestDeletedAt) {
- earliestDeletedAt = deletedAt
- }
- }
- if err := rows.Err(); err != nil {
- _ = rows.Close()
- return rollback(err)
- }
- if err := rows.Close(); err != nil {
- return rollback(err)
- }
- if deleted > 0 {
- if err := invalidateGroupUsageRollupsAt(ctx, tx, earliestDeletedAt); err != nil {
- return rollback(err)
- }
- }
- if err := tx.Commit(); err != nil {
- return 0, err
- }
- return deleted, nil
- }
- func buildUsageCleanupWhere(filters service.UsageCleanupFilters) (string, []any) {
- conditions := make([]string, 0, 8)
- args := make([]any, 0, 8)
- idx := 1
- if !filters.StartTime.IsZero() {
- conditions = append(conditions, fmt.Sprintf("created_at >= $%d", idx))
- args = append(args, filters.StartTime)
- idx++
- }
- if !filters.EndTime.IsZero() {
- conditions = append(conditions, fmt.Sprintf("created_at <= $%d", idx))
- args = append(args, filters.EndTime)
- idx++
- }
- if filters.UserID != nil {
- conditions = append(conditions, fmt.Sprintf("user_id = $%d", idx))
- args = append(args, *filters.UserID)
- idx++
- }
- if filters.APIKeyID != nil {
- conditions = append(conditions, fmt.Sprintf("api_key_id = $%d", idx))
- args = append(args, *filters.APIKeyID)
- idx++
- }
- if filters.AccountID != nil {
- conditions = append(conditions, fmt.Sprintf("account_id = $%d", idx))
- args = append(args, *filters.AccountID)
- idx++
- }
- if filters.GroupID != nil {
- conditions = append(conditions, fmt.Sprintf("group_id = $%d", idx))
- args = append(args, *filters.GroupID)
- idx++
- }
- if filters.Model != nil {
- model := strings.TrimSpace(*filters.Model)
- if model != "" {
- conditions = append(conditions, fmt.Sprintf("model = $%d", idx))
- args = append(args, model)
- idx++
- }
- }
- if filters.RequestType != nil {
- condition, conditionArgs := buildRequestTypeFilterCondition(idx, *filters.RequestType)
- conditions = append(conditions, condition)
- args = append(args, conditionArgs...)
- idx += len(conditionArgs)
- } else if filters.Stream != nil {
- conditions = append(conditions, fmt.Sprintf("stream = $%d", idx))
- args = append(args, *filters.Stream)
- idx++
- }
- if filters.BillingType != nil {
- conditions = append(conditions, fmt.Sprintf("billing_type = $%d", idx))
- args = append(args, *filters.BillingType)
- }
- return strings.Join(conditions, " AND "), args
- }
- func (r *usageCleanupRepository) createTaskWithEnt(ctx context.Context, task *service.UsageCleanupTask) error {
- client := clientFromContext(ctx, r.client)
- filtersJSON, err := json.Marshal(task.Filters)
- if err != nil {
- return fmt.Errorf("marshal cleanup filters: %w", err)
- }
- created, err := client.UsageCleanupTask.
- Create().
- SetStatus(task.Status).
- SetFilters(json.RawMessage(filtersJSON)).
- SetCreatedBy(task.CreatedBy).
- SetDeletedRows(task.DeletedRows).
- Save(ctx)
- if err != nil {
- return err
- }
- task.ID = created.ID
- task.CreatedAt = created.CreatedAt
- task.UpdatedAt = created.UpdatedAt
- return nil
- }
- func (r *usageCleanupRepository) createTaskWithSQL(ctx context.Context, task *service.UsageCleanupTask) error {
- filtersJSON, err := json.Marshal(task.Filters)
- if err != nil {
- return fmt.Errorf("marshal cleanup filters: %w", err)
- }
- query := `
- INSERT INTO usage_cleanup_tasks (
- status,
- filters,
- created_by,
- deleted_rows
- ) VALUES ($1, $2, $3, $4)
- RETURNING id, created_at, updated_at
- `
- if err := scanSingleRow(ctx, r.sql, query, []any{task.Status, filtersJSON, task.CreatedBy, task.DeletedRows}, &task.ID, &task.CreatedAt, &task.UpdatedAt); err != nil {
- return err
- }
- return nil
- }
- func (r *usageCleanupRepository) listTasksWithEnt(ctx context.Context, params pagination.PaginationParams) ([]service.UsageCleanupTask, *pagination.PaginationResult, error) {
- client := clientFromContext(ctx, r.client)
- query := client.UsageCleanupTask.Query()
- total, err := query.Clone().Count(ctx)
- if err != nil {
- return nil, nil, err
- }
- if total == 0 {
- return []service.UsageCleanupTask{}, paginationResultFromTotal(0, params), nil
- }
- rows, err := query.
- Order(dbent.Desc(dbusagecleanuptask.FieldCreatedAt), dbent.Desc(dbusagecleanuptask.FieldID)).
- Offset(params.Offset()).
- Limit(params.Limit()).
- All(ctx)
- if err != nil {
- return nil, nil, err
- }
- tasks := make([]service.UsageCleanupTask, 0, len(rows))
- for _, row := range rows {
- task, err := usageCleanupTaskFromEnt(row)
- if err != nil {
- return nil, nil, err
- }
- tasks = append(tasks, task)
- }
- return tasks, paginationResultFromTotal(int64(total), params), nil
- }
- func (r *usageCleanupRepository) getTaskStatusWithEnt(ctx context.Context, taskID int64) (string, error) {
- client := clientFromContext(ctx, r.client)
- task, err := client.UsageCleanupTask.Query().
- Where(dbusagecleanuptask.IDEQ(taskID)).
- Only(ctx)
- if err != nil {
- if dbent.IsNotFound(err) {
- return "", sql.ErrNoRows
- }
- return "", err
- }
- return task.Status, nil
- }
- func (r *usageCleanupRepository) updateTaskProgressWithEnt(ctx context.Context, taskID int64, deletedRows int64) error {
- client := clientFromContext(ctx, r.client)
- now := time.Now()
- _, err := client.UsageCleanupTask.Update().
- Where(dbusagecleanuptask.IDEQ(taskID)).
- SetDeletedRows(deletedRows).
- SetUpdatedAt(now).
- Save(ctx)
- return err
- }
- func (r *usageCleanupRepository) cancelTaskWithEnt(ctx context.Context, taskID int64, canceledBy int64) (bool, error) {
- client := clientFromContext(ctx, r.client)
- now := time.Now()
- affected, err := client.UsageCleanupTask.Update().
- Where(
- dbusagecleanuptask.IDEQ(taskID),
- dbusagecleanuptask.StatusIn(service.UsageCleanupStatusPending, service.UsageCleanupStatusRunning),
- ).
- SetStatus(service.UsageCleanupStatusCanceled).
- SetCanceledBy(canceledBy).
- SetCanceledAt(now).
- SetFinishedAt(now).
- ClearErrorMessage().
- SetUpdatedAt(now).
- Save(ctx)
- if err != nil {
- return false, err
- }
- return affected > 0, nil
- }
- func (r *usageCleanupRepository) markTaskSucceededWithEnt(ctx context.Context, taskID int64, deletedRows int64) error {
- client := clientFromContext(ctx, r.client)
- now := time.Now()
- _, err := client.UsageCleanupTask.Update().
- Where(dbusagecleanuptask.IDEQ(taskID)).
- SetStatus(service.UsageCleanupStatusSucceeded).
- SetDeletedRows(deletedRows).
- SetFinishedAt(now).
- SetUpdatedAt(now).
- Save(ctx)
- return err
- }
- func (r *usageCleanupRepository) markTaskFailedWithEnt(ctx context.Context, taskID int64, deletedRows int64, errorMsg string) error {
- client := clientFromContext(ctx, r.client)
- now := time.Now()
- _, err := client.UsageCleanupTask.Update().
- Where(dbusagecleanuptask.IDEQ(taskID)).
- SetStatus(service.UsageCleanupStatusFailed).
- SetDeletedRows(deletedRows).
- SetErrorMessage(errorMsg).
- SetFinishedAt(now).
- SetUpdatedAt(now).
- Save(ctx)
- return err
- }
- func usageCleanupTaskFromEnt(row *dbent.UsageCleanupTask) (service.UsageCleanupTask, error) {
- task := service.UsageCleanupTask{
- ID: row.ID,
- Status: row.Status,
- CreatedBy: row.CreatedBy,
- DeletedRows: row.DeletedRows,
- CreatedAt: row.CreatedAt,
- UpdatedAt: row.UpdatedAt,
- }
- if len(row.Filters) > 0 {
- if err := json.Unmarshal(row.Filters, &task.Filters); err != nil {
- return service.UsageCleanupTask{}, fmt.Errorf("parse cleanup filters: %w", err)
- }
- }
- if row.ErrorMessage != nil {
- task.ErrorMsg = row.ErrorMessage
- }
- if row.CanceledBy != nil {
- task.CanceledBy = row.CanceledBy
- }
- if row.CanceledAt != nil {
- task.CanceledAt = row.CanceledAt
- }
- if row.StartedAt != nil {
- task.StartedAt = row.StartedAt
- }
- if row.FinishedAt != nil {
- task.FinishedAt = row.FinishedAt
- }
- return task, nil
- }
|