- Implemented comprehensive job management features, including pagination for job listing, active job retrieval, and job counting. - Enhanced job saving logic to support both creation and updates, ensuring proper handling of job metadata. - Introduced category management with automatic creation and retrieval of categories during job operations. - Added execution management capabilities, including progress tracking and logging for job executions. - Improved error handling and validation across job and execution methods, ensuring robustness in job processing. - Updated documentation to reflect new features and usage examples for job management and execution tracking.
320 lines
7.4 KiB
Go
320 lines
7.4 KiB
Go
package job_test
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/yaoapp/yao/config"
|
|
"github.com/yaoapp/yao/job"
|
|
"github.com/yaoapp/yao/test"
|
|
)
|
|
|
|
// TestWorkerManagerLifecycle tests worker manager lifecycle
|
|
func TestWorkerManagerLifecycle(t *testing.T) {
|
|
test.Prepare(t, config.Conf)
|
|
defer test.Clean()
|
|
|
|
// Create a new worker manager for testing (not singleton)
|
|
wm := job.NewWorkerManagerForTest(2)
|
|
if wm == nil {
|
|
t.Fatal("Failed to create worker manager")
|
|
}
|
|
|
|
// Test initial state
|
|
if wm.GetActiveWorkers() != 0 {
|
|
t.Error("Expected 0 active workers initially")
|
|
}
|
|
|
|
// Test start
|
|
wm.Start()
|
|
if wm.GetActiveWorkers() == 0 {
|
|
t.Error("Expected active workers after start")
|
|
}
|
|
|
|
// Test stop
|
|
wm.Stop()
|
|
// Note: Workers might still be active briefly after stop due to cleanup time
|
|
time.Sleep(100 * time.Millisecond)
|
|
|
|
// Test restart
|
|
wm.Start()
|
|
if wm.GetActiveWorkers() == 0 {
|
|
t.Error("Expected active workers after restart")
|
|
}
|
|
|
|
// Final cleanup
|
|
wm.Stop()
|
|
}
|
|
|
|
// TestWorkerJobSubmission tests job submission to workers
|
|
func TestWorkerJobSubmission(t *testing.T) {
|
|
test.Prepare(t, config.Conf)
|
|
defer test.Clean()
|
|
|
|
// Create test job
|
|
testJob, err := job.Once(job.GOROUTINE, map[string]interface{}{
|
|
"name": "Test Worker Submission Job",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Failed to create test job: %v", err)
|
|
}
|
|
|
|
// Test handler
|
|
executed := make(chan bool, 1)
|
|
testHandler := func(ctx context.Context, execution *job.Execution) error {
|
|
execution.Info("Test handler executed")
|
|
executed <- true
|
|
return nil
|
|
}
|
|
|
|
// Add handler and save job
|
|
err = testJob.Add(1, testHandler)
|
|
if err != nil {
|
|
t.Fatalf("Failed to add handler: %v", err)
|
|
}
|
|
|
|
// Create worker manager for testing
|
|
wm := job.NewWorkerManagerForTest(2)
|
|
wm.Start()
|
|
defer wm.Stop()
|
|
|
|
// Submit job
|
|
err = wm.SubmitJob(testJob, testHandler)
|
|
if err != nil {
|
|
t.Fatalf("Failed to submit job: %v", err)
|
|
}
|
|
|
|
// Wait for execution
|
|
select {
|
|
case <-executed:
|
|
t.Log("Job executed successfully")
|
|
case <-time.After(5 * time.Second):
|
|
t.Error("Job execution timeout")
|
|
}
|
|
|
|
// Check executions
|
|
executions, err := testJob.GetExecutions()
|
|
if err != nil {
|
|
t.Fatalf("Failed to get executions: %v", err)
|
|
}
|
|
if len(executions) == 0 {
|
|
t.Error("Expected at least one execution")
|
|
}
|
|
}
|
|
|
|
// TestWorkerModes tests different worker execution modes
|
|
func TestWorkerModes(t *testing.T) {
|
|
test.Prepare(t, config.Conf)
|
|
defer test.Clean()
|
|
|
|
wm := job.NewWorkerManagerForTest(4)
|
|
wm.Start()
|
|
defer wm.Stop()
|
|
|
|
// Test GOROUTINE mode
|
|
goroutineJob, err := job.Once(job.GOROUTINE, map[string]interface{}{
|
|
"name": "Test Goroutine Mode Job",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Failed to create goroutine job: %v", err)
|
|
}
|
|
|
|
goroutineExecuted := make(chan bool, 1)
|
|
goroutineHandler := func(ctx context.Context, execution *job.Execution) error {
|
|
if execution.Job != nil && execution.Job.Mode != job.GOROUTINE {
|
|
t.Errorf("Expected GOROUTINE mode, got %v", execution.Job.Mode)
|
|
}
|
|
goroutineExecuted <- true
|
|
return nil
|
|
}
|
|
|
|
err = goroutineJob.Add(1, goroutineHandler)
|
|
if err != nil {
|
|
t.Fatalf("Failed to add goroutine handler: %v", err)
|
|
}
|
|
|
|
goroutineJob.SetWorkerManager(wm)
|
|
|
|
err = goroutineJob.Start()
|
|
if err != nil {
|
|
t.Fatalf("Failed to start goroutine job: %v", err)
|
|
}
|
|
|
|
// Test PROCESS mode
|
|
processJob, err := job.Once(job.PROCESS, map[string]interface{}{
|
|
"name": "Test Process Mode Job",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Failed to create process job: %v", err)
|
|
}
|
|
|
|
processExecuted := make(chan bool, 1)
|
|
processHandler := func(ctx context.Context, execution *job.Execution) error {
|
|
if execution.Job != nil && execution.Job.Mode != job.PROCESS {
|
|
t.Errorf("Expected PROCESS mode, got %v", execution.Job.Mode)
|
|
}
|
|
processExecuted <- true
|
|
return nil
|
|
}
|
|
|
|
err = processJob.Add(1, processHandler)
|
|
if err != nil {
|
|
t.Fatalf("Failed to add process handler: %v", err)
|
|
}
|
|
|
|
processJob.SetWorkerManager(wm)
|
|
|
|
err = processJob.Start()
|
|
if err != nil {
|
|
t.Fatalf("Failed to start process job: %v", err)
|
|
}
|
|
|
|
// Wait for both executions
|
|
timeout := time.After(10 * time.Second)
|
|
goroutineDone := false
|
|
processDone := false
|
|
|
|
for !goroutineDone || !processDone {
|
|
select {
|
|
case <-goroutineExecuted:
|
|
goroutineDone = true
|
|
t.Log("Goroutine job executed successfully")
|
|
case <-processExecuted:
|
|
processDone = true
|
|
t.Log("Process job executed successfully")
|
|
case <-timeout:
|
|
t.Error("Jobs execution timeout")
|
|
return
|
|
}
|
|
}
|
|
|
|
// Give extra time for database operations to complete
|
|
time.Sleep(500 * time.Millisecond)
|
|
}
|
|
|
|
// TestWorkerErrorHandling tests worker error handling
|
|
func TestWorkerErrorHandling(t *testing.T) {
|
|
test.Prepare(t, config.Conf)
|
|
defer test.Clean()
|
|
|
|
// Create test job
|
|
errorJob, err := job.Once(job.GOROUTINE, map[string]interface{}{
|
|
"name": "Test Error Handling Job",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Failed to create error job: %v", err)
|
|
}
|
|
|
|
// Error handler
|
|
errorHandler := func(ctx context.Context, execution *job.Execution) error {
|
|
execution.Error("Test error occurred")
|
|
return fmt.Errorf("intentional test error")
|
|
}
|
|
|
|
err = errorJob.Add(1, errorHandler)
|
|
if err != nil {
|
|
t.Fatalf("Failed to add error handler: %v", err)
|
|
}
|
|
|
|
wm := job.NewWorkerManagerForTest(4)
|
|
wm.Start()
|
|
defer wm.Stop()
|
|
errorJob.SetWorkerManager(wm)
|
|
|
|
err = errorJob.Start()
|
|
if err != nil {
|
|
t.Fatalf("Failed to start error job: %v", err)
|
|
}
|
|
|
|
// Wait for execution to complete
|
|
time.Sleep(2 * time.Second)
|
|
|
|
// Give extra time for database operations to complete
|
|
time.Sleep(500 * time.Millisecond)
|
|
|
|
// Check execution status
|
|
executions, err := errorJob.GetExecutions()
|
|
if err != nil {
|
|
t.Fatalf("Failed to get executions: %v", err)
|
|
}
|
|
if len(executions) == 0 {
|
|
t.Error("Expected at least one execution")
|
|
return
|
|
}
|
|
|
|
execution := executions[0]
|
|
if execution.Status != "failed" {
|
|
t.Errorf("Expected execution status 'failed', got '%s'", execution.Status)
|
|
}
|
|
|
|
if execution.ErrorInfo == nil {
|
|
t.Error("Expected error info to be set")
|
|
}
|
|
}
|
|
|
|
// TestWorkerConcurrency tests worker concurrency
|
|
func TestWorkerConcurrency(t *testing.T) {
|
|
test.Prepare(t, config.Conf)
|
|
defer test.Clean()
|
|
|
|
wm := job.NewWorkerManagerForTest(4)
|
|
wm.Start()
|
|
defer wm.Stop()
|
|
|
|
const numJobs = 5
|
|
executed := make(chan bool, numJobs)
|
|
|
|
// Create and submit multiple jobs concurrently
|
|
for i := 0; i < numJobs; i++ {
|
|
testJob, err := job.Once(job.GOROUTINE, map[string]interface{}{
|
|
"name": fmt.Sprintf("Concurrent Test Job %d", i+1),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Failed to create test job %d: %v", i+1, err)
|
|
}
|
|
|
|
jobID := i + 1
|
|
testHandler := func(ctx context.Context, execution *job.Execution) error {
|
|
execution.Info("Concurrent job %d executed", jobID)
|
|
time.Sleep(100 * time.Millisecond) // Simulate work
|
|
executed <- true
|
|
return nil
|
|
}
|
|
|
|
err = testJob.Add(1, testHandler)
|
|
if err != nil {
|
|
t.Fatalf("Failed to add handler for job %d: %v", i+1, err)
|
|
}
|
|
|
|
testJob.SetWorkerManager(wm)
|
|
|
|
err = testJob.Start()
|
|
if err != nil {
|
|
t.Fatalf("Failed to start job %d: %v", i+1, err)
|
|
}
|
|
}
|
|
|
|
// Wait for all jobs to complete
|
|
completed := 0
|
|
timeout := time.After(10 * time.Second)
|
|
|
|
for completed < numJobs {
|
|
select {
|
|
case <-executed:
|
|
completed++
|
|
t.Logf("Job %d completed", completed)
|
|
case <-timeout:
|
|
t.Errorf("Timeout: only %d out of %d jobs completed", completed, numJobs)
|
|
return
|
|
}
|
|
}
|
|
|
|
if completed != numJobs {
|
|
t.Errorf("Expected %d jobs to complete, got %d", numJobs, completed)
|
|
}
|
|
|
|
// Give extra time for database operations to complete
|
|
time.Sleep(500 * time.Millisecond)
|
|
}
|