mirror of
https://github.com/hanzoai/traces.git
synced 2026-10-30 18:20:41 +00:00
* [backend-scheduler] initial work file sharding for more targeted marshaling * Add integration coverage for both work implementations * Update config manifest * Add integration coverage for sharded work configuration * Fix integration test config path * Add test coverage for sharded work migrations and marshaling * Use new sharded work file in the backend and handle migration * Clean up backend after migration * Use shared method for shard file name * Add method to write files atomically and test * Update work interface for simpler flush/load interactions * Update callers for local flush/load interface methods * Fix lint * Fix lint * Fix lint * Clean up * Clean up after refactor new interface functions * Promote the sharded implementation to be the only implementation * Clean up comments * Generate config manifest * Clean up interface and improve test coverage * Drop transitional comments * Protect work marshal/unmarshal with a mutex * Add test coverage for concurrent marshal/unmarshal * Lint * Fix race condition in Marshal/Unmarshal when jobs are operating * Relocate methods * Update changelog * Drop interface method only used in tests * Rename method for clarity, add doc * Update doc for clarity * Lock for safety * Clarify and improve locking flush behavior * Encapsulate the lock/unlock in a func * Address some PR feedback * Further unifi shard file name generation * Modify work to use int rather than uint8 * Fix lint
797 lines
22 KiB
Go
797 lines
22 KiB
Go
package work
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/grafana/tempo/pkg/tempopb"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestShardedRoundTrip(t *testing.T) {
|
|
shardedWork := New(Config{})
|
|
|
|
// Add multiple jobs to different shards
|
|
jobs := []*Job{
|
|
createTestJob("test-job-1", tempopb.JobType_JOB_TYPE_COMPACTION),
|
|
createTestJob("test-job-2", tempopb.JobType_JOB_TYPE_COMPACTION),
|
|
createTestJob("test-job-3", tempopb.JobType_JOB_TYPE_RETENTION),
|
|
}
|
|
|
|
for _, job := range jobs {
|
|
err := shardedWork.AddJob(job)
|
|
require.NoError(t, err, "failed to add job: %v", err)
|
|
}
|
|
|
|
// Verify jobs were added
|
|
require.Equal(t, len(jobs), len(shardedWork.ListJobs()), "unexpected number of jobs before marshal")
|
|
|
|
// Marshal the sharded work
|
|
data, err := shardedWork.Marshal()
|
|
require.NoError(t, err, "failed to marshal sharded work")
|
|
|
|
// Unmarshal into a new instance
|
|
newShardedWork := New(Config{})
|
|
err = newShardedWork.Unmarshal(data)
|
|
require.NoError(t, err, "failed to unmarshal sharded work")
|
|
|
|
// Verify the lengths match
|
|
require.Equal(t, len(shardedWork.ListJobs()), len(newShardedWork.ListJobs()), "sharded work lengths do not match")
|
|
|
|
// Verify all jobs are present in the new instance
|
|
originalJobs := shardedWork.ListJobs()
|
|
newJobs := newShardedWork.ListJobs()
|
|
|
|
require.Equal(t, len(originalJobs), len(newJobs), "job counts don't match")
|
|
|
|
// Create maps for easier comparison
|
|
originalJobMap := make(map[string]*Job)
|
|
for _, job := range originalJobs {
|
|
originalJobMap[job.ID] = job
|
|
}
|
|
|
|
newJobMap := make(map[string]*Job)
|
|
for _, job := range newJobs {
|
|
newJobMap[job.ID] = job
|
|
}
|
|
|
|
// Verify all original jobs exist in the new instance
|
|
for jobID, originalJob := range originalJobMap {
|
|
newJob, exists := newJobMap[jobID]
|
|
require.True(t, exists, "job %s should exist in unmarshaled instance", jobID)
|
|
require.Equal(t, originalJob.ID, newJob.ID, "job IDs should match")
|
|
}
|
|
}
|
|
|
|
func TestAddJob(t *testing.T) {
|
|
work := New(Config{})
|
|
|
|
t.Run("successful add", func(t *testing.T) {
|
|
job := createTestJob("test-job", tempopb.JobType_JOB_TYPE_COMPACTION)
|
|
err := work.AddJob(job)
|
|
require.NoError(t, err)
|
|
|
|
// Verify job was added with correct state
|
|
retrievedJob := work.GetJob("test-job")
|
|
require.NotNil(t, retrievedJob)
|
|
require.Equal(t, "test-job", retrievedJob.ID)
|
|
require.Equal(t, tempopb.JobStatus_JOB_STATUS_UNSPECIFIED, retrievedJob.Status)
|
|
require.False(t, retrievedJob.CreatedTime.IsZero())
|
|
})
|
|
|
|
t.Run("nil job", func(t *testing.T) {
|
|
err := work.AddJob(nil)
|
|
require.Error(t, err)
|
|
require.Equal(t, ErrJobNil, err)
|
|
})
|
|
|
|
t.Run("duplicate job", func(t *testing.T) {
|
|
job1 := createTestJob("duplicate-job", tempopb.JobType_JOB_TYPE_COMPACTION)
|
|
job2 := createTestJob("duplicate-job", tempopb.JobType_JOB_TYPE_COMPACTION)
|
|
|
|
err1 := work.AddJob(job1)
|
|
require.NoError(t, err1)
|
|
|
|
err2 := work.AddJob(job2)
|
|
require.Error(t, err2)
|
|
require.Equal(t, ErrJobAlreadyExists, err2)
|
|
})
|
|
}
|
|
|
|
func TestJobOperations(t *testing.T) {
|
|
work := New(Config{})
|
|
job := createTestJob("ops-test", tempopb.JobType_JOB_TYPE_COMPACTION)
|
|
err := work.AddJob(job)
|
|
require.NoError(t, err)
|
|
|
|
t.Run("start job", func(t *testing.T) {
|
|
work.StartJob("ops-test")
|
|
retrievedJob := work.GetJob("ops-test")
|
|
require.Equal(t, tempopb.JobStatus_JOB_STATUS_RUNNING, retrievedJob.Status)
|
|
require.False(t, retrievedJob.StartTime.IsZero())
|
|
})
|
|
|
|
t.Run("complete job", func(t *testing.T) {
|
|
work.CompleteJob("ops-test")
|
|
retrievedJob := work.GetJob("ops-test")
|
|
require.Equal(t, tempopb.JobStatus_JOB_STATUS_SUCCEEDED, retrievedJob.Status)
|
|
require.False(t, retrievedJob.EndTime.IsZero())
|
|
})
|
|
|
|
t.Run("set compaction output", func(t *testing.T) {
|
|
output := []string{"output1", "output2"}
|
|
work.SetJobCompactionOutput("ops-test", output)
|
|
retrievedJob := work.GetJob("ops-test")
|
|
require.Equal(t, output, retrievedJob.GetCompactionOutput())
|
|
})
|
|
|
|
t.Run("fail job", func(t *testing.T) {
|
|
failJob := createTestJob("fail-test", tempopb.JobType_JOB_TYPE_COMPACTION)
|
|
err = work.AddJob(failJob)
|
|
require.NoError(t, err)
|
|
work.StartJob("fail-test")
|
|
|
|
work.FailJob("fail-test")
|
|
retrievedJob := work.GetJob("fail-test")
|
|
require.Equal(t, tempopb.JobStatus_JOB_STATUS_FAILED, retrievedJob.Status)
|
|
require.False(t, retrievedJob.EndTime.IsZero())
|
|
})
|
|
|
|
t.Run("remove job", func(t *testing.T) {
|
|
removeJob := createTestJob("remove-test", tempopb.JobType_JOB_TYPE_COMPACTION)
|
|
err = work.AddJob(removeJob)
|
|
require.NoError(t, err)
|
|
|
|
// Verify job exists
|
|
require.NotNil(t, work.GetJob("remove-test"))
|
|
|
|
// Remove job
|
|
work.RemoveJob("remove-test")
|
|
|
|
// Verify job is gone
|
|
require.Nil(t, work.GetJob("remove-test"))
|
|
})
|
|
}
|
|
|
|
func TestListJobs(t *testing.T) {
|
|
work := New(Config{})
|
|
|
|
// Add jobs with different creation times
|
|
jobs := []*Job{
|
|
createTestJob("job-1", tempopb.JobType_JOB_TYPE_COMPACTION),
|
|
createTestJob("job-2", tempopb.JobType_JOB_TYPE_COMPACTION),
|
|
createTestJob("job-3", tempopb.JobType_JOB_TYPE_COMPACTION),
|
|
}
|
|
|
|
var err error
|
|
|
|
for i, job := range jobs {
|
|
err = work.AddJob(job)
|
|
require.NoError(t, err)
|
|
// Manually set creation time for testing sorting
|
|
retrievedJob := work.GetJob(job.ID)
|
|
retrievedJob.CreatedTime = time.Now().Add(time.Duration(i) * time.Second)
|
|
}
|
|
|
|
listedJobs := work.ListJobs()
|
|
require.Equal(t, len(jobs), len(listedJobs))
|
|
|
|
// Verify jobs are sorted by creation time
|
|
for i := 1; i < len(listedJobs); i++ {
|
|
require.True(t, listedJobs[i-1].CreatedTime.Before(listedJobs[i].CreatedTime) ||
|
|
listedJobs[i-1].CreatedTime.Equal(listedJobs[i].CreatedTime))
|
|
}
|
|
}
|
|
|
|
func TestGetJobForWorker(t *testing.T) {
|
|
var (
|
|
work = New(Config{})
|
|
ctx = context.Background()
|
|
err error
|
|
)
|
|
|
|
// Add jobs for different workers
|
|
job1 := createTestJob("worker1-job", tempopb.JobType_JOB_TYPE_COMPACTION)
|
|
err = work.AddJob(job1)
|
|
require.NoError(t, err)
|
|
work.StartJob(job1.ID) // Sets status to RUNNING
|
|
retrievedJob1 := work.GetJob(job1.ID)
|
|
retrievedJob1.SetWorkerID("worker-1")
|
|
|
|
job2 := createTestJob("worker2-job", tempopb.JobType_JOB_TYPE_COMPACTION)
|
|
err = work.AddJob(job2)
|
|
require.NoError(t, err)
|
|
retrievedJob2 := work.GetJob(job2.ID)
|
|
retrievedJob2.SetWorkerID("worker-2")
|
|
// job2 remains in UNSPECIFIED status
|
|
|
|
// Test finding job for worker-1
|
|
foundJob := work.GetJobForWorker(ctx, "worker-1")
|
|
require.NotNil(t, foundJob)
|
|
require.Equal(t, "worker1-job", foundJob.ID)
|
|
|
|
// Test finding job for worker-2
|
|
foundJob = work.GetJobForWorker(ctx, "worker-2")
|
|
require.NotNil(t, foundJob)
|
|
require.Equal(t, "worker2-job", foundJob.ID)
|
|
|
|
// Test worker with no jobs
|
|
foundJob = work.GetJobForWorker(ctx, "nonexistent-worker")
|
|
require.Nil(t, foundJob)
|
|
}
|
|
|
|
func TestPrune(t *testing.T) {
|
|
cfg := Config{
|
|
PruneAge: time.Hour,
|
|
DeadJobTimeout: 30 * time.Minute,
|
|
}
|
|
work := New(cfg)
|
|
ctx := context.Background()
|
|
|
|
now := time.Now()
|
|
|
|
var err error
|
|
|
|
// Add old completed job (should be pruned)
|
|
oldCompleted := createTestJob("old-completed", tempopb.JobType_JOB_TYPE_COMPACTION)
|
|
err = work.AddJob(oldCompleted)
|
|
require.NoError(t, err)
|
|
work.CompleteJob(oldCompleted.ID) // Sets status to SUCCEEDED
|
|
retrievedOldCompleted := work.GetJob(oldCompleted.ID)
|
|
retrievedOldCompleted.EndTime = now.Add(-2 * time.Hour)
|
|
|
|
// Add recent completed job (should not be pruned)
|
|
recentCompleted := createTestJob("recent-completed", tempopb.JobType_JOB_TYPE_COMPACTION)
|
|
err = work.AddJob(recentCompleted)
|
|
require.NoError(t, err)
|
|
work.CompleteJob(recentCompleted.ID) // Sets status to SUCCEEDED
|
|
retrievedRecentCompleted := work.GetJob(recentCompleted.ID)
|
|
retrievedRecentCompleted.EndTime = now.Add(-10 * time.Minute)
|
|
|
|
// Add old running job (should be failed)
|
|
oldRunning := createTestJob("old-running", tempopb.JobType_JOB_TYPE_COMPACTION)
|
|
err = work.AddJob(oldRunning)
|
|
require.NoError(t, err)
|
|
work.StartJob(oldRunning.ID) // Sets status to RUNNING
|
|
retrievedOldRunning := work.GetJob(oldRunning.ID)
|
|
retrievedOldRunning.StartTime = now.Add(-time.Hour)
|
|
|
|
// Add recent running job (should remain running)
|
|
recentRunning := createTestJob("recent-running", tempopb.JobType_JOB_TYPE_COMPACTION)
|
|
err = work.AddJob(recentRunning)
|
|
require.NoError(t, err)
|
|
work.StartJob(recentRunning.ID) // Sets status to RUNNING
|
|
retrievedRecentRunning := work.GetJob(recentRunning.ID)
|
|
retrievedRecentRunning.StartTime = now.Add(-10 * time.Minute)
|
|
|
|
// Run prune
|
|
work.Prune(ctx)
|
|
|
|
// Verify results
|
|
require.Nil(t, work.GetJob("old-completed"), "old completed job should be pruned")
|
|
require.NotNil(t, work.GetJob("recent-completed"), "recent completed job should remain")
|
|
|
|
oldRunningJob := work.GetJob("old-running")
|
|
require.NotNil(t, oldRunningJob, "old running job should still exist")
|
|
require.Equal(t, tempopb.JobStatus_JOB_STATUS_FAILED, oldRunningJob.Status, "old running job should be failed")
|
|
|
|
recentRunningJob := work.GetJob("recent-running")
|
|
require.NotNil(t, recentRunningJob, "recent running job should remain")
|
|
require.Equal(t, tempopb.JobStatus_JOB_STATUS_RUNNING, recentRunningJob.Status, "recent running job should stay running")
|
|
}
|
|
|
|
func TestShardingMethods(t *testing.T) {
|
|
work := New(Config{}).(*Work)
|
|
var err error
|
|
|
|
t.Run("shard ID consistency", func(t *testing.T) {
|
|
jobID := "test-job-123"
|
|
shardID1 := work.GetShardID(jobID)
|
|
shardID2 := work.GetShardID(jobID)
|
|
require.Equal(t, shardID1, shardID2, "shard ID should be consistent")
|
|
require.Less(t, shardID1, ShardCount, "shard ID should be within valid range")
|
|
})
|
|
|
|
t.Run("marshal shard", func(t *testing.T) {
|
|
// Add job to a specific shard
|
|
job := createTestJob("shard-test", tempopb.JobType_JOB_TYPE_COMPACTION)
|
|
err = work.AddJob(job)
|
|
require.NoError(t, err)
|
|
|
|
shardID := work.GetShardID("shard-test")
|
|
data, err := work.MarshalShard(shardID)
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, data)
|
|
})
|
|
|
|
t.Run("unmarshal shard", func(t *testing.T) {
|
|
// Create shard data
|
|
job := createTestJob("unmarshal-test", tempopb.JobType_JOB_TYPE_COMPACTION)
|
|
err = work.AddJob(job)
|
|
require.NoError(t, err)
|
|
|
|
shardID := work.GetShardID("unmarshal-test")
|
|
data, err := work.MarshalShard(shardID)
|
|
require.NoError(t, err)
|
|
|
|
// Create new work instance and unmarshal specific shard
|
|
newWork := New(Config{}).(*Work)
|
|
err = newWork.UnmarshalShard(shardID, data)
|
|
require.NoError(t, err)
|
|
|
|
// Verify job exists in the correct shard
|
|
retrievedJob := newWork.GetJob("unmarshal-test")
|
|
require.NotNil(t, retrievedJob)
|
|
require.Equal(t, "unmarshal-test", retrievedJob.ID)
|
|
})
|
|
|
|
t.Run("shard stats", func(t *testing.T) {
|
|
// Clear and add specific jobs for stats testing
|
|
statsWork := New(Config{}).(*Work)
|
|
|
|
var err error
|
|
|
|
// Add jobs to create non-uniform distribution
|
|
for i := range 10 {
|
|
job := createTestJob(fmt.Sprintf("stats-job-%d", i), tempopb.JobType_JOB_TYPE_COMPACTION)
|
|
err = statsWork.AddJob(job)
|
|
require.NoError(t, err, "failed to add job for stats")
|
|
}
|
|
|
|
stats := statsWork.GetShardStats()
|
|
require.Contains(t, stats, "total_jobs")
|
|
require.Contains(t, stats, "total_shards")
|
|
require.Contains(t, stats, "non_empty_shards")
|
|
require.Contains(t, stats, "avg_jobs_per_shard")
|
|
require.Contains(t, stats, "avg_jobs_per_active_shard")
|
|
require.Contains(t, stats, "shard_sizes")
|
|
|
|
require.Equal(t, 10, stats["total_jobs"])
|
|
require.Equal(t, ShardCount, stats["total_shards"])
|
|
require.Greater(t, stats["non_empty_shards"], 0)
|
|
})
|
|
}
|
|
|
|
func TestLocalFileOperations(t *testing.T) {
|
|
work := New(Config{})
|
|
ctx := context.Background()
|
|
|
|
// Create temporary directory
|
|
tmpDir, err := os.MkdirTemp("", "work-test-")
|
|
require.NoError(t, err)
|
|
defer os.RemoveAll(tmpDir)
|
|
|
|
// Add test jobs
|
|
jobs := []*Job{
|
|
createTestJob("local-job-1", tempopb.JobType_JOB_TYPE_COMPACTION),
|
|
createTestJob("local-job-2", tempopb.JobType_JOB_TYPE_COMPACTION),
|
|
createTestJob("local-job-3", tempopb.JobType_JOB_TYPE_COMPACTION),
|
|
}
|
|
|
|
for _, job := range jobs {
|
|
err := work.AddJob(job)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
t.Run("flush all to local", func(t *testing.T) {
|
|
err := work.FlushToLocal(ctx, tmpDir, nil)
|
|
require.NoError(t, err)
|
|
|
|
// Verify shard files were created
|
|
files, err := filepath.Glob(filepath.Join(tmpDir, "shard_*.json"))
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, files)
|
|
})
|
|
|
|
t.Run("flush affected to local", func(t *testing.T) {
|
|
affectedDir := filepath.Join(tmpDir, "affected")
|
|
err := os.MkdirAll(affectedDir, 0o700)
|
|
require.NoError(t, err)
|
|
|
|
// Flush only specific jobs
|
|
affectedJobs := []string{"local-job-1", "local-job-3"}
|
|
err = work.FlushToLocal(ctx, affectedDir, affectedJobs)
|
|
require.NoError(t, err)
|
|
|
|
// Verify only affected shard files were created
|
|
files, err := filepath.Glob(filepath.Join(affectedDir, "shard_*.json"))
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, files)
|
|
})
|
|
|
|
t.Run("load from local", func(t *testing.T) {
|
|
// Create new work instance and load from saved files
|
|
newWork := New(Config{})
|
|
err := newWork.LoadFromLocal(ctx, tmpDir)
|
|
require.NoError(t, err)
|
|
|
|
// Verify all jobs were loaded
|
|
require.Equal(t, len(work.ListJobs()), len(newWork.ListJobs()))
|
|
|
|
originalJobs := work.ListJobs()
|
|
loadedJobs := newWork.ListJobs()
|
|
|
|
require.Equal(t, len(originalJobs), len(loadedJobs))
|
|
|
|
// Verify job IDs match
|
|
originalIDs := make(map[string]bool)
|
|
for _, job := range originalJobs {
|
|
originalIDs[job.ID] = true
|
|
}
|
|
|
|
for _, job := range loadedJobs {
|
|
require.True(t, originalIDs[job.ID], "loaded job %s should exist in original", job.ID)
|
|
}
|
|
})
|
|
|
|
t.Run("load from nonexistent path", func(t *testing.T) {
|
|
nonexistentWork := New(Config{})
|
|
err := nonexistentWork.LoadFromLocal(ctx, "/nonexistent/path")
|
|
require.NoError(t, err) // Should succeed with empty shards
|
|
require.Equal(t, 0, len(nonexistentWork.ListJobs()))
|
|
})
|
|
}
|
|
|
|
func TestConcurrency(t *testing.T) {
|
|
work := New(Config{})
|
|
ctx := context.Background()
|
|
|
|
// Test concurrent operations
|
|
t.Run("concurrent adds", func(t *testing.T) {
|
|
const numGoroutines = 10
|
|
const jobsPerGoroutine = 10
|
|
|
|
// Add jobs concurrently
|
|
done := make(chan struct{})
|
|
for i := range numGoroutines {
|
|
go func(goroutineID int) {
|
|
defer func() { done <- struct{}{} }()
|
|
|
|
for j := range jobsPerGoroutine {
|
|
job := createTestJob(fmt.Sprintf("concurrent-%d-%d", goroutineID, j), tempopb.JobType_JOB_TYPE_COMPACTION)
|
|
err := work.AddJob(job)
|
|
require.NoError(t, err)
|
|
}
|
|
}(i)
|
|
}
|
|
|
|
// Wait for all goroutines
|
|
for range numGoroutines {
|
|
<-done
|
|
}
|
|
|
|
// Verify all jobs were added (should be numGoroutines * jobsPerGoroutine)
|
|
require.Equal(t, numGoroutines*jobsPerGoroutine, len(work.ListJobs()))
|
|
})
|
|
|
|
t.Run("concurrent operations", func(t *testing.T) {
|
|
var err error
|
|
|
|
// Add initial jobs
|
|
for i := range 50 {
|
|
job := createTestJob(fmt.Sprintf("ops-job-%d", i), tempopb.JobType_JOB_TYPE_COMPACTION)
|
|
err = work.AddJob(job)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
// Perform concurrent operations
|
|
const numWorkers = 5
|
|
done := make(chan struct{})
|
|
|
|
for i := range numWorkers {
|
|
go func(workerID int) {
|
|
defer func() { done <- struct{}{} }()
|
|
|
|
// Perform various operations
|
|
jobs := work.ListJobs()
|
|
if len(jobs) > 0 {
|
|
job := jobs[workerID%len(jobs)]
|
|
work.StartJob(job.ID)
|
|
work.GetJob(job.ID)
|
|
if workerID%2 == 0 {
|
|
work.CompleteJob(job.ID)
|
|
} else {
|
|
work.FailJob(job.ID)
|
|
}
|
|
}
|
|
}(i)
|
|
}
|
|
|
|
// Wait for all workers
|
|
for range numWorkers {
|
|
<-done
|
|
}
|
|
|
|
// Verify operations completed without panic
|
|
jobs := work.ListJobs()
|
|
require.NotEmpty(t, jobs)
|
|
})
|
|
|
|
t.Run("concurrent prune", func(t *testing.T) {
|
|
cfg := Config{
|
|
PruneAge: time.Minute,
|
|
DeadJobTimeout: time.Minute,
|
|
}
|
|
pruneWork := New(cfg)
|
|
|
|
var err error
|
|
|
|
// Add jobs with old timestamps
|
|
for i := range 20 {
|
|
job := createTestJob(fmt.Sprintf("prune-job-%d", i), tempopb.JobType_JOB_TYPE_COMPACTION)
|
|
err = pruneWork.AddJob(job)
|
|
require.NoError(t, err)
|
|
pruneWork.CompleteJob(job.ID) // Sets status to SUCCEEDED
|
|
retrievedJob := pruneWork.GetJob(job.ID)
|
|
retrievedJob.EndTime = time.Now().Add(-2 * time.Hour)
|
|
}
|
|
|
|
// Run prune concurrently
|
|
const numPruners = 3
|
|
done := make(chan struct{})
|
|
|
|
for range numPruners {
|
|
go func() {
|
|
defer func() { done <- struct{}{} }()
|
|
pruneWork.Prune(ctx)
|
|
}()
|
|
}
|
|
|
|
// Wait for all pruners
|
|
for range numPruners {
|
|
<-done
|
|
}
|
|
|
|
// Verify jobs were pruned without race conditions
|
|
require.Equal(t, 0, len(pruneWork.ListJobs()))
|
|
})
|
|
}
|
|
|
|
func TestEdgeCases(t *testing.T) {
|
|
work := New(Config{})
|
|
|
|
t.Run("operations on nonexistent jobs", func(t *testing.T) {
|
|
// These should not panic
|
|
work.StartJob("nonexistent")
|
|
work.CompleteJob("nonexistent")
|
|
work.FailJob("nonexistent")
|
|
work.SetJobCompactionOutput("nonexistent", []string{"output"})
|
|
work.RemoveJob("nonexistent")
|
|
|
|
job := work.GetJob("nonexistent")
|
|
require.Nil(t, job)
|
|
})
|
|
|
|
t.Run("empty work operations", func(t *testing.T) {
|
|
emptyWork := New(Config{})
|
|
|
|
require.Equal(t, 0, len(emptyWork.ListJobs()))
|
|
require.Empty(t, emptyWork.ListJobs())
|
|
|
|
ctx := context.Background()
|
|
foundJob := emptyWork.GetJobForWorker(ctx, "any-worker")
|
|
require.Nil(t, foundJob)
|
|
|
|
// Prune should not panic on empty work
|
|
emptyWork.Prune(ctx)
|
|
|
|
// Marshal/unmarshal empty work
|
|
data, err := emptyWork.Marshal()
|
|
require.NoError(t, err)
|
|
|
|
newWork := New(Config{})
|
|
err = newWork.Unmarshal(data)
|
|
require.NoError(t, err)
|
|
require.Equal(t, 0, len(newWork.ListJobs()))
|
|
})
|
|
|
|
t.Run("unmarshal invalid data", func(t *testing.T) {
|
|
newWork := New(Config{})
|
|
err := newWork.Unmarshal([]byte("invalid json"))
|
|
require.Error(t, err)
|
|
})
|
|
}
|
|
|
|
// TestFullMarshalUnmarshal tests the full work cache serialization across all shards
|
|
func TestFullMarshalUnmarshal(t *testing.T) {
|
|
work := New(Config{})
|
|
var err error
|
|
|
|
// Create jobs with variety in states for comprehensive testing
|
|
totalJobs := 20
|
|
expectedJobsByID := make(map[string]*Job)
|
|
pendingJobCount := 0
|
|
|
|
for i := range totalJobs {
|
|
jobID := fmt.Sprintf("marshal-test-job-%d", i)
|
|
job := createTestJob(jobID, tempopb.JobType_JOB_TYPE_COMPACTION)
|
|
|
|
// Add variety in job states
|
|
switch i % 4 {
|
|
case 0:
|
|
err = work.AddJob(job)
|
|
require.NoError(t, err)
|
|
pendingJobCount++ // This job remains pending
|
|
case 1:
|
|
err = work.AddJob(job)
|
|
require.NoError(t, err)
|
|
work.StartJob(jobID)
|
|
// Running jobs are not counted as "pending" by Len()
|
|
case 2:
|
|
err = work.AddJob(job)
|
|
require.NoError(t, err)
|
|
work.StartJob(jobID)
|
|
work.CompleteJob(jobID)
|
|
// Completed jobs are not counted as "pending" by Len()
|
|
case 3:
|
|
err = work.AddJob(job)
|
|
require.NoError(t, err)
|
|
work.StartJob(jobID)
|
|
work.FailJob(jobID)
|
|
// Failed jobs are not counted as "pending" by Len()
|
|
}
|
|
|
|
expectedJobsByID[jobID] = work.GetJob(jobID)
|
|
}
|
|
|
|
t.Logf("Created %d total jobs (%d pending)", totalJobs, pendingJobCount)
|
|
originalJobCount := len(work.ListJobs())
|
|
require.Equal(t, totalJobs, originalJobCount, "Should have created all jobs")
|
|
|
|
// Test full marshal
|
|
data, err := work.Marshal()
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, data)
|
|
|
|
t.Logf("Marshaled data size: %d bytes", len(data))
|
|
|
|
// Create new work instance and unmarshal
|
|
newWork := New(Config{})
|
|
err = newWork.Unmarshal(data)
|
|
require.NoError(t, err)
|
|
|
|
// Verify all jobs were restored correctly
|
|
require.Equal(t, originalJobCount, len(newWork.ListJobs()))
|
|
|
|
// Verify each job's data and state
|
|
for jobID, expectedJob := range expectedJobsByID {
|
|
actualJob := newWork.GetJob(jobID)
|
|
require.NotNil(t, actualJob, "Job %s should exist after unmarshal", jobID)
|
|
require.Equal(t, expectedJob.ID, actualJob.ID)
|
|
require.Equal(t, expectedJob.Type, actualJob.Type)
|
|
require.Equal(t, expectedJob.Status, actualJob.Status)
|
|
require.Equal(t, expectedJob.WorkerID, actualJob.WorkerID)
|
|
require.Equal(t, expectedJob.Tenant(), actualJob.Tenant())
|
|
require.Equal(t, expectedJob.GetCompactionInput(), actualJob.GetCompactionInput())
|
|
}
|
|
|
|
// Verify ListJobs returns same jobs (order doesn't matter)
|
|
originalJobs := work.ListJobs()
|
|
newJobs := newWork.ListJobs()
|
|
require.Equal(t, len(originalJobs), len(newJobs))
|
|
|
|
originalJobIDs := make(map[string]bool)
|
|
for _, job := range originalJobs {
|
|
originalJobIDs[job.ID] = true
|
|
}
|
|
|
|
for _, job := range newJobs {
|
|
require.True(t, originalJobIDs[job.ID], "Job %s should exist in original set", job.ID)
|
|
}
|
|
}
|
|
|
|
// TestMarshalUnmarshalEdgeCases tests edge cases and error conditions
|
|
func TestMarshalUnmarshalEdgeCases(t *testing.T) {
|
|
t.Run("empty work cache", func(t *testing.T) {
|
|
work := New(Config{})
|
|
|
|
// Marshal empty cache
|
|
data, err := work.Marshal()
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, data)
|
|
|
|
// Unmarshal into new instance
|
|
newWork := New(Config{})
|
|
err = newWork.Unmarshal(data)
|
|
require.NoError(t, err)
|
|
require.Equal(t, 0, len(newWork.ListJobs()))
|
|
})
|
|
|
|
t.Run("invalid json unmarshal", func(t *testing.T) {
|
|
work := New(Config{})
|
|
|
|
err := work.Unmarshal([]byte("invalid json"))
|
|
require.Error(t, err)
|
|
})
|
|
}
|
|
|
|
// TestConcurrentMarshalUnmarshal tests that Marshal/Unmarshal are safe with concurrent shard operations
|
|
func TestConcurrentMarshalUnmarshal(t *testing.T) {
|
|
work := New(Config{})
|
|
|
|
// Add initial jobs
|
|
for i := range 10 {
|
|
job := createTestJob(fmt.Sprintf("marshal-concurrent-job-%d", i), tempopb.JobType_JOB_TYPE_COMPACTION)
|
|
err := work.AddJob(job)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
// Run concurrent operations
|
|
done := make(chan struct{})
|
|
numWorkers := 5
|
|
|
|
// Workers that continuously modify shards
|
|
for i := range numWorkers {
|
|
go func(workerID int) {
|
|
defer func() { done <- struct{}{} }()
|
|
|
|
for j := range 20 {
|
|
jobID := fmt.Sprintf("worker-%d-job-%d", workerID, j)
|
|
job := createTestJob(jobID, tempopb.JobType_JOB_TYPE_COMPACTION)
|
|
|
|
err := work.AddJob(job)
|
|
require.NoError(t, err)
|
|
|
|
work.StartJob(jobID)
|
|
work.CompleteJob(jobID)
|
|
work.RemoveJob(jobID)
|
|
}
|
|
}(i)
|
|
}
|
|
|
|
// Worker that continuously marshals/unmarshals
|
|
go func() {
|
|
defer func() { done <- struct{}{} }()
|
|
|
|
for range 10 {
|
|
// Marshal current state
|
|
data, err := work.Marshal()
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, data)
|
|
|
|
// Create new work instance and unmarshal
|
|
newWork := New(Config{})
|
|
err = newWork.Unmarshal(data)
|
|
require.NoError(t, err)
|
|
|
|
// Basic validation
|
|
require.NotNil(t, newWork)
|
|
}
|
|
}()
|
|
|
|
// Wait for all workers to complete
|
|
for range numWorkers + 1 {
|
|
select {
|
|
case <-done:
|
|
case <-time.After(5 * time.Second):
|
|
t.Fatal("Test timed out - possible deadlock")
|
|
}
|
|
}
|
|
|
|
// Final validation - work should still be functional
|
|
finalJob := createTestJob("final-test", tempopb.JobType_JOB_TYPE_COMPACTION)
|
|
err := work.AddJob(finalJob)
|
|
require.NoError(t, err)
|
|
|
|
retrievedJob := work.GetJob("final-test")
|
|
require.NotNil(t, retrievedJob)
|
|
require.Equal(t, "final-test", retrievedJob.ID)
|
|
}
|
|
|
|
func createTestJob(id string, jobType tempopb.JobType) *Job {
|
|
return &Job{
|
|
ID: id,
|
|
Type: jobType,
|
|
JobDetail: tempopb.JobDetail{
|
|
Tenant: "test-tenant",
|
|
Compaction: &tempopb.CompactionDetail{
|
|
Input: []string{"block1", "block2"},
|
|
},
|
|
},
|
|
}
|
|
}
|