Added optional delay to tasks. Pool buffers when encoding.

This commit is contained in:
mikestefanello 2024-06-20 22:11:53 -04:00
parent 912ae2ca6b
commit b68f9bdf3c
8 changed files with 96 additions and 131 deletions

View file

@ -1,32 +1,3 @@
# Determine if you have docker-compose or docker compose installed locally
# If this does not work on your system, just set the name of the executable you have installed
DCO_BIN := $(shell { command -v docker-compose || command -v docker compose; } 2>/dev/null)
# Connect to the primary database
.PHONY: db
db:
docker exec -it pagoda_db psql postgresql://admin:admin@localhost:5432/app
# Connect to the test database (you must run tests first before running this)
.PHONY: db-test
db-test:
docker exec -it pagoda_db psql postgresql://admin:admin@localhost:5432/app_test
# Connect to the primary cache
.PHONY: cache
cache:
docker exec -it pagoda_cache redis-cli
# Clear the primary cache
.PHONY: cache-clear
cache-clear:
docker exec -it pagoda_cache redis-cli flushall
# Connect to the test cache
.PHONY: cache-test
cache-test:
docker exec -it pagoda_cache redis-cli -n 1
# Install Ent code-generation module # Install Ent code-generation module
.PHONY: ent-install .PHONY: ent-install
ent-install: ent-install:
@ -42,28 +13,6 @@ ent-gen:
ent-new: ent-new:
go run entgo.io/ent/cmd/ent new $(name) go run entgo.io/ent/cmd/ent new $(name)
# Start the Docker containers
.PHONY: up
up:
$(DCO_BIN) up -d
sleep 3
# Stop the Docker containers
.PHONY: stop
stop:
$(DCO_BIN) stop
# Drop the Docker containers to wipe all data
.PHONY: down
down:
$(DCO_BIN) down
# Rebuild Docker containers to wipe all data
.PHONY: reset
reset:
$(DCO_BIN) down
make up
# Run the application # Run the application
.PHONY: run .PHONY: run
run: run:
@ -73,13 +22,7 @@ run:
# Run all tests # Run all tests
.PHONY: test .PHONY: test
test: test:
go test -count=1 -p 1 ./... go test ./...
# Run the worker
.PHONY: worker
worker:
clear
go run cmd/worker/main.go
# Check for direct dependency updates # Check for direct dependency updates
.PHONY: check-updates .PHONY: check-updates

View file

