callbacks service draft
This commit is contained in:
99
api/edge/callbacks/internal/storage/module.go
Normal file
99
api/edge/callbacks/internal/storage/module.go
Normal file
@@ -0,0 +1,99 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/tech/sendico/pkg/db"
|
||||
"github.com/tech/sendico/pkg/mlogger"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
)
|
||||
|
||||
// TaskStatus tracks delivery task lifecycle.
|
||||
type TaskStatus string
|
||||
|
||||
const (
|
||||
TaskStatusPending TaskStatus = "PENDING"
|
||||
TaskStatusRetry TaskStatus = "RETRY"
|
||||
TaskStatusDelivered TaskStatus = "DELIVERED"
|
||||
TaskStatusFailed TaskStatus = "FAILED"
|
||||
)
|
||||
|
||||
// Endpoint describes one target callback endpoint.
|
||||
type Endpoint struct {
|
||||
ID bson.ObjectID
|
||||
ClientID string
|
||||
URL string
|
||||
SigningMode string
|
||||
SecretRef string
|
||||
Headers map[string]string
|
||||
MaxAttempts int
|
||||
MinDelay time.Duration
|
||||
MaxDelay time.Duration
|
||||
RequestTimeout time.Duration
|
||||
}
|
||||
|
||||
// Task is one callback delivery job.
|
||||
type Task struct {
|
||||
ID bson.ObjectID
|
||||
EventID string
|
||||
EndpointID bson.ObjectID
|
||||
EndpointURL string
|
||||
SigningMode string
|
||||
SecretRef string
|
||||
Headers map[string]string
|
||||
Payload []byte
|
||||
Attempt int
|
||||
MaxAttempts int
|
||||
MinDelay time.Duration
|
||||
MaxDelay time.Duration
|
||||
RequestTimeout time.Duration
|
||||
Status TaskStatus
|
||||
NextAttemptAt time.Time
|
||||
}
|
||||
|
||||
// TaskDefaults are applied when creating tasks.
|
||||
type TaskDefaults struct {
|
||||
MaxAttempts int
|
||||
MinDelay time.Duration
|
||||
MaxDelay time.Duration
|
||||
RequestTimeout time.Duration
|
||||
}
|
||||
|
||||
// Options configures mongo collections.
|
||||
type Options struct {
|
||||
InboxCollection string
|
||||
TasksCollection string
|
||||
EndpointsCollection string
|
||||
}
|
||||
|
||||
// InboxRepo controls event dedupe state.
|
||||
type InboxRepo interface {
|
||||
TryInsert(ctx context.Context, eventID, clientID, eventType string, at time.Time) (bool, error)
|
||||
}
|
||||
|
||||
// EndpointRepo resolves endpoints for events.
|
||||
type EndpointRepo interface {
|
||||
FindActiveByClientAndType(ctx context.Context, clientID, eventType string) ([]Endpoint, error)
|
||||
}
|
||||
|
||||
// TaskRepo manages callback tasks.
|
||||
type TaskRepo interface {
|
||||
UpsertTasks(ctx context.Context, eventID string, endpoints []Endpoint, payload []byte, defaults TaskDefaults, at time.Time) error
|
||||
LockNextTask(ctx context.Context, now time.Time, workerID string, lockTTL time.Duration) (*Task, error)
|
||||
MarkDelivered(ctx context.Context, taskID bson.ObjectID, httpCode int, latency time.Duration, at time.Time) error
|
||||
MarkRetry(ctx context.Context, taskID bson.ObjectID, attempt int, nextAttemptAt time.Time, lastError string, httpCode int, at time.Time) error
|
||||
MarkFailed(ctx context.Context, taskID bson.ObjectID, attempt int, lastError string, httpCode int, at time.Time) error
|
||||
}
|
||||
|
||||
// Repository is the callbacks persistence contract.
|
||||
type Repository interface {
|
||||
Inbox() InboxRepo
|
||||
Endpoints() EndpointRepo
|
||||
Tasks() TaskRepo
|
||||
}
|
||||
|
||||
// New creates a Mongo-backed callbacks repository.
|
||||
func New(logger mlogger.Logger, conn *db.MongoConnection) (Repository, error) {
|
||||
return newMongoRepository(logger, conn)
|
||||
}
|
||||
513
api/edge/callbacks/internal/storage/service.go
Normal file
513
api/edge/callbacks/internal/storage/service.go
Normal file
@@ -0,0 +1,513 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/tech/sendico/pkg/db"
|
||||
"github.com/tech/sendico/pkg/db/repository"
|
||||
"github.com/tech/sendico/pkg/db/repository/builder"
|
||||
ri "github.com/tech/sendico/pkg/db/repository/index"
|
||||
"github.com/tech/sendico/pkg/db/storable"
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
"github.com/tech/sendico/pkg/mlogger"
|
||||
mutil "github.com/tech/sendico/pkg/mutil/db"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
"go.mongodb.org/mongo-driver/v2/mongo"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
const (
|
||||
inboxCollection string = "inbox"
|
||||
tasksCollection string = "tasks"
|
||||
endpointsCollection string = "endpoints"
|
||||
)
|
||||
|
||||
type mongoRepository struct {
|
||||
logger mlogger.Logger
|
||||
|
||||
inboxRepo repository.Repository
|
||||
tasksRepo repository.Repository
|
||||
endpointsRepo repository.Repository
|
||||
|
||||
inbox InboxRepo
|
||||
endpoints EndpointRepo
|
||||
tasks TaskRepo
|
||||
}
|
||||
|
||||
type inboxDoc struct {
|
||||
storable.Base `bson:",inline"`
|
||||
EventID string `bson:"event_id"`
|
||||
ClientID string `bson:"client_id"`
|
||||
EventType string `bson:"event_type"`
|
||||
}
|
||||
|
||||
func (d *inboxDoc) Collection() string {
|
||||
return inboxCollection
|
||||
}
|
||||
|
||||
type delayConfig struct {
|
||||
MinDelayMS int `bson:"min_ms"`
|
||||
MaxDelayMS int `bson:"max_ms"`
|
||||
}
|
||||
|
||||
type deliveryPolicy struct {
|
||||
delayConfig `bson:",inline"`
|
||||
SigningMode string `bson:"signing_mode"`
|
||||
SecretRef string `bson:"secret_ref"`
|
||||
Headers map[string]string `bson:"headers"`
|
||||
MaxAttempts int `bson:"max_attempts"`
|
||||
RequestTimeoutMS int `bson:"request_timeout_ms"`
|
||||
}
|
||||
|
||||
type endpointDoc struct {
|
||||
storable.Base `bson:",inline"`
|
||||
deliveryPolicy `bson:"retry_policy"`
|
||||
ClientID string `bson:"client_id"`
|
||||
Status string `bson:"status"`
|
||||
URL string `bson:"url"`
|
||||
EventTypes []string `bson:"event_types"`
|
||||
}
|
||||
|
||||
func (d *endpointDoc) Collection() string {
|
||||
return endpointsCollection
|
||||
}
|
||||
|
||||
type taskDoc struct {
|
||||
storable.Base `bson:",inline"`
|
||||
deliveryPolicy `bson:"retry_policy"`
|
||||
EventID string `bson:"event_id"`
|
||||
EndpointID bson.ObjectID `bson:"endpoint_id"`
|
||||
EndpointURL string `bson:"endpoint_url"`
|
||||
Payload []byte `bson:"payload"`
|
||||
Status TaskStatus `bson:"status"`
|
||||
Attempt int `bson:"attempt"`
|
||||
LastError string `bson:"last_error,omitempty"`
|
||||
LastHTTPCode int `bson:"last_http_code,omitempty"`
|
||||
NextAttemptAt time.Time `bson:"next_attempt_at"`
|
||||
LockedUntil *time.Time `bson:"locked_until,omitempty"`
|
||||
WorkerID string `bson:"worker_id,omitempty"`
|
||||
DeliveredAt *time.Time `bson:"delivered_at,omitempty"`
|
||||
}
|
||||
|
||||
func (d *taskDoc) Collection() string {
|
||||
return tasksCollection
|
||||
}
|
||||
|
||||
func newMongoRepository(logger mlogger.Logger, conn *db.MongoConnection) (Repository, error) {
|
||||
if logger == nil {
|
||||
logger = zap.NewNop()
|
||||
}
|
||||
if conn == nil {
|
||||
return nil, merrors.InvalidArgument("callbacks storage: mongo connection is required", "conn")
|
||||
}
|
||||
|
||||
repo := &mongoRepository{
|
||||
logger: logger.Named("storage"),
|
||||
inboxRepo: repository.CreateMongoRepository(conn.Database(), inboxCollection),
|
||||
tasksRepo: repository.CreateMongoRepository(conn.Database(), tasksCollection),
|
||||
endpointsRepo: repository.CreateMongoRepository(conn.Database(), endpointsCollection),
|
||||
}
|
||||
|
||||
if err := repo.ensureIndexes(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
repo.inbox = &inboxStore{logger: repo.logger.Named(repo.inboxRepo.Collection()), repo: repo.inboxRepo}
|
||||
repo.endpoints = &endpointStore{logger: repo.logger.Named(repo.endpointsRepo.Collection()), repo: repo.endpointsRepo}
|
||||
repo.tasks = &taskStore{logger: repo.logger.Named(repo.tasksRepo.Collection()), repo: repo.tasksRepo}
|
||||
|
||||
return repo, nil
|
||||
}
|
||||
|
||||
func (m *mongoRepository) Inbox() InboxRepo {
|
||||
return m.inbox
|
||||
}
|
||||
|
||||
func (m *mongoRepository) Endpoints() EndpointRepo {
|
||||
return m.endpoints
|
||||
}
|
||||
|
||||
func (m *mongoRepository) Tasks() TaskRepo {
|
||||
return m.tasks
|
||||
}
|
||||
|
||||
func (m *mongoRepository) ensureIndexes() error {
|
||||
if err := m.inboxRepo.CreateIndex(&ri.Definition{
|
||||
Name: "uq_event_id",
|
||||
Unique: true,
|
||||
Keys: []ri.Key{
|
||||
{Field: "event_id", Sort: ri.Asc},
|
||||
},
|
||||
}); err != nil {
|
||||
return merrors.InternalWrap(err, "callbacks storage: failed to create inbox indexes")
|
||||
}
|
||||
|
||||
for _, def := range []*ri.Definition{
|
||||
{
|
||||
Name: "uq_event_endpoint",
|
||||
Unique: true,
|
||||
Keys: []ri.Key{
|
||||
{Field: "event_id", Sort: ri.Asc},
|
||||
{Field: "endpoint_id", Sort: ri.Asc},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "idx_dispatch_scan",
|
||||
Keys: []ri.Key{
|
||||
{Field: "status", Sort: ri.Asc},
|
||||
{Field: "next_attempt_at", Sort: ri.Asc},
|
||||
{Field: "locked_until", Sort: ri.Asc},
|
||||
},
|
||||
},
|
||||
} {
|
||||
if err := m.tasksRepo.CreateIndex(def); err != nil {
|
||||
return merrors.InternalWrap(err, "callbacks storage: failed to create tasks indexes")
|
||||
}
|
||||
}
|
||||
|
||||
if err := m.endpointsRepo.CreateIndex(&ri.Definition{
|
||||
Name: "idx_client_event",
|
||||
Keys: []ri.Key{
|
||||
{Field: "client_id", Sort: ri.Asc},
|
||||
{Field: "status", Sort: ri.Asc},
|
||||
{Field: "event_types", Sort: ri.Asc},
|
||||
},
|
||||
}); err != nil {
|
||||
return merrors.InternalWrap(err, "callbacks storage: failed to create endpoint indexes")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type inboxStore struct {
|
||||
logger mlogger.Logger
|
||||
repo repository.Repository
|
||||
}
|
||||
|
||||
func (r *inboxStore) TryInsert(ctx context.Context, eventID, clientID, eventType string, at time.Time) (bool, error) {
|
||||
doc := &inboxDoc{
|
||||
EventID: strings.TrimSpace(eventID),
|
||||
ClientID: strings.TrimSpace(clientID),
|
||||
EventType: strings.TrimSpace(eventType),
|
||||
}
|
||||
|
||||
filter := repository.Filter("event_id", doc.EventID)
|
||||
if err := r.repo.Insert(ctx, doc, filter); err != nil {
|
||||
if errors.Is(err, merrors.ErrDataConflict) {
|
||||
return false, nil
|
||||
}
|
||||
r.logger.Warn("Failed to insert inbox dedupe marker", zap.String("event_id", eventID), zap.Error(err))
|
||||
return false, merrors.InternalWrap(err, "callbacks inbox insert failed")
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
type endpointStore struct {
|
||||
logger mlogger.Logger
|
||||
repo repository.Repository
|
||||
}
|
||||
|
||||
func (r *endpointStore) FindActiveByClientAndType(ctx context.Context, clientID, eventType string) ([]Endpoint, error) {
|
||||
clientID = strings.TrimSpace(clientID)
|
||||
eventType = strings.TrimSpace(eventType)
|
||||
if clientID == "" {
|
||||
return nil, merrors.InvalidArgument("client_id is required", "client_id")
|
||||
}
|
||||
if eventType == "" {
|
||||
return nil, merrors.InvalidArgument("event type is required", "event_type")
|
||||
}
|
||||
|
||||
query := repository.Query().
|
||||
Filter(repository.Field("client_id"), clientID).
|
||||
In(repository.Field("status"), "active", "enabled")
|
||||
|
||||
out := make([]Endpoint, 0)
|
||||
err := r.repo.FindManyByFilter(ctx, query, func(cur *mongo.Cursor) error {
|
||||
doc := &endpointDoc{}
|
||||
if err := cur.Decode(doc); err != nil {
|
||||
return err
|
||||
}
|
||||
if strings.TrimSpace(doc.URL) == "" {
|
||||
return nil
|
||||
}
|
||||
if !supportsEventType(doc.EventTypes, eventType) {
|
||||
return nil
|
||||
}
|
||||
out = append(out, Endpoint{
|
||||
ID: doc.ID,
|
||||
ClientID: doc.ClientID,
|
||||
URL: strings.TrimSpace(doc.URL),
|
||||
SigningMode: strings.TrimSpace(doc.SigningMode),
|
||||
SecretRef: strings.TrimSpace(doc.SecretRef),
|
||||
Headers: cloneHeaders(doc.Headers),
|
||||
MaxAttempts: doc.MaxAttempts,
|
||||
MinDelay: time.Duration(doc.MinDelayMS) * time.Millisecond,
|
||||
MaxDelay: time.Duration(doc.MaxDelayMS) * time.Millisecond,
|
||||
RequestTimeout: time.Duration(doc.RequestTimeoutMS) * time.Millisecond,
|
||||
})
|
||||
return nil
|
||||
})
|
||||
if err != nil && !errors.Is(err, merrors.ErrNoData) {
|
||||
return nil, merrors.InternalWrap(err, "callbacks endpoint lookup failed")
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func supportsEventType(eventTypes []string, eventType string) bool {
|
||||
if len(eventTypes) == 0 {
|
||||
return true
|
||||
}
|
||||
eventType = strings.TrimSpace(eventType)
|
||||
for _, t := range eventTypes {
|
||||
current := strings.TrimSpace(t)
|
||||
if current == "" {
|
||||
continue
|
||||
}
|
||||
if current == "*" || current == eventType {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type taskStore struct {
|
||||
logger mlogger.Logger
|
||||
repo repository.Repository
|
||||
}
|
||||
|
||||
func (r *taskStore) UpsertTasks(ctx context.Context, eventID string, endpoints []Endpoint, payload []byte, defaults TaskDefaults, at time.Time) error {
|
||||
eventID = strings.TrimSpace(eventID)
|
||||
if eventID == "" {
|
||||
return merrors.InvalidArgument("event id is required", "event_id")
|
||||
}
|
||||
if len(endpoints) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
now := at.UTC()
|
||||
for _, endpoint := range endpoints {
|
||||
if endpoint.ID == bson.NilObjectID {
|
||||
continue
|
||||
}
|
||||
|
||||
maxAttempts := endpoint.MaxAttempts
|
||||
if maxAttempts <= 0 {
|
||||
maxAttempts = defaults.MaxAttempts
|
||||
}
|
||||
if maxAttempts <= 0 {
|
||||
maxAttempts = 1
|
||||
}
|
||||
|
||||
minDelay := endpoint.MinDelay
|
||||
if minDelay <= 0 {
|
||||
minDelay = defaults.MinDelay
|
||||
}
|
||||
if minDelay <= 0 {
|
||||
minDelay = time.Second
|
||||
}
|
||||
|
||||
maxDelay := endpoint.MaxDelay
|
||||
if maxDelay <= 0 {
|
||||
maxDelay = defaults.MaxDelay
|
||||
}
|
||||
if maxDelay < minDelay {
|
||||
maxDelay = minDelay
|
||||
}
|
||||
|
||||
requestTimeout := endpoint.RequestTimeout
|
||||
if requestTimeout <= 0 {
|
||||
requestTimeout = defaults.RequestTimeout
|
||||
}
|
||||
|
||||
doc := &taskDoc{}
|
||||
doc.EventID = eventID
|
||||
doc.EndpointID = endpoint.ID
|
||||
doc.EndpointURL = strings.TrimSpace(endpoint.URL)
|
||||
doc.SigningMode = strings.TrimSpace(endpoint.SigningMode)
|
||||
doc.SecretRef = strings.TrimSpace(endpoint.SecretRef)
|
||||
doc.Headers = cloneHeaders(endpoint.Headers)
|
||||
doc.Payload = append([]byte(nil), payload...)
|
||||
doc.Status = TaskStatusPending
|
||||
doc.Attempt = 0
|
||||
doc.MaxAttempts = maxAttempts
|
||||
doc.MinDelayMS = int(minDelay / time.Millisecond)
|
||||
doc.MaxDelayMS = int(maxDelay / time.Millisecond)
|
||||
doc.RequestTimeoutMS = int(requestTimeout / time.Millisecond)
|
||||
doc.NextAttemptAt = now
|
||||
|
||||
filter := repository.Filter("event_id", eventID).And(repository.Filter("endpoint_id", endpoint.ID))
|
||||
if err := r.repo.Insert(ctx, doc, filter); err != nil {
|
||||
if errors.Is(err, merrors.ErrDataConflict) {
|
||||
continue
|
||||
}
|
||||
return merrors.InternalWrap(err, "callbacks task upsert failed")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *taskStore) LockNextTask(ctx context.Context, now time.Time, workerID string, lockTTL time.Duration) (*Task, error) {
|
||||
workerID = strings.TrimSpace(workerID)
|
||||
if workerID == "" {
|
||||
return nil, merrors.InvalidArgument("worker id is required", "worker_id")
|
||||
}
|
||||
|
||||
now = now.UTC()
|
||||
limit := int64(32)
|
||||
lockFilter := repository.Query().Or(
|
||||
repository.Query().Comparison(repository.Field("locked_until"), builder.Exists, false),
|
||||
repository.Query().Filter(repository.Field("locked_until"), nil),
|
||||
repository.Query().Comparison(repository.Field("locked_until"), builder.Lte, now),
|
||||
)
|
||||
|
||||
query := repository.Query().
|
||||
In(repository.Field("status"), string(TaskStatusPending), string(TaskStatusRetry)).
|
||||
Comparison(repository.Field("next_attempt_at"), builder.Lte, now).
|
||||
And(lockFilter).
|
||||
Sort(repository.Field("next_attempt_at"), true).
|
||||
Sort(repository.Field("created_at"), true).
|
||||
Limit(&limit)
|
||||
|
||||
candidates, err := mutil.GetObjects[taskDoc](ctx, r.logger, query, nil, r.repo)
|
||||
if err != nil {
|
||||
if errors.Is(err, merrors.ErrNoData) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, merrors.InternalWrap(err, "callbacks task query failed")
|
||||
}
|
||||
|
||||
lockedUntil := now.Add(lockTTL)
|
||||
for _, candidate := range candidates {
|
||||
patch := repository.Patch().
|
||||
Set(repository.Field("locked_until"), lockedUntil).
|
||||
Set(repository.Field("worker_id"), workerID)
|
||||
|
||||
conditional := repository.IDFilter(candidate.ID).And(
|
||||
repository.Query().In(repository.Field("status"), string(TaskStatusPending), string(TaskStatusRetry)),
|
||||
repository.Query().Comparison(repository.Field("next_attempt_at"), builder.Lte, now),
|
||||
lockFilter,
|
||||
)
|
||||
|
||||
updated, err := r.repo.PatchMany(ctx, conditional, patch)
|
||||
if err != nil {
|
||||
return nil, merrors.InternalWrap(err, "callbacks task lock update failed")
|
||||
}
|
||||
if updated == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
locked := &taskDoc{}
|
||||
if err := r.repo.Get(ctx, candidate.ID, locked); err != nil {
|
||||
if errors.Is(err, merrors.ErrNoData) {
|
||||
continue
|
||||
}
|
||||
return nil, merrors.InternalWrap(err, "callbacks task lock reload failed")
|
||||
}
|
||||
if strings.TrimSpace(locked.WorkerID) != workerID {
|
||||
continue
|
||||
}
|
||||
|
||||
return mapTaskDoc(locked), nil
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *taskStore) MarkDelivered(ctx context.Context, taskID bson.ObjectID, httpCode int, latency time.Duration, at time.Time) error {
|
||||
_ = latency
|
||||
if taskID == bson.NilObjectID {
|
||||
return merrors.InvalidArgument("task id is required", "task_id")
|
||||
}
|
||||
|
||||
patch := repository.Patch().
|
||||
Set(repository.Field("status"), TaskStatusDelivered).
|
||||
Set(repository.Field("last_http_code"), httpCode).
|
||||
Set(repository.Field("delivered_at"), time.Now()).
|
||||
Set(repository.Field("locked_until"), nil).
|
||||
Set(repository.Field("worker_id"), "").
|
||||
Set(repository.Field("last_error"), "")
|
||||
|
||||
if err := r.repo.Patch(ctx, taskID, patch); err != nil {
|
||||
return merrors.InternalWrap(err, "callbacks task mark delivered failed")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *taskStore) MarkRetry(ctx context.Context, taskID bson.ObjectID, attempt int, nextAttemptAt time.Time, lastError string, httpCode int, at time.Time) error {
|
||||
if taskID == bson.NilObjectID {
|
||||
return merrors.InvalidArgument("task id is required", "task_id")
|
||||
}
|
||||
|
||||
patch := repository.Patch().
|
||||
Set(repository.Field("status"), TaskStatusRetry).
|
||||
Set(repository.Field("attempt"), attempt).
|
||||
Set(repository.Field("next_attempt_at"), nextAttemptAt.UTC()).
|
||||
Set(repository.Field("last_error"), strings.TrimSpace(lastError)).
|
||||
Set(repository.Field("last_http_code"), httpCode).
|
||||
Set(repository.Field("locked_until"), nil).
|
||||
Set(repository.Field("worker_id"), "")
|
||||
|
||||
if err := r.repo.Patch(ctx, taskID, patch); err != nil {
|
||||
return merrors.InternalWrap(err, "callbacks task mark retry failed")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *taskStore) MarkFailed(ctx context.Context, taskID bson.ObjectID, attempt int, lastError string, httpCode int, at time.Time) error {
|
||||
if taskID == bson.NilObjectID {
|
||||
return merrors.InvalidArgument("task id is required", "task_id")
|
||||
}
|
||||
|
||||
patch := repository.Patch().
|
||||
Set(repository.Field("status"), TaskStatusFailed).
|
||||
Set(repository.Field("attempt"), attempt).
|
||||
Set(repository.Field("last_error"), strings.TrimSpace(lastError)).
|
||||
Set(repository.Field("last_http_code"), httpCode).
|
||||
Set(repository.Field("locked_until"), nil).
|
||||
Set(repository.Field("worker_id"), "")
|
||||
|
||||
if err := r.repo.Patch(ctx, taskID, patch); err != nil {
|
||||
return merrors.InternalWrap(err, "callbacks task mark failed failed")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func mapTaskDoc(doc *taskDoc) *Task {
|
||||
if doc == nil {
|
||||
return nil
|
||||
}
|
||||
return &Task{
|
||||
ID: doc.ID,
|
||||
EventID: doc.EventID,
|
||||
EndpointID: doc.EndpointID,
|
||||
EndpointURL: doc.EndpointURL,
|
||||
SigningMode: doc.SigningMode,
|
||||
SecretRef: doc.SecretRef,
|
||||
Headers: cloneHeaders(doc.Headers),
|
||||
Payload: append([]byte(nil), doc.Payload...),
|
||||
Attempt: doc.Attempt,
|
||||
MaxAttempts: doc.MaxAttempts,
|
||||
MinDelay: time.Duration(doc.MinDelayMS) * time.Millisecond,
|
||||
MaxDelay: time.Duration(doc.MaxDelayMS) * time.Millisecond,
|
||||
RequestTimeout: time.Duration(doc.RequestTimeoutMS) * time.Millisecond,
|
||||
Status: doc.Status,
|
||||
NextAttemptAt: doc.NextAttemptAt,
|
||||
}
|
||||
}
|
||||
|
||||
func cloneHeaders(in map[string]string) map[string]string {
|
||||
if len(in) == 0 {
|
||||
return map[string]string{}
|
||||
}
|
||||
out := make(map[string]string, len(in))
|
||||
for key, val := range in {
|
||||
out[key] = val
|
||||
}
|
||||
return out
|
||||
}
|
||||
Reference in New Issue
Block a user