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 }