Add redis support (#44)

This commit is contained in:
Adriano Caloiaro 2023-04-14 19:11:22 -06:00 committed by GitHub
parent 219ea84d76
commit aaed37eeb5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 933 additions and 81 deletions

View file

@ -20,6 +20,13 @@ jobs:
--health-interval 10s --health-interval 10s
--health-timeout 5s --health-timeout 5s
--health-retries 5 --health-retries 5
redis:
image: redis
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5
container: golang:1.20 container: golang:1.20
@ -29,6 +36,7 @@ jobs:
run: make mod test coverage run: make mod test coverage
env: env:
TEST_DATABASE_URL: postgres://postgres:postgres@postgres:5432/postgres TEST_DATABASE_URL: postgres://postgres:postgres@postgres:5432/postgres
TEST_REDIS_URL: redis:6379
- name: upload results - name: upload results
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v3
with: with:

6
.gitignore vendored
View file

@ -18,5 +18,11 @@ vendor/
# Some tests require environment variables to be set # Some tests require environment variables to be set
.env .env
# it's handy to have a docker-compose available during development
docker-compose.yml
.pre-commit-config.yaml
dist/ dist/

View file

@ -10,19 +10,24 @@ Background job processing for Go
# About # About
Neoq is a background job framework for Go applications. Its purpose is to minimize the infrastructure necessary to run production applications. It does so by implementing queue durability with modular backends. Neoq is a queue-agnostic background job framework for Go.
This allows application to use the same type of data store for both application data and backround job processing. At the moment an in-memory and Postgres backends are provided. However, the goal is to have backends for every major datastore: Postgres, Redis, MySQL, etc. Neoq job handlers are the same, whether queues are in-memory for development/testing, or Postgres, Redis, or a custom queue for production -- allowing queue infrastructure to change without code change.
Neoq does not aim to be the _fastest_ background job processor. It aims to be _fast_, _reliable_, and demand a minimal infrastructure footprint. Developing/testing or don't need a durable queue? Use the in-memory queue.
Running an application in production? Use Postgres.
Have higher throughput demands in production? Use Redis.
Neoq does not aim to be the _fastest_ background job processor. It aims to be _fast_, _reliable_, and demand a _minimal infrastructure footprint_.
# What it does # What it does
- **Background job Processing**: Neoq has an in-memory and Postgres backend out of the box. Users may supply their own without changing neoq directly. - **Multiple Backends**: In-memory, Postgres, Redis, or user-supplied custom backends.
- **Retries**: Jobs may be retried a configurable number of times with exponential backoff and jitter to prevent thundering herds - **Retries**: Jobs may be retried a configurable number of times with exponential backoff and jitter to prevent thundering herds
- **Job uniqueness**: jobs are fingerprinted based on their payload and status to prevent job duplication (multiple unprocessed jobs with the same payload cannot be queued) - **Job uniqueness**: jobs are fingerprinted based on their payload and status to prevent job duplication (multiple jobs with the same payload are not re-queued)
- **Deadlines**: Queue handlers can be configured with per-job time deadlines with millisecond accuracy - **Job Timeouts**: Queue handlers can be configured with per-job timeouts with millisecond accuracy
- **Configurable transaction idle time**: Don't let your background worker transactions run away with db resources. By default, worker transactions may idle for 60 seconds.
- **Periodic Jobs**: Jobs can be scheduled periodically using standard cron syntax - **Periodic Jobs**: Jobs can be scheduled periodically using standard cron syntax
- **Future Jobs**: Jobs can be scheduled either for the future or immediate execution - **Future Jobs**: Jobs can be scheduled either for the future or immediate execution
- **Concurrency**: Concurrency is configurable for every queue - **Concurrency**: Concurrency is configurable for every queue
@ -70,6 +75,32 @@ nq.Enqueue(ctx, &jobs.Job{
}) })
``` ```
## Redis
**Example**: Process jobs on the "hello_world" queue and add a job to it using the redis backend
```go
ctx := context.Background()
nq, _ := neoq.New(ctx,
neoq.WithBackend(redis.Backend),
redis.WithAddr("localhost:6379"),
redis.WithPassword(""),
)
nq.Start(ctx, "hello_world", handler.New(func(ctx context.Context) (err error) {
j, _ := jobs.FromContext(ctx)
log.Println("got job id:", j.ID, "messsage:", j.Payload["message"])
return
}))
nq.Enqueue(ctx, &jobs.Job{
Queue: "hello_world",
Payload: map[string]interface{}{
"message": "hello world",
},
})
```
## Postgres ## Postgres
**Example**: Process jobs on the "hello_world" queue and add a job to it using the postgres backend **Example**: Process jobs on the "hello_world" queue and add a job to it using the postgres backend

View file

@ -67,7 +67,7 @@ func Backend(ctx context.Context, opts ...config.Option) (backend types.Backend,
} }
// Enqueue queues jobs to be executed asynchronously // Enqueue queues jobs to be executed asynchronously
func (m *MemBackend) Enqueue(ctx context.Context, job *jobs.Job) (jobID int64, err error) { func (m *MemBackend) Enqueue(ctx context.Context, job *jobs.Job) (jobID string, err error) {
var queueChan chan *jobs.Job var queueChan chan *jobs.Job
var qc any var qc any
var ok bool var ok bool
@ -107,7 +107,7 @@ func (m *MemBackend) Enqueue(ctx context.Context, job *jobs.Job) (jobID int64, e
m.mu.Unlock() m.mu.Unlock()
job.ID = m.jobCount job.ID = m.jobCount
jobID = m.jobCount jobID = fmt.Sprint(m.jobCount)
if job.RunAfter.Equal(now) { if job.RunAfter.Equal(now) {
queueChan <- job queueChan <- job

View file

@ -4,6 +4,7 @@ import (
"context" "context"
"fmt" "fmt"
"log" "log"
"strconv"
"strings" "strings"
"sync" "sync"
"testing" "testing"
@ -186,8 +187,9 @@ func TestFutureJobScheduling(t *testing.T) {
t.Error(err) t.Error(err)
} }
jobID, _ := strconv.ParseInt(jid, 0, 64)
var ok bool var ok bool
if _, ok = testFutureJobs.Load(jid); !ok { if _, ok = testFutureJobs.Load(jobID); !ok {
t.Error(err) t.Error(err)
} }
} }
@ -208,7 +210,7 @@ func TestCron(t *testing.T) {
}) })
h.WithOptions( h.WithOptions(
handler.Deadline(500*time.Millisecond), handler.JobTimeout(500*time.Millisecond),
handler.Concurrency(1), handler.Concurrency(1),
) )

