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 }