@ -59,7 +59,7 @@ func main() {
// Register all task queues // Register all task queues
tasks.Register(c) tasks.Register(c)
// Start the task runner to executed queued tasks // Start the task runner to execute queued tasks
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
c.Tasks.StartRunner(ctx) c.Tasks.StartRunner(ctx)

View file

@ -106,10 +106,6 @@ type (
// TasksConfig stores the tasks configuration // TasksConfig stores the tasks configuration
TasksConfig struct { TasksConfig struct {
// TODO remove separate DB?
Driver string
Connection string
TestConnection string
PollInterval time.Duration PollInterval time.Duration
MaxRetries int MaxRetries int
Goroutines int Goroutines int

View file

@ -72,10 +72,11 @@ func (h *Contact) Submit(ctx echo.Context) error {
return err return err
} }
// TODO create a new page for this
err = h.tasks.New(tasks.ExampleTask{ err = h.tasks.New(tasks.ExampleTask{
Message: input.Message, Message: input.Message,
}). }).
Wait(30 * time.Second). Wait(10 * time.Second).
Save() Save()
if err != nil { if err != nil {
return err return err

View file

@ -71,8 +71,9 @@ type (
// locking, and we need to keep track of this index in order to keep everything in sync. // locking, and we need to keep track of this index in order to keep everything in sync.
// If using something like Redis for caching, you can leverage sets to store the index. // If using something like Redis for caching, you can leverage sets to store the index.
// Cache tags can be useful and convenient, so you should decide if your app benefits enough from this. // Cache tags can be useful and convenient, so you should decide if your app benefits enough from this.
// As it stands there, there is no limiting how much memory this will consume and it will track all keys // As it stands here, there is no limiting how much memory this will consume and it will track all keys
// and tags added and removed from the cache. // and tags added and removed from the cache. You could store these in the cache itself but allowing these to
// be evicted poses challenges.
tagIndex struct { tagIndex struct {
sync.Mutex sync.Mutex
tags map[string]map[string]struct{} // tag->keys tags map[string]map[string]struct{} // tag->keys
@ -314,7 +315,7 @@ func (i *tagIndex) purgeTags(tags ...string) []string {
keys := make([]string, 0) keys := make([]string, 0)
for _, tag := range tags { for _, tag := range tags {
tagKeys := i.tags[tag] if tagKeys, exists := i.tags[tag]; exists {
delete(i.tags, tag) delete(i.tags, tag)
for key := range tagKeys { for key := range tagKeys {
@ -326,6 +327,7 @@ func (i *tagIndex) purgeTags(tags ...string) []string {
keys = append(keys, key) keys = append(keys, key)
} }
} }
}
return keys return keys
} }
@ -335,7 +337,7 @@ func (i *tagIndex) purgeKeys(keys ...string) {
defer i.Unlock() defer i.Unlock()
for _, key := range keys { for _, key := range keys {
keyTags := i.keys[key] if keyTags, exists := i.keys[key]; exists {
delete(i.keys, key) delete(i.keys, key)
for tag := range keyTags { for tag := range keyTags {
@ -345,4 +347,5 @@ func (i *tagIndex) purgeKeys(keys ...string) {
} }
} }
} }
}
} }

View file

@ -13,6 +13,7 @@ func TestCacheClient(t *testing.T) {
type cacheTest struct { type cacheTest struct {
Value string Value string
} }
// Cache some data // Cache some data
data := cacheTest{Value: "abcdef"} data := cacheTest{Value: "abcdef"}
group := "testgroup" group := "testgroup"
@ -22,7 +23,7 @@ func TestCacheClient(t *testing.T) {
Group(group). Group(group).
Key(key). Key(key).
Data(data). Data(data).
Expiration(time.Hour). Expiration(500 * time.Millisecond).
Save(context.Background()) Save(context.Background())
require.NoError(t, err) require.NoError(t, err)
@ -53,7 +54,7 @@ func TestCacheClient(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
// The data should be gone // The data should be gone
assertFlushed := func() { assertFlushed := func(key string) {
// The data should be gone // The data should be gone
_, err = c.Cache. _, err = c.Cache.
Get(). Get().
@ -62,20 +63,33 @@ func TestCacheClient(t *testing.T) {
Fetch(context.Background()) Fetch(context.Background())
assert.Equal(t, ErrCacheMiss, err) assert.Equal(t, ErrCacheMiss, err)
} }
assertFlushed() assertFlushed(key)
// Set with tags // Set with tags
key = "testkey2"
err = c.Cache. err = c.Cache.
Set(). Set().
Group(group). Group(group).
Key(key). Key(key).
Data(data). Data(data).
Tags("tag1"). Tags("tag1", "tag2").
Expiration(time.Hour). Expiration(time.Hour).
Save(context.Background()) Save(context.Background())
require.NoError(t, err) require.NoError(t, err)
// Flush the tag // Check the tag index
index := c.Cache.store.(*inMemoryCacheStore).tagIndex
gk := c.Cache.cacheKey(group, key)
_, exists := index.tags["tag1"][gk]
assert.True(t, exists)
_, exists = index.tags["tag2"][gk]
assert.True(t, exists)
_, exists = index.keys[gk]["tag1"]
assert.True(t, exists)
_, exists = index.keys[gk]["tag2"]
assert.True(t, exists)
// Flush one of tags
err = c.Cache. err = c.Cache.
Flush(). Flush().
Tags("tag1"). Tags("tag1").
@ -83,22 +97,9 @@ func TestCacheClient(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
// The data should be gone // The data should be gone
assertFlushed() assertFlushed(key)
// Set with expiration // The index should be empty
err = c.Cache. assert.Empty(t, index.tags)
Set(). assert.Empty(t, index.keys)
Group(group).
Key(key).
Data(data).
Expiration(time.Millisecond).
Save(context.Background())
require.NoError(t, err)
// Wait for expiration
// TODO why does this need to wait so long?
time.Sleep(time.Millisecond * 500)
// The data should be gone
assertFlushed()
} }

View file

@ -1,10 +1,12 @@
package services package services
import ( import (
"bytes"
"context" "context"
"database/sql" "database/sql"
"encoding/json" "encoding/gob"
"strings" "strings"
"sync"
"time" "time"
"github.com/maragudk/goqite" "github.com/maragudk/goqite"
@ -20,6 +22,7 @@ type (
TaskClient struct { TaskClient struct {
queue *goqite.Queue queue *goqite.Queue
runner *jobs.Runner runner *jobs.Runner
buffers sync.Pool
} }
// Task is a job that can be added to a queue and later passed to and executed by a QueueSubscriber. // Task is a job that can be added to a queue and later passed to and executed by a QueueSubscriber.
@ -75,6 +78,11 @@ func NewTaskClient(cfg config.TasksConfig, db *sql.DB) (*TaskClient, error) {
Name: "tasks", Name: "tasks",
MaxReceive: cfg.MaxRetries, MaxReceive: cfg.MaxRetries,
}), }),
buffers: sync.Pool{
New: func() interface{} {
return bytes.NewBuffer(nil)
},
},
} }
t.runner = jobs.NewRunner(jobs.NewRunnerOpts{ t.runner = jobs.NewRunner(jobs.NewRunnerOpts{
@ -87,12 +95,6 @@ func NewTaskClient(cfg config.TasksConfig, db *sql.DB) (*TaskClient, error) {
return t, nil return t, nil
} }
//// Close closes the connection to the task service
//func (t *TaskClient) Close() error {
// // TODO close the runner
// return t.db.Close()
//}
// StartRunner starts the scheduler service which adds scheduled tasks to the queue. // StartRunner starts the scheduler service which adds scheduled tasks to the queue.
// This must be running in order to execute queued tasked. // This must be running in order to execute queued tasked.
// To stop the runner, cancel the context. // To stop the runner, cancel the context.
@ -131,28 +133,46 @@ func (t *TaskSaveOp) Tx(tx *sql.Tx) *TaskSaveOp {
return t return t
} }
// Save saves the task so it can be queued for execution // Save saves the task, so it can be queued for execution
func (t *TaskSaveOp) Save() error { func (t *TaskSaveOp) Save() error {
// Build the payload type message struct {
// TODO use gob? Name string
payload, err := json.Marshal(t.task) Message []byte
if err != nil { }
// Encode the task
taskBuf := t.client.buffers.Get().(*bytes.Buffer)
if err := gob.NewEncoder(taskBuf).Encode(t.task); err != nil {
return err return err
} }
//msg := goqite.Message{ // Wrap and encode the message
// Body: payload, // This is needed as a workaround because goqite doesn't support delays using the jobs package,
//} // so we format the message the way it expects but use the queue to supply the delay
// msgBuf := t.client.buffers.Get().(*bytes.Buffer)
//if t.wait != nil { wrapper := message{Name: t.task.Name(), Message: taskBuf.Bytes()}
// msg.Delay = *t.wait if err := gob.NewEncoder(msgBuf).Encode(wrapper); err != nil {
//} return err
// TODO support delay }
//return t.client.queue.Send(context.Background(), msg)
msg := goqite.Message{
Body: msgBuf.Bytes(),
}
if t.wait != nil {
msg.Delay = *t.wait
}
// Put the buffers back in the pool for re-use
taskBuf.Reset()
msgBuf.Reset()
t.client.buffers.Put(taskBuf)
t.client.buffers.Put(msgBuf)
if t.tx == nil { if t.tx == nil {
return jobs.Create(context.Background(), t.client.queue, t.task.Name(), payload) return t.client.queue.Send(context.Background(), msg)
} else { } else {
return jobs.CreateTx(context.Background(), t.tx, t.client.queue, t.task.Name(), payload) return t.client.queue.SendTx(context.Background(), t.tx, msg)
} }
} }
@ -174,7 +194,7 @@ func (q *queue[T]) Name() string {
func (q *queue[T]) Receive(ctx context.Context, payload []byte) error { func (q *queue[T]) Receive(ctx context.Context, payload []byte) error {
var obj T var obj T
err := json.Unmarshal(payload, &obj) err := gob.NewDecoder(bytes.NewReader(payload)).Decode(&obj)
if err != nil { if err != nil {
return err return err
} }

View file

@ -1,5 +1,6 @@
package services package services
// TODO
//func TestTaskClient_New(t *testing.T) { //func TestTaskClient_New(t *testing.T) {
// now := time.Now() // now := time.Now()
// tk := c.Tasks. // tk := c.Tasks.