View file

@ -5,7 +5,6 @@ import (
"errors" "errors"
"fmt" "fmt"
"os" "os"
"strconv"
"sync" "sync"
"time" "time"
@ -55,11 +54,11 @@ type contextKey struct{}
var ( var (
txCtxVarKey contextKey txCtxVarKey contextKey
shutdownJobID int64 = -1 // job ID announced when triggering a shutdown shutdownJobID = "-1" // job ID announced when triggering a shutdown
shutdownAnnouncementAllowance = 100 // ms shutdownAnnouncementAllowance = 100 // ms
ErrCnxString = errors.New("invalid connecton string: see documentation for valid connection strings") ErrCnxString = errors.New("invalid connecton string: see documentation for valid connection strings")
ErrDuplicateJobID = errors.New("duplicate job id") ErrDuplicateJobID = errors.New("duplicate job id")
ErrNoTransactionInContext = errors.New("context does not have a Tx set") ErrNoTransactionInContext = errors.New("context does not have a Tx set")
) )
// PgBackend is a Postgres-based Neoq backend // PgBackend is a Postgres-based Neoq backend
@ -70,7 +69,7 @@ type PgBackend struct {
cron *cron.Cron cron *cron.Cron
mu *sync.Mutex // mutex to protect mutating state on a pgWorker mu *sync.Mutex // mutex to protect mutating state on a pgWorker
pool *pgxpool.Pool pool *pgxpool.Pool
futureJobs map[int64]time.Time // map of future job IDs to their due time futureJobs map[string]time.Time // map of future job IDs to their due time
handlers map[string]handler.Handler // a map of queue names to queue handlers handlers map[string]handler.Handler // a map of queue names to queue handlers
cancelFuncs []context.CancelFunc // A collection of cancel functions to be called upon Shutdown() cancelFuncs []context.CancelFunc // A collection of cancel functions to be called upon Shutdown()
} }
@ -104,7 +103,7 @@ func Backend(ctx context.Context, opts ...config.Option) (pb types.Backend, err
mu: &sync.Mutex{}, mu: &sync.Mutex{},
config: config.New(), config: config.New(),
handlers: make(map[string]handler.Handler), handlers: make(map[string]handler.Handler),
futureJobs: make(map[int64]time.Time), futureJobs: make(map[string]time.Time),
logger: slog.New(slog.NewTextHandler(os.Stdout)), logger: slog.New(slog.NewTextHandler(os.Stdout)),
cron: cron.New(), cron: cron.New(),
cancelFuncs: []context.CancelFunc{}, cancelFuncs: []context.CancelFunc{},
@ -321,7 +320,7 @@ func (p *PgBackend) initializeDB(ctx context.Context) (err error) {
} }
// Enqueue adds jobs to the specified queue // Enqueue adds jobs to the specified queue
func (p *PgBackend) Enqueue(ctx context.Context, job *jobs.Job) (jobID int64, err error) { func (p *PgBackend) Enqueue(ctx context.Context, job *jobs.Job) (jobID string, err error) {
ctx, cancel := context.WithCancel(ctx) ctx, cancel := context.WithCancel(ctx)
p.mu.Lock() p.mu.Lock()
p.cancelFuncs = append(p.cancelFuncs, cancel) p.cancelFuncs = append(p.cancelFuncs, cancel)
@ -359,6 +358,7 @@ func (p *PgBackend) Enqueue(ctx context.Context, job *jobs.Job) (jobID int64, er
if err != nil { if err != nil {
err = fmt.Errorf("error enqueuing job: %w", err) err = fmt.Errorf("error enqueuing job: %w", err)
} }
if jobID == jobs.DuplicateJobID { if jobID == jobs.DuplicateJobID {
err = ErrDuplicateJobID err = ErrDuplicateJobID
return return
@ -366,7 +366,7 @@ func (p *PgBackend) Enqueue(ctx context.Context, job *jobs.Job) (jobID int64, er
// notify listeners that a new job has arrived if it's not a future job // notify listeners that a new job has arrived if it's not a future job
if job.RunAfter.Equal(now) { if job.RunAfter.Equal(now) {
_, err = tx.Exec(ctx, fmt.Sprintf("NOTIFY %s, '%d'", job.Queue, jobID)) _, err = tx.Exec(ctx, fmt.Sprintf("NOTIFY %s, '%s'", job.Queue, jobID))
if err != nil { if err != nil {
err = fmt.Errorf("error executing transaction: %w", err) err = fmt.Errorf("error executing transaction: %w", err)
} }
@ -465,7 +465,7 @@ func (p *PgBackend) Shutdown(ctx context.Context) {
// //
// Jobs that are not already fingerprinted are fingerprinted before being added // Jobs that are not already fingerprinted are fingerprinted before being added
// Duplicate jobs are not added to the queue. Any two unprocessed jobs with the same fingerprint are duplicates // Duplicate jobs are not added to the queue. Any two unprocessed jobs with the same fingerprint are duplicates
func (p *PgBackend) enqueueJob(ctx context.Context, tx pgx.Tx, j *jobs.Job) (jobID int64, err error) { func (p *PgBackend) enqueueJob(ctx context.Context, tx pgx.Tx, j *jobs.Job) (jobID string, err error) {
err = jobs.FingerprintJob(j) err = jobs.FingerprintJob(j)
if err != nil { if err != nil {
return return
@ -569,7 +569,7 @@ func (p *PgBackend) updateJob(ctx context.Context, jobErr error) (err error) {
if time.Until(runAfter) > 0 { if time.Until(runAfter) > 0 {
p.mu.Lock() p.mu.Lock()
p.futureJobs[job.ID] = runAfter p.futureJobs[fmt.Sprint(job.ID)] = runAfter
p.mu.Unlock() p.mu.Unlock()
} }
@ -600,7 +600,7 @@ func (p *PgBackend) start(ctx context.Context, queue string) (err error) {
for i := 0; i < h.Concurrency; i++ { for i := 0; i < h.Concurrency; i++ {
go func() { go func() {
var err error var err error
var jobID int64 var jobID string
for { for {
select { select {
@ -630,7 +630,7 @@ func (p *PgBackend) start(ctx context.Context, queue string) (err error) {
} }
// removeFutureJob removes a future job from the in-memory list of jobs that will execute in the future // removeFutureJob removes a future job from the in-memory list of jobs that will execute in the future
func (p *PgBackend) removeFutureJob(jobID int64) { func (p *PgBackend) removeFutureJob(jobID string) {
if _, ok := p.futureJobs[jobID]; ok { if _, ok := p.futureJobs[jobID]; ok {
p.mu.Lock() p.mu.Lock()
delete(p.futureJobs, jobID) delete(p.futureJobs, jobID)
@ -647,7 +647,7 @@ func (p *PgBackend) initFutureJobs(ctx context.Context, queue string) (err error
return return
} }
var id int64 var id string
var runAfter time.Time var runAfter time.Time
_, err = pgx.ForEachRow(rows, []any{&id, &runAfter}, func() error { _, err = pgx.ForEachRow(rows, []any{&id, &runAfter}, func() error {
p.mu.Lock() p.mu.Lock()
@ -675,7 +675,7 @@ func (p *PgBackend) scheduleFutureJobs(ctx context.Context, queue string) {
timeUntillRunAfter := time.Until(runAfter) timeUntillRunAfter := time.Until(runAfter)
if timeUntillRunAfter <= p.config.FutureJobWindow { if timeUntillRunAfter <= p.config.FutureJobWindow {
p.removeFutureJob(jobID) p.removeFutureJob(jobID)
go func(jid int64) { go func(jid string) {
scheduleCh := time.After(timeUntillRunAfter) scheduleCh := time.After(timeUntillRunAfter)
<-scheduleCh <-scheduleCh
p.announceJob(ctx, queue, jid) p.announceJob(ctx, queue, jid)
@ -695,7 +695,7 @@ func (p *PgBackend) scheduleFutureJobs(ctx context.Context, queue string) {
// announceJob announces jobs to queue listeners. // announceJob announces jobs to queue listeners.
// //
// Announced jobs are executed by the first worker to respond to the announcement. // Announced jobs are executed by the first worker to respond to the announcement.
func (p *PgBackend) announceJob(ctx context.Context, queue string, jobID int64) { func (p *PgBackend) announceJob(ctx context.Context, queue, jobID string) {
conn, err := p.pool.Acquire(ctx) conn, err := p.pool.Acquire(ctx)
if err != nil { if err != nil {
return return
@ -712,7 +712,7 @@ func (p *PgBackend) announceJob(ctx context.Context, queue string, jobID int64)
defer func(ctx context.Context) { _ = tx.Rollback(ctx) }(ctx) defer func(ctx context.Context) { _ = tx.Rollback(ctx) }(ctx)
// notify listeners that a job is ready to run // notify listeners that a job is ready to run
_, err = tx.Exec(ctx, fmt.Sprintf("NOTIFY %s, '%d'", queue, jobID)) _, err = tx.Exec(ctx, fmt.Sprintf("NOTIFY %s, '%s'", queue, jobID))
if err != nil { if err != nil {
return return
} }
@ -723,8 +723,8 @@ func (p *PgBackend) announceJob(ctx context.Context, queue string, jobID int64)
} }
} }
func (p *PgBackend) pendingJobs(ctx context.Context, queue string) (jobsCh chan int64) { func (p *PgBackend) pendingJobs(ctx context.Context, queue string) (jobsCh chan string) {
jobsCh = make(chan int64) jobsCh = make(chan string)
conn, err := p.pool.Acquire(ctx) conn, err := p.pool.Acquire(ctx)
if err != nil { if err != nil {
@ -756,7 +756,7 @@ func (p *PgBackend) pendingJobs(ctx context.Context, queue string) (jobsCh chan
// it receives pending, periodic, and retry job ids asynchronously // it receives pending, periodic, and retry job ids asynchronously
// 1. handleJob first creates a transactions inside of which a row lock is acquired for the job to be processed. // 1. handleJob first creates a transactions inside of which a row lock is acquired for the job to be processed.
// 2. handleJob secondly calls the handler on the job, and finally updates the job's status // 2. handleJob secondly calls the handler on the job, and finally updates the job's status
func (p *PgBackend) handleJob(ctx context.Context, jobID int64, h handler.Handler) (err error) { func (p *PgBackend) handleJob(ctx context.Context, jobID string, h handler.Handler) (err error) {
var job *jobs.Job var job *jobs.Job
var tx pgx.Tx var tx pgx.Tx
conn, err := p.pool.Acquire(ctx) conn, err := p.pool.Acquire(ctx)
@ -815,8 +815,8 @@ func (p *PgBackend) handleJob(ctx context.Context, jobID int64, h handler.Handle
// TODO: There is currently no handling of listener disconnects in PgBackend. // TODO: There is currently no handling of listener disconnects in PgBackend.
// This will lead to jobs not getting processed until the worker is restarted. // This will lead to jobs not getting processed until the worker is restarted.
// Implement disconnect handling. // Implement disconnect handling.
func (p *PgBackend) listen(ctx context.Context, queue string) (c chan int64, ready chan bool) { func (p *PgBackend) listen(ctx context.Context, queue string) (c chan string, ready chan bool) {
c = make(chan int64) c = make(chan string)
ready = make(chan bool) ready = make(chan bool)
go func(ctx context.Context) { go func(ctx context.Context) {
@ -849,18 +849,12 @@ func (p *PgBackend) listen(ctx context.Context, queue string) (c chan int64, rea
continue continue
} }
var jobID int64
if jobID, err = strconv.ParseInt(notification.Payload, 0, 64); err != nil {
p.logger.Error("unable to fetch job", err)
continue
}
// check if Shutdown() has been called // check if Shutdown() has been called
if jobID == shutdownJobID { if notification.Payload == shutdownJobID {
return return
} }
c <- jobID c <- notification.Payload
} }
}(ctx) }(ctx)
@ -881,7 +875,7 @@ func (p *PgBackend) release(ctx context.Context, conn *pgxpool.Conn, queue strin
conn.Release() conn.Release()
} }
func (p *PgBackend) getPendingJob(ctx context.Context, tx pgx.Tx, jobID int64) (job *jobs.Job, err error) { func (p *PgBackend) getPendingJob(ctx context.Context, tx pgx.Tx, jobID string) (job *jobs.Job, err error) {
row, err := tx.Query(ctx, PendingJobQuery, jobID) row, err := tx.Query(ctx, PendingJobQuery, jobID)
if err != nil { if err != nil {
return return
@ -895,7 +889,7 @@ func (p *PgBackend) getPendingJob(ctx context.Context, tx pgx.Tx, jobID int64) (
return return
} }
func (p *PgBackend) getPendingJobID(ctx context.Context, conn *pgxpool.Conn, queue string) (jobID int64, err error) { func (p *PgBackend) getPendingJobID(ctx context.Context, conn *pgxpool.Conn, queue string) (jobID string, err error) {
err = conn.QueryRow(ctx, PendingJobIDQuery, queue).Scan(&jobID) err = conn.QueryRow(ctx, PendingJobIDQuery, queue).Scan(&jobID)
return return
} }

View file

@ -174,7 +174,7 @@ func TestCron(t *testing.T) {
}) })
h.WithOptions( h.WithOptions(
handler.Deadline(500*time.Millisecond), handler.JobTimeout(500*time.Millisecond),
handler.Concurrency(1), handler.Concurrency(1),
) )

View file

@ -0,0 +1,312 @@
package redis
import (
"context"
"encoding/json"
"errors"
"fmt"
"log"
"os"
"runtime"
"strings"
"sync"
"time"
"github.com/acaloiaro/neoq/config"
"github.com/acaloiaro/neoq/handler"
"github.com/acaloiaro/neoq/internal"
"github.com/acaloiaro/neoq/jobs"
"github.com/acaloiaro/neoq/logging"
"github.com/acaloiaro/neoq/types"
"github.com/hibiken/asynq"
"github.com/iancoleman/strcase"
"github.com/jsuar/go-cron-descriptor/pkg/crondescriptor"
"golang.org/x/exp/slog"
)
var (
// ErrInvalidAddr indicates that the provided address is not a valid redis connection string
ErrInvalidAddr = errors.New("invalid connecton string: see documentation for valid connection strings")
)
// redisBackend is a Redis-backed neoq backend
type redisBackend struct {
types.Backend
client *asynq.Client
server *asynq.Server
mux *asynq.ServeMux
config *config.Config
logger logging.Logger
mu *sync.Mutex // mutext to protect mutating backend state
taskProvider *MemoryTaskConfigProvider
mgr *asynq.PeriodicTaskManager
}
type MemoryTaskConfigProvider struct {
mu *sync.Mutex
configs []*asynq.PeriodicTaskConfig
}
// NewMemoryTaskConfigProvider returns a new asynq MemoryTaskConfigProvider
func NewMemoryTaskConfigProvider() (p *MemoryTaskConfigProvider) {
p = &MemoryTaskConfigProvider{
mu: &sync.Mutex{},
configs: []*asynq.PeriodicTaskConfig{},
}
return
}
// GetConfigs returns this provider's periodic task configurations
func (m *MemoryTaskConfigProvider) GetConfigs() (c []*asynq.PeriodicTaskConfig, err error) {
m.mu.Lock()
cfgs := m.configs
m.mu.Unlock()
return cfgs, nil
}
// addConfig adds a periodic task configuration to this provider's configs
func (m *MemoryTaskConfigProvider) addConfig(taskConfig *asynq.PeriodicTaskConfig) {
m.mu.Lock()
m.configs = append(m.configs, taskConfig)
m.mu.Unlock()
}
// Backend is a [config.BackendInitializer] that initializes a new Redis-backed neoq backend
func Backend(ctx context.Context, opts ...config.Option) (backend types.Backend, err error) {
b := &redisBackend{
config: config.New(),
mu: &sync.Mutex{},
logger: slog.New(slog.NewTextHandler(os.Stdout)),
taskProvider: NewMemoryTaskConfigProvider(),
}
for _, opt := range opts {
opt(b.config)
}
if b.config.ConnectionString == "" {
err = ErrInvalidAddr
return
}
if b.config.BackendConcurrency <= 0 {
b.config.BackendConcurrency = runtime.NumCPU()
}
clientOpt := asynq.RedisClientOpt{Addr: b.config.ConnectionString}
if b.config.BackendAuthPassword != "" {
clientOpt.Password = b.config.BackendAuthPassword
}
b.client = asynq.NewClient(clientOpt)
b.server = asynq.NewServer(
clientOpt,
asynq.Config{
Concurrency: b.config.BackendConcurrency,
ShutdownTimeout: b.config.ShutdownTimeout,
},
)
b.mux = asynq.NewServeMux()
b.mgr, err = asynq.NewPeriodicTaskManager(
asynq.PeriodicTaskManagerOpts{
RedisConnOpt: clientOpt,
PeriodicTaskConfigProvider: b.taskProvider,
SyncInterval: 500 * time.Millisecond,
SchedulerOpts: &asynq.SchedulerOpts{
PostEnqueueFunc: func(info *asynq.TaskInfo, err error) {
if err != nil {
b.logger.Error("unable to schedule task", err)
}
},
},
})
if err != nil {
err = fmt.Errorf("failed to initialize periodic task manager: %w", err)
log.Fatal(err)
}
go func() {
if err := b.mgr.Run(); err != nil {
log.Fatal(err)
}
}()
go func() {
if err := b.server.Run(b.mux); err != nil {
log.Fatal(err)
}
}()
backend = b
return backend, err
}
// WithAddr configures neoq to connect to Redis with the given address
func WithAddr(addr string) config.Option {
return func(c *config.Config) {
c.ConnectionString = addr
}
}
// WithPassword configures neoq to connect to Redis with the given password
func WithPassword(password string) config.Option {
return func(c *config.Config) {
c.BackendAuthPassword = password
}
}
// WithConcurrency configures the number of workers available to process jobs across all queues
func WithConcurrency(concurrency int) config.Option {
return func(c *config.Config) {
c.BackendConcurrency = concurrency
}
}
// WithShutdownTimeout specifies the duration to wait to let workers finish their tasks
// before forcing them to abort durning Shutdown()
//
// If unset or zero, default timeout of 8 seconds is used.
func WithShutdownTimeout(timeout time.Duration) config.Option {
return func(c *config.Config) {
c.ShutdownTimeout = timeout
}
}
// Enqueue queues jobs to be executed asynchronously
func (b *redisBackend) Enqueue(ctx context.Context, job *jobs.Job) (jobID string, err error) {
if job.Queue == "" {
err = jobs.ErrNoQueueSpecified
return
}
err = jobs.FingerprintJob(job)
if err != nil {
return
}
var payload []byte
payload, err = json.Marshal(job.Payload)
if err != nil {
return
}
task := asynq.NewTask(job.Queue, payload)
if job.RunAfter.IsZero() {
_, err = b.client.EnqueueContext(ctx, task, asynq.TaskID(job.Fingerprint))
} else {
_, err = b.client.EnqueueContext(ctx, task, asynq.TaskID(job.Fingerprint), asynq.ProcessAt(job.RunAfter))
}
if err != nil {
err = fmt.Errorf("unable to enqueue task: %w", err)
}
return job.Fingerprint, err
}
// Start starts processing jobs with the specified queue and handler
func (b *redisBackend) Start(_ context.Context, queue string, h handler.Handler) (err error) {
b.mux.HandleFunc(queue, func(ctx context.Context, t *asynq.Task) (err error) {
var p map[string]any
if err = json.Unmarshal(t.Payload(), &p); err != nil {
b.logger.Info("job has no payload")
}
job := &jobs.Job{
CreatedAt: time.Now(),
Queue: queue,
Payload: p,
}
ctx = withJobContext(ctx, job)
err = handler.Exec(ctx, h)
if err != nil {
b.logger.Error("error handling job", err)
}
return
})
return
}
// StartCron starts processing jobs with the specified cron schedule and handler
//
// See: https://pkg.go.dev/github.com/robfig/cron?#hdr-CRON_Expression_Format for details on the cron spec format
func (b *redisBackend) StartCron(ctx context.Context, cronSpec string, h handler.Handler) (err error) {
cd, err := crondescriptor.NewCronDescriptor(cronSpec)
if err != nil {
return fmt.Errorf("error creating cron descriptor: %w", err)
}
cdStr, err := cd.GetDescription(crondescriptor.Full)
if err != nil {
return fmt.Errorf("error getting cron description: %w", err)
}
queue := internal.StripNonAlphanum(strcase.ToSnake(*cdStr))
err = b.Start(ctx, queue, h)
if err != nil {
return
}
aCronSpec := toAsynqCronspec(cronSpec)
c := &asynq.PeriodicTaskConfig{
Cronspec: aCronSpec,
Task: asynq.NewTask(queue, nil),
}
b.taskProvider.addConfig(c)
return
}
// Asynq does not currently support the seconds field in cron specs. However, it does supports seconds using the
// alternative syntax: @every Xs, where X is the number of seconds between executions
//
// Because of this, when cron specs have six fields (contain seconds), we must convert the seconds field to asynq
// seconds format.
//
// # This implementation is very crude, and unlikely covers the full breadth of the cron spec, panic()ing when necessary
//
// TODO: Refactor if asynq merges https://github.com/hibiken/asynq/pull/644
func toAsynqCronspec(cronSpec string) string {
// nolint: nestif, gomnd
if strings.Count(cronSpec, "*") == 6 {
fields := strings.Fields(cronSpec)
// nolint: gomnd
if len(fields) == 6 {
secondsField := fields[0]
if secondsField == "*" {
return "@every 1s"
}
// Handle cronspec divisor syntax, e.g. */30 is every 30 seconds, or @every 30s
if strings.Contains(secondsField, "/") {
seconds := strings.Split(secondsField, "/")[1]
return fmt.Sprintf("@every %ss", seconds)
}
} else {
panic(fmt.Sprintf("invalid cron spec: %s", cronSpec))
}
}
return cronSpec
}
// SetLogger sets this backend's logger
func (b *redisBackend) SetLogger(logger logging.Logger) {
b.logger = logger
}
// Shutdown halts the worker
func (b *redisBackend) Shutdown(ctx context.Context) {
b.client.Close()
b.server.Shutdown()
}
// withJobContext creates a new context with the Job set
func withJobContext(ctx context.Context, j *jobs.Job) context.Context {
return context.WithValue(ctx, internal.JobCtxVarKey, j)
}

View file

@ -0,0 +1,310 @@
package redis
import (
"context"
"fmt"
"log"
"os"
"strings"
"testing"
"time"
"github.com/acaloiaro/neoq"
"github.com/acaloiaro/neoq/handler"
"github.com/acaloiaro/neoq/internal"
"github.com/acaloiaro/neoq/jobs"
"github.com/acaloiaro/neoq/testutils"
"github.com/hibiken/asynq"
)
const queue = "testing"
const queue2 = "testing2"
func init() {
var err error
var connString = os.Getenv("TEST_REDIS_URL")
if connString == "" {
return
}
password := os.Getenv("REDIS_PASSWORD")
clientOpt := asynq.RedisClientOpt{Addr: connString}
if password != "" {
clientOpt.Password = password
}
inspector := asynq.NewInspector(clientOpt)
queues, err := inspector.Queues()
if err != nil {
panic(err)
}
for _, queue := range queues {
_, err = inspector.DeleteAllPendingTasks(queue)
if err != nil {
panic(err)
}
_, err = inspector.DeleteAllCompletedTasks(queue)
if err != nil {
panic(err)
}
_, err = inspector.DeleteAllRetryTasks(queue)
if err != nil {
panic(err)
}
_, err = inspector.DeleteAllArchivedTasks(queue)
if err != nil {
panic(err)
}
}
}
func TestBasicJobProcessing(t *testing.T) {
var timeoutTimer = time.After(5 * time.Second)
var done = make(chan bool)
var connString = os.Getenv("TEST_REDIS_URL")
if connString == "" {
t.Skip("Skipping: TEST_REDIS_URL not set")
return
}
password := os.Getenv("REDIS_PASSWORD")
ctx := context.TODO()
nq, err := neoq.New(
ctx,
neoq.WithBackend(Backend),
WithAddr(connString),
WithPassword(password),
WithShutdownTimeout(time.Millisecond))
if err != nil {
t.Fatal(err)
}
defer nq.Shutdown(ctx)
h := handler.New(func(_ context.Context) (err error) {
done <- true
return
})
err = nq.Start(ctx, queue, h)
if err != nil {
t.Error(err)
}
jid, e := nq.Enqueue(ctx, &jobs.Job{
Queue: queue,
Payload: map[string]interface{}{
"message": fmt.Sprintf("hello world: %d", internal.RandInt(10000000000)),
},
})
if e != nil || jid == jobs.DuplicateJobID {
t.Error(e)
}
select {
case <-timeoutTimer:
err = jobs.ErrJobTimeout
case <-done:
log.Println("DONE")
}
if err != nil {
t.Error(err)
}
}
// TestBasicJobMultipleQueue tests that the redis backend is able to process jobs on multiple queues
func TestBasicJobMultipleQueue(t *testing.T) {
var done = make(chan bool)
var doneCnt = 0
var timeoutTimer = time.After(30 * time.Second)
var connString = os.Getenv("TEST_REDIS_URL")
if connString == "" {
t.Skip("Skipping: TEST_REDIS_URL not set")
return
}
password := os.Getenv("REDIS_PASSWORD")
ctx := context.TODO()
nq, err := neoq.New(ctx,
neoq.WithBackend(Backend),
WithAddr(connString),
WithPassword(password),
WithShutdownTimeout(time.Millisecond))
if err != nil {
t.Fatal(err)
}
defer nq.Shutdown(ctx)
h := handler.New(func(_ context.Context) (err error) {
done <- true
return
})
h2 := handler.New(func(_ context.Context) (err error) {
done <- true
return
})
err = nq.Start(ctx, queue, h)
if err != nil {
t.Error(err)
}
err = nq.Start(ctx, queue2, h2)
if err != nil {
t.Error(err)
}
jid, e := nq.Enqueue(ctx, &jobs.Job{
Queue: queue,
Payload: map[string]interface{}{
"message": fmt.Sprintf("hello world: %d", internal.RandInt(10000000000)),
},
})
if e != nil || jid == jobs.DuplicateJobID {
t.Error(e)
}
jid2, e := nq.Enqueue(ctx, &jobs.Job{
Queue: queue2,
Payload: map[string]interface{}{
"message": fmt.Sprintf("hello world: %d", internal.RandInt(10000000000)),
},
})
if e != nil || jid2 == jobs.DuplicateJobID {
t.Error(e)
}
results_loop:
for {
select {
case <-timeoutTimer:
err = jobs.ErrJobTimeout
break results_loop
case <-done:
log.Println("DONE")
doneCnt++
if doneCnt == 2 {
break results_loop
}
}
}
if err != nil {
t.Error(err)
}
}
func TestStartCron(t *testing.T) {
done := make(chan bool)
var timeoutTimer = time.After(5 * time.Second)
var connString = os.Getenv("TEST_REDIS_URL")
if connString == "" {
t.Skip("Skipping: TEST_REDIS_URL not set")
return
}
password := os.Getenv("REDIS_PASSWORD")
ctx := context.TODO()
nq, err := neoq.New(ctx, neoq.WithBackend(Backend), WithAddr(connString), WithPassword(password))
if err != nil {
t.Fatal(err)
}
defer nq.Shutdown(ctx)
h := handler.New(func(_ context.Context) (err error) {
done <- true
return
})
err = nq.StartCron(ctx, "* * * * * *", h)
if err != nil {
t.Error(err)
}
select {
case <-timeoutTimer:
err = jobs.ErrJobTimeout
case <-done:
}
if err != nil {
t.Error(err)
}
}
func TestJobProcessingWithOptions(t *testing.T) {
const queue = "testing"
var timeoutTimer = time.After(5 * time.Second)
var done = make(chan bool)
var connString = os.Getenv("TEST_REDIS_URL")
if connString == "" {
t.Skip("Skipping: TEST_REDIS_URL not set")
return
}
password := os.Getenv("REDIS_PASSWORD")
ctx := context.TODO()
nq, err := neoq.New(
ctx,
neoq.WithBackend(Backend),
WithAddr(connString),
WithPassword(password),
WithShutdownTimeout(time.Millisecond))
if err != nil {
t.Fatal(err)
}
defer nq.Shutdown(ctx)
buf := &strings.Builder{}
nq.SetLogger(testutils.TestLogger{L: log.New(buf, "", 0), Done: done})
h := handler.New(func(_ context.Context) (err error) {
time.Sleep(50 * time.Millisecond)
done <- true
return
})
h.WithOptions(
handler.JobTimeout(1*time.Millisecond),
handler.Concurrency(1),
)
err = nq.Start(ctx, queue, h)
if err != nil {
t.Error(err)
}
jid, e := nq.Enqueue(ctx, &jobs.Job{
Queue: queue,
Payload: map[string]interface{}{
"message": fmt.Sprintf("hello world: %d", internal.RandInt(10000000000)),
},
})
if e != nil || jid == jobs.DuplicateJobID {
t.Error(e)
}
select {
case <-timeoutTimer:
err = jobs.ErrJobTimeout
case <-done:
expectedLogMsg := "error handling job job exceeded its 1ms timeout: context deadline exceeded" //nolint: dupword
actualLogMsg := strings.Trim(buf.String(), "\n")
if actualLogMsg != expectedLogMsg {
t.Error(fmt.Errorf("%s != %s", actualLogMsg, expectedLogMsg)) //nolint:all
}
}
if err != nil {
t.Error(err)
}
}

View file

@ -17,12 +17,20 @@ const (
DefaultJobCheckInterval = 5 * time.Second DefaultJobCheckInterval = 5 * time.Second
) )
// Config configures neoq and its backends
//
// This configuration struct includes options for all backends. As such, some of its options are not implicable to all
// backends. [BackendConcurrency], for example, is only used by the redis backend. Other backends manage concurrency on a
// per-handler basis.
type Config struct { type Config struct {
BackendInitializer BackendInitializer BackendInitializer BackendInitializer
BackendAuthPassword string // password with which to authenticate to the backend's data provider
BackendConcurrency int // total number of backend processes available to process jobs
ConnectionString string // a string containing connection details for the backend ConnectionString string // a string containing connection details for the backend
JobCheckInterval time.Duration // the interval of time between checking for new future/retry jobs JobCheckInterval time.Duration // the interval of time between checking for new future/retry jobs
FutureJobWindow time.Duration // time duration between current time and job.RunAfter that goroutines schedule for future jobs FutureJobWindow time.Duration // time duration between current time and job.RunAfter that goroutines schedule for future jobs
IdleTransactionTimeout int // the number of milliseconds PgBackend transaction may idle before the connection is killed IdleTransactionTimeout int // the number of milliseconds PgBackend transaction may idle before the connection is killed
ShutdownTimeout time.Duration // duration to wait for jobs to finish during shutdown
} }
// Option is a function that sets optional backend configuration // Option is a function that sets optional backend configuration

15
doc.go
View file

@ -1,8 +1,13 @@
// Package neoq provides background job processing for Go applications. // Package neoq is a queue-agnostic background job framework for Go.
// //
// Neoq's goal is to minimize the infrastructure necessary to add background job processing to // Neoq job handlers are the same, whether queues are in-memory for development/testing,
// Go applications. It does so by implementing queue durability with modular backends, rather // or Postgres, Redis, or a custom queue for production -- allowing queue infrastructure to
// than introducing a strict dependency on a particular backend such as Redis. // change without code change.
// //
// An in-memory and Postgres backend are provided out of the box. // Developing/testing or don't need a durable queue? Use the in-memory queue.
// Running an application in production? Use Postgres.
// Have higher throughput demands in production? Use Redis.
// Neoq does not aim to be the _fastest_ background job processor. It aims to be _fast_, _reliable_, and demand a
// _minimal infrastructure footprint_.
package neoq package neoq

View file

@ -1,3 +1,4 @@
TEST_DATABASE_URL=<TEST_DATABASE_URL> TEST_DATABASE_URL=<TEST_DATABASE_URL>
TEST_REDIS_URL=<TEST_REDIS_URL> TEST_REDIS_URL=<TEST_REDIS_URL>
REDIS_PASSWORD=<REDIS_PASSWORD>

View file

@ -54,7 +54,7 @@ func main() {
<-done <-done
// job's status will be 'failed' and 'error' will be 'job exceeded its 10ms deadline' // job's status will be 'failed' and 'error' will be 'job exceeded its 10ms timeout'
// until either the job's Sleep statement is decreased/removed or the handler's deadline is increased // until either the job's Sleep statement is decreased/removed or the handler's timeout is increased
// this job will continue to fail and ultimately land on the dead jobs queue // this job will continue to fail and ultimately land on the dead jobs queue
} }

View file

@ -37,8 +37,8 @@ func main() {
return return
}) })
// this 10ms deadline will cause our job that sleeps for 1s to fail // this 10ms timeout will cause our job that sleeps for 1s to fail
h.WithOptions(handler.Deadline(10 * time.Millisecond)) h.WithOptions(handler.JobTimeout(10 * time.Millisecond))
err = nq.Start(ctx, queue, h) err = nq.Start(ctx, queue, h)
if err != nil { if err != nil {
@ -57,7 +57,7 @@ func main() {
<-done <-done
// job's status will be 'failed' and 'error' will be 'job exceeded its 10ms deadline' // job's status will be 'failed' and 'error' will be 'job exceeded its 10ms timeout'
// until either the job's Sleep statement is decreased/removed or the handler's deadline is increased // until either the job's Sleep statement is decreased/removed or the handler's timeout is increased
// this job will continue to fail and ultimately land on the dead jobs queue // this job will continue to fail and ultimately land on the dead jobs queue
} }

View file

@ -23,7 +23,7 @@ func main() {
return return
}) })
h.WithOptions( h.WithOptions(
handler.Deadline(500*time.Millisecond), handler.JobTimeout(500*time.Millisecond),
handler.Concurrency(1), handler.Concurrency(1),
) )

View file

@ -0,0 +1,39 @@
package main
import (
"context"
"log"
"github.com/acaloiaro/neoq"
"github.com/acaloiaro/neoq/backends/redis"
"github.com/acaloiaro/neoq/handler"
"github.com/acaloiaro/neoq/jobs"
)
func main() {
done := make(chan bool)
ctx := context.Background()
nq, _ := neoq.New(ctx,
neoq.WithBackend(redis.Backend),
redis.WithAddr("localhost:6379"),
redis.WithPassword(""),
)
nq.Start(ctx, "hello_world", handler.New(func(ctx context.Context) (err error) {
j, _ := jobs.FromContext(ctx)
log.Println("got job id:", j.ID, "messsage:", j.Payload["message"])
done <- true
return
}))
nq.Enqueue(ctx, &jobs.Job{
Queue: "hello_world",
Payload: map[string]interface{}{
"message": "hello world",
},
})
<-done
nq.Shutdown(ctx)
}

12
go.mod
View file

@ -13,13 +13,25 @@ require (
) )
require ( require (
github.com/cespare/xxhash/v2 v2.1.1 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/go-redis/redis/v8 v8.11.2 // indirect
github.com/golang/protobuf v1.4.2 // indirect
github.com/google/uuid v1.2.0 // indirect
github.com/hibiken/asynq v0.24.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/jackc/puddle/v2 v2.2.0 // indirect github.com/jackc/puddle/v2 v2.2.0 // indirect
github.com/robfig/cron/v3 v3.0.1 // indirect
github.com/spf13/cast v1.3.1 // indirect
go.uber.org/atomic v1.10.0 // indirect go.uber.org/atomic v1.10.0 // indirect
go.uber.org/goleak v1.1.12 // indirect
go.uber.org/multierr v1.9.0 // indirect go.uber.org/multierr v1.9.0 // indirect
go.uber.org/zap v1.24.0 // indirect go.uber.org/zap v1.24.0 // indirect
golang.org/x/crypto v0.6.0 // indirect golang.org/x/crypto v0.6.0 // indirect
golang.org/x/sync v0.1.0 // indirect golang.org/x/sync v0.1.0 // indirect
golang.org/x/sys v0.5.0 // indirect
golang.org/x/text v0.7.0 // indirect golang.org/x/text v0.7.0 // indirect
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 // indirect
google.golang.org/protobuf v1.25.0 // indirect
) )

126
go.sum
View file

@ -1,12 +1,47 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/go-redis/redis/v8 v8.11.2 h1:WqlSpAwz8mxDSMCvbyz1Mkiqe0LE5OY4j3lgkvu1Ts0=
github.com/go-redis/redis/v8 v8.11.2/go.mod h1:DLomh7y2e3ggQXQLd1YgmvIfecPJoFl7WU5SOQ/r06M=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.2.0 h1:qJYtXnJRWmpe7m/3XlyhrsLrEURqHRM2kxzoxXqyUDs=
github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/guregu/null v4.0.0+incompatible h1:4zw0ckM7ECd6FNNddc3Fu4aty9nTlpkkzH7dPn4/4Gw= github.com/guregu/null v4.0.0+incompatible h1:4zw0ckM7ECd6FNNddc3Fu4aty9nTlpkkzH7dPn4/4Gw=
github.com/guregu/null v4.0.0+incompatible/go.mod h1:ePGpQaN9cw0tj45IR5E5ehMvsFlLlQZAkkOXZurJ3NM= github.com/guregu/null v4.0.0+incompatible/go.mod h1:ePGpQaN9cw0tj45IR5E5ehMvsFlLlQZAkkOXZurJ3NM=
github.com/hibiken/asynq v0.24.0 h1:r1CiSVYCy1vGq9REKGI/wdB2D5n/QmtzihYHHXOuBUs=
github.com/hibiken/asynq v0.24.0/go.mod h1:FVnRfUTm6gcoDkM/EjF4OIh5/06ergCPUO6pS2B2y+w=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/iancoleman/strcase v0.2.0 h1:05I4QRnGpI0m37iZQRuskXh+w77mr6Z41lwQzuHLwW0= github.com/iancoleman/strcase v0.2.0 h1:05I4QRnGpI0m37iZQRuskXh+w77mr6Z41lwQzuHLwW0=
github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
@ -23,22 +58,38 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
github.com/onsi/ginkgo v1.15.0/go.mod h1:hF8qUzuuC8DJGygJH3726JnCZX4MYbRB8yFfISqnKUg=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/onsi/gomega v1.10.5/go.mod h1:gza4q3jKQJijlu05nKWRCW/GavJumGt8aNRxWg7mt48=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/robfig/cron v1.2.0 h1:ZjScXvvxeQ63Dbyxy76Fj3AT3Ut0aKsyd2/tl3DTMuQ= github.com/robfig/cron v1.2.0 h1:ZjScXvvxeQ63Dbyxy76Fj3AT3Ut0aKsyd2/tl3DTMuQ=
github.com/robfig/cron v1.2.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k= github.com/robfig/cron v1.2.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng=
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ= go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ=
go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI= go.uber.org/goleak v1.1.12 h1:gZAh5/EyT/HQwlpkCy6wTpqfH9H8Lz8zbm3dZh+OyzA=
go.uber.org/goleak v1.1.12/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
@ -48,32 +99,105 @@ go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60=
go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc= golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc=
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20230213192124-5e25df0256eb h1:PaBZQdo+iSDyHT053FjUCgZQ/9uqVwPOcl7KSWhKn6w= golang.org/x/exp v0.0.0-20230213192124-5e25df0256eb h1:PaBZQdo+iSDyHT053FjUCgZQ/9uqVwPOcl7KSWhKn6w=
golang.org/x/exp v0.0.0-20230213192124-5e25df0256eb/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= golang.org/x/exp v0.0.0-20230213192124-5e25df0256eb/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=

View file

@ -9,7 +9,7 @@ import (
) )
const ( const (
DefaultHandlerDeadline = 30 * time.Second DefaultHandlerTimeout = 30 * time.Second
) )
var ( var (
@ -25,7 +25,7 @@ type Func func(ctx context.Context) error
type Handler struct { type Handler struct {
Handle Func Handle Func
Concurrency int Concurrency int
Deadline time.Duration JobTimeout time.Duration
QueueCapacity int64 QueueCapacity int64
} }
@ -39,12 +39,12 @@ func (h *Handler) WithOptions(opts ...Option) {
} }
} }
// Deadline configures handlers with a time deadline for every executed job // JobTimeout configures handlers with a time deadline for every executed job
// The deadline is the amount of time that can be spent executing the handler's Func // The timeout is the amount of time that can be spent executing the handler's Func
// when a deadline is exceeded, the job is failed and enters its retry phase // when a timeout is exceeded, the job fails and enters its retry phase
func Deadline(d time.Duration) Option { func JobTimeout(d time.Duration) Option {
return func(h *Handler) { return func(h *Handler) {
h.Deadline = d h.JobTimeout = d
} }
} }
@ -72,22 +72,22 @@ func New(f Func, opts ...Option) (h Handler) {
h.WithOptions(opts...) h.WithOptions(opts...)
// default to running one fewer threads than CPUs // default to running on as many goroutines as there are CPUs
if h.Concurrency == 0 { if h.Concurrency == 0 {
Concurrency(runtime.NumCPU() - 1)(&h) Concurrency(runtime.NumCPU())(&h)
} }
// always set a job deadline if none is set // always set a job timeout if none is set
if h.Deadline == 0 { if h.JobTimeout == 0 {
Deadline(DefaultHandlerDeadline)(&h) JobTimeout(DefaultHandlerTimeout)(&h)
} }
return return
} }
// Exec executes handler functions with a concrete time deadline // Exec executes handler functions with a concrete timeout
func Exec(ctx context.Context, handler Handler) (err error) { func Exec(ctx context.Context, handler Handler) (err error) {
deadlineCtx, cancel := context.WithDeadline(ctx, time.Now().Add(handler.Deadline)) timeoutCtx, cancel := context.WithTimeout(ctx, handler.JobTimeout)
defer cancel() defer cancel()
var errCh = make(chan error, 1) var errCh = make(chan error, 1)
@ -104,10 +104,10 @@ func Exec(ctx context.Context, handler Handler) (err error) {
err = fmt.Errorf("job failed to process: %w", err) err = fmt.Errorf("job failed to process: %w", err)
} }
case <-deadlineCtx.Done(): case <-timeoutCtx.Done():
ctxErr := deadlineCtx.Err() ctxErr := timeoutCtx.Err()
if errors.Is(ctxErr, context.DeadlineExceeded) { if errors.Is(ctxErr, context.DeadlineExceeded) {
err = fmt.Errorf("job exceeded its %s deadline: %w", handler.Deadline, ctxErr) err = fmt.Errorf("job exceeded its %s timeout: %w", handler.JobTimeout, ctxErr)
} else if errors.Is(ctxErr, context.Canceled) { } else if errors.Is(ctxErr, context.Canceled) {
err = ctxErr err = ctxErr
} else { } else {

View file

@ -20,8 +20,8 @@ var (
) )
const ( const (
DuplicateJobID = -1 DuplicateJobID = "-1"
UnqueuedJobID = -2 UnqueuedJobID = "-2"
) )
// Job contains all the data pertaining to jobs // Job contains all the data pertaining to jobs

View file

@ -105,7 +105,7 @@ func TestStart(t *testing.T) {
return return
}) })
h.WithOptions( h.WithOptions(
handler.Deadline(500*time.Millisecond), handler.JobTimeout(500*time.Millisecond),
handler.Concurrency(1), handler.Concurrency(1),
) )
@ -125,7 +125,7 @@ func TestStart(t *testing.T) {
"message": fmt.Sprintf("hello world: %d", i), "message": fmt.Sprintf("hello world: %d", i),
}, },
}) })
if err != nil || jid == -1 { if err != nil || jid == jobs.DuplicateJobID {
t.Fatal("job was not enqueued. either it was duplicate or this error caused it:", err) t.Fatal("job was not enqueued. either it was duplicate or this error caused it:", err)
} }
} }
@ -169,7 +169,7 @@ func TestStartCron(t *testing.T) {
}) })
h.WithOptions( h.WithOptions(
handler.Deadline(500*time.Millisecond), handler.JobTimeout(500*time.Millisecond),
handler.Concurrency(1), handler.Concurrency(1),
) )

View file

@ -15,7 +15,7 @@ import (
// - [pkg/github.com/acaloiaro/neoq/backends/postgres.PgBackend] // - [pkg/github.com/acaloiaro/neoq/backends/postgres.PgBackend]
type Backend interface { type Backend interface {
// Enqueue queues jobs to be executed asynchronously // Enqueue queues jobs to be executed asynchronously
Enqueue(ctx context.Context, job *jobs.Job) (jobID int64, err error) Enqueue(ctx context.Context, job *jobs.Job) (jobID string, err error)
// Start starts processing jobs with the specified queue and handler // Start starts processing jobs with the specified queue and handler
Start(ctx context.Context, queue string, h handler.Handler) (err error) Start(ctx context.Context, queue string, h handler.Handler) (err error)