github.com/wfusion/gofusion@v1.1.14/common/infra/asynq/client.go (about)

     1  // Copyright 2020 Kentaro Hibino. All rights reserved.
     2  // Use of this source code is governed by a MIT license
     3  // that can be found in the LICENSE file.
     4  
     5  package asynq
     6  
     7  import (
     8  	"context"
     9  	"fmt"
    10  	"strings"
    11  	"time"
    12  
    13  	"github.com/google/uuid"
    14  	"github.com/redis/go-redis/v9"
    15  	"github.com/wfusion/gofusion/common/infra/asynq/pkg/base"
    16  	"github.com/wfusion/gofusion/common/infra/asynq/pkg/errors"
    17  	"github.com/wfusion/gofusion/common/infra/asynq/pkg/rdb"
    18  )
    19  
    20  // A Client is responsible for scheduling tasks.
    21  //
    22  // A Client is used to register tasks that should be processed
    23  // immediately or some time in the future.
    24  //
    25  // Clients are safe for concurrent use by multiple goroutines.
    26  type Client struct {
    27  	broker base.Broker
    28  }
    29  
    30  // NewClient returns a new Client instance given a redis connection option.
    31  func NewClient(r RedisConnOpt) *Client {
    32  	c, ok := r.MakeRedisClient().(redis.UniversalClient)
    33  	if !ok {
    34  		panic(fmt.Sprintf("asynq: unsupported RedisConnOpt type %T", r))
    35  	}
    36  	return &Client{broker: rdb.NewRDB(c)}
    37  }
    38  
    39  type OptionType int
    40  
    41  const (
    42  	MaxRetryOpt OptionType = iota
    43  	QueueOpt
    44  	TimeoutOpt
    45  	DeadlineOpt
    46  	UniqueOpt
    47  	ProcessAtOpt
    48  	ProcessInOpt
    49  	TaskIDOpt
    50  	RetentionOpt
    51  	GroupOpt
    52  	UniqueKeyOpt
    53  )
    54  
    55  // Option specifies the task processing behavior.
    56  type Option interface {
    57  	// String returns a string representation of the option.
    58  	String() string
    59  
    60  	// Type describes the type of the option.
    61  	Type() OptionType
    62  
    63  	// Value returns a value used to create this option.
    64  	Value() any
    65  }
    66  
    67  // Internal option representations.
    68  type (
    69  	retryOption     int
    70  	queueOption     string
    71  	taskIDOption    string
    72  	timeoutOption   time.Duration
    73  	deadlineOption  time.Time
    74  	uniqueOption    time.Duration
    75  	uniqueKeyOption string
    76  	processAtOption time.Time
    77  	processInOption time.Duration
    78  	retentionOption time.Duration
    79  	groupOption     string
    80  )
    81  
    82  // MaxRetry returns an option to specify the max number of times
    83  // the task will be retried.
    84  //
    85  // Negative retry count is treated as zero retry.
    86  func MaxRetry(n int) Option {
    87  	if n < 0 {
    88  		n = 0
    89  	}
    90  	return retryOption(n)
    91  }
    92  
    93  func (n retryOption) String() string   { return fmt.Sprintf("MaxRetry(%d)", int(n)) }
    94  func (n retryOption) Type() OptionType { return MaxRetryOpt }
    95  func (n retryOption) Value() any       { return int(n) }
    96  
    97  // Queue returns an option to specify the queue to enqueue the task into.
    98  func Queue(name string) Option {
    99  	return queueOption(name)
   100  }
   101  
   102  func (name queueOption) String() string   { return fmt.Sprintf("Queue(%q)", string(name)) }
   103  func (name queueOption) Type() OptionType { return QueueOpt }
   104  func (name queueOption) Value() any       { return string(name) }
   105  
   106  // TaskID returns an option to specify the task ID.
   107  func TaskID(id string) Option {
   108  	return taskIDOption(id)
   109  }
   110  
   111  func (id taskIDOption) String() string   { return fmt.Sprintf("TaskID(%q)", string(id)) }
   112  func (id taskIDOption) Type() OptionType { return TaskIDOpt }
   113  func (id taskIDOption) Value() any       { return string(id) }
   114  
   115  // Timeout returns an option to specify how long a task may run.
   116  // If the timeout elapses before the Handler returns, then the task
   117  // will be retried.
   118  //
   119  // Zero duration means no limit.
   120  //
   121  // If there's a conflicting Deadline option, whichever comes earliest
   122  // will be used.
   123  func Timeout(d time.Duration) Option {
   124  	return timeoutOption(d)
   125  }
   126  
   127  func (d timeoutOption) String() string   { return fmt.Sprintf("Timeout(%v)", time.Duration(d)) }
   128  func (d timeoutOption) Type() OptionType { return TimeoutOpt }
   129  func (d timeoutOption) Value() any       { return time.Duration(d) }
   130  
   131  // Deadline returns an option to specify the deadline for the given task.
   132  // If it reaches the deadline before the Handler returns, then the task
   133  // will be retried.
   134  //
   135  // If there's a conflicting Timeout option, whichever comes earliest
   136  // will be used.
   137  func Deadline(t time.Time) Option {
   138  	return deadlineOption(t)
   139  }
   140  
   141  func (t deadlineOption) String() string {
   142  	return fmt.Sprintf("Deadline(%v)", time.Time(t).Format(time.UnixDate))
   143  }
   144  func (t deadlineOption) Type() OptionType { return DeadlineOpt }
   145  func (t deadlineOption) Value() any       { return time.Time(t) }
   146  
   147  // Unique returns an option to enqueue a task only if the given task is unique.
   148  // Task enqueued with this option is guaranteed to be unique within the given ttl.
   149  // Once the task gets processed successfully or once the TTL has expired,
   150  // another task with the same uniqueness may be enqueued.
   151  // ErrDuplicateTask error is returned when enqueueing a duplicate task.
   152  // TTL duration must be greater than or equal to 1 second.
   153  //
   154  // Uniqueness of a task is based on the following properties:
   155  //     - Task Type
   156  //     - Task Payload
   157  //     - Queue Name
   158  func Unique(ttl time.Duration) Option {
   159  	return uniqueOption(ttl)
   160  }
   161  
   162  func (ttl uniqueOption) String() string   { return fmt.Sprintf("Unique(%v)", time.Duration(ttl)) }
   163  func (ttl uniqueOption) Type() OptionType { return UniqueOpt }
   164  func (ttl uniqueOption) Value() any       { return time.Duration(ttl) }
   165  
   166  func UniqueKey(k string) Option {
   167  	return uniqueKeyOption(k)
   168  }
   169  
   170  func (u uniqueKeyOption) String() string   { return fmt.Sprintf("UniqueKey(%s)", u) }
   171  func (u uniqueKeyOption) Type() OptionType { return UniqueKeyOpt }
   172  func (u uniqueKeyOption) Value() any       { return u }
   173  
   174  // ProcessAt returns an option to specify when to process the given task.
   175  //
   176  // If there's a conflicting ProcessIn option, the last option passed to Enqueue overrides the others.
   177  func ProcessAt(t time.Time) Option {
   178  	return processAtOption(t)
   179  }
   180  
   181  func (t processAtOption) String() string {
   182  	return fmt.Sprintf("ProcessAt(%v)", time.Time(t).Format(time.UnixDate))
   183  }
   184  func (t processAtOption) Type() OptionType { return ProcessAtOpt }
   185  func (t processAtOption) Value() any       { return time.Time(t) }
   186  
   187  // ProcessIn returns an option to specify when to process the given task relative to the current time.
   188  //
   189  // If there's a conflicting ProcessAt option, the last option passed to Enqueue overrides the others.
   190  func ProcessIn(d time.Duration) Option {
   191  	return processInOption(d)
   192  }
   193  
   194  func (d processInOption) String() string   { return fmt.Sprintf("ProcessIn(%v)", time.Duration(d)) }
   195  func (d processInOption) Type() OptionType { return ProcessInOpt }
   196  func (d processInOption) Value() any       { return time.Duration(d) }
   197  
   198  // Retention returns an option to specify the duration of retention period for the task.
   199  // If this option is provided, the task will be stored as a completed task after successful processing.
   200  // A completed task will be deleted after the specified duration elapses.
   201  func Retention(d time.Duration) Option {
   202  	return retentionOption(d)
   203  }
   204  
   205  func (ttl retentionOption) String() string   { return fmt.Sprintf("Retention(%v)", time.Duration(ttl)) }
   206  func (ttl retentionOption) Type() OptionType { return RetentionOpt }
   207  func (ttl retentionOption) Value() any       { return time.Duration(ttl) }
   208  
   209  // Group returns an option to specify the group used for the task.
   210  // Tasks in a given queue with the same group will be aggregated into one task before passed to Handler.
   211  func Group(name string) Option {
   212  	return groupOption(name)
   213  }
   214  
   215  func (name groupOption) String() string   { return fmt.Sprintf("Group(%q)", string(name)) }
   216  func (name groupOption) Type() OptionType { return GroupOpt }
   217  func (name groupOption) Value() any       { return string(name) }
   218  
   219  // ErrDuplicateTask indicates that the given task could not be enqueued since it's a duplicate of another task.
   220  //
   221  // ErrDuplicateTask error only applies to tasks enqueued with a Unique option.
   222  var ErrDuplicateTask = errors.New("task already exists")
   223  
   224  // ErrTaskIDConflict indicates that the given task could not be enqueued since its task ID already exists.
   225  //
   226  // ErrTaskIDConflict error only applies to tasks enqueued with a TaskID option.
   227  var ErrTaskIDConflict = errors.New("task ID conflicts with another task")
   228  
   229  type option struct {
   230  	retry     int
   231  	queue     string
   232  	taskID    string
   233  	timeout   time.Duration
   234  	deadline  time.Time
   235  	uniqueTTL time.Duration
   236  	uniqueKey string
   237  	processAt time.Time
   238  	retention time.Duration
   239  	group     string
   240  }
   241  
   242  // composeOptions merges user provided options into the default options
   243  // and returns the composed option.
   244  // It also validates the user provided options and returns an error if any of
   245  // the user provided options fail the validations.
   246  func composeOptions(opts ...Option) (option, error) {
   247  	res := option{
   248  		retry:     defaultMaxRetry,
   249  		queue:     base.DefaultQueueName,
   250  		taskID:    uuid.NewString(),
   251  		timeout:   0, // do not set to defaultTimeout here
   252  		deadline:  time.Time{},
   253  		processAt: time.Now(),
   254  	}
   255  	for _, opt := range opts {
   256  		switch opt := opt.(type) {
   257  		case retryOption:
   258  			res.retry = int(opt)
   259  		case queueOption:
   260  			qname := string(opt)
   261  			if err := base.ValidateQueueName(qname); err != nil {
   262  				return option{}, err
   263  			}
   264  			res.queue = qname
   265  		case taskIDOption:
   266  			id := string(opt)
   267  			if isBlank(id) {
   268  				return option{}, errors.New("task ID cannot be empty")
   269  			}
   270  			res.taskID = id
   271  		case timeoutOption:
   272  			res.timeout = time.Duration(opt)
   273  		case deadlineOption:
   274  			res.deadline = time.Time(opt)
   275  		case uniqueOption:
   276  			ttl := time.Duration(opt)
   277  			if ttl < 1*time.Second {
   278  				return option{}, errors.New("Unique TTL cannot be less than 1s")
   279  			}
   280  			res.uniqueTTL = ttl
   281  		case processAtOption:
   282  			res.processAt = time.Time(opt)
   283  		case processInOption:
   284  			res.processAt = time.Now().Add(time.Duration(opt))
   285  		case retentionOption:
   286  			res.retention = time.Duration(opt)
   287  		case groupOption:
   288  			key := string(opt)
   289  			if isBlank(key) {
   290  				return option{}, errors.New("group key cannot be empty")
   291  			}
   292  			res.group = key
   293  		default:
   294  			// ignore unexpected option
   295  		}
   296  	}
   297  	return res, nil
   298  }
   299  
   300  // isBlank returns true if the given s is empty or consist of all whitespaces.
   301  func isBlank(s string) bool {
   302  	return strings.TrimSpace(s) == ""
   303  }
   304  
   305  const (
   306  	// Default max retry count used if nothing is specified.
   307  	defaultMaxRetry = 25
   308  
   309  	// Default timeout used if both timeout and deadline are not specified.
   310  	defaultTimeout = 30 * time.Minute
   311  )
   312  
   313  // Value zero indicates no timeout and no deadline.
   314  var (
   315  	noTimeout  time.Duration = 0
   316  	noDeadline               = time.Unix(0, 0)
   317  )
   318  
   319  // Close closes the connection with redis.
   320  func (c *Client) Close() error {
   321  	return c.broker.Close()
   322  }
   323  
   324  // Enqueue enqueues the given task to a queue.
   325  //
   326  // Enqueue returns TaskInfo and nil error if the task is enqueued successfully, otherwise returns a non-nil error.
   327  //
   328  // The argument opts specifies the behavior of task processing.
   329  // If there are conflicting Option values the last one overrides others.
   330  // Any options provided to NewTask can be overridden by options passed to Enqueue.
   331  // By default, max retry is set to 25 and timeout is set to 30 minutes.
   332  //
   333  // If no ProcessAt or ProcessIn options are provided, the task will be pending immediately.
   334  //
   335  // Enqueue uses context.Background internally; to specify the context, use EnqueueContext.
   336  func (c *Client) Enqueue(task *Task, opts ...Option) (*TaskInfo, error) {
   337  	return c.EnqueueContext(context.Background(), task, opts...)
   338  }
   339  
   340  // EnqueueContext enqueues the given task to a queue.
   341  //
   342  // EnqueueContext returns TaskInfo and nil error if the task is enqueued successfully,
   343  // otherwise returns a non-nil error.
   344  //
   345  // The argument opts specifies the behavior of task processing.
   346  // If there are conflicting Option values the last one overrides others.
   347  // Any options provided to NewTask can be overridden by options passed to Enqueue.
   348  // By default, max retry is set to 25 and timeout is set to 30 minutes.
   349  //
   350  // If no ProcessAt or ProcessIn options are provided, the task will be pending immediately.
   351  //
   352  // The first argument context applies to the enqueue operation. To specify task timeout and deadline,
   353  // use Timeout and Deadline option instead.
   354  func (c *Client) EnqueueContext(ctx context.Context, task *Task, opts ...Option) (info *TaskInfo, err error) {
   355  	if task == nil {
   356  		return nil, fmt.Errorf("task cannot be nil")
   357  	}
   358  	if strings.TrimSpace(task.Type()) == "" {
   359  		return nil, fmt.Errorf("task typename cannot be empty")
   360  	}
   361  	// merge task options with the options provided at enqueue time.
   362  	opts = append(task.opts, opts...)
   363  	opt, err := composeOptions(opts...)
   364  	if err != nil {
   365  		return nil, err
   366  	}
   367  	deadline := noDeadline
   368  	if !opt.deadline.IsZero() {
   369  		deadline = opt.deadline
   370  	}
   371  	timeout := noTimeout
   372  	if opt.timeout != 0 {
   373  		timeout = opt.timeout
   374  	}
   375  	if deadline.Equal(noDeadline) && timeout == noTimeout {
   376  		// If neither deadline nor timeout are set, use default timeout.
   377  		timeout = defaultTimeout
   378  	}
   379  	var uniqueKey string
   380  	if opt.uniqueTTL > 0 {
   381  		uniqueKey = opt.uniqueKey
   382  		if uniqueKey == "" {
   383  			uniqueKey = base.UniqueKey(opt.queue, task.Type(), task.Payload())
   384  		}
   385  	}
   386  	msg := &base.TaskMessage{
   387  		ID:        opt.taskID,
   388  		Type:      task.Type(),
   389  		Payload:   task.Payload(),
   390  		Queue:     opt.queue,
   391  		Retry:     opt.retry,
   392  		Deadline:  deadline.Unix(),
   393  		Timeout:   int64(timeout.Seconds()),
   394  		UniqueKey: uniqueKey,
   395  		GroupKey:  opt.group,
   396  		Retention: int64(opt.retention.Seconds()),
   397  	}
   398  	now := time.Now()
   399  	var state base.TaskState
   400  	if opt.processAt.After(now) {
   401  		err = c.schedule(ctx, msg, opt.processAt, opt.uniqueTTL)
   402  		state = base.TaskStateScheduled
   403  	} else if opt.group != "" {
   404  		// Use zero value for processAt since we don't know when the task will be aggregated and processed.
   405  		opt.processAt = time.Time{}
   406  		err = c.addToGroup(ctx, msg, uniqueKey, opt.group, opt.uniqueTTL)
   407  		state = base.TaskStateAggregating
   408  	} else {
   409  		opt.processAt = now
   410  		err = c.enqueue(ctx, msg, opt.uniqueTTL)
   411  		state = base.TaskStatePending
   412  	}
   413  	info = newTaskInfo(msg, state, opt.processAt, nil)
   414  	switch {
   415  	case errors.Is(err, errors.ErrDuplicateTask):
   416  		return info, fmt.Errorf("%w", ErrDuplicateTask)
   417  	case errors.Is(err, errors.ErrTaskIdConflict):
   418  		return info, fmt.Errorf("%w", ErrTaskIDConflict)
   419  	case err != nil:
   420  		return info, err
   421  	}
   422  
   423  	return
   424  }
   425  
   426  func (c *Client) enqueue(ctx context.Context, msg *base.TaskMessage, uniqueTTL time.Duration) error {
   427  	if uniqueTTL > 0 {
   428  		return c.broker.EnqueueUnique(ctx, msg, uniqueTTL)
   429  	}
   430  	return c.broker.Enqueue(ctx, msg)
   431  }
   432  
   433  func (c *Client) schedule(ctx context.Context, msg *base.TaskMessage, t time.Time, uniqueTTL time.Duration) error {
   434  	if uniqueTTL > 0 {
   435  		ttl := time.Until(t.Add(uniqueTTL))
   436  		return c.broker.ScheduleUnique(ctx, msg, t, ttl)
   437  	}
   438  	return c.broker.Schedule(ctx, msg, t)
   439  }
   440  
   441  func (c *Client) addToGroup(ctx context.Context, msg *base.TaskMessage,
   442  	uniqueKey, group string, uniqueTTL time.Duration) error {
   443  	if uniqueTTL > 0 {
   444  		return c.broker.AddToGroupUnique(ctx, msg, uniqueKey, group, uniqueTTL)
   445  	}
   446  	return c.broker.AddToGroup(ctx, msg, group)
   447  }