mirror of
https://github.com/hibiken/asynq.git
synced 2025-10-20 09:16:12 +08:00
Compare commits
26 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
69ad583278 | ||
|
23f46dde52 | ||
|
39188fe930 | ||
|
4492ed9255 | ||
|
4e3e053989 | ||
|
aef0775c05 | ||
|
de146993d2 | ||
|
60cbf8dc5a | ||
|
fb38086590 | ||
|
cfcd19a222 | ||
|
24ee4b9693 | ||
|
7849b395bd | ||
|
fa3082e5bb | ||
|
d13f7e900f | ||
|
b63476ddc8 | ||
|
210b026b01 | ||
|
556b2103fe | ||
|
0289bc7a10 | ||
|
ae942c93e5 | ||
|
0faf97f146 | ||
|
711bfa371f | ||
|
73d62844e6 | ||
|
00b82904c6 | ||
|
a866369866 | ||
|
26b78136ba | ||
|
44aad7f037 |
22
CHANGELOG.md
22
CHANGELOG.md
@@ -7,6 +7,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [0.9.1] - 2020-05-29
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- `GetTaskID`, `GetRetryCount`, and `GetMaxRetry` functions were added to extract task metadata from context.
|
||||||
|
|
||||||
|
## [0.9.0] - 2020-05-16
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- `Logger` interface has changed. Please see the godoc for the new interface.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- `LogLevel` type is added. Server's log level can be specified through `LogLevel` field in `Config`.
|
||||||
|
|
||||||
|
## [0.8.3] - 2020-05-08
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- `Close` method is added to `Client`.
|
||||||
|
|
||||||
## [0.8.2] - 2020-05-03
|
## [0.8.2] - 2020-05-03
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
36
README.md
36
README.md
@@ -68,9 +68,10 @@ const (
|
|||||||
ImageProcessing = "image:process"
|
ImageProcessing = "image:process"
|
||||||
)
|
)
|
||||||
|
|
||||||
//--------------------------------------------
|
//----------------------------------------------
|
||||||
// Write function NewXXXTask to create a task.
|
// Write a function NewXXXTask to create a task.
|
||||||
//--------------------------------------------
|
// A task consists of a type and a payload.
|
||||||
|
//----------------------------------------------
|
||||||
|
|
||||||
func NewEmailDeliveryTask(userID int, tmplID string) *asynq.Task {
|
func NewEmailDeliveryTask(userID int, tmplID string) *asynq.Task {
|
||||||
payload := map[string]interface{}{"user_id": userID, "template_id": tmplID}
|
payload := map[string]interface{}{"user_id": userID, "template_id": tmplID}
|
||||||
@@ -82,13 +83,13 @@ func NewImageProcessingTask(src, dst string) *asynq.Task {
|
|||||||
return asynq.NewTask(ImageProcessing, payload)
|
return asynq.NewTask(ImageProcessing, payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
//-------------------------------------------------------------
|
//---------------------------------------------------------------
|
||||||
// Write function HandleXXXTask to handle the given task.
|
// Write a function HandleXXXTask to handle the input task.
|
||||||
// NOTE: It satisfies the asynq.HandlerFunc interface.
|
// Note that it satisfies the asynq.HandlerFunc interface.
|
||||||
//
|
//
|
||||||
// Handler doesn't need to be a function. You can define a type
|
// Handler doesn't need to be a function. You can define a type
|
||||||
// that satisfies asynq.Handler interface. See example below.
|
// that satisfies asynq.Handler interface. See examples below.
|
||||||
//-------------------------------------------------------------
|
//---------------------------------------------------------------
|
||||||
|
|
||||||
func HandleEmailDeliveryTask(ctx context.Context, t *asynq.Task) error {
|
func HandleEmailDeliveryTask(ctx context.Context, t *asynq.Task) error {
|
||||||
userID, err := t.Payload.GetInt("user_id")
|
userID, err := t.Payload.GetInt("user_id")
|
||||||
@@ -104,11 +105,11 @@ func HandleEmailDeliveryTask(ctx context.Context, t *asynq.Task) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ImageProcessor implements asynq.Handler interface.
|
||||||
type ImageProcesser struct {
|
type ImageProcesser struct {
|
||||||
// ... fields for struct
|
// ... fields for struct
|
||||||
}
|
}
|
||||||
|
|
||||||
// ImageProcessor implements asynq.Handler.
|
|
||||||
func (p *ImageProcessor) ProcessTask(ctx context.Context, t *asynq.Task) error {
|
func (p *ImageProcessor) ProcessTask(ctx context.Context, t *asynq.Task) error {
|
||||||
src, err := t.Payload.GetString("src")
|
src, err := t.Payload.GetString("src")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -147,11 +148,12 @@ const redisAddr = "127.0.0.1:6379"
|
|||||||
func main() {
|
func main() {
|
||||||
r := asynq.RedisClientOpt{Addr: redisAddr}
|
r := asynq.RedisClientOpt{Addr: redisAddr}
|
||||||
c := asynq.NewClient(r)
|
c := asynq.NewClient(r)
|
||||||
|
defer c.Close()
|
||||||
|
|
||||||
// ----------------------------------------------------
|
// ------------------------------------------------------
|
||||||
// Example 1: Enqueue task to be processed immediately.
|
// Example 1: Enqueue task to be processed immediately.
|
||||||
// Use (*Client).Enqueue method.
|
// Use (*Client).Enqueue method.
|
||||||
// ----------------------------------------------------
|
// ------------------------------------------------------
|
||||||
|
|
||||||
t := tasks.NewEmailDeliveryTask(42, "some:template:id")
|
t := tasks.NewEmailDeliveryTask(42, "some:template:id")
|
||||||
err := c.Enqueue(t)
|
err := c.Enqueue(t)
|
||||||
@@ -160,10 +162,10 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// ----------------------------------------------------------
|
// ------------------------------------------------------------
|
||||||
// Example 2: Schedule task to be processed in the future.
|
// Example 2: Schedule task to be processed in the future.
|
||||||
// Use (*Client).EnqueueIn or (*Client).EnqueueAt.
|
// Use (*Client).EnqueueIn or (*Client).EnqueueAt.
|
||||||
// ----------------------------------------------------------
|
// ------------------------------------------------------------
|
||||||
|
|
||||||
t = tasks.NewEmailDeliveryTask(42, "other:template:id")
|
t = tasks.NewEmailDeliveryTask(42, "other:template:id")
|
||||||
err = c.EnqueueIn(24*time.Hour, t)
|
err = c.EnqueueIn(24*time.Hour, t)
|
||||||
@@ -172,10 +174,10 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// --------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
// Example 3: Set options to tune task processing behavior.
|
// Example 3: Set options to tune task processing behavior.
|
||||||
// Options include MaxRetry, Queue, Timeout, Deadline, Unique etc.
|
// Options include MaxRetry, Queue, Timeout, Deadline, Unique etc.
|
||||||
// --------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
c.SetDefaultOptions(tasks.ImageProcessing, asynq.MaxRetry(10), asynq.Timeout(time.Minute))
|
c.SetDefaultOptions(tasks.ImageProcessing, asynq.MaxRetry(10), asynq.Timeout(time.Minute))
|
||||||
|
|
||||||
@@ -185,10 +187,10 @@ func main() {
|
|||||||
log.Fatal("could not enqueue task: %v", err)
|
log.Fatal("could not enqueue task: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// --------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Example 4: Pass options to tune task processing behavior at enqueue time.
|
// Example 4: Pass options to tune task processing behavior at enqueue time.
|
||||||
// Options passed at enqueue time override default ones, if any.
|
// Options passed at enqueue time override default ones, if any.
|
||||||
// --------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
t = tasks.NewImageProcessingTask("some/blobstore/url", "other/blobstore/url")
|
t = tasks.NewImageProcessingTask("some/blobstore/url", "other/blobstore/url")
|
||||||
err = c.Enqueue(t, asynq.Queue("critical"), asynq.Timeout(30*time.Second))
|
err = c.Enqueue(t, asynq.Queue("critical"), asynq.Timeout(30*time.Second))
|
||||||
|
@@ -5,7 +5,7 @@
|
|||||||
package asynq
|
package asynq
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"flag"
|
||||||
"sort"
|
"sort"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@@ -15,16 +15,28 @@ import (
|
|||||||
"github.com/hibiken/asynq/internal/log"
|
"github.com/hibiken/asynq/internal/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
// This file defines test helper functions used by
|
//============================================================================
|
||||||
// other test files.
|
// This file defines helper functions and variables used in other test files.
|
||||||
|
//============================================================================
|
||||||
|
|
||||||
// redis used for package testing.
|
// variables used for package testing.
|
||||||
const (
|
var (
|
||||||
redisAddr = "localhost:6379"
|
redisAddr string
|
||||||
redisDB = 14
|
redisDB int
|
||||||
|
|
||||||
|
testLogLevel = FatalLevel
|
||||||
)
|
)
|
||||||
|
|
||||||
var testLogger = log.NewLogger(os.Stderr)
|
var testLogger *log.Logger
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
flag.StringVar(&redisAddr, "redis_addr", "localhost:6379", "redis address to use in testing")
|
||||||
|
flag.IntVar(&redisDB, "redis_db", 14, "redis db number to use in testing")
|
||||||
|
flag.Var(&testLogLevel, "loglevel", "log level to use in testing")
|
||||||
|
|
||||||
|
testLogger = log.NewLogger(nil)
|
||||||
|
testLogger.SetLevel(toInternalLogLevel(testLogLevel))
|
||||||
|
}
|
||||||
|
|
||||||
func setup(tb testing.TB) *redis.Client {
|
func setup(tb testing.TB) *redis.Client {
|
||||||
tb.Helper()
|
tb.Helper()
|
||||||
|
@@ -209,6 +209,11 @@ func (c *Client) EnqueueIn(d time.Duration, task *Task, opts ...Option) error {
|
|||||||
return c.enqueueAt(time.Now().Add(d), task, opts...)
|
return c.enqueueAt(time.Now().Add(d), task, opts...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Close closes the connection with redis server.
|
||||||
|
func (c *Client) Close() error {
|
||||||
|
return c.rdb.Close()
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Client) enqueueAt(t time.Time, task *Task, opts ...Option) error {
|
func (c *Client) enqueueAt(t time.Time, task *Task, opts ...Option) error {
|
||||||
c.mu.Lock()
|
c.mu.Lock()
|
||||||
defer c.mu.Unlock()
|
defer c.mu.Unlock()
|
||||||
|
85
context.go
Normal file
85
context.go
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
// Copyright 2020 Kentaro Hibino. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT license
|
||||||
|
// that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package asynq
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/hibiken/asynq/internal/base"
|
||||||
|
)
|
||||||
|
|
||||||
|
// A taskMetadata holds task scoped data to put in context.
|
||||||
|
type taskMetadata struct {
|
||||||
|
id string
|
||||||
|
maxRetry int
|
||||||
|
retryCount int
|
||||||
|
}
|
||||||
|
|
||||||
|
// ctxKey type is unexported to prevent collisions with context keys defined in
|
||||||
|
// other packages.
|
||||||
|
type ctxKey int
|
||||||
|
|
||||||
|
// metadataCtxKey is the context key for the task metadata.
|
||||||
|
// Its value of zero is arbitrary.
|
||||||
|
const metadataCtxKey ctxKey = 0
|
||||||
|
|
||||||
|
// createContext returns a context and cancel function for a given task message.
|
||||||
|
func createContext(msg *base.TaskMessage) (ctx context.Context, cancel context.CancelFunc) {
|
||||||
|
metadata := taskMetadata{
|
||||||
|
id: msg.ID.String(),
|
||||||
|
maxRetry: msg.Retry,
|
||||||
|
retryCount: msg.Retried,
|
||||||
|
}
|
||||||
|
ctx = context.WithValue(context.Background(), metadataCtxKey, metadata)
|
||||||
|
timeout, err := time.ParseDuration(msg.Timeout)
|
||||||
|
if err == nil && timeout != 0 {
|
||||||
|
ctx, cancel = context.WithTimeout(ctx, timeout)
|
||||||
|
}
|
||||||
|
deadline, err := time.Parse(time.RFC3339, msg.Deadline)
|
||||||
|
if err == nil && !deadline.IsZero() {
|
||||||
|
ctx, cancel = context.WithDeadline(ctx, deadline)
|
||||||
|
}
|
||||||
|
if cancel == nil {
|
||||||
|
ctx, cancel = context.WithCancel(ctx)
|
||||||
|
}
|
||||||
|
return ctx, cancel
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTaskID extracts a task ID from a context, if any.
|
||||||
|
//
|
||||||
|
// ID of a task is guaranteed to be unique.
|
||||||
|
// ID of a task doesn't change if the task is being retried.
|
||||||
|
func GetTaskID(ctx context.Context) (id string, ok bool) {
|
||||||
|
metadata, ok := ctx.Value(metadataCtxKey).(taskMetadata)
|
||||||
|
if !ok {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
return metadata.id, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetRetryCount extracts retry count from a context, if any.
|
||||||
|
//
|
||||||
|
// Return value n indicates the number of times associated task has been
|
||||||
|
// retried so far.
|
||||||
|
func GetRetryCount(ctx context.Context) (n int, ok bool) {
|
||||||
|
metadata, ok := ctx.Value(metadataCtxKey).(taskMetadata)
|
||||||
|
if !ok {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
return metadata.retryCount, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMaxRetry extracts maximum retry from a context, if any.
|
||||||
|
//
|
||||||
|
// Return value n indicates the maximum number of times the assoicated task
|
||||||
|
// can be retried if ProcessTask returns a non-nil error.
|
||||||
|
func GetMaxRetry(ctx context.Context) (n int, ok bool) {
|
||||||
|
metadata, ok := ctx.Value(metadataCtxKey).(taskMetadata)
|
||||||
|
if !ok {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
return metadata.maxRetry, true
|
||||||
|
}
|
157
context_test.go
Normal file
157
context_test.go
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
// Copyright 2020 Kentaro Hibino. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT license
|
||||||
|
// that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package asynq
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/go-cmp/cmp"
|
||||||
|
"github.com/google/go-cmp/cmp/cmpopts"
|
||||||
|
"github.com/hibiken/asynq/internal/base"
|
||||||
|
"github.com/rs/xid"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCreateContextWithTimeRestrictions(t *testing.T) {
|
||||||
|
var (
|
||||||
|
noTimeout = time.Duration(0)
|
||||||
|
noDeadline = time.Time{}
|
||||||
|
)
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
desc string
|
||||||
|
timeout time.Duration
|
||||||
|
deadline time.Time
|
||||||
|
wantDeadline time.Time
|
||||||
|
}{
|
||||||
|
{"only with timeout", 10 * time.Second, noDeadline, time.Now().Add(10 * time.Second)},
|
||||||
|
{"only with deadline", noTimeout, time.Now().Add(time.Hour), time.Now().Add(time.Hour)},
|
||||||
|
{"with timeout and deadline (timeout < deadline)", 10 * time.Second, time.Now().Add(time.Hour), time.Now().Add(10 * time.Second)},
|
||||||
|
{"with timeout and deadline (timeout > deadline)", 10 * time.Minute, time.Now().Add(30 * time.Second), time.Now().Add(30 * time.Second)},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
msg := &base.TaskMessage{
|
||||||
|
Type: "something",
|
||||||
|
ID: xid.New(),
|
||||||
|
Timeout: tc.timeout.String(),
|
||||||
|
Deadline: tc.deadline.Format(time.RFC3339),
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := createContext(msg)
|
||||||
|
|
||||||
|
select {
|
||||||
|
case x := <-ctx.Done():
|
||||||
|
t.Errorf("%s: <-ctx.Done() == %v, want nothing (it should block)", tc.desc, x)
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
got, ok := ctx.Deadline()
|
||||||
|
if !ok {
|
||||||
|
t.Errorf("%s: ctx.Deadline() returned false, want deadline to be set", tc.desc)
|
||||||
|
}
|
||||||
|
if !cmp.Equal(tc.wantDeadline, got, cmpopts.EquateApproxTime(time.Second)) {
|
||||||
|
t.Errorf("%s: ctx.Deadline() returned %v, want %v", tc.desc, got, tc.wantDeadline)
|
||||||
|
}
|
||||||
|
|
||||||
|
cancel()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
default:
|
||||||
|
t.Errorf("ctx.Done() blocked, want it to be non-blocking")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateContextWithoutTimeRestrictions(t *testing.T) {
|
||||||
|
msg := &base.TaskMessage{
|
||||||
|
Type: "something",
|
||||||
|
ID: xid.New(),
|
||||||
|
Timeout: time.Duration(0).String(), // zero value to indicate no timeout
|
||||||
|
Deadline: time.Time{}.Format(time.RFC3339), // zero value to indicate no deadline
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := createContext(msg)
|
||||||
|
|
||||||
|
select {
|
||||||
|
case x := <-ctx.Done():
|
||||||
|
t.Errorf("<-ctx.Done() == %v, want nothing (it should block)", x)
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
_, ok := ctx.Deadline()
|
||||||
|
if ok {
|
||||||
|
t.Error("ctx.Deadline() returned true, want deadline to not be set")
|
||||||
|
}
|
||||||
|
|
||||||
|
cancel()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
default:
|
||||||
|
t.Error("ctx.Done() blocked, want it to be non-blocking")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetTaskMetadataFromContext(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
desc string
|
||||||
|
msg *base.TaskMessage
|
||||||
|
}{
|
||||||
|
{"with zero retried message", &base.TaskMessage{Type: "something", ID: xid.New(), Retry: 25, Retried: 0}},
|
||||||
|
{"with non-zero retried message", &base.TaskMessage{Type: "something", ID: xid.New(), Retry: 10, Retried: 5}},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
ctx, _ := createContext(tc.msg)
|
||||||
|
|
||||||
|
id, ok := GetTaskID(ctx)
|
||||||
|
if !ok {
|
||||||
|
t.Errorf("%s: GetTaskID(ctx) returned ok == false", tc.desc)
|
||||||
|
}
|
||||||
|
if ok && id != tc.msg.ID.String() {
|
||||||
|
t.Errorf("%s: GetTaskID(ctx) returned id == %q, want %q", tc.desc, id, tc.msg.ID.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
retried, ok := GetRetryCount(ctx)
|
||||||
|
if !ok {
|
||||||
|
t.Errorf("%s: GetRetryCount(ctx) returned ok == false", tc.desc)
|
||||||
|
}
|
||||||
|
if ok && retried != tc.msg.Retried {
|
||||||
|
t.Errorf("%s: GetRetryCount(ctx) returned n == %d want %d", tc.desc, retried, tc.msg.Retried)
|
||||||
|
}
|
||||||
|
|
||||||
|
maxRetry, ok := GetMaxRetry(ctx)
|
||||||
|
if !ok {
|
||||||
|
t.Errorf("%s: GetMaxRetry(ctx) returned ok == false", tc.desc)
|
||||||
|
}
|
||||||
|
if ok && maxRetry != tc.msg.Retry {
|
||||||
|
t.Errorf("%s: GetMaxRetry(ctx) returned n == %d want %d", tc.desc, maxRetry, tc.msg.Retry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetTaskMetadataFromContextError(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
desc string
|
||||||
|
ctx context.Context
|
||||||
|
}{
|
||||||
|
{"with background context", context.Background()},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
if _, ok := GetTaskID(tc.ctx); ok {
|
||||||
|
t.Errorf("%s: GetTaskID(ctx) returned ok == true", tc.desc)
|
||||||
|
}
|
||||||
|
if _, ok := GetRetryCount(tc.ctx); ok {
|
||||||
|
t.Errorf("%s: GetRetryCount(ctx) returned ok == true", tc.desc)
|
||||||
|
}
|
||||||
|
if _, ok := GetMaxRetry(tc.ctx); ok {
|
||||||
|
t.Errorf("%s: GetMaxRetry(ctx) returned ok == true", tc.desc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
26
heartbeat.go
26
heartbeat.go
@@ -9,12 +9,13 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/hibiken/asynq/internal/base"
|
"github.com/hibiken/asynq/internal/base"
|
||||||
|
"github.com/hibiken/asynq/internal/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
// heartbeater is responsible for writing process info to redis periodically to
|
// heartbeater is responsible for writing process info to redis periodically to
|
||||||
// indicate that the background worker process is up.
|
// indicate that the background worker process is up.
|
||||||
type heartbeater struct {
|
type heartbeater struct {
|
||||||
logger Logger
|
logger *log.Logger
|
||||||
broker base.Broker
|
broker base.Broker
|
||||||
|
|
||||||
ss *base.ServerState
|
ss *base.ServerState
|
||||||
@@ -26,18 +27,25 @@ type heartbeater struct {
|
|||||||
interval time.Duration
|
interval time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
func newHeartbeater(l Logger, b base.Broker, ss *base.ServerState, interval time.Duration) *heartbeater {
|
type heartbeaterParams struct {
|
||||||
|
logger *log.Logger
|
||||||
|
broker base.Broker
|
||||||
|
serverState *base.ServerState
|
||||||
|
interval time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
func newHeartbeater(params heartbeaterParams) *heartbeater {
|
||||||
return &heartbeater{
|
return &heartbeater{
|
||||||
logger: l,
|
logger: params.logger,
|
||||||
broker: b,
|
broker: params.broker,
|
||||||
ss: ss,
|
ss: params.serverState,
|
||||||
done: make(chan struct{}),
|
done: make(chan struct{}),
|
||||||
interval: interval,
|
interval: params.interval,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *heartbeater) terminate() {
|
func (h *heartbeater) terminate() {
|
||||||
h.logger.Info("Heartbeater shutting down...")
|
h.logger.Debug("Heartbeater shutting down...")
|
||||||
// Signal the heartbeater goroutine to stop.
|
// Signal the heartbeater goroutine to stop.
|
||||||
h.done <- struct{}{}
|
h.done <- struct{}{}
|
||||||
}
|
}
|
||||||
@@ -53,7 +61,7 @@ func (h *heartbeater) start(wg *sync.WaitGroup) {
|
|||||||
select {
|
select {
|
||||||
case <-h.done:
|
case <-h.done:
|
||||||
h.broker.ClearServerState(h.ss)
|
h.broker.ClearServerState(h.ss)
|
||||||
h.logger.Info("Heartbeater done")
|
h.logger.Debug("Heartbeater done")
|
||||||
return
|
return
|
||||||
case <-time.After(h.interval):
|
case <-time.After(h.interval):
|
||||||
h.beat()
|
h.beat()
|
||||||
@@ -67,6 +75,6 @@ func (h *heartbeater) beat() {
|
|||||||
// and short enough to expire quickly once the process is shut down or killed.
|
// and short enough to expire quickly once the process is shut down or killed.
|
||||||
err := h.broker.WriteServerState(h.ss, h.interval*2)
|
err := h.broker.WriteServerState(h.ss, h.interval*2)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
h.logger.Error("could not write heartbeat data: %v", err)
|
h.logger.Errorf("could not write heartbeat data: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -38,7 +38,12 @@ func TestHeartbeater(t *testing.T) {
|
|||||||
h.FlushDB(t, r)
|
h.FlushDB(t, r)
|
||||||
|
|
||||||
state := base.NewServerState(tc.host, tc.pid, tc.concurrency, tc.queues, false)
|
state := base.NewServerState(tc.host, tc.pid, tc.concurrency, tc.queues, false)
|
||||||
hb := newHeartbeater(testLogger, rdbClient, state, tc.interval)
|
hb := newHeartbeater(heartbeaterParams{
|
||||||
|
logger: testLogger,
|
||||||
|
broker: rdbClient,
|
||||||
|
serverState: state,
|
||||||
|
interval: tc.interval,
|
||||||
|
})
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
hb.start(&wg)
|
hb.start(&wg)
|
||||||
@@ -115,7 +120,12 @@ func TestHeartbeaterWithRedisDown(t *testing.T) {
|
|||||||
r := rdb.NewRDB(setup(t))
|
r := rdb.NewRDB(setup(t))
|
||||||
testBroker := testbroker.NewTestBroker(r)
|
testBroker := testbroker.NewTestBroker(r)
|
||||||
ss := base.NewServerState("localhost", 1234, 10, map[string]int{"default": 1}, false)
|
ss := base.NewServerState("localhost", 1234, 10, map[string]int{"default": 1}, false)
|
||||||
hb := newHeartbeater(testLogger, testBroker, ss, time.Second)
|
hb := newHeartbeater(heartbeaterParams{
|
||||||
|
logger: testLogger,
|
||||||
|
broker: testBroker,
|
||||||
|
serverState: ss,
|
||||||
|
interval: time.Second,
|
||||||
|
})
|
||||||
|
|
||||||
testBroker.Sleep()
|
testBroker.Sleep()
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
|
@@ -6,52 +6,210 @@
|
|||||||
package log
|
package log
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
stdlog "log"
|
stdlog "log"
|
||||||
"os"
|
"os"
|
||||||
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewLogger creates and returns a new instance of Logger.
|
// Base supports logging at various log levels.
|
||||||
func NewLogger(out io.Writer) *Logger {
|
type Base interface {
|
||||||
return &Logger{
|
// Debug logs a message at Debug level.
|
||||||
stdlog.New(out, "", stdlog.Ldate|stdlog.Ltime|stdlog.Lmicroseconds|stdlog.LUTC),
|
Debug(args ...interface{})
|
||||||
}
|
|
||||||
|
// Info logs a message at Info level.
|
||||||
|
Info(args ...interface{})
|
||||||
|
|
||||||
|
// Warn logs a message at Warning level.
|
||||||
|
Warn(args ...interface{})
|
||||||
|
|
||||||
|
// Error logs a message at Error level.
|
||||||
|
Error(args ...interface{})
|
||||||
|
|
||||||
|
// Fatal logs a message at Fatal level
|
||||||
|
// and process will exit with status set to 1.
|
||||||
|
Fatal(args ...interface{})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Logger is a wrapper object around log.Logger from the standard library.
|
// baseLogger is a wrapper object around log.Logger from the standard library.
|
||||||
// It supports logging at various log levels.
|
// It supports logging at various log levels.
|
||||||
type Logger struct {
|
type baseLogger struct {
|
||||||
*stdlog.Logger
|
*stdlog.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
// Debug logs a message at Debug level.
|
// Debug logs a message at Debug level.
|
||||||
func (l *Logger) Debug(format string, args ...interface{}) {
|
func (l *baseLogger) Debug(args ...interface{}) {
|
||||||
format = "DEBUG: " + format
|
l.prefixPrint("DEBUG: ", args...)
|
||||||
l.Printf(format, args...)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Info logs a message at Info level.
|
// Info logs a message at Info level.
|
||||||
func (l *Logger) Info(format string, args ...interface{}) {
|
func (l *baseLogger) Info(args ...interface{}) {
|
||||||
format = "INFO: " + format
|
l.prefixPrint("INFO: ", args...)
|
||||||
l.Printf(format, args...)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Warn logs a message at Warning level.
|
// Warn logs a message at Warning level.
|
||||||
func (l *Logger) Warn(format string, args ...interface{}) {
|
func (l *baseLogger) Warn(args ...interface{}) {
|
||||||
format = "WARN: " + format
|
l.prefixPrint("WARN: ", args...)
|
||||||
l.Printf(format, args...)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Error logs a message at Error level.
|
// Error logs a message at Error level.
|
||||||
func (l *Logger) Error(format string, args ...interface{}) {
|
func (l *baseLogger) Error(args ...interface{}) {
|
||||||
format = "ERROR: " + format
|
l.prefixPrint("ERROR: ", args...)
|
||||||
l.Printf(format, args...)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fatal logs a message at Fatal level
|
// Fatal logs a message at Fatal level
|
||||||
// and process will exit with status set to 1.
|
// and process will exit with status set to 1.
|
||||||
func (l *Logger) Fatal(format string, args ...interface{}) {
|
func (l *baseLogger) Fatal(args ...interface{}) {
|
||||||
format = "FATAL: " + format
|
l.prefixPrint("FATAL: ", args...)
|
||||||
l.Printf(format, args...)
|
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (l *baseLogger) prefixPrint(prefix string, args ...interface{}) {
|
||||||
|
args = append([]interface{}{prefix}, args...)
|
||||||
|
l.Print(args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// newBase creates and returns a new instance of baseLogger.
|
||||||
|
func newBase(out io.Writer) *baseLogger {
|
||||||
|
prefix := fmt.Sprintf("asynq: pid=%d ", os.Getpid())
|
||||||
|
return &baseLogger{
|
||||||
|
stdlog.New(out, prefix, stdlog.Ldate|stdlog.Ltime|stdlog.Lmicroseconds|stdlog.LUTC),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewLogger creates and returns a new instance of Logger.
|
||||||
|
// Log level is set to DebugLevel by default.
|
||||||
|
func NewLogger(base Base) *Logger {
|
||||||
|
if base == nil {
|
||||||
|
base = newBase(os.Stderr)
|
||||||
|
}
|
||||||
|
return &Logger{base: base, level: DebugLevel}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logger logs message to io.Writer at various log levels.
|
||||||
|
type Logger struct {
|
||||||
|
base Base
|
||||||
|
|
||||||
|
mu sync.Mutex
|
||||||
|
// Minimum log level for this logger.
|
||||||
|
// Message with level lower than this level won't be outputted.
|
||||||
|
level Level
|
||||||
|
}
|
||||||
|
|
||||||
|
// Level represents a log level.
|
||||||
|
type Level int32
|
||||||
|
|
||||||
|
const (
|
||||||
|
// DebugLevel is the lowest level of logging.
|
||||||
|
// Debug logs are intended for debugging and development purposes.
|
||||||
|
DebugLevel Level = iota
|
||||||
|
|
||||||
|
// InfoLevel is used for general informational log messages.
|
||||||
|
InfoLevel
|
||||||
|
|
||||||
|
// WarnLevel is used for undesired but relatively expected events,
|
||||||
|
// which may indicate a problem.
|
||||||
|
WarnLevel
|
||||||
|
|
||||||
|
// ErrorLevel is used for undesired and unexpected events that
|
||||||
|
// the program can recover from.
|
||||||
|
ErrorLevel
|
||||||
|
|
||||||
|
// FatalLevel is used for undesired and unexpected events that
|
||||||
|
// the program cannot recover from.
|
||||||
|
FatalLevel
|
||||||
|
)
|
||||||
|
|
||||||
|
// String is part of the fmt.Stringer interface.
|
||||||
|
//
|
||||||
|
// Used for testing and debugging purposes.
|
||||||
|
func (l Level) String() string {
|
||||||
|
switch l {
|
||||||
|
case DebugLevel:
|
||||||
|
return "debug"
|
||||||
|
case InfoLevel:
|
||||||
|
return "info"
|
||||||
|
case WarnLevel:
|
||||||
|
return "warning"
|
||||||
|
case ErrorLevel:
|
||||||
|
return "error"
|
||||||
|
case FatalLevel:
|
||||||
|
return "fatal"
|
||||||
|
default:
|
||||||
|
return "unknown"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// canLogAt reports whether logger can log at level v.
|
||||||
|
func (l *Logger) canLogAt(v Level) bool {
|
||||||
|
l.mu.Lock()
|
||||||
|
defer l.mu.Unlock()
|
||||||
|
return v >= l.level
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Logger) Debug(args ...interface{}) {
|
||||||
|
if !l.canLogAt(DebugLevel) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
l.base.Debug(args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Logger) Info(args ...interface{}) {
|
||||||
|
if !l.canLogAt(InfoLevel) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
l.base.Info(args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Logger) Warn(args ...interface{}) {
|
||||||
|
if !l.canLogAt(WarnLevel) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
l.base.Warn(args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Logger) Error(args ...interface{}) {
|
||||||
|
if !l.canLogAt(ErrorLevel) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
l.base.Error(args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Logger) Fatal(args ...interface{}) {
|
||||||
|
if !l.canLogAt(FatalLevel) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
l.base.Fatal(args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Logger) Debugf(format string, args ...interface{}) {
|
||||||
|
l.Debug(fmt.Sprintf(format, args...))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Logger) Infof(format string, args ...interface{}) {
|
||||||
|
l.Info(fmt.Sprintf(format, args...))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Logger) Warnf(format string, args ...interface{}) {
|
||||||
|
l.Warn(fmt.Sprintf(format, args...))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Logger) Errorf(format string, args ...interface{}) {
|
||||||
|
l.Error(fmt.Sprintf(format, args...))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Logger) Fatalf(format string, args ...interface{}) {
|
||||||
|
l.Fatal(fmt.Sprintf(format, args...))
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetLevel sets the logger level.
|
||||||
|
// It panics if v is less than DebugLevel or greater than FatalLevel.
|
||||||
|
func (l *Logger) SetLevel(v Level) {
|
||||||
|
l.mu.Lock()
|
||||||
|
defer l.mu.Unlock()
|
||||||
|
if v < DebugLevel || v > FatalLevel {
|
||||||
|
panic("log: invalid log level")
|
||||||
|
}
|
||||||
|
l.level = v
|
||||||
|
}
|
||||||
|
@@ -13,6 +13,7 @@ import (
|
|||||||
|
|
||||||
// regexp for timestamps
|
// regexp for timestamps
|
||||||
const (
|
const (
|
||||||
|
rgxPID = `[0-9]+`
|
||||||
rgxdate = `[0-9][0-9][0-9][0-9]/[0-9][0-9]/[0-9][0-9]`
|
rgxdate = `[0-9][0-9][0-9][0-9]/[0-9][0-9]/[0-9][0-9]`
|
||||||
rgxtime = `[0-9][0-9]:[0-9][0-9]:[0-9][0-9]`
|
rgxtime = `[0-9][0-9]:[0-9][0-9]:[0-9][0-9]`
|
||||||
rgxmicroseconds = `\.[0-9][0-9][0-9][0-9][0-9][0-9]`
|
rgxmicroseconds = `\.[0-9][0-9][0-9][0-9][0-9][0-9]`
|
||||||
@@ -27,20 +28,22 @@ type tester struct {
|
|||||||
func TestLoggerDebug(t *testing.T) {
|
func TestLoggerDebug(t *testing.T) {
|
||||||
tests := []tester{
|
tests := []tester{
|
||||||
{
|
{
|
||||||
desc: "without trailing newline, logger adds newline",
|
desc: "without trailing newline, logger adds newline",
|
||||||
message: "hello, world!",
|
message: "hello, world!",
|
||||||
wantPattern: fmt.Sprintf("^%s %s%s DEBUG: hello, world!\n$", rgxdate, rgxtime, rgxmicroseconds),
|
wantPattern: fmt.Sprintf("^asynq: pid=%s %s %s%s DEBUG: hello, world!\n$",
|
||||||
|
rgxPID, rgxdate, rgxtime, rgxmicroseconds),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
desc: "with trailing newline, logger preserves newline",
|
desc: "with trailing newline, logger preserves newline",
|
||||||
message: "hello, world!\n",
|
message: "hello, world!\n",
|
||||||
wantPattern: fmt.Sprintf("^%s %s%s DEBUG: hello, world!\n$", rgxdate, rgxtime, rgxmicroseconds),
|
wantPattern: fmt.Sprintf("^asynq: pid=%s %s %s%s DEBUG: hello, world!\n$",
|
||||||
|
rgxPID, rgxdate, rgxtime, rgxmicroseconds),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range tests {
|
for _, tc := range tests {
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
logger := NewLogger(&buf)
|
logger := NewLogger(newBase(&buf))
|
||||||
|
|
||||||
logger.Debug(tc.message)
|
logger.Debug(tc.message)
|
||||||
|
|
||||||
@@ -50,7 +53,7 @@ func TestLoggerDebug(t *testing.T) {
|
|||||||
t.Fatal("pattern did not compile:", err)
|
t.Fatal("pattern did not compile:", err)
|
||||||
}
|
}
|
||||||
if !matched {
|
if !matched {
|
||||||
t.Errorf("logger.info(%q) outputted %q, should match pattern %q",
|
t.Errorf("logger.Debug(%q) outputted %q, should match pattern %q",
|
||||||
tc.message, got, tc.wantPattern)
|
tc.message, got, tc.wantPattern)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -59,20 +62,22 @@ func TestLoggerDebug(t *testing.T) {
|
|||||||
func TestLoggerInfo(t *testing.T) {
|
func TestLoggerInfo(t *testing.T) {
|
||||||
tests := []tester{
|
tests := []tester{
|
||||||
{
|
{
|
||||||
desc: "without trailing newline, logger adds newline",
|
desc: "without trailing newline, logger adds newline",
|
||||||
message: "hello, world!",
|
message: "hello, world!",
|
||||||
wantPattern: fmt.Sprintf("^%s %s%s INFO: hello, world!\n$", rgxdate, rgxtime, rgxmicroseconds),
|
wantPattern: fmt.Sprintf("^asynq: pid=%s %s %s%s INFO: hello, world!\n$",
|
||||||
|
rgxPID, rgxdate, rgxtime, rgxmicroseconds),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
desc: "with trailing newline, logger preserves newline",
|
desc: "with trailing newline, logger preserves newline",
|
||||||
message: "hello, world!\n",
|
message: "hello, world!\n",
|
||||||
wantPattern: fmt.Sprintf("^%s %s%s INFO: hello, world!\n$", rgxdate, rgxtime, rgxmicroseconds),
|
wantPattern: fmt.Sprintf("^asynq: pid=%s %s %s%s INFO: hello, world!\n$",
|
||||||
|
rgxPID, rgxdate, rgxtime, rgxmicroseconds),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range tests {
|
for _, tc := range tests {
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
logger := NewLogger(&buf)
|
logger := NewLogger(newBase(&buf))
|
||||||
|
|
||||||
logger.Info(tc.message)
|
logger.Info(tc.message)
|
||||||
|
|
||||||
@@ -82,7 +87,7 @@ func TestLoggerInfo(t *testing.T) {
|
|||||||
t.Fatal("pattern did not compile:", err)
|
t.Fatal("pattern did not compile:", err)
|
||||||
}
|
}
|
||||||
if !matched {
|
if !matched {
|
||||||
t.Errorf("logger.info(%q) outputted %q, should match pattern %q",
|
t.Errorf("logger.Info(%q) outputted %q, should match pattern %q",
|
||||||
tc.message, got, tc.wantPattern)
|
tc.message, got, tc.wantPattern)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -91,20 +96,22 @@ func TestLoggerInfo(t *testing.T) {
|
|||||||
func TestLoggerWarn(t *testing.T) {
|
func TestLoggerWarn(t *testing.T) {
|
||||||
tests := []tester{
|
tests := []tester{
|
||||||
{
|
{
|
||||||
desc: "without trailing newline, logger adds newline",
|
desc: "without trailing newline, logger adds newline",
|
||||||
message: "hello, world!",
|
message: "hello, world!",
|
||||||
wantPattern: fmt.Sprintf("^%s %s%s WARN: hello, world!\n$", rgxdate, rgxtime, rgxmicroseconds),
|
wantPattern: fmt.Sprintf("^asynq: pid=%s %s %s%s WARN: hello, world!\n$",
|
||||||
|
rgxPID, rgxdate, rgxtime, rgxmicroseconds),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
desc: "with trailing newline, logger preserves newline",
|
desc: "with trailing newline, logger preserves newline",
|
||||||
message: "hello, world!\n",
|
message: "hello, world!\n",
|
||||||
wantPattern: fmt.Sprintf("^%s %s%s WARN: hello, world!\n$", rgxdate, rgxtime, rgxmicroseconds),
|
wantPattern: fmt.Sprintf("^asynq: pid=%s %s %s%s WARN: hello, world!\n$",
|
||||||
|
rgxPID, rgxdate, rgxtime, rgxmicroseconds),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range tests {
|
for _, tc := range tests {
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
logger := NewLogger(&buf)
|
logger := NewLogger(newBase(&buf))
|
||||||
|
|
||||||
logger.Warn(tc.message)
|
logger.Warn(tc.message)
|
||||||
|
|
||||||
@@ -114,7 +121,7 @@ func TestLoggerWarn(t *testing.T) {
|
|||||||
t.Fatal("pattern did not compile:", err)
|
t.Fatal("pattern did not compile:", err)
|
||||||
}
|
}
|
||||||
if !matched {
|
if !matched {
|
||||||
t.Errorf("logger.info(%q) outputted %q, should match pattern %q",
|
t.Errorf("logger.Warn(%q) outputted %q, should match pattern %q",
|
||||||
tc.message, got, tc.wantPattern)
|
tc.message, got, tc.wantPattern)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -123,20 +130,22 @@ func TestLoggerWarn(t *testing.T) {
|
|||||||
func TestLoggerError(t *testing.T) {
|
func TestLoggerError(t *testing.T) {
|
||||||
tests := []tester{
|
tests := []tester{
|
||||||
{
|
{
|
||||||
desc: "without trailing newline, logger adds newline",
|
desc: "without trailing newline, logger adds newline",
|
||||||
message: "hello, world!",
|
message: "hello, world!",
|
||||||
wantPattern: fmt.Sprintf("^%s %s%s ERROR: hello, world!\n$", rgxdate, rgxtime, rgxmicroseconds),
|
wantPattern: fmt.Sprintf("^asynq: pid=%s %s %s%s ERROR: hello, world!\n$",
|
||||||
|
rgxPID, rgxdate, rgxtime, rgxmicroseconds),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
desc: "with trailing newline, logger preserves newline",
|
desc: "with trailing newline, logger preserves newline",
|
||||||
message: "hello, world!\n",
|
message: "hello, world!\n",
|
||||||
wantPattern: fmt.Sprintf("^%s %s%s ERROR: hello, world!\n$", rgxdate, rgxtime, rgxmicroseconds),
|
wantPattern: fmt.Sprintf("^asynq: pid=%s %s %s%s ERROR: hello, world!\n$",
|
||||||
|
rgxPID, rgxdate, rgxtime, rgxmicroseconds),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range tests {
|
for _, tc := range tests {
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
logger := NewLogger(&buf)
|
logger := NewLogger(newBase(&buf))
|
||||||
|
|
||||||
logger.Error(tc.message)
|
logger.Error(tc.message)
|
||||||
|
|
||||||
@@ -146,8 +155,234 @@ func TestLoggerError(t *testing.T) {
|
|||||||
t.Fatal("pattern did not compile:", err)
|
t.Fatal("pattern did not compile:", err)
|
||||||
}
|
}
|
||||||
if !matched {
|
if !matched {
|
||||||
t.Errorf("logger.info(%q) outputted %q, should match pattern %q",
|
t.Errorf("logger.Error(%q) outputted %q, should match pattern %q",
|
||||||
tc.message, got, tc.wantPattern)
|
tc.message, got, tc.wantPattern)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type formatTester struct {
|
||||||
|
desc string
|
||||||
|
format string
|
||||||
|
args []interface{}
|
||||||
|
wantPattern string // regexp that log output must match
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoggerDebugf(t *testing.T) {
|
||||||
|
tests := []formatTester{
|
||||||
|
{
|
||||||
|
desc: "Formats message with DEBUG prefix",
|
||||||
|
format: "hello, %s!",
|
||||||
|
args: []interface{}{"Gopher"},
|
||||||
|
wantPattern: fmt.Sprintf("^asynq: pid=%s %s %s%s DEBUG: hello, Gopher!\n$",
|
||||||
|
rgxPID, rgxdate, rgxtime, rgxmicroseconds),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
logger := NewLogger(newBase(&buf))
|
||||||
|
|
||||||
|
logger.Debugf(tc.format, tc.args...)
|
||||||
|
|
||||||
|
got := buf.String()
|
||||||
|
matched, err := regexp.MatchString(tc.wantPattern, got)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("pattern did not compile:", err)
|
||||||
|
}
|
||||||
|
if !matched {
|
||||||
|
t.Errorf("logger.Debugf(%q, %v) outputted %q, should match pattern %q",
|
||||||
|
tc.format, tc.args, got, tc.wantPattern)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoggerInfof(t *testing.T) {
|
||||||
|
tests := []formatTester{
|
||||||
|
{
|
||||||
|
desc: "Formats message with INFO prefix",
|
||||||
|
format: "%d,%d,%d",
|
||||||
|
args: []interface{}{1, 2, 3},
|
||||||
|
wantPattern: fmt.Sprintf("^asynq: pid=%s %s %s%s INFO: 1,2,3\n$",
|
||||||
|
rgxPID, rgxdate, rgxtime, rgxmicroseconds),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
logger := NewLogger(newBase(&buf))
|
||||||
|
|
||||||
|
logger.Infof(tc.format, tc.args...)
|
||||||
|
|
||||||
|
got := buf.String()
|
||||||
|
matched, err := regexp.MatchString(tc.wantPattern, got)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("pattern did not compile:", err)
|
||||||
|
}
|
||||||
|
if !matched {
|
||||||
|
t.Errorf("logger.Infof(%q, %v) outputted %q, should match pattern %q",
|
||||||
|
tc.format, tc.args, got, tc.wantPattern)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoggerWarnf(t *testing.T) {
|
||||||
|
tests := []formatTester{
|
||||||
|
{
|
||||||
|
desc: "Formats message with WARN prefix",
|
||||||
|
format: "hello, %s",
|
||||||
|
args: []interface{}{"Gophers"},
|
||||||
|
wantPattern: fmt.Sprintf("^asynq: pid=%s %s %s%s WARN: hello, Gophers\n$",
|
||||||
|
rgxPID, rgxdate, rgxtime, rgxmicroseconds),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
logger := NewLogger(newBase(&buf))
|
||||||
|
|
||||||
|
logger.Warnf(tc.format, tc.args...)
|
||||||
|
|
||||||
|
got := buf.String()
|
||||||
|
matched, err := regexp.MatchString(tc.wantPattern, got)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("pattern did not compile:", err)
|
||||||
|
}
|
||||||
|
if !matched {
|
||||||
|
t.Errorf("logger.Warnf(%q, %v) outputted %q, should match pattern %q",
|
||||||
|
tc.format, tc.args, got, tc.wantPattern)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoggerErrorf(t *testing.T) {
|
||||||
|
tests := []formatTester{
|
||||||
|
{
|
||||||
|
desc: "Formats message with ERROR prefix",
|
||||||
|
format: "hello, %s",
|
||||||
|
args: []interface{}{"Gophers"},
|
||||||
|
wantPattern: fmt.Sprintf("^asynq: pid=%s %s %s%s ERROR: hello, Gophers\n$",
|
||||||
|
rgxPID, rgxdate, rgxtime, rgxmicroseconds),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
logger := NewLogger(newBase(&buf))
|
||||||
|
|
||||||
|
logger.Errorf(tc.format, tc.args...)
|
||||||
|
|
||||||
|
got := buf.String()
|
||||||
|
matched, err := regexp.MatchString(tc.wantPattern, got)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("pattern did not compile:", err)
|
||||||
|
}
|
||||||
|
if !matched {
|
||||||
|
t.Errorf("logger.Errorf(%q, %v) outputted %q, should match pattern %q",
|
||||||
|
tc.format, tc.args, got, tc.wantPattern)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoggerWithLowerLevels(t *testing.T) {
|
||||||
|
// Logger should not log messages at a level
|
||||||
|
// lower than the specified level.
|
||||||
|
tests := []struct {
|
||||||
|
level Level
|
||||||
|
op string
|
||||||
|
}{
|
||||||
|
// with level one above
|
||||||
|
{InfoLevel, "Debug"},
|
||||||
|
{InfoLevel, "Debugf"},
|
||||||
|
{WarnLevel, "Info"},
|
||||||
|
{WarnLevel, "Infof"},
|
||||||
|
{ErrorLevel, "Warn"},
|
||||||
|
{ErrorLevel, "Warnf"},
|
||||||
|
{FatalLevel, "Error"},
|
||||||
|
{FatalLevel, "Errorf"},
|
||||||
|
// with skip level
|
||||||
|
{WarnLevel, "Debug"},
|
||||||
|
{ErrorLevel, "Infof"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
logger := NewLogger(newBase(&buf))
|
||||||
|
logger.SetLevel(tc.level)
|
||||||
|
|
||||||
|
switch tc.op {
|
||||||
|
case "Debug":
|
||||||
|
logger.Debug("hello")
|
||||||
|
case "Debugf":
|
||||||
|
logger.Debugf("hello, %s", "world")
|
||||||
|
case "Info":
|
||||||
|
logger.Info("hello")
|
||||||
|
case "Infof":
|
||||||
|
logger.Infof("hello, %s", "world")
|
||||||
|
case "Warn":
|
||||||
|
logger.Warn("hello")
|
||||||
|
case "Warnf":
|
||||||
|
logger.Warnf("hello, %s", "world")
|
||||||
|
case "Error":
|
||||||
|
logger.Error("hello")
|
||||||
|
case "Errorf":
|
||||||
|
logger.Errorf("hello, %s", "world")
|
||||||
|
default:
|
||||||
|
t.Fatalf("unexpected op: %q", tc.op)
|
||||||
|
}
|
||||||
|
|
||||||
|
if buf.String() != "" {
|
||||||
|
t.Errorf("logger.%s outputted log message when level is set to %v", tc.op, tc.level)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoggerWithSameOrHigherLevels(t *testing.T) {
|
||||||
|
// Logger should log messages at a level
|
||||||
|
// same as or higher than the specified level.
|
||||||
|
tests := []struct {
|
||||||
|
level Level
|
||||||
|
op string
|
||||||
|
}{
|
||||||
|
// same level
|
||||||
|
{DebugLevel, "Debug"},
|
||||||
|
{InfoLevel, "Infof"},
|
||||||
|
{WarnLevel, "Warn"},
|
||||||
|
{ErrorLevel, "Errorf"},
|
||||||
|
// higher level
|
||||||
|
{DebugLevel, "Info"},
|
||||||
|
{InfoLevel, "Warnf"},
|
||||||
|
{WarnLevel, "Error"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
logger := NewLogger(newBase(&buf))
|
||||||
|
logger.SetLevel(tc.level)
|
||||||
|
|
||||||
|
switch tc.op {
|
||||||
|
case "Debug":
|
||||||
|
logger.Debug("hello")
|
||||||
|
case "Debugf":
|
||||||
|
logger.Debugf("hello, %s", "world")
|
||||||
|
case "Info":
|
||||||
|
logger.Info("hello")
|
||||||
|
case "Infof":
|
||||||
|
logger.Infof("hello, %s", "world")
|
||||||
|
case "Warn":
|
||||||
|
logger.Warn("hello")
|
||||||
|
case "Warnf":
|
||||||
|
logger.Warnf("hello, %s", "world")
|
||||||
|
case "Error":
|
||||||
|
logger.Error("hello")
|
||||||
|
case "Errorf":
|
||||||
|
logger.Errorf("hello, %s", "world")
|
||||||
|
default:
|
||||||
|
t.Fatalf("unexpected op: %q", tc.op)
|
||||||
|
}
|
||||||
|
|
||||||
|
if buf.String() == "" {
|
||||||
|
t.Errorf("logger.%s did not output log message when level is set to %v", tc.op, tc.level)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
61
processor.go
61
processor.go
@@ -13,12 +13,13 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/hibiken/asynq/internal/base"
|
"github.com/hibiken/asynq/internal/base"
|
||||||
|
"github.com/hibiken/asynq/internal/log"
|
||||||
"github.com/hibiken/asynq/internal/rdb"
|
"github.com/hibiken/asynq/internal/rdb"
|
||||||
"golang.org/x/time/rate"
|
"golang.org/x/time/rate"
|
||||||
)
|
)
|
||||||
|
|
||||||
type processor struct {
|
type processor struct {
|
||||||
logger Logger
|
logger *log.Logger
|
||||||
broker base.Broker
|
broker base.Broker
|
||||||
|
|
||||||
ss *base.ServerState
|
ss *base.ServerState
|
||||||
@@ -63,8 +64,8 @@ type processor struct {
|
|||||||
|
|
||||||
type retryDelayFunc func(n int, err error, task *Task) time.Duration
|
type retryDelayFunc func(n int, err error, task *Task) time.Duration
|
||||||
|
|
||||||
type newProcessorParams struct {
|
type processorParams struct {
|
||||||
logger Logger
|
logger *log.Logger
|
||||||
broker base.Broker
|
broker base.Broker
|
||||||
ss *base.ServerState
|
ss *base.ServerState
|
||||||
retryDelayFunc retryDelayFunc
|
retryDelayFunc retryDelayFunc
|
||||||
@@ -75,7 +76,7 @@ type newProcessorParams struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// newProcessor constructs a new processor.
|
// newProcessor constructs a new processor.
|
||||||
func newProcessor(params newProcessorParams) *processor {
|
func newProcessor(params processorParams) *processor {
|
||||||
info := params.ss.GetInfo()
|
info := params.ss.GetInfo()
|
||||||
qcfg := normalizeQueueCfg(info.Queues)
|
qcfg := normalizeQueueCfg(info.Queues)
|
||||||
orderedQueues := []string(nil)
|
orderedQueues := []string(nil)
|
||||||
@@ -105,7 +106,7 @@ func newProcessor(params newProcessorParams) *processor {
|
|||||||
// It's safe to call this method multiple times.
|
// It's safe to call this method multiple times.
|
||||||
func (p *processor) stop() {
|
func (p *processor) stop() {
|
||||||
p.once.Do(func() {
|
p.once.Do(func() {
|
||||||
p.logger.Info("Processor shutting down...")
|
p.logger.Debug("Processor shutting down...")
|
||||||
// Unblock if processor is waiting for sema token.
|
// Unblock if processor is waiting for sema token.
|
||||||
close(p.abort)
|
close(p.abort)
|
||||||
// Signal the processor goroutine to stop processing tasks
|
// Signal the processor goroutine to stop processing tasks
|
||||||
@@ -144,7 +145,7 @@ func (p *processor) start(wg *sync.WaitGroup) {
|
|||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-p.done:
|
case <-p.done:
|
||||||
p.logger.Info("Processor done")
|
p.logger.Debug("Processor done")
|
||||||
return
|
return
|
||||||
default:
|
default:
|
||||||
p.exec()
|
p.exec()
|
||||||
@@ -158,19 +159,20 @@ func (p *processor) start(wg *sync.WaitGroup) {
|
|||||||
func (p *processor) exec() {
|
func (p *processor) exec() {
|
||||||
qnames := p.queues()
|
qnames := p.queues()
|
||||||
msg, err := p.broker.Dequeue(qnames...)
|
msg, err := p.broker.Dequeue(qnames...)
|
||||||
if err == rdb.ErrNoProcessableTask { // TODO: Need to decouple this error from rdb to support other brokers
|
switch {
|
||||||
|
case err == rdb.ErrNoProcessableTask:
|
||||||
// queues are empty, this is a normal behavior.
|
// queues are empty, this is a normal behavior.
|
||||||
if len(p.queueConfig) > 1 {
|
if len(qnames) > 1 {
|
||||||
// sleep to avoid slamming redis and let scheduler move tasks into queues.
|
// sleep to avoid slamming redis and let scheduler move tasks into queues.
|
||||||
// Note: With multiple queues, we are not using blocking pop operation and
|
// Note: With multiple queues, we are not using blocking pop operation and
|
||||||
// polling queues instead. This adds significant load to redis.
|
// polling queues instead. This adds significant load to redis.
|
||||||
time.Sleep(time.Second)
|
time.Sleep(time.Second)
|
||||||
}
|
}
|
||||||
|
p.logger.Debug("All queues are empty")
|
||||||
return
|
return
|
||||||
}
|
case err != nil:
|
||||||
if err != nil {
|
|
||||||
if p.errLogLimiter.Allow() {
|
if p.errLogLimiter.Allow() {
|
||||||
p.logger.Error("Dequeue error: %v", err)
|
p.logger.Errorf("Dequeue error: %v", err)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -185,7 +187,7 @@ func (p *processor) exec() {
|
|||||||
go func() {
|
go func() {
|
||||||
defer func() {
|
defer func() {
|
||||||
p.ss.DeleteWorkerStats(msg)
|
p.ss.DeleteWorkerStats(msg)
|
||||||
<-p.sema /* release token */
|
<-p.sema // release token
|
||||||
}()
|
}()
|
||||||
|
|
||||||
ctx, cancel := createContext(msg)
|
ctx, cancel := createContext(msg)
|
||||||
@@ -202,7 +204,7 @@ func (p *processor) exec() {
|
|||||||
select {
|
select {
|
||||||
case <-p.quit:
|
case <-p.quit:
|
||||||
// time is up, quit this worker goroutine.
|
// time is up, quit this worker goroutine.
|
||||||
p.logger.Warn("Quitting worker. task id=%s", msg.ID)
|
p.logger.Warnf("Quitting worker. task id=%s", msg.ID)
|
||||||
return
|
return
|
||||||
case resErr := <-resCh:
|
case resErr := <-resCh:
|
||||||
// Note: One of three things should happen.
|
// Note: One of three things should happen.
|
||||||
@@ -231,17 +233,17 @@ func (p *processor) exec() {
|
|||||||
func (p *processor) restore() {
|
func (p *processor) restore() {
|
||||||
n, err := p.broker.RequeueAll()
|
n, err := p.broker.RequeueAll()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
p.logger.Error("Could not restore unfinished tasks: %v", err)
|
p.logger.Errorf("Could not restore unfinished tasks: %v", err)
|
||||||
}
|
}
|
||||||
if n > 0 {
|
if n > 0 {
|
||||||
p.logger.Info("Restored %d unfinished tasks back to queue", n)
|
p.logger.Infof("Restored %d unfinished tasks back to queue", n)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *processor) requeue(msg *base.TaskMessage) {
|
func (p *processor) requeue(msg *base.TaskMessage) {
|
||||||
err := p.broker.Requeue(msg)
|
err := p.broker.Requeue(msg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
p.logger.Error("Could not push task id=%s back to queue: %v", msg.ID, err)
|
p.logger.Errorf("Could not push task id=%s back to queue: %v", msg.ID, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -249,7 +251,7 @@ func (p *processor) markAsDone(msg *base.TaskMessage) {
|
|||||||
err := p.broker.Done(msg)
|
err := p.broker.Done(msg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errMsg := fmt.Sprintf("Could not remove task id=%s from %q", msg.ID, base.InProgressQueue)
|
errMsg := fmt.Sprintf("Could not remove task id=%s from %q", msg.ID, base.InProgressQueue)
|
||||||
p.logger.Warn("%s; Will retry syncing", errMsg)
|
p.logger.Warnf("%s; Will retry syncing", errMsg)
|
||||||
p.syncRequestCh <- &syncRequest{
|
p.syncRequestCh <- &syncRequest{
|
||||||
fn: func() error {
|
fn: func() error {
|
||||||
return p.broker.Done(msg)
|
return p.broker.Done(msg)
|
||||||
@@ -265,7 +267,7 @@ func (p *processor) retry(msg *base.TaskMessage, e error) {
|
|||||||
err := p.broker.Retry(msg, retryAt, e.Error())
|
err := p.broker.Retry(msg, retryAt, e.Error())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errMsg := fmt.Sprintf("Could not move task id=%s from %q to %q", msg.ID, base.InProgressQueue, base.RetryQueue)
|
errMsg := fmt.Sprintf("Could not move task id=%s from %q to %q", msg.ID, base.InProgressQueue, base.RetryQueue)
|
||||||
p.logger.Warn("%s; Will retry syncing", errMsg)
|
p.logger.Warnf("%s; Will retry syncing", errMsg)
|
||||||
p.syncRequestCh <- &syncRequest{
|
p.syncRequestCh <- &syncRequest{
|
||||||
fn: func() error {
|
fn: func() error {
|
||||||
return p.broker.Retry(msg, retryAt, e.Error())
|
return p.broker.Retry(msg, retryAt, e.Error())
|
||||||
@@ -276,11 +278,11 @@ func (p *processor) retry(msg *base.TaskMessage, e error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *processor) kill(msg *base.TaskMessage, e error) {
|
func (p *processor) kill(msg *base.TaskMessage, e error) {
|
||||||
p.logger.Warn("Retry exhausted for task id=%s", msg.ID)
|
p.logger.Warnf("Retry exhausted for task id=%s", msg.ID)
|
||||||
err := p.broker.Kill(msg, e.Error())
|
err := p.broker.Kill(msg, e.Error())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errMsg := fmt.Sprintf("Could not move task id=%s from %q to %q", msg.ID, base.InProgressQueue, base.DeadQueue)
|
errMsg := fmt.Sprintf("Could not move task id=%s from %q to %q", msg.ID, base.InProgressQueue, base.DeadQueue)
|
||||||
p.logger.Warn("%s; Will retry syncing", errMsg)
|
p.logger.Warnf("%s; Will retry syncing", errMsg)
|
||||||
p.syncRequestCh <- &syncRequest{
|
p.syncRequestCh <- &syncRequest{
|
||||||
fn: func() error {
|
fn: func() error {
|
||||||
return p.broker.Kill(msg, e.Error())
|
return p.broker.Kill(msg, e.Error())
|
||||||
@@ -308,7 +310,7 @@ func (p *processor) queues() []string {
|
|||||||
}
|
}
|
||||||
var names []string
|
var names []string
|
||||||
for qname, priority := range p.queueConfig {
|
for qname, priority := range p.queueConfig {
|
||||||
for i := 0; i < int(priority); i++ {
|
for i := 0; i < priority; i++ {
|
||||||
names = append(names, qname)
|
names = append(names, qname)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -403,20 +405,3 @@ func gcd(xs ...int) int {
|
|||||||
}
|
}
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
// createContext returns a context and cancel function for a given task message.
|
|
||||||
func createContext(msg *base.TaskMessage) (ctx context.Context, cancel context.CancelFunc) {
|
|
||||||
ctx = context.Background()
|
|
||||||
timeout, err := time.ParseDuration(msg.Timeout)
|
|
||||||
if err == nil && timeout != 0 {
|
|
||||||
ctx, cancel = context.WithTimeout(ctx, timeout)
|
|
||||||
}
|
|
||||||
deadline, err := time.Parse(time.RFC3339, msg.Deadline)
|
|
||||||
if err == nil && !deadline.IsZero() {
|
|
||||||
ctx, cancel = context.WithDeadline(ctx, deadline)
|
|
||||||
}
|
|
||||||
if cancel == nil {
|
|
||||||
ctx, cancel = context.WithCancel(ctx)
|
|
||||||
}
|
|
||||||
return ctx, cancel
|
|
||||||
}
|
|
||||||
|
@@ -17,7 +17,6 @@ import (
|
|||||||
h "github.com/hibiken/asynq/internal/asynqtest"
|
h "github.com/hibiken/asynq/internal/asynqtest"
|
||||||
"github.com/hibiken/asynq/internal/base"
|
"github.com/hibiken/asynq/internal/base"
|
||||||
"github.com/hibiken/asynq/internal/rdb"
|
"github.com/hibiken/asynq/internal/rdb"
|
||||||
"github.com/rs/xid"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestProcessorSuccess(t *testing.T) {
|
func TestProcessorSuccess(t *testing.T) {
|
||||||
@@ -65,7 +64,7 @@ func TestProcessorSuccess(t *testing.T) {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
ss := base.NewServerState("localhost", 1234, 10, defaultQueueConfig, false)
|
ss := base.NewServerState("localhost", 1234, 10, defaultQueueConfig, false)
|
||||||
p := newProcessor(newProcessorParams{
|
p := newProcessor(processorParams{
|
||||||
logger: testLogger,
|
logger: testLogger,
|
||||||
broker: rdbClient,
|
broker: rdbClient,
|
||||||
ss: ss,
|
ss: ss,
|
||||||
@@ -170,7 +169,7 @@ func TestProcessorRetry(t *testing.T) {
|
|||||||
n++
|
n++
|
||||||
}
|
}
|
||||||
ss := base.NewServerState("localhost", 1234, 10, defaultQueueConfig, false)
|
ss := base.NewServerState("localhost", 1234, 10, defaultQueueConfig, false)
|
||||||
p := newProcessor(newProcessorParams{
|
p := newProcessor(processorParams{
|
||||||
logger: testLogger,
|
logger: testLogger,
|
||||||
broker: rdbClient,
|
broker: rdbClient,
|
||||||
ss: ss,
|
ss: ss,
|
||||||
@@ -243,7 +242,7 @@ func TestProcessorQueues(t *testing.T) {
|
|||||||
|
|
||||||
for _, tc := range tests {
|
for _, tc := range tests {
|
||||||
ss := base.NewServerState("localhost", 1234, 10, tc.queueCfg, false)
|
ss := base.NewServerState("localhost", 1234, 10, tc.queueCfg, false)
|
||||||
p := newProcessor(newProcessorParams{
|
p := newProcessor(processorParams{
|
||||||
logger: testLogger,
|
logger: testLogger,
|
||||||
broker: nil,
|
broker: nil,
|
||||||
ss: ss,
|
ss: ss,
|
||||||
@@ -319,7 +318,7 @@ func TestProcessorWithStrictPriority(t *testing.T) {
|
|||||||
}
|
}
|
||||||
// Note: Set concurrency to 1 to make sure tasks are processed one at a time.
|
// Note: Set concurrency to 1 to make sure tasks are processed one at a time.
|
||||||
ss := base.NewServerState("localhost", 1234, 1 /* concurrency */, queueCfg, true /*strict*/)
|
ss := base.NewServerState("localhost", 1234, 1 /* concurrency */, queueCfg, true /*strict*/)
|
||||||
p := newProcessor(newProcessorParams{
|
p := newProcessor(processorParams{
|
||||||
logger: testLogger,
|
logger: testLogger,
|
||||||
broker: rdbClient,
|
broker: rdbClient,
|
||||||
ss: ss,
|
ss: ss,
|
||||||
@@ -391,88 +390,6 @@ func TestPerform(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCreateContextWithTimeRestrictions(t *testing.T) {
|
|
||||||
var (
|
|
||||||
noTimeout = time.Duration(0)
|
|
||||||
noDeadline = time.Time{}
|
|
||||||
)
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
desc string
|
|
||||||
timeout time.Duration
|
|
||||||
deadline time.Time
|
|
||||||
wantDeadline time.Time
|
|
||||||
}{
|
|
||||||
{"only with timeout", 10 * time.Second, noDeadline, time.Now().Add(10 * time.Second)},
|
|
||||||
{"only with deadline", noTimeout, time.Now().Add(time.Hour), time.Now().Add(time.Hour)},
|
|
||||||
{"with timeout and deadline (timeout < deadline)", 10 * time.Second, time.Now().Add(time.Hour), time.Now().Add(10 * time.Second)},
|
|
||||||
{"with timeout and deadline (timeout > deadline)", 10 * time.Minute, time.Now().Add(30 * time.Second), time.Now().Add(30 * time.Second)},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range tests {
|
|
||||||
msg := &base.TaskMessage{
|
|
||||||
Type: "something",
|
|
||||||
ID: xid.New(),
|
|
||||||
Timeout: tc.timeout.String(),
|
|
||||||
Deadline: tc.deadline.Format(time.RFC3339),
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx, cancel := createContext(msg)
|
|
||||||
|
|
||||||
select {
|
|
||||||
case x := <-ctx.Done():
|
|
||||||
t.Errorf("%s: <-ctx.Done() == %v, want nothing (it should block)", tc.desc, x)
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
|
|
||||||
got, ok := ctx.Deadline()
|
|
||||||
if !ok {
|
|
||||||
t.Errorf("%s: ctx.Deadline() returned false, want deadline to be set", tc.desc)
|
|
||||||
}
|
|
||||||
if !cmp.Equal(tc.wantDeadline, got, cmpopts.EquateApproxTime(time.Second)) {
|
|
||||||
t.Errorf("%s: ctx.Deadline() returned %v, want %v", tc.desc, got, tc.wantDeadline)
|
|
||||||
}
|
|
||||||
|
|
||||||
cancel()
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
default:
|
|
||||||
t.Errorf("ctx.Done() blocked, want it to be non-blocking")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCreateContextWithoutTimeRestrictions(t *testing.T) {
|
|
||||||
msg := &base.TaskMessage{
|
|
||||||
Type: "something",
|
|
||||||
ID: xid.New(),
|
|
||||||
Timeout: time.Duration(0).String(), // zero value to indicate no timeout
|
|
||||||
Deadline: time.Time{}.Format(time.RFC3339), // zero value to indicate no deadline
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx, cancel := createContext(msg)
|
|
||||||
|
|
||||||
select {
|
|
||||||
case x := <-ctx.Done():
|
|
||||||
t.Errorf("<-ctx.Done() == %v, want nothing (it should block)", x)
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
|
|
||||||
_, ok := ctx.Deadline()
|
|
||||||
if ok {
|
|
||||||
t.Error("ctx.Deadline() returned true, want deadline to not be set")
|
|
||||||
}
|
|
||||||
|
|
||||||
cancel()
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
default:
|
|
||||||
t.Error("ctx.Done() blocked, want it to be non-blocking")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGCD(t *testing.T) {
|
func TestGCD(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
input []int
|
input []int
|
||||||
|
26
scheduler.go
26
scheduler.go
@@ -9,10 +9,11 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/hibiken/asynq/internal/base"
|
"github.com/hibiken/asynq/internal/base"
|
||||||
|
"github.com/hibiken/asynq/internal/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
type scheduler struct {
|
type scheduler struct {
|
||||||
logger Logger
|
logger *log.Logger
|
||||||
broker base.Broker
|
broker base.Broker
|
||||||
|
|
||||||
// channel to communicate back to the long running "scheduler" goroutine.
|
// channel to communicate back to the long running "scheduler" goroutine.
|
||||||
@@ -25,22 +26,29 @@ type scheduler struct {
|
|||||||
qnames []string
|
qnames []string
|
||||||
}
|
}
|
||||||
|
|
||||||
func newScheduler(l Logger, b base.Broker, avgInterval time.Duration, qcfg map[string]int) *scheduler {
|
type schedulerParams struct {
|
||||||
|
logger *log.Logger
|
||||||
|
broker base.Broker
|
||||||
|
interval time.Duration
|
||||||
|
queues map[string]int
|
||||||
|
}
|
||||||
|
|
||||||
|
func newScheduler(params schedulerParams) *scheduler {
|
||||||
var qnames []string
|
var qnames []string
|
||||||
for q := range qcfg {
|
for q := range params.queues {
|
||||||
qnames = append(qnames, q)
|
qnames = append(qnames, q)
|
||||||
}
|
}
|
||||||
return &scheduler{
|
return &scheduler{
|
||||||
logger: l,
|
logger: params.logger,
|
||||||
broker: b,
|
broker: params.broker,
|
||||||
done: make(chan struct{}),
|
done: make(chan struct{}),
|
||||||
avgInterval: avgInterval,
|
avgInterval: params.interval,
|
||||||
qnames: qnames,
|
qnames: qnames,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *scheduler) terminate() {
|
func (s *scheduler) terminate() {
|
||||||
s.logger.Info("Scheduler shutting down...")
|
s.logger.Debug("Scheduler shutting down...")
|
||||||
// Signal the scheduler goroutine to stop polling.
|
// Signal the scheduler goroutine to stop polling.
|
||||||
s.done <- struct{}{}
|
s.done <- struct{}{}
|
||||||
}
|
}
|
||||||
@@ -53,7 +61,7 @@ func (s *scheduler) start(wg *sync.WaitGroup) {
|
|||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-s.done:
|
case <-s.done:
|
||||||
s.logger.Info("Scheduler done")
|
s.logger.Debug("Scheduler done")
|
||||||
return
|
return
|
||||||
case <-time.After(s.avgInterval):
|
case <-time.After(s.avgInterval):
|
||||||
s.exec()
|
s.exec()
|
||||||
@@ -64,6 +72,6 @@ func (s *scheduler) start(wg *sync.WaitGroup) {
|
|||||||
|
|
||||||
func (s *scheduler) exec() {
|
func (s *scheduler) exec() {
|
||||||
if err := s.broker.CheckAndEnqueue(s.qnames...); err != nil {
|
if err := s.broker.CheckAndEnqueue(s.qnames...); err != nil {
|
||||||
s.logger.Error("Could not enqueue scheduled tasks: %v", err)
|
s.logger.Errorf("Could not enqueue scheduled tasks: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -19,7 +19,12 @@ func TestScheduler(t *testing.T) {
|
|||||||
r := setup(t)
|
r := setup(t)
|
||||||
rdbClient := rdb.NewRDB(r)
|
rdbClient := rdb.NewRDB(r)
|
||||||
const pollInterval = time.Second
|
const pollInterval = time.Second
|
||||||
s := newScheduler(testLogger, rdbClient, pollInterval, defaultQueueConfig)
|
s := newScheduler(schedulerParams{
|
||||||
|
logger: testLogger,
|
||||||
|
broker: rdbClient,
|
||||||
|
interval: pollInterval,
|
||||||
|
queues: defaultQueueConfig,
|
||||||
|
})
|
||||||
t1 := h.NewTaskMessage("gen_thumbnail", nil)
|
t1 := h.NewTaskMessage("gen_thumbnail", nil)
|
||||||
t2 := h.NewTaskMessage("send_email", nil)
|
t2 := h.NewTaskMessage("send_email", nil)
|
||||||
t3 := h.NewTaskMessage("reindex", nil)
|
t3 := h.NewTaskMessage("reindex", nil)
|
||||||
|
@@ -15,7 +15,7 @@ import (
|
|||||||
// ServeMux is a multiplexer for asynchronous tasks.
|
// ServeMux is a multiplexer for asynchronous tasks.
|
||||||
// It matches the type of each task against a list of registered patterns
|
// It matches the type of each task against a list of registered patterns
|
||||||
// and calls the handler for the pattern that most closely matches the
|
// and calls the handler for the pattern that most closely matches the
|
||||||
// taks's type name.
|
// task's type name.
|
||||||
//
|
//
|
||||||
// Longer patterns take precedence over shorter ones, so that if there are
|
// Longer patterns take precedence over shorter ones, so that if there are
|
||||||
// handlers registered for both "images" and "images:thumbnails",
|
// handlers registered for both "images" and "images:thumbnails",
|
||||||
|
152
server.go
152
server.go
@@ -12,6 +12,7 @@ import (
|
|||||||
"math/rand"
|
"math/rand"
|
||||||
"os"
|
"os"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -35,7 +36,7 @@ import (
|
|||||||
type Server struct {
|
type Server struct {
|
||||||
ss *base.ServerState
|
ss *base.ServerState
|
||||||
|
|
||||||
logger Logger
|
logger *log.Logger
|
||||||
|
|
||||||
broker base.Broker
|
broker base.Broker
|
||||||
|
|
||||||
@@ -112,6 +113,11 @@ type Config struct {
|
|||||||
// If unset, default logger is used.
|
// If unset, default logger is used.
|
||||||
Logger Logger
|
Logger Logger
|
||||||
|
|
||||||
|
// LogLevel specifies the minimum log level to enable.
|
||||||
|
//
|
||||||
|
// If unset, InfoLevel is used by default.
|
||||||
|
LogLevel LogLevel
|
||||||
|
|
||||||
// ShutdownTimeout specifies the duration to wait to let workers finish their tasks
|
// ShutdownTimeout specifies the duration to wait to let workers finish their tasks
|
||||||
// before forcing them to abort when stopping the server.
|
// before forcing them to abort when stopping the server.
|
||||||
//
|
//
|
||||||
@@ -133,23 +139,104 @@ func (fn ErrorHandlerFunc) HandleError(task *Task, err error, retried, maxRetry
|
|||||||
fn(task, err, retried, maxRetry)
|
fn(task, err, retried, maxRetry)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Logger implements logging with various log levels.
|
// Logger supports logging at various log levels.
|
||||||
type Logger interface {
|
type Logger interface {
|
||||||
// Debug logs a message at Debug level.
|
// Debug logs a message at Debug level.
|
||||||
Debug(format string, args ...interface{})
|
Debug(args ...interface{})
|
||||||
|
|
||||||
// Info logs a message at Info level.
|
// Info logs a message at Info level.
|
||||||
Info(format string, args ...interface{})
|
Info(args ...interface{})
|
||||||
|
|
||||||
// Warn logs a message at Warning level.
|
// Warn logs a message at Warning level.
|
||||||
Warn(format string, args ...interface{})
|
Warn(args ...interface{})
|
||||||
|
|
||||||
// Error logs a message at Error level.
|
// Error logs a message at Error level.
|
||||||
Error(format string, args ...interface{})
|
Error(args ...interface{})
|
||||||
|
|
||||||
// Fatal logs a message at Fatal level
|
// Fatal logs a message at Fatal level
|
||||||
// and process will exit with status set to 1.
|
// and process will exit with status set to 1.
|
||||||
Fatal(format string, args ...interface{})
|
Fatal(args ...interface{})
|
||||||
|
}
|
||||||
|
|
||||||
|
// LogLevel represents logging level.
|
||||||
|
//
|
||||||
|
// It satisfies flag.Value interface.
|
||||||
|
type LogLevel int32
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Note: reserving value zero to differentiate unspecified case.
|
||||||
|
level_unspecified LogLevel = iota
|
||||||
|
|
||||||
|
// DebugLevel is the lowest level of logging.
|
||||||
|
// Debug logs are intended for debugging and development purposes.
|
||||||
|
DebugLevel
|
||||||
|
|
||||||
|
// InfoLevel is used for general informational log messages.
|
||||||
|
InfoLevel
|
||||||
|
|
||||||
|
// WarnLevel is used for undesired but relatively expected events,
|
||||||
|
// which may indicate a problem.
|
||||||
|
WarnLevel
|
||||||
|
|
||||||
|
// ErrorLevel is used for undesired and unexpected events that
|
||||||
|
// the program can recover from.
|
||||||
|
ErrorLevel
|
||||||
|
|
||||||
|
// FatalLevel is used for undesired and unexpected events that
|
||||||
|
// the program cannot recover from.
|
||||||
|
FatalLevel
|
||||||
|
)
|
||||||
|
|
||||||
|
// String is part of the flag.Value interface.
|
||||||
|
func (l *LogLevel) String() string {
|
||||||
|
switch *l {
|
||||||
|
case DebugLevel:
|
||||||
|
return "debug"
|
||||||
|
case InfoLevel:
|
||||||
|
return "info"
|
||||||
|
case WarnLevel:
|
||||||
|
return "warn"
|
||||||
|
case ErrorLevel:
|
||||||
|
return "error"
|
||||||
|
case FatalLevel:
|
||||||
|
return "fatal"
|
||||||
|
}
|
||||||
|
panic(fmt.Sprintf("asynq: unexpected log level: %v", *l))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set is part of the flag.Value interface.
|
||||||
|
func (l *LogLevel) Set(val string) error {
|
||||||
|
switch strings.ToLower(val) {
|
||||||
|
case "debug":
|
||||||
|
*l = DebugLevel
|
||||||
|
case "info":
|
||||||
|
*l = InfoLevel
|
||||||
|
case "warn", "warning":
|
||||||
|
*l = WarnLevel
|
||||||
|
case "error":
|
||||||
|
*l = ErrorLevel
|
||||||
|
case "fatal":
|
||||||
|
*l = FatalLevel
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("asynq: unsupported log level %q", val)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func toInternalLogLevel(l LogLevel) log.Level {
|
||||||
|
switch l {
|
||||||
|
case DebugLevel:
|
||||||
|
return log.DebugLevel
|
||||||
|
case InfoLevel:
|
||||||
|
return log.InfoLevel
|
||||||
|
case WarnLevel:
|
||||||
|
return log.WarnLevel
|
||||||
|
case ErrorLevel:
|
||||||
|
return log.ErrorLevel
|
||||||
|
case FatalLevel:
|
||||||
|
return log.FatalLevel
|
||||||
|
}
|
||||||
|
panic(fmt.Sprintf("asynq: unexpected log level: %v", l))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Formula taken from https://github.com/mperham/sidekiq.
|
// Formula taken from https://github.com/mperham/sidekiq.
|
||||||
@@ -185,14 +272,16 @@ func NewServer(r RedisConnOpt, cfg Config) *Server {
|
|||||||
if len(queues) == 0 {
|
if len(queues) == 0 {
|
||||||
queues = defaultQueueConfig
|
queues = defaultQueueConfig
|
||||||
}
|
}
|
||||||
logger := cfg.Logger
|
|
||||||
if logger == nil {
|
|
||||||
logger = log.NewLogger(os.Stderr)
|
|
||||||
}
|
|
||||||
shutdownTimeout := cfg.ShutdownTimeout
|
shutdownTimeout := cfg.ShutdownTimeout
|
||||||
if shutdownTimeout == 0 {
|
if shutdownTimeout == 0 {
|
||||||
shutdownTimeout = defaultShutdownTimeout
|
shutdownTimeout = defaultShutdownTimeout
|
||||||
}
|
}
|
||||||
|
logger := log.NewLogger(cfg.Logger)
|
||||||
|
loglevel := cfg.LogLevel
|
||||||
|
if loglevel == level_unspecified {
|
||||||
|
loglevel = InfoLevel
|
||||||
|
}
|
||||||
|
logger.SetLevel(toInternalLogLevel(loglevel))
|
||||||
|
|
||||||
host, err := os.Hostname()
|
host, err := os.Hostname()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -204,11 +293,30 @@ func NewServer(r RedisConnOpt, cfg Config) *Server {
|
|||||||
ss := base.NewServerState(host, pid, n, queues, cfg.StrictPriority)
|
ss := base.NewServerState(host, pid, n, queues, cfg.StrictPriority)
|
||||||
syncCh := make(chan *syncRequest)
|
syncCh := make(chan *syncRequest)
|
||||||
cancels := base.NewCancelations()
|
cancels := base.NewCancelations()
|
||||||
syncer := newSyncer(logger, syncCh, 5*time.Second)
|
|
||||||
heartbeater := newHeartbeater(logger, rdb, ss, 5*time.Second)
|
syncer := newSyncer(syncerParams{
|
||||||
scheduler := newScheduler(logger, rdb, 5*time.Second, queues)
|
logger: logger,
|
||||||
subscriber := newSubscriber(logger, rdb, cancels)
|
requestsCh: syncCh,
|
||||||
processor := newProcessor(newProcessorParams{
|
interval: 5 * time.Second,
|
||||||
|
})
|
||||||
|
heartbeater := newHeartbeater(heartbeaterParams{
|
||||||
|
logger: logger,
|
||||||
|
broker: rdb,
|
||||||
|
serverState: ss,
|
||||||
|
interval: 5 * time.Second,
|
||||||
|
})
|
||||||
|
scheduler := newScheduler(schedulerParams{
|
||||||
|
logger: logger,
|
||||||
|
broker: rdb,
|
||||||
|
interval: 5 * time.Second,
|
||||||
|
queues: queues,
|
||||||
|
})
|
||||||
|
subscriber := newSubscriber(subscriberParams{
|
||||||
|
logger: logger,
|
||||||
|
broker: rdb,
|
||||||
|
cancelations: cancels,
|
||||||
|
})
|
||||||
|
processor := newProcessor(processorParams{
|
||||||
logger: logger,
|
logger: logger,
|
||||||
broker: rdb,
|
broker: rdb,
|
||||||
ss: ss,
|
ss: ss,
|
||||||
@@ -291,13 +399,6 @@ func (srv *Server) Start(handler Handler) error {
|
|||||||
srv.ss.SetStatus(base.StatusRunning)
|
srv.ss.SetStatus(base.StatusRunning)
|
||||||
srv.processor.handler = handler
|
srv.processor.handler = handler
|
||||||
|
|
||||||
type prefixLogger interface {
|
|
||||||
SetPrefix(prefix string)
|
|
||||||
}
|
|
||||||
// If logger supports setting prefix, then set prefix for log output.
|
|
||||||
if l, ok := srv.logger.(prefixLogger); ok {
|
|
||||||
l.SetPrefix(fmt.Sprintf("asynq: pid=%d ", os.Getpid()))
|
|
||||||
}
|
|
||||||
srv.logger.Info("Starting processing")
|
srv.logger.Info("Starting processing")
|
||||||
|
|
||||||
srv.heartbeater.start(&srv.wg)
|
srv.heartbeater.start(&srv.wg)
|
||||||
@@ -319,7 +420,6 @@ func (srv *Server) Stop() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println() // print newline for prettier log.
|
|
||||||
srv.logger.Info("Starting graceful shutdown")
|
srv.logger.Info("Starting graceful shutdown")
|
||||||
// Note: The order of termination is important.
|
// Note: The order of termination is important.
|
||||||
// Sender goroutines should be terminated before the receiver goroutines.
|
// Sender goroutines should be terminated before the receiver goroutines.
|
||||||
@@ -335,12 +435,14 @@ func (srv *Server) Stop() {
|
|||||||
srv.broker.Close()
|
srv.broker.Close()
|
||||||
srv.ss.SetStatus(base.StatusStopped)
|
srv.ss.SetStatus(base.StatusStopped)
|
||||||
|
|
||||||
srv.logger.Info("Bye!")
|
srv.logger.Info("Exiting")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Quiet signals the server to stop pulling new tasks off queues.
|
// Quiet signals the server to stop pulling new tasks off queues.
|
||||||
// Quiet should be used before stopping the server.
|
// Quiet should be used before stopping the server.
|
||||||
func (srv *Server) Quiet() {
|
func (srv *Server) Quiet() {
|
||||||
|
srv.logger.Info("Stopping processor")
|
||||||
srv.processor.stop()
|
srv.processor.stop()
|
||||||
srv.ss.SetStatus(base.StatusQuiet)
|
srv.ss.SetStatus(base.StatusQuiet)
|
||||||
|
srv.logger.Info("Processor stopped")
|
||||||
}
|
}
|
||||||
|
@@ -28,6 +28,7 @@ func TestServer(t *testing.T) {
|
|||||||
c := NewClient(r)
|
c := NewClient(r)
|
||||||
srv := NewServer(r, Config{
|
srv := NewServer(r, Config{
|
||||||
Concurrency: 10,
|
Concurrency: 10,
|
||||||
|
LogLevel: testLogLevel,
|
||||||
})
|
})
|
||||||
|
|
||||||
// no-op handler
|
// no-op handler
|
||||||
@@ -58,7 +59,7 @@ func TestServerRun(t *testing.T) {
|
|||||||
ignoreOpt := goleak.IgnoreTopFunction("github.com/go-redis/redis/v7/internal/pool.(*ConnPool).reaper")
|
ignoreOpt := goleak.IgnoreTopFunction("github.com/go-redis/redis/v7/internal/pool.(*ConnPool).reaper")
|
||||||
defer goleak.VerifyNoLeaks(t, ignoreOpt)
|
defer goleak.VerifyNoLeaks(t, ignoreOpt)
|
||||||
|
|
||||||
srv := NewServer(RedisClientOpt{Addr: ":6379"}, Config{})
|
srv := NewServer(RedisClientOpt{Addr: ":6379"}, Config{LogLevel: testLogLevel})
|
||||||
|
|
||||||
done := make(chan struct{})
|
done := make(chan struct{})
|
||||||
// Make sure server exits when receiving TERM signal.
|
// Make sure server exits when receiving TERM signal.
|
||||||
@@ -83,7 +84,7 @@ func TestServerRun(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestServerErrServerStopped(t *testing.T) {
|
func TestServerErrServerStopped(t *testing.T) {
|
||||||
srv := NewServer(RedisClientOpt{Addr: ":6379"}, Config{})
|
srv := NewServer(RedisClientOpt{Addr: ":6379"}, Config{LogLevel: testLogLevel})
|
||||||
handler := NewServeMux()
|
handler := NewServeMux()
|
||||||
if err := srv.Start(handler); err != nil {
|
if err := srv.Start(handler); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@@ -96,7 +97,7 @@ func TestServerErrServerStopped(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestServerErrNilHandler(t *testing.T) {
|
func TestServerErrNilHandler(t *testing.T) {
|
||||||
srv := NewServer(RedisClientOpt{Addr: ":6379"}, Config{})
|
srv := NewServer(RedisClientOpt{Addr: ":6379"}, Config{LogLevel: testLogLevel})
|
||||||
err := srv.Start(nil)
|
err := srv.Start(nil)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Error("Starting server with nil handler: (*Server).Start(nil) did not return error")
|
t.Error("Starting server with nil handler: (*Server).Start(nil) did not return error")
|
||||||
@@ -105,7 +106,7 @@ func TestServerErrNilHandler(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestServerErrServerRunning(t *testing.T) {
|
func TestServerErrServerRunning(t *testing.T) {
|
||||||
srv := NewServer(RedisClientOpt{Addr: ":6379"}, Config{})
|
srv := NewServer(RedisClientOpt{Addr: ":6379"}, Config{LogLevel: testLogLevel})
|
||||||
handler := NewServeMux()
|
handler := NewServeMux()
|
||||||
if err := srv.Start(handler); err != nil {
|
if err := srv.Start(handler); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@@ -126,7 +127,7 @@ func TestServerWithRedisDown(t *testing.T) {
|
|||||||
}()
|
}()
|
||||||
r := rdb.NewRDB(setup(t))
|
r := rdb.NewRDB(setup(t))
|
||||||
testBroker := testbroker.NewTestBroker(r)
|
testBroker := testbroker.NewTestBroker(r)
|
||||||
srv := NewServer(RedisClientOpt{Addr: ":6379"}, Config{})
|
srv := NewServer(RedisClientOpt{Addr: ":6379"}, Config{LogLevel: testLogLevel})
|
||||||
srv.broker = testBroker
|
srv.broker = testBroker
|
||||||
srv.scheduler.broker = testBroker
|
srv.scheduler.broker = testBroker
|
||||||
srv.heartbeater.broker = testBroker
|
srv.heartbeater.broker = testBroker
|
||||||
@@ -158,7 +159,7 @@ func TestServerWithFlakyBroker(t *testing.T) {
|
|||||||
}()
|
}()
|
||||||
r := rdb.NewRDB(setup(t))
|
r := rdb.NewRDB(setup(t))
|
||||||
testBroker := testbroker.NewTestBroker(r)
|
testBroker := testbroker.NewTestBroker(r)
|
||||||
srv := NewServer(RedisClientOpt{Addr: redisAddr, DB: redisDB}, Config{})
|
srv := NewServer(RedisClientOpt{Addr: redisAddr, DB: redisDB}, Config{LogLevel: testLogLevel})
|
||||||
srv.broker = testBroker
|
srv.broker = testBroker
|
||||||
srv.scheduler.broker = testBroker
|
srv.scheduler.broker = testBroker
|
||||||
srv.heartbeater.broker = testBroker
|
srv.heartbeater.broker = testBroker
|
||||||
@@ -208,3 +209,32 @@ func TestServerWithFlakyBroker(t *testing.T) {
|
|||||||
|
|
||||||
srv.Stop()
|
srv.Stop()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestLogLevel(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
flagVal string
|
||||||
|
want LogLevel
|
||||||
|
wantStr string
|
||||||
|
}{
|
||||||
|
{"debug", DebugLevel, "debug"},
|
||||||
|
{"Info", InfoLevel, "info"},
|
||||||
|
{"WARN", WarnLevel, "warn"},
|
||||||
|
{"warning", WarnLevel, "warn"},
|
||||||
|
{"Error", ErrorLevel, "error"},
|
||||||
|
{"fatal", FatalLevel, "fatal"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
level := new(LogLevel)
|
||||||
|
if err := level.Set(tc.flagVal); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if *level != tc.want {
|
||||||
|
t.Errorf("Set(%q): got %v, want %v", tc.flagVal, level, &tc.want)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if got := level.String(); got != tc.wantStr {
|
||||||
|
t.Errorf("String() returned %q, want %q", got, tc.wantStr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -10,10 +10,11 @@ import (
|
|||||||
|
|
||||||
"github.com/go-redis/redis/v7"
|
"github.com/go-redis/redis/v7"
|
||||||
"github.com/hibiken/asynq/internal/base"
|
"github.com/hibiken/asynq/internal/base"
|
||||||
|
"github.com/hibiken/asynq/internal/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
type subscriber struct {
|
type subscriber struct {
|
||||||
logger Logger
|
logger *log.Logger
|
||||||
broker base.Broker
|
broker base.Broker
|
||||||
|
|
||||||
// channel to communicate back to the long running "subscriber" goroutine.
|
// channel to communicate back to the long running "subscriber" goroutine.
|
||||||
@@ -26,18 +27,24 @@ type subscriber struct {
|
|||||||
retryTimeout time.Duration
|
retryTimeout time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
func newSubscriber(l Logger, b base.Broker, cancelations *base.Cancelations) *subscriber {
|
type subscriberParams struct {
|
||||||
|
logger *log.Logger
|
||||||
|
broker base.Broker
|
||||||
|
cancelations *base.Cancelations
|
||||||
|
}
|
||||||
|
|
||||||
|
func newSubscriber(params subscriberParams) *subscriber {
|
||||||
return &subscriber{
|
return &subscriber{
|
||||||
logger: l,
|
logger: params.logger,
|
||||||
broker: b,
|
broker: params.broker,
|
||||||
done: make(chan struct{}),
|
done: make(chan struct{}),
|
||||||
cancelations: cancelations,
|
cancelations: params.cancelations,
|
||||||
retryTimeout: 5 * time.Second,
|
retryTimeout: 5 * time.Second,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *subscriber) terminate() {
|
func (s *subscriber) terminate() {
|
||||||
s.logger.Info("Subscriber shutting down...")
|
s.logger.Debug("Subscriber shutting down...")
|
||||||
// Signal the subscriber goroutine to stop.
|
// Signal the subscriber goroutine to stop.
|
||||||
s.done <- struct{}{}
|
s.done <- struct{}{}
|
||||||
}
|
}
|
||||||
@@ -54,12 +61,12 @@ func (s *subscriber) start(wg *sync.WaitGroup) {
|
|||||||
for {
|
for {
|
||||||
pubsub, err = s.broker.CancelationPubSub()
|
pubsub, err = s.broker.CancelationPubSub()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.logger.Error("cannot subscribe to cancelation channel: %v", err)
|
s.logger.Errorf("cannot subscribe to cancelation channel: %v", err)
|
||||||
select {
|
select {
|
||||||
case <-time.After(s.retryTimeout):
|
case <-time.After(s.retryTimeout):
|
||||||
continue
|
continue
|
||||||
case <-s.done:
|
case <-s.done:
|
||||||
s.logger.Info("Subscriber done")
|
s.logger.Debug("Subscriber done")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -70,7 +77,7 @@ func (s *subscriber) start(wg *sync.WaitGroup) {
|
|||||||
select {
|
select {
|
||||||
case <-s.done:
|
case <-s.done:
|
||||||
pubsub.Close()
|
pubsub.Close()
|
||||||
s.logger.Info("Subscriber done")
|
s.logger.Debug("Subscriber done")
|
||||||
return
|
return
|
||||||
case msg := <-cancelCh:
|
case msg := <-cancelCh:
|
||||||
cancel, ok := s.cancelations.Get(msg.Payload)
|
cancel, ok := s.cancelations.Get(msg.Payload)
|
||||||
|
@@ -38,7 +38,11 @@ func TestSubscriber(t *testing.T) {
|
|||||||
cancelations := base.NewCancelations()
|
cancelations := base.NewCancelations()
|
||||||
cancelations.Add(tc.registeredID, fakeCancelFunc)
|
cancelations.Add(tc.registeredID, fakeCancelFunc)
|
||||||
|
|
||||||
subscriber := newSubscriber(testLogger, rdbClient, cancelations)
|
subscriber := newSubscriber(subscriberParams{
|
||||||
|
logger: testLogger,
|
||||||
|
broker: rdbClient,
|
||||||
|
cancelations: cancelations,
|
||||||
|
})
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
subscriber.start(&wg)
|
subscriber.start(&wg)
|
||||||
defer subscriber.terminate()
|
defer subscriber.terminate()
|
||||||
@@ -75,7 +79,11 @@ func TestSubscriberWithRedisDown(t *testing.T) {
|
|||||||
testBroker := testbroker.NewTestBroker(r)
|
testBroker := testbroker.NewTestBroker(r)
|
||||||
|
|
||||||
cancelations := base.NewCancelations()
|
cancelations := base.NewCancelations()
|
||||||
subscriber := newSubscriber(testLogger, testBroker, cancelations)
|
subscriber := newSubscriber(subscriberParams{
|
||||||
|
logger: testLogger,
|
||||||
|
broker: testBroker,
|
||||||
|
cancelations: cancelations,
|
||||||
|
})
|
||||||
subscriber.retryTimeout = 1 * time.Second // set shorter retry timeout for testing purpose.
|
subscriber.retryTimeout = 1 * time.Second // set shorter retry timeout for testing purpose.
|
||||||
|
|
||||||
testBroker.Sleep() // simulate a situation where subscriber cannot connect to redis.
|
testBroker.Sleep() // simulate a situation where subscriber cannot connect to redis.
|
||||||
|
22
syncer.go
22
syncer.go
@@ -7,12 +7,14 @@ package asynq
|
|||||||
import (
|
import (
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/hibiken/asynq/internal/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
// syncer is responsible for queuing up failed requests to redis and retry
|
// syncer is responsible for queuing up failed requests to redis and retry
|
||||||
// those requests to sync state between the background process and redis.
|
// those requests to sync state between the background process and redis.
|
||||||
type syncer struct {
|
type syncer struct {
|
||||||
logger Logger
|
logger *log.Logger
|
||||||
|
|
||||||
requestsCh <-chan *syncRequest
|
requestsCh <-chan *syncRequest
|
||||||
|
|
||||||
@@ -28,17 +30,23 @@ type syncRequest struct {
|
|||||||
errMsg string // error message
|
errMsg string // error message
|
||||||
}
|
}
|
||||||
|
|
||||||
func newSyncer(l Logger, requestsCh <-chan *syncRequest, interval time.Duration) *syncer {
|
type syncerParams struct {
|
||||||
|
logger *log.Logger
|
||||||
|
requestsCh <-chan *syncRequest
|
||||||
|
interval time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
func newSyncer(params syncerParams) *syncer {
|
||||||
return &syncer{
|
return &syncer{
|
||||||
logger: l,
|
logger: params.logger,
|
||||||
requestsCh: requestsCh,
|
requestsCh: params.requestsCh,
|
||||||
done: make(chan struct{}),
|
done: make(chan struct{}),
|
||||||
interval: interval,
|
interval: params.interval,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *syncer) terminate() {
|
func (s *syncer) terminate() {
|
||||||
s.logger.Info("Syncer shutting down...")
|
s.logger.Debug("Syncer shutting down...")
|
||||||
// Signal the syncer goroutine to stop.
|
// Signal the syncer goroutine to stop.
|
||||||
s.done <- struct{}{}
|
s.done <- struct{}{}
|
||||||
}
|
}
|
||||||
@@ -57,7 +65,7 @@ func (s *syncer) start(wg *sync.WaitGroup) {
|
|||||||
s.logger.Error(req.errMsg)
|
s.logger.Error(req.errMsg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
s.logger.Info("Syncer done")
|
s.logger.Debug("Syncer done")
|
||||||
return
|
return
|
||||||
case req := <-s.requestsCh:
|
case req := <-s.requestsCh:
|
||||||
requests = append(requests, req)
|
requests = append(requests, req)
|
||||||
|
@@ -27,7 +27,11 @@ func TestSyncer(t *testing.T) {
|
|||||||
|
|
||||||
const interval = time.Second
|
const interval = time.Second
|
||||||
syncRequestCh := make(chan *syncRequest)
|
syncRequestCh := make(chan *syncRequest)
|
||||||
syncer := newSyncer(testLogger, syncRequestCh, interval)
|
syncer := newSyncer(syncerParams{
|
||||||
|
logger: testLogger,
|
||||||
|
requestsCh: syncRequestCh,
|
||||||
|
interval: interval,
|
||||||
|
})
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
syncer.start(&wg)
|
syncer.start(&wg)
|
||||||
defer syncer.terminate()
|
defer syncer.terminate()
|
||||||
@@ -52,7 +56,11 @@ func TestSyncer(t *testing.T) {
|
|||||||
func TestSyncerRetry(t *testing.T) {
|
func TestSyncerRetry(t *testing.T) {
|
||||||
const interval = time.Second
|
const interval = time.Second
|
||||||
syncRequestCh := make(chan *syncRequest)
|
syncRequestCh := make(chan *syncRequest)
|
||||||
syncer := newSyncer(testLogger, syncRequestCh, interval)
|
syncer := newSyncer(syncerParams{
|
||||||
|
logger: testLogger,
|
||||||
|
requestsCh: syncRequestCh,
|
||||||
|
interval: interval,
|
||||||
|
})
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
syncer.start(&wg)
|
syncer.start(&wg)
|
||||||
|
Reference in New Issue
Block a user