task/queue/queue_test.go

198 lines
5.6 KiB
Go

package queue_test
import (
"fmt"
"net"
"net/url"
"os"
"testing"
"time"
"apigo.cc/go/redis"
"apigo.cc/go/task/queue"
)
type queueItem struct {
ID int
Text string
}
func TestMain(m *testing.M) {
redisURL := os.Getenv("TASK_QUEUE_TEST_REDIS")
if redisURL == "" {
redisURL = "redis://:@localhost:6379/14?connectTimeout=100ms&readTimeout=100ms&writeTimeout=100ms"
}
parsedURL, err := url.Parse(redisURL)
if err != nil {
fmt.Printf("Invalid TASK_QUEUE_TEST_REDIS: %v\n", err)
os.Exit(1)
}
conn, err := net.DialTimeout("tcp", parsedURL.Host, 500*time.Millisecond)
if err != nil {
fmt.Printf("Redis server is not running at %s, skipping queue tests.\n", parsedURL.Host)
os.Exit(0)
}
_ = conn.Close()
redis.SetConfig("task", redisURL)
os.Exit(m.Run())
}
func TestAddFetchFinish(t *testing.T) {
queueName := testQueueName(t)
cleanupQueue(t, queueName, "default")
want := queueItem{ID: 1, Text: "first"}
if err := queue.Add(queueName, want); err != nil {
t.Fatalf("Add failed: %v", err)
}
items, err := queue.Fetch(queueName, "default", 10)
if err != nil {
t.Fatalf("Fetch failed: %v", err)
}
if len(items) != 1 {
t.Fatalf("Fetch returned %d items, want 1", len(items))
}
rawProcessing := redis.GetRedis("task", nil).LRANGE("queue:"+queueName+":default:processing", 0, -1)
if len(rawProcessing) != 1 {
t.Fatalf("processing contains %d raw items, want only the fetched task", len(rawProcessing))
}
if rawProcessing[0].String() == "\x00apigo-task-processing\x00" {
t.Fatal("processing retained its compatibility marker")
}
got, err := queue.Decode[queueItem](items[0])
if err != nil {
t.Fatalf("Decode failed: %v", err)
}
if got != want {
t.Fatalf("Fetch returned %+v, want %+v", got, want)
}
if count := queue.FetchCount(queueName, "default"); count != 1 {
t.Fatalf("FetchCount returned %d while processing, want 1", count)
}
if err := queue.Finish(queueName, "default"); err != nil {
t.Fatalf("Finish failed: %v", err)
}
if count := queue.FetchCount(queueName, "default"); count != 0 {
t.Fatalf("FetchCount returned %d after Finish, want 0", count)
}
}
func TestFetchRecoversProcessingBeforePending(t *testing.T) {
queueName := testQueueName(t)
cleanupQueue(t, queueName, "default")
processingItem := queueItem{ID: 1, Text: "processing"}
if err := queue.Add(queueName, processingItem); err != nil {
t.Fatalf("Add processing item failed: %v", err)
}
firstFetch, err := queue.Fetch(queueName, "default", 1)
if err != nil {
t.Fatalf("initial Fetch failed: %v", err)
}
if len(firstFetch) != 1 {
t.Fatalf("initial Fetch returned %d items, want 1", len(firstFetch))
}
pendingItem := queueItem{ID: 2, Text: "pending"}
if err := queue.Add(queueName, pendingItem); err != nil {
t.Fatalf("Add pending item failed: %v", err)
}
recovered, err := queue.Fetch(queueName, "default", 50)
if err != nil {
t.Fatalf("recovery Fetch failed: %v", err)
}
if len(recovered) != 1 {
t.Fatalf("recovery Fetch returned %d items, want the existing processing batch only", len(recovered))
}
got, err := queue.Decode[queueItem](recovered[0])
if err != nil {
t.Fatalf("Decode recovered item failed: %v", err)
}
if got != processingItem {
t.Fatalf("recovery Fetch returned %+v, want %+v", got, processingItem)
}
if err := queue.Finish(queueName, "default"); err != nil {
t.Fatalf("Finish recovered batch failed: %v", err)
}
next, err := queue.Fetch(queueName, "default", 1)
if err != nil {
t.Fatalf("Fetch pending item failed: %v", err)
}
if len(next) != 1 {
t.Fatalf("Fetch pending item returned %d items, want 1", len(next))
}
got, err = queue.Decode[queueItem](next[0])
if err != nil {
t.Fatalf("Decode pending item failed: %v", err)
}
if got != pendingItem {
t.Fatalf("Fetch after Finish returned %+v, want %+v", got, pendingItem)
}
}
func TestAddAfterPromotesOnlyDueItems(t *testing.T) {
queueName := testQueueName(t)
cleanupQueue(t, queueName, "default")
due := queueItem{ID: 1, Text: "due"}
future := queueItem{ID: 2, Text: "future"}
if err := queue.AddAfter(queueName, future, time.Hour); err != nil {
t.Fatalf("AddAfter future item failed: %v", err)
}
if err := queue.AddAfter(queueName, due, -time.Millisecond); err != nil {
t.Fatalf("AddAfter due item failed: %v", err)
}
items, err := queue.Fetch(queueName, "default", 10)
if err != nil {
t.Fatalf("Fetch delayed item failed: %v", err)
}
if len(items) != 1 {
t.Fatalf("Fetch returned %d delayed items, want only the due item", len(items))
}
got, err := queue.Decode[queueItem](items[0])
if err != nil {
t.Fatalf("Decode delayed item failed: %v", err)
}
if got != due {
t.Fatalf("Fetch returned delayed item %+v, want %+v", got, due)
}
rd := redis.GetRedis("task", nil)
if count := rd.Do("ZCARD", delayedKey(queueName)).Int(); count != 1 {
t.Fatalf("delayed queue contains %d items after promotion, want the future item only", count)
}
}
func testQueueName(t *testing.T) string {
t.Helper()
return fmt.Sprintf("task-queue-test:%d:%s", time.Now().UnixNano(), t.Name())
}
func cleanupQueue(t *testing.T, queueName, consumerID string) {
t.Helper()
rd := redis.GetRedis("task", nil)
if rd == nil || rd.Error != nil {
t.Fatalf("task Redis is unavailable: %v", rd.Error)
}
keys := []string{
"queue:" + queueName + ":pending",
"queue:" + queueName + ":" + consumerID + ":processing",
delayedKey(queueName),
}
if result := rd.Do("DEL", keys); result.Error != nil {
t.Fatalf("clean queue before test: %v", result.Error)
}
t.Cleanup(func() {
_ = rd.Do("DEL", keys)
})
}
func delayedKey(queueName string) string {
return "queue:" + queueName + ":delayed"
}