github.com/wfusion/gofusion@v1.1.14/common/infra/asynq/asynq.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  	"crypto/tls"
    10  	"fmt"
    11  	"net"
    12  	"net/url"
    13  	"strconv"
    14  	"strings"
    15  	"time"
    16  
    17  	"github.com/redis/go-redis/v9"
    18  
    19  	"github.com/wfusion/gofusion/common/infra/asynq/pkg/base"
    20  )
    21  
    22  // Task represents a unit of work to be performed.
    23  type Task struct {
    24  	// typename indicates the type of task to be performed.
    25  	typename string
    26  
    27  	// payload holds data needed to perform the task.
    28  	payload []byte
    29  
    30  	// opts holds options for the task.
    31  	opts []Option
    32  
    33  	// w is the ResultWriter for the task.
    34  	w *ResultWriter
    35  }
    36  
    37  func (t *Task) Type() string    { return t.typename }
    38  func (t *Task) Payload() []byte { return t.payload }
    39  
    40  // ResultWriter returns a pointer to the ResultWriter associated with the task.
    41  //
    42  // Nil pointer is returned if called on a newly created task (i.e. task created by calling NewTask).
    43  // Only the tasks passed to Handler.ProcessTask have a valid ResultWriter pointer.
    44  func (t *Task) ResultWriter() *ResultWriter { return t.w }
    45  
    46  // NewTask returns a new Task given a type name and payload data.
    47  // Options can be passed to configure task processing behavior.
    48  func NewTask(typename string, payload []byte, opts ...Option) *Task {
    49  	return &Task{
    50  		typename: typename,
    51  		payload:  payload,
    52  		opts:     opts,
    53  	}
    54  }
    55  
    56  // newTask creates a task with the given typename, payload and ResultWriter.
    57  func newTask(typename string, payload []byte, w *ResultWriter) *Task {
    58  	return &Task{
    59  		typename: typename,
    60  		payload:  payload,
    61  		w:        w,
    62  	}
    63  }
    64  
    65  // A TaskInfo describes a task and its metadata.
    66  type TaskInfo struct {
    67  	// ID is the identifier of the task.
    68  	ID string
    69  
    70  	// Queue is the name of the queue in which the task belongs.
    71  	Queue string
    72  
    73  	// Type is the type name of the task.
    74  	Type string
    75  
    76  	// Payload is the payload data of the task.
    77  	Payload []byte
    78  
    79  	// State indicates the task state.
    80  	State TaskState
    81  
    82  	// MaxRetry is the maximum number of times the task can be retried.
    83  	MaxRetry int
    84  
    85  	// Retried is the number of times the task has retried so far.
    86  	Retried int
    87  
    88  	// LastErr is the error message from the last failure.
    89  	LastErr string
    90  
    91  	// LastFailedAt is the time time of the last failure if any.
    92  	// If the task has no failures, LastFailedAt is zero time (i.e. time.Time{}).
    93  	LastFailedAt time.Time
    94  
    95  	// Timeout is the duration the task can be processed by Handler before being retried,
    96  	// zero if not specified
    97  	Timeout time.Duration
    98  
    99  	// Deadline is the deadline for the task, zero value if not specified.
   100  	Deadline time.Time
   101  
   102  	// Group is the name of the group in which the task belongs.
   103  	//
   104  	// Tasks in the same queue can be grouped together by Group name and will be aggregated into one task
   105  	// by a Server processing the queue.
   106  	//
   107  	// Empty string (default) indicates task does not belong to any groups, and no aggregation will be applied to the task.
   108  	Group string
   109  
   110  	// NextProcessAt is the time the task is scheduled to be processed,
   111  	// zero if not applicable.
   112  	NextProcessAt time.Time
   113  
   114  	// IsOrphaned describes whether the task is left in active state with no worker processing it.
   115  	// An orphaned task indicates that the worker has crashed or experienced network failures and was not able to
   116  	// extend its lease on the task.
   117  	//
   118  	// This task will be recovered by running a server against the queue the task is in.
   119  	// This field is only applicable to tasks with TaskStateActive.
   120  	IsOrphaned bool
   121  
   122  	// Retention is duration of the retention period after the task is successfully processed.
   123  	Retention time.Duration
   124  
   125  	// CompletedAt is the time when the task is processed successfully.
   126  	// Zero value (i.e. time.Time{}) indicates no value.
   127  	CompletedAt time.Time
   128  
   129  	// Result holds the result data associated with the task.
   130  	// Use ResultWriter to write result data from the Handler.
   131  	Result []byte
   132  }
   133  
   134  // If t is non-zero, returns time converted from t as unix time in seconds.
   135  // If t is zero, returns zero value of time.Time.
   136  func fromUnixTimeOrZero(t int64) time.Time {
   137  	if t == 0 {
   138  		return time.Time{}
   139  	}
   140  	return time.Unix(t, 0)
   141  }
   142  
   143  func newTaskInfo(msg *base.TaskMessage, state base.TaskState, nextProcessAt time.Time, result []byte) *TaskInfo {
   144  	info := TaskInfo{
   145  		ID:            msg.ID,
   146  		Queue:         msg.Queue,
   147  		Type:          msg.Type,
   148  		Payload:       msg.Payload, // Do we need to make a copy?
   149  		MaxRetry:      msg.Retry,
   150  		Retried:       msg.Retried,
   151  		LastErr:       msg.ErrorMsg,
   152  		Group:         msg.GroupKey,
   153  		Timeout:       time.Duration(msg.Timeout) * time.Second,
   154  		Deadline:      fromUnixTimeOrZero(msg.Deadline),
   155  		Retention:     time.Duration(msg.Retention) * time.Second,
   156  		NextProcessAt: nextProcessAt,
   157  		LastFailedAt:  fromUnixTimeOrZero(msg.LastFailedAt),
   158  		CompletedAt:   fromUnixTimeOrZero(msg.CompletedAt),
   159  		Result:        result,
   160  	}
   161  
   162  	switch state {
   163  	case base.TaskStateActive:
   164  		info.State = TaskStateActive
   165  	case base.TaskStatePending:
   166  		info.State = TaskStatePending
   167  	case base.TaskStateScheduled:
   168  		info.State = TaskStateScheduled
   169  	case base.TaskStateRetry:
   170  		info.State = TaskStateRetry
   171  	case base.TaskStateArchived:
   172  		info.State = TaskStateArchived
   173  	case base.TaskStateCompleted:
   174  		info.State = TaskStateCompleted
   175  	case base.TaskStateAggregating:
   176  		info.State = TaskStateAggregating
   177  	default:
   178  		panic(fmt.Sprintf("internal error: unknown state: %d", state))
   179  	}
   180  	return &info
   181  }
   182  
   183  // TaskState denotes the state of a task.
   184  type TaskState int
   185  
   186  const (
   187  	// Indicates that the task is currently being processed by Handler.
   188  	TaskStateActive TaskState = iota + 1
   189  
   190  	// Indicates that the task is ready to be processed by Handler.
   191  	TaskStatePending
   192  
   193  	// Indicates that the task is scheduled to be processed some time in the future.
   194  	TaskStateScheduled
   195  
   196  	// Indicates that the task has previously failed and scheduled to be processed some time in the future.
   197  	TaskStateRetry
   198  
   199  	// Indicates that the task is archived and stored for inspection purposes.
   200  	TaskStateArchived
   201  
   202  	// Indicates that the task is processed successfully and retained until the retention TTL expires.
   203  	TaskStateCompleted
   204  
   205  	// Indicates that the task is waiting in a group to be aggregated into one task.
   206  	TaskStateAggregating
   207  )
   208  
   209  func (s TaskState) String() string {
   210  	switch s {
   211  	case TaskStateActive:
   212  		return "active"
   213  	case TaskStatePending:
   214  		return "pending"
   215  	case TaskStateScheduled:
   216  		return "scheduled"
   217  	case TaskStateRetry:
   218  		return "retry"
   219  	case TaskStateArchived:
   220  		return "archived"
   221  	case TaskStateCompleted:
   222  		return "completed"
   223  	case TaskStateAggregating:
   224  		return "aggregating"
   225  	}
   226  	panic("asynq: unknown task state")
   227  }
   228  
   229  // RedisConnOpt is a discriminated union of types that represent Redis connection configuration option.
   230  //
   231  // RedisConnOpt represents a sum of following types:
   232  //
   233  //   - RedisClientOpt
   234  //   - RedisFailoverClientOpt
   235  //   - RedisClusterClientOpt
   236  type RedisConnOpt interface {
   237  	// MakeRedisClient returns a new redis client instance.
   238  	// Return value is intentionally opaque to hide the implementation detail of redis client.
   239  	MakeRedisClient() any
   240  }
   241  
   242  // RedisClientOpt is used to create a redis client that connects
   243  // to a redis server directly.
   244  type RedisClientOpt struct {
   245  	// Network type to use, either tcp or unix.
   246  	// Default is tcp.
   247  	Network string
   248  
   249  	// Redis server address in "host:port" format.
   250  	Addr string
   251  
   252  	// Username to authenticate the current connection when Redis ACLs are used.
   253  	// See: https://redis.io/commands/auth.
   254  	Username string
   255  
   256  	// Password to authenticate the current connection.
   257  	// See: https://redis.io/commands/auth.
   258  	Password string
   259  
   260  	// Redis DB to select after connecting to a server.
   261  	// See: https://redis.io/commands/select.
   262  	DB int
   263  
   264  	// Dial timeout for establishing new connections.
   265  	// Default is 5 seconds.
   266  	DialTimeout time.Duration
   267  
   268  	// Timeout for socket reads.
   269  	// If timeout is reached, read commands will fail with a timeout error
   270  	// instead of blocking.
   271  	//
   272  	// Use value -1 for no timeout and 0 for default.
   273  	// Default is 3 seconds.
   274  	ReadTimeout time.Duration
   275  
   276  	// Timeout for socket writes.
   277  	// If timeout is reached, write commands will fail with a timeout error
   278  	// instead of blocking.
   279  	//
   280  	// Use value -1 for no timeout and 0 for default.
   281  	// Default is ReadTimout.
   282  	WriteTimeout time.Duration
   283  
   284  	// Maximum number of socket connections.
   285  	// Default is 10 connections per every CPU as reported by runtime.NumCPU.
   286  	PoolSize int
   287  
   288  	// TLS Config used to connect to a server.
   289  	// TLS will be negotiated only if this field is set.
   290  	TLSConfig *tls.Config
   291  }
   292  
   293  func (opt RedisClientOpt) MakeRedisClient() any {
   294  	return redis.NewClient(&redis.Options{
   295  		Network:      opt.Network,
   296  		Addr:         opt.Addr,
   297  		Username:     opt.Username,
   298  		Password:     opt.Password,
   299  		DB:           opt.DB,
   300  		DialTimeout:  opt.DialTimeout,
   301  		ReadTimeout:  opt.ReadTimeout,
   302  		WriteTimeout: opt.WriteTimeout,
   303  		PoolSize:     opt.PoolSize,
   304  		TLSConfig:    opt.TLSConfig,
   305  	})
   306  }
   307  
   308  // RedisFailoverClientOpt is used to creates a redis client that talks
   309  // to redis sentinels for service discovery and has an automatic failover
   310  // capability.
   311  type RedisFailoverClientOpt struct {
   312  	// Redis master name that monitored by sentinels.
   313  	MasterName string
   314  
   315  	// Addresses of sentinels in "host:port" format.
   316  	// Use at least three sentinels to avoid problems described in
   317  	// https://redis.io/topics/sentinel.
   318  	SentinelAddrs []string
   319  
   320  	// Redis sentinel password.
   321  	SentinelPassword string
   322  
   323  	// Username to authenticate the current connection when Redis ACLs are used.
   324  	// See: https://redis.io/commands/auth.
   325  	Username string
   326  
   327  	// Password to authenticate the current connection.
   328  	// See: https://redis.io/commands/auth.
   329  	Password string
   330  
   331  	// Redis DB to select after connecting to a server.
   332  	// See: https://redis.io/commands/select.
   333  	DB int
   334  
   335  	// Dial timeout for establishing new connections.
   336  	// Default is 5 seconds.
   337  	DialTimeout time.Duration
   338  
   339  	// Timeout for socket reads.
   340  	// If timeout is reached, read commands will fail with a timeout error
   341  	// instead of blocking.
   342  	//
   343  	// Use value -1 for no timeout and 0 for default.
   344  	// Default is 3 seconds.
   345  	ReadTimeout time.Duration
   346  
   347  	// Timeout for socket writes.
   348  	// If timeout is reached, write commands will fail with a timeout error
   349  	// instead of blocking.
   350  	//
   351  	// Use value -1 for no timeout and 0 for default.
   352  	// Default is ReadTimeout
   353  	WriteTimeout time.Duration
   354  
   355  	// Maximum number of socket connections.
   356  	// Default is 10 connections per every CPU as reported by runtime.NumCPU.
   357  	PoolSize int
   358  
   359  	// TLS Config used to connect to a server.
   360  	// TLS will be negotiated only if this field is set.
   361  	TLSConfig *tls.Config
   362  }
   363  
   364  func (opt RedisFailoverClientOpt) MakeRedisClient() any {
   365  	return redis.NewFailoverClient(&redis.FailoverOptions{
   366  		MasterName:       opt.MasterName,
   367  		SentinelAddrs:    opt.SentinelAddrs,
   368  		SentinelPassword: opt.SentinelPassword,
   369  		Username:         opt.Username,
   370  		Password:         opt.Password,
   371  		DB:               opt.DB,
   372  		DialTimeout:      opt.DialTimeout,
   373  		ReadTimeout:      opt.ReadTimeout,
   374  		WriteTimeout:     opt.WriteTimeout,
   375  		PoolSize:         opt.PoolSize,
   376  		TLSConfig:        opt.TLSConfig,
   377  	})
   378  }
   379  
   380  // RedisClusterClientOpt is used to creates a redis client that connects to
   381  // redis cluster.
   382  type RedisClusterClientOpt struct {
   383  	// A seed list of host:port addresses of cluster nodes.
   384  	Addrs []string
   385  
   386  	// The maximum number of retries before giving up.
   387  	// Command is retried on network errors and MOVED/ASK redirects.
   388  	// Default is 8 retries.
   389  	MaxRedirects int
   390  
   391  	// Username to authenticate the current connection when Redis ACLs are used.
   392  	// See: https://redis.io/commands/auth.
   393  	Username string
   394  
   395  	// Password to authenticate the current connection.
   396  	// See: https://redis.io/commands/auth.
   397  	Password string
   398  
   399  	// Dial timeout for establishing new connections.
   400  	// Default is 5 seconds.
   401  	DialTimeout time.Duration
   402  
   403  	// Timeout for socket reads.
   404  	// If timeout is reached, read commands will fail with a timeout error
   405  	// instead of blocking.
   406  	//
   407  	// Use value -1 for no timeout and 0 for default.
   408  	// Default is 3 seconds.
   409  	ReadTimeout time.Duration
   410  
   411  	// Timeout for socket writes.
   412  	// If timeout is reached, write commands will fail with a timeout error
   413  	// instead of blocking.
   414  	//
   415  	// Use value -1 for no timeout and 0 for default.
   416  	// Default is ReadTimeout.
   417  	WriteTimeout time.Duration
   418  
   419  	// TLS Config used to connect to a server.
   420  	// TLS will be negotiated only if this field is set.
   421  	TLSConfig *tls.Config
   422  }
   423  
   424  func (opt RedisClusterClientOpt) MakeRedisClient() any {
   425  	return redis.NewClusterClient(&redis.ClusterOptions{
   426  		Addrs:        opt.Addrs,
   427  		MaxRedirects: opt.MaxRedirects,
   428  		Username:     opt.Username,
   429  		Password:     opt.Password,
   430  		DialTimeout:  opt.DialTimeout,
   431  		ReadTimeout:  opt.ReadTimeout,
   432  		WriteTimeout: opt.WriteTimeout,
   433  		TLSConfig:    opt.TLSConfig,
   434  	})
   435  }
   436  
   437  // ParseRedisURI parses redis uri string and returns RedisConnOpt if uri is valid.
   438  // It returns a non-nil error if uri cannot be parsed.
   439  //
   440  // Three URI schemes are supported, which are redis:, rediss:, redis-socket:, and redis-sentinel:.
   441  // Supported formats are:
   442  //     redis://[:password@]host[:port][/dbnumber]
   443  //     rediss://[:password@]host[:port][/dbnumber]
   444  //     redis-socket://[:password@]path[?db=dbnumber]
   445  //     redis-sentinel://[:password@]host1[:port][,host2:[:port]][,hostN:[:port]][?master=masterName]
   446  func ParseRedisURI(uri string) (RedisConnOpt, error) {
   447  	u, err := url.Parse(uri)
   448  	if err != nil {
   449  		return nil, fmt.Errorf("asynq: could not parse redis uri: %v", err)
   450  	}
   451  	switch u.Scheme {
   452  	case "redis", "rediss":
   453  		return parseRedisURI(u)
   454  	case "redis-socket":
   455  		return parseRedisSocketURI(u)
   456  	case "redis-sentinel":
   457  		return parseRedisSentinelURI(u)
   458  	default:
   459  		return nil, fmt.Errorf("asynq: unsupported uri scheme: %q", u.Scheme)
   460  	}
   461  }
   462  
   463  func parseRedisURI(u *url.URL) (RedisConnOpt, error) {
   464  	var db int
   465  	var err error
   466  	var redisConnOpt RedisClientOpt
   467  
   468  	if len(u.Path) > 0 {
   469  		xs := strings.Split(strings.Trim(u.Path, "/"), "/")
   470  		db, err = strconv.Atoi(xs[0])
   471  		if err != nil {
   472  			return nil, fmt.Errorf("asynq: could not parse redis uri: database number should be the first segment of the path")
   473  		}
   474  	}
   475  	var password string
   476  	if v, ok := u.User.Password(); ok {
   477  		password = v
   478  	}
   479  
   480  	if u.Scheme == "rediss" {
   481  		h, _, err := net.SplitHostPort(u.Host)
   482  		if err != nil {
   483  			h = u.Host
   484  		}
   485  		redisConnOpt.TLSConfig = &tls.Config{ServerName: h}
   486  	}
   487  
   488  	redisConnOpt.Addr = u.Host
   489  	redisConnOpt.Password = password
   490  	redisConnOpt.DB = db
   491  
   492  	return redisConnOpt, nil
   493  }
   494  
   495  func parseRedisSocketURI(u *url.URL) (RedisConnOpt, error) {
   496  	const errPrefix = "asynq: could not parse redis socket uri"
   497  	if len(u.Path) == 0 {
   498  		return nil, fmt.Errorf("%s: path does not exist", errPrefix)
   499  	}
   500  	q := u.Query()
   501  	var db int
   502  	var err error
   503  	if n := q.Get("db"); n != "" {
   504  		db, err = strconv.Atoi(n)
   505  		if err != nil {
   506  			return nil, fmt.Errorf("%s: query param `db` should be a number", errPrefix)
   507  		}
   508  	}
   509  	var password string
   510  	if v, ok := u.User.Password(); ok {
   511  		password = v
   512  	}
   513  	return RedisClientOpt{Network: "unix", Addr: u.Path, DB: db, Password: password}, nil
   514  }
   515  
   516  func parseRedisSentinelURI(u *url.URL) (RedisConnOpt, error) {
   517  	addrs := strings.Split(u.Host, ",")
   518  	master := u.Query().Get("master")
   519  	var password string
   520  	if v, ok := u.User.Password(); ok {
   521  		password = v
   522  	}
   523  	return RedisFailoverClientOpt{MasterName: master, SentinelAddrs: addrs, SentinelPassword: password}, nil
   524  }
   525  
   526  // ResultWriter is a client interface to write result data for a task.
   527  // It writes the data to the redis instance the server is connected to.
   528  type ResultWriter struct {
   529  	id     string // task ID this writer is responsible for
   530  	qname  string // queue name the task belongs to
   531  	broker base.Broker
   532  	ctx    context.Context // context associated with the task
   533  }
   534  
   535  // Write writes the given data as a result of the task the ResultWriter is associated with.
   536  func (w *ResultWriter) Write(data []byte) (n int, err error) {
   537  	select {
   538  	case <-w.ctx.Done():
   539  		return 0, fmt.Errorf("failed to result task result: %v", w.ctx.Err())
   540  	default:
   541  	}
   542  	return w.broker.WriteResult(w.qname, w.id, data)
   543  }
   544  
   545  // TaskID returns the ID of the task the ResultWriter is associated with.
   546  func (w *ResultWriter) TaskID() string {
   547  	return w.id
   548